From d1f220c1a2b8647b861caee4b2e08f29a7976a92 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 16 Oct 2026 05:10:03 -0400 Subject: [PATCH] [Refactor] (CMakeLists.txt, MG_Backend/DirectGLES): split Managers.cpp by object kind - helpers and state several files share lose `static`; ManagersInternal.h declares them --- CMakeLists.txt | 7 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 13115 +--------------- .../MG_Backend/DirectGLES/ManagersBuffers.cpp | 2949 ++++ .../DirectGLES/ManagersFramebuffers.cpp | 1479 ++ .../MG_Backend/DirectGLES/ManagersInternal.h | 658 + .../DirectGLES/ManagersPrograms.cpp | 2865 ++++ .../DirectGLES/ManagersSamplers.cpp | 271 + .../DirectGLES/ManagersTextureSync.cpp | 2361 +++ .../DirectGLES/ManagersTextures.cpp | 1605 ++ .../DirectGLES/ManagersVertexArrays.cpp | 1119 ++ 10 files changed, 13319 insertions(+), 13110 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersInternal.h create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b9e8e91..0993f930 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -514,6 +514,13 @@ set(SOURCE_FILES MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/Utils.cpp MobileGL/MG_Backend/DirectGLES/Managers.cpp + MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp + MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp + MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp + MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp + MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp + MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp + MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 41e7b8e1..8e2654ca 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -6,80 +6,11 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -#include "Managers.h" -#include "SubgroupPolicy.h" -#include -// P3a: the handle-shaped resource op table, the applier's records and the reverse channel. -// Both headers are compiled into the library only under push, so they are included here -// under the same condition - a pull build must gain no declaration it cannot link. -#include -#include -#include -// P5e (pg): the DEFINITION of the archive MGPipeShaderCsoRecord holds by SharedPtr. PipeApply.h -// needs only the name - the deleter is captured where the archive is constructed - but the -// program build reads through it, so the one translation unit that does needs the whole type. -#include -// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for -// why GLESBufferResource does not simply gain a member. -#include -#include -#if MOBILEGL_BUILD_DISAGGREGATED -#include -// P5c (ct): object_death's producer (CONTRACT-P5C.md §5.2) - the death notice's split arm -// emits the record through the client's emit helper instead of hopping a stack struct to the -// apply thread. -#include -// P11 B2 (T0): the client's AHardwareBuffer store - its reference counting and, on Android, the -// allocation the POST self-test makes. -#include -#if defined(__ANDROID__) -#include -#endif -#endif - -#include "Utils.h" -#include "DirectGLES.h" -#include "BackendObject_DirectGLES.h" -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "ContextEpoch.h" +#include "ManagersInternal.h" namespace MobileGL::MG_Backend::DirectGLES { Uint g_backendContextGeneration = 1; - - constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false; - constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance"; - constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID"; - constexpr const char* BASE_VERTEX_UNIFORM_NAME = "mg_BaseVertex"; constexpr const char* BASE_INSTANCE_LOWERED_NAME = "mg_BaseInstanceLowered"; - constexpr const char* BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME = "mg_BaseInstanceWordIndex"; - constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams"; constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID"; // See the block comment on ForEachViewportRoutingPass in Managers.h. Auto is ON, including on @@ -92,35 +23,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool g_anyProgramRoutesViewportIndex = false; - // 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; - } - - static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() { - // IsAngleLlvmpipeRenderer combined with the - // MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle, - // both resolved in FillInGLESCapabilities. - return g_GLESCapabilities.AvoidSamplerMipmapMinFilter; - } - - static Bool ShouldAvoidExplicitLodBiasOnAngleLlvmpipe() { - // IsAngleLlvmpipeRenderer combined with the MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS - // feature toggle, both resolved in FillInGLESCapabilities. - return g_GLESCapabilities.AvoidExplicitLodBias; - } - - static GLenum ResolveBackendMinFilter(const SamplerParameters& samplerParams, - Bool avoidMipmapMinFilter) { + GLenum ResolveBackendMinFilter(const SamplerParameters& samplerParams, Bool avoidMipmapMinFilter) { GLenum filter = MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.minFilter, samplerParams.mipmapMode); if (!avoidMipmapMinFilter) { @@ -138,40 +41,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so this is the floor MobileGL - // advertises whatever the ES driver reports. It steers ClampMultisampleFetchesForEssl - // AND is part of the L2 translation-memo key, so the transpile and the key must read - // the same constant - hence one definition rather than two locals. - // Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): this is - // backend code and must not reach into the GL frontend. 4 is that translation unit's - // kFrontendMaxSamples, which is the source of truth - keep the two in step. - constexpr Int kFrontendMaxSamples = 4; - - // Whether a usampler2DMS fetch's sample index is bounded at RUNTIME by the bound texture's - // real sample count rather than at compile time by the integer ceiling: armed when that - // ceiling is squeezed below what MobileGL advertises AND the depth ceiling - which the - // stencil aspect of a multisample depth/stencil texture is allocated under, and which a - // usampler reads too - exceeds it, so no one static bound is right for both. Read on both - // sides of the translation-memo boundary (the SPIR-V pass defers, the ESSL pass takes - // over); it is a function of three values the memo key already carries, so the two halves - // cannot disagree about one program. - static Bool ClampsUnsignedMultisampleFetchesAtRuntime() { - const Int advertisedMaxSamples = std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); - return g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples && - g_GLESCapabilities.MaxDepthTextureSamples > g_GLESCapabilities.MaxIntegerSamples; - } - - static Uint ResolveBackendEsslVersion() { - const auto& version = g_GLESCapabilities.GLESVersion; - if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) { - return 320; - } - if (version.Major == 3 && version.Minor >= 1) { - return 310; - } - return 300; - } - String ReplaceIdentifier(String source, const String& from, const String& to) { SizeT pos = 0; while ((pos = source.find(from, pos)) != String::npos) { @@ -207,6 +76,7 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace { Bool g_processTeardown = false; std::once_flag g_teardownSentinelOnce; + #if MOBILEGL_BUILD_DISAGGREGATED // P12: raised only while DropEveryTwinForEndedServerSession empties the twin tables, on the // apply thread that is the only reader of the twins (Managers.h). @@ -626,7 +496,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // one that needs no cross-stage channel. The cost is one varying slot, which a program // already at GL_MAX_VARYING_VECTORS cannot spare. constexpr const char* VIEWPORT_INDEX_VARYING_NAME = "mg_ViewportIndex"; - constexpr const char* VIEWPORT_PASS_MASK_UNIFORM_NAME = "mg_ViewportPassMask"; constexpr const char* VIEWPORT_GATED_ENTRY_POINT_NAME = "mg_ViewportGatedMain"; constexpr const char* ESSL_ENTRY_POINT_SIGNATURE = "void main()"; static_assert(RenderStateParameters::MAX_VIEWPORTS == 16, @@ -721,2187 +590,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - // The transpile pipeline invents image binding numbers: when the GL source declares - // an image uniform without layout(binding), glslang auto-assigns one (desktop GL - // allows that and lets the app pick the unit with glUniform1i, which ES forbids on - // image uniforms). The unit the app actually addresses lives in frontend state: the - // layout(binding) reflected at link time, or whatever glUniform1i stored afterwards. - // Rewrite every image uniform declaration to that unit so imageLoad/Store hits the - // unit the app bound with glBindImageTexture. - String RebindImageUniformsToFrontendUnits(String source, const PrgramImpl::ProgramBuildSource& src) { - // P5e (pg): the SOURCE, not the frontend object. The two reads below - the uniform's - // location and the unit glUniform1i put there - are exactly the pair the record's - // set_program_bindings tail carries, so on the handle arm this bakes the unit the client - // observed rather than one it would have had to dereference a live ProgramObject for. - // The null test the SharedPtr form carried moved to the caller, which returned early on - // a null program long before it reached here. - if (source.find("image") == String::npos) { - return source; - } - static const std::regex imageDeclRegex( - R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)"); - static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)"); - - String result; - result.reserve(source.size()); - SizeT lineStart = 0; - while (lineStart <= source.size()) { - const SizeT lineEnd = source.find('\n', lineStart); - const Bool lastLine = lineEnd == String::npos; - String line = source.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart); - - std::smatch match; - if (std::regex_search(line, match, imageDeclRegex)) { - const String name = match[3].str(); - Int location = src.GetUniformLocation(name); - if (location < 0) { - location = src.GetUniformLocation(name + "[0]"); - } - if (location >= 0) { - const Int unit = src.GetUniformSamplerOrImageUnitIndex(location); - if (unit >= 0) { - const String bindingText = "binding = " + std::to_string(unit); - if (std::regex_search(line, bindingValueRegex)) { - line = std::regex_replace(line, bindingValueRegex, bindingText); - } else if (match[1].matched) { - const SizeT layoutOpen = line.find('(', match.position(1)); - line.insert(layoutOpen + 1, bindingText + ", "); - } else { - line.insert(match.position(0), "layout(" + bindingText + ") "); - } - } - } - } - - result += line; - if (lastLine) { - break; - } - result += '\n'; - lineStart = lineEnd + 1; - } - return result; - } - namespace BufferImpl { - namespace { - using MG_State::GLState::BackendBufferResource; - using MG_State::GLState::BufferObject; - - // GL_ARRAY_BUFFER redundant-bind cache (id 0 = unknown/none). - Uint g_boundArrayBufferId = 0; - Bool g_boundArrayBufferKnown = false; - - // Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see - // Managers.h). Resting state between operations is 0; scopes in the - // readback/upload paths bind what they need through the cache and - // return to 0, so a stale user PBO can never capture a later - // readback that meant to target client memory. - Uint g_boundPixelPackBufferId = 0; - Bool g_boundPixelPackBufferKnown = false; - Uint g_boundPixelUnpackBufferId = 0; - Bool g_boundPixelUnpackBufferKnown = false; - - // Bumped whenever the backend ES context is destroyed; resources with - // an older generation hold ids from a dead context. - Uint g_bufferContextGeneration = 1; - - // Buffer-mutation epoch backing store (contract, mutation-site list and - // memory-ordering rules: Managers.h at the accessor declarations). - // Starts at 1 so the memo stamps' 0 means "never stamped". Atomic: - // frontend buffer ops may run on non-draw threads while the draw thread - // reads; the release-bump-AFTER-mutation / acquire-read-BEFORE-probes - // pairing makes a stamp taken against stale state impossible to consume. - std::atomic g_bufferMutationEpoch{1}; - - // Defined next to the indexed-binding shadow below; forward-declared so - // every glDeleteBuffers site in this namespace can scrub stale shadow - // entries (GL resets a deleted buffer's bindings - indexed and pixel - // pack/unpack alike - to 0, and a recycled name matching a stale shadow - // entry would otherwise false-skip the rebind). - void ScrubBufferBindingShadowsForId(Uint id); - - // Defined next to the same shadow, and the counterpart to the scrub above for a - // buffer whose STORE was re-specified rather than deleted: the binding survives - - // nothing unbound the id - but the extent the driver resolved for it at bind time - // does not. Marks those points unknown so the next sync issues a real - // glBindBufferBase/Range instead of skipping it. - void InvalidateIndexedBufferBindingShadowsForId(Uint id); - - // Resources whose owning BufferObject died; ids deleted at the next - // sync point with a current ES context. - Vector> g_deferredBufferReleases; - std::mutex g_deferredBufferReleasesMutex; - // Cheap emptiness probe so the per-draw drain can skip the mutex and - // context check when nothing was enqueued (the overwhelmingly common - // case). Written only under the mutex; read lock-free. - std::atomic g_hasDeferredBufferReleases{false}; - - // --- Buffer-storage pool (buffer-object recycling) ----------------------- - // Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers - // (which triggers the kgsl_sharedmem_free -> mmu_unmap -> smmu/power/bandwidth - // cascade that dominated per-frame driver cost). An id retired during frame N - // is handed back only once the GPU has completed frame N (fence watermark, see - // DirectGLES::CompletedFrameSerial), then reseeded in place with glBufferSubData - // (no glBufferData realloc). All GL access is on the ES-context-owning thread; - // the mutex only guards against off-thread deferred-release enrollment races. - struct PooledBuffer { - Uint id = 0; - SizeT size = 0; - Uint contextGeneration = 0; - Uint64 retireSerial = 0; - }; - UnorderedMap> g_bufferPool; - SizeT g_pooledBytes = 0; - std::mutex g_poolMutex; - constexpr SizeT kMaxPoolableBufferBytes = 8u * 1024u * 1024u; // bigger buffers: delete now - constexpr SizeT kMaxPoolBytes = 64u * 1024u * 1024u; // total pool budget - constexpr SizeT kMaxEntriesPerBucket = 32; - - Bool IsPoolable(const GLESBufferResource& r) { - // Require working fences: recycling is gated on the frame-completion - // watermark, which only advances if Present can insert/poll fences. - return g_GLESFuncs.glFenceSync != nullptr && g_GLESFuncs.glGetSynciv != nullptr && - r.id != 0 && !r.persistentMapped && !r.immutableStorage && - r.contextGeneration == g_bufferContextGeneration && r.storageInitialized && - r.storageSize > 0 && r.storageSize <= kMaxPoolableBufferBytes; - } - - // Retire a buffer id into the pool (owning thread; caller verified IsPoolable). - // Zeroes r.id to keep the single-owner invariant {live | deferred | pool}. - void EnrollIntoPool(GLESBufferResource& r) { - if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) { - InvalidateArrayBufferBindingCache(); - } - // Pooling keeps the id alive (and thus any driver binding of it); - // drop to unknown rather than claiming the post-delete 0 state. - if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) || - (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) { - InvalidatePixelBufferBindingCaches(); - } - const std::lock_guard lock(g_poolMutex); - auto& bucket = g_bufferPool[r.storageSize]; - if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) { - ScrubBufferBindingShadowsForId(r.id); - g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool - r.id = 0; - return; - } - // +1: Present increments the serial at frame END, so during the frame - // now being built CurrentFrameSerial() reads (frame-1). A buffer used - // this frame is only GPU-done once THIS frame's fence (serial+1) signals. - bucket.push_back( - {r.id, r.storageSize, r.contextGeneration, DirectGLES::CurrentFrameSerial() + 1}); - g_pooledBytes += r.storageSize; - r.id = 0; - } - - // Hand back an idle pooled id of EXACTLY `size` whose GPU work is complete, - // else 0. Owning thread only. Drops stale-generation entries encountered. - Uint AcquireFromPool(SizeT size) { - const Uint64 completed = DirectGLES::CompletedFrameSerial(); - const std::lock_guard lock(g_poolMutex); - auto it = g_bufferPool.find(size); - if (it == g_bufferPool.end()) return 0; - auto& bucket = it->second; - for (SizeT i = bucket.size(); i-- > 0;) { // newest-first: hottest + most-likely-idle - PooledBuffer& e = bucket[i]; - if (e.contextGeneration != g_bufferContextGeneration) { - g_pooledBytes -= e.size; // dead-context id: drop, no GL - bucket[i] = bucket.back(); - bucket.pop_back(); - continue; - } - if (e.retireSerial <= completed) { - const Uint id = e.id; - g_pooledBytes -= e.size; - bucket[i] = bucket.back(); - bucket.pop_back(); - return id; - } - } - return 0; - } - - // --- Persistent-mapped bump rings (see Managers.h) ----------------------- - // Shared machinery behind BOTH the global-UBO ring and the texture - // unpack-PBO ring: one EXT_buffer_storage persistent|coherent map per ring, - // monotonic head/tail cursors, and reclamation riding the Present() - // frame-fence watermark. The two rings differ only in size cap, offset - // alignment and log label - the reclamation, context-loss and - // emergency-drain rules are the part that was hard to get right, so they - // are shared rather than copied. - constexpr SizeT kUboRingInitialBytes = 4u * 1024u * 1024u; - constexpr SizeT kUboRingMaxBytes = 64u * 1024u * 1024u; - constexpr SizeT kUnpackRingInitialBytes = 4u * 1024u * 1024u; - constexpr SizeT kUnpackRingMaxBytes = 64u * 1024u * 1024u; - // A PBO-sourced glTexSubImage constrains the offset only to the pixel - // TYPE's size (GL_INVALID_OPERATION otherwise), and no ES client type is - // wider than 4 bytes - unlike a UBO bind, which owes the driver - // GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. 64 covers every type with room to - // spare and keeps consecutive staged blocks off each other's cache lines. - constexpr SizeT kUnpackRingAlignment = 64; - // glCopyBufferSubData carries no offset-alignment requirement at all; 64 - // keeps staged blocks cache-line separated, same as the unpack ring. - constexpr SizeT kUploadRingInitialBytes = 4u * 1024u * 1024u; - constexpr SizeT kUploadRingMaxBytes = 64u * 1024u * 1024u; - constexpr SizeT kUploadRingAlignment = 64; - - struct PersistentRingStore { - Uint id = 0; - Uint8* mappedPtr = nullptr; - SizeT size = 0; - // Monotonic linear cursors: `head` counts every byte ever allocated - // (incl. wrap padding); everything below `tail` is GPU-complete. Ring - // offset of a linear position is pos % size, so in-flight bytes are - // head - tail and must stay <= size. - Uint64 head = 0; - Uint64 tail = 0; - Uint32 generation = 0; // bumped on every (re)create/grow; 0 = never valid - Uint contextGeneration = 0; - SizeT alignment = 256; - // A hard storage-creation failure under this context; stop retrying - // per use (cleared when the context generation moves on). - Bool creationFailed = false; - }; - - // Grown-away ring stores: deletable only once the GPU finished the last - // frame that could reference them (same watermark as the buffer pool). - struct RetiredRingStore { - Uint id = 0; - Uint contextGeneration = 0; - Uint64 retireSerial = 0; - }; - - // Present()-time high-water marks: every byte below headAtPresent was - // written during frames <= frameSerial, so once frameSerial completes, - // tail may advance to headAtPresent. FIFO by construction. - struct RingFrameMark { - Uint64 frameSerial = 0; - Uint64 headAtPresent = 0; - }; - - // One ring: its live store, its two reclamation lists, and the immutable - // knobs that tell it apart from the other one. - struct PersistentRing { - PersistentRingStore store; - Vector retired; - Vector frameMarks; - SizeT initialBytes = 0; - SizeT maxBytes = 0; - // 0: take the offset alignment from GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT - // at store-creation time (the UBO ring's binds require it). - SizeT fixedAlignment = 0; - const char* label = ""; - }; - - // THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at - // Present, which say "everything this context wrote before its fence N is done once N is". - // One process-wide set used to be shared by every session's contexts: a fence from one - // context retired bytes another context's not-yet-executed copies still had to read - // (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's - // staging overwrote them - its pixels landed, at the wrong offsets, in the first one's - // textures. With one set per context, a context's bytes wait for that context's own - // fence. (The buffers themselves are shared objects; only who stages into them is split.) - // Staging ring for app buffer updates whose destination store may still be - // referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR - // hazard by BLOCKING in the call (osup_sync_object_wait) until every - // referencing job retires - under Minecraft 26.3's per-frame UBO and - // chunk-mesh SubData streams that serialized whole frames (~1 fps while - // chunks stream in). Staging the bytes here and issuing a - // glCopyBufferSubData instead keeps the hazard on the GPU timeline where it - // is just job ordering, and the CPU never waits. - struct RingSet { - PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"}; - PersistentRing unpack{{}, - {}, - {}, - kUnpackRingInitialBytes, - kUnpackRingMaxBytes, - kUnpackRingAlignment, - "Texture unpack ring"}; - PersistentRing upload{{}, - {}, - {}, - kUploadRingInitialBytes, - kUploadRingMaxBytes, - kUploadRingAlignment, - "Buffer upload ring"}; - RingSet(); - ~RingSet(); - RingSet(const RingSet&) = delete; - RingSet& operator=(const RingSet&) = delete; - }; - - // A dead context's ring buffers: shared objects, so they outlive it; deleted at the next - // Present of any context of the same share-group generation. - struct OrphanRingBuffer { - Uint id = 0; - Uint contextGeneration = 0; - }; - std::mutex g_orphanRingBuffersMutex; - Vector& OrphanRingBuffers() { - static auto* orphans = new Vector(); - return *orphans; - } - - // Each set's generations start in a range of their own: a per-program slot cache keyed - // by (generation, offset) must never take one context's ring for another's. - std::atomic g_ringGenerationBases{0}; - - RingSet::RingSet() { - const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12; - ubo.store.generation = base; - unpack.store.generation = base; - upload.store.generation = base; - } - - RingSet::~RingSet() { - const std::lock_guard lock(g_orphanRingBuffersMutex); - for (const PersistentRing* ring : {&ubo, &unpack, &upload}) { - if (ring->store.id != 0) { - OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration}); - } - for (const auto& retired : ring->retired) { - if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration}); - } - } - } - - DirectGLES::PerNativeContext g_ringSets; - PersistentRing& UboRing() { return g_ringSets.Current().ubo; } - PersistentRing& UnpackRing() { return g_ringSets.Current().unpack; } - PersistentRing& UploadRing() { return g_ringSets.Current().upload; } - - // The ES context the ring's id/map belonged to is gone (or was never - // seen): drop every handle without GL calls and re-arm creation. The - // generation counter must survive the reset — frame serials also survive - // context recreation, so a restarted counter could revalidate a stale - // per-program slot cache against the new ring. - void ResetRingForNewContext(PersistentRing& ring) { - const Uint32 keptGeneration = ring.store.generation; - ring.store = {}; - ring.store.generation = keptGeneration; - ring.store.contextGeneration = g_bufferContextGeneration; - // Keep the ring's own alignment across the wipe: the slow path rounds a - // request with it BEFORE the store that would set it exists. - if (ring.fixedAlignment != 0) ring.store.alignment = ring.fixedAlignment; - ring.retired.clear(); - ring.frameMarks.clear(); - } - - Bool CanTouchGLNow() { - return DirectGLES::IsBackendContextCurrentOnThisThread(); - } - - // P3a: ONE body per helper, parameterised on where its host bytes and its - // extent come from, so the legacy arm and the handle arm of a push build share - // the three-tier flush, the respecify and the range upload rather than owning a - // copy each - a second copy of the tier ladder is exactly how a tier changes in - // silence. The #else below is the PULL build, whose text must stay byte-identical - // to the pre-P3a one (G1: the pull build gains no symbol and resizes none); it is - // frozen by that gate and retires with the pull path at P13. - // (Re)specify backend storage from the shadow copy: glBufferData. - // The orphaning point - the ES driver performs the actual rename. - // TODO(buffer-pool Phase 2): orphan-on-respecify is NOT yet implemented. - // When the current id is BUSY (lastUseFrameSerial > CompletedFrameSerial()) - // && !persistentMapped && !noOrphan, express the orphan as an id-swap - // (retire the busy id into the pool, bind a fresh/pooled id) instead of the - // in-place glBufferData below, to avoid the driver's own rename/stall. Not - // pursued yet: glBufferData/glBufferSubData currently sit below profiler - // noise, so respecify is not a hot path in the profiled scenes. - // The body both arms run. `size`, `usage`, `initialData` and `syncedSerial` are the - // four things the legacy arm reads off the frontend object and the handle arm reads - // off the applier's stored descriptor and the shadow base the call carried; nothing - // else in here differs, so there is ONE glBufferData and one extent rule rather - // than two that can drift apart. - void RespecifyStorageWith(GLESBufferResource& resource, SizeT size, GLenum usage, - const void* initialData, Uint64 syncedSerial) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // Read BEFORE the fields below are overwritten: whether this respecify changes - // the store's EXTENT is what decides if the indexed-binding shadow still - // describes the driver. - const Bool extentChanged = !resource.storageInitialized || resource.storageSize != size; - BindBufferId(TempBufferTarget, resource.id); - g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage); - if (MG_Util::PipeStats::Enabled() && initialData != nullptr) { - // An ORPHANING respecify passes NULL and moves nothing, which is exactly - // why the test is on initialData rather than on size. - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - resource.storageSize = size; - resource.storageInitialized = true; - resource.pendingRespecify = false; - resource.pendingRanges.clear(); - resource.pendingResidentWrites.clear(); - resource.syncedChangeSerial = syncedSerial; - // A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a - // rebind whenever the shadow already records this id at that index - so on a - // driver that resolves a whole-buffer indexed binding's extent at BIND time - // (Adreno does; Mali does not) the shader keeps seeing the old, smaller range: - // stores past it are dropped and loads return zero. Forget what the shadow - // claims for this id so the next SyncBufferBindingPoints issues the bind for - // real. Only when the extent actually moved: an orphaning respecify at the same - // size is Minecraft's per-frame hot path and its bindings are still exact. - if (extentChanged) { - InvalidateIndexedBufferBindingShadowsForId(resource.id); - } - } - - void RespecifyStorageNow(GLESBufferResource& resource, BufferObject& bufferObject) { - const SizeT size = bufferObject.GetSize(); - // An orphaning respecify (glBufferData with NULL, content never - // written since) stays a pure NULL reallocation: the driver renames - // the store without a stall and nothing is transferred. Uploading - // the stale shadow here turned Minecraft-style orphaning into a - // full-size synchronized upload. - const void* initialData = - (size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr; - RespecifyStorageWith(resource, size, MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage()), - initialData, bufferObject.GetChangeSerial()); - } - - Bool StorageMatchesSize(const GLESBufferResource& resource, SizeT size) { - return resource.storageInitialized && !resource.pendingRespecify && resource.storageSize == size; - } - - Bool StorageMatches(const GLESBufferResource& resource, const BufferObject& bufferObject) { - return StorageMatchesSize(resource, bufferObject.GetSize()); - } - - // ------------------------------------------------------------------------------- - // R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Package v1. - // - // The rule, the evidence and the reason the storage is a side table rather than a - // member of GLESBufferResource are all in MG_Remote/Server/StagedShadow.h. This is - // only the instance and the four call sites. - // - // ONE PER PROCESS, and its copying arm is decided ONCE at first use: the two arms - // hold the authoritative bytes in DIFFERENT places, so an answer that changed - // mid-run would strand every resource already staged - the same reason - // ResolveResourceSubsystemArm latches (Managers.h). Leaked at exit like every other - // MG_Remote singleton (ID-8): ~BufferObject reaches the destroy path from exit - // handlers, after this TU's globals would already be gone. - // ------------------------------------------------------------------------------- - MG_Record::StagedShadowStore& ServerStaged() { - static MG_Record::StagedShadowStore& store = - *new MG_Record::StagedShadowStore( - MG_Config::Transport != MG_Config::TransportMode::Monolith); - return store; - } - - // THE COVERAGE RULE FOR THE THREE-TIER FLUSH DRAIN, ASSERTED BEFORE THE CALL AND NOT - // INSIDE IT. FlushPendingRangesFrom is a G5-pinned body (p3a_untouched_regions.sh's - // PINNED_FUNCTIONS, ID-41): the split arm CALLS it, it does not re-spell it, and it - // does not add a line to it either. Its tier-1 arm - the range-invalidating map - - // copies with its own Memcpy and never reaches UploadRangeFrom, so the one tier that - // DECLARES THE OLD BYTES DEAD is the one tier no later check can see; the only place - // left to say "every queued range is inside the staged coverage" is the instant before - // the drain takes the queue. The clamp is the drain's own (limit = the smaller of the - // frontend size and the backend store; bytes past either end have nowhere to land and - // are not this rule's subject), so the two agree about which bytes are meant. - // - // Fires only for a base that IS this resource's server shadow: RequireCoverage - // answers nothing for the legacy arm's MappedData(), which is valid for the whole - // store. `pendingMutex` is taken here and released before the drain takes it again. - void RequireStagedCoverageForPendingRanges(GLESBufferResource& resource, const Uint8* hostBase, - SizeT frontendSize, const char* site) { - if (hostBase == nullptr) return; - const SizeT limit = std::min(frontendSize, resource.storageSize); - const std::lock_guard lock(resource.pendingMutex); - for (const auto& range : resource.pendingRanges) { - const SizeT end = std::min(range.end, limit); - const SizeT start = std::min(range.start, end); - if (start == end) continue; - ServerStaged().RequireCoverage(&resource, hostBase, start, end, site); - } - } - -#if MOBILEGL_BUILD_DISAGGREGATED - // ------------------------------------------------------------------------------- - // P11 B2 (T0) - WHEN AN IMPORTED STORE'S AHardwareBuffer MAY BE LET GO. - // - // A T0 store is the client's AHardwareBuffer imported with glBufferStorageExternalEXT - // (Ops_H_ImportExternal below). The twin holds its own reference, and every path that - // ends the store - a respecify, a destroy, a re-import - deletes the GL id and then - // RETIRES the reference here: a fence is inserted after the delete, and the reference is - // released only once that fence has signalled, i.e. once every command that could still - // read or write the pages has completed on this context's GPU. The client's own - // reference ends independently (it releases on respecify/destroy); the pages live until - // the last of the two. No link watermark is involved (F-L2): the fence is this server's. - // - // Swept (non-blocking) at every import, every deferred-release drain and every ensure; - // a context loss releases the lot, because the GPU work those fences named ended with - // the context. Apply thread only, like every GL call here. - // ------------------------------------------------------------------------------- - struct T0RetiredImport { - GLsync fence = nullptr; - void* ahb = nullptr; - Uint generation = 0; - }; - Vector g_t0Retired; - // T0 twins whose destroy found no current context: Ops_OnDestroy queued them on the - // deferred-release list and keeps a second reference here, so the reference is - // retired only after that drain deleted the id (ProcessDeferredT0Retires). Guarded by - // g_deferredBufferReleasesMutex, like the list it shadows. - Vector> g_t0DeferredDestroys; - std::atomic g_hasT0DeferredDestroys{false}; - Uint64 g_t0ImportsLive = 0; - - void SweepT0Retired(Bool contextGone) { - for (SizeT i = 0; i < g_t0Retired.size();) { - T0RetiredImport& retired = g_t0Retired[i]; - const Bool sameContext = !contextGone && retired.generation == g_bufferContextGeneration && - CanTouchGLNow(); - Bool done = contextGone || retired.generation != g_bufferContextGeneration || - retired.fence == nullptr; - if (!done && sameContext && g_GLESFuncs.glClientWaitSync) { - const GLenum status = g_GLESFuncs.glClientWaitSync(retired.fence, 0, 0); - done = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED || - status == GL_WAIT_FAILED; - } - if (!done) { - ++i; - continue; - } - if (retired.fence != nullptr && sameContext && g_GLESFuncs.glDeleteSync) - g_GLESFuncs.glDeleteSync(retired.fence); - MG_Remote::Transport::AdoptT0::ReleaseImported(retired.ahb); - retired = g_t0Retired.back(); - g_t0Retired.pop_back(); - } - } - - // Call AFTER the store's GL id is deleted (or known dead): the fence then follows every - // command that used it. Clears the twin's reference whatever the context state. - void RetireT0Import(GLESBufferResource& resource) { - if (resource.externalAhb == nullptr) return; - T0RetiredImport retired; - retired.ahb = resource.externalAhb; - retired.generation = resource.contextGeneration; - if (CanTouchGLNow() && resource.contextGeneration == g_bufferContextGeneration && - g_GLESFuncs.glFenceSync) { - retired.fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - } - resource.externalAhb = nullptr; - if (g_t0ImportsLive > 0) --g_t0ImportsLive; - g_t0Retired.push_back(retired); - SweepT0Retired(false); - } -#else - // No transport, no imported store: nothing ever needs retiring (P13 W5). - inline void RetireT0Import(GLESBufferResource&) {} -#endif // MOBILEGL_BUILD_DISAGGREGATED - -// The four hostBytes sites read the same in both builds. The non-split expansion is the -// ORIGINAL EXPRESSION, character for character - `raw - offset` - so a push or verify build -// compiles exactly what it compiled before R-11 and the split arm is the only new behaviour. -#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \ - ServerStaged().Adopt(&(res), (width), (bytes), (offset), (size)) -#define MGL_SERVER_STAGED_DROP(res) ServerStaged().Drop(&(res)) -#define MGL_SERVER_STAGED_DROP_ALL() ServerStaged().DropAll() -#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) \ - ServerStaged().RequireCoverage(&(res), (base), (start), (end), (site)) -#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) \ - RequireStagedCoverageForPendingRanges((res), (base), (frontendSize), (site)) - - // The host bytes a range upload/flush reads. On the legacy arm the frontend object's - // shadow; on the handle arm the base the last content-carrying call handed over. - void UploadRangeFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (start >= end || hostBase == nullptr) return; - MGL_SERVER_STAGED_REQUIRE(resource, hostBase, start, end, "upload_range"); - BindBufferId(TempBufferTarget, resource.id); - g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start), - hostBase + start); - } - - void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) { - UploadRangeFrom(resource, bufferObject.MappedData(), start, end); - } - - // Ring machinery shared with the UBO/unpack rings; defined further down in - // this same unnamed namespace. - Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset); - Bool RingAvailable(PersistentRing& ring); - - // True when a pending-range flush can go through the staging ring right - // now: kill switch off, the ES copy entry point resolved, and the ring's - // own availability gate (EXT_buffer_storage + fences + live context) up. - Bool UploadRingUsableNow() { - if (MG_Config::Features.EsprytDisableUploadRing) return false; - if (!g_GLESFuncs.glCopyBufferSubData) return false; - return RingAvailable(UploadRing()); - } - - // A partial range below this goes through the staging ring instead of a - // range-invalidating map: the map's page-substitution fast path needs a - // sizeable (page-coverable) range to engage, and below it the driver - // falls back to waiting out the WAR hazard on the CPU. - constexpr SizeT kInvalidateRangeMinBytes = 128u * 1024u; - // The push arm's copy of this threshold lives in Managers.h, where a unit case can - // reach it (InvalidateFlushAccessFor). Two constants, one value, and the compiler - // is what keeps them one: the pull arm's FlushPendingRangesNow is byte-frozen - // against 5cb826b0 (ID-15), so the constant it reads may not move to a header. - static_assert(kInvalidateRangeMinBytes == kEsprytInvalidateRangeMinBytes, - "the two arms of the three-tier ladder must use the same tier-1 threshold"); - - // Push every queued range of `resource` from the shadow into the backend - // store, without ever letting a driver resolve the WAR hazard against - // in-flight frames at the WHOLE BUFFER's expense. Three tiers: - // - // 1. glMapBufferRange(WRITE | INVALIDATE_RANGE) + memcpy. The entire - // mapped range is rewritten from the authoritative shadow, so - // declaring its old bytes dead is exact - and it lets the driver - // swap fresh pages in for JUST that range. This is the only tier - // whose cost scales with the RANGE on this Mali driver: both the - // immediate glBufferSubData (pre-queueing) and a staged - // glCopyBufferSubData into a busy MUTABLE store ghost the whole - // destination with a worker-thread memcpy - Minecraft 26.3 streams - // ~1MB section meshes into 128MB arenas about nine times a frame - // during a camera pan, and 9 x 128MB of ghosting per frame is - // ~380ms, the measured 2-4 fps. (Backing the arenas with immutable - // stores also kills the ghost, but eagerly commits every arena's - // full extent - +hundreds of MB - which LMK'd the whole device.) - // 2. The staging ring + glCopyBufferSubData: the copy is ordered on - // the GPU timeline, no CPU wait (MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH - // forces this tier as the map path's negative control). - // 3. Direct glBufferSubData (potentially stalling) when neither the - // map entry points nor the ring exist. - // - // The ranges are flushed AS QUEUED (VecRange1D::Add already merges - // near-adjacent ones): bytes, not flush calls, are the cost axis here, - // and collapsing a scattered flush into its union re-copied nearly whole - // chunk-mesh arenas every frame. - // The caller owns syncedChangeSerial; this only drains the queue. - // - // ONE body for both arms, and deliberately so: the three-tier decision (whole-buffer - // orphan map / >= 128 KiB range-invalidating map / staged ring copy) is what the MC - // 26.3 p99 depends on, and a second copy of it for the handle arm is exactly how a - // tier silently changes. The arms differ only in where `hostBase` and `frontendSize` - // come from. - // - // R-11'S PARAMETER, AND IT IS A PARAMETER RATHER THAN AN ASSUMPTION (P5 b1). - // `hostBase` is re-read at every use today precisely because a shadow resize or an - // adoption moves what an earlier base pointed at. Under split the server may hold - // no pointer into the client's shadow at all, so the base becomes a SNAPSHOT taken - // into SEG_STAGE at emission - and a snapshot covers a RANGE, not the store. The - // two extra arguments say which range `hostBase` is good for; the default is the - // whole store, which is exactly what a live shadow is, so every caller today is - // byte-identical. The moment w1 passes a real snapshot extent, tier 1's widening - // refusal below stops being unreachable and a too-narrow snapshot is named instead - // of silently clobbering GPU-written bytes with stale ones. - constexpr SizeT kHostBaseCoversWholeStore = ~static_cast(0); - void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize, - SizeT hostBaseFrom = 0, - SizeT hostBaseTo = kHostBaseCoversWholeStore) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - VecRange1D ranges; - { - const std::lock_guard lock(resource.pendingMutex); - if (resource.pendingRanges.empty()) return; - ranges = std::move(resource.pendingRanges); - resource.pendingRanges.clear(); - } - // No shadow to read from: only reachable on the handle arm, where the base - // arrives with the call, and only for a resource that queued a range before any - // content-carrying call named one. The queue is already drained, so the next - // full re-upload is what puts the store right. - if (hostBase == nullptr) return; - // Clamp against BOTH extents: the readback flush may run while the - // frontend size and the backend store disagree (a pending respecify - // resolves that later; bytes past either end have nowhere to land). - const SizeT limit = std::min(frontendSize, resource.storageSize); - const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush && - g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer; - const Bool ringUsable = UploadRingUsableNow(); - for (const auto& range : ranges) { - const SizeT end = std::min(range.end, limit); - const SizeT start = std::min(range.start, end); - const SizeT size = end - start; - if (size == 0) continue; - if (MG_Util::PipeStats::Enabled()) { - // Counted once per queued range, before the three delivery shapes - // below diverge: all three move exactly these bytes, and it is the - // byte count - not the shape - that sizes SEG_STAGE. - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - // The invalidating map's fast path is SHAPE-dependent on this Mali - // driver: a whole-buffer invalidation renames the store outright, - // and a large range gets fresh pages - but a small unaligned range - // of a busy store makes the map WAIT (osup_sync_object_wait, ~9% - // of a Minecraft 26.3 replay). So: whole buffer -> orphan-map; - // large range -> range-invalidating map; small range -> the staged - // ring copy, whose worst case (a whole-destination ghost) is only - // ever the small destination itself. - // - // The map covers EXACTLY the queued range: only those bytes are the - // shadow's to rewrite. Widening to page bounds looked free and was - // not - the widened bytes clobbered GPU-written data (an SSBO - // counter beside the app's SubData) with the stale shadow. - // The extent the bytes behind `hostBase` are actually good for, clamped - // to the store. With the default it IS [start, end), so the refusal below - // cannot fire and the ladder is unchanged; with a real SEG_STAGE snapshot - // it is the snapshot's window and a disagreement drops this range to the - // staging ring instead of letting a widened INVALIDATE_RANGE declare bytes - // dead that nothing is about to rewrite. - const SizeT coveredFrom = hostBaseFrom > start ? hostBaseFrom : start; - const SizeT coveredTo = hostBaseTo < end ? hostBaseTo : end; -#if MOBILEGL_PIPE_VERIFY - if (coveredFrom != start || coveredTo != end) { - MGLOG_E_ONCE("MGPipe: Fatal{StageSnapshotTooNarrow} flush_pending_ranges: the " - "staged bytes cover [%zu, %zu) and the queued range is [%zu, %zu) " - "- tiers 2 and 3 would copy from outside the snapshot", - hostBaseFrom, hostBaseTo, start, end); - } -#endif - // Read per range, like the pull arm: a ring copy staged for an earlier range of - // this same drain puts every later partial range of the store behind it - // (InvalidateFlushAccessFor's queued-copy rule, dev f973008c). - const Bool ringCopyInFlight = resource.ringCopyRetireSerial > DirectGLES::CompletedFrameSerial(); - const GLbitfield access = - mapUsable ? InvalidateFlushAccessFor(start, end, coveredFrom, coveredTo, limit, - resource.storageSize, ringCopyInFlight) - : 0u; - if (access != 0) { - BindBufferId(TempBufferTarget, resource.id); - void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start, - (GLsizeiptr)size, access); - if (dst) { - Memcpy(dst, hostBase + start, size); - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - continue; - } - } - SizeT ringOffset = 0; - if (ringUsable && size <= kUploadRingMaxBytes && - RingAllocate(UploadRing(), size, ringOffset)) { - Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size); - BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); - BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); - g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, - (GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size); - // Commands recorded now retire with the fence the next Present inserts. - resource.ringCopyRetireSerial = DirectGLES::CurrentFrameSerial() + 1; - } else { - UploadRangeFrom(resource, hostBase, start, end); - } - } - } - - // THE TWO-ARM SHAPE, AND BOTH ARMS ARE HASHED FROM HERE ON (ID-15, which - // supersedes ID-13's "FlushPendingRangesNow is defined exactly once, outside any - // `#if`"). - // - // This file has TWO three-tier drains, and exactly one of them is compiled into any - // given build: `FlushPendingRangesFrom` above, inside `#if MOBILEGL_PIPE_PUSH`, is - // what a PUSH build runs - both of its call sites, the legacy one and - // Ops_H_Readback, reach it - and `FlushPendingRangesNow` inside the `#else` below, - // byte-identical to 5cb826b0, is what a PULL build runs. A push build compiles no - // FlushPendingRangesNow at all. - // - // NO FORWARDER HERE, which is what forced the two-arm shape: G5's extractor is - // preprocessor-blind, so a `FlushPendingRangesNow` that forwarded onto the ladder - // above would leave TWO definitions of that one name in the file and the gate would - // exit 2 - a gate that cannot run - rather than compare anything. (The handle arm - // cannot call FlushPendingRangesNow itself either: its signature takes a - // BufferObject&, and having no frontend object to offer is the whole point of the - // conversion.) - // - // WHAT ID-15 ADDS. The shape above is sound, but a gate keyed on the NAME - // `FlushPendingRangesNow` alone would from here on protect only the text the - // shipping build never compiles: a tier-threshold or map-access-bit edit made in - // FlushPendingRangesFrom - the ladder that actually runs, and the one MC 26.3's p99 - // depends on - would satisfy both G1 (the pull text did not move) and G5 (the pull - // name still hashes the same). So scripts/p3a_untouched_regions.sh hashes BOTH - // names: eleven functions, the pull ladder compared against the P3a base ref and - // this one against a sha pinned at 3e298c9a. Neither ladder may drift, and neither - // may drift AWAY FROM THE OTHER without the gate saying so. - - // P15: the small-store write rename (WholeStoreRenameApplies, Managers.h), applied to the - // queue BEFORE the drain takes it. The ladder above is pinned and is not touched: it is - // handed one whole-store range, which its own tier 1 already answers with the - // whole-buffer orphan-map. Draw path only - the readback drains keep their exact ranges. - void RenameSmallStoreForDrain(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize) { - const SizeT store = resource.storageSize; - const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush && - g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer; - const Bool foreignBytes = resource.serverGpuWritten || resource.persistentMapped || - resource.immutableStorage || resource.externalAhb != nullptr; - // The size gate first, with the whole store as the queue: it answers every refusal - // that does not depend on the queue before any lock is taken. - if (!WholeStoreRenameApplies(store, frontendSize, store, hostBase != nullptr, foreignBytes, mapUsable)) - return; - // Under split the host bytes are the server's staged copy, which only holds what the - // content records delivered; monolith's base is the frontend shadow, the whole store. - if (ServerStaged().CopiesIntoServerStorage() && !ServerStaged().IsCovered(&resource, 0, store)) return; - const std::lock_guard lock(resource.pendingMutex); - if (resource.pendingRanges.empty() || !resource.pendingResidentWrites.empty()) return; - SizeT queuedBytes = 0; - for (const auto& range : resource.pendingRanges) { - const SizeT end = std::min(range.end, store); - queuedBytes += end - std::min(range.start, end); - } - if (!WholeStoreRenameApplies(store, frontendSize, queuedBytes, true, false, true)) return; - resource.pendingRanges.clear(); - resource.pendingRanges.Add({0, store}); - } - - // Land the app bytes queued for an ADOPTED store on the GPU timeline: staged - // into the upload ring and delivered by glCopyBufferSubData. The destination - // is the IMMUTABLE persistent store, which the driver can neither rename nor - // ghost, so the copy is plain job ordering - after every in-flight reader, - // before the next consumer - which is exactly glBufferSubData's contract. - // (The in-place host write these bytes replaced tore the frames still - // reading the old vertex data: one-frame wrong geometry during fast camera - // movement.) Fallback: direct glBufferSubData - the adopted store carries - // DYNAMIC_STORAGE, and immutability again forbids the whole-store ghost. - // - // It never read the frontend object (the bytes are on the resource's own queue and - // the extent is the backend store's), so it takes none: the handle arm calls exactly - // this function with exactly these semantics. - void DrainResidentWritesNow(GLESBufferResource& resource) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - Vector writes; - { - const std::lock_guard lock(resource.pendingMutex); - if (resource.pendingResidentWrites.empty()) return; - writes = std::move(resource.pendingResidentWrites); - resource.pendingResidentWrites.clear(); - } - const SizeT limit = resource.storageSize; - const Bool ringUsable = UploadRingUsableNow(); - for (const auto& write : writes) { - if (write.offset >= limit) continue; - const SizeT size = std::min(write.bytes.size(), limit - write.offset); - if (size == 0) continue; - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - SizeT ringOffset = 0; - if (ringUsable && size <= kUploadRingMaxBytes && - RingAllocate(UploadRing(), size, ringOffset)) { - Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size); - BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); - BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); - g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, - (GLintptr)ringOffset, (GLintptr)write.offset, - (GLsizeiptr)size); - } else { - BindBufferId(TempBufferTarget, resource.id); - g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)write.offset, (GLsizeiptr)size, - write.bytes.data()); - } - } - } - - // The two-argument spelling the legacy arm's call sites use, kept so those sites - // are the SAME text in both builds (G1). The object was never read. - void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) { - (void)bufferObject; - DrainResidentWritesNow(resource); - } - - // EXT_buffer_storage bit values (same numeric values as the desktop ARB - // tokens); defined locally so this compiles regardless of which GLES headers - // expose the EXT tokens. - constexpr GLbitfield kMapPersistentBit = 0x0040; - constexpr GLbitfield kMapCoherentBit = 0x0080; - constexpr GLbitfield kDynamicStorageBit = 0x0100; - - void Ops_OnDestroy(SharedPtr&& resource) { - if (!resource) return; - auto* glesResource = static_cast(resource.get()); - if (glesResource->contextGeneration != g_bufferContextGeneration) { - glesResource->id = 0; // id belonged to a destroyed context - RetireT0Import(*glesResource); // P11 B2: its GL store died with that context - return; - } - if (CanTouchGLNow()) { - if (IsPoolable(*glesResource)) { - EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers - return; - } - if (glesResource->id != 0) { - if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { - InvalidateArrayBufferBindingCache(); - } - ScrubBufferBindingShadowsForId(glesResource->id); - g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); - glesResource->id = 0; - } - // P11 B2: a T0 store's AHardwareBuffer goes only after a fence behind the delete. - RetireT0Import(*glesResource); - return; - } - const std::lock_guard lock(g_deferredBufferReleasesMutex); -#if MOBILEGL_BUILD_DISAGGREGATED - if (glesResource->externalAhb != nullptr) { // P11 B2: retired after the drain deletes it - g_t0DeferredDestroys.push_back(resource); - g_hasT0DeferredDestroys.store(true, std::memory_order_release); - } -#endif - g_deferredBufferReleases.push_back(std::move(resource)); - g_hasDeferredBufferReleases.store(true, std::memory_order_release); - } - - // ---- P3a: the same seven ops plus the create/unmap pair, BY HANDLE ------------ - // - // Every body below is its Ops_* counterpart above with exactly the substitutions - // D-A2's table names and nothing else: the frontend object's GetSize() / GetUsage() - // / HasDefinedContent() / MappedData() / GetChangeSerial() become the applier - // record's Desc.Width / Desc.Usage / Desc.HasDefinedContent, the shadow base the - // call carried, and the record's server-owned Serial. Every branch survives - - // pendingRespecify early-out, the off-thread queue, the adopted zero-copy stamp, - // the upload-ring kill switch, the Mali WAR-stall queue-only default. - // - // NOTHING here reads a frontend type. The one thing a handle op cannot do is ask - // an object for its shadow, which is why GLESBufferResource::hostBytes exists: the - // three content-carrying calls hand the base over and the later drains read it. - - // The applier's record for this resource, or null when the client never created it - // (or created it into a slot that has since been recycled). - const MG_Pipe::MGPipeResourceRecord* ResourceRecordOf(MG_Pipe::MGPipeHandle res) { - if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; - const auto& records = MG_Pipe::MGPipeApplier().Resources; - if (res.Slot >= records.size()) return nullptr; - const auto& record = records[res.Slot]; - if (!record.Live || record.Gen != res.Gen) return nullptr; - return &record; - } - - // The server-owned MGGen this backend mirrors in syncedChangeSerial. Zero for a - // resource with no record, which is the same "never synced" answer a frontend - // change serial of zero gave. - Uint64 ResourceSerialOf(MG_Pipe::MGPipeHandle res) { - const auto* record = ResourceRecordOf(res); - return record != nullptr ? record->Serial : 0; - } - SizeT ResourceWidthOf(MG_Pipe::MGPipeHandle res) { - const auto* record = ResourceRecordOf(res); - return record != nullptr ? static_cast(record->Desc.Width) : 0; - } - // ResourceContentIsDeclared MOVED OUT OF THIS ANONYMOUS NAMESPACE (P3b/P4b espryt - // D1 slice 1) to sit beside RequireStagedCoverage at namespace scope: the XFB - // scatter in DirectGLES.cpp has to ask the same M-3 question this file's pool-reuse - // ladder asks, and the two must not be able to answer it differently. - - void Ops_H_Create(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) { - (void)res; - (void)desc; - // Nothing, and that is the row D-A2 writes: storage is defined lazily by the - // first resource_respecify, and the ensure path already tolerates a resource - // with none. Minting the twin here would only move the allocation earlier. - } - - void Ops_H_Respecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, - const void* initialBytes) { - auto* resource = FindBufferResourceForHandle(res); - // P13 W4a (§1.4): ON MONOLITH'S RECORD ARM THE TWIN IS MINTED HERE, not lazily at - // the first draw. The record arm's readers have no frontend object to re-read a - // base from, so the base this call carries - the client's shadow, for an orphaning - // respecify too (PipeFill passes it there on this arm) - is the only way they learn - // it. Minting is GL-free; the storage stays lazy. - const Bool aliasesFrontend = MG_Config::RecordArmAliasesFrontend(); - if (resource == nullptr && aliasesFrontend && initialBytes != nullptr) { - resource = GetOrCreateBufferResourceForHandle(res); - } - if (resource != nullptr) { - // A respecify's companion pointer IS the shadow base (offset 0) - and an - // ORPHANING one (glBufferData with NULL) carries HasDefinedContent clear and - // therefore no pointer at all. That is also the call that RESIZES the - // client's shadow (PipeResource::ResizeShadow is reserve + resize, so a grow - // past the reserve reallocates and frees the block a previous call's base - // pointed into), so the recorded base is dead exactly when no new one - // arrives. FORGET it here rather than leave a pointer nothing refreshes: - // every reader below treats a null base as "no bytes to move", which is the - // honest answer, and the ensure path re-reads the live base from the - // frontend object it still holds. - // P13 W4a: an ALIASING store records the base whether or not the content is - // defined - the pointer is the frontend shadow's new allocation either way, and - // the upload below stays gated on the descriptor (RespecifyStorageWith's - // initialData), so an orphan still uploads nothing. - if ((desc.HasDefinedContent != 0 || aliasesFrontend) && initialBytes != nullptr) { - // R-11: in monolith this is the client's shadow base, unchanged; under - // split it is copied into server-owned storage first. A respecify's - // companion pointer is the base at offset 0 and covers the whole store. - // Under split it is ALWAYS null (contract table 1 row 19 - - // initialBytes does not cross, and the content arrives as - // resource_subdata records right behind this record), so in practice - // this arm is the monolith one and the else arm is the split one. - resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, static_cast(desc.Width), - initialBytes, 0, - static_cast(desc.Width)); - } else { - MGL_SERVER_STAGED_DROP(*resource); - resource->hostBytes = nullptr; - } - // P8-C: whatever a shader wrote belonged to the store this call replaces. - resource->serverGpuWritten = false; - resource->contentSinceRespecify = false; - } - if (!resource) return; // lazy: the ensure path full-uploads on creation - if (resource->immutableStorage) { - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - if (resource->id != 0 && CanTouchGLNow() && - resource->contextGeneration == g_bufferContextGeneration) { - NoteBufferIdDeleted(resource->id); - // Frontend VAO bindings survive respecification; force their - // backend twins to bind the replacement buffer name. (dev@d7655247, - // carried into this arm as well: the handle arm duplicates the - // immediate retire path, so a fix that lands in only one of the two - // leaves the bug alive on whichever arm the operator selects.) - ++g_bufferBackendIdGeneration; - g_GLESFuncs.glDeleteBuffers(1, &resource->id); - resource->id = 0; - resource->immutableStorage = false; - } - // P11 B2: a respecified T0 store ends here on the server's side too. Its - // AHardwareBuffer is released only after a fence behind the delete (or at once - // when the store's context is already gone) - never under a frame in flight, - // however the client's own release (at this respecify) raced it. - RetireT0Import(*resource); - resource->storageInitialized = false; - resource->storageSize = 0; - resource->pendingRespecify = true; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - return; - } - if (!CanTouchGLNow() || resource->id == 0 || - resource->contextGeneration != g_bufferContextGeneration) { - resource->pendingRespecify = true; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - return; - } - if (desc.Width == 0) { - resource->storageInitialized = false; - resource->storageSize = 0; - resource->pendingRespecify = false; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - return; - } - // Same orphaning rule, expressed on the descriptor: a NULL-data respecify has - // HasDefinedContent clear and the client sends no bytes for it. - const void* initialData = desc.HasDefinedContent != 0 ? initialBytes : nullptr; - RespecifyStorageWith(*resource, static_cast(desc.Width), - MG_Util::ConvertBufferUsageToGLEnum(static_cast(desc.Usage)), - initialData, ResourceSerialOf(res)); - } - - void Ops_H_SubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, const void* bytes) { - const SizeT offset = static_cast(MG_Pipe::MGPipeSubDataBufferOffset(record)); - const SizeT size = static_cast(MG_Pipe::MGPipeSubDataBufferSize(record)); - auto* resource = FindBufferResourceForHandle(res); - // R-11 / ID-52 item 3: under an ACTIVE TRANSPORT this record's bytes are the ONLY - // delivery of the buffer's content - the client sends no companion pointer and the - // server has no MappedData to fall back on. But the twin is created LAZILY at the - // first draw (Ops_H_Create is a no-op by D-A2), which is AFTER this record, so a - // subdata that finds no twin would return here and the bytes would be lost - the - // draw then uploads an empty/shifted store (TriangleScenario read a blue triangle - // before this). So under split the subdata MINTS the twin it needs to stage into. - // Monolith is untouched: the twin stays lazy there because the frontend object's - // MappedData is the source and nothing is lost by deferring the allocation (D-A2). - if (resource == nullptr && bytes != nullptr) { - resource = GetOrCreateBufferResourceForHandle(res); - } - if (!resource) return; - // P11 B2: A T0 STORE TAKES A SERVER-ORIGINATED WRITE GPU-ORDERED. Its pages are the - // client's AHardwareBuffer, so there is no staged copy to refresh and the adopted - // early return below ("the coherent map already has the bytes") is false for bytes - // the SERVER produced - P9 W1's pack-buffer landing (PipeApplier.cpp - // LandReadbackInBuffer) is the one producer, and it would lose its pixels there. They - // are queued exactly as a resident SubData is (Ops_H_ResidentSubData): landed after - // every command already recorded, before the next one that reads the store. - if (resource->externalAhb != nullptr && bytes != nullptr && size != 0) { - { - const std::lock_guard lock(resource->pendingMutex); - auto& write = resource->pendingResidentWrites.emplace_back(); - write.offset = offset; - const auto* source = static_cast(bytes); - write.bytes.assign(source, source + size); - } - resource->syncedChangeSerial = ResourceSerialOf(res); - return; - } - // P13 W4b: A SERVER-ORIGINATED WRITE INTO AN ALIASING STORE (monolith's record arm) - // lands in the GL store, never in the base: the bytes are the sink's scratch, and the - // base is the frontend's shadow, which this side does not write. Queued uploads land - // first, so they cannot later overwrite these bytes from the stale shadow; the store - // is then marked GPU-written, which sends the frontend (its own pack-buffer mark) and - // every CPU reader here (SplitHostBytesForCpuRead) back to the GL store for them. - if (MG_Pipe::MGPipeApplyingServerOriginatedWrite() && MG_Config::RecordArmAliasesFrontend() && - bytes != nullptr && size != 0) { - // The store must exist to take them: a pack buffer is often never drawn from, so - // nothing else has built it. Only a store that does not exist yet is ensured: the - // applier has already counted THIS write into the record's serial, so the ensure - // of a built store would read it as an untracked mutation and re-upload the - // shadow over the runs this landing wrote before it. - if (CanTouchGLNow() && (resource->id == 0 || !resource->storageInitialized || - resource->pendingRespecify || - resource->contextGeneration != g_bufferContextGeneration)) { - resource = EnsureBufferResourceForHandle(nullptr, res); - } - if (resource != nullptr && CanTouchGLNow() && resource->id != 0 && resource->storageInitialized && - !resource->persistentMapped && !resource->pendingRespecify && - resource->contextGeneration == g_bufferContextGeneration && - StorageMatchesSize(*resource, ResourceWidthOf(res))) { - FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res)); - BindBufferId(TempBufferTarget, resource->id); - g_GLESFuncs.glBufferSubData(TempBufferTarget, static_cast(offset), - static_cast(size), bytes); - // The store holds everything the record now says (the applier bumped its - // serial for this write): without the stamp the next ensure would see a - // newer serial and re-upload the shadow over the bytes that just landed. - resource->syncedChangeSerial = ResourceSerialOf(res); - } else if (resource != nullptr) { - const std::lock_guard lock(resource->pendingMutex); - auto& write = resource->pendingResidentWrites.emplace_back(); - write.offset = offset; - const auto* source = static_cast(bytes); - write.bytes.assign(source, source + size); - } - if (resource != nullptr) resource->serverGpuWritten = true; - return; - } - // M-2: UNDER pendingMutex, because this line runs BEFORE the CanTouchGLNow() - // test below - i.e. on the arm D-A2 deliberately keeps reachable off the render - // thread - while every reader of hostBytes (Ops_H_Readback's drain, the - // kill-switch map arm of Ops_H_FlushRange, the ensure path, the fp64 narrowing) - // is on the render thread. It was the one member of this struct that the - // off-thread path touched with neither a lock nor an atomic: pendingRanges and - // pendingResidentWrites are under this mutex and syncedChangeSerial is an - // std::atomic read with acquire. The lock it is put under is deliberately THAT - // one and not a new one - the base and the queued range are one fact ("these - // bytes, at this base"), the drain takes this mutex to lift the ranges, and a - // drain that sees a range therefore sees the base that range was queued - // against. The store is kept on both arms rather than restricted to the - // CanTouchGLNow() one because Ops_H_Readback can drain with no draw in between, - // and the ensure path's republication (the 3c55e027 fix) is what runs at a draw. - if (bytes != nullptr) { - const std::lock_guard lock(resource->pendingMutex); - // R-11: MONOLITH keeps the client's base; SPLIT copies [offset, offset+size) - // into server-owned storage and points hostBytes at that. Inside the SAME - // lock as the queued range, because the base and the range are one fact - // ("these bytes, at this base") and the drain takes this mutex to lift them. - resource->hostBytes = - MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size); - if (size != 0) resource->contentSinceRespecify = true; - } - if (resource->pendingRespecify) return; // full re-upload pending anyway - if (!CanTouchGLNow() || resource->id == 0 || - resource->contextGeneration != g_bufferContextGeneration || - !StorageMatchesSize(*resource, ResourceWidthOf(res))) { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add({offset, offset + size}); - return; - } - // The adopted zero-copy store already HAS the bytes; a driver upload here would - // re-synchronize what coherent mapping made free. - if (resource->persistentMapped && resource->persistentPtr) { - resource->syncedChangeSerial = ResourceSerialOf(res); - return; - } - if (MG_Config::Features.EsprytDisableUploadRing) { - UploadRangeFrom(*resource, resource->hostBytes, offset, offset + size); - resource->syncedChangeSerial = ResourceSerialOf(res); - return; - } - // The Mali WAR-stall fix, unchanged: queue and let draw-time sync stage the - // merged ranges through the upload ring. - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add({offset, offset + size}); - } - - void Ops_H_ResidentSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, - const void* bytes) { - const SizeT offset = static_cast(MG_Pipe::MGPipeSubDataBufferOffset(record)); - const SizeT size = static_cast(MG_Pipe::MGPipeSubDataBufferSize(record)); - auto* resource = FindBufferResourceForHandle(res); - if (!resource || size == 0 || bytes == nullptr) return; - // DELIBERATELY not stored as hostBytes: these are the application's staging - // store and are valid for the duration of the call only (BufferObject.h:84-85), - // which is exactly why they are COPIED here rather than referenced later. - const std::lock_guard lock(resource->pendingMutex); - auto& write = resource->pendingResidentWrites.emplace_back(); - write.offset = offset; - const auto* source = static_cast(bytes); - write.bytes.assign(source, source + size); - } - - void Ops_H_FlushRange(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record, - const void* bytes) { - auto* resource = FindBufferResourceForHandle(res); - if (!resource) return; - const SizeT start = static_cast(record.Offset); - const SizeT end = start + static_cast(record.Size); - // M-2, the second store site - same lock, same reason as Ops_H_SubData's, and - // the same R-11 copy. UNDER SPLIT `bytes` IS ALWAYS NULL HERE by ruling (C-6 / - // contract table 1 row 20: resource_flush_range carries no bytes at all - the - // ladder it drives rewrites its range from the authoritative shadow, which rule - // C makes server-owned, so resource_subdata is already the only way bytes reach - // it and a second carrier would be a forgeable way to say the same thing). So - // this arm keeps whatever the preceding subdata records staged, which is - // exactly what the ladder must read. - if (bytes != nullptr) { - const std::lock_guard lock(resource->pendingMutex); - resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, - start, end - start); - if (end > start) resource->contentSinceRespecify = true; - } - if (resource->pendingRespecify) return; - if (!CanTouchGLNow() || resource->id == 0 || - resource->contextGeneration != g_bufferContextGeneration || - !StorageMatchesSize(*resource, ResourceWidthOf(res))) { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add({start, end}); - return; - } - if (resource->persistentMapped && resource->persistentPtr) { - resource->syncedChangeSerial = ResourceSerialOf(res); - return; - } - if (!MG_Config::Features.EsprytDisableUploadRing) { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add({start, end}); - return; - } - // The kill-switch arm, and it reads the application's REAL flags per call - - // which is why MGPFlushRange carries them unnormalised. - const Flags appAccess{ - static_cast>(record.AccessFlags)}; - const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) || - (appAccess & BufferMappingAccessBit::InvalidateBuffer); - const Bool unsynchronized = static_cast(appAccess & BufferMappingAccessBit::Unsynchronized); - if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized) && - resource->hostBytes != nullptr) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // The kill-switch arm's own Memcpy, and the one that passes - // GL_MAP_INVALIDATE_RANGE_BIT - i.e. the one that tells the driver the old - // bytes are dead. Bytes outside the staged coverage are exactly the ones - // that claim is false for. - MGL_SERVER_STAGED_REQUIRE(*resource, resource->hostBytes, start, end, - "flush_range_invalidate_map"); - BindBufferId(TempBufferTarget, resource->id); - void* mappedData = g_GLESFuncs.glMapBufferRange( - TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start), - GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) | - (unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0)); - if (mappedData) { - Memcpy(mappedData, resource->hostBytes + start, end - start); - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - resource->syncedChangeSerial = ResourceSerialOf(res); - return; - } - MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData", - resource->id); - } - UploadRangeFrom(*resource, resource->hostBytes, start, end); - resource->syncedChangeSerial = ResourceSerialOf(res); - } - - void Ops_H_Readback(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) { - // An empty capture may conservatively request a writeback before - // any draw materialized this buffer. Resolve its staged resource - // exactly as a GPU consumer would, then return the real bytes. - auto* resource = EnsureBufferResourceForHandle(nullptr, res); - if (!resource || resource->id == 0 || !resource->storageInitialized) return; - if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return; - if (resource->persistentMapped) { - // Queued resident SubData bytes land first (GPU-ordered), then the finish - // makes them - and any shader writes already queued on this context - - // visible through the coherent mapping the reads use. There is no backend - // copy to read back in this case. - DrainResidentWritesNow(*resource); - if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); - return; - } - if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return; - // The map below starts at record.Offset where the legacy arm mapped at literal - // zero, so the clamp has to be against what is left of the BACKEND store past - // that offset: the applier's BufferRangeFault checked the range against - // Desc.Width, and a store that is smaller than the descriptor (a respecify the - // backend has not applied yet) would otherwise be mapped past its end. - const SizeT readOffset = static_cast(record.Offset); - if (readOffset >= resource->storageSize) return; - const SizeT size = - std::min(static_cast(record.Size), resource->storageSize - readOffset); - if (size == 0) return; - - // Queued app writes must land in the backend store before it is read back, or - // the writeback below would revert them in the shadow. Under split every queued - // range must lie inside the server shadow's staged coverage first (R-11 / the - // M-6 ruling): the drain's tier-1 map would otherwise declare live GPU bytes - // dead over a range nothing staged. - MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, resource->hostBytes, ResourceWidthOf(res), - "readback_flush_pending"); - FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res)); - - BindBufferId(TempBufferTarget, resource->id); - void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)record.Offset, - static_cast(size), GL_MAP_READ_BIT); - if (mapped == nullptr) { - MGLOG_E_ONCE("Ops_H_Readback: glMapBufferRange(read) failed for buffer %u", resource->id); - return; - } - // The reverse channel replaces BufferObject::WritebackFromBackend: the backend - // no longer reaches into the frontend's address space, it ANSWERS. In monolith - // the client's implementation is one call away, so SyncGpuWrites' caller still - // sees the reconciled shadow on return, exactly as before. - // - // THE ORDER FROM HERE IS A CORRECTNESS RULE, NOT A PREFERENCE - // (ARCHITECTURE.md 8.2: "写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 - // server 应用;反向通道需要与正向通道相同的有序保证"): - // 1. writeback - the client's shadow takes the bytes, - // 2. unmap - the read mapping goes away, - // 3. serial stamp- syncedChangeSerial catches up with the record, so the next - // draw does not re-upload the readback over itself, - // 4. epoch bump - in Ops_H_ReadbackTracked, i.e. strictly AFTER 1-3, so a - // draw-clean memo re-probed by the bump can never observe a - // half-reconciled resource. The bump must NEVER move earlier. - if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) { - MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( - res, record.Offset, - MG_Pipe::MGPBlobRef{reinterpret_cast(mapped), static_cast(size), - MG_Pipe::kMGHostSpanSegNone, 0}); - } else { - MGLOG_E_ONCE("Ops_H_Readback: no reverse channel is installed, so the GPU-written bytes of " - "buffer %u cannot reach the client shadow", - resource->id); - } - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - // The shadow now matches the backend byte for byte; without this the next draw - // would see a newer serial and re-upload the readback over it. - resource->syncedChangeSerial = ResourceSerialOf(res); - // P13 W4a: on monolith's record arm the base this twin reads IS the shadow the - // writeback just filled, so a whole-store readback is also the refresh - // SplitHostBytesForCpuRead would owe. (A copying store's copy is not the - // client's shadow, and keeps its mark.) - if (MG_Config::RecordArmAliasesFrontend() && readOffset == 0 && size >= ResourceWidthOf(res)) { - resource->serverGpuWritten = false; - } - } - - void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) { - // R-11's server copy dies with the resource, and BEFORE the twin leaves the - // table - the side map is keyed by the twin's address, so this is the last - // moment that address can be looked up. - if (GLESBufferResource* dying = FindBufferResourceForHandle(res); dying != nullptr) { - MGL_SERVER_STAGED_DROP(*dying); - } - // The twin comes OUT of the table first, so the three outcomes below are - // reached with the entry already retired. The SLOT is the client's to free, - // after this returns (D-L). - SharedPtr twin = g_backendBufferResources.ReleaseByHandle(res); - // Verbatim Ops_OnDestroy: stale generation -> zero the id; on-thread -> - // IsPoolable/EnrollIntoPool else scrub + glDeleteBuffers; off-thread -> - // g_deferredBufferReleases plus the lock-free flag. - Ops_OnDestroy(std::move(twin)); - } - - // D-E: the ONLY two changes are the signature and the three reads that went - // through the frontend object (GetSize() -> size, MappedData() -> seedBytes, - // SetBackendResource(...) -> the slot table's GetOrCreate). Every other statement - // is byte-identical to Ops_AcquirePersistentMap above - the four-way capability - // gate, the stale-generation wipe BEFORE the new stamp, the idempotency hit, the - // fresh-id sequence with ++g_bufferBackendIdGeneration, immutableStorage set as - // soon as the store exists, the MGLOG_E_ONCE decline and the success stamps. - void* Ops_H_MapPersistent(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) { - if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || - !g_GLESFuncs.glGenBuffers) { - return nullptr; - } - if (size == 0) return nullptr; - - auto* resource = GetOrCreateBufferResourceForHandle(res); - if (!resource) return nullptr; - // Before the generation is stamped, not after: everything on the resource - // describes a context that is gone, and the idempotency check below would - // otherwise hand the caller the dead context's mapped pointer. - if (resource->contextGeneration != g_bufferContextGeneration) { - resource->id = 0; - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - resource->immutableStorage = false; - resource->storageInitialized = false; - resource->storageSize = 0; - } - resource->contextGeneration = g_bufferContextGeneration; - - if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) { - return resource->persistentPtr; // idempotent - } - - // Need a fresh id: glBufferStorage fails on a buffer that already has - // immutable storage, and any prior mutable store is replaced anyway. - if (resource->id != 0) { - NoteBufferIdDeleted(resource->id); - // Driver VAOs may have this id baked into attribute/element bindings - // keyed on versions this re-mint does not move. - ++g_bufferBackendIdGeneration; - g_GLESFuncs.glDeleteBuffers(1, &resource->id); - resource->id = 0; - resource->immutableStorage = false; - } - g_GLESFuncs.glGenBuffers(1, &resource->id); - if (resource->id == 0) return nullptr; - - // Seed from the shadow (the client's bytes are still live at this point: it - // adopts and drops them only after this returns). - BindBufferId(TempBufferTarget, resource->id); - const void* initial = seedBytes; - g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(size), initial, - GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit | - kDynamicStorageBit); - // Set as soon as the store exists, not once the map succeeds: the failure - // path below leaves this id holding immutable storage, and whoever touches - // it next has to know that glBufferData cannot redefine it. - resource->immutableStorage = true; - void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), - GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); - if (!ptr) { - MGLOG_E_ONCE("Ops_H_MapPersistent: glMapBufferRange(persistent) failed for buffer %u", - resource->id); - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - return nullptr; - } - resource->persistentPtr = ptr; - resource->persistentMapped = true; - // The ONE statement here that is not in Ops_AcquirePersistentMap, and it is - // about a member the legacy arm does not have rather than about the map: the - // client adopts this pointer the instant we return it and - // PipeResource::AdoptPersistentMap then does m_shadow->clear() + - // shrink_to_fit(), so the shadow base any earlier content-carrying call - // recorded is a FREED allocation from here on. The live bytes are persistentPtr. - // R-11's server copy goes with it: the bytes are the coherent map's now, and a - // stale server shadow would answer a later drain with pre-map content. - MGL_SERVER_STAGED_DROP(*resource); - resource->hostBytes = nullptr; - resource->storageSize = static_cast(size); - resource->storageInitialized = true; - resource->pendingRespecify = false; - { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - } - resource->syncedChangeSerial = ResourceSerialOf(res); - return ptr; - } - - void Ops_H_UnmapPersistent(MG_Pipe::MGPipeHandle res) { - (void)res; - // P3a emits this from nowhere: the donation is permanent for the life of the - // store and is ended by the respecify / destroy paths, which already retire - // the immutable id. The entry exists so the transport has both halves of the - // pair (D-E), and giving it a body that unmapped a live coherent store would - // be the one thing D-B4 forbids. - } - - // The epoch-tracking wrappers, duplicated for this table - same contract as the - // seven above, including AcquirePersistentMap's bump EVEN ON DECLINE (the client - // still enters a persistent map the per-draw probes must start seeing). - void Ops_H_RespecifyTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, - const void* initialBytes) { - Ops_H_Respecify(res, desc, initialBytes); - BumpBufferMutationEpoch(); - } - void Ops_H_SubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, - const void* bytes) { - Ops_H_SubData(res, record, bytes); - BumpBufferMutationEpoch(); - } - void Ops_H_ResidentSubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, - const void* bytes) { - Ops_H_ResidentSubData(res, record, bytes); - BumpBufferMutationEpoch(); - } - void Ops_H_FlushRangeTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record, - const void* bytes) { - Ops_H_FlushRange(res, record, bytes); - BumpBufferMutationEpoch(); - } - void Ops_H_ReadbackTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) { - Ops_H_Readback(res, record); - BumpBufferMutationEpoch(); - } - void Ops_H_DestroyTracked(MG_Pipe::MGPipeHandle res) { - Ops_H_Destroy(res); - BumpBufferMutationEpoch(); - } - void* Ops_H_MapPersistentTracked(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) { - void* result = Ops_H_MapPersistent(res, size, seedBytes); - // Bump even on decline: the client still enters a persistent map the per-draw - // probes must start seeing. - BumpBufferMutationEpoch(); - return result; - } - -#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) - // ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A STORE'S RESIDENT TWIN ------------ - // - // The split twin of Ops_H_MapPersistent: where a monolith map_persistent mints a - // coherent persistent store (glBufferStorageEXT) and hands its pointer to the frontend, - // a T0 map_persistent IMPORTS the pages the client already holds and maps - - // glBufferStorageExternalEXT on eglGetNativeClientBufferANDROID(ahb) - and the result - // is the SAME resource shape (immutable, persistentMapped, persistentPtr = this - // server's coherent map of those pages), so every adopted-store path in this file - // applies unchanged: draws bind it with nothing to upload, resident SubData lands - // GPU-ordered (DrainResidentWritesNow), a readback drains and waits and posts no - // bytes. The R-11 staged copy is dropped: the pages are the client's. - // - // THE ENTRY POINTS ARE OPTIONAL: neither is in the loader's tables (an EGL function - // missing there is MGLOG_F), and eglGetProcAddress hands back live-looking stubs, so - // the extension strings decide and the pointers are only then taken. - using T0GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*); - using T0BufferStorageExternalFn = void (*)(GLenum, GLintptr, GLsizeiptr, EGLClientBuffer, GLbitfield); - T0GetNativeClientBufferFn g_t0GetNativeClientBuffer = nullptr; - T0BufferStorageExternalFn g_t0BufferStorageExternal = nullptr; - constexpr GLbitfield kT0MapReadBit = 0x0001; - constexpr GLbitfield kT0MapWriteBit = 0x0002; - constexpr GLbitfield kT0StorageFlags = - kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit | kDynamicStorageBit; - constexpr GLbitfield kT0MapFlags = kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit; - - using MG_Util::BackendLoader::HasExtensionWord; - - Bool ResolveT0EntryPoints(String& why) { - if (g_t0GetNativeClientBuffer != nullptr && g_t0BufferStorageExternal != nullptr) return true; - if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay || - !g_GLESFuncs.glGetString || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers) { - why = "the loader has no eglGetProcAddress/eglQueryString/glGetString/glMapBufferRange"; - return false; - } - const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS); - const char* gl = reinterpret_cast(g_GLESFuncs.glGetString(GL_EXTENSIONS)); - if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) { - why = "EGL_ANDROID_get_native_client_buffer is not advertised"; - return false; - } - if (!HasExtensionWord(gl, "GL_EXT_external_buffer") || !HasExtensionWord(gl, "GL_EXT_buffer_storage")) { - why = "GL_EXT_external_buffer / GL_EXT_buffer_storage is not advertised"; - return false; - } - g_t0GetNativeClientBuffer = reinterpret_cast( - g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID")); - g_t0BufferStorageExternal = reinterpret_cast( - g_EGLFuncs.eglGetProcAddress("glBufferStorageExternalEXT")); - if (g_t0GetNativeClientBuffer == nullptr || g_t0BufferStorageExternal == nullptr) { - why = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / glBufferStorageExternalEXT"; - g_t0GetNativeClientBuffer = nullptr; - g_t0BufferStorageExternal = nullptr; - return false; - } - return true; - } - - void DrainGLErrors() { - if (!g_GLESFuncs.glGetError) return; - for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) { - } - } - - // A fresh GL buffer whose storage IS `ahb`, mapped persistent + coherent. 0 on failure - // (with `why`); nothing on the resource is touched here. `throwaway` = the POST's own - // context is current: bind raw and keep Espryt's binding caches (which describe the - // session's context, whose name space this is not) out of it. - Uint ImportAhbAsBuffer(void* ahb, Uint64 size, void** outPtr, String& why, Bool throwaway = false) { - *outPtr = nullptr; - const EGLClientBuffer clientBuffer = - g_t0GetNativeClientBuffer(static_cast(ahb)); - if (clientBuffer == nullptr) { - why = "eglGetNativeClientBufferANDROID returned null"; - return 0; - } - Uint id = 0; - g_GLESFuncs.glGenBuffers(1, &id); - if (id == 0) { - why = "glGenBuffers gave no name"; - return 0; - } - DrainGLErrors(); - if (throwaway) g_GLESFuncs.glBindBuffer(TempBufferTarget, id); - else BindBufferId(TempBufferTarget, id); - g_t0BufferStorageExternal(TempBufferTarget, 0, static_cast(size), clientBuffer, - kT0StorageFlags); - const GLenum stored = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR; - void* ptr = stored == GL_NO_ERROR - ? g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), kT0MapFlags) - : nullptr; - if (ptr == nullptr) { - char text[160]; - std::snprintf(text, sizeof(text), "glBufferStorageExternalEXT -> 0x%04x, map %s", stored, - stored == GL_NO_ERROR ? "failed" : "not attempted"); - why = text; - if (!throwaway) { - NoteBufferIdDeleted(id); - ++g_bufferBackendIdGeneration; - } - g_GLESFuncs.glDeleteBuffers(1, &id); - return 0; - } - *outPtr = ptr; - return id; - } - - Bool T0Answer(char* out, Uint64 outBytes, Bool ok, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - std::vsnprintf(out, static_cast(outBytes), fmt, args); - va_end(args); - return ok; - } - - // Installs an imported store on the twin. The caller has retired whatever was there. - void InstallT0Store(GLESBufferResource& resource, Uint id, void* ptr, Uint64 size) { - resource.id = id; - resource.immutableStorage = true; - resource.persistentPtr = ptr; - resource.persistentMapped = true; - resource.storageSize = static_cast(size); - resource.storageInitialized = true; - resource.pendingRespecify = false; - resource.contextGeneration = g_bufferContextGeneration; - const std::lock_guard lock(resource.pendingMutex); - resource.pendingRanges.clear(); - resource.pendingResidentWrites.clear(); - } - - Bool Ops_H_ImportExternal(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) { - SweepT0Retired(false); - if (!CanTouchGLNow() || size == 0 || ahb == nullptr) return false; - String why; - if (!ResolveT0EntryPoints(why)) { - MGLOG_W_ONCE("Espryt T0: no import on this context - %s", why.c_str()); - return false; - } - auto* resource = GetOrCreateBufferResourceForHandle(res); - if (resource == nullptr) return false; - // IMPORT FIRST, retire second: a refused import leaves the T2 store (and its staged - // copy) exactly as it was, so the DECLINED the client gets leaves nothing to repair. - void* ptr = nullptr; - const Uint id = ImportAhbAsBuffer(ahb, size, &ptr, why); - if (id == 0) { - MGLOG_W("Espryt T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen, - static_cast(size), why.c_str()); - return false; - } - if (resource->contextGeneration == g_bufferContextGeneration && resource->id != 0) { - NoteBufferIdDeleted(resource->id); - // Driver VAOs may have the old id baked into bindings keyed on versions this - // replacement does not move (the Ops_H_MapPersistent re-mint's rule). - ++g_bufferBackendIdGeneration; - g_GLESFuncs.glDeleteBuffers(1, &resource->id); - } - resource->id = 0; - RetireT0Import(*resource); // a previous import of this handle, fenced - InstallT0Store(*resource, id, ptr, size); - MG_Remote::Transport::AdoptT0::AcquireImported(ahb); - resource->externalAhb = ahb; - ++g_t0ImportsLive; - // The bytes are the client's pages now; R-11's copy would answer a later drain - // with pre-import content. - MGL_SERVER_STAGED_DROP(*resource); - resource->hostBytes = nullptr; - resource->syncedChangeSerial = ResourceSerialOf(res); - MGLOG_D("Espryt T0: imported {slot=%u, gen=%u} as buffer %u (%llu bytes); %llu live imports", - res.Slot, res.Gen, id, static_cast(size), - static_cast(g_t0ImportsLive)); - return true; - } - - Bool Ops_H_ImportExternalTracked(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) { - const Bool imported = Ops_H_ImportExternal(res, ahb, size); - BumpBufferMutationEpoch(); - return imported; - } - - // The POST's second round: the CPU writes region 1 through the held lock only now, after - // the import and a GPU use of it, and reads region 4 (the first round's fill) back into - // its cache; a second dispatch copies region 1 out and overwrites region 4. Appends its - // counts to `detail`. - Bool SelfTestExternalSteady(GLuint imported, GLuint out, void* heldPtr, char* detail, SizeT detailBytes) { - constexpr Uint32 kWords = 1024; - constexpr Uint32 kSteadyAtWord = kWords; // region 1 - constexpr Uint32 kFillAtWord = 4 * kWords; // region 4 - auto* held = static_cast(heldPtr); - Uint32 cached = 0; - for (Uint32 i = 0; i < kWords; ++i) { - held[kSteadyAtWord + i] = 0xA5000000u ^ (i * 40503u); - cached ^= held[kFillAtWord + i]; - } - (void)cached; - static const char* kSource = - "#version 310 es\n" - "layout(local_size_x = 64) in;\n" - "layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n" - "layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n" - "void main() {\n" - " uint i = gl_GlobalInvocationID.x;\n" - " outb.w[i] = ext.w[1024u + i];\n" - " ext.w[4096u + i] = 0x3D5EA11Eu ^ i;\n" - "}\n"; - const GLuint program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource); - GLint linked = 0; - if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); - const SizeT used = std::strlen(detail); - if (!linked) { - if (program != 0) g_GLESFuncs.glDeleteProgram(program); - std::snprintf(detail + used, detailBytes - used, "; steady: the second program did not link"); - return false; - } - g_GLESFuncs.glUseProgram(program); - g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported); - g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out); - g_GLESFuncs.glMemoryBarrier(0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/); - g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1); - g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | - 0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/); - g_GLESFuncs.glFinish(); - Uint32 steadyRead = 0, steadySaw = 0; - for (Uint32 i = 0; i < kWords; ++i) - if (held[kFillAtWord + i] == (0x3D5EA11Eu ^ i)) ++steadySaw; - g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out); - const auto* copied = static_cast( - g_GLESFuncs.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT)); - if (copied != nullptr) { - for (Uint32 i = 0; i < kWords; ++i) - if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead; - g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); - } - g_GLESFuncs.glDeleteProgram(program); - std::snprintf(detail + used, detailBytes - used, - "; with the import in use, GPU read %u/%u words the CPU wrote after it, GPU write " - "seen through the held lock %u/%u words", - steadyRead, kWords, steadySaw, kWords); - return steadyRead == kWords && steadySaw == kWords; - } - - // THE POST (B2, the standing rule: decide by probe, never by driver name). The AHB API - // promises CPU coherence only across an unlock/lock; T0 keeps the client's lock for the - // store's life, so the pattern it depends on is exactly what this exercises, on this - // context's own driver, before kCapAdoptT0 may be published: - // hold - a 64 KiB BLOB allocated and locked by this test, never unlocked while it runs; - // read - this server's own host map of the import reads the pattern the CPU wrote - // through the held pointer (every hostBytes fallback reads persistentPtr); - // gpu - a compute dispatch copies that pattern out of the import (the GPU read) and - // fills another region of it (the GPU write); - // back - after glFinish the fill is read through the HELD pointer and the copy is - // read back from the dispatch's output; - // steady - the same again with bytes the CPU wrote AFTER the import and its first GPU - // use (VkBufferManager::SelfTestWireImport says why the first round is not enough). - // In a throwaway ES 3.1 pbuffer context, so none of this context's state or Espryt's - // binding caches is disturbed; the caller's bind is restored before returning. - Bool Ops_H_SelfTestExternal(char* whyOut, Uint64 whyBytes) { - const auto answer = [&](Bool ok, const char* text, const char* arg = "") { - return T0Answer(whyOut, whyBytes, ok, text, arg); - }; - String why; - if (!ResolveT0EntryPoints(why)) return answer(false, "%s", why.c_str()); - if (!g_EGLFuncs.eglChooseConfig || !g_EGLFuncs.eglCreateContext || !g_EGLFuncs.eglMakeCurrent || - !g_EGLFuncs.eglCreatePbufferSurface || !g_GLESFuncs.glCreateShaderProgramv || - !g_GLESFuncs.glDispatchCompute || !g_GLESFuncs.glMemoryBarrier || !g_GLESFuncs.glFinish) - return answer(false, "the loader lacks the EGL/GLES 3.1 entry points the test needs"); - const EGLDisplay display = g_EGLFuncs.eglGetCurrentDisplay(); - const EGLContext savedContext = g_EGLFuncs.eglGetCurrentContext(); - const EGLSurface savedDraw = g_EGLFuncs.eglGetCurrentSurface(EGL_DRAW); - const EGLSurface savedRead = g_EGLFuncs.eglGetCurrentSurface(EGL_READ); - const EGLint configAttribs[] = {EGL_RENDERABLE_TYPE, 0x0040 /*EGL_OPENGL_ES3_BIT*/, EGL_SURFACE_TYPE, - EGL_PBUFFER_BIT, EGL_NONE}; - EGLConfig config = nullptr; - EGLint configs = 0; - if (!g_EGLFuncs.eglChooseConfig(display, configAttribs, &config, 1, &configs) || configs < 1) - return answer(false, "no ES3 pbuffer config for the throwaway context"); - const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; - const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(display, config, surfaceAttribs); - const EGLint contextAttribs[] = {0x3098 /*EGL_CONTEXT_MAJOR_VERSION*/, 3, 0x30FB /*MINOR*/, 1, EGL_NONE}; - const EGLContext context = surface != EGL_NO_SURFACE - ? g_EGLFuncs.eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs) - : EGL_NO_CONTEXT; - const auto restore = [&] { - g_EGLFuncs.eglMakeCurrent(display, savedDraw, savedRead, savedContext); - if (context != EGL_NO_CONTEXT) g_EGLFuncs.eglDestroyContext(display, context); - if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(display, surface); - }; - if (context == EGL_NO_CONTEXT || !g_EGLFuncs.eglMakeCurrent(display, surface, surface, context)) { - restore(); - return answer(false, "the throwaway ES 3.1 context could not be made current"); - } - - constexpr Uint64 kBytes = 64 * 1024; - constexpr Uint32 kWords = 1024; // one 4 KiB region - constexpr Uint32 kFillAtWord = 4 * kWords; // region 4 - MG_Remote::Transport::AdoptT0::HeldStore held; - Vector pattern(kBytes / 4); - for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast(i * 2654435761u); - if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why)) { - restore(); - return answer(false, "hold: %s", why.c_str()); - } - Bool ok = false; - GLuint out = 0, program = 0; - void* hostMap = nullptr; - const GLuint imported = ImportAhbAsBuffer(held.ahb, kBytes, &hostMap, why, /*throwaway=*/true); - char detail[512] = {}; - if (imported == 0) { - std::snprintf(detail, sizeof(detail), "import: %s", why.c_str()); - } else if (std::memcmp(hostMap, pattern.data(), kWords * 4) != 0) { - std::snprintf(detail, sizeof(detail), "read: the server's host map of the import does not " - "hold the CPU's pattern"); - } else { - static const char* kSource = - "#version 310 es\n" - "layout(local_size_x = 64) in;\n" - "layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n" - "layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n" - "void main() {\n" - " uint i = gl_GlobalInvocationID.x;\n" - " outb.w[i] = ext.w[i];\n" - " ext.w[4096u + i] = 0x6C0FFEE0u ^ i;\n" - "}\n"; - program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource); - GLint linked = 0; - if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); - g_GLESFuncs.glGenBuffers(1, &out); - g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out); - g_GLESFuncs.glBufferData(GL_SHADER_STORAGE_BUFFER, kWords * 4, nullptr, GL_DYNAMIC_READ); - if (!linked) { - std::snprintf(detail, sizeof(detail), "gpu: the test's compute program did not link"); - } else { - g_GLESFuncs.glUseProgram(program); - g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported); - g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out); - g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1); - g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | - 0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/); - g_GLESFuncs.glFinish(); - Uint32 gpuRead = 0, heldSaw = 0; - const auto* heldWords = static_cast(held.ptr); - for (Uint32 i = 0; i < kWords; ++i) - if (heldWords[kFillAtWord + i] == (0x6C0FFEE0u ^ i)) ++heldSaw; - g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out); - const auto* copied = static_cast(g_GLESFuncs.glMapBufferRange( - GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT)); - if (copied != nullptr) { - for (Uint32 i = 0; i < kWords; ++i) - if (copied[i] == pattern[i]) ++gpuRead; - g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); - } - ok = gpuRead == kWords && heldSaw == kWords; - std::snprintf(detail, sizeof(detail), - "64 KiB AHB, lock held throughout: host map read ok, GPU read %u/%u words of " - "the CPU's pattern, GPU write seen through the held lock %u/%u words", - gpuRead, kWords, heldSaw, kWords); - if (ok) ok = SelfTestExternalSteady(imported, out, held.ptr, detail, sizeof(detail)); - } - } - if (program != 0) g_GLESFuncs.glDeleteProgram(program); - if (out != 0) g_GLESFuncs.glDeleteBuffers(1, &out); - if (imported != 0) g_GLESFuncs.glDeleteBuffers(1, &imported); - g_GLESFuncs.glFinish(); - restore(); - MG_Remote::Transport::AdoptT0::ReleaseHeld(held); - return answer(ok, "%s", detail); - } -#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__ - - // ---- P5c (tx): the TEXTURE half of the resource family, staged server-side -------- - // - // The buffer half's R-11 pattern (ServerStaged() above), for texture levels: the - // staged bytes of resource_subdata's texture half are adopted into the server's - // StagedTextureStore AT APPLY TIME, because `bytes` names SEG_STAGE and is dead the - // moment the record retires (rule C). The store, its ownership and its coverage - // rules are documented in MG_Remote/Server/StagedTextureStore.h; these are only the - // three hook bodies. All three are no-ops in monolith (CopiesIntoServerStorage() - // false), which is what keeps the monolith expression character for character. - // - // NOTHING here mints or reads a texture twin: the store is keyed by the wire handle - // the record carried (StagedTextureStore.h explains why not the twin address), so - // adoption needs no GL call and no frontend object. - - // GetUploadTargets() answered from the descriptor's Target: one static list per - // target, matching the frontend classes' own lists member for member - // (TextureObject*.h / TextureObjectStubs.h: a cube is six faces, a cube array is - // the single CubeMapArray target, everything else its own one). - const Vector& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget) { - static const Vector kUnknown{}; - static const Vector kTex1D{TextureUploadTarget::Texture1D}; - static const Vector kTex2D{TextureUploadTarget::Texture2D}; - static const Vector kTex3D{TextureUploadTarget::Texture3D}; - static const Vector kTex1DArray{TextureUploadTarget::Texture1DArray}; - static const Vector kTex2DArray{TextureUploadTarget::Texture2DArray}; - static const Vector kTexCube{ - TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX, - TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY, - TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ}; - static const Vector kTexCubeArray{TextureUploadTarget::CubeMapArray}; - static const Vector kTex2DMS{TextureUploadTarget::Texture2DMultisample}; - static const Vector kTex2DMSArray{TextureUploadTarget::Texture2DMultisampleArray}; - static const Vector kTexRect{TextureUploadTarget::TextureRectangle}; - static const Vector kTexBuffer{TextureUploadTarget::TextureBuffer}; - switch (static_cast(pipeResourceTarget)) { - case MG_Pipe::MGPipeResourceTarget::Tex1D: return kTex1D; - case MG_Pipe::MGPipeResourceTarget::Tex2D: return kTex2D; - case MG_Pipe::MGPipeResourceTarget::Tex3D: return kTex3D; - case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return kTex1DArray; - case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return kTex2DArray; - case MG_Pipe::MGPipeResourceTarget::TexCube: return kTexCube; - case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return kTexCubeArray; - case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return kTex2DMS; - case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return kTex2DMSArray; - case MG_Pipe::MGPipeResourceTarget::TexRect: return kTexRect; - case MG_Pipe::MGPipeResourceTarget::TexBuffer: return kTexBuffer; - default: return kUnknown; - } - } - -// P5e (ID-124): OUT OF THE DISAGGREGATED BLOCK ON PURPOSE. This is a pure -// MGPipeResourceTarget -> TextureTarget switch with no wire, no record and no role in it, but -// it sat inside the nested `#if MOBILEGL_BUILD_DISAGGREGATED` below while its only caller, -// MGB_TEXPARAM_TARGET, is guarded by `#if MOBILEGL_PIPE_PUSH` alone - so a push build without -// disaggregation did not compile. The narrower fix (guarding the macro block too) is wrong: -// that block also carries TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push -// flavour does need. Caught by the `flavours` gate step; see ID-124. - // The inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the sync's - // target reads (ConvertTextureTargetToBackendGLEnum and MapToBackendTextureTarget - // both want the frontend enum). - TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget) { - switch (static_cast(pipeResourceTarget)) { - case MG_Pipe::MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D; - case MG_Pipe::MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D; - case MG_Pipe::MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D; - case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray; - case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray; - case MG_Pipe::MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap; - case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray; - case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample; - case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray; - case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle; - case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer; - default: return TextureTarget::Unknown; - } - } - - void Ops_H_TextureSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, - const void* bytes, const MG_Pipe::MGPSubRegion* regions) { - // The region set is the upload planner's shape and stays in the applier's - // pending set; the store's coverage is the staged run itself - // (StagedTextureStore.h's coverage ruling). It is ALSO what places that run in - // the level image when the record carries a piece of the level rather than the - // whole of it (fix A2), which is why it is read here now. - auto& store = MG_Record::ServerStagedTexture(); - // P13 W4b: an aliasing store (monolith's record arm) adopts too - it records where - // the run sits in the frontend's level shadow instead of copying it. - if (!store.Holds()) return; - // The applier's gate has already faulted every shape that reaches here without - // bytes, and under split the codec declared the run's length (Blob.Size) - - // TextureEmit.h's "the bytes this record declares ARE the level shadow". - if (bytes == nullptr || record.Blob.Size == 0) return; - const auto* stored = PipeTextureRecordForHandle(res); - if (stored == nullptr) return; - const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); - const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target); - const IntVec3 extent = MG_Record::StagedTextureUploadExtent(stored->Desc, record); - const SizeT runBytes = static_cast(record.Blob.Size); - // P8-E (docs/archive/notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW - // (FollowCopyImageInStagedStore). The run is the client's shadow, stale everywhere it - // did not write, so only the texels it names land - its regions, or its union box - // when it carries none (D-D3's "the box is the whole story"). A run the store cannot - // place that way is adopted as ever, and the store stops standing for the texture. - if (store.LevelHoldsFollowedCopy(key, uploadTarget, record.Level) && - (record.RegionCount == 0 || regions != nullptr)) { - Vector boxes; - if (record.RegionCount == 0) { - const auto& box = record.UnionBox; - boxes.push_back({box.X, box.Y, box.Z, box.W, box.H, box.D}); - } else { - for (Uint32 i = 0; i < record.RegionCount; ++i) { - const auto& region = regions[i]; - boxes.push_back({region.X, region.Y, region.Z, region.W, region.H, region.D}); - } - } - const Uint64 imageOffset = record.RegionCount == 0 - ? 0 : MG_Record::StagedTextureRunImageOffset(record, regions); - if (store.AdoptClientBoxes(key, uploadTarget, record.Level, extent, imageOffset, bytes, runBytes, - boxes.data(), boxes.size())) { - return; - } - store.MarkDriverWritten(key); - } - // RegionCount == 0 IS the whole-level spelling - "the run is the level shadow", - // beginning at the level's first byte - so it is adopted with the spelling that - // replaces the level; every other record names a RUN of the level by its own box. - // A client record's run: under monolith's record arm it IS the frontend level shadow, - // the one source an aliasing store keeps by address. - if (record.RegionCount == 0) { - store.Adopt(key, uploadTarget, record.Level, extent, bytes, runBytes, /*frontendShadow=*/true); - return; - } - store.AdoptRun(key, uploadTarget, record.Level, extent, - MG_Record::StagedTextureRunImageOffset(record, regions), bytes, - runBytes, /*frontendShadow=*/true); - } - - void Ops_H_TextureRespecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, - const MG_Pipe::MGPRespecifiedLevel* level) { - auto& store = MG_Record::ServerStagedTexture(); - if (!store.Holds()) return; - const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); - if (level != nullptr) { - // ONE glTexImage*D redefined one level: its exact extent crossed with the - // accepted scope carrier, including a legal noncanonical mutable mip. - store.NoteLevelDefined( - key, MG_Pipe::MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level, - IntVec3{static_cast(level->Width), static_cast(level->Height), - static_cast(level->Depth)}, - desc.Target, desc.InternalFormat); - return; - } - // A whole-resource redefinition: every level's old coordinate system is gone. - // glTexStorage* then defines the WHOLE chain at once (GL 4.6 core 8.19 - all six - // cube faces included), so an immutable descriptor re-marks every level of every - // upload target; a mutable whole-resource respecify (a texture view) defines - // nothing here and the store simply forgets the old levels. - store.ResetLevels(key); - if (desc.Immutable == 0 || desc.Levels == 0) return; - for (const auto& uploadTarget : StagedUploadTargetsForPipeTarget(desc.Target)) { - for (Uint32 levelIndex = 0; levelIndex < desc.Levels; ++levelIndex) { - store.NoteLevelDefined( - key, static_cast(uploadTarget), static_cast(levelIndex), - MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, levelIndex), - desc.Target, desc.InternalFormat); - } - } - } - - void Ops_H_TextureDestroy(MG_Pipe::MGPipeHandle res) { - // Deliberately NOT gated on CopiesIntoServerStorage(): Drop's own m_any gate - // makes the monolith call one acquire load, and an unconditional drop cannot - // strand a key the latch state was misread for. - MG_Record::ServerStagedTexture().Drop( - MG_Record::StagedTextureStore::KeyForHandle(res)); - } - - const MG_Pipe::MGPipeResourceOps g_glesResourceOps = { - .Create = Ops_H_Create, - .Respecify = Ops_H_RespecifyTracked, - .SubData = Ops_H_SubDataTracked, - .SubDataResident = Ops_H_ResidentSubDataTracked, - .FlushRange = Ops_H_FlushRangeTracked, - .Readback = Ops_H_ReadbackTracked, - .Destroy = Ops_H_DestroyTracked, - .MapPersistent = Ops_H_MapPersistentTracked, - .UnmapPersistent = Ops_H_UnmapPersistent, - .TextureSubData = Ops_H_TextureSubData, - .TextureRespecify = Ops_H_TextureRespecify, - .TextureDestroy = Ops_H_TextureDestroy, -#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) - // P11 B2 (T0). Android only: nowhere else is there an AHardwareBuffer to import. - .ImportExternal = Ops_H_ImportExternalTracked, - .SelfTestExternal = Ops_H_SelfTestExternal, -#endif - }; - - } // namespace - - Uint64 CurrentBufferMutationEpoch() { - return g_bufferMutationEpoch.load(std::memory_order_acquire); - } - -#if MOBILEGL_BUILD_DISAGGREGATED - namespace { - // P5e (vi): how many times BumpBufferMutationEpoch ran off the apply thread while - // one was running, with a live transport. See the note in the bump itself; the - // accessor is what a test can assert zero on instead of reading a call graph. - std::atomic g_bufferMutationEpochBumpsOffTheApplyThread{0}; - } // namespace - - Uint64 BufferMutationEpochBumpsOffTheApplyThread() { - return g_bufferMutationEpochBumpsOffTheApplyThread.load(std::memory_order_relaxed); - } -#endif - - void BumpBufferMutationEpoch() { -#if MOBILEGL_BUILD_DISAGGREGATED - // P5e (vi), CONTRACT-P5E §5.1's first pin (scout S1 R2, BRIEF-P5E's open question 2): - // "does any GL-THREAD path bump this under a transport". PINNED HERE RATHER THAN - // ARGUED, because the argument is the kind that is true until someone adds a caller. - // - // It matters because this counter is the ONE non-serial input left in the record - // arm's clean gate: SyncVaoAttributeBuffersByRecord stamps the pre-pass value and - // skips every IsBufferDrawCleanByHandle probe while the stamp holds. A bump from the - // GL thread would make that stamp a cross-thread read of a value the applier never - // observed - the memo would go on reading clean over buffers the client had just - // dirtied - and under run-ahead the client is a frame ahead, so the window is a - // frame wide rather than a record wide. - // - // THE MEASURED ANSWER, and it is not the clean "no" the scout expected: there is - // EXACTLY ONE class of non-apply-thread bump under a transport, and it happens - // BEFORE THERE IS AN APPLY THREAD AT ALL. BackendObject_DirectGLES::Initialize() - // runs on the app thread by design ("the backend object is BUILT on the app thread - // while its context is never OWNED by it", ServerLoop.h) and calls - // RegisterBufferBackendOps(), whose last act is a bump. An unguarded refusal aborts - // the whole split lane in EGL bring-up, which is how this was found rather than - // reasoned. - // - // That class is harmless and the apply-thread key tells it apart from the one that - // would not be: with g_applyThreadKey == 0 no record has been applied, so no twin - // exists, so no memo holds a stamp for the bump to slide under. - // - // WHAT IS COUNTED AND WHAT IS FATAL, and why they are not the same thing. Every - // bump taken off the apply thread WHILE ONE IS RUNNING is counted and named once, - // and is Fatal UNDER THE STRICT LANE - which is where this phase's pins live, and - // which is the lane the answer is claimed for. It is not Fatal by default because - // the unit fixtures that drive the production op tables directly - // (StagedShadowProductionTest / ServerLoopTest, which register the real tables and - // call them from the test thread beside a started loop) are the server role without - // being the apply thread, and taking those down would be this pin policing a - // fixture rather than the draw path. - const Uint64 applyThreadKey = - MG_Remote::Server::Detail::g_applyThreadKey.load(std::memory_order_relaxed); - if (MG_Config::Transport != MG_Config::TransportMode::Monolith && applyThreadKey != 0 && - !MG_Remote::Server::ServerLoop::OnApplyThread()) { - g_bufferMutationEpochBumpsOffTheApplyThread.fetch_add(1, std::memory_order_relaxed); - if (MG_Config::Ipc.StrictErrors) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"CurrentBufferMutationEpoch\"} - the " - "buffer-mutation epoch was bumped off the apply thread while an apply " - "thread was running, with a live transport. The draw-path memos stamp " - "this counter and skip their clean probes while the stamp holds " - "(CONTRACT-P5E §5.1), so a producer on the client's thread means the " - "backend is reachable from a thread that owns none of this state"); - std::abort(); - } - MGLOG_E_ONCE("MGPipe: the buffer-mutation epoch was bumped off the apply thread " - "with a live transport (CONTRACT-P5E §5.1's pin). Run the strict lane " - "to make this fatal at the producer"); - } -#endif - g_bufferMutationEpoch.fetch_add(1, std::memory_order_release); - } - - // The seventh slot table. A process-lifetime global, like the other six, so it links - // itself into its own holder list from its own constructor with no initialisation - // order question to answer (SlotTables.h). - BackendBufferResourceTable g_backendBufferResources; - // The two P3a families get the three-way treatment ClassifyEsprytSlotArm established for // bit 5, rather than reading their bit and stopping there: "the bit is clear AND the // legacy arm has been taken away" is a real configuration an operator can ask for, and a @@ -2947,1188 +636,6 @@ namespace MobileGL::MG_Backend::DirectGLES { if (legacyArmSurvivesLegacyMemos || legacyMemosEnabled) return PipeSubsystemArmVerdict::Legacy; return PipeSubsystemArmVerdict::NoArm; } - - GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { - if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; - // The table refuses both of these itself; this is the release-build VOICE for the - // refusal, because MOBILEGL_ASSERT compiles out at INFO and a resource that silently - // stops being twinned is the failure mode the refusal exists to replace. - if (res.Slot >= BackendBufferResourceTable::kMaxHandleSlot) { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 - MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, - "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - " - "buffer twin request named slot %u, past this table's %u bound", - res.Slot, BackendBufferResourceTable::kMaxHandleSlot); -#else - MGLOG_E_ONCE("MGPipe: resource handle slot %u is past the backend table's %u bound - " - "refusing to twin it", - res.Slot, BackendBufferResourceTable::kMaxHandleSlot); - return nullptr; -#endif - } - const Uint32 liveGen = g_backendBufferResources.LiveGenAt(res.Slot); - if (liveGen != 0 && liveGen > res.Gen) { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 - MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, - "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - " - "buffer twin request named generation %u at slot %u, behind live generation %u", - res.Gen, res.Slot, liveGen); -#else - MGLOG_E_ONCE("MGPipe: resource handle {%u, %u} names a generation BEHIND the live twin's " - "%u - refusing rather than dropping the incumbent's driver storage", - res.Slot, res.Gen, liveGen); - return nullptr; -#endif - } - auto& twin = g_backendBufferResources.GetOrCreate(res); - if (!twin) { - twin = MakeShared(); - // Same seed the lazy legacy path gives a resource it has just minted: nothing - // has defined storage yet, so the first sync owes a full (re)specification. - twin->pendingRespecify = true; - } - return twin.get(); - } - - GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { - auto* twin = g_backendBufferResources.FindByHandle(res); - return twin ? twin->get() : nullptr; - } - - void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, - SizeT end, const char* site) { - ServerStaged().RequireCoverage(&resource, hostBase, start, end, site); - } - - // M-3's rule for the WHOLE-STORE readers (v1 round 3). The descriptor is the - // application's own statement about the store: HasDefinedContent set means it SUPPLIED - // the content (glBufferData(size, data)), which under split arrives as resource_subdata - // records behind the respecify (table 1 row 19) - so a coverage gap at the draw is a - // MISSING RECORD and the zero-fill past the coverage is not the application's bytes. - // Clear means it ORPHANED the store (glBufferData(size, NULL), glBufferStorage(NULL)): - // every byte it has not staged since is UNDEFINED by its own declaration, the streaming - // idiom (orphan, partial glBufferSubData, draw) is the ordinary case, and uploading the - // shadow's zero-fill for the rest is exactly what the monolith arm uploads from - // MappedData(). Round 2 refused both shapes and aborted six LargeArenaAdoption / - // ResourceSubsystemControl entries on the joint by name (v1-v3.md 6). - // - // AT NAMESPACE SCOPE, not in the anonymous namespace it was written in (P3b/P4b espryt - // D1 slice 1): the XFB scatter guards ITS whole-store read with the same question, and - // a second spelling of "did the application declare these bytes" is how the pool-reuse - // ladder and the scatter end up disagreeing about one buffer. - Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res) { - const auto* record = ResourceRecordOf(res); - return record != nullptr && record->Desc.HasDefinedContent != 0; - } - - // P13 W4a (§1.4): THE GPU-WRITE REFRESH OF AN ALIASING STORE. Monolith's record arm reads - // the frontend shadow in place (StagedShadowStore keeps the caller's pointer), so a store a - // shader wrote is refreshed the way the client refreshes it under split: the bytes are - // read back from this process's GL buffer and handed to OnBufferWriteback, whose consumer - // (the frontend, one call away) copies them into that shadow. The backend never writes - // frontend memory itself. Ops_H_Readback's order, for its reasons: queued uploads first, - // resident writes next, then the read, the serial stamp and - last - the epoch bump. - const Uint8* RefreshAliasedStoreFromGpu(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, - const char* site) { - if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized || - resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration || - !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) { - MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read " - "back here, so the CPU reads the shadow as it is", - site, res.Slot, res.Gen); - return SplitHostBytes(resource); - } - const SizeT width = ResourceWidthOf(res); - const SizeT size = std::min(width, resource.storageSize); - if (size == 0) return SplitHostBytes(resource); - if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) { - MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU and no reverse channel is " - "installed to refresh the shadow it aliases", - site, res.Slot, res.Gen); - return SplitHostBytes(resource); - } - FlushPendingRangesFrom(resource, resource.hostBytes, width); - DrainResidentWritesNow(resource); - BindBufferId(TempBufferTarget, resource.id); - void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), - GL_MAP_READ_BIT); - if (mapped == nullptr) { - MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the " - "shadow as it is", - site, resource.id); - return SplitHostBytes(resource); - } - MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( - res, 0, - MG_Pipe::MGPBlobRef{reinterpret_cast(mapped), static_cast(size), - MG_Pipe::kMGHostSpanSegNone, 0}); - if (resource.hostBytes == nullptr) { - // Every content-carrying record and every respecify of a store with a size hands - // its base over on this arm, so no base here means a shape nothing recorded. Keep - // the bytes in a private copy rather than answer "nothing" for GPU-written bytes. - MGLOG_W_ONCE("%s: GPU-written buffer {%u, %u} has no recorded shadow base; its refreshed " - "bytes are kept in a private copy", - site, res.Slot, res.Gen); - const std::lock_guard lock(resource.pendingMutex); - resource.hostBytes = ServerStaged().AdoptPrivateCopy(&resource, width, mapped, size); - } - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - resource.serverGpuWritten = false; - resource.syncedChangeSerial = ResourceSerialOf(res); - BumpBufferMutationEpoch(); - return resource.hostBytes; - } - - // P8-C. The declaration says what; this says in what order, and it is Ops_H_Readback's - // order because it is the same question asked by the server of itself instead of by the - // client: (1) queued uploads land in the GL store first - reading back past them would - // revert them in the copy - under the same staged-coverage assertion that drain carries; - // (2) queued resident writes land (GPU-ordered, behind every command already recorded); - // (3) the whole store is read back into the staged copy, which leaves its coverage whole - // and so also answers RequireStagedCoverage for an orphaned store's first copy; (4) the - // serial stamp and the epoch bump, as Ops_H_ReadbackTracked does them. - const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, - const char* site) { - // A coherent store - T0's import, or (P13 W4a) a map monolith's record arm donated - - // has no copy: its bytes are read through the map once queued writes have landed. - const Bool coherent = resource.persistentMapped && resource.persistentPtr != nullptr; - Bool residentWritesQueued = false; - if (coherent) { - const std::lock_guard lock(resource.pendingMutex); - residentWritesQueued = !resource.pendingResidentWrites.empty(); - } - if (!resource.serverGpuWritten && !residentWritesQueued) return SplitHostBytes(resource); - if (!ServerStaged().CopiesIntoServerStorage() && !coherent) { - // P13 W4a (§1.4): an ALIASING store (monolith's record arm) is refreshed through - // the frontend - the writeback lands in the shadow the base points into. Any other - // non-copying store has no marks to answer. - if (MG_Config::RecordArmAliasesFrontend()) return RefreshAliasedStoreFromGpu(resource, res, site); - return SplitHostBytes(resource); - } - if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized || - resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration || - !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) { - MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read " - "back here, so the CPU reads the staged copy as it is", - site, res.Slot, res.Gen); - return SplitHostBytes(resource); - } - if (coherent) { - // T0: the copy IS this server's coherent map of the client's pages. Landing the - // queued writes and finishing makes them, and every shader write already queued, - // visible through it - Ops_H_Readback's persistent-map arm, for the same reason. - DrainResidentWritesNow(resource); - if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); - resource.serverGpuWritten = false; - return SplitHostBytes(resource); - } - const SizeT width = ResourceWidthOf(res); - const SizeT size = std::min(width, resource.storageSize); - if (size == 0) return SplitHostBytes(resource); - MGL_SERVER_STAGED_REQUIRE_PENDING(resource, resource.hostBytes, width, "gpu_written_refresh_flush_pending"); - FlushPendingRangesFrom(resource, resource.hostBytes, width); - DrainResidentWritesNow(resource); - BindBufferId(TempBufferTarget, resource.id); - void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), - GL_MAP_READ_BIT); - if (mapped == nullptr) { - MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the " - "staged copy as it is", - site, resource.id); - return SplitHostBytes(resource); - } - { - const std::lock_guard lock(resource.pendingMutex); - resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, width, mapped, 0, size); - } - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - resource.serverGpuWritten = false; - resource.syncedChangeSerial = ResourceSerialOf(res); - BumpBufferMutationEpoch(); - MGLOG_D("%s: refreshed the staged copy of GPU-written buffer {%u, %u} (%zu bytes)", site, res.Slot, - res.Gen, size); - return resource.hostBytes; - } - - void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes, - SizeT offset, SizeT size) { - if (bytes == nullptr || size == 0 || resource.externalAhb != nullptr) return; - // Only a COPYING store may take bytes that die with a mapping: the monolith one keeps - // the caller's pointer (StagedShadow.h), which here would dangle after the unmap. - if (!ServerStaged().CopiesIntoServerStorage()) return; - // Under pendingMutex, as every other hostBytes store is (M-2). - const std::lock_guard lock(resource.pendingMutex); - resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size); - } - - SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); } - Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res) { return ResourceSerialOf(res); } - Uint CurrentBufferContextGeneration() { return g_bufferContextGeneration; } - - // See the declaration: re-mints of a live resource's driver id. Written only on - // the context thread (all re-mint sites run there), read only by the VAO sync. - Uint64 g_bufferBackendIdGeneration = 0; - - void RegisterBufferBackendOps() { - // P3a: the handle-shaped resource table (P13: the only one - the legacy - // BufferBackendOps table is deleted). Registration is UNCONDITIONAL: the subsystem bit is the FRONTEND's dispatch predicate - // (MGPipeResourceSubsystemEnabled reads the bit AND this table's presence), so a - // build with bit 7 clear registers a table nobody calls and the A/B stays a pure - // configuration question rather than a bring-up-order one. - MG_Pipe::MGPipeSetResourceOps(&g_glesResourceOps); - // Frontend writes issued while ops were unregistered advanced change - // serials with no per-op bump; re-open every draw-clean memo. Advanced directly, - // past BumpBufferMutationEpoch's apply-thread pin: a registration mutates no buffer - // and only makes every memo probe again, and with several sessions in one process - // a new session's backend registers on its own thread while another session's - // apply thread is already running - which the pin would otherwise read as a - // client-thread producer and, under the strict lane, abort the server for. - g_bufferMutationEpoch.fetch_add(1, std::memory_order_release); - } - - void UnregisterBufferBackendOps() { - if (MG_Pipe::MGPipeGetResourceOps() == &g_glesResourceOps) { - MG_Pipe::MGPipeSetResourceOps(nullptr); - } - // From here on frontend writes bypass the tracked ops entirely. - BumpBufferMutationEpoch(); - InvalidateArrayBufferBindingCache(); - // Pooled ids belong to the dying context too; drop them without glDeleteBuffers. - ClearBufferPool(); - const std::lock_guard lock(g_deferredBufferReleasesMutex); - // The ES context owning these ids is going away; just drop the handles. - g_deferredBufferReleases.clear(); - g_hasDeferredBufferReleases.store(false, std::memory_order_release); - } - - void OnServerSessionEndedWithOthersLive() { - // Only what belongs to the ending session: its staged bytes (both stores are - // session-scoped). The rings, the ops table and the buffer generation are the - // process's and every other session's GL buffers are still alive in the shared group. - MGL_SERVER_STAGED_DROP_ALL(); -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Record::ServerStagedTexture().DropAll(); -#endif - } - - void OnBackendContextDestroyed() { - UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch - ++g_bufferContextGeneration; - // The generation moved AFTER the unregister bump above; re-open the - // memos again so no stamp can predate the generation change. - BumpBufferMutationEpoch(); - InvalidateArrayBufferBindingCache(); - InvalidateIndexedBufferBindingCache(); - InvalidatePixelBufferBindingCaches(); - // The rings' ids and persistent maps died with the context; drop the - // handles (no GL) and let the next draw / texture upload recreate them. - ResetRingForNewContext(UboRing()); - ResetRingForNewContext(UnpackRing()); - // R-11's server copies die with the context for the same reason the rings' ids do: - // the resource twins they are keyed by are about to be rebuilt against a new - // generation, and a shadow that outlived its twin would be looked up by a RECYCLED - // address on the next allocation - which is the quietest possible wrong answer. - MGL_SERVER_STAGED_DROP_ALL(); - // tx's texture shadows die for the same reason, keyed by handle rather than address - // but with the same recycled-identity failure mode: a new context's allocator may - // hand out a {slot, gen} the old one's store still answers for. - MG_Record::ServerStagedTexture().DropAll(); - // P11 B2: every retired T0 import's fence named work on the context that just ended, - // so all of them let go now. LIVE imports keep their reference: the next ensure - // imports them again on the new context (EnsureBufferResourceForHandle). -#if MOBILEGL_BUILD_DISAGGREGATED - ProcessDeferredT0Retires(); - SweepT0Retired(/*contextGone=*/true); -#endif - } - - void ProcessDeferredBufferReleases() { - // Runs on every draw; skip the context check, mutex and vector churn - // outright when nothing was enqueued since the last drain. - if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return; - if (!CanTouchGLNow()) return; - Vector> releases; - { - const std::lock_guard lock(g_deferredBufferReleasesMutex); - releases.swap(g_deferredBufferReleases); - g_hasDeferredBufferReleases.store(false, std::memory_order_release); - } - for (auto& resource : releases) { - auto* glesResource = static_cast(resource.get()); - if (glesResource->contextGeneration != g_bufferContextGeneration) { - glesResource->id = 0; - continue; - } - if (IsPoolable(*glesResource)) { - EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers - continue; - } - if (glesResource->id != 0) { - if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { - InvalidateArrayBufferBindingCache(); - } - ScrubBufferBindingShadowsForId(glesResource->id); - g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); - glesResource->id = 0; - } - } - } - -#if MOBILEGL_BUILD_DISAGGREGATED - // P11 B2: the T0 half of the deferred-release drain, run right after it at both sync - // points (ProcessDeferredBufferReleases itself moves verbatim, G5). Retired imports whose - // fence signalled let go of their AHardwareBuffer; a deferred T0 destroy is retired once - // the drain deleted its id (the fence then follows the delete) or its context is gone. - void ProcessDeferredT0Retires() { - if (!g_t0Retired.empty()) SweepT0Retired(false); - if (!g_hasT0DeferredDestroys.load(std::memory_order_acquire)) return; - Vector> ready; - { - const std::lock_guard lock(g_deferredBufferReleasesMutex); - for (SizeT i = 0; i < g_t0DeferredDestroys.size();) { - auto* r = static_cast(g_t0DeferredDestroys[i].get()); - if (r->id == 0 || r->contextGeneration != g_bufferContextGeneration) { - ready.push_back(std::move(g_t0DeferredDestroys[i])); - g_t0DeferredDestroys[i] = std::move(g_t0DeferredDestroys.back()); - g_t0DeferredDestroys.pop_back(); - } else { - ++i; - } - } - g_hasT0DeferredDestroys.store(!g_t0DeferredDestroys.empty(), std::memory_order_release); - } - for (auto& resource : ready) RetireT0Import(*static_cast(resource.get())); - } -#endif - - // The same five questions, asked of the applier instead of the frontend object, with - // IDENTICAL semantics (D-A4): - // identity the slot table's twin at this handle, not GetBackendResource() - // size record.Desc.Width, not GetSize() - // freshness record.Serial vs syncedChangeSerial, not GetChangeSerial() - // map state record.HasLiveHostWrites AND the frontend's IsMapped() - see below - // the rest unchanged, and server-side to begin with - // - // HasLiveHostWrites is ALWAYS FALSE in P3a and is written by nobody; it is here so the - // phase that pushes persistent-mapped host writes can set it with no new record kind, - // and the assertion below is what stops that phase landing a silent semantic change. - // - // BECAUSE it is pinned false, it CANNOT stand in for the frontend's IsMapped() yet, and - // the probe still asks the object: an EMULATED (non-adopted) persistent map - under the - // 16 MiB adoption threshold, or with DisableLargeBufferAdoption, or with no - // EXT_buffer_storage - mutates the client's shadow with no call, no serial and no epoch, - // which is the entire reason the legacy probe asks a map question instead of a serial - // one. Answering only HasLiveHostWrites made such a buffer read draw-clean forever, so - // SyncPersistentMappedRange (D-N keeps it on the ensure path for all of P3a) was never - // reached again and the frame drew the last uploaded bytes with no diagnostic anywhere. - // The frontend read retires the moment P5 gives HasLiveHostWrites a producer, and it is - // the last frontend read in this function. - Bool IsBufferDrawCleanByHandle(MG_Pipe::MGPipeHandle res, const GLESBufferResource* resource, - const MG_State::GLState::BufferObject* frontend) { - if (!resource) return false; - const auto* twin = g_backendBufferResources.FindByHandle(res); - if (twin == nullptr || twin->get() != resource) return false; - if (resource->contextGeneration != g_bufferContextGeneration) return false; - if (resource->id == 0) return false; - if (resource->persistentMapped) { - return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty(); - } - const auto* record = ResourceRecordOf(res); - if (record == nullptr) return false; - if (record->HasLiveHostWrites) return false; - // THE LAST FRONTEND READ IN THIS FUNCTION, AND P5 b1 RETIRES IT - under split - // only, because that is the only build where it is both wrong and replaceable. - // - // It asked the object whether it was mapped because HasLiveHostWrites was pinned - // false and an emulated persistent map mutates the shadow with no call, no serial - // and no epoch; answering from the record alone made such a buffer read - // draw-CLEAN forever, SyncPersistentMappedRange was never reached again, and the - // frame drew the last uploaded bytes with no diagnostic - // (MG_Test/SanityTest.cpp's DirectGLESBufferDrawProbe is exactly that case). - // - // TWO THINGS REPLACE IT AND BOTH HAD TO LAND FIRST: the record now carries the map - // state (the line above, from MGPSubData::HasLiveHostWrites), and the client - // pushes the mapped span by block at every validate point, so the serial moves for - // a write the application made with no API call. Under a spawn there is no object - // on this side to ask, which is why this was never going to stay a choice. - // - // A null object is the "no frontend to ask" case and is treated as "not mapped", - // which is what the record already says. - if (resource->pendingRespecify || !resource->storageInitialized) return false; - if (!resource->pendingRanges.empty()) return false; - if (resource->storageSize != static_cast(record->Desc.Width)) return false; - return resource->syncedChangeSerial.load(std::memory_order_acquire) == record->Serial; - } - - // The handle arm of the ensure path. Same shape, same order, same branches; the four - // frontend reads become the applier's stored descriptor, its Serial and the shadow base - // the last content-carrying call handed over. - // - // It still takes the frontend object for ONE reason, recorded rather than hidden: - // BufferObject::SyncPersistentMappedRange() is one of the eleven Espryt sites - // ROADMAP/D-N explicitly keeps where it is for P3a (the per-site attribution table is - // P8's to execute). Every other line here is handle-shaped. When P8 moves that call to - // the client this signature loses its last frontend argument. - GLESBufferResource* EnsureBufferResourceForHandle(const SharedPtr& bufferObject, - MG_Pipe::MGPipeHandle res) { - auto* resource = GetOrCreateBufferResourceForHandle(res); - if (!resource) return nullptr; - - // The client's shadow base, RE-READ at every use, exactly as the legacy arm re-read - // MappedData() at every use. It is deliberately NOT resource->hostBytes here: two - // ordinary events move or free what a base recorded by an earlier call points at - - // a shadow resize (PipeResource::ResizeShadow is reserve + resize, so a grow past - // the reserve reallocates) and persistent-map adoption (the shadow is cleared and - // shrunk to fit) - and this path still holds the frontend object anyway, because - // D-N keeps SyncPersistentMappedRange here for all of P3a. hostBytes stays the - // fallback for the drains that have NO object (the readback flush), and it is - // nulled at both of those events. - const auto liveHostBase = [&resource, &bufferObject]() -> const Uint8* { - // M-2 / codex 3 / ID-52 item 3: under an ACTIVE TRANSPORT the server's staged - // copy (resource->hostBytes, filled by MGL_SERVER_STAGED_ADOPT in Ops_H_SubData / - // Ops_H_FlushRange) is the ONLY authoritative base. Preferring the frontend - // object's MappedData() here - which under inproc is always non-null, same process - // - meant every reduced-path drain read the CLIENT's shadow and NEVER the server's, - // so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5 - // 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that - // does not read the client object's memory. Under monolith nothing changes. - return resource->hostBytes; - }; - - if (resource->contextGeneration != g_bufferContextGeneration) { - // The id (if any) belonged to a destroyed ES context. - resource->id = 0; - resource->storageInitialized = false; - resource->storageSize = 0; - resource->pendingRespecify = true; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - resource->contextGeneration = g_bufferContextGeneration; - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - resource->immutableStorage = false; - // m-5 / codex 5: OnBackendContextDestroyed ran MGL_SERVER_STAGED_DROP_ALL(), which - // frees every server shadow but does NOT null the hostBytes that name them - so a - // twin that SURVIVES a context loss still carries a base into the freed allocation. - // Null it here, where a surviving twin is re-armed - but ONLY when the shadow is - // really gone. This block also runs on a twin's FIRST ensure (contextGeneration - // starts mismatched), and there the shadow a preceding resource_subdata just staged - // is still live; nulling it then would drop the reduced path's own bytes (it did, - // and TriangleScenario read the wrong VBO). HasShadow is the discriminator: false - // after DropAll, true after an ordinary Adopt. - // - // TRANSPORT-GUARDED like the other two split hunks in this file (review v2 N-7): - // under MONOLITH transport in a disaggregated build the store never copies, Adopt - // never sets m_any, HasShadow answers false for everything, and this null would - // run on every twin's first ensure - harmless there only because liveHostBase() - // prefers MappedData() under monolith, and "under monolith nothing changes" should - // be true by construction rather than by luck. - if (ServerStaged().CopiesIntoServerStorage() && - !ServerStaged().HasShadow(resource)) { - resource->hostBytes = nullptr; - } -#if defined(__ANDROID__) && MOBILEGL_BUILD_DISAGGREGATED - // P11 B2: A T0 STORE OUTLIVES ITS CONTEXT. Its pages are the client's - // AHardwareBuffer, which this twin still holds, so the store is imported again on - // the new context - the client keeps writing through the same pointer and nothing - // has to be re-sent. A refusal here leaves the twin without a store and says so. - if (resource->externalAhb != nullptr) { - String why; - void* ptr = nullptr; - const Uint64 size = ResourceWidthOf(res); - const Uint id = ResolveT0EntryPoints(why) ? ImportAhbAsBuffer(resource->externalAhb, size, &ptr, why) - : 0; - if (id != 0) { - InstallT0Store(*resource, id, ptr, size); - MGLOG_I("Espryt T0: store {slot=%u, gen=%u} re-imported on the new context (%llu bytes)", - res.Slot, res.Gen, static_cast(size)); - } else { - MGLOG_E("Espryt T0: store {slot=%u, gen=%u} could not be re-imported after a context loss - " - "%s; the store has no GPU backing", - res.Slot, res.Gen, why.c_str()); - RetireT0Import(*resource); - } - } -#endif - } - - // An immutable store nothing maps any more, retired here on the thread that can. - if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) { - NoteBufferIdDeleted(resource->id); - ++g_bufferBackendIdGeneration; - g_GLESFuncs.glDeleteBuffers(1, &resource->id); - resource->id = 0; - resource->immutableStorage = false; - resource->storageInitialized = false; - resource->storageSize = 0; - resource->pendingRespecify = true; - } - - // Zero-copy coherent persistent buffer: nothing to (re)upload at draw time. - if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) { - DrainResidentWritesNow(*resource); - return resource; - } - - if (resource->id == 0) { - const SizeT poolSize = ResourceWidthOf(res); - // The pool DECISION is the legacy one, unchanged: extent and residence only. - // An earlier cut added "and a shadow base was recorded", which silently stopped - // a buffer recycling pool ids - a change to D-F's pool behaviour, not to "the - // source of bytes" the conversion is allowed to move, and one that moves - // AcquireFromPool call counts StreamedArenaScenario observes. - const Uint reused = - (poolSize > 0 && !resource->persistentMapped) ? AcquireFromPool(poolSize) : 0; - if (reused != 0) { - resource->id = reused; - resource->storageSize = poolSize; - resource->storageInitialized = true; - resource->pendingRespecify = false; - BindBufferId(TempBufferTarget, reused); - // M-3 / codex 4: this is a WHOLE-STORE upload from the base, and under split - // the base is the server shadow (M-2). For a store whose content the - // application SUPPLIED, zero-filled bytes past the staged coverage are not the - // application's - uploading them is the silent data loss the M-6 ruling - // forbids, and a gap is a missing record: Fatal by name. For a store the - // application ORPHANED the gap is its own undefined content and the upload is - // legal (ResourceContentIsDeclared, above). RequireCoverage is a no-op for the - // legacy arm's MappedData() and for a non-copying store. - if (ResourceContentIsDeclared(res)) { - MGL_SERVER_STAGED_REQUIRE(*resource, liveHostBase(), 0, poolSize, - "pool_reuse_whole_store"); - } - g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, liveHostBase()); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(poolSize)); - } - { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - } - resource->syncedChangeSerial = ResourceSerialOf(res); - } else { - g_GLESFuncs.glGenBuffers(1, &resource->id); - if (resource->id == 0) { - MGLOG_E_ONCE("Failed to generate buffer object."); - MGLOG_E_ONCE("ES glGetError(): %s", - MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - return resource; - } - resource->storageInitialized = false; - resource->pendingRespecify = true; - } - } - - // D-N keeps this call here for P3a. It can queue ranges and move the record, so - // everything below is read AFTER it. - // P5c (hd, CONTRACT-P5C §3.8): under an active transport the frontend accessor is a - // layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb - // persistent-map push is the only producer; the call is skipped outright. Under - // monolith D-N's placement stands. - - const auto* record = ResourceRecordOf(res); - const SizeT size = record != nullptr ? static_cast(record->Desc.Width) : 0; - if (size == 0) { - return resource; - } - const Uint64 serial = record->Serial; - const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(static_cast(record->Desc.Usage)); - // Read AFTER SyncPersistentMappedRange above, and through liveHostBase for the - // reason written where it is declared. - const Uint8* const hostBase = liveHostBase(); - // AND PUBLISH IT, because the readers that have NO frontend object read - // resource->hostBytes and nothing else: the readback flush (Ops_H_Readback), the - // kill-switch map arm of Ops_H_FlushRange, and the fp64 narrowing - // (SyncConvertedAttributeByHandle, whose legacy counterpart reads - // bufferObject->MappedData()). A twin is created LAZILY - D-A2's row makes - // Ops_H_Create a no-op - so the very first resource_respecify of a buffer finds - // FindBufferResourceForHandle == nullptr and its base is dropped on the floor; with - // glBufferData(..., data) followed by no further content call (the ordinary static - // vertex array) hostBytes then stayed null for the object's whole life and the fp64 - // narrowing refused every draw, disabling the array. This is the one place that both - // holds the object and runs before every draw that uses the store, so it is where the - // base is refreshed. Only reached for a NON-adopted resource (the adopted arm - // returned above), so C-2's rule is intact: an adopted store's hostBytes stays null - // and its bytes are read through persistentPtr. - if (hostBase != nullptr) resource->hostBytes = hostBase; - // "DOES THE SHADOW THIS PATH IS ABOUT TO UPLOAD HOLD MEANINGFUL BYTES?" - and it has - // to be asked of the SAME thing the bytes come from. The legacy arm pairs - // bufferObject.MappedData() with bufferObject.HasDefinedContent() (RespecifyStorageNow) - // and this arm pairs the same two, so the source of the bytes and the statement about - // them can never disagree. - // - // P5c (hd, CONTRACT-P5C §3.6 / B1): with an active transport the answer is the - // DESCRIPTOR's bit - which the client publishes on ResourceCreate/Respecify - OR the - // staged coverage the content records behind it delivered. The coverage half is what - // keeps the answer equal to the frontend flag's in the one case the descriptor alone - // cannot see: an ORPHANING respecify (descriptor clear) followed by glBufferSubData, - // which defines content without a new descriptor - the bytes crossed as - // resource_subdata and the staged store's coverage is exactly "content defined since - // the last orphan". With no coverage and a clear bit the store is undefined by the - // application's own declaration and the upload stays a pure NULL reallocation. - const Bool shadowHasContent = - (record->Desc.HasDefinedContent != 0 || - (ServerStaged().CopiesIntoServerStorage() ? ServerStaged().CoveredRunCount(resource) != 0 - : resource->contentSinceRespecify)); - const void* initialData = shadowHasContent ? hostBase : nullptr; - // M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE - // [0, size) upload from the base, so it owes the same coverage the pending-range drain - // below owes - the two respecify arms were the readers M-6's "never widened" rule did - // not reach. No-op for the legacy arm's MappedData() and for a non-copying store; a - // Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} under split when the - // DESCRIPTOR says the application supplied the content and the shadow's coverage does - // not span the store (a missing record), instead of uploading its zero-fill as content. - // NOT for a store the application orphaned: there `shadowHasContent` is the frontend - // object's flag, which the streaming idiom's partial glBufferSubData flips to true, and - // the bytes it did not write are undefined by its own glBufferData(NULL) - the rule at - // ResourceContentIsDeclared. Round 2 refused that idiom and aborted six joint entries. - const auto requireWholeStoreCoverage = [&]() { - if (initialData != nullptr && record->Desc.HasDefinedContent != 0) { - MGL_SERVER_STAGED_REQUIRE(*resource, static_cast(initialData), 0, - size, "respecify_whole_store"); - } - }; - - if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) { - requireWholeStoreCoverage(); - RespecifyStorageWith(*resource, size, usage, initialData, serial); - } else if (!resource->pendingRanges.empty()) { - // Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object - // `hostBase` is the server shadow and every queued range must be staged. - RenameSmallStoreForDrain(*resource, hostBase, size); - MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, hostBase, size, "ensure_flush_pending"); - FlushPendingRangesFrom(*resource, hostBase, size); - resource->syncedChangeSerial = serial; - } else if (resource->syncedChangeSerial != serial) { - // Mutations this backend could not track (the table was unregistered between - // contexts); re-upload everything. - requireWholeStoreCoverage(); - RespecifyStorageWith(*resource, size, usage, initialData, serial); - } - return resource; - } - - void BindBufferId(GLenum target, Uint id) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (target == GL_ARRAY_BUFFER) { - if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) { - return; - } - g_boundArrayBufferId = id; - g_boundArrayBufferKnown = true; - } - g_GLESFuncs.glBindBuffer(target, id); - } - - void InvalidateArrayBufferBindingCache() { - g_boundArrayBufferId = 0; - g_boundArrayBufferKnown = false; - } - - void BindPixelPackBufferId(Uint id) { - if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { - return; - } - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id); - g_boundPixelPackBufferId = id; - g_boundPixelPackBufferKnown = true; - } - - void BindPixelUnpackBufferId(Uint id) { - if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) { - return; - } - g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id); - g_boundPixelUnpackBufferId = id; - g_boundPixelUnpackBufferKnown = true; - } - - void InvalidatePixelBufferBindingCaches() { - g_boundPixelPackBufferId = 0; - g_boundPixelPackBufferKnown = false; - g_boundPixelUnpackBufferId = 0; - g_boundPixelUnpackBufferKnown = false; - } - - void NoteBufferIdDeleted(Uint id) { - if (id == 0) { - return; - } - if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) { - InvalidateArrayBufferBindingCache(); - } - ScrubBufferBindingShadowsForId(id); - } - - namespace { - // Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range - // (same index + id + range) are skipped. isBase distinguishes a whole-buffer - // base bind from a sub-range bind. Fresh/reset context: every point is base(0) - // == unbound, which matches the GL default. - struct IndexedBufferBinding { - Uint id = 0; - GLintptr offset = 0; - GLsizeiptr size = 0; - Bool isBase = true; - // False when the driver's binding at this point is no longer described by the - // fields above and the next bind must be issued whatever it asks for. Set by - // InvalidateIndexedBufferBindingShadowsForId after a store was re-specified at - // a new size: the id is still bound, so the entry must NOT be scrubbed to - // base(0) (a later bind of 0 would then be false-skipped) - only distrusted. - Bool known = true; - }; - constexpr SizeT kMaxIndexedBufferBindings = 64; - IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings]; - IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings]; - // Transform feedback gets a shadow for a reason the other two do not have: the - // capture points are synced from the application's TOUCHED high-water mark, which - // deqp/glcts permanently raises to GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by - // clearing every point after each test case. Without a shadow every capture that - // uses fewer points than that (i.e. every INTERLEAVED_ATTRIBS capture) re-issued a - // redundant glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) for the unused - // tail immediately before glBeginTransformFeedback - calls a plain GL application - // never makes there, and the only thing MobileGL does differently from one. - // - // Unlike the UBO/SSBO points these are NOT context state: they belong to the bound - // transform feedback OBJECT, so XfbImpl::BindTransformFeedback drops the whole - // shadow to unknown on every object switch (InvalidateTransformFeedbackBindingShadows). - IndexedBufferBinding g_indexedXFBBindings[kMaxIndexedBufferBindings]; - IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) { - if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache - if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index]; - if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index]; - if (glTarget == GL_TRANSFORM_FEEDBACK_BUFFER) return &g_indexedXFBBindings[index]; - return nullptr; - } - - // glDeleteBuffers resets the deleted buffer's bindings (indexed and - // pixel pack/unpack ones included) to 0 in the current context; mirror - // that in the shadows, or a later buffer recycling the same name with a - // matching shadow entry would false-skip its rebind. Default - // IndexedBufferBinding{} == base(0) == the post-delete GL state. - void ScrubBufferBindingShadowsForId(Uint id) { - if (id == 0) return; - for (auto& binding : g_indexedUBOBindings) { - if (binding.id == id) binding = {}; - } - for (auto& binding : g_indexedSSBOBindings) { - if (binding.id == id) binding = {}; - } - for (auto& binding : g_indexedXFBBindings) { - if (binding.id == id) binding = {}; - } - if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { - g_boundPixelPackBufferId = 0; - } - if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) { - g_boundPixelUnpackBufferId = 0; - } - } - - void InvalidateIndexedBufferBindingShadowsForId(Uint id) { - if (id == 0) return; - for (auto& binding : g_indexedUBOBindings) { - if (binding.id == id) binding.known = false; - } - for (auto& binding : g_indexedSSBOBindings) { - if (binding.id == id) binding.known = false; - } - for (auto& binding : g_indexedXFBBindings) { - if (binding.id == id) binding.known = false; - } - } - } // namespace - - // The capture points belong to the bound transform feedback object, so a bind (or a - // delete, which reverts to the default object) replaces all of them at once with - // state this shadow has never seen. Distrust rather than scrub: the driver's bindings - // are whatever the newly bound object holds, which is NOT necessarily base(0), and - // scrubbing would let a later bind of 0 be false-skipped. - void InvalidateTransformFeedbackBindingShadows() { - for (auto& binding : g_indexedXFBBindings) binding.known = false; - } - - void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) { - auto* s = IndexedBindingShadow(glTarget, index); - if (s && s->known && s->isBase && s->id == id) return; - g_GLESFuncs.glBindBufferBase(glTarget, index, id); - if (s) *s = {id, 0, 0, true, true}; - } - - void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size) { - auto* s = IndexedBindingShadow(glTarget, index); - if (s && s->known && !s->isBase && s->id == id && s->offset == offset && s->size == size) return; - g_GLESFuncs.glBindBufferRange(glTarget, index, id, offset, size); - if (s) *s = {id, offset, size, false, true}; - } - - void InvalidateIndexedBufferBindingCache() { - for (auto& b : g_indexedUBOBindings) b = {}; - for (auto& b : g_indexedSSBOBindings) b = {}; - for (auto& b : g_indexedXFBBindings) b = {}; - } - - void TrimBufferPool() { - const std::lock_guard lock(g_poolMutex); - if (g_pooledBytes <= kMaxPoolBytes) return; - // Over budget: evict oldest-retireSerial entries with real glDeleteBuffers. - while (g_pooledBytes > kMaxPoolBytes) { - SizeT oldestKey = 0, oldestIdx = 0; - Uint64 oldestSerial = ~Uint64{0}; - Bool found = false; - for (auto& kv : g_bufferPool) { - for (SizeT i = 0; i < kv.second.size(); ++i) { - if (kv.second[i].retireSerial < oldestSerial) { - oldestSerial = kv.second[i].retireSerial; - oldestKey = kv.first; - oldestIdx = i; - found = true; - } - } - } - if (!found) break; - auto& bucket = g_bufferPool[oldestKey]; - PooledBuffer& e = bucket[oldestIdx]; - if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) { - ScrubBufferBindingShadowsForId(e.id); - g_GLESFuncs.glDeleteBuffers(1, &e.id); - } - g_pooledBytes -= e.size; - bucket[oldestIdx] = bucket.back(); - bucket.pop_back(); - } - } - - void ClearBufferPool() { - const std::lock_guard lock(g_poolMutex); - // Ids belong to the dying context; drop without glDeleteBuffers (mirrors - // the g_deferredBufferReleases.clear() discipline). - g_bufferPool.clear(); - g_pooledBytes = 0; - } - - // --- Persistent-mapped bump rings (see Managers.h) ------------------------ - namespace { - // (Re)create the ring store with room for at least minBytes. Any live - // store is retired (deleted once the GPU finished the last frame that - // could reference its slots), never deleted in place. Returns false and - // leaves the current store untouched when minBytes cannot fit under the - // size cap; a GL failure loses the store and latches creationFailed so - // later uses stop retrying under this context. - Bool CreateRingStorage(PersistentRing& ring, SizeT minBytes) { - SizeT newSize = ring.initialBytes; - while (newSize < minBytes) newSize *= 2; - if (newSize > ring.maxBytes) return false; - - if (ring.store.id != 0) { - ring.retired.push_back( - {ring.store.id, ring.store.contextGeneration, DirectGLES::CurrentFrameSerial() + 1}); - } - const Uint32 nextGeneration = ring.store.generation + 1; - ring.store.id = 0; - ring.store.mappedPtr = nullptr; - - Uint id = 0; - g_GLESFuncs.glGenBuffers(1, &id); - if (id != 0) { - BindBufferId(TempBufferTarget, id); - g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(newSize), nullptr, - GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); - void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(newSize), - GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); - if (!ptr) { - // The dying id is what the array-buffer cache has recorded as - // bound; a later buffer recycling the name would false-skip. - InvalidateArrayBufferBindingCache(); - g_GLESFuncs.glDeleteBuffers(1, &id); - id = 0; - } else { - ring.store.mappedPtr = static_cast(ptr); - } - } - if (id == 0) { - MGLOG_E_ONCE("%s: persistent storage creation failed (%zu bytes); falling back to the " - "client-memory upload path.", - ring.label, newSize); - ring.store.creationFailed = true; - return false; - } - - SizeT alignment = ring.fixedAlignment; - if (alignment == 0) { - const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment; - alignment = capsAlignment > 0 ? static_cast(capsAlignment) : 256; - } - ring.store.id = id; - ring.store.size = newSize; - ring.store.head = 0; - ring.store.tail = 0; - ring.store.generation = nextGeneration; - ring.store.alignment = alignment; - ring.frameMarks.clear(); - MGLOG_D("%s: %zu MiB persistent store ready (id %u, gen %u, align %zu).", ring.label, - newSize / (1024u * 1024u), id, nextGeneration, alignment); - return true; - } - - // Shared half of the availability gate (the per-ring kill switch sits in - // the exported wrappers). Reclamation rides the Present fence watermark; - // without working fences slots would never be provably GPU-idle (same rule - // as IsPoolable). - Bool RingAvailable(PersistentRing& ring) { - if (!g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers || - !g_GLESFuncs.glFenceSync || !g_GLESFuncs.glGetSynciv) { - return false; - } - if (!CanTouchGLNow()) return false; - if (ring.store.contextGeneration != g_bufferContextGeneration) { - ResetRingForNewContext(ring); - } - return !ring.store.creationFailed; - } - - // Division-based rounding fallback: the spec doesn't promise a power-of-two - // alignment. Slot offsets stay multiples of the alignment because every - // slot size is, and wrap padding restarts at ring offset 0. - inline SizeT RingAlignUp(SizeT size, SizeT alignment) { - if ((alignment & (alignment - 1)) == 0) { - return (size + alignment - 1) & ~(alignment - 1); - } - return (size + alignment - 1) / alignment * alignment; - } - Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset); - - // Bump-allocate `size` bytes out of `ring`. - // - // Fast path: a live store under the current context with room before both - // the wrap boundary and the in-flight tail. Touches no GL and probes no - // frame marks - callers sit behind the ring's availability gate, so the - // context checks have already run for this draw/upload. `tail` may be stale - // here (marks are only retired on Present and on the slow path); staleness - // is conservative - the in-flight span reads too large, the check fails, - // and the slow path retires marks and re-tries. - Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset) { - if (size == 0) return false; - auto& store = ring.store; - if (store.id != 0 && store.contextGeneration == g_bufferContextGeneration) { - const SizeT alignedSize = RingAlignUp(size, store.alignment); - // Ring sizes are the ring's initial size (a power of two) doubled - // some number of times, so the offset modulo reduces to a mask. - static_assert((kUboRingInitialBytes & (kUboRingInitialBytes - 1)) == 0, - "ring offset mask below requires power-of-two ring sizes"); - static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0, - "ring offset mask below requires power-of-two ring sizes"); - static_assert((kUploadRingInitialBytes & (kUploadRingInitialBytes - 1)) == 0, - "ring offset mask below requires power-of-two ring sizes"); - const SizeT offset = static_cast(store.head & (store.size - 1)); - if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) { - store.head += alignedSize; - outOffset = offset; - return true; - } - } - return RingAllocateSlow(ring, size, outOffset); - } - - Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset) { - if (!RingAvailable(ring)) return false; - auto& store = ring.store; - // Sizing the store first, so the request is rounded with the alignment - // the live store actually carries rather than the pre-creation default. - if (store.id == 0 && !CreateRingStorage(ring, RingAlignUp(size, store.alignment))) { - return false; - } - const SizeT alignedSize = RingAlignUp(size, store.alignment); - - // Advance tail past every frame the GPU provably finished. - const Uint64 completed = DirectGLES::CompletedFrameSerial(); - SizeT retiredMarks = 0; - for (const auto& mark : ring.frameMarks) { - if (mark.frameSerial > completed) break; - if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent; - ++retiredMarks; - } - if (retiredMarks > 0) { - ring.frameMarks.erase(ring.frameMarks.begin(), - ring.frameMarks.begin() + static_cast(retiredMarks)); - } - - // A slot may not straddle the ring end; pad the cursor to the boundary. - SizeT offset = static_cast(store.head % store.size); - if (offset + alignedSize > store.size) { - store.head += store.size - offset; - offset = 0; - } - - if (store.head + alignedSize - store.tail > store.size) { - // In-flight span would overrun live slots: grow instead of overwrite. - if (CreateRingStorage(ring, std::max(store.size * 2, alignedSize))) { - offset = 0; - } else if (store.creationFailed) { - return false; // store lost; callers fall back to their legacy path - } else { - // At the size cap (>maxBytes in flight). First try to free room - // by waiting for the OLDEST in-flight frames to retire - a - // bounded wait that ends as soon as enough tail space exists, - // instead of draining the entire queue. - constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame - while (!ring.frameMarks.empty() && - store.head + alignedSize - store.tail > store.size) { - const auto& oldest = ring.frameMarks.front(); - if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) { - break; - } - if (oldest.headAtPresent > store.tail) store.tail = oldest.headAtPresent; - ring.frameMarks.erase(ring.frameMarks.begin()); - } - if (store.head + alignedSize - store.tail <= store.size) { - offset = static_cast(store.head % store.size); - if (offset + alignedSize > store.size) { - store.head += store.size - offset; - offset = 0; - } - store.head += alignedSize; - outOffset = offset; - return true; - } - // No usable fence covers the oldest frames: drain once rather than - // corrupt live slots. - if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); - store.tail = store.head; - ring.frameMarks.clear(); - // Same-frame slots written before the drain may now be recycled by - // the very next allocations; a generation bump keeps later draws - // from rebinding those cached offsets. - ++store.generation; - offset = static_cast(store.head % store.size); - if (offset + alignedSize > store.size) { - store.head += store.size - offset; - offset = 0; - } - } - } - - store.head += alignedSize; - outOffset = offset; - return true; - } - - void DeleteOrphanRingBuffers() { - const std::lock_guard lock(g_orphanRingBuffersMutex); - auto& orphans = OrphanRingBuffers(); - if (orphans.empty()) return; - for (const OrphanRingBuffer& orphan : orphans) { - // A buffer of a share group that is gone was reclaimed with it. - if (orphan.contextGeneration != g_bufferContextGeneration) continue; - ScrubBufferBindingShadowsForId(orphan.id); - g_GLESFuncs.glDeleteBuffers(1, &orphan.id); - } - orphans.clear(); - } - - // Present()-time upkeep shared by both rings. - void RingOnPresent(PersistentRing& ring) { - if (!CanTouchGLNow()) return; - DeleteOrphanRingBuffers(); - - // Delete grown-away stores the GPU is provably done with. - const Uint64 completed = DirectGLES::CompletedFrameSerial(); - for (SizeT i = ring.retired.size(); i-- > 0;) { - RetiredRingStore& entry = ring.retired[i]; - const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration; - if (!staleContext && entry.retireSerial > completed) continue; - if (!staleContext && entry.id != 0) { - ScrubBufferBindingShadowsForId(entry.id); - g_GLESFuncs.glDeleteBuffers(1, &entry.id); - } - ring.retired[i] = ring.retired.back(); - ring.retired.pop_back(); - } - - auto& store = ring.store; - if (store.id == 0 || store.contextGeneration != g_bufferContextGeneration) return; - // Retire completed marks here too — RingAllocate is the main consumer, - // but frames that used the ring for nothing would otherwise let the list - // grow one entry per Present, unboundedly. - SizeT retiredMarks = 0; - for (const auto& mark : ring.frameMarks) { - if (mark.frameSerial > completed) break; - if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent; - ++retiredMarks; - } - if (retiredMarks > 0) { - ring.frameMarks.erase(ring.frameMarks.begin(), - ring.frameMarks.begin() + static_cast(retiredMarks)); - } - // Record this frame's high-water mark (Present just fenced the serial now - // reported by CurrentFrameSerial()). A fence-less Present repeats the - // serial; fold into the existing mark. - const Uint64 serial = DirectGLES::CurrentFrameSerial(); - if (!ring.frameMarks.empty() && ring.frameMarks.back().frameSerial == serial) { - ring.frameMarks.back().headAtPresent = store.head; - } else { - ring.frameMarks.push_back({serial, store.head}); - } - } - } // namespace - - Bool UboRingAvailable() { - if (MG_Config::Features.EsprytDisableUboRing) return false; - return RingAvailable(UboRing()); - } - - Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); } - - UboRingSlotState UboRingPrepareSlot(UboRingAllocation& slot, Uint32 contentVersion, Uint64 frameSerial, - SizeT bindSize, void*& outWrite, Uint& outBufferId) { - if (MG_Config::Features.EsprytDisableUboRing) return UboRingSlotState::Unavailable; - PersistentRing& ring = UboRing(); - if (!RingAvailable(ring)) return UboRingSlotState::Unavailable; - if (slot.ringGeneration == ring.store.generation && slot.frameSerial == frameSerial && - slot.contentVersion == contentVersion) { - outBufferId = ring.store.id; - return UboRingSlotState::Reused; - } - SizeT offset = 0; - if (!RingAllocate(ring, bindSize, offset)) return UboRingSlotState::Failed; - // Read after the allocation: a grow mints a new store and generation. - outWrite = static_cast(ring.store.mappedPtr) + offset; - slot = {contentVersion, ring.store.generation, frameSerial, offset}; - outBufferId = ring.store.id; - return UboRingSlotState::Allocated; - } - - void* UboRingMappedPtr() { return UboRing().store.mappedPtr; } - Uint UboRingBufferId() { return UboRing().store.id; } - Uint32 UboRingGeneration() { return UboRing().store.generation; } - - void UboRingOnPresent() { RingOnPresent(UboRing()); } - - Bool UnpackRingAvailable() { - if (MG_Config::Features.EsprytDisableUnpackRing) return false; - return RingAvailable(UnpackRing()); - } - - Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) { - // A request the ring could never satisfy even empty would otherwise walk - // the whole grow/drain ladder before failing. - if (size > kUnpackRingMaxBytes) return false; - return RingAllocate(UnpackRing(), size, outOffset); - } - - void* UnpackRingMappedPtr() { return UnpackRing().store.mappedPtr; } - Uint UnpackRingBufferId() { return UnpackRing().store.id; } - SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; } - - void UnpackRingOnPresent() { RingOnPresent(UnpackRing()); } - - void UploadRingOnPresent() { RingOnPresent(UploadRing()); } } // namespace BufferImpl // ---- P4a (D-K3): the four family arm resolvers, beside BufferImpl's two ---- @@ -4142,6 +649,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // them per family). So all four can reach NoArm and all four stop with the named // Fatal{PipeLegacyMemosDisabled, ...} rather than running the very arm the operator asked // to have taken away and handing back a result measured on it. + namespace { // Shared by the dependent families, so the refusal reads the same way every time and a // future family cannot invent a different wording. Returns true when the dependency is @@ -4203,87 +711,6 @@ namespace MobileGL::MG_Backend::DirectGLES { bit, detail); BufferImpl::StopOnArmlessPipeSubsystem(what); } - - // The release-build VOICE for StateBackendObjectRegistry::GetOrCreateByHandle's two - // silent refusals, shared by the kinds P4a re-keys so the wording cannot drift between - // them. It is the shape GetOrCreateBufferResourceForHandle gives P3a's buffer family, - // lifted into one template because a copy per kind is a chance per kind to write one of - // them differently. - // - // ITS CALLERS IN THIS PACKAGE ARE THE FRAMEBUFFER ATTACHMENT WALK'S TWO - // (FramebufferImpl::SyncAttachmentSurface: the texture registry and the renderbuffer - // registry), because that is where a HANDLE arrives in a payload - MGPSurface::Res - - // and an ADOPTION is what the arm owes rather than a lookup. The other three re-keyed - // kinds resolve at sites in DirectGLES.cpp (package E's SyncCurrentFBO, the lazy - // sampler mint and SyncCurrentProgram), so their first adoption is E's to write; this - // template is what they call so all five say the same thing. - // - // WHY A VOICE AT ALL: the table asserts, and MOBILEGL_ASSERT compiles out at INFO - - // which all three gate builds and every shipped build are - so an object that silently - // stops being twinned is exactly the failure mode the refusal exists to replace. - // - // Returns null on the legacy arm too, so a caller that reaches this without checking - // its family's arm gets nothing rather than a twin the legacy arm would never see. - // - // SPLIT IN TWO (review N-3), and the split is what makes the two refusals REACHABLE. - // Both of this package's call sites stand behind a cross-check that the record's Res - // equals registry.HandleOf(frontendObject) - the LIVE handle - so by the time the - // adoption ran, the slot was in bounds and the generation was current BY CONSTRUCTION - // and neither refusal below could ever fire: the letter of M-4 was closed and its - // substance was not. The record's handle is the UNTRUSTED input and the frontend is the - // corroboration, so the validity question is asked FIRST, on its own, at both sites; - // the adoption then performs the resolve. Asking it first is also side-effect free, - // which the ordering rule N-1 states for the framebuffer refusal wants here too: - // GetOrCreateByHandle can reset an incumbent twin, and a handle this predicate rejects - // must not reach it. - template - Bool PipeTwinHandleIsAdoptable(Registry& registry, MG_Pipe::MGPipeHandle handle, const char* kindName) { - if (MG_Pipe::MGPipeHandleIsNull(handle)) return false; - if (handle.Slot >= Registry::SlotTable::kMaxHandleSlot) { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 - MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, - "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - " - "%s handle slot %u is past this table's %u bound", - kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot); -#else - MGLOG_E_ONCE("MGPipe: %s handle slot %u is past the backend table's %u bound - " - "refusing to twin it", - kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot); - return false; -#endif - } - const Uint32 liveGen = registry.LiveGenAt(handle.Slot); - if (liveGen != 0 && liveGen > handle.Gen) { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 - MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, - "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - " - "%s handle {%u, %u} names generation behind live generation %u", - kindName, handle.Slot, handle.Gen, liveGen); -#else - // SlotTables.h:301-321: forward is a recycle and resets the twin, BACKWARD is - // refused, because adopting it would release the incumbent LIVE twin's driver - // id and then stamp the slot back to the dead object's generation, after which - // the incumbent's own FindByHandle refuses it and it is silently handed a - // fresh, empty twin - a leak AND an object that loses its storage with no - // diagnostic. That is the shape commit d7655247 fixed on the buffer side. - MGLOG_E_ONCE("MGPipe: %s handle {%u, %u} names a generation BEHIND the live " - "twin's %u - refusing rather than dropping the incumbent's driver " - "object", - kindName, handle.Slot, handle.Gen, liveGen); - return false; -#endif - } - return true; - } - - template - typename Registry::BackendPtr* AdoptTwinByHandle(Registry& registry, MG_Pipe::MGPipeHandle handle, - const char* kindName) { - if (!PipeTwinHandleIsAdoptable(registry, handle, kindName)) return nullptr; - return registry.GetOrCreateByHandle(handle); - } } // namespace Bool ResolveFramebufferSubsystemArm() { @@ -4454,49 +881,10 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - // ---- P4a: the record readers (Managers.h documents the contract) ---- - namespace { - // One shape for all five, so a kind cannot grow a different liveness rule than its - // neighbours: index the applier's own dense table, refuse a slot it has not grown to, - // refuse a record that is not Live, and refuse a generation that has moved on under - // the caller. The last of the three is the one that matters: a stale handle resolving - // to its successor's record is how a twin ends up describing another object's storage. - template - const Record* PipeRecordAt(const Vector& table, MG_Pipe::MGPipeHandle handle) { - if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; - if (handle.Slot >= table.size()) return nullptr; - const Record& record = table[handle.Slot]; - if (!record.Live || record.Gen != handle.Gen) return nullptr; - return &record; - } - } // namespace - const MG_Pipe::MGPipeResourceRecord* PipeTextureRecordForHandle(MG_Pipe::MGPipeHandle res) { return PipeRecordAt(MG_Pipe::MGPipeApplier().TextureResources, res); } - const MG_Pipe::MGPipeResourceRecord* PipeRenderbufferRecordForHandle(MG_Pipe::MGPipeHandle res) { - return PipeRecordAt(MG_Pipe::MGPipeApplier().RenderbufferResources, res); - } - - // P3b/P4b (wave 2-D, package D3). See the declaration for why a view's storage clauses are - // asked of another record, why null means "ask again" and why the deletion order cannot - // strand this walk. - const MG_Pipe::MGPipeResourceRecord* PipeTextureStorageRecordForRecord( - const MG_Pipe::MGPipeResourceRecord& record) { - // ONE hop is the shape the client emits and the second is already generous; the bound is - // here so a cyclic chain is a null answer rather than a hang, not because a real chain - // is ever this long. - constexpr Uint32 kViewChainBound = 4; - const MG_Pipe::MGPipeResourceRecord* storage = &record; - for (Uint32 hop = 0; hop < kViewChainBound; ++hop) { - if (MG_Pipe::MGPipeHandleIsNull(storage->Desc.ViewOf)) return storage; - storage = PipeTextureRecordForHandle(storage->Desc.ViewOf); - if (storage == nullptr) return nullptr; - } - return nullptr; - } - MobileGL::TextureTarget PipeTextureTargetForHandle(MG_Pipe::MGPipeHandle res) { const auto* record = PipeTextureRecordForHandle(res); if (record == nullptr) return MobileGL::TextureTarget::Unknown; @@ -4540,21 +928,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return PipeRecordAt(MG_Pipe::MGPipeApplier().SamplerViewCsos, view); } - const MG_Pipe::MGPipeShaderCsoRecord* PipeShaderCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) { - // The composite band is a CLIENT-side indexing detail (contract D13) and the server - // must not learn about it: one reader, one answer, and the band's own table is indexed - // by (slot - base) exactly as the allocator's is. - if (MG_Pipe::MGPipeIsCompositeShaderSlot(cso.Slot)) { - const auto& band = MG_Pipe::MGPipeApplier().CompositeShaderCsos; - const Uint32 index = cso.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase; - if (index >= band.size()) return nullptr; - const auto& record = band[index]; - if (!record.Live || record.Gen != cso.Gen) return nullptr; - return &record; - } - return PipeRecordAt(MG_Pipe::MGPipeApplier().ShaderCsos, cso); - } - // N-7: D KEYS ON THE UPLOAD HALF; WIRE (AccumulatePendingUpload, PipeApply.cpp) KEYS ON // THE WHOLE PACKED FIELD. That asymmetry is resolved HERE, in D's favour and with the // reason, rather than by widening the two comparisons: D's question is per (uploadTarget, @@ -4571,8 +944,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // EmitOneLevel), so the two are carried and equal by construction and any disagreement is a // seam defect, not a shape to learn. This is also MGPResourceDesc::Target's first reader in // this package (esprytobj-v2.md §2(6) recorded it as having none). - static void NotePipeSubDataTargetLowByte(const MG_Pipe::MGPipeResourceRecord& record, Uint16 packedTarget, - Uint16 level) { + void NotePipeSubDataTargetLowByte(const MG_Pipe::MGPipeResourceRecord& record, Uint16 packedTarget, Uint16 level) { const Uint8 carried = MG_Pipe::MGPipeSubDataResourceTargetOf(packedTarget); if (carried == record.Desc.Target) return; MGLOG_E_ONCE("MGPipe: a pending upload for level %u carries resource target %u while its " @@ -4696,9484 +1068,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - namespace SamplerViewImpl { - BackendSamplerViewTable g_backendSamplerViews; - - BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view) { - auto* twin = g_backendSamplerViews.FindByHandle(view); - return twin ? twin->get() : nullptr; - } - - } // namespace SamplerViewImpl - - namespace VertexArrayImpl { - namespace { - SizeT GetDataTypeSize(DataType type) { - switch (type) { - case DataType::Int8: - case DataType::Uint8: - return 1; - case DataType::Int16: - case DataType::Uint16: - case DataType::Float16: - return 2; - case DataType::Int32: - case DataType::Uint32: - case DataType::Float32: - case DataType::Fixed32: - return 4; - case DataType::Float64: - return 8; - default: - return 0; - } - } - - // Tightly-packed byte size of one vertex element: 4 for the 2_10_10_10 types and GL_BGRA - // (one 32-bit word / 4 bytes), componentSize * size otherwise. 0 for unknown types. - SizeT GetAttributeByteSize(DataType type, int size, Bool isBgra) { - if (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev || isBgra) { - return 4; - } - const SizeT componentSize = GetDataTypeSize(type); - return componentSize == 0 ? 0 : componentSize * static_cast(size); - } - - // Deinterleaves elementCount elements of componentCount doubles into a tightly packed - // float32 stream - the fetch half of the fp64 demotion the shader side already does - // unconditionally (DemoteFloat64Pass). GL byte strides and offsets are arbitrary, so - // no component carries an 8-byte alignment guarantee and each is copied out before it - // is narrowed. - void NarrowDoubleStreamToFloat32(const Uint8* sourceBase, SizeT sourceStride, SizeT componentCount, - SizeT elementCount, Vector& outData) { - outData.resize(elementCount * componentCount); - for (SizeT element = 0; element < elementCount; ++element) { - const Uint8* sourceElement = sourceBase + element * sourceStride; - Float* destinationElement = outData.data() + element * componentCount; - for (SizeT component = 0; component < componentCount; ++component) { - Double value = 0.0; - Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double)); - destinationElement[component] = static_cast(value); - } - } - } - - // GL_BGRA as a vertex SIZE is desktop-only - ES rejects it outright - and it means - // nothing more than "the element's first and third components are stored swapped". - // So the element is put back in RGBA order and fetched as an ordinary size-4 array of - // the same type: for GL_UNSIGNED_BYTE the first and third BYTES swap, for the two - // 2_10_10_10_REV types the 10-bit fields in bits 0-9 and 20-29 (the core spec's packed - // component layout: with BGRA, x comes from bits 20-29 and z from bits 0-9). The 2-bit - // w field and the middle field stay where they are. Elements are copied word-wise, so - // no source alignment is assumed; the output is tightly packed, one word per element. - void SwizzleBgraElementToRgba(Uint8* element, DataType type) { - if (type == DataType::Uint8) { - std::swap(element[0], element[2]); - return; - } - Uint32 word = 0; - Memcpy(&word, element, sizeof(word)); - constexpr Uint32 kField = 0x3FFu; - const Uint32 low = word & kField; - const Uint32 high = (word >> 20) & kField; - word = (word & ~(kField | (kField << 20))) | high | (low << 20); - Memcpy(element, &word, sizeof(word)); - } - - void SwizzleBgraStreamToRgba(const Uint8* sourceBase, SizeT sourceStride, SizeT elementCount, - DataType type, Vector& outData) { - outData.resize(elementCount * 4); - for (SizeT element = 0; element < elementCount; ++element) { - Uint8* destination = outData.data() + element * 4; - Memcpy(destination, sourceBase + element * sourceStride, 4); - SwizzleBgraElementToRgba(destination, type); - } - } - } // namespace - - BackendVertexArrayObject::BackendVertexArrayObject() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - m_clientAttributeBufferIds.fill(0); - m_convertedAttributeBufferIds.fill(0); - g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId); - if (m_backendVAOId == 0) { - MGLOG_E_ONCE("Failed to generate vertex array object."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } else { - MGLOG_D("Generated vertex array object with ID: %u.", m_backendVAOId); - } - } - - BackendVertexArrayObject::~BackendVertexArrayObject() { - if (InProcessTeardown()) { - return; // see InProcessTeardown(): the driver may be unloaded already - } - if (m_backendVAOId != 0) { - NoteVAOIdDeleted(m_backendVAOId); - g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId); - m_backendVAOId = 0; - } - for (auto& bufferId : m_clientAttributeBufferIds) { - if (bufferId != 0) { - BufferImpl::NoteBufferIdDeleted(bufferId); - g_GLESFuncs.glDeleteBuffers(1, &bufferId); - bufferId = 0; - } - } - for (auto& bufferId : m_convertedAttributeBufferIds) { - if (bufferId != 0) { - BufferImpl::NoteBufferIdDeleted(bufferId); - g_GLESFuncs.glDeleteBuffers(1, &bufferId); - bufferId = 0; - } - } - } - - namespace { - Uint g_boundBackendVAOId = 0; - Bool g_boundBackendVAOKnown = false; - } // namespace - - void BindBackendVAOId(Uint id) { - if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) { - return; - } - g_GLESFuncs.glBindVertexArray(id); - g_boundBackendVAOId = id; - g_boundBackendVAOKnown = true; - } - - void InvalidateVAOBindingCache() { - g_boundBackendVAOKnown = false; - } - - void NoteVAOIdDeleted(Uint id) { - if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) { - g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0 - } - } - - void BackendVertexArrayObject::Bind() const { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - BindBackendVAOId(m_backendVAOId); - } - - // P15: the driver-VAO shadow (Managers.h). Callers have bound this twin's VAO. - namespace { - inline Bool VaoShadowOn() { - return MG_Config::Features.EsprytVaoShadow != MG_Config::QuirkOverride::ForceOff; - } - } // namespace - - void BackendVertexArrayObject::HostSetEnabled(Uint index, Bool enabled) { - const Uint32 bit = 1u << index; - if (VaoShadowOn() && (m_hostEnabledKnownMask & bit) && ((m_hostEnabledMask & bit) != 0) == enabled) return; - if (enabled) g_GLESFuncs.glEnableVertexAttribArray(index); - else g_GLESFuncs.glDisableVertexAttribArray(index); - m_hostEnabledKnownMask |= bit; - m_hostEnabledMask = enabled ? (m_hostEnabledMask | bit) : (m_hostEnabledMask & ~bit); - } - - void BackendVertexArrayObject::HostSetDivisor(Uint index, Uint32 divisor) { - const Uint32 bit = 1u << index; - if (VaoShadowOn() && (m_hostDivisorKnownMask & bit) && m_hostDivisor[index] == divisor) return; - g_GLESFuncs.glVertexAttribDivisor(index, divisor); - m_hostDivisorKnownMask |= bit; - m_hostDivisor[index] = divisor; - } - - void BackendVertexArrayObject::HostForgetAll() { - m_hostEnabledKnownMask = 0; - m_hostDivisorKnownMask = 0; - for (auto& pointer : m_hostPointer) pointer.valid = false; - } - - // ES 3.1 core. Queried through the loader rather than the version, because the whole - // point of using it is to express something the pointer API cannot, and falling back - // silently on a driver that lacks it is better than crashing on a null entry point. - inline Bool HasVertexBindingApi() { - return g_GLESFuncs.glBindVertexBuffer != nullptr && g_GLESFuncs.glVertexAttribFormat != nullptr && - g_GLESFuncs.glVertexAttribIFormat != nullptr && g_GLESFuncs.glVertexAttribBinding != nullptr && - g_GLESFuncs.glVertexBindingDivisor != nullptr; - } - - - Bool BackendUsesNativeBaseInstance() { return g_GLESCapabilities.SupportsBaseInstance; } - - // The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift - // of the array's own offset. Only divisor'd arrays step per instance, so only they move. - // - // baseInstance is added to the ELEMENT index, not to instance/divisor - the divisor - // therefore does not appear here, and the shift is a whole number of strides. - // - // A resolved stride of zero is the binding model's "never advance" (see - // VertexAttribute::Stride), so such an array reads the same element for every instance - // and a baseInstance cannot move it. The arithmetic already yields zero for that case. - inline SizeT BaseInstanceByteShift(const MG_State::GLState::VertexAttribute& attrib, Uint32 baseInstance) { - if (baseInstance == 0 || attrib.Divisor == 0) { - return 0; - } - return static_cast(baseInstance) * static_cast(attrib.Stride); - } - - // ---- the handle arm's small helpers ------------------------------------------- - // - // Each one is its object-shaped counterpart above with the frontend reads replaced by - // the two wire views, and nothing else. Divisor is deliberately NOT on the attribute - // view: it is resolved per binding point and rides in MGPVertexBuffer::Divisor, which - // is where glVertexAttribDivisor reads it (D-G2). - - // The entry of the applied set that feeds ATTRIBUTE `attributeIndex`. - // - // THE KEY IS THE ATTRIBUTE INDEX, NOT MGPVertexAttribWire::BindingIndex, and the two - // live in different spaces. Espryt consumes RESOLVED attributes, so the client emits - // set_vertex_buffers as one entry per attribute slot with - // MGPVertexBuffer::BindingIndex == the attribute index (EmitVertexBuffers in VertexInputEmit.h - - // "Espryt consumes RESOLVED attributes, so the set is one entry per attribute - // slot with BindingIndex == the attribute index"), each carrying that attribute's - // already-folded buffer, stride and divisor. MGPVertexAttribWire::BindingIndex is the - // OTHER thing: the GL binding POINT the attribute was attached to by - // glVertexAttribBinding, which is the key of the binding-point view this arm never - // reads (it needs no separate view, because the resolution already happened on the - // client). The two coincide whenever the attribute was configured through - // glVertexAttribPointer, which is why every scenario that uses the pointer API stayed - // green while glVertexAttribBinding(2, 3) / (0, 5) fetched the wrong entry or none - - // KHR-GL43.vertex_attrib_binding's whole subject. - const MG_Pipe::MGPVertexBuffer* VertexBufferForAttributeIndex(const MG_Pipe::MGPipeApplierState& st, - Uint32 attributeIndex) { - if (st.VertexBufferCount == 0) return nullptr; - const Uint32 begin = st.VertexBufferStart; - const Uint32 end = begin + st.VertexBufferCount; - if (attributeIndex >= begin && attributeIndex < end && - attributeIndex < MG_Pipe::kMGPipeMaxVertexAttribs && - st.VertexBuffers[attributeIndex].BindingIndex == attributeIndex) { - return &st.VertexBuffers[attributeIndex]; - } - for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) { - if (st.VertexBuffers[i].BindingIndex == attributeIndex) return &st.VertexBuffers[i]; - } - return nullptr; - } - - // BaseInstanceByteShift, on the wire views. baseInstance is added to the ELEMENT index, - // so the divisor does not appear here; a resolved stride of zero never advances and the - // arithmetic already yields zero for it. - inline SizeT BaseInstanceByteShiftWire(Int32 stride, Uint32 divisor, Uint32 baseInstance) { - if (baseInstance == 0 || divisor == 0) return 0; - return static_cast(baseInstance) * static_cast(stride); - } - - // (The attribute-buffer bind lives in SyncToBackendFromApplier's pointer path now: it - // resolves the driver id from the slot table and skips the bind and the pointer when the - // driver VAO already holds them. It does NOT ensure storage: on this arm the draw's - // buffers were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw.) - - // SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved - // stride of zero, which glVertexAttribPointer's zero means the opposite of. - inline Bool SyncZeroStrideAttributeByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib, - const MG_Pipe::MGPVertexBuffer& binding) { - if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) { - MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex); - return false; - } - auto* backendResource = BufferImpl::FindBufferResourceForHandle(binding.Res); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex); - return false; - } - if (!attrib.IsInteger) { - const GLint glSize = static_cast(attrib.Size); - g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize, - MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), - attrib.Normalized ? GL_TRUE : GL_FALSE, 0); - } else { - g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast(attrib.Size), - MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), - 0); - } - g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex); - // The resolved offset goes on the binding point, not into a relative offset (the - // relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET, a buffer - // offset is not). BindBufferId is bypassed deliberately. - g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id, - static_cast(attrib.Offset), 0); - return true; - } - - // The record the applier holds for the bound vertex-elements CSO, or null. - const MG_Pipe::MGPipeVertexElementsRecord* BoundVertexElementsRecord( - const MG_Pipe::MGPipeApplierState& st) { - const MG_Pipe::MGPipeHandle cso = st.BoundVertexElements; - if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; - if (cso.Slot >= st.VertexElementsCsos.size()) return nullptr; - const auto& record = st.VertexElementsCsos[cso.Slot]; - if (!record.Live || record.Gen != cso.Gen) return nullptr; - return &record; - } - - // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract; the body is the one - // SamplerImpl::ResolveSamplerCsoTwin already runs, with the sync left to vi. - BackendVertexArrayObject* ResolveVaoTwin(MG_Pipe::MGPipeHandle elements) { - if (MG_Pipe::MGPipeHandleIsNull(elements)) return nullptr; - // THE RECORD FIRST, before the table is touched: a handle with no record means the - // client named a vertex-elements CSO it never described (or one it deleted while a - // binding still names it), and adopting a slot for it would leave a twin nothing - // can ever sync. The applier's own Gen check is what turns a stale handle into this - // null rather than into its successor's record. - if (PipeRecordAt(MG_Pipe::MGPipeApplier().VertexElementsCsos, elements) == nullptr) { - MGLOG_E_ONCE("MGPipe: vertex-elements CSO {%u, %u} has no applier record on the handle " - "arm, so no driver VAO can be built for it and the draw keeps what is bound", - elements.Slot, elements.Gen); - return nullptr; - } - auto* slot = AdoptTwinByHandle(g_backendVertexArrayObjects, elements, "vertex-elements CSO"); - if (slot == nullptr) return nullptr; - if (!*slot) *slot = MakeShared(); - return slot->get(); - } - - // The same function, driven by the applier's records (D-G4). Every branch of the walk - // survives - the enable/disable block, the fp64 narrowing with its Adreno disable, the - // zero-stride binding-API path, the attribute-buffer bind, the BGRA swizzle, the - // pointer/IPointer at the shifted fetch offset and the divisor - and the gate is - // re-keyed onto three server-owned MGGen counters plus the CSO's {slot, gen}: - // - // m_syncedElementsHandle + m_syncedElementsSerial <- config version + the whole - // per-attribute version array - // m_syncedVertexBuffersSerial <- (new) the buffer set's own - // m_syncedIndexSerial <- wrapping Uint16 + identity patch - // m_syncedBufferIdGeneration <- UNCHANGED, server-local, and - // still the only thing that - // catches a driver-id re-mint no - // counter on either side moves - // m_hasConvertedAttribute <- UNCHANGED: a narrowed stream is - // derived from buffer CONTENT, - // which no serial covers - // m_syncedFetchBaseInstance <- no longer in the gate as a - // DIRTY input of its own (a base - // instance change is a - // ContentHash input and so moves - // VertexBuffersSerial), but kept - // as the record of what was last - // EMITTED, which is what the next - // sync has to correct - void BackendVertexArrayObject::SyncToBackendFromApplier() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); - const auto* rec = BoundVertexElementsRecord(st); - if (rec == nullptr) { - MGLOG_E_ONCE("MGPipe: no vertex-elements record is bound, so the driver VAO cannot be " - "configured - nothing emitted create_vertex_elements/bind_vertex_elements"); - return; - } - - const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration; - const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration; - // THIS GATE DEPENDS ON A SERIAL RULE THAT LIVES IN ANOTHER PACKAGE, and it is named - // here because nothing else in this file would say it: MGPipeApplierReset() runs on - // EVERY change of the current GLContext (MG_Impl/Pipe/Tracker.h's `if (m_context != - // &ctx) Reset();`, not only on a fresh one), while this twin SURVIVES the excursion - - // BackendVertexArrayObject has no context-generation member and - // OnBackendContextDestroyed runs on destroy, not on make-current. A reset that sent - // VertexBuffersSerial and IndexBufferSerial back to ZERO would therefore walk them - // back through values this twin has already stamped, and a memo could read clean over - // state the applier had just cleared. MG_Pipe/PipeApply.cpp's reset must ADVANCE - // those two serials instead (wire's C2), which is what makes - // m_syncedVertexBuffersSerial below unable to match after a reset - and that in turn - // forces the whole AND dirty, which is also what rescues the per-record ContentSerial - // half (an applier reset is not a slot recycle, so a re-created CSO at the same - // {slot, gen} restarts its ContentSerial at 1). If that rule is ever reverted, this - // gate is unsafe on any application that changes contexts. - const Bool elementsDirty = bufferIdsRemitted || !m_hasSyncedElements || - !(m_syncedElementsHandle == st.BoundVertexElements) || - m_syncedElementsSerial != rec->ContentSerial; - const Bool attributesDirty = elementsDirty || m_syncedVertexBuffersSerial != st.VertexBuffersSerial; - const Bool indexBufferDirty = bufferIdsRemitted || m_syncedIndexSerial != st.IndexBufferSerial; - // Emulation is server-owned: the client sends the draw's RAW base instance and never - // learns the answer. Applied here as well as in the applier so the decision is the - // same whichever side resolved it first - it is idempotent. - const Uint32 fetchBaseInstance = - BackendUsesNativeBaseInstance() ? 0u : st.VertexFetchBaseInstance; - const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance; - const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedAttribute; - if (!emitAttributes && !indexBufferDirty) { - return; - } - // P14: ONLY THE BUFFER SET MOVED (a Minecraft chunk draw: same layout, next chunk's - // buffer). The driver VAO's enable flags are a function of the configuration alone, - // and the last full walk left them exactly as this configuration wants them, so the - // walk below re-points just the enabled attributes and skips the 32 enable/disable - // calls. Only after a walk in which every enabled attribute took the plain pointer - // path: the fp64, zero-stride, BGRA and no-buffer branches toggle enables or bind - // through other state, and the client-memory upload rewrites the VAO behind this - // gate, so any of them sends the next sync through the whole walk again. - const Bool buffersOnly = emitAttributes && !elementsDirty && m_lastWalkWasPlain && - !m_hasConvertedAttribute; - Bool walkWasPlain = true; - m_hasConvertedAttribute = false; - // Rebuilt by every walk; a buffers-only one follows a plain walk, which set none. - if (emitAttributes && !buffersOnly) m_disabledEnabledAttribMask = 0; - - Bind(); - if (m_hostPointerBufferGeneration != currentBufferIdGeneration) { - for (auto& pointer : m_hostPointer) pointer.valid = false; - m_hostPointerBufferGeneration = currentBufferIdGeneration; - } - - // ALL 32 SLOTS, not rec->AttributeCount, and the difference is the disable arm. - // The legacy walk ran over the frontend's whole 32-slot attribute array and reached - // glDisableVertexAttribArray for every attribute that was off; a walk bounded by the - // record's count leaves an attribute the configuration has just DROPPED enabled in - // the driver VAO, pointing at whatever buffer it last held - which is the class the - // first Adreno workaround below exists to prevent. Nothing in the contract requires - // the client to emit 32 entries (D-H3 sets the precedent the other way for the - // BUFFER set: "truncated to the highest enabled attribute + 1"), so the bound was a - // requirement on B that was neither stated nor pinned. It costs nothing to drop it: - // MGPipeApplyCreateVertexElements zeroes both arrays before it unpacks, so every - // entry past AttributeCount reads Enabled = 0, Type = 0 (Int8, not Float64) and - // IsLong = 0 - i.e. exactly "disable this array", with no dependency on B at all. - for (Uint attribIndex = 0; attribIndex < MG_Pipe::kMGPipeMaxVertexAttribs && emitAttributes; - ++attribIndex) { - const MGPVertexAttribWire& attrib = rec->Attributes[attribIndex]; - if (buffersOnly && !attrib.Enabled) continue; - const MG_Pipe::MGPVertexBuffer* binding = - VertexBufferForAttributeIndex(st, attribIndex); - const Uint32 divisor = binding != nullptr ? binding->Divisor : 0u; - - // The enable/disable block. On this arm there is no per-attribute version to - // compare: the applier's Attributes[] IS what was last pushed, so a moved - // ContentSerial means re-emit and an unchanged one means the early-out above - // already returned. A buffers-only walk leaves the flags as they are (above). - if (!buffersOnly) HostSetEnabled(attribIndex, attrib.Enabled != 0); - - // The fp64 narrowing, verbatim in behaviour including the Adreno workaround: - // when no float32 stream can be built the array is DISABLED rather than left - // enabled with no pointer, which is what the driver turns into a SIGSEGV inside - // the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case4). IsLong is - // carried separately from Type == Float64 on the wire precisely so this test - // can still tell the two apart. - // - // A GL_BGRA-sized array takes the same stream, swizzled instead of narrowed, and - // the same disable when it cannot be built: GL_BGRA as a vertex SIZE is - // desktop-only, ES rejects it, and the rejected call leaves the array ENABLED - // with no pointer - the shape the Adreno driver dereferences null on inside the - // next draw (KHR-GL43.vertex_attrib_binding.basic-input-case5). - const Bool bgraArray = attrib.IsBgra != 0 && !attrib.IsInteger; - if (attrib.IsLong || attrib.Type == static_cast(DataType::Float64) || bgraArray) { - walkWasPlain = false; - const Bool convertible = bgraArray || attrib.Type == static_cast(DataType::Float64); - if (attrib.Enabled && convertible && binding != nullptr && - SyncConvertedAttributeByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) { - m_hasConvertedAttribute = true; - // The conversion pointed the attribute at its own stream (and may have - // used the binding API), so the pointer and divisor shadow are stale. - // An earlier walk that could not build the stream disabled this array. - m_hostPointer[attribIndex].valid = false; - m_hostDivisorKnownMask &= ~(1u << attribIndex); - HostSetEnabled(attribIndex, true); - HostSetDivisor(attribIndex, divisor); - continue; - } - if (attrib.Enabled) { - MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a %s array whose source stream could not " - "be converted - disabling the array", - attribIndex, bgraArray ? "GL_BGRA" : "64-bit (GL_DOUBLE)"); - // The draw now reads the attribute's current generic value, which the - // application had no reason to set for an enabled array. - m_disabledEnabledAttribMask |= 1u << attribIndex; - } - HostSetEnabled(attribIndex, false); - continue; - } - - if (!attrib.Enabled) continue; - if (binding == nullptr) { - walkWasPlain = false; - continue; - } - - // A resolved stride of zero is the binding model's "never advance" and - // glVertexAttribPointer cannot say it - its zero means "tightly packed", i.e. - // the opposite. ES 3.1's binding-point API can. - if (attrib.Stride == 0 && HasVertexBindingApi()) { - walkWasPlain = false; - // The binding API rewrote this attribute's format and binding: whatever the - // shadow said about its pointer no longer holds. - m_hostPointer[attribIndex].valid = false; - if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) { - continue; - } - // No shift here on purpose: a zero stride never advances. Binding index == - // attribute index here, so this IS the attribute's divisor for the shadow. - g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor); - m_hostDivisorKnownMask |= 1u << attribIndex; - m_hostDivisor[attribIndex] = divisor; - continue; - } - - if (MG_Pipe::MGPipeHandleIsNull(binding->Res)) { - MGLOG_W_ONCE("Attribute has no bound buffer, skipping."); - walkWasPlain = false; - continue; - } - const auto* attribBuffer = BufferImpl::FindBufferResourceForHandle(binding->Res); - if (!attribBuffer || attribBuffer->id == 0) { - MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute."); - walkWasPlain = false; - continue; - } - - const SizeT fetchOffset = static_cast(attrib.Offset) + - BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance); - const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)); - const GLint glSize = static_cast(attrib.Size); - - // The pointer the driver VAO already holds for this attribute - the buffer it - // captured included - needs no glBindBuffer + pointer call. - auto& shadowPointer = m_hostPointer[attribIndex]; - const Bool pointerHeld = VaoShadowOn() && shadowPointer.valid && - shadowPointer.bufferId == attribBuffer->id && shadowPointer.size == glSize && - shadowPointer.type == static_cast(glType) && - shadowPointer.stride == static_cast(attrib.Stride) && - shadowPointer.offset == fetchOffset && - shadowPointer.normalized == (attrib.Normalized != 0) && - shadowPointer.integer == (attrib.IsInteger != 0); - if (!pointerHeld) { - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, attribBuffer->id); - if (!attrib.IsInteger) { - g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType, - attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, - (const void*)fetchOffset); - } else { - g_GLESFuncs.glVertexAttribIPointer(attribIndex, glSize, glType, attrib.Stride, - (const void*)fetchOffset); - } - shadowPointer = {attribBuffer->id, glSize, static_cast(glType), - static_cast(attrib.Stride), fetchOffset, attrib.Normalized != 0, - attrib.IsInteger != 0, true}; - } - - HostSetDivisor(attribIndex, divisor); - } - - if (indexBufferDirty) { - Bool indexBufferSynced = false; - if (!MG_Pipe::MGPipeHandleIsNull(st.IndexBuffer.Res)) { - // RESOLVE ONLY, no ensure - unlike the legacy arm, which called - // EnsureBufferResource here. PrepareForDraw runs SyncNeccessaryBuffers before - // SyncCurrentVAO, so an INDEXED draw has already ensured this store; a - // non-indexed draw whose IndexBufferSerial moved has not, and then this logs - // once and leaves the binding alone. That is noisy rather than wrong: nothing - // is stamped on the miss (indexBufferSynced stays false), so the next indexed - // draw repairs it. Ensuring here would need the frontend object this arm - // deliberately does not hold. - auto* backendResource = BufferImpl::FindBufferResourceForHandle(st.IndexBuffer.Res); - if (backendResource && backendResource->id != 0) { - BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id); - indexBufferSynced = true; - } else { - MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer."); - } - } else { - g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - indexBufferSynced = true; - } - if (indexBufferSynced) { - m_syncedIndexRes = st.IndexBuffer.Res; - m_syncedIndexValid = true; - // The two element-array restore scopes (the restart substitution's and - // MultiDrawImpl's) put the DRIVER id back without touching this serial, - // which is correct: the serial records what the APPLIER last said, and - // those scopes restored exactly what the applier said. - m_syncedIndexSerial = st.IndexBufferSerial; - } - } - - if (attributesDirty) { - m_syncedElementsHandle = st.BoundVertexElements; - m_syncedElementsSerial = rec->ContentSerial; - m_syncedVertexBuffersSerial = st.VertexBuffersSerial; - m_hasSyncedElements = true; - // The window this walk emitted, for AdoptUnchangedBufferSets. - m_syncedBufferStart = st.VertexBufferStart; - m_syncedBufferCount = std::min(st.VertexBufferCount, - MG_Pipe::kMGPipeMaxVertexAttribs - st.VertexBufferStart); - m_syncedBufferBaseInstance = st.VertexFetchBaseInstance; - for (Uint32 i = 0; i < m_syncedBufferCount; ++i) - m_syncedBuffers[i] = st.VertexBuffers[m_syncedBufferStart + i]; - m_syncedBuffersValid = st.VertexBufferStart <= MG_Pipe::kMGPipeMaxVertexAttribs; - m_syncedContextSerial = st.ContextSerial; - } - if (emitAttributes) { - m_syncedFetchBaseInstance = fetchBaseInstance; - m_lastWalkWasPlain = walkWasPlain; - } - m_syncedBufferIdGeneration = currentBufferIdGeneration; - } - - void BackendVertexArrayObject::AdoptUnchangedBufferSets(const MG_Pipe::MGPipeApplierState& st) { - if (!m_hasSyncedElements || m_syncedContextSerial != st.ContextSerial) return; - if (m_syncedBuffersValid && m_syncedVertexBuffersSerial != st.VertexBuffersSerial && - st.VertexBufferStart == m_syncedBufferStart && st.VertexFetchBaseInstance == m_syncedBufferBaseInstance && - std::min(st.VertexBufferCount, MG_Pipe::kMGPipeMaxVertexAttribs - st.VertexBufferStart) == - m_syncedBufferCount) { - Bool same = true; - for (Uint32 i = 0; same && i < m_syncedBufferCount; ++i) { - const auto& a = m_syncedBuffers[i]; - const auto& b = st.VertexBuffers[m_syncedBufferStart + i]; - same = a.Res == b.Res && a.Offset == b.Offset && a.Stride == b.Stride && - a.Divisor == b.Divisor && a.BindingIndex == b.BindingIndex; - } - if (same) { - if (m_resolvedDrawBuffers.valid && m_resolvedDrawBuffers.buffersSerial == m_syncedVertexBuffersSerial) - m_resolvedDrawBuffers.buffersSerial = st.VertexBuffersSerial; - m_syncedVertexBuffersSerial = st.VertexBuffersSerial; - } - } - if (m_syncedIndexValid && m_syncedIndexSerial != st.IndexBufferSerial && m_syncedIndexRes == st.IndexBuffer.Res) { - if (m_resolvedDrawBuffers.iboSerial == m_syncedIndexSerial) m_resolvedDrawBuffers.iboSerial = st.IndexBufferSerial; - m_syncedIndexSerial = st.IndexBufferSerial; - } - } - - void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays( - const SharedPtr& stateVAOObject, GLint first, GLsizei count, - Uint32 fetchBaseInstance) { - if (!stateVAOObject || count <= 0 || first < 0) { - return; - } - // This rewrites the driver VAO's arrays behind SyncToBackendFromApplier's gate, so - // the next sync must not take its buffers-only walk, nor adopt an equal buffer set. - m_lastWalkWasPlain = false; - m_syncedBuffersValid = false; - HostForgetAll(); // the uploads below set enables, pointers and divisors directly - - Bind(); - - const auto& allAttributes = stateVAOObject->GetAllAttributes(); - for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { - const auto& attrib = allAttributes[attribIndex]; - if (!attrib.Enabled || attrib.Buffer) { - continue; - } - - // Same narrowing as SyncToBackend (the long note lives there), with the draw's own - // fetch range standing in for the buffer extent a client array does not have. The - // upload starts at element 0 so `first` keeps indexing the stream, exactly as the - // unconverted upload below does. The test is on the storage rather than on IsLong - // because glVertexAttribFormat(GL_DOUBLE) is 64-bit data without being long. - if (attrib.IsLong || attrib.Type == DataType::Float64) { - const auto* sourceBase = reinterpret_cast(attrib.Offset); - if (attrib.Type != DataType::Float64 || sourceBase == nullptr || attrib.Size < 1 || - attrib.Size > 4) { - g_GLESFuncs.glDisableVertexAttribArray(attribIndex); - continue; - } - - auto& bufferId = m_clientAttributeBufferIds[attribIndex]; - if (bufferId == 0) { - g_GLESFuncs.glGenBuffers(1, &bufferId); - if (bufferId == 0) { - MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer."); - g_GLESFuncs.glDisableVertexAttribArray(attribIndex); - continue; - } - } - - const SizeT componentCount = static_cast(attrib.Size); - const SizeT sourceElementSize = componentCount * sizeof(Double); - // A client array only ever reaches here through a pointer call, whose stride 0 - // the frontend already resolved to the element size, so this is a guard rather - // than a case (VertexAttribute::Stride). - const SizeT sourceStride = - attrib.Stride > 0 ? static_cast(attrib.Stride) : sourceElementSize; - const SizeT elementCount = static_cast(first) + static_cast(count); - Vector converted; - NarrowDoubleStreamToFloat32(sourceBase, sourceStride, componentCount, elementCount, converted); - - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); - g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, - static_cast(converted.size() * sizeof(Float)), - converted.data(), GL_STREAM_DRAW); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, - static_cast(converted.size() * sizeof(Float))); - } - // GL ignores `normalized` for floating-point array types, so it is not - // forwarded here either. - g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE, - static_cast(componentCount * sizeof(Float)), - // The converted stream is tightly packed, so a divisor'd array's - // baseInstance shift is a whole number of CONVERTED elements - - // not BaseInstanceByteShift's source-stride arithmetic. - reinterpret_cast( - attrib.Divisor == 0 ? 0u : static_cast(fetchBaseInstance) * - componentCount * - sizeof(Float))); - // glVertexAttribPointer's final argument IS that shift. A client array - // declared through the pointer API keeps element 0 at the buffer's byte 0, so - // the shift rides in the pointer argument the call above passes. - // The float64 narrowing itself is shared with SyncToBackend's conversion - // branch; only the fetch RANGE differs, and the draw's is the accurate one. - g_GLESFuncs.glEnableVertexAttribArray(attribIndex); - continue; - } - - const auto* clientData = reinterpret_cast(attrib.Offset); - const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra); - if (!clientData || elementSize == 0 || attrib.Size <= 0) { - continue; - } - - const SizeT stride = attrib.Stride > 0 ? static_cast(attrib.Stride) : elementSize; - const SizeT uploadSize = static_cast(first + count - 1) * stride + elementSize; - - auto& bufferId = m_clientAttributeBufferIds[attribIndex]; - if (bufferId == 0) { - g_GLESFuncs.glGenBuffers(1, &bufferId); - if (bufferId == 0) { - MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer."); - continue; - } - } - - // A GL_BGRA array is swizzled into RGBA order on the way, as on the buffer path - // (SyncConvertedAttributeByHandle): ES has no BGRA vertex size to declare. - const Bool bgra = attrib.IsBgra && !attrib.IsInteger; - Vector swizzled; - if (bgra) { - swizzled.assign(clientData, clientData + uploadSize); - for (SizeT element = 0; element < static_cast(first + count); ++element) { - SwizzleBgraElementToRgba(swizzled.data() + element * stride, attrib.Type); - } - } - - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); - g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(uploadSize), - bgra ? static_cast(swizzled.data()) : clientData, - GL_STREAM_DRAW); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, - static_cast(uploadSize)); - } - - if (!attrib.IsInteger) { - const GLint glSize = bgra ? 4 : attrib.Size; - g_GLESFuncs.glVertexAttribPointer( - attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type), - attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast(stride), - // A client array's element 0 sits at its buffer's byte 0, so the - // divisor'd arrays' baseInstance shift is exactly BaseInstanceByteShift - - // the same arithmetic SyncToBackend applies to a VBO-backed array. - reinterpret_cast(BaseInstanceByteShift(attrib, fetchBaseInstance))); - } else { - g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size, - MG_Util::ConvertDataTypeToGLEnum(attrib.Type), - static_cast(stride), - // See the pointer call above: the divisor'd - // arrays' baseInstance shift is the pointer. - reinterpret_cast( - BaseInstanceByteShift(attrib, fetchBaseInstance))); - } - } - - } - - - Bool BackendVertexArrayObject::SyncConvertedAttributeByHandle( - Uint attribIndex, const MGPVertexAttribWire& attrib, const MG_Pipe::MGPVertexBuffer& binding, - Uint32 fetchBaseInstance) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (attribIndex >= m_convertedAttributeBufferIds.size() || attrib.Size < 1 || attrib.Size > 4) { - return false; - } - // Which of the two conversions: a BGRA array is a 4-byte element whatever its type, - // and only the three types GL allows with it reach here (the frontend refuses the - // rest at the format call). - const DataType sourceType = static_cast(attrib.Type); - const Bool bgra = attrib.IsBgra != 0; - if (bgra && sourceType != DataType::Uint8 && sourceType != DataType::Int2101010Rev && - sourceType != DataType::Uint2101010Rev) { - return false; - } - if (!bgra && sourceType != DataType::Float64) return false; - if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) { - // A client-memory 64-bit array is narrowed on the draw path instead, which is - // the only place its fetch range is known. - return false; - } - auto* resource = BufferImpl::FindBufferResourceForHandle(binding.Res); - if (resource == nullptr) return false; - - // WHAT IS NOT HERE, AND IT IS A RULED DEVIATION FOR P3a RATHER THAN AN OVERSIGHT - // (M-3; integrator ruling, this commit). - // - // The legacy arm opens with bufferObject->SyncGpuWrites(), one of the eleven Espryt - // SyncPersistentMappedRange / SyncGpuWrites sites D-N keeps where they are for P3a. - // D-N's wording is "no MOVE of those sites off the frontend", and this arm does not - // move it: it CANNOT MAKE IT AT ALL. The call needs a frontend BufferObject and this - // path holds only a handle, because the server has no inverse map back to a frontend - // object - that absence is the design, not a gap in it (ARCHITECTURE.md 4.2). The - // two ways to keep the site here would each break something D-N or D-J protects: a - // handle -> object map is the very thing the split removes, and pulling the bytes - // eagerly on the client at every draw is new behaviour and new cost. - // - // SO THE DEVIATION IS DECLARED, WITH ITS BLAST RADIUS. Under the default mask a - // 64-bit vertex array whose SOURCE buffer was written by a shader and not yet pulled - // back narrows STALE bytes on this arm and fresh bytes on the legacy one. That is - // the whole of it: fp64 vertex arrays, fed by a buffer a shader wrote, read without - // any intervening explicit readback. No other attribute type reads through this - // path, and a persistently mapped source is excluded separately below (the memo - // never trusts one, so those re-read every draw through the coherent map). - // - // P8-C CLOSES IT UNDER A TRANSPORT, on the server rather than by moving the pull to the - // client: a source a shader wrote on this side is read back from this server's own GL - // buffer (SplitHostBytesForCpuRead), and that write also voids the memo below, whose - // serial a GPU write never moves. The MONOLITH arm of this function keeps the - // deviation (its hostBytes is the client's shadow and its mark is the frontend - // object's); that half is dev's (ID-P8-3). - // WHERE THE BYTES ARE, and it is not one fixed place: an ADOPTED store has no client - // shadow left at all (PipeResource::AdoptPersistentMap clears and shrinks it), so - // the coherent map IS the source of truth - which is exactly the case the memo - // exclusion below makes the most frequent, since a persistently mapped source is - // never trusted and this narrowing therefore re-reads on every draw. Reading the - // recorded shadow base for such a resource read freed memory; hostBytes is nulled - // at adoption for the same reason, so the fallback below is a null and a refusal - // rather than a stale read. - const Bool sourceGpuWritten = - resource->serverGpuWritten; - if (sourceGpuWritten) { - BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res, - bgra ? "bgra_vertex_swizzle" : "fp64_vertex_narrowing"); - } - const Uint8* const sourceBase = - (resource->persistentMapped && resource->persistentPtr != nullptr) - ? static_cast(resource->persistentPtr) - : resource->hostBytes; - const SizeT sourceSize = BufferImpl::ResourceWidthForHandle(binding.Res); - if (sourceBase == nullptr || attrib.Offset >= sourceSize) { - return false; - } - - // A BGRA element is always one 32-bit word, and so is its RGBA replacement; a 64-bit - // element narrows to the float32 vector of the same component count. - const SizeT componentCount = static_cast(attrib.Size); - const SizeT sourceElementSize = bgra ? 4 : componentCount * sizeof(Double); - const SizeT convertedElementSize = bgra ? 4 : componentCount * sizeof(Float); - const SizeT available = sourceSize - static_cast(attrib.Offset); - if (available < sourceElementSize) { - return false; - } - - const Bool neverAdvances = attrib.Stride <= 0; - const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast(attrib.Stride); - const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1; - - const SizeT firstElement = (fetchBaseInstance != 0 && binding.Divisor != 0 && !neverAdvances) - ? static_cast(fetchBaseInstance) - : 0; - if (firstElement >= elementCount) { - return false; - } - - auto& stream = m_convertedAttributeStreams[attribIndex]; - Uint& convertedBufferId = m_convertedAttributeBufferIds[attribIndex]; - const Uint64 sourceSerial = BufferImpl::ResourceSerialForHandle(binding.Res); - // The memo, re-keyed: the pin is the source buffer's {slot, gen} instead of a - // frontend lifetime id, and the freshness input is the applier's server-owned - // Serial instead of the frontend change serial. A PERSISTENTLY MAPPED source is - // still never trusted - it is written through the pointer with no call at all, so - // no serial on either side can prove the converted copy is current. The conversion - // itself is part of the key: one attribute index can carry both shapes over its life, - // over the same bytes. - const Bool memoHit = stream.valid && convertedBufferId != 0 && !resource->persistentMapped && - !sourceGpuWritten && stream.sourceHandle == binding.Res && - stream.sourceChangeSerial == sourceSerial && - stream.sourceOffset == static_cast(attrib.Offset) && - stream.sourceStride == sourceStride && stream.bgra == bgra && - stream.sourceType == attrib.Type && stream.componentCount == componentCount && - stream.elementCount == elementCount; - if (!memoHit) { - if (convertedBufferId == 0) { - g_GLESFuncs.glGenBuffers(1, &convertedBufferId); - if (convertedBufferId == 0) { - MGLOG_E_ONCE("Failed to create the scratch buffer for the converted vertex array at " - "attribute %u.", - attribIndex); - return false; - } - } - Vector narrowed; - Vector swizzled; - const void* convertedData = nullptr; - SizeT convertedBytes = 0; - if (bgra) { - SwizzleBgraStreamToRgba(sourceBase + attrib.Offset, sourceStride, elementCount, sourceType, - swizzled); - convertedData = swizzled.data(); - convertedBytes = swizzled.size(); - } else { - NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, - elementCount, narrowed); - convertedData = narrowed.data(); - convertedBytes = narrowed.size() * sizeof(Float); - } - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId); - g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(convertedBytes), convertedData, - GL_STREAM_DRAW); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, - static_cast(convertedBytes)); - } - stream.valid = true; - stream.sourceHandle = binding.Res; - stream.sourceChangeSerial = sourceSerial; - stream.sourceOffset = static_cast(attrib.Offset); - stream.sourceStride = sourceStride; - stream.bgra = bgra; - stream.sourceType = attrib.Type; - stream.componentCount = componentCount; - stream.elementCount = elementCount; - MGLOG_D("DirectGLES: converted the %s vertex array at attribute %u, %zu element(s).", - bgra ? "BGRA" : "64-bit", attribIndex, elementCount); - } - - // What the converted stream is to the driver. The swizzled BGRA stream keeps its - // source type and `normalized` (GL accepts BGRA only normalized, and a packed type's - // signedness is the type's own). The narrowed stream is float32, and `normalized` is - // deliberately GL_FALSE there rather than attrib.Normalized: GL ignores it for - // floating-point array types, and honouring it would scale the fetched values. - const GLint convertedSize = bgra ? 4 : static_cast(attrib.Size); - const GLenum convertedType = bgra ? MG_Util::ConvertDataTypeToGLEnum(sourceType) : GL_FLOAT; - const GLboolean convertedNormalized = (bgra && attrib.Normalized) ? GL_TRUE : GL_FALSE; - if (neverAdvances) { - // Only the binding-point API can say "stride 0". - if (!HasVertexBindingApi()) { - return false; - } - g_GLESFuncs.glVertexAttribFormat(attribIndex, convertedSize, convertedType, convertedNormalized, 0); - g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex); - g_GLESFuncs.glBindVertexBuffer(attribIndex, convertedBufferId, 0, 0); - return true; - } - - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId); - g_GLESFuncs.glVertexAttribPointer(attribIndex, convertedSize, convertedType, convertedNormalized, - static_cast(convertedElementSize), - (const void*)(firstElement * convertedElementSize)); - return true; - } - - TwinRegistry - g_backendVertexArrayObjects; - } // namespace VertexArrayImpl - namespace TextureImpl { - BackendTextureObject::BackendTextureObject() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - g_GLESFuncs.glGenTextures(1, &m_backendTextureId); - m_contextGeneration = g_backendContextGeneration; - if (m_backendTextureId == 0) { - MGLOG_E_ONCE("Failed to generate texture object."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } else { - MGLOG_D("Generated texture object with ID: %u.", m_backendTextureId); - } - } - - BackendTextureObject::~BackendTextureObject() { - if (InProcessTeardown()) { - return; // see InProcessTeardown(): the driver may be unloaded already - } - if (m_backendTextureId == 0) { - return; - } - // Scrub every driver-state shadow that could false-skip when the name - // or this heap address is recycled - regardless of whether the id can - // still be deleted. - ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); - for (auto& unitCache : g_boundTexturesCache) { - for (auto& boundTexture : unitCache) { - if (boundTexture == this) { - boundTexture = nullptr; - } - } - } - // TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes. - // Discriminator for the mali-mem-purge hiccup theory: never hand the - // driver a texture free, so the purge daemon has nothing to reclaim. - static const Bool s_expLeakTextureDeletes = [] { - FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb"); - if (!f) return false; - std::fclose(f); - return true; - }(); - if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures && - !s_expLeakTextureDeletes) { - g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); - if (m_bufferImageSplitViewId != 0) { - g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId); - } - } - m_backendTextureId = 0; - m_bufferImageSplitViewId = 0; - } - - void BackendTextureObject::Bind(GLenum target, Uint unit) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (g_activeTextureUnit != unit) { - ActivateTextureUnit(unit); - } - - auto targetN = static_cast(MG_Util::ConvertGLEnumToTextureTarget(target)); - if (this == g_boundTexturesCache[unit][targetN]) return; - - g_GLESFuncs.glBindTexture(target, m_backendTextureId); - g_boundTexturesCache[unit][targetN] = this; - } - - Uint BackendTextureObject::GetBackendTextureId() const { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - return m_backendTextureId; - } - - // P5e (tx2), CONTRACT-P5E §5.2 / scout G-S2-5. See the declaration. - void BackendTextureObject::RequireImageBindableStorageByHandle( - MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record) { - if (m_imageBindableStorageRequired) { - return; - } - // P13 W4b (defect h): monolith's record arm takes the body too; its old arm only - // cleared m_isInitialized and replayed the frontend shadow at the next sync. - const auto format = static_cast(record.Desc.InternalFormat); - // ES image binding needs immutable storage, but an already - // immutable core-format allocation needs no replacement. Keep - // its GPU contents and native name; normal serial/shape checks - // below still consume pending uploads and real redefinitions. - if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) { - m_imageBindableStorageRequired = true; - m_forceTextureParamsResync = true; - return; - } - // P9 W2 (docs/archive/notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE - // SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies, - // generated mips, image stores) and the staged store holds what the client uploaded; - // the client is never asked, so no pull protocol exists. Reading only the store would - // be wrong: a GPU-written level's store run is stale or absent - both host corpus - // re-mints (iris-photon, iris-derivative) are render targets with no staged bytes. - const Bool hadBackendStorage = m_isInitialized; - // The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be - // produced on the host (llvmpipe renders every image format), so the test lane forces - // one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs. - static const Bool s_forceReadbackFailure = - std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr; - auto& store = MG_Record::ServerStagedTexture(); - const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); - const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); - GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE; - MG_Util::TextureFormatProcessor::NormalizePixelFormat( - MG_Util::ConvertTextureInternalFormatToGLEnum(format), {}, nullptr, - &canonicalFormat, &canonicalType); - for (const auto& uploadTarget : - BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) { - for (Uint32 level = 0; level < record.Desc.Levels; ++level) { - const auto targetCode = static_cast(uploadTarget); - const auto levelCode = static_cast(level); - const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode); - if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue; - const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode); - const Bool covered = store.IsCovered(key, targetCode, levelCode); - const MG_Pipe::MGPBox whole{0, 0, 0, static_cast(extent.x()), - static_cast(extent.y()), static_cast(extent.z())}; - // A genuinely full pending write owns every byte. A - // union box covering the level is not sufficient when - // its explicit rectangles leave GPU-written gaps. - const auto coversWhole = [&](const auto& box) { - return box.X == 0 && box.Y == 0 && box.Z == 0 && - box.W == whole.W && box.H == whole.H && box.D == whole.D; - }; - const Bool pendingWhole = pending && (pending->Regions.empty() - ? coversWhole(pending->UnionBox) - : std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole)); - // W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's - // Defined-ness follows the client's respecifies, the driver's follows this - // twin's last sync, and the two differ for a level defined (null data, or null - // data plus a partial upload) after that sync: the driver has no such level, so - // the GPU cannot have written it. A covered level then replays its staged run - // (the client's whole-level shadow, pending boxes included) and an uncovered one - // is undefined content with nothing to preserve. Reading it anyway was - // Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL. - const Bool nativeLevel = - m_isInitialized && - NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget, - static_cast(level), extent); - if (nativeLevel && !(covered && pendingWhole)) { - Vector snapshot; - const Bool readBack = - !s_forceReadbackFailure && - ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format, - static_cast(level), extent, - /*sourceUsesImageCarrier=*/false, canonicalFormat, - canonicalType, snapshot); - if (!readBack && !covered) { - // The GPU holds this level, the driver will not hand it back, and no - // CPU byte of it exists anywhere: the one case with no honest answer. - MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u", - res.Slot, res.Gen, level); - std::abort(); - } - if (!readBack) { - // W2-b: the driver refused the readback (a format it cannot attach - // to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm - // without EXT_texture_norm16, float without EXT_color_buffer_float) - // but the store covers the level. Its staged run is exactly what the - // monolith re-mint replays; texels the GPU wrote since that upload - // are not preserved, which is monolith's answer too. - MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver " - "refused the readback (internal format %u); replaying the level's " - "staged bytes instead (remint-readback-fallback)", - res.Slot, res.Gen, level, record.Desc.InternalFormat); - } - if (readBack && pending) { - // Native contents include prior draws/image - // writes; the pending stage owns ONLY its boxes. - // Merge those boxes before adopting a whole-level - // snapshot, never overwrite untouched GPU texels - // with the rest of a stale client staging run. - const SizeT pixels = static_cast(extent.x()) * extent.y() * extent.z(); - const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode); - if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) { - MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}"); - std::abort(); - } - const SizeT bpp = stagedSize / pixels; - const Uint8* staged = store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion"); - const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { - if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W || - Uint64(y) + h > whole.H || Uint64(z) + d > whole.D) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}"); - std::abort(); - } - for (Uint32 slice = 0; slice < d; ++slice) { - for (Uint32 row = 0; row < h; ++row) { - const SizeT offset = ((static_cast(z + slice) * whole.H + y + row) * whole.W + x) * bpp; - Memcpy(snapshot.data() + offset, staged + offset, static_cast(w) * bpp); - } - } - }; - if (pending->Regions.empty()) { - const auto& box = pending->UnionBox; - merge(box.X, box.Y, box.Z, box.W, box.H, box.D); - } else { - for (const auto& box : pending->Regions) - merge(box.X, box.Y, box.Z, box.W, box.H, box.D); - } - } - if (readBack) { - store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size()); - } - } - // Undefined contents have no byte run to preserve. A - // first allocation can still be image-bindable without - // inventing an upload or consulting the frontend. - if (!store.IsCovered(key, targetCode, levelCode)) continue; - if (!RearmPipeTextureLevelUpload(res, - MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast(uploadTarget)), - levelCode, whole)) { - MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}"); - std::abort(); - } - } - } - // W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as - // on the frontend one (which counts in RequireImageBindableStorage). The keep path - // above is not a re-mint and returns before this; a first allocation holds nothing. - if (hadBackendStorage && MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1); - } - // Every defined byte now belongs to the server and every required - // replay is in its pending set. The existing upload path builds and - // fills the immutable/widened carrier from those records. - m_imageBindableStorageRequired = true; - m_isInitialized = false; - // The widened carrier's swizzle override, exactly as the frontend overload's tail - // sets it and for the same reason: the parameter sync is gated on a params version - // this transition does not move. - m_forceTextureParamsResync = true; - } - - // Replaces one level's shadow bytes with what the driver texture holds for it, then copies - // back the client writes the driver has not been sent yet - exactly the texels written - // (the level's dirty footprint), not the union box or the rect list, which also span texels - // the client never wrote and which only the driver's copy holds correctly. Returns false, - // leaving the shadow untouched, when the driver's copy cannot stand in for the shadow: the - // client wrote every texel of the level, the level is stored compressed, the driver has no - // image at this level and extent (defined after the last sync), or the readback cannot be - // served (a multisample image, a format the driver will not attach to a framebuffer). - // - // P11 M, `pipePending` (push builds): THE CLIENT WRITES THE DRIVER HAS NOT BEEN SENT MAY - // LIVE IN THE APPLIER'S PENDING SET INSTEAD. On the texture handle arm the client clears its - // own dirty flags when it emits a level (D-D5), so the frontend model below answers "clean" - // for a write that is still only in the shadow; the entry the applier accumulated for this - // (uploadTarget, level) names the same shadow-grid boxes. Both are merged over the - // snapshot. Only a box-only entry whose box IS the level owns every byte of it (dev's rule: - // a union box spanning the level with gaps in it is not a whole-level write); an entry with - // regions never short-circuits, since accumulated regions may overlap and their texels do - // not add up - the readback and the merge below give the same bytes when they do cover it. - // Null on the pull build's arm and the legacy arm. - static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture, - const MG_State::GLState::ITextureObject& stateTextureObject, - MG_State::GLState::TextureObjectMipmap& mipmapObject, - TextureUploadTarget uploadTarget, Uint level - , - const MG_Pipe::MGPipeResourceRecord::PendingUpload* pipePending - ) { - const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level); - const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)}; - const SizeT texelCount = - static_cast(extent.x()) * static_cast(extent.y()) * static_cast(extent.z()); - Vector pending; - mipmapObject.GetStorageDirtyFootprint(uploadTarget, level, pending); - // Disjoint boxes, so their texels add up; a union box spanning the level is not enough. - SizeT pendingTexels = 0; - for (const auto& box : pending) { - pendingTexels += box.TexelCount(); - } - if (!pending.empty() && pendingTexels >= texelCount) return false; - // Every box the entry names, checked against the level before anything is read: a box - // outside it is a corrupt record, and the answer is the pre-readback one (keep the - // shadow) rather than a copy out of bounds. - const auto pipeBoxInLevel = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { - return x >= 0 && y >= 0 && z >= 0 && Uint64(x) + w <= Uint64(extent.x()) && - Uint64(y) + h <= Uint64(extent.y()) && Uint64(z) + d <= Uint64(extent.z()); - }; - if (pipePending != nullptr) { - const auto& box = pipePending->UnionBox; - if (!pipeBoxInLevel(box.X, box.Y, box.Z, box.W, box.H, box.D)) { - MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's box lies outside the level; the " - "image-bindable re-mint replays the shadow as it stands", - stateTextureObject.GetExternalIndex(), level); - return false; - } - for (const auto& region : pipePending->Regions) { - if (!pipeBoxInLevel(region.X, region.Y, region.Z, region.W, region.H, region.D)) { - MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's region lies outside the " - "level; the image-bindable re-mint replays the shadow as it stands", - stateTextureObject.GetExternalIndex(), level); - return false; - } - } - if (pipePending->Regions.empty() && box.X == 0 && box.Y == 0 && box.Z == 0 && - box.W == static_cast(extent.x()) && box.H == static_cast(extent.y()) && - box.D == static_cast(extent.z())) { - return false; - } - } - if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false; - if (!g_GLESFuncs.glGetTexLevelParameteriv) return false; - - const TextureTarget target = stateTextureObject.GetTarget(); - const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(target); - const GLenum faceTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - const IntVec3 backendExtent = GetBackendUploadSize(target, texelSize); - GLint driverWidth = 0, driverHeight = 0, driverDepth = 0; - backendTexture.Bind(nativeTarget); - g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_WIDTH, &driverWidth); - g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_HEIGHT, - &driverHeight); - g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_DEPTH, &driverDepth); - if (driverWidth != backendExtent.x() || driverHeight != backendExtent.y() || - std::max(driverDepth, 1) != std::max(backendExtent.z(), 1)) { - return false; - } - - const TextureInternalFormat format = stateTextureObject.GetFormat(); - GLenum shadowFormat = GL_NONE, shadowType = GL_NONE; - MG_Util::TextureFormatProcessor::NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(format), - {}, nullptr, &shadowFormat, &shadowType); - Vector snapshot; - if (!ReadTextureLevelTight(backendTextureId, target, uploadTarget, format, static_cast(level), - texelSize, /*sourceUsesImageCarrier=*/false, shadowFormat, shadowType, - snapshot)) { - return false; - } - const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level); - auto* shadow = static_cast(mipmapObject.MapMipmapData(uploadTarget, level)); - if (shadow == nullptr || snapshot.size() != shadowBytes || texelCount == 0 || shadowBytes % texelCount) { - return false; - } - - if (!pending.empty()) { - // The boxes index the SHADOW's texel grid (a 1D array's layers are its rows), which - // is also the snapshot's byte order. - const SizeT bpp = shadowBytes / texelCount; - const SizeT rowBytes = static_cast(extent.x()) * bpp; - const SizeT sliceBytes = static_cast(extent.y()) * rowBytes; - for (const auto& box : pending) { - if (box.Empty()) continue; - const SizeT spanBytes = static_cast(box.hi.x() - box.lo.x()) * bpp; - for (Int z = box.lo.z(); z < box.hi.z(); ++z) { - for (Int y = box.lo.y(); y < box.hi.y(); ++y) { - const SizeT offset = static_cast(z) * sliceBytes + static_cast(y) * rowBytes + - static_cast(box.lo.x()) * bpp; - Memcpy(snapshot.data() + offset, shadow + offset, spanBytes); - } - } - } - } - if (pipePending != nullptr) { - // The same merge for the applier's entry: its regions when it carries a list, its - // union box when it does not (RegionCount 0 = "the box is the whole story", D-D3). - const SizeT bpp = shadowBytes / texelCount; - const SizeT rowBytes = static_cast(extent.x()) * bpp; - const SizeT sliceBytes = static_cast(extent.y()) * rowBytes; - const auto mergePipeBox = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { - for (Uint32 slice = 0; slice < d; ++slice) { - for (Uint32 row = 0; row < h; ++row) { - const SizeT offset = static_cast(z + static_cast(slice)) * sliceBytes + - static_cast(y + static_cast(row)) * rowBytes + - static_cast(x) * bpp; - Memcpy(snapshot.data() + offset, shadow + offset, static_cast(w) * bpp); - } - } - }; - if (pipePending->Regions.empty()) { - const auto& box = pipePending->UnionBox; - mergePipeBox(box.X, box.Y, box.Z, box.W, box.H, box.D); - } else { - for (const auto& region : pipePending->Regions) { - mergePipeBox(region.X, region.Y, region.Z, region.W, region.H, region.D); - } - } - } - Memcpy(shadow, snapshot.data(), shadowBytes); - return true; - } - - void BackendTextureObject::RecreateBackendTexture() { - if (m_backendTextureId != 0) { - ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); - // Application FBO twins that attached the dying id memoize on FRONTEND - // attachment versions, which this backend-side re-mint does not move; - // without this bump their driver FBOs would keep the deleted name - // attached forever (see g_attachmentBackendIdGeneration). - ++FramebufferImpl::g_attachmentBackendIdGeneration; - if (m_contextGeneration == g_backendContextGeneration) { - g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); - } - for (auto& unitCache : g_boundTexturesCache) { - for (auto& boundTexture : unitCache) { - if (boundTexture == this) { - boundTexture = nullptr; - } - } - } - } - -#if MOBILEGL_BUILD_DISAGGREGATED - // A shared image's EGLImage lives exactly as long as the name it was bound to. - m_sharedImage = nullptr; -#endif - g_GLESFuncs.glGenTextures(1, &m_backendTextureId); - m_contextGeneration = g_backendContextGeneration; - if (m_backendTextureId == 0) { - MGLOG_E_ONCE("Failed to regenerate texture object."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } else { - MGLOG_D("Regenerated texture object with ID: %u.", m_backendTextureId); - } - m_isInitialized = false; - m_backendStorageImmutable = false; - m_prevTextureInfo = {}; - // The new ES texture starts at the ES defaults, so every parameter this object had - // already pushed onto the old one is gone. The change-detection caches below would - // otherwise still claim those values are in force and SyncTextureParamsToBackend - // would skip the whole pass on the unchanged params version, leaving the driver - // texture at defaults for the rest of its life. Latent for swizzle, LOD range and - // border colour long before GL_DEPTH_STENCIL_TEXTURE_MODE joined them; the mode - // makes it visible because falling back to the default silently samples the wrong - // aspect rather than merely mis-filtering. - m_cacheLodRange = {0, 1000}; - m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f}; - m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, TextureSwizzleParam::Blue, - TextureSwizzleParam::Alpha}; - m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT; - m_forceTextureParamsResync = true; - // The filter/wrap/LOD cache belongs to the name that just went away, and its gate is - // the frontend SAMPLER's version, which a backend re-mint does not move - so without - // this the new driver texture keeps the ES defaults for life. See m_forceSamplerResync. - m_cacheSamplerParameters = SamplerParameters{}; - m_forceSamplerResync = true; - } - - // Sets the backend GL unpack state to MobileGL's upload default for the scope, - // then restores it. The previous state is read from a shadow instead of via - // glGetIntegerv - that query forces a driver pipeline sync and, because texture - // uploads run it per dirty texture per frame, it dominated the DirectGLES draw - // path. The backend unpack state is set ONLY by MobileGL's own save/restore - // helpers (this class and, historically, the R32F copy path), all of - // which restore to the resting default, so the shadow stays accurate; a one-time - // forced sync pins the backend to that known default up front. Apply() is - // compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op. - class ScopedDefaultUnpackState { - public: - ScopedDefaultUnpackState() { - EnsureShadowSynced(); - m_prevAlignment = s_alignment; - m_prevRowLength = s_rowLength; - m_prevSkipRows = s_skipRows; - m_prevSkipPixels = s_skipPixels; - m_prevImageHeight = s_imageHeight; - m_prevSkipImages = s_skipImages; - // Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits - // width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1. - // Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride, - // shifting every row of a non-multiple-of-4 upload by one pixel. - Apply(1, 0, 0, 0, 0, 0); - } - - ~ScopedDefaultUnpackState() { - Apply(m_prevAlignment, m_prevRowLength, m_prevSkipRows, m_prevSkipPixels, m_prevImageHeight, - m_prevSkipImages); - } - - private: - static void EnsureShadowSynced() { -#if MOBILEGL_BUILD_DISAGGREGATED - const auto epoch = CurrentContextEpoch(); - if (s_epoch != epoch) { - s_synced = false; - s_epoch = epoch; - } -#endif - if (s_synced) { - return; - } - s_synced = true; - g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0); - s_alignment = 4; - s_rowLength = 0; - s_skipRows = 0; - s_skipPixels = 0; - s_imageHeight = 0; - s_skipImages = 0; - } - - static void Apply(GLint alignment, GLint rowLength, GLint skipRows, GLint skipPixels, GLint imageHeight, - GLint skipImages) { - if (alignment != s_alignment) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); s_alignment = alignment; } - if (rowLength != s_rowLength) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowLength); s_rowLength = rowLength; } - if (skipRows != s_skipRows) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, skipRows); s_skipRows = skipRows; } - if (skipPixels != s_skipPixels) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, skipPixels); s_skipPixels = skipPixels; } - if (imageHeight != s_imageHeight) { g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); s_imageHeight = imageHeight; } - if (skipImages != s_skipImages) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, skipImages); s_skipImages = skipImages; } - } - - GLint m_prevAlignment = 4; - GLint m_prevRowLength = 0; - GLint m_prevSkipRows = 0; - GLint m_prevSkipPixels = 0; - GLint m_prevImageHeight = 0; - GLint m_prevSkipImages = 0; - - // Shadow of the backend GL unpack state (GL defaults). See class comment. - static inline Bool s_synced = false; -#if MOBILEGL_BUILD_DISAGGREGATED - static inline ContextEpoch s_epoch{}; -#endif - static inline GLint s_alignment = 4; - static inline GLint s_rowLength = 0; - static inline GLint s_skipRows = 0; - static inline GLint s_skipPixels = 0; - static inline GLint s_imageHeight = 0; - static inline GLint s_skipImages = 0; - }; - - void ExerciseDefaultUnpackScopeForTesting() { - const ScopedDefaultUnpackState scope; - } - - // --- Unpack-ring staging (see BufferImpl::UnpackRingAvailable) --------------- - // One rectangular (or whole-level) region of a level shadow, repacked TIGHTLY - // into the persistently-mapped unpack PBO. The upload that follows passes the - // returned ring offset with UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT left at the - // surrounding ScopedDefaultUnpackState's 0, so the driver reads exactly the - // bytes staged here - strictly fewer than the ROW_LENGTH-strided client-pointer - // upload it replaces, which made the driver walk the whole level's stride. - // - // `blocks` describes one or more source regions to stage back-to-back into a - // SINGLE ring allocation; the ring offset of block i lands in blocks[i].offset. - // Deliberately without default member initializers: call sites declare a - // kMaxDirtyRects-sized array of these per dirty level and fill only the entries - // they use, so the type stays trivially default-constructible and the - // declaration costs nothing on the levels that never reach the ring. - struct UnpackStagingBlock { - const Uint8* src; // top-left texel of the region in the level's upload bytes - SizeT rowBytes; // bytes per region row (region width * bpp) - SizeT rows; // region height - SizeT slices; // region depth (1 for 2D) - SizeT srcRowStride; // level row pitch - SizeT srcSliceStride; // level slice pitch - SizeT offset; // out: byte offset into the ring store - }; - - // False (nothing staged, ring untouched) whenever the caller must keep the - // client-pointer path: ring unavailable, a degenerate block, or a total that - // overflows / exceeds the ring's cap. - static Bool StageBlocksIntoUnpackRing(UnpackStagingBlock* blocks, SizeT blockCount) { - if (blocks == nullptr || blockCount == 0) return false; - if (!BufferImpl::UnpackRingAvailable()) return false; - - const SizeT maxBytes = BufferImpl::UnpackRingMaxBytes(); - SizeT total = 0; - for (SizeT i = 0; i < blockCount; ++i) { - const UnpackStagingBlock& b = blocks[i]; - if (b.src == nullptr || b.rowBytes == 0 || b.rows == 0 || b.slices == 0) return false; - // Every factor is bounded by the level's own byte size, but the - // multiplications are on SizeT: check each against the cap instead of - // trusting a product that could have wrapped. - if (b.rowBytes > maxBytes || b.rows > maxBytes / b.rowBytes) return false; - const SizeT sliceBytes = b.rowBytes * b.rows; - if (b.slices > maxBytes / sliceBytes) return false; - const SizeT blockBytes = sliceBytes * b.slices; - if (blockBytes > maxBytes - total) return false; - total += blockBytes; - } - - SizeT base = 0; - if (!BufferImpl::UnpackRingAllocate(total, base)) return false; - // AFTER the allocation: growing the ring replaces the store and its map. - auto* ringBytes = static_cast(BufferImpl::UnpackRingMappedPtr()); - if (ringBytes == nullptr) return false; - - SizeT cursor = base; - for (SizeT i = 0; i < blockCount; ++i) { - UnpackStagingBlock& b = blocks[i]; - b.offset = cursor; - Uint8* dst = ringBytes + cursor; - // Each block's size is a whole number of texels, so consecutive block - // offsets stay multiples of the pixel type's size just as `base` is. - if (b.rowBytes == b.srcRowStride && b.rowBytes * b.rows == b.srcSliceStride) { - Memcpy(dst, b.src, b.rowBytes * b.rows * b.slices); // region IS the level - } else { - for (SizeT z = 0; z < b.slices; ++z) { - const Uint8* srcSlice = b.src + z * b.srcSliceStride; - if (b.rowBytes == b.srcRowStride) { - Memcpy(dst, srcSlice, b.rowBytes * b.rows); // full-width rows - dst += b.rowBytes * b.rows; - continue; - } - for (SizeT y = 0; y < b.rows; ++y) { - Memcpy(dst, srcSlice + y * b.srcRowStride, b.rowBytes); - dst += b.rowBytes; - } - } - } - cursor += b.rowBytes * b.rows * b.slices; - } - return true; - } - - // The `pixels` argument of a PBO-sourced glTexSubImage: a byte offset dressed - // up as a pointer. - static const void* UnpackRingPixelOffset(SizeT offset) { - return reinterpret_cast(static_cast(offset)); - } - - // See Managers.h. One flag for every context, like the image-unit high-water mark: a - // dispatch on another context can consume an upload's flag before the uploading - // context's own dispatch sees it - a hole only a shared texture re-uploaded on one context - // and image-accessed on another can fall into, and GL wants a fence between those anyway. - static Bool g_inPlaceTextureUploadPending = false; - - void NoteInPlaceTextureUpload() { g_inPlaceTextureUploadPending = true; } - - GLbitfield TakeInPlaceTextureUploadBarrierBits() { - if (!g_inPlaceTextureUploadPending) return 0; - g_inPlaceTextureUploadPending = false; - return GL_TEXTURE_UPDATE_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT; - } - - static Uint GetNormFallbackComponentCount(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R8Snorm: - case TextureInternalFormat::R16: - case TextureInternalFormat::R16Snorm: - return 1; - case TextureInternalFormat::RG8Snorm: - case TextureInternalFormat::RG16: - case TextureInternalFormat::RG16Snorm: - return 2; - case TextureInternalFormat::RGB8Snorm: - case TextureInternalFormat::RGB16: - case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow) - case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow) - case TextureInternalFormat::RGB16Snorm: - return 3; - case TextureInternalFormat::RGBA8Snorm: - case TextureInternalFormat::RGBA16: - case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow) - case TextureInternalFormat::RGBA16Snorm: - return 4; - default: - return 0; - } - } - - static Bool IsSnormFallbackFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R8Snorm: - case TextureInternalFormat::RG8Snorm: - case TextureInternalFormat::RGB8Snorm: - case TextureInternalFormat::RGBA8Snorm: - case TextureInternalFormat::R16Snorm: - case TextureInternalFormat::RG16Snorm: - case TextureInternalFormat::RGB16Snorm: - case TextureInternalFormat::RGBA16Snorm: - return true; - default: - return false; - } - } - - static Bool IsNorm8FallbackFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R8Snorm: - case TextureInternalFormat::RG8Snorm: - case TextureInternalFormat::RGB8Snorm: - case TextureInternalFormat::RGBA8Snorm: - return true; - default: - return false; - } - } - - // Components per texel the frontend format's client data carries. Only the three-channel - // formats that can be widened to a four-channel render target need an answer (see - // PrepareChannelWidenedUpload); everything else keeps its own layout and reports 0. - Uint GetWidenableClientComponentCount(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::RGB8Snorm: - case TextureInternalFormat::RGB16Snorm: - case TextureInternalFormat::RGB16: - case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow) - case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow) - case TextureInternalFormat::RGB16F: - case TextureInternalFormat::RGB32F: - case TextureInternalFormat::SRGB8: - case TextureInternalFormat::RGB8I: - case TextureInternalFormat::RGB8UI: - case TextureInternalFormat::RGB16I: - case TextureInternalFormat::RGB16UI: - case TextureInternalFormat::RGB32I: - case TextureInternalFormat::RGB32UI: - return 3; - default: - return 0; - } - } - - // True when the widened format's client data is integer rather than normalized. The two - // classes share every narrow component type - GL_RGB8I and GL_RGB8_SNORM are both uploaded - // as GL_BYTE - but their "1.0" differs: an integer channel's one is the integer 1, a - // normalized channel's is the saturated field. The type alone cannot tell them apart, so - // the source format has to. - Bool IsIntegerWidenableFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::RGB8I: - case TextureInternalFormat::RGB8UI: - case TextureInternalFormat::RGB16I: - case TextureInternalFormat::RGB16UI: - case TextureInternalFormat::RGB32I: - case TextureInternalFormat::RGB32UI: - return true; - default: - return false; - } - } - - // The bit pattern of 1.0 in an upload component type: what a format without alpha reads - // back as, and therefore what the synthetic fourth channel of a widened render target has - // to hold. Integer components carry the integer one, not a saturated field - and since - // GL_BYTE/GL_SHORT/GL_UNSIGNED_BYTE/GL_UNSIGNED_SHORT serve both classes, `integerData` - // is what decides, not the type. - static Bool GetUploadComponentOneBits(GLenum uploadType, Bool integerData, Uint8* outOneBits, - SizeT* outComponentSize) { - switch (uploadType) { - case GL_BYTE: { - const Int8 one = integerData ? Int8(1) : Int8(0x7F); - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_UNSIGNED_BYTE: { - const Uint8 one = integerData ? Uint8(1) : Uint8(0xFF); - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_SHORT: { - const Int16 one = integerData ? Int16(1) : Int16(0x7FFF); - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_UNSIGNED_SHORT: { - const Uint16 one = integerData ? Uint16(1) : Uint16(0xFFFF); - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_HALF_FLOAT: { - const Uint16 one = 0x3C00; // half 1.0 - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_FLOAT: { - const Float one = 1.0f; - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_INT: { - const Int32 one = 1; - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - case GL_UNSIGNED_INT: { - const Uint32 one = 1; - Memcpy(outOneBits, &one, sizeof(one)); - *outComponentSize = sizeof(one); - return true; - } - default: - return false; - } - } - - // A three-channel format widened to four to keep a colour attachment renderable (see - // NormalizePixelFormat) is described to the driver as a four-component transfer, so the - // three-component client data has to be repacked with an alpha of 1.0 - otherwise the - // driver walks three texels' worth of data per four-texel row and the image shears. - // `componentCount` is the SOURCE component count and `byteSize` the source's size, so this - // runs after any type conversion (which keeps the component count) has already happened. - const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, - const void* data, SizeT byteSize, GLenum uploadType, - Vector& widenedData, Bool integerData, - Uint32 alphaOneCodeOverride) { - Uint8 oneBits[8] = {}; - SizeT componentSize = 0; - // One and two source components as well as three: the image-format widening carries - // GL_R8UI in a GL_RGBA8UI and GL_RG32F in a GL_RGBA32F (see - // TextureImpl::GetImageBindableStorageWidening), and their surplus channels take the - // same values the three-channel case gives its single added one - zeroes, and the - // format's implied 1 in alpha. - if (componentCount == 0 || componentCount > 3 || data == nullptr || byteSize == 0 || - !GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) { - return data; - } - // ...except where the carrier holds CODES of a normalized value (GL_R16 in a - // GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that - // type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated - // 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose - // saturated code is 0x7FFF. The caller passes the channel's own maximum instead. - // Written through a value of the component's own width rather than as the low - // `componentSize` bytes of the Uint32, so the encoding does not turn on the host's - // byte order. - if (alphaOneCodeOverride != 0u) { - if (componentSize == sizeof(Uint16)) { - const auto one = static_cast(alphaOneCodeOverride); - Memcpy(oneBits, &one, sizeof(one)); - } else if (componentSize == sizeof(Uint32)) { - Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride)); - } else if (componentSize == sizeof(Uint8)) { - const auto one = static_cast(alphaOneCodeOverride); - Memcpy(oneBits, &one, sizeof(one)); - } - } - - const SizeT srcTexelBytes = componentSize * componentCount; - // Sized from the level, never from the source: the driver reads a full - // width*height*depth*4 components for the transfer it was handed, so a source that - // somehow holds fewer texels must still leave a full destination behind (its tail - // reads as transparent black with the format's implied opaque alpha) rather than a - // short buffer the driver would run off the end of. - const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * - static_cast(std::max(texelSize.y(), 0)) * - static_cast(std::max(texelSize.z(), 1)); - if (texelCount == 0) { - return data; - } - const SizeT copyTexelCount = std::min(texelCount, byteSize / srcTexelBytes); - - widenedData.assign(texelCount * componentSize * 4, 0); - const auto* src = static_cast(data); - Uint8* dst = widenedData.data(); - for (SizeT i = 0; i < texelCount; ++i, dst += componentSize * 4) { - if (i < copyTexelCount) { - Memcpy(dst, src, srcTexelBytes); - src += srcTexelBytes; - } - // ALWAYS at component 3, never at `componentCount`: GL's implied 1 is the ALPHA - // channel, and a one- or two-component source leaves the channels between it and - // alpha at the zero `assign` already wrote. For three components the two - // expressions coincide, which is what this used to be written as. - Memcpy(dst + componentSize * 3, oneBits, componentSize); - } - return widenedData.data(); - } - - static const void* PrepareNormFloatFallbackUpload(TextureInternalFormat format, - const IntVec3& texelSize, - const void* data, - SizeT byteSize, - GLenum uploadType, - Vector& convertedData) { - const Uint componentCount = GetNormFallbackComponentCount(format); - if (componentCount == 0 || uploadType != GL_FLOAT || data == nullptr || byteSize == 0) { - return data; - } - - const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * - static_cast(std::max(texelSize.y(), 0)) * - static_cast(std::max(texelSize.z(), 0)); - const SizeT componentTotal = texelCount * static_cast(componentCount); - const SizeT sourceComponentSize = IsNorm8FallbackFormat(format) ? sizeof(Int8) : sizeof(Uint16); - const SizeT sourceComponentTotal = byteSize / sourceComponentSize; - if (componentTotal == 0 || sourceComponentTotal == 0) { - return nullptr; - } - - convertedData.assign(componentTotal, 0.0f); - const SizeT copyComponentTotal = std::min(componentTotal, sourceComponentTotal); - if (IsNorm8FallbackFormat(format)) { - const Int8* src = static_cast(data); - constexpr Float invMaxSnorm8 = 1.0f / 127.0f; - for (SizeT i = 0; i < copyComponentTotal; ++i) { - convertedData[i] = std::max(static_cast(src[i]) * invMaxSnorm8, -1.0f); - } - } else if (IsSnormFallbackFormat(format)) { - const Int16* src = static_cast(data); - constexpr Float invMaxSnorm16 = 1.0f / 32767.0f; - for (SizeT i = 0; i < copyComponentTotal; ++i) { - convertedData[i] = std::max(static_cast(src[i]) * invMaxSnorm16, -1.0f); - } - } else { - const Uint16* src = static_cast(data); - constexpr Float invMaxUnorm16 = 1.0f / 65535.0f; - for (SizeT i = 0; i < copyComponentTotal; ++i) { - convertedData[i] = static_cast(src[i]) * invMaxUnorm16; - } - } - return convertedData.data(); - } - - // The two shadow -> upload conversions a fallback storage format can need, in order: - // the component type first (SNORM/UNORM shadows into the float the fallback stores), then - // the component count (three-channel client data into a four-channel widened render - // target). They compose: GL_RGB8_SNORM on a driver with no renderable three-channel - // format becomes GL_RGBA16F, so its Int8x3 shadow is converted to Float x3 and then - // repacked as Float x4 with alpha 1.0. - // - // Both scratch buffers belong to the caller so they outlive the returned pointer; the - // return value is `data` itself whenever neither conversion applies, which is what the - // sub-rect upload fast path tests for. - static const void* PrepareFallbackUpload(TextureInternalFormat format, TextureTarget target, - const IntVec3& texelSize, const void* data, SizeT byteSize, - GLenum uploadType, Vector& convertedData, - Vector& widenedData) { - const void* uploadData = - PrepareNormFloatFallbackUpload(format, texelSize, data, byteSize, uploadType, convertedData); - // The component-count switch first: it rules out every format that cannot be widened - // (which is nearly all of them, including GL_RGBA8) without touching the capability - // cache, so an ordinary atlas upload does not pay for a per-level cache lookup. - const Uint componentCount = GetWidenableClientComponentCount(format); - if (componentCount == 0 || !TextureImpl::BackendTextureFormatAddsAlpha(format, target)) { - return uploadData; - } - // The type conversion above rewrites the level into `convertedData` at four bytes per - // component while keeping the component count, so the widening's source size is that - // buffer's, not the shadow's. - const SizeT uploadByteSize = (!convertedData.empty() && uploadData == convertedData.data()) - ? convertedData.size() * sizeof(Float) - : byteSize; - return PrepareChannelWidenedUpload(componentCount, texelSize, uploadData, uploadByteSize, uploadType, - widenedData, IsIntegerWidenableFormat(format)); - } - - // One channel of a packed r11f_g11f_b10f word as a float. The two 11-bit channels are - // e5m6 and the 10-bit one e5m5 - IEEE-shaped but UNSIGNED, so there is no sign bit to - // read and the exponent bias is the 15 a 5-bit exponent always carries. - static Float DecodePackedUnsignedFloat(Uint32 bits, Uint mantissaBits) { - const Uint32 mantissaScale = 1u << mantissaBits; - const Uint32 mantissa = bits & (mantissaScale - 1u); - const Uint32 exponent = bits >> mantissaBits; - if (exponent == 0u) { - // Subnormal, and zero with it: no implied leading 1, and the exponent is the - // smallest NORMAL one rather than the encoded 0. - return std::ldexp(static_cast(mantissa) / static_cast(mantissaScale), -14); - } - if (exponent == 31u) { - return mantissa == 0u ? std::numeric_limits::infinity() - : std::numeric_limits::quiet_NaN(); - } - return std::ldexp(1.0f + static_cast(mantissa) / static_cast(mantissaScale), - static_cast(exponent) - 15); - } - - // The r11f_g11f_b10f shadow decoded into the GL_RGBA / GL_FLOAT level its GL_RGBA16F - // carrier is uploaded as. Alpha is the 1 GL defines for a format that has no alpha - // channel, which is the same constant the shader-side mask writes, so a texel this - // function produced and a texel an imageStore produced are indistinguishable. - // - // Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: - // the driver reads a full width*height*depth*4 floats for the transfer it was handed. - static const void* PreparePackedFloatWidenedUpload(const IntVec3& texelSize, const void* data, - SizeT byteSize, Vector& widenedData) { - constexpr SizeT kSourceTexelBytes = sizeof(Uint32); - if (data == nullptr || byteSize < kSourceTexelBytes) { - return data; - } - const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * - static_cast(std::max(texelSize.y(), 0)) * - static_cast(std::max(texelSize.z(), 1)); - if (texelCount == 0) { - return data; - } - const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes); - - widenedData.assign(texelCount * 4u * sizeof(Float), 0); - const auto* src = static_cast(data); - auto* dst = reinterpret_cast(widenedData.data()); - for (SizeT i = 0; i < texelCount; ++i, dst += 4) { - Float rgb[3] = {0.0f, 0.0f, 0.0f}; - if (i < copyTexelCount) { - Uint32 packed = 0; - // Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte - // buffer with no alignment promise of its own. - Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed)); - rgb[0] = DecodePackedUnsignedFloat(packed & 0x7FFu, 6u); - rgb[1] = DecodePackedUnsignedFloat((packed >> 11u) & 0x7FFu, 6u); - rgb[2] = DecodePackedUnsignedFloat((packed >> 22u) & 0x3FFu, 5u); - } - dst[0] = rgb[0]; - dst[1] = rgb[1]; - dst[2] = rgb[2]; - dst[3] = 1.0f; - } - return widenedData.data(); - } - - // The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its - // GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST - // component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19, - // blue 20-29 and alpha 30-31. - // - // The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an - // rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side - // unpack divides out. Neither is scaled here - the carrier holds the format's own bits. - // - // Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the - // driver reads a full width*height*depth*4 shorts for the transfer it was handed. - const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, - SizeT byteSize, Vector& widenedData) { - constexpr SizeT kSourceTexelBytes = sizeof(Uint32); - if (data == nullptr || byteSize < kSourceTexelBytes) { - return data; - } - const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * - static_cast(std::max(texelSize.y(), 0)) * - static_cast(std::max(texelSize.z(), 1)); - if (texelCount == 0) { - return data; - } - const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes); - - widenedData.assign(texelCount * 4u * sizeof(Uint16), 0); - const auto* src = static_cast(data); - auto* dst = reinterpret_cast(widenedData.data()); - for (SizeT i = 0; i < texelCount; ++i, dst += 4) { - Uint32 packed = 0; - if (i < copyTexelCount) { - // Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte - // buffer with no alignment promise of its own. - Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed)); - } - dst[0] = static_cast(packed & 0x3FFu); - dst[1] = static_cast((packed >> 10u) & 0x3FFu); - dst[2] = static_cast((packed >> 20u) & 0x3FFu); - dst[3] = static_cast((packed >> 30u) & 0x3u); - } - return widenedData.data(); - } - - // The transfer half of the image-format widening: an image-bindable texture whose ES - // storage was widened to a core carrier is described to the driver as a four-component - // transfer, so its narrower client data has to be repacked the same way the three-channel - // colour-renderable widening repacks its own. - // - // Three shapes, because the carriers come in three kinds. Most of them keep the frontend - // format's component TYPE and only add channels, so padding the shadow out to four - // components is the whole conversion. The two PACKED formats do not: their shadow is one - // 32-bit word per texel, so the word has to be split - into four floats for - // r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such - // a word as components of the carrier's type - what the repack below would do - takes - // twelve or sixteen bytes from a four-byte texel and shears the level, which is what the - // allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel - // the upload got wrong). - // - // Composes with PrepareFallbackUpload rather than replacing it, and the composition is a - // no-op by construction: none of the widened formats is one GetWidenableClientComponentCount - // reports a count for, and the SNORM shadow-to-float conversion only fires for a GL_FLOAT - // transfer type, which the widened triple never picks for the two SNORM8 formats. So the - // shadow reaches this untouched and one conversion is all that runs. - static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening, - const IntVec3& texelSize, const void* data, SizeT byteSize, - Vector& widenedData) { - if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) { - return data; - } - switch (widening.SourceEncoding) { - case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f: - return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData); - case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev: - return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData); - case TextureImpl::ImageWidenSourceEncoding::Components: - break; - } - if (widening.SourceChannels == 4) { - return data; - } - return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type, - widenedData, widening.IntegerData, - widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u); - } - - // Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose - // with the widened carrier's. Deliberately unconditional on anything but the widening - // itself: whatever renderability fallback the triple carried, an image the driver refuses - // to bind is useless, so the image constraint wins. - static void ApplyImageBindableStorageWidening(const TextureImpl::ImageBindableStorageWidening& widening, - GLenum* inOutInternalFormat, GLenum* inOutFormat, - GLenum* inOutType) { - if (!widening) { - return; - } - if (inOutInternalFormat) *inOutInternalFormat = widening.InternalFormat; - if (inOutFormat) *inOutFormat = widening.Format; - if (inOutType) *inOutType = widening.Type; - } - - // RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE - // leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is - // implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors, - // drifting mid-range texels one 5-bit step down and failing the KHR-GL3x - // pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into - // the packed 16-bit client type with round-to-nearest instead - that recovers the original - // 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the - // driver stores them verbatim with no requantization left to its discretion. 4-bit formats - // (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding. - // Always retargets *inOutType for these formats (even for null data) so every upload of a - // level uses the same client type. - static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize, - const void* data, SizeT byteSize, GLenum* inOutType, - Vector& packedData) { - if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) { - return data; - } - // With the storage widened to 8-bit-per-channel (the packed16 field-order quirk) - // there is no driver requantization left for the repack to pre-empt - the shadow's - // UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg - // retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all. - if (TextureImpl::UsesWidenedPacked16NormStorage(format)) { - return data; - } - const Bool hasAlpha = format == TextureInternalFormat::RGB5A1; - const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5; - // Idempotent across a region's level loop: glType is shared, so later levels arrive with - // the already-retargeted packed type and must still be converted. - if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) { - return data; - } - *inOutType = packedType; - if (data == nullptr || byteSize == 0) { - return data; - } - const SizeT srcPixelBytes = hasAlpha ? 4 : 3; - const SizeT texelCount = std::min(static_cast(std::max(texelSize.x(), 0)) * - static_cast(std::max(texelSize.y(), 0)) * - static_cast(std::max(texelSize.z(), 1)), - byteSize / srcPixelBytes); - packedData.resize(texelCount * sizeof(Uint16)); - const Uint8* src = static_cast(data); - auto* dst = reinterpret_cast(packedData.data()); - for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) { - const Uint32 r = (static_cast(src[0]) * 31u + 127u) / 255u; - const Uint32 b = (static_cast(src[2]) * 31u + 127u) / 255u; - if (hasAlpha) { - const Uint32 g = (static_cast(src[1]) * 31u + 127u) / 255u; - dst[i] = static_cast((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u)); - } else { - const Uint32 g = (static_cast(src[1]) * 63u + 127u) / 255u; - dst[i] = static_cast((r << 11) | (g << 5) | b); - } - } - return packedData.data(); - } - - // "Some level of this texture holds an image", which is all the sync gate below actually - // needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also - // answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with - // undefined lower levels - but such a texture still has to be uploaded, or the level that - // IS defined never reaches the driver at all. - static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) { - const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject); - if (mipmapObject == nullptr) return false; - const auto levelCount = mipmapObject->GetMipmapLevelCount(); - for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) { - for (Uint level = 0; level < levelCount; ++level) { - const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level); - if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) { - return true; - } - } - } - return false; - } - - // P5e (tx2): the GL name a log line wants, from whichever side of the arm can answer it. - // Desc.GlNameForDiag is the declared diagnostics carrier (MGPipeTypes.h) and it is the - // ONLY thing the by-handle arm may say about a texture's frontend name - a GL name is - // never an identity, never a memo key and never part of a hash (section 4.2.1). The - // twenty-odd GetExternalIndex() log reads this family carried (scout G-S2-4) all become - // this call. - static Uint32 TextureDiagName(const SharedPtr& stateTextureObject, - const MG_Pipe::MGPipeResourceRecord* record) { - if (stateTextureObject) return static_cast(stateTextureObject->GetExternalIndex()); - return record != nullptr ? record->Desc.GlNameForDiag : 0u; - } -// True while this twin has NO noted handle, i.e. it is the monolith-glue half and the frontend -// object below is the only thing that can answer. A macro and not a member function because the -// pull build has no such member at all and this has to fold to a constant there (D-P: the pull -// build's preprocessed text may not gain a call). -#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (MG_Pipe::MGPipeHandleIsNull((twin).PushedSyncHandle())) - - // P5e (fix1, ID-81 / CONTRACT-P5E §5.8): THE NULL FRONTEND OBJECT IS A CONTRACT AND THIS - // IS WHERE IT IS CHECKED, ONCE, FOR ALL THREE SYNC BODIES. - // - // tx2's three sync bodies accept a null `stateTextureObject` because the RECORD answers - // every read they make - but only where the record arm is actually SELECTED. Two reads - // inside SyncMipmapsToBackend are gated on `Transport != Monolith` (the texture-VIEW test - // and RequireImageBindableStorage's re-dirty transition, §5.2), and every read in all - // three is gated on TextureResourceSubsystemEnabled() through `pushedStorage`. So a null - // object is servable exactly when BOTH hold, and the by-handle entries beside them - // (SyncTextureToBackendByHandle / SyncMipmapsToBackendByHandle) pass null on the strength - // of a NOTED HANDLE, which says nothing about either. - // - // The device found the gap: with MOBILEGL_TRANSPORT=monolith the push build still drives - // its framebuffer attachments from the record (P4a, D-C2), fb's SyncAttachmentSurface - // called the by-handle storage sync from that arm, and `stateTextureObject->IsTextureView()` - // dereferenced the null SharedPtr in Lightmap. -> clearColorTexture -> glClear. - // The arm selection is repaired at that call site; this refusal is what makes the shape - // UNREPEATABLE rather than merely repaired - a null frontend object reaching a body that - // can still read one aborts BY NAME instead of taking a SIGSEGV three frames deep. - // - - // Returns true when the caller must decline (no handle noted: the caller's bug it always - // was, named by the caller's own log line), false when the record arm can serve the call, - // and never returns at all when a handle was noted on an arm that cannot serve it. - static Bool RefuseNullFrontendTextureOffTheHandleArm(const char* entry, - MG_Pipe::MGPipeHandle notedHandle) { - if (MG_Pipe::MGPipeHandleIsNull(notedHandle)) return true; - return false; - } -// The guard the three sync bodies spell. In the PULL build it folds to the constant the -// pre-fix1 text folded to, so that build's preprocessed text gains no call (D-P). -#define MGB_TEXTURE_NULL_FRONTEND_REFUSED(twin, entry) \ - (RefuseNullFrontendTextureOffTheHandleArm((entry), (twin).PushedSyncHandle())) -// P5e (tx2), CONTRACT-P5E §5.2: THE TEXTURE'S TARGET, from the descriptor whenever a record was -// resolved. The two live `GetTarget()` reads left INSIDE the handle arm (scout G-S2-2, the -// parameter sync and the built-in sampler sync) read Desc.Target here, through the same -// StagedTextureTargetForPipeTarget mapping the storage sync already routes through - so all -// three bodies answer the question from one carrier rather than two authorities. -#define MGB_TEXPARAM_TARGET(rec, obj) \ - ((rec) != nullptr ? BufferImpl::StagedTextureTargetForPipeTarget((rec)->Desc.Target) : (obj)->GetTarget()) -// The ~20 GetExternalIndex() reads this family makes in LOG LINES (scout G-S2-4). The pull -// expansion is the pre-P5e expression with one pair of parentheses around the receiver. -#define MGB_TEXTURE_DIAG_NAME(rec, obj) (TextureDiagName((obj), (rec))) -// The storage KIND, from the descriptor whenever one was resolved - the backstop arm of the -// storage switch used to ask the frontend object for a value it had just read from the record. -#define MGB_TEXTURE_STORAGE_KIND(rec, obj) \ - ((rec) != nullptr ? static_cast((rec)->Desc.StorageKind) : (obj)->GetStorageType()) - - // The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought. - // Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability - // is the extension AND the pointer, because eglGetProcAddress hands back live-looking - // stubs (see AcquireGLESFunctions). - static MG_External::GLES::glTextureViewEXT_PTR ResolveTextureViewEntryPoint() { - if (g_GLESFuncs.glTextureViewEXT != nullptr) { - return g_GLESFuncs.glTextureViewEXT; - } - return reinterpret_cast(g_GLESFuncs.glTextureViewOES); - } - - // P5e (tx2): see the declaration for the split between what the record answers and what - // the wire still cannot say about a view. - void BackendTextureObject::SyncTextureViewToBackendByRecord( - MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& stateTextureObject) { - if (!g_GLESCapabilities.SupportsTextureView) { - // Unreachable through the API - the frontend refuses glTextureView without - // GL_ARB_texture_view - and kept for the reason the frontend arm keeps its twin. - MGLOG_E_ONCE("Texture view %u reached the backend on a driver without " - "EXT/OES_texture_view.", - record.Desc.GlNameForDiag); - return; - } - // The storage owner by handle, through the same by-handle entry every other draw-path - // texture takes: no frontend object, no allocator probe. - const SharedPtr storageBackendObject = - SyncTextureToBackendByHandle(record.Desc.ViewOf, m_imageBindableStorageRequired); - if (!storageBackendObject) { - MGLOG_E_ONCE("Failed to sync the storage texture {%u, %u} of view %u on the handle arm.", - record.Desc.ViewOf.Slot, record.Desc.ViewOf.Gen, record.Desc.GlNameForDiag); - return; - } - const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId(); - if (storageBackendTextureId == 0) { - MGLOG_D("Storage texture of view %u has no ES name yet.", record.Desc.GlNameForDiag); - return; - } - // P3b/P4b D3: THE SERIAL THIS TWIN STAMPS IS THE STORAGE'S, not this view's, because - // the gate that reads it back (IsDrawSyncCleanByRecord) now asks the same record. It - // is read AFTER the storage sync above - which has just consumed the owner's pending - // uploads - and taken by VALUE, because the applier's vector may move under any later - // call and a Uint64 cannot dangle. A storage record that has gone answers 0, which - // the gate reads as "never clean": the direction that re-syncs. - const MG_Pipe::MGPipeResourceRecord* storageRecord = PipeTextureStorageRecordForRecord(record); - const Uint64 storageSerial = storageRecord != nullptr ? storageRecord->Serial : 0; - if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) { - // The steady case, and the only one this arm has to be fast at: one record read, - // one integer compare and the serial stamp that keeps the aggregate clean gate - // (IsDrawSyncCleanByRecord) answering true for the next draw. - m_syncedResourceSerial = storageSerial; - return; - } - // The view window is a sampler-view CSO, created even when this - // texture is only image-bound or attached to an FBO. It is not part - // of the resource descriptor, and needs no frontend fallback. - (void)stateTextureObject; - const auto& applier = MG_Pipe::MGPipeApplier(); - const auto handle = record.ViewCso; - if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= applier.SamplerViewCsos.size()) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-cso\"} {%u,%u}", res.Slot, res.Gen); - std::abort(); - } - const auto& cso = applier.SamplerViewCsos[handle.Slot]; - const auto& view = cso.View; - if (!cso.Live || cso.Gen != handle.Gen || view.Texture != res || !view.NumLevels || !view.NumLayers) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-window\"} {%u,%u}", res.Slot, res.Gen); - std::abort(); - } - const auto viewTarget = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); - const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(viewTarget); - GLenum internalFormat = 0, transferFormat = 0, transferType = 0; - GenerateTextureFormatInfo(static_cast(view.InternalFormat), - &internalFormat, &transferFormat, &transferType, viewTarget); - // A name already bound or used as a view cannot be redefined by - // glTextureView. This also invalidates native binding/FBO caches. - RecreateBackendTexture(); - DebugImpl::ErrorLopper::Clear(); - ResolveTextureViewEntryPoint()(m_backendTextureId, nativeTarget, storageBackendTextureId, - internalFormat, view.MinLevel, view.NumLevels, view.MinLayer, view.NumLayers); - const GLenum error = g_GLESFuncs.glGetError(); - if (error != GL_NO_ERROR) { - MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"texture-view-native\"} {%u,%u} error=0x%x", - res.Slot, res.Gen, error); - std::abort(); - } - m_viewSourceBackendTextureId = storageBackendTextureId; - m_isInitialized = true; - m_backendStorageImmutable = true; - m_prevTextureInfo = {static_cast(record.Desc.InternalFormat), - record.Desc.Width, record.Desc.Height, record.Desc.Depth, view.NumLevels, 0, - record.Desc.Samples, record.Desc.FixedSampleLocations != 0}; - // The storage's, for the steady arm's reason above (P3b/P4b D3). - m_syncedResourceSerial = storageSerial; - } - - // P4a (D-D5), the dirty-ownership inversion, applied at the two places the upload loops - // below actually ask: "does this (uploadTarget, level) owe an upload" and "it has been - // uploaded". ARCHITECTURE.md §6 puts the rect model and the emission cursor on the - // CLIENT, which clears its own flags AT EMISSION - so on the handle arm the frontend's - // IsStorageDirty is already false for every level the applier accepted, and asking it - // would upload nothing at all. The applier's pending set is what these two read instead; - // it is server-side state, it survives this function's bail arms (an incomplete texture - // returns early, a multisample target refreshes and skips), and an entry is consumed - // ONLY where the level actually uploaded. - // - // THE UPLOAD-TARGET ENCODING is static_cast(TextureUploadTarget), which is what - // package B must emit into MGPSubData::Target's cube-face half; it is recorded as a - // deviation because the contract left it unstated. - // - // MACROS AND NOT LAMBDAS, and that is a G1 requirement rather than a style choice: - // SyncMipmapsToBackend already holds nine ErrorLopper lambdas whose mangled names are - // ...::$_N BY POSITION, so inserting two more renumbered every one of them - measured as - // eight symbols added, eight removed and four std::function thunks resized in the PULL - // build, whose symbol set P4a may not move by one byte (D-P). A macro expands to the - // pre-P4a expression exactly when MOBILEGL_PIPE_PUSH is off, so the pull build's - // preprocessed text, and therefore its object code, is unchanged. Both are #undef'd - // immediately after the function. -// P5c (tx): THE READS THE FOUR UPLOAD ARMS MAKE, re-sourced. With an active transport the -// apply thread may not name the client's TextureObjectMipmap at all (rule E), so on the -// handle arm: -// -// * the level TEXELS come from the server's staged-texture store, adopted at apply time -// (MGB_LEVEL_TEXELS - Fatal{StageSnapshotTooNarrow} when no record covered the level, -// which is the data-correctness refusal of the texture half); -// * the per-level EXTENT and DEFINED-NESS come from the same store (fed by the sub-data -// adoption and the respecify hook; {0,0,0} for a level nothing defined, which is exactly -// GetMipmapTexelSize's answer for one); -// * the level BYTE SIZE is the adopted run's length; -// * the texture TARGET and the UPLOAD-TARGET list come from the descriptor. -// -// Every macro keeps the P4a discipline: the non-disaggregated expansion is the original -// frontend read, character for character modulo one pair of parentheses, so the pull and -// push builds compile exactly what they compiled before tx, and every disaggregated arm -// falls back to the frontend read when there is no active transport (the legacy arm, and -// monolith). MGB_STAGED_TEXTURE_LIVE is the runtime discriminator; pushedStorage/pushedRes -// are the function's own locals. All are #undef'd with the rest after the function. -#define MGB_STAGED_TEXTURE_LIVE \ - (pushedStorage != nullptr && MG_Record::ServerStagedTexture().Holds()) -#define MGB_TEXTURE_TARGET(obj) \ - (MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedTextureTargetForPipeTarget(pushedStorage->Desc.Target) \ - : (obj)->GetTarget()) -#define MGB_UPLOAD_TARGETS(obj) \ - (MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedUploadTargetsForPipeTarget(pushedStorage->Desc.Target) \ - : (obj)->GetUploadTargets()) -#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) \ - (MGB_STAGED_TEXTURE_LIVE \ - ? MG_Record::ServerStagedTexture().LevelExtentOrUndefined( \ - MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ - static_cast(lvl)) \ - : (obj)->GetMipmapTexelSize(tgt, lvl)) -#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) \ - (MGB_STAGED_TEXTURE_LIVE \ - ? MG_Record::ServerStagedTexture().LevelByteSize( \ - MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ - static_cast(lvl)) \ - : (obj)->GetMipmapByteSize(tgt, lvl)) -#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) \ - (MGB_STAGED_TEXTURE_LIVE \ - ? MG_Record::ServerStagedTexture().RequireLevelBytes( \ - MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ - static_cast(lvl), site) \ - : (obj)->MapMipmapData(tgt, lvl)) - -// P5c (tx): the disaggregated pair adds ONE term and ONE clear to the P4a shapes - the -// staged-texture store's GPU-dirty mark (T5: a level the GPU generated dirties the SERVER's -// shadow, and the pending set cannot see it). The mark is never set on Espryt today - its -// GenerateMipmap fills the levels on the driver - so the term is inert here and is the -// contract-shaped answer (§2.2's last row) rather than a hot-path cost: one m_any acquire -// load when the store is empty. -#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \ - (pushedStorage != nullptr \ - ? (FindPipeTextureUpload(*pushedStorage, static_cast(tgt), static_cast(lvl)) != nullptr || \ - MG_Record::ServerStagedTexture().IsLevelGpuDirty( \ - MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ - static_cast(lvl))) \ - : (obj)->IsStorageDirty(tgt, lvl)) -// Re-resolves the record itself, so it is safe after any amount of driver work - and it -// invalidates any PendingUpload* taken earlier for THIS texture, which is why every such pointer -// is used and dropped inside one level's iteration. -#define MGB_LEVEL_UPLOAD_DONE(obj, tgt, lvl) \ - do { \ - if (pushedStorage != nullptr) { \ - ConsumePipeTextureUpload(pushedRes, static_cast(tgt), static_cast(lvl)); \ - if (MGB_STAGED_TEXTURE_LIVE) { \ - MG_Record::ServerStagedTexture().MarkLevelGpuDirty( \ - MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ - static_cast(lvl), false); \ - } \ - } else { \ - (obj)->MarkStorageDirty(tgt, lvl, false); \ - } \ - } while (0) - - // THE STORAGE SHAPE, and on the handle arm it is the DESCRIPTOR's (C.3's d2: "the - // m_prevTextureInfo probe re-keyed onto the resource record's Serial AND Desc"; review - // M-2). v1 moved the parameters and the uploads and left the allocation reading the - // frontend object, which put TWO AUTHORITIES on one value inside one twin - - // SyncTextureParamsToBackend already answers MGB_TEXPARAM_FORMAT from - // Desc.InternalFormat while this function allocated from GetFormat(). In monolith they - // agree, so no lane can expose the split; under split the descriptor is all there is. - // - // WHAT THE DESCRIPTOR ANSWERS: internal format, base extent, level count, sample count, - // fixed sample locations, immutability, storage kind and the buffer-texture window. What - // it does NOT answer, and what still comes from the frontend shadow, is the PER-LEVEL - // extent and the level BYTES - MGPResourceDesc carries one base extent, the per-level - // sizes are derived from it, and the texels themselves ride on sub-data. A level the - // descriptor's Levels claims and the shadow has not defined reads back {0,0,0} and the - // per-level loops skip it, which is the same arm they take for a sparse chain today. - // - // AND Desc.HasDefinedContent IS DELIBERATELY NOT READ HERE (review N-8, and the - // enumeration above exists to be exhaustive, so the omission is stated rather than - // left to be noticed). c0e made it storage-defining and the BUFFER family reads it - // three times, because a buffer's content is ONE fact about the whole resource. A - // texture's is not: content is per (uploadTarget, level) and its authority is the - // pending set (D-D5), which is per level and survives a bail. A whole-resource - // "content is undefined" would say nothing the per-level loops do not already answer - // by finding no level to upload, and acting on it would mean throwing away levels the - // set still owes. The field stays a respecify CLASSIFIER (it moves the descriptor, so - // wire refuses to call such a respecify metadata-only) and nothing more on this arm. - // - // MACROS, NOT LOCALS, FOR THE SAME REASON AS THE PAIR ABOVE (DV-9, and it was measured): - // routing these through locals resized SyncTextureParamsToBackend by +9 bytes in the - // PULL build and P4a's admitted-resize set is EMPTY. Each takes the object it would - // otherwise have been spelled on, so the pull expansion is the pre-P4a expression with - // one pair of parentheses around the receiver. All are #undef'd with the pair above. -#define MGB_STORAGE_FORMAT(obj) \ - (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.InternalFormat) \ - : (obj)->GetFormat()) -#define MGB_STORAGE_BASE_SIZE(obj) \ - (pushedStorage != nullptr ? IntVec3{static_cast(pushedStorage->Desc.Width), \ - static_cast(pushedStorage->Desc.Height), \ - static_cast(pushedStorage->Desc.Depth)} \ - : (obj)->GetBaseSize()) -#define MGB_STORAGE_LEVELS(obj) \ - (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.Levels) : (obj)->GetMipmapLevelCount()) -#define MGB_STORAGE_SAMPLES(obj) \ - (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.Samples) : (obj)->GetSamples()) -#define MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(obj) \ - (pushedStorage != nullptr ? pushedStorage->Desc.FixedSampleLocations != 0 : (obj)->HasFixedSampleLocations()) -#define MGB_STORAGE_IMMUTABLE(obj) \ - (pushedStorage != nullptr ? pushedStorage->Desc.Immutable != 0 : (obj)->IsImmutable()) -#define MGB_STORAGE_KIND(obj) \ - (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.StorageKind) \ - : (obj)->GetStorageType()) - -#if MOBILEGL_BUILD_DISAGGREGATED - // SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). The client defined - // level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor - // still says so, but the texels are the image's, so nothing below may allocate or upload. - Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) { - const Uint64 imageId = record.SharedImageId; - if (imageId == 0) { - // Respecified away from the image. The ES name is an EGLImage sibling, which must not - // be redefined in place: the immutable flag sends the normal path through - // RecreateBackendTexture, and the EGLImage goes with the name it was bound to. - m_sharedImageId = 0; - m_isInitialized = false; - m_backendStorageImmutable = true; - return false; - } - - // Steady state: bound to this image already (the serial moves with sub-data and with a - // metadata respecify, neither of which changes what the name samples). A failed bind is - // retried only once the record moves. Pending sub-data is the caller's - // (ReplaySharedImageUploads), after the image's sync state has been acquired. - if (m_sharedImageId == imageId && m_isInitialized && - (m_sharedImage != nullptr || m_syncedResourceSerial == record.Serial)) { - m_syncedResourceSerial = record.Serial; - return true; - } - - // A fresh mutable name: the current one may hold immutable storage (which no EGLImage - // can replace) or another image's sibling. RecreateBackendTexture also moves the FBO - // twins' attachment generation and forces the parameter resync the new name needs. - RecreateBackendTexture(); - m_sharedImage = nullptr; - m_sharedImageId = imageId; - m_isInitialized = true; - // Whatever comes next redefines storage through a re-mint (see the imageId == 0 arm). - m_backendStorageImmutable = true; - m_syncedResourceSerial = record.Serial; - - const TextureTarget target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); - if (ConvertTextureTargetToBackendGLEnum(target) != GL_TEXTURE_2D) { - MGLOG_E_ONCE("Shared image texture %u: target %s cannot take an EGLImage; left empty", - record.Desc.GlNameForDiag, MG_Util::ConvertTextureTargetToString(target).c_str()); - return true; - } - String why; - SharedImageImpl::EglImageRef image = SharedImageImpl::CreateEglImage(imageId, why); - if (image == nullptr) { - MGLOG_E_ONCE("Shared image texture %u: image %llu cannot be bound (%s); left empty", - record.Desc.GlNameForDiag, static_cast(imageId), why.c_str()); - return true; - } - Bind(GL_TEXTURE_2D); - if (SharedImageImpl::IsYuv(image)) { - // A YUV image is CONVERTED into the name's own RGBA8 level at each frame's first use - // (SharedImageImpl::ConvertYuvForSampling), never made its storage. - while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - } - g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, static_cast(record.Desc.Width), - static_cast(record.Desc.Height)); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - MGLOG_E_ONCE("Shared image texture %u: no %ux%u RGBA8 storage for YUV image %llu; left empty", - record.Desc.GlNameForDiag, record.Desc.Width, record.Desc.Height, - static_cast(imageId)); - return true; - } - } else if (!SharedImageImpl::TargetBoundTexture2D(image)) { - MGLOG_E_ONCE("Shared image texture %u: glEGLImageTargetTexture2DOES refused image %llu; left empty", - record.Desc.GlNameForDiag, static_cast(imageId)); - return true; - } - m_sharedImage = std::move(image); - m_sharedImageUploadsRgb = false; - // The shape the name now has: the descriptor's level 0, and that one level only. - m_prevTextureInfo = {static_cast(record.Desc.InternalFormat), - record.Desc.Width, - record.Desc.Height, - record.Desc.Depth, - 1, - 0, - record.Desc.Samples, - record.Desc.FixedSampleLocations != 0}; - return true; - } - - // SUB-DATA ON A SHARED IMAGE IS WRITTEN INTO IT. An X server drawing with glamor uploads - // its clients' pixels (MIT-SHM / PutImage) into window pixmaps whose level 0 is a shared - // image, with glTexSubImage2D: the texels are in the staged store (the client defined the - // level without data, so only the uploaded boxes are covered), and they go into the - // EGLImage-backed name box by box. Every pending entry leaves the set either way, or the - // per-draw clean gate would never close. Called after AcquireForSampling, so the upload is - // ordered after the image's pending reads and writes like any other use. - void BackendTextureObject::ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record) { - if (record.PendingUploads.empty()) return; - Vector> keys; - Vector> boxes; - keys.reserve(record.PendingUploads.size()); - for (const auto& pending : record.PendingUploads) { - const Uint16 uploadHalf = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget); - keys.emplace_back(uploadHalf, pending.Level); - if (pending.Level != 0) continue; // an image has one level - if (pending.Regions.empty()) { - boxes.emplace_back(uploadHalf, pending.UnionBox); - } else { - for (const auto& region : pending.Regions) - boxes.emplace_back(uploadHalf, MG_Pipe::MGPBox{region.X, region.Y, region.Z, region.W, region.H, - region.D}); - } - } - if (m_sharedImage != nullptr && !boxes.empty()) { - auto& store = MG_Record::ServerStagedTexture(); - const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(record.Desc.Resource); - Bind(GL_TEXTURE_2D); - ScopedDefaultUnpackState unpackState; - BufferImpl::BindPixelUnpackBufferId(0); - for (const auto& [uploadHalf, box] : boxes) { - Int rowTexels = 0; - // A shared image is RGBA8 to every client that names it (the level the client - // defined over it): four bytes a texel. - const Uint8* bytes = - store.CoveredBoxBytes(key, uploadHalf, 0, box.X, box.Y, box.W, box.H, 4, &rowTexels); - if (bytes == nullptr) { - MGLOG_E_ONCE("Shared image texture %u: an upload of %ux%u at (%d,%d) has no staged texels; " - "not written", - record.Desc.GlNameForDiag, box.W, box.H, box.X, box.Y); - continue; - } - const Uint8* origin = - bytes + (static_cast(box.Y) * static_cast(rowTexels) + static_cast(box.X)) * 4; - if (!m_sharedImageUploadsRgb) { - while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - } - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowTexels); - g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast(box.W), - static_cast(box.H), GL_RGBA, GL_UNSIGNED_BYTE, origin); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - // An image of an X format (no alpha) is an RGB8 texture to the driver, which - // takes only RGB texels: this image's uploads are converted from now on. - if (g_GLESFuncs.glGetError() != GL_INVALID_OPERATION) continue; - m_sharedImageUploadsRgb = true; - } - Vector rgb(static_cast(box.W) * box.H * 3); - for (Uint32 row = 0; row < box.H; ++row) { - const Uint8* src = origin + static_cast(row) * static_cast(rowTexels) * 4; - Uint8* dst = rgb.data() + static_cast(row) * box.W * 3; - for (Uint32 x = 0; x < box.W; ++x) { - dst[x * 3 + 0] = src[x * 4 + 0]; - dst[x * 3 + 1] = src[x * 4 + 1]; - dst[x * 3 + 2] = src[x * 4 + 2]; - } - } - g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast(box.W), - static_cast(box.H), GL_RGB, GL_UNSIGNED_BYTE, rgb.data()); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - } - MGLOG_I_ONCE("Shared image texture %u: sub-data written into its image (%s texels)", - record.Desc.GlNameForDiag, m_sharedImageUploadsRgb ? "RGB" : "RGBA"); - } - for (const auto& [uploadHalf, level] : keys) ConsumePipeTextureUpload(record.Desc.Resource, uploadHalf, level); - } -#endif - - // Stamps the same per-draw clean-gate keys a completed storage sync stamps, so a view - // that needs no work costs the same nothing per draw that any other synced texture does. - void BackendTextureObject::StampViewSyncKeys( - const SharedPtr& stateTextureObject) { - if (MG_Pipe::gPipeInputs->IsLive()) { - m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId(); - m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); - m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); - } - m_syncedContentVersion = stateTextureObject->GetContentVersion(); - } - - void BackendTextureObject::SyncMipmapsToBackend( - const SharedPtr& stateTextureObject) { - // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; - // see the twin note in SyncBuiltinSamplerToBackend. P5e (fix1): "a handle was noted" - // is not on its own the statement that the record arm will be SELECTED - the view - // test below and RequireImageBindableStorage's transition are transport-gated - so - // the check is the one shared refusal and a noted handle on an arm that cannot serve - // it aborts by name rather than falling into the frontend reads. - if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncMipmapsToBackend")) { - MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); - return; - } - - // P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads. - // P5e (tx2) hoists the resolution to the TOP of the function, because the view test - // below needs it before anything else does; nothing else about it moves. - // - // The record pointer stays valid for the whole function: only a create/respecify for - // a NEW slot grows MGPipeApplierState::TextureResources, and nothing this function - // calls emits one. Pointers INTO record->PendingUploads do not - the consume is a - // swap-and-pop - so every one of them is taken, used and dropped inside one level's - // iteration, and never held across a consume. - const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr; - MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle; - pushedStorage = ResolveOwnRecord(stateTextureObject); - pushedRes = !MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle) - ? m_pushedSyncHandle - : (pushedStorage != nullptr ? pushedStorage->Desc.Resource - : MG_Pipe::kMGPipeNullHandle); - if (pushedStorage == nullptr) { - MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " - "storage and uploads cannot be driven from the pushed descriptor " - "(handle {%u, %u})", - TextureDiagName(stateTextureObject, pushedStorage), pushedRes.Slot, - pushedRes.Gen); - return; - } -#if MOBILEGL_BUILD_DISAGGREGATED - // SHARED IMAGES: ahead of the view test and of the staged-store and serial gates below, - // none of which knows the texels belong to an image - the staged store holds none of - // them, and the storage path would allocate fresh, empty storage over the image. - if (pushedStorage != nullptr && (pushedStorage->SharedImageId != 0 || m_sharedImageId != 0) && - SyncSharedImageStorage(*pushedStorage)) { - // EVERY use, not only the attach (the draw-clean gates never pass a bound image): - // a write that landed since the last one is waited for on this context's GPU - // before the command this sync is for, and the session's frame notes the read. - if (m_sharedImage != nullptr) SharedImageImpl::AcquireForSampling(m_sharedImage); - ReplaySharedImageUploads(*pushedStorage); - // A YUV image is converted into the name at the frame's first use. - if (m_sharedImage != nullptr && SharedImageImpl::IsYuv(m_sharedImage)) - SharedImageImpl::ConvertYuvForSampling(m_sharedImage, GetBackendTextureId(), - pushedStorage->Desc.Width, pushedStorage->Desc.Height); - return; - } -#endif - // P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE - // RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it - // had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage - // owner BY HANDLE, so both the question and its answer are server-owned. - // - // Ruling 1's arm: the push-monolith build keeps the frontend test below token for - // token, because there the two answers are the same answer and the verify comparator - // needs the frontend arm. - if (pushedStorage != nullptr && - !MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) { - SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject); - return; - } - -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - // P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads. - // Resolved once, here, because three things below want it - the cheap gate, the - // per-level dirty questions and the union box / rect list / strides the staging - // planner takes its shape from. - // - // The record pointer stays valid for the whole function: only a create/respecify - // for a NEW slot grows MGPipeApplierState::TextureResources, and nothing this - // function calls emits one. Pointers INTO record->PendingUploads do not - the - // consume is a swap-and-pop - so every one of them is taken, used and dropped - // inside one level's iteration, and never held across a consume. - if (pushedStorage != nullptr) { - // THE METADATA RESPECIFY, HONOURED (ID-18 M4, review M-2). BindMask and - // ImageBindableHint are STICKY facts the client discovers AFTER allocation - a - // texture first bound as a shader image - and an IMMUTABLE texture never has a - // later redefinition to carry them, so they arrive on a respecify that restates - // the storage the resource already has. The applier classifies such a record as a - // metadata update: no reallocation ack, no pending-upload clear, Serial moved. - // THE TWIN'S HALF IS TO RE-DERIVE ITS STORAGE FLAGS FROM THE NEW MASK AND - // RECREATE ONLY WHERE THE BACKEND NEEDS THE FLAG AT CREATION - and image - // bindability is exactly such a flag on this backend: the carrier may be channel- - // widened, which is a different allocation, not a different binding. - // - // RequireImageBindableStorage IS that path and it is idempotent, so this is the - // whole of the re-derivation: it sets the sticky flag, clears m_isInitialized so - // the storage below is regenerated, re-arms every defined level in BOTH dirty - // models (see its own comment) so the widened carrier is replayed rather than - // allocated empty, and forces the parameter resync the widening's swizzle needs. - // Arriving on the CREATE instead - the common case once B publishes the hint - - // costs nothing: m_isInitialized is already false and there are no levels to - // replay. - const Bool pushedWantsImageBindableStorage = - pushedStorage->Desc.ImageBindableHint != 0 || - (pushedStorage->Desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0; - if (pushedWantsImageBindableStorage && !m_imageBindableStorageRequired) { - // P5e (tx2), ruling 1's arm: under a transport the re-dirty half is a NAMED - // refusal at its entry (§5.2) instead of a walk of the client's level shadows; - // the push-monolith build keeps the frontend transition exactly as it is. - RequireImageBindableStorageByHandle(pushedRes, *pushedStorage); - } - - // ONE compare replaces the whole cheap-gate trio AND the content version. The - // applier bumps Serial on every respecify and every sub-data it applies to this - // resource, which is exactly the union those four keys covered - without the - // coarse behaviour the sampling-resolution generation had, where any texture's - // shape churn re-opened every other texture's gate. - // - // The pending set is the second half and it is not redundant: a previous sync - // may have BAILED (an incomplete texture returns early, a multisample target - // refreshes and skips) with the serial already stamped, and the set is what - // survives that. Empty means every level this record ever declared has been - // consumed by a sync that actually uploaded it. - if (m_isInitialized && m_syncedResourceSerial != 0 && - m_syncedResourceSerial == pushedStorage->Serial && - pushedStorage->PendingUploads.empty()) { - return; - } - } - - // First-level clean gate (see the member comment): three version compares and no - // virtual shape walk. Every mutation the slower probe below would catch bumps one of - // the keys - shape via the context's sampling-resolution generation (coarse: any - // texture's shape churn re-opens every gate, which only costs a fall-through to the - // probe), CPU pixels via the content version, samples/fixed-locations via the params - // version - and backend-side storage resets clear m_isInitialized. Restricted to - // Mipmap storage like the probe fast path: a buffer texture's backing store can move - // without any of these keys noticing. - // - // LEGACY ARM ONLY from P4a on: the memos it reads are the pre-handle ones and the - // record above answers the same question in one compare. - if (pushedStorage == nullptr) - if (m_isInitialized && m_syncedShapeContextId != 0 && MG_Pipe::gPipeInputs->IsLive() && - m_syncedShapeContextId == MG_Pipe::gPipeInputs->GetTextureContextId() && - m_syncedShapeGeneration == MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration() && - m_syncedContentVersion == stateTextureObject->GetContentVersion() && - m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() && - stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { - return; - } - - MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId, - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - - GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject)); - auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject); - MGLOG_D(" Texture target for syncing is %s", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - if (!IsSupportedTextureTarget(targetInternal)) { - MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - return; - } - - // The texture needs to be regenerated completely with glTexImage* calls if: - // 1. Not initialized - // 2. InternalFormat changed - // 3. Size changed - // 4. Mipmap levels changed - - // IsComplete() is the sampling predicate, and it calls a chain whose lower levels are - // undefined incomplete - which is what a top-down build (upload level N, then level 0) - // and ARB_clear_texture's conformance cases both produce. Bailing out on that shape - // left the backend name with no levels whatsoever, so the level that WAS defined could - // never be sampled or read back. Sync whenever some level holds an image; the per-level - // loops below skip the degenerate ones individually. - // P5c (gt, CONTRACT-P5C §6 layer 1): on the staged arm the same question is answered - // out of the SERVER's staged-texture store, never out of the frontend object - the - // store's Defined-ness (fed by the respecify hook and the sub-data adoption) IS "some - // level holds an image", and IsComplete()'s sampling half is redundant with it: a - // complete mipmap chain has a defined level 0, and the gate's whole job is to let an - // incomplete-but-partly-defined chain through. The one divergence is deliberate and - // stated: a chain whose EVERY level is 0x0 is complete-by-quirk on the frontend (the - // "0x0 in last level" relaxation, TextureObject.cpp) and undefined here - and syncing - // it would upload nothing either way, because every per-level loop below skips a - // {0,0,0} level individually. A BUFFER texture never has staged levels, so its gate - // is the descriptor's format - exactly what TextureObjectBase::IsComplete() reduces - // to for that storage kind. - if (MGB_STAGED_TEXTURE_LIVE) { - const auto& stagedDesc = pushedStorage->Desc; - Bool anyDefined = false; - if (static_cast(stagedDesc.StorageKind) == TextureStorageType::Buffer) { - // A buffer texture has no staged levels; its gate is the descriptor's - // format, which is what TextureObjectBase::IsComplete() reduces to here. - anyDefined = static_cast(stagedDesc.InternalFormat) != - TextureInternalFormat::Unknown; - } else { - auto& stagedStore = MG_Record::ServerStagedTexture(); - const Uint64 stagedKey = MG_Record::StagedTextureStore::KeyForHandle(pushedRes); - for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(stagedDesc.Target)) { - for (Uint32 level = 0; level < stagedDesc.Levels; ++level) { - if (stagedStore.IsLevelDefined(stagedKey, static_cast(uploadTarget), - static_cast(level))) { - anyDefined = true; - break; - } - } - if (anyDefined) break; - } - } - if (!anyDefined) { - MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - return; - } - } else - if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) { - MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", - stateTextureObject->GetExternalIndex()); - return; - } - - // Fast path: a fully-synced mipmap texture is the common per-draw case. - // SyncNeccessaryTextures re-syncs every bound texture each draw, and the - // scratch Bind below targets the temp unit - which sequential distinct - // textures thrash, forcing a real glBindTexture per texture per draw. When - // nothing needs uploading, skip the bind + upload machinery entirely; - // BindCurrentTextures() re-establishes the real sampling bindings regardless. - // The content-version stamp short-circuits before any shape probing: it - // bumps on every CPU-side pixel mutation, so an unchanged stamp plus an - // unchanged shape means no level can be dirty. Shape stays a separate - // compare because a NULL-data glTexImage changes it without touching the - // content version. - // - // LEGACY ARM ONLY from P4a on: on the handle arm the record's Serial plus an empty - // pending set is the same statement in one compare, made above and BEFORE the - // IsComplete()/shape walk rather than after it - so this probe would only re-derive - // an answer already given, out of the very frontend state the arm exists to stop - // reading. - if (pushedStorage == nullptr) - if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap && - m_syncedContentVersion != 0 && - m_syncedContentVersion == stateTextureObject->GetContentVersion()) { - auto* mipmapObject = - static_cast(stateTextureObject.get()); - const auto probeBaseSize = stateTextureObject->GetBaseSize(); - StateTextureBasicInfo probe = {stateTextureObject->GetFormat(), - static_cast(probeBaseSize.x()), - static_cast(probeBaseSize.y()), - static_cast(probeBaseSize.z()), - static_cast(mipmapObject->GetMipmapLevelCount()), - 0, - stateTextureObject->GetSamples(), - stateTextureObject->HasFixedSampleLocations()}; - if (probe == m_prevTextureInfo) { - MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.", - m_backendTextureId); - // The probe just proved "fully synced" from the real state, so the cheap - // gate may be (re)stamped here: the coarse generation only ever goes stale - // from OTHER textures' churn, and this draw re-validated this one. - if (MG_Pipe::gPipeInputs->IsLive()) { - m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId(); - m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); - m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); - } - return; - } - } - - Bind(target); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - const auto baseSize = MGB_STORAGE_BASE_SIZE(stateTextureObject); - StateTextureBasicInfo currentTextureInfo = {MGB_STORAGE_FORMAT(stateTextureObject), - static_cast(baseSize.x()), - static_cast(baseSize.y()), - static_cast(baseSize.z()), - 0, - 0, - MGB_STORAGE_SAMPLES(stateTextureObject), - MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject)}; - switch (MGB_STORAGE_KIND(stateTextureObject)) { - case TextureStorageType::Mipmap: { - auto* textureMipmapObject = - static_cast(stateTextureObject.get()); - const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject); - currentTextureInfo.mipmapLevels = mipmapCount; - - Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo); - if (needsRegeneration && m_backendStorageImmutable) { - RecreateBackendTexture(); - Bind(target); - } - - // Only a texture that is actually image-bound pays for the widening: it doubles - // or quadruples the storage, and RequireImageBindableStorage is sticky, so a - // texture that is merely sampled keeps its narrow format for life. See - // TextureImpl::GetImageBindableStorageWidening for what widens and why. - const TextureImpl::ImageBindableStorageWidening imageWidening = - m_imageBindableStorageRequired - ? TextureImpl::GetImageBindableStorageWidening(MGB_STORAGE_FORMAT(textureMipmapObject)) - : TextureImpl::ImageBindableStorageWidening{}; - - const Bool canAppendMipmaps = - m_isInitialized && - !m_imageBindableStorageRequired && - !MGB_STORAGE_IMMUTABLE(stateTextureObject) && - currentTextureInfo.internalFormat == m_prevTextureInfo.internalFormat && - currentTextureInfo.width == m_prevTextureInfo.width && - currentTextureInfo.height == m_prevTextureInfo.height && - currentTextureInfo.depth == m_prevTextureInfo.depth && - currentTextureInfo.bufferExternalIndex == m_prevTextureInfo.bufferExternalIndex && - currentTextureInfo.samples == m_prevTextureInfo.samples && - currentTextureInfo.fixedSampleLocations == m_prevTextureInfo.fixedSampleLocations && - currentTextureInfo.mipmapLevels > m_prevTextureInfo.mipmapLevels && - !TextureImpl::IsMultisampleTextureTarget(targetInternal); - - MGLOG_D("%s: Got texture info: %dx%dx%d, mips %d, format %s", __func__, baseSize.x(), baseSize.y(), - baseSize.z(), mipmapCount, - MG_Util::ConvertTextureInternalFormatToString(MGB_STORAGE_FORMAT(textureMipmapObject)).c_str()); - - if (canAppendMipmaps) { - MGLOG_D("Texture mip count increased for backend ID %u, appending levels %zu..%zu", - m_backendTextureId, m_prevTextureInfo.mipmapLevels, mipmapCount - 1); - - GLenum glInternalFormat, glType, glFormat; - TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat, - &glFormat, &glType, targetInternal); - - const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); - ScopedDefaultUnpackState unpackState; - for (auto& uploadTarget : uploadTargets) { - for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) { - auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); - // A level the application never defined reads back as {0, 0, 0}; now that a - // sparse chain is synced rather than skipped whole, leave those undefined on - // the driver instead of giving the name a 0x0 image at that index. - if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) { - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - continue; - } - auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); - bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level); - auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - auto* pData = (levelDirty && levelByteSize != 0) - ? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, - "append-mips") - : nullptr; - Vector convertedUploadData; - Vector widenedUploadData; - const void* uploadData = PrepareFallbackUpload( - MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData, - levelByteSize, glType, convertedUploadData, widenedUploadData); - Vector packedUploadData; - uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize, - uploadData, levelByteSize, &glType, packedUploadData); - - DebugImpl::ErrorLopper::Clear(); - BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned - const IntVec3 uploadSize = - GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), levelTexelSize); - switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) { - case TextureTarget::Texture2D: - case TextureTarget::TextureCubeMap: - g_GLESFuncs.glTexImage2D( - glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(uploadSize.x()), static_cast(uploadSize.y()), - 0, glFormat, glType, uploadData); - break; - case TextureTarget::Texture3D: - case TextureTarget::Texture2DArray: - // ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly - // like a 2D array whose depth is 6 * the cube count. - case TextureTarget::TextureCubeMapArray: - g_GLESFuncs.glTexImage3D( - glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(uploadSize.x()), static_cast(uploadSize.y()), - static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); - break; - default: - MGLOG_E_ONCE("Unhandled texture target %s", - MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str()); - break; - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, - glUploadTarget, glInternalFormat, glFormat, glType, - pData](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, format=%s, " - "type=%s, pixels=%p", - func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), - MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(), - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), - MG_Util::ConvertGLEnumToString(glFormat).c_str(), - MG_Util::ConvertGLEnumToString(glType).c_str(), pData); - }); - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - } - } - needsRegeneration = false; - } - - if (needsRegeneration) { - MGLOG_D("Texture state changed significantly or not initialized, regenerating texture with ID: %u", - m_backendTextureId); - // A REDEFINITION IN PLACE TAKES THE SAME GENERATION A RE-MINT TAKES (P4a fable - // seam F-3, the pre-handle half). Mutable driver storage is redefined on the - // SAME id below, so unlike RecreateBackendTexture nothing moves the FBO twins' - // memo: the frontend framebuffer versions do not see a texture's respecify - // and the id did not change. An attached texture whose format moved to one - // with the same carrier (GL_SRGB8 -> GL_SRGB8_ALPHA8 on a driver that widens - // the first) therefore kept the framebuffer's alpha-widening mask, and every - // draw into it stayed masked. The first definition is not a redefinition and - // bumps nothing; a redefinition that went through RecreateBackendTexture above - // has already bumped. - // PUSH BUILDS ONLY. This is Espryt code the pull build would share, and G1 - // keeps the pull library byte-identical to the P4a baseline (the bump resized - // this function and the renderbuffer twin's SyncToBackend: 0/0/2/0). So the - // pull build carries the pre-P4a hole until these lines land on dev on their - // own; every arm of a push build has the fix. - if (m_isInitialized && !m_backendStorageImmutable) { - ++FramebufferImpl::g_attachmentBackendIdGeneration; - } - - // Regenerate all mipmap levels - GLenum glInternalFormat, glType, glFormat; - TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat, - &glFormat, &glType, targetInternal); - ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); - - const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); - if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { - DebugImpl::ErrorLopper::Clear(); - BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned - // The frontend validates against the count MobileGL advertises, which can - // exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4, - // GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call: - // stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and - // framebuffer completeness still report what the application asked for. - const auto backendSamples = static_cast(ClampSamplesToBackendSupport( - GetFormatCapabilityTargetIndex(targetInternal), MGB_STORAGE_FORMAT(textureMipmapObject), - glFormat, static_cast(MGB_STORAGE_SAMPLES(stateTextureObject)))); - // ES 3.1 8.19 requires width/height (and depth, for the array target) >= 1, - // so a degenerate size has nothing to allocate and must not reach the - // driver. The frontend deallocates such an image rather than defining it - // (GL 4.6 core 8.8), so this is belt and braces for any path that still - // syncs one. - const Bool hasAllocatableSize = - baseSize.x() >= 1 && baseSize.y() >= 1 && - (targetInternal != TextureTarget::Texture2DMultisampleArray || baseSize.z() >= 1); - if (!hasAllocatableSize) { - MGLOG_D("Skipping multisample storage for texture %u: degenerate size (%d, %d, %d)", - m_backendTextureId, baseSize.x(), baseSize.y(), baseSize.z()); - } else { - switch (targetInternal) { - case TextureTarget::Texture2DMultisample: - g_GLESFuncs.glTexStorage2DMultisample( - target, backendSamples, glInternalFormat, - static_cast(baseSize.x()), static_cast(baseSize.y()), - MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE); - break; - case TextureTarget::Texture2DMultisampleArray: - g_GLESFuncs.glTexStorage3DMultisample( - target, backendSamples, glInternalFormat, - static_cast(baseSize.x()), static_cast(baseSize.y()), - static_cast(baseSize.z()), - MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE); - break; - default: - MOBILEGL_ASSERT(false, "Unexpected multisample target: %d", - static_cast(targetInternal)); - break; - } - m_backendStorageImmutable = true; - } - // The one storage branch that cleared the ES error queue without ever - // draining it again, so anything this call raised was left for an - // unrelated later query to trip over. Paired with its two siblings now. - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target, - glInternalFormat, backendSamples](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*Multisample: target=%s, internalformat=%s, " - "samples=%d", - func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), - MG_Util::ConvertGLEnumToString(target).c_str(), - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), - static_cast(backendSamples)); - }); - for (const auto& uploadTarget : uploadTargets) { - for (SizeT level = 0; level < mipmapCount; ++level) { - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - } - } - } else if (MGB_STORAGE_IMMUTABLE(stateTextureObject) || m_imageBindableStorageRequired) { - DebugImpl::ErrorLopper::Clear(); - BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned - const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize); - switch (MapToBackendTextureTarget(targetInternal)) { - case TextureTarget::Texture2D: - case TextureTarget::TextureCubeMap: - g_GLESFuncs.glTexStorage2D(target, static_cast(mipmapCount), glInternalFormat, - static_cast(storageSize.x()), - static_cast(storageSize.y())); - break; - case TextureTarget::Texture3D: - case TextureTarget::Texture2DArray: - case TextureTarget::TextureCubeMapArray: - g_GLESFuncs.glTexStorage3D(target, static_cast(mipmapCount), glInternalFormat, - static_cast(storageSize.x()), - static_cast(storageSize.y()), - static_cast(storageSize.z())); - break; - default: - MGLOG_E_ONCE("Unhandled immutable texture target %s", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - break; - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target, - glInternalFormat](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*: target=%s, internalformat=%s", func, - file, line, MG_Util::ConvertGLEnumToString(err).c_str(), - MG_Util::ConvertGLEnumToString(target).c_str(), - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); - }); - m_backendStorageImmutable = true; - - ScopedDefaultUnpackState unpackState; - for (auto& uploadTarget : uploadTargets) { - for (SizeT level = 0; level < mipmapCount; ++level) { - auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); - const bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level); - if (levelDirty && levelByteSize != 0) { - auto levelTexelSize = - MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); - auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - auto* pData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, - "immutable-regen"); - Vector convertedUploadData; - Vector widenedUploadData; - const void* uploadData = PrepareFallbackUpload( - MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData, - levelByteSize, glType, convertedUploadData, widenedUploadData); - Vector packedUploadData; - uploadData = - PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize, - uploadData, levelByteSize, &glType, packedUploadData); - Vector imageWidenedUploadData; - uploadData = PrepareImageWidenedUpload(imageWidening, levelTexelSize, uploadData, - levelByteSize, imageWidenedUploadData); - - DebugImpl::ErrorLopper::Clear(); - BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned - const IntVec3 uploadSize = - GetBackendUploadSize(targetInternal, levelTexelSize); - switch (MapToBackendTextureTarget(targetInternal)) { - case TextureTarget::Texture2D: - case TextureTarget::TextureCubeMap: - g_GLESFuncs.glTexSubImage2D( - glUploadTarget, static_cast(level), 0, 0, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), glFormat, glType, uploadData); - break; - case TextureTarget::Texture3D: - case TextureTarget::Texture2DArray: - case TextureTarget::TextureCubeMapArray: - g_GLESFuncs.glTexSubImage3D( - glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), - static_cast(uploadSize.z()), glFormat, glType, uploadData); - break; - default: - break; - } - DebugImpl::ErrorLopper::Loop( - [file = __FILE__, line = __LINE__, func = __func__, glUploadTarget, - glFormat, glType, pData](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s. glTexSubImage*: target=%s, format=%s, " - "type=%s, pixels=%p", - func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), - MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(), - MG_Util::ConvertGLEnumToString(glFormat).c_str(), - MG_Util::ConvertGLEnumToString(glType).c_str(), pData); - }); - } - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - } - } - } else { - m_backendStorageImmutable = false; - ScopedDefaultUnpackState unpackState; - for (auto& uploadTarget : uploadTargets) { - for (SizeT level = 0; level < mipmapCount; ++level) { - auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); - // See the append-mips loop: an undefined level stays undefined on the - // driver rather than becoming a 0x0 image. - if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || - levelTexelSize.z() <= 0) { - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - continue; - } - auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); - bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level); - auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - auto* pData = (levelDirty && levelByteSize != 0) - ? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, - "mutable-regen") - : nullptr; - Vector convertedUploadData; - Vector widenedUploadData; - const void* uploadData = PrepareFallbackUpload( - MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData, - levelByteSize, glType, convertedUploadData, widenedUploadData); - Vector packedUploadData; - uploadData = - PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize, - uploadData, levelByteSize, &glType, packedUploadData); - MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, " - "levelDirty = %s", - __func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(), - level, levelTexelSize.x(), levelTexelSize.y(), levelTexelSize.z(), - levelByteSize, pData, levelDirty ? "true" : "false"); - - DebugImpl::ErrorLopper::Clear(); - BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned - auto textureTarget = MGB_TEXTURE_TARGET(stateTextureObject); - const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize); - switch (MapToBackendTextureTarget(textureTarget)) { - case TextureTarget::Texture2D: - case TextureTarget::TextureCubeMap: { - g_GLESFuncs.glTexImage2D( - glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), 0, glFormat, glType, uploadData); - break; - } - case TextureTarget::Texture3D: - case TextureTarget::Texture2DArray: - case TextureTarget::TextureCubeMapArray: { - g_GLESFuncs.glTexImage3D( - glUploadTarget, static_cast(level), (GLint)glInternalFormat, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), - static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); - break; - } - default: { - MGLOG_E_ONCE("Unhandled texture target %s", - MG_Util::ConvertTextureTargetToString(textureTarget).c_str()); - } - } - DebugImpl::ErrorLopper::Loop( - [file = __FILE__, line = __LINE__, func = __func__, glUploadTarget, - glInternalFormat, glFormat, glType, pData](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, " - "format=%s, type=%s, pixels=%p", - func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), - MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(), - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), - MG_Util::ConvertGLEnumToString(glFormat).c_str(), - MG_Util::ConvertGLEnumToString(glType).c_str(), pData); - }); - MGLOG_D("Regenerated mipmap level %d for texture with ID: %u", level, - m_backendTextureId); - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - } - } - } - - m_isInitialized = true; - } - - { // Update all dirty mipmap levels - if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { - const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); - for (const auto& uploadTarget : uploadTargets) { - for (SizeT level = 0; level < mipmapCount; ++level) { - if (MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) { - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - } - } - } - break; - } - - const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject); - GLenum glInternalFormat, glType, glFormat; - TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat, - &glFormat, &glType, targetInternal); - // The storage this level is being written into was widened when it was minted - // (see above), so the transfer pair has to describe the carrier here too - ES - // requires glTexSubImage's `format` to match the storage's base internal - // format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION. - ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); - const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); - ScopedDefaultUnpackState unpackState; - // Per-level scratch of the exact-footprint path, hoisted to reuse capacity. - Vector dirtyFootprint; - Vector stagingBlockOverflow; - for (auto& uploadTarget : uploadTargets) { - for (SizeT level = 0; level < mipmapCount; ++level) { - if (!MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) { - continue; - } - - auto byteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); - if (byteSize == 0) { - MGLOG_D("Mipmap level %d has no data, skipping update.", level); - continue; - } - - if (level > 0) - MGLOG_D("%s: Updating dirty mip %d for texture ID %u, size: %dx%d, " - "byteSize: %d", - __func__, level, m_backendTextureId, - MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).x(), - MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).y(), byteSize); - - auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned - DebugImpl::ErrorLopper::Loop( - [file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, - MG_Util::ConvertGLEnumToString(err).c_str()); - }); - auto texelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); - const void* mipData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, - "dirty-level"); - Vector convertedUploadData; - Vector widenedUploadData; - const void* uploadData = PrepareFallbackUpload( - MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, texelSize, mipData, byteSize, - glType, convertedUploadData, widenedUploadData); - Vector packedUploadData; - uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), texelSize, - uploadData, byteSize, &glType, packedUploadData); - // Leaves `uploadData` pointing at its own buffer when it fires, with - // texels four components wide where the shadow's are one or two. - Vector imageWidenedUploadData; - uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize, - imageWidenedUploadData); - const IntVec3 uploadSize = - GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), texelSize); - // Sub-rect upload: when only a region of the level changed (a - // 16x16 sprite in a 1024x512 atlas, the per-frame lightmap), upload - // just that region with UNPACK_ROW_LENGTH striding into the level. - // Not an optimization alone: the shadow never sees a GPU write, so - // the texels OUTSIDE the region may be stale there and must not go - // up over what the GPU drew. Every conversion above rewrites the - // whole level texel for texel into a tight buffer of its own, so the - // region is taken from whichever buffer `uploadData` now names, at - // that buffer's bytes per texel. - // - // THE SAME ON EVERY ARM (P11 M). `mipData` is the frontend shadow on - // the pull and push-monolith arms and the server's staged level on the - // split arms - the same tight client layout either way - and the - // conversions run on whichever side uploads, so the handle arm takes - // its region from the converted buffer exactly as the shadow arm - // does. What it must NOT take from the record then is the carried - // pitch and offset (below): those index the client's layout, not the - // converted one. - SizeT uploadBytes = 0; - if (uploadData == mipData) { - uploadBytes = byteSize; - } else if (!imageWidenedUploadData.empty() && - uploadData == imageWidenedUploadData.data()) { - uploadBytes = imageWidenedUploadData.size(); - } else if (!packedUploadData.empty() && uploadData == packedUploadData.data()) { - uploadBytes = packedUploadData.size(); - } else if (!widenedUploadData.empty() && uploadData == widenedUploadData.data()) { - uploadBytes = widenedUploadData.size(); - } else if (!convertedUploadData.empty() && uploadData == convertedUploadData.data()) { - uploadBytes = convertedUploadData.size() * sizeof(Float); - } - // A 1D array's ES image is a 2D array one texel high with the layers - // in depth, while the shadow keeps the layers as rows: same texels in - // the same order, so a region maps across by moving its y to z. (The - // target is the descriptor's on the split arms: the server may not - // name the client's texture object.) - const Bool layersAsRows = - MGB_TEXTURE_TARGET(stateTextureObject) == TextureTarget::Texture1DArray; - // P4a (D-D6, and ARCHITECTURE.md §9.1's ONE item moved out of the - // Espryt do-not-touch list): the DECISION below is unchanged and - // stays on the server - it is the side that pays the GPU cost, and - // the union-box-versus-N-rects choice is worth 16x the bytes on one - // axis and +6 ms/frame of Mali job cost on the other. What moves is - // the SOURCE of the shape and of the strides. - // - // On the handle arm the box and the rect list come from the record - // the client emitted; on the legacy arm they come from the frontend's - // own rect model, exactly as before. Both spellings feed the same - // three staging shapes and the same StageBlocksIntoUnpackRing, which - // is byte-identical (G5). - const MG_Pipe::MGPipeResourceRecord::PendingUpload* pendingUpload = - pushedStorage != nullptr - ? FindPipeTextureUpload(*pushedStorage, static_cast(uploadTarget), - static_cast(level)) - : nullptr; - const auto dirtyRegion = [&]() -> MG_State::GLState::MipmapDirtyRegion { - if (pendingUpload == nullptr) { - // P5c (tx): a level this arm owes with NO pending upload behind it - // was dirtied by the GPU (T5), and the dirty answer is the - // server's own mark on the staged shadow - the whole level, - // because a generation touches all of it. The client is never - // asked (§2.2's last row). - if (MGB_STAGED_TEXTURE_LIVE) { - const IntVec3 gpuExtent = - MG_Record::ServerStagedTexture().LevelExtentOrUndefined( - MG_Record::StagedTextureStore::KeyForHandle(pushedRes), - static_cast(uploadTarget), static_cast(level)); - return MG_State::GLState::MipmapDirtyRegion{IntVec3{0, 0, 0}, gpuExtent}; - } - return textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level); - } - // MGPBox is {origin, extent}; MipmapDirtyRegion is {lo, hi}. The - // conversion is the whole difference between the two spellings. - const auto& box = pendingUpload->UnionBox; - return MG_State::GLState::MipmapDirtyRegion{ - IntVec3{box.X, box.Y, box.Z}, - IntVec3{box.X + static_cast(box.W), box.Y + static_cast(box.H), - box.Z + static_cast(box.D)}}; - }(); - const SizeT texelCount = static_cast(texelSize.x()) * - static_cast(texelSize.y()) * - static_cast(std::max(texelSize.z(), 1)); - // Whether a box of the level can be addressed at all: whole texels in - // the upload bytes, and an ES image shaped like the shadow. - const Bool regionsAddressable = - !dirtyRegion.Empty() && texelCount > 0 && uploadBytes > 0 && - uploadBytes % texelCount == 0 && - (layersAsRows ? std::max(texelSize.z(), 1) == 1 - : uploadSize.x() == texelSize.x() && uploadSize.y() == texelSize.y() && - std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1)); - const Bool subRectEligible = regionsAddressable && !dirtyRegion.CoversWholeLevel(texelSize); - const SizeT bpp = regionsAddressable ? uploadBytes / texelCount : 0; - const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(), - dirtyRegion.hi.y() - dirtyRegion.lo.y(), - dirtyRegion.hi.z() - dirtyRegion.lo.z()}; - // STRIDES ARE CARRIED, NEVER INFERRED (D-D3), once there is a record - // to carry them: a sub-rect's rows are not contiguous in the level - // shadow, so each MGPSubRegion states the level's row and slice pitch - // and 0 means "tightly packed", which is what a whole-level region - // sends. The legacy arm keeps computing them from the level extent - // and the bytes-per-texel it just derived - the same two numbers, - // from the other side of the wire. - // m-2: THE PITCH IS THE LEVEL'S (D-D3), so every region of one level - // states the same one - and v1 read Regions.front() and would have - // strided the rest at the first one's pitch, silently, if a record - // ever disagreed. This is the assert the review asked for rather than - // a per-region loop feeding per-region strides: a record whose regions - // disagree is corrupt, not a shape the upload planner should learn. - // On disagreement the carried pair is DROPPED and the level's own - // extent times bpp is used, which is what the legacy arm computes. - Bool pendingStridesAgree = true; - if (pendingUpload != nullptr && !pendingUpload->Regions.empty()) { - const auto& firstRegion = pendingUpload->Regions.front(); - for (const auto& region : pendingUpload->Regions) { - if (region.SrcRowStride == firstRegion.SrcRowStride && - region.SrcSliceStride == firstRegion.SrcSliceStride) { - continue; - } - pendingStridesAgree = false; - MGLOG_E_ONCE("MGPipe: texture %u level %u carries regions with two " - "different level pitches (%u/%u and %u/%u) - a pitch is " - "the LEVEL's, so the carried pair is dropped and the " - "extent is used instead", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), - static_cast(level), firstRegion.SrcRowStride, - firstRegion.SrcSliceStride, region.SrcRowStride, - region.SrcSliceStride); - break; - } - } - // P11 M: THE CARRIED PITCH AND OFFSET INDEX THE CLIENT'S LAYOUT - the - // level shadow, or the server's staged copy of it, which is `mipData`. - // A converted level's region is cut from the conversion's own tight - // buffer (above), whose texels are wider, so there the record's numbers - // name the wrong bytes and the pitch is derived from the upload - // buffer's bpp instead - which is what the shadow arm does. - const Bool carriedLayoutApplies = uploadData == mipData; - const SizeT carriedRowBytes = - (pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies && - !pendingUpload->Regions.empty() && - pendingUpload->Regions.front().SrcRowStride != 0) - ? static_cast(pendingUpload->Regions.front().SrcRowStride) - : 0; - const SizeT carriedSliceBytes = - (pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies && - !pendingUpload->Regions.empty() && - pendingUpload->Regions.front().SrcSliceStride != 0) - ? static_cast(pendingUpload->Regions.front().SrcSliceStride) - : 0; - const SizeT levelRowBytes = - carriedRowBytes != 0 ? carriedRowBytes : static_cast(texelSize.x()) * bpp; - const SizeT levelSliceBytes = carriedSliceBytes != 0 - ? carriedSliceBytes - : static_cast(texelSize.y()) * levelRowBytes; - // N-5: THE SAME DISCIPLINE FOR SrcOffset AS FOR THE STRIDES, and the - // asymmetry it removes was the review's point: the strides got a loud - // refusal in the commit that started reading them while the offset - - // which decides the ADDRESS glTexSubImage reads (w*bpp x h x d) bytes - // from - was consumed raw. The cross-check is free, because - // rectUploadPtr four lines down computes exactly the same number from - // the region origin and the two pitches. A record whose regions - // disagree with their own origins is corrupt, so on disagreement the - // carried offsets are DROPPED - every use site falls back to the - // derived pointer, which is what the legacy arm sends - and one named - // line says so. Gated on subRectEligible because bpp is 0 otherwise - // and the regions are not read at all, and on carriedLayoutApplies - // because a converted level never reads them (MGB_RECT_SRC_PTR). - // - // FIX A2 IS THE SECOND LEGITIMATE DISAGREEMENT, and the fallback is - // exactly right for it: a level too large to stage whole crosses as - // several slab records (TextureEmit.h's MGPipeForEachTextureSlab) whose - // regions carry offsets into THEIR OWN run rather than into the level - // shadow. Every piece of the level is merged into this one entry, the - // entry's rects are then read out of the SERVER'S WHOLE LEVEL IMAGE - - // which is what the derived pointer names - and those piece-relative - // offsets are precisely the ones that must not be used here. - // - // GATED ON regionsAddressable, NOT subRectEligible: the per-region arm - // for a level the GPU may have written (m_driverMayHoldGpuWrites below) - // reads MGB_RECT_SRC_PTR too, and it runs exactly when the union box is - // the whole level - which is what two or more slab pieces always add up - // to. Checked only under subRectEligible, a damage rect split across - // pieces kept the later pieces' run-relative offsets and read their - // rows from the top of the level image (KWin's wl_shm window textures: - // the panel repeated down the desktop). bpp is valid whenever the - // regions are addressable. - Bool pendingOffsetsAgree = true; - if (pendingUpload != nullptr && regionsAddressable && carriedLayoutApplies) { - for (const auto& region : pendingUpload->Regions) { - const SizeT derivedOffset = static_cast(region.Z) * levelSliceBytes + - static_cast(region.Y) * levelRowBytes + - static_cast(region.X) * bpp; - if (static_cast(region.SrcOffset) == derivedOffset) continue; - pendingOffsetsAgree = false; - MGLOG_E_ONCE("MGPipe: texture %u level %u carries a region whose SrcOffset " - "%llu is not its own origin (%d,%d,%d) at the level's pitch " - "(%llu expected) - the carried offsets are dropped and every " - "rect is read from the shadow instead. Expected once for a " - "level cut into stage-chunk pieces, whose regions index their " - "own run; otherwise this names a record whose regions and " - "origins disagree", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), static_cast(level), - static_cast(region.SrcOffset), region.X, - region.Y, region.Z, - static_cast(derivedOffset)); - break; - } - } - const Uint8* regionPtr = - static_cast(uploadData) + - static_cast(dirtyRegion.lo.z()) * levelSliceBytes + - static_cast(dirtyRegion.lo.y()) * levelRowBytes + - static_cast(dirtyRegion.lo.x()) * bpp; - // Scatter refinement behind the union box: ~100 sprite - // writes into an atlas leave a box that spans nearly the - // whole level while the touched texels are a few percent of - // it. The storage's bounded rect list recovers the true - // footprint; each rect is uploaded with the same - // ROW_LENGTH striding into the level as the box - // path. The storage only hands the list out when its - // summed area is materially smaller than the box (0 - // otherwise), so the extra calls always move fewer bytes. - MG_State::GLState::MipmapDirtyRegion - dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; - SizeT dirtyRectCount = 0; - if (subRectEligible) { - if (pendingUpload != nullptr) { - // RegionCount == 0 is LEGAL and means "the union box is the - // whole story" (D-D3), which is the same statement the - // frontend's GetStorageDirtyRects makes by returning 0 - so - // both arms reach the box branch below by the same route. - // - // P11 M2: the record's list is the level's EXACT footprint now - // (TextureEmit.h), which the storage's own rect list only - // covered, and the storage withheld that list - "0 rects" - when - // it held more rects than it keeps or when they added up to 3/4 - // of the union box (one driver call on nearly the same bytes). - // Both cut-offs are this side's to apply now, so the box-or-rects - // choice on a texture the GPU has not written is what it was. - // (Once the GPU may have written it, the footprint below uploads - // every region exactly, whatever this chose.) Never a partial - // list: a truncated list is dropped writes. - SizeT summedTexels = 0; - for (const auto& region : pendingUpload->Regions) { - summedTexels += static_cast(region.W) * static_cast(region.H) * - static_cast(std::max(region.D, 1)); - } - const Bool listWithheld = - pendingUpload->Regions.size() < 2 || - pendingUpload->Regions.size() > MG_State::GLState::MipmapStorage::kMaxDirtyRects || - summedTexels * 4 >= dirtyRegion.TexelCount() * 3; - dirtyRectCount = listWithheld ? 0 : pendingUpload->Regions.size(); - for (SizeT r = 0; r < dirtyRectCount; ++r) { - const auto& region = pendingUpload->Regions[r]; - dirtyRects[r] = MG_State::GLState::MipmapDirtyRegion{ - IntVec3{region.X, region.Y, region.Z}, - IntVec3{region.X + static_cast(region.W), - region.Y + static_cast(region.H), - region.Z + static_cast(region.D)}}; - } - } else - // tx: with an active transport the rect list is the record's (the - // pendingUpload arm above) or nothing - the server's GPU-dirty mark - // is whole-level and has no scatter refinement to hand out, and the - // frontend's rect model is not this side's to read. - if (!MGB_STAGED_TEXTURE_LIVE) - dirtyRectCount = textureMipmapObject->GetStorageDirtyRects( - uploadTarget, level, dirtyRects, - MG_State::GLState::MipmapStorage::kMaxDirtyRects); - // The scatter refinement pays only on the client-pointer path, - // where fewer bytes mean less driver-side copying. Through the - // unpack ring every glTexSubImage is a GPU copy job (Mali), so - // ~100 sprite rects become ~100 jobs whose fixed cost dwarfs - // the union box's extra bytes - measured +6 ms/frame of GPU - // time in MC's animated-atlas ticks. One box, one job. - if (BufferImpl::UnpackRingAvailable()) { - dirtyRectCount = 0; - } - } - // Everything above - the union box, the ring's one-box rule, the rect - // list with its merges, fold and 3/4 cut-off - may upload shadow texels - // the client did not write, which is only harmless while the driver - // still holds those texels as they were last uploaded. Once it may hold - // GPU writes, the level goes up as exactly the texels written, one box - // (one ring job) each, whatever that costs: more boxes, and a union box - // that spans the whole level no longer means a whole-level upload. - const MG_State::GLState::MipmapDirtyRegion* uploadRects = dirtyRects; - SizeT uploadRectCount = dirtyRectCount; - if (m_driverMayHoldGpuWrites && regionsAddressable) { - if (pendingUpload != nullptr) { - // The handle arm's footprint is the record's region list, all - // of it and in order (MGB_RECT_SRC_PTR indexes the regions by - // position); a box-only entry is its union box. - dirtyFootprint.clear(); - for (const auto& region : pendingUpload->Regions) { - dirtyFootprint.push_back(MG_State::GLState::MipmapDirtyRegion{ - IntVec3{region.X, region.Y, region.Z}, - IntVec3{region.X + static_cast(region.W), - region.Y + static_cast(region.H), - region.Z + static_cast(region.D)}}); - } - } else - // With an active transport and no record behind the level, the - // server's own whole-level GPU-dirty mark is the whole story (the - // same reason the rect list above is not read here). - if (MGB_STAGED_TEXTURE_LIVE) { - dirtyFootprint.clear(); - } else - textureMipmapObject->GetStorageDirtyFootprint(uploadTarget, level, dirtyFootprint); - // One box is the union box itself, which the branches below already - // upload exactly (as the whole level when it is the whole level). - if (dirtyFootprint.size() >= 2) { - uploadRects = dirtyFootprint.data(); - uploadRectCount = dirtyFootprint.size(); - } else { - uploadRectCount = 0; - } - } - const Bool perRectUpload = regionsAddressable && uploadRectCount >= 2; - const auto rectUploadPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) { - return static_cast(uploadData) + - static_cast(rect.lo.z()) * levelSliceBytes + - static_cast(rect.lo.y()) * levelRowBytes + - static_cast(rect.lo.x()) * bpp; - }; -// m-1 / D-D3, "carried, never inferred": MGPSubRegion::SrcOffset is the byte offset of that -// region's origin into the level shadow - the same number the lambda above derives from the -// origin and the two strides - and on the handle arm it is READ rather than re-derived, after -// the cross-check above has agreed that the two spellings say the same thing (N-5). It is read -// only while carriedLayoutApplies (uploadData == mipData), so the base the offset is relative to -// is the level shadow itself and never a conversion buffer: since dev's 8966d5c2 a converted -// level is sub-rect eligible too, and its rects are cut at the conversion's bpp (rectUploadPtr). -// -// THREE USE SITES, AND THE FIRST OF THEM IS UNREACHABLE (N-5's second half, and the correction -// to what esprytobj-v2.md m-1 claimed): dirtyRectCount is forced to 0 whenever -// UnpackRingAvailable(), and the staging-block loop that takes MGB_RECT_SRC_PTR(r, rect) first -// requires `ringUsable && dirtyRectCount >= 2`. The two conditions cannot both hold. That shape -// is pre-existing (the ring/scatter decision, not this package's), so the loop is left as it -// stands and the count is corrected here instead of the loop being deleted underneath it. -// -// A MACRO AND NOT A SECOND LAMBDA, for DV-9's measured reason: this function's nine ErrorLopper -// lambdas are mangled ...::$_N BY POSITION, so one more renumbers every one of them and moves -// the PULL build's symbol set, whose admitted-change set is EMPTY. The pull expansion is the -// pre-P4a call with one pair of parentheses. #undef'd with the rest. -#define MGB_RECT_SRC_PTR(idx, rect) \ - ((pendingUpload != nullptr && pendingOffsetsAgree && carriedLayoutApplies && \ - static_cast(idx) < pendingUpload->Regions.size()) \ - ? static_cast(uploadData) + \ - static_cast(pendingUpload->Regions[static_cast(idx)].SrcOffset) \ - : rectUploadPtr(rect)) - // A shadow-grid box as the ES image addresses it, and the image height - // that strides the source to match; they differ only for a 1D array - // (layersAsRows), whose rows are ES layers of a one-texel-high image. - struct BackendBox { - GLint x, y, z; - GLsizei w, h, d; - }; - const auto ToBackendBox = [&](const MG_State::GLState::MipmapDirtyRegion& rect) { - const GLsizei w = static_cast(rect.hi.x() - rect.lo.x()); - const GLsizei h = static_cast(rect.hi.y() - rect.lo.y()); - const GLsizei d = static_cast(rect.hi.z() - rect.lo.z()); - return layersAsRows ? BackendBox{rect.lo.x(), 0, rect.lo.y(), w, 1, h} - : BackendBox{rect.lo.x(), rect.lo.y(), rect.lo.z(), w, h, d}; - }; - const GLint imageHeight = layersAsRows ? 1 : texelSize.y(); - // Unpack-ring staging plan, decided ONCE for whichever branch - // below runs: either every glTexSubImage of this level sources - // from the ring or none does, so the pixel-unpack binding is - // toggled exactly once per level. The plan's three shapes line - // up 1:1 with the switch's three branches. - // - // Regions are repacked TIGHTLY (row length = the region's own - // width), which is why the ring path issues no glPixelStorei at - // all: the surrounding ScopedDefaultUnpackState already holds - // ROW_LENGTH/IMAGE_HEIGHT at 0, which is exactly what a tight - // block wants. It also stages strictly fewer bytes than the - // client-pointer path makes the driver walk, which strides over - // the whole level width. - UnpackStagingBlock - stagingBlockArray[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; - UnpackStagingBlock* stagingBlocks = stagingBlockArray; - SizeT stagingBlockCount = 0; - const Bool ringUsable = BufferImpl::UnpackRingAvailable(); - if (!ringUsable) { - // Nothing to plan: every branch below keeps the client - // pointer and its ROW_LENGTH striding, unchanged. - } else if (perRectUpload) { - // Only an exact footprint outgrows the array (see uploadRects). - if (uploadRectCount > MG_State::GLState::MipmapStorage::kMaxDirtyRects) { - stagingBlockOverflow.resize(uploadRectCount); - stagingBlocks = stagingBlockOverflow.data(); - } - for (SizeT r = 0; r < uploadRectCount; ++r) { - const auto& rect = uploadRects[r]; - stagingBlocks[r] = { - MGB_RECT_SRC_PTR(r, rect), - static_cast(rect.hi.x() - rect.lo.x()) * bpp, - static_cast(rect.hi.y() - rect.lo.y()), - static_cast(std::max(rect.hi.z() - rect.lo.z(), 1)), - levelRowBytes, - levelSliceBytes, - 0}; - } - stagingBlockCount = uploadRectCount; - } else if (subRectEligible) { - stagingBlocks[0] = {regionPtr, - static_cast(regionSize.x()) * bpp, - static_cast(regionSize.y()), - static_cast(std::max(regionSize.z(), 1)), - levelRowBytes, - levelSliceBytes, - 0}; - stagingBlockCount = 1; - } else if (uploadData == mipData && texelCount > 0 && - byteSize % texelCount == 0) { - // Whole level, shadow bytes verbatim: `byteSize` is exactly - // what the driver would have read from the client pointer, - // and the shadow is already tightly packed. A CONVERTED - // level stays on the pointer path - its buffer's length is - // the conversion's, not the shadow's, so nothing here can - // size the driver's read of it. The whole-texels check is - // the same sanity the sub-rect path applies, and it is what - // makes "the shadow's bytes per texel == the transfer's" - // hold. (A 1D array's upload size permutes height and depth, - // but the texel count and the tight byte order are the same, - // so the flat copy still describes what the driver reads.) - stagingBlocks[0] = {static_cast(uploadData), - static_cast(byteSize), - 1, - 1, - static_cast(byteSize), - static_cast(byteSize), - 0}; - stagingBlockCount = 1; - } - const Bool ringStaged = - stagingBlockCount > 0 && - StageBlocksIntoUnpackRing(stagingBlocks, stagingBlockCount); - if (ringStaged) { - BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId()); - } - // Ring-sourced or not: see NoteInPlaceTextureUpload. - NoteInPlaceTextureUpload(); - if (MG_Util::PipeStats::Enabled()) { - // One emission per (upload target, level) that ships texels; - // the switch below turns it into either one union-box job or - // dirtyRectCount rect jobs. The box/rect split is counted - // separately from the bytes on purpose: SSIM is blind to it - // and the +6 ms/frame Mali cliff was a shape regression, not - // a byte regression (plan section 7.3). - const Bool rectShape = subRectEligible && dirtyRectCount >= 2; - Uint64 shippedBytes = 0; - if (rectShape) { - for (SizeT r = 0; r < dirtyRectCount; ++r) { - const auto& rect = dirtyRects[r]; - shippedBytes += static_cast(rect.hi.x() - rect.lo.x()) * - static_cast(rect.hi.y() - rect.lo.y()) * - static_cast(std::max(rect.hi.z() - rect.lo.z(), 1)) * - static_cast(bpp); - } - } else if (subRectEligible) { - shippedBytes = static_cast(regionSize.x()) * - static_cast(regionSize.y()) * - static_cast(std::max(regionSize.z(), 1)) * - static_cast(bpp); - } else { - shippedBytes = static_cast(byteSize); - } - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture, - shippedBytes); - MG_Util::PipeStats::AddCalls( - MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1); - MG_Util::PipeStats::AddCalls( - rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions - : MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, - 1); - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, - rectShape ? static_cast(dirtyRectCount) : 1u); - } - switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) { - case TextureTarget::Texture2D: - case TextureTarget::TextureCubeMap: - if (perRectUpload) { - if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); - for (SizeT r = 0; r < uploadRectCount; ++r) { - const auto& rect = uploadRects[r]; - g_GLESFuncs.glTexSubImage2D( - glUploadTarget, static_cast(level), rect.lo.x(), - rect.lo.y(), static_cast(rect.hi.x() - rect.lo.x()), - static_cast(rect.hi.y() - rect.lo.y()), glFormat, - glType, - ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset) - : static_cast(MGB_RECT_SRC_PTR(r, rect))); - } - // The surrounding ScopedDefaultUnpackState shadow says 0. - if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - } else if (subRectEligible) { - if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); - g_GLESFuncs.glTexSubImage2D( - glUploadTarget, static_cast(level), dirtyRegion.lo.x(), - dirtyRegion.lo.y(), static_cast(regionSize.x()), - static_cast(regionSize.y()), glFormat, glType, - ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset) - : static_cast(regionPtr)); - // The surrounding ScopedDefaultUnpackState shadow says 0. - if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - } else { - g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast(level), 0, 0, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), glFormat, - glType, - ringStaged - ? UnpackRingPixelOffset(stagingBlocks[0].offset) - : uploadData); - } - break; - case TextureTarget::Texture3D: - case TextureTarget::Texture2DArray: - // ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly - // like a 2D array whose depth is 6 * the cube count. - case TextureTarget::TextureCubeMapArray: - if (perRectUpload) { - if (!ringStaged) { - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); - g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); - } - for (SizeT r = 0; r < uploadRectCount; ++r) { - const auto box = ToBackendBox(uploadRects[r]); - g_GLESFuncs.glTexSubImage3D( - glUploadTarget, static_cast(level), box.x, box.y, box.z, - box.w, box.h, box.d, glFormat, glType, - ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset) - : static_cast(MGB_RECT_SRC_PTR(r, uploadRects[r]))); - } - if (!ringStaged) { - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); - } - } else if (subRectEligible) { - if (!ringStaged) { - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); - g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); - } - const auto box = ToBackendBox(dirtyRegion); - g_GLESFuncs.glTexSubImage3D( - glUploadTarget, static_cast(level), box.x, box.y, box.z, box.w, box.h, - box.d, glFormat, glType, - ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset) - : static_cast(regionPtr)); - if (!ringStaged) { - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); - } - } else { - g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), - static_cast(uploadSize.z()), glFormat, - glType, - ringStaged - ? UnpackRingPixelOffset(stagingBlocks[0].offset) - : uploadData); - } - break; - default: - MGLOG_E_ONCE("Unhandled texture target %s", - MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str()); - break; - } - if (ringStaged) { - // Back to the resting unbound state every other upload site - // in this file assumes (their BindPixelUnpackBufferId(0) is - // meant to stay a shadow no-op). - BufferImpl::BindPixelUnpackBufferId(0); - } - MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); - } - } - } - break; - } - case TextureStorageType::Buffer: { - auto* textureBufferObject = - static_cast(stateTextureObject.get()); - // P5e (tx2), scout R5 RESOLVED. THE BACKING BUFFER IS THE DESCRIPTOR'S, AND SO IS - // ITS WINDOW - `Desc.BufferForTexBuffer` names the store and `Desc.BufOffset / - // BufSize` name the range, so the frontend binding slot below is not read at all - // on the handle arm. - // - // The one thing that argument had to answer is D-A4's `frontend` parameter of - // EnsureBufferResourceForHandle, which existed for the emulated-persistent-map - // question. The E note's reading is confirmed at this head: that function's three - // frontend reads (Managers.cpp's MappedData / SyncPersistentMappedRange / - // HasDefinedContent) are all inside its MONOLITH arm - under an active transport - // it is client-free (kimi audit row 29) - so `nullptr` is not a hole, it is the - // whole of the split answer, and it is what §4.2's Buffer row already prescribes - // (`EnsureBufferResourceForHandle(nullptr, h)`). - // - // WHOLE-VS-RANGE likewise stops asking the buffer object for its size: the - // sentinel kMGPipeWholeBuffer IS "the whole store", which is the question - // `rangeSize == buffer->GetSize()` was spelling the long way round. - MG_Pipe::MGPipeHandle pushedTexBuffer = MG_Pipe::kMGPipeNullHandle; - if (pushedStorage != nullptr) { - pushedTexBuffer = pushedStorage->Desc.BufferForTexBuffer; - if (MG_Pipe::MGPipeHandleIsNull(pushedTexBuffer)) { - MGLOG_E_ONCE("MGPipe: buffer texture %u's descriptor names no backing buffer on " - "the handle arm, so it cannot be backed (handle {%u, %u})", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), pushedRes.Slot, pushedRes.Gen); - return; - } - } - // P5e (fix1 follow-up, ID-110): the third conjunct is the one fix1's report - // asked for a ruling about. This arm hands the backing-buffer lookup - // kNoFrontendBuffer and leans on the record answering; it was unreachable - // under monolith only because every writer of a handle-note is - // transport-gated - safe by a chain rather than by its own statement, which - // is exactly the shape that cost the monolith arm 137 scenarios. Under a - // transport the conjunct is already implied by `pushedStorage != nullptr`, so - // this changes no behaviour and closes no door; it says the thing instead of - // depending on it. - const Bool texBufferByRecord = pushedStorage != nullptr && - MGB_TEXTURE_HANDLE_ARM_OFF(*this) == false; - static const SharedPtr kNoFrontendBuffer{}; - auto& buffer = texBufferByRecord ? kNoFrontendBuffer - : textureBufferObject->GetBufferBindingSlot().GetBoundObject(); - if (!buffer && !texBufferByRecord) { - MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - return; - } - auto bufferIndex = buffer ? buffer->GetExternalIndex() : 0u; - // The memo key is the HANDLE's slot on the handle arm: a GL name is never an - // identity (section 4.2.1), and the record's Serial covers a slot recycled at a - // new generation because a respecify moves it. - currentTextureInfo.bufferExternalIndex = - pushedStorage != nullptr ? static_cast(pushedTexBuffer.Slot) : bufferIndex; - - Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo); - - // Need to sync texture buffer if not synced yet - auto* backendBufferResource = - BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer); - if (!backendBufferResource || backendBufferResource->id == 0) { - MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - return; - } - - // Bind buffer to texture - auto backendId = backendBufferResource->id; - - GLenum glInternalFormat, glType, glFormat; - TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureBufferObject), &glInternalFormat, &glFormat, - &glType, TextureTarget::TextureBuffer); - // The view half of the buffer-image SPLIT. A buffer texture has no storage of its - // own to widen, but the VIEW its format describes can be re-described one - // component at a time over the same bytes - rg32f over N texels is r32f over 2N - - // and WidenImageFormatsPass rewrites every access to subscript it that way. Only - // for a texture that is actually image-bound: a sampled-only buffer texture keeps - // the format the application asked for (see GetImageBindableBufferSplitFormat). - // - // The split goes on a SEPARATE name (m_bufferImageSplitViewId), not on this one. - // Re-describing the application's own texture also re-describes what a - // samplerBuffer reading it sees, and the sampler side is not subscript-rewritten - - // so texelFetch(s, i) started returning component 2i of the base view instead of - // texel i. rg32f is a legal SAMPLED buffer-texture format in ES 3.2; only the - // IMAGE binding needs the split, so only the image binding's name carries it. - const GLenum bufferImageSplitFormat = - m_imageBindableStorageRequired - ? TextureImpl::GetImageBindableBufferSplitFormat(MGB_STORAGE_FORMAT(textureBufferObject)) - : GL_UNKNOWN_MGL; - - if (needsRegeneration) { - // Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a - // 4.x context, so glTexBuffer is a legal call the app may make on any driver - - // but ES only gained them in 3.2, and g_GLESFuncs.glTexBuffer is simply null - // below that without EXT/OES_texture_buffer. Calling it was an unconditional - // null dereference. There is no conformant way to refuse the call (it is valid - // in the context MobileGL claims), so the texture is left unbacked and the - // reason is stated once per object, latched by the flag below. It was parked - // at MGLOG_I while the level ordering compiled MGLOG_W out of INFO builds; - // W is the correct level and now survives there. - if (!AreBufferTexturesSupported()) { - if (m_bufferTextureUnsupportedReported) { - break; - } - m_bufferTextureUnsupportedReported = true; - MGLOG_W("Texture buffer %u cannot be backed: this ES driver has no buffer " - "textures (%s). Every draw sampling it will read zero and every " - "shader declaring a samplerBuffer will fail to compile. MobileGL " - "still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an " - "OpenGL 4.x context may not report 0.", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), GetBufferTextureTierName(), - g_GLESCapabilities.MaxTextureBufferSize); - break; - } - MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with " - "ID: %u, buffer ID: %u, buffer size: %zu, format: %s", - m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize, - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); - // A texture that names a window of the buffer needs the range form; the - // whole-buffer forms report offset 0 and the buffer's current size, which - // glTexBuffer expresses more directly (and works where the range entry point - // is absent). - // The WINDOW is carried: MGPResourceDesc::BufOffset / BufSize, with - // kMGPipeWholeBuffer (~0) meaning "the whole store, resolved live" - which is - // why the whole-buffer arm still asks the BUFFER for its size. That is the - // buffer family's own object (P3a's, not this record's), and resolving it - // here is exactly what the sentinel is documented to mean. - const SizeT rangeOffset = pushedStorage != nullptr - ? static_cast(pushedStorage->Desc.BufOffset) - : textureBufferObject->GetBufferRangeOffset(); - // P5e (tx2): "the whole store" is the SENTINEL on the handle arm, not a size - // compare against a frontend BufferObject this arm may not name. The monolith - // arm keeps resolving the sentinel live, exactly as its comment says. - const Bool wholeBufferRange = - pushedStorage != nullptr - ? (pushedStorage->Desc.BufOffset == 0 && - pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer) - : (textureBufferObject->GetBufferRangeOffset() == 0 && - textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize()); - const SizeT rangeSize = - pushedStorage == nullptr - ? textureBufferObject->GetBufferRangeSizeInBytes() - : (pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer - ? (buffer ? buffer->GetSize() : 0u) - : static_cast(pushedStorage->Desc.BufSize)); - // Through CallTexBuffer/CallTexBufferRange rather than g_GLESFuncs directly: - // the unsuffixed entry points are the ES 3.2 core spelling, and a driver - // whose buffer textures come from EXT/OES_texture_buffer exports the - // suffixed ones instead. The dispatchers pick whichever this tier ships. - if (wholeBufferRange) { - CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); - } else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId, - static_cast(rangeOffset), - static_cast(rangeSize))) { - MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no " - "glTexBufferRange; binding the whole buffer instead", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); - } - DebugImpl::ErrorLopper::Loop( - [file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) { - MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s", - func, file, line, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), - backendId, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - // The image half of the SPLIT, on its own name over the same buffer. Minted - // lazily - only a texture that is both image-bound AND holds a format with no - // ESSL image spelling ever gets one - and re-pointed here, in the same - // regeneration gate as the view above, so the two never describe different - // buffers or different windows of one. - if (bufferImageSplitFormat != GL_UNKNOWN_MGL) { - if (m_bufferImageSplitViewId == 0) { - g_GLESFuncs.glGenTextures(1, &m_bufferImageSplitViewId); - } - if (m_bufferImageSplitViewId == 0) { - MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; " - "its image binding will read the unsplit view.", - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - } else { - g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId); - if (wholeBufferRange) { - CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId); - } else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId, - static_cast(rangeOffset), - static_cast(rangeSize))) { - CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId); - } - // The raw bind above went behind Bind()'s shadow, which tracks objects - // rather than names: leaving it claiming THIS object is bound would - // make the next Bind(GL_TEXTURE_BUFFER) a no-op and leave the split - // view bound in the application texture's place. - g_boundTexturesCache[g_activeTextureUnit][static_cast( - TextureTarget::TextureBuffer)] = nullptr; - } - } - } - break; - } - default: - // TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a - // backstop for a state object that grew a new storage kind. Skipping the upload - // renders wrong; throwing unwinds through the C GL ABI and kills the process. - MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; " - "skipping this sync", - static_cast(MGB_TEXTURE_STORAGE_KIND(pushedStorage, stateTextureObject)), - MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); - break; - } - - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - m_prevTextureInfo = currentTextureInfo; - if (pushedStorage != nullptr) { - // The handle arm's one stamp. Deliberately AFTER the switch, at the same - // instant the legacy arm stamps its four keys, so a bail arm that returned - // early leaves it untouched and the next sync re-runs - which is the property - // the pending set exists to make safe (D-D5). - // - // The record pointer was resolved before any driver work and TextureResources - // has not been grown since (nothing this function calls emits a create), so - // re-reading Serial through it is sound; the PendingUploads entries it named - // have been consumed one at a time by MGB_LEVEL_UPLOAD_DONE. - m_syncedResourceSerial = pushedStorage->Serial; - return; - } - // Everything dirty at entry is uploaded (or provably has no bytes to - // upload); stamp the version so per-draw re-syncs short-circuit until - // the next CPU-side mutation. - m_syncedContentVersion = stateTextureObject->GetContentVersion(); - // Same instant, so the cheap gate's keys describe exactly this synced state. - // Only Mipmap storage may arm it - the gate refuses other storage types anyway, - // but a stale trio must not linger on an object that later switches type. - if (MG_Pipe::gPipeInputs->IsLive() && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { - m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId(); - m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); - m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); - } else { - m_syncedShapeContextId = 0; - } - } -#undef MGB_LEVEL_NEEDS_UPLOAD -#undef MGB_LEVEL_UPLOAD_DONE #undef MGB_RECT_SRC_PTR -#undef MGB_STORAGE_FORMAT -#undef MGB_STORAGE_BASE_SIZE -#undef MGB_STORAGE_LEVELS -#undef MGB_STORAGE_SAMPLES -#undef MGB_STORAGE_FIXED_SAMPLE_LOCATIONS -#undef MGB_STORAGE_IMMUTABLE -#undef MGB_STORAGE_KIND -#undef MGB_TEXTURE_TARGET -#undef MGB_UPLOAD_TARGETS -#undef MGB_LEVEL_TEXEL_SIZE -#undef MGB_LEVEL_BYTE_SIZE -#undef MGB_LEVEL_TEXELS -#undef MGB_STAGED_TEXTURE_LIVE - - // P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD, by the handle the caller noted on - // the twin before it resolved it; the client allocator is never probed. - const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolveOwnRecord( - const SharedPtr& stateTextureObject) const { - (void)stateTextureObject; - // P13: a twin is only ever reached by handle, and the handle is noted on it first. - if (MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)) return nullptr; - return PipeTextureRecordForHandle(m_pushedSyncHandle); - } - - const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler( - const SharedPtr& stateTextureObject, - const MG_Pipe::MGPipeResourceRecord* record) { - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " - "built-in sampler cannot be pushed (handle {%u, %u})", - TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot, - m_pushedSyncHandle.Gen); - return nullptr; - } - // NO set_texture_params HAS BEEN APPLIED FOR THIS TEXTURE YET, which is not the same - // thing as a malformed one and must not be read as one. ParamsSerial is the - // applier's own "have I ever stored a params record here" (it is bumped on every - // set_texture_params and starts at 0), and Params is value-initialised beside it - - // so on a texture the application has never given a parameter to, BuiltinSampler is - // null because the record was never written, not because a client wrote a null. - // There is nothing to push in that state; SyncTextureParamsToBackend's own gate - // reaches the same conclusion silently (m_syncedParamsSerial starts at 0 too and the - // compare skips), and this decline is made silent for the same reason. Found by the - // verification round's census: three cases were declining loudly here on textures - // that had simply never been glTexParameter'd. - if (record->ParamsSerial == 0) { - MGLOG_D("Texture %u has no set_texture_params record yet, so it has no built-in " - "sampler to push.", - TextureDiagName(stateTextureObject, record)); - return nullptr; - } - // D-E1: kMGPipeNullHandle in a record that WAS written is Fatal{ProtocolCorruption} - // territory - EVERY ITextureObject owns a SamplerObject, so a null is a malformed - // record and not "no sampler". The applier is where that verdict is raised; this - // side names it and declines, because a backend that sampled through whatever the - // driver texture last held would be the silent half of the same bug. - const MG_Pipe::MGPipeHandle builtin = record->Params.BuiltinSampler; - if (MG_Pipe::MGPipeHandleIsNull(builtin)) { - MGLOG_E_ONCE("MGPipe: texture %u's set_texture_params names the null handle as its " - "built-in sampler CSO; every texture owns a sampler object, so this " - "record is malformed - declining the built-in sampler push", - TextureDiagName(stateTextureObject, record)); - return nullptr; - } - const auto* cso = PipeSamplerCsoRecordForHandle(builtin); - if (cso == nullptr) { - MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier " - "record - declining the built-in sampler push", - TextureDiagName(stateTextureObject, record), builtin.Slot, builtin.Gen); - return nullptr; - } - // THE HANDLE IS PART OF THE KEY, not just the serial. Sampler CSOs are - // content-addressed at capacity 256 (D-F1), so two textures with identical sampling - // share one CSO and a texture whose sampling diverges is re-pointed at a DIFFERENT - // CSO whose serial may well be smaller than the one it left. Keying on the serial - // alone would then skip the re-push and leave the driver texture filtering with the - // old CSO's values for ever. - // - // SamplerResync is the second half of the gate and it is the one that matters more - // than mis-filtering: ES makes a texture INCOMPLETE when its filters do not suit its - // level set, and an incomplete texture samples (0,0,0,1) rather than its contents. - // The server SETS it (RecreateBackendTexture) and the wire byte exists so the client - // can force a resync it knows about; neither side clears the other's copy (D-E2), so - // it is read here and never written back. - if (m_syncedBuiltinSampler == builtin && m_syncedBuiltinSamplerSerial == cso->Serial && - record->Params.SamplerResync == 0 && !m_forceSamplerResync) { - MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", - m_backendTextureId); - return nullptr; - } - m_syncedBuiltinSampler = builtin; - m_syncedBuiltinSamplerSerial = cso->Serial; - m_forceSamplerResync = false; - return &cso->Params; - } - - void BackendTextureObject::SyncBuiltinSamplerToBackend( - const SharedPtr& stateTextureObject) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - // P5e (tx2): NULL IS THE BY-HANDLE ARM, not an error, once a handle has been noted on - // this twin - the record and the built-in sampler's CSO answer everything below and - // the frontend object is not consulted at all. Without a handle it is still the - // caller's bug it always was. P5e (fix1): and with a handle noted on an arm that - // cannot serve it, it is Fatal{RoleViolation, "texture-handle-arm"} - see the - // refusal's own comment beside MGB_TEXTURE_HANDLE_ARM_OFF. - if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncBuiltinSamplerToBackend")) { - MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); - return; - } - - // P4a (D-E1): the fifteen sampler values are SAMPLER state (GL 4.6 table 23.18) and - // Espryt pushes them with glTexParameter* onto the TEXTURE rather than with - // glBindSampler onto the unit - behaviour P4a preserves exactly. What changes is - // where the values and the "did they move" answer come from. - // - // The whole arm choice is a preprocessor #if/#else rather than a runtime branch - // around one shared prologue, and that is a G1 requirement (D-P): the PULL build's - // text below the #else is the pre-P4a text token for token, so its object code - // cannot move. Routing the pull path through a pointer it did not have before was - // measured at -2 bytes on this function, which is a G1 failure. - const SamplerParameters* pendingSamplerParams = nullptr; - // P5e (tx2): resolved ONCE and shared with the target read below, which used to be - // the frontend `GetTarget()` this arm was not allowed to make. - const MG_Pipe::MGPipeResourceRecord* pushedDesc = nullptr; - pushedDesc = ResolveOwnRecord(stateTextureObject); - pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject, pushedDesc); - if (pendingSamplerParams == nullptr) return; - - MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", - m_backendTextureId, TextureDiagName(stateTextureObject, pushedDesc)); - auto targetInternal = MGB_TEXPARAM_TARGET(pushedDesc, stateTextureObject); - GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal); - MGLOG_D(" Texture target for syncing is %s", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - if (!IsSupportedTextureTarget(targetInternal)) { - MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - return; - } - - const SamplerParameters& samplerParams = *pendingSamplerParams; - if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { - m_cacheSamplerParameters = samplerParams; - return; - } - - Bind(target); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - // Update built-in sampler parameters - MGLOG_D("Updating sampler parameters for texture with ID: %u", m_backendTextureId); - -#define SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \ - if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \ - g_GLESFuncs.glTexParameteri(target, glName, \ - MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \ - m_cacheSamplerParameters.internalName = samplerParams.internalName; \ - DebugImpl::ErrorLopper::Loop( \ - [file = __FILE__, line = __LINE__, func = __func__, \ - t = MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)](GLenum err) { \ - MGLOG_D("%s(%s:%d) ES error %s, GL_TEXTURE_MIN_FILTER = %s", func, file, line, \ - MG_Util::ConvertGLEnumToString(err).c_str(), MG_Util::ConvertGLEnumToString(t).c_str()); \ - }); \ - } - - if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter || - m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) { - g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MIN_FILTER, - (GLint)ResolveBackendMinFilter(samplerParams, IsAngleLlvmpipeRenderer())); - m_cacheSamplerParameters.minFilter = samplerParams.minFilter; - m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode; - } - if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) { - g_GLESFuncs.glTexParameteri( - target, GL_TEXTURE_MAG_FILTER, - (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None)); - m_cacheSamplerParameters.magFilter = samplerParams.magFilter; - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode) - SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode) - if (SupportsWrapR(targetInternal)) { - SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode) - } else { - m_cacheSamplerParameters.wrapR = samplerParams.wrapR; - } - SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc) - SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode) - if (m_cacheSamplerParameters.minLod != samplerParams.minLod) { - g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MIN_LOD, samplerParams.minLod); - m_cacheSamplerParameters.minLod = samplerParams.minLod; - } - if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) { - g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); - m_cacheSamplerParameters.maxLod = samplerParams.maxLod; - } - if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { - if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { - g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, - samplerParams.maxAnisotropy); - } - // Unsupported GLES backends intentionally treat anisotropy as a - // frontend-only no-op; remember the observed value so the cache - // remains coherent without issuing an illegal enum every sync. - m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); -#undef SYNC_TEX_SAMPLER_PARAM_IF_CHANGED - } - - const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams( - const SharedPtr& stateTextureObject) { - // P5e (tx2): the handle the caller adopted this twin with, and only failing that the - // client allocator - ResolveOwnRecord holds the whole of that rule. - const auto* record = ResolveOwnRecord(stateTextureObject); - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " - "parameters cannot be pushed (handle {%u, %u})", - TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot, - m_pushedSyncHandle.Gen); - return nullptr; - } - // ParamsSerial + ForceResync replace m_syncedTextureParamsVersion + - // m_forceTextureParamsResync. ForceResync exists because the widened-channel - // carrier needs a swizzle override the frontend params version does not move for; - // it is SET by the server (RequireImageBindableStorage, RecreateBackendTexture set - // m_forceTextureParamsResync on the twin) and by the client on the wire, and - // NEITHER SIDE CLEARS THE OTHER'S (D-E2) - so the wire byte is read and never - // written back, while the twin's own flag is cleared here exactly as it was before. - if (m_syncedParamsSerial == record->ParamsSerial && record->Params.ForceResync == 0 && - !m_forceTextureParamsResync) { - MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.", - m_backendTextureId); - return nullptr; - } - m_syncedParamsSerial = record->ParamsSerial; - m_forceTextureParamsResync = false; - return record; - } - - // P4a (D-E1): the six values SyncTextureParamsToBackend pushes, spelled once per arm. - // MACROS, for the reason MGB_LEVEL_NEEDS_UPLOAD gives one function up: in the PULL build - // each expands to the pre-P4a expression at its original site, so that build's tokens - - // and its object code, whose symbol sizes G1 pins to the byte - are unchanged. Reading - // them into locals instead was measured at +9 bytes on this function. - // - // Four of the six come from MGPTextureParams, which is per texture OBJECT and - // independent of any binding - that is what closes D-E3's gap, where a texture that is - // only a READ-framebuffer attachment reaches SyncMipmapsToBackend and nothing else. The - // FORMAT comes from the descriptor beside them, and the BORDER comes from the built-in - // sampler's CSO (D-F4), because a border colour is sampler state. -#define MGB_TEXPARAM_LEVEL_RANGE \ - (pushedRecord != nullptr ? UintVec2{static_cast(pushedRecord->Params.BaseLevel), \ - static_cast(pushedRecord->Params.MaxLevel)} \ - : stateTextureObject->GetLevelRange()) -#define MGB_TEXPARAM_FORMAT \ - (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.InternalFormat) \ - : stateTextureObject->GetFormat()) -#define MGB_TEXPARAM_SWIZZLE \ - (pushedRecord != nullptr \ - ? Vec4{static_cast(pushedRecord->Params.Swizzle[0]), \ - static_cast(pushedRecord->Params.Swizzle[1]), \ - static_cast(pushedRecord->Params.Swizzle[2]), \ - static_cast(pushedRecord->Params.Swizzle[3])} \ - : stateTextureObject->GetAllSwizzleParams()) -// The contract now spells this encoding (c0c, ID-12 DV-2): kMGPipeDepthStencilModeDepth = 0 = -// GL_DEPTH_COMPONENT, kMGPipeDepthStencilModeStencil = 1 = GL_STENCIL_INDEX. v1 open-coded the -// direction as a bare `!= 0` and recorded it as a deviation; the constants are read here instead, -// so a future re-numbering moves both sides at once. ZERO MEANS THE DEFAULT is still what decides -// the direction: GL_DEPTH_COMPONENT is the GL and ES default and a texture that never asks for -// the stencil aspect never emits the call, so a zeroed record has to decode to exactly what such -// a texture already has. -#define MGB_TEXPARAM_DS_MODE \ - (pushedRecord != nullptr \ - ? (pushedRecord->Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX \ - : GL_DEPTH_COMPONENT) \ - : stateTextureObject->GetDepthStencilTextureMode()) -#define MGB_TEXPARAM_BORDER_F (pushedBorder != nullptr ? pushedBorder->borderColor : stateTextureObject->GetBorderColor()) -#define MGB_TEXPARAM_BORDER_I \ - (pushedBorder != nullptr ? pushedBorder->borderColorI : stateTextureObject->GetBorderColorI()) -#define MGB_TEXPARAM_BORDER_UI \ - (pushedBorder != nullptr ? pushedBorder->borderColorUI : stateTextureObject->GetBorderColorUI()) -#define MGB_TEXPARAM_BORDER_FORM \ - (pushedBorder != nullptr ? pushedBorder->borderColorForm : stateTextureObject->GetBorderColorForm()) - - void BackendTextureObject::SyncTextureParamsToBackend( - const SharedPtr& stateTextureObject) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; - // see the twin note in SyncBuiltinSamplerToBackend. - if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncTextureParamsToBackend")) { - MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); - return; - } - - // P4a (D-E1/D-E3): where the parameter values come from, and what says they moved. - // Null on the handle arm means "nothing to do" and is NEVER a fall-back to the - // frontend; the legacy arm keeps the params-version pair verbatim, token for token, - // because the PULL build's object code may not move (D-P). - const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr; - const SamplerParameters* pushedBorder = nullptr; - // Whether the four border comparands can be READ at all this sync. On the handle arm - // a missing sampler CSO makes them unreadable and the border block below is skipped - // with the refusal already named; on the legacy arm they always come from the - // frontend object, so it is constant true and folds away. - Bool borderColorReadable = true; - pushedRecord = ResolvePushedTextureParams(stateTextureObject); - if (pushedRecord == nullptr) return; - // D-F4: the border colour is SAMPLER state (GL 4.6 table 23.18) and lives on the - // built-in sampler's CSO, not on MGPTextureParams - P4a deliberately does not - // put SamplerParameters on the wire twice. All FOUR comparands (float, int, - // uint, form) still cross and are still all compared below, because two integer - // borders differing above 2^24 share one float and a Float -> Int transition can - // leave every number unchanged while needing a different driver entry point. - const auto* borderCso = PipeSamplerCsoRecordForHandle(pushedRecord->Params.BuiltinSampler); - if (borderCso != nullptr) { - pushedBorder = &borderCso->Params; - } else { - borderColorReadable = false; - MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier " - "record, so its border colour cannot be pushed", - TextureDiagName(stateTextureObject, pushedRecord), - pushedRecord->Params.BuiltinSampler.Slot, - pushedRecord->Params.BuiltinSampler.Gen); - } - - MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId, - TextureDiagName(stateTextureObject, pushedRecord)); - - auto targetInternal = MGB_TEXPARAM_TARGET(pushedRecord, stateTextureObject); - GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal); - MGLOG_D(" Texture target for syncing is %s", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - if (!IsSupportedTextureTarget(targetInternal)) { - MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", - MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); - return; - } - - // Multisample targets reject the *sampler* parameters (LOD range, border color) but - // GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them. - // Bailing out entirely used to drop every swizzle write on the floor, which is what the - // frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which - // deliberately omits the swizzle enums). Note the caches for the skipped parameters are - // still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the - // change detection below would swallow the very writes we came here to emit. - const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal); - if (isMultisampleTarget) { - m_cacheLodRange = MGB_TEXPARAM_LEVEL_RANGE; - if (borderColorReadable) m_cacheBorderColor = MGB_TEXPARAM_BORDER_F; - } - - Bind(target); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - // Update texture parameters - MGLOG_D("Updating texture parameters for texture with ID: %u", m_backendTextureId); - - const auto& levelRange = MGB_TEXPARAM_LEVEL_RANGE; - - if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) { - g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast(levelRange.x())); - m_cacheLodRange.x() = levelRange.x(); - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) { - g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast(levelRange.y())); - m_cacheLodRange.y() = levelRange.y(); - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - // A three-channel format widened to four to keep the image colour-renderable (see - // NormalizePixelFormat) gains an alpha channel the frontend format does not have, and - // whatever the draw that filled it wrote there is not what GL would report: a format - // without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the - // promotion stays invisible, composed with the swizzle the application asked for. - Vec4 swizzleParams = MGB_TEXPARAM_SWIZZLE; - if (TextureImpl::BackendTextureFormatAddsAlpha(MGB_TEXPARAM_FORMAT, targetInternal)) { - for (SizeT channel = 0; channel < 4; ++channel) { - if (swizzleParams[channel] == TextureSwizzleParam::Alpha) { - swizzleParams[channel] = TextureSwizzleParam::One; - } - } - } - // The same composition for the image-format widening, which can add TWO or THREE - // channels rather than one (GL_R8UI carried in a GL_RGBA8UI). GL reads a channel the - // format does not have as 0, except alpha, which reads as 1 - so a sampler must see - // those constants and not whatever the widened storage holds. The upload and the - // shader's own store mask already keep them at exactly these values; this covers - // storage nothing has written yet (glTexStorage with no upload), whose surplus - // channels are undefined. Composed with the application's own swizzle for the same - // reason as the alpha case above: GL_TEXTURE_SWIZZLE names a SOURCE channel of the - // logical texel, so it is the source that is substituted, never the destination. - if (const auto imageWidening = - m_imageBindableStorageRequired - ? TextureImpl::GetImageBindableStorageWidening(MGB_TEXPARAM_FORMAT) - : TextureImpl::ImageBindableStorageWidening{}) { - for (SizeT channel = 0; channel < 4; ++channel) { - switch (swizzleParams[channel]) { - case TextureSwizzleParam::Green: - if (imageWidening.SourceChannels < 2) swizzleParams[channel] = TextureSwizzleParam::Zero; - break; - case TextureSwizzleParam::Blue: - if (imageWidening.SourceChannels < 3) swizzleParams[channel] = TextureSwizzleParam::Zero; - break; - case TextureSwizzleParam::Alpha: - if (imageWidening.SourceChannels < 4) swizzleParams[channel] = TextureSwizzleParam::One; - break; - default: - break; - } - } - } - if (swizzleParams != m_cacheSwizzleParams) { -#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \ - if (m_cacheSwizzleParams.func != swizzleParams.func) { \ - g_GLESFuncs.glTexParameteri(target, glEnum, MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams.func)); \ - m_cacheSwizzleParams.func = swizzleParams.func; \ - } - SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(r(), GL_TEXTURE_SWIZZLE_R); - SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(g(), GL_TEXTURE_SWIZZLE_G); - SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(b(), GL_TEXTURE_SWIZZLE_B); - SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(a(), GL_TEXTURE_SWIZZLE_A); -#undef SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED - m_cacheSwizzleParams = swizzleParams; - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } - - // GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver - // without it every such call is INVALID_ENUM, so the parameter is simply not synced. - // - // The FORM has to be forwarded along with the value. A border colour set through - // glTexParameterIiv/Iuiv is an integer one, and an isampler2D/usampler2D fetch of the - // border returns whatever the driver's integer border register holds - so pushing it - // through glTexParameterfv handed the driver float 255.0 and the shader read back - // 1132396544, the bit pattern of that float. glTexParameterIiv/Iuiv are ES 3.2 core - // beside GL_TEXTURE_BORDER_COLOR itself, so they sit behind the same capability gate; - // the entry-point null check covers a driver that advertises the extension without them. - // The redundancy filter has to look at the AUTHORITATIVE representation, not just the - // float one: two integer borders that differ above 2^24 (16777216 and 16777217, say) - // collapse onto the same float, so a float-only comparison would skip the second sync and - // leave the driver holding the first value forever. - // m-3: GATED, because on the handle arm an unreadable border means the CSO record is - // missing and the block below is skipped with the refusal already named - so v1's - // unconditional initialiser was a live frontend read on a switched-over path whose - // value nothing then used. The value a skipped block sees is the cache's own, which - // cannot make the condition true. - const auto borderColorForm = borderColorReadable ? MGB_TEXPARAM_BORDER_FORM : m_cacheBorderColorForm; - if (!isMultisampleTarget && - borderColorReadable && - g_GLESCapabilities.SupportsTextureBorderClamp && - (m_cacheBorderColor != MGB_TEXPARAM_BORDER_F || - m_cacheBorderColorI != MGB_TEXPARAM_BORDER_I || - m_cacheBorderColorUI != MGB_TEXPARAM_BORDER_UI || - m_cacheBorderColorForm != borderColorForm)) { - if (borderColorForm == BorderColorForm::Int && g_GLESFuncs.glTexParameterIiv) { - const auto& borderColorI = MGB_TEXPARAM_BORDER_I; - const GLint borderColorArray[4] = {borderColorI.x(), borderColorI.y(), borderColorI.z(), - borderColorI.w()}; - g_GLESFuncs.glTexParameterIiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); - } else if (borderColorForm == BorderColorForm::Uint && g_GLESFuncs.glTexParameterIuiv) { - const auto& borderColorUI = MGB_TEXPARAM_BORDER_UI; - const GLuint borderColorArray[4] = {borderColorUI.x(), borderColorUI.y(), borderColorUI.z(), - borderColorUI.w()}; - g_GLESFuncs.glTexParameterIuiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); - } else { - const auto& borderColor = MGB_TEXPARAM_BORDER_F; - const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), - borderColor.w()}; - g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); - } - m_cacheBorderColor = MGB_TEXPARAM_BORDER_F; - m_cacheBorderColorI = MGB_TEXPARAM_BORDER_I; - m_cacheBorderColorUI = MGB_TEXPARAM_BORDER_UI; - m_cacheBorderColorForm = borderColorForm; - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } - - // GL_DEPTH_STENCIL_TEXTURE_MODE (GL_ARB_stencil_texturing / ES 3.1 core): which aspect - // of a packed depth/stencil image a sampler reads. Until this was forwarded the - // frontend kept the mode as a pure shadow - glGetTexParameter answered it, sampling - // ignored it - so a usampler2D bound to a D24S8 texture in STENCIL_INDEX mode read the - // depth aspect. Texture state rather than sampler state, so multisample targets take - // it too (ES 3.1 8.10 lists it among the three pnames they accept). It is only sent - // when it has moved, which for the overwhelming majority of textures is never. - const Bool supportsStencilTextureMode = - g_GLESCapabilities.GLESVersion.Major > 3 || - (g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1); - if (supportsStencilTextureMode) { - const GLenum depthStencilTextureMode = MGB_TEXPARAM_DS_MODE; - if (m_cacheDepthStencilTextureMode != depthStencilTextureMode) { - g_GLESFuncs.glTexParameteri(target, GL_DEPTH_STENCIL_TEXTURE_MODE, - static_cast(depthStencilTextureMode)); - m_cacheDepthStencilTextureMode = depthStencilTextureMode; - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { - MGLOG_D("%s(%s:%d) ES error %s", func, file, line, - MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } - } - } -#undef MGB_TEXPARAM_LEVEL_RANGE -#undef MGB_TEXPARAM_FORMAT -#undef MGB_TEXPARAM_SWIZZLE -#undef MGB_TEXPARAM_DS_MODE -#undef MGB_TEXPARAM_BORDER_F -#undef MGB_TEXPARAM_BORDER_I -#undef MGB_TEXPARAM_BORDER_UI -#undef MGB_TEXPARAM_BORDER_FORM - - void ActivateTextureUnit(Uint unit) { - if (unit == g_activeTextureUnit) { - return; - } - g_GLESFuncs.glActiveTexture(GL_TEXTURE0 + unit); - g_activeTextureUnit = unit; - } - - void UnbindTexture(Uint unit, GLenum target) { // Activates `unit` when an unbind is issued - auto targetN = static_cast(MG_Util::ConvertGLEnumToTextureTarget(target)); - if (g_boundTexturesCache[unit][targetN] == nullptr) return; - - ActivateTextureUnit(unit); - g_GLESFuncs.glBindTexture(target, 0); - g_boundTexturesCache[unit][targetN] = nullptr; - } - - Uint g_activeTextureUnit = 0; - Array, - MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> - g_boundTexturesCache; - TwinRegistry g_backendTextureObjects; - -#if MOBILEGL_BUILD_DISAGGREGATED - void WritePendingSharedImageUploads() { - auto& applier = MG_Pipe::MGPipeApplier(); - for (SizeT slot = 0; slot < applier.TextureResources.size(); ++slot) { - const auto& record = applier.TextureResources[slot]; - if (!record.Live || record.SharedImageId == 0 || record.PendingUploads.empty()) continue; - const MG_Pipe::MGPipeHandle handle{static_cast(slot), record.Gen}; - auto* twin = g_backendTextureObjects.FindByHandle(handle); - if (twin == nullptr || !*twin) continue; - (*twin)->SyncMipmapsToBackendByHandle(handle); - } - } -#endif - - // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract. - BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res) { - if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; - // THE RECORD FIRST: a texture handle with no resource record is a seam defect - the - // client named a texture it never created, or one whose resource_destroy has - // already applied while a binding still names it - and a twin adopted for it would - // own a driver texture nothing ever uploads to. - if (PipeTextureRecordForHandle(res) == nullptr) { - MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle " - "arm, so no driver texture can be built for it and the unit keeps what " - "it holds", - res.Slot, res.Gen); - return nullptr; - } - auto* slot = AdoptTwinByHandle(g_backendTextureObjects, res, "texture"); - if (slot == nullptr) return nullptr; - if (!*slot) *slot = MakeShared(); - return slot->get(); - } - - void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite) { - // The storage owner's handle, one hop per view like the frontend walk above (the - // client composes a view of a view onto its root, so one hop is the real shape). - MG_Pipe::MGPipeHandle storage = res; - for (Uint hop = 0; hop < 4 && !MG_Pipe::MGPipeHandleIsNull(storage); ++hop) { - const auto* record = PipeTextureRecordForHandle(storage); - if (record == nullptr || MG_Pipe::MGPipeHandleIsNull(record->Desc.ViewOf)) break; - storage = record->Desc.ViewOf; - } - if (auto* twin = ResolveTextureTwin(storage)) twin->NoteDriverSideWrite(); - // P8-E: the staged store stops standing for this texture's content (a no-op on a - // monolith arm, where the store does not copy). - if (!storeFollowsTheWrite && !MG_Pipe::MGPipeHandleIsNull(storage)) { - MG_Record::ServerStagedTexture().MarkDriverWritten( - MG_Record::StagedTextureStore::KeyForHandle(storage)); - } - } - Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin, - MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel, - const IntVec3& destinationOrigin, const IntVec3& size) { - auto& store = MG_Record::ServerStagedTexture(); - // P13 W4b: an aliasing store (monolith's record arm) follows too; see FollowCopy. - if (!store.Holds()) return false; - // The store's level key for an endpoint copy-image addresses as ONE image with its - // layers on z: exactly one upload target (not a cube map, whose faces are six store - // levels) and not a 1D array (the store keeps its layers as rows). A view's own levels - // are byteless, so FollowCopy answers false for it by itself. - const auto storeTarget = [](MG_Pipe::MGPipeHandle handle, Uint16& target) { - const auto* record = PipeTextureRecordForHandle(handle); - if (record == nullptr || - record->Desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex1DArray)) { - return false; - } - const auto& targets = BufferImpl::StagedUploadTargetsForPipeTarget(record->Desc.Target); - if (targets.size() != 1) return false; - target = static_cast(targets[0]); - return true; - }; - Uint16 sourceTarget = 0, destinationTarget = 0; - if (!storeTarget(source, sourceTarget) || !storeTarget(destination, destinationTarget)) return false; - // ONE FORMAT BOTH ENDS. Copy-image moves the GL texel bits; the store holds each - // format's canonical shadow, which for most formats IS those bits and for some is not - // (the 16-bit packed formats are kept as UNorm8, RGB10 / RGB12 as UNorm16). Between two - // levels of one format the canonical move and the GL move agree whatever the format; - // across two compatible formats they need not, so that copy is not followed. - if (PipeTextureRecordForHandle(source)->Desc.InternalFormat != - PipeTextureRecordForHandle(destination)->Desc.InternalFormat) { - return false; - } - return store.FollowCopy(MG_Record::StagedTextureStore::KeyForHandle(source), sourceTarget, - static_cast(sourceLevel), sourceOrigin, - MG_Record::StagedTextureStore::KeyForHandle(destination), - destinationTarget, static_cast(destinationLevel), destinationOrigin, - size); - } } // namespace TextureImpl - - namespace FramebufferImpl { - BackendFramebufferObject::BackendFramebufferObject() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // Identity until a non-identity draw-buffer array forces a relocation. A framebuffer - // that is never draw-bound never runs the recompute, so the table has to start out - // matching what the attachment loop will physically do. - for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) { - m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i; - } - g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId); - m_contextGeneration = g_backendContextGeneration; - if (m_backendFBOId == 0) { - MGLOG_E_ONCE("Failed to generate framebuffer object."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } else { - MGLOG_D("Generated framebuffer object with ID: %u.", m_backendFBOId); - } - } - - BackendFramebufferObject::~BackendFramebufferObject() { - if (InProcessTeardown()) { - return; // see InProcessTeardown(): the driver may be unloaded already - } - if (m_backendFBOId == 0) { - return; - } - // Scrub the binding shadow whether or not the id can still be deleted: a - // recycled name must never satisfy the shadow's dedup. - NoteFramebufferIdDeleted(m_backendFBOId); - if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) { - g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId); - } - m_backendFBOId = 0; - } - - void BackendFramebufferObject::Bind(FramebufferTarget target) const { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (target == FramebufferTarget::Read) - BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId); - else - BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId); - } - - namespace { - // Driver-level framebuffer-binding shadow (see Managers.h). Indexed by - // FramebufferTarget {Draw, Read}. - Array g_driverFBOBindings = {0, 0}; - Array g_driverFBOBindingKnown = {false, false}; - } // namespace - - void BindFramebufferId(GLenum fbTarget, Uint id) { - const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER; - const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER; - const SizeT drawIdx = SizeT(FramebufferTarget::Draw); - const SizeT readIdx = SizeT(FramebufferTarget::Read); - const Bool drawMatches = - !bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id); - const Bool readMatches = - !bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id); - if (drawMatches && readMatches) { - return; - } - g_GLESFuncs.glBindFramebuffer(fbTarget, id); - if (bindsDraw) { - g_driverFBOBindings[drawIdx] = id; - g_driverFBOBindingKnown[drawIdx] = true; - } - if (bindsRead) { - g_driverFBOBindings[readIdx] = id; - g_driverFBOBindingKnown[readIdx] = true; - } - } - - Uint CurrentFramebufferBinding(FramebufferTarget target) { - const SizeT idx = SizeT(target); - if (!g_driverFBOBindingKnown[idx]) { - // Cold path: pin the shadow from the driver once (init probes and - // pre-shadow code bind raw but restore what they found). - GLint binding = 0; - g_GLESFuncs.glGetIntegerv( - target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING, - &binding); - g_driverFBOBindings[idx] = static_cast(binding); - g_driverFBOBindingKnown[idx] = true; - } - return g_driverFBOBindings[idx]; - } - - void NoteFramebufferIdDeleted(Uint id) { - if (id == 0) { - return; - } - for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) { - if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) { - g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0 - } - } - } - - void InvalidateFramebufferBindingCache() { - g_driverFBOBindings = {0, 0}; - g_driverFBOBindingKnown = {false, false}; - // The per-target "already synced" memo describes work pushed into the ES context - // that is being left or replaced. Both callers (MakeCurrent, DestroyEGLContext) - // mean the context may have been reset under us, so claim nothing is synced: - // a null object never matches a real binding, so SyncCurrentFBO re-pushes. - g_fboSyncedSlotVersions = {0}; - g_fboSyncedObjectVersions = {0}; - g_fboSyncedObjects = {}; -#if MOBILEGL_ESPRYT_FBO_HANDLE_ARM_MEMOS_LINKED - // E's MAJOR-4 / A9: the handle arm's own memos say the same thing about the same - // targets, so they are invalidated HERE rather than at each of the nine call sites - - // six in DirectGLES.cpp and three in SanityTest.cpp, and it was the three that were - // missed. See the declaration in Managers.h for why the call is gated and which line - // each package flips. - InvalidateFramebufferHandleArmMemos(); -#endif - } - - void BackendFramebufferObject::InvalidateSyncedState() { - std::fill(std::begin(m_frontendDrawBuffers), std::end(m_frontendDrawBuffers), - FramebufferAttachmentType::Unknown); - std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE); - // NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every - // attachment it had, possibly under a non-identity permutation. Declaring the table - // identity here is safe only because every gate that decides which points get - // re-issued is invalidated too - the attachment versions below, and under - // MOBILEGL_PIPE_PUSH the record memos stamped at the end of this function - so the - // next sync re-attaches all non-empty attachments at their identity points AND - // (see SyncToBackend's attachment loop) detaches any colour point whose frontend owner - // is empty. Without that detach a stale image would survive under a point the table now - // claims for a different, empty attachment. - for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) { - m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i; - } - m_frontendReadBuffer = FramebufferAttachmentType::Unknown; - m_backendReadBuffer = GL_NONE; - std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(), - static_cast(~0u)); - // Every attachment version is invalidated above, so the next walk re-attaches - // everything regardless; stamp the generation so it does not re-arm twice. - m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration; - // The record memos are the gate the PIPE_PUSH arms' attachment walks actually read - - // the handle arm has nothing narrower under its key - so leaving them standing here - // would let a re-sync of the same record skip the re-attach this function exists to - // force. 0 is never a live hash, so a zeroed memo is a guaranteed miss. - m_syncedRecordHashes = {0}; - m_syncedAttachmentRecordHash = 0; - m_syncedNoAttachmentDefaultsValid = false; - } - - static Bool IsSnormFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R8Snorm: - case TextureInternalFormat::RG8Snorm: - case TextureInternalFormat::RGB8Snorm: - case TextureInternalFormat::RGBA8Snorm: - case TextureInternalFormat::R16Snorm: - case TextureInternalFormat::RG16Snorm: - case TextureInternalFormat::RGB16Snorm: - case TextureInternalFormat::RGBA16Snorm: - return true; - default: - return false; - } - } - - static Bool IsUnormFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R16: - case TextureInternalFormat::RG16: - case TextureInternalFormat::RGB16: - case TextureInternalFormat::RGBA16: - return true; - default: - return false; - } - } - - Uint32 g_alphaWidenedDrawBufferMask = 0; - Uint32 g_integerColorDrawBufferMask = 0; - - static Bool IsIntegerColorFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R8I: - case TextureInternalFormat::R8UI: - case TextureInternalFormat::R16I: - case TextureInternalFormat::R16UI: - case TextureInternalFormat::R32I: - case TextureInternalFormat::R32UI: - case TextureInternalFormat::RG8I: - case TextureInternalFormat::RG8UI: - case TextureInternalFormat::RG16I: - case TextureInternalFormat::RG16UI: - case TextureInternalFormat::RG32I: - case TextureInternalFormat::RG32UI: - case TextureInternalFormat::RGB8I: - case TextureInternalFormat::RGB8UI: - case TextureInternalFormat::RGB16I: - case TextureInternalFormat::RGB16UI: - case TextureInternalFormat::RGB32I: - case TextureInternalFormat::RGB32UI: - case TextureInternalFormat::RGBA8I: - case TextureInternalFormat::RGBA8UI: - case TextureInternalFormat::RGBA16I: - case TextureInternalFormat::RGBA16UI: - case TextureInternalFormat::RGBA32I: - case TextureInternalFormat::RGBA32UI: - case TextureInternalFormat::RGB10A2UI: - return true; - default: - return false; - } - } - - // THE FOUR CROSS-OBJECT MASKS, ANSWERED FROM THE RECORD (ID-12 DV-5, review M-3 / DV-5). - // - // All four reduce to (internal format, TEXTURE TARGET) for a texture attachment and to - // (internal format) for a renderbuffer one, and MGPSurface now carries both inline - - // InternalFormat since D-C1 and TextureTarget since c0c widened Pad0 into it. So the - // handle arm stops reading the frontend attachment objects to compute them, which is - // what D-C1's "the four cross-object masks fall out at push time with no lookup" meant. - // - // ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha / their renderbuffer - // siblings are UNTOUCHED - two of them are on D-N's byte-identical list. Only what they - // are ASKED changes, which is the whole point of carrying the target rather than - // inventing a TextureUploadTarget -> TextureTarget inverse: feeding a D-N-pinned - // function a guessed input would silently change what every texture is allocated as. - // - // Kind IS the gate (c0c: "consulted only when Kind == kMGPipeSurfaceKindTexture"), and - // kMGPipeSurfaceNoTextureTarget on a texture point is a seam defect - refused loudly and - // answered false, never guessed. False is the safe direction for all four: the mask - // turns an emulation ON, and an emulation that does not run leaves the driver's own - // (correct-for-the-real-format) behaviour, while one that runs on the wrong buffer - // clamps or overwrites texels the application wrote. - static Bool PushedSurfaceTextureTarget(const MG_Pipe::MGPSurface& surface, TextureTarget* out) { - if (surface.TextureTarget == MG_Pipe::kMGPipeSurfaceNoTextureTarget) { - MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} says Kind=texture but carries no " - "texture target - refusing to guess one for the cross-object masks", - surface.Res.Slot, surface.Res.Gen); - return false; - } - *out = static_cast(surface.TextureTarget); - return true; - } - - static Bool IsSnormFallbackSurface(const MG_Pipe::MGPSurface& surface) { - const auto format = static_cast(surface.InternalFormat); - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { - TextureTarget target = TextureTarget::Unknown; - return PushedSurfaceTextureTarget(surface, &target) && IsSnormFormat(format) && - TextureImpl::ShouldUseCaveatTextureFormat(format, target); - } - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { - return IsSnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format); - } - return false; - } - - static Bool IsUnormFallbackSurface(const MG_Pipe::MGPSurface& surface) { - const auto format = static_cast(surface.InternalFormat); - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { - TextureTarget target = TextureTarget::Unknown; - return PushedSurfaceTextureTarget(surface, &target) && IsUnormFormat(format) && - TextureImpl::ShouldUseCaveatTextureFormat(format, target); - } - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { - return IsUnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format); - } - return false; - } - - static Bool IsAlphaWidenedColorSurface(const MG_Pipe::MGPSurface& surface) { - const auto format = static_cast(surface.InternalFormat); - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { - TextureTarget target = TextureTarget::Unknown; - return PushedSurfaceTextureTarget(surface, &target) && - TextureImpl::BackendTextureFormatAddsAlpha(format, target); - } - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { - return TextureImpl::BackendRenderbufferFormatAddsAlpha(format); - } - return false; - } - - static Bool IsIntegerColorSurface(const MG_Pipe::MGPSurface& surface) { - // No target and no caveat table: integerness is a property of the format alone, so - // this one is the same question on both arms. - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) { - return false; - } - return IsIntegerColorFormat(static_cast(surface.InternalFormat)); - } - - // The record's surface for an attachment POINT, or null when this record does not - // describe that point at all. MGPFramebufferState carries Color[8] + Depth + Stencil - // (D-C1), which is every point a framebuffer can hold on the handle arm: D-C3 refuses - // bit 9 outright on a driver reporting more than 8 colour attachments, and the - // FRONT/BACK points belong to the DEFAULT framebuffer, which has no twin at all. - const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record, - FramebufferAttachmentType point) { - if (point == FramebufferAttachmentType::Depth) return &record.Depth; - if (point == FramebufferAttachmentType::Stencil) return &record.Stencil; - if (point < FramebufferAttachmentType::Color0 || point > FramebufferAttachmentType::Color31) { - return nullptr; - } - const Int index = static_cast(point) - static_cast(FramebufferAttachmentType::Color0); - if (index >= static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) return nullptr; - return &record.Color[static_cast(index)]; - } - - // SyncAttachmentObject's handle arm (review M-3). WHAT MOVES IS THE RESOLUTION: the twin - // is adopted from MGPSurface::Res through the slot table instead of being looked up by - // the frontend object's ADDRESS, and the attach SHAPE - layered, level, layer, the - // upload target and the texture target - is read off the surface instead of off the - // frontend attachment object. - // - // P5e (fb, CONTRACT-P5E.md §5.4): THE FRONTEND ATTACHMENT PARAMETER IS GONE. It was - // here for two things and P5e retires both. The storage syncs now have by-handle forms - // (SyncMipmapsToBackendByHandle / SyncToBackendByHandle), so the object is no longer - // needed to reach the texels; and the record is now the ONLY statement of what is - // attached, so the cross-checks had nothing left to corroborate against - a second - // answer that only a monolith build could produce is not a check, it is a second - // writer. With the object gone, review N-6's hole ("record empty, frontend still - // holding") is UNREPRESENTABLE rather than refused: the caller's detach and this - // function's attach now read the SAME test, `Kind == None || Res null`. - // - // The two P5c cross-checks that ran HandleOf on the apply thread go with it. The - // renderbuffer one (the one that was NOT transport-gated, unlike its texture sibling) - // is deleted by the rekey rather than gated, which is what CONTRACT-P5E §4.4 asks for: - // gating it would have left a client-allocator probe compiled into a path a run-ahead - // apply reaches, and there is no longer anything for it to say. - // - // P5e (fix1, ID-81 / §5.8): THE STORAGE SYNC IS ARM-SELECTED AND THE MONOLITH ARM KEEPS - // ITS FRONTEND OBJECT. This function has TWO callers and they are on opposite arms: the - // handle form (SyncToBackendByHandle) has no frontend object and must not acquire one, - // while the OBJECT form (SyncToBackend, the push-monolith arm - P4a drives its - // attachments from the record too, gated on FramebufferSubsystemEnabled() alone, D-C2) - // is holding the frontend attachment the whole time. fb dropped the parameter for both, - // which routed the monolith arm into tx2's by-handle storage seam; that seam passes a - // NULL SharedPtr and relies on the record arm being selected, and under - // Transport == Monolith it is not - `stateTextureObject->IsTextureView()` then - // dereferenced null (the device's Lightmap. -> clearColorTexture -> glClear - // SIGSEGV). So the parameter comes back as MONOLITH GLUE, used for nothing but the - // storage sync: the attach SHAPE, the empty-point test and the twin adoption stay the - // record's on both arms, which is what keeps N-6's hole closed and the cross-checks - // deleted. It is a pointer and it is null on the handle arm, so "which arm am I on" is - // never inferred from it - MG_Config::Transport decides, as §5.8 requires, and a - // monolith arm that somehow arrives without one refuses by name instead of attaching a - // texture whose levels were never pushed. - static Bool SyncAttachmentSurface(GLenum glFBOTarget, const MG_Pipe::MGPSurface& surface, - GLenum glBackendAttachment, - const MG_State::GLState::FramebufferAttachmentObject* monolithAttachment) { - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || - MG_Pipe::MGPipeHandleIsNull(surface.Res)) { - // An empty point: the caller detached it on exactly this test, so attaching - // nothing here is the whole of the agreement. (The legacy arm does the same.) - return true; - } - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { - // The record's handle is validated BEFORE the frontend is consulted (N-3): it is - // the untrusted half, the frontend cross-check below is the corroboration, and - // asking in this order is what gives the two adoption refusals a reachable - // caller at all. - if (!PipeTwinHandleIsAdoptable(TextureImpl::g_backendTextureObjects, surface.Res, "texture")) { - return false; - } - auto* twinSlot = AdoptTwinByHandle(TextureImpl::g_backendTextureObjects, surface.Res, "texture"); - if (twinSlot == nullptr) { - return false; // AdoptTwinByHandle named the refusal - } - if (!*twinSlot) { - *twinSlot = MakeShared(); - } - // COPIED OUT of the table: SyncMipmapsToBackend can grow it (a re-mint adopts - // another handle), and Managers.h:381-385's pointer-invalidation warning applies - // to exactly this sequence. - SharedPtr backendTextureObject = *twinSlot; - if (!backendTextureObject) { - MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__); - return false; - } - // P5e (fb): the storage sync BY HANDLE (tx2's seam). The level shadow it reads - // is the applier's staged texture store, not a frontend object, which is what - // let the object parameter go - UNDER A TRANSPORT. P5e (fix1, ID-81): the - // push-monolith arm keeps the frontend sync it had at f6cfcbd3, token for token, - // because tx2's record arm inside SyncMipmapsToBackend is itself selected by - // `Transport != Monolith` and cannot answer here. - backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res); - // From here on every draw, clear and blit into this framebuffer can write the - // texture behind the staged shadow's back (the frontend arm's note, by handle). - TextureImpl::NoteDriverSideTextureWriteByHandle(surface.Res); - const auto uploadTarget = static_cast(surface.UploadTarget); - if (surface.Layered != 0) { - g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment, - backendTextureObject->GetBackendTextureId(), - static_cast(surface.Level)); - } else if (uploadTarget == TextureUploadTarget::Texture3D || - uploadTarget == TextureUploadTarget::Texture2DArray || - uploadTarget == TextureUploadTarget::Texture1DArray || - uploadTarget == TextureUploadTarget::CubeMapArray || - uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) { - g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment, - backendTextureObject->GetBackendTextureId(), - static_cast(surface.Level), - static_cast(surface.Layer)); - } else { - auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - if (glTextureTarget == GL_UNKNOWN_MGL) { - TextureTarget target = TextureTarget::Unknown; - if (!PushedSurfaceTextureTarget(surface, &target)) { - return false; - } - glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); - } - // Same cube-face rule as the legacy arm: glBindTexture rejects the face - // enums, so bind through the owning cube target and attach with the face. - const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && - glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; - backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget); - g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget, - backendTextureObject->GetBackendTextureId(), - static_cast(surface.Level)); - } - return true; - } - if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { - // Same order as the texture arm above, for the same reason (N-3). - if (!PipeTwinHandleIsAdoptable(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res, - "renderbuffer")) { - return false; - } - auto* twinSlot = AdoptTwinByHandle(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res, - "renderbuffer"); - if (twinSlot == nullptr) { - return false; - } - if (!*twinSlot) { - *twinSlot = MakeShared(); - } - SharedPtr backendRenderbufferObject = *twinSlot; - if (!backendRenderbufferObject) { - MGLOG_E_ONCE("%s: No backend renderbuffer found for FBO attachment.", __func__); - return false; - } - // P5e (fb): the four allocation values were already record-supplied (D-D2); what - // moves here is the LOOKUP - the handle the surface carries, never HandleOf on - // an object the apply thread was handed. - // - // P5e (fix1, ID-81): arm-selected for the texture arm's reason, and here it is a - // BEHAVIOUR difference even though the by-handle body dereferences nothing: the - // object form reports a refused allocation to the application through the live - // GLContext (RecordError) and the by-handle form deliberately does not, because - // under a transport there is no application on this side to report it to. On the - // push-monolith arm there is, so the monolith arm keeps the object form. - backendRenderbufferObject->SyncToBackendByHandle(surface.Res); - backendRenderbufferObject->Bind(); - g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, - backendRenderbufferObject->GetBackendRenderbufferId()); - return true; - } - MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} carries Kind=%u, which is neither a texture " - "nor a renderbuffer nor an empty point - refusing to attach", - surface.Res.Slot, surface.Res.Gen, surface.Kind); - return false; - } - - // The read attachment's storage carries an alpha channel its frontend format does not - // (the three-channel colour-renderable widening). GL answers such a read with 1.0, but - // the storage holds whatever the draw wrote there, so the readback has to overwrite it. - Bool IsAlphaWidenedFallbackReadAttachment() { - const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer(); - return record && IsAlphaWidenedColorSurface(record->ReadSurface); - } - - Bool IsFixedPointFallbackReadAttachment() { - const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer(); - return record && record->ReadSurface.Kind != MG_Pipe::kMGPipeSurfaceKindNone && - IsSnormFormat(static_cast(record->ReadSurface.InternalFormat)); - const auto& readFBO = - MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); - if (!readFBO) { - return false; - } - const auto readBuffer = readFBO->GetReadBuffer(); - if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) { - return false; - } - // Any signed-normalized attachment, not just the ones currently substituted: - // ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer - // hands back the negative half that desktop GL clamps away. - const auto& attachmentObject = readFBO->GetAttachment(readBuffer); - if (attachmentObject.IsTexture()) { - const auto& textureObject = attachmentObject.GetTexture(); - return textureObject && IsSnormFormat(textureObject->GetFormat()); - } - if (attachmentObject.IsRenderbuffer()) { - const auto& renderbufferObject = attachmentObject.GetRenderbuffer(); - return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat()); - } - return false; - } - - const MG_Pipe::MGPFramebufferState* PushedFramebufferRecord(MG_Pipe::MGPipeHandle fbo) { - // FramebufferRecordFor answers null for the null handle and for a slot nothing has - // described (both silent - that is every framebuffer before its first emission), and - // refuses a stale generation loudly while counting it in - // StaleFramebufferRecordLookups. So there is no second emptiness test here to get out - // of step with the applier's, and none of the three cases is a binding question. - return MG_Pipe::MGPipeApplier().FramebufferRecordFor(fbo); - } - - // The record's DrawBuffers[] is an ATTACHMENT INDEX with -1 for None (D-C1), which is - // what the frontend array holds for every framebuffer that has a twin. It deliberately - // cannot spell FrontLeft / FrontRight / BackLeft / BackRight: those are the DEFAULT - // framebuffer's own tokens, the default framebuffer has no BackendFramebufferObject at - // all (it is pDefaultFramebufferInfo->defaultFBO, which is why MGPipeHandles.h reserves - // {0,1} for it), and the legacy arm's branch for them says so in as many words - "shouldn't - // remap". So the conversion below is total for every object that reaches this twin, and - // the record's IsDefault is the thing a later phase would test if that ever changed. - static void DecodePushedDrawBuffers(const MG_Pipe::MGPFramebufferState& record, - FramebufferAttachmentType* out) { - for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { - const Int8 index = record.DrawBuffers[i]; - out[i] = index < 0 ? FramebufferAttachmentType::None - : static_cast( - static_cast(FramebufferAttachmentType::Color0) + index); - } - } - - // P5e (fb, §5.4): the record half of SyncReadBufferToBackend, lifted out of it so the - // by-handle sync can run it WITHOUT a frontend object. Nothing inside reads one: the - // frontend index was only ever a diagnostic, and it is passed as a number now - // (GlNameForDiag is 0 on the handle arm, which reads as "the record did not say"). - void BackendFramebufferObject::ApplyReadBufferFromRecord(const MG_Pipe::MGPFramebufferState& record, - Uint glNameForDiag) { - // D-C1 makes the NULL Res the spelling of "no attachment" and c0c's - // kMGPipeSurfaceKindNone makes Kind agree with it on a zero-initialised record. - // Res stays the test - it is the one D-C1 fixes - and Kind is CROSS-CHECKED - // rather than consulted: the two are one statement the emitter has to make - // twice, and a record where they disagree is a seam defect, not an empty point. - // Picking a winner silently is what this refuses to do. - const Bool readSurfaceEmpty = MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res); - if (readSurfaceEmpty != (record.ReadSurface.Kind == MG_Pipe::kMGPipeSurfaceKindNone)) { - MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface says Res={%u, %u} and " - "Kind=%u, which disagree about whether it names anything - refusing " - "rather than choosing one of them", - glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen, - record.ReadSurface.Kind); - return; - } - FramebufferAttachmentType pushedReadBuf = FramebufferAttachmentType::None; - if (!readSurfaceEmpty) { - // EVERY FIELD THAT IDENTIFIES THE IMAGE, not the three v1 compared (m-4). - // The same image legally sits at two colour points - a layered attachment of - // one array and a single slice of it are different surfaces of one Res - so a - // partial comparison resolves to the lower index and the refusal branch below - // ("refusing to guess") would then be doing part of the guessing itself. - // InternalFormat and TextureTarget are properties of the RESOURCE rather than - // of the point, so they add nothing to the identity and are left out. - Bool matched = false; - for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { - const auto& color = record.Color[i]; - if (color.Res == record.ReadSurface.Res && color.Kind == record.ReadSurface.Kind && - color.Layered == record.ReadSurface.Layered && - color.Level == record.ReadSurface.Level && color.Layer == record.ReadSurface.Layer && - color.UploadTarget == record.ReadSurface.UploadTarget) { - pushedReadBuf = static_cast( - static_cast(FramebufferAttachmentType::Color0) + static_cast(i)); - matched = true; - break; - } - } - if (!matched) { - MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface {%u, %u} matches no " - "colour attachment of its own record - refusing to guess a read " - "buffer", - glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen); - return; - } - } - if (pushedReadBuf == m_frontendReadBuffer) { - return; - } - m_frontendReadBuffer = pushedReadBuf; - - const GLenum glPushedReadBuffer = GetBackendAttachmentType(pushedReadBuf); - if (m_backendReadBuffer != glPushedReadBuffer) { - m_backendReadBuffer = glPushedReadBuffer; - // Still bound as READ first, for the reason the legacy arm gives below: - // glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER. - Bind(FramebufferTarget::Read); - g_GLESFuncs.glReadBuffer(glPushedReadBuffer); - } - } - - // P5e (fb): the by-handle entry, for SyncCurrentFBOByRecord's "one object on both - // bindings" skip - the one path that applies a read buffer without doing the rest of - // the sync. A framebuffer with no record is the caller's seam defect, not a silent - // no-op, so it says so. - void BackendFramebufferObject::SyncReadBufferToBackendByHandle(MG_Pipe::MGPipeHandle fbo) { - const auto* record = PushedFramebufferRecord(fbo); - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so its read " - "buffer cannot be pushed", - fbo.Slot, fbo.Gen); - return; - } - ApplyReadBufferFromRecord(*record, 0); - } - - Bool BackendFramebufferObject::RecomputeBackendColorSlots( - const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) { - // Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own - // limit (ValidateColorAttachmentInRange, which reads the clamped - // GetDynamicParameters().MaxColorAttachments) is never larger than this raw ES cap, so an - // index the frontend accepted is always < slotCount. Indices at or above it can never own - // an image and stay on their identity point - never touched, never a GL error. - const Uint slotCount = - std::min(MAX_COLOR_ATTACHMENT_SLOTS, - static_cast(std::max(g_GLESCapabilities.MaxColorAttachments, 1))); - - GLenum newSlots[MAX_COLOR_ATTACHMENT_SLOTS]; - for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) { - newSlots[i] = GL_COLOR_ATTACHMENT0 + i; - } - Bool assigned[MAX_COLOR_ATTACHMENT_SLOTS] = {false}; - Bool slotTaken[MAX_COLOR_ATTACHMENT_SLOTS] = {false}; - - // 1. ES pins draw-buffer slot s to GL_COLOR_ATTACHMENTs, so an attachment named by draw - // buffer slot s has no choice: its image must sit at backend point s. This has to - // agree with the compaction the caller just pushed through glDrawBuffers. - for (Uint s = 0; s < FramebufferObject::MAX_DRAW_BUFFERS && s < slotCount; ++s) { - const auto frontendBuf = stateDrawBuffers[s]; - if (frontendBuf < FramebufferAttachmentType::Color0 || - frontendBuf > FramebufferAttachmentType::Color31) { - continue; // GL_NONE, or a default-framebuffer FRONT/BACK token: never relocated. - } - const Uint a = - static_cast(frontendBuf) - static_cast(FramebufferAttachmentType::Color0); - // Neither guard may ever fire: a duplicate draw buffer is already INVALID_OPERATION - // and an out-of-range one is rejected by ValidateColorAttachmentInRange. If one did - // fire the table would disagree with the glDrawBuffers the caller already issued, - // which is the exact non-injectivity this table exists to remove. - MOBILEGL_ASSERT(a < slotCount && !assigned[a], - "Draw buffer %u names colour attachment %u which is out of range or duplicated.", s, - a); - if (a >= slotCount || assigned[a]) { - continue; - } - newSlots[a] = GL_COLOR_ATTACHMENT0 + s; - assigned[a] = true; - slotTaken[s] = true; - } - - // 2. Everything else keeps its identity point when that point survived step 1. This is - // what makes the ordinary drawBuffers[s] == COLOR_ATTACHMENTs case a strict no-op: - // the table stays identity, nothing moves, no attachment is re-issued. - for (Uint a = 0; a < slotCount; ++a) { - if (assigned[a] || slotTaken[a]) { - continue; - } - newSlots[a] = GL_COLOR_ATTACHMENT0 + a; - assigned[a] = true; - slotTaken[a] = true; - } - - // 3. What is left are attachments whose identity point step 1 took away. Park them on the - // lowest free point. They are not draw buffers, so nothing is rendered through them; - // they only have to stay addressable for glReadBuffer and blits, and the map has to - // stay injective so reading one of them cannot land on another's image. - for (Uint a = 0; a < slotCount; ++a) { - if (assigned[a]) { - continue; - } - for (Uint s = 0; s < slotCount; ++s) { - if (!slotTaken[s]) { - newSlots[a] = GL_COLOR_ATTACHMENT0 + s; - assigned[a] = true; - slotTaken[s] = true; - break; - } - } - } - - Bool moved = false; - for (Uint a = 0; a < MAX_COLOR_ATTACHMENT_SLOTS; ++a) { - if (m_backendColorSlots[a] == newSlots[a]) { - continue; - } - m_backendColorSlots[a] = newSlots[a]; - moved = true; - // This attachment's image now belongs at a different backend point. Its frontend - // version has not changed, so the attachment loop would skip it; force it. - m_syncedFrontendAttachmentVersions[static_cast(FramebufferAttachmentType::Color0) + a] = - static_cast(~0u); - } - return moved; - } - - // ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------ - // - // WHY IT EXISTS. ScopedDetachedTextureFramebufferAttachments used to answer "which - // framebuffers currently have this texture attached" by walking every live twin and - // reading each one's FRONTEND attachment array. Under P5e that walk is the last place - // the server holds a frontend framebuffer across records - id re-typed ForEachLive and - // left the StateForHandle read greppable, naming this package as the one that replaces - // it. The records already carry the answer: every applied set_framebuffer_state names - // its eleven surfaces, so the relation is maintained where the record is consumed. - // - // KEYED ON THE TEXTURE SLOT rather than on {slot, gen}. The question a detach asks is - // "does the driver have this texture id hanging off some framebuffer", the points are - // re-derived from the record on every hit, and a stale entry left by a recycled slot - // therefore costs one record walk that matches nothing. The FRAMEBUFFER handles are - // stored whole, so a recycled framebuffer slot never answers for its predecessor - // (FindByHandle refuses a moved Gen). - // - // MAINTAINED AT THE SYNC, NOT AT THE APPLY, and that is the same set rather than a - // smaller one: a framebuffer whose record was never synced has no driver attachment to - // detach, and every path that can have one runs this sync first. - namespace { - UnorderedMap> g_framebuffersByAttachedTextureSlot; - UnorderedMap> g_attachedTextureSlotsByFramebufferSlot; - - void ForgetFramebufferTextureAttachmentsLocked(Uint32 fboSlot) { - auto it = g_attachedTextureSlotsByFramebufferSlot.find(fboSlot); - if (it == g_attachedTextureSlotsByFramebufferSlot.end()) return; - for (const Uint32 textureSlot : it->second) { - auto listIt = g_framebuffersByAttachedTextureSlot.find(textureSlot); - if (listIt == g_framebuffersByAttachedTextureSlot.end()) continue; - auto& list = listIt->second; - list.erase(std::remove_if(list.begin(), list.end(), - [&](MG_Pipe::MGPipeHandle h) { return h.Slot == fboSlot; }), - list.end()); - if (list.empty()) g_framebuffersByAttachedTextureSlot.erase(listIt); - } - g_attachedTextureSlotsByFramebufferSlot.erase(it); - } - } // namespace - - void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo, - const MG_Pipe::MGPFramebufferState& record) { - if (MG_Pipe::MGPipeHandleIsNull(fbo)) return; - ForgetFramebufferTextureAttachmentsLocked(fbo.Slot); - Vector textureSlots; - const auto note = [&](const MG_Pipe::MGPSurface& surface) { - if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; - if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return; - if (std::find(textureSlots.begin(), textureSlots.end(), surface.Res.Slot) != textureSlots.end()) { - return; // one texture at two points is one entry - } - textureSlots.push_back(surface.Res.Slot); - g_framebuffersByAttachedTextureSlot[surface.Res.Slot].push_back(fbo); - }; - for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) note(record.Color[i]); - note(record.Depth); - note(record.Stencil); - if (textureSlots.empty()) return; // the common Minecraft frame: renderbuffers only - g_attachedTextureSlotsByFramebufferSlot[fbo.Slot] = std::move(textureSlots); - } - - // ---- P5e (fb, §5.4): BackendFramebufferObject::SyncToBackend BY HANDLE --------------- - // - // The record's ELEVEN SURFACES ARE THE POINT SET. The object form walks the frontend's - // 41 attachment points to learn which exist; this one does not have to, because the - // emitter REFUSES a framebuffer holding a point at or above the wire width - // (FramebufferEmit.h's two refusals) and the FRONT/BACK tokens belong to the default - // framebuffer, which has no twin. So Color[0..7] + Depth + Stencil is the whole of it, - // and a framebuffer that could not be described that way never produced a record at all. - // - // THE PER-POINT VERSION MEMO IS GONE and is not replaced. On the object form it is a - // second, narrower gate under the record hash, hedging against "a frontend attachment - // moved without the record moving" - which is unrepresentable once the record is the - // only statement of what is attached (the hash covers every surface, and Fbo is inside - // it, so a recycled handle cannot be suppressed against its predecessor). Keeping it - // would have meant keeping the frontend array that carries it, which is the whole point - // of this arm. The two SERVER-owned gates stay, because no client-side value can answer - // what they answer: g_attachmentBackendIdGeneration ("did I re-mint a driver id") and - // the re-entry it drives. - void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo, - FramebufferTarget asTarget) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - const MG_Pipe::MGPFramebufferState* const recordPtr = PushedFramebufferRecord(fbo); - if (recordPtr == nullptr) { - MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so it is not " - "configured here; nothing is bound by this refusal, but the caller's own " - "bind still targets it", - fbo.Slot, fbo.Gen); - return; - } - const MG_Pipe::MGPFramebufferState& record = *recordPtr; - if (record.IsDefault != 0) { - // The default framebuffer has no twin (pDefaultFramebufferInfo->defaultFBO is the - // frontend object and MGPipeHandles.h reserves {0,1} for it), so reaching this - // body with one is a caller that skipped its own IsDefault branch. - MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} describes the DEFAULT framebuffer, " - "which has no twin to configure", - fbo.Slot, fbo.Gen); - return; - } - // Kept in step for the monolith-glue overloads that still resolve their record - // through it (SyncReadBufferToBackend's push arm). - m_pushedSyncHandle = fbo; - const GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget); - Bind(asTarget); - - FramebufferObject::FramebufferAttachmentArray stateDrawBuffers{}; - DecodePushedDrawBuffers(record, stateDrawBuffers.data()); - - // 1. Remap draw buffers. Same rule as the object form: glDrawBuffers writes the - // DRAW-bound framebuffer's state, so a READ-only sync must neither issue it nor - // stamp the memo (the Minecraft 26.x OIT bug the object form names). - Bool attachmentPointsMoved = false; - const Bool drawBufferClean = - memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(), - FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0; - if (!drawBufferClean && asTarget == FramebufferTarget::Draw) { - memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(), - FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)); - std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE); - int nEffectiveBuffers = 0; - for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) { - const auto frontendBuf = stateDrawBuffers[i]; - if (frontendBuf == FramebufferAttachmentType::None) { - m_backendDrawBuffers[i] = GL_NONE; - continue; - } - // DecodePushedDrawBuffers cannot produce a FRONT/BACK token - those are the - // default framebuffer's own and it has no twin - so the object form's - // "shouldn't remap" branch has no counterpart here. - m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i; - nEffectiveBuffers = i + 1; - } - g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers); - if (RecomputeBackendColorSlots(stateDrawBuffers)) { - m_frontendReadBuffer = FramebufferAttachmentType::Unknown; - attachmentPointsMoved = true; - } - } - - // The four cross-object masks, off the record's own surfaces (ID-12 DV-5). - if (asTarget == FramebufferTarget::Draw) { - Uint32 snormClampOutputMask = 0; - Uint32 unormClampOutputMask = 0; - Uint32 alphaWidenedMask = 0; - Uint32 integerColorMask = 0; - for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) { - const auto frontendBuf = stateDrawBuffers[i]; - if (frontendBuf < FramebufferAttachmentType::Color0 || - frontendBuf > FramebufferAttachmentType::Color31) { - continue; - } - const MG_Pipe::MGPSurface* surface = PushedSurfaceForAttachment(record, frontendBuf); - if (surface == nullptr) continue; // unrepresentable: DecodePushedDrawBuffers' range - if (IsSnormFallbackSurface(*surface)) { - snormClampOutputMask |= (1u << i); - } else if (IsUnormFallbackSurface(*surface)) { - unormClampOutputMask |= (1u << i); - } - if (IsAlphaWidenedColorSurface(*surface)) alphaWidenedMask |= (1u << i); - if (IsIntegerColorSurface(*surface)) integerColorMask |= (1u << i); - } - PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask; - PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask; - g_alphaWidenedDrawBufferMask = alphaWidenedMask; - g_integerColorDrawBufferMask = integerColorMask; - } - - // 2. Remap read buffer, READ-target only, for the reason the object form gives. - if (asTarget == FramebufferTarget::Read) { - ApplyReadBufferFromRecord(record, 0); - } - - // 3. The attachments. Three gates, and the key is the object's rather than the - // binding's: idGenerationMoved ("I re-minted a driver id, so a point may hold a - // dead one"), attachmentPointsMoved ("the colour permutation moved, so a point - // whose record did not change still has to be re-issued") and the record hash. - // NOT m_syncedRecordHashes: the per-target array is the object arm's re-arm - // trigger, and the points written below belong to the driver FBO rather than to - // the binding they were walked for - see m_syncedAttachmentRecordHash. - const Bool idGenerationMoved = m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration; - if (idGenerationMoved) { - m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration; - } - if (idGenerationMoved || attachmentPointsMoved || - m_syncedAttachmentRecordHash != record.ContentHash) { - const auto applyPoint = [&](FramebufferAttachmentType point, - const MG_Pipe::MGPSurface& surface) { - const Bool isColorPoint = point >= FramebufferAttachmentType::Color0 && - point <= FramebufferAttachmentType::Color31; - const GLenum glBackendAttachment = - isColorPoint ? GetBackendAttachmentType(point) - : MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(point); - if (glBackendAttachment == GL_NONE || glBackendAttachment == GL_UNKNOWN_MGL) return; - const Bool empty = surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || - MG_Pipe::MGPipeHandleIsNull(surface.Res); - // The detach that keeps m_backendColorSlots a permutation of the PHYSICAL - // layout: a colour point whose record says empty must not keep the previous - // owner's image, or glReadBuffer returns it. Bounded by the driver's cap, - // because GL_COLOR_ATTACHMENTn above it is INVALID_ENUM. - if (isColorPoint && empty) { - const Int index = - static_cast(point) - static_cast(FramebufferAttachmentType::Color0); - if (index < g_GLESCapabilities.MaxColorAttachments) { - g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, - GL_RENDERBUFFER, 0); - } - return; - } - // No frontend attachment on this arm, by construction: the record IS the - // point set here (§5.4). Null is what selects nothing - the transport test - // inside is what selects the by-handle storage syncs. - SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment, - /*monolithAttachment=*/nullptr); - }; - for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { - applyPoint(static_cast( - static_cast(FramebufferAttachmentType::Color0) + static_cast(i)), - record.Color[i]); - } - applyPoint(FramebufferAttachmentType::Depth, record.Depth); - applyPoint(FramebufferAttachmentType::Stencil, record.Stencil); - m_syncedAttachmentRecordHash = record.ContentHash; - NoteFramebufferTextureAttachments(fbo, record); - } - - // 4. ARB_framebuffer_no_attachments. With no attachment at all the record's geometry - // IS the framebuffer's default width/height/layers/samples (FillGeometry), and the - // driver FBO needs them too: without a default size an attachment-less ES - // framebuffer is incomplete and every draw into it is dropped. With attachments the - // defaults are ignored by GL, so they are only pushed for the empty shape. - Bool hasAnyAttachment = !(record.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone || - MG_Pipe::MGPipeHandleIsNull(record.Depth.Res)) || - !(record.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone || - MG_Pipe::MGPipeHandleIsNull(record.Stencil.Res)); - for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && !hasAnyAttachment; ++i) { - hasAnyAttachment = !(record.Color[i].Kind == MG_Pipe::kMGPipeSurfaceKindNone || - MG_Pipe::MGPipeHandleIsNull(record.Color[i].Res)); - } - if (!hasAnyAttachment) { - const Array defaults = { - static_cast(record.Width), static_cast(record.Height), - static_cast(record.Layers), - std::min(static_cast(record.Samples), g_GLESCapabilities.MaxFramebufferSamples), - static_cast(record.FixedSampleLocations != 0 ? GL_TRUE : GL_FALSE)}; - if (!m_syncedNoAttachmentDefaultsValid || m_syncedNoAttachmentDefaults != defaults) { - g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_WIDTH, defaults[0]); - g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_HEIGHT, defaults[1]); - if (g_GLESCapabilities.SupportsFramebufferDefaultLayers) { - g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_LAYERS, defaults[2]); - } - g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_SAMPLES, defaults[3]); - g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS, - defaults[4]); - m_syncedNoAttachmentDefaults = defaults; - m_syncedNoAttachmentDefaultsValid = true; - } - } - - // The walk itself can re-mint a driver id (the storage sync behind a texture - // surface), invalidating points this walk already attached. Re-enter until the - // generation is quiescent, exactly as the object form does - every pass syncs each - // dirty texture clean, so each repeat finds strictly fewer re-mints. - if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) { - SyncToBackendByHandle(fbo, asTarget); - } - } - - GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const { - // Only colour attachments are ever relocated; depth/stencil, the default framebuffer's - // FRONT/BACK names and None map straight through. - if (frontendAtt < FramebufferAttachmentType::Color0 || frontendAtt > FramebufferAttachmentType::Color31) { - return MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt); - } - // The table is a permutation of the backend colour points, so this is the one point that - // owns this attachment. Searching the draw-buffer array instead returned the identity - // point for every attachment that was not a draw buffer - which is exactly the point a - // relocated draw buffer had just taken over, so COLOR_ATTACHMENT0 read back the image of - // whatever attachment was last made the draw buffer. - const Uint index = static_cast(frontendAtt) - static_cast(FramebufferAttachmentType::Color0); - return m_backendColorSlots[index]; - } - - TwinRegistry - g_backendFramebufferObjects; - Array g_fboSyncedSlotVersions = {0}; - // Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change) - // per target: re-attaching textures or changing draw buffers on an already-bound FBO - // must re-sync it even when the binding-slot version has not moved. - Array g_fboSyncedObjectVersions = {0}; - Array - g_fboSyncedObjects = {}; - Uint64 g_attachmentBackendIdGeneration = 0; - Array g_fboSyncedBackendIdGenerations = {0}; - } // namespace FramebufferImpl - - namespace ScratchFBOImpl { - namespace { - // ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it - // (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it. - struct ScratchSet { - ScratchFramebuffer temp; - ScratchFramebuffer blitRead; - ScratchFramebuffer blitDraw; - Uint completeTinyFBOId = 0; - Uint completeTinyRBOId = 0; - }; - PerNativeContext g_scratchSets; - - // Detach every point the shadow no longer vouches for. Used when the - // shadow is unknown (context reset, texture id deleted while attached). - void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - fb.colorTex = 0; - fb.colorTarget = 0; - fb.colorLevel = 0; - fb.colorLayer = -1; - fb.depthTex = 0; - fb.depthTarget = 0; - fb.depthLevel = 0; - fb.depthLayer = -1; - fb.depthHasStencil = false; - fb.depthIsStencilOnly = false; - fb.attachmentsKnown = true; - } - - void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) { - if (!fb.attachmentsKnown) { - ScrubAllAttachments(fb, fbTarget); - } - } - - // The post-attach glGetError probe below must not misread an error some - // earlier operation left queued; drain before attaching (rare path - - // only runs when the attachment actually changes). - void DrainPendingGLErrors() { - while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - } - } - - // Record the color point as detached when the shadow said something was - // there; the actual detach call is the caller's (it may be replaced by - // the new attach directly when the point is being overwritten). - void RecordNoColor(ScratchFramebuffer& fb) { - fb.colorTex = 0; - fb.colorTarget = 0; - fb.colorLevel = 0; - fb.colorLayer = -1; - } - - void RecordNoDepth(ScratchFramebuffer& fb) { - fb.depthTex = 0; - fb.depthTarget = 0; - fb.depthLevel = 0; - fb.depthLayer = -1; - fb.depthHasStencil = false; - fb.depthIsStencilOnly = false; - } - } // namespace - - ScratchFramebuffer& TempFramebuffer() { - return g_scratchSets.Current().temp; - } - ScratchFramebuffer& BlitReadFramebuffer() { - return g_scratchSets.Current().blitRead; - } - ScratchFramebuffer& BlitDrawFramebuffer() { - return g_scratchSets.Current().blitDraw; - } - - Uint EnsureId(ScratchFramebuffer& fb) { - if (fb.id == 0) { - g_GLESFuncs.glGenFramebuffers(1, &fb.id); - // A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw - // buffers (the ES defaults for a non-default framebuffer). - fb.attachmentsKnown = true; - RecordNoColor(fb); - RecordNoDepth(fb); - fb.readBuffer = GL_COLOR_ATTACHMENT0; - fb.drawBuffer = GL_COLOR_ATTACHMENT0; - } - return fb.id; - } - - void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, - GLint level) { - PrepareForUse(fb, fbTarget); - if (fb.depthTex != 0) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - RecordNoDepth(fb); - } - if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) { - return; - } - if (fb.colorTex != 0) { - // Detach first: if the new attach fails, the point must read as - // missing (incomplete FBO), not silently keep the old texture. - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - } - DrainPendingGLErrors(); - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - RecordNoColor(fb); - return; - } - fb.colorTex = tex; - fb.colorTarget = texTarget; - fb.colorLevel = level; - fb.colorLayer = -1; - } - - void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) { - PrepareForUse(fb, fbTarget); - if (fb.depthTex != 0) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - RecordNoDepth(fb); - } - if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) { - return; - } - if (fb.colorTex != 0) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - } - DrainPendingGLErrors(); - g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - RecordNoColor(fb); - return; - } - fb.colorTex = tex; - fb.colorTarget = 0; - fb.colorLevel = level; - fb.colorLayer = layer; - } - - void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level, - Bool withStencil, Bool stencilOnly) { - PrepareForUse(fb, fbTarget); - if (fb.colorTex != 0) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - RecordNoColor(fb); - } - if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level && - fb.depthLayer < 0 && fb.depthHasStencil == withStencil && - fb.depthIsStencilOnly == stencilOnly) { - return; - } - if (fb.depthTex != 0) { - // One call clears both depth and stencil points regardless of how - // the previous attachment was made. - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - } - DrainPendingGLErrors(); - // A stencil-only texture has no depth point to attach at, and a depth one has no - // stencil point; GL_DEPTH_STENCIL_ATTACHMENT is only the combined texture's form. - g_GLESFuncs.glFramebufferTexture2D(fbTarget, - stencilOnly ? GL_STENCIL_ATTACHMENT - : (withStencil ? GL_DEPTH_STENCIL_ATTACHMENT - : GL_DEPTH_ATTACHMENT), - texTarget, tex, level); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - RecordNoDepth(fb); - return; - } - fb.depthTex = tex; - fb.depthTarget = texTarget; - fb.depthLayer = -1; - fb.depthLevel = level; - fb.depthHasStencil = withStencil; - fb.depthIsStencilOnly = stencilOnly; - } - - // The layer-point sibling of EnsureColorAttachmentLayer above, for a view's mip chain in - // a depth-only storage owner. depthTarget == 0 is the marker that this point was made - // with glFramebufferTextureLayer, exactly as colorTarget == 0 is on the color side, so a - // later 2D attach to the same level is never deduped against it. - void EnsureDepthAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) { - PrepareForUse(fb, fbTarget); - if (fb.colorTex != 0) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - RecordNoColor(fb); - } - if (fb.depthTex == tex && fb.depthTarget == 0 && fb.depthLevel == level && fb.depthLayer == layer && - !fb.depthHasStencil && !fb.depthIsStencilOnly) { - return; - } - if (fb.depthTex != 0) { - g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - } - DrainPendingGLErrors(); - g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_DEPTH_ATTACHMENT, tex, level, layer); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - RecordNoDepth(fb); - return; - } - fb.depthTex = tex; - fb.depthTarget = 0; - fb.depthLevel = level; - fb.depthLayer = layer; - fb.depthHasStencil = false; - fb.depthIsStencilOnly = false; - } - - void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) { - if (fb.readBuffer == readBuffer) { - return; - } - g_GLESFuncs.glReadBuffer(readBuffer); - fb.readBuffer = readBuffer; - } - - void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) { - if (fb.drawBuffer == drawBuffer) { - return; - } - g_GLESFuncs.glDrawBuffers(1, &drawBuffer); - fb.drawBuffer = drawBuffer; - } - - Uint EnsureCompleteTinyFramebufferId() { - ScratchSet& set = g_scratchSets.Current(); - Uint& g_completeTinyFBOId = set.completeTinyFBOId; - Uint& g_completeTinyRBOId = set.completeTinyRBOId; - if (g_completeTinyFBOId != 0) { - return g_completeTinyFBOId; - } - // One-time creation: the renderbuffer binding is context state with no - // shadow, so save/restore it by query here (cold path only). - GLint prevRenderbuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); - g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId); - g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId); - FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId); - g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1); - g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - g_completeTinyRBOId); - const GLenum drawBuffer = GL_COLOR_ATTACHMENT0; - g_GLESFuncs.glDrawBuffers(1, &drawBuffer); - g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Scratch 1x1 framebuffer is incomplete."); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); - return g_completeTinyFBOId; - } - - void NoteTextureIdDeleted(Uint textureId) { - if (textureId == 0) { - return; - } - // Textures ARE shared, so the deleted id may sit in any context's scratch FBO. - g_scratchSets.ForEach([textureId](ScratchSet& set) { - for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) { - if (fb->colorTex == textureId || fb->depthTex == textureId) { - fb->attachmentsKnown = false; - } - } - }); - } - - void OnBackendContextDestroyed() { - g_scratchSets.Clear(); - } - } // namespace ScratchFBOImpl - - namespace PixelStoreImpl { - namespace { - PackState g_packState; - Bool g_packStateKnown = false; - - void PinPackState(const PackState& value) { - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels); - g_packState = value; - g_packStateKnown = true; - } - } // namespace - - void ApplyPackState(const PackState& desired) { - if (!g_packStateKnown) { - PinPackState(desired); - return; - } - if (desired.Alignment != g_packState.Alignment) { - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment); - g_packState.Alignment = desired.Alignment; - } - if (desired.RowLength != g_packState.RowLength) { - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength); - g_packState.RowLength = desired.RowLength; - } - if (desired.SkipRows != g_packState.SkipRows) { - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows); - g_packState.SkipRows = desired.SkipRows; - } - if (desired.SkipPixels != g_packState.SkipPixels) { - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels); - g_packState.SkipPixels = desired.SkipPixels; - } - } - - PackState CurrentPackState() { - if (!g_packStateKnown) { - // Fresh/unknown context: pin to the GL defaults (what a new context - // starts with; writing them makes the shadow authoritative either way). - PinPackState(PackState{}); - } - return g_packState; - } - - void InvalidatePackStateCache() { - g_packStateKnown = false; - } - } // namespace PixelStoreImpl - - namespace PrgramImpl { - Uint32 g_snormFallbackClampOutputMask = 0; - Uint g_fragColorBroadcastCount = 1; - Uint32 g_unormFallbackClampOutputMask = 0; - Uint g_lastUsedBackendProgramId = 0; - // Every error-queue drain in the program build path is bounded by this: a lost - // context never answers GL_NO_ERROR, and the build runs on the thread that would - // then spin forever. - constexpr Int kMaxDrainedProgramErrors = 32; - TwinRegistry g_backendProgramObjects; - - // ---- P5e (pg): the source, both ways round --------------------------------------- - - Int ProgramArchiveSource::GetUniformLocation(const String& name) const { - // ProgramObject::GetUniformLocation over the archive. Reproduced rather than called - // because the frontend's form is a non-static member over Artifacts() and this arm - // has no ProgramObject to call it on; the ARRAY RULES are the whole body and they - // are the part a paraphrase would get wrong, so they are transcribed exactly. - const auto it = Link->uniformLocations.find(name); - if (it != Link->uniformLocations.end()) return static_cast(it->second); - if (name.empty()) return -1; - // Reflection stores GL-style names: an array uniform is keyed "arr[0]" at its base - // location, so a bare "arr" resolves to that entry. - if (name.back() != ']') { - const auto suffixed = Link->uniformLocations.find(name + "[0]"); - if (suffixed != Link->uniformLocations.end()) return static_cast(suffixed->second); - return -1; - } - if (name.length() < 4) return -1; - // An ARRAY OF ARRAYS is keyed by its full "[0]"-terminated spelling ("a[2][1][0]"), - // so a query that already ends in a subscript may still be the NAME of an array - // rather than an element of one. That reading is tried FIRST; only then is the - // trailing subscript read as an element index. - { - const auto arrayOfArrays = Link->uniformLocations.find(name + "[0]"); - if (arrayOfArrays != Link->uniformLocations.end()) { - return static_cast(arrayOfArrays->second); - } - } - const SizeT bracket = name.rfind('['); - if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1; - Uint element = 0; - for (SizeT i = bracket + 1; i < name.length() - 1; ++i) { - if (name[i] < '0' || name[i] > '9') return -1; - element = element * 10 + static_cast(name[i] - '0'); - if (element > 0x0FFFFFFFu) return -1; - } - auto base = Link->uniformLocations.find(name.substr(0, bracket) + "[0]"); - if (base == Link->uniformLocations.end()) { - base = Link->uniformLocations.find(name.substr(0, bracket)); - if (base == Link->uniformLocations.end()) return -1; - } - const Int baseLocation = static_cast(base->second); - if (!IsValidUniformLocation(baseLocation)) return -1; - const Int index = Link->uniformIndexInTProgram[baseLocation]; - if (!UniformAt(index).type.isArray) return -1; - if (static_cast(element) >= - MG_State::GLState::ProgramObject::GetUniformArraySizeByTIndex(*Link, index)) { - return -1; - } - const Int location = baseLocation + static_cast(element); - if (!UniformLocationsAliasSameUniform(baseLocation, location)) return -1; - return location; - } - - const String& ProgramArchiveSource::GetUniformBlockName(Uint index) const { - static const String kEmpty; - if (index >= Link->glBlockIndexToTProgram.size()) return kEmpty; - const Int tBlockIndex = Link->glBlockIndexToTProgram[index]; - if (tBlockIndex < 0 || static_cast(tBlockIndex) >= Link->blockReflection.size()) { - return kEmpty; - } - return Link->blockReflection[tBlockIndex].name; - } - - Uint ProgramArchiveSource::GetUniformBlockBinding(Uint index) const { - // THE OVERLAY WINS WHEN THERE IS ONE, and the archive's link-time value stands when - // there is not. glUniformBlockBinding moves this AFTER the link that produced the - // archive, so a twin answering from the archive alone would bind the program's - // blocks to the points it was LINKED with rather than the ones it is BOUND with. - if (OverlayGoverns && BlockBindingOverlay != nullptr) { - if (index < BlockBindingOverlay->size()) { - return static_cast((*BlockBindingOverlay)[index]); - } - // A block past the tail the record declared. The tail is dense over - // GetActiveUniformBlocksCount(), so this is a record and an archive that - // disagree about how many blocks the program has - which can only happen if a - // create and a bindings record crossed. 0 is GL's own default and is the safe - // answer; the mismatch is the emitter's to fix, not this read's to hide. - return 0; - } - if (index >= Link->uniformBlockBinding.size()) return 0; - return static_cast(Link->uniformBlockBinding[index]); - } - - Int ProgramArchiveSource::GetUniformSamplerOrImageUnitIndex(Uint location) const { - if (OverlayGoverns) { - // THE TAIL IS THE WHOLE SET, so a location it does not name has no unit - NOT - // whatever the archive's link-time snapshot said. The client emits every - // assigned unit it can see, including the ones the link seeded from - // layout(binding=N), so "absent" really does mean -1. - // - // Sparse and ASCENDING BY LOCATION on the wire (CONTRACT-P5E §1), which is what - // lets this be a binary search: the build walks every location from 0 to - // maxUniformLocation, and a linear scan per location would be quadratic on a - // program with a thousand locations and a handful of samplers. - if (SamplerUnitOverlay == nullptr) return -1; - const auto it = std::lower_bound( - SamplerUnitOverlay->begin(), SamplerUnitOverlay->end(), location, - [](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) { - return entry.Location < value; - }); - if (it == SamplerUnitOverlay->end() || it->Location != location) return -1; - return static_cast(it->Unit); - } - if (location >= Link->uniformSamplerOrImageUnitIndex.size()) return -1; - return Link->uniformSamplerOrImageUnitIndex[location]; - } - - ProgramArchiveSource ProgramArchiveSource::FromRecord( - MG_Pipe::MGPipeHandle cso, const MG_Pipe::MGPipeShaderCsoRecord& record) { - // THE HANDLE ARM. Every field below is server-owned: the archive the create adopted, - // the descriptor the create stored, and the three tails set_program_bindings - // carried. Nothing here touches a ProgramObject, which is the property the strict - // lane measures. - ProgramArchiveSource source; - source.Link = &record.Archive->Link; - source.Spirv = &record.Archive->Spirv; - source.Identity = cso.Slot; - source.IdentityIsHandleSlot = true; - // LinkStatus IS A FIELD NOW, and that is the whole point of c0e adding it: before - // P5e "linked" was only implied by the create existing, which is a different - // statement - create_shader_state is re-issued at every link that moves the link - // version, and a FAILED relink of a bound program moves it too (ID-88). - source.Linked = record.Desc.LinkStatus != 0; - source.SpirvUsable = record.Desc.SpirvStatus != 0; - source.SpirvValidationEnabled = record.Desc.EnableSpirvValidation != 0; - source.PointSizeWasDemoted = record.Desc.PointSizeDemoted != 0; - source.GlobalUboSize = record.Desc.GlobalUboSize; - source.LinkedStages.reserve(record.Archive->LinkedStages.size()); - for (const Uint32 stage : record.Archive->LinkedStages) { - source.LinkedStages.push_back(static_cast(stage)); - } - // "HAS A BINDINGS RECORD EVER BEEN APPLIED TO THIS RECORD" - not "is the tail - // non-empty". A program with no uniform blocks, no sampler and no override emits a - // record with three empty tails, and that record is the statement that the archive's - // link-time values are no longer the answer. - source.OverlayGoverns = record.BindingsSerial != 0; - source.BlockBindingOverlay = &record.BlockBindings; - source.SamplerUnitOverlay = &record.SamplerUnits; - if (source.OverlayGoverns) { - source.StorageOverrides.reserve(record.StorageOverrides.size()); - for (const auto& entry : record.StorageOverrides) { - source.StorageOverrides[entry.Name] = entry.Binding; - } - } else { - source.StorageOverrides = record.Archive->Link.shaderStorageBlockBinding; - } - // THE RECORD'S NUMBER EITHER WAY, and deliberately not a recomputation when the - // overlay is absent. This is the value the twin STAMPS and the draw path's clean - // clause COMPARES (CONTRACT-P5E §5.5 names record.Signature as the input), so the - // two have to be the same field or every draw of a program with an override - // rebuilds. A record with no bindings applied carries 0 while the archive may hold - // a link-time override map - the next set_program_bindings then produces a non-zero - // signature and forces exactly one rebuild, which is correct and, since the emitter - // sends the bindings in the same breath as the create, not a state a draw reaches. - source.StorageOverrideSignature = record.Signature; - return source; - } - - Uint ProgramBlockBindingFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Int blockIndex) { - if (blockIndex < 0) return 0; - const SizeT index = static_cast(blockIndex); - if (record.BindingsSerial != 0) { - // The tail is the whole set; a block past it is a record and an archive that - // disagree about the block count, and GL's own default of 0 is the safe answer. - return index < record.BlockBindings.size() - ? static_cast(record.BlockBindings[index]) - : 0; - } - if (!record.Archive || index >= record.Archive->Link.uniformBlockBinding.size()) return 0; - return static_cast(record.Archive->Link.uniformBlockBinding[index]); - } - - Int ProgramSamplerUnitFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Uint location) { - if (record.BindingsSerial != 0) { - const auto it = std::lower_bound( - record.SamplerUnits.begin(), record.SamplerUnits.end(), location, - [](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) { - return entry.Location < value; - }); - if (it == record.SamplerUnits.end() || it->Location != location) return -1; - return static_cast(it->Unit); - } - if (!record.Archive || - location >= record.Archive->Link.uniformSamplerOrImageUnitIndex.size()) { - return -1; - } - return record.Archive->Link.uniformSamplerOrImageUnitIndex[location]; - } - - // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract - in particular why the - // composite band had to land in the same package as this function. - BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso) { - if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; - // THE RECORD FIRST, and through the BAND-AWARE reader: a composite pipeline's CSO - // lives in the applier's CompositeShaderCsos table, and asking the ordinary one for - // it would answer null for a program that is perfectly live. - if (PipeShaderCsoRecordForHandle(cso) == nullptr) { - MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record on the handle arm, so " - "no driver program can be built for it and the draw keeps what is bound", - cso.Slot, cso.Gen); - return nullptr; - } - auto* slot = AdoptTwinByHandle(g_backendProgramObjects, cso, "shader CSO"); - if (slot == nullptr) return nullptr; - if (!*slot) *slot = MakeShared(); - return slot->get(); - } - - BackendProgramObjectImpl::BackendProgramObjectImpl() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - m_backendProgramId = g_GLESFuncs.glCreateProgram(); - if (m_backendProgramId == 0) { - MGLOG_E_ONCE("Failed to create program object in backend."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - - } else { - MGLOG_D("Created backend program object with ID: %u", m_backendProgramId); - } - } - - BackendProgramObjectImpl::~BackendProgramObjectImpl() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (InProcessTeardown()) { - return; // see InProcessTeardown(): the driver may be unloaded already - } - if (m_backendProgramId != 0) { - MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId); - g_GLESFuncs.glDeleteProgram(m_backendProgramId); - // The driver may recycle this GL name for a future program; a stale - // guard entry would then wrongly skip the glUseProgram for it. - if (g_lastUsedBackendProgramId == m_backendProgramId) { - g_lastUsedBackendProgramId = 0; - } - } - } - - Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding) { - if (backendProgramId == 0 || blockName.empty()) return false; - if (!g_GLESFuncs.glGetProgramResourceIndex || !g_GLESFuncs.glShaderStorageBlockBinding) return false; - GLuint driverIndex = - g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, blockName.c_str()); - if (driverIndex == GL_INVALID_INDEX) { - // An arrayed block is enumerated per element by GL but declared once; the - // generated ESSL carries the bare block name. - const auto bracket = blockName.rfind('['); - if (bracket == String::npos || blockName.back() != ']') return false; - driverIndex = g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, - blockName.substr(0, bracket).c_str()); - if (driverIndex == GL_INVALID_INDEX) return false; - } - g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, driverIndex, binding); - return true; - } - - void ReseedShaderStorageBlockBindings(Uint backendProgramId, - const ProgramBuildSource& src) { - const auto& overrides = src.GetShaderStorageBlockBindingOverrides(); - if (overrides.empty()) return; // the overwhelming majority of programs - for (const auto& [blockName, binding] : overrides) { - if (binding < 0) continue; - ApplyShaderStorageBlockBinding(backendProgramId, blockName, static_cast(binding)); - } - } - - // P5e (pg). ON A PUSH BUILD THE SIGNATURE IS THE SOURCE'S, TAKEN VERBATIM, and it is not - // recomputed here for a reason worth stating: on the handle arm the number the DRAW PATH - // compares against is MGPipeShaderCsoRecord::Signature - the CLIENT's commutative hash, - // computed in MG_Impl/Pipe/ProgramEmit.h. If the server recomputed its own from the - // record's override tail, the two formulas would have to stay bit-identical for ever or - // every draw of every program with an override would rebuild. Taking the client's number - // verbatim removes that coupling: there is one authority, and the twin's stamp and the - // clean clause read the same field. The monolith arm's source carries the answer of the - // function this replaces, computed from the frontend map, so its behaviour is unchanged. - Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src) { - return src.StorageOverrideSignature; - } - - namespace { - // The GL internal format bound to an image unit right now. GL_NONE for a unit - // outside the frontend's array, which cannot be addressed at all. - Uint BoundImageUnitFormat(Int unit) { - if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0; - // P5e (fb, §5.4): the format-less image bake asks what format the APPLICATION - // named at this unit, and under a transport the record is the only statement of - // it - MGPImageView::InternalFormat is that same GLenum, copied by the client - // out of the unit's own binding. A unit the applier has never been told about - // answers 0, which is what an unbound unit answers on the frontend arm too. - const auto& st = MG_Pipe::MGPipeApplier(); - if (static_cast(unit) >= st.BoundShaderImages.size()) return 0; - return static_cast(st.BoundShaderImages[static_cast(unit)].InternalFormat); - } - - // Combines one (unit, format) pair into a running digest. Commutative, so the order - // the uniforms are walked in cannot change the answer, and mixed rather than summed - // so a unit and a format cannot trade places between two pairs and cancel out. - Uint64 MixImageUnitFormat(Uint64 signature, Int unit, Uint format) { - Uint64 entry = static_cast(static_cast(unit)) + 0x9e3779b97f4a7c15ull; - entry ^= static_cast(format) + 0xbf58476d1ce4e5b9ull + (entry << 6) + (entry >> 2); - return signature + entry; - } - - // Pipeline position of a shader stage. Names the PRODUCER of an inter-stage - // interface block: a block one stage consumes was written by the stage before it. - // ShaderStage is declared in pipeline order, so the enum value IS the position; - // compute has no inter-stage interface at all and is reported as -1. - Int InterStagePipelineIndex(ShaderStage stage) { - switch (stage) { - case ShaderStage::Vertex: - case ShaderStage::TessControl: - case ShaderStage::TessEval: - case ShaderStage::Geometry: - case ShaderStage::Fragment: - return static_cast(stage); - default: - return -1; - } - } - - // Whether a stage can declare interface blocks in BOTH directions at once, i.e. - // whether one block name can name two different blocks inside it. A vertex INPUT - // and a fragment OUTPUT cannot be blocks and compute has neither, so only these - // three can. This is what keeps the module probe off every program without - // tessellation or geometry - which is every program Minecraft and its shader packs - // build. - Bool CanDeclareBlocksInBothDirections(ShaderStage stage) { - return stage == ShaderStage::TessControl || stage == ShaderStage::TessEval || - stage == ShaderStage::Geometry; - } - - // Reflection names an array uniform after its first element ("g_image[0]") at every - // location it spans; SPIR-V names the variable once, without the subscript. This is - // the name both sides agree on. - String ImageUniformBaseName(const String& reflectionName) { - if (reflectionName.size() >= 3 && reflectionName.compare(reflectionName.size() - 3, 3, "[0]") == 0) { - return reflectionName.substr(0, reflectionName.size() - 3); - } - return reflectionName; - } - - // GL_MAX__IMAGE_UNIFORMS as the ES driver reports it, which is also exactly what - // MobileGL advertises for it (GL_Getter answers from the same DynamicBackendParameters). - // -1 for a stage the ES side has no such limit for, which is the "cannot say" answer - // the diagnostic that reads it prints rather than a made-up number. [[maybe_unused]] - // because its only caller is an MGLOG_E argument, and MGLOG_E compiles to nothing in a - // build whose MOBILEGL_LOG_ACTIVE_LEVEL is above ERROR. - [[maybe_unused]] Int AdvertisedStageImageUniformLimit(ShaderStage stage) { - switch (stage) { - case ShaderStage::Vertex: return g_GLESCapabilities.MaxVertexImageUniforms; - case ShaderStage::TessControl: return g_GLESCapabilities.MaxTessControlImageUniforms; - case ShaderStage::TessEval: return g_GLESCapabilities.MaxTessEvaluationImageUniforms; - case ShaderStage::Geometry: return g_GLESCapabilities.MaxGeometryImageUniforms; - case ShaderStage::Fragment: return g_GLESCapabilities.MaxFragmentImageUniforms; - case ShaderStage::Compute: return g_GLESCapabilities.MaxComputeImageUniforms; - default: return -1; - } - } - - // Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only - // through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes - // through untouched - the emitted ESSL still has to be legal for the driver. - Bool IsCoreEsslLayoutFormat(glslang::TLayoutFormat format) { - switch (format) { - case glslang::ElfRgba32f: - case glslang::ElfRgba16f: - case glslang::ElfR32f: - case glslang::ElfRgba8: - case glslang::ElfRgba8Snorm: - case glslang::ElfRgba32i: - case glslang::ElfRgba16i: - case glslang::ElfRgba8i: - case glslang::ElfR32i: - case glslang::ElfRgba32ui: - case glslang::ElfRgba16ui: - case glslang::ElfRgba8ui: - case glslang::ElfR32ui: - return true; - default: - return false; - } - } - - // The GL internal format a glslang layout format names, for the eighteen non-core - // formats WidenImageFormatsForEssl carries losslessly plus nothing else: the only - // question asked of it is "does this DECLARED format widen", and answering 0 for - // everything else is the same "no" a non-widenable format gets. Kept as its own - // switch rather than routed through the frontend's enum converters because a - // TLayoutFormat is a glslang value and the reflection snapshot stores it raw. - // - // IT MUST LIST EXACTLY WHAT WideningOfSpirvImageFormat DOES. This table is what arms - // the pass (ImageFormatWillBeWidened -> declaresWidenableImageFormat), so a format the - // pass would carry but this switch answers 0 for never gets the chance: the module - // reaches SPIRV-Cross with its original qualifier, the throw takes the stage, and the - // only visible symptom is the "no GLSL ES spelling" diagnostic for a format that has - // one. That is exactly what r11f_g11f_b10f did until it was added here. - Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) { - switch (format) { - case glslang::ElfRg32f: return 0x8230; // GL_RG32F - case glslang::ElfRg16f: return 0x822F; // GL_RG16F - case glslang::ElfR16f: return 0x822D; // GL_R16F - case glslang::ElfRg8: return 0x822B; // GL_RG8 - case glslang::ElfR8: return 0x8229; // GL_R8 - case glslang::ElfRg8Snorm: return 0x8F95; // GL_RG8_SNORM - case glslang::ElfR8Snorm: return 0x8F94; // GL_R8_SNORM - case glslang::ElfRg32i: return 0x823B; // GL_RG32I - case glslang::ElfRg16i: return 0x8239; // GL_RG16I - case glslang::ElfR16i: return 0x8233; // GL_R16I - case glslang::ElfRg8i: return 0x8237; // GL_RG8I - case glslang::ElfR8i: return 0x8231; // GL_R8I - case glslang::ElfRg32ui: return 0x823C; // GL_RG32UI - case glslang::ElfRg16ui: return 0x823A; // GL_RG16UI - case glslang::ElfR16ui: return 0x8234; // GL_R16UI - case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI - case glslang::ElfR8ui: return 0x8232; // GL_R8UI - // Not a channel widening but a lossless re-encoding into rgba16f - the one entry - // here whose carrier has a different per-channel layout. See - // WidenImageFormatsPass.h. - case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F - // 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same - // channel count, every value representable. Only the transfer is re-encoded. - case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI - // The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES. - // These are the entries whose carrier changes the shader-visible type as well as - // the qualifier (image2D becomes uimage2D), so every access through them is - // wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h. - case glslang::ElfRgba16: return 0x805B; // GL_RGBA16 - case glslang::ElfRg16: return 0x822C; // GL_RG16 - case glslang::ElfR16: return 0x822A; // GL_R16 - case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2 - case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM - case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM - case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM - default: - return 0; - } - } - - // Whether the ESSL chain will re-declare an image of this format in a core carrier - // and mask its accesses (WidenImageFormatsForEssl). The same rule - // TextureImpl::GetImageBindableStorageWidening applies to the storage and the bind - - // the three layers move together or the shader addresses a texel size the storage - // does not have. - // - // Without GL_NV_image_formats there is no legal spelling for any non-core format, so - // everything carriable widens. WITH the extension only the formats SPIRV-Cross - // refuses to print do: it throws for its is_desktop_only_format set instead of - // emitting a token, and the throw loses the stage however willing the driver was. - Bool ImageFormatWillBeWidened(Uint glInternalFormat) { - if (glInternalFormat == 0) return false; - if (MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) == 0) { - return false; - } - return !g_GLESCapabilities.SupportsExtendedImageFormats || - !MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat( - glInternalFormat); - } - } // namespace - - // What the format bake needs from the frontend, collected in one walk of the uniform - // reflection: which image uniforms declared NO format (the only ones a bake may touch - - // a declared format is authoritative and stays), what the units they address currently - // hold, and whether any format in play - declared or baked - is outside the ES core set. - ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src) { - 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 = src.GetMaxUniformLocation(); - for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { - const auto& name = src.GetUniformName(loc); - if (name.empty()) continue; - if (!IsImageUniformType(src.GetUniformType(loc))) continue; - const auto& type = src.GetUniformTypeFacts(loc); - if (type.hasFormat) { - // Declared, and therefore never overridden by the BAKE - but a non-core - // spelling still has to become legal ESSL somehow. - const auto declaredFormat = static_cast(type.layoutFormat); - if (!IsCoreEsslLayoutFormat(declaredFormat)) { - // Eighteen of the twenty-six non-core formats are re-declared in a core - // format that carries them losslessly, with every access masked back to - // the channels GL says they have (WidenImageFormatsForEssl, and the - // matching storage/bind widening in TextureImpl). Those need neither the - // extension nor the diagnostic: there IS a legal spelling for them now. - if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) { - inputs.declaresWidenableImageFormat = true; - } else { - inputs.needsExtendedImageFormats = true; - if (!g_GLESCapabilities.SupportsExtendedImageFormats) { - // From the OWNED TypeFacts, not from a live TType: the reflection - // snapshot already carries the declared layout format, and there - // is no glslang object to ask on a translation-cache L1 hit. - recordUnspellableFormat( - name, glslang::TQualifier::getLayoutFormatString(declaredFormat)); - } - } - } - continue; - } - const Int unit = src.GetUniformSamplerOrImageUnitIndex(loc); - if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue; - const Uint boundFormat = BoundImageUnitFormat(unit); - - // Every format-less uniform contributes to the rebuild key, including one whose - // unit holds nothing yet: an image bound for the first time AFTER the link has - // to move the key, or the program built against "nothing bound" would never be - // rebuilt against the real format. - inputs.units.push_back(unit); - inputs.signature = MixImageUnitFormat(inputs.signature, unit, boundFormat); - - if (boundFormat == 0) continue; - if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) { - // The same three-way split the DECLARED branch above makes, and it has to be - // the same one: a format-less image is baked with the bound format, so from - // WidenImageFormatsForEssl's point of view the two routes hand it identical - // modules and must arm it identically. - if (ImageFormatWillBeWidened(boundFormat)) { - // The bake writes this format INTO the module, so the widening that runs - // straight after has to be armed for it even though nothing DECLARED it - - // and armed WHETHER OR NOT the driver has GL_NV_image_formats. SPIRV-Cross - // throws for its is_desktop_only_format set the moment it targets ESSL, - // however willing the driver was, so the extension decides HOW MUCH gets - // widened (widenOnlyUnprintableImageFormats) and never WHETHER. Arming - // this only on the no-extension path left the shader half of the widening - // switched off while TextureImpl's storage/bind half - which keys on - // SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran: - // the stage threw, the program linked without it, and every dispatch - // silently did nothing. That is the whole of - // KHR-GL43.stencil_texturing.functional's compute half, whose uni_image is - // a format-less uimage2D bound to an R8UI texture. - inputs.declaresWidenableImageFormat = true; - } else if (!g_GLESCapabilities.SupportsExtendedImageFormats) { - // Outside the GLSL ES core set, with no GL_NV_image_formats to spell it - // and no core format that carries it exactly: there is no legal ESSL for - // this stage at all. Leaving the image format-LESS is NOT a softer - // failure: all three test devices reject a format-less image declaration - // outright ("all images have to define layout format"), readonly and - // writeonly alike, so it trades one hard compile error for another. The - // unit stays in the rebuild key either way, so a rebind to a spellable - // format still rebuilds and works. - MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which " - "GLSL ES core cannot spell, this driver has no GL_NV_image_formats for, and " - "no core format carries exactly.", - name.c_str(), unit, boundFormat); - recordUnspellableFormat( - name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat)); - continue; - } else { - inputs.needsExtendedImageFormats = true; - } - } - const String baseName = ImageUniformBaseName(name); - const auto existing = inputs.glFormatByUniformName.find(baseName); - if (existing == inputs.glFormatByUniformName.end()) { - inputs.glFormatByUniformName.emplace(baseName, boundFormat); - } else if (existing->second != boundFormat) { - // An ARRAY whose elements were pointed at units holding different formats. - // One declaration carries one qualifier, so there is no spelling for it, and - // the uniform is left format-less rather than given a format that is wrong - // for all but one element. Marked in place with GL_NONE and swept below - - // never by erasing here, because the entry is reached again by the array's - // remaining elements and a flat hash map must not be mutated structurally - // while an iterator into it is live. - existing->second = 0; - } - } - for (const auto& entry : inputs.glFormatByUniformName) { - if (entry.second == 0) inputs.conflictedNames.push_back(entry.first); - } - for (const auto& conflicted : inputs.conflictedNames) { - inputs.glFormatByUniformName.erase(conflicted); - } - // Split off the ones SPIRV-Cross will not print. They cannot go through the module - - // it throws for them when targeting ESSL, and the stage is lost - so they are spelled - // into the emitted text instead. Collected first, erased after, because a flat hash - // map must not be restructured while it is being walked. - Vector textCompleted; - for (const auto& entry : inputs.glFormatByUniformName) { - if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) { - continue; - } - // A format the widening carries stays on the module route even though SPIRV-Cross - // would not print it: by the time SPIRV-Cross sees the declaration it names the - // core carrier, which it does print. Writing the narrow spelling into the text - // instead would put back exactly the token the driver rejects. - if (ImageFormatWillBeWidened(entry.second)) { - continue; - } - String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second); - if (spelling.empty()) continue; // no image-format spelling at all; nothing to write - inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling)); - textCompleted.push_back(entry.first); - } - 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; - } - - // Every image ARRAY whose elements the application did NOT leave on units consecutive from - // element zero - the only shape ESSL can spell, since an image unit there comes solely - // from the one layout(binding=N) an array declaration carries. Desktop GL assigns them - // per element with glUniform1i, which ES makes an INVALID_OPERATION on an image uniform, - // so there is nothing to fix at the API end and the emitted text has to carry it - // (RemapImageArrayElementUnits). Empty for every program that does not do this, which is - // very nearly all of them - one walk of the reflection and no allocation in that case. - Vector CollectNonConsecutiveImageArrayPlans(const ProgramBuildSource& src) { - Vector plans; - const Uint maxUniformLoc = src.GetMaxUniformLocation(); - for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { - const auto& name = src.GetUniformName(loc); - if (name.empty()) continue; - if (!IsImageUniformType(src.GetUniformType(loc))) continue; - // Reflection repeats the array's "g_image[0]" spelling at EVERY location the array - // spans, so only the location that name resolves back to is the array itself. - if (src.GetUniformLocation(name) != static_cast(loc)) continue; - const String baseName = ImageUniformBaseName(name); - if (baseName == name) continue; // a scalar image: one binding says it all - - ImageArrayUnitPlan plan; - plan.name = baseName; - for (Uint element = loc; element <= maxUniformLoc && - src.UniformLocationsAliasSameUniform( - static_cast(loc), static_cast(element)); - ++element) { - plan.units.push_back(src.GetUniformSamplerOrImageUnitIndex(element)); - } - if (plan.units.size() < 2) continue; - - Bool consecutive = true; - for (SizeT element = 0; element < plan.units.size(); ++element) { - if (plan.units[element] != plan.units[0] + static_cast(element)) { - consecutive = false; - break; - } - } - // What ESSL does unaided is already right; leaving these out is what keeps the - // emitted text of every ordinary image shader byte-identical to before. - if (consecutive) continue; - plans.push_back(Move(plan)); - } - return plans; - } - - Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const { - if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs - Uint64 signature = 0; - for (const Int unit : m_formatlessImageUnits) { - signature = MixImageUnitFormat(signature, unit, BoundImageUnitFormat(unit)); - } - return signature; - } - - Bool BackendProgramObjectImpl::ImageUnitFormatsStillMatch() const { - if (m_formatlessImageUnits.empty()) return m_imageUnitFormatSignature == 0; - return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature; - } - - - // ===== THE MEMOIZED SEGMENT (shader translation memo, level 2) ===== - // - // One stage's sanitized SPIR-V turned into the ESSL SPIRV-Cross emits, through the - // DirectGLES-specific pass chain. Extracted out of SyncToBackend's loop so that the - // boundary the L2 memo keys on is a function signature rather than a comment: every - // input this reads is either an argument below or a process-global capability bit, - // and EVERY ONE OF THEM IS IN EsslTranslationKeyInputs. If you add a read here, add - // it to BuildEsslTranslationKey too - an under-specified key here is a silently - // miscompiled shader. - // - // Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray, - // MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples, - // SupportsNoperspectiveInterpolation, SupportsExtendedImageFormats, GLESVersion} - // (the last via ResolveBackendEsslVersion); and m_backendProgramId, for a log line only. - // - // Deliberately NOT in here, and therefore NOT in the key: the text-level passes that - // follow in SyncToBackend. They are cheap string work and they read a long tail of - // live per-program state (RebindImageUniformsToFrontendUnits walks the ProgramObject - // reflection, the norm-clamp masks and the fragColor broadcast count are live - // globals, the buffer-texture tier retargets an #extension line) whose inclusion - // would make the key both enormous and fragile for no measurable saving. - // - // Returns false when SPIRV-Cross refused the module; `outError` then holds its - // message and nothing is memoized. - Bool BackendProgramObjectImpl::TranspileSpirvToEssl( - const Vector& spirvCode, const GLenum glShaderType, - const std::set& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake, - const UnorderedMap& storageBlockBindingOverrides, - const std::map& inputBlockRenames, - const std::map& outputBlockRenames, const Bool stripInputBlockLocations, - const Bool stripOutputBlockLocations, - const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource, - std::set& outFlattenedXfbBlockNames, Vector& outAtomicCounterGlBindings, - String& outError) const { - // ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to - // plain globals (mg_*) before handing the module to SPIRV-Cross. - Vector loweredSpirv; - const Vector* effectiveSpirv = &spirvCode; - if (glShaderType == GL_VERTEX_SHADER && - MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) && - !loweredSpirv.empty()) { - effectiveSpirv = &loweredSpirv; - } - - // A geometry stage that writes memory, on a driver that drops the writes it makes - // after EmitVertex(): every emit is deferred behind them (DeferGeometryEmitsPass). FIRST - // in the chain, while every output is still an Output variable - the viewport-index - // lowering below demotes one, and must see the replay's stores to it as well. - // Self-gated on the module (an emit and a memory write), so a geometry stage that - // writes nothing comes back untouched. Keyed by deferGeometryEmitsArmed. - Vector deferredEmitSpirv; - if (glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit) { - bool deferred = false; - if (MG_Util::ShaderTranspiler::ShaderCompiler::DeferGeometryEmitsForEssl( - *effectiveSpirv, deferredEmitSpirv, deferred, enableSpirvValidation) && - deferred && !deferredEmitSpirv.empty()) { - effectiveSpirv = &deferredEmitSpirv; - MGLOG_I_ONCE("DirectGLES is deferring a geometry stage's EmitVertex() calls behind its " - "memory writes, because this driver drops storage writes made after an emit."); - } - } - - // GL_KHR_shader_subgroup where the ES driver has none (SubgroupPolicy.h): the compute - // stage's subgroup operations are lowered onto a 32-lane virtual subgroup in - // workgroup-shared memory, so the ESSL needs nothing from the driver but barrier() - // and shared memory. Where the driver has subgroups, the operations stay and - // SPIRV-Cross spells them as GL_KHR_shader_subgroup_* built-ins. iterationRP's - // scratch race is patched first unless MOBILEGL_ITRP_FIX is ForceOff, as - // on DirectVulkan. On a driver whose shared-memory loads go stale across barrier() - // the scratch is read through atomic loads. Keyed by subgroupEmulationArmed / - // iterationRPBarrierFixArmed / subgroupScratchAtomicReads. - Vector barrierFixedSpirv; - Vector emulatedSubgroupSpirv; - if (glShaderType == GL_COMPUTE_SHADER && - ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated) { - if (ShouldApplyIterationRPFixes() && - MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan( - *effectiveSpirv, barrierFixedSpirv, enableSpirvValidation) && - !barrierFixedSpirv.empty()) { - effectiveSpirv = &barrierFixedSpirv; - } - if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan( - *effectiveSpirv, emulatedSubgroupSpirv, - static_cast(g_GLESCapabilities.MaxComputeSharedMemorySize), enableSpirvValidation, - g_GLESCapabilities.SharedMemoryLoadsGoStale) && - !emulatedSubgroupSpirv.empty()) { - effectiveSpirv = &emulatedSubgroupSpirv; - } else { - MGLOG_E("DirectGLES: subgroup emulation declined a compute stage; it keeps subgroup " - "operations this ES driver cannot compile"); - } - } - - // ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters - // there IS an extension that provides it - so this runs only when the driver does - // NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and - // gets the `#extension` request added to the decompiled source below instead. - // Demoting the builtin costs the multi-viewport routing (every invocation lands in - // viewport 0), which is the degradation ViewportArrayScenario already documents - // for this backend; NOT demoting it costs the whole program, because the stage - // fails to compile and every draw made with it silently renders nothing. - // Gated on the module actually declaring the output, so no other stage pays an - // optimizer round trip for it. - // One parse of the module answers every armed pass gate below. The per-gate - // Declares* probes each cost a BuildModule per stage, and on a driver where both - // gates are armed (Mali: no GL_OES_viewport_array AND integer multisample - // squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower. - // Probing the pre-lowering module is sound for both gates: demoting - // gl_ViewportIndex neither adds nor removes multisampled image types. - // Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): - // this is backend code and must not reach into the GL frontend. 4 is that - // translation unit's kFrontendMaxSamples, which is the source of truth - - // keep the two in step. - const Int advertisedMaxSamples = - std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); - // Armed by the EMULATION as well as by the missing extension, and the emulation is on - // by default (MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a - // reason to keep the builtin: it only ever gave the SHADER a compilable name, while - // the driver's INDEXED viewport state was never programmed by anything in MobileGL - // (SyncRenderState pushes index 0 and stops), so an extension-capable driver - // rasterized every index as index 0 exactly like a driver without it. Lowering here - // is what lets the ESSL passes downstream turn the builtin into the flat varying the - // replay gates on. - // - // Restricted to the three stages GL lets WRITE the builtin (4.1 core gives it to the - // geometry stage, ARB_shader_viewport_layer_array adds vertex and tessellation - // evaluation). A fragment stage's gl_ViewportIndex is an INPUT, which the pass - // declines anyway, and a compute stage has none - so arming those two only ever - // bought them the shared probe's BuildModule for nothing. - const Bool stageCanWriteViewportIndex = glShaderType == GL_VERTEX_SHADER || - glShaderType == GL_TESS_EVALUATION_SHADER || - glShaderType == GL_GEOMETRY_SHADER; - const Bool viewportLoweringArmed = - stageCanWriteViewportIndex && - (ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray); - const Bool sampleClampArmed = - g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples || - g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples || - g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples; - // The image-format widening is armed on EVERY driver, so its probe has to ride the - // shared parse rather than add one: it is asked of every stage of every program, and - // a BuildModule per stage per gate is exactly what cost compile-heavy CTS cases ~10% - // before this struct existed. What differs per driver is only HOW MUCH it widens - - // everything carriable where there is no GL_NV_image_formats to spell the narrow - // format, and only the formats SPIRV-Cross refuses to print where there is. - const Bool widenOnlyUnprintableImageFormats = g_GLESCapabilities.SupportsExtendedImageFormats; - MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates; - if (viewportLoweringArmed || sampleClampArmed) { - spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures( - *effectiveSpirv); - } - - Vector loweredViewportSpirv; - if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput && - MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl( - *effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) && - !loweredViewportSpirv.empty()) { - effectiveSpirv = &loweredViewportSpirv; - MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which ESSL has no core " - "spelling for. The builtin was demoted to a plain global; %s.", - m_backendProgramId, - MG_Util::ConvertGLEnumToString(glShaderType).c_str(), - ViewportArrayEmulationEnabled() - ? "the ESSL passes below promote it to a routing varying" - : "every invocation renders into viewport 0"); - } - - // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample - // ceiling MobileGL advertises is floored to 4 no matter what the ES driver - // reports - but the realised allocation cannot be, and - // ClampSamplesToBackendSupport quietly gives an integer or depth multisample - // texture the ONE sample Adreno and Mali actually support for it. A shader - // written against the advertised ceiling then fetches a sample that storage does - // not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and - // KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp - // the Sample operand to the backend-real per-category maximum so the fetch lands - // inside the allocation. Gated on some category actually being squeezed AND the - // module actually declaring a multisampled image, so no other stage pays an - // optimizer round trip for it. DirectVulkan is deliberately not given this: it - // allocates the sample count it was asked for, so its modules are already right. - Vector clampedSampleSpirv; - if (sampleClampArmed && spirvGates.DeclaresMultisampledImage && - MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl( - *effectiveSpirv, clampedSampleSpirv, - g_GLESCapabilities.MaxColorTextureSamples, - g_GLESCapabilities.MaxIntegerSamples, - g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples, - enableSpirvValidation, ClampsUnsignedMultisampleFetchesAtRuntime()) && - !clampedSampleSpirv.empty()) { - effectiveSpirv = &clampedSampleSpirv; - } - - // GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module - // rather than emulating them, so this has to happen before it sees the binary. - Vector splitArrayInputSpirv; - if (glShaderType == GL_VERTEX_SHADER && - MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl( - *effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) && - !splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) { - // Only when the pass ACTUALLY split something. The optimizer hands back a - // re-serialised copy either way, and adopting that copy for every vertex - // shader would put every one of them through a round trip they do not need - // - which is not free: it cost the create-indirect retrace 0.15 SSIM the - // first time this gate was missing (the original capture; the case is now - // minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). - effectiveSpirv = &splitArrayInputSpirv; - } - - // Adopt the rewritten module only when THIS stage actually had one of the - // blocks - the optimizer hands back a re-serialised copy either way, and taking - // that copy for a module it did not rewrite is not free (it cost the - // create-indirect retrace 0.15 SSIM when the array-input split first missed - // this gate; the case is now - // minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). The report has to be - // per stage, not cumulative: a fragment shader consuming the same block reports a - // name the vertex stage already reported, - // and its own rewrite must still be taken or the two stages stop matching. - Vector flattenedXfbSpirv; - if (!xfbCaptureBlockNames.empty()) { - // Reported into a local first, and published only if the module is really - // adopted. The caller unions the published set unconditionally (so that a - // cache HIT contributes its names too), so publishing a name for a rewrite - // that was declined would rename a capture the emitted ESSL never renamed. - std::set flattenedNames; - if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl( - *effectiveSpirv, xfbCaptureBlockNames, flattenedNames, flattenedXfbSpirv, - enableSpirvValidation) && - !flattenedXfbSpirv.empty() && !flattenedNames.empty()) { - effectiveSpirv = &flattenedXfbSpirv; - outFlattenedXfbBlockNames = Move(flattenedNames); - } - } - - // The producer-keyed interface-block rename, planned program-wide by the caller and - // applied to this stage: the blocks it CONSUMES are spelled after the previous stage - // present in the program and the ones it PRODUCES after itself, so a tessellation - // evaluation stage's two TCSOutputBlocks stop being one name and every other stage - // still agrees with it. See UniquifyIoBlockNamesPass for why Mali needs it. - // - // INSIDE THE MEMOIZED SEGMENT, and at exactly the position it was written in - after - // the XFB flatten, before the UBO precision strip. Both halves of that matter: - // * INSIDE, because it rewrites the MODULE and the emitted ESSL carries the result. - // Left outside, a second program sharing this stage's key would be served ESSL - // with the blocks un-renamed and the repair would silently stop working - the - // same trap SetAtomicCounterBlockBindings sets one screen down. - // * AT THIS POSITION, because moving a SPIR-V pass in a chain is a behavioural - // change, and this one arrived device-verified on Mali. Hoisting it above the - // cache probe instead would have needed no key material at all (the rename would - // already be in the module bytes the key hashes) and was rejected for that - // reason: it reorders the chain, and it would re-serialise the module on every - // build including the ones the memo is there to make free. - // Its two rename maps are therefore KEY MATERIAL - see the caller. - Vector uniquifiedIoBlockSpirv; - if (!inputBlockRenames.empty() || !outputBlockRenames.empty()) { - std::set stageRenamedIoBlockNames; - if (MG_Util::ShaderTranspiler::ShaderCompiler::UniquifyIoBlockNamesForEssl( - *effectiveSpirv, inputBlockRenames, outputBlockRenames, - stageRenamedIoBlockNames, uniquifiedIoBlockSpirv, enableSpirvValidation) && - !uniquifiedIoBlockSpirv.empty() && !stageRenamedIoBlockNames.empty()) { - effectiveSpirv = &uniquifiedIoBlockSpirv; - MGLOG_D("Program %u stage %s: %zu inter-stage interface block(s) renamed per " - "producing stage, because some stage of this program declares the same " - "block name in both directions and the ES driver may alias the two.", - m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(), - stageRenamedIoBlockNames.size()); - } - } - - // ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints - // a RelaxedPrecision member as explicit "mediump" in the vertex stage and as - // UNQUALIFIED (mediump-by-default) in the fragment stage; after - // ForceSupporterOutput swaps the fragment header to highp, that member reads - // back as highp and the ES driver refuses to link ("definitions of uniform - // block ... do not match"). Strip the hint from block structs so both stages - // declare the member highp; nothing else about emission changes. - Vector uboPrecisionSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl( - *effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) && - !uboPrecisionSpirv.empty()) { - effectiveSpirv = &uboPrecisionSpirv; - } - - // noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective - // decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension - // GL_NV_shader_noperspective_interpolation : require`; a driver without that extension - // rejects the require. So on such devices emulate screen-linear interpolation instead - // (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop - // the decoration - exact, extension-free. Devices that have the extension keep the - // decoration and let the hardware do it natively. - Vector noperspectiveSpirv; - if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation && - MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl( - *effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) && - !noperspectiveSpirv.empty()) { - effectiveSpirv = &noperspectiveSpirv; - } - - // ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather - // than approximating one. The shared pass turns the type into the 2D one and - // divides the coordinate of every normalized-coordinate lookup by the texture - // size, which is the whole of the difference between the two. - Vector rectLoweredSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) && - !rectLoweredSpirv.empty()) { - effectiveSpirv = &rectLoweredSpirv; - } - - // ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height - // 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D - // as 2D for images without ever asking whether the type is arrayed, so a - // 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a - // two-component constructor, which every driver rejects, taking the whole - // program with it. The pass does the conversion properly - type to 2D array, - // coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own. - Vector arrayImageSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv, - arrayImageSpirv, enableSpirvValidation) && - !arrayImageSpirv.empty()) { - effectiveSpirv = &arrayImageSpirv; - } - - // The SAMPLER half of the same 1D story, and a defect one layer deeper than the one - // above. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - it just prints the - // OFFSET and the two GRADIENT operands with the arity the desktop shader spelled, so - // textureLodOffset(sampler1DArray, vec2, float, int) is emitted against a - // sampler2DArray and the driver answers "no matching overloaded function found", - // losing the stage and silently no-oping every dispatch that used it. Widening the - // operands alone would be an INVALID module (the validator derives the required arity - // from the image's own Dim), so the pass moves the type to 2D and widens coordinate, - // offset and gradients together. - // - // NO KEY MATERIAL, by the same test LegalizeResourceArrayIndexingForEssl passes: - // it takes the module and nothing else, no capability bit arms it, and it self-gates - // on the module's own content (BinaryHasOffsetOrGrad1DSampledImage). The module is - // already the largest thing in the L2 key, so it is covered completely. - Vector sampled1DSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DSampledImagesForEssl( - *effectiveSpirv, sampled1DSpirv, enableSpirvValidation) && - !sampled1DSpirv.empty()) { - effectiveSpirv = &sampled1DSpirv; - } - - // GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop - // GLSL 4.2 and an Adreno ES compile error ("all images have to define layout - // format"), which loses the whole program. Give each such image the format the - // application bound to its unit - the one GL's format-class rules make correct - - // so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which - // also rewrites image types, so this one is looking at the final shapes. - // - // Gated on the module actually declaring one: the map is empty for every program - // whose images all declare formats, and the cheap probe keeps a program that has - // an unbound format-less image from paying an optimizer round trip per stage. - Vector imageFormatSpirv; - if (!imageFormatBake.glFormatByUniformName.empty() && - MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) && - MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl( - *effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv, - enableSpirvValidation) && - !imageFormatSpirv.empty()) { - effectiveSpirv = &imageFormatSpirv; - } - - // GL has forty image formats and GLSL ES core has thirteen; the other twenty-seven - // reach ES only through GL_NV_image_formats, which no tested driver advertises. A - // shader declaring one of them has NO legal ESSL spelling at all - SPIRV-Cross throws - // for some of them and the driver rejects the token for the rest ("'rg32f' : not a - // legal layout qualifier id"), and dropping the qualifier is refused too ("all images - // have to define layout format") - so the stage is lost and every draw with the - // program silently renders nothing. Seventeen of them widen EXACTLY into a core format - // of the same per-channel width, and this rewrites those declarations to the carrier - // and masks every access back to the channels GL says the format has. The other nine - // have no exact carrier and keep the honest diagnostic - // CollectImageFormatBakeInputs emits. - // - // A driver that HAS GL_NV_image_formats still needs part of this. SPIRV-Cross throws - // for its is_desktop_only_format set when it targets ESSL rather than printing a - // token, and the throw loses the stage however willing the driver was - llvmpipe - // advertises the extension and `layout(r8ui) uimage2D` lost its whole program there - // until the widening ran for it too. So the driver bit decides HOW MUCH is widened, - // never WHETHER. - // - // AFTER the bake above, deliberately: a format-less image whose unit holds a non-core - // format is baked with that format and widened here, so both routes end in the same - // place and there is no second widening rule for baked declarations. - // - // KEY MATERIAL: g_GLESCapabilities.SupportsExtendedImageFormats, which selects the - // mode - see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes - // no other input: what it rewrites is a pure function of the module's own declared - // formats and that mode, and the module is already the largest thing in the L2 key. - // The ARMING flag is deliberately NOT key material: it only decides whether the pass - // runs, and the module below is adopted only when the pass actually changed the bytes - // - so a program-wide flag that over-arms a stage costs an optimizer round trip and - // changes no output. - // - // DirectVulkan is deliberately not given this: it takes the declared format natively - // and resolves the descriptor's view format from the same bind state. - Vector widenedImageFormatSpirv; - if (imageFormatBake.declaresWidenableImageFormat && - MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl( - *effectiveSpirv, widenedImageFormatSpirv, widenOnlyUnprintableImageFormats, - enableSpirvValidation) && - !widenedImageFormatSpirv.empty() && widenedImageFormatSpirv != *effectiveSpirv) { - effectiveSpirv = &widenedImageFormatSpirv; - } - - // GLSL ES demands a constant integral expression to index a fragment output - // array; SPIR-V does not, so a shader that writes coeff[i] from a loop - // reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects - // outright ("array indexes for fragment outputs must be constant integral - // expressions"), linking no program and silently no-oping every draw that - // uses it. llvmpipe accepts it, ANGLE does not - which is the whole of the - // improved-transparency-minecraft-26.3 failure. Fold or lower the index here, - // on the ESSL path only: the same module is legal for DirectVulkan. - Vector outputIndexSpirv; - if (glShaderType == GL_FRAGMENT_SHADER && - MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl( - *effectiveSpirv, outputIndexSpirv, enableSpirvValidation) && - !outputIndexSpirv.empty()) { - effectiveSpirv = &outputIndexSpirv; - } - - // Same rule, two more resources, every stage: desktop GL lets an array of - // storage blocks and an array of image uniforms be indexed with any - // dynamically-uniform expression, GLSL ES keeps the ES 3.1 - // constant-expression rule for both, and the drivers enforce it - Qualcomm - // ("indexing into an SSBO array using a non-constant expression is not - // permitted"), llvmpipe ("image arrays indexed with non-constant expressions are - // forbidden in GLSL ES") - losing the stage, the program, and every draw or - // dispatch that used it, while the frontend keeps reporting the link glslang - // performed. Fold or lower the index here, on the ESSL path only: the same - // module is legal for DirectVulkan, which binds the array as one descriptor - // array. - // - // The image half is also what makes RemapImageArrayElementUnits below possible - // at all: that pass rewrites `g_image[k]` into a per-element declaration, and it - // can only do that once every k the emitted ESSL spells is a literal. - // - // NO KEY MATERIAL, and that is a conclusion rather than an omission: this takes the - // module and nothing else - no capability bit arms it, no per-program plan steers - // it - and it self-gates on the module's own content - // (BinaryHasDynamicResourceArrayIndexing). The module is already the largest - // thing in the L2 key, so it is fully covered. Contrast LowerViewportIndexForEssl, - // whose signature is equally module-only but which SupportsViewportArray ARMS - - // that bit is in the key precisely because of it. - Vector blockArrayIndexSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeResourceArrayIndexingForEssl( - *effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) && - !blockArrayIndexSpirv.empty()) { - effectiveSpirv = &blockArrayIndexSpirv; - } - - // glslang kept the application's layout(offset = N) on the atomic counters it - // lowered onto gl_AtomicCounterBlock_, and no std140/std430 layout can put - // member 0 anywhere but offset 0 - so SPIRV-Cross throws ("cannot be expressed as - // neither std430 nor std140") and the stage never reaches the driver. Collapse the - // block into one uint array at offset 0 and re-index each counter to the element - // that used to be at its byte offset; the buffer then stays bound whole, which it - // has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 64 on Adreno 830 and 32 or - // more everywhere else, so an 8-byte bind offset is not expressible on any of them). - // BEFORE SetAtomicCounterBlockBindings - // below, which only moves the block's BINDING and needs the block intact. - // - // NO KEY MATERIAL either, for the same reason - and note where the application's - // layout(offset = N) values live: glslang already baked them into the module as - // member Offset decorations, so they are in the key as MODULE BYTES. There is no - // separate offset input to carry. - Vector atomicCounterSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl( - *effectiveSpirv, atomicCounterSpirv, enableSpirvValidation) && - !atomicCounterSpirv.empty()) { - effectiveSpirv = &atomicCounterSpirv; - } - - // The second half of the inter-stage interface-block repair, and the one that - // actually closes the 420pack group: this driver drops the payload of a block that - // carries an explicit layout(location=) whenever a tessellation or geometry stage - // is in the pipeline, so the qualifier comes off and ES matches the block by name - // and member sequence instead. The names those two sides agree on are the ones the - // rename above just fixed, which is why this runs AFTER it and not before. - // - // The caller arms the two directions; both are false unless the driver POST - // measured the defect AND this program has a stage that can hit it. Adopted only - // when this stage really had a located block, for the reason the array-input split - // documents: the optimizer hands back a re-serialised copy either way. - // - // LAST IN THE CHAIN, and that position is load-bearing. Vulkan SPIR-V REQUIRES a - // Location on every user-defined Input/Output variable - // ([VUID-StandaloneSpirv-Location-04915]), so the module this produces is - // deliberately no longer valid Vulkan SPIR-V - it is an ESSL-emission intermediate - // that goes straight into SPIRV-Cross and reaches no driver as SPIR-V. Running it - // here means no later pass validates what it produced; the pass itself skips - // validation for the same reason (see StripIoBlockLocationsForEssl). Anywhere - // earlier and every remaining pass would latch a validation failure on a module - // that is doing exactly what it was asked to. - Vector strippedIoBlockLocationSpirv; - if (stripInputBlockLocations || stripOutputBlockLocations) { - Bool strippedAny = false; - if (MG_Util::ShaderTranspiler::ShaderCompiler::StripIoBlockLocationsForEssl( - *effectiveSpirv, stripInputBlockLocations, stripOutputBlockLocations, - strippedAny, strippedIoBlockLocationSpirv, enableSpirvValidation) && - !strippedIoBlockLocationSpirv.empty() && strippedAny) { - effectiveSpirv = &strippedIoBlockLocationSpirv; - // THE ARMING SIGNAL, and it is INFO on purpose: the per-stage line below is - // MGLOG_D, which is compiled out of every build CI and the device runs, so - // nothing outside a debug build could tell an armed repair from a silently - // un-armed one. Latched, so it costs one line per process rather than one - // per stage of every program. The integration lane that pins the emulation - // on asserts on exactly this line - see UnlocatedIoBlockScenario. - MGLOG_I_ONCE("DirectGLES is emitting inter-stage interface blocks WITHOUT their " - "layout(location) qualifier, because this driver loses a located " - "block's payload across a tessellation or geometry boundary."); - MGLOG_D("Program %u stage %s: interface-block location qualifiers dropped " - "(%s), because this driver loses a located block's payload across a " - "tessellation or geometry boundary.", - m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(), - stripInputBlockLocations - ? (stripOutputBlockLocations ? "consumed and produced" : "consumed") - : "produced"); - } - } - - MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, - MG_Util::ShaderTranspiler::SessionUsageBit::Transpile); - - spvc_compiler_options options; - spvcSession.CreateOptions(&options); - - spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, - ResolveBackendEsslVersion()); - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); - - spvcSession.SetOptions(options); - - // ES fixes a storage block's binding at link from its layout(binding=) qualifier - // and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding - // can only be honoured by printing it INTO the qualifier. Rewriting the Binding - // decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding - // then deliberately preserves the qualifier for `buffer` declarations. - if (!storageBlockBindingOverrides.empty()) { // empty for almost every program - 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. - // - // BOTH HALVES ARE MEMO STATE. `atomicCounterEsslBindingTop` decides the binding - // this prints into the ESSL, so it is in the L2 key; `outAtomicCounterGlBindings` - // is an OUTPUT this stage produces and the draw path consumes, so it is in the L2 - // payload. A hit that replayed only the text would leave the bindings empty and - // every counter buffer unbound - the same class of silent loss the flattened XFB - // block names would have been. - spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop, - outAtomicCounterGlBindings); - - // `layout(index = 0)` is the GL default spelled out loud, and GLSL ES has no such - // qualifier in core - a stage that prints it is refused with "index layout - // qualifier requires EXT_blend_func_extended" and the whole program then draws - // nothing. Drop the decoration when it carries the default; a REAL dual-source - // index (1) is left alone, because that one genuinely needs the extension and the - // driver has to see it. Fragment stage only: no other stage can carry it. - if (glShaderType == GL_FRAGMENT_SHADER) { - spvcSession.DropDefaultFragmentOutputColorIndex(); - } - - // `readonly writeonly` together says the buffer variable can only be asked its - // .length(), which the frontend has already enforced - so the pair is inert, and - // printing it is not. llvmpipe's ES compiler refuses a block spelled that way and the - // stage never reaches the program. - spvcSession.RelaxReadWriteExclusiveStorageBuffers(); - - // gl_ShadingRateEXT / gl_PrimitiveShadingRateEXT: SPIRV-Cross spells them only for - // Vulkan GLSL, so they go through under a sentinel name and are restored below. Only - // a program whose stages use them gets here with anything to do, and only a driver - // with GL_EXT_fragment_shading_rate gets such a program past the frontend. - const Bool restoreShadingRateBuiltins = spvcSession.PrepareShadingRateBuiltinsForEssl(); - - const char* result = nullptr; - spvcSession.Compile(&result); - - if (!result) { - // The caller owns the diagnostic: it is the one that knows the - // frontend program id, and a failed transpile must NOT be memoized - - // the message names the stage and is worth re-emitting every time. - const char* lastError = spvcSession.GetLastErrorString(); - outError = lastError ? lastError : ""; - return false; - } - - outSource = result; - if (restoreShadingRateBuiltins) { - MG_Util::ShaderTranspiler::SpvcSession::FinishShadingRateBuiltinsForEssl(outSource); - } - return true; - } - - // GL 4.6 core 11.2.2 lets a program have a tessellation EVALUATION shader and no CONTROL - // shader: the input patch is passed through unmodified and the levels come from the - // PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. OpenGL ES 3.2 has no such - // state and no such allowance - it rejects the program at link, and with an EMPTY info - // log, which was verified on an Adreno 830 with no MobileGL in the process (TES-only: - // link=0, log empty; the same shaders plus any TCS: link=1, with or without the SSBO and - // atomic counter the failing conformance case also declares). The frontend's own glslang - // link succeeds, so GL_LINK_STATUS reads TRUE, program 0 is bound in its place, and every - // draw silently renders nothing - a black framebuffer, an atomic counter still at 0 and - // an untouched SSBO, with no error anywhere. - // - // So the missing stage is synthesized and attached here, alongside the program's own. - // DirectVulkan already does exactly this for the same structural reason - // (ProgramFactory::BuildPassthroughTessControlSource), so this completes the pair rather - // than inventing an approach. - // - // Nothing that works today can be harmed by it: it fires ONLY for a program that has an - // evaluation stage and no control stage, and every such program fails to link on ES right - // now. The worst case is that the synthesized stage fails to compile or link, which leaves - // the program exactly as dead as it already was - but with a driver log that says why, - // where today there is an empty one. - void BackendProgramObjectImpl::AttachPassthroughTessControlStage( - const ProgramBuildSource& src, const Int tessEvalShaderIndex, - const Vector>& shaderSpirvs, const String& vertexStageEssl, - const String& tessEvalStageEssl) { - // PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output - // patch size - so a program built for one value is stale for another. Recorded here - // and compared on the draw path (SyncCurrentProgram), the same shape as the - // storage-block and image-format signatures next to it. - const Uint patchVertices = MG_Pipe::gPipeInputs->IsLive() - ? MG_Pipe::gPipeInputs->GetPatchVertices() - : 3u; - m_passthroughTessControlPatchVertices = static_cast(patchVertices); - // PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked - // into the same stage (ES has no such state and no entry point to forward them to), so - // they are recorded and compared alongside the patch size - the two move together, as - // BuildPassthroughTessControlEssl's contract says. - m_passthroughTessControlOuterLevel = MG_Pipe::gPipeInputs->IsLive() - ? MG_Pipe::gPipeInputs->GetPatchDefaultOuterLevel() - : FloatVec4(1.0f, 1.0f, 1.0f, 1.0f); - m_passthroughTessControlInnerLevel = MG_Pipe::gPipeInputs->IsLive() - ? MG_Pipe::gPipeInputs->GetPatchDefaultInnerLevel() - : FloatVec2(1.0f, 1.0f); - - if (tessEvalShaderIndex < 0 || - static_cast(tessEvalShaderIndex) >= shaderSpirvs.size()) { - MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no " - "SPIR-V for it; the pass-through control stage GL describes cannot be checked, so " - "the program is left to fail its ES link.", - src.GetExternalIndex()); - m_backendProgramUsable = false; - return; - } - - // The one shape the pass-through cannot stand in for. It forwards gl_Position and - // nothing else, so an evaluation stage that reads a user-defined varying or a - // per-patch input - both of which carry a Location, where every built-in it needs - // does not - would start reading undefined values the moment a control stage sat - // between it and the vertex stage. Declining keeps that from being silent; it is the - // identical rule DirectVulkan applies in ReflectPassthroughTessControlNeed. - if (MG_Util::ShaderTranspiler::ShaderCompiler::ModuleReadsLocatedInput( - shaderSpirvs[static_cast(tessEvalShaderIndex)])) { - MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a " - "user-defined input through it; a synthesized pass-through control stage cannot " - "forward that, so the program is declined rather than fed an undefined varying.", - src.GetExternalIndex()); - m_backendProgramUsable = false; - return; - } - - // Mirrored from the neighbours rather than fixed: whether SPIRV-Cross redeclares - // gl_PerVertex, and with which members, depends on what the application's shaders - // touched, and a synthesized stage that redeclares a DIFFERENT shape than the stage - // it feeds is an ES link error against a program with no other problem. gl_in copies - // the vertex stage's OUT block (that is what arrives) and gl_out the evaluation - // stage's IN block (that is what is expected). A neighbour that redeclared nothing - // yields an empty list, which leaves the driver's own built-in declaration in place - - // which is exactly what matching it requires. - const String inMembers = - ExtractPerVertexBlockMembers(vertexStageEssl, /*input=*/false).value_or(String()); - const String outMembers = - ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String()); - - String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, - inMembers, outMembers, - m_passthroughTessControlOuterLevel, - m_passthroughTessControlInnerLevel); - // The mirrored member lists can carry gl_PointSize - the neighbour stage declared it, - // so matching it is the whole point - and a redeclaration is exactly as illegal as a - // reference in ESSL without the extension. Same directive, same never-speculative - // rule as the per-stage loop; a driver with neither spelling gets nothing added and - // fails below with its own message, which is the honest outcome for a shape it - // cannot express. - const char* passthroughPointSizeExtension = - source.find("gl_PointSize") != String::npos - ? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true) - : nullptr; - source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension); - // Same for gl_ClipDistance/gl_CullDistance mirrored out of a vertex stage that - // writes them: the neighbour could only compile with the extension, so it exists. - source = RequestClipCullDistanceExtension(Move(source)); - - const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER); - if (backendShaderId == 0) { - MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for " - "program %u.", - src.GetExternalIndex()); - m_backendProgramUsable = false; - return; - } - - const char* sourceCStr = source.c_str(); - MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices " - "%u):\n%s", - src.GetExternalIndex(), patchVertices, sourceCStr); - g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr); - g_GLESFuncs.glCompileShader(backendShaderId); - - // GL_FALSE, not GL_TRUE, for the reason the per-stage loop states: an unwritten - // out-param must read as "compile failed" and never as a silent success. - GLint compileStatus = GL_FALSE; - g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus); - if (compileStatus == GL_FALSE) { - GLint logLength = 0; - g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength); - if (logLength < 0) logLength = 0; - Vector log(static_cast(logLength) + 1, '\0'); - g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data()); - log.back() = '\0'; - MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for " - "program %u. Driver log: %s\nSource:\n%s", - src.GetExternalIndex(), log.data(), sourceCStr); - m_backendProgramUsable = false; - g_GLESFuncs.glDeleteShader(backendShaderId); - return; - } - - g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId); - // Same ownership handover as every other stage: glDeleteShader only FLAGS, so this is - // what makes the program own it and what keeps a relink from leaking it. - g_GLESFuncs.glDeleteShader(backendShaderId); - } - - // ---- THE PROGRAM BUILD, AND THE ONE SOURCE IT READS (P5e pg) ---------------------- - // - // ONE BODY, TWO HEADS, AND THE PULL BUILD'S HEAD IS THE ORIGINAL ONE. In the pull build - // `ProgramBuildSource` IS `MG_State::GLState::ProgramObject`, this function keeps its - // name, its signature and its null guard, and `src` is a reference to the object it was - // already dereferencing on every line - so the pull build compiles the same reads - // through the same accessors and G1's byte identity survives a rename and nothing else. - // - // In a PUSH build the body is a private worker that the two public overloads feed: - // SyncToBackend(program) - the monolith-glue half, ruling 1 - wraps the frontend's own - // archive, and SyncToBackendByHandle(cso) wraps the RECORD's. That is the whole of this - // family's rule F: after this, nothing in a program build dereferences a frontend - // object, and the forty accessor calls below read memory the server owns. - void BackendProgramObjectImpl::SyncToBackendFromSource(const ProgramBuildSource& src) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // Recorded before either early return below, so Use() can always name the GL - // program a no-op draw belongs to - including the "linked but not drawable" exit. - m_frontendProgramId = src.GetExternalIndex(); - - // GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was - // cancelled (teardown) or whose optimizer run failed is fully linked and fully - // queryable, but has no SPIR-V to build a driver program out of. GL cannot retract - // a LINK_STATUS it already reported true, so "linked but not drawable" is the - // answer, and this is where the ES backend expresses it. - if (!src.GetLinkStatus() || !src.GetSpirvStatus()) { - MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State " - "program ID: %u", - src.GetExternalIndex()); - return; - } - - MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u", - src.GetExternalIndex(), m_backendProgramId); - // Every link-derived cache below (incl. m_samplerUniformBindings and its - // lastAssignedUnit/lastAssignedLodBias program-state mirrors) is rebuilt; - // the sampler-pass memo keyed on them must not survive. - m_samplerPassMemo.valid = false; - m_backendProgramUsable = true; - m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask; - m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask; - m_fragColorBroadcastCount = g_fragColorBroadcastCount; - // The generated ESSL bakes these in (see the SetShaderStorageBlockBinding call in the - // transpile loop below), so the set they were generated against is part of what makes - // this build current - the draw path compares the signature and rebuilds on a change. - const auto& storageBlockBindingOverrides = src.GetShaderStorageBlockBindingOverrides(); - m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(src); - // 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(); - // Re-established by AttachPassthroughTessControlStage below when this program needs - // one; cleared first so a program that stops needing one (a relink that now attaches - // a real control stage) does not keep comparing against a stale patch size. The - // default levels are re-established from the same call and gated on the same -1. - m_passthroughTessControlPatchVertices = -1; - m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f); - m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f); - // 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). - // Taken BEFORE the transpile loop so both the bake and the key see one snapshot. - const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(src); - m_formatlessImageUnits = imageFormatBake.units; - m_imageUnitFormatSignature = imageFormatBake.signature; - for (const auto& conflicted : imageFormatBake.conflictedNames) { - MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with " - "different bound formats; left format-less.", - conflicted.c_str(), src.GetExternalIndex()); - } - // ...and once more for image ARRAYS whose per-element units are not consecutive, which - // ESSL has no way to express in one declaration. Program-wide, like the bake, and read - // from the same snapshot of the reflection; the per-stage rewrite happens below. - const Vector nonConsecutiveImageArrays = - CollectNonConsecutiveImageArrayPlans(src); - - // Detach all existing shaders - GLint attachedCount = 0; - g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_ATTACHED_SHADERS, &attachedCount); - MGLOG_D("Currently attached shaders count: %d", attachedCount); - - if (attachedCount > 0) { - Vector attachedShaders(attachedCount); - // Every GL out-param in this function is pre-initialized and every count is - // re-clamped after the query. A driver that returns without writing the - // out-param (no current context, a lost context, a stubbed entry point) would - // otherwise leak an uninitialized stack value straight into a container size - // or a loop bound - which is exactly how this path used to throw - // length_error out of a Vector fill-ctor. - GLsizei actualCount = 0; - g_GLESFuncs.glGetAttachedShaders(m_backendProgramId, attachedCount, &actualCount, - attachedShaders.data()); - actualCount = std::clamp(actualCount, 0, static_cast(attachedShaders.size())); - MGLOG_D("Detaching %d existing shaders from program %u", actualCount, m_backendProgramId); - - for (GLsizei i = 0; i < actualCount; ++i) { - MGLOG_D("Detaching shader ID: %u from program %u", attachedShaders[i], m_backendProgramId); - g_GLESFuncs.glDetachShader(m_backendProgramId, attachedShaders[i]); - } - } - - // Attach current shaders. - // - // The EXECUTABLE's stage list, not GetAttachedShaders(): this loop indexes - // shaderSpirvs by the same running index, and the generated SPIR-V is a link - // artifact while the attach list is live. GL 4.6 core 7.3 makes glAttachShader take - // effect only at the next link, so a program that is attached to after it linked has - // MORE entries in the attach list than there are modules - and pairing the two read - // straight off the end of shaderSpirvs (a std::vector copy from garbage, which is - // how this crashed). GetLinkedShaderStages() is the list the modules were generated - // from, one entry per module, in module order. - const Vector linkedStages = src.GetLinkedShaderStages(); - auto& shaderSpirvs = src.GetGeneratedSpirv(); - // Both come from the same Link(), so they agree by construction. If they ever did - // not there would be no index this function could safely use for EITHER array, so - // this refuses the build instead of picking one and hoping. - if (linkedStages.size() != shaderSpirvs.size()) { - MGLOG_E_ONCE("Program %u: %zu linked stage(s) but %zu generated SPIR-V module(s); refusing to " - "build a backend program from mismatched link artifacts.", - src.GetExternalIndex(), linkedStages.size(), shaderSpirvs.size()); - m_backendProgramUsable = false; - return; - } - if (src.PointSizeDemoted()) { - // THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that - // pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because - // every rendering assertion stays green on a healthy driver whether the - // demotion ran or was silently disarmed. See PointSizeDemotionScenario. - MGLOG_I_ONCE("DirectGLES is building programs whose tessellation/geometry gl_PointSize was " - "demoted to an ordinary varying, because this driver cannot host the built-in " - "in those stages."); - } - MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId); - for (const auto& ref : src.GetLinkedShaderSnapshot()) { - if (!ref.shader) continue; - const auto& stage = - MG_Util::ConvertGLEnumToString(MG_Util::ConvertShaderStageToGLEnum(ref.shader->GetShaderStage())); - // The source THIS link consumed, which a later glShaderSource does not disturb. - const String& src = ref.source ? *ref.source : ref.shader->GetShaderSource(); - MGLOG_D("Original src @ %s: \n", stage.c_str()); - MGLOG_D("%s:", src.empty() ? "" : src.c_str()); - } - const Bool enableSpirvValidation = src.GetSpirvValidationEnabled(); - - // Blocks a transform-feedback capture request names a member of ("StageData" of - // "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and - // then captures nothing at all for it, so those blocks - and ONLY those - get - // flattened into per-member variables below, in EVERY stage, so a producer and its - // consumer keep matching. gl_PerVertex members ("gl_Position") carry no block - // prefix and so never enter this set. - std::set xfbCaptureBlockNames; - for (const auto& xfbVarying : src.GetTransformFeedbackVaryings()) { - const SizeT dot = xfbVarying.name.find('.'); - if (dot != String::npos && dot > 0) { - xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot)); - } - } - std::set flattenedXfbBlockNames; - // Stages whose ESSL had a read+write image declaration doubled into a coherent - // read/write pair, and by how many. Empty for every program but a handful; consulted - // ONLY when the link then fails, because the doubling spends the driver's per-stage - // GL_MAX_*_IMAGE_UNIFORMS budget that MobileGL keeps advertising unadjusted (halving - // the advertised value would fail basic-api and NotSupported-out cases that never pay - // the doubling, so the limit must stay honest and the connection has to be made here - // instead). See the budget note on SplitReadWriteImageUniforms. - struct SplitImageUniformStage { - ShaderStage stage; - Uint splitCount; - }; - Vector splitImageUniformStages; - - // Desktop GLSL keeps SEPARATE name namespaces for input and output interface - // blocks, so ONE stage may legally declare `in FOO {...}` and `out FOO {...}` at - // the same time - which the tessellation evaluation stage of both interface-block - // tests in KHR-GL42/43.shading_language_420pack does ("in TCSOutputBlock ... out - // TCSOutputBlock"). SPIRV-Cross keeps the same split (block_input_names vs - // block_output_names) and re-emits BOTH under the name FOO, so the generated ESSL - // declares two different blocks called FOO in one shader. Adreno's ES compiler - // keeps them apart; Mali's does not - the stage compiles, the program links, and - // the output block's payload never reaches the next stage. All 22 of that group's - // Mali failures are exactly the two tests that write this shape, and every one of - // them passes on Adreno and on DirectVulkan. - // - // The repair is a rename keyed on the PRODUCING stage, planned here and applied - // per stage below so a producer and its consumer keep naming the same block. - // Gated twice over, because a re-serialised module is not free (it cost the - // create-indirect retrace 0.15 SSIM the first time the array-input split missed - // its gate; the case is now - // minecraft-1.21.1-neoforge-create-indirect-in-world-align1024): only a tessellation - // or geometry stage can declare blocks in both directions at all, and even then the - // probe has to FIND a collision before any - // stage is rewritten. - std::set collidingIoBlockNames; - std::set declaredIoBlockNames; - Vector stagePipelineIndices(linkedStages.size(), -1); - Bool anyStageCanDeclareBlocksInBothDirections = false; - for (SizeT index = 0; index < linkedStages.size(); ++index) { - const ShaderStage stage = linkedStages[index]; - stagePipelineIndices[index] = InterStagePipelineIndex(stage); - if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true; - } - // A SECOND, INDEPENDENT interface-block repair riding the same gate, because it - // needs the same question answered: "does this program have a stage where an - // inter-stage block can go wrong?". CanDeclareBlocksInBothDirections is true for - // exactly the tessellation and geometry stages, which is also exactly the set of - // stages whose presence makes this driver drop a LOCATED block's payload (a - // vertex-to-fragment located block is fine on the same driver, measured). The two - // repairs are otherwise unrelated: the rename fixes a name collision inside ONE - // stage, this drops a qualifier from EVERY block of the program - so it does not - // wait for the collision probe to find anything. - const Bool ioBlockLocationStripArmed = - !g_GLESCapabilities.SupportsLocatedInterStageIoBlocks && - anyStageCanDeclareBlocksInBothDirections; - if (anyStageCanDeclareBlocksInBothDirections) { - for (SizeT index = 0; index < shaderSpirvs.size(); ++index) { - MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl( - shaderSpirvs[index], collidingIoBlockNames, declaredIoBlockNames); - } - // A block a capture request names is resolved BY NAME at - // glTransformFeedbackVaryings time - and flattened away entirely by the pass - // below - so renaming one would ask the driver for a block the request does - // not spell. - for (const auto& xfbCaptureBlockName : xfbCaptureBlockNames) { - collidingIoBlockNames.erase(xfbCaptureBlockName); - } - } - // The one spelling every stage of THIS program agrees on for `blockName` as written - // by pipeline stage `producerPipelineIndex`. "__" is reserved in GLSL, so a name - // already ending in '_' does not get another one, and the digit-suffix loop steps - // off any name the program already spells. - const auto uniqueIoBlockName = [&declaredIoBlockNames](const String& blockName, - Int producerPipelineIndex) { - const char* separator = (!blockName.empty() && blockName.back() == '_') ? "" : "_"; - String candidate = blockName + separator + "mgio" + std::to_string(producerPipelineIndex); - while (declaredIoBlockNames.find(candidate) != declaredIoBlockNames.end()) { - candidate += "0"; - } - return candidate; - }; - - // Desktop GL makes the tessellation CONTROL stage optional; OpenGL ES 3.2 rejects a - // program that has an evaluation stage without one, with an empty info log. When that - // is this program's shape, one is synthesized below - and it has to be spelled to - // MATCH the two stages it sits between, so their emitted ESSL is kept here as it is - // produced. Empty for every program that has a control stage of its own, which is - // all but a handful. - Bool hasTessEvalStage = false; - Bool hasTessControlStage = false; - Int tessEvalShaderIndex = -1; - String vertexStageEssl; - String tessEvalStageEssl; - // - // Asked of the executable for the same reason the loop below indexes it: a - // tessellation evaluation shader merely ATTACHED to a linked vertex+fragment program - // is not part of what this program runs, and synthesizing a control stage for it - // would build a tessellating driver program for an executable that does not - // tessellate (and would take tessEvalShaderIndex past the end of shaderSpirvs). - for (SizeT index = 0; index < linkedStages.size(); ++index) { - const ShaderStage stage = linkedStages[index]; - if (stage == ShaderStage::TessControl) hasTessControlStage = true; - if (stage == ShaderStage::TessEval) { - hasTessEvalStage = true; - tessEvalShaderIndex = static_cast(index); - } - } - const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage; - - // The stage order the loop below walks, with every FRAGMENT stage moved to the end. - // The viewport-routing gate is the reason: whether a fragment stage needs one is a - // question about the OTHER stages ("does any of them still write gl_ViewportIndex?"), - // and the honest, free answer to it is the promotion the producing stage's own text - // pass just performed. Answering it any other way costs a BuildModule per - // pre-rasterization stage of every program - the parse the shared SpirvGateFeatures - // probe exists to avoid. Nothing else in the loop is order-sensitive: the two - // passthrough-tessellation sources it captures are a vertex and an evaluation stage, - // and the three sets it accumulates are unions. - // - // The pre-rasterization stages are walked in PIPELINE order (the stage enum's), so a - // consumer's input-block redeclaration below can mirror the output block its producer - // has already emitted. - Vector stageOrder; - stageOrder.reserve(linkedStages.size()); - for (SizeT index = 0; index < linkedStages.size(); ++index) { - if (linkedStages[index] != ShaderStage::Fragment) stageOrder.push_back(index); - } - std::stable_sort(stageOrder.begin(), stageOrder.end(), [&linkedStages](SizeT a, SizeT b) { - return static_cast(linkedStages[a]) < static_cast(linkedStages[b]); - }); - for (SizeT index = 0; index < linkedStages.size(); ++index) { - if (linkedStages[index] == ShaderStage::Fragment) stageOrder.push_back(index); - } - // The output gl_PerVertex members the last pre-rasterization stage emitted, for the - // next stage's input redeclaration (RedeclareClipCullDistanceInputsInPerVertexBlock). - // A synthesized pass-through control stage copies its input block through, so the - // vertex stage's members are what an evaluation stage behind one sees. - String producerOutPerVertexMembers; - // Set by whichever pre-rasterization stage's demoted mg_ViewportIndex global the text - // pass turned into a varying; read by the fragment stage to decide whether to inject - // the gate that consumes it. - Bool programRoutesViewportIndex = false; - // No fragment stage, no gate - and without a gate the promotion below would only add - // an output nothing can read. That is not merely useless: in a separable program - // pipeline the fragment stage lives in a DIFFERENT program, which never saw this - // build and cannot be given a gate, so promoting there would hang an unmatched - // varying off a program to buy nothing. Both cases keep the pre-emulation behaviour, - // which is what a program with no fragment stage had anyway. - const Bool programHasFragmentStage = - std::find(linkedStages.begin(), linkedStages.end(), ShaderStage::Fragment) != - linkedStages.end(); - const Bool viewportEmulationForThisProgram = - ViewportArrayEmulationEnabled() && programHasFragmentStage; - - for (const SizeT index : stageOrder) { - GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(linkedStages[index]); - GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType); - - if (backendShaderId == 0) { - MGLOG_E_ONCE("Failed to create backend shader for attachment."); - continue; - } - String source; - auto& spirvCode = shaderSpirvs[index]; - - // A samplerBuffer is core in the OpenGL 3.1+ context MobileGL advertises but needs - // ES 3.2 or EXT/OES_texture_buffer on the host. Without it SPIRV-Cross emits - // `#extension GL_EXT_texture_buffer : require` and the driver rejects both that - // and the isamplerBuffer keyword - the program never links and every draw using it - // becomes a silent no-op. Say so here, naming the stage. Deliberately unlatched: - // this is bounded by program count, and which stage failed is the whole point. - // Gated on the capability so the module walk never runs on a healthy driver. - if (!AreBufferTexturesSupported() && - MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirvCode)) { - MGLOG_E("Program %u stage %s samples a buffer texture, which this ES driver " - "cannot provide (%s). The shader will not compile and the program will " - "not link; every draw using it is a no-op.", - m_backendProgramId, - MG_Util::ConvertGLEnumToString(glShaderType).c_str(), - GetBufferTextureTierName()); - m_backendProgramUsable = false; - g_GLESFuncs.glDeleteShader(backendShaderId); - continue; - } - - // ---- L2 of the shader translation memo ------------------------------- - // The whole DirectGLES SPIR-V pass chain plus SPIRV-Cross for this stage, - // memoized on the module bytes and on every capability bit and per-program - // input that steers them. See TranslationCache.h for the key inventory and - // for why the text-level passes below stay outside the boundary. - MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs; - esslKeyInputs.spirv = &spirvCode; - esslKeyInputs.shaderType = glShaderType; - // The EFFECTIVE arming, computed the same way TranspileSpirvToEssl computes it. - // Duplicated rather than shared because the two live on opposite sides of the - // memo boundary - and a key that disagrees with the pass it is keying is the one - // failure mode of this cache that renders wrong pixels instead of being slow. - esslKeyInputs.viewportIndexLoweringArmed = - (glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER || - glShaderType == GL_GEOMETRY_SHADER) && - (ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray); - esslKeyInputs.deferGeometryEmitsArmed = - glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit; - esslKeyInputs.subgroupEmulationArmed = - glShaderType == GL_COMPUTE_SHADER && - ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated; - esslKeyInputs.subgroupEmulationScratchBytes = - esslKeyInputs.subgroupEmulationArmed ? g_GLESCapabilities.MaxComputeSharedMemorySize : 0; - esslKeyInputs.iterationRPBarrierFixArmed = - esslKeyInputs.subgroupEmulationArmed && ShouldApplyIterationRPFixes(); - esslKeyInputs.subgroupScratchAtomicReads = - esslKeyInputs.subgroupEmulationArmed && g_GLESCapabilities.SharedMemoryLoadsGoStale; - esslKeyInputs.supportsNoperspectiveInterpolation = - g_GLESCapabilities.SupportsNoperspectiveInterpolation; - esslKeyInputs.supportsExtendedImageFormats = - g_GLESCapabilities.SupportsExtendedImageFormats; - esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples; - esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples; - esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples; - esslKeyInputs.advertisedMaxSamples = - std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); - esslKeyInputs.xfbCaptureBlockNames = &xfbCaptureBlockNames; - esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName; - esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides; - esslKeyInputs.esslVersion = ResolveBackendEsslVersion(); - esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop; - - // THIS STAGE's share of the program-wide interface-block rename plan built above - // the loop. Resolved here, outside the memoized segment, because it is planning - // and not translation - exactly like the image-format bake map - and because that - // makes the two maps a plain function argument the L2 key can carry. - // - // KEYING ON THE RESOLVED MAPS rather than on what they were derived from - // (collidingIoBlockNames, declaredIoBlockNames, stagePipelineIndices, this - // stage's index) is deliberate: the maps ARE the pass's arguments, so they are - // exactly as fine as the pass's behaviour and no finer. Two programs whose - // collision plans differ but whose maps for THIS stage come out identical really - // do produce the same ESSL and should share the entry. - // - // A block whose other end is NOT in this program is deliberately left out of the - // plan: in a separate-shader-objects pipeline the interface it matches across - // lives in another program that never saw this plan, and renaming one side of - // THAT would break a program pipeline to repair a driver quirk. That is what the - // producer/consumer presence tests below are for - in a monolithic program both - // are trivially satisfied for every interface the collision can touch. - std::map inputBlockRenames; - std::map outputBlockRenames; - if (!collidingIoBlockNames.empty() && stagePipelineIndices[index] >= 0) { - const Int myPipelineIndex = stagePipelineIndices[index]; - Int producerPipelineIndex = -1; - Bool hasConsumerStage = false; - for (const Int otherPipelineIndex : stagePipelineIndices) { - if (otherPipelineIndex < 0) continue; - if (otherPipelineIndex < myPipelineIndex && - otherPipelineIndex > producerPipelineIndex) { - producerPipelineIndex = otherPipelineIndex; - } - if (otherPipelineIndex > myPipelineIndex) hasConsumerStage = true; - } - for (const auto& collidingBlockName : collidingIoBlockNames) { - if (producerPipelineIndex >= 0) { - inputBlockRenames[collidingBlockName] = - uniqueIoBlockName(collidingBlockName, producerPipelineIndex); - } - if (hasConsumerStage) { - outputBlockRenames[collidingBlockName] = - uniqueIoBlockName(collidingBlockName, myPipelineIndex); - } - } - } - esslKeyInputs.inputBlockRenames = &inputBlockRenames; - esslKeyInputs.outputBlockRenames = &outputBlockRenames; - - // ...and THIS STAGE's share of the interface-block LOCATION strip, planned the - // same way and for the same reason. The gate has three parts, all of which have - // to hold before a single block loses its qualifier: - // * the driver POST measured the defect (never a renderer-string quirk list); - // * this program has a stage that can hit it - a located block between a - // vertex and a fragment stage works on the affected driver, so a program - // with neither tessellation nor geometry keeps its ESSL byte for byte; - // * for THIS stage and THIS direction, this program HAS a stage on that side - // of it. That is the same test the rename plan above makes, and the same - // approximation: it asks "is some stage of this program earlier/later than - // me", not "is the exact partner of every one of my blocks here". The two - // coincide for every program MobileGL builds, because a separable pipeline - // is flattened into one composite carrying every stage that has a shader - // (GLContext::GetProgramForDraw) and a program bound with glUseProgram has - // no partner program at all - so a stage set with a gap in it does not - // arise. Should one ever arise, this must become the nearest-stage - // resolution the rename plan computes, or the two ends of the gap would - // disagree about the qualifier. - // The direction tests deliberately mirror that plan rather than inventing a - // second rule for the same question. - Bool stripInputBlockLocations = false; - Bool stripOutputBlockLocations = false; - if (ioBlockLocationStripArmed && stagePipelineIndices[index] >= 0) { - const Int myPipelineIndex = stagePipelineIndices[index]; - for (const Int otherPipelineIndex : stagePipelineIndices) { - if (otherPipelineIndex < 0) continue; - if (otherPipelineIndex < myPipelineIndex) stripInputBlockLocations = true; - if (otherPipelineIndex > myPipelineIndex) stripOutputBlockLocations = true; - } - } - esslKeyInputs.stripInputBlockLocations = stripInputBlockLocations; - esslKeyInputs.stripOutputBlockLocations = stripOutputBlockLocations; - esslKeyInputs.enableSpirvValidation = enableSpirvValidation; - - auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache(); - MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey; - if (MG_Util::ShaderTranspiler::ShaderTranslationCacheEnabled()) { - esslCacheKey = MG_Util::ShaderTranspiler::BuildEsslTranslationKey(esslKeyInputs); - } - - std::set stageFlattenedXfbBlockNames; - // Per stage, and NOT m_atomicCounterGlBindings directly: on a miss the - // transpile appends to this, on a hit the payload supplies it, and only then - // is it folded into the program-wide vector. Pointing the transpile straight - // at the member would have made the miss path and the hit path disagree about - // who owns the append. - Vector stageAtomicCounterGlBindings; - const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit = - esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr; - if (esslHit) { - source = esslHit->essl; - stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames; - stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings; - } else { - String transpileError; - if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames, - imageFormatBake, storageBlockBindingOverrides, - inputBlockRenames, outputBlockRenames, - stripInputBlockLocations, stripOutputBlockLocations, - m_atomicCounterEsslBindingTop, - enableSpirvValidation, source, - stageFlattenedXfbBlockNames, - stageAtomicCounterGlBindings, transpileError)) { - // MGLOG_E, unlatched, like the compile- and link-failure diagnostics - // below: one line per failing stage is bounded by program count and - // naming the stage is the entire diagnostic value. A stage that never - // reaches the driver leaves the program short of that stage, so the link - // fails with an EMPTY driver info log - the least debuggable failure - // MobileGL can produce, and what hid the whole - // KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros". - MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, " - "SPIRV-Cross error: %s", - src.GetExternalIndex(), - MG_Util::ConvertGLEnumToString(glShaderType).c_str(), - transpileError.c_str()); - m_backendProgramUsable = false; - continue; - } - if (esslCacheKey.Valid()) { - auto payload = MakeShared(); - payload->essl = source; - payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames; - payload->atomicCounterGlBindings = stageAtomicCounterGlBindings; - const SizeT payloadBytes = - MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload); - esslCache.Insert( - esslCacheKey, - MG_Util::ShaderTranspiler::EsslTranslationResultPtr(Move(payload)), - payloadBytes); - } - } - // Per stage, never cumulative: a fragment shader consuming the same block - // reports a name the vertex stage already reported, and its own rewrite must - // still be taken or the two stages stop matching. Done here rather than inside - // the transpile so a cache HIT contributes its names too. - flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(), - stageFlattenedXfbBlockNames.end()); - // Same rule for the atomic-counter bindings this stage declared, and for the - // same reason: the loop below de-duplicates across stages, so a hit that - // contributed nothing would silently drop a counter buffer the draw path has - // to bind. - m_atomicCounterGlBindings.insert(m_atomicCounterGlBindings.end(), - stageAtomicCounterGlBindings.begin(), - stageAtomicCounterGlBindings.end()); - - // SPIRV-Cross sizes written clip/cull distances with a standalone - // `out float gl_ClipDistance[N];`, which an ES compiler may refuse as a reserved - // name and so lose the whole program. Redeclared as gl_PerVertex members instead. - // BEFORE the gl_PointSize extension probe below (the block can name it) and - // before vertexStageEssl is captured, so a synthesized pass-through control - // stage mirrors the block this produces. - source = RedeclareClipCullDistancesInPerVertexBlock(std::move(source), glShaderType); - // ...and the consumer half: a stage reading gl_in[i].gl_ClipDistance/CullDistance - // redeclares its input block with the sizes the producer wrote. - source = RedeclareClipCullDistanceInputsInPerVertexBlock(std::move(source), glShaderType, - producerOutPerVertexMembers); - if (glShaderType != GL_FRAGMENT_SHADER) { - producerOutPerVertexMembers = - ExtractPerVertexBlockMembers(source, /*input=*/false).value_or(String()); - } - - // Position in the chain is arbitrary: this is the only header-level rewrite, it - // edits #extension directives and never the body, and the replacement is the - // same length and stays an #extension line - so it commutes with every pass - // below, including ForceSupporterOutput's scan for the last directive. First, - // because a header concern reads better before the body ones. - source = RetargetTextureBufferExtension(std::move(source), - g_GLESCapabilities.TextureBufferSupport); - // The other header-level rewrite, and next to that one for the same reason. The - // formats it covers are both the ones the bake above put into the module and the - // ones the application declared itself - either can be outside the thirteen GLSL - // ES has in core, and neither reaches the driver without this directive. - source = RequestExtendedImageFormats(std::move(source), - imageFormatBake.needsExtendedImageFormats && - g_GLESCapabilities.SupportsExtendedImageFormats); - // The third header-level rewrite, for the builtin SPIRV-Cross prints bare: - // gl_ViewportIndex is in no version of ESSL core, so without this directive the - // stage does not compile and the whole program - not just its viewport routing - - // is lost. The token probe keeps the line off every other program and the - // capability gate keeps it off drivers that would hard-error on an unadvertised - // name; a driver without the extension took the LowerViewportIndexPass fallback - // above and its source no longer names the builtin at all, so the two are mutually - // exclusive by construction. Read `source` BEFORE it is moved from. - // The routing emulation is the third way this can be reached and the only one - // that needs no directive: it renames the fragment stage's read onto the varying - // the producing stage now writes, a few passes below. - const Bool needsViewportArrayExtension = - g_GLESCapabilities.SupportsViewportArray && - !(ViewportArrayEmulationEnabled() && programRoutesViewportIndex) && - source.find("gl_ViewportIndex") != String::npos; - source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension); - - // The fourth header-level rewrite, and the same shape as the third: ESSL has no - // gl_PointSize in a tessellation or geometry stage at ANY version - 320 makes the - // stages core and still leaves the built-in behind EXT/OES_..._point_size - while - // SPIRV-Cross prints it bare. Without the directive the stage fails to compile - // with "`gl_PointSize' undeclared", which takes the whole program to program 0: - // the draw renders nothing AND glBeginTransformFeedback is rejected, so a capture - // of anything at all off that program silently comes back empty. The token probe - // keeps the line off every other program and PointSizeExtensionName returns - // nullptr - i.e. nothing is emitted - on a driver advertising neither spelling. - if (source.find("gl_PointSize") != String::npos) { - const Bool tessellationStage = glShaderType == GL_TESS_CONTROL_SHADER || - glShaderType == GL_TESS_EVALUATION_SHADER; - if (tessellationStage || glShaderType == GL_GEOMETRY_SHADER) { - const auto tier = tessellationStage ? g_GLESCapabilities.TessellationPointSizeSupport - : g_GLESCapabilities.GeometryPointSizeSupport; - const char* pointSizeExtension = PointSizeExtensionName(tier, tessellationStage); - if (pointSizeExtension == nullptr) { - // Latched, and an ERROR rather than a warning: what follows is a - // driver compile failure whose text names a built-in the application - // never mis-spelled, and the reason is a missing driver capability - // rather than anything in the shader. Saying so here is the whole - // difference between a legible skip and an unexplained black draw. - MGLOG_E_ONCE("This driver advertises neither the EXT nor the OES %s_point_size " - "extension, so its ESSL has no gl_PointSize in a %s stage; program %u " - "will fail to compile. Point size from a non-vertex stage is not " - "available on this device.", - tessellationStage ? "tessellation" : "geometry", - tessellationStage ? "tessellation" : "geometry", - src.GetExternalIndex()); - } - source = RequestPointSizeExtension(std::move(source), pointSizeExtension); - } - } - - source = RebindImageUniformsToFrontendUnits(std::move(source), src); - // The completion half of the format bake, for the formats SPIRV-Cross throws on - // rather than prints (r8ui and the rest of its desktop-only set). Empty for every - // program whose format-less images bound a format the module could carry, which - // is the normal case - those were baked into the SPIR-V above and this pass finds - // their declarations already qualified. AFTER the rebind, so the layout qualifier - // it edits is the one that already exists; BEFORE the split and the binding - // strip, so both halves of a split image inherit the format. - source = BakeImageFormatQualifiers(std::move(source), - imageFormatBake.esslFormatQualifierByUniformName); - // An image ARRAY whose elements do not sit on consecutive units cannot be spelled - // by the single layout(binding=N) the rebind above stamped: ESSL gives element k - // the unit N+k and there is no glUniform1i to correct it with. Split the array - // into one scalar declaration per element, each with its own binding. AFTER the - // rebind and the format bake, both of which look the array up by its GL uniform - // name and need the binding already there; BEFORE the read+write split, so an - // element that is both read and written is split with its own binding on it. - if (!nonConsecutiveImageArrays.empty()) { - Vector declinedImageArrays; - source = RemapImageArrayElementUnits(source, nonConsecutiveImageArrays, - &declinedImageArrays); - for (const auto& declined : declinedImageArrays) { - // MGLOG_E, unlatched, like the transpile- and compile-failure diagnostics - // around it: this is the "linked, drew, produced wrong numbers, said - // nothing" shape that cost earlier waves whole days, and one line per - // declined array is bounded by program count. There is no honest GL answer - // to give instead - the frontend has already reported LINK_STATUS = true. - MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach " - "from one declaration, so this stage will read and write the WRONG units. State " - "program ID: %u, stage: %s.", - declined.c_str(), src.GetExternalIndex(), - MG_Util::ConvertGLEnumToString(glShaderType).c_str()); - } - } - // Wedged between those two on purpose: - // * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto - // both halves of a split image is already the frontend texture unit (and so - // that pass never has to reason about the alias it introduces); - // * BEFORE RemoveLayoutBinding, whose keepBindingRegex recognises an image - // declaration and preserves its binding - an image unit cannot be set from - // the API in ES, so the qualifier is the only binding mechanism there is, - // and both halves of the pair have to still be carrying theirs when it runs. - // Takes no stage: the qualifier it adds is a decision about THIS text's accesses - // and the rename that keeps two stages from declaring one image uniform - // differently is keyed on that same decision, so two stages that agree still - // share one uniform (see the location-budget note on the pass). - Uint splitImageUniformCount = 0; - source = SplitReadWriteImageUniforms(source, &splitImageUniformCount); - if (splitImageUniformCount != 0) { - splitImageUniformStages.push_back({linkedStages[index], splitImageUniformCount}); - } - source = RemoveLayoutBinding(source); - source = ProcessOutColorLocations(source); - source = ForceFlatIntegerVaryings(source, glShaderType); - source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount); - source = LowerZeroGradCubeArrayShadowLookups(source, glShaderType); - source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe()); - if (ClampsUnsignedMultisampleFetchesAtRuntime()) { - source = ClampUnsignedMultisampleFetches(source); - } - source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); - source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType); - // The two halves of the gl_ViewportIndex routing emulation, next to the draw- - // parameter promotion because they are the same shape: a builtin ESSL cannot - // spell, demoted to a plain global by a SPIR-V pass, given a real interface here. - // BEFORE ForceSupporterOutput, so the `precision highp` statements it hoists to - // the top land above the declarations these inject; AFTER - // ForceFlatIntegerVaryings, which matches only declarations carrying a - // layout(...) qualifier and so cannot touch either of them. - if (viewportEmulationForThisProgram) { - if (glShaderType == GL_FRAGMENT_SHADER) { - if (programRoutesViewportIndex && !InjectViewportIndexPassGate(source)) { - // MGLOG_E, unlatched, like the transpile- and compile-failure - // diagnostics around it: the program still links and still draws, so - // nothing else in the process will ever say that its viewport routing - // silently collapsed back to one rectangle. - MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no " - "entry point to gate, so the routing cannot be emulated: every " - "index will rasterize against viewport 0. State program ID: %u.", - m_backendProgramId, src.GetExternalIndex()); - } - } else if (PromoteViewportIndexGlobalToVarying(source)) { - programRoutesViewportIndex = true; - } - } - source = ForceSupporterOutput(source); - source = ClampNormFallbackOutputs(std::move(source), glShaderType, - m_snormFallbackClampOutputMask, - m_unormFallbackClampOutputMask); - - // Patch for Photon compiler precision issue - String findStr = "1000000.0"; - String replaceStr = "65500.0"; - auto pos = source.find(findStr); - while (pos != String::npos) { - MGLOG_D("Applying patch #2 to Photon..."); - source.replace(pos, findStr.length(), replaceStr); - pos = source.find(findStr, pos); - } - - const char* sourceCStr = source.c_str(); - MGLOG_D("Setting shader source for backend shader ID: %u\nsrc:\n%s", backendShaderId, sourceCStr); - g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr); - g_GLESFuncs.glCompileShader(backendShaderId); - - // GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed" - // and take the diagnostic path, never as a silent success that attaches an - // uncompiled shader. - GLint compileStatus = GL_FALSE; - g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus); - if (compileStatus == GL_FALSE) { - GLint logLength = 0; - g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength); - if (logLength < 0) logLength = 0; - // +1 and zero-filled: GL_INFO_LOG_LENGTH already counts the terminator, - // but a driver that reports 0 (or fails the query) must still leave - // log.data() a readable empty C string for the %s below. - Vector log(static_cast(logLength) + 1, '\0'); - g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data()); - log.back() = '\0'; - // MGLOG_E, unlatched. This was parked at MGLOG_I while the level ordering - // compiled E and W out of every INFO build: the Android retrace artifact - // carried 294 INFO lines and zero ERROR lines while two generated shaders - // were being rejected outright, and the lane could not say why it was - // rendering an empty translucent layer. A shader the driver refuses is - // never noise. - // - // A BOUNDED EXCERPT of the source goes with it. The driver log names a line - // and a column in text that exists nowhere but here, so without any source at - // all the only way to read "`gl_PointSize' undeclared" is to rebuild the whole - // library at DEBUG - but the full dump cannot go at E either. This is not - // "one line per refused shader": SyncToBackend's rebuild gate keys on - // per-draw state (the enabled-draw-buffer count among it), so a program used - // across passes with different draw-buffer counts re-transpiles, re-compiles - // and re-fails on every alternation, i.e. per frame. At E - live at the - // production INFO level - each of those records would push the whole - // post-SPIRV-Cross ESSL through the global log mutex with a forced flush onto - // /sdcard/MG/latest.log, the file users are asked to share. The excerpt keeps - // the record O(1); the full text is still there at D, printed against this - // same backend shader id by the "Setting shader source" line above, so - // nothing needs to be dumped twice. - constexpr SizeT kMaxLoggedSourceBytes = 2048; - String truncatedSource; - const char* sourceForLog = source.c_str(); - if (source.size() > kMaxLoggedSourceBytes) { - // Back up to a line boundary when there is one inside the window, so the - // excerpt ends on a whole statement rather than mid-token. Built only on - // this branch: a stage that fits keeps its own buffer and is not copied. - SizeT cut = kMaxLoggedSourceBytes; - if (const SizeT lastNewline = source.rfind('\n', cut); - lastNewline != String::npos && lastNewline > 0) { - cut = lastNewline + 1; - } - truncatedSource = source.substr(0, cut); - truncatedSource += "... [" + std::to_string(source.size() - cut) + - " more bytes; the whole stage is printed at the DEBUG level]\n"; - sourceForLog = truncatedSource.c_str(); - } - MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: " - "%u, driver log: %s\nSource:\n%s", - src.GetExternalIndex(), - MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId, - log.data(), sourceForLog); - m_backendProgramUsable = false; - // Nothing will ever attach this one, so nothing else can free it. - g_GLESFuncs.glDeleteShader(backendShaderId); - continue; - } - - MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId); - g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId); - // Hand the shader's lifetime to the program, immediately and unconditionally. - // - // glDeleteShader only FLAGS a shader; the driver frees it when it is attached to - // nothing. Flagging it here is what makes the program own it, so deleting the - // program (or the detach loop above, on a relink) is what actually frees it. - // Without this call every program build leaked its shader objects for the process - // lifetime, and a relink leaked them twice - the detach loop above dropped the - // program's reference to shaders nothing had flagged, so they became unreachable - // AND undeletable. The GL swizzle conformance test builds 1,296 programs per case, - // so a handful of cases left tens of thousands of live driver shaders behind and - // the driver started mis-serving them (KHR-GL33/GL40.texture_swizzle.smoke_*). - // Same class of defect as the missing framebuffer/renderbuffer/sampler destructors - // fixed in Wave 1, and the last of that family: this is the one backend GL object - // MobileGL creates without an owning wrapper to destroy it. - g_GLESFuncs.glDeleteShader(backendShaderId); - - // Kept AFTER every text-level pass, so what the synthesized control stage mirrors - // is the text the driver actually sees, not an intermediate form. - if (needsPassthroughTessControl) { - if (glShaderType == GL_VERTEX_SHADER) { - vertexStageEssl = source; - } else if (glShaderType == GL_TESS_EVALUATION_SHADER) { - tessEvalStageEssl = source; - } - } - - MGLOG_D("Processed shader source length: %zu", source.length()); - } - - if (needsPassthroughTessControl) { - AttachPassthroughTessControlStage(src, tessEvalShaderIndex, shaderSpirvs, - vertexStageEssl, tessEvalStageEssl); - } - - // 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 - // the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the - // frontend's requested names carry over unchanged. - SizeT declaredXfbVaryingCount = 0; - if (src.GetTransformFeedbackVaryingCount() > 0 && - g_GLESFuncs.glTransformFeedbackVaryings != nullptr) { - const auto& xfbVaryings = src.GetTransformFeedbackVaryings(); - Vector xfbNames; - xfbNames.reserve(xfbVaryings.size()); - // A block this build flattened no longer HAS the member the application asked - // for; it has the variable that replaced it. Everything else - including a - // member of a block that was left alone - keeps the application's spelling. - // Storage first, pointers after: xfbNames holds pointers into these strings. - // - // Same rule for a demoted gl_PointSize: the capture stage's ESSL no longer - // spells the built-in at all - the value lives in the carrier the demotion - // named - so the driver-side request has to follow it there. Only when the - // capture stage IS a demoted one (geometry, else evaluation): a program whose - // capture stage is the vertex shader keeps the built-in and its spelling, - // whatever happened to a control stage behind it. - Bool captureStageDemoted = false; - if (src.PointSizeDemoted()) { - for (const ShaderStage linkedStage : linkedStages) { - if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) { - captureStageDemoted = true; - break; - } - } - } - Vector rewrittenXfbNames(xfbVaryings.size()); - for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) { - String flatName; - if (!flattenedXfbBlockNames.empty() && - MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock( - xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) { - rewrittenXfbNames[nameIndex] = std::move(flatName); - } else if (captureStageDemoted && xfbVaryings[nameIndex].name == "gl_PointSize") { - rewrittenXfbNames[nameIndex] = - MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME; - } else { - rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name; - } - } - for (const auto& xfbName : rewrittenXfbNames) { - xfbNames.push_back(xfbName.c_str()); - } - MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(), - m_backendProgramId); - // Bounded: a lost context never answers GL_NO_ERROR, and this runs on the - // thread that would then spin forever. - for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { - } - g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast(xfbNames.size()), - xfbNames.data(), - src.GetTransformFeedbackBufferMode()); - // Unchecked before. A rejected capture set leaves the program linking happily - // with NO capture set at all, and then every draw of every span records - // nothing while the application reads its buffer's pre-draw bytes and - // GL_NO_ERROR - the signature four conformance families were stuck on. - if (const GLenum xfbError = g_GLESFuncs.glGetError(); xfbError != GL_NO_ERROR) { - String declared; - for (const auto& xfbName : rewrittenXfbNames) { - if (!declared.empty()) declared += ", "; - declared += xfbName; - } - MGLOG_E("The ES driver REJECTED the transform feedback capture set for backend program %u with " - "%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.", - m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(), - MG_Util::ConvertGLEnumToString( - src.GetTransformFeedbackBufferMode()).c_str(), - declared.c_str(), src.GetExternalIndex()); - } - declaredXfbVaryingCount = xfbNames.size(); - } - - // Link program - MGLOG_D("Linking program %u", m_backendProgramId); - g_GLESFuncs.glLinkProgram(m_backendProgramId); - - GLint linkStatus = GL_FALSE; - g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus); - m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE; - if (linkStatus != GL_TRUE) { - GLint logLength = 0; - g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength); - if (logLength < 0) logLength = 0; - Vector log(static_cast(logLength) + 1, '\0'); - g_GLESFuncs.glGetProgramInfoLog(m_backendProgramId, logLength, nullptr, log.data()); - log.back() = '\0'; - // MGLOG_I for the same reason as the compile failure above: a program that - // links nothing no-ops every draw that uses it, and that has to be readable - // in an INFO-level artifact. - MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s", - src.GetExternalIndex(), m_backendProgramId, log.data()); - // The one link failure MobileGL can name a cause for that the driver's log never - // will: ESSL has no legal single declaration for a read+write image outside - // r32f/r32i/r32ui, so those are split into a coherent pair and the stage ends up - // declaring more image uniforms than the application did - against a - // GL_MAX_*_IMAGE_UNIFORMS that is still the driver's raw number, because lowering - // it would fail basic-api and NotSupported-out every case that only ever uses - // readonly/writeonly images. A shader declaring more than half a stage's budget in - // read+write images therefore links here and nowhere else, and without this line - // the next reader has only a generic driver message to go on. - for (const SplitImageUniformStage& split : splitImageUniformStages) { - MGLOG_E("...and %u read+write image uniform(s) in stage %s were split into coherent " - "read/write pairs, so that stage declares %u image uniform(s) more than the " - "program did; GL_MAX_*_IMAGE_UNIFORMS for it is %d. If the driver log names " - "image uniforms, that is the cause.", - split.splitCount, - MG_Util::ConvertGLEnumToString( - MG_Util::ConvertShaderStageToGLEnum(split.stage)).c_str(), - split.splitCount, AdvertisedStageImageUniformLimit(split.stage)); - } - } else { - MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId); - // A link that SUCCEEDS can still have dropped the capture set: ESSL rejects a - // requested name the transpiled shader does not actually declare by simply not - // capturing it, and a program whose last vertex-processing stage was rewritten - // by a SPIR-V pass (viewport-index lowering, gl_PerVertex handling, the - // synthesized pass-through tessellation control stage) can end up spelling its - // outputs differently from the frontend's request. Asking the driver what it - // ACTUALLY linked is the only way to tell that apart from a driver that just - // captures nothing - which is the whole ambiguity the empty-capture failures - // across geometry_shader / tessellation_shader / gpu_shader5 / DSA sat on. - if (declaredXfbVaryingCount > 0) { - GLint linkedXfbVaryings = 0; - GLint linkedXfbBufferMode = 0; - g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_VARYINGS, - &linkedXfbVaryings); - g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_BUFFER_MODE, - &linkedXfbBufferMode); - for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { - } - const GLenum requestedMode = src.GetTransformFeedbackBufferMode(); - if (static_cast(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount || - static_cast(linkedXfbBufferMode) != requestedMode) { - MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not " - "agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) " - "in mode %s. Captures made with it will be empty or wrongly laid out.", - m_backendProgramId, src.GetExternalIndex(), declaredXfbVaryingCount, - MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings, - MG_Util::ConvertGLEnumToString( - static_cast(linkedXfbBufferMode)).c_str()); - } else { - MGLOG_D("Backend program %u capture set confirmed by the driver: %d varying(s), mode %s", - m_backendProgramId, linkedXfbVaryings, - MG_Util::ConvertGLEnumToString( - static_cast(linkedXfbBufferMode)).c_str()); - } - } - } - // The driver program was relinked IN PLACE, so its GL name no longer identifies - // the executable behind it - and that name is exactly what Use()'s - // g_lastUsedBackendProgramId early-out treats as identifying it. Without this, - // a rebuild of the program that is already bound issues no glUseProgram at all - // and the driver keeps running whatever the last one installed. - // - // GL 4.6 core 7.3 does promise that a successful re-link of a program in use - // installs the new executable - but only "for all shader stages where the program - // is active", and a stage the previous link did not produce is not active for - // anything. So a relink that ADDS a stage is precisely the case the promise does - // not cover. Verified with no MobileGL in the process (bare EGL + GLES 3.2, - // llvmpipe 26.1.4): vertex+fragment linked, used and drawn renders; a geometry - // shader attached and relinked reports LINK_STATUS true with an empty info log, - // and the next draw renders NOTHING and raises no error - while the same draw - // after a fresh glUseProgram of the same name renders again. - // - // A flag rather than zeroing the guard: 0 is also the id Use() binds for a build - // that did NOT come out usable, and a zeroed guard would make it skip that - // glUseProgram(0) and leave the failed program's previous executable running - - // the silent wrong-shader draw Use() exists to prevent. - m_rebindAfterRelink = true; - m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, - BASE_INSTANCE_UNIFORM_NAME); - m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME); - m_baseVertexUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, - BASE_VERTEX_UNIFORM_NAME); - m_baseInstanceWordIndexUniformLocation = - g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME); - // Asked of the DRIVER rather than remembered from the injection, deliberately: the - // gate is only real if the uniform survived compilation and linking, and this is the - // one question whose answer covers both. A gate the driver optimized away would - // otherwise leave the draw path replaying passes whose mask reaches nothing, which - // renders every index's primitives in every pass. - m_viewportPassMaskUniformLocation = - g_GLESFuncs.glGetUniformLocation(m_backendProgramId, VIEWPORT_PASS_MASK_UNIFORM_NAME); - if (m_viewportPassMaskUniformLocation >= 0) { - // Sticky, and never cleared on a relink: it only ever short-circuits a per-draw - // check, so being late to go false costs a pointer compare and being late to go - // true would cost correctness. - g_anyProgramRoutesViewportIndex = true; - } - // The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind - // SSBO blocks after compile); record it so draws bind the indirect buffer there. - m_indirectParamsBinding = -1; - if (m_baseInstanceWordIndexUniformLocation >= 0 && g_GLESFuncs.glGetProgramResourceIndex) { - const GLuint blockIndex = g_GLESFuncs.glGetProgramResourceIndex( - m_backendProgramId, GL_SHADER_STORAGE_BLOCK, INDIRECT_PARAMS_BLOCK_NAME); - if (blockIndex != GL_INVALID_INDEX && g_GLESCapabilities.MaxShaderStorageBufferBindings > 0) { - m_indirectParamsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings - 1; - } - } - - // Create global UBO - if (src.GetUBOSize() > 0) { - g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId); - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId); - g_GLESFuncs.glBufferData(GL_UNIFORM_BUFFER, src.GetUBOSize(), nullptr, GL_STREAM_DRAW); - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); - } else { - m_backendGlobalUBOId = 0; - } - - CacheResourceLocations(src); - // NOT the mechanism that makes a rebinding work - the transpiled qualifier above is. - // glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes, - // so this replay is a no-op almost everywhere; it stays because it is still correct - // (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL - // driver used as the ES backend. AFTER the link either way, because it needs the - // driver's linked interface. - ReseedShaderStorageBlockBindings(m_backendProgramId, src); - - m_isInitialized = true; - MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId); - } - - // P5e (pg), CONTRACT-P5E §5.5: THE BUILD THAT NAMES NO CLIENT MEMORY. Everything it - // reads is the record: the archive create_shader_state carried, the three binding tails - // set_program_bindings carried, and the descriptor. Nothing here resolves a frontend - // identity, probes the client allocator or holds a SharedPtr to a frontend object - the - // three absences rule F is made of. - void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - const auto* record = PipeShaderCsoRecordForHandle(cso); - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record, so no driver " - "program can be built for it", - cso.Slot, cso.Gen); - return; - } - // THE ARCHIVE IS THE RECORD'S OR THE BUILD DOES NOT HAPPEN. A record with no archive - // is one the transport arm never filled - a monolith create that reached a handle - // arm - and building from an empty reflection would produce a program with no - // uniforms and no modules, i.e. a black screen with no marker anywhere. The - // create's own trip wire already refuses a record that declares no blobs AND - // carries no artefacts (PipeApply.cpp); this is the same statement one level later, - // where the twin can name the handle. - if (!record->Archive) { - MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} carries no server-owned archive; its " - "create_shader_state was emitted by the monolith arm and this is " - "the handle arm, so there is nothing to build from", - cso.Slot, cso.Gen); - m_backendProgramUsable = false; - return; - } - SyncToBackendFromSource(ProgramArchiveSource::FromRecord(cso, *record)); - // The two server-owned keys the draw path's clean condition reads instead of - // GetLinkVersion()/GetImageUnitVersion(). BindingsSerial is the second because a - // glUniform1i on an IMAGE uniform is baked into the generated ESSL rather than - // re-issued per draw - the record that carries it must therefore force a rebuild, - // exactly as m_imageUnitVersion used to. - m_syncedShaderCsoSerial = record->Serial; - m_syncedBindingsSerial = record->BindingsSerial; - } - - namespace { - // The GL name of the array element that lives at `location`, given the reflection - // name reported for it. Reflection reports one name per UNIFORM ("goku[0]") but - // one location per ELEMENT, so a caller walking locations sees the same name - // repeatedly; this turns it back into "goku[k]". Anything that is not an array - // (or whose base location cannot be resolved) comes back unchanged, so the only - // behaviour that moves is the array case. - String SubscriptUniformNameForElement(const ProgramBuildSource& program, const String& name, - Uint location) { - if (name.size() < 3 || name.compare(name.size() - 3, 3, "[0]") != 0) return name; - const Int base = program.GetUniformLocation(name); - if (base < 0 || static_cast(base) > location) return name; - const Uint element = location - static_cast(base); - if (element == 0) return name; - return name.substr(0, name.size() - 3) + "[" + std::to_string(element) + "]"; - } - } // namespace - - // Resolves every name-based resource lookup once per link so the per-draw path - // (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex / - // glGetUniformLocation string queries; block-to-binding-point assignments are - // program state and only need to be established here. - void BackendProgramObjectImpl::CacheResourceLocations(const ProgramBuildSource& src) { - m_globalUboBackendBlockIndex = -1; - m_globalUboBackendBlockSize = 0; - m_lastUploadedGlobalUboVersion = ~0u; - m_globalUboRingAllocation = {}; - if (src.GetUBOSize() > 0) { - const Uint blockIndex = - g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); - if (blockIndex != GL_INVALID_INDEX) { - m_globalUboBackendBlockIndex = static_cast(blockIndex); - g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0); - // Ring bindings are ranges and must span the block as the backend - // compiled it (its std140 padding may exceed the frontend's - // SPIR-V-reflected size). - if (g_GLESFuncs.glGetActiveUniformBlockiv) { - GLint blockDataSize = 0; - g_GLESFuncs.glGetActiveUniformBlockiv(m_backendProgramId, blockIndex, - GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize); - m_globalUboBackendBlockSize = static_cast(blockDataSize); - } - } else { - MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.", - src.GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); - } - } - - const Int uboCount = src.GetActiveUniformBlocksCount(); - m_uniformBlockBackendIndices.assign(static_cast(std::max(uboCount, 0)), -1); - Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO - for (Int i = 0; i < uboCount; ++i) { - ++lastUBOBinding; - const auto& name = src.GetUniformBlockName(static_cast(i)); - const GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, name.c_str()); - if (backendBlkIdx == GL_INVALID_INDEX) { - // Either eliminated as unused, or an SSBO block (frontend reflection - // lists those among uniform blocks); SSBO bindings are baked into the ESSL. - continue; - } - m_uniformBlockBackendIndices[static_cast(i)] = static_cast(backendBlkIdx); - g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding); - MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u", - src.GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx, - lastUBOBinding); - } - - m_samplerUniformBindings.clear(); - const Uint maxUniformLoc = src.GetMaxUniformLocation(); - for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { - const auto& name = src.GetUniformName(loc); - if (name.empty()) continue; - const GLenum uniformType = src.GetUniformType(loc); - if (IsImageUniformType(uniformType)) { - // ES image units come exclusively from the layout(binding=N) qualifier - // (preserved in the transpiled ESSL); glUniform1i on an image uniform - // is an INVALID_OPERATION. - continue; - } - // Reflection names an array uniform after its FIRST element ("goku[0]") at - // every location the array spans, so asking the driver for that one name - // once per location hands back the same backend location N times. The - // per-draw pass then issues N glUniform1i calls against it and only the - // last element's unit survives - "layout(binding = 1) uniform sampler2D - // goku[7]" ended up with goku[0] on unit 7 and goku[1..6] still on 0. - // Address each element by its own name instead; the frontend already - // reserves one location per element, so the element index is the distance - // from the array's base location. - const String elementName = SubscriptUniformNameForElement(src, name, loc); - const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, elementName.c_str()); - if (backendLoc < 0) continue; - SamplerUniformBinding binding; - binding.frontendLocation = loc; - binding.backendLocation = backendLoc; - binding.uniformType = uniformType; - binding.lastAssignedUnit = -1; - // Present only for the samplers EmulateTextureLodBias actually rewrote; the - // pass names it after the sampler, which SPIRV-Cross preserves verbatim. - binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation( - m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str()); - binding.lastAssignedLodBias = 0.0f; - // Present only for the unsigned multisample samplers ClampUnsignedMultisampleFetches - // rewrote, named the same way. - if (uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE || - uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY) { - binding.msSampleMaxLocation = g_GLESFuncs.glGetUniformLocation( - m_backendProgramId, (String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + elementName).c_str()); - } - binding.lastAssignedMsSampleMax = 0; - m_samplerUniformBindings.push_back(binding); - } - } - - void BackendProgramObjectImpl::Use() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // glUseProgram on a program that did not link is an INVALID_OPERATION and - // leaves the *previous* program current, so the draw would silently render - // with an unrelated shader (KHR-GL3x.texture_size_promotion read another - // test case's alpha that way once a sampler2DRect stage failed to - // transpile). Bind nothing instead: the draw is then a visible no-op. - const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0; - // ...unless SyncToBackend relinked this program since the last bind, in which case - // the id matching proves nothing about the executable behind it. - if (g_lastUsedBackendProgramId == programToBind && !m_rebindAfterRelink) { - return; - } - m_rebindAfterRelink = false; - if (!m_backendProgramUsable) { - // Every draw made with this program renders nothing and raises no GL error, so - // without this line the only symptom is a framebuffer that kept its clear - // colour. Latched: the early return above only dedupes CONSECUTIVE binds, so an - // app alternating a healthy and a broken program would otherwise log every - // single draw. Parked at MGLOG_I until the level ordering was fixed. - MGLOG_E_ONCE("Backend program for GL program %u is unusable (a shader failed to transpile, " - "compile or link); binding program 0 - draws with it will render nothing", - m_frontendProgramId); - } - MGLOG_D("Using program %u", programToBind); - g_GLESFuncs.glUseProgram(programToBind); - g_lastUsedBackendProgramId = programToBind; - } - - void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const { - if (m_baseInstanceUniformLocation >= 0) { - g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast(baseInstance)); - } - // A direct value disables the indirect-command-buffer read. - SetBaseInstanceWordIndex(-1); - } - - // The uniform is written one-based so that its GLSL initial value, zero, already reads - // as "no indirect command" - see PromoteDrawParameterGlobalsToUniforms. - void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const { - if (m_baseInstanceWordIndexUniformLocation >= 0) { - g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, - wordIndex < 0 ? 0 : wordIndex + 1); - } - } - - void BackendProgramObjectImpl::SetBaseVertex(Int32 baseVertex) const { - if (m_baseVertexUniformLocation >= 0) { - g_GLESFuncs.glUniform1i(m_baseVertexUniformLocation, baseVertex); - } - } - - void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const { - if (m_drawIdUniformLocation < 0) { - return; - } - g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast(drawId)); - } - - void BackendProgramObjectImpl::SetViewportPassMask(Uint32 indexMask) const { - if (m_viewportPassMaskUniformLocation < 0) { - return; - } - g_GLESFuncs.glUniform1i(m_viewportPassMaskUniformLocation, static_cast(indexMask)); - } - } // namespace PrgramImpl - - namespace SamplerImpl { - BackendSamplerObject::BackendSamplerObject() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId); - m_contextGeneration = g_backendContextGeneration; - if (m_backendSamplerId == 0) { - MGLOG_E_ONCE("Failed to generate sampler object."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } else { - MGLOG_D("Generated sampler object with ID: %u.", m_backendSamplerId); - } - } - - BackendSamplerObject::~BackendSamplerObject() { - if (InProcessTeardown()) { - return; // see InProcessTeardown(): the driver may be unloaded already - } - if (m_backendSamplerId == 0) { - return; - } - // Scrub the unit shadow whether or not the id can still be deleted - the next - // twin can land on this heap address and would otherwise false-skip its Bind. - for (auto& boundSampler : g_boundSamplersCache) { - if (boundSampler == this) { - boundSampler = nullptr; // glDeleteSamplers unbinds from every unit - } - } - if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) { - g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId); - } - m_backendSamplerId = 0; - } - - void BackendSamplerObject::SyncToBackend( - const SharedPtr& stateSamplerObject, - MG_Pipe::MGPipeHandle pushedCso) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (!stateSamplerObject) { - // A CSO TWIN HAS NO FRONTEND OBJECT (P4a fable seam F-4, ResolveSamplerCsoTwin): - // the content-addressed handle is its identity and the applier's record its - // only authority, so a null object beside a live handle is the record arm and - // not the pre-P4a error. Everything below that names the object is guarded on - // it; the pull build's text is the three lines the #if brackets. - if (MG_Pipe::MGPipeHandleIsNull(pushedCso)) { - MGLOG_E_ONCE("State sampler object is null, cannot sync to backend."); - return; - } - } - - // P4a (D-F1): a SamplerObject is a pure 100-byte value with no driver-side per-object - // binding state, so its CSO is CONTENT-ADDRESSED on the client at capacity 256 and - // two identical samplers share one record. What that means here is that the record's - // server-owned Serial, not the frontend object's version, is what says the values - // moved - and that the values themselves cross byte for byte INCLUDING - // borderColorForm, which is why all four border comparands below are still compared. - // - // The whole arm choice is a preprocessor #if/#else so the PULL build's text is the - // pre-P4a text token for token (D-P). - const SamplerParameters* pushedParams = nullptr; - // The GL name for the two log lines below, or 0 for a CSO twin, which has no object - // to name (F-4): the handle in the same line is its name. - const Uint samplerName = stateSamplerObject ? stateSamplerObject->GetExternalIndex() : 0u; - // THE HANDLE COMES FROM THE CALLER, NOT FROM THIS TWIN'S REGISTRY, and the - // difference is the seam D's verification round found on the integrated tree. - // - // v2 asked g_backendSamplerObjects.HandleOf(object) - the twin's own identity - // handle, minted off the frontend lifetime id - and looked the record up by it. - // That can only ever miss: a SamplerCso is CONTENT-ADDRESSED on the client - // (D-F1, ID-14/ID-17), its handles come out of MGPipeSlots().Allocate keyed on a - // parameter hash, and nothing ever emits a create_sampler_state at an identity - // handle. On the integrated tree every glBindSampler-driven parameter set was - // therefore refused and the driver sampler kept its defaults - two integration - // scenarios red, and the twin's own comment below (content-addressed, the - // record's Serial is the authority) already said why it could not work. - // - // The carried fact is MGPipeApplier().BoundSamplerStates[unit], written by the - // client at bind_sampler_states; the caller that knows the unit passes it as - // `pushedCso`. See the declaration for why the parameter is push-only. - if (!MG_Pipe::MGPipeHandleIsNull(pushedCso)) { - const auto* record = PipeSamplerCsoRecordForHandle(pushedCso); - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: sampler %u has no applier record on the handle arm, so its " - "parameters cannot be pushed (handle {%u, %u})", - samplerName, pushedCso.Slot, pushedCso.Gen); - return; - } - if (m_isInitialized && m_syncedSamplerSerial != 0 && m_syncedSamplerSerial == record->Serial) { - MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.", - samplerName); - return; - } - m_syncedSamplerSerial = record->Serial; - pushedParams = &record->Params; - } else { - // P13: every sampler twin is a CSO twin, synced with its handle - // (ResolveSamplerCsoTwin); a sampler with no record has nothing to push. - MGLOG_E_ONCE("MGPipe: sampler %u was synced without a SamplerCso handle", samplerName); - return; - } - - MGLOG_D("Syncing sampler with backend ID %u to backend for state ID %u", m_backendSamplerId, samplerName); - - const SamplerParameters& samplerParams = *pushedParams; - -#define SYNC_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \ - if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \ - g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, glName, \ - (GLint)MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \ - m_cacheSamplerParameters.internalName = samplerParams.internalName; \ - } - - if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter || - m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) { - g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, GL_TEXTURE_MIN_FILTER, - (GLint)ResolveBackendMinFilter( - samplerParams, - ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe())); - m_cacheSamplerParameters.minFilter = samplerParams.minFilter; - m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode; - } - if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) { - g_GLESFuncs.glSamplerParameteri( - m_backendSamplerId, GL_TEXTURE_MAG_FILTER, - (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None)); - m_cacheSamplerParameters.magFilter = samplerParams.magFilter; - } - - SYNC_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode) - SYNC_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode) - SYNC_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode) - SYNC_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc) - SYNC_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode) - if (m_cacheSamplerParameters.minLod != samplerParams.minLod) { - g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MIN_LOD, samplerParams.minLod); - m_cacheSamplerParameters.minLod = samplerParams.minLod; - } - if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) { - g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); - m_cacheSamplerParameters.maxLod = samplerParams.maxLod; - } - if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { - if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { - g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT, - samplerParams.maxAnisotropy); - } - m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; - } - if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor || - m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI || - m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI || - m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) { - // Same gate as the texture-side border colour above, and the same reason for - // branching on the form: an integer border colour must reach the driver through - // glSamplerParameterIiv/Iuiv or an integer sampler reads the float's bit pattern - // back instead of the value. - if (g_GLESCapabilities.SupportsTextureBorderClamp) { - if (samplerParams.borderColorForm == BorderColorForm::Int && - g_GLESFuncs.glSamplerParameterIiv) { - const GLint borderColorArray[4] = { - samplerParams.borderColorI.x(), samplerParams.borderColorI.y(), - samplerParams.borderColorI.z(), samplerParams.borderColorI.w()}; - g_GLESFuncs.glSamplerParameterIiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, - borderColorArray); - } else if (samplerParams.borderColorForm == BorderColorForm::Uint && - g_GLESFuncs.glSamplerParameterIuiv) { - const GLuint borderColorArray[4] = { - samplerParams.borderColorUI.x(), samplerParams.borderColorUI.y(), - samplerParams.borderColorUI.z(), samplerParams.borderColorUI.w()}; - g_GLESFuncs.glSamplerParameterIuiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, - borderColorArray); - } else if (g_GLESFuncs.glSamplerParameterfv) { - const GLfloat borderColorArray[4] = { - samplerParams.borderColor.x(), samplerParams.borderColor.y(), - samplerParams.borderColor.z(), samplerParams.borderColor.w()}; - g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, - borderColorArray); - } - } - m_cacheSamplerParameters.borderColor = samplerParams.borderColor; - m_cacheSamplerParameters.borderColorI = samplerParams.borderColorI; - m_cacheSamplerParameters.borderColorUI = samplerParams.borderColorUI; - m_cacheSamplerParameters.borderColorForm = samplerParams.borderColorForm; - } -#undef SYNC_SAMPLER_PARAM_IF_CHANGED - m_isInitialized = true; - } - - void BackendSamplerObject::Bind(Uint unit) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (g_boundSamplersCache[unit] == this) return; - - g_GLESFuncs.glBindSampler(static_cast(unit), m_backendSamplerId); - g_boundSamplersCache[unit] = this; - } - - Uint BackendSamplerObject::GetBackendSamplerId() const { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - return m_backendSamplerId; - } - - void UnbindSampler(Uint unit) { - if (g_boundSamplersCache[unit] == nullptr) return; - - g_GLESFuncs.glBindSampler(static_cast(unit), 0); - g_boundSamplersCache[unit] = nullptr; - } - - Array g_boundSamplersCache; - TwinRegistry g_backendSamplerObjects; - - // P4a fable seam F-4. THE TWIN FOR A CONTENT-ADDRESSED SamplerCso HANDLE, keyed by that - // handle and synced from its record - see the declaration for why the identity-keyed - // lookup it replaces could never hit. - BackendSamplerObject* ResolveSamplerCsoTwin(MG_Pipe::MGPipeHandle cso) { - if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; - // THE RECORD FIRST, before the table is touched: a handle with no record is a seam - // defect (the client minted and named a CSO it never described, or evicted one a - // standing set still names), and adopting a slot for it would leave a twin that - // syncs nothing. The census stem is the sampler family's. - const auto* record = PipeSamplerCsoRecordForHandle(cso); - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} has no applier record on the handle arm, so no " - "driver sampler can be built for it and the unit keeps what it holds", - cso.Slot, cso.Gen); - return nullptr; - } - // The same slot table the identity twins live in: one allocator serves both handle - // families of this kind, so a content-addressed slot and an identity-minted slot can - // never coincide, and the generation discipline (forward = recycle, backward = - // refused) is what retires a twin whose CSO the client's LRU evicted and re-minted. - auto* slot = g_backendSamplerObjects.GetOrCreateByHandle(cso); - if (slot == nullptr) { - MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} cannot be adopted on the handle arm (the slot's " - "live generation is %u), so no driver sampler is built for it", - cso.Slot, cso.Gen, g_backendSamplerObjects.LiveGenAt(cso.Slot)); - return nullptr; - } - if (!*slot) *slot = MakeShared(); - // Serial-gated inside: a CSO whose record did not move since this twin last synced - // costs the record lookup above and one compare. - (*slot)->SyncToBackend(nullptr, cso); - return slot->get(); - } - } // namespace SamplerImpl - - namespace RenderbufferImpl { - BackendRenderbufferObject::BackendRenderbufferObject() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId); - m_contextGeneration = g_backendContextGeneration; - if (m_backendRBOId == 0) { - MGLOG_E_ONCE("Failed to generate renderbuffer object."); - MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); - } - } - - BackendRenderbufferObject::~BackendRenderbufferObject() { - if (InProcessTeardown()) { - return; // see InProcessTeardown(): the driver may be unloaded already - } - if (m_backendRBOId == 0) { - return; - } - // No driver-level renderbuffer-binding shadow exists (Bind() always issues the - // call), so there is nothing to scrub here - only the id to release. - if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) { - g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId); - } - m_backendRBOId = 0; - } - - void BackendRenderbufferObject::Bind() const { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, m_backendRBOId); - } - - // P4a (D-D2): the four values BackendRenderbufferObject::SyncToBackend allocates from, - // spelled once per arm. Macros for the reason the texture-parameter block gives: in the - // PULL build each expands to the pre-P4a expression at its original site, so that - // build's tokens and its symbol sizes are unchanged (D-P). - // - // ARCHITECTURE.md §4.1 keeps the renderbuffer an INDEPENDENT frontend class while giving - // it the same discriminated MGPResourceDesc a texture gets, and this is where that pays: - // the same four fields, read out of the same record type, with Desc.Target telling the - // applier which of the three per-kind tables the record lives in. -#define MGB_RBO_FORMAT \ - (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.InternalFormat) \ - : stateRBOObject->GetInternalFormat()) -#define MGB_RBO_WIDTH (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Width) : static_cast(stateRBOObject->GetWidth())) -#define MGB_RBO_HEIGHT \ - (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Height) : static_cast(stateRBOObject->GetHeight())) -#define MGB_RBO_SAMPLES \ - (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Samples) : static_cast(stateRBOObject->GetSamples())) - -#undef MGB_RBO_FORMAT -#undef MGB_RBO_WIDTH -#undef MGB_RBO_HEIGHT -#undef MGB_RBO_SAMPLES - - // P5e (fb, CONTRACT-P5E.md §5.4): the same allocation, keyed on the renderbuffer HANDLE. - // - // THE FOUR VALUES WERE ALREADY THE RECORD'S at P4a (D-D2) - what the object form still - // needed the frontend for was the LOOKUP, `HandleOf(stateRBOObject.get())`, a - // client-allocator probe on the apply thread. On this arm the handle arrives from the - // attachment surface that named it, so nothing is probed and nothing is dereferenced. - // An OVERLOAD rather than a changed signature (the monolith glue keeps its tokens). - void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - const MG_Pipe::MGPipeResourceRecord* const pushedRecord = - PipeRenderbufferRecordForHandle(renderbuffer); - if (pushedRecord == nullptr) { - MGLOG_E_ONCE("MGPipe: renderbuffer handle {%u, %u} has no applier record, so its " - "storage cannot be allocated from the pushed descriptor", - renderbuffer.Slot, renderbuffer.Gen); - return; - } - if (m_isInitialized && m_syncedResourceSerial != 0 && - m_syncedResourceSerial == pushedRecord->Serial) { - return; - } - - // A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3): the - // allocation below re-defines the storage behind the name the framebuffer twins - // already attached, and no record of THEIRS moves for it, so the attachment memo - // has to be re-armed the same way a texture re-mint arms it. The first allocation - // is not a redefinition. - if (m_isInitialized) { - ++FramebufferImpl::g_attachmentBackendIdGeneration; - } - - Bind(); - - const auto internalFormat = static_cast(pushedRecord->Desc.InternalFormat); - const Int width = static_cast(pushedRecord->Desc.Width); - const Int height = static_cast(pushedRecord->Desc.Height); - const Int samples = static_cast(pushedRecord->Desc.Samples); - GLenum glInternalFormat, glType, glFormat; - TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType); - - // Drain first, for the object form's reason: a driver that refuses the allocation - // (a multi-gigabyte renderbuffer is refused routinely) used to leave the twin - // claiming storage it does not have, and the attachment then rendered nowhere. - DebugImpl::ErrorLopper::Clear(); - if (samples > 0) { - const auto backendSamples = static_cast(ClampSamplesToBackendSupport( - GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples)); - g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat, - static_cast(width), - static_cast(height)); - } else { - g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast(width), - static_cast(height)); - } - if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) { - MGLOG_E_ONCE("Renderbuffer {%u, %u} storage allocation ran out of memory: %dx%d, " - "samples=%d, format=%s", - renderbuffer.Slot, renderbuffer.Gen, width, height, samples, - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); - // NO RecordError HERE. The object form reports the OOM to the application - // through the live GLContext; on this arm there is no application on this side - // of the wire to report it to, and gPipeInputs->RecordError is itself one of the - // BARRIER-PULLED rows (FieldOwnership.def) that rule F forbids an unbarriered - // apply to touch. The client raises its own errors from its own validator. - } - DebugImpl::ErrorLopper::Clear(); - - m_cacheInternalFormat = internalFormat; - m_cacheWidth = width; - m_cacheHeight = height; - m_cacheSamples = samples; - m_syncedResourceSerial = pushedRecord->Serial; - m_isInitialized = true; - } - - TwinRegistry - g_backendRenderbufferObjects; - } // namespace RenderbufferImpl - - // ===================================================================================== - // P5e SEAMS: DECLARED AND REFUSED HERE, BODIED BY THE FAMILY PACKAGES - // (docs/archive/contracts/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature - // in c0e so both sides compile from day one") - // ===================================================================================== - // - // WHY THEY ARE ALL IN ONE BLOCK AND WHY THE BODIES ABORT. P5e lands as eight packages in - // eight parallel worktrees, and five of them are on opposite sides of these calls: tx2 - // writes SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle while fb's - // attachment sync and image sweep CALL them; fb writes the two SyncToBackendByHandle - // overloads while tx2's detach walk needs the reverse index behind them; id rekeys the - // registries the three resolvers read. A package that had to add its own declaration would - // collide with the package that added the other half, which is the merge trap P4a's - // contract package was written to avoid. - // - // So the signature is fixed HERE, once, and each family's commit replaces a body. The body - // ABORTS BY NAME rather than returning null or doing nothing: a resolver that answered null - // would render a blank draw and a sync that did nothing would render stale pixels, and both - // would be green lanes. Nothing calls any of them at this commit - every call site is still - // on the frontend overload beside it - so the abort is a link-time seam, not a runtime one. - // - // A package REPLACES the body in place and deletes the matching comment; it does not add a - // second definition elsewhere, or the linker's answer depends on link order. - namespace { - } // namespace - - namespace TextureImpl { - // ---- P5e (tx2): THE TEXTURE FAMILY'S BY-HANDLE ENTRY, CONTRACT-P5E §5.2 -------------- - // - // SamplerImpl::ResolveSamplerCsoTwin is the shape (Managers.cpp, the sampler half was - // already run-ahead clean on its record arm and this is that shape brought to the texture - // half): RECORD FIRST, twin by GetOrCreateByHandle, three syncs driven from the record, - // no frontend touch and no allocator probe anywhere on the path. - // - // WHAT THE FRONTEND ENTRY PAYS AND THIS ONE DOES NOT. SyncTextureObjectToBackend takes a - // by-VALUE copy of the twin and does a second Find at the tail, because its registry's - // Find returns a reference INTO an open-addressed map that a NESTED sync (a view syncing - // its storage texture) can rehash out from under it. A slot-table entry is an ARRAY - // ELEMENT: a nested GetOrCreateByHandle can only grow the vector, so the answer is - // re-INDEXED at the tail rather than pinned by a refcount, and the copy is a refcount - // this arm does not pay. - // - // A null return is a NAMED decline and never a mint: no record (the client named a - // texture it never created, or one whose resource_destroy has already applied), or a slot - // whose live generation is ahead of the handle's. A caller binds nothing and the unit - // keeps what it holds. - SharedPtr& SyncTextureToBackendByHandle(MG_Pipe::MGPipeHandle texture, - Bool imageBindableStorageRequired) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // The one storage every decline arm can return a reference to. Never written. - static SharedPtr s_noTwin{}; - s_noTwin = nullptr; - - const auto* record = PipeTextureRecordForHandle(texture); - if (record == nullptr) { - MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle " - "arm, so no driver texture can be built for it and the caller keeps " - "what it holds", - texture.Slot, texture.Gen); - return s_noTwin; - } - auto* slot = g_backendTextureObjects.GetOrCreateByHandle(texture); - if (slot == nullptr) { - MGLOG_E_ONCE("MGPipe: texture {%u, %u} cannot be adopted on the handle arm (the slot's " - "live generation is %u), so no driver texture is built for it", - texture.Slot, texture.Gen, g_backendTextureObjects.LiveGenAt(texture.Slot)); - return s_noTwin; - } - if (!*slot) *slot = MakeShared(); - // THE HANDLE IS NOTED BEFORE ANY SYNC RUNS. It is what the three prologues read - // instead of probing the client allocator, and what selects the by-handle arm inside - // them, so it has to be in place before the first of them is called. - (*slot)->NotePushedSyncHandle(texture); - - const SharedPtr twin = *slot; - if (imageBindableStorageRequired) { - twin->RequireImageBindableStorageByHandle(texture, *record); - } - static const SharedPtr kNoFrontendTexture{}; - twin->SyncTextureParamsToBackend(kNoFrontendTexture); - twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture); - twin->SyncMipmapsToBackend(kNoFrontendTexture); - // The storage sync may RE-MINT the driver texture, which discards every parameter the - // two calls above just pushed - the frontend entry's argument, unchanged. - if (twin->NeedsParameterResync()) { - twin->SyncTextureParamsToBackend(kNoFrontendTexture); - twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture); - } - // ONE re-index, and only because a nested adopt may have GROWN the vector; nothing on - // this arm erases a live slot, so the entry itself cannot have gone. - auto* refreshed = g_backendTextureObjects.FindByHandle(texture); - if (refreshed != nullptr && *refreshed) { - return *refreshed; - } - s_noTwin = twin; - return s_noTwin; - } - - void BackendTextureObject::SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture) { - // The STORAGE half alone, for fb's attachment sync and the image sweep: an attachment - // needs its levels on the driver, not its sampler parameters (§5.4). The handle note - // is what routes the body below onto the record. - NotePushedSyncHandle(texture); - static const SharedPtr kNoFrontendTexture{}; - SyncMipmapsToBackend(kNoFrontendTexture); - } - - // P5e (tx2), CONTRACT-P5E §5.2. See the declaration for the clause-by-clause derivation - // and for why BOTH halves of each resync pair are read here. - Bool BackendTextureObject::IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res, - const MG_Pipe::MGPipeResourceRecord& record) const { - (void)res; - // P3b/P4b D3: THE TWO STORAGE CLAUSES ARE ASKED OF THE STORAGE RECORD, which is this - // record for every texture that owns its texels and the record named by Desc.ViewOf - // for a glTextureView. A view's own Serial is moved by nothing an upload does - the - // client keys every resource_subdata on the storage owner, and the applier moves that - // record's Serial and PendingUploads alone - so reading the view's here answered - // "clean" for every owner write after the view's first sample. The three PARAMETER - // clauses below stay this record's: a view has its own texture parameters and its own - // built-in sampler, which is the whole point of the name existing. See - // PipeTextureStorageRecordForRecord and CONTRACT-P5E.md §5.2. - const MG_Pipe::MGPipeResourceRecord* storage = PipeTextureStorageRecordForRecord(record); - if (storage == nullptr) return false; -#if MOBILEGL_BUILD_DISAGGREGATED - // Never clean while bound to a shared image: see IsDrawSyncClean. - if (m_sharedImage != nullptr) return false; -#endif - // SyncMipmapsToBackend's own early-out, verbatim. - if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != storage->Serial || - !storage->PendingUploads.empty()) { - return false; - } - // SyncTextureParamsToBackend's (ResolvePushedTextureParams' gate). - if (m_syncedParamsSerial != record.ParamsSerial || record.Params.ForceResync != 0 || - m_forceTextureParamsResync) { - return false; - } - // SyncBuiltinSamplerToBackend's (ResolvePushedBuiltinSampler's gate). A texture whose - // params have never been set has no built-in sampler to push and that prologue - // declines SILENTLY - so a zero ParamsSerial is clean here for the same reason, and - // the CSO lookup below is only made when there is a CSO to look up. - if (record.ParamsSerial != 0) { - const MG_Pipe::MGPipeHandle builtin = record.Params.BuiltinSampler; - if (m_syncedBuiltinSampler != builtin) return false; - const auto* cso = PipeSamplerCsoRecordForHandle(builtin); - if (cso == nullptr || m_syncedBuiltinSamplerSerial != cso->Serial) return false; - if (record.Params.SamplerResync != 0 || m_forceSamplerResync) return false; - } else if (m_forceSamplerResync) { - return false; - } - // The storage-kind restriction the frontend gate carries, from the descriptor: a - // buffer texture's backing store can move without any serial above noticing. - return static_cast(record.Desc.StorageKind) == TextureStorageType::Mipmap; - } - } // namespace TextureImpl - - // FramebufferImpl::BackendFramebufferObject::SyncToBackendByHandle and - // RenderbufferImpl::BackendRenderbufferObject::SyncToBackendByHandle LANDED with package fb - // (P5e); their bodies are beside the object forms they overload, where the attachment walk - // and the storage allocation they share can be read side by side. - - // PrgramImpl's seam is GONE, not stubbed: package pg wrote - // BackendProgramObjectImpl::SyncToBackendByHandle's real body beside the frontend overload - // it is the twin of (above, with the two ProgramArchiveSource heads). A second definition - // here would make the linker's answer depend on link order, which is the one failure this - // block's own comment was written to prevent. - } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp new file mode 100644 index 00000000..39509e18 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp @@ -0,0 +1,2949 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false; + + namespace BufferImpl { + namespace { + // GL_ARRAY_BUFFER redundant-bind cache (id 0 = unknown/none). + Uint g_boundArrayBufferId = 0; + Bool g_boundArrayBufferKnown = false; + + // Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see + // Managers.h). Resting state between operations is 0; scopes in the + // readback/upload paths bind what they need through the cache and + // return to 0, so a stale user PBO can never capture a later + // readback that meant to target client memory. + Uint g_boundPixelPackBufferId = 0; + Bool g_boundPixelPackBufferKnown = false; + Uint g_boundPixelUnpackBufferId = 0; + Bool g_boundPixelUnpackBufferKnown = false; + } // namespace + + // Bumped whenever the backend ES context is destroyed; resources with + // an older generation hold ids from a dead context. + Uint g_bufferContextGeneration = 1; + + namespace { + // Buffer-mutation epoch backing store (contract, mutation-site list and + // memory-ordering rules: Managers.h at the accessor declarations). + // Starts at 1 so the memo stamps' 0 means "never stamped". Atomic: + // frontend buffer ops may run on non-draw threads while the draw thread + // reads; the release-bump-AFTER-mutation / acquire-read-BEFORE-probes + // pairing makes a stamp taken against stale state impossible to consume. + std::atomic g_bufferMutationEpoch{1}; + + // Defined next to the indexed-binding shadow below; forward-declared so + // every glDeleteBuffers site in this namespace can scrub stale shadow + // entries (GL resets a deleted buffer's bindings - indexed and pixel + // pack/unpack alike - to 0, and a recycled name matching a stale shadow + // entry would otherwise false-skip the rebind). + void ScrubBufferBindingShadowsForId(Uint id); + + // Defined next to the same shadow, and the counterpart to the scrub above for a + // buffer whose STORE was re-specified rather than deleted: the binding survives - + // nothing unbound the id - but the extent the driver resolved for it at bind time + // does not. Marks those points unknown so the next sync issues a real + // glBindBufferBase/Range instead of skipping it. + void InvalidateIndexedBufferBindingShadowsForId(Uint id); + + // Resources whose owning BufferObject died; ids deleted at the next + // sync point with a current ES context. + Vector> g_deferredBufferReleases; + std::mutex g_deferredBufferReleasesMutex; + // Cheap emptiness probe so the per-draw drain can skip the mutex and + // context check when nothing was enqueued (the overwhelmingly common + // case). Written only under the mutex; read lock-free. + std::atomic g_hasDeferredBufferReleases{false}; + + // --- Buffer-storage pool (buffer-object recycling) ----------------------- + // Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers + // (which triggers the kgsl_sharedmem_free -> mmu_unmap -> smmu/power/bandwidth + // cascade that dominated per-frame driver cost). An id retired during frame N + // is handed back only once the GPU has completed frame N (fence watermark, see + // DirectGLES::CompletedFrameSerial), then reseeded in place with glBufferSubData + // (no glBufferData realloc). All GL access is on the ES-context-owning thread; + // the mutex only guards against off-thread deferred-release enrollment races. + struct PooledBuffer { + Uint id = 0; + SizeT size = 0; + Uint contextGeneration = 0; + Uint64 retireSerial = 0; + }; + UnorderedMap> g_bufferPool; + SizeT g_pooledBytes = 0; + std::mutex g_poolMutex; + constexpr SizeT kMaxPoolableBufferBytes = 8u * 1024u * 1024u; // bigger buffers: delete now + constexpr SizeT kMaxPoolBytes = 64u * 1024u * 1024u; // total pool budget + constexpr SizeT kMaxEntriesPerBucket = 32; + + Bool IsPoolable(const GLESBufferResource& r) { + // Require working fences: recycling is gated on the frame-completion + // watermark, which only advances if Present can insert/poll fences. + return g_GLESFuncs.glFenceSync != nullptr && g_GLESFuncs.glGetSynciv != nullptr && + r.id != 0 && !r.persistentMapped && !r.immutableStorage && + r.contextGeneration == g_bufferContextGeneration && r.storageInitialized && + r.storageSize > 0 && r.storageSize <= kMaxPoolableBufferBytes; + } + + // Retire a buffer id into the pool (owning thread; caller verified IsPoolable). + // Zeroes r.id to keep the single-owner invariant {live | deferred | pool}. + void EnrollIntoPool(GLESBufferResource& r) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) { + InvalidateArrayBufferBindingCache(); + } + // Pooling keeps the id alive (and thus any driver binding of it); + // drop to unknown rather than claiming the post-delete 0 state. + if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) || + (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) { + InvalidatePixelBufferBindingCaches(); + } + const std::lock_guard lock(g_poolMutex); + auto& bucket = g_bufferPool[r.storageSize]; + if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) { + ScrubBufferBindingShadowsForId(r.id); + g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool + r.id = 0; + return; + } + // +1: Present increments the serial at frame END, so during the frame + // now being built CurrentFrameSerial() reads (frame-1). A buffer used + // this frame is only GPU-done once THIS frame's fence (serial+1) signals. + bucket.push_back( + {r.id, r.storageSize, r.contextGeneration, DirectGLES::CurrentFrameSerial() + 1}); + g_pooledBytes += r.storageSize; + r.id = 0; + } + + // Hand back an idle pooled id of EXACTLY `size` whose GPU work is complete, + // else 0. Owning thread only. Drops stale-generation entries encountered. + Uint AcquireFromPool(SizeT size) { + const Uint64 completed = DirectGLES::CompletedFrameSerial(); + const std::lock_guard lock(g_poolMutex); + auto it = g_bufferPool.find(size); + if (it == g_bufferPool.end()) return 0; + auto& bucket = it->second; + for (SizeT i = bucket.size(); i-- > 0;) { // newest-first: hottest + most-likely-idle + PooledBuffer& e = bucket[i]; + if (e.contextGeneration != g_bufferContextGeneration) { + g_pooledBytes -= e.size; // dead-context id: drop, no GL + bucket[i] = bucket.back(); + bucket.pop_back(); + continue; + } + if (e.retireSerial <= completed) { + const Uint id = e.id; + g_pooledBytes -= e.size; + bucket[i] = bucket.back(); + bucket.pop_back(); + return id; + } + } + return 0; + } + + // A dead context's ring buffers: shared objects, so they outlive it; deleted at the next + // Present of any context of the same share-group generation. + struct OrphanRingBuffer { + Uint id = 0; + Uint contextGeneration = 0; + }; + std::mutex g_orphanRingBuffersMutex; + Vector& OrphanRingBuffers() { + static auto* orphans = new Vector(); + return *orphans; + } + + // Each set's generations start in a range of their own: a per-program slot cache keyed + // by (generation, offset) must never take one context's ring for another's. + std::atomic g_ringGenerationBases{0}; + } // namespace + + RingSet::RingSet() { + const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12; + ubo.store.generation = base; + unpack.store.generation = base; + upload.store.generation = base; + } + + RingSet::~RingSet() { + const std::lock_guard lock(g_orphanRingBuffersMutex); + for (const PersistentRing* ring : {&ubo, &unpack, &upload}) { + if (ring->store.id != 0) { + OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration}); + } + for (const auto& retired : ring->retired) { + if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration}); + } + } + } + + DirectGLES::PerNativeContext g_ringSets; + + namespace { + PersistentRing& UboRing() { return g_ringSets.Current().ubo; } + PersistentRing& UnpackRing() { return g_ringSets.Current().unpack; } + PersistentRing& UploadRing() { return g_ringSets.Current().upload; } + + // The ES context the ring's id/map belonged to is gone (or was never + // seen): drop every handle without GL calls and re-arm creation. The + // generation counter must survive the reset — frame serials also survive + // context recreation, so a restarted counter could revalidate a stale + // per-program slot cache against the new ring. + void ResetRingForNewContext(PersistentRing& ring) { + const Uint32 keptGeneration = ring.store.generation; + ring.store = {}; + ring.store.generation = keptGeneration; + ring.store.contextGeneration = g_bufferContextGeneration; + // Keep the ring's own alignment across the wipe: the slow path rounds a + // request with it BEFORE the store that would set it exists. + if (ring.fixedAlignment != 0) ring.store.alignment = ring.fixedAlignment; + ring.retired.clear(); + ring.frameMarks.clear(); + } + + // P3a: ONE body per helper, parameterised on where its host bytes and its + // extent come from, so the legacy arm and the handle arm of a push build share + // the three-tier flush, the respecify and the range upload rather than owning a + // copy each - a second copy of the tier ladder is exactly how a tier changes in + // silence. The #else below is the PULL build, whose text must stay byte-identical + // to the pre-P3a one (G1: the pull build gains no symbol and resizes none); it is + // frozen by that gate and retires with the pull path at P13. + // (Re)specify backend storage from the shadow copy: glBufferData. + // The orphaning point - the ES driver performs the actual rename. + // TODO(buffer-pool Phase 2): orphan-on-respecify is NOT yet implemented. + // When the current id is BUSY (lastUseFrameSerial > CompletedFrameSerial()) + // && !persistentMapped && !noOrphan, express the orphan as an id-swap + // (retire the busy id into the pool, bind a fresh/pooled id) instead of the + // in-place glBufferData below, to avoid the driver's own rename/stall. Not + // pursued yet: glBufferData/glBufferSubData currently sit below profiler + // noise, so respecify is not a hot path in the profiled scenes. + // The body both arms run. `size`, `usage`, `initialData` and `syncedSerial` are the + // four things the legacy arm reads off the frontend object and the handle arm reads + // off the applier's stored descriptor and the shadow base the call carried; nothing + // else in here differs, so there is ONE glBufferData and one extent rule rather + // than two that can drift apart. + void RespecifyStorageWith(GLESBufferResource& resource, SizeT size, GLenum usage, + const void* initialData, Uint64 syncedSerial) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // Read BEFORE the fields below are overwritten: whether this respecify changes + // the store's EXTENT is what decides if the indexed-binding shadow still + // describes the driver. + const Bool extentChanged = !resource.storageInitialized || resource.storageSize != size; + BindBufferId(TempBufferTarget, resource.id); + g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage); + if (MG_Util::PipeStats::Enabled() && initialData != nullptr) { + // An ORPHANING respecify passes NULL and moves nothing, which is exactly + // why the test is on initialData rather than on size. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(size)); + } + resource.storageSize = size; + resource.storageInitialized = true; + resource.pendingRespecify = false; + resource.pendingRanges.clear(); + resource.pendingResidentWrites.clear(); + resource.syncedChangeSerial = syncedSerial; + // A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a + // rebind whenever the shadow already records this id at that index - so on a + // driver that resolves a whole-buffer indexed binding's extent at BIND time + // (Adreno does; Mali does not) the shader keeps seeing the old, smaller range: + // stores past it are dropped and loads return zero. Forget what the shadow + // claims for this id so the next SyncBufferBindingPoints issues the bind for + // real. Only when the extent actually moved: an orphaning respecify at the same + // size is Minecraft's per-frame hot path and its bindings are still exact. + if (extentChanged) { + InvalidateIndexedBufferBindingShadowsForId(resource.id); + } + } + + void RespecifyStorageNow(GLESBufferResource& resource, BufferObject& bufferObject) { + const SizeT size = bufferObject.GetSize(); + // An orphaning respecify (glBufferData with NULL, content never + // written since) stays a pure NULL reallocation: the driver renames + // the store without a stall and nothing is transferred. Uploading + // the stale shadow here turned Minecraft-style orphaning into a + // full-size synchronized upload. + const void* initialData = + (size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr; + RespecifyStorageWith(resource, size, MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage()), + initialData, bufferObject.GetChangeSerial()); + } + + Bool StorageMatchesSize(const GLESBufferResource& resource, SizeT size) { + return resource.storageInitialized && !resource.pendingRespecify && resource.storageSize == size; + } + + Bool StorageMatches(const GLESBufferResource& resource, const BufferObject& bufferObject) { + return StorageMatchesSize(resource, bufferObject.GetSize()); + } + } // namespace + + // ------------------------------------------------------------------------------- + // R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Package v1. + // + // The rule, the evidence and the reason the storage is a side table rather than a + // member of GLESBufferResource are all in MG_Remote/Server/StagedShadow.h. This is + // only the instance and the four call sites. + // + // ONE PER PROCESS, and its copying arm is decided ONCE at first use: the two arms + // hold the authoritative bytes in DIFFERENT places, so an answer that changed + // mid-run would strand every resource already staged - the same reason + // ResolveResourceSubsystemArm latches (Managers.h). Leaked at exit like every other + // MG_Remote singleton (ID-8): ~BufferObject reaches the destroy path from exit + // handlers, after this TU's globals would already be gone. + // ------------------------------------------------------------------------------- + MG_Record::StagedShadowStore& ServerStaged() { + static MG_Record::StagedShadowStore& store = + *new MG_Record::StagedShadowStore( + MG_Config::Transport != MG_Config::TransportMode::Monolith); + return store; + } + + namespace { +#if MOBILEGL_BUILD_DISAGGREGATED + // ------------------------------------------------------------------------------- + // P11 B2 (T0) - WHEN AN IMPORTED STORE'S AHardwareBuffer MAY BE LET GO. + // + // A T0 store is the client's AHardwareBuffer imported with glBufferStorageExternalEXT + // (Ops_H_ImportExternal below). The twin holds its own reference, and every path that + // ends the store - a respecify, a destroy, a re-import - deletes the GL id and then + // RETIRES the reference here: a fence is inserted after the delete, and the reference is + // released only once that fence has signalled, i.e. once every command that could still + // read or write the pages has completed on this context's GPU. The client's own + // reference ends independently (it releases on respecify/destroy); the pages live until + // the last of the two. No link watermark is involved (F-L2): the fence is this server's. + // + // Swept (non-blocking) at every import, every deferred-release drain and every ensure; + // a context loss releases the lot, because the GPU work those fences named ended with + // the context. Apply thread only, like every GL call here. + // ------------------------------------------------------------------------------- + struct T0RetiredImport { + GLsync fence = nullptr; + void* ahb = nullptr; + Uint generation = 0; + }; + Vector g_t0Retired; + // T0 twins whose destroy found no current context: Ops_OnDestroy queued them on the + // deferred-release list and keeps a second reference here, so the reference is + // retired only after that drain deleted the id (ProcessDeferredT0Retires). Guarded by + // g_deferredBufferReleasesMutex, like the list it shadows. + Vector> g_t0DeferredDestroys; + std::atomic g_hasT0DeferredDestroys{false}; + Uint64 g_t0ImportsLive = 0; + + void SweepT0Retired(Bool contextGone) { + for (SizeT i = 0; i < g_t0Retired.size();) { + T0RetiredImport& retired = g_t0Retired[i]; + const Bool sameContext = !contextGone && retired.generation == g_bufferContextGeneration && + CanTouchGLNow(); + Bool done = contextGone || retired.generation != g_bufferContextGeneration || + retired.fence == nullptr; + if (!done && sameContext && g_GLESFuncs.glClientWaitSync) { + const GLenum status = g_GLESFuncs.glClientWaitSync(retired.fence, 0, 0); + done = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED || + status == GL_WAIT_FAILED; + } + if (!done) { + ++i; + continue; + } + if (retired.fence != nullptr && sameContext && g_GLESFuncs.glDeleteSync) + g_GLESFuncs.glDeleteSync(retired.fence); + MG_Remote::Transport::AdoptT0::ReleaseImported(retired.ahb); + retired = g_t0Retired.back(); + g_t0Retired.pop_back(); + } + } + + // Call AFTER the store's GL id is deleted (or known dead): the fence then follows every + // command that used it. Clears the twin's reference whatever the context state. + void RetireT0Import(GLESBufferResource& resource) { + if (resource.externalAhb == nullptr) return; + T0RetiredImport retired; + retired.ahb = resource.externalAhb; + retired.generation = resource.contextGeneration; + if (CanTouchGLNow() && resource.contextGeneration == g_bufferContextGeneration && + g_GLESFuncs.glFenceSync) { + retired.fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + resource.externalAhb = nullptr; + if (g_t0ImportsLive > 0) --g_t0ImportsLive; + g_t0Retired.push_back(retired); + SweepT0Retired(false); + } +#else + // No transport, no imported store: nothing ever needs retiring (P13 W5). + inline void RetireT0Import(GLESBufferResource&) {} +#endif // MOBILEGL_BUILD_DISAGGREGATED +#define MGL_SERVER_STAGED_DROP(res) ServerStaged().Drop(&(res)) +#define MGL_SERVER_STAGED_DROP_ALL() ServerStaged().DropAll() +#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) \ + ServerStaged().RequireCoverage(&(res), (base), (start), (end), (site)) + + // The host bytes a range upload/flush reads. On the legacy arm the frontend object's + // shadow; on the handle arm the base the last content-carrying call handed over. + void UploadRangeFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (start >= end || hostBase == nullptr) return; + MGL_SERVER_STAGED_REQUIRE(resource, hostBase, start, end, "upload_range"); + BindBufferId(TempBufferTarget, resource.id); + g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start), + hostBase + start); + } + + void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) { + UploadRangeFrom(resource, bufferObject.MappedData(), start, end); + } + Bool RingAvailable(PersistentRing& ring); + + // True when a pending-range flush can go through the staging ring right + // now: kill switch off, the ES copy entry point resolved, and the ring's + // own availability gate (EXT_buffer_storage + fences + live context) up. + Bool UploadRingUsableNow() { + if (MG_Config::Features.EsprytDisableUploadRing) return false; + if (!g_GLESFuncs.glCopyBufferSubData) return false; + return RingAvailable(UploadRing()); + } + + // A partial range below this goes through the staging ring instead of a + // range-invalidating map: the map's page-substitution fast path needs a + // sizeable (page-coverable) range to engage, and below it the driver + // falls back to waiting out the WAR hazard on the CPU. + constexpr SizeT kInvalidateRangeMinBytes = 128u * 1024u; + // The push arm's copy of this threshold lives in Managers.h, where a unit case can + // reach it (InvalidateFlushAccessFor). Two constants, one value, and the compiler + // is what keeps them one: the pull arm's FlushPendingRangesNow is byte-frozen + // against 5cb826b0 (ID-15), so the constant it reads may not move to a header. + static_assert(kInvalidateRangeMinBytes == kEsprytInvalidateRangeMinBytes, + "the two arms of the three-tier ladder must use the same tier-1 threshold"); + } // namespace + void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize, + SizeT hostBaseFrom, SizeT hostBaseTo) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + VecRange1D ranges; + { + const std::lock_guard lock(resource.pendingMutex); + if (resource.pendingRanges.empty()) return; + ranges = std::move(resource.pendingRanges); + resource.pendingRanges.clear(); + } + // No shadow to read from: only reachable on the handle arm, where the base + // arrives with the call, and only for a resource that queued a range before any + // content-carrying call named one. The queue is already drained, so the next + // full re-upload is what puts the store right. + if (hostBase == nullptr) return; + // Clamp against BOTH extents: the readback flush may run while the + // frontend size and the backend store disagree (a pending respecify + // resolves that later; bytes past either end have nowhere to land). + const SizeT limit = std::min(frontendSize, resource.storageSize); + const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush && + g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer; + const Bool ringUsable = UploadRingUsableNow(); + for (const auto& range : ranges) { + const SizeT end = std::min(range.end, limit); + const SizeT start = std::min(range.start, end); + const SizeT size = end - start; + if (size == 0) continue; + if (MG_Util::PipeStats::Enabled()) { + // Counted once per queued range, before the three delivery shapes + // below diverge: all three move exactly these bytes, and it is the + // byte count - not the shape - that sizes SEG_STAGE. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(size)); + } + // The invalidating map's fast path is SHAPE-dependent on this Mali + // driver: a whole-buffer invalidation renames the store outright, + // and a large range gets fresh pages - but a small unaligned range + // of a busy store makes the map WAIT (osup_sync_object_wait, ~9% + // of a Minecraft 26.3 replay). So: whole buffer -> orphan-map; + // large range -> range-invalidating map; small range -> the staged + // ring copy, whose worst case (a whole-destination ghost) is only + // ever the small destination itself. + // + // The map covers EXACTLY the queued range: only those bytes are the + // shadow's to rewrite. Widening to page bounds looked free and was + // not - the widened bytes clobbered GPU-written data (an SSBO + // counter beside the app's SubData) with the stale shadow. + // The extent the bytes behind `hostBase` are actually good for, clamped + // to the store. With the default it IS [start, end), so the refusal below + // cannot fire and the ladder is unchanged; with a real SEG_STAGE snapshot + // it is the snapshot's window and a disagreement drops this range to the + // staging ring instead of letting a widened INVALIDATE_RANGE declare bytes + // dead that nothing is about to rewrite. + const SizeT coveredFrom = hostBaseFrom > start ? hostBaseFrom : start; + const SizeT coveredTo = hostBaseTo < end ? hostBaseTo : end; +#if MOBILEGL_PIPE_VERIFY + if (coveredFrom != start || coveredTo != end) { + MGLOG_E_ONCE("MGPipe: Fatal{StageSnapshotTooNarrow} flush_pending_ranges: the " + "staged bytes cover [%zu, %zu) and the queued range is [%zu, %zu) " + "- tiers 2 and 3 would copy from outside the snapshot", + hostBaseFrom, hostBaseTo, start, end); + } +#endif + // Read per range, like the pull arm: a ring copy staged for an earlier range of + // this same drain puts every later partial range of the store behind it + // (InvalidateFlushAccessFor's queued-copy rule, dev f973008c). + const Bool ringCopyInFlight = resource.ringCopyRetireSerial > DirectGLES::CompletedFrameSerial(); + const GLbitfield access = + mapUsable ? InvalidateFlushAccessFor(start, end, coveredFrom, coveredTo, limit, + resource.storageSize, ringCopyInFlight) + : 0u; + if (access != 0) { + BindBufferId(TempBufferTarget, resource.id); + void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start, + (GLsizeiptr)size, access); + if (dst) { + Memcpy(dst, hostBase + start, size); + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + continue; + } + } + SizeT ringOffset = 0; + if (ringUsable && size <= kUploadRingMaxBytes && + RingAllocate(UploadRing(), size, ringOffset)) { + Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size); + BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); + BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); + g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, + (GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size); + // Commands recorded now retire with the fence the next Present inserts. + resource.ringCopyRetireSerial = DirectGLES::CurrentFrameSerial() + 1; + } else { + UploadRangeFrom(resource, hostBase, start, end); + } + } + } + + namespace { + // THE TWO-ARM SHAPE, AND BOTH ARMS ARE HASHED FROM HERE ON (ID-15, which + // supersedes ID-13's "FlushPendingRangesNow is defined exactly once, outside any + // `#if`"). + // + // This file has TWO three-tier drains, and exactly one of them is compiled into any + // given build: `FlushPendingRangesFrom` above, inside `#if MOBILEGL_PIPE_PUSH`, is + // what a PUSH build runs - both of its call sites, the legacy one and + // Ops_H_Readback, reach it - and `FlushPendingRangesNow` inside the `#else` below, + // byte-identical to 5cb826b0, is what a PULL build runs. A push build compiles no + // FlushPendingRangesNow at all. + // + // NO FORWARDER HERE, which is what forced the two-arm shape: G5's extractor is + // preprocessor-blind, so a `FlushPendingRangesNow` that forwarded onto the ladder + // above would leave TWO definitions of that one name in the file and the gate would + // exit 2 - a gate that cannot run - rather than compare anything. (The handle arm + // cannot call FlushPendingRangesNow itself either: its signature takes a + // BufferObject&, and having no frontend object to offer is the whole point of the + // conversion.) + // + // WHAT ID-15 ADDS. The shape above is sound, but a gate keyed on the NAME + // `FlushPendingRangesNow` alone would from here on protect only the text the + // shipping build never compiles: a tier-threshold or map-access-bit edit made in + // FlushPendingRangesFrom - the ladder that actually runs, and the one MC 26.3's p99 + // depends on - would satisfy both G1 (the pull text did not move) and G5 (the pull + // name still hashes the same). So scripts/p3a_untouched_regions.sh hashes BOTH + // names: eleven functions, the pull ladder compared against the P3a base ref and + // this one against a sha pinned at 3e298c9a. Neither ladder may drift, and neither + // may drift AWAY FROM THE OTHER without the gate saying so. + + // P15: the small-store write rename (WholeStoreRenameApplies, Managers.h), applied to the + // queue BEFORE the drain takes it. The ladder above is pinned and is not touched: it is + // handed one whole-store range, which its own tier 1 already answers with the + // whole-buffer orphan-map. Draw path only - the readback drains keep their exact ranges. + void RenameSmallStoreForDrain(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize) { + const SizeT store = resource.storageSize; + const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush && + g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer; + const Bool foreignBytes = resource.serverGpuWritten || resource.persistentMapped || + resource.immutableStorage || resource.externalAhb != nullptr; + // The size gate first, with the whole store as the queue: it answers every refusal + // that does not depend on the queue before any lock is taken. + if (!WholeStoreRenameApplies(store, frontendSize, store, hostBase != nullptr, foreignBytes, mapUsable)) + return; + // Under split the host bytes are the server's staged copy, which only holds what the + // content records delivered; monolith's base is the frontend shadow, the whole store. + if (ServerStaged().CopiesIntoServerStorage() && !ServerStaged().IsCovered(&resource, 0, store)) return; + const std::lock_guard lock(resource.pendingMutex); + if (resource.pendingRanges.empty() || !resource.pendingResidentWrites.empty()) return; + SizeT queuedBytes = 0; + for (const auto& range : resource.pendingRanges) { + const SizeT end = std::min(range.end, store); + queuedBytes += end - std::min(range.start, end); + } + if (!WholeStoreRenameApplies(store, frontendSize, queuedBytes, true, false, true)) return; + resource.pendingRanges.clear(); + resource.pendingRanges.Add({0, store}); + } + } // namespace + + // Land the app bytes queued for an ADOPTED store on the GPU timeline: staged + // into the upload ring and delivered by glCopyBufferSubData. The destination + // is the IMMUTABLE persistent store, which the driver can neither rename nor + // ghost, so the copy is plain job ordering - after every in-flight reader, + // before the next consumer - which is exactly glBufferSubData's contract. + // (The in-place host write these bytes replaced tore the frames still + // reading the old vertex data: one-frame wrong geometry during fast camera + // movement.) Fallback: direct glBufferSubData - the adopted store carries + // DYNAMIC_STORAGE, and immutability again forbids the whole-store ghost. + // + // It never read the frontend object (the bytes are on the resource's own queue and + // the extent is the backend store's), so it takes none: the handle arm calls exactly + // this function with exactly these semantics. + void DrainResidentWritesNow(GLESBufferResource& resource) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + Vector writes; + { + const std::lock_guard lock(resource.pendingMutex); + if (resource.pendingResidentWrites.empty()) return; + writes = std::move(resource.pendingResidentWrites); + resource.pendingResidentWrites.clear(); + } + const SizeT limit = resource.storageSize; + const Bool ringUsable = UploadRingUsableNow(); + for (const auto& write : writes) { + if (write.offset >= limit) continue; + const SizeT size = std::min(write.bytes.size(), limit - write.offset); + if (size == 0) continue; + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(size)); + } + SizeT ringOffset = 0; + if (ringUsable && size <= kUploadRingMaxBytes && + RingAllocate(UploadRing(), size, ringOffset)) { + Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size); + BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); + BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); + g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, + (GLintptr)ringOffset, (GLintptr)write.offset, + (GLsizeiptr)size); + } else { + BindBufferId(TempBufferTarget, resource.id); + g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)write.offset, (GLsizeiptr)size, + write.bytes.data()); + } + } + } + + namespace { + // EXT_buffer_storage bit values (same numeric values as the desktop ARB + // tokens); defined locally so this compiles regardless of which GLES headers + // expose the EXT tokens. + constexpr GLbitfield kMapPersistentBit = 0x0040; + constexpr GLbitfield kMapCoherentBit = 0x0080; + constexpr GLbitfield kDynamicStorageBit = 0x0100; + + void Ops_OnDestroy(SharedPtr&& resource) { + if (!resource) return; + auto* glesResource = static_cast(resource.get()); + if (glesResource->contextGeneration != g_bufferContextGeneration) { + glesResource->id = 0; // id belonged to a destroyed context + RetireT0Import(*glesResource); // P11 B2: its GL store died with that context + return; + } + if (CanTouchGLNow()) { + if (IsPoolable(*glesResource)) { + EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers + return; + } + if (glesResource->id != 0) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { + InvalidateArrayBufferBindingCache(); + } + ScrubBufferBindingShadowsForId(glesResource->id); + g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); + glesResource->id = 0; + } + // P11 B2: a T0 store's AHardwareBuffer goes only after a fence behind the delete. + RetireT0Import(*glesResource); + return; + } + const std::lock_guard lock(g_deferredBufferReleasesMutex); +#if MOBILEGL_BUILD_DISAGGREGATED + if (glesResource->externalAhb != nullptr) { // P11 B2: retired after the drain deletes it + g_t0DeferredDestroys.push_back(resource); + g_hasT0DeferredDestroys.store(true, std::memory_order_release); + } +#endif + g_deferredBufferReleases.push_back(std::move(resource)); + g_hasDeferredBufferReleases.store(true, std::memory_order_release); + } + // ResourceContentIsDeclared MOVED OUT OF THIS ANONYMOUS NAMESPACE (P3b/P4b espryt + // D1 slice 1) to sit beside RequireStagedCoverage at namespace scope: the XFB + // scatter in DirectGLES.cpp has to ask the same M-3 question this file's pool-reuse + // ladder asks, and the two must not be able to answer it differently. + + void Ops_H_Create(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) { + (void)res; + (void)desc; + // Nothing, and that is the row D-A2 writes: storage is defined lazily by the + // first resource_respecify, and the ensure path already tolerates a resource + // with none. Minting the twin here would only move the allocation earlier. + } + + void Ops_H_Respecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, + const void* initialBytes) { + auto* resource = FindBufferResourceForHandle(res); + // P13 W4a (§1.4): ON MONOLITH'S RECORD ARM THE TWIN IS MINTED HERE, not lazily at + // the first draw. The record arm's readers have no frontend object to re-read a + // base from, so the base this call carries - the client's shadow, for an orphaning + // respecify too (PipeFill passes it there on this arm) - is the only way they learn + // it. Minting is GL-free; the storage stays lazy. + const Bool aliasesFrontend = MG_Config::RecordArmAliasesFrontend(); + if (resource == nullptr && aliasesFrontend && initialBytes != nullptr) { + resource = GetOrCreateBufferResourceForHandle(res); + } + if (resource != nullptr) { + // A respecify's companion pointer IS the shadow base (offset 0) - and an + // ORPHANING one (glBufferData with NULL) carries HasDefinedContent clear and + // therefore no pointer at all. That is also the call that RESIZES the + // client's shadow (PipeResource::ResizeShadow is reserve + resize, so a grow + // past the reserve reallocates and frees the block a previous call's base + // pointed into), so the recorded base is dead exactly when no new one + // arrives. FORGET it here rather than leave a pointer nothing refreshes: + // every reader below treats a null base as "no bytes to move", which is the + // honest answer, and the ensure path re-reads the live base from the + // frontend object it still holds. + // P13 W4a: an ALIASING store records the base whether or not the content is + // defined - the pointer is the frontend shadow's new allocation either way, and + // the upload below stays gated on the descriptor (RespecifyStorageWith's + // initialData), so an orphan still uploads nothing. + if ((desc.HasDefinedContent != 0 || aliasesFrontend) && initialBytes != nullptr) { + // R-11: in monolith this is the client's shadow base, unchanged; under + // split it is copied into server-owned storage first. A respecify's + // companion pointer is the base at offset 0 and covers the whole store. + // Under split it is ALWAYS null (contract table 1 row 19 - + // initialBytes does not cross, and the content arrives as + // resource_subdata records right behind this record), so in practice + // this arm is the monolith one and the else arm is the split one. + resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, static_cast(desc.Width), + initialBytes, 0, + static_cast(desc.Width)); + } else { + MGL_SERVER_STAGED_DROP(*resource); + resource->hostBytes = nullptr; + } + // P8-C: whatever a shader wrote belonged to the store this call replaces. + resource->serverGpuWritten = false; + resource->contentSinceRespecify = false; + } + if (!resource) return; // lazy: the ensure path full-uploads on creation + if (resource->immutableStorage) { + resource->persistentMapped = false; + resource->persistentPtr = nullptr; + if (resource->id != 0 && CanTouchGLNow() && + resource->contextGeneration == g_bufferContextGeneration) { + NoteBufferIdDeleted(resource->id); + // Frontend VAO bindings survive respecification; force their + // backend twins to bind the replacement buffer name. (dev@d7655247, + // carried into this arm as well: the handle arm duplicates the + // immediate retire path, so a fix that lands in only one of the two + // leaves the bug alive on whichever arm the operator selects.) + ++g_bufferBackendIdGeneration; + g_GLESFuncs.glDeleteBuffers(1, &resource->id); + resource->id = 0; + resource->immutableStorage = false; + } + // P11 B2: a respecified T0 store ends here on the server's side too. Its + // AHardwareBuffer is released only after a fence behind the delete (or at once + // when the store's context is already gone) - never under a frame in flight, + // however the client's own release (at this respecify) raced it. + RetireT0Import(*resource); + resource->storageInitialized = false; + resource->storageSize = 0; + resource->pendingRespecify = true; + resource->pendingRanges.clear(); + resource->pendingResidentWrites.clear(); + return; + } + if (!CanTouchGLNow() || resource->id == 0 || + resource->contextGeneration != g_bufferContextGeneration) { + resource->pendingRespecify = true; + resource->pendingRanges.clear(); + resource->pendingResidentWrites.clear(); + return; + } + if (desc.Width == 0) { + resource->storageInitialized = false; + resource->storageSize = 0; + resource->pendingRespecify = false; + resource->pendingRanges.clear(); + resource->pendingResidentWrites.clear(); + return; + } + // Same orphaning rule, expressed on the descriptor: a NULL-data respecify has + // HasDefinedContent clear and the client sends no bytes for it. + const void* initialData = desc.HasDefinedContent != 0 ? initialBytes : nullptr; + RespecifyStorageWith(*resource, static_cast(desc.Width), + MG_Util::ConvertBufferUsageToGLEnum(static_cast(desc.Usage)), + initialData, ResourceSerialOf(res)); + } + + void Ops_H_SubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, const void* bytes) { + const SizeT offset = static_cast(MG_Pipe::MGPipeSubDataBufferOffset(record)); + const SizeT size = static_cast(MG_Pipe::MGPipeSubDataBufferSize(record)); + auto* resource = FindBufferResourceForHandle(res); + // R-11 / ID-52 item 3: under an ACTIVE TRANSPORT this record's bytes are the ONLY + // delivery of the buffer's content - the client sends no companion pointer and the + // server has no MappedData to fall back on. But the twin is created LAZILY at the + // first draw (Ops_H_Create is a no-op by D-A2), which is AFTER this record, so a + // subdata that finds no twin would return here and the bytes would be lost - the + // draw then uploads an empty/shifted store (TriangleScenario read a blue triangle + // before this). So under split the subdata MINTS the twin it needs to stage into. + // Monolith is untouched: the twin stays lazy there because the frontend object's + // MappedData is the source and nothing is lost by deferring the allocation (D-A2). + if (resource == nullptr && bytes != nullptr) { + resource = GetOrCreateBufferResourceForHandle(res); + } + if (!resource) return; + // P11 B2: A T0 STORE TAKES A SERVER-ORIGINATED WRITE GPU-ORDERED. Its pages are the + // client's AHardwareBuffer, so there is no staged copy to refresh and the adopted + // early return below ("the coherent map already has the bytes") is false for bytes + // the SERVER produced - P9 W1's pack-buffer landing (PipeApplier.cpp + // LandReadbackInBuffer) is the one producer, and it would lose its pixels there. They + // are queued exactly as a resident SubData is (Ops_H_ResidentSubData): landed after + // every command already recorded, before the next one that reads the store. + if (resource->externalAhb != nullptr && bytes != nullptr && size != 0) { + { + const std::lock_guard lock(resource->pendingMutex); + auto& write = resource->pendingResidentWrites.emplace_back(); + write.offset = offset; + const auto* source = static_cast(bytes); + write.bytes.assign(source, source + size); + } + resource->syncedChangeSerial = ResourceSerialOf(res); + return; + } + // P13 W4b: A SERVER-ORIGINATED WRITE INTO AN ALIASING STORE (monolith's record arm) + // lands in the GL store, never in the base: the bytes are the sink's scratch, and the + // base is the frontend's shadow, which this side does not write. Queued uploads land + // first, so they cannot later overwrite these bytes from the stale shadow; the store + // is then marked GPU-written, which sends the frontend (its own pack-buffer mark) and + // every CPU reader here (SplitHostBytesForCpuRead) back to the GL store for them. + if (MG_Pipe::MGPipeApplyingServerOriginatedWrite() && MG_Config::RecordArmAliasesFrontend() && + bytes != nullptr && size != 0) { + // The store must exist to take them: a pack buffer is often never drawn from, so + // nothing else has built it. Only a store that does not exist yet is ensured: the + // applier has already counted THIS write into the record's serial, so the ensure + // of a built store would read it as an untracked mutation and re-upload the + // shadow over the runs this landing wrote before it. + if (CanTouchGLNow() && (resource->id == 0 || !resource->storageInitialized || + resource->pendingRespecify || + resource->contextGeneration != g_bufferContextGeneration)) { + resource = EnsureBufferResourceForHandle(nullptr, res); + } + if (resource != nullptr && CanTouchGLNow() && resource->id != 0 && resource->storageInitialized && + !resource->persistentMapped && !resource->pendingRespecify && + resource->contextGeneration == g_bufferContextGeneration && + StorageMatchesSize(*resource, ResourceWidthOf(res))) { + FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res)); + BindBufferId(TempBufferTarget, resource->id); + g_GLESFuncs.glBufferSubData(TempBufferTarget, static_cast(offset), + static_cast(size), bytes); + // The store holds everything the record now says (the applier bumped its + // serial for this write): without the stamp the next ensure would see a + // newer serial and re-upload the shadow over the bytes that just landed. + resource->syncedChangeSerial = ResourceSerialOf(res); + } else if (resource != nullptr) { + const std::lock_guard lock(resource->pendingMutex); + auto& write = resource->pendingResidentWrites.emplace_back(); + write.offset = offset; + const auto* source = static_cast(bytes); + write.bytes.assign(source, source + size); + } + if (resource != nullptr) resource->serverGpuWritten = true; + return; + } + // M-2: UNDER pendingMutex, because this line runs BEFORE the CanTouchGLNow() + // test below - i.e. on the arm D-A2 deliberately keeps reachable off the render + // thread - while every reader of hostBytes (Ops_H_Readback's drain, the + // kill-switch map arm of Ops_H_FlushRange, the ensure path, the fp64 narrowing) + // is on the render thread. It was the one member of this struct that the + // off-thread path touched with neither a lock nor an atomic: pendingRanges and + // pendingResidentWrites are under this mutex and syncedChangeSerial is an + // std::atomic read with acquire. The lock it is put under is deliberately THAT + // one and not a new one - the base and the queued range are one fact ("these + // bytes, at this base"), the drain takes this mutex to lift the ranges, and a + // drain that sees a range therefore sees the base that range was queued + // against. The store is kept on both arms rather than restricted to the + // CanTouchGLNow() one because Ops_H_Readback can drain with no draw in between, + // and the ensure path's republication (the 3c55e027 fix) is what runs at a draw. + if (bytes != nullptr) { + const std::lock_guard lock(resource->pendingMutex); + // R-11: MONOLITH keeps the client's base; SPLIT copies [offset, offset+size) + // into server-owned storage and points hostBytes at that. Inside the SAME + // lock as the queued range, because the base and the range are one fact + // ("these bytes, at this base") and the drain takes this mutex to lift them. + resource->hostBytes = + MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size); + if (size != 0) resource->contentSinceRespecify = true; + } + if (resource->pendingRespecify) return; // full re-upload pending anyway + if (!CanTouchGLNow() || resource->id == 0 || + resource->contextGeneration != g_bufferContextGeneration || + !StorageMatchesSize(*resource, ResourceWidthOf(res))) { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.Add({offset, offset + size}); + return; + } + // The adopted zero-copy store already HAS the bytes; a driver upload here would + // re-synchronize what coherent mapping made free. + if (resource->persistentMapped && resource->persistentPtr) { + resource->syncedChangeSerial = ResourceSerialOf(res); + return; + } + if (MG_Config::Features.EsprytDisableUploadRing) { + UploadRangeFrom(*resource, resource->hostBytes, offset, offset + size); + resource->syncedChangeSerial = ResourceSerialOf(res); + return; + } + // The Mali WAR-stall fix, unchanged: queue and let draw-time sync stage the + // merged ranges through the upload ring. + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.Add({offset, offset + size}); + } + + void Ops_H_ResidentSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, + const void* bytes) { + const SizeT offset = static_cast(MG_Pipe::MGPipeSubDataBufferOffset(record)); + const SizeT size = static_cast(MG_Pipe::MGPipeSubDataBufferSize(record)); + auto* resource = FindBufferResourceForHandle(res); + if (!resource || size == 0 || bytes == nullptr) return; + // DELIBERATELY not stored as hostBytes: these are the application's staging + // store and are valid for the duration of the call only (BufferObject.h:84-85), + // which is exactly why they are COPIED here rather than referenced later. + const std::lock_guard lock(resource->pendingMutex); + auto& write = resource->pendingResidentWrites.emplace_back(); + write.offset = offset; + const auto* source = static_cast(bytes); + write.bytes.assign(source, source + size); + } + + void Ops_H_FlushRange(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record, + const void* bytes) { + auto* resource = FindBufferResourceForHandle(res); + if (!resource) return; + const SizeT start = static_cast(record.Offset); + const SizeT end = start + static_cast(record.Size); + // M-2, the second store site - same lock, same reason as Ops_H_SubData's, and + // the same R-11 copy. UNDER SPLIT `bytes` IS ALWAYS NULL HERE by ruling (C-6 / + // contract table 1 row 20: resource_flush_range carries no bytes at all - the + // ladder it drives rewrites its range from the authoritative shadow, which rule + // C makes server-owned, so resource_subdata is already the only way bytes reach + // it and a second carrier would be a forgeable way to say the same thing). So + // this arm keeps whatever the preceding subdata records staged, which is + // exactly what the ladder must read. + if (bytes != nullptr) { + const std::lock_guard lock(resource->pendingMutex); + resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, + start, end - start); + if (end > start) resource->contentSinceRespecify = true; + } + if (resource->pendingRespecify) return; + if (!CanTouchGLNow() || resource->id == 0 || + resource->contextGeneration != g_bufferContextGeneration || + !StorageMatchesSize(*resource, ResourceWidthOf(res))) { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.Add({start, end}); + return; + } + if (resource->persistentMapped && resource->persistentPtr) { + resource->syncedChangeSerial = ResourceSerialOf(res); + return; + } + if (!MG_Config::Features.EsprytDisableUploadRing) { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.Add({start, end}); + return; + } + // The kill-switch arm, and it reads the application's REAL flags per call - + // which is why MGPFlushRange carries them unnormalised. + const Flags appAccess{ + static_cast>(record.AccessFlags)}; + const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) || + (appAccess & BufferMappingAccessBit::InvalidateBuffer); + const Bool unsynchronized = static_cast(appAccess & BufferMappingAccessBit::Unsynchronized); + if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized) && + resource->hostBytes != nullptr) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // The kill-switch arm's own Memcpy, and the one that passes + // GL_MAP_INVALIDATE_RANGE_BIT - i.e. the one that tells the driver the old + // bytes are dead. Bytes outside the staged coverage are exactly the ones + // that claim is false for. + MGL_SERVER_STAGED_REQUIRE(*resource, resource->hostBytes, start, end, + "flush_range_invalidate_map"); + BindBufferId(TempBufferTarget, resource->id); + void* mappedData = g_GLESFuncs.glMapBufferRange( + TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start), + GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) | + (unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0)); + if (mappedData) { + Memcpy(mappedData, resource->hostBytes + start, end - start); + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + resource->syncedChangeSerial = ResourceSerialOf(res); + return; + } + MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData", + resource->id); + } + UploadRangeFrom(*resource, resource->hostBytes, start, end); + resource->syncedChangeSerial = ResourceSerialOf(res); + } + + void Ops_H_Readback(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) { + // An empty capture may conservatively request a writeback before + // any draw materialized this buffer. Resolve its staged resource + // exactly as a GPU consumer would, then return the real bytes. + auto* resource = EnsureBufferResourceForHandle(nullptr, res); + if (!resource || resource->id == 0 || !resource->storageInitialized) return; + if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return; + if (resource->persistentMapped) { + // Queued resident SubData bytes land first (GPU-ordered), then the finish + // makes them - and any shader writes already queued on this context - + // visible through the coherent mapping the reads use. There is no backend + // copy to read back in this case. + DrainResidentWritesNow(*resource); + if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); + return; + } + if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return; + // The map below starts at record.Offset where the legacy arm mapped at literal + // zero, so the clamp has to be against what is left of the BACKEND store past + // that offset: the applier's BufferRangeFault checked the range against + // Desc.Width, and a store that is smaller than the descriptor (a respecify the + // backend has not applied yet) would otherwise be mapped past its end. + const SizeT readOffset = static_cast(record.Offset); + if (readOffset >= resource->storageSize) return; + const SizeT size = + std::min(static_cast(record.Size), resource->storageSize - readOffset); + if (size == 0) return; + + // Queued app writes must land in the backend store before it is read back, or + // the writeback below would revert them in the shadow. Under split every queued + // range must lie inside the server shadow's staged coverage first (R-11 / the + // M-6 ruling): the drain's tier-1 map would otherwise declare live GPU bytes + // dead over a range nothing staged. + MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, resource->hostBytes, ResourceWidthOf(res), + "readback_flush_pending"); + FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res)); + + BindBufferId(TempBufferTarget, resource->id); + void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)record.Offset, + static_cast(size), GL_MAP_READ_BIT); + if (mapped == nullptr) { + MGLOG_E_ONCE("Ops_H_Readback: glMapBufferRange(read) failed for buffer %u", resource->id); + return; + } + // The reverse channel replaces BufferObject::WritebackFromBackend: the backend + // no longer reaches into the frontend's address space, it ANSWERS. In monolith + // the client's implementation is one call away, so SyncGpuWrites' caller still + // sees the reconciled shadow on return, exactly as before. + // + // THE ORDER FROM HERE IS A CORRECTNESS RULE, NOT A PREFERENCE + // (ARCHITECTURE.md 8.2: "写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 + // server 应用;反向通道需要与正向通道相同的有序保证"): + // 1. writeback - the client's shadow takes the bytes, + // 2. unmap - the read mapping goes away, + // 3. serial stamp- syncedChangeSerial catches up with the record, so the next + // draw does not re-upload the readback over itself, + // 4. epoch bump - in Ops_H_ReadbackTracked, i.e. strictly AFTER 1-3, so a + // draw-clean memo re-probed by the bump can never observe a + // half-reconciled resource. The bump must NEVER move earlier. + if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) { + MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( + res, record.Offset, + MG_Pipe::MGPBlobRef{reinterpret_cast(mapped), static_cast(size), + MG_Pipe::kMGHostSpanSegNone, 0}); + } else { + MGLOG_E_ONCE("Ops_H_Readback: no reverse channel is installed, so the GPU-written bytes of " + "buffer %u cannot reach the client shadow", + resource->id); + } + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + // The shadow now matches the backend byte for byte; without this the next draw + // would see a newer serial and re-upload the readback over it. + resource->syncedChangeSerial = ResourceSerialOf(res); + // P13 W4a: on monolith's record arm the base this twin reads IS the shadow the + // writeback just filled, so a whole-store readback is also the refresh + // SplitHostBytesForCpuRead would owe. (A copying store's copy is not the + // client's shadow, and keeps its mark.) + if (MG_Config::RecordArmAliasesFrontend() && readOffset == 0 && size >= ResourceWidthOf(res)) { + resource->serverGpuWritten = false; + } + } + + void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) { + // R-11's server copy dies with the resource, and BEFORE the twin leaves the + // table - the side map is keyed by the twin's address, so this is the last + // moment that address can be looked up. + if (GLESBufferResource* dying = FindBufferResourceForHandle(res); dying != nullptr) { + MGL_SERVER_STAGED_DROP(*dying); + } + // The twin comes OUT of the table first, so the three outcomes below are + // reached with the entry already retired. The SLOT is the client's to free, + // after this returns (D-L). + SharedPtr twin = g_backendBufferResources.ReleaseByHandle(res); + // Verbatim Ops_OnDestroy: stale generation -> zero the id; on-thread -> + // IsPoolable/EnrollIntoPool else scrub + glDeleteBuffers; off-thread -> + // g_deferredBufferReleases plus the lock-free flag. + Ops_OnDestroy(std::move(twin)); + } + + // D-E: the ONLY two changes are the signature and the three reads that went + // through the frontend object (GetSize() -> size, MappedData() -> seedBytes, + // SetBackendResource(...) -> the slot table's GetOrCreate). Every other statement + // is byte-identical to Ops_AcquirePersistentMap above - the four-way capability + // gate, the stale-generation wipe BEFORE the new stamp, the idempotency hit, the + // fresh-id sequence with ++g_bufferBackendIdGeneration, immutableStorage set as + // soon as the store exists, the MGLOG_E_ONCE decline and the success stamps. + void* Ops_H_MapPersistent(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) { + if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || + !g_GLESFuncs.glGenBuffers) { + return nullptr; + } + if (size == 0) return nullptr; + + auto* resource = GetOrCreateBufferResourceForHandle(res); + if (!resource) return nullptr; + // Before the generation is stamped, not after: everything on the resource + // describes a context that is gone, and the idempotency check below would + // otherwise hand the caller the dead context's mapped pointer. + if (resource->contextGeneration != g_bufferContextGeneration) { + resource->id = 0; + resource->persistentMapped = false; + resource->persistentPtr = nullptr; + resource->immutableStorage = false; + resource->storageInitialized = false; + resource->storageSize = 0; + } + resource->contextGeneration = g_bufferContextGeneration; + + if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) { + return resource->persistentPtr; // idempotent + } + + // Need a fresh id: glBufferStorage fails on a buffer that already has + // immutable storage, and any prior mutable store is replaced anyway. + if (resource->id != 0) { + NoteBufferIdDeleted(resource->id); + // Driver VAOs may have this id baked into attribute/element bindings + // keyed on versions this re-mint does not move. + ++g_bufferBackendIdGeneration; + g_GLESFuncs.glDeleteBuffers(1, &resource->id); + resource->id = 0; + resource->immutableStorage = false; + } + g_GLESFuncs.glGenBuffers(1, &resource->id); + if (resource->id == 0) return nullptr; + + // Seed from the shadow (the client's bytes are still live at this point: it + // adopts and drops them only after this returns). + BindBufferId(TempBufferTarget, resource->id); + const void* initial = seedBytes; + g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(size), initial, + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit | + kDynamicStorageBit); + // Set as soon as the store exists, not once the map succeeds: the failure + // path below leaves this id holding immutable storage, and whoever touches + // it next has to know that glBufferData cannot redefine it. + resource->immutableStorage = true; + void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); + if (!ptr) { + MGLOG_E_ONCE("Ops_H_MapPersistent: glMapBufferRange(persistent) failed for buffer %u", + resource->id); + resource->persistentMapped = false; + resource->persistentPtr = nullptr; + return nullptr; + } + resource->persistentPtr = ptr; + resource->persistentMapped = true; + // The ONE statement here that is not in Ops_AcquirePersistentMap, and it is + // about a member the legacy arm does not have rather than about the map: the + // client adopts this pointer the instant we return it and + // PipeResource::AdoptPersistentMap then does m_shadow->clear() + + // shrink_to_fit(), so the shadow base any earlier content-carrying call + // recorded is a FREED allocation from here on. The live bytes are persistentPtr. + // R-11's server copy goes with it: the bytes are the coherent map's now, and a + // stale server shadow would answer a later drain with pre-map content. + MGL_SERVER_STAGED_DROP(*resource); + resource->hostBytes = nullptr; + resource->storageSize = static_cast(size); + resource->storageInitialized = true; + resource->pendingRespecify = false; + { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.clear(); + resource->pendingResidentWrites.clear(); + } + resource->syncedChangeSerial = ResourceSerialOf(res); + return ptr; + } + + void Ops_H_UnmapPersistent(MG_Pipe::MGPipeHandle res) { + (void)res; + // P3a emits this from nowhere: the donation is permanent for the life of the + // store and is ended by the respecify / destroy paths, which already retire + // the immutable id. The entry exists so the transport has both halves of the + // pair (D-E), and giving it a body that unmapped a live coherent store would + // be the one thing D-B4 forbids. + } + + // The epoch-tracking wrappers, duplicated for this table - same contract as the + // seven above, including AcquirePersistentMap's bump EVEN ON DECLINE (the client + // still enters a persistent map the per-draw probes must start seeing). + void Ops_H_RespecifyTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, + const void* initialBytes) { + Ops_H_Respecify(res, desc, initialBytes); + BumpBufferMutationEpoch(); + } + void Ops_H_SubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, + const void* bytes) { + Ops_H_SubData(res, record, bytes); + BumpBufferMutationEpoch(); + } + void Ops_H_ResidentSubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, + const void* bytes) { + Ops_H_ResidentSubData(res, record, bytes); + BumpBufferMutationEpoch(); + } + void Ops_H_FlushRangeTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record, + const void* bytes) { + Ops_H_FlushRange(res, record, bytes); + BumpBufferMutationEpoch(); + } + void Ops_H_ReadbackTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) { + Ops_H_Readback(res, record); + BumpBufferMutationEpoch(); + } + void Ops_H_DestroyTracked(MG_Pipe::MGPipeHandle res) { + Ops_H_Destroy(res); + BumpBufferMutationEpoch(); + } + void* Ops_H_MapPersistentTracked(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) { + void* result = Ops_H_MapPersistent(res, size, seedBytes); + // Bump even on decline: the client still enters a persistent map the per-draw + // probes must start seeing. + BumpBufferMutationEpoch(); + return result; + } + +#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) + // ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A STORE'S RESIDENT TWIN ------------ + // + // The split twin of Ops_H_MapPersistent: where a monolith map_persistent mints a + // coherent persistent store (glBufferStorageEXT) and hands its pointer to the frontend, + // a T0 map_persistent IMPORTS the pages the client already holds and maps - + // glBufferStorageExternalEXT on eglGetNativeClientBufferANDROID(ahb) - and the result + // is the SAME resource shape (immutable, persistentMapped, persistentPtr = this + // server's coherent map of those pages), so every adopted-store path in this file + // applies unchanged: draws bind it with nothing to upload, resident SubData lands + // GPU-ordered (DrainResidentWritesNow), a readback drains and waits and posts no + // bytes. The R-11 staged copy is dropped: the pages are the client's. + // + // THE ENTRY POINTS ARE OPTIONAL: neither is in the loader's tables (an EGL function + // missing there is MGLOG_F), and eglGetProcAddress hands back live-looking stubs, so + // the extension strings decide and the pointers are only then taken. + using T0GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*); + using T0BufferStorageExternalFn = void (*)(GLenum, GLintptr, GLsizeiptr, EGLClientBuffer, GLbitfield); + T0GetNativeClientBufferFn g_t0GetNativeClientBuffer = nullptr; + T0BufferStorageExternalFn g_t0BufferStorageExternal = nullptr; + constexpr GLbitfield kT0MapReadBit = 0x0001; + constexpr GLbitfield kT0MapWriteBit = 0x0002; + constexpr GLbitfield kT0StorageFlags = + kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit | kDynamicStorageBit; + constexpr GLbitfield kT0MapFlags = kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit; + + Bool ResolveT0EntryPoints(String& why) { + if (g_t0GetNativeClientBuffer != nullptr && g_t0BufferStorageExternal != nullptr) return true; + if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay || + !g_GLESFuncs.glGetString || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers) { + why = "the loader has no eglGetProcAddress/eglQueryString/glGetString/glMapBufferRange"; + return false; + } + const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS); + const char* gl = reinterpret_cast(g_GLESFuncs.glGetString(GL_EXTENSIONS)); + if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) { + why = "EGL_ANDROID_get_native_client_buffer is not advertised"; + return false; + } + if (!HasExtensionWord(gl, "GL_EXT_external_buffer") || !HasExtensionWord(gl, "GL_EXT_buffer_storage")) { + why = "GL_EXT_external_buffer / GL_EXT_buffer_storage is not advertised"; + return false; + } + g_t0GetNativeClientBuffer = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID")); + g_t0BufferStorageExternal = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("glBufferStorageExternalEXT")); + if (g_t0GetNativeClientBuffer == nullptr || g_t0BufferStorageExternal == nullptr) { + why = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / glBufferStorageExternalEXT"; + g_t0GetNativeClientBuffer = nullptr; + g_t0BufferStorageExternal = nullptr; + return false; + } + return true; + } + + void DrainGLErrors() { + if (!g_GLESFuncs.glGetError) return; + for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) { + } + } + + // A fresh GL buffer whose storage IS `ahb`, mapped persistent + coherent. 0 on failure + // (with `why`); nothing on the resource is touched here. `throwaway` = the POST's own + // context is current: bind raw and keep Espryt's binding caches (which describe the + // session's context, whose name space this is not) out of it. + Uint ImportAhbAsBuffer(void* ahb, Uint64 size, void** outPtr, String& why, Bool throwaway = false) { + *outPtr = nullptr; + const EGLClientBuffer clientBuffer = + g_t0GetNativeClientBuffer(static_cast(ahb)); + if (clientBuffer == nullptr) { + why = "eglGetNativeClientBufferANDROID returned null"; + return 0; + } + Uint id = 0; + g_GLESFuncs.glGenBuffers(1, &id); + if (id == 0) { + why = "glGenBuffers gave no name"; + return 0; + } + DrainGLErrors(); + if (throwaway) g_GLESFuncs.glBindBuffer(TempBufferTarget, id); + else BindBufferId(TempBufferTarget, id); + g_t0BufferStorageExternal(TempBufferTarget, 0, static_cast(size), clientBuffer, + kT0StorageFlags); + const GLenum stored = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR; + void* ptr = stored == GL_NO_ERROR + ? g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), kT0MapFlags) + : nullptr; + if (ptr == nullptr) { + char text[160]; + std::snprintf(text, sizeof(text), "glBufferStorageExternalEXT -> 0x%04x, map %s", stored, + stored == GL_NO_ERROR ? "failed" : "not attempted"); + why = text; + if (!throwaway) { + NoteBufferIdDeleted(id); + ++g_bufferBackendIdGeneration; + } + g_GLESFuncs.glDeleteBuffers(1, &id); + return 0; + } + *outPtr = ptr; + return id; + } + + Bool T0Answer(char* out, Uint64 outBytes, Bool ok, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + std::vsnprintf(out, static_cast(outBytes), fmt, args); + va_end(args); + return ok; + } + + // Installs an imported store on the twin. The caller has retired whatever was there. + void InstallT0Store(GLESBufferResource& resource, Uint id, void* ptr, Uint64 size) { + resource.id = id; + resource.immutableStorage = true; + resource.persistentPtr = ptr; + resource.persistentMapped = true; + resource.storageSize = static_cast(size); + resource.storageInitialized = true; + resource.pendingRespecify = false; + resource.contextGeneration = g_bufferContextGeneration; + const std::lock_guard lock(resource.pendingMutex); + resource.pendingRanges.clear(); + resource.pendingResidentWrites.clear(); + } + + Bool Ops_H_ImportExternal(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) { + SweepT0Retired(false); + if (!CanTouchGLNow() || size == 0 || ahb == nullptr) return false; + String why; + if (!ResolveT0EntryPoints(why)) { + MGLOG_W_ONCE("Espryt T0: no import on this context - %s", why.c_str()); + return false; + } + auto* resource = GetOrCreateBufferResourceForHandle(res); + if (resource == nullptr) return false; + // IMPORT FIRST, retire second: a refused import leaves the T2 store (and its staged + // copy) exactly as it was, so the DECLINED the client gets leaves nothing to repair. + void* ptr = nullptr; + const Uint id = ImportAhbAsBuffer(ahb, size, &ptr, why); + if (id == 0) { + MGLOG_W("Espryt T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen, + static_cast(size), why.c_str()); + return false; + } + if (resource->contextGeneration == g_bufferContextGeneration && resource->id != 0) { + NoteBufferIdDeleted(resource->id); + // Driver VAOs may have the old id baked into bindings keyed on versions this + // replacement does not move (the Ops_H_MapPersistent re-mint's rule). + ++g_bufferBackendIdGeneration; + g_GLESFuncs.glDeleteBuffers(1, &resource->id); + } + resource->id = 0; + RetireT0Import(*resource); // a previous import of this handle, fenced + InstallT0Store(*resource, id, ptr, size); + MG_Remote::Transport::AdoptT0::AcquireImported(ahb); + resource->externalAhb = ahb; + ++g_t0ImportsLive; + // The bytes are the client's pages now; R-11's copy would answer a later drain + // with pre-import content. + MGL_SERVER_STAGED_DROP(*resource); + resource->hostBytes = nullptr; + resource->syncedChangeSerial = ResourceSerialOf(res); + MGLOG_D("Espryt T0: imported {slot=%u, gen=%u} as buffer %u (%llu bytes); %llu live imports", + res.Slot, res.Gen, id, static_cast(size), + static_cast(g_t0ImportsLive)); + return true; + } + + Bool Ops_H_ImportExternalTracked(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) { + const Bool imported = Ops_H_ImportExternal(res, ahb, size); + BumpBufferMutationEpoch(); + return imported; + } + + // The POST's second round: the CPU writes region 1 through the held lock only now, after + // the import and a GPU use of it, and reads region 4 (the first round's fill) back into + // its cache; a second dispatch copies region 1 out and overwrites region 4. Appends its + // counts to `detail`. + Bool SelfTestExternalSteady(GLuint imported, GLuint out, void* heldPtr, char* detail, SizeT detailBytes) { + constexpr Uint32 kWords = 1024; + constexpr Uint32 kSteadyAtWord = kWords; // region 1 + constexpr Uint32 kFillAtWord = 4 * kWords; // region 4 + auto* held = static_cast(heldPtr); + Uint32 cached = 0; + for (Uint32 i = 0; i < kWords; ++i) { + held[kSteadyAtWord + i] = 0xA5000000u ^ (i * 40503u); + cached ^= held[kFillAtWord + i]; + } + (void)cached; + static const char* kSource = + "#version 310 es\n" + "layout(local_size_x = 64) in;\n" + "layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n" + "layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n" + "void main() {\n" + " uint i = gl_GlobalInvocationID.x;\n" + " outb.w[i] = ext.w[1024u + i];\n" + " ext.w[4096u + i] = 0x3D5EA11Eu ^ i;\n" + "}\n"; + const GLuint program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource); + GLint linked = 0; + if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); + const SizeT used = std::strlen(detail); + if (!linked) { + if (program != 0) g_GLESFuncs.glDeleteProgram(program); + std::snprintf(detail + used, detailBytes - used, "; steady: the second program did not link"); + return false; + } + g_GLESFuncs.glUseProgram(program); + g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported); + g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out); + g_GLESFuncs.glMemoryBarrier(0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/); + g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1); + g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | + 0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/); + g_GLESFuncs.glFinish(); + Uint32 steadyRead = 0, steadySaw = 0; + for (Uint32 i = 0; i < kWords; ++i) + if (held[kFillAtWord + i] == (0x3D5EA11Eu ^ i)) ++steadySaw; + g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out); + const auto* copied = static_cast( + g_GLESFuncs.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT)); + if (copied != nullptr) { + for (Uint32 i = 0; i < kWords; ++i) + if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead; + g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); + } + g_GLESFuncs.glDeleteProgram(program); + std::snprintf(detail + used, detailBytes - used, + "; with the import in use, GPU read %u/%u words the CPU wrote after it, GPU write " + "seen through the held lock %u/%u words", + steadyRead, kWords, steadySaw, kWords); + return steadyRead == kWords && steadySaw == kWords; + } + + // THE POST (B2, the standing rule: decide by probe, never by driver name). The AHB API + // promises CPU coherence only across an unlock/lock; T0 keeps the client's lock for the + // store's life, so the pattern it depends on is exactly what this exercises, on this + // context's own driver, before kCapAdoptT0 may be published: + // hold - a 64 KiB BLOB allocated and locked by this test, never unlocked while it runs; + // read - this server's own host map of the import reads the pattern the CPU wrote + // through the held pointer (every hostBytes fallback reads persistentPtr); + // gpu - a compute dispatch copies that pattern out of the import (the GPU read) and + // fills another region of it (the GPU write); + // back - after glFinish the fill is read through the HELD pointer and the copy is + // read back from the dispatch's output; + // steady - the same again with bytes the CPU wrote AFTER the import and its first GPU + // use (VkBufferManager::SelfTestWireImport says why the first round is not enough). + // In a throwaway ES 3.1 pbuffer context, so none of this context's state or Espryt's + // binding caches is disturbed; the caller's bind is restored before returning. + Bool Ops_H_SelfTestExternal(char* whyOut, Uint64 whyBytes) { + const auto answer = [&](Bool ok, const char* text, const char* arg = "") { + return T0Answer(whyOut, whyBytes, ok, text, arg); + }; + String why; + if (!ResolveT0EntryPoints(why)) return answer(false, "%s", why.c_str()); + if (!g_EGLFuncs.eglChooseConfig || !g_EGLFuncs.eglCreateContext || !g_EGLFuncs.eglMakeCurrent || + !g_EGLFuncs.eglCreatePbufferSurface || !g_GLESFuncs.glCreateShaderProgramv || + !g_GLESFuncs.glDispatchCompute || !g_GLESFuncs.glMemoryBarrier || !g_GLESFuncs.glFinish) + return answer(false, "the loader lacks the EGL/GLES 3.1 entry points the test needs"); + const EGLDisplay display = g_EGLFuncs.eglGetCurrentDisplay(); + const EGLContext savedContext = g_EGLFuncs.eglGetCurrentContext(); + const EGLSurface savedDraw = g_EGLFuncs.eglGetCurrentSurface(EGL_DRAW); + const EGLSurface savedRead = g_EGLFuncs.eglGetCurrentSurface(EGL_READ); + const EGLint configAttribs[] = {EGL_RENDERABLE_TYPE, 0x0040 /*EGL_OPENGL_ES3_BIT*/, EGL_SURFACE_TYPE, + EGL_PBUFFER_BIT, EGL_NONE}; + EGLConfig config = nullptr; + EGLint configs = 0; + if (!g_EGLFuncs.eglChooseConfig(display, configAttribs, &config, 1, &configs) || configs < 1) + return answer(false, "no ES3 pbuffer config for the throwaway context"); + const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(display, config, surfaceAttribs); + const EGLint contextAttribs[] = {0x3098 /*EGL_CONTEXT_MAJOR_VERSION*/, 3, 0x30FB /*MINOR*/, 1, EGL_NONE}; + const EGLContext context = surface != EGL_NO_SURFACE + ? g_EGLFuncs.eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs) + : EGL_NO_CONTEXT; + const auto restore = [&] { + g_EGLFuncs.eglMakeCurrent(display, savedDraw, savedRead, savedContext); + if (context != EGL_NO_CONTEXT) g_EGLFuncs.eglDestroyContext(display, context); + if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(display, surface); + }; + if (context == EGL_NO_CONTEXT || !g_EGLFuncs.eglMakeCurrent(display, surface, surface, context)) { + restore(); + return answer(false, "the throwaway ES 3.1 context could not be made current"); + } + + constexpr Uint64 kBytes = 64 * 1024; + constexpr Uint32 kWords = 1024; // one 4 KiB region + constexpr Uint32 kFillAtWord = 4 * kWords; // region 4 + MG_Remote::Transport::AdoptT0::HeldStore held; + Vector pattern(kBytes / 4); + for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast(i * 2654435761u); + if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why)) { + restore(); + return answer(false, "hold: %s", why.c_str()); + } + Bool ok = false; + GLuint out = 0, program = 0; + void* hostMap = nullptr; + const GLuint imported = ImportAhbAsBuffer(held.ahb, kBytes, &hostMap, why, /*throwaway=*/true); + char detail[512] = {}; + if (imported == 0) { + std::snprintf(detail, sizeof(detail), "import: %s", why.c_str()); + } else if (std::memcmp(hostMap, pattern.data(), kWords * 4) != 0) { + std::snprintf(detail, sizeof(detail), "read: the server's host map of the import does not " + "hold the CPU's pattern"); + } else { + static const char* kSource = + "#version 310 es\n" + "layout(local_size_x = 64) in;\n" + "layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n" + "layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n" + "void main() {\n" + " uint i = gl_GlobalInvocationID.x;\n" + " outb.w[i] = ext.w[i];\n" + " ext.w[4096u + i] = 0x6C0FFEE0u ^ i;\n" + "}\n"; + program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource); + GLint linked = 0; + if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); + g_GLESFuncs.glGenBuffers(1, &out); + g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out); + g_GLESFuncs.glBufferData(GL_SHADER_STORAGE_BUFFER, kWords * 4, nullptr, GL_DYNAMIC_READ); + if (!linked) { + std::snprintf(detail, sizeof(detail), "gpu: the test's compute program did not link"); + } else { + g_GLESFuncs.glUseProgram(program); + g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported); + g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out); + g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1); + g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | + 0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/); + g_GLESFuncs.glFinish(); + Uint32 gpuRead = 0, heldSaw = 0; + const auto* heldWords = static_cast(held.ptr); + for (Uint32 i = 0; i < kWords; ++i) + if (heldWords[kFillAtWord + i] == (0x6C0FFEE0u ^ i)) ++heldSaw; + g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out); + const auto* copied = static_cast(g_GLESFuncs.glMapBufferRange( + GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT)); + if (copied != nullptr) { + for (Uint32 i = 0; i < kWords; ++i) + if (copied[i] == pattern[i]) ++gpuRead; + g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); + } + ok = gpuRead == kWords && heldSaw == kWords; + std::snprintf(detail, sizeof(detail), + "64 KiB AHB, lock held throughout: host map read ok, GPU read %u/%u words of " + "the CPU's pattern, GPU write seen through the held lock %u/%u words", + gpuRead, kWords, heldSaw, kWords); + if (ok) ok = SelfTestExternalSteady(imported, out, held.ptr, detail, sizeof(detail)); + } + } + if (program != 0) g_GLESFuncs.glDeleteProgram(program); + if (out != 0) g_GLESFuncs.glDeleteBuffers(1, &out); + if (imported != 0) g_GLESFuncs.glDeleteBuffers(1, &imported); + g_GLESFuncs.glFinish(); + restore(); + MG_Remote::Transport::AdoptT0::ReleaseHeld(held); + return answer(ok, "%s", detail); + } +#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__ + } // namespace + + // ---- P5c (tx): the TEXTURE half of the resource family, staged server-side -------- + // + // The buffer half's R-11 pattern (ServerStaged() above), for texture levels: the + // staged bytes of resource_subdata's texture half are adopted into the server's + // StagedTextureStore AT APPLY TIME, because `bytes` names SEG_STAGE and is dead the + // moment the record retires (rule C). The store, its ownership and its coverage + // rules are documented in MG_Remote/Server/StagedTextureStore.h; these are only the + // three hook bodies. All three are no-ops in monolith (CopiesIntoServerStorage() + // false), which is what keeps the monolith expression character for character. + // + // NOTHING here mints or reads a texture twin: the store is keyed by the wire handle + // the record carried (StagedTextureStore.h explains why not the twin address), so + // adoption needs no GL call and no frontend object. + + // GetUploadTargets() answered from the descriptor's Target: one static list per + // target, matching the frontend classes' own lists member for member + // (TextureObject*.h / TextureObjectStubs.h: a cube is six faces, a cube array is + // the single CubeMapArray target, everything else its own one). + const Vector& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget) { + static const Vector kUnknown{}; + static const Vector kTex1D{TextureUploadTarget::Texture1D}; + static const Vector kTex2D{TextureUploadTarget::Texture2D}; + static const Vector kTex3D{TextureUploadTarget::Texture3D}; + static const Vector kTex1DArray{TextureUploadTarget::Texture1DArray}; + static const Vector kTex2DArray{TextureUploadTarget::Texture2DArray}; + static const Vector kTexCube{ + TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX, + TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY, + TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ}; + static const Vector kTexCubeArray{TextureUploadTarget::CubeMapArray}; + static const Vector kTex2DMS{TextureUploadTarget::Texture2DMultisample}; + static const Vector kTex2DMSArray{TextureUploadTarget::Texture2DMultisampleArray}; + static const Vector kTexRect{TextureUploadTarget::TextureRectangle}; + static const Vector kTexBuffer{TextureUploadTarget::TextureBuffer}; + switch (static_cast(pipeResourceTarget)) { + case MG_Pipe::MGPipeResourceTarget::Tex1D: return kTex1D; + case MG_Pipe::MGPipeResourceTarget::Tex2D: return kTex2D; + case MG_Pipe::MGPipeResourceTarget::Tex3D: return kTex3D; + case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return kTex1DArray; + case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return kTex2DArray; + case MG_Pipe::MGPipeResourceTarget::TexCube: return kTexCube; + case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return kTexCubeArray; + case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return kTex2DMS; + case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return kTex2DMSArray; + case MG_Pipe::MGPipeResourceTarget::TexRect: return kTexRect; + case MG_Pipe::MGPipeResourceTarget::TexBuffer: return kTexBuffer; + default: return kUnknown; + } + } + +// P5e (ID-124): OUT OF THE DISAGGREGATED BLOCK ON PURPOSE. This is a pure +// MGPipeResourceTarget -> TextureTarget switch with no wire, no record and no role in it, but +// it sat inside the nested `#if MOBILEGL_BUILD_DISAGGREGATED` below while its only caller, +// MGB_TEXPARAM_TARGET, is guarded by `#if MOBILEGL_PIPE_PUSH` alone - so a push build without +// disaggregation did not compile. The narrower fix (guarding the macro block too) is wrong: +// that block also carries TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push +// flavour does need. Caught by the `flavours` gate step; see ID-124. + // The inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the sync's + // target reads (ConvertTextureTargetToBackendGLEnum and MapToBackendTextureTarget + // both want the frontend enum). + TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget) { + switch (static_cast(pipeResourceTarget)) { + case MG_Pipe::MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D; + case MG_Pipe::MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D; + case MG_Pipe::MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D; + case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray; + case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray; + case MG_Pipe::MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap; + case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray; + case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample; + case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray; + case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle; + case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer; + default: return TextureTarget::Unknown; + } + } + + namespace { + void Ops_H_TextureSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, + const void* bytes, const MG_Pipe::MGPSubRegion* regions) { + // The region set is the upload planner's shape and stays in the applier's + // pending set; the store's coverage is the staged run itself + // (StagedTextureStore.h's coverage ruling). It is ALSO what places that run in + // the level image when the record carries a piece of the level rather than the + // whole of it (fix A2), which is why it is read here now. + auto& store = MG_Record::ServerStagedTexture(); + // P13 W4b: an aliasing store (monolith's record arm) adopts too - it records where + // the run sits in the frontend's level shadow instead of copying it. + if (!store.Holds()) return; + // The applier's gate has already faulted every shape that reaches here without + // bytes, and under split the codec declared the run's length (Blob.Size) - + // TextureEmit.h's "the bytes this record declares ARE the level shadow". + if (bytes == nullptr || record.Blob.Size == 0) return; + const auto* stored = PipeTextureRecordForHandle(res); + if (stored == nullptr) return; + const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); + const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target); + const IntVec3 extent = MG_Record::StagedTextureUploadExtent(stored->Desc, record); + const SizeT runBytes = static_cast(record.Blob.Size); + // P8-E (docs/archive/notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW + // (FollowCopyImageInStagedStore). The run is the client's shadow, stale everywhere it + // did not write, so only the texels it names land - its regions, or its union box + // when it carries none (D-D3's "the box is the whole story"). A run the store cannot + // place that way is adopted as ever, and the store stops standing for the texture. + if (store.LevelHoldsFollowedCopy(key, uploadTarget, record.Level) && + (record.RegionCount == 0 || regions != nullptr)) { + Vector boxes; + if (record.RegionCount == 0) { + const auto& box = record.UnionBox; + boxes.push_back({box.X, box.Y, box.Z, box.W, box.H, box.D}); + } else { + for (Uint32 i = 0; i < record.RegionCount; ++i) { + const auto& region = regions[i]; + boxes.push_back({region.X, region.Y, region.Z, region.W, region.H, region.D}); + } + } + const Uint64 imageOffset = record.RegionCount == 0 + ? 0 : MG_Record::StagedTextureRunImageOffset(record, regions); + if (store.AdoptClientBoxes(key, uploadTarget, record.Level, extent, imageOffset, bytes, runBytes, + boxes.data(), boxes.size())) { + return; + } + store.MarkDriverWritten(key); + } + // RegionCount == 0 IS the whole-level spelling - "the run is the level shadow", + // beginning at the level's first byte - so it is adopted with the spelling that + // replaces the level; every other record names a RUN of the level by its own box. + // A client record's run: under monolith's record arm it IS the frontend level shadow, + // the one source an aliasing store keeps by address. + if (record.RegionCount == 0) { + store.Adopt(key, uploadTarget, record.Level, extent, bytes, runBytes, /*frontendShadow=*/true); + return; + } + store.AdoptRun(key, uploadTarget, record.Level, extent, + MG_Record::StagedTextureRunImageOffset(record, regions), bytes, + runBytes, /*frontendShadow=*/true); + } + + void Ops_H_TextureRespecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc, + const MG_Pipe::MGPRespecifiedLevel* level) { + auto& store = MG_Record::ServerStagedTexture(); + if (!store.Holds()) return; + const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); + if (level != nullptr) { + // ONE glTexImage*D redefined one level: its exact extent crossed with the + // accepted scope carrier, including a legal noncanonical mutable mip. + store.NoteLevelDefined( + key, MG_Pipe::MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level, + IntVec3{static_cast(level->Width), static_cast(level->Height), + static_cast(level->Depth)}, + desc.Target, desc.InternalFormat); + return; + } + // A whole-resource redefinition: every level's old coordinate system is gone. + // glTexStorage* then defines the WHOLE chain at once (GL 4.6 core 8.19 - all six + // cube faces included), so an immutable descriptor re-marks every level of every + // upload target; a mutable whole-resource respecify (a texture view) defines + // nothing here and the store simply forgets the old levels. + store.ResetLevels(key); + if (desc.Immutable == 0 || desc.Levels == 0) return; + for (const auto& uploadTarget : StagedUploadTargetsForPipeTarget(desc.Target)) { + for (Uint32 levelIndex = 0; levelIndex < desc.Levels; ++levelIndex) { + store.NoteLevelDefined( + key, static_cast(uploadTarget), static_cast(levelIndex), + MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, levelIndex), + desc.Target, desc.InternalFormat); + } + } + } + + void Ops_H_TextureDestroy(MG_Pipe::MGPipeHandle res) { + // Deliberately NOT gated on CopiesIntoServerStorage(): Drop's own m_any gate + // makes the monolith call one acquire load, and an unconditional drop cannot + // strand a key the latch state was misread for. + MG_Record::ServerStagedTexture().Drop( + MG_Record::StagedTextureStore::KeyForHandle(res)); + } + + const MG_Pipe::MGPipeResourceOps g_glesResourceOps = { + .Create = Ops_H_Create, + .Respecify = Ops_H_RespecifyTracked, + .SubData = Ops_H_SubDataTracked, + .SubDataResident = Ops_H_ResidentSubDataTracked, + .FlushRange = Ops_H_FlushRangeTracked, + .Readback = Ops_H_ReadbackTracked, + .Destroy = Ops_H_DestroyTracked, + .MapPersistent = Ops_H_MapPersistentTracked, + .UnmapPersistent = Ops_H_UnmapPersistent, + .TextureSubData = Ops_H_TextureSubData, + .TextureRespecify = Ops_H_TextureRespecify, + .TextureDestroy = Ops_H_TextureDestroy, +#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) + // P11 B2 (T0). Android only: nowhere else is there an AHardwareBuffer to import. + .ImportExternal = Ops_H_ImportExternalTracked, + .SelfTestExternal = Ops_H_SelfTestExternal, +#endif + }; + } // namespace + + Uint64 CurrentBufferMutationEpoch() { + return g_bufferMutationEpoch.load(std::memory_order_acquire); + } + +#if MOBILEGL_BUILD_DISAGGREGATED + namespace { + // P5e (vi): how many times BumpBufferMutationEpoch ran off the apply thread while + // one was running, with a live transport. See the note in the bump itself; the + // accessor is what a test can assert zero on instead of reading a call graph. + std::atomic g_bufferMutationEpochBumpsOffTheApplyThread{0}; + } // namespace + + Uint64 BufferMutationEpochBumpsOffTheApplyThread() { + return g_bufferMutationEpochBumpsOffTheApplyThread.load(std::memory_order_relaxed); + } +#endif + + void BumpBufferMutationEpoch() { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (vi), CONTRACT-P5E §5.1's first pin (scout S1 R2, BRIEF-P5E's open question 2): + // "does any GL-THREAD path bump this under a transport". PINNED HERE RATHER THAN + // ARGUED, because the argument is the kind that is true until someone adds a caller. + // + // It matters because this counter is the ONE non-serial input left in the record + // arm's clean gate: SyncVaoAttributeBuffersByRecord stamps the pre-pass value and + // skips every IsBufferDrawCleanByHandle probe while the stamp holds. A bump from the + // GL thread would make that stamp a cross-thread read of a value the applier never + // observed - the memo would go on reading clean over buffers the client had just + // dirtied - and under run-ahead the client is a frame ahead, so the window is a + // frame wide rather than a record wide. + // + // THE MEASURED ANSWER, and it is not the clean "no" the scout expected: there is + // EXACTLY ONE class of non-apply-thread bump under a transport, and it happens + // BEFORE THERE IS AN APPLY THREAD AT ALL. BackendObject_DirectGLES::Initialize() + // runs on the app thread by design ("the backend object is BUILT on the app thread + // while its context is never OWNED by it", ServerLoop.h) and calls + // RegisterBufferBackendOps(), whose last act is a bump. An unguarded refusal aborts + // the whole split lane in EGL bring-up, which is how this was found rather than + // reasoned. + // + // That class is harmless and the apply-thread key tells it apart from the one that + // would not be: with g_applyThreadKey == 0 no record has been applied, so no twin + // exists, so no memo holds a stamp for the bump to slide under. + // + // WHAT IS COUNTED AND WHAT IS FATAL, and why they are not the same thing. Every + // bump taken off the apply thread WHILE ONE IS RUNNING is counted and named once, + // and is Fatal UNDER THE STRICT LANE - which is where this phase's pins live, and + // which is the lane the answer is claimed for. It is not Fatal by default because + // the unit fixtures that drive the production op tables directly + // (StagedShadowProductionTest / ServerLoopTest, which register the real tables and + // call them from the test thread beside a started loop) are the server role without + // being the apply thread, and taking those down would be this pin policing a + // fixture rather than the draw path. + const Uint64 applyThreadKey = + MG_Remote::Server::Detail::g_applyThreadKey.load(std::memory_order_relaxed); + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && applyThreadKey != 0 && + !MG_Remote::Server::ServerLoop::OnApplyThread()) { + g_bufferMutationEpochBumpsOffTheApplyThread.fetch_add(1, std::memory_order_relaxed); + if (MG_Config::Ipc.StrictErrors) { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"CurrentBufferMutationEpoch\"} - the " + "buffer-mutation epoch was bumped off the apply thread while an apply " + "thread was running, with a live transport. The draw-path memos stamp " + "this counter and skip their clean probes while the stamp holds " + "(CONTRACT-P5E §5.1), so a producer on the client's thread means the " + "backend is reachable from a thread that owns none of this state"); + std::abort(); + } + MGLOG_E_ONCE("MGPipe: the buffer-mutation epoch was bumped off the apply thread " + "with a live transport (CONTRACT-P5E §5.1's pin). Run the strict lane " + "to make this fatal at the producer"); + } +#endif + g_bufferMutationEpoch.fetch_add(1, std::memory_order_release); + } + + // The seventh slot table. A process-lifetime global, like the other six, so it links + // itself into its own holder list from its own constructor with no initialisation + // order question to answer (SlotTables.h). + BackendBufferResourceTable g_backendBufferResources; + + GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { + if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; + // The table refuses both of these itself; this is the release-build VOICE for the + // refusal, because MOBILEGL_ASSERT compiles out at INFO and a resource that silently + // stops being twinned is the failure mode the refusal exists to replace. + if (res.Slot >= BackendBufferResourceTable::kMaxHandleSlot) { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 + MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - " + "buffer twin request named slot %u, past this table's %u bound", + res.Slot, BackendBufferResourceTable::kMaxHandleSlot); +#else + MGLOG_E_ONCE("MGPipe: resource handle slot %u is past the backend table's %u bound - " + "refusing to twin it", + res.Slot, BackendBufferResourceTable::kMaxHandleSlot); + return nullptr; +#endif + } + const Uint32 liveGen = g_backendBufferResources.LiveGenAt(res.Slot); + if (liveGen != 0 && liveGen > res.Gen) { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 + MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - " + "buffer twin request named generation %u at slot %u, behind live generation %u", + res.Gen, res.Slot, liveGen); +#else + MGLOG_E_ONCE("MGPipe: resource handle {%u, %u} names a generation BEHIND the live twin's " + "%u - refusing rather than dropping the incumbent's driver storage", + res.Slot, res.Gen, liveGen); + return nullptr; +#endif + } + auto& twin = g_backendBufferResources.GetOrCreate(res); + if (!twin) { + twin = MakeShared(); + // Same seed the lazy legacy path gives a resource it has just minted: nothing + // has defined storage yet, so the first sync owes a full (re)specification. + twin->pendingRespecify = true; + } + return twin.get(); + } + + GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { + auto* twin = g_backendBufferResources.FindByHandle(res); + return twin ? twin->get() : nullptr; + } + + void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, + SizeT end, const char* site) { + ServerStaged().RequireCoverage(&resource, hostBase, start, end, site); + } + + // M-3's rule for the WHOLE-STORE readers (v1 round 3). The descriptor is the + // application's own statement about the store: HasDefinedContent set means it SUPPLIED + // the content (glBufferData(size, data)), which under split arrives as resource_subdata + // records behind the respecify (table 1 row 19) - so a coverage gap at the draw is a + // MISSING RECORD and the zero-fill past the coverage is not the application's bytes. + // Clear means it ORPHANED the store (glBufferData(size, NULL), glBufferStorage(NULL)): + // every byte it has not staged since is UNDEFINED by its own declaration, the streaming + // idiom (orphan, partial glBufferSubData, draw) is the ordinary case, and uploading the + // shadow's zero-fill for the rest is exactly what the monolith arm uploads from + // MappedData(). Round 2 refused both shapes and aborted six LargeArenaAdoption / + // ResourceSubsystemControl entries on the joint by name (v1-v3.md 6). + // + // AT NAMESPACE SCOPE, not in the anonymous namespace it was written in (P3b/P4b espryt + // D1 slice 1): the XFB scatter guards ITS whole-store read with the same question, and + // a second spelling of "did the application declare these bytes" is how the pool-reuse + // ladder and the scatter end up disagreeing about one buffer. + Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res) { + const auto* record = ResourceRecordOf(res); + return record != nullptr && record->Desc.HasDefinedContent != 0; + } + + void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes, + SizeT offset, SizeT size) { + if (bytes == nullptr || size == 0 || resource.externalAhb != nullptr) return; + // Only a COPYING store may take bytes that die with a mapping: the monolith one keeps + // the caller's pointer (StagedShadow.h), which here would dangle after the unmap. + if (!ServerStaged().CopiesIntoServerStorage()) return; + // Under pendingMutex, as every other hostBytes store is (M-2). + const std::lock_guard lock(resource.pendingMutex); + resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size); + } + + SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); } + Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res) { return ResourceSerialOf(res); } + Uint CurrentBufferContextGeneration() { return g_bufferContextGeneration; } + + // See the declaration: re-mints of a live resource's driver id. Written only on + // the context thread (all re-mint sites run there), read only by the VAO sync. + Uint64 g_bufferBackendIdGeneration = 0; + + void RegisterBufferBackendOps() { + // P3a: the handle-shaped resource table (P13: the only one - the legacy + // BufferBackendOps table is deleted). Registration is UNCONDITIONAL: the subsystem bit is the FRONTEND's dispatch predicate + // (MGPipeResourceSubsystemEnabled reads the bit AND this table's presence), so a + // build with bit 7 clear registers a table nobody calls and the A/B stays a pure + // configuration question rather than a bring-up-order one. + MG_Pipe::MGPipeSetResourceOps(&g_glesResourceOps); + // Frontend writes issued while ops were unregistered advanced change + // serials with no per-op bump; re-open every draw-clean memo. Advanced directly, + // past BumpBufferMutationEpoch's apply-thread pin: a registration mutates no buffer + // and only makes every memo probe again, and with several sessions in one process + // a new session's backend registers on its own thread while another session's + // apply thread is already running - which the pin would otherwise read as a + // client-thread producer and, under the strict lane, abort the server for. + g_bufferMutationEpoch.fetch_add(1, std::memory_order_release); + } + + void UnregisterBufferBackendOps() { + if (MG_Pipe::MGPipeGetResourceOps() == &g_glesResourceOps) { + MG_Pipe::MGPipeSetResourceOps(nullptr); + } + // From here on frontend writes bypass the tracked ops entirely. + BumpBufferMutationEpoch(); + InvalidateArrayBufferBindingCache(); + // Pooled ids belong to the dying context too; drop them without glDeleteBuffers. + ClearBufferPool(); + const std::lock_guard lock(g_deferredBufferReleasesMutex); + // The ES context owning these ids is going away; just drop the handles. + g_deferredBufferReleases.clear(); + g_hasDeferredBufferReleases.store(false, std::memory_order_release); + } + + void OnServerSessionEndedWithOthersLive() { + // Only what belongs to the ending session: its staged bytes (both stores are + // session-scoped). The rings, the ops table and the buffer generation are the + // process's and every other session's GL buffers are still alive in the shared group. + MGL_SERVER_STAGED_DROP_ALL(); +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Record::ServerStagedTexture().DropAll(); +#endif + } + + void OnBackendContextDestroyed() { + UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch + ++g_bufferContextGeneration; + // The generation moved AFTER the unregister bump above; re-open the + // memos again so no stamp can predate the generation change. + BumpBufferMutationEpoch(); + InvalidateArrayBufferBindingCache(); + InvalidateIndexedBufferBindingCache(); + InvalidatePixelBufferBindingCaches(); + // The rings' ids and persistent maps died with the context; drop the + // handles (no GL) and let the next draw / texture upload recreate them. + ResetRingForNewContext(UboRing()); + ResetRingForNewContext(UnpackRing()); + // R-11's server copies die with the context for the same reason the rings' ids do: + // the resource twins they are keyed by are about to be rebuilt against a new + // generation, and a shadow that outlived its twin would be looked up by a RECYCLED + // address on the next allocation - which is the quietest possible wrong answer. + MGL_SERVER_STAGED_DROP_ALL(); + // tx's texture shadows die for the same reason, keyed by handle rather than address + // but with the same recycled-identity failure mode: a new context's allocator may + // hand out a {slot, gen} the old one's store still answers for. + MG_Record::ServerStagedTexture().DropAll(); + // P11 B2: every retired T0 import's fence named work on the context that just ended, + // so all of them let go now. LIVE imports keep their reference: the next ensure + // imports them again on the new context (EnsureBufferResourceForHandle). +#if MOBILEGL_BUILD_DISAGGREGATED + ProcessDeferredT0Retires(); + SweepT0Retired(/*contextGone=*/true); +#endif + } + + void ProcessDeferredBufferReleases() { + // Runs on every draw; skip the context check, mutex and vector churn + // outright when nothing was enqueued since the last drain. + if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return; + if (!CanTouchGLNow()) return; + Vector> releases; + { + const std::lock_guard lock(g_deferredBufferReleasesMutex); + releases.swap(g_deferredBufferReleases); + g_hasDeferredBufferReleases.store(false, std::memory_order_release); + } + for (auto& resource : releases) { + auto* glesResource = static_cast(resource.get()); + if (glesResource->contextGeneration != g_bufferContextGeneration) { + glesResource->id = 0; + continue; + } + if (IsPoolable(*glesResource)) { + EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers + continue; + } + if (glesResource->id != 0) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { + InvalidateArrayBufferBindingCache(); + } + ScrubBufferBindingShadowsForId(glesResource->id); + g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); + glesResource->id = 0; + } + } + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // P11 B2: the T0 half of the deferred-release drain, run right after it at both sync + // points (ProcessDeferredBufferReleases itself moves verbatim, G5). Retired imports whose + // fence signalled let go of their AHardwareBuffer; a deferred T0 destroy is retired once + // the drain deleted its id (the fence then follows the delete) or its context is gone. + void ProcessDeferredT0Retires() { + if (!g_t0Retired.empty()) SweepT0Retired(false); + if (!g_hasT0DeferredDestroys.load(std::memory_order_acquire)) return; + Vector> ready; + { + const std::lock_guard lock(g_deferredBufferReleasesMutex); + for (SizeT i = 0; i < g_t0DeferredDestroys.size();) { + auto* r = static_cast(g_t0DeferredDestroys[i].get()); + if (r->id == 0 || r->contextGeneration != g_bufferContextGeneration) { + ready.push_back(std::move(g_t0DeferredDestroys[i])); + g_t0DeferredDestroys[i] = std::move(g_t0DeferredDestroys.back()); + g_t0DeferredDestroys.pop_back(); + } else { + ++i; + } + } + g_hasT0DeferredDestroys.store(!g_t0DeferredDestroys.empty(), std::memory_order_release); + } + for (auto& resource : ready) RetireT0Import(*static_cast(resource.get())); + } +#endif + + // The same five questions, asked of the applier instead of the frontend object, with + // IDENTICAL semantics (D-A4): + // identity the slot table's twin at this handle, not GetBackendResource() + // size record.Desc.Width, not GetSize() + // freshness record.Serial vs syncedChangeSerial, not GetChangeSerial() + // map state record.HasLiveHostWrites AND the frontend's IsMapped() - see below + // the rest unchanged, and server-side to begin with + // + // HasLiveHostWrites is ALWAYS FALSE in P3a and is written by nobody; it is here so the + // phase that pushes persistent-mapped host writes can set it with no new record kind, + // and the assertion below is what stops that phase landing a silent semantic change. + // + // BECAUSE it is pinned false, it CANNOT stand in for the frontend's IsMapped() yet, and + // the probe still asks the object: an EMULATED (non-adopted) persistent map - under the + // 16 MiB adoption threshold, or with DisableLargeBufferAdoption, or with no + // EXT_buffer_storage - mutates the client's shadow with no call, no serial and no epoch, + // which is the entire reason the legacy probe asks a map question instead of a serial + // one. Answering only HasLiveHostWrites made such a buffer read draw-clean forever, so + // SyncPersistentMappedRange (D-N keeps it on the ensure path for all of P3a) was never + // reached again and the frame drew the last uploaded bytes with no diagnostic anywhere. + // The frontend read retires the moment P5 gives HasLiveHostWrites a producer, and it is + // the last frontend read in this function. + Bool IsBufferDrawCleanByHandle(MG_Pipe::MGPipeHandle res, const GLESBufferResource* resource, + const MG_State::GLState::BufferObject* frontend) { + if (!resource) return false; + const auto* twin = g_backendBufferResources.FindByHandle(res); + if (twin == nullptr || twin->get() != resource) return false; + if (resource->contextGeneration != g_bufferContextGeneration) return false; + if (resource->id == 0) return false; + if (resource->persistentMapped) { + return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty(); + } + const auto* record = ResourceRecordOf(res); + if (record == nullptr) return false; + if (record->HasLiveHostWrites) return false; + // THE LAST FRONTEND READ IN THIS FUNCTION, AND P5 b1 RETIRES IT - under split + // only, because that is the only build where it is both wrong and replaceable. + // + // It asked the object whether it was mapped because HasLiveHostWrites was pinned + // false and an emulated persistent map mutates the shadow with no call, no serial + // and no epoch; answering from the record alone made such a buffer read + // draw-CLEAN forever, SyncPersistentMappedRange was never reached again, and the + // frame drew the last uploaded bytes with no diagnostic + // (MG_Test/SanityTest.cpp's DirectGLESBufferDrawProbe is exactly that case). + // + // TWO THINGS REPLACE IT AND BOTH HAD TO LAND FIRST: the record now carries the map + // state (the line above, from MGPSubData::HasLiveHostWrites), and the client + // pushes the mapped span by block at every validate point, so the serial moves for + // a write the application made with no API call. Under a spawn there is no object + // on this side to ask, which is why this was never going to stay a choice. + // + // A null object is the "no frontend to ask" case and is treated as "not mapped", + // which is what the record already says. + if (resource->pendingRespecify || !resource->storageInitialized) return false; + if (!resource->pendingRanges.empty()) return false; + if (resource->storageSize != static_cast(record->Desc.Width)) return false; + return resource->syncedChangeSerial.load(std::memory_order_acquire) == record->Serial; + } + + // The handle arm of the ensure path. Same shape, same order, same branches; the four + // frontend reads become the applier's stored descriptor, its Serial and the shadow base + // the last content-carrying call handed over. + // + // It still takes the frontend object for ONE reason, recorded rather than hidden: + // BufferObject::SyncPersistentMappedRange() is one of the eleven Espryt sites + // ROADMAP/D-N explicitly keeps where it is for P3a (the per-site attribution table is + // P8's to execute). Every other line here is handle-shaped. When P8 moves that call to + // the client this signature loses its last frontend argument. + GLESBufferResource* EnsureBufferResourceForHandle(const SharedPtr& bufferObject, + MG_Pipe::MGPipeHandle res) { + auto* resource = GetOrCreateBufferResourceForHandle(res); + if (!resource) return nullptr; + + // The client's shadow base, RE-READ at every use, exactly as the legacy arm re-read + // MappedData() at every use. It is deliberately NOT resource->hostBytes here: two + // ordinary events move or free what a base recorded by an earlier call points at - + // a shadow resize (PipeResource::ResizeShadow is reserve + resize, so a grow past + // the reserve reallocates) and persistent-map adoption (the shadow is cleared and + // shrunk to fit) - and this path still holds the frontend object anyway, because + // D-N keeps SyncPersistentMappedRange here for all of P3a. hostBytes stays the + // fallback for the drains that have NO object (the readback flush), and it is + // nulled at both of those events. + const auto liveHostBase = [&resource, &bufferObject]() -> const Uint8* { + // M-2 / codex 3 / ID-52 item 3: under an ACTIVE TRANSPORT the server's staged + // copy (resource->hostBytes, filled by MGL_SERVER_STAGED_ADOPT in Ops_H_SubData / + // Ops_H_FlushRange) is the ONLY authoritative base. Preferring the frontend + // object's MappedData() here - which under inproc is always non-null, same process + // - meant every reduced-path drain read the CLIENT's shadow and NEVER the server's, + // so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5 + // 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that + // does not read the client object's memory. Under monolith nothing changes. + return resource->hostBytes; + }; + + if (resource->contextGeneration != g_bufferContextGeneration) { + // The id (if any) belonged to a destroyed ES context. + resource->id = 0; + resource->storageInitialized = false; + resource->storageSize = 0; + resource->pendingRespecify = true; + resource->pendingRanges.clear(); + resource->pendingResidentWrites.clear(); + resource->contextGeneration = g_bufferContextGeneration; + resource->persistentMapped = false; + resource->persistentPtr = nullptr; + resource->immutableStorage = false; + // m-5 / codex 5: OnBackendContextDestroyed ran MGL_SERVER_STAGED_DROP_ALL(), which + // frees every server shadow but does NOT null the hostBytes that name them - so a + // twin that SURVIVES a context loss still carries a base into the freed allocation. + // Null it here, where a surviving twin is re-armed - but ONLY when the shadow is + // really gone. This block also runs on a twin's FIRST ensure (contextGeneration + // starts mismatched), and there the shadow a preceding resource_subdata just staged + // is still live; nulling it then would drop the reduced path's own bytes (it did, + // and TriangleScenario read the wrong VBO). HasShadow is the discriminator: false + // after DropAll, true after an ordinary Adopt. + // + // TRANSPORT-GUARDED like the other two split hunks in this file (review v2 N-7): + // under MONOLITH transport in a disaggregated build the store never copies, Adopt + // never sets m_any, HasShadow answers false for everything, and this null would + // run on every twin's first ensure - harmless there only because liveHostBase() + // prefers MappedData() under monolith, and "under monolith nothing changes" should + // be true by construction rather than by luck. + if (ServerStaged().CopiesIntoServerStorage() && + !ServerStaged().HasShadow(resource)) { + resource->hostBytes = nullptr; + } +#if defined(__ANDROID__) && MOBILEGL_BUILD_DISAGGREGATED + // P11 B2: A T0 STORE OUTLIVES ITS CONTEXT. Its pages are the client's + // AHardwareBuffer, which this twin still holds, so the store is imported again on + // the new context - the client keeps writing through the same pointer and nothing + // has to be re-sent. A refusal here leaves the twin without a store and says so. + if (resource->externalAhb != nullptr) { + String why; + void* ptr = nullptr; + const Uint64 size = ResourceWidthOf(res); + const Uint id = ResolveT0EntryPoints(why) ? ImportAhbAsBuffer(resource->externalAhb, size, &ptr, why) + : 0; + if (id != 0) { + InstallT0Store(*resource, id, ptr, size); + MGLOG_I("Espryt T0: store {slot=%u, gen=%u} re-imported on the new context (%llu bytes)", + res.Slot, res.Gen, static_cast(size)); + } else { + MGLOG_E("Espryt T0: store {slot=%u, gen=%u} could not be re-imported after a context loss - " + "%s; the store has no GPU backing", + res.Slot, res.Gen, why.c_str()); + RetireT0Import(*resource); + } + } +#endif + } + + // An immutable store nothing maps any more, retired here on the thread that can. + if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) { + NoteBufferIdDeleted(resource->id); + ++g_bufferBackendIdGeneration; + g_GLESFuncs.glDeleteBuffers(1, &resource->id); + resource->id = 0; + resource->immutableStorage = false; + resource->storageInitialized = false; + resource->storageSize = 0; + resource->pendingRespecify = true; + } + + // Zero-copy coherent persistent buffer: nothing to (re)upload at draw time. + if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) { + DrainResidentWritesNow(*resource); + return resource; + } + + if (resource->id == 0) { + const SizeT poolSize = ResourceWidthOf(res); + // The pool DECISION is the legacy one, unchanged: extent and residence only. + // An earlier cut added "and a shadow base was recorded", which silently stopped + // a buffer recycling pool ids - a change to D-F's pool behaviour, not to "the + // source of bytes" the conversion is allowed to move, and one that moves + // AcquireFromPool call counts StreamedArenaScenario observes. + const Uint reused = + (poolSize > 0 && !resource->persistentMapped) ? AcquireFromPool(poolSize) : 0; + if (reused != 0) { + resource->id = reused; + resource->storageSize = poolSize; + resource->storageInitialized = true; + resource->pendingRespecify = false; + BindBufferId(TempBufferTarget, reused); + // M-3 / codex 4: this is a WHOLE-STORE upload from the base, and under split + // the base is the server shadow (M-2). For a store whose content the + // application SUPPLIED, zero-filled bytes past the staged coverage are not the + // application's - uploading them is the silent data loss the M-6 ruling + // forbids, and a gap is a missing record: Fatal by name. For a store the + // application ORPHANED the gap is its own undefined content and the upload is + // legal (ResourceContentIsDeclared, above). RequireCoverage is a no-op for the + // legacy arm's MappedData() and for a non-copying store. + if (ResourceContentIsDeclared(res)) { + MGL_SERVER_STAGED_REQUIRE(*resource, liveHostBase(), 0, poolSize, + "pool_reuse_whole_store"); + } + g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, liveHostBase()); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(poolSize)); + } + { + const std::lock_guard lock(resource->pendingMutex); + resource->pendingRanges.clear(); + resource->pendingResidentWrites.clear(); + } + resource->syncedChangeSerial = ResourceSerialOf(res); + } else { + g_GLESFuncs.glGenBuffers(1, &resource->id); + if (resource->id == 0) { + MGLOG_E_ONCE("Failed to generate buffer object."); + MGLOG_E_ONCE("ES glGetError(): %s", + MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + return resource; + } + resource->storageInitialized = false; + resource->pendingRespecify = true; + } + } + + // D-N keeps this call here for P3a. It can queue ranges and move the record, so + // everything below is read AFTER it. + // P5c (hd, CONTRACT-P5C §3.8): under an active transport the frontend accessor is a + // layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb + // persistent-map push is the only producer; the call is skipped outright. Under + // monolith D-N's placement stands. + + const auto* record = ResourceRecordOf(res); + const SizeT size = record != nullptr ? static_cast(record->Desc.Width) : 0; + if (size == 0) { + return resource; + } + const Uint64 serial = record->Serial; + const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(static_cast(record->Desc.Usage)); + // Read AFTER SyncPersistentMappedRange above, and through liveHostBase for the + // reason written where it is declared. + const Uint8* const hostBase = liveHostBase(); + // AND PUBLISH IT, because the readers that have NO frontend object read + // resource->hostBytes and nothing else: the readback flush (Ops_H_Readback), the + // kill-switch map arm of Ops_H_FlushRange, and the fp64 narrowing + // (SyncConvertedAttributeByHandle, whose legacy counterpart reads + // bufferObject->MappedData()). A twin is created LAZILY - D-A2's row makes + // Ops_H_Create a no-op - so the very first resource_respecify of a buffer finds + // FindBufferResourceForHandle == nullptr and its base is dropped on the floor; with + // glBufferData(..., data) followed by no further content call (the ordinary static + // vertex array) hostBytes then stayed null for the object's whole life and the fp64 + // narrowing refused every draw, disabling the array. This is the one place that both + // holds the object and runs before every draw that uses the store, so it is where the + // base is refreshed. Only reached for a NON-adopted resource (the adopted arm + // returned above), so C-2's rule is intact: an adopted store's hostBytes stays null + // and its bytes are read through persistentPtr. + if (hostBase != nullptr) resource->hostBytes = hostBase; + // "DOES THE SHADOW THIS PATH IS ABOUT TO UPLOAD HOLD MEANINGFUL BYTES?" - and it has + // to be asked of the SAME thing the bytes come from. The legacy arm pairs + // bufferObject.MappedData() with bufferObject.HasDefinedContent() (RespecifyStorageNow) + // and this arm pairs the same two, so the source of the bytes and the statement about + // them can never disagree. + // + // P5c (hd, CONTRACT-P5C §3.6 / B1): with an active transport the answer is the + // DESCRIPTOR's bit - which the client publishes on ResourceCreate/Respecify - OR the + // staged coverage the content records behind it delivered. The coverage half is what + // keeps the answer equal to the frontend flag's in the one case the descriptor alone + // cannot see: an ORPHANING respecify (descriptor clear) followed by glBufferSubData, + // which defines content without a new descriptor - the bytes crossed as + // resource_subdata and the staged store's coverage is exactly "content defined since + // the last orphan". With no coverage and a clear bit the store is undefined by the + // application's own declaration and the upload stays a pure NULL reallocation. + const Bool shadowHasContent = + (record->Desc.HasDefinedContent != 0 || + (ServerStaged().CopiesIntoServerStorage() ? ServerStaged().CoveredRunCount(resource) != 0 + : resource->contentSinceRespecify)); + const void* initialData = shadowHasContent ? hostBase : nullptr; + // M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE + // [0, size) upload from the base, so it owes the same coverage the pending-range drain + // below owes - the two respecify arms were the readers M-6's "never widened" rule did + // not reach. No-op for the legacy arm's MappedData() and for a non-copying store; a + // Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} under split when the + // DESCRIPTOR says the application supplied the content and the shadow's coverage does + // not span the store (a missing record), instead of uploading its zero-fill as content. + // NOT for a store the application orphaned: there `shadowHasContent` is the frontend + // object's flag, which the streaming idiom's partial glBufferSubData flips to true, and + // the bytes it did not write are undefined by its own glBufferData(NULL) - the rule at + // ResourceContentIsDeclared. Round 2 refused that idiom and aborted six joint entries. + const auto requireWholeStoreCoverage = [&]() { + if (initialData != nullptr && record->Desc.HasDefinedContent != 0) { + MGL_SERVER_STAGED_REQUIRE(*resource, static_cast(initialData), 0, + size, "respecify_whole_store"); + } + }; + + if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) { + requireWholeStoreCoverage(); + RespecifyStorageWith(*resource, size, usage, initialData, serial); + } else if (!resource->pendingRanges.empty()) { + // Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object + // `hostBase` is the server shadow and every queued range must be staged. + RenameSmallStoreForDrain(*resource, hostBase, size); + MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, hostBase, size, "ensure_flush_pending"); + FlushPendingRangesFrom(*resource, hostBase, size); + resource->syncedChangeSerial = serial; + } else if (resource->syncedChangeSerial != serial) { + // Mutations this backend could not track (the table was unregistered between + // contexts); re-upload everything. + requireWholeStoreCoverage(); + RespecifyStorageWith(*resource, size, usage, initialData, serial); + } + return resource; + } + + void BindBufferId(GLenum target, Uint id) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (target == GL_ARRAY_BUFFER) { + if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) { + return; + } + g_boundArrayBufferId = id; + g_boundArrayBufferKnown = true; + } + g_GLESFuncs.glBindBuffer(target, id); + } + + void InvalidateArrayBufferBindingCache() { + g_boundArrayBufferId = 0; + g_boundArrayBufferKnown = false; + } + + void BindPixelPackBufferId(Uint id) { + if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { + return; + } + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id); + g_boundPixelPackBufferId = id; + g_boundPixelPackBufferKnown = true; + } + + void BindPixelUnpackBufferId(Uint id) { + if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) { + return; + } + g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id); + g_boundPixelUnpackBufferId = id; + g_boundPixelUnpackBufferKnown = true; + } + + void InvalidatePixelBufferBindingCaches() { + g_boundPixelPackBufferId = 0; + g_boundPixelPackBufferKnown = false; + g_boundPixelUnpackBufferId = 0; + g_boundPixelUnpackBufferKnown = false; + } + + void NoteBufferIdDeleted(Uint id) { + if (id == 0) { + return; + } + if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) { + InvalidateArrayBufferBindingCache(); + } + ScrubBufferBindingShadowsForId(id); + } + + namespace { + // Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range + // (same index + id + range) are skipped. isBase distinguishes a whole-buffer + // base bind from a sub-range bind. Fresh/reset context: every point is base(0) + // == unbound, which matches the GL default. + struct IndexedBufferBinding { + Uint id = 0; + GLintptr offset = 0; + GLsizeiptr size = 0; + Bool isBase = true; + // False when the driver's binding at this point is no longer described by the + // fields above and the next bind must be issued whatever it asks for. Set by + // InvalidateIndexedBufferBindingShadowsForId after a store was re-specified at + // a new size: the id is still bound, so the entry must NOT be scrubbed to + // base(0) (a later bind of 0 would then be false-skipped) - only distrusted. + Bool known = true; + }; + constexpr SizeT kMaxIndexedBufferBindings = 64; + IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings]; + IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings]; + // Transform feedback gets a shadow for a reason the other two do not have: the + // capture points are synced from the application's TOUCHED high-water mark, which + // deqp/glcts permanently raises to GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by + // clearing every point after each test case. Without a shadow every capture that + // uses fewer points than that (i.e. every INTERLEAVED_ATTRIBS capture) re-issued a + // redundant glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) for the unused + // tail immediately before glBeginTransformFeedback - calls a plain GL application + // never makes there, and the only thing MobileGL does differently from one. + // + // Unlike the UBO/SSBO points these are NOT context state: they belong to the bound + // transform feedback OBJECT, so XfbImpl::BindTransformFeedback drops the whole + // shadow to unknown on every object switch (InvalidateTransformFeedbackBindingShadows). + IndexedBufferBinding g_indexedXFBBindings[kMaxIndexedBufferBindings]; + IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) { + if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache + if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index]; + if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index]; + if (glTarget == GL_TRANSFORM_FEEDBACK_BUFFER) return &g_indexedXFBBindings[index]; + return nullptr; + } + + // glDeleteBuffers resets the deleted buffer's bindings (indexed and + // pixel pack/unpack ones included) to 0 in the current context; mirror + // that in the shadows, or a later buffer recycling the same name with a + // matching shadow entry would false-skip its rebind. Default + // IndexedBufferBinding{} == base(0) == the post-delete GL state. + void ScrubBufferBindingShadowsForId(Uint id) { + if (id == 0) return; + for (auto& binding : g_indexedUBOBindings) { + if (binding.id == id) binding = {}; + } + for (auto& binding : g_indexedSSBOBindings) { + if (binding.id == id) binding = {}; + } + for (auto& binding : g_indexedXFBBindings) { + if (binding.id == id) binding = {}; + } + if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { + g_boundPixelPackBufferId = 0; + } + if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) { + g_boundPixelUnpackBufferId = 0; + } + } + + void InvalidateIndexedBufferBindingShadowsForId(Uint id) { + if (id == 0) return; + for (auto& binding : g_indexedUBOBindings) { + if (binding.id == id) binding.known = false; + } + for (auto& binding : g_indexedSSBOBindings) { + if (binding.id == id) binding.known = false; + } + for (auto& binding : g_indexedXFBBindings) { + if (binding.id == id) binding.known = false; + } + } + } // namespace + + // The capture points belong to the bound transform feedback object, so a bind (or a + // delete, which reverts to the default object) replaces all of them at once with + // state this shadow has never seen. Distrust rather than scrub: the driver's bindings + // are whatever the newly bound object holds, which is NOT necessarily base(0), and + // scrubbing would let a later bind of 0 be false-skipped. + void InvalidateTransformFeedbackBindingShadows() { + for (auto& binding : g_indexedXFBBindings) binding.known = false; + } + + void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) { + auto* s = IndexedBindingShadow(glTarget, index); + if (s && s->known && s->isBase && s->id == id) return; + g_GLESFuncs.glBindBufferBase(glTarget, index, id); + if (s) *s = {id, 0, 0, true, true}; + } + + void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size) { + auto* s = IndexedBindingShadow(glTarget, index); + if (s && s->known && !s->isBase && s->id == id && s->offset == offset && s->size == size) return; + g_GLESFuncs.glBindBufferRange(glTarget, index, id, offset, size); + if (s) *s = {id, offset, size, false, true}; + } + + void InvalidateIndexedBufferBindingCache() { + for (auto& b : g_indexedUBOBindings) b = {}; + for (auto& b : g_indexedSSBOBindings) b = {}; + for (auto& b : g_indexedXFBBindings) b = {}; + } + + void TrimBufferPool() { + const std::lock_guard lock(g_poolMutex); + if (g_pooledBytes <= kMaxPoolBytes) return; + // Over budget: evict oldest-retireSerial entries with real glDeleteBuffers. + while (g_pooledBytes > kMaxPoolBytes) { + SizeT oldestKey = 0, oldestIdx = 0; + Uint64 oldestSerial = ~Uint64{0}; + Bool found = false; + for (auto& kv : g_bufferPool) { + for (SizeT i = 0; i < kv.second.size(); ++i) { + if (kv.second[i].retireSerial < oldestSerial) { + oldestSerial = kv.second[i].retireSerial; + oldestKey = kv.first; + oldestIdx = i; + found = true; + } + } + } + if (!found) break; + auto& bucket = g_bufferPool[oldestKey]; + PooledBuffer& e = bucket[oldestIdx]; + if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) { + ScrubBufferBindingShadowsForId(e.id); + g_GLESFuncs.glDeleteBuffers(1, &e.id); + } + g_pooledBytes -= e.size; + bucket[oldestIdx] = bucket.back(); + bucket.pop_back(); + } + } + + void ClearBufferPool() { + const std::lock_guard lock(g_poolMutex); + // Ids belong to the dying context; drop without glDeleteBuffers (mirrors + // the g_deferredBufferReleases.clear() discipline). + g_bufferPool.clear(); + g_pooledBytes = 0; + } + + // --- Persistent-mapped bump rings (see Managers.h) ------------------------ + // (Re)create the ring store with room for at least minBytes. Any live + // store is retired (deleted once the GPU finished the last frame that + // could reference its slots), never deleted in place. Returns false and + // leaves the current store untouched when minBytes cannot fit under the + // size cap; a GL failure loses the store and latches creationFailed so + // later uses stop retrying under this context. + Bool CreateRingStorage(PersistentRing& ring, SizeT minBytes) { + SizeT newSize = ring.initialBytes; + while (newSize < minBytes) newSize *= 2; + if (newSize > ring.maxBytes) return false; + + if (ring.store.id != 0) { + ring.retired.push_back( + {ring.store.id, ring.store.contextGeneration, DirectGLES::CurrentFrameSerial() + 1}); + } + const Uint32 nextGeneration = ring.store.generation + 1; + ring.store.id = 0; + ring.store.mappedPtr = nullptr; + + Uint id = 0; + g_GLESFuncs.glGenBuffers(1, &id); + if (id != 0) { + BindBufferId(TempBufferTarget, id); + g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(newSize), nullptr, + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); + void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(newSize), + GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); + if (!ptr) { + // The dying id is what the array-buffer cache has recorded as + // bound; a later buffer recycling the name would false-skip. + InvalidateArrayBufferBindingCache(); + g_GLESFuncs.glDeleteBuffers(1, &id); + id = 0; + } else { + ring.store.mappedPtr = static_cast(ptr); + } + } + if (id == 0) { + MGLOG_E_ONCE("%s: persistent storage creation failed (%zu bytes); falling back to the " + "client-memory upload path.", + ring.label, newSize); + ring.store.creationFailed = true; + return false; + } + + SizeT alignment = ring.fixedAlignment; + if (alignment == 0) { + const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment; + alignment = capsAlignment > 0 ? static_cast(capsAlignment) : 256; + } + ring.store.id = id; + ring.store.size = newSize; + ring.store.head = 0; + ring.store.tail = 0; + ring.store.generation = nextGeneration; + ring.store.alignment = alignment; + ring.frameMarks.clear(); + MGLOG_D("%s: %zu MiB persistent store ready (id %u, gen %u, align %zu).", ring.label, + newSize / (1024u * 1024u), id, nextGeneration, alignment); + return true; + } + + namespace { + // Shared half of the availability gate (the per-ring kill switch sits in + // the exported wrappers). Reclamation rides the Present fence watermark; + // without working fences slots would never be provably GPU-idle (same rule + // as IsPoolable). + Bool RingAvailable(PersistentRing& ring) { + if (!g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers || + !g_GLESFuncs.glFenceSync || !g_GLESFuncs.glGetSynciv) { + return false; + } + if (!CanTouchGLNow()) return false; + if (ring.store.contextGeneration != g_bufferContextGeneration) { + ResetRingForNewContext(ring); + } + return !ring.store.creationFailed; + } + + // Division-based rounding fallback: the spec doesn't promise a power-of-two + // alignment. Slot offsets stay multiples of the alignment because every + // slot size is, and wrap padding restarts at ring offset 0. + inline SizeT RingAlignUp(SizeT size, SizeT alignment) { + if ((alignment & (alignment - 1)) == 0) { + return (size + alignment - 1) & ~(alignment - 1); + } + return (size + alignment - 1) / alignment * alignment; + } + } // namespace + + // Bump-allocate `size` bytes out of `ring`. + // + // Fast path: a live store under the current context with room before both + // the wrap boundary and the in-flight tail. Touches no GL and probes no + // frame marks - callers sit behind the ring's availability gate, so the + // context checks have already run for this draw/upload. `tail` may be stale + // here (marks are only retired on Present and on the slow path); staleness + // is conservative - the in-flight span reads too large, the check fails, + // and the slow path retires marks and re-tries. + Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset) { + if (size == 0) return false; + auto& store = ring.store; + if (store.id != 0 && store.contextGeneration == g_bufferContextGeneration) { + const SizeT alignedSize = RingAlignUp(size, store.alignment); + // Ring sizes are the ring's initial size (a power of two) doubled + // some number of times, so the offset modulo reduces to a mask. + static_assert((kUboRingInitialBytes & (kUboRingInitialBytes - 1)) == 0, + "ring offset mask below requires power-of-two ring sizes"); + static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0, + "ring offset mask below requires power-of-two ring sizes"); + static_assert((kUploadRingInitialBytes & (kUploadRingInitialBytes - 1)) == 0, + "ring offset mask below requires power-of-two ring sizes"); + const SizeT offset = static_cast(store.head & (store.size - 1)); + if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) { + store.head += alignedSize; + outOffset = offset; + return true; + } + } + return RingAllocateSlow(ring, size, outOffset); + } + + Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset) { + if (!RingAvailable(ring)) return false; + auto& store = ring.store; + // Sizing the store first, so the request is rounded with the alignment + // the live store actually carries rather than the pre-creation default. + if (store.id == 0 && !CreateRingStorage(ring, RingAlignUp(size, store.alignment))) { + return false; + } + const SizeT alignedSize = RingAlignUp(size, store.alignment); + + // Advance tail past every frame the GPU provably finished. + const Uint64 completed = DirectGLES::CompletedFrameSerial(); + SizeT retiredMarks = 0; + for (const auto& mark : ring.frameMarks) { + if (mark.frameSerial > completed) break; + if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent; + ++retiredMarks; + } + if (retiredMarks > 0) { + ring.frameMarks.erase(ring.frameMarks.begin(), + ring.frameMarks.begin() + static_cast(retiredMarks)); + } + + // A slot may not straddle the ring end; pad the cursor to the boundary. + SizeT offset = static_cast(store.head % store.size); + if (offset + alignedSize > store.size) { + store.head += store.size - offset; + offset = 0; + } + + if (store.head + alignedSize - store.tail > store.size) { + // In-flight span would overrun live slots: grow instead of overwrite. + if (CreateRingStorage(ring, std::max(store.size * 2, alignedSize))) { + offset = 0; + } else if (store.creationFailed) { + return false; // store lost; callers fall back to their legacy path + } else { + // At the size cap (>maxBytes in flight). First try to free room + // by waiting for the OLDEST in-flight frames to retire - a + // bounded wait that ends as soon as enough tail space exists, + // instead of draining the entire queue. + constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame + while (!ring.frameMarks.empty() && + store.head + alignedSize - store.tail > store.size) { + const auto& oldest = ring.frameMarks.front(); + if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) { + break; + } + if (oldest.headAtPresent > store.tail) store.tail = oldest.headAtPresent; + ring.frameMarks.erase(ring.frameMarks.begin()); + } + if (store.head + alignedSize - store.tail <= store.size) { + offset = static_cast(store.head % store.size); + if (offset + alignedSize > store.size) { + store.head += store.size - offset; + offset = 0; + } + store.head += alignedSize; + outOffset = offset; + return true; + } + // No usable fence covers the oldest frames: drain once rather than + // corrupt live slots. + if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); + store.tail = store.head; + ring.frameMarks.clear(); + // Same-frame slots written before the drain may now be recycled by + // the very next allocations; a generation bump keeps later draws + // from rebinding those cached offsets. + ++store.generation; + offset = static_cast(store.head % store.size); + if (offset + alignedSize > store.size) { + store.head += store.size - offset; + offset = 0; + } + } + } + + store.head += alignedSize; + outOffset = offset; + return true; + } + + namespace { + void DeleteOrphanRingBuffers() { + const std::lock_guard lock(g_orphanRingBuffersMutex); + auto& orphans = OrphanRingBuffers(); + if (orphans.empty()) return; + for (const OrphanRingBuffer& orphan : orphans) { + // A buffer of a share group that is gone was reclaimed with it. + if (orphan.contextGeneration != g_bufferContextGeneration) continue; + ScrubBufferBindingShadowsForId(orphan.id); + g_GLESFuncs.glDeleteBuffers(1, &orphan.id); + } + orphans.clear(); + } + + // Present()-time upkeep shared by both rings. + void RingOnPresent(PersistentRing& ring) { + if (!CanTouchGLNow()) return; + DeleteOrphanRingBuffers(); + + // Delete grown-away stores the GPU is provably done with. + const Uint64 completed = DirectGLES::CompletedFrameSerial(); + for (SizeT i = ring.retired.size(); i-- > 0;) { + RetiredRingStore& entry = ring.retired[i]; + const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration; + if (!staleContext && entry.retireSerial > completed) continue; + if (!staleContext && entry.id != 0) { + ScrubBufferBindingShadowsForId(entry.id); + g_GLESFuncs.glDeleteBuffers(1, &entry.id); + } + ring.retired[i] = ring.retired.back(); + ring.retired.pop_back(); + } + + auto& store = ring.store; + if (store.id == 0 || store.contextGeneration != g_bufferContextGeneration) return; + // Retire completed marks here too — RingAllocate is the main consumer, + // but frames that used the ring for nothing would otherwise let the list + // grow one entry per Present, unboundedly. + SizeT retiredMarks = 0; + for (const auto& mark : ring.frameMarks) { + if (mark.frameSerial > completed) break; + if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent; + ++retiredMarks; + } + if (retiredMarks > 0) { + ring.frameMarks.erase(ring.frameMarks.begin(), + ring.frameMarks.begin() + static_cast(retiredMarks)); + } + // Record this frame's high-water mark (Present just fenced the serial now + // reported by CurrentFrameSerial()). A fence-less Present repeats the + // serial; fold into the existing mark. + const Uint64 serial = DirectGLES::CurrentFrameSerial(); + if (!ring.frameMarks.empty() && ring.frameMarks.back().frameSerial == serial) { + ring.frameMarks.back().headAtPresent = store.head; + } else { + ring.frameMarks.push_back({serial, store.head}); + } + } + } // namespace + + Bool UboRingAvailable() { + if (MG_Config::Features.EsprytDisableUboRing) return false; + return RingAvailable(UboRing()); + } + + Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); } + + UboRingSlotState UboRingPrepareSlot(UboRingAllocation& slot, Uint32 contentVersion, Uint64 frameSerial, + SizeT bindSize, void*& outWrite, Uint& outBufferId) { + if (MG_Config::Features.EsprytDisableUboRing) return UboRingSlotState::Unavailable; + PersistentRing& ring = UboRing(); + if (!RingAvailable(ring)) return UboRingSlotState::Unavailable; + if (slot.ringGeneration == ring.store.generation && slot.frameSerial == frameSerial && + slot.contentVersion == contentVersion) { + outBufferId = ring.store.id; + return UboRingSlotState::Reused; + } + SizeT offset = 0; + if (!RingAllocate(ring, bindSize, offset)) return UboRingSlotState::Failed; + // Read after the allocation: a grow mints a new store and generation. + outWrite = static_cast(ring.store.mappedPtr) + offset; + slot = {contentVersion, ring.store.generation, frameSerial, offset}; + outBufferId = ring.store.id; + return UboRingSlotState::Allocated; + } + + void* UboRingMappedPtr() { return UboRing().store.mappedPtr; } + Uint UboRingBufferId() { return UboRing().store.id; } + Uint32 UboRingGeneration() { return UboRing().store.generation; } + + void UboRingOnPresent() { RingOnPresent(UboRing()); } + + Bool UnpackRingAvailable() { + if (MG_Config::Features.EsprytDisableUnpackRing) return false; + return RingAvailable(UnpackRing()); + } + + Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) { + // A request the ring could never satisfy even empty would otherwise walk + // the whole grow/drain ladder before failing. + if (size > kUnpackRingMaxBytes) return false; + return RingAllocate(UnpackRing(), size, outOffset); + } + + void* UnpackRingMappedPtr() { return UnpackRing().store.mappedPtr; } + Uint UnpackRingBufferId() { return UnpackRing().store.id; } + SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; } + + void UnpackRingOnPresent() { RingOnPresent(UnpackRing()); } + + void UploadRingOnPresent() { RingOnPresent(UploadRing()); } + } // namespace BufferImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp new file mode 100644 index 00000000..74a76bc5 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp @@ -0,0 +1,1479 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + const MG_Pipe::MGPipeResourceRecord* PipeRenderbufferRecordForHandle(MG_Pipe::MGPipeHandle res) { + return PipeRecordAt(MG_Pipe::MGPipeApplier().RenderbufferResources, res); + } + + namespace FramebufferImpl { + BackendFramebufferObject::BackendFramebufferObject() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // Identity until a non-identity draw-buffer array forces a relocation. A framebuffer + // that is never draw-bound never runs the recompute, so the table has to start out + // matching what the attachment loop will physically do. + for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) { + m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i; + } + g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId); + m_contextGeneration = g_backendContextGeneration; + if (m_backendFBOId == 0) { + MGLOG_E_ONCE("Failed to generate framebuffer object."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + } else { + MGLOG_D("Generated framebuffer object with ID: %u.", m_backendFBOId); + } + } + + BackendFramebufferObject::~BackendFramebufferObject() { + if (InProcessTeardown()) { + return; // see InProcessTeardown(): the driver may be unloaded already + } + if (m_backendFBOId == 0) { + return; + } + // Scrub the binding shadow whether or not the id can still be deleted: a + // recycled name must never satisfy the shadow's dedup. + NoteFramebufferIdDeleted(m_backendFBOId); + if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) { + g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId); + } + m_backendFBOId = 0; + } + + void BackendFramebufferObject::Bind(FramebufferTarget target) const { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (target == FramebufferTarget::Read) + BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId); + else + BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId); + } + + namespace { + // Driver-level framebuffer-binding shadow (see Managers.h). Indexed by + // FramebufferTarget {Draw, Read}. + Array g_driverFBOBindings = {0, 0}; + Array g_driverFBOBindingKnown = {false, false}; + } // namespace + + void BindFramebufferId(GLenum fbTarget, Uint id) { + const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER; + const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER; + const SizeT drawIdx = SizeT(FramebufferTarget::Draw); + const SizeT readIdx = SizeT(FramebufferTarget::Read); + const Bool drawMatches = + !bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id); + const Bool readMatches = + !bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id); + if (drawMatches && readMatches) { + return; + } + g_GLESFuncs.glBindFramebuffer(fbTarget, id); + if (bindsDraw) { + g_driverFBOBindings[drawIdx] = id; + g_driverFBOBindingKnown[drawIdx] = true; + } + if (bindsRead) { + g_driverFBOBindings[readIdx] = id; + g_driverFBOBindingKnown[readIdx] = true; + } + } + + Uint CurrentFramebufferBinding(FramebufferTarget target) { + const SizeT idx = SizeT(target); + if (!g_driverFBOBindingKnown[idx]) { + // Cold path: pin the shadow from the driver once (init probes and + // pre-shadow code bind raw but restore what they found). + GLint binding = 0; + g_GLESFuncs.glGetIntegerv( + target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING, + &binding); + g_driverFBOBindings[idx] = static_cast(binding); + g_driverFBOBindingKnown[idx] = true; + } + return g_driverFBOBindings[idx]; + } + + void NoteFramebufferIdDeleted(Uint id) { + if (id == 0) { + return; + } + for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) { + if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) { + g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0 + } + } + } + + void InvalidateFramebufferBindingCache() { + g_driverFBOBindings = {0, 0}; + g_driverFBOBindingKnown = {false, false}; + // The per-target "already synced" memo describes work pushed into the ES context + // that is being left or replaced. Both callers (MakeCurrent, DestroyEGLContext) + // mean the context may have been reset under us, so claim nothing is synced: + // a null object never matches a real binding, so SyncCurrentFBO re-pushes. + g_fboSyncedSlotVersions = {0}; + g_fboSyncedObjectVersions = {0}; + g_fboSyncedObjects = {}; +#if MOBILEGL_ESPRYT_FBO_HANDLE_ARM_MEMOS_LINKED + // E's MAJOR-4 / A9: the handle arm's own memos say the same thing about the same + // targets, so they are invalidated HERE rather than at each of the nine call sites - + // six in DirectGLES.cpp and three in SanityTest.cpp, and it was the three that were + // missed. See the declaration in Managers.h for why the call is gated and which line + // each package flips. + InvalidateFramebufferHandleArmMemos(); +#endif + } + + void BackendFramebufferObject::InvalidateSyncedState() { + std::fill(std::begin(m_frontendDrawBuffers), std::end(m_frontendDrawBuffers), + FramebufferAttachmentType::Unknown); + std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE); + // NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every + // attachment it had, possibly under a non-identity permutation. Declaring the table + // identity here is safe only because every gate that decides which points get + // re-issued is invalidated too - the attachment versions below, and under + // MOBILEGL_PIPE_PUSH the record memos stamped at the end of this function - so the + // next sync re-attaches all non-empty attachments at their identity points AND + // (see SyncToBackend's attachment loop) detaches any colour point whose frontend owner + // is empty. Without that detach a stale image would survive under a point the table now + // claims for a different, empty attachment. + for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) { + m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i; + } + m_frontendReadBuffer = FramebufferAttachmentType::Unknown; + m_backendReadBuffer = GL_NONE; + std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(), + static_cast(~0u)); + // Every attachment version is invalidated above, so the next walk re-attaches + // everything regardless; stamp the generation so it does not re-arm twice. + m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration; + // The record memos are the gate the PIPE_PUSH arms' attachment walks actually read - + // the handle arm has nothing narrower under its key - so leaving them standing here + // would let a re-sync of the same record skip the re-attach this function exists to + // force. 0 is never a live hash, so a zeroed memo is a guaranteed miss. + m_syncedRecordHashes = {0}; + m_syncedAttachmentRecordHash = 0; + m_syncedNoAttachmentDefaultsValid = false; + } + + static Bool IsSnormFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGBA8Snorm: + case TextureInternalFormat::R16Snorm: + case TextureInternalFormat::RG16Snorm: + case TextureInternalFormat::RGB16Snorm: + case TextureInternalFormat::RGBA16Snorm: + return true; + default: + return false; + } + } + + static Bool IsUnormFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R16: + case TextureInternalFormat::RG16: + case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGBA16: + return true; + default: + return false; + } + } + + Uint32 g_alphaWidenedDrawBufferMask = 0; + Uint32 g_integerColorDrawBufferMask = 0; + + static Bool IsIntegerColorFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8I: + case TextureInternalFormat::R8UI: + case TextureInternalFormat::R16I: + case TextureInternalFormat::R16UI: + case TextureInternalFormat::R32I: + case TextureInternalFormat::R32UI: + case TextureInternalFormat::RG8I: + case TextureInternalFormat::RG8UI: + case TextureInternalFormat::RG16I: + case TextureInternalFormat::RG16UI: + case TextureInternalFormat::RG32I: + case TextureInternalFormat::RG32UI: + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGB32UI: + case TextureInternalFormat::RGBA8I: + case TextureInternalFormat::RGBA8UI: + case TextureInternalFormat::RGBA16I: + case TextureInternalFormat::RGBA16UI: + case TextureInternalFormat::RGBA32I: + case TextureInternalFormat::RGBA32UI: + case TextureInternalFormat::RGB10A2UI: + return true; + default: + return false; + } + } + + // THE FOUR CROSS-OBJECT MASKS, ANSWERED FROM THE RECORD (ID-12 DV-5, review M-3 / DV-5). + // + // All four reduce to (internal format, TEXTURE TARGET) for a texture attachment and to + // (internal format) for a renderbuffer one, and MGPSurface now carries both inline - + // InternalFormat since D-C1 and TextureTarget since c0c widened Pad0 into it. So the + // handle arm stops reading the frontend attachment objects to compute them, which is + // what D-C1's "the four cross-object masks fall out at push time with no lookup" meant. + // + // ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha / their renderbuffer + // siblings are UNTOUCHED - two of them are on D-N's byte-identical list. Only what they + // are ASKED changes, which is the whole point of carrying the target rather than + // inventing a TextureUploadTarget -> TextureTarget inverse: feeding a D-N-pinned + // function a guessed input would silently change what every texture is allocated as. + // + // Kind IS the gate (c0c: "consulted only when Kind == kMGPipeSurfaceKindTexture"), and + // kMGPipeSurfaceNoTextureTarget on a texture point is a seam defect - refused loudly and + // answered false, never guessed. False is the safe direction for all four: the mask + // turns an emulation ON, and an emulation that does not run leaves the driver's own + // (correct-for-the-real-format) behaviour, while one that runs on the wrong buffer + // clamps or overwrites texels the application wrote. + static Bool PushedSurfaceTextureTarget(const MG_Pipe::MGPSurface& surface, TextureTarget* out) { + if (surface.TextureTarget == MG_Pipe::kMGPipeSurfaceNoTextureTarget) { + MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} says Kind=texture but carries no " + "texture target - refusing to guess one for the cross-object masks", + surface.Res.Slot, surface.Res.Gen); + return false; + } + *out = static_cast(surface.TextureTarget); + return true; + } + + static Bool IsSnormFallbackSurface(const MG_Pipe::MGPSurface& surface) { + const auto format = static_cast(surface.InternalFormat); + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { + TextureTarget target = TextureTarget::Unknown; + return PushedSurfaceTextureTarget(surface, &target) && IsSnormFormat(format) && + TextureImpl::ShouldUseCaveatTextureFormat(format, target); + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { + return IsSnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format); + } + return false; + } + + static Bool IsUnormFallbackSurface(const MG_Pipe::MGPSurface& surface) { + const auto format = static_cast(surface.InternalFormat); + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { + TextureTarget target = TextureTarget::Unknown; + return PushedSurfaceTextureTarget(surface, &target) && IsUnormFormat(format) && + TextureImpl::ShouldUseCaveatTextureFormat(format, target); + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { + return IsUnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format); + } + return false; + } + + static Bool IsAlphaWidenedColorSurface(const MG_Pipe::MGPSurface& surface) { + const auto format = static_cast(surface.InternalFormat); + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { + TextureTarget target = TextureTarget::Unknown; + return PushedSurfaceTextureTarget(surface, &target) && + TextureImpl::BackendTextureFormatAddsAlpha(format, target); + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { + return TextureImpl::BackendRenderbufferFormatAddsAlpha(format); + } + return false; + } + + static Bool IsIntegerColorSurface(const MG_Pipe::MGPSurface& surface) { + // No target and no caveat table: integerness is a property of the format alone, so + // this one is the same question on both arms. + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) { + return false; + } + return IsIntegerColorFormat(static_cast(surface.InternalFormat)); + } + + // The record's surface for an attachment POINT, or null when this record does not + // describe that point at all. MGPFramebufferState carries Color[8] + Depth + Stencil + // (D-C1), which is every point a framebuffer can hold on the handle arm: D-C3 refuses + // bit 9 outright on a driver reporting more than 8 colour attachments, and the + // FRONT/BACK points belong to the DEFAULT framebuffer, which has no twin at all. + const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record, + FramebufferAttachmentType point) { + if (point == FramebufferAttachmentType::Depth) return &record.Depth; + if (point == FramebufferAttachmentType::Stencil) return &record.Stencil; + if (point < FramebufferAttachmentType::Color0 || point > FramebufferAttachmentType::Color31) { + return nullptr; + } + const Int index = static_cast(point) - static_cast(FramebufferAttachmentType::Color0); + if (index >= static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) return nullptr; + return &record.Color[static_cast(index)]; + } + + // SyncAttachmentObject's handle arm (review M-3). WHAT MOVES IS THE RESOLUTION: the twin + // is adopted from MGPSurface::Res through the slot table instead of being looked up by + // the frontend object's ADDRESS, and the attach SHAPE - layered, level, layer, the + // upload target and the texture target - is read off the surface instead of off the + // frontend attachment object. + // + // P5e (fb, CONTRACT-P5E.md §5.4): THE FRONTEND ATTACHMENT PARAMETER IS GONE. It was + // here for two things and P5e retires both. The storage syncs now have by-handle forms + // (SyncMipmapsToBackendByHandle / SyncToBackendByHandle), so the object is no longer + // needed to reach the texels; and the record is now the ONLY statement of what is + // attached, so the cross-checks had nothing left to corroborate against - a second + // answer that only a monolith build could produce is not a check, it is a second + // writer. With the object gone, review N-6's hole ("record empty, frontend still + // holding") is UNREPRESENTABLE rather than refused: the caller's detach and this + // function's attach now read the SAME test, `Kind == None || Res null`. + // + // The two P5c cross-checks that ran HandleOf on the apply thread go with it. The + // renderbuffer one (the one that was NOT transport-gated, unlike its texture sibling) + // is deleted by the rekey rather than gated, which is what CONTRACT-P5E §4.4 asks for: + // gating it would have left a client-allocator probe compiled into a path a run-ahead + // apply reaches, and there is no longer anything for it to say. + // + // P5e (fix1, ID-81 / §5.8): THE STORAGE SYNC IS ARM-SELECTED AND THE MONOLITH ARM KEEPS + // ITS FRONTEND OBJECT. This function has TWO callers and they are on opposite arms: the + // handle form (SyncToBackendByHandle) has no frontend object and must not acquire one, + // while the OBJECT form (SyncToBackend, the push-monolith arm - P4a drives its + // attachments from the record too, gated on FramebufferSubsystemEnabled() alone, D-C2) + // is holding the frontend attachment the whole time. fb dropped the parameter for both, + // which routed the monolith arm into tx2's by-handle storage seam; that seam passes a + // NULL SharedPtr and relies on the record arm being selected, and under + // Transport == Monolith it is not - `stateTextureObject->IsTextureView()` then + // dereferenced null (the device's Lightmap. -> clearColorTexture -> glClear + // SIGSEGV). So the parameter comes back as MONOLITH GLUE, used for nothing but the + // storage sync: the attach SHAPE, the empty-point test and the twin adoption stay the + // record's on both arms, which is what keeps N-6's hole closed and the cross-checks + // deleted. It is a pointer and it is null on the handle arm, so "which arm am I on" is + // never inferred from it - MG_Config::Transport decides, as §5.8 requires, and a + // monolith arm that somehow arrives without one refuses by name instead of attaching a + // texture whose levels were never pushed. + static Bool SyncAttachmentSurface(GLenum glFBOTarget, const MG_Pipe::MGPSurface& surface, + GLenum glBackendAttachment, + const MG_State::GLState::FramebufferAttachmentObject* monolithAttachment) { + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(surface.Res)) { + // An empty point: the caller detached it on exactly this test, so attaching + // nothing here is the whole of the agreement. (The legacy arm does the same.) + return true; + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) { + // The record's handle is validated BEFORE the frontend is consulted (N-3): it is + // the untrusted half, the frontend cross-check below is the corroboration, and + // asking in this order is what gives the two adoption refusals a reachable + // caller at all. + if (!PipeTwinHandleIsAdoptable(TextureImpl::g_backendTextureObjects, surface.Res, "texture")) { + return false; + } + auto* twinSlot = AdoptTwinByHandle(TextureImpl::g_backendTextureObjects, surface.Res, "texture"); + if (twinSlot == nullptr) { + return false; // AdoptTwinByHandle named the refusal + } + if (!*twinSlot) { + *twinSlot = MakeShared(); + } + // COPIED OUT of the table: SyncMipmapsToBackend can grow it (a re-mint adopts + // another handle), and Managers.h:381-385's pointer-invalidation warning applies + // to exactly this sequence. + SharedPtr backendTextureObject = *twinSlot; + if (!backendTextureObject) { + MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__); + return false; + } + // P5e (fb): the storage sync BY HANDLE (tx2's seam). The level shadow it reads + // is the applier's staged texture store, not a frontend object, which is what + // let the object parameter go - UNDER A TRANSPORT. P5e (fix1, ID-81): the + // push-monolith arm keeps the frontend sync it had at f6cfcbd3, token for token, + // because tx2's record arm inside SyncMipmapsToBackend is itself selected by + // `Transport != Monolith` and cannot answer here. + backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res); + // From here on every draw, clear and blit into this framebuffer can write the + // texture behind the staged shadow's back (the frontend arm's note, by handle). + TextureImpl::NoteDriverSideTextureWriteByHandle(surface.Res); + const auto uploadTarget = static_cast(surface.UploadTarget); + if (surface.Layered != 0) { + g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment, + backendTextureObject->GetBackendTextureId(), + static_cast(surface.Level)); + } else if (uploadTarget == TextureUploadTarget::Texture3D || + uploadTarget == TextureUploadTarget::Texture2DArray || + uploadTarget == TextureUploadTarget::Texture1DArray || + uploadTarget == TextureUploadTarget::CubeMapArray || + uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) { + g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment, + backendTextureObject->GetBackendTextureId(), + static_cast(surface.Level), + static_cast(surface.Layer)); + } else { + auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + if (glTextureTarget == GL_UNKNOWN_MGL) { + TextureTarget target = TextureTarget::Unknown; + if (!PushedSurfaceTextureTarget(surface, &target)) { + return false; + } + glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + } + // Same cube-face rule as the legacy arm: glBindTexture rejects the face + // enums, so bind through the owning cube target and attach with the face. + const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && + glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; + backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget); + g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget, + backendTextureObject->GetBackendTextureId(), + static_cast(surface.Level)); + } + return true; + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) { + // Same order as the texture arm above, for the same reason (N-3). + if (!PipeTwinHandleIsAdoptable(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res, + "renderbuffer")) { + return false; + } + auto* twinSlot = AdoptTwinByHandle(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res, + "renderbuffer"); + if (twinSlot == nullptr) { + return false; + } + if (!*twinSlot) { + *twinSlot = MakeShared(); + } + SharedPtr backendRenderbufferObject = *twinSlot; + if (!backendRenderbufferObject) { + MGLOG_E_ONCE("%s: No backend renderbuffer found for FBO attachment.", __func__); + return false; + } + // P5e (fb): the four allocation values were already record-supplied (D-D2); what + // moves here is the LOOKUP - the handle the surface carries, never HandleOf on + // an object the apply thread was handed. + // + // P5e (fix1, ID-81): arm-selected for the texture arm's reason, and here it is a + // BEHAVIOUR difference even though the by-handle body dereferences nothing: the + // object form reports a refused allocation to the application through the live + // GLContext (RecordError) and the by-handle form deliberately does not, because + // under a transport there is no application on this side to report it to. On the + // push-monolith arm there is, so the monolith arm keeps the object form. + backendRenderbufferObject->SyncToBackendByHandle(surface.Res); + backendRenderbufferObject->Bind(); + g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, + backendRenderbufferObject->GetBackendRenderbufferId()); + return true; + } + MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} carries Kind=%u, which is neither a texture " + "nor a renderbuffer nor an empty point - refusing to attach", + surface.Res.Slot, surface.Res.Gen, surface.Kind); + return false; + } + + // The read attachment's storage carries an alpha channel its frontend format does not + // (the three-channel colour-renderable widening). GL answers such a read with 1.0, but + // the storage holds whatever the draw wrote there, so the readback has to overwrite it. + Bool IsAlphaWidenedFallbackReadAttachment() { + const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer(); + return record && IsAlphaWidenedColorSurface(record->ReadSurface); + } + + Bool IsFixedPointFallbackReadAttachment() { + const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer(); + return record && record->ReadSurface.Kind != MG_Pipe::kMGPipeSurfaceKindNone && + IsSnormFormat(static_cast(record->ReadSurface.InternalFormat)); + const auto& readFBO = + MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); + if (!readFBO) { + return false; + } + const auto readBuffer = readFBO->GetReadBuffer(); + if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) { + return false; + } + // Any signed-normalized attachment, not just the ones currently substituted: + // ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer + // hands back the negative half that desktop GL clamps away. + const auto& attachmentObject = readFBO->GetAttachment(readBuffer); + if (attachmentObject.IsTexture()) { + const auto& textureObject = attachmentObject.GetTexture(); + return textureObject && IsSnormFormat(textureObject->GetFormat()); + } + if (attachmentObject.IsRenderbuffer()) { + const auto& renderbufferObject = attachmentObject.GetRenderbuffer(); + return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat()); + } + return false; + } + + const MG_Pipe::MGPFramebufferState* PushedFramebufferRecord(MG_Pipe::MGPipeHandle fbo) { + // FramebufferRecordFor answers null for the null handle and for a slot nothing has + // described (both silent - that is every framebuffer before its first emission), and + // refuses a stale generation loudly while counting it in + // StaleFramebufferRecordLookups. So there is no second emptiness test here to get out + // of step with the applier's, and none of the three cases is a binding question. + return MG_Pipe::MGPipeApplier().FramebufferRecordFor(fbo); + } + + // The record's DrawBuffers[] is an ATTACHMENT INDEX with -1 for None (D-C1), which is + // what the frontend array holds for every framebuffer that has a twin. It deliberately + // cannot spell FrontLeft / FrontRight / BackLeft / BackRight: those are the DEFAULT + // framebuffer's own tokens, the default framebuffer has no BackendFramebufferObject at + // all (it is pDefaultFramebufferInfo->defaultFBO, which is why MGPipeHandles.h reserves + // {0,1} for it), and the legacy arm's branch for them says so in as many words - "shouldn't + // remap". So the conversion below is total for every object that reaches this twin, and + // the record's IsDefault is the thing a later phase would test if that ever changed. + static void DecodePushedDrawBuffers(const MG_Pipe::MGPFramebufferState& record, + FramebufferAttachmentType* out) { + for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + const Int8 index = record.DrawBuffers[i]; + out[i] = index < 0 ? FramebufferAttachmentType::None + : static_cast( + static_cast(FramebufferAttachmentType::Color0) + index); + } + } + + // P5e (fb, §5.4): the record half of SyncReadBufferToBackend, lifted out of it so the + // by-handle sync can run it WITHOUT a frontend object. Nothing inside reads one: the + // frontend index was only ever a diagnostic, and it is passed as a number now + // (GlNameForDiag is 0 on the handle arm, which reads as "the record did not say"). + void BackendFramebufferObject::ApplyReadBufferFromRecord(const MG_Pipe::MGPFramebufferState& record, + Uint glNameForDiag) { + // D-C1 makes the NULL Res the spelling of "no attachment" and c0c's + // kMGPipeSurfaceKindNone makes Kind agree with it on a zero-initialised record. + // Res stays the test - it is the one D-C1 fixes - and Kind is CROSS-CHECKED + // rather than consulted: the two are one statement the emitter has to make + // twice, and a record where they disagree is a seam defect, not an empty point. + // Picking a winner silently is what this refuses to do. + const Bool readSurfaceEmpty = MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res); + if (readSurfaceEmpty != (record.ReadSurface.Kind == MG_Pipe::kMGPipeSurfaceKindNone)) { + MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface says Res={%u, %u} and " + "Kind=%u, which disagree about whether it names anything - refusing " + "rather than choosing one of them", + glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen, + record.ReadSurface.Kind); + return; + } + FramebufferAttachmentType pushedReadBuf = FramebufferAttachmentType::None; + if (!readSurfaceEmpty) { + // EVERY FIELD THAT IDENTIFIES THE IMAGE, not the three v1 compared (m-4). + // The same image legally sits at two colour points - a layered attachment of + // one array and a single slice of it are different surfaces of one Res - so a + // partial comparison resolves to the lower index and the refusal branch below + // ("refusing to guess") would then be doing part of the guessing itself. + // InternalFormat and TextureTarget are properties of the RESOURCE rather than + // of the point, so they add nothing to the identity and are left out. + Bool matched = false; + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + const auto& color = record.Color[i]; + if (color.Res == record.ReadSurface.Res && color.Kind == record.ReadSurface.Kind && + color.Layered == record.ReadSurface.Layered && + color.Level == record.ReadSurface.Level && color.Layer == record.ReadSurface.Layer && + color.UploadTarget == record.ReadSurface.UploadTarget) { + pushedReadBuf = static_cast( + static_cast(FramebufferAttachmentType::Color0) + static_cast(i)); + matched = true; + break; + } + } + if (!matched) { + MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface {%u, %u} matches no " + "colour attachment of its own record - refusing to guess a read " + "buffer", + glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen); + return; + } + } + if (pushedReadBuf == m_frontendReadBuffer) { + return; + } + m_frontendReadBuffer = pushedReadBuf; + + const GLenum glPushedReadBuffer = GetBackendAttachmentType(pushedReadBuf); + if (m_backendReadBuffer != glPushedReadBuffer) { + m_backendReadBuffer = glPushedReadBuffer; + // Still bound as READ first, for the reason the legacy arm gives below: + // glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER. + Bind(FramebufferTarget::Read); + g_GLESFuncs.glReadBuffer(glPushedReadBuffer); + } + } + + // P5e (fb): the by-handle entry, for SyncCurrentFBOByRecord's "one object on both + // bindings" skip - the one path that applies a read buffer without doing the rest of + // the sync. A framebuffer with no record is the caller's seam defect, not a silent + // no-op, so it says so. + void BackendFramebufferObject::SyncReadBufferToBackendByHandle(MG_Pipe::MGPipeHandle fbo) { + const auto* record = PushedFramebufferRecord(fbo); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so its read " + "buffer cannot be pushed", + fbo.Slot, fbo.Gen); + return; + } + ApplyReadBufferFromRecord(*record, 0); + } + + Bool BackendFramebufferObject::RecomputeBackendColorSlots( + const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) { + // Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own + // limit (ValidateColorAttachmentInRange, which reads the clamped + // GetDynamicParameters().MaxColorAttachments) is never larger than this raw ES cap, so an + // index the frontend accepted is always < slotCount. Indices at or above it can never own + // an image and stay on their identity point - never touched, never a GL error. + const Uint slotCount = + std::min(MAX_COLOR_ATTACHMENT_SLOTS, + static_cast(std::max(g_GLESCapabilities.MaxColorAttachments, 1))); + + GLenum newSlots[MAX_COLOR_ATTACHMENT_SLOTS]; + for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) { + newSlots[i] = GL_COLOR_ATTACHMENT0 + i; + } + Bool assigned[MAX_COLOR_ATTACHMENT_SLOTS] = {false}; + Bool slotTaken[MAX_COLOR_ATTACHMENT_SLOTS] = {false}; + + // 1. ES pins draw-buffer slot s to GL_COLOR_ATTACHMENTs, so an attachment named by draw + // buffer slot s has no choice: its image must sit at backend point s. This has to + // agree with the compaction the caller just pushed through glDrawBuffers. + for (Uint s = 0; s < FramebufferObject::MAX_DRAW_BUFFERS && s < slotCount; ++s) { + const auto frontendBuf = stateDrawBuffers[s]; + if (frontendBuf < FramebufferAttachmentType::Color0 || + frontendBuf > FramebufferAttachmentType::Color31) { + continue; // GL_NONE, or a default-framebuffer FRONT/BACK token: never relocated. + } + const Uint a = + static_cast(frontendBuf) - static_cast(FramebufferAttachmentType::Color0); + // Neither guard may ever fire: a duplicate draw buffer is already INVALID_OPERATION + // and an out-of-range one is rejected by ValidateColorAttachmentInRange. If one did + // fire the table would disagree with the glDrawBuffers the caller already issued, + // which is the exact non-injectivity this table exists to remove. + MOBILEGL_ASSERT(a < slotCount && !assigned[a], + "Draw buffer %u names colour attachment %u which is out of range or duplicated.", s, + a); + if (a >= slotCount || assigned[a]) { + continue; + } + newSlots[a] = GL_COLOR_ATTACHMENT0 + s; + assigned[a] = true; + slotTaken[s] = true; + } + + // 2. Everything else keeps its identity point when that point survived step 1. This is + // what makes the ordinary drawBuffers[s] == COLOR_ATTACHMENTs case a strict no-op: + // the table stays identity, nothing moves, no attachment is re-issued. + for (Uint a = 0; a < slotCount; ++a) { + if (assigned[a] || slotTaken[a]) { + continue; + } + newSlots[a] = GL_COLOR_ATTACHMENT0 + a; + assigned[a] = true; + slotTaken[a] = true; + } + + // 3. What is left are attachments whose identity point step 1 took away. Park them on the + // lowest free point. They are not draw buffers, so nothing is rendered through them; + // they only have to stay addressable for glReadBuffer and blits, and the map has to + // stay injective so reading one of them cannot land on another's image. + for (Uint a = 0; a < slotCount; ++a) { + if (assigned[a]) { + continue; + } + for (Uint s = 0; s < slotCount; ++s) { + if (!slotTaken[s]) { + newSlots[a] = GL_COLOR_ATTACHMENT0 + s; + assigned[a] = true; + slotTaken[s] = true; + break; + } + } + } + + Bool moved = false; + for (Uint a = 0; a < MAX_COLOR_ATTACHMENT_SLOTS; ++a) { + if (m_backendColorSlots[a] == newSlots[a]) { + continue; + } + m_backendColorSlots[a] = newSlots[a]; + moved = true; + // This attachment's image now belongs at a different backend point. Its frontend + // version has not changed, so the attachment loop would skip it; force it. + m_syncedFrontendAttachmentVersions[static_cast(FramebufferAttachmentType::Color0) + a] = + static_cast(~0u); + } + return moved; + } + + // ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------ + // + // WHY IT EXISTS. ScopedDetachedTextureFramebufferAttachments used to answer "which + // framebuffers currently have this texture attached" by walking every live twin and + // reading each one's FRONTEND attachment array. Under P5e that walk is the last place + // the server holds a frontend framebuffer across records - id re-typed ForEachLive and + // left the StateForHandle read greppable, naming this package as the one that replaces + // it. The records already carry the answer: every applied set_framebuffer_state names + // its eleven surfaces, so the relation is maintained where the record is consumed. + // + // KEYED ON THE TEXTURE SLOT rather than on {slot, gen}. The question a detach asks is + // "does the driver have this texture id hanging off some framebuffer", the points are + // re-derived from the record on every hit, and a stale entry left by a recycled slot + // therefore costs one record walk that matches nothing. The FRAMEBUFFER handles are + // stored whole, so a recycled framebuffer slot never answers for its predecessor + // (FindByHandle refuses a moved Gen). + // + // MAINTAINED AT THE SYNC, NOT AT THE APPLY, and that is the same set rather than a + // smaller one: a framebuffer whose record was never synced has no driver attachment to + // detach, and every path that can have one runs this sync first. + + namespace { + UnorderedMap> g_framebuffersByAttachedTextureSlot; + UnorderedMap> g_attachedTextureSlotsByFramebufferSlot; + + void ForgetFramebufferTextureAttachmentsLocked(Uint32 fboSlot) { + auto it = g_attachedTextureSlotsByFramebufferSlot.find(fboSlot); + if (it == g_attachedTextureSlotsByFramebufferSlot.end()) return; + for (const Uint32 textureSlot : it->second) { + auto listIt = g_framebuffersByAttachedTextureSlot.find(textureSlot); + if (listIt == g_framebuffersByAttachedTextureSlot.end()) continue; + auto& list = listIt->second; + list.erase(std::remove_if(list.begin(), list.end(), + [&](MG_Pipe::MGPipeHandle h) { return h.Slot == fboSlot; }), + list.end()); + if (list.empty()) g_framebuffersByAttachedTextureSlot.erase(listIt); + } + g_attachedTextureSlotsByFramebufferSlot.erase(it); + } + } // namespace + + void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo, + const MG_Pipe::MGPFramebufferState& record) { + if (MG_Pipe::MGPipeHandleIsNull(fbo)) return; + ForgetFramebufferTextureAttachmentsLocked(fbo.Slot); + Vector textureSlots; + const auto note = [&](const MG_Pipe::MGPSurface& surface) { + if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; + if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return; + if (std::find(textureSlots.begin(), textureSlots.end(), surface.Res.Slot) != textureSlots.end()) { + return; // one texture at two points is one entry + } + textureSlots.push_back(surface.Res.Slot); + g_framebuffersByAttachedTextureSlot[surface.Res.Slot].push_back(fbo); + }; + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) note(record.Color[i]); + note(record.Depth); + note(record.Stencil); + if (textureSlots.empty()) return; // the common Minecraft frame: renderbuffers only + g_attachedTextureSlotsByFramebufferSlot[fbo.Slot] = std::move(textureSlots); + } + + // ---- P5e (fb, §5.4): BackendFramebufferObject::SyncToBackend BY HANDLE --------------- + // + // The record's ELEVEN SURFACES ARE THE POINT SET. The object form walks the frontend's + // 41 attachment points to learn which exist; this one does not have to, because the + // emitter REFUSES a framebuffer holding a point at or above the wire width + // (FramebufferEmit.h's two refusals) and the FRONT/BACK tokens belong to the default + // framebuffer, which has no twin. So Color[0..7] + Depth + Stencil is the whole of it, + // and a framebuffer that could not be described that way never produced a record at all. + // + // THE PER-POINT VERSION MEMO IS GONE and is not replaced. On the object form it is a + // second, narrower gate under the record hash, hedging against "a frontend attachment + // moved without the record moving" - which is unrepresentable once the record is the + // only statement of what is attached (the hash covers every surface, and Fbo is inside + // it, so a recycled handle cannot be suppressed against its predecessor). Keeping it + // would have meant keeping the frontend array that carries it, which is the whole point + // of this arm. The two SERVER-owned gates stay, because no client-side value can answer + // what they answer: g_attachmentBackendIdGeneration ("did I re-mint a driver id") and + // the re-entry it drives. + void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo, + FramebufferTarget asTarget) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + const MG_Pipe::MGPFramebufferState* const recordPtr = PushedFramebufferRecord(fbo); + if (recordPtr == nullptr) { + MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so it is not " + "configured here; nothing is bound by this refusal, but the caller's own " + "bind still targets it", + fbo.Slot, fbo.Gen); + return; + } + const MG_Pipe::MGPFramebufferState& record = *recordPtr; + if (record.IsDefault != 0) { + // The default framebuffer has no twin (pDefaultFramebufferInfo->defaultFBO is the + // frontend object and MGPipeHandles.h reserves {0,1} for it), so reaching this + // body with one is a caller that skipped its own IsDefault branch. + MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} describes the DEFAULT framebuffer, " + "which has no twin to configure", + fbo.Slot, fbo.Gen); + return; + } + // Kept in step for the monolith-glue overloads that still resolve their record + // through it (SyncReadBufferToBackend's push arm). + m_pushedSyncHandle = fbo; + const GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget); + Bind(asTarget); + + FramebufferObject::FramebufferAttachmentArray stateDrawBuffers{}; + DecodePushedDrawBuffers(record, stateDrawBuffers.data()); + + // 1. Remap draw buffers. Same rule as the object form: glDrawBuffers writes the + // DRAW-bound framebuffer's state, so a READ-only sync must neither issue it nor + // stamp the memo (the Minecraft 26.x OIT bug the object form names). + Bool attachmentPointsMoved = false; + const Bool drawBufferClean = + memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(), + FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0; + if (!drawBufferClean && asTarget == FramebufferTarget::Draw) { + memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(), + FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)); + std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE); + int nEffectiveBuffers = 0; + for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + const auto frontendBuf = stateDrawBuffers[i]; + if (frontendBuf == FramebufferAttachmentType::None) { + m_backendDrawBuffers[i] = GL_NONE; + continue; + } + // DecodePushedDrawBuffers cannot produce a FRONT/BACK token - those are the + // default framebuffer's own and it has no twin - so the object form's + // "shouldn't remap" branch has no counterpart here. + m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i; + nEffectiveBuffers = i + 1; + } + g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers); + if (RecomputeBackendColorSlots(stateDrawBuffers)) { + m_frontendReadBuffer = FramebufferAttachmentType::Unknown; + attachmentPointsMoved = true; + } + } + + // The four cross-object masks, off the record's own surfaces (ID-12 DV-5). + if (asTarget == FramebufferTarget::Draw) { + Uint32 snormClampOutputMask = 0; + Uint32 unormClampOutputMask = 0; + Uint32 alphaWidenedMask = 0; + Uint32 integerColorMask = 0; + for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) { + const auto frontendBuf = stateDrawBuffers[i]; + if (frontendBuf < FramebufferAttachmentType::Color0 || + frontendBuf > FramebufferAttachmentType::Color31) { + continue; + } + const MG_Pipe::MGPSurface* surface = PushedSurfaceForAttachment(record, frontendBuf); + if (surface == nullptr) continue; // unrepresentable: DecodePushedDrawBuffers' range + if (IsSnormFallbackSurface(*surface)) { + snormClampOutputMask |= (1u << i); + } else if (IsUnormFallbackSurface(*surface)) { + unormClampOutputMask |= (1u << i); + } + if (IsAlphaWidenedColorSurface(*surface)) alphaWidenedMask |= (1u << i); + if (IsIntegerColorSurface(*surface)) integerColorMask |= (1u << i); + } + PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask; + PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask; + g_alphaWidenedDrawBufferMask = alphaWidenedMask; + g_integerColorDrawBufferMask = integerColorMask; + } + + // 2. Remap read buffer, READ-target only, for the reason the object form gives. + if (asTarget == FramebufferTarget::Read) { + ApplyReadBufferFromRecord(record, 0); + } + + // 3. The attachments. Three gates, and the key is the object's rather than the + // binding's: idGenerationMoved ("I re-minted a driver id, so a point may hold a + // dead one"), attachmentPointsMoved ("the colour permutation moved, so a point + // whose record did not change still has to be re-issued") and the record hash. + // NOT m_syncedRecordHashes: the per-target array is the object arm's re-arm + // trigger, and the points written below belong to the driver FBO rather than to + // the binding they were walked for - see m_syncedAttachmentRecordHash. + const Bool idGenerationMoved = m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration; + if (idGenerationMoved) { + m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration; + } + if (idGenerationMoved || attachmentPointsMoved || + m_syncedAttachmentRecordHash != record.ContentHash) { + const auto applyPoint = [&](FramebufferAttachmentType point, + const MG_Pipe::MGPSurface& surface) { + const Bool isColorPoint = point >= FramebufferAttachmentType::Color0 && + point <= FramebufferAttachmentType::Color31; + const GLenum glBackendAttachment = + isColorPoint ? GetBackendAttachmentType(point) + : MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(point); + if (glBackendAttachment == GL_NONE || glBackendAttachment == GL_UNKNOWN_MGL) return; + const Bool empty = surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(surface.Res); + // The detach that keeps m_backendColorSlots a permutation of the PHYSICAL + // layout: a colour point whose record says empty must not keep the previous + // owner's image, or glReadBuffer returns it. Bounded by the driver's cap, + // because GL_COLOR_ATTACHMENTn above it is INVALID_ENUM. + if (isColorPoint && empty) { + const Int index = + static_cast(point) - static_cast(FramebufferAttachmentType::Color0); + if (index < g_GLESCapabilities.MaxColorAttachments) { + g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, + GL_RENDERBUFFER, 0); + } + return; + } + // No frontend attachment on this arm, by construction: the record IS the + // point set here (§5.4). Null is what selects nothing - the transport test + // inside is what selects the by-handle storage syncs. + SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment, + /*monolithAttachment=*/nullptr); + }; + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + applyPoint(static_cast( + static_cast(FramebufferAttachmentType::Color0) + static_cast(i)), + record.Color[i]); + } + applyPoint(FramebufferAttachmentType::Depth, record.Depth); + applyPoint(FramebufferAttachmentType::Stencil, record.Stencil); + m_syncedAttachmentRecordHash = record.ContentHash; + NoteFramebufferTextureAttachments(fbo, record); + } + + // 4. ARB_framebuffer_no_attachments. With no attachment at all the record's geometry + // IS the framebuffer's default width/height/layers/samples (FillGeometry), and the + // driver FBO needs them too: without a default size an attachment-less ES + // framebuffer is incomplete and every draw into it is dropped. With attachments the + // defaults are ignored by GL, so they are only pushed for the empty shape. + Bool hasAnyAttachment = !(record.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(record.Depth.Res)) || + !(record.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(record.Stencil.Res)); + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && !hasAnyAttachment; ++i) { + hasAnyAttachment = !(record.Color[i].Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(record.Color[i].Res)); + } + if (!hasAnyAttachment) { + const Array defaults = { + static_cast(record.Width), static_cast(record.Height), + static_cast(record.Layers), + std::min(static_cast(record.Samples), g_GLESCapabilities.MaxFramebufferSamples), + static_cast(record.FixedSampleLocations != 0 ? GL_TRUE : GL_FALSE)}; + if (!m_syncedNoAttachmentDefaultsValid || m_syncedNoAttachmentDefaults != defaults) { + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_WIDTH, defaults[0]); + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_HEIGHT, defaults[1]); + if (g_GLESCapabilities.SupportsFramebufferDefaultLayers) { + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_LAYERS, defaults[2]); + } + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_SAMPLES, defaults[3]); + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS, + defaults[4]); + m_syncedNoAttachmentDefaults = defaults; + m_syncedNoAttachmentDefaultsValid = true; + } + } + + // The walk itself can re-mint a driver id (the storage sync behind a texture + // surface), invalidating points this walk already attached. Re-enter until the + // generation is quiescent, exactly as the object form does - every pass syncs each + // dirty texture clean, so each repeat finds strictly fewer re-mints. + if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) { + SyncToBackendByHandle(fbo, asTarget); + } + } + + GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const { + // Only colour attachments are ever relocated; depth/stencil, the default framebuffer's + // FRONT/BACK names and None map straight through. + if (frontendAtt < FramebufferAttachmentType::Color0 || frontendAtt > FramebufferAttachmentType::Color31) { + return MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt); + } + // The table is a permutation of the backend colour points, so this is the one point that + // owns this attachment. Searching the draw-buffer array instead returned the identity + // point for every attachment that was not a draw buffer - which is exactly the point a + // relocated draw buffer had just taken over, so COLOR_ATTACHMENT0 read back the image of + // whatever attachment was last made the draw buffer. + const Uint index = static_cast(frontendAtt) - static_cast(FramebufferAttachmentType::Color0); + return m_backendColorSlots[index]; + } + + TwinRegistry + g_backendFramebufferObjects; + Array g_fboSyncedSlotVersions = {0}; + // Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change) + // per target: re-attaching textures or changing draw buffers on an already-bound FBO + // must re-sync it even when the binding-slot version has not moved. + Array g_fboSyncedObjectVersions = {0}; + Array + g_fboSyncedObjects = {}; + Uint64 g_attachmentBackendIdGeneration = 0; + Array g_fboSyncedBackendIdGenerations = {0}; + } // namespace FramebufferImpl + + namespace ScratchFBOImpl { + namespace { + // ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it + // (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it. + struct ScratchSet { + ScratchFramebuffer temp; + ScratchFramebuffer blitRead; + ScratchFramebuffer blitDraw; + Uint completeTinyFBOId = 0; + Uint completeTinyRBOId = 0; + }; + PerNativeContext g_scratchSets; + + // Detach every point the shadow no longer vouches for. Used when the + // shadow is unknown (context reset, texture id deleted while attached). + void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + fb.colorTex = 0; + fb.colorTarget = 0; + fb.colorLevel = 0; + fb.colorLayer = -1; + fb.depthTex = 0; + fb.depthTarget = 0; + fb.depthLevel = 0; + fb.depthLayer = -1; + fb.depthHasStencil = false; + fb.depthIsStencilOnly = false; + fb.attachmentsKnown = true; + } + + void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) { + if (!fb.attachmentsKnown) { + ScrubAllAttachments(fb, fbTarget); + } + } + + // The post-attach glGetError probe below must not misread an error some + // earlier operation left queued; drain before attaching (rare path - + // only runs when the attachment actually changes). + void DrainPendingGLErrors() { + while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + } + } + + // Record the color point as detached when the shadow said something was + // there; the actual detach call is the caller's (it may be replaced by + // the new attach directly when the point is being overwritten). + void RecordNoColor(ScratchFramebuffer& fb) { + fb.colorTex = 0; + fb.colorTarget = 0; + fb.colorLevel = 0; + fb.colorLayer = -1; + } + + void RecordNoDepth(ScratchFramebuffer& fb) { + fb.depthTex = 0; + fb.depthTarget = 0; + fb.depthLevel = 0; + fb.depthLayer = -1; + fb.depthHasStencil = false; + fb.depthIsStencilOnly = false; + } + } // namespace + + ScratchFramebuffer& TempFramebuffer() { + return g_scratchSets.Current().temp; + } + ScratchFramebuffer& BlitReadFramebuffer() { + return g_scratchSets.Current().blitRead; + } + ScratchFramebuffer& BlitDrawFramebuffer() { + return g_scratchSets.Current().blitDraw; + } + + Uint EnsureId(ScratchFramebuffer& fb) { + if (fb.id == 0) { + g_GLESFuncs.glGenFramebuffers(1, &fb.id); + // A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw + // buffers (the ES defaults for a non-default framebuffer). + fb.attachmentsKnown = true; + RecordNoColor(fb); + RecordNoDepth(fb); + fb.readBuffer = GL_COLOR_ATTACHMENT0; + fb.drawBuffer = GL_COLOR_ATTACHMENT0; + } + return fb.id; + } + + void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, + GLint level) { + PrepareForUse(fb, fbTarget); + if (fb.depthTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + RecordNoDepth(fb); + } + if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) { + return; + } + if (fb.colorTex != 0) { + // Detach first: if the new attach fails, the point must read as + // missing (incomplete FBO), not silently keep the old texture. + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoColor(fb); + return; + } + fb.colorTex = tex; + fb.colorTarget = texTarget; + fb.colorLevel = level; + fb.colorLayer = -1; + } + + void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) { + PrepareForUse(fb, fbTarget); + if (fb.depthTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + RecordNoDepth(fb); + } + if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) { + return; + } + if (fb.colorTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoColor(fb); + return; + } + fb.colorTex = tex; + fb.colorTarget = 0; + fb.colorLevel = level; + fb.colorLayer = layer; + } + + void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level, + Bool withStencil, Bool stencilOnly) { + PrepareForUse(fb, fbTarget); + if (fb.colorTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + RecordNoColor(fb); + } + if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level && + fb.depthLayer < 0 && fb.depthHasStencil == withStencil && + fb.depthIsStencilOnly == stencilOnly) { + return; + } + if (fb.depthTex != 0) { + // One call clears both depth and stencil points regardless of how + // the previous attachment was made. + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + // A stencil-only texture has no depth point to attach at, and a depth one has no + // stencil point; GL_DEPTH_STENCIL_ATTACHMENT is only the combined texture's form. + g_GLESFuncs.glFramebufferTexture2D(fbTarget, + stencilOnly ? GL_STENCIL_ATTACHMENT + : (withStencil ? GL_DEPTH_STENCIL_ATTACHMENT + : GL_DEPTH_ATTACHMENT), + texTarget, tex, level); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoDepth(fb); + return; + } + fb.depthTex = tex; + fb.depthTarget = texTarget; + fb.depthLayer = -1; + fb.depthLevel = level; + fb.depthHasStencil = withStencil; + fb.depthIsStencilOnly = stencilOnly; + } + + // The layer-point sibling of EnsureColorAttachmentLayer above, for a view's mip chain in + // a depth-only storage owner. depthTarget == 0 is the marker that this point was made + // with glFramebufferTextureLayer, exactly as colorTarget == 0 is on the color side, so a + // later 2D attach to the same level is never deduped against it. + void EnsureDepthAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) { + PrepareForUse(fb, fbTarget); + if (fb.colorTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + RecordNoColor(fb); + } + if (fb.depthTex == tex && fb.depthTarget == 0 && fb.depthLevel == level && fb.depthLayer == layer && + !fb.depthHasStencil && !fb.depthIsStencilOnly) { + return; + } + if (fb.depthTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_DEPTH_ATTACHMENT, tex, level, layer); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoDepth(fb); + return; + } + fb.depthTex = tex; + fb.depthTarget = 0; + fb.depthLevel = level; + fb.depthLayer = layer; + fb.depthHasStencil = false; + fb.depthIsStencilOnly = false; + } + + void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) { + if (fb.readBuffer == readBuffer) { + return; + } + g_GLESFuncs.glReadBuffer(readBuffer); + fb.readBuffer = readBuffer; + } + + void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) { + if (fb.drawBuffer == drawBuffer) { + return; + } + g_GLESFuncs.glDrawBuffers(1, &drawBuffer); + fb.drawBuffer = drawBuffer; + } + + Uint EnsureCompleteTinyFramebufferId() { + ScratchSet& set = g_scratchSets.Current(); + Uint& g_completeTinyFBOId = set.completeTinyFBOId; + Uint& g_completeTinyRBOId = set.completeTinyRBOId; + if (g_completeTinyFBOId != 0) { + return g_completeTinyFBOId; + } + // One-time creation: the renderbuffer binding is context state with no + // shadow, so save/restore it by query here (cold path only). + GLint prevRenderbuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); + g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId); + g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId); + FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId); + g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1); + g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + g_completeTinyRBOId); + const GLenum drawBuffer = GL_COLOR_ATTACHMENT0; + g_GLESFuncs.glDrawBuffers(1, &drawBuffer); + g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Scratch 1x1 framebuffer is incomplete."); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); + return g_completeTinyFBOId; + } + + void NoteTextureIdDeleted(Uint textureId) { + if (textureId == 0) { + return; + } + // Textures ARE shared, so the deleted id may sit in any context's scratch FBO. + g_scratchSets.ForEach([textureId](ScratchSet& set) { + for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) { + if (fb->colorTex == textureId || fb->depthTex == textureId) { + fb->attachmentsKnown = false; + } + } + }); + } + + void OnBackendContextDestroyed() { + g_scratchSets.Clear(); + } + } // namespace ScratchFBOImpl + + namespace PixelStoreImpl { + namespace { + PackState g_packState; + Bool g_packStateKnown = false; + + void PinPackState(const PackState& value) { + g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment); + g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels); + g_packState = value; + g_packStateKnown = true; + } + } // namespace + + void ApplyPackState(const PackState& desired) { + if (!g_packStateKnown) { + PinPackState(desired); + return; + } + if (desired.Alignment != g_packState.Alignment) { + g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment); + g_packState.Alignment = desired.Alignment; + } + if (desired.RowLength != g_packState.RowLength) { + g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength); + g_packState.RowLength = desired.RowLength; + } + if (desired.SkipRows != g_packState.SkipRows) { + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows); + g_packState.SkipRows = desired.SkipRows; + } + if (desired.SkipPixels != g_packState.SkipPixels) { + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels); + g_packState.SkipPixels = desired.SkipPixels; + } + } + + PackState CurrentPackState() { + if (!g_packStateKnown) { + // Fresh/unknown context: pin to the GL defaults (what a new context + // starts with; writing them makes the shadow authoritative either way). + PinPackState(PackState{}); + } + return g_packState; + } + + void InvalidatePackStateCache() { + g_packStateKnown = false; + } + } // namespace PixelStoreImpl + + namespace RenderbufferImpl { + BackendRenderbufferObject::BackendRenderbufferObject() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId); + m_contextGeneration = g_backendContextGeneration; + if (m_backendRBOId == 0) { + MGLOG_E_ONCE("Failed to generate renderbuffer object."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + } + } + + BackendRenderbufferObject::~BackendRenderbufferObject() { + if (InProcessTeardown()) { + return; // see InProcessTeardown(): the driver may be unloaded already + } + if (m_backendRBOId == 0) { + return; + } + // No driver-level renderbuffer-binding shadow exists (Bind() always issues the + // call), so there is nothing to scrub here - only the id to release. + if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) { + g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId); + } + m_backendRBOId = 0; + } + + void BackendRenderbufferObject::Bind() const { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, m_backendRBOId); + } + + // P4a (D-D2): the four values BackendRenderbufferObject::SyncToBackend allocates from, + // spelled once per arm. Macros for the reason the texture-parameter block gives: in the + // PULL build each expands to the pre-P4a expression at its original site, so that + // build's tokens and its symbol sizes are unchanged (D-P). + // + // ARCHITECTURE.md §4.1 keeps the renderbuffer an INDEPENDENT frontend class while giving + // it the same discriminated MGPResourceDesc a texture gets, and this is where that pays: + // the same four fields, read out of the same record type, with Desc.Target telling the + // applier which of the three per-kind tables the record lives in. +#define MGB_RBO_FORMAT \ + (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.InternalFormat) \ + : stateRBOObject->GetInternalFormat()) +#define MGB_RBO_WIDTH (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Width) : static_cast(stateRBOObject->GetWidth())) +#define MGB_RBO_HEIGHT \ + (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Height) : static_cast(stateRBOObject->GetHeight())) +#define MGB_RBO_SAMPLES \ + (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Samples) : static_cast(stateRBOObject->GetSamples())) + +#undef MGB_RBO_FORMAT +#undef MGB_RBO_WIDTH +#undef MGB_RBO_HEIGHT +#undef MGB_RBO_SAMPLES + + // P5e (fb, CONTRACT-P5E.md §5.4): the same allocation, keyed on the renderbuffer HANDLE. + // + // THE FOUR VALUES WERE ALREADY THE RECORD'S at P4a (D-D2) - what the object form still + // needed the frontend for was the LOOKUP, `HandleOf(stateRBOObject.get())`, a + // client-allocator probe on the apply thread. On this arm the handle arrives from the + // attachment surface that named it, so nothing is probed and nothing is dereferenced. + // An OVERLOAD rather than a changed signature (the monolith glue keeps its tokens). + void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + const MG_Pipe::MGPipeResourceRecord* const pushedRecord = + PipeRenderbufferRecordForHandle(renderbuffer); + if (pushedRecord == nullptr) { + MGLOG_E_ONCE("MGPipe: renderbuffer handle {%u, %u} has no applier record, so its " + "storage cannot be allocated from the pushed descriptor", + renderbuffer.Slot, renderbuffer.Gen); + return; + } + if (m_isInitialized && m_syncedResourceSerial != 0 && + m_syncedResourceSerial == pushedRecord->Serial) { + return; + } + + // A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3): the + // allocation below re-defines the storage behind the name the framebuffer twins + // already attached, and no record of THEIRS moves for it, so the attachment memo + // has to be re-armed the same way a texture re-mint arms it. The first allocation + // is not a redefinition. + if (m_isInitialized) { + ++FramebufferImpl::g_attachmentBackendIdGeneration; + } + + Bind(); + + const auto internalFormat = static_cast(pushedRecord->Desc.InternalFormat); + const Int width = static_cast(pushedRecord->Desc.Width); + const Int height = static_cast(pushedRecord->Desc.Height); + const Int samples = static_cast(pushedRecord->Desc.Samples); + GLenum glInternalFormat, glType, glFormat; + TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType); + + // Drain first, for the object form's reason: a driver that refuses the allocation + // (a multi-gigabyte renderbuffer is refused routinely) used to leave the twin + // claiming storage it does not have, and the attachment then rendered nowhere. + DebugImpl::ErrorLopper::Clear(); + if (samples > 0) { + const auto backendSamples = static_cast(ClampSamplesToBackendSupport( + GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples)); + g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat, + static_cast(width), + static_cast(height)); + } else { + g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast(width), + static_cast(height)); + } + if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) { + MGLOG_E_ONCE("Renderbuffer {%u, %u} storage allocation ran out of memory: %dx%d, " + "samples=%d, format=%s", + renderbuffer.Slot, renderbuffer.Gen, width, height, samples, + MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); + // NO RecordError HERE. The object form reports the OOM to the application + // through the live GLContext; on this arm there is no application on this side + // of the wire to report it to, and gPipeInputs->RecordError is itself one of the + // BARRIER-PULLED rows (FieldOwnership.def) that rule F forbids an unbarriered + // apply to touch. The client raises its own errors from its own validator. + } + DebugImpl::ErrorLopper::Clear(); + + m_cacheInternalFormat = internalFormat; + m_cacheWidth = width; + m_cacheHeight = height; + m_cacheSamples = samples; + m_syncedResourceSerial = pushedRecord->Serial; + m_isInitialized = true; + } + + TwinRegistry + g_backendRenderbufferObjects; + } // namespace RenderbufferImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersInternal.h b/MobileGL/MG_Backend/DirectGLES/ManagersInternal.h new file mode 100644 index 00000000..2a6612a2 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersInternal.h @@ -0,0 +1,658 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersInternal.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include "Managers.h" +#include "SubgroupPolicy.h" +#include +// P3a: the handle-shaped resource op table, the applier's records and the reverse channel. +// Both headers are compiled into the library only under push, so they are included here +// under the same condition - a pull build must gain no declaration it cannot link. +#include +#include +#include +// P5e (pg): the DEFINITION of the archive MGPipeShaderCsoRecord holds by SharedPtr. PipeApply.h +// needs only the name - the deleter is captured where the archive is constructed - but the +// program build reads through it, so the one translation unit that does needs the whole type. +#include +// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for +// why GLESBufferResource does not simply gain a member. +#include +#include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +// P5c (ct): object_death's producer (CONTRACT-P5C.md §5.2) - the death notice's split arm +// emits the record through the client's emit helper instead of hopping a stack struct to the +// apply thread. +#include +// P11 B2 (T0): the client's AHardwareBuffer store - its reference counting and, on Android, the +// allocation the POST self-test makes. +#include +#if defined(__ANDROID__) +#include +#endif +#endif + +#include "Utils.h" +#include "DirectGLES.h" +#include "BackendObject_DirectGLES.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ContextEpoch.h" + +namespace MobileGL::MG_Backend::DirectGLES { + constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance"; + constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID"; + constexpr const char* BASE_VERTEX_UNIFORM_NAME = "mg_BaseVertex"; + constexpr const char* BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME = "mg_BaseInstanceWordIndex"; + constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams"; + + Bool ViewportArrayEmulationEnabled(); + + GLenum ResolveBackendMinFilter(const SamplerParameters& samplerParams, Bool avoidMipmapMinFilter); + + String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); + + String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType); + constexpr const char* VIEWPORT_PASS_MASK_UNIFORM_NAME = "mg_ViewportPassMask"; + + Bool PromoteViewportIndexGlobalToVarying(String& source); + + Bool InjectViewportIndexPassGate(String& source); + + namespace BufferImpl { + using MG_State::GLState::BackendBufferResource; + using MG_State::GLState::BufferObject; + + extern Uint g_bufferContextGeneration; + + // --- Persistent-mapped bump rings (see Managers.h) ----------------------- + // Shared machinery behind BOTH the global-UBO ring and the texture + // unpack-PBO ring: one EXT_buffer_storage persistent|coherent map per ring, + // monotonic head/tail cursors, and reclamation riding the Present() + // frame-fence watermark. The two rings differ only in size cap, offset + // alignment and log label - the reclamation, context-loss and + // emergency-drain rules are the part that was hard to get right, so they + // are shared rather than copied. + constexpr SizeT kUboRingInitialBytes = 4u * 1024u * 1024u; + constexpr SizeT kUboRingMaxBytes = 64u * 1024u * 1024u; + constexpr SizeT kUnpackRingInitialBytes = 4u * 1024u * 1024u; + constexpr SizeT kUnpackRingMaxBytes = 64u * 1024u * 1024u; + // A PBO-sourced glTexSubImage constrains the offset only to the pixel + // TYPE's size (GL_INVALID_OPERATION otherwise), and no ES client type is + // wider than 4 bytes - unlike a UBO bind, which owes the driver + // GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. 64 covers every type with room to + // spare and keeps consecutive staged blocks off each other's cache lines. + constexpr SizeT kUnpackRingAlignment = 64; + // glCopyBufferSubData carries no offset-alignment requirement at all; 64 + // keeps staged blocks cache-line separated, same as the unpack ring. + constexpr SizeT kUploadRingInitialBytes = 4u * 1024u * 1024u; + constexpr SizeT kUploadRingMaxBytes = 64u * 1024u * 1024u; + constexpr SizeT kUploadRingAlignment = 64; + + struct PersistentRingStore { + Uint id = 0; + Uint8* mappedPtr = nullptr; + SizeT size = 0; + // Monotonic linear cursors: `head` counts every byte ever allocated + // (incl. wrap padding); everything below `tail` is GPU-complete. Ring + // offset of a linear position is pos % size, so in-flight bytes are + // head - tail and must stay <= size. + Uint64 head = 0; + Uint64 tail = 0; + Uint32 generation = 0; // bumped on every (re)create/grow; 0 = never valid + Uint contextGeneration = 0; + SizeT alignment = 256; + // A hard storage-creation failure under this context; stop retrying + // per use (cleared when the context generation moves on). + Bool creationFailed = false; + }; + + // Grown-away ring stores: deletable only once the GPU finished the last + // frame that could reference them (same watermark as the buffer pool). + struct RetiredRingStore { + Uint id = 0; + Uint contextGeneration = 0; + Uint64 retireSerial = 0; + }; + + // Present()-time high-water marks: every byte below headAtPresent was + // written during frames <= frameSerial, so once frameSerial completes, + // tail may advance to headAtPresent. FIFO by construction. + struct RingFrameMark { + Uint64 frameSerial = 0; + Uint64 headAtPresent = 0; + }; + + // One ring: its live store, its two reclamation lists, and the immutable + // knobs that tell it apart from the other one. + struct PersistentRing { + PersistentRingStore store; + Vector retired; + Vector frameMarks; + SizeT initialBytes = 0; + SizeT maxBytes = 0; + // 0: take the offset alignment from GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT + // at store-creation time (the UBO ring's binds require it). + SizeT fixedAlignment = 0; + const char* label = ""; + }; + + // THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at + // Present, which say "everything this context wrote before its fence N is done once N is". + // One process-wide set used to be shared by every session's contexts: a fence from one + // context retired bytes another context's not-yet-executed copies still had to read + // (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's + // staging overwrote them - its pixels landed, at the wrong offsets, in the first one's + // textures. With one set per context, a context's bytes wait for that context's own + // fence. (The buffers themselves are shared objects; only who stages into them is split.) + // Staging ring for app buffer updates whose destination store may still be + // referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR + // hazard by BLOCKING in the call (osup_sync_object_wait) until every + // referencing job retires - under Minecraft 26.3's per-frame UBO and + // chunk-mesh SubData streams that serialized whole frames (~1 fps while + // chunks stream in). Staging the bytes here and issuing a + // glCopyBufferSubData instead keeps the hazard on the GPU timeline where it + // is just job ordering, and the CPU never waits. + struct RingSet { + PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"}; + PersistentRing unpack{ + {}, {}, {}, kUnpackRingInitialBytes, kUnpackRingMaxBytes, kUnpackRingAlignment, "Texture unpack ring"}; + PersistentRing upload{ + {}, {}, {}, kUploadRingInitialBytes, kUploadRingMaxBytes, kUploadRingAlignment, "Buffer upload ring"}; + RingSet(); + ~RingSet(); + RingSet(const RingSet&) = delete; + RingSet& operator=(const RingSet&) = delete; + }; + + extern DirectGLES::PerNativeContext g_ringSets; + + namespace { + Bool CanTouchGLNow() { + return DirectGLES::IsBackendContextCurrentOnThisThread(); + } + } // namespace + + MG_Record::StagedShadowStore& ServerStaged(); + + namespace { + // THE COVERAGE RULE FOR THE THREE-TIER FLUSH DRAIN, ASSERTED BEFORE THE CALL AND NOT + // INSIDE IT. FlushPendingRangesFrom is a G5-pinned body (p3a_untouched_regions.sh's + // PINNED_FUNCTIONS, ID-41): the split arm CALLS it, it does not re-spell it, and it + // does not add a line to it either. Its tier-1 arm - the range-invalidating map - + // copies with its own Memcpy and never reaches UploadRangeFrom, so the one tier that + // DECLARES THE OLD BYTES DEAD is the one tier no later check can see; the only place + // left to say "every queued range is inside the staged coverage" is the instant before + // the drain takes the queue. The clamp is the drain's own (limit = the smaller of the + // frontend size and the backend store; bytes past either end have nowhere to land and + // are not this rule's subject), so the two agree about which bytes are meant. + // + // Fires only for a base that IS this resource's server shadow: RequireCoverage + // answers nothing for the legacy arm's MappedData(), which is valid for the whole + // store. `pendingMutex` is taken here and released before the drain takes it again. + void RequireStagedCoverageForPendingRanges(GLESBufferResource& resource, const Uint8* hostBase, + SizeT frontendSize, const char* site) { + if (hostBase == nullptr) return; + const SizeT limit = std::min(frontendSize, resource.storageSize); + const std::lock_guard lock(resource.pendingMutex); + for (const auto& range : resource.pendingRanges) { + const SizeT end = std::min(range.end, limit); + const SizeT start = std::min(range.start, end); + if (start == end) continue; + ServerStaged().RequireCoverage(&resource, hostBase, start, end, site); + } + } + +// The four hostBytes sites read the same in both builds. The non-split expansion is the +// ORIGINAL EXPRESSION, character for character - `raw - offset` - so a push or verify build +// compiles exactly what it compiled before R-11 and the split arm is the only new behaviour. +#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \ + ServerStaged().Adopt(&(res), (width), (bytes), (offset), (size)) +#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) \ + RequireStagedCoverageForPendingRanges((res), (base), (frontendSize), (site)) + } // namespace + + // Ring machinery shared with the UBO/unpack rings; defined further down in + // this same unnamed namespace. + Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset); + + // Push every queued range of `resource` from the shadow into the backend + // store, without ever letting a driver resolve the WAR hazard against + // in-flight frames at the WHOLE BUFFER's expense. Three tiers: + // + // 1. glMapBufferRange(WRITE | INVALIDATE_RANGE) + memcpy. The entire + // mapped range is rewritten from the authoritative shadow, so + // declaring its old bytes dead is exact - and it lets the driver + // swap fresh pages in for JUST that range. This is the only tier + // whose cost scales with the RANGE on this Mali driver: both the + // immediate glBufferSubData (pre-queueing) and a staged + // glCopyBufferSubData into a busy MUTABLE store ghost the whole + // destination with a worker-thread memcpy - Minecraft 26.3 streams + // ~1MB section meshes into 128MB arenas about nine times a frame + // during a camera pan, and 9 x 128MB of ghosting per frame is + // ~380ms, the measured 2-4 fps. (Backing the arenas with immutable + // stores also kills the ghost, but eagerly commits every arena's + // full extent - +hundreds of MB - which LMK'd the whole device.) + // 2. The staging ring + glCopyBufferSubData: the copy is ordered on + // the GPU timeline, no CPU wait (MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH + // forces this tier as the map path's negative control). + // 3. Direct glBufferSubData (potentially stalling) when neither the + // map entry points nor the ring exist. + // + // The ranges are flushed AS QUEUED (VecRange1D::Add already merges + // near-adjacent ones): bytes, not flush calls, are the cost axis here, + // and collapsing a scattered flush into its union re-copied nearly whole + // chunk-mesh arenas every frame. + // The caller owns syncedChangeSerial; this only drains the queue. + // + // ONE body for both arms, and deliberately so: the three-tier decision (whole-buffer + // orphan map / >= 128 KiB range-invalidating map / staged ring copy) is what the MC + // 26.3 p99 depends on, and a second copy of it for the handle arm is exactly how a + // tier silently changes. The arms differ only in where `hostBase` and `frontendSize` + // come from. + // + // R-11'S PARAMETER, AND IT IS A PARAMETER RATHER THAN AN ASSUMPTION (P5 b1). + // `hostBase` is re-read at every use today precisely because a shadow resize or an + // adoption moves what an earlier base pointed at. Under split the server may hold + // no pointer into the client's shadow at all, so the base becomes a SNAPSHOT taken + // into SEG_STAGE at emission - and a snapshot covers a RANGE, not the store. The + // two extra arguments say which range `hostBase` is good for; the default is the + // whole store, which is exactly what a live shadow is, so every caller today is + // byte-identical. The moment w1 passes a real snapshot extent, tier 1's widening + // refusal below stops being unreachable and a too-narrow snapshot is named instead + // of silently clobbering GPU-written bytes with stale ones. + constexpr SizeT kHostBaseCoversWholeStore = ~static_cast(0); + + void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize, + SizeT hostBaseFrom = 0, SizeT hostBaseTo = kHostBaseCoversWholeStore); + + void DrainResidentWritesNow(GLESBufferResource& resource); + + namespace { + // The two-argument spelling the legacy arm's call sites use, kept so those sites + // are the SAME text in both builds (G1). The object was never read. + void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) { + (void)bufferObject; + DrainResidentWritesNow(resource); + } + + // ---- P3a: the same seven ops plus the create/unmap pair, BY HANDLE ------------ + // + // Every body below is its Ops_* counterpart above with exactly the substitutions + // D-A2's table names and nothing else: the frontend object's GetSize() / GetUsage() + // / HasDefinedContent() / MappedData() / GetChangeSerial() become the applier + // record's Desc.Width / Desc.Usage / Desc.HasDefinedContent, the shadow base the + // call carried, and the record's server-owned Serial. Every branch survives - + // pendingRespecify early-out, the off-thread queue, the adopted zero-copy stamp, + // the upload-ring kill switch, the Mali WAR-stall queue-only default. + // + // NOTHING here reads a frontend type. The one thing a handle op cannot do is ask + // an object for its shadow, which is why GLESBufferResource::hostBytes exists: the + // three content-carrying calls hand the base over and the later drains read it. + + // The applier's record for this resource, or null when the client never created it + // (or created it into a slot that has since been recycled). + const MG_Pipe::MGPipeResourceRecord* ResourceRecordOf(MG_Pipe::MGPipeHandle res) { + if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; + const auto& records = MG_Pipe::MGPipeApplier().Resources; + if (res.Slot >= records.size()) return nullptr; + const auto& record = records[res.Slot]; + if (!record.Live || record.Gen != res.Gen) return nullptr; + return &record; + } + + // The server-owned MGGen this backend mirrors in syncedChangeSerial. Zero for a + // resource with no record, which is the same "never synced" answer a frontend + // change serial of zero gave. + Uint64 ResourceSerialOf(MG_Pipe::MGPipeHandle res) { + const auto* record = ResourceRecordOf(res); + return record != nullptr ? record->Serial : 0; + } + SizeT ResourceWidthOf(MG_Pipe::MGPipeHandle res) { + const auto* record = ResourceRecordOf(res); + return record != nullptr ? static_cast(record->Desc.Width) : 0; + } + } // namespace + +#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) + using MG_Util::BackendLoader::HasExtensionWord; +#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__ + + const Vector& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget); + + TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget); + + void BumpBufferMutationEpoch(); + + GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res); + + SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res); + + Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res); + + GLESBufferResource* EnsureBufferResourceForHandle( + const SharedPtr& bufferObject, MG_Pipe::MGPipeHandle res); + + void BindBufferId(GLenum target, Uint id); + + void BindPixelUnpackBufferId(Uint id); + + void NoteBufferIdDeleted(Uint id); + + Bool CreateRingStorage(PersistentRing& ring, SizeT minBytes); + Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset); + + Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset); + + Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset); + + Bool UnpackRingAvailable(); + + Bool UnpackRingAllocate(SizeT size, SizeT& outOffset); + + void* UnpackRingMappedPtr(); + + Uint UnpackRingBufferId(); + + SizeT UnpackRingMaxBytes(); + } // namespace BufferImpl + + // The release-build VOICE for StateBackendObjectRegistry::GetOrCreateByHandle's two + // silent refusals, shared by the kinds P4a re-keys so the wording cannot drift between + // them. It is the shape GetOrCreateBufferResourceForHandle gives P3a's buffer family, + // lifted into one template because a copy per kind is a chance per kind to write one of + // them differently. + // + // ITS CALLERS IN THIS PACKAGE ARE THE FRAMEBUFFER ATTACHMENT WALK'S TWO + // (FramebufferImpl::SyncAttachmentSurface: the texture registry and the renderbuffer + // registry), because that is where a HANDLE arrives in a payload - MGPSurface::Res - + // and an ADOPTION is what the arm owes rather than a lookup. The other three re-keyed + // kinds resolve at sites in DirectGLES.cpp (package E's SyncCurrentFBO, the lazy + // sampler mint and SyncCurrentProgram), so their first adoption is E's to write; this + // template is what they call so all five say the same thing. + // + // WHY A VOICE AT ALL: the table asserts, and MOBILEGL_ASSERT compiles out at INFO - + // which all three gate builds and every shipped build are - so an object that silently + // stops being twinned is exactly the failure mode the refusal exists to replace. + // + // Returns null on the legacy arm too, so a caller that reaches this without checking + // its family's arm gets nothing rather than a twin the legacy arm would never see. + // + // SPLIT IN TWO (review N-3), and the split is what makes the two refusals REACHABLE. + // Both of this package's call sites stand behind a cross-check that the record's Res + // equals registry.HandleOf(frontendObject) - the LIVE handle - so by the time the + // adoption ran, the slot was in bounds and the generation was current BY CONSTRUCTION + // and neither refusal below could ever fire: the letter of M-4 was closed and its + // substance was not. The record's handle is the UNTRUSTED input and the frontend is the + // corroboration, so the validity question is asked FIRST, on its own, at both sites; + // the adoption then performs the resolve. Asking it first is also side-effect free, + // which the ordering rule N-1 states for the framebuffer refusal wants here too: + // GetOrCreateByHandle can reset an incumbent twin, and a handle this predicate rejects + // must not reach it. + template + Bool PipeTwinHandleIsAdoptable(Registry& registry, MG_Pipe::MGPipeHandle handle, const char* kindName) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return false; + if (handle.Slot >= Registry::SlotTable::kMaxHandleSlot) { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 + MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - " + "%s handle slot %u is past this table's %u bound", + kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot); +#else + MGLOG_E_ONCE("MGPipe: %s handle slot %u is past the backend table's %u bound - " + "refusing to twin it", + kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot); + return false; +#endif + } + const Uint32 liveGen = registry.LiveGenAt(handle.Slot); + if (liveGen != 0 && liveGen > handle.Gen) { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12 + MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - " + "%s handle {%u, %u} names generation behind live generation %u", + kindName, handle.Slot, handle.Gen, liveGen); +#else + // SlotTables.h:301-321: forward is a recycle and resets the twin, BACKWARD is + // refused, because adopting it would release the incumbent LIVE twin's driver + // id and then stamp the slot back to the dead object's generation, after which + // the incumbent's own FindByHandle refuses it and it is silently handed a + // fresh, empty twin - a leak AND an object that loses its storage with no + // diagnostic. That is the shape commit d7655247 fixed on the buffer side. + MGLOG_E_ONCE("MGPipe: %s handle {%u, %u} names a generation BEHIND the live " + "twin's %u - refusing rather than dropping the incumbent's driver " + "object", + kindName, handle.Slot, handle.Gen, liveGen); + return false; +#endif + } + return true; + } + + namespace { + template + typename Registry::BackendPtr* AdoptTwinByHandle(Registry& registry, MG_Pipe::MGPipeHandle handle, + const char* kindName) { + if (!PipeTwinHandleIsAdoptable(registry, handle, kindName)) return nullptr; + return registry.GetOrCreateByHandle(handle); + } + } // namespace + + // ---- P4a: the record readers (Managers.h documents the contract) ---- + + namespace { + // One shape for all five, so a kind cannot grow a different liveness rule than its + // neighbours: index the applier's own dense table, refuse a slot it has not grown to, + // refuse a record that is not Live, and refuse a generation that has moved on under + // the caller. The last of the three is the one that matters: a stale handle resolving + // to its successor's record is how a twin ends up describing another object's storage. + template + const Record* PipeRecordAt(const Vector& table, MG_Pipe::MGPipeHandle handle) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; + if (handle.Slot >= table.size()) return nullptr; + const Record& record = table[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) return nullptr; + return &record; + } + } // namespace + + const MG_Pipe::MGPipeResourceRecord* PipeTextureRecordForHandle(MG_Pipe::MGPipeHandle res); + + const MG_Pipe::MGPipeSamplerCsoRecord* PipeSamplerCsoRecordForHandle(MG_Pipe::MGPipeHandle cso); + + void NotePipeSubDataTargetLowByte(const MG_Pipe::MGPipeResourceRecord& record, Uint16 packedTarget, Uint16 level); + + const MG_Pipe::MGPipeResourceRecord::PendingUpload* FindPipeTextureUpload( + const MG_Pipe::MGPipeResourceRecord& record, Uint16 uploadTarget, Uint16 level); + + void ConsumePipeTextureUpload(MG_Pipe::MGPipeHandle res, Uint16 uploadTarget, Uint16 level); + + Bool RearmPipeTextureLevelUpload(MG_Pipe::MGPipeHandle res, Uint16 packedTarget, Uint16 level, + const MG_Pipe::MGPBox& wholeLevel); + + namespace TextureImpl { + // Sets the backend GL unpack state to MobileGL's upload default for the scope, + // then restores it. The previous state is read from a shadow instead of via + // glGetIntegerv - that query forces a driver pipeline sync and, because texture + // uploads run it per dirty texture per frame, it dominated the DirectGLES draw + // path. The backend unpack state is set ONLY by MobileGL's own save/restore + // helpers (this class and, historically, the R32F copy path), all of + // which restore to the resting default, so the shadow stays accurate; a one-time + // forced sync pins the backend to that known default up front. Apply() is + // compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op. + class ScopedDefaultUnpackState { + public: + ScopedDefaultUnpackState() { + EnsureShadowSynced(); + m_prevAlignment = s_alignment; + m_prevRowLength = s_rowLength; + m_prevSkipRows = s_skipRows; + m_prevSkipPixels = s_skipPixels; + m_prevImageHeight = s_imageHeight; + m_prevSkipImages = s_skipImages; + // Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits + // width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1. + // Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride, + // shifting every row of a non-multiple-of-4 upload by one pixel. + Apply(1, 0, 0, 0, 0, 0); + } + + ~ScopedDefaultUnpackState() { + Apply(m_prevAlignment, m_prevRowLength, m_prevSkipRows, m_prevSkipPixels, m_prevImageHeight, + m_prevSkipImages); + } + + private: + static void EnsureShadowSynced() { +#if MOBILEGL_BUILD_DISAGGREGATED + const auto epoch = CurrentContextEpoch(); + if (s_epoch != epoch) { + s_synced = false; + s_epoch = epoch; + } +#endif + if (s_synced) { + return; + } + s_synced = true; + g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + s_alignment = 4; + s_rowLength = 0; + s_skipRows = 0; + s_skipPixels = 0; + s_imageHeight = 0; + s_skipImages = 0; + } + + static void Apply(GLint alignment, GLint rowLength, GLint skipRows, GLint skipPixels, GLint imageHeight, + GLint skipImages) { + if (alignment != s_alignment) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); s_alignment = alignment; } + if (rowLength != s_rowLength) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowLength); s_rowLength = rowLength; } + if (skipRows != s_skipRows) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, skipRows); s_skipRows = skipRows; } + if (skipPixels != s_skipPixels) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, skipPixels); s_skipPixels = skipPixels; } + if (imageHeight != s_imageHeight) { g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); s_imageHeight = imageHeight; } + if (skipImages != s_skipImages) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, skipImages); s_skipImages = skipImages; } + } + + GLint m_prevAlignment = 4; + GLint m_prevRowLength = 0; + GLint m_prevSkipRows = 0; + GLint m_prevSkipPixels = 0; + GLint m_prevImageHeight = 0; + GLint m_prevSkipImages = 0; + + // Shadow of the backend GL unpack state (GL defaults). See class comment. + static inline Bool s_synced = false; +#if MOBILEGL_BUILD_DISAGGREGATED + static inline ContextEpoch s_epoch{}; +#endif + static inline GLint s_alignment = 4; + static inline GLint s_rowLength = 0; + static inline GLint s_skipRows = 0; + static inline GLint s_skipPixels = 0; + static inline GLint s_imageHeight = 0; + static inline GLint s_skipImages = 0; + }; + + void NoteInPlaceTextureUpload(); + + Uint GetWidenableClientComponentCount(TextureInternalFormat format); + + Bool IsIntegerWidenableFormat(TextureInternalFormat format); + + // P5e (tx2): the GL name a log line wants, from whichever side of the arm can answer it. + // Desc.GlNameForDiag is the declared diagnostics carrier (MGPipeTypes.h) and it is the + // ONLY thing the by-handle arm may say about a texture's frontend name - a GL name is + // never an identity, never a memo key and never part of a hash (section 4.2.1). The + // twenty-odd GetExternalIndex() log reads this family carried (scout G-S2-4) all become + // this call. + static Uint32 TextureDiagName(const SharedPtr& stateTextureObject, + const MG_Pipe::MGPipeResourceRecord* record) { + if (stateTextureObject) return static_cast(stateTextureObject->GetExternalIndex()); + return record != nullptr ? record->Desc.GlNameForDiag : 0u; + } + + // P5e (fix1, ID-81 / CONTRACT-P5E §5.8): THE NULL FRONTEND OBJECT IS A CONTRACT AND THIS + // IS WHERE IT IS CHECKED, ONCE, FOR ALL THREE SYNC BODIES. + // + // tx2's three sync bodies accept a null `stateTextureObject` because the RECORD answers + // every read they make - but only where the record arm is actually SELECTED. Two reads + // inside SyncMipmapsToBackend are gated on `Transport != Monolith` (the texture-VIEW test + // and RequireImageBindableStorage's re-dirty transition, §5.2), and every read in all + // three is gated on TextureResourceSubsystemEnabled() through `pushedStorage`. So a null + // object is servable exactly when BOTH hold, and the by-handle entries beside them + // (SyncTextureToBackendByHandle / SyncMipmapsToBackendByHandle) pass null on the strength + // of a NOTED HANDLE, which says nothing about either. + // + // The device found the gap: with MOBILEGL_TRANSPORT=monolith the push build still drives + // its framebuffer attachments from the record (P4a, D-C2), fb's SyncAttachmentSurface + // called the by-handle storage sync from that arm, and `stateTextureObject->IsTextureView()` + // dereferenced the null SharedPtr in Lightmap. -> clearColorTexture -> glClear. + // The arm selection is repaired at that call site; this refusal is what makes the shape + // UNREPEATABLE rather than merely repaired - a null frontend object reaching a body that + // can still read one aborts BY NAME instead of taking a SIGSEGV three frames deep. + // + + // Returns true when the caller must decline (no handle noted: the caller's bug it always + // was, named by the caller's own log line), false when the record arm can serve the call, + // and never returns at all when a handle was noted on an arm that cannot serve it. + static Bool RefuseNullFrontendTextureOffTheHandleArm(const char* entry, + MG_Pipe::MGPipeHandle notedHandle) { + if (MG_Pipe::MGPipeHandleIsNull(notedHandle)) return true; + return false; + } +// The guard the three sync bodies spell. In the PULL build it folds to the constant the +// pre-fix1 text folded to, so that build's preprocessed text gains no call (D-P). +#define MGB_TEXTURE_NULL_FRONTEND_REFUSED(twin, entry) \ + (RefuseNullFrontendTextureOffTheHandleArm((entry), (twin).PushedSyncHandle())) + + extern Array, + MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> + g_boundTexturesCache; + + void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite); + } // namespace TextureImpl + + namespace ScratchFBOImpl { + void NoteTextureIdDeleted(Uint textureId); + } // namespace ScratchFBOImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp new file mode 100644 index 00000000..8ec01af5 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp @@ -0,0 +1,2865 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + // 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 ShouldAvoidExplicitLodBiasOnAngleLlvmpipe() { + // IsAngleLlvmpipeRenderer combined with the MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS + // feature toggle, both resolved in FillInGLESCapabilities. + return g_GLESCapabilities.AvoidExplicitLodBias; + } + + // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so this is the floor MobileGL + // advertises whatever the ES driver reports. It steers ClampMultisampleFetchesForEssl + // AND is part of the L2 translation-memo key, so the transpile and the key must read + // the same constant - hence one definition rather than two locals. + // Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): this is + // backend code and must not reach into the GL frontend. 4 is that translation unit's + // kFrontendMaxSamples, which is the source of truth - keep the two in step. + constexpr Int kFrontendMaxSamples = 4; + + // Whether a usampler2DMS fetch's sample index is bounded at RUNTIME by the bound texture's + // real sample count rather than at compile time by the integer ceiling: armed when that + // ceiling is squeezed below what MobileGL advertises AND the depth ceiling - which the + // stencil aspect of a multisample depth/stencil texture is allocated under, and which a + // usampler reads too - exceeds it, so no one static bound is right for both. Read on both + // sides of the translation-memo boundary (the SPIR-V pass defers, the ESSL pass takes + // over); it is a function of three values the memo key already carries, so the two halves + // cannot disagree about one program. + static Bool ClampsUnsignedMultisampleFetchesAtRuntime() { + const Int advertisedMaxSamples = std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); + return g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples && + g_GLESCapabilities.MaxDepthTextureSamples > g_GLESCapabilities.MaxIntegerSamples; + } + + static Uint ResolveBackendEsslVersion() { + const auto& version = g_GLESCapabilities.GLESVersion; + if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) { + return 320; + } + if (version.Major == 3 && version.Minor >= 1) { + return 310; + } + return 300; + } + + // The transpile pipeline invents image binding numbers: when the GL source declares + // an image uniform without layout(binding), glslang auto-assigns one (desktop GL + // allows that and lets the app pick the unit with glUniform1i, which ES forbids on + // image uniforms). The unit the app actually addresses lives in frontend state: the + // layout(binding) reflected at link time, or whatever glUniform1i stored afterwards. + // Rewrite every image uniform declaration to that unit so imageLoad/Store hits the + // unit the app bound with glBindImageTexture. + String RebindImageUniformsToFrontendUnits(String source, const PrgramImpl::ProgramBuildSource& src) { + // P5e (pg): the SOURCE, not the frontend object. The two reads below - the uniform's + // location and the unit glUniform1i put there - are exactly the pair the record's + // set_program_bindings tail carries, so on the handle arm this bakes the unit the client + // observed rather than one it would have had to dereference a live ProgramObject for. + // The null test the SharedPtr form carried moved to the caller, which returned early on + // a null program long before it reached here. + if (source.find("image") == String::npos) { + return source; + } + static const std::regex imageDeclRegex( + R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)"); + static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)"); + + String result; + result.reserve(source.size()); + SizeT lineStart = 0; + while (lineStart <= source.size()) { + const SizeT lineEnd = source.find('\n', lineStart); + const Bool lastLine = lineEnd == String::npos; + String line = source.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart); + + std::smatch match; + if (std::regex_search(line, match, imageDeclRegex)) { + const String name = match[3].str(); + Int location = src.GetUniformLocation(name); + if (location < 0) { + location = src.GetUniformLocation(name + "[0]"); + } + if (location >= 0) { + const Int unit = src.GetUniformSamplerOrImageUnitIndex(location); + if (unit >= 0) { + const String bindingText = "binding = " + std::to_string(unit); + if (std::regex_search(line, bindingValueRegex)) { + line = std::regex_replace(line, bindingValueRegex, bindingText); + } else if (match[1].matched) { + const SizeT layoutOpen = line.find('(', match.position(1)); + line.insert(layoutOpen + 1, bindingText + ", "); + } else { + line.insert(match.position(0), "layout(" + bindingText + ") "); + } + } + } + } + + result += line; + if (lastLine) { + break; + } + result += '\n'; + lineStart = lineEnd + 1; + } + return result; + } + + const MG_Pipe::MGPipeShaderCsoRecord* PipeShaderCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) { + // The composite band is a CLIENT-side indexing detail (contract D13) and the server + // must not learn about it: one reader, one answer, and the band's own table is indexed + // by (slot - base) exactly as the allocator's is. + if (MG_Pipe::MGPipeIsCompositeShaderSlot(cso.Slot)) { + const auto& band = MG_Pipe::MGPipeApplier().CompositeShaderCsos; + const Uint32 index = cso.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase; + if (index >= band.size()) return nullptr; + const auto& record = band[index]; + if (!record.Live || record.Gen != cso.Gen) return nullptr; + return &record; + } + return PipeRecordAt(MG_Pipe::MGPipeApplier().ShaderCsos, cso); + } + + namespace PrgramImpl { + Uint32 g_snormFallbackClampOutputMask = 0; + Uint g_fragColorBroadcastCount = 1; + Uint32 g_unormFallbackClampOutputMask = 0; + Uint g_lastUsedBackendProgramId = 0; + // Every error-queue drain in the program build path is bounded by this: a lost + // context never answers GL_NO_ERROR, and the build runs on the thread that would + // then spin forever. + constexpr Int kMaxDrainedProgramErrors = 32; + TwinRegistry g_backendProgramObjects; + + // ---- P5e (pg): the source, both ways round --------------------------------------- + + Int ProgramArchiveSource::GetUniformLocation(const String& name) const { + // ProgramObject::GetUniformLocation over the archive. Reproduced rather than called + // because the frontend's form is a non-static member over Artifacts() and this arm + // has no ProgramObject to call it on; the ARRAY RULES are the whole body and they + // are the part a paraphrase would get wrong, so they are transcribed exactly. + const auto it = Link->uniformLocations.find(name); + if (it != Link->uniformLocations.end()) return static_cast(it->second); + if (name.empty()) return -1; + // Reflection stores GL-style names: an array uniform is keyed "arr[0]" at its base + // location, so a bare "arr" resolves to that entry. + if (name.back() != ']') { + const auto suffixed = Link->uniformLocations.find(name + "[0]"); + if (suffixed != Link->uniformLocations.end()) return static_cast(suffixed->second); + return -1; + } + if (name.length() < 4) return -1; + // An ARRAY OF ARRAYS is keyed by its full "[0]"-terminated spelling ("a[2][1][0]"), + // so a query that already ends in a subscript may still be the NAME of an array + // rather than an element of one. That reading is tried FIRST; only then is the + // trailing subscript read as an element index. + { + const auto arrayOfArrays = Link->uniformLocations.find(name + "[0]"); + if (arrayOfArrays != Link->uniformLocations.end()) { + return static_cast(arrayOfArrays->second); + } + } + const SizeT bracket = name.rfind('['); + if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1; + Uint element = 0; + for (SizeT i = bracket + 1; i < name.length() - 1; ++i) { + if (name[i] < '0' || name[i] > '9') return -1; + element = element * 10 + static_cast(name[i] - '0'); + if (element > 0x0FFFFFFFu) return -1; + } + auto base = Link->uniformLocations.find(name.substr(0, bracket) + "[0]"); + if (base == Link->uniformLocations.end()) { + base = Link->uniformLocations.find(name.substr(0, bracket)); + if (base == Link->uniformLocations.end()) return -1; + } + const Int baseLocation = static_cast(base->second); + if (!IsValidUniformLocation(baseLocation)) return -1; + const Int index = Link->uniformIndexInTProgram[baseLocation]; + if (!UniformAt(index).type.isArray) return -1; + if (static_cast(element) >= + MG_State::GLState::ProgramObject::GetUniformArraySizeByTIndex(*Link, index)) { + return -1; + } + const Int location = baseLocation + static_cast(element); + if (!UniformLocationsAliasSameUniform(baseLocation, location)) return -1; + return location; + } + + const String& ProgramArchiveSource::GetUniformBlockName(Uint index) const { + static const String kEmpty; + if (index >= Link->glBlockIndexToTProgram.size()) return kEmpty; + const Int tBlockIndex = Link->glBlockIndexToTProgram[index]; + if (tBlockIndex < 0 || static_cast(tBlockIndex) >= Link->blockReflection.size()) { + return kEmpty; + } + return Link->blockReflection[tBlockIndex].name; + } + + Uint ProgramArchiveSource::GetUniformBlockBinding(Uint index) const { + // THE OVERLAY WINS WHEN THERE IS ONE, and the archive's link-time value stands when + // there is not. glUniformBlockBinding moves this AFTER the link that produced the + // archive, so a twin answering from the archive alone would bind the program's + // blocks to the points it was LINKED with rather than the ones it is BOUND with. + if (OverlayGoverns && BlockBindingOverlay != nullptr) { + if (index < BlockBindingOverlay->size()) { + return static_cast((*BlockBindingOverlay)[index]); + } + // A block past the tail the record declared. The tail is dense over + // GetActiveUniformBlocksCount(), so this is a record and an archive that + // disagree about how many blocks the program has - which can only happen if a + // create and a bindings record crossed. 0 is GL's own default and is the safe + // answer; the mismatch is the emitter's to fix, not this read's to hide. + return 0; + } + if (index >= Link->uniformBlockBinding.size()) return 0; + return static_cast(Link->uniformBlockBinding[index]); + } + + Int ProgramArchiveSource::GetUniformSamplerOrImageUnitIndex(Uint location) const { + if (OverlayGoverns) { + // THE TAIL IS THE WHOLE SET, so a location it does not name has no unit - NOT + // whatever the archive's link-time snapshot said. The client emits every + // assigned unit it can see, including the ones the link seeded from + // layout(binding=N), so "absent" really does mean -1. + // + // Sparse and ASCENDING BY LOCATION on the wire (CONTRACT-P5E §1), which is what + // lets this be a binary search: the build walks every location from 0 to + // maxUniformLocation, and a linear scan per location would be quadratic on a + // program with a thousand locations and a handful of samplers. + if (SamplerUnitOverlay == nullptr) return -1; + const auto it = std::lower_bound( + SamplerUnitOverlay->begin(), SamplerUnitOverlay->end(), location, + [](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) { + return entry.Location < value; + }); + if (it == SamplerUnitOverlay->end() || it->Location != location) return -1; + return static_cast(it->Unit); + } + if (location >= Link->uniformSamplerOrImageUnitIndex.size()) return -1; + return Link->uniformSamplerOrImageUnitIndex[location]; + } + + ProgramArchiveSource ProgramArchiveSource::FromRecord( + MG_Pipe::MGPipeHandle cso, const MG_Pipe::MGPipeShaderCsoRecord& record) { + // THE HANDLE ARM. Every field below is server-owned: the archive the create adopted, + // the descriptor the create stored, and the three tails set_program_bindings + // carried. Nothing here touches a ProgramObject, which is the property the strict + // lane measures. + ProgramArchiveSource source; + source.Link = &record.Archive->Link; + source.Spirv = &record.Archive->Spirv; + source.Identity = cso.Slot; + source.IdentityIsHandleSlot = true; + // LinkStatus IS A FIELD NOW, and that is the whole point of c0e adding it: before + // P5e "linked" was only implied by the create existing, which is a different + // statement - create_shader_state is re-issued at every link that moves the link + // version, and a FAILED relink of a bound program moves it too (ID-88). + source.Linked = record.Desc.LinkStatus != 0; + source.SpirvUsable = record.Desc.SpirvStatus != 0; + source.SpirvValidationEnabled = record.Desc.EnableSpirvValidation != 0; + source.PointSizeWasDemoted = record.Desc.PointSizeDemoted != 0; + source.GlobalUboSize = record.Desc.GlobalUboSize; + source.LinkedStages.reserve(record.Archive->LinkedStages.size()); + for (const Uint32 stage : record.Archive->LinkedStages) { + source.LinkedStages.push_back(static_cast(stage)); + } + // "HAS A BINDINGS RECORD EVER BEEN APPLIED TO THIS RECORD" - not "is the tail + // non-empty". A program with no uniform blocks, no sampler and no override emits a + // record with three empty tails, and that record is the statement that the archive's + // link-time values are no longer the answer. + source.OverlayGoverns = record.BindingsSerial != 0; + source.BlockBindingOverlay = &record.BlockBindings; + source.SamplerUnitOverlay = &record.SamplerUnits; + if (source.OverlayGoverns) { + source.StorageOverrides.reserve(record.StorageOverrides.size()); + for (const auto& entry : record.StorageOverrides) { + source.StorageOverrides[entry.Name] = entry.Binding; + } + } else { + source.StorageOverrides = record.Archive->Link.shaderStorageBlockBinding; + } + // THE RECORD'S NUMBER EITHER WAY, and deliberately not a recomputation when the + // overlay is absent. This is the value the twin STAMPS and the draw path's clean + // clause COMPARES (CONTRACT-P5E §5.5 names record.Signature as the input), so the + // two have to be the same field or every draw of a program with an override + // rebuilds. A record with no bindings applied carries 0 while the archive may hold + // a link-time override map - the next set_program_bindings then produces a non-zero + // signature and forces exactly one rebuild, which is correct and, since the emitter + // sends the bindings in the same breath as the create, not a state a draw reaches. + source.StorageOverrideSignature = record.Signature; + return source; + } + + Uint ProgramBlockBindingFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Int blockIndex) { + if (blockIndex < 0) return 0; + const SizeT index = static_cast(blockIndex); + if (record.BindingsSerial != 0) { + // The tail is the whole set; a block past it is a record and an archive that + // disagree about the block count, and GL's own default of 0 is the safe answer. + return index < record.BlockBindings.size() + ? static_cast(record.BlockBindings[index]) + : 0; + } + if (!record.Archive || index >= record.Archive->Link.uniformBlockBinding.size()) return 0; + return static_cast(record.Archive->Link.uniformBlockBinding[index]); + } + + Int ProgramSamplerUnitFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Uint location) { + if (record.BindingsSerial != 0) { + const auto it = std::lower_bound( + record.SamplerUnits.begin(), record.SamplerUnits.end(), location, + [](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) { + return entry.Location < value; + }); + if (it == record.SamplerUnits.end() || it->Location != location) return -1; + return static_cast(it->Unit); + } + if (!record.Archive || + location >= record.Archive->Link.uniformSamplerOrImageUnitIndex.size()) { + return -1; + } + return record.Archive->Link.uniformSamplerOrImageUnitIndex[location]; + } + + // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract - in particular why the + // composite band had to land in the same package as this function. + BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso) { + if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; + // THE RECORD FIRST, and through the BAND-AWARE reader: a composite pipeline's CSO + // lives in the applier's CompositeShaderCsos table, and asking the ordinary one for + // it would answer null for a program that is perfectly live. + if (PipeShaderCsoRecordForHandle(cso) == nullptr) { + MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record on the handle arm, so " + "no driver program can be built for it and the draw keeps what is bound", + cso.Slot, cso.Gen); + return nullptr; + } + auto* slot = AdoptTwinByHandle(g_backendProgramObjects, cso, "shader CSO"); + if (slot == nullptr) return nullptr; + if (!*slot) *slot = MakeShared(); + return slot->get(); + } + + BackendProgramObjectImpl::BackendProgramObjectImpl() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + m_backendProgramId = g_GLESFuncs.glCreateProgram(); + if (m_backendProgramId == 0) { + MGLOG_E_ONCE("Failed to create program object in backend."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + + } else { + MGLOG_D("Created backend program object with ID: %u", m_backendProgramId); + } + } + + BackendProgramObjectImpl::~BackendProgramObjectImpl() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (InProcessTeardown()) { + return; // see InProcessTeardown(): the driver may be unloaded already + } + if (m_backendProgramId != 0) { + MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId); + g_GLESFuncs.glDeleteProgram(m_backendProgramId); + // The driver may recycle this GL name for a future program; a stale + // guard entry would then wrongly skip the glUseProgram for it. + if (g_lastUsedBackendProgramId == m_backendProgramId) { + g_lastUsedBackendProgramId = 0; + } + } + } + + Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding) { + if (backendProgramId == 0 || blockName.empty()) return false; + if (!g_GLESFuncs.glGetProgramResourceIndex || !g_GLESFuncs.glShaderStorageBlockBinding) return false; + GLuint driverIndex = + g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, blockName.c_str()); + if (driverIndex == GL_INVALID_INDEX) { + // An arrayed block is enumerated per element by GL but declared once; the + // generated ESSL carries the bare block name. + const auto bracket = blockName.rfind('['); + if (bracket == String::npos || blockName.back() != ']') return false; + driverIndex = g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, + blockName.substr(0, bracket).c_str()); + if (driverIndex == GL_INVALID_INDEX) return false; + } + g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, driverIndex, binding); + return true; + } + + void ReseedShaderStorageBlockBindings(Uint backendProgramId, + const ProgramBuildSource& src) { + const auto& overrides = src.GetShaderStorageBlockBindingOverrides(); + if (overrides.empty()) return; // the overwhelming majority of programs + for (const auto& [blockName, binding] : overrides) { + if (binding < 0) continue; + ApplyShaderStorageBlockBinding(backendProgramId, blockName, static_cast(binding)); + } + } + + // P5e (pg). ON A PUSH BUILD THE SIGNATURE IS THE SOURCE'S, TAKEN VERBATIM, and it is not + // recomputed here for a reason worth stating: on the handle arm the number the DRAW PATH + // compares against is MGPipeShaderCsoRecord::Signature - the CLIENT's commutative hash, + // computed in MG_Impl/Pipe/ProgramEmit.h. If the server recomputed its own from the + // record's override tail, the two formulas would have to stay bit-identical for ever or + // every draw of every program with an override would rebuild. Taking the client's number + // verbatim removes that coupling: there is one authority, and the twin's stamp and the + // clean clause read the same field. The monolith arm's source carries the answer of the + // function this replaces, computed from the frontend map, so its behaviour is unchanged. + Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src) { + return src.StorageOverrideSignature; + } + + namespace { + // The GL internal format bound to an image unit right now. GL_NONE for a unit + // outside the frontend's array, which cannot be addressed at all. + Uint BoundImageUnitFormat(Int unit) { + if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0; + // P5e (fb, §5.4): the format-less image bake asks what format the APPLICATION + // named at this unit, and under a transport the record is the only statement of + // it - MGPImageView::InternalFormat is that same GLenum, copied by the client + // out of the unit's own binding. A unit the applier has never been told about + // answers 0, which is what an unbound unit answers on the frontend arm too. + const auto& st = MG_Pipe::MGPipeApplier(); + if (static_cast(unit) >= st.BoundShaderImages.size()) return 0; + return static_cast(st.BoundShaderImages[static_cast(unit)].InternalFormat); + } + + // Combines one (unit, format) pair into a running digest. Commutative, so the order + // the uniforms are walked in cannot change the answer, and mixed rather than summed + // so a unit and a format cannot trade places between two pairs and cancel out. + Uint64 MixImageUnitFormat(Uint64 signature, Int unit, Uint format) { + Uint64 entry = static_cast(static_cast(unit)) + 0x9e3779b97f4a7c15ull; + entry ^= static_cast(format) + 0xbf58476d1ce4e5b9ull + (entry << 6) + (entry >> 2); + return signature + entry; + } + + // Pipeline position of a shader stage. Names the PRODUCER of an inter-stage + // interface block: a block one stage consumes was written by the stage before it. + // ShaderStage is declared in pipeline order, so the enum value IS the position; + // compute has no inter-stage interface at all and is reported as -1. + Int InterStagePipelineIndex(ShaderStage stage) { + switch (stage) { + case ShaderStage::Vertex: + case ShaderStage::TessControl: + case ShaderStage::TessEval: + case ShaderStage::Geometry: + case ShaderStage::Fragment: + return static_cast(stage); + default: + return -1; + } + } + + // Whether a stage can declare interface blocks in BOTH directions at once, i.e. + // whether one block name can name two different blocks inside it. A vertex INPUT + // and a fragment OUTPUT cannot be blocks and compute has neither, so only these + // three can. This is what keeps the module probe off every program without + // tessellation or geometry - which is every program Minecraft and its shader packs + // build. + Bool CanDeclareBlocksInBothDirections(ShaderStage stage) { + return stage == ShaderStage::TessControl || stage == ShaderStage::TessEval || + stage == ShaderStage::Geometry; + } + + // Reflection names an array uniform after its first element ("g_image[0]") at every + // location it spans; SPIR-V names the variable once, without the subscript. This is + // the name both sides agree on. + String ImageUniformBaseName(const String& reflectionName) { + if (reflectionName.size() >= 3 && reflectionName.compare(reflectionName.size() - 3, 3, "[0]") == 0) { + return reflectionName.substr(0, reflectionName.size() - 3); + } + return reflectionName; + } + + // GL_MAX__IMAGE_UNIFORMS as the ES driver reports it, which is also exactly what + // MobileGL advertises for it (GL_Getter answers from the same DynamicBackendParameters). + // -1 for a stage the ES side has no such limit for, which is the "cannot say" answer + // the diagnostic that reads it prints rather than a made-up number. [[maybe_unused]] + // because its only caller is an MGLOG_E argument, and MGLOG_E compiles to nothing in a + // build whose MOBILEGL_LOG_ACTIVE_LEVEL is above ERROR. + [[maybe_unused]] Int AdvertisedStageImageUniformLimit(ShaderStage stage) { + switch (stage) { + case ShaderStage::Vertex: return g_GLESCapabilities.MaxVertexImageUniforms; + case ShaderStage::TessControl: return g_GLESCapabilities.MaxTessControlImageUniforms; + case ShaderStage::TessEval: return g_GLESCapabilities.MaxTessEvaluationImageUniforms; + case ShaderStage::Geometry: return g_GLESCapabilities.MaxGeometryImageUniforms; + case ShaderStage::Fragment: return g_GLESCapabilities.MaxFragmentImageUniforms; + case ShaderStage::Compute: return g_GLESCapabilities.MaxComputeImageUniforms; + default: return -1; + } + } + + // Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only + // through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes + // through untouched - the emitted ESSL still has to be legal for the driver. + Bool IsCoreEsslLayoutFormat(glslang::TLayoutFormat format) { + switch (format) { + case glslang::ElfRgba32f: + case glslang::ElfRgba16f: + case glslang::ElfR32f: + case glslang::ElfRgba8: + case glslang::ElfRgba8Snorm: + case glslang::ElfRgba32i: + case glslang::ElfRgba16i: + case glslang::ElfRgba8i: + case glslang::ElfR32i: + case glslang::ElfRgba32ui: + case glslang::ElfRgba16ui: + case glslang::ElfRgba8ui: + case glslang::ElfR32ui: + return true; + default: + return false; + } + } + + // The GL internal format a glslang layout format names, for the eighteen non-core + // formats WidenImageFormatsForEssl carries losslessly plus nothing else: the only + // question asked of it is "does this DECLARED format widen", and answering 0 for + // everything else is the same "no" a non-widenable format gets. Kept as its own + // switch rather than routed through the frontend's enum converters because a + // TLayoutFormat is a glslang value and the reflection snapshot stores it raw. + // + // IT MUST LIST EXACTLY WHAT WideningOfSpirvImageFormat DOES. This table is what arms + // the pass (ImageFormatWillBeWidened -> declaresWidenableImageFormat), so a format the + // pass would carry but this switch answers 0 for never gets the chance: the module + // reaches SPIRV-Cross with its original qualifier, the throw takes the stage, and the + // only visible symptom is the "no GLSL ES spelling" diagnostic for a format that has + // one. That is exactly what r11f_g11f_b10f did until it was added here. + Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) { + switch (format) { + case glslang::ElfRg32f: return 0x8230; // GL_RG32F + case glslang::ElfRg16f: return 0x822F; // GL_RG16F + case glslang::ElfR16f: return 0x822D; // GL_R16F + case glslang::ElfRg8: return 0x822B; // GL_RG8 + case glslang::ElfR8: return 0x8229; // GL_R8 + case glslang::ElfRg8Snorm: return 0x8F95; // GL_RG8_SNORM + case glslang::ElfR8Snorm: return 0x8F94; // GL_R8_SNORM + case glslang::ElfRg32i: return 0x823B; // GL_RG32I + case glslang::ElfRg16i: return 0x8239; // GL_RG16I + case glslang::ElfR16i: return 0x8233; // GL_R16I + case glslang::ElfRg8i: return 0x8237; // GL_RG8I + case glslang::ElfR8i: return 0x8231; // GL_R8I + case glslang::ElfRg32ui: return 0x823C; // GL_RG32UI + case glslang::ElfRg16ui: return 0x823A; // GL_RG16UI + case glslang::ElfR16ui: return 0x8234; // GL_R16UI + case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI + case glslang::ElfR8ui: return 0x8232; // GL_R8UI + // Not a channel widening but a lossless re-encoding into rgba16f - the one entry + // here whose carrier has a different per-channel layout. See + // WidenImageFormatsPass.h. + case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F + // 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same + // channel count, every value representable. Only the transfer is re-encoded. + case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI + // The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES. + // These are the entries whose carrier changes the shader-visible type as well as + // the qualifier (image2D becomes uimage2D), so every access through them is + // wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h. + case glslang::ElfRgba16: return 0x805B; // GL_RGBA16 + case glslang::ElfRg16: return 0x822C; // GL_RG16 + case glslang::ElfR16: return 0x822A; // GL_R16 + case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2 + case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM + case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM + case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM + default: + return 0; + } + } + + // Whether the ESSL chain will re-declare an image of this format in a core carrier + // and mask its accesses (WidenImageFormatsForEssl). The same rule + // TextureImpl::GetImageBindableStorageWidening applies to the storage and the bind - + // the three layers move together or the shader addresses a texel size the storage + // does not have. + // + // Without GL_NV_image_formats there is no legal spelling for any non-core format, so + // everything carriable widens. WITH the extension only the formats SPIRV-Cross + // refuses to print do: it throws for its is_desktop_only_format set instead of + // emitting a token, and the throw loses the stage however willing the driver was. + Bool ImageFormatWillBeWidened(Uint glInternalFormat) { + if (glInternalFormat == 0) return false; + if (MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) == 0) { + return false; + } + return !g_GLESCapabilities.SupportsExtendedImageFormats || + !MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat( + glInternalFormat); + } + } // namespace + + // What the format bake needs from the frontend, collected in one walk of the uniform + // reflection: which image uniforms declared NO format (the only ones a bake may touch - + // a declared format is authoritative and stays), what the units they address currently + // hold, and whether any format in play - declared or baked - is outside the ES core set. + ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src) { + 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 = src.GetMaxUniformLocation(); + for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { + const auto& name = src.GetUniformName(loc); + if (name.empty()) continue; + if (!IsImageUniformType(src.GetUniformType(loc))) continue; + const auto& type = src.GetUniformTypeFacts(loc); + if (type.hasFormat) { + // Declared, and therefore never overridden by the BAKE - but a non-core + // spelling still has to become legal ESSL somehow. + const auto declaredFormat = static_cast(type.layoutFormat); + if (!IsCoreEsslLayoutFormat(declaredFormat)) { + // Eighteen of the twenty-six non-core formats are re-declared in a core + // format that carries them losslessly, with every access masked back to + // the channels GL says they have (WidenImageFormatsForEssl, and the + // matching storage/bind widening in TextureImpl). Those need neither the + // extension nor the diagnostic: there IS a legal spelling for them now. + if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) { + inputs.declaresWidenableImageFormat = true; + } else { + inputs.needsExtendedImageFormats = true; + if (!g_GLESCapabilities.SupportsExtendedImageFormats) { + // From the OWNED TypeFacts, not from a live TType: the reflection + // snapshot already carries the declared layout format, and there + // is no glslang object to ask on a translation-cache L1 hit. + recordUnspellableFormat( + name, glslang::TQualifier::getLayoutFormatString(declaredFormat)); + } + } + } + continue; + } + const Int unit = src.GetUniformSamplerOrImageUnitIndex(loc); + if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue; + const Uint boundFormat = BoundImageUnitFormat(unit); + + // Every format-less uniform contributes to the rebuild key, including one whose + // unit holds nothing yet: an image bound for the first time AFTER the link has + // to move the key, or the program built against "nothing bound" would never be + // rebuilt against the real format. + inputs.units.push_back(unit); + inputs.signature = MixImageUnitFormat(inputs.signature, unit, boundFormat); + + if (boundFormat == 0) continue; + if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) { + // The same three-way split the DECLARED branch above makes, and it has to be + // the same one: a format-less image is baked with the bound format, so from + // WidenImageFormatsForEssl's point of view the two routes hand it identical + // modules and must arm it identically. + if (ImageFormatWillBeWidened(boundFormat)) { + // The bake writes this format INTO the module, so the widening that runs + // straight after has to be armed for it even though nothing DECLARED it - + // and armed WHETHER OR NOT the driver has GL_NV_image_formats. SPIRV-Cross + // throws for its is_desktop_only_format set the moment it targets ESSL, + // however willing the driver was, so the extension decides HOW MUCH gets + // widened (widenOnlyUnprintableImageFormats) and never WHETHER. Arming + // this only on the no-extension path left the shader half of the widening + // switched off while TextureImpl's storage/bind half - which keys on + // SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran: + // the stage threw, the program linked without it, and every dispatch + // silently did nothing. That is the whole of + // KHR-GL43.stencil_texturing.functional's compute half, whose uni_image is + // a format-less uimage2D bound to an R8UI texture. + inputs.declaresWidenableImageFormat = true; + } else if (!g_GLESCapabilities.SupportsExtendedImageFormats) { + // Outside the GLSL ES core set, with no GL_NV_image_formats to spell it + // and no core format that carries it exactly: there is no legal ESSL for + // this stage at all. Leaving the image format-LESS is NOT a softer + // failure: all three test devices reject a format-less image declaration + // outright ("all images have to define layout format"), readonly and + // writeonly alike, so it trades one hard compile error for another. The + // unit stays in the rebuild key either way, so a rebind to a spellable + // format still rebuilds and works. + MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which " + "GLSL ES core cannot spell, this driver has no GL_NV_image_formats for, and " + "no core format carries exactly.", + name.c_str(), unit, boundFormat); + recordUnspellableFormat( + name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat)); + continue; + } else { + inputs.needsExtendedImageFormats = true; + } + } + const String baseName = ImageUniformBaseName(name); + const auto existing = inputs.glFormatByUniformName.find(baseName); + if (existing == inputs.glFormatByUniformName.end()) { + inputs.glFormatByUniformName.emplace(baseName, boundFormat); + } else if (existing->second != boundFormat) { + // An ARRAY whose elements were pointed at units holding different formats. + // One declaration carries one qualifier, so there is no spelling for it, and + // the uniform is left format-less rather than given a format that is wrong + // for all but one element. Marked in place with GL_NONE and swept below - + // never by erasing here, because the entry is reached again by the array's + // remaining elements and a flat hash map must not be mutated structurally + // while an iterator into it is live. + existing->second = 0; + } + } + for (const auto& entry : inputs.glFormatByUniformName) { + if (entry.second == 0) inputs.conflictedNames.push_back(entry.first); + } + for (const auto& conflicted : inputs.conflictedNames) { + inputs.glFormatByUniformName.erase(conflicted); + } + // Split off the ones SPIRV-Cross will not print. They cannot go through the module - + // it throws for them when targeting ESSL, and the stage is lost - so they are spelled + // into the emitted text instead. Collected first, erased after, because a flat hash + // map must not be restructured while it is being walked. + Vector textCompleted; + for (const auto& entry : inputs.glFormatByUniformName) { + if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) { + continue; + } + // A format the widening carries stays on the module route even though SPIRV-Cross + // would not print it: by the time SPIRV-Cross sees the declaration it names the + // core carrier, which it does print. Writing the narrow spelling into the text + // instead would put back exactly the token the driver rejects. + if (ImageFormatWillBeWidened(entry.second)) { + continue; + } + String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second); + if (spelling.empty()) continue; // no image-format spelling at all; nothing to write + inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling)); + textCompleted.push_back(entry.first); + } + 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; + } + + // Every image ARRAY whose elements the application did NOT leave on units consecutive from + // element zero - the only shape ESSL can spell, since an image unit there comes solely + // from the one layout(binding=N) an array declaration carries. Desktop GL assigns them + // per element with glUniform1i, which ES makes an INVALID_OPERATION on an image uniform, + // so there is nothing to fix at the API end and the emitted text has to carry it + // (RemapImageArrayElementUnits). Empty for every program that does not do this, which is + // very nearly all of them - one walk of the reflection and no allocation in that case. + Vector CollectNonConsecutiveImageArrayPlans(const ProgramBuildSource& src) { + Vector plans; + const Uint maxUniformLoc = src.GetMaxUniformLocation(); + for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { + const auto& name = src.GetUniformName(loc); + if (name.empty()) continue; + if (!IsImageUniformType(src.GetUniformType(loc))) continue; + // Reflection repeats the array's "g_image[0]" spelling at EVERY location the array + // spans, so only the location that name resolves back to is the array itself. + if (src.GetUniformLocation(name) != static_cast(loc)) continue; + const String baseName = ImageUniformBaseName(name); + if (baseName == name) continue; // a scalar image: one binding says it all + + ImageArrayUnitPlan plan; + plan.name = baseName; + for (Uint element = loc; element <= maxUniformLoc && + src.UniformLocationsAliasSameUniform( + static_cast(loc), static_cast(element)); + ++element) { + plan.units.push_back(src.GetUniformSamplerOrImageUnitIndex(element)); + } + if (plan.units.size() < 2) continue; + + Bool consecutive = true; + for (SizeT element = 0; element < plan.units.size(); ++element) { + if (plan.units[element] != plan.units[0] + static_cast(element)) { + consecutive = false; + break; + } + } + // What ESSL does unaided is already right; leaving these out is what keeps the + // emitted text of every ordinary image shader byte-identical to before. + if (consecutive) continue; + plans.push_back(Move(plan)); + } + return plans; + } + + Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const { + if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs + Uint64 signature = 0; + for (const Int unit : m_formatlessImageUnits) { + signature = MixImageUnitFormat(signature, unit, BoundImageUnitFormat(unit)); + } + return signature; + } + + Bool BackendProgramObjectImpl::ImageUnitFormatsStillMatch() const { + if (m_formatlessImageUnits.empty()) return m_imageUnitFormatSignature == 0; + return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature; + } + + + // ===== THE MEMOIZED SEGMENT (shader translation memo, level 2) ===== + // + // One stage's sanitized SPIR-V turned into the ESSL SPIRV-Cross emits, through the + // DirectGLES-specific pass chain. Extracted out of SyncToBackend's loop so that the + // boundary the L2 memo keys on is a function signature rather than a comment: every + // input this reads is either an argument below or a process-global capability bit, + // and EVERY ONE OF THEM IS IN EsslTranslationKeyInputs. If you add a read here, add + // it to BuildEsslTranslationKey too - an under-specified key here is a silently + // miscompiled shader. + // + // Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray, + // MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples, + // SupportsNoperspectiveInterpolation, SupportsExtendedImageFormats, GLESVersion} + // (the last via ResolveBackendEsslVersion); and m_backendProgramId, for a log line only. + // + // Deliberately NOT in here, and therefore NOT in the key: the text-level passes that + // follow in SyncToBackend. They are cheap string work and they read a long tail of + // live per-program state (RebindImageUniformsToFrontendUnits walks the ProgramObject + // reflection, the norm-clamp masks and the fragColor broadcast count are live + // globals, the buffer-texture tier retargets an #extension line) whose inclusion + // would make the key both enormous and fragile for no measurable saving. + // + // Returns false when SPIRV-Cross refused the module; `outError` then holds its + // message and nothing is memoized. + Bool BackendProgramObjectImpl::TranspileSpirvToEssl( + const Vector& spirvCode, const GLenum glShaderType, + const std::set& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake, + const UnorderedMap& storageBlockBindingOverrides, + const std::map& inputBlockRenames, + const std::map& outputBlockRenames, const Bool stripInputBlockLocations, + const Bool stripOutputBlockLocations, + const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource, + std::set& outFlattenedXfbBlockNames, Vector& outAtomicCounterGlBindings, + String& outError) const { + // ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to + // plain globals (mg_*) before handing the module to SPIRV-Cross. + Vector loweredSpirv; + const Vector* effectiveSpirv = &spirvCode; + if (glShaderType == GL_VERTEX_SHADER && + MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) && + !loweredSpirv.empty()) { + effectiveSpirv = &loweredSpirv; + } + + // A geometry stage that writes memory, on a driver that drops the writes it makes + // after EmitVertex(): every emit is deferred behind them (DeferGeometryEmitsPass). FIRST + // in the chain, while every output is still an Output variable - the viewport-index + // lowering below demotes one, and must see the replay's stores to it as well. + // Self-gated on the module (an emit and a memory write), so a geometry stage that + // writes nothing comes back untouched. Keyed by deferGeometryEmitsArmed. + Vector deferredEmitSpirv; + if (glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit) { + bool deferred = false; + if (MG_Util::ShaderTranspiler::ShaderCompiler::DeferGeometryEmitsForEssl( + *effectiveSpirv, deferredEmitSpirv, deferred, enableSpirvValidation) && + deferred && !deferredEmitSpirv.empty()) { + effectiveSpirv = &deferredEmitSpirv; + MGLOG_I_ONCE("DirectGLES is deferring a geometry stage's EmitVertex() calls behind its " + "memory writes, because this driver drops storage writes made after an emit."); + } + } + + // GL_KHR_shader_subgroup where the ES driver has none (SubgroupPolicy.h): the compute + // stage's subgroup operations are lowered onto a 32-lane virtual subgroup in + // workgroup-shared memory, so the ESSL needs nothing from the driver but barrier() + // and shared memory. Where the driver has subgroups, the operations stay and + // SPIRV-Cross spells them as GL_KHR_shader_subgroup_* built-ins. iterationRP's + // scratch race is patched first unless MOBILEGL_ITRP_FIX is ForceOff, as + // on DirectVulkan. On a driver whose shared-memory loads go stale across barrier() + // the scratch is read through atomic loads. Keyed by subgroupEmulationArmed / + // iterationRPBarrierFixArmed / subgroupScratchAtomicReads. + Vector barrierFixedSpirv; + Vector emulatedSubgroupSpirv; + if (glShaderType == GL_COMPUTE_SHADER && + ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated) { + if (ShouldApplyIterationRPFixes() && + MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan( + *effectiveSpirv, barrierFixedSpirv, enableSpirvValidation) && + !barrierFixedSpirv.empty()) { + effectiveSpirv = &barrierFixedSpirv; + } + if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan( + *effectiveSpirv, emulatedSubgroupSpirv, + static_cast(g_GLESCapabilities.MaxComputeSharedMemorySize), enableSpirvValidation, + g_GLESCapabilities.SharedMemoryLoadsGoStale) && + !emulatedSubgroupSpirv.empty()) { + effectiveSpirv = &emulatedSubgroupSpirv; + } else { + MGLOG_E("DirectGLES: subgroup emulation declined a compute stage; it keeps subgroup " + "operations this ES driver cannot compile"); + } + } + + // ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters + // there IS an extension that provides it - so this runs only when the driver does + // NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and + // gets the `#extension` request added to the decompiled source below instead. + // Demoting the builtin costs the multi-viewport routing (every invocation lands in + // viewport 0), which is the degradation ViewportArrayScenario already documents + // for this backend; NOT demoting it costs the whole program, because the stage + // fails to compile and every draw made with it silently renders nothing. + // Gated on the module actually declaring the output, so no other stage pays an + // optimizer round trip for it. + // One parse of the module answers every armed pass gate below. The per-gate + // Declares* probes each cost a BuildModule per stage, and on a driver where both + // gates are armed (Mali: no GL_OES_viewport_array AND integer multisample + // squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower. + // Probing the pre-lowering module is sound for both gates: demoting + // gl_ViewportIndex neither adds nor removes multisampled image types. + // Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): + // this is backend code and must not reach into the GL frontend. 4 is that + // translation unit's kFrontendMaxSamples, which is the source of truth - + // keep the two in step. + const Int advertisedMaxSamples = + std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); + // Armed by the EMULATION as well as by the missing extension, and the emulation is on + // by default (MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a + // reason to keep the builtin: it only ever gave the SHADER a compilable name, while + // the driver's INDEXED viewport state was never programmed by anything in MobileGL + // (SyncRenderState pushes index 0 and stops), so an extension-capable driver + // rasterized every index as index 0 exactly like a driver without it. Lowering here + // is what lets the ESSL passes downstream turn the builtin into the flat varying the + // replay gates on. + // + // Restricted to the three stages GL lets WRITE the builtin (4.1 core gives it to the + // geometry stage, ARB_shader_viewport_layer_array adds vertex and tessellation + // evaluation). A fragment stage's gl_ViewportIndex is an INPUT, which the pass + // declines anyway, and a compute stage has none - so arming those two only ever + // bought them the shared probe's BuildModule for nothing. + const Bool stageCanWriteViewportIndex = glShaderType == GL_VERTEX_SHADER || + glShaderType == GL_TESS_EVALUATION_SHADER || + glShaderType == GL_GEOMETRY_SHADER; + const Bool viewportLoweringArmed = + stageCanWriteViewportIndex && + (ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray); + const Bool sampleClampArmed = + g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples || + g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples || + g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples; + // The image-format widening is armed on EVERY driver, so its probe has to ride the + // shared parse rather than add one: it is asked of every stage of every program, and + // a BuildModule per stage per gate is exactly what cost compile-heavy CTS cases ~10% + // before this struct existed. What differs per driver is only HOW MUCH it widens - + // everything carriable where there is no GL_NV_image_formats to spell the narrow + // format, and only the formats SPIRV-Cross refuses to print where there is. + const Bool widenOnlyUnprintableImageFormats = g_GLESCapabilities.SupportsExtendedImageFormats; + MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates; + if (viewportLoweringArmed || sampleClampArmed) { + spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures( + *effectiveSpirv); + } + + Vector loweredViewportSpirv; + if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput && + MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl( + *effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) && + !loweredViewportSpirv.empty()) { + effectiveSpirv = &loweredViewportSpirv; + MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which ESSL has no core " + "spelling for. The builtin was demoted to a plain global; %s.", + m_backendProgramId, + MG_Util::ConvertGLEnumToString(glShaderType).c_str(), + ViewportArrayEmulationEnabled() + ? "the ESSL passes below promote it to a routing varying" + : "every invocation renders into viewport 0"); + } + + // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample + // ceiling MobileGL advertises is floored to 4 no matter what the ES driver + // reports - but the realised allocation cannot be, and + // ClampSamplesToBackendSupport quietly gives an integer or depth multisample + // texture the ONE sample Adreno and Mali actually support for it. A shader + // written against the advertised ceiling then fetches a sample that storage does + // not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and + // KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp + // the Sample operand to the backend-real per-category maximum so the fetch lands + // inside the allocation. Gated on some category actually being squeezed AND the + // module actually declaring a multisampled image, so no other stage pays an + // optimizer round trip for it. DirectVulkan is deliberately not given this: it + // allocates the sample count it was asked for, so its modules are already right. + Vector clampedSampleSpirv; + if (sampleClampArmed && spirvGates.DeclaresMultisampledImage && + MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl( + *effectiveSpirv, clampedSampleSpirv, + g_GLESCapabilities.MaxColorTextureSamples, + g_GLESCapabilities.MaxIntegerSamples, + g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples, + enableSpirvValidation, ClampsUnsignedMultisampleFetchesAtRuntime()) && + !clampedSampleSpirv.empty()) { + effectiveSpirv = &clampedSampleSpirv; + } + + // GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module + // rather than emulating them, so this has to happen before it sees the binary. + Vector splitArrayInputSpirv; + if (glShaderType == GL_VERTEX_SHADER && + MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl( + *effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) && + !splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) { + // Only when the pass ACTUALLY split something. The optimizer hands back a + // re-serialised copy either way, and adopting that copy for every vertex + // shader would put every one of them through a round trip they do not need + // - which is not free: it cost the create-indirect retrace 0.15 SSIM the + // first time this gate was missing (the original capture; the case is now + // minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). + effectiveSpirv = &splitArrayInputSpirv; + } + + // Adopt the rewritten module only when THIS stage actually had one of the + // blocks - the optimizer hands back a re-serialised copy either way, and taking + // that copy for a module it did not rewrite is not free (it cost the + // create-indirect retrace 0.15 SSIM when the array-input split first missed + // this gate; the case is now + // minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). The report has to be + // per stage, not cumulative: a fragment shader consuming the same block reports a + // name the vertex stage already reported, + // and its own rewrite must still be taken or the two stages stop matching. + Vector flattenedXfbSpirv; + if (!xfbCaptureBlockNames.empty()) { + // Reported into a local first, and published only if the module is really + // adopted. The caller unions the published set unconditionally (so that a + // cache HIT contributes its names too), so publishing a name for a rewrite + // that was declined would rename a capture the emitted ESSL never renamed. + std::set flattenedNames; + if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl( + *effectiveSpirv, xfbCaptureBlockNames, flattenedNames, flattenedXfbSpirv, + enableSpirvValidation) && + !flattenedXfbSpirv.empty() && !flattenedNames.empty()) { + effectiveSpirv = &flattenedXfbSpirv; + outFlattenedXfbBlockNames = Move(flattenedNames); + } + } + + // The producer-keyed interface-block rename, planned program-wide by the caller and + // applied to this stage: the blocks it CONSUMES are spelled after the previous stage + // present in the program and the ones it PRODUCES after itself, so a tessellation + // evaluation stage's two TCSOutputBlocks stop being one name and every other stage + // still agrees with it. See UniquifyIoBlockNamesPass for why Mali needs it. + // + // INSIDE THE MEMOIZED SEGMENT, and at exactly the position it was written in - after + // the XFB flatten, before the UBO precision strip. Both halves of that matter: + // * INSIDE, because it rewrites the MODULE and the emitted ESSL carries the result. + // Left outside, a second program sharing this stage's key would be served ESSL + // with the blocks un-renamed and the repair would silently stop working - the + // same trap SetAtomicCounterBlockBindings sets one screen down. + // * AT THIS POSITION, because moving a SPIR-V pass in a chain is a behavioural + // change, and this one arrived device-verified on Mali. Hoisting it above the + // cache probe instead would have needed no key material at all (the rename would + // already be in the module bytes the key hashes) and was rejected for that + // reason: it reorders the chain, and it would re-serialise the module on every + // build including the ones the memo is there to make free. + // Its two rename maps are therefore KEY MATERIAL - see the caller. + Vector uniquifiedIoBlockSpirv; + if (!inputBlockRenames.empty() || !outputBlockRenames.empty()) { + std::set stageRenamedIoBlockNames; + if (MG_Util::ShaderTranspiler::ShaderCompiler::UniquifyIoBlockNamesForEssl( + *effectiveSpirv, inputBlockRenames, outputBlockRenames, + stageRenamedIoBlockNames, uniquifiedIoBlockSpirv, enableSpirvValidation) && + !uniquifiedIoBlockSpirv.empty() && !stageRenamedIoBlockNames.empty()) { + effectiveSpirv = &uniquifiedIoBlockSpirv; + MGLOG_D("Program %u stage %s: %zu inter-stage interface block(s) renamed per " + "producing stage, because some stage of this program declares the same " + "block name in both directions and the ES driver may alias the two.", + m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(), + stageRenamedIoBlockNames.size()); + } + } + + // ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints + // a RelaxedPrecision member as explicit "mediump" in the vertex stage and as + // UNQUALIFIED (mediump-by-default) in the fragment stage; after + // ForceSupporterOutput swaps the fragment header to highp, that member reads + // back as highp and the ES driver refuses to link ("definitions of uniform + // block ... do not match"). Strip the hint from block structs so both stages + // declare the member highp; nothing else about emission changes. + Vector uboPrecisionSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl( + *effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) && + !uboPrecisionSpirv.empty()) { + effectiveSpirv = &uboPrecisionSpirv; + } + + // noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective + // decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension + // GL_NV_shader_noperspective_interpolation : require`; a driver without that extension + // rejects the require. So on such devices emulate screen-linear interpolation instead + // (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop + // the decoration - exact, extension-free. Devices that have the extension keep the + // decoration and let the hardware do it natively. + Vector noperspectiveSpirv; + if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation && + MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl( + *effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) && + !noperspectiveSpirv.empty()) { + effectiveSpirv = &noperspectiveSpirv; + } + + // ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather + // than approximating one. The shared pass turns the type into the 2D one and + // divides the coordinate of every normalized-coordinate lookup by the texture + // size, which is the whole of the difference between the two. + Vector rectLoweredSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) && + !rectLoweredSpirv.empty()) { + effectiveSpirv = &rectLoweredSpirv; + } + + // ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height + // 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D + // as 2D for images without ever asking whether the type is arrayed, so a + // 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a + // two-component constructor, which every driver rejects, taking the whole + // program with it. The pass does the conversion properly - type to 2D array, + // coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own. + Vector arrayImageSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv, + arrayImageSpirv, enableSpirvValidation) && + !arrayImageSpirv.empty()) { + effectiveSpirv = &arrayImageSpirv; + } + + // The SAMPLER half of the same 1D story, and a defect one layer deeper than the one + // above. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - it just prints the + // OFFSET and the two GRADIENT operands with the arity the desktop shader spelled, so + // textureLodOffset(sampler1DArray, vec2, float, int) is emitted against a + // sampler2DArray and the driver answers "no matching overloaded function found", + // losing the stage and silently no-oping every dispatch that used it. Widening the + // operands alone would be an INVALID module (the validator derives the required arity + // from the image's own Dim), so the pass moves the type to 2D and widens coordinate, + // offset and gradients together. + // + // NO KEY MATERIAL, by the same test LegalizeResourceArrayIndexingForEssl passes: + // it takes the module and nothing else, no capability bit arms it, and it self-gates + // on the module's own content (BinaryHasOffsetOrGrad1DSampledImage). The module is + // already the largest thing in the L2 key, so it is covered completely. + Vector sampled1DSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DSampledImagesForEssl( + *effectiveSpirv, sampled1DSpirv, enableSpirvValidation) && + !sampled1DSpirv.empty()) { + effectiveSpirv = &sampled1DSpirv; + } + + // GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop + // GLSL 4.2 and an Adreno ES compile error ("all images have to define layout + // format"), which loses the whole program. Give each such image the format the + // application bound to its unit - the one GL's format-class rules make correct - + // so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which + // also rewrites image types, so this one is looking at the final shapes. + // + // Gated on the module actually declaring one: the map is empty for every program + // whose images all declare formats, and the cheap probe keeps a program that has + // an unbound format-less image from paying an optimizer round trip per stage. + Vector imageFormatSpirv; + if (!imageFormatBake.glFormatByUniformName.empty() && + MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) && + MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl( + *effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv, + enableSpirvValidation) && + !imageFormatSpirv.empty()) { + effectiveSpirv = &imageFormatSpirv; + } + + // GL has forty image formats and GLSL ES core has thirteen; the other twenty-seven + // reach ES only through GL_NV_image_formats, which no tested driver advertises. A + // shader declaring one of them has NO legal ESSL spelling at all - SPIRV-Cross throws + // for some of them and the driver rejects the token for the rest ("'rg32f' : not a + // legal layout qualifier id"), and dropping the qualifier is refused too ("all images + // have to define layout format") - so the stage is lost and every draw with the + // program silently renders nothing. Seventeen of them widen EXACTLY into a core format + // of the same per-channel width, and this rewrites those declarations to the carrier + // and masks every access back to the channels GL says the format has. The other nine + // have no exact carrier and keep the honest diagnostic + // CollectImageFormatBakeInputs emits. + // + // A driver that HAS GL_NV_image_formats still needs part of this. SPIRV-Cross throws + // for its is_desktop_only_format set when it targets ESSL rather than printing a + // token, and the throw loses the stage however willing the driver was - llvmpipe + // advertises the extension and `layout(r8ui) uimage2D` lost its whole program there + // until the widening ran for it too. So the driver bit decides HOW MUCH is widened, + // never WHETHER. + // + // AFTER the bake above, deliberately: a format-less image whose unit holds a non-core + // format is baked with that format and widened here, so both routes end in the same + // place and there is no second widening rule for baked declarations. + // + // KEY MATERIAL: g_GLESCapabilities.SupportsExtendedImageFormats, which selects the + // mode - see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes + // no other input: what it rewrites is a pure function of the module's own declared + // formats and that mode, and the module is already the largest thing in the L2 key. + // The ARMING flag is deliberately NOT key material: it only decides whether the pass + // runs, and the module below is adopted only when the pass actually changed the bytes + // - so a program-wide flag that over-arms a stage costs an optimizer round trip and + // changes no output. + // + // DirectVulkan is deliberately not given this: it takes the declared format natively + // and resolves the descriptor's view format from the same bind state. + Vector widenedImageFormatSpirv; + if (imageFormatBake.declaresWidenableImageFormat && + MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl( + *effectiveSpirv, widenedImageFormatSpirv, widenOnlyUnprintableImageFormats, + enableSpirvValidation) && + !widenedImageFormatSpirv.empty() && widenedImageFormatSpirv != *effectiveSpirv) { + effectiveSpirv = &widenedImageFormatSpirv; + } + + // GLSL ES demands a constant integral expression to index a fragment output + // array; SPIR-V does not, so a shader that writes coeff[i] from a loop + // reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects + // outright ("array indexes for fragment outputs must be constant integral + // expressions"), linking no program and silently no-oping every draw that + // uses it. llvmpipe accepts it, ANGLE does not - which is the whole of the + // improved-transparency-minecraft-26.3 failure. Fold or lower the index here, + // on the ESSL path only: the same module is legal for DirectVulkan. + Vector outputIndexSpirv; + if (glShaderType == GL_FRAGMENT_SHADER && + MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl( + *effectiveSpirv, outputIndexSpirv, enableSpirvValidation) && + !outputIndexSpirv.empty()) { + effectiveSpirv = &outputIndexSpirv; + } + + // Same rule, two more resources, every stage: desktop GL lets an array of + // storage blocks and an array of image uniforms be indexed with any + // dynamically-uniform expression, GLSL ES keeps the ES 3.1 + // constant-expression rule for both, and the drivers enforce it - Qualcomm + // ("indexing into an SSBO array using a non-constant expression is not + // permitted"), llvmpipe ("image arrays indexed with non-constant expressions are + // forbidden in GLSL ES") - losing the stage, the program, and every draw or + // dispatch that used it, while the frontend keeps reporting the link glslang + // performed. Fold or lower the index here, on the ESSL path only: the same + // module is legal for DirectVulkan, which binds the array as one descriptor + // array. + // + // The image half is also what makes RemapImageArrayElementUnits below possible + // at all: that pass rewrites `g_image[k]` into a per-element declaration, and it + // can only do that once every k the emitted ESSL spells is a literal. + // + // NO KEY MATERIAL, and that is a conclusion rather than an omission: this takes the + // module and nothing else - no capability bit arms it, no per-program plan steers + // it - and it self-gates on the module's own content + // (BinaryHasDynamicResourceArrayIndexing). The module is already the largest + // thing in the L2 key, so it is fully covered. Contrast LowerViewportIndexForEssl, + // whose signature is equally module-only but which SupportsViewportArray ARMS - + // that bit is in the key precisely because of it. + Vector blockArrayIndexSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeResourceArrayIndexingForEssl( + *effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) && + !blockArrayIndexSpirv.empty()) { + effectiveSpirv = &blockArrayIndexSpirv; + } + + // glslang kept the application's layout(offset = N) on the atomic counters it + // lowered onto gl_AtomicCounterBlock_, and no std140/std430 layout can put + // member 0 anywhere but offset 0 - so SPIRV-Cross throws ("cannot be expressed as + // neither std430 nor std140") and the stage never reaches the driver. Collapse the + // block into one uint array at offset 0 and re-index each counter to the element + // that used to be at its byte offset; the buffer then stays bound whole, which it + // has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 64 on Adreno 830 and 32 or + // more everywhere else, so an 8-byte bind offset is not expressible on any of them). + // BEFORE SetAtomicCounterBlockBindings + // below, which only moves the block's BINDING and needs the block intact. + // + // NO KEY MATERIAL either, for the same reason - and note where the application's + // layout(offset = N) values live: glslang already baked them into the module as + // member Offset decorations, so they are in the key as MODULE BYTES. There is no + // separate offset input to carry. + Vector atomicCounterSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl( + *effectiveSpirv, atomicCounterSpirv, enableSpirvValidation) && + !atomicCounterSpirv.empty()) { + effectiveSpirv = &atomicCounterSpirv; + } + + // The second half of the inter-stage interface-block repair, and the one that + // actually closes the 420pack group: this driver drops the payload of a block that + // carries an explicit layout(location=) whenever a tessellation or geometry stage + // is in the pipeline, so the qualifier comes off and ES matches the block by name + // and member sequence instead. The names those two sides agree on are the ones the + // rename above just fixed, which is why this runs AFTER it and not before. + // + // The caller arms the two directions; both are false unless the driver POST + // measured the defect AND this program has a stage that can hit it. Adopted only + // when this stage really had a located block, for the reason the array-input split + // documents: the optimizer hands back a re-serialised copy either way. + // + // LAST IN THE CHAIN, and that position is load-bearing. Vulkan SPIR-V REQUIRES a + // Location on every user-defined Input/Output variable + // ([VUID-StandaloneSpirv-Location-04915]), so the module this produces is + // deliberately no longer valid Vulkan SPIR-V - it is an ESSL-emission intermediate + // that goes straight into SPIRV-Cross and reaches no driver as SPIR-V. Running it + // here means no later pass validates what it produced; the pass itself skips + // validation for the same reason (see StripIoBlockLocationsForEssl). Anywhere + // earlier and every remaining pass would latch a validation failure on a module + // that is doing exactly what it was asked to. + Vector strippedIoBlockLocationSpirv; + if (stripInputBlockLocations || stripOutputBlockLocations) { + Bool strippedAny = false; + if (MG_Util::ShaderTranspiler::ShaderCompiler::StripIoBlockLocationsForEssl( + *effectiveSpirv, stripInputBlockLocations, stripOutputBlockLocations, + strippedAny, strippedIoBlockLocationSpirv, enableSpirvValidation) && + !strippedIoBlockLocationSpirv.empty() && strippedAny) { + effectiveSpirv = &strippedIoBlockLocationSpirv; + // THE ARMING SIGNAL, and it is INFO on purpose: the per-stage line below is + // MGLOG_D, which is compiled out of every build CI and the device runs, so + // nothing outside a debug build could tell an armed repair from a silently + // un-armed one. Latched, so it costs one line per process rather than one + // per stage of every program. The integration lane that pins the emulation + // on asserts on exactly this line - see UnlocatedIoBlockScenario. + MGLOG_I_ONCE("DirectGLES is emitting inter-stage interface blocks WITHOUT their " + "layout(location) qualifier, because this driver loses a located " + "block's payload across a tessellation or geometry boundary."); + MGLOG_D("Program %u stage %s: interface-block location qualifiers dropped " + "(%s), because this driver loses a located block's payload across a " + "tessellation or geometry boundary.", + m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(), + stripInputBlockLocations + ? (stripOutputBlockLocations ? "consumed and produced" : "consumed") + : "produced"); + } + } + + MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, + MG_Util::ShaderTranspiler::SessionUsageBit::Transpile); + + spvc_compiler_options options; + spvcSession.CreateOptions(&options); + + spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, + ResolveBackendEsslVersion()); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); + + spvcSession.SetOptions(options); + + // ES fixes a storage block's binding at link from its layout(binding=) qualifier + // and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding + // can only be honoured by printing it INTO the qualifier. Rewriting the Binding + // decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding + // then deliberately preserves the qualifier for `buffer` declarations. + if (!storageBlockBindingOverrides.empty()) { // empty for almost every program + 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. + // + // BOTH HALVES ARE MEMO STATE. `atomicCounterEsslBindingTop` decides the binding + // this prints into the ESSL, so it is in the L2 key; `outAtomicCounterGlBindings` + // is an OUTPUT this stage produces and the draw path consumes, so it is in the L2 + // payload. A hit that replayed only the text would leave the bindings empty and + // every counter buffer unbound - the same class of silent loss the flattened XFB + // block names would have been. + spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop, + outAtomicCounterGlBindings); + + // `layout(index = 0)` is the GL default spelled out loud, and GLSL ES has no such + // qualifier in core - a stage that prints it is refused with "index layout + // qualifier requires EXT_blend_func_extended" and the whole program then draws + // nothing. Drop the decoration when it carries the default; a REAL dual-source + // index (1) is left alone, because that one genuinely needs the extension and the + // driver has to see it. Fragment stage only: no other stage can carry it. + if (glShaderType == GL_FRAGMENT_SHADER) { + spvcSession.DropDefaultFragmentOutputColorIndex(); + } + + // `readonly writeonly` together says the buffer variable can only be asked its + // .length(), which the frontend has already enforced - so the pair is inert, and + // printing it is not. llvmpipe's ES compiler refuses a block spelled that way and the + // stage never reaches the program. + spvcSession.RelaxReadWriteExclusiveStorageBuffers(); + + // gl_ShadingRateEXT / gl_PrimitiveShadingRateEXT: SPIRV-Cross spells them only for + // Vulkan GLSL, so they go through under a sentinel name and are restored below. Only + // a program whose stages use them gets here with anything to do, and only a driver + // with GL_EXT_fragment_shading_rate gets such a program past the frontend. + const Bool restoreShadingRateBuiltins = spvcSession.PrepareShadingRateBuiltinsForEssl(); + + const char* result = nullptr; + spvcSession.Compile(&result); + + if (!result) { + // The caller owns the diagnostic: it is the one that knows the + // frontend program id, and a failed transpile must NOT be memoized - + // the message names the stage and is worth re-emitting every time. + const char* lastError = spvcSession.GetLastErrorString(); + outError = lastError ? lastError : ""; + return false; + } + + outSource = result; + if (restoreShadingRateBuiltins) { + MG_Util::ShaderTranspiler::SpvcSession::FinishShadingRateBuiltinsForEssl(outSource); + } + return true; + } + + // GL 4.6 core 11.2.2 lets a program have a tessellation EVALUATION shader and no CONTROL + // shader: the input patch is passed through unmodified and the levels come from the + // PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. OpenGL ES 3.2 has no such + // state and no such allowance - it rejects the program at link, and with an EMPTY info + // log, which was verified on an Adreno 830 with no MobileGL in the process (TES-only: + // link=0, log empty; the same shaders plus any TCS: link=1, with or without the SSBO and + // atomic counter the failing conformance case also declares). The frontend's own glslang + // link succeeds, so GL_LINK_STATUS reads TRUE, program 0 is bound in its place, and every + // draw silently renders nothing - a black framebuffer, an atomic counter still at 0 and + // an untouched SSBO, with no error anywhere. + // + // So the missing stage is synthesized and attached here, alongside the program's own. + // DirectVulkan already does exactly this for the same structural reason + // (ProgramFactory::BuildPassthroughTessControlSource), so this completes the pair rather + // than inventing an approach. + // + // Nothing that works today can be harmed by it: it fires ONLY for a program that has an + // evaluation stage and no control stage, and every such program fails to link on ES right + // now. The worst case is that the synthesized stage fails to compile or link, which leaves + // the program exactly as dead as it already was - but with a driver log that says why, + // where today there is an empty one. + void BackendProgramObjectImpl::AttachPassthroughTessControlStage( + const ProgramBuildSource& src, const Int tessEvalShaderIndex, + const Vector>& shaderSpirvs, const String& vertexStageEssl, + const String& tessEvalStageEssl) { + // PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output + // patch size - so a program built for one value is stale for another. Recorded here + // and compared on the draw path (SyncCurrentProgram), the same shape as the + // storage-block and image-format signatures next to it. + const Uint patchVertices = MG_Pipe::gPipeInputs->IsLive() + ? MG_Pipe::gPipeInputs->GetPatchVertices() + : 3u; + m_passthroughTessControlPatchVertices = static_cast(patchVertices); + // PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked + // into the same stage (ES has no such state and no entry point to forward them to), so + // they are recorded and compared alongside the patch size - the two move together, as + // BuildPassthroughTessControlEssl's contract says. + m_passthroughTessControlOuterLevel = MG_Pipe::gPipeInputs->IsLive() + ? MG_Pipe::gPipeInputs->GetPatchDefaultOuterLevel() + : FloatVec4(1.0f, 1.0f, 1.0f, 1.0f); + m_passthroughTessControlInnerLevel = MG_Pipe::gPipeInputs->IsLive() + ? MG_Pipe::gPipeInputs->GetPatchDefaultInnerLevel() + : FloatVec2(1.0f, 1.0f); + + if (tessEvalShaderIndex < 0 || + static_cast(tessEvalShaderIndex) >= shaderSpirvs.size()) { + MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no " + "SPIR-V for it; the pass-through control stage GL describes cannot be checked, so " + "the program is left to fail its ES link.", + src.GetExternalIndex()); + m_backendProgramUsable = false; + return; + } + + // The one shape the pass-through cannot stand in for. It forwards gl_Position and + // nothing else, so an evaluation stage that reads a user-defined varying or a + // per-patch input - both of which carry a Location, where every built-in it needs + // does not - would start reading undefined values the moment a control stage sat + // between it and the vertex stage. Declining keeps that from being silent; it is the + // identical rule DirectVulkan applies in ReflectPassthroughTessControlNeed. + if (MG_Util::ShaderTranspiler::ShaderCompiler::ModuleReadsLocatedInput( + shaderSpirvs[static_cast(tessEvalShaderIndex)])) { + MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a " + "user-defined input through it; a synthesized pass-through control stage cannot " + "forward that, so the program is declined rather than fed an undefined varying.", + src.GetExternalIndex()); + m_backendProgramUsable = false; + return; + } + + // Mirrored from the neighbours rather than fixed: whether SPIRV-Cross redeclares + // gl_PerVertex, and with which members, depends on what the application's shaders + // touched, and a synthesized stage that redeclares a DIFFERENT shape than the stage + // it feeds is an ES link error against a program with no other problem. gl_in copies + // the vertex stage's OUT block (that is what arrives) and gl_out the evaluation + // stage's IN block (that is what is expected). A neighbour that redeclared nothing + // yields an empty list, which leaves the driver's own built-in declaration in place - + // which is exactly what matching it requires. + const String inMembers = + ExtractPerVertexBlockMembers(vertexStageEssl, /*input=*/false).value_or(String()); + const String outMembers = + ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String()); + + String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, + inMembers, outMembers, + m_passthroughTessControlOuterLevel, + m_passthroughTessControlInnerLevel); + // The mirrored member lists can carry gl_PointSize - the neighbour stage declared it, + // so matching it is the whole point - and a redeclaration is exactly as illegal as a + // reference in ESSL without the extension. Same directive, same never-speculative + // rule as the per-stage loop; a driver with neither spelling gets nothing added and + // fails below with its own message, which is the honest outcome for a shape it + // cannot express. + const char* passthroughPointSizeExtension = + source.find("gl_PointSize") != String::npos + ? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true) + : nullptr; + source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension); + // Same for gl_ClipDistance/gl_CullDistance mirrored out of a vertex stage that + // writes them: the neighbour could only compile with the extension, so it exists. + source = RequestClipCullDistanceExtension(Move(source)); + + const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER); + if (backendShaderId == 0) { + MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for " + "program %u.", + src.GetExternalIndex()); + m_backendProgramUsable = false; + return; + } + + const char* sourceCStr = source.c_str(); + MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices " + "%u):\n%s", + src.GetExternalIndex(), patchVertices, sourceCStr); + g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr); + g_GLESFuncs.glCompileShader(backendShaderId); + + // GL_FALSE, not GL_TRUE, for the reason the per-stage loop states: an unwritten + // out-param must read as "compile failed" and never as a silent success. + GLint compileStatus = GL_FALSE; + g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus); + if (compileStatus == GL_FALSE) { + GLint logLength = 0; + g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength); + if (logLength < 0) logLength = 0; + Vector log(static_cast(logLength) + 1, '\0'); + g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data()); + log.back() = '\0'; + MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for " + "program %u. Driver log: %s\nSource:\n%s", + src.GetExternalIndex(), log.data(), sourceCStr); + m_backendProgramUsable = false; + g_GLESFuncs.glDeleteShader(backendShaderId); + return; + } + + g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId); + // Same ownership handover as every other stage: glDeleteShader only FLAGS, so this is + // what makes the program own it and what keeps a relink from leaking it. + g_GLESFuncs.glDeleteShader(backendShaderId); + } + + // ---- THE PROGRAM BUILD, AND THE ONE SOURCE IT READS (P5e pg) ---------------------- + // + // ONE BODY, TWO HEADS, AND THE PULL BUILD'S HEAD IS THE ORIGINAL ONE. In the pull build + // `ProgramBuildSource` IS `MG_State::GLState::ProgramObject`, this function keeps its + // name, its signature and its null guard, and `src` is a reference to the object it was + // already dereferencing on every line - so the pull build compiles the same reads + // through the same accessors and G1's byte identity survives a rename and nothing else. + // + // In a PUSH build the body is a private worker that the two public overloads feed: + // SyncToBackend(program) - the monolith-glue half, ruling 1 - wraps the frontend's own + // archive, and SyncToBackendByHandle(cso) wraps the RECORD's. That is the whole of this + // family's rule F: after this, nothing in a program build dereferences a frontend + // object, and the forty accessor calls below read memory the server owns. + void BackendProgramObjectImpl::SyncToBackendFromSource(const ProgramBuildSource& src) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // Recorded before either early return below, so Use() can always name the GL + // program a no-op draw belongs to - including the "linked but not drawable" exit. + m_frontendProgramId = src.GetExternalIndex(); + + // GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was + // cancelled (teardown) or whose optimizer run failed is fully linked and fully + // queryable, but has no SPIR-V to build a driver program out of. GL cannot retract + // a LINK_STATUS it already reported true, so "linked but not drawable" is the + // answer, and this is where the ES backend expresses it. + if (!src.GetLinkStatus() || !src.GetSpirvStatus()) { + MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State " + "program ID: %u", + src.GetExternalIndex()); + return; + } + + MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u", + src.GetExternalIndex(), m_backendProgramId); + // Every link-derived cache below (incl. m_samplerUniformBindings and its + // lastAssignedUnit/lastAssignedLodBias program-state mirrors) is rebuilt; + // the sampler-pass memo keyed on them must not survive. + m_samplerPassMemo.valid = false; + m_backendProgramUsable = true; + m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask; + m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask; + m_fragColorBroadcastCount = g_fragColorBroadcastCount; + // The generated ESSL bakes these in (see the SetShaderStorageBlockBinding call in the + // transpile loop below), so the set they were generated against is part of what makes + // this build current - the draw path compares the signature and rebuilds on a change. + const auto& storageBlockBindingOverrides = src.GetShaderStorageBlockBindingOverrides(); + m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(src); + // 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(); + // Re-established by AttachPassthroughTessControlStage below when this program needs + // one; cleared first so a program that stops needing one (a relink that now attaches + // a real control stage) does not keep comparing against a stale patch size. The + // default levels are re-established from the same call and gated on the same -1. + m_passthroughTessControlPatchVertices = -1; + m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f); + m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f); + // 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). + // Taken BEFORE the transpile loop so both the bake and the key see one snapshot. + const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(src); + m_formatlessImageUnits = imageFormatBake.units; + m_imageUnitFormatSignature = imageFormatBake.signature; + for (const auto& conflicted : imageFormatBake.conflictedNames) { + MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with " + "different bound formats; left format-less.", + conflicted.c_str(), src.GetExternalIndex()); + } + // ...and once more for image ARRAYS whose per-element units are not consecutive, which + // ESSL has no way to express in one declaration. Program-wide, like the bake, and read + // from the same snapshot of the reflection; the per-stage rewrite happens below. + const Vector nonConsecutiveImageArrays = + CollectNonConsecutiveImageArrayPlans(src); + + // Detach all existing shaders + GLint attachedCount = 0; + g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_ATTACHED_SHADERS, &attachedCount); + MGLOG_D("Currently attached shaders count: %d", attachedCount); + + if (attachedCount > 0) { + Vector attachedShaders(attachedCount); + // Every GL out-param in this function is pre-initialized and every count is + // re-clamped after the query. A driver that returns without writing the + // out-param (no current context, a lost context, a stubbed entry point) would + // otherwise leak an uninitialized stack value straight into a container size + // or a loop bound - which is exactly how this path used to throw + // length_error out of a Vector fill-ctor. + GLsizei actualCount = 0; + g_GLESFuncs.glGetAttachedShaders(m_backendProgramId, attachedCount, &actualCount, + attachedShaders.data()); + actualCount = std::clamp(actualCount, 0, static_cast(attachedShaders.size())); + MGLOG_D("Detaching %d existing shaders from program %u", actualCount, m_backendProgramId); + + for (GLsizei i = 0; i < actualCount; ++i) { + MGLOG_D("Detaching shader ID: %u from program %u", attachedShaders[i], m_backendProgramId); + g_GLESFuncs.glDetachShader(m_backendProgramId, attachedShaders[i]); + } + } + + // Attach current shaders. + // + // The EXECUTABLE's stage list, not GetAttachedShaders(): this loop indexes + // shaderSpirvs by the same running index, and the generated SPIR-V is a link + // artifact while the attach list is live. GL 4.6 core 7.3 makes glAttachShader take + // effect only at the next link, so a program that is attached to after it linked has + // MORE entries in the attach list than there are modules - and pairing the two read + // straight off the end of shaderSpirvs (a std::vector copy from garbage, which is + // how this crashed). GetLinkedShaderStages() is the list the modules were generated + // from, one entry per module, in module order. + const Vector linkedStages = src.GetLinkedShaderStages(); + auto& shaderSpirvs = src.GetGeneratedSpirv(); + // Both come from the same Link(), so they agree by construction. If they ever did + // not there would be no index this function could safely use for EITHER array, so + // this refuses the build instead of picking one and hoping. + if (linkedStages.size() != shaderSpirvs.size()) { + MGLOG_E_ONCE("Program %u: %zu linked stage(s) but %zu generated SPIR-V module(s); refusing to " + "build a backend program from mismatched link artifacts.", + src.GetExternalIndex(), linkedStages.size(), shaderSpirvs.size()); + m_backendProgramUsable = false; + return; + } + if (src.PointSizeDemoted()) { + // THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that + // pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because + // every rendering assertion stays green on a healthy driver whether the + // demotion ran or was silently disarmed. See PointSizeDemotionScenario. + MGLOG_I_ONCE("DirectGLES is building programs whose tessellation/geometry gl_PointSize was " + "demoted to an ordinary varying, because this driver cannot host the built-in " + "in those stages."); + } + MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId); + for (const auto& ref : src.GetLinkedShaderSnapshot()) { + if (!ref.shader) continue; + const auto& stage = + MG_Util::ConvertGLEnumToString(MG_Util::ConvertShaderStageToGLEnum(ref.shader->GetShaderStage())); + // The source THIS link consumed, which a later glShaderSource does not disturb. + const String& src = ref.source ? *ref.source : ref.shader->GetShaderSource(); + MGLOG_D("Original src @ %s: \n", stage.c_str()); + MGLOG_D("%s:", src.empty() ? "" : src.c_str()); + } + const Bool enableSpirvValidation = src.GetSpirvValidationEnabled(); + + // Blocks a transform-feedback capture request names a member of ("StageData" of + // "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and + // then captures nothing at all for it, so those blocks - and ONLY those - get + // flattened into per-member variables below, in EVERY stage, so a producer and its + // consumer keep matching. gl_PerVertex members ("gl_Position") carry no block + // prefix and so never enter this set. + std::set xfbCaptureBlockNames; + for (const auto& xfbVarying : src.GetTransformFeedbackVaryings()) { + const SizeT dot = xfbVarying.name.find('.'); + if (dot != String::npos && dot > 0) { + xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot)); + } + } + std::set flattenedXfbBlockNames; + // Stages whose ESSL had a read+write image declaration doubled into a coherent + // read/write pair, and by how many. Empty for every program but a handful; consulted + // ONLY when the link then fails, because the doubling spends the driver's per-stage + // GL_MAX_*_IMAGE_UNIFORMS budget that MobileGL keeps advertising unadjusted (halving + // the advertised value would fail basic-api and NotSupported-out cases that never pay + // the doubling, so the limit must stay honest and the connection has to be made here + // instead). See the budget note on SplitReadWriteImageUniforms. + struct SplitImageUniformStage { + ShaderStage stage; + Uint splitCount; + }; + Vector splitImageUniformStages; + + // Desktop GLSL keeps SEPARATE name namespaces for input and output interface + // blocks, so ONE stage may legally declare `in FOO {...}` and `out FOO {...}` at + // the same time - which the tessellation evaluation stage of both interface-block + // tests in KHR-GL42/43.shading_language_420pack does ("in TCSOutputBlock ... out + // TCSOutputBlock"). SPIRV-Cross keeps the same split (block_input_names vs + // block_output_names) and re-emits BOTH under the name FOO, so the generated ESSL + // declares two different blocks called FOO in one shader. Adreno's ES compiler + // keeps them apart; Mali's does not - the stage compiles, the program links, and + // the output block's payload never reaches the next stage. All 22 of that group's + // Mali failures are exactly the two tests that write this shape, and every one of + // them passes on Adreno and on DirectVulkan. + // + // The repair is a rename keyed on the PRODUCING stage, planned here and applied + // per stage below so a producer and its consumer keep naming the same block. + // Gated twice over, because a re-serialised module is not free (it cost the + // create-indirect retrace 0.15 SSIM the first time the array-input split missed + // its gate; the case is now + // minecraft-1.21.1-neoforge-create-indirect-in-world-align1024): only a tessellation + // or geometry stage can declare blocks in both directions at all, and even then the + // probe has to FIND a collision before any + // stage is rewritten. + std::set collidingIoBlockNames; + std::set declaredIoBlockNames; + Vector stagePipelineIndices(linkedStages.size(), -1); + Bool anyStageCanDeclareBlocksInBothDirections = false; + for (SizeT index = 0; index < linkedStages.size(); ++index) { + const ShaderStage stage = linkedStages[index]; + stagePipelineIndices[index] = InterStagePipelineIndex(stage); + if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true; + } + // A SECOND, INDEPENDENT interface-block repair riding the same gate, because it + // needs the same question answered: "does this program have a stage where an + // inter-stage block can go wrong?". CanDeclareBlocksInBothDirections is true for + // exactly the tessellation and geometry stages, which is also exactly the set of + // stages whose presence makes this driver drop a LOCATED block's payload (a + // vertex-to-fragment located block is fine on the same driver, measured). The two + // repairs are otherwise unrelated: the rename fixes a name collision inside ONE + // stage, this drops a qualifier from EVERY block of the program - so it does not + // wait for the collision probe to find anything. + const Bool ioBlockLocationStripArmed = + !g_GLESCapabilities.SupportsLocatedInterStageIoBlocks && + anyStageCanDeclareBlocksInBothDirections; + if (anyStageCanDeclareBlocksInBothDirections) { + for (SizeT index = 0; index < shaderSpirvs.size(); ++index) { + MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl( + shaderSpirvs[index], collidingIoBlockNames, declaredIoBlockNames); + } + // A block a capture request names is resolved BY NAME at + // glTransformFeedbackVaryings time - and flattened away entirely by the pass + // below - so renaming one would ask the driver for a block the request does + // not spell. + for (const auto& xfbCaptureBlockName : xfbCaptureBlockNames) { + collidingIoBlockNames.erase(xfbCaptureBlockName); + } + } + // The one spelling every stage of THIS program agrees on for `blockName` as written + // by pipeline stage `producerPipelineIndex`. "__" is reserved in GLSL, so a name + // already ending in '_' does not get another one, and the digit-suffix loop steps + // off any name the program already spells. + const auto uniqueIoBlockName = [&declaredIoBlockNames](const String& blockName, + Int producerPipelineIndex) { + const char* separator = (!blockName.empty() && blockName.back() == '_') ? "" : "_"; + String candidate = blockName + separator + "mgio" + std::to_string(producerPipelineIndex); + while (declaredIoBlockNames.find(candidate) != declaredIoBlockNames.end()) { + candidate += "0"; + } + return candidate; + }; + + // Desktop GL makes the tessellation CONTROL stage optional; OpenGL ES 3.2 rejects a + // program that has an evaluation stage without one, with an empty info log. When that + // is this program's shape, one is synthesized below - and it has to be spelled to + // MATCH the two stages it sits between, so their emitted ESSL is kept here as it is + // produced. Empty for every program that has a control stage of its own, which is + // all but a handful. + Bool hasTessEvalStage = false; + Bool hasTessControlStage = false; + Int tessEvalShaderIndex = -1; + String vertexStageEssl; + String tessEvalStageEssl; + // + // Asked of the executable for the same reason the loop below indexes it: a + // tessellation evaluation shader merely ATTACHED to a linked vertex+fragment program + // is not part of what this program runs, and synthesizing a control stage for it + // would build a tessellating driver program for an executable that does not + // tessellate (and would take tessEvalShaderIndex past the end of shaderSpirvs). + for (SizeT index = 0; index < linkedStages.size(); ++index) { + const ShaderStage stage = linkedStages[index]; + if (stage == ShaderStage::TessControl) hasTessControlStage = true; + if (stage == ShaderStage::TessEval) { + hasTessEvalStage = true; + tessEvalShaderIndex = static_cast(index); + } + } + const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage; + + // The stage order the loop below walks, with every FRAGMENT stage moved to the end. + // The viewport-routing gate is the reason: whether a fragment stage needs one is a + // question about the OTHER stages ("does any of them still write gl_ViewportIndex?"), + // and the honest, free answer to it is the promotion the producing stage's own text + // pass just performed. Answering it any other way costs a BuildModule per + // pre-rasterization stage of every program - the parse the shared SpirvGateFeatures + // probe exists to avoid. Nothing else in the loop is order-sensitive: the two + // passthrough-tessellation sources it captures are a vertex and an evaluation stage, + // and the three sets it accumulates are unions. + // + // The pre-rasterization stages are walked in PIPELINE order (the stage enum's), so a + // consumer's input-block redeclaration below can mirror the output block its producer + // has already emitted. + Vector stageOrder; + stageOrder.reserve(linkedStages.size()); + for (SizeT index = 0; index < linkedStages.size(); ++index) { + if (linkedStages[index] != ShaderStage::Fragment) stageOrder.push_back(index); + } + std::stable_sort(stageOrder.begin(), stageOrder.end(), [&linkedStages](SizeT a, SizeT b) { + return static_cast(linkedStages[a]) < static_cast(linkedStages[b]); + }); + for (SizeT index = 0; index < linkedStages.size(); ++index) { + if (linkedStages[index] == ShaderStage::Fragment) stageOrder.push_back(index); + } + // The output gl_PerVertex members the last pre-rasterization stage emitted, for the + // next stage's input redeclaration (RedeclareClipCullDistanceInputsInPerVertexBlock). + // A synthesized pass-through control stage copies its input block through, so the + // vertex stage's members are what an evaluation stage behind one sees. + String producerOutPerVertexMembers; + // Set by whichever pre-rasterization stage's demoted mg_ViewportIndex global the text + // pass turned into a varying; read by the fragment stage to decide whether to inject + // the gate that consumes it. + Bool programRoutesViewportIndex = false; + // No fragment stage, no gate - and without a gate the promotion below would only add + // an output nothing can read. That is not merely useless: in a separable program + // pipeline the fragment stage lives in a DIFFERENT program, which never saw this + // build and cannot be given a gate, so promoting there would hang an unmatched + // varying off a program to buy nothing. Both cases keep the pre-emulation behaviour, + // which is what a program with no fragment stage had anyway. + const Bool programHasFragmentStage = + std::find(linkedStages.begin(), linkedStages.end(), ShaderStage::Fragment) != + linkedStages.end(); + const Bool viewportEmulationForThisProgram = + ViewportArrayEmulationEnabled() && programHasFragmentStage; + + for (const SizeT index : stageOrder) { + GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(linkedStages[index]); + GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType); + + if (backendShaderId == 0) { + MGLOG_E_ONCE("Failed to create backend shader for attachment."); + continue; + } + String source; + auto& spirvCode = shaderSpirvs[index]; + + // A samplerBuffer is core in the OpenGL 3.1+ context MobileGL advertises but needs + // ES 3.2 or EXT/OES_texture_buffer on the host. Without it SPIRV-Cross emits + // `#extension GL_EXT_texture_buffer : require` and the driver rejects both that + // and the isamplerBuffer keyword - the program never links and every draw using it + // becomes a silent no-op. Say so here, naming the stage. Deliberately unlatched: + // this is bounded by program count, and which stage failed is the whole point. + // Gated on the capability so the module walk never runs on a healthy driver. + if (!AreBufferTexturesSupported() && + MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirvCode)) { + MGLOG_E("Program %u stage %s samples a buffer texture, which this ES driver " + "cannot provide (%s). The shader will not compile and the program will " + "not link; every draw using it is a no-op.", + m_backendProgramId, + MG_Util::ConvertGLEnumToString(glShaderType).c_str(), + GetBufferTextureTierName()); + m_backendProgramUsable = false; + g_GLESFuncs.glDeleteShader(backendShaderId); + continue; + } + + // ---- L2 of the shader translation memo ------------------------------- + // The whole DirectGLES SPIR-V pass chain plus SPIRV-Cross for this stage, + // memoized on the module bytes and on every capability bit and per-program + // input that steers them. See TranslationCache.h for the key inventory and + // for why the text-level passes below stay outside the boundary. + MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs; + esslKeyInputs.spirv = &spirvCode; + esslKeyInputs.shaderType = glShaderType; + // The EFFECTIVE arming, computed the same way TranspileSpirvToEssl computes it. + // Duplicated rather than shared because the two live on opposite sides of the + // memo boundary - and a key that disagrees with the pass it is keying is the one + // failure mode of this cache that renders wrong pixels instead of being slow. + esslKeyInputs.viewportIndexLoweringArmed = + (glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER || + glShaderType == GL_GEOMETRY_SHADER) && + (ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray); + esslKeyInputs.deferGeometryEmitsArmed = + glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit; + esslKeyInputs.subgroupEmulationArmed = + glShaderType == GL_COMPUTE_SHADER && + ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated; + esslKeyInputs.subgroupEmulationScratchBytes = + esslKeyInputs.subgroupEmulationArmed ? g_GLESCapabilities.MaxComputeSharedMemorySize : 0; + esslKeyInputs.iterationRPBarrierFixArmed = + esslKeyInputs.subgroupEmulationArmed && ShouldApplyIterationRPFixes(); + esslKeyInputs.subgroupScratchAtomicReads = + esslKeyInputs.subgroupEmulationArmed && g_GLESCapabilities.SharedMemoryLoadsGoStale; + esslKeyInputs.supportsNoperspectiveInterpolation = + g_GLESCapabilities.SupportsNoperspectiveInterpolation; + esslKeyInputs.supportsExtendedImageFormats = + g_GLESCapabilities.SupportsExtendedImageFormats; + esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples; + esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples; + esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples; + esslKeyInputs.advertisedMaxSamples = + std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); + esslKeyInputs.xfbCaptureBlockNames = &xfbCaptureBlockNames; + esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName; + esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides; + esslKeyInputs.esslVersion = ResolveBackendEsslVersion(); + esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop; + + // THIS STAGE's share of the program-wide interface-block rename plan built above + // the loop. Resolved here, outside the memoized segment, because it is planning + // and not translation - exactly like the image-format bake map - and because that + // makes the two maps a plain function argument the L2 key can carry. + // + // KEYING ON THE RESOLVED MAPS rather than on what they were derived from + // (collidingIoBlockNames, declaredIoBlockNames, stagePipelineIndices, this + // stage's index) is deliberate: the maps ARE the pass's arguments, so they are + // exactly as fine as the pass's behaviour and no finer. Two programs whose + // collision plans differ but whose maps for THIS stage come out identical really + // do produce the same ESSL and should share the entry. + // + // A block whose other end is NOT in this program is deliberately left out of the + // plan: in a separate-shader-objects pipeline the interface it matches across + // lives in another program that never saw this plan, and renaming one side of + // THAT would break a program pipeline to repair a driver quirk. That is what the + // producer/consumer presence tests below are for - in a monolithic program both + // are trivially satisfied for every interface the collision can touch. + std::map inputBlockRenames; + std::map outputBlockRenames; + if (!collidingIoBlockNames.empty() && stagePipelineIndices[index] >= 0) { + const Int myPipelineIndex = stagePipelineIndices[index]; + Int producerPipelineIndex = -1; + Bool hasConsumerStage = false; + for (const Int otherPipelineIndex : stagePipelineIndices) { + if (otherPipelineIndex < 0) continue; + if (otherPipelineIndex < myPipelineIndex && + otherPipelineIndex > producerPipelineIndex) { + producerPipelineIndex = otherPipelineIndex; + } + if (otherPipelineIndex > myPipelineIndex) hasConsumerStage = true; + } + for (const auto& collidingBlockName : collidingIoBlockNames) { + if (producerPipelineIndex >= 0) { + inputBlockRenames[collidingBlockName] = + uniqueIoBlockName(collidingBlockName, producerPipelineIndex); + } + if (hasConsumerStage) { + outputBlockRenames[collidingBlockName] = + uniqueIoBlockName(collidingBlockName, myPipelineIndex); + } + } + } + esslKeyInputs.inputBlockRenames = &inputBlockRenames; + esslKeyInputs.outputBlockRenames = &outputBlockRenames; + + // ...and THIS STAGE's share of the interface-block LOCATION strip, planned the + // same way and for the same reason. The gate has three parts, all of which have + // to hold before a single block loses its qualifier: + // * the driver POST measured the defect (never a renderer-string quirk list); + // * this program has a stage that can hit it - a located block between a + // vertex and a fragment stage works on the affected driver, so a program + // with neither tessellation nor geometry keeps its ESSL byte for byte; + // * for THIS stage and THIS direction, this program HAS a stage on that side + // of it. That is the same test the rename plan above makes, and the same + // approximation: it asks "is some stage of this program earlier/later than + // me", not "is the exact partner of every one of my blocks here". The two + // coincide for every program MobileGL builds, because a separable pipeline + // is flattened into one composite carrying every stage that has a shader + // (GLContext::GetProgramForDraw) and a program bound with glUseProgram has + // no partner program at all - so a stage set with a gap in it does not + // arise. Should one ever arise, this must become the nearest-stage + // resolution the rename plan computes, or the two ends of the gap would + // disagree about the qualifier. + // The direction tests deliberately mirror that plan rather than inventing a + // second rule for the same question. + Bool stripInputBlockLocations = false; + Bool stripOutputBlockLocations = false; + if (ioBlockLocationStripArmed && stagePipelineIndices[index] >= 0) { + const Int myPipelineIndex = stagePipelineIndices[index]; + for (const Int otherPipelineIndex : stagePipelineIndices) { + if (otherPipelineIndex < 0) continue; + if (otherPipelineIndex < myPipelineIndex) stripInputBlockLocations = true; + if (otherPipelineIndex > myPipelineIndex) stripOutputBlockLocations = true; + } + } + esslKeyInputs.stripInputBlockLocations = stripInputBlockLocations; + esslKeyInputs.stripOutputBlockLocations = stripOutputBlockLocations; + esslKeyInputs.enableSpirvValidation = enableSpirvValidation; + + auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache(); + MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey; + if (MG_Util::ShaderTranspiler::ShaderTranslationCacheEnabled()) { + esslCacheKey = MG_Util::ShaderTranspiler::BuildEsslTranslationKey(esslKeyInputs); + } + + std::set stageFlattenedXfbBlockNames; + // Per stage, and NOT m_atomicCounterGlBindings directly: on a miss the + // transpile appends to this, on a hit the payload supplies it, and only then + // is it folded into the program-wide vector. Pointing the transpile straight + // at the member would have made the miss path and the hit path disagree about + // who owns the append. + Vector stageAtomicCounterGlBindings; + const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit = + esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr; + if (esslHit) { + source = esslHit->essl; + stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames; + stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings; + } else { + String transpileError; + if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames, + imageFormatBake, storageBlockBindingOverrides, + inputBlockRenames, outputBlockRenames, + stripInputBlockLocations, stripOutputBlockLocations, + m_atomicCounterEsslBindingTop, + enableSpirvValidation, source, + stageFlattenedXfbBlockNames, + stageAtomicCounterGlBindings, transpileError)) { + // MGLOG_E, unlatched, like the compile- and link-failure diagnostics + // below: one line per failing stage is bounded by program count and + // naming the stage is the entire diagnostic value. A stage that never + // reaches the driver leaves the program short of that stage, so the link + // fails with an EMPTY driver info log - the least debuggable failure + // MobileGL can produce, and what hid the whole + // KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros". + MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, " + "SPIRV-Cross error: %s", + src.GetExternalIndex(), + MG_Util::ConvertGLEnumToString(glShaderType).c_str(), + transpileError.c_str()); + m_backendProgramUsable = false; + continue; + } + if (esslCacheKey.Valid()) { + auto payload = MakeShared(); + payload->essl = source; + payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames; + payload->atomicCounterGlBindings = stageAtomicCounterGlBindings; + const SizeT payloadBytes = + MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload); + esslCache.Insert( + esslCacheKey, + MG_Util::ShaderTranspiler::EsslTranslationResultPtr(Move(payload)), + payloadBytes); + } + } + // Per stage, never cumulative: a fragment shader consuming the same block + // reports a name the vertex stage already reported, and its own rewrite must + // still be taken or the two stages stop matching. Done here rather than inside + // the transpile so a cache HIT contributes its names too. + flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(), + stageFlattenedXfbBlockNames.end()); + // Same rule for the atomic-counter bindings this stage declared, and for the + // same reason: the loop below de-duplicates across stages, so a hit that + // contributed nothing would silently drop a counter buffer the draw path has + // to bind. + m_atomicCounterGlBindings.insert(m_atomicCounterGlBindings.end(), + stageAtomicCounterGlBindings.begin(), + stageAtomicCounterGlBindings.end()); + + // SPIRV-Cross sizes written clip/cull distances with a standalone + // `out float gl_ClipDistance[N];`, which an ES compiler may refuse as a reserved + // name and so lose the whole program. Redeclared as gl_PerVertex members instead. + // BEFORE the gl_PointSize extension probe below (the block can name it) and + // before vertexStageEssl is captured, so a synthesized pass-through control + // stage mirrors the block this produces. + source = RedeclareClipCullDistancesInPerVertexBlock(std::move(source), glShaderType); + // ...and the consumer half: a stage reading gl_in[i].gl_ClipDistance/CullDistance + // redeclares its input block with the sizes the producer wrote. + source = RedeclareClipCullDistanceInputsInPerVertexBlock(std::move(source), glShaderType, + producerOutPerVertexMembers); + if (glShaderType != GL_FRAGMENT_SHADER) { + producerOutPerVertexMembers = + ExtractPerVertexBlockMembers(source, /*input=*/false).value_or(String()); + } + + // Position in the chain is arbitrary: this is the only header-level rewrite, it + // edits #extension directives and never the body, and the replacement is the + // same length and stays an #extension line - so it commutes with every pass + // below, including ForceSupporterOutput's scan for the last directive. First, + // because a header concern reads better before the body ones. + source = RetargetTextureBufferExtension(std::move(source), + g_GLESCapabilities.TextureBufferSupport); + // The other header-level rewrite, and next to that one for the same reason. The + // formats it covers are both the ones the bake above put into the module and the + // ones the application declared itself - either can be outside the thirteen GLSL + // ES has in core, and neither reaches the driver without this directive. + source = RequestExtendedImageFormats(std::move(source), + imageFormatBake.needsExtendedImageFormats && + g_GLESCapabilities.SupportsExtendedImageFormats); + // The third header-level rewrite, for the builtin SPIRV-Cross prints bare: + // gl_ViewportIndex is in no version of ESSL core, so without this directive the + // stage does not compile and the whole program - not just its viewport routing - + // is lost. The token probe keeps the line off every other program and the + // capability gate keeps it off drivers that would hard-error on an unadvertised + // name; a driver without the extension took the LowerViewportIndexPass fallback + // above and its source no longer names the builtin at all, so the two are mutually + // exclusive by construction. Read `source` BEFORE it is moved from. + // The routing emulation is the third way this can be reached and the only one + // that needs no directive: it renames the fragment stage's read onto the varying + // the producing stage now writes, a few passes below. + const Bool needsViewportArrayExtension = + g_GLESCapabilities.SupportsViewportArray && + !(ViewportArrayEmulationEnabled() && programRoutesViewportIndex) && + source.find("gl_ViewportIndex") != String::npos; + source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension); + + // The fourth header-level rewrite, and the same shape as the third: ESSL has no + // gl_PointSize in a tessellation or geometry stage at ANY version - 320 makes the + // stages core and still leaves the built-in behind EXT/OES_..._point_size - while + // SPIRV-Cross prints it bare. Without the directive the stage fails to compile + // with "`gl_PointSize' undeclared", which takes the whole program to program 0: + // the draw renders nothing AND glBeginTransformFeedback is rejected, so a capture + // of anything at all off that program silently comes back empty. The token probe + // keeps the line off every other program and PointSizeExtensionName returns + // nullptr - i.e. nothing is emitted - on a driver advertising neither spelling. + if (source.find("gl_PointSize") != String::npos) { + const Bool tessellationStage = glShaderType == GL_TESS_CONTROL_SHADER || + glShaderType == GL_TESS_EVALUATION_SHADER; + if (tessellationStage || glShaderType == GL_GEOMETRY_SHADER) { + const auto tier = tessellationStage ? g_GLESCapabilities.TessellationPointSizeSupport + : g_GLESCapabilities.GeometryPointSizeSupport; + const char* pointSizeExtension = PointSizeExtensionName(tier, tessellationStage); + if (pointSizeExtension == nullptr) { + // Latched, and an ERROR rather than a warning: what follows is a + // driver compile failure whose text names a built-in the application + // never mis-spelled, and the reason is a missing driver capability + // rather than anything in the shader. Saying so here is the whole + // difference between a legible skip and an unexplained black draw. + MGLOG_E_ONCE("This driver advertises neither the EXT nor the OES %s_point_size " + "extension, so its ESSL has no gl_PointSize in a %s stage; program %u " + "will fail to compile. Point size from a non-vertex stage is not " + "available on this device.", + tessellationStage ? "tessellation" : "geometry", + tessellationStage ? "tessellation" : "geometry", + src.GetExternalIndex()); + } + source = RequestPointSizeExtension(std::move(source), pointSizeExtension); + } + } + + source = RebindImageUniformsToFrontendUnits(std::move(source), src); + // The completion half of the format bake, for the formats SPIRV-Cross throws on + // rather than prints (r8ui and the rest of its desktop-only set). Empty for every + // program whose format-less images bound a format the module could carry, which + // is the normal case - those were baked into the SPIR-V above and this pass finds + // their declarations already qualified. AFTER the rebind, so the layout qualifier + // it edits is the one that already exists; BEFORE the split and the binding + // strip, so both halves of a split image inherit the format. + source = BakeImageFormatQualifiers(std::move(source), + imageFormatBake.esslFormatQualifierByUniformName); + // An image ARRAY whose elements do not sit on consecutive units cannot be spelled + // by the single layout(binding=N) the rebind above stamped: ESSL gives element k + // the unit N+k and there is no glUniform1i to correct it with. Split the array + // into one scalar declaration per element, each with its own binding. AFTER the + // rebind and the format bake, both of which look the array up by its GL uniform + // name and need the binding already there; BEFORE the read+write split, so an + // element that is both read and written is split with its own binding on it. + if (!nonConsecutiveImageArrays.empty()) { + Vector declinedImageArrays; + source = RemapImageArrayElementUnits(source, nonConsecutiveImageArrays, + &declinedImageArrays); + for (const auto& declined : declinedImageArrays) { + // MGLOG_E, unlatched, like the transpile- and compile-failure diagnostics + // around it: this is the "linked, drew, produced wrong numbers, said + // nothing" shape that cost earlier waves whole days, and one line per + // declined array is bounded by program count. There is no honest GL answer + // to give instead - the frontend has already reported LINK_STATUS = true. + MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach " + "from one declaration, so this stage will read and write the WRONG units. State " + "program ID: %u, stage: %s.", + declined.c_str(), src.GetExternalIndex(), + MG_Util::ConvertGLEnumToString(glShaderType).c_str()); + } + } + // Wedged between those two on purpose: + // * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto + // both halves of a split image is already the frontend texture unit (and so + // that pass never has to reason about the alias it introduces); + // * BEFORE RemoveLayoutBinding, whose keepBindingRegex recognises an image + // declaration and preserves its binding - an image unit cannot be set from + // the API in ES, so the qualifier is the only binding mechanism there is, + // and both halves of the pair have to still be carrying theirs when it runs. + // Takes no stage: the qualifier it adds is a decision about THIS text's accesses + // and the rename that keeps two stages from declaring one image uniform + // differently is keyed on that same decision, so two stages that agree still + // share one uniform (see the location-budget note on the pass). + Uint splitImageUniformCount = 0; + source = SplitReadWriteImageUniforms(source, &splitImageUniformCount); + if (splitImageUniformCount != 0) { + splitImageUniformStages.push_back({linkedStages[index], splitImageUniformCount}); + } + source = RemoveLayoutBinding(source); + source = ProcessOutColorLocations(source); + source = ForceFlatIntegerVaryings(source, glShaderType); + source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount); + source = LowerZeroGradCubeArrayShadowLookups(source, glShaderType); + source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe()); + if (ClampsUnsignedMultisampleFetchesAtRuntime()) { + source = ClampUnsignedMultisampleFetches(source); + } + source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); + source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType); + // The two halves of the gl_ViewportIndex routing emulation, next to the draw- + // parameter promotion because they are the same shape: a builtin ESSL cannot + // spell, demoted to a plain global by a SPIR-V pass, given a real interface here. + // BEFORE ForceSupporterOutput, so the `precision highp` statements it hoists to + // the top land above the declarations these inject; AFTER + // ForceFlatIntegerVaryings, which matches only declarations carrying a + // layout(...) qualifier and so cannot touch either of them. + if (viewportEmulationForThisProgram) { + if (glShaderType == GL_FRAGMENT_SHADER) { + if (programRoutesViewportIndex && !InjectViewportIndexPassGate(source)) { + // MGLOG_E, unlatched, like the transpile- and compile-failure + // diagnostics around it: the program still links and still draws, so + // nothing else in the process will ever say that its viewport routing + // silently collapsed back to one rectangle. + MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no " + "entry point to gate, so the routing cannot be emulated: every " + "index will rasterize against viewport 0. State program ID: %u.", + m_backendProgramId, src.GetExternalIndex()); + } + } else if (PromoteViewportIndexGlobalToVarying(source)) { + programRoutesViewportIndex = true; + } + } + source = ForceSupporterOutput(source); + source = ClampNormFallbackOutputs(std::move(source), glShaderType, + m_snormFallbackClampOutputMask, + m_unormFallbackClampOutputMask); + + // Patch for Photon compiler precision issue + String findStr = "1000000.0"; + String replaceStr = "65500.0"; + auto pos = source.find(findStr); + while (pos != String::npos) { + MGLOG_D("Applying patch #2 to Photon..."); + source.replace(pos, findStr.length(), replaceStr); + pos = source.find(findStr, pos); + } + + const char* sourceCStr = source.c_str(); + MGLOG_D("Setting shader source for backend shader ID: %u\nsrc:\n%s", backendShaderId, sourceCStr); + g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr); + g_GLESFuncs.glCompileShader(backendShaderId); + + // GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed" + // and take the diagnostic path, never as a silent success that attaches an + // uncompiled shader. + GLint compileStatus = GL_FALSE; + g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus); + if (compileStatus == GL_FALSE) { + GLint logLength = 0; + g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength); + if (logLength < 0) logLength = 0; + // +1 and zero-filled: GL_INFO_LOG_LENGTH already counts the terminator, + // but a driver that reports 0 (or fails the query) must still leave + // log.data() a readable empty C string for the %s below. + Vector log(static_cast(logLength) + 1, '\0'); + g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data()); + log.back() = '\0'; + // MGLOG_E, unlatched. This was parked at MGLOG_I while the level ordering + // compiled E and W out of every INFO build: the Android retrace artifact + // carried 294 INFO lines and zero ERROR lines while two generated shaders + // were being rejected outright, and the lane could not say why it was + // rendering an empty translucent layer. A shader the driver refuses is + // never noise. + // + // A BOUNDED EXCERPT of the source goes with it. The driver log names a line + // and a column in text that exists nowhere but here, so without any source at + // all the only way to read "`gl_PointSize' undeclared" is to rebuild the whole + // library at DEBUG - but the full dump cannot go at E either. This is not + // "one line per refused shader": SyncToBackend's rebuild gate keys on + // per-draw state (the enabled-draw-buffer count among it), so a program used + // across passes with different draw-buffer counts re-transpiles, re-compiles + // and re-fails on every alternation, i.e. per frame. At E - live at the + // production INFO level - each of those records would push the whole + // post-SPIRV-Cross ESSL through the global log mutex with a forced flush onto + // /sdcard/MG/latest.log, the file users are asked to share. The excerpt keeps + // the record O(1); the full text is still there at D, printed against this + // same backend shader id by the "Setting shader source" line above, so + // nothing needs to be dumped twice. + constexpr SizeT kMaxLoggedSourceBytes = 2048; + String truncatedSource; + const char* sourceForLog = source.c_str(); + if (source.size() > kMaxLoggedSourceBytes) { + // Back up to a line boundary when there is one inside the window, so the + // excerpt ends on a whole statement rather than mid-token. Built only on + // this branch: a stage that fits keeps its own buffer and is not copied. + SizeT cut = kMaxLoggedSourceBytes; + if (const SizeT lastNewline = source.rfind('\n', cut); + lastNewline != String::npos && lastNewline > 0) { + cut = lastNewline + 1; + } + truncatedSource = source.substr(0, cut); + truncatedSource += "... [" + std::to_string(source.size() - cut) + + " more bytes; the whole stage is printed at the DEBUG level]\n"; + sourceForLog = truncatedSource.c_str(); + } + MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: " + "%u, driver log: %s\nSource:\n%s", + src.GetExternalIndex(), + MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId, + log.data(), sourceForLog); + m_backendProgramUsable = false; + // Nothing will ever attach this one, so nothing else can free it. + g_GLESFuncs.glDeleteShader(backendShaderId); + continue; + } + + MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId); + g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId); + // Hand the shader's lifetime to the program, immediately and unconditionally. + // + // glDeleteShader only FLAGS a shader; the driver frees it when it is attached to + // nothing. Flagging it here is what makes the program own it, so deleting the + // program (or the detach loop above, on a relink) is what actually frees it. + // Without this call every program build leaked its shader objects for the process + // lifetime, and a relink leaked them twice - the detach loop above dropped the + // program's reference to shaders nothing had flagged, so they became unreachable + // AND undeletable. The GL swizzle conformance test builds 1,296 programs per case, + // so a handful of cases left tens of thousands of live driver shaders behind and + // the driver started mis-serving them (KHR-GL33/GL40.texture_swizzle.smoke_*). + // Same class of defect as the missing framebuffer/renderbuffer/sampler destructors + // fixed in Wave 1, and the last of that family: this is the one backend GL object + // MobileGL creates without an owning wrapper to destroy it. + g_GLESFuncs.glDeleteShader(backendShaderId); + + // Kept AFTER every text-level pass, so what the synthesized control stage mirrors + // is the text the driver actually sees, not an intermediate form. + if (needsPassthroughTessControl) { + if (glShaderType == GL_VERTEX_SHADER) { + vertexStageEssl = source; + } else if (glShaderType == GL_TESS_EVALUATION_SHADER) { + tessEvalStageEssl = source; + } + } + + MGLOG_D("Processed shader source length: %zu", source.length()); + } + + if (needsPassthroughTessControl) { + AttachPassthroughTessControlStage(src, tessEvalShaderIndex, shaderSpirvs, + vertexStageEssl, tessEvalStageEssl); + } + + // 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 + // the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the + // frontend's requested names carry over unchanged. + SizeT declaredXfbVaryingCount = 0; + if (src.GetTransformFeedbackVaryingCount() > 0 && + g_GLESFuncs.glTransformFeedbackVaryings != nullptr) { + const auto& xfbVaryings = src.GetTransformFeedbackVaryings(); + Vector xfbNames; + xfbNames.reserve(xfbVaryings.size()); + // A block this build flattened no longer HAS the member the application asked + // for; it has the variable that replaced it. Everything else - including a + // member of a block that was left alone - keeps the application's spelling. + // Storage first, pointers after: xfbNames holds pointers into these strings. + // + // Same rule for a demoted gl_PointSize: the capture stage's ESSL no longer + // spells the built-in at all - the value lives in the carrier the demotion + // named - so the driver-side request has to follow it there. Only when the + // capture stage IS a demoted one (geometry, else evaluation): a program whose + // capture stage is the vertex shader keeps the built-in and its spelling, + // whatever happened to a control stage behind it. + Bool captureStageDemoted = false; + if (src.PointSizeDemoted()) { + for (const ShaderStage linkedStage : linkedStages) { + if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) { + captureStageDemoted = true; + break; + } + } + } + Vector rewrittenXfbNames(xfbVaryings.size()); + for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) { + String flatName; + if (!flattenedXfbBlockNames.empty() && + MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock( + xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) { + rewrittenXfbNames[nameIndex] = std::move(flatName); + } else if (captureStageDemoted && xfbVaryings[nameIndex].name == "gl_PointSize") { + rewrittenXfbNames[nameIndex] = + MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME; + } else { + rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name; + } + } + for (const auto& xfbName : rewrittenXfbNames) { + xfbNames.push_back(xfbName.c_str()); + } + MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(), + m_backendProgramId); + // Bounded: a lost context never answers GL_NO_ERROR, and this runs on the + // thread that would then spin forever. + for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { + } + g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast(xfbNames.size()), + xfbNames.data(), + src.GetTransformFeedbackBufferMode()); + // Unchecked before. A rejected capture set leaves the program linking happily + // with NO capture set at all, and then every draw of every span records + // nothing while the application reads its buffer's pre-draw bytes and + // GL_NO_ERROR - the signature four conformance families were stuck on. + if (const GLenum xfbError = g_GLESFuncs.glGetError(); xfbError != GL_NO_ERROR) { + String declared; + for (const auto& xfbName : rewrittenXfbNames) { + if (!declared.empty()) declared += ", "; + declared += xfbName; + } + MGLOG_E("The ES driver REJECTED the transform feedback capture set for backend program %u with " + "%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.", + m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(), + MG_Util::ConvertGLEnumToString( + src.GetTransformFeedbackBufferMode()).c_str(), + declared.c_str(), src.GetExternalIndex()); + } + declaredXfbVaryingCount = xfbNames.size(); + } + + // Link program + MGLOG_D("Linking program %u", m_backendProgramId); + g_GLESFuncs.glLinkProgram(m_backendProgramId); + + GLint linkStatus = GL_FALSE; + g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus); + m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE; + if (linkStatus != GL_TRUE) { + GLint logLength = 0; + g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength); + if (logLength < 0) logLength = 0; + Vector log(static_cast(logLength) + 1, '\0'); + g_GLESFuncs.glGetProgramInfoLog(m_backendProgramId, logLength, nullptr, log.data()); + log.back() = '\0'; + // MGLOG_I for the same reason as the compile failure above: a program that + // links nothing no-ops every draw that uses it, and that has to be readable + // in an INFO-level artifact. + MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s", + src.GetExternalIndex(), m_backendProgramId, log.data()); + // The one link failure MobileGL can name a cause for that the driver's log never + // will: ESSL has no legal single declaration for a read+write image outside + // r32f/r32i/r32ui, so those are split into a coherent pair and the stage ends up + // declaring more image uniforms than the application did - against a + // GL_MAX_*_IMAGE_UNIFORMS that is still the driver's raw number, because lowering + // it would fail basic-api and NotSupported-out every case that only ever uses + // readonly/writeonly images. A shader declaring more than half a stage's budget in + // read+write images therefore links here and nowhere else, and without this line + // the next reader has only a generic driver message to go on. + for (const SplitImageUniformStage& split : splitImageUniformStages) { + MGLOG_E("...and %u read+write image uniform(s) in stage %s were split into coherent " + "read/write pairs, so that stage declares %u image uniform(s) more than the " + "program did; GL_MAX_*_IMAGE_UNIFORMS for it is %d. If the driver log names " + "image uniforms, that is the cause.", + split.splitCount, + MG_Util::ConvertGLEnumToString( + MG_Util::ConvertShaderStageToGLEnum(split.stage)).c_str(), + split.splitCount, AdvertisedStageImageUniformLimit(split.stage)); + } + } else { + MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId); + // A link that SUCCEEDS can still have dropped the capture set: ESSL rejects a + // requested name the transpiled shader does not actually declare by simply not + // capturing it, and a program whose last vertex-processing stage was rewritten + // by a SPIR-V pass (viewport-index lowering, gl_PerVertex handling, the + // synthesized pass-through tessellation control stage) can end up spelling its + // outputs differently from the frontend's request. Asking the driver what it + // ACTUALLY linked is the only way to tell that apart from a driver that just + // captures nothing - which is the whole ambiguity the empty-capture failures + // across geometry_shader / tessellation_shader / gpu_shader5 / DSA sat on. + if (declaredXfbVaryingCount > 0) { + GLint linkedXfbVaryings = 0; + GLint linkedXfbBufferMode = 0; + g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_VARYINGS, + &linkedXfbVaryings); + g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_BUFFER_MODE, + &linkedXfbBufferMode); + for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { + } + const GLenum requestedMode = src.GetTransformFeedbackBufferMode(); + if (static_cast(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount || + static_cast(linkedXfbBufferMode) != requestedMode) { + MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not " + "agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) " + "in mode %s. Captures made with it will be empty or wrongly laid out.", + m_backendProgramId, src.GetExternalIndex(), declaredXfbVaryingCount, + MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings, + MG_Util::ConvertGLEnumToString( + static_cast(linkedXfbBufferMode)).c_str()); + } else { + MGLOG_D("Backend program %u capture set confirmed by the driver: %d varying(s), mode %s", + m_backendProgramId, linkedXfbVaryings, + MG_Util::ConvertGLEnumToString( + static_cast(linkedXfbBufferMode)).c_str()); + } + } + } + // The driver program was relinked IN PLACE, so its GL name no longer identifies + // the executable behind it - and that name is exactly what Use()'s + // g_lastUsedBackendProgramId early-out treats as identifying it. Without this, + // a rebuild of the program that is already bound issues no glUseProgram at all + // and the driver keeps running whatever the last one installed. + // + // GL 4.6 core 7.3 does promise that a successful re-link of a program in use + // installs the new executable - but only "for all shader stages where the program + // is active", and a stage the previous link did not produce is not active for + // anything. So a relink that ADDS a stage is precisely the case the promise does + // not cover. Verified with no MobileGL in the process (bare EGL + GLES 3.2, + // llvmpipe 26.1.4): vertex+fragment linked, used and drawn renders; a geometry + // shader attached and relinked reports LINK_STATUS true with an empty info log, + // and the next draw renders NOTHING and raises no error - while the same draw + // after a fresh glUseProgram of the same name renders again. + // + // A flag rather than zeroing the guard: 0 is also the id Use() binds for a build + // that did NOT come out usable, and a zeroed guard would make it skip that + // glUseProgram(0) and leave the failed program's previous executable running - + // the silent wrong-shader draw Use() exists to prevent. + m_rebindAfterRelink = true; + m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, + BASE_INSTANCE_UNIFORM_NAME); + m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME); + m_baseVertexUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, + BASE_VERTEX_UNIFORM_NAME); + m_baseInstanceWordIndexUniformLocation = + g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME); + // Asked of the DRIVER rather than remembered from the injection, deliberately: the + // gate is only real if the uniform survived compilation and linking, and this is the + // one question whose answer covers both. A gate the driver optimized away would + // otherwise leave the draw path replaying passes whose mask reaches nothing, which + // renders every index's primitives in every pass. + m_viewportPassMaskUniformLocation = + g_GLESFuncs.glGetUniformLocation(m_backendProgramId, VIEWPORT_PASS_MASK_UNIFORM_NAME); + if (m_viewportPassMaskUniformLocation >= 0) { + // Sticky, and never cleared on a relink: it only ever short-circuits a per-draw + // check, so being late to go false costs a pointer compare and being late to go + // true would cost correctness. + g_anyProgramRoutesViewportIndex = true; + } + // The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind + // SSBO blocks after compile); record it so draws bind the indirect buffer there. + m_indirectParamsBinding = -1; + if (m_baseInstanceWordIndexUniformLocation >= 0 && g_GLESFuncs.glGetProgramResourceIndex) { + const GLuint blockIndex = g_GLESFuncs.glGetProgramResourceIndex( + m_backendProgramId, GL_SHADER_STORAGE_BLOCK, INDIRECT_PARAMS_BLOCK_NAME); + if (blockIndex != GL_INVALID_INDEX && g_GLESCapabilities.MaxShaderStorageBufferBindings > 0) { + m_indirectParamsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings - 1; + } + } + + // Create global UBO + if (src.GetUBOSize() > 0) { + g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId); + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId); + g_GLESFuncs.glBufferData(GL_UNIFORM_BUFFER, src.GetUBOSize(), nullptr, GL_STREAM_DRAW); + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); + } else { + m_backendGlobalUBOId = 0; + } + + CacheResourceLocations(src); + // NOT the mechanism that makes a rebinding work - the transpiled qualifier above is. + // glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes, + // so this replay is a no-op almost everywhere; it stays because it is still correct + // (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL + // driver used as the ES backend. AFTER the link either way, because it needs the + // driver's linked interface. + ReseedShaderStorageBlockBindings(m_backendProgramId, src); + + m_isInitialized = true; + MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId); + } + + // P5e (pg), CONTRACT-P5E §5.5: THE BUILD THAT NAMES NO CLIENT MEMORY. Everything it + // reads is the record: the archive create_shader_state carried, the three binding tails + // set_program_bindings carried, and the descriptor. Nothing here resolves a frontend + // identity, probes the client allocator or holds a SharedPtr to a frontend object - the + // three absences rule F is made of. + void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + const auto* record = PipeShaderCsoRecordForHandle(cso); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record, so no driver " + "program can be built for it", + cso.Slot, cso.Gen); + return; + } + // THE ARCHIVE IS THE RECORD'S OR THE BUILD DOES NOT HAPPEN. A record with no archive + // is one the transport arm never filled - a monolith create that reached a handle + // arm - and building from an empty reflection would produce a program with no + // uniforms and no modules, i.e. a black screen with no marker anywhere. The + // create's own trip wire already refuses a record that declares no blobs AND + // carries no artefacts (PipeApply.cpp); this is the same statement one level later, + // where the twin can name the handle. + if (!record->Archive) { + MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} carries no server-owned archive; its " + "create_shader_state was emitted by the monolith arm and this is " + "the handle arm, so there is nothing to build from", + cso.Slot, cso.Gen); + m_backendProgramUsable = false; + return; + } + SyncToBackendFromSource(ProgramArchiveSource::FromRecord(cso, *record)); + // The two server-owned keys the draw path's clean condition reads instead of + // GetLinkVersion()/GetImageUnitVersion(). BindingsSerial is the second because a + // glUniform1i on an IMAGE uniform is baked into the generated ESSL rather than + // re-issued per draw - the record that carries it must therefore force a rebuild, + // exactly as m_imageUnitVersion used to. + m_syncedShaderCsoSerial = record->Serial; + m_syncedBindingsSerial = record->BindingsSerial; + } + + namespace { + // The GL name of the array element that lives at `location`, given the reflection + // name reported for it. Reflection reports one name per UNIFORM ("goku[0]") but + // one location per ELEMENT, so a caller walking locations sees the same name + // repeatedly; this turns it back into "goku[k]". Anything that is not an array + // (or whose base location cannot be resolved) comes back unchanged, so the only + // behaviour that moves is the array case. + String SubscriptUniformNameForElement(const ProgramBuildSource& program, const String& name, + Uint location) { + if (name.size() < 3 || name.compare(name.size() - 3, 3, "[0]") != 0) return name; + const Int base = program.GetUniformLocation(name); + if (base < 0 || static_cast(base) > location) return name; + const Uint element = location - static_cast(base); + if (element == 0) return name; + return name.substr(0, name.size() - 3) + "[" + std::to_string(element) + "]"; + } + } // namespace + + // Resolves every name-based resource lookup once per link so the per-draw path + // (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex / + // glGetUniformLocation string queries; block-to-binding-point assignments are + // program state and only need to be established here. + void BackendProgramObjectImpl::CacheResourceLocations(const ProgramBuildSource& src) { + m_globalUboBackendBlockIndex = -1; + m_globalUboBackendBlockSize = 0; + m_lastUploadedGlobalUboVersion = ~0u; + m_globalUboRingAllocation = {}; + if (src.GetUBOSize() > 0) { + const Uint blockIndex = + g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); + if (blockIndex != GL_INVALID_INDEX) { + m_globalUboBackendBlockIndex = static_cast(blockIndex); + g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0); + // Ring bindings are ranges and must span the block as the backend + // compiled it (its std140 padding may exceed the frontend's + // SPIR-V-reflected size). + if (g_GLESFuncs.glGetActiveUniformBlockiv) { + GLint blockDataSize = 0; + g_GLESFuncs.glGetActiveUniformBlockiv(m_backendProgramId, blockIndex, + GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize); + m_globalUboBackendBlockSize = static_cast(blockDataSize); + } + } else { + MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.", + src.GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME); + } + } + + const Int uboCount = src.GetActiveUniformBlocksCount(); + m_uniformBlockBackendIndices.assign(static_cast(std::max(uboCount, 0)), -1); + Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO + for (Int i = 0; i < uboCount; ++i) { + ++lastUBOBinding; + const auto& name = src.GetUniformBlockName(static_cast(i)); + const GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, name.c_str()); + if (backendBlkIdx == GL_INVALID_INDEX) { + // Either eliminated as unused, or an SSBO block (frontend reflection + // lists those among uniform blocks); SSBO bindings are baked into the ESSL. + continue; + } + m_uniformBlockBackendIndices[static_cast(i)] = static_cast(backendBlkIdx); + g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding); + MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u", + src.GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx, + lastUBOBinding); + } + + m_samplerUniformBindings.clear(); + const Uint maxUniformLoc = src.GetMaxUniformLocation(); + for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { + const auto& name = src.GetUniformName(loc); + if (name.empty()) continue; + const GLenum uniformType = src.GetUniformType(loc); + if (IsImageUniformType(uniformType)) { + // ES image units come exclusively from the layout(binding=N) qualifier + // (preserved in the transpiled ESSL); glUniform1i on an image uniform + // is an INVALID_OPERATION. + continue; + } + // Reflection names an array uniform after its FIRST element ("goku[0]") at + // every location the array spans, so asking the driver for that one name + // once per location hands back the same backend location N times. The + // per-draw pass then issues N glUniform1i calls against it and only the + // last element's unit survives - "layout(binding = 1) uniform sampler2D + // goku[7]" ended up with goku[0] on unit 7 and goku[1..6] still on 0. + // Address each element by its own name instead; the frontend already + // reserves one location per element, so the element index is the distance + // from the array's base location. + const String elementName = SubscriptUniformNameForElement(src, name, loc); + const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, elementName.c_str()); + if (backendLoc < 0) continue; + SamplerUniformBinding binding; + binding.frontendLocation = loc; + binding.backendLocation = backendLoc; + binding.uniformType = uniformType; + binding.lastAssignedUnit = -1; + // Present only for the samplers EmulateTextureLodBias actually rewrote; the + // pass names it after the sampler, which SPIRV-Cross preserves verbatim. + binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation( + m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str()); + binding.lastAssignedLodBias = 0.0f; + // Present only for the unsigned multisample samplers ClampUnsignedMultisampleFetches + // rewrote, named the same way. + if (uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE || + uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY) { + binding.msSampleMaxLocation = g_GLESFuncs.glGetUniformLocation( + m_backendProgramId, (String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + elementName).c_str()); + } + binding.lastAssignedMsSampleMax = 0; + m_samplerUniformBindings.push_back(binding); + } + } + + void BackendProgramObjectImpl::Use() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // glUseProgram on a program that did not link is an INVALID_OPERATION and + // leaves the *previous* program current, so the draw would silently render + // with an unrelated shader (KHR-GL3x.texture_size_promotion read another + // test case's alpha that way once a sampler2DRect stage failed to + // transpile). Bind nothing instead: the draw is then a visible no-op. + const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0; + // ...unless SyncToBackend relinked this program since the last bind, in which case + // the id matching proves nothing about the executable behind it. + if (g_lastUsedBackendProgramId == programToBind && !m_rebindAfterRelink) { + return; + } + m_rebindAfterRelink = false; + if (!m_backendProgramUsable) { + // Every draw made with this program renders nothing and raises no GL error, so + // without this line the only symptom is a framebuffer that kept its clear + // colour. Latched: the early return above only dedupes CONSECUTIVE binds, so an + // app alternating a healthy and a broken program would otherwise log every + // single draw. Parked at MGLOG_I until the level ordering was fixed. + MGLOG_E_ONCE("Backend program for GL program %u is unusable (a shader failed to transpile, " + "compile or link); binding program 0 - draws with it will render nothing", + m_frontendProgramId); + } + MGLOG_D("Using program %u", programToBind); + g_GLESFuncs.glUseProgram(programToBind); + g_lastUsedBackendProgramId = programToBind; + } + + void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const { + if (m_baseInstanceUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast(baseInstance)); + } + // A direct value disables the indirect-command-buffer read. + SetBaseInstanceWordIndex(-1); + } + + // The uniform is written one-based so that its GLSL initial value, zero, already reads + // as "no indirect command" - see PromoteDrawParameterGlobalsToUniforms. + void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const { + if (m_baseInstanceWordIndexUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, + wordIndex < 0 ? 0 : wordIndex + 1); + } + } + + void BackendProgramObjectImpl::SetBaseVertex(Int32 baseVertex) const { + if (m_baseVertexUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseVertexUniformLocation, baseVertex); + } + } + + void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const { + if (m_drawIdUniformLocation < 0) { + return; + } + g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast(drawId)); + } + + void BackendProgramObjectImpl::SetViewportPassMask(Uint32 indexMask) const { + if (m_viewportPassMaskUniformLocation < 0) { + return; + } + g_GLESFuncs.glUniform1i(m_viewportPassMaskUniformLocation, static_cast(indexMask)); + } + } // namespace PrgramImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp new file mode 100644 index 00000000..f46a12a3 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp @@ -0,0 +1,271 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() { + // IsAngleLlvmpipeRenderer combined with the + // MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle, + // both resolved in FillInGLESCapabilities. + return g_GLESCapabilities.AvoidSamplerMipmapMinFilter; + } + + namespace SamplerImpl { + BackendSamplerObject::BackendSamplerObject() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId); + m_contextGeneration = g_backendContextGeneration; + if (m_backendSamplerId == 0) { + MGLOG_E_ONCE("Failed to generate sampler object."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + } else { + MGLOG_D("Generated sampler object with ID: %u.", m_backendSamplerId); + } + } + + BackendSamplerObject::~BackendSamplerObject() { + if (InProcessTeardown()) { + return; // see InProcessTeardown(): the driver may be unloaded already + } + if (m_backendSamplerId == 0) { + return; + } + // Scrub the unit shadow whether or not the id can still be deleted - the next + // twin can land on this heap address and would otherwise false-skip its Bind. + for (auto& boundSampler : g_boundSamplersCache) { + if (boundSampler == this) { + boundSampler = nullptr; // glDeleteSamplers unbinds from every unit + } + } + if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) { + g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId); + } + m_backendSamplerId = 0; + } + + void BackendSamplerObject::SyncToBackend( + const SharedPtr& stateSamplerObject, + MG_Pipe::MGPipeHandle pushedCso) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (!stateSamplerObject) { + // A CSO TWIN HAS NO FRONTEND OBJECT (P4a fable seam F-4, ResolveSamplerCsoTwin): + // the content-addressed handle is its identity and the applier's record its + // only authority, so a null object beside a live handle is the record arm and + // not the pre-P4a error. Everything below that names the object is guarded on + // it; the pull build's text is the three lines the #if brackets. + if (MG_Pipe::MGPipeHandleIsNull(pushedCso)) { + MGLOG_E_ONCE("State sampler object is null, cannot sync to backend."); + return; + } + } + + // P4a (D-F1): a SamplerObject is a pure 100-byte value with no driver-side per-object + // binding state, so its CSO is CONTENT-ADDRESSED on the client at capacity 256 and + // two identical samplers share one record. What that means here is that the record's + // server-owned Serial, not the frontend object's version, is what says the values + // moved - and that the values themselves cross byte for byte INCLUDING + // borderColorForm, which is why all four border comparands below are still compared. + // + // The whole arm choice is a preprocessor #if/#else so the PULL build's text is the + // pre-P4a text token for token (D-P). + const SamplerParameters* pushedParams = nullptr; + // The GL name for the two log lines below, or 0 for a CSO twin, which has no object + // to name (F-4): the handle in the same line is its name. + const Uint samplerName = stateSamplerObject ? stateSamplerObject->GetExternalIndex() : 0u; + // THE HANDLE COMES FROM THE CALLER, NOT FROM THIS TWIN'S REGISTRY, and the + // difference is the seam D's verification round found on the integrated tree. + // + // v2 asked g_backendSamplerObjects.HandleOf(object) - the twin's own identity + // handle, minted off the frontend lifetime id - and looked the record up by it. + // That can only ever miss: a SamplerCso is CONTENT-ADDRESSED on the client + // (D-F1, ID-14/ID-17), its handles come out of MGPipeSlots().Allocate keyed on a + // parameter hash, and nothing ever emits a create_sampler_state at an identity + // handle. On the integrated tree every glBindSampler-driven parameter set was + // therefore refused and the driver sampler kept its defaults - two integration + // scenarios red, and the twin's own comment below (content-addressed, the + // record's Serial is the authority) already said why it could not work. + // + // The carried fact is MGPipeApplier().BoundSamplerStates[unit], written by the + // client at bind_sampler_states; the caller that knows the unit passes it as + // `pushedCso`. See the declaration for why the parameter is push-only. + if (!MG_Pipe::MGPipeHandleIsNull(pushedCso)) { + const auto* record = PipeSamplerCsoRecordForHandle(pushedCso); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: sampler %u has no applier record on the handle arm, so its " + "parameters cannot be pushed (handle {%u, %u})", + samplerName, pushedCso.Slot, pushedCso.Gen); + return; + } + if (m_isInitialized && m_syncedSamplerSerial != 0 && m_syncedSamplerSerial == record->Serial) { + MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.", + samplerName); + return; + } + m_syncedSamplerSerial = record->Serial; + pushedParams = &record->Params; + } else { + // P13: every sampler twin is a CSO twin, synced with its handle + // (ResolveSamplerCsoTwin); a sampler with no record has nothing to push. + MGLOG_E_ONCE("MGPipe: sampler %u was synced without a SamplerCso handle", samplerName); + return; + } + + MGLOG_D("Syncing sampler with backend ID %u to backend for state ID %u", m_backendSamplerId, samplerName); + + const SamplerParameters& samplerParams = *pushedParams; + +#define SYNC_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \ + if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \ + g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, glName, \ + (GLint)MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \ + m_cacheSamplerParameters.internalName = samplerParams.internalName; \ + } + + if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter || + m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) { + g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, GL_TEXTURE_MIN_FILTER, + (GLint)ResolveBackendMinFilter( + samplerParams, + ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe())); + m_cacheSamplerParameters.minFilter = samplerParams.minFilter; + m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode; + } + if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) { + g_GLESFuncs.glSamplerParameteri( + m_backendSamplerId, GL_TEXTURE_MAG_FILTER, + (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None)); + m_cacheSamplerParameters.magFilter = samplerParams.magFilter; + } + + SYNC_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode) + SYNC_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode) + SYNC_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode) + SYNC_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc) + SYNC_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode) + if (m_cacheSamplerParameters.minLod != samplerParams.minLod) { + g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MIN_LOD, samplerParams.minLod); + m_cacheSamplerParameters.minLod = samplerParams.minLod; + } + if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) { + g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); + m_cacheSamplerParameters.maxLod = samplerParams.maxLod; + } + if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { + if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { + g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT, + samplerParams.maxAnisotropy); + } + m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; + } + if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor || + m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI || + m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI || + m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) { + // Same gate as the texture-side border colour above, and the same reason for + // branching on the form: an integer border colour must reach the driver through + // glSamplerParameterIiv/Iuiv or an integer sampler reads the float's bit pattern + // back instead of the value. + if (g_GLESCapabilities.SupportsTextureBorderClamp) { + if (samplerParams.borderColorForm == BorderColorForm::Int && + g_GLESFuncs.glSamplerParameterIiv) { + const GLint borderColorArray[4] = { + samplerParams.borderColorI.x(), samplerParams.borderColorI.y(), + samplerParams.borderColorI.z(), samplerParams.borderColorI.w()}; + g_GLESFuncs.glSamplerParameterIiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, + borderColorArray); + } else if (samplerParams.borderColorForm == BorderColorForm::Uint && + g_GLESFuncs.glSamplerParameterIuiv) { + const GLuint borderColorArray[4] = { + samplerParams.borderColorUI.x(), samplerParams.borderColorUI.y(), + samplerParams.borderColorUI.z(), samplerParams.borderColorUI.w()}; + g_GLESFuncs.glSamplerParameterIuiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, + borderColorArray); + } else if (g_GLESFuncs.glSamplerParameterfv) { + const GLfloat borderColorArray[4] = { + samplerParams.borderColor.x(), samplerParams.borderColor.y(), + samplerParams.borderColor.z(), samplerParams.borderColor.w()}; + g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, + borderColorArray); + } + } + m_cacheSamplerParameters.borderColor = samplerParams.borderColor; + m_cacheSamplerParameters.borderColorI = samplerParams.borderColorI; + m_cacheSamplerParameters.borderColorUI = samplerParams.borderColorUI; + m_cacheSamplerParameters.borderColorForm = samplerParams.borderColorForm; + } +#undef SYNC_SAMPLER_PARAM_IF_CHANGED + m_isInitialized = true; + } + + void BackendSamplerObject::Bind(Uint unit) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (g_boundSamplersCache[unit] == this) return; + + g_GLESFuncs.glBindSampler(static_cast(unit), m_backendSamplerId); + g_boundSamplersCache[unit] = this; + } + + Uint BackendSamplerObject::GetBackendSamplerId() const { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + return m_backendSamplerId; + } + + void UnbindSampler(Uint unit) { + if (g_boundSamplersCache[unit] == nullptr) return; + + g_GLESFuncs.glBindSampler(static_cast(unit), 0); + g_boundSamplersCache[unit] = nullptr; + } + + Array g_boundSamplersCache; + TwinRegistry g_backendSamplerObjects; + + // P4a fable seam F-4. THE TWIN FOR A CONTENT-ADDRESSED SamplerCso HANDLE, keyed by that + // handle and synced from its record - see the declaration for why the identity-keyed + // lookup it replaces could never hit. + BackendSamplerObject* ResolveSamplerCsoTwin(MG_Pipe::MGPipeHandle cso) { + if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; + // THE RECORD FIRST, before the table is touched: a handle with no record is a seam + // defect (the client minted and named a CSO it never described, or evicted one a + // standing set still names), and adopting a slot for it would leave a twin that + // syncs nothing. The census stem is the sampler family's. + const auto* record = PipeSamplerCsoRecordForHandle(cso); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} has no applier record on the handle arm, so no " + "driver sampler can be built for it and the unit keeps what it holds", + cso.Slot, cso.Gen); + return nullptr; + } + // The same slot table the identity twins live in: one allocator serves both handle + // families of this kind, so a content-addressed slot and an identity-minted slot can + // never coincide, and the generation discipline (forward = recycle, backward = + // refused) is what retires a twin whose CSO the client's LRU evicted and re-minted. + auto* slot = g_backendSamplerObjects.GetOrCreateByHandle(cso); + if (slot == nullptr) { + MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} cannot be adopted on the handle arm (the slot's " + "live generation is %u), so no driver sampler is built for it", + cso.Slot, cso.Gen, g_backendSamplerObjects.LiveGenAt(cso.Slot)); + return nullptr; + } + if (!*slot) *slot = MakeShared(); + // Serial-gated inside: a CSO whose record did not move since this twin last synced + // costs the record lookup above and one compare. + (*slot)->SyncToBackend(nullptr, cso); + return slot->get(); + } + } // namespace SamplerImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp new file mode 100644 index 00000000..ed0da334 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp @@ -0,0 +1,2361 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace TextureImpl { + // --- Unpack-ring staging (see BufferImpl::UnpackRingAvailable) --------------- + // One rectangular (or whole-level) region of a level shadow, repacked TIGHTLY + // into the persistently-mapped unpack PBO. The upload that follows passes the + // returned ring offset with UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT left at the + // surrounding ScopedDefaultUnpackState's 0, so the driver reads exactly the + // bytes staged here - strictly fewer than the ROW_LENGTH-strided client-pointer + // upload it replaces, which made the driver walk the whole level's stride. + // + // `blocks` describes one or more source regions to stage back-to-back into a + // SINGLE ring allocation; the ring offset of block i lands in blocks[i].offset. + // Deliberately without default member initializers: call sites declare a + // kMaxDirtyRects-sized array of these per dirty level and fill only the entries + // they use, so the type stays trivially default-constructible and the + // declaration costs nothing on the levels that never reach the ring. + struct UnpackStagingBlock { + const Uint8* src; // top-left texel of the region in the level's upload bytes + SizeT rowBytes; // bytes per region row (region width * bpp) + SizeT rows; // region height + SizeT slices; // region depth (1 for 2D) + SizeT srcRowStride; // level row pitch + SizeT srcSliceStride; // level slice pitch + SizeT offset; // out: byte offset into the ring store + }; + + // False (nothing staged, ring untouched) whenever the caller must keep the + // client-pointer path: ring unavailable, a degenerate block, or a total that + // overflows / exceeds the ring's cap. + static Bool StageBlocksIntoUnpackRing(UnpackStagingBlock* blocks, SizeT blockCount) { + if (blocks == nullptr || blockCount == 0) return false; + if (!BufferImpl::UnpackRingAvailable()) return false; + + const SizeT maxBytes = BufferImpl::UnpackRingMaxBytes(); + SizeT total = 0; + for (SizeT i = 0; i < blockCount; ++i) { + const UnpackStagingBlock& b = blocks[i]; + if (b.src == nullptr || b.rowBytes == 0 || b.rows == 0 || b.slices == 0) return false; + // Every factor is bounded by the level's own byte size, but the + // multiplications are on SizeT: check each against the cap instead of + // trusting a product that could have wrapped. + if (b.rowBytes > maxBytes || b.rows > maxBytes / b.rowBytes) return false; + const SizeT sliceBytes = b.rowBytes * b.rows; + if (b.slices > maxBytes / sliceBytes) return false; + const SizeT blockBytes = sliceBytes * b.slices; + if (blockBytes > maxBytes - total) return false; + total += blockBytes; + } + + SizeT base = 0; + if (!BufferImpl::UnpackRingAllocate(total, base)) return false; + // AFTER the allocation: growing the ring replaces the store and its map. + auto* ringBytes = static_cast(BufferImpl::UnpackRingMappedPtr()); + if (ringBytes == nullptr) return false; + + SizeT cursor = base; + for (SizeT i = 0; i < blockCount; ++i) { + UnpackStagingBlock& b = blocks[i]; + b.offset = cursor; + Uint8* dst = ringBytes + cursor; + // Each block's size is a whole number of texels, so consecutive block + // offsets stay multiples of the pixel type's size just as `base` is. + if (b.rowBytes == b.srcRowStride && b.rowBytes * b.rows == b.srcSliceStride) { + Memcpy(dst, b.src, b.rowBytes * b.rows * b.slices); // region IS the level + } else { + for (SizeT z = 0; z < b.slices; ++z) { + const Uint8* srcSlice = b.src + z * b.srcSliceStride; + if (b.rowBytes == b.srcRowStride) { + Memcpy(dst, srcSlice, b.rowBytes * b.rows); // full-width rows + dst += b.rowBytes * b.rows; + continue; + } + for (SizeT y = 0; y < b.rows; ++y) { + Memcpy(dst, srcSlice + y * b.srcRowStride, b.rowBytes); + dst += b.rowBytes; + } + } + } + cursor += b.rowBytes * b.rows * b.slices; + } + return true; + } + + // The `pixels` argument of a PBO-sourced glTexSubImage: a byte offset dressed + // up as a pointer. + static const void* UnpackRingPixelOffset(SizeT offset) { + return reinterpret_cast(static_cast(offset)); + } + + static Uint GetNormFallbackComponentCount(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::R16: + case TextureInternalFormat::R16Snorm: + return 1; + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RG16: + case TextureInternalFormat::RG16Snorm: + return 2; + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow) + case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow) + case TextureInternalFormat::RGB16Snorm: + return 3; + case TextureInternalFormat::RGBA8Snorm: + case TextureInternalFormat::RGBA16: + case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow) + case TextureInternalFormat::RGBA16Snorm: + return 4; + default: + return 0; + } + } + + static Bool IsSnormFallbackFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGBA8Snorm: + case TextureInternalFormat::R16Snorm: + case TextureInternalFormat::RG16Snorm: + case TextureInternalFormat::RGB16Snorm: + case TextureInternalFormat::RGBA16Snorm: + return true; + default: + return false; + } + } + + static Bool IsNorm8FallbackFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::R8Snorm: + case TextureInternalFormat::RG8Snorm: + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGBA8Snorm: + return true; + default: + return false; + } + } + + // The bit pattern of 1.0 in an upload component type: what a format without alpha reads + // back as, and therefore what the synthetic fourth channel of a widened render target has + // to hold. Integer components carry the integer one, not a saturated field - and since + // GL_BYTE/GL_SHORT/GL_UNSIGNED_BYTE/GL_UNSIGNED_SHORT serve both classes, `integerData` + // is what decides, not the type. + static Bool GetUploadComponentOneBits(GLenum uploadType, Bool integerData, Uint8* outOneBits, + SizeT* outComponentSize) { + switch (uploadType) { + case GL_BYTE: { + const Int8 one = integerData ? Int8(1) : Int8(0x7F); + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_UNSIGNED_BYTE: { + const Uint8 one = integerData ? Uint8(1) : Uint8(0xFF); + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_SHORT: { + const Int16 one = integerData ? Int16(1) : Int16(0x7FFF); + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_UNSIGNED_SHORT: { + const Uint16 one = integerData ? Uint16(1) : Uint16(0xFFFF); + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_HALF_FLOAT: { + const Uint16 one = 0x3C00; // half 1.0 + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_FLOAT: { + const Float one = 1.0f; + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_INT: { + const Int32 one = 1; + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + case GL_UNSIGNED_INT: { + const Uint32 one = 1; + Memcpy(outOneBits, &one, sizeof(one)); + *outComponentSize = sizeof(one); + return true; + } + default: + return false; + } + } + + // A three-channel format widened to four to keep a colour attachment renderable (see + // NormalizePixelFormat) is described to the driver as a four-component transfer, so the + // three-component client data has to be repacked with an alpha of 1.0 - otherwise the + // driver walks three texels' worth of data per four-texel row and the image shears. + // `componentCount` is the SOURCE component count and `byteSize` the source's size, so this + // runs after any type conversion (which keeps the component count) has already happened. + const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, + const void* data, SizeT byteSize, GLenum uploadType, + Vector& widenedData, Bool integerData, + Uint32 alphaOneCodeOverride) { + Uint8 oneBits[8] = {}; + SizeT componentSize = 0; + // One and two source components as well as three: the image-format widening carries + // GL_R8UI in a GL_RGBA8UI and GL_RG32F in a GL_RGBA32F (see + // TextureImpl::GetImageBindableStorageWidening), and their surplus channels take the + // same values the three-channel case gives its single added one - zeroes, and the + // format's implied 1 in alpha. + if (componentCount == 0 || componentCount > 3 || data == nullptr || byteSize == 0 || + !GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) { + return data; + } + // ...except where the carrier holds CODES of a normalized value (GL_R16 in a + // GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that + // type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated + // 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose + // saturated code is 0x7FFF. The caller passes the channel's own maximum instead. + // Written through a value of the component's own width rather than as the low + // `componentSize` bytes of the Uint32, so the encoding does not turn on the host's + // byte order. + if (alphaOneCodeOverride != 0u) { + if (componentSize == sizeof(Uint16)) { + const auto one = static_cast(alphaOneCodeOverride); + Memcpy(oneBits, &one, sizeof(one)); + } else if (componentSize == sizeof(Uint32)) { + Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride)); + } else if (componentSize == sizeof(Uint8)) { + const auto one = static_cast(alphaOneCodeOverride); + Memcpy(oneBits, &one, sizeof(one)); + } + } + + const SizeT srcTexelBytes = componentSize * componentCount; + // Sized from the level, never from the source: the driver reads a full + // width*height*depth*4 components for the transfer it was handed, so a source that + // somehow holds fewer texels must still leave a full destination behind (its tail + // reads as transparent black with the format's implied opaque alpha) rather than a + // short buffer the driver would run off the end of. + const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(std::max(texelSize.z(), 1)); + if (texelCount == 0) { + return data; + } + const SizeT copyTexelCount = std::min(texelCount, byteSize / srcTexelBytes); + + widenedData.assign(texelCount * componentSize * 4, 0); + const auto* src = static_cast(data); + Uint8* dst = widenedData.data(); + for (SizeT i = 0; i < texelCount; ++i, dst += componentSize * 4) { + if (i < copyTexelCount) { + Memcpy(dst, src, srcTexelBytes); + src += srcTexelBytes; + } + // ALWAYS at component 3, never at `componentCount`: GL's implied 1 is the ALPHA + // channel, and a one- or two-component source leaves the channels between it and + // alpha at the zero `assign` already wrote. For three components the two + // expressions coincide, which is what this used to be written as. + Memcpy(dst + componentSize * 3, oneBits, componentSize); + } + return widenedData.data(); + } + + static const void* PrepareNormFloatFallbackUpload(TextureInternalFormat format, + const IntVec3& texelSize, + const void* data, + SizeT byteSize, + GLenum uploadType, + Vector& convertedData) { + const Uint componentCount = GetNormFallbackComponentCount(format); + if (componentCount == 0 || uploadType != GL_FLOAT || data == nullptr || byteSize == 0) { + return data; + } + + const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(std::max(texelSize.z(), 0)); + const SizeT componentTotal = texelCount * static_cast(componentCount); + const SizeT sourceComponentSize = IsNorm8FallbackFormat(format) ? sizeof(Int8) : sizeof(Uint16); + const SizeT sourceComponentTotal = byteSize / sourceComponentSize; + if (componentTotal == 0 || sourceComponentTotal == 0) { + return nullptr; + } + + convertedData.assign(componentTotal, 0.0f); + const SizeT copyComponentTotal = std::min(componentTotal, sourceComponentTotal); + if (IsNorm8FallbackFormat(format)) { + const Int8* src = static_cast(data); + constexpr Float invMaxSnorm8 = 1.0f / 127.0f; + for (SizeT i = 0; i < copyComponentTotal; ++i) { + convertedData[i] = std::max(static_cast(src[i]) * invMaxSnorm8, -1.0f); + } + } else if (IsSnormFallbackFormat(format)) { + const Int16* src = static_cast(data); + constexpr Float invMaxSnorm16 = 1.0f / 32767.0f; + for (SizeT i = 0; i < copyComponentTotal; ++i) { + convertedData[i] = std::max(static_cast(src[i]) * invMaxSnorm16, -1.0f); + } + } else { + const Uint16* src = static_cast(data); + constexpr Float invMaxUnorm16 = 1.0f / 65535.0f; + for (SizeT i = 0; i < copyComponentTotal; ++i) { + convertedData[i] = static_cast(src[i]) * invMaxUnorm16; + } + } + return convertedData.data(); + } + + // The two shadow -> upload conversions a fallback storage format can need, in order: + // the component type first (SNORM/UNORM shadows into the float the fallback stores), then + // the component count (three-channel client data into a four-channel widened render + // target). They compose: GL_RGB8_SNORM on a driver with no renderable three-channel + // format becomes GL_RGBA16F, so its Int8x3 shadow is converted to Float x3 and then + // repacked as Float x4 with alpha 1.0. + // + // Both scratch buffers belong to the caller so they outlive the returned pointer; the + // return value is `data` itself whenever neither conversion applies, which is what the + // sub-rect upload fast path tests for. + static const void* PrepareFallbackUpload(TextureInternalFormat format, TextureTarget target, + const IntVec3& texelSize, const void* data, SizeT byteSize, + GLenum uploadType, Vector& convertedData, + Vector& widenedData) { + const void* uploadData = + PrepareNormFloatFallbackUpload(format, texelSize, data, byteSize, uploadType, convertedData); + // The component-count switch first: it rules out every format that cannot be widened + // (which is nearly all of them, including GL_RGBA8) without touching the capability + // cache, so an ordinary atlas upload does not pay for a per-level cache lookup. + const Uint componentCount = GetWidenableClientComponentCount(format); + if (componentCount == 0 || !TextureImpl::BackendTextureFormatAddsAlpha(format, target)) { + return uploadData; + } + // The type conversion above rewrites the level into `convertedData` at four bytes per + // component while keeping the component count, so the widening's source size is that + // buffer's, not the shadow's. + const SizeT uploadByteSize = (!convertedData.empty() && uploadData == convertedData.data()) + ? convertedData.size() * sizeof(Float) + : byteSize; + return PrepareChannelWidenedUpload(componentCount, texelSize, uploadData, uploadByteSize, uploadType, + widenedData, IsIntegerWidenableFormat(format)); + } + + // One channel of a packed r11f_g11f_b10f word as a float. The two 11-bit channels are + // e5m6 and the 10-bit one e5m5 - IEEE-shaped but UNSIGNED, so there is no sign bit to + // read and the exponent bias is the 15 a 5-bit exponent always carries. + static Float DecodePackedUnsignedFloat(Uint32 bits, Uint mantissaBits) { + const Uint32 mantissaScale = 1u << mantissaBits; + const Uint32 mantissa = bits & (mantissaScale - 1u); + const Uint32 exponent = bits >> mantissaBits; + if (exponent == 0u) { + // Subnormal, and zero with it: no implied leading 1, and the exponent is the + // smallest NORMAL one rather than the encoded 0. + return std::ldexp(static_cast(mantissa) / static_cast(mantissaScale), -14); + } + if (exponent == 31u) { + return mantissa == 0u ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + } + return std::ldexp(1.0f + static_cast(mantissa) / static_cast(mantissaScale), + static_cast(exponent) - 15); + } + + // The r11f_g11f_b10f shadow decoded into the GL_RGBA / GL_FLOAT level its GL_RGBA16F + // carrier is uploaded as. Alpha is the 1 GL defines for a format that has no alpha + // channel, which is the same constant the shader-side mask writes, so a texel this + // function produced and a texel an imageStore produced are indistinguishable. + // + // Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: + // the driver reads a full width*height*depth*4 floats for the transfer it was handed. + static const void* PreparePackedFloatWidenedUpload(const IntVec3& texelSize, const void* data, + SizeT byteSize, Vector& widenedData) { + constexpr SizeT kSourceTexelBytes = sizeof(Uint32); + if (data == nullptr || byteSize < kSourceTexelBytes) { + return data; + } + const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(std::max(texelSize.z(), 1)); + if (texelCount == 0) { + return data; + } + const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes); + + widenedData.assign(texelCount * 4u * sizeof(Float), 0); + const auto* src = static_cast(data); + auto* dst = reinterpret_cast(widenedData.data()); + for (SizeT i = 0; i < texelCount; ++i, dst += 4) { + Float rgb[3] = {0.0f, 0.0f, 0.0f}; + if (i < copyTexelCount) { + Uint32 packed = 0; + // Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte + // buffer with no alignment promise of its own. + Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed)); + rgb[0] = DecodePackedUnsignedFloat(packed & 0x7FFu, 6u); + rgb[1] = DecodePackedUnsignedFloat((packed >> 11u) & 0x7FFu, 6u); + rgb[2] = DecodePackedUnsignedFloat((packed >> 22u) & 0x3FFu, 5u); + } + dst[0] = rgb[0]; + dst[1] = rgb[1]; + dst[2] = rgb[2]; + dst[3] = 1.0f; + } + return widenedData.data(); + } + + // The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its + // GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST + // component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19, + // blue 20-29 and alpha 30-31. + // + // The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an + // rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side + // unpack divides out. Neither is scaled here - the carrier holds the format's own bits. + // + // Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the + // driver reads a full width*height*depth*4 shorts for the transfer it was handed. + const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, + SizeT byteSize, Vector& widenedData) { + constexpr SizeT kSourceTexelBytes = sizeof(Uint32); + if (data == nullptr || byteSize < kSourceTexelBytes) { + return data; + } + const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(std::max(texelSize.z(), 1)); + if (texelCount == 0) { + return data; + } + const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes); + + widenedData.assign(texelCount * 4u * sizeof(Uint16), 0); + const auto* src = static_cast(data); + auto* dst = reinterpret_cast(widenedData.data()); + for (SizeT i = 0; i < texelCount; ++i, dst += 4) { + Uint32 packed = 0; + if (i < copyTexelCount) { + // Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte + // buffer with no alignment promise of its own. + Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed)); + } + dst[0] = static_cast(packed & 0x3FFu); + dst[1] = static_cast((packed >> 10u) & 0x3FFu); + dst[2] = static_cast((packed >> 20u) & 0x3FFu); + dst[3] = static_cast((packed >> 30u) & 0x3u); + } + return widenedData.data(); + } + + // The transfer half of the image-format widening: an image-bindable texture whose ES + // storage was widened to a core carrier is described to the driver as a four-component + // transfer, so its narrower client data has to be repacked the same way the three-channel + // colour-renderable widening repacks its own. + // + // Three shapes, because the carriers come in three kinds. Most of them keep the frontend + // format's component TYPE and only add channels, so padding the shadow out to four + // components is the whole conversion. The two PACKED formats do not: their shadow is one + // 32-bit word per texel, so the word has to be split - into four floats for + // r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such + // a word as components of the carrier's type - what the repack below would do - takes + // twelve or sixteen bytes from a four-byte texel and shears the level, which is what the + // allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel + // the upload got wrong). + // + // Composes with PrepareFallbackUpload rather than replacing it, and the composition is a + // no-op by construction: none of the widened formats is one GetWidenableClientComponentCount + // reports a count for, and the SNORM shadow-to-float conversion only fires for a GL_FLOAT + // transfer type, which the widened triple never picks for the two SNORM8 formats. So the + // shadow reaches this untouched and one conversion is all that runs. + static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening, + const IntVec3& texelSize, const void* data, SizeT byteSize, + Vector& widenedData) { + if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) { + return data; + } + switch (widening.SourceEncoding) { + case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f: + return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData); + case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev: + return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData); + case TextureImpl::ImageWidenSourceEncoding::Components: + break; + } + if (widening.SourceChannels == 4) { + return data; + } + return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type, + widenedData, widening.IntegerData, + widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u); + } + + // Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose + // with the widened carrier's. Deliberately unconditional on anything but the widening + // itself: whatever renderability fallback the triple carried, an image the driver refuses + // to bind is useless, so the image constraint wins. + static void ApplyImageBindableStorageWidening(const TextureImpl::ImageBindableStorageWidening& widening, + GLenum* inOutInternalFormat, GLenum* inOutFormat, + GLenum* inOutType) { + if (!widening) { + return; + } + if (inOutInternalFormat) *inOutInternalFormat = widening.InternalFormat; + if (inOutFormat) *inOutFormat = widening.Format; + if (inOutType) *inOutType = widening.Type; + } + + // RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE + // leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is + // implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors, + // drifting mid-range texels one 5-bit step down and failing the KHR-GL3x + // pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into + // the packed 16-bit client type with round-to-nearest instead - that recovers the original + // 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the + // driver stores them verbatim with no requantization left to its discretion. 4-bit formats + // (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding. + // Always retargets *inOutType for these formats (even for null data) so every upload of a + // level uses the same client type. + static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize, + const void* data, SizeT byteSize, GLenum* inOutType, + Vector& packedData) { + if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) { + return data; + } + // With the storage widened to 8-bit-per-channel (the packed16 field-order quirk) + // there is no driver requantization left for the repack to pre-empt - the shadow's + // UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg + // retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all. + if (TextureImpl::UsesWidenedPacked16NormStorage(format)) { + return data; + } + const Bool hasAlpha = format == TextureInternalFormat::RGB5A1; + const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5; + // Idempotent across a region's level loop: glType is shared, so later levels arrive with + // the already-retargeted packed type and must still be converted. + if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) { + return data; + } + *inOutType = packedType; + if (data == nullptr || byteSize == 0) { + return data; + } + const SizeT srcPixelBytes = hasAlpha ? 4 : 3; + const SizeT texelCount = std::min(static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(std::max(texelSize.z(), 1)), + byteSize / srcPixelBytes); + packedData.resize(texelCount * sizeof(Uint16)); + const Uint8* src = static_cast(data); + auto* dst = reinterpret_cast(packedData.data()); + for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) { + const Uint32 r = (static_cast(src[0]) * 31u + 127u) / 255u; + const Uint32 b = (static_cast(src[2]) * 31u + 127u) / 255u; + if (hasAlpha) { + const Uint32 g = (static_cast(src[1]) * 31u + 127u) / 255u; + dst[i] = static_cast((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u)); + } else { + const Uint32 g = (static_cast(src[1]) * 63u + 127u) / 255u; + dst[i] = static_cast((r << 11) | (g << 5) | b); + } + } + return packedData.data(); + } + + // "Some level of this texture holds an image", which is all the sync gate below actually + // needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also + // answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with + // undefined lower levels - but such a texture still has to be uploaded, or the level that + // IS defined never reaches the driver at all. + static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) { + const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject); + if (mipmapObject == nullptr) return false; + const auto levelCount = mipmapObject->GetMipmapLevelCount(); + for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) { + for (Uint level = 0; level < levelCount; ++level) { + const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level); + if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) { + return true; + } + } + } + return false; + } +// True while this twin has NO noted handle, i.e. it is the monolith-glue half and the frontend +// object below is the only thing that can answer. A macro and not a member function because the +// pull build has no such member at all and this has to fold to a constant there (D-P: the pull +// build's preprocessed text may not gain a call). +#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (MG_Pipe::MGPipeHandleIsNull((twin).PushedSyncHandle())) +// The ~20 GetExternalIndex() reads this family makes in LOG LINES (scout G-S2-4). The pull +// expansion is the pre-P5e expression with one pair of parentheses around the receiver. +#define MGB_TEXTURE_DIAG_NAME(rec, obj) (TextureDiagName((obj), (rec))) +// The storage KIND, from the descriptor whenever one was resolved - the backstop arm of the +// storage switch used to ask the frontend object for a value it had just read from the record. +#define MGB_TEXTURE_STORAGE_KIND(rec, obj) \ + ((rec) != nullptr ? static_cast((rec)->Desc.StorageKind) : (obj)->GetStorageType()) + + // P4a (D-D5), the dirty-ownership inversion, applied at the two places the upload loops + // below actually ask: "does this (uploadTarget, level) owe an upload" and "it has been + // uploaded". ARCHITECTURE.md §6 puts the rect model and the emission cursor on the + // CLIENT, which clears its own flags AT EMISSION - so on the handle arm the frontend's + // IsStorageDirty is already false for every level the applier accepted, and asking it + // would upload nothing at all. The applier's pending set is what these two read instead; + // it is server-side state, it survives this function's bail arms (an incomplete texture + // returns early, a multisample target refreshes and skips), and an entry is consumed + // ONLY where the level actually uploaded. + // + // THE UPLOAD-TARGET ENCODING is static_cast(TextureUploadTarget), which is what + // package B must emit into MGPSubData::Target's cube-face half; it is recorded as a + // deviation because the contract left it unstated. + // + // MACROS AND NOT LAMBDAS, and that is a G1 requirement rather than a style choice: + // SyncMipmapsToBackend already holds nine ErrorLopper lambdas whose mangled names are + // ...::$_N BY POSITION, so inserting two more renumbered every one of them - measured as + // eight symbols added, eight removed and four std::function thunks resized in the PULL + // build, whose symbol set P4a may not move by one byte (D-P). A macro expands to the + // pre-P4a expression exactly when MOBILEGL_PIPE_PUSH is off, so the pull build's + // preprocessed text, and therefore its object code, is unchanged. Both are #undef'd + // immediately after the function. +// P5c (tx): THE READS THE FOUR UPLOAD ARMS MAKE, re-sourced. With an active transport the +// apply thread may not name the client's TextureObjectMipmap at all (rule E), so on the +// handle arm: +// +// * the level TEXELS come from the server's staged-texture store, adopted at apply time +// (MGB_LEVEL_TEXELS - Fatal{StageSnapshotTooNarrow} when no record covered the level, +// which is the data-correctness refusal of the texture half); +// * the per-level EXTENT and DEFINED-NESS come from the same store (fed by the sub-data +// adoption and the respecify hook; {0,0,0} for a level nothing defined, which is exactly +// GetMipmapTexelSize's answer for one); +// * the level BYTE SIZE is the adopted run's length; +// * the texture TARGET and the UPLOAD-TARGET list come from the descriptor. +// +// Every macro keeps the P4a discipline: the non-disaggregated expansion is the original +// frontend read, character for character modulo one pair of parentheses, so the pull and +// push builds compile exactly what they compiled before tx, and every disaggregated arm +// falls back to the frontend read when there is no active transport (the legacy arm, and +// monolith). MGB_STAGED_TEXTURE_LIVE is the runtime discriminator; pushedStorage/pushedRes +// are the function's own locals. All are #undef'd with the rest after the function. +#define MGB_STAGED_TEXTURE_LIVE \ + (pushedStorage != nullptr && MG_Record::ServerStagedTexture().Holds()) +#define MGB_TEXTURE_TARGET(obj) \ + (MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedTextureTargetForPipeTarget(pushedStorage->Desc.Target) \ + : (obj)->GetTarget()) +#define MGB_UPLOAD_TARGETS(obj) \ + (MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedUploadTargetsForPipeTarget(pushedStorage->Desc.Target) \ + : (obj)->GetUploadTargets()) +#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) \ + (MGB_STAGED_TEXTURE_LIVE \ + ? MG_Record::ServerStagedTexture().LevelExtentOrUndefined( \ + MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ + static_cast(lvl)) \ + : (obj)->GetMipmapTexelSize(tgt, lvl)) +#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) \ + (MGB_STAGED_TEXTURE_LIVE \ + ? MG_Record::ServerStagedTexture().LevelByteSize( \ + MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ + static_cast(lvl)) \ + : (obj)->GetMipmapByteSize(tgt, lvl)) +#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) \ + (MGB_STAGED_TEXTURE_LIVE \ + ? MG_Record::ServerStagedTexture().RequireLevelBytes( \ + MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ + static_cast(lvl), site) \ + : (obj)->MapMipmapData(tgt, lvl)) + +// P5c (tx): the disaggregated pair adds ONE term and ONE clear to the P4a shapes - the +// staged-texture store's GPU-dirty mark (T5: a level the GPU generated dirties the SERVER's +// shadow, and the pending set cannot see it). The mark is never set on Espryt today - its +// GenerateMipmap fills the levels on the driver - so the term is inert here and is the +// contract-shaped answer (§2.2's last row) rather than a hot-path cost: one m_any acquire +// load when the store is empty. +#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \ + (pushedStorage != nullptr \ + ? (FindPipeTextureUpload(*pushedStorage, static_cast(tgt), static_cast(lvl)) != nullptr || \ + MG_Record::ServerStagedTexture().IsLevelGpuDirty( \ + MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ + static_cast(lvl))) \ + : (obj)->IsStorageDirty(tgt, lvl)) +// Re-resolves the record itself, so it is safe after any amount of driver work - and it +// invalidates any PendingUpload* taken earlier for THIS texture, which is why every such pointer +// is used and dropped inside one level's iteration. +#define MGB_LEVEL_UPLOAD_DONE(obj, tgt, lvl) \ + do { \ + if (pushedStorage != nullptr) { \ + ConsumePipeTextureUpload(pushedRes, static_cast(tgt), static_cast(lvl)); \ + if (MGB_STAGED_TEXTURE_LIVE) { \ + MG_Record::ServerStagedTexture().MarkLevelGpuDirty( \ + MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast(tgt), \ + static_cast(lvl), false); \ + } \ + } else { \ + (obj)->MarkStorageDirty(tgt, lvl, false); \ + } \ + } while (0) + + // THE STORAGE SHAPE, and on the handle arm it is the DESCRIPTOR's (C.3's d2: "the + // m_prevTextureInfo probe re-keyed onto the resource record's Serial AND Desc"; review + // M-2). v1 moved the parameters and the uploads and left the allocation reading the + // frontend object, which put TWO AUTHORITIES on one value inside one twin - + // SyncTextureParamsToBackend already answers MGB_TEXPARAM_FORMAT from + // Desc.InternalFormat while this function allocated from GetFormat(). In monolith they + // agree, so no lane can expose the split; under split the descriptor is all there is. + // + // WHAT THE DESCRIPTOR ANSWERS: internal format, base extent, level count, sample count, + // fixed sample locations, immutability, storage kind and the buffer-texture window. What + // it does NOT answer, and what still comes from the frontend shadow, is the PER-LEVEL + // extent and the level BYTES - MGPResourceDesc carries one base extent, the per-level + // sizes are derived from it, and the texels themselves ride on sub-data. A level the + // descriptor's Levels claims and the shadow has not defined reads back {0,0,0} and the + // per-level loops skip it, which is the same arm they take for a sparse chain today. + // + // AND Desc.HasDefinedContent IS DELIBERATELY NOT READ HERE (review N-8, and the + // enumeration above exists to be exhaustive, so the omission is stated rather than + // left to be noticed). c0e made it storage-defining and the BUFFER family reads it + // three times, because a buffer's content is ONE fact about the whole resource. A + // texture's is not: content is per (uploadTarget, level) and its authority is the + // pending set (D-D5), which is per level and survives a bail. A whole-resource + // "content is undefined" would say nothing the per-level loops do not already answer + // by finding no level to upload, and acting on it would mean throwing away levels the + // set still owes. The field stays a respecify CLASSIFIER (it moves the descriptor, so + // wire refuses to call such a respecify metadata-only) and nothing more on this arm. + // + // MACROS, NOT LOCALS, FOR THE SAME REASON AS THE PAIR ABOVE (DV-9, and it was measured): + // routing these through locals resized SyncTextureParamsToBackend by +9 bytes in the + // PULL build and P4a's admitted-resize set is EMPTY. Each takes the object it would + // otherwise have been spelled on, so the pull expansion is the pre-P4a expression with + // one pair of parentheses around the receiver. All are #undef'd with the pair above. +#define MGB_STORAGE_FORMAT(obj) \ + (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.InternalFormat) \ + : (obj)->GetFormat()) +#define MGB_STORAGE_BASE_SIZE(obj) \ + (pushedStorage != nullptr ? IntVec3{static_cast(pushedStorage->Desc.Width), \ + static_cast(pushedStorage->Desc.Height), \ + static_cast(pushedStorage->Desc.Depth)} \ + : (obj)->GetBaseSize()) +#define MGB_STORAGE_LEVELS(obj) \ + (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.Levels) : (obj)->GetMipmapLevelCount()) +#define MGB_STORAGE_SAMPLES(obj) \ + (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.Samples) : (obj)->GetSamples()) +#define MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(obj) \ + (pushedStorage != nullptr ? pushedStorage->Desc.FixedSampleLocations != 0 : (obj)->HasFixedSampleLocations()) +#define MGB_STORAGE_IMMUTABLE(obj) \ + (pushedStorage != nullptr ? pushedStorage->Desc.Immutable != 0 : (obj)->IsImmutable()) +#define MGB_STORAGE_KIND(obj) \ + (pushedStorage != nullptr ? static_cast(pushedStorage->Desc.StorageKind) \ + : (obj)->GetStorageType()) + + void BackendTextureObject::SyncMipmapsToBackend( + const SharedPtr& stateTextureObject) { + // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; + // see the twin note in SyncBuiltinSamplerToBackend. P5e (fix1): "a handle was noted" + // is not on its own the statement that the record arm will be SELECTED - the view + // test below and RequireImageBindableStorage's transition are transport-gated - so + // the check is the one shared refusal and a noted handle on an arm that cannot serve + // it aborts by name rather than falling into the frontend reads. + if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncMipmapsToBackend")) { + MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); + return; + } + + // P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads. + // P5e (tx2) hoists the resolution to the TOP of the function, because the view test + // below needs it before anything else does; nothing else about it moves. + // + // The record pointer stays valid for the whole function: only a create/respecify for + // a NEW slot grows MGPipeApplierState::TextureResources, and nothing this function + // calls emits one. Pointers INTO record->PendingUploads do not - the consume is a + // swap-and-pop - so every one of them is taken, used and dropped inside one level's + // iteration, and never held across a consume. + const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr; + MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle; + pushedStorage = ResolveOwnRecord(stateTextureObject); + pushedRes = !MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle) + ? m_pushedSyncHandle + : (pushedStorage != nullptr ? pushedStorage->Desc.Resource + : MG_Pipe::kMGPipeNullHandle); + if (pushedStorage == nullptr) { + MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " + "storage and uploads cannot be driven from the pushed descriptor " + "(handle {%u, %u})", + TextureDiagName(stateTextureObject, pushedStorage), pushedRes.Slot, + pushedRes.Gen); + return; + } +#if MOBILEGL_BUILD_DISAGGREGATED + // SHARED IMAGES: ahead of the view test and of the staged-store and serial gates below, + // none of which knows the texels belong to an image - the staged store holds none of + // them, and the storage path would allocate fresh, empty storage over the image. + if (pushedStorage != nullptr && (pushedStorage->SharedImageId != 0 || m_sharedImageId != 0) && + SyncSharedImageStorage(*pushedStorage)) { + // EVERY use, not only the attach (the draw-clean gates never pass a bound image): + // a write that landed since the last one is waited for on this context's GPU + // before the command this sync is for, and the session's frame notes the read. + if (m_sharedImage != nullptr) SharedImageImpl::AcquireForSampling(m_sharedImage); + ReplaySharedImageUploads(*pushedStorage); + // A YUV image is converted into the name at the frame's first use. + if (m_sharedImage != nullptr && SharedImageImpl::IsYuv(m_sharedImage)) + SharedImageImpl::ConvertYuvForSampling(m_sharedImage, GetBackendTextureId(), + pushedStorage->Desc.Width, pushedStorage->Desc.Height); + return; + } +#endif + // P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE + // RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it + // had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage + // owner BY HANDLE, so both the question and its answer are server-owned. + // + // Ruling 1's arm: the push-monolith build keeps the frontend test below token for + // token, because there the two answers are the same answer and the verify comparator + // needs the frontend arm. + if (pushedStorage != nullptr && + !MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) { + SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject); + return; + } + +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + + // P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads. + // Resolved once, here, because three things below want it - the cheap gate, the + // per-level dirty questions and the union box / rect list / strides the staging + // planner takes its shape from. + // + // The record pointer stays valid for the whole function: only a create/respecify + // for a NEW slot grows MGPipeApplierState::TextureResources, and nothing this + // function calls emits one. Pointers INTO record->PendingUploads do not - the + // consume is a swap-and-pop - so every one of them is taken, used and dropped + // inside one level's iteration, and never held across a consume. + if (pushedStorage != nullptr) { + // THE METADATA RESPECIFY, HONOURED (ID-18 M4, review M-2). BindMask and + // ImageBindableHint are STICKY facts the client discovers AFTER allocation - a + // texture first bound as a shader image - and an IMMUTABLE texture never has a + // later redefinition to carry them, so they arrive on a respecify that restates + // the storage the resource already has. The applier classifies such a record as a + // metadata update: no reallocation ack, no pending-upload clear, Serial moved. + // THE TWIN'S HALF IS TO RE-DERIVE ITS STORAGE FLAGS FROM THE NEW MASK AND + // RECREATE ONLY WHERE THE BACKEND NEEDS THE FLAG AT CREATION - and image + // bindability is exactly such a flag on this backend: the carrier may be channel- + // widened, which is a different allocation, not a different binding. + // + // RequireImageBindableStorage IS that path and it is idempotent, so this is the + // whole of the re-derivation: it sets the sticky flag, clears m_isInitialized so + // the storage below is regenerated, re-arms every defined level in BOTH dirty + // models (see its own comment) so the widened carrier is replayed rather than + // allocated empty, and forces the parameter resync the widening's swizzle needs. + // Arriving on the CREATE instead - the common case once B publishes the hint - + // costs nothing: m_isInitialized is already false and there are no levels to + // replay. + const Bool pushedWantsImageBindableStorage = + pushedStorage->Desc.ImageBindableHint != 0 || + (pushedStorage->Desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0; + if (pushedWantsImageBindableStorage && !m_imageBindableStorageRequired) { + // P5e (tx2), ruling 1's arm: under a transport the re-dirty half is a NAMED + // refusal at its entry (§5.2) instead of a walk of the client's level shadows; + // the push-monolith build keeps the frontend transition exactly as it is. + RequireImageBindableStorageByHandle(pushedRes, *pushedStorage); + } + + // ONE compare replaces the whole cheap-gate trio AND the content version. The + // applier bumps Serial on every respecify and every sub-data it applies to this + // resource, which is exactly the union those four keys covered - without the + // coarse behaviour the sampling-resolution generation had, where any texture's + // shape churn re-opened every other texture's gate. + // + // The pending set is the second half and it is not redundant: a previous sync + // may have BAILED (an incomplete texture returns early, a multisample target + // refreshes and skips) with the serial already stamped, and the set is what + // survives that. Empty means every level this record ever declared has been + // consumed by a sync that actually uploaded it. + if (m_isInitialized && m_syncedResourceSerial != 0 && + m_syncedResourceSerial == pushedStorage->Serial && + pushedStorage->PendingUploads.empty()) { + return; + } + } + + // First-level clean gate (see the member comment): three version compares and no + // virtual shape walk. Every mutation the slower probe below would catch bumps one of + // the keys - shape via the context's sampling-resolution generation (coarse: any + // texture's shape churn re-opens every gate, which only costs a fall-through to the + // probe), CPU pixels via the content version, samples/fixed-locations via the params + // version - and backend-side storage resets clear m_isInitialized. Restricted to + // Mipmap storage like the probe fast path: a buffer texture's backing store can move + // without any of these keys noticing. + // + // LEGACY ARM ONLY from P4a on: the memos it reads are the pre-handle ones and the + // record above answers the same question in one compare. + if (pushedStorage == nullptr) + if (m_isInitialized && m_syncedShapeContextId != 0 && MG_Pipe::gPipeInputs->IsLive() && + m_syncedShapeContextId == MG_Pipe::gPipeInputs->GetTextureContextId() && + m_syncedShapeGeneration == MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration() && + m_syncedContentVersion == stateTextureObject->GetContentVersion() && + m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() && + stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { + return; + } + + MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId, + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + + GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject)); + auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject); + MGLOG_D(" Texture target for syncing is %s", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + if (!IsSupportedTextureTarget(targetInternal)) { + MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + return; + } + + // The texture needs to be regenerated completely with glTexImage* calls if: + // 1. Not initialized + // 2. InternalFormat changed + // 3. Size changed + // 4. Mipmap levels changed + + // IsComplete() is the sampling predicate, and it calls a chain whose lower levels are + // undefined incomplete - which is what a top-down build (upload level N, then level 0) + // and ARB_clear_texture's conformance cases both produce. Bailing out on that shape + // left the backend name with no levels whatsoever, so the level that WAS defined could + // never be sampled or read back. Sync whenever some level holds an image; the per-level + // loops below skip the degenerate ones individually. + // P5c (gt, CONTRACT-P5C §6 layer 1): on the staged arm the same question is answered + // out of the SERVER's staged-texture store, never out of the frontend object - the + // store's Defined-ness (fed by the respecify hook and the sub-data adoption) IS "some + // level holds an image", and IsComplete()'s sampling half is redundant with it: a + // complete mipmap chain has a defined level 0, and the gate's whole job is to let an + // incomplete-but-partly-defined chain through. The one divergence is deliberate and + // stated: a chain whose EVERY level is 0x0 is complete-by-quirk on the frontend (the + // "0x0 in last level" relaxation, TextureObject.cpp) and undefined here - and syncing + // it would upload nothing either way, because every per-level loop below skips a + // {0,0,0} level individually. A BUFFER texture never has staged levels, so its gate + // is the descriptor's format - exactly what TextureObjectBase::IsComplete() reduces + // to for that storage kind. + if (MGB_STAGED_TEXTURE_LIVE) { + const auto& stagedDesc = pushedStorage->Desc; + Bool anyDefined = false; + if (static_cast(stagedDesc.StorageKind) == TextureStorageType::Buffer) { + // A buffer texture has no staged levels; its gate is the descriptor's + // format, which is what TextureObjectBase::IsComplete() reduces to here. + anyDefined = static_cast(stagedDesc.InternalFormat) != + TextureInternalFormat::Unknown; + } else { + auto& stagedStore = MG_Record::ServerStagedTexture(); + const Uint64 stagedKey = MG_Record::StagedTextureStore::KeyForHandle(pushedRes); + for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(stagedDesc.Target)) { + for (Uint32 level = 0; level < stagedDesc.Levels; ++level) { + if (stagedStore.IsLevelDefined(stagedKey, static_cast(uploadTarget), + static_cast(level))) { + anyDefined = true; + break; + } + } + if (anyDefined) break; + } + } + if (!anyDefined) { + MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + return; + } + } else + if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) { + MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", + stateTextureObject->GetExternalIndex()); + return; + } + + // Fast path: a fully-synced mipmap texture is the common per-draw case. + // SyncNeccessaryTextures re-syncs every bound texture each draw, and the + // scratch Bind below targets the temp unit - which sequential distinct + // textures thrash, forcing a real glBindTexture per texture per draw. When + // nothing needs uploading, skip the bind + upload machinery entirely; + // BindCurrentTextures() re-establishes the real sampling bindings regardless. + // The content-version stamp short-circuits before any shape probing: it + // bumps on every CPU-side pixel mutation, so an unchanged stamp plus an + // unchanged shape means no level can be dirty. Shape stays a separate + // compare because a NULL-data glTexImage changes it without touching the + // content version. + // + // LEGACY ARM ONLY from P4a on: on the handle arm the record's Serial plus an empty + // pending set is the same statement in one compare, made above and BEFORE the + // IsComplete()/shape walk rather than after it - so this probe would only re-derive + // an answer already given, out of the very frontend state the arm exists to stop + // reading. + if (pushedStorage == nullptr) + if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap && + m_syncedContentVersion != 0 && + m_syncedContentVersion == stateTextureObject->GetContentVersion()) { + auto* mipmapObject = + static_cast(stateTextureObject.get()); + const auto probeBaseSize = stateTextureObject->GetBaseSize(); + StateTextureBasicInfo probe = {stateTextureObject->GetFormat(), + static_cast(probeBaseSize.x()), + static_cast(probeBaseSize.y()), + static_cast(probeBaseSize.z()), + static_cast(mipmapObject->GetMipmapLevelCount()), + 0, + stateTextureObject->GetSamples(), + stateTextureObject->HasFixedSampleLocations()}; + if (probe == m_prevTextureInfo) { + MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.", + m_backendTextureId); + // The probe just proved "fully synced" from the real state, so the cheap + // gate may be (re)stamped here: the coarse generation only ever goes stale + // from OTHER textures' churn, and this draw re-validated this one. + if (MG_Pipe::gPipeInputs->IsLive()) { + m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId(); + m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); + m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); + } + return; + } + } + + Bind(target); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + const auto baseSize = MGB_STORAGE_BASE_SIZE(stateTextureObject); + StateTextureBasicInfo currentTextureInfo = {MGB_STORAGE_FORMAT(stateTextureObject), + static_cast(baseSize.x()), + static_cast(baseSize.y()), + static_cast(baseSize.z()), + 0, + 0, + MGB_STORAGE_SAMPLES(stateTextureObject), + MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject)}; + switch (MGB_STORAGE_KIND(stateTextureObject)) { + case TextureStorageType::Mipmap: { + auto* textureMipmapObject = + static_cast(stateTextureObject.get()); + const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject); + currentTextureInfo.mipmapLevels = mipmapCount; + + Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo); + if (needsRegeneration && m_backendStorageImmutable) { + RecreateBackendTexture(); + Bind(target); + } + + // Only a texture that is actually image-bound pays for the widening: it doubles + // or quadruples the storage, and RequireImageBindableStorage is sticky, so a + // texture that is merely sampled keeps its narrow format for life. See + // TextureImpl::GetImageBindableStorageWidening for what widens and why. + const TextureImpl::ImageBindableStorageWidening imageWidening = + m_imageBindableStorageRequired + ? TextureImpl::GetImageBindableStorageWidening(MGB_STORAGE_FORMAT(textureMipmapObject)) + : TextureImpl::ImageBindableStorageWidening{}; + + const Bool canAppendMipmaps = + m_isInitialized && + !m_imageBindableStorageRequired && + !MGB_STORAGE_IMMUTABLE(stateTextureObject) && + currentTextureInfo.internalFormat == m_prevTextureInfo.internalFormat && + currentTextureInfo.width == m_prevTextureInfo.width && + currentTextureInfo.height == m_prevTextureInfo.height && + currentTextureInfo.depth == m_prevTextureInfo.depth && + currentTextureInfo.bufferExternalIndex == m_prevTextureInfo.bufferExternalIndex && + currentTextureInfo.samples == m_prevTextureInfo.samples && + currentTextureInfo.fixedSampleLocations == m_prevTextureInfo.fixedSampleLocations && + currentTextureInfo.mipmapLevels > m_prevTextureInfo.mipmapLevels && + !TextureImpl::IsMultisampleTextureTarget(targetInternal); + + MGLOG_D("%s: Got texture info: %dx%dx%d, mips %d, format %s", __func__, baseSize.x(), baseSize.y(), + baseSize.z(), mipmapCount, + MG_Util::ConvertTextureInternalFormatToString(MGB_STORAGE_FORMAT(textureMipmapObject)).c_str()); + + if (canAppendMipmaps) { + MGLOG_D("Texture mip count increased for backend ID %u, appending levels %zu..%zu", + m_backendTextureId, m_prevTextureInfo.mipmapLevels, mipmapCount - 1); + + GLenum glInternalFormat, glType, glFormat; + TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat, + &glFormat, &glType, targetInternal); + + const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); + ScopedDefaultUnpackState unpackState; + for (auto& uploadTarget : uploadTargets) { + for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) { + auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); + // A level the application never defined reads back as {0, 0, 0}; now that a + // sparse chain is synced rather than skipped whole, leave those undefined on + // the driver instead of giving the name a 0x0 image at that index. + if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) { + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + continue; + } + auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); + bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + auto* pData = (levelDirty && levelByteSize != 0) + ? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, + "append-mips") + : nullptr; + Vector convertedUploadData; + Vector widenedUploadData; + const void* uploadData = PrepareFallbackUpload( + MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData, + levelByteSize, glType, convertedUploadData, widenedUploadData); + Vector packedUploadData; + uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize, + uploadData, levelByteSize, &glType, packedUploadData); + + DebugImpl::ErrorLopper::Clear(); + BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + const IntVec3 uploadSize = + GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), levelTexelSize); + switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) { + case TextureTarget::Texture2D: + case TextureTarget::TextureCubeMap: + g_GLESFuncs.glTexImage2D( + glUploadTarget, static_cast(level), (GLint)glInternalFormat, + static_cast(uploadSize.x()), static_cast(uploadSize.y()), + 0, glFormat, glType, uploadData); + break; + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: + // ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly + // like a 2D array whose depth is 6 * the cube count. + case TextureTarget::TextureCubeMapArray: + g_GLESFuncs.glTexImage3D( + glUploadTarget, static_cast(level), (GLint)glInternalFormat, + static_cast(uploadSize.x()), static_cast(uploadSize.y()), + static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); + break; + default: + MGLOG_E_ONCE("Unhandled texture target %s", + MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str()); + break; + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, + glUploadTarget, glInternalFormat, glFormat, glType, + pData](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, format=%s, " + "type=%s, pixels=%p", + func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), + MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(), + MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), + MG_Util::ConvertGLEnumToString(glFormat).c_str(), + MG_Util::ConvertGLEnumToString(glType).c_str(), pData); + }); + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + } + } + needsRegeneration = false; + } + + if (needsRegeneration) { + MGLOG_D("Texture state changed significantly or not initialized, regenerating texture with ID: %u", + m_backendTextureId); + // A REDEFINITION IN PLACE TAKES THE SAME GENERATION A RE-MINT TAKES (P4a fable + // seam F-3, the pre-handle half). Mutable driver storage is redefined on the + // SAME id below, so unlike RecreateBackendTexture nothing moves the FBO twins' + // memo: the frontend framebuffer versions do not see a texture's respecify + // and the id did not change. An attached texture whose format moved to one + // with the same carrier (GL_SRGB8 -> GL_SRGB8_ALPHA8 on a driver that widens + // the first) therefore kept the framebuffer's alpha-widening mask, and every + // draw into it stayed masked. The first definition is not a redefinition and + // bumps nothing; a redefinition that went through RecreateBackendTexture above + // has already bumped. + // PUSH BUILDS ONLY. This is Espryt code the pull build would share, and G1 + // keeps the pull library byte-identical to the P4a baseline (the bump resized + // this function and the renderbuffer twin's SyncToBackend: 0/0/2/0). So the + // pull build carries the pre-P4a hole until these lines land on dev on their + // own; every arm of a push build has the fix. + if (m_isInitialized && !m_backendStorageImmutable) { + ++FramebufferImpl::g_attachmentBackendIdGeneration; + } + + // Regenerate all mipmap levels + GLenum glInternalFormat, glType, glFormat; + TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat, + &glFormat, &glType, targetInternal); + ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); + + const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); + if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { + DebugImpl::ErrorLopper::Clear(); + BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + // The frontend validates against the count MobileGL advertises, which can + // exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4, + // GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call: + // stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and + // framebuffer completeness still report what the application asked for. + const auto backendSamples = static_cast(ClampSamplesToBackendSupport( + GetFormatCapabilityTargetIndex(targetInternal), MGB_STORAGE_FORMAT(textureMipmapObject), + glFormat, static_cast(MGB_STORAGE_SAMPLES(stateTextureObject)))); + // ES 3.1 8.19 requires width/height (and depth, for the array target) >= 1, + // so a degenerate size has nothing to allocate and must not reach the + // driver. The frontend deallocates such an image rather than defining it + // (GL 4.6 core 8.8), so this is belt and braces for any path that still + // syncs one. + const Bool hasAllocatableSize = + baseSize.x() >= 1 && baseSize.y() >= 1 && + (targetInternal != TextureTarget::Texture2DMultisampleArray || baseSize.z() >= 1); + if (!hasAllocatableSize) { + MGLOG_D("Skipping multisample storage for texture %u: degenerate size (%d, %d, %d)", + m_backendTextureId, baseSize.x(), baseSize.y(), baseSize.z()); + } else { + switch (targetInternal) { + case TextureTarget::Texture2DMultisample: + g_GLESFuncs.glTexStorage2DMultisample( + target, backendSamples, glInternalFormat, + static_cast(baseSize.x()), static_cast(baseSize.y()), + MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE); + break; + case TextureTarget::Texture2DMultisampleArray: + g_GLESFuncs.glTexStorage3DMultisample( + target, backendSamples, glInternalFormat, + static_cast(baseSize.x()), static_cast(baseSize.y()), + static_cast(baseSize.z()), + MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE); + break; + default: + MOBILEGL_ASSERT(false, "Unexpected multisample target: %d", + static_cast(targetInternal)); + break; + } + m_backendStorageImmutable = true; + } + // The one storage branch that cleared the ES error queue without ever + // draining it again, so anything this call raised was left for an + // unrelated later query to trip over. Paired with its two siblings now. + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target, + glInternalFormat, backendSamples](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*Multisample: target=%s, internalformat=%s, " + "samples=%d", + func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), + MG_Util::ConvertGLEnumToString(target).c_str(), + MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), + static_cast(backendSamples)); + }); + for (const auto& uploadTarget : uploadTargets) { + for (SizeT level = 0; level < mipmapCount; ++level) { + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + } + } + } else if (MGB_STORAGE_IMMUTABLE(stateTextureObject) || m_imageBindableStorageRequired) { + DebugImpl::ErrorLopper::Clear(); + BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize); + switch (MapToBackendTextureTarget(targetInternal)) { + case TextureTarget::Texture2D: + case TextureTarget::TextureCubeMap: + g_GLESFuncs.glTexStorage2D(target, static_cast(mipmapCount), glInternalFormat, + static_cast(storageSize.x()), + static_cast(storageSize.y())); + break; + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: + case TextureTarget::TextureCubeMapArray: + g_GLESFuncs.glTexStorage3D(target, static_cast(mipmapCount), glInternalFormat, + static_cast(storageSize.x()), + static_cast(storageSize.y()), + static_cast(storageSize.z())); + break; + default: + MGLOG_E_ONCE("Unhandled immutable texture target %s", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + break; + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target, + glInternalFormat](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*: target=%s, internalformat=%s", func, + file, line, MG_Util::ConvertGLEnumToString(err).c_str(), + MG_Util::ConvertGLEnumToString(target).c_str(), + MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); + }); + m_backendStorageImmutable = true; + + ScopedDefaultUnpackState unpackState; + for (auto& uploadTarget : uploadTargets) { + for (SizeT level = 0; level < mipmapCount; ++level) { + auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); + const bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level); + if (levelDirty && levelByteSize != 0) { + auto levelTexelSize = + MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + auto* pData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, + "immutable-regen"); + Vector convertedUploadData; + Vector widenedUploadData; + const void* uploadData = PrepareFallbackUpload( + MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData, + levelByteSize, glType, convertedUploadData, widenedUploadData); + Vector packedUploadData; + uploadData = + PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize, + uploadData, levelByteSize, &glType, packedUploadData); + Vector imageWidenedUploadData; + uploadData = PrepareImageWidenedUpload(imageWidening, levelTexelSize, uploadData, + levelByteSize, imageWidenedUploadData); + + DebugImpl::ErrorLopper::Clear(); + BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + const IntVec3 uploadSize = + GetBackendUploadSize(targetInternal, levelTexelSize); + switch (MapToBackendTextureTarget(targetInternal)) { + case TextureTarget::Texture2D: + case TextureTarget::TextureCubeMap: + g_GLESFuncs.glTexSubImage2D( + glUploadTarget, static_cast(level), 0, 0, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, glType, uploadData); + break; + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: + case TextureTarget::TextureCubeMapArray: + g_GLESFuncs.glTexSubImage3D( + glUploadTarget, static_cast(level), 0, 0, 0, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, glType, uploadData); + break; + default: + break; + } + DebugImpl::ErrorLopper::Loop( + [file = __FILE__, line = __LINE__, func = __func__, glUploadTarget, + glFormat, glType, pData](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s. glTexSubImage*: target=%s, format=%s, " + "type=%s, pixels=%p", + func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), + MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(), + MG_Util::ConvertGLEnumToString(glFormat).c_str(), + MG_Util::ConvertGLEnumToString(glType).c_str(), pData); + }); + } + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + } + } + } else { + m_backendStorageImmutable = false; + ScopedDefaultUnpackState unpackState; + for (auto& uploadTarget : uploadTargets) { + for (SizeT level = 0; level < mipmapCount; ++level) { + auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); + // See the append-mips loop: an undefined level stays undefined on the + // driver rather than becoming a 0x0 image. + if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || + levelTexelSize.z() <= 0) { + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + continue; + } + auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); + bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level); + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + auto* pData = (levelDirty && levelByteSize != 0) + ? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, + "mutable-regen") + : nullptr; + Vector convertedUploadData; + Vector widenedUploadData; + const void* uploadData = PrepareFallbackUpload( + MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData, + levelByteSize, glType, convertedUploadData, widenedUploadData); + Vector packedUploadData; + uploadData = + PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize, + uploadData, levelByteSize, &glType, packedUploadData); + MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, " + "levelDirty = %s", + __func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(), + level, levelTexelSize.x(), levelTexelSize.y(), levelTexelSize.z(), + levelByteSize, pData, levelDirty ? "true" : "false"); + + DebugImpl::ErrorLopper::Clear(); + BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + auto textureTarget = MGB_TEXTURE_TARGET(stateTextureObject); + const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize); + switch (MapToBackendTextureTarget(textureTarget)) { + case TextureTarget::Texture2D: + case TextureTarget::TextureCubeMap: { + g_GLESFuncs.glTexImage2D( + glUploadTarget, static_cast(level), (GLint)glInternalFormat, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), 0, glFormat, glType, uploadData); + break; + } + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: + case TextureTarget::TextureCubeMapArray: { + g_GLESFuncs.glTexImage3D( + glUploadTarget, static_cast(level), (GLint)glInternalFormat, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), 0, glFormat, glType, uploadData); + break; + } + default: { + MGLOG_E_ONCE("Unhandled texture target %s", + MG_Util::ConvertTextureTargetToString(textureTarget).c_str()); + } + } + DebugImpl::ErrorLopper::Loop( + [file = __FILE__, line = __LINE__, func = __func__, glUploadTarget, + glInternalFormat, glFormat, glType, pData](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, " + "format=%s, type=%s, pixels=%p", + func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(), + MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(), + MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), + MG_Util::ConvertGLEnumToString(glFormat).c_str(), + MG_Util::ConvertGLEnumToString(glType).c_str(), pData); + }); + MGLOG_D("Regenerated mipmap level %d for texture with ID: %u", level, + m_backendTextureId); + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + } + } + } + + m_isInitialized = true; + } + + { // Update all dirty mipmap levels + if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { + const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); + for (const auto& uploadTarget : uploadTargets) { + for (SizeT level = 0; level < mipmapCount; ++level) { + if (MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) { + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + } + } + } + break; + } + + const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject); + GLenum glInternalFormat, glType, glFormat; + TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat, + &glFormat, &glType, targetInternal); + // The storage this level is being written into was widened when it was minted + // (see above), so the transfer pair has to describe the carrier here too - ES + // requires glTexSubImage's `format` to match the storage's base internal + // format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION. + ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); + const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject); + ScopedDefaultUnpackState unpackState; + // Per-level scratch of the exact-footprint path, hoisted to reuse capacity. + Vector dirtyFootprint; + Vector stagingBlockOverflow; + for (auto& uploadTarget : uploadTargets) { + for (SizeT level = 0; level < mipmapCount; ++level) { + if (!MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) { + continue; + } + + auto byteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level); + if (byteSize == 0) { + MGLOG_D("Mipmap level %d has no data, skipping update.", level); + continue; + } + + if (level > 0) + MGLOG_D("%s: Updating dirty mip %d for texture ID %u, size: %dx%d, " + "byteSize: %d", + __func__, level, m_backendTextureId, + MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).x(), + MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).y(), byteSize); + + auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + DebugImpl::ErrorLopper::Loop( + [file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, + MG_Util::ConvertGLEnumToString(err).c_str()); + }); + auto texelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level); + const void* mipData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, + "dirty-level"); + Vector convertedUploadData; + Vector widenedUploadData; + const void* uploadData = PrepareFallbackUpload( + MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, texelSize, mipData, byteSize, + glType, convertedUploadData, widenedUploadData); + Vector packedUploadData; + uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), texelSize, + uploadData, byteSize, &glType, packedUploadData); + // Leaves `uploadData` pointing at its own buffer when it fires, with + // texels four components wide where the shadow's are one or two. + Vector imageWidenedUploadData; + uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize, + imageWidenedUploadData); + const IntVec3 uploadSize = + GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), texelSize); + // Sub-rect upload: when only a region of the level changed (a + // 16x16 sprite in a 1024x512 atlas, the per-frame lightmap), upload + // just that region with UNPACK_ROW_LENGTH striding into the level. + // Not an optimization alone: the shadow never sees a GPU write, so + // the texels OUTSIDE the region may be stale there and must not go + // up over what the GPU drew. Every conversion above rewrites the + // whole level texel for texel into a tight buffer of its own, so the + // region is taken from whichever buffer `uploadData` now names, at + // that buffer's bytes per texel. + // + // THE SAME ON EVERY ARM (P11 M). `mipData` is the frontend shadow on + // the pull and push-monolith arms and the server's staged level on the + // split arms - the same tight client layout either way - and the + // conversions run on whichever side uploads, so the handle arm takes + // its region from the converted buffer exactly as the shadow arm + // does. What it must NOT take from the record then is the carried + // pitch and offset (below): those index the client's layout, not the + // converted one. + SizeT uploadBytes = 0; + if (uploadData == mipData) { + uploadBytes = byteSize; + } else if (!imageWidenedUploadData.empty() && + uploadData == imageWidenedUploadData.data()) { + uploadBytes = imageWidenedUploadData.size(); + } else if (!packedUploadData.empty() && uploadData == packedUploadData.data()) { + uploadBytes = packedUploadData.size(); + } else if (!widenedUploadData.empty() && uploadData == widenedUploadData.data()) { + uploadBytes = widenedUploadData.size(); + } else if (!convertedUploadData.empty() && uploadData == convertedUploadData.data()) { + uploadBytes = convertedUploadData.size() * sizeof(Float); + } + // A 1D array's ES image is a 2D array one texel high with the layers + // in depth, while the shadow keeps the layers as rows: same texels in + // the same order, so a region maps across by moving its y to z. (The + // target is the descriptor's on the split arms: the server may not + // name the client's texture object.) + const Bool layersAsRows = + MGB_TEXTURE_TARGET(stateTextureObject) == TextureTarget::Texture1DArray; + // P4a (D-D6, and ARCHITECTURE.md §9.1's ONE item moved out of the + // Espryt do-not-touch list): the DECISION below is unchanged and + // stays on the server - it is the side that pays the GPU cost, and + // the union-box-versus-N-rects choice is worth 16x the bytes on one + // axis and +6 ms/frame of Mali job cost on the other. What moves is + // the SOURCE of the shape and of the strides. + // + // On the handle arm the box and the rect list come from the record + // the client emitted; on the legacy arm they come from the frontend's + // own rect model, exactly as before. Both spellings feed the same + // three staging shapes and the same StageBlocksIntoUnpackRing, which + // is byte-identical (G5). + const MG_Pipe::MGPipeResourceRecord::PendingUpload* pendingUpload = + pushedStorage != nullptr + ? FindPipeTextureUpload(*pushedStorage, static_cast(uploadTarget), + static_cast(level)) + : nullptr; + const auto dirtyRegion = [&]() -> MG_State::GLState::MipmapDirtyRegion { + if (pendingUpload == nullptr) { + // P5c (tx): a level this arm owes with NO pending upload behind it + // was dirtied by the GPU (T5), and the dirty answer is the + // server's own mark on the staged shadow - the whole level, + // because a generation touches all of it. The client is never + // asked (§2.2's last row). + if (MGB_STAGED_TEXTURE_LIVE) { + const IntVec3 gpuExtent = + MG_Record::ServerStagedTexture().LevelExtentOrUndefined( + MG_Record::StagedTextureStore::KeyForHandle(pushedRes), + static_cast(uploadTarget), static_cast(level)); + return MG_State::GLState::MipmapDirtyRegion{IntVec3{0, 0, 0}, gpuExtent}; + } + return textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level); + } + // MGPBox is {origin, extent}; MipmapDirtyRegion is {lo, hi}. The + // conversion is the whole difference between the two spellings. + const auto& box = pendingUpload->UnionBox; + return MG_State::GLState::MipmapDirtyRegion{ + IntVec3{box.X, box.Y, box.Z}, + IntVec3{box.X + static_cast(box.W), box.Y + static_cast(box.H), + box.Z + static_cast(box.D)}}; + }(); + const SizeT texelCount = static_cast(texelSize.x()) * + static_cast(texelSize.y()) * + static_cast(std::max(texelSize.z(), 1)); + // Whether a box of the level can be addressed at all: whole texels in + // the upload bytes, and an ES image shaped like the shadow. + const Bool regionsAddressable = + !dirtyRegion.Empty() && texelCount > 0 && uploadBytes > 0 && + uploadBytes % texelCount == 0 && + (layersAsRows ? std::max(texelSize.z(), 1) == 1 + : uploadSize.x() == texelSize.x() && uploadSize.y() == texelSize.y() && + std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1)); + const Bool subRectEligible = regionsAddressable && !dirtyRegion.CoversWholeLevel(texelSize); + const SizeT bpp = regionsAddressable ? uploadBytes / texelCount : 0; + const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(), + dirtyRegion.hi.y() - dirtyRegion.lo.y(), + dirtyRegion.hi.z() - dirtyRegion.lo.z()}; + // STRIDES ARE CARRIED, NEVER INFERRED (D-D3), once there is a record + // to carry them: a sub-rect's rows are not contiguous in the level + // shadow, so each MGPSubRegion states the level's row and slice pitch + // and 0 means "tightly packed", which is what a whole-level region + // sends. The legacy arm keeps computing them from the level extent + // and the bytes-per-texel it just derived - the same two numbers, + // from the other side of the wire. + // m-2: THE PITCH IS THE LEVEL'S (D-D3), so every region of one level + // states the same one - and v1 read Regions.front() and would have + // strided the rest at the first one's pitch, silently, if a record + // ever disagreed. This is the assert the review asked for rather than + // a per-region loop feeding per-region strides: a record whose regions + // disagree is corrupt, not a shape the upload planner should learn. + // On disagreement the carried pair is DROPPED and the level's own + // extent times bpp is used, which is what the legacy arm computes. + Bool pendingStridesAgree = true; + if (pendingUpload != nullptr && !pendingUpload->Regions.empty()) { + const auto& firstRegion = pendingUpload->Regions.front(); + for (const auto& region : pendingUpload->Regions) { + if (region.SrcRowStride == firstRegion.SrcRowStride && + region.SrcSliceStride == firstRegion.SrcSliceStride) { + continue; + } + pendingStridesAgree = false; + MGLOG_E_ONCE("MGPipe: texture %u level %u carries regions with two " + "different level pitches (%u/%u and %u/%u) - a pitch is " + "the LEVEL's, so the carried pair is dropped and the " + "extent is used instead", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), + static_cast(level), firstRegion.SrcRowStride, + firstRegion.SrcSliceStride, region.SrcRowStride, + region.SrcSliceStride); + break; + } + } + // P11 M: THE CARRIED PITCH AND OFFSET INDEX THE CLIENT'S LAYOUT - the + // level shadow, or the server's staged copy of it, which is `mipData`. + // A converted level's region is cut from the conversion's own tight + // buffer (above), whose texels are wider, so there the record's numbers + // name the wrong bytes and the pitch is derived from the upload + // buffer's bpp instead - which is what the shadow arm does. + const Bool carriedLayoutApplies = uploadData == mipData; + const SizeT carriedRowBytes = + (pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies && + !pendingUpload->Regions.empty() && + pendingUpload->Regions.front().SrcRowStride != 0) + ? static_cast(pendingUpload->Regions.front().SrcRowStride) + : 0; + const SizeT carriedSliceBytes = + (pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies && + !pendingUpload->Regions.empty() && + pendingUpload->Regions.front().SrcSliceStride != 0) + ? static_cast(pendingUpload->Regions.front().SrcSliceStride) + : 0; + const SizeT levelRowBytes = + carriedRowBytes != 0 ? carriedRowBytes : static_cast(texelSize.x()) * bpp; + const SizeT levelSliceBytes = carriedSliceBytes != 0 + ? carriedSliceBytes + : static_cast(texelSize.y()) * levelRowBytes; + // N-5: THE SAME DISCIPLINE FOR SrcOffset AS FOR THE STRIDES, and the + // asymmetry it removes was the review's point: the strides got a loud + // refusal in the commit that started reading them while the offset - + // which decides the ADDRESS glTexSubImage reads (w*bpp x h x d) bytes + // from - was consumed raw. The cross-check is free, because + // rectUploadPtr four lines down computes exactly the same number from + // the region origin and the two pitches. A record whose regions + // disagree with their own origins is corrupt, so on disagreement the + // carried offsets are DROPPED - every use site falls back to the + // derived pointer, which is what the legacy arm sends - and one named + // line says so. Gated on subRectEligible because bpp is 0 otherwise + // and the regions are not read at all, and on carriedLayoutApplies + // because a converted level never reads them (MGB_RECT_SRC_PTR). + // + // FIX A2 IS THE SECOND LEGITIMATE DISAGREEMENT, and the fallback is + // exactly right for it: a level too large to stage whole crosses as + // several slab records (TextureEmit.h's MGPipeForEachTextureSlab) whose + // regions carry offsets into THEIR OWN run rather than into the level + // shadow. Every piece of the level is merged into this one entry, the + // entry's rects are then read out of the SERVER'S WHOLE LEVEL IMAGE - + // which is what the derived pointer names - and those piece-relative + // offsets are precisely the ones that must not be used here. + // + // GATED ON regionsAddressable, NOT subRectEligible: the per-region arm + // for a level the GPU may have written (m_driverMayHoldGpuWrites below) + // reads MGB_RECT_SRC_PTR too, and it runs exactly when the union box is + // the whole level - which is what two or more slab pieces always add up + // to. Checked only under subRectEligible, a damage rect split across + // pieces kept the later pieces' run-relative offsets and read their + // rows from the top of the level image (KWin's wl_shm window textures: + // the panel repeated down the desktop). bpp is valid whenever the + // regions are addressable. + Bool pendingOffsetsAgree = true; + if (pendingUpload != nullptr && regionsAddressable && carriedLayoutApplies) { + for (const auto& region : pendingUpload->Regions) { + const SizeT derivedOffset = static_cast(region.Z) * levelSliceBytes + + static_cast(region.Y) * levelRowBytes + + static_cast(region.X) * bpp; + if (static_cast(region.SrcOffset) == derivedOffset) continue; + pendingOffsetsAgree = false; + MGLOG_E_ONCE("MGPipe: texture %u level %u carries a region whose SrcOffset " + "%llu is not its own origin (%d,%d,%d) at the level's pitch " + "(%llu expected) - the carried offsets are dropped and every " + "rect is read from the shadow instead. Expected once for a " + "level cut into stage-chunk pieces, whose regions index their " + "own run; otherwise this names a record whose regions and " + "origins disagree", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), static_cast(level), + static_cast(region.SrcOffset), region.X, + region.Y, region.Z, + static_cast(derivedOffset)); + break; + } + } + const Uint8* regionPtr = + static_cast(uploadData) + + static_cast(dirtyRegion.lo.z()) * levelSliceBytes + + static_cast(dirtyRegion.lo.y()) * levelRowBytes + + static_cast(dirtyRegion.lo.x()) * bpp; + // Scatter refinement behind the union box: ~100 sprite + // writes into an atlas leave a box that spans nearly the + // whole level while the touched texels are a few percent of + // it. The storage's bounded rect list recovers the true + // footprint; each rect is uploaded with the same + // ROW_LENGTH striding into the level as the box + // path. The storage only hands the list out when its + // summed area is materially smaller than the box (0 + // otherwise), so the extra calls always move fewer bytes. + MG_State::GLState::MipmapDirtyRegion + dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; + SizeT dirtyRectCount = 0; + if (subRectEligible) { + if (pendingUpload != nullptr) { + // RegionCount == 0 is LEGAL and means "the union box is the + // whole story" (D-D3), which is the same statement the + // frontend's GetStorageDirtyRects makes by returning 0 - so + // both arms reach the box branch below by the same route. + // + // P11 M2: the record's list is the level's EXACT footprint now + // (TextureEmit.h), which the storage's own rect list only + // covered, and the storage withheld that list - "0 rects" - when + // it held more rects than it keeps or when they added up to 3/4 + // of the union box (one driver call on nearly the same bytes). + // Both cut-offs are this side's to apply now, so the box-or-rects + // choice on a texture the GPU has not written is what it was. + // (Once the GPU may have written it, the footprint below uploads + // every region exactly, whatever this chose.) Never a partial + // list: a truncated list is dropped writes. + SizeT summedTexels = 0; + for (const auto& region : pendingUpload->Regions) { + summedTexels += static_cast(region.W) * static_cast(region.H) * + static_cast(std::max(region.D, 1)); + } + const Bool listWithheld = + pendingUpload->Regions.size() < 2 || + pendingUpload->Regions.size() > MG_State::GLState::MipmapStorage::kMaxDirtyRects || + summedTexels * 4 >= dirtyRegion.TexelCount() * 3; + dirtyRectCount = listWithheld ? 0 : pendingUpload->Regions.size(); + for (SizeT r = 0; r < dirtyRectCount; ++r) { + const auto& region = pendingUpload->Regions[r]; + dirtyRects[r] = MG_State::GLState::MipmapDirtyRegion{ + IntVec3{region.X, region.Y, region.Z}, + IntVec3{region.X + static_cast(region.W), + region.Y + static_cast(region.H), + region.Z + static_cast(region.D)}}; + } + } else + // tx: with an active transport the rect list is the record's (the + // pendingUpload arm above) or nothing - the server's GPU-dirty mark + // is whole-level and has no scatter refinement to hand out, and the + // frontend's rect model is not this side's to read. + if (!MGB_STAGED_TEXTURE_LIVE) + dirtyRectCount = textureMipmapObject->GetStorageDirtyRects( + uploadTarget, level, dirtyRects, + MG_State::GLState::MipmapStorage::kMaxDirtyRects); + // The scatter refinement pays only on the client-pointer path, + // where fewer bytes mean less driver-side copying. Through the + // unpack ring every glTexSubImage is a GPU copy job (Mali), so + // ~100 sprite rects become ~100 jobs whose fixed cost dwarfs + // the union box's extra bytes - measured +6 ms/frame of GPU + // time in MC's animated-atlas ticks. One box, one job. + if (BufferImpl::UnpackRingAvailable()) { + dirtyRectCount = 0; + } + } + // Everything above - the union box, the ring's one-box rule, the rect + // list with its merges, fold and 3/4 cut-off - may upload shadow texels + // the client did not write, which is only harmless while the driver + // still holds those texels as they were last uploaded. Once it may hold + // GPU writes, the level goes up as exactly the texels written, one box + // (one ring job) each, whatever that costs: more boxes, and a union box + // that spans the whole level no longer means a whole-level upload. + const MG_State::GLState::MipmapDirtyRegion* uploadRects = dirtyRects; + SizeT uploadRectCount = dirtyRectCount; + if (m_driverMayHoldGpuWrites && regionsAddressable) { + if (pendingUpload != nullptr) { + // The handle arm's footprint is the record's region list, all + // of it and in order (MGB_RECT_SRC_PTR indexes the regions by + // position); a box-only entry is its union box. + dirtyFootprint.clear(); + for (const auto& region : pendingUpload->Regions) { + dirtyFootprint.push_back(MG_State::GLState::MipmapDirtyRegion{ + IntVec3{region.X, region.Y, region.Z}, + IntVec3{region.X + static_cast(region.W), + region.Y + static_cast(region.H), + region.Z + static_cast(region.D)}}); + } + } else + // With an active transport and no record behind the level, the + // server's own whole-level GPU-dirty mark is the whole story (the + // same reason the rect list above is not read here). + if (MGB_STAGED_TEXTURE_LIVE) { + dirtyFootprint.clear(); + } else + textureMipmapObject->GetStorageDirtyFootprint(uploadTarget, level, dirtyFootprint); + // One box is the union box itself, which the branches below already + // upload exactly (as the whole level when it is the whole level). + if (dirtyFootprint.size() >= 2) { + uploadRects = dirtyFootprint.data(); + uploadRectCount = dirtyFootprint.size(); + } else { + uploadRectCount = 0; + } + } + const Bool perRectUpload = regionsAddressable && uploadRectCount >= 2; + const auto rectUploadPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) { + return static_cast(uploadData) + + static_cast(rect.lo.z()) * levelSliceBytes + + static_cast(rect.lo.y()) * levelRowBytes + + static_cast(rect.lo.x()) * bpp; + }; +// m-1 / D-D3, "carried, never inferred": MGPSubRegion::SrcOffset is the byte offset of that +// region's origin into the level shadow - the same number the lambda above derives from the +// origin and the two strides - and on the handle arm it is READ rather than re-derived, after +// the cross-check above has agreed that the two spellings say the same thing (N-5). It is read +// only while carriedLayoutApplies (uploadData == mipData), so the base the offset is relative to +// is the level shadow itself and never a conversion buffer: since dev's 8966d5c2 a converted +// level is sub-rect eligible too, and its rects are cut at the conversion's bpp (rectUploadPtr). +// +// THREE USE SITES, AND THE FIRST OF THEM IS UNREACHABLE (N-5's second half, and the correction +// to what esprytobj-v2.md m-1 claimed): dirtyRectCount is forced to 0 whenever +// UnpackRingAvailable(), and the staging-block loop that takes MGB_RECT_SRC_PTR(r, rect) first +// requires `ringUsable && dirtyRectCount >= 2`. The two conditions cannot both hold. That shape +// is pre-existing (the ring/scatter decision, not this package's), so the loop is left as it +// stands and the count is corrected here instead of the loop being deleted underneath it. +// +// A MACRO AND NOT A SECOND LAMBDA, for DV-9's measured reason: this function's nine ErrorLopper +// lambdas are mangled ...::$_N BY POSITION, so one more renumbers every one of them and moves +// the PULL build's symbol set, whose admitted-change set is EMPTY. The pull expansion is the +// pre-P4a call with one pair of parentheses. #undef'd with the rest. +#define MGB_RECT_SRC_PTR(idx, rect) \ + ((pendingUpload != nullptr && pendingOffsetsAgree && carriedLayoutApplies && \ + static_cast(idx) < pendingUpload->Regions.size()) \ + ? static_cast(uploadData) + \ + static_cast(pendingUpload->Regions[static_cast(idx)].SrcOffset) \ + : rectUploadPtr(rect)) + // A shadow-grid box as the ES image addresses it, and the image height + // that strides the source to match; they differ only for a 1D array + // (layersAsRows), whose rows are ES layers of a one-texel-high image. + struct BackendBox { + GLint x, y, z; + GLsizei w, h, d; + }; + const auto ToBackendBox = [&](const MG_State::GLState::MipmapDirtyRegion& rect) { + const GLsizei w = static_cast(rect.hi.x() - rect.lo.x()); + const GLsizei h = static_cast(rect.hi.y() - rect.lo.y()); + const GLsizei d = static_cast(rect.hi.z() - rect.lo.z()); + return layersAsRows ? BackendBox{rect.lo.x(), 0, rect.lo.y(), w, 1, h} + : BackendBox{rect.lo.x(), rect.lo.y(), rect.lo.z(), w, h, d}; + }; + const GLint imageHeight = layersAsRows ? 1 : texelSize.y(); + // Unpack-ring staging plan, decided ONCE for whichever branch + // below runs: either every glTexSubImage of this level sources + // from the ring or none does, so the pixel-unpack binding is + // toggled exactly once per level. The plan's three shapes line + // up 1:1 with the switch's three branches. + // + // Regions are repacked TIGHTLY (row length = the region's own + // width), which is why the ring path issues no glPixelStorei at + // all: the surrounding ScopedDefaultUnpackState already holds + // ROW_LENGTH/IMAGE_HEIGHT at 0, which is exactly what a tight + // block wants. It also stages strictly fewer bytes than the + // client-pointer path makes the driver walk, which strides over + // the whole level width. + UnpackStagingBlock + stagingBlockArray[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; + UnpackStagingBlock* stagingBlocks = stagingBlockArray; + SizeT stagingBlockCount = 0; + const Bool ringUsable = BufferImpl::UnpackRingAvailable(); + if (!ringUsable) { + // Nothing to plan: every branch below keeps the client + // pointer and its ROW_LENGTH striding, unchanged. + } else if (perRectUpload) { + // Only an exact footprint outgrows the array (see uploadRects). + if (uploadRectCount > MG_State::GLState::MipmapStorage::kMaxDirtyRects) { + stagingBlockOverflow.resize(uploadRectCount); + stagingBlocks = stagingBlockOverflow.data(); + } + for (SizeT r = 0; r < uploadRectCount; ++r) { + const auto& rect = uploadRects[r]; + stagingBlocks[r] = { + MGB_RECT_SRC_PTR(r, rect), + static_cast(rect.hi.x() - rect.lo.x()) * bpp, + static_cast(rect.hi.y() - rect.lo.y()), + static_cast(std::max(rect.hi.z() - rect.lo.z(), 1)), + levelRowBytes, + levelSliceBytes, + 0}; + } + stagingBlockCount = uploadRectCount; + } else if (subRectEligible) { + stagingBlocks[0] = {regionPtr, + static_cast(regionSize.x()) * bpp, + static_cast(regionSize.y()), + static_cast(std::max(regionSize.z(), 1)), + levelRowBytes, + levelSliceBytes, + 0}; + stagingBlockCount = 1; + } else if (uploadData == mipData && texelCount > 0 && + byteSize % texelCount == 0) { + // Whole level, shadow bytes verbatim: `byteSize` is exactly + // what the driver would have read from the client pointer, + // and the shadow is already tightly packed. A CONVERTED + // level stays on the pointer path - its buffer's length is + // the conversion's, not the shadow's, so nothing here can + // size the driver's read of it. The whole-texels check is + // the same sanity the sub-rect path applies, and it is what + // makes "the shadow's bytes per texel == the transfer's" + // hold. (A 1D array's upload size permutes height and depth, + // but the texel count and the tight byte order are the same, + // so the flat copy still describes what the driver reads.) + stagingBlocks[0] = {static_cast(uploadData), + static_cast(byteSize), + 1, + 1, + static_cast(byteSize), + static_cast(byteSize), + 0}; + stagingBlockCount = 1; + } + const Bool ringStaged = + stagingBlockCount > 0 && + StageBlocksIntoUnpackRing(stagingBlocks, stagingBlockCount); + if (ringStaged) { + BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId()); + } + // Ring-sourced or not: see NoteInPlaceTextureUpload. + NoteInPlaceTextureUpload(); + if (MG_Util::PipeStats::Enabled()) { + // One emission per (upload target, level) that ships texels; + // the switch below turns it into either one union-box job or + // dirtyRectCount rect jobs. The box/rect split is counted + // separately from the bytes on purpose: SSIM is blind to it + // and the +6 ms/frame Mali cliff was a shape regression, not + // a byte regression (plan section 7.3). + const Bool rectShape = subRectEligible && dirtyRectCount >= 2; + Uint64 shippedBytes = 0; + if (rectShape) { + for (SizeT r = 0; r < dirtyRectCount; ++r) { + const auto& rect = dirtyRects[r]; + shippedBytes += static_cast(rect.hi.x() - rect.lo.x()) * + static_cast(rect.hi.y() - rect.lo.y()) * + static_cast(std::max(rect.hi.z() - rect.lo.z(), 1)) * + static_cast(bpp); + } + } else if (subRectEligible) { + shippedBytes = static_cast(regionSize.x()) * + static_cast(regionSize.y()) * + static_cast(std::max(regionSize.z(), 1)) * + static_cast(bpp); + } else { + shippedBytes = static_cast(byteSize); + } + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture, + shippedBytes); + MG_Util::PipeStats::AddCalls( + MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1); + MG_Util::PipeStats::AddCalls( + rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions + : MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, + 1); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, + rectShape ? static_cast(dirtyRectCount) : 1u); + } + switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) { + case TextureTarget::Texture2D: + case TextureTarget::TextureCubeMap: + if (perRectUpload) { + if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + for (SizeT r = 0; r < uploadRectCount; ++r) { + const auto& rect = uploadRects[r]; + g_GLESFuncs.glTexSubImage2D( + glUploadTarget, static_cast(level), rect.lo.x(), + rect.lo.y(), static_cast(rect.hi.x() - rect.lo.x()), + static_cast(rect.hi.y() - rect.lo.y()), glFormat, + glType, + ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset) + : static_cast(MGB_RECT_SRC_PTR(r, rect))); + } + // The surrounding ScopedDefaultUnpackState shadow says 0. + if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } else if (subRectEligible) { + if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + g_GLESFuncs.glTexSubImage2D( + glUploadTarget, static_cast(level), dirtyRegion.lo.x(), + dirtyRegion.lo.y(), static_cast(regionSize.x()), + static_cast(regionSize.y()), glFormat, glType, + ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset) + : static_cast(regionPtr)); + // The surrounding ScopedDefaultUnpackState shadow says 0. + if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } else { + g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast(level), 0, 0, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, + glType, + ringStaged + ? UnpackRingPixelOffset(stagingBlocks[0].offset) + : uploadData); + } + break; + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: + // ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly + // like a 2D array whose depth is 6 * the cube count. + case TextureTarget::TextureCubeMapArray: + if (perRectUpload) { + if (!ringStaged) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); + } + for (SizeT r = 0; r < uploadRectCount; ++r) { + const auto box = ToBackendBox(uploadRects[r]); + g_GLESFuncs.glTexSubImage3D( + glUploadTarget, static_cast(level), box.x, box.y, box.z, + box.w, box.h, box.d, glFormat, glType, + ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset) + : static_cast(MGB_RECT_SRC_PTR(r, uploadRects[r]))); + } + if (!ringStaged) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + } + } else if (subRectEligible) { + if (!ringStaged) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); + } + const auto box = ToBackendBox(dirtyRegion); + g_GLESFuncs.glTexSubImage3D( + glUploadTarget, static_cast(level), box.x, box.y, box.z, box.w, box.h, + box.d, glFormat, glType, + ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset) + : static_cast(regionPtr)); + if (!ringStaged) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + } + } else { + g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, + glType, + ringStaged + ? UnpackRingPixelOffset(stagingBlocks[0].offset) + : uploadData); + } + break; + default: + MGLOG_E_ONCE("Unhandled texture target %s", + MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str()); + break; + } + if (ringStaged) { + // Back to the resting unbound state every other upload site + // in this file assumes (their BindPixelUnpackBufferId(0) is + // meant to stay a shadow no-op). + BufferImpl::BindPixelUnpackBufferId(0); + } + MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level); + } + } + } + break; + } + case TextureStorageType::Buffer: { + auto* textureBufferObject = + static_cast(stateTextureObject.get()); + // P5e (tx2), scout R5 RESOLVED. THE BACKING BUFFER IS THE DESCRIPTOR'S, AND SO IS + // ITS WINDOW - `Desc.BufferForTexBuffer` names the store and `Desc.BufOffset / + // BufSize` name the range, so the frontend binding slot below is not read at all + // on the handle arm. + // + // The one thing that argument had to answer is D-A4's `frontend` parameter of + // EnsureBufferResourceForHandle, which existed for the emulated-persistent-map + // question. The E note's reading is confirmed at this head: that function's three + // frontend reads (Managers.cpp's MappedData / SyncPersistentMappedRange / + // HasDefinedContent) are all inside its MONOLITH arm - under an active transport + // it is client-free (kimi audit row 29) - so `nullptr` is not a hole, it is the + // whole of the split answer, and it is what §4.2's Buffer row already prescribes + // (`EnsureBufferResourceForHandle(nullptr, h)`). + // + // WHOLE-VS-RANGE likewise stops asking the buffer object for its size: the + // sentinel kMGPipeWholeBuffer IS "the whole store", which is the question + // `rangeSize == buffer->GetSize()` was spelling the long way round. + MG_Pipe::MGPipeHandle pushedTexBuffer = MG_Pipe::kMGPipeNullHandle; + if (pushedStorage != nullptr) { + pushedTexBuffer = pushedStorage->Desc.BufferForTexBuffer; + if (MG_Pipe::MGPipeHandleIsNull(pushedTexBuffer)) { + MGLOG_E_ONCE("MGPipe: buffer texture %u's descriptor names no backing buffer on " + "the handle arm, so it cannot be backed (handle {%u, %u})", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), pushedRes.Slot, pushedRes.Gen); + return; + } + } + // P5e (fix1 follow-up, ID-110): the third conjunct is the one fix1's report + // asked for a ruling about. This arm hands the backing-buffer lookup + // kNoFrontendBuffer and leans on the record answering; it was unreachable + // under monolith only because every writer of a handle-note is + // transport-gated - safe by a chain rather than by its own statement, which + // is exactly the shape that cost the monolith arm 137 scenarios. Under a + // transport the conjunct is already implied by `pushedStorage != nullptr`, so + // this changes no behaviour and closes no door; it says the thing instead of + // depending on it. + const Bool texBufferByRecord = pushedStorage != nullptr && + MGB_TEXTURE_HANDLE_ARM_OFF(*this) == false; + static const SharedPtr kNoFrontendBuffer{}; + auto& buffer = texBufferByRecord ? kNoFrontendBuffer + : textureBufferObject->GetBufferBindingSlot().GetBoundObject(); + if (!buffer && !texBufferByRecord) { + MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + return; + } + auto bufferIndex = buffer ? buffer->GetExternalIndex() : 0u; + // The memo key is the HANDLE's slot on the handle arm: a GL name is never an + // identity (section 4.2.1), and the record's Serial covers a slot recycled at a + // new generation because a respecify moves it. + currentTextureInfo.bufferExternalIndex = + pushedStorage != nullptr ? static_cast(pushedTexBuffer.Slot) : bufferIndex; + + Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo); + + // Need to sync texture buffer if not synced yet + auto* backendBufferResource = + BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer); + if (!backendBufferResource || backendBufferResource->id == 0) { + MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + return; + } + + // Bind buffer to texture + auto backendId = backendBufferResource->id; + + GLenum glInternalFormat, glType, glFormat; + TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureBufferObject), &glInternalFormat, &glFormat, + &glType, TextureTarget::TextureBuffer); + // The view half of the buffer-image SPLIT. A buffer texture has no storage of its + // own to widen, but the VIEW its format describes can be re-described one + // component at a time over the same bytes - rg32f over N texels is r32f over 2N - + // and WidenImageFormatsPass rewrites every access to subscript it that way. Only + // for a texture that is actually image-bound: a sampled-only buffer texture keeps + // the format the application asked for (see GetImageBindableBufferSplitFormat). + // + // The split goes on a SEPARATE name (m_bufferImageSplitViewId), not on this one. + // Re-describing the application's own texture also re-describes what a + // samplerBuffer reading it sees, and the sampler side is not subscript-rewritten - + // so texelFetch(s, i) started returning component 2i of the base view instead of + // texel i. rg32f is a legal SAMPLED buffer-texture format in ES 3.2; only the + // IMAGE binding needs the split, so only the image binding's name carries it. + const GLenum bufferImageSplitFormat = + m_imageBindableStorageRequired + ? TextureImpl::GetImageBindableBufferSplitFormat(MGB_STORAGE_FORMAT(textureBufferObject)) + : GL_UNKNOWN_MGL; + + if (needsRegeneration) { + // Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a + // 4.x context, so glTexBuffer is a legal call the app may make on any driver - + // but ES only gained them in 3.2, and g_GLESFuncs.glTexBuffer is simply null + // below that without EXT/OES_texture_buffer. Calling it was an unconditional + // null dereference. There is no conformant way to refuse the call (it is valid + // in the context MobileGL claims), so the texture is left unbacked and the + // reason is stated once per object, latched by the flag below. It was parked + // at MGLOG_I while the level ordering compiled MGLOG_W out of INFO builds; + // W is the correct level and now survives there. + if (!AreBufferTexturesSupported()) { + if (m_bufferTextureUnsupportedReported) { + break; + } + m_bufferTextureUnsupportedReported = true; + MGLOG_W("Texture buffer %u cannot be backed: this ES driver has no buffer " + "textures (%s). Every draw sampling it will read zero and every " + "shader declaring a samplerBuffer will fail to compile. MobileGL " + "still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an " + "OpenGL 4.x context may not report 0.", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), GetBufferTextureTierName(), + g_GLESCapabilities.MaxTextureBufferSize); + break; + } + MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with " + "ID: %u, buffer ID: %u, buffer size: %zu, format: %s", + m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize, + MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); + // A texture that names a window of the buffer needs the range form; the + // whole-buffer forms report offset 0 and the buffer's current size, which + // glTexBuffer expresses more directly (and works where the range entry point + // is absent). + // The WINDOW is carried: MGPResourceDesc::BufOffset / BufSize, with + // kMGPipeWholeBuffer (~0) meaning "the whole store, resolved live" - which is + // why the whole-buffer arm still asks the BUFFER for its size. That is the + // buffer family's own object (P3a's, not this record's), and resolving it + // here is exactly what the sentinel is documented to mean. + const SizeT rangeOffset = pushedStorage != nullptr + ? static_cast(pushedStorage->Desc.BufOffset) + : textureBufferObject->GetBufferRangeOffset(); + // P5e (tx2): "the whole store" is the SENTINEL on the handle arm, not a size + // compare against a frontend BufferObject this arm may not name. The monolith + // arm keeps resolving the sentinel live, exactly as its comment says. + const Bool wholeBufferRange = + pushedStorage != nullptr + ? (pushedStorage->Desc.BufOffset == 0 && + pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer) + : (textureBufferObject->GetBufferRangeOffset() == 0 && + textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize()); + const SizeT rangeSize = + pushedStorage == nullptr + ? textureBufferObject->GetBufferRangeSizeInBytes() + : (pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer + ? (buffer ? buffer->GetSize() : 0u) + : static_cast(pushedStorage->Desc.BufSize)); + // Through CallTexBuffer/CallTexBufferRange rather than g_GLESFuncs directly: + // the unsuffixed entry points are the ES 3.2 core spelling, and a driver + // whose buffer textures come from EXT/OES_texture_buffer exports the + // suffixed ones instead. The dispatchers pick whichever this tier ships. + if (wholeBufferRange) { + CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); + } else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId, + static_cast(rangeOffset), + static_cast(rangeSize))) { + MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no " + "glTexBufferRange; binding the whole buffer instead", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); + } + DebugImpl::ErrorLopper::Loop( + [file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) { + MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s", + func, file, line, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), + backendId, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + // The image half of the SPLIT, on its own name over the same buffer. Minted + // lazily - only a texture that is both image-bound AND holds a format with no + // ESSL image spelling ever gets one - and re-pointed here, in the same + // regeneration gate as the view above, so the two never describe different + // buffers or different windows of one. + if (bufferImageSplitFormat != GL_UNKNOWN_MGL) { + if (m_bufferImageSplitViewId == 0) { + g_GLESFuncs.glGenTextures(1, &m_bufferImageSplitViewId); + } + if (m_bufferImageSplitViewId == 0) { + MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; " + "its image binding will read the unsplit view.", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + } else { + g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId); + if (wholeBufferRange) { + CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId); + } else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId, + static_cast(rangeOffset), + static_cast(rangeSize))) { + CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId); + } + // The raw bind above went behind Bind()'s shadow, which tracks objects + // rather than names: leaving it claiming THIS object is bound would + // make the next Bind(GL_TEXTURE_BUFFER) a no-op and leave the split + // view bound in the application texture's place. + g_boundTexturesCache[g_activeTextureUnit][static_cast( + TextureTarget::TextureBuffer)] = nullptr; + } + } + } + break; + } + default: + // TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a + // backstop for a state object that grew a new storage kind. Skipping the upload + // renders wrong; throwing unwinds through the C GL ABI and kills the process. + MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; " + "skipping this sync", + static_cast(MGB_TEXTURE_STORAGE_KIND(pushedStorage, stateTextureObject)), + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + break; + } + + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + m_prevTextureInfo = currentTextureInfo; + if (pushedStorage != nullptr) { + // The handle arm's one stamp. Deliberately AFTER the switch, at the same + // instant the legacy arm stamps its four keys, so a bail arm that returned + // early leaves it untouched and the next sync re-runs - which is the property + // the pending set exists to make safe (D-D5). + // + // The record pointer was resolved before any driver work and TextureResources + // has not been grown since (nothing this function calls emits a create), so + // re-reading Serial through it is sound; the PendingUploads entries it named + // have been consumed one at a time by MGB_LEVEL_UPLOAD_DONE. + m_syncedResourceSerial = pushedStorage->Serial; + return; + } + // Everything dirty at entry is uploaded (or provably has no bytes to + // upload); stamp the version so per-draw re-syncs short-circuit until + // the next CPU-side mutation. + m_syncedContentVersion = stateTextureObject->GetContentVersion(); + // Same instant, so the cheap gate's keys describe exactly this synced state. + // Only Mipmap storage may arm it - the gate refuses other storage types anyway, + // but a stale trio must not linger on an object that later switches type. + if (MG_Pipe::gPipeInputs->IsLive() && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { + m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId(); + m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); + m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); + } else { + m_syncedShapeContextId = 0; + } + } +#undef MGB_LEVEL_NEEDS_UPLOAD +#undef MGB_LEVEL_UPLOAD_DONE +#undef MGB_STORAGE_FORMAT +#undef MGB_STORAGE_BASE_SIZE +#undef MGB_STORAGE_LEVELS +#undef MGB_STORAGE_SAMPLES +#undef MGB_STORAGE_FIXED_SAMPLE_LOCATIONS +#undef MGB_STORAGE_IMMUTABLE +#undef MGB_STORAGE_KIND +#undef MGB_TEXTURE_TARGET +#undef MGB_UPLOAD_TARGETS +#undef MGB_LEVEL_TEXEL_SIZE +#undef MGB_LEVEL_BYTE_SIZE +#undef MGB_LEVEL_TEXELS +#undef MGB_STAGED_TEXTURE_LIVE + + // P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD, by the handle the caller noted on + // the twin before it resolved it; the client allocator is never probed. + const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolveOwnRecord( + const SharedPtr& stateTextureObject) const { + (void)stateTextureObject; + // P13: a twin is only ever reached by handle, and the handle is noted on it first. + if (MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)) return nullptr; + return PipeTextureRecordForHandle(m_pushedSyncHandle); + } + } // namespace TextureImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp new file mode 100644 index 00000000..6a78fc75 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp @@ -0,0 +1,1605 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + static Bool IsAngleLlvmpipeRenderer() { + return g_GLESCapabilities.IsAngleLlvmpipeRenderer; + } + + // P3b/P4b (wave 2-D, package D3). See the declaration for why a view's storage clauses are + // asked of another record, why null means "ask again" and why the deletion order cannot + // strand this walk. + const MG_Pipe::MGPipeResourceRecord* PipeTextureStorageRecordForRecord( + const MG_Pipe::MGPipeResourceRecord& record) { + // ONE hop is the shape the client emits and the second is already generous; the bound is + // here so a cyclic chain is a null answer rather than a hang, not because a real chain + // is ever this long. + constexpr Uint32 kViewChainBound = 4; + const MG_Pipe::MGPipeResourceRecord* storage = &record; + for (Uint32 hop = 0; hop < kViewChainBound; ++hop) { + if (MG_Pipe::MGPipeHandleIsNull(storage->Desc.ViewOf)) return storage; + storage = PipeTextureRecordForHandle(storage->Desc.ViewOf); + if (storage == nullptr) return nullptr; + } + return nullptr; + } + + namespace TextureImpl { + BackendTextureObject::BackendTextureObject() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + g_GLESFuncs.glGenTextures(1, &m_backendTextureId); + m_contextGeneration = g_backendContextGeneration; + if (m_backendTextureId == 0) { + MGLOG_E_ONCE("Failed to generate texture object."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + } else { + MGLOG_D("Generated texture object with ID: %u.", m_backendTextureId); + } + } + + BackendTextureObject::~BackendTextureObject() { + if (InProcessTeardown()) { + return; // see InProcessTeardown(): the driver may be unloaded already + } + if (m_backendTextureId == 0) { + return; + } + // Scrub every driver-state shadow that could false-skip when the name + // or this heap address is recycled - regardless of whether the id can + // still be deleted. + ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); + for (auto& unitCache : g_boundTexturesCache) { + for (auto& boundTexture : unitCache) { + if (boundTexture == this) { + boundTexture = nullptr; + } + } + } + // TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes. + // Discriminator for the mali-mem-purge hiccup theory: never hand the + // driver a texture free, so the purge daemon has nothing to reclaim. + static const Bool s_expLeakTextureDeletes = [] { + FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb"); + if (!f) return false; + std::fclose(f); + return true; + }(); + if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures && + !s_expLeakTextureDeletes) { + g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); + if (m_bufferImageSplitViewId != 0) { + g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId); + } + } + m_backendTextureId = 0; + m_bufferImageSplitViewId = 0; + } + + void BackendTextureObject::Bind(GLenum target, Uint unit) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (g_activeTextureUnit != unit) { + ActivateTextureUnit(unit); + } + + auto targetN = static_cast(MG_Util::ConvertGLEnumToTextureTarget(target)); + if (this == g_boundTexturesCache[unit][targetN]) return; + + g_GLESFuncs.glBindTexture(target, m_backendTextureId); + g_boundTexturesCache[unit][targetN] = this; + } + + Uint BackendTextureObject::GetBackendTextureId() const { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + return m_backendTextureId; + } + + // P5e (tx2), CONTRACT-P5E §5.2 / scout G-S2-5. See the declaration. + void BackendTextureObject::RequireImageBindableStorageByHandle( + MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record) { + if (m_imageBindableStorageRequired) { + return; + } + // P13 W4b (defect h): monolith's record arm takes the body too; its old arm only + // cleared m_isInitialized and replayed the frontend shadow at the next sync. + const auto format = static_cast(record.Desc.InternalFormat); + // ES image binding needs immutable storage, but an already + // immutable core-format allocation needs no replacement. Keep + // its GPU contents and native name; normal serial/shape checks + // below still consume pending uploads and real redefinitions. + if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) { + m_imageBindableStorageRequired = true; + m_forceTextureParamsResync = true; + return; + } + // P9 W2 (docs/archive/notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE + // SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies, + // generated mips, image stores) and the staged store holds what the client uploaded; + // the client is never asked, so no pull protocol exists. Reading only the store would + // be wrong: a GPU-written level's store run is stale or absent - both host corpus + // re-mints (iris-photon, iris-derivative) are render targets with no staged bytes. + const Bool hadBackendStorage = m_isInitialized; + // The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be + // produced on the host (llvmpipe renders every image format), so the test lane forces + // one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs. + static const Bool s_forceReadbackFailure = + std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr; + auto& store = MG_Record::ServerStagedTexture(); + const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); + const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); + GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE; + MG_Util::TextureFormatProcessor::NormalizePixelFormat( + MG_Util::ConvertTextureInternalFormatToGLEnum(format), {}, nullptr, + &canonicalFormat, &canonicalType); + for (const auto& uploadTarget : + BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) { + for (Uint32 level = 0; level < record.Desc.Levels; ++level) { + const auto targetCode = static_cast(uploadTarget); + const auto levelCode = static_cast(level); + const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode); + if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue; + const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode); + const Bool covered = store.IsCovered(key, targetCode, levelCode); + const MG_Pipe::MGPBox whole{0, 0, 0, static_cast(extent.x()), + static_cast(extent.y()), static_cast(extent.z())}; + // A genuinely full pending write owns every byte. A + // union box covering the level is not sufficient when + // its explicit rectangles leave GPU-written gaps. + const auto coversWhole = [&](const auto& box) { + return box.X == 0 && box.Y == 0 && box.Z == 0 && + box.W == whole.W && box.H == whole.H && box.D == whole.D; + }; + const Bool pendingWhole = pending && (pending->Regions.empty() + ? coversWhole(pending->UnionBox) + : std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole)); + // W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's + // Defined-ness follows the client's respecifies, the driver's follows this + // twin's last sync, and the two differ for a level defined (null data, or null + // data plus a partial upload) after that sync: the driver has no such level, so + // the GPU cannot have written it. A covered level then replays its staged run + // (the client's whole-level shadow, pending boxes included) and an uncovered one + // is undefined content with nothing to preserve. Reading it anyway was + // Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL. + const Bool nativeLevel = + m_isInitialized && + NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget, + static_cast(level), extent); + if (nativeLevel && !(covered && pendingWhole)) { + Vector snapshot; + const Bool readBack = + !s_forceReadbackFailure && + ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format, + static_cast(level), extent, + /*sourceUsesImageCarrier=*/false, canonicalFormat, + canonicalType, snapshot); + if (!readBack && !covered) { + // The GPU holds this level, the driver will not hand it back, and no + // CPU byte of it exists anywhere: the one case with no honest answer. + MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u", + res.Slot, res.Gen, level); + std::abort(); + } + if (!readBack) { + // W2-b: the driver refused the readback (a format it cannot attach + // to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm + // without EXT_texture_norm16, float without EXT_color_buffer_float) + // but the store covers the level. Its staged run is exactly what the + // monolith re-mint replays; texels the GPU wrote since that upload + // are not preserved, which is monolith's answer too. + MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver " + "refused the readback (internal format %u); replaying the level's " + "staged bytes instead (remint-readback-fallback)", + res.Slot, res.Gen, level, record.Desc.InternalFormat); + } + if (readBack && pending) { + // Native contents include prior draws/image + // writes; the pending stage owns ONLY its boxes. + // Merge those boxes before adopting a whole-level + // snapshot, never overwrite untouched GPU texels + // with the rest of a stale client staging run. + const SizeT pixels = static_cast(extent.x()) * extent.y() * extent.z(); + const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode); + if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) { + MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}"); + std::abort(); + } + const SizeT bpp = stagedSize / pixels; + const Uint8* staged = store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion"); + const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { + if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W || + Uint64(y) + h > whole.H || Uint64(z) + d > whole.D) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}"); + std::abort(); + } + for (Uint32 slice = 0; slice < d; ++slice) { + for (Uint32 row = 0; row < h; ++row) { + const SizeT offset = ((static_cast(z + slice) * whole.H + y + row) * whole.W + x) * bpp; + Memcpy(snapshot.data() + offset, staged + offset, static_cast(w) * bpp); + } + } + }; + if (pending->Regions.empty()) { + const auto& box = pending->UnionBox; + merge(box.X, box.Y, box.Z, box.W, box.H, box.D); + } else { + for (const auto& box : pending->Regions) + merge(box.X, box.Y, box.Z, box.W, box.H, box.D); + } + } + if (readBack) { + store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size()); + } + } + // Undefined contents have no byte run to preserve. A + // first allocation can still be image-bindable without + // inventing an upload or consulting the frontend. + if (!store.IsCovered(key, targetCode, levelCode)) continue; + if (!RearmPipeTextureLevelUpload(res, + MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast(uploadTarget)), + levelCode, whole)) { + MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}"); + std::abort(); + } + } + } + // W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as + // on the frontend one (which counts in RequireImageBindableStorage). The keep path + // above is not a re-mint and returns before this; a first allocation holds nothing. + if (hadBackendStorage && MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1); + } + // Every defined byte now belongs to the server and every required + // replay is in its pending set. The existing upload path builds and + // fills the immutable/widened carrier from those records. + m_imageBindableStorageRequired = true; + m_isInitialized = false; + // The widened carrier's swizzle override, exactly as the frontend overload's tail + // sets it and for the same reason: the parameter sync is gated on a params version + // this transition does not move. + m_forceTextureParamsResync = true; + } + + // Replaces one level's shadow bytes with what the driver texture holds for it, then copies + // back the client writes the driver has not been sent yet - exactly the texels written + // (the level's dirty footprint), not the union box or the rect list, which also span texels + // the client never wrote and which only the driver's copy holds correctly. Returns false, + // leaving the shadow untouched, when the driver's copy cannot stand in for the shadow: the + // client wrote every texel of the level, the level is stored compressed, the driver has no + // image at this level and extent (defined after the last sync), or the readback cannot be + // served (a multisample image, a format the driver will not attach to a framebuffer). + // + // P11 M, `pipePending` (push builds): THE CLIENT WRITES THE DRIVER HAS NOT BEEN SENT MAY + // LIVE IN THE APPLIER'S PENDING SET INSTEAD. On the texture handle arm the client clears its + // own dirty flags when it emits a level (D-D5), so the frontend model below answers "clean" + // for a write that is still only in the shadow; the entry the applier accumulated for this + // (uploadTarget, level) names the same shadow-grid boxes. Both are merged over the + // snapshot. Only a box-only entry whose box IS the level owns every byte of it (dev's rule: + // a union box spanning the level with gaps in it is not a whole-level write); an entry with + // regions never short-circuits, since accumulated regions may overlap and their texels do + // not add up - the readback and the merge below give the same bytes when they do cover it. + // Null on the pull build's arm and the legacy arm. + static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture, + const MG_State::GLState::ITextureObject& stateTextureObject, + MG_State::GLState::TextureObjectMipmap& mipmapObject, + TextureUploadTarget uploadTarget, Uint level + , + const MG_Pipe::MGPipeResourceRecord::PendingUpload* pipePending + ) { + const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level); + const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)}; + const SizeT texelCount = + static_cast(extent.x()) * static_cast(extent.y()) * static_cast(extent.z()); + Vector pending; + mipmapObject.GetStorageDirtyFootprint(uploadTarget, level, pending); + // Disjoint boxes, so their texels add up; a union box spanning the level is not enough. + SizeT pendingTexels = 0; + for (const auto& box : pending) { + pendingTexels += box.TexelCount(); + } + if (!pending.empty() && pendingTexels >= texelCount) return false; + // Every box the entry names, checked against the level before anything is read: a box + // outside it is a corrupt record, and the answer is the pre-readback one (keep the + // shadow) rather than a copy out of bounds. + const auto pipeBoxInLevel = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { + return x >= 0 && y >= 0 && z >= 0 && Uint64(x) + w <= Uint64(extent.x()) && + Uint64(y) + h <= Uint64(extent.y()) && Uint64(z) + d <= Uint64(extent.z()); + }; + if (pipePending != nullptr) { + const auto& box = pipePending->UnionBox; + if (!pipeBoxInLevel(box.X, box.Y, box.Z, box.W, box.H, box.D)) { + MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's box lies outside the level; the " + "image-bindable re-mint replays the shadow as it stands", + stateTextureObject.GetExternalIndex(), level); + return false; + } + for (const auto& region : pipePending->Regions) { + if (!pipeBoxInLevel(region.X, region.Y, region.Z, region.W, region.H, region.D)) { + MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's region lies outside the " + "level; the image-bindable re-mint replays the shadow as it stands", + stateTextureObject.GetExternalIndex(), level); + return false; + } + } + if (pipePending->Regions.empty() && box.X == 0 && box.Y == 0 && box.Z == 0 && + box.W == static_cast(extent.x()) && box.H == static_cast(extent.y()) && + box.D == static_cast(extent.z())) { + return false; + } + } + if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false; + if (!g_GLESFuncs.glGetTexLevelParameteriv) return false; + + const TextureTarget target = stateTextureObject.GetTarget(); + const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(target); + const GLenum faceTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + const IntVec3 backendExtent = GetBackendUploadSize(target, texelSize); + GLint driverWidth = 0, driverHeight = 0, driverDepth = 0; + backendTexture.Bind(nativeTarget); + g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_WIDTH, &driverWidth); + g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_HEIGHT, + &driverHeight); + g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast(level), GL_TEXTURE_DEPTH, &driverDepth); + if (driverWidth != backendExtent.x() || driverHeight != backendExtent.y() || + std::max(driverDepth, 1) != std::max(backendExtent.z(), 1)) { + return false; + } + + const TextureInternalFormat format = stateTextureObject.GetFormat(); + GLenum shadowFormat = GL_NONE, shadowType = GL_NONE; + MG_Util::TextureFormatProcessor::NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(format), + {}, nullptr, &shadowFormat, &shadowType); + Vector snapshot; + if (!ReadTextureLevelTight(backendTextureId, target, uploadTarget, format, static_cast(level), + texelSize, /*sourceUsesImageCarrier=*/false, shadowFormat, shadowType, + snapshot)) { + return false; + } + const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level); + auto* shadow = static_cast(mipmapObject.MapMipmapData(uploadTarget, level)); + if (shadow == nullptr || snapshot.size() != shadowBytes || texelCount == 0 || shadowBytes % texelCount) { + return false; + } + + if (!pending.empty()) { + // The boxes index the SHADOW's texel grid (a 1D array's layers are its rows), which + // is also the snapshot's byte order. + const SizeT bpp = shadowBytes / texelCount; + const SizeT rowBytes = static_cast(extent.x()) * bpp; + const SizeT sliceBytes = static_cast(extent.y()) * rowBytes; + for (const auto& box : pending) { + if (box.Empty()) continue; + const SizeT spanBytes = static_cast(box.hi.x() - box.lo.x()) * bpp; + for (Int z = box.lo.z(); z < box.hi.z(); ++z) { + for (Int y = box.lo.y(); y < box.hi.y(); ++y) { + const SizeT offset = static_cast(z) * sliceBytes + static_cast(y) * rowBytes + + static_cast(box.lo.x()) * bpp; + Memcpy(snapshot.data() + offset, shadow + offset, spanBytes); + } + } + } + } + if (pipePending != nullptr) { + // The same merge for the applier's entry: its regions when it carries a list, its + // union box when it does not (RegionCount 0 = "the box is the whole story", D-D3). + const SizeT bpp = shadowBytes / texelCount; + const SizeT rowBytes = static_cast(extent.x()) * bpp; + const SizeT sliceBytes = static_cast(extent.y()) * rowBytes; + const auto mergePipeBox = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { + for (Uint32 slice = 0; slice < d; ++slice) { + for (Uint32 row = 0; row < h; ++row) { + const SizeT offset = static_cast(z + static_cast(slice)) * sliceBytes + + static_cast(y + static_cast(row)) * rowBytes + + static_cast(x) * bpp; + Memcpy(snapshot.data() + offset, shadow + offset, static_cast(w) * bpp); + } + } + }; + if (pipePending->Regions.empty()) { + const auto& box = pipePending->UnionBox; + mergePipeBox(box.X, box.Y, box.Z, box.W, box.H, box.D); + } else { + for (const auto& region : pipePending->Regions) { + mergePipeBox(region.X, region.Y, region.Z, region.W, region.H, region.D); + } + } + } + Memcpy(shadow, snapshot.data(), shadowBytes); + return true; + } + + void BackendTextureObject::RecreateBackendTexture() { + if (m_backendTextureId != 0) { + ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); + // Application FBO twins that attached the dying id memoize on FRONTEND + // attachment versions, which this backend-side re-mint does not move; + // without this bump their driver FBOs would keep the deleted name + // attached forever (see g_attachmentBackendIdGeneration). + ++FramebufferImpl::g_attachmentBackendIdGeneration; + if (m_contextGeneration == g_backendContextGeneration) { + g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); + } + for (auto& unitCache : g_boundTexturesCache) { + for (auto& boundTexture : unitCache) { + if (boundTexture == this) { + boundTexture = nullptr; + } + } + } + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // A shared image's EGLImage lives exactly as long as the name it was bound to. + m_sharedImage = nullptr; +#endif + g_GLESFuncs.glGenTextures(1, &m_backendTextureId); + m_contextGeneration = g_backendContextGeneration; + if (m_backendTextureId == 0) { + MGLOG_E_ONCE("Failed to regenerate texture object."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + } else { + MGLOG_D("Regenerated texture object with ID: %u.", m_backendTextureId); + } + m_isInitialized = false; + m_backendStorageImmutable = false; + m_prevTextureInfo = {}; + // The new ES texture starts at the ES defaults, so every parameter this object had + // already pushed onto the old one is gone. The change-detection caches below would + // otherwise still claim those values are in force and SyncTextureParamsToBackend + // would skip the whole pass on the unchanged params version, leaving the driver + // texture at defaults for the rest of its life. Latent for swizzle, LOD range and + // border colour long before GL_DEPTH_STENCIL_TEXTURE_MODE joined them; the mode + // makes it visible because falling back to the default silently samples the wrong + // aspect rather than merely mis-filtering. + m_cacheLodRange = {0, 1000}; + m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f}; + m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, TextureSwizzleParam::Blue, + TextureSwizzleParam::Alpha}; + m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT; + m_forceTextureParamsResync = true; + // The filter/wrap/LOD cache belongs to the name that just went away, and its gate is + // the frontend SAMPLER's version, which a backend re-mint does not move - so without + // this the new driver texture keeps the ES defaults for life. See m_forceSamplerResync. + m_cacheSamplerParameters = SamplerParameters{}; + m_forceSamplerResync = true; + } + + void ExerciseDefaultUnpackScopeForTesting() { + const ScopedDefaultUnpackState scope; + } + + // See Managers.h. One flag for every context, like the image-unit high-water mark: a + // dispatch on another context can consume an upload's flag before the uploading + // context's own dispatch sees it - a hole only a shared texture re-uploaded on one context + // and image-accessed on another can fall into, and GL wants a fence between those anyway. + static Bool g_inPlaceTextureUploadPending = false; + + void NoteInPlaceTextureUpload() { g_inPlaceTextureUploadPending = true; } + + GLbitfield TakeInPlaceTextureUploadBarrierBits() { + if (!g_inPlaceTextureUploadPending) return 0; + g_inPlaceTextureUploadPending = false; + return GL_TEXTURE_UPDATE_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT; + } + + // Components per texel the frontend format's client data carries. Only the three-channel + // formats that can be widened to a four-channel render target need an answer (see + // PrepareChannelWidenedUpload); everything else keeps its own layout and reports 0. + Uint GetWidenableClientComponentCount(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::RGB8Snorm: + case TextureInternalFormat::RGB16Snorm: + case TextureInternalFormat::RGB16: + case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow) + case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow) + case TextureInternalFormat::RGB16F: + case TextureInternalFormat::RGB32F: + case TextureInternalFormat::SRGB8: + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGB32UI: + return 3; + default: + return 0; + } + } + + // True when the widened format's client data is integer rather than normalized. The two + // classes share every narrow component type - GL_RGB8I and GL_RGB8_SNORM are both uploaded + // as GL_BYTE - but their "1.0" differs: an integer channel's one is the integer 1, a + // normalized channel's is the saturated field. The type alone cannot tell them apart, so + // the source format has to. + Bool IsIntegerWidenableFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::RGB8I: + case TextureInternalFormat::RGB8UI: + case TextureInternalFormat::RGB16I: + case TextureInternalFormat::RGB16UI: + case TextureInternalFormat::RGB32I: + case TextureInternalFormat::RGB32UI: + return true; + default: + return false; + } + } +// P5e (tx2), CONTRACT-P5E §5.2: THE TEXTURE'S TARGET, from the descriptor whenever a record was +// resolved. The two live `GetTarget()` reads left INSIDE the handle arm (scout G-S2-2, the +// parameter sync and the built-in sampler sync) read Desc.Target here, through the same +// StagedTextureTargetForPipeTarget mapping the storage sync already routes through - so all +// three bodies answer the question from one carrier rather than two authorities. +#define MGB_TEXPARAM_TARGET(rec, obj) \ + ((rec) != nullptr ? BufferImpl::StagedTextureTargetForPipeTarget((rec)->Desc.Target) : (obj)->GetTarget()) + + // The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought. + // Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability + // is the extension AND the pointer, because eglGetProcAddress hands back live-looking + // stubs (see AcquireGLESFunctions). + static MG_External::GLES::glTextureViewEXT_PTR ResolveTextureViewEntryPoint() { + if (g_GLESFuncs.glTextureViewEXT != nullptr) { + return g_GLESFuncs.glTextureViewEXT; + } + return reinterpret_cast(g_GLESFuncs.glTextureViewOES); + } + + // P5e (tx2): see the declaration for the split between what the record answers and what + // the wire still cannot say about a view. + void BackendTextureObject::SyncTextureViewToBackendByRecord( + MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& stateTextureObject) { + if (!g_GLESCapabilities.SupportsTextureView) { + // Unreachable through the API - the frontend refuses glTextureView without + // GL_ARB_texture_view - and kept for the reason the frontend arm keeps its twin. + MGLOG_E_ONCE("Texture view %u reached the backend on a driver without " + "EXT/OES_texture_view.", + record.Desc.GlNameForDiag); + return; + } + // The storage owner by handle, through the same by-handle entry every other draw-path + // texture takes: no frontend object, no allocator probe. + const SharedPtr storageBackendObject = + SyncTextureToBackendByHandle(record.Desc.ViewOf, m_imageBindableStorageRequired); + if (!storageBackendObject) { + MGLOG_E_ONCE("Failed to sync the storage texture {%u, %u} of view %u on the handle arm.", + record.Desc.ViewOf.Slot, record.Desc.ViewOf.Gen, record.Desc.GlNameForDiag); + return; + } + const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId(); + if (storageBackendTextureId == 0) { + MGLOG_D("Storage texture of view %u has no ES name yet.", record.Desc.GlNameForDiag); + return; + } + // P3b/P4b D3: THE SERIAL THIS TWIN STAMPS IS THE STORAGE'S, not this view's, because + // the gate that reads it back (IsDrawSyncCleanByRecord) now asks the same record. It + // is read AFTER the storage sync above - which has just consumed the owner's pending + // uploads - and taken by VALUE, because the applier's vector may move under any later + // call and a Uint64 cannot dangle. A storage record that has gone answers 0, which + // the gate reads as "never clean": the direction that re-syncs. + const MG_Pipe::MGPipeResourceRecord* storageRecord = PipeTextureStorageRecordForRecord(record); + const Uint64 storageSerial = storageRecord != nullptr ? storageRecord->Serial : 0; + if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) { + // The steady case, and the only one this arm has to be fast at: one record read, + // one integer compare and the serial stamp that keeps the aggregate clean gate + // (IsDrawSyncCleanByRecord) answering true for the next draw. + m_syncedResourceSerial = storageSerial; + return; + } + // The view window is a sampler-view CSO, created even when this + // texture is only image-bound or attached to an FBO. It is not part + // of the resource descriptor, and needs no frontend fallback. + (void)stateTextureObject; + const auto& applier = MG_Pipe::MGPipeApplier(); + const auto handle = record.ViewCso; + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= applier.SamplerViewCsos.size()) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-cso\"} {%u,%u}", res.Slot, res.Gen); + std::abort(); + } + const auto& cso = applier.SamplerViewCsos[handle.Slot]; + const auto& view = cso.View; + if (!cso.Live || cso.Gen != handle.Gen || view.Texture != res || !view.NumLevels || !view.NumLayers) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-window\"} {%u,%u}", res.Slot, res.Gen); + std::abort(); + } + const auto viewTarget = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); + const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(viewTarget); + GLenum internalFormat = 0, transferFormat = 0, transferType = 0; + GenerateTextureFormatInfo(static_cast(view.InternalFormat), + &internalFormat, &transferFormat, &transferType, viewTarget); + // A name already bound or used as a view cannot be redefined by + // glTextureView. This also invalidates native binding/FBO caches. + RecreateBackendTexture(); + DebugImpl::ErrorLopper::Clear(); + ResolveTextureViewEntryPoint()(m_backendTextureId, nativeTarget, storageBackendTextureId, + internalFormat, view.MinLevel, view.NumLevels, view.MinLayer, view.NumLayers); + const GLenum error = g_GLESFuncs.glGetError(); + if (error != GL_NO_ERROR) { + MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"texture-view-native\"} {%u,%u} error=0x%x", + res.Slot, res.Gen, error); + std::abort(); + } + m_viewSourceBackendTextureId = storageBackendTextureId; + m_isInitialized = true; + m_backendStorageImmutable = true; + m_prevTextureInfo = {static_cast(record.Desc.InternalFormat), + record.Desc.Width, record.Desc.Height, record.Desc.Depth, view.NumLevels, 0, + record.Desc.Samples, record.Desc.FixedSampleLocations != 0}; + // The storage's, for the steady arm's reason above (P3b/P4b D3). + m_syncedResourceSerial = storageSerial; + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). The client defined + // level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor + // still says so, but the texels are the image's, so nothing below may allocate or upload. + Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) { + const Uint64 imageId = record.SharedImageId; + if (imageId == 0) { + // Respecified away from the image. The ES name is an EGLImage sibling, which must not + // be redefined in place: the immutable flag sends the normal path through + // RecreateBackendTexture, and the EGLImage goes with the name it was bound to. + m_sharedImageId = 0; + m_isInitialized = false; + m_backendStorageImmutable = true; + return false; + } + + // Steady state: bound to this image already (the serial moves with sub-data and with a + // metadata respecify, neither of which changes what the name samples). A failed bind is + // retried only once the record moves. Pending sub-data is the caller's + // (ReplaySharedImageUploads), after the image's sync state has been acquired. + if (m_sharedImageId == imageId && m_isInitialized && + (m_sharedImage != nullptr || m_syncedResourceSerial == record.Serial)) { + m_syncedResourceSerial = record.Serial; + return true; + } + + // A fresh mutable name: the current one may hold immutable storage (which no EGLImage + // can replace) or another image's sibling. RecreateBackendTexture also moves the FBO + // twins' attachment generation and forces the parameter resync the new name needs. + RecreateBackendTexture(); + m_sharedImage = nullptr; + m_sharedImageId = imageId; + m_isInitialized = true; + // Whatever comes next redefines storage through a re-mint (see the imageId == 0 arm). + m_backendStorageImmutable = true; + m_syncedResourceSerial = record.Serial; + + const TextureTarget target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); + if (ConvertTextureTargetToBackendGLEnum(target) != GL_TEXTURE_2D) { + MGLOG_E_ONCE("Shared image texture %u: target %s cannot take an EGLImage; left empty", + record.Desc.GlNameForDiag, MG_Util::ConvertTextureTargetToString(target).c_str()); + return true; + } + String why; + SharedImageImpl::EglImageRef image = SharedImageImpl::CreateEglImage(imageId, why); + if (image == nullptr) { + MGLOG_E_ONCE("Shared image texture %u: image %llu cannot be bound (%s); left empty", + record.Desc.GlNameForDiag, static_cast(imageId), why.c_str()); + return true; + } + Bind(GL_TEXTURE_2D); + if (SharedImageImpl::IsYuv(image)) { + // A YUV image is CONVERTED into the name's own RGBA8 level at each frame's first use + // (SharedImageImpl::ConvertYuvForSampling), never made its storage. + while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + } + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, static_cast(record.Desc.Width), + static_cast(record.Desc.Height)); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + MGLOG_E_ONCE("Shared image texture %u: no %ux%u RGBA8 storage for YUV image %llu; left empty", + record.Desc.GlNameForDiag, record.Desc.Width, record.Desc.Height, + static_cast(imageId)); + return true; + } + } else if (!SharedImageImpl::TargetBoundTexture2D(image)) { + MGLOG_E_ONCE("Shared image texture %u: glEGLImageTargetTexture2DOES refused image %llu; left empty", + record.Desc.GlNameForDiag, static_cast(imageId)); + return true; + } + m_sharedImage = std::move(image); + m_sharedImageUploadsRgb = false; + // The shape the name now has: the descriptor's level 0, and that one level only. + m_prevTextureInfo = {static_cast(record.Desc.InternalFormat), + record.Desc.Width, + record.Desc.Height, + record.Desc.Depth, + 1, + 0, + record.Desc.Samples, + record.Desc.FixedSampleLocations != 0}; + return true; + } + + // SUB-DATA ON A SHARED IMAGE IS WRITTEN INTO IT. An X server drawing with glamor uploads + // its clients' pixels (MIT-SHM / PutImage) into window pixmaps whose level 0 is a shared + // image, with glTexSubImage2D: the texels are in the staged store (the client defined the + // level without data, so only the uploaded boxes are covered), and they go into the + // EGLImage-backed name box by box. Every pending entry leaves the set either way, or the + // per-draw clean gate would never close. Called after AcquireForSampling, so the upload is + // ordered after the image's pending reads and writes like any other use. + void BackendTextureObject::ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record) { + if (record.PendingUploads.empty()) return; + Vector> keys; + Vector> boxes; + keys.reserve(record.PendingUploads.size()); + for (const auto& pending : record.PendingUploads) { + const Uint16 uploadHalf = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget); + keys.emplace_back(uploadHalf, pending.Level); + if (pending.Level != 0) continue; // an image has one level + if (pending.Regions.empty()) { + boxes.emplace_back(uploadHalf, pending.UnionBox); + } else { + for (const auto& region : pending.Regions) + boxes.emplace_back(uploadHalf, MG_Pipe::MGPBox{region.X, region.Y, region.Z, region.W, region.H, + region.D}); + } + } + if (m_sharedImage != nullptr && !boxes.empty()) { + auto& store = MG_Record::ServerStagedTexture(); + const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(record.Desc.Resource); + Bind(GL_TEXTURE_2D); + ScopedDefaultUnpackState unpackState; + BufferImpl::BindPixelUnpackBufferId(0); + for (const auto& [uploadHalf, box] : boxes) { + Int rowTexels = 0; + // A shared image is RGBA8 to every client that names it (the level the client + // defined over it): four bytes a texel. + const Uint8* bytes = + store.CoveredBoxBytes(key, uploadHalf, 0, box.X, box.Y, box.W, box.H, 4, &rowTexels); + if (bytes == nullptr) { + MGLOG_E_ONCE("Shared image texture %u: an upload of %ux%u at (%d,%d) has no staged texels; " + "not written", + record.Desc.GlNameForDiag, box.W, box.H, box.X, box.Y); + continue; + } + const Uint8* origin = + bytes + (static_cast(box.Y) * static_cast(rowTexels) + static_cast(box.X)) * 4; + if (!m_sharedImageUploadsRgb) { + while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + } + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowTexels); + g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast(box.W), + static_cast(box.H), GL_RGBA, GL_UNSIGNED_BYTE, origin); + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + // An image of an X format (no alpha) is an RGB8 texture to the driver, which + // takes only RGB texels: this image's uploads are converted from now on. + if (g_GLESFuncs.glGetError() != GL_INVALID_OPERATION) continue; + m_sharedImageUploadsRgb = true; + } + Vector rgb(static_cast(box.W) * box.H * 3); + for (Uint32 row = 0; row < box.H; ++row) { + const Uint8* src = origin + static_cast(row) * static_cast(rowTexels) * 4; + Uint8* dst = rgb.data() + static_cast(row) * box.W * 3; + for (Uint32 x = 0; x < box.W; ++x) { + dst[x * 3 + 0] = src[x * 4 + 0]; + dst[x * 3 + 1] = src[x * 4 + 1]; + dst[x * 3 + 2] = src[x * 4 + 2]; + } + } + g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast(box.W), + static_cast(box.H), GL_RGB, GL_UNSIGNED_BYTE, rgb.data()); + g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } + MGLOG_I_ONCE("Shared image texture %u: sub-data written into its image (%s texels)", + record.Desc.GlNameForDiag, m_sharedImageUploadsRgb ? "RGB" : "RGBA"); + } + for (const auto& [uploadHalf, level] : keys) ConsumePipeTextureUpload(record.Desc.Resource, uploadHalf, level); + } +#endif + + // Stamps the same per-draw clean-gate keys a completed storage sync stamps, so a view + // that needs no work costs the same nothing per draw that any other synced texture does. + void BackendTextureObject::StampViewSyncKeys( + const SharedPtr& stateTextureObject) { + if (MG_Pipe::gPipeInputs->IsLive()) { + m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId(); + m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); + m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion(); + } + m_syncedContentVersion = stateTextureObject->GetContentVersion(); + } + + const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler( + const SharedPtr& stateTextureObject, + const MG_Pipe::MGPipeResourceRecord* record) { + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " + "built-in sampler cannot be pushed (handle {%u, %u})", + TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot, + m_pushedSyncHandle.Gen); + return nullptr; + } + // NO set_texture_params HAS BEEN APPLIED FOR THIS TEXTURE YET, which is not the same + // thing as a malformed one and must not be read as one. ParamsSerial is the + // applier's own "have I ever stored a params record here" (it is bumped on every + // set_texture_params and starts at 0), and Params is value-initialised beside it - + // so on a texture the application has never given a parameter to, BuiltinSampler is + // null because the record was never written, not because a client wrote a null. + // There is nothing to push in that state; SyncTextureParamsToBackend's own gate + // reaches the same conclusion silently (m_syncedParamsSerial starts at 0 too and the + // compare skips), and this decline is made silent for the same reason. Found by the + // verification round's census: three cases were declining loudly here on textures + // that had simply never been glTexParameter'd. + if (record->ParamsSerial == 0) { + MGLOG_D("Texture %u has no set_texture_params record yet, so it has no built-in " + "sampler to push.", + TextureDiagName(stateTextureObject, record)); + return nullptr; + } + // D-E1: kMGPipeNullHandle in a record that WAS written is Fatal{ProtocolCorruption} + // territory - EVERY ITextureObject owns a SamplerObject, so a null is a malformed + // record and not "no sampler". The applier is where that verdict is raised; this + // side names it and declines, because a backend that sampled through whatever the + // driver texture last held would be the silent half of the same bug. + const MG_Pipe::MGPipeHandle builtin = record->Params.BuiltinSampler; + if (MG_Pipe::MGPipeHandleIsNull(builtin)) { + MGLOG_E_ONCE("MGPipe: texture %u's set_texture_params names the null handle as its " + "built-in sampler CSO; every texture owns a sampler object, so this " + "record is malformed - declining the built-in sampler push", + TextureDiagName(stateTextureObject, record)); + return nullptr; + } + const auto* cso = PipeSamplerCsoRecordForHandle(builtin); + if (cso == nullptr) { + MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier " + "record - declining the built-in sampler push", + TextureDiagName(stateTextureObject, record), builtin.Slot, builtin.Gen); + return nullptr; + } + // THE HANDLE IS PART OF THE KEY, not just the serial. Sampler CSOs are + // content-addressed at capacity 256 (D-F1), so two textures with identical sampling + // share one CSO and a texture whose sampling diverges is re-pointed at a DIFFERENT + // CSO whose serial may well be smaller than the one it left. Keying on the serial + // alone would then skip the re-push and leave the driver texture filtering with the + // old CSO's values for ever. + // + // SamplerResync is the second half of the gate and it is the one that matters more + // than mis-filtering: ES makes a texture INCOMPLETE when its filters do not suit its + // level set, and an incomplete texture samples (0,0,0,1) rather than its contents. + // The server SETS it (RecreateBackendTexture) and the wire byte exists so the client + // can force a resync it knows about; neither side clears the other's copy (D-E2), so + // it is read here and never written back. + if (m_syncedBuiltinSampler == builtin && m_syncedBuiltinSamplerSerial == cso->Serial && + record->Params.SamplerResync == 0 && !m_forceSamplerResync) { + MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", + m_backendTextureId); + return nullptr; + } + m_syncedBuiltinSampler = builtin; + m_syncedBuiltinSamplerSerial = cso->Serial; + m_forceSamplerResync = false; + return &cso->Params; + } + + void BackendTextureObject::SyncBuiltinSamplerToBackend( + const SharedPtr& stateTextureObject) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + + // P5e (tx2): NULL IS THE BY-HANDLE ARM, not an error, once a handle has been noted on + // this twin - the record and the built-in sampler's CSO answer everything below and + // the frontend object is not consulted at all. Without a handle it is still the + // caller's bug it always was. P5e (fix1): and with a handle noted on an arm that + // cannot serve it, it is Fatal{RoleViolation, "texture-handle-arm"} - see the + // refusal's own comment beside MGB_TEXTURE_HANDLE_ARM_OFF. + if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncBuiltinSamplerToBackend")) { + MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); + return; + } + + // P4a (D-E1): the fifteen sampler values are SAMPLER state (GL 4.6 table 23.18) and + // Espryt pushes them with glTexParameter* onto the TEXTURE rather than with + // glBindSampler onto the unit - behaviour P4a preserves exactly. What changes is + // where the values and the "did they move" answer come from. + // + // The whole arm choice is a preprocessor #if/#else rather than a runtime branch + // around one shared prologue, and that is a G1 requirement (D-P): the PULL build's + // text below the #else is the pre-P4a text token for token, so its object code + // cannot move. Routing the pull path through a pointer it did not have before was + // measured at -2 bytes on this function, which is a G1 failure. + const SamplerParameters* pendingSamplerParams = nullptr; + // P5e (tx2): resolved ONCE and shared with the target read below, which used to be + // the frontend `GetTarget()` this arm was not allowed to make. + const MG_Pipe::MGPipeResourceRecord* pushedDesc = nullptr; + pushedDesc = ResolveOwnRecord(stateTextureObject); + pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject, pushedDesc); + if (pendingSamplerParams == nullptr) return; + + MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", + m_backendTextureId, TextureDiagName(stateTextureObject, pushedDesc)); + auto targetInternal = MGB_TEXPARAM_TARGET(pushedDesc, stateTextureObject); + GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal); + MGLOG_D(" Texture target for syncing is %s", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + if (!IsSupportedTextureTarget(targetInternal)) { + MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + return; + } + + const SamplerParameters& samplerParams = *pendingSamplerParams; + if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { + m_cacheSamplerParameters = samplerParams; + return; + } + + Bind(target); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + // Update built-in sampler parameters + MGLOG_D("Updating sampler parameters for texture with ID: %u", m_backendTextureId); + +#define SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \ + if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \ + g_GLESFuncs.glTexParameteri(target, glName, \ + MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \ + m_cacheSamplerParameters.internalName = samplerParams.internalName; \ + DebugImpl::ErrorLopper::Loop( \ + [file = __FILE__, line = __LINE__, func = __func__, \ + t = MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)](GLenum err) { \ + MGLOG_D("%s(%s:%d) ES error %s, GL_TEXTURE_MIN_FILTER = %s", func, file, line, \ + MG_Util::ConvertGLEnumToString(err).c_str(), MG_Util::ConvertGLEnumToString(t).c_str()); \ + }); \ + } + + if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter || + m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) { + g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MIN_FILTER, + (GLint)ResolveBackendMinFilter(samplerParams, IsAngleLlvmpipeRenderer())); + m_cacheSamplerParameters.minFilter = samplerParams.minFilter; + m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode; + } + if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) { + g_GLESFuncs.glTexParameteri( + target, GL_TEXTURE_MAG_FILTER, + (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None)); + m_cacheSamplerParameters.magFilter = samplerParams.magFilter; + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode) + SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode) + if (SupportsWrapR(targetInternal)) { + SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode) + } else { + m_cacheSamplerParameters.wrapR = samplerParams.wrapR; + } + SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc) + SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode) + if (m_cacheSamplerParameters.minLod != samplerParams.minLod) { + g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MIN_LOD, samplerParams.minLod); + m_cacheSamplerParameters.minLod = samplerParams.minLod; + } + if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) { + g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod); + m_cacheSamplerParameters.maxLod = samplerParams.maxLod; + } + if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) { + if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) { + g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, + samplerParams.maxAnisotropy); + } + // Unsupported GLES backends intentionally treat anisotropy as a + // frontend-only no-op; remember the observed value so the cache + // remains coherent without issuing an illegal enum every sync. + m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); +#undef SYNC_TEX_SAMPLER_PARAM_IF_CHANGED + } + + const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams( + const SharedPtr& stateTextureObject) { + // P5e (tx2): the handle the caller adopted this twin with, and only failing that the + // client allocator - ResolveOwnRecord holds the whole of that rule. + const auto* record = ResolveOwnRecord(stateTextureObject); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " + "parameters cannot be pushed (handle {%u, %u})", + TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot, + m_pushedSyncHandle.Gen); + return nullptr; + } + // ParamsSerial + ForceResync replace m_syncedTextureParamsVersion + + // m_forceTextureParamsResync. ForceResync exists because the widened-channel + // carrier needs a swizzle override the frontend params version does not move for; + // it is SET by the server (RequireImageBindableStorage, RecreateBackendTexture set + // m_forceTextureParamsResync on the twin) and by the client on the wire, and + // NEITHER SIDE CLEARS THE OTHER'S (D-E2) - so the wire byte is read and never + // written back, while the twin's own flag is cleared here exactly as it was before. + if (m_syncedParamsSerial == record->ParamsSerial && record->Params.ForceResync == 0 && + !m_forceTextureParamsResync) { + MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.", + m_backendTextureId); + return nullptr; + } + m_syncedParamsSerial = record->ParamsSerial; + m_forceTextureParamsResync = false; + return record; + } + + // P4a (D-E1): the six values SyncTextureParamsToBackend pushes, spelled once per arm. + // MACROS, for the reason MGB_LEVEL_NEEDS_UPLOAD gives one function up: in the PULL build + // each expands to the pre-P4a expression at its original site, so that build's tokens - + // and its object code, whose symbol sizes G1 pins to the byte - are unchanged. Reading + // them into locals instead was measured at +9 bytes on this function. + // + // Four of the six come from MGPTextureParams, which is per texture OBJECT and + // independent of any binding - that is what closes D-E3's gap, where a texture that is + // only a READ-framebuffer attachment reaches SyncMipmapsToBackend and nothing else. The + // FORMAT comes from the descriptor beside them, and the BORDER comes from the built-in + // sampler's CSO (D-F4), because a border colour is sampler state. +#define MGB_TEXPARAM_LEVEL_RANGE \ + (pushedRecord != nullptr ? UintVec2{static_cast(pushedRecord->Params.BaseLevel), \ + static_cast(pushedRecord->Params.MaxLevel)} \ + : stateTextureObject->GetLevelRange()) +#define MGB_TEXPARAM_FORMAT \ + (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.InternalFormat) \ + : stateTextureObject->GetFormat()) +#define MGB_TEXPARAM_SWIZZLE \ + (pushedRecord != nullptr \ + ? Vec4{static_cast(pushedRecord->Params.Swizzle[0]), \ + static_cast(pushedRecord->Params.Swizzle[1]), \ + static_cast(pushedRecord->Params.Swizzle[2]), \ + static_cast(pushedRecord->Params.Swizzle[3])} \ + : stateTextureObject->GetAllSwizzleParams()) +// The contract now spells this encoding (c0c, ID-12 DV-2): kMGPipeDepthStencilModeDepth = 0 = +// GL_DEPTH_COMPONENT, kMGPipeDepthStencilModeStencil = 1 = GL_STENCIL_INDEX. v1 open-coded the +// direction as a bare `!= 0` and recorded it as a deviation; the constants are read here instead, +// so a future re-numbering moves both sides at once. ZERO MEANS THE DEFAULT is still what decides +// the direction: GL_DEPTH_COMPONENT is the GL and ES default and a texture that never asks for +// the stencil aspect never emits the call, so a zeroed record has to decode to exactly what such +// a texture already has. +#define MGB_TEXPARAM_DS_MODE \ + (pushedRecord != nullptr \ + ? (pushedRecord->Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX \ + : GL_DEPTH_COMPONENT) \ + : stateTextureObject->GetDepthStencilTextureMode()) +#define MGB_TEXPARAM_BORDER_F (pushedBorder != nullptr ? pushedBorder->borderColor : stateTextureObject->GetBorderColor()) +#define MGB_TEXPARAM_BORDER_I \ + (pushedBorder != nullptr ? pushedBorder->borderColorI : stateTextureObject->GetBorderColorI()) +#define MGB_TEXPARAM_BORDER_UI \ + (pushedBorder != nullptr ? pushedBorder->borderColorUI : stateTextureObject->GetBorderColorUI()) +#define MGB_TEXPARAM_BORDER_FORM \ + (pushedBorder != nullptr ? pushedBorder->borderColorForm : stateTextureObject->GetBorderColorForm()) + + void BackendTextureObject::SyncTextureParamsToBackend( + const SharedPtr& stateTextureObject) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + + // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; + // see the twin note in SyncBuiltinSamplerToBackend. + if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncTextureParamsToBackend")) { + MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); + return; + } + + // P4a (D-E1/D-E3): where the parameter values come from, and what says they moved. + // Null on the handle arm means "nothing to do" and is NEVER a fall-back to the + // frontend; the legacy arm keeps the params-version pair verbatim, token for token, + // because the PULL build's object code may not move (D-P). + const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr; + const SamplerParameters* pushedBorder = nullptr; + // Whether the four border comparands can be READ at all this sync. On the handle arm + // a missing sampler CSO makes them unreadable and the border block below is skipped + // with the refusal already named; on the legacy arm they always come from the + // frontend object, so it is constant true and folds away. + Bool borderColorReadable = true; + pushedRecord = ResolvePushedTextureParams(stateTextureObject); + if (pushedRecord == nullptr) return; + // D-F4: the border colour is SAMPLER state (GL 4.6 table 23.18) and lives on the + // built-in sampler's CSO, not on MGPTextureParams - P4a deliberately does not + // put SamplerParameters on the wire twice. All FOUR comparands (float, int, + // uint, form) still cross and are still all compared below, because two integer + // borders differing above 2^24 share one float and a Float -> Int transition can + // leave every number unchanged while needing a different driver entry point. + const auto* borderCso = PipeSamplerCsoRecordForHandle(pushedRecord->Params.BuiltinSampler); + if (borderCso != nullptr) { + pushedBorder = &borderCso->Params; + } else { + borderColorReadable = false; + MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier " + "record, so its border colour cannot be pushed", + TextureDiagName(stateTextureObject, pushedRecord), + pushedRecord->Params.BuiltinSampler.Slot, + pushedRecord->Params.BuiltinSampler.Gen); + } + + MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId, + TextureDiagName(stateTextureObject, pushedRecord)); + + auto targetInternal = MGB_TEXPARAM_TARGET(pushedRecord, stateTextureObject); + GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal); + MGLOG_D(" Texture target for syncing is %s", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + if (!IsSupportedTextureTarget(targetInternal)) { + MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); + return; + } + + // Multisample targets reject the *sampler* parameters (LOD range, border color) but + // GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them. + // Bailing out entirely used to drop every swizzle write on the floor, which is what the + // frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which + // deliberately omits the swizzle enums). Note the caches for the skipped parameters are + // still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the + // change detection below would swallow the very writes we came here to emit. + const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal); + if (isMultisampleTarget) { + m_cacheLodRange = MGB_TEXPARAM_LEVEL_RANGE; + if (borderColorReadable) m_cacheBorderColor = MGB_TEXPARAM_BORDER_F; + } + + Bind(target); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + // Update texture parameters + MGLOG_D("Updating texture parameters for texture with ID: %u", m_backendTextureId); + + const auto& levelRange = MGB_TEXPARAM_LEVEL_RANGE; + + if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) { + g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast(levelRange.x())); + m_cacheLodRange.x() = levelRange.x(); + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) { + g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast(levelRange.y())); + m_cacheLodRange.y() = levelRange.y(); + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + // A three-channel format widened to four to keep the image colour-renderable (see + // NormalizePixelFormat) gains an alpha channel the frontend format does not have, and + // whatever the draw that filled it wrote there is not what GL would report: a format + // without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the + // promotion stays invisible, composed with the swizzle the application asked for. + Vec4 swizzleParams = MGB_TEXPARAM_SWIZZLE; + if (TextureImpl::BackendTextureFormatAddsAlpha(MGB_TEXPARAM_FORMAT, targetInternal)) { + for (SizeT channel = 0; channel < 4; ++channel) { + if (swizzleParams[channel] == TextureSwizzleParam::Alpha) { + swizzleParams[channel] = TextureSwizzleParam::One; + } + } + } + // The same composition for the image-format widening, which can add TWO or THREE + // channels rather than one (GL_R8UI carried in a GL_RGBA8UI). GL reads a channel the + // format does not have as 0, except alpha, which reads as 1 - so a sampler must see + // those constants and not whatever the widened storage holds. The upload and the + // shader's own store mask already keep them at exactly these values; this covers + // storage nothing has written yet (glTexStorage with no upload), whose surplus + // channels are undefined. Composed with the application's own swizzle for the same + // reason as the alpha case above: GL_TEXTURE_SWIZZLE names a SOURCE channel of the + // logical texel, so it is the source that is substituted, never the destination. + if (const auto imageWidening = + m_imageBindableStorageRequired + ? TextureImpl::GetImageBindableStorageWidening(MGB_TEXPARAM_FORMAT) + : TextureImpl::ImageBindableStorageWidening{}) { + for (SizeT channel = 0; channel < 4; ++channel) { + switch (swizzleParams[channel]) { + case TextureSwizzleParam::Green: + if (imageWidening.SourceChannels < 2) swizzleParams[channel] = TextureSwizzleParam::Zero; + break; + case TextureSwizzleParam::Blue: + if (imageWidening.SourceChannels < 3) swizzleParams[channel] = TextureSwizzleParam::Zero; + break; + case TextureSwizzleParam::Alpha: + if (imageWidening.SourceChannels < 4) swizzleParams[channel] = TextureSwizzleParam::One; + break; + default: + break; + } + } + } + if (swizzleParams != m_cacheSwizzleParams) { +#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \ + if (m_cacheSwizzleParams.func != swizzleParams.func) { \ + g_GLESFuncs.glTexParameteri(target, glEnum, MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams.func)); \ + m_cacheSwizzleParams.func = swizzleParams.func; \ + } + SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(r(), GL_TEXTURE_SWIZZLE_R); + SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(g(), GL_TEXTURE_SWIZZLE_G); + SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(b(), GL_TEXTURE_SWIZZLE_B); + SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(a(), GL_TEXTURE_SWIZZLE_A); +#undef SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED + m_cacheSwizzleParams = swizzleParams; + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + + // GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver + // without it every such call is INVALID_ENUM, so the parameter is simply not synced. + // + // The FORM has to be forwarded along with the value. A border colour set through + // glTexParameterIiv/Iuiv is an integer one, and an isampler2D/usampler2D fetch of the + // border returns whatever the driver's integer border register holds - so pushing it + // through glTexParameterfv handed the driver float 255.0 and the shader read back + // 1132396544, the bit pattern of that float. glTexParameterIiv/Iuiv are ES 3.2 core + // beside GL_TEXTURE_BORDER_COLOR itself, so they sit behind the same capability gate; + // the entry-point null check covers a driver that advertises the extension without them. + // The redundancy filter has to look at the AUTHORITATIVE representation, not just the + // float one: two integer borders that differ above 2^24 (16777216 and 16777217, say) + // collapse onto the same float, so a float-only comparison would skip the second sync and + // leave the driver holding the first value forever. + // m-3: GATED, because on the handle arm an unreadable border means the CSO record is + // missing and the block below is skipped with the refusal already named - so v1's + // unconditional initialiser was a live frontend read on a switched-over path whose + // value nothing then used. The value a skipped block sees is the cache's own, which + // cannot make the condition true. + const auto borderColorForm = borderColorReadable ? MGB_TEXPARAM_BORDER_FORM : m_cacheBorderColorForm; + if (!isMultisampleTarget && + borderColorReadable && + g_GLESCapabilities.SupportsTextureBorderClamp && + (m_cacheBorderColor != MGB_TEXPARAM_BORDER_F || + m_cacheBorderColorI != MGB_TEXPARAM_BORDER_I || + m_cacheBorderColorUI != MGB_TEXPARAM_BORDER_UI || + m_cacheBorderColorForm != borderColorForm)) { + if (borderColorForm == BorderColorForm::Int && g_GLESFuncs.glTexParameterIiv) { + const auto& borderColorI = MGB_TEXPARAM_BORDER_I; + const GLint borderColorArray[4] = {borderColorI.x(), borderColorI.y(), borderColorI.z(), + borderColorI.w()}; + g_GLESFuncs.glTexParameterIiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); + } else if (borderColorForm == BorderColorForm::Uint && g_GLESFuncs.glTexParameterIuiv) { + const auto& borderColorUI = MGB_TEXPARAM_BORDER_UI; + const GLuint borderColorArray[4] = {borderColorUI.x(), borderColorUI.y(), borderColorUI.z(), + borderColorUI.w()}; + g_GLESFuncs.glTexParameterIuiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); + } else { + const auto& borderColor = MGB_TEXPARAM_BORDER_F; + const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), + borderColor.w()}; + g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); + } + m_cacheBorderColor = MGB_TEXPARAM_BORDER_F; + m_cacheBorderColorI = MGB_TEXPARAM_BORDER_I; + m_cacheBorderColorUI = MGB_TEXPARAM_BORDER_UI; + m_cacheBorderColorForm = borderColorForm; + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + + // GL_DEPTH_STENCIL_TEXTURE_MODE (GL_ARB_stencil_texturing / ES 3.1 core): which aspect + // of a packed depth/stencil image a sampler reads. Until this was forwarded the + // frontend kept the mode as a pure shadow - glGetTexParameter answered it, sampling + // ignored it - so a usampler2D bound to a D24S8 texture in STENCIL_INDEX mode read the + // depth aspect. Texture state rather than sampler state, so multisample targets take + // it too (ES 3.1 8.10 lists it among the three pnames they accept). It is only sent + // when it has moved, which for the overwhelming majority of textures is never. + const Bool supportsStencilTextureMode = + g_GLESCapabilities.GLESVersion.Major > 3 || + (g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1); + if (supportsStencilTextureMode) { + const GLenum depthStencilTextureMode = MGB_TEXPARAM_DS_MODE; + if (m_cacheDepthStencilTextureMode != depthStencilTextureMode) { + g_GLESFuncs.glTexParameteri(target, GL_DEPTH_STENCIL_TEXTURE_MODE, + static_cast(depthStencilTextureMode)); + m_cacheDepthStencilTextureMode = depthStencilTextureMode; + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, + MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + } + } +#undef MGB_TEXPARAM_LEVEL_RANGE +#undef MGB_TEXPARAM_FORMAT +#undef MGB_TEXPARAM_SWIZZLE +#undef MGB_TEXPARAM_DS_MODE +#undef MGB_TEXPARAM_BORDER_F +#undef MGB_TEXPARAM_BORDER_I +#undef MGB_TEXPARAM_BORDER_UI +#undef MGB_TEXPARAM_BORDER_FORM + + void ActivateTextureUnit(Uint unit) { + if (unit == g_activeTextureUnit) { + return; + } + g_GLESFuncs.glActiveTexture(GL_TEXTURE0 + unit); + g_activeTextureUnit = unit; + } + + void UnbindTexture(Uint unit, GLenum target) { // Activates `unit` when an unbind is issued + auto targetN = static_cast(MG_Util::ConvertGLEnumToTextureTarget(target)); + if (g_boundTexturesCache[unit][targetN] == nullptr) return; + + ActivateTextureUnit(unit); + g_GLESFuncs.glBindTexture(target, 0); + g_boundTexturesCache[unit][targetN] = nullptr; + } + + Uint g_activeTextureUnit = 0; + Array, + MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> + g_boundTexturesCache; + TwinRegistry g_backendTextureObjects; + +#if MOBILEGL_BUILD_DISAGGREGATED + void WritePendingSharedImageUploads() { + auto& applier = MG_Pipe::MGPipeApplier(); + for (SizeT slot = 0; slot < applier.TextureResources.size(); ++slot) { + const auto& record = applier.TextureResources[slot]; + if (!record.Live || record.SharedImageId == 0 || record.PendingUploads.empty()) continue; + const MG_Pipe::MGPipeHandle handle{static_cast(slot), record.Gen}; + auto* twin = g_backendTextureObjects.FindByHandle(handle); + if (twin == nullptr || !*twin) continue; + (*twin)->SyncMipmapsToBackendByHandle(handle); + } + } +#endif + + // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract. + BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res) { + if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; + // THE RECORD FIRST: a texture handle with no resource record is a seam defect - the + // client named a texture it never created, or one whose resource_destroy has + // already applied while a binding still names it - and a twin adopted for it would + // own a driver texture nothing ever uploads to. + if (PipeTextureRecordForHandle(res) == nullptr) { + MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle " + "arm, so no driver texture can be built for it and the unit keeps what " + "it holds", + res.Slot, res.Gen); + return nullptr; + } + auto* slot = AdoptTwinByHandle(g_backendTextureObjects, res, "texture"); + if (slot == nullptr) return nullptr; + if (!*slot) *slot = MakeShared(); + return slot->get(); + } + + void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite) { + // The storage owner's handle, one hop per view like the frontend walk above (the + // client composes a view of a view onto its root, so one hop is the real shape). + MG_Pipe::MGPipeHandle storage = res; + for (Uint hop = 0; hop < 4 && !MG_Pipe::MGPipeHandleIsNull(storage); ++hop) { + const auto* record = PipeTextureRecordForHandle(storage); + if (record == nullptr || MG_Pipe::MGPipeHandleIsNull(record->Desc.ViewOf)) break; + storage = record->Desc.ViewOf; + } + if (auto* twin = ResolveTextureTwin(storage)) twin->NoteDriverSideWrite(); + // P8-E: the staged store stops standing for this texture's content (a no-op on a + // monolith arm, where the store does not copy). + if (!storeFollowsTheWrite && !MG_Pipe::MGPipeHandleIsNull(storage)) { + MG_Record::ServerStagedTexture().MarkDriverWritten( + MG_Record::StagedTextureStore::KeyForHandle(storage)); + } + } + Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin, + MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel, + const IntVec3& destinationOrigin, const IntVec3& size) { + auto& store = MG_Record::ServerStagedTexture(); + // P13 W4b: an aliasing store (monolith's record arm) follows too; see FollowCopy. + if (!store.Holds()) return false; + // The store's level key for an endpoint copy-image addresses as ONE image with its + // layers on z: exactly one upload target (not a cube map, whose faces are six store + // levels) and not a 1D array (the store keeps its layers as rows). A view's own levels + // are byteless, so FollowCopy answers false for it by itself. + const auto storeTarget = [](MG_Pipe::MGPipeHandle handle, Uint16& target) { + const auto* record = PipeTextureRecordForHandle(handle); + if (record == nullptr || + record->Desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex1DArray)) { + return false; + } + const auto& targets = BufferImpl::StagedUploadTargetsForPipeTarget(record->Desc.Target); + if (targets.size() != 1) return false; + target = static_cast(targets[0]); + return true; + }; + Uint16 sourceTarget = 0, destinationTarget = 0; + if (!storeTarget(source, sourceTarget) || !storeTarget(destination, destinationTarget)) return false; + // ONE FORMAT BOTH ENDS. Copy-image moves the GL texel bits; the store holds each + // format's canonical shadow, which for most formats IS those bits and for some is not + // (the 16-bit packed formats are kept as UNorm8, RGB10 / RGB12 as UNorm16). Between two + // levels of one format the canonical move and the GL move agree whatever the format; + // across two compatible formats they need not, so that copy is not followed. + if (PipeTextureRecordForHandle(source)->Desc.InternalFormat != + PipeTextureRecordForHandle(destination)->Desc.InternalFormat) { + return false; + } + return store.FollowCopy(MG_Record::StagedTextureStore::KeyForHandle(source), sourceTarget, + static_cast(sourceLevel), sourceOrigin, + MG_Record::StagedTextureStore::KeyForHandle(destination), + destinationTarget, static_cast(destinationLevel), destinationOrigin, + size); + } + } // namespace TextureImpl + + // ===================================================================================== + // P5e SEAMS: DECLARED AND REFUSED HERE, BODIED BY THE FAMILY PACKAGES + // (docs/archive/contracts/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature + // in c0e so both sides compile from day one") + // ===================================================================================== + // + // WHY THEY ARE ALL IN ONE BLOCK AND WHY THE BODIES ABORT. P5e lands as eight packages in + // eight parallel worktrees, and five of them are on opposite sides of these calls: tx2 + // writes SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle while fb's + // attachment sync and image sweep CALL them; fb writes the two SyncToBackendByHandle + // overloads while tx2's detach walk needs the reverse index behind them; id rekeys the + // registries the three resolvers read. A package that had to add its own declaration would + // collide with the package that added the other half, which is the merge trap P4a's + // contract package was written to avoid. + // + // So the signature is fixed HERE, once, and each family's commit replaces a body. The body + // ABORTS BY NAME rather than returning null or doing nothing: a resolver that answered null + // would render a blank draw and a sync that did nothing would render stale pixels, and both + // would be green lanes. Nothing calls any of them at this commit - every call site is still + // on the frontend overload beside it - so the abort is a link-time seam, not a runtime one. + // + // A package REPLACES the body in place and deletes the matching comment; it does not add a + // second definition elsewhere, or the linker's answer depends on link order. + + namespace TextureImpl { + // ---- P5e (tx2): THE TEXTURE FAMILY'S BY-HANDLE ENTRY, CONTRACT-P5E §5.2 -------------- + // + // SamplerImpl::ResolveSamplerCsoTwin is the shape (Managers.cpp, the sampler half was + // already run-ahead clean on its record arm and this is that shape brought to the texture + // half): RECORD FIRST, twin by GetOrCreateByHandle, three syncs driven from the record, + // no frontend touch and no allocator probe anywhere on the path. + // + // WHAT THE FRONTEND ENTRY PAYS AND THIS ONE DOES NOT. SyncTextureObjectToBackend takes a + // by-VALUE copy of the twin and does a second Find at the tail, because its registry's + // Find returns a reference INTO an open-addressed map that a NESTED sync (a view syncing + // its storage texture) can rehash out from under it. A slot-table entry is an ARRAY + // ELEMENT: a nested GetOrCreateByHandle can only grow the vector, so the answer is + // re-INDEXED at the tail rather than pinned by a refcount, and the copy is a refcount + // this arm does not pay. + // + // A null return is a NAMED decline and never a mint: no record (the client named a + // texture it never created, or one whose resource_destroy has already applied), or a slot + // whose live generation is ahead of the handle's. A caller binds nothing and the unit + // keeps what it holds. + SharedPtr& SyncTextureToBackendByHandle(MG_Pipe::MGPipeHandle texture, + Bool imageBindableStorageRequired) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // The one storage every decline arm can return a reference to. Never written. + static SharedPtr s_noTwin{}; + s_noTwin = nullptr; + + const auto* record = PipeTextureRecordForHandle(texture); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle " + "arm, so no driver texture can be built for it and the caller keeps " + "what it holds", + texture.Slot, texture.Gen); + return s_noTwin; + } + auto* slot = g_backendTextureObjects.GetOrCreateByHandle(texture); + if (slot == nullptr) { + MGLOG_E_ONCE("MGPipe: texture {%u, %u} cannot be adopted on the handle arm (the slot's " + "live generation is %u), so no driver texture is built for it", + texture.Slot, texture.Gen, g_backendTextureObjects.LiveGenAt(texture.Slot)); + return s_noTwin; + } + if (!*slot) *slot = MakeShared(); + // THE HANDLE IS NOTED BEFORE ANY SYNC RUNS. It is what the three prologues read + // instead of probing the client allocator, and what selects the by-handle arm inside + // them, so it has to be in place before the first of them is called. + (*slot)->NotePushedSyncHandle(texture); + + const SharedPtr twin = *slot; + if (imageBindableStorageRequired) { + twin->RequireImageBindableStorageByHandle(texture, *record); + } + static const SharedPtr kNoFrontendTexture{}; + twin->SyncTextureParamsToBackend(kNoFrontendTexture); + twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture); + twin->SyncMipmapsToBackend(kNoFrontendTexture); + // The storage sync may RE-MINT the driver texture, which discards every parameter the + // two calls above just pushed - the frontend entry's argument, unchanged. + if (twin->NeedsParameterResync()) { + twin->SyncTextureParamsToBackend(kNoFrontendTexture); + twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture); + } + // ONE re-index, and only because a nested adopt may have GROWN the vector; nothing on + // this arm erases a live slot, so the entry itself cannot have gone. + auto* refreshed = g_backendTextureObjects.FindByHandle(texture); + if (refreshed != nullptr && *refreshed) { + return *refreshed; + } + s_noTwin = twin; + return s_noTwin; + } + + void BackendTextureObject::SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture) { + // The STORAGE half alone, for fb's attachment sync and the image sweep: an attachment + // needs its levels on the driver, not its sampler parameters (§5.4). The handle note + // is what routes the body below onto the record. + NotePushedSyncHandle(texture); + static const SharedPtr kNoFrontendTexture{}; + SyncMipmapsToBackend(kNoFrontendTexture); + } + + // P5e (tx2), CONTRACT-P5E §5.2. See the declaration for the clause-by-clause derivation + // and for why BOTH halves of each resync pair are read here. + Bool BackendTextureObject::IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res, + const MG_Pipe::MGPipeResourceRecord& record) const { + (void)res; + // P3b/P4b D3: THE TWO STORAGE CLAUSES ARE ASKED OF THE STORAGE RECORD, which is this + // record for every texture that owns its texels and the record named by Desc.ViewOf + // for a glTextureView. A view's own Serial is moved by nothing an upload does - the + // client keys every resource_subdata on the storage owner, and the applier moves that + // record's Serial and PendingUploads alone - so reading the view's here answered + // "clean" for every owner write after the view's first sample. The three PARAMETER + // clauses below stay this record's: a view has its own texture parameters and its own + // built-in sampler, which is the whole point of the name existing. See + // PipeTextureStorageRecordForRecord and CONTRACT-P5E.md §5.2. + const MG_Pipe::MGPipeResourceRecord* storage = PipeTextureStorageRecordForRecord(record); + if (storage == nullptr) return false; +#if MOBILEGL_BUILD_DISAGGREGATED + // Never clean while bound to a shared image: see IsDrawSyncClean. + if (m_sharedImage != nullptr) return false; +#endif + // SyncMipmapsToBackend's own early-out, verbatim. + if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != storage->Serial || + !storage->PendingUploads.empty()) { + return false; + } + // SyncTextureParamsToBackend's (ResolvePushedTextureParams' gate). + if (m_syncedParamsSerial != record.ParamsSerial || record.Params.ForceResync != 0 || + m_forceTextureParamsResync) { + return false; + } + // SyncBuiltinSamplerToBackend's (ResolvePushedBuiltinSampler's gate). A texture whose + // params have never been set has no built-in sampler to push and that prologue + // declines SILENTLY - so a zero ParamsSerial is clean here for the same reason, and + // the CSO lookup below is only made when there is a CSO to look up. + if (record.ParamsSerial != 0) { + const MG_Pipe::MGPipeHandle builtin = record.Params.BuiltinSampler; + if (m_syncedBuiltinSampler != builtin) return false; + const auto* cso = PipeSamplerCsoRecordForHandle(builtin); + if (cso == nullptr || m_syncedBuiltinSamplerSerial != cso->Serial) return false; + if (record.Params.SamplerResync != 0 || m_forceSamplerResync) return false; + } else if (m_forceSamplerResync) { + return false; + } + // The storage-kind restriction the frontend gate carries, from the descriptor: a + // buffer texture's backing store can move without any serial above noticing. + return static_cast(record.Desc.StorageKind) == TextureStorageType::Mipmap; + } + } // namespace TextureImpl + + // FramebufferImpl::BackendFramebufferObject::SyncToBackendByHandle and + // RenderbufferImpl::BackendRenderbufferObject::SyncToBackendByHandle LANDED with package fb + // (P5e); their bodies are beside the object forms they overload, where the attachment walk + // and the storage allocation they share can be read side by side. + + // PrgramImpl's seam is GONE, not stubbed: package pg wrote + // BackendProgramObjectImpl::SyncToBackendByHandle's real body beside the frontend overload + // it is the twin of (above, with the two ProgramArchiveSource heads). A second definition + // here would make the linker's answer depend on link order, which is the one failure this + // block's own comment was written to prevent. +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp new file mode 100644 index 00000000..d0cb6c19 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp @@ -0,0 +1,1119 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "ManagersInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace BufferImpl { + // P13 W4a (§1.4): THE GPU-WRITE REFRESH OF AN ALIASING STORE. Monolith's record arm reads + // the frontend shadow in place (StagedShadowStore keeps the caller's pointer), so a store a + // shader wrote is refreshed the way the client refreshes it under split: the bytes are + // read back from this process's GL buffer and handed to OnBufferWriteback, whose consumer + // (the frontend, one call away) copies them into that shadow. The backend never writes + // frontend memory itself. Ops_H_Readback's order, for its reasons: queued uploads first, + // resident writes next, then the read, the serial stamp and - last - the epoch bump. + const Uint8* RefreshAliasedStoreFromGpu(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, + const char* site) { + if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized || + resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration || + !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) { + MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read " + "back here, so the CPU reads the shadow as it is", + site, res.Slot, res.Gen); + return SplitHostBytes(resource); + } + const SizeT width = ResourceWidthOf(res); + const SizeT size = std::min(width, resource.storageSize); + if (size == 0) return SplitHostBytes(resource); + if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) { + MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU and no reverse channel is " + "installed to refresh the shadow it aliases", + site, res.Slot, res.Gen); + return SplitHostBytes(resource); + } + FlushPendingRangesFrom(resource, resource.hostBytes, width); + DrainResidentWritesNow(resource); + BindBufferId(TempBufferTarget, resource.id); + void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), + GL_MAP_READ_BIT); + if (mapped == nullptr) { + MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the " + "shadow as it is", + site, resource.id); + return SplitHostBytes(resource); + } + MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( + res, 0, + MG_Pipe::MGPBlobRef{reinterpret_cast(mapped), static_cast(size), + MG_Pipe::kMGHostSpanSegNone, 0}); + if (resource.hostBytes == nullptr) { + // Every content-carrying record and every respecify of a store with a size hands + // its base over on this arm, so no base here means a shape nothing recorded. Keep + // the bytes in a private copy rather than answer "nothing" for GPU-written bytes. + MGLOG_W_ONCE("%s: GPU-written buffer {%u, %u} has no recorded shadow base; its refreshed " + "bytes are kept in a private copy", + site, res.Slot, res.Gen); + const std::lock_guard lock(resource.pendingMutex); + resource.hostBytes = ServerStaged().AdoptPrivateCopy(&resource, width, mapped, size); + } + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + resource.serverGpuWritten = false; + resource.syncedChangeSerial = ResourceSerialOf(res); + BumpBufferMutationEpoch(); + return resource.hostBytes; + } + + // P8-C. The declaration says what; this says in what order, and it is Ops_H_Readback's + // order because it is the same question asked by the server of itself instead of by the + // client: (1) queued uploads land in the GL store first - reading back past them would + // revert them in the copy - under the same staged-coverage assertion that drain carries; + // (2) queued resident writes land (GPU-ordered, behind every command already recorded); + // (3) the whole store is read back into the staged copy, which leaves its coverage whole + // and so also answers RequireStagedCoverage for an orphaned store's first copy; (4) the + // serial stamp and the epoch bump, as Ops_H_ReadbackTracked does them. + const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, + const char* site) { + // A coherent store - T0's import, or (P13 W4a) a map monolith's record arm donated - + // has no copy: its bytes are read through the map once queued writes have landed. + const Bool coherent = resource.persistentMapped && resource.persistentPtr != nullptr; + Bool residentWritesQueued = false; + if (coherent) { + const std::lock_guard lock(resource.pendingMutex); + residentWritesQueued = !resource.pendingResidentWrites.empty(); + } + if (!resource.serverGpuWritten && !residentWritesQueued) return SplitHostBytes(resource); + if (!ServerStaged().CopiesIntoServerStorage() && !coherent) { + // P13 W4a (§1.4): an ALIASING store (monolith's record arm) is refreshed through + // the frontend - the writeback lands in the shadow the base points into. Any other + // non-copying store has no marks to answer. + if (MG_Config::RecordArmAliasesFrontend()) return RefreshAliasedStoreFromGpu(resource, res, site); + return SplitHostBytes(resource); + } + if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized || + resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration || + !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) { + MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read " + "back here, so the CPU reads the staged copy as it is", + site, res.Slot, res.Gen); + return SplitHostBytes(resource); + } + if (coherent) { + // T0: the copy IS this server's coherent map of the client's pages. Landing the + // queued writes and finishing makes them, and every shader write already queued, + // visible through it - Ops_H_Readback's persistent-map arm, for the same reason. + DrainResidentWritesNow(resource); + if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); + resource.serverGpuWritten = false; + return SplitHostBytes(resource); + } + const SizeT width = ResourceWidthOf(res); + const SizeT size = std::min(width, resource.storageSize); + if (size == 0) return SplitHostBytes(resource); + MGL_SERVER_STAGED_REQUIRE_PENDING(resource, resource.hostBytes, width, "gpu_written_refresh_flush_pending"); + FlushPendingRangesFrom(resource, resource.hostBytes, width); + DrainResidentWritesNow(resource); + BindBufferId(TempBufferTarget, resource.id); + void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), + GL_MAP_READ_BIT); + if (mapped == nullptr) { + MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the " + "staged copy as it is", + site, resource.id); + return SplitHostBytes(resource); + } + { + const std::lock_guard lock(resource.pendingMutex); + resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, width, mapped, 0, size); + } + g_GLESFuncs.glUnmapBuffer(TempBufferTarget); + resource.serverGpuWritten = false; + resource.syncedChangeSerial = ResourceSerialOf(res); + BumpBufferMutationEpoch(); + MGLOG_D("%s: refreshed the staged copy of GPU-written buffer {%u, %u} (%zu bytes)", site, res.Slot, + res.Gen, size); + return resource.hostBytes; + } + } // namespace BufferImpl + + namespace SamplerViewImpl { + BackendSamplerViewTable g_backendSamplerViews; + + BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view) { + auto* twin = g_backendSamplerViews.FindByHandle(view); + return twin ? twin->get() : nullptr; + } + } // namespace SamplerViewImpl + + namespace VertexArrayImpl { + namespace { + SizeT GetDataTypeSize(DataType type) { + switch (type) { + case DataType::Int8: + case DataType::Uint8: + return 1; + case DataType::Int16: + case DataType::Uint16: + case DataType::Float16: + return 2; + case DataType::Int32: + case DataType::Uint32: + case DataType::Float32: + case DataType::Fixed32: + return 4; + case DataType::Float64: + return 8; + default: + return 0; + } + } + + // Tightly-packed byte size of one vertex element: 4 for the 2_10_10_10 types and GL_BGRA + // (one 32-bit word / 4 bytes), componentSize * size otherwise. 0 for unknown types. + SizeT GetAttributeByteSize(DataType type, int size, Bool isBgra) { + if (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev || isBgra) { + return 4; + } + const SizeT componentSize = GetDataTypeSize(type); + return componentSize == 0 ? 0 : componentSize * static_cast(size); + } + + // Deinterleaves elementCount elements of componentCount doubles into a tightly packed + // float32 stream - the fetch half of the fp64 demotion the shader side already does + // unconditionally (DemoteFloat64Pass). GL byte strides and offsets are arbitrary, so + // no component carries an 8-byte alignment guarantee and each is copied out before it + // is narrowed. + void NarrowDoubleStreamToFloat32(const Uint8* sourceBase, SizeT sourceStride, SizeT componentCount, + SizeT elementCount, Vector& outData) { + outData.resize(elementCount * componentCount); + for (SizeT element = 0; element < elementCount; ++element) { + const Uint8* sourceElement = sourceBase + element * sourceStride; + Float* destinationElement = outData.data() + element * componentCount; + for (SizeT component = 0; component < componentCount; ++component) { + Double value = 0.0; + Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double)); + destinationElement[component] = static_cast(value); + } + } + } + + // GL_BGRA as a vertex SIZE is desktop-only - ES rejects it outright - and it means + // nothing more than "the element's first and third components are stored swapped". + // So the element is put back in RGBA order and fetched as an ordinary size-4 array of + // the same type: for GL_UNSIGNED_BYTE the first and third BYTES swap, for the two + // 2_10_10_10_REV types the 10-bit fields in bits 0-9 and 20-29 (the core spec's packed + // component layout: with BGRA, x comes from bits 20-29 and z from bits 0-9). The 2-bit + // w field and the middle field stay where they are. Elements are copied word-wise, so + // no source alignment is assumed; the output is tightly packed, one word per element. + void SwizzleBgraElementToRgba(Uint8* element, DataType type) { + if (type == DataType::Uint8) { + std::swap(element[0], element[2]); + return; + } + Uint32 word = 0; + Memcpy(&word, element, sizeof(word)); + constexpr Uint32 kField = 0x3FFu; + const Uint32 low = word & kField; + const Uint32 high = (word >> 20) & kField; + word = (word & ~(kField | (kField << 20))) | high | (low << 20); + Memcpy(element, &word, sizeof(word)); + } + + void SwizzleBgraStreamToRgba(const Uint8* sourceBase, SizeT sourceStride, SizeT elementCount, + DataType type, Vector& outData) { + outData.resize(elementCount * 4); + for (SizeT element = 0; element < elementCount; ++element) { + Uint8* destination = outData.data() + element * 4; + Memcpy(destination, sourceBase + element * sourceStride, 4); + SwizzleBgraElementToRgba(destination, type); + } + } + } // namespace + + BackendVertexArrayObject::BackendVertexArrayObject() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + m_clientAttributeBufferIds.fill(0); + m_convertedAttributeBufferIds.fill(0); + g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId); + if (m_backendVAOId == 0) { + MGLOG_E_ONCE("Failed to generate vertex array object."); + MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); + } else { + MGLOG_D("Generated vertex array object with ID: %u.", m_backendVAOId); + } + } + + BackendVertexArrayObject::~BackendVertexArrayObject() { + if (InProcessTeardown()) { + return; // see InProcessTeardown(): the driver may be unloaded already + } + if (m_backendVAOId != 0) { + NoteVAOIdDeleted(m_backendVAOId); + g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId); + m_backendVAOId = 0; + } + for (auto& bufferId : m_clientAttributeBufferIds) { + if (bufferId != 0) { + BufferImpl::NoteBufferIdDeleted(bufferId); + g_GLESFuncs.glDeleteBuffers(1, &bufferId); + bufferId = 0; + } + } + for (auto& bufferId : m_convertedAttributeBufferIds) { + if (bufferId != 0) { + BufferImpl::NoteBufferIdDeleted(bufferId); + g_GLESFuncs.glDeleteBuffers(1, &bufferId); + bufferId = 0; + } + } + } + + namespace { + Uint g_boundBackendVAOId = 0; + Bool g_boundBackendVAOKnown = false; + } // namespace + + void BindBackendVAOId(Uint id) { + if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) { + return; + } + g_GLESFuncs.glBindVertexArray(id); + g_boundBackendVAOId = id; + g_boundBackendVAOKnown = true; + } + + void InvalidateVAOBindingCache() { + g_boundBackendVAOKnown = false; + } + + void NoteVAOIdDeleted(Uint id) { + if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) { + g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0 + } + } + + void BackendVertexArrayObject::Bind() const { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + BindBackendVAOId(m_backendVAOId); + } + + // P15: the driver-VAO shadow (Managers.h). Callers have bound this twin's VAO. + + namespace { + inline Bool VaoShadowOn() { + return MG_Config::Features.EsprytVaoShadow != MG_Config::QuirkOverride::ForceOff; + } + } // namespace + + void BackendVertexArrayObject::HostSetEnabled(Uint index, Bool enabled) { + const Uint32 bit = 1u << index; + if (VaoShadowOn() && (m_hostEnabledKnownMask & bit) && ((m_hostEnabledMask & bit) != 0) == enabled) return; + if (enabled) g_GLESFuncs.glEnableVertexAttribArray(index); + else g_GLESFuncs.glDisableVertexAttribArray(index); + m_hostEnabledKnownMask |= bit; + m_hostEnabledMask = enabled ? (m_hostEnabledMask | bit) : (m_hostEnabledMask & ~bit); + } + + void BackendVertexArrayObject::HostSetDivisor(Uint index, Uint32 divisor) { + const Uint32 bit = 1u << index; + if (VaoShadowOn() && (m_hostDivisorKnownMask & bit) && m_hostDivisor[index] == divisor) return; + g_GLESFuncs.glVertexAttribDivisor(index, divisor); + m_hostDivisorKnownMask |= bit; + m_hostDivisor[index] = divisor; + } + + void BackendVertexArrayObject::HostForgetAll() { + m_hostEnabledKnownMask = 0; + m_hostDivisorKnownMask = 0; + for (auto& pointer : m_hostPointer) pointer.valid = false; + } + + // ES 3.1 core. Queried through the loader rather than the version, because the whole + // point of using it is to express something the pointer API cannot, and falling back + // silently on a driver that lacks it is better than crashing on a null entry point. + inline Bool HasVertexBindingApi() { + return g_GLESFuncs.glBindVertexBuffer != nullptr && g_GLESFuncs.glVertexAttribFormat != nullptr && + g_GLESFuncs.glVertexAttribIFormat != nullptr && g_GLESFuncs.glVertexAttribBinding != nullptr && + g_GLESFuncs.glVertexBindingDivisor != nullptr; + } + + Bool BackendUsesNativeBaseInstance() { return g_GLESCapabilities.SupportsBaseInstance; } + + // The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift + // of the array's own offset. Only divisor'd arrays step per instance, so only they move. + // + // baseInstance is added to the ELEMENT index, not to instance/divisor - the divisor + // therefore does not appear here, and the shift is a whole number of strides. + // + // A resolved stride of zero is the binding model's "never advance" (see + // VertexAttribute::Stride), so such an array reads the same element for every instance + // and a baseInstance cannot move it. The arithmetic already yields zero for that case. + inline SizeT BaseInstanceByteShift(const MG_State::GLState::VertexAttribute& attrib, Uint32 baseInstance) { + if (baseInstance == 0 || attrib.Divisor == 0) { + return 0; + } + return static_cast(baseInstance) * static_cast(attrib.Stride); + } + + // ---- the handle arm's small helpers ------------------------------------------- + // + // Each one is its object-shaped counterpart above with the frontend reads replaced by + // the two wire views, and nothing else. Divisor is deliberately NOT on the attribute + // view: it is resolved per binding point and rides in MGPVertexBuffer::Divisor, which + // is where glVertexAttribDivisor reads it (D-G2). + + // The entry of the applied set that feeds ATTRIBUTE `attributeIndex`. + // + // THE KEY IS THE ATTRIBUTE INDEX, NOT MGPVertexAttribWire::BindingIndex, and the two + // live in different spaces. Espryt consumes RESOLVED attributes, so the client emits + // set_vertex_buffers as one entry per attribute slot with + // MGPVertexBuffer::BindingIndex == the attribute index (EmitVertexBuffers in VertexInputEmit.h - + // "Espryt consumes RESOLVED attributes, so the set is one entry per attribute + // slot with BindingIndex == the attribute index"), each carrying that attribute's + // already-folded buffer, stride and divisor. MGPVertexAttribWire::BindingIndex is the + // OTHER thing: the GL binding POINT the attribute was attached to by + // glVertexAttribBinding, which is the key of the binding-point view this arm never + // reads (it needs no separate view, because the resolution already happened on the + // client). The two coincide whenever the attribute was configured through + // glVertexAttribPointer, which is why every scenario that uses the pointer API stayed + // green while glVertexAttribBinding(2, 3) / (0, 5) fetched the wrong entry or none - + // KHR-GL43.vertex_attrib_binding's whole subject. + const MG_Pipe::MGPVertexBuffer* VertexBufferForAttributeIndex(const MG_Pipe::MGPipeApplierState& st, + Uint32 attributeIndex) { + if (st.VertexBufferCount == 0) return nullptr; + const Uint32 begin = st.VertexBufferStart; + const Uint32 end = begin + st.VertexBufferCount; + if (attributeIndex >= begin && attributeIndex < end && + attributeIndex < MG_Pipe::kMGPipeMaxVertexAttribs && + st.VertexBuffers[attributeIndex].BindingIndex == attributeIndex) { + return &st.VertexBuffers[attributeIndex]; + } + for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) { + if (st.VertexBuffers[i].BindingIndex == attributeIndex) return &st.VertexBuffers[i]; + } + return nullptr; + } + + // BaseInstanceByteShift, on the wire views. baseInstance is added to the ELEMENT index, + // so the divisor does not appear here; a resolved stride of zero never advances and the + // arithmetic already yields zero for it. + inline SizeT BaseInstanceByteShiftWire(Int32 stride, Uint32 divisor, Uint32 baseInstance) { + if (baseInstance == 0 || divisor == 0) return 0; + return static_cast(baseInstance) * static_cast(stride); + } + + // (The attribute-buffer bind lives in SyncToBackendFromApplier's pointer path now: it + // resolves the driver id from the slot table and skips the bind and the pointer when the + // driver VAO already holds them. It does NOT ensure storage: on this arm the draw's + // buffers were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw.) + + // SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved + // stride of zero, which glVertexAttribPointer's zero means the opposite of. + inline Bool SyncZeroStrideAttributeByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib, + const MG_Pipe::MGPVertexBuffer& binding) { + if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) { + MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex); + return false; + } + auto* backendResource = BufferImpl::FindBufferResourceForHandle(binding.Res); + if (!backendResource || backendResource->id == 0) { + MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex); + return false; + } + if (!attrib.IsInteger) { + const GLint glSize = static_cast(attrib.Size); + g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize, + MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), + attrib.Normalized ? GL_TRUE : GL_FALSE, 0); + } else { + g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast(attrib.Size), + MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), + 0); + } + g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex); + // The resolved offset goes on the binding point, not into a relative offset (the + // relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET, a buffer + // offset is not). BindBufferId is bypassed deliberately. + g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id, + static_cast(attrib.Offset), 0); + return true; + } + + // The record the applier holds for the bound vertex-elements CSO, or null. + const MG_Pipe::MGPipeVertexElementsRecord* BoundVertexElementsRecord( + const MG_Pipe::MGPipeApplierState& st) { + const MG_Pipe::MGPipeHandle cso = st.BoundVertexElements; + if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; + if (cso.Slot >= st.VertexElementsCsos.size()) return nullptr; + const auto& record = st.VertexElementsCsos[cso.Slot]; + if (!record.Live || record.Gen != cso.Gen) return nullptr; + return &record; + } + + // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract; the body is the one + // SamplerImpl::ResolveSamplerCsoTwin already runs, with the sync left to vi. + BackendVertexArrayObject* ResolveVaoTwin(MG_Pipe::MGPipeHandle elements) { + if (MG_Pipe::MGPipeHandleIsNull(elements)) return nullptr; + // THE RECORD FIRST, before the table is touched: a handle with no record means the + // client named a vertex-elements CSO it never described (or one it deleted while a + // binding still names it), and adopting a slot for it would leave a twin nothing + // can ever sync. The applier's own Gen check is what turns a stale handle into this + // null rather than into its successor's record. + if (PipeRecordAt(MG_Pipe::MGPipeApplier().VertexElementsCsos, elements) == nullptr) { + MGLOG_E_ONCE("MGPipe: vertex-elements CSO {%u, %u} has no applier record on the handle " + "arm, so no driver VAO can be built for it and the draw keeps what is bound", + elements.Slot, elements.Gen); + return nullptr; + } + auto* slot = AdoptTwinByHandle(g_backendVertexArrayObjects, elements, "vertex-elements CSO"); + if (slot == nullptr) return nullptr; + if (!*slot) *slot = MakeShared(); + return slot->get(); + } + + // The same function, driven by the applier's records (D-G4). Every branch of the walk + // survives - the enable/disable block, the fp64 narrowing with its Adreno disable, the + // zero-stride binding-API path, the attribute-buffer bind, the BGRA swizzle, the + // pointer/IPointer at the shifted fetch offset and the divisor - and the gate is + // re-keyed onto three server-owned MGGen counters plus the CSO's {slot, gen}: + // + // m_syncedElementsHandle + m_syncedElementsSerial <- config version + the whole + // per-attribute version array + // m_syncedVertexBuffersSerial <- (new) the buffer set's own + // m_syncedIndexSerial <- wrapping Uint16 + identity patch + // m_syncedBufferIdGeneration <- UNCHANGED, server-local, and + // still the only thing that + // catches a driver-id re-mint no + // counter on either side moves + // m_hasConvertedAttribute <- UNCHANGED: a narrowed stream is + // derived from buffer CONTENT, + // which no serial covers + // m_syncedFetchBaseInstance <- no longer in the gate as a + // DIRTY input of its own (a base + // instance change is a + // ContentHash input and so moves + // VertexBuffersSerial), but kept + // as the record of what was last + // EMITTED, which is what the next + // sync has to correct + void BackendVertexArrayObject::SyncToBackendFromApplier() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); + const auto* rec = BoundVertexElementsRecord(st); + if (rec == nullptr) { + MGLOG_E_ONCE("MGPipe: no vertex-elements record is bound, so the driver VAO cannot be " + "configured - nothing emitted create_vertex_elements/bind_vertex_elements"); + return; + } + + const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration; + const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration; + // THIS GATE DEPENDS ON A SERIAL RULE THAT LIVES IN ANOTHER PACKAGE, and it is named + // here because nothing else in this file would say it: MGPipeApplierReset() runs on + // EVERY change of the current GLContext (MG_Impl/Pipe/Tracker.h's `if (m_context != + // &ctx) Reset();`, not only on a fresh one), while this twin SURVIVES the excursion - + // BackendVertexArrayObject has no context-generation member and + // OnBackendContextDestroyed runs on destroy, not on make-current. A reset that sent + // VertexBuffersSerial and IndexBufferSerial back to ZERO would therefore walk them + // back through values this twin has already stamped, and a memo could read clean over + // state the applier had just cleared. MG_Pipe/PipeApply.cpp's reset must ADVANCE + // those two serials instead (wire's C2), which is what makes + // m_syncedVertexBuffersSerial below unable to match after a reset - and that in turn + // forces the whole AND dirty, which is also what rescues the per-record ContentSerial + // half (an applier reset is not a slot recycle, so a re-created CSO at the same + // {slot, gen} restarts its ContentSerial at 1). If that rule is ever reverted, this + // gate is unsafe on any application that changes contexts. + const Bool elementsDirty = bufferIdsRemitted || !m_hasSyncedElements || + !(m_syncedElementsHandle == st.BoundVertexElements) || + m_syncedElementsSerial != rec->ContentSerial; + const Bool attributesDirty = elementsDirty || m_syncedVertexBuffersSerial != st.VertexBuffersSerial; + const Bool indexBufferDirty = bufferIdsRemitted || m_syncedIndexSerial != st.IndexBufferSerial; + // Emulation is server-owned: the client sends the draw's RAW base instance and never + // learns the answer. Applied here as well as in the applier so the decision is the + // same whichever side resolved it first - it is idempotent. + const Uint32 fetchBaseInstance = + BackendUsesNativeBaseInstance() ? 0u : st.VertexFetchBaseInstance; + const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance; + const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedAttribute; + if (!emitAttributes && !indexBufferDirty) { + return; + } + // P14: ONLY THE BUFFER SET MOVED (a Minecraft chunk draw: same layout, next chunk's + // buffer). The driver VAO's enable flags are a function of the configuration alone, + // and the last full walk left them exactly as this configuration wants them, so the + // walk below re-points just the enabled attributes and skips the 32 enable/disable + // calls. Only after a walk in which every enabled attribute took the plain pointer + // path: the fp64, zero-stride, BGRA and no-buffer branches toggle enables or bind + // through other state, and the client-memory upload rewrites the VAO behind this + // gate, so any of them sends the next sync through the whole walk again. + const Bool buffersOnly = emitAttributes && !elementsDirty && m_lastWalkWasPlain && + !m_hasConvertedAttribute; + Bool walkWasPlain = true; + m_hasConvertedAttribute = false; + // Rebuilt by every walk; a buffers-only one follows a plain walk, which set none. + if (emitAttributes && !buffersOnly) m_disabledEnabledAttribMask = 0; + + Bind(); + if (m_hostPointerBufferGeneration != currentBufferIdGeneration) { + for (auto& pointer : m_hostPointer) pointer.valid = false; + m_hostPointerBufferGeneration = currentBufferIdGeneration; + } + + // ALL 32 SLOTS, not rec->AttributeCount, and the difference is the disable arm. + // The legacy walk ran over the frontend's whole 32-slot attribute array and reached + // glDisableVertexAttribArray for every attribute that was off; a walk bounded by the + // record's count leaves an attribute the configuration has just DROPPED enabled in + // the driver VAO, pointing at whatever buffer it last held - which is the class the + // first Adreno workaround below exists to prevent. Nothing in the contract requires + // the client to emit 32 entries (D-H3 sets the precedent the other way for the + // BUFFER set: "truncated to the highest enabled attribute + 1"), so the bound was a + // requirement on B that was neither stated nor pinned. It costs nothing to drop it: + // MGPipeApplyCreateVertexElements zeroes both arrays before it unpacks, so every + // entry past AttributeCount reads Enabled = 0, Type = 0 (Int8, not Float64) and + // IsLong = 0 - i.e. exactly "disable this array", with no dependency on B at all. + for (Uint attribIndex = 0; attribIndex < MG_Pipe::kMGPipeMaxVertexAttribs && emitAttributes; + ++attribIndex) { + const MGPVertexAttribWire& attrib = rec->Attributes[attribIndex]; + if (buffersOnly && !attrib.Enabled) continue; + const MG_Pipe::MGPVertexBuffer* binding = + VertexBufferForAttributeIndex(st, attribIndex); + const Uint32 divisor = binding != nullptr ? binding->Divisor : 0u; + + // The enable/disable block. On this arm there is no per-attribute version to + // compare: the applier's Attributes[] IS what was last pushed, so a moved + // ContentSerial means re-emit and an unchanged one means the early-out above + // already returned. A buffers-only walk leaves the flags as they are (above). + if (!buffersOnly) HostSetEnabled(attribIndex, attrib.Enabled != 0); + + // The fp64 narrowing, verbatim in behaviour including the Adreno workaround: + // when no float32 stream can be built the array is DISABLED rather than left + // enabled with no pointer, which is what the driver turns into a SIGSEGV inside + // the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case4). IsLong is + // carried separately from Type == Float64 on the wire precisely so this test + // can still tell the two apart. + // + // A GL_BGRA-sized array takes the same stream, swizzled instead of narrowed, and + // the same disable when it cannot be built: GL_BGRA as a vertex SIZE is + // desktop-only, ES rejects it, and the rejected call leaves the array ENABLED + // with no pointer - the shape the Adreno driver dereferences null on inside the + // next draw (KHR-GL43.vertex_attrib_binding.basic-input-case5). + const Bool bgraArray = attrib.IsBgra != 0 && !attrib.IsInteger; + if (attrib.IsLong || attrib.Type == static_cast(DataType::Float64) || bgraArray) { + walkWasPlain = false; + const Bool convertible = bgraArray || attrib.Type == static_cast(DataType::Float64); + if (attrib.Enabled && convertible && binding != nullptr && + SyncConvertedAttributeByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) { + m_hasConvertedAttribute = true; + // The conversion pointed the attribute at its own stream (and may have + // used the binding API), so the pointer and divisor shadow are stale. + // An earlier walk that could not build the stream disabled this array. + m_hostPointer[attribIndex].valid = false; + m_hostDivisorKnownMask &= ~(1u << attribIndex); + HostSetEnabled(attribIndex, true); + HostSetDivisor(attribIndex, divisor); + continue; + } + if (attrib.Enabled) { + MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a %s array whose source stream could not " + "be converted - disabling the array", + attribIndex, bgraArray ? "GL_BGRA" : "64-bit (GL_DOUBLE)"); + // The draw now reads the attribute's current generic value, which the + // application had no reason to set for an enabled array. + m_disabledEnabledAttribMask |= 1u << attribIndex; + } + HostSetEnabled(attribIndex, false); + continue; + } + + if (!attrib.Enabled) continue; + if (binding == nullptr) { + walkWasPlain = false; + continue; + } + + // A resolved stride of zero is the binding model's "never advance" and + // glVertexAttribPointer cannot say it - its zero means "tightly packed", i.e. + // the opposite. ES 3.1's binding-point API can. + if (attrib.Stride == 0 && HasVertexBindingApi()) { + walkWasPlain = false; + // The binding API rewrote this attribute's format and binding: whatever the + // shadow said about its pointer no longer holds. + m_hostPointer[attribIndex].valid = false; + if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) { + continue; + } + // No shift here on purpose: a zero stride never advances. Binding index == + // attribute index here, so this IS the attribute's divisor for the shadow. + g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor); + m_hostDivisorKnownMask |= 1u << attribIndex; + m_hostDivisor[attribIndex] = divisor; + continue; + } + + if (MG_Pipe::MGPipeHandleIsNull(binding->Res)) { + MGLOG_W_ONCE("Attribute has no bound buffer, skipping."); + walkWasPlain = false; + continue; + } + const auto* attribBuffer = BufferImpl::FindBufferResourceForHandle(binding->Res); + if (!attribBuffer || attribBuffer->id == 0) { + MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute."); + walkWasPlain = false; + continue; + } + + const SizeT fetchOffset = static_cast(attrib.Offset) + + BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance); + const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)); + const GLint glSize = static_cast(attrib.Size); + + // The pointer the driver VAO already holds for this attribute - the buffer it + // captured included - needs no glBindBuffer + pointer call. + auto& shadowPointer = m_hostPointer[attribIndex]; + const Bool pointerHeld = VaoShadowOn() && shadowPointer.valid && + shadowPointer.bufferId == attribBuffer->id && shadowPointer.size == glSize && + shadowPointer.type == static_cast(glType) && + shadowPointer.stride == static_cast(attrib.Stride) && + shadowPointer.offset == fetchOffset && + shadowPointer.normalized == (attrib.Normalized != 0) && + shadowPointer.integer == (attrib.IsInteger != 0); + if (!pointerHeld) { + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, attribBuffer->id); + if (!attrib.IsInteger) { + g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType, + attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, + (const void*)fetchOffset); + } else { + g_GLESFuncs.glVertexAttribIPointer(attribIndex, glSize, glType, attrib.Stride, + (const void*)fetchOffset); + } + shadowPointer = {attribBuffer->id, glSize, static_cast(glType), + static_cast(attrib.Stride), fetchOffset, attrib.Normalized != 0, + attrib.IsInteger != 0, true}; + } + + HostSetDivisor(attribIndex, divisor); + } + + if (indexBufferDirty) { + Bool indexBufferSynced = false; + if (!MG_Pipe::MGPipeHandleIsNull(st.IndexBuffer.Res)) { + // RESOLVE ONLY, no ensure - unlike the legacy arm, which called + // EnsureBufferResource here. PrepareForDraw runs SyncNeccessaryBuffers before + // SyncCurrentVAO, so an INDEXED draw has already ensured this store; a + // non-indexed draw whose IndexBufferSerial moved has not, and then this logs + // once and leaves the binding alone. That is noisy rather than wrong: nothing + // is stamped on the miss (indexBufferSynced stays false), so the next indexed + // draw repairs it. Ensuring here would need the frontend object this arm + // deliberately does not hold. + auto* backendResource = BufferImpl::FindBufferResourceForHandle(st.IndexBuffer.Res); + if (backendResource && backendResource->id != 0) { + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id); + indexBufferSynced = true; + } else { + MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer."); + } + } else { + g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + indexBufferSynced = true; + } + if (indexBufferSynced) { + m_syncedIndexRes = st.IndexBuffer.Res; + m_syncedIndexValid = true; + // The two element-array restore scopes (the restart substitution's and + // MultiDrawImpl's) put the DRIVER id back without touching this serial, + // which is correct: the serial records what the APPLIER last said, and + // those scopes restored exactly what the applier said. + m_syncedIndexSerial = st.IndexBufferSerial; + } + } + + if (attributesDirty) { + m_syncedElementsHandle = st.BoundVertexElements; + m_syncedElementsSerial = rec->ContentSerial; + m_syncedVertexBuffersSerial = st.VertexBuffersSerial; + m_hasSyncedElements = true; + // The window this walk emitted, for AdoptUnchangedBufferSets. + m_syncedBufferStart = st.VertexBufferStart; + m_syncedBufferCount = std::min(st.VertexBufferCount, + MG_Pipe::kMGPipeMaxVertexAttribs - st.VertexBufferStart); + m_syncedBufferBaseInstance = st.VertexFetchBaseInstance; + for (Uint32 i = 0; i < m_syncedBufferCount; ++i) + m_syncedBuffers[i] = st.VertexBuffers[m_syncedBufferStart + i]; + m_syncedBuffersValid = st.VertexBufferStart <= MG_Pipe::kMGPipeMaxVertexAttribs; + m_syncedContextSerial = st.ContextSerial; + } + if (emitAttributes) { + m_syncedFetchBaseInstance = fetchBaseInstance; + m_lastWalkWasPlain = walkWasPlain; + } + m_syncedBufferIdGeneration = currentBufferIdGeneration; + } + + void BackendVertexArrayObject::AdoptUnchangedBufferSets(const MG_Pipe::MGPipeApplierState& st) { + if (!m_hasSyncedElements || m_syncedContextSerial != st.ContextSerial) return; + if (m_syncedBuffersValid && m_syncedVertexBuffersSerial != st.VertexBuffersSerial && + st.VertexBufferStart == m_syncedBufferStart && st.VertexFetchBaseInstance == m_syncedBufferBaseInstance && + std::min(st.VertexBufferCount, MG_Pipe::kMGPipeMaxVertexAttribs - st.VertexBufferStart) == + m_syncedBufferCount) { + Bool same = true; + for (Uint32 i = 0; same && i < m_syncedBufferCount; ++i) { + const auto& a = m_syncedBuffers[i]; + const auto& b = st.VertexBuffers[m_syncedBufferStart + i]; + same = a.Res == b.Res && a.Offset == b.Offset && a.Stride == b.Stride && + a.Divisor == b.Divisor && a.BindingIndex == b.BindingIndex; + } + if (same) { + if (m_resolvedDrawBuffers.valid && m_resolvedDrawBuffers.buffersSerial == m_syncedVertexBuffersSerial) + m_resolvedDrawBuffers.buffersSerial = st.VertexBuffersSerial; + m_syncedVertexBuffersSerial = st.VertexBuffersSerial; + } + } + if (m_syncedIndexValid && m_syncedIndexSerial != st.IndexBufferSerial && m_syncedIndexRes == st.IndexBuffer.Res) { + if (m_resolvedDrawBuffers.iboSerial == m_syncedIndexSerial) m_resolvedDrawBuffers.iboSerial = st.IndexBufferSerial; + m_syncedIndexSerial = st.IndexBufferSerial; + } + } + + void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays( + const SharedPtr& stateVAOObject, GLint first, GLsizei count, + Uint32 fetchBaseInstance) { + if (!stateVAOObject || count <= 0 || first < 0) { + return; + } + // This rewrites the driver VAO's arrays behind SyncToBackendFromApplier's gate, so + // the next sync must not take its buffers-only walk, nor adopt an equal buffer set. + m_lastWalkWasPlain = false; + m_syncedBuffersValid = false; + HostForgetAll(); // the uploads below set enables, pointers and divisors directly + + Bind(); + + const auto& allAttributes = stateVAOObject->GetAllAttributes(); + for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { + const auto& attrib = allAttributes[attribIndex]; + if (!attrib.Enabled || attrib.Buffer) { + continue; + } + + // Same narrowing as SyncToBackend (the long note lives there), with the draw's own + // fetch range standing in for the buffer extent a client array does not have. The + // upload starts at element 0 so `first` keeps indexing the stream, exactly as the + // unconverted upload below does. The test is on the storage rather than on IsLong + // because glVertexAttribFormat(GL_DOUBLE) is 64-bit data without being long. + if (attrib.IsLong || attrib.Type == DataType::Float64) { + const auto* sourceBase = reinterpret_cast(attrib.Offset); + if (attrib.Type != DataType::Float64 || sourceBase == nullptr || attrib.Size < 1 || + attrib.Size > 4) { + g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + continue; + } + + auto& bufferId = m_clientAttributeBufferIds[attribIndex]; + if (bufferId == 0) { + g_GLESFuncs.glGenBuffers(1, &bufferId); + if (bufferId == 0) { + MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer."); + g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + continue; + } + } + + const SizeT componentCount = static_cast(attrib.Size); + const SizeT sourceElementSize = componentCount * sizeof(Double); + // A client array only ever reaches here through a pointer call, whose stride 0 + // the frontend already resolved to the element size, so this is a guard rather + // than a case (VertexAttribute::Stride). + const SizeT sourceStride = + attrib.Stride > 0 ? static_cast(attrib.Stride) : sourceElementSize; + const SizeT elementCount = static_cast(first) + static_cast(count); + Vector converted; + NarrowDoubleStreamToFloat32(sourceBase, sourceStride, componentCount, elementCount, converted); + + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); + g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, + static_cast(converted.size() * sizeof(Float)), + converted.data(), GL_STREAM_DRAW); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, + static_cast(converted.size() * sizeof(Float))); + } + // GL ignores `normalized` for floating-point array types, so it is not + // forwarded here either. + g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE, + static_cast(componentCount * sizeof(Float)), + // The converted stream is tightly packed, so a divisor'd array's + // baseInstance shift is a whole number of CONVERTED elements - + // not BaseInstanceByteShift's source-stride arithmetic. + reinterpret_cast( + attrib.Divisor == 0 ? 0u : static_cast(fetchBaseInstance) * + componentCount * + sizeof(Float))); + // glVertexAttribPointer's final argument IS that shift. A client array + // declared through the pointer API keeps element 0 at the buffer's byte 0, so + // the shift rides in the pointer argument the call above passes. + // The float64 narrowing itself is shared with SyncToBackend's conversion + // branch; only the fetch RANGE differs, and the draw's is the accurate one. + g_GLESFuncs.glEnableVertexAttribArray(attribIndex); + continue; + } + + const auto* clientData = reinterpret_cast(attrib.Offset); + const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra); + if (!clientData || elementSize == 0 || attrib.Size <= 0) { + continue; + } + + const SizeT stride = attrib.Stride > 0 ? static_cast(attrib.Stride) : elementSize; + const SizeT uploadSize = static_cast(first + count - 1) * stride + elementSize; + + auto& bufferId = m_clientAttributeBufferIds[attribIndex]; + if (bufferId == 0) { + g_GLESFuncs.glGenBuffers(1, &bufferId); + if (bufferId == 0) { + MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer."); + continue; + } + } + + // A GL_BGRA array is swizzled into RGBA order on the way, as on the buffer path + // (SyncConvertedAttributeByHandle): ES has no BGRA vertex size to declare. + const Bool bgra = attrib.IsBgra && !attrib.IsInteger; + Vector swizzled; + if (bgra) { + swizzled.assign(clientData, clientData + uploadSize); + for (SizeT element = 0; element < static_cast(first + count); ++element) { + SwizzleBgraElementToRgba(swizzled.data() + element * stride, attrib.Type); + } + } + + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); + g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(uploadSize), + bgra ? static_cast(swizzled.data()) : clientData, + GL_STREAM_DRAW); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, + static_cast(uploadSize)); + } + + if (!attrib.IsInteger) { + const GLint glSize = bgra ? 4 : attrib.Size; + g_GLESFuncs.glVertexAttribPointer( + attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type), + attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast(stride), + // A client array's element 0 sits at its buffer's byte 0, so the + // divisor'd arrays' baseInstance shift is exactly BaseInstanceByteShift - + // the same arithmetic SyncToBackend applies to a VBO-backed array. + reinterpret_cast(BaseInstanceByteShift(attrib, fetchBaseInstance))); + } else { + g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size, + MG_Util::ConvertDataTypeToGLEnum(attrib.Type), + static_cast(stride), + // See the pointer call above: the divisor'd + // arrays' baseInstance shift is the pointer. + reinterpret_cast( + BaseInstanceByteShift(attrib, fetchBaseInstance))); + } + } + } + + Bool BackendVertexArrayObject::SyncConvertedAttributeByHandle( + Uint attribIndex, const MGPVertexAttribWire& attrib, const MG_Pipe::MGPVertexBuffer& binding, + Uint32 fetchBaseInstance) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (attribIndex >= m_convertedAttributeBufferIds.size() || attrib.Size < 1 || attrib.Size > 4) { + return false; + } + // Which of the two conversions: a BGRA array is a 4-byte element whatever its type, + // and only the three types GL allows with it reach here (the frontend refuses the + // rest at the format call). + const DataType sourceType = static_cast(attrib.Type); + const Bool bgra = attrib.IsBgra != 0; + if (bgra && sourceType != DataType::Uint8 && sourceType != DataType::Int2101010Rev && + sourceType != DataType::Uint2101010Rev) { + return false; + } + if (!bgra && sourceType != DataType::Float64) return false; + if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) { + // A client-memory 64-bit array is narrowed on the draw path instead, which is + // the only place its fetch range is known. + return false; + } + auto* resource = BufferImpl::FindBufferResourceForHandle(binding.Res); + if (resource == nullptr) return false; + + // WHAT IS NOT HERE, AND IT IS A RULED DEVIATION FOR P3a RATHER THAN AN OVERSIGHT + // (M-3; integrator ruling, this commit). + // + // The legacy arm opens with bufferObject->SyncGpuWrites(), one of the eleven Espryt + // SyncPersistentMappedRange / SyncGpuWrites sites D-N keeps where they are for P3a. + // D-N's wording is "no MOVE of those sites off the frontend", and this arm does not + // move it: it CANNOT MAKE IT AT ALL. The call needs a frontend BufferObject and this + // path holds only a handle, because the server has no inverse map back to a frontend + // object - that absence is the design, not a gap in it (ARCHITECTURE.md 4.2). The + // two ways to keep the site here would each break something D-N or D-J protects: a + // handle -> object map is the very thing the split removes, and pulling the bytes + // eagerly on the client at every draw is new behaviour and new cost. + // + // SO THE DEVIATION IS DECLARED, WITH ITS BLAST RADIUS. Under the default mask a + // 64-bit vertex array whose SOURCE buffer was written by a shader and not yet pulled + // back narrows STALE bytes on this arm and fresh bytes on the legacy one. That is + // the whole of it: fp64 vertex arrays, fed by a buffer a shader wrote, read without + // any intervening explicit readback. No other attribute type reads through this + // path, and a persistently mapped source is excluded separately below (the memo + // never trusts one, so those re-read every draw through the coherent map). + // + // P8-C CLOSES IT UNDER A TRANSPORT, on the server rather than by moving the pull to the + // client: a source a shader wrote on this side is read back from this server's own GL + // buffer (SplitHostBytesForCpuRead), and that write also voids the memo below, whose + // serial a GPU write never moves. The MONOLITH arm of this function keeps the + // deviation (its hostBytes is the client's shadow and its mark is the frontend + // object's); that half is dev's (ID-P8-3). + // WHERE THE BYTES ARE, and it is not one fixed place: an ADOPTED store has no client + // shadow left at all (PipeResource::AdoptPersistentMap clears and shrinks it), so + // the coherent map IS the source of truth - which is exactly the case the memo + // exclusion below makes the most frequent, since a persistently mapped source is + // never trusted and this narrowing therefore re-reads on every draw. Reading the + // recorded shadow base for such a resource read freed memory; hostBytes is nulled + // at adoption for the same reason, so the fallback below is a null and a refusal + // rather than a stale read. + const Bool sourceGpuWritten = + resource->serverGpuWritten; + if (sourceGpuWritten) { + BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res, + bgra ? "bgra_vertex_swizzle" : "fp64_vertex_narrowing"); + } + const Uint8* const sourceBase = + (resource->persistentMapped && resource->persistentPtr != nullptr) + ? static_cast(resource->persistentPtr) + : resource->hostBytes; + const SizeT sourceSize = BufferImpl::ResourceWidthForHandle(binding.Res); + if (sourceBase == nullptr || attrib.Offset >= sourceSize) { + return false; + } + + // A BGRA element is always one 32-bit word, and so is its RGBA replacement; a 64-bit + // element narrows to the float32 vector of the same component count. + const SizeT componentCount = static_cast(attrib.Size); + const SizeT sourceElementSize = bgra ? 4 : componentCount * sizeof(Double); + const SizeT convertedElementSize = bgra ? 4 : componentCount * sizeof(Float); + const SizeT available = sourceSize - static_cast(attrib.Offset); + if (available < sourceElementSize) { + return false; + } + + const Bool neverAdvances = attrib.Stride <= 0; + const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast(attrib.Stride); + const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1; + + const SizeT firstElement = (fetchBaseInstance != 0 && binding.Divisor != 0 && !neverAdvances) + ? static_cast(fetchBaseInstance) + : 0; + if (firstElement >= elementCount) { + return false; + } + + auto& stream = m_convertedAttributeStreams[attribIndex]; + Uint& convertedBufferId = m_convertedAttributeBufferIds[attribIndex]; + const Uint64 sourceSerial = BufferImpl::ResourceSerialForHandle(binding.Res); + // The memo, re-keyed: the pin is the source buffer's {slot, gen} instead of a + // frontend lifetime id, and the freshness input is the applier's server-owned + // Serial instead of the frontend change serial. A PERSISTENTLY MAPPED source is + // still never trusted - it is written through the pointer with no call at all, so + // no serial on either side can prove the converted copy is current. The conversion + // itself is part of the key: one attribute index can carry both shapes over its life, + // over the same bytes. + const Bool memoHit = stream.valid && convertedBufferId != 0 && !resource->persistentMapped && + !sourceGpuWritten && stream.sourceHandle == binding.Res && + stream.sourceChangeSerial == sourceSerial && + stream.sourceOffset == static_cast(attrib.Offset) && + stream.sourceStride == sourceStride && stream.bgra == bgra && + stream.sourceType == attrib.Type && stream.componentCount == componentCount && + stream.elementCount == elementCount; + if (!memoHit) { + if (convertedBufferId == 0) { + g_GLESFuncs.glGenBuffers(1, &convertedBufferId); + if (convertedBufferId == 0) { + MGLOG_E_ONCE("Failed to create the scratch buffer for the converted vertex array at " + "attribute %u.", + attribIndex); + return false; + } + } + Vector narrowed; + Vector swizzled; + const void* convertedData = nullptr; + SizeT convertedBytes = 0; + if (bgra) { + SwizzleBgraStreamToRgba(sourceBase + attrib.Offset, sourceStride, elementCount, sourceType, + swizzled); + convertedData = swizzled.data(); + convertedBytes = swizzled.size(); + } else { + NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, + elementCount, narrowed); + convertedData = narrowed.data(); + convertedBytes = narrowed.size() * sizeof(Float); + } + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId); + g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(convertedBytes), convertedData, + GL_STREAM_DRAW); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, + static_cast(convertedBytes)); + } + stream.valid = true; + stream.sourceHandle = binding.Res; + stream.sourceChangeSerial = sourceSerial; + stream.sourceOffset = static_cast(attrib.Offset); + stream.sourceStride = sourceStride; + stream.bgra = bgra; + stream.sourceType = attrib.Type; + stream.componentCount = componentCount; + stream.elementCount = elementCount; + MGLOG_D("DirectGLES: converted the %s vertex array at attribute %u, %zu element(s).", + bgra ? "BGRA" : "64-bit", attribIndex, elementCount); + } + + // What the converted stream is to the driver. The swizzled BGRA stream keeps its + // source type and `normalized` (GL accepts BGRA only normalized, and a packed type's + // signedness is the type's own). The narrowed stream is float32, and `normalized` is + // deliberately GL_FALSE there rather than attrib.Normalized: GL ignores it for + // floating-point array types, and honouring it would scale the fetched values. + const GLint convertedSize = bgra ? 4 : static_cast(attrib.Size); + const GLenum convertedType = bgra ? MG_Util::ConvertDataTypeToGLEnum(sourceType) : GL_FLOAT; + const GLboolean convertedNormalized = (bgra && attrib.Normalized) ? GL_TRUE : GL_FALSE; + if (neverAdvances) { + // Only the binding-point API can say "stride 0". + if (!HasVertexBindingApi()) { + return false; + } + g_GLESFuncs.glVertexAttribFormat(attribIndex, convertedSize, convertedType, convertedNormalized, 0); + g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex); + g_GLESFuncs.glBindVertexBuffer(attribIndex, convertedBufferId, 0, 0); + return true; + } + + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId); + g_GLESFuncs.glVertexAttribPointer(attribIndex, convertedSize, convertedType, convertedNormalized, + static_cast(convertedElementSize), + (const void*)(firstElement * convertedElementSize)); + return true; + } + + TwinRegistry + g_backendVertexArrayObjects; + } // namespace VertexArrayImpl +} // namespace MobileGL::MG_Backend::DirectGLES