From b0f73e379546eb45d64ddba5f183f92e1cd8bb51 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 16 Oct 2026 05:09:31 -0400 Subject: [PATCH] [Refactor] (CMakeLists.txt, MG_Backend/DirectGLES): split DirectGLES.cpp by area - helpers and state several files share lose `static`; DirectGLESInternal.h declares them --- CMakeLists.txt | 11 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 15392 +--------------- .../MG_Backend/DirectGLES/DirectGLESBind.cpp | 1884 ++ .../MG_Backend/DirectGLES/DirectGLESBlit.cpp | 1330 ++ .../DirectGLES/DirectGLESBuffers.cpp | 720 + .../DirectGLES/DirectGLESContext.cpp | 1659 ++ .../MG_Backend/DirectGLES/DirectGLESCopy.cpp | 1731 ++ .../MG_Backend/DirectGLES/DirectGLESDraw.cpp | 1517 ++ .../DirectGLES/DirectGLESInternal.h | 1078 ++ .../DirectGLES/DirectGLESReadback.cpp | 2067 +++ .../DirectGLES/DirectGLESSharedImage.cpp | 887 + .../MG_Backend/DirectGLES/DirectGLESState.cpp | 1499 ++ .../MG_Backend/DirectGLES/DirectGLESSync.cpp | 680 + .../DirectGLES/DirectGLESTextures.cpp | 1178 ++ .../DirectGLES/WireTextureReadback.inc | 469 - 15 files changed, 16242 insertions(+), 15860 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESContext.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.cpp delete mode 100644 MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bdd1d19..4b9e8e91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -500,6 +500,17 @@ set(SOURCE_FILES MobileGL/MG_Backend/GpuHangWatch.cpp MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESContext.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp + MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.cpp MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/Utils.cpp MobileGL/MG_Backend/DirectGLES/Managers.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index c23f6f14..fb92801a 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -6,197 +6,15 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -#include "DirectGLES.h" -#include "ContextEpoch.h" -#include "MG_Util/X11/DisplayGuard.h" -#include "EGL/egl.h" -#include "MG_Util/Types.h" -#include "Utils.h" -#include "Managers.h" -#include "MultiDraw.h" -#include -#include -#include -#include -#include -#include -// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against. -#include -// P5c ev: the surface-changed event's producer callback, installed by the server session. -#include -// P5e (pa): MGPipeShaderCsoRecord::Archive is a SharedPtr and PipeApply.h -// deliberately only forward-declares the type (its own note at :46 says to come here for it). -// The draw path's attribute-values sync reads Archive->Link, so this TU needs it complete - -// Managers.cpp already includes it for ProgramArchiveSource, which is the same reason. -#include -// P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads. -#include -// The record arm's deaths and latches (MGPipeRecordFail / MGPipeRecordLatch), in every build. -#include -#if MOBILEGL_BUILD_DISAGGREGATED -// Shared images: the registry the EGLImages are built from (SharedImageImpl below). -#include -// Device loss ends the session through MG_Pipe's latch seam (LatchIfDeviceLost). -#include -#if defined(__ANDROID__) -#include -#include -#endif -#endif -#if defined(__ANDROID__) -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if defined(__linux__) && !defined(__ANDROID__) && __has_include() -#pragma push_macro("Bool") -#pragma push_macro("None") -#include -#pragma pop_macro("None") -#pragma pop_macro("Bool") -#endif +#include "DirectGLESInternal.h" namespace MobileGL::MG_Backend::DirectGLES { - // A readback on the server writes REPLY SCRATCH, so it has no pack buffer - and this - // function is how every pack-PBO lookup in this file says so under a transport. - // - // THE REASON IS NOT "THE CLIENT REFUSES PACK_BUFFER BEFORE EMISSION", which is what this - // comment used to claim and is not true of the tree (P3b/P4b espryt D1 slice 4). The client - // SERVICES the pack PBO itself, AFTER the reply lands: EmitTables.cpp uploads the reply's - // tight rows into the bound buffer with pbo->UploadSubData, one row at a time at the stride - // the application's GL_PACK_* state implies, so padding and untouched bytes survive. A - // refusal would have been visible as a GL error; what actually happens is an ordinary - // buffer upload the client performs on its own side. - // - // The server's half is ID-49's: read with NEUTRAL pack state into a tight w*h*bpp reply. - // Which is exactly why the server must see NO pack buffer here - not because the client - // forbade one, but because the client kept it. - static const SharedPtr& SplitReadbackPackBuffer() { - static const SharedPtr none; - return none; - } - MG_External::EGLFunctionsTable g_EGLFuncs; MG_External::GLESFunctionsTable g_GLESFuncs; MG_External::GLESCapabilities g_GLESCapabilities; - - static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight); static SharedPtr g_rawDepthFetchSamplerState; static SharedPtr g_rawDepthFetchSamplerBackend; - - // Cached addresses of the frontend's framebuffer binding slots. The frontend - // getter linear-scans its slot array per call and the draw path asks for these - // slots several times per draw (FBO sync, FBO texture-attachment sync, the - // draw-buffer broadcast memo, the draw-time bind). The slots are by-value - // members of the GLContext, so their addresses are a pure function of the - // context's address: the cache is keyed on the raw context pointer, and a - // recreated context landing on the SAME address has its slots at the same - // addresses again - the cached pointers cannot go stale. Invalidation is - // exactly the pointer compare below. - using FbBindingSlot = - std::remove_reference_tGetFramebufferBindingSlot(FramebufferTarget::Draw))>; - // P2 step e4. On the {slot, gen} arm this is an ORDINARY read of pushed state and the cache - // above is not consulted, which closes the P1 accessor bypass: the cached raw pointer ran - // the checked accessor once per context change and then handed out the pointee forever, so - // at all five call sites the per-verb poison stamp (MGP_INPUT_CHECK) and the verify - // read-hook (MGP_INPUT_VERIFY_READ) were skipped. A verb that legitimately never fills - // GetFramebufferBindingSlot could not be caught here, and a verify build compared the field - // only where the slow accessor was used. - // - // The cache stays on the LEGACY arm, gated on the same subsystem bit as the rest of this - // slice, so that MOBILEGL_PIPE_PUSH=0 keeps being the faithful all-subsystems-pull control - // ConfigLoader.cpp documents - "reproduces P1's behaviour exactly" has to include this - // path, or the integrator's A/B measures e4 on both arms and attributes it to neither. In - // the pull build gPipeInputs is the live GLContext, there is no poison to bypass and the - // frontend getter still linear-scans, so the cache is exactly the code it was. - static inline FbBindingSlot& GetFramebufferBindingSlotChecked(FramebufferTarget target) { - if (EsprytSlotTablesEnabled()) { - return MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(target); - } - // No legacy arm compiled: EsprytSlotTablesEnabled() is unconditionally true above. - return MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(target); - } - - // A4, TAKEN AT THE VERIFICATION ROUND, AND THE FOUR TEMPORARY LATCHES ARE GONE WITH IT. - // This package landed BEFORE D and so carried its own bit tests, plus a copy of D-K2's - // dependency directions, so that a half-running mask was refused in the meantime too. - // Package D has landed, so every consult below calls D's PUBLIC WRAPPERS instead - - // FramebufferSubsystemEnabled() / TextureResourceSubsystemEnabled() / - // SamplerSubsystemEnabled() / ProgramSubsystemEnabled() (Managers.h, beside the four - // ResolveSubsystemArm() resolvers; the WRAPPER holds the `static const` latch, so - // the resolver is never the thing to call) - which additionally LOG each dependency - // refusal naming both bits and reach Fatal{PipeLegacyMemosDisabled} under - // MOBILEGL_PIPE_LEGACY_MEMOS=0, neither of which a copy here could do. - // - // NOTHING HERE RESTATES A DEPENDENCY DIRECTION any more - D-K2's fourth row (bit 10 - // requires bit 11, ID-15) included. It is in ResolveTextureResourceSubsystemArm, which is - // its owner, and two writers of one diagnostic is how the two drift. - - // THE ONE PLACE THIS FILE ASKS THE APPLIER "which framebuffer record describes this - // binding". Six reads used to spell `MGPipeApplier().DrawFramebuffer` / `.ReadFramebuffer` - // inline; they go through here instead, for exactly one reason. - // - // ID-19 turns the applier's framebuffer state into a PER-OBJECT table keyed by the - // framebuffer handle, with `BoundFramebuffer[Draw|Read]` holding the bound handles, so that - // the DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) can be - // handed a record for a framebuffer that is bound to neither binding. Wire v3 keeps - // `DrawFramebuffer` / `ReadFramebuffer` as ACCESSORS OF THOSE NAMES resolving through the - // bound handle, precisely so this package keeps its shape. If they land as member functions - // rather than as members, this one function grows a pair of parentheses and nothing else in - // this file moves; if the accessor can answer "no record for the bound handle", that answer - // arrives here as a null Fbo, which is already what every caller treats as a decline. - // - // WIRE v3 LANDED BOTH HALVES OF THAT PREDICTION: they are member FUNCTIONS and they return - // a NULLABLE POINTER, so the four call sites gained a null check each and nothing else in - // this file moved. Null is a real answer with exactly three causes (PipeApply.h): nothing - // is bound to that binding, no record has been written at the bound handle's slot, or the - // slot's generation has moved on under the handle - the third counted AND logged by the - // applier itself in StaleFramebufferRecordLookups. All three already arrived here as a - // null Fbo and were already a decline; the callers test the pointer instead. - static const MG_Pipe::MGPFramebufferState* BoundFramebufferRecord(FramebufferTarget target) { - const auto& st = MG_Pipe::MGPipeApplier(); - return target == FramebufferTarget::Draw ? st.DrawFramebuffer() : st.ReadFramebuffer(); - } - - [[noreturn]] static void RefuseFramebufferBindingSlotRead() { - MG_Pipe::MGPipeInputPoisonFatalForVerb(MG_Pipe::MGPipeInputField::GetFramebufferBindingSlot, - MG_Pipe::gPipeInputs->CurrentVerb()); - } - - static Bool IsDualSourceBlendFactor(BlendFactor v) { - switch (v) { - case BlendFactor::Src1Color: - case BlendFactor::OneMinusSrc1Color: - case BlendFactor::Src1Alpha: - case BlendFactor::OneMinusSrc1Alpha: - return true; - default: - return false; - } - } - // P4a (D-F3, [correction]). THE RAW-DEPTH-FETCH SUBSTITUTION STAYS ON THE SERVER, and it // keeps constructing a frontend SamplerObject inside MG_Backend on MONOLITH to do it. That is // deliberate and it is not this phase's to change: ARCHITECTURE.md assigns the two @@ -261,49 +79,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return NeedsRawDepthFetchSampler(samplerObject->GetAllSamplerParameters(), textureFormat); } - const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { - // P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer resolves - // from the handle the verb record carried, and its CPU bytes are the SERVER's staged - // shadow (rule E) - the client's GL_DRAW_INDIRECT_BUFFER binding slot, the frontend - // object's MappedData()/GetSize() (B3) and the client slot allocator (T2) are never - // read. Coverage is asserted: commands read from bytes no record staged would be - // zero-filled, which is a missing record, not a valid command stream. - const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer; - if (!MG_Pipe::MGPipeHandleIsNull(handle)) { - auto* resource = BufferImpl::FindBufferResourceForHandle(handle); - const Uint8* base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr; - // P8-C: a store nothing ever staged but a shader wrote (the GPU-culling shape) - // takes its first copy from this server's own GL buffer. A store that HAS a copy - // is not refreshed here: most indirect draws execute natively and read no field - // on the CPU, so ExecuteIndexed/ArraysIndirectCommands refresh the fields only - // when they consume them. - if (base == nullptr && resource != nullptr) { - base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_bytes"); - } - const SizeT commandOffset = reinterpret_cast(indirect); - if (base == nullptr) { - MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged " - "shadow on this side and no GPU write to take one from", - label, handle.Slot, handle.Gen); - return nullptr; - } - if (commandOffset + requiredBytes > BufferImpl::ResourceWidthForHandle(handle)) { - MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); - return nullptr; - } - BufferImpl::RequireStagedCoverage(*resource, base, commandOffset, - commandOffset + requiredBytes, "indirect_command_bytes"); - return base + commandOffset; } - // No buffer was bound: the frontend already raised the GL error; keep the old - // fall-through shape (a null indirect pointer is the same skip it was). - if (!indirect) { - MGLOG_E_ONCE("%s skipped: indirect pointer is null", label); - return nullptr; - } - MGLOG_E_ONCE("%s skipped: the verb carried no indirect buffer handle", label); - return nullptr; - } - namespace DebugImpl { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG void ErrorLopper::Loop(const std::function& func) { @@ -371,15169 +146,4 @@ namespace MobileGL::MG_Backend::DirectGLES { OpenGLScopeMarker::~OpenGLScopeMarker() {} #endif } // namespace DebugImpl - - // TODO: deletion for deleted objects - - namespace BufferImpl { - // P5e (mv): THE ARM SELECTOR of this family - VertexInputReadsRecords() - moved to - // Managers.h, unchanged. MultiDraw.cpp is a second translation unit on the same draw - // path and has to select the same arm; a copy of the conjunction there would be two - // selectors for one family. Its rationale travelled with it. - - // P5e (vi): "does this draw fetch any attribute out of the application's own memory", - // answered FROM THE RECORD instead of from the frontend VAO. The emitter publishes - // Res == kMGPipeNullHandle for a client-memory array (VertexInputEmit.h says so in as - // many words: "A client-memory array is Res == kMGPipeNullHandle, and that is not a - // hole"), and the window is truncated at the highest ENABLED attribute, so a null Res - // inside [Start, Start+Count) is exactly the client-sourced case and nothing else. - // - // BOTH VIEWS ARE NEEDED, and taking only the buffer one would be wrong in the common - // direction: the window covers [0, highest enabled + 1), so a VAO whose attribute 0 is - // DISABLED and whose attribute 1 feeds from a VBO publishes entry[0].Res == null too. - // A null Res is "client-sourced" only for an attribute the CONFIGURATION has enabled, - // which is what the vertex-elements record answers. Reading one view and not the other - // would send every such ordinary draw back to the frontend VAO and keep the row this - // package is retiring alive for no reason. - // - // A prefix scan rather than a memo: Count is 1-3 on the measured workload, not 32, and - // this replaces a cross-TU accessor call plus a SharedPtr dereference per DrawArrays. - Bool AnyClientSideVertexArrayInRecord() { - const auto& st = MG_Pipe::MGPipeApplier(); - if (st.VertexBufferCount == 0) return false; - const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements; - if (MG_Pipe::MGPipeHandleIsNull(elements) || elements.Slot >= st.VertexElementsCsos.size()) { - return false; - } - const auto& record = st.VertexElementsCsos[elements.Slot]; - if (!record.Live || record.Gen != elements.Gen) return false; - - const Uint32 begin = st.VertexBufferStart; - const Uint32 end = begin + st.VertexBufferCount; - for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) { - if (!record.Attributes[i].Enabled) continue; - if (MG_Pipe::MGPipeHandleIsNull(st.VertexBuffers[i].Res)) return true; - } - return false; - } - - // See Managers.h for why these two are named functions over the applier state and - // nothing else. - Uint ResolveDrawVertexBuffersFromRecord(const MG_Pipe::MGPipeApplierState& st, - DrawVertexBufferRequest* out, Uint capacity) { - Uint count = 0; - const Uint32 begin = st.VertexBufferStart; - const Uint32 end = begin + st.VertexBufferCount; - for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) { - const MG_Pipe::MGPVertexBuffer& binding = st.VertexBuffers[i]; - if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) continue; - - // A {slot, gen} compare, not a raw address compare: an address a successor - // object can reproduce is the identity hazard this phase exists to remove. - Bool alreadySeen = false; - for (Uint j = 0; j < count; ++j) { - if (out[j].Res == binding.Res) { - alreadySeen = true; - break; - } - } - if (alreadySeen) continue; - if (count >= capacity) { - MGLOG_E_ONCE("MGPipe: the vertex-buffer window named more distinct buffers than " - "a draw can hold (%u); the tail is dropped", capacity); - break; - } - out[count].Res = binding.Res; - out[count].BindingIndex = binding.BindingIndex; - ++count; - } - return count; - } - - DrawIndexBufferRequest ResolveDrawIndexBufferFromRecord(const MG_Pipe::MGPipeApplierState& st) { - DrawIndexBufferRequest request; - request.Res = st.IndexBuffer.Res; - request.Serial = st.IndexBufferSerial; - return request; - } - - // True from a draw or dispatch that ran with a storage buffer bound until the next - // barrier that orders storage-buffer writes. Every bound storage point counts as written, - // the client's own conservative set (see serverGpuWritten below). - // - // WHAT IT IS FOR: two dispatches with no barrier between them that write DIFFERENT - // components of one vector in the same storage buffer. GL leaves the two unordered, which - // allows them to run concurrently, but every write still lands - they are separate memory - // locations. A driver that stores a vector component as a read-modify-write of the whole - // vector loses one of them instead: KHR-GL43.shader_storage_buffer_object. - // advanced-switchBuffers-cs writes digest.x .. digest.w of one uvec4 from four - // back-to-back dispatches and read back a zero component in 8 runs of 20 on Adreno (0 of - // 20 with a storage barrier between the dispatches, 0 of 20 with uint[4]). The fix is the - // barrier the application did not owe: PrepareForCompute issues it when the previous - // shader work may have written a storage buffer and nothing ordered those writes since. - // - // Compute only. A dispatch that follows a dispatch is the shape the test has and the one - // a driver may overlap; draws keep their own ordering and are not taxed, they only SET - // the flag here - one store in the walk that already visits every bound point. - static Bool g_shaderStorageWritesUnordered = false; - - // Called by the application's own glMemoryBarrier: a barrier that orders storage writes - // already did the work. - static void NoteShaderStorageWritesOrdered() { g_shaderStorageWritesUnordered = false; } - - // The record arm of SyncBufferBindingPoints. Same shape, same order, same branches; the - // four frontend reads become the applier's window and a handle per entry. - // - // WHAT THE WINDOW IS: ShaderBufferCount[class] starting at ShaderBufferStart[class], - // which is the client's touched high-water mark clamped to the 84 the wire carries - // (ruling 10). It is NOT clamped on the client to the device's ceiling, so the ES clamp - // below stays exactly where it was - a client reading GL_MAX_UNIFORM_BUFFER_BINDINGS - // would be answering a driver question from the wrong side of the wire. - // - // WHOLE-VS-RANGE IS `Offset == 0 && Size == kMGPipeWholeBuffer` and obj->GetSize() - // disappears from this function: a base binding does not freeze an extent, the client - // deliberately does not resolve one, and the re-resolution happens HERE against the - // server's own storage - which is what makes a glBufferData issued between the emission - // and this apply bind the NEW extent, as GL does. - void SyncBufferBindingPointsByRecord(Uint32 shaderBufferClass, GLenum glTarget) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); - const Uint32 start = st.ShaderBufferStart[shaderBufferClass]; - SizeT end = static_cast(start) + st.ShaderBufferCount[shaderBufferClass]; - // ...and never past what the ES driver itself can hold, exactly as the frontend - // walk does and for the same reason recorded there. - if (glTarget == GL_UNIFORM_BUFFER && g_GLESCapabilities.MaxUniformBufferBindings > 0) { - end = std::min(end, static_cast(g_GLESCapabilities.MaxUniformBufferBindings)); - } - for (SizeT i = start; i < end; ++i) { - const MG_Pipe::MGPBufferRange& entry = - st.BoundShaderBuffers[shaderBufferClass][i]; - if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) { - BindBufferBaseCached(glTarget, static_cast(i), 0); - continue; - } - auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry.Res); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for %s binding point %zu (handle {%u, %u}).", - MG_Util::ConvertGLEnumToString(glTarget).c_str(), i, entry.Res.Slot, - entry.Res.Gen); - continue; - } - // P8-C: the SERVER's half of the mark MarkShaderStorageBuffersGpuWritten no longer - // makes under a transport - on this side's twin, for this side's CPU readers - // (SplitHostBytesForCpuRead). Every bound storage point, which is the client's - // own conservative set (ShaderBufferEmit.h marks every one writable); a uniform - // point is read-only by definition. - if (shaderBufferClass != MG_Pipe::kMGPipeShaderBufferClassUniform) { - backendResource->serverGpuWritten = true; - g_shaderStorageWritesUnordered = true; - } - const auto backendBufferId = backendResource->id; - if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) { - BindBufferBaseCached(glTarget, static_cast(i), backendBufferId); - } else { - // CLAMPED AGAINST THE SERVER's OWN STORAGE, which is what the frontend walk - // clamped against obj->GetSize() for: a range that outruns the storage is a - // GL_INVALID_VALUE the driver would raise on a bind the application already - // made legally against a buffer that has since shrunk. - const SizeT storage = backendResource->storageSize; - const SizeT rangeStart = std::min(static_cast(entry.Offset), storage); - const SizeT rangeEnd = - std::min(static_cast(entry.Offset + entry.Size), storage); - BindBufferRangeCached(glTarget, static_cast(i), backendBufferId, - static_cast(rangeStart), - static_cast(rangeEnd - rangeStart)); - } - } - } - - void SyncAtomicCounterBuffers(const Vector& glBindings, Int esslBindingTop) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // P5e (sb, §5.6): THE RECORD ARM. `glBindings` stays the backend program's own list - // - it is server-owned since the link and names which GL counter bindings THIS - // program declares - and only the points themselves move sides. The bound is the - // applier's window instead of GetBufferBindingPointCount's sticky forward, which is - // the row this site was the Espryt reader of. - const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); - const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; - const SizeT windowEnd = static_cast(windowStart) + - st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; - for (const Int glBinding : glBindings) { - if (glBinding < 0) continue; - const Int esslBinding = esslBindingTop - glBinding; - // Already diagnosed once when the block was transpiled; nothing was bound to - // it there either, so there is nothing to unbind here. - if (esslBinding < 0) continue; - // OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's - // default says too - so it takes the unbind arm rather than being skipped. - // Skipping would leave whatever the driver last had at that ESSL point, - // which is the hole the frontend walk's `continue` never had because its - // bound was the whole 84-point array. - const MG_Pipe::MGPBufferRange* entry = nullptr; - if (static_cast(glBinding) >= windowStart && - static_cast(glBinding) < windowEnd) { - entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter] - [static_cast(glBinding)]; - } - if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) { - BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), 0); - continue; - } - auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d " - "(handle {%u, %u}).", - glBinding, entry->Res.Slot, entry->Res.Gen); - continue; - } - if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) { - BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), - backendResource->id); - } else { - const SizeT storage = backendResource->storageSize; - const SizeT rangeStart = std::min(static_cast(entry->Offset), storage); - const SizeT rangeEnd = - std::min(static_cast(entry->Offset + entry->Size), storage); - BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), - backendResource->id, static_cast(rangeStart), - static_cast(rangeEnd - rangeStart)); - } - // AND NO MarkBufferGpuWritten HERE. It was the second of the three backend - // GPU-write sites and it reached into a client object; the client's - // MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set - // from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched - // counter point rather than only the ones this program declares, which is - // the safe direction for an over-approximate set. - // - // P8-C: the server's own twin is marked instead, for the server's own CPU - // readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a - // parameter or element buffer is read on this side. - backendResource->serverGpuWritten = true; - } - return; - } - - // P5e (vi), CONTRACT-P5E §5.1: THE SAME MEMO, THE SAME THREE-VALUE KEY, AND A WALK THAT - // READS NO FRONTEND AT ALL. The arm above keeps the frontend attribute walk because the - // push-monolith build has to keep its bytes (§5.8); this one is what a server with no - // client objects on its side runs, and the difference between them is exactly three - // substitutions: - // - // the walk st.VertexBuffers[Start .. Start+Count) instead of - // GetAllAttributes(): the SAME set, because the emitter resolves the - // attributes on the client and publishes one entry per attribute slot - // (VertexInputEmit.h's EmitVertexBuffers), with Res == null for a - // client-memory array and for a disabled slot - which is why the null - // skip below is not a hole but the record's own "nothing to ensure". - // the dedupe a {slot, gen} compare instead of a raw address compare. An address - // a successor object can reproduce is the identity hazard this whole - // phase is about; a handle cannot be reproduced (Gen moves on reuse). - // the ensure EnsureBufferResourceForHandle(nullptr, Res). The frontend argument - // is only ever used for BufferObject::SyncPersistentMappedRange, which - // that function already skips under a transport (D-N), so passing - // nullptr removes a read rather than dropping work - and (nullptr, h) - // is already the live shape of the indirect-buffer ensure two hundred - // lines below. - // - // The IBO half lives in SyncNeccessaryBuffers beside the legacy one, for the reason it - // always did: the index slot is not part of the configuration (D5). - void SyncVaoAttributeBuffersByRecord( - VertexArrayImpl::BackendVertexArrayObject::ResolvedDrawBuffers* memo, Uint64 bufferEpoch) { - const auto& st = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements; - Uint64 elementsSerial = 0; - Bool haveElementsRecord = false; - if (!MG_Pipe::MGPipeHandleIsNull(elements) && elements.Slot < st.VertexElementsCsos.size()) { - const auto& record = st.VertexElementsCsos[elements.Slot]; - if (record.Live && record.Gen == elements.Gen) { - elementsSerial = record.ContentSerial; - haveElementsRecord = true; - } - } - const Uint64 buffersSerial = st.VertexBuffersSerial; - - // NO LIVE ELEMENTS RECORD IS A MISS, NEVER A HIT - the arm above states the whole - // argument (MGPipeApplierReset empties the table at every context change and the - // key would then never move again). - const Bool memoKeyIsMeaningful = haveElementsRecord; - - if (memo && memo->valid && memoKeyIsMeaningful && memo->elementsHandle == elements && - memo->elementsSerial == elementsSerial && memo->buffersSerial == buffersSerial) { - if (memo->vboCleanEpoch != bufferEpoch) { - Bool allClean = true; - for (Uint i = 0; i < memo->count; ++i) { - auto& entry = memo->entries[i]; - // nullptr, not entry.frontend: the frontend question inside - // IsBufferDrawCleanByHandle is monolith-only already - // (askTheObjectWhetherItIsMapped), and a raw client address held across - // records is precisely what §4.4 stops the apply thread dereferencing. - if (IsBufferDrawCleanByHandle(entry.handle, entry.resource, nullptr)) continue; - allClean = false; - // Re-ensured BY HANDLE. The legacy repair went back through the memoed - // attribute index to re-read attrib.Buffer; here the entry already - // names the resource the record named, so the repair needs nothing the - // walk did not already have. - entry.resource = EnsureBufferResourceForHandle(nullptr, entry.handle); - } - memo->vboCleanEpoch = allClean ? bufferEpoch : 0; - } - return; - } - - // Full walk, once per distinct buffer, rebuilding the memo as it goes. WHICH - // buffers is ResolveDrawVertexBuffersFromRecord's answer and only its answer - see - // Managers.h: that separation is what lets a unit case drive the source decision - // this package changed without an ES context, and what makes a revert of it visible - // rather than duplicated. - DrawVertexBufferRequest requests[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS]; - const Uint syncedBufferCount = ResolveDrawVertexBuffersFromRecord( - st, requests, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); - for (Uint i = 0; i < syncedBufferCount; ++i) { - auto* resource = EnsureBufferResourceForHandle(nullptr, requests[i].Res); - if (memo) { - auto& entry = memo->entries[i]; - // Explicitly cleared rather than left as whatever the previous build of - // this memo stored: nothing on this arm may dereference it, and a stale - // address that merely happens never to be read is the kind of thing a - // later reader takes for a live one. - entry.frontend = nullptr; - entry.attribIndex = static_cast(requests[i].BindingIndex); - entry.resource = resource; - entry.handle = requests[i].Res; - } - } - if (memo) { - memo->count = syncedBufferCount; - memo->elementsHandle = elements; - memo->elementsSerial = elementsSerial; - memo->buffersSerial = buffersSerial; - memo->valid = memoKeyIsMeaningful; - // Rebuilt via the ensure, not probed clean: the next probe pass stamps it. - memo->vboCleanEpoch = 0; - } - } - - // `vaoConfigVersion` is the caller's early read of currentVAOObject->GetConfigVersion(): - // the VAO's config fields live on a cache line the draw path touches nowhere else, and - // cycling section VAOs makes that a guaranteed miss - reading it at the top of - // PrepareForDraw overlaps the miss with the program/texture-key work instead of - // stalling the memo check below. Nothing between the read and here can move it - // (only frontend GL entry points mutate VAO config, none run inside a preparation). - void SyncNeccessaryBuffers(const SharedPtr& currentVAOObject, - VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Uint32 vaoConfigVersion, - Bool includeIBO = false, Bool includeIndirectBuffer = false) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - ProcessDeferredBufferReleases(); -#if MOBILEGL_BUILD_DISAGGREGATED - ProcessDeferredT0Retires(); // P11 B2 -#endif - - // All buffers we need are: - // 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed) - // PBO is not needed since it should be handled in frontend - - // VBO. The distinct-buffer set is memoed on the VAO's backend twin: cycling - // hundreds of section VAOs re-walked 32 cold VertexAttribute slots per draw - // only to rediscover the same one or two static buffers. While the config - // version holds (every attach/enable/disable bumps it, and it pins each - // memoed frontend pointer via the attribute SharedPtrs), the walk reduces to - // an IsBufferDrawClean probe per distinct buffer; only dirty entries take - // EnsureBufferResource, re-fetched through their attribute index. - // Pre-pass epoch read (acquire), stamped into the memo only after a pass in - // which EVERY probe came up clean: while the stamp still equals the current - // epoch, no path that can dirty any buffer has run (see the mutation-site - // enumeration at CurrentBufferMutationEpoch's declaration in Managers.h), - // so the probes themselves are skipped. A mid-pass bump lands after this - // read, makes the stamp stale, and re-runs the probes next draw. - const Uint64 bufferEpoch = CurrentBufferMutationEpoch(); - auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr; - const Uint32 configVersion = vaoConfigVersion; - SyncVaoAttributeBuffersByRecord(memo, bufferEpoch); - - // IBO, memoed by bound-object identity (its slot version is not covered by - // the config version). A stale identity hit is impossible in effect: the - // clean probe re-validates the resource against the LIVE bound object. - if (includeIBO) { - // P5e (vi), CONTRACT-P5E §5.1: the index buffer is st.IndexBuffer.Res and the - // "has it moved" question is st.IndexBufferSerial. The identity compare stays - - // it always did, because the index slot is outside the configuration version - // (D5) - but it now compares a handle the RECORD named instead of a frontend - // address the slot was probed for, and the serial joins it because this arm has - // no live slot to re-read: see ResolvedDrawBuffers::iboSerial in Managers.h. - // - // A null Res is "no element buffer bound" and is a no-op here exactly as the - // absent `possibleIBO` is below: SyncToBackendFromApplier is what binds - // GL_ELEMENT_ARRAY_BUFFER back to 0 for such a draw, and it keys on the same - // serial. - const DrawIndexBufferRequest request = - ResolveDrawIndexBufferFromRecord(MG_Pipe::MGPipeApplier()); - const MG_Pipe::MGPipeHandle iboHandle = request.Res; - const Uint64 iboSerial = request.Serial; - if (!MG_Pipe::MGPipeHandleIsNull(iboHandle)) { - const Bool identityHolds = - memo != nullptr && memo->iboHandle == iboHandle && memo->iboSerial == iboSerial; - if (identityHolds && memo->iboCleanEpoch == bufferEpoch) { - // probed fully clean at this epoch; nothing can have dirtied it - } else if (identityHolds && - IsBufferDrawCleanByHandle(iboHandle, memo->iboResource, nullptr)) { - memo->iboCleanEpoch = bufferEpoch; - } else { - auto* resource = EnsureBufferResourceForHandle(nullptr, iboHandle); - if (memo) { - memo->iboHandle = iboHandle; - memo->iboSerial = iboSerial; - memo->iboFrontend = nullptr; // see Entry::frontend's note - memo->iboResource = resource; - // Repaired, not probed clean: stamp on the next clean probe. - memo->iboCleanEpoch = 0; - } - } - } - } - - // Indirect Buffer Object - must also be bound to GL_DRAW_INDIRECT_BUFFER on the ES - // context since indirect draws now execute natively on the GPU. - if (includeIndirectBuffer) { - // P5c (hd, CONTRACT-P5C §3.5): under an active transport the buffer is the verb - // record's handle; the frontend binding slot and the client allocator are - // never read (T2). The twin's ensure is what SyncBoundBuffer's did. - const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer; - if (!MG_Pipe::MGPipeHandleIsNull(handle)) { - auto* resource = EnsureBufferResourceForHandle(nullptr, handle); - if (resource && resource->id != 0) { - BindBufferId(GL_DRAW_INDIRECT_BUFFER, resource->id); - } - } - } - - // UBO binding points are (re)established per draw by BindCurrentProgramWithResources at - // their compacted link-time points: CacheResourceLocations glUniformBlockBinding's the - // transpiled ESSL blocks to points 0,1,2,... (layout(binding=N) is stripped from the - // ESSL), so those compacted points are the only ones the shader reads. A frontend-indexed - // sync here would bind points the shader never reads and is unconditionally overwritten by - // the program rebind that always follows in PrepareForDraw - i.e. redundant for draws - so - // it is intentionally omitted (see SyncComputeBuffers for the compute path, which needs it). - // SSBOs are different: their block bindings are baked into the ESSL at compile time and - // BindCurrentProgramWithResources binds no SSBO points, so this is their sole draw-path - // binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers). - // P5e (sb, §5.6): the record arm, selected by transport + bit 13. Same `if (…) else` - // shape the indirect buffer above already uses. - SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, - GL_SHADER_STORAGE_BUFFER); - } - - void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - ProcessDeferredBufferReleases(); -#if MOBILEGL_BUILD_DISAGGREGATED - ProcessDeferredT0Retires(); // P11 B2 -#endif - // THE COMPUTE PATH IS THE ONE THAT NEEDS THE FRONTEND-INDEXED UNIFORM PASS, and it - // is why the uniform class is emitted at all: compute does NOT go through the - // per-program block remap in BindCurrentProgramWithResources, so these are the - // points a compute shader actually reads. - SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform, - GL_UNIFORM_BUFFER); - SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, - GL_SHADER_STORAGE_BUFFER); - if (includeDispatchIndirectBuffer) { - // P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb - // record's handle, never the frontend binding slot or the client allocator. - const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer; - if (!MG_Pipe::MGPipeHandleIsNull(handle)) { - auto* resource = EnsureBufferResourceForHandle(nullptr, handle); - if (resource && resource->id != 0) { - BindBufferId(GL_DISPATCH_INDIRECT_BUFFER, resource->id); - } - } - } - } - } // namespace BufferImpl - - // Transform feedback is captured by the real ES driver: the backend program - // declares the capture set at link time (see BackendProgramObjectImpl::SyncToBackend) - // and the span below wraps the driver's own glBeginTransformFeedback/glEndTransformFeedback. - // - // The driver-side Begin is deferred from the frontend's glBeginTransformFeedback to - // the first draw of the span: ES requires the capturing program to be current and - // the capture buffers bound when Begin is issued, and both of those only become true - // once PrepareForDraw has run. A span that never draws therefore never touches the - // driver at all, which is also what the GL semantics amount to. - // - // Transform feedback objects (ARB_transform_feedback2) are ES 3.0 core, so each - // frontend object gets one of the driver's: a paused span lives inside the ES object, - // which is the only way several of them can be paused at once - and the only reason - // the default object alone would not do. - namespace XfbImpl { - namespace { - struct XfbCaptureTarget { - SharedPtr buffer; - Uint backendId = 0; - SizeT start = 0; - SizeT end = 0; - // WHICH capture buffer of the program this is. The list is COMPACTED - a - // capture buffer with no bound buffer object contributes no entry - so the - // position in the vector is not the program's buffer index, and everything - // that asks the program about a target (its stride, which varyings land in - // it) has to ask about this index instead. A capture list beginning with - // gl_NextBuffer is the shape that makes them differ: buffer 0 has stride 0 - // and nothing bound, so target 0 describes buffer 1. - SizeT bufferIndex = 0; - MG_Pipe::MGPipeHandle handle = MG_Pipe::kMGPipeNullHandle; - }; - - - struct XfbProgramSource { - SharedPtr Frontend; - SharedPtr Archive; - explicit operator Bool() const { return Frontend || Archive; } - const XfbProgramSource* operator->() const { return this; } - SizeT GetTransformFeedbackBufferCount() const { - return Archive ? Archive->Link.xfbStrides.size() : Frontend->GetTransformFeedbackBufferCount(); - } - Uint32 GetTransformFeedbackStride(Uint32 index) const { - return Archive ? (index < Archive->Link.xfbStrides.size() ? Archive->Link.xfbStrides[index] : 0) - : Frontend->GetTransformFeedbackStride(index); - } - Bool NeedsScatteredTransformFeedbackCapture() const { - return Archive ? Archive->Link.xfbNeedsScatteredCapture : Frontend->NeedsScatteredTransformFeedbackCapture(); - } - Uint32 GetTransformFeedbackPackedStride() const { - return Archive ? Archive->Link.xfbPackedStride : Frontend->GetTransformFeedbackPackedStride(); - } - GLenum GetTransformFeedbackBufferMode() const { - return Archive ? Archive->Link.xfbBufferMode : Frontend->GetTransformFeedbackBufferMode(); - } - const auto& GetTransformFeedbackVaryings() const { - return Archive ? Archive->Link.xfbVaryings : Frontend->GetTransformFeedbackVaryings(); - } - }; - // Per frontend transform feedback object. The default object (name 0) maps to - // the driver's default object (id 0) and is always present. - struct XfbObjectState { - GLuint esId = 0; - Bool pending = false; // frontend Begin seen, driver capture not started yet - Bool started = false; // driver capture running - Bool paused = false; // frontend Pause seen and not yet resumed - GLenum primitiveMode = GL_POINTS; - Vector targets; - // Set for a layout ES cannot express (gl_SkipComponents / gl_NextBuffer): - // the driver captures gap-free records into the scratch buffer below and - // End scatters them into `targets`. - Bool scattered = false; - SharedPtr scatterProgram; - SharedPtr scatterArchive; - // Paused spans in different served contexts may coexist. Their captured - // bytes must not share the monolith's one reusable scratch buffer. - GLuint serverScatterBuffer = 0; - SizeT serverScatterSize = 0; - SizeT scatterCapacityVertices = 0; - }; - - // One scratch ES buffer serves every scattered capture: only one span can be - // recording at a time (the driver would reject a second Begin), so its contents - // are consumed by the End that follows. - GLuint g_scatterBufferId = 0; - SizeT g_scatterBufferSize = 0; - - UnorderedMap g_xfbObjects; - GLuint g_currentXfbName = 0; - // Cached address of g_xfbObjects[g_currentXfbName]: PrepareForDraw consults - // CurrentXfb on EVERY draw (StartPendingTransformFeedback) and the map - // lookup was pure per-draw overhead for the overwhelmingly common no-capture - // case. Open addressing keeps values in the bucket array, so ANY insert can - // rehash and move them - and erase moves them too, by shifting the rest of the - // probe cluster into the hole, which reaches entries other than the erased one. - // Every site that mutates the map or rebinds the current name resets this to - // null instead of reasoning about stability, and CurrentXfb re-resolves lazily. - XfbObjectState* g_currentXfbState = nullptr; - - struct RoleXfbState { - Uint NativeGeneration = 0; - // Server keys are record-supplied, never-reused XFB lifetime ids. - // The monolith role keeps its existing GL-name semantics separately. - UnorderedMap Objects; - Uint64 Current = 0; - XfbObjectState* Cached = nullptr; - GLuint ScatterBuffer = 0; - SizeT ScatterSize = 0; - Bool NeedsBind = true; - }; - // P13 W6: one role - the record arm's (the frontend arm's GL-name keyed state went). - Array g_roleXfbState; - - RoleXfbState& ActiveXfbState() { - auto& state = g_roleXfbState[0]; - if (state.NativeGeneration != g_backendContextGeneration) { - state = {}; - state.NativeGeneration = g_backendContextGeneration; - } - return state; - } - - XfbObjectState& CurrentXfb() { - auto& state = ActiveXfbState(); - const Uint64 lifetime = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId; - if (state.Current != lifetime || state.Cached == nullptr || state.NeedsBind) - BindTransformFeedback(0); // identity is the applier's lifetime, not this argument - auto& g_currentXfbState = state.Cached; - auto& g_xfbObjects = state.Objects; - const auto g_currentXfbName = state.Current; - if (g_currentXfbState == nullptr) { - g_currentXfbState = &g_xfbObjects[g_currentXfbName]; - } - return *g_currentXfbState; - } - - // EVERY way this path can lose a capture used to be silent: three unlogged early - // returns before the driver Begin, an unchecked glBeginTransformFeedback, and two - // `continue`s in the readback. The application sees a buffer that kept its - // pre-draw bytes, GL_NO_ERROR, and GL_LINK_STATUS true - which is how one defect - // reached ~320 conformance bodies across four families before anyone could say - // which of the branches fired. Nothing below changes what MobileGL DOES on a - // healthy capture; it only makes a lost one name itself in /sdcard/MG/latest.log. - // - // MGLOG_E_ONCE (not _D) on purpose: these have to be readable in an INFO-level - // artifact, the same reason the backend link failure at Managers.cpp is MGLOG_E. - constexpr Int kMaxDrainedXfbErrors = 32; - - // The ES error raised by the call just issued, GL_NO_ERROR if it succeeded. Drains - // the rest of the queue so the next probe cannot read this one as its own. - GLenum TakeXfbDriverError() { - const GLenum first = g_GLESFuncs.glGetError(); - if (first == GL_NO_ERROR) return GL_NO_ERROR; - for (Int i = 0; i < kMaxDrainedXfbErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { - } - return first; - } - - Bool AreTransformFeedbackObjectsSupported() { - return g_GLESFuncs.glGenTransformFeedbacks != nullptr && - g_GLESFuncs.glBindTransformFeedback != nullptr && - g_GLESFuncs.glDeleteTransformFeedbacks != nullptr && - g_GLESFuncs.glPauseTransformFeedback != nullptr && - g_GLESFuncs.glResumeTransformFeedback != nullptr; - } - - // How wide one OnBufferWriteback may be for a capture range of `rangeBytes`, which is - // the whole range when the transport imposes no limit (monolith, where the callback is - // a direct call with no ring under it) or when the range already fits one. - // - // BOTH XFB WRITEBACK PRODUCERS ASK THE SAME FUNCTION. They post from different sources - // - one from a live glMapBufferRange mapping, one from a staged Vector - but - // they post the same KIND of record into the same ring, and a second spelling of the - // width is how one of them ends up being the producer that still kills the server. - SizeT XfbWritebackSliceStep(SizeT rangeBytes) { - const Uint64 slice = MG_Pipe::MGPipeBufferWritebackSliceBytes(); - if (slice == 0 || slice >= static_cast(rangeBytes)) return rangeBytes; - return static_cast(slice); - } - - // Mirrors one capture span's results into the frontend CPU shadows. The GPU wrote - // the capture buffers behind the frontend's back, so the shadows that back - // MapBuffer/GetBufferSubData still hold the pre-draw bytes. Buffers whose storage - // the backend already owns (coherent persistent map) need nothing: reads resolve - // against that storage directly. - void ReadbackCapturedRanges(Vector& targets) { - if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) { - MGLOG_E_ONCE("EndTransformFeedback: the ES driver exposes no glMapBufferRange/glUnmapBuffer, so " - "captured data can never reach the application's buffers"); - } - if (targets.empty()) { - // The span closed with nothing to mirror back. Either the deferred Begin - // never ran (a span with no draw - legal) or it ran and found no bound - // capture buffer, which is not. - MGLOG_D("EndTransformFeedback: capture span closed with no recorded targets"); - } - if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) { - for (const auto& target : targets) { - // P5c: under an active transport the frontend object is client - // memory (rule E), so the persistence question is the server - // resource's and the captured bytes go back as a writeback EVENT - - // WritebackFromBackend from the apply thread is the R1/R2 shape. - const MG_Pipe::MGPipeHandle res = target.handle; - auto* resource = BufferImpl::FindBufferResourceForHandle(res); - if (resource == nullptr) continue; - if (resource->persistentMapped) { - // P8-C: nothing to copy - the capture reaches a coherent map - - // but this side's CPU readers must finish before reading it. - resource->serverGpuWritten = true; - continue; - } - const SizeT size = target.end - target.start; - BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId); - void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, - static_cast(target.start), - static_cast(size), GL_MAP_READ_BIT); - if (mapped == nullptr) { - MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u [%zu, %zu) for " - "capture readback (ES error %s); the captured data will NOT be visible to " - "the application", - target.backendId, target.start, target.end, - MG_Util::ConvertGLEnumToString(TakeXfbDriverError()).c_str()); - continue; - } - if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) { - // ONE EVENT PER SLICE, NOT ONE PER CAPTURE. The bytes travel - // INLINE in a SEG_EVENT record and the ring refuses any record - // above half its capacity outright, so a whole-range post of a - // capture wider than ~128 KiB was Fatal{EventRingOverflow} - the - // SERVER dying on a large buffer. The slicing shape is - // BufferObject.cpp's one ring over: in-order delivery makes the - // last slice's landing imply every earlier one, so the client - // needs nothing new to reassemble them. - // - // INSIDE THE MAPPING, deliberately: the map is the expensive part - // and the slices are reads of one live pointer, so this unmaps - // once below however many events it posts. - const SizeT step = XfbWritebackSliceStep(size); - for (SizeT off = 0; off < size; off += step) { - const SizeT bytes = std::min(step, size - off); - MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( - res, target.start + off, - MG_Pipe::MGPBlobRef{ - reinterpret_cast(static_cast(mapped) + off), - static_cast(bytes), MG_Pipe::kMGHostSpanSegNone, 0}); - } - } else { - MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the " - "captured bytes of buffer {%u,%u} cannot reach the client shadow", - res.Slot, res.Gen); - } - // P8-C: the server's own staged copy takes the same bytes, from the - // same mapping, so this side's CPU readers see the capture too. - BufferImpl::StageServerWrittenRange(*resource, res, mapped, target.start, size); - g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget); - // Ops_H_Readback's order: writeback, unmap, THEN the serial - // stamp, so the next draw does not re-upload the server's stale - // staged bytes over the capture that just landed. - resource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(res); - BufferImpl::BumpBufferMutationEpoch(); - continue; - } - } - targets.clear(); - } - - // Binds a scratch buffer, sized for `capacityVertices` gap-free records, to - // capture point 0 in place of the application's buffers. Returns false when the - // scratch storage cannot be provided, in which case the caller falls back to the - // direct binding (which produces a wrong layout, but is what happened before). - Bool BindScatterCaptureBuffer(SizeT packedStride, SizeT capacityVertices) { - auto& g_scatterBufferId = CurrentXfb().serverScatterBuffer; - auto& g_scatterBufferSize = CurrentXfb().serverScatterSize; - if (packedStride == 0 || capacityVertices == 0) return false; - if (g_GLESFuncs.glGenBuffers == nullptr || g_GLESFuncs.glBufferData == nullptr) return false; - const SizeT required = packedStride * capacityVertices; - if (g_scatterBufferId == 0) { - g_GLESFuncs.glGenBuffers(1, &g_scatterBufferId); - if (g_scatterBufferId == 0) return false; - g_scatterBufferSize = 0; - } - if (g_scatterBufferSize < required) { - BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); - g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(required), nullptr, - GL_DYNAMIC_COPY); - g_scatterBufferSize = required; - } - // Point 0 carries every captured varying: the gl_NextBuffer / gl_SkipComponents - // entries are consumed at link time and never reach the driver, so the ES - // program is declared INTERLEAVED over a single buffer and point 0 is the only - // point it can write (GL 4.6 core 13.2.2). - // - // The other points are therefore left exactly as they are. Clearing them - which - // this used to do, across the application's whole touched high-water mark - is - // both unnecessary (the ES program cannot write an unused point) and the precise - // trigger for the Mali G1-Ultra capture loss: see - // SyncTransformFeedbackBindingPoints for the mechanism and the device evidence. - // KHR-GL46.transform_feedback.capture_special_interleaved_test is the case that - // reaches this path. - BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0, - static_cast(required)); - return true; - } - - // Distributes the gap-free records the driver captured into the application's - // buffers at the offsets the GL layout asks for. Only the bytes a varying actually - // occupies are written, so the holes gl_SkipComponents asks for keep whatever the - // application had put there - which is the whole point of the feature. - void ScatterCapturedRecords(XfbObjectState& xfb) { - auto& g_scatterBufferId = xfb.serverScatterBuffer; - const XfbProgramSource program{xfb.scatterProgram, xfb.scatterArchive}; - if (!program || xfb.targets.empty()) return; - if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return; - - const SizeT packedStride = program->GetTransformFeedbackPackedStride(); - const SizeT modelledVertices = - static_cast(MG_Pipe::gPipeInputs->GetTransformFeedbackCapturedVertices()); - const SizeT vertices = std::min(modelledVertices, xfb.scatterCapacityVertices); - if (packedStride == 0 || vertices == 0) { - // The scatter path redirected the DRIVER's capture into the scratch buffer, - // so bailing here leaves the application's buffers holding their pre-draw - // bytes - a total data loss, not a no-op. The vertex count is the CPU model - // (AccountTransformFeedbackPrimitives), which is 0 for any draw mode - // CountPrimitivesForDraw does not know and for the instanced/indirect entry - // points that never call it. - MGLOG_E_ONCE("EndTransformFeedback: scattered capture discarded - packedStride=%zu, " - "CPU-modelled captured vertices=%zu, scratch capacity=%zu. The capture buffers keep " - "their pre-draw contents.", - packedStride, modelledVertices, xfb.scatterCapacityVertices); - return; - } - - BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); - const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0, - static_cast(packedStride * vertices), - GL_MAP_READ_BIT); - if (packed == nullptr) { - MGLOG_E_ONCE("EndTransformFeedback: failed to map the scatter capture buffer"); - return; - } - - // One staged copy per destination buffer: start from what the application had - // (the shadow is authoritative - uploads go shadow -> ES, and previous captures - // were mirrored back into it), patch the captured varyings in, then push the - // whole range down once. - for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) { - const auto& target = xfb.targets[targetIndex]; - // By BUFFER index, not by position in the compacted list - see XfbCaptureTarget. - const SizeT stride = program->GetTransformFeedbackStride(static_cast(target.bufferIndex)); - if (stride == 0) continue; - const SizeT rangeBytes = target.end - target.start; - Vector staged(rangeBytes); - // P5c: under an active transport BufferObject::MappedData on the apply - // thread is Fatal{RoleViolation, "buffer-legacy-arm"} (CONTRACT-P5C §3.8) - - // the pre-capture bytes are the SERVER's staged shadow, and the reconciled - // range goes back as a writeback EVENT instead of a WritebackFromBackend - // poke into client memory. - MG_Pipe::MGPipeHandle splitRes = MG_Pipe::kMGPipeNullHandle; - BufferImpl::GLESBufferResource* splitResource = nullptr; - splitRes = target.handle; - splitResource = BufferImpl::FindBufferResourceForHandle(splitRes); - if (splitResource == nullptr) { - // THE ONLY DISCARD LEFT, and it is the impossible one: this target - // was recorded by BindBufferRange/Base against a handle the applier - // had already minted a twin for, so no twin here means the slot - // table and the capture list disagree about one resource. There is - // no backendId to upload into either, so there is nothing to do but - // name it. - MGLOG_E_ONCE("EndTransformFeedback: a scattered capture target (handle {%u,%u}) has " - "no server twin at all; its capture is discarded", - splitRes.Slot, splitRes.Gen); - continue; - } - // P8-C: made current first - a store a shader wrote here is refreshed. - const Uint8* hostBytes = BufferImpl::SplitHostBytesForCpuRead(*splitResource, splitRes, - "xfb_scatter_pre_capture"); - // A NULL SHADOW IS NOT A LOST CAPTURE. It is the ORPHANED store - // (glBufferData(size, NULL) with no resource_subdata since), which - // M-3 declares legal: every byte the application has not staged is - // UNDEFINED by its own declaration, and `staged` is already - // value-initialised to zero - which is byte for byte what the monolith - // arm scatters over when it reads a fresh MappedData(). Skipping the - // Memcpy is therefore the WHOLE of the difference; skipping the TARGET - // threw the varyings away and left the application's buffer holding its - // pre-draw bytes under GL_NO_ERROR, the silent-loss class this file's - // preamble exists to close. The streaming idiom that hits it - orphan, - // partial glBufferSubData, capture into the rest - is ordinary. - if (hostBytes != nullptr) { - // Managers.cpp's pool-reuse ladder guards its whole-store - // MGL_SERVER_STAGED_REQUIRE with exactly this predicate and for - // exactly this reason: for a store whose content the application - // SUPPLIED a coverage gap is a missing record and zero-filling past - // it is silent data loss (Fatal by name); for an orphaned one the - // gap is its own undefined content and the read is legal. - if (BufferImpl::ResourceContentIsDeclared(splitRes)) { - BufferImpl::RequireStagedCoverage(*splitResource, hostBytes, target.start, - target.end, "xfb_scatter_pre_capture"); - } - Memcpy(staged.data(), hostBytes + target.start, rangeBytes); - } - - for (const auto& varying : program->GetTransformFeedbackVaryings()) { - if (varying.bufferIndex != target.bufferIndex) continue; - for (SizeT v = 0; v < vertices; ++v) { - const SizeT dstOffset = v * stride + varying.offsetBytes; - if (dstOffset + varying.byteSize > rangeBytes) break; - Memcpy(staged.data() + dstOffset, - static_cast(packed) + v * packedStride + varying.packedOffsetBytes, - varying.byteSize); - } - } - - if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) { - // Sliced for the reason the readback path above is sliced: the bytes - // are INLINE in a SEG_EVENT record and the ring refuses a record wider - // than half its capacity outright, so one whole-range post of a - // capture over ~128 KiB was Fatal{EventRingOverflow} and the server - // died. Only the EVENT is cut; the glBufferSubData below stays one - // call over the whole `staged` vector, because the ES store has no - // ring and cutting it would change how many backend calls one - // application call makes. - const SizeT step = XfbWritebackSliceStep(rangeBytes); - for (SizeT off = 0; off < rangeBytes; off += step) { - const SizeT bytes = std::min(step, rangeBytes - off); - MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( - splitRes, target.start + off, - MG_Pipe::MGPBlobRef{reinterpret_cast(staged.data() + off), - static_cast(bytes), - MG_Pipe::kMGHostSpanSegNone, 0}); - } - } else { - MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the " - "scattered capture of buffer {%u,%u} cannot reach the client shadow", - splitRes.Slot, splitRes.Gen); - } - // P8-C: the staged copy takes the composed range too, so this side's CPU - // readers see the capture (before P8 it kept the pre-capture bytes). - BufferImpl::StageServerWrittenRange(*splitResource, splitRes, staged.data(), target.start, - rangeBytes); - // Ops_H_Readback's stamp, for its reason: the GL store now holds the - // capture, and the next draw must not re-upload an older range over it. - splitResource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(splitRes); - // Serial bumped with no backend op (see ReadbackCapturedRanges). - BufferImpl::BumpBufferMutationEpoch(); - if (g_GLESFuncs.glBufferSubData != nullptr) { - BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId); - g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, - static_cast(target.start), - static_cast(rangeBytes), staged.data()); - } - BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); - } - g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget); - xfb.targets.clear(); - } - } // namespace - - Bool AreTransformFeedbacksSupported() { - return g_GLESFuncs.glBeginTransformFeedback != nullptr && - g_GLESFuncs.glEndTransformFeedback != nullptr && - g_GLESFuncs.glTransformFeedbackVaryings != nullptr; - } - - Bool IsCaptureSpanOpen() { - const auto& xfb = CurrentXfb(); - return (xfb.pending || xfb.started) && !xfb.paused; - } - - void BeginTransformFeedback(GLenum primitiveMode) { - if (!AreTransformFeedbacksSupported()) return; - auto& xfb = CurrentXfb(); - xfb.primitiveMode = primitiveMode; - xfb.pending = true; - xfb.started = false; - xfb.paused = false; - xfb.targets.clear(); - } - - // Tail of PrepareForDraw: the program is bound and every buffer the draw needs - // is up to date, so the capture buffers can be bound and the span opened. - void StartPendingTransformFeedback() { - auto& xfb = CurrentXfb(); - // A span that was paused before its first draw must not open here: the draw is - // not captured, and opening the span would also subject it to the capture - // primitive-mode rule the paused draw is exempt from. - if (!xfb.pending || xfb.paused) return; - XfbProgramSource program; - const MG_Pipe::MGPStreamOutputBegin* span = nullptr; - const auto& state = MG_Pipe::MGPipeApplier(); - const auto found = state.StreamOutputSpans.find(state.BoundStreamOutputLifetimeId); - if (found != state.StreamOutputSpans.end()) { - span = &found->second; - const auto* record = PipeShaderCsoRecordForHandle(span->CaptureProgram); - if (record) program.Archive = record->Archive; - } - if (!program) { - // The pending flag is deliberately NOT consumed here. It used to be cleared - // before this check, so a single draw that could not see the capture program - // retired the span permanently: every later draw of the same span found - // pending==false, the driver Begin never happened, and End found started==false - // and skipped the readback - a whole capture lost with no GL error anywhere. - // The frontend only reaches a draw with an active span after glBeginTransformFeedback - // stored a program, so this is a "cannot happen" that must stay recoverable. - MGLOG_E_ONCE("StartPendingTransformFeedback: an active capture span has no capture program; the " - "driver span stays closed and this draw is not captured"); - return; - } - xfb.pending = false; - - // Snapshot what the driver is about to capture into. GL forbids rebinding the - // capture buffers while the span is open, so this stays valid until End, and - // recording it here keeps End independent of the frontend capture state. - const SizeT bufferCount = program->GetTransformFeedbackBufferCount(); - for (SizeT i = 0; i < bufferCount; ++i) { - if (!span || i >= 4) continue; - const auto& range = span->Targets[i]; - if (MG_Pipe::MGPipeHandleIsNull(range.Res) || range.Size == 0) continue; - auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, range.Res); - if (!resource || resource->id == 0) continue; - xfb.targets.push_back({nullptr, resource->id, static_cast(range.Offset), - static_cast(range.Offset + range.Size), i, range.Res}); - BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast(i), - resource->id, static_cast(range.Offset), static_cast(range.Size)); - continue; - } - - - // A layout with holes or several interleaved buffers is not expressible on ES: - // capture gap-free into scratch storage and place the records at End instead. - xfb.scattered = false; - xfb.scatterProgram.reset(); - xfb.scatterArchive.reset(); - xfb.scatterCapacityVertices = 0; - if (program->NeedsScatteredTransformFeedbackCapture()) { - SizeT capacityVertices = ~SizeT(0); - for (const auto& target : xfb.targets) { - // By BUFFER index. Reading the stride at the target's POSITION made a - // capture list beginning with gl_NextBuffer - buffer 0 has stride 0 and - // nothing bound, so target 0 describes buffer 1 - read stride 0, skip every - // target, and leave the capacity at zero. - const SizeT stride = program->GetTransformFeedbackStride(static_cast(target.bufferIndex)); - if (stride == 0) continue; - capacityVertices = std::min(capacityVertices, (target.end - target.start) / stride); - } - if (capacityVertices == ~SizeT(0)) capacityVertices = 0; - if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) { - xfb.scattered = true; - xfb.scatterProgram = program.Frontend; - xfb.scatterArchive = program.Archive; - xfb.scatterCapacityVertices = capacityVertices; - } else { - // NO SPAN RATHER THAN A SPAN THAT WRITES SOMEWHERE ELSE. The ES program for a - // scattered capture is a single-buffer INTERLEAVED one (the gl_NextBuffer / - // gl_SkipComponents entries are consumed at link time and never reach the - // driver), so it writes capture point 0 and nothing else. Point 0 here is - // either unbound or - the dangerous case - still holds whatever an earlier - // capture in this process bound there, because the frontend's own - // glBindBufferBase is state-only and nothing else in the backend touches the - // indexed points. Opening the span would then have the driver capture over an - // application buffer that has nothing to do with this draw, and the frontend - // shadow would never learn of it. - // - // Leaving the span closed reproduces exactly what the old high-water clear - // loop achieved by binding 0 here and letting the driver refuse the Begin - - // the capture records nothing - without issuing a capture-point bind the - // application did not ask for, which is the thing that loses captures whole - // on Mali (see SyncTransformFeedbackBindingPoints). - MGLOG_E_ONCE("StartPendingTransformFeedback: no scratch storage for a scattered capture " - "(packed stride %zu, capacity %zu vertices); leaving the driver span CLOSED so the " - "capture cannot land in a stale binding. Nothing will be captured.", - program->GetTransformFeedbackPackedStride(), capacityVertices); - xfb.targets.clear(); - return; - } - } - - // A capture program with buffers bound must have produced at least one target; - // an empty list means End has nothing to mirror back and the application will - // read its buffer's pre-draw bytes however well the GPU captured. - if (xfb.targets.empty()) { - MGLOG_E_ONCE("StartPendingTransformFeedback: opening a capture span with NO capture targets " - "(program declares %zu capture buffer(s), none of them resolved to a bound backend " - "buffer with a non-empty range); nothing will be read back", - bufferCount); - } - - g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode); - // Unchecked before. Every ES error condition here (already active, a current - // program with no capture set, a capture point the program uses with no buffer) - // ends the same way: the driver records nothing, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN - // reads 0 and the application sees no error at all - MobileGL's own error state is - // separate from the driver's, so a driver rejection here is invisible to it. - if (const GLenum beginError = TakeXfbDriverError(); beginError != GL_NO_ERROR) { - // The mode is printed as a number as well as a name: GL_POINTS is 0, which the - // enum converter spells "GL_FALSE", and a reader chasing a lost capture should - // not have to know that. - MGLOG_E_ONCE("StartPendingTransformFeedback: the ES driver REJECTED " - "glBeginTransformFeedback(%s / 0x%04x) with %s - nothing will be captured. Backend " - "program %u, %zu capture buffer(s), %zu target(s), mode=%s.", - MG_Util::ConvertGLEnumToString(xfb.primitiveMode).c_str(), - static_cast(xfb.primitiveMode), - MG_Util::ConvertGLEnumToString(beginError).c_str(), - PrgramImpl::g_lastUsedBackendProgramId, bufferCount, - xfb.targets.size(), - MG_Util::ConvertGLEnumToString(program->GetTransformFeedbackBufferMode()).c_str()); - } - xfb.started = true; - } - - void EndTransformFeedback() { - auto& xfb = CurrentXfb(); - const Bool wasPending = xfb.pending; - xfb.pending = false; - xfb.paused = false; - if (!xfb.started) { - // A span that never drew is legal and captures nothing by definition; one that - // is STILL pending here drew nothing the backend saw, which for a span the - // application expected data from is the whole bug in one line. - MGLOG_D("EndTransformFeedback: closing a span the driver never opened (pending=%d)", - wasPending ? 1 : 0); - return; - } - xfb.started = false; - g_GLESFuncs.glEndTransformFeedback(); - if (const GLenum endError = TakeXfbDriverError(); endError != GL_NO_ERROR) { - MGLOG_E_ONCE("EndTransformFeedback: the ES driver rejected glEndTransformFeedback with %s - the " - "driver's capture state and MobileGL's have diverged", - MG_Util::ConvertGLEnumToString(endError).c_str()); - } - if (xfb.scattered) { - ScatterCapturedRecords(xfb); - xfb.scattered = false; - xfb.scatterProgram.reset(); - xfb.scatterArchive.reset(); - } else { - ReadbackCapturedRanges(xfb.targets); - } - } - - void PauseTransformFeedback() { - auto& xfb = CurrentXfb(); - xfb.paused = true; - // A span the driver never opened (paused before the first draw) has nothing to - // pause; the flag above is what holds the deferred Begin back until the resume. - if (!xfb.started || g_GLESFuncs.glPauseTransformFeedback == nullptr) return; - g_GLESFuncs.glPauseTransformFeedback(); - } - - void ResumeTransformFeedback() { - auto& xfb = CurrentXfb(); - xfb.paused = false; - if (!xfb.started || g_GLESFuncs.glResumeTransformFeedback == nullptr) return; - g_GLESFuncs.glResumeTransformFeedback(); - } - - void BindTransformFeedback(GLuint name) { - auto& roleState = ActiveXfbState(); - auto& g_currentXfbState = roleState.Cached; - auto& g_xfbObjects = roleState.Objects; - auto& g_currentXfbName = roleState.Current; - const Uint64 key = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId; - (void)name; - roleState.NeedsBind = false; - g_currentXfbState = nullptr; // name changes; operator[] below may also rehash - // The capture buffer bindings are the OBJECT's, not the context's: the bind below - // swaps all of them for whatever the target object holds, which the redundant-bind - // shadow has never seen. - BufferImpl::InvalidateTransformFeedbackBindingShadows(); - if (!AreTransformFeedbackObjectsSupported()) { - // Without driver objects there is only the default span; keep the frontend - // name so the bookkeeping below stays consistent. - g_currentXfbName = - key; - return; - } - auto& xfb = g_xfbObjects[key]; - // Even each virtual context's name-0 object gets its own native object. - if (xfb.esId == 0) { - g_GLESFuncs.glGenTransformFeedbacks(1, &xfb.esId); - } - g_GLESFuncs.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, xfb.esId); - g_currentXfbName = - key; - } - - void DeleteTransformFeedback(GLuint name) { - auto& roleState = ActiveXfbState(); - auto& g_currentXfbState = roleState.Cached; - auto& g_xfbObjects = roleState.Objects; - auto& g_currentXfbName = roleState.Current; - const Uint64 key = MG_Pipe::MGPipeApplier().VerbDeleteStreamOutputLifetimeId; - (void)name; - // A generated but never-bound name has no native object or lifetime. - if (key == 0) return; - const auto it = g_xfbObjects.find(key); - if (it == g_xfbObjects.end()) return; - if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) { - g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId); - } - g_currentXfbState = nullptr; // erase shifts the probe cluster, moving other entries - g_xfbObjects.erase(it); - // The frontend reverts to the default object when the bound one is deleted. - if (g_currentXfbName == - key - ) { - // The following BindStreamOutput names this context's actual - // default lifetime. Never create a native object for key 0. - g_currentXfbName = 0; - roleState.NeedsBind = true; - BufferImpl::InvalidateTransformFeedbackBindingShadows(); - return; - } - } - - // The ES context went away (or is being torn down): the spans, their buffer ids, the - // driver objects and the frontend objects they pinned all belonged to it. - void OnBackendContextDestroyed() { - for (auto& state : g_roleXfbState) state = {}; - g_currentXfbState = nullptr; - g_xfbObjects.clear(); - g_currentXfbName = 0; - g_scatterBufferId = 0; - g_scatterBufferSize = 0; - BufferImpl::InvalidateTransformFeedbackBindingShadows(); - } - } // namespace XfbImpl - - namespace VertexArrayImpl { - // P5e (vi): the record arm's entry, and it takes NO frontend object - CONTRACT-P5E §4.1 - // ("the object parameter is deleted from the transport overload, not defaulted to - // null"). A null twin here is the null BoundVertexElements, i.e. "no VAO bound", which - // PrepareForDraw turns into BindBackendVAOId(0) exactly as a null frontend VAO did; - // logging about it would fire on every draw of a session that legitimately has no - // vertex-elements CSO yet. - void SyncCurrentVAOFromRecords(BackendVertexArrayObject* vaoTwin) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (!vaoTwin) return; - vaoTwin->SyncToBackendFromApplier(); - } - - // THE CLIENT-MEMORY VERTEX ARRAY UPLOAD, and the one read of this family that P5e does - // NOT retire - it MOVES the decision instead, which is what ID-82 / ruling 2 settles. - // - // The bytes of a client-memory array exist only in the application's own memory: the - // emitter publishes Res == kMGPipeNullHandle for such an attribute by design - // (VertexInputEmit.h) and this upload is where the server used to dereference - // attrib.Offset as a raw client pointer (Managers.cpp's - // SyncClientSideAttributesForDrawArrays, kimi audit row 14). Under run-ahead those bytes - // are movable and the read is torn by construction, so the CLIENT refuses the draw by - // name before it is ever emitted (EmitTables.cpp, Fatal{UnmigratedVerb, - // "DrawArrays+CLIENT_ARRAYS"}); staging them as a record tail is P8's. - // - // What is left here is therefore reachable in exactly two situations, and both are safe: - // * monolith / the push-monolith build - one process, one thread, no wire; - // * a LOCKSTEP split session - the client is parked in WaitForApplied for - // this very record, so its memory is stable - // (rule F is scoped to unbarriered records, - // ruling 4). Escalation (ii) of §2.1 pins the - // same thing from the other side: a draw - // carrying kDrawClientArrays is BARRIERED. - // Making it monolith-only instead would silently drop the attribute upload of a - // lockstep split session, which is a wrong picture today - before ra has landed - // anything - so the refusal is the client's to raise, not this arm's to assume. - // - // The frontend VAO is taken ONLY when the applier's own vertex-buffer window says a - // client-sourced attribute exists, which is what retires the per-draw - // GetBoundVertexArray row of the ordinary (VBO-backed) DrawArrays - the strict lane's - // `GetBoundVertexArray@DrawArrays` marker - without touching this path's behaviour. - // `static` on purpose (G1): the pull build must not gain an exported name for a - // function that only re-homes two identical inline blocks the two DrawArrays entry - // points used to carry. The same spelling governs the three below, and for a second - // reason on top of the symbol count: a Debug build drops CXX_VISIBILITY_PRESET to - // `default` (CMakeLists.txt:707-719), so an external-linkage helper here would be a - // DYNAMIC export in exactly the configuration a symbol gate cannot see it in. - // - // ALL FLAVORS, NOT JUST THE SPLIT ARM. These three were inside - // `#if MOBILEGL_BUILD_DISAGGREGATED` for one round and that was a misreading of their - // own gate: the body's first statement returns early when `MG_Config::Transport != - // Monolith`, i.e. they exist FOR the monolith arm - and the monolith arm is what the - // pull/verify/push builds run. Their callers sit in entry points that compile in every - // flavor (the indexed family, both indirect executors), so a guard here breaks the - // pull build outright rather than merely omitting a split-arm path. In those flavors - // the guard's own test folds to a compile-time false (Config.h:566) and the body is - // the transport-free upload it always was. - // - // ---- THE MONOLITH ARM'S CLIENT-MEMORY SNAPSHOT, PER DRAW ----------------------------- - // - // A client-memory attribute is uploaded HERE because its bytes have no store on this side - // to bind. SyncClientSideVertexArraysForDrawArrays below does that for the non-indexed - // family, whose fetched elements are exactly the contiguous (first, count) run. Every - // OTHER draw shape fetches by index and by instance, so its elements have to come from - // MGPipeClientFetchPlan - the same answer the wire arm's owned snapshot is built from - // (MG_Impl/Pipe/OwnedDrawInputs.h, which is why the two arms agree by construction). - // - // WHAT WAS MISSING BEFORE THIS: an indexed draw over a client array uploaded nothing at - // all. The ES context kept whatever the last VBO-backed attribute had left for that - // index - normally the array DISABLED, i.e. every vertex reading the generic current - // value - so the draw painted nothing, with no GL error anywhere to say why. - struct ClientSnapshotSources { - const SharedPtr* ElementBuffer = nullptr; - }; - - // The indirect executors' form: the command's own words ARE the draw's fetch, and a - // command block a shader wrote this frame is exactly what these shapes exist for - so the - // command buffer's shadow is reconciled before it is read, and `commandBytes` (the - // caller's possibly stale copy) is only the fallback for a client-authored block. - static Bool SyncClientSideVertexArraysForIndirectFetch( - const Uint8* commandBytes, SizeT commandOffset, GLsizei stride, GLsizei index, Uint8 indexSize, - const SharedPtr& commandBuffer) { - return true; - } - - // The indexed entry points' form. `indices` is a byte offset into the bound element buffer - // when one is bound and the application's own array otherwise, which is the same - // distinction every one of them already makes when it issues the draw. - static Bool SyncClientSideVertexArraysForIndexedFetch(GLenum type, GLsizei count, const void* indices, - GLsizei instanceCount, GLint baseVertex, - GLuint baseInstance) { - if (count <= 0 || instanceCount <= 0) return true; - // MONOLITH ONLY - see SyncClientSideVertexArraysForFetch; this wrapper's own read of - // the bound VAO is the frontend read that must not happen on an apply thread. - return true; - } - - static void SyncClientSideVertexArraysForDrawArrays(GLint first, GLsizei count) { - if (!BufferImpl::AnyClientSideVertexArrayInRecord()) return; - // P13: client-memory arrays are monolith-only - under a transport the client refuses - // them (Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}) before any record goes out, - // so a record naming one here means the bytes below belong to another process. - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"ClientArrays\"} - a draw carrying a " - "client-memory vertex array reached the backend under a transport; the " - "client's refusal exists to make this unreachable"); - std::abort(); - } - auto* twin = ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements); - if (twin == nullptr) return; - const auto& currentVAO = MG_Pipe::gPipeInputs->GetBoundVertexArray(); - if (!currentVAO) return; - twin->SyncClientSideAttributesForDrawArrays(currentVAO, first, count); - return; - } - - // GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current - // value* (context state set by glVertexAttrib*, default (0,0,0,1)) rather than any buffer. - // MobileGL stores those values in MG_State only, so without this step the ES driver would feed - // the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1). - // SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES - // current value is what the shader will actually read. - void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin, - const SharedPtr& program) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (!program) return; - if (!vaoTwin) return; - - // P5e (vi), CONTRACT-P5E §5.1: the bound-VAO row is retired here. On the record arm - // the twin IS the bound vertex-elements CSO (the caller resolved it from - // st.BoundVertexElements), so "is a VAO bound" is already answered by vaoTwin being - // non-null and GetBoundVertexArray() has nothing left to add. - // - // WHAT IS NOT RETIRED HERE, and it is S4/pg's not vi's: the two program reads below - // (GetActiveAttributeLocationMask, GetAttribType). They are frontend object rows of - // the PROGRAM family and stay until pg carries the attribute mask and types in the - // ShaderCso record. Until then this function is still a frontend reader on the - // apply thread - a BARRIERED one, which is legal (§4.4 / ruling 4) and is why vi - // does its half now rather than waiting: the VAO half is what this package owns and - // leaving it would keep GetBoundVertexArray alive for a reason that is not the - // program's. - const MG_Pipe::MGPipeVertexElementsRecord* elementsRecord = nullptr; - MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle; - Uint64 elementsSerial = 0; - const auto& st = MG_Pipe::MGPipeApplier(); - elementsHandle = st.BoundVertexElements; - if (!MG_Pipe::MGPipeHandleIsNull(elementsHandle) && - elementsHandle.Slot < st.VertexElementsCsos.size()) { - const auto& record = st.VertexElementsCsos[elementsHandle.Slot]; - if (record.Live && record.Gen == elementsHandle.Gen) { - elementsRecord = &record; - elementsSerial = record.ContentSerial; - } - } - // No record is "the configuration was never described": there is nothing to - // decide which locations lack an array from, and guessing would either - // re-issue every current value every draw or silently skip the ones that need - // it. The next create_vertex_elements re-opens the memo by serial. - if (elementsRecord == nullptr) return; - - const SharedPtr noVao; - const auto& vao = noVao; - - const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask(); - if (activeAttribMask == 0) return; - - // Which of the program's attributes lack an enabled array cannot change without - // the VAO's config version moving (Enable/DisableAttribute bump it) or the - // program's active mask changing, so the per-draw Enabled probes reduce to two - // compares. The memo lives on the twin (1:1 with the VAO, no identity key - // needed): a function-static single entry missed on every draw once the app - // cycled section VAOs, re-reading the cold attribute slots each time; here a - // cycle re-hits every VAO's own entry. The rebuild visits only ACTIVE locations. - auto& memo = vaoTwin->GetPendingAttribValueMaskMemo(); - // The re-keyed memo: {elementsHandle, ContentSerial, activeMask}. Every - // Enable/DisableVertexAttribArray moves the frontend configuration version, the - // client re-emits create_vertex_elements on the same handle for it, and the - // applier ++s ContentSerial - so this key opens exactly when the old one did - // and never wraps (Managers.h states the strictly-stronger argument). - if (!memo.valid || !(memo.elementsHandle == elementsHandle) || - memo.elementsSerial != elementsSerial || activeAttribMask != memo.activeMask) { - Uint32 pending = 0; - for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) { - const Uint32 location = static_cast(std::countr_zero(remaining)); - // rec->Attributes[] IS what was last pushed for this configuration; the - // record carries all 32 slots and zeroes the tail - // (MGPipeApplyCreateVertexElements), so a location past AttributeCount - // reads Enabled = 0, which is the same answer the frontend's cold - // attribute slot gave. - if (location >= MG_Pipe::kMGPipeMaxVertexAttribs || - !elementsRecord->Attributes[location].Enabled) { - pending |= (1u << location); - } - } - memo.elementsHandle = elementsHandle; - memo.elementsSerial = elementsSerial; - memo.activeMask = activeAttribMask; - memo.pendingMask = pending; - memo.valid = true; - } - // Plus the enabled arrays the walk had to disable (Managers.h, GetDisabledEnabledAttribMask). - const Uint32 pendingMask = memo.pendingMask | (vaoTwin->GetDisabledEnabledAttribMask() & memo.activeMask); - if (pendingMask == 0) return; - - for (Uint32 remaining = pendingMask; remaining != 0; remaining &= remaining - 1) { - const Uint32 location = static_cast(std::countr_zero(remaining)); - - const auto& currentValue = MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location); - const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(program->GetAttribType(location)); - switch (typeInfo.baseType) { - case MG_State::GLState::VertexAttribBaseType::Float: - g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data()); - break; - case MG_State::GLState::VertexAttribBaseType::Int: - g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data()); - break; - case MG_State::GLState::VertexAttribBaseType::Uint: - g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data()); - break; - case MG_State::GLState::VertexAttribBaseType::Unsupported: - MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its " - "shader input type 0x%x is not supported as a current generic vertex attribute", - program->GetExternalIndex(), location, program->GetAttribType(location)); - break; - } - } - } - - // P5e (pa), CONTRACT-P5E §5.5 / §5.8 (ruling ID-81): THE SAME SYNC, OFF THE RECORD. - // - // The overload above is the MONOLITH GLUE and keeps its text token for token; this one is - // what the handle arm calls, and it answers the note vi left inside it - "the two program - // reads below (GetActiveAttributeLocationMask, GetAttribType) ... stay until pg carries - // the attribute mask and types in the ShaderCso record". pg's archive carries both: - // `attribs` and `attribTypes` are members of LinkArtifacts, the record ADOPTS the whole of - // it at create_shader_state, and neither is one of the three post-link mutable fields the - // bindings tails exist for (glUniformBlockBinding / glUniform1i on a sampler / - // glShaderStorageBlockBinding move those, and nothing moves these without a relink, which - // re-issues the create). So this arm adds NOTHING to the wire and changes no record - it - // reads rows that already arrived. - // - // WHY A SECOND FUNCTION rather than an `#if` inside the body above: §5.8's idiom, and G1 - - // the frontend overload's preprocessed text does not move, so the pull build gains no - // symbol and no byte. The price is that vi's memo arm is COPIED rather than shared; a - // shared tail would have rewritten the pull build's one, which G1 measures at 0/0/0/0. - void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin, MG_Pipe::MGPipeHandle cso) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // THE SAME THREE-PART TEST SyncCurrentProgramByHandle MAKES, from the descriptor, and - // it is this arm's spelling of `if (!program) return;` above rather than a weaker one. - // LinkStatus is a FIELD and not an implication (ID-88): 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, so "a record exists" and "the program linked" are different - // statements. A program that did not link, or whose SPIR-V never arrived, is one the - // same Prepare has just bound program 0 for - there is no shader to feed a current - // generic attribute to, which is what the frontend arm gets from an unlinked - // program's empty `attribs`. - const MG_Pipe::MGPipeShaderCsoRecord* const record = PipeShaderCsoRecordForHandle(cso); - if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { - return; - } - // A NULL ARCHIVE ON THIS ARM IS A NAMED REFUSAL, NEVER A FALL-BACK TO THE FRONTEND - - // the shape of RefuseNullFrontendTextureOffTheHandleArm / MGB_TEXTURE_RECORD_ARM_- - // SELECTED (Managers.cpp, ID-110). This function is reached from ProgramHandleArm() - // only, i.e. Transport != Monolith AND the program subsystem bit, and under a - // transport pg fills Archive at every create_shader_state; so a LIVE record with a - // LINKED descriptor and no archive is a seam defect and not a state a draw may run - // in. Reaching back for GetProgramForDraw() here would answer the two rows out of the - // client's own live LinkArtifacts - the memory Link() replaces in place, i.e. exactly - // the read this package retires - and would do it behind a picture that still looks - // right, which is what the subsystem A/B exists to expose. - if (!record->Archive) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"program-handle-arm\"} - " - "SyncCurrentVertexAttributeValues needs the active-attribute mask and the " - "attribute types of ShaderCso {%u, %u} and that record carries no " - "server-owned archive. The arm is selected by Transport != Monolith AND " - "the program subsystem bit (CONTRACT-P5E §5.8, ID-81), so this record's " - "create_shader_state was applied under a transport and must have adopted " - "one; the frontend ProgramObject is not a fall-back here, it IS the client " - "memory this row retires", - cso.Slot, cso.Gen); - std::abort(); - } - const MG_State::GLState::LinkArtifacts& link = record->Archive->Link; - if (!vaoTwin) return; - - // vi's half, unchanged in meaning and copied rather than shared (see above). The two - // family bits are independent A/Bs, so a program on the handle arm says nothing about - // whether the vertex-input family is - this arm has to carry both of vi's. - const MG_Pipe::MGPipeVertexElementsRecord* elementsRecord = nullptr; - MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle; - Uint64 elementsSerial = 0; - const auto& st = MG_Pipe::MGPipeApplier(); - elementsHandle = st.BoundVertexElements; - if (!MG_Pipe::MGPipeHandleIsNull(elementsHandle) && - elementsHandle.Slot < st.VertexElementsCsos.size()) { - const auto& elements = st.VertexElementsCsos[elementsHandle.Slot]; - if (elements.Live && elements.Gen == elementsHandle.Gen) { - elementsRecord = &elements; - elementsSerial = elements.ContentSerial; - } - } - if (elementsRecord == nullptr) return; - - const SharedPtr noVao; - const auto& vao = noVao; - - // ProgramObject::GetActiveAttributeLocationMask, over the archive's own `attribs`: - // the same 32-location bound and the same "an empty name is not an active attribute" - // rule, computed where the names live instead of through an accessor MG_Backend - // cannot reach without a frontend object. - Uint32 activeAttribMask = 0; - { - const SizeT attribCount = std::min(link.attribs.size(), 32); - for (SizeT index = 0; index < attribCount; ++index) { - if (!link.attribs[index].empty()) activeAttribMask |= (1u << index); - } - } - if (activeAttribMask == 0) return; - - auto& memo = vaoTwin->GetPendingAttribValueMaskMemo(); - if (!memo.valid || !(memo.elementsHandle == elementsHandle) || - memo.elementsSerial != elementsSerial || activeAttribMask != memo.activeMask) { - Uint32 pending = 0; - for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) { - const Uint32 location = static_cast(std::countr_zero(remaining)); - if (location >= MG_Pipe::kMGPipeMaxVertexAttribs || - !elementsRecord->Attributes[location].Enabled) { - pending |= (1u << location); - } - } - memo.elementsHandle = elementsHandle; - memo.elementsSerial = elementsSerial; - memo.activeMask = activeAttribMask; - memo.pendingMask = pending; - memo.valid = true; - } - // Plus the enabled arrays the walk had to disable (Managers.h, GetDisabledEnabledAttribMask). - const Uint32 pendingMask = memo.pendingMask | (vaoTwin->GetDisabledEnabledAttribMask() & memo.activeMask); - if (pendingMask == 0) return; - - for (Uint32 remaining = pendingMask; remaining != 0; remaining &= remaining - 1) { - const Uint32 location = static_cast(std::countr_zero(remaining)); - - // BOUNDS-CHECKED, exactly as ProgramObject::GetAttribType is not: the frontend's - // form indexes attribTypes raw because the mask it walked came off the same - // artefacts instance, and here the two vectors arrived over a wire. A short tail - // is an archive whose halves disagree; 0 classifies as Unsupported and says so - // below rather than reading past the end. - const GLenum attribType = - location < link.attribTypes.size() ? link.attribTypes[location] : 0; - const auto& currentValue = MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location); - const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(attribType); - switch (typeInfo.baseType) { - case MG_State::GLState::VertexAttribBaseType::Float: - g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data()); - break; - case MG_State::GLState::VertexAttribBaseType::Int: - g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data()); - break; - case MG_State::GLState::VertexAttribBaseType::Uint: - g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data()); - break; - case MG_State::GLState::VertexAttribBaseType::Unsupported: - // THE HANDLE, NOT A GL NAME: the server does not know the client's program - // names and must not learn them (ProgramArchiveSource::Identity states the - // same rule for the twin's log lines). - MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: ShaderCso {%u, %u} location=%u has no " - "enabled array and its shader input type 0x%x is not supported as a current " - "generic vertex attribute", - cso.Slot, cso.Gen, location, attribType); - break; - } - } - } - } // namespace VertexArrayImpl - - namespace TextureImpl { - // Identity snapshot of what one texture unit has bound: the object in every binding - // slot plus the unit's sampler object. This - not the context's texture bind - // generation - is what the per-draw texture memos key on: the generation also bumps - // on REDUNDANT re-binds (glBindSampler of the sampler the unit already carries, which - // 26.2 issues around every texture-unit switch), so a generation-keyed memo re-derives - // everything on draws that changed nothing. The snapshot compares the bindings - // themselves, so only a real change can invalidate it. What it deliberately does NOT - // cover, and its users must key on separately: - // * membership/completeness flips with no binding moving - a default texture's image - // appearing or vanishing, any texture shape or sampler parameter change - all bump - // the sampling-resolution generation (BumpShapeVersion / SamplerObject::BumpVersion - // are their only writers and bump it unconditionally); - // * everything unit bindings say nothing about: the touched-unit high-water mark, - // the frontend context identity, the backend ES context generation, and (for the - // resolution memo) the program keys that arbitrate aliased targets. - // P2: the snapshot stops holding weak_ptrs and holds the frontend objects' LIFETIME IDs. - // The weak_ptr was here for one reason - a raw address lies once the allocator recycles - // it - and a lifetime id is a monotone per-class counter that is never handed out twice, - // so it answers the same question with an integer compare and without pinning a control - // block. 0 means "nothing bound", which no live object can collide with (the counters - // start at 1). This is not the twin table's {slot, gen}: a bound texture that has never - // been synced has no twin and therefore no handle, so a handle-keyed snapshot would read - // two never-synced textures as equal. The identity has to exist before the twin does. - // - // Split by the RUNTIME arm, not by the build, for exactly the reason g_fbSlotCache is: - // MOBILEGL_PIPE_PUSH=0 has to reproduce P1's behaviour (ConfigLoader.cpp), and a - // legacy-arm run that debounced on lifetime ids would be running P2's mechanism while - // the A/B attributed the result to P1. The two answers are equivalent - OwnerEquals on - // two empty pointers is true and LifetimeIdOf(nullptr) == 0 == 0; a live-versus-expired - // control block and two distinct lifetime ids both compare unequal - so keeping the - // legacy fields costs that arm nothing but the words, and gives the control back its - // fidelity. A build with no legacy arm compiled carries neither the fields nor the - // branch. - struct UnitBindingsSnapshot { - Array slotObjects{}; - Uint64 samplerObject = 0; - }; - - static Uint64 LifetimeIdOf(const SharedPtr& object) { - return object ? object->GetLifetimeId() : 0; - } - - static Uint64 LifetimeIdOf(const SharedPtr& object) { - return object ? object->GetLifetimeId() : 0; - } - - - static void CaptureUnitBindings(Int maxTouchedUnit, Vector& out) { - out.resize(static_cast(maxTouchedUnit + 1)); - for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { - auto& textureUnit = MG_Pipe::gPipeInputs->GetTextureUnitObject(unit); - auto& snapshot = out[static_cast(unit)]; - const auto& slots = textureUnit.GetAllBindingSlots(); - for (SizeT i = 0; i < slots.size(); ++i) { - snapshot.slotObjects[i] = LifetimeIdOf(slots[i].GetBoundObject()); - } - snapshot.samplerObject = LifetimeIdOf(textureUnit.GetSamplerObject()); - } - } - - static Bool UnitBindingsUnchanged(Int maxTouchedUnit, const Vector& snapshots) { - if (snapshots.size() != static_cast(maxTouchedUnit + 1)) return false; - for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { - auto& textureUnit = MG_Pipe::gPipeInputs->GetTextureUnitObject(unit); - const auto& snapshot = snapshots[static_cast(unit)]; - const auto& slots = textureUnit.GetAllBindingSlots(); - for (SizeT i = 0; i < slots.size(); ++i) { - if (snapshot.slotObjects[i] != LifetimeIdOf(slots[i].GetBoundObject())) return false; - } - if (snapshot.samplerObject != LifetimeIdOf(textureUnit.GetSamplerObject())) return false; - } - return true; - } - - static Vector g_observedUnitBindings; - static Uint64 g_observedUnitBindingsContextId = 0; - static Uint64 g_observedUnitBindingsGeneration = 0; - static Int g_observedUnitBindingsMaxUnit = -1; - static Uint64 g_unitBindingsEpoch = 0; - - // Epoch of the touched units' bindings: moves exactly when WHAT is bound changes, - // never on a redundant re-bind. Memos snapshot the returned value instead of holding - // their own UnitBindingsSnapshot, so the owner-compare walk runs at most once per - // draw no matter how many memos key on it. The observation triple gates the walk: - // while (context id, bind generation, high-water mark) are unchanged nothing can - // have moved, because every path that changes a binding bumps the generation. A - // given epoch value names one observed (context, high-water mark, bindings) state - - // a context switch, a high-water-mark move or a bindings change each recapture and - // bump - so epoch equality alone proves the bindings a consumer resolved against - // are the bindings on the units now. - // P4a e2 (D-G1). THE UNIT-BINDINGS EPOCH DERIVATION, REPLACED BY THE RECORDS' SERIALS. - // - // CurrentUnitBindingsEpoch below exists to answer one question - "did WHAT the touched - // units hold change?" - without re-deriving on a redundant re-bind, and it pays for the - // answer with a per-unit snapshot walk over every binding slot plus the unit's sampler - // object, run whenever the frontend's texture bind generation moves (26.2 moves it - // around every texture-unit switch). The applier already holds the answer: - // set_sampler_views and bind_sampler_states go out through a ContentHash suppressor, - // so their serials move IF AND ONLY IF the resolved per-unit view set or the per-unit - // sampler set really moved. Two Uint64 reads replace the walk, and the suppressor is - // what makes that sound - the hash is over every entry of each tail, so suppression - // cannot hide a change. - // - // BOTH serials, because the snapshot covered both halves: slotObjects is the view set - // and samplerObject is the sampler-state set. - // - // DECLINES until both windows have arrived at least once, and the COUNT is what says - // so rather than the serial: MGPipeApplierReset advances every P4a working-state - // serial whether or not anything was ever emitted, so a serial is never "no set has - // been received". A declined answer leaves the caller on the snapshot walk, which is - // the honest thing for a tree whose client half has not landed. - // - // The two arms' epoch values share one number space during the transition (a process - // can run the walk before the first emission and the serials after it). That is safe - // by construction: consumers only ever ask "is this the same value I stamped", the - // walk's counter is small and dense and this one is a 64-bit mix, so the two cannot - // meet except with vanishing probability - and a collision costs a spare rebuild. - static Bool UnitBindingsEpochFromRecords(Uint64& out, Int maxTouchedUnit) { - const auto& st = MG_Pipe::MGPipeApplier(); - // P4a decline-site T2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, then - // the decline, and MEASURED before it was believed. Bits 10 and 11 are on (the - // caller asked SamplerSubsystemEnabled(), which refuses one without the other) - // and package C has landed, so a draw that reaches here with NEITHER window ever - // received looks like a seam defect - but the first real-path run said otherwise - // for one whole class of draw and the condition is narrowed by that measurement, - // not by argument. - // - // maxTouchedUnit < 0 IS "THIS DRAW TOUCHES NO TEXTURE UNIT AT ALL", and for such - // a draw an empty sampler-view and sampler-state window is the correct and only - // possible emission: there is nothing to describe. 161 of the integration lane's - // 477 processes are that shape, every one of them at units 0..-1, and a line that - // fires on a third of a green lane is a line nobody reads. So the refusal is - // scoped to a draw that actually touches a unit, where an empty window really - // does mean no set_sampler_views and no bind_sampler_states ever arrived. - // - // It declines to the snapshot walk rather than refusing the draw, because the - // walk answers the same question correctly and a wrong picture is not the failure - // mode here - a silent permanent fallback is, and that is what the line is for. - // The caller's MINOR-4 gate tick still counts EVERY decline, loud or not. - if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) { - // P5e (tx2), CONTRACT-P5E §5.3: UNDER A LIVE WIRE THE DECLINE IS A REFUSAL. - // - // The fall-back below is the pre-handle snapshot walk over GetTextureUnitObject, - // and on a run-ahead server that walk reads client memory for a record that has - // already been answered - there is no wait left in which the answer could be - // right. Ruling 19 makes the missing window unrepresentable at the sink; this is - // the same statement one level down, for the backend that would have papered over - // it. Under monolith the fall-back is correct and stays. - if (maxTouchedUnit >= 0) { - // The windows are the PEER's records, so in a served session this ends THAT - // session (the in-process display server serves every client from one - // process; an abort here took the compositor down with the offender). The - // answer while the latch drains is "no unit is bound": the walk the decline - // would run reads client memory, and the session stops at its next record. - // Unarmed, SessionLatch is the old abort (P13 W5: through the record latch seam, - // which is SessionLatch wherever MG_Remote is linked). - if (!MG_Pipe::MGPipeRecordLatch( - MG_Pipe::MGFatalFamily::ProtocolCorruption, - "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - a draw " - "touches units 0..%d and neither a sampler-view nor a sampler-state " - "window has ever been applied while the sampler subsystem bit is set; " - "the pre-handle unit-bindings snapshot walk reads client memory and is " - "refused under an active transport (CONTRACT-P5E.md §5.3)", - static_cast(maxTouchedUnit))) { - out = 0; - return true; - } - } - if (maxTouchedUnit >= 0) { - MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw " - "touches units 0..%d and neither a sampler-view nor a sampler-state " - "window has ever been applied while the sampler subsystem bit is " - "set; running the pre-handle unit-bindings snapshot walk.", - static_cast(maxTouchedUnit)); - } - return false; - } - // Local, because the tracker's MGPipeMixShutter lives in MG_Impl and no backend - // translation unit may reach for it. - const Uint64 mixed = - (st.SamplerViewsSerial * 0x9E3779B97F4A7C15ull) ^ (st.SamplerStatesSerial + 0x165667B19E3779F9ull); - out = mixed; - return true; - } - - static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) { - // P4a decline-site T1: M - the mask says this family is not switched on, and it - // IS D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification - // round: the mask's word is the one thing a decline may be quiet about. - Uint64 epochFromRecords = 0; - if (UnitBindingsEpochFromRecords(epochFromRecords, maxTouchedUnit)) { - // No accessor reads and no walk on this arm; the two PipeStats accessor - // ticks below are not counted because no accessor was called. - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, - /*hit=*/true); - } - return epochFromRecords; - } - // MINOR-4. THE RECORD ARM'S DECLINE IS TICKED AS A MISS, and it has to be: - // the walk arm below ticks the SAME gate with its own hit/miss, so without - // this a fully declining tree reports the walk memo's high hit rate on - // Gate::EsprytUnitBindingsEpoch and the verification round's "hit rate goes to - // 100%" cannot tell "the record arm engaged" from "the walk arm's memo hit". - // With it, every declining call contributes at least one miss, so 100% means - // exactly one thing. - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, - /*hit=*/false); - } - const Uint64 contextId = MG_Pipe::gPipeInputs->GetTextureContextId(); - const Uint64 bindGeneration = MG_Pipe::gPipeInputs->GetTextureBindGeneration(); - if (MG_Util::PipeStats::Enabled()) { - // Two accessor calls whichever way the shutter goes; only the unit WALK is - // gated, and that walk reads no GLContext accessor of its own. - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2); - } - if (g_observedUnitBindingsContextId == contextId && g_observedUnitBindingsMaxUnit == maxTouchedUnit && - g_observedUnitBindingsGeneration == bindGeneration) { - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/true); - } - return g_unitBindingsEpoch; - } - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/false); - } - if (g_observedUnitBindingsContextId != contextId || g_observedUnitBindingsMaxUnit != maxTouchedUnit || - !UnitBindingsUnchanged(maxTouchedUnit, g_observedUnitBindings)) { - CaptureUnitBindings(maxTouchedUnit, g_observedUnitBindings); - ++g_unitBindingsEpoch; - } - g_observedUnitBindingsContextId = contextId; - g_observedUnitBindingsMaxUnit = maxTouchedUnit; - g_observedUnitBindingsGeneration = bindGeneration; - return g_unitBindingsEpoch; - } - - // Work list behind SyncNeccessaryTextures' per-draw unit walk. WHICH textures the - // touched units hold is a pure function of the unit bindings, so the GLContext identity - // (a never-reused id, not the heap address a recreated context can land on again), the - // unit-bindings snapshot and the touched-unit high-water mark are a complete key - - // WHAT each entry then has to do is still decided per draw by the version compares - // inside the sync calls, which is why texture content, shape and parameter changes need - // no key here. - // - // Entries borrow, they never own. `slot` points at the binding slot's shared_ptr, whose - // address is fixed for the context's lifetime (TextureState holds the unit array by - // value) and whose VALUE cannot change without bumping the bind generation. `backend` is - // the registry's object for the texture in that slot; the registry only erases an entry - // once the frontend texture has expired, and a frontend texture cannot expire while a - // slot the key covers still holds a reference to it. Holding either side by shared_ptr - // instead would keep dead frontend textures alive and defeat the registry's - // weak-reference GC. - // - // `texture` records WHICH frontend object `backend` was paired with when the entry was - // built, and PairingsIntact re-checks it before any replay. The keys above are the - // primary guard, but they are all derived state: a slot swap that never reaches the - // bind generation (the DSA by-name emulation used to swap a slot silently) would leave - // every key matching while the borrowed slot pointed at a different texture, and the - // replay would then drive texture A's backend twin from texture B's frontend state - - // re-specifying A's backend storage with B's shape and destroying A's contents. A raw - // pointer compare per entry is far cheaper than the walk it guards, and a stale pairing - // costs only a list rebuild, so this stays as the structural net under the keys. - struct UnitTextureSyncEntry { - const SharedPtr* slot = nullptr; - MG_State::GLState::ITextureObject* texture = nullptr; - BackendTextureObject* backend = nullptr; - }; - // P5e (tx2), CONTRACT-P5E §5.2: THE UNIT WORK LIST WITH NO BORROWED SLOT IN IT. - // - // The pre-P5e entry borrows a pointer INTO a frontend binding slot and remembers which - // frontend object it was paired with, and PairingsIntact re-checks that pairing before - // every replay. Both exist because the list's key is derived state and a silent slot swap - // could leave a stale pairing driving texture A's twin from texture B's frontend state. - // - // A HANDLE HAS NO SUCH HAZARD. `Res` is {slot, gen}: a recycled slot arrives at a higher - // Gen, which the twin table's forward-Gen rule answers by RESETTING the twin, and a - // backward Gen is refused outright - so there is nothing for a pairing check to catch and - // PairingsIntact is not carried on this list at all. The key is the applier's own - // (ContextSerial, SamplerViewsSerial, window) plus the backend context generation, and - // SamplerViewsSerial moves IFF the resolved per-unit view set moved, which the emitter's - // content-hash suppressor is what makes sound. - // - // Its OWN list and its own key rather than a re-shape of UnitTextureSyncEntry, because - // that struct is also the draw-FBO and read-FBO attachment lists' (fb's), and one name - // carrying two shapes across two packages' worktrees is the merge trap the contract - // package exists to avoid. - struct UnitTextureSyncByHandleEntry { - MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; - BackendTextureObject* backend = nullptr; - }; - static Vector g_unitTextureSyncListByHandle; - static Bool g_unitTextureSyncListByHandleValid = false; - static Uint64 g_unitTextureSyncListByHandleContextSerial = 0; - static Uint64 g_unitTextureSyncListByHandleViewsSerial = 0; - static Uint32 g_unitTextureSyncListByHandleStart = 0; - static Uint32 g_unitTextureSyncListByHandleCount = 0; - static Uint g_unitTextureSyncListByHandleContextGeneration = 0; - static Vector g_unitTextureSyncList; - static Bool g_unitTextureSyncListValid = false; - static Uint64 g_unitTextureSyncListContextId = 0; - static Int g_unitTextureSyncListMaxUnit = -1; - static Uint g_unitTextureSyncListContextGeneration = 0; - static Uint64 g_unitTextureSyncListEpoch = 0; - static Uint64 g_unitTextureSyncListSamplingGeneration = 0; - - // P5e (fb, CONTRACT-P5E.md §5.4): THE TWO ATTACHMENT LISTS GET AN ENTRY OF THEIR OWN, - // and it borrows nothing. - // - // The unit list's entry borrows the binding slot's SharedPtr and re-checks the pairing - // before every replay, because its key (the unit-bindings epoch) is derived state that - // a silent slot swap could get past. An attachment list under the record arm has - // neither problem and cannot afford the borrow: the key IS the record - {Fbo, - // ContentHash} - and every attachment edit moves the hash by construction, so the - // pairing has nothing left to catch; and the borrowed pointer was into a frontend - // FramebufferObject, which is exactly the thing this package stops holding. So an entry - // is the texture HANDLE the record named plus the twin it resolved to, PairingsIntact - // becomes `entry.Res == surface.Res` folded into the rebuild, and the list owns no - // reference to anything the client can free. - // - // The twin pointer is still a BORROW, on the unit list's own terms: the registry keeps - // a twin alive until its object dies, an attached texture cannot die while the record - // that names it stands, and the very next thing done with the entry is a by-handle sync - // that would re-resolve it anyway. - struct FboAttachmentSyncEntry { - MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; - BackendTextureObject* backend = nullptr; - }; - - // Sibling memo for the draw FBO's texture attachments (see the use site in - // SyncNeccessaryTextures for the key derivation and the borrow rules, which are the - // unit list's). A null FBO pointer means "not stamped". - static Vector g_fboTextureSyncList; - static MG_State::GLState::FramebufferObject* g_fboTextureSyncListFbo = nullptr; - static Uint16 g_fboTextureSyncListSlotVersion = 0; - static Uint16 g_fboTextureSyncListObjectVersion = 0; - static Uint64 g_fboTextureSyncListContextId = 0; - static Uint g_fboTextureSyncListContextGeneration = 0; - // P4a e2. The record-keyed half of the same memo. The draw framebuffer record's - // ContentHash replaces the two VERSION halves of the key - the hash is taken over - // every field the record carries, so an attachment edit always moves it, and Fbo is - // inside it, so a recycled framebuffer handle cannot be suppressed against its - // predecessor. THE IDENTITY HALF IS STILL A POINTER COMPARE and deliberately stays - // one: this list borrows the attachment's own SharedPtr slots, so what it needs to - // know is that the frontend object it borrowed from is the one bound now, and that - // question has no handle in it until package D re-keys the twin tables. Held in its - // own fields with its own flag rather than reusing the versions', because a process - // runs the frontend key before the client's first emission and the record key after - // it, and one field carrying two key shapes is how a stale half gets compared against - // a live one. - static Uint64 g_fboTextureSyncListContentHash = 0; - static Bool g_fboTextureSyncListRecordKeyed = false; - // P5e (fb): the record-keyed list itself, held apart from the pre-handle one for the - // reason the two key shapes are held apart - one field carrying two shapes is how a - // stale half gets compared against a live one. Its key is - // {Fbo, ContentHash, ContextSerial, g_backendContextGeneration}: the applier's - // ContextSerial replaces GetTextureContextId (a frontend read the record arm may not - // take), and the two version halves are gone because the hash covers them. - static Vector g_fboAttachmentSyncList; - static MG_Pipe::MGPipeHandle g_fboAttachmentSyncListFbo = MG_Pipe::kMGPipeNullHandle; - static Uint64 g_fboAttachmentSyncListContentHash = 0; - static Uint64 g_fboAttachmentSyncListContextSerial = 0; - static Uint g_fboAttachmentSyncListContextGeneration = 0; - static Bool g_fboAttachmentSyncListValid = false; - - // The read framebuffer's, same shape and same reasons. Its own list because it is - // keyed on a different framebuffer and deduped against the draw one. - static Vector g_readFboAttachmentSyncList; - static MG_Pipe::MGPipeHandle g_readFboAttachmentSyncListFbo = MG_Pipe::kMGPipeNullHandle; - static Uint64 g_readFboAttachmentSyncListContentHash = 0; - static Uint64 g_readFboAttachmentSyncListContextSerial = 0; - static Uint g_readFboAttachmentSyncListContextGeneration = 0; - static Bool g_readFboAttachmentSyncListValid = false; - - // Rebuild-or-replay, shared by both lists. The record's texture surfaces ARE the - // membership (§5.4); a renderbuffer-only framebuffer - the common Minecraft frame - - // reduces to the key compare and an empty loop. - // - // ONE CALL PER ENTRY, and it is tx2's by-handle seam rather than the three the - // pre-handle arm makes: SyncTextureToBackendByHandle is the by-handle twin of - // SyncTextureObjectToBackend, which is what the REBUILD has always called here, so the - // params / builtin-sampler / mipmap work and its own clean gate are inside it. Calling - // the three separately would need by-handle forms of the first two that nothing - // declares, and would put this file's copy of the clean gate beside tx2's. - static void SyncRecordAttachmentTextures(const MG_Pipe::MGPFramebufferState& record, - Vector& list, Bool listValid) { - if (listValid) { - for (const auto& entry : list) { - // P14: the unit list's aggregate clean gate first (SyncNeccessaryTextures): an - // attachment whose storage, parameters and sampler are already the driver's - // costs two record reads instead of three nested syncs and their SharedPtr - // traffic. Same borrow rule as there - the list's key proves the twin alive. - const auto* record = PipeTextureRecordForHandle(entry.Res); - if (record != nullptr && entry.backend != nullptr && - entry.backend->IsDrawSyncCleanByRecord(entry.Res, *record)) - continue; - TextureImpl::SyncTextureToBackendByHandle(entry.Res, /*imageBindableStorageRequired=*/false); - } - return; - } - list.clear(); - const auto add = [&](const MG_Pipe::MGPSurface& surface) { - if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; - if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return; - auto& twin = TextureImpl::SyncTextureToBackendByHandle(surface.Res, - /*imageBindableStorageRequired=*/false); - list.push_back({surface.Res, twin.get()}); - }; - for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) add(record.Color[i]); - add(record.Depth); - add(record.Stencil); - } - - // The frontend texture-state keys the per-draw texture stages - // (SyncNeccessaryTextures, then BindCurrentTextures) both consume. Captured - // ONCE per draw/dispatch preparation and passed to both: nothing between the - // two stages can move any of these - only frontend GL entry points mutate - // them, and none run inside a backend preparation (the FBO/program syncs in - // between only read frontend state). The no-arg wrappers keep capturing at - // the call for every non-draw call site (Clear, readbacks). - struct DrawTextureSyncKeys { - Uint64 contextId = 0; - Uint64 samplingGeneration = 0; - Uint64 unitBindingsEpoch = 0; - Int maxTouchedUnit = -1; - }; - - DrawTextureSyncKeys CaptureDrawTextureSyncKeys() { - DrawTextureSyncKeys keys; - keys.contextId = MG_Pipe::gPipeInputs->GetTextureContextId(); - // Units past the frontend's high-water mark have provably-empty slots. - // - // P4a e2 DELIBERATELY DOES NOT take this off the sampler-view window's Count, and - // the reason is asymmetric risk. D-G2 makes Count the client's spelling of exactly - // this number, so the substitution looks free - but the two failure directions are - // not alike: a window that is larger than the frontend's mark costs a walk over - // provably-empty units, while one that is SMALLER silently drops the sync of a - // texture a draw is about to sample, which no gate on this tree can see because - // this tree emits no windows at all. The design assigns the high-water mark to the - // CLIENT as the count argument and says nothing about re-deriving it server-side. - // It is one accessor read per draw; the integrator can move it in one line once a - // tree exists where the two can be compared. - keys.maxTouchedUnit = MG_Pipe::gPipeInputs->GetMaxTouchedTextureUnit(); - keys.samplingGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); - keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit); - if (MG_Util::PipeStats::Enabled()) { - // The three reads above; CurrentUnitBindingsEpoch counts its own two when it - // takes the walk arm and none when the records answered. - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); - } - return keys; - } - - // P4a e2 (D-E3). The READ framebuffer's texture attachments, and why this exists. - // - // A texture that is an attachment of the READ framebuffer ONLY reaches - // SyncAttachmentObject, which calls SyncMipmapsToBackend AND NOTHING ELSE - so its - // swizzle, its GL_DEPTH_STENCIL_TEXTURE_MODE, its LOD clamp and its border colour - // never reach the driver. The list that pushes texture PARAMETERS for an attachment is - // the one below, and it reads the DRAW binding slot alone. That is the gap - // ARCHITECTURE.md's D10 names, and the roadmap makes closing it a mandatory, - // red-before deliverable of this phase: set_texture_params is addressed by RESOURCE - // and is independent of any binding, so a texture's parameters exist the moment they - // move and an attachment - draw or read - gets them. - // - // DELIBERATELY NOT GATED ON THE FRAMEBUFFER SUBSYSTEM BIT. It is a deliverable of the - // phase rather than an arm of it, so both arms of the object A/B carry it and the A/B - // keeps measuring the arm rather than the arm plus a behaviour change. It is inside - // MOBILEGL_PIPE_PUSH, so the pull build is untouched. - // - // Its own list and its own key rather than an extension of the draw list's, because - // the draw list is keyed on the draw binding and its pull-build copy must stay - // textually what it was. - static Vector g_readFboTextureSyncList; - static MG_State::GLState::FramebufferObject* g_readFboTextureSyncListFbo = nullptr; - static Uint16 g_readFboTextureSyncListSlotVersion = 0; - static Uint16 g_readFboTextureSyncListObjectVersion = 0; - static Uint64 g_readFboTextureSyncListContextId = 0; - static Uint g_readFboTextureSyncListContextGeneration = 0; - static Uint64 g_readFboTextureSyncListContentHash = 0; - static Bool g_readFboTextureSyncListRecordKeyed = false; - - // P5e (fb, CONTRACT-P5E.md §5.4): THE RECORD ARM OF BOTH ATTACHMENT LISTS, and it is - // the package's core - this is the one unconditional per-draw live read the framebuffer - // family had, taken on every draw whether the memo hit or missed. - // - // Three frontend reads go in one move: the binding slot (twice), the FramebufferObject - // the entries borrowed their attachment slots from, and GetTextureContextId. The key is - // {Fbo, ContentHash, ContextSerial, g_backendContextGeneration} - the record's own hash - // in place of the slot/object version pair (it covers every surface, and Fbo is inside - // it, so a recycled framebuffer cannot be suppressed against its predecessor), the - // APPLIER's ContextSerial in place of the frontend context id, and the backend context - // generation unchanged because it answers a question about driver names that no - // client-side value can answer. - // - // The two lists are still two, and the dedupe is still "one object on both bindings" - - // asked of the two BOUND HANDLES rather than of two pointers, which is the same - // question with a spelling a recycled slot cannot fool. - static void SyncFramebufferAttachmentTexturesByRecord() { - const auto& st = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle drawHandle = - st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)]; - const MG_Pipe::MGPipeHandle readHandle = - st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)]; - const MG_Pipe::MGPFramebufferState* const drawRecord = - BoundFramebufferRecord(FramebufferTarget::Draw); - const MG_Pipe::MGPFramebufferState* const readRecord = - BoundFramebufferRecord(FramebufferTarget::Read); - const Uint64 contextSerial = st.ContextSerial; - - if (drawRecord == nullptr) { - // Not a decline this function can refuse on: SyncCurrentFBO runs before every - // caller of this one and is where the missing record is named and refused - // (RefuseFramebufferBindingSlotRead). Here it only means "nothing to walk". - g_fboAttachmentSyncListValid = false; - g_fboAttachmentSyncList.clear(); - } else { - const Bool listValid = g_fboAttachmentSyncListValid && - g_fboAttachmentSyncListFbo == drawRecord->Fbo && - g_fboAttachmentSyncListContentHash == drawRecord->ContentHash && - g_fboAttachmentSyncListContextSerial == contextSerial && - g_fboAttachmentSyncListContextGeneration == g_backendContextGeneration; - SyncRecordAttachmentTextures(*drawRecord, g_fboAttachmentSyncList, listValid); - g_fboAttachmentSyncListFbo = drawRecord->Fbo; - g_fboAttachmentSyncListContentHash = drawRecord->ContentHash; - g_fboAttachmentSyncListContextSerial = contextSerial; - g_fboAttachmentSyncListContextGeneration = g_backendContextGeneration; - g_fboAttachmentSyncListValid = true; - } - - if (readRecord == nullptr || readHandle == drawHandle) { - g_readFboAttachmentSyncListValid = false; - g_readFboAttachmentSyncList.clear(); - return; - } - const Bool readListValid = g_readFboAttachmentSyncListValid && - g_readFboAttachmentSyncListFbo == readRecord->Fbo && - g_readFboAttachmentSyncListContentHash == readRecord->ContentHash && - g_readFboAttachmentSyncListContextSerial == contextSerial && - g_readFboAttachmentSyncListContextGeneration == g_backendContextGeneration; - SyncRecordAttachmentTextures(*readRecord, g_readFboAttachmentSyncList, readListValid); - g_readFboAttachmentSyncListFbo = readRecord->Fbo; - g_readFboAttachmentSyncListContentHash = readRecord->ContentHash; - g_readFboAttachmentSyncListContextSerial = contextSerial; - g_readFboAttachmentSyncListContextGeneration = g_backendContextGeneration; - g_readFboAttachmentSyncListValid = true; - } - - void SyncNeccessaryTextures(const DrawTextureSyncKeys& keys) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - // All textures we need are: - // 1. textures bound to texture units (TODO: only sync ones that are used in current program) - // 2. textures used in current FBO - // 3. textures bound to image units (TODO) - - const Int maxTouchedUnit = keys.maxTouchedUnit; - const Uint64 samplingGeneration = keys.samplingGeneration; - const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch; - // P5e (tx2), CONTRACT-P5E §5.2. THE UNIT HALF, BY HANDLE. - // - // set_sampler_views is the RESOLVED answer to the question this walk asks - which - // texture does each touched unit sample - and the client has already applied every - // drop the walk below performs by hand: an image-less default texture and a texture - // that samples as incomplete are both null in the record (SamplerEmit.h's two drops), - // and the alias contest between two real textures on one native target was arbitrated - // by the program's own sampler types. So the window IS the work list. - // - // NAMED BEHAVIOUR DELTA (P5e-5, contract §5.2): this syncs the PROGRAM-RESOLVED - // texture per unit, where the frontend walk syncs every slot of every touched unit. - // Coverage is unchanged - views UNION image units UNION the FBO attachment lists - // UNION the waited texture ops still cover every texture anything reads - so it - // narrows WORK and not what reaches the driver. - const auto& st = MG_Pipe::MGPipeApplier(); - const Bool listValid = g_unitTextureSyncListByHandleValid && - g_unitTextureSyncListByHandleContextSerial == st.ContextSerial && - g_unitTextureSyncListByHandleViewsSerial == st.SamplerViewsSerial && - g_unitTextureSyncListByHandleStart == st.SamplerViewStart && - g_unitTextureSyncListByHandleCount == st.SamplerViewCount && - g_unitTextureSyncListByHandleContextGeneration == g_backendContextGeneration; - if (listValid) { - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true); - } - for (const auto& entry : g_unitTextureSyncListByHandle) { - const auto* record = PipeTextureRecordForHandle(entry.Res); - if (record == nullptr) continue; - // The aggregate gate, from the twin's own serials against the record's - - // no frontend version, no context id, no sampling generation. - if (entry.backend->IsDrawSyncCleanByRecord(entry.Res, *record)) continue; - SyncTextureToBackendByHandle(entry.Res); - } - } else { - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false); - } - g_unitTextureSyncListByHandleValid = false; - g_unitTextureSyncListByHandle.clear(); - const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount; - for (Uint32 index = st.SamplerViewStart; - index < windowEnd && index < st.BoundSamplerViews.size(); ++index) { - const auto& view = st.BoundSamplerViews[index]; - if (MG_Pipe::MGPipeHandleIsNull(view.Texture)) continue; - auto& twin = SyncTextureToBackendByHandle(view.Texture); - if (!twin) continue; - g_unitTextureSyncListByHandle.push_back({view.Texture, twin.get()}); - } - g_unitTextureSyncListByHandleContextSerial = st.ContextSerial; - g_unitTextureSyncListByHandleViewsSerial = st.SamplerViewsSerial; - g_unitTextureSyncListByHandleStart = st.SamplerViewStart; - g_unitTextureSyncListByHandleCount = st.SamplerViewCount; - g_unitTextureSyncListByHandleContextGeneration = g_backendContextGeneration; - g_unitTextureSyncListByHandleValid = true; - } - // The pre-handle list must not be replayed after this arm ran: its entries borrow - // frontend slots this arm never refreshed. - g_unitTextureSyncListValid = false; - - // Texture attachments of the draw FBO, memoised like the unit list above: WHICH - // textures hang off the FBO only changes with a rebind (slot version), an - // attachment/draw-buffer edit (object version - the documented invariant - // SyncCurrentFBO's memo already leans on), a different FBO landing on a recycled - // heap address (pointer + slot version together, the StampSyncedFBO trio), another - // frontend context (context id) or a rebuilt ES context (backend generation). WHAT - // each texture then needs is still decided per draw by the version gates inside the - // three sync calls. Entry lifetime mirrors the unit list: the attachment holds the - // texture's SharedPtr at a stable address while the FBO is unchanged, and the - // registry keeps a backend object alive until its frontend texture expires, which an - // attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame - - // reduces to the key compare and an empty loop. - // P5e (fb): under a transport with the framebuffer bit set, BOTH attachment lists - // are answered from the two records and neither binding slot is touched. The - // pre-handle pair below stays compiled and stays the monolith path (ruling 1). - SyncFramebufferAttachmentTexturesByRecord(); - return; - } - - void SyncNeccessaryTextures() { SyncNeccessaryTextures(CaptureDrawTextureSyncKeys()); } - - // Whether glBindImageTexture's `layered` means anything for this target - asked of the - // target the DRIVER will see, not the one the application named. A GL_TEXTURE_1D_ARRAY - // is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking - // the state target instead answered "no" for it and pinned every 1D-array image binding - // to layer 0, whatever the application passed. - // - // `layer` travels with the answer, because GL 4.6 core 8.26 (and ES 3.2 8.22, word for - // word) makes them one rule: "If the texture identified by texture does not have - // multiple layers or faces, the entire texture level is bound, regardless of the values - // of layered and layer." REGARDLESS means ignored - not clamped, and not an error - so - // the driver must not be handed a layer index the texture has no room for. Adreno takes - // such a request literally and leaves the image unit reading zero, which is what failed - // KHR-GL42.bind_image_texture.single_layer's layer:1 rows on GL_TEXTURE_2D and on the - // GL_TEXTURE_1D that is stored as one. Normalizing here and not in the frontend shadow - // is deliberate: GL_IMAGE_BINDING_LAYER must keep echoing what the application passed. - static Bool SupportsLayeredImageBinding(TextureTarget target) { - const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target); - return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap || - backendTarget == TextureTarget::Texture2DArray || - backendTarget == TextureTarget::TextureCubeMapArray || - backendTarget == TextureTarget::Texture2DMultisampleArray; - } - - // Which image units currently hold a WRITABLE buffer texture, and how many. Kept here - // rather than recomputed per draw because the frontend tracks 192 image units and - // almost every program uses none of them: the draw path pays one integer test. - // - // Maintained by SyncImageTextureBinding, which is the single funnel for an image-unit - // change on this backend - glBindImageTextures is a frontend loop over - // glBindImageTexture, and the whole-sweep SyncImageTextureBindings goes through it too. - // Anything that clears a binding WITHOUT coming through here (a deleted texture, a - // recreated context) can only leave a bit set for a unit that no longer has one; the - // sweep re-reads the binding, finds nothing to mark, and CLEARS the bit on its way past. - // So the error is self-healing, and in the direction that costs one wasted look rather - // than one missed write. - static Array - g_writableImageBufferUnits{}; - static Uint g_writableImageBufferUnitCount = 0; - - static void TrackWritableImageBufferUnit(Uint unit, Bool writableBufferTexture) { - Bool& tracked = g_writableImageBufferUnits[unit]; - if (tracked == writableBufferTexture) return; - tracked = writableBufferTexture; - // Written as two guarded steps rather than one signed add: the count is unsigned, - // and a decrement that ever ran one time too many would not saturate at zero, it - // would wrap to four billion and defeat the early-out for the rest of the process. - if (writableBufferTexture) { - ++g_writableImageBufferUnitCount; - } else if (g_writableImageBufferUnitCount > 0) { - --g_writableImageBufferUnitCount; - } - } - - // Highest image unit that has ever been given a texture, plus one. Maintained by the - // single funnel below, so it is a sound "no draw in this context can be reading an image" - // test: nothing reaches an image unit without going through SyncImageTextureBinding. - // Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the - // draw-path staleness check below at one integer test. - static Uint g_imageUnitHighWaterMark = 0; - - // P5e (fb): the eager glBindImageTexture funnel's HANDLE ARM, and it binds nothing. - // - // `bind_shader_image` is an unbarriered row (§2.2) whose backend entry point takes GL - // arguments and a GL NAME - the record's own MGPipeHandle (MGPImageBind::Res) stops at - // the sink - so there is no way to resolve the texture's twin from what arrives here, - // and the frontend binding this used to read is a BARRIER_PULLED row. - // - // Nothing is lost by deferring: SyncImageTextureBindings at the next validate point - // re-binds every unit up to the mark from the applier's own MGPImageView array, and the - // sink bumps the texture shutter serial immediately after this call - // (MGPipeApplierNoteTextureStateMoved), which is one of the four values the sweep's gate - // is keyed on - so the sweep that follows this bind cannot be suppressed. An image unit - // has no reader but a shader, and no shader runs between here and that validate point. - // - // WHAT MUST STILL HAPPEN HERE is the high-water mark: it is the "no draw in this context - // can be reading an image" early-out, so a unit that is given a texture without raising - // it would be skipped by every sweep afterwards. - static void NoteImageUnitBoundWithoutReadingTheFrontend(Uint unit, Bool holdsTexture) { - if (!holdsTexture) return; - if (unit + 1 > g_imageUnitHighWaterMark) { - g_imageUnitHighWaterMark = unit + 1; - } - } - // ---- P5e (fb, CONTRACT-P5E.md §5.4 / ruling 16 / ID-94): the image unit's bind ------ - // - // P4a e3's ResolveShaderImageRecord IS GONE, identity test, I5 seam log and all. It - // existed because there were TWO answers for one unit - the record and the frontend - // binding - and it had to decide which of them the driver was driven from; its identity - // test (`view.Res != HandleOf(boundTexture)`) was itself a client-allocator probe on the - // apply thread. Under a transport there is no second answer left: the record IS the - // unit, so the arm below reads it and nothing corroborates it. - // - // AND ACCESS IS DECODED. The comment this replaced said MGPImageView::Access carried - // "an ENCODING whose definition belongs to the client emitter and does not exist at the - // contract commit". That was STALE at the time and is what ruling 16 / ID-94 corrects: - // MGPipeEncodeImageAccess has folded the three GL names into 0/1/2 since P4a, and c0e - // moved the numbers into MG_Pipe/MGPipeValueTypes.h so one table serves both roles. - // Reading the frontend's GLenum "because it is the same value by construction" was the - // last field of this record the server declined to believe. - - // The bind itself, factored out of both arms, because the format rules below are the - // part that is easy to get subtly different in two copies - and a divergence there is - // an out-of-bounds image read on nineteen of twenty-six formats, not a wrong pixel. - // Everything it takes is a VALUE: the arm above it decides where each one came from. - // - // COMPILED IN THE PULL BUILD TOO, and that is a deliberate exception to the D-P - // discipline the renderbuffer macros keep (each arm's expression at its original site). - // A macro cannot carry a hundred lines of format reasoning, and the alternative was the - // same hundred lines twice; the function is `static` with ONE caller in the pull build, - // so it has no symbol of its own there and the code it generates is the code that was - // written inline before. Named here rather than discovered by a G1 diff. - // NOLINTNEXTLINE(readability-function-cognitive-complexity) - static void IssueImageTextureBind(Uint unit, BackendTextureObject& backendTexture, - TextureTarget textureTarget, TextureInternalFormat textureFormat, - GLenum appFormat, GLint level, Bool layeredRequested, - GLint layerRequested, GLenum access) { - const Bool layerable = SupportsLayeredImageBinding(textureTarget); - const GLboolean layered = layerable ? static_cast(layeredRequested) : GL_FALSE; - const GLint layer = layerable ? layerRequested : 0; - // The bind half of the image-format widening. The storage sync has just allocated - // this texture's storage in the core carrier of its format (the call above is the - // one that marks it image-bindable), and glBindImageTexture's `format` has to name - // the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on Adreno - // for nineteen of the twenty-six non-core formats and on both Malis for twenty-five, - // and every driver that DOES accept a narrow texture through a wide image accepts it - // silently, reading and writing out of bounds. The frontend's own ImageTextureBinding - // keeps the application's format untouched, so GL_IMAGE_BINDING_FORMAT still answers - // what was passed in. - // - // Widened from the format the APPLICATION named rather than from the texture's own, - // because GL lets the two differ inside one format class and the shader was widened - // from the class the application named too (an r32ui view of an r32f image is a legal - // reinterpretation). The two carriers always have the same texel size - every format - // in a class widens to the four-channel form of that same class - so the storage - // still describes what the bind claims. Gated on the TEXTURE having been widened, so - // a bind format that names a class the storage does not have is left alone: GL - // already calls that undefined, and inventing a carrier for it would only make the - // out-of-class read wider. - // - // A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of - // its own to widen (its texels are the application's buffer object), so - // WidenImageFormatsPass declines to widen every buffer image and the bind must decline - // with it, or the driver would be handed a carrier the shader never addressed. See the - // Dim::Buffer guard there for the 32-byte GL_RG32F measurement that pinned it. - // - // What a buffer image takes instead is the SPLIT, which is the same three-layer move - // through a different door: a private glTexBuffer view names the single-channel base - // format, the bind below names it too, and the shader subscripts it two components per - // original texel. Same gate on all three, so they cannot disagree. - // - // The split view is a SEPARATE texture name over the same buffer, and the bind has to - // name it rather than the application's own: the application's texture keeps the - // format it asked for so that a samplerBuffer reading the same buffer texture - which - // is NOT subscript-rewritten - still sees whole texels. See - // BackendTextureObject::m_bufferImageSplitViewId. - GLenum bindFormat = appFormat; - GLuint bindTextureId = backendTexture.GetBackendTextureId(); - if (textureTarget == TextureTarget::TextureBuffer) { - if (TextureImpl::GetImageBindableBufferSplitFormat(textureFormat) != GL_UNKNOWN_MGL) { - if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat( - MG_Util::ConvertGLEnumToTextureInternalFormat(appFormat)); - boundFormatSplit != GL_UNKNOWN_MGL) { - bindFormat = boundFormatSplit; - if (const Uint splitViewId = backendTexture.GetBufferImageSplitViewId(); - splitViewId != 0) { - bindTextureId = splitViewId; - } - } - } - } else if (TextureImpl::GetImageBindableStorageWidening(textureFormat)) { - const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening( - MG_Util::ConvertGLEnumToTextureInternalFormat(appFormat)); - if (boundFormatWidening) { - bindFormat = boundFormatWidening.InternalFormat; - } - } - g_GLESFuncs.glBindImageTexture(unit, bindTextureId, level, layered, layer, access, bindFormat); - } - - // MGPImageView::Access -> the GL token glBindImageTexture takes. The three numbers are - // MGPipeValueTypes.h's, so this is a spelling change and not a second table; anything - // else on the wire is Fatal{ProtocolCorruption, "ImageView.Access"} AT THE READER, - // which is what the value header's comment asks for (it may not log, so it answers the - // question and the caller owns the refusal). - static GLenum GLAccessForImageView(const MG_Pipe::MGPImageView& view) { - if (!MGPipeImageAccessIsValid(view.Access)) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ImageView.Access\"} An image record " - "carries Access=%u at unit %u, and only 0/1/2 are defined.", - static_cast(view.Access), static_cast(view.Unit)); - std::abort(); - } - switch (MGPipeDecodeImageAccess(view.Access)) { - case MGPipeImageAccess::WriteOnly: return GL_WRITE_ONLY; - case MGPipeImageAccess::ReadWrite: return GL_READ_WRITE; - case MGPipeImageAccess::ReadOnly: - case MGPipeImageAccess::Count: break; - } - return GL_READ_ONLY; - } - - // A writable buffer image, said by the record: the access byte and the resource - // descriptor's storage kind, neither of which is a frontend read. Same question - // IsWritableImageBufferTexture asks of the frontend binding. - static Bool IsWritableImageBufferView(const MG_Pipe::MGPImageView& view) { - if (MG_Pipe::MGPipeHandleIsNull(view.Res)) return false; - if (!MGPipeImageAccessIsValid(view.Access)) return false; - if (!MGPipeImageAccessWrites(MGPipeDecodeImageAccess(view.Access))) return false; - const auto* record = PipeTextureRecordForHandle(view.Res); - return record != nullptr && - static_cast(record->Desc.StorageKind) == TextureStorageType::Buffer; - } - - // GL 4.6 core 8.26: an access through an image unit whose texture does not HAVE the - // (level, layer) the unit names is INVALID - a load returns zero, a store does nothing, - // and neither is an error. The ES storage behind such a unit may well have that level - // (a mutable texture's storage is allocated for its whole chain the first time it is - // image-bound), and the driver would read whatever that undefined level holds. The - // client sends the bindings GL calls incomplete with level kMGPipeImageLevelNone - // (KHR-GL46.shader_image_load_store.incomplete_textures binds level 2 of a texture that - // defines only level 0); a level or layer past the storage is caught here. Decided from - // the applier's records alone, by the same walk the Vulkan backend makes - // (VkTextureManager::ResolveWireTextureStorage): through texture views to the root, - // against each window's levels and layers. - static Bool ImageViewNamesNoTexel(const MG_Pipe::MGPImageView& image) { - using T = MG_Pipe::MGPipeResourceTarget; - const auto& state = MG_Pipe::MGPipeApplier(); - MG_Pipe::MGPipeHandle handle = image.Res; - if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= state.TextureResources.size()) return false; - const auto& first = state.TextureResources[handle.Slot]; - if (!first.Live || first.Gen != handle.Gen) return false; - // GL ignores Layer for a target with no layers, and for a layered bind. - const auto firstTarget = static_cast(first.Desc.Target); - const Bool selectsLayer = !image.Layered && - (firstTarget == T::Tex1DArray || firstTarget == T::Tex2DArray || firstTarget == T::Tex2DMSArray || - firstTarget == T::Tex3D || firstTarget == T::TexCube || firstTarget == T::TexCubeArray); - Uint32 level = image.Level; - Uint32 layer = selectsLayer ? image.Layer : 0; - for (SizeT hop = 0; hop <= state.TextureResources.size(); ++hop) { - if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= state.TextureResources.size()) return false; - const auto& record = state.TextureResources[handle.Slot]; - if (!record.Live || record.Gen != handle.Gen) return false; - if (MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) { - if (static_cast(record.Desc.Target) == T::TexBuffer) return false; - if (level >= record.Desc.Levels) return true; - const Uint32 layers = static_cast(record.Desc.Target) == T::Tex3D - ? std::max(record.Desc.Depth >> level, 1u) - : std::max(record.Desc.ArrayLayers, 1u); - return layer >= layers; - } - const auto viewHandle = record.ViewCso; - if (MG_Pipe::MGPipeHandleIsNull(viewHandle) || viewHandle.Slot >= state.SamplerViewCsos.size()) - return false; - const auto& viewRecord = state.SamplerViewCsos[viewHandle.Slot]; - const auto& view = viewRecord.View; - if (!viewRecord.Live || viewRecord.Gen != viewHandle.Gen || view.Texture != handle) return false; - if (level >= view.NumLevels || layer >= view.NumLayers) return true; - level += view.MinLevel; - layer += view.MinLayer; - handle = record.Desc.ViewOf; - } - return false; - } - - // What such a unit is bound to instead, on a driver whose EMPTY unit is not enough - // (GLESCapabilities::UseInvalidImagePlaceholder): ES 3.2 8.22 leaves an invalid load's - // alpha undefined, Adreno answers 1 where GL says 0, and ANGLE faults outright. - // Elsewhere the unit stays empty, which is exact. A texel outside - // a level is invalid too, and that Adreno answers (0, 0, 0, 0) with the store dropped - so - // the unit gets a 1x1 texture of the shape and format the bind names, cleared to zero each - // time it is bound. Only texel (0, 0[, 0]) of it exists; it is private to the unit, so a - // store through one invalid unit cannot reach a load through another. Every context of the - // process shares one group (CreateContextTuple), so one set serves them all. - struct InvalidImagePlaceholder { - GLuint Id = 0; - GLenum Target = 0; - GLenum InternalFormat = 0; - }; - static Array - g_invalidImagePlaceholders{}; - - // Zero when the shape has no 1x1 ES form (multisample, buffer) or the driver refused it; - // the caller then leaves the unit empty, which is right for every channel but alpha. - static GLuint EnsureInvalidImagePlaceholder(Uint unit, TextureTarget textureTarget, GLenum internalFormat) { - GLenum target = 0; - GLsizei depth = 1; - switch (TextureImpl::MapToBackendTextureTarget(textureTarget)) { - case TextureTarget::Texture2D: target = GL_TEXTURE_2D; break; - case TextureTarget::Texture2DArray: target = GL_TEXTURE_2D_ARRAY; break; - case TextureTarget::Texture3D: target = GL_TEXTURE_3D; break; - case TextureTarget::TextureCubeMap: target = GL_TEXTURE_CUBE_MAP; break; - case TextureTarget::TextureCubeMapArray: target = GL_TEXTURE_CUBE_MAP_ARRAY; depth = 6; break; - default: return 0; - } - GLenum format = 0, type = 0; - MG_Util::TextureFormatProcessor::NormalizePixelFormat(internalFormat, Flags{}, - nullptr, &format, &type); - if (format == 0 || type == 0) return 0; - InvalidImagePlaceholder& slot = g_invalidImagePlaceholders[unit]; - if (slot.Id != 0 && (slot.Target != target || slot.InternalFormat != internalFormat)) { - g_GLESFuncs.glDeleteTextures(1, &slot.Id); - slot = {}; - } - GLint previous = 0; - const GLenum bindingQuery = target == GL_TEXTURE_2D ? GL_TEXTURE_BINDING_2D - : target == GL_TEXTURE_2D_ARRAY ? GL_TEXTURE_BINDING_2D_ARRAY - : target == GL_TEXTURE_3D ? GL_TEXTURE_BINDING_3D - : target == GL_TEXTURE_CUBE_MAP ? GL_TEXTURE_BINDING_CUBE_MAP - : GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; - g_GLESFuncs.glGetIntegerv(bindingQuery, &previous); - DebugImpl::ErrorLopper::Clear(); - if (slot.Id == 0) { - g_GLESFuncs.glGenTextures(1, &slot.Id); - g_GLESFuncs.glBindTexture(target, slot.Id); - if (depth > 1 || target == GL_TEXTURE_2D_ARRAY || target == GL_TEXTURE_3D) { - g_GLESFuncs.glTexStorage3D(target, 1, internalFormat, 1, 1, depth); - } else { - g_GLESFuncs.glTexStorage2D(target, 1, internalFormat, 1, 1); - } - slot.Target = target; - slot.InternalFormat = internalFormat; - } else { - g_GLESFuncs.glBindTexture(target, slot.Id); - } - // Six faces of the widest texel (rgba32) is the most any shape here holds. - static constexpr Uint8 kZeros[6 * 16] = {}; - // The backend unpack state rests at the ES default (Managers.cpp - // ScopedDefaultUnpackState), and a single row ignores the alignment. - BufferImpl::BindPixelUnpackBufferId(0); - if (target == GL_TEXTURE_CUBE_MAP) { - for (GLenum face = 0; face < 6; ++face) - g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 0, 0, 1, 1, format, type, - kZeros); - } else if (target == GL_TEXTURE_2D) { - g_GLESFuncs.glTexSubImage2D(target, 0, 0, 0, 1, 1, format, type, kZeros); - } else { - g_GLESFuncs.glTexSubImage3D(target, 0, 0, 0, 0, 1, 1, depth, format, type, kZeros); - } - g_GLESFuncs.glBindTexture(target, static_cast(previous)); - Bool failed = false; - DebugImpl::ErrorLopper::Loop([&failed, internalFormat](GLenum err) { - failed = true; - MGLOG_D("DirectGLES: invalid-image placeholder (%s) refused: %s", - MG_Util::ConvertGLEnumToString(internalFormat).c_str(), - MG_Util::ConvertGLEnumToString(err).c_str()); - }); - if (failed) { - g_GLESFuncs.glDeleteTextures(1, &slot.Id); - slot = {}; - } - return slot.Id; - } - - // The record form. `Res` is the texture, the four well-defined fields are the record's, - // Access is decoded, and the texture's own TARGET and STORAGE FORMAT - the two - // properties the bind rules need and MGPImageView has no room for (24 bytes, no pad) - - // come off the RESOURCE DESCRIPTOR, which is where the server already keeps them. - void SyncImageTextureBinding(const MG_Pipe::MGPImageView& view) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - const Uint unit = static_cast(view.Unit); - if (unit >= g_writableImageBufferUnits.size()) return; - TrackWritableImageBufferUnit(unit, IsWritableImageBufferView(view)); - if (MG_Pipe::MGPipeHandleIsNull(view.Res)) { - g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); - return; - } - if (unit + 1 > g_imageUnitHighWaterMark) { - g_imageUnitHighWaterMark = unit + 1; - } - const auto* resource = PipeTextureRecordForHandle(view.Res); - if (resource == nullptr) { - // A record naming a texture the applier has no descriptor for is a seam defect, - // not a binding: there is nothing to allocate image-bindable storage from and - // nothing to widen against. Unbind rather than leave the unit on whatever the - // previous draw put there. - MGLOG_E_ONCE("MGPipe: image unit %u names texture {%u, %u}, which has no applier " - "resource record; unbinding the unit", - static_cast(unit), view.Res.Slot, view.Res.Gen); - g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); - return; - } - if (ImageViewNamesNoTexel(view)) { - if (!g_GLESCapabilities.UseInvalidImagePlaceholder) { - g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); - return; - } - // The format the shader was widened from is the one the application bound, so - // the placeholder takes that format's carrier, as the storage would have. - GLenum format = static_cast(view.InternalFormat); - if (const auto widening = TextureImpl::GetImageBindableStorageWidening( - MG_Util::ConvertGLEnumToTextureInternalFormat(format))) { - format = widening.InternalFormat; - } - const GLuint placeholder = - EnsureInvalidImagePlaceholder(unit, PipeTextureTargetForHandle(view.Res), format); - const GLboolean layered = placeholder != 0 && view.Layered != 0 ? GL_TRUE : GL_FALSE; - g_GLESFuncs.glBindImageTexture(unit, placeholder, 0, layered, 0, GLAccessForImageView(view), - placeholder != 0 ? format : GL_RGBA8); - return; - } - const auto textureFormat = static_cast(resource->Desc.InternalFormat); - const TextureTarget textureTarget = PipeTextureTargetForHandle(view.Res); - const GLenum access = GLAccessForImageView(view); - // Before the sync, for the frontend arm's reason: the note may adopt a twin, and - // `backendTexture` below is a reference into the same registry. - if (access != GL_READ_ONLY) { - NoteDriverSideTextureWriteByHandle(view.Res); - } - auto& backendTexture = SyncTextureToBackendByHandle(view.Res, /*imageBindableStorageRequired=*/true); - if (!backendTexture) return; - IssueImageTextureBind(unit, *backendTexture, textureTarget, textureFormat, - static_cast(view.InternalFormat), static_cast(view.Level), - view.Layered != 0, static_cast(view.Layer), access); - } - - // A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to - // write, and those writes land in the ES driver's buffer object - behind the frontend's - // CPU shadow, which is what MapBuffer and GetBufferSubData read. Same flag, and for the - // same reason, as MarkShaderStorageBuffersGpuWritten does for a storage block; the - // difference is only which binding the shader reaches the buffer through. A GL_READ_ONLY - // binding is left alone: marking it would make the next map wait on - and then re-read - - // a dispatch that could not have changed a byte of it. - // - // Called from the draw and dispatch preparations rather than from the eager sync - // glBindImageTexture performs: that one runs before any shader has touched the buffer, - // and flagging there would pull the driver's copy over a shadow the application may - // still be writing into. - // - // P5e (fb, CONTRACT-P5E.md §5.4): DELETED UNDER A TRANSPORT, and it is one edit paired - // with sb's deletion of the backend's storage-block GPU-write marks. The two were one - // mechanism: the backend told the frontend's buffer shadow "a shader wrote through - // this" because only the backend knew which bindings a dispatch could reach. Under a - // split the CLIENT owns that set and keeps it itself (MG_Impl/Pipe/GpuWritePending.h), - // so the mark has an owner on the side that reads it - and this body cannot run there - // anyway: every line of it is a frontend read (the image binding, the downcast to - // TextureObjectBuffer, the buffer binding slot) plus a HandleOfBuffer probe behind - // MarkBufferGpuWritten. The gate is at the top rather than at the two call sites so - // there is one statement of it, and the unit-tracking bookkeeping below stays out of - // reach of an apply thread entirely. - void MarkWritableImageBufferTexturesGpuWritten() { - // P8-C: the SERVER's twin of the buffer behind each writable buffer image, for - // this side's CPU readers (SplitHostBytesForCpuRead). Every read is a record: - // the unit set SyncImageTextureBinding(view) tracked, the applier's image view, - // and the texture descriptor's backing-store handle. - if (g_writableImageBufferUnitCount == 0) return; - const auto& st = MG_Pipe::MGPipeApplier(); - for (Uint unit = 0; unit < g_writableImageBufferUnits.size() && unit < st.BoundShaderImages.size(); - ++unit) { - if (!g_writableImageBufferUnits[unit]) continue; - const MG_Pipe::MGPImageView& view = st.BoundShaderImages[unit]; - if (!IsWritableImageBufferView(view)) continue; - const auto* record = PipeTextureRecordForHandle(view.Res); - if (record == nullptr) continue; - if (auto* buffer = BufferImpl::FindBufferResourceForHandle(record->Desc.BufferForTexBuffer)) { - buffer->serverGpuWritten = true; - } - } - return; - } - - void SyncImageTextureBindings() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // The frontend tracks more image units than ES exposes; binding past the device - // limit raises GL_INVALID_VALUE on every dispatch. - const Uint unitCount = std::min(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS, - static_cast(std::max(g_GLESCapabilities.MaxImageUnits, 0))); - // P4a e3 (D-G2, D-J2), CORRECTED BY THE REVIEW'S MAJOR-1. - // - // THE RECORD'S WINDOW IS NOT THIS SWEEP'S MEMBERSHIP. D-J2 and PipeApply.h's MGPipeWorkingState - // are a rule about RECORD RETENTION - entries outside the var-tail window are not - // cleared in the applier - and say nothing about which DRIVER units a re-issue - // sweep has to visit. The membership this loop needs is "every unit the driver - // currently has an image on", and that is g_imageUnitHighWaterMark, which is - // server-owned by construction: SyncImageTextureBinding is the only path to - // glBindImageTexture and raises the mark on every unit it hands a texture, so the - // mark is monotonic and covers every unit a re-mint could strand. - // - // The first version of this arm walked [Start, Start + Count) and returned, which - // is the SMALLER-WINDOW failure direction DEV-3 refused for keys.maxTouchedUnit: if - // set_shader_images is the program-RESOLVED set, a program declaring only unit 0 - // pushes a window of [0,1), and unit 5's image - established eagerly by an earlier - // dispatch and never revisited - is skipped. A texture re-minted between the two - // dispatches then leaves unit 5 on a deleted driver name: a write through a freed - // allocation on Adreno, a rejected dispatch on Mali. The pre-handle sweep bound - // EVERY unit precisely so that could not happen, and that guarantee is what this - // sweep exists for. So the record's window is UNIONED with the mark, never - // substituted for it, and the walk starts at 0 rather than at Start (a unit BELOW - // Start is as undescribed as one above Start + Count). - // - // A8, which the review adds to this package's assumption list: NOTHING IN THE - // CONTRACT PINS THE WINDOW'S MEMBERSHIP. PipeApply.h's MGPipeWorkingState says only that the - // record is the last set as received, and that Start + Count above the bound is - // Fatal{ProtocolCorruption}. Until package C documents in ImageEmit.h that the - // window covers every unit the driver holds, this union is load-bearing; the - // verification round logs (ShaderImageStart, ShaderImageCount) against the mark for - // a Minecraft frame and says whether it can ever be relaxed. - // - // What the record still buys is real and is e3's actual deliverable: the four field - // VALUES come off it inside SyncImageTextureBinding, and the walk stops at the - // high-water mark instead of at the device's MaxImageUnits. The units in - // [end, unitCount) the pre-handle arm additionally visits have never been given a - // texture through the only funnel that can give one, so re-binding 0 on them is a - // provable no-op. - { - const auto& st = MG_Pipe::MGPipeApplier(); - // P4a decline-site I6: M - a set that never arrived means the full pre-handle - // sweep below, which is the SAFE (wider) direction. SILENT HERE, CONFIRMED AT - // THE VERIFICATION ROUND, and it is silent here because I2 says it once per - // process from inside that sweep, where the unit that actually holds an image - // texture can be named. Two lines for one condition is how a grep's count - // stops meaning anything. - if (st.ShaderImageCount != 0) { - // P4a decline-site I7: the window/mark UNION (MAJOR-1, fixed here); no flip - // remains at the verification round, only the A8 measurement above. - // - // P5e (fb) KEEPS THE UNION and says so again, because ruling 7 / ID-86 make - // it contract text now (§5.4: "never narrowed to the window"). Under a - // transport the walk additionally READS the applier's array rather than the - // frontend binding at each unit - a unit inside the mark but outside the - // window keeps whatever MGPImageView the last set that covered it left - // there (D-J2: entries outside the window are not cleared), which is the - // record of the very binding that raised the mark. - const Uint32 end = std::min( - std::max(st.ShaderImageStart + st.ShaderImageCount, - static_cast(g_imageUnitHighWaterMark)), - static_cast(unitCount)); - for (Uint32 unit = 0; unit < end; ++unit) { - if (unit >= st.BoundShaderImages.size()) break; - // The union can include a slot no set_shader_images - // record has described yet. Its zero-initialized Unit - // is not a binding identity: treating it as one unbinds - // real unit 0 after binding the current program's images. - // Array position identifies both retained and empty slots. - auto view = st.BoundShaderImages[unit]; - view.Unit = unit; - SyncImageTextureBinding(view); - } - return; - } - } - // The wide pre-handle sweep reads the frontend binding of every unit, which an - // apply thread may not do. Under a transport there is nothing for it to do anyway: - // g_imageUnitHighWaterMark is raised only by the funnel above, so if no set has - // ever arrived no unit has ever been given an image, and re-binding 0 on units that - // never held one is the no-op the comment above already proved. - return; - } - - // What the draw path last swept the image units against. A draw never swept them at all: - // an image unit was established once, eagerly, by glBindImageTexture and never revisited. - // That is stale the moment the texture behind it is re-specified with a new size or - // format, because ES 3.1 only allows IMMUTABLE storage on an image unit - // (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be - // redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at - // the deleted one and imageSize() reporting the old dimensions - // (KHR-GL43.shader_image_size.advanced-changeSize). - static Uint64 g_imageSweepContextId = 0; - static Uint64 g_imageSweepSamplingGeneration = 0; - static Uint g_imageSweepBackendContextGeneration = 0; - static Bool g_imageSweepValid = false; - // P5e (fb, ruling 7 / ID-86): the record arm's three applier serials. Held in their own - // fields beside the frontend pair rather than reusing them, for the reason the two - // framebuffer list keys are held apart - one field carrying two key shapes is how a - // stale half gets compared against a live one. - static Uint64 g_imageSweepShaderImagesSerial = 0; - static Uint64 g_imageSweepTextureShutterSerial = 0; - static Uint64 g_imageSweepContextSerial = 0; - - // The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the - // frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion - // moves on exactly the shape and format changes that can force the re-mint. Deliberately - // NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be - // the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside - // this very sweep, so a backend-side trigger would be bumped after the gate had already - // declined to run it. - // - // P4a e3 (D-G4) leaves BOTH properties of this gate exactly as they are, and says so - // because they are the kind of thing a later optimisation deletes: - // - // 1. the g_imageUnitHighWaterMark == 0 early-out is what makes every Minecraft draw - // pay one integer test, and it is asked BEFORE anything reads a record; - // 2. the gate key stays the FRONTEND sampling-resolution generation and is - // deliberately not re-keyed onto a server-owned serial, for the reason above: a - // texture bound only to an image unit is re-minted INSIDE this sweep, so an epoch - // the server bumps would move after the gate had already declined to run it. The - // client's own NewShaderImages shutter mixes the same three frontend counters - // (texture content, texture params, the program's image-unit version), so the - // property is preserved on both sides by construction rather than by agreement. - // - // P5e (fb, ruling 7 / ID-86): PROPERTY 1 IS UNTOUCHED. PROPERTY 2 IS RE-KEYED, and the - // argument behind it is kept rather than dropped - it is what decides WHICH four values - // the new key may be made of. - // - // The frontend sampling-resolution generation cannot be read on an apply thread, so the - // key becomes (ShaderImagesSerial, TextureShutterSerial, ContextSerial, - // g_backendContextGeneration). Three of those are APPLIER-DERIVED - they move when the - // client's own shutters send a new set of images, new texture content or new texture - // parameters, i.e. at exactly the moments the frontend counter moved - and the fourth - // is the backend's ES-context generation, which the old key already carried. - // - // NONE OF THEM IS A BACKEND RE-MINT COUNTER, and that is the whole of the ruling: the - // hazard the old comment names is a texture bound ONLY to an image unit being re-minted - // INSIDE this sweep, which would bump a backend-side epoch after the gate had already - // declined to run. An applier serial moves when the CLIENT said something, strictly - // before the sweep the saying provoked, so the property survives the re-key intact. - // g_backendContextGeneration is not a counter of that kind either - it moves when the - // ES context is rebuilt, which no sweep does. - // - // THE FOUR SERIALS SAY NOTHING ABOUT CONTENT, so the key carries each image texture's - // content serial as well. A glTexSubImage* into a texture that is bound ONLY to an image - // unit moves that texture's record Serial and none of the four: the upload stays in the - // record's pending set, and nothing else flushes it before the draw - the sampled-unit - // walk and the attachment lists never visit an image-only texture. The draw then - // imageStore()s into the driver texture, and the stale upload lands on top of those - // stores at the next sync that does visit the texture (a readback), which is the - // ordering the application asked for in reverse (KHR-GL42.shader_image_load_store. - // early-fragment-tests, which zeroes its image before every draw). - // - // Per unit, keyed on the bound HANDLE and its record's Serial: the handle carries the - // slot's generation, so a recycled slot cannot answer with its predecessor's serial, and - // the record Serial is the server's own monotone content version. The walk is bounded by - // the high-water mark, so it costs nothing on the draws the first early-out already - // returns from. - struct ImageSweepUnitContent { - MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; - Uint64 Serial = 0; - }; - static Array g_imageSweepUnitContent{}; - static Uint32 g_imageSweepUnitContentCount = 0; - - static Uint32 ImageSweepUnitContentCount(const MG_Pipe::MGPipeApplierState& st) { - return std::min(static_cast(g_imageUnitHighWaterMark), - static_cast(st.BoundShaderImages.size())); - } - - static Uint64 ImageUnitContentSerial(MG_Pipe::MGPipeHandle res) { - if (MG_Pipe::MGPipeHandleIsNull(res)) return 0; - const auto* record = PipeTextureRecordForHandle(res); - return record != nullptr ? record->Serial : 0; - } - - static Bool ImageSweepContentUnchanged(const MG_Pipe::MGPipeApplierState& st) { - const Uint32 count = ImageSweepUnitContentCount(st); - if (count != g_imageSweepUnitContentCount) return false; - for (Uint32 unit = 0; unit < count; ++unit) { - const MG_Pipe::MGPipeHandle res = st.BoundShaderImages[unit].Res; - const auto& memo = g_imageSweepUnitContent[unit]; - if (!(memo.Res == res) || memo.Serial != ImageUnitContentSerial(res)) return false; - } - return true; - } - - static void StampImageSweepContent(const MG_Pipe::MGPipeApplierState& st) { - const Uint32 count = ImageSweepUnitContentCount(st); - for (Uint32 unit = 0; unit < count; ++unit) { - const MG_Pipe::MGPipeHandle res = st.BoundShaderImages[unit].Res; - g_imageSweepUnitContent[unit] = {res, ImageUnitContentSerial(res)}; - } - g_imageSweepUnitContentCount = count; - } - - void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) { - if (g_imageUnitHighWaterMark == 0) return; - const auto& st = MG_Pipe::MGPipeApplier(); - if (g_imageSweepValid && g_imageSweepShaderImagesSerial == st.ShaderImagesSerial && - g_imageSweepTextureShutterSerial == st.TextureShutterSerial && - g_imageSweepContextSerial == st.ContextSerial && - g_imageSweepBackendContextGeneration == g_backendContextGeneration && - ImageSweepContentUnchanged(st)) { - return; - } - SyncImageTextureBindings(); - // The sweep may just have landed a re-upload into a texture the draw reaches through - // an image unit (the content clause above is one of the ways it gets here). - if (const GLbitfield barriers = TakeInPlaceTextureUploadBarrierBits(); - barriers != 0 && g_GLESFuncs.glMemoryBarrier) { - g_GLESFuncs.glMemoryBarrier(barriers); - } - g_imageSweepShaderImagesSerial = st.ShaderImagesSerial; - g_imageSweepTextureShutterSerial = st.TextureShutterSerial; - g_imageSweepContextSerial = st.ContextSerial; - g_imageSweepBackendContextGeneration = g_backendContextGeneration; - // After the sweep: it is the sync that consumed the pending set, and it may raise the - // high-water mark the walk is bounded by. - StampImageSweepContent(st); - g_imageSweepValid = true; - return; - } - } // namespace TextureImpl - - namespace FramebufferImpl { - // P4a e1 (D-C2, D-B3). The framebuffer family's handle arm. - // - // THE FOUR-PART MEMO BECOMES TWO VALUES, and the halves are not symmetric. - // g_fboSyncedSlotVersions / ObjectVersions / Objects are three answers to one question - // - "has the state the client would describe moved?" - asked of the frontend from the - // backend. The applier answers it with one number: FramebufferSerial is bumped by the - // applier before the backend is told anything, on every set_framebuffer_state it - // accepts, and a record the client's ContentHash suppressor withheld provably describes - // state that did not move. So one Uint64 compare replaces the three. - // - // The FOURTH stays, and must: g_attachmentBackendIdGeneration answers "did *I* re-mint - // a driver texture id", which no client-side version can answer, and dropping it is the - // shape of bug commit d7655247 fixed on the buffer side (D-B3, D-O). The ES context - // generation joins it for the same reason the legacy arm's other memos carry it - the - // driver FBO names die with the context. - // Kept PER TARGET, like the arrays it replaces, and for the one reason that survives - // the collapse: ForceBindCurrentFBO syncs a single target and has to be able to say so. - // What collapses is the three-value "has the state moved" question into one serial. - struct SyncedFramebufferSerialMemo { - Uint64 serial = 0; // the FramebufferSerial this target was last synced at - Uint64 backendIdGeneration = 0; - Uint contextGeneration = 0; - Bool valid = false; - }; - static Array - g_fboSyncedSerials{}; - - // Whether the last SyncCurrentFBO found the two records describing the two bindings. - // BindCurrentFBO's handle arm reads it instead of re-asking, because the whole value - // of that arm is that it does NOT read the binding slot - and because every entry - // point that binds runs the sync first, immediately before (PrepareForDraw, Clear, the - // blits, the DSA clears, the readbacks). It starts false and every decline clears it, - // so a bind that somehow arrives without a preceding sync takes the pre-handle arm - // rather than trusting a record nothing checked. - static Bool g_fboRecordsTrusted = false; - - // A9 (the review's MAJOR-4, and it is PACKAGE D's fix, not this file's): every call - // site of this function in DirectGLES.cpp is paired with - // FramebufferImpl::InvalidateFramebufferBindingCache() - all six, verified - but that - // function lives in Managers.cpp (D's file for the whole phase) and has three further - // callers this package cannot reach: MG_Test/SanityTest.cpp's ScopedStateGuardMocks:: - // ResetShadows and ScopedBackendTwinMocks' ctor and dtor. Those three clear the - // pre-handle trio and leave g_fboSyncedSerials and g_fboRecordsTrusted stale across a - // GLES function-table swap - harmless while every arm declines, but on the integrated - // tree with bit 9 set a fixture can be entered with the records trusted and a memo - // claiming a target is synced, so BindCurrentFBO would bind from a record while the - // mock table is installed. THE FIX IS TO CALL THIS FROM INSIDE - // InvalidateFramebufferBindingCache so no caller can forget; it is one line in D's - // file and it is on D's rework list. - static void InvalidateFramebufferHandleArmMemos() { - for (auto& memo : g_fboSyncedSerials) memo.valid = false; - g_fboRecordsTrusted = false; - } - - // Record that `target` now reflects the applier's record as of `serial`. The two - // generations ride along for the reason D-B3 keeps them: they answer questions about - // the DRIVER's own ids that no client-side version can answer. - // The stamp's opposite, for a path that leaves a target correctly bound WITHOUT having - // consulted the record (MINOR-3, ForceBindCurrentFBO). Costs one extra sync at worst. - static void InvalidateSyncedFramebufferSerial(FramebufferTarget target) { - g_fboSyncedSerials[SizeT(target)].valid = false; - } - - static void StampSyncedFramebufferSerial(FramebufferTarget target, Uint64 serial) { - auto& memo = g_fboSyncedSerials[SizeT(target)]; - memo.serial = serial; - memo.backendIdGeneration = g_attachmentBackendIdGeneration; - memo.contextGeneration = g_backendContextGeneration; - memo.valid = true; - } - - // The seam check, and it is the reason this arm can be trusted at all on a tree where - // the client half is mid-landing. The record says which framebuffer each binding - // holds; the binding slot says the same thing. If they disagree, the record is stale - // or mis-keyed and driving the driver from it would sync - or skip - the wrong - // framebuffer, which is the one failure this path could produce that no pixel test on - // this tree could see. Disagreement DECLINES the whole arm rather than fixing up one - // target, because the two records are emitted together and one of them being wrong - // says nothing good about the other. - // - // The default framebuffer is compared through IsDefault rather than through Fbo, - // because the reserved handle {0,1} is not the handle the twin table minted for the - // default FramebufferObject; comparing it against HandleOf would fail for the one case - // the reserved handle exists to make easy. This is the identity comparison D-C1 - // retires, kept here ONLY as a consistency check and only until package D's twin API - // takes the record instead of the object - at which point there is no second answer - // left to disagree with. - // - // P5e (fb, CONTRACT-P5E.md §5.4): MONOLITH-ONLY. The sentence above ends "...and only - // until package D's twin API takes the record instead of the object - at which point - // there is no second answer left to disagree with". That point is this package: the - // twin's sync takes the handle, the record was resolved from the applier's OWN bound - // handle, and the only thing this could still consult is the binding slot - a - // BARRIER_PULLED row an apply thread may not read. So under a transport the check does - // not run at all, rather than running on a value it is not allowed to have. - static Bool FramebufferRecordMatchesBinding(FramebufferTarget target, - const MG_Pipe::MGPFramebufferState& record) { - return true; - } - - static Bool SyncedFramebufferSerialIsCurrent(FramebufferTarget target, Uint64 serial) { - const auto& memo = g_fboSyncedSerials[SizeT(target)]; - return memo.valid && memo.serial == serial && - memo.backendIdGeneration == g_attachmentBackendIdGeneration && - memo.contextGeneration == g_backendContextGeneration; - } - - // The handle arm of SyncCurrentFBO. Returns false when it declined - which is not a - // failure and is the only honest answer while no client has emitted yet. - // - // NO LIVE RECORD IS A MISS, NEVER A HIT, and it is the same rule the vertex-input arm - // states at SyncVaoAttributeBuffersByHandle. What is NOT the test, and this is a trap - // worth naming: FramebufferSerial is not "has the client ever described a framebuffer". - // MGPipeApplierReset ADVANCES every P4a working-state serial unconditionally - // (PipeApply.cpp), because a cleared window is itself a change a twin has to hear - // about - so the serial is non-zero after the first make-current even though no - // set_framebuffer_state has ever been applied, and gating on it would take this arm - // with two empty records and leave every framebuffer unsynced. The record's own Fbo - // handle is the test, and BOTH bindings have to be described before this arm can run - // at all, because it answers for both in one pass: an emitter walks {Draw, Read} and a - // Target = Both record writes both, so a half-described applier is the transitional - // state of a tree whose client half has not landed yet, and that state belongs to the - // pre-handle arm. - static Bool SyncCurrentFBOByRecord() { - g_fboRecordsTrusted = false; - const auto& st = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPFramebufferState* const drawRecord = BoundFramebufferRecord(FramebufferTarget::Draw); - const MG_Pipe::MGPFramebufferState* const readRecord = BoundFramebufferRecord(FramebufferTarget::Read); - const Bool drawUnrecorded = drawRecord == nullptr || MG_Pipe::MGPipeHandleIsNull(drawRecord->Fbo); - const Bool readUnrecorded = readRecord == nullptr || MG_Pipe::MGPipeHandleIsNull(readRecord->Fbo); - // P4a decline-site F2: S - BOTH CASES ARE LOUD, the half-described one since v2 - // (ID-19) and the NEITHER-DESCRIBED one from this verification round. - // - // EXACTLY ONE recorded: an emitter walks {Draw, Read} together and a Target = Both - // record writes both, so no correct emitter can produce it. It is the shape a - // PARTIALLY LANDED emitter produces and it is invisible in a pixel test because - // the arm just declines. - // - // NEITHER recorded WAS the transitional state of a tree whose client half had not - // landed - on this branch, before B and C were integrated, every draw was that, - // which is why v2 left it silent. Package B now emits set_framebuffer_state for - // every bound framebuffer and this function only runs with bit 9 (and therefore - // 10, 11 and 7) set, so reaching a draw with NEITHER binding described means the - // record for the bound framebuffer never arrived, or arrived at a slot whose - // generation has already moved - the applier counts that second cause separately - // in StaleFramebufferRecordLookups, so the two are told apart without a third - // message. Both are seam defects and both now say so, in the words section 8's - // one grep looks for. - if (drawUnrecorded || readUnrecorded) { - if (drawUnrecorded != readUnrecorded) { - const MG_Pipe::MGPFramebufferState& described = *(drawUnrecorded ? readRecord : drawRecord); - MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the %s " - "binding has no record while %s names {slot %u, gen %u} - a " - "half-described applier; running the pre-handle framebuffer sync.", - drawUnrecorded ? "DRAW" : "READ", drawUnrecorded ? "READ" : "DRAW", - static_cast(described.Fbo.Slot), - static_cast(described.Fbo.Gen)); - } else { - MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: NEITHER " - "the DRAW nor the READ binding has one while the framebuffer " - "subsystem bit is set (bound handles {slot %u, gen %u} / " - "{slot %u, gen %u}, %llu stale-generation lookup(s) so far); " - "running the pre-handle framebuffer sync.", - static_cast( - st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)].Slot), - static_cast( - st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)].Gen), - static_cast( - st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)].Slot), - static_cast( - st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)].Gen), - static_cast(st.StaleFramebufferRecordLookups)); - } - return false; - } - // P4a decline-site F3: S - the framebuffer seam, already loud; the round keeps this - // wording VERBATIM because it is the stem the other two seams now share. - if (!FramebufferRecordMatchesBinding(FramebufferTarget::Draw, *drawRecord) || - !FramebufferRecordMatchesBinding(FramebufferTarget::Read, *readRecord)) { - MGLOG_E_ONCE("A framebuffer record does not describe the binding it names; " - "running the pre-handle framebuffer sync."); - return false; - } - g_fboRecordsTrusted = true; - - const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read}; - for (auto& target : fboTargets) { - if (SyncedFramebufferSerialIsCurrent(target, st.FramebufferSerial)) continue; - - // Not a third accessor call: these are the two pointers F2 proved non-null a - // few lines up, so nothing here dereferences an unchecked result. - const MG_Pipe::MGPFramebufferState& record = - target == FramebufferTarget::Draw ? *drawRecord : *readRecord; - if (record.IsDefault != 0) { - // The default framebuffer, said by the record rather than by comparing the - // bound object against pDefaultFramebufferInfo->defaultFBO - which is one - // of the four identity comparisons the reserved handle {0,1} exists to - // retire (MGPipeHandles.h). Nothing to sync except the widened-attachment - // masks, which only SyncToBackend ever writes and would otherwise still - // describe the user FBO that was draw-bound before. The window surface is a - // real RGBA buffer, so nothing here is ever widened. - if (target == FramebufferTarget::Draw) { - g_alphaWidenedDrawBufferMask = 0; - g_integerColorDrawBufferMask = 0; - } - StampSyncedFramebufferSerial(target, st.FramebufferSerial); - continue; - } - - // MONOLITH GLUE, and named as such: on the monolith arm the twin's SyncToBackend - // still reads the frontend object, and the bound object is still this call's - // ARGUMENT. The IDENTITY is already the handle - GetOrCreateByHandle(record.Fbo) - - // so a recycled FBO cannot be mistaken for its predecessor here. - // - // A1, TAKEN AT THE VERIFICATION ROUND AGAINST WHAT D ACTUALLY BUILT. The twin - // is resolved BY HANDLE ONLY: `BackendPtr* GetOrCreateByHandle(MGPipeHandle)` - // (Managers.h ~453) returns a POINTER rather than the reference - // GetOrCreate(StatePtr) returns, deliberately, because it has three ways to - // decline - the legacy arm, a slot past the table's sanity bound, and a - // generation BEHIND the live entry's - and every one of them has to be visible - // here rather than answered with a parked twin. - // - // P5e (fb, CONTRACT-P5E.md §5.4): AND ON THE RECORD ARM THE OBJECT IS GONE TOO. - // `SyncToBackendByHandle(record.Fbo, target)` takes the record's own handle, so - // there is no binding slot to read, no object to hand over, and no state note - // to leave behind - NoteStateForHandle was the server's last cross-record hold - // on a frontend framebuffer, and the named blit that needed it now reads the - // record too. What is left frontend under a transport is nothing at all. - // - // THE POINTER-INVALIDATION CONTRACT (Managers.h ~381-385): a handle-arm result - // is a stable array element that only a table-GROWING GetOrCreate can move. - // GetOrCreateByHandle is exactly such a call, so its result is used and dropped - // inside this iteration and never held across another registry call. - auto* const twinSlot = g_backendFramebufferObjects.GetOrCreateByHandle(record.Fbo); - // A1's third decline. Bit 9 implies bit 10 implies bit 7, so the slot tables are - // armed whenever this function runs and the legacy-arm cause cannot fire here; - // what is left is a slot past the sanity bound or a generation behind the live - // entry's, and both are seam defects - a handle the emitter minted for a - // recycled slot and never described, or one it had already retired. The whole - // arm declines rather than this one target, for F4's reason. - if (twinSlot == nullptr) { - MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the " - "%s record's handle {slot %u, gen %u} is refused by the twin slot " - "table (live generation %u); running the pre-handle framebuffer " - "sync.", - target == FramebufferTarget::Read ? "READ" : "DRAW", - static_cast(record.Fbo.Slot), - static_cast(record.Fbo.Gen), - static_cast( - g_backendFramebufferObjects.LiveGenAt(record.Fbo.Slot))); - g_fboRecordsTrusted = false; - return false; - } - auto& backendObj = *twinSlot; - if (!backendObj) { - backendObj = MakeShared(); - } - - // THE "SAME FBO AS DRAW" SKIP IS A FIELD, not a pointer comparison against the - // object the previous iteration happened to sync. Target = Both is the client - // saying one object is bound to both bindings, so the attachment and - // draw-buffer work the DRAW pass already did is not repeated - and the read - // buffer, which is READ-target-specific and is what that skip used to drop, is - // applied unconditionally on this path. - // - // ID-27 (wire review v2, MAJOR-1): THE QUESTION IS ASKED OF THE BOUND HANDLES, - // NOT OF THE RECORD'S STORED TARGET, and it is no longer possible to ask it any - // other way in this file. `MGPFramebufferState::Target` is the target of the - // LAST EMISSION that wrote the record, and since ID-19(c) that emission may be - // a `Named` one from any of B's sixteen DSA sites - so a framebuffer really - // bound to both bindings can carry `Target == Named` and a framebuffer bound to - // neither can carry `Target == Both` left over from when it was. The skip's - // real question is "is one object bound to both bindings", and the applier - // answers it directly: BoundFramebuffer[Draw] == BoundFramebuffer[Read]. Both - // are non-null here (F2 rejected a null on either), and a handle compares by - // {slot, gen}, so a recycled slot is not its predecessor. - // - // MINOR-1's correction is RETIRED by P5e: the read buffer is no longer applied - // "FROM THE FRONTEND OBJECT" on this arm - ApplyReadBufferFromRecord resolves it - // out of MGPFramebufferState::ReadSurface, which was already the record's - // answer, and the by-handle entry is what reaches it here. - const auto& boundHandles = st.BoundFramebuffer; - const Bool sameObjectOnBothBindings = - target == FramebufferTarget::Read && - boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)] == - boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)]; - - if (sameObjectOnBothBindings) { - backendObj->SyncReadBufferToBackendByHandle(record.Fbo); - } else { - backendObj->SyncToBackendByHandle(record.Fbo, target); - } - StampSyncedFramebufferSerial(target, st.FramebufferSerial); - continue; - } - - return true; - } - - void SyncCurrentFBO() { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - // The handle arm first, and it declines rather than half-running: with no record - // yet it hands the walk straight back to the pre-handle arm below, unchanged. - // P4a decline-site F1: M - the mask says this family is not switched on, and it - // IS D's FramebufferSubsystemEnabled() now. Silent, confirmed at the - // verification round. - if (SyncCurrentFBOByRecord()) return; - // P5e (fb, CONTRACT-P5E.md §5.4): AND UNDER A TRANSPORT THE DECLINE IS THE END OF - // IT. The fallback below reads GetFramebufferBindingSlot, a BARRIER_PULLED row; the - // value it would read belongs to a client this apply is no longer synchronous with, - // so "run the pre-handle sync" stops being a safe default and becomes a wrong - // picture taken from torn state. Every cause of the decline is already named by the - // MGLOG_E_ONCE that produced it (F2's half-described / neither-described applier, - // F4's unbound binding, A1's refused handle); this turns that log into the abort the - // contract asks for, with the verb in the message so the strict lane's marker table - // points at the site. - RefuseFramebufferBindingSlotRead(); - } - } // namespace FramebufferImpl - - namespace RenderStateImpl { - static Uint16 g_syncedRenderStateVersion = 0; - static Bool g_hasSyncedRenderState = false; - static RenderStateParameters g_syncedRenderStateParameters; - static IntVec4 g_syncedBackendViewport = IntVec4(-1, -1, -1, -1); - // The RESOLVED scissor rectangle last pushed (see the scissor block in SyncRenderState); - // an impossible value so the first sync always pushes. - static IntVec4 g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1); - // GLES starts with sRGB framebuffer encoding on, so the first sync always has to push the - // frontend's (desktop-GL default) disabled state down. - static Bool g_syncedSrgbFramebufferWrites = true; - // Set when the shadow below stops describing the real ES context. The ES context - // OUTLIVES every MobileGL context, so a MobileGL context switch leaves it holding the - // previous context's enable state while the frontend's parameter block AND its - // version counter both restart from defaults. Every "differs from what I last pushed" - // test in SyncRenderState would then agree that nothing needs pushing, and the - // leftover state silently applies to the new context - the class of bug the CTS - // caught as GL_FRAMEBUFFER_SRGB surviving from vertex_attrib_binding into - // direct_state_access.renderbuffers_storage. One unconditional push settles the whole - // block rather than the one cap that happened to be noticed. - static Bool g_forceFullRenderStateResync = true; - // Which draw buffers' alpha channel the colour mask last pushed to the driver had forced - // OFF - i.e. the value of `appliedWidenMask` in the last SyncRenderState that reached the - // colour-mask block. NOT derivable from the frontend parameter block: it depends on the - // bound DRAW framebuffer's attachment formats and on whether the caller is a draw or a - // clear, neither of which bumps the frontend render-state version. Without it a - // clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits - // the clear's undoctored mask. - static Uint32 g_syncedColorMaskAlphaWidenMask = 0; - // Scratch for the dual-source-blend decline path in the blend block below. File-scope - // rather than a local so the ordinary draw pays nothing for it: it is written only on a - // driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and - // SyncRenderState runs on the GL thread only. - static Array - g_dualSourceDeclinedBlendStates; -#if MOBILEGL_BUILD_DISAGGREGATED - // The context epoch the shadow was last synced in (SyncRenderState). - static ContextEpoch g_syncedRenderStateEpoch{}; -#endif - void InvalidateSyncedRenderState() { - g_forceFullRenderStateResync = true; - g_hasSyncedRenderState = false; - g_syncedBackendViewport = IntVec4(-1, -1, -1, -1); - g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1); - } - - // Whether the shadow's bits are the current native context's. An invalidation (a context - // switch, the backend's turn moving to another session's thread) only raises flags - the - // bits stay, and they are then another context's. - static Bool SyncedRenderStateDescribesCurrentContext() { - if (!g_hasSyncedRenderState || g_forceFullRenderStateResync) return false; -#if MOBILEGL_BUILD_DISAGGREGATED - if (g_syncedRenderStateEpoch != CurrentContextEpoch()) return false; -#endif - return true; - } - - Bool DriverScissorTestEnabled() { - if (SyncedRenderStateDescribesCurrentContext()) - return (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; - return g_GLESFuncs.glIsEnabled != nullptr && g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE; - } - - void SetSyncedScissorTestForTesting(Bool enabled) { - g_syncedRenderStateParameters.ScissorTestEnabledMask = enabled ? 1u : 0u; - g_hasSyncedRenderState = true; - g_forceFullRenderStateResync = false; -#if MOBILEGL_BUILD_DISAGGREGATED - g_syncedRenderStateEpoch = CurrentContextEpoch(); -#endif - } - - void SyncRenderState(Bool forColorClear) { -#if MOBILEGL_BUILD_DISAGGREGATED - const auto epoch = CurrentContextEpoch(); - if (g_syncedRenderStateEpoch != epoch) { - InvalidateSyncedRenderState(); - g_syncedRenderStateEpoch = epoch; - } -#endif -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - Uint16 currentRenderStateVersion = MG_Pipe::gPipeInputs->GetRenderStateParametersVersion(); - const Bool forceFullPush = g_forceFullRenderStateResync; - g_forceFullRenderStateResync = false; - // The alpha discipline for widened colour attachments (see the header comment on - // SyncRenderState): a DRAW must not be able to move the stored alpha off 1.0, a CLEAR - // is what puts it there. So the draw path masks alpha off on every widened draw - // buffer and the clear path masks nothing. - const Uint32 appliedWidenMask = forColorClear ? 0u : FramebufferImpl::g_alphaWidenedDrawBufferMask; - const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask; - if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState && - currentRenderStateVersion == g_syncedRenderStateVersion) { - // Gate 1 of section 2.3.1: the steady-state cost of this whole function is - // the one Uint16 read above plus this compare. - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/true); - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); - } - return; - } - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/false); - // The version read above, the parameter-block fetch and the viewport fetch - // below - the three accessor calls this function makes unconditionally on a - // miss. The conditional sRGB capability read further down is deliberately - // NOT counted (see the inventory in PipeStats.cpp). - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); - } - - const auto& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters(); - - // The frontend has ONE version for the whole parameter block, so a per-draw blend - // toggle used to re-diff all ~40 pieces of state field by field on every draw - // (Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), making this the - // hottest thing mc_state_toggle did). Split the struct into three contiguous byte - // spans - the head (viewport/point/line/polygon-offset scalars), the blend array, - // and everything after it - and let one memcmp per span decide whether its blocks - // run at all. memcmp can false-DIFFER on padding bytes (harmless: the field-wise - // block runs and finds nothing) but can never false-match, and after the first full - // sync the tail memcpy below makes the shadow byte-identical, padding included, so - // in the steady state a span memcmp is exact. Blocks whose inputs are NOT in the - // parameter struct (the surface-size viewport fallback, the sRGB context - // capability) stay outside the gates. - static_assert(std::is_trivially_copyable_v, - "span memcmp/memcpy below treats the parameter block as raw bytes"); - constexpr SizeT kBlendSpanBegin = offsetof(RenderStateParameters, BlendStates); - constexpr SizeT kBlendSpanEnd = offsetof(RenderStateParameters, LogicOp); - const auto* currentBytes = reinterpret_cast(¶meters); - const auto* syncedBytes = reinterpret_cast(&g_syncedRenderStateParameters); - const Bool headSpanDirty = - !g_hasSyncedRenderState || std::memcmp(currentBytes, syncedBytes, kBlendSpanBegin) != 0; - const Bool blendSpanDirty = - !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanBegin, syncedBytes + kBlendSpanBegin, - kBlendSpanEnd - kBlendSpanBegin) != 0; - const Bool tailSpanDirty = - !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd, - sizeof(RenderStateParameters) - kBlendSpanEnd) != 0; - - IntVec4 backendViewport = MG_Pipe::gPipeInputs->GetViewport(); - if (backendViewport.z() <= 0 || backendViewport.w() <= 0) { - Int surfaceWidth = 0; - Int surfaceHeight = 0; - if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { - backendViewport = IntVec4(0, 0, surfaceWidth, surfaceHeight); - } - } - if (backendViewport != g_syncedBackendViewport) { - g_GLESFuncs.glViewport(backendViewport.x(), backendViewport.y(), backendViewport.z(), - backendViewport.w()); - g_syncedBackendViewport = backendViewport; - } - - // Every capability bool (and the scissor-test mask below) lives after LogicOp in the - // struct, i.e. in the tail span. - if (tailSpanDirty) { -#define SYNC_CAPABILITY(cap_mg, cap_gl) \ - if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \ - if (parameters.cap_mg##Enabled) { \ - g_GLESFuncs.glEnable(cap_gl); \ - } else { \ - g_GLESFuncs.glDisable(cap_gl); \ - } \ - } - SYNC_CAPABILITY(DepthTest, GL_DEPTH_TEST); - SYNC_CAPABILITY(ColorLogicOp, GL_COLOR_LOGIC_OP); - SYNC_CAPABILITY(Dither, GL_DITHER); - SYNC_CAPABILITY(Multisample, GL_MULTISAMPLE); - SYNC_CAPABILITY(SampleAlphaToCoverage, GL_SAMPLE_ALPHA_TO_COVERAGE); - SYNC_CAPABILITY(SampleCoverage, GL_SAMPLE_COVERAGE); - SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK); - SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL); - SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD); - SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST); - SYNC_CAPABILITY(CullFace, GL_CULL_FACE); - -#undef SYNC_CAPABILITY - - // GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a - // 16-bit mask and not a "Enabled" bool the macro above could key off. ES - // has exactly one scissor rectangle and one scissor enable, so only bit 0 - the - // index every ES draw rasterizes against - can be forwarded; a program that - // enables the test for viewport 3 alone gets viewport 0's answer here. That is - // the same limitation as the unemulated gl_ViewportIndex on this backend and is - // why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt. - { - const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0; - const Bool syncedScissorTest = - (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; - if (forceFullPush || scissorTest != syncedScissorTest) { - scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - } - } - } - - if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) { - // gl_ClipDistance clipping is per-distance enable state in GL, and ES reaches it - // only through GL_EXT_clip_cull_distance. That extension reuses the desktop enum - // values for CLIP_DISTANCE0_EXT..7_EXT, but the token is absent from the ES - // headers this file compiles against, hence the local name. Without the - // extension there is nowhere to put the state and the shader could not have - // compiled either, so the whole block is gated rather than silently no-op'ing. - constexpr GLenum kClipDistance0 = 0x3000; - constexpr Uint kClipDistanceCount = 8; - const Uint32 mask = parameters.ClipDistanceEnabledMask; - const Uint32 syncedMask = g_syncedRenderStateParameters.ClipDistanceEnabledMask; - if (forceFullPush || mask != syncedMask) { - const Uint32 changed = forceFullPush ? ~0u : (mask ^ syncedMask); - for (Uint i = 0; i < kClipDistanceCount; ++i) { - const Uint32 bit = 1u << i; - if ((changed & bit) == 0) continue; - const GLenum cap = static_cast(kClipDistance0 + i); - (mask & bit) ? g_GLESFuncs.glEnable(cap) : g_GLESFuncs.glDisable(cap); - } - } - } - - { // sRGB framebuffer writes. GLES core always encodes a write into an sRGB attachment, - // while GL_FRAMEBUFFER_SRGB is disabled by default in desktop GL and the frontend - // never turns it on, so the driver has to be told to write raw. Without this a render - // into an sRGB colour buffer comes back encoded once too often (the shader's own - // decode on the next fetch then leaves the value one conversion short). - const Bool srgbWrites = MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); - if (g_GLESCapabilities.SupportsSrgbWriteControl && - (forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) { - srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB) - : g_GLESFuncs.glDisable(GL_FRAMEBUFFER_SRGB); - g_syncedSrgbFramebufferWrites = srgbWrites; - } - } - - if (tailSpanDirty) { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed - // all-ones value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to - // it. An arbitrary non-fixed restart index is rejected at draw time (see DrawElements). - const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled; - const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled || - g_syncedRenderStateParameters.PrimitiveRestartEnabled; - if (forceFullPush || restart != syncedRestart) { - restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX) - : g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); - } - } - - const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; }; - - // Which draw buffers the blend block below DECLINED (see it for why). Needed again at - // the shadow write-back at the end of this function: the span memcpy there clones the - // FRONTEND block, which for a declined draw buffer is not what the driver was handed. - Uint32 dualSourceDeclinedMask = 0; - - if (blendSpanDirty) { // Blend State - using FBO = MG_State::GLState::FramebufferObject; - auto& syncedStates = g_syncedRenderStateParameters.BlendStates; - - // Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with - // glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none. - // Detected at load and surfaced in the POST. There is no fallback that BLENDS - // correctly, so a draw that asks for a SRC1 factor on a driver without the extension - // gets the blend DECLINED: that draw buffer is pushed with blending off and neutral - // One/Zero factors, and the loss is logged once. The two rejected alternatives are - // both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise - // GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend - // against stale state), and throwing, which is what this did until now, takes the - // whole process down over one unsupported blend factor. Declining is defined, - // survivable and visible in the log. - // - // NOT gated on Enabled, deliberately, and the same way the Vulkan twin is not gated - // on effectiveBlendEnabled: what has to be kept away from the driver is the FACTOR - // ENUM, and the factor push below never consults Enabled - one glBlendFuncSeparate - // serves every draw buffer when they agree, and the per-index arm diffs factors - // alone. So `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` followed by any - // draw OR clear would otherwise hand a GL_SRC1_ALPHA to a driver that answers - // GL_INVALID_ENUM, leaving a spurious error in ITS queue for the next internal - // no-error probe to read as its own, and leaving this shadow recording factors the - // ES context rejected. Blending being off makes the picture unaffected; it does not - // make the enum acceptable. - const auto* effectiveBlendStates = ¶meters.BlendStates; - if (!g_GLESCapabilities.SupportsDualSourceBlend) { - Uint32 declinedWithBlendingOnMask = 0; - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - const auto& s = parameters.BlendStates[i]; - if (IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) || - IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha)) { - dualSourceDeclinedMask |= 1u << i; - if (s.Enabled) declinedWithBlendingOnMask |= 1u << i; - } - } - if (dualSourceDeclinedMask != 0) { - // Two masks in the message because they mean different things to whoever - // reads the log: the second one is where a PICTURE was lost. A draw buffer - // in the first mask but not the second had blending off anyway, so nothing - // was blended and nothing was dropped - only the unusable enum was kept out - // of the driver. - MGLOG_E_ONCE( - "SyncRenderState: a GL_SRC1_* (dual-source) blend factor was set on draw buffer " - "mask 0x%x, but the GLES driver does not expose GL_EXT_blend_func_extended (see " - "the dual-source blend row in the driver POST). Those draw buffers are pushed " - "with neutral One/Zero factors instead. Blending was actually ENABLED on mask " - "0x%x, and only there is anything lost: the fragment's first output is written " - "unblended and the second source is dropped.", - dualSourceDeclinedMask, declinedWithBlendingOnMask); - g_dualSourceDeclinedBlendStates = parameters.BlendStates; - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - if ((dualSourceDeclinedMask & (1u << i)) == 0) continue; - auto& s = g_dualSourceDeclinedBlendStates[i]; - // Both halves, for the same reason the Vulkan arm neutralises both: the - // enable so nothing blends against a source the driver cannot produce, - // the factors so no GL_SRC1_* enum is ever handed over. Clearing Enabled - // on a buffer that was already off is a no-op, which is what makes one - // ungated rule serve both cases. - s.Enabled = false; - s.SrcFactorRGB = BlendFactor::One; - s.DstFactorRGB = BlendFactor::Zero; - s.SrcFactorAlpha = BlendFactor::One; - s.DstFactorAlpha = BlendFactor::Zero; - } - effectiveBlendStates = &g_dualSourceDeclinedBlendStates; - } - } - // The rest of the block reads the EFFECTIVE state. The per-field writes it makes - // into `syncedStates` are provisional - the span memcpy at the end of this function - // overwrites the whole blend span with the frontend's own bytes - so the declined - // draw buffers are put back there, see the write-back below. - const auto& targetStates = *effectiveBlendStates; - - Bool allEnabled = true; - Bool allDisabled = true; - Bool anyCapDirty = forceFullPush; - - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - Bool enabled = targetStates[i].Enabled; - if (enabled) - allDisabled = false; - else - allEnabled = false; - - if (enabled != syncedStates[i].Enabled) { - anyCapDirty = true; - } - } - - if (anyCapDirty) { - if (allEnabled) { - g_GLESFuncs.glEnable(GL_BLEND); - for (auto& s : syncedStates) - s.Enabled = true; - } else if (allDisabled) { - g_GLESFuncs.glDisable(GL_BLEND); - for (auto& s : syncedStates) - s.Enabled = false; - } else { - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - if (forceFullPush || targetStates[i].Enabled != syncedStates[i].Enabled) { - syncedStates[i].Enabled = targetStates[i].Enabled; - syncedStates[i].Enabled ? g_GLESFuncs.glEnablei(GL_BLEND, i) - : g_GLESFuncs.glDisablei(GL_BLEND, i); - } - } - } - } - - Bool allFuncsSame = true; - Bool anyFuncDirty = forceFullPush; - const auto& first = targetStates[0]; - - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - const auto& cur = targetStates[i]; - const auto& syn = syncedStates[i]; - - Bool isDiffFromSyn = - (cur.SrcFactorRGB != syn.SrcFactorRGB || cur.DstFactorRGB != syn.DstFactorRGB || - cur.SrcFactorAlpha != syn.SrcFactorAlpha || cur.DstFactorAlpha != syn.DstFactorAlpha); - - if (isDiffFromSyn) anyFuncDirty = true; - - if (allFuncsSame && i > 0) { - if (cur.SrcFactorRGB != first.SrcFactorRGB || cur.DstFactorRGB != first.DstFactorRGB || - cur.SrcFactorAlpha != first.SrcFactorAlpha || cur.DstFactorAlpha != first.DstFactorAlpha) { - allFuncsSame = false; - } - } - } - - if (anyFuncDirty) { - if (allFuncsSame) { - g_GLESFuncs.glBlendFuncSeparate(MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorRGB), - MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorRGB), - MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorAlpha), - MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorAlpha)); - - for (auto& syn : syncedStates) { - syn.SrcFactorRGB = first.SrcFactorRGB; - syn.DstFactorRGB = first.DstFactorRGB; - syn.SrcFactorAlpha = first.SrcFactorAlpha; - syn.DstFactorAlpha = first.DstFactorAlpha; - } - } else { - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - const auto& cur = targetStates[i]; - auto& syn = syncedStates[i]; - - if (forceFullPush || cur.SrcFactorRGB != syn.SrcFactorRGB || - cur.DstFactorRGB != syn.DstFactorRGB || - cur.SrcFactorAlpha != syn.SrcFactorAlpha || - cur.DstFactorAlpha != syn.DstFactorAlpha) { - syn.SrcFactorRGB = cur.SrcFactorRGB; - syn.DstFactorRGB = cur.DstFactorRGB; - syn.SrcFactorAlpha = cur.SrcFactorAlpha; - syn.DstFactorAlpha = cur.DstFactorAlpha; - - g_GLESFuncs.glBlendFuncSeparatei( - i, MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorRGB), - MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorRGB), - MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorAlpha), - MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorAlpha)); - } - } - } - } - - Bool allEquationsSame = true; - Bool anyEquationDirty = forceFullPush; - - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - const auto& cur = targetStates[i]; - const auto& syn = syncedStates[i]; - - if (cur.ColorEquation != syn.ColorEquation || cur.AlphaEquation != syn.AlphaEquation) { - anyEquationDirty = true; - } - - if (allEquationsSame && i > 0) { - if (cur.ColorEquation != first.ColorEquation || cur.AlphaEquation != first.AlphaEquation) { - allEquationsSame = false; - } - } - } - - if (anyEquationDirty) { - if (allEquationsSame) { - g_GLESFuncs.glBlendEquationSeparate(MG_Util::ConvertBlendEquationToGLEnum(first.ColorEquation), - MG_Util::ConvertBlendEquationToGLEnum(first.AlphaEquation)); - - for (auto& syn : syncedStates) { - syn.ColorEquation = first.ColorEquation; - syn.AlphaEquation = first.AlphaEquation; - } - } else { - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - const auto& cur = targetStates[i]; - auto& syn = syncedStates[i]; - - if (forceFullPush || cur.ColorEquation != syn.ColorEquation || - cur.AlphaEquation != syn.AlphaEquation) { - syn.ColorEquation = cur.ColorEquation; - syn.AlphaEquation = cur.AlphaEquation; - - g_GLESFuncs.glBlendEquationSeparatei( - i, MG_Util::ConvertBlendEquationToGLEnum(cur.ColorEquation), - MG_Util::ConvertBlendEquationToGLEnum(cur.AlphaEquation)); - } - } - } - } - } - - if (tailSpanDirty) { // Depth state - if (forceFullPush || parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) { - g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc)); - } - if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) { - g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE); - } - if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) { - g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y()); - } - } - - if (tailSpanDirty) { // Stencil state - for (SizeT faceIndex = 0; faceIndex < parameters.StencilStates.size(); ++faceIndex) { - const StencilFaceState& current = parameters.StencilStates[faceIndex]; - const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex]; - const GLenum glFace = faceIndex == 0 ? GL_FRONT : GL_BACK; - - if (forceFullPush || current.Func != synced.Func || current.Ref != synced.Ref || - current.ValueMask != synced.ValueMask) { - g_GLESFuncs.glStencilFuncSeparate( - glFace, MG_Util::ConvertDepthTestFuncToGLEnum(current.Func), current.Ref, - current.ValueMask); - } - if (forceFullPush || current.WriteMask != synced.WriteMask) { - g_GLESFuncs.glStencilMaskSeparate(glFace, current.WriteMask); - } - if (forceFullPush || current.FailOp != synced.FailOp || - current.PassDepthFailOp != synced.PassDepthFailOp || - current.PassDepthPassOp != synced.PassDepthPassOp) { - g_GLESFuncs.glStencilOpSeparate( - glFace, MG_Util::ConvertStencilOperationToGLEnum(current.FailOp), - MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthFailOp), - MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthPassOp)); - } - } - } - - if (tailSpanDirty || colorMaskWidenDirty) { // Color mask. Uniform masks use the non-indexed glColorMask - // (works everywhere); divergent per-draw-buffer masks use the indexed glColorMaski when - // draw_buffers_indexed is available, otherwise fall back to broadcasting draw buffer 0. - // Mirrors the blend block. - using FBO = MG_State::GLState::FramebufferObject; - const auto& targetMasks = parameters.ColorMasks; - const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks; - - // What the DRIVER is told for draw buffer i. Identical to the application's mask - // except on a widened attachment during a draw, where alpha is forced off; the - // frontend's own array is never written, so glGet(GL_COLOR_WRITEMASK) keeps - // answering with the application's value. - const auto driverMask = [&](Uint i) -> BoolVec4 { - BoolVec4 m = targetMasks[i]; - if (i < 32 && (appliedWidenMask & (1u << i)) != 0) { - m.w() = false; - } - return m; - }; - - Bool anyDirty = forceFullPush || colorMaskWidenDirty; - Bool allSame = true; - const BoolVec4 driverMask0 = driverMask(0); - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - if (targetMasks[i] != syncedMasks[i]) anyDirty = true; - if (i > 0 && driverMask(i) != driverMask0) allSame = false; - } - - if (anyDirty) { - if (allSame || !g_GLESCapabilities.SupportsIndexedColorMask) { - // Without draw_buffers_indexed there is only one mask for the whole - // framebuffer, so a widened draw buffer 0 costs every other buffer its - // alpha writes. ES 3.2 makes glColorMaski core and ES 3.1 has it as - // EXT/OES; the only devices that reach this line are ES 3.0-class, where - // MRT with a mixed widened/native colour attachment set is already rare. - const BoolVec4& m = driverMask0; - g_GLESFuncs.glColorMask(ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()), - ToGLBoolean(m.w())); - } else { - const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski - : g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT - : g_GLESFuncs.glColorMaskiOES; - for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { - // colorMaskWidenDirty forces every slot: the previous push may have - // been the non-indexed glColorMask above (which set all of them), and - // the per-slot diff below only knows about the application's array. - if (forceFullPush || colorMaskWidenDirty || targetMasks[i] != syncedMasks[i]) { - const BoolVec4 m = driverMask(i); - colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()), - ToGLBoolean(m.w())); - } - } - } - } - g_syncedColorMaskAlphaWidenMask = appliedWidenMask; - } - - if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present. - // Without the extension the mode stays FILL and non-FILL requests are dropped. - if ((forceFullPush || parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront) && - g_GLESCapabilities.SupportsPolygonMode) { - const auto polygonModeFn = - g_GLESFuncs.glPolygonModeNV ? g_GLESFuncs.glPolygonModeNV : g_GLESFuncs.glPolygonModeANGLE; - polygonModeFn(GL_FRONT_AND_BACK, parameters.PolygonModeFront); - } - } - - if (tailSpanDirty) { // Clear values - if (forceFullPush || parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) { - const FloatVec4& clearCol = parameters.ClearColor; - g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w()); - } - if (forceFullPush || parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) { - g_GLESFuncs.glClearDepthf(parameters.ClearDepth); - } - if (forceFullPush || parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) { - g_GLESFuncs.glClearStencil(static_cast(parameters.ClearStencil)); - } - if (forceFullPush || parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) { - const FloatVec4& blendColor = parameters.BlendColor; - g_GLESFuncs.glBlendColor(blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w()); - } - } - - if (tailSpanDirty) { // Cull face mode - if (forceFullPush || parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) { - const CullFaceMode& cfm = parameters.CullFaceModeSetting; - g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm)); - } - if (forceFullPush || parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) { - const FrontFaceMode& ffm = parameters.FrontFaceModeSetting; - g_GLESFuncs.glFrontFace(MG_Util::ConvertFrontFaceModeToGLEnum(ffm)); - } - } - - if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and - // for the same reason: what has to reach the driver is NOT simply the parameter - // field. RenderStateParameters::ScissorBoxes starts all-zero, which means "the - // application has never called glScissor" - it is not a GL scissor box. GL's - // initial box is the whole window, which the frontend has no way to spell before a - // surface exists. The pre-resync code got away with pushing the field verbatim only - // by accident: the shadow held the same default, the field never compared unequal, - // and the ES context kept its own correct default. Under the forced full push that - // accident is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle, - // and everything drawn with GL_SCISSOR_TEST enabled before the app's first - // glScissor is clipped away - Minecraft 26.2 keeps only its unscissored sky and - // hand and loses the terrain and the whole GUI. - // - // The condition is the WRITTEN FLAG, not the extent. An empty rectangle is a - // perfectly legal thing to ask for - glScissor(0,0,0,0) means "the scissor test - // rejects every fragment" - so testing `width <= 0 || height <= 0` substituted the - // whole surface for a deliberately empty box and inverted the request into "accept - // every fragment", no matter how many times the application had already called - // glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is exactly that: all 16 - // boxes zero-sized with the test enabled, requiring the draw to be clipped away - // entirely. Reading the flag preserves the Minecraft protection bit-for-bit - before - // the first glScissor the bit is clear and the surface size is still substituted - - // while an explicit empty box now reaches the driver verbatim. Negative extents - // cannot arrive here at all: all three entry points reject them with - // GL_INVALID_VALUE before storing (GL_RenderState.cpp's ValidateNonNegativeExtent). - IntVec4 backendScissorBox = parameters.ScissorBoxes[0]; - if ((parameters.ScissorBoxWrittenMask & 1u) == 0) { - Int surfaceWidth = 0; - Int surfaceHeight = 0; - if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { - backendScissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight); - } - } - // Compared against what was actually PUSHED, not against the parameter field, so - // the resolved value and the diff can never disagree. - if (backendScissorBox != g_syncedBackendScissorBox) { - g_GLESFuncs.glScissor(backendScissorBox.x(), backendScissorBox.y(), backendScissorBox.z(), - backendScissorBox.w()); - g_syncedBackendScissorBox = backendScissorBox; - } - } - - if (tailSpanDirty) { // Logic op (first field of the tail span) - // glLogicOp is GLES 1.x / EXT only - eglGetProcAddress returns null for it on a - // plain ES 3.x driver. Before the forced resync it was reached only when an app - // actually set a logic op; now every MakeCurrent would call it, so the null check - // is mandatory rather than defensive. - if (g_GLESFuncs.glLogicOp && - (forceFullPush || parameters.LogicOp != g_syncedRenderStateParameters.LogicOp)) { - g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp)); - } - } - - if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below) - if (forceFullPush || parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor || - parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits || - parameters.PolygonOffsetClamp != g_syncedRenderStateParameters.PolygonOffsetClamp) { - // glPolygonOffset IS glPolygonOffsetClampEXT(factor, units, 0), so where the - // driver has the clamp entry point it carries all three values; without it the - // clamp cannot be expressed and the offset goes through unclamped. - if (g_GLESCapabilities.SupportsPolygonOffsetClamp) { - g_GLESFuncs.glPolygonOffsetClampEXT(parameters.PolygonOffsetFactor, - parameters.PolygonOffsetUnits, - parameters.PolygonOffsetClamp); - } else { - g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits); - } - } - } - - if (headSpanDirty) { // Clip control. GL_ARB_clip_control is only advertised where the - // driver has GL_EXT_clip_control, so without it the state can only be the default. - if (g_GLESCapabilities.SupportsClipControl && - (forceFullPush || parameters.ClipOrigin != g_syncedRenderStateParameters.ClipOrigin || - parameters.ClipDepthMode != g_syncedRenderStateParameters.ClipDepthMode)) { - g_GLESFuncs.glClipControlEXT(parameters.ClipOrigin, parameters.ClipDepthMode); - } - } - - if (headSpanDirty) { // Line width - if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) { - g_GLESFuncs.glLineWidth(parameters.LineWidth); - } - } - - if (headSpanDirty) { // Point size (GLES 1.x only - ES 2+ sets it from gl_PointSize, - // so the entry point is absent on most drivers; see the glLogicOp note above) - if (g_GLESFuncs.glPointSize && - (forceFullPush || parameters.PointSize != g_syncedRenderStateParameters.PointSize)) { - g_GLESFuncs.glPointSize(parameters.PointSize); - } - } - - if (tailSpanDirty) { // Sample coverage - if (forceFullPush || parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue || - parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) { - g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue, - ToGLBoolean(parameters.SampleCoverageInvert)); - } - } - - if (tailSpanDirty) { // Sample mask - if (g_GLESFuncs.glSampleMaski && - (forceFullPush || parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue)) { - g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue); - } - } - - if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core) - // Both halves are gated on the same entry point rather than on a version check: - // GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core / - // OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would - // only push an INVALID_ENUM into the driver's queue. This is NOT part of the - // SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to - // put a guard. - if (g_GLESFuncs.glMinSampleShading) { - if (forceFullPush || - parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) { - if (parameters.SampleShadingEnabled) { - g_GLESFuncs.glEnable(GL_SAMPLE_SHADING); - } else { - g_GLESFuncs.glDisable(GL_SAMPLE_SHADING); - } - } - if (forceFullPush || parameters.MinSampleShadingValue != - g_syncedRenderStateParameters.MinSampleShadingValue) { - g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue); - } - } - } - - if (tailSpanDirty && g_GLESCapabilities.SupportsFragmentShadingRate) { // GL_EXT_fragment_shading_rate - // The frontend stores the packed rate and op indices; the driver takes the enums. - // Only forwarded where the driver has the extension, which is also the only place - // the frontend advertised it - so a non-default value cannot exist anywhere else. - if (forceFullPush || parameters.ShadingRate != g_syncedRenderStateParameters.ShadingRate) { - const GLenum rate = MGUnpackShadingRate(parameters.ShadingRate); - g_GLESFuncs.glShadingRateEXT(rate != GL_NONE ? rate : GL_SHADING_RATE_1X1_PIXELS_EXT); - } - if (forceFullPush || - parameters.ShadingRateCombinerOp0 != g_syncedRenderStateParameters.ShadingRateCombinerOp0 || - parameters.ShadingRateCombinerOp1 != g_syncedRenderStateParameters.ShadingRateCombinerOp1) { - g_GLESFuncs.glShadingRateCombinerOpsEXT(MGShadingRateCombinerOpEnum(parameters.ShadingRateCombinerOp0), - MGShadingRateCombinerOpEnum(parameters.ShadingRateCombinerOp1)); - } - } - - g_syncedRenderStateVersion = currentRenderStateVersion; - // Byte copy, not member copy: it also clones the frontend struct's padding bytes, - // which is what lets the span memcmps above answer "unchanged" exactly instead of - // tripping on indeterminate padding every draw. Only the dirty spans need copying - - // a clean span's bytes are already identical by the very memcmp that skipped it. - auto* syncedBytesMut = reinterpret_cast(&g_syncedRenderStateParameters); - if (headSpanDirty) { - std::memcpy(syncedBytesMut, currentBytes, kBlendSpanBegin); - } - if (blendSpanDirty) { - std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin, - kBlendSpanEnd - kBlendSpanBegin); - // ...except for a draw buffer whose dual-source blend was DECLINED, where the - // frontend block is precisely what did NOT reach the driver. The shadow has to hold - // what was pushed or the next diff compares against state the ES context never got: - // going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides, - // the enable block finds nothing to do, and blending stays off from the decline. - // The span stays permanently "dirty" against the frontend as a result, which costs - // one memcmp plus this block per render-state VERSION change - the top-of-function - // version early-out still skips repeat draws entirely. - for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { - if ((dualSourceDeclinedMask & (1u << i)) == 0) continue; - g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i]; - } - } - if (tailSpanDirty) { - std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd, - sizeof(RenderStateParameters) - kBlendSpanEnd); - } - g_hasSyncedRenderState = true; - } - } // namespace RenderStateImpl - - namespace PrgramImpl { - // The twin SyncCurrentProgram resolved for the draw/dispatch being prepared, - // consumed by BindCurrentProgramWithResources and the post-draw draw-parameter - // updates in the same GL entry point — they all used to repeat the registry - // Find. Valid only while the keyed frontend program is the current draw - // program: every consumer compares the raw key against GetProgramForDraw() - // before trusting the twin, and SyncCurrentProgram rewrites the pair at the - // top of every PrepareForDraw/PrepareForCompute, so a recycled address can - // never be consumed (the stale pair is overwritten before any consumer runs). - static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr; - static BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr; - // P5e (pg), CONTRACT-P5E.md §5.5: THE SAME STASH, KEYED ON THE HANDLE. The raw frontend - // pointer above is one of the five frontend-keyed reads this family retires: every - // consumer compared it against GetProgramForDraw().get(), which under run-ahead is a - // pointer to an object the client may already have relinked or freed. The handle arm - // compares against MGPipeApplier().DrawProgram instead - a {slot, gen} the record - // carried, which cannot be recycled behind the server's back because the gen moves with - // the slot. - // - // BOTH HALVES SURVIVE because the two arms do (ruling 1): the pointer is the monolith - // glue's key and the handle is the transport's, and SyncCurrentProgram* clears the one - // it does not use so a stale key can never be consumed. - static MG_Pipe::MGPipeHandle g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle; - - // Memo of the per-draw enabled-draw-buffers walk feeding g_fragColorBroadcastCount: - // the answer is a pure function of WHICH FBO is bound and its draw-buffer edits, so - // it is keyed exactly like SyncCurrentFBO's synced trio - object pointer (identity), - // slot version (a recycled heap address cannot re-match: every real rebind bumps - // it), and object version (every attachment/draw-buffer/read-buffer edit bumps it, - // the same documented invariant the FBO sync memo already leans on). A null FBO - // keys on (nullptr, slot version, 0). - static const MG_State::GLState::FramebufferObject* g_broadcastMemoFbo = nullptr; - static Uint16 g_broadcastMemoSlotVersion = 0; - static Uint16 g_broadcastMemoObjectVersion = 0; - static Bool g_broadcastMemoValid = false; - static Uint g_broadcastMemoCount = 1; - - // P4a e1 (D-C4). The handle arm's key is the framebuffer record's ContentHash, and it - // is a COMPLETE key for this answer rather than a cheaper approximation of the trio - // above: the hash is taken over every field the record carries - Fbo included, so a - // recycled framebuffer handle cannot be suppressed against its predecessor's record, - // and DrawBuffers[8] included, so an unchanged hash provably means the draw-buffer - // array did not move, which provably means the broadcast count did not move. - // - // The ORDERING that made this read come from the frontend is preserved and is what - // makes reading a record legal here: the client emits every set_* BEFORE the verb and - // the server specialises AT the verb, so by the time this runs the record already - // describes the framebuffer this draw will use - which is exactly the property the - // frontend read was buying (a program compiled against a stale count would otherwise - // not be relinked until the draw after the one that needed it). - // - // Separate statics rather than reusing the trio's: FramebufferSerial goes from 0 to - // non-zero exactly once in a process's life, so both arms can run in one run, and one - // set of fields carrying two key shapes is how a stale half is compared against a live - // one. - static Uint64 g_broadcastMemoContentHash = 0; - static Bool g_broadcastMemoHandleValid = false; - static Uint g_broadcastMemoHandleCount = 1; - - // The identity+version key above is only monotonic WITHIN one GLContext: a - // library teardown + re-init frees every FramebufferObject and restarts the - // draw slot's counter at zero, so a recycled FBO address with coinciding - // fresh versions would false-hit. Cleared at the same boundaries as the - // structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache). - void InvalidateBroadcastMemo() { - g_broadcastMemoValid = false; - g_broadcastMemoHandleValid = false; - } - - // P4a e4 (D-H6, D-H7). The ShaderCso record behind a handle, WITH the composite band - // resolved: the band starts at kMGPipeShaderCsoCompositeSlotBase and the applier keeps - // it in a second dense table indexed by (slot - base), because one composite in the - // slot-indexed vector would grow it to ~983k records. The SERVER STILL NEVER LEARNS IT - // IS A COMPOSITE - a composite handle is an ordinary ShaderCso handle and every call - // names it as one; this is an indexing detail, and it lives in one place so that only - // one reader has to know it. - static const MG_Pipe::MGPipeShaderCsoRecord* FindShaderCsoRecord(MG_Pipe::MGPipeHandle handle) { - if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; - const auto& st = MG_Pipe::MGPipeApplier(); - const Vector* table = nullptr; - Uint32 index = 0; - if (MG_Pipe::MGPipeIsCompositeShaderSlot(handle.Slot)) { - table = &st.CompositeShaderCsos; - index = handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase; - } else { - table = &st.ShaderCsos; - index = handle.Slot; - } - // P4a decline-site P2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, and it - // TELLS THE TWO BANDS APART as the review requires. An out-of-range ORDINARY slot - // means the create_shader_state for this program never arrived - a client seam. - // An out-of-range COMPOSITE index is a different finding entirely: that table is - // dense and its band base is a fixed contract constant, so a handle above its end - // is a mis-decoded slot, i.e. a CONTRACT-side bug, and saying "the record has not - // arrived" about it would send the reader to the wrong package. - if (index >= table->size()) { - if (MG_Pipe::MGPipeIsCompositeShaderSlot(handle.Slot)) { - MGLOG_E_ONCE("A program record does not describe the binding it names: composite " - "ShaderCso {slot %u, gen %u} decodes to band index %u but the " - "composite table holds %llu - the band base or the decode is wrong, " - "not the emission; running the frontend's own uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - static_cast(index), - static_cast(table->size())); - } else { - MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso " - "{slot %u, gen %u} is above the applier's table of %llu record(s), so " - "no create_shader_state for it has been applied; running the " - "frontend's own uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - static_cast(table->size())); - } - return nullptr; - } - const MG_Pipe::MGPipeShaderCsoRecord& record = (*table)[index]; - // P4a decline-site P3: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, and it - // is ID-8's stale-generation refusal: every GetOrCreate(handle) in the phase owes - // one. On this tree a bound program whose record is dead, or whose generation has - // moved under the handle, is a death/recycle seam - the emitter kept handing out - // a handle it had retired, or minted a successor at the slot and never described - // it - and the two causes are named apart because they point at different halves - // of the client. - if (!record.Live || record.Gen != handle.Gen) { - MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso " - "{slot %u, gen %u} finds a %s record (generation %u); running the " - "frontend's own uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - record.Live ? "live but differently-generationed" : "dead", - static_cast(record.Gen)); - return nullptr; - } - return &record; - } - - // The record whose GLOBAL CONSTANTS may be uploaded for this program, or null. - // - // Four things have to hold before the default uniform block is taken off a record - // rather than off the frontend program, and each of them is a way the two could - // legitimately disagree on a tree whose client half is mid-landing: - // - // * the program has a handle at all, and the record at it is live; - // * the record's own Desc names that same handle - the identity check, and the seam - // where a mis-keyed emission becomes visible instead of becoming a wrong upload; - // * the version is not the ~0u NEVER-UPLOADED SENTINEL, which the client must never - // emit and which is what the record starts at; - // * the block image is at least as long as the program says the block is, because - // the upload copies GetUBOSize() bytes and a short record would read past it. - // - // Anything else and the frontend's own MapUBO answers, exactly as it does today. - // P5e (pg): `uboSize` is what the block image is checked against, and it is a PARAMETER - // now rather than a `program->GetUBOSize()` read inside the body - on the handle arm the - // size is the record's own Desc.GlobalUboSize and there is no ProgramObject to ask. - // `program` is null on that arm and is used for nothing but the monolith identity - // resolution below. - static const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecordForHandle( - MG_Pipe::MGPipeHandle handle, Uint uboSize) { - const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(handle); - // P4a decline-site P4: S - FOLDED INTO P2/P3 AT THE VERIFICATION ROUND, which is - // what the review asked for and is why this one stays silent: both reasons - // FindShaderCsoRecord can answer null have just spoken for themselves, with the - // band and the generation named. A third line here would say less and fire on - // every one of them. - if (record == nullptr) return nullptr; - // P4a decline-site P5: S - THE PROGRAM SEAM, loud (MAJOR-3) and in the same - // words as the framebuffer and image seams, so one grep polices all three. No - // further flip. Unlike the image seam this one has no eager funnel to be wrong - // at: the only caller is the global-UBO upload at the draw validate point, where - // the record for THIS draw has been applied, so a descriptor that names a - // different handle than the slot it sits in is always a mis-keyed emission. - if (record->Desc.Cso != handle) { - MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso " - "record at {slot %u, gen %u} names {slot %u, gen %u} in its own " - "descriptor; running the frontend's own uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - static_cast(record->Desc.Cso.Slot), - static_cast(record->Desc.Cso.Gen)); - return nullptr; - } - // P4a decline-site P6: M, then S - FLIPPED AT THE VERIFICATION ROUND, and the - // condition the review names is already the CALLER'S, so the test is not repeated - // here. The ~0u NEVER-UPLOADED SENTINEL is what the record starts at and is - // legitimate before the first set_global_constants for a program. But the only - // caller of this function is the global-UBO upload, which runs inside - // `currentProgram->GetUBOSize() > 0 && backendProgram.HasGlobalUboBlock()` - so - // reaching this line at all means bit 12 is on, this program HAS a default - // uniform block, and it is being drawn with no global constants ever emitted for - // it. That is the seam defect, and it says so. - if (record->GlobalConstantsVersion == ~Uint32{0}) { - MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso " - "{slot %u, gen %u} still carries the never-uploaded sentinel at a draw " - "of a program with a %u-byte default uniform block; running the " - "frontend's own uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - static_cast(uboSize)); - return nullptr; - } - // P4a decline-site P7: S, and LOUD (ID-19) - this one is not a missing record, - // it is PROTOCOL CORRUPTION. The upload copies GetUBOSize() bytes out of - // GlobalConstants, so a shorter block image is a read past the end of the - // applier's own buffer; no correct emitter can produce it, and quietly handing - // the draw back to MapUBO() would file a memory-safety bug as a perf regression. - // - // THE VERDICT IS THE ONE THE CONTRACT ALREADY SPELLS, and deliberately not a new - // one: PipeApply.cpp's "the verdict of every trip wire in this file, in one - // place" says a poison or verify build STOPS and writes the Fatal{} marker G4 - // greps the retrace logs for, while a shipped push build logs at error level and - // carries on from a defined state. So the gate lanes - which is where a seam - // defect has to be caught - never reach MapUBO() at all, and a shipped build does - // not abort a running game over a client-side bug. If the integrator wants the - // stop on every arm, it is one #if away and this comment is where to say so. - if (record->GlobalConstants.size() < static_cast(uboSize)) { -#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY - MGLOG_F("MGPipe: Fatal{ProtocolCorruption} " - "A program record does not describe the binding it names: the ShaderCso " - "record {slot %u, gen %u} carries a %llu-byte global-constant block for a " - "%llu-byte default uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - static_cast(record->GlobalConstants.size()), - static_cast(uboSize)); - std::abort(); -#else - MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso " - "record {slot %u, gen %u} carries a %llu-byte global-constant block for a " - "%llu-byte default uniform block; running the frontend's own uniform block.", - static_cast(handle.Slot), static_cast(handle.Gen), - static_cast(record->GlobalConstants.size()), - static_cast(uboSize)); -#endif - return nullptr; - } - return record; - } - - // Read from the frontend rather than from the backend framebuffer sync, which - // only runs later in PrepareForDraw: a program compiled against a stale count - // would not be relinked until the draw after the one that needed it. - // - // P5e (pg): ITS OWN FUNCTION AND ITS OWN POLICY, because the three callers want three - // different answers when the record arm has nothing to say: - // - // FrontendFallback - the monolith arm, unchanged: read the draw-FBO binding slot. - // RefuseFallback - the handle arm AT A DRAW. With a live wire that binding slot is a - // BARRIER_PULLED row the client may already have moved, so a count - // read from it would compile the program against a framebuffer that - // is not the one being drawn to - silently, and only sometimes. - // The record is the answer or there is no answer (rule F). - // RecordOnly - the handle arm AT A DISPATCH. A compute program has no fragment - // stage and a dispatch has no draw framebuffer, so this count is not - // an input to what is being built; PrepareForCompute also does not - // run SyncCurrentFBO, so the framebuffer records are legitimately - // untrusted here and a refusal would abort a correct program. The - // previous resolution stands, which is what the monolith arm - // effectively does too when nothing rebound between the two calls. - enum class BroadcastCountPolicy { FrontendFallback, RefuseFallback, RecordOnly }; - - static void ResolveFragColorBroadcastCount(BroadcastCountPolicy policy) { - Bool broadcastCountResolved = false; - // The trust latch is fresh here and costs nothing: PrepareForDraw runs - // SyncCurrentFBO immediately before this, so the records have just been - // checked against the two bindings. - if (FramebufferImpl::g_fboRecordsTrusted) { - const MG_Pipe::MGPFramebufferState* const recordPtr = - BoundFramebufferRecord(FramebufferTarget::Draw); - // P4a decline-site F7: FLIPPED AT THE VERIFICATION ROUND from a silent - // decline to an assertion, because it is unreachable and a silent decline - // here is indistinguishable from a legitimate one. g_fboRecordsTrusted - // implies a record with a non-null handle on BOTH targets: - // SyncCurrentFBOByRecord sets the latch only after F2 has rejected a null - // pointer or handle on either, and it runs immediately before this in - // PrepareForDraw. Kept as a checked decline rather than deleted so a - // release build cannot dereference null if that ordering ever changes. - MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)); - if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) { - const MG_Pipe::MGPFramebufferState& record = *recordPtr; - if (!g_broadcastMemoHandleValid || g_broadcastMemoContentHash != record.ContentHash) { - Uint enabledDrawBuffers = 0; - for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { - // -1 is FramebufferAttachmentType::None on the wire; every - // other value is an attachment index. - if (record.DrawBuffers[i] >= 0) { - enabledDrawBuffers = i + 1; - } - } - g_broadcastMemoContentHash = record.ContentHash; - g_broadcastMemoHandleCount = std::max(enabledDrawBuffers, 1); - g_broadcastMemoHandleValid = true; - } - g_fragColorBroadcastCount = g_broadcastMemoHandleCount; - broadcastCountResolved = true; - } - } - if (policy == BroadcastCountPolicy::RecordOnly) return; - if (policy == BroadcastCountPolicy::RefuseFallback && !broadcastCountResolved) { - // The record arm declined under a transport. g_fboRecordsTrusted is set by - // SyncCurrentFBOByRecord, which PrepareForDraw runs immediately before this, - // so reaching here means the framebuffer family's own records are missing or - // untrusted - and the frontend fallback below is exactly the read this phase - // exists to retire. - MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, " - "\"GetFramebufferBindingSlot@SyncCurrentProgram\"} - the draw " - "framebuffer's record did not answer the fragColor broadcast count on " - "the handle arm, and the frontend binding slot is a row the client owns"); - std::abort(); - } - if (!broadcastCountResolved) { - const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw); - const auto& drawFBO = drawSlot.GetBoundObject(); - const Uint16 slotVersion = drawSlot.GetVersion(); - const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0; - if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() || - g_broadcastMemoSlotVersion != slotVersion || - g_broadcastMemoObjectVersion != objectVersion) { - Uint enabledDrawBuffers = 0; - if (drawFBO) { - const auto& drawBuffers = drawFBO->GetDrawBuffers(); - for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { - if (drawBuffers[i] != FramebufferAttachmentType::None) { - enabledDrawBuffers = i + 1; - } - } - } - g_broadcastMemoFbo = drawFBO.get(); - g_broadcastMemoSlotVersion = slotVersion; - g_broadcastMemoObjectVersion = objectVersion; - g_broadcastMemoCount = std::max(enabledDrawBuffers, 1); - g_broadcastMemoValid = true; - } - g_fragColorBroadcastCount = g_broadcastMemoCount; - } - } - - // P5e (pg), CONTRACT-P5E.md §5.5: THE SAME SYNC, FROM THE HANDLE THE RECORD CARRIED. - // Two overloads and not an `#if` inside one body (ruling 1 / ID-81): the frontend one - // above is visibly the monolith-glue half, this one names no frontend object at all. - // - // WHAT THE NINE-CLAUSE CONDITION BECOMES, and the clause COUNT does not shrink - its - // inputs move (D-H5, §5.5): - // GetLinkVersion() -> record.Serial vs GetSyncedShaderCsoSerial() - // GetImageUnitVersion() -> record.BindingsSerial vs GetSyncedBindingsSerial() - // ComputeShaderStorage...() -> record.Signature - // GetLinkStatus/SpirvStatus -> Desc.LinkStatus / Desc.SpirvStatus, above - // and the clamp masks, the broadcast count, ImageUnitFormatsStillMatch and the three - // patch clauses are unchanged - they are backend globals, fb's row and VALUE rows of the - // residual block, none of which this family owns. - void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - - g_currentDrawFrontendProgram = nullptr; - g_currentDrawBackendProgram = nullptr; - g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle; - - const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(cso); - // THE SAME THREE-PART TEST THE FRONTEND ARM MAKES, from the descriptor. LinkStatus - // is a field rather than an implication (ID-88): 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, so "a record exists" and "the program linked" are different - // statements. A program the frontend reports unlinked draws nothing, which is - // exactly what the monolith arm does with GetLinkStatus() == false. - if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { - g_GLESFuncs.glUseProgram(0); - g_lastUsedBackendProgramId = 0; - return; - } - // REFUSED at a draw, record-only at a dispatch: see the policy's own note. - ResolveFragColorBroadcastCount(forDraw ? BroadcastCountPolicy::RefuseFallback - : BroadcastCountPolicy::RecordOnly); - - BackendProgramObjectImpl* const twin = ResolveProgramTwin(cso); - if (twin == nullptr) { - // ResolveProgramTwin has already named the handle. A draw with no twin binds - // nothing, which is the visible no-op Use() makes of an unusable program rather - // than a draw with somebody else's shader. - g_GLESFuncs.glUseProgram(0); - g_lastUsedBackendProgramId = 0; - return; - } - if (!twin->GetBackendProgramId() || - twin->GetSyncedShaderCsoSerial() != record->Serial || - twin->GetSyncedBindingsSerial() != record->BindingsSerial || - twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask || - twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask || - twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount || - twin->GetShaderStorageBlockBindingSignature() != record->Signature || - !twin->ImageUnitFormatsStillMatch() || - (twin->GetPassthroughTessControlPatchVertices() >= 0 && - (twin->GetPassthroughTessControlPatchVertices() != - static_cast(MG_Pipe::gPipeInputs->GetPatchVertices()) || - !BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(), - MG_Pipe::gPipeInputs->GetPatchDefaultOuterLevel()) || - !BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(), - MG_Pipe::gPipeInputs->GetPatchDefaultInnerLevel())))) { - twin->SyncToBackendByHandle(cso); - } - g_currentDrawProgramHandle = cso; - g_currentDrawBackendProgram = twin; - } - } // namespace PrgramImpl - - // Defined with the surface registry, below. - static void FlushAheadOfWindowBufferUse(); - - void BindCurrentFBO(FramebufferTarget target) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // The handle arm reads the record instead of the binding slot, which is one of the - // five GetFramebufferBindingSlotChecked call sites this phase retires. Everything the - // pre-handle arm below is careful about is careful here for the same reasons and in - // the same order: NO FAST PATH on any version (the skip is BindFramebufferId's job and - // its shadow is where the cost actually is), and the DEFAULT framebuffer is bound - // THROUGH THE SHADOW, never raw. What changes is only where the two questions - "which - // framebuffer" and "is it the default one" - are answered: the record's Fbo and its - // IsDefault byte, rather than the bound object's address and a comparison against - // pDefaultFramebufferInfo->defaultFBO. - if (FramebufferImpl::g_fboRecordsTrusted) { - const MG_Pipe::MGPFramebufferState* const recordPtr = BoundFramebufferRecord(target); - // The record's own handle is the "has this binding ever been described" test, not - // FramebufferSerial - which MGPipeApplierReset advances whether or not anything - // was ever emitted (see SyncCurrentFBOByRecord). The trust latch above is the - // other half: it says the sync that ran a moment ago found these two records - // describing these two bindings. - // P4a decline-site F5: FLIPPED AT THE VERIFICATION ROUND to an assertion, same - // argument and same shape as F7 above - the trust latch implies a record with a - // non-null handle on both targets, and a silent decline here would look exactly - // like a legitimate one. - MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)); - if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) { - const MG_Pipe::MGPFramebufferState& record = *recordPtr; - if (record.IsDefault == 0) { - auto* twinEntry = FramebufferImpl::g_backendFramebufferObjects.FindByHandle(record.Fbo); - // P4a decline-site F6: S - already loud, and it KEPT this shape at the - // verification round: binding nothing is exactly what the pre-handle arm - // does in the same situation, so the two arms stay at parity. - if (twinEntry && *twinEntry) { - (*twinEntry)->Bind(target); - } else { - MGLOG_E_ONCE( - "No backend FBO found (maybe not synced) for the current %s FBO record, " - "cannot bind FBO.", - (target == FramebufferTarget::Read ? "READ" : "DRAW")); - } - } else { - MGLOG_D("Binding default framebuffer as %s FBO", - (target == FramebufferTarget::Read ? "READ" : "DRAW")); - if (FramebufferImpl::CurrentFramebufferBinding(target) != 0) FlushAheadOfWindowBufferUse(); - // Through the shadow: a raw bind here would leave the shadow claiming - // the previous user FBO, false-skipping its next re-bind. - FramebufferImpl::BindFramebufferId( - target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); - } - return; - } - } - // P5e (fb, §5.4): the pre-handle arm below reads the binding slot, so under a transport - // reaching it is the same refusal SyncCurrentFBO takes. Reaching it means either the - // sync's trust latch is clear - and the sync aborted before it could be - or this bind - // ran with no sync in front of it, which the latch exists to catch. - RefuseFramebufferBindingSlotRead(); - } - - // P5c (hd, CONTRACT-P5C §3.2/§3.3): the handle-keyed form of SyncAndBindFramebufferObject. - // With an active transport a framebuffer reaches the server as the handle a record - // carried - the named blit's ReadFbo/DrawFbo, or the applier's own bound handle for a - // binding restore - and the client's binding slots and slot allocator are never probed - // (T2/T3). The twin is resolved (or minted) BY HANDLE; its sync is keyed on THIS handle's - // applier record through m_pushedSyncHandle; and the frontend object the sync body still - // walks comes from the table's own state note (the object-class channel P3b/P4b retires), - // never from a client binding slot. A twin with no noted object was never synced through - // any binding - the DSA-on-a-never-bound-framebuffer case, which the split path already - // refuses for the named clears - and is loud rather than silently unconfigured. - void SyncAndBindFramebufferByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget target, - Bool forceSync = false) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (MG_Pipe::MGPipeHandleIsNull(fbo) || fbo == MG_Pipe::kMGPipeDefaultFramebuffer) { - // Same reset as the object form's default branch, for its reason. - if (target == FramebufferTarget::Draw) { - FramebufferImpl::g_alphaWidenedDrawBufferMask = 0; - FramebufferImpl::g_integerColorDrawBufferMask = 0; - } - if (FramebufferImpl::CurrentFramebufferBinding(target) != 0) FlushAheadOfWindowBufferUse(); - FramebufferImpl::BindFramebufferId( - target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); - return; - } - - auto& registry = FramebufferImpl::g_backendFramebufferObjects; - auto* twinSlot = registry.GetOrCreateByHandle(fbo); - if (twinSlot == nullptr) { - MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} is refused by the twin slot table " - "(live generation %u); nothing is bound for the %s target", - fbo.Slot, fbo.Gen, registry.LiveGenAt(fbo.Slot), - target == FramebufferTarget::Read ? "READ" : "DRAW"); - return; - } - auto& backendObj = *twinSlot; - if (!backendObj) { - backendObj = MakeShared(); - } - backendObj->m_pushedSyncHandle = fbo; - // P5e (fb, CONTRACT-P5E.md §5.4): THE STATE NOTE IS GONE. The paragraph above described - // a handle arm that still had to find "the frontend object the sync body walks", and - // its failure mode - a framebuffer named by a DSA entry point that was never bound, so - // no note exists and the twin is bound unconfigured - was the last consequence of that - // detour. SyncToBackendByHandle configures the twin from the RECORD, which every named - // framebuffer has by construction (a Named record precedes every DSA site, ID-19), so - // the never-bound case is now configured correctly rather than named and refused. - if (forceSync) { - backendObj->InvalidateSyncedState(); - } - backendObj->SyncToBackendByHandle(fbo, target); - backendObj->Bind(target); - } - - void ForceBindCurrentFBO(FramebufferTarget target) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // P5c (hd): with an active transport the bound framebuffer's handle is the applier's - // own working state; the object form's registry probe never runs (T2). - // - // P5e (fb, CONTRACT-P5E.md §5.4): AND THE BINDING SLOT IS READ INSIDE THE ELSE, NOT - // BEFORE THE IF. The read and the three stamps below used to straddle this arm - S3's - // finding #7 - so the handle path still touched a BARRIER_PULLED row on both sides of - // the one branch that existed to avoid it. The stamps are the LEGACY memo's - // (g_fboSynced*), and skipping them here is safe for exactly the reason the serial memo - // below is invalidated rather than stamped: nothing on this path consulted the frontend - // trio, so the honest thing is to leave it invalid and pay one extra sync. Under a - // transport the legacy arm is never taken anyway (SyncCurrentFBO refuses first), so the - // memo it feeds has no reader. - SyncAndBindFramebufferByHandle( - MG_Pipe::MGPipeApplier().BoundFramebuffer[static_cast( - target == FramebufferTarget::Read ? MG_Pipe::MGPipeFramebufferTarget::Read - : MG_Pipe::MGPipeFramebufferTarget::Draw)], - target); - // P4a decline-site F9 / MINOR-3: THE HANDLE ARM'S MEMO IS INVALIDATED HERE, NOT - // STAMPED, and it is done per target for the reason the memo is kept per target at - // all - this entry point syncs ONE binding and must not say anything about the other. - // - // A stamp would claim "this target reflects the applier's RECORD as of this serial", - // and nothing on this path consulted the record: SyncAndBindFramebufferObject above - // syncs the FRONTEND object out of the binding slot. I could not turn the stamp into - // a wrong-pixel scenario (any edit to the framebuffer moves its ContentHash and hence - // the serial), but it is an invariant break with no demonstrable failure, and the - // honest form costs at most one extra sync on the next draw. - FramebufferImpl::InvalidateSyncedFramebufferSerial(target); - } - - static void BindCurrentProgramWithResources( - const SharedPtr& currentProgram, - const TextureImpl::DrawTextureSyncKeys& keys, MG_Pipe::MGPipeHandle programCso); - static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, - const SharedPtr& currentProgram); - - void PrepareForDraw(DrawSyncFlags syncBit) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // One twin resolve serves the whole draw: the buffer sync (which hosts the - // resolved-buffers memo on the twin), the VAO sync and the draw-time bind - // below. Nothing in between can invalidate it — the bound VAO is pinned by - // the context, and no step here erases or replaces a live VAO's twin. - // - // P5e (vi), CONTRACT-P5E §4.2 / §5.1: under a transport the twin is resolved from - // st.BoundVertexElements - THE SAME HANDLE the frontend probe used to mint, since - // EmitVertexElements acquires it off the VAO's own lifetime id - and neither the bound - // VAO row nor the configuration version is read at all. `currentVAO` stays null on that - // arm, which is what the two callees' record arms expect; the null twin case (no - // vertex-elements CSO bound) reaches the same BindBackendVAOId(0) below that a null - // frontend VAO always did. - const SharedPtr noFrontendVao; - const auto& currentVAO = noFrontendVao; - VertexArrayImpl::BackendVertexArrayObject* vaoTwin = nullptr; - Uint32 vaoConfigVersion = 0; - vaoTwin = VertexArrayImpl::ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements); - if (vaoTwin != nullptr) vaoTwin->AdoptUnchangedBufferSets(MG_Pipe::MGPipeApplier()); - // One program resolve and one texture-key capture serve the whole draw, for - // the same reason the twin resolve does: only frontend GL entry points move - // either, and none can run inside this preparation. GetProgramForDraw is a - // cross-TU call with a guarded static inside - repeating it per stage showed - // up in draw-loop profiles. - // - // P5e (pa), kimi row 91 / S4 1.1: AND ON THE RECORD ARM IT IS NOT MADE AT ALL. The row - // this package retires is this single call - it is what MGP_INPUT_CHECK trips on, once - // per draw, and it is 61 of the strict lane's 109 red entries. The arm is - // DrawProgramFromRecords(), which names every consumer of the value rather than this - // family alone; `currentProgram` stays null on it and the four callees below take their - // own handle arms, none of which consults it. The null twin case (no program bound) - // reaches the same "nothing to bind" answers a null frontend program always did. - const SharedPtr noFrontendProgram; - const auto& currentProgram = noFrontendProgram; - if (MG_Util::PipeStats::Enabled()) { - // THE per-draw denominator for Espryt, plus this function's own two accessor - // calls (the VAO and the draw program). Everything the callees below read is - // counted by the callees that are instrumented; the rest is not counted (see - // the inventory in PipeStats.cpp). - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); - // ZERO on the record arm, one per row still pulled otherwise: P5e (vi) retired the - // bound-VAO accessor call and P5e (pa) the draw-program one, and the ledger has to - // say so or the phase's own "accessor calls per draw" number would keep counting - // reads that no longer happen. Two independent bits, so it is a sum and not a - // three-way pick. - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, - (0) + (0)); - } - const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys(); - - BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion, - syncBit & DrawSyncBit::IndexBuffer, - syncBit & DrawSyncBit::IndirectBuffer); - VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin); - TextureImpl::SyncNeccessaryTextures(textureKeys); - // A draw reads and writes through its image units too, so the unit bindings have to be - // as current as the sampled ones. Gated (see the sweep): a program with no image binding - // pays one integer test, and one with images re-issues them only when a texture shape - // moved under them. - TextureImpl::SyncImageTextureBindingsForDraw(textureKeys); - // A draw writes through its image units too - the conformance case that found this - // stores into a buffer texture from the FRAGMENT stage, not from a dispatch. - TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); - FramebufferImpl::SyncCurrentFBO(); - // P5e (pg), kimi row 91 / S4 1.1: the SharedPtr pull above is the row this family - // retires. On the handle arm the program is MGPipeApplier().DrawProgram - a handle the - // set_draw_program record carried - and `currentProgram` is not consulted for the sync - // at all; the remaining uses of it below are the two callees' own, and both have a - // handle arm of their own. P5e (pa) finished it: the pull itself is gone on that arm and - // `currentProgram` is a null SharedPtr there, which this branch never reaches for. - PrgramImpl::SyncCurrentProgramByHandle(MG_Pipe::MGPipeApplier().DrawProgram, true); - RenderStateImpl::SyncRenderState(); - - BindCurrentFBO(FramebufferTarget::Draw); - - { -#ifdef TRACY_ENABLE - ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND); -#endif - if (vaoTwin) { - vaoTwin->Bind(); - } else { - VertexArrayImpl::BindBackendVAOId(0); - } - } - - // P5e (pa): the third consumer, on ITS OWN arm and not on the hoist's. The hoist above is - // a conjunction over four callees; this one is served by the record whenever the PROGRAM - // family is on the handle arm, which includes the mixed A/B where the texture bits are - // off and `currentProgram` above is therefore still a real object. - VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, MG_Pipe::MGPipeApplier().DrawProgram); - - BindCurrentTextures(textureKeys, currentProgram); - BindCurrentProgramWithResources(currentProgram, textureKeys, - MG_Pipe::MGPipeApplier().DrawProgram - ); - - // Last: opening the capture span needs the program current and the capture - // buffers bound, and ES rejects most binding changes once it is open. - XfbImpl::StartPendingTransformFeedback(); - } - - // Resolves every frontend texture unit's textures onto the backend context. Returns - // false when the resolution could not be completed from the state it read - a bound - // texture that has no backend object yet is skipped, and a later draw would bind it - // without any of the memo keys below moving - so the caller must not memoise it. - // - // P5e (pa): `currentProgram` IS THE LEGACY WALK'S PARAMETER and is null under - // TextureImpl::UnitTexturesByHandle(), where the by-handle pass below returns before the - // only reader of it (sampledTargetForUnit) is even declared. The caller drops the object - // only once that same test holds, so this is the arm speaking and not a pointer test. - static Bool ResolveAndBindUnitTextures(const SharedPtr& currentProgram, - Int maxTouchedUnit) { -#ifdef TRACY_ENABLE - ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND); -#endif - Bool fullyResolved = true; - // P5e (tx2), CONTRACT-P5E §5.2. ONE PASS OVER THE SAMPLER-VIEW WINDOW, AND THEN AN - // UNBIND OF EVERY TARGET THE WINDOW DID NOT CLAIM. - // - // The three-pass slot walk below exists to arbitrate: desktop 1D/1D-array targets alias - // ES 2D/2D-array, a unit can hold a real texture on one of an aliased pair and a default - // object on the other, and two REAL textures can want one native target - which the - // program's sampler types decide. THE CLIENT HAS ALREADY DECIDED ALL OF IT - // (SamplerEmit.h's per-unit resolution is keyed on exactly those sampler types), so the - // arbiter dies here rather than being re-implemented over records: one texture per unit, - // named by the record, with its frontend target in the view CSO. - // - // The unbind half keeps its shape and changes its driver: instead of walking the frontend - // unit's binding slots it walks the fixed TextureTarget enum against g_boundTexturesCache, - // which is SERVER memory and is the shadow every redundant-bind filter in this backend - // already trusts. That is what makes "the window did not claim it" mean "unbind it" - the - // same statement the pre-handle pass makes with `!boundBackendTargets[...]`. - const auto& st = MG_Pipe::MGPipeApplier(); - const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount; - for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { - const Uint32 index = static_cast(unit); - Array boundBackendTargets{}; - if (index >= st.SamplerViewStart && index < windowEnd && index < st.BoundSamplerViews.size()) { - const auto& view = st.BoundSamplerViews[index]; - if (!MG_Pipe::MGPipeHandleIsNull(view.Texture)) { - // The TARGET is the view CSO's, which is the frontend target the client - // resolved this unit against - not a second derivation from the texture's - // descriptor, which would be a second authority for one value. - const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); - const auto target = viewRecord != nullptr - ? static_cast(viewRecord->View.Target) - : TextureTarget::Unknown; - if (viewRecord == nullptr) { - MGLOG_E_ONCE("MGPipe: unit %d's sampler view {%u, %u} has no applier record, " - "so the target to bind texture {%u, %u} at is unknown; the unit " - "keeps what it holds", - static_cast(unit), view.View.Slot, view.View.Gen, - view.Texture.Slot, view.Texture.Gen); - fullyResolved = false; - } else if (!TextureImpl::IsSupportedTextureTarget(target)) { - MGLOG_D(" Texture target %s is not supported, skipping.", - MG_Util::ConvertTextureTargetToString(target).c_str()); - } else if (auto* twin = TextureImpl::ResolveTextureTwin(view.Texture)) { - const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); - twin->Bind(targetGL, unit); - boundBackendTargets[static_cast( - TextureImpl::MapToBackendTextureTarget(target))] = true; - } else { - // No twin yet: the sync pass that would have built it declined and - // named why. Not memoisable - a later draw would bind it without any - // key moving. - fullyResolved = false; - } - } - } - // The backend half of glBindTexture(..., 0) for every native target this unit no - // longer claims, driven by the server's own binding shadow. - Array visitedBackendTargets{}; - for (SizeT t = 0; t < (SizeT)TextureTarget::TextureTargetCount; ++t) { - const auto target = static_cast(t); - if (!TextureImpl::IsSupportedTextureTarget(target)) continue; - const auto backendTargetIndex = - static_cast(TextureImpl::MapToBackendTextureTarget(target)); - if (visitedBackendTargets[backendTargetIndex]) continue; - visitedBackendTargets[backendTargetIndex] = true; - if (boundBackendTargets[backendTargetIndex]) continue; - if (TextureImpl::g_boundTexturesCache[static_cast(unit)][backendTargetIndex] == nullptr) { - continue; - } - TextureImpl::UnbindTexture(unit, TextureImpl::ConvertTextureTargetToBackendGLEnum(target)); - } - } - return fullyResolved; - } - - // Per-unit memo of the sampler-registry lookup, NOT of the resulting unit binding - // (BackendSamplerObject::Bind already dedups against g_boundSamplersCache). The registry - // pairs a live frontend sampler with one backend object for the frontend's whole - // lifetime: GetOrCreate replaces a pairing only when the frontend expired, entries are - // erased only when the frontend expired, and a unit's sampler cannot expire while the - // unit holds it - so owner-equality of the unit's current sampler against the snapshot - // proves the cached pointer is exactly what Find would return. A miss result is never - // cached: the backend object may not exist yet when the unit pass runs (the program - // pass creates it later in the same draw), and a cached miss would keep skipping the - // bind after it appears. - struct UnitSamplerLookupMemo { - // The {slot, gen} of the frontend sampler this row was resolved for. It replaces the - // weak_ptr and its owner compare: a stale row cannot match, because the successor of a - // freed sampler is handed the same slot only with a higher Gen. - MG_Pipe::MGPipeHandle frontendHandle = MG_Pipe::kMGPipeNullHandle; - SamplerImpl::BackendSamplerObject* backend = nullptr; - }; - static Array - g_unitSamplerLookupMemos; - - // Puts each touched unit's frontend sampler object on the backend unit. Not part - // of the texture memo below (BindCurrentProgramWithResources may rewrite sampled - // units' samplers right after this runs - raw-depth-fetch substitution, - // per-program sampler objects); instead the walk carries its own memo: - // - // The walk is a pure function of WHICH sampler object each touched unit holds - - // covered by (context id, unit-bindings epoch, high-water mark; the epoch - // snapshot includes each unit's sampler object and bumps when the mark moves) - - // and replaying it as "do nothing" additionally requires the sampler bindings it - // left to still be on the driver, which the row compare against - // g_boundSamplersCache (the shadow every sampler bind routes through) proves. - // Any later writer - the program pass substituting a raw-depth sampler, a - // frontend sampler bind/unbind (epoch), the ES-context generation moving - lands - // in one of those inputs. A unit whose sampler had no backend object yet leaves - // its row untouched; the object is only created by the program pass, whose Bind - // moves the row and thereby re-opens this memo, so the miss cannot be latched. - static Uint64 g_unitSamplerWalkContextId = 0; - static Uint64 g_unitSamplerWalkEpoch = 0; - static Int g_unitSamplerWalkMaxUnit = -1; - static Uint g_unitSamplerWalkContextGeneration = 0; - static Bool g_unitSamplerWalkValid = false; - static decltype(SamplerImpl::g_boundSamplersCache) g_unitSamplerWalkRows{}; - - static void BindCurrentUnitSamplers(const TextureImpl::DrawTextureSyncKeys& keys) { - const Int maxTouchedUnit = keys.maxTouchedUnit; - const SizeT rowBytes = - static_cast(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]); - if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId && - g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit && - g_unitSamplerWalkContextGeneration == g_backendContextGeneration && - std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) { - return; - } - g_unitSamplerWalkValid = false; - Bool walkedFromRecords = false; - // P4a e4 (D-G2). THE WALK ITSELF READS THE APPLIER'S PER-UNIT HANDLE ARRAY. - // - // bind_sampler_states is the resolved answer to exactly the question this loop asks - - // which sampler CSO does each touched unit carry - so the handle IS the lookup: no - // GetTextureUnitObject per unit, no lifetime-id probe, and no UnitSamplerLookupMemo - // row to keep, because FindByHandle is already one array index. The memo below stays - // for the arm that still resolves through a frontend object. - // - // A NULL HANDLE MEANS "this unit has no sampler object", which is the record's way of - // saying what the frontend's null SharedPtr says, and it unbinds - the texture's own - // built-in sampler then applies, exactly as today. A live handle resolves to THE CSO's - // OWN TWIN (P4a fable seam F-4, SamplerImpl::ResolveSamplerCsoTwin): the sentence that - // stood here - "a handle whose twin does not exist yet is left alone ... the program - // pass creates the twin later in the same draw" - described a lookup that could never - // hit, because the handle is content-addressed and the registry's twins were minted off - // lifetime ids, so this arm bound nothing on every draw and only the pre-handle program - // pass ever put a glBindSampler'd object on the driver. The program pass now binds the - // same CSO twin for the units it samples, which is what stops the two arms ping-ponging - // a unit between two driver samplers. - // - // Declines - falls through to the frontend walk - until the set has arrived, and the - // COUNT says so rather than the serial (MGPipeApplierReset advances serials whether or - // not anything was emitted). - // P4a decline-site S1: M - the mask says this family is not switched on, and it IS - // D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification round. - const auto& st = MG_Pipe::MGPipeApplier(); - // P4a decline-site S2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, then - // the frontend walk. Bit 11 is on (the enclosing SamplerSubsystemEnabled()) and - // package C has landed, so a draw that touches texture units with no - // bind_sampler_states ever applied is a seam defect. The frontend walk still - // runs: it answers the same question correctly, and what this line exists to stop - // is the SILENT permanent fallback, not the fallback. - // NARROWED BY THE FIRST REAL-PATH RUN, exactly as T2 was and for the same - // reason: maxTouchedUnit < 0 is a draw that touches no texture unit, for which an - // empty window is the correct emission. All 161 hits of the unnarrowed line were - // that shape - the log said `units 0..-1` on every one of them. - if (st.SamplerStateCount == 0 && maxTouchedUnit >= 0) { - // P5e (tx2), CONTRACT-P5E §5.3: the same refusal CurrentUnitBindingsEpoch makes - - // the frontend walk below reads GetTextureUnitObject per unit, which an active - // transport may not do for a record whose client has moved on. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - a draw " - "touches units 0..%d and no bind_sampler_states has ever been applied " - "while the sampler subsystem bit is set; the pre-handle sampler walk " - "reads client memory and is refused under an active transport " - "(CONTRACT-P5E.md §5.3)", - static_cast(maxTouchedUnit)); - std::abort(); - MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw touches " - "units 0..%d and no bind_sampler_states has ever been applied while the " - "sampler subsystem bit is set; running the pre-handle sampler walk.", - static_cast(maxTouchedUnit)); - } - if (st.SamplerStateCount != 0) { - for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { - const Uint32 index = static_cast(unit); - // P4a decline-site S3: the OUT-OF-WINDOW UNBIND (MAJOR-2, fixed here); no - // flip remains at the verification round, only the A8 measurement below. - // - // A UNIT THE RECORD DOES NOT DESCRIBE IS UNBOUND, NOT SKIPPED. The var-tail - // window IS the bound for RECORD RETENTION - entries outside it are not - // cleared in the applier - but this loop is not reading records, it is - // writing DRIVER state, and for a driver binding "no record" means "no - // sampler object", which is exactly what the pre-handle arm's - // `else { UnbindSampler(unit); }` below says and why it was written: a - // sampler object left on the unit by an earlier draw keeps being applied, - // and on a multisample texture - which takes no sampler object at all - the - // draw is rejected outright. `continue` here reintroduced that defect on - // the new arm for every unit below SamplerStateStart or above the window, - // which is the same smaller-window direction DEV-3 refused. - // - // A8 again: nothing in the contract pins the window to cover - // [0, maxTouchedUnit]. If package C documents that it does, this branch - // becomes unreachable and can be asserted rather than executed; until then - // it is load-bearing, and the verification round measures - // (SamplerStateStart, SamplerStateCount) against maxTouchedUnit to say so. - if (index < st.SamplerStateStart || index - st.SamplerStateStart >= st.SamplerStateCount) { - SamplerImpl::UnbindSampler(unit); - continue; - } - const MG_Pipe::MGPipeHandle sampler = st.BoundSamplerStates[index]; - if (MG_Pipe::MGPipeHandleIsNull(sampler)) { - SamplerImpl::UnbindSampler(unit); - continue; - } - // P4a decline-site S4, RETIRED at the fable seam round (F-4): what stood here - // was `g_backendSamplerObjects.FindByHandle(sampler)` - an identity-keyed - // table asked for a content-addressed handle, a miss by construction on - // every draw - with a comment that read the miss as a within-draw ordering - // fact. The twin is the CSO's own now; a null answer has already named its - // reason (no record, or a slot that cannot be adopted) and leaves the unit - // as it is, which is the one decline this arm still has. - if (auto* twin = SamplerImpl::ResolveSamplerCsoTwin(sampler)) { - twin->Bind(unit); - } - } - walkedFromRecords = true; - } - if (!walkedFromRecords) { - // P13: no bind_sampler_states window applied, i.e. no unit holds a sampler object - - // the record arm emits one for every unit that does. Unbind, so a sampler object - // left on a unit by an earlier draw is not applied to this one. - for (Int unit = 0; unit <= maxTouchedUnit; ++unit) SamplerImpl::UnbindSampler(unit); - } - g_unitSamplerWalkContextId = keys.contextId; - g_unitSamplerWalkEpoch = keys.unitBindingsEpoch; - g_unitSamplerWalkMaxUnit = maxTouchedUnit; - g_unitSamplerWalkContextGeneration = g_backendContextGeneration; - std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes); - g_unitSamplerWalkValid = true; - } - - // Memo of the resolved per-unit TEXTURE bindings, so a steady-state draw loop stops - // re-deriving an answer nothing has invalidated (a Minecraft frame issues thousands of - // draws that touch none of the inputs below, and the resolution walks every binding slot - // of every touched unit three times). - // - // ResolveAndBindUnitTextures is a pure function of: - // * the GLContext identity - a never-reused id, since the counters below restart at 0 in - // a new context and a recreated one can land on the old heap address; - // * the per-unit bindings - which texture object every slot holds and which sampler - // object each unit carries, keyed as TextureImpl::CurrentUnitBindingsEpoch rather - // than as the texture bind generation: the generation also bumps on redundant - // re-binds (26.2 re-binds the unit's own sampler around every texture-unit switch), - // which would force a full re-resolution per draw for an answer that cannot have - // moved. The one input the old generation key covered that the epoch does not - a - // default texture gaining or losing an image with no bind moving - bumps the - // sampling-resolution generation below (SetInternalFormat -> BumpShapeVersion); - // * the touched-unit high-water mark (units above it have provably-empty slots); - // * the program that arbitrates two real textures aliased onto one native target - - // identity, plus the lifetime id because a freed program can be replaced at the same - // address, plus the backend-state version which moves on every relink and on every - // sampler-uniform unit assignment, plus the link status; - // * the sampling-resolution generation - any texture shape change or any sampler - // parameter change, i.e. everything mipmap-completeness is computed from, and - // completeness is what decides whether a texture is bound at all; - // * the ES context generation, because the backend texture ids and the driver's own - // binding state die with the context. - // - // A matching key only says the ANSWER is unchanged; replaying it as "do nothing" also - // requires the bindings it established to still be on the driver. Rather than enumerate - // every writer, the memo keeps the binding shadow it left and compares it: that shadow is - // already the authority every redundant-bind filter in this backend trusts, and every path - // that puts a texture on a unit behind this function's back maintains it - the upload - // path's scratch bind on the temp unit (BackendTextureObject::Bind out of - // SyncMipmapsToBackend), CopyTexSubImage2D and GenerateMipmap binding on the active unit, - // the glBindTextures fast path, and the self-scrub a BackendTextureObject performs when it - // is destroyed or respecified. - struct ResolvedTextureBindingMemo { - Bool valid = false; - Uint64 glContextId = 0; - Int maxTouchedUnit = -1; - Uint64 unitBindingsEpoch = 0; - Uint64 samplingResolutionGeneration = 0; - const void* program = nullptr; - Uint64 programLifetimeId = 0; - Uint32 programBackendStateVersion = 0; - Bool programLinked = false; - Uint contextGeneration = 0; - // P5e (pg) -> tx2, ruling 6 / ID-86: THE PROGRAM HALF OF THIS KEY HAS TO GO BY HANDLE, - // and this struct is tx2's line, so pg states the shape rather than writing it. The four - // program fields above are read off the frontend ProgramObject on EVERY DRAW, memo hit - // included (kimi row 100, the worst row of the program family). What replaces them on - // the handle arm is {DrawProgram.Slot, DrawProgram.Gen, ShaderCso record Serial, - // BindingsSerial}: Serial moves on every applied create_shader_state (how a relink - // travels) and BindingsSerial on every applied set_program_bindings (how a glUniform1i - // on a sampler travels). Both are needed - ruling 6's "both" - because a relink with - // UNCHANGED texture resolution still hands the twin new unit assignments. - // P5e (tx2), CONTRACT-P5E §5.3 / ruling 6 (ID-86): THE KEY IS BOTH HALVES. - // - // S2 argued SamplerViewsSerial alone - it already mixes the program-resolved opaque units, - // so a resolution that moved moves it - and S4 argued the program's rows. The integrator - // ruled BOTH, for the case neither covers alone: a RELINK whose resolution happens to be - // unchanged still needs the twin's new unit assignments, and SamplerViewsSerial does not - // move for it (the emitter's content hash is over the resolved set, which did not change). - // - // The frontend program rows above (pointer, lifetime id, backend-state version, link - // status) stay for the monolith arm and are simply not read on this one. - MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle; - Uint64 shaderCsoSerial = 0; - Uint64 bindingsSerial = 0; - Uint64 samplerViewsSerial = 0; - Uint64 contextSerial = 0; - decltype(TextureImpl::g_boundTexturesCache) boundTextures{}; - }; - // Small per-PROGRAM memo set, not one global: the program is part of the key - // (it arbitrates aliased native targets), so a frame that cycles a handful of - // programs over UNCHANGED unit bindings - Sodium issues 62 glUseProgram per - // frame - invalidated a single memo on every switch and re-ran the full - // three-pass slot walk for an answer that had not moved. Entries are selected - // by raw program pointer; everything a recycled address could confuse is - // caught by the full key compare (lifetime id) exactly as before. Round-robin - // eviction; 4 entries cover the shader set a Minecraft-shaped frame cycles. - static Array g_resolvedTextureBindingMemos; - static Uint32 g_resolvedTextureBindingMemoCursor = 0; - - // Rebinds every frontend texture unit's textures (and sampler objects) on the - // backend context. Needed before draws AND compute dispatches: content syncs - // (SyncTextureObjectToBackend) bind scratch textures on the active unit as a - // side effect, so unit bindings must be re-established afterwards or shaders - // sample whatever texture the last sync left behind (e.g. Flywheel's depth - // pyramid downsample reading a stale unit-0 binding instead of the depth - // attachment). - // - // P5e (pa): `currentProgram` IS NULL ON THE HANDLE ARM, and that is a statement about the - // caller's arm rather than something this body may test for. Every read of it below sits on - // the `byHandle == false` side of a branch whose condition is TextureImpl::UnitTexturesByHandle() - // - the memo's entry selection, the four frontend key rows, and ResolveAndBindUnitTextures' - // sampledTargetForUnit lambda, which its own handle arm returns before reaching. The hoist in - // PrepareForDraw only drops the object once THAT test holds too (DrawProgramFromRecords), so - // this parameter is null exactly where nothing consults it. - static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, - const SharedPtr& currentProgram) { -#ifdef TRACY_ENABLE - ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); -#endif - // Units past the frontend's high-water mark have provably-empty slots. - const Int maxTouchedUnit = keys.maxTouchedUnit; - - // P5e (tx2) / ruling 6: on the handle arm the key is the applier's rows and nothing - // frontend - INCLUDING the entry selection, which used the frontend program's address. - // maxTouchedUnit stays in it because the unbind sweep this memo replays is bounded by it. - MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle; - Uint64 shaderCsoSerial = 0; - Uint64 bindingsSerial = 0; - Uint64 samplerViewsSerial = 0; - Uint64 contextSerial = 0; - const auto& st = MG_Pipe::MGPipeApplier(); - drawProgram = st.DrawProgram; - samplerViewsSerial = st.SamplerViewsSerial; - contextSerial = st.ContextSerial; - if (const auto* cso = PipeShaderCsoRecordForHandle(drawProgram)) { - shaderCsoSerial = cso->Serial; - bindingsSerial = cso->BindingsSerial; - } - // Entry selection by program pointer; a missing program takes the round-robin - // victim. WHICH entry is used is only a performance choice - correctness sits - // entirely in the full key + shadow compare below, unchanged from the single - // memo this set replaces. - // - // P5e (pa): NOT EVEN THE POINTER, on the handle arm. The by-handle branch below selects - // and keys on {DrawProgram, ShaderCso.Serial, BindingsSerial} (ruling ID-86) and never - // looks at this value, but it was still being loaded out of the caller's SharedPtr every - // draw - and once the caller stops holding one there is nothing there to load. - const void* programKey = - nullptr; - ResolvedTextureBindingMemo* memoSlot = nullptr; - for (auto& candidate : g_resolvedTextureBindingMemos) { - if (candidate.valid && candidate.drawProgram.Slot == drawProgram.Slot && - candidate.drawProgram.Gen == drawProgram.Gen) { - memoSlot = &candidate; - break; - } - continue; - } - if (!memoSlot) { - g_resolvedTextureBindingMemoCursor = - (g_resolvedTextureBindingMemoCursor + 1) % g_resolvedTextureBindingMemos.size(); - memoSlot = &g_resolvedTextureBindingMemos[g_resolvedTextureBindingMemoCursor]; - } - auto& memo = *memoSlot; - const SizeT shadowBytes = - static_cast(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]); - const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch; - // P5e (ID-124): the legacy key is a LAZY LAMBDA, not an eagerly-computed Bool. It was - // the else-arm of a ternary whose declaration sat inside `#if MOBILEGL_BUILD_DISAGGREGATED` - // while the arm itself and every use of `keysMatch` sat outside it, so neither - // non-disaggregated flavour compiled. Hoisting it to an eager Bool would have been - // WORSE than the build break: it reads `currentProgram`, which is null on the handle - // arm, so the laziness the ternary gave for free is load-bearing (ID-81 / ID-110 - - // the arm decides, and the other arm's reads must not happen at all). - const auto legacyKeysMatch = [&]() -> Bool { - return (memo.valid && memo.glContextId == keys.contextId && - memo.maxTouchedUnit == maxTouchedUnit && - memo.unitBindingsEpoch == unitBindingsEpoch && - memo.samplingResolutionGeneration == keys.samplingGeneration && - memo.program == programKey && - memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) && - memo.programBackendStateVersion == - (currentProgram ? currentProgram->GetBackendStateVersion() : 0) && - memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) && - memo.contextGeneration == g_backendContextGeneration); - }; - const Bool keysMatch = - (memo.valid && memo.maxTouchedUnit == maxTouchedUnit && - memo.contextSerial == contextSerial && memo.samplerViewsSerial == samplerViewsSerial && - memo.drawProgram.Slot == drawProgram.Slot && memo.drawProgram.Gen == drawProgram.Gen && - memo.shaderCsoSerial == shaderCsoSerial && memo.bindingsSerial == bindingsSerial && - memo.contextGeneration == g_backendContextGeneration); - // Short-circuited: the shadow compare is only meaningful once the key (and with it the - // snapshotted row count) matches. - if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), - shadowBytes) != 0) { - memo.valid = false; - if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) { - memo.glContextId = keys.contextId; - memo.maxTouchedUnit = maxTouchedUnit; - memo.unitBindingsEpoch = unitBindingsEpoch; - memo.samplingResolutionGeneration = keys.samplingGeneration; - memo.program = programKey; - // P5e (pa): the four frontend program rows belong to the LEGACY key and are not - // written on the handle arm - decided by `byHandle`, which is the arm, and not by - // the object happening to be null, which is only a consequence of it. They are - // zeroed rather than skipped so an entry cannot carry a row nobody wrote. - memo.programLifetimeId = 0; - memo.programBackendStateVersion = 0; - memo.programLinked = false; - memo.contextGeneration = g_backendContextGeneration; - memo.drawProgram = drawProgram; - memo.shaderCsoSerial = shaderCsoSerial; - memo.bindingsSerial = bindingsSerial; - memo.samplerViewsSerial = samplerViewsSerial; - memo.contextSerial = contextSerial; - std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes); - memo.valid = true; - } - } - - BindCurrentUnitSamplers(keys); - } - - void BindCurrentTextures() { - // P5e (pa): the exported no-argument entry (DirectGLES.h) takes the SAME arm the hoist in - // PrepareForDraw does, and for the same reason: the pull is what trips the input check, so - // an entry point that keeps it would keep the row alive for whichever caller reaches this - // overload next. Today that is SanityTest's unit cases, which run under monolith - // transport and therefore take the frontend arm below unchanged. - const SharedPtr noFrontendProgram; - BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), - noFrontendProgram); - } - - // Binds the current program's backend object and re-establishes its per-program - // resources: global UBO contents, uniform-block bindings, and sampler uniform - // units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the - // association must be rebuilt through the API). Compute dispatches depend on - // this as much as draws do — e.g. Flywheel's cull shader reads the - // _FlwFrameUniforms block and the _flw_depthPyramid sampler. - // - // P5e (pa): `currentProgram` IS NULL WHENEVER `handleArm` HOLDS. Every read of it in the - // body below is on the `:` side of a `handleArm ? :` pick or inside the `else` of - // `if (handleArm)`, with one pair that is guarded instead - the two `globalConstants ? ... :` - // reads, which the [[noreturn]] refusal a few lines above them makes unreachable on that arm - // (its note says so). The parameter stays for the monolith arm, which is ruling ID-81: that - // build keeps this function's frontend text token for token. - static void BindCurrentProgramWithResources( - const SharedPtr& currentProgram, - const TextureImpl::DrawTextureSyncKeys& keys, MG_Pipe::MGPipeHandle programCso) { - // P5e (pg): THE HANDLE ARM'S GATE, from the record rather than from the object. Hoisted - // out of the `if` below because the whole body's entry condition is "the program linked - // and has SPIR-V", and on this arm both answers are fields of the descriptor - // (Desc.LinkStatus is ID-88's, Desc.SpirvStatus P4a's). - const MG_Pipe::MGPipeShaderCsoRecord* programRecord = nullptr; - programRecord = PrgramImpl::FindShaderCsoRecord(programCso); - if (programRecord != nullptr && - (programRecord->Desc.LinkStatus == 0 || programRecord->Desc.SpirvStatus == 0)) { - programRecord = nullptr; - } - const Bool programUsable = - programRecord != nullptr; - if (programUsable) { -#ifdef TRACY_ENABLE - ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND); -#endif - PrgramImpl::BackendProgramObjectImpl* twin = nullptr; - // THE STASH, KEYED ON THE HANDLE (CONTRACT-P5E §5.5). SyncCurrentProgramByHandle - // wrote it a few lines ago in the same Prepare; the fallback is the by-handle - // resolver, which is record-first and touches no frontend identity - so unlike - // the frontend fallback below it is not a registry probe at all. - twin = PrgramImpl::g_currentDrawProgramHandle == programCso - ? PrgramImpl::g_currentDrawBackendProgram - : nullptr; - if (!twin) twin = PrgramImpl::ResolveProgramTwin(programCso); - if (twin) { - auto& backendProgram = *twin; - backendProgram.Use(); - - // Global UBO: block index and binding-point assignment are cached at - // link time (CacheResourceLocations); re-upload only when the CPU shadow - // actually changed since the last upload for this program. - // P5e (pg): the block's SIZE is Desc.GlobalUboSize on the handle arm. It is the - // same number GetUBOSize() answers - the emitter takes it from there - but it is - // the record's copy, which is the point. - const Uint globalUboSize = - programRecord->Desc.GlobalUboSize; - if (globalUboSize > 0 && backendProgram.HasGlobalUboBlock()) { -#ifdef TRACY_ENABLE - ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND); -#endif - // P4a e4 (D-H6): set_global_constants' counterpart. The version and the - // block image come off the ShaderCso record when it carries them; the UBO - // RING BELOW IS UNTOUCHED - it is on the Espryt do-not-touch list, keeps - // its {contentVersion, ringGeneration, frameSerial, offset} allocation - // key, keeps its glBufferSubData fallback and keeps accounting to - // ByteClass::StageUboGlobal. What changes is where the version and the - // bytes come from, and nothing else. - // - // The record covers the DEFAULT UNIFORM BLOCK ONLY. Named UBOs are the - // block right after this one and they are P4b's (dirty bits 15-17), which - // is why nothing here touches them. - const MG_Pipe::MGPipeShaderCsoRecord* const globalConstants = - PrgramImpl::ResolveGlobalConstantsRecordForHandle(programCso, - globalUboSize); - // P5e (pg), gap G-C: `MapUBO()` HAS NO RUN-AHEAD ANSWER. It returns - // ProgramObject::globalUboScratch - the live CPU array every glUniform* - // writes into - so under a transport it is a torn read of client memory by - // construction, not a read that happens to race. Every reason - // ResolveGlobalConstantsRecord* can decline is a SEAM DEFECT here (bit 12 - // off, the mis-keyed descriptor, the never-uploaded sentinel, a short block - // image), and each of them has already said so by name one level down; what - // this adds is that on the handle arm there is nothing legal to fall back - // to. CONTRACT-P5E §5.5: Fatal{UnmigratedVerb, "set_global_constants"}, not - // a torn upload. - // - // P5e (pa): AND IT IS ALSO WHAT KEEPS THE TWO `globalConstants ? ... : - // currentProgram->...` READS BELOW OFF THE FRONTEND. std::abort() is - // [[noreturn]], so past this statement `handleArm` implies - // `globalConstants != nullptr` in one step and in this function - not by a - // chain through other files, which is the distinction ID-110 draws. It - // matters now: pa's caller passes a NULL ProgramObject on that arm, so the - // `:` sides are unreachable rather than merely unused. - if (globalConstants == nullptr) { - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_global_constants\"} - the " - "ShaderCso record {%u, %u} does not answer the default uniform " - "block for a draw of a program that declares %u bytes of it, and " - "MapUBO() is the client's live scratch array", - programCso.Slot, programCso.Gen, globalUboSize); - std::abort(); - } - const Uint32 uboContentVersion = - globalConstants ? globalConstants->GlobalConstantsVersion : - currentProgram->GetUBOContentVersion(); - const SizeT uboSize = static_cast(globalUboSize); - // Substituted at the two upload sites rather than hoisted into a local: - // MapUBO() is called there today and only there, and hoisting it would - // call it on paths that skip the upload entirely. -#define MGB_UBO_BYTES \ - (globalConstants ? static_cast(globalConstants->GlobalConstants.data()) \ - : static_cast(currentProgram->MapUBO())) - // Preferred path: write changed contents into a fresh slot of the - // shared persistent-mapped ring and bind it as a range. The GPU - // never reads bytes the CPU is writing, so the driver has no - // write-after-read hazard to resolve — the in-place glBufferSubData - // below forced Adreno into a ghost/stall on every uniform-dirtying - // draw (MC dirties uniforms every draw), which dominated frame time. - Bool ringBound = false; - { - const SizeT bindSize = - std::max(uboSize, static_cast(backendProgram.GetGlobalUboBackendBlockSize())); - auto& ringSlot = backendProgram.GetGlobalUboRingAllocation(); - void* write = nullptr; - Uint ringId = 0; - const auto state = BufferImpl::UboRingPrepareSlot(ringSlot, uboContentVersion, - CurrentFrameSerial(), bindSize, write, - ringId); - if (state == BufferImpl::UboRingSlotState::Allocated) { - std::memcpy(write, MGB_UBO_BYTES, uboSize); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal, - static_cast(uboSize)); - } - } - if (state == BufferImpl::UboRingSlotState::Allocated || - state == BufferImpl::UboRingSlotState::Reused) { - BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, ringId, - static_cast(ringSlot.offset), - static_cast(bindSize)); - ringBound = true; - } - } - if (!ringBound) { - // Fallback (no EXT_buffer_storage / fences, or ring creation - // failed): the original in-place upload. - if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) { - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId()); - g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, globalUboSize, - MGB_UBO_BYTES); - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes( - MG_Util::PipeStats::ByteClass::StageUboGlobal, - static_cast(globalUboSize)); - } - g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); - backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); - } - BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0, - backendProgram.GetBackendGlobalUBOId()); - } -#undef MGB_UBO_BYTES - } - - { -#ifdef TRACY_ENABLE - ZoneScopedNC("UpdateUBO", TRACY_ZONECOLOR_BACKEND); -#endif - // Normal UBOs: backend block indices and glUniformBlockBinding - // assignments are cached at link time; per draw only the buffer - // bindings are re-established (they follow frontend binding points). - const auto& blockBackendIndices = backendProgram.GetUniformBlockBackendIndices(); - const auto uboCount = static_cast(blockBackendIndices.size()); - // Pre-loop epoch read; the per-resource drawCleanEpoch stamps below - // follow the same stamp-the-pre-read-value rule as the VAO memo - // (Managers.h documents the contract and the mutation-site list). - const Uint64 bufferEpoch = - uboCount > 0 ? BufferImpl::CurrentBufferMutationEpoch() : 0; - Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO - for (Int i = 0; i < uboCount; ++i) { - ++lastUBOBinding; - if (blockBackendIndices[i] < 0) { - continue; - } - - // Connect buffer to backend binding point. - // - // P5e (pg): glUniformBlockBinding MOVES THIS AFTER THE LINK, so it is - // one of the three post-link mutable reflection fields the archive alone - // cannot answer - set_program_bindings' first tail carries it, dense in - // this very index space. The BINDING POINT it names is sb's row (the - // GetBufferBindingPoint below); this line is the boundary S4 R5 named. - auto binding = - PrgramImpl::ProgramBlockBindingFromRecord(*programRecord, i); - // P5e (sb, §5.6): THE POINT HALF, by record. `binding` above is the - // PROGRAM's block binding and stays exactly where it is (it is package - // pg's row); what moves is the point it indexes. The index space is the - // same one - Start is 0 by contract - so `binding` addresses the - // applier's window directly, and a binding at or above Count means - // "nothing bound", which is what the frontend array's default said too. - const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); - const Uint32 windowStart = - st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform]; - const SizeT windowEnd = - static_cast(windowStart) + - st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform]; - if (static_cast(binding) < windowStart || - static_cast(binding) >= windowEnd) { - continue; // nothing bound there - the frontend arm's `if (bufferObj)` - } - const MG_Pipe::MGPBufferRange& entry = - st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform] - [static_cast(binding)]; - if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue; - // The clean probe is the SAME five questions IsBufferDrawClean asks - // the frontend object, asked by handle: the twin's identity, the - // resource record's Serial against the twin's synced one, and the - // pending-work sets - all server-owned (D-A4). The drawCleanEpoch - // short-circuit is unchanged; it was never the point read. - auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res); - if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) { - if (backendResource && BufferImpl::IsBufferDrawCleanByHandle( - entry.Res, backendResource, nullptr)) { - backendResource->drawCleanEpoch = bufferEpoch; - } else { - backendResource = - BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res); - } - } - if (backendResource && backendResource->id != 0) { - // WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a - // base binding travels as kMGPipeWholeBuffer precisely so the - // extent is re-resolved HERE, against the storage this server - // holds, rather than frozen at the client's emission. - if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) { - BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding, - backendResource->id); - } else { - const SizeT storage = backendResource->storageSize; - const SizeT rangeStart = - std::min(static_cast(entry.Offset), storage); - const SizeT rangeEnd = std::min( - static_cast(entry.Offset + entry.Size), storage); - BufferImpl::BindBufferRangeCached( - GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id, - static_cast(rangeStart), - static_cast(rangeEnd - rangeStart)); - } - } else { - MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO."); - } - continue; - } - } - - // Atomic counter buffers. Bound here rather than beside the storage-buffer sync - // in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads - // them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS - // program declares, and no other program's blocks live there. Both the draw and - // the dispatch path reach this, which is what a compute-shader counter needs. - if (!backendProgram.GetAtomicCounterBindings().empty()) { - BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(), - backendProgram.GetAtomicCounterEsslBindingTop()); - } - - { -#ifdef TRACY_ENABLE - ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND); -#endif - // Sampler unit binding: backend locations are cached at link time; - // glUniform1i is program state, so it is only re-issued when the - // frontend-assigned unit differs from what this program last saw. - // - // The whole pass sits behind the per-twin SamplerPassMemo (see its - // declaration for the invalidation enumeration): while the keys hold - // and every previously touched unit's sampler-shadow row is exactly - // what this pass last left there, re-running it is a provable no-op. - auto& samplerPassMemo = backendProgram.GetSamplerPassMemo(); - // P5e (pg), CONTRACT-P5E §5.5: THE SAMPLER PASS MEMO KEYS ON BindingsSerial - // on the handle arm. m_backendStateVersion is a frontend counter read on - // every draw (kimi row 105); what it is standing in for here is "has a - // sampler uniform's unit moved", and that is exactly what an applied - // set_program_bindings means. Truncated to the memo's Uint32 field, which is - // safe for a memo key - it is a change detector, not an ordering - and the - // record's serial would need four billion binding records to wrap. - const Uint32 programBackendStateVersion = - static_cast(programRecord->BindingsSerial); - Bool samplerPassClean = - samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId && - samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch && - samplerPassMemo.samplingGeneration == keys.samplingGeneration && - samplerPassMemo.backendStateVersion == programBackendStateVersion && - samplerPassMemo.textureContextGeneration == g_backendContextGeneration; - if (samplerPassClean) { - for (Uint i = 0; i < samplerPassMemo.count; ++i) { - if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] != - samplerPassMemo.rows[i]) { - samplerPassClean = false; - break; - } - } - } - if (!samplerPassClean) { - constexpr SizeT kMaxMemoEntries = - PrgramImpl::BackendProgramObjectImpl::SamplerPassMemo::kMaxEntries; - samplerPassMemo.valid = false; - samplerPassMemo.count = 0; - Bool memoisable = true; - for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) { - // P5e (pg): the unit glUniform1i put on this sampler - the second - // of the three post-link mutable fields, carried sparse and ascending - // by location in set_program_bindings' second tail. - const auto unit = - PrgramImpl::ProgramSamplerUnitFromRecord( - *programRecord, samplerBinding.frontendLocation); - if (unit == -1) continue; - // Record the touched unit for the memo; a pass touching more - // units than the memo can carry (or an out-of-range unit) - // simply never memoises. - if (memoisable && samplerPassMemo.count < kMaxMemoEntries && unit >= 0 && - unit < static_cast(SamplerImpl::g_boundSamplersCache.size())) { - samplerPassMemo.units[samplerPassMemo.count++] = static_cast(unit); - } else { - memoisable = false; - } - if (samplerBinding.lastAssignedUnit != unit) { - g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit); - samplerBinding.lastAssignedUnit = unit; - } - - // P5e (tx2), CONTRACT-P5E §5.3. THE SAMPLER PASS, FROM RECORDS. - // - // Four frontend reads used to live in this loop body and every one of - // them has a carrier: the unit's sampler object is - // st.BoundSamplerStates[u] (null = the texture's built-in sampler, which - // is TextureResources[view.Texture].Params.BuiltinSampler); its fifteen - // values - lodBias among them - are the CSO record's - // (MGPipeValueTypes.h); and the sampled texture's FORMAT, which decides - // the raw-depth-fetch substitution, is - // SamplerViewCsos[View].View.InternalFormat. - // - // THE SUBSTITUTION ITSELF STAYS ON THE SERVER (ARCHITECTURE.md §5.5 - // assigns it there by name); what moves is where its two inputs come - // from. - const auto& st = MG_Pipe::MGPipeApplier(); - const auto index = static_cast(unit); - const Bool inViewWindow = index >= st.SamplerViewStart && - index - st.SamplerViewStart < st.SamplerViewCount && - index < st.BoundSamplerViews.size(); - const MG_Pipe::MGPBoundView view = - inViewWindow ? st.BoundSamplerViews[index] : MG_Pipe::MGPBoundView{}; - const Bool inStateWindow = index >= st.SamplerStateStart && - index - st.SamplerStateStart < st.SamplerStateCount && - index < st.BoundSamplerStates.size(); - const MG_Pipe::MGPipeHandle unitSampler = - inStateWindow ? st.BoundSamplerStates[index] : MG_Pipe::kMGPipeNullHandle; - - // The EFFECTIVE sampler of the unit: the bound CSO if there is one, - // else the sampled texture's built-in sampler CSO - exactly the - // override order GL states and the pre-handle body spells with - // `samplerObject ? ... : texture->GetSamplerObject()`. - MG_Pipe::MGPipeHandle effectiveCso = unitSampler; - if (MG_Pipe::MGPipeHandleIsNull(effectiveCso) && - !MG_Pipe::MGPipeHandleIsNull(view.Texture)) { - if (const auto* texRecord = PipeTextureRecordForHandle(view.Texture)) { - effectiveCso = texRecord->Params.BuiltinSampler; - } - } - const MG_Pipe::MGPipeSamplerCsoRecord* effectiveCsoRecord = - PipeSamplerCsoRecordForHandle(effectiveCso); - - if (samplerBinding.lodBiasLocation >= 0) { - const Float lodBias = - effectiveCsoRecord != nullptr ? effectiveCsoRecord->Params.lodBias : 0.0f; - if (lodBias != samplerBinding.lastAssignedLodBias) { - g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias); - samplerBinding.lastAssignedLodBias = lodBias; - } - } - - // The last sample a usampler2DMS fetch may name, from the texture - // actually bound: a stencil texture's real count is not an integer - // colour texture's. Keyed like the bias above - a texture's storage or - // a unit's binding moving is what invalidates this pass's memo. - if (samplerBinding.msSampleMaxLocation >= 0) { - const Int sampleMax = - std::max(PipeTextureBackendSampleCountForHandle(view.Texture), 1) - 1; - if (sampleMax != samplerBinding.lastAssignedMsSampleMax) { - g_GLESFuncs.glUniform1i(samplerBinding.msSampleMaxLocation, sampleMax); - samplerBinding.lastAssignedMsSampleMax = sampleMax; - } - } - - const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); - const Bool sampledIsTexture2D = - viewRecord != nullptr && - static_cast(viewRecord->View.Target) == TextureTarget::Texture2D; - if (samplerBinding.uniformType == GL_SAMPLER_2D && sampledIsTexture2D && - effectiveCsoRecord != nullptr && - NeedsRawDepthFetchSampler( - effectiveCsoRecord->Params, - static_cast(viewRecord->View.InternalFormat))) { - GetRawDepthFetchSampler()->Bind(unit); - MGLOG_D("Using raw depth fetch sampler on unit %d.", unit); - } else if (!MG_Pipe::MGPipeHandleIsNull(unitSampler)) { - // THE MINT IS GONE (§4.2): the unit's sampler is the CSO's own - // twin, the same one BindCurrentUnitSamplers binds, so the two - // arms cannot hand a unit back and forth between two driver - // samplers - and there is no arm left that creates a twin from a - // frontend SamplerObject. - if (auto* csoTwin = SamplerImpl::ResolveSamplerCsoTwin(unitSampler)) { - csoTwin->Bind(unit); - } - } else { - SamplerImpl::UnbindSampler(unit); - } - continue; - } - if (memoisable) { - // Snapshot the rows AFTER the pass: they are exactly what a - // clean replay must find untouched. - for (Uint i = 0; i < samplerPassMemo.count; ++i) { - samplerPassMemo.rows[i] = SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]]; - } - samplerPassMemo.contextId = keys.contextId; - samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch; - samplerPassMemo.samplingGeneration = keys.samplingGeneration; - samplerPassMemo.backendStateVersion = programBackendStateVersion; - samplerPassMemo.textureContextGeneration = g_backendContextGeneration; - samplerPassMemo.valid = true; - } - } - } - } else { - g_GLESFuncs.glUseProgram(0); - PrgramImpl::g_lastUsedBackendProgramId = 0; - MGLOG_E_ONCE("No backend program found (maybe not synced) for current program, cannot use program."); - } - } - } - - // Raw pointer: only ever used inside one GL entry point after PrepareForDraw/ - // PrepareForCompute, where the current program (and therefore its registry twin) - // is pinned for the duration. Prefers the per-draw stash those preparations wrote. - static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() { - // P5e (pg), kimi row 107: THE PER-SUB-DRAW ROW. This is reached once per indirect - // sub-draw for the gl_DrawID / gl_BaseVertex / gl_BaseInstance uniforms, and before P5e - // every one of those pulled GetProgramForDraw() and, on a stash miss, probed the client - // allocator. On the handle arm the stash key is the applier's own DrawProgram and the - // miss path is the by-handle resolver, which is record-first and names no frontend - // identity - so the whole entry point leaves the frontend. - const MG_Pipe::MGPipeHandle cso = MG_Pipe::MGPipeApplier().DrawProgram; - if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; - const auto* const record = PrgramImpl::FindShaderCsoRecord(cso); - if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { - return nullptr; - } - if (PrgramImpl::g_currentDrawProgramHandle == cso) { - return PrgramImpl::g_currentDrawBackendProgram; - } - return PrgramImpl::ResolveProgramTwin(cso); - } - - void SetCurrentBaseInstance(Uint32 baseInstance) { - if (const auto program = GetCurrentBackendProgram()) { - program->SetBaseInstance(baseInstance); - } - } - - void SetCurrentDrawID(Uint32 drawId) { - if (const auto program = GetCurrentBackendProgram()) { - program->SetDrawID(drawId); - } - } - - void SetCurrentBaseVertex(Int32 baseVertex) { - if (const auto program = GetCurrentBackendProgram()) { - program->SetBaseVertex(baseVertex); - } - } - - Bool CurrentProgramReadsDrawID() { - const auto program = GetCurrentBackendProgram(); - return program != nullptr && program->ReadsDrawID(); - } - - Bool CurrentProgramReadsBaseVertex() { - const auto program = GetCurrentBackendProgram(); - return program != nullptr && program->ReadsBaseVertex(); - } - - // The two questions above, asked from BEFORE PrepareForDraw - where neither can be - // answered honestly. GetCurrentBackendProgram only sees a twin that a previous draw - // already synced, and a twin from before a relink still carries the previous link's - // uniform locations, so "no" there means "not known yet" at least as often as it - // means no. The multi-draw compute tier has to decide whether to flatten a batch - // before PrepareForDraw runs (its dispatch cannot come after the draw state), and - // flattening a batch that turns out to need per-sub-draw values is unrecoverable - - // so an unanswerable program counts as needing them. - Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) { - const auto program = GetCurrentBackendProgram(); - if (program == nullptr) return true; - // P5e (pg), kimi row 108: the same "is this twin current" question, from the record's - // serial instead of the frontend's link version. The ANSWER TO AN UNANSWERABLE PROGRAM - // IS STILL `true` - a flattened batch that turns out to need per-sub-draw values is - // unrecoverable, so "not known yet" has to count as "may need them", and a missing - // record is the most not-known-yet a program can be. - const auto* const record = - PrgramImpl::FindShaderCsoRecord(MG_Pipe::MGPipeApplier().DrawProgram); - if (record == nullptr || program->GetSyncedShaderCsoSerial() != record->Serial) { - return true; - } - return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex()); - } - - // ---- gl_ViewportIndex routing emulation, draw half --------------------------------------- - // See the block comment in Managers.h for what this is and why. Here is the state half: one - // replay pass per DISTINCT viewport state, each pushing that state onto the ES context's one - // viewport / one scissor / one depth range and telling the fragment gate which indices it - // serves. - namespace ViewportRoutingImpl { - // One replay pass: the state to push, and the set of gl_ViewportIndex values whose - // fragments this pass is allowed to keep. - struct RoutingPass { - IntVec4 viewport{}; - IntVec4 scissorBox{}; - FloatVec2 depthRange{}; - Bool scissorTest = false; - Uint32 indexMask = 0; - }; - - static constexpr Uint32 kAllViewportsMask = - (RenderStateParameters::MAX_VIEWPORTS >= 32) - ? 0xFFFFFFFFu - : ((1u << RenderStateParameters::MAX_VIEWPORTS) - 1u); - - // The plan for the draw currently being issued. A file-scope buffer rather than a return - // value because Begin/Apply/End are three calls around a draw the caller writes, and a - // fixed array of 16 keeps it allocation-free on a path that is per draw. NOT re-entrant, - // which is a property of the call sites and not an accident: every wrap in this file and - // in MultiDraw.cpp is around the innermost native glDraw*, so no replay can begin inside - // another - and a multi-draw tier that replayed its whole loop would be nesting. - static Array g_passes{}; - static Uint g_passCount = 0; - static PrgramImpl::BackendProgramObjectImpl* g_routedProgram = nullptr; - - // What index `i` actually rasterizes against, resolved exactly the way SyncRenderState - // resolves index 0 - including both substitutions it makes, which are not cosmetic: - // - // * a viewport of zero extent means "the application has never called glViewport", and - // GL's initial viewport is the whole surface, which the frontend cannot spell before - // a surface exists; - // * a scissor rectangle is read through the WRITTEN flag and not through its extent, - // because glScissor(0, 0, 0, 0) is a legal request meaning "reject every fragment" - // and is byte-identical to the never-written default that means the opposite. - // - // Resolving them here rather than deferring to SyncRenderState is what makes the grouping - // below correct: two indices that differ only in a field that resolves to the same - // rectangle really do rasterize identically and must share one pass. - static RoutingPass ResolveIndexState(const RenderStateParameters& parameters, Uint index, - Int surfaceWidth, Int surfaceHeight) { - RoutingPass pass; - const FloatVec4& viewport = parameters.Viewports[index]; - pass.viewport = IntVec4(static_cast(std::lround(viewport.x())), - static_cast(std::lround(viewport.y())), - static_cast(std::lround(viewport.z())), - static_cast(std::lround(viewport.w()))); - if ((pass.viewport.z() <= 0 || pass.viewport.w() <= 0) && surfaceWidth > 0 && surfaceHeight > 0) { - pass.viewport = IntVec4(0, 0, surfaceWidth, surfaceHeight); - } - pass.scissorBox = parameters.ScissorBoxes[index]; - if ((parameters.ScissorBoxWrittenMask & (1u << index)) == 0 && surfaceWidth > 0 && - surfaceHeight > 0) { - pass.scissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight); - } - pass.depthRange = parameters.DepthRanges[index]; - pass.scissorTest = (parameters.ScissorTestEnabledMask & (1u << index)) != 0; - return pass; - } - - static Bool SameState(const RoutingPass& a, const RoutingPass& b) { - return a.viewport == b.viewport && a.scissorBox == b.scissorBox && - a.depthRange == b.depthRange && a.scissorTest == b.scissorTest; - } - } // namespace ViewportRoutingImpl - - Uint BeginViewportRoutingPasses() { - using namespace ViewportRoutingImpl; - g_passCount = 1; - g_routedProgram = nullptr; - - // The whole emulation behind one static load, for every application that has never built - // a program writing gl_ViewportIndex - which is all of them but the conformance suite. - // Without it every draw in the process would pay GetCurrentBackendProgram's chain of - // frontend lookups for an answer that cannot change. - if (!g_anyProgramRoutesViewportIndex) { - return 1; - } - - auto* program = GetCurrentBackendProgram(); - if (program == nullptr || !program->RoutesViewportIndex()) { - return 1; - } - g_routedProgram = program; - // The gate reads zero until something writes it, and a zero mask discards every fragment. - // So this is not an optimization that can be skipped in the one-pass case - it is what - // keeps a routing program drawing at all. - program->SetViewportPassMask(kAllViewportsMask); - - // Replaying multiplies every side effect the vertex and geometry stages have, and the - // fragment gate can only undo the ones that happen in the FRAGMENT stage. Transform - // feedback records per emitted primitive, so a replayed draw would write its vertices N - // times; rasterizer discard means there are no fragments to gate at all, so replaying - // would be pure cost with nothing to show for it. Both fall back to a single pass with an - // open gate, i.e. to the pre-emulation behaviour, rather than to wrong data. - if (MG_Pipe::gPipeInputs->IsTransformFeedbackActive() || - MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { - return 1; - } - - const auto& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters(); - Int surfaceWidth = 0; - Int surfaceHeight = 0; - if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { - surfaceWidth = 0; - surfaceHeight = 0; - } - - Uint count = 0; - for (Uint index = 0; index < RenderStateParameters::MAX_VIEWPORTS; ++index) { - const RoutingPass resolved = - ResolveIndexState(parameters, index, surfaceWidth, surfaceHeight); - Uint existing = 0; - for (; existing < count; ++existing) { - if (SameState(g_passes[existing], resolved)) break; - } - if (existing == count) { - g_passes[count] = resolved; - ++count; - } - g_passes[existing].indexMask |= (1u << index); - } - - // One group is the overwhelmingly common case - it is what glViewport, glScissor and - // glDepthRange leave behind, because ARB_viewport_array defines all three as writing - // EVERY index. The mask is already open and index 0's state is what SyncRenderState - // pushed, so there is nothing to replay and nothing to restore. - if (count <= 1) { - g_passCount = 1; - return 1; - } - g_passCount = count; - return count; - } - - void ApplyViewportRoutingPass(Uint pass) { - using namespace ViewportRoutingImpl; - if (pass >= g_passCount || g_routedProgram == nullptr) { - return; - } - const RoutingPass& entry = g_passes[pass]; - g_GLESFuncs.glViewport(entry.viewport.x(), entry.viewport.y(), entry.viewport.z(), - entry.viewport.w()); - g_GLESFuncs.glScissor(entry.scissorBox.x(), entry.scissorBox.y(), entry.scissorBox.z(), - entry.scissorBox.w()); - // ES has one scissor-test enable where GL has sixteen, so the per-index bit becomes a - // per-pass glEnable/glDisable. This is the half DirectVulkan cannot do at all (Vulkan has - // no per-viewport scissor toggle either and has to widen a disabled index's rectangle to - // the whole framebuffer instead); here the rectangle stays honest. - entry.scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - g_GLESFuncs.glDepthRangef(entry.depthRange.x(), entry.depthRange.y()); - g_routedProgram->SetViewportPassMask(entry.indexMask); - } - - void EndViewportRoutingPasses(Uint passCount) { - using namespace ViewportRoutingImpl; - if (passCount <= 1) { - // Nothing was pushed and the mask is already open; leaving the shadow alone here is - // what keeps a non-routing draw at exactly its previous cost. - g_routedProgram = nullptr; - return; - } - if (g_routedProgram != nullptr) { - // Any draw that reaches the driver without going through a replay - an internal blit, - // or a path this emulation has not been taught about - must not inherit the last - // pass's mask and paint nothing. - g_routedProgram->SetViewportPassMask(kAllViewportsMask); - } - g_routedProgram = nullptr; - g_passCount = 0; - // The viewport, scissor, scissor-test enable and depth range now on the ES context belong - // to the last replay pass, and the shadow SyncRenderState diffs against does not know it. - // A full resync is the honest repair and costs one state push on the next draw, which - // only a viewport-routing workload ever pays. - RenderStateImpl::InvalidateSyncedRenderState(); - } - - static Bool SupportsNativeIndirectDraws() { - return g_GLESCapabilities.SupportsDrawIndirect; - } - - // P8-C: the command bytes an indirect executor is about to CONSUME on the CPU, made current. - // Consumed means: every field on the CPU path (no native indirect), or on the native path the - // uniforms fed from the command - gl_BaseVertex whenever the program reads it, and - // gl_BaseInstance when the program reads it without the mg_IndirectParams storage view that - // would give it the GPU's word. A native draw that feeds neither reads nothing and keeps its - // pointer, so a compute-culled indirect buffer drawn every frame costs no readback. - static const Uint8* CurrentIndirectCommandBytes(MG_Pipe::MGPipeHandle handle, const Uint8* commandBytes, - SizeT commandOffset, Bool useNative, Bool indexed) { - if (MG_Pipe::MGPipeHandleIsNull(handle)) return commandBytes; - if (useNative) { - const auto program = GetCurrentBackendProgram(); - if (program == nullptr) return commandBytes; - const Bool baseVertexFromCpu = indexed && program->ReadsBaseVertex(); - const Bool baseInstanceFromCpu = program->GetIndirectParamsBinding() < 0 && program->ReadsBaseInstance(); - if (!baseVertexFromCpu && !baseInstanceFromCpu) return commandBytes; - } - auto* resource = BufferImpl::FindBufferResourceForHandle(handle); - if (resource == nullptr) return commandBytes; - const Uint8* base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_fields"); - return base != nullptr ? base + commandOffset : commandBytes; - } - - // Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws - // execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's - // culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted - // for the per-command baseInstance, which is CPU-authored, to feed the mg_BaseInstance - // shader emulation. Without GL_EXT_base_instance a non-zero baseInstance in the command is - // technically undefined in ES; mobile drivers ignore the reserved word, instanced-array - // fetches were never baseInstance-offset here anyway, and the CPU fallback cannot see - // GPU-written command fields at all - so native is never worse. ANGLE-on-Vulkan instead - // leaks the word into gl_InstanceID (it becomes vkCmdDrawIndexedIndirect's firstInstance); - // the shader rewrite compensates by rebasing gl_InstanceID during these draws when - // IndirectDrawInstanceIdIncludesBaseInstance is set (PromoteDrawParameterGlobalsToUniforms). - // Only client-memory commands take the CPU per-command loop. - static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes, - SizeT commandOffset, - const SharedPtr& drawIndirectBuffer, - GLsizei drawcount, GLsizei stride, const char* label) { - (void)label; - // An indirect command's firstIndex/count live in GPU memory, so the substitution has - // to rewrite the whole element array buffer rather than this draw's range - which is - // exactly what it does when no CPU-known count is handed to it. Held for the whole - // command loop so every command in the batch reads the rewritten copy. - // - // firstIndex counts ELEMENTS, so it survives a widened copy untouched; what does not - // survive is the type and the element size, which are re-taken from the substitution - // below for both the native and the CPU-unrolled path. - const ScopedRestartIndexSubstitution restart(type, /*count=*/0, /*indices=*/nullptr); - if (!restart.DrawIsValid()) return; - type = restart.IndexType(); - indexSize = MG_Util::GetGLTypeSize(type); - // P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer is the - // handle the verb record carried (MGPipeApplier's verb stash) and the SSBO view's - // resource resolves from it by handle - the frontend binding slot and the client slot - // allocator are never read (T2). - const MG_Pipe::MGPipeHandle verbBufferHandle = - MG_Pipe::MGPipeApplier().VerbIndirectBuffer; - const Bool useNative = - !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws(); - commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative, - /*indexed=*/true); - if (useNative) { - // gl_BaseInstance must observe GPU-written command fields; expose the indirect - // buffer to the program's mg_IndirectParams SSBO view and address it per draw. - const auto backendProgram = GetCurrentBackendProgram(); - const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; - if (paramsBinding >= 0) { - auto* resource = - BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle); - if (resource && resource->id != 0) { - BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), - resource->id); - } - } - // gl_BaseVertex has no SSBO view of its own: the command's baseVertex word is read - // from the CPU shadow, so a command whose baseVertex a compute shader wrote this - // frame is not observable here (baseInstance is, through the view above). Feeding - // the stale-but-usually-correct shadow beats leaving the uniform at the previous - // draw's value, which is what a program reading gl_BaseVertex saw before. - const Bool feedBaseVertex = CurrentProgramReadsBaseVertex(); - for (GLsizei i = 0; i < drawcount; ++i) { - const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; - SetCurrentDrawID(static_cast(i)); - if (paramsBinding >= 0 && backendProgram) { - // baseInstance is the 5th word of DrawElementsIndirectCommand. - backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 16) / 4)); - if (feedBaseVertex) { - DrawElementsIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); - SetCurrentBaseVertex(cmd.baseVertex); - } - } else { - DrawElementsIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); - SetCurrentBaseInstance(cmd.baseInstance); - SetCurrentBaseVertex(cmd.baseVertex); - } - ForEachViewportRoutingPass([&] { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( - commandBytes, commandOffset, stride, i, static_cast(indexSize), drawIndirectBuffer)) - return; - g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(cmdByteOffset)); - }); - } - } else { - for (GLsizei i = 0; i < drawcount; ++i) { - DrawElementsIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); - if (cmd.count == 0 || cmd.instanceCount == 0) { - continue; - } - SetCurrentDrawID(static_cast(i)); - SetCurrentBaseInstance(cmd.baseInstance); - SetCurrentBaseVertex(cmd.baseVertex); - const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; - ForEachViewportRoutingPass([&] { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( - commandBytes, commandOffset, stride, i, static_cast(indexSize), drawIndirectBuffer)) - return; - g_GLESFuncs.glDrawElementsInstancedBaseVertex( - mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), - static_cast(cmd.instanceCount), cmd.baseVertex); - }); - } - } - SetCurrentDrawID(0); - SetCurrentBaseInstance(0); - SetCurrentBaseVertex(0); - } - - static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset, - const SharedPtr& drawIndirectBuffer, - GLsizei drawcount, GLsizei stride, const char* label) { - (void)label; - // DrawArraysIndirectCommand has no baseVertex word, so gl_BaseVertex is zero for every - // command here. Written BEFORE the draws, not merely restored after them: the previous - // draw is what leaves a stale value, and restoring afterwards would only protect the - // NEXT draw while these commands ran with the stale one. - SetCurrentBaseVertex(0); - // P5c (hd, CONTRACT-P5C §3.5): see ExecuteIndexedIndirectCommands - the verb's handle, - // never the frontend binding slot or the client allocator. - const MG_Pipe::MGPipeHandle verbBufferHandle = - MG_Pipe::MGPipeApplier().VerbIndirectBuffer; - const Bool useNative = - !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws(); - commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative, - /*indexed=*/false); - if (useNative) { - const auto backendProgram = GetCurrentBackendProgram(); - const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; - if (paramsBinding >= 0) { - auto* resource = - BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle); - if (resource && resource->id != 0) { - BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), - resource->id); - } - } - for (GLsizei i = 0; i < drawcount; ++i) { - const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; - SetCurrentDrawID(static_cast(i)); - if (paramsBinding >= 0 && backendProgram) { - // baseInstance is the 4th word of DrawArraysIndirectCommand. - backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 12) / 4)); - } else { - DrawArraysIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); - SetCurrentBaseInstance(cmd.baseInstance); - } - ForEachViewportRoutingPass([&] { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( - commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer)) - return; - g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast(cmdByteOffset)); - }); - } - } else { - for (GLsizei i = 0; i < drawcount; ++i) { - DrawArraysIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); - if (cmd.count == 0 || cmd.instanceCount == 0) { - continue; - } - SetCurrentDrawID(static_cast(i)); - SetCurrentBaseInstance(cmd.baseInstance); - ForEachViewportRoutingPass([&] { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( - commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer)) - return; - g_GLESFuncs.glDrawArraysInstanced(mode, static_cast(cmd.first), - static_cast(cmd.count), - static_cast(cmd.instanceCount)); - }); - } - } - SetCurrentDrawID(0); - SetCurrentBaseInstance(0); - } - - void PrepareForCompute(Bool includeDispatchIndirectBuffer) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // Single per-dispatch program resolve and texture-key capture, as in - // PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a - // pipeline bound this is its compute stage program, which is a whole program on its - // own - the graphics composite a draw builds carries no compute stage. - // - // P5e (pa), kimi row 92: THE DISPATCH TWIN OF THE SAME RETIREMENT, the same arm and the - // same conjunction - the three callees this value reaches are the ones PrepareForDraw - // reaches, minus the attribute-values sync (a dispatch has no vertex stage) and the - // frontend link/SPIR-V gate below, which already has a record arm of its own. These are - // the 7 strict-lane entries of GetProgramForDispatch@DispatchCompute. - const SharedPtr noFrontendProgram; - const auto& currentProgram = - noFrontendProgram; - const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys(); - - // Read BEFORE this dispatch's own binding walk sets it again (see the flag's comment). - const Bool storageWritesUnordered = BufferImpl::g_shaderStorageWritesUnordered; - BufferImpl::g_shaderStorageWritesUnordered = false; - BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer); - TextureImpl::SyncNeccessaryTextures(textureKeys); - TextureImpl::SyncImageTextureBindings(); - TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); - // Both syncs above may have re-uploaded a texture this dispatch reads or stores - // (Managers.h, NoteInPlaceTextureUpload), and the previous shader work may have left - // storage writes unordered; one barrier covers whichever of the two applies. - { - GLbitfield barriers = TextureImpl::TakeInPlaceTextureUploadBarrierBits(); - if (storageWritesUnordered) barriers |= GL_SHADER_STORAGE_BARRIER_BIT; - if (barriers != 0 && g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(barriers); - } - // P5e (pg), kimi row 92: the DISPATCH half of the same retirement - - // MGPipeApplier().DispatchProgram, which set_dispatch_program carried. A compute-only - // pipeline has no draw program at all, which is why the two applier fields are separate. - const MG_Pipe::MGPipeHandle dispatchCso = - MG_Pipe::MGPipeApplier().DispatchProgram; - PrgramImpl::SyncCurrentProgramByHandle(dispatchCso, false); - - // The same gate as the frontend one below, from the record: a dispatch of a program - // the frontend reports unlinked, or one whose SPIR-V never arrived, binds nothing. - const auto* const record = PrgramImpl::FindShaderCsoRecord(dispatchCso); - if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { - g_GLESFuncs.glUseProgram(0); - PrgramImpl::g_lastUsedBackendProgramId = 0; - return; - } - - // Compute shaders sample textures through the same unit bindings as draws - // (e.g. Flywheel's depth-pyramid downsample reads the depth attachment on - // unit 0), so re-establish unit bindings after the content syncs above. - BindCurrentTextures(textureKeys, currentProgram); - // Compute programs need the same per-program resource sync as draws: - // uniform-block bindings and sampler units only exist through the API - // because layout(binding) is stripped from the transpiled ESSL. - BindCurrentProgramWithResources(currentProgram, textureKeys, - dispatchCso - ); - } - - - void Clear(GLbitfield mask) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - TextureImpl::SyncNeccessaryTextures(); - FramebufferImpl::SyncCurrentFBO(); - // A colour clear is exactly the operation that is allowed to write a widened - // attachment's alpha - it is what puts the 1.0 there that every later draw is masked - // away from. SyncCurrentFBO ran first, so g_alphaWidenedDrawBufferMask already describes - // the framebuffer this clear will land on. - RenderStateImpl::SyncRenderState(/*forColorClear=*/(mask & GL_COLOR_BUFFER_BIT) != 0); - - BindCurrentFBO(FramebufferTarget::Draw); - - // Debug-only diagnostics: is each offscreen clear complete and unscissored? -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG - { - static int diagCount = 0; - GLint fboId = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fboId); - if (fboId != 0 && diagCount++ < 900) { - GLint color0 = 0, depthName = 0, box[4] = {0}; - GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST); - GLboolean cmask[4] = {0}; - GLfloat cc[4] = {0}; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &color0); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &depthName); - g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, box); - g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, cmask); - g_GLESFuncs.glGetFloatv(GL_COLOR_CLEAR_VALUE, cc); - MGLOG_D("CLEAR fbo=%d color0=%d depth=%d mask=0x%x scissor=%d box=(%d,%d,%d,%d) cmask=%d%d%d%d cc=(%g,%g,%g,%g)", - fboId, color0, depthName, mask, (int)scissor, box[0], box[1], box[2], box[3], (int)cmask[0], - (int)cmask[1], (int)cmask[2], (int)cmask[3], cc[0], cc[1], cc[2], cc[3]); - } - } -#endif - - // GLES clamps the glClearColor state to [0,1] (desktop GL keeps it unclamped - // for float color buffers). Minecraft's OIT clears its depth-bounds RGBA32F - // target to -FLT_MAX as the MAX-blend identity, so an out-of-range clear - // color must go through glClearBufferfv, which GLES does not clamp. - GLbitfield remainingMask = mask; - if ((mask & GL_COLOR_BUFFER_BIT) != 0) { - const FloatVec4& cc = MG_Pipe::gPipeInputs->GetRenderStateParameters().ClearColor; - const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f || - cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f; - // A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE - // clear colour to every draw buffer - so a framebuffer that mixes a widened - // attachment with a native one cannot be served by doctoring glClearColor. Take the - // same per-draw-buffer glClearBufferfv route the out-of-range case already uses and - // substitute the alpha only where it belongs. Scissor and the colour write mask apply - // to glClearBufferfv exactly as they do to glClear, so a scissored clear stays - // scissored and an application that masked alpha off still gets its way (the storage - // then keeps the 1.0 an earlier clear left, which is the same answer). - // - // glClearBufferfv on an INTEGER colour buffer is GL_INVALID_OPERATION, so a - // framebuffer with one of those as a draw buffer keeps plain glClear - which ES - // leaves undefined for integer colour buffers anyway, and which an application that - // wants a defined answer must replace with glClearBufferuiv/iv (those DO substitute - // the widened alpha). The out-of-range trigger is left exactly as it was. - const Uint32 widenedDrawBuffers = FramebufferImpl::g_alphaWidenedDrawBufferMask; - const Bool widenedColorClear = - widenedDrawBuffers != 0 && FramebufferImpl::g_integerColorDrawBufferMask == 0; - GLint clearDrawFbo = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo); - if ((outOfRange || widenedColorClear) && clearDrawFbo != 0) { - GLint maxDrawBuffers = 0; - GLint clearedCount = 0; - GLint firstDb = -1; - g_GLESFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); - for (GLint i = 0; i < maxDrawBuffers; ++i) { - GLint db = GL_NONE; - g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast(i), &db); - if (i == 0) firstDb = db; - if (db != GL_NONE) { - const Bool widened = i < 32 && (widenedDrawBuffers & (1u << i)) != 0; - const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), widened ? 1.0f : cc.w()}; - g_GLESFuncs.glClearBufferfv(GL_COLOR, i, value); - ++clearedCount; - } - } - remainingMask &= ~static_cast(GL_COLOR_BUFFER_BIT); - (void)clearedCount; - (void)firstDb; - // Debug-only diagnostics: verify the unclamped clear actually landed. -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG - { - static int diagCount = 0; - if (diagCount++ < 20) { - const GLenum clrErr = g_GLESFuncs.glGetError(); - GLint prevRead = 0, prevPbo = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead); - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPbo); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)clearDrawFbo); - GLint prevReadBuf = GL_COLOR_ATTACHMENT0; - g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &prevReadBuf); - g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); - GLfloat rb[4] = {0}; - g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb); - const GLenum rbErr = g_GLESFuncs.glGetError(); - int feDb0 = -1, feDb1 = -1; - Uint feIdx = 0, feVer = 0; - // P5e (fb): DEBUG-ONLY, and still a BARRIER_PULLED read. A diagnostic - // is not a reason to touch the binding slot on an apply thread, so - // under a transport the same four numbers come off the draw record - - // the external index has no record counterpart and stays 0, which reads - // as "the record did not say". - if (const auto* rec = BoundFramebufferRecord( - FramebufferTarget::Draw)) { - feVer = static_cast(rec->ContentHash); - feDb0 = (int)rec->DrawBuffers[0]; - feDb1 = (int)rec->DrawBuffers[1]; - } - MGLOG_D("CLEARV fbo=%d clrErr=0x%x rbErr=0x%x cc.x=%g cleared=%d firstDb=0x%x prevReadBuf=0x%x " - "feFbo=%u feVer=%u feDb=[%d,%d] stored=(%g,%g,%g,%g)", - clearDrawFbo, clrErr, rbErr, cc.x(), clearedCount, firstDb, prevReadBuf, feIdx, feVer, - feDb0, feDb1, rb[0], rb[1], rb[2], rb[3]); - g_GLESFuncs.glReadBuffer(static_cast(prevReadBuf)); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevRead); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, (GLuint)prevPbo); - } - } -#endif - } - } - if (remainingMask != 0) { - g_GLESFuncs.glClear(remainingMask); - } - } - - // --------------------------------------------------------------------------- - // Arbitrary-index primitive restart - // - // Desktop GL restarts on whatever index glPrimitiveRestartIndex named; GLES core only - // ever restarts on the all-ones value of the index type. When the two agree - which - // includes every GL_PRIMITIVE_RESTART_FIXED_INDEX user - the render state push at - // SyncRenderState is the whole implementation and nothing here does any work. When they - // disagree the index DATA is rewritten into a scratch element array buffer. - // - // This used to throw instead. A throw here unwinds a C++ exception through the C GL ABI - // and takes the process down - the same hazard GL_Texture.cpp and RenderState.cpp - // already call out - so an application that merely asked for a legal desktop feature - // died rather than got an error. - // --------------------------------------------------------------------------- - - namespace { - struct RestartScratchBuffer { - Uint id = 0; - SizeT capacity = 0; - }; - - RestartScratchBuffer g_restartIndices; - Vector g_restartStaging; - - // Past this the rewrite would stage and re-upload hundreds of megabytes on EVERY - // draw (the copy is not memoised, exactly as on the Vulkan side). Decline instead of - // trying: a draw that renders nothing is recoverable, a stall of that size is not. - constexpr SizeT kMaxRestartRewriteBytes = SizeT{1} << 26; // 64 MiB - - // The index type one step wider than this one, or 0 when there is none. Widening is how - // an all-ones value that is a REAL vertex index keeps its meaning while the all-ones - // value of the destination type serves as the restart sentinel: a source that cannot - // spell 0xFFFF cannot collide with a 16-bit sentinel, and likewise 8 -> 16. - GLenum WiderIndexType(GLenum indexType) { - switch (indexType) { - case GL_UNSIGNED_BYTE: return GL_UNSIGNED_SHORT; - case GL_UNSIGNED_SHORT: return GL_UNSIGNED_INT; - default: return 0; - } - } - - Uint32 ReadIndex(const Uint8* source, SizeT i, SizeT indexSize) { - switch (indexSize) { - case 1: return source[i]; - case 2: { - Uint16 narrow = 0; - std::memcpy(&narrow, source + i * 2, sizeof(narrow)); - return narrow; - } - default: { - Uint32 wide = 0; - std::memcpy(&wide, source + i * 4, sizeof(wide)); - return wide; - } - } - } - - void WriteIndex(Uint8* destination, SizeT i, SizeT indexSize, Uint32 value) { - switch (indexSize) { - case 1: destination[i] = static_cast(value); break; - case 2: { - const Uint16 narrow = static_cast(value); - std::memcpy(destination + i * 2, &narrow, sizeof(narrow)); - break; - } - default: std::memcpy(destination + i * 4, &value, sizeof(value)); break; - } - } - - // True when any index in the range already holds the type's all-ones value, i.e. when - // that value is doing double duty as a real vertex index and so cannot also be the - // restart sentinel. Only asked on the rare substitution path. - Bool ContainsFixedRestartIndex(const Uint8* source, SizeT indexCount, SizeT indexSize, - Uint32 fixedMax) { - for (SizeT i = 0; i < indexCount; ++i) { - if (ReadIndex(source, i, indexSize) == fixedMax) return true; - } - return false; - } - - // Copies index data, replacing every occurrence of the application's restart index with - // the all-ones value of the DESTINATION type - the only one GLES restarts on. The - // destination may be wider than the source, which is what makes the copy lossless: a - // source index equal to the source's all-ones value zero-extends to something the wider - // sentinel can never equal, so it stays the vertex it was. - // - // Same width in and out is the degenerate case, used when the source contains no - // all-ones index at all (nothing to protect) or when there is no wider type to move to. - // In that last case only - a GL_UNSIGNED_INT stream that really does use index - // 0xFFFFFFFF while asking to restart on a different one - a legal index has to be - // nudged to 0xFFFFFFFE, because 32 bits cannot hold both meanings. The caller logs it; - // it is the one input this feature cannot represent. - void RewriteRestartIndices(const Uint8* source, SizeT indexCount, SizeT sourceIndexSize, - SizeT destinationIndexSize, Uint32 applicationRestartIndex, - Uint32 destinationFixedMax, Vector& output) { - output.resize(indexCount * destinationIndexSize); - for (SizeT i = 0; i < indexCount; ++i) { - Uint32 value = ReadIndex(source, i, sourceIndexSize); - if (value == applicationRestartIndex) { - value = destinationFixedMax; - } else if (value == destinationFixedMax) { - // Only reachable when no widening was possible; see above. - value = destinationFixedMax - 1; - } - WriteIndex(output.data(), i, destinationIndexSize, value); - } - } - - // Whole-buffer respecify through the manager-wide staging target, so binding it - // disturbs no VAO state. glBufferData orphans the previous store, so the upload - // never waits on a draw still reading the old contents out of the same name. - Bool UploadRestartScratch(SizeT bytes, const void* data) { - if (g_restartIndices.id == 0) { - GLuint id = 0; - g_GLESFuncs.glGenBuffers(1, &id); - if (id == 0) return false; - g_restartIndices.id = id; - g_restartIndices.capacity = 0; - } - BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_restartIndices.id); - SizeT capacity = g_restartIndices.capacity == 0 ? bytes : g_restartIndices.capacity; - while (capacity < bytes) capacity *= 2; - g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(capacity), nullptr, - GL_STREAM_DRAW); - g_restartIndices.capacity = capacity; - if (data != nullptr && bytes != 0) { - g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast(bytes), data); - if (MG_Util::PipeStats::Enabled()) { - // Rewritten index list staged on the draw path: in a split build these - // bytes are the index-mirror-versus-ship decision of section 8. - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient, - static_cast(bytes)); - } - } - return true; - } - - } // namespace - - RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) { - if (!MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || - MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { - return RestartSubstitutionKind::None; - } - const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType); - if (fixedMax == 0) return RestartSubstitutionKind::None; - const Uint32 restartIndex = MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex(); - if (restartIndex == fixedMax) return RestartSubstitutionKind::None; - // Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index - // zero-extended against the full 32-bit state, so an index this type cannot hold matches - // nothing. Truncating instead - glPrimitiveRestartIndex(0x100) over GL_UNSIGNED_BYTE data - // becoming "restart on 0" - turns the most common index in any mesh into a restart. - if (restartIndex > fixedMax) return RestartSubstitutionKind::SuppressRestart; - return RestartSubstitutionKind::RewriteIndices; - } - - void OnRestartSubstitutionContextDestroyed() { - g_restartIndices = {}; - g_restartStaging.clear(); - g_restartStaging.shrink_to_fit(); - } - - ScopedSuppressedPrimitiveRestart::ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind) { - if (kind != RestartSubstitutionKind::SuppressRestart) return; - // SyncRenderState turned the driver's fixed-index restart on because GL_PRIMITIVE_RESTART - // is enabled; for this draw's index type it would restart on a value the application - // never named. Toggled directly rather than through the render-state shadow, and put back - // in the destructor, so the shadow stays true and the next draw pays nothing. - g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); - m_suppressed = true; - } - - ScopedSuppressedPrimitiveRestart::~ScopedSuppressedPrimitiveRestart() { - if (!m_suppressed) return; - g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX); - } - - ScopedRestartIndexSubstitution::ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, - const void* indices) - : m_kind(ResolveRestartSubstitution(indexType)), m_capOverride(m_kind), m_indices(indices), - m_indexType(indexType) { - if (m_kind != RestartSubstitutionKind::RewriteIndices) { - return; - } - const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType); - const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType); - const Uint32 applicationRestartIndex = MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex(); - - const Uint8* source = nullptr; - SizeT indexCount = 0; - SizeT sourceByteOffset = 0; - Bool hasElementBuffer = false; - // -1: monolith, or this arm never resolved a backend id, and the tail asks - // BoundElementArrayBufferId() exactly as it always did. - Int serverElementBinding = -1; - - // P5c (hd, CONTRACT-P5C §3.5): with an active transport this constructor runs on the - // apply thread, where the frontend BufferObject's legacy accessors are Fatal by name - // (BufferObject.cpp's "buffer-legacy-arm" refusal). The element buffer therefore - // resolves from the applier's set_index_buffer state (T2) and the bytes read from the - // server's own staged shadow - "the backend's ScopedRestartIndexSubstitution reads the - // index bytes it needs on its side" (CONTRACT-P5B d1), the same arm - // MultiDrawElementsIndirectCount takes for its command buffer. A draw whose record - // carried client indices (no index buffer bound) takes the client-pointer arm below, - // `indices` already resolved through the server's segment resolver. Monolith takes - // the original body, verbatim. - const auto& applierState = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle elements = applierState.IndexBuffer.Res; - serverElementBinding = 0; - if (!MG_Pipe::MGPipeHandleIsNull(elements)) { - auto* elementResource = BufferImpl::FindBufferResourceForHandle(elements); - // The WHOLE buffer is rewritten, not just this draw's range, so that every - // index keeps its position: an indirect draw's firstIndex lives in GPU memory - // and cannot be adjusted from here. It is an ELEMENT index, so it survives - // widening unchanged. - const SizeT sizeBytes = BufferImpl::ResourceWidthForHandle(elements); - if (sizeBytes < sourceIndexSize) { - return; // Nothing to restart on; let the driver see the draw unchanged. - } - if (sizeBytes > kMaxRestartRewriteBytes) { - MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element " - "array buffer rewritten every draw, which is past the %zu-byte ceiling. Use " - "GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones " - "value of the index type.", - applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes); - m_valid = false; - return; - } - // The staged shadow is the source of truth for CPU reads on this side: a - // persistent map's blocks were consumed by the applier before this verb ran, and - // (P8-C) a store a shader wrote on this side is read back from this server's own - // GL buffer first - no writeback ever reaches the server's copy. - const Uint8* hostBytes = - elementResource - ? BufferImpl::SplitHostBytesForCpuRead(*elementResource, elements, - "primitive_restart_substitution") - : nullptr; - if (hostBytes == nullptr) { - MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of " - "the bound element array buffer and none is available.", - applicationRestartIndex); - m_valid = false; - return; - } - // The M-3 rule for a whole-store reader (Managers.cpp:3238): for a store whose - // content the application SUPPLIED a coverage gap is a missing record and is - // Fatal by name; for one it ORPHANED the gap is its own undefined content and - // reading the shadow's zero-fill is exactly what the monolith arm's - // MappedData() answers. - const MG_Pipe::MGPipeResourceRecord* elementRecord = nullptr; - if (elements.Slot < applierState.Resources.size()) { - const auto& candidate = applierState.Resources[elements.Slot]; - if (candidate.Live && candidate.Gen == elements.Gen) elementRecord = &candidate; - } - if (elementRecord != nullptr && elementRecord->Desc.HasDefinedContent != 0) { - BufferImpl::RequireStagedCoverage(*elementResource, hostBytes, 0, sizeBytes, - "primitive_restart_substitution"); - } - hasElementBuffer = true; - serverElementBinding = static_cast(elementResource->id); - source = hostBytes; - indexCount = sizeBytes / sourceIndexSize; - sourceByteOffset = reinterpret_cast(indices); - } - if (!hasElementBuffer) { - // No element array buffer: `indices` is a client pointer, so only the draw's own - // range is readable and an indirect draw has nothing to read at all. - if (count <= 0 || indices == nullptr || sourceIndexSize == 0) { - MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs either a bound element " - "array buffer or a client index array with a CPU-known count.", - applicationRestartIndex); - m_valid = false; - return; - } - if (static_cast(count) * sourceIndexSize > kMaxRestartRewriteBytes) { - MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART index rewrite of %zu bytes is past the %zu-byte " - "ceiling.", - static_cast(count) * sourceIndexSize, kMaxRestartRewriteBytes); - m_valid = false; - return; - } - source = static_cast(indices); - indexCount = static_cast(count); - } - - // Widen only when the source really does use the all-ones value as a vertex index - - // otherwise the sentinel is free and the copy stays the caller's width, which keeps the - // common substitution allocation-for-allocation identical to the narrow form. - GLenum destinationType = indexType; - SizeT destinationIndexSize = sourceIndexSize; - if (ContainsFixedRestartIndex(source, indexCount, sourceIndexSize, fixedMax)) { - const GLenum wider = WiderIndexType(indexType); - // An element-array offset that is not a whole number of indices cannot be rescaled - // into the widened copy, so such a draw keeps the narrow (lossy) form. - const Bool offsetIsWholeIndices = sourceIndexSize != 0 && (sourceByteOffset % sourceIndexSize) == 0; - if (wider != 0 && offsetIsWholeIndices && - indexCount * MG_Util::GetGLTypeSize(wider) <= kMaxRestartRewriteBytes) { - destinationType = wider; - destinationIndexSize = MG_Util::GetGLTypeSize(wider); - } else { - MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses the " - "all-ones index %u: this index type cannot spell both, so every all-ones index is " - "drawn as %u instead. Use GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones " - "value out of the index data.", - applicationRestartIndex, fixedMax, fixedMax - 1); - } - } - - const Uint32 destinationFixedMax = MG_Util::FixedRestartIndexForGLType(destinationType); - RewriteRestartIndices(source, indexCount, sourceIndexSize, destinationIndexSize, applicationRestartIndex, - destinationFixedMax, g_restartStaging); - - if (!UploadRestartScratch(g_restartStaging.size(), g_restartStaging.data())) { - MGLOG_E_ONCE("Draw skipped: could not allocate the scratch element array buffer for GL_PRIMITIVE_RESTART " - "index substitution."); - m_valid = false; - return; - } - m_previousBinding = 0; - if (serverElementBinding >= 0) m_previousBinding = static_cast(serverElementBinding); - BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id); - m_substituted = true; - m_indexType = destinationType; - // The rewritten copy starts at byte 0 of the scratch buffer and holds one - // destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT - // offset (rescaled to the new width) and a client-memory draw reads from the front. - m_indices = hasElementBuffer - ? reinterpret_cast((sourceByteOffset / sourceIndexSize) * destinationIndexSize) - : nullptr; - } - - ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution() { - if (!m_substituted) return; - BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, m_previousBinding); - } - - void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return; -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawElements(mode, count, restart.IndexType(), restart.Indices()); - }); - } - - void DrawArrays(GLenum mode, GLint first, GLsizei count) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - DrawSyncFlags syncBit = DrawSyncBit::None; - PrepareForDraw(syncBit); - VertexArrayImpl::SyncClientSideVertexArraysForDrawArrays(first, count); - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawArrays(mode, first, count); - }); - } - - void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return; -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - SetCurrentBaseVertex(basevertex); - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex); - }); - SetCurrentBaseVertex(0); - } - - void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - DrawSyncFlags syncBit = DrawSyncBit::None; - PrepareForDraw(syncBit); - - // This loop IS the emulation - there is no batched tier for the non-indexed form - - // so each sub-draw has to be given its own gl_DrawID here, exactly as the indexed - // ladder and the indirect executors do. Without it every sub-draw of a - // glMultiDrawArrays read draw index 0. - const Bool feedDrawID = CurrentProgramReadsDrawID(); - for (GLsizei i = 0; i < drawcount; ++i) { - // Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path. - VertexArrayImpl::SyncClientSideVertexArraysForDrawArrays(first[i], count[i]); - if (feedDrawID) SetCurrentDrawID(static_cast(i)); - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawArrays(mode, first[i], count[i]); - }); - } - if (feedDrawID) SetCurrentDrawID(0); - } - - void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, - GLsizei drawcount, const GLint* basevertex) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex); - } - - void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - if (drawcount <= 0) { - return; - } - if (stride == 0) { - stride = sizeof(DrawElementsIndirectCommand); - } - if (stride < static_cast(sizeof(DrawElementsIndirectCommand))) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu", - stride, sizeof(DrawElementsIndirectCommand)); - return; - } - - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - - const SizeT indexSize = MG_Util::GetGLTypeSize(type); - if (indexSize == 0) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type); - return; - } - - const auto* commandBytes = ResolveIndirectCommandBytes( - indirect, - static_cast(stride) * static_cast(drawcount - 1) + sizeof(DrawElementsIndirectCommand), - "MultiDrawElementsIndirect"); - if (!commandBytes) { - return; - } - - const auto& drawIndirectBuffer = - // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot - // is never read - the Execute* helpers resolve the buffer from the verb record's - // handle (T2). Monolith reads the slot as it always did. - SharedPtr(nullptr); - ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), - drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect"); - } - - void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, - GLsizei maxdrawcount, GLsizei stride) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - if (maxdrawcount <= 0) { - return; - } - if (stride == 0) { - stride = sizeof(DrawElementsIndirectCommand); - } - if (stride < static_cast(sizeof(DrawElementsIndirectCommand))) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu", - stride, sizeof(DrawElementsIndirectCommand)); - return; - } - - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - - const SizeT indexSize = MG_Util::GetGLTypeSize(type); - if (indexSize == 0) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type); - return; - } - - // P5c (hd, CONTRACT-P5C §3.5): with an active transport both buffers resolve from the - // verb record's handles (MGPDrawIndirect::Buffer / ParameterBuffer) and the count - // reads from the SERVER's staged shadow - the frontend binding slots, the frontend - // accessors (B3) and the client allocator (T2) are never read. Monolith takes the - // original body below, verbatim. - const auto& applierState = MG_Pipe::MGPipeApplier(); - const SizeT commandOffset = reinterpret_cast(indirect); - const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + - sizeof(DrawElementsIndirectCommand); - auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer); - auto* paramResource = - BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer); - // P8-C: the count is CONSUMED here, so the parameter buffer is made current first - // (a store a shader wrote on this side is read back); the command buffer only gets a - // first copy when it has none - its fields are made current by the executor, and - // only when it consumes them (CurrentIndirectCommandBytes). - const Uint8* const parameterBytes = - paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource, - applierState.VerbIndirectParameterBuffer, - "indirect_count_parameter") - : nullptr; - const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr; - if (drawBytes == nullptr && drawResource != nullptr) { - drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer, - "indirect_count_commands"); - } - if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); - return; - } - if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > - BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); - return; - } - // No staged shadow means no count to read, not a wrong one - the monolith body's - // own rule for a buffer with no CPU shadow. - if (parameterBytes == nullptr || drawBytes == nullptr) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or " - "draw-indirect buffer"); - return; - } - BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes, - "multidraw_elements_indirect_count_commands"); - BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast(drawcount), - static_cast(drawcount) + sizeof(Uint32), - "multidraw_elements_indirect_count_parameter"); - Uint32 actualDrawCount = 0; - std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount)); - actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); - ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBytes + commandOffset, commandOffset, - nullptr, static_cast(actualDrawCount), stride, - "MultiDrawElementsIndirectCount"); - return; - } - - void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - if (drawcount <= 0) { - return; - } - if (stride == 0) { - stride = sizeof(DrawArraysIndirectCommand); - } - if (stride < static_cast(sizeof(DrawArraysIndirectCommand))) { - MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu", - stride, sizeof(DrawArraysIndirectCommand)); - return; - } - - DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - - const auto* commandBytes = ResolveIndirectCommandBytes( - indirect, - static_cast(stride) * static_cast(drawcount - 1) + sizeof(DrawArraysIndirectCommand), - "MultiDrawArraysIndirect"); - if (!commandBytes) { - return; - } - - const auto& drawIndirectBuffer = - // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot - // is never read - the Execute* helpers resolve the buffer from the verb record's - // handle (T2). Monolith reads the slot as it always did. - SharedPtr(nullptr); - ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, - drawcount, stride, "MultiDrawArraysIndirect"); - } - - // The non-indexed twin of MultiDrawElementsIndirectCount, and structurally identical to it: - // ES has no GL_PARAMETER_BUFFER at all, so the draw count is read from the CPU shadow of the - // bound one and the batch degenerates into an ordinary indirect multi-draw of that many - // commands. Missing from the backend table until now, which made every - // glMultiDrawArraysIndirectCount an INVALID_OPERATION ("backend does not support - // indirect-parameter array draws") on DirectGLES while the extension was advertised. - void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, - GLsizei stride) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - if (maxdrawcount <= 0) { - return; - } - if (stride == 0) { - stride = sizeof(DrawArraysIndirectCommand); - } - if (stride < static_cast(sizeof(DrawArraysIndirectCommand))) { - MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu", - stride, sizeof(DrawArraysIndirectCommand)); - return; - } - - DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - - // P5c (hd, CONTRACT-P5C §3.5): see the indexed twin - the verb record's handles and - // the SERVER's staged shadow, never the frontend bindings or the client allocator. - const auto& applierState = MG_Pipe::MGPipeApplier(); - const SizeT commandOffset = reinterpret_cast(indirect); - const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + - sizeof(DrawArraysIndirectCommand); - auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer); - auto* paramResource = - BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer); - // P8-C: the count is CONSUMED here, so the parameter buffer is made current first - // (a store a shader wrote on this side is read back); the command buffer only gets a - // first copy when it has none - its fields are made current by the executor, and - // only when it consumes them (CurrentIndirectCommandBytes). - const Uint8* const parameterBytes = - paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource, - applierState.VerbIndirectParameterBuffer, - "indirect_count_parameter") - : nullptr; - const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr; - if (drawBytes == nullptr && drawResource != nullptr) { - drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer, - "indirect_count_commands"); - } - if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) { - MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); - return; - } - if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > - BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) { - MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); - return; - } - if (parameterBytes == nullptr || drawBytes == nullptr) { - MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or " - "draw-indirect buffer"); - return; - } - BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes, - "multidraw_arrays_indirect_count_commands"); - BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast(drawcount), - static_cast(drawcount) + sizeof(Uint32), - "multidraw_arrays_indirect_count_parameter"); - Uint32 actualDrawCount = 0; - std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount)); - actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); - ExecuteArraysIndirectCommands(mode, drawBytes + commandOffset, commandOffset, nullptr, - static_cast(actualDrawCount), stride, - "MultiDrawArraysIndirectCount"); - return; - } - - void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, - const void* indices, GLint basevertex) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return; - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - SetCurrentBaseVertex(basevertex); - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, restart.IndexType(), restart.Indices(), - basevertex); - }); - SetCurrentBaseVertex(0); - } - - void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return; - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawRangeElements(mode, start, end, count, restart.IndexType(), restart.Indices()); - }); - } - - // True when the driver will apply baseInstance to the vertex fetch itself, in which case the - // attribute-offset emulation must stay out of the way. SetCurrentBaseInstance is orthogonal - // and runs either way - it feeds the shader's gl_BaseInstance, not the fetch. - inline Bool UseNativeBaseInstance() { - return g_GLESCapabilities.SupportsBaseInstance; - } - - // The emulated shift has to be in place before PrepareForDraw, because that is what syncs the - // VAO; a zero here is what un-shifts the arrays for the next ordinary draw. - // - // P3a (D-H2): this is the LEGACY arm's carrier. On the handle arm the draw's raw base - // instance rides in MGPVertexBuffers::BaseInstance - a ContentHash input, so a base-instance - // change that moves no buffer is still emitted rather than suppressed - and the server - // decides whether to shift, because emulation ownership is the server's. The scopes below - // are therefore compiled only where the pre-handle arm is (a pull build always). - inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) { - return UseNativeBaseInstance() ? 0u : static_cast(baseinstance); - } - -#define MGL_SCOPED_FETCH_BASE_INSTANCE(name, value) ((void)(value)) - - void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, - GLsizei instancecount, GLint basevertex, GLuint baseinstance) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex, - baseinstance)) return; - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; - MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - SetCurrentBaseInstance(baseinstance); - SetCurrentBaseVertex(basevertex); - ForEachViewportRoutingPass([&] { - if (UseNativeBaseInstance()) { - g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, restart.IndexType(), - restart.Indices(), instancecount, - basevertex, baseinstance); - } else { - g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(), - instancecount, basevertex); - } - }); - SetCurrentBaseVertex(0); - SetCurrentBaseInstance(0); - } - - void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, - GLsizei instancecount, GLint basevertex) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex, - 0)) return; - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - SetCurrentBaseVertex(basevertex); - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount, - basevertex); - }); - SetCurrentBaseVertex(0); - } - - void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, - GLsizei instancecount, GLuint baseinstance) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, - baseinstance)) return; - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; - MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - SetCurrentBaseInstance(baseinstance); - ForEachViewportRoutingPass([&] { - if (UseNativeBaseInstance()) { - g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(), - instancecount, baseinstance); - } else { - g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount); - } - }); - SetCurrentBaseInstance(0); - } - - void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { - if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, 0)) - return; - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - const ScopedRestartIndexSubstitution restart(type, count, indices); - if (!restart.DrawIsValid()) return; - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount); - }); - } - - void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { - DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - - const SizeT indexSize = MG_Util::GetGLTypeSize(type); - if (indexSize == 0) { - MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type); - return; - } - - const auto* commandBytes = - ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand), "DrawElementsIndirect"); - if (!commandBytes) { - return; - } - - const auto& drawIndirectBuffer = - // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot - // is never read - the Execute* helpers resolve the buffer from the verb record's - // handle (T2). Monolith reads the slot as it always did. - SharedPtr(nullptr); - ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), - drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand), - "DrawElementsIndirect"); - } - - void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, - GLuint baseinstance) { - DrawSyncFlags syncBit = DrawSyncBit::Instancing; - MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); - PrepareForDraw(syncBit); - SetCurrentBaseInstance(baseinstance); - ForEachViewportRoutingPass([&] { - if (UseNativeBaseInstance()) { - g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance); - } else { - g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); - } - }); - SetCurrentBaseInstance(0); - } - - void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { - DrawSyncFlags syncBit = DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - ForEachViewportRoutingPass([&] { - g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); - }); - } - - void DrawArraysIndirect(GLenum mode, const void* indirect) { - DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; - PrepareForDraw(syncBit); - - const auto* commandBytes = - ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); - if (!commandBytes) { - return; - } - - const auto& drawIndirectBuffer = - // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot - // is never read - the Execute* helpers resolve the buffer from the verb record's - // handle (T2). Monolith reads the slot as it always did. - SharedPtr(nullptr); - ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, 1, - sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); - } - - // Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a - // lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would - // otherwise spin an unbounded drain forever inside whichever GL entry point happened to be - // cleaning up, which is how a GPU reset reads as an unkillable process whose log simply - // stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the - // diagnostic. Every drain in this backend goes through here so the bound cannot drift apart - // between them. - static constexpr Int kMaxDrainedGLErrors = 32; - - static void DrainDriverErrors(const char* site) { - Int drained = 0; - while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) { - ++drained; - } - if (drained == kMaxDrainedGLErrors) { - MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost", - site, kMaxDrainedGLErrors); - } - } - - static void DrainBlitErrors() { DrainDriverErrors("BlitFramebuffer"); } - - // Sized internal format of the currently bound READ framebuffer's read colour - // attachment, 0 when it cannot be determined. - static GLenum QueryReadColorAttachmentInternalFormat() { - GLint attachmentType = 0; - GLint attachmentName = 0; - // Ask the point the backend read buffer actually names, not COLOR_ATTACHMENT0. The colour map - // in BackendFramebufferObject is a permutation, so the read attachment's image only sits at - // CA0 when that map is identity; querying CA0 unconditionally would size the resolve - // renderbuffer from a different attachment's format and either convert wrongly or fail the - // blit outright. - GLint readBuffer = GL_COLOR_ATTACHMENT0; - g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); - if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) { - readBuffer = GL_COLOR_ATTACHMENT0; - } - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &attachmentType); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &attachmentName); - if (attachmentName == 0) { - return 0; - } - GLint internalFormat = 0; - if (attachmentType == GL_RENDERBUFFER) { - if (!g_GLESFuncs.glGetRenderbufferParameteriv) return 0; - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(attachmentName)); - g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, - &internalFormat); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); - } else if (attachmentType == GL_TEXTURE) { - if (!g_GLESFuncs.glGetTexLevelParameteriv) return 0; - // The resolve source of interest is always a multisample 2D texture; a - // single-sample source would not have taken the fallback in the first place. - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &previous); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast(attachmentName)); - g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D_MULTISAMPLE, 0, GL_TEXTURE_INTERNAL_FORMAT, - &internalFormat); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast(previous)); - } - return static_cast(internalFormat); - } - - // ES rejects any blit out of a multisample read framebuffer whose format differs - // from the draw framebuffer's. Desktop GL only requires identical formats when BOTH - // framebuffers are multisampled, so a multisample resolve is allowed to convert - // format on the way out (KHR-GL3x.framebuffer_blit resolves an R8/R16F multisample - // texture straight into an RGBA8 target). Emulate it in two steps: resolve into a - // scratch buffer of the source's own format, then run the caller's blit from there - - // that second one is single-sample on both sides, where ES does allow conversion. - static Bool ResolveThenBlit(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, - GLint dstX1, GLint dstY1, GLenum filter) { - static NativeContextName s_resolveFramebuffer; // per context: a framebuffer is never shared - static Uint s_resolveRenderbuffer = 0; - static GLenum s_resolveFormat = 0; - static GLsizei s_resolveWidth = 0; - static GLsizei s_resolveHeight = 0; - static Uint s_resolveContextGeneration = ~0u; - - if (!g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glGenRenderbuffers || - !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glFramebufferRenderbuffer) { - return false; - } - const GLenum sourceFormat = QueryReadColorAttachmentInternalFormat(); - if (sourceFormat == 0) { - return false; - } - - const GLint left = std::min(srcX0, srcX1); - const GLint right = std::max(srcX0, srcX1); - const GLint bottom = std::min(srcY0, srcY1); - const GLint top = std::max(srcY0, srcY1); - const GLsizei width = static_cast(right - left); - const GLsizei height = static_cast(top - bottom); - if (width <= 0 || height <= 0) { - return false; - } - - if (s_resolveContextGeneration != g_backendContextGeneration) { - // The ids belonged to a dead context; the context reclaimed them with it. - s_resolveFramebuffer = 0; - s_resolveRenderbuffer = 0; - s_resolveFormat = 0; - s_resolveContextGeneration = g_backendContextGeneration; - } - if (s_resolveFramebuffer == 0) { - g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer); - if (s_resolveFramebuffer == 0) return false; - } - if (s_resolveRenderbuffer == 0) { - g_GLESFuncs.glGenRenderbuffers(1, &s_resolveRenderbuffer); - if (s_resolveRenderbuffer == 0) return false; - s_resolveFormat = 0; - } - - GLint previousRenderbuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previousRenderbuffer); - if (s_resolveFormat != sourceFormat || s_resolveWidth < width || s_resolveHeight < height) { - s_resolveWidth = std::max(s_resolveWidth, width); - s_resolveHeight = std::max(s_resolveHeight, height); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, s_resolveRenderbuffer); - g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, sourceFormat, s_resolveWidth, s_resolveHeight); - s_resolveFormat = sourceFormat; - } - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previousRenderbuffer)); - - GLint previousDraw = 0; - GLint previousRead = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); - - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_resolveFramebuffer); - g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - s_resolveRenderbuffer); - Bool resolved = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; - if (resolved) { - DrainBlitErrors(); - // A blit is scissored like a draw (the replicate path's guard documents the - // same rule): the application's box would clip this resolve into the - // scratch, and the second blit would then copy never-written scratch texels - // into the destination - silently, since scissor clipping raises no GL - // error. Disable for the staging blit only; the caller-visible blit below - // keeps the blit's native scissor semantics. Tracked via the render-state - // shadow, exactly like ScopedScissorDisable. - const Bool scissorWasEnabled = RenderStateImpl::DriverScissorTestEnabled(); - if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, GL_COLOR_BUFFER_BIT, - GL_NEAREST); - if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); - resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - if (resolved) { - // Mirror the caller's orientation into the scratch-relative source rect so a - // flipped blit stays flipped. - const GLint blitX0 = srcX0 <= srcX1 ? 0 : width; - const GLint blitX1 = srcX0 <= srcX1 ? width : 0; - const GLint blitY0 = srcY0 <= srcY1 ? 0 : height; - const GLint blitY1 = srcY0 <= srcY1 ? height : 0; - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_resolveFramebuffer); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); - DrainBlitErrors(); - g_GLESFuncs.glBlitFramebuffer(blitX0, blitY0, blitX1, blitY1, dstX0, dstY0, dstX1, dstY1, - GL_COLOR_BUFFER_BIT, filter); - resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - if (!resolved) { - MGLOG_E_ONCE("BlitFramebuffer: multisample resolve fallback failed"); - } - return resolved; - } - - // --------------------------------------------------------------------------------- - // Shared guard for the emulation passes that have to DRAW to get their work done: the - // single-sample -> multisample blit replicate below, and the depth/stencil readback - // emulation further down. Both borrow the application's ES context for a full-screen - // pass, so everything they disturb is captured here and put back on the way out - the - // sync layer's shadow of the driver state has to stay true, and one leaked binding - // regresses every draw that follows. - // - // Three members of that set are not obvious: - // - // - The borrowed texture unit. Both passes bind their scratch texture to - // TextureImpl::TempTextureUnit, and the unit's binding is only restored correctly by - // asking TextureImpl::g_boundTexturesCache what is supposed to be there: the raw - // glBindTexture the passes issue never moves that cache, so restoring a *queried* - // id leaves the driver and the cache disagreeing and the per-draw binding memo - // false-skips the re-bind - the borrowed-slot failure mode that showed up as - // process-wide glyph death under Iris. Reading GL_TEXTURE_BINDING_2D was doubly - // wrong here because the replicate path may already have switched units and bound - // its own scratch texture by the time it asked. - // - // - The sampler object on that unit. It would override the scratch texture's own - // filter and compare parameters. Unbinding through SamplerImpl::UnbindSampler moves - // the sampler cache, which is exactly what re-opens BindCurrentUnitSamplers' memo, - // so the application's sampler comes back on the next draw with no explicit restore. - // - // - An active transform feedback capture. GL rejects a draw issued with a program - // other than the one that began the capture, and would otherwise append the - // emulation's vertices to the application's buffers. - // - // - The application's clip control. The passes place their triangle in the default - // lower-left, [-1, 1] clip space; an upper-left origin would mirror it vertically. - class ScopedEmulationDrawState { - public: - ScopedEmulationDrawState() { - g_GLESFuncs.glGetIntegerv(GL_CURRENT_PROGRAM, &m_program); - g_GLESFuncs.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &m_vertexArray); - g_GLESFuncs.glGetIntegerv(GL_VIEWPORT, m_viewport); - g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, m_scissorBox); - g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, m_colorMask); - g_GLESFuncs.glGetIntegerv(GL_DEPTH_FUNC, &m_depthFunc); - g_GLESFuncs.glGetBooleanv(GL_DEPTH_WRITEMASK, &m_depthMask); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_FUNC, &m_stencilFunc[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FUNC, &m_stencilFunc[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_REF, &m_stencilRef[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_REF, &m_stencilRef[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_VALUE_MASK, &m_stencilValueMask[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &m_stencilValueMask[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_WRITEMASK, &m_stencilWriteMask[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &m_stencilWriteMask[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_FAIL, &m_stencilFail[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FAIL, &m_stencilFail[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &m_stencilDepthFail[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &m_stencilDepthFail[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &m_stencilPass[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &m_stencilPass[1]); - if (g_GLESCapabilities.SupportsClipDistance) { - m_capabilityCount = kBaseCapabilityCount + kClipDistanceCapabilityCount; - } - for (Uint i = 0; i < m_capabilityCount; ++i) { - m_capabilities[i].enabled = g_GLESFuncs.glIsEnabled(m_capabilities[i].cap); - } - // GL_SAMPLE_MASK is ES 3.1; on an older driver the query above just raised - // GL_INVALID_ENUM and answered GL_FALSE, which is also the right thing to - // restore. Drop the flag so it is not misattributed to the emulation's own work. - DrainBlitErrors(); - - if (MG_Pipe::gPipeInputs->IsTransformFeedbackActive() && - !MG_Pipe::gPipeInputs->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) { - g_GLESFuncs.glPauseTransformFeedback(); - m_pausedTransformFeedback = true; - DrainBlitErrors(); - } - - if (g_GLESCapabilities.SupportsClipControl) { - g_GLESFuncs.glGetIntegerv(GL_CLIP_ORIGIN, &m_clipOrigin); - g_GLESFuncs.glGetIntegerv(GL_CLIP_DEPTH_MODE, &m_clipDepthMode); - g_GLESFuncs.glClipControlEXT(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE); - } - - m_activeTextureUnit = TextureImpl::g_activeTextureUnit; - TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); - SamplerImpl::UnbindSampler(TextureImpl::TempTextureUnit); - - // The neutral baseline every emulation pass wants: nothing culled, nothing - // clipped, nothing tested, no coverage games, and colour writes open. Callers - // turn back on only what they need (the replicate pass wants the depth and - // stencil tests, and masks colour off because it writes neither). - for (Uint i = 0; i < m_capabilityCount; ++i) { - g_GLESFuncs.glDisable(m_capabilities[i].cap); - } - g_GLESFuncs.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - g_GLESFuncs.glDepthMask(GL_FALSE); - DrainBlitErrors(); - } - - ~ScopedEmulationDrawState() { - g_GLESFuncs.glUseProgram(static_cast(m_program)); - // Put back whatever the binding cache says lives on the borrowed unit, not what - // the driver happened to hold: see the class comment. - auto* cachedBound = - TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] - [static_cast(TextureTarget::Texture2D)]; - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0); - TextureImpl::ActivateTextureUnit(m_activeTextureUnit); - VertexArrayImpl::BindBackendVAOId(static_cast(m_vertexArray)); - g_GLESFuncs.glViewport(m_viewport[0], m_viewport[1], m_viewport[2], m_viewport[3]); - g_GLESFuncs.glScissor(m_scissorBox[0], m_scissorBox[1], m_scissorBox[2], m_scissorBox[3]); - g_GLESFuncs.glColorMask(m_colorMask[0], m_colorMask[1], m_colorMask[2], m_colorMask[3]); - g_GLESFuncs.glDepthFunc(static_cast(m_depthFunc)); - g_GLESFuncs.glDepthMask(m_depthMask); - const GLenum faces[2] = {GL_FRONT, GL_BACK}; - for (SizeT face = 0; face < 2; ++face) { - g_GLESFuncs.glStencilFuncSeparate(faces[face], static_cast(m_stencilFunc[face]), - m_stencilRef[face], - static_cast(m_stencilValueMask[face])); - g_GLESFuncs.glStencilOpSeparate(faces[face], static_cast(m_stencilFail[face]), - static_cast(m_stencilDepthFail[face]), - static_cast(m_stencilPass[face])); - g_GLESFuncs.glStencilMaskSeparate(faces[face], static_cast(m_stencilWriteMask[face])); - } - for (Uint i = 0; i < m_capabilityCount; ++i) { - if (m_capabilities[i].enabled) { - g_GLESFuncs.glEnable(m_capabilities[i].cap); - } else { - g_GLESFuncs.glDisable(m_capabilities[i].cap); - } - } - // The per-draw-buffer colour masks are not covered by the non-indexed - // glColorMask above. Restore what the SYNC actually pushed, not the raw - // application masks: a widened attachment's alpha write is forced off by - // SyncRenderState and memoized in g_syncedColorMaskAlphaWidenMask, and the - // next sync early-outs on an unchanged version - restoring the undoctored - // mask here would leave alpha writes enabled on the widened buffer with - // nothing left to repair it. Same three-way pointer fallback as - // SyncRenderState's push: gating on the core name alone left EXT/OES-only - // devices holding buffer 0's mask broadcast across every buffer. - const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski - : g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT - : g_GLESFuncs.glColorMaskiOES; - if (colorMaskiFn) { - for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { - BoolVec4 colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; - if (index < 32 && (RenderStateImpl::g_syncedColorMaskAlphaWidenMask & (1u << index)) != 0) { - colorMask.w() = false; - } - colorMaskiFn(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE, - colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE); - } - } - if (g_GLESCapabilities.SupportsClipControl) { - g_GLESFuncs.glClipControlEXT(static_cast(m_clipOrigin), static_cast(m_clipDepthMode)); - } - if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) { - g_GLESFuncs.glResumeTransformFeedback(); - } - DrainBlitErrors(); - } - - ScopedEmulationDrawState(const ScopedEmulationDrawState&) = delete; - ScopedEmulationDrawState& operator=(const ScopedEmulationDrawState&) = delete; - - private: - struct CapabilityState { - GLenum cap; - GLboolean enabled; - }; - - GLint m_program = 0; - GLint m_vertexArray = 0; - GLint m_viewport[4] = {0, 0, 0, 0}; - GLint m_scissorBox[4] = {0, 0, 0, 0}; - GLboolean m_colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; - GLint m_depthFunc = GL_LESS; - GLboolean m_depthMask = GL_TRUE; - GLint m_stencilFunc[2] = {GL_ALWAYS, GL_ALWAYS}; - GLint m_stencilRef[2] = {0, 0}; - GLint m_stencilValueMask[2] = {~0, ~0}; - GLint m_stencilWriteMask[2] = {~0, ~0}; - GLint m_stencilFail[2] = {GL_KEEP, GL_KEEP}; - GLint m_stencilDepthFail[2] = {GL_KEEP, GL_KEEP}; - GLint m_stencilPass[2] = {GL_KEEP, GL_KEEP}; - Uint m_activeTextureUnit = 0; - Bool m_pausedTransformFeedback = false; - GLint m_clipOrigin = GL_LOWER_LEFT; - GLint m_clipDepthMode = GL_NEGATIVE_ONE_TO_ONE; - // The eight GL_CLIP_DISTANCE0_EXT..7_EXT entries are last so that a driver without - // GL_EXT_clip_cull_distance can be served by shortening the count instead of asking - // it about tokens it does not know. They belong here at all because an emulation pass - // draws its full-screen triangle with its OWN program, which writes no gl_ClipDistance: - // leaving the app's enables on would clip that triangle by undefined distances. - static constexpr Uint kBaseCapabilityCount = 10; - static constexpr Uint kClipDistanceCapabilityCount = 8; - Uint m_capabilityCount = kBaseCapabilityCount; - CapabilityState m_capabilities[kBaseCapabilityCount + kClipDistanceCapabilityCount] = { - {GL_SCISSOR_TEST, GL_FALSE}, {GL_DEPTH_TEST, GL_FALSE}, - {GL_STENCIL_TEST, GL_FALSE}, {GL_CULL_FACE, GL_FALSE}, - {GL_BLEND, GL_FALSE}, {GL_RASTERIZER_DISCARD, GL_FALSE}, - {GL_POLYGON_OFFSET_FILL, GL_FALSE}, {GL_SAMPLE_ALPHA_TO_COVERAGE, GL_FALSE}, - {GL_SAMPLE_COVERAGE, GL_FALSE}, {GL_SAMPLE_MASK, GL_FALSE}, - {0x3000, GL_FALSE}, {0x3001, GL_FALSE}, - {0x3002, GL_FALSE}, {0x3003, GL_FALSE}, - {0x3004, GL_FALSE}, {0x3005, GL_FALSE}, - {0x3006, GL_FALSE}, {0x3007, GL_FALSE}, - }; - }; - - // --------------------------------------------------------------------------------- - // Single-sample -> multisample blit ("replicate") - // - // Desktop GL replicates the source sample into every destination sample when the read - // framebuffer is single-sampled and the draw framebuffer is not. ES forbids the whole - // call ("INVALID_OPERATION if SAMPLE_BUFFERS for the draw framebuffer is greater than - // zero"), so the blit silently did nothing - KHR-GL3x.packed_depth_stencil.blit's - // second loop then read a destination that still held its clear values. - // - // Emulated by drawing a full-screen triangle into the multisample framebuffer: every - // pixel is fully covered, so every sample of it receives the same value, which is - // exactly what the replicate rule asks for. Depth comes from gl_FragDepth; stencil has - // no shader output on ES, so it is written one bit plane at a time with REPLACE and a - // discard for the pixels whose source bit is clear. - // Defined further down with the other small GL helpers; the attachment-format probe below - // needs it to tell a rejected bind apart from a successful one. - static void ClearGLErrors(); - - namespace ReplicateBlitImpl { - static Uint s_contextGeneration = ~0u; - static NativeContextName s_framebuffer; // per context, as is the vertex array below - static GLuint s_texture = 0; - static GLenum s_textureFormat = 0; - static GLsizei s_textureWidth = 0; - static GLsizei s_textureHeight = 0; - static NativeContextName s_vertexArray; - static GLuint s_depthProgram = 0; - static GLuint s_stencilProgram = 0; - static GLint s_depthUvTransform = -1; - static GLint s_stencilUvTransform = -1; - static GLint s_stencilBit = -1; - static Bool s_programsFailed = false; - - static const char* const kVertexSource = - "#version 300 es\n" - "uniform vec4 uUvTransform;\n" - "out vec2 vUv;\n" - "void main() {\n" - " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" - " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" - " vUv = p * uUvTransform.xy + uUvTransform.zw;\n" - "}\n"; - - static const char* const kDepthFragmentSource = - "#version 300 es\n" - "precision highp float;\n" - "precision highp sampler2D;\n" - "uniform sampler2D uSource;\n" - "in vec2 vUv;\n" - "void main() {\n" - " gl_FragDepth = texture(uSource, vUv).r;\n" - "}\n"; - - static const char* const kStencilFragmentSource = - "#version 300 es\n" - "precision highp float;\n" - "precision highp usampler2D;\n" - "uniform usampler2D uSource;\n" - "uniform uint uBit;\n" - "in vec2 vUv;\n" - "void main() {\n" - " if ((texture(uSource, vUv).r & uBit) == 0u) discard;\n" - "}\n"; - - static GLuint BuildProgram(const char* fragmentSource) { - const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER); - const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER); - if (vertexShader == 0 || fragmentShader == 0) { - return 0; - } - g_GLESFuncs.glShaderSource(vertexShader, 1, &kVertexSource, nullptr); - g_GLESFuncs.glCompileShader(vertexShader); - g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); - g_GLESFuncs.glCompileShader(fragmentShader); - - const GLuint program = g_GLESFuncs.glCreateProgram(); - GLint linked = GL_FALSE; - if (program != 0) { - g_GLESFuncs.glAttachShader(program, vertexShader); - g_GLESFuncs.glAttachShader(program, fragmentShader); - g_GLESFuncs.glLinkProgram(program); - g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); - } - g_GLESFuncs.glDeleteShader(vertexShader); - g_GLESFuncs.glDeleteShader(fragmentShader); - if (linked != GL_TRUE) { - if (program != 0) g_GLESFuncs.glDeleteProgram(program); - return 0; - } - return program; - } - - static Bool EnsureResources() { - if (s_contextGeneration != g_backendContextGeneration) { - // The ids belonged to a dead context; the context reclaimed them with it. - s_framebuffer = 0; - s_texture = 0; - s_textureFormat = 0; - s_textureWidth = 0; - s_textureHeight = 0; - s_vertexArray = 0; - s_depthProgram = 0; - s_stencilProgram = 0; - s_programsFailed = false; - s_contextGeneration = g_backendContextGeneration; - } - if (s_programsFailed) { - return false; - } - if (s_depthProgram == 0) { - s_depthProgram = BuildProgram(kDepthFragmentSource); - s_stencilProgram = BuildProgram(kStencilFragmentSource); - if (s_depthProgram == 0 || s_stencilProgram == 0) { - s_programsFailed = true; - MGLOG_E_ONCE("BlitFramebuffer: could not build the multisample replicate programs"); - return false; - } - s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); - s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); - s_stencilBit = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uBit"); - } - if (s_framebuffer == 0) { - g_GLESFuncs.glGenFramebuffers(1, &s_framebuffer); - if (s_framebuffer == 0) return false; - } - if (s_vertexArray == 0) { - g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray); - if (s_vertexArray == 0) return false; - } - return true; - } - - // Sized internal format of ONE attachment point of the bound READ framebuffer, or 0 - // when there is no object there to ask (the default framebuffer's buffers have no - // queryable format at all). The scratch copy has to use the very same one: ES rejects - // a depth/stencil blit between differing formats even when both sides are - // single-sampled. - // - // The texture branch can only ask about a GL_TEXTURE_2D, and a name whose target is - // something else (an array or cube texture attached by glFramebufferTextureLayer / - // glFramebufferTexture) makes glBindTexture answer GL_INVALID_OPERATION and change - // nothing. Reading glGetTexLevelParameteriv after that failed bind does NOT return 0 - - // it truthfully describes whatever texture was already on GL_TEXTURE_2D, which on this - // path is the emulation's own staging scratch. That is a wrong answer that looks like a - // right one, so the bind has to be error-checked rather than trusted. - static GLenum QueryAttachmentSizedFormat(GLenum attachment) { - GLint objectType = 0; - GLint objectName = 0; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName); - if (objectName == 0) { - return 0; - } - GLint internalFormat = 0; - if (objectType == GL_RENDERBUFFER) { - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); - ClearGLErrors(); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(objectName)); - const Bool bound = g_GLESFuncs.glGetError() == GL_NO_ERROR; - if (bound) { - g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, - &internalFormat); - } - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); - ClearGLErrors(); - } else if (objectType == GL_TEXTURE) { - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous); - ClearGLErrors(); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(objectName)); - const Bool bound = g_GLESFuncs.glGetError() == GL_NO_ERROR; - if (bound) { - g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, - &internalFormat); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - internalFormat = 0; - } - } - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previous)); - ClearGLErrors(); - } - return static_cast(internalFormat); - } - - // The read framebuffer's depth format, or - when it has no depth - its stencil one. - static GLenum QueryReadDepthStencilFormat(GLenum* outAttachment) { - const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT}; - for (const GLenum attachment : attachments) { - const GLenum internalFormat = QueryAttachmentSizedFormat(attachment); - if (internalFormat != 0) { - if (outAttachment) *outAttachment = attachment; - return internalFormat; - } - } - return 0; - } - - static Bool FormatHasDepth(GLenum internalFormat) { - return internalFormat == GL_DEPTH_COMPONENT16 || internalFormat == GL_DEPTH_COMPONENT24 || - internalFormat == GL_DEPTH_COMPONENT32F || internalFormat == GL_DEPTH24_STENCIL8 || - internalFormat == GL_DEPTH32F_STENCIL8; - } - - static Bool FormatHasStencil(GLenum internalFormat) { - return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 || - internalFormat == GL_STENCIL_INDEX8; - } - - static GLenum ScratchAttachmentFor(GLenum internalFormat) { - if (FormatHasDepth(internalFormat) && FormatHasStencil(internalFormat)) { - return GL_DEPTH_STENCIL_ATTACHMENT; - } - return FormatHasDepth(internalFormat) ? GL_DEPTH_ATTACHMENT : GL_STENCIL_ATTACHMENT; - } - } // namespace ReplicateBlitImpl - - // Returns true when the request was serviced (or is not this fallback's business). - static Bool ReplicateBlitIntoMultisampleDraw(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, - GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) { - using namespace ReplicateBlitImpl; - if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { - return false; - } - if (!EnsureResources()) { - return false; - } - - const GLsizei srcWidth = static_cast(std::abs(srcX1 - srcX0)); - const GLsizei srcHeight = static_cast(std::abs(srcY1 - srcY0)); - const GLsizei dstWidth = static_cast(std::abs(dstX1 - dstX0)); - const GLsizei dstHeight = static_cast(std::abs(dstY1 - dstY0)); - if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { - return false; - } - - GLenum readAttachment = GL_DEPTH_ATTACHMENT; - const GLenum sourceFormat = QueryReadDepthStencilFormat(&readAttachment); - if (sourceFormat == 0) { - return false; - } - const Bool wantDepth = (mask & GL_DEPTH_BUFFER_BIT) != 0 && FormatHasDepth(sourceFormat); - const Bool wantStencil = (mask & GL_STENCIL_BUFFER_BIT) != 0 && FormatHasStencil(sourceFormat); - if (!wantDepth && !wantStencil) { - return false; - } - // Sampling the stencil half of a packed texture goes through - // GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the pname - // would just raise GL_INVALID_ENUM and the shader would read depth bits as stencil. - const Bool supportsStencilTextureMode = g_GLESCapabilities.GLESVersion.Major > 3 || - (g_GLESCapabilities.GLESVersion.Major == 3 && - g_GLESCapabilities.GLESVersion.Minor >= 1); - if (wantStencil && !supportsStencilTextureMode) { - return false; - } - - GLint previousDraw = 0; - GLint previousRead = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); - - // Everything below borrows the application's context - a texture unit for the - // scratch sampling and the whole rasterization pipeline for the replicate passes - - // so the guard is taken before the first of those, not just before the draws. It - // also puts the scissor test where the staging blit below needs it: a blit is - // scissored like a draw, and the application's box would otherwise clip the copy - // into the scratch texture. - ScopedEmulationDrawState emulationState; - - // Copy the source rectangle into a scratch texture of its own format: both sides of - // that blit are single-sampled, which ES does allow. - Bool ok = true; - if (s_texture == 0 || s_textureFormat != sourceFormat || s_textureWidth < srcWidth || - s_textureHeight < srcHeight) { - if (s_texture != 0) { - g_GLESFuncs.glDeleteTextures(1, &s_texture); // immutable storage cannot be resized - s_texture = 0; - } - s_textureWidth = std::max(s_textureWidth, srcWidth); - s_textureHeight = std::max(s_textureHeight, srcHeight); - g_GLESFuncs.glGenTextures(1, &s_texture); - if (s_texture == 0) { - ok = false; - } else { - // The guard already activated TempTextureUnit and cleared its sampler. - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); - DrainBlitErrors(); - g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, sourceFormat, s_textureWidth, s_textureHeight); - ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - s_textureFormat = ok ? sourceFormat : 0; - } - } - - if (ok) { - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_framebuffer); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, ScratchAttachmentFor(sourceFormat), GL_TEXTURE_2D, - s_texture, 0); - ok = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; - } - if (ok) { - const GLint left = std::min(srcX0, srcX1); - const GLint bottom = std::min(srcY0, srcY1); - DrainBlitErrors(); - g_GLESFuncs.glBlitFramebuffer(left, bottom, left + srcWidth, bottom + srcHeight, 0, 0, srcWidth, srcHeight, - mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT), GL_NEAREST); - ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - if (!ok) { - MGLOG_E_ONCE("BlitFramebuffer: could not stage the source for the multisample replicate"); - return false; - } - - const GLint dstLeft = std::min(dstX0, dstX1); - const GLint dstBottom = std::min(dstY0, dstY1); - const Bool mirrorX = (srcX1 > srcX0) != (dstX1 > dstX0); - const Bool mirrorY = (srcY1 > srcY0) != (dstY1 > dstY0); - // The scratch holds the source rectangle at its origin, so the texture is larger than - // the copied region: scale the [0,1] quad coordinates down to the region it occupies. - const Float uvScaleX = static_cast(srcWidth) / static_cast(s_textureWidth); - const Float uvScaleY = static_cast(srcHeight) / static_cast(s_textureHeight); - const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY, - mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f}; - - // The guard already left the pipeline neutral (nothing culled, tested, blended or - // coverage-masked) on the borrowed texture unit; what is left is this pass's own - // choices - the destination rectangle, and colour writes off because it writes only - // depth and stencil. - VertexArrayImpl::BindBackendVAOId(s_vertexArray); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); - g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight); - g_GLESFuncs.glScissor(dstLeft, dstBottom, dstWidth, dstHeight); - g_GLESFuncs.glEnable(GL_SCISSOR_TEST); - g_GLESFuncs.glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - DrainBlitErrors(); - - if (wantDepth) { - if (FormatHasStencil(sourceFormat)) { - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); - } - g_GLESFuncs.glUseProgram(s_depthProgram); - g_GLESFuncs.glUniform4f(s_depthUvTransform, uvTransform[0], uvTransform[1], uvTransform[2], - uvTransform[3]); - g_GLESFuncs.glEnable(GL_DEPTH_TEST); - g_GLESFuncs.glDepthFunc(GL_ALWAYS); - g_GLESFuncs.glDepthMask(GL_TRUE); - g_GLESFuncs.glDisable(GL_STENCIL_TEST); - g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); - } - - if (wantStencil) { - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); - g_GLESFuncs.glUseProgram(s_stencilProgram); - g_GLESFuncs.glUniform4f(s_stencilUvTransform, uvTransform[0], uvTransform[1], uvTransform[2], - uvTransform[3]); - g_GLESFuncs.glDisable(GL_DEPTH_TEST); - g_GLESFuncs.glDepthMask(GL_FALSE); - g_GLESFuncs.glEnable(GL_STENCIL_TEST); - // The bit planes are written by ORing in the set bits, so the destination has to - // start from zero. The blit overwrites the whole rectangle anyway, and the scissor - // keeps the clear inside it. - const GLint zero = 0; - g_GLESFuncs.glStencilMask(0xFFu); - g_GLESFuncs.glClearBufferiv(GL_STENCIL, 0, &zero); - g_GLESFuncs.glStencilFunc(GL_ALWAYS, 0xFF, 0xFFu); - g_GLESFuncs.glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); - for (Uint bit = 0; bit < 8; ++bit) { - g_GLESFuncs.glStencilMask(1u << bit); - g_GLESFuncs.glUniform1ui(s_stencilBit, 1u << bit); - g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); - } - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); - } - - const Bool replicated = g_GLESFuncs.glGetError() == GL_NO_ERROR; - - // Everything the pass disturbed goes back through emulationState's destructor. - if (!replicated) { - MGLOG_E_ONCE("BlitFramebuffer: multisample replicate fallback failed"); - } - return replicated; - } - - // ---- glBlitFramebuffer within one image ------------------------------------------------ - // - // GL 4.6 lets a blit read and write the same image as long as the two rectangles do not - // overlap (only the overlap is undefined). ES 3.x makes identical read and draw buffers - // GL_INVALID_OPERATION outright, so the driver refuses a blit from one half of a texture - // into the other and nothing is written. Emulate it in two steps: copy the source rectangle - // into a scratch framebuffer of the source's own native format, then run the caller's blit - // - same mask, same filter, same orientation - from the scratch into the real destination. - // Overlapping rectangles stay undefined, as GL leaves them. A LINEAR blit that scales reads - // clamped texels at the rectangle's edge where GL would read the neighbouring ones. - - // What a framebuffer attachment point resolves to on the DRIVER side: the native object and, - // for a texture, the level, layer and cube face. A null name means the point is empty. - struct DriverAttachmentImage { - GLint Type = GL_NONE; - GLint Name = 0; - GLint Level = 0; - GLint Layer = 0; - GLint Face = 0; - }; - - static DriverAttachmentImage QueryDriverAttachmentImage(GLenum framebufferTarget, GLenum attachment) { - DriverAttachmentImage image; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &image.Type); - if (image.Type != GL_TEXTURE && image.Type != GL_RENDERBUFFER) return {}; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &image.Name); - if (image.Type == GL_TEXTURE) { - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, - GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &image.Level); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv( - framebufferTarget, attachment, GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE, &image.Face); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, - GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER, &image.Layer); - } - return image; - } - - static Bool IsSameDriverImage(const DriverAttachmentImage& a, const DriverAttachmentImage& b) { - return a.Name != 0 && a.Type == b.Type && a.Name == b.Name && a.Level == b.Level && a.Layer == b.Layer && - a.Face == b.Face; - } - - // The colour attachment point the bound READ framebuffer reads from, GL_NONE when it reads - // nothing. - static GLenum DriverReadColorAttachment() { - GLint readBuffer = GL_NONE; - g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); - if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) return GL_NONE; - return static_cast(readBuffer); - } - - // The aspects of `mask` whose read and draw buffers are one image on the driver side - the - // aspects ES refuses to blit. The window-system framebuffer (driver name 0) has no attachment - // to compare or take a format from and is not considered. - static GLbitfield DriverBlitAspectsSharingAnImage(GLbitfield mask) { - GLint readFramebuffer = 0; - GLint drawFramebuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer); - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFramebuffer); - if (readFramebuffer == 0 || drawFramebuffer == 0) return 0; - - GLbitfield shared = 0; - if ((mask & GL_COLOR_BUFFER_BIT) != 0) { - const GLenum readAttachment = DriverReadColorAttachment(); - if (readAttachment != GL_NONE) { - const DriverAttachmentImage source = QueryDriverAttachmentImage(GL_READ_FRAMEBUFFER, readAttachment); - GLint maxDrawBuffers = 0; - g_GLESFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); - for (GLint i = 0; i < maxDrawBuffers && source.Name != 0; ++i) { - GLint drawBuffer = GL_NONE; - g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + i, &drawBuffer); - if (drawBuffer < GL_COLOR_ATTACHMENT0 || drawBuffer > GL_COLOR_ATTACHMENT31) continue; - if (IsSameDriverImage(source, QueryDriverAttachmentImage(GL_DRAW_FRAMEBUFFER, - static_cast(drawBuffer)))) { - shared |= GL_COLOR_BUFFER_BIT; - break; - } - } - } - } - const struct { - GLbitfield bit; - GLenum attachment; - } depthStencil[] = {{GL_DEPTH_BUFFER_BIT, GL_DEPTH_ATTACHMENT}, {GL_STENCIL_BUFFER_BIT, GL_STENCIL_ATTACHMENT}}; - for (const auto& aspect : depthStencil) { - if ((mask & aspect.bit) == 0) continue; - if (IsSameDriverImage(QueryDriverAttachmentImage(GL_READ_FRAMEBUFFER, aspect.attachment), - QueryDriverAttachmentImage(GL_DRAW_FRAMEBUFFER, aspect.attachment))) { - shared |= aspect.bit; - } - } - return shared; - } - - // The native sized internal format of what a READ framebuffer attachment point holds, 0 when - // it cannot be determined. ES has no "which target is this texture" query, so a texture is - // found by binding it to each candidate target in turn: glBindTexture refuses a name created - // for another target and leaves the binding alone. - static GLenum QueryDriverReadAttachmentInternalFormat(GLenum attachment) { - const DriverAttachmentImage image = QueryDriverAttachmentImage(GL_READ_FRAMEBUFFER, attachment); - if (image.Name == 0) return 0; - GLint internalFormat = 0; - if (image.Type == GL_RENDERBUFFER) { - if (!g_GLESFuncs.glGetRenderbufferParameteriv) return 0; - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(image.Name)); - g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, - &internalFormat); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); - } else { - if (!g_GLESFuncs.glGetTexLevelParameteriv) return 0; - struct Candidate { - GLenum target; - GLenum binding; - }; - const Candidate cube[] = {{GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BINDING_CUBE_MAP}}; - const Candidate others[] = {{GL_TEXTURE_2D, GL_TEXTURE_BINDING_2D}, - {GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BINDING_2D_ARRAY}, - {GL_TEXTURE_3D, GL_TEXTURE_BINDING_3D}, - {GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_BINDING_CUBE_MAP_ARRAY}}; - const Bool isCubeFace = image.Face != 0 && image.Face != GL_NONE; - const Candidate* candidates = isCubeFace ? cube : others; - const SizeT candidateCount = isCubeFace ? std::size(cube) : std::size(others); - for (SizeT i = 0; i < candidateCount && internalFormat == 0; ++i) { - const Candidate& candidate = candidates[i]; - DrainBlitErrors(); - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(candidate.binding, &previous); - g_GLESFuncs.glBindTexture(candidate.target, static_cast(image.Name)); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) continue; - const GLenum levelTarget = isCubeFace ? static_cast(image.Face) : candidate.target; - g_GLESFuncs.glGetTexLevelParameteriv(levelTarget, image.Level, GL_TEXTURE_INTERNAL_FORMAT, - &internalFormat); - g_GLESFuncs.glBindTexture(candidate.target, static_cast(previous)); - } - DrainBlitErrors(); - } - // A texture specified with an unsized format reports it back; renderbuffer storage - // takes only sized ones. - switch (internalFormat) { - case GL_RGBA: return GL_RGBA8; - case GL_RGB: return GL_RGB8; - case GL_DEPTH_STENCIL: return GL_DEPTH24_STENCIL8; - case GL_DEPTH_COMPONENT: return GL_DEPTH_COMPONENT24; - default: return static_cast(internalFormat); - } - } - - static Bool IsPackedDepthStencilFormat(GLenum format) { - return format == GL_DEPTH24_STENCIL8 || format == GL_DEPTH32F_STENCIL8; - } - - // The two-step copy itself. Both framebuffers are single-sample (ES cannot blit into a - // multisample one at all). Returns false, having written nothing to the destination, when - // the scratch cannot hold the source. - static Bool BlitWithinOneImage(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, - GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { - static NativeContextName s_scratchFramebuffer; // per context: a framebuffer is never shared - static Uint s_colorRenderbuffer = 0; - static Uint s_depthStencilRenderbuffer = 0; - static GLenum s_colorFormat = 0; - static GLenum s_depthStencilFormat = 0; - static GLsizei s_colorWidth = 0, s_colorHeight = 0; - static GLsizei s_depthStencilWidth = 0, s_depthStencilHeight = 0; - static Uint s_scratchContextGeneration = ~0u; - - if (!g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glGenRenderbuffers || - !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glFramebufferRenderbuffer) { - return false; - } - const GLint left = std::min(srcX0, srcX1); - const GLint right = std::max(srcX0, srcX1); - const GLint bottom = std::min(srcY0, srcY1); - const GLint top = std::max(srcY0, srcY1); - const GLsizei width = static_cast(right - left); - const GLsizei height = static_cast(top - bottom); - if (width <= 0 || height <= 0) return false; - - // The scratch takes each aspect the READ framebuffer has: one it lacks is silently - // skipped by the blit either way, and leaving it off the scratch keeps it skipped. - GLenum colorFormat = 0; - if ((mask & GL_COLOR_BUFFER_BIT) != 0) { - const GLenum readAttachment = DriverReadColorAttachment(); - if (readAttachment != GL_NONE) { - colorFormat = QueryDriverReadAttachmentInternalFormat(readAttachment); - if (colorFormat == 0) return false; - } - } - GLenum depthStencilFormat = 0; - GLenum depthStencilAttachment = GL_NONE; - if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) { - const GLenum depthFormat = (mask & GL_DEPTH_BUFFER_BIT) != 0 - ? QueryDriverReadAttachmentInternalFormat(GL_DEPTH_ATTACHMENT) - : 0; - const GLenum stencilFormat = (mask & GL_STENCIL_BUFFER_BIT) != 0 - ? QueryDriverReadAttachmentInternalFormat(GL_STENCIL_ATTACHMENT) - : 0; - // ES blits depth/stencil only between identical formats, which a scratch renderbuffer - // of the source's own format is. Separate depth and stencil images would need two - // scratch buffers; that shape is declined. - if (depthFormat != 0 && stencilFormat != 0 && depthFormat != stencilFormat) return false; - depthStencilFormat = depthFormat != 0 ? depthFormat : stencilFormat; - if (depthStencilFormat != 0) { - depthStencilAttachment = IsPackedDepthStencilFormat(depthStencilFormat) ? GL_DEPTH_STENCIL_ATTACHMENT - : depthFormat != 0 ? GL_DEPTH_ATTACHMENT - : GL_STENCIL_ATTACHMENT; - } - } - if (colorFormat == 0 && depthStencilFormat == 0) return false; - - if (s_scratchContextGeneration != g_backendContextGeneration) { - // The ids belonged to a dead context; the context reclaimed them with it. - s_scratchFramebuffer = 0; - s_colorRenderbuffer = 0; - s_depthStencilRenderbuffer = 0; - s_colorFormat = 0; - s_depthStencilFormat = 0; - s_scratchContextGeneration = g_backendContextGeneration; - } - if (s_scratchFramebuffer == 0) { - g_GLESFuncs.glGenFramebuffers(1, &s_scratchFramebuffer); - if (s_scratchFramebuffer == 0) return false; - } - - GLint previousRenderbuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previousRenderbuffer); - const auto ensureStorage = [](Uint& renderbuffer, GLenum& currentFormat, GLsizei& currentWidth, - GLsizei& currentHeight, GLenum format, GLsizei w, GLsizei h) { - if (renderbuffer == 0) { - g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer); - currentFormat = 0; - } - if (renderbuffer == 0) return; - if (currentFormat != format || currentWidth < w || currentHeight < h) { - currentWidth = std::max(currentWidth, w); - currentHeight = std::max(currentHeight, h); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, format, currentWidth, currentHeight); - currentFormat = format; - } - }; - if (colorFormat != 0) { - ensureStorage(s_colorRenderbuffer, s_colorFormat, s_colorWidth, s_colorHeight, colorFormat, width, - height); - } - if (depthStencilFormat != 0) { - ensureStorage(s_depthStencilRenderbuffer, s_depthStencilFormat, s_depthStencilWidth, - s_depthStencilHeight, depthStencilFormat, width, height); - } - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previousRenderbuffer)); - - GLint previousDraw = 0; - GLint previousRead = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); - - // Every point is (re)attached on every call: the scratch is shared between colour and - // depth/stencil shapes, and a stale attachment would make it incomplete or blit an - // aspect the source does not have. - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_scratchFramebuffer); - g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - colorFormat != 0 ? s_colorRenderbuffer : 0); - g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0); - if (depthStencilAttachment != GL_NONE) { - g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, depthStencilAttachment, GL_RENDERBUFFER, - s_depthStencilRenderbuffer); - } - const GLbitfield stagedMask = - (colorFormat != 0 ? static_cast(GL_COLOR_BUFFER_BIT) : 0) | - (depthStencilFormat != 0 ? mask & static_cast(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) - : 0); - Bool copied = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; - if (copied) { - // The staging copy is not the application's blit: its scissor box would clip the - // copy and leave never-written scratch texels for the second blit to carry over. The - // second blit keeps the scissor, which is the application's semantics. - DrainBlitErrors(); - const Bool scissorWasEnabled = RenderStateImpl::DriverScissorTestEnabled(); - if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, stagedMask, GL_NEAREST); - if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); - copied = g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - if (copied) { - // Mirror the caller's orientation into the scratch-relative source rectangle so a - // flipped blit stays flipped. - const GLint blitX0 = srcX0 <= srcX1 ? 0 : width; - const GLint blitX1 = srcX0 <= srcX1 ? width : 0; - const GLint blitY0 = srcY0 <= srcY1 ? 0 : height; - const GLint blitY1 = srcY0 <= srcY1 ? height : 0; - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_scratchFramebuffer); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); - DrainBlitErrors(); - g_GLESFuncs.glBlitFramebuffer(blitX0, blitY0, blitX1, blitY1, dstX0, dstY0, dstX1, dstY1, stagedMask, - filter); - copied = g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - return copied; - } - - static void IssueBlitWithResolveFallback(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, - GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { - DrainBlitErrors(); - g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - return; - } - // Two ES restrictions desktop GL does not have are worth a second attempt: a - // multisample resolve that also converts colour format, and any blit into a - // multisample draw framebuffer. Both need to know how the two sides are sampled. - GLint readSamples = 0; - GLint drawSamples = 0; - { - GLint previousRead = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); - GLint previousDraw = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); - g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(previousRead)); - g_GLESFuncs.glGetIntegerv(GL_SAMPLES, &readSamples); - g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(previousDraw)); - g_GLESFuncs.glGetIntegerv(GL_SAMPLES, &drawSamples); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - } - if (readSamples <= 0 && drawSamples <= 0 && DriverBlitAspectsSharingAnImage(mask) != 0) { - // ES refused it because one image is both ends; GL only refuses an overlap, which it - // leaves undefined. When the scratch cannot take the source, the per-aspect retries - // below still deliver every aspect that is not shared. - if (BlitWithinOneImage(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter)) { - DrainBlitErrors(); - return; - } - MGLOG_E_ONCE("BlitFramebuffer: a blit within one image could not be staged through a scratch copy"); - } - if (readSamples <= 0 && drawSamples > 0) { - // Single-sample source into a multisample destination: ES rejects the call - // outright, desktop GL replicates the source sample into every destination one. - if (ReplicateBlitIntoMultisampleDraw(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask)) { - if ((mask & GL_COLOR_BUFFER_BIT) != 0) { - MGLOG_E_ONCE("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated"); - } - } - return; - } - // The combined call raised an error, so by GL 4.6 2.3.1 it wrote nothing at all: BOTH - // aspect groups still owe their copy, and each has to be retried on its own. Re-issuing - // the depth/stencil half only as a rider on a SUCCESSFUL colour resolve dropped it - // silently whenever the colour half could not be emulated - and on a framebuffer whose - // only attachment is depth it never can, because the colour emulation has no attachment - // to take a format from (KHR-GL33.framebuffer_blit's depth config test blits - // COLOR|DEPTH|STENCIL across depth-only framebuffers and kept reading the clear value). - const GLbitfield colourBit = mask & static_cast(GL_COLOR_BUFFER_BIT); - const GLbitfield dsBits = mask & static_cast(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - // The colour group's one emulation is the multisample resolve that also converts format, - // which is the shape this names. It used to double as an early-out for the whole - // function, which is what cost a depth-only mask its single-aspect retry. - const Bool multisampleResolve = readSamples > 0 && drawSamples <= 0; - if (colourBit != 0) { - DrainBlitErrors(); - g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, colourBit, filter); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - const Bool emulated = - multisampleResolve && - ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter); - if (!emulated) { - MGLOG_E_ONCE("BlitFramebuffer: the colour aspect was dropped - the driver rejected it on its " - "own and no emulation applies"); - } - } - } - if (dsBits != 0) { - DrainBlitErrors(); - g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, dsBits, filter); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - // Nothing to fall back on yet: ResolveThenBlit is colour-only and the replicate - // pass runs in the opposite direction, so a driver that declines a multisample - // depth/stencil resolve leaves the destination holding its clear value. The log - // is the whole diagnostic - the frontend performs no validation of its own, so - // this never reaches the application as a GL error. - MGLOG_E_ONCE("BlitFramebuffer: the depth/stencil aspect was dropped - the driver rejected it on " - "its own and no emulation applies"); - } - } - DrainBlitErrors(); - } - - // ---- P5e (fb, CONTRACT-P5E.md §5.4): the same aspect plan, from the two RECORDS --------- - // - // The blit was the last entry point in this family that still reached into two frontend - // FramebufferObjects on every call - GetDrawBuffers, GetReadBuffer, GetAttachment and then - // six texture properties per aspect - and it did it on BOTH arms, the named one reaching - // the objects through the twin table's state note. Every one of those questions is a field - // of MGPFramebufferState: - // - // GetDrawBuffers() -> DrawBuffers[8] (attachment index, -1 = None) - // GetReadBuffer() -> ReadSurface matched against Color[] (the same match - // SyncReadBufferToBackend makes; ReadSurface is resolved - // from THIS framebuffer's own read buffer under every - // Target, Named included) - // GetAttachment(point) -> PushedSurfaceForAttachment - // IsTexture / IsLayered -> Kind / Layered - // GetTextureLevel / Layer -> Level / Layer - // GetFormat -> InternalFormat (the attachment's, which is the texture's) - // GetSamples -> record.Samples - // - // SAMPLES COMES OFF THE RECORD RATHER THAN OFF EACH TEXTURE, and that is a deliberate - // narrowing: the frontend form asked each endpoint texture, the record states it per - // FRAMEBUFFER. A framebuffer whose attachments disagree about sample count is incomplete, - // so the two cannot differ where this substitution is allowed to fire; and the test the - // answer feeds ("either end is multisample -> leave it to the driver") is conservative in - // the direction that hands the call back. - static FramebufferAttachmentType PushedReadBufferPoint(const MG_Pipe::MGPFramebufferState& record) { - if (MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res)) return FramebufferAttachmentType::None; - 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) { - return static_cast( - static_cast(FramebufferAttachmentType::Color0) + static_cast(i)); - } - } - return FramebufferAttachmentType::None; - } - - static GLbitfield BlitLayeredDestinationAspects(const MG_Pipe::MGPFramebufferState& readRecord, - const MG_Pipe::MGPFramebufferState& drawRecord, GLint srcX0, - GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, - GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) { - if (mask == 0) return 0; - if (!g_GLESFuncs.glCopyImageSubData) return 0; - if (!MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer(g_GLESFuncs)) return 0; - // The default framebuffer has no layers to get wrong. - if (readRecord.IsDefault != 0 || drawRecord.IsDefault != 0) return 0; - - const Int width = srcX1 - srcX0; - const Int height = srcY1 - srcY0; - const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height; - // The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two - // different operations no matter how the rectangles line up. - const Bool scissorEnabled = RenderStateImpl::DriverScissorTestEnabled(); - - using MobileGL::FramebufferAttachmentType; - struct AspectPlan { - GLbitfield bit; - FramebufferAttachmentType source; - FramebufferAttachmentType destination; - }; - // The buffer is FOUND rather than assumed to be slot 0, for the frontend form's reason: - // a blit writes every ENABLED draw buffer, and glDrawBuffers(NONE, NONE, NONE, - // COLOR_ATTACHMENT0) leaves slot 0 empty while still naming exactly one destination. - // -1 is the record's spelling of GL_NONE and is not "no such attachment". - Int enabledDrawBuffers = 0; - FramebufferAttachmentType colorDestination = FramebufferAttachmentType::None; - for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { - const Int8 index = drawRecord.DrawBuffers[i]; - if (index < 0) continue; - ++enabledDrawBuffers; - if (enabledDrawBuffers == 1) { - colorDestination = static_cast( - static_cast(FramebufferAttachmentType::Color0) + static_cast(index)); - } - } - const AspectPlan plans[] = { - {GL_COLOR_BUFFER_BIT, PushedReadBufferPoint(readRecord), colorDestination}, - {GL_DEPTH_BUFFER_BIT, FramebufferAttachmentType::Depth, FramebufferAttachmentType::Depth}, - {GL_STENCIL_BUFFER_BIT, FramebufferAttachmentType::Stencil, FramebufferAttachmentType::Stencil}, - }; - - GLbitfield handled = 0; - for (const AspectPlan& plan : plans) { - if ((mask & plan.bit) == 0) continue; - if (plan.source == FramebufferAttachmentType::Unknown || - plan.destination == FramebufferAttachmentType::Unknown || - plan.source == FramebufferAttachmentType::None || - plan.destination == FramebufferAttachmentType::None) { - continue; - } - const MG_Pipe::MGPSurface* const sourceSurface = - FramebufferImpl::PushedSurfaceForAttachment(readRecord, plan.source); - const MG_Pipe::MGPSurface* const destinationSurface = - FramebufferImpl::PushedSurfaceForAttachment(drawRecord, plan.destination); - if (sourceSurface == nullptr || destinationSurface == nullptr) continue; - // Renderbuffers have no layers, so a destination that is one cannot be hitting this. - if (sourceSurface->Kind != MG_Pipe::kMGPipeSurfaceKindTexture || - destinationSurface->Kind != MG_Pipe::kMGPipeSurfaceKindTexture) { - continue; - } - if (MG_Pipe::MGPipeHandleIsNull(sourceSurface->Res) || - MG_Pipe::MGPipeHandleIsNull(destinationSurface->Res)) { - continue; - } - // Layer 0 is the case the driver gets right, and a LAYERED attachment - // (glFramebufferTexture with no layer) blits its layer 0 by spec - neither is this - // defect. - if (destinationSurface->Layer == 0) continue; - if (destinationSurface->Layered != 0 || sourceSurface->Layered != 0) continue; - - // glCopyImageSubData moves texel blocks: same format both ends, or it is a different - // operation. Multisample endpoints would additionally have to agree on sample count, - // which is a resolve the driver still owns. - if (sourceSurface->InternalFormat != destinationSurface->InternalFormat) continue; - if (readRecord.Samples > 0 || drawRecord.Samples > 0) continue; - // Copying an image region onto itself is undefined for glCopyImageSubData, and a blit - // whose source and destination overlap is undefined for GL too - so this is not a - // shape to substitute FOR, it is one to leave exactly as the application wrote it. - if (sourceSurface->Res == destinationSurface->Res && - sourceSurface->Level == destinationSurface->Level && - sourceSurface->Layer == destinationSurface->Layer) { - continue; - } - - // A combined depth-stencil texture is ONE image to glCopyImageSubData: it carries both - // aspects across whether or not the mask asked for both. Taking only GL_DEPTH_BUFFER_BIT - // on a DEPTH24_STENCIL8 destination would overwrite a stencil the application asked to - // keep, so the copy is only allowed when the mask covers everything the format holds. - const auto format = static_cast(destinationSurface->InternalFormat); - const Bool hasDepth = MG_Util::IsDepthFormatInternalFormat(format); - const Bool hasStencil = MG_Util::IsStencilFormatInternalFormat(format); - if (hasDepth && (mask & GL_DEPTH_BUFFER_BIT) == 0) continue; - if (hasStencil && (mask & GL_STENCIL_BUFFER_BIT) == 0) continue; - // ... and having carried both, it must be credited with both, or the caller hands the - // stencil half to the driver and it lands on layer 0 after all. - const GLbitfield aspectBits = - hasDepth || hasStencil - ? static_cast((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) | - (hasStencil ? GL_STENCIL_BUFFER_BIT : 0)) - : static_cast(GL_COLOR_BUFFER_BIT); - if ((handled & aspectBits) == aspectBits) continue; - - if (!oneToOne || scissorEnabled || (plan.bit == GL_COLOR_BUFFER_BIT && enabledDrawBuffers != 1)) { - MGLOG_E_ONCE("BlitFramebuffer: this driver ignores a non-zero destination array layer and this " - "blit cannot be expressed as a copy (%s), so it will land on layer 0", - !oneToOne ? "it scales or flips" - : scissorEnabled ? "the scissor test is enabled" - : "the destination has more than one draw buffer"); - continue; - } - - auto& backendSource = - TextureImpl::SyncTextureToBackendByHandle(sourceSurface->Res, - /*imageBindableStorageRequired=*/false); - if (!backendSource) continue; - const GLuint sourceName = backendSource->GetBackendTextureId(); - // BY VALUE past this point, for the reason the copy-image endpoint builder gives: - // the second sync can grow the registry and relocate the first result. - auto& backendDestinationSlot = - TextureImpl::SyncTextureToBackendByHandle(destinationSurface->Res, - /*imageBindableStorageRequired=*/false); - if (!backendDestinationSlot) continue; - const GLuint destinationName = backendDestinationSlot->GetBackendTextureId(); - if (sourceName == 0 || destinationName == 0) continue; - const GLenum sourceTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( - static_cast(sourceSurface->TextureTarget)); - const GLenum destinationTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( - static_cast(destinationSurface->TextureTarget)); - - ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(sourceName, sourceTarget, static_cast(sourceSurface->Level), - srcX0, srcY0, static_cast(sourceSurface->Layer), - destinationName, destinationTarget, - static_cast(destinationSurface->Level), dstX0, dstY0, - static_cast(destinationSurface->Layer), width, height, 1); - if (const GLenum error = g_GLESFuncs.glGetError(); error != GL_NO_ERROR) { - // The driver blit still runs for this aspect - onto the wrong layer, but the - // substitute has to leave the call no worse off than it found it. - MGLOG_E_ONCE("BlitFramebuffer: the layered-destination copy substitute failed with %s; the " - "driver blit will run instead and land on layer 0", - MG_Util::ConvertGLEnumToString(error).c_str()); - continue; - } - handled |= aspectBits; - } - return handled & mask; - } - - // The two endpoint records of a blit, or nulls. `fbo` null-or-default means "the record of - // whatever is bound to that target", which is what the bound arm asks; a named blit passes - // the handles its record carried. - static const MG_Pipe::MGPFramebufferState* BlitEndpointRecord(MG_Pipe::MGPipeHandle fbo, - FramebufferTarget boundTarget) { - const auto& st = MG_Pipe::MGPipeApplier(); - if (MG_Pipe::MGPipeHandleIsNull(fbo)) { - return boundTarget == FramebufferTarget::Read ? st.ReadFramebuffer() : st.DrawFramebuffer(); - } - return st.FramebufferRecordFor(fbo); - } - - void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, - GLint dstY1, GLbitfield mask, GLenum filter) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - - // P5c (hd, CONTRACT-P5C §3.3): THE NAMED ARM. A blit record whose ReadFbo/DrawFbo are - // non-null is a glBlitNamedFramebuffer: both framebuffers resolve from the record's - // handles through the FBO twin table (the client's ScopedBlitBindings staging and this - // function's binding-slot read at the bottom are gone from the split path), and the - // sink is told the pair was consumed - a backend that reaches here without consuming - // it has no named arm and the verb declines there. - auto& applierState = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle readFbo = applierState.VerbBlitReadFbo; - const MG_Pipe::MGPipeHandle drawFbo = applierState.VerbBlitDrawFbo; - if (!MG_Pipe::MGPipeHandleIsNull(readFbo) || !MG_Pipe::MGPipeHandleIsNull(drawFbo)) { - applierState.VerbBlitNamedConsumed = true; - TextureImpl::SyncNeccessaryTextures(); - RenderStateImpl::SyncRenderState(); - - SyncAndBindFramebufferByHandle(readFbo, FramebufferTarget::Read, /*forceSync=*/true); - SyncAndBindFramebufferByHandle(drawFbo, FramebufferTarget::Draw, /*forceSync=*/true); - - MGLOG_D("ES BlitNamedFramebuffer({%u,%u} -> {%u,%u}, %d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", - readFbo.Slot, readFbo.Gen, drawFbo.Slot, drawFbo.Gen, srcX0, srcY0, srcX1, - srcY1, dstX0, dstY0, dstX1, dstY1, mask, - MG_Util::ConvertGLEnumToString(filter).c_str()); - // P5e (fb, §5.4): only the probed defect makes this do anything, and it now - // asks the two RECORDS. The state-note detour this used to take - the twin - // table handing back the frontend object it was synced from, plus - // pDefaultFramebufferInfo->defaultFBO for a default endpoint - is gone with the - // note itself; a named framebuffer always has a record (a Named record precedes - // every DSA site, ID-19), so the "missing object skips the workaround" case is - // now a missing record and means the same thing. - const auto* readRecord = BlitEndpointRecord(readFbo, FramebufferTarget::Read); - const auto* drawRecord = BlitEndpointRecord(drawFbo, FramebufferTarget::Draw); - if (readRecord != nullptr && drawRecord != nullptr) { - mask &= ~BlitLayeredDestinationAspects(*readRecord, *drawRecord, srcX0, srcY0, srcX1, - srcY1, dstX0, dstY0, dstX1, dstY1, mask); - } - if (mask != 0) { - IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, - mask, filter); - } - // The driver's bindings are now the two NAMED framebuffers; put the bound ones - // back exactly as the monolith named entry point does. - ForceBindCurrentFBO(FramebufferTarget::Read); - ForceBindCurrentFBO(FramebufferTarget::Draw); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - return; - } - - TextureImpl::SyncNeccessaryTextures(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - FramebufferImpl::SyncCurrentFBO(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - RenderStateImpl::SyncRenderState(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - BindCurrentFBO(FramebufferTarget::Draw); - BindCurrentFBO(FramebufferTarget::Read); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, - dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str()); - // A no-op on every driver that honours a non-zero destination array layer, which is all - // of them but the probed one. Whatever it performs itself is taken out of the mask. - // P5e (fb, §5.4): the bound arm's two binding-slot reads were the last GetBoundObject - // pair on this path; under a transport the two bound RECORDS answer instead - // (FramebufferRecordFor(st.BoundFramebuffer[t]), through BlitEndpointRecord's null - // case). SyncCurrentFBO ran a few lines up and refuses if either is missing, so a null - // here means the blit is against the default framebuffer's record, which the overload - // declines on IsDefault. - const auto* readRecord = - BlitEndpointRecord(MG_Pipe::kMGPipeNullHandle, FramebufferTarget::Read); - const auto* drawRecord = - BlitEndpointRecord(MG_Pipe::kMGPipeNullHandle, FramebufferTarget::Draw); - if (readRecord != nullptr && drawRecord != nullptr) { - mask &= ~BlitLayeredDestinationAspects(*readRecord, *drawRecord, srcX0, srcY0, srcX1, - srcY1, dstX0, dstY0, dstX1, dstY1, mask); - } - if (mask != 0) { - IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); - } - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } - - Bool UpdateTextureBindingAtTarget(GLenum target) { -#ifdef TRACY_ENABLE - ZoneScopedNC(__func__, TRACY_ZONECOLOR_BACKEND); -#endif - auto unit = MG_Pipe::gPipeInputs->GetActiveTextureUnit(); - - auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) { - MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", - MG_Util::ConvertTextureTargetToString(textureTarget).c_str()); - return false; - } - - // P5c (hd, CONTRACT-P5C §3.4): under an active transport the destination is the handle - // the copy_framebuffer_to_texture record carried; the unit binding slot and the client - // allocator are never read (T2/T4). - const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; - auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(dstHandle); - if (backendTextureSlot == nullptr) { - MGLOG_E_ONCE("%s: the verb's destination texture handle {%u, %u} is refused by " - "the twin slot table (live generation %u)", - __func__, dstHandle.Slot, dstHandle.Gen, - TextureImpl::g_backendTextureObjects.LiveGenAt(dstHandle.Slot)); - return false; - } - auto& backendObj = *backendTextureSlot; - if (!backendObj) { - backendObj = MakeShared(); - } - backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit); - return true; - } - - // ---- Scoped driver-state guards for the readback/copy/blit emulation paths -------------------- - // These paths borrow driver state (FBO bindings, scratch-FBO attachments, PACK - // pixel-store, the pack-PBO binding, scissor) that the app never asked to - // change; every mutation is scoped by an RAII guard so no exit path can leak - // it. Saves/restores go through the DirectGLES driver-state shadows - // (Managers.h) instead of glGetIntegerv - no driver round-trips, and redundant - // rebinds/resets no-op. - - // Saves the driver READ/DRAW framebuffer binding(s) and restores them on exit. - // Per-instance state: nesting-safe. - class ScopedFramebufferBinding { - public: - ScopedFramebufferBinding(Bool saveRead, Bool saveDraw) : m_saveRead(saveRead), m_saveDraw(saveDraw) { - if (m_saveRead) m_prevRead = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Read); - if (m_saveDraw) m_prevDraw = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Draw); - } - ~ScopedFramebufferBinding() { - if (m_saveRead) FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, m_prevRead); - if (m_saveDraw) FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_prevDraw); - } - ScopedFramebufferBinding(const ScopedFramebufferBinding&) = delete; - ScopedFramebufferBinding& operator=(const ScopedFramebufferBinding&) = delete; - - private: - const Bool m_saveRead; - const Bool m_saveDraw; - GLuint m_prevRead = 0; - GLuint m_prevDraw = 0; - }; - - // Applies a PACK pixel-store configuration and restores the previous one on exit. - class ScopedPackState { - public: - explicit ScopedPackState(const PixelStoreImpl::PackState& desired) - : m_prev(PixelStoreImpl::CurrentPackState()) { - PixelStoreImpl::ApplyPackState(desired); - } - ~ScopedPackState() { PixelStoreImpl::ApplyPackState(m_prev); } - ScopedPackState(const ScopedPackState&) = delete; - ScopedPackState& operator=(const ScopedPackState&) = delete; - - private: - const PixelStoreImpl::PackState m_prev; - }; - - // The frontend's current PACK parameters, for readbacks the ES driver serves - // directly with the client's layout. - static PixelStoreImpl::PackState PackStateFromContext() { - const auto packParams = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false); - return {static_cast(packParams.Alignment), static_cast(packParams.RowLength), - static_cast(packParams.SkipRows), static_cast(packParams.SkipPixels)}; - } - - // Binds a pixel PACK buffer (0 = client memory) for the scope and returns the - // binding to the resting 0 state on exit, so no later readback can accidentally - // capture into a stale PBO. - class ScopedPixelPackBuffer { - public: - explicit ScopedPixelPackBuffer(GLuint id) { BufferImpl::BindPixelPackBufferId(id); } - ~ScopedPixelPackBuffer() { BufferImpl::BindPixelPackBufferId(0); } - ScopedPixelPackBuffer(const ScopedPixelPackBuffer&) = delete; - ScopedPixelPackBuffer& operator=(const ScopedPixelPackBuffer&) = delete; - }; - - // Force-disables GL_SCISSOR_TEST for the scope (emulation blits and clears are - // scissored; readback copies must not be clipped by app scissor state) and - // restores the app state on exit. - // - // WHAT THE DRIVER HAS, NOT WHAT THE SHADOW LAST SAID. The render-state shadow is one per - // process and is synced at draws; a session that presents or reads back without drawing - // since another session's context synced (a frame that redraws nothing, a client whose - // turn came after the compositor's) found the other context's bit there. Its own scissor, - // still on from its last draw, then clipped the shared-image copy to a box - flipped into - // the wrong rows, too - and the image kept the frame it held before. - class ScopedScissorDisable { - public: - ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::DriverScissorTestEnabled()) { - if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - } - ~ScopedScissorDisable() { - if (m_wasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); - } - ScopedScissorDisable(const ScopedScissorDisable&) = delete; - ScopedScissorDisable& operator=(const ScopedScissorDisable&) = delete; - - private: - const Bool m_wasEnabled; - }; - - namespace RenderStateImpl { - void RunScissorDisabledForTesting(void (*inside)(void*), void* data) { - ScopedScissorDisable scissorOff; - inside(data); - } - } // namespace RenderStateImpl - - // Binds the shared scratch FBO at READ (isRead) or DRAW for one temp operation, - // restoring the previous binding on exit. Attachments are managed through the - // ScratchFBOImpl attachment shadow by the caller (see Framebuffer()). - class TempFBOBinder { - public: - explicit TempFBOBinder(Bool isRead) - : m_binding(/*saveRead=*/isRead, /*saveDraw=*/!isRead), - m_target(isRead ? GL_READ_FRAMEBUFFER : GL_DRAW_FRAMEBUFFER) { - FramebufferImpl::BindFramebufferId(m_target, ScratchFBOImpl::EnsureId(Framebuffer())); - } - ScratchFBOImpl::ScratchFramebuffer& Framebuffer() const { return ScratchFBOImpl::TempFramebuffer(); } - GLenum Target() const { return m_target; } - - private: - ScopedFramebufferBinding m_binding; - const GLenum m_target; - }; - - static Bool IsDepthOnlyFormat(TextureInternalFormat format) { - return MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format); - } - - static Bool IsColorOnlyFormat(TextureInternalFormat format) { - return !MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format); - } - - static Bool IsIntegerColorFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::RGB10A2UI: - 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: - return true; - default: - return false; - } - } - - // P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two - // descriptor reads this file makes (Managers.cpp has the same inverse beside the storage - // sync; it is file-local there, and a header for eleven cases would be the wrong trade). - static TextureTarget TextureTargetForPipeResourceTarget(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; - } - } - - // P8-B1: THE LEVELS A generate_mipmap RECORD GENERATES, end-exclusive. GL generates levels - // BASE_LEVEL+1 .. q, q = min(p, MAX_LEVEL) with p the last level the base image's own chain - // reaches, and an immutable texture's storage bounds the chain too (GL 4.6 core 8.14.4). The - // client already computed that window (ComputeMipmapGenerationRange) and defined its levels - // before emitting the verb, so the record's plan is the window and the descriptor is what - // holds it: Base = VerbMipBaseLevel, End = min(VerbMipLevelCount, Desc.Levels, MaxLevel + 1, - // Base + the base extent's chain) - the same clip Magma's GenerateWireMipmap makes. - // - // What stood here instead demanded the full chain from LEVEL 0 and died with - // Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the descriptor held less: a - // MAX_LEVEL below the chain, or glTexStorage2D with fewer levels, both legal GL. - struct RecordMipWindow { - Uint32 Base = 0; - Uint32 End = 0; // end-exclusive; End <= Base + 1 generates nothing - }; - - static RecordMipWindow GenerateMipmapWindowByRecord(const MG_Pipe::MGPipeResourceRecord& record) { - const auto& applier = MG_Pipe::MGPipeApplier(); - const auto& desc = record.Desc; - RecordMipWindow window{applier.VerbMipBaseLevel, applier.VerbMipBaseLevel}; - if (desc.Width == 0 || desc.Levels == 0 || window.Base >= desc.Levels) return window; - Uint32 end = std::min(applier.VerbMipLevelCount, desc.Levels); - if (record.ParamsSerial) end = std::min(end, static_cast(record.Params.MaxLevel) + 1u); - const IntVec3 baseExtent = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, window.Base); - const Int shrinking = MG_Record::StagedTextureShrinkingAxisCount(desc.Target); - Int largest = 1; - for (Int axis = 0; axis < shrinking && axis < 3; ++axis) largest = std::max(largest, baseExtent[axis]); - Uint32 chain = 1; - while (largest > 1) { - largest /= 2; - ++chain; - } - window.End = std::max(window.Base, std::min(end, window.Base + chain)); - return window; - } - - static void AssertNoGLError(const char* operation) { - const GLenum err = g_GLESFuncs.glGetError(); - MOBILEGL_ASSERT(err == GL_NO_ERROR, "%s failed: %s", operation, - MG_Util::ConvertGLEnumToString(err).c_str()); - } - - static ErrorCode ConvertGLESErrorToErrorCode(GLenum err) { - switch (err) { - case GL_INVALID_ENUM: - return ErrorCode::InvalidEnum; - case GL_INVALID_VALUE: - return ErrorCode::InvalidValue; - case GL_INVALID_FRAMEBUFFER_OPERATION: - return ErrorCode::InvalidFramebufferOperation; - case GL_OUT_OF_MEMORY: - return ErrorCode::OutOfMemory; - case GL_INVALID_OPERATION: - default: - return ErrorCode::InvalidOperation; - } - } - - static Bool RecordGLError(const char* operation, GLenum target, TextureInternalFormat format) { - const GLenum err = g_GLESFuncs.glGetError(); - if (err == GL_NO_ERROR) { - return true; - } - - MGLOG_E_ONCE("%s failed: %s. target=%s, format=%s", operation, - MG_Util::ConvertGLEnumToString(err).c_str(), - MG_Util::ConvertGLEnumToString(target).c_str(), - MG_Util::ConvertTextureInternalFormatToString(format).c_str()); - MG_Pipe::gPipeInputs->RecordError( - ConvertGLESErrorToErrorCode(err), - MakeUnique("DirectGLES", operation, - MG_Util::ConvertGLEnumToString(err))); - return false; - } - - static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); } - - // Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the - // scope (GenerateMipmap must respecify texture storage while no incomplete - // user FBO is bound); restores the previous bindings on exit. - class ScopedCompleteFramebufferBinding { - public: - ScopedCompleteFramebufferBinding() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) { - FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, ScratchFBOImpl::EnsureCompleteTinyFramebufferId()); - } - - private: - ScopedFramebufferBinding m_binding; - }; - - class ScopedDetachedTextureFramebufferAttachments { - public: - // P5e (tx2). THE SAME SWEEP, KEYED BY THE TEXTURE'S HANDLE. - // - // The frontend constructor below answers "which framebuffers attach this texture" by - // walking every live FBO twin's frontend FramebufferObject and comparing attachment - // SharedPtrs - kimi audit row 63, an unmemoised walk of client objects. The applier - // already holds the answer: every framebuffer's record carries its eleven MGPSurfaces - // and each one names its texture BY HANDLE (`MGPSurface::Res`), so the comparison is - // {slot, gen} against {slot, gen} and the level / layered / upload-target details the - // detach needs come off the same surface. - // - // This is the same reverse question fb's texture -> FBO-slot index answers for its - // attachment sync; here it is asked over the records directly, because the walk is on a - // rare path (per glGenerateMipmap) and does not need an index to be cheap. - explicit ScopedDetachedTextureFramebufferAttachments(MG_Pipe::MGPipeHandle textureHandle) { - if (MG_Pipe::MGPipeHandleIsNull(textureHandle)) return; - auto* twin = TextureImpl::ResolveTextureTwin(textureHandle); - if (twin == nullptr) return; - const GLuint backendTextureId = twin->GetBackendTextureId(); - const auto& st = MG_Pipe::MGPipeApplier(); - - FramebufferImpl::g_backendFramebufferObjects.ForEachLive( - [&](MG_Pipe::MGPipeHandle fbo, - const SharedPtr& backendFBO) { - if (!backendFBO) return; - const auto* record = st.FramebufferRecordFor(fbo); - if (record == nullptr || record->IsDefault != 0) return; - - const auto detachPoint = [&](const MG_Pipe::MGPSurface& surface, - FramebufferAttachmentType frontendType) { - if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; - if (surface.Res.Slot != textureHandle.Slot || surface.Res.Gen != textureHandle.Gen) { - return; - } - GLenum backendAttachment = GL_NONE; - if (frontendType >= FramebufferAttachmentType::Color0 && - frontendType <= FramebufferAttachmentType::Color31) { - backendAttachment = backendFBO->GetBackendAttachmentType(frontendType); - } else { - backendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType); - } - if (backendAttachment == GL_NONE || backendAttachment == GL_UNKNOWN_MGL) return; - - GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( - static_cast(surface.UploadTarget)); - if (textureTarget == GL_UNKNOWN_MGL) { - textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( - static_cast(surface.TextureTarget)); - } - const GLuint backendFBOId = backendFBO->GetBackendFramebufferId(); - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId); - if (surface.Layered != 0) { - g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0); - } else { - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, backendAttachment, - textureTarget, 0, 0); - } - ClearGLErrors(); - m_detachedAttachments.push_back({backendFBOId, backendAttachment, textureTarget, - backendTextureId, static_cast(surface.Level), - surface.Layered != 0}); - }; - - for (SizeT i = 0; i < 8; ++i) { - detachPoint(record->Color[i], - static_cast( - static_cast(FramebufferAttachmentType::Color0) + i)); - } - detachPoint(record->Depth, FramebufferAttachmentType::Depth); - detachPoint(record->Stencil, FramebufferAttachmentType::Stencil); - }); - } - - ~ScopedDetachedTextureFramebufferAttachments() { - for (const auto& attachment : m_detachedAttachments) { - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, attachment.framebuffer); - if (attachment.layered) { - g_GLESFuncs.glFramebufferTexture( - GL_DRAW_FRAMEBUFFER, attachment.attachment, attachment.texture, attachment.level); - } else { - g_GLESFuncs.glFramebufferTexture2D( - GL_DRAW_FRAMEBUFFER, attachment.attachment, attachment.textureTarget, - attachment.texture, attachment.level); - } - } - } - - private: - struct DetachedAttachment { - GLuint framebuffer = 0; - GLenum attachment = GL_NONE; - GLenum textureTarget = GL_TEXTURE_2D; - GLuint texture = 0; - GLint level = 0; - Bool layered = false; - }; - - // Declared first so its restore runs after the reattach loop in the dtor. - ScopedFramebufferBinding m_binding{/*saveRead=*/true, /*saveDraw=*/true}; - Vector m_detachedAttachments; - }; - - // Binds the scratch blit READ/DRAW framebuffers with scissor forced off (blits - // are scissored) for one texture-to-texture copy; restores the bindings and the - // scissor state on exit. Attachments on the two scratch FBOs are managed by the - // blit helpers through the ScratchFBOImpl attachment shadow. - class ScopedDepthBlitState { - public: - ScopedDepthBlitState() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) { - FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, - ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitReadFramebuffer())); - AssertNoGLError("bind depth blit read framebuffer"); - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, - ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitDrawFramebuffer())); - AssertNoGLError("bind depth blit draw framebuffer"); - } - - private: - ScopedScissorDisable m_scissorOff; - ScopedFramebufferBinding m_binding; - }; - - static void BlitDepthTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth, - GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY, - GLsizei dstWidth, GLsizei dstHeight) { - MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Depth blit requires valid backend textures."); - MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Depth blit mip levels must be non-negative."); - MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, - "Depth blit dimensions must be positive."); - - ClearGLErrors(); - ScopedDepthBlitState state; - auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); - auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); - ScratchFBOImpl::EnsureDepthAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel, - /*withStencil=*/false); - AssertNoGLError("attach depth blit source texture"); - ScratchFBOImpl::EnsureDepthAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel, - /*withStencil=*/false); - AssertNoGLError("attach depth blit destination texture"); - ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE); - AssertNoGLError("set depth blit read buffer"); - ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE); - AssertNoGLError("set depth blit draw buffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Depth blit read framebuffer is incomplete."); - AssertNoGLError("check depth blit read framebuffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Depth blit draw framebuffer is incomplete."); - AssertNoGLError("check depth blit draw framebuffer"); - - g_GLESFuncs.glBlitFramebuffer(srcX, srcY, srcX + srcWidth, srcY + srcHeight, - dstX, dstY, dstX + dstWidth, dstY + dstHeight, - GL_DEPTH_BUFFER_BIT, GL_NEAREST); - AssertNoGLError("depth texture blit"); - } - - static void BlitColorTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth, - GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY, - GLsizei dstWidth, GLsizei dstHeight, GLenum filter) { - MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Color blit requires valid backend textures."); - MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Color blit mip levels must be non-negative."); - MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, - "Color blit dimensions must be positive."); - MOBILEGL_ASSERT(filter == GL_NEAREST || filter == GL_LINEAR, "Color blit filter must be nearest or linear."); - - ClearGLErrors(); - ScopedDepthBlitState state; - auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); - auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); - ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel); - AssertNoGLError("attach color blit source texture"); - ScratchFBOImpl::EnsureColorAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel); - AssertNoGLError("attach color blit destination texture"); - ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); - AssertNoGLError("set color blit read buffer"); - ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0); - AssertNoGLError("set color blit draw buffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Color blit read framebuffer is incomplete."); - AssertNoGLError("check color blit read framebuffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Color blit draw framebuffer is incomplete."); - AssertNoGLError("check color blit draw framebuffer"); - - g_GLESFuncs.glBlitFramebuffer(srcX, srcY, srcX + srcWidth, srcY + srcHeight, - dstX, dstY, dstX + dstWidth, dstY + dstHeight, - GL_COLOR_BUFFER_BIT, filter); - AssertNoGLError("color texture blit"); - } - - // The layer-point siblings of the two blits above, for a texture VIEW's mip chain: those - // texels live in the storage owner's layers [MinLayer, MinLayer + NumLayers) and a 2D - // attach would address layer 0 of every one of them. One layer's extents are the whole - // rectangle, exactly as in the 2D helpers. - static void BlitDepthTextureLayer(GLuint srcTexture, GLint srcLevel, GLint srcLayer, GLsizei srcWidth, - GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstLayer, - GLsizei dstWidth, GLsizei dstHeight) { - MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Depth blit requires valid backend textures."); - MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Depth blit mip levels must be non-negative."); - MOBILEGL_ASSERT(srcLayer >= 0 && dstLayer >= 0, "Depth blit layers must be non-negative."); - MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, - "Depth blit dimensions must be positive."); - - ClearGLErrors(); - ScopedDepthBlitState state; - auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); - auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); - ScratchFBOImpl::EnsureDepthAttachmentLayer(readFB, GL_READ_FRAMEBUFFER, srcTexture, srcLevel, srcLayer); - AssertNoGLError("attach depth blit source layer"); - ScratchFBOImpl::EnsureDepthAttachmentLayer(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, dstLevel, dstLayer); - AssertNoGLError("attach depth blit destination layer"); - ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE); - AssertNoGLError("set depth blit read buffer"); - ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE); - AssertNoGLError("set depth blit draw buffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Depth blit read framebuffer is incomplete."); - AssertNoGLError("check depth blit read framebuffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Depth blit draw framebuffer is incomplete."); - AssertNoGLError("check depth blit draw framebuffer"); - - g_GLESFuncs.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight, GL_DEPTH_BUFFER_BIT, - GL_NEAREST); - AssertNoGLError("depth texture layer blit"); - } - - static void BlitColorTextureLayer(GLuint srcTexture, GLint srcLevel, GLint srcLayer, GLsizei srcWidth, - GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstLayer, - GLsizei dstWidth, GLsizei dstHeight, GLenum filter) { - MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Color blit requires valid backend textures."); - MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Color blit mip levels must be non-negative."); - MOBILEGL_ASSERT(srcLayer >= 0 && dstLayer >= 0, "Color blit layers must be non-negative."); - MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, - "Color blit dimensions must be positive."); - MOBILEGL_ASSERT(filter == GL_NEAREST || filter == GL_LINEAR, "Color blit filter must be nearest or linear."); - - ClearGLErrors(); - ScopedDepthBlitState state; - auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); - auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); - ScratchFBOImpl::EnsureColorAttachmentLayer(readFB, GL_READ_FRAMEBUFFER, srcTexture, srcLevel, srcLayer); - AssertNoGLError("attach color blit source layer"); - ScratchFBOImpl::EnsureColorAttachmentLayer(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, dstLevel, dstLayer); - AssertNoGLError("attach color blit destination layer"); - ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); - AssertNoGLError("set color blit read buffer"); - ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0); - AssertNoGLError("set color blit draw buffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Color blit read framebuffer is incomplete."); - AssertNoGLError("check color blit read framebuffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "Color blit draw framebuffer is incomplete."); - AssertNoGLError("check color blit draw framebuffer"); - - g_GLESFuncs.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight, GL_COLOR_BUFFER_BIT, - filter); - AssertNoGLError("color texture layer blit"); - } - - // A mip chain generated inside a texture VIEW's window. Only the LAYER axis needs the - // manual path: BASE_LEVEL/MAX_LEVEL are the view's own parameters, so the native - // glGenerateMipmap already clips the LEVEL axis the way the view describes - but it writes - // every layer of the storage the view aliases, and a view's layer window is a subset of - // those. - // - // The levels here are the VIEW's own (level 0 aliases the storage's MinLevel), because that - // is the space a view's descriptor extents and the frontend's own level sizes are measured - // in. The blits are the one place the two systems meet, so the storage level a blit names is - // MinLevel + the view's level. - struct MipmapViewWindow { - GLuint StorageTextureId = 0; - Uint32 MinLevel = 0; - Uint32 MinLayer = 0; - Uint32 NumLayers = 1; - Uint32 LogicalBase = 0; - Uint32 LogicalEnd = 0; // view-relative, end-exclusive - Bool Depth = false; - GLenum Filter = GL_LINEAR; - }; - - template - static void GenerateMipmapThroughViewWindow(const MipmapViewWindow& window, ExtentAtLevel&& extentAtLevel) { - if (window.StorageTextureId == 0 || window.NumLayers == 0) return; - for (Uint32 level = window.LogicalBase + 1; level < window.LogicalEnd; ++level) { - const IntVec3 srcSize = extentAtLevel(level - 1); - const IntVec3 dstSize = extentAtLevel(level); - MOBILEGL_ASSERT(srcSize.x() > 0 && srcSize.y() > 0 && dstSize.x() > 0 && dstSize.y() > 0, - "View mipmap generation needs non-empty source and destination extents."); - const GLint srcLevel = static_cast(window.MinLevel + level - 1); - const GLint dstLevel = static_cast(window.MinLevel + level); - for (Uint32 i = 0; i < window.NumLayers; ++i) { - const GLint layer = static_cast(window.MinLayer + i); - if (window.Depth) { - BlitDepthTextureLayer(window.StorageTextureId, srcLevel, layer, static_cast(srcSize.x()), - static_cast(srcSize.y()), window.StorageTextureId, dstLevel, - layer, static_cast(dstSize.x()), static_cast(dstSize.y())); - } else { - BlitColorTextureLayer(window.StorageTextureId, srcLevel, layer, static_cast(srcSize.x()), - static_cast(srcSize.y()), window.StorageTextureId, dstLevel, - layer, static_cast(dstSize.x()), - static_cast(dstSize.y()), window.Filter); - } - } - } - } - - static void CopyR32FTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei width, - GLsizei height, GLuint dstTexture, GLenum dstTarget, GLint dstLevel, GLint dstX, - GLint dstY) { - MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "R32F copy requires valid backend textures."); - MOBILEGL_ASSERT(dstTarget == GL_TEXTURE_2D, "R32F copy only supports GL_TEXTURE_2D destinations."); - MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "R32F copy mip levels must be non-negative."); - MOBILEGL_ASSERT(width > 0 && height > 0, "R32F copy dimensions must be positive."); - - ClearGLErrors(); - ScopedDepthBlitState state; - auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); - ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel); - AssertNoGLError("attach R32F copy source texture"); - ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); - AssertNoGLError("set R32F copy read buffer"); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "R32F copy read framebuffer is incomplete."); - AssertNoGLError("check R32F copy read framebuffer"); - - Vector pixels(static_cast(width) * static_cast(height)); - { - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); - g_GLESFuncs.glReadPixels(srcX, srcY, width, height, GL_RED, GL_FLOAT, pixels.data()); - AssertNoGLError("read R32F copy pixels"); - } - - // Upload side: the rows are tightly packed floats, which the resting driver - // UNPACK state (4/0/0/0, maintained by ScopedDefaultUnpackState) parses - // correctly; the unpack-PBO binding rests at 0 by the same discipline (the - // call below no-ops unless something diverged). - BufferImpl::BindPixelUnpackBufferId(0); - TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); - g_GLESFuncs.glBindTexture(dstTarget, dstTexture); - g_GLESFuncs.glTexSubImage2D(dstTarget, dstLevel, dstX, dstY, width, height, GL_RED, GL_FLOAT, pixels.data()); - AssertNoGLError("upload R32F copy pixels"); - // Re-bind what the texture-binding cache says lives on the temp unit so the - // cache stays truthful without a driver query. - auto* cachedBound = - TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] - [static_cast(MG_Util::ConvertGLEnumToTextureTarget(dstTarget))]; - g_GLESFuncs.glBindTexture(dstTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); - } - - // WHICH LAYERS A VIEW'S MIP CHAIN MAY WRITE. A generation through a view has two coordinate - // systems in it - the view's own level/layer window (the sampler view CSO) and its storage - // owner's - and the native arm below carries only part of the first one: BASE_LEVEL and - // MAX_LEVEL are the view's own parameters, so glGenerateMipmap clips the LEVEL axis the way - // the view describes, but the layer set it writes is every layer of the storage the view - // aliases. A windowed view's generation therefore rewrites owner layers outside its window, - // which the view window itself says it must not. - // - // True means this record's generation was handled here (or refuted) and the native arm must - // NOT run; false means "not this shape": no view, a view whose window covers the whole - // storage - where the native call already agrees with GL - a multisample view, which has no - // mip chain to generate, or an aspect the layer blits do not carry. - static Bool GenerateMipmapThroughViewWindowByRecord(MG_Pipe::MGPipeHandle mipRes, - const MG_Pipe::MGPipeResourceRecord& record, - TextureInternalFormat format) { - if (MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) return false; - if (record.Desc.Samples != 0) return false; - const Bool depth = IsDepthOnlyFormat(format); - if (!depth && !IsColorOnlyFormat(format)) return false; - - // The view record, validated as SyncTextureViewToBackendByRecord validates it: this - // window is the only place the four numbers come from, and a stale or foreign one would - // generate into another texture's levels. - const auto* viewRecord = PipeSamplerViewRecordForHandle(record.ViewCso); - if (viewRecord == nullptr || !viewRecord->Live || viewRecord->Gen != record.ViewCso.Gen) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-record\"} {%u,%u}", mipRes.Slot, mipRes.Gen); - std::abort(); - } - const auto& view = viewRecord->View; - if (view.Texture != mipRes || view.NumLevels == 0 || view.NumLayers == 0) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-window\"} {%u,%u}", mipRes.Slot, mipRes.Gen); - std::abort(); - } - const auto* storageRecord = PipeTextureRecordForHandle(record.Desc.ViewOf); - if (storageRecord == nullptr) { - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-storage\"} {%u,%u}", mipRes.Slot, mipRes.Gen); - std::abort(); - } - if (view.MinLayer == 0 && view.NumLayers >= std::max(storageRecord->Desc.ArrayLayers, 1u)) - return false; - - const auto& applier = MG_Pipe::MGPipeApplier(); - MipmapViewWindow window; - window.MinLevel = view.MinLevel; - window.MinLayer = view.MinLayer; - window.NumLayers = view.NumLayers; - window.LogicalBase = applier.VerbMipBaseLevel; - // LevelCount is the logical END-EXCLUSIVE level, not a count after BaseLevel, and the - // frontend already clipped it to the object's own range; the view's window is the bound - // only this side can apply. - window.LogicalEnd = std::min({static_cast(applier.VerbMipLevelCount), - static_cast(record.Desc.Levels), - static_cast(view.NumLevels)}); - window.Depth = depth; - window.Filter = IsIntegerColorFormat(format) ? GL_NEAREST : GL_LINEAR; - if (window.LogicalEnd <= window.LogicalBase + 1) return true; - - // The STORAGE's ES name is what the blits write: the view's own name aliases the same - // texels, but only the owner's name carries every level and layer this window reaches. - auto& storage = TextureImpl::SyncTextureToBackendByHandle(record.Desc.ViewOf); - if (!storage || storage->GetBackendTextureId() == 0) { - MGLOG_E_ONCE("MGPipe: generate_mipmap through view {%u, %u} has no storage texture to write; " - "the call is dropped", mipRes.Slot, mipRes.Gen); - return true; - } - window.StorageTextureId = storage->GetBackendTextureId(); - GenerateMipmapThroughViewWindow(window, [&record](Uint32 level) { - return MG_Record::StagedTextureMipExtent(record.Desc.Target, record.Desc.Width, - record.Desc.Height, record.Desc.Depth, level); - }); - return true; - } - - // P5e (tx2): the two CPU-blit mip chains, driven by the RECORD the verb named. What changes - // against the P5c arms below is only the resolution: those took the frontend texture and - // re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as - // barriered), and the handle is now simply passed in - the extent derivation, the per-level - // blits and the format question are §1's descriptor reads exactly as P5c wrote them. - // - // P8-B1: both chains run over the record's WINDOW (GenerateMipmapWindowByRecord), not from - // level 1 to the end of the descriptor: a BASE_LEVEL above 0 is the source and a level past - // MAX_LEVEL is left as it is, which is what the monolith twins below still get wrong. - static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture, - const RecordMipWindow& window) { - const auto& desc = record.Desc; - const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint32 level = window.Base + 1; level < window.End; ++level) { - const IntVec3 srcSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, level - 1); - const IntVec3 dstSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, level); - BlitDepthTexture2D(textureId, static_cast(level - 1), 0, 0, - static_cast(srcSize.x()), static_cast(srcSize.y()), textureId, - static_cast(level), 0, 0, static_cast(dstSize.x()), - static_cast(dstSize.y())); - } - } - - static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture, - const RecordMipWindow& window) { - const auto& desc = record.Desc; - const GLenum filter = - IsIntegerColorFormat(static_cast(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR; - const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint32 level = window.Base + 1; level < window.End; ++level) { - const IntVec3 srcSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, level - 1); - const IntVec3 dstSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, level); - BlitColorTexture2D(textureId, static_cast(level - 1), 0, 0, static_cast(srcSize.x()), - static_cast(srcSize.y()), textureId, static_cast(level), 0, 0, - static_cast(dstSize.x()), static_cast(dstSize.y()), filter); - } - } - - void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, - GLsizei height, GLint border) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - DebugImpl::ErrorLopper errorLopper; - MGLOG_D("%s: Backend", __func__); - TextureImpl::SyncNeccessaryTextures(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - FramebufferImpl::SyncCurrentFBO(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - RenderStateImpl::SyncRenderState(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - if (!UpdateTextureBindingAtTarget(target)) return; - - // Bind necessary FBO and texture - BindCurrentFBO(FramebufferTarget::Read); - Uint activeTextureUnit = MG_Pipe::gPipeInputs->GetActiveTextureUnit(); - TextureImpl::BackendTextureObject* dstBackendTexture = nullptr; - TextureInternalFormat mgInternalFormat{}; - // P5c (hd, CONTRACT-P5C §3.4): with an active transport the destination resolves from - // the handle the copy_framebuffer_to_texture record carried (MGPCopyFromFramebuffer:: - // Dst, the verb stash) and its format is the applier descriptor's - the client's - // texture-unit binding slot and the client slot allocator are never read (T2/T4). - const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; - const auto* dstRecord = PipeTextureRecordForHandle(dstHandle); - // P5e (tx2): SYNCED, not merely looked up. The destination of a copy is not sampled - // by the draw program, so the per-draw unit list - which is the sampler-view window - // now - never brings it across; before P5e the frontend walk over every slot of every - // touched unit happened to sync it as a side effect. The named narrowing (P5e-5) says - // the union that still covers every texture includes "the waited texture ops", and - // this is one of them: it syncs its own endpoint by handle. - TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle); - auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle); - if (!backendTexture || dstRecord == nullptr) { - MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin " - "or no applier record on this side", - dstHandle.Slot, dstHandle.Gen); - return; - } - dstBackendTexture = backendTexture.get(); - mgInternalFormat = static_cast(dstRecord->Desc.InternalFormat); - dstBackendTexture->Bind(target, activeTextureUnit); - - GLenum format = GL_DEPTH_COMPONENT; - GLenum type = GL_UNSIGNED_INT; - TextureImpl::GenerateTextureFormatInfo(mgInternalFormat, &internalformat, &format, &type, - MG_Util::ConvertGLEnumToTextureTarget(target)); - MOBILEGL_ASSERT(format != GL_NONE && type != GL_NONE, - "%s: cannot GenerateTextureFormatInfo(%s): out internalformat=%s, format=%s, type=%s", - MG_Util::ConvertTextureInternalFormatToString(mgInternalFormat).c_str(), - MG_Util::ConvertGLEnumToString(internalformat).c_str(), - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); - TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); - - Bool isDepthFormat = - MG_Util::IsDepthFormatInternalFormat(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat)); - Bool isStencilFormat = - MG_Util::IsStencilFormatInternalFormat(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat)); - - if (!isDepthFormat) { - g_GLESFuncs.glCopyTexImage2D(target, level, internalformat, x, y, width, height, border); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } else { - MGLOG_D("%s: Backend depth", __func__); - // nullptr means "uninitialized storage" only while no unpack PBO is - // bound; enforce the resting 0 state instead of assuming it (no-op - // through the binding cache unless something diverged). - BufferImpl::BindPixelUnpackBufferId(0); - g_GLESFuncs.glTexImage2D(target, level, (GLint)internalformat, width, height, border, format, type, - nullptr); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - auto currentTex = (GLint)dstBackendTexture->GetBackendTextureId(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - TempFBOBinder tempFBOBinder(false); - ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit - ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, - static_cast(currentTex), target, level, isStencilFormat); - - if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE"); - return; - } - - g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height, - GL_DEPTH_BUFFER_BIT | (isStencilFormat ? GL_STENCIL_BUFFER_BIT : 0), - GL_NEAREST); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } - } - - void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, - GLsizei height) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - DebugImpl::ErrorLopper errorLopper; - - MGLOG_D("%s: Backend", __func__); - TextureImpl::SyncNeccessaryTextures(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - FramebufferImpl::SyncCurrentFBO(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - RenderStateImpl::SyncRenderState(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - - if (!UpdateTextureBindingAtTarget(target)) return; - - // Bind necessary FBO and texture - BindCurrentFBO(FramebufferTarget::Read); - auto activeTextureUnit = MG_Pipe::gPipeInputs->GetActiveTextureUnit(); - TextureImpl::BackendTextureObject* dstBackendTexture = nullptr; - // P5c (hd, CONTRACT-P5C §3.4): see CopyTexImage2D - the record's Dst handle, never the - // client's unit binding slot or the client allocator (T2/T4). - const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; - // P5e (tx2): synced by handle - see CopyTexImage2D's note. A copy destination is not - // in the sampler-view window, so nothing else on this path would build its twin. - TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle); - auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle); - if (!backendTexture) { - MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no " - "twin on this side", - dstHandle.Slot, dstHandle.Gen); - return; - } - dstBackendTexture = backendTexture.get(); - dstBackendTexture->Bind(target, activeTextureUnit); - - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - GLenum internalFormat; - g_GLESFuncs.glGetTexLevelParameteriv(target, level, GL_TEXTURE_INTERNAL_FORMAT, (GLint*)&internalFormat); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - auto mgInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalFormat); - - Bool isDepthFormat = MG_Util::IsDepthFormatInternalFormat(mgInternalFormat); - Bool isStencilFormat = MG_Util::IsStencilFormatInternalFormat(mgInternalFormat); - - if (!isDepthFormat) { - g_GLESFuncs.glCopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } else { - MGLOG_D("%s: Backend depth", __func__); - auto currentTex = dstBackendTexture->GetBackendTextureId(); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - TempFBOBinder tempFBOBinder(false); - ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit - ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, currentTex, - target, level, isStencilFormat); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE"); - return; - } - - g_GLESFuncs.glBlitFramebuffer( - x, y, x + width, y + height, xoffset, yoffset, xoffset + width, yoffset + height, - GL_DEPTH_BUFFER_BIT | (isStencilFormat ? GL_STENCIL_BUFFER_BIT : 0), GL_NEAREST); - DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { - MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); - }); - } - } - - // P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL. - // - // ES generates a chain only for a colour-renderable, filterable format, and whether the - // driver's storage for these two is one depends on the driver: Espryt stores them natively - // where the driver renders the three-channel form and widens them to RGBA16F / RGBA32F where - // it does not (TextureImpl::BackendTextureFormatAddsAlpha), and a widened RGBA32F still needs - // OES_texture_float_linear to be filterable. So GenerateMipmapByRecord asks the driver first - // and comes here only when it refuses - or when MGITEST_ESPRYT_FORCE_CPU_MIPMAP says to, which - // is the only way a host whose driver never refuses reaches this arm. - // - // THE SOURCE IS THE SERVER'S LEVEL, NEVER THE CLIENT'S. The base level is read back from the - // driver (ReadTextureLevelTight), because a render target's texels exist only there - the - // monolith arm (GenerateThreeChannelFloatMipmapOnCpu) filters the frontend's level shadow - // instead and reads a stale base after a draw. Only when the driver cannot read the level back - // (storage it cannot attach) does the staged store answer, and a store that does not cover the - // level declines by name rather than inventing texels. - static Bool ForcedServerCpuMipmap() { - // The R-16 control. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs. - static const Bool s_forced = std::getenv("MGITEST_ESPRYT_FORCE_CPU_MIPMAP") != nullptr; - return s_forced; - } - - // WireTextureReadback.inc (included further down) defines it: E's extent-scoped stand-in for a - // driver that refuses to read a colour level back, asked here too so a host reaches the store - // route below (P8-SE). - static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent); - - // The base level as tight RGB floats: the driver's texels first, the staged store second. - // - // P8-SE: THE STORE ANSWERS ONLY FOR A TEXTURE THE DRIVER NEVER WROTE (P8-E's sticky mark). A - // render target's level 0 lives on the driver; the store still holds whatever crossed before - // the draw, and filtering it would hand the chain stale texels. `driverWrittenBefore` is the - // mark as it stood BEFORE this generation noted its own write (GenerateMipmap's split arm), - // because that note is unconditional and would otherwise decline every store read. Anything - // the store cannot stand for is a named decline, `declined` says why. - static Bool ReadThreeChannelFloatLevelOnServer(MG_Pipe::MGPipeHandle mipRes, - const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture, - TextureTarget textureTarget, TextureUploadTarget face, Uint32 level, - const IntVec3& extent, Bool driverWrittenBefore, - Vector& rgb, const char*& declined) { - const auto format = static_cast(record.Desc.InternalFormat); - const SizeT texels = static_cast(extent.x()) * static_cast(extent.y()); - Vector bytes; - if (!WireTextureReadbackRefusedForTesting(extent) && - ReadTextureLevelTight(backendTexture->GetBackendTextureId(), textureTarget, face, format, - static_cast(level), extent, backendTexture->RequiresImageBindableStorage(), - GL_RGB, GL_FLOAT, bytes) && - bytes.size() == texels * 3 * sizeof(Float)) { - rgb.resize(texels * 3); - Memcpy(rgb.data(), bytes.data(), bytes.size()); - return true; - } - auto& store = MG_Record::ServerStagedTexture(); - const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(mipRes); - const auto faceCode = static_cast(face); - const auto levelCode = static_cast(level); - const Bool half = format == TextureInternalFormat::RGB16F; - const SizeT componentBytes = half ? sizeof(Uint16) : sizeof(Float); - // A view's staged bytes live under its storage owner's key, in the owner's levels; a view - // whose level the driver cannot read has no answer here. - if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) { - declined = "a view has no staged level of its own"; - } else if (!store.Holds()) { - declined = "this arm keeps no staged store"; - } else if (driverWrittenBefore) { - declined = "the driver has written the texture since its bytes crossed"; - } else if (!store.IsCovered(key, faceCode, levelCode)) { - declined = "not every byte of the level crossed"; - } else if (store.LevelByteSize(key, faceCode, levelCode) != texels * 3 * componentBytes) { - declined = "the staged level is not the size of the level"; - } - if (declined != nullptr) return false; - const Uint8* staged = store.RequireLevelBytes(key, faceCode, levelCode, "generate-mipmap-server-cpu"); - rgb.resize(texels * 3); - for (SizeT i = 0; i < texels * 3; ++i) { - if (half) { - Uint16 bits = 0; - Memcpy(&bits, staged + i * componentBytes, sizeof(bits)); - rgb[i] = MG_Util::DecodeHalfBitsToFloat(bits); - } else { - Memcpy(&rgb[i], staged + i * componentBytes, sizeof(Float)); - } - } - MGLOG_W_ONCE("MGPipe: generate-mipmap-server-cpu: texture {%u, %u} level %u could not be read back from " - "the driver; filtering the staged store's copy of it instead", - mipRes.Slot, mipRes.Gen, level); - return true; - } - - static void GenerateThreeChannelFloatMipmapOnServer(MG_Pipe::MGPipeHandle mipRes, - const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture, - Bool driverWrittenBefore, const char* why) { - const auto& desc = record.Desc; - const auto format = static_cast(desc.InternalFormat); - const auto textureTarget = TextureTargetForPipeResourceTarget(desc.Target); - const RecordMipWindow window = GenerateMipmapWindowByRecord(record); - if (window.End <= window.Base + 1) return; - Vector faces; - if (textureTarget == TextureTarget::Texture2D) { - faces.push_back(TextureUploadTarget::Texture2D); - } else if (textureTarget == TextureTarget::TextureCubeMap) { - for (Int face = 0; face < 6; ++face) - faces.push_back(static_cast( - static_cast(TextureUploadTarget::CubeMapPositiveX) + face)); - } - // The upload spelling the driver's storage takes: GL_FLOAT is legal for all four float - // internal formats (ES 3.2 table 8.2), and the channel count follows the storage. - GLenum esInternal = GL_NONE, esFormat = GL_NONE, esType = GL_NONE; - TextureImpl::GenerateTextureFormatInfo(format, &esInternal, &esFormat, &esType, textureTarget); - const Bool fourChannel = esInternal == GL_RGBA16F || esInternal == GL_RGBA32F; - if (faces.empty() || (!fourChannel && esInternal != GL_RGB16F && esInternal != GL_RGB32F)) { - MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: target %s stored as %s has " - "no server CPU arm (%s); levels %u..%u are left as they are", - mipRes.Slot, mipRes.Gen, MG_Util::ConvertTextureTargetToString(textureTarget).c_str(), - MG_Util::ConvertGLEnumToString(esInternal).c_str(), why, window.Base + 1, window.End - 1); - return; - } - MGLOG_I_ONCE("MGPipe: generate-mipmap-server-cpu: %s levels filtered on the server's CPU from its own " - "level (%s)", MG_Util::ConvertTextureInternalFormatToString(format).c_str(), why); - - const GLuint textureId = backendTexture->GetBackendTextureId(); - const GLenum bindTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget); - for (const TextureUploadTarget face : faces) { - IntVec3 extent = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, - desc.Depth, window.Base); - Vector level; - const char* declined = nullptr; - if (!ReadThreeChannelFloatLevelOnServer(mipRes, record, backendTexture, textureTarget, face, window.Base, - extent, driverWrittenBefore, level, declined)) { - MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: base level %u could not " - "be read back from the driver and the staged store cannot stand for it (%s); levels " - "%u..%u are left as they are - DECLINED (generate-mipmap-store-declined)", - mipRes.Slot, mipRes.Gen, window.Base, declined != nullptr ? declined : "?", - window.Base + 1, window.End - 1); - return; - } - for (Uint32 dst = window.Base + 1; dst < window.End; ++dst) { - const IntVec3 dstExtent = - MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, dst); - // The monolith's box filter: the 2x2 source footprint, clamped where a dimension - // is already 1 (GL 4.6 core 8.14.4 leaves the filter to the implementation). - Vector next(static_cast(dstExtent.x()) * static_cast(dstExtent.y()) * 3); - const auto at = [&](Int x, Int y, Int c) { - return level[(static_cast(y) * static_cast(extent.x()) + static_cast(x)) * 3 + - static_cast(c)]; - }; - for (Int y = 0; y < dstExtent.y(); ++y) { - for (Int x = 0; x < dstExtent.x(); ++x) { - const Int x0 = std::min(x * 2, extent.x() - 1), x1 = std::min(x * 2 + 1, extent.x() - 1); - const Int y0 = std::min(y * 2, extent.y() - 1), y1 = std::min(y * 2 + 1, extent.y() - 1); - for (Int c = 0; c < 3; ++c) - next[(static_cast(y) * static_cast(dstExtent.x()) + static_cast(x)) * - 3 + static_cast(c)] = - 0.25f * (at(x0, y0, c) + at(x1, y0, c) + at(x0, y1, c) + at(x1, y1, c)); - } - } - Vector upload; - if (fourChannel) { - const SizeT count = next.size() / 3; - upload.resize(count * 4); - for (SizeT i = 0; i < count; ++i) { - upload[i * 4] = next[i * 3]; - upload[i * 4 + 1] = next[i * 3 + 1]; - upload[i * 4 + 2] = next[i * 3 + 2]; - upload[i * 4 + 3] = 1.0f; - } - } - // Tight float rows are a multiple of four bytes, which the resting UNPACK state - // (4/0/0/0, and the unpack-PBO binding at 0) parses as they are - CopyR32FTexture2D's - // discipline, including the re-bind that keeps the binding cache truthful. - ClearGLErrors(); - BufferImpl::BindPixelUnpackBufferId(0); - TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); - g_GLESFuncs.glBindTexture(bindTarget, textureId); - g_GLESFuncs.glTexSubImage2D(TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(face), - static_cast(dst), 0, 0, static_cast(dstExtent.x()), - static_cast(dstExtent.y()), fourChannel ? GL_RGBA : GL_RGB, - GL_FLOAT, fourChannel ? upload.data() : next.data()); - const GLenum err = g_GLESFuncs.glGetError(); - auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] - [static_cast(textureTarget)]; - g_GLESFuncs.glBindTexture(bindTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); - if (err != GL_NO_ERROR) { - MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu: uploading level %u of texture {%u, %u} " - "failed: %s", dst, mipRes.Slot, mipRes.Gen, - MG_Util::ConvertGLEnumToString(err).c_str()); - return; - } - level = Move(next); - extent = dstExtent; - } - } - } - - // P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from - // VerbMipRes. Same four arms as the frontend body, with every shape question answered from - // the descriptor. - // - // RULING 14's TRAILING FLIP. tx2 kept this verb's PipeCalls.def row at kWaitApplied because - // one frontend contact was left on the path: the RGB16F/RGB32F CPU filter reached into the - // client's level shadows. P8-B2 retired it (GenerateThreeChannelFloatMipmapOnServer reads the - // server's own level), so no arm here reads client memory any more; the WaitClass flip itself - // is a protocol change and is left to the integrator. - // - // `driverWrittenBefore`: P8-E's driver-written mark on the texture's staged store as it stood - // before this call noted its own write - the CPU arm's store route reads it (P8-SE). - static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes, - const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture, - Bool driverWrittenBefore) { - const auto format = static_cast(record.Desc.InternalFormat); - const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target); - const Bool threeChannelFloat = - format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F; - - // A view's layer window is the one bound the native arm cannot express, so it is asked - // for before the format arms - those two are 2D-only emulations of the same generation - // and a view reaches them too. - if (GenerateMipmapThroughViewWindowByRecord(mipRes, record, format)) return; - if (IsDepthOnlyFormat(format)) { - GenerateDepthTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record)); - return; - } - if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) { - GenerateColorTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record)); - return; - } - if (threeChannelFloat && ForcedServerCpuMipmap()) { - GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore, - "forced by MGITEST_ESPRYT_FORCE_CPU_MIPMAP"); - return; - } - - const GLenum backendTarget = - TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); - GLenum refused = GL_NO_ERROR; - { - // TempTextureUnit, not the active unit: the scratch bind is the backend's own and - // BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit - // was never anything but the frontend's way of naming the texture, and the record - // names it now. - backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit); - // ANGLE and some other drivers may validate the currently bound FBO while generating mipmaps, so the - // source texture is detached from every framebuffer that attaches it - by handle, - // over the applier's own framebuffer records. - ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes); - ScopedCompleteFramebufferBinding completeFramebuffer; - ClearGLErrors(); - g_GLESFuncs.glGenerateMipmap(backendTarget); - if (!threeChannelFloat) { - RecordGLError("glGenerateMipmap", backendTarget, format); - return; - } - // The driver's refusal of a three-channel float chain is not the application's - // error: the CPU arm below answers it. - refused = g_GLESFuncs.glGetError(); - } - if (refused == GL_NO_ERROR) return; - GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore, - "the driver refused glGenerateMipmap"); - } - - void PatchParameteri(GLenum pname, GLint value) { - if (g_GLESFuncs.glPatchParameteri == nullptr) return; - g_GLESFuncs.glPatchParameteri(pname, value); - } - - void GenerateMipmap(GLenum target) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - // P5e (tx2), CONTRACT-P5E §5.2 / ruling 14 (ID-92's neighbour): GENERATE_MIPMAP RESOLVES - // ITS TEXTURE FROM VerbMipRes, NEVER FROM THE ACTIVE UNIT. - // - // The verb's own record names the texture (MGPMipPlan::Res, stashed as VerbMipRes) and - // EnsureGenerateMipmapStorageAllocated's disaggregated arm has read it since P5c. What - // stood here instead was GetActiveTextureUnit + GetTextureUnitObject + the target's - // binding slot - three BARRIER_PULLED reads to re-derive a handle the record carried - - // and they are the ONLY reason this row still waits: ClientSession.cpp parks the client - // so that the unit it reads is the unit the call was made on. With the unit read gone the - // wait has nothing left to protect, which is what lets PipeCalls.def's WaitClass column - // for GenerateMipmap move to kWaitNone. - const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes; - // P8-SE: whether the staged store still stands for the texture is read BEFORE the note - // below sets the mark, or the CPU arm's store route could never answer at all. - const Bool driverWrittenBefore = MG_Record::ServerStagedTexture().IsDriverWritten( - MG_Record::StagedTextureStore::KeyForHandle(mipRes)); - // Every arm GenerateMipmapByRecord takes writes the levels above the base on the - // driver only (P8-B2's CPU arm uploads its levels there too), so the note is - // unconditional. - TextureImpl::NoteDriverSideTextureWriteByHandle(mipRes); - auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes); - const auto* record = PipeTextureRecordForHandle(mipRes); - if (!backendTexture || record == nullptr) { - MGLOG_E_ONCE("MGPipe: generate_mipmap names texture {%u, %u}, which has no applier " - "record or no driver texture; the call is dropped", - mipRes.Slot, mipRes.Gen); - return; - } - GenerateMipmapByRecord(target, mipRes, *record, backendTexture, driverWrittenBefore); - return; - } - - const GLubyte* GetString(GLenum name) { - return g_GLESFuncs.glGetString(name); - } - - void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - PrepareForCompute(false); - g_GLESFuncs.glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ); - } - - void DispatchComputeIndirect(GLintptr indirect) { -#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER - DebugImpl::OpenGLScopeMarker marker(__func__); -#endif - PrepareForCompute(true); - g_GLESFuncs.glDispatchComputeIndirect(indirect); - } - - // An atomic counter is a shader storage block by the time it reaches the ES driver (glslang - // lowers every atomic_uint onto one), so an application that asks only for the counter - // barrier is asking about memory the driver knows as storage-buffer memory. Ordering one - // does not oblige a driver to order the other, so the counter bit implies the storage bit - // here - which is what the lowering costs and the only place it can be paid. - static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) { - if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) { - barriers |= GL_SHADER_STORAGE_BARRIER_BIT; - } - return barriers; - } - - void MemoryBarrier(GLbitfield barriers) { - barriers = LowerAtomicCounterBarrierBits(barriers); - if ((barriers & GL_SHADER_STORAGE_BARRIER_BIT) != 0) BufferImpl::NoteShaderStorageWritesOrdered(); - g_GLESFuncs.glMemoryBarrier(barriers); - if (g_GLESCapabilities.IsAngleRenderer) { - g_GLESFuncs.glFlush(); - } - } - - void MemoryBarrierByRegion(GLbitfield barriers) { - g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers)); - } - - // One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it. - // - // The frontend hands this backend the target the APPLICATION named, and three of the - // targets core GL has do not exist in ES at all. They are not missing here either - the - // texture managers already store a 1D texture as a height-1 2D one, a 1D array as a - // height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) - - // but glCopyImageSubData was the one path that never asked for that translation and passed - // 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen, - // and with the error only asserted on (asserts are compiled out of an INFO build) the - // destination silently keeps whatever it held. - // - // The 1D-array case is not just a rename: GL addresses its layers with y/height while the - // ES 2D array that backs it addresses them with z/depth, so the two axes swap with the - // target. - // - // GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and - // GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while - // ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand - // the driver GL_UNKNOWN_MGL. - struct GLESCopyImageEndpoint { - GLenum target = GL_TEXTURE_2D; - // Exactly one of the two is set. The backend object is kept rather than its id, because - // the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture), - // so it is read at the point of use. - SharedPtr texture; - SharedPtr renderbuffer; - GLint x = 0; - GLint y = 0; - GLint z = 0; - - Bool IsRenderbuffer() const { return renderbuffer != nullptr; } - GLuint Name() const { - if (renderbuffer) return renderbuffer->GetBackendRenderbufferId(); - return texture ? texture->GetBackendTextureId() : 0u; - } - }; - - static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y, - GLint z, GLESCopyImageEndpoint& out) { - if (endpoint.IsRenderbuffer()) { - if (!PipeRenderbufferRecordForHandle(endpoint.RenderbufferHandle)) return false; - auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreateByHandle(endpoint.RenderbufferHandle); - if (!slot) return false; - if (!*slot) *slot = MakeShared(); - out.renderbuffer = *slot; - out.renderbuffer->SyncToBackendByHandle(endpoint.RenderbufferHandle); - if (!out.renderbuffer) return false; - out.target = GL_RENDERBUFFER; - out.x = x; - out.y = y; - out.z = z; - return true; - } - // BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a - // slot inside the backend texture registry, and the second call mutates that very map: - // GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by - // robin-hood displacement well under the load factor), and Find drops any - // entry whose state object has expired - which, with the map open-addressed and erasing - // by shifting the probe cluster backwards, relocates entries other than the erased one. - // Either way a reference taken by the first call is stale by the time the second returns, - // and it is read four more times below. Copying the SharedPtr costs two refcount bumps on - // a path that is already doing a texture copy. - // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never - // reaches here - but the assertion that says so is compiled out of a release build, and - // SyncTextureObjectToBackend would register a null state object. - out.texture = TextureImpl::SyncTextureToBackendByHandle(endpoint.TextureHandle); - if (!out.texture) return false; - const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); - out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); - // No axis remap for GL_TEXTURE_1D_ARRAY. The frontend STORES a 1D array with its layers - // on y (GetBackendUploadSize moves them across to the ES 2D array's z), but this entry - // point does not ADDRESS it that way: GL 4.6 core 18.3.2 treats every array texture as a - // stack of slices on z and gives a 1D array a height of 1 - exactly the shape the ES 2D - // array has - so GL's (x, 0, layer) and the ES image's (x, 0, layer) already agree. - // Remapping y into z here fetched the wrong slice for every call that spelled the layer - // the way GL defines it. - out.x = x; - out.y = y; - out.z = z; - return true; - } - - static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) { - const auto* record = endpoint.IsRenderbuffer() - ? PipeRenderbufferRecordForHandle(endpoint.RenderbufferHandle) - : PipeTextureRecordForHandle(endpoint.TextureHandle); - return record ? static_cast(record->Desc.InternalFormat) - : TextureInternalFormat::Unknown; - if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat(); - return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown; - } - - // Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one - // upload target (so not a cube map, whose six chains the z axis selects between) and layers on - // the z axis (GL_TEXTURE_1D_ARRAY carries them on y). - static Bool CanMirrorCopyImageShadow(const SharedPtr& texture) { - if (!texture) return false; - if (texture->GetTarget() == TextureTarget::Texture1DArray) return false; - return texture->GetUploadTargets().size() == 1; - } - - // glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU - // shadow has to stay authoritative - a packed format with redundant encodings, where a GPU - // readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) - - // the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already - // put these texels on the GPU, and flagging the level would only schedule a redundant upload - // back over them. - // - // Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot - // address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level - // whose shadow is missing or not a plain texel grid, a region outside either level, or a - // self-copy within one level, where the row copies could overlap. - static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX, - GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, - GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, - GLsizei width, GLsizei height, GLsizei depth) { - if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return; - if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return; - if (width <= 0 || height <= 0 || depth <= 0) return; - if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) { - return; - } - auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get()); - auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get()); - if (!srcMipmap || !dstMipmap) return; - // UNDER A REAL TRANSPORT THE SERVER HAS NO FRONTEND SHADOW TO MIRROR INTO (P5b i1, ruling - // §6.7), and P8-E settled where the move lives instead - not on the client (ROADMAP P8's - // original row) but in the server's staged store: CopyImageSubData's handle arm makes the - // same texel-block move there (TextureImpl::FollowCopyImageInStagedStore) for EVERY - // format, not only the redundant-encoding ones this mirror serves, and marks the store - // driver-written where it cannot. The store is what answers a split glGetTexImage the - // driver refuses to read back (WireTextureReadback.inc's store route); every other split - // read of the destination reads the GPU level itself (docs/archive/notes/p8/E.md). - // - // BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1), - // and the arm is the TRANSPORT and not the build - build-split runs its unit and - // integration-gpu lanes under MOBILEGL_TRANSPORT=monolith, where this mirror is on an - // ordinary correct path and must still run. - return; - } - - void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, - GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const CopyImageEndpoint& dstEndpoint, - GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, - GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - GLESCopyImageEndpoint src{}; - GLESCopyImageEndpoint dst{}; - // The DirectVulkan half of this entry point died exactly here, on a texture whose sync - // produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was - // supposed to catch it expands to nothing. The four Name() calls below are the same - // dereference. The frontend validator is what keeps this unreachable and what reports - // the error the application is owed; declining is only how a future gap up there stops - // being a crash. See the level guard in VulkanRenderer::CopyImageSubData. - // The handle arm's endpoint names its texture by handle, and the server has no frontend - // object to note. The staged store's half of the note waits for the copy (P8-E, below). - const Bool handleArmDestination = - !dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle); - if (handleArmDestination) { - TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle, /*storeFollowsTheWrite=*/true); - } - if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) || - !MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) { - MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__); - return; - } - // P8-E (docs/archive/notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced, - // so every byte the store holds for them is on the driver, and the copy below is a raw - // texel-block move - the store makes the same move, which keeps the destination level - // answerable from here when the driver will not read it back (ReadTextureImageWire's store - // route) and keeps a whole-level replay of it (a re-mint's refused readback, a full-level - // re-upload) from putting the pre-copy texels back. Where the store cannot follow (a - // renderbuffer, a cube or 1D-array endpoint, a source the driver has written, a level whose - // bytes did not all cross) it is marked driver-written instead, so it declines rather than - // answering with stale bytes. Nothing moves to the client (ROADMAP P8's original row). - if (handleArmDestination) { - const Bool followed = - !srcEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(srcEndpoint.TextureHandle) && - TextureImpl::FollowCopyImageInStagedStore( - srcEndpoint.TextureHandle, static_cast(srcLevel), IntVec3{srcX, srcY, srcZ}, - dstEndpoint.TextureHandle, static_cast(dstLevel), IntVec3{dstX, dstY, dstZ}, - IntVec3{srcWidth, srcHeight, srcDepth}); - if (!followed) TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle); - } - - // Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto - // wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint. - const GLsizei copyHeight = srcHeight; - const GLsizei copyDepth = srcDepth; - - const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint); - const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint); - // Both emulation fallbacks below are written against TEXTURE ids and texture targets, so - // an endpoint that is a renderbuffer takes the native ES copy - which accepts - // GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver - // turns it down. - const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer(); - - const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat); - const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat); - const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat); - const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat); - if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) { - MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil, - "DirectGLES CopyImageSubData only supports depth-only image copies."); - MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, - "DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D."); - MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, - "DirectGLES depth CopyImageSubData only supports single-layer copies."); - BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, - dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); - return; - } - - if (!anyRenderbuffer && - (srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) { - // The single glGetError below decides the fallback dispatch, and - // ErrorLopper::Clear is compiled out at the default log level - drain - // with the always-live helper so a stale flag cannot misroute a - // succeeded native copy into the 2D-only fallback. - ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, - dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z, - srcWidth, copyHeight, copyDepth); - const GLenum copyImageError = g_GLESFuncs.glGetError(); - if (copyImageError == GL_NO_ERROR) { - return; - } - MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat), - "DirectGLES CopyImageSubData only supports color-only or depth-only copies."); - MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, - "DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D."); - MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, - "DirectGLES color CopyImageSubData only supports single-layer copies."); - CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, - dst.Name(), dst.target, dstLevel, dst.x, dst.y); - return; - } - - ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, - dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z, - srcWidth, copyHeight, copyDepth); - // Every error condition glCopyImageSubData has was already ruled out by the frontend - // validator, so a driver error here is an internal invariant violation, not something - // the application can provoke. Say so where an INFO build can still see it, then trap - // in the builds that trap - the previous bare assert left a release build with a - // destination that silently kept its old contents. - const GLenum copyImageError = g_GLESFuncs.glGetError(); - if (copyImageError != GL_NO_ERROR) { - MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)", - MG_Util::ConvertGLEnumToString(copyImageError).c_str(), - MG_Util::ConvertGLEnumToString(src.target).c_str(), - MG_Util::ConvertGLEnumToString(srcTarget).c_str(), - MG_Util::ConvertGLEnumToString(dst.target).c_str(), - MG_Util::ConvertGLEnumToString(dstTarget).c_str()); - MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); - // P8-E: the store already made the move the driver just refused. - if (handleArmDestination) { - TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle); - } - return; - } - // The copy landed on the GPU. For a destination whose readback cannot be bit-exact the - // CPU shadow is what glGetTexImage answers from, so it has to follow the same move - - // otherwise it hands back whatever the level held before this copy. - if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) { - MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel, - dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); - } - } - - void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, - GLenum format) { - (void)level; - (void)layered; - (void)layer; - (void)access; - (void)format; - // P5e (fb): see NoteImageUnitBoundWithoutReadingTheFrontend. Under a transport this - // entry point records the high-water mark and lets the next validate point's sweep do - // the bind from the record; reading the frontend image binding here is the - // GetImageTextureBinding row that rule F forbids, and `bind_shader_image` is not a - // barriered row. - TextureImpl::NoteImageUnitBoundWithoutReadingTheFrontend(unit, texture != 0); - return; - } - - // Only the pnames MG_Impl/GLImpl/Getter/GL_Getter.cpp has no case for reach here. Every - // indexed pname naming FRONTEND state - the indexed buffer bindings, the per-unit - // texture/sampler bindings, the image-unit bindings, the viewport rectangles, the indexed - // capabilities - is answered there and returns before the table is consulted, so the arms - // this function used to carry for GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were - // unreachable duplicates of the frontend's, and they did not even agree with it (the - // frontend reports the range glBindBufferRange was ASKED for, verbatim and unclamped; these - // clamped it to the buffer's current storage). In practice what arrives is - // GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE, which the driver owns. - void GetIntegeri_v(GLenum target, GLuint index, GLint* data) { - if (!data) return; - if (g_GLESFuncs.glGetIntegeri_v) { - g_GLESFuncs.glGetIntegeri_v(target, index, data); - } else { - *data = 0; - } - } - - // NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry - // point must never be the thing that BUILDS a backend program. - // - // The frontend has already recorded the rebinding on the program object - // (SetShaderStorageBlockBinding) - that record is what GL_BUFFER_BINDING reports and what - // BackendProgramObjectImpl::SyncToBackend replays onto every driver program it builds. So - // the only work left here is an optimisation: push the change straight onto a driver - // program that is ALREADY built and already current with this link, so the next draw does - // not have to be preceded by a rebuild. - // - // Calling GetBackendProgramId() instead would sync-on-demand from a non-draw entry point, - // i.e. transpile and compile the whole program while the draw-path globals that the ESSL - // is generated against (PrgramImpl::g_fragColorBroadcastCount, the snorm/unorm clamp - // masks - established by SyncCurrentProgram) still hold another program's values. That - // bakes a program against the wrong state, and under CPU load it was also observed to - // fail the driver compile outright. Deferring is spec-fine: a binding only has to take - // effect by the block's next use. - void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) { - if (!storageBlockName) return; - const auto handle = MG_Pipe::MGPipeApplier().VerbStorageBlockProgram; - auto* slot = PrgramImpl::g_backendProgramObjects.FindByHandle(handle); - if (slot && *slot && (*slot)->GetBackendProgramId()) { - PrgramImpl::ApplyShaderStorageBlockBinding((*slot)->GetBackendProgramId(), - storageBlockName, storageBlockBinding); - } - return; - } - - void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { - TextureImpl::SyncNeccessaryTextures(); - FramebufferImpl::SyncCurrentFBO(); - RenderStateImpl::SyncRenderState(); - - BindCurrentFBO(FramebufferTarget::Draw); - - g_GLESFuncs.glClearBufferfi(buffer, drawbuffer, depth, stencil); - } - - namespace { - using FramebufferImpl::SubstituteWidenedClearAlpha; - - // A colour attachment the backend widened from three channels to four has to end up - // holding alpha 1.0 - the value GL reports for a channel the application's format does - // not have - so an explicit per-buffer clear of it writes 1.0 rather than whatever the - // application passed (SubstituteWidenedClearAlpha, in Managers.h). Draws can never move - // it again: their alpha write mask is forced off, see SyncRenderState. That pairing is - // what makes GL_DST_ALPHA blending, glReadPixels and glBlitFramebuffer all see the right - // value without any of them being intercepted. - - // Whether draw buffer `drawbuffer` of the framebuffer currently bound as DRAW is such an - // attachment. Answered from the mask SyncCurrentFBO just recomputed, so it costs nothing. - Bool IsWidenedBoundDrawBuffer(GLenum buffer, GLint drawbuffer) { - return buffer == GL_COLOR && drawbuffer >= 0 && drawbuffer < 32 && - (FramebufferImpl::g_alphaWidenedDrawBufferMask & (1u << drawbuffer)) != 0; - } - - } // namespace - - void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { - TextureImpl::SyncNeccessaryTextures(); - FramebufferImpl::SyncCurrentFBO(); - RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR); - - BindCurrentFBO(FramebufferTarget::Draw); - - GLfloat widenedValue[4] = {}; - g_GLESFuncs.glClearBufferfv( - buffer, drawbuffer, - SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), 1.0f, widenedValue)); - } - - void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { - TextureImpl::SyncNeccessaryTextures(); - FramebufferImpl::SyncCurrentFBO(); - RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR); - - // SyncCurrentFBO early-outs for the default framebuffer, so without this - // bind a user-FBO -> default-FBO switch would leave the clear landing on - // the stale driver DRAW binding (the fi/fv/uiv siblings all bind too). - BindCurrentFBO(FramebufferTarget::Draw); - - GLint widenedValue[4] = {}; - g_GLESFuncs.glClearBufferiv( - buffer, drawbuffer, - SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLint(1), widenedValue)); - } - - void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { - TextureImpl::SyncNeccessaryTextures(); - FramebufferImpl::SyncCurrentFBO(); - RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR); - - BindCurrentFBO(FramebufferTarget::Draw); - - GLuint widenedValue[4] = {}; - g_GLESFuncs.glClearBufferuiv( - buffer, drawbuffer, - SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLuint(1), widenedValue)); - } - - static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) { - const SizeT resolvedAlignment = static_cast(std::max(alignment, 1)); - return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); - } - - // Destination walk shared by the depth, stencil and packed depth-stencil readbacks. - // Each of those produces its rows tightly packed and has to land them in the caller's - // destination - client memory or the bound pixel-pack buffer - under the PACK - // pixel-store parameters. `fillRow` is handed the row index and a buffer of exactly one - // packed row to populate. Only real pixel rows are written, so PACK skip/row-length gap - // regions stay untouched. - template - static Bool StoreReadbackRowsToClient(GLsizei width, GLsizei height, SizeT dstPixelBytes, void* pixels, - const char* what, FillRow&& fillRow) { - const auto packParams = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT rowBytes = static_cast(width) * dstPixelBytes; - const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + rowBytes; - const auto& pixelPackBufferObject = - SplitReadbackPackBuffer(); - const SizeT pboOffset = reinterpret_cast(pixels); - if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { - MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what); - return false; - } - Vector rowBuf(rowBytes); - for (GLsizei row = 0; row < height; ++row) { - fillRow(row, rowBuf.data()); - const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; - if (pixelPackBufferObject) { - pixelPackBufferObject->WritebackFromBackend({rowBuf.data(), rowBytes}, pboOffset + rowOffset); - } else if (pixels != nullptr) { - Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), rowBytes); - } - } - if (pixelPackBufferObject) { - // WritebackFromBackend bumps change serials with no backend op; re-open the - // buffer draw-clean memos (once for the whole row loop). - BufferImpl::BumpBufferMutationEpoch(); - } - return true; - } - - // A normalized depth scaled into the full range of an unsigned integer of `maxValue` - // (GL 4.6 core table 18.2), without ever rounding past the top of that range. - static Uint32 NormalizedDepthToUnsigned(Float depth, Double maxValue) { - const Double clamped = std::min(std::max(static_cast(depth), 0.0), 1.0); - const Double scaled = clamped * maxValue + 0.5; - return static_cast(scaled >= maxValue ? maxValue : scaled); - } - - // --------------------------------------------------------------------------------- - // Depth / stencil readback by shader sampling - // - // Desktop GL reads depth and stencil back through glReadPixels; ES has no such call at - // all. GL_DEPTH_COMPONENT, GL_STENCIL_INDEX and GL_DEPTH_STENCIL are simply not - // accepted formats there, and the optional extensions that add them (GL_NV_read_depth, - // GL_NV_read_stencil, GL_NV_read_depth_stencil) are absent on both the Adreno device - // and llvmpipe's ES. Every native attempt therefore failed with a GL error and wrote - // NOTHING, so the caller kept whatever its buffer already held - which is how the CTS - // reports "expected DEPTH[0.25] but got DEPTH[0.2]": 0.2 is the poison value the test - // itself put there. - // - // What ES *can* do is sample a depth texture, so the emulation goes the long way round: - // - // 1. Stage. glBlitFramebuffer the requested rectangle out of the bound READ - // framebuffer into a scratch depth(-stencil) TEXTURE of the very same sized - // internal format. One staging copy serves every source kind uniformly - a - // texture attachment of any target/level/layer, a renderbuffer (not samplable at - // all), the default framebuffer, and a multisample attachment (the blit resolves - // it on the way). ES rejects a depth/stencil blit between differing formats, so - // the scratch has to match the source exactly; see DescribeReadDepthStencilSource. - // 2. Convert. Draw a full-screen triangle that samples the staged texture into a - // scratch R32UI colour target: depth as floatBitsToUint (bit-exact for every depth - // format, and an integer colour target needs no float-renderable extension), - // stencil through GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX. - // 3. Read. glReadPixels the colour target with GL_RGBA_INTEGER / GL_UNSIGNED_INT - - // the pair ES guarantees for an unsigned-integer attachment - and hand the values - // to the existing re-encoders, which already own the client-side (format, type) - // layout and the PACK pixel-store parameters. - // - // Both scratch images hold the rectangle at their own origin and the pass runs with a - // matching viewport, so GL's bottom-up row order survives untouched: scratch row 0 is - // source row `y`, which is exactly the first row glReadPixels(x, y, ...) owes the - // caller. No flip anywhere. - namespace DepthStencilSamplingReadImpl { - static Uint s_contextGeneration = ~0u; - static NativeContextName s_colorFramebuffer; // per context, as are the vertex array and the - // slots' framebuffers below - static GLuint s_colorTexture = 0; - static GLsizei s_colorWidth = 0; - static GLsizei s_colorHeight = 0; - static NativeContextName s_vertexArray; - static GLuint s_depthProgram = 0; - static GLuint s_stencilProgram = 0; - static GLint s_depthUvTransform = -1; - static GLint s_stencilUvTransform = -1; - static Bool s_programsFailed = false; - - // One staging slot per aspect. A framebuffer is allowed to carry its depth and its - // stencil in two DIFFERENT objects with two different formats - the framebuffer_blit - // cases pair a DEPTH_COMPONENT* attachment with a separate STENCIL_INDEX8 one - and - // the two aspects are staged independently for exactly that reason. A single shared - // slot would also throw its immutable storage away and re-create it on every - // alternation between the two. - struct StageSlot { - // A framebuffer of its own, not a shared one. GL_DEPTH_STENCIL_ATTACHMENT sets - // the depth AND the stencil point, so a packed scratch staged for one aspect - // would leave the other aspect's point pointing at it; the next stage of that - // other aspect attaches its own (differently formatted) texture to its own point - // and the framebuffer is then incomplete - which reads as "no candidate format - // worked" and writes nothing at all. - GLuint texture = 0; - GLenum format = 0; - GLsizei width = 0; - GLsizei height = 0; - // The default framebuffer has no queryable internal format, so the one that turns - // out to be blit-compatible is remembered: it cannot change for the life of the - // context, and re-probing it on every read would cost a failed blit each time. - GLenum defaultFramebufferFormat = 0; - }; - static StageSlot s_slots[2]; // [0] depth, [1] stencil - // Each slot's own framebuffer (see StageSlot), one per native context. - static NativeContextName s_slotFramebuffers[2]; - static NativeContextName& SlotFramebuffer(const StageSlot& slot) { return s_slotFramebuffers[&slot - s_slots]; } - - // `precision highp int` is not decoration: ESSL 3.00 defaults integers to mediump in - // the fragment language, which is allowed to be 16 bits - it would saw the top half - // off every depth bit pattern and every stencil fetch. - static const char* const kDepthFetchFragmentSource = - "#version 300 es\n" - "precision highp float;\n" - "precision highp int;\n" - "precision highp sampler2D;\n" - "uniform sampler2D uSource;\n" - "in vec2 vUv;\n" - "layout(location = 0) out uvec4 oBits;\n" - "void main() {\n" - " oBits = uvec4(floatBitsToUint(texture(uSource, vUv).r), 0u, 0u, 0u);\n" - "}\n"; - - static const char* const kStencilFetchFragmentSource = - "#version 300 es\n" - "precision highp float;\n" - "precision highp int;\n" - "precision highp usampler2D;\n" - "uniform usampler2D uSource;\n" - "in vec2 vUv;\n" - "layout(location = 0) out uvec4 oBits;\n" - "void main() {\n" - " oBits = uvec4(texture(uSource, vUv).r, 0u, 0u, 0u);\n" - "}\n"; - - static Bool EnsureResources() { - if (s_contextGeneration != g_backendContextGeneration) { - // The ids belonged to a dead context; the context reclaimed them with it. - s_colorFramebuffer = 0; - s_colorTexture = 0; - s_colorWidth = 0; - s_colorHeight = 0; - s_vertexArray = 0; - s_depthProgram = 0; - s_stencilProgram = 0; - s_programsFailed = false; - s_slots[0] = StageSlot{}; - s_slots[1] = StageSlot{}; - s_slotFramebuffers[0] = 0; - s_slotFramebuffers[1] = 0; - s_contextGeneration = g_backendContextGeneration; - } - if (s_programsFailed) { - return false; - } - if (s_depthProgram == 0) { - // Same full-screen vertex shader (and its uUvTransform) as the replicate - // blit: a staging slot is sized to the largest rectangle seen so far, so the - // quad's [0,1] coordinates have to be scaled down to the part it occupies. - s_depthProgram = ReplicateBlitImpl::BuildProgram(kDepthFetchFragmentSource); - s_stencilProgram = ReplicateBlitImpl::BuildProgram(kStencilFetchFragmentSource); - if (s_depthProgram == 0 || s_stencilProgram == 0) { - s_programsFailed = true; - MGLOG_E_ONCE("ReadPixels: could not build the depth/stencil readback programs"); - return false; - } - s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); - s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); - } - for (NativeContextName& framebuffer : s_slotFramebuffers) { - if (framebuffer == 0) { - g_GLESFuncs.glGenFramebuffers(1, &framebuffer); - if (framebuffer == 0) return false; - } - } - if (s_colorFramebuffer == 0) { - g_GLESFuncs.glGenFramebuffers(1, &s_colorFramebuffer); - if (s_colorFramebuffer == 0) return false; - } - if (s_vertexArray == 0) { - g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray); - if (s_vertexArray == 0) return false; - } - return true; - } - - // Ordered guesses at the sized internal format backing one aspect. More than one - // entry only when the source's own format cannot be queried (the default - // framebuffer), in which case the reported channel sizes narrow it down and the - // staging blit picks the winner by being the only one that raises no GL error. - struct AspectCandidates { - GLenum formats[4] = {0, 0, 0, 0}; - Uint count = 0; - void Push(GLenum format) { - for (Uint i = 0; i < count; ++i) { - if (formats[i] == format) return; - } - if (count < 4) formats[count++] = format; - } - }; - - static GLenum AttachmentPointFor(Bool isDefault, Bool stencilAspect) { - // The default framebuffer names its buffers GL_DEPTH / GL_STENCIL; a user - // framebuffer names them GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT, and asking - // one for the other's spelling is GL_INVALID_ENUM. - if (isDefault) return stencilAspect ? GL_STENCIL : GL_DEPTH; - return stencilAspect ? GL_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT; - } - - // Returns false when the bound READ framebuffer has no such aspect at all. - static Bool DescribeAspect(Bool stencilAspect, Bool* outIsDefault, AspectCandidates* out) { - GLint readFramebuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer); - const Bool isDefault = readFramebuffer == 0; - if (outIsDefault) *outIsDefault = isDefault; - const GLenum point = AttachmentPointFor(isDefault, stencilAspect); - - ClearGLErrors(); - GLint objectType = GL_NONE; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, point, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); - ClearGLErrors(); - - // The channel sizes are read before the "is there anything here" decision, because - // they are the more trustworthy witness. Adreno answers GL_NONE for OBJECT_TYPE on - // an attachment made by glFramebufferTexture (a layered cube/array attachment) while - // still reporting its depth and stencil bits correctly, and taking OBJECT_TYPE at - // its word there makes the whole readback report "no such aspect" for a framebuffer - // that plainly has one. The OTHER aspect's size matters as much as this one's - a - // depth buffer that also carries stencil has to be staged into a packed scratch, - // because ES only blits depth between identical formats. - GLint depthBits = 0; - GLint stencilBits = 0; - GLint componentType = GL_UNSIGNED_NORMALIZED; - ClearGLErrors(); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, - AttachmentPointFor(isDefault, false), - GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, - AttachmentPointFor(isDefault, true), - GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv( - GL_READ_FRAMEBUFFER, AttachmentPointFor(isDefault, false), - GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType); - ClearGLErrors(); - - const GLint aspectBits = stencilAspect ? stencilBits : depthBits; - if (objectType == GL_NONE && aspectBits <= 0) { - return false; - } - - if (!isDefault) { - // A real object: ask it directly and try that first. It is only a preference, - // not a verdict - the probe binds the attachment as GL_TEXTURE_2D, and a name - // whose target is not GL_TEXTURE_2D can leave it describing the wrong texture - // (see QueryAttachmentSizedFormat). The size-derived guesses below therefore - // stay in the list behind it, so a wrong first answer costs one rejected blit - // instead of the whole readback. The probe's own refusal must not be left on - // the error queue for the caller's next glGetError to pick up as its own. - // - // The cost of keeping the fallbacks: a source whose OWN format cannot back a - // staging texture (GL_STENCIL_INDEX8 without EXT/OES_texture_stencil8, say) no - // longer fails cleanly - it retries with a packed format, and a driver lax - // enough to accept the resulting mismatched depth/stencil blit would hand back - // data indistinguishable from a correct read. ES conformance forbids that blit, - // so this trades a spec-guaranteed rejection for a driver-bug-only wrong answer; - // the shapes it rescues (array and cube attachments) are otherwise unreadable. - const GLenum exact = ReplicateBlitImpl::QueryAttachmentSizedFormat(point); - ClearGLErrors(); - if (exact != 0) { - out->Push(exact); - } - } else if (s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat != 0) { - out->Push(s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat); - } - - const Bool floatDepth = componentType == GL_FLOAT; - const Bool packed = depthBits > 0 && stencilBits > 0; - if (stencilAspect) { - if (packed) { - out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8); - out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8); - } - out->Push(GL_STENCIL_INDEX8); - if (!packed) { - out->Push(GL_DEPTH24_STENCIL8); - } - } else if (packed) { - out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8); - out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8); - } else if (floatDepth) { - out->Push(GL_DEPTH_COMPONENT32F); - out->Push(GL_DEPTH32F_STENCIL8); - } else if (depthBits > 0 && depthBits <= 16) { - out->Push(GL_DEPTH_COMPONENT16); - out->Push(GL_DEPTH_COMPONENT24); - } else { - out->Push(GL_DEPTH_COMPONENT24); - out->Push(GL_DEPTH24_STENCIL8); - out->Push(GL_DEPTH_COMPONENT32F); - } - return out->count != 0; - } - - // Point a staging slot at `format`, growing it if the rectangle needs it, and leave - // it bound on the borrowed texture unit. - static Bool EnsureStageTexture(StageSlot& slot, GLenum format, GLsizei width, GLsizei height) { - if (slot.texture != 0 && slot.format == format && slot.width >= width && slot.height >= height) { - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); - return true; - } - if (slot.texture != 0) { - g_GLESFuncs.glDeleteTextures(1, &slot.texture); // immutable storage cannot be resized - ScratchFBOImpl::NoteTextureIdDeleted(slot.texture); - slot.texture = 0; - } - slot.format = 0; - slot.width = std::max(slot.width, width); - slot.height = std::max(slot.height, height); - g_GLESFuncs.glGenTextures(1, &slot.texture); - if (slot.texture == 0) return false; - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); - ClearGLErrors(); - g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, format, slot.width, slot.height); - const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - // A depth texture left in compare mode samples to 0/1 instead of the stored value. - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); - ClearGLErrors(); - slot.format = ok ? format : 0; - return ok; - } - - static Bool EnsureColorTexture(GLsizei width, GLsizei height) { - if (s_colorTexture != 0 && s_colorWidth >= width && s_colorHeight >= height) { - return true; - } - if (s_colorTexture != 0) { - g_GLESFuncs.glDeleteTextures(1, &s_colorTexture); - ScratchFBOImpl::NoteTextureIdDeleted(s_colorTexture); - s_colorTexture = 0; - } - s_colorWidth = std::max(s_colorWidth, width); - s_colorHeight = std::max(s_colorHeight, height); - g_GLESFuncs.glGenTextures(1, &s_colorTexture); - if (s_colorTexture == 0) return false; - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_colorTexture); - ClearGLErrors(); - // R32UI is colour-renderable in ES 3.0 core - no float-renderability extension - // needed - and carries a depth bit pattern or a stencil index without loss. - g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, s_colorWidth, s_colorHeight); - const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - ClearGLErrors(); - if (!ok) { - s_colorWidth = 0; - s_colorHeight = 0; - } - return ok; - } - - // Copy one aspect of the requested rectangle out of the bound READ framebuffer into - // its staging slot, trying each candidate format until one is blit-compatible. - // Requires the slot's own framebuffer bound as DRAW. - static Bool StageAspect(StageSlot& slot, const AspectCandidates& candidates, Bool stencilAspect, Bool isDefault, - GLint x, GLint y, GLsizei width, GLsizei height) { - const GLbitfield aspectBit = stencilAspect ? GL_STENCIL_BUFFER_BIT : GL_DEPTH_BUFFER_BIT; - for (Uint candidate = 0; candidate < candidates.count; ++candidate) { - const GLenum format = candidates.formats[candidate]; - const Bool formatHasAspect = stencilAspect ? ReplicateBlitImpl::FormatHasStencil(format) - : ReplicateBlitImpl::FormatHasDepth(format); - if (!formatHasAspect) { - continue; - } - if (!EnsureStageTexture(slot, format, width, height)) { - continue; - } - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, - ReplicateBlitImpl::ScratchAttachmentFor(format), GL_TEXTURE_2D, - slot.texture, 0); - if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - continue; - } - ClearGLErrors(); - // Only this aspect: a packed scratch standing in for a separate attachment - // has a second half with nothing to copy into it. - g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height, aspectBit, GL_NEAREST); - const GLenum blitErr = g_GLESFuncs.glGetError(); - if (blitErr != GL_NO_ERROR) { - continue; - } - if (isDefault) { - slot.defaultFramebufferFormat = format; - } - return true; - } - return false; - } - - // Runs one conversion pass over a staged slot and reads its colour target back. - // Requires the slot's texture bound on the borrowed unit and s_colorFramebuffer - // bound as DRAW. - static Bool ConvertAndRead(const StageSlot& slot, Bool stencilAspect, GLsizei width, GLsizei height, - Vector& outWords) { - if (ReplicateBlitImpl::FormatHasDepth(slot.format) && ReplicateBlitImpl::FormatHasStencil(slot.format)) { - g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, - stencilAspect ? GL_STENCIL_INDEX : GL_DEPTH_COMPONENT); - } - const Float uvScaleX = static_cast(width) / static_cast(slot.width); - const Float uvScaleY = static_cast(height) / static_cast(slot.height); - g_GLESFuncs.glUseProgram(stencilAspect ? s_stencilProgram : s_depthProgram); - g_GLESFuncs.glUniform4f(stencilAspect ? s_stencilUvTransform : s_depthUvTransform, uvScaleX, uvScaleY, - 0.0f, 0.0f); - g_GLESFuncs.glViewport(0, 0, width, height); - ClearGLErrors(); - g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); - if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { - MGLOG_E_ONCE("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth"); - return false; - } - - // GL_RGBA_INTEGER / GL_UNSIGNED_INT is the pair ES guarantees for an unsigned - // integer attachment whatever its channel count, so four words come back per - // pixel and only the first carries anything. - outWords.assign(static_cast(width) * static_cast(height) * 4u, 0u); - ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false); - FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, s_colorFramebuffer); - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); - ClearGLErrors(); - g_GLESFuncs.glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, outWords.data()); - const GLenum readError = g_GLESFuncs.glGetError(); - if (readError != GL_NO_ERROR) { - MGLOG_E_ONCE("ReadPixels: could not read the %s conversion target back: %s", - stencilAspect ? "stencil" : "depth", MG_Util::ConvertGLEnumToString(readError).c_str()); - return false; - } - return true; - } - - // Stage, convert and read one aspect. The caller owns the state guard and the DRAW - // framebuffer scope. - static Bool ReadAspect(Bool stencilAspect, GLint x, GLint y, GLsizei width, GLsizei height, - Vector& outWords) { - Bool isDefault = false; - AspectCandidates candidates; - if (!DescribeAspect(stencilAspect, &isDefault, &candidates)) { - return false; - } - StageSlot& slot = s_slots[stencilAspect ? 1 : 0]; - - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, SlotFramebuffer(slot)); - if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) { - MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source", - stencilAspect ? "stencil" : "depth"); - return false; - } - - if (!EnsureColorTexture(width, height)) { - MGLOG_E_ONCE("ReadPixels: could not allocate the depth/stencil conversion target"); - return false; - } - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, s_colorFramebuffer); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - s_colorTexture, 0); - if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E_ONCE("ReadPixels: the depth/stencil conversion target is not renderable"); - return false; - } - - // EnsureColorTexture may have taken the borrowed unit for its own storage call. - VertexArrayImpl::BindBackendVAOId(s_vertexArray); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); - return ConvertAndRead(slot, stencilAspect, width, height, outWords); - } - - // Fills whichever of the two outputs the caller asked for from the bound READ - // framebuffer. Returns false when the emulation could not service the request at - // all, leaving the caller to report the failure the way it always has. - static Bool Read(GLint x, GLint y, GLsizei width, GLsizei height, Vector* outDepth, - Vector* outStencil) { - if (width <= 0 || height <= 0 || (outDepth == nullptr && outStencil == nullptr)) { - return false; - } - // Sampling the stencil half of a packed texture goes through - // GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the - // pname would just raise GL_INVALID_ENUM and the shader would read depth bits as - // stencil. - const Bool supportsStencilTextureMode = - g_GLESCapabilities.GLESVersion.Major > 3 || - (g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1); - if (outStencil != nullptr && !supportsStencilTextureMode) { - return false; - } - if (!EnsureResources()) { - return false; - } - - // Taken before the first scratch texture bind: the guard owns the borrowed - // texture unit as well as the pipeline, and it is what puts the scissor test out - // of the way of the staging blit. - ScopedEmulationDrawState emulationState; - ScopedFramebufferBinding drawBinding(/*saveRead=*/false, /*saveDraw=*/true); - - const SizeT pixelCount = static_cast(width) * static_cast(height); - Vector words; - if (outDepth != nullptr) { - if (!ReadAspect(/*stencilAspect=*/false, x, y, width, height, words)) { - return false; - } - outDepth->assign(pixelCount, 0.0f); - for (SizeT i = 0; i < pixelCount; ++i) { - const Uint32 bits = words[i * 4u]; - Float value = 0.0f; - Memcpy(&value, &bits, sizeof(value)); - (*outDepth)[i] = value; - } - } - if (outStencil != nullptr) { - if (!ReadAspect(/*stencilAspect=*/true, x, y, width, height, words)) { - return false; - } - outStencil->assign(pixelCount, 0); - for (SizeT i = 0; i < pixelCount; ++i) { - (*outStencil)[i] = static_cast(words[i * 4u] & 0xFFu); - } - } - return true; - } - } // namespace DepthStencilSamplingReadImpl - - // One normalized depth value per pixel, tightly packed. Which native read a driver - // accepts depends on the attached format: a fixed-point depth buffer takes - // GL_UNSIGNED_INT, while a floating-point one (DEPTH_COMPONENT32F, - // DEPTH32F_STENCIL8 - what dEQP's own fbo-surface-type wrapper framebuffer uses) - // rejects it with GL_INVALID_OPERATION and only reads back as GL_FLOAT. Try both. - static Bool ReadDepthValuesNative(GLint x, GLint y, GLsizei width, GLsizei height, Vector& outDepth) { - outDepth.assign(static_cast(width) * static_cast(height), 0.0f); - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); - GLenum floatError = GL_INVALID_OPERATION; - if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) { - Vector raw(outDepth.size()); - // Drain first: a stale flag some earlier best-effort call left queued - // must not be misattributed to this read (it would silently drop the - // whole readback in production builds where ErrorLopper is compiled out). - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, raw.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - for (SizeT i = 0; i < outDepth.size(); ++i) { - outDepth[i] = static_cast(static_cast(raw[i]) / 4294967295.0); - } - return true; - } - - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, outDepth.data()); - floatError = g_GLESFuncs.glGetError(); - if (floatError == GL_NO_ERROR) { - return true; - } - } - - // Neither native spelling exists on this driver (which is the ordinary case: ES has - // no depth readback in core and GL_NV_read_depth is rare), so sample the attachment - // instead. The scoped pack state above is irrelevant to that path - it reads its own - // scratch colour target - but harmless, and leaving it in place keeps the restore in - // one place. - if (DepthStencilSamplingReadImpl::Read(x, y, width, height, &outDepth, /*outStencil=*/nullptr)) { - return true; - } - MGLOG_E_ONCE("ReadPixels: no depth readback path is available: native reads failed with %s and the " - "sampling emulation could not service the source", - MG_Util::ConvertGLEnumToString(floatError).c_str()); - return false; - } - - // GL_DEPTH_COMPONENT readback into the client's layout, honouring the PACK pixel-store - // parameters. GL 4.6 core 18.2.8 table 8.6: the normalized depth is written as-is for the - // floating-point client types and scaled into the full range of whichever integer width the - // client asked for otherwise - 2^n - 1 for an unsigned width, 2^(n-1) - 1 for a signed one. - // Sharing one denominator across the two signednesses would put a 0.5 depth at 32768, which - // as a GLshort is -32768, so GL_BYTE/GL_SHORT/GL_INT each keep their own. - static Bool ReadPixelsDepthComponent(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, - void* pixels) { - SizeT dstPixelBytes = 0; - switch (type) { - case GL_BYTE: - case GL_UNSIGNED_BYTE: dstPixelBytes = sizeof(Uint8); break; - case GL_SHORT: - case GL_UNSIGNED_SHORT: - case GL_HALF_FLOAT: dstPixelBytes = sizeof(Uint16); break; - case GL_INT: - case GL_UNSIGNED_INT: - case GL_FLOAT: dstPixelBytes = sizeof(Uint32); break; - default: return false; - } - if (width <= 0 || height <= 0) { - return true; - } - - Vector depth; - if (!ReadDepthValuesNative(x, y, width, height, depth)) { - return true; // already reported; nothing was written, as before - } - - StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "depth", - [&](GLsizei row, Uint8* dst) { - const Float* srcRow = - depth.data() + static_cast(row) * static_cast(width); - for (GLsizei col = 0; col < width; ++col) { - const Float depthValue = srcRow[col]; - switch (type) { - case GL_BYTE: - reinterpret_cast(dst)[col] = static_cast( - NormalizedDepthToUnsigned(depthValue, 127.0)); - break; - case GL_UNSIGNED_BYTE: - dst[col] = static_cast( - NormalizedDepthToUnsigned(depthValue, 255.0)); - break; - case GL_SHORT: - reinterpret_cast(dst)[col] = static_cast( - NormalizedDepthToUnsigned(depthValue, 32767.0)); - break; - case GL_UNSIGNED_SHORT: - reinterpret_cast(dst)[col] = static_cast( - NormalizedDepthToUnsigned(depthValue, 65535.0)); - break; - case GL_INT: - reinterpret_cast(dst)[col] = static_cast( - NormalizedDepthToUnsigned(depthValue, 2147483647.0)); - break; - case GL_UNSIGNED_INT: - reinterpret_cast(dst)[col] = - NormalizedDepthToUnsigned(depthValue, 4294967295.0); - break; - case GL_HALF_FLOAT: - reinterpret_cast(dst)[col] = - MG_Util::EncodeFloatToHalfBits(depthValue); - break; - default: - reinterpret_cast(dst)[col] = depthValue; - break; - } - } - }); - return true; - } - - // One stencil byte per pixel, tightly packed. ES has no guaranteed stencil readback - // at all: GL_STENCIL_INDEX needs GL_NV_read_stencil, and a driver without it rejects - // the read outright. Where the attachment is a combined depth-stencil buffer the - // packed GL_DEPTH_STENCIL read (which the packed_depth_stencil.verify_* cases already - // rely on) carries the same bytes in its low octet, so use that as the fallback. - static Bool ReadStencilBytesNative(GLint x, GLint y, GLsizei width, GLsizei height, Vector& outStencil) { - outStencil.assign(static_cast(width) * static_cast(height), 0); - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); - GLenum packedError = GL_INVALID_OPERATION; - if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) { - // Drain first: see ReadDepthValuesNative. - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, outStencil.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - return true; - } - - Vector packed(outStencil.size(), 0); - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - for (SizeT i = 0; i < outStencil.size(); ++i) { - outStencil[i] = static_cast(packed[i] & 0xFFu); - } - return true; - } - - // A DEPTH32F_STENCIL8 attachment rejects the 24_8 type: its packed layout is a - // float depth followed by a padded stencil byte, eight bytes per pixel with the - // index at offset 4. - Vector packed32f(outStencil.size() * 8u, 0); - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, - packed32f.data()); - packedError = g_GLESFuncs.glGetError(); - if (packedError == GL_NO_ERROR) { - for (SizeT i = 0; i < outStencil.size(); ++i) { - outStencil[i] = packed32f[i * 8u + 4u]; - } - return true; - } - } - - // No native spelling worked, which is the ordinary case on ES: sample the stencil - // half of the attachment instead. - if (DepthStencilSamplingReadImpl::Read(x, y, width, height, /*outDepth=*/nullptr, &outStencil)) { - return true; - } - MGLOG_E_ONCE("ReadPixels: no stencil readback path is available: native reads failed with %s and the " - "sampling emulation could not service the source", - MG_Util::ConvertGLEnumToString(packedError).c_str()); - return false; - } - - // GL_STENCIL_INDEX readback into the client's integer layout, honouring the PACK - // pixel-store parameters. Handles the widths desktop clients ask for; the stencil - // values themselves are always 8 bits. - static Bool ReadPixelsStencilViaNative(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, - void* pixels) { - // GL 4.6 core 18.2.8: a stencil index is written unconverted into whichever integer width - // the client asked for, and converted to a float value for the floating-point widths - both - // GL_FLOAT and GL_HALF_FLOAT. The signed widths are as legal as the unsigned ones - the CTS - // reads stencil with GL_INT - and rejecting them here used to let the call fall through to a - // native ES read the driver refuses, after which nothing was written at all and the caller - // kept its zeros. - SizeT dstPixelBytes = 0; - switch (type) { - case GL_UNSIGNED_BYTE: - case GL_BYTE: dstPixelBytes = sizeof(Uint8); break; - case GL_UNSIGNED_SHORT: - case GL_SHORT: - case GL_HALF_FLOAT: dstPixelBytes = sizeof(Uint16); break; - case GL_UNSIGNED_INT: - case GL_INT: dstPixelBytes = sizeof(Uint32); break; - case GL_FLOAT: dstPixelBytes = sizeof(GLfloat); break; - default: return false; - } - if (width <= 0 || height <= 0) { - return true; - } - - Vector raw; - if (!ReadStencilBytesNative(x, y, width, height, raw)) { - return true; - } - - StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "stencil", - [&](GLsizei row, Uint8* dst) { - const Uint8* srcRow = - raw.data() + static_cast(row) * static_cast(width); - for (GLsizei col = 0; col < width; ++col) { - switch (type) { - case GL_UNSIGNED_BYTE: - case GL_BYTE: - dst[static_cast(col)] = srcRow[col]; - break; - case GL_UNSIGNED_SHORT: - case GL_SHORT: - reinterpret_cast(dst)[col] = srcRow[col]; - break; - case GL_FLOAT: - reinterpret_cast(dst)[col] = static_cast(srcRow[col]); - break; - case GL_HALF_FLOAT: - reinterpret_cast(dst)[col] = - MG_Util::EncodeFloatToHalfBits(static_cast(srcRow[col])); - break; - default: - reinterpret_cast(dst)[col] = srcRow[col]; - break; - } - } - }); - return true; - } - - // GL_DEPTH_STENCIL readback: the two aspects are fetched separately and woven into the - // packed layout the client asked for (GL 4.6 core table 8.6). This is what - // KHR-GL3x.packed_depth_stencil.verify_read_pixels / verify_get_tex_image / - // verify_copy_tex_image read their gradients with. - static Bool ReadPixelsDepthStencilPacked(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, - void* pixels) { - SizeT dstPixelBytes = 0; - switch (type) { - case GL_UNSIGNED_INT_24_8: dstPixelBytes = sizeof(Uint32); break; - case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: dstPixelBytes = sizeof(Float) + sizeof(Uint32); break; - default: return false; - } - if (width <= 0 || height <= 0) { - return true; - } - - Vector depth; - Vector stencil; - if (!ReadDepthValuesNative(x, y, width, height, depth)) { - return true; - } - if (!ReadStencilBytesNative(x, y, width, height, stencil)) { - return true; - } - - StoreReadbackRowsToClient( - width, height, dstPixelBytes, pixels, "packed depth/stencil", [&](GLsizei row, Uint8* dst) { - const SizeT base = static_cast(row) * static_cast(width); - for (GLsizei col = 0; col < width; ++col) { - const Uint32 stencilIndex = stencil[base + static_cast(col)]; - const Float depthValue = depth[base + static_cast(col)]; - if (type == GL_UNSIGNED_INT_24_8) { - reinterpret_cast(dst)[col] = - (NormalizedDepthToUnsigned(depthValue, 16777215.0) << 8) | stencilIndex; - } else { - // Depth float first, then a word whose low octet is the index and - // whose top 24 bits are unused. - Uint8* pixel = dst + static_cast(col) * (sizeof(Float) + sizeof(Uint32)); - Memcpy(pixel, &depthValue, sizeof(depthValue)); - Memcpy(pixel + sizeof(Float), &stencilIndex, sizeof(stencilIndex)); - } - } - }); - return true; - } - - // ---- Client-format readback conversion --------------------------------------------------------------------- - // ES 3.x glReadPixels only guarantees GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/GL_(UNSIGNED_)INT, - // GL_RGBA/GL_FLOAT for float buffers plus one implementation-defined pair, while desktop GL clients read - // back narrower layouts (RED, RG, RGB, BGR, byte-order packed types, ...). For those we read a guaranteed - // wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU, - // honoring the client-side PACK pixel-store parameters. The pure repacking helpers live in - // ClientReadback (MG_Backend/ClientReadback.cpp) so unit tests can assert the exact packed words. - - using ClientReadback::GetReadbackChannelMapping; - using ClientReadback::GetReadbackComponentSize; - using ClientReadback::GetReadbackDstPixelSize; - using ClientReadback::ReadbackChannelMapping; - - static Bool CanDecodeWideSourceType(GLenum type) { - switch (type) { - case GL_UNSIGNED_BYTE: - case GL_BYTE: - case GL_UNSIGNED_SHORT: - case GL_SHORT: - case GL_HALF_FLOAT: - case GL_FLOAT: - return true; - default: - return false; - } - } - - // Component-array read formats usable as (possibly narrow) wide-read sources. - static Int GetWideReadChannelCount(GLenum format) { - switch (format) { - case GL_RED: - case GL_RED_INTEGER: - return 1; - case GL_RG: - case GL_RG_INTEGER: - return 2; - case GL_RGB: - case GL_RGB_INTEGER: - return 3; - case GL_RGBA: - case GL_RGBA_INTEGER: - return 4; - default: - return 0; - } - } - - static Bool IsIntegerReadFormat(GLint format) { - return format == GL_RED_INTEGER || format == GL_RG_INTEGER || format == GL_RGB_INTEGER || - format == GL_RGBA_INTEGER; - } - - // The bit pattern of 1.0 in a wide-read component type: what GL reports for a channel the - // attachment's format does not have. - static void FillWideReadOneBits(GLenum componentType, Uint8* oneBits) { - switch (componentType) { - case GL_UNSIGNED_BYTE: - oneBits[0] = 0xFF; - break; - case GL_BYTE: - oneBits[0] = 0x7F; - break; - case GL_UNSIGNED_SHORT: { - const Uint16 one = 0xFFFF; - Memcpy(oneBits, &one, sizeof(one)); - break; - } - case GL_SHORT: { - const Int16 one = 0x7FFF; - Memcpy(oneBits, &one, sizeof(one)); - break; - } - case GL_HALF_FLOAT: { - const Uint16 one = 0x3C00; - Memcpy(oneBits, &one, sizeof(one)); - break; - } - case GL_FLOAT: { - const Float one = 1.0f; - Memcpy(oneBits, &one, sizeof(one)); - break; - } - case GL_UNSIGNED_INT: - case GL_INT: { - const Uint32 one = 1; - Memcpy(oneBits, &one, sizeof(one)); - break; - } - default: - break; - } - } - - // Overwrites the alpha of a 4-channel wide read with the format's implied 1.0. Used for an - // attachment the backend widened from three channels to keep it colour-renderable: the storage - // has a real alpha channel holding whatever the draw wrote, but the format the application - // asked for has none, and GL reads a missing channel back as one. - static void ForceWideReadAlphaToOne(Vector& data, SizeT pixelCount, GLenum componentType) { - const SizeT componentSize = GetReadbackComponentSize(componentType); - if (componentSize == 0 || data.size() < pixelCount * 4 * componentSize) { - return; - } - Uint8 oneBits[4] = {0, 0, 0, 0}; - FillWideReadOneBits(componentType, oneBits); - for (SizeT i = 0; i < pixelCount; ++i) { - Memcpy(data.data() + (i * 4 + 3) * componentSize, oneBits, componentSize); - } - } - - // Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA - // layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in - // the source component type. - static void ExpandNarrowWideRead(Vector& data, SizeT pixelCount, Int srcChannels, GLenum componentType) { - const SizeT componentSize = GetReadbackComponentSize(componentType); - if (componentSize == 0 || srcChannels <= 0 || srcChannels >= 4) { - return; - } - Uint8 zeroBits[4] = {0, 0, 0, 0}; - Uint8 oneBits[4] = {0, 0, 0, 0}; - FillWideReadOneBits(componentType, oneBits); - - Vector expanded(pixelCount * 4 * componentSize); - for (SizeT i = 0; i < pixelCount; ++i) { - const Uint8* src = data.data() + i * static_cast(srcChannels) * componentSize; - Uint8* dst = expanded.data() + i * 4 * componentSize; - for (Int ch = 0; ch < 4; ++ch) { - if (ch < srcChannels) { - Memcpy(dst + static_cast(ch) * componentSize, src + static_cast(ch) * componentSize, - componentSize); - } else { - Memcpy(dst + static_cast(ch) * componentSize, ch == 3 ? oneBits : zeroBits, componentSize); - } - } - } - data = std::move(expanded); - } - - static void DrainESErrors() { DrainDriverErrors("ReadPixels"); } - - static GLenum QueryReadAttachmentComponentType() { - GLint framebufferId = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &framebufferId); - if (framebufferId == 0) { - return GL_UNSIGNED_NORMALIZED; // default framebuffers are normalized fixed-point - } - GLint readBuffer = GL_COLOR_ATTACHMENT0; - g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); - if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) { - readBuffer = GL_COLOR_ATTACHMENT0; - } - GLint componentType = 0; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), - GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType); - DrainESErrors(); - return componentType != 0 ? static_cast(componentType) : GL_UNSIGNED_NORMALIZED; - } - - - // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's - // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its - // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op. - // `forceOpaqueAlpha`: the source image is a three-channel format the backend widened to four to - // keep it colour-renderable, so its alpha channel holds whatever the draw wrote and has to be - // answered with the 1.0 the application's format implies. Passed in rather than derived here: - // glReadPixels reads the bound READ framebuffer, but glGetTexImage reads a texture through a - // scratch framebuffer, so the frontend's READ binding describes a different image entirely - - // consulting it there would both miss real widenings and corrupt readbacks of ordinary - // textures taken while some unrelated widened attachment happened to be bound. - // The image-format widening's READ half, for the seven normalized formats whose carrier holds - // their channels as INTEGER CODES (GL_RGBA16 stored as a GL_RGBA16UI - see - // TextureImpl::GetImageBindableStorageWidening). Nothing else in the readback would get those - // right: the attachment is an integer one while the application's format is normalized, so the - // class check below would refuse the read outright, and a repack that got past it would hand - // back 65535.0 where GL owes 1.0. - // - // Inactive (ChannelMax all zero) for every other read, which is all but a handful. - struct NormalizedImageCarrierRead { - Uint ChannelMax[4] = {0u, 0u, 0u, 0u}; - Bool SignedNormalized = false; - - Bool Active() const { return ChannelMax[0] != 0u; } - }; - - static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, - GLenum type, void* pixels, Bool honorPackImageParams, - Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha, - const NormalizedImageCarrierRead& normalizedCarrier = {}) { - ReadbackChannelMapping mapping{}; - if (!GetReadbackChannelMapping(format, mapping)) { - return false; - } - // Covers unknown types, packed field-count/format mismatches and float types on integer formats. - const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); - if (dstPixelBytes == 0) { - return false; - } - - if (width <= 0 || height <= 0) { - return true; - } - const auto& pixelPackBufferObject = - SplitReadbackPackBuffer(); - if (!pixelPackBufferObject && pixels == nullptr) { - return true; - } - - const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); - const Bool integerAttachment = - attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; - // A normalized image carrier is EXACTLY the case where the two disagree on purpose, and - // it is the caller - which knows the TEXTURE being read, not just the attachment - that - // says so. An integer client format through such a carrier is not a shape GL can ask for - // (the frontend format is normalized), so it is refused here rather than converted. - if (normalizedCarrier.Active() && (mapping.isInteger || !integerAttachment)) { - MGLOG_E_ONCE("Readback conversion: a normalized image carrier was read as %s, which is not a " - "normalized client format; skipping", - MG_Util::ConvertGLEnumToString(format).c_str()); - return true; - } - if (!normalizedCarrier.Active() && mapping.isInteger != integerAttachment) { - MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", - MG_Util::ConvertGLEnumToString(format).c_str()); - return true; - } - - // Prefer the implementation-defined pair (full precision on e.g. norm16 buffers, and possibly - // a narrow format like GL_RED/GL_UNSIGNED_SHORT), then the spec/extension-guaranteed pair for - // the attachment class. Narrow reads are expanded to RGBA on the CPU afterwards. - GLint implFormat = 0; - GLint implType = 0; - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); - - struct WideReadCandidate { - GLenum format; - GLenum type; - }; - WideReadCandidate candidates[4]; - Int candidateCount = 0; - if (normalizedCarrier.Active()) { - // The storage IS an integer texture, whatever the application's format says, so the - // only read that can answer is the integer one. The codes it hands back are turned - // into the floats the client asked for below. - candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT}; - } else if (mapping.isInteger) { - if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && IsIntegerReadFormat(implFormat) && - (implType == GL_INT || implType == GL_UNSIGNED_INT)) { - candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; - } - candidates[candidateCount++] = { - GL_RGBA_INTEGER, - attachmentComponentType == GL_INT ? static_cast(GL_INT) : static_cast(GL_UNSIGNED_INT)}; - } else { - if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && !IsIntegerReadFormat(implFormat) && - (CanDecodeWideSourceType(static_cast(implType)) || - (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) { - candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; - } - if (attachmentComponentType == GL_FLOAT) { - candidates[candidateCount++] = {GL_RGBA, GL_FLOAT}; - } - if (attachmentComponentType == GL_SIGNED_NORMALIZED) { - // EXT_render_snorm attachments read back as RGBA/BYTE (8-bit) or RGBA/SHORT (16-bit). - candidates[candidateCount++] = {GL_RGBA, GL_SHORT}; - candidates[candidateCount++] = {GL_RGBA, GL_BYTE}; - } else { - candidates[candidateCount++] = {GL_RGBA, GL_UNSIGNED_BYTE}; - } - } - - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); - - Vector wide; - GLenum wideType = GL_NONE; - GLenum readFormat = GL_NONE; - Int readChannels = 0; - DrainESErrors(); - for (Int i = 0; i < candidateCount; ++i) { - const WideReadCandidate candidate = candidates[i]; - Bool alreadyTried = false; - for (Int j = 0; j < i; ++j) { - alreadyTried = - alreadyTried || (candidates[j].format == candidate.format && candidates[j].type == candidate.type); - } - if (alreadyTried) { - continue; - } - const Int channels = GetWideReadChannelCount(candidate.format); - const SizeT candidateComponentSize = GetReadbackComponentSize(candidate.type); - wide.resize(static_cast(width) * static_cast(height) * - static_cast(channels) * candidateComponentSize); - g_GLESFuncs.glReadPixels(x, y, width, height, candidate.format, candidate.type, wide.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - wideType = candidate.type; - readFormat = candidate.format; - readChannels = channels; - break; - } - } - if (wideType == GL_NONE) { - MGLOG_E_ONCE("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); - return true; - } - - if (normalizedCarrier.Active()) { - // GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same - // denominators, so a texel an imageStore wrote and a texel the upload seeded read back - // identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with - // the signed code recovered from the low sixteen bits of the unsigned channel. - const SizeT pixelCount = static_cast(width) * static_cast(height); - Vector floatWide(pixelCount * 4 * sizeof(Float)); - auto* dst = reinterpret_cast(floatWide.data()); - const auto* src = reinterpret_cast(wide.data()); - for (SizeT i = 0; i < pixelCount; ++i) { - for (SizeT channel = 0; channel < 4; ++channel) { - const Uint32 code = src[i * 4 + channel]; - const auto denominator = static_cast(normalizedCarrier.ChannelMax[channel]); - if (normalizedCarrier.SignedNormalized) { - const auto signedCode = static_cast(static_cast(code)); - dst[i * 4 + channel] = - std::max(static_cast(signedCode) / denominator, -1.0f); - } else { - dst[i * 4 + channel] = static_cast(code) / denominator; - } - } - } - wide = Move(floatWide); - wideType = GL_FLOAT; - readChannels = 4; - } - if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { - // Unpack the packed words into a float wide buffer (full 10-bit precision on e.g. - // GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV). - const SizeT pixelCount = static_cast(width) * static_cast(height); - Vector floatWide(pixelCount * 4 * sizeof(Float)); - auto* dst = reinterpret_cast(floatWide.data()); - for (SizeT i = 0; i < pixelCount; ++i) { - Uint32 word; - Memcpy(&word, wide.data() + i * 4, sizeof(word)); - dst[i * 4 + 0] = static_cast(word & 0x3FFu) / 1023.0f; - dst[i * 4 + 1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; - dst[i * 4 + 2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; - dst[i * 4 + 3] = static_cast((word >> 30) & 0x3u) / 3.0f; - } - wide = std::move(floatWide); - wideType = GL_FLOAT; - readChannels = 4; - } - if (readChannels < 4) { - ExpandNarrowWideRead(wide, static_cast(width) * static_cast(height), readChannels, wideType); - } - - // Undo the three-channel widening (see the parameter's comment). Deliberately not gated on - // applyFixedPointReadClamp: that flag implements GL_CLAMP_READ_COLOR, which glGetTexImage - // is exempt from, whereas "a format without alpha reads as 1.0" is the format's own - // semantics and applies to every read. - if (forceOpaqueAlpha) { - ForceWideReadAlphaToOne(wide, static_cast(width) * static_cast(height), wideType); - } - - // GL clamps a read from a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR - // defaults to GL_FIXED_ONLY). Formats the backend substitutes with a floating-point - // one keep the out-of-range value the app stored, so apply the clamp here - a - // GL_R16_SNORM target holding -0.125 must still read back as 0. - // glReadPixels only: GL_CLAMP_READ_COLOR does not apply to glGetTexImage, which - // reaches this helper through the same scratch-framebuffer path. - if (applyFixedPointReadClamp && FramebufferImpl::IsFixedPointFallbackReadAttachment()) { - const SizeT componentSize = GetReadbackComponentSize(wideType); - const SizeT valueCount = componentSize != 0 ? wide.size() / componentSize : 0; - switch (wideType) { - case GL_FLOAT: { - auto* values = reinterpret_cast(wide.data()); - for (SizeT i = 0; i < valueCount; ++i) values[i] = std::clamp(values[i], 0.0f, 1.0f); - break; - } - case GL_HALF_FLOAT: { - auto* values = reinterpret_cast(wide.data()); - for (SizeT i = 0; i < valueCount; ++i) { - values[i] = MG_Util::EncodeFloatToHalfBits( - std::clamp(MG_Util::DecodeHalfBitsToFloat(values[i]), 0.0f, 1.0f)); - } - break; - } - case GL_SHORT: { - // Signed normalized: the negative half is exactly what the clamp removes. - auto* values = reinterpret_cast(wide.data()); - for (SizeT i = 0; i < valueCount; ++i) values[i] = std::max(values[i], 0); - break; - } - case GL_BYTE: { - auto* values = reinterpret_cast(wide.data()); - for (SizeT i = 0; i < valueCount; ++i) values[i] = std::max(values[i], 0); - break; - } - default: - break; - } - } - - if (!ClientReadback::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, - type, pixels, honorPackImageParams)) { - return false; - } - - MGLOG_D("Readback conversion: converted %s/%s from wide %s/%s", MG_Util::ConvertGLEnumToString(format).c_str(), - MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(readFormat).c_str(), - MG_Util::ConvertGLEnumToString(wideType).c_str()); - return true; - } - - // ---- Bit-exact readback of a 32-bit packed colour level --------------------------------------- - // - // glGetTexImage of a packed format read with its OWN client type owes the application the words - // the image HOLDS, and neither of the two routes above can promise that once anything other than - // a glTexImage has written the level: - // - // * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an - // RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000, same value, different bits) and - // collapses an R11F_G11F_B10F NaN to the canonical payload 1 - // (MG_Util::EncodeFloatToUnsignedSmallFloat) - and a copy-image from RGB9_E5 lands exactly - // such a NaN in the 10-bit blue field every time, because the source's shared-exponent - // field is all ones; - // * the CPU shadow only ever holds what was UPLOADED, so for a level glCopyImageSubData wrote - // it answers with the PRE-COPY contents. MirrorCopyImageIntoDestinationShadow patches that - // up for the shapes it can address texel-exactly and declines for the rest - a renderbuffer - // source (which has no shadow to mirror from at all), a cube or 1D-array endpoint, a - // self-copy - and the decline is silent, so the stale words are served as truth. - // - // glCopyImageSubData is a raw texel-block move and EXT_copy_image puts every 32-bit colour - // format in one compatibility class, so copying the level into a scratch GL_R32UI image and - // reading THAT back as unsigned integers hands over the stored words themselves, whoever wrote - // them. This is what lets the shadow stop being the authority for these formats: it is tried - // first, and every step reports rather than guesses, so a driver that turns any of it down - // simply leaves the old shadow/attachment fallbacks to run. - static GLuint g_packedWordScratchTextureId = 0; - static GLsizei g_packedWordScratchWidth = 0; - static GLsizei g_packedWordScratchHeight = 0; - - // Grow-only, so a readback sweep over a mip chain allocates once. Zero when the driver refused - // the storage, which is a decline and not an error. - static GLuint EnsurePackedWordScratchTexture(GLsizei width, GLsizei height) { - if (g_packedWordScratchTextureId != 0 && g_packedWordScratchWidth >= width && - g_packedWordScratchHeight >= height) { - return g_packedWordScratchTextureId; - } - const GLsizei newWidth = std::max(width, g_packedWordScratchWidth); - const GLsizei newHeight = std::max(height, g_packedWordScratchHeight); - if (g_packedWordScratchTextureId != 0) { - // A scratch FBO may still name the old id, and the driver is free to hand the same - // number back for the replacement - which would false-skip the re-attach. - ScratchFBOImpl::NoteTextureIdDeleted(g_packedWordScratchTextureId); - g_GLESFuncs.glDeleteTextures(1, &g_packedWordScratchTextureId); - g_packedWordScratchTextureId = 0; - g_packedWordScratchWidth = 0; - g_packedWordScratchHeight = 0; - } - GLuint texture = 0; - g_GLESFuncs.glGenTextures(1, &texture); - if (texture == 0) return 0; - - ClearGLErrors(); - TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, texture); - // Immutable single-level storage: glCopyImageSubData wants a complete image, and - // glTexStorage clamps TEXTURE_MAX_LEVEL, which is what makes a one-level texture complete - // under the default mipmapping filter. - g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, newWidth, newHeight); - const GLenum storageError = g_GLESFuncs.glGetError(); - // Re-bind whatever the binding cache says lives on the temp unit, so the cache stays - // truthful without a driver query (same discipline as CopyR32FTexture2D). - auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] - [static_cast(TextureTarget::Texture2D)]; - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0); - if (storageError != GL_NO_ERROR) { - g_GLESFuncs.glDeleteTextures(1, &texture); - MGLOG_D("GetTexImage: no %dx%d GL_R32UI scratch image (%s); the verbatim word readback is unavailable", - newWidth, newHeight, MG_Util::ConvertGLEnumToString(storageError).c_str()); - return 0; - } - g_packedWordScratchTextureId = texture; - g_packedWordScratchWidth = newWidth; - g_packedWordScratchHeight = newHeight; - return texture; - } - - static void ReleasePackedWordScratchTexture() { - // The ES context (and the name with it) is gone; deleting here would target a recycled - // name in the successor context. - g_packedWordScratchTextureId = 0; - g_packedWordScratchWidth = 0; - g_packedWordScratchHeight = 0; - } - - // One slice of `backendTarget`'s level, as width*height stored 32-bit words in `outWords`. - static Bool ReadPackedLevelWordsViaScratch(GLuint texture, GLenum backendTarget, GLint level, GLint slice, - GLsizei width, GLsizei height, Uint32* outWords) { - if (texture == 0 || outWords == nullptr || width <= 0 || height <= 0 || level < 0 || slice < 0) return false; - if (!g_GLESFuncs.glCopyImageSubData) return false; - - // Horizontal bands, so neither the scratch image nor the staging buffer scales with the - // level. The scratch is grow-only on purpose - a sweep down a mip chain must not - // reallocate per level - which without a band cap would leave a 4096x4096 readback's - // 64 MiB image parked for the rest of the process. The cap is 1 MiB of GL_R32UI, with - // 4 MiB of staging behind it because the read lands four words per texel. - constexpr SizeT kMaxScratchTexels = SizeT{1} << 18; - const GLsizei bandRows = std::max( - 1, static_cast(std::min(kMaxScratchTexels / static_cast(width), - static_cast(height)))); - const GLuint scratch = EnsurePackedWordScratchTexture(width, bandRows); - if (scratch == 0) return false; - - ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false); - auto& scratchFB = ScratchFBOImpl::BlitReadFramebuffer(); - FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, ScratchFBOImpl::EnsureId(scratchFB)); - ScratchFBOImpl::EnsureColorAttachment2D(scratchFB, GL_READ_FRAMEBUFFER, scratch, GL_TEXTURE_2D, 0); - ScratchFBOImpl::EnsureReadBuffer(scratchFB, GL_COLOR_ATTACHMENT0); - if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_D("GetTexImage: the GL_R32UI scratch attachment is incomplete; falling back"); - return false; - } - - // GL_RGBA_INTEGER/GL_UNSIGNED_INT is the one combination ES guarantees for an integer - // colour buffer, so the read lands four words per texel and the red one is compacted out - // here. The PACK scope is the tight default rather than the application's, so a row comes - // back packed at exactly `width * 4` words. One glGetError covers the whole loop: it - // accumulates, and a failure anywhere means the caller falls back rather than trusting a - // partial result. - const SizeT wordsPerRow = static_cast(width) * 4; - Vector staging(static_cast(bandRows) * wordsPerRow); - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); - ClearGLErrors(); - for (GLsizei y = 0; y < height; y += bandRows) { - const GLsizei rows = std::min(bandRows, height - y); - g_GLESFuncs.glCopyImageSubData(texture, backendTarget, level, 0, y, slice, scratch, GL_TEXTURE_2D, 0, 0, - 0, 0, width, rows, 1); - g_GLESFuncs.glReadPixels(0, 0, width, rows, GL_RGBA_INTEGER, GL_UNSIGNED_INT, staging.data()); - for (GLsizei row = 0; row < rows; ++row) { - const Uint32* srcRow = staging.data() + static_cast(row) * wordsPerRow; - Uint32* dstRow = outWords + static_cast(y + row) * static_cast(width); - for (GLsizei x = 0; x < width; ++x) dstRow[x] = srcRow[static_cast(x) * 4]; - } - } - const GLenum error = g_GLESFuncs.glGetError(); - if (error != GL_NO_ERROR) { - MGLOG_D("GetTexImage: the GL_R32UI word readback of %s was refused (%s); falling back", - MG_Util::ConvertGLEnumToString(backendTarget).c_str(), - MG_Util::ConvertGLEnumToString(error).c_str()); - return false; - } - return true; - } - -#include "WireTextureReadback.inc" - - static Bool IsLegacyNativeReadPixelsFormat(GLenum format) { - return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER || - format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL; - } - - static Bool IsLegacyNativeReadPixelsType(GLenum type) { - // GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV are only ever valid - // paired with GL_DEPTH_STENCIL (packed_depth_stencil.verify_read_pixels); the real - // driver already implements this readback natively. - return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV || - type == GL_INT || type == GL_FLOAT || type == GL_UNSIGNED_INT_24_8 || - type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; - } - - // The (format, type) pairs the depth/stencil helpers below can service. They are not - // covered by the colour tables above - GetReadbackChannelMapping has no entry for any - // depth or stencil format, so without this gate a read the helpers CAN serve (a - // GL_UNSIGNED_SHORT depth, a GL_SHORT stencil) is turned away before it reaches them. - // The signed widths and GL_HALF_FLOAT belong in it for the same reason: the helpers scale a - // depth into 127 / 32767 / 2147483647 for the signed ones and hand GL_HALF_FLOAT the value - // as a half word, so a pair listed here is one they answer in full. - static Bool IsSupportedDepthStencilReadPixelsPair(GLenum format, GLenum type) { - switch (format) { - case GL_DEPTH_COMPONENT: - return type == GL_BYTE || type == GL_UNSIGNED_BYTE || type == GL_SHORT || - type == GL_UNSIGNED_SHORT || type == GL_INT || type == GL_UNSIGNED_INT || - type == GL_FLOAT || type == GL_HALF_FLOAT; - case GL_STENCIL_INDEX: - return type == GL_UNSIGNED_BYTE || type == GL_BYTE || type == GL_UNSIGNED_SHORT || type == GL_SHORT || - type == GL_UNSIGNED_INT || type == GL_INT || type == GL_FLOAT || type == GL_HALF_FLOAT; - case GL_DEPTH_STENCIL: - return type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; - default: - return false; - } - } - - void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { - MGLOG_D("ReadPixels: x=%d y=%d w=%d h=%d format=%s type=%s pixels=%p", x, y, width, height, - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels); -#if MOBILEGL_BUILD_DISAGGREGATED - // A reset context reads back nothing it rendered: the session is latched and the read declined. - if (LatchIfDeviceLost("read-pixels")) return; -#endif - - // Combinations the ES driver has always handled directly keep the native path; other color layouts go - // through the wide-format conversion path. Anything still uncovered degrades to a logged no-op instead - // of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer. - const Bool useNativeReadback = (IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type)) || - IsSupportedDepthStencilReadPixelsPair(format, type); - ReadbackChannelMapping conversionMapping{}; - const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && - GetReadbackDstPixelSize(conversionMapping, type) != 0; - if (!useNativeReadback && !convertible) { - MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); - return; - } - - MGLOG_D("ReadPixels: SyncNeccessaryTextures()"); - TextureImpl::SyncNeccessaryTextures(); - - MGLOG_D("ReadPixels: SyncCurrentFBO()"); - FramebufferImpl::SyncCurrentFBO(); - - MGLOG_D("ReadPixels: BindCurrentFBO(Read)"); - BindCurrentFBO(FramebufferTarget::Read); - - MGLOG_D("ReadPixels: Applying the PACK pixel-store scope"); - ScopedPackState packParamsScope(PackStateFromContext()); - - GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); - MGLOG_D("ReadPixels: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str()); - - if (fbStatus != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E_ONCE("ReadPixels: bound READ FBO is not complete"); - return; - } - // ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the - // matching attachment class; every other convertible color layout (including GL_RGBA/GL_FLOAT - // and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type - // the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so - // it always takes the conversion path (which swaps on the CPU). - const Bool packSwapBytes = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false).SwapBytes; - // The read buffer is what glReadPixels reads, so the frontend's READ binding is exactly - // the right thing to ask here. - const Bool forceOpaqueAlpha = FramebufferImpl::IsAlphaWidenedFallbackReadAttachment(); - // An attachment widened from three channels to stay colour-renderable also has to leave - // the fast pair: only the conversion path knows to answer its alpha with the 1.0 the - // application's format implies instead of whatever the draw wrote into the added channel. - const Bool nativeFastPair = !packSwapBytes && !forceOpaqueAlpha && - ((format == GL_RGBA && type == GL_UNSIGNED_BYTE) || - (format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT))); - if (convertible && !nativeFastPair) { - if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels, - /*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true, - forceOpaqueAlpha)) { - MGLOG_D("ReadPixels: finished via client-format conversion"); - return; - } - MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); - return; - } - // Every depth and stencil read goes through the helpers, not just the widening ones: - // ES has no guaranteed readback for either aspect, so even a byte-for-byte case - // needs the fallback chain (the other native spelling, then the sampling emulation) - // on a driver without GL_NV_read_depth / GL_NV_read_stencil. - if (format == GL_DEPTH_COMPONENT && ReadPixelsDepthComponent(x, y, width, height, type, pixels)) { - MGLOG_D("ReadPixels: finished via depth readback helper"); - return; - } - if (format == GL_STENCIL_INDEX && ReadPixelsStencilViaNative(x, y, width, height, type, pixels)) { - MGLOG_D("ReadPixels: finished via stencil readback helper"); - return; - } - if (format == GL_DEPTH_STENCIL && ReadPixelsDepthStencilPacked(x, y, width, height, type, pixels)) { - MGLOG_D("ReadPixels: finished via packed depth/stencil readback helper"); - return; - } - - // Handle PBO. The pack binding is scoped: it returns to the resting 0 state - // on every exit path, so a later readback can never land in a stale PBO - // (the driver-level binding used to stay on the user PBO after this call, - // capturing subsequent client-memory readbacks into it). - auto& pixelPackBufferObject = - SplitReadbackPackBuffer(); - Bool usePBO = false; - GLuint packBufferId = 0; - // P13: SplitReadbackPackBuffer() is always null - a pack buffer reaches the readback - // by handle on the verb path - so this entry never packs into a buffer. - MGLOG_D("ReadPixels: Not using PBO"); - ScopedPixelPackBuffer packBufferBinding(packBufferId); - - MGLOG_D("ReadPixels: glReadPixels()"); - DrainESErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, format, type, pixels); - const GLenum nativeReadError = g_GLESFuncs.glGetError(); - if (nativeReadError != GL_NO_ERROR) { - // ES drivers only guarantee GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/(U)INT, float RGBA and one - // implementation-defined pair; legacy combos like GL_RED/GL_UNSIGNED_INT are rejected by e.g. - // Adreno with a GL error and an untouched destination (GL CTS packed_pixels r8_format_red). The - // failed read wrote nothing (client memory and PBO alike), so re-service the request through the - // wide-format conversion path before any PBO writeback can capture stale contents. The conversion - // helper saves/restores the ES pixel-pack binding and handles the state-layer PBO itself. - DrainESErrors(); - MGLOG_D("ReadPixels: native read of %s/%s failed (%s), retrying via client-format conversion", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), - MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); - if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels, - /*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true, - forceOpaqueAlpha)) { - MGLOG_D("ReadPixels: finished via client-format conversion after native failure"); - return; - } - MGLOG_E_ONCE("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, " - "skipping readback", - MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), - MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); - return; - } - if (usePBO) { - // pull back to client memory if PBO is used - MGLOG_D("ReadPixels: PBO used, mapping buffer to client memory"); - GLvoid* pboMappedPtr = g_GLESFuncs.glMapBufferRange( - GL_PIXEL_PACK_BUFFER, 0, (GLsizeiptr)pixelPackBufferObject->GetSize(), GL_MAP_READ_BIT); - if (pboMappedPtr) { - MGLOG_D("ReadPixels: Copying data from PBO to client memory"); - SizeT size = pixelPackBufferObject->GetSize(); - pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0); - // Serial bumped with no backend op; re-open the buffer draw-clean memos. - BufferImpl::BumpBufferMutationEpoch(); - MGLOG_D("ReadPixels: Unmapping PBO"); - g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER); - } else { - MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr"); - } - } - MGLOG_D("ReadPixels: finished"); - } - - void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs) { - g_EGLFuncs = eglFuncs; - } - - void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs) { - g_GLESFuncs = glesFuncs; - } - - void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities) { - g_GLESCapabilities = capabilities; - } - - // --------------------------------------------------------------------------------- - // P14 S4 (docs/Disaggregated/design/11-state-ownership.md): the native tuple is per context. - // - // THE DISPLAY STAYS ONE OBJECT. eglInitialize is once per process and eglTerminate runs when the - // last native context has gone, so a second context - or a second session - never terminates the - // display the first one is still using. - // - // EVERYTHING ELSE THAT WAS A FILE-LEVEL SINGLETON IS KEYED. A native EGLContext, the config it - // was created with and the native surfaces are keyed by (session, client context token): the - // identity S1 put on the wire (bind_context / SurfaceOp.context) and S2 gave a per-thread owner. - // The key is resolved on the calling thread through a probe the server layer installs - // (SetNativeContextKeyResolver); with none installed the answer is the single key {0, 0}, which - // is byte-for-byte the process singleton this replaces - monolith, a frontend-only process and - // every unit case keep exactly one tuple. - // - // WHY THE SURFACES ARE KEYED BY SESSION AND NOT BY CONTEXT: an EGLSurface is not owned by an - // EGLContext (EGL has no such relationship - only the draw/read BINDING is per context), so a - // session's surfaces are one set and each context records which of them it currently draws to. - // That is also what makes pbuffer -> window -> pbuffer keep its objects: creating the second - // surface does not touch the context, so the twins the first surface's draws built are still - // there when the third one is bound. - - static EGLDisplay g_Display = EGL_NO_DISPLAY; - // eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0. - static Uint32 g_displayUsers = 0; - // How many times the display has been terminated. eglTerminate destroys every EGLImage made on - // it, and a re-initialized display hands back the same EGLDisplay handle, so a holder destroys - // its EGLImage itself only while this has not moved since the image was made. - static std::atomic g_displayTerminations{0}; -#if MOBILEGL_BUILD_DISAGGREGATED - namespace SharedImageImpl { - // The display (and every EGLImage and renderbuffer made on it) is gone: forget the present - // targets without calling into the driver, releasing the images they held. - void ForgetPresentTargetsAfterTerminate(); - } // namespace SharedImageImpl -#endif - static void NoteDisplayTerminated() { - g_displayTerminations.fetch_add(1, std::memory_order_acq_rel); -#if MOBILEGL_BUILD_DISAGGREGATED - SharedImageImpl::ForgetPresentTargetsAfterTerminate(); -#endif - } - - struct NativeContextTuple { - Uint64 SessionKey = 0; - Uint64 ContextToken = 0; - Uint64 ShareGroupToken = 0; - EGLContext Context = EGL_NO_CONTEXT; - // Context's lifetime serial (CurrentNativeContextSerial); a new one per eglCreateContext. - Uint64 Serial = 0; - EGLConfig Config = nullptr; - // The session surface this context draws to / reads from, as last made current for it. - EGLSurface Draw = EGL_NO_SURFACE; - EGLSurface Read = EGL_NO_SURFACE; - }; - - struct NativeSessionState { - Uint64 SessionKey = 0; - // Every native surface this session created, so its teardown destroys its own and only its own. - // The window is recorded with each entry: a stale surface still bound to a window that a - // LATER session wants (a crashed predecessor's server-owned window) is found and destroyed - // by InitWindowSurface before the driver refuses the second eglCreateWindowSurface. - struct SurfaceEntry { - NativeWindowType Window; - EGLSurface Surface; - // Presents made on this native surface since it was created: a pbuffer's content is - // the last presented frame once there has been one (its buffer age is then 1). - Uint64 Presents = 0; - }; - Vector Surfaces; - // The surface the session currently draws to / reads from - what a bind to another context - // re-makes current, and what Present/eglSwapBuffers names. - EGLSurface Draw = EGL_NO_SURFACE; - EGLSurface Read = EGL_NO_SURFACE; - }; - - static std::mutex g_nativeRegistryMutex; - // Bumped on every registry mutation. The per-thread active-tuple cache below is keyed on it, so - // a destroyed tuple can never be handed out from a stale cache entry. - static std::atomic g_nativeRegistryEpoch{1}; - // P15: moves whenever the size QueryCurrentSurfaceSize would answer may have moved - after every - // swap (a window surface takes a new size at its next swap) and at every change of a session's - // draw surface (NoteDrawSurfaceMoved at each `session.Draw =`). Surface creation and destruction - // also move g_nativeRegistryEpoch, which the size memo keys on as well. - static std::atomic g_surfaceSizeEpoch{1}; - static void NoteDrawSurfaceMoved() { g_surfaceSizeEpoch.fetch_add(1, std::memory_order_acq_rel); } - // Never erased while the process lives: a context teardown marks its tuple dead (Context = - // EGL_NO_CONTEXT) instead of freeing it, so a cached pointer is never dangling between the - // epoch bump and the next resolution. - static Vector& NativeContexts() { - static Vector* contexts = new Vector(); - return *contexts; - } - static Vector& NativeSessions() { - static Vector* sessions = new Vector(); - return *sessions; - } - - static std::atomic g_nativeContextKeyResolver{nullptr}; - - // A one-way override for a case that drives the registry with no session runtime installed: - // when `t_native.forcedValid` is set, the key is the forced pair instead of the probe's - // answer. The setter clears the active-tuple cache, which is keyed on the effective key. - // - // P14: ONE thread-local block for this file's per-thread native-context state rather than ten - // separate thread_locals. The library is built with emulated TLS (minSdk 26), where every - // thread_local access is a call; the per-draw "is my context current" check read seven of - // them. One block is one lookup per function. - struct NativeThreadState { - Bool forcedValid = false; - Uint64 forcedSession = 0; - Uint64 forcedToken = 0; - NativeContextTuple* activeTuple = nullptr; - Uint64 activeEpoch = 0; - Uint64 activeSession = 0; - Uint64 activeToken = 0; - EGLContext boundContext = EGL_NO_CONTEXT; - EGLSurface boundDraw = EGL_NO_SURFACE; - // QueryCurrentSurfaceSize's memo (P15): the last answer, good while the session, the - // registry epoch and the surface-size epoch all hold. - Bool sizeMemoValid = false; - Uint64 sizeMemoSession = 0, sizeMemoToken = 0, sizeMemoRegistryEpoch = 0, sizeMemoSizeEpoch = 0; - Int sizeMemoWidth = 0, sizeMemoHeight = 0; - }; - thread_local NativeThreadState t_native; - - void SetNativeContextKeyResolver(NativeContextKeyResolver resolver) { - g_nativeContextKeyResolver.store(resolver, std::memory_order_release); - } - - static void ResolveNativeKey(Uint64& outSession, Uint64& outToken) { - if (t_native.forcedValid) { - outSession = t_native.forcedSession; - outToken = t_native.forcedToken; - return; - } - outSession = 0; - outToken = 0; - const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire); - if (resolver != nullptr) (void)resolver(&outSession, &outToken); - } - - // P14 S6: the twin tables' key. Same probe/fallback shape as the native tuple's above and - // for the same reason (the whole single-context world must keep getting one key), and - // deliberately a SEPARATE resolver: this one answers for a table that is consulted on paths - // the native tuple is not, and a case that drives twins without EGL must be able to set it - // alone. See DirectGLES.h for what the two halves mean. - static std::atomic g_twinKeyResolver{nullptr}; - - void SetTwinKeyResolver(TwinKeyResolver resolver) { - g_twinKeyResolver.store(resolver, std::memory_order_release); - } - - Bool CurrentTwinKey(TwinKey* outKey) { - outKey->SessionKey = 0; - outKey->ShareGroupKey = 0; - const TwinKeyResolver resolver = g_twinKeyResolver.load(std::memory_order_acquire); - if (resolver == nullptr) return false; - return resolver(outKey); - } - - Uint64 CurrentNativeSessionKey() { - Uint64 session = 0; - Uint64 token = 0; - ResolveNativeKey(session, token); - return session; - } - - namespace { - // The active tuple for this thread. The fast path is a key compare against a thread-local - // cache, because IsBackendContextCurrentOnThisThread sits under every draw; the slow path - // (a first call, a bind_context switch, or any registry mutation) walks the registry, which - // holds one tuple per live client context. - // (t_native.activeTuple / activeEpoch / activeSession / activeToken) - // The native context THIS THREAD last made current. An EGLContext can be current on only one - // thread, and MakeCurrent is the only writer, so a plain thread-local is exact - and it is - // what makes MakeNativeContextCurrentForBoundToken a no-op when the bind did not actually - // change the native context (the single-context world's every bind). - // (t_native.boundContext) - // And the surface it was made current with: a bind to the same context on a different - // surface (one client context drawing to several windows) is a real switch. - // (t_native.boundDraw) - // ...and that context's lifetime serial, the key of its container objects. - // (t_nativeContextThread.BoundSerial, DirectGLES.h) - std::atomic g_nativeContextSerials{0}; - - std::mutex& PerNativeContextRegistryMutex() { - static auto* mutex = new std::mutex(); - return *mutex; - } - Vector& PerNativeContextRegistry() { - static auto* registry = new Vector(); - return *registry; - } - } // namespace - - - namespace { - std::atomic g_perNativeContextEpoch{1}; - } - Uint64 PerNativeContextEpoch() { return g_perNativeContextEpoch.load(std::memory_order_acquire); } - void BumpPerNativeContextEpoch() { g_perNativeContextEpoch.fetch_add(1, std::memory_order_acq_rel); } - - namespace { - std::atomic g_perNativeContextCacheSlots{0}; - } - - PerNativeContextBase::PerNativeContextBase() - : m_cacheSlot(g_perNativeContextCacheSlots.fetch_add(1, std::memory_order_relaxed) % - NativeContextThreadState::kCacheEntries) { - const std::lock_guard lock(PerNativeContextRegistryMutex()); - PerNativeContextRegistry().push_back(this); - } - - PerNativeContextBase::~PerNativeContextBase() { - BumpPerNativeContextEpoch(); - const std::lock_guard lock(PerNativeContextRegistryMutex()); - auto& registry = PerNativeContextRegistry(); - registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end()); - } - - void ForgetNativeContextObjects(Uint64 serial) { - if (serial == 0) return; - const std::lock_guard lock(PerNativeContextRegistryMutex()); - for (PerNativeContextBase* holder : PerNativeContextRegistry()) holder->ForgetContext(serial); - } - - static NativeSessionState& AcquireNativeSession(Uint64 sessionKey) { - for (auto* session : NativeSessions()) { - if (session->SessionKey == sessionKey) return *session; - } - auto* created = new NativeSessionState{}; - created->SessionKey = sessionKey; - NativeSessions().push_back(created); - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - return *created; - } - - static NativeSessionState& ActiveNativeSession() { - Uint64 session = 0; - Uint64 token = 0; - ResolveNativeKey(session, token); - const std::lock_guard lock(g_nativeRegistryMutex); - return AcquireNativeSession(session); - } - - // The tuple the calling thread works on, created on first use. Lazy creation is what keeps the - // pre-S4 shapes working unchanged: a session that never crossed a CreateContext - the whole - // single-context world, and every existing fixture - resolves to its token-0 tuple and gets - // exactly the one native context it always had. - static NativeContextTuple& ActiveNativeContext() { - NativeThreadState& tls = t_native; - Uint64 session = 0; - Uint64 token = 0; - if (tls.forcedValid) { - session = tls.forcedSession; - token = tls.forcedToken; - } else { - const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire); - if (resolver != nullptr) (void)resolver(&session, &token); - } - const Uint64 epoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); - if (tls.activeTuple != nullptr && tls.activeEpoch == epoch && tls.activeSession == session && - tls.activeToken == token) { - return *tls.activeTuple; - } - const std::lock_guard lock(g_nativeRegistryMutex); - NativeContextTuple* found = nullptr; - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey == session && tuple->ContextToken == token) { - found = tuple; - break; - } - } - if (found == nullptr) { - found = new NativeContextTuple{}; - found->SessionKey = session; - found->ContextToken = token; - NativeContexts().push_back(found); - (void)AcquireNativeSession(session); - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - t_native.activeTuple = found; - t_native.activeEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); - t_native.activeSession = session; - t_native.activeToken = token; - return *found; - } - - static NativeContextTuple* FindNativeContext(Uint64 sessionKey, Uint64 contextToken) { - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey == sessionKey && tuple->ContextToken == contextToken) return tuple; - } - return nullptr; - } - - Uint64 NativeContextCount() { - const std::lock_guard lock(g_nativeRegistryMutex); - Uint64 count = 0; - for (auto* tuple : NativeContexts()) { - if (tuple->Context != EGL_NO_CONTEXT) ++count; - } - return count; - } - - Uint64 NativeContextCountForSession(Uint64 sessionKey) { - const std::lock_guard lock(g_nativeRegistryMutex); - Uint64 count = 0; - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey == sessionKey && tuple->Context != EGL_NO_CONTEXT) ++count; - } - return count; - } - - EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken) { - const std::lock_guard lock(g_nativeRegistryMutex); - const NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken); - return tuple != nullptr ? tuple->Context : EGL_NO_CONTEXT; - } - - void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken) { - t_native.forcedSession = sessionKey; - t_native.forcedToken = contextToken; - t_native.forcedValid = true; - // The cache is keyed on the effective key, so drop it rather than let a stale entry answer. - t_native.activeTuple = nullptr; - t_native.activeEpoch = 0; - } - - // The display's one-time initialization. Returns false only when there is no EGL to talk to. - static Bool EnsureDisplayInitialized() { - if (g_Display != EGL_NO_DISPLAY) return true; - if (!g_EGLFuncs.eglGetDisplay) return false; - g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY); - if (g_Display == EGL_NO_DISPLAY) return false; - if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) { - g_Display = EGL_NO_DISPLAY; - return false; - } - if (g_EGLFuncs.eglBindAPI) g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API); - return true; - } - -#if MOBILEGL_BUILD_DISAGGREGATED - // P12 (on-screen server window), D3: true only while InitWindowSurface publishes the default - // framebuffer's shape, so that publish - and no pbuffer's - may carry the surface's extent. - // Split builds only: the pull build's publish is unchanged (G1). - static Bool g_publishWindowExtent = false; -#endif - - static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) { - outWidth = 0; - outHeight = 0; - // P15: ANSWERED FROM A MEMO while nothing that moves the size moved. Two eglQuerySurface - // calls behind the registry lock on every dirty render-state sync (draws and clears whose - // scissor box the app never wrote) cost 0.065 ms/frame on FCL Espryt. - Uint64 memoSession = 0, memoToken = 0; - ResolveNativeKey(memoSession, memoToken); - const Uint64 registryEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); - const Uint64 sizeEpoch = g_surfaceSizeEpoch.load(std::memory_order_acquire); - NativeThreadState& memo = t_native; - if (memo.sizeMemoValid && memo.sizeMemoSession == memoSession && memo.sizeMemoToken == memoToken && - memo.sizeMemoRegistryEpoch == registryEpoch && memo.sizeMemoSizeEpoch == sizeEpoch) { - outWidth = memo.sizeMemoWidth; - outHeight = memo.sizeMemoHeight; - return true; - } - // P14 S4: the surface is the SESSION's, not the process's - the tuple that draws to it is - // one of the session's contexts. - const EGLSurface surface = ActiveNativeSession().Draw; - if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) { - return false; - } - - EGLint width = 0; - EGLint height = 0; - if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &width) || - !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height) || - width <= 0 || height <= 0) { - return false; - } - - outWidth = static_cast(width); - outHeight = static_cast(height); - memo.sizeMemoValid = true; - memo.sizeMemoSession = memoSession; - memo.sizeMemoToken = memoToken; - memo.sizeMemoRegistryEpoch = registryEpoch; - memo.sizeMemoSizeEpoch = sizeEpoch; - memo.sizeMemoWidth = outWidth; - memo.sizeMemoHeight = outHeight; - return true; - } - - // The frontend's default framebuffer is a placeholder FramebufferObject whose attachments - // carry a format and nothing else (MG_Impl/Init.cpp), and it is built before any surface - // exists - so it starts on a guess, GL_DEPTH32F_STENCIL8. Every attachment query about the - // default framebuffer is answered out of that guess, and being wrong is not cosmetic: GL - // blits depth/stencil only between IDENTICAL formats, so a caller that reads - // GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was told about and blits - // gets GL_INVALID_OPERATION and a silently dropped blit - colour bits included, because a - // rejected glBlitFramebuffer transfers nothing at all. DirectVulkan already publishes its - // real format when it creates the swapchain (SwapchainObject::Create); this is the - // DirectGLES half, and here the answer can simply be asked of the ES default framebuffer. - // - // Only the format is published. The placeholder's 512x512 extent is left alone: all three - // attachments share it, and FramebufferObject::CheckCompleteness requires them to agree, so - // resizing depth/stencil without colour would report the default framebuffer incomplete. - static void PublishDefaultFramebufferDepthStencilFormat() { - auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo; - // P8-SE: THE PLACEHOLDER IS ONLY THE MONOLITH ARM'S TARGET. With a transport the answer is - // posted (OnSurfaceChanged, below) and the client writes its own placeholder; a spawn or tcp - // server process has no frontend at all, so pDefaultFramebufferInfo is null there. Gating on - // it returned before the post, the client kept Init.cpp's GL_DEPTH32F_STENCIL8 guess for a - // D24S8 pbuffer, and a blit into a buffer allocated from that guess lost depth and stencil - // (docs/archive/notes/p8/SE.md). Inproc only passed because client and server share the process. - if (!g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return; - if (!defaultFBOInfo && MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == nullptr) return; - - // GL_DEPTH / GL_STENCIL are the default framebuffer's spellings; a user framebuffer - // would need GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT and answers GL_INVALID_ENUM - // for these. Nothing else can be bound this early, but bind explicitly so the answer - // describes the default framebuffer even if this is ever called later. - GLint previousDrawFramebuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFramebuffer); - if (previousDrawFramebuffer != 0) { - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - } - - ClearGLErrors(); - GLint depthBits = 0; - GLint stencilBits = 0; - GLint depthComponentType = GL_UNSIGNED_NORMALIZED; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH, - GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL, - GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH, - GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, - &depthComponentType); - ClearGLErrors(); - - if (previousDrawFramebuffer != 0) { - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDrawFramebuffer)); - } - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - - // A driver that refuses the query leaves both at 0. Fall back to what the EGL config - // was chosen with, which is what the surface actually has. P14 S4: the config belongs to the - // context the surface is bound to, so it is the active tuple's. - const EGLConfig activeConfig = ActiveNativeContext().Config; - if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY && - activeConfig != nullptr) { - EGLint eglDepth = 0; - EGLint eglStencil = 0; - if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_DEPTH_SIZE, &eglDepth)) { - depthBits = static_cast(eglDepth); - } - if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_STENCIL_SIZE, &eglStencil)) { - stencilBits = static_cast(eglStencil); - } - } - - if (depthBits <= 0 && stencilBits <= 0) { - // Nothing usable came back from either witness. Returning here leaves whatever a - // PREVIOUS surface published in place, which would be stale - this function runs - // once per surface activation, not once per process. It is written this way anyway - // because the branch is unreachable for a surface MobileGL chose itself: - // ChooseConfigForSurface asks eglChooseConfig for EGL_DEPTH_SIZE 24 and - // EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so the context's config always - // has both and the EGL fallback above always answers. A caller that supplies its own - // depth-less config would keep the previous surface's description; publishing a - // guess instead would be a different lie, and the placeholder attachment model has - // no way to say "this buffer does not exist" short of detaching it. - MGLOG_D("DirectGLES: default framebuffer reports no depth or stencil; leaving the " - "placeholder attachment formats untouched"); - return; - } - - const Bool floatDepth = depthComponentType == GL_FLOAT; - TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8; - TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8; - if (depthBits > 0 && stencilBits > 0) { - // Packed: both frontend attachments name the same combined format, as DirectVulkan does. - depthFormat = (floatDepth || depthBits > 24) ? TextureInternalFormat::Depth32FStencil8 - : TextureInternalFormat::Depth24Stencil8; - stencilFormat = depthFormat; - } else if (depthBits > 0) { - depthFormat = floatDepth ? TextureInternalFormat::DepthComponent32F - : (depthBits <= 16) ? TextureInternalFormat::DepthComponent16 - : TextureInternalFormat::DepthComponent24; - stencilFormat = depthFormat; - } else { - depthFormat = TextureInternalFormat::StencilIndex8; - stencilFormat = TextureInternalFormat::StencilIndex8; - } - - // Null in a spawn / tcp server (above); only the monolith arm below writes through them. - auto* depthTexture = defaultFBOInfo ? defaultFBOInfo->depthAttachment.get() : nullptr; - auto* stencilTexture = defaultFBOInfo ? defaultFBOInfo->stencilAttachment.get() : nullptr; - if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) { - // P5c ev (CONTRACT-P5C §4.2): with an active transport the attachments are CLIENT - // memory and this thread may not write them - the backend fills MGPSurfaceInfo - // and posts, and the client applies it to its own pDefaultFramebufferInfo on the - // GL thread. FORMAT ONLY, exactly as the monolith arm below: Width/Height stay 0, - // which is the consumer's cue to leave the placeholder extent untouched. The - // callback's presence IS the transport probe - the server session installs it at - // Accept, and under monolith nobody ever does. - MG_Pipe::MGPSurfaceInfo info{}; - info.InternalFormat = static_cast(depthFormat); - info.IsDefault = 1; -#if MOBILEGL_BUILD_DISAGGREGATED - // P12 (on-screen server window), D3: A WINDOW SURFACE OF A REMOTE SESSION ALSO SAYS HOW - // BIG IT IS. Under spawn / tcp (this server process set Transport=Spawn in RunSession) - // the only window surface is the server's own - a headless client's ServerOwned one - - // and that client learns its extent here and from the reply, as Magma's swapchain - // already tells it. An extent switches the client's consumer to the reallocating arm - // (ClientSession.cpp ApplySurfaceChangedToClient), so it is deliberately NOT published - // for a pbuffer, nor under inproc / monolith: those keep the format-only shape above. - if (g_publishWindowExtent && MG_Config::Transport == MG_Config::TransportMode::Spawn) { - // THE DISPLAY'S EXTENT, NEVER THE SURFACE'S: eglQuerySurface answers the buffer last - // dequeued, which after a resize is still the old size - publishing it told the client a - // size the window no longer had. - Uint32 extentWidth = 0; - Uint32 extentHeight = 0; - if (MG_Pipe::MGPipeServerOwnedWindowExtent(&extentWidth, &extentHeight)) { - info.Width = extentWidth; - info.Height = extentHeight; - } - } -#endif - MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info); - } else - { - if (depthTexture) depthTexture->SetInternalFormat(depthFormat); - if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat); - } - MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment " - "formats depth=%d stencil=%d", - depthBits, stencilBits, floatDepth ? 1 : 0, static_cast(depthFormat), - static_cast(stencilFormat)); - } - -#if defined(__linux__) && !defined(__ANDROID__) - static void* OpenX11Lib() { - void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW); - if (!x11Lib) { - x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW); - } - return x11Lib; - } -#endif - - static EGLint QueryDefaultX11VisualId() { -#if defined(__linux__) && !defined(__ANDROID__) - const char* displayName = std::getenv("DISPLAY"); - if (!displayName || !MG_Util::X11::MayDialX11Display(displayName)) { - return 0; - } - - void* x11Lib = OpenX11Lib(); - if (!x11Lib) { - return 0; - } - - using XOpenDisplayFn = void* (*)(const char*); - using XDefaultScreenFn = int (*)(void*); - using XDefaultVisualFn = void* (*)(void*, int); - using XVisualIDFromVisualFn = unsigned long (*)(void*); - using XCloseDisplayFn = int (*)(void*); - - auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); - auto* xDefaultScreen = reinterpret_cast(dlsym(x11Lib, "XDefaultScreen")); - auto* xDefaultVisual = reinterpret_cast(dlsym(x11Lib, "XDefaultVisual")); - auto* xVisualIDFromVisual = reinterpret_cast(dlsym(x11Lib, "XVisualIDFromVisual")); - auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); - if (!xOpenDisplay || !xDefaultScreen || !xDefaultVisual || !xVisualIDFromVisual || !xCloseDisplay) { - dlclose(x11Lib); - return 0; - } - - void* display = xOpenDisplay(displayName); - if (!display) { - dlclose(x11Lib); - return 0; - } - const int screen = xDefaultScreen(display); - void* visual = xDefaultVisual(display, screen); - const auto visualId = visual ? static_cast(xVisualIDFromVisual(visual)) : 0; - xCloseDisplay(display); - dlclose(x11Lib); - return visualId; -#else - return 0; -#endif - } - - static EGLint QueryX11WindowVisualId(NativeWindowType window) { -#if defined(__linux__) && !defined(__ANDROID__) && __has_include() - if (!window) { - return 0; - } - const char* displayName = std::getenv("DISPLAY"); - if (!displayName || !MG_Util::X11::MayDialX11Display(displayName)) { - return 0; - } - - void* x11Lib = OpenX11Lib(); - if (!x11Lib) { - return 0; - } - - using XOpenDisplayFn = Display* (*)(const char*); - using XGetWindowAttributesFn = int (*)(Display*, Window, XWindowAttributes*); - using XVisualIDFromVisualFn = unsigned long (*)(Visual*); - using XCloseDisplayFn = int (*)(Display*); - - auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); - auto* xGetWindowAttributes = - reinterpret_cast(dlsym(x11Lib, "XGetWindowAttributes")); - auto* xVisualIDFromVisual = reinterpret_cast(dlsym(x11Lib, "XVisualIDFromVisual")); - auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); - if (!xOpenDisplay || !xGetWindowAttributes || !xVisualIDFromVisual || !xCloseDisplay) { - dlclose(x11Lib); - return 0; - } - - Display* display = xOpenDisplay(displayName); - if (!display) { - dlclose(x11Lib); - return 0; - } - - XWindowAttributes attrs{}; - EGLint visualId = 0; - if (xGetWindowAttributes(display, static_cast(window), &attrs) && attrs.visual) { - visualId = static_cast(xVisualIDFromVisual(attrs.visual)); - } - xCloseDisplay(display); - dlclose(x11Lib); - return visualId; -#else - (void)window; - return 0; -#endif - } - - static Bool GetConfigAttrib(EGLConfig config, EGLint attr, EGLint& value) { - return g_EGLFuncs.eglGetConfigAttrib && g_EGLFuncs.eglGetConfigAttrib(g_Display, config, attr, &value); - } - - static Bool ConfigSupports(EGLConfig config, EGLint surfaceBit) { - EGLint surfaceType = 0; - EGLint renderableType = 0; - if (!GetConfigAttrib(config, EGL_SURFACE_TYPE, surfaceType)) { - return false; - } - if (!GetConfigAttrib(config, EGL_RENDERABLE_TYPE, renderableType)) { - return false; - } - return (surfaceType & surfaceBit) && (renderableType & EGL_OPENGL_ES3_BIT); - } - - static Bool ChooseConfigForSurface(EGLint surfaceBit, EGLConfig& outConfig, - NativeWindowType window = static_cast(0)) { - const EGLint configAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, - EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, - EGL_NONE}; - - EGLint numConfigs = 0; - if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, nullptr, 0, &numConfigs) || numConfigs == 0) { - return false; - } - - Vector configs(static_cast(numConfigs)); - if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, configs.data(), numConfigs, &numConfigs) || - numConfigs == 0) { - return false; - } - configs.resize(static_cast(numConfigs)); - - if (surfaceBit == EGL_WINDOW_BIT) { - // X11 drivers can reserve every alpha-8 config for 32-bit ARGB - // visuals (NVIDIA), so a default-visual (depth 24) window only - // matches an alpha-0 config; keep those as a second candidate tier - // for the visual match or eglCreateWindowSurface hits BAD_CONFIG. - Vector alphaFreeConfigs; - const EGLint alphaFreeAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, - EGL_OPENGL_ES3_BIT, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, - 8, - EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, - 24, - EGL_STENCIL_SIZE, 8, EGL_NONE}; - EGLint numAlphaFree = 0; - if (g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, nullptr, 0, &numAlphaFree) && - numAlphaFree > 0) { - alphaFreeConfigs.resize(static_cast(numAlphaFree)); - if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(), - numAlphaFree, &numAlphaFree)) { - numAlphaFree = 0; - } - alphaFreeConfigs.resize(static_cast(numAlphaFree)); - } - - const EGLint windowVisualId = QueryX11WindowVisualId(window); - const EGLint visualIds[] = {windowVisualId, QueryDefaultX11VisualId()}; - for (const auto visualId : visualIds) { - if (visualId == 0) { - continue; - } - for (const auto* candidates : {&configs, &alphaFreeConfigs}) { - for (const auto config : *candidates) { - EGLint nativeVisualId = 0; - if (ConfigSupports(config, surfaceBit) && - GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) && - nativeVisualId == visualId) { - outConfig = config; - return true; - } - } - } - } - } - - for (const auto config : configs) { - if (ConfigSupports(config, surfaceBit)) { - outConfig = config; - return true; - } - } - - outConfig = configs.front(); - return true; - } - - // P14 S4. THE CONTEXT FOR `tuple`, AND THE SURFACE'S CONFIG, WITH NO TEARDOWN IN FRONT. - // - // The S4 deletion is the first statement this function used to begin with: `DestroyEGLContext()` - // ran UNCONDITIONALLY, so creating a second surface (let alone a second context) destroyed the - // native context the first one's objects lived in. A tuple that already has a native context - // keeps it and only (re)chooses the config the new surface is created with; a tuple that has - // none builds one, sharing with the group leader when CreateNativeContextFor named a group. - // - // A CONFIG THAT IS ALREADY SET IS NOT RE-CHOSEN FOR A DIFFERENT SURFACE KIND, and that is the - // deliberate trade: an EGLSurface's config must be compatible with the context's, so re-choosing - // would mean recreating the context - exactly the teardown this slice deletes. A driver that - // refuses the resulting eglCreate*Surface refuses it by name, which is the honest failure. - // P14 S4. THE BRING-UP DIAGNOSIS, in one place because BOTH surface paths owe it (it used to be - // the first statement of InitDisplayAndContext, which both went through). Extracted rather than - // duplicated when that function was replaced by the per-tuple EnsureNativeContext. - // - // Diagnose, and deliberately NOT resolve: resolving raises - // Fatal{PipeLegacyMemosDisabled}, and this function runs inside eglMakeCurrent, which - // the integration harness pre-flights in a FORKED CHILD - // (MG_IntegrationTest/Harness/HeadlessGL.cpp). A child that dies on a signal is reported - // to the parent as "no usable GPU/display/ICD" and every scenario in the lane is - // SKIPPED - so the stop became a green lane that ran nothing, on exactly the two env - // vars the D14/D18 A/B is driven with (ARCHITECTURE.md §13.2). The stop now belongs to the first - // twin lookup, which happens in a scenario body where a crash IS a test failure. - static void DiagnoseEsprytSlotArmAtBringUp() { - DiagnoseEsprytSlotArm(); - } - - // RESET NOTIFICATION, decided once per process by its first native context (EnsureNativeContext - // says why). MOBILEGL_ESPRYT_RESET_NOTIFICATION=0 keeps the driver's default. - namespace { - // EGL_EXT_create_context_robustness (eglext.h is not part of this file's include set). - constexpr EGLint kEglResetNotificationStrategyExt = 0x3138; - constexpr EGLint kEglLoseContextOnResetExt = 0x31BF; - enum class ResetNotification : Uint8 { Undecided, On, Off }; - std::atomic g_resetNotification{ResetNotification::Undecided}; - - Bool ResetNotificationWanted() { - const ResetNotification decided = g_resetNotification.load(std::memory_order_acquire); - if (decided != ResetNotification::Undecided) return decided == ResetNotification::On; - if (const char* value = std::getenv("MOBILEGL_ESPRYT_RESET_NOTIFICATION"); value != nullptr && value[0] == '0') - return false; - if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) return false; - const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); - if (extensions == nullptr) return false; - constexpr const char kWord[] = "EGL_EXT_create_context_robustness"; - for (const char* at = std::strstr(extensions, kWord); at != nullptr; at = std::strstr(at + 1, kWord)) { - const char after = at[sizeof(kWord) - 1]; - if ((at == extensions || at[-1] == ' ') && (after == '\0' || after == ' ')) return true; - } - return false; - } - - void NoteResetNotificationRefused() { - ResetNotification expected = ResetNotification::Undecided; - if (g_resetNotification.compare_exchange_strong(expected, ResetNotification::Off, std::memory_order_acq_rel)) - MGLOG_W("DirectGLES: the driver refused EGL_LOSE_CONTEXT_ON_RESET; a context that hangs the GPU is " - "not told, and its session cannot be ended for it"); - } - - void NoteResetNotificationDecided(Bool used) { - ResetNotification expected = ResetNotification::Undecided; - const ResetNotification decided = used ? ResetNotification::On : ResetNotification::Off; - if (g_resetNotification.compare_exchange_strong(expected, decided, std::memory_order_acq_rel)) - MGLOG_I("DirectGLES: native contexts %s reset notification (EGL_LOSE_CONTEXT_ON_RESET): a context " - "whose work hangs or faults the GPU %s", - used ? "ask for" : "do not ask for", - used ? "is named by the driver and ends its own session" : "is not told"); - } - } // namespace - - Bool NativeContextsNotifyResets() { - return g_resetNotification.load(std::memory_order_acquire) == ResetNotification::On; - } - - static Bool EnsureNativeContext(NativeContextTuple& tuple, EGLint surfaceBit, NativeWindowType window) { - if (!EnsureDisplayInitialized()) { - MGLOG_E("EnsureNativeContext: the EGL display did not initialize"); - return false; - } - if (tuple.Context != EGL_NO_CONTEXT) return true; - - if (tuple.Config == nullptr && !ChooseConfigForSurface(surfaceBit, tuple.Config, window)) { - MGLOG_E("EnsureNativeContext: no EGL config for surface bit 0x%x (window=%p)", - static_cast(surfaceBit), reinterpret_cast(window)); - return false; - } - - // A share group whose leader has a live native context hands it to eglCreateContext: this is - // the whole of "shared contexts see the same objects" on the server. The group is recorded - // by CreateNativeContextFor before this runs; a tuple nobody named a group for starts one. - EGLContext share = EGL_NO_CONTEXT; - for (auto* other : NativeContexts()) { - if (other != &tuple && other->SessionKey == tuple.SessionKey && - other->Context != EGL_NO_CONTEXT && tuple.ShareGroupToken != 0 && - other->ShareGroupToken == tuple.ShareGroupToken) { - share = other->Context; - break; - } - } - // P14: AND OTHERWISE ANY LIVE NATIVE CONTEXT IN THE PROCESS. The backend keeps buffers, - // textures and syncs of its own in process-wide state (the UBO/unpack/upload rings, the - // buffer pool, scratch textures, frame fences) and every session's apply thread uses - // them; they are only names in another session's context if all the contexts are one - // share group. Client isolation does not come from the driver's namespaces - the twin - // tables are bucketed per {session, share group} and never hand one bucket's names to - // another - so a process-wide group costs nothing in separation. - if (share == EGL_NO_CONTEXT) { - for (auto* other : NativeContexts()) { - if (other != &tuple && other->Context != EGL_NO_CONTEXT) { - share = other->Context; - break; - } - } - } - - // Negotiate the highest ES 3.x context. Version-strict EGL implementations - // (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3 - // request yields a 3.0 context that lacks the 3.1/3.2 texture targets the - // capability probes exercise; mobile drivers ignore the minor and hand out - // their maximum either way. - // - // RESET NOTIFICATION. With the driver's default strategy a context whose work hung the GPU - // is never told: after Adreno's ~2 s timeout reset it silently drops everything that context - // submits (the client's window just stops), and its client never learns it should recreate. - // Asking for EGL_LOSE_CONTEXT_ON_RESET lets the driver name the guilty context - // (glGetGraphicsResetStatus answers GUILTY, once) and LatchIfDeviceLost ends that session - // alone, so the client reads a lost context and can recover. (A ~10 s reset that a failed - // preemption ends was not reported to the server's contexts in measurement - see - // GpuHangWatch.h.) Every context of the process shares one group, and contexts that share - // must agree on the strategy, so it is decided once, by the first context, and kept. - for (const EGLint minorVersion : {2, 1, 0}) { - const Bool notify = ResetNotificationWanted(); - const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, - EGL_CONTEXT_MINOR_VERSION, minorVersion, - notify ? kEglResetNotificationStrategyExt : EGL_NONE, - kEglLoseContextOnResetExt, - EGL_NONE}; - tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs); - if (tuple.Context == EGL_NO_CONTEXT && notify && share == EGL_NO_CONTEXT) { - // The process's first context: if the same version WITHOUT the strategy is accepted, - // it was the strategy the driver refused, and no context of the process asks for it. - const EGLint plainAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, minorVersion, - EGL_NONE}; - tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, plainAttribs); - if (tuple.Context != EGL_NO_CONTEXT) NoteResetNotificationRefused(); - } - if (tuple.Context != EGL_NO_CONTEXT) { - NoteResetNotificationDecided(notify); - tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; - ++g_displayUsers; - return true; - } - } - - const Bool notify = ResetNotificationWanted(); - const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, - notify ? kEglResetNotificationStrategyExt : EGL_NONE, - kEglLoseContextOnResetExt, EGL_NONE}; - tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs); - if (tuple.Context == EGL_NO_CONTEXT) return false; - NoteResetNotificationDecided(notify); - tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; - ++g_displayUsers; - return true; - } - - Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken) { - if (contextToken == 0) return false; - const Uint64 sessionKey = CurrentNativeSessionKey(); - NativeContextTuple* tuple = nullptr; - { - const std::lock_guard lock(g_nativeRegistryMutex); - AcquireNativeSession(sessionKey); - for (auto* candidate : NativeContexts()) { - if (candidate->SessionKey == sessionKey && candidate->ContextToken == contextToken) { - tuple = candidate; - break; - } - } - if (tuple == nullptr) { - tuple = new NativeContextTuple{}; - tuple->SessionKey = sessionKey; - tuple->ContextToken = contextToken; - NativeContexts().push_back(tuple); - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - } - if (tuple->Context != EGL_NO_CONTEXT) return true; // idempotent - tuple->ShareGroupToken = shareGroupToken; - return EnsureNativeContext(*tuple, EGL_PBUFFER_BIT, static_cast(0)); - } - - void DestroyNativeContextFor(Uint64 contextToken) { - const Uint64 sessionKey = CurrentNativeSessionKey(); - if (contextToken == 0) return; - const std::lock_guard lock(g_nativeRegistryMutex); - NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken); - if (tuple == nullptr || tuple->Context == EGL_NO_CONTEXT) return; -#if MOBILEGL_BUILD_DISAGGREGATED - // Its last shared-image reads, fenced while it is still current. - if (t_native.boundContext == tuple->Context) SharedImageImpl::PublishPendingReads(); -#endif - if (g_EGLFuncs.eglDestroyContext && g_Display != EGL_NO_DISPLAY) { - // The context may be current on this very thread; unbinding first is what EGL requires - // (eglDestroyContext of a current context is EGL_BAD_ACCESS). - if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() == tuple->Context && - g_EGLFuncs.eglMakeCurrent) { - g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context); - } - // This thread no longer has it current either way; a context created later at the same - // address must not read as "already bound". - if (t_native.boundContext == tuple->Context) { - t_native.boundContext = EGL_NO_CONTEXT; - t_native.boundDraw = EGL_NO_SURFACE; - t_nativeContextThread.BoundSerial = 0; - } - ForgetNativeContextObjects(tuple->Serial); - tuple->Serial = 0; - tuple->Context = EGL_NO_CONTEXT; - tuple->Config = nullptr; - tuple->Draw = EGL_NO_SURFACE; - tuple->Read = EGL_NO_SURFACE; - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - if (g_displayUsers > 0) --g_displayUsers; - // NOT WHILE A SESSION STILL HOLDS SURFACES. eglTerminate destroys them with the display, and - // the session keeps naming them: the next context made current on one (an application that - // destroys its only context and creates another on the same surface, as eglinfo does with - // its surfaceless stand-in) bound a surface the driver no longer had. The session's own - // teardown terminates the display once its surfaces are gone (DestroyNativeStateOfSession). - Bool surfacesRemain = false; - for (const auto* session : NativeSessions()) surfacesRemain = surfacesRemain || !session->Surfaces.empty(); - if (g_displayUsers == 0 && !surfacesRemain && g_Display != EGL_NO_DISPLAY && g_EGLFuncs.eglTerminate) { - g_EGLFuncs.eglTerminate(g_Display); - NoteDisplayTerminated(); - g_Display = EGL_NO_DISPLAY; - } - } - - // P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. See DirectGLES.h; in one line, `bind_context` - // moves the session's context attribution and this makes the calling thread's native tuple - // follow it, binding the session's current surface to the newly named context's EGLContext. - static Bool SessionOwnsSurface(const NativeSessionState& session, EGLSurface surface) { - if (surface == EGL_NO_SURFACE) return false; - for (const auto& entry : session.Surfaces) { - if (entry.Surface == surface) return true; - } - return false; - } - - Bool MakeNativeContextCurrentForBoundToken() { - NativeContextTuple& tuple = ActiveNativeContext(); - // Nothing to switch to: a session that never sent CreateContext has no native context for - // this token, and building one here would bind a context the client does not know it has. - if (tuple.Context == EGL_NO_CONTEXT) return true; - // A MULTI-WINDOW CLIENT: each context draws to the surface it was last made current with, - // not to whichever surface the session bound most recently - Qt renders each window on its - // own thread with its own context, and binding a context back onto another window's - // surface drew one window's frame into the other. - NativeSessionState& session = ActiveNativeSession(); - // Nothing to make it current on yet: a context bound for the records of its own creation - // (EGLImpl's CreateContext), which no make-current has given a surface (or whose surface - // is gone). Binding it to whichever surface the session holds would hand it another - // context's surface - one the driver may not even have any more. Its first make-current - // binds it natively. - if (tuple.Draw == EGL_NO_SURFACE) return true; - if (tuple.Draw != session.Draw && SessionOwnsSurface(session, tuple.Draw)) { - session.Draw = tuple.Draw; - NoteDrawSurfaceMoved(); - session.Read = SessionOwnsSurface(session, tuple.Read) ? tuple.Read : tuple.Draw; - } - if (t_native.boundContext == tuple.Context && t_native.boundDraw == session.Draw) return true; - return MakeCurrent(); - } - - EGLSurface CurrentSessionDrawSurface() { return ActiveNativeSession().Draw; } - - EGLSurface CurrentContextDrawSurface() { return ActiveNativeContext().Draw; } - - namespace { - // Whether the driver of `display` has EGL_EXT_buffer_age (or EGL_KHR_partial_update, which - // defines the same query). Asked once per display. - // EGL_BUFFER_AGE_EXT (= EGL_BUFFER_AGE_KHR), spelled out: this file sees only the core EGL header. - constexpr EGLint kEglBufferAge = 0x313D; - - // How the driver answers a buffer age, if at all. EGL_EXT_buffer_age promises the whole - // buffer's content for the age it reports. EGL_KHR_partial_update alone promises it only - // OUTSIDE the frame's damage region, which is the whole surface until eglSetDamageRegionKHR - // narrows it - a frame that asks the age and then draws part of the surface without setting - // the region finds the rest undefined (on a tiler: tiles never loaded, noise on screen). - struct NativeBufferAge { - Bool Ext = false; // EGL_EXT_buffer_age - Bool Partial = false; // EGL_KHR_partial_update - EGLBoolean (*SetDamageRegion)(EGLDisplay, EGLSurface, EGLint*, EGLint) = nullptr; - }; - const NativeBufferAge& NativeBufferAgeSupport() { - static EGLDisplay asked = EGL_NO_DISPLAY; - static NativeBufferAge support; - if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) { - static const NativeBufferAge none; - return none; - } - if (asked != g_Display) { - asked = g_Display; - const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); - support = {}; - support.Ext = extensions != nullptr && std::strstr(extensions, "EGL_EXT_buffer_age") != nullptr; - support.Partial = extensions != nullptr && std::strstr(extensions, "EGL_KHR_partial_update") != nullptr; - if (support.Partial && g_EGLFuncs.eglGetProcAddress) { - support.SetDamageRegion = reinterpret_cast( - g_EGLFuncs.eglGetProcAddress("eglSetDamageRegionKHR")); - } - if (support.SetDamageRegion == nullptr) support.Partial = false; - MGLOG_I("DirectGLES: native buffer age: EGL_EXT_buffer_age %s, EGL_KHR_partial_update %s", - support.Ext ? "yes" : "no", support.Partial ? "yes" : "no"); - } - return support; - } - - // The window surface whose age the driver was asked in the frame being drawn on this (the - // session's apply) thread; the present ends the frame. - thread_local EGLSurface t_ageAskedSurface = EGL_NO_SURFACE; - - // Whether the frame being drawn on this thread has had its flush ahead of the window - // buffer (FlushAheadOfWindowBufferUse); the present ends the frame. - thread_local Bool t_windowBufferFlushIssued = false; - - // A WINDOW SURFACE'S BUFFER AGE, on unless MOBILEGL_ESPRYT_WINDOW_AGE=0 (or, on Android, the - // property debug.mobilegl.espryt_window_age=0); off, a window answers 0 and its client - // repaints whole frames. It was opt-in while a window repainted by age sometimes showed a - // client frame two or three presents old: the client's shared image had kept its previous - // frame, its copy clipped by a scissor guard that trusted another session's render-state - // shadow (ScopedScissorDisable) - whole repaints only happened to leave that shadow - // agreeing with the client. Pbuffers (client windows) are not affected. - Bool WindowBufferAgeEnabled() { - static const Bool enabled = [] { - if (const char* env = std::getenv("MOBILEGL_ESPRYT_WINDOW_AGE")) return std::strcmp(env, "0") != 0; -#if defined(__ANDROID__) - char property[PROP_VALUE_MAX] = {}; - if (__system_property_get("debug.mobilegl.espryt_window_age", property) > 0) - return std::strcmp(property, "0") != 0; -#endif - return true; - }(); - return enabled; - } - - // The calling session's record of `surface`; the registry lock is held by the caller (another - // session's window bring-up may erase stale entries of this one). - NativeSessionState::SurfaceEntry* FindSessionSurfaceLocked(NativeSessionState& session, EGLSurface surface) { - for (auto& entry : session.Surfaces) { - if (entry.Surface == surface) return &entry; - } - return nullptr; - } - } // namespace - - // THE OFF-SCREEN PART OF A FRAME DOES NOT WAIT FOR THE WINDOW BUFFER. A window surface takes - // its next buffer when the frame first uses the default framebuffer, and about every other - // frame that is the buffer the display has only just released: its release fence is still - // pending. Whatever the driver has not yet submitted then goes out at the swap, in one batch - // behind that fence - the frame's off-screen passes included - so the GPU starts the whole - // frame only after the display's next commit. A flush just before the first use lets the - // off-screen work start now; only the window pass waits. Once per frame, on window surfaces - // only (a pbuffer has no buffer to wait for), and a flush is not a wait. - static void FlushAheadOfWindowBufferUse() { - if (t_windowBufferFlushIssued) return; - t_windowBufferFlushIssued = true; - NativeSessionState& session = ActiveNativeSession(); - const EGLSurface surface = session.Draw; - if (surface == EGL_NO_SURFACE) return; - { - const std::lock_guard lock(g_nativeRegistryMutex); - const auto* entry = FindSessionSurfaceLocked(session, surface); - if (entry == nullptr || entry->Window == static_cast(0)) return; - } - if (g_GLESFuncs.glFlush) g_GLESFuncs.glFlush(); - } - - Int32 CurrentDrawBufferAge(Bool damageRegionFollows) { - NativeSessionState& session = ActiveNativeSession(); - const EGLSurface surface = session.Draw; - if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return 0; - Bool window = false; - Uint64 presents = 0; - { - const std::lock_guard lock(g_nativeRegistryMutex); - const auto* entry = FindSessionSurfaceLocked(session, surface); - if (entry == nullptr) return 0; - window = entry->Window != static_cast(0); - presents = entry->Presents; - } - if (!window) return presents > 0 ? 1 : 0; - if (!WindowBufferAgeEnabled()) return 0; - // The driver's answer. On Android the query dequeues the buffer the next frame draws into - - // what the frame's first draw would do anyway, and the client asks before it draws. - const NativeBufferAge& support = NativeBufferAgeSupport(); - const Bool askable = MG_Util::Damage::NativeAgeAskable(support.Ext, support.Partial, damageRegionFollows); - if (presents == 0 || !askable || !g_EGLFuncs.eglQuerySurface) return 0; - EGLint age = 0; - if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &age) || age < 0) return 0; - t_ageAskedSurface = surface; - return age; - } - - Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region) { - NativeSessionState& session = ActiveNativeSession(); - const EGLSurface surface = session.Draw; - if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return true; - { - const std::lock_guard lock(g_nativeRegistryMutex); - const auto* entry = FindSessionSurfaceLocked(session, surface); - if (entry == nullptr || entry->Window == static_cast(0)) return true; - } - const NativeBufferAge& support = NativeBufferAgeSupport(); - if (!support.Partial || !WindowBufferAgeEnabled()) return true; - // EGL_KHR_partial_update wants the age asked in this frame before the region is set; a client - // that was answered 0 without the driver being asked (its first frame) repaints everything. - if (t_ageAskedSurface != surface) { - EGLint ignored = 0; - (void)g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &ignored); - t_ageAskedSurface = surface; - } - EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; - const EGLint count = - region.IsFull() ? 0 : static_cast(MG_Util::Damage::PackRects(region, rects, MG_Util::Damage::kMaxRects)); - if (support.SetDamageRegion(g_Display, surface, count > 0 ? rects : nullptr, count) != EGL_TRUE) { - MGLOG_W_ONCE("DirectGLES: eglSetDamageRegionKHR refused a %d-rectangle region (EGL error 0x%x)", count, - g_EGLFuncs.eglGetError ? static_cast(g_EGLFuncs.eglGetError()) : 0u); - } - return true; - } - - Bool SelectSessionSurface(EGLSurface surface) { - NativeSessionState& session = ActiveNativeSession(); - if (!SessionOwnsSurface(session, surface)) return false; - session.Draw = surface; - NoteDrawSurfaceMoved(); - session.Read = surface; - return true; - } - - Bool BindSessionSurface(EGLSurface surface) { - NativeSessionState& session = ActiveNativeSession(); - if (!SessionOwnsSurface(session, surface)) return false; - session.Draw = surface; - NoteDrawSurfaceMoved(); - session.Read = surface; - if (ActiveNativeContext().Context == EGL_NO_CONTEXT) return true; - if (t_native.boundContext == ActiveNativeContext().Context && t_native.boundDraw == surface) return true; - return MakeCurrent(); - } - - EGLSurface CreateSessionPbuffer(EGLint width, EGLint height) { - NativeContextTuple& tuple = ActiveNativeContext(); - if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast(0))) return EGL_NO_SURFACE; - const EGLint attribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; - const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, attribs); - if (surface == EGL_NO_SURFACE) return EGL_NO_SURFACE; - NativeSessionState& session = ActiveNativeSession(); - const std::lock_guard lock(g_nativeRegistryMutex); - session.Surfaces.push_back({static_cast(0), surface}); - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - return surface; - } - - void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement) { - if (previous == EGL_NO_SURFACE || replacement == EGL_NO_SURFACE || previous == replacement) return; - NativeSessionState& session = ActiveNativeSession(); - { - const std::lock_guard lock(g_nativeRegistryMutex); - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey != session.SessionKey) continue; - if (tuple->Draw == previous) tuple->Draw = replacement; - if (tuple->Read == previous) tuple->Read = replacement; - } - auto& entries = session.Surfaces; - entries.erase(std::remove_if(entries.begin(), entries.end(), - [&](const NativeSessionState::SurfaceEntry& entry) { - return entry.Surface == previous; - }), - entries.end()); - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - if (session.Draw == previous) { - session.Draw = replacement; - NoteDrawSurfaceMoved(); - } - if (session.Read == previous) session.Read = replacement; - // The calling thread drew to the old one: draw to the new one from here (the old is then - // no longer current, and its destruction below is immediate). - if (t_native.boundDraw == previous && ActiveNativeContext().Context != EGL_NO_CONTEXT) (void)MakeCurrent(); - if (g_EGLFuncs.eglDestroySurface && g_Display != EGL_NO_DISPLAY) g_EGLFuncs.eglDestroySurface(g_Display, previous); - } - - void DestroySessionSurface(EGLSurface surface) { - if (surface == EGL_NO_SURFACE) return; - NativeSessionState& session = ActiveNativeSession(); - { - const std::lock_guard lock(g_nativeRegistryMutex); - auto& entries = session.Surfaces; - const auto it = std::find_if(entries.begin(), entries.end(), - [&](const NativeSessionState::SurfaceEntry& entry) { return entry.Surface == surface; }); - if (it == entries.end()) return; - entries.erase(it); - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey != session.SessionKey) continue; - if (tuple->Draw == surface) tuple->Draw = EGL_NO_SURFACE; - if (tuple->Read == surface) tuple->Read = EGL_NO_SURFACE; - } - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - if (session.Draw == surface || session.Read == surface) { - // Fall back onto a surface the session still has, so the next bind has somewhere to go. - const EGLSurface fallback = session.Surfaces.empty() ? EGL_NO_SURFACE : session.Surfaces.front().Surface; - session.Draw = fallback; - NoteDrawSurfaceMoved(); - session.Read = fallback; - if (t_native.boundDraw == surface && fallback != EGL_NO_SURFACE && - ActiveNativeContext().Context != EGL_NO_CONTEXT) { - (void)MakeCurrent(); - } - } - // A surface still current on this thread is destroyed by EGL once it is not (EGL 1.5 - // 3.5.6), so this needs no unbind first. - if (g_EGLFuncs.eglDestroySurface && g_Display != EGL_NO_DISPLAY) g_EGLFuncs.eglDestroySurface(g_Display, surface); - if (t_native.boundDraw == surface && session.Draw == EGL_NO_SURFACE) t_native.boundDraw = EGL_NO_SURFACE; - } - - namespace { - // Last swap interval the app requested through eglSwapInterval; -1 = never - // requested (keep the EGL default of 1). Re-applied when the window surface - // is (re)created since interval is per-surface state. - Int g_requestedSwapInterval = -1; - - void ApplyRequestedSwapInterval() { - if (g_requestedSwapInterval < 0) return; - const EGLSurface surface = ActiveNativeSession().Draw; - if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) return; - const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval); - MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval, - ok ? "ok" : "failed"); - } - } // namespace - - void SetSwapInterval(Int interval) { - g_requestedSwapInterval = interval; - ApplyRequestedSwapInterval(); - } - -#if MOBILEGL_BUILD_DISAGGREGATED - void ForgetRequestedSwapInterval() { g_requestedSwapInterval = -1; } -#endif - - // A crashed predecessor session may have left its surface bound to this window (the - // server-owned display window is one ANativeWindow for the process's lifetime, and - // several drivers refuse a second eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW). - // Teardown is keyed on the session the surface belongs to, so a session that died without - // one leaks it; find and destroy those before asking the driver. - static void DestroyStaleSurfacesOnWindow(NativeWindowType window) { - const std::lock_guard lock(g_nativeRegistryMutex); - for (auto* session : NativeSessions()) { - auto& entries = session->Surfaces; - for (auto it = entries.begin(); it != entries.end();) { - if (it->Window == window) { - if (g_EGLFuncs.eglDestroySurface) g_EGLFuncs.eglDestroySurface(g_Display, it->Surface); - if (session->Draw == it->Surface) session->Draw = EGL_NO_SURFACE; - if (session->Read == it->Surface) session->Read = EGL_NO_SURFACE; - it = entries.erase(it); - } else { - ++it; - } - } - } - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - - // The default framebuffer's shape after a window surface was bound - with its extent when the - // window is the server's own (see the push arm of the publish). - static void PublishWindowSurfaceShape(NativeWindowType window) { -#if MOBILEGL_BUILD_DISAGGREGATED - // P12 (D3): this publish may carry the window's extent (see the push arm of the publish) - - // review fix: only when the window is the SERVER's own (a headless client's ServerOwned - // surface). A window the client named keeps the format-only publish without the knob. - g_publishWindowExtent = MG_Pipe::MGPipeServerOwnedWindow() != nullptr && - reinterpret_cast(window) == MG_Pipe::MGPipeServerOwnedWindow(); - PublishDefaultFramebufferDepthStencilFormat(); - g_publishWindowExtent = false; -#else - (void)window; - PublishDefaultFramebufferDepthStencilFormat(); -#endif - } - - // THE SERVER WINDOW CHANGED SIZE (the app's layout moved the view: a bar shown or hidden, a - // rotation). The new extent - the display's, set by the server loop before this runs - is published - // as the creation publishes it, at once: the client sizes its viewport and default framebuffer from - // it, and a compositor with nothing to redraw makes no present to hang this on. - Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window) { - if (surface == EGL_NO_SURFACE || !window) return false; - // Bound, because the publish reads the default framebuffer's format. - if (!BindSessionSurface(surface) || t_native.boundDraw != surface) return false; - PublishWindowSurfaceShape(window); - return true; - } - - EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface) { - if (windowSurface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; - if (!SessionOwnsSurface(ActiveNativeSession(), windowSurface)) return EGL_NO_SURFACE; - // 1x1: nothing is shown while the window is gone, and nothing is published for it - the - // client keeps the extent it has until the resume publishes the new window's. - const EGLSurface placeholder = CreateSessionPbuffer(1, 1); - if (placeholder == EGL_NO_SURFACE) { - const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; - MGLOG_E("DirectGLES: no placeholder pbuffer could stand in for the window surface %p (EGL error 0x%x)", - reinterpret_cast(windowSurface), static_cast(error)); - return EGL_NO_SURFACE; - } - ReplaceSessionSurface(windowSurface, placeholder); - // The rebind onto the placeholder failed and the window surface is still current here - EGL - // destroys a current surface only once it is not, and the window goes when this returns. - if (t_native.boundDraw == windowSurface) (void)ReleaseCurrent(); - return placeholder; - } - - EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window) { - if (!window || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; - NativeContextTuple& tuple = ActiveNativeContext(); - if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return EGL_NO_SURFACE; - DestroyStaleSurfacesOnWindow(window); - const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); - if (surface == EGL_NO_SURFACE) { - const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; - MGLOG_E("DirectGLES: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x on resume", - reinterpret_cast(window), static_cast(error)); - return EGL_NO_SURFACE; - } - NativeSessionState& session = ActiveNativeSession(); - { - const std::lock_guard lock(g_nativeRegistryMutex); - session.Surfaces.push_back({window, surface}); - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - if (placeholder != EGL_NO_SURFACE && SessionOwnsSurface(session, placeholder)) { - ReplaceSessionSurface(placeholder, surface); - } - // Bound, because the publish below reads the default framebuffer. - if (!BindSessionSurface(surface) || t_native.boundDraw != surface) { - MGLOG_E("DirectGLES: the resumed window surface %p could not be made current; its extent is published " - "at its next bind", - reinterpret_cast(surface)); - return surface; - } - ApplyRequestedSwapInterval(); - PublishWindowSurfaceShape(window); - return surface; - } - - Bool InitWindowSurface(NativeWindowType window) { - if (!window) return false; - - DiagnoseEsprytSlotArmAtBringUp(); - - NativeContextTuple& tuple = ActiveNativeContext(); - if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return false; - - DestroyStaleSurfacesOnWindow(window); - - const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); - if (surface == EGL_NO_SURFACE) { - const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; - MGLOG_E("InitWindowSurface: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x", - reinterpret_cast(window), static_cast(error)); - return false; - } - NativeSessionState& session = ActiveNativeSession(); - session.Surfaces.push_back({window, surface}); - session.Draw = surface; - NoteDrawSurfaceMoved(); - session.Read = surface; - tuple.Draw = surface; - tuple.Read = surface; - - if (!MakeCurrent()) return false; - - ApplyRequestedSwapInterval(); - PublishWindowSurfaceShape(window); - - MGLOG_D("EGL window surface created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, - surface, tuple.Context, window); - return true; - } - - Bool InitPbufferSurface(EGLint width, EGLint height) { - if (width <= 0 || height <= 0) return false; - - DiagnoseEsprytSlotArmAtBringUp(); - - NativeContextTuple& tuple = ActiveNativeContext(); - if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast(0))) return false; - - const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; - const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, surfaceAttribs); - if (surface == EGL_NO_SURFACE) return false; - NativeSessionState& session = ActiveNativeSession(); - session.Surfaces.push_back({static_cast(0), surface}); - session.Draw = surface; - NoteDrawSurfaceMoved(); - session.Read = surface; - tuple.Draw = surface; - tuple.Read = surface; - - if (!MakeCurrent()) return false; - - PublishDefaultFramebufferDepthStencilFormat(); - - MGLOG_D("EGL pbuffer surface created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display, - surface, tuple.Context, width, height); - return true; - } - - namespace { - // The single backend ES context migrates between app threads (FCL/pojav-style - // LWJGL hands the EGL context from JVM thread to JVM thread). Ownership must - // live in ONE global slot: a per-thread flag can never be cleared on the - // LOSING thread when another thread takes (or destroys/releases) the context, - // leaving a stale "current" claim behind. A stale claim makes buffer ops issue - // GL calls that silently no-op (no context is current on that thread) while - // still updating shadow bookkeeping (bind cache, synced serials), permanently - // desynchronizing backend buffer state. - std::atomic g_backendContextOwnerThread{}; - - // Bumped whenever the backend ES context is destroyed; fence and - // timer-query handles created under an older generation belong to a - // dead context and must never be passed back to GL (mirrors - // BufferImpl's context tracking). - Uint g_syncContextGeneration = 1; - - // One-fence-per-frame ring driving the buffer-storage pool's recycle gate. - // A buffer retired during frame N is safe to reuse once frame N's fence has - // signaled. Touched only in Present()/DestroyEGLContext() on the owning - // thread -> no lock. Each fence carries the sync generation so a dead-context - // GLsync is never polled/deleted. Depth 4 >> the 2-3 frames Adreno keeps in - // flight; wrap-before-signal only happens during a stall and just degrades to - // the allocate path. - std::atomic g_currentFrameSerial{0}; - std::atomic g_completedFrameSerial{0}; - // Deep enough for several sessions presenting in turn: a slot is only reused once its - // fence has signaled (InsertFrameFence waits for it rather than dropping it). - constexpr int kFrameFenceRingDepth = 32; - struct FrameFence { - GLsync sync = nullptr; - Uint contextGeneration = 0; - Uint64 serial = 0; - }; - FrameFence g_frameFenceRing[kFrameFenceRingDepth]; - - // Backend fence handle: a native ES sync plus the ES context - // generation it was created under. - struct GLESSyncObject { - GLsync esSync = nullptr; - Uint contextGeneration = 0; - }; - - // Backend timer-query handle: a native GL query object name plus the - // ES context generation it was created under. Stale-generation - // handles read as available with a zero result, and deleting them - // only frees the wrapper (the dead ES context already reclaimed the - // query object). - struct GLESQueryObject { - GLuint queryId = 0; - Uint contextGeneration = 0; - // Non-zero for the core (non-timer) query targets - occlusion and transform - // feedback primitives - and then holds the glBeginQuery target, which glEndQuery - // needs back. Their results are counts reachable only through the core - // glGetQueryObjectuiv getter: GL_EXT_disjoint_timer_query's 64-bit - // glGetQueryObjectui64vEXT is timer-specific and may be entirely absent on - // drivers that otherwise fully support these core ES queries. - GLenum coreTarget = 0; - }; - } - - // Defined next to the stamp globals below; every owner-id writer must reset the - // EGL verification stamp BEFORE publishing the new owner. - void InvalidateEglVerifiedStamp(); - - // EVERY SHADOW OF THE CURRENT NATIVE CONTEXT'S BINDINGS, dropped whenever the context the - // calling thread renders into may have changed (MakeCurrent, a turn handoff between two - // sessions' apply threads). Re-issuing a bind is cheap; trusting a shadow that describes - // another context skips one the driver needed (a texture sampled from the wrong unit, a - // draw against another context's VAO). Requires a current context: it re-establishes - // GL_TEXTURE0 so the active-unit shadow is true rather than guessed. - static void InvalidateNativeBindingShadows() { - // Conservatively drop the redundant-glUseProgram guard: re-issuing one bind - // after a MakeCurrent is cheaper than trusting a possibly-reset context. - PrgramImpl::g_lastUsedBackendProgramId = 0; - // The GLContext becoming current may be a fresh one whose slot versions - // restarted at zero; the broadcast memo's key is only monotonic within one. - PrgramImpl::InvalidateBroadcastMemo(); - BufferImpl::InvalidateArrayBufferBindingCache(); - BufferImpl::InvalidateIndexedBufferBindingCache(); - BufferImpl::InvalidatePixelBufferBindingCaches(); - BufferImpl::InvalidateTransformFeedbackBindingShadows(); - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - VertexArrayImpl::InvalidateVAOBindingCache(); - PixelStoreImpl::InvalidatePackStateCache(); - // The render-state shadow belongs in this list for the same reason as the ones above: - // it describes the real ES context, which outlives the MobileGL context that is - // becoming current. See InvalidateSyncedRenderState. - RenderStateImpl::InvalidateSyncedRenderState(); - TextureImpl::g_boundTexturesCache = {}; - SamplerImpl::g_boundSamplersCache = {}; - if (g_GLESFuncs.glActiveTexture) g_GLESFuncs.glActiveTexture(GL_TEXTURE0); - TextureImpl::g_activeTextureUnit = 0; - } - - Bool MakeCurrent() { - // P14 S4: the context comes from the calling thread's tuple (the session's bound token), - // the surface from the session. Both halves used to be file-level singletons. - NativeContextTuple& tuple = ActiveNativeContext(); - NativeSessionState& session = ActiveNativeSession(); - tuple.Draw = session.Draw; - tuple.Read = session.Read; - if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || session.Draw == EGL_NO_SURFACE || - tuple.Context == EGL_NO_CONTEXT) { - MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized"); - return false; - } -#if MOBILEGL_BUILD_DISAGGREGATED - // Shared-image reads can only be fenced from the context that made them. - if (t_native.boundContext != EGL_NO_CONTEXT && t_native.boundContext != tuple.Context) { - SharedImageImpl::PublishPendingReads(); - } -#endif - if (!g_EGLFuncs.eglMakeCurrent(g_Display, session.Draw, session.Draw, tuple.Context)) { - const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; - MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error); - return false; - } - g_EGLFuncs.eglSwapBuffersWithDamageEXT = - MG_Util::BackendLoader::ResolveSwapBuffersWithDamage(g_EGLFuncs); - InvalidateEglVerifiedStamp(); - t_native.boundContext = tuple.Context; - t_native.boundDraw = session.Draw; - t_nativeContextThread.BoundSerial = tuple.Serial; - g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); - // The ops table may have been unregistered when a previous ES context was - // destroyed (e.g. a probe context); re-register now that GL is usable. - BufferImpl::RegisterBufferBackendOps(); - InvalidateNativeBindingShadows(); - // eglSwapInterval requires a current context; a request made while none was - // current (and dropped by the driver) is retried here. - ApplyRequestedSwapInterval(); - return true; - } - - // P14: THE BACKEND'S TURN MOVED TO ANOTHER APPLY THREAD (ServerLoop's BackendTurn). Each - // session's apply thread keeps its own native context current, but the backend's owner - // thread and every binding shadow are process-wide and still describe the context of the - // thread that held the turn before. Take ownership for this thread and forget them. - void OnBackendTurnHandoff() { - InvalidateEglVerifiedStamp(); - if (t_native.boundContext == EGL_NO_CONTEXT) { - g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); - return; - } - g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); - InvalidateNativeBindingShadows(); - } - - Bool ReleaseCurrent() { - if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) { - InvalidateEglVerifiedStamp(); - g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); - return true; - } -#if MOBILEGL_BUILD_DISAGGREGATED - if (t_native.boundContext != EGL_NO_CONTEXT) SharedImageImpl::PublishPendingReads(); -#endif - if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { - const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; - MGLOG_E_ONCE("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error); - return false; - } - // Clearing the global owner works from ANY thread (a release request can - // legally arrive on a thread other than the current owner); erring towards - // "not current" only defers buffer ops, which is always safe. - InvalidateEglVerifiedStamp(); - t_native.boundContext = EGL_NO_CONTEXT; - t_native.boundDraw = EGL_NO_SURFACE; - t_nativeContextThread.BoundSerial = 0; - g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); - return true; - } - - namespace { - // EGL ground-truth verification stamp. glvnd's eglGetCurrentContext performs - // fork detection with a real getpid() syscall on every call, and this - // predicate sits 2-3 deep in every draw - measured at 16% of the render - // thread on a live workload. NOT thread_local although the stamp is - // per-owner-thread state: a shared-library thread_local costs a - // __tls_get_addr call per access, which itself showed at 1.5% of the draw - // loop. Plain globals are equivalent because only the thread that passes the - // owner-identity check below can ever stamp or trust them, only one thread - // can be the owner at a time, and the ONLY writers of the owner id - - // MakeCurrent/ReleaseCurrent - reset the stamps before publishing a new - // owner, so a stamp can never leak across an ownership change. Atomics - // (relaxed) because the resets may come from a non-owner thread; the - // owner-id release/acquire pairing orders them. - std::atomic g_eglVerifiedFrameSerial{~0ull}; - std::atomic g_eglVerifiedContextGeneration{0}; - } // namespace - - void InvalidateEglVerifiedStamp() { - // ~0 frame serial matches no real frame, so the next owner-thread call - // re-verifies against EGL itself. - g_eglVerifiedFrameSerial.store(~0ull, std::memory_order_relaxed); - g_eglVerifiedContextGeneration.store(0, std::memory_order_relaxed); - } - - Bool IsBackendContextCurrentOnThisThread() { - const NativeContextTuple& tuple = ActiveNativeContext(); - if (tuple.Context == EGL_NO_CONTEXT) { - return false; - } - if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) { - return false; - } - // Belt and braces: EGL itself is the ground truth. A migration that bypassed - // MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim - // standing, or GL calls would silently no-op while shadow bookkeeping (bind - // cache, synced serials) still advances. Re-verify once per (frame, context - // generation) rather than per call: an external migration is caught at the - // next frame boundary instead of the next call, which recovers the - // bookkeeping just the same, without paying a syscall on every draw. - const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed); - if (g_eglVerifiedFrameSerial.load(std::memory_order_relaxed) == frameSerial && - g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) { - return true; - } - if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != tuple.Context) { - return false; - } - g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed); - g_eglVerifiedContextGeneration.store(g_syncContextGeneration, std::memory_order_relaxed); - return true; - } - - BackendSyncHandle FenceSync() { - // ES fences can only be created on the thread that owns the ES context - // (Flywheel and friends fence on the render thread, which does). - // Returning null makes the frontend fall back to an always-signaled - // sync object. - if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glFenceSync) { - return nullptr; - } - GLsync esSync = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - if (esSync == nullptr) { - return nullptr; - } - return new GLESSyncObject{esSync, g_syncContextGeneration}; - } - - GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { - const auto* sync = static_cast(handle); - if (sync == nullptr) { - return GL_ALREADY_SIGNALED; - } - // The creating ES context is gone: its GPU work either completed or - // died with the context; waiting is meaningless either way. - if (sync->contextGeneration != g_syncContextGeneration) { - return GL_ALREADY_SIGNALED; - } - // Degraded path: a thread that does not own the ES context cannot - // issue GL calls, so report signaled instead of blocking on state we - // cannot observe. Fence waits normally arrive on the render thread, - // which owns the context. - if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) { - return GL_ALREADY_SIGNALED; - } - return g_GLESFuncs.glClientWaitSync(sync->esSync, flags & GL_SYNC_FLUSH_COMMANDS_BIT, timeout); - } - - void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { - (void)flags; - (void)timeout; - const auto* sync = static_cast(handle); - if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration || - !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glWaitSync) { - return; - } - // ES 3.0 requires flags == 0 and timeout == GL_TIMEOUT_IGNORED. - g_GLESFuncs.glWaitSync(sync->esSync, 0, GL_TIMEOUT_IGNORED); - } - - void DeleteSync(BackendSyncHandle handle) { - auto* sync = static_cast(handle); - if (sync == nullptr) { - return; - } - if (sync->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() && - g_GLESFuncs.glDeleteSync) { - g_GLESFuncs.glDeleteSync(sync->esSync); - } - // Otherwise the ES sync is abandoned; the ES context reclaims all of - // its sync objects when it is destroyed. - delete sync; - } - - Bool GetSyncStatus(BackendSyncHandle handle) { - const auto* sync = static_cast(handle); - if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration || - !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetSynciv) { - return true; - } - GLint status = GL_SIGNALED; - GLsizei length = 0; - g_GLESFuncs.glGetSynciv(sync->esSync, GL_SYNC_STATUS, 1, &length, &status); - return status == GL_SIGNALED; - } - - // GL timer queries, backed by GL_EXT_disjoint_timer_query. The desktop - // tokens from glext.h are used throughout: GL_TIME_ELAPSED (0x88BF), - // GL_TIMESTAMP (0x8E28), GL_QUERY_RESULT (0x8866) and - // GL_QUERY_RESULT_AVAILABLE (0x8867) are numerically identical to their - // _EXT counterparts. - - Bool AreTimerQueriesSupported() { - return g_GLESCapabilities.SupportsDisjointTimerQuery && g_GLESFuncs.glGenQueries && - g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery && - g_GLESFuncs.glGetQueryObjectuiv && g_GLESFuncs.glQueryCounterEXT && - g_GLESFuncs.glGetQueryObjectui64vEXT; - } - - namespace { - // The entry point the resolved tier's support ships, or null when there is none. - MG_External::GLES::glTexBuffer_PTR ResolveTexBufferEntryPoint() { - using Tier = MG_External::GLESCapabilities::TextureBufferTier; - switch (g_GLESCapabilities.TextureBufferSupport) { - case Tier::ExtensionEXT: - return g_GLESFuncs.glTexBufferEXT ? g_GLESFuncs.glTexBufferEXT : g_GLESFuncs.glTexBuffer; - case Tier::ExtensionOES: - return g_GLESFuncs.glTexBufferOES ? g_GLESFuncs.glTexBufferOES : g_GLESFuncs.glTexBuffer; - case Tier::CoreEs32: - return g_GLESFuncs.glTexBuffer; - case Tier::None: - default: - return nullptr; - } - } - - MG_External::GLES::glTexBufferRange_PTR ResolveTexBufferRangeEntryPoint() { - using Tier = MG_External::GLESCapabilities::TextureBufferTier; - switch (g_GLESCapabilities.TextureBufferSupport) { - case Tier::ExtensionEXT: - return g_GLESFuncs.glTexBufferRangeEXT ? g_GLESFuncs.glTexBufferRangeEXT - : g_GLESFuncs.glTexBufferRange; - case Tier::ExtensionOES: - return g_GLESFuncs.glTexBufferRangeOES ? g_GLESFuncs.glTexBufferRangeOES - : g_GLESFuncs.glTexBufferRange; - case Tier::CoreEs32: - return g_GLESFuncs.glTexBufferRange; - case Tier::None: - default: - return nullptr; - } - } - } // namespace - - Bool AreBufferTexturesSupported() { - // Both halves matter. The tier is what the driver ADVERTISES, and it is only meaningful - // once the capabilities have been filled in; the resolved pointer is what MobileGL can - // actually call, through the spelling that tier's support ships. Gating on the - // unsuffixed name alone would call an entry point an EXT/OES driver never exported. - return g_GLESCapabilities.TextureBufferSupport != - MG_External::GLESCapabilities::TextureBufferTier::None && - ResolveTexBufferEntryPoint() != nullptr; - } - - void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer) { - MG_External::GLES::glTexBuffer_PTR entryPoint = ResolveTexBufferEntryPoint(); - if (entryPoint == nullptr) { - return; - } - entryPoint(target, internalFormat, buffer); - } - - Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size) { - MG_External::GLES::glTexBufferRange_PTR entryPoint = ResolveTexBufferRangeEntryPoint(); - if (entryPoint == nullptr) { - return false; - } - entryPoint(target, internalFormat, buffer, offset, size); - return true; - } - - const char* GetBufferTextureTierName() { - using Tier = MG_External::GLESCapabilities::TextureBufferTier; - switch (g_GLESCapabilities.TextureBufferSupport) { - case Tier::CoreEs32: - return "core (ES 3.2)"; - case Tier::ExtensionEXT: - return "GL_EXT_texture_buffer"; - case Tier::ExtensionOES: - return "GL_OES_texture_buffer"; - case Tier::None: - default: - return "unsupported"; - } - } - - BackendQueryHandle BeginTimeElapsedQuery() { - // Query objects can only be created on the thread that owns the ES - // context (MC's F3 profiler queries on the render thread, which - // does). Returning null makes the frontend fall back to an - // immediately available zero result. - if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) { - return nullptr; - } - GLuint queryId = 0; - g_GLESFuncs.glGenQueries(1, &queryId); - if (queryId == 0) { - return nullptr; - } - g_GLESFuncs.glBeginQuery(GL_TIME_ELAPSED, queryId); - return new GLESQueryObject{queryId, g_syncContextGeneration}; - } - - void EndTimeElapsedQuery(BackendQueryHandle handle) { - const auto* query = static_cast(handle); - if (query == nullptr || query->contextGeneration != g_syncContextGeneration || - !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) { - return; - } - // ES tracks the active query per target, not per object, so the - // handle only guards the degraded paths above. - g_GLESFuncs.glEndQuery(GL_TIME_ELAPSED); - } - - BackendQueryHandle QueryCounterTimestamp() { - if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) { - return nullptr; - } - GLuint queryId = 0; - g_GLESFuncs.glGenQueries(1, &queryId); - if (queryId == 0) { - return nullptr; - } - g_GLESFuncs.glQueryCounterEXT(queryId, GL_TIMESTAMP); - return new GLESQueryObject{queryId, g_syncContextGeneration}; - } - - // The core (non-timer) query targets - occlusion and transform feedback primitives. - // Unlike the timer queries above these are core ES (no GL_EXT_disjoint_timer_query - // needed), so they share one begin/end pair keyed on the glBeginQuery target. - static Bool AreCoreQueriesSupported() { - return g_GLESFuncs.glGenQueries && g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && - g_GLESFuncs.glEndQuery && g_GLESFuncs.glGetQueryObjectuiv; - } - - static BackendQueryHandle BeginCoreQuery(GLenum target) { - if (!IsBackendContextCurrentOnThisThread() || !AreCoreQueriesSupported()) { - return nullptr; - } - GLuint queryId = 0; - g_GLESFuncs.glGenQueries(1, &queryId); - if (queryId == 0) { - return nullptr; - } - g_GLESFuncs.glBeginQuery(target, queryId); - return new GLESQueryObject{queryId, g_syncContextGeneration, target}; - } - - static void EndCoreQuery(BackendQueryHandle handle) { - const auto* query = static_cast(handle); - if (query == nullptr || query->coreTarget == 0 || - query->contextGeneration != g_syncContextGeneration || !IsBackendContextCurrentOnThisThread() || - !g_GLESFuncs.glEndQuery) { - return; - } - g_GLESFuncs.glEndQuery(query->coreTarget); - } - - BackendQueryHandle BeginOcclusionQuery() { - // ES only implements the boolean ANY_SAMPLES_PASSED variant, not an exact - // GL_SAMPLES_PASSED count; the frontend already coerces ANY_SAMPLES_PASSED* - // targets to boolean, and desktop GL_SAMPLES_PASSED reads a 0/1 approximation. - return BeginCoreQuery(GL_ANY_SAMPLES_PASSED); - } - - void EndOcclusionQuery(BackendQueryHandle handle) { EndCoreQuery(handle); } - - // GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED. The frontend - // otherwise counts primitives on the CPU from the draw calls, which cannot see a - // geometry shader's amplification; the real driver's counters are exact. Returning - // null keeps that CPU accounting as the fallback. - BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) { - // GL_PRIMITIVES_GENERATED is only a legal query target from ES 3.2 on (it comes - // with geometry shaders); issuing it earlier just leaves a stray GL_INVALID_ENUM - // that some later unrelated glGetError would report as its own failure. - if (generated && g_GLESCapabilities.GLESVersion.Major * 10 + g_GLESCapabilities.GLESVersion.Minor < 32) { - return nullptr; - } - return BeginCoreQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); - } - - void EndXfbPrimitivesQuery(BackendQueryHandle handle) { EndCoreQuery(handle); } - - Bool IsQueryResultAvailable(BackendQueryHandle handle) { - const auto* query = static_cast(handle); - // Null/stale handles report available so the frontend proceeds to - // GetQueryResult64, which finalizes them as zero. A thread that does - // not own the ES context also reports available: GetQueryResult64 - // then returns false and the frontend keeps the handle for a later - // read from the owning thread. - if (query == nullptr || query->contextGeneration != g_syncContextGeneration || - !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetQueryObjectuiv) { - return true; - } - GLuint available = GL_FALSE; - g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available); - return available != GL_FALSE; - } - - Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) { - *outNanoseconds = 0; - const auto* query = static_cast(handle); - // Null handles never had a GL query object, handles from a - // since-destroyed ES context lost theirs, and missing entry points - // can never produce a reading (belt and braces: the creators already - // require them): zero is the FINAL result in all three cases, so - // report it as produced and let the frontend cache it and release - // the handle. - if (query == nullptr || query->contextGeneration != g_syncContextGeneration || - !g_GLESFuncs.glGetQueryObjectuiv || - (query->coreTarget == 0 && !g_GLESFuncs.glGetQueryObjectui64vEXT)) { - return true; - } - // A thread that does not own the ES context cannot issue GL calls, - // but the result still lands on the owning context eventually: report - // "not obtainable yet" so the frontend keeps the handle and a later - // availability poll / result read from the owning thread can still - // produce the real value. - if (!IsBackendContextCurrentOnThisThread()) { - return false; - } - if (wait) { - // Reading GL_QUERY_RESULT blocks in the driver until the result - // lands, but only after the commands were flushed; flush once, - // then poll availability for a bounded ~100ms before dropping to - // a glFinish as the last resort (ClientWaitSync has no polling - // loop to mirror - it delegates its timeout to the driver, which - // a query-object read cannot do). - if (g_GLESFuncs.glFlush) { - g_GLESFuncs.glFlush(); - } - constexpr Int kMaxAvailabilityPolls = 1000; // ~100ms at 100us per poll - GLuint available = GL_FALSE; - for (Int i = 0; i < kMaxAvailabilityPolls && available == GL_FALSE; ++i) { - g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available); - if (available == GL_FALSE) { - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } - } - if (available == GL_FALSE && g_GLESFuncs.glFinish) { - g_GLESFuncs.glFinish(); - } - } - // GL_EXT_disjoint_timer_query's GPU_DISJOINT_EXT signal is - // deliberately ignored: after a disjoint event (power state change, - // context switch) the result may be garbage, which is tolerable for - // an F3 GPU% readout, and consuming the latched flag here could hide - // the event from another observer. - if (query->coreTarget != 0) { - GLuint result32 = 0; - g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT, &result32); - *outNanoseconds = static_cast(result32); - return true; - } - GLuint64 result = 0; - g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result); - *outNanoseconds = static_cast(result); - return true; - } - - void DeleteBackendQuery(BackendQueryHandle handle) { - auto* query = static_cast(handle); - if (query == nullptr) { - return; - } - if (query->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() && - g_GLESFuncs.glDeleteQueries) { - g_GLESFuncs.glDeleteQueries(1, &query->queryId); - } - // Otherwise the GL query object is abandoned; the ES context reclaims - // all of its query objects when it is destroyed. - delete query; - } - - Int64 GetGpuTimestampNs() { - // Synchronous GPU clock sample; 0 tells the frontend GL_TIMESTAMP - // getter to fall back. - if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported() || - !g_GLESFuncs.glGetInteger64v) { - return 0; - } - GLint64 timestamp = 0; - g_GLESFuncs.glGetInteger64v(GL_TIMESTAMP, ×tamp); - return static_cast(timestamp); - } - - Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); } - Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); } - - namespace { - // THE WATERMARK IS CONTIGUOUS. Fences signal in submission order only within ONE context, - // and the serials are handed out to every native context of every session in turn, so the - // highest signaled serial says nothing about an older fence of another context still - // pending. A serial is complete when no live fence at or below it is pending. - void PollFrameFences() { - if (!g_GLESFuncs.glGetSynciv) return; - Uint64 lowestPending = ~Uint64{0}; - for (FrameFence& slot : g_frameFenceRing) { - if (!slot.sync) continue; - if (slot.contextGeneration != g_syncContextGeneration) { - slot.sync = nullptr; // its context reclaimed it, and its work is over - continue; - } - GLint status = GL_SIGNALED; - GLsizei length = 0; - g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status); - if (status == GL_SIGNALED) { - if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); - slot.sync = nullptr; - } else if (slot.serial < lowestPending) { - lowestPending = slot.serial; - } - } - const Uint64 completed = - lowestPending == ~Uint64{0} ? g_currentFrameSerial.load(std::memory_order_relaxed) : lowestPending - 1; - if (completed > g_completedFrameSerial.load(std::memory_order_relaxed)) { - g_completedFrameSerial.store(completed, std::memory_order_relaxed); - } - } - - FrameFence* OldestPendingFrameFence() { - FrameFence* oldest = nullptr; - for (FrameFence& slot : g_frameFenceRing) { - if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; - if (!oldest || slot.serial < oldest->serial) oldest = &slot; - } - return oldest; - } - } // namespace - - Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) { - if (CompletedFrameSerial() >= serial) return true; - if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false; - // Oldest pending fence first: the watermark can only pass a serial once every older fence - // (of any context) has signaled. Each fence was flushed when it was made (Present's swap), - // so waiting from this context needs no flush of the other. - for (int guard = 0; guard < kFrameFenceRingDepth + 1; ++guard) { - PollFrameFences(); - if (CompletedFrameSerial() >= serial) return true; - FrameFence* oldest = OldestPendingFrameFence(); - if (!oldest) return false; - const GLenum status = g_GLESFuncs.glClientWaitSync(oldest->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs); - if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false; - } - PollFrameFences(); - return CompletedFrameSerial() >= serial; - } - -#if MOBILEGL_BUILD_DISAGGREGATED - namespace { - // Once per thread: a latched session declines everything after this verb anyway, and the - // apply thread is the session's own. - thread_local Bool t_deviceLossLatched = false; - - Bool LatchOnResetStatus(const char* site, GLenum status) { - if (status == GL_NO_ERROR) return false; - // ONLY THE GUILTY CONTEXT ENDS ITS SESSION. The contexts ask for reset notification - // (EnsureNativeContext) so the driver can name the context whose work hung or faulted the - // GPU; a reset it reports to an INNOCENT context - one whose work only waited behind the - // stall, the compositor's above all - is noted and the session goes on as it did before - // contexts were told. Adreno 750 measured: the guilty context reads GUILTY once, after - // which the driver drops everything it submits; innocent ones read NO_ERROR. - static thread_local Bool innocentNoted = false; - if (status == GL_INNOCENT_CONTEXT_RESET) { - if (!innocentNoted) { - innocentNoted = true; - MGLOG_W("Espryt:%s - the driver reports a GPU reset this session's context did not cause " - "(GL_INNOCENT_CONTEXT_RESET); the session goes on", - site != nullptr ? site : "?"); - } - return false; - } - t_deviceLossLatched = true; - (void)MG_Pipe::MGPipeSessionLatch(MG_Pipe::MGPipeFatalFamily::DeviceLost, - "MGPipe: Fatal{BackendDeviceLost, \"Espryt:%s\"} - this session's EGL " - "context was reset (glGetGraphicsResetStatus 0x%x); the session ends " - "and its client reads a lost context, every other session keeps running", - site != nullptr ? site : "?", static_cast(status)); - return true; - } - } // namespace - - Bool LatchIfDeviceLost(const char* site, Bool debugKnob) { - if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false; - if (t_deviceLossLatched) return true; - GLenum status = GL_NO_ERROR; - if (g_GLESFuncs.glGetGraphicsResetStatus != nullptr && IsBackendContextCurrentOnThisThread()) { - status = g_GLESFuncs.glGetGraphicsResetStatus(); - } - if (status == GL_NO_ERROR && debugKnob && MG_Pipe::MGPipeDebugDeviceLossDue()) status = GL_UNKNOWN_CONTEXT_RESET; - return LatchOnResetStatus(site, status); - } - - Bool LatchIfGuiltyBeforeApply() { - if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false; - if (t_deviceLossLatched) return true; - // Asked every time the apply thread wakes, so it must leave nothing behind: not - // IsBackendContextCurrentOnThisThread, whose once-per-frame EGL verification stamp it would - // spend at the top of the loop (where the session may be between two of its contexts) - only - // whether this thread holds a native context and the backend's ownership. - if (g_GLESFuncs.glGetGraphicsResetStatus == nullptr || t_native.boundContext == EGL_NO_CONTEXT || - g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) - return false; - return LatchOnResetStatus("apply", g_GLESFuncs.glGetGraphicsResetStatus()); - } -#endif - - void Present() { -#if MOBILEGL_BUILD_DISAGGREGATED - if (LatchIfDeviceLost("present", /*debugKnob=*/false)) return; -#endif - // Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush - // makes it reachable), then non-blocking-poll prior frames' fences AFTER to - // advance the completed-frame watermark that gates buffer-pool recycling. - const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync; - Uint64 presentedSerial = 0; - if (canFence) { - const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1; - presentedSerial = serial; - FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth]; - if (slot.sync && slot.contextGeneration == g_syncContextGeneration) { - // The slot's fence is a ring-depth of serials old and may be another context's: - // dropping it unsignaled would let the watermark pass work nobody waited for. - if (g_GLESFuncs.glClientWaitSync) { - constexpr Uint64 kOldFenceWaitNs = 100ull * 1000 * 1000; - (void)g_GLESFuncs.glClientWaitSync(slot.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kOldFenceWaitNs); - } - if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); - } - slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial}; - } - -#if MOBILEGL_BUILD_DISAGGREGATED - // The session's frame boundary: the shared images its frame sampled get the frame's fence, - // so their producers' next copies wait for it. - SharedImageImpl::PublishPendingReads(); -#endif - - // P14 S4: the session's surface, not the process's. - // - // EXT -> KHR -> plain. The loader fills eglSwapBuffersWithDamageEXT from whichever - // spelling the driver provides (Adreno exposes only eglSwapBuffersWithDamageKHR), so - // the null check below is the whole chain: a driver with neither presents with plain - // eglSwapBuffers. Passing n_rects == 0 is not a partial present - the extension - // defines it as "the entire surface is implicitly damaged and the behaviour is - // equivalent to calling eglSwapBuffers" - and the wire protocol carries no damage - // rectangles for the server to forward, so the full surface is what is meant anyway. - // - // The damage is the client's, from the present record (CurrentPresentDamage): GL window - // coordinates, which is what the native call takes. Whole surface = no rectangles. - // - // ONLY A WINDOW SURFACE TAKES RECTANGLES: the platform's swap-with-damage hands them to the - // surface's native window, and a pbuffer has none (Android's dereferences it regardless). - NativeSessionState& session = ActiveNativeSession(); // before the lock: it takes it too - const EGLSurface drawSurface = session.Draw; - Bool windowSurface = false; - if (drawSurface != EGL_NO_SURFACE) { - const std::lock_guard lock(g_nativeRegistryMutex); - if (const auto* entry = FindSessionSurfaceLocked(session, drawSurface)) - windowSurface = entry->Window != static_cast(0); - } - EGLBoolean mglSwapped = EGL_FALSE; - if (g_EGLFuncs.eglSwapBuffersWithDamageEXT != nullptr) { - const MG_Util::Damage::Region& damage = MG_Backend::CurrentPresentDamage(); - EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; - const EGLint count = static_cast(MG_Util::Damage::NativeSwapRects(windowSurface, damage, rects)); - mglSwapped = g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, count > 0 ? rects : nullptr, count); - // A DRIVER THAT NAMES THE ENTRY POINT AND THEN REFUSES THE SWAP. Some vendor EGLs - // export eglSwapBuffersWithDamageEXT and answer EGL_FALSE on a window surface without - // setting an error (measured on a 4.14 vendor kernel with an Adreno driver; the plain - // entry point presents the same surface happily). Keeping the frame on that answer - // queues nothing at all: the compositor composites into a window whose buffer queue - // never sees a buffer. Fall back to the plain call instead of dropping the frame. - if (mglSwapped == EGL_FALSE && g_EGLFuncs.eglSwapBuffers != nullptr) { - MGLOG_W_ONCE("DirectGLES: this driver's eglSwapBuffersWithDamageEXT refused a window " - "surface; presenting through eglSwapBuffers for the rest of the process"); - mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); - } - } else if (g_EGLFuncs.eglSwapBuffers != nullptr) { - mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); - } - NoteDrawSurfaceMoved(); // a window surface takes a new size at its swap - if (drawSurface != EGL_NO_SURFACE) { - const std::lock_guard lock(g_nativeRegistryMutex); - if (auto* entry = FindSessionSurfaceLocked(session, drawSurface)) ++entry->Presents; - } - // A frame boundary: the next frame's age is asked anew, and its window buffer use flushed. - t_ageAskedSurface = EGL_NO_SURFACE; - t_windowBufferFlushIssued = false; - - // P15: MOBILEGL_FRAMES_IN_FLIGHT, Espryt's half. Magma bounds the frames the GPU has - // queued with its frame contexts (frame S+1 reuses the context of frame S+1-N); this is - // the same bound on the ES driver: after presenting S, wait for S+1-N to complete. The - // platform's own queueBuffer throttle already waits for S-1, so only N = 1 is stricter - // than what Android does by itself; larger N is the same contract stated, not a change. - const Uint32 framesInFlight = MG_Config::Features.FramesInFlight; - if (presentedSerial != 0 && framesInFlight != 0 && - framesInFlight < static_cast(kFrameFenceRingDepth) && g_GLESFuncs.glClientWaitSync && - presentedSerial + 1 > framesInFlight) { - const Uint64 target = presentedSerial + 1 - framesInFlight; - const FrameFence& oldest = g_frameFenceRing[target % kFrameFenceRingDepth]; - if (oldest.sync && oldest.serial == target && oldest.contextGeneration == g_syncContextGeneration) { - constexpr Uint64 kFramesInFlightWaitNs = 1000ull * 1000 * 1000; - (void)g_GLESFuncs.glClientWaitSync(oldest.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kFramesInFlightWaitNs); - } - } - - if (canFence) PollFrameFences(); - - // After the watermark advanced: retire grown-away ring stores and record the - // frame's ring high-water marks for slot reclamation. - BufferImpl::UboRingOnPresent(); - BufferImpl::UnpackRingOnPresent(); - BufferImpl::UploadRingOnPresent(); - BufferImpl::TrimBufferPool(); - - // THE frame boundary for the MGPipe counters: publish this frame's plots, fold the - // frame into the run totals and, every 120th frame, emit the summary line. - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::OnPresent(); - } - } - - // P14 S4: THE FULL TEARDOWN, SCOPED TO THE CALLING THREAD'S SESSION. - // - // This used to destroy the process's one context, its one surface and terminate the display. - // The callers (ReleaseEGLResources, ~BackendObject_DirectGLES) mean "this session's EGL world - // goes away", so under S4 it destroys EVERY native context that session holds, its surfaces, and - // terminates the display only when no context anywhere in the process is left - which is what - // keeps a second live session (S2) serving while this one closes. With no resolver installed - // there is exactly one session (key 0) and this is the old statement, byte for word. - // - // ONE CONTEXT's teardown is DestroyNativeContextFor (the DestroyContext control frame); this is - // the session's. - static void DestroyNativeStateOfSession(Uint64 sessionKey); - - void DestroyEGLContext() { -#if MOBILEGL_BUILD_DISAGGREGATED - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) - MG_Pipe::MGPipeServerSetContextLive(false); -#endif - // P14: ANOTHER SESSION'S CONTEXT STILL LIVES. Every native context is in one share group, so - // the buffers, textures and syncs this process's backend holds outlive this session's - // contexts: bumping the process-wide generations here told every live session that its - // GL objects were dead, and they were rebuilt EMPTY (a client exiting or closing its last - // window turned the compositor's textures to noise). Only this session's own state goes; - // the process-wide reset below is for the last context in the process. - const Uint64 endingSession = CurrentNativeSessionKey(); - Bool othersLive = false; - { - const std::lock_guard lock(g_nativeRegistryMutex); - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey != endingSession && tuple->Context != EGL_NO_CONTEXT) { - othersLive = true; - break; - } - } - } - if (othersLive) { - BufferImpl::OnServerSessionEndedWithOthersLive(); - DestroyNativeStateOfSession(endingSession); - return; - } - BufferImpl::OnBackendContextDestroyed(); - XfbImpl::OnBackendContextDestroyed(); - MultiDrawImpl::OnBackendContextDestroyed(); - OnRestartSubstitutionContextDestroyed(); - ScratchFBOImpl::OnBackendContextDestroyed(); - ReleasePackedWordScratchTexture(); - FramebufferImpl::InvalidateFramebufferBindingCache(); - FramebufferImpl::InvalidateFramebufferHandleArmMemos(); - VertexArrayImpl::InvalidateVAOBindingCache(); - PixelStoreImpl::InvalidatePackStateCache(); - PrgramImpl::InvalidateBroadcastMemo(); - // Texture ids belong to the dying context; wrappers destroyed later must - // not glDeleteTextures a recycled name in a successor context. - ++g_backendContextGeneration; - g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); - // Outstanding fence handles now refer to a dead context; treat them as - // signaled from here on. - ++g_syncContextGeneration; - // The frame-fence ring's syncs belong to the dead context too; abandon them - // (the context reclaims its syncs) and floor the completed watermark to the - // current serial so buffers retired under the old context read as GPU-idle. - for (FrameFence& slot : g_frameFenceRing) slot = {}; - g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed), - std::memory_order_relaxed); - - DestroyNativeStateOfSession(CurrentNativeSessionKey()); - } - - // This session's native contexts and surfaces, and the display when they were the last. - static void DestroyNativeStateOfSession(Uint64 sessionKey) { - if (g_Display == EGL_NO_DISPLAY) return; -#if MOBILEGL_BUILD_DISAGGREGATED - if (t_native.boundContext != EGL_NO_CONTEXT) SharedImageImpl::PublishPendingReads(); -#endif - { - const std::lock_guard lock(g_nativeRegistryMutex); - if (g_EGLFuncs.eglMakeCurrent) { - g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - t_native.boundContext = EGL_NO_CONTEXT; - t_native.boundDraw = EGL_NO_SURFACE; - t_nativeContextThread.BoundSerial = 0; - for (auto* tuple : NativeContexts()) { - if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue; - ForgetNativeContextObjects(tuple->Serial); - tuple->Serial = 0; - if (g_EGLFuncs.eglDestroyContext) g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context); - tuple->Context = EGL_NO_CONTEXT; - tuple->Config = nullptr; - tuple->Draw = EGL_NO_SURFACE; - tuple->Read = EGL_NO_SURFACE; - if (g_displayUsers > 0) --g_displayUsers; - } - for (auto* session : NativeSessions()) { - if (session->SessionKey != sessionKey) continue; - if (g_EGLFuncs.eglDestroySurface) { - for (const auto& entry : session->Surfaces) { - if (entry.Surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(g_Display, entry.Surface); - } - } - session->Surfaces.clear(); - session->Draw = EGL_NO_SURFACE; - session->Read = EGL_NO_SURFACE; - } - g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); - } - // THE LAST CONTEXT ANYWHERE IN THE PROCESS IS WHAT TERMINATES THE DISPLAY. A session that - // closes while another is rendering must not take eglTerminate with it. - if (g_displayUsers == 0 && g_EGLFuncs.eglTerminate) { - g_EGLFuncs.eglTerminate(g_Display); - NoteDisplayTerminated(); - g_Display = EGL_NO_DISPLAY; - } - } - -#if MOBILEGL_BUILD_DISAGGREGATED - // ---- SHARED IMAGES (DirectGLES.h; docs/archive/notes/anland/plan-ahb-dmabuf.md) ------ - namespace SharedImageImpl { -#if defined(__ANDROID__) - namespace { - namespace SI = MG_Remote::Server::SharedImages; - - // EGL_ANDROID_image_native_buffer's target and EGL_KHR_image_base's attribute, spelled - // here so no extension header is needed. - constexpr EGLenum kEglNativeBufferAndroid = 0x3140; - constexpr EGLint kEglImagePreservedKhr = 0x30D2; - - using GetNativeClientBufferFn = EGLClientBuffer (*)(const AHardwareBuffer*); - using CreateImageKhrFn = void* (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*); - using DestroyImageKhrFn = EGLBoolean (*)(EGLDisplay, void*); - using ImageTargetFn = void (*)(GLenum, void*); - - // EGL_KHR_fence_sync / EGL_KHR_wait_sync / EGL_ANDROID_native_fence_sync, likewise. - constexpr EGLenum kEglSyncNativeFenceAndroid = 0x3144; - constexpr EGLint kEglSyncNativeFenceFdAndroid = 0x3145; - constexpr EGLint kEglConditionSatisfiedKhr = 0x30F6; - using CreateSyncKhrFn = void* (*)(EGLDisplay, EGLenum, const EGLint*); - using DestroySyncKhrFn = EGLBoolean (*)(EGLDisplay, void*); - using WaitSyncKhrFn = EGLint (*)(EGLDisplay, void*, EGLint); - using ClientWaitSyncKhrFn = EGLint (*)(EGLDisplay, void*, EGLint, Uint64); - using DupNativeFenceFdFn = EGLint (*)(EGLDisplay, void*); - - struct EntryPoints { - Bool Resolved = false; - Bool Usable = false; - String Why; - GetNativeClientBufferFn GetNativeClientBuffer = nullptr; - CreateImageKhrFn CreateImage = nullptr; - DestroyImageKhrFn DestroyImage = nullptr; - ImageTargetFn TargetTexture2D = nullptr; - ImageTargetFn TargetRenderbuffer = nullptr; - // Fences as sync_files: what lets one session's GPU wait for another's without a - // CPU in between. Without them every wait below is a CPU wait. - Bool FencesUsable = false; - CreateSyncKhrFn CreateSync = nullptr; - DestroySyncKhrFn DestroySync = nullptr; - WaitSyncKhrFn WaitSync = nullptr; - ClientWaitSyncKhrFn ClientWaitSync = nullptr; - DupNativeFenceFdFn DupNativeFenceFd = nullptr; - // YUV images: sampled as external textures (GL_OES_EGL_image_external_essl3), and - // through GL_EXT_YUV_target's raw Y'CbCr sampler where the driver has it, so the - // conversion is the import's own (its colour hints) rather than the driver's guess. - Bool ExternalEssl3 = false; - Bool YuvTarget = false; - }; - - using MG_Util::BackendLoader::HasExtensionWord; - - // None of these is in the loader's tables, and eglGetProcAddress hands back live-looking - // stubs for entry points a driver lacks, so the extension strings decide first. Resolved - // once, from the first call that has a context current to read the strings from; the - // answer is never modified afterwards, so it is read without the lock. - const EntryPoints& ResolvedEntryPoints() { - static auto* mutex = new std::mutex(); - static auto* entry = new EntryPoints(); - const std::lock_guard lock(*mutex); - if (entry->Resolved) return *entry; - if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_GLESFuncs.glGetString) { - entry->Why = "the loader has no eglGetProcAddress/eglQueryString/glGetString"; - return *entry; - } - const char* egl = g_Display != EGL_NO_DISPLAY ? g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS) - : nullptr; - const char* gl = reinterpret_cast(g_GLESFuncs.glGetString(GL_EXTENSIONS)); - if (egl == nullptr || gl == nullptr) { - entry->Why = "no current display and context to read the extension strings from"; - return *entry; // asked again on the next call - } - entry->Resolved = true; - if (HasExtensionWord(egl, "EGL_KHR_fence_sync") && HasExtensionWord(egl, "EGL_KHR_wait_sync") && - HasExtensionWord(egl, "EGL_ANDROID_native_fence_sync")) { - entry->CreateSync = - reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglCreateSyncKHR")); - entry->DestroySync = - reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglDestroySyncKHR")); - entry->WaitSync = reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglWaitSyncKHR")); - entry->ClientWaitSync = - reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglClientWaitSyncKHR")); - entry->DupNativeFenceFd = reinterpret_cast( - g_EGLFuncs.eglGetProcAddress("eglDupNativeFenceFDANDROID")); - entry->FencesUsable = entry->CreateSync && entry->DestroySync && entry->WaitSync && - entry->ClientWaitSync && entry->DupNativeFenceFd; - } - if (!entry->FencesUsable) { - MGLOG_I("Shared images: no EGL native fence sync / wait sync; sessions order their access " - "to an image with CPU waits"); - } - if (!HasExtensionWord(egl, "EGL_KHR_image_base") || - !HasExtensionWord(egl, "EGL_ANDROID_image_native_buffer") || - !HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) { - entry->Why = "EGL_KHR_image_base / EGL_ANDROID_image_native_buffer / " - "EGL_ANDROID_get_native_client_buffer is not advertised"; - return *entry; - } - if (!HasExtensionWord(gl, "GL_OES_EGL_image")) { - entry->Why = "GL_OES_EGL_image is not advertised"; - return *entry; - } - entry->ExternalEssl3 = HasExtensionWord(gl, "GL_OES_EGL_image_external_essl3"); - entry->YuvTarget = HasExtensionWord(gl, "GL_EXT_YUV_target"); - entry->GetNativeClientBuffer = reinterpret_cast( - g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID")); - entry->CreateImage = - reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR")); - entry->DestroyImage = - reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR")); - entry->TargetTexture2D = - reinterpret_cast(g_EGLFuncs.eglGetProcAddress("glEGLImageTargetTexture2DOES")); - entry->TargetRenderbuffer = reinterpret_cast( - g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES")); - if (!entry->GetNativeClientBuffer || !entry->CreateImage || !entry->DestroyImage || - !entry->TargetTexture2D || !entry->TargetRenderbuffer) { - entry->Why = "eglGetProcAddress gave no address for an advertised EGLImage entry point"; - return *entry; - } - entry->Usable = true; - return *entry; - } - - void DrainErrors() { - for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) { - } - } - } // namespace -#endif - - struct EglImage { -#if defined(__ANDROID__) - SI::ImageRef Image; - void* Handle = nullptr; - EGLDisplay Display = EGL_NO_DISPLAY; - Uint64 DisplayTerminations = 0; - DestroyImageKhrFn Destroy = nullptr; - // AcquireForSampling's memory, touched only by the session whose twin holds this: the - // write generation last waited for and the native context the wait was queued in (a - // GPU wait orders that context alone), and the read frame the image was last noted in. - Uint64 WaitedGeneration = 0; - Uint64 WaitedContext = 0; - Uint64 NotedFrame = 0; - // A YUV image (ConvertYuvForSampling): an external texture over the EGLImage, the source - // of the conversion into the texture's own RGBA storage, and the reader's frame the - // conversion last ran in. - GLuint ExternalTexture = 0; - Uint64 ConvertedFrame = 0; - - ~EglImage() { - // eglDestroyImageKHR needs the display, not a context, so a session's twin teardown - // (no context current) still releases the image; only exit() and a terminated - // display - which destroyed the image itself - skip it. - if (Handle == nullptr || Destroy == nullptr || InProcessExit()) return; - if (g_displayTerminations.load(std::memory_order_acquire) != DisplayTerminations) return; - // Texture names are the share group's: deleting one needs any context of it, and - // without one current the name goes with the display. - if (ExternalTexture != 0 && t_native.boundContext != EGL_NO_CONTEXT) - g_GLESFuncs.glDeleteTextures(1, &ExternalTexture); - Destroy(Display, Handle); - } -#endif - }; - -#if defined(__ANDROID__) - EglImageRef CreateEglImage(Uint64 id, String& why) { - const EntryPoints& entry = ResolvedEntryPoints(); - if (!entry.Usable) { - why = entry.Why; - return nullptr; - } - SI::ImageRef image = SI::Find(id); - if (image == nullptr) { - why = "no live shared image has this id"; - return nullptr; - } - if (image->Native == nullptr) { - why = "the image has no AHardwareBuffer"; - return nullptr; - } - const EGLClientBuffer buffer = - entry.GetNativeClientBuffer(static_cast(image->Native)); - if (buffer == nullptr) { - why = "eglGetNativeClientBufferANDROID returned null"; - return nullptr; - } - const EGLint attribs[] = {kEglImagePreservedKhr, EGL_TRUE, EGL_NONE}; - void* handle = entry.CreateImage(g_Display, EGL_NO_CONTEXT, kEglNativeBufferAndroid, buffer, attribs); - if (handle == nullptr) { - char text[96]; - std::snprintf(text, sizeof(text), "eglCreateImageKHR failed (EGL error 0x%04x)", - g_EGLFuncs.eglGetError ? static_cast(g_EGLFuncs.eglGetError()) : 0u); - why = text; - return nullptr; - } - auto result = MakeShared(); - result->Image = std::move(image); - result->Handle = handle; - result->Display = g_Display; - result->DisplayTerminations = g_displayTerminations.load(std::memory_order_acquire); - result->Destroy = entry.DestroyImage; - return result; - } - - Bool TargetBoundTexture2D(const EglImageRef& image) { - const EntryPoints& entry = ResolvedEntryPoints(); - if (!entry.Usable || image == nullptr || image->Handle == nullptr) return false; - DrainErrors(); - entry.TargetTexture2D(GL_TEXTURE_2D, image->Handle); - return g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - - namespace { - // ONE PRESENT TARGET PER IMAGE, PROCESS-WIDE: the EGLImage and a renderbuffer whose - // storage it is. Every native context is in one share group, so the renderbuffer is - // one object for every session's context; only the framebuffer it is attached to is - // per context. Kept until no session holds the image any more (pruned on every present - // - the cache's own reference is then the only one), with a small cap so a client that - // cycles through many images cannot pile them up between presents. - struct PresentTarget { - EglImageRef Image; - GLuint Renderbuffer = 0; - Uint64 LastUse = 0; - // A present has written the image through this target (its content is a frame). - Bool Written = false; - }; - struct PresentTargets { - std::mutex Mutex; - std::unordered_map ById; - Uint64 UseClock = 0; - }; - constexpr SizeT kMaxPresentTargets = 8; - - PresentTargets& Targets() { - static auto* targets = new PresentTargets(); - return *targets; - } - - // The framebuffer a present draws through, one per native context (container objects - // are never shared). It carries an attachment only for the duration of a present, so - // no context's framebuffer keeps a dropped target's renderbuffer alive. - PerNativeContext g_presentFramebuffers; - - Bool TargetIsCurrent(const PresentTarget& target) { - return target.Image != nullptr && - target.Image->DisplayTerminations == g_displayTerminations.load(std::memory_order_acquire); - } - - void DeleteTarget(PresentTarget& target) { - if (target.Renderbuffer != 0 && TargetIsCurrent(target)) { - g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer); - } - target.Renderbuffer = 0; - target.Image = nullptr; - } - - // Caller holds the mutex and has a context of the share group current. - void PruneLocked(PresentTargets& targets) { - for (auto it = targets.ById.begin(); it != targets.ById.end();) { - if (!TargetIsCurrent(it->second) || it->second.Image->Image.use_count() <= 1) { - DeleteTarget(it->second); - it = targets.ById.erase(it); - } else { - ++it; - } - } - } - - GLuint MakeRenderbuffer(const EglImage& image, String& why) { - GLuint renderbuffer = 0; - g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer); - if (renderbuffer == 0) { - why = "glGenRenderbuffers gave no name"; - return 0; - } - // Espryt keeps no renderbuffer-binding shadow; put the driver's binding back as found. - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); - DrainErrors(); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - ResolvedEntryPoints().TargetRenderbuffer(GL_RENDERBUFFER, image.Handle); - const GLenum error = g_GLESFuncs.glGetError(); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); - if (error != GL_NO_ERROR) { - char text[96]; - std::snprintf(text, sizeof(text), "glEGLImageTargetRenderbufferStorageOES -> 0x%04x", - static_cast(error)); - why = text; - g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer); - return 0; - } - return renderbuffer; - } - - // Everything the current context has submitted, waited out on the CPU: the fallback for - // a driver that cannot hand a fence to another session. - Bool WaitForCompletion() { - if (g_GLESFuncs.glFenceSync && g_GLESFuncs.glClientWaitSync && g_GLESFuncs.glDeleteSync) { - if (GLsync fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) { - constexpr GLuint64 kSliceNs = 1000ull * 1000 * 1000; - GLenum status = GL_TIMEOUT_EXPIRED; - GLbitfield flags = GL_SYNC_FLUSH_COMMANDS_BIT; - for (Uint slice = 0; slice < 10 && status == GL_TIMEOUT_EXPIRED; ++slice) { - status = g_GLESFuncs.glClientWaitSync(fence, flags, kSliceNs); - flags = 0; - } - g_GLESFuncs.glDeleteSync(fence); - if (status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) return true; - if (status == GL_TIMEOUT_EXPIRED) { - MGLOG_E("Shared images: the submitted work did not complete within 10 s"); - return false; - } - // GL_WAIT_FAILED: glFinish is the same promise without the object. - } - } - g_GLESFuncs.glFinish(); - return true; - } - - // How long a CPU wait on another session's fence may take before the order is given up - // on (logged): a session whose GPU work never completes must not wedge its peers. - constexpr Uint32 kFenceCpuWaitMs = 2000; - - void CloseFd(int fd) { - if (fd >= 0) ::close(fd); - } - - // The current context's LATER commands wait for `fence` (a sync_file) on the GPU; a CPU - // wait when the driver cannot. Takes ownership of `fence`: a native fence sync owns it - // from a successful create on, every other path closes it here. - void WaitForFenceBeforeLaterCommands(int fence) { - if (fence < 0) return; - const EntryPoints& entry = ResolvedEntryPoints(); - if (entry.FencesUsable) { - const EGLint attribs[] = {kEglSyncNativeFenceFdAndroid, fence, EGL_NONE}; - if (void* sync = entry.CreateSync(g_Display, kEglSyncNativeFenceAndroid, attribs)) { - if (entry.WaitSync(g_Display, sync, 0) != EGL_TRUE) { - constexpr Uint64 kWaitNs = Uint64{kFenceCpuWaitMs} * 1000 * 1000; - if (entry.ClientWaitSync(g_Display, sync, 0, kWaitNs) != kEglConditionSatisfiedKhr) { - MGLOG_E_ONCE("Shared images: a peer session's fence could not be waited for"); - } - } - // A pending server wait keeps the sync alive past this (EGL_KHR_wait_sync). - entry.DestroySync(g_Display, sync); - return; - } - } - if (!SI::SyncFile::Wait(fence, kFenceCpuWaitMs)) { - MGLOG_E_ONCE("Shared images: a peer session's fence did not signal within %u ms", - static_cast(kFenceCpuWaitMs)); - } - CloseFd(fence); - } - - // A sync_file for everything the current context has submitted so far (flushed, so it - // will signal), in `*fence`. False when the driver cannot export one; the caller then - // waits on the CPU and publishes -1. - Bool ExportFence(int* fence) { - *fence = -1; - const EntryPoints& entry = ResolvedEntryPoints(); - if (!entry.FencesUsable) return false; - const EGLint attribs[] = {EGL_NONE}; - void* sync = entry.CreateSync(g_Display, kEglSyncNativeFenceAndroid, attribs); - if (sync == nullptr) return false; - // The fence has no descriptor until its command reaches the driver's queue. - g_GLESFuncs.glFlush(); - const EGLint fd = entry.DupNativeFenceFd(g_Display, sync); - entry.DestroySync(g_Display, sync); - if (fd < 0) return false; - *fence = fd; - return true; - } - - // ONE READ FRAME PER SESSION: what the session has sampled since its last frame - // boundary. A session is served by exactly one apply thread, so the thread owns it - - // nothing about it is process-wide, and a session's end (its thread's) drops it. `Id` - // is drawn from one counter across all sessions and renewed at every boundary, so an - // EglImage's NotedFrame is a once-per-frame gate no other session's frame can match. - struct ReadFrame { - Uint64 Id = 0; - SI::ReadTracker Tracker; - // The session ends frames at publishing flushes (PublishPendingAccesses): it writes - // images, so its first use of one in a frame waits for that image's pending reads. - Bool ImplicitSync = false; - // When a YUV image is converted again (ConvertYuvForSampling): renewed at EVERY - // boundary - present, publishing flush, and each native fence whatever the session - // is - since an offscreen producer (a browser's GPU process) may have no other. - Uint64 YuvEpoch = 0; - }; - std::atomic g_readFrameIds{0}; - - ReadFrame& CurrentReadFrame() { - thread_local ReadFrame frame; - if (frame.Id == 0) frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; - if (frame.YuvEpoch == 0) frame.YuvEpoch = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; - return frame; - } - - void RenewYuvEpoch(ReadFrame& frame) { - frame.YuvEpoch = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; - } - } // namespace - - void AcquireForSampling(const EglImageRef& image) { - if (image == nullptr || image->Image == nullptr) return; - EglImage& state = *image; - // Steady state: one lock and a compare (the write fence, once signaled, is dropped by - // the registry and nothing is duplicated any more). - Uint64 generation = 0; - int fence = SI::DupWriteFence(*state.Image, &generation); - const Uint64 context = CurrentNativeContextSerial(); - if (generation != state.WaitedGeneration || context != state.WaitedContext) { - state.WaitedGeneration = generation; - state.WaitedContext = context; - WaitForFenceBeforeLaterCommands(fence); - fence = -1; - } - CloseFd(fence); - ReadFrame& frame = CurrentReadFrame(); - if (state.NotedFrame != frame.Id) { - // A session that renders into images (it ends frames with a publishing flush) may be - // about to overwrite this one: what other sessions still sample goes first. - if (frame.ImplicitSync) { - for (const int read : SI::DupPendingReadFences(*state.Image)) WaitForFenceBeforeLaterCommands(read); - } - frame.Tracker.NoteRead(state.Image); - state.NotedFrame = frame.Id; - } - } - - Bool IsYuv(const EglImageRef& image) { - return image != nullptr && image->Image != nullptr && SI::FourccIsYuv(image->Image->Fourcc); - } - - namespace { - // THE CONVERSION PASS: one triangle over the texture, sampling the image through an - // external texture. Programs are the share group's and rebuilt with a new context - // generation; the framebuffer and vertex array are per context (never shared). - struct YuvConversion { - Uint ContextGeneration = ~0u; - GLuint ExternalProgram = 0; // the driver's conversion (samplerExternalOES) - GLuint TargetProgram = 0; // the import's own (GL_EXT_YUV_target) - GLint TargetMatrix = -1; - GLint TargetOffset = -1; - Bool Failed = false; - NativeContextName Framebuffer; - NativeContextName VertexArray; - }; - YuvConversion& Conversion() { - static auto* conversion = new YuvConversion(); - return *conversion; - } - - constexpr const char* kYuvVertexSource = - "#version 300 es\n" - "out vec2 vUv;\n" - "void main() {\n" - " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" - " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" - " vUv = p;\n" - "}\n"; - constexpr const char* kYuvExternalFragmentSource = - "#version 300 es\n" - "#extension GL_OES_EGL_image_external_essl3 : require\n" - "precision highp float;\n" - "uniform samplerExternalOES uSource;\n" - "in vec2 vUv;\n" - "out vec4 oColor;\n" - "void main() { oColor = vec4(texture(uSource, vUv).rgb, 1.0); }\n"; - constexpr const char* kYuvTargetFragmentSource = - "#version 300 es\n" - "#extension GL_EXT_YUV_target : require\n" - "precision highp float;\n" - "uniform __samplerExternal2DY2YEXT uSource;\n" - "uniform mat3 uMatrix;\n" - "uniform vec3 uOffset;\n" - "in vec2 vUv;\n" - "out vec4 oColor;\n" - "void main() {\n" - " vec3 ycbcr = texture(uSource, vUv).xyz;\n" - " oColor = vec4(clamp(uMatrix * (ycbcr - uOffset), 0.0, 1.0), 1.0);\n" - "}\n"; - - GLuint BuildYuvProgram(const char* fragmentSource) { - const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER); - const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER); - GLint linked = GL_FALSE; - GLuint program = 0; - if (vertexShader != 0 && fragmentShader != 0) { - g_GLESFuncs.glShaderSource(vertexShader, 1, &kYuvVertexSource, nullptr); - g_GLESFuncs.glCompileShader(vertexShader); - g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); - g_GLESFuncs.glCompileShader(fragmentShader); - program = g_GLESFuncs.glCreateProgram(); - if (program != 0) { - g_GLESFuncs.glAttachShader(program, vertexShader); - g_GLESFuncs.glAttachShader(program, fragmentShader); - g_GLESFuncs.glLinkProgram(program); - g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); - } - } - if (vertexShader != 0) g_GLESFuncs.glDeleteShader(vertexShader); - if (fragmentShader != 0) g_GLESFuncs.glDeleteShader(fragmentShader); - if (linked != GL_TRUE) { - if (program != 0) g_GLESFuncs.glDeleteProgram(program); - return 0; - } - return program; - } - - // MOBILEGL_YUV_DRIVER_CONVERSION=1: always the driver's conversion (samplerExternalOES), - // even where GL_EXT_YUV_target would let the import's hints decide. - Bool DriverConversionForced() { - static const Bool forced = [] { - const char* value = std::getenv("MOBILEGL_YUV_DRIVER_CONVERSION"); - return value != nullptr && value[0] == '1'; - }(); - return forced; - } - } // namespace - - void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height) { - if (!IsYuv(image) || target == 0 || width == 0 || height == 0) return; - EglImage& state = *image; - ReadFrame& frame = CurrentReadFrame(); - if (state.ConvertedFrame == frame.YuvEpoch) return; - // Whatever happens below, this frame does not try again. - state.ConvertedFrame = frame.YuvEpoch; - const EntryPoints& entry = ResolvedEntryPoints(); - if (!entry.Usable || !entry.ExternalEssl3) { - MGLOG_E_ONCE("Shared images: a YUV image cannot be sampled here (%s)", - entry.Usable ? "no GL_OES_EGL_image_external_essl3" : entry.Why.c_str()); - return; - } - // THE CPU-COPY FALLBACK, for a buffer this server did not allocate (SharedImageRegistry.h). - String why; - if (state.Image->IsForeign() && !SI::RefreshForeign(*state.Image, why)) { - MGLOG_E_ONCE("Shared images: foreign YUV image %llu could not be refreshed: %s", - static_cast(state.Image->Id), why.c_str()); - } - - YuvConversion& conversion = Conversion(); - if (conversion.ContextGeneration != g_backendContextGeneration) { - conversion.ExternalProgram = 0; - conversion.TargetProgram = 0; - conversion.TargetMatrix = conversion.TargetOffset = -1; - conversion.Failed = false; - conversion.ContextGeneration = g_backendContextGeneration; - } - if (conversion.Failed) return; - const Bool ownConversion = entry.YuvTarget && !DriverConversionForced(); - GLuint& program = ownConversion ? conversion.TargetProgram : conversion.ExternalProgram; - if (program == 0) { - program = BuildYuvProgram(ownConversion ? kYuvTargetFragmentSource : kYuvExternalFragmentSource); - if (program == 0) { - conversion.Failed = true; - MGLOG_E_ONCE("Shared images: the YUV conversion program could not be built"); - return; - } - if (ownConversion) { - conversion.TargetMatrix = g_GLESFuncs.glGetUniformLocation(program, "uMatrix"); - conversion.TargetOffset = g_GLESFuncs.glGetUniformLocation(program, "uOffset"); - } - MGLOG_I("Shared images: YUV images convert through %s", - ownConversion ? "GL_EXT_YUV_target with the import's colour hints" - : "the driver's external-sampler conversion"); - } - if (conversion.Framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &conversion.Framebuffer); - if (conversion.VertexArray == 0) g_GLESFuncs.glGenVertexArrays(1, &conversion.VertexArray); - if (conversion.Framebuffer == 0 || conversion.VertexArray == 0) return; - - // Every binding goes through Espryt's shadows or a guard that puts it back. - ScopedEmulationDrawState drawState; // also activates TextureImpl::TempTextureUnit - ScopedFramebufferBinding binding(/*saveRead=*/false, /*saveDraw=*/true); - DrainErrors(); - if (state.ExternalTexture == 0) { - g_GLESFuncs.glGenTextures(1, &state.ExternalTexture); - g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, state.ExternalTexture); - entry.TargetTexture2D(GL_TEXTURE_EXTERNAL_OES, state.Handle); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - const GLenum error = g_GLESFuncs.glGetError(); - if (error != GL_NO_ERROR) { - MGLOG_E_ONCE("Shared images: YUV image %llu cannot be bound as an external texture (0x%04x)", - static_cast(state.Image->Id), static_cast(error)); - } - } else { - g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, state.ExternalTexture); - } - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, conversion.Framebuffer); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target, 0); - const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); - if (status == GL_FRAMEBUFFER_COMPLETE) { - VertexArrayImpl::BindBackendVAOId(conversion.VertexArray); - g_GLESFuncs.glUseProgram(program); - g_GLESFuncs.glUniform1i(g_GLESFuncs.glGetUniformLocation(program, "uSource"), - static_cast(TextureImpl::TempTextureUnit)); - if (ownConversion) { - // A Y2Y sample is the stored code normalized as the format stores it: 8 bits, or - // P010's 10 in the high end of 16. - const double sampleMax = state.Image->Fourcc == SI::kFourccP010 ? 65535.0 / 64.0 : 255.0; - const auto c = SI::Yuv::ConversionFor(SI::GetYuvHints(*state.Image), sampleMax); - g_GLESFuncs.glUniformMatrix3fv(conversion.TargetMatrix, 1, GL_FALSE, c.Matrix); - g_GLESFuncs.glUniform3fv(conversion.TargetOffset, 1, c.Offset); - } - g_GLESFuncs.glViewport(0, 0, static_cast(width), static_cast(height)); - g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); - const GLenum error = g_GLESFuncs.glGetError(); - if (error != GL_NO_ERROR) { - MGLOG_E_ONCE("Shared images: the YUV conversion draw failed (0x%04x)", static_cast(error)); - } - } else { - MGLOG_E_ONCE("Shared images: a YUV conversion target is not renderable (0x%04x)", - static_cast(status)); - } - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); - } - - Bool PublishPendingAccesses() { - ReadFrame& frame = CurrentReadFrame(); - frame.ImplicitSync = true; - RenewYuvEpoch(frame); - // Sub-data waiting for a sync that nothing of this session would otherwise trigger. - if (t_native.boundContext != EGL_NO_CONTEXT) TextureImpl::WritePendingSharedImageUploads(); - if (frame.Tracker.Pending() == 0) return true; - int fence = -1; - if (t_native.boundContext != EGL_NO_CONTEXT && !ExportFence(&fence)) { - // No fence to publish: the work is waited out here, and published as completed. - (void)WaitForCompletion(); - } - frame.Tracker.PublishFrameAsWrite(fence); - frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; - return true; - } - - Bool ExportNativeFence(int* fence) { - *fence = -1; - ReadFrame& frame = CurrentReadFrame(); - RenewYuvEpoch(frame); - // No context current: nothing of this session is in flight to fence. - if (t_native.boundContext == EGL_NO_CONTEXT) return true; - TextureImpl::WritePendingSharedImageUploads(); - int exported = -1; - if (!ExportFence(&exported)) { - // No descriptor to give: the work is waited out, so the caller may report it done. - exported = -1; - (void)WaitForCompletion(); - } - // A session that writes images (one that ends frames at flushes) ends one here too; a - // compositor's reads stay for its swap, which comes after the fence's work anyway. - if (frame.ImplicitSync && frame.Tracker.Pending() != 0) { - frame.Tracker.PublishFrameAsWrite(exported >= 0 ? ::dup(exported) : -1); - frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; - } - *fence = exported; - return true; - } - - void PublishPendingReads() { - ReadFrame& frame = CurrentReadFrame(); - RenewYuvEpoch(frame); - if (frame.Tracker.Pending() == 0) return; - int fence = -1; - // No context current: the reads' context is gone (and its work with it), and there is - // nothing to fence or wait from - the frame is only forgotten. - if (t_native.boundContext != EGL_NO_CONTEXT && !ExportFence(&fence)) { - // FALLBACK: there is no fence to hand a writer, so the frame's reads are waited out - // here instead - once this session moves on (and, say, releases the buffer to its - // producer), nothing it submitted still samples the image. - (void)WaitForCompletion(); - } - // A session that writes images publishes every boundary as a write: an image it rendered - // into must not leave the frame without its generation moving. - if (frame.ImplicitSync) - frame.Tracker.PublishFrameAsWrite(fence); - else - frame.Tracker.PublishFrame(fence); - frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; - } - - Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) { - if (view.Id == 0 || view.Width == 0 || view.Height == 0 || view.NativeBuffer == nullptr) return false; - if (!IsBackendContextCurrentOnThisThread()) { - MGLOG_E_ONCE("Shared image present: no backend context is current on the calling thread"); - return false; - } - if (LatchIfDeviceLost("shared-image-present")) return false; - const EGLSurface surface = CurrentContextDrawSurface(); - EGLint sourceWidth = 0; - EGLint sourceHeight = 0; - if (surface == EGL_NO_SURFACE || !g_EGLFuncs.eglQuerySurface || - !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &sourceWidth) || - !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &sourceHeight) || sourceWidth <= 0 || - sourceHeight <= 0) { - MGLOG_E_ONCE("Shared image present: the current context has no default framebuffer to copy"); - return false; - } - const SI::ImageRef image = SI::Find(view.Id); - if (image == nullptr) { - MGLOG_E_ONCE("Shared image present: image %llu is no longer live", - static_cast(view.Id)); - return false; - } - - // WRITE AFTER READ: the frames that still sample the image finish before the copy - // overwrites it - queued on this context's GPU timeline, or waited out on the CPU when - // the driver has no fence to queue. Outside the targets' lock, which every session's - // present takes. - if (ResolvedEntryPoints().FencesUsable) { - for (const int fence : SI::DupPendingReadFences(*image)) WaitForFenceBeforeLaterCommands(fence); - } else if (!SI::WaitForReads(*image, kFenceCpuWaitMs)) { - MGLOG_E_ONCE("Shared image present: image %llu's readers did not finish within %u ms", - static_cast(view.Id), static_cast(kFenceCpuWaitMs)); - } - - Bool copied = false; - { - auto& targets = Targets(); - const std::lock_guard lock(targets.Mutex); - PruneLocked(targets); - auto it = targets.ById.find(view.Id); - if (it != targets.ById.end() && it->second.Image->Image->Native != view.NativeBuffer) { - DeleteTarget(it->second); - targets.ById.erase(it); - it = targets.ById.end(); - } - if (it == targets.ById.end()) { - String why; - PresentTarget target; - target.Image = CreateEglImage(view.Id, why); - if (target.Image != nullptr) target.Renderbuffer = MakeRenderbuffer(*target.Image, why); - if (target.Renderbuffer == 0) { - MGLOG_E_ONCE("Shared image present: image %llu cannot be rendered to: %s", - static_cast(view.Id), why.c_str()); - return false; - } - while (targets.ById.size() >= kMaxPresentTargets) { - auto oldest = targets.ById.begin(); - for (auto candidate = targets.ById.begin(); candidate != targets.ById.end(); ++candidate) { - if (candidate->second.LastUse < oldest->second.LastUse) oldest = candidate; - } - DeleteTarget(oldest->second); - targets.ById.erase(oldest); - } - it = targets.ById.emplace(view.Id, std::move(target)).first; - } - it->second.LastUse = ++targets.UseClock; - // A target written before keeps what it was given; one new to this process is - // copied whole whatever the client asked (it cannot know the image's history here). - const Bool wholeTarget = !it->second.Written; - it->second.Written = true; - - GLuint& framebuffer = g_presentFramebuffers.Current(); - if (framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &framebuffer); - if (framebuffer == 0) { - MGLOG_E_ONCE("Shared image present: glGenFramebuffers gave no name"); - return false; - } - - // Every binding goes through Espryt's shadows (BindFramebufferId, the scissor test's - // render-state shadow) and is put back by the guards, so the application's state and - // the caches that describe it are what they were. A blit is scissored like a draw. - ScopedScissorDisable scissorOff; - ScopedFramebufferBinding binding(/*saveRead=*/true, /*saveDraw=*/true); - FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 0); - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, framebuffer); - g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - it->second.Renderbuffer); - const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); - if (status == GL_FRAMEBUFFER_COMPLETE) { - // The default framebuffer's read buffer is context state the application may - // have set to GL_NONE; the copy reads the back buffer either way. - GLint readBuffer = GL_BACK; - g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); - if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(GL_BACK); - DrainErrors(); - const Bool sameExtent = static_cast(sourceWidth) == view.Width && - static_cast(sourceHeight) == view.Height; - // TOP ROW FIRST: GL's top row (y = height - 1) lands in the image's first memory - // row, which an AHardwareBuffer-backed renderbuffer addresses as y = 0. - // - // Only where the image differs from the frame: the client keeps, per image, the - // damage it missed (BufferDamageTracker), and a partial blit keeps the rest of - // the renderbuffer as GL promises (checked texel for texel against the frame on - // Adreno). What once left old texels around the rectangles was the scissor - // guard above trusting another session's shadow: the copy was clipped, and - // the missed-damage bookkeeping then never brought those texels over again. - if (sameExtent && !wholeTarget && !region.IsFull()) { - // Each rectangle flipped the same way (an empty region copies nothing: the - // image already is the frame). - for (const MG_Util::Damage::FlippedCopy& copy : - MG_Util::Damage::FlippedCopies(region, sourceWidth, sourceHeight)) { - const MG_Util::Damage::Rect& from = copy.Source; - const MG_Util::Damage::Rect& to = copy.Destination; - g_GLESFuncs.glBlitFramebuffer(from.X, from.Y, from.X + from.Width, from.Y + from.Height, - to.X, to.Y + to.Height, to.X + to.Width, to.Y, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - } - } else { - g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, - static_cast(view.Height), static_cast(view.Width), - 0, GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR); - } - const GLenum error = g_GLESFuncs.glGetError(); - copied = error == GL_NO_ERROR; - if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(static_cast(readBuffer)); - if (!copied) { - MGLOG_E_ONCE("Shared image present: glBlitFramebuffer %dx%d -> %ux%u failed (0x%04x)", - sourceWidth, sourceHeight, view.Width, view.Height, - static_cast(error)); - } - } else { - MGLOG_E_ONCE("Shared image present: image %llu's framebuffer is incomplete (0x%04x)", - static_cast(view.Id), static_cast(status)); - } - g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); - } - if (!copied) return false; - - // The copy's completion goes to the readers as a fence and the present returns at once; - // a reader that binds the image waits for it on its own GPU. Without fence export the - // copy is waited out here and published as already complete. - int fence = -1; - if (!ExportFence(&fence) && !WaitForCompletion()) return false; - SI::PublishWrite(*image, fence); - // A session that samples images and presents into one (a nested compositor) closes its - // read frame here too. - PublishPendingReads(); - return true; - } - - void ForgetPresentTargetsAfterTerminate() { - auto& targets = Targets(); - const std::lock_guard lock(targets.Mutex); - // TargetIsCurrent is already false for every entry, so nothing reaches the driver. - for (auto& entry : targets.ById) DeleteTarget(entry.second); - targets.ById.clear(); - } -#else - EglImageRef CreateEglImage(Uint64 id, String& why) { - (void)id; - why = "shared images are imported through AHardwareBuffer, which only Android has"; - return nullptr; - } - Bool TargetBoundTexture2D(const EglImageRef& image) { - (void)image; - return false; - } - Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) { - (void)view, (void)region; - return false; - } - void AcquireForSampling(const EglImageRef& image) { (void)image; } - - Bool IsYuv(const EglImageRef& image) { - (void)image; - return false; - } - - void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height) { - (void)image, (void)target, (void)width, (void)height; - } - void PublishPendingReads() {} - Bool PublishPendingAccesses() { return true; } - Bool ExportNativeFence(int* fence) { - *fence = -1; - return true; - } - void ForgetPresentTargetsAfterTerminate() {} -#endif - } // namespace SharedImageImpl -#endif - } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp new file mode 100644 index 00000000..95a72661 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp @@ -0,0 +1,1884 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESBind.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace BufferImpl { + // P5e (vi), CONTRACT-P5E §5.1: THE SAME MEMO, THE SAME THREE-VALUE KEY, AND A WALK THAT + // READS NO FRONTEND AT ALL. The arm above keeps the frontend attribute walk because the + // push-monolith build has to keep its bytes (§5.8); this one is what a server with no + // client objects on its side runs, and the difference between them is exactly three + // substitutions: + // + // the walk st.VertexBuffers[Start .. Start+Count) instead of + // GetAllAttributes(): the SAME set, because the emitter resolves the + // attributes on the client and publishes one entry per attribute slot + // (VertexInputEmit.h's EmitVertexBuffers), with Res == null for a + // client-memory array and for a disabled slot - which is why the null + // skip below is not a hole but the record's own "nothing to ensure". + // the dedupe a {slot, gen} compare instead of a raw address compare. An address + // a successor object can reproduce is the identity hazard this whole + // phase is about; a handle cannot be reproduced (Gen moves on reuse). + // the ensure EnsureBufferResourceForHandle(nullptr, Res). The frontend argument + // is only ever used for BufferObject::SyncPersistentMappedRange, which + // that function already skips under a transport (D-N), so passing + // nullptr removes a read rather than dropping work - and (nullptr, h) + // is already the live shape of the indirect-buffer ensure two hundred + // lines below. + // + // The IBO half lives in SyncNeccessaryBuffers beside the legacy one, for the reason it + // always did: the index slot is not part of the configuration (D5). + void SyncVaoAttributeBuffersByRecord( + VertexArrayImpl::BackendVertexArrayObject::ResolvedDrawBuffers* memo, Uint64 bufferEpoch) { + const auto& st = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements; + Uint64 elementsSerial = 0; + Bool haveElementsRecord = false; + if (!MG_Pipe::MGPipeHandleIsNull(elements) && elements.Slot < st.VertexElementsCsos.size()) { + const auto& record = st.VertexElementsCsos[elements.Slot]; + if (record.Live && record.Gen == elements.Gen) { + elementsSerial = record.ContentSerial; + haveElementsRecord = true; + } + } + const Uint64 buffersSerial = st.VertexBuffersSerial; + + // NO LIVE ELEMENTS RECORD IS A MISS, NEVER A HIT - the arm above states the whole + // argument (MGPipeApplierReset empties the table at every context change and the + // key would then never move again). + const Bool memoKeyIsMeaningful = haveElementsRecord; + + if (memo && memo->valid && memoKeyIsMeaningful && memo->elementsHandle == elements && + memo->elementsSerial == elementsSerial && memo->buffersSerial == buffersSerial) { + if (memo->vboCleanEpoch != bufferEpoch) { + Bool allClean = true; + for (Uint i = 0; i < memo->count; ++i) { + auto& entry = memo->entries[i]; + // nullptr, not entry.frontend: the frontend question inside + // IsBufferDrawCleanByHandle is monolith-only already + // (askTheObjectWhetherItIsMapped), and a raw client address held across + // records is precisely what §4.4 stops the apply thread dereferencing. + if (IsBufferDrawCleanByHandle(entry.handle, entry.resource, nullptr)) continue; + allClean = false; + // Re-ensured BY HANDLE. The legacy repair went back through the memoed + // attribute index to re-read attrib.Buffer; here the entry already + // names the resource the record named, so the repair needs nothing the + // walk did not already have. + entry.resource = EnsureBufferResourceForHandle(nullptr, entry.handle); + } + memo->vboCleanEpoch = allClean ? bufferEpoch : 0; + } + return; + } + + // Full walk, once per distinct buffer, rebuilding the memo as it goes. WHICH + // buffers is ResolveDrawVertexBuffersFromRecord's answer and only its answer - see + // Managers.h: that separation is what lets a unit case drive the source decision + // this package changed without an ES context, and what makes a revert of it visible + // rather than duplicated. + DrawVertexBufferRequest requests[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS]; + const Uint syncedBufferCount = ResolveDrawVertexBuffersFromRecord( + st, requests, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); + for (Uint i = 0; i < syncedBufferCount; ++i) { + auto* resource = EnsureBufferResourceForHandle(nullptr, requests[i].Res); + if (memo) { + auto& entry = memo->entries[i]; + // Explicitly cleared rather than left as whatever the previous build of + // this memo stored: nothing on this arm may dereference it, and a stale + // address that merely happens never to be read is the kind of thing a + // later reader takes for a live one. + entry.frontend = nullptr; + entry.attribIndex = static_cast(requests[i].BindingIndex); + entry.resource = resource; + entry.handle = requests[i].Res; + } + } + if (memo) { + memo->count = syncedBufferCount; + memo->elementsHandle = elements; + memo->elementsSerial = elementsSerial; + memo->buffersSerial = buffersSerial; + memo->valid = memoKeyIsMeaningful; + // Rebuilt via the ensure, not probed clean: the next probe pass stamps it. + memo->vboCleanEpoch = 0; + } + } + + // `vaoConfigVersion` is the caller's early read of currentVAOObject->GetConfigVersion(): + // the VAO's config fields live on a cache line the draw path touches nowhere else, and + // cycling section VAOs makes that a guaranteed miss - reading it at the top of + // PrepareForDraw overlaps the miss with the program/texture-key work instead of + // stalling the memo check below. Nothing between the read and here can move it + // (only frontend GL entry points mutate VAO config, none run inside a preparation). + void SyncNeccessaryBuffers(const SharedPtr& currentVAOObject, + VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Uint32 vaoConfigVersion, + Bool includeIBO = false, Bool includeIndirectBuffer = false) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + ProcessDeferredBufferReleases(); +#if MOBILEGL_BUILD_DISAGGREGATED + ProcessDeferredT0Retires(); // P11 B2 +#endif + + // All buffers we need are: + // 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed) + // PBO is not needed since it should be handled in frontend + + // VBO. The distinct-buffer set is memoed on the VAO's backend twin: cycling + // hundreds of section VAOs re-walked 32 cold VertexAttribute slots per draw + // only to rediscover the same one or two static buffers. While the config + // version holds (every attach/enable/disable bumps it, and it pins each + // memoed frontend pointer via the attribute SharedPtrs), the walk reduces to + // an IsBufferDrawClean probe per distinct buffer; only dirty entries take + // EnsureBufferResource, re-fetched through their attribute index. + // Pre-pass epoch read (acquire), stamped into the memo only after a pass in + // which EVERY probe came up clean: while the stamp still equals the current + // epoch, no path that can dirty any buffer has run (see the mutation-site + // enumeration at CurrentBufferMutationEpoch's declaration in Managers.h), + // so the probes themselves are skipped. A mid-pass bump lands after this + // read, makes the stamp stale, and re-runs the probes next draw. + const Uint64 bufferEpoch = CurrentBufferMutationEpoch(); + auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr; + const Uint32 configVersion = vaoConfigVersion; + SyncVaoAttributeBuffersByRecord(memo, bufferEpoch); + + // IBO, memoed by bound-object identity (its slot version is not covered by + // the config version). A stale identity hit is impossible in effect: the + // clean probe re-validates the resource against the LIVE bound object. + if (includeIBO) { + // P5e (vi), CONTRACT-P5E §5.1: the index buffer is st.IndexBuffer.Res and the + // "has it moved" question is st.IndexBufferSerial. The identity compare stays - + // it always did, because the index slot is outside the configuration version + // (D5) - but it now compares a handle the RECORD named instead of a frontend + // address the slot was probed for, and the serial joins it because this arm has + // no live slot to re-read: see ResolvedDrawBuffers::iboSerial in Managers.h. + // + // A null Res is "no element buffer bound" and is a no-op here exactly as the + // absent `possibleIBO` is below: SyncToBackendFromApplier is what binds + // GL_ELEMENT_ARRAY_BUFFER back to 0 for such a draw, and it keys on the same + // serial. + const DrawIndexBufferRequest request = + ResolveDrawIndexBufferFromRecord(MG_Pipe::MGPipeApplier()); + const MG_Pipe::MGPipeHandle iboHandle = request.Res; + const Uint64 iboSerial = request.Serial; + if (!MG_Pipe::MGPipeHandleIsNull(iboHandle)) { + const Bool identityHolds = + memo != nullptr && memo->iboHandle == iboHandle && memo->iboSerial == iboSerial; + if (identityHolds && memo->iboCleanEpoch == bufferEpoch) { + // probed fully clean at this epoch; nothing can have dirtied it + } else if (identityHolds && + IsBufferDrawCleanByHandle(iboHandle, memo->iboResource, nullptr)) { + memo->iboCleanEpoch = bufferEpoch; + } else { + auto* resource = EnsureBufferResourceForHandle(nullptr, iboHandle); + if (memo) { + memo->iboHandle = iboHandle; + memo->iboSerial = iboSerial; + memo->iboFrontend = nullptr; // see Entry::frontend's note + memo->iboResource = resource; + // Repaired, not probed clean: stamp on the next clean probe. + memo->iboCleanEpoch = 0; + } + } + } + } + + // Indirect Buffer Object - must also be bound to GL_DRAW_INDIRECT_BUFFER on the ES + // context since indirect draws now execute natively on the GPU. + if (includeIndirectBuffer) { + // P5c (hd, CONTRACT-P5C §3.5): under an active transport the buffer is the verb + // record's handle; the frontend binding slot and the client allocator are + // never read (T2). The twin's ensure is what SyncBoundBuffer's did. + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + auto* resource = EnsureBufferResourceForHandle(nullptr, handle); + if (resource && resource->id != 0) { + BindBufferId(GL_DRAW_INDIRECT_BUFFER, resource->id); + } + } + } + + // UBO binding points are (re)established per draw by BindCurrentProgramWithResources at + // their compacted link-time points: CacheResourceLocations glUniformBlockBinding's the + // transpiled ESSL blocks to points 0,1,2,... (layout(binding=N) is stripped from the + // ESSL), so those compacted points are the only ones the shader reads. A frontend-indexed + // sync here would bind points the shader never reads and is unconditionally overwritten by + // the program rebind that always follows in PrepareForDraw - i.e. redundant for draws - so + // it is intentionally omitted (see SyncComputeBuffers for the compute path, which needs it). + // SSBOs are different: their block bindings are baked into the ESSL at compile time and + // BindCurrentProgramWithResources binds no SSBO points, so this is their sole draw-path + // binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers). + // P5e (sb, §5.6): the record arm, selected by transport + bit 13. Same `if (…) else` + // shape the indirect buffer above already uses. + SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, + GL_SHADER_STORAGE_BUFFER); + } + } // namespace BufferImpl + + namespace XfbImpl { + namespace { + // Binds a scratch buffer, sized for `capacityVertices` gap-free records, to + // capture point 0 in place of the application's buffers. Returns false when the + // scratch storage cannot be provided, in which case the caller falls back to the + // direct binding (which produces a wrong layout, but is what happened before). + Bool BindScatterCaptureBuffer(SizeT packedStride, SizeT capacityVertices) { + auto& g_scatterBufferId = CurrentXfb().serverScatterBuffer; + auto& g_scatterBufferSize = CurrentXfb().serverScatterSize; + if (packedStride == 0 || capacityVertices == 0) return false; + if (g_GLESFuncs.glGenBuffers == nullptr || g_GLESFuncs.glBufferData == nullptr) return false; + const SizeT required = packedStride * capacityVertices; + if (g_scatterBufferId == 0) { + g_GLESFuncs.glGenBuffers(1, &g_scatterBufferId); + if (g_scatterBufferId == 0) return false; + g_scatterBufferSize = 0; + } + if (g_scatterBufferSize < required) { + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); + g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(required), nullptr, + GL_DYNAMIC_COPY); + g_scatterBufferSize = required; + } + // Point 0 carries every captured varying: the gl_NextBuffer / gl_SkipComponents + // entries are consumed at link time and never reach the driver, so the ES + // program is declared INTERLEAVED over a single buffer and point 0 is the only + // point it can write (GL 4.6 core 13.2.2). + // + // The other points are therefore left exactly as they are. Clearing them - which + // this used to do, across the application's whole touched high-water mark - is + // both unnecessary (the ES program cannot write an unused point) and the precise + // trigger for the Mali G1-Ultra capture loss: see + // SyncTransformFeedbackBindingPoints for the mechanism and the device evidence. + // KHR-GL46.transform_feedback.capture_special_interleaved_test is the case that + // reaches this path. + BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0, + static_cast(required)); + return true; + } + } // namespace + + // Tail of PrepareForDraw: the program is bound and every buffer the draw needs + // is up to date, so the capture buffers can be bound and the span opened. + void StartPendingTransformFeedback() { + auto& xfb = CurrentXfb(); + // A span that was paused before its first draw must not open here: the draw is + // not captured, and opening the span would also subject it to the capture + // primitive-mode rule the paused draw is exempt from. + if (!xfb.pending || xfb.paused) return; + XfbProgramSource program; + const MG_Pipe::MGPStreamOutputBegin* span = nullptr; + const auto& state = MG_Pipe::MGPipeApplier(); + const auto found = state.StreamOutputSpans.find(state.BoundStreamOutputLifetimeId); + if (found != state.StreamOutputSpans.end()) { + span = &found->second; + const auto* record = PipeShaderCsoRecordForHandle(span->CaptureProgram); + if (record) program.Archive = record->Archive; + } + if (!program) { + // The pending flag is deliberately NOT consumed here. It used to be cleared + // before this check, so a single draw that could not see the capture program + // retired the span permanently: every later draw of the same span found + // pending==false, the driver Begin never happened, and End found started==false + // and skipped the readback - a whole capture lost with no GL error anywhere. + // The frontend only reaches a draw with an active span after glBeginTransformFeedback + // stored a program, so this is a "cannot happen" that must stay recoverable. + MGLOG_E_ONCE("StartPendingTransformFeedback: an active capture span has no capture program; the " + "driver span stays closed and this draw is not captured"); + return; + } + xfb.pending = false; + + // Snapshot what the driver is about to capture into. GL forbids rebinding the + // capture buffers while the span is open, so this stays valid until End, and + // recording it here keeps End independent of the frontend capture state. + const SizeT bufferCount = program->GetTransformFeedbackBufferCount(); + for (SizeT i = 0; i < bufferCount; ++i) { + if (!span || i >= 4) continue; + const auto& range = span->Targets[i]; + if (MG_Pipe::MGPipeHandleIsNull(range.Res) || range.Size == 0) continue; + auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, range.Res); + if (!resource || resource->id == 0) continue; + xfb.targets.push_back({nullptr, resource->id, static_cast(range.Offset), + static_cast(range.Offset + range.Size), i, range.Res}); + BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast(i), + resource->id, static_cast(range.Offset), static_cast(range.Size)); + continue; + } + + + // A layout with holes or several interleaved buffers is not expressible on ES: + // capture gap-free into scratch storage and place the records at End instead. + xfb.scattered = false; + xfb.scatterProgram.reset(); + xfb.scatterArchive.reset(); + xfb.scatterCapacityVertices = 0; + if (program->NeedsScatteredTransformFeedbackCapture()) { + SizeT capacityVertices = ~SizeT(0); + for (const auto& target : xfb.targets) { + // By BUFFER index. Reading the stride at the target's POSITION made a + // capture list beginning with gl_NextBuffer - buffer 0 has stride 0 and + // nothing bound, so target 0 describes buffer 1 - read stride 0, skip every + // target, and leave the capacity at zero. + const SizeT stride = program->GetTransformFeedbackStride(static_cast(target.bufferIndex)); + if (stride == 0) continue; + capacityVertices = std::min(capacityVertices, (target.end - target.start) / stride); + } + if (capacityVertices == ~SizeT(0)) capacityVertices = 0; + if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) { + xfb.scattered = true; + xfb.scatterProgram = program.Frontend; + xfb.scatterArchive = program.Archive; + xfb.scatterCapacityVertices = capacityVertices; + } else { + // NO SPAN RATHER THAN A SPAN THAT WRITES SOMEWHERE ELSE. The ES program for a + // scattered capture is a single-buffer INTERLEAVED one (the gl_NextBuffer / + // gl_SkipComponents entries are consumed at link time and never reach the + // driver), so it writes capture point 0 and nothing else. Point 0 here is + // either unbound or - the dangerous case - still holds whatever an earlier + // capture in this process bound there, because the frontend's own + // glBindBufferBase is state-only and nothing else in the backend touches the + // indexed points. Opening the span would then have the driver capture over an + // application buffer that has nothing to do with this draw, and the frontend + // shadow would never learn of it. + // + // Leaving the span closed reproduces exactly what the old high-water clear + // loop achieved by binding 0 here and letting the driver refuse the Begin - + // the capture records nothing - without issuing a capture-point bind the + // application did not ask for, which is the thing that loses captures whole + // on Mali (see SyncTransformFeedbackBindingPoints). + MGLOG_E_ONCE("StartPendingTransformFeedback: no scratch storage for a scattered capture " + "(packed stride %zu, capacity %zu vertices); leaving the driver span CLOSED so the " + "capture cannot land in a stale binding. Nothing will be captured.", + program->GetTransformFeedbackPackedStride(), capacityVertices); + xfb.targets.clear(); + return; + } + } + + // A capture program with buffers bound must have produced at least one target; + // an empty list means End has nothing to mirror back and the application will + // read its buffer's pre-draw bytes however well the GPU captured. + if (xfb.targets.empty()) { + MGLOG_E_ONCE("StartPendingTransformFeedback: opening a capture span with NO capture targets " + "(program declares %zu capture buffer(s), none of them resolved to a bound backend " + "buffer with a non-empty range); nothing will be read back", + bufferCount); + } + + g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode); + // Unchecked before. Every ES error condition here (already active, a current + // program with no capture set, a capture point the program uses with no buffer) + // ends the same way: the driver records nothing, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN + // reads 0 and the application sees no error at all - MobileGL's own error state is + // separate from the driver's, so a driver rejection here is invisible to it. + if (const GLenum beginError = TakeXfbDriverError(); beginError != GL_NO_ERROR) { + // The mode is printed as a number as well as a name: GL_POINTS is 0, which the + // enum converter spells "GL_FALSE", and a reader chasing a lost capture should + // not have to know that. + MGLOG_E_ONCE("StartPendingTransformFeedback: the ES driver REJECTED " + "glBeginTransformFeedback(%s / 0x%04x) with %s - nothing will be captured. Backend " + "program %u, %zu capture buffer(s), %zu target(s), mode=%s.", + MG_Util::ConvertGLEnumToString(xfb.primitiveMode).c_str(), + static_cast(xfb.primitiveMode), + MG_Util::ConvertGLEnumToString(beginError).c_str(), + PrgramImpl::g_lastUsedBackendProgramId, bufferCount, + xfb.targets.size(), + MG_Util::ConvertGLEnumToString(program->GetTransformFeedbackBufferMode()).c_str()); + } + xfb.started = true; + } + } // namespace XfbImpl + + namespace VertexArrayImpl { + // P5e (vi): the record arm's entry, and it takes NO frontend object - CONTRACT-P5E §4.1 + // ("the object parameter is deleted from the transport overload, not defaulted to + // null"). A null twin here is the null BoundVertexElements, i.e. "no VAO bound", which + // PrepareForDraw turns into BindBackendVAOId(0) exactly as a null frontend VAO did; + // logging about it would fire on every draw of a session that legitimately has no + // vertex-elements CSO yet. + void SyncCurrentVAOFromRecords(BackendVertexArrayObject* vaoTwin) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (!vaoTwin) return; + vaoTwin->SyncToBackendFromApplier(); + } + + // GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current + // value* (context state set by glVertexAttrib*, default (0,0,0,1)) rather than any buffer. + // MobileGL stores those values in MG_State only, so without this step the ES driver would feed + // the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1). + // SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES + // current value is what the shader will actually read. + void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin, + const SharedPtr& program) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (!program) return; + if (!vaoTwin) return; + + // P5e (vi), CONTRACT-P5E §5.1: the bound-VAO row is retired here. On the record arm + // the twin IS the bound vertex-elements CSO (the caller resolved it from + // st.BoundVertexElements), so "is a VAO bound" is already answered by vaoTwin being + // non-null and GetBoundVertexArray() has nothing left to add. + // + // WHAT IS NOT RETIRED HERE, and it is S4/pg's not vi's: the two program reads below + // (GetActiveAttributeLocationMask, GetAttribType). They are frontend object rows of + // the PROGRAM family and stay until pg carries the attribute mask and types in the + // ShaderCso record. Until then this function is still a frontend reader on the + // apply thread - a BARRIERED one, which is legal (§4.4 / ruling 4) and is why vi + // does its half now rather than waiting: the VAO half is what this package owns and + // leaving it would keep GetBoundVertexArray alive for a reason that is not the + // program's. + const MG_Pipe::MGPipeVertexElementsRecord* elementsRecord = nullptr; + MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle; + Uint64 elementsSerial = 0; + const auto& st = MG_Pipe::MGPipeApplier(); + elementsHandle = st.BoundVertexElements; + if (!MG_Pipe::MGPipeHandleIsNull(elementsHandle) && + elementsHandle.Slot < st.VertexElementsCsos.size()) { + const auto& record = st.VertexElementsCsos[elementsHandle.Slot]; + if (record.Live && record.Gen == elementsHandle.Gen) { + elementsRecord = &record; + elementsSerial = record.ContentSerial; + } + } + // No record is "the configuration was never described": there is nothing to + // decide which locations lack an array from, and guessing would either + // re-issue every current value every draw or silently skip the ones that need + // it. The next create_vertex_elements re-opens the memo by serial. + if (elementsRecord == nullptr) return; + + const SharedPtr noVao; + const auto& vao = noVao; + + const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask(); + if (activeAttribMask == 0) return; + + // Which of the program's attributes lack an enabled array cannot change without + // the VAO's config version moving (Enable/DisableAttribute bump it) or the + // program's active mask changing, so the per-draw Enabled probes reduce to two + // compares. The memo lives on the twin (1:1 with the VAO, no identity key + // needed): a function-static single entry missed on every draw once the app + // cycled section VAOs, re-reading the cold attribute slots each time; here a + // cycle re-hits every VAO's own entry. The rebuild visits only ACTIVE locations. + auto& memo = vaoTwin->GetPendingAttribValueMaskMemo(); + // The re-keyed memo: {elementsHandle, ContentSerial, activeMask}. Every + // Enable/DisableVertexAttribArray moves the frontend configuration version, the + // client re-emits create_vertex_elements on the same handle for it, and the + // applier ++s ContentSerial - so this key opens exactly when the old one did + // and never wraps (Managers.h states the strictly-stronger argument). + if (!memo.valid || !(memo.elementsHandle == elementsHandle) || + memo.elementsSerial != elementsSerial || activeAttribMask != memo.activeMask) { + Uint32 pending = 0; + for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) { + const Uint32 location = static_cast(std::countr_zero(remaining)); + // rec->Attributes[] IS what was last pushed for this configuration; the + // record carries all 32 slots and zeroes the tail + // (MGPipeApplyCreateVertexElements), so a location past AttributeCount + // reads Enabled = 0, which is the same answer the frontend's cold + // attribute slot gave. + if (location >= MG_Pipe::kMGPipeMaxVertexAttribs || + !elementsRecord->Attributes[location].Enabled) { + pending |= (1u << location); + } + } + memo.elementsHandle = elementsHandle; + memo.elementsSerial = elementsSerial; + memo.activeMask = activeAttribMask; + memo.pendingMask = pending; + memo.valid = true; + } + // Plus the enabled arrays the walk had to disable (Managers.h, GetDisabledEnabledAttribMask). + const Uint32 pendingMask = memo.pendingMask | (vaoTwin->GetDisabledEnabledAttribMask() & memo.activeMask); + if (pendingMask == 0) return; + + for (Uint32 remaining = pendingMask; remaining != 0; remaining &= remaining - 1) { + const Uint32 location = static_cast(std::countr_zero(remaining)); + + const auto& currentValue = MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location); + const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(program->GetAttribType(location)); + switch (typeInfo.baseType) { + case MG_State::GLState::VertexAttribBaseType::Float: + g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data()); + break; + case MG_State::GLState::VertexAttribBaseType::Int: + g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data()); + break; + case MG_State::GLState::VertexAttribBaseType::Uint: + g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data()); + break; + case MG_State::GLState::VertexAttribBaseType::Unsupported: + MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its " + "shader input type 0x%x is not supported as a current generic vertex attribute", + program->GetExternalIndex(), location, program->GetAttribType(location)); + break; + } + } + } + + // P5e (pa), CONTRACT-P5E §5.5 / §5.8 (ruling ID-81): THE SAME SYNC, OFF THE RECORD. + // + // The overload above is the MONOLITH GLUE and keeps its text token for token; this one is + // what the handle arm calls, and it answers the note vi left inside it - "the two program + // reads below (GetActiveAttributeLocationMask, GetAttribType) ... stay until pg carries + // the attribute mask and types in the ShaderCso record". pg's archive carries both: + // `attribs` and `attribTypes` are members of LinkArtifacts, the record ADOPTS the whole of + // it at create_shader_state, and neither is one of the three post-link mutable fields the + // bindings tails exist for (glUniformBlockBinding / glUniform1i on a sampler / + // glShaderStorageBlockBinding move those, and nothing moves these without a relink, which + // re-issues the create). So this arm adds NOTHING to the wire and changes no record - it + // reads rows that already arrived. + // + // WHY A SECOND FUNCTION rather than an `#if` inside the body above: §5.8's idiom, and G1 - + // the frontend overload's preprocessed text does not move, so the pull build gains no + // symbol and no byte. The price is that vi's memo arm is COPIED rather than shared; a + // shared tail would have rewritten the pull build's one, which G1 measures at 0/0/0/0. + void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin, MG_Pipe::MGPipeHandle cso) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // THE SAME THREE-PART TEST SyncCurrentProgramByHandle MAKES, from the descriptor, and + // it is this arm's spelling of `if (!program) return;` above rather than a weaker one. + // LinkStatus is a FIELD and not an implication (ID-88): 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, so "a record exists" and "the program linked" are different + // statements. A program that did not link, or whose SPIR-V never arrived, is one the + // same Prepare has just bound program 0 for - there is no shader to feed a current + // generic attribute to, which is what the frontend arm gets from an unlinked + // program's empty `attribs`. + const MG_Pipe::MGPipeShaderCsoRecord* const record = PipeShaderCsoRecordForHandle(cso); + if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { + return; + } + // A NULL ARCHIVE ON THIS ARM IS A NAMED REFUSAL, NEVER A FALL-BACK TO THE FRONTEND - + // the shape of RefuseNullFrontendTextureOffTheHandleArm / MGB_TEXTURE_RECORD_ARM_- + // SELECTED (Managers.cpp, ID-110). This function is reached from ProgramHandleArm() + // only, i.e. Transport != Monolith AND the program subsystem bit, and under a + // transport pg fills Archive at every create_shader_state; so a LIVE record with a + // LINKED descriptor and no archive is a seam defect and not a state a draw may run + // in. Reaching back for GetProgramForDraw() here would answer the two rows out of the + // client's own live LinkArtifacts - the memory Link() replaces in place, i.e. exactly + // the read this package retires - and would do it behind a picture that still looks + // right, which is what the subsystem A/B exists to expose. + if (!record->Archive) { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"program-handle-arm\"} - " + "SyncCurrentVertexAttributeValues needs the active-attribute mask and the " + "attribute types of ShaderCso {%u, %u} and that record carries no " + "server-owned archive. The arm is selected by Transport != Monolith AND " + "the program subsystem bit (CONTRACT-P5E §5.8, ID-81), so this record's " + "create_shader_state was applied under a transport and must have adopted " + "one; the frontend ProgramObject is not a fall-back here, it IS the client " + "memory this row retires", + cso.Slot, cso.Gen); + std::abort(); + } + const MG_State::GLState::LinkArtifacts& link = record->Archive->Link; + if (!vaoTwin) return; + + // vi's half, unchanged in meaning and copied rather than shared (see above). The two + // family bits are independent A/Bs, so a program on the handle arm says nothing about + // whether the vertex-input family is - this arm has to carry both of vi's. + const MG_Pipe::MGPipeVertexElementsRecord* elementsRecord = nullptr; + MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle; + Uint64 elementsSerial = 0; + const auto& st = MG_Pipe::MGPipeApplier(); + elementsHandle = st.BoundVertexElements; + if (!MG_Pipe::MGPipeHandleIsNull(elementsHandle) && + elementsHandle.Slot < st.VertexElementsCsos.size()) { + const auto& elements = st.VertexElementsCsos[elementsHandle.Slot]; + if (elements.Live && elements.Gen == elementsHandle.Gen) { + elementsRecord = &elements; + elementsSerial = elements.ContentSerial; + } + } + if (elementsRecord == nullptr) return; + + const SharedPtr noVao; + const auto& vao = noVao; + + // ProgramObject::GetActiveAttributeLocationMask, over the archive's own `attribs`: + // the same 32-location bound and the same "an empty name is not an active attribute" + // rule, computed where the names live instead of through an accessor MG_Backend + // cannot reach without a frontend object. + Uint32 activeAttribMask = 0; + { + const SizeT attribCount = std::min(link.attribs.size(), 32); + for (SizeT index = 0; index < attribCount; ++index) { + if (!link.attribs[index].empty()) activeAttribMask |= (1u << index); + } + } + if (activeAttribMask == 0) return; + + auto& memo = vaoTwin->GetPendingAttribValueMaskMemo(); + if (!memo.valid || !(memo.elementsHandle == elementsHandle) || + memo.elementsSerial != elementsSerial || activeAttribMask != memo.activeMask) { + Uint32 pending = 0; + for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) { + const Uint32 location = static_cast(std::countr_zero(remaining)); + if (location >= MG_Pipe::kMGPipeMaxVertexAttribs || + !elementsRecord->Attributes[location].Enabled) { + pending |= (1u << location); + } + } + memo.elementsHandle = elementsHandle; + memo.elementsSerial = elementsSerial; + memo.activeMask = activeAttribMask; + memo.pendingMask = pending; + memo.valid = true; + } + // Plus the enabled arrays the walk had to disable (Managers.h, GetDisabledEnabledAttribMask). + const Uint32 pendingMask = memo.pendingMask | (vaoTwin->GetDisabledEnabledAttribMask() & memo.activeMask); + if (pendingMask == 0) return; + + for (Uint32 remaining = pendingMask; remaining != 0; remaining &= remaining - 1) { + const Uint32 location = static_cast(std::countr_zero(remaining)); + + // BOUNDS-CHECKED, exactly as ProgramObject::GetAttribType is not: the frontend's + // form indexes attribTypes raw because the mask it walked came off the same + // artefacts instance, and here the two vectors arrived over a wire. A short tail + // is an archive whose halves disagree; 0 classifies as Unsupported and says so + // below rather than reading past the end. + const GLenum attribType = + location < link.attribTypes.size() ? link.attribTypes[location] : 0; + const auto& currentValue = MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location); + const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(attribType); + switch (typeInfo.baseType) { + case MG_State::GLState::VertexAttribBaseType::Float: + g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data()); + break; + case MG_State::GLState::VertexAttribBaseType::Int: + g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data()); + break; + case MG_State::GLState::VertexAttribBaseType::Uint: + g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data()); + break; + case MG_State::GLState::VertexAttribBaseType::Unsupported: + // THE HANDLE, NOT A GL NAME: the server does not know the client's program + // names and must not learn them (ProgramArchiveSource::Identity states the + // same rule for the twin's log lines). + MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: ShaderCso {%u, %u} location=%u has no " + "enabled array and its shader input type 0x%x is not supported as a current " + "generic vertex attribute", + cso.Slot, cso.Gen, location, attribType); + break; + } + } + } + } // namespace VertexArrayImpl + + namespace TextureImpl { + static Uint g_imageSweepBackendContextGeneration = 0; + static Bool g_imageSweepValid = false; + // P5e (fb, ruling 7 / ID-86): the record arm's three applier serials. Held in their own + // fields beside the frontend pair rather than reusing them, for the reason the two + // framebuffer list keys are held apart - one field carrying two key shapes is how a + // stale half gets compared against a live one. + static Uint64 g_imageSweepShaderImagesSerial = 0; + static Uint64 g_imageSweepTextureShutterSerial = 0; + static Uint64 g_imageSweepContextSerial = 0; + + // The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the + // frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion + // moves on exactly the shape and format changes that can force the re-mint. Deliberately + // NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be + // the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside + // this very sweep, so a backend-side trigger would be bumped after the gate had already + // declined to run it. + // + // P4a e3 (D-G4) leaves BOTH properties of this gate exactly as they are, and says so + // because they are the kind of thing a later optimisation deletes: + // + // 1. the g_imageUnitHighWaterMark == 0 early-out is what makes every Minecraft draw + // pay one integer test, and it is asked BEFORE anything reads a record; + // 2. the gate key stays the FRONTEND sampling-resolution generation and is + // deliberately not re-keyed onto a server-owned serial, for the reason above: a + // texture bound only to an image unit is re-minted INSIDE this sweep, so an epoch + // the server bumps would move after the gate had already declined to run it. The + // client's own NewShaderImages shutter mixes the same three frontend counters + // (texture content, texture params, the program's image-unit version), so the + // property is preserved on both sides by construction rather than by agreement. + // + // P5e (fb, ruling 7 / ID-86): PROPERTY 1 IS UNTOUCHED. PROPERTY 2 IS RE-KEYED, and the + // argument behind it is kept rather than dropped - it is what decides WHICH four values + // the new key may be made of. + // + // The frontend sampling-resolution generation cannot be read on an apply thread, so the + // key becomes (ShaderImagesSerial, TextureShutterSerial, ContextSerial, + // g_backendContextGeneration). Three of those are APPLIER-DERIVED - they move when the + // client's own shutters send a new set of images, new texture content or new texture + // parameters, i.e. at exactly the moments the frontend counter moved - and the fourth + // is the backend's ES-context generation, which the old key already carried. + // + // NONE OF THEM IS A BACKEND RE-MINT COUNTER, and that is the whole of the ruling: the + // hazard the old comment names is a texture bound ONLY to an image unit being re-minted + // INSIDE this sweep, which would bump a backend-side epoch after the gate had already + // declined to run. An applier serial moves when the CLIENT said something, strictly + // before the sweep the saying provoked, so the property survives the re-key intact. + // g_backendContextGeneration is not a counter of that kind either - it moves when the + // ES context is rebuilt, which no sweep does. + // + // THE FOUR SERIALS SAY NOTHING ABOUT CONTENT, so the key carries each image texture's + // content serial as well. A glTexSubImage* into a texture that is bound ONLY to an image + // unit moves that texture's record Serial and none of the four: the upload stays in the + // record's pending set, and nothing else flushes it before the draw - the sampled-unit + // walk and the attachment lists never visit an image-only texture. The draw then + // imageStore()s into the driver texture, and the stale upload lands on top of those + // stores at the next sync that does visit the texture (a readback), which is the + // ordering the application asked for in reverse (KHR-GL42.shader_image_load_store. + // early-fragment-tests, which zeroes its image before every draw). + // + // Per unit, keyed on the bound HANDLE and its record's Serial: the handle carries the + // slot's generation, so a recycled slot cannot answer with its predecessor's serial, and + // the record Serial is the server's own monotone content version. The walk is bounded by + // the high-water mark, so it costs nothing on the draws the first early-out already + // returns from. + struct ImageSweepUnitContent { + MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; + Uint64 Serial = 0; + }; + static Array g_imageSweepUnitContent{}; + static Uint32 g_imageSweepUnitContentCount = 0; + + static Uint32 ImageSweepUnitContentCount(const MG_Pipe::MGPipeApplierState& st) { + return std::min(static_cast(g_imageUnitHighWaterMark), + static_cast(st.BoundShaderImages.size())); + } + + static Uint64 ImageUnitContentSerial(MG_Pipe::MGPipeHandle res) { + if (MG_Pipe::MGPipeHandleIsNull(res)) return 0; + const auto* record = PipeTextureRecordForHandle(res); + return record != nullptr ? record->Serial : 0; + } + + static Bool ImageSweepContentUnchanged(const MG_Pipe::MGPipeApplierState& st) { + const Uint32 count = ImageSweepUnitContentCount(st); + if (count != g_imageSweepUnitContentCount) return false; + for (Uint32 unit = 0; unit < count; ++unit) { + const MG_Pipe::MGPipeHandle res = st.BoundShaderImages[unit].Res; + const auto& memo = g_imageSweepUnitContent[unit]; + if (!(memo.Res == res) || memo.Serial != ImageUnitContentSerial(res)) return false; + } + return true; + } + + static void StampImageSweepContent(const MG_Pipe::MGPipeApplierState& st) { + const Uint32 count = ImageSweepUnitContentCount(st); + for (Uint32 unit = 0; unit < count; ++unit) { + const MG_Pipe::MGPipeHandle res = st.BoundShaderImages[unit].Res; + g_imageSweepUnitContent[unit] = {res, ImageUnitContentSerial(res)}; + } + g_imageSweepUnitContentCount = count; + } + + void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) { + if (g_imageUnitHighWaterMark == 0) return; + const auto& st = MG_Pipe::MGPipeApplier(); + if (g_imageSweepValid && g_imageSweepShaderImagesSerial == st.ShaderImagesSerial && + g_imageSweepTextureShutterSerial == st.TextureShutterSerial && + g_imageSweepContextSerial == st.ContextSerial && + g_imageSweepBackendContextGeneration == g_backendContextGeneration && + ImageSweepContentUnchanged(st)) { + return; + } + SyncImageTextureBindings(); + // The sweep may just have landed a re-upload into a texture the draw reaches through + // an image unit (the content clause above is one of the ways it gets here). + if (const GLbitfield barriers = TakeInPlaceTextureUploadBarrierBits(); + barriers != 0 && g_GLESFuncs.glMemoryBarrier) { + g_GLESFuncs.glMemoryBarrier(barriers); + } + g_imageSweepShaderImagesSerial = st.ShaderImagesSerial; + g_imageSweepTextureShutterSerial = st.TextureShutterSerial; + g_imageSweepContextSerial = st.ContextSerial; + g_imageSweepBackendContextGeneration = g_backendContextGeneration; + // After the sweep: it is the sync that consumed the pending set, and it may raise the + // high-water mark the walk is bounded by. + StampImageSweepContent(st); + g_imageSweepValid = true; + return; + } + } // namespace TextureImpl + + void BindCurrentFBO(FramebufferTarget target) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // The handle arm reads the record instead of the binding slot, which is one of the + // five GetFramebufferBindingSlotChecked call sites this phase retires. Everything the + // pre-handle arm below is careful about is careful here for the same reasons and in + // the same order: NO FAST PATH on any version (the skip is BindFramebufferId's job and + // its shadow is where the cost actually is), and the DEFAULT framebuffer is bound + // THROUGH THE SHADOW, never raw. What changes is only where the two questions - "which + // framebuffer" and "is it the default one" - are answered: the record's Fbo and its + // IsDefault byte, rather than the bound object's address and a comparison against + // pDefaultFramebufferInfo->defaultFBO. + if (FramebufferImpl::g_fboRecordsTrusted) { + const MG_Pipe::MGPFramebufferState* const recordPtr = BoundFramebufferRecord(target); + // The record's own handle is the "has this binding ever been described" test, not + // FramebufferSerial - which MGPipeApplierReset advances whether or not anything + // was ever emitted (see SyncCurrentFBOByRecord). The trust latch above is the + // other half: it says the sync that ran a moment ago found these two records + // describing these two bindings. + // P4a decline-site F5: FLIPPED AT THE VERIFICATION ROUND to an assertion, same + // argument and same shape as F7 above - the trust latch implies a record with a + // non-null handle on both targets, and a silent decline here would look exactly + // like a legitimate one. + MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)); + if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) { + const MG_Pipe::MGPFramebufferState& record = *recordPtr; + if (record.IsDefault == 0) { + auto* twinEntry = FramebufferImpl::g_backendFramebufferObjects.FindByHandle(record.Fbo); + // P4a decline-site F6: S - already loud, and it KEPT this shape at the + // verification round: binding nothing is exactly what the pre-handle arm + // does in the same situation, so the two arms stay at parity. + if (twinEntry && *twinEntry) { + (*twinEntry)->Bind(target); + } else { + MGLOG_E_ONCE( + "No backend FBO found (maybe not synced) for the current %s FBO record, " + "cannot bind FBO.", + (target == FramebufferTarget::Read ? "READ" : "DRAW")); + } + } else { + MGLOG_D("Binding default framebuffer as %s FBO", + (target == FramebufferTarget::Read ? "READ" : "DRAW")); + if (FramebufferImpl::CurrentFramebufferBinding(target) != 0) FlushAheadOfWindowBufferUse(); + // Through the shadow: a raw bind here would leave the shadow claiming + // the previous user FBO, false-skipping its next re-bind. + FramebufferImpl::BindFramebufferId( + target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); + } + return; + } + } + // P5e (fb, §5.4): the pre-handle arm below reads the binding slot, so under a transport + // reaching it is the same refusal SyncCurrentFBO takes. Reaching it means either the + // sync's trust latch is clear - and the sync aborted before it could be - or this bind + // ran with no sync in front of it, which the latch exists to catch. + RefuseFramebufferBindingSlotRead(); + } + + void PrepareForDraw(DrawSyncFlags syncBit) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // One twin resolve serves the whole draw: the buffer sync (which hosts the + // resolved-buffers memo on the twin), the VAO sync and the draw-time bind + // below. Nothing in between can invalidate it — the bound VAO is pinned by + // the context, and no step here erases or replaces a live VAO's twin. + // + // P5e (vi), CONTRACT-P5E §4.2 / §5.1: under a transport the twin is resolved from + // st.BoundVertexElements - THE SAME HANDLE the frontend probe used to mint, since + // EmitVertexElements acquires it off the VAO's own lifetime id - and neither the bound + // VAO row nor the configuration version is read at all. `currentVAO` stays null on that + // arm, which is what the two callees' record arms expect; the null twin case (no + // vertex-elements CSO bound) reaches the same BindBackendVAOId(0) below that a null + // frontend VAO always did. + const SharedPtr noFrontendVao; + const auto& currentVAO = noFrontendVao; + VertexArrayImpl::BackendVertexArrayObject* vaoTwin = nullptr; + Uint32 vaoConfigVersion = 0; + vaoTwin = VertexArrayImpl::ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements); + if (vaoTwin != nullptr) vaoTwin->AdoptUnchangedBufferSets(MG_Pipe::MGPipeApplier()); + // One program resolve and one texture-key capture serve the whole draw, for + // the same reason the twin resolve does: only frontend GL entry points move + // either, and none can run inside this preparation. GetProgramForDraw is a + // cross-TU call with a guarded static inside - repeating it per stage showed + // up in draw-loop profiles. + // + // P5e (pa), kimi row 91 / S4 1.1: AND ON THE RECORD ARM IT IS NOT MADE AT ALL. The row + // this package retires is this single call - it is what MGP_INPUT_CHECK trips on, once + // per draw, and it is 61 of the strict lane's 109 red entries. The arm is + // DrawProgramFromRecords(), which names every consumer of the value rather than this + // family alone; `currentProgram` stays null on it and the four callees below take their + // own handle arms, none of which consults it. The null twin case (no program bound) + // reaches the same "nothing to bind" answers a null frontend program always did. + const SharedPtr noFrontendProgram; + const auto& currentProgram = noFrontendProgram; + if (MG_Util::PipeStats::Enabled()) { + // THE per-draw denominator for Espryt, plus this function's own two accessor + // calls (the VAO and the draw program). Everything the callees below read is + // counted by the callees that are instrumented; the rest is not counted (see + // the inventory in PipeStats.cpp). + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); + // ZERO on the record arm, one per row still pulled otherwise: P5e (vi) retired the + // bound-VAO accessor call and P5e (pa) the draw-program one, and the ledger has to + // say so or the phase's own "accessor calls per draw" number would keep counting + // reads that no longer happen. Two independent bits, so it is a sum and not a + // three-way pick. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, + (0) + (0)); + } + const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys(); + + BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion, + syncBit & DrawSyncBit::IndexBuffer, + syncBit & DrawSyncBit::IndirectBuffer); + VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin); + TextureImpl::SyncNeccessaryTextures(textureKeys); + // A draw reads and writes through its image units too, so the unit bindings have to be + // as current as the sampled ones. Gated (see the sweep): a program with no image binding + // pays one integer test, and one with images re-issues them only when a texture shape + // moved under them. + TextureImpl::SyncImageTextureBindingsForDraw(textureKeys); + // A draw writes through its image units too - the conformance case that found this + // stores into a buffer texture from the FRAGMENT stage, not from a dispatch. + TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); + FramebufferImpl::SyncCurrentFBO(); + // P5e (pg), kimi row 91 / S4 1.1: the SharedPtr pull above is the row this family + // retires. On the handle arm the program is MGPipeApplier().DrawProgram - a handle the + // set_draw_program record carried - and `currentProgram` is not consulted for the sync + // at all; the remaining uses of it below are the two callees' own, and both have a + // handle arm of their own. P5e (pa) finished it: the pull itself is gone on that arm and + // `currentProgram` is a null SharedPtr there, which this branch never reaches for. + PrgramImpl::SyncCurrentProgramByHandle(MG_Pipe::MGPipeApplier().DrawProgram, true); + RenderStateImpl::SyncRenderState(); + + BindCurrentFBO(FramebufferTarget::Draw); + + { +#ifdef TRACY_ENABLE + ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND); +#endif + if (vaoTwin) { + vaoTwin->Bind(); + } else { + VertexArrayImpl::BindBackendVAOId(0); + } + } + + // P5e (pa): the third consumer, on ITS OWN arm and not on the hoist's. The hoist above is + // a conjunction over four callees; this one is served by the record whenever the PROGRAM + // family is on the handle arm, which includes the mixed A/B where the texture bits are + // off and `currentProgram` above is therefore still a real object. + VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, MG_Pipe::MGPipeApplier().DrawProgram); + + BindCurrentTextures(textureKeys, currentProgram); + BindCurrentProgramWithResources(currentProgram, textureKeys, + MG_Pipe::MGPipeApplier().DrawProgram + ); + + // Last: opening the capture span needs the program current and the capture + // buffers bound, and ES rejects most binding changes once it is open. + XfbImpl::StartPendingTransformFeedback(); + } + + // Resolves every frontend texture unit's textures onto the backend context. Returns + // false when the resolution could not be completed from the state it read - a bound + // texture that has no backend object yet is skipped, and a later draw would bind it + // without any of the memo keys below moving - so the caller must not memoise it. + // + // P5e (pa): `currentProgram` IS THE LEGACY WALK'S PARAMETER and is null under + // TextureImpl::UnitTexturesByHandle(), where the by-handle pass below returns before the + // only reader of it (sampledTargetForUnit) is even declared. The caller drops the object + // only once that same test holds, so this is the arm speaking and not a pointer test. + Bool ResolveAndBindUnitTextures(const SharedPtr& currentProgram, + Int maxTouchedUnit) { +#ifdef TRACY_ENABLE + ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND); +#endif + Bool fullyResolved = true; + // P5e (tx2), CONTRACT-P5E §5.2. ONE PASS OVER THE SAMPLER-VIEW WINDOW, AND THEN AN + // UNBIND OF EVERY TARGET THE WINDOW DID NOT CLAIM. + // + // The three-pass slot walk below exists to arbitrate: desktop 1D/1D-array targets alias + // ES 2D/2D-array, a unit can hold a real texture on one of an aliased pair and a default + // object on the other, and two REAL textures can want one native target - which the + // program's sampler types decide. THE CLIENT HAS ALREADY DECIDED ALL OF IT + // (SamplerEmit.h's per-unit resolution is keyed on exactly those sampler types), so the + // arbiter dies here rather than being re-implemented over records: one texture per unit, + // named by the record, with its frontend target in the view CSO. + // + // The unbind half keeps its shape and changes its driver: instead of walking the frontend + // unit's binding slots it walks the fixed TextureTarget enum against g_boundTexturesCache, + // which is SERVER memory and is the shadow every redundant-bind filter in this backend + // already trusts. That is what makes "the window did not claim it" mean "unbind it" - the + // same statement the pre-handle pass makes with `!boundBackendTargets[...]`. + const auto& st = MG_Pipe::MGPipeApplier(); + const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount; + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { + const Uint32 index = static_cast(unit); + Array boundBackendTargets{}; + if (index >= st.SamplerViewStart && index < windowEnd && index < st.BoundSamplerViews.size()) { + const auto& view = st.BoundSamplerViews[index]; + if (!MG_Pipe::MGPipeHandleIsNull(view.Texture)) { + // The TARGET is the view CSO's, which is the frontend target the client + // resolved this unit against - not a second derivation from the texture's + // descriptor, which would be a second authority for one value. + const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); + const auto target = viewRecord != nullptr + ? static_cast(viewRecord->View.Target) + : TextureTarget::Unknown; + if (viewRecord == nullptr) { + MGLOG_E_ONCE("MGPipe: unit %d's sampler view {%u, %u} has no applier record, " + "so the target to bind texture {%u, %u} at is unknown; the unit " + "keeps what it holds", + static_cast(unit), view.View.Slot, view.View.Gen, + view.Texture.Slot, view.Texture.Gen); + fullyResolved = false; + } else if (!TextureImpl::IsSupportedTextureTarget(target)) { + MGLOG_D(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(target).c_str()); + } else if (auto* twin = TextureImpl::ResolveTextureTwin(view.Texture)) { + const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + twin->Bind(targetGL, unit); + boundBackendTargets[static_cast( + TextureImpl::MapToBackendTextureTarget(target))] = true; + } else { + // No twin yet: the sync pass that would have built it declined and + // named why. Not memoisable - a later draw would bind it without any + // key moving. + fullyResolved = false; + } + } + } + // The backend half of glBindTexture(..., 0) for every native target this unit no + // longer claims, driven by the server's own binding shadow. + Array visitedBackendTargets{}; + for (SizeT t = 0; t < (SizeT)TextureTarget::TextureTargetCount; ++t) { + const auto target = static_cast(t); + if (!TextureImpl::IsSupportedTextureTarget(target)) continue; + const auto backendTargetIndex = + static_cast(TextureImpl::MapToBackendTextureTarget(target)); + if (visitedBackendTargets[backendTargetIndex]) continue; + visitedBackendTargets[backendTargetIndex] = true; + if (boundBackendTargets[backendTargetIndex]) continue; + if (TextureImpl::g_boundTexturesCache[static_cast(unit)][backendTargetIndex] == nullptr) { + continue; + } + TextureImpl::UnbindTexture(unit, TextureImpl::ConvertTextureTargetToBackendGLEnum(target)); + } + } + return fullyResolved; + } + + // Per-unit memo of the sampler-registry lookup, NOT of the resulting unit binding + // (BackendSamplerObject::Bind already dedups against g_boundSamplersCache). The registry + // pairs a live frontend sampler with one backend object for the frontend's whole + // lifetime: GetOrCreate replaces a pairing only when the frontend expired, entries are + // erased only when the frontend expired, and a unit's sampler cannot expire while the + // unit holds it - so owner-equality of the unit's current sampler against the snapshot + // proves the cached pointer is exactly what Find would return. A miss result is never + // cached: the backend object may not exist yet when the unit pass runs (the program + // pass creates it later in the same draw), and a cached miss would keep skipping the + // bind after it appears. + struct UnitSamplerLookupMemo { + // The {slot, gen} of the frontend sampler this row was resolved for. It replaces the + // weak_ptr and its owner compare: a stale row cannot match, because the successor of a + // freed sampler is handed the same slot only with a higher Gen. + MG_Pipe::MGPipeHandle frontendHandle = MG_Pipe::kMGPipeNullHandle; + SamplerImpl::BackendSamplerObject* backend = nullptr; + }; + static Array + g_unitSamplerLookupMemos; + + // Puts each touched unit's frontend sampler object on the backend unit. Not part + // of the texture memo below (BindCurrentProgramWithResources may rewrite sampled + // units' samplers right after this runs - raw-depth-fetch substitution, + // per-program sampler objects); instead the walk carries its own memo: + // + // The walk is a pure function of WHICH sampler object each touched unit holds - + // covered by (context id, unit-bindings epoch, high-water mark; the epoch + // snapshot includes each unit's sampler object and bumps when the mark moves) - + // and replaying it as "do nothing" additionally requires the sampler bindings it + // left to still be on the driver, which the row compare against + // g_boundSamplersCache (the shadow every sampler bind routes through) proves. + // Any later writer - the program pass substituting a raw-depth sampler, a + // frontend sampler bind/unbind (epoch), the ES-context generation moving - lands + // in one of those inputs. A unit whose sampler had no backend object yet leaves + // its row untouched; the object is only created by the program pass, whose Bind + // moves the row and thereby re-opens this memo, so the miss cannot be latched. + Uint64 g_unitSamplerWalkContextId = 0; + Uint64 g_unitSamplerWalkEpoch = 0; + Int g_unitSamplerWalkMaxUnit = -1; + Uint g_unitSamplerWalkContextGeneration = 0; + Bool g_unitSamplerWalkValid = false; + static decltype(SamplerImpl::g_boundSamplersCache) g_unitSamplerWalkRows{}; + + void BindCurrentUnitSamplers(const TextureImpl::DrawTextureSyncKeys& keys) { + const Int maxTouchedUnit = keys.maxTouchedUnit; + const SizeT rowBytes = + static_cast(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]); + if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId && + g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit && + g_unitSamplerWalkContextGeneration == g_backendContextGeneration && + std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) { + return; + } + g_unitSamplerWalkValid = false; + Bool walkedFromRecords = false; + // P4a e4 (D-G2). THE WALK ITSELF READS THE APPLIER'S PER-UNIT HANDLE ARRAY. + // + // bind_sampler_states is the resolved answer to exactly the question this loop asks - + // which sampler CSO does each touched unit carry - so the handle IS the lookup: no + // GetTextureUnitObject per unit, no lifetime-id probe, and no UnitSamplerLookupMemo + // row to keep, because FindByHandle is already one array index. The memo below stays + // for the arm that still resolves through a frontend object. + // + // A NULL HANDLE MEANS "this unit has no sampler object", which is the record's way of + // saying what the frontend's null SharedPtr says, and it unbinds - the texture's own + // built-in sampler then applies, exactly as today. A live handle resolves to THE CSO's + // OWN TWIN (P4a fable seam F-4, SamplerImpl::ResolveSamplerCsoTwin): the sentence that + // stood here - "a handle whose twin does not exist yet is left alone ... the program + // pass creates the twin later in the same draw" - described a lookup that could never + // hit, because the handle is content-addressed and the registry's twins were minted off + // lifetime ids, so this arm bound nothing on every draw and only the pre-handle program + // pass ever put a glBindSampler'd object on the driver. The program pass now binds the + // same CSO twin for the units it samples, which is what stops the two arms ping-ponging + // a unit between two driver samplers. + // + // Declines - falls through to the frontend walk - until the set has arrived, and the + // COUNT says so rather than the serial (MGPipeApplierReset advances serials whether or + // not anything was emitted). + // P4a decline-site S1: M - the mask says this family is not switched on, and it IS + // D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification round. + const auto& st = MG_Pipe::MGPipeApplier(); + // P4a decline-site S2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, then + // the frontend walk. Bit 11 is on (the enclosing SamplerSubsystemEnabled()) and + // package C has landed, so a draw that touches texture units with no + // bind_sampler_states ever applied is a seam defect. The frontend walk still + // runs: it answers the same question correctly, and what this line exists to stop + // is the SILENT permanent fallback, not the fallback. + // NARROWED BY THE FIRST REAL-PATH RUN, exactly as T2 was and for the same + // reason: maxTouchedUnit < 0 is a draw that touches no texture unit, for which an + // empty window is the correct emission. All 161 hits of the unnarrowed line were + // that shape - the log said `units 0..-1` on every one of them. + if (st.SamplerStateCount == 0 && maxTouchedUnit >= 0) { + // P5e (tx2), CONTRACT-P5E §5.3: the same refusal CurrentUnitBindingsEpoch makes - + // the frontend walk below reads GetTextureUnitObject per unit, which an active + // transport may not do for a record whose client has moved on. + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - a draw " + "touches units 0..%d and no bind_sampler_states has ever been applied " + "while the sampler subsystem bit is set; the pre-handle sampler walk " + "reads client memory and is refused under an active transport " + "(CONTRACT-P5E.md §5.3)", + static_cast(maxTouchedUnit)); + std::abort(); + MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw touches " + "units 0..%d and no bind_sampler_states has ever been applied while the " + "sampler subsystem bit is set; running the pre-handle sampler walk.", + static_cast(maxTouchedUnit)); + } + if (st.SamplerStateCount != 0) { + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { + const Uint32 index = static_cast(unit); + // P4a decline-site S3: the OUT-OF-WINDOW UNBIND (MAJOR-2, fixed here); no + // flip remains at the verification round, only the A8 measurement below. + // + // A UNIT THE RECORD DOES NOT DESCRIBE IS UNBOUND, NOT SKIPPED. The var-tail + // window IS the bound for RECORD RETENTION - entries outside it are not + // cleared in the applier - but this loop is not reading records, it is + // writing DRIVER state, and for a driver binding "no record" means "no + // sampler object", which is exactly what the pre-handle arm's + // `else { UnbindSampler(unit); }` below says and why it was written: a + // sampler object left on the unit by an earlier draw keeps being applied, + // and on a multisample texture - which takes no sampler object at all - the + // draw is rejected outright. `continue` here reintroduced that defect on + // the new arm for every unit below SamplerStateStart or above the window, + // which is the same smaller-window direction DEV-3 refused. + // + // A8 again: nothing in the contract pins the window to cover + // [0, maxTouchedUnit]. If package C documents that it does, this branch + // becomes unreachable and can be asserted rather than executed; until then + // it is load-bearing, and the verification round measures + // (SamplerStateStart, SamplerStateCount) against maxTouchedUnit to say so. + if (index < st.SamplerStateStart || index - st.SamplerStateStart >= st.SamplerStateCount) { + SamplerImpl::UnbindSampler(unit); + continue; + } + const MG_Pipe::MGPipeHandle sampler = st.BoundSamplerStates[index]; + if (MG_Pipe::MGPipeHandleIsNull(sampler)) { + SamplerImpl::UnbindSampler(unit); + continue; + } + // P4a decline-site S4, RETIRED at the fable seam round (F-4): what stood here + // was `g_backendSamplerObjects.FindByHandle(sampler)` - an identity-keyed + // table asked for a content-addressed handle, a miss by construction on + // every draw - with a comment that read the miss as a within-draw ordering + // fact. The twin is the CSO's own now; a null answer has already named its + // reason (no record, or a slot that cannot be adopted) and leaves the unit + // as it is, which is the one decline this arm still has. + if (auto* twin = SamplerImpl::ResolveSamplerCsoTwin(sampler)) { + twin->Bind(unit); + } + } + walkedFromRecords = true; + } + if (!walkedFromRecords) { + // P13: no bind_sampler_states window applied, i.e. no unit holds a sampler object - + // the record arm emits one for every unit that does. Unbind, so a sampler object + // left on a unit by an earlier draw is not applied to this one. + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) SamplerImpl::UnbindSampler(unit); + } + g_unitSamplerWalkContextId = keys.contextId; + g_unitSamplerWalkEpoch = keys.unitBindingsEpoch; + g_unitSamplerWalkMaxUnit = maxTouchedUnit; + g_unitSamplerWalkContextGeneration = g_backendContextGeneration; + std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes); + g_unitSamplerWalkValid = true; + } + // Small per-PROGRAM memo set, not one global: the program is part of the key + // (it arbitrates aliased native targets), so a frame that cycles a handful of + // programs over UNCHANGED unit bindings - Sodium issues 62 glUseProgram per + // frame - invalidated a single memo on every switch and re-ran the full + // three-pass slot walk for an answer that had not moved. Entries are selected + // by raw program pointer; everything a recycled address could confuse is + // caught by the full key compare (lifetime id) exactly as before. Round-robin + // eviction; 4 entries cover the shader set a Minecraft-shaped frame cycles. + Array g_resolvedTextureBindingMemos; + Uint32 g_resolvedTextureBindingMemoCursor = 0; + + // Rebinds every frontend texture unit's textures (and sampler objects) on the + // backend context. Needed before draws AND compute dispatches: content syncs + // (SyncTextureObjectToBackend) bind scratch textures on the active unit as a + // side effect, so unit bindings must be re-established afterwards or shaders + // sample whatever texture the last sync left behind (e.g. Flywheel's depth + // pyramid downsample reading a stale unit-0 binding instead of the depth + // attachment). + // + // P5e (pa): `currentProgram` IS NULL ON THE HANDLE ARM, and that is a statement about the + // caller's arm rather than something this body may test for. Every read of it below sits on + // the `byHandle == false` side of a branch whose condition is TextureImpl::UnitTexturesByHandle() + // - the memo's entry selection, the four frontend key rows, and ResolveAndBindUnitTextures' + // sampledTargetForUnit lambda, which its own handle arm returns before reaching. The hoist in + // PrepareForDraw only drops the object once THAT test holds too (DrawProgramFromRecords), so + // this parameter is null exactly where nothing consults it. + void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, + const SharedPtr& currentProgram) { +#ifdef TRACY_ENABLE + ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); +#endif + // Units past the frontend's high-water mark have provably-empty slots. + const Int maxTouchedUnit = keys.maxTouchedUnit; + + // P5e (tx2) / ruling 6: on the handle arm the key is the applier's rows and nothing + // frontend - INCLUDING the entry selection, which used the frontend program's address. + // maxTouchedUnit stays in it because the unbind sweep this memo replays is bounded by it. + MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle; + Uint64 shaderCsoSerial = 0; + Uint64 bindingsSerial = 0; + Uint64 samplerViewsSerial = 0; + Uint64 contextSerial = 0; + const auto& st = MG_Pipe::MGPipeApplier(); + drawProgram = st.DrawProgram; + samplerViewsSerial = st.SamplerViewsSerial; + contextSerial = st.ContextSerial; + if (const auto* cso = PipeShaderCsoRecordForHandle(drawProgram)) { + shaderCsoSerial = cso->Serial; + bindingsSerial = cso->BindingsSerial; + } + // Entry selection by program pointer; a missing program takes the round-robin + // victim. WHICH entry is used is only a performance choice - correctness sits + // entirely in the full key + shadow compare below, unchanged from the single + // memo this set replaces. + // + // P5e (pa): NOT EVEN THE POINTER, on the handle arm. The by-handle branch below selects + // and keys on {DrawProgram, ShaderCso.Serial, BindingsSerial} (ruling ID-86) and never + // looks at this value, but it was still being loaded out of the caller's SharedPtr every + // draw - and once the caller stops holding one there is nothing there to load. + const void* programKey = + nullptr; + ResolvedTextureBindingMemo* memoSlot = nullptr; + for (auto& candidate : g_resolvedTextureBindingMemos) { + if (candidate.valid && candidate.drawProgram.Slot == drawProgram.Slot && + candidate.drawProgram.Gen == drawProgram.Gen) { + memoSlot = &candidate; + break; + } + continue; + } + if (!memoSlot) { + g_resolvedTextureBindingMemoCursor = + (g_resolvedTextureBindingMemoCursor + 1) % g_resolvedTextureBindingMemos.size(); + memoSlot = &g_resolvedTextureBindingMemos[g_resolvedTextureBindingMemoCursor]; + } + auto& memo = *memoSlot; + const SizeT shadowBytes = + static_cast(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]); + const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch; + // P5e (ID-124): the legacy key is a LAZY LAMBDA, not an eagerly-computed Bool. It was + // the else-arm of a ternary whose declaration sat inside `#if MOBILEGL_BUILD_DISAGGREGATED` + // while the arm itself and every use of `keysMatch` sat outside it, so neither + // non-disaggregated flavour compiled. Hoisting it to an eager Bool would have been + // WORSE than the build break: it reads `currentProgram`, which is null on the handle + // arm, so the laziness the ternary gave for free is load-bearing (ID-81 / ID-110 - + // the arm decides, and the other arm's reads must not happen at all). + const auto legacyKeysMatch = [&]() -> Bool { + return (memo.valid && memo.glContextId == keys.contextId && + memo.maxTouchedUnit == maxTouchedUnit && + memo.unitBindingsEpoch == unitBindingsEpoch && + memo.samplingResolutionGeneration == keys.samplingGeneration && + memo.program == programKey && + memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) && + memo.programBackendStateVersion == + (currentProgram ? currentProgram->GetBackendStateVersion() : 0) && + memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) && + memo.contextGeneration == g_backendContextGeneration); + }; + const Bool keysMatch = + (memo.valid && memo.maxTouchedUnit == maxTouchedUnit && + memo.contextSerial == contextSerial && memo.samplerViewsSerial == samplerViewsSerial && + memo.drawProgram.Slot == drawProgram.Slot && memo.drawProgram.Gen == drawProgram.Gen && + memo.shaderCsoSerial == shaderCsoSerial && memo.bindingsSerial == bindingsSerial && + memo.contextGeneration == g_backendContextGeneration); + // Short-circuited: the shadow compare is only meaningful once the key (and with it the + // snapshotted row count) matches. + if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), + shadowBytes) != 0) { + memo.valid = false; + if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) { + memo.glContextId = keys.contextId; + memo.maxTouchedUnit = maxTouchedUnit; + memo.unitBindingsEpoch = unitBindingsEpoch; + memo.samplingResolutionGeneration = keys.samplingGeneration; + memo.program = programKey; + // P5e (pa): the four frontend program rows belong to the LEGACY key and are not + // written on the handle arm - decided by `byHandle`, which is the arm, and not by + // the object happening to be null, which is only a consequence of it. They are + // zeroed rather than skipped so an entry cannot carry a row nobody wrote. + memo.programLifetimeId = 0; + memo.programBackendStateVersion = 0; + memo.programLinked = false; + memo.contextGeneration = g_backendContextGeneration; + memo.drawProgram = drawProgram; + memo.shaderCsoSerial = shaderCsoSerial; + memo.bindingsSerial = bindingsSerial; + memo.samplerViewsSerial = samplerViewsSerial; + memo.contextSerial = contextSerial; + std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes); + memo.valid = true; + } + } + + BindCurrentUnitSamplers(keys); + } + + void BindCurrentTextures() { + // P5e (pa): the exported no-argument entry (DirectGLES.h) takes the SAME arm the hoist in + // PrepareForDraw does, and for the same reason: the pull is what trips the input check, so + // an entry point that keeps it would keep the row alive for whichever caller reaches this + // overload next. Today that is SanityTest's unit cases, which run under monolith + // transport and therefore take the frontend arm below unchanged. + const SharedPtr noFrontendProgram; + BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), + noFrontendProgram); + } + + // Binds the current program's backend object and re-establishes its per-program + // resources: global UBO contents, uniform-block bindings, and sampler uniform + // units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the + // association must be rebuilt through the API). Compute dispatches depend on + // this as much as draws do — e.g. Flywheel's cull shader reads the + // _FlwFrameUniforms block and the _flw_depthPyramid sampler. + // + // P5e (pa): `currentProgram` IS NULL WHENEVER `handleArm` HOLDS. Every read of it in the + // body below is on the `:` side of a `handleArm ? :` pick or inside the `else` of + // `if (handleArm)`, with one pair that is guarded instead - the two `globalConstants ? ... :` + // reads, which the [[noreturn]] refusal a few lines above them makes unreachable on that arm + // (its note says so). The parameter stays for the monolith arm, which is ruling ID-81: that + // build keeps this function's frontend text token for token. + void BindCurrentProgramWithResources(const SharedPtr& currentProgram, + const TextureImpl::DrawTextureSyncKeys& keys, + MG_Pipe::MGPipeHandle programCso) { + // P5e (pg): THE HANDLE ARM'S GATE, from the record rather than from the object. Hoisted + // out of the `if` below because the whole body's entry condition is "the program linked + // and has SPIR-V", and on this arm both answers are fields of the descriptor + // (Desc.LinkStatus is ID-88's, Desc.SpirvStatus P4a's). + const MG_Pipe::MGPipeShaderCsoRecord* programRecord = nullptr; + programRecord = PrgramImpl::FindShaderCsoRecord(programCso); + if (programRecord != nullptr && + (programRecord->Desc.LinkStatus == 0 || programRecord->Desc.SpirvStatus == 0)) { + programRecord = nullptr; + } + const Bool programUsable = + programRecord != nullptr; + if (programUsable) { +#ifdef TRACY_ENABLE + ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND); +#endif + PrgramImpl::BackendProgramObjectImpl* twin = nullptr; + // THE STASH, KEYED ON THE HANDLE (CONTRACT-P5E §5.5). SyncCurrentProgramByHandle + // wrote it a few lines ago in the same Prepare; the fallback is the by-handle + // resolver, which is record-first and touches no frontend identity - so unlike + // the frontend fallback below it is not a registry probe at all. + twin = PrgramImpl::g_currentDrawProgramHandle == programCso + ? PrgramImpl::g_currentDrawBackendProgram + : nullptr; + if (!twin) twin = PrgramImpl::ResolveProgramTwin(programCso); + if (twin) { + auto& backendProgram = *twin; + backendProgram.Use(); + + // Global UBO: block index and binding-point assignment are cached at + // link time (CacheResourceLocations); re-upload only when the CPU shadow + // actually changed since the last upload for this program. + // P5e (pg): the block's SIZE is Desc.GlobalUboSize on the handle arm. It is the + // same number GetUBOSize() answers - the emitter takes it from there - but it is + // the record's copy, which is the point. + const Uint globalUboSize = + programRecord->Desc.GlobalUboSize; + if (globalUboSize > 0 && backendProgram.HasGlobalUboBlock()) { +#ifdef TRACY_ENABLE + ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND); +#endif + // P4a e4 (D-H6): set_global_constants' counterpart. The version and the + // block image come off the ShaderCso record when it carries them; the UBO + // RING BELOW IS UNTOUCHED - it is on the Espryt do-not-touch list, keeps + // its {contentVersion, ringGeneration, frameSerial, offset} allocation + // key, keeps its glBufferSubData fallback and keeps accounting to + // ByteClass::StageUboGlobal. What changes is where the version and the + // bytes come from, and nothing else. + // + // The record covers the DEFAULT UNIFORM BLOCK ONLY. Named UBOs are the + // block right after this one and they are P4b's (dirty bits 15-17), which + // is why nothing here touches them. + const MG_Pipe::MGPipeShaderCsoRecord* const globalConstants = + PrgramImpl::ResolveGlobalConstantsRecordForHandle(programCso, + globalUboSize); + // P5e (pg), gap G-C: `MapUBO()` HAS NO RUN-AHEAD ANSWER. It returns + // ProgramObject::globalUboScratch - the live CPU array every glUniform* + // writes into - so under a transport it is a torn read of client memory by + // construction, not a read that happens to race. Every reason + // ResolveGlobalConstantsRecord* can decline is a SEAM DEFECT here (bit 12 + // off, the mis-keyed descriptor, the never-uploaded sentinel, a short block + // image), and each of them has already said so by name one level down; what + // this adds is that on the handle arm there is nothing legal to fall back + // to. CONTRACT-P5E §5.5: Fatal{UnmigratedVerb, "set_global_constants"}, not + // a torn upload. + // + // P5e (pa): AND IT IS ALSO WHAT KEEPS THE TWO `globalConstants ? ... : + // currentProgram->...` READS BELOW OFF THE FRONTEND. std::abort() is + // [[noreturn]], so past this statement `handleArm` implies + // `globalConstants != nullptr` in one step and in this function - not by a + // chain through other files, which is the distinction ID-110 draws. It + // matters now: pa's caller passes a NULL ProgramObject on that arm, so the + // `:` sides are unreachable rather than merely unused. + if (globalConstants == nullptr) { + MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_global_constants\"} - the " + "ShaderCso record {%u, %u} does not answer the default uniform " + "block for a draw of a program that declares %u bytes of it, and " + "MapUBO() is the client's live scratch array", + programCso.Slot, programCso.Gen, globalUboSize); + std::abort(); + } + const Uint32 uboContentVersion = + globalConstants ? globalConstants->GlobalConstantsVersion : + currentProgram->GetUBOContentVersion(); + const SizeT uboSize = static_cast(globalUboSize); + // Substituted at the two upload sites rather than hoisted into a local: + // MapUBO() is called there today and only there, and hoisting it would + // call it on paths that skip the upload entirely. +#define MGB_UBO_BYTES \ + (globalConstants ? static_cast(globalConstants->GlobalConstants.data()) \ + : static_cast(currentProgram->MapUBO())) + // Preferred path: write changed contents into a fresh slot of the + // shared persistent-mapped ring and bind it as a range. The GPU + // never reads bytes the CPU is writing, so the driver has no + // write-after-read hazard to resolve — the in-place glBufferSubData + // below forced Adreno into a ghost/stall on every uniform-dirtying + // draw (MC dirties uniforms every draw), which dominated frame time. + Bool ringBound = false; + { + const SizeT bindSize = + std::max(uboSize, static_cast(backendProgram.GetGlobalUboBackendBlockSize())); + auto& ringSlot = backendProgram.GetGlobalUboRingAllocation(); + void* write = nullptr; + Uint ringId = 0; + const auto state = BufferImpl::UboRingPrepareSlot(ringSlot, uboContentVersion, + CurrentFrameSerial(), bindSize, write, + ringId); + if (state == BufferImpl::UboRingSlotState::Allocated) { + std::memcpy(write, MGB_UBO_BYTES, uboSize); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal, + static_cast(uboSize)); + } + } + if (state == BufferImpl::UboRingSlotState::Allocated || + state == BufferImpl::UboRingSlotState::Reused) { + BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, ringId, + static_cast(ringSlot.offset), + static_cast(bindSize)); + ringBound = true; + } + } + if (!ringBound) { + // Fallback (no EXT_buffer_storage / fences, or ring creation + // failed): the original in-place upload. + if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) { + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId()); + g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, globalUboSize, + MGB_UBO_BYTES); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes( + MG_Util::PipeStats::ByteClass::StageUboGlobal, + static_cast(globalUboSize)); + } + g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); + backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); + } + BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0, + backendProgram.GetBackendGlobalUBOId()); + } +#undef MGB_UBO_BYTES + } + + { +#ifdef TRACY_ENABLE + ZoneScopedNC("UpdateUBO", TRACY_ZONECOLOR_BACKEND); +#endif + // Normal UBOs: backend block indices and glUniformBlockBinding + // assignments are cached at link time; per draw only the buffer + // bindings are re-established (they follow frontend binding points). + const auto& blockBackendIndices = backendProgram.GetUniformBlockBackendIndices(); + const auto uboCount = static_cast(blockBackendIndices.size()); + // Pre-loop epoch read; the per-resource drawCleanEpoch stamps below + // follow the same stamp-the-pre-read-value rule as the VAO memo + // (Managers.h documents the contract and the mutation-site list). + const Uint64 bufferEpoch = + uboCount > 0 ? BufferImpl::CurrentBufferMutationEpoch() : 0; + Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO + for (Int i = 0; i < uboCount; ++i) { + ++lastUBOBinding; + if (blockBackendIndices[i] < 0) { + continue; + } + + // Connect buffer to backend binding point. + // + // P5e (pg): glUniformBlockBinding MOVES THIS AFTER THE LINK, so it is + // one of the three post-link mutable reflection fields the archive alone + // cannot answer - set_program_bindings' first tail carries it, dense in + // this very index space. The BINDING POINT it names is sb's row (the + // GetBufferBindingPoint below); this line is the boundary S4 R5 named. + auto binding = + PrgramImpl::ProgramBlockBindingFromRecord(*programRecord, i); + // P5e (sb, §5.6): THE POINT HALF, by record. `binding` above is the + // PROGRAM's block binding and stays exactly where it is (it is package + // pg's row); what moves is the point it indexes. The index space is the + // same one - Start is 0 by contract - so `binding` addresses the + // applier's window directly, and a binding at or above Count means + // "nothing bound", which is what the frontend array's default said too. + const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); + const Uint32 windowStart = + st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform]; + const SizeT windowEnd = + static_cast(windowStart) + + st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform]; + if (static_cast(binding) < windowStart || + static_cast(binding) >= windowEnd) { + continue; // nothing bound there - the frontend arm's `if (bufferObj)` + } + const MG_Pipe::MGPBufferRange& entry = + st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform] + [static_cast(binding)]; + if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue; + // The clean probe is the SAME five questions IsBufferDrawClean asks + // the frontend object, asked by handle: the twin's identity, the + // resource record's Serial against the twin's synced one, and the + // pending-work sets - all server-owned (D-A4). The drawCleanEpoch + // short-circuit is unchanged; it was never the point read. + auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res); + if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) { + if (backendResource && BufferImpl::IsBufferDrawCleanByHandle( + entry.Res, backendResource, nullptr)) { + backendResource->drawCleanEpoch = bufferEpoch; + } else { + backendResource = + BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res); + } + } + if (backendResource && backendResource->id != 0) { + // WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a + // base binding travels as kMGPipeWholeBuffer precisely so the + // extent is re-resolved HERE, against the storage this server + // holds, rather than frozen at the client's emission. + if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) { + BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding, + backendResource->id); + } else { + const SizeT storage = backendResource->storageSize; + const SizeT rangeStart = + std::min(static_cast(entry.Offset), storage); + const SizeT rangeEnd = std::min( + static_cast(entry.Offset + entry.Size), storage); + BufferImpl::BindBufferRangeCached( + GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id, + static_cast(rangeStart), + static_cast(rangeEnd - rangeStart)); + } + } else { + MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO."); + } + continue; + } + } + + // Atomic counter buffers. Bound here rather than beside the storage-buffer sync + // in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads + // them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS + // program declares, and no other program's blocks live there. Both the draw and + // the dispatch path reach this, which is what a compute-shader counter needs. + if (!backendProgram.GetAtomicCounterBindings().empty()) { + BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(), + backendProgram.GetAtomicCounterEsslBindingTop()); + } + + { +#ifdef TRACY_ENABLE + ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND); +#endif + // Sampler unit binding: backend locations are cached at link time; + // glUniform1i is program state, so it is only re-issued when the + // frontend-assigned unit differs from what this program last saw. + // + // The whole pass sits behind the per-twin SamplerPassMemo (see its + // declaration for the invalidation enumeration): while the keys hold + // and every previously touched unit's sampler-shadow row is exactly + // what this pass last left there, re-running it is a provable no-op. + auto& samplerPassMemo = backendProgram.GetSamplerPassMemo(); + // P5e (pg), CONTRACT-P5E §5.5: THE SAMPLER PASS MEMO KEYS ON BindingsSerial + // on the handle arm. m_backendStateVersion is a frontend counter read on + // every draw (kimi row 105); what it is standing in for here is "has a + // sampler uniform's unit moved", and that is exactly what an applied + // set_program_bindings means. Truncated to the memo's Uint32 field, which is + // safe for a memo key - it is a change detector, not an ordering - and the + // record's serial would need four billion binding records to wrap. + const Uint32 programBackendStateVersion = + static_cast(programRecord->BindingsSerial); + Bool samplerPassClean = + samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId && + samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch && + samplerPassMemo.samplingGeneration == keys.samplingGeneration && + samplerPassMemo.backendStateVersion == programBackendStateVersion && + samplerPassMemo.textureContextGeneration == g_backendContextGeneration; + if (samplerPassClean) { + for (Uint i = 0; i < samplerPassMemo.count; ++i) { + if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] != + samplerPassMemo.rows[i]) { + samplerPassClean = false; + break; + } + } + } + if (!samplerPassClean) { + constexpr SizeT kMaxMemoEntries = + PrgramImpl::BackendProgramObjectImpl::SamplerPassMemo::kMaxEntries; + samplerPassMemo.valid = false; + samplerPassMemo.count = 0; + Bool memoisable = true; + for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) { + // P5e (pg): the unit glUniform1i put on this sampler - the second + // of the three post-link mutable fields, carried sparse and ascending + // by location in set_program_bindings' second tail. + const auto unit = + PrgramImpl::ProgramSamplerUnitFromRecord( + *programRecord, samplerBinding.frontendLocation); + if (unit == -1) continue; + // Record the touched unit for the memo; a pass touching more + // units than the memo can carry (or an out-of-range unit) + // simply never memoises. + if (memoisable && samplerPassMemo.count < kMaxMemoEntries && unit >= 0 && + unit < static_cast(SamplerImpl::g_boundSamplersCache.size())) { + samplerPassMemo.units[samplerPassMemo.count++] = static_cast(unit); + } else { + memoisable = false; + } + if (samplerBinding.lastAssignedUnit != unit) { + g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit); + samplerBinding.lastAssignedUnit = unit; + } + + // P5e (tx2), CONTRACT-P5E §5.3. THE SAMPLER PASS, FROM RECORDS. + // + // Four frontend reads used to live in this loop body and every one of + // them has a carrier: the unit's sampler object is + // st.BoundSamplerStates[u] (null = the texture's built-in sampler, which + // is TextureResources[view.Texture].Params.BuiltinSampler); its fifteen + // values - lodBias among them - are the CSO record's + // (MGPipeValueTypes.h); and the sampled texture's FORMAT, which decides + // the raw-depth-fetch substitution, is + // SamplerViewCsos[View].View.InternalFormat. + // + // THE SUBSTITUTION ITSELF STAYS ON THE SERVER (ARCHITECTURE.md §5.5 + // assigns it there by name); what moves is where its two inputs come + // from. + const auto& st = MG_Pipe::MGPipeApplier(); + const auto index = static_cast(unit); + const Bool inViewWindow = index >= st.SamplerViewStart && + index - st.SamplerViewStart < st.SamplerViewCount && + index < st.BoundSamplerViews.size(); + const MG_Pipe::MGPBoundView view = + inViewWindow ? st.BoundSamplerViews[index] : MG_Pipe::MGPBoundView{}; + const Bool inStateWindow = index >= st.SamplerStateStart && + index - st.SamplerStateStart < st.SamplerStateCount && + index < st.BoundSamplerStates.size(); + const MG_Pipe::MGPipeHandle unitSampler = + inStateWindow ? st.BoundSamplerStates[index] : MG_Pipe::kMGPipeNullHandle; + + // The EFFECTIVE sampler of the unit: the bound CSO if there is one, + // else the sampled texture's built-in sampler CSO - exactly the + // override order GL states and the pre-handle body spells with + // `samplerObject ? ... : texture->GetSamplerObject()`. + MG_Pipe::MGPipeHandle effectiveCso = unitSampler; + if (MG_Pipe::MGPipeHandleIsNull(effectiveCso) && + !MG_Pipe::MGPipeHandleIsNull(view.Texture)) { + if (const auto* texRecord = PipeTextureRecordForHandle(view.Texture)) { + effectiveCso = texRecord->Params.BuiltinSampler; + } + } + const MG_Pipe::MGPipeSamplerCsoRecord* effectiveCsoRecord = + PipeSamplerCsoRecordForHandle(effectiveCso); + + if (samplerBinding.lodBiasLocation >= 0) { + const Float lodBias = + effectiveCsoRecord != nullptr ? effectiveCsoRecord->Params.lodBias : 0.0f; + if (lodBias != samplerBinding.lastAssignedLodBias) { + g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias); + samplerBinding.lastAssignedLodBias = lodBias; + } + } + + // The last sample a usampler2DMS fetch may name, from the texture + // actually bound: a stencil texture's real count is not an integer + // colour texture's. Keyed like the bias above - a texture's storage or + // a unit's binding moving is what invalidates this pass's memo. + if (samplerBinding.msSampleMaxLocation >= 0) { + const Int sampleMax = + std::max(PipeTextureBackendSampleCountForHandle(view.Texture), 1) - 1; + if (sampleMax != samplerBinding.lastAssignedMsSampleMax) { + g_GLESFuncs.glUniform1i(samplerBinding.msSampleMaxLocation, sampleMax); + samplerBinding.lastAssignedMsSampleMax = sampleMax; + } + } + + const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); + const Bool sampledIsTexture2D = + viewRecord != nullptr && + static_cast(viewRecord->View.Target) == TextureTarget::Texture2D; + if (samplerBinding.uniformType == GL_SAMPLER_2D && sampledIsTexture2D && + effectiveCsoRecord != nullptr && + NeedsRawDepthFetchSampler( + effectiveCsoRecord->Params, + static_cast(viewRecord->View.InternalFormat))) { + GetRawDepthFetchSampler()->Bind(unit); + MGLOG_D("Using raw depth fetch sampler on unit %d.", unit); + } else if (!MG_Pipe::MGPipeHandleIsNull(unitSampler)) { + // THE MINT IS GONE (§4.2): the unit's sampler is the CSO's own + // twin, the same one BindCurrentUnitSamplers binds, so the two + // arms cannot hand a unit back and forth between two driver + // samplers - and there is no arm left that creates a twin from a + // frontend SamplerObject. + if (auto* csoTwin = SamplerImpl::ResolveSamplerCsoTwin(unitSampler)) { + csoTwin->Bind(unit); + } + } else { + SamplerImpl::UnbindSampler(unit); + } + continue; + } + if (memoisable) { + // Snapshot the rows AFTER the pass: they are exactly what a + // clean replay must find untouched. + for (Uint i = 0; i < samplerPassMemo.count; ++i) { + samplerPassMemo.rows[i] = SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]]; + } + samplerPassMemo.contextId = keys.contextId; + samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch; + samplerPassMemo.samplingGeneration = keys.samplingGeneration; + samplerPassMemo.backendStateVersion = programBackendStateVersion; + samplerPassMemo.textureContextGeneration = g_backendContextGeneration; + samplerPassMemo.valid = true; + } + } + } + } else { + g_GLESFuncs.glUseProgram(0); + PrgramImpl::g_lastUsedBackendProgramId = 0; + MGLOG_E_ONCE("No backend program found (maybe not synced) for current program, cannot use program."); + } + } + } + + // Raw pointer: only ever used inside one GL entry point after PrepareForDraw/ + // PrepareForCompute, where the current program (and therefore its registry twin) + // is pinned for the duration. Prefers the per-draw stash those preparations wrote. + PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() { + // P5e (pg), kimi row 107: THE PER-SUB-DRAW ROW. This is reached once per indirect + // sub-draw for the gl_DrawID / gl_BaseVertex / gl_BaseInstance uniforms, and before P5e + // every one of those pulled GetProgramForDraw() and, on a stash miss, probed the client + // allocator. On the handle arm the stash key is the applier's own DrawProgram and the + // miss path is the by-handle resolver, which is record-first and names no frontend + // identity - so the whole entry point leaves the frontend. + const MG_Pipe::MGPipeHandle cso = MG_Pipe::MGPipeApplier().DrawProgram; + if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr; + const auto* const record = PrgramImpl::FindShaderCsoRecord(cso); + if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { + return nullptr; + } + if (PrgramImpl::g_currentDrawProgramHandle == cso) { + return PrgramImpl::g_currentDrawBackendProgram; + } + return PrgramImpl::ResolveProgramTwin(cso); + } + + void SetCurrentBaseInstance(Uint32 baseInstance) { + if (const auto program = GetCurrentBackendProgram()) { + program->SetBaseInstance(baseInstance); + } + } + + void SetCurrentDrawID(Uint32 drawId) { + if (const auto program = GetCurrentBackendProgram()) { + program->SetDrawID(drawId); + } + } + + void SetCurrentBaseVertex(Int32 baseVertex) { + if (const auto program = GetCurrentBackendProgram()) { + program->SetBaseVertex(baseVertex); + } + } + + Bool CurrentProgramReadsDrawID() { + const auto program = GetCurrentBackendProgram(); + return program != nullptr && program->ReadsDrawID(); + } + + Bool CurrentProgramReadsBaseVertex() { + const auto program = GetCurrentBackendProgram(); + return program != nullptr && program->ReadsBaseVertex(); + } + + // The two questions above, asked from BEFORE PrepareForDraw - where neither can be + // answered honestly. GetCurrentBackendProgram only sees a twin that a previous draw + // already synced, and a twin from before a relink still carries the previous link's + // uniform locations, so "no" there means "not known yet" at least as often as it + // means no. The multi-draw compute tier has to decide whether to flatten a batch + // before PrepareForDraw runs (its dispatch cannot come after the draw state), and + // flattening a batch that turns out to need per-sub-draw values is unrecoverable - + // so an unanswerable program counts as needing them. + Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) { + const auto program = GetCurrentBackendProgram(); + if (program == nullptr) return true; + // P5e (pg), kimi row 108: the same "is this twin current" question, from the record's + // serial instead of the frontend's link version. The ANSWER TO AN UNANSWERABLE PROGRAM + // IS STILL `true` - a flattened batch that turns out to need per-sub-draw values is + // unrecoverable, so "not known yet" has to count as "may need them", and a missing + // record is the most not-known-yet a program can be. + const auto* const record = + PrgramImpl::FindShaderCsoRecord(MG_Pipe::MGPipeApplier().DrawProgram); + if (record == nullptr || program->GetSyncedShaderCsoSerial() != record->Serial) { + return true; + } + return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex()); + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.cpp new file mode 100644 index 00000000..9fa0b5be --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.cpp @@ -0,0 +1,1330 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace FramebufferImpl { + // Record that `target` now reflects the applier's record as of `serial`. The two + // generations ride along for the reason D-B3 keeps them: they answer questions about + // the DRIVER's own ids that no client-side version can answer. + // The stamp's opposite, for a path that leaves a target correctly bound WITHOUT having + // consulted the record (MINOR-3, ForceBindCurrentFBO). Costs one extra sync at worst. + static void InvalidateSyncedFramebufferSerial(FramebufferTarget target) { + g_fboSyncedSerials[SizeT(target)].valid = false; + } + } // namespace FramebufferImpl + + // P5c (hd, CONTRACT-P5C §3.2/§3.3): the handle-keyed form of SyncAndBindFramebufferObject. + // With an active transport a framebuffer reaches the server as the handle a record + // carried - the named blit's ReadFbo/DrawFbo, or the applier's own bound handle for a + // binding restore - and the client's binding slots and slot allocator are never probed + // (T2/T3). The twin is resolved (or minted) BY HANDLE; its sync is keyed on THIS handle's + // applier record through m_pushedSyncHandle; and the frontend object the sync body still + // walks comes from the table's own state note (the object-class channel P3b/P4b retires), + // never from a client binding slot. A twin with no noted object was never synced through + // any binding - the DSA-on-a-never-bound-framebuffer case, which the split path already + // refuses for the named clears - and is loud rather than silently unconfigured. + void SyncAndBindFramebufferByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget target, + Bool forceSync = false) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (MG_Pipe::MGPipeHandleIsNull(fbo) || fbo == MG_Pipe::kMGPipeDefaultFramebuffer) { + // Same reset as the object form's default branch, for its reason. + if (target == FramebufferTarget::Draw) { + FramebufferImpl::g_alphaWidenedDrawBufferMask = 0; + FramebufferImpl::g_integerColorDrawBufferMask = 0; + } + if (FramebufferImpl::CurrentFramebufferBinding(target) != 0) FlushAheadOfWindowBufferUse(); + FramebufferImpl::BindFramebufferId( + target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); + return; + } + + auto& registry = FramebufferImpl::g_backendFramebufferObjects; + auto* twinSlot = registry.GetOrCreateByHandle(fbo); + if (twinSlot == nullptr) { + MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} is refused by the twin slot table " + "(live generation %u); nothing is bound for the %s target", + fbo.Slot, fbo.Gen, registry.LiveGenAt(fbo.Slot), + target == FramebufferTarget::Read ? "READ" : "DRAW"); + return; + } + auto& backendObj = *twinSlot; + if (!backendObj) { + backendObj = MakeShared(); + } + backendObj->m_pushedSyncHandle = fbo; + // P5e (fb, CONTRACT-P5E.md §5.4): THE STATE NOTE IS GONE. The paragraph above described + // a handle arm that still had to find "the frontend object the sync body walks", and + // its failure mode - a framebuffer named by a DSA entry point that was never bound, so + // no note exists and the twin is bound unconfigured - was the last consequence of that + // detour. SyncToBackendByHandle configures the twin from the RECORD, which every named + // framebuffer has by construction (a Named record precedes every DSA site, ID-19), so + // the never-bound case is now configured correctly rather than named and refused. + if (forceSync) { + backendObj->InvalidateSyncedState(); + } + backendObj->SyncToBackendByHandle(fbo, target); + backendObj->Bind(target); + } + + void ForceBindCurrentFBO(FramebufferTarget target) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // P5c (hd): with an active transport the bound framebuffer's handle is the applier's + // own working state; the object form's registry probe never runs (T2). + // + // P5e (fb, CONTRACT-P5E.md §5.4): AND THE BINDING SLOT IS READ INSIDE THE ELSE, NOT + // BEFORE THE IF. The read and the three stamps below used to straddle this arm - S3's + // finding #7 - so the handle path still touched a BARRIER_PULLED row on both sides of + // the one branch that existed to avoid it. The stamps are the LEGACY memo's + // (g_fboSynced*), and skipping them here is safe for exactly the reason the serial memo + // below is invalidated rather than stamped: nothing on this path consulted the frontend + // trio, so the honest thing is to leave it invalid and pay one extra sync. Under a + // transport the legacy arm is never taken anyway (SyncCurrentFBO refuses first), so the + // memo it feeds has no reader. + SyncAndBindFramebufferByHandle( + MG_Pipe::MGPipeApplier().BoundFramebuffer[static_cast( + target == FramebufferTarget::Read ? MG_Pipe::MGPipeFramebufferTarget::Read + : MG_Pipe::MGPipeFramebufferTarget::Draw)], + target); + // P4a decline-site F9 / MINOR-3: THE HANDLE ARM'S MEMO IS INVALIDATED HERE, NOT + // STAMPED, and it is done per target for the reason the memo is kept per target at + // all - this entry point syncs ONE binding and must not say anything about the other. + // + // A stamp would claim "this target reflects the applier's RECORD as of this serial", + // and nothing on this path consulted the record: SyncAndBindFramebufferObject above + // syncs the FRONTEND object out of the binding slot. I could not turn the stamp into + // a wrong-pixel scenario (any edit to the framebuffer moves its ContentHash and hence + // the serial), but it is an invariant break with no demonstrable failure, and the + // honest form costs at most one extra sync on the next draw. + FramebufferImpl::InvalidateSyncedFramebufferSerial(target); + } + + // Sized internal format of the currently bound READ framebuffer's read colour + // attachment, 0 when it cannot be determined. + static GLenum QueryReadColorAttachmentInternalFormat() { + GLint attachmentType = 0; + GLint attachmentName = 0; + // Ask the point the backend read buffer actually names, not COLOR_ATTACHMENT0. The colour map + // in BackendFramebufferObject is a permutation, so the read attachment's image only sits at + // CA0 when that map is identity; querying CA0 unconditionally would size the resolve + // renderbuffer from a different attachment's format and either convert wrongly or fail the + // blit outright. + GLint readBuffer = GL_COLOR_ATTACHMENT0; + g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); + if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) { + readBuffer = GL_COLOR_ATTACHMENT0; + } + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &attachmentType); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &attachmentName); + if (attachmentName == 0) { + return 0; + } + GLint internalFormat = 0; + if (attachmentType == GL_RENDERBUFFER) { + if (!g_GLESFuncs.glGetRenderbufferParameteriv) return 0; + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(attachmentName)); + g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); + } else if (attachmentType == GL_TEXTURE) { + if (!g_GLESFuncs.glGetTexLevelParameteriv) return 0; + // The resolve source of interest is always a multisample 2D texture; a + // single-sample source would not have taken the fallback in the first place. + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &previous); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast(attachmentName)); + g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D_MULTISAMPLE, 0, GL_TEXTURE_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast(previous)); + } + return static_cast(internalFormat); + } + + // ES rejects any blit out of a multisample read framebuffer whose format differs + // from the draw framebuffer's. Desktop GL only requires identical formats when BOTH + // framebuffers are multisampled, so a multisample resolve is allowed to convert + // format on the way out (KHR-GL3x.framebuffer_blit resolves an R8/R16F multisample + // texture straight into an RGBA8 target). Emulate it in two steps: resolve into a + // scratch buffer of the source's own format, then run the caller's blit from there - + // that second one is single-sample on both sides, where ES does allow conversion. + static Bool ResolveThenBlit(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, + GLint dstX1, GLint dstY1, GLenum filter) { + static NativeContextName s_resolveFramebuffer; // per context: a framebuffer is never shared + static Uint s_resolveRenderbuffer = 0; + static GLenum s_resolveFormat = 0; + static GLsizei s_resolveWidth = 0; + static GLsizei s_resolveHeight = 0; + static Uint s_resolveContextGeneration = ~0u; + + if (!g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glGenRenderbuffers || + !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glFramebufferRenderbuffer) { + return false; + } + const GLenum sourceFormat = QueryReadColorAttachmentInternalFormat(); + if (sourceFormat == 0) { + return false; + } + + const GLint left = std::min(srcX0, srcX1); + const GLint right = std::max(srcX0, srcX1); + const GLint bottom = std::min(srcY0, srcY1); + const GLint top = std::max(srcY0, srcY1); + const GLsizei width = static_cast(right - left); + const GLsizei height = static_cast(top - bottom); + if (width <= 0 || height <= 0) { + return false; + } + + if (s_resolveContextGeneration != g_backendContextGeneration) { + // The ids belonged to a dead context; the context reclaimed them with it. + s_resolveFramebuffer = 0; + s_resolveRenderbuffer = 0; + s_resolveFormat = 0; + s_resolveContextGeneration = g_backendContextGeneration; + } + if (s_resolveFramebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer); + if (s_resolveFramebuffer == 0) return false; + } + if (s_resolveRenderbuffer == 0) { + g_GLESFuncs.glGenRenderbuffers(1, &s_resolveRenderbuffer); + if (s_resolveRenderbuffer == 0) return false; + s_resolveFormat = 0; + } + + GLint previousRenderbuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previousRenderbuffer); + if (s_resolveFormat != sourceFormat || s_resolveWidth < width || s_resolveHeight < height) { + s_resolveWidth = std::max(s_resolveWidth, width); + s_resolveHeight = std::max(s_resolveHeight, height); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, s_resolveRenderbuffer); + g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, sourceFormat, s_resolveWidth, s_resolveHeight); + s_resolveFormat = sourceFormat; + } + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previousRenderbuffer)); + + GLint previousDraw = 0; + GLint previousRead = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); + + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_resolveFramebuffer); + g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + s_resolveRenderbuffer); + Bool resolved = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; + if (resolved) { + DrainBlitErrors(); + // A blit is scissored like a draw (the replicate path's guard documents the + // same rule): the application's box would clip this resolve into the + // scratch, and the second blit would then copy never-written scratch texels + // into the destination - silently, since scissor clipping raises no GL + // error. Disable for the staging blit only; the caller-visible blit below + // keeps the blit's native scissor semantics. Tracked via the render-state + // shadow, exactly like ScopedScissorDisable. + const Bool scissorWasEnabled = RenderStateImpl::DriverScissorTestEnabled(); + if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, GL_COLOR_BUFFER_BIT, + GL_NEAREST); + if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); + resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + if (resolved) { + // Mirror the caller's orientation into the scratch-relative source rect so a + // flipped blit stays flipped. + const GLint blitX0 = srcX0 <= srcX1 ? 0 : width; + const GLint blitX1 = srcX0 <= srcX1 ? width : 0; + const GLint blitY0 = srcY0 <= srcY1 ? 0 : height; + const GLint blitY1 = srcY0 <= srcY1 ? height : 0; + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_resolveFramebuffer); + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(blitX0, blitY0, blitX1, blitY1, dstX0, dstY0, dstX1, dstY1, + GL_COLOR_BUFFER_BIT, filter); + resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + if (!resolved) { + MGLOG_E_ONCE("BlitFramebuffer: multisample resolve fallback failed"); + } + return resolved; + } + + namespace ReplicateBlitImpl { + static Uint s_contextGeneration = ~0u; + static NativeContextName s_framebuffer; // per context, as is the vertex array below + static GLuint s_texture = 0; + static GLenum s_textureFormat = 0; + static GLsizei s_textureWidth = 0; + static GLsizei s_textureHeight = 0; + static NativeContextName s_vertexArray; + static GLuint s_depthProgram = 0; + static GLuint s_stencilProgram = 0; + static GLint s_depthUvTransform = -1; + static GLint s_stencilUvTransform = -1; + static GLint s_stencilBit = -1; + static Bool s_programsFailed = false; + + static const char* const kVertexSource = + "#version 300 es\n" + "uniform vec4 uUvTransform;\n" + "out vec2 vUv;\n" + "void main() {\n" + " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" + " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" + " vUv = p * uUvTransform.xy + uUvTransform.zw;\n" + "}\n"; + + static const char* const kDepthFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp sampler2D;\n" + "uniform sampler2D uSource;\n" + "in vec2 vUv;\n" + "void main() {\n" + " gl_FragDepth = texture(uSource, vUv).r;\n" + "}\n"; + + static const char* const kStencilFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp usampler2D;\n" + "uniform usampler2D uSource;\n" + "uniform uint uBit;\n" + "in vec2 vUv;\n" + "void main() {\n" + " if ((texture(uSource, vUv).r & uBit) == 0u) discard;\n" + "}\n"; + + GLuint BuildProgram(const char* fragmentSource) { + const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER); + const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER); + if (vertexShader == 0 || fragmentShader == 0) { + return 0; + } + g_GLESFuncs.glShaderSource(vertexShader, 1, &kVertexSource, nullptr); + g_GLESFuncs.glCompileShader(vertexShader); + g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); + g_GLESFuncs.glCompileShader(fragmentShader); + + const GLuint program = g_GLESFuncs.glCreateProgram(); + GLint linked = GL_FALSE; + if (program != 0) { + g_GLESFuncs.glAttachShader(program, vertexShader); + g_GLESFuncs.glAttachShader(program, fragmentShader); + g_GLESFuncs.glLinkProgram(program); + g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); + } + g_GLESFuncs.glDeleteShader(vertexShader); + g_GLESFuncs.glDeleteShader(fragmentShader); + if (linked != GL_TRUE) { + if (program != 0) g_GLESFuncs.glDeleteProgram(program); + return 0; + } + return program; + } + + static Bool EnsureResources() { + if (s_contextGeneration != g_backendContextGeneration) { + // The ids belonged to a dead context; the context reclaimed them with it. + s_framebuffer = 0; + s_texture = 0; + s_textureFormat = 0; + s_textureWidth = 0; + s_textureHeight = 0; + s_vertexArray = 0; + s_depthProgram = 0; + s_stencilProgram = 0; + s_programsFailed = false; + s_contextGeneration = g_backendContextGeneration; + } + if (s_programsFailed) { + return false; + } + if (s_depthProgram == 0) { + s_depthProgram = BuildProgram(kDepthFragmentSource); + s_stencilProgram = BuildProgram(kStencilFragmentSource); + if (s_depthProgram == 0 || s_stencilProgram == 0) { + s_programsFailed = true; + MGLOG_E_ONCE("BlitFramebuffer: could not build the multisample replicate programs"); + return false; + } + s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); + s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); + s_stencilBit = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uBit"); + } + if (s_framebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &s_framebuffer); + if (s_framebuffer == 0) return false; + } + if (s_vertexArray == 0) { + g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray); + if (s_vertexArray == 0) return false; + } + return true; + } + + // Sized internal format of ONE attachment point of the bound READ framebuffer, or 0 + // when there is no object there to ask (the default framebuffer's buffers have no + // queryable format at all). The scratch copy has to use the very same one: ES rejects + // a depth/stencil blit between differing formats even when both sides are + // single-sampled. + // + // The texture branch can only ask about a GL_TEXTURE_2D, and a name whose target is + // something else (an array or cube texture attached by glFramebufferTextureLayer / + // glFramebufferTexture) makes glBindTexture answer GL_INVALID_OPERATION and change + // nothing. Reading glGetTexLevelParameteriv after that failed bind does NOT return 0 - + // it truthfully describes whatever texture was already on GL_TEXTURE_2D, which on this + // path is the emulation's own staging scratch. That is a wrong answer that looks like a + // right one, so the bind has to be error-checked rather than trusted. + GLenum QueryAttachmentSizedFormat(GLenum attachment) { + GLint objectType = 0; + GLint objectName = 0; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName); + if (objectName == 0) { + return 0; + } + GLint internalFormat = 0; + if (objectType == GL_RENDERBUFFER) { + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); + ClearGLErrors(); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(objectName)); + const Bool bound = g_GLESFuncs.glGetError() == GL_NO_ERROR; + if (bound) { + g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, + &internalFormat); + } + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); + ClearGLErrors(); + } else if (objectType == GL_TEXTURE) { + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous); + ClearGLErrors(); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(objectName)); + const Bool bound = g_GLESFuncs.glGetError() == GL_NO_ERROR; + if (bound) { + g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, + &internalFormat); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + internalFormat = 0; + } + } + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previous)); + ClearGLErrors(); + } + return static_cast(internalFormat); + } + + // The read framebuffer's depth format, or - when it has no depth - its stencil one. + static GLenum QueryReadDepthStencilFormat(GLenum* outAttachment) { + const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT}; + for (const GLenum attachment : attachments) { + const GLenum internalFormat = QueryAttachmentSizedFormat(attachment); + if (internalFormat != 0) { + if (outAttachment) *outAttachment = attachment; + return internalFormat; + } + } + return 0; + } + } // namespace ReplicateBlitImpl + + // Returns true when the request was serviced (or is not this fallback's business). + static Bool ReplicateBlitIntoMultisampleDraw(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, + GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) { + using namespace ReplicateBlitImpl; + if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { + return false; + } + if (!EnsureResources()) { + return false; + } + + const GLsizei srcWidth = static_cast(std::abs(srcX1 - srcX0)); + const GLsizei srcHeight = static_cast(std::abs(srcY1 - srcY0)); + const GLsizei dstWidth = static_cast(std::abs(dstX1 - dstX0)); + const GLsizei dstHeight = static_cast(std::abs(dstY1 - dstY0)); + if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { + return false; + } + + GLenum readAttachment = GL_DEPTH_ATTACHMENT; + const GLenum sourceFormat = QueryReadDepthStencilFormat(&readAttachment); + if (sourceFormat == 0) { + return false; + } + const Bool wantDepth = (mask & GL_DEPTH_BUFFER_BIT) != 0 && FormatHasDepth(sourceFormat); + const Bool wantStencil = (mask & GL_STENCIL_BUFFER_BIT) != 0 && FormatHasStencil(sourceFormat); + if (!wantDepth && !wantStencil) { + return false; + } + // Sampling the stencil half of a packed texture goes through + // GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the pname + // would just raise GL_INVALID_ENUM and the shader would read depth bits as stencil. + const Bool supportsStencilTextureMode = g_GLESCapabilities.GLESVersion.Major > 3 || + (g_GLESCapabilities.GLESVersion.Major == 3 && + g_GLESCapabilities.GLESVersion.Minor >= 1); + if (wantStencil && !supportsStencilTextureMode) { + return false; + } + + GLint previousDraw = 0; + GLint previousRead = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); + + // Everything below borrows the application's context - a texture unit for the + // scratch sampling and the whole rasterization pipeline for the replicate passes - + // so the guard is taken before the first of those, not just before the draws. It + // also puts the scissor test where the staging blit below needs it: a blit is + // scissored like a draw, and the application's box would otherwise clip the copy + // into the scratch texture. + ScopedEmulationDrawState emulationState; + + // Copy the source rectangle into a scratch texture of its own format: both sides of + // that blit are single-sampled, which ES does allow. + Bool ok = true; + if (s_texture == 0 || s_textureFormat != sourceFormat || s_textureWidth < srcWidth || + s_textureHeight < srcHeight) { + if (s_texture != 0) { + g_GLESFuncs.glDeleteTextures(1, &s_texture); // immutable storage cannot be resized + s_texture = 0; + } + s_textureWidth = std::max(s_textureWidth, srcWidth); + s_textureHeight = std::max(s_textureHeight, srcHeight); + g_GLESFuncs.glGenTextures(1, &s_texture); + if (s_texture == 0) { + ok = false; + } else { + // The guard already activated TempTextureUnit and cleared its sampler. + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); + DrainBlitErrors(); + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, sourceFormat, s_textureWidth, s_textureHeight); + ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + s_textureFormat = ok ? sourceFormat : 0; + } + } + + if (ok) { + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_framebuffer); + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, ScratchAttachmentFor(sourceFormat), GL_TEXTURE_2D, + s_texture, 0); + ok = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; + } + if (ok) { + const GLint left = std::min(srcX0, srcX1); + const GLint bottom = std::min(srcY0, srcY1); + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(left, bottom, left + srcWidth, bottom + srcHeight, 0, 0, srcWidth, srcHeight, + mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT), GL_NEAREST); + ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + if (!ok) { + MGLOG_E_ONCE("BlitFramebuffer: could not stage the source for the multisample replicate"); + return false; + } + + const GLint dstLeft = std::min(dstX0, dstX1); + const GLint dstBottom = std::min(dstY0, dstY1); + const Bool mirrorX = (srcX1 > srcX0) != (dstX1 > dstX0); + const Bool mirrorY = (srcY1 > srcY0) != (dstY1 > dstY0); + // The scratch holds the source rectangle at its origin, so the texture is larger than + // the copied region: scale the [0,1] quad coordinates down to the region it occupies. + const Float uvScaleX = static_cast(srcWidth) / static_cast(s_textureWidth); + const Float uvScaleY = static_cast(srcHeight) / static_cast(s_textureHeight); + const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY, + mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f}; + + // The guard already left the pipeline neutral (nothing culled, tested, blended or + // coverage-masked) on the borrowed texture unit; what is left is this pass's own + // choices - the destination rectangle, and colour writes off because it writes only + // depth and stencil. + VertexArrayImpl::BindBackendVAOId(s_vertexArray); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); + g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight); + g_GLESFuncs.glScissor(dstLeft, dstBottom, dstWidth, dstHeight); + g_GLESFuncs.glEnable(GL_SCISSOR_TEST); + g_GLESFuncs.glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + DrainBlitErrors(); + + if (wantDepth) { + if (FormatHasStencil(sourceFormat)) { + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + } + g_GLESFuncs.glUseProgram(s_depthProgram); + g_GLESFuncs.glUniform4f(s_depthUvTransform, uvTransform[0], uvTransform[1], uvTransform[2], + uvTransform[3]); + g_GLESFuncs.glEnable(GL_DEPTH_TEST); + g_GLESFuncs.glDepthFunc(GL_ALWAYS); + g_GLESFuncs.glDepthMask(GL_TRUE); + g_GLESFuncs.glDisable(GL_STENCIL_TEST); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + } + + if (wantStencil) { + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + g_GLESFuncs.glUseProgram(s_stencilProgram); + g_GLESFuncs.glUniform4f(s_stencilUvTransform, uvTransform[0], uvTransform[1], uvTransform[2], + uvTransform[3]); + g_GLESFuncs.glDisable(GL_DEPTH_TEST); + g_GLESFuncs.glDepthMask(GL_FALSE); + g_GLESFuncs.glEnable(GL_STENCIL_TEST); + // The bit planes are written by ORing in the set bits, so the destination has to + // start from zero. The blit overwrites the whole rectangle anyway, and the scissor + // keeps the clear inside it. + const GLint zero = 0; + g_GLESFuncs.glStencilMask(0xFFu); + g_GLESFuncs.glClearBufferiv(GL_STENCIL, 0, &zero); + g_GLESFuncs.glStencilFunc(GL_ALWAYS, 0xFF, 0xFFu); + g_GLESFuncs.glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + for (Uint bit = 0; bit < 8; ++bit) { + g_GLESFuncs.glStencilMask(1u << bit); + g_GLESFuncs.glUniform1ui(s_stencilBit, 1u << bit); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + } + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + } + + const Bool replicated = g_GLESFuncs.glGetError() == GL_NO_ERROR; + + // Everything the pass disturbed goes back through emulationState's destructor. + if (!replicated) { + MGLOG_E_ONCE("BlitFramebuffer: multisample replicate fallback failed"); + } + return replicated; + } + + // ---- glBlitFramebuffer within one image ------------------------------------------------ + // + // GL 4.6 lets a blit read and write the same image as long as the two rectangles do not + // overlap (only the overlap is undefined). ES 3.x makes identical read and draw buffers + // GL_INVALID_OPERATION outright, so the driver refuses a blit from one half of a texture + // into the other and nothing is written. Emulate it in two steps: copy the source rectangle + // into a scratch framebuffer of the source's own native format, then run the caller's blit + // - same mask, same filter, same orientation - from the scratch into the real destination. + // Overlapping rectangles stay undefined, as GL leaves them. A LINEAR blit that scales reads + // clamped texels at the rectangle's edge where GL would read the neighbouring ones. + + // What a framebuffer attachment point resolves to on the DRIVER side: the native object and, + // for a texture, the level, layer and cube face. A null name means the point is empty. + struct DriverAttachmentImage { + GLint Type = GL_NONE; + GLint Name = 0; + GLint Level = 0; + GLint Layer = 0; + GLint Face = 0; + }; + + static DriverAttachmentImage QueryDriverAttachmentImage(GLenum framebufferTarget, GLenum attachment) { + DriverAttachmentImage image; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &image.Type); + if (image.Type != GL_TEXTURE && image.Type != GL_RENDERBUFFER) return {}; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &image.Name); + if (image.Type == GL_TEXTURE) { + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, + GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &image.Level); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv( + framebufferTarget, attachment, GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE, &image.Face); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(framebufferTarget, attachment, + GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER, &image.Layer); + } + return image; + } + + static Bool IsSameDriverImage(const DriverAttachmentImage& a, const DriverAttachmentImage& b) { + return a.Name != 0 && a.Type == b.Type && a.Name == b.Name && a.Level == b.Level && a.Layer == b.Layer && + a.Face == b.Face; + } + + // The colour attachment point the bound READ framebuffer reads from, GL_NONE when it reads + // nothing. + static GLenum DriverReadColorAttachment() { + GLint readBuffer = GL_NONE; + g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); + if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) return GL_NONE; + return static_cast(readBuffer); + } + + // The aspects of `mask` whose read and draw buffers are one image on the driver side - the + // aspects ES refuses to blit. The window-system framebuffer (driver name 0) has no attachment + // to compare or take a format from and is not considered. + static GLbitfield DriverBlitAspectsSharingAnImage(GLbitfield mask) { + GLint readFramebuffer = 0; + GLint drawFramebuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer); + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFramebuffer); + if (readFramebuffer == 0 || drawFramebuffer == 0) return 0; + + GLbitfield shared = 0; + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + const GLenum readAttachment = DriverReadColorAttachment(); + if (readAttachment != GL_NONE) { + const DriverAttachmentImage source = QueryDriverAttachmentImage(GL_READ_FRAMEBUFFER, readAttachment); + GLint maxDrawBuffers = 0; + g_GLESFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); + for (GLint i = 0; i < maxDrawBuffers && source.Name != 0; ++i) { + GLint drawBuffer = GL_NONE; + g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + i, &drawBuffer); + if (drawBuffer < GL_COLOR_ATTACHMENT0 || drawBuffer > GL_COLOR_ATTACHMENT31) continue; + if (IsSameDriverImage(source, QueryDriverAttachmentImage(GL_DRAW_FRAMEBUFFER, + static_cast(drawBuffer)))) { + shared |= GL_COLOR_BUFFER_BIT; + break; + } + } + } + } + const struct { + GLbitfield bit; + GLenum attachment; + } depthStencil[] = {{GL_DEPTH_BUFFER_BIT, GL_DEPTH_ATTACHMENT}, {GL_STENCIL_BUFFER_BIT, GL_STENCIL_ATTACHMENT}}; + for (const auto& aspect : depthStencil) { + if ((mask & aspect.bit) == 0) continue; + if (IsSameDriverImage(QueryDriverAttachmentImage(GL_READ_FRAMEBUFFER, aspect.attachment), + QueryDriverAttachmentImage(GL_DRAW_FRAMEBUFFER, aspect.attachment))) { + shared |= aspect.bit; + } + } + return shared; + } + + // The native sized internal format of what a READ framebuffer attachment point holds, 0 when + // it cannot be determined. ES has no "which target is this texture" query, so a texture is + // found by binding it to each candidate target in turn: glBindTexture refuses a name created + // for another target and leaves the binding alone. + static GLenum QueryDriverReadAttachmentInternalFormat(GLenum attachment) { + const DriverAttachmentImage image = QueryDriverAttachmentImage(GL_READ_FRAMEBUFFER, attachment); + if (image.Name == 0) return 0; + GLint internalFormat = 0; + if (image.Type == GL_RENDERBUFFER) { + if (!g_GLESFuncs.glGetRenderbufferParameteriv) return 0; + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(image.Name)); + g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); + } else { + if (!g_GLESFuncs.glGetTexLevelParameteriv) return 0; + struct Candidate { + GLenum target; + GLenum binding; + }; + const Candidate cube[] = {{GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BINDING_CUBE_MAP}}; + const Candidate others[] = {{GL_TEXTURE_2D, GL_TEXTURE_BINDING_2D}, + {GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BINDING_2D_ARRAY}, + {GL_TEXTURE_3D, GL_TEXTURE_BINDING_3D}, + {GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_BINDING_CUBE_MAP_ARRAY}}; + const Bool isCubeFace = image.Face != 0 && image.Face != GL_NONE; + const Candidate* candidates = isCubeFace ? cube : others; + const SizeT candidateCount = isCubeFace ? std::size(cube) : std::size(others); + for (SizeT i = 0; i < candidateCount && internalFormat == 0; ++i) { + const Candidate& candidate = candidates[i]; + DrainBlitErrors(); + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(candidate.binding, &previous); + g_GLESFuncs.glBindTexture(candidate.target, static_cast(image.Name)); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) continue; + const GLenum levelTarget = isCubeFace ? static_cast(image.Face) : candidate.target; + g_GLESFuncs.glGetTexLevelParameteriv(levelTarget, image.Level, GL_TEXTURE_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindTexture(candidate.target, static_cast(previous)); + } + DrainBlitErrors(); + } + // A texture specified with an unsized format reports it back; renderbuffer storage + // takes only sized ones. + switch (internalFormat) { + case GL_RGBA: return GL_RGBA8; + case GL_RGB: return GL_RGB8; + case GL_DEPTH_STENCIL: return GL_DEPTH24_STENCIL8; + case GL_DEPTH_COMPONENT: return GL_DEPTH_COMPONENT24; + default: return static_cast(internalFormat); + } + } + + static Bool IsPackedDepthStencilFormat(GLenum format) { + return format == GL_DEPTH24_STENCIL8 || format == GL_DEPTH32F_STENCIL8; + } + + // The two-step copy itself. Both framebuffers are single-sample (ES cannot blit into a + // multisample one at all). Returns false, having written nothing to the destination, when + // the scratch cannot hold the source. + static Bool BlitWithinOneImage(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, + GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { + static NativeContextName s_scratchFramebuffer; // per context: a framebuffer is never shared + static Uint s_colorRenderbuffer = 0; + static Uint s_depthStencilRenderbuffer = 0; + static GLenum s_colorFormat = 0; + static GLenum s_depthStencilFormat = 0; + static GLsizei s_colorWidth = 0, s_colorHeight = 0; + static GLsizei s_depthStencilWidth = 0, s_depthStencilHeight = 0; + static Uint s_scratchContextGeneration = ~0u; + + if (!g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glGenRenderbuffers || + !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glFramebufferRenderbuffer) { + return false; + } + const GLint left = std::min(srcX0, srcX1); + const GLint right = std::max(srcX0, srcX1); + const GLint bottom = std::min(srcY0, srcY1); + const GLint top = std::max(srcY0, srcY1); + const GLsizei width = static_cast(right - left); + const GLsizei height = static_cast(top - bottom); + if (width <= 0 || height <= 0) return false; + + // The scratch takes each aspect the READ framebuffer has: one it lacks is silently + // skipped by the blit either way, and leaving it off the scratch keeps it skipped. + GLenum colorFormat = 0; + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + const GLenum readAttachment = DriverReadColorAttachment(); + if (readAttachment != GL_NONE) { + colorFormat = QueryDriverReadAttachmentInternalFormat(readAttachment); + if (colorFormat == 0) return false; + } + } + GLenum depthStencilFormat = 0; + GLenum depthStencilAttachment = GL_NONE; + if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) { + const GLenum depthFormat = (mask & GL_DEPTH_BUFFER_BIT) != 0 + ? QueryDriverReadAttachmentInternalFormat(GL_DEPTH_ATTACHMENT) + : 0; + const GLenum stencilFormat = (mask & GL_STENCIL_BUFFER_BIT) != 0 + ? QueryDriverReadAttachmentInternalFormat(GL_STENCIL_ATTACHMENT) + : 0; + // ES blits depth/stencil only between identical formats, which a scratch renderbuffer + // of the source's own format is. Separate depth and stencil images would need two + // scratch buffers; that shape is declined. + if (depthFormat != 0 && stencilFormat != 0 && depthFormat != stencilFormat) return false; + depthStencilFormat = depthFormat != 0 ? depthFormat : stencilFormat; + if (depthStencilFormat != 0) { + depthStencilAttachment = IsPackedDepthStencilFormat(depthStencilFormat) ? GL_DEPTH_STENCIL_ATTACHMENT + : depthFormat != 0 ? GL_DEPTH_ATTACHMENT + : GL_STENCIL_ATTACHMENT; + } + } + if (colorFormat == 0 && depthStencilFormat == 0) return false; + + if (s_scratchContextGeneration != g_backendContextGeneration) { + // The ids belonged to a dead context; the context reclaimed them with it. + s_scratchFramebuffer = 0; + s_colorRenderbuffer = 0; + s_depthStencilRenderbuffer = 0; + s_colorFormat = 0; + s_depthStencilFormat = 0; + s_scratchContextGeneration = g_backendContextGeneration; + } + if (s_scratchFramebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &s_scratchFramebuffer); + if (s_scratchFramebuffer == 0) return false; + } + + GLint previousRenderbuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previousRenderbuffer); + const auto ensureStorage = [](Uint& renderbuffer, GLenum& currentFormat, GLsizei& currentWidth, + GLsizei& currentHeight, GLenum format, GLsizei w, GLsizei h) { + if (renderbuffer == 0) { + g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer); + currentFormat = 0; + } + if (renderbuffer == 0) return; + if (currentFormat != format || currentWidth < w || currentHeight < h) { + currentWidth = std::max(currentWidth, w); + currentHeight = std::max(currentHeight, h); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, format, currentWidth, currentHeight); + currentFormat = format; + } + }; + if (colorFormat != 0) { + ensureStorage(s_colorRenderbuffer, s_colorFormat, s_colorWidth, s_colorHeight, colorFormat, width, + height); + } + if (depthStencilFormat != 0) { + ensureStorage(s_depthStencilRenderbuffer, s_depthStencilFormat, s_depthStencilWidth, + s_depthStencilHeight, depthStencilFormat, width, height); + } + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previousRenderbuffer)); + + GLint previousDraw = 0; + GLint previousRead = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); + + // Every point is (re)attached on every call: the scratch is shared between colour and + // depth/stencil shapes, and a stale attachment would make it incomplete or blit an + // aspect the source does not have. + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_scratchFramebuffer); + g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + colorFormat != 0 ? s_colorRenderbuffer : 0); + g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0); + if (depthStencilAttachment != GL_NONE) { + g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, depthStencilAttachment, GL_RENDERBUFFER, + s_depthStencilRenderbuffer); + } + const GLbitfield stagedMask = + (colorFormat != 0 ? static_cast(GL_COLOR_BUFFER_BIT) : 0) | + (depthStencilFormat != 0 ? mask & static_cast(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) + : 0); + Bool copied = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; + if (copied) { + // The staging copy is not the application's blit: its scissor box would clip the + // copy and leave never-written scratch texels for the second blit to carry over. The + // second blit keeps the scissor, which is the application's semantics. + DrainBlitErrors(); + const Bool scissorWasEnabled = RenderStateImpl::DriverScissorTestEnabled(); + if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, stagedMask, GL_NEAREST); + if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); + copied = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + if (copied) { + // Mirror the caller's orientation into the scratch-relative source rectangle so a + // flipped blit stays flipped. + const GLint blitX0 = srcX0 <= srcX1 ? 0 : width; + const GLint blitX1 = srcX0 <= srcX1 ? width : 0; + const GLint blitY0 = srcY0 <= srcY1 ? 0 : height; + const GLint blitY1 = srcY0 <= srcY1 ? height : 0; + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_scratchFramebuffer); + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(blitX0, blitY0, blitX1, blitY1, dstX0, dstY0, dstX1, dstY1, stagedMask, + filter); + copied = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + return copied; + } + + static void IssueBlitWithResolveFallback(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, + GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + return; + } + // Two ES restrictions desktop GL does not have are worth a second attempt: a + // multisample resolve that also converts colour format, and any blit into a + // multisample draw framebuffer. Both need to know how the two sides are sampled. + GLint readSamples = 0; + GLint drawSamples = 0; + { + GLint previousRead = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); + GLint previousDraw = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); + g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(previousRead)); + g_GLESFuncs.glGetIntegerv(GL_SAMPLES, &readSamples); + g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, static_cast(previousDraw)); + g_GLESFuncs.glGetIntegerv(GL_SAMPLES, &drawSamples); + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + } + if (readSamples <= 0 && drawSamples <= 0 && DriverBlitAspectsSharingAnImage(mask) != 0) { + // ES refused it because one image is both ends; GL only refuses an overlap, which it + // leaves undefined. When the scratch cannot take the source, the per-aspect retries + // below still deliver every aspect that is not shared. + if (BlitWithinOneImage(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter)) { + DrainBlitErrors(); + return; + } + MGLOG_E_ONCE("BlitFramebuffer: a blit within one image could not be staged through a scratch copy"); + } + if (readSamples <= 0 && drawSamples > 0) { + // Single-sample source into a multisample destination: ES rejects the call + // outright, desktop GL replicates the source sample into every destination one. + if (ReplicateBlitIntoMultisampleDraw(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask)) { + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + MGLOG_E_ONCE("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated"); + } + } + return; + } + // The combined call raised an error, so by GL 4.6 2.3.1 it wrote nothing at all: BOTH + // aspect groups still owe their copy, and each has to be retried on its own. Re-issuing + // the depth/stencil half only as a rider on a SUCCESSFUL colour resolve dropped it + // silently whenever the colour half could not be emulated - and on a framebuffer whose + // only attachment is depth it never can, because the colour emulation has no attachment + // to take a format from (KHR-GL33.framebuffer_blit's depth config test blits + // COLOR|DEPTH|STENCIL across depth-only framebuffers and kept reading the clear value). + const GLbitfield colourBit = mask & static_cast(GL_COLOR_BUFFER_BIT); + const GLbitfield dsBits = mask & static_cast(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + // The colour group's one emulation is the multisample resolve that also converts format, + // which is the shape this names. It used to double as an early-out for the whole + // function, which is what cost a depth-only mask its single-aspect retry. + const Bool multisampleResolve = readSamples > 0 && drawSamples <= 0; + if (colourBit != 0) { + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, colourBit, filter); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + const Bool emulated = + multisampleResolve && + ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter); + if (!emulated) { + MGLOG_E_ONCE("BlitFramebuffer: the colour aspect was dropped - the driver rejected it on its " + "own and no emulation applies"); + } + } + } + if (dsBits != 0) { + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, dsBits, filter); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + // Nothing to fall back on yet: ResolveThenBlit is colour-only and the replicate + // pass runs in the opposite direction, so a driver that declines a multisample + // depth/stencil resolve leaves the destination holding its clear value. The log + // is the whole diagnostic - the frontend performs no validation of its own, so + // this never reaches the application as a GL error. + MGLOG_E_ONCE("BlitFramebuffer: the depth/stencil aspect was dropped - the driver rejected it on " + "its own and no emulation applies"); + } + } + DrainBlitErrors(); + } + + // ---- P5e (fb, CONTRACT-P5E.md §5.4): the same aspect plan, from the two RECORDS --------- + // + // The blit was the last entry point in this family that still reached into two frontend + // FramebufferObjects on every call - GetDrawBuffers, GetReadBuffer, GetAttachment and then + // six texture properties per aspect - and it did it on BOTH arms, the named one reaching + // the objects through the twin table's state note. Every one of those questions is a field + // of MGPFramebufferState: + // + // GetDrawBuffers() -> DrawBuffers[8] (attachment index, -1 = None) + // GetReadBuffer() -> ReadSurface matched against Color[] (the same match + // SyncReadBufferToBackend makes; ReadSurface is resolved + // from THIS framebuffer's own read buffer under every + // Target, Named included) + // GetAttachment(point) -> PushedSurfaceForAttachment + // IsTexture / IsLayered -> Kind / Layered + // GetTextureLevel / Layer -> Level / Layer + // GetFormat -> InternalFormat (the attachment's, which is the texture's) + // GetSamples -> record.Samples + // + // SAMPLES COMES OFF THE RECORD RATHER THAN OFF EACH TEXTURE, and that is a deliberate + // narrowing: the frontend form asked each endpoint texture, the record states it per + // FRAMEBUFFER. A framebuffer whose attachments disagree about sample count is incomplete, + // so the two cannot differ where this substitution is allowed to fire; and the test the + // answer feeds ("either end is multisample -> leave it to the driver") is conservative in + // the direction that hands the call back. + static FramebufferAttachmentType PushedReadBufferPoint(const MG_Pipe::MGPFramebufferState& record) { + if (MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res)) return FramebufferAttachmentType::None; + 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) { + return static_cast( + static_cast(FramebufferAttachmentType::Color0) + static_cast(i)); + } + } + return FramebufferAttachmentType::None; + } + + static GLbitfield BlitLayeredDestinationAspects(const MG_Pipe::MGPFramebufferState& readRecord, + const MG_Pipe::MGPFramebufferState& drawRecord, GLint srcX0, + GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, + GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) { + if (mask == 0) return 0; + if (!g_GLESFuncs.glCopyImageSubData) return 0; + if (!MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer(g_GLESFuncs)) return 0; + // The default framebuffer has no layers to get wrong. + if (readRecord.IsDefault != 0 || drawRecord.IsDefault != 0) return 0; + + const Int width = srcX1 - srcX0; + const Int height = srcY1 - srcY0; + const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height; + // The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two + // different operations no matter how the rectangles line up. + const Bool scissorEnabled = RenderStateImpl::DriverScissorTestEnabled(); + + using MobileGL::FramebufferAttachmentType; + struct AspectPlan { + GLbitfield bit; + FramebufferAttachmentType source; + FramebufferAttachmentType destination; + }; + // The buffer is FOUND rather than assumed to be slot 0, for the frontend form's reason: + // a blit writes every ENABLED draw buffer, and glDrawBuffers(NONE, NONE, NONE, + // COLOR_ATTACHMENT0) leaves slot 0 empty while still naming exactly one destination. + // -1 is the record's spelling of GL_NONE and is not "no such attachment". + Int enabledDrawBuffers = 0; + FramebufferAttachmentType colorDestination = FramebufferAttachmentType::None; + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + const Int8 index = drawRecord.DrawBuffers[i]; + if (index < 0) continue; + ++enabledDrawBuffers; + if (enabledDrawBuffers == 1) { + colorDestination = static_cast( + static_cast(FramebufferAttachmentType::Color0) + static_cast(index)); + } + } + const AspectPlan plans[] = { + {GL_COLOR_BUFFER_BIT, PushedReadBufferPoint(readRecord), colorDestination}, + {GL_DEPTH_BUFFER_BIT, FramebufferAttachmentType::Depth, FramebufferAttachmentType::Depth}, + {GL_STENCIL_BUFFER_BIT, FramebufferAttachmentType::Stencil, FramebufferAttachmentType::Stencil}, + }; + + GLbitfield handled = 0; + for (const AspectPlan& plan : plans) { + if ((mask & plan.bit) == 0) continue; + if (plan.source == FramebufferAttachmentType::Unknown || + plan.destination == FramebufferAttachmentType::Unknown || + plan.source == FramebufferAttachmentType::None || + plan.destination == FramebufferAttachmentType::None) { + continue; + } + const MG_Pipe::MGPSurface* const sourceSurface = + FramebufferImpl::PushedSurfaceForAttachment(readRecord, plan.source); + const MG_Pipe::MGPSurface* const destinationSurface = + FramebufferImpl::PushedSurfaceForAttachment(drawRecord, plan.destination); + if (sourceSurface == nullptr || destinationSurface == nullptr) continue; + // Renderbuffers have no layers, so a destination that is one cannot be hitting this. + if (sourceSurface->Kind != MG_Pipe::kMGPipeSurfaceKindTexture || + destinationSurface->Kind != MG_Pipe::kMGPipeSurfaceKindTexture) { + continue; + } + if (MG_Pipe::MGPipeHandleIsNull(sourceSurface->Res) || + MG_Pipe::MGPipeHandleIsNull(destinationSurface->Res)) { + continue; + } + // Layer 0 is the case the driver gets right, and a LAYERED attachment + // (glFramebufferTexture with no layer) blits its layer 0 by spec - neither is this + // defect. + if (destinationSurface->Layer == 0) continue; + if (destinationSurface->Layered != 0 || sourceSurface->Layered != 0) continue; + + // glCopyImageSubData moves texel blocks: same format both ends, or it is a different + // operation. Multisample endpoints would additionally have to agree on sample count, + // which is a resolve the driver still owns. + if (sourceSurface->InternalFormat != destinationSurface->InternalFormat) continue; + if (readRecord.Samples > 0 || drawRecord.Samples > 0) continue; + // Copying an image region onto itself is undefined for glCopyImageSubData, and a blit + // whose source and destination overlap is undefined for GL too - so this is not a + // shape to substitute FOR, it is one to leave exactly as the application wrote it. + if (sourceSurface->Res == destinationSurface->Res && + sourceSurface->Level == destinationSurface->Level && + sourceSurface->Layer == destinationSurface->Layer) { + continue; + } + + // A combined depth-stencil texture is ONE image to glCopyImageSubData: it carries both + // aspects across whether or not the mask asked for both. Taking only GL_DEPTH_BUFFER_BIT + // on a DEPTH24_STENCIL8 destination would overwrite a stencil the application asked to + // keep, so the copy is only allowed when the mask covers everything the format holds. + const auto format = static_cast(destinationSurface->InternalFormat); + const Bool hasDepth = MG_Util::IsDepthFormatInternalFormat(format); + const Bool hasStencil = MG_Util::IsStencilFormatInternalFormat(format); + if (hasDepth && (mask & GL_DEPTH_BUFFER_BIT) == 0) continue; + if (hasStencil && (mask & GL_STENCIL_BUFFER_BIT) == 0) continue; + // ... and having carried both, it must be credited with both, or the caller hands the + // stencil half to the driver and it lands on layer 0 after all. + const GLbitfield aspectBits = + hasDepth || hasStencil + ? static_cast((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) | + (hasStencil ? GL_STENCIL_BUFFER_BIT : 0)) + : static_cast(GL_COLOR_BUFFER_BIT); + if ((handled & aspectBits) == aspectBits) continue; + + if (!oneToOne || scissorEnabled || (plan.bit == GL_COLOR_BUFFER_BIT && enabledDrawBuffers != 1)) { + MGLOG_E_ONCE("BlitFramebuffer: this driver ignores a non-zero destination array layer and this " + "blit cannot be expressed as a copy (%s), so it will land on layer 0", + !oneToOne ? "it scales or flips" + : scissorEnabled ? "the scissor test is enabled" + : "the destination has more than one draw buffer"); + continue; + } + + auto& backendSource = + TextureImpl::SyncTextureToBackendByHandle(sourceSurface->Res, + /*imageBindableStorageRequired=*/false); + if (!backendSource) continue; + const GLuint sourceName = backendSource->GetBackendTextureId(); + // BY VALUE past this point, for the reason the copy-image endpoint builder gives: + // the second sync can grow the registry and relocate the first result. + auto& backendDestinationSlot = + TextureImpl::SyncTextureToBackendByHandle(destinationSurface->Res, + /*imageBindableStorageRequired=*/false); + if (!backendDestinationSlot) continue; + const GLuint destinationName = backendDestinationSlot->GetBackendTextureId(); + if (sourceName == 0 || destinationName == 0) continue; + const GLenum sourceTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( + static_cast(sourceSurface->TextureTarget)); + const GLenum destinationTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( + static_cast(destinationSurface->TextureTarget)); + + ClearGLErrors(); + g_GLESFuncs.glCopyImageSubData(sourceName, sourceTarget, static_cast(sourceSurface->Level), + srcX0, srcY0, static_cast(sourceSurface->Layer), + destinationName, destinationTarget, + static_cast(destinationSurface->Level), dstX0, dstY0, + static_cast(destinationSurface->Layer), width, height, 1); + if (const GLenum error = g_GLESFuncs.glGetError(); error != GL_NO_ERROR) { + // The driver blit still runs for this aspect - onto the wrong layer, but the + // substitute has to leave the call no worse off than it found it. + MGLOG_E_ONCE("BlitFramebuffer: the layered-destination copy substitute failed with %s; the " + "driver blit will run instead and land on layer 0", + MG_Util::ConvertGLEnumToString(error).c_str()); + continue; + } + handled |= aspectBits; + } + return handled & mask; + } + + // The two endpoint records of a blit, or nulls. `fbo` null-or-default means "the record of + // whatever is bound to that target", which is what the bound arm asks; a named blit passes + // the handles its record carried. + static const MG_Pipe::MGPFramebufferState* BlitEndpointRecord(MG_Pipe::MGPipeHandle fbo, + FramebufferTarget boundTarget) { + const auto& st = MG_Pipe::MGPipeApplier(); + if (MG_Pipe::MGPipeHandleIsNull(fbo)) { + return boundTarget == FramebufferTarget::Read ? st.ReadFramebuffer() : st.DrawFramebuffer(); + } + return st.FramebufferRecordFor(fbo); + } + + void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, + GLint dstY1, GLbitfield mask, GLenum filter) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + + // P5c (hd, CONTRACT-P5C §3.3): THE NAMED ARM. A blit record whose ReadFbo/DrawFbo are + // non-null is a glBlitNamedFramebuffer: both framebuffers resolve from the record's + // handles through the FBO twin table (the client's ScopedBlitBindings staging and this + // function's binding-slot read at the bottom are gone from the split path), and the + // sink is told the pair was consumed - a backend that reaches here without consuming + // it has no named arm and the verb declines there. + auto& applierState = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle readFbo = applierState.VerbBlitReadFbo; + const MG_Pipe::MGPipeHandle drawFbo = applierState.VerbBlitDrawFbo; + if (!MG_Pipe::MGPipeHandleIsNull(readFbo) || !MG_Pipe::MGPipeHandleIsNull(drawFbo)) { + applierState.VerbBlitNamedConsumed = true; + TextureImpl::SyncNeccessaryTextures(); + RenderStateImpl::SyncRenderState(); + + SyncAndBindFramebufferByHandle(readFbo, FramebufferTarget::Read, /*forceSync=*/true); + SyncAndBindFramebufferByHandle(drawFbo, FramebufferTarget::Draw, /*forceSync=*/true); + + MGLOG_D("ES BlitNamedFramebuffer({%u,%u} -> {%u,%u}, %d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", + readFbo.Slot, readFbo.Gen, drawFbo.Slot, drawFbo.Gen, srcX0, srcY0, srcX1, + srcY1, dstX0, dstY0, dstX1, dstY1, mask, + MG_Util::ConvertGLEnumToString(filter).c_str()); + // P5e (fb, §5.4): only the probed defect makes this do anything, and it now + // asks the two RECORDS. The state-note detour this used to take - the twin + // table handing back the frontend object it was synced from, plus + // pDefaultFramebufferInfo->defaultFBO for a default endpoint - is gone with the + // note itself; a named framebuffer always has a record (a Named record precedes + // every DSA site, ID-19), so the "missing object skips the workaround" case is + // now a missing record and means the same thing. + const auto* readRecord = BlitEndpointRecord(readFbo, FramebufferTarget::Read); + const auto* drawRecord = BlitEndpointRecord(drawFbo, FramebufferTarget::Draw); + if (readRecord != nullptr && drawRecord != nullptr) { + mask &= ~BlitLayeredDestinationAspects(*readRecord, *drawRecord, srcX0, srcY0, srcX1, + srcY1, dstX0, dstY0, dstX1, dstY1, mask); + } + if (mask != 0) { + IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, + mask, filter); + } + // The driver's bindings are now the two NAMED framebuffers; put the bound ones + // back exactly as the monolith named entry point does. + ForceBindCurrentFBO(FramebufferTarget::Read); + ForceBindCurrentFBO(FramebufferTarget::Draw); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + return; + } + + TextureImpl::SyncNeccessaryTextures(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + FramebufferImpl::SyncCurrentFBO(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + RenderStateImpl::SyncRenderState(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + BindCurrentFBO(FramebufferTarget::Draw); + BindCurrentFBO(FramebufferTarget::Read); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, + dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str()); + // A no-op on every driver that honours a non-zero destination array layer, which is all + // of them but the probed one. Whatever it performs itself is taken out of the mask. + // P5e (fb, §5.4): the bound arm's two binding-slot reads were the last GetBoundObject + // pair on this path; under a transport the two bound RECORDS answer instead + // (FramebufferRecordFor(st.BoundFramebuffer[t]), through BlitEndpointRecord's null + // case). SyncCurrentFBO ran a few lines up and refuses if either is missing, so a null + // here means the blit is against the default framebuffer's record, which the overload + // declines on IsDefault. + const auto* readRecord = + BlitEndpointRecord(MG_Pipe::kMGPipeNullHandle, FramebufferTarget::Read); + const auto* drawRecord = + BlitEndpointRecord(MG_Pipe::kMGPipeNullHandle, FramebufferTarget::Draw); + if (readRecord != nullptr && drawRecord != nullptr) { + mask &= ~BlitLayeredDestinationAspects(*readRecord, *drawRecord, srcX0, srcY0, srcX1, + srcY1, dstX0, dstY0, dstX1, dstY1, mask); + } + if (mask != 0) { + IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + + namespace RenderStateImpl { + void RunScissorDisabledForTesting(void (*inside)(void*), void* data) { + ScopedScissorDisable scissorOff; + inside(data); + } + } // namespace RenderStateImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp new file mode 100644 index 00000000..70ebfe5a --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp @@ -0,0 +1,720 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace BufferImpl { + // See Managers.h for why these two are named functions over the applier state and + // nothing else. + Uint ResolveDrawVertexBuffersFromRecord(const MG_Pipe::MGPipeApplierState& st, + DrawVertexBufferRequest* out, Uint capacity) { + Uint count = 0; + const Uint32 begin = st.VertexBufferStart; + const Uint32 end = begin + st.VertexBufferCount; + for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) { + const MG_Pipe::MGPVertexBuffer& binding = st.VertexBuffers[i]; + if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) continue; + + // A {slot, gen} compare, not a raw address compare: an address a successor + // object can reproduce is the identity hazard this phase exists to remove. + Bool alreadySeen = false; + for (Uint j = 0; j < count; ++j) { + if (out[j].Res == binding.Res) { + alreadySeen = true; + break; + } + } + if (alreadySeen) continue; + if (count >= capacity) { + MGLOG_E_ONCE("MGPipe: the vertex-buffer window named more distinct buffers than " + "a draw can hold (%u); the tail is dropped", capacity); + break; + } + out[count].Res = binding.Res; + out[count].BindingIndex = binding.BindingIndex; + ++count; + } + return count; + } + + DrawIndexBufferRequest ResolveDrawIndexBufferFromRecord(const MG_Pipe::MGPipeApplierState& st) { + DrawIndexBufferRequest request; + request.Res = st.IndexBuffer.Res; + request.Serial = st.IndexBufferSerial; + return request; + } + + // True from a draw or dispatch that ran with a storage buffer bound until the next + // barrier that orders storage-buffer writes. Every bound storage point counts as written, + // the client's own conservative set (see serverGpuWritten below). + // + // WHAT IT IS FOR: two dispatches with no barrier between them that write DIFFERENT + // components of one vector in the same storage buffer. GL leaves the two unordered, which + // allows them to run concurrently, but every write still lands - they are separate memory + // locations. A driver that stores a vector component as a read-modify-write of the whole + // vector loses one of them instead: KHR-GL43.shader_storage_buffer_object. + // advanced-switchBuffers-cs writes digest.x .. digest.w of one uvec4 from four + // back-to-back dispatches and read back a zero component in 8 runs of 20 on Adreno (0 of + // 20 with a storage barrier between the dispatches, 0 of 20 with uint[4]). The fix is the + // barrier the application did not owe: PrepareForCompute issues it when the previous + // shader work may have written a storage buffer and nothing ordered those writes since. + // + // Compute only. A dispatch that follows a dispatch is the shape the test has and the one + // a driver may overlap; draws keep their own ordering and are not taxed, they only SET + // the flag here - one store in the walk that already visits every bound point. + Bool g_shaderStorageWritesUnordered = false; + + // The record arm of SyncBufferBindingPoints. Same shape, same order, same branches; the + // four frontend reads become the applier's window and a handle per entry. + // + // WHAT THE WINDOW IS: ShaderBufferCount[class] starting at ShaderBufferStart[class], + // which is the client's touched high-water mark clamped to the 84 the wire carries + // (ruling 10). It is NOT clamped on the client to the device's ceiling, so the ES clamp + // below stays exactly where it was - a client reading GL_MAX_UNIFORM_BUFFER_BINDINGS + // would be answering a driver question from the wrong side of the wire. + // + // WHOLE-VS-RANGE IS `Offset == 0 && Size == kMGPipeWholeBuffer` and obj->GetSize() + // disappears from this function: a base binding does not freeze an extent, the client + // deliberately does not resolve one, and the re-resolution happens HERE against the + // server's own storage - which is what makes a glBufferData issued between the emission + // and this apply bind the NEW extent, as GL does. + void SyncBufferBindingPointsByRecord(Uint32 shaderBufferClass, GLenum glTarget) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); + const Uint32 start = st.ShaderBufferStart[shaderBufferClass]; + SizeT end = static_cast(start) + st.ShaderBufferCount[shaderBufferClass]; + // ...and never past what the ES driver itself can hold, exactly as the frontend + // walk does and for the same reason recorded there. + if (glTarget == GL_UNIFORM_BUFFER && g_GLESCapabilities.MaxUniformBufferBindings > 0) { + end = std::min(end, static_cast(g_GLESCapabilities.MaxUniformBufferBindings)); + } + for (SizeT i = start; i < end; ++i) { + const MG_Pipe::MGPBufferRange& entry = + st.BoundShaderBuffers[shaderBufferClass][i]; + if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) { + BindBufferBaseCached(glTarget, static_cast(i), 0); + continue; + } + auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry.Res); + if (!backendResource || backendResource->id == 0) { + MGLOG_E_ONCE("No backend buffer found for %s binding point %zu (handle {%u, %u}).", + MG_Util::ConvertGLEnumToString(glTarget).c_str(), i, entry.Res.Slot, + entry.Res.Gen); + continue; + } + // P8-C: the SERVER's half of the mark MarkShaderStorageBuffersGpuWritten no longer + // makes under a transport - on this side's twin, for this side's CPU readers + // (SplitHostBytesForCpuRead). Every bound storage point, which is the client's + // own conservative set (ShaderBufferEmit.h marks every one writable); a uniform + // point is read-only by definition. + if (shaderBufferClass != MG_Pipe::kMGPipeShaderBufferClassUniform) { + backendResource->serverGpuWritten = true; + g_shaderStorageWritesUnordered = true; + } + const auto backendBufferId = backendResource->id; + if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) { + BindBufferBaseCached(glTarget, static_cast(i), backendBufferId); + } else { + // CLAMPED AGAINST THE SERVER's OWN STORAGE, which is what the frontend walk + // clamped against obj->GetSize() for: a range that outruns the storage is a + // GL_INVALID_VALUE the driver would raise on a bind the application already + // made legally against a buffer that has since shrunk. + const SizeT storage = backendResource->storageSize; + const SizeT rangeStart = std::min(static_cast(entry.Offset), storage); + const SizeT rangeEnd = + std::min(static_cast(entry.Offset + entry.Size), storage); + BindBufferRangeCached(glTarget, static_cast(i), backendBufferId, + static_cast(rangeStart), + static_cast(rangeEnd - rangeStart)); + } + } + } + + void SyncAtomicCounterBuffers(const Vector& glBindings, Int esslBindingTop) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // P5e (sb, §5.6): THE RECORD ARM. `glBindings` stays the backend program's own list + // - it is server-owned since the link and names which GL counter bindings THIS + // program declares - and only the points themselves move sides. The bound is the + // applier's window instead of GetBufferBindingPointCount's sticky forward, which is + // the row this site was the Espryt reader of. + const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); + const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; + const SizeT windowEnd = static_cast(windowStart) + + st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; + for (const Int glBinding : glBindings) { + if (glBinding < 0) continue; + const Int esslBinding = esslBindingTop - glBinding; + // Already diagnosed once when the block was transpiled; nothing was bound to + // it there either, so there is nothing to unbind here. + if (esslBinding < 0) continue; + // OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's + // default says too - so it takes the unbind arm rather than being skipped. + // Skipping would leave whatever the driver last had at that ESSL point, + // which is the hole the frontend walk's `continue` never had because its + // bound was the whole 84-point array. + const MG_Pipe::MGPBufferRange* entry = nullptr; + if (static_cast(glBinding) >= windowStart && + static_cast(glBinding) < windowEnd) { + entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter] + [static_cast(glBinding)]; + } + if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) { + BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), 0); + continue; + } + auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res); + if (!backendResource || backendResource->id == 0) { + MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d " + "(handle {%u, %u}).", + glBinding, entry->Res.Slot, entry->Res.Gen); + continue; + } + if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) { + BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), + backendResource->id); + } else { + const SizeT storage = backendResource->storageSize; + const SizeT rangeStart = std::min(static_cast(entry->Offset), storage); + const SizeT rangeEnd = + std::min(static_cast(entry->Offset + entry->Size), storage); + BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), + backendResource->id, static_cast(rangeStart), + static_cast(rangeEnd - rangeStart)); + } + // AND NO MarkBufferGpuWritten HERE. It was the second of the three backend + // GPU-write sites and it reached into a client object; the client's + // MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set + // from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched + // counter point rather than only the ones this program declares, which is + // the safe direction for an over-approximate set. + // + // P8-C: the server's own twin is marked instead, for the server's own CPU + // readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a + // parameter or element buffer is read on this side. + backendResource->serverGpuWritten = true; + } + return; + } + } // namespace BufferImpl + + namespace XfbImpl { + // One scratch ES buffer serves every scattered capture: only one span can be + // recording at a time (the driver would reject a second Begin), so its contents + // are consumed by the End that follows. + GLuint g_scatterBufferId = 0; + SizeT g_scatterBufferSize = 0; + + UnorderedMap g_xfbObjects; + GLuint g_currentXfbName = 0; + // Cached address of g_xfbObjects[g_currentXfbName]: PrepareForDraw consults + // CurrentXfb on EVERY draw (StartPendingTransformFeedback) and the map + // lookup was pure per-draw overhead for the overwhelmingly common no-capture + // case. Open addressing keeps values in the bucket array, so ANY insert can + // rehash and move them - and erase moves them too, by shifting the rest of the + // probe cluster into the hole, which reaches entries other than the erased one. + // Every site that mutates the map or rebinds the current name resets this to + // null instead of reasoning about stability, and CurrentXfb re-resolves lazily. + XfbObjectState* g_currentXfbState = nullptr; + // P13 W6: one role - the record arm's (the frontend arm's GL-name keyed state went). + Array g_roleXfbState; + + namespace { + RoleXfbState& ActiveXfbState() { + auto& state = g_roleXfbState[0]; + if (state.NativeGeneration != g_backendContextGeneration) { + state = {}; + state.NativeGeneration = g_backendContextGeneration; + } + return state; + } + } // namespace + + XfbObjectState& CurrentXfb() { + auto& state = ActiveXfbState(); + const Uint64 lifetime = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId; + if (state.Current != lifetime || state.Cached == nullptr || state.NeedsBind) + BindTransformFeedback(0); // identity is the applier's lifetime, not this argument + auto& g_currentXfbState = state.Cached; + auto& g_xfbObjects = state.Objects; + const auto g_currentXfbName = state.Current; + if (g_currentXfbState == nullptr) { + g_currentXfbState = &g_xfbObjects[g_currentXfbName]; + } + return *g_currentXfbState; + } + + namespace { + Bool AreTransformFeedbackObjectsSupported() { + return g_GLESFuncs.glGenTransformFeedbacks != nullptr && + g_GLESFuncs.glBindTransformFeedback != nullptr && + g_GLESFuncs.glDeleteTransformFeedbacks != nullptr && + g_GLESFuncs.glPauseTransformFeedback != nullptr && + g_GLESFuncs.glResumeTransformFeedback != nullptr; + } + + // How wide one OnBufferWriteback may be for a capture range of `rangeBytes`, which is + // the whole range when the transport imposes no limit (monolith, where the callback is + // a direct call with no ring under it) or when the range already fits one. + // + // BOTH XFB WRITEBACK PRODUCERS ASK THE SAME FUNCTION. They post from different sources + // - one from a live glMapBufferRange mapping, one from a staged Vector - but + // they post the same KIND of record into the same ring, and a second spelling of the + // width is how one of them ends up being the producer that still kills the server. + SizeT XfbWritebackSliceStep(SizeT rangeBytes) { + const Uint64 slice = MG_Pipe::MGPipeBufferWritebackSliceBytes(); + if (slice == 0 || slice >= static_cast(rangeBytes)) return rangeBytes; + return static_cast(slice); + } + + // Mirrors one capture span's results into the frontend CPU shadows. The GPU wrote + // the capture buffers behind the frontend's back, so the shadows that back + // MapBuffer/GetBufferSubData still hold the pre-draw bytes. Buffers whose storage + // the backend already owns (coherent persistent map) need nothing: reads resolve + // against that storage directly. + void ReadbackCapturedRanges(Vector& targets) { + if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) { + MGLOG_E_ONCE("EndTransformFeedback: the ES driver exposes no glMapBufferRange/glUnmapBuffer, so " + "captured data can never reach the application's buffers"); + } + if (targets.empty()) { + // The span closed with nothing to mirror back. Either the deferred Begin + // never ran (a span with no draw - legal) or it ran and found no bound + // capture buffer, which is not. + MGLOG_D("EndTransformFeedback: capture span closed with no recorded targets"); + } + if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) { + for (const auto& target : targets) { + // P5c: under an active transport the frontend object is client + // memory (rule E), so the persistence question is the server + // resource's and the captured bytes go back as a writeback EVENT - + // WritebackFromBackend from the apply thread is the R1/R2 shape. + const MG_Pipe::MGPipeHandle res = target.handle; + auto* resource = BufferImpl::FindBufferResourceForHandle(res); + if (resource == nullptr) continue; + if (resource->persistentMapped) { + // P8-C: nothing to copy - the capture reaches a coherent map - + // but this side's CPU readers must finish before reading it. + resource->serverGpuWritten = true; + continue; + } + const SizeT size = target.end - target.start; + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId); + void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, + static_cast(target.start), + static_cast(size), GL_MAP_READ_BIT); + if (mapped == nullptr) { + MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u [%zu, %zu) for " + "capture readback (ES error %s); the captured data will NOT be visible to " + "the application", + target.backendId, target.start, target.end, + MG_Util::ConvertGLEnumToString(TakeXfbDriverError()).c_str()); + continue; + } + if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) { + // ONE EVENT PER SLICE, NOT ONE PER CAPTURE. The bytes travel + // INLINE in a SEG_EVENT record and the ring refuses any record + // above half its capacity outright, so a whole-range post of a + // capture wider than ~128 KiB was Fatal{EventRingOverflow} - the + // SERVER dying on a large buffer. The slicing shape is + // BufferObject.cpp's one ring over: in-order delivery makes the + // last slice's landing imply every earlier one, so the client + // needs nothing new to reassemble them. + // + // INSIDE THE MAPPING, deliberately: the map is the expensive part + // and the slices are reads of one live pointer, so this unmaps + // once below however many events it posts. + const SizeT step = XfbWritebackSliceStep(size); + for (SizeT off = 0; off < size; off += step) { + const SizeT bytes = std::min(step, size - off); + MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( + res, target.start + off, + MG_Pipe::MGPBlobRef{ + reinterpret_cast(static_cast(mapped) + off), + static_cast(bytes), MG_Pipe::kMGHostSpanSegNone, 0}); + } + } else { + MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the " + "captured bytes of buffer {%u,%u} cannot reach the client shadow", + res.Slot, res.Gen); + } + // P8-C: the server's own staged copy takes the same bytes, from the + // same mapping, so this side's CPU readers see the capture too. + BufferImpl::StageServerWrittenRange(*resource, res, mapped, target.start, size); + g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget); + // Ops_H_Readback's order: writeback, unmap, THEN the serial + // stamp, so the next draw does not re-upload the server's stale + // staged bytes over the capture that just landed. + resource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(res); + BufferImpl::BumpBufferMutationEpoch(); + continue; + } + } + targets.clear(); + } + + // Distributes the gap-free records the driver captured into the application's + // buffers at the offsets the GL layout asks for. Only the bytes a varying actually + // occupies are written, so the holes gl_SkipComponents asks for keep whatever the + // application had put there - which is the whole point of the feature. + void ScatterCapturedRecords(XfbObjectState& xfb) { + auto& g_scatterBufferId = xfb.serverScatterBuffer; + const XfbProgramSource program{xfb.scatterProgram, xfb.scatterArchive}; + if (!program || xfb.targets.empty()) return; + if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return; + + const SizeT packedStride = program->GetTransformFeedbackPackedStride(); + const SizeT modelledVertices = + static_cast(MG_Pipe::gPipeInputs->GetTransformFeedbackCapturedVertices()); + const SizeT vertices = std::min(modelledVertices, xfb.scatterCapacityVertices); + if (packedStride == 0 || vertices == 0) { + // The scatter path redirected the DRIVER's capture into the scratch buffer, + // so bailing here leaves the application's buffers holding their pre-draw + // bytes - a total data loss, not a no-op. The vertex count is the CPU model + // (AccountTransformFeedbackPrimitives), which is 0 for any draw mode + // CountPrimitivesForDraw does not know and for the instanced/indirect entry + // points that never call it. + MGLOG_E_ONCE("EndTransformFeedback: scattered capture discarded - packedStride=%zu, " + "CPU-modelled captured vertices=%zu, scratch capacity=%zu. The capture buffers keep " + "their pre-draw contents.", + packedStride, modelledVertices, xfb.scatterCapacityVertices); + return; + } + + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); + const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0, + static_cast(packedStride * vertices), + GL_MAP_READ_BIT); + if (packed == nullptr) { + MGLOG_E_ONCE("EndTransformFeedback: failed to map the scatter capture buffer"); + return; + } + + // One staged copy per destination buffer: start from what the application had + // (the shadow is authoritative - uploads go shadow -> ES, and previous captures + // were mirrored back into it), patch the captured varyings in, then push the + // whole range down once. + for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) { + const auto& target = xfb.targets[targetIndex]; + // By BUFFER index, not by position in the compacted list - see XfbCaptureTarget. + const SizeT stride = program->GetTransformFeedbackStride(static_cast(target.bufferIndex)); + if (stride == 0) continue; + const SizeT rangeBytes = target.end - target.start; + Vector staged(rangeBytes); + // P5c: under an active transport BufferObject::MappedData on the apply + // thread is Fatal{RoleViolation, "buffer-legacy-arm"} (CONTRACT-P5C §3.8) - + // the pre-capture bytes are the SERVER's staged shadow, and the reconciled + // range goes back as a writeback EVENT instead of a WritebackFromBackend + // poke into client memory. + MG_Pipe::MGPipeHandle splitRes = MG_Pipe::kMGPipeNullHandle; + BufferImpl::GLESBufferResource* splitResource = nullptr; + splitRes = target.handle; + splitResource = BufferImpl::FindBufferResourceForHandle(splitRes); + if (splitResource == nullptr) { + // THE ONLY DISCARD LEFT, and it is the impossible one: this target + // was recorded by BindBufferRange/Base against a handle the applier + // had already minted a twin for, so no twin here means the slot + // table and the capture list disagree about one resource. There is + // no backendId to upload into either, so there is nothing to do but + // name it. + MGLOG_E_ONCE("EndTransformFeedback: a scattered capture target (handle {%u,%u}) has " + "no server twin at all; its capture is discarded", + splitRes.Slot, splitRes.Gen); + continue; + } + // P8-C: made current first - a store a shader wrote here is refreshed. + const Uint8* hostBytes = BufferImpl::SplitHostBytesForCpuRead(*splitResource, splitRes, + "xfb_scatter_pre_capture"); + // A NULL SHADOW IS NOT A LOST CAPTURE. It is the ORPHANED store + // (glBufferData(size, NULL) with no resource_subdata since), which + // M-3 declares legal: every byte the application has not staged is + // UNDEFINED by its own declaration, and `staged` is already + // value-initialised to zero - which is byte for byte what the monolith + // arm scatters over when it reads a fresh MappedData(). Skipping the + // Memcpy is therefore the WHOLE of the difference; skipping the TARGET + // threw the varyings away and left the application's buffer holding its + // pre-draw bytes under GL_NO_ERROR, the silent-loss class this file's + // preamble exists to close. The streaming idiom that hits it - orphan, + // partial glBufferSubData, capture into the rest - is ordinary. + if (hostBytes != nullptr) { + // Managers.cpp's pool-reuse ladder guards its whole-store + // MGL_SERVER_STAGED_REQUIRE with exactly this predicate and for + // exactly this reason: for a store whose content the application + // SUPPLIED a coverage gap is a missing record and zero-filling past + // it is silent data loss (Fatal by name); for an orphaned one the + // gap is its own undefined content and the read is legal. + if (BufferImpl::ResourceContentIsDeclared(splitRes)) { + BufferImpl::RequireStagedCoverage(*splitResource, hostBytes, target.start, + target.end, "xfb_scatter_pre_capture"); + } + Memcpy(staged.data(), hostBytes + target.start, rangeBytes); + } + + for (const auto& varying : program->GetTransformFeedbackVaryings()) { + if (varying.bufferIndex != target.bufferIndex) continue; + for (SizeT v = 0; v < vertices; ++v) { + const SizeT dstOffset = v * stride + varying.offsetBytes; + if (dstOffset + varying.byteSize > rangeBytes) break; + Memcpy(staged.data() + dstOffset, + static_cast(packed) + v * packedStride + varying.packedOffsetBytes, + varying.byteSize); + } + } + + if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) { + // Sliced for the reason the readback path above is sliced: the bytes + // are INLINE in a SEG_EVENT record and the ring refuses a record wider + // than half its capacity outright, so one whole-range post of a + // capture over ~128 KiB was Fatal{EventRingOverflow} and the server + // died. Only the EVENT is cut; the glBufferSubData below stays one + // call over the whole `staged` vector, because the ES store has no + // ring and cutting it would change how many backend calls one + // application call makes. + const SizeT step = XfbWritebackSliceStep(rangeBytes); + for (SizeT off = 0; off < rangeBytes; off += step) { + const SizeT bytes = std::min(step, rangeBytes - off); + MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( + splitRes, target.start + off, + MG_Pipe::MGPBlobRef{reinterpret_cast(staged.data() + off), + static_cast(bytes), + MG_Pipe::kMGHostSpanSegNone, 0}); + } + } else { + MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the " + "scattered capture of buffer {%u,%u} cannot reach the client shadow", + splitRes.Slot, splitRes.Gen); + } + // P8-C: the staged copy takes the composed range too, so this side's CPU + // readers see the capture (before P8 it kept the pre-capture bytes). + BufferImpl::StageServerWrittenRange(*splitResource, splitRes, staged.data(), target.start, + rangeBytes); + // Ops_H_Readback's stamp, for its reason: the GL store now holds the + // capture, and the next draw must not re-upload an older range over it. + splitResource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(splitRes); + // Serial bumped with no backend op (see ReadbackCapturedRanges). + BufferImpl::BumpBufferMutationEpoch(); + if (g_GLESFuncs.glBufferSubData != nullptr) { + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId); + g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, + static_cast(target.start), + static_cast(rangeBytes), staged.data()); + } + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); + } + g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget); + xfb.targets.clear(); + } + } // namespace + + Bool AreTransformFeedbacksSupported() { + return g_GLESFuncs.glBeginTransformFeedback != nullptr && + g_GLESFuncs.glEndTransformFeedback != nullptr && + g_GLESFuncs.glTransformFeedbackVaryings != nullptr; + } + + Bool IsCaptureSpanOpen() { + const auto& xfb = CurrentXfb(); + return (xfb.pending || xfb.started) && !xfb.paused; + } + + void BeginTransformFeedback(GLenum primitiveMode) { + if (!AreTransformFeedbacksSupported()) return; + auto& xfb = CurrentXfb(); + xfb.primitiveMode = primitiveMode; + xfb.pending = true; + xfb.started = false; + xfb.paused = false; + xfb.targets.clear(); + } + + void EndTransformFeedback() { + auto& xfb = CurrentXfb(); + const Bool wasPending = xfb.pending; + xfb.pending = false; + xfb.paused = false; + if (!xfb.started) { + // A span that never drew is legal and captures nothing by definition; one that + // is STILL pending here drew nothing the backend saw, which for a span the + // application expected data from is the whole bug in one line. + MGLOG_D("EndTransformFeedback: closing a span the driver never opened (pending=%d)", + wasPending ? 1 : 0); + return; + } + xfb.started = false; + g_GLESFuncs.glEndTransformFeedback(); + if (const GLenum endError = TakeXfbDriverError(); endError != GL_NO_ERROR) { + MGLOG_E_ONCE("EndTransformFeedback: the ES driver rejected glEndTransformFeedback with %s - the " + "driver's capture state and MobileGL's have diverged", + MG_Util::ConvertGLEnumToString(endError).c_str()); + } + if (xfb.scattered) { + ScatterCapturedRecords(xfb); + xfb.scattered = false; + xfb.scatterProgram.reset(); + xfb.scatterArchive.reset(); + } else { + ReadbackCapturedRanges(xfb.targets); + } + } + + void PauseTransformFeedback() { + auto& xfb = CurrentXfb(); + xfb.paused = true; + // A span the driver never opened (paused before the first draw) has nothing to + // pause; the flag above is what holds the deferred Begin back until the resume. + if (!xfb.started || g_GLESFuncs.glPauseTransformFeedback == nullptr) return; + g_GLESFuncs.glPauseTransformFeedback(); + } + + void ResumeTransformFeedback() { + auto& xfb = CurrentXfb(); + xfb.paused = false; + if (!xfb.started || g_GLESFuncs.glResumeTransformFeedback == nullptr) return; + g_GLESFuncs.glResumeTransformFeedback(); + } + + void BindTransformFeedback(GLuint name) { + auto& roleState = ActiveXfbState(); + auto& g_currentXfbState = roleState.Cached; + auto& g_xfbObjects = roleState.Objects; + auto& g_currentXfbName = roleState.Current; + const Uint64 key = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId; + (void)name; + roleState.NeedsBind = false; + g_currentXfbState = nullptr; // name changes; operator[] below may also rehash + // The capture buffer bindings are the OBJECT's, not the context's: the bind below + // swaps all of them for whatever the target object holds, which the redundant-bind + // shadow has never seen. + BufferImpl::InvalidateTransformFeedbackBindingShadows(); + if (!AreTransformFeedbackObjectsSupported()) { + // Without driver objects there is only the default span; keep the frontend + // name so the bookkeeping below stays consistent. + g_currentXfbName = + key; + return; + } + auto& xfb = g_xfbObjects[key]; + // Even each virtual context's name-0 object gets its own native object. + if (xfb.esId == 0) { + g_GLESFuncs.glGenTransformFeedbacks(1, &xfb.esId); + } + g_GLESFuncs.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, xfb.esId); + g_currentXfbName = + key; + } + + void DeleteTransformFeedback(GLuint name) { + auto& roleState = ActiveXfbState(); + auto& g_currentXfbState = roleState.Cached; + auto& g_xfbObjects = roleState.Objects; + auto& g_currentXfbName = roleState.Current; + const Uint64 key = MG_Pipe::MGPipeApplier().VerbDeleteStreamOutputLifetimeId; + (void)name; + // A generated but never-bound name has no native object or lifetime. + if (key == 0) return; + const auto it = g_xfbObjects.find(key); + if (it == g_xfbObjects.end()) return; + if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) { + g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId); + } + g_currentXfbState = nullptr; // erase shifts the probe cluster, moving other entries + g_xfbObjects.erase(it); + // The frontend reverts to the default object when the bound one is deleted. + if (g_currentXfbName == + key + ) { + // The following BindStreamOutput names this context's actual + // default lifetime. Never create a native object for key 0. + g_currentXfbName = 0; + roleState.NeedsBind = true; + BufferImpl::InvalidateTransformFeedbackBindingShadows(); + return; + } + } + + // The ES context went away (or is being torn down): the spans, their buffer ids, the + // driver objects and the frontend objects they pinned all belonged to it. + void OnBackendContextDestroyed() { + for (auto& state : g_roleXfbState) state = {}; + g_currentXfbState = nullptr; + g_xfbObjects.clear(); + g_currentXfbName = 0; + g_scatterBufferId = 0; + g_scatterBufferSize = 0; + BufferImpl::InvalidateTransformFeedbackBindingShadows(); + } + } // namespace XfbImpl + + namespace VertexArrayImpl { + // THE CLIENT-MEMORY VERTEX ARRAY UPLOAD, and the one read of this family that P5e does + // NOT retire - it MOVES the decision instead, which is what ID-82 / ruling 2 settles. + // + // The bytes of a client-memory array exist only in the application's own memory: the + // emitter publishes Res == kMGPipeNullHandle for such an attribute by design + // (VertexInputEmit.h) and this upload is where the server used to dereference + // attrib.Offset as a raw client pointer (Managers.cpp's + // SyncClientSideAttributesForDrawArrays, kimi audit row 14). Under run-ahead those bytes + // are movable and the read is torn by construction, so the CLIENT refuses the draw by + // name before it is ever emitted (EmitTables.cpp, Fatal{UnmigratedVerb, + // "DrawArrays+CLIENT_ARRAYS"}); staging them as a record tail is P8's. + // + // What is left here is therefore reachable in exactly two situations, and both are safe: + // * monolith / the push-monolith build - one process, one thread, no wire; + // * a LOCKSTEP split session - the client is parked in WaitForApplied for + // this very record, so its memory is stable + // (rule F is scoped to unbarriered records, + // ruling 4). Escalation (ii) of §2.1 pins the + // same thing from the other side: a draw + // carrying kDrawClientArrays is BARRIERED. + // Making it monolith-only instead would silently drop the attribute upload of a + // lockstep split session, which is a wrong picture today - before ra has landed + // anything - so the refusal is the client's to raise, not this arm's to assume. + // + // The frontend VAO is taken ONLY when the applier's own vertex-buffer window says a + // client-sourced attribute exists, which is what retires the per-draw + // GetBoundVertexArray row of the ordinary (VBO-backed) DrawArrays - the strict lane's + // `GetBoundVertexArray@DrawArrays` marker - without touching this path's behaviour. + // `static` on purpose (G1): the pull build must not gain an exported name for a + // function that only re-homes two identical inline blocks the two DrawArrays entry + // points used to carry. The same spelling governs the three below, and for a second + // reason on top of the symbol count: a Debug build drops CXX_VISIBILITY_PRESET to + // `default` (CMakeLists.txt:707-719), so an external-linkage helper here would be a + // DYNAMIC export in exactly the configuration a symbol gate cannot see it in. + // + // ALL FLAVORS, NOT JUST THE SPLIT ARM. These three were inside + // `#if MOBILEGL_BUILD_DISAGGREGATED` for one round and that was a misreading of their + // own gate: the body's first statement returns early when `MG_Config::Transport != + // Monolith`, i.e. they exist FOR the monolith arm - and the monolith arm is what the + // pull/verify/push builds run. Their callers sit in entry points that compile in every + // flavor (the indexed family, both indirect executors), so a guard here breaks the + // pull build outright rather than merely omitting a split-arm path. In those flavors + // the guard's own test folds to a compile-time false (Config.h:566) and the body is + // the transport-free upload it always was. + // + // ---- THE MONOLITH ARM'S CLIENT-MEMORY SNAPSHOT, PER DRAW ----------------------------- + // + // A client-memory attribute is uploaded HERE because its bytes have no store on this side + // to bind. SyncClientSideVertexArraysForDrawArrays below does that for the non-indexed + // family, whose fetched elements are exactly the contiguous (first, count) run. Every + // OTHER draw shape fetches by index and by instance, so its elements have to come from + // MGPipeClientFetchPlan - the same answer the wire arm's owned snapshot is built from + // (MG_Impl/Pipe/OwnedDrawInputs.h, which is why the two arms agree by construction). + // + // WHAT WAS MISSING BEFORE THIS: an indexed draw over a client array uploaded nothing at + // all. The ES context kept whatever the last VBO-backed attribute had left for that + // index - normally the array DISABLED, i.e. every vertex reading the generic current + // value - so the draw painted nothing, with no GL error anywhere to say why. + struct ClientSnapshotSources { + const SharedPtr* ElementBuffer = nullptr; + }; + } // namespace VertexArrayImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESContext.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESContext.cpp new file mode 100644 index 00000000..982e0f67 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESContext.cpp @@ -0,0 +1,1659 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESContext.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace PrgramImpl { + // The identity+version key above is only monotonic WITHIN one GLContext: a + // library teardown + re-init frees every FramebufferObject and restarts the + // draw slot's counter at zero, so a recycled FBO address with coinciding + // fresh versions would false-hit. Cleared at the same boundaries as the + // structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache). + void InvalidateBroadcastMemo() { + g_broadcastMemoValid = false; + g_broadcastMemoHandleValid = false; + } + } // namespace PrgramImpl + + static void ReleasePackedWordScratchTexture() { + // The ES context (and the name with it) is gone; deleting here would target a recycled + // name in the successor context. + g_packedWordScratchTextureId = 0; + g_packedWordScratchWidth = 0; + g_packedWordScratchHeight = 0; + } + + // --------------------------------------------------------------------------------- + // P14 S4 (docs/Disaggregated/design/11-state-ownership.md): the native tuple is per context. + // + // THE DISPLAY STAYS ONE OBJECT. eglInitialize is once per process and eglTerminate runs when the + // last native context has gone, so a second context - or a second session - never terminates the + // display the first one is still using. + // + // EVERYTHING ELSE THAT WAS A FILE-LEVEL SINGLETON IS KEYED. A native EGLContext, the config it + // was created with and the native surfaces are keyed by (session, client context token): the + // identity S1 put on the wire (bind_context / SurfaceOp.context) and S2 gave a per-thread owner. + // The key is resolved on the calling thread through a probe the server layer installs + // (SetNativeContextKeyResolver); with none installed the answer is the single key {0, 0}, which + // is byte-for-byte the process singleton this replaces - monolith, a frontend-only process and + // every unit case keep exactly one tuple. + // + // WHY THE SURFACES ARE KEYED BY SESSION AND NOT BY CONTEXT: an EGLSurface is not owned by an + // EGLContext (EGL has no such relationship - only the draw/read BINDING is per context), so a + // session's surfaces are one set and each context records which of them it currently draws to. + // That is also what makes pbuffer -> window -> pbuffer keep its objects: creating the second + // surface does not touch the context, so the twins the first surface's draws built are still + // there when the third one is bound. + + EGLDisplay g_Display = EGL_NO_DISPLAY; + // eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0. + static Uint32 g_displayUsers = 0; + // How many times the display has been terminated. eglTerminate destroys every EGLImage made on + // it, and a re-initialized display hands back the same EGLDisplay handle, so a holder destroys + // its EGLImage itself only while this has not moved since the image was made. + std::atomic g_displayTerminations{0}; + +#if MOBILEGL_BUILD_DISAGGREGATED + namespace SharedImageImpl { + // The display (and every EGLImage and renderbuffer made on it) is gone: forget the present + // targets without calling into the driver, releasing the images they held. + void ForgetPresentTargetsAfterTerminate(); + } // namespace SharedImageImpl +#endif + static void NoteDisplayTerminated() { + g_displayTerminations.fetch_add(1, std::memory_order_acq_rel); +#if MOBILEGL_BUILD_DISAGGREGATED + SharedImageImpl::ForgetPresentTargetsAfterTerminate(); +#endif + } + + std::mutex g_nativeRegistryMutex; + // Bumped on every registry mutation. The per-thread active-tuple cache below is keyed on it, so + // a destroyed tuple can never be handed out from a stale cache entry. + std::atomic g_nativeRegistryEpoch{1}; + // P15: moves whenever the size QueryCurrentSurfaceSize would answer may have moved - after every + // swap (a window surface takes a new size at its next swap) and at every change of a session's + // draw surface (NoteDrawSurfaceMoved at each `session.Draw =`). Surface creation and destruction + // also move g_nativeRegistryEpoch, which the size memo keys on as well. + std::atomic g_surfaceSizeEpoch{1}; + // Never erased while the process lives: a context teardown marks its tuple dead (Context = + // EGL_NO_CONTEXT) instead of freeing it, so a cached pointer is never dangling between the + // epoch bump and the next resolution. + Vector& NativeContexts() { + static Vector* contexts = new Vector(); + return *contexts; + } + Vector& NativeSessions() { + static Vector* sessions = new Vector(); + return *sessions; + } + + std::atomic g_nativeContextKeyResolver{nullptr}; + thread_local NativeThreadState t_native; + + void SetNativeContextKeyResolver(NativeContextKeyResolver resolver) { + g_nativeContextKeyResolver.store(resolver, std::memory_order_release); + } + + // P14 S6: the twin tables' key. Same probe/fallback shape as the native tuple's above and + // for the same reason (the whole single-context world must keep getting one key), and + // deliberately a SEPARATE resolver: this one answers for a table that is consulted on paths + // the native tuple is not, and a case that drives twins without EGL must be able to set it + // alone. See DirectGLES.h for what the two halves mean. + static std::atomic g_twinKeyResolver{nullptr}; + + void SetTwinKeyResolver(TwinKeyResolver resolver) { + g_twinKeyResolver.store(resolver, std::memory_order_release); + } + + Bool CurrentTwinKey(TwinKey* outKey) { + outKey->SessionKey = 0; + outKey->ShareGroupKey = 0; + const TwinKeyResolver resolver = g_twinKeyResolver.load(std::memory_order_acquire); + if (resolver == nullptr) return false; + return resolver(outKey); + } + + Uint64 CurrentNativeSessionKey() { + Uint64 session = 0; + Uint64 token = 0; + ResolveNativeKey(session, token); + return session; + } + + namespace { + // The active tuple for this thread. The fast path is a key compare against a thread-local + // cache, because IsBackendContextCurrentOnThisThread sits under every draw; the slow path + // (a first call, a bind_context switch, or any registry mutation) walks the registry, which + // holds one tuple per live client context. + // (t_native.activeTuple / activeEpoch / activeSession / activeToken) + // The native context THIS THREAD last made current. An EGLContext can be current on only one + // thread, and MakeCurrent is the only writer, so a plain thread-local is exact - and it is + // what makes MakeNativeContextCurrentForBoundToken a no-op when the bind did not actually + // change the native context (the single-context world's every bind). + // (t_native.boundContext) + // And the surface it was made current with: a bind to the same context on a different + // surface (one client context drawing to several windows) is a real switch. + // (t_native.boundDraw) + // ...and that context's lifetime serial, the key of its container objects. + // (t_nativeContextThread.BoundSerial, DirectGLES.h) + std::atomic g_nativeContextSerials{0}; + + std::mutex& PerNativeContextRegistryMutex() { + static auto* mutex = new std::mutex(); + return *mutex; + } + Vector& PerNativeContextRegistry() { + static auto* registry = new Vector(); + return *registry; + } + } // namespace + + namespace { + std::atomic g_perNativeContextEpoch{1}; + } + Uint64 PerNativeContextEpoch() { return g_perNativeContextEpoch.load(std::memory_order_acquire); } + void BumpPerNativeContextEpoch() { g_perNativeContextEpoch.fetch_add(1, std::memory_order_acq_rel); } + + namespace { + std::atomic g_perNativeContextCacheSlots{0}; + } + + PerNativeContextBase::PerNativeContextBase() + : m_cacheSlot(g_perNativeContextCacheSlots.fetch_add(1, std::memory_order_relaxed) % + NativeContextThreadState::kCacheEntries) { + const std::lock_guard lock(PerNativeContextRegistryMutex()); + PerNativeContextRegistry().push_back(this); + } + + PerNativeContextBase::~PerNativeContextBase() { + BumpPerNativeContextEpoch(); + const std::lock_guard lock(PerNativeContextRegistryMutex()); + auto& registry = PerNativeContextRegistry(); + registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end()); + } + + void ForgetNativeContextObjects(Uint64 serial) { + if (serial == 0) return; + const std::lock_guard lock(PerNativeContextRegistryMutex()); + for (PerNativeContextBase* holder : PerNativeContextRegistry()) holder->ForgetContext(serial); + } + + // The tuple the calling thread works on, created on first use. Lazy creation is what keeps the + // pre-S4 shapes working unchanged: a session that never crossed a CreateContext - the whole + // single-context world, and every existing fixture - resolves to its token-0 tuple and gets + // exactly the one native context it always had. + NativeContextTuple& ActiveNativeContext() { + NativeThreadState& tls = t_native; + Uint64 session = 0; + Uint64 token = 0; + if (tls.forcedValid) { + session = tls.forcedSession; + token = tls.forcedToken; + } else { + const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire); + if (resolver != nullptr) (void)resolver(&session, &token); + } + const Uint64 epoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); + if (tls.activeTuple != nullptr && tls.activeEpoch == epoch && tls.activeSession == session && + tls.activeToken == token) { + return *tls.activeTuple; + } + const std::lock_guard lock(g_nativeRegistryMutex); + NativeContextTuple* found = nullptr; + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey == session && tuple->ContextToken == token) { + found = tuple; + break; + } + } + if (found == nullptr) { + found = new NativeContextTuple{}; + found->SessionKey = session; + found->ContextToken = token; + NativeContexts().push_back(found); + (void)AcquireNativeSession(session); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + t_native.activeTuple = found; + t_native.activeEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); + t_native.activeSession = session; + t_native.activeToken = token; + return *found; + } + + static NativeContextTuple* FindNativeContext(Uint64 sessionKey, Uint64 contextToken) { + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey == sessionKey && tuple->ContextToken == contextToken) return tuple; + } + return nullptr; + } + + Uint64 NativeContextCount() { + const std::lock_guard lock(g_nativeRegistryMutex); + Uint64 count = 0; + for (auto* tuple : NativeContexts()) { + if (tuple->Context != EGL_NO_CONTEXT) ++count; + } + return count; + } + + Uint64 NativeContextCountForSession(Uint64 sessionKey) { + const std::lock_guard lock(g_nativeRegistryMutex); + Uint64 count = 0; + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey == sessionKey && tuple->Context != EGL_NO_CONTEXT) ++count; + } + return count; + } + + EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken) { + const std::lock_guard lock(g_nativeRegistryMutex); + const NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken); + return tuple != nullptr ? tuple->Context : EGL_NO_CONTEXT; + } + + void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken) { + t_native.forcedSession = sessionKey; + t_native.forcedToken = contextToken; + t_native.forcedValid = true; + // The cache is keyed on the effective key, so drop it rather than let a stale entry answer. + t_native.activeTuple = nullptr; + t_native.activeEpoch = 0; + } + + // The display's one-time initialization. Returns false only when there is no EGL to talk to. + static Bool EnsureDisplayInitialized() { + if (g_Display != EGL_NO_DISPLAY) return true; + if (!g_EGLFuncs.eglGetDisplay) return false; + g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (g_Display == EGL_NO_DISPLAY) return false; + if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) { + g_Display = EGL_NO_DISPLAY; + return false; + } + if (g_EGLFuncs.eglBindAPI) g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API); + return true; + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // P12 (on-screen server window), D3: true only while InitWindowSurface publishes the default + // framebuffer's shape, so that publish - and no pbuffer's - may carry the surface's extent. + // Split builds only: the pull build's publish is unchanged (G1). + static Bool g_publishWindowExtent = false; +#endif + + Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) { + outWidth = 0; + outHeight = 0; + // P15: ANSWERED FROM A MEMO while nothing that moves the size moved. Two eglQuerySurface + // calls behind the registry lock on every dirty render-state sync (draws and clears whose + // scissor box the app never wrote) cost 0.065 ms/frame on FCL Espryt. + Uint64 memoSession = 0, memoToken = 0; + ResolveNativeKey(memoSession, memoToken); + const Uint64 registryEpoch = g_nativeRegistryEpoch.load(std::memory_order_acquire); + const Uint64 sizeEpoch = g_surfaceSizeEpoch.load(std::memory_order_acquire); + NativeThreadState& memo = t_native; + if (memo.sizeMemoValid && memo.sizeMemoSession == memoSession && memo.sizeMemoToken == memoToken && + memo.sizeMemoRegistryEpoch == registryEpoch && memo.sizeMemoSizeEpoch == sizeEpoch) { + outWidth = memo.sizeMemoWidth; + outHeight = memo.sizeMemoHeight; + return true; + } + // P14 S4: the surface is the SESSION's, not the process's - the tuple that draws to it is + // one of the session's contexts. + const EGLSurface surface = ActiveNativeSession().Draw; + if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) { + return false; + } + + EGLint width = 0; + EGLint height = 0; + if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &width) || + !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &height) || + width <= 0 || height <= 0) { + return false; + } + + outWidth = static_cast(width); + outHeight = static_cast(height); + memo.sizeMemoValid = true; + memo.sizeMemoSession = memoSession; + memo.sizeMemoToken = memoToken; + memo.sizeMemoRegistryEpoch = registryEpoch; + memo.sizeMemoSizeEpoch = sizeEpoch; + memo.sizeMemoWidth = outWidth; + memo.sizeMemoHeight = outHeight; + return true; + } + + // The frontend's default framebuffer is a placeholder FramebufferObject whose attachments + // carry a format and nothing else (MG_Impl/Init.cpp), and it is built before any surface + // exists - so it starts on a guess, GL_DEPTH32F_STENCIL8. Every attachment query about the + // default framebuffer is answered out of that guess, and being wrong is not cosmetic: GL + // blits depth/stencil only between IDENTICAL formats, so a caller that reads + // GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was told about and blits + // gets GL_INVALID_OPERATION and a silently dropped blit - colour bits included, because a + // rejected glBlitFramebuffer transfers nothing at all. DirectVulkan already publishes its + // real format when it creates the swapchain (SwapchainObject::Create); this is the + // DirectGLES half, and here the answer can simply be asked of the ES default framebuffer. + // + // Only the format is published. The placeholder's 512x512 extent is left alone: all three + // attachments share it, and FramebufferObject::CheckCompleteness requires them to agree, so + // resizing depth/stencil without colour would report the default framebuffer incomplete. + static void PublishDefaultFramebufferDepthStencilFormat() { + auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo; + // P8-SE: THE PLACEHOLDER IS ONLY THE MONOLITH ARM'S TARGET. With a transport the answer is + // posted (OnSurfaceChanged, below) and the client writes its own placeholder; a spawn or tcp + // server process has no frontend at all, so pDefaultFramebufferInfo is null there. Gating on + // it returned before the post, the client kept Init.cpp's GL_DEPTH32F_STENCIL8 guess for a + // D24S8 pbuffer, and a blit into a buffer allocated from that guess lost depth and stencil + // (docs/archive/notes/p8/SE.md). Inproc only passed because client and server share the process. + if (!g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return; + if (!defaultFBOInfo && MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == nullptr) return; + + // GL_DEPTH / GL_STENCIL are the default framebuffer's spellings; a user framebuffer + // would need GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT and answers GL_INVALID_ENUM + // for these. Nothing else can be bound this early, but bind explicitly so the answer + // describes the default framebuffer even if this is ever called later. + GLint previousDrawFramebuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFramebuffer); + if (previousDrawFramebuffer != 0) { + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + ClearGLErrors(); + GLint depthBits = 0; + GLint stencilBits = 0; + GLint depthComponentType = GL_UNSIGNED_NORMALIZED; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH, + GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL, + GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH, + GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, + &depthComponentType); + ClearGLErrors(); + + if (previousDrawFramebuffer != 0) { + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDrawFramebuffer)); + } + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + + // A driver that refuses the query leaves both at 0. Fall back to what the EGL config + // was chosen with, which is what the surface actually has. P14 S4: the config belongs to the + // context the surface is bound to, so it is the active tuple's. + const EGLConfig activeConfig = ActiveNativeContext().Config; + if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY && + activeConfig != nullptr) { + EGLint eglDepth = 0; + EGLint eglStencil = 0; + if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_DEPTH_SIZE, &eglDepth)) { + depthBits = static_cast(eglDepth); + } + if (g_EGLFuncs.eglGetConfigAttrib(g_Display, activeConfig, EGL_STENCIL_SIZE, &eglStencil)) { + stencilBits = static_cast(eglStencil); + } + } + + if (depthBits <= 0 && stencilBits <= 0) { + // Nothing usable came back from either witness. Returning here leaves whatever a + // PREVIOUS surface published in place, which would be stale - this function runs + // once per surface activation, not once per process. It is written this way anyway + // because the branch is unreachable for a surface MobileGL chose itself: + // ChooseConfigForSurface asks eglChooseConfig for EGL_DEPTH_SIZE 24 and + // EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so the context's config always + // has both and the EGL fallback above always answers. A caller that supplies its own + // depth-less config would keep the previous surface's description; publishing a + // guess instead would be a different lie, and the placeholder attachment model has + // no way to say "this buffer does not exist" short of detaching it. + MGLOG_D("DirectGLES: default framebuffer reports no depth or stencil; leaving the " + "placeholder attachment formats untouched"); + return; + } + + const Bool floatDepth = depthComponentType == GL_FLOAT; + TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8; + TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8; + if (depthBits > 0 && stencilBits > 0) { + // Packed: both frontend attachments name the same combined format, as DirectVulkan does. + depthFormat = (floatDepth || depthBits > 24) ? TextureInternalFormat::Depth32FStencil8 + : TextureInternalFormat::Depth24Stencil8; + stencilFormat = depthFormat; + } else if (depthBits > 0) { + depthFormat = floatDepth ? TextureInternalFormat::DepthComponent32F + : (depthBits <= 16) ? TextureInternalFormat::DepthComponent16 + : TextureInternalFormat::DepthComponent24; + stencilFormat = depthFormat; + } else { + depthFormat = TextureInternalFormat::StencilIndex8; + stencilFormat = TextureInternalFormat::StencilIndex8; + } + + // Null in a spawn / tcp server (above); only the monolith arm below writes through them. + auto* depthTexture = defaultFBOInfo ? defaultFBOInfo->depthAttachment.get() : nullptr; + auto* stencilTexture = defaultFBOInfo ? defaultFBOInfo->stencilAttachment.get() : nullptr; + if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) { + // P5c ev (CONTRACT-P5C §4.2): with an active transport the attachments are CLIENT + // memory and this thread may not write them - the backend fills MGPSurfaceInfo + // and posts, and the client applies it to its own pDefaultFramebufferInfo on the + // GL thread. FORMAT ONLY, exactly as the monolith arm below: Width/Height stay 0, + // which is the consumer's cue to leave the placeholder extent untouched. The + // callback's presence IS the transport probe - the server session installs it at + // Accept, and under monolith nobody ever does. + MG_Pipe::MGPSurfaceInfo info{}; + info.InternalFormat = static_cast(depthFormat); + info.IsDefault = 1; +#if MOBILEGL_BUILD_DISAGGREGATED + // P12 (on-screen server window), D3: A WINDOW SURFACE OF A REMOTE SESSION ALSO SAYS HOW + // BIG IT IS. Under spawn / tcp (this server process set Transport=Spawn in RunSession) + // the only window surface is the server's own - a headless client's ServerOwned one - + // and that client learns its extent here and from the reply, as Magma's swapchain + // already tells it. An extent switches the client's consumer to the reallocating arm + // (ClientSession.cpp ApplySurfaceChangedToClient), so it is deliberately NOT published + // for a pbuffer, nor under inproc / monolith: those keep the format-only shape above. + if (g_publishWindowExtent && MG_Config::Transport == MG_Config::TransportMode::Spawn) { + // THE DISPLAY'S EXTENT, NEVER THE SURFACE'S: eglQuerySurface answers the buffer last + // dequeued, which after a resize is still the old size - publishing it told the client a + // size the window no longer had. + Uint32 extentWidth = 0; + Uint32 extentHeight = 0; + if (MG_Pipe::MGPipeServerOwnedWindowExtent(&extentWidth, &extentHeight)) { + info.Width = extentWidth; + info.Height = extentHeight; + } + } +#endif + MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info); + } else + { + if (depthTexture) depthTexture->SetInternalFormat(depthFormat); + if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat); + } + MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment " + "formats depth=%d stencil=%d", + depthBits, stencilBits, floatDepth ? 1 : 0, static_cast(depthFormat), + static_cast(stencilFormat)); + } + +#if defined(__linux__) && !defined(__ANDROID__) + static void* OpenX11Lib() { + void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW); + if (!x11Lib) { + x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW); + } + return x11Lib; + } +#endif + + static EGLint QueryDefaultX11VisualId() { +#if defined(__linux__) && !defined(__ANDROID__) + const char* displayName = std::getenv("DISPLAY"); + if (!displayName || !MG_Util::X11::MayDialX11Display(displayName)) { + return 0; + } + + void* x11Lib = OpenX11Lib(); + if (!x11Lib) { + return 0; + } + + using XOpenDisplayFn = void* (*)(const char*); + using XDefaultScreenFn = int (*)(void*); + using XDefaultVisualFn = void* (*)(void*, int); + using XVisualIDFromVisualFn = unsigned long (*)(void*); + using XCloseDisplayFn = int (*)(void*); + + auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); + auto* xDefaultScreen = reinterpret_cast(dlsym(x11Lib, "XDefaultScreen")); + auto* xDefaultVisual = reinterpret_cast(dlsym(x11Lib, "XDefaultVisual")); + auto* xVisualIDFromVisual = reinterpret_cast(dlsym(x11Lib, "XVisualIDFromVisual")); + auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); + if (!xOpenDisplay || !xDefaultScreen || !xDefaultVisual || !xVisualIDFromVisual || !xCloseDisplay) { + dlclose(x11Lib); + return 0; + } + + void* display = xOpenDisplay(displayName); + if (!display) { + dlclose(x11Lib); + return 0; + } + const int screen = xDefaultScreen(display); + void* visual = xDefaultVisual(display, screen); + const auto visualId = visual ? static_cast(xVisualIDFromVisual(visual)) : 0; + xCloseDisplay(display); + dlclose(x11Lib); + return visualId; +#else + return 0; +#endif + } + + static EGLint QueryX11WindowVisualId(NativeWindowType window) { +#if defined(__linux__) && !defined(__ANDROID__) && __has_include() + if (!window) { + return 0; + } + const char* displayName = std::getenv("DISPLAY"); + if (!displayName || !MG_Util::X11::MayDialX11Display(displayName)) { + return 0; + } + + void* x11Lib = OpenX11Lib(); + if (!x11Lib) { + return 0; + } + + using XOpenDisplayFn = Display* (*)(const char*); + using XGetWindowAttributesFn = int (*)(Display*, Window, XWindowAttributes*); + using XVisualIDFromVisualFn = unsigned long (*)(Visual*); + using XCloseDisplayFn = int (*)(Display*); + + auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); + auto* xGetWindowAttributes = + reinterpret_cast(dlsym(x11Lib, "XGetWindowAttributes")); + auto* xVisualIDFromVisual = reinterpret_cast(dlsym(x11Lib, "XVisualIDFromVisual")); + auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); + if (!xOpenDisplay || !xGetWindowAttributes || !xVisualIDFromVisual || !xCloseDisplay) { + dlclose(x11Lib); + return 0; + } + + Display* display = xOpenDisplay(displayName); + if (!display) { + dlclose(x11Lib); + return 0; + } + + XWindowAttributes attrs{}; + EGLint visualId = 0; + if (xGetWindowAttributes(display, static_cast(window), &attrs) && attrs.visual) { + visualId = static_cast(xVisualIDFromVisual(attrs.visual)); + } + xCloseDisplay(display); + dlclose(x11Lib); + return visualId; +#else + (void)window; + return 0; +#endif + } + + static Bool GetConfigAttrib(EGLConfig config, EGLint attr, EGLint& value) { + return g_EGLFuncs.eglGetConfigAttrib && g_EGLFuncs.eglGetConfigAttrib(g_Display, config, attr, &value); + } + + static Bool ConfigSupports(EGLConfig config, EGLint surfaceBit) { + EGLint surfaceType = 0; + EGLint renderableType = 0; + if (!GetConfigAttrib(config, EGL_SURFACE_TYPE, surfaceType)) { + return false; + } + if (!GetConfigAttrib(config, EGL_RENDERABLE_TYPE, renderableType)) { + return false; + } + return (surfaceType & surfaceBit) && (renderableType & EGL_OPENGL_ES3_BIT); + } + + static Bool ChooseConfigForSurface(EGLint surfaceBit, EGLConfig& outConfig, + NativeWindowType window = static_cast(0)) { + const EGLint configAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, + EGL_NONE}; + + EGLint numConfigs = 0; + if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, nullptr, 0, &numConfigs) || numConfigs == 0) { + return false; + } + + Vector configs(static_cast(numConfigs)); + if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, configs.data(), numConfigs, &numConfigs) || + numConfigs == 0) { + return false; + } + configs.resize(static_cast(numConfigs)); + + if (surfaceBit == EGL_WINDOW_BIT) { + // X11 drivers can reserve every alpha-8 config for 32-bit ARGB + // visuals (NVIDIA), so a default-visual (depth 24) window only + // matches an alpha-0 config; keep those as a second candidate tier + // for the visual match or eglCreateWindowSurface hits BAD_CONFIG. + Vector alphaFreeConfigs; + const EGLint alphaFreeAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES3_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, + 24, + EGL_STENCIL_SIZE, 8, EGL_NONE}; + EGLint numAlphaFree = 0; + if (g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, nullptr, 0, &numAlphaFree) && + numAlphaFree > 0) { + alphaFreeConfigs.resize(static_cast(numAlphaFree)); + if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(), + numAlphaFree, &numAlphaFree)) { + numAlphaFree = 0; + } + alphaFreeConfigs.resize(static_cast(numAlphaFree)); + } + + const EGLint windowVisualId = QueryX11WindowVisualId(window); + const EGLint visualIds[] = {windowVisualId, QueryDefaultX11VisualId()}; + for (const auto visualId : visualIds) { + if (visualId == 0) { + continue; + } + for (const auto* candidates : {&configs, &alphaFreeConfigs}) { + for (const auto config : *candidates) { + EGLint nativeVisualId = 0; + if (ConfigSupports(config, surfaceBit) && + GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) && + nativeVisualId == visualId) { + outConfig = config; + return true; + } + } + } + } + } + + for (const auto config : configs) { + if (ConfigSupports(config, surfaceBit)) { + outConfig = config; + return true; + } + } + + outConfig = configs.front(); + return true; + } + + // P14 S4. THE CONTEXT FOR `tuple`, AND THE SURFACE'S CONFIG, WITH NO TEARDOWN IN FRONT. + // + // The S4 deletion is the first statement this function used to begin with: `DestroyEGLContext()` + // ran UNCONDITIONALLY, so creating a second surface (let alone a second context) destroyed the + // native context the first one's objects lived in. A tuple that already has a native context + // keeps it and only (re)chooses the config the new surface is created with; a tuple that has + // none builds one, sharing with the group leader when CreateNativeContextFor named a group. + // + // A CONFIG THAT IS ALREADY SET IS NOT RE-CHOSEN FOR A DIFFERENT SURFACE KIND, and that is the + // deliberate trade: an EGLSurface's config must be compatible with the context's, so re-choosing + // would mean recreating the context - exactly the teardown this slice deletes. A driver that + // refuses the resulting eglCreate*Surface refuses it by name, which is the honest failure. + // P14 S4. THE BRING-UP DIAGNOSIS, in one place because BOTH surface paths owe it (it used to be + // the first statement of InitDisplayAndContext, which both went through). Extracted rather than + // duplicated when that function was replaced by the per-tuple EnsureNativeContext. + // + // Diagnose, and deliberately NOT resolve: resolving raises + // Fatal{PipeLegacyMemosDisabled}, and this function runs inside eglMakeCurrent, which + // the integration harness pre-flights in a FORKED CHILD + // (MG_IntegrationTest/Harness/HeadlessGL.cpp). A child that dies on a signal is reported + // to the parent as "no usable GPU/display/ICD" and every scenario in the lane is + // SKIPPED - so the stop became a green lane that ran nothing, on exactly the two env + // vars the D14/D18 A/B is driven with (ARCHITECTURE.md §13.2). The stop now belongs to the first + // twin lookup, which happens in a scenario body where a crash IS a test failure. + static void DiagnoseEsprytSlotArmAtBringUp() { + DiagnoseEsprytSlotArm(); + } + + // RESET NOTIFICATION, decided once per process by its first native context (EnsureNativeContext + // says why). MOBILEGL_ESPRYT_RESET_NOTIFICATION=0 keeps the driver's default. + + namespace { + // EGL_EXT_create_context_robustness (eglext.h is not part of this file's include set). + constexpr EGLint kEglResetNotificationStrategyExt = 0x3138; + constexpr EGLint kEglLoseContextOnResetExt = 0x31BF; + enum class ResetNotification : Uint8 { Undecided, On, Off }; + std::atomic g_resetNotification{ResetNotification::Undecided}; + + Bool ResetNotificationWanted() { + const ResetNotification decided = g_resetNotification.load(std::memory_order_acquire); + if (decided != ResetNotification::Undecided) return decided == ResetNotification::On; + if (const char* value = std::getenv("MOBILEGL_ESPRYT_RESET_NOTIFICATION"); value != nullptr && value[0] == '0') + return false; + if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) return false; + const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); + if (extensions == nullptr) return false; + constexpr const char kWord[] = "EGL_EXT_create_context_robustness"; + for (const char* at = std::strstr(extensions, kWord); at != nullptr; at = std::strstr(at + 1, kWord)) { + const char after = at[sizeof(kWord) - 1]; + if ((at == extensions || at[-1] == ' ') && (after == '\0' || after == ' ')) return true; + } + return false; + } + + void NoteResetNotificationRefused() { + ResetNotification expected = ResetNotification::Undecided; + if (g_resetNotification.compare_exchange_strong(expected, ResetNotification::Off, std::memory_order_acq_rel)) + MGLOG_W("DirectGLES: the driver refused EGL_LOSE_CONTEXT_ON_RESET; a context that hangs the GPU is " + "not told, and its session cannot be ended for it"); + } + + void NoteResetNotificationDecided(Bool used) { + ResetNotification expected = ResetNotification::Undecided; + const ResetNotification decided = used ? ResetNotification::On : ResetNotification::Off; + if (g_resetNotification.compare_exchange_strong(expected, decided, std::memory_order_acq_rel)) + MGLOG_I("DirectGLES: native contexts %s reset notification (EGL_LOSE_CONTEXT_ON_RESET): a context " + "whose work hangs or faults the GPU %s", + used ? "ask for" : "do not ask for", + used ? "is named by the driver and ends its own session" : "is not told"); + } + } // namespace + + Bool NativeContextsNotifyResets() { + return g_resetNotification.load(std::memory_order_acquire) == ResetNotification::On; + } + + static Bool EnsureNativeContext(NativeContextTuple& tuple, EGLint surfaceBit, NativeWindowType window) { + if (!EnsureDisplayInitialized()) { + MGLOG_E("EnsureNativeContext: the EGL display did not initialize"); + return false; + } + if (tuple.Context != EGL_NO_CONTEXT) return true; + + if (tuple.Config == nullptr && !ChooseConfigForSurface(surfaceBit, tuple.Config, window)) { + MGLOG_E("EnsureNativeContext: no EGL config for surface bit 0x%x (window=%p)", + static_cast(surfaceBit), reinterpret_cast(window)); + return false; + } + + // A share group whose leader has a live native context hands it to eglCreateContext: this is + // the whole of "shared contexts see the same objects" on the server. The group is recorded + // by CreateNativeContextFor before this runs; a tuple nobody named a group for starts one. + EGLContext share = EGL_NO_CONTEXT; + for (auto* other : NativeContexts()) { + if (other != &tuple && other->SessionKey == tuple.SessionKey && + other->Context != EGL_NO_CONTEXT && tuple.ShareGroupToken != 0 && + other->ShareGroupToken == tuple.ShareGroupToken) { + share = other->Context; + break; + } + } + // P14: AND OTHERWISE ANY LIVE NATIVE CONTEXT IN THE PROCESS. The backend keeps buffers, + // textures and syncs of its own in process-wide state (the UBO/unpack/upload rings, the + // buffer pool, scratch textures, frame fences) and every session's apply thread uses + // them; they are only names in another session's context if all the contexts are one + // share group. Client isolation does not come from the driver's namespaces - the twin + // tables are bucketed per {session, share group} and never hand one bucket's names to + // another - so a process-wide group costs nothing in separation. + if (share == EGL_NO_CONTEXT) { + for (auto* other : NativeContexts()) { + if (other != &tuple && other->Context != EGL_NO_CONTEXT) { + share = other->Context; + break; + } + } + } + + // Negotiate the highest ES 3.x context. Version-strict EGL implementations + // (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3 + // request yields a 3.0 context that lacks the 3.1/3.2 texture targets the + // capability probes exercise; mobile drivers ignore the minor and hand out + // their maximum either way. + // + // RESET NOTIFICATION. With the driver's default strategy a context whose work hung the GPU + // is never told: after Adreno's ~2 s timeout reset it silently drops everything that context + // submits (the client's window just stops), and its client never learns it should recreate. + // Asking for EGL_LOSE_CONTEXT_ON_RESET lets the driver name the guilty context + // (glGetGraphicsResetStatus answers GUILTY, once) and LatchIfDeviceLost ends that session + // alone, so the client reads a lost context and can recover. (A ~10 s reset that a failed + // preemption ends was not reported to the server's contexts in measurement - see + // GpuHangWatch.h.) Every context of the process shares one group, and contexts that share + // must agree on the strategy, so it is decided once, by the first context, and kept. + for (const EGLint minorVersion : {2, 1, 0}) { + const Bool notify = ResetNotificationWanted(); + const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, + EGL_CONTEXT_MINOR_VERSION, minorVersion, + notify ? kEglResetNotificationStrategyExt : EGL_NONE, + kEglLoseContextOnResetExt, + EGL_NONE}; + tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs); + if (tuple.Context == EGL_NO_CONTEXT && notify && share == EGL_NO_CONTEXT) { + // The process's first context: if the same version WITHOUT the strategy is accepted, + // it was the strategy the driver refused, and no context of the process asks for it. + const EGLint plainAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, minorVersion, + EGL_NONE}; + tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, plainAttribs); + if (tuple.Context != EGL_NO_CONTEXT) NoteResetNotificationRefused(); + } + if (tuple.Context != EGL_NO_CONTEXT) { + NoteResetNotificationDecided(notify); + tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; + ++g_displayUsers; + return true; + } + } + + const Bool notify = ResetNotificationWanted(); + const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, + notify ? kEglResetNotificationStrategyExt : EGL_NONE, + kEglLoseContextOnResetExt, EGL_NONE}; + tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs); + if (tuple.Context == EGL_NO_CONTEXT) return false; + NoteResetNotificationDecided(notify); + tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; + ++g_displayUsers; + return true; + } + + Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken) { + if (contextToken == 0) return false; + const Uint64 sessionKey = CurrentNativeSessionKey(); + NativeContextTuple* tuple = nullptr; + { + const std::lock_guard lock(g_nativeRegistryMutex); + AcquireNativeSession(sessionKey); + for (auto* candidate : NativeContexts()) { + if (candidate->SessionKey == sessionKey && candidate->ContextToken == contextToken) { + tuple = candidate; + break; + } + } + if (tuple == nullptr) { + tuple = new NativeContextTuple{}; + tuple->SessionKey = sessionKey; + tuple->ContextToken = contextToken; + NativeContexts().push_back(tuple); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + } + if (tuple->Context != EGL_NO_CONTEXT) return true; // idempotent + tuple->ShareGroupToken = shareGroupToken; + return EnsureNativeContext(*tuple, EGL_PBUFFER_BIT, static_cast(0)); + } + + void DestroyNativeContextFor(Uint64 contextToken) { + const Uint64 sessionKey = CurrentNativeSessionKey(); + if (contextToken == 0) return; + const std::lock_guard lock(g_nativeRegistryMutex); + NativeContextTuple* tuple = FindNativeContext(sessionKey, contextToken); + if (tuple == nullptr || tuple->Context == EGL_NO_CONTEXT) return; +#if MOBILEGL_BUILD_DISAGGREGATED + // Its last shared-image reads, fenced while it is still current. + if (t_native.boundContext == tuple->Context) SharedImageImpl::PublishPendingReads(); +#endif + if (g_EGLFuncs.eglDestroyContext && g_Display != EGL_NO_DISPLAY) { + // The context may be current on this very thread; unbinding first is what EGL requires + // (eglDestroyContext of a current context is EGL_BAD_ACCESS). + if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() == tuple->Context && + g_EGLFuncs.eglMakeCurrent) { + g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context); + } + // This thread no longer has it current either way; a context created later at the same + // address must not read as "already bound". + if (t_native.boundContext == tuple->Context) { + t_native.boundContext = EGL_NO_CONTEXT; + t_native.boundDraw = EGL_NO_SURFACE; + t_nativeContextThread.BoundSerial = 0; + } + ForgetNativeContextObjects(tuple->Serial); + tuple->Serial = 0; + tuple->Context = EGL_NO_CONTEXT; + tuple->Config = nullptr; + tuple->Draw = EGL_NO_SURFACE; + tuple->Read = EGL_NO_SURFACE; + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + if (g_displayUsers > 0) --g_displayUsers; + // NOT WHILE A SESSION STILL HOLDS SURFACES. eglTerminate destroys them with the display, and + // the session keeps naming them: the next context made current on one (an application that + // destroys its only context and creates another on the same surface, as eglinfo does with + // its surfaceless stand-in) bound a surface the driver no longer had. The session's own + // teardown terminates the display once its surfaces are gone (DestroyNativeStateOfSession). + Bool surfacesRemain = false; + for (const auto* session : NativeSessions()) surfacesRemain = surfacesRemain || !session->Surfaces.empty(); + if (g_displayUsers == 0 && !surfacesRemain && g_Display != EGL_NO_DISPLAY && g_EGLFuncs.eglTerminate) { + g_EGLFuncs.eglTerminate(g_Display); + NoteDisplayTerminated(); + g_Display = EGL_NO_DISPLAY; + } + } + + // P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. See DirectGLES.h; in one line, `bind_context` + // moves the session's context attribution and this makes the calling thread's native tuple + // follow it, binding the session's current surface to the newly named context's EGLContext. + static Bool SessionOwnsSurface(const NativeSessionState& session, EGLSurface surface) { + if (surface == EGL_NO_SURFACE) return false; + for (const auto& entry : session.Surfaces) { + if (entry.Surface == surface) return true; + } + return false; + } + + Bool MakeNativeContextCurrentForBoundToken() { + NativeContextTuple& tuple = ActiveNativeContext(); + // Nothing to switch to: a session that never sent CreateContext has no native context for + // this token, and building one here would bind a context the client does not know it has. + if (tuple.Context == EGL_NO_CONTEXT) return true; + // A MULTI-WINDOW CLIENT: each context draws to the surface it was last made current with, + // not to whichever surface the session bound most recently - Qt renders each window on its + // own thread with its own context, and binding a context back onto another window's + // surface drew one window's frame into the other. + NativeSessionState& session = ActiveNativeSession(); + // Nothing to make it current on yet: a context bound for the records of its own creation + // (EGLImpl's CreateContext), which no make-current has given a surface (or whose surface + // is gone). Binding it to whichever surface the session holds would hand it another + // context's surface - one the driver may not even have any more. Its first make-current + // binds it natively. + if (tuple.Draw == EGL_NO_SURFACE) return true; + if (tuple.Draw != session.Draw && SessionOwnsSurface(session, tuple.Draw)) { + session.Draw = tuple.Draw; + NoteDrawSurfaceMoved(); + session.Read = SessionOwnsSurface(session, tuple.Read) ? tuple.Read : tuple.Draw; + } + if (t_native.boundContext == tuple.Context && t_native.boundDraw == session.Draw) return true; + return MakeCurrent(); + } + + EGLSurface CurrentSessionDrawSurface() { return ActiveNativeSession().Draw; } + + EGLSurface CurrentContextDrawSurface() { return ActiveNativeContext().Draw; } + + namespace { + // Whether the driver of `display` has EGL_EXT_buffer_age (or EGL_KHR_partial_update, which + // defines the same query). Asked once per display. + // EGL_BUFFER_AGE_EXT (= EGL_BUFFER_AGE_KHR), spelled out: this file sees only the core EGL header. + constexpr EGLint kEglBufferAge = 0x313D; + + // How the driver answers a buffer age, if at all. EGL_EXT_buffer_age promises the whole + // buffer's content for the age it reports. EGL_KHR_partial_update alone promises it only + // OUTSIDE the frame's damage region, which is the whole surface until eglSetDamageRegionKHR + // narrows it - a frame that asks the age and then draws part of the surface without setting + // the region finds the rest undefined (on a tiler: tiles never loaded, noise on screen). + struct NativeBufferAge { + Bool Ext = false; // EGL_EXT_buffer_age + Bool Partial = false; // EGL_KHR_partial_update + EGLBoolean (*SetDamageRegion)(EGLDisplay, EGLSurface, EGLint*, EGLint) = nullptr; + }; + const NativeBufferAge& NativeBufferAgeSupport() { + static EGLDisplay asked = EGL_NO_DISPLAY; + static NativeBufferAge support; + if (g_Display == EGL_NO_DISPLAY || !g_EGLFuncs.eglQueryString) { + static const NativeBufferAge none; + return none; + } + if (asked != g_Display) { + asked = g_Display; + const char* extensions = g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS); + support = {}; + support.Ext = extensions != nullptr && std::strstr(extensions, "EGL_EXT_buffer_age") != nullptr; + support.Partial = extensions != nullptr && std::strstr(extensions, "EGL_KHR_partial_update") != nullptr; + if (support.Partial && g_EGLFuncs.eglGetProcAddress) { + support.SetDamageRegion = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("eglSetDamageRegionKHR")); + } + if (support.SetDamageRegion == nullptr) support.Partial = false; + MGLOG_I("DirectGLES: native buffer age: EGL_EXT_buffer_age %s, EGL_KHR_partial_update %s", + support.Ext ? "yes" : "no", support.Partial ? "yes" : "no"); + } + return support; + } + } // namespace + + // The window surface whose age the driver was asked in the frame being drawn on this (the + // session's apply) thread; the present ends the frame. + thread_local EGLSurface t_ageAskedSurface = EGL_NO_SURFACE; + + // Whether the frame being drawn on this thread has had its flush ahead of the window + // buffer (FlushAheadOfWindowBufferUse); the present ends the frame. + thread_local Bool t_windowBufferFlushIssued = false; + + namespace { + // A WINDOW SURFACE'S BUFFER AGE, on unless MOBILEGL_ESPRYT_WINDOW_AGE=0 (or, on Android, the + // property debug.mobilegl.espryt_window_age=0); off, a window answers 0 and its client + // repaints whole frames. It was opt-in while a window repainted by age sometimes showed a + // client frame two or three presents old: the client's shared image had kept its previous + // frame, its copy clipped by a scissor guard that trusted another session's render-state + // shadow (ScopedScissorDisable) - whole repaints only happened to leave that shadow + // agreeing with the client. Pbuffers (client windows) are not affected. + Bool WindowBufferAgeEnabled() { + static const Bool enabled = [] { + if (const char* env = std::getenv("MOBILEGL_ESPRYT_WINDOW_AGE")) return std::strcmp(env, "0") != 0; +#if defined(__ANDROID__) + char property[PROP_VALUE_MAX] = {}; + if (__system_property_get("debug.mobilegl.espryt_window_age", property) > 0) + return std::strcmp(property, "0") != 0; +#endif + return true; + }(); + return enabled; + } + } // namespace + + // THE OFF-SCREEN PART OF A FRAME DOES NOT WAIT FOR THE WINDOW BUFFER. A window surface takes + // its next buffer when the frame first uses the default framebuffer, and about every other + // frame that is the buffer the display has only just released: its release fence is still + // pending. Whatever the driver has not yet submitted then goes out at the swap, in one batch + // behind that fence - the frame's off-screen passes included - so the GPU starts the whole + // frame only after the display's next commit. A flush just before the first use lets the + // off-screen work start now; only the window pass waits. Once per frame, on window surfaces + // only (a pbuffer has no buffer to wait for), and a flush is not a wait. + void FlushAheadOfWindowBufferUse() { + if (t_windowBufferFlushIssued) return; + t_windowBufferFlushIssued = true; + NativeSessionState& session = ActiveNativeSession(); + const EGLSurface surface = session.Draw; + if (surface == EGL_NO_SURFACE) return; + { + const std::lock_guard lock(g_nativeRegistryMutex); + const auto* entry = FindSessionSurfaceLocked(session, surface); + if (entry == nullptr || entry->Window == static_cast(0)) return; + } + if (g_GLESFuncs.glFlush) g_GLESFuncs.glFlush(); + } + + Int32 CurrentDrawBufferAge(Bool damageRegionFollows) { + NativeSessionState& session = ActiveNativeSession(); + const EGLSurface surface = session.Draw; + if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return 0; + Bool window = false; + Uint64 presents = 0; + { + const std::lock_guard lock(g_nativeRegistryMutex); + const auto* entry = FindSessionSurfaceLocked(session, surface); + if (entry == nullptr) return 0; + window = entry->Window != static_cast(0); + presents = entry->Presents; + } + if (!window) return presents > 0 ? 1 : 0; + if (!WindowBufferAgeEnabled()) return 0; + // The driver's answer. On Android the query dequeues the buffer the next frame draws into - + // what the frame's first draw would do anyway, and the client asks before it draws. + const NativeBufferAge& support = NativeBufferAgeSupport(); + const Bool askable = MG_Util::Damage::NativeAgeAskable(support.Ext, support.Partial, damageRegionFollows); + if (presents == 0 || !askable || !g_EGLFuncs.eglQuerySurface) return 0; + EGLint age = 0; + if (!g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &age) || age < 0) return 0; + t_ageAskedSurface = surface; + return age; + } + + Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region) { + NativeSessionState& session = ActiveNativeSession(); + const EGLSurface surface = session.Draw; + if (surface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return true; + { + const std::lock_guard lock(g_nativeRegistryMutex); + const auto* entry = FindSessionSurfaceLocked(session, surface); + if (entry == nullptr || entry->Window == static_cast(0)) return true; + } + const NativeBufferAge& support = NativeBufferAgeSupport(); + if (!support.Partial || !WindowBufferAgeEnabled()) return true; + // EGL_KHR_partial_update wants the age asked in this frame before the region is set; a client + // that was answered 0 without the driver being asked (its first frame) repaints everything. + if (t_ageAskedSurface != surface) { + EGLint ignored = 0; + (void)g_EGLFuncs.eglQuerySurface(g_Display, surface, kEglBufferAge, &ignored); + t_ageAskedSurface = surface; + } + EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; + const EGLint count = + region.IsFull() ? 0 : static_cast(MG_Util::Damage::PackRects(region, rects, MG_Util::Damage::kMaxRects)); + if (support.SetDamageRegion(g_Display, surface, count > 0 ? rects : nullptr, count) != EGL_TRUE) { + MGLOG_W_ONCE("DirectGLES: eglSetDamageRegionKHR refused a %d-rectangle region (EGL error 0x%x)", count, + g_EGLFuncs.eglGetError ? static_cast(g_EGLFuncs.eglGetError()) : 0u); + } + return true; + } + + Bool SelectSessionSurface(EGLSurface surface) { + NativeSessionState& session = ActiveNativeSession(); + if (!SessionOwnsSurface(session, surface)) return false; + session.Draw = surface; + NoteDrawSurfaceMoved(); + session.Read = surface; + return true; + } + + Bool BindSessionSurface(EGLSurface surface) { + NativeSessionState& session = ActiveNativeSession(); + if (!SessionOwnsSurface(session, surface)) return false; + session.Draw = surface; + NoteDrawSurfaceMoved(); + session.Read = surface; + if (ActiveNativeContext().Context == EGL_NO_CONTEXT) return true; + if (t_native.boundContext == ActiveNativeContext().Context && t_native.boundDraw == surface) return true; + return MakeCurrent(); + } + + EGLSurface CreateSessionPbuffer(EGLint width, EGLint height) { + NativeContextTuple& tuple = ActiveNativeContext(); + if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast(0))) return EGL_NO_SURFACE; + const EGLint attribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; + const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, attribs); + if (surface == EGL_NO_SURFACE) return EGL_NO_SURFACE; + NativeSessionState& session = ActiveNativeSession(); + const std::lock_guard lock(g_nativeRegistryMutex); + session.Surfaces.push_back({static_cast(0), surface}); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + return surface; + } + + void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement) { + if (previous == EGL_NO_SURFACE || replacement == EGL_NO_SURFACE || previous == replacement) return; + NativeSessionState& session = ActiveNativeSession(); + { + const std::lock_guard lock(g_nativeRegistryMutex); + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey != session.SessionKey) continue; + if (tuple->Draw == previous) tuple->Draw = replacement; + if (tuple->Read == previous) tuple->Read = replacement; + } + auto& entries = session.Surfaces; + entries.erase(std::remove_if(entries.begin(), entries.end(), + [&](const NativeSessionState::SurfaceEntry& entry) { + return entry.Surface == previous; + }), + entries.end()); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + if (session.Draw == previous) { + session.Draw = replacement; + NoteDrawSurfaceMoved(); + } + if (session.Read == previous) session.Read = replacement; + // The calling thread drew to the old one: draw to the new one from here (the old is then + // no longer current, and its destruction below is immediate). + if (t_native.boundDraw == previous && ActiveNativeContext().Context != EGL_NO_CONTEXT) (void)MakeCurrent(); + if (g_EGLFuncs.eglDestroySurface && g_Display != EGL_NO_DISPLAY) g_EGLFuncs.eglDestroySurface(g_Display, previous); + } + + void DestroySessionSurface(EGLSurface surface) { + if (surface == EGL_NO_SURFACE) return; + NativeSessionState& session = ActiveNativeSession(); + { + const std::lock_guard lock(g_nativeRegistryMutex); + auto& entries = session.Surfaces; + const auto it = std::find_if(entries.begin(), entries.end(), + [&](const NativeSessionState::SurfaceEntry& entry) { return entry.Surface == surface; }); + if (it == entries.end()) return; + entries.erase(it); + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey != session.SessionKey) continue; + if (tuple->Draw == surface) tuple->Draw = EGL_NO_SURFACE; + if (tuple->Read == surface) tuple->Read = EGL_NO_SURFACE; + } + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + if (session.Draw == surface || session.Read == surface) { + // Fall back onto a surface the session still has, so the next bind has somewhere to go. + const EGLSurface fallback = session.Surfaces.empty() ? EGL_NO_SURFACE : session.Surfaces.front().Surface; + session.Draw = fallback; + NoteDrawSurfaceMoved(); + session.Read = fallback; + if (t_native.boundDraw == surface && fallback != EGL_NO_SURFACE && + ActiveNativeContext().Context != EGL_NO_CONTEXT) { + (void)MakeCurrent(); + } + } + // A surface still current on this thread is destroyed by EGL once it is not (EGL 1.5 + // 3.5.6), so this needs no unbind first. + if (g_EGLFuncs.eglDestroySurface && g_Display != EGL_NO_DISPLAY) g_EGLFuncs.eglDestroySurface(g_Display, surface); + if (t_native.boundDraw == surface && session.Draw == EGL_NO_SURFACE) t_native.boundDraw = EGL_NO_SURFACE; + } + + namespace { + // Last swap interval the app requested through eglSwapInterval; -1 = never + // requested (keep the EGL default of 1). Re-applied when the window surface + // is (re)created since interval is per-surface state. + Int g_requestedSwapInterval = -1; + + void ApplyRequestedSwapInterval() { + if (g_requestedSwapInterval < 0) return; + const EGLSurface surface = ActiveNativeSession().Draw; + if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE) return; + const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval); + MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval, + ok ? "ok" : "failed"); + } + } // namespace + + void SetSwapInterval(Int interval) { + g_requestedSwapInterval = interval; + ApplyRequestedSwapInterval(); + } + +#if MOBILEGL_BUILD_DISAGGREGATED + void ForgetRequestedSwapInterval() { g_requestedSwapInterval = -1; } +#endif + + // A crashed predecessor session may have left its surface bound to this window (the + // server-owned display window is one ANativeWindow for the process's lifetime, and + // several drivers refuse a second eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW). + // Teardown is keyed on the session the surface belongs to, so a session that died without + // one leaks it; find and destroy those before asking the driver. + static void DestroyStaleSurfacesOnWindow(NativeWindowType window) { + const std::lock_guard lock(g_nativeRegistryMutex); + for (auto* session : NativeSessions()) { + auto& entries = session->Surfaces; + for (auto it = entries.begin(); it != entries.end();) { + if (it->Window == window) { + if (g_EGLFuncs.eglDestroySurface) g_EGLFuncs.eglDestroySurface(g_Display, it->Surface); + if (session->Draw == it->Surface) session->Draw = EGL_NO_SURFACE; + if (session->Read == it->Surface) session->Read = EGL_NO_SURFACE; + it = entries.erase(it); + } else { + ++it; + } + } + } + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + + // The default framebuffer's shape after a window surface was bound - with its extent when the + // window is the server's own (see the push arm of the publish). + static void PublishWindowSurfaceShape(NativeWindowType window) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P12 (D3): this publish may carry the window's extent (see the push arm of the publish) - + // review fix: only when the window is the SERVER's own (a headless client's ServerOwned + // surface). A window the client named keeps the format-only publish without the knob. + g_publishWindowExtent = MG_Pipe::MGPipeServerOwnedWindow() != nullptr && + reinterpret_cast(window) == MG_Pipe::MGPipeServerOwnedWindow(); + PublishDefaultFramebufferDepthStencilFormat(); + g_publishWindowExtent = false; +#else + (void)window; + PublishDefaultFramebufferDepthStencilFormat(); +#endif + } + + // THE SERVER WINDOW CHANGED SIZE (the app's layout moved the view: a bar shown or hidden, a + // rotation). The new extent - the display's, set by the server loop before this runs - is published + // as the creation publishes it, at once: the client sizes its viewport and default framebuffer from + // it, and a compositor with nothing to redraw makes no present to hang this on. + Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window) { + if (surface == EGL_NO_SURFACE || !window) return false; + // Bound, because the publish reads the default framebuffer's format. + if (!BindSessionSurface(surface) || t_native.boundDraw != surface) return false; + PublishWindowSurfaceShape(window); + return true; + } + + EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface) { + if (windowSurface == EGL_NO_SURFACE || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; + if (!SessionOwnsSurface(ActiveNativeSession(), windowSurface)) return EGL_NO_SURFACE; + // 1x1: nothing is shown while the window is gone, and nothing is published for it - the + // client keeps the extent it has until the resume publishes the new window's. + const EGLSurface placeholder = CreateSessionPbuffer(1, 1); + if (placeholder == EGL_NO_SURFACE) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E("DirectGLES: no placeholder pbuffer could stand in for the window surface %p (EGL error 0x%x)", + reinterpret_cast(windowSurface), static_cast(error)); + return EGL_NO_SURFACE; + } + ReplaceSessionSurface(windowSurface, placeholder); + // The rebind onto the placeholder failed and the window surface is still current here - EGL + // destroys a current surface only once it is not, and the window goes when this returns. + if (t_native.boundDraw == windowSurface) (void)ReleaseCurrent(); + return placeholder; + } + + EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window) { + if (!window || g_Display == EGL_NO_DISPLAY) return EGL_NO_SURFACE; + NativeContextTuple& tuple = ActiveNativeContext(); + if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return EGL_NO_SURFACE; + DestroyStaleSurfacesOnWindow(window); + const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); + if (surface == EGL_NO_SURFACE) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E("DirectGLES: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x on resume", + reinterpret_cast(window), static_cast(error)); + return EGL_NO_SURFACE; + } + NativeSessionState& session = ActiveNativeSession(); + { + const std::lock_guard lock(g_nativeRegistryMutex); + session.Surfaces.push_back({window, surface}); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + if (placeholder != EGL_NO_SURFACE && SessionOwnsSurface(session, placeholder)) { + ReplaceSessionSurface(placeholder, surface); + } + // Bound, because the publish below reads the default framebuffer. + if (!BindSessionSurface(surface) || t_native.boundDraw != surface) { + MGLOG_E("DirectGLES: the resumed window surface %p could not be made current; its extent is published " + "at its next bind", + reinterpret_cast(surface)); + return surface; + } + ApplyRequestedSwapInterval(); + PublishWindowSurfaceShape(window); + return surface; + } + + Bool InitWindowSurface(NativeWindowType window) { + if (!window) return false; + + DiagnoseEsprytSlotArmAtBringUp(); + + NativeContextTuple& tuple = ActiveNativeContext(); + if (!EnsureNativeContext(tuple, EGL_WINDOW_BIT, window)) return false; + + DestroyStaleSurfacesOnWindow(window); + + const EGLSurface surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, tuple.Config, window, nullptr); + if (surface == EGL_NO_SURFACE) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E("InitWindowSurface: eglCreateWindowSurface(window=%p) failed with EGL error 0x%x", + reinterpret_cast(window), static_cast(error)); + return false; + } + NativeSessionState& session = ActiveNativeSession(); + session.Surfaces.push_back({window, surface}); + session.Draw = surface; + NoteDrawSurfaceMoved(); + session.Read = surface; + tuple.Draw = surface; + tuple.Read = surface; + + if (!MakeCurrent()) return false; + + ApplyRequestedSwapInterval(); + PublishWindowSurfaceShape(window); + + MGLOG_D("EGL window surface created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, + surface, tuple.Context, window); + return true; + } + + Bool InitPbufferSurface(EGLint width, EGLint height) { + if (width <= 0 || height <= 0) return false; + + DiagnoseEsprytSlotArmAtBringUp(); + + NativeContextTuple& tuple = ActiveNativeContext(); + if (!EnsureNativeContext(tuple, EGL_PBUFFER_BIT, static_cast(0))) return false; + + const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; + const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, tuple.Config, surfaceAttribs); + if (surface == EGL_NO_SURFACE) return false; + NativeSessionState& session = ActiveNativeSession(); + session.Surfaces.push_back({static_cast(0), surface}); + session.Draw = surface; + NoteDrawSurfaceMoved(); + session.Read = surface; + tuple.Draw = surface; + tuple.Read = surface; + + if (!MakeCurrent()) return false; + + PublishDefaultFramebufferDepthStencilFormat(); + + MGLOG_D("EGL pbuffer surface created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display, + surface, tuple.Context, width, height); + return true; + } + // The single backend ES context migrates between app threads (FCL/pojav-style + // LWJGL hands the EGL context from JVM thread to JVM thread). Ownership must + // live in ONE global slot: a per-thread flag can never be cleared on the + // LOSING thread when another thread takes (or destroys/releases) the context, + // leaving a stale "current" claim behind. A stale claim makes buffer ops issue + // GL calls that silently no-op (no context is current on that thread) while + // still updating shadow bookkeeping (bind cache, synced serials), permanently + // desynchronizing backend buffer state. + std::atomic g_backendContextOwnerThread{}; + + // Bumped whenever the backend ES context is destroyed; fence and + // timer-query handles created under an older generation belong to a + // dead context and must never be passed back to GL (mirrors + // BufferImpl's context tracking). + Uint g_syncContextGeneration = 1; + + // One-fence-per-frame ring driving the buffer-storage pool's recycle gate. + // A buffer retired during frame N is safe to reuse once frame N's fence has + // signaled. Touched only in Present()/DestroyEGLContext() on the owning + // thread -> no lock. Each fence carries the sync generation so a dead-context + // GLsync is never polled/deleted. Depth 4 >> the 2-3 frames Adreno keeps in + // flight; wrap-before-signal only happens during a stall and just degrades to + // the allocate path. + std::atomic g_currentFrameSerial{0}; + std::atomic g_completedFrameSerial{0}; + FrameFence g_frameFenceRing[kFrameFenceRingDepth]; + + // Defined next to the stamp globals below; every owner-id writer must reset the + // EGL verification stamp BEFORE publishing the new owner. + void InvalidateEglVerifiedStamp(); + + // EVERY SHADOW OF THE CURRENT NATIVE CONTEXT'S BINDINGS, dropped whenever the context the + // calling thread renders into may have changed (MakeCurrent, a turn handoff between two + // sessions' apply threads). Re-issuing a bind is cheap; trusting a shadow that describes + // another context skips one the driver needed (a texture sampled from the wrong unit, a + // draw against another context's VAO). Requires a current context: it re-establishes + // GL_TEXTURE0 so the active-unit shadow is true rather than guessed. + static void InvalidateNativeBindingShadows() { + // Conservatively drop the redundant-glUseProgram guard: re-issuing one bind + // after a MakeCurrent is cheaper than trusting a possibly-reset context. + PrgramImpl::g_lastUsedBackendProgramId = 0; + // The GLContext becoming current may be a fresh one whose slot versions + // restarted at zero; the broadcast memo's key is only monotonic within one. + PrgramImpl::InvalidateBroadcastMemo(); + BufferImpl::InvalidateArrayBufferBindingCache(); + BufferImpl::InvalidateIndexedBufferBindingCache(); + BufferImpl::InvalidatePixelBufferBindingCaches(); + BufferImpl::InvalidateTransformFeedbackBindingShadows(); + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + VertexArrayImpl::InvalidateVAOBindingCache(); + PixelStoreImpl::InvalidatePackStateCache(); + // The render-state shadow belongs in this list for the same reason as the ones above: + // it describes the real ES context, which outlives the MobileGL context that is + // becoming current. See InvalidateSyncedRenderState. + RenderStateImpl::InvalidateSyncedRenderState(); + TextureImpl::g_boundTexturesCache = {}; + SamplerImpl::g_boundSamplersCache = {}; + if (g_GLESFuncs.glActiveTexture) g_GLESFuncs.glActiveTexture(GL_TEXTURE0); + TextureImpl::g_activeTextureUnit = 0; + } + + Bool MakeCurrent() { + // P14 S4: the context comes from the calling thread's tuple (the session's bound token), + // the surface from the session. Both halves used to be file-level singletons. + NativeContextTuple& tuple = ActiveNativeContext(); + NativeSessionState& session = ActiveNativeSession(); + tuple.Draw = session.Draw; + tuple.Read = session.Read; + if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || session.Draw == EGL_NO_SURFACE || + tuple.Context == EGL_NO_CONTEXT) { + MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized"); + return false; + } +#if MOBILEGL_BUILD_DISAGGREGATED + // Shared-image reads can only be fenced from the context that made them. + if (t_native.boundContext != EGL_NO_CONTEXT && t_native.boundContext != tuple.Context) { + SharedImageImpl::PublishPendingReads(); + } +#endif + if (!g_EGLFuncs.eglMakeCurrent(g_Display, session.Draw, session.Draw, tuple.Context)) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error); + return false; + } + g_EGLFuncs.eglSwapBuffersWithDamageEXT = + MG_Util::BackendLoader::ResolveSwapBuffersWithDamage(g_EGLFuncs); + InvalidateEglVerifiedStamp(); + t_native.boundContext = tuple.Context; + t_native.boundDraw = session.Draw; + t_nativeContextThread.BoundSerial = tuple.Serial; + g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); + // The ops table may have been unregistered when a previous ES context was + // destroyed (e.g. a probe context); re-register now that GL is usable. + BufferImpl::RegisterBufferBackendOps(); + InvalidateNativeBindingShadows(); + // eglSwapInterval requires a current context; a request made while none was + // current (and dropped by the driver) is retried here. + ApplyRequestedSwapInterval(); + return true; + } + + // P14: THE BACKEND'S TURN MOVED TO ANOTHER APPLY THREAD (ServerLoop's BackendTurn). Each + // session's apply thread keeps its own native context current, but the backend's owner + // thread and every binding shadow are process-wide and still describe the context of the + // thread that held the turn before. Take ownership for this thread and forget them. + void OnBackendTurnHandoff() { + InvalidateEglVerifiedStamp(); + if (t_native.boundContext == EGL_NO_CONTEXT) { + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); + return; + } + g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); + InvalidateNativeBindingShadows(); + } + + Bool ReleaseCurrent() { + if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) { + InvalidateEglVerifiedStamp(); + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); + return true; + } +#if MOBILEGL_BUILD_DISAGGREGATED + if (t_native.boundContext != EGL_NO_CONTEXT) SharedImageImpl::PublishPendingReads(); +#endif + if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { + const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS; + MGLOG_E_ONCE("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error); + return false; + } + // Clearing the global owner works from ANY thread (a release request can + // legally arrive on a thread other than the current owner); erring towards + // "not current" only defers buffer ops, which is always safe. + InvalidateEglVerifiedStamp(); + t_native.boundContext = EGL_NO_CONTEXT; + t_native.boundDraw = EGL_NO_SURFACE; + t_nativeContextThread.BoundSerial = 0; + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); + return true; + } + + void InvalidateEglVerifiedStamp() { + // ~0 frame serial matches no real frame, so the next owner-thread call + // re-verifies against EGL itself. + g_eglVerifiedFrameSerial.store(~0ull, std::memory_order_relaxed); + g_eglVerifiedContextGeneration.store(0, std::memory_order_relaxed); + } + + // P14 S4: THE FULL TEARDOWN, SCOPED TO THE CALLING THREAD'S SESSION. + // + // This used to destroy the process's one context, its one surface and terminate the display. + // The callers (ReleaseEGLResources, ~BackendObject_DirectGLES) mean "this session's EGL world + // goes away", so under S4 it destroys EVERY native context that session holds, its surfaces, and + // terminates the display only when no context anywhere in the process is left - which is what + // keeps a second live session (S2) serving while this one closes. With no resolver installed + // there is exactly one session (key 0) and this is the old statement, byte for word. + // + // ONE CONTEXT's teardown is DestroyNativeContextFor (the DestroyContext control frame); this is + // the session's. + static void DestroyNativeStateOfSession(Uint64 sessionKey); + + void DestroyEGLContext() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + MG_Pipe::MGPipeServerSetContextLive(false); +#endif + // P14: ANOTHER SESSION'S CONTEXT STILL LIVES. Every native context is in one share group, so + // the buffers, textures and syncs this process's backend holds outlive this session's + // contexts: bumping the process-wide generations here told every live session that its + // GL objects were dead, and they were rebuilt EMPTY (a client exiting or closing its last + // window turned the compositor's textures to noise). Only this session's own state goes; + // the process-wide reset below is for the last context in the process. + const Uint64 endingSession = CurrentNativeSessionKey(); + Bool othersLive = false; + { + const std::lock_guard lock(g_nativeRegistryMutex); + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey != endingSession && tuple->Context != EGL_NO_CONTEXT) { + othersLive = true; + break; + } + } + } + if (othersLive) { + BufferImpl::OnServerSessionEndedWithOthersLive(); + DestroyNativeStateOfSession(endingSession); + return; + } + BufferImpl::OnBackendContextDestroyed(); + XfbImpl::OnBackendContextDestroyed(); + MultiDrawImpl::OnBackendContextDestroyed(); + OnRestartSubstitutionContextDestroyed(); + ScratchFBOImpl::OnBackendContextDestroyed(); + ReleasePackedWordScratchTexture(); + FramebufferImpl::InvalidateFramebufferBindingCache(); + FramebufferImpl::InvalidateFramebufferHandleArmMemos(); + VertexArrayImpl::InvalidateVAOBindingCache(); + PixelStoreImpl::InvalidatePackStateCache(); + PrgramImpl::InvalidateBroadcastMemo(); + // Texture ids belong to the dying context; wrappers destroyed later must + // not glDeleteTextures a recycled name in a successor context. + ++g_backendContextGeneration; + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); + // Outstanding fence handles now refer to a dead context; treat them as + // signaled from here on. + ++g_syncContextGeneration; + // The frame-fence ring's syncs belong to the dead context too; abandon them + // (the context reclaims its syncs) and floor the completed watermark to the + // current serial so buffers retired under the old context read as GPU-idle. + for (FrameFence& slot : g_frameFenceRing) slot = {}; + g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed), + std::memory_order_relaxed); + + DestroyNativeStateOfSession(CurrentNativeSessionKey()); + } + + // This session's native contexts and surfaces, and the display when they were the last. + static void DestroyNativeStateOfSession(Uint64 sessionKey) { + if (g_Display == EGL_NO_DISPLAY) return; +#if MOBILEGL_BUILD_DISAGGREGATED + if (t_native.boundContext != EGL_NO_CONTEXT) SharedImageImpl::PublishPendingReads(); +#endif + { + const std::lock_guard lock(g_nativeRegistryMutex); + if (g_EGLFuncs.eglMakeCurrent) { + g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + t_native.boundContext = EGL_NO_CONTEXT; + t_native.boundDraw = EGL_NO_SURFACE; + t_nativeContextThread.BoundSerial = 0; + for (auto* tuple : NativeContexts()) { + if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue; + ForgetNativeContextObjects(tuple->Serial); + tuple->Serial = 0; + if (g_EGLFuncs.eglDestroyContext) g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context); + tuple->Context = EGL_NO_CONTEXT; + tuple->Config = nullptr; + tuple->Draw = EGL_NO_SURFACE; + tuple->Read = EGL_NO_SURFACE; + if (g_displayUsers > 0) --g_displayUsers; + } + for (auto* session : NativeSessions()) { + if (session->SessionKey != sessionKey) continue; + if (g_EGLFuncs.eglDestroySurface) { + for (const auto& entry : session->Surfaces) { + if (entry.Surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(g_Display, entry.Surface); + } + } + session->Surfaces.clear(); + session->Draw = EGL_NO_SURFACE; + session->Read = EGL_NO_SURFACE; + } + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + } + // THE LAST CONTEXT ANYWHERE IN THE PROCESS IS WHAT TERMINATES THE DISPLAY. A session that + // closes while another is rendering must not take eglTerminate with it. + if (g_displayUsers == 0 && g_EGLFuncs.eglTerminate) { + g_EGLFuncs.eglTerminate(g_Display); + NoteDisplayTerminated(); + g_Display = EGL_NO_DISPLAY; + } + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.cpp new file mode 100644 index 00000000..7676b7cc --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.cpp @@ -0,0 +1,1731 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace BufferImpl { + // Called by the application's own glMemoryBarrier: a barrier that orders storage writes + // already did the work. + static void NoteShaderStorageWritesOrdered() { g_shaderStorageWritesUnordered = false; } + + void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + ProcessDeferredBufferReleases(); +#if MOBILEGL_BUILD_DISAGGREGATED + ProcessDeferredT0Retires(); // P11 B2 +#endif + // THE COMPUTE PATH IS THE ONE THAT NEEDS THE FRONTEND-INDEXED UNIFORM PASS, and it + // is why the uniform class is emitted at all: compute does NOT go through the + // per-program block remap in BindCurrentProgramWithResources, so these are the + // points a compute shader actually reads. + SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform, + GL_UNIFORM_BUFFER); + SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, + GL_SHADER_STORAGE_BUFFER); + if (includeDispatchIndirectBuffer) { + // P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb + // record's handle, never the frontend binding slot or the client allocator. + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + auto* resource = EnsureBufferResourceForHandle(nullptr, handle); + if (resource && resource->id != 0) { + BindBufferId(GL_DISPATCH_INDIRECT_BUFFER, resource->id); + } + } + } + } + } // namespace BufferImpl + + namespace TextureImpl { + // P5e (fb): the eager glBindImageTexture funnel's HANDLE ARM, and it binds nothing. + // + // `bind_shader_image` is an unbarriered row (§2.2) whose backend entry point takes GL + // arguments and a GL NAME - the record's own MGPipeHandle (MGPImageBind::Res) stops at + // the sink - so there is no way to resolve the texture's twin from what arrives here, + // and the frontend binding this used to read is a BARRIER_PULLED row. + // + // Nothing is lost by deferring: SyncImageTextureBindings at the next validate point + // re-binds every unit up to the mark from the applier's own MGPImageView array, and the + // sink bumps the texture shutter serial immediately after this call + // (MGPipeApplierNoteTextureStateMoved), which is one of the four values the sweep's gate + // is keyed on - so the sweep that follows this bind cannot be suppressed. An image unit + // has no reader but a shader, and no shader runs between here and that validate point. + // + // WHAT MUST STILL HAPPEN HERE is the high-water mark: it is the "no draw in this context + // can be reading an image" early-out, so a unit that is given a texture without raising + // it would be skipped by every sweep afterwards. + static void NoteImageUnitBoundWithoutReadingTheFrontend(Uint unit, Bool holdsTexture) { + if (!holdsTexture) return; + if (unit + 1 > g_imageUnitHighWaterMark) { + g_imageUnitHighWaterMark = unit + 1; + } + } + } // namespace TextureImpl + + void PrepareForCompute(Bool includeDispatchIndirectBuffer) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // Single per-dispatch program resolve and texture-key capture, as in + // PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a + // pipeline bound this is its compute stage program, which is a whole program on its + // own - the graphics composite a draw builds carries no compute stage. + // + // P5e (pa), kimi row 92: THE DISPATCH TWIN OF THE SAME RETIREMENT, the same arm and the + // same conjunction - the three callees this value reaches are the ones PrepareForDraw + // reaches, minus the attribute-values sync (a dispatch has no vertex stage) and the + // frontend link/SPIR-V gate below, which already has a record arm of its own. These are + // the 7 strict-lane entries of GetProgramForDispatch@DispatchCompute. + const SharedPtr noFrontendProgram; + const auto& currentProgram = + noFrontendProgram; + const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys(); + + // Read BEFORE this dispatch's own binding walk sets it again (see the flag's comment). + const Bool storageWritesUnordered = BufferImpl::g_shaderStorageWritesUnordered; + BufferImpl::g_shaderStorageWritesUnordered = false; + BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer); + TextureImpl::SyncNeccessaryTextures(textureKeys); + TextureImpl::SyncImageTextureBindings(); + TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); + // Both syncs above may have re-uploaded a texture this dispatch reads or stores + // (Managers.h, NoteInPlaceTextureUpload), and the previous shader work may have left + // storage writes unordered; one barrier covers whichever of the two applies. + { + GLbitfield barriers = TextureImpl::TakeInPlaceTextureUploadBarrierBits(); + if (storageWritesUnordered) barriers |= GL_SHADER_STORAGE_BARRIER_BIT; + if (barriers != 0 && g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(barriers); + } + // P5e (pg), kimi row 92: the DISPATCH half of the same retirement - + // MGPipeApplier().DispatchProgram, which set_dispatch_program carried. A compute-only + // pipeline has no draw program at all, which is why the two applier fields are separate. + const MG_Pipe::MGPipeHandle dispatchCso = + MG_Pipe::MGPipeApplier().DispatchProgram; + PrgramImpl::SyncCurrentProgramByHandle(dispatchCso, false); + + // The same gate as the frontend one below, from the record: a dispatch of a program + // the frontend reports unlinked, or one whose SPIR-V never arrived, binds nothing. + const auto* const record = PrgramImpl::FindShaderCsoRecord(dispatchCso); + if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { + g_GLESFuncs.glUseProgram(0); + PrgramImpl::g_lastUsedBackendProgramId = 0; + return; + } + + // Compute shaders sample textures through the same unit bindings as draws + // (e.g. Flywheel's depth-pyramid downsample reads the depth attachment on + // unit 0), so re-establish unit bindings after the content syncs above. + BindCurrentTextures(textureKeys, currentProgram); + // Compute programs need the same per-program resource sync as draws: + // uniform-block bindings and sampler units only exist through the API + // because layout(binding) is stripped from the transpiled ESSL. + BindCurrentProgramWithResources(currentProgram, textureKeys, + dispatchCso + ); + } + + Bool UpdateTextureBindingAtTarget(GLenum target) { +#ifdef TRACY_ENABLE + ZoneScopedNC(__func__, TRACY_ZONECOLOR_BACKEND); +#endif + auto unit = MG_Pipe::gPipeInputs->GetActiveTextureUnit(); + + auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); + if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) { + MGLOG_E_ONCE(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(textureTarget).c_str()); + return false; + } + + // P5c (hd, CONTRACT-P5C §3.4): under an active transport the destination is the handle + // the copy_framebuffer_to_texture record carried; the unit binding slot and the client + // allocator are never read (T2/T4). + const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(dstHandle); + if (backendTextureSlot == nullptr) { + MGLOG_E_ONCE("%s: the verb's destination texture handle {%u, %u} is refused by " + "the twin slot table (live generation %u)", + __func__, dstHandle.Slot, dstHandle.Gen, + TextureImpl::g_backendTextureObjects.LiveGenAt(dstHandle.Slot)); + return false; + } + auto& backendObj = *backendTextureSlot; + if (!backendObj) { + backendObj = MakeShared(); + } + backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit); + return true; + } + + static Bool IsDepthOnlyFormat(TextureInternalFormat format) { + return MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format); + } + + static Bool IsColorOnlyFormat(TextureInternalFormat format) { + return !MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format); + } + + static Bool IsIntegerColorFormat(TextureInternalFormat format) { + switch (format) { + case TextureInternalFormat::RGB10A2UI: + 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: + return true; + default: + return false; + } + } + + // P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two + // descriptor reads this file makes (Managers.cpp has the same inverse beside the storage + // sync; it is file-local there, and a header for eleven cases would be the wrong trade). + static TextureTarget TextureTargetForPipeResourceTarget(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; + } + } + + // P8-B1: THE LEVELS A generate_mipmap RECORD GENERATES, end-exclusive. GL generates levels + // BASE_LEVEL+1 .. q, q = min(p, MAX_LEVEL) with p the last level the base image's own chain + // reaches, and an immutable texture's storage bounds the chain too (GL 4.6 core 8.14.4). The + // client already computed that window (ComputeMipmapGenerationRange) and defined its levels + // before emitting the verb, so the record's plan is the window and the descriptor is what + // holds it: Base = VerbMipBaseLevel, End = min(VerbMipLevelCount, Desc.Levels, MaxLevel + 1, + // Base + the base extent's chain) - the same clip Magma's GenerateWireMipmap makes. + // + // What stood here instead demanded the full chain from LEVEL 0 and died with + // Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the descriptor held less: a + // MAX_LEVEL below the chain, or glTexStorage2D with fewer levels, both legal GL. + struct RecordMipWindow { + Uint32 Base = 0; + Uint32 End = 0; // end-exclusive; End <= Base + 1 generates nothing + }; + + static RecordMipWindow GenerateMipmapWindowByRecord(const MG_Pipe::MGPipeResourceRecord& record) { + const auto& applier = MG_Pipe::MGPipeApplier(); + const auto& desc = record.Desc; + RecordMipWindow window{applier.VerbMipBaseLevel, applier.VerbMipBaseLevel}; + if (desc.Width == 0 || desc.Levels == 0 || window.Base >= desc.Levels) return window; + Uint32 end = std::min(applier.VerbMipLevelCount, desc.Levels); + if (record.ParamsSerial) end = std::min(end, static_cast(record.Params.MaxLevel) + 1u); + const IntVec3 baseExtent = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, window.Base); + const Int shrinking = MG_Record::StagedTextureShrinkingAxisCount(desc.Target); + Int largest = 1; + for (Int axis = 0; axis < shrinking && axis < 3; ++axis) largest = std::max(largest, baseExtent[axis]); + Uint32 chain = 1; + while (largest > 1) { + largest /= 2; + ++chain; + } + window.End = std::max(window.Base, std::min(end, window.Base + chain)); + return window; + } + + static void AssertNoGLError(const char* operation) { + const GLenum err = g_GLESFuncs.glGetError(); + MOBILEGL_ASSERT(err == GL_NO_ERROR, "%s failed: %s", operation, + MG_Util::ConvertGLEnumToString(err).c_str()); + } + + static ErrorCode ConvertGLESErrorToErrorCode(GLenum err) { + switch (err) { + case GL_INVALID_ENUM: + return ErrorCode::InvalidEnum; + case GL_INVALID_VALUE: + return ErrorCode::InvalidValue; + case GL_INVALID_FRAMEBUFFER_OPERATION: + return ErrorCode::InvalidFramebufferOperation; + case GL_OUT_OF_MEMORY: + return ErrorCode::OutOfMemory; + case GL_INVALID_OPERATION: + default: + return ErrorCode::InvalidOperation; + } + } + + static Bool RecordGLError(const char* operation, GLenum target, TextureInternalFormat format) { + const GLenum err = g_GLESFuncs.glGetError(); + if (err == GL_NO_ERROR) { + return true; + } + + MGLOG_E_ONCE("%s failed: %s. target=%s, format=%s", operation, + MG_Util::ConvertGLEnumToString(err).c_str(), + MG_Util::ConvertGLEnumToString(target).c_str(), + MG_Util::ConvertTextureInternalFormatToString(format).c_str()); + MG_Pipe::gPipeInputs->RecordError( + ConvertGLESErrorToErrorCode(err), + MakeUnique("DirectGLES", operation, + MG_Util::ConvertGLEnumToString(err))); + return false; + } + + // Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the + // scope (GenerateMipmap must respecify texture storage while no incomplete + // user FBO is bound); restores the previous bindings on exit. + class ScopedCompleteFramebufferBinding { + public: + ScopedCompleteFramebufferBinding() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) { + FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, ScratchFBOImpl::EnsureCompleteTinyFramebufferId()); + } + + private: + ScopedFramebufferBinding m_binding; + }; + + class ScopedDetachedTextureFramebufferAttachments { + public: + // P5e (tx2). THE SAME SWEEP, KEYED BY THE TEXTURE'S HANDLE. + // + // The frontend constructor below answers "which framebuffers attach this texture" by + // walking every live FBO twin's frontend FramebufferObject and comparing attachment + // SharedPtrs - kimi audit row 63, an unmemoised walk of client objects. The applier + // already holds the answer: every framebuffer's record carries its eleven MGPSurfaces + // and each one names its texture BY HANDLE (`MGPSurface::Res`), so the comparison is + // {slot, gen} against {slot, gen} and the level / layered / upload-target details the + // detach needs come off the same surface. + // + // This is the same reverse question fb's texture -> FBO-slot index answers for its + // attachment sync; here it is asked over the records directly, because the walk is on a + // rare path (per glGenerateMipmap) and does not need an index to be cheap. + explicit ScopedDetachedTextureFramebufferAttachments(MG_Pipe::MGPipeHandle textureHandle) { + if (MG_Pipe::MGPipeHandleIsNull(textureHandle)) return; + auto* twin = TextureImpl::ResolveTextureTwin(textureHandle); + if (twin == nullptr) return; + const GLuint backendTextureId = twin->GetBackendTextureId(); + const auto& st = MG_Pipe::MGPipeApplier(); + + FramebufferImpl::g_backendFramebufferObjects.ForEachLive( + [&](MG_Pipe::MGPipeHandle fbo, + const SharedPtr& backendFBO) { + if (!backendFBO) return; + const auto* record = st.FramebufferRecordFor(fbo); + if (record == nullptr || record->IsDefault != 0) return; + + const auto detachPoint = [&](const MG_Pipe::MGPSurface& surface, + FramebufferAttachmentType frontendType) { + if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; + if (surface.Res.Slot != textureHandle.Slot || surface.Res.Gen != textureHandle.Gen) { + return; + } + GLenum backendAttachment = GL_NONE; + if (frontendType >= FramebufferAttachmentType::Color0 && + frontendType <= FramebufferAttachmentType::Color31) { + backendAttachment = backendFBO->GetBackendAttachmentType(frontendType); + } else { + backendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType); + } + if (backendAttachment == GL_NONE || backendAttachment == GL_UNKNOWN_MGL) return; + + GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + static_cast(surface.UploadTarget)); + if (textureTarget == GL_UNKNOWN_MGL) { + textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( + static_cast(surface.TextureTarget)); + } + const GLuint backendFBOId = backendFBO->GetBackendFramebufferId(); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId); + if (surface.Layered != 0) { + g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0); + } else { + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, backendAttachment, + textureTarget, 0, 0); + } + ClearGLErrors(); + m_detachedAttachments.push_back({backendFBOId, backendAttachment, textureTarget, + backendTextureId, static_cast(surface.Level), + surface.Layered != 0}); + }; + + for (SizeT i = 0; i < 8; ++i) { + detachPoint(record->Color[i], + static_cast( + static_cast(FramebufferAttachmentType::Color0) + i)); + } + detachPoint(record->Depth, FramebufferAttachmentType::Depth); + detachPoint(record->Stencil, FramebufferAttachmentType::Stencil); + }); + } + + ~ScopedDetachedTextureFramebufferAttachments() { + for (const auto& attachment : m_detachedAttachments) { + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, attachment.framebuffer); + if (attachment.layered) { + g_GLESFuncs.glFramebufferTexture( + GL_DRAW_FRAMEBUFFER, attachment.attachment, attachment.texture, attachment.level); + } else { + g_GLESFuncs.glFramebufferTexture2D( + GL_DRAW_FRAMEBUFFER, attachment.attachment, attachment.textureTarget, + attachment.texture, attachment.level); + } + } + } + + private: + struct DetachedAttachment { + GLuint framebuffer = 0; + GLenum attachment = GL_NONE; + GLenum textureTarget = GL_TEXTURE_2D; + GLuint texture = 0; + GLint level = 0; + Bool layered = false; + }; + + // Declared first so its restore runs after the reattach loop in the dtor. + ScopedFramebufferBinding m_binding{/*saveRead=*/true, /*saveDraw=*/true}; + Vector m_detachedAttachments; + }; + + // Binds the scratch blit READ/DRAW framebuffers with scissor forced off (blits + // are scissored) for one texture-to-texture copy; restores the bindings and the + // scissor state on exit. Attachments on the two scratch FBOs are managed by the + // blit helpers through the ScratchFBOImpl attachment shadow. + class ScopedDepthBlitState { + public: + ScopedDepthBlitState() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) { + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, + ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitReadFramebuffer())); + AssertNoGLError("bind depth blit read framebuffer"); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, + ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitDrawFramebuffer())); + AssertNoGLError("bind depth blit draw framebuffer"); + } + + private: + ScopedScissorDisable m_scissorOff; + ScopedFramebufferBinding m_binding; + }; + + static void BlitDepthTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth, + GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY, + GLsizei dstWidth, GLsizei dstHeight) { + MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Depth blit requires valid backend textures."); + MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Depth blit mip levels must be non-negative."); + MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, + "Depth blit dimensions must be positive."); + + ClearGLErrors(); + ScopedDepthBlitState state; + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); + ScratchFBOImpl::EnsureDepthAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel, + /*withStencil=*/false); + AssertNoGLError("attach depth blit source texture"); + ScratchFBOImpl::EnsureDepthAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel, + /*withStencil=*/false); + AssertNoGLError("attach depth blit destination texture"); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE); + AssertNoGLError("set depth blit read buffer"); + ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE); + AssertNoGLError("set depth blit draw buffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Depth blit read framebuffer is incomplete."); + AssertNoGLError("check depth blit read framebuffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Depth blit draw framebuffer is incomplete."); + AssertNoGLError("check depth blit draw framebuffer"); + + g_GLESFuncs.glBlitFramebuffer(srcX, srcY, srcX + srcWidth, srcY + srcHeight, + dstX, dstY, dstX + dstWidth, dstY + dstHeight, + GL_DEPTH_BUFFER_BIT, GL_NEAREST); + AssertNoGLError("depth texture blit"); + } + + static void BlitColorTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth, + GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY, + GLsizei dstWidth, GLsizei dstHeight, GLenum filter) { + MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Color blit requires valid backend textures."); + MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Color blit mip levels must be non-negative."); + MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, + "Color blit dimensions must be positive."); + MOBILEGL_ASSERT(filter == GL_NEAREST || filter == GL_LINEAR, "Color blit filter must be nearest or linear."); + + ClearGLErrors(); + ScopedDepthBlitState state; + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); + ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel); + AssertNoGLError("attach color blit source texture"); + ScratchFBOImpl::EnsureColorAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel); + AssertNoGLError("attach color blit destination texture"); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); + AssertNoGLError("set color blit read buffer"); + ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0); + AssertNoGLError("set color blit draw buffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Color blit read framebuffer is incomplete."); + AssertNoGLError("check color blit read framebuffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Color blit draw framebuffer is incomplete."); + AssertNoGLError("check color blit draw framebuffer"); + + g_GLESFuncs.glBlitFramebuffer(srcX, srcY, srcX + srcWidth, srcY + srcHeight, + dstX, dstY, dstX + dstWidth, dstY + dstHeight, + GL_COLOR_BUFFER_BIT, filter); + AssertNoGLError("color texture blit"); + } + + // The layer-point siblings of the two blits above, for a texture VIEW's mip chain: those + // texels live in the storage owner's layers [MinLayer, MinLayer + NumLayers) and a 2D + // attach would address layer 0 of every one of them. One layer's extents are the whole + // rectangle, exactly as in the 2D helpers. + static void BlitDepthTextureLayer(GLuint srcTexture, GLint srcLevel, GLint srcLayer, GLsizei srcWidth, + GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstLayer, + GLsizei dstWidth, GLsizei dstHeight) { + MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Depth blit requires valid backend textures."); + MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Depth blit mip levels must be non-negative."); + MOBILEGL_ASSERT(srcLayer >= 0 && dstLayer >= 0, "Depth blit layers must be non-negative."); + MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, + "Depth blit dimensions must be positive."); + + ClearGLErrors(); + ScopedDepthBlitState state; + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); + ScratchFBOImpl::EnsureDepthAttachmentLayer(readFB, GL_READ_FRAMEBUFFER, srcTexture, srcLevel, srcLayer); + AssertNoGLError("attach depth blit source layer"); + ScratchFBOImpl::EnsureDepthAttachmentLayer(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, dstLevel, dstLayer); + AssertNoGLError("attach depth blit destination layer"); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE); + AssertNoGLError("set depth blit read buffer"); + ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE); + AssertNoGLError("set depth blit draw buffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Depth blit read framebuffer is incomplete."); + AssertNoGLError("check depth blit read framebuffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Depth blit draw framebuffer is incomplete."); + AssertNoGLError("check depth blit draw framebuffer"); + + g_GLESFuncs.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight, GL_DEPTH_BUFFER_BIT, + GL_NEAREST); + AssertNoGLError("depth texture layer blit"); + } + + static void BlitColorTextureLayer(GLuint srcTexture, GLint srcLevel, GLint srcLayer, GLsizei srcWidth, + GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstLayer, + GLsizei dstWidth, GLsizei dstHeight, GLenum filter) { + MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Color blit requires valid backend textures."); + MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Color blit mip levels must be non-negative."); + MOBILEGL_ASSERT(srcLayer >= 0 && dstLayer >= 0, "Color blit layers must be non-negative."); + MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0, + "Color blit dimensions must be positive."); + MOBILEGL_ASSERT(filter == GL_NEAREST || filter == GL_LINEAR, "Color blit filter must be nearest or linear."); + + ClearGLErrors(); + ScopedDepthBlitState state; + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); + ScratchFBOImpl::EnsureColorAttachmentLayer(readFB, GL_READ_FRAMEBUFFER, srcTexture, srcLevel, srcLayer); + AssertNoGLError("attach color blit source layer"); + ScratchFBOImpl::EnsureColorAttachmentLayer(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, dstLevel, dstLayer); + AssertNoGLError("attach color blit destination layer"); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); + AssertNoGLError("set color blit read buffer"); + ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0); + AssertNoGLError("set color blit draw buffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Color blit read framebuffer is incomplete."); + AssertNoGLError("check color blit read framebuffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Color blit draw framebuffer is incomplete."); + AssertNoGLError("check color blit draw framebuffer"); + + g_GLESFuncs.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight, GL_COLOR_BUFFER_BIT, + filter); + AssertNoGLError("color texture layer blit"); + } + + // A mip chain generated inside a texture VIEW's window. Only the LAYER axis needs the + // manual path: BASE_LEVEL/MAX_LEVEL are the view's own parameters, so the native + // glGenerateMipmap already clips the LEVEL axis the way the view describes - but it writes + // every layer of the storage the view aliases, and a view's layer window is a subset of + // those. + // + // The levels here are the VIEW's own (level 0 aliases the storage's MinLevel), because that + // is the space a view's descriptor extents and the frontend's own level sizes are measured + // in. The blits are the one place the two systems meet, so the storage level a blit names is + // MinLevel + the view's level. + struct MipmapViewWindow { + GLuint StorageTextureId = 0; + Uint32 MinLevel = 0; + Uint32 MinLayer = 0; + Uint32 NumLayers = 1; + Uint32 LogicalBase = 0; + Uint32 LogicalEnd = 0; // view-relative, end-exclusive + Bool Depth = false; + GLenum Filter = GL_LINEAR; + }; + + template + static void GenerateMipmapThroughViewWindow(const MipmapViewWindow& window, ExtentAtLevel&& extentAtLevel) { + if (window.StorageTextureId == 0 || window.NumLayers == 0) return; + for (Uint32 level = window.LogicalBase + 1; level < window.LogicalEnd; ++level) { + const IntVec3 srcSize = extentAtLevel(level - 1); + const IntVec3 dstSize = extentAtLevel(level); + MOBILEGL_ASSERT(srcSize.x() > 0 && srcSize.y() > 0 && dstSize.x() > 0 && dstSize.y() > 0, + "View mipmap generation needs non-empty source and destination extents."); + const GLint srcLevel = static_cast(window.MinLevel + level - 1); + const GLint dstLevel = static_cast(window.MinLevel + level); + for (Uint32 i = 0; i < window.NumLayers; ++i) { + const GLint layer = static_cast(window.MinLayer + i); + if (window.Depth) { + BlitDepthTextureLayer(window.StorageTextureId, srcLevel, layer, static_cast(srcSize.x()), + static_cast(srcSize.y()), window.StorageTextureId, dstLevel, + layer, static_cast(dstSize.x()), static_cast(dstSize.y())); + } else { + BlitColorTextureLayer(window.StorageTextureId, srcLevel, layer, static_cast(srcSize.x()), + static_cast(srcSize.y()), window.StorageTextureId, dstLevel, + layer, static_cast(dstSize.x()), + static_cast(dstSize.y()), window.Filter); + } + } + } + } + + static void CopyR32FTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei width, + GLsizei height, GLuint dstTexture, GLenum dstTarget, GLint dstLevel, GLint dstX, + GLint dstY) { + MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "R32F copy requires valid backend textures."); + MOBILEGL_ASSERT(dstTarget == GL_TEXTURE_2D, "R32F copy only supports GL_TEXTURE_2D destinations."); + MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "R32F copy mip levels must be non-negative."); + MOBILEGL_ASSERT(width > 0 && height > 0, "R32F copy dimensions must be positive."); + + ClearGLErrors(); + ScopedDepthBlitState state; + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel); + AssertNoGLError("attach R32F copy source texture"); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); + AssertNoGLError("set R32F copy read buffer"); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "R32F copy read framebuffer is incomplete."); + AssertNoGLError("check R32F copy read framebuffer"); + + Vector pixels(static_cast(width) * static_cast(height)); + { + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); + g_GLESFuncs.glReadPixels(srcX, srcY, width, height, GL_RED, GL_FLOAT, pixels.data()); + AssertNoGLError("read R32F copy pixels"); + } + + // Upload side: the rows are tightly packed floats, which the resting driver + // UNPACK state (4/0/0/0, maintained by ScopedDefaultUnpackState) parses + // correctly; the unpack-PBO binding rests at 0 by the same discipline (the + // call below no-ops unless something diverged). + BufferImpl::BindPixelUnpackBufferId(0); + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); + g_GLESFuncs.glBindTexture(dstTarget, dstTexture); + g_GLESFuncs.glTexSubImage2D(dstTarget, dstLevel, dstX, dstY, width, height, GL_RED, GL_FLOAT, pixels.data()); + AssertNoGLError("upload R32F copy pixels"); + // Re-bind what the texture-binding cache says lives on the temp unit so the + // cache stays truthful without a driver query. + auto* cachedBound = + TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(MG_Util::ConvertGLEnumToTextureTarget(dstTarget))]; + g_GLESFuncs.glBindTexture(dstTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); + } + + // WHICH LAYERS A VIEW'S MIP CHAIN MAY WRITE. A generation through a view has two coordinate + // systems in it - the view's own level/layer window (the sampler view CSO) and its storage + // owner's - and the native arm below carries only part of the first one: BASE_LEVEL and + // MAX_LEVEL are the view's own parameters, so glGenerateMipmap clips the LEVEL axis the way + // the view describes, but the layer set it writes is every layer of the storage the view + // aliases. A windowed view's generation therefore rewrites owner layers outside its window, + // which the view window itself says it must not. + // + // True means this record's generation was handled here (or refuted) and the native arm must + // NOT run; false means "not this shape": no view, a view whose window covers the whole + // storage - where the native call already agrees with GL - a multisample view, which has no + // mip chain to generate, or an aspect the layer blits do not carry. + static Bool GenerateMipmapThroughViewWindowByRecord(MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + TextureInternalFormat format) { + if (MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) return false; + if (record.Desc.Samples != 0) return false; + const Bool depth = IsDepthOnlyFormat(format); + if (!depth && !IsColorOnlyFormat(format)) return false; + + // The view record, validated as SyncTextureViewToBackendByRecord validates it: this + // window is the only place the four numbers come from, and a stale or foreign one would + // generate into another texture's levels. + const auto* viewRecord = PipeSamplerViewRecordForHandle(record.ViewCso); + if (viewRecord == nullptr || !viewRecord->Live || viewRecord->Gen != record.ViewCso.Gen) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-record\"} {%u,%u}", mipRes.Slot, mipRes.Gen); + std::abort(); + } + const auto& view = viewRecord->View; + if (view.Texture != mipRes || view.NumLevels == 0 || view.NumLayers == 0) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-window\"} {%u,%u}", mipRes.Slot, mipRes.Gen); + std::abort(); + } + const auto* storageRecord = PipeTextureRecordForHandle(record.Desc.ViewOf); + if (storageRecord == nullptr) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-storage\"} {%u,%u}", mipRes.Slot, mipRes.Gen); + std::abort(); + } + if (view.MinLayer == 0 && view.NumLayers >= std::max(storageRecord->Desc.ArrayLayers, 1u)) + return false; + + const auto& applier = MG_Pipe::MGPipeApplier(); + MipmapViewWindow window; + window.MinLevel = view.MinLevel; + window.MinLayer = view.MinLayer; + window.NumLayers = view.NumLayers; + window.LogicalBase = applier.VerbMipBaseLevel; + // LevelCount is the logical END-EXCLUSIVE level, not a count after BaseLevel, and the + // frontend already clipped it to the object's own range; the view's window is the bound + // only this side can apply. + window.LogicalEnd = std::min({static_cast(applier.VerbMipLevelCount), + static_cast(record.Desc.Levels), + static_cast(view.NumLevels)}); + window.Depth = depth; + window.Filter = IsIntegerColorFormat(format) ? GL_NEAREST : GL_LINEAR; + if (window.LogicalEnd <= window.LogicalBase + 1) return true; + + // The STORAGE's ES name is what the blits write: the view's own name aliases the same + // texels, but only the owner's name carries every level and layer this window reaches. + auto& storage = TextureImpl::SyncTextureToBackendByHandle(record.Desc.ViewOf); + if (!storage || storage->GetBackendTextureId() == 0) { + MGLOG_E_ONCE("MGPipe: generate_mipmap through view {%u, %u} has no storage texture to write; " + "the call is dropped", mipRes.Slot, mipRes.Gen); + return true; + } + window.StorageTextureId = storage->GetBackendTextureId(); + GenerateMipmapThroughViewWindow(window, [&record](Uint32 level) { + return MG_Record::StagedTextureMipExtent(record.Desc.Target, record.Desc.Width, + record.Desc.Height, record.Desc.Depth, level); + }); + return true; + } + + // P5e (tx2): the two CPU-blit mip chains, driven by the RECORD the verb named. What changes + // against the P5c arms below is only the resolution: those took the frontend texture and + // re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as + // barriered), and the handle is now simply passed in - the extent derivation, the per-level + // blits and the format question are §1's descriptor reads exactly as P5c wrote them. + // + // P8-B1: both chains run over the record's WINDOW (GenerateMipmapWindowByRecord), not from + // level 1 to the end of the descriptor: a BASE_LEVEL above 0 is the source and a level past + // MAX_LEVEL is left as it is, which is what the monolith twins below still get wrong. + static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + const RecordMipWindow& window) { + const auto& desc = record.Desc; + const GLuint textureId = backendTexture->GetBackendTextureId(); + for (Uint32 level = window.Base + 1; level < window.End; ++level) { + const IntVec3 srcSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level - 1); + const IntVec3 dstSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level); + BlitDepthTexture2D(textureId, static_cast(level - 1), 0, 0, + static_cast(srcSize.x()), static_cast(srcSize.y()), textureId, + static_cast(level), 0, 0, static_cast(dstSize.x()), + static_cast(dstSize.y())); + } + } + + static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + const RecordMipWindow& window) { + const auto& desc = record.Desc; + const GLenum filter = + IsIntegerColorFormat(static_cast(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR; + const GLuint textureId = backendTexture->GetBackendTextureId(); + for (Uint32 level = window.Base + 1; level < window.End; ++level) { + const IntVec3 srcSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level - 1); + const IntVec3 dstSize = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level); + BlitColorTexture2D(textureId, static_cast(level - 1), 0, 0, static_cast(srcSize.x()), + static_cast(srcSize.y()), textureId, static_cast(level), 0, 0, + static_cast(dstSize.x()), static_cast(dstSize.y()), filter); + } + } + + void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, + GLsizei height, GLint border) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + DebugImpl::ErrorLopper errorLopper; + MGLOG_D("%s: Backend", __func__); + TextureImpl::SyncNeccessaryTextures(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + FramebufferImpl::SyncCurrentFBO(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + RenderStateImpl::SyncRenderState(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + if (!UpdateTextureBindingAtTarget(target)) return; + + // Bind necessary FBO and texture + BindCurrentFBO(FramebufferTarget::Read); + Uint activeTextureUnit = MG_Pipe::gPipeInputs->GetActiveTextureUnit(); + TextureImpl::BackendTextureObject* dstBackendTexture = nullptr; + TextureInternalFormat mgInternalFormat{}; + // P5c (hd, CONTRACT-P5C §3.4): with an active transport the destination resolves from + // the handle the copy_framebuffer_to_texture record carried (MGPCopyFromFramebuffer:: + // Dst, the verb stash) and its format is the applier descriptor's - the client's + // texture-unit binding slot and the client slot allocator are never read (T2/T4). + const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + const auto* dstRecord = PipeTextureRecordForHandle(dstHandle); + // P5e (tx2): SYNCED, not merely looked up. The destination of a copy is not sampled + // by the draw program, so the per-draw unit list - which is the sampler-view window + // now - never brings it across; before P5e the frontend walk over every slot of every + // touched unit happened to sync it as a side effect. The named narrowing (P5e-5) says + // the union that still covers every texture includes "the waited texture ops", and + // this is one of them: it syncs its own endpoint by handle. + TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle); + auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle); + if (!backendTexture || dstRecord == nullptr) { + MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin " + "or no applier record on this side", + dstHandle.Slot, dstHandle.Gen); + return; + } + dstBackendTexture = backendTexture.get(); + mgInternalFormat = static_cast(dstRecord->Desc.InternalFormat); + dstBackendTexture->Bind(target, activeTextureUnit); + + GLenum format = GL_DEPTH_COMPONENT; + GLenum type = GL_UNSIGNED_INT; + TextureImpl::GenerateTextureFormatInfo(mgInternalFormat, &internalformat, &format, &type, + MG_Util::ConvertGLEnumToTextureTarget(target)); + MOBILEGL_ASSERT(format != GL_NONE && type != GL_NONE, + "%s: cannot GenerateTextureFormatInfo(%s): out internalformat=%s, format=%s, type=%s", + MG_Util::ConvertTextureInternalFormatToString(mgInternalFormat).c_str(), + MG_Util::ConvertGLEnumToString(internalformat).c_str(), + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); + + Bool isDepthFormat = + MG_Util::IsDepthFormatInternalFormat(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat)); + Bool isStencilFormat = + MG_Util::IsStencilFormatInternalFormat(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat)); + + if (!isDepthFormat) { + g_GLESFuncs.glCopyTexImage2D(target, level, internalformat, x, y, width, height, border); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } else { + MGLOG_D("%s: Backend depth", __func__); + // nullptr means "uninitialized storage" only while no unpack PBO is + // bound; enforce the resting 0 state instead of assuming it (no-op + // through the binding cache unless something diverged). + BufferImpl::BindPixelUnpackBufferId(0); + g_GLESFuncs.glTexImage2D(target, level, (GLint)internalformat, width, height, border, format, type, + nullptr); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + auto currentTex = (GLint)dstBackendTexture->GetBackendTextureId(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + TempFBOBinder tempFBOBinder(false); + ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit + ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, + static_cast(currentTex), target, level, isStencilFormat); + + if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE"); + return; + } + + g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height, + GL_DEPTH_BUFFER_BIT | (isStencilFormat ? GL_STENCIL_BUFFER_BIT : 0), + GL_NEAREST); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + } + + void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, + GLsizei height) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + DebugImpl::ErrorLopper errorLopper; + + MGLOG_D("%s: Backend", __func__); + TextureImpl::SyncNeccessaryTextures(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + FramebufferImpl::SyncCurrentFBO(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + RenderStateImpl::SyncRenderState(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + + if (!UpdateTextureBindingAtTarget(target)) return; + + // Bind necessary FBO and texture + BindCurrentFBO(FramebufferTarget::Read); + auto activeTextureUnit = MG_Pipe::gPipeInputs->GetActiveTextureUnit(); + TextureImpl::BackendTextureObject* dstBackendTexture = nullptr; + // P5c (hd, CONTRACT-P5C §3.4): see CopyTexImage2D - the record's Dst handle, never the + // client's unit binding slot or the client allocator (T2/T4). + const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + // P5e (tx2): synced by handle - see CopyTexImage2D's note. A copy destination is not + // in the sampler-view window, so nothing else on this path would build its twin. + TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle); + auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle); + if (!backendTexture) { + MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no " + "twin on this side", + dstHandle.Slot, dstHandle.Gen); + return; + } + dstBackendTexture = backendTexture.get(); + dstBackendTexture->Bind(target, activeTextureUnit); + + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + GLenum internalFormat; + g_GLESFuncs.glGetTexLevelParameteriv(target, level, GL_TEXTURE_INTERNAL_FORMAT, (GLint*)&internalFormat); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + auto mgInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalFormat); + + Bool isDepthFormat = MG_Util::IsDepthFormatInternalFormat(mgInternalFormat); + Bool isStencilFormat = MG_Util::IsStencilFormatInternalFormat(mgInternalFormat); + + if (!isDepthFormat) { + g_GLESFuncs.glCopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } else { + MGLOG_D("%s: Backend depth", __func__); + auto currentTex = dstBackendTexture->GetBackendTextureId(); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + TempFBOBinder tempFBOBinder(false); + ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit + ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, currentTex, + target, level, isStencilFormat); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE"); + return; + } + + g_GLESFuncs.glBlitFramebuffer( + x, y, x + width, y + height, xoffset, yoffset, xoffset + width, yoffset + height, + GL_DEPTH_BUFFER_BIT | (isStencilFormat ? GL_STENCIL_BUFFER_BIT : 0), GL_NEAREST); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + } + + // P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL. + // + // ES generates a chain only for a colour-renderable, filterable format, and whether the + // driver's storage for these two is one depends on the driver: Espryt stores them natively + // where the driver renders the three-channel form and widens them to RGBA16F / RGBA32F where + // it does not (TextureImpl::BackendTextureFormatAddsAlpha), and a widened RGBA32F still needs + // OES_texture_float_linear to be filterable. So GenerateMipmapByRecord asks the driver first + // and comes here only when it refuses - or when MGITEST_ESPRYT_FORCE_CPU_MIPMAP says to, which + // is the only way a host whose driver never refuses reaches this arm. + // + // THE SOURCE IS THE SERVER'S LEVEL, NEVER THE CLIENT'S. The base level is read back from the + // driver (ReadTextureLevelTight), because a render target's texels exist only there - the + // monolith arm (GenerateThreeChannelFloatMipmapOnCpu) filters the frontend's level shadow + // instead and reads a stale base after a draw. Only when the driver cannot read the level back + // (storage it cannot attach) does the staged store answer, and a store that does not cover the + // level declines by name rather than inventing texels. + static Bool ForcedServerCpuMipmap() { + // The R-16 control. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs. + static const Bool s_forced = std::getenv("MGITEST_ESPRYT_FORCE_CPU_MIPMAP") != nullptr; + return s_forced; + } + + // The base level as tight RGB floats: the driver's texels first, the staged store second. + // + // P8-SE: THE STORE ANSWERS ONLY FOR A TEXTURE THE DRIVER NEVER WROTE (P8-E's sticky mark). A + // render target's level 0 lives on the driver; the store still holds whatever crossed before + // the draw, and filtering it would hand the chain stale texels. `driverWrittenBefore` is the + // mark as it stood BEFORE this generation noted its own write (GenerateMipmap's split arm), + // because that note is unconditional and would otherwise decline every store read. Anything + // the store cannot stand for is a named decline, `declined` says why. + static Bool ReadThreeChannelFloatLevelOnServer(MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + TextureTarget textureTarget, TextureUploadTarget face, Uint32 level, + const IntVec3& extent, Bool driverWrittenBefore, + Vector& rgb, const char*& declined) { + const auto format = static_cast(record.Desc.InternalFormat); + const SizeT texels = static_cast(extent.x()) * static_cast(extent.y()); + Vector bytes; + if (!WireTextureReadbackRefusedForTesting(extent) && + ReadTextureLevelTight(backendTexture->GetBackendTextureId(), textureTarget, face, format, + static_cast(level), extent, backendTexture->RequiresImageBindableStorage(), + GL_RGB, GL_FLOAT, bytes) && + bytes.size() == texels * 3 * sizeof(Float)) { + rgb.resize(texels * 3); + Memcpy(rgb.data(), bytes.data(), bytes.size()); + return true; + } + auto& store = MG_Record::ServerStagedTexture(); + const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(mipRes); + const auto faceCode = static_cast(face); + const auto levelCode = static_cast(level); + const Bool half = format == TextureInternalFormat::RGB16F; + const SizeT componentBytes = half ? sizeof(Uint16) : sizeof(Float); + // A view's staged bytes live under its storage owner's key, in the owner's levels; a view + // whose level the driver cannot read has no answer here. + if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) { + declined = "a view has no staged level of its own"; + } else if (!store.Holds()) { + declined = "this arm keeps no staged store"; + } else if (driverWrittenBefore) { + declined = "the driver has written the texture since its bytes crossed"; + } else if (!store.IsCovered(key, faceCode, levelCode)) { + declined = "not every byte of the level crossed"; + } else if (store.LevelByteSize(key, faceCode, levelCode) != texels * 3 * componentBytes) { + declined = "the staged level is not the size of the level"; + } + if (declined != nullptr) return false; + const Uint8* staged = store.RequireLevelBytes(key, faceCode, levelCode, "generate-mipmap-server-cpu"); + rgb.resize(texels * 3); + for (SizeT i = 0; i < texels * 3; ++i) { + if (half) { + Uint16 bits = 0; + Memcpy(&bits, staged + i * componentBytes, sizeof(bits)); + rgb[i] = MG_Util::DecodeHalfBitsToFloat(bits); + } else { + Memcpy(&rgb[i], staged + i * componentBytes, sizeof(Float)); + } + } + MGLOG_W_ONCE("MGPipe: generate-mipmap-server-cpu: texture {%u, %u} level %u could not be read back from " + "the driver; filtering the staged store's copy of it instead", + mipRes.Slot, mipRes.Gen, level); + return true; + } + + static void GenerateThreeChannelFloatMipmapOnServer(MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + Bool driverWrittenBefore, const char* why) { + const auto& desc = record.Desc; + const auto format = static_cast(desc.InternalFormat); + const auto textureTarget = TextureTargetForPipeResourceTarget(desc.Target); + const RecordMipWindow window = GenerateMipmapWindowByRecord(record); + if (window.End <= window.Base + 1) return; + Vector faces; + if (textureTarget == TextureTarget::Texture2D) { + faces.push_back(TextureUploadTarget::Texture2D); + } else if (textureTarget == TextureTarget::TextureCubeMap) { + for (Int face = 0; face < 6; ++face) + faces.push_back(static_cast( + static_cast(TextureUploadTarget::CubeMapPositiveX) + face)); + } + // The upload spelling the driver's storage takes: GL_FLOAT is legal for all four float + // internal formats (ES 3.2 table 8.2), and the channel count follows the storage. + GLenum esInternal = GL_NONE, esFormat = GL_NONE, esType = GL_NONE; + TextureImpl::GenerateTextureFormatInfo(format, &esInternal, &esFormat, &esType, textureTarget); + const Bool fourChannel = esInternal == GL_RGBA16F || esInternal == GL_RGBA32F; + if (faces.empty() || (!fourChannel && esInternal != GL_RGB16F && esInternal != GL_RGB32F)) { + MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: target %s stored as %s has " + "no server CPU arm (%s); levels %u..%u are left as they are", + mipRes.Slot, mipRes.Gen, MG_Util::ConvertTextureTargetToString(textureTarget).c_str(), + MG_Util::ConvertGLEnumToString(esInternal).c_str(), why, window.Base + 1, window.End - 1); + return; + } + MGLOG_I_ONCE("MGPipe: generate-mipmap-server-cpu: %s levels filtered on the server's CPU from its own " + "level (%s)", MG_Util::ConvertTextureInternalFormatToString(format).c_str(), why); + + const GLuint textureId = backendTexture->GetBackendTextureId(); + const GLenum bindTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget); + for (const TextureUploadTarget face : faces) { + IntVec3 extent = MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, window.Base); + Vector level; + const char* declined = nullptr; + if (!ReadThreeChannelFloatLevelOnServer(mipRes, record, backendTexture, textureTarget, face, window.Base, + extent, driverWrittenBefore, level, declined)) { + MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: base level %u could not " + "be read back from the driver and the staged store cannot stand for it (%s); levels " + "%u..%u are left as they are - DECLINED (generate-mipmap-store-declined)", + mipRes.Slot, mipRes.Gen, window.Base, declined != nullptr ? declined : "?", + window.Base + 1, window.End - 1); + return; + } + for (Uint32 dst = window.Base + 1; dst < window.End; ++dst) { + const IntVec3 dstExtent = + MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, dst); + // The monolith's box filter: the 2x2 source footprint, clamped where a dimension + // is already 1 (GL 4.6 core 8.14.4 leaves the filter to the implementation). + Vector next(static_cast(dstExtent.x()) * static_cast(dstExtent.y()) * 3); + const auto at = [&](Int x, Int y, Int c) { + return level[(static_cast(y) * static_cast(extent.x()) + static_cast(x)) * 3 + + static_cast(c)]; + }; + for (Int y = 0; y < dstExtent.y(); ++y) { + for (Int x = 0; x < dstExtent.x(); ++x) { + const Int x0 = std::min(x * 2, extent.x() - 1), x1 = std::min(x * 2 + 1, extent.x() - 1); + const Int y0 = std::min(y * 2, extent.y() - 1), y1 = std::min(y * 2 + 1, extent.y() - 1); + for (Int c = 0; c < 3; ++c) + next[(static_cast(y) * static_cast(dstExtent.x()) + static_cast(x)) * + 3 + static_cast(c)] = + 0.25f * (at(x0, y0, c) + at(x1, y0, c) + at(x0, y1, c) + at(x1, y1, c)); + } + } + Vector upload; + if (fourChannel) { + const SizeT count = next.size() / 3; + upload.resize(count * 4); + for (SizeT i = 0; i < count; ++i) { + upload[i * 4] = next[i * 3]; + upload[i * 4 + 1] = next[i * 3 + 1]; + upload[i * 4 + 2] = next[i * 3 + 2]; + upload[i * 4 + 3] = 1.0f; + } + } + // Tight float rows are a multiple of four bytes, which the resting UNPACK state + // (4/0/0/0, and the unpack-PBO binding at 0) parses as they are - CopyR32FTexture2D's + // discipline, including the re-bind that keeps the binding cache truthful. + ClearGLErrors(); + BufferImpl::BindPixelUnpackBufferId(0); + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); + g_GLESFuncs.glBindTexture(bindTarget, textureId); + g_GLESFuncs.glTexSubImage2D(TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(face), + static_cast(dst), 0, 0, static_cast(dstExtent.x()), + static_cast(dstExtent.y()), fourChannel ? GL_RGBA : GL_RGB, + GL_FLOAT, fourChannel ? upload.data() : next.data()); + const GLenum err = g_GLESFuncs.glGetError(); + auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(textureTarget)]; + g_GLESFuncs.glBindTexture(bindTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); + if (err != GL_NO_ERROR) { + MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu: uploading level %u of texture {%u, %u} " + "failed: %s", dst, mipRes.Slot, mipRes.Gen, + MG_Util::ConvertGLEnumToString(err).c_str()); + return; + } + level = Move(next); + extent = dstExtent; + } + } + } + + // P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from + // VerbMipRes. Same four arms as the frontend body, with every shape question answered from + // the descriptor. + // + // RULING 14's TRAILING FLIP. tx2 kept this verb's PipeCalls.def row at kWaitApplied because + // one frontend contact was left on the path: the RGB16F/RGB32F CPU filter reached into the + // client's level shadows. P8-B2 retired it (GenerateThreeChannelFloatMipmapOnServer reads the + // server's own level), so no arm here reads client memory any more; the WaitClass flip itself + // is a protocol change and is left to the integrator. + // + // `driverWrittenBefore`: P8-E's driver-written mark on the texture's staged store as it stood + // before this call noted its own write - the CPU arm's store route reads it (P8-SE). + static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + Bool driverWrittenBefore) { + const auto format = static_cast(record.Desc.InternalFormat); + const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target); + const Bool threeChannelFloat = + format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F; + + // A view's layer window is the one bound the native arm cannot express, so it is asked + // for before the format arms - those two are 2D-only emulations of the same generation + // and a view reaches them too. + if (GenerateMipmapThroughViewWindowByRecord(mipRes, record, format)) return; + if (IsDepthOnlyFormat(format)) { + GenerateDepthTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record)); + return; + } + if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) { + GenerateColorTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record)); + return; + } + if (threeChannelFloat && ForcedServerCpuMipmap()) { + GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore, + "forced by MGITEST_ESPRYT_FORCE_CPU_MIPMAP"); + return; + } + + const GLenum backendTarget = + TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); + GLenum refused = GL_NO_ERROR; + { + // TempTextureUnit, not the active unit: the scratch bind is the backend's own and + // BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit + // was never anything but the frontend's way of naming the texture, and the record + // names it now. + backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit); + // ANGLE and some other drivers may validate the currently bound FBO while generating mipmaps, so the + // source texture is detached from every framebuffer that attaches it - by handle, + // over the applier's own framebuffer records. + ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes); + ScopedCompleteFramebufferBinding completeFramebuffer; + ClearGLErrors(); + g_GLESFuncs.glGenerateMipmap(backendTarget); + if (!threeChannelFloat) { + RecordGLError("glGenerateMipmap", backendTarget, format); + return; + } + // The driver's refusal of a three-channel float chain is not the application's + // error: the CPU arm below answers it. + refused = g_GLESFuncs.glGetError(); + } + if (refused == GL_NO_ERROR) return; + GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore, + "the driver refused glGenerateMipmap"); + } + + void PatchParameteri(GLenum pname, GLint value) { + if (g_GLESFuncs.glPatchParameteri == nullptr) return; + g_GLESFuncs.glPatchParameteri(pname, value); + } + + void GenerateMipmap(GLenum target) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + // P5e (tx2), CONTRACT-P5E §5.2 / ruling 14 (ID-92's neighbour): GENERATE_MIPMAP RESOLVES + // ITS TEXTURE FROM VerbMipRes, NEVER FROM THE ACTIVE UNIT. + // + // The verb's own record names the texture (MGPMipPlan::Res, stashed as VerbMipRes) and + // EnsureGenerateMipmapStorageAllocated's disaggregated arm has read it since P5c. What + // stood here instead was GetActiveTextureUnit + GetTextureUnitObject + the target's + // binding slot - three BARRIER_PULLED reads to re-derive a handle the record carried - + // and they are the ONLY reason this row still waits: ClientSession.cpp parks the client + // so that the unit it reads is the unit the call was made on. With the unit read gone the + // wait has nothing left to protect, which is what lets PipeCalls.def's WaitClass column + // for GenerateMipmap move to kWaitNone. + const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes; + // P8-SE: whether the staged store still stands for the texture is read BEFORE the note + // below sets the mark, or the CPU arm's store route could never answer at all. + const Bool driverWrittenBefore = MG_Record::ServerStagedTexture().IsDriverWritten( + MG_Record::StagedTextureStore::KeyForHandle(mipRes)); + // Every arm GenerateMipmapByRecord takes writes the levels above the base on the + // driver only (P8-B2's CPU arm uploads its levels there too), so the note is + // unconditional. + TextureImpl::NoteDriverSideTextureWriteByHandle(mipRes); + auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes); + const auto* record = PipeTextureRecordForHandle(mipRes); + if (!backendTexture || record == nullptr) { + MGLOG_E_ONCE("MGPipe: generate_mipmap names texture {%u, %u}, which has no applier " + "record or no driver texture; the call is dropped", + mipRes.Slot, mipRes.Gen); + return; + } + GenerateMipmapByRecord(target, mipRes, *record, backendTexture, driverWrittenBefore); + return; + } + + const GLubyte* GetString(GLenum name) { + return g_GLESFuncs.glGetString(name); + } + + void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + PrepareForCompute(false); + g_GLESFuncs.glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ); + } + + void DispatchComputeIndirect(GLintptr indirect) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + PrepareForCompute(true); + g_GLESFuncs.glDispatchComputeIndirect(indirect); + } + + // An atomic counter is a shader storage block by the time it reaches the ES driver (glslang + // lowers every atomic_uint onto one), so an application that asks only for the counter + // barrier is asking about memory the driver knows as storage-buffer memory. Ordering one + // does not oblige a driver to order the other, so the counter bit implies the storage bit + // here - which is what the lowering costs and the only place it can be paid. + static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) { + if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) { + barriers |= GL_SHADER_STORAGE_BARRIER_BIT; + } + return barriers; + } + + void MemoryBarrier(GLbitfield barriers) { + barriers = LowerAtomicCounterBarrierBits(barriers); + if ((barriers & GL_SHADER_STORAGE_BARRIER_BIT) != 0) BufferImpl::NoteShaderStorageWritesOrdered(); + g_GLESFuncs.glMemoryBarrier(barriers); + if (g_GLESCapabilities.IsAngleRenderer) { + g_GLESFuncs.glFlush(); + } + } + + void MemoryBarrierByRegion(GLbitfield barriers) { + g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers)); + } + + // One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it. + // + // The frontend hands this backend the target the APPLICATION named, and three of the + // targets core GL has do not exist in ES at all. They are not missing here either - the + // texture managers already store a 1D texture as a height-1 2D one, a 1D array as a + // height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) - + // but glCopyImageSubData was the one path that never asked for that translation and passed + // 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen, + // and with the error only asserted on (asserts are compiled out of an INFO build) the + // destination silently keeps whatever it held. + // + // The 1D-array case is not just a rename: GL addresses its layers with y/height while the + // ES 2D array that backs it addresses them with z/depth, so the two axes swap with the + // target. + // + // GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and + // GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while + // ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand + // the driver GL_UNKNOWN_MGL. + struct GLESCopyImageEndpoint { + GLenum target = GL_TEXTURE_2D; + // Exactly one of the two is set. The backend object is kept rather than its id, because + // the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture), + // so it is read at the point of use. + SharedPtr texture; + SharedPtr renderbuffer; + GLint x = 0; + GLint y = 0; + GLint z = 0; + + Bool IsRenderbuffer() const { return renderbuffer != nullptr; } + GLuint Name() const { + if (renderbuffer) return renderbuffer->GetBackendRenderbufferId(); + return texture ? texture->GetBackendTextureId() : 0u; + } + }; + + static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y, + GLint z, GLESCopyImageEndpoint& out) { + if (endpoint.IsRenderbuffer()) { + if (!PipeRenderbufferRecordForHandle(endpoint.RenderbufferHandle)) return false; + auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreateByHandle(endpoint.RenderbufferHandle); + if (!slot) return false; + if (!*slot) *slot = MakeShared(); + out.renderbuffer = *slot; + out.renderbuffer->SyncToBackendByHandle(endpoint.RenderbufferHandle); + if (!out.renderbuffer) return false; + out.target = GL_RENDERBUFFER; + out.x = x; + out.y = y; + out.z = z; + return true; + } + // BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a + // slot inside the backend texture registry, and the second call mutates that very map: + // GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by + // robin-hood displacement well under the load factor), and Find drops any + // entry whose state object has expired - which, with the map open-addressed and erasing + // by shifting the probe cluster backwards, relocates entries other than the erased one. + // Either way a reference taken by the first call is stale by the time the second returns, + // and it is read four more times below. Copying the SharedPtr costs two refcount bumps on + // a path that is already doing a texture copy. + // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never + // reaches here - but the assertion that says so is compiled out of a release build, and + // SyncTextureObjectToBackend would register a null state object. + out.texture = TextureImpl::SyncTextureToBackendByHandle(endpoint.TextureHandle); + if (!out.texture) return false; + const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); + out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); + // No axis remap for GL_TEXTURE_1D_ARRAY. The frontend STORES a 1D array with its layers + // on y (GetBackendUploadSize moves them across to the ES 2D array's z), but this entry + // point does not ADDRESS it that way: GL 4.6 core 18.3.2 treats every array texture as a + // stack of slices on z and gives a 1D array a height of 1 - exactly the shape the ES 2D + // array has - so GL's (x, 0, layer) and the ES image's (x, 0, layer) already agree. + // Remapping y into z here fetched the wrong slice for every call that spelled the layer + // the way GL defines it. + out.x = x; + out.y = y; + out.z = z; + return true; + } + + static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) { + const auto* record = endpoint.IsRenderbuffer() + ? PipeRenderbufferRecordForHandle(endpoint.RenderbufferHandle) + : PipeTextureRecordForHandle(endpoint.TextureHandle); + return record ? static_cast(record->Desc.InternalFormat) + : TextureInternalFormat::Unknown; + if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat(); + return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown; + } + + // Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one + // upload target (so not a cube map, whose six chains the z axis selects between) and layers on + // the z axis (GL_TEXTURE_1D_ARRAY carries them on y). + static Bool CanMirrorCopyImageShadow(const SharedPtr& texture) { + if (!texture) return false; + if (texture->GetTarget() == TextureTarget::Texture1DArray) return false; + return texture->GetUploadTargets().size() == 1; + } + + // glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU + // shadow has to stay authoritative - a packed format with redundant encodings, where a GPU + // readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) - + // the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already + // put these texels on the GPU, and flagging the level would only schedule a redundant upload + // back over them. + // + // Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot + // address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level + // whose shadow is missing or not a plain texel grid, a region outside either level, or a + // self-copy within one level, where the row copies could overlap. + static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX, + GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, + GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, + GLsizei width, GLsizei height, GLsizei depth) { + if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return; + if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return; + if (width <= 0 || height <= 0 || depth <= 0) return; + if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) { + return; + } + auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get()); + auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get()); + if (!srcMipmap || !dstMipmap) return; + // UNDER A REAL TRANSPORT THE SERVER HAS NO FRONTEND SHADOW TO MIRROR INTO (P5b i1, ruling + // §6.7), and P8-E settled where the move lives instead - not on the client (ROADMAP P8's + // original row) but in the server's staged store: CopyImageSubData's handle arm makes the + // same texel-block move there (TextureImpl::FollowCopyImageInStagedStore) for EVERY + // format, not only the redundant-encoding ones this mirror serves, and marks the store + // driver-written where it cannot. The store is what answers a split glGetTexImage the + // driver refuses to read back (DirectGLESReadback.cpp's store route); every other split + // read of the destination reads the GPU level itself (docs/archive/notes/p8/E.md). + // + // BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1), + // and the arm is the TRANSPORT and not the build - build-split runs its unit and + // integration-gpu lanes under MOBILEGL_TRANSPORT=monolith, where this mirror is on an + // ordinary correct path and must still run. + return; + } + + void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, + GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, + const CopyImageEndpoint& dstEndpoint, + GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, + GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { + GLESCopyImageEndpoint src{}; + GLESCopyImageEndpoint dst{}; + // The DirectVulkan half of this entry point died exactly here, on a texture whose sync + // produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was + // supposed to catch it expands to nothing. The four Name() calls below are the same + // dereference. The frontend validator is what keeps this unreachable and what reports + // the error the application is owed; declining is only how a future gap up there stops + // being a crash. See the level guard in VulkanRenderer::CopyImageSubData. + // The handle arm's endpoint names its texture by handle, and the server has no frontend + // object to note. The staged store's half of the note waits for the copy (P8-E, below). + const Bool handleArmDestination = + !dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle); + if (handleArmDestination) { + TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle, /*storeFollowsTheWrite=*/true); + } + if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) || + !MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) { + MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__); + return; + } + // P8-E (docs/archive/notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced, + // so every byte the store holds for them is on the driver, and the copy below is a raw + // texel-block move - the store makes the same move, which keeps the destination level + // answerable from here when the driver will not read it back (ReadTextureImageWire's store + // route) and keeps a whole-level replay of it (a re-mint's refused readback, a full-level + // re-upload) from putting the pre-copy texels back. Where the store cannot follow (a + // renderbuffer, a cube or 1D-array endpoint, a source the driver has written, a level whose + // bytes did not all cross) it is marked driver-written instead, so it declines rather than + // answering with stale bytes. Nothing moves to the client (ROADMAP P8's original row). + if (handleArmDestination) { + const Bool followed = + !srcEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(srcEndpoint.TextureHandle) && + TextureImpl::FollowCopyImageInStagedStore( + srcEndpoint.TextureHandle, static_cast(srcLevel), IntVec3{srcX, srcY, srcZ}, + dstEndpoint.TextureHandle, static_cast(dstLevel), IntVec3{dstX, dstY, dstZ}, + IntVec3{srcWidth, srcHeight, srcDepth}); + if (!followed) TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle); + } + + // Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto + // wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint. + const GLsizei copyHeight = srcHeight; + const GLsizei copyDepth = srcDepth; + + const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint); + const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint); + // Both emulation fallbacks below are written against TEXTURE ids and texture targets, so + // an endpoint that is a renderbuffer takes the native ES copy - which accepts + // GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver + // turns it down. + const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer(); + + const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat); + const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat); + const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat); + const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat); + if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) { + MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil, + "DirectGLES CopyImageSubData only supports depth-only image copies."); + MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, + "DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D."); + MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, + "DirectGLES depth CopyImageSubData only supports single-layer copies."); + BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, + dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); + return; + } + + if (!anyRenderbuffer && + (srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) { + // The single glGetError below decides the fallback dispatch, and + // ErrorLopper::Clear is compiled out at the default log level - drain + // with the always-live helper so a stale flag cannot misroute a + // succeeded native copy into the 2D-only fallback. + ClearGLErrors(); + g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, + dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z, + srcWidth, copyHeight, copyDepth); + const GLenum copyImageError = g_GLESFuncs.glGetError(); + if (copyImageError == GL_NO_ERROR) { + return; + } + MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat), + "DirectGLES CopyImageSubData only supports color-only or depth-only copies."); + MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, + "DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D."); + MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, + "DirectGLES color CopyImageSubData only supports single-layer copies."); + CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, + dst.Name(), dst.target, dstLevel, dst.x, dst.y); + return; + } + + ClearGLErrors(); + g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, + dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z, + srcWidth, copyHeight, copyDepth); + // Every error condition glCopyImageSubData has was already ruled out by the frontend + // validator, so a driver error here is an internal invariant violation, not something + // the application can provoke. Say so where an INFO build can still see it, then trap + // in the builds that trap - the previous bare assert left a release build with a + // destination that silently kept its old contents. + const GLenum copyImageError = g_GLESFuncs.glGetError(); + if (copyImageError != GL_NO_ERROR) { + MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)", + MG_Util::ConvertGLEnumToString(copyImageError).c_str(), + MG_Util::ConvertGLEnumToString(src.target).c_str(), + MG_Util::ConvertGLEnumToString(srcTarget).c_str(), + MG_Util::ConvertGLEnumToString(dst.target).c_str(), + MG_Util::ConvertGLEnumToString(dstTarget).c_str()); + MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); + // P8-E: the store already made the move the driver just refused. + if (handleArmDestination) { + TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle); + } + return; + } + // The copy landed on the GPU. For a destination whose readback cannot be bit-exact the + // CPU shadow is what glGetTexImage answers from, so it has to follow the same move - + // otherwise it hands back whatever the level held before this copy. + if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) { + MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel, + dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); + } + } + + void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, + GLenum format) { + (void)level; + (void)layered; + (void)layer; + (void)access; + (void)format; + // P5e (fb): see NoteImageUnitBoundWithoutReadingTheFrontend. Under a transport this + // entry point records the high-water mark and lets the next validate point's sweep do + // the bind from the record; reading the frontend image binding here is the + // GetImageTextureBinding row that rule F forbids, and `bind_shader_image` is not a + // barriered row. + TextureImpl::NoteImageUnitBoundWithoutReadingTheFrontend(unit, texture != 0); + return; + } + + // Only the pnames MG_Impl/GLImpl/Getter/GL_Getter.cpp has no case for reach here. Every + // indexed pname naming FRONTEND state - the indexed buffer bindings, the per-unit + // texture/sampler bindings, the image-unit bindings, the viewport rectangles, the indexed + // capabilities - is answered there and returns before the table is consulted, so the arms + // this function used to carry for GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were + // unreachable duplicates of the frontend's, and they did not even agree with it (the + // frontend reports the range glBindBufferRange was ASKED for, verbatim and unclamped; these + // clamped it to the buffer's current storage). In practice what arrives is + // GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE, which the driver owns. + void GetIntegeri_v(GLenum target, GLuint index, GLint* data) { + if (!data) return; + if (g_GLESFuncs.glGetIntegeri_v) { + g_GLESFuncs.glGetIntegeri_v(target, index, data); + } else { + *data = 0; + } + } + + // NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry + // point must never be the thing that BUILDS a backend program. + // + // The frontend has already recorded the rebinding on the program object + // (SetShaderStorageBlockBinding) - that record is what GL_BUFFER_BINDING reports and what + // BackendProgramObjectImpl::SyncToBackend replays onto every driver program it builds. So + // the only work left here is an optimisation: push the change straight onto a driver + // program that is ALREADY built and already current with this link, so the next draw does + // not have to be preceded by a rebuild. + // + // Calling GetBackendProgramId() instead would sync-on-demand from a non-draw entry point, + // i.e. transpile and compile the whole program while the draw-path globals that the ESSL + // is generated against (PrgramImpl::g_fragColorBroadcastCount, the snorm/unorm clamp + // masks - established by SyncCurrentProgram) still hold another program's values. That + // bakes a program against the wrong state, and under CPU load it was also observed to + // fail the driver compile outright. Deferring is spec-fine: a binding only has to take + // effect by the block's next use. + void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) { + if (!storageBlockName) return; + const auto handle = MG_Pipe::MGPipeApplier().VerbStorageBlockProgram; + auto* slot = PrgramImpl::g_backendProgramObjects.FindByHandle(handle); + if (slot && *slot && (*slot)->GetBackendProgramId()) { + PrgramImpl::ApplyShaderStorageBlockBinding((*slot)->GetBackendProgramId(), + storageBlockName, storageBlockBinding); + } + return; + } + + void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + TextureImpl::SyncNeccessaryTextures(); + FramebufferImpl::SyncCurrentFBO(); + RenderStateImpl::SyncRenderState(); + + BindCurrentFBO(FramebufferTarget::Draw); + + g_GLESFuncs.glClearBufferfi(buffer, drawbuffer, depth, stencil); + } + + namespace { + // A colour attachment the backend widened from three channels to four has to end up + // holding alpha 1.0 - the value GL reports for a channel the application's format does + // not have - so an explicit per-buffer clear of it writes 1.0 rather than whatever the + // application passed (SubstituteWidenedClearAlpha, in Managers.h). Draws can never move + // it again: their alpha write mask is forced off, see SyncRenderState. That pairing is + // what makes GL_DST_ALPHA blending, glReadPixels and glBlitFramebuffer all see the right + // value without any of them being intercepted. + + // Whether draw buffer `drawbuffer` of the framebuffer currently bound as DRAW is such an + // attachment. Answered from the mask SyncCurrentFBO just recomputed, so it costs nothing. + Bool IsWidenedBoundDrawBuffer(GLenum buffer, GLint drawbuffer) { + return buffer == GL_COLOR && drawbuffer >= 0 && drawbuffer < 32 && + (FramebufferImpl::g_alphaWidenedDrawBufferMask & (1u << drawbuffer)) != 0; + } + } // namespace + + void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { + TextureImpl::SyncNeccessaryTextures(); + FramebufferImpl::SyncCurrentFBO(); + RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR); + + BindCurrentFBO(FramebufferTarget::Draw); + + GLfloat widenedValue[4] = {}; + g_GLESFuncs.glClearBufferfv( + buffer, drawbuffer, + SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), 1.0f, widenedValue)); + } + + void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { + TextureImpl::SyncNeccessaryTextures(); + FramebufferImpl::SyncCurrentFBO(); + RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR); + + // SyncCurrentFBO early-outs for the default framebuffer, so without this + // bind a user-FBO -> default-FBO switch would leave the clear landing on + // the stale driver DRAW binding (the fi/fv/uiv siblings all bind too). + BindCurrentFBO(FramebufferTarget::Draw); + + GLint widenedValue[4] = {}; + g_GLESFuncs.glClearBufferiv( + buffer, drawbuffer, + SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLint(1), widenedValue)); + } + + void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { + TextureImpl::SyncNeccessaryTextures(); + FramebufferImpl::SyncCurrentFBO(); + RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR); + + BindCurrentFBO(FramebufferTarget::Draw); + + GLuint widenedValue[4] = {}; + g_GLESFuncs.glClearBufferuiv( + buffer, drawbuffer, + SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLuint(1), widenedValue)); + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.cpp new file mode 100644 index 00000000..8e1435e4 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.cpp @@ -0,0 +1,1517 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { + // P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer resolves + // from the handle the verb record carried, and its CPU bytes are the SERVER's staged + // shadow (rule E) - the client's GL_DRAW_INDIRECT_BUFFER binding slot, the frontend + // object's MappedData()/GetSize() (B3) and the client slot allocator (T2) are never + // read. Coverage is asserted: commands read from bytes no record staged would be + // zero-filled, which is a missing record, not a valid command stream. + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + auto* resource = BufferImpl::FindBufferResourceForHandle(handle); + const Uint8* base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr; + // P8-C: a store nothing ever staged but a shader wrote (the GPU-culling shape) + // takes its first copy from this server's own GL buffer. A store that HAS a copy + // is not refreshed here: most indirect draws execute natively and read no field + // on the CPU, so ExecuteIndexed/ArraysIndirectCommands refresh the fields only + // when they consume them. + if (base == nullptr && resource != nullptr) { + base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_bytes"); + } + const SizeT commandOffset = reinterpret_cast(indirect); + if (base == nullptr) { + MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged " + "shadow on this side and no GPU write to take one from", + label, handle.Slot, handle.Gen); + return nullptr; + } + if (commandOffset + requiredBytes > BufferImpl::ResourceWidthForHandle(handle)) { + MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); + return nullptr; + } + BufferImpl::RequireStagedCoverage(*resource, base, commandOffset, + commandOffset + requiredBytes, "indirect_command_bytes"); + return base + commandOffset; } + // No buffer was bound: the frontend already raised the GL error; keep the old + // fall-through shape (a null indirect pointer is the same skip it was). + if (!indirect) { + MGLOG_E_ONCE("%s skipped: indirect pointer is null", label); + return nullptr; + } + MGLOG_E_ONCE("%s skipped: the verb carried no indirect buffer handle", label); + return nullptr; + } + + // TODO: deletion for deleted objects + + namespace BufferImpl { + // P5e (mv): THE ARM SELECTOR of this family - VertexInputReadsRecords() - moved to + // Managers.h, unchanged. MultiDraw.cpp is a second translation unit on the same draw + // path and has to select the same arm; a copy of the conjunction there would be two + // selectors for one family. Its rationale travelled with it. + + // P5e (vi): "does this draw fetch any attribute out of the application's own memory", + // answered FROM THE RECORD instead of from the frontend VAO. The emitter publishes + // Res == kMGPipeNullHandle for a client-memory array (VertexInputEmit.h says so in as + // many words: "A client-memory array is Res == kMGPipeNullHandle, and that is not a + // hole"), and the window is truncated at the highest ENABLED attribute, so a null Res + // inside [Start, Start+Count) is exactly the client-sourced case and nothing else. + // + // BOTH VIEWS ARE NEEDED, and taking only the buffer one would be wrong in the common + // direction: the window covers [0, highest enabled + 1), so a VAO whose attribute 0 is + // DISABLED and whose attribute 1 feeds from a VBO publishes entry[0].Res == null too. + // A null Res is "client-sourced" only for an attribute the CONFIGURATION has enabled, + // which is what the vertex-elements record answers. Reading one view and not the other + // would send every such ordinary draw back to the frontend VAO and keep the row this + // package is retiring alive for no reason. + // + // A prefix scan rather than a memo: Count is 1-3 on the measured workload, not 32, and + // this replaces a cross-TU accessor call plus a SharedPtr dereference per DrawArrays. + Bool AnyClientSideVertexArrayInRecord() { + const auto& st = MG_Pipe::MGPipeApplier(); + if (st.VertexBufferCount == 0) return false; + const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements; + if (MG_Pipe::MGPipeHandleIsNull(elements) || elements.Slot >= st.VertexElementsCsos.size()) { + return false; + } + const auto& record = st.VertexElementsCsos[elements.Slot]; + if (!record.Live || record.Gen != elements.Gen) return false; + + const Uint32 begin = st.VertexBufferStart; + const Uint32 end = begin + st.VertexBufferCount; + for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) { + if (!record.Attributes[i].Enabled) continue; + if (MG_Pipe::MGPipeHandleIsNull(st.VertexBuffers[i].Res)) return true; + } + return false; + } + } // namespace BufferImpl + + namespace VertexArrayImpl { + // The indirect executors' form: the command's own words ARE the draw's fetch, and a + // command block a shader wrote this frame is exactly what these shapes exist for - so the + // command buffer's shadow is reconciled before it is read, and `commandBytes` (the + // caller's possibly stale copy) is only the fallback for a client-authored block. + static Bool SyncClientSideVertexArraysForIndirectFetch( + const Uint8* commandBytes, SizeT commandOffset, GLsizei stride, GLsizei index, Uint8 indexSize, + const SharedPtr& commandBuffer) { + return true; + } + + // The indexed entry points' form. `indices` is a byte offset into the bound element buffer + // when one is bound and the application's own array otherwise, which is the same + // distinction every one of them already makes when it issues the draw. + static Bool SyncClientSideVertexArraysForIndexedFetch(GLenum type, GLsizei count, const void* indices, + GLsizei instanceCount, GLint baseVertex, + GLuint baseInstance) { + if (count <= 0 || instanceCount <= 0) return true; + // MONOLITH ONLY - see SyncClientSideVertexArraysForFetch; this wrapper's own read of + // the bound VAO is the frontend read that must not happen on an apply thread. + return true; + } + + static void SyncClientSideVertexArraysForDrawArrays(GLint first, GLsizei count) { + if (!BufferImpl::AnyClientSideVertexArrayInRecord()) return; + // P13: client-memory arrays are monolith-only - under a transport the client refuses + // them (Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}) before any record goes out, + // so a record naming one here means the bytes below belong to another process. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"ClientArrays\"} - a draw carrying a " + "client-memory vertex array reached the backend under a transport; the " + "client's refusal exists to make this unreachable"); + std::abort(); + } + auto* twin = ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements); + if (twin == nullptr) return; + const auto& currentVAO = MG_Pipe::gPipeInputs->GetBoundVertexArray(); + if (!currentVAO) return; + twin->SyncClientSideAttributesForDrawArrays(currentVAO, first, count); + return; + } + } // namespace VertexArrayImpl + + // ---- gl_ViewportIndex routing emulation, draw half --------------------------------------- + // See the block comment in Managers.h for what this is and why. Here is the state half: one + // replay pass per DISTINCT viewport state, each pushing that state onto the ES context's one + // viewport / one scissor / one depth range and telling the fragment gate which indices it + // serves. + + namespace ViewportRoutingImpl { + // One replay pass: the state to push, and the set of gl_ViewportIndex values whose + // fragments this pass is allowed to keep. + struct RoutingPass { + IntVec4 viewport{}; + IntVec4 scissorBox{}; + FloatVec2 depthRange{}; + Bool scissorTest = false; + Uint32 indexMask = 0; + }; + + static constexpr Uint32 kAllViewportsMask = + (RenderStateParameters::MAX_VIEWPORTS >= 32) + ? 0xFFFFFFFFu + : ((1u << RenderStateParameters::MAX_VIEWPORTS) - 1u); + + // The plan for the draw currently being issued. A file-scope buffer rather than a return + // value because Begin/Apply/End are three calls around a draw the caller writes, and a + // fixed array of 16 keeps it allocation-free on a path that is per draw. NOT re-entrant, + // which is a property of the call sites and not an accident: every wrap in this file and + // in MultiDraw.cpp is around the innermost native glDraw*, so no replay can begin inside + // another - and a multi-draw tier that replayed its whole loop would be nesting. + static Array g_passes{}; + static Uint g_passCount = 0; + static PrgramImpl::BackendProgramObjectImpl* g_routedProgram = nullptr; + + // What index `i` actually rasterizes against, resolved exactly the way SyncRenderState + // resolves index 0 - including both substitutions it makes, which are not cosmetic: + // + // * a viewport of zero extent means "the application has never called glViewport", and + // GL's initial viewport is the whole surface, which the frontend cannot spell before + // a surface exists; + // * a scissor rectangle is read through the WRITTEN flag and not through its extent, + // because glScissor(0, 0, 0, 0) is a legal request meaning "reject every fragment" + // and is byte-identical to the never-written default that means the opposite. + // + // Resolving them here rather than deferring to SyncRenderState is what makes the grouping + // below correct: two indices that differ only in a field that resolves to the same + // rectangle really do rasterize identically and must share one pass. + static RoutingPass ResolveIndexState(const RenderStateParameters& parameters, Uint index, + Int surfaceWidth, Int surfaceHeight) { + RoutingPass pass; + const FloatVec4& viewport = parameters.Viewports[index]; + pass.viewport = IntVec4(static_cast(std::lround(viewport.x())), + static_cast(std::lround(viewport.y())), + static_cast(std::lround(viewport.z())), + static_cast(std::lround(viewport.w()))); + if ((pass.viewport.z() <= 0 || pass.viewport.w() <= 0) && surfaceWidth > 0 && surfaceHeight > 0) { + pass.viewport = IntVec4(0, 0, surfaceWidth, surfaceHeight); + } + pass.scissorBox = parameters.ScissorBoxes[index]; + if ((parameters.ScissorBoxWrittenMask & (1u << index)) == 0 && surfaceWidth > 0 && + surfaceHeight > 0) { + pass.scissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight); + } + pass.depthRange = parameters.DepthRanges[index]; + pass.scissorTest = (parameters.ScissorTestEnabledMask & (1u << index)) != 0; + return pass; + } + + static Bool SameState(const RoutingPass& a, const RoutingPass& b) { + return a.viewport == b.viewport && a.scissorBox == b.scissorBox && + a.depthRange == b.depthRange && a.scissorTest == b.scissorTest; + } + } // namespace ViewportRoutingImpl + + Uint BeginViewportRoutingPasses() { + using namespace ViewportRoutingImpl; + g_passCount = 1; + g_routedProgram = nullptr; + + // The whole emulation behind one static load, for every application that has never built + // a program writing gl_ViewportIndex - which is all of them but the conformance suite. + // Without it every draw in the process would pay GetCurrentBackendProgram's chain of + // frontend lookups for an answer that cannot change. + if (!g_anyProgramRoutesViewportIndex) { + return 1; + } + + auto* program = GetCurrentBackendProgram(); + if (program == nullptr || !program->RoutesViewportIndex()) { + return 1; + } + g_routedProgram = program; + // The gate reads zero until something writes it, and a zero mask discards every fragment. + // So this is not an optimization that can be skipped in the one-pass case - it is what + // keeps a routing program drawing at all. + program->SetViewportPassMask(kAllViewportsMask); + + // Replaying multiplies every side effect the vertex and geometry stages have, and the + // fragment gate can only undo the ones that happen in the FRAGMENT stage. Transform + // feedback records per emitted primitive, so a replayed draw would write its vertices N + // times; rasterizer discard means there are no fragments to gate at all, so replaying + // would be pure cost with nothing to show for it. Both fall back to a single pass with an + // open gate, i.e. to the pre-emulation behaviour, rather than to wrong data. + if (MG_Pipe::gPipeInputs->IsTransformFeedbackActive() || + MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { + return 1; + } + + const auto& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters(); + Int surfaceWidth = 0; + Int surfaceHeight = 0; + if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { + surfaceWidth = 0; + surfaceHeight = 0; + } + + Uint count = 0; + for (Uint index = 0; index < RenderStateParameters::MAX_VIEWPORTS; ++index) { + const RoutingPass resolved = + ResolveIndexState(parameters, index, surfaceWidth, surfaceHeight); + Uint existing = 0; + for (; existing < count; ++existing) { + if (SameState(g_passes[existing], resolved)) break; + } + if (existing == count) { + g_passes[count] = resolved; + ++count; + } + g_passes[existing].indexMask |= (1u << index); + } + + // One group is the overwhelmingly common case - it is what glViewport, glScissor and + // glDepthRange leave behind, because ARB_viewport_array defines all three as writing + // EVERY index. The mask is already open and index 0's state is what SyncRenderState + // pushed, so there is nothing to replay and nothing to restore. + if (count <= 1) { + g_passCount = 1; + return 1; + } + g_passCount = count; + return count; + } + + void ApplyViewportRoutingPass(Uint pass) { + using namespace ViewportRoutingImpl; + if (pass >= g_passCount || g_routedProgram == nullptr) { + return; + } + const RoutingPass& entry = g_passes[pass]; + g_GLESFuncs.glViewport(entry.viewport.x(), entry.viewport.y(), entry.viewport.z(), + entry.viewport.w()); + g_GLESFuncs.glScissor(entry.scissorBox.x(), entry.scissorBox.y(), entry.scissorBox.z(), + entry.scissorBox.w()); + // ES has one scissor-test enable where GL has sixteen, so the per-index bit becomes a + // per-pass glEnable/glDisable. This is the half DirectVulkan cannot do at all (Vulkan has + // no per-viewport scissor toggle either and has to widen a disabled index's rectangle to + // the whole framebuffer instead); here the rectangle stays honest. + entry.scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + g_GLESFuncs.glDepthRangef(entry.depthRange.x(), entry.depthRange.y()); + g_routedProgram->SetViewportPassMask(entry.indexMask); + } + + void EndViewportRoutingPasses(Uint passCount) { + using namespace ViewportRoutingImpl; + if (passCount <= 1) { + // Nothing was pushed and the mask is already open; leaving the shadow alone here is + // what keeps a non-routing draw at exactly its previous cost. + g_routedProgram = nullptr; + return; + } + if (g_routedProgram != nullptr) { + // Any draw that reaches the driver without going through a replay - an internal blit, + // or a path this emulation has not been taught about - must not inherit the last + // pass's mask and paint nothing. + g_routedProgram->SetViewportPassMask(kAllViewportsMask); + } + g_routedProgram = nullptr; + g_passCount = 0; + // The viewport, scissor, scissor-test enable and depth range now on the ES context belong + // to the last replay pass, and the shadow SyncRenderState diffs against does not know it. + // A full resync is the honest repair and costs one state push on the next draw, which + // only a viewport-routing workload ever pays. + RenderStateImpl::InvalidateSyncedRenderState(); + } + + static Bool SupportsNativeIndirectDraws() { + return g_GLESCapabilities.SupportsDrawIndirect; + } + + // P8-C: the command bytes an indirect executor is about to CONSUME on the CPU, made current. + // Consumed means: every field on the CPU path (no native indirect), or on the native path the + // uniforms fed from the command - gl_BaseVertex whenever the program reads it, and + // gl_BaseInstance when the program reads it without the mg_IndirectParams storage view that + // would give it the GPU's word. A native draw that feeds neither reads nothing and keeps its + // pointer, so a compute-culled indirect buffer drawn every frame costs no readback. + static const Uint8* CurrentIndirectCommandBytes(MG_Pipe::MGPipeHandle handle, const Uint8* commandBytes, + SizeT commandOffset, Bool useNative, Bool indexed) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return commandBytes; + if (useNative) { + const auto program = GetCurrentBackendProgram(); + if (program == nullptr) return commandBytes; + const Bool baseVertexFromCpu = indexed && program->ReadsBaseVertex(); + const Bool baseInstanceFromCpu = program->GetIndirectParamsBinding() < 0 && program->ReadsBaseInstance(); + if (!baseVertexFromCpu && !baseInstanceFromCpu) return commandBytes; + } + auto* resource = BufferImpl::FindBufferResourceForHandle(handle); + if (resource == nullptr) return commandBytes; + const Uint8* base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_fields"); + return base != nullptr ? base + commandOffset : commandBytes; + } + + // Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws + // execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's + // culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted + // for the per-command baseInstance, which is CPU-authored, to feed the mg_BaseInstance + // shader emulation. Without GL_EXT_base_instance a non-zero baseInstance in the command is + // technically undefined in ES; mobile drivers ignore the reserved word, instanced-array + // fetches were never baseInstance-offset here anyway, and the CPU fallback cannot see + // GPU-written command fields at all - so native is never worse. ANGLE-on-Vulkan instead + // leaks the word into gl_InstanceID (it becomes vkCmdDrawIndexedIndirect's firstInstance); + // the shader rewrite compensates by rebasing gl_InstanceID during these draws when + // IndirectDrawInstanceIdIncludesBaseInstance is set (PromoteDrawParameterGlobalsToUniforms). + // Only client-memory commands take the CPU per-command loop. + static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes, + SizeT commandOffset, + const SharedPtr& drawIndirectBuffer, + GLsizei drawcount, GLsizei stride, const char* label) { + (void)label; + // An indirect command's firstIndex/count live in GPU memory, so the substitution has + // to rewrite the whole element array buffer rather than this draw's range - which is + // exactly what it does when no CPU-known count is handed to it. Held for the whole + // command loop so every command in the batch reads the rewritten copy. + // + // firstIndex counts ELEMENTS, so it survives a widened copy untouched; what does not + // survive is the type and the element size, which are re-taken from the substitution + // below for both the native and the CPU-unrolled path. + const ScopedRestartIndexSubstitution restart(type, /*count=*/0, /*indices=*/nullptr); + if (!restart.DrawIsValid()) return; + type = restart.IndexType(); + indexSize = MG_Util::GetGLTypeSize(type); + // P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer is the + // handle the verb record carried (MGPipeApplier's verb stash) and the SSBO view's + // resource resolves from it by handle - the frontend binding slot and the client slot + // allocator are never read (T2). + const MG_Pipe::MGPipeHandle verbBufferHandle = + MG_Pipe::MGPipeApplier().VerbIndirectBuffer; + const Bool useNative = + !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws(); + commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative, + /*indexed=*/true); + if (useNative) { + // gl_BaseInstance must observe GPU-written command fields; expose the indirect + // buffer to the program's mg_IndirectParams SSBO view and address it per draw. + const auto backendProgram = GetCurrentBackendProgram(); + const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; + if (paramsBinding >= 0) { + auto* resource = + BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle); + if (resource && resource->id != 0) { + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), + resource->id); + } + } + // gl_BaseVertex has no SSBO view of its own: the command's baseVertex word is read + // from the CPU shadow, so a command whose baseVertex a compute shader wrote this + // frame is not observable here (baseInstance is, through the view above). Feeding + // the stale-but-usually-correct shadow beats leaving the uniform at the previous + // draw's value, which is what a program reading gl_BaseVertex saw before. + const Bool feedBaseVertex = CurrentProgramReadsBaseVertex(); + for (GLsizei i = 0; i < drawcount; ++i) { + const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; + SetCurrentDrawID(static_cast(i)); + if (paramsBinding >= 0 && backendProgram) { + // baseInstance is the 5th word of DrawElementsIndirectCommand. + backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 16) / 4)); + if (feedBaseVertex) { + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + SetCurrentBaseVertex(cmd.baseVertex); + } + } else { + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + SetCurrentBaseInstance(cmd.baseInstance); + SetCurrentBaseVertex(cmd.baseVertex); + } + ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, static_cast(indexSize), drawIndirectBuffer)) + return; + g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(cmdByteOffset)); + }); + } + } else { + for (GLsizei i = 0; i < drawcount; ++i) { + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + if (cmd.count == 0 || cmd.instanceCount == 0) { + continue; + } + SetCurrentDrawID(static_cast(i)); + SetCurrentBaseInstance(cmd.baseInstance); + SetCurrentBaseVertex(cmd.baseVertex); + const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; + ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, static_cast(indexSize), drawIndirectBuffer)) + return; + g_GLESFuncs.glDrawElementsInstancedBaseVertex( + mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), + static_cast(cmd.instanceCount), cmd.baseVertex); + }); + } + } + SetCurrentDrawID(0); + SetCurrentBaseInstance(0); + SetCurrentBaseVertex(0); + } + + static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset, + const SharedPtr& drawIndirectBuffer, + GLsizei drawcount, GLsizei stride, const char* label) { + (void)label; + // DrawArraysIndirectCommand has no baseVertex word, so gl_BaseVertex is zero for every + // command here. Written BEFORE the draws, not merely restored after them: the previous + // draw is what leaves a stale value, and restoring afterwards would only protect the + // NEXT draw while these commands ran with the stale one. + SetCurrentBaseVertex(0); + // P5c (hd, CONTRACT-P5C §3.5): see ExecuteIndexedIndirectCommands - the verb's handle, + // never the frontend binding slot or the client allocator. + const MG_Pipe::MGPipeHandle verbBufferHandle = + MG_Pipe::MGPipeApplier().VerbIndirectBuffer; + const Bool useNative = + !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws(); + commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative, + /*indexed=*/false); + if (useNative) { + const auto backendProgram = GetCurrentBackendProgram(); + const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; + if (paramsBinding >= 0) { + auto* resource = + BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle); + if (resource && resource->id != 0) { + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), + resource->id); + } + } + for (GLsizei i = 0; i < drawcount; ++i) { + const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; + SetCurrentDrawID(static_cast(i)); + if (paramsBinding >= 0 && backendProgram) { + // baseInstance is the 4th word of DrawArraysIndirectCommand. + backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 12) / 4)); + } else { + DrawArraysIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + SetCurrentBaseInstance(cmd.baseInstance); + } + ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer)) + return; + g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast(cmdByteOffset)); + }); + } + } else { + for (GLsizei i = 0; i < drawcount; ++i) { + DrawArraysIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + if (cmd.count == 0 || cmd.instanceCount == 0) { + continue; + } + SetCurrentDrawID(static_cast(i)); + SetCurrentBaseInstance(cmd.baseInstance); + ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer)) + return; + g_GLESFuncs.glDrawArraysInstanced(mode, static_cast(cmd.first), + static_cast(cmd.count), + static_cast(cmd.instanceCount)); + }); + } + } + SetCurrentDrawID(0); + SetCurrentBaseInstance(0); + } + + + void Clear(GLbitfield mask) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + TextureImpl::SyncNeccessaryTextures(); + FramebufferImpl::SyncCurrentFBO(); + // A colour clear is exactly the operation that is allowed to write a widened + // attachment's alpha - it is what puts the 1.0 there that every later draw is masked + // away from. SyncCurrentFBO ran first, so g_alphaWidenedDrawBufferMask already describes + // the framebuffer this clear will land on. + RenderStateImpl::SyncRenderState(/*forColorClear=*/(mask & GL_COLOR_BUFFER_BIT) != 0); + + BindCurrentFBO(FramebufferTarget::Draw); + + // Debug-only diagnostics: is each offscreen clear complete and unscissored? +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG + { + static int diagCount = 0; + GLint fboId = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fboId); + if (fboId != 0 && diagCount++ < 900) { + GLint color0 = 0, depthName = 0, box[4] = {0}; + GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST); + GLboolean cmask[4] = {0}; + GLfloat cc[4] = {0}; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &color0); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &depthName); + g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, box); + g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, cmask); + g_GLESFuncs.glGetFloatv(GL_COLOR_CLEAR_VALUE, cc); + MGLOG_D("CLEAR fbo=%d color0=%d depth=%d mask=0x%x scissor=%d box=(%d,%d,%d,%d) cmask=%d%d%d%d cc=(%g,%g,%g,%g)", + fboId, color0, depthName, mask, (int)scissor, box[0], box[1], box[2], box[3], (int)cmask[0], + (int)cmask[1], (int)cmask[2], (int)cmask[3], cc[0], cc[1], cc[2], cc[3]); + } + } +#endif + + // GLES clamps the glClearColor state to [0,1] (desktop GL keeps it unclamped + // for float color buffers). Minecraft's OIT clears its depth-bounds RGBA32F + // target to -FLT_MAX as the MAX-blend identity, so an out-of-range clear + // color must go through glClearBufferfv, which GLES does not clamp. + GLbitfield remainingMask = mask; + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + const FloatVec4& cc = MG_Pipe::gPipeInputs->GetRenderStateParameters().ClearColor; + const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f || + cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f; + // A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE + // clear colour to every draw buffer - so a framebuffer that mixes a widened + // attachment with a native one cannot be served by doctoring glClearColor. Take the + // same per-draw-buffer glClearBufferfv route the out-of-range case already uses and + // substitute the alpha only where it belongs. Scissor and the colour write mask apply + // to glClearBufferfv exactly as they do to glClear, so a scissored clear stays + // scissored and an application that masked alpha off still gets its way (the storage + // then keeps the 1.0 an earlier clear left, which is the same answer). + // + // glClearBufferfv on an INTEGER colour buffer is GL_INVALID_OPERATION, so a + // framebuffer with one of those as a draw buffer keeps plain glClear - which ES + // leaves undefined for integer colour buffers anyway, and which an application that + // wants a defined answer must replace with glClearBufferuiv/iv (those DO substitute + // the widened alpha). The out-of-range trigger is left exactly as it was. + const Uint32 widenedDrawBuffers = FramebufferImpl::g_alphaWidenedDrawBufferMask; + const Bool widenedColorClear = + widenedDrawBuffers != 0 && FramebufferImpl::g_integerColorDrawBufferMask == 0; + GLint clearDrawFbo = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo); + if ((outOfRange || widenedColorClear) && clearDrawFbo != 0) { + GLint maxDrawBuffers = 0; + GLint clearedCount = 0; + GLint firstDb = -1; + g_GLESFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); + for (GLint i = 0; i < maxDrawBuffers; ++i) { + GLint db = GL_NONE; + g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast(i), &db); + if (i == 0) firstDb = db; + if (db != GL_NONE) { + const Bool widened = i < 32 && (widenedDrawBuffers & (1u << i)) != 0; + const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), widened ? 1.0f : cc.w()}; + g_GLESFuncs.glClearBufferfv(GL_COLOR, i, value); + ++clearedCount; + } + } + remainingMask &= ~static_cast(GL_COLOR_BUFFER_BIT); + (void)clearedCount; + (void)firstDb; + // Debug-only diagnostics: verify the unclamped clear actually landed. +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG + { + static int diagCount = 0; + if (diagCount++ < 20) { + const GLenum clrErr = g_GLESFuncs.glGetError(); + GLint prevRead = 0, prevPbo = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead); + g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPbo); + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)clearDrawFbo); + GLint prevReadBuf = GL_COLOR_ATTACHMENT0; + g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &prevReadBuf); + g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); + GLfloat rb[4] = {0}; + g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb); + const GLenum rbErr = g_GLESFuncs.glGetError(); + int feDb0 = -1, feDb1 = -1; + Uint feIdx = 0, feVer = 0; + // P5e (fb): DEBUG-ONLY, and still a BARRIER_PULLED read. A diagnostic + // is not a reason to touch the binding slot on an apply thread, so + // under a transport the same four numbers come off the draw record - + // the external index has no record counterpart and stays 0, which reads + // as "the record did not say". + if (const auto* rec = BoundFramebufferRecord( + FramebufferTarget::Draw)) { + feVer = static_cast(rec->ContentHash); + feDb0 = (int)rec->DrawBuffers[0]; + feDb1 = (int)rec->DrawBuffers[1]; + } + MGLOG_D("CLEARV fbo=%d clrErr=0x%x rbErr=0x%x cc.x=%g cleared=%d firstDb=0x%x prevReadBuf=0x%x " + "feFbo=%u feVer=%u feDb=[%d,%d] stored=(%g,%g,%g,%g)", + clearDrawFbo, clrErr, rbErr, cc.x(), clearedCount, firstDb, prevReadBuf, feIdx, feVer, + feDb0, feDb1, rb[0], rb[1], rb[2], rb[3]); + g_GLESFuncs.glReadBuffer(static_cast(prevReadBuf)); + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevRead); + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, (GLuint)prevPbo); + } + } +#endif + } + } + if (remainingMask != 0) { + g_GLESFuncs.glClear(remainingMask); + } + } + + // --------------------------------------------------------------------------- + // Arbitrary-index primitive restart + // + // Desktop GL restarts on whatever index glPrimitiveRestartIndex named; GLES core only + // ever restarts on the all-ones value of the index type. When the two agree - which + // includes every GL_PRIMITIVE_RESTART_FIXED_INDEX user - the render state push at + // SyncRenderState is the whole implementation and nothing here does any work. When they + // disagree the index DATA is rewritten into a scratch element array buffer. + // + // This used to throw instead. A throw here unwinds a C++ exception through the C GL ABI + // and takes the process down - the same hazard GL_Texture.cpp and RenderState.cpp + // already call out - so an application that merely asked for a legal desktop feature + // died rather than got an error. + // --------------------------------------------------------------------------- + + namespace { + struct RestartScratchBuffer { + Uint id = 0; + SizeT capacity = 0; + }; + + RestartScratchBuffer g_restartIndices; + Vector g_restartStaging; + + // Past this the rewrite would stage and re-upload hundreds of megabytes on EVERY + // draw (the copy is not memoised, exactly as on the Vulkan side). Decline instead of + // trying: a draw that renders nothing is recoverable, a stall of that size is not. + constexpr SizeT kMaxRestartRewriteBytes = SizeT{1} << 26; // 64 MiB + + // The index type one step wider than this one, or 0 when there is none. Widening is how + // an all-ones value that is a REAL vertex index keeps its meaning while the all-ones + // value of the destination type serves as the restart sentinel: a source that cannot + // spell 0xFFFF cannot collide with a 16-bit sentinel, and likewise 8 -> 16. + GLenum WiderIndexType(GLenum indexType) { + switch (indexType) { + case GL_UNSIGNED_BYTE: return GL_UNSIGNED_SHORT; + case GL_UNSIGNED_SHORT: return GL_UNSIGNED_INT; + default: return 0; + } + } + + Uint32 ReadIndex(const Uint8* source, SizeT i, SizeT indexSize) { + switch (indexSize) { + case 1: return source[i]; + case 2: { + Uint16 narrow = 0; + std::memcpy(&narrow, source + i * 2, sizeof(narrow)); + return narrow; + } + default: { + Uint32 wide = 0; + std::memcpy(&wide, source + i * 4, sizeof(wide)); + return wide; + } + } + } + + void WriteIndex(Uint8* destination, SizeT i, SizeT indexSize, Uint32 value) { + switch (indexSize) { + case 1: destination[i] = static_cast(value); break; + case 2: { + const Uint16 narrow = static_cast(value); + std::memcpy(destination + i * 2, &narrow, sizeof(narrow)); + break; + } + default: std::memcpy(destination + i * 4, &value, sizeof(value)); break; + } + } + + // True when any index in the range already holds the type's all-ones value, i.e. when + // that value is doing double duty as a real vertex index and so cannot also be the + // restart sentinel. Only asked on the rare substitution path. + Bool ContainsFixedRestartIndex(const Uint8* source, SizeT indexCount, SizeT indexSize, + Uint32 fixedMax) { + for (SizeT i = 0; i < indexCount; ++i) { + if (ReadIndex(source, i, indexSize) == fixedMax) return true; + } + return false; + } + + // Copies index data, replacing every occurrence of the application's restart index with + // the all-ones value of the DESTINATION type - the only one GLES restarts on. The + // destination may be wider than the source, which is what makes the copy lossless: a + // source index equal to the source's all-ones value zero-extends to something the wider + // sentinel can never equal, so it stays the vertex it was. + // + // Same width in and out is the degenerate case, used when the source contains no + // all-ones index at all (nothing to protect) or when there is no wider type to move to. + // In that last case only - a GL_UNSIGNED_INT stream that really does use index + // 0xFFFFFFFF while asking to restart on a different one - a legal index has to be + // nudged to 0xFFFFFFFE, because 32 bits cannot hold both meanings. The caller logs it; + // it is the one input this feature cannot represent. + void RewriteRestartIndices(const Uint8* source, SizeT indexCount, SizeT sourceIndexSize, + SizeT destinationIndexSize, Uint32 applicationRestartIndex, + Uint32 destinationFixedMax, Vector& output) { + output.resize(indexCount * destinationIndexSize); + for (SizeT i = 0; i < indexCount; ++i) { + Uint32 value = ReadIndex(source, i, sourceIndexSize); + if (value == applicationRestartIndex) { + value = destinationFixedMax; + } else if (value == destinationFixedMax) { + // Only reachable when no widening was possible; see above. + value = destinationFixedMax - 1; + } + WriteIndex(output.data(), i, destinationIndexSize, value); + } + } + + // Whole-buffer respecify through the manager-wide staging target, so binding it + // disturbs no VAO state. glBufferData orphans the previous store, so the upload + // never waits on a draw still reading the old contents out of the same name. + Bool UploadRestartScratch(SizeT bytes, const void* data) { + if (g_restartIndices.id == 0) { + GLuint id = 0; + g_GLESFuncs.glGenBuffers(1, &id); + if (id == 0) return false; + g_restartIndices.id = id; + g_restartIndices.capacity = 0; + } + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_restartIndices.id); + SizeT capacity = g_restartIndices.capacity == 0 ? bytes : g_restartIndices.capacity; + while (capacity < bytes) capacity *= 2; + g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(capacity), nullptr, + GL_STREAM_DRAW); + g_restartIndices.capacity = capacity; + if (data != nullptr && bytes != 0) { + g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast(bytes), data); + if (MG_Util::PipeStats::Enabled()) { + // Rewritten index list staged on the draw path: in a split build these + // bytes are the index-mirror-versus-ship decision of section 8. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient, + static_cast(bytes)); + } + } + return true; + } + } // namespace + + RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) { + if (!MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { + return RestartSubstitutionKind::None; + } + const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType); + if (fixedMax == 0) return RestartSubstitutionKind::None; + const Uint32 restartIndex = MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex(); + if (restartIndex == fixedMax) return RestartSubstitutionKind::None; + // Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index + // zero-extended against the full 32-bit state, so an index this type cannot hold matches + // nothing. Truncating instead - glPrimitiveRestartIndex(0x100) over GL_UNSIGNED_BYTE data + // becoming "restart on 0" - turns the most common index in any mesh into a restart. + if (restartIndex > fixedMax) return RestartSubstitutionKind::SuppressRestart; + return RestartSubstitutionKind::RewriteIndices; + } + + void OnRestartSubstitutionContextDestroyed() { + g_restartIndices = {}; + g_restartStaging.clear(); + g_restartStaging.shrink_to_fit(); + } + + ScopedSuppressedPrimitiveRestart::ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind) { + if (kind != RestartSubstitutionKind::SuppressRestart) return; + // SyncRenderState turned the driver's fixed-index restart on because GL_PRIMITIVE_RESTART + // is enabled; for this draw's index type it would restart on a value the application + // never named. Toggled directly rather than through the render-state shadow, and put back + // in the destructor, so the shadow stays true and the next draw pays nothing. + g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + m_suppressed = true; + } + + ScopedSuppressedPrimitiveRestart::~ScopedSuppressedPrimitiveRestart() { + if (!m_suppressed) return; + g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + } + + ScopedRestartIndexSubstitution::ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, + const void* indices) + : m_kind(ResolveRestartSubstitution(indexType)), m_capOverride(m_kind), m_indices(indices), + m_indexType(indexType) { + if (m_kind != RestartSubstitutionKind::RewriteIndices) { + return; + } + const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType); + const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType); + const Uint32 applicationRestartIndex = MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex(); + + const Uint8* source = nullptr; + SizeT indexCount = 0; + SizeT sourceByteOffset = 0; + Bool hasElementBuffer = false; + // -1: monolith, or this arm never resolved a backend id, and the tail asks + // BoundElementArrayBufferId() exactly as it always did. + Int serverElementBinding = -1; + + // P5c (hd, CONTRACT-P5C §3.5): with an active transport this constructor runs on the + // apply thread, where the frontend BufferObject's legacy accessors are Fatal by name + // (BufferObject.cpp's "buffer-legacy-arm" refusal). The element buffer therefore + // resolves from the applier's set_index_buffer state (T2) and the bytes read from the + // server's own staged shadow - "the backend's ScopedRestartIndexSubstitution reads the + // index bytes it needs on its side" (CONTRACT-P5B d1), the same arm + // MultiDrawElementsIndirectCount takes for its command buffer. A draw whose record + // carried client indices (no index buffer bound) takes the client-pointer arm below, + // `indices` already resolved through the server's segment resolver. Monolith takes + // the original body, verbatim. + const auto& applierState = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle elements = applierState.IndexBuffer.Res; + serverElementBinding = 0; + if (!MG_Pipe::MGPipeHandleIsNull(elements)) { + auto* elementResource = BufferImpl::FindBufferResourceForHandle(elements); + // The WHOLE buffer is rewritten, not just this draw's range, so that every + // index keeps its position: an indirect draw's firstIndex lives in GPU memory + // and cannot be adjusted from here. It is an ELEMENT index, so it survives + // widening unchanged. + const SizeT sizeBytes = BufferImpl::ResourceWidthForHandle(elements); + if (sizeBytes < sourceIndexSize) { + return; // Nothing to restart on; let the driver see the draw unchanged. + } + if (sizeBytes > kMaxRestartRewriteBytes) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element " + "array buffer rewritten every draw, which is past the %zu-byte ceiling. Use " + "GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones " + "value of the index type.", + applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes); + m_valid = false; + return; + } + // The staged shadow is the source of truth for CPU reads on this side: a + // persistent map's blocks were consumed by the applier before this verb ran, and + // (P8-C) a store a shader wrote on this side is read back from this server's own + // GL buffer first - no writeback ever reaches the server's copy. + const Uint8* hostBytes = + elementResource + ? BufferImpl::SplitHostBytesForCpuRead(*elementResource, elements, + "primitive_restart_substitution") + : nullptr; + if (hostBytes == nullptr) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of " + "the bound element array buffer and none is available.", + applicationRestartIndex); + m_valid = false; + return; + } + // The M-3 rule for a whole-store reader (Managers.cpp:3238): for a store whose + // content the application SUPPLIED a coverage gap is a missing record and is + // Fatal by name; for one it ORPHANED the gap is its own undefined content and + // reading the shadow's zero-fill is exactly what the monolith arm's + // MappedData() answers. + const MG_Pipe::MGPipeResourceRecord* elementRecord = nullptr; + if (elements.Slot < applierState.Resources.size()) { + const auto& candidate = applierState.Resources[elements.Slot]; + if (candidate.Live && candidate.Gen == elements.Gen) elementRecord = &candidate; + } + if (elementRecord != nullptr && elementRecord->Desc.HasDefinedContent != 0) { + BufferImpl::RequireStagedCoverage(*elementResource, hostBytes, 0, sizeBytes, + "primitive_restart_substitution"); + } + hasElementBuffer = true; + serverElementBinding = static_cast(elementResource->id); + source = hostBytes; + indexCount = sizeBytes / sourceIndexSize; + sourceByteOffset = reinterpret_cast(indices); + } + if (!hasElementBuffer) { + // No element array buffer: `indices` is a client pointer, so only the draw's own + // range is readable and an indirect draw has nothing to read at all. + if (count <= 0 || indices == nullptr || sourceIndexSize == 0) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs either a bound element " + "array buffer or a client index array with a CPU-known count.", + applicationRestartIndex); + m_valid = false; + return; + } + if (static_cast(count) * sourceIndexSize > kMaxRestartRewriteBytes) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART index rewrite of %zu bytes is past the %zu-byte " + "ceiling.", + static_cast(count) * sourceIndexSize, kMaxRestartRewriteBytes); + m_valid = false; + return; + } + source = static_cast(indices); + indexCount = static_cast(count); + } + + // Widen only when the source really does use the all-ones value as a vertex index - + // otherwise the sentinel is free and the copy stays the caller's width, which keeps the + // common substitution allocation-for-allocation identical to the narrow form. + GLenum destinationType = indexType; + SizeT destinationIndexSize = sourceIndexSize; + if (ContainsFixedRestartIndex(source, indexCount, sourceIndexSize, fixedMax)) { + const GLenum wider = WiderIndexType(indexType); + // An element-array offset that is not a whole number of indices cannot be rescaled + // into the widened copy, so such a draw keeps the narrow (lossy) form. + const Bool offsetIsWholeIndices = sourceIndexSize != 0 && (sourceByteOffset % sourceIndexSize) == 0; + if (wider != 0 && offsetIsWholeIndices && + indexCount * MG_Util::GetGLTypeSize(wider) <= kMaxRestartRewriteBytes) { + destinationType = wider; + destinationIndexSize = MG_Util::GetGLTypeSize(wider); + } else { + MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses the " + "all-ones index %u: this index type cannot spell both, so every all-ones index is " + "drawn as %u instead. Use GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones " + "value out of the index data.", + applicationRestartIndex, fixedMax, fixedMax - 1); + } + } + + const Uint32 destinationFixedMax = MG_Util::FixedRestartIndexForGLType(destinationType); + RewriteRestartIndices(source, indexCount, sourceIndexSize, destinationIndexSize, applicationRestartIndex, + destinationFixedMax, g_restartStaging); + + if (!UploadRestartScratch(g_restartStaging.size(), g_restartStaging.data())) { + MGLOG_E_ONCE("Draw skipped: could not allocate the scratch element array buffer for GL_PRIMITIVE_RESTART " + "index substitution."); + m_valid = false; + return; + } + m_previousBinding = 0; + if (serverElementBinding >= 0) m_previousBinding = static_cast(serverElementBinding); + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id); + m_substituted = true; + m_indexType = destinationType; + // The rewritten copy starts at byte 0 of the scratch buffer and holds one + // destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT + // offset (rescaled to the new width) and a client-memory draw reads from the front. + m_indices = hasElementBuffer + ? reinterpret_cast((sourceByteOffset / sourceIndexSize) * destinationIndexSize) + : nullptr; + } + + ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution() { + if (!m_substituted) return; + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, m_previousBinding); + } + + void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return; +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawElements(mode, count, restart.IndexType(), restart.Indices()); + }); + } + + void DrawArrays(GLenum mode, GLint first, GLsizei count) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + DrawSyncFlags syncBit = DrawSyncBit::None; + PrepareForDraw(syncBit); + VertexArrayImpl::SyncClientSideVertexArraysForDrawArrays(first, count); + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawArrays(mode, first, count); + }); + } + + void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return; +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + SetCurrentBaseVertex(basevertex); + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex); + }); + SetCurrentBaseVertex(0); + } + + void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + DrawSyncFlags syncBit = DrawSyncBit::None; + PrepareForDraw(syncBit); + + // This loop IS the emulation - there is no batched tier for the non-indexed form - + // so each sub-draw has to be given its own gl_DrawID here, exactly as the indexed + // ladder and the indirect executors do. Without it every sub-draw of a + // glMultiDrawArrays read draw index 0. + const Bool feedDrawID = CurrentProgramReadsDrawID(); + for (GLsizei i = 0; i < drawcount; ++i) { + // Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path. + VertexArrayImpl::SyncClientSideVertexArraysForDrawArrays(first[i], count[i]); + if (feedDrawID) SetCurrentDrawID(static_cast(i)); + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawArrays(mode, first[i], count[i]); + }); + } + if (feedDrawID) SetCurrentDrawID(0); + } + + void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex); + } + + void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + if (drawcount <= 0) { + return; + } + if (stride == 0) { + stride = sizeof(DrawElementsIndirectCommand); + } + if (stride < static_cast(sizeof(DrawElementsIndirectCommand))) { + MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu", + stride, sizeof(DrawElementsIndirectCommand)); + return; + } + + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + const SizeT indexSize = MG_Util::GetGLTypeSize(type); + if (indexSize == 0) { + MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type); + return; + } + + const auto* commandBytes = ResolveIndirectCommandBytes( + indirect, + static_cast(stride) * static_cast(drawcount - 1) + sizeof(DrawElementsIndirectCommand), + "MultiDrawElementsIndirect"); + if (!commandBytes) { + return; + } + + const auto& drawIndirectBuffer = + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + SharedPtr(nullptr); + ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), + drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect"); + } + + void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, + GLsizei maxdrawcount, GLsizei stride) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + if (maxdrawcount <= 0) { + return; + } + if (stride == 0) { + stride = sizeof(DrawElementsIndirectCommand); + } + if (stride < static_cast(sizeof(DrawElementsIndirectCommand))) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu", + stride, sizeof(DrawElementsIndirectCommand)); + return; + } + + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + const SizeT indexSize = MG_Util::GetGLTypeSize(type); + if (indexSize == 0) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type); + return; + } + + // P5c (hd, CONTRACT-P5C §3.5): with an active transport both buffers resolve from the + // verb record's handles (MGPDrawIndirect::Buffer / ParameterBuffer) and the count + // reads from the SERVER's staged shadow - the frontend binding slots, the frontend + // accessors (B3) and the client allocator (T2) are never read. Monolith takes the + // original body below, verbatim. + const auto& applierState = MG_Pipe::MGPipeApplier(); + const SizeT commandOffset = reinterpret_cast(indirect); + const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + + sizeof(DrawElementsIndirectCommand); + auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer); + auto* paramResource = + BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer); + // P8-C: the count is CONSUMED here, so the parameter buffer is made current first + // (a store a shader wrote on this side is read back); the command buffer only gets a + // first copy when it has none - its fields are made current by the executor, and + // only when it consumes them (CurrentIndirectCommandBytes). + const Uint8* const parameterBytes = + paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource, + applierState.VerbIndirectParameterBuffer, + "indirect_count_parameter") + : nullptr; + const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr; + if (drawBytes == nullptr && drawResource != nullptr) { + drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer, + "indirect_count_commands"); + } + if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); + return; + } + if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > + BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); + return; + } + // No staged shadow means no count to read, not a wrong one - the monolith body's + // own rule for a buffer with no CPU shadow. + if (parameterBytes == nullptr || drawBytes == nullptr) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or " + "draw-indirect buffer"); + return; + } + BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes, + "multidraw_elements_indirect_count_commands"); + BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast(drawcount), + static_cast(drawcount) + sizeof(Uint32), + "multidraw_elements_indirect_count_parameter"); + Uint32 actualDrawCount = 0; + std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount)); + actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); + ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBytes + commandOffset, commandOffset, + nullptr, static_cast(actualDrawCount), stride, + "MultiDrawElementsIndirectCount"); + return; + } + + void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + if (drawcount <= 0) { + return; + } + if (stride == 0) { + stride = sizeof(DrawArraysIndirectCommand); + } + if (stride < static_cast(sizeof(DrawArraysIndirectCommand))) { + MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu", + stride, sizeof(DrawArraysIndirectCommand)); + return; + } + + DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + const auto* commandBytes = ResolveIndirectCommandBytes( + indirect, + static_cast(stride) * static_cast(drawcount - 1) + sizeof(DrawArraysIndirectCommand), + "MultiDrawArraysIndirect"); + if (!commandBytes) { + return; + } + + const auto& drawIndirectBuffer = + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + SharedPtr(nullptr); + ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, + drawcount, stride, "MultiDrawArraysIndirect"); + } + + // The non-indexed twin of MultiDrawElementsIndirectCount, and structurally identical to it: + // ES has no GL_PARAMETER_BUFFER at all, so the draw count is read from the CPU shadow of the + // bound one and the batch degenerates into an ordinary indirect multi-draw of that many + // commands. Missing from the backend table until now, which made every + // glMultiDrawArraysIndirectCount an INVALID_OPERATION ("backend does not support + // indirect-parameter array draws") on DirectGLES while the extension was advertised. + void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, + GLsizei stride) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + if (maxdrawcount <= 0) { + return; + } + if (stride == 0) { + stride = sizeof(DrawArraysIndirectCommand); + } + if (stride < static_cast(sizeof(DrawArraysIndirectCommand))) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu", + stride, sizeof(DrawArraysIndirectCommand)); + return; + } + + DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + // P5c (hd, CONTRACT-P5C §3.5): see the indexed twin - the verb record's handles and + // the SERVER's staged shadow, never the frontend bindings or the client allocator. + const auto& applierState = MG_Pipe::MGPipeApplier(); + const SizeT commandOffset = reinterpret_cast(indirect); + const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + + sizeof(DrawArraysIndirectCommand); + auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer); + auto* paramResource = + BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer); + // P8-C: the count is CONSUMED here, so the parameter buffer is made current first + // (a store a shader wrote on this side is read back); the command buffer only gets a + // first copy when it has none - its fields are made current by the executor, and + // only when it consumes them (CurrentIndirectCommandBytes). + const Uint8* const parameterBytes = + paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource, + applierState.VerbIndirectParameterBuffer, + "indirect_count_parameter") + : nullptr; + const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr; + if (drawBytes == nullptr && drawResource != nullptr) { + drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer, + "indirect_count_commands"); + } + if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); + return; + } + if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > + BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); + return; + } + if (parameterBytes == nullptr || drawBytes == nullptr) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or " + "draw-indirect buffer"); + return; + } + BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes, + "multidraw_arrays_indirect_count_commands"); + BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast(drawcount), + static_cast(drawcount) + sizeof(Uint32), + "multidraw_arrays_indirect_count_parameter"); + Uint32 actualDrawCount = 0; + std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount)); + actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); + ExecuteArraysIndirectCommands(mode, drawBytes + commandOffset, commandOffset, nullptr, + static_cast(actualDrawCount), stride, + "MultiDrawArraysIndirectCount"); + return; + } + + void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, + const void* indices, GLint basevertex) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + SetCurrentBaseVertex(basevertex); + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, restart.IndexType(), restart.Indices(), + basevertex); + }); + SetCurrentBaseVertex(0); + } + + void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawRangeElements(mode, start, end, count, restart.IndexType(), restart.Indices()); + }); + } + + // True when the driver will apply baseInstance to the vertex fetch itself, in which case the + // attribute-offset emulation must stay out of the way. SetCurrentBaseInstance is orthogonal + // and runs either way - it feeds the shader's gl_BaseInstance, not the fetch. + inline Bool UseNativeBaseInstance() { + return g_GLESCapabilities.SupportsBaseInstance; + } + + // The emulated shift has to be in place before PrepareForDraw, because that is what syncs the + // VAO; a zero here is what un-shifts the arrays for the next ordinary draw. + // + // P3a (D-H2): this is the LEGACY arm's carrier. On the handle arm the draw's raw base + // instance rides in MGPVertexBuffers::BaseInstance - a ContentHash input, so a base-instance + // change that moves no buffer is still emitted rather than suppressed - and the server + // decides whether to shift, because emulation ownership is the server's. The scopes below + // are therefore compiled only where the pre-handle arm is (a pull build always). + inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) { + return UseNativeBaseInstance() ? 0u : static_cast(baseinstance); + } + +#define MGL_SCOPED_FETCH_BASE_INSTANCE(name, value) ((void)(value)) + + void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLsizei instancecount, GLint basevertex, GLuint baseinstance) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex, + baseinstance)) return; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + SetCurrentBaseInstance(baseinstance); + SetCurrentBaseVertex(basevertex); + ForEachViewportRoutingPass([&] { + if (UseNativeBaseInstance()) { + g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, restart.IndexType(), + restart.Indices(), instancecount, + basevertex, baseinstance); + } else { + g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(), + instancecount, basevertex); + } + }); + SetCurrentBaseVertex(0); + SetCurrentBaseInstance(0); + } + + void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLsizei instancecount, GLint basevertex) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex, + 0)) return; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + SetCurrentBaseVertex(basevertex); + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount, + basevertex); + }); + SetCurrentBaseVertex(0); + } + + void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLsizei instancecount, GLuint baseinstance) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, + baseinstance)) return; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + SetCurrentBaseInstance(baseinstance); + ForEachViewportRoutingPass([&] { + if (UseNativeBaseInstance()) { + g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(), + instancecount, baseinstance); + } else { + g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount); + } + }); + SetCurrentBaseInstance(0); + } + + void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, 0)) + return; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + const ScopedRestartIndexSubstitution restart(type, count, indices); + if (!restart.DrawIsValid()) return; + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount); + }); + } + + void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + const SizeT indexSize = MG_Util::GetGLTypeSize(type); + if (indexSize == 0) { + MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type); + return; + } + + const auto* commandBytes = + ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand), "DrawElementsIndirect"); + if (!commandBytes) { + return; + } + + const auto& drawIndirectBuffer = + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + SharedPtr(nullptr); + ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), + drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand), + "DrawElementsIndirect"); + } + + void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, + GLuint baseinstance) { + DrawSyncFlags syncBit = DrawSyncBit::Instancing; + MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); + PrepareForDraw(syncBit); + SetCurrentBaseInstance(baseinstance); + ForEachViewportRoutingPass([&] { + if (UseNativeBaseInstance()) { + g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance); + } else { + g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); + } + }); + SetCurrentBaseInstance(0); + } + + void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { + DrawSyncFlags syncBit = DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + ForEachViewportRoutingPass([&] { + g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); + }); + } + + void DrawArraysIndirect(GLenum mode, const void* indirect) { + DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + const auto* commandBytes = + ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); + if (!commandBytes) { + return; + } + + const auto& drawIndirectBuffer = + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + SharedPtr(nullptr); + ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, 1, + sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); + } + + // Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a + // lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would + // otherwise spin an unbounded drain forever inside whichever GL entry point happened to be + // cleaning up, which is how a GPU reset reads as an unkillable process whose log simply + // stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the + // diagnostic. Every drain in this backend goes through here so the bound cannot drift apart + // between them. + static constexpr Int kMaxDrainedGLErrors = 32; + + void DrainDriverErrors(const char* site) { + Int drained = 0; + while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) { + ++drained; + } + if (drained == kMaxDrainedGLErrors) { + MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost", + site, kMaxDrainedGLErrors); + } + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h b/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h new file mode 100644 index 00000000..007f4adc --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h @@ -0,0 +1,1078 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.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 "DirectGLES.h" +#include "ContextEpoch.h" +#include "MG_Util/X11/DisplayGuard.h" +#include "EGL/egl.h" +#include "MG_Util/Types.h" +#include "Utils.h" +#include "Managers.h" +#include "MultiDraw.h" +#include +#include +#include +#include +#include +#include +// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against. +#include +// P5c ev: the surface-changed event's producer callback, installed by the server session. +#include +// P5e (pa): MGPipeShaderCsoRecord::Archive is a SharedPtr and PipeApply.h +// deliberately only forward-declares the type (its own note at :46 says to come here for it). +// The draw path's attribute-values sync reads Archive->Link, so this TU needs it complete - +// Managers.cpp already includes it for ProgramArchiveSource, which is the same reason. +#include +// P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads. +#include +// The record arm's deaths and latches (MGPipeRecordFail / MGPipeRecordLatch), in every build. +#include +#if MOBILEGL_BUILD_DISAGGREGATED +// Shared images: the registry the EGLImages are built from (SharedImageImpl below). +#include +// Device loss ends the session through MG_Pipe's latch seam (LatchIfDeviceLost). +#include +#if defined(__ANDROID__) +#include +#include +#endif +#endif +#if defined(__ANDROID__) +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__linux__) && !defined(__ANDROID__) && __has_include() +#pragma push_macro("Bool") +#pragma push_macro("None") +#include +#pragma pop_macro("None") +#pragma pop_macro("Bool") +#endif + +namespace MobileGL::MG_Backend::DirectGLES { + Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight); + + // A4, TAKEN AT THE VERIFICATION ROUND, AND THE FOUR TEMPORARY LATCHES ARE GONE WITH IT. + // This package landed BEFORE D and so carried its own bit tests, plus a copy of D-K2's + // dependency directions, so that a half-running mask was refused in the meantime too. + // Package D has landed, so every consult below calls D's PUBLIC WRAPPERS instead - + // FramebufferSubsystemEnabled() / TextureResourceSubsystemEnabled() / + // SamplerSubsystemEnabled() / ProgramSubsystemEnabled() (Managers.h, beside the four + // ResolveSubsystemArm() resolvers; the WRAPPER holds the `static const` latch, so + // the resolver is never the thing to call) - which additionally LOG each dependency + // refusal naming both bits and reach Fatal{PipeLegacyMemosDisabled} under + // MOBILEGL_PIPE_LEGACY_MEMOS=0, neither of which a copy here could do. + // + // NOTHING HERE RESTATES A DEPENDENCY DIRECTION any more - D-K2's fourth row (bit 10 + // requires bit 11, ID-15) included. It is in ResolveTextureResourceSubsystemArm, which is + // its owner, and two writers of one diagnostic is how the two drift. + + // THE ONE PLACE THIS FILE ASKS THE APPLIER "which framebuffer record describes this + // binding". Six reads used to spell `MGPipeApplier().DrawFramebuffer` / `.ReadFramebuffer` + // inline; they go through here instead, for exactly one reason. + // + // ID-19 turns the applier's framebuffer state into a PER-OBJECT table keyed by the + // framebuffer handle, with `BoundFramebuffer[Draw|Read]` holding the bound handles, so that + // the DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) can be + // handed a record for a framebuffer that is bound to neither binding. Wire v3 keeps + // `DrawFramebuffer` / `ReadFramebuffer` as ACCESSORS OF THOSE NAMES resolving through the + // bound handle, precisely so this package keeps its shape. If they land as member functions + // rather than as members, this one function grows a pair of parentheses and nothing else in + // this file moves; if the accessor can answer "no record for the bound handle", that answer + // arrives here as a null Fbo, which is already what every caller treats as a decline. + // + // WIRE v3 LANDED BOTH HALVES OF THAT PREDICTION: they are member FUNCTIONS and they return + // a NULLABLE POINTER, so the four call sites gained a null check each and nothing else in + // this file moved. Null is a real answer with exactly three causes (PipeApply.h): nothing + // is bound to that binding, no record has been written at the bound handle's slot, or the + // slot's generation has moved on under the handle - the third counted AND logged by the + // applier itself in StaleFramebufferRecordLookups. All three already arrived here as a + // null Fbo and were already a decline; the callers test the pointer instead. + static const MG_Pipe::MGPFramebufferState* BoundFramebufferRecord(FramebufferTarget target) { + const auto& st = MG_Pipe::MGPipeApplier(); + return target == FramebufferTarget::Draw ? st.DrawFramebuffer() : st.ReadFramebuffer(); + } + + [[noreturn]] static void RefuseFramebufferBindingSlotRead() { + MG_Pipe::MGPipeInputPoisonFatalForVerb(MG_Pipe::MGPipeInputField::GetFramebufferBindingSlot, + MG_Pipe::gPipeInputs->CurrentVerb()); + } + + SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler(); + + Bool NeedsRawDepthFetchSampler(const SamplerParameters& samplerParams, TextureInternalFormat textureFormat); + + Bool NeedsRawDepthFetchSampler(const SharedPtr& samplerObject, + TextureInternalFormat textureFormat); + + namespace BufferImpl { + Uint ResolveDrawVertexBuffersFromRecord(const MG_Pipe::MGPipeApplierState& st, DrawVertexBufferRequest* out, + Uint capacity); + + DrawIndexBufferRequest ResolveDrawIndexBufferFromRecord(const MG_Pipe::MGPipeApplierState& st); + + extern Bool g_shaderStorageWritesUnordered; + + void SyncBufferBindingPointsByRecord(Uint32 shaderBufferClass, GLenum glTarget); + + void SyncAtomicCounterBuffers(const Vector& glBindings, Int esslBindingTop); + } // namespace BufferImpl + + // Transform feedback is captured by the real ES driver: the backend program + // declares the capture set at link time (see BackendProgramObjectImpl::SyncToBackend) + // and the span below wraps the driver's own glBeginTransformFeedback/glEndTransformFeedback. + // + // The driver-side Begin is deferred from the frontend's glBeginTransformFeedback to + // the first draw of the span: ES requires the capturing program to be current and + // the capture buffers bound when Begin is issued, and both of those only become true + // once PrepareForDraw has run. A span that never draws therefore never touches the + // driver at all, which is also what the GL semantics amount to. + // + // Transform feedback objects (ARB_transform_feedback2) are ES 3.0 core, so each + // frontend object gets one of the driver's: a paused span lives inside the ES object, + // which is the only way several of them can be paused at once - and the only reason + // the default object alone would not do. + + namespace XfbImpl { + struct XfbCaptureTarget { + SharedPtr buffer; + Uint backendId = 0; + SizeT start = 0; + SizeT end = 0; + // WHICH capture buffer of the program this is. The list is COMPACTED - a + // capture buffer with no bound buffer object contributes no entry - so the + // position in the vector is not the program's buffer index, and everything + // that asks the program about a target (its stride, which varyings land in + // it) has to ask about this index instead. A capture list beginning with + // gl_NextBuffer is the shape that makes them differ: buffer 0 has stride 0 + // and nothing bound, so target 0 describes buffer 1. + SizeT bufferIndex = 0; + MG_Pipe::MGPipeHandle handle = MG_Pipe::kMGPipeNullHandle; + }; + + struct XfbProgramSource { + SharedPtr Frontend; + SharedPtr Archive; + explicit operator Bool() const { return Frontend || Archive; } + const XfbProgramSource* operator->() const { return this; } + SizeT GetTransformFeedbackBufferCount() const { + return Archive ? Archive->Link.xfbStrides.size() : Frontend->GetTransformFeedbackBufferCount(); + } + Uint32 GetTransformFeedbackStride(Uint32 index) const { + return Archive ? (index < Archive->Link.xfbStrides.size() ? Archive->Link.xfbStrides[index] : 0) + : Frontend->GetTransformFeedbackStride(index); + } + Bool NeedsScatteredTransformFeedbackCapture() const { + return Archive ? Archive->Link.xfbNeedsScatteredCapture + : Frontend->NeedsScatteredTransformFeedbackCapture(); + } + Uint32 GetTransformFeedbackPackedStride() const { + return Archive ? Archive->Link.xfbPackedStride : Frontend->GetTransformFeedbackPackedStride(); + } + GLenum GetTransformFeedbackBufferMode() const { + return Archive ? Archive->Link.xfbBufferMode : Frontend->GetTransformFeedbackBufferMode(); + } + const auto& GetTransformFeedbackVaryings() const { + return Archive ? Archive->Link.xfbVaryings : Frontend->GetTransformFeedbackVaryings(); + } + }; + // Per frontend transform feedback object. The default object (name 0) maps to + // the driver's default object (id 0) and is always present. + struct XfbObjectState { + GLuint esId = 0; + Bool pending = false; // frontend Begin seen, driver capture not started yet + Bool started = false; // driver capture running + Bool paused = false; // frontend Pause seen and not yet resumed + GLenum primitiveMode = GL_POINTS; + Vector targets; + // Set for a layout ES cannot express (gl_SkipComponents / gl_NextBuffer): + // the driver captures gap-free records into the scratch buffer below and + // End scatters them into `targets`. + Bool scattered = false; + SharedPtr scatterProgram; + SharedPtr scatterArchive; + // Paused spans in different served contexts may coexist. Their captured + // bytes must not share the monolith's one reusable scratch buffer. + GLuint serverScatterBuffer = 0; + SizeT serverScatterSize = 0; + SizeT scatterCapacityVertices = 0; + }; + + extern GLuint g_scatterBufferId; + + extern SizeT g_scatterBufferSize; + + extern UnorderedMap g_xfbObjects; + + extern GLuint g_currentXfbName; + + extern XfbObjectState* g_currentXfbState; + + struct RoleXfbState { + Uint NativeGeneration = 0; + // Server keys are record-supplied, never-reused XFB lifetime ids. + // The monolith role keeps its existing GL-name semantics separately. + UnorderedMap Objects; + Uint64 Current = 0; + XfbObjectState* Cached = nullptr; + GLuint ScatterBuffer = 0; + SizeT ScatterSize = 0; + Bool NeedsBind = true; + }; + + extern Array g_roleXfbState; + + XfbObjectState& CurrentXfb(); + + // EVERY way this path can lose a capture used to be silent: three unlogged early + // returns before the driver Begin, an unchecked glBeginTransformFeedback, and two + // `continue`s in the readback. The application sees a buffer that kept its + // pre-draw bytes, GL_NO_ERROR, and GL_LINK_STATUS true - which is how one defect + // reached ~320 conformance bodies across four families before anyone could say + // which of the branches fired. Nothing below changes what MobileGL DOES on a + // healthy capture; it only makes a lost one name itself in /sdcard/MG/latest.log. + // + // MGLOG_E_ONCE (not _D) on purpose: these have to be readable in an INFO-level + // artifact, the same reason the backend link failure at Managers.cpp is MGLOG_E. + constexpr Int kMaxDrainedXfbErrors = 32; + + namespace { + // The ES error raised by the call just issued, GL_NO_ERROR if it succeeded. Drains + // the rest of the queue so the next probe cannot read this one as its own. + GLenum TakeXfbDriverError() { + const GLenum first = g_GLESFuncs.glGetError(); + if (first == GL_NO_ERROR) return GL_NO_ERROR; + for (Int i = 0; i < kMaxDrainedXfbErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) { + } + return first; + } + } // namespace + + void OnBackendContextDestroyed(); + } // namespace XfbImpl + + namespace TextureImpl { + // The frontend texture-state keys the per-draw texture stages + // (SyncNeccessaryTextures, then BindCurrentTextures) both consume. Captured + // ONCE per draw/dispatch preparation and passed to both: nothing between the + // two stages can move any of these - only frontend GL entry points mutate + // them, and none run inside a backend preparation (the FBO/program syncs in + // between only read frontend state). The no-arg wrappers keep capturing at + // the call for every non-draw call site (Clear, readbacks). + struct DrawTextureSyncKeys { + Uint64 contextId = 0; + Uint64 samplingGeneration = 0; + Uint64 unitBindingsEpoch = 0; + Int maxTouchedUnit = -1; + }; + + DrawTextureSyncKeys CaptureDrawTextureSyncKeys(); + + void SyncNeccessaryTextures(const DrawTextureSyncKeys& keys); + + void SyncNeccessaryTextures(); + + extern Uint g_imageUnitHighWaterMark; + + void SyncImageTextureBinding(const MG_Pipe::MGPImageView& view); + + void MarkWritableImageBufferTexturesGpuWritten(); + + void SyncImageTextureBindings(); + } // namespace TextureImpl + + namespace FramebufferImpl { + // P4a e1 (D-C2, D-B3). The framebuffer family's handle arm. + // + // THE FOUR-PART MEMO BECOMES TWO VALUES, and the halves are not symmetric. + // g_fboSyncedSlotVersions / ObjectVersions / Objects are three answers to one question + // - "has the state the client would describe moved?" - asked of the frontend from the + // backend. The applier answers it with one number: FramebufferSerial is bumped by the + // applier before the backend is told anything, on every set_framebuffer_state it + // accepts, and a record the client's ContentHash suppressor withheld provably describes + // state that did not move. So one Uint64 compare replaces the three. + // + // The FOURTH stays, and must: g_attachmentBackendIdGeneration answers "did *I* re-mint + // a driver texture id", which no client-side version can answer, and dropping it is the + // shape of bug commit d7655247 fixed on the buffer side (D-B3, D-O). The ES context + // generation joins it for the same reason the legacy arm's other memos carry it - the + // driver FBO names die with the context. + // Kept PER TARGET, like the arrays it replaces, and for the one reason that survives + // the collapse: ForceBindCurrentFBO syncs a single target and has to be able to say so. + // What collapses is the three-value "has the state moved" question into one serial. + struct SyncedFramebufferSerialMemo { + Uint64 serial = 0; // the FramebufferSerial this target was last synced at + Uint64 backendIdGeneration = 0; + Uint contextGeneration = 0; + Bool valid = false; + }; + + extern Array g_fboSyncedSerials; + + extern Bool g_fboRecordsTrusted; + + // A9 (the review's MAJOR-4, and it is PACKAGE D's fix, not this file's): every call + // site of this function in DirectGLES.cpp is paired with + // FramebufferImpl::InvalidateFramebufferBindingCache() - all six, verified - but that + // function lives in Managers.cpp (D's file for the whole phase) and has three further + // callers this package cannot reach: MG_Test/SanityTest.cpp's ScopedStateGuardMocks:: + // ResetShadows and ScopedBackendTwinMocks' ctor and dtor. Those three clear the + // pre-handle trio and leave g_fboSyncedSerials and g_fboRecordsTrusted stale across a + // GLES function-table swap - harmless while every arm declines, but on the integrated + // tree with bit 9 set a fixture can be entered with the records trusted and a memo + // claiming a target is synced, so BindCurrentFBO would bind from a record while the + // mock table is installed. THE FIX IS TO CALL THIS FROM INSIDE + // InvalidateFramebufferBindingCache so no caller can forget; it is one line in D's + // file and it is on D's rework list. + static void InvalidateFramebufferHandleArmMemos() { + for (auto& memo : g_fboSyncedSerials) memo.valid = false; + g_fboRecordsTrusted = false; + } + + void SyncCurrentFBO(); + } // namespace FramebufferImpl + + namespace RenderStateImpl { + extern RenderStateParameters g_syncedRenderStateParameters; + + extern Uint32 g_syncedColorMaskAlphaWidenMask; + + void InvalidateSyncedRenderState(); + + Bool DriverScissorTestEnabled(); + + void SyncRenderState(Bool forColorClear); + } // namespace RenderStateImpl + + namespace PrgramImpl { + extern BackendProgramObjectImpl* g_currentDrawBackendProgram; + + extern MG_Pipe::MGPipeHandle g_currentDrawProgramHandle; + + extern Bool g_broadcastMemoValid; + + extern Bool g_broadcastMemoHandleValid; + + const MG_Pipe::MGPipeShaderCsoRecord* FindShaderCsoRecord(MG_Pipe::MGPipeHandle handle); + + const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecordForHandle(MG_Pipe::MGPipeHandle handle, + Uint uboSize); + + void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw); + } // namespace PrgramImpl + + // Defined with the surface registry, below. + void FlushAheadOfWindowBufferUse(); + + void BindCurrentFBO(FramebufferTarget target); + + void BindCurrentProgramWithResources(const SharedPtr& currentProgram, + const TextureImpl::DrawTextureSyncKeys& keys, + MG_Pipe::MGPipeHandle programCso); + void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, + const SharedPtr& currentProgram); + + void PrepareForDraw(DrawSyncFlags syncBit); + + Bool ResolveAndBindUnitTextures(const SharedPtr& currentProgram, + Int maxTouchedUnit); + + extern Uint64 g_unitSamplerWalkContextId; + + extern Uint64 g_unitSamplerWalkEpoch; + + extern Int g_unitSamplerWalkMaxUnit; + + extern Uint g_unitSamplerWalkContextGeneration; + + extern Bool g_unitSamplerWalkValid; + + void BindCurrentUnitSamplers(const TextureImpl::DrawTextureSyncKeys& keys); + + // Memo of the resolved per-unit TEXTURE bindings, so a steady-state draw loop stops + // re-deriving an answer nothing has invalidated (a Minecraft frame issues thousands of + // draws that touch none of the inputs below, and the resolution walks every binding slot + // of every touched unit three times). + // + // ResolveAndBindUnitTextures is a pure function of: + // * the GLContext identity - a never-reused id, since the counters below restart at 0 in + // a new context and a recreated one can land on the old heap address; + // * the per-unit bindings - which texture object every slot holds and which sampler + // object each unit carries, keyed as TextureImpl::CurrentUnitBindingsEpoch rather + // than as the texture bind generation: the generation also bumps on redundant + // re-binds (26.2 re-binds the unit's own sampler around every texture-unit switch), + // which would force a full re-resolution per draw for an answer that cannot have + // moved. The one input the old generation key covered that the epoch does not - a + // default texture gaining or losing an image with no bind moving - bumps the + // sampling-resolution generation below (SetInternalFormat -> BumpShapeVersion); + // * the touched-unit high-water mark (units above it have provably-empty slots); + // * the program that arbitrates two real textures aliased onto one native target - + // identity, plus the lifetime id because a freed program can be replaced at the same + // address, plus the backend-state version which moves on every relink and on every + // sampler-uniform unit assignment, plus the link status; + // * the sampling-resolution generation - any texture shape change or any sampler + // parameter change, i.e. everything mipmap-completeness is computed from, and + // completeness is what decides whether a texture is bound at all; + // * the ES context generation, because the backend texture ids and the driver's own + // binding state die with the context. + // + // A matching key only says the ANSWER is unchanged; replaying it as "do nothing" also + // requires the bindings it established to still be on the driver. Rather than enumerate + // every writer, the memo keeps the binding shadow it left and compares it: that shadow is + // already the authority every redundant-bind filter in this backend trusts, and every path + // that puts a texture on a unit behind this function's back maintains it - the upload + // path's scratch bind on the temp unit (BackendTextureObject::Bind out of + // SyncMipmapsToBackend), CopyTexSubImage2D and GenerateMipmap binding on the active unit, + // the glBindTextures fast path, and the self-scrub a BackendTextureObject performs when it + // is destroyed or respecified. + struct ResolvedTextureBindingMemo { + Bool valid = false; + Uint64 glContextId = 0; + Int maxTouchedUnit = -1; + Uint64 unitBindingsEpoch = 0; + Uint64 samplingResolutionGeneration = 0; + const void* program = nullptr; + Uint64 programLifetimeId = 0; + Uint32 programBackendStateVersion = 0; + Bool programLinked = false; + Uint contextGeneration = 0; + // P5e (pg) -> tx2, ruling 6 / ID-86: THE PROGRAM HALF OF THIS KEY HAS TO GO BY HANDLE, + // and this struct is tx2's line, so pg states the shape rather than writing it. The four + // program fields above are read off the frontend ProgramObject on EVERY DRAW, memo hit + // included (kimi row 100, the worst row of the program family). What replaces them on + // the handle arm is {DrawProgram.Slot, DrawProgram.Gen, ShaderCso record Serial, + // BindingsSerial}: Serial moves on every applied create_shader_state (how a relink + // travels) and BindingsSerial on every applied set_program_bindings (how a glUniform1i + // on a sampler travels). Both are needed - ruling 6's "both" - because a relink with + // UNCHANGED texture resolution still hands the twin new unit assignments. + // P5e (tx2), CONTRACT-P5E §5.3 / ruling 6 (ID-86): THE KEY IS BOTH HALVES. + // + // S2 argued SamplerViewsSerial alone - it already mixes the program-resolved opaque units, + // so a resolution that moved moves it - and S4 argued the program's rows. The integrator + // ruled BOTH, for the case neither covers alone: a RELINK whose resolution happens to be + // unchanged still needs the twin's new unit assignments, and SamplerViewsSerial does not + // move for it (the emitter's content hash is over the resolved set, which did not change). + // + // The frontend program rows above (pointer, lifetime id, backend-state version, link + // status) stay for the monolith arm and are simply not read on this one. + MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle; + Uint64 shaderCsoSerial = 0; + Uint64 bindingsSerial = 0; + Uint64 samplerViewsSerial = 0; + Uint64 contextSerial = 0; + decltype(TextureImpl::g_boundTexturesCache) boundTextures{}; + }; + + extern Array g_resolvedTextureBindingMemos; + + extern Uint32 g_resolvedTextureBindingMemoCursor; + + void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, + const SharedPtr& currentProgram); + + void BindCurrentTextures(); + + void BindCurrentProgramWithResources(const SharedPtr& currentProgram, + const TextureImpl::DrawTextureSyncKeys& keys, + MG_Pipe::MGPipeHandle programCso); + + PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram(); + + void SetCurrentBaseInstance(Uint32 baseInstance); + + void SetCurrentDrawID(Uint32 drawId); + + void SetCurrentBaseVertex(Int32 baseVertex); + + Bool CurrentProgramReadsDrawID(); + + Bool CurrentProgramReadsBaseVertex(); + + void OnRestartSubstitutionContextDestroyed(); + + void DrainDriverErrors(const char* site); + + static void DrainBlitErrors() { DrainDriverErrors("BlitFramebuffer"); } + + // --------------------------------------------------------------------------------- + // Shared guard for the emulation passes that have to DRAW to get their work done: the + // single-sample -> multisample blit replicate below, and the depth/stencil readback + // emulation further down. Both borrow the application's ES context for a full-screen + // pass, so everything they disturb is captured here and put back on the way out - the + // sync layer's shadow of the driver state has to stay true, and one leaked binding + // regresses every draw that follows. + // + // Three members of that set are not obvious: + // + // - The borrowed texture unit. Both passes bind their scratch texture to + // TextureImpl::TempTextureUnit, and the unit's binding is only restored correctly by + // asking TextureImpl::g_boundTexturesCache what is supposed to be there: the raw + // glBindTexture the passes issue never moves that cache, so restoring a *queried* + // id leaves the driver and the cache disagreeing and the per-draw binding memo + // false-skips the re-bind - the borrowed-slot failure mode that showed up as + // process-wide glyph death under Iris. Reading GL_TEXTURE_BINDING_2D was doubly + // wrong here because the replicate path may already have switched units and bound + // its own scratch texture by the time it asked. + // + // - The sampler object on that unit. It would override the scratch texture's own + // filter and compare parameters. Unbinding through SamplerImpl::UnbindSampler moves + // the sampler cache, which is exactly what re-opens BindCurrentUnitSamplers' memo, + // so the application's sampler comes back on the next draw with no explicit restore. + // + // - An active transform feedback capture. GL rejects a draw issued with a program + // other than the one that began the capture, and would otherwise append the + // emulation's vertices to the application's buffers. + // + // - The application's clip control. The passes place their triangle in the default + // lower-left, [-1, 1] clip space; an upper-left origin would mirror it vertically. + class ScopedEmulationDrawState { + public: + ScopedEmulationDrawState() { + g_GLESFuncs.glGetIntegerv(GL_CURRENT_PROGRAM, &m_program); + g_GLESFuncs.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &m_vertexArray); + g_GLESFuncs.glGetIntegerv(GL_VIEWPORT, m_viewport); + g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, m_scissorBox); + g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, m_colorMask); + g_GLESFuncs.glGetIntegerv(GL_DEPTH_FUNC, &m_depthFunc); + g_GLESFuncs.glGetBooleanv(GL_DEPTH_WRITEMASK, &m_depthMask); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_FUNC, &m_stencilFunc[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FUNC, &m_stencilFunc[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_REF, &m_stencilRef[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_REF, &m_stencilRef[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_VALUE_MASK, &m_stencilValueMask[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &m_stencilValueMask[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_WRITEMASK, &m_stencilWriteMask[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &m_stencilWriteMask[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_FAIL, &m_stencilFail[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FAIL, &m_stencilFail[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &m_stencilDepthFail[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &m_stencilDepthFail[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &m_stencilPass[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &m_stencilPass[1]); + if (g_GLESCapabilities.SupportsClipDistance) { + m_capabilityCount = kBaseCapabilityCount + kClipDistanceCapabilityCount; + } + for (Uint i = 0; i < m_capabilityCount; ++i) { + m_capabilities[i].enabled = g_GLESFuncs.glIsEnabled(m_capabilities[i].cap); + } + // GL_SAMPLE_MASK is ES 3.1; on an older driver the query above just raised + // GL_INVALID_ENUM and answered GL_FALSE, which is also the right thing to + // restore. Drop the flag so it is not misattributed to the emulation's own work. + DrainBlitErrors(); + + if (MG_Pipe::gPipeInputs->IsTransformFeedbackActive() && + !MG_Pipe::gPipeInputs->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) { + g_GLESFuncs.glPauseTransformFeedback(); + m_pausedTransformFeedback = true; + DrainBlitErrors(); + } + + if (g_GLESCapabilities.SupportsClipControl) { + g_GLESFuncs.glGetIntegerv(GL_CLIP_ORIGIN, &m_clipOrigin); + g_GLESFuncs.glGetIntegerv(GL_CLIP_DEPTH_MODE, &m_clipDepthMode); + g_GLESFuncs.glClipControlEXT(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE); + } + + m_activeTextureUnit = TextureImpl::g_activeTextureUnit; + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); + SamplerImpl::UnbindSampler(TextureImpl::TempTextureUnit); + + // The neutral baseline every emulation pass wants: nothing culled, nothing + // clipped, nothing tested, no coverage games, and colour writes open. Callers + // turn back on only what they need (the replicate pass wants the depth and + // stencil tests, and masks colour off because it writes neither). + for (Uint i = 0; i < m_capabilityCount; ++i) { + g_GLESFuncs.glDisable(m_capabilities[i].cap); + } + g_GLESFuncs.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + g_GLESFuncs.glDepthMask(GL_FALSE); + DrainBlitErrors(); + } + + ~ScopedEmulationDrawState() { + g_GLESFuncs.glUseProgram(static_cast(m_program)); + // Put back whatever the binding cache says lives on the borrowed unit, not what + // the driver happened to hold: see the class comment. + auto* cachedBound = + TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(TextureTarget::Texture2D)]; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0); + TextureImpl::ActivateTextureUnit(m_activeTextureUnit); + VertexArrayImpl::BindBackendVAOId(static_cast(m_vertexArray)); + g_GLESFuncs.glViewport(m_viewport[0], m_viewport[1], m_viewport[2], m_viewport[3]); + g_GLESFuncs.glScissor(m_scissorBox[0], m_scissorBox[1], m_scissorBox[2], m_scissorBox[3]); + g_GLESFuncs.glColorMask(m_colorMask[0], m_colorMask[1], m_colorMask[2], m_colorMask[3]); + g_GLESFuncs.glDepthFunc(static_cast(m_depthFunc)); + g_GLESFuncs.glDepthMask(m_depthMask); + const GLenum faces[2] = {GL_FRONT, GL_BACK}; + for (SizeT face = 0; face < 2; ++face) { + g_GLESFuncs.glStencilFuncSeparate(faces[face], static_cast(m_stencilFunc[face]), + m_stencilRef[face], + static_cast(m_stencilValueMask[face])); + g_GLESFuncs.glStencilOpSeparate(faces[face], static_cast(m_stencilFail[face]), + static_cast(m_stencilDepthFail[face]), + static_cast(m_stencilPass[face])); + g_GLESFuncs.glStencilMaskSeparate(faces[face], static_cast(m_stencilWriteMask[face])); + } + for (Uint i = 0; i < m_capabilityCount; ++i) { + if (m_capabilities[i].enabled) { + g_GLESFuncs.glEnable(m_capabilities[i].cap); + } else { + g_GLESFuncs.glDisable(m_capabilities[i].cap); + } + } + // The per-draw-buffer colour masks are not covered by the non-indexed + // glColorMask above. Restore what the SYNC actually pushed, not the raw + // application masks: a widened attachment's alpha write is forced off by + // SyncRenderState and memoized in g_syncedColorMaskAlphaWidenMask, and the + // next sync early-outs on an unchanged version - restoring the undoctored + // mask here would leave alpha writes enabled on the widened buffer with + // nothing left to repair it. Same three-way pointer fallback as + // SyncRenderState's push: gating on the core name alone left EXT/OES-only + // devices holding buffer 0's mask broadcast across every buffer. + const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski + : g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT + : g_GLESFuncs.glColorMaskiOES; + if (colorMaskiFn) { + for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { + BoolVec4 colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; + if (index < 32 && (RenderStateImpl::g_syncedColorMaskAlphaWidenMask & (1u << index)) != 0) { + colorMask.w() = false; + } + colorMaskiFn(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE, + colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE); + } + } + if (g_GLESCapabilities.SupportsClipControl) { + g_GLESFuncs.glClipControlEXT(static_cast(m_clipOrigin), static_cast(m_clipDepthMode)); + } + if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) { + g_GLESFuncs.glResumeTransformFeedback(); + } + DrainBlitErrors(); + } + + ScopedEmulationDrawState(const ScopedEmulationDrawState&) = delete; + ScopedEmulationDrawState& operator=(const ScopedEmulationDrawState&) = delete; + + private: + struct CapabilityState { + GLenum cap; + GLboolean enabled; + }; + + GLint m_program = 0; + GLint m_vertexArray = 0; + GLint m_viewport[4] = {0, 0, 0, 0}; + GLint m_scissorBox[4] = {0, 0, 0, 0}; + GLboolean m_colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; + GLint m_depthFunc = GL_LESS; + GLboolean m_depthMask = GL_TRUE; + GLint m_stencilFunc[2] = {GL_ALWAYS, GL_ALWAYS}; + GLint m_stencilRef[2] = {0, 0}; + GLint m_stencilValueMask[2] = {~0, ~0}; + GLint m_stencilWriteMask[2] = {~0, ~0}; + GLint m_stencilFail[2] = {GL_KEEP, GL_KEEP}; + GLint m_stencilDepthFail[2] = {GL_KEEP, GL_KEEP}; + GLint m_stencilPass[2] = {GL_KEEP, GL_KEEP}; + Uint m_activeTextureUnit = 0; + Bool m_pausedTransformFeedback = false; + GLint m_clipOrigin = GL_LOWER_LEFT; + GLint m_clipDepthMode = GL_NEGATIVE_ONE_TO_ONE; + // The eight GL_CLIP_DISTANCE0_EXT..7_EXT entries are last so that a driver without + // GL_EXT_clip_cull_distance can be served by shortening the count instead of asking + // it about tokens it does not know. They belong here at all because an emulation pass + // draws its full-screen triangle with its OWN program, which writes no gl_ClipDistance: + // leaving the app's enables on would clip that triangle by undefined distances. + static constexpr Uint kBaseCapabilityCount = 10; + static constexpr Uint kClipDistanceCapabilityCount = 8; + Uint m_capabilityCount = kBaseCapabilityCount; + CapabilityState m_capabilities[kBaseCapabilityCount + kClipDistanceCapabilityCount] = { + {GL_SCISSOR_TEST, GL_FALSE}, {GL_DEPTH_TEST, GL_FALSE}, + {GL_STENCIL_TEST, GL_FALSE}, {GL_CULL_FACE, GL_FALSE}, + {GL_BLEND, GL_FALSE}, {GL_RASTERIZER_DISCARD, GL_FALSE}, + {GL_POLYGON_OFFSET_FILL, GL_FALSE}, {GL_SAMPLE_ALPHA_TO_COVERAGE, GL_FALSE}, + {GL_SAMPLE_COVERAGE, GL_FALSE}, {GL_SAMPLE_MASK, GL_FALSE}, + {0x3000, GL_FALSE}, {0x3001, GL_FALSE}, + {0x3002, GL_FALSE}, {0x3003, GL_FALSE}, + {0x3004, GL_FALSE}, {0x3005, GL_FALSE}, + {0x3006, GL_FALSE}, {0x3007, GL_FALSE}, + }; + }; + + // --------------------------------------------------------------------------------- + // Single-sample -> multisample blit ("replicate") + // + // Desktop GL replicates the source sample into every destination sample when the read + // framebuffer is single-sampled and the draw framebuffer is not. ES forbids the whole + // call ("INVALID_OPERATION if SAMPLE_BUFFERS for the draw framebuffer is greater than + // zero"), so the blit silently did nothing - KHR-GL3x.packed_depth_stencil.blit's + // second loop then read a destination that still held its clear values. + // + // Emulated by drawing a full-screen triangle into the multisample framebuffer: every + // pixel is fully covered, so every sample of it receives the same value, which is + // exactly what the replicate rule asks for. Depth comes from gl_FragDepth; stencil has + // no shader output on ES, so it is written one bit plane at a time with REPLACE and a + // discard for the pixels whose source bit is clear. + // Defined further down with the other small GL helpers; the attachment-format probe below + // needs it to tell a rejected bind apart from a successful one. + static void ClearGLErrors(); + + namespace ReplicateBlitImpl { + GLuint BuildProgram(const char* fragmentSource); + + GLenum QueryAttachmentSizedFormat(GLenum attachment); + + static Bool FormatHasDepth(GLenum internalFormat) { + return internalFormat == GL_DEPTH_COMPONENT16 || internalFormat == GL_DEPTH_COMPONENT24 || + internalFormat == GL_DEPTH_COMPONENT32F || internalFormat == GL_DEPTH24_STENCIL8 || + internalFormat == GL_DEPTH32F_STENCIL8; + } + + static Bool FormatHasStencil(GLenum internalFormat) { + return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 || + internalFormat == GL_STENCIL_INDEX8; + } + + static GLenum ScratchAttachmentFor(GLenum internalFormat) { + if (FormatHasDepth(internalFormat) && FormatHasStencil(internalFormat)) { + return GL_DEPTH_STENCIL_ATTACHMENT; + } + return FormatHasDepth(internalFormat) ? GL_DEPTH_ATTACHMENT : GL_STENCIL_ATTACHMENT; + } + } // namespace ReplicateBlitImpl + + // ---- Scoped driver-state guards for the readback/copy/blit emulation paths -------------------- + // These paths borrow driver state (FBO bindings, scratch-FBO attachments, PACK + // pixel-store, the pack-PBO binding, scissor) that the app never asked to + // change; every mutation is scoped by an RAII guard so no exit path can leak + // it. Saves/restores go through the DirectGLES driver-state shadows + // (Managers.h) instead of glGetIntegerv - no driver round-trips, and redundant + // rebinds/resets no-op. + + // Saves the driver READ/DRAW framebuffer binding(s) and restores them on exit. + // Per-instance state: nesting-safe. + class ScopedFramebufferBinding { + public: + ScopedFramebufferBinding(Bool saveRead, Bool saveDraw) : m_saveRead(saveRead), m_saveDraw(saveDraw) { + if (m_saveRead) m_prevRead = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Read); + if (m_saveDraw) m_prevDraw = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Draw); + } + ~ScopedFramebufferBinding() { + if (m_saveRead) FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, m_prevRead); + if (m_saveDraw) FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_prevDraw); + } + ScopedFramebufferBinding(const ScopedFramebufferBinding&) = delete; + ScopedFramebufferBinding& operator=(const ScopedFramebufferBinding&) = delete; + + private: + const Bool m_saveRead; + const Bool m_saveDraw; + GLuint m_prevRead = 0; + GLuint m_prevDraw = 0; + }; + + // Applies a PACK pixel-store configuration and restores the previous one on exit. + class ScopedPackState { + public: + explicit ScopedPackState(const PixelStoreImpl::PackState& desired) + : m_prev(PixelStoreImpl::CurrentPackState()) { + PixelStoreImpl::ApplyPackState(desired); + } + ~ScopedPackState() { PixelStoreImpl::ApplyPackState(m_prev); } + ScopedPackState(const ScopedPackState&) = delete; + ScopedPackState& operator=(const ScopedPackState&) = delete; + + private: + const PixelStoreImpl::PackState m_prev; + }; + + // Binds a pixel PACK buffer (0 = client memory) for the scope and returns the + // binding to the resting 0 state on exit, so no later readback can accidentally + // capture into a stale PBO. + class ScopedPixelPackBuffer { + public: + explicit ScopedPixelPackBuffer(GLuint id) { BufferImpl::BindPixelPackBufferId(id); } + ~ScopedPixelPackBuffer() { BufferImpl::BindPixelPackBufferId(0); } + ScopedPixelPackBuffer(const ScopedPixelPackBuffer&) = delete; + ScopedPixelPackBuffer& operator=(const ScopedPixelPackBuffer&) = delete; + }; + + // Force-disables GL_SCISSOR_TEST for the scope (emulation blits and clears are + // scissored; readback copies must not be clipped by app scissor state) and + // restores the app state on exit. + // + // WHAT THE DRIVER HAS, NOT WHAT THE SHADOW LAST SAID. The render-state shadow is one per + // process and is synced at draws; a session that presents or reads back without drawing + // since another session's context synced (a frame that redraws nothing, a client whose + // turn came after the compositor's) found the other context's bit there. Its own scissor, + // still on from its last draw, then clipped the shared-image copy to a box - flipped into + // the wrong rows, too - and the image kept the frame it held before. + class ScopedScissorDisable { + public: + ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::DriverScissorTestEnabled()) { + if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + } + ~ScopedScissorDisable() { + if (m_wasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); + } + ScopedScissorDisable(const ScopedScissorDisable&) = delete; + ScopedScissorDisable& operator=(const ScopedScissorDisable&) = delete; + + private: + const Bool m_wasEnabled; + }; + + // Binds the shared scratch FBO at READ (isRead) or DRAW for one temp operation, + // restoring the previous binding on exit. Attachments are managed through the + // ScratchFBOImpl attachment shadow by the caller (see Framebuffer()). + class TempFBOBinder { + public: + explicit TempFBOBinder(Bool isRead) + : m_binding(/*saveRead=*/isRead, /*saveDraw=*/!isRead), + m_target(isRead ? GL_READ_FRAMEBUFFER : GL_DRAW_FRAMEBUFFER) { + FramebufferImpl::BindFramebufferId(m_target, ScratchFBOImpl::EnsureId(Framebuffer())); + } + ScratchFBOImpl::ScratchFramebuffer& Framebuffer() const { return ScratchFBOImpl::TempFramebuffer(); } + GLenum Target() const { return m_target; } + + private: + ScopedFramebufferBinding m_binding; + const GLenum m_target; + }; + + static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); } + + // DirectGLESReadback.cpp defines it: E's extent-scoped stand-in for a + // driver that refuses to read a colour level back, asked here too so a host reaches the store + // route below (P8-SE). + Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent); + using FramebufferImpl::SubstituteWidenedClearAlpha; + + // ---- Client-format readback conversion --------------------------------------------------------------------- + // ES 3.x glReadPixels only guarantees GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/GL_(UNSIGNED_)INT, + // GL_RGBA/GL_FLOAT for float buffers plus one implementation-defined pair, while desktop GL clients read + // back narrower layouts (RED, RG, RGB, BGR, byte-order packed types, ...). For those we read a guaranteed + // wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU, + // honoring the client-side PACK pixel-store parameters. The pure repacking helpers live in + // ClientReadback (MG_Backend/ClientReadback.cpp) so unit tests can assert the exact packed words. + + using ClientReadback::GetReadbackChannelMapping; + using ClientReadback::GetReadbackComponentSize; + using ClientReadback::GetReadbackDstPixelSize; + using ClientReadback::ReadbackChannelMapping; + + extern GLuint g_packedWordScratchTextureId; + + extern GLsizei g_packedWordScratchWidth; + + extern GLsizei g_packedWordScratchHeight; + + Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, + TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent, + Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector& bytes); + + Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent); + + extern EGLDisplay g_Display; + + extern std::atomic g_displayTerminations; + + struct NativeContextTuple { + Uint64 SessionKey = 0; + Uint64 ContextToken = 0; + Uint64 ShareGroupToken = 0; + EGLContext Context = EGL_NO_CONTEXT; + // Context's lifetime serial (CurrentNativeContextSerial); a new one per eglCreateContext. + Uint64 Serial = 0; + EGLConfig Config = nullptr; + // The session surface this context draws to / reads from, as last made current for it. + EGLSurface Draw = EGL_NO_SURFACE; + EGLSurface Read = EGL_NO_SURFACE; + }; + + struct NativeSessionState { + Uint64 SessionKey = 0; + // Every native surface this session created, so its teardown destroys its own and only its own. + // The window is recorded with each entry: a stale surface still bound to a window that a + // LATER session wants (a crashed predecessor's server-owned window) is found and destroyed + // by InitWindowSurface before the driver refuses the second eglCreateWindowSurface. + struct SurfaceEntry { + NativeWindowType Window; + EGLSurface Surface; + // Presents made on this native surface since it was created: a pbuffer's content is + // the last presented frame once there has been one (its buffer age is then 1). + Uint64 Presents = 0; + }; + Vector Surfaces; + // The surface the session currently draws to / reads from - what a bind to another context + // re-makes current, and what Present/eglSwapBuffers names. + EGLSurface Draw = EGL_NO_SURFACE; + EGLSurface Read = EGL_NO_SURFACE; + }; + + extern std::mutex g_nativeRegistryMutex; + + extern std::atomic g_nativeRegistryEpoch; + + extern std::atomic g_surfaceSizeEpoch; + static void NoteDrawSurfaceMoved() { g_surfaceSizeEpoch.fetch_add(1, std::memory_order_acq_rel); } + + Vector& NativeContexts(); + + Vector& NativeSessions(); + + extern std::atomic g_nativeContextKeyResolver; + + // A one-way override for a case that drives the registry with no session runtime installed: + // when `t_native.forcedValid` is set, the key is the forced pair instead of the probe's + // answer. The setter clears the active-tuple cache, which is keyed on the effective key. + // + // P14: ONE thread-local block for this file's per-thread native-context state rather than ten + // separate thread_locals. The library is built with emulated TLS (minSdk 26), where every + // thread_local access is a call; the per-draw "is my context current" check read seven of + // them. One block is one lookup per function. + struct NativeThreadState { + Bool forcedValid = false; + Uint64 forcedSession = 0; + Uint64 forcedToken = 0; + NativeContextTuple* activeTuple = nullptr; + Uint64 activeEpoch = 0; + Uint64 activeSession = 0; + Uint64 activeToken = 0; + EGLContext boundContext = EGL_NO_CONTEXT; + EGLSurface boundDraw = EGL_NO_SURFACE; + // QueryCurrentSurfaceSize's memo (P15): the last answer, good while the session, the + // registry epoch and the surface-size epoch all hold. + Bool sizeMemoValid = false; + Uint64 sizeMemoSession = 0, sizeMemoToken = 0, sizeMemoRegistryEpoch = 0, sizeMemoSizeEpoch = 0; + Int sizeMemoWidth = 0, sizeMemoHeight = 0; + }; + + extern thread_local NativeThreadState t_native; + + static void ResolveNativeKey(Uint64& outSession, Uint64& outToken) { + if (t_native.forcedValid) { + outSession = t_native.forcedSession; + outToken = t_native.forcedToken; + return; + } + outSession = 0; + outToken = 0; + const NativeContextKeyResolver resolver = g_nativeContextKeyResolver.load(std::memory_order_acquire); + if (resolver != nullptr) (void)resolver(&outSession, &outToken); + } + + static NativeSessionState& AcquireNativeSession(Uint64 sessionKey) { + for (auto* session : NativeSessions()) { + if (session->SessionKey == sessionKey) return *session; + } + auto* created = new NativeSessionState{}; + created->SessionKey = sessionKey; + NativeSessions().push_back(created); + g_nativeRegistryEpoch.fetch_add(1, std::memory_order_acq_rel); + return *created; + } + + static NativeSessionState& ActiveNativeSession() { + Uint64 session = 0; + Uint64 token = 0; + ResolveNativeKey(session, token); + const std::lock_guard lock(g_nativeRegistryMutex); + return AcquireNativeSession(session); + } + + NativeContextTuple& ActiveNativeContext(); + + Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight); + + EGLSurface CurrentContextDrawSurface(); + + extern thread_local EGLSurface t_ageAskedSurface; + + extern thread_local Bool t_windowBufferFlushIssued; + + namespace { + // The calling session's record of `surface`; the registry lock is held by the caller (another + // session's window bring-up may erase stale entries of this one). + NativeSessionState::SurfaceEntry* FindSessionSurfaceLocked(NativeSessionState& session, EGLSurface surface) { + for (auto& entry : session.Surfaces) { + if (entry.Surface == surface) return &entry; + } + return nullptr; + } + } // namespace + + void FlushAheadOfWindowBufferUse(); + + extern std::atomic g_backendContextOwnerThread; + + extern Uint g_syncContextGeneration; + + extern std::atomic g_currentFrameSerial; + + extern std::atomic g_completedFrameSerial; + // Deep enough for several sessions presenting in turn: a slot is only reused once its + // fence has signaled (InsertFrameFence waits for it rather than dropping it). + constexpr int kFrameFenceRingDepth = 32; + struct FrameFence { + GLsync sync = nullptr; + Uint contextGeneration = 0; + Uint64 serial = 0; + }; + + extern FrameFence g_frameFenceRing[kFrameFenceRingDepth]; + + extern std::atomic g_eglVerifiedFrameSerial; + + extern std::atomic g_eglVerifiedContextGeneration; + + Bool IsBackendContextCurrentOnThisThread(); + + GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout); + + void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout); + + Uint64 CurrentFrameSerial(); + +#if MOBILEGL_BUILD_DISAGGREGATED + Bool LatchIfDeviceLost(const char* site, Bool debugKnob); +#endif + +#if MOBILEGL_BUILD_DISAGGREGATED + namespace SharedImageImpl { +#if defined(__ANDROID__) + using MG_Util::BackendLoader::HasExtensionWord; +#endif + } // namespace SharedImageImpl +#endif +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.cpp new file mode 100644 index 00000000..33bd5aae --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.cpp @@ -0,0 +1,2067 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + // A readback on the server writes REPLY SCRATCH, so it has no pack buffer - and this + // function is how every pack-PBO lookup in this file says so under a transport. + // + // THE REASON IS NOT "THE CLIENT REFUSES PACK_BUFFER BEFORE EMISSION", which is what this + // comment used to claim and is not true of the tree (P3b/P4b espryt D1 slice 4). The client + // SERVICES the pack PBO itself, AFTER the reply lands: EmitTables.cpp uploads the reply's + // tight rows into the bound buffer with pbo->UploadSubData, one row at a time at the stride + // the application's GL_PACK_* state implies, so padding and untouched bytes survive. A + // refusal would have been visible as a GL error; what actually happens is an ordinary + // buffer upload the client performs on its own side. + // + // The server's half is ID-49's: read with NEUTRAL pack state into a tight w*h*bpp reply. + // Which is exactly why the server must see NO pack buffer here - not because the client + // forbade one, but because the client kept it. + static const SharedPtr& SplitReadbackPackBuffer() { + static const SharedPtr none; + return none; + } + + // The frontend's current PACK parameters, for readbacks the ES driver serves + // directly with the client's layout. + static PixelStoreImpl::PackState PackStateFromContext() { + const auto packParams = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false); + return {static_cast(packParams.Alignment), static_cast(packParams.RowLength), + static_cast(packParams.SkipRows), static_cast(packParams.SkipPixels)}; + } + + static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) { + const SizeT resolvedAlignment = static_cast(std::max(alignment, 1)); + return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); + } + + // Destination walk shared by the depth, stencil and packed depth-stencil readbacks. + // Each of those produces its rows tightly packed and has to land them in the caller's + // destination - client memory or the bound pixel-pack buffer - under the PACK + // pixel-store parameters. `fillRow` is handed the row index and a buffer of exactly one + // packed row to populate. Only real pixel rows are written, so PACK skip/row-length gap + // regions stay untouched. + template + static Bool StoreReadbackRowsToClient(GLsizei width, GLsizei height, SizeT dstPixelBytes, void* pixels, + const char* what, FillRow&& fillRow) { + const auto packParams = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false); + const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); + const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); + const SizeT dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; + const SizeT rowBytes = static_cast(width) * dstPixelBytes; + const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + rowBytes; + const auto& pixelPackBufferObject = + SplitReadbackPackBuffer(); + const SizeT pboOffset = reinterpret_cast(pixels); + if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { + MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what); + return false; + } + Vector rowBuf(rowBytes); + for (GLsizei row = 0; row < height; ++row) { + fillRow(row, rowBuf.data()); + const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend({rowBuf.data(), rowBytes}, pboOffset + rowOffset); + } else if (pixels != nullptr) { + Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), rowBytes); + } + } + if (pixelPackBufferObject) { + // WritebackFromBackend bumps change serials with no backend op; re-open the + // buffer draw-clean memos (once for the whole row loop). + BufferImpl::BumpBufferMutationEpoch(); + } + return true; + } + + // A normalized depth scaled into the full range of an unsigned integer of `maxValue` + // (GL 4.6 core table 18.2), without ever rounding past the top of that range. + static Uint32 NormalizedDepthToUnsigned(Float depth, Double maxValue) { + const Double clamped = std::min(std::max(static_cast(depth), 0.0), 1.0); + const Double scaled = clamped * maxValue + 0.5; + return static_cast(scaled >= maxValue ? maxValue : scaled); + } + + // --------------------------------------------------------------------------------- + // Depth / stencil readback by shader sampling + // + // Desktop GL reads depth and stencil back through glReadPixels; ES has no such call at + // all. GL_DEPTH_COMPONENT, GL_STENCIL_INDEX and GL_DEPTH_STENCIL are simply not + // accepted formats there, and the optional extensions that add them (GL_NV_read_depth, + // GL_NV_read_stencil, GL_NV_read_depth_stencil) are absent on both the Adreno device + // and llvmpipe's ES. Every native attempt therefore failed with a GL error and wrote + // NOTHING, so the caller kept whatever its buffer already held - which is how the CTS + // reports "expected DEPTH[0.25] but got DEPTH[0.2]": 0.2 is the poison value the test + // itself put there. + // + // What ES *can* do is sample a depth texture, so the emulation goes the long way round: + // + // 1. Stage. glBlitFramebuffer the requested rectangle out of the bound READ + // framebuffer into a scratch depth(-stencil) TEXTURE of the very same sized + // internal format. One staging copy serves every source kind uniformly - a + // texture attachment of any target/level/layer, a renderbuffer (not samplable at + // all), the default framebuffer, and a multisample attachment (the blit resolves + // it on the way). ES rejects a depth/stencil blit between differing formats, so + // the scratch has to match the source exactly; see DescribeReadDepthStencilSource. + // 2. Convert. Draw a full-screen triangle that samples the staged texture into a + // scratch R32UI colour target: depth as floatBitsToUint (bit-exact for every depth + // format, and an integer colour target needs no float-renderable extension), + // stencil through GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX. + // 3. Read. glReadPixels the colour target with GL_RGBA_INTEGER / GL_UNSIGNED_INT - + // the pair ES guarantees for an unsigned-integer attachment - and hand the values + // to the existing re-encoders, which already own the client-side (format, type) + // layout and the PACK pixel-store parameters. + // + // Both scratch images hold the rectangle at their own origin and the pass runs with a + // matching viewport, so GL's bottom-up row order survives untouched: scratch row 0 is + // source row `y`, which is exactly the first row glReadPixels(x, y, ...) owes the + // caller. No flip anywhere. + + namespace DepthStencilSamplingReadImpl { + static Uint s_contextGeneration = ~0u; + static NativeContextName s_colorFramebuffer; // per context, as are the vertex array and the + // slots' framebuffers below + static GLuint s_colorTexture = 0; + static GLsizei s_colorWidth = 0; + static GLsizei s_colorHeight = 0; + static NativeContextName s_vertexArray; + static GLuint s_depthProgram = 0; + static GLuint s_stencilProgram = 0; + static GLint s_depthUvTransform = -1; + static GLint s_stencilUvTransform = -1; + static Bool s_programsFailed = false; + + // One staging slot per aspect. A framebuffer is allowed to carry its depth and its + // stencil in two DIFFERENT objects with two different formats - the framebuffer_blit + // cases pair a DEPTH_COMPONENT* attachment with a separate STENCIL_INDEX8 one - and + // the two aspects are staged independently for exactly that reason. A single shared + // slot would also throw its immutable storage away and re-create it on every + // alternation between the two. + struct StageSlot { + // A framebuffer of its own, not a shared one. GL_DEPTH_STENCIL_ATTACHMENT sets + // the depth AND the stencil point, so a packed scratch staged for one aspect + // would leave the other aspect's point pointing at it; the next stage of that + // other aspect attaches its own (differently formatted) texture to its own point + // and the framebuffer is then incomplete - which reads as "no candidate format + // worked" and writes nothing at all. + GLuint texture = 0; + GLenum format = 0; + GLsizei width = 0; + GLsizei height = 0; + // The default framebuffer has no queryable internal format, so the one that turns + // out to be blit-compatible is remembered: it cannot change for the life of the + // context, and re-probing it on every read would cost a failed blit each time. + GLenum defaultFramebufferFormat = 0; + }; + static StageSlot s_slots[2]; // [0] depth, [1] stencil + // Each slot's own framebuffer (see StageSlot), one per native context. + static NativeContextName s_slotFramebuffers[2]; + static NativeContextName& SlotFramebuffer(const StageSlot& slot) { return s_slotFramebuffers[&slot - s_slots]; } + + // `precision highp int` is not decoration: ESSL 3.00 defaults integers to mediump in + // the fragment language, which is allowed to be 16 bits - it would saw the top half + // off every depth bit pattern and every stencil fetch. + static const char* const kDepthFetchFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp int;\n" + "precision highp sampler2D;\n" + "uniform sampler2D uSource;\n" + "in vec2 vUv;\n" + "layout(location = 0) out uvec4 oBits;\n" + "void main() {\n" + " oBits = uvec4(floatBitsToUint(texture(uSource, vUv).r), 0u, 0u, 0u);\n" + "}\n"; + + static const char* const kStencilFetchFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp int;\n" + "precision highp usampler2D;\n" + "uniform usampler2D uSource;\n" + "in vec2 vUv;\n" + "layout(location = 0) out uvec4 oBits;\n" + "void main() {\n" + " oBits = uvec4(texture(uSource, vUv).r, 0u, 0u, 0u);\n" + "}\n"; + + static Bool EnsureResources() { + if (s_contextGeneration != g_backendContextGeneration) { + // The ids belonged to a dead context; the context reclaimed them with it. + s_colorFramebuffer = 0; + s_colorTexture = 0; + s_colorWidth = 0; + s_colorHeight = 0; + s_vertexArray = 0; + s_depthProgram = 0; + s_stencilProgram = 0; + s_programsFailed = false; + s_slots[0] = StageSlot{}; + s_slots[1] = StageSlot{}; + s_slotFramebuffers[0] = 0; + s_slotFramebuffers[1] = 0; + s_contextGeneration = g_backendContextGeneration; + } + if (s_programsFailed) { + return false; + } + if (s_depthProgram == 0) { + // Same full-screen vertex shader (and its uUvTransform) as the replicate + // blit: a staging slot is sized to the largest rectangle seen so far, so the + // quad's [0,1] coordinates have to be scaled down to the part it occupies. + s_depthProgram = ReplicateBlitImpl::BuildProgram(kDepthFetchFragmentSource); + s_stencilProgram = ReplicateBlitImpl::BuildProgram(kStencilFetchFragmentSource); + if (s_depthProgram == 0 || s_stencilProgram == 0) { + s_programsFailed = true; + MGLOG_E_ONCE("ReadPixels: could not build the depth/stencil readback programs"); + return false; + } + s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); + s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); + } + for (NativeContextName& framebuffer : s_slotFramebuffers) { + if (framebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &framebuffer); + if (framebuffer == 0) return false; + } + } + if (s_colorFramebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &s_colorFramebuffer); + if (s_colorFramebuffer == 0) return false; + } + if (s_vertexArray == 0) { + g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray); + if (s_vertexArray == 0) return false; + } + return true; + } + + // Ordered guesses at the sized internal format backing one aspect. More than one + // entry only when the source's own format cannot be queried (the default + // framebuffer), in which case the reported channel sizes narrow it down and the + // staging blit picks the winner by being the only one that raises no GL error. + struct AspectCandidates { + GLenum formats[4] = {0, 0, 0, 0}; + Uint count = 0; + void Push(GLenum format) { + for (Uint i = 0; i < count; ++i) { + if (formats[i] == format) return; + } + if (count < 4) formats[count++] = format; + } + }; + + static GLenum AttachmentPointFor(Bool isDefault, Bool stencilAspect) { + // The default framebuffer names its buffers GL_DEPTH / GL_STENCIL; a user + // framebuffer names them GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT, and asking + // one for the other's spelling is GL_INVALID_ENUM. + if (isDefault) return stencilAspect ? GL_STENCIL : GL_DEPTH; + return stencilAspect ? GL_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT; + } + + // Returns false when the bound READ framebuffer has no such aspect at all. + static Bool DescribeAspect(Bool stencilAspect, Bool* outIsDefault, AspectCandidates* out) { + GLint readFramebuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer); + const Bool isDefault = readFramebuffer == 0; + if (outIsDefault) *outIsDefault = isDefault; + const GLenum point = AttachmentPointFor(isDefault, stencilAspect); + + ClearGLErrors(); + GLint objectType = GL_NONE; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, point, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); + ClearGLErrors(); + + // The channel sizes are read before the "is there anything here" decision, because + // they are the more trustworthy witness. Adreno answers GL_NONE for OBJECT_TYPE on + // an attachment made by glFramebufferTexture (a layered cube/array attachment) while + // still reporting its depth and stencil bits correctly, and taking OBJECT_TYPE at + // its word there makes the whole readback report "no such aspect" for a framebuffer + // that plainly has one. The OTHER aspect's size matters as much as this one's - a + // depth buffer that also carries stencil has to be staged into a packed scratch, + // because ES only blits depth between identical formats. + GLint depthBits = 0; + GLint stencilBits = 0; + GLint componentType = GL_UNSIGNED_NORMALIZED; + ClearGLErrors(); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, + AttachmentPointFor(isDefault, false), + GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, + AttachmentPointFor(isDefault, true), + GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv( + GL_READ_FRAMEBUFFER, AttachmentPointFor(isDefault, false), + GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType); + ClearGLErrors(); + + const GLint aspectBits = stencilAspect ? stencilBits : depthBits; + if (objectType == GL_NONE && aspectBits <= 0) { + return false; + } + + if (!isDefault) { + // A real object: ask it directly and try that first. It is only a preference, + // not a verdict - the probe binds the attachment as GL_TEXTURE_2D, and a name + // whose target is not GL_TEXTURE_2D can leave it describing the wrong texture + // (see QueryAttachmentSizedFormat). The size-derived guesses below therefore + // stay in the list behind it, so a wrong first answer costs one rejected blit + // instead of the whole readback. The probe's own refusal must not be left on + // the error queue for the caller's next glGetError to pick up as its own. + // + // The cost of keeping the fallbacks: a source whose OWN format cannot back a + // staging texture (GL_STENCIL_INDEX8 without EXT/OES_texture_stencil8, say) no + // longer fails cleanly - it retries with a packed format, and a driver lax + // enough to accept the resulting mismatched depth/stencil blit would hand back + // data indistinguishable from a correct read. ES conformance forbids that blit, + // so this trades a spec-guaranteed rejection for a driver-bug-only wrong answer; + // the shapes it rescues (array and cube attachments) are otherwise unreadable. + const GLenum exact = ReplicateBlitImpl::QueryAttachmentSizedFormat(point); + ClearGLErrors(); + if (exact != 0) { + out->Push(exact); + } + } else if (s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat != 0) { + out->Push(s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat); + } + + const Bool floatDepth = componentType == GL_FLOAT; + const Bool packed = depthBits > 0 && stencilBits > 0; + if (stencilAspect) { + if (packed) { + out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8); + out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8); + } + out->Push(GL_STENCIL_INDEX8); + if (!packed) { + out->Push(GL_DEPTH24_STENCIL8); + } + } else if (packed) { + out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8); + out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8); + } else if (floatDepth) { + out->Push(GL_DEPTH_COMPONENT32F); + out->Push(GL_DEPTH32F_STENCIL8); + } else if (depthBits > 0 && depthBits <= 16) { + out->Push(GL_DEPTH_COMPONENT16); + out->Push(GL_DEPTH_COMPONENT24); + } else { + out->Push(GL_DEPTH_COMPONENT24); + out->Push(GL_DEPTH24_STENCIL8); + out->Push(GL_DEPTH_COMPONENT32F); + } + return out->count != 0; + } + + // Point a staging slot at `format`, growing it if the rectangle needs it, and leave + // it bound on the borrowed texture unit. + static Bool EnsureStageTexture(StageSlot& slot, GLenum format, GLsizei width, GLsizei height) { + if (slot.texture != 0 && slot.format == format && slot.width >= width && slot.height >= height) { + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); + return true; + } + if (slot.texture != 0) { + g_GLESFuncs.glDeleteTextures(1, &slot.texture); // immutable storage cannot be resized + ScratchFBOImpl::NoteTextureIdDeleted(slot.texture); + slot.texture = 0; + } + slot.format = 0; + slot.width = std::max(slot.width, width); + slot.height = std::max(slot.height, height); + g_GLESFuncs.glGenTextures(1, &slot.texture); + if (slot.texture == 0) return false; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); + ClearGLErrors(); + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, format, slot.width, slot.height); + const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + // A depth texture left in compare mode samples to 0/1 instead of the stored value. + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); + ClearGLErrors(); + slot.format = ok ? format : 0; + return ok; + } + + static Bool EnsureColorTexture(GLsizei width, GLsizei height) { + if (s_colorTexture != 0 && s_colorWidth >= width && s_colorHeight >= height) { + return true; + } + if (s_colorTexture != 0) { + g_GLESFuncs.glDeleteTextures(1, &s_colorTexture); + ScratchFBOImpl::NoteTextureIdDeleted(s_colorTexture); + s_colorTexture = 0; + } + s_colorWidth = std::max(s_colorWidth, width); + s_colorHeight = std::max(s_colorHeight, height); + g_GLESFuncs.glGenTextures(1, &s_colorTexture); + if (s_colorTexture == 0) return false; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_colorTexture); + ClearGLErrors(); + // R32UI is colour-renderable in ES 3.0 core - no float-renderability extension + // needed - and carries a depth bit pattern or a stencil index without loss. + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, s_colorWidth, s_colorHeight); + const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ClearGLErrors(); + if (!ok) { + s_colorWidth = 0; + s_colorHeight = 0; + } + return ok; + } + + // Copy one aspect of the requested rectangle out of the bound READ framebuffer into + // its staging slot, trying each candidate format until one is blit-compatible. + // Requires the slot's own framebuffer bound as DRAW. + static Bool StageAspect(StageSlot& slot, const AspectCandidates& candidates, Bool stencilAspect, Bool isDefault, + GLint x, GLint y, GLsizei width, GLsizei height) { + const GLbitfield aspectBit = stencilAspect ? GL_STENCIL_BUFFER_BIT : GL_DEPTH_BUFFER_BIT; + for (Uint candidate = 0; candidate < candidates.count; ++candidate) { + const GLenum format = candidates.formats[candidate]; + const Bool formatHasAspect = stencilAspect ? ReplicateBlitImpl::FormatHasStencil(format) + : ReplicateBlitImpl::FormatHasDepth(format); + if (!formatHasAspect) { + continue; + } + if (!EnsureStageTexture(slot, format, width, height)) { + continue; + } + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, + ReplicateBlitImpl::ScratchAttachmentFor(format), GL_TEXTURE_2D, + slot.texture, 0); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + continue; + } + ClearGLErrors(); + // Only this aspect: a packed scratch standing in for a separate attachment + // has a second half with nothing to copy into it. + g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height, aspectBit, GL_NEAREST); + const GLenum blitErr = g_GLESFuncs.glGetError(); + if (blitErr != GL_NO_ERROR) { + continue; + } + if (isDefault) { + slot.defaultFramebufferFormat = format; + } + return true; + } + return false; + } + + // Runs one conversion pass over a staged slot and reads its colour target back. + // Requires the slot's texture bound on the borrowed unit and s_colorFramebuffer + // bound as DRAW. + static Bool ConvertAndRead(const StageSlot& slot, Bool stencilAspect, GLsizei width, GLsizei height, + Vector& outWords) { + if (ReplicateBlitImpl::FormatHasDepth(slot.format) && ReplicateBlitImpl::FormatHasStencil(slot.format)) { + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, + stencilAspect ? GL_STENCIL_INDEX : GL_DEPTH_COMPONENT); + } + const Float uvScaleX = static_cast(width) / static_cast(slot.width); + const Float uvScaleY = static_cast(height) / static_cast(slot.height); + g_GLESFuncs.glUseProgram(stencilAspect ? s_stencilProgram : s_depthProgram); + g_GLESFuncs.glUniform4f(stencilAspect ? s_stencilUvTransform : s_depthUvTransform, uvScaleX, uvScaleY, + 0.0f, 0.0f); + g_GLESFuncs.glViewport(0, 0, width, height); + ClearGLErrors(); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + MGLOG_E_ONCE("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth"); + return false; + } + + // GL_RGBA_INTEGER / GL_UNSIGNED_INT is the pair ES guarantees for an unsigned + // integer attachment whatever its channel count, so four words come back per + // pixel and only the first carries anything. + outWords.assign(static_cast(width) * static_cast(height) * 4u, 0u); + ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false); + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, s_colorFramebuffer); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); + ClearGLErrors(); + g_GLESFuncs.glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, outWords.data()); + const GLenum readError = g_GLESFuncs.glGetError(); + if (readError != GL_NO_ERROR) { + MGLOG_E_ONCE("ReadPixels: could not read the %s conversion target back: %s", + stencilAspect ? "stencil" : "depth", MG_Util::ConvertGLEnumToString(readError).c_str()); + return false; + } + return true; + } + + // Stage, convert and read one aspect. The caller owns the state guard and the DRAW + // framebuffer scope. + static Bool ReadAspect(Bool stencilAspect, GLint x, GLint y, GLsizei width, GLsizei height, + Vector& outWords) { + Bool isDefault = false; + AspectCandidates candidates; + if (!DescribeAspect(stencilAspect, &isDefault, &candidates)) { + return false; + } + StageSlot& slot = s_slots[stencilAspect ? 1 : 0]; + + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, SlotFramebuffer(slot)); + if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) { + MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source", + stencilAspect ? "stencil" : "depth"); + return false; + } + + if (!EnsureColorTexture(width, height)) { + MGLOG_E_ONCE("ReadPixels: could not allocate the depth/stencil conversion target"); + return false; + } + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, s_colorFramebuffer); + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + s_colorTexture, 0); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E_ONCE("ReadPixels: the depth/stencil conversion target is not renderable"); + return false; + } + + // EnsureColorTexture may have taken the borrowed unit for its own storage call. + VertexArrayImpl::BindBackendVAOId(s_vertexArray); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); + return ConvertAndRead(slot, stencilAspect, width, height, outWords); + } + + // Fills whichever of the two outputs the caller asked for from the bound READ + // framebuffer. Returns false when the emulation could not service the request at + // all, leaving the caller to report the failure the way it always has. + static Bool Read(GLint x, GLint y, GLsizei width, GLsizei height, Vector* outDepth, + Vector* outStencil) { + if (width <= 0 || height <= 0 || (outDepth == nullptr && outStencil == nullptr)) { + return false; + } + // Sampling the stencil half of a packed texture goes through + // GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the + // pname would just raise GL_INVALID_ENUM and the shader would read depth bits as + // stencil. + const Bool supportsStencilTextureMode = + g_GLESCapabilities.GLESVersion.Major > 3 || + (g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1); + if (outStencil != nullptr && !supportsStencilTextureMode) { + return false; + } + if (!EnsureResources()) { + return false; + } + + // Taken before the first scratch texture bind: the guard owns the borrowed + // texture unit as well as the pipeline, and it is what puts the scissor test out + // of the way of the staging blit. + ScopedEmulationDrawState emulationState; + ScopedFramebufferBinding drawBinding(/*saveRead=*/false, /*saveDraw=*/true); + + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector words; + if (outDepth != nullptr) { + if (!ReadAspect(/*stencilAspect=*/false, x, y, width, height, words)) { + return false; + } + outDepth->assign(pixelCount, 0.0f); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint32 bits = words[i * 4u]; + Float value = 0.0f; + Memcpy(&value, &bits, sizeof(value)); + (*outDepth)[i] = value; + } + } + if (outStencil != nullptr) { + if (!ReadAspect(/*stencilAspect=*/true, x, y, width, height, words)) { + return false; + } + outStencil->assign(pixelCount, 0); + for (SizeT i = 0; i < pixelCount; ++i) { + (*outStencil)[i] = static_cast(words[i * 4u] & 0xFFu); + } + } + return true; + } + } // namespace DepthStencilSamplingReadImpl + + // One normalized depth value per pixel, tightly packed. Which native read a driver + // accepts depends on the attached format: a fixed-point depth buffer takes + // GL_UNSIGNED_INT, while a floating-point one (DEPTH_COMPONENT32F, + // DEPTH32F_STENCIL8 - what dEQP's own fbo-surface-type wrapper framebuffer uses) + // rejects it with GL_INVALID_OPERATION and only reads back as GL_FLOAT. Try both. + static Bool ReadDepthValuesNative(GLint x, GLint y, GLsizei width, GLsizei height, Vector& outDepth) { + outDepth.assign(static_cast(width) * static_cast(height), 0.0f); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + GLenum floatError = GL_INVALID_OPERATION; + if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) { + Vector raw(outDepth.size()); + // Drain first: a stale flag some earlier best-effort call left queued + // must not be misattributed to this read (it would silently drop the + // whole readback in production builds where ErrorLopper is compiled out). + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, raw.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + for (SizeT i = 0; i < outDepth.size(); ++i) { + outDepth[i] = static_cast(static_cast(raw[i]) / 4294967295.0); + } + return true; + } + + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, outDepth.data()); + floatError = g_GLESFuncs.glGetError(); + if (floatError == GL_NO_ERROR) { + return true; + } + } + + // Neither native spelling exists on this driver (which is the ordinary case: ES has + // no depth readback in core and GL_NV_read_depth is rare), so sample the attachment + // instead. The scoped pack state above is irrelevant to that path - it reads its own + // scratch colour target - but harmless, and leaving it in place keeps the restore in + // one place. + if (DepthStencilSamplingReadImpl::Read(x, y, width, height, &outDepth, /*outStencil=*/nullptr)) { + return true; + } + MGLOG_E_ONCE("ReadPixels: no depth readback path is available: native reads failed with %s and the " + "sampling emulation could not service the source", + MG_Util::ConvertGLEnumToString(floatError).c_str()); + return false; + } + + // GL_DEPTH_COMPONENT readback into the client's layout, honouring the PACK pixel-store + // parameters. GL 4.6 core 18.2.8 table 8.6: the normalized depth is written as-is for the + // floating-point client types and scaled into the full range of whichever integer width the + // client asked for otherwise - 2^n - 1 for an unsigned width, 2^(n-1) - 1 for a signed one. + // Sharing one denominator across the two signednesses would put a 0.5 depth at 32768, which + // as a GLshort is -32768, so GL_BYTE/GL_SHORT/GL_INT each keep their own. + static Bool ReadPixelsDepthComponent(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, + void* pixels) { + SizeT dstPixelBytes = 0; + switch (type) { + case GL_BYTE: + case GL_UNSIGNED_BYTE: dstPixelBytes = sizeof(Uint8); break; + case GL_SHORT: + case GL_UNSIGNED_SHORT: + case GL_HALF_FLOAT: dstPixelBytes = sizeof(Uint16); break; + case GL_INT: + case GL_UNSIGNED_INT: + case GL_FLOAT: dstPixelBytes = sizeof(Uint32); break; + default: return false; + } + if (width <= 0 || height <= 0) { + return true; + } + + Vector depth; + if (!ReadDepthValuesNative(x, y, width, height, depth)) { + return true; // already reported; nothing was written, as before + } + + StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "depth", + [&](GLsizei row, Uint8* dst) { + const Float* srcRow = + depth.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + const Float depthValue = srcRow[col]; + switch (type) { + case GL_BYTE: + reinterpret_cast(dst)[col] = static_cast( + NormalizedDepthToUnsigned(depthValue, 127.0)); + break; + case GL_UNSIGNED_BYTE: + dst[col] = static_cast( + NormalizedDepthToUnsigned(depthValue, 255.0)); + break; + case GL_SHORT: + reinterpret_cast(dst)[col] = static_cast( + NormalizedDepthToUnsigned(depthValue, 32767.0)); + break; + case GL_UNSIGNED_SHORT: + reinterpret_cast(dst)[col] = static_cast( + NormalizedDepthToUnsigned(depthValue, 65535.0)); + break; + case GL_INT: + reinterpret_cast(dst)[col] = static_cast( + NormalizedDepthToUnsigned(depthValue, 2147483647.0)); + break; + case GL_UNSIGNED_INT: + reinterpret_cast(dst)[col] = + NormalizedDepthToUnsigned(depthValue, 4294967295.0); + break; + case GL_HALF_FLOAT: + reinterpret_cast(dst)[col] = + MG_Util::EncodeFloatToHalfBits(depthValue); + break; + default: + reinterpret_cast(dst)[col] = depthValue; + break; + } + } + }); + return true; + } + + // One stencil byte per pixel, tightly packed. ES has no guaranteed stencil readback + // at all: GL_STENCIL_INDEX needs GL_NV_read_stencil, and a driver without it rejects + // the read outright. Where the attachment is a combined depth-stencil buffer the + // packed GL_DEPTH_STENCIL read (which the packed_depth_stencil.verify_* cases already + // rely on) carries the same bytes in its low octet, so use that as the fallback. + static Bool ReadStencilBytesNative(GLint x, GLint y, GLsizei width, GLsizei height, Vector& outStencil) { + outStencil.assign(static_cast(width) * static_cast(height), 0); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + GLenum packedError = GL_INVALID_OPERATION; + if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) { + // Drain first: see ReadDepthValuesNative. + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, outStencil.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + return true; + } + + Vector packed(outStencil.size(), 0); + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + for (SizeT i = 0; i < outStencil.size(); ++i) { + outStencil[i] = static_cast(packed[i] & 0xFFu); + } + return true; + } + + // A DEPTH32F_STENCIL8 attachment rejects the 24_8 type: its packed layout is a + // float depth followed by a padded stencil byte, eight bytes per pixel with the + // index at offset 4. + Vector packed32f(outStencil.size() * 8u, 0); + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, + packed32f.data()); + packedError = g_GLESFuncs.glGetError(); + if (packedError == GL_NO_ERROR) { + for (SizeT i = 0; i < outStencil.size(); ++i) { + outStencil[i] = packed32f[i * 8u + 4u]; + } + return true; + } + } + + // No native spelling worked, which is the ordinary case on ES: sample the stencil + // half of the attachment instead. + if (DepthStencilSamplingReadImpl::Read(x, y, width, height, /*outDepth=*/nullptr, &outStencil)) { + return true; + } + MGLOG_E_ONCE("ReadPixels: no stencil readback path is available: native reads failed with %s and the " + "sampling emulation could not service the source", + MG_Util::ConvertGLEnumToString(packedError).c_str()); + return false; + } + + // GL_STENCIL_INDEX readback into the client's integer layout, honouring the PACK + // pixel-store parameters. Handles the widths desktop clients ask for; the stencil + // values themselves are always 8 bits. + static Bool ReadPixelsStencilViaNative(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, + void* pixels) { + // GL 4.6 core 18.2.8: a stencil index is written unconverted into whichever integer width + // the client asked for, and converted to a float value for the floating-point widths - both + // GL_FLOAT and GL_HALF_FLOAT. The signed widths are as legal as the unsigned ones - the CTS + // reads stencil with GL_INT - and rejecting them here used to let the call fall through to a + // native ES read the driver refuses, after which nothing was written at all and the caller + // kept its zeros. + SizeT dstPixelBytes = 0; + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: dstPixelBytes = sizeof(Uint8); break; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_HALF_FLOAT: dstPixelBytes = sizeof(Uint16); break; + case GL_UNSIGNED_INT: + case GL_INT: dstPixelBytes = sizeof(Uint32); break; + case GL_FLOAT: dstPixelBytes = sizeof(GLfloat); break; + default: return false; + } + if (width <= 0 || height <= 0) { + return true; + } + + Vector raw; + if (!ReadStencilBytesNative(x, y, width, height, raw)) { + return true; + } + + StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "stencil", + [&](GLsizei row, Uint8* dst) { + const Uint8* srcRow = + raw.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + dst[static_cast(col)] = srcRow[col]; + break; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + reinterpret_cast(dst)[col] = srcRow[col]; + break; + case GL_FLOAT: + reinterpret_cast(dst)[col] = static_cast(srcRow[col]); + break; + case GL_HALF_FLOAT: + reinterpret_cast(dst)[col] = + MG_Util::EncodeFloatToHalfBits(static_cast(srcRow[col])); + break; + default: + reinterpret_cast(dst)[col] = srcRow[col]; + break; + } + } + }); + return true; + } + + // GL_DEPTH_STENCIL readback: the two aspects are fetched separately and woven into the + // packed layout the client asked for (GL 4.6 core table 8.6). This is what + // KHR-GL3x.packed_depth_stencil.verify_read_pixels / verify_get_tex_image / + // verify_copy_tex_image read their gradients with. + static Bool ReadPixelsDepthStencilPacked(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, + void* pixels) { + SizeT dstPixelBytes = 0; + switch (type) { + case GL_UNSIGNED_INT_24_8: dstPixelBytes = sizeof(Uint32); break; + case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: dstPixelBytes = sizeof(Float) + sizeof(Uint32); break; + default: return false; + } + if (width <= 0 || height <= 0) { + return true; + } + + Vector depth; + Vector stencil; + if (!ReadDepthValuesNative(x, y, width, height, depth)) { + return true; + } + if (!ReadStencilBytesNative(x, y, width, height, stencil)) { + return true; + } + + StoreReadbackRowsToClient( + width, height, dstPixelBytes, pixels, "packed depth/stencil", [&](GLsizei row, Uint8* dst) { + const SizeT base = static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + const Uint32 stencilIndex = stencil[base + static_cast(col)]; + const Float depthValue = depth[base + static_cast(col)]; + if (type == GL_UNSIGNED_INT_24_8) { + reinterpret_cast(dst)[col] = + (NormalizedDepthToUnsigned(depthValue, 16777215.0) << 8) | stencilIndex; + } else { + // Depth float first, then a word whose low octet is the index and + // whose top 24 bits are unused. + Uint8* pixel = dst + static_cast(col) * (sizeof(Float) + sizeof(Uint32)); + Memcpy(pixel, &depthValue, sizeof(depthValue)); + Memcpy(pixel + sizeof(Float), &stencilIndex, sizeof(stencilIndex)); + } + } + }); + return true; + } + + static Bool CanDecodeWideSourceType(GLenum type) { + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + case GL_UNSIGNED_SHORT: + case GL_SHORT: + case GL_HALF_FLOAT: + case GL_FLOAT: + return true; + default: + return false; + } + } + + // Component-array read formats usable as (possibly narrow) wide-read sources. + static Int GetWideReadChannelCount(GLenum format) { + switch (format) { + case GL_RED: + case GL_RED_INTEGER: + return 1; + case GL_RG: + case GL_RG_INTEGER: + return 2; + case GL_RGB: + case GL_RGB_INTEGER: + return 3; + case GL_RGBA: + case GL_RGBA_INTEGER: + return 4; + default: + return 0; + } + } + + static Bool IsIntegerReadFormat(GLint format) { + return format == GL_RED_INTEGER || format == GL_RG_INTEGER || format == GL_RGB_INTEGER || + format == GL_RGBA_INTEGER; + } + + // The bit pattern of 1.0 in a wide-read component type: what GL reports for a channel the + // attachment's format does not have. + static void FillWideReadOneBits(GLenum componentType, Uint8* oneBits) { + switch (componentType) { + case GL_UNSIGNED_BYTE: + oneBits[0] = 0xFF; + break; + case GL_BYTE: + oneBits[0] = 0x7F; + break; + case GL_UNSIGNED_SHORT: { + const Uint16 one = 0xFFFF; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_SHORT: { + const Int16 one = 0x7FFF; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_HALF_FLOAT: { + const Uint16 one = 0x3C00; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_FLOAT: { + const Float one = 1.0f; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + case GL_UNSIGNED_INT: + case GL_INT: { + const Uint32 one = 1; + Memcpy(oneBits, &one, sizeof(one)); + break; + } + default: + break; + } + } + + // Overwrites the alpha of a 4-channel wide read with the format's implied 1.0. Used for an + // attachment the backend widened from three channels to keep it colour-renderable: the storage + // has a real alpha channel holding whatever the draw wrote, but the format the application + // asked for has none, and GL reads a missing channel back as one. + static void ForceWideReadAlphaToOne(Vector& data, SizeT pixelCount, GLenum componentType) { + const SizeT componentSize = GetReadbackComponentSize(componentType); + if (componentSize == 0 || data.size() < pixelCount * 4 * componentSize) { + return; + } + Uint8 oneBits[4] = {0, 0, 0, 0}; + FillWideReadOneBits(componentType, oneBits); + for (SizeT i = 0; i < pixelCount; ++i) { + Memcpy(data.data() + (i * 4 + 3) * componentSize, oneBits, componentSize); + } + } + + // Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA + // layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in + // the source component type. + static void ExpandNarrowWideRead(Vector& data, SizeT pixelCount, Int srcChannels, GLenum componentType) { + const SizeT componentSize = GetReadbackComponentSize(componentType); + if (componentSize == 0 || srcChannels <= 0 || srcChannels >= 4) { + return; + } + Uint8 zeroBits[4] = {0, 0, 0, 0}; + Uint8 oneBits[4] = {0, 0, 0, 0}; + FillWideReadOneBits(componentType, oneBits); + + Vector expanded(pixelCount * 4 * componentSize); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* src = data.data() + i * static_cast(srcChannels) * componentSize; + Uint8* dst = expanded.data() + i * 4 * componentSize; + for (Int ch = 0; ch < 4; ++ch) { + if (ch < srcChannels) { + Memcpy(dst + static_cast(ch) * componentSize, src + static_cast(ch) * componentSize, + componentSize); + } else { + Memcpy(dst + static_cast(ch) * componentSize, ch == 3 ? oneBits : zeroBits, componentSize); + } + } + } + data = std::move(expanded); + } + + static void DrainESErrors() { DrainDriverErrors("ReadPixels"); } + + static GLenum QueryReadAttachmentComponentType() { + GLint framebufferId = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &framebufferId); + if (framebufferId == 0) { + return GL_UNSIGNED_NORMALIZED; // default framebuffers are normalized fixed-point + } + GLint readBuffer = GL_COLOR_ATTACHMENT0; + g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); + if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) { + readBuffer = GL_COLOR_ATTACHMENT0; + } + GLint componentType = 0; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast(readBuffer), + GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType); + DrainESErrors(); + return componentType != 0 ? static_cast(componentType) : GL_UNSIGNED_NORMALIZED; + } + + + // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's + // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its + // "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op. + // `forceOpaqueAlpha`: the source image is a three-channel format the backend widened to four to + // keep it colour-renderable, so its alpha channel holds whatever the draw wrote and has to be + // answered with the 1.0 the application's format implies. Passed in rather than derived here: + // glReadPixels reads the bound READ framebuffer, but glGetTexImage reads a texture through a + // scratch framebuffer, so the frontend's READ binding describes a different image entirely - + // consulting it there would both miss real widenings and corrupt readbacks of ordinary + // textures taken while some unrelated widened attachment happened to be bound. + // The image-format widening's READ half, for the seven normalized formats whose carrier holds + // their channels as INTEGER CODES (GL_RGBA16 stored as a GL_RGBA16UI - see + // TextureImpl::GetImageBindableStorageWidening). Nothing else in the readback would get those + // right: the attachment is an integer one while the application's format is normalized, so the + // class check below would refuse the read outright, and a repack that got past it would hand + // back 65535.0 where GL owes 1.0. + // + // Inactive (ChannelMax all zero) for every other read, which is all but a handful. + struct NormalizedImageCarrierRead { + Uint ChannelMax[4] = {0u, 0u, 0u, 0u}; + Bool SignedNormalized = false; + + Bool Active() const { return ChannelMax[0] != 0u; } + }; + + static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, + GLenum type, void* pixels, Bool honorPackImageParams, + Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha, + const NormalizedImageCarrierRead& normalizedCarrier = {}) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) { + return false; + } + // Covers unknown types, packed field-count/format mismatches and float types on integer formats. + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + if (dstPixelBytes == 0) { + return false; + } + + if (width <= 0 || height <= 0) { + return true; + } + const auto& pixelPackBufferObject = + SplitReadbackPackBuffer(); + if (!pixelPackBufferObject && pixels == nullptr) { + return true; + } + + const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); + const Bool integerAttachment = + attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; + // A normalized image carrier is EXACTLY the case where the two disagree on purpose, and + // it is the caller - which knows the TEXTURE being read, not just the attachment - that + // says so. An integer client format through such a carrier is not a shape GL can ask for + // (the frontend format is normalized), so it is refused here rather than converted. + if (normalizedCarrier.Active() && (mapping.isInteger || !integerAttachment)) { + MGLOG_E_ONCE("Readback conversion: a normalized image carrier was read as %s, which is not a " + "normalized client format; skipping", + MG_Util::ConvertGLEnumToString(format).c_str()); + return true; + } + if (!normalizedCarrier.Active() && mapping.isInteger != integerAttachment) { + MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", + MG_Util::ConvertGLEnumToString(format).c_str()); + return true; + } + + // Prefer the implementation-defined pair (full precision on e.g. norm16 buffers, and possibly + // a narrow format like GL_RED/GL_UNSIGNED_SHORT), then the spec/extension-guaranteed pair for + // the attachment class. Narrow reads are expanded to RGBA on the CPU afterwards. + GLint implFormat = 0; + GLint implType = 0; + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); + + struct WideReadCandidate { + GLenum format; + GLenum type; + }; + WideReadCandidate candidates[4]; + Int candidateCount = 0; + if (normalizedCarrier.Active()) { + // The storage IS an integer texture, whatever the application's format says, so the + // only read that can answer is the integer one. The codes it hands back are turned + // into the floats the client asked for below. + candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT}; + } else if (mapping.isInteger) { + if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && IsIntegerReadFormat(implFormat) && + (implType == GL_INT || implType == GL_UNSIGNED_INT)) { + candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; + } + candidates[candidateCount++] = { + GL_RGBA_INTEGER, + attachmentComponentType == GL_INT ? static_cast(GL_INT) : static_cast(GL_UNSIGNED_INT)}; + } else { + if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && !IsIntegerReadFormat(implFormat) && + (CanDecodeWideSourceType(static_cast(implType)) || + (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) { + candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; + } + if (attachmentComponentType == GL_FLOAT) { + candidates[candidateCount++] = {GL_RGBA, GL_FLOAT}; + } + if (attachmentComponentType == GL_SIGNED_NORMALIZED) { + // EXT_render_snorm attachments read back as RGBA/BYTE (8-bit) or RGBA/SHORT (16-bit). + candidates[candidateCount++] = {GL_RGBA, GL_SHORT}; + candidates[candidateCount++] = {GL_RGBA, GL_BYTE}; + } else { + candidates[candidateCount++] = {GL_RGBA, GL_UNSIGNED_BYTE}; + } + } + + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + + Vector wide; + GLenum wideType = GL_NONE; + GLenum readFormat = GL_NONE; + Int readChannels = 0; + DrainESErrors(); + for (Int i = 0; i < candidateCount; ++i) { + const WideReadCandidate candidate = candidates[i]; + Bool alreadyTried = false; + for (Int j = 0; j < i; ++j) { + alreadyTried = + alreadyTried || (candidates[j].format == candidate.format && candidates[j].type == candidate.type); + } + if (alreadyTried) { + continue; + } + const Int channels = GetWideReadChannelCount(candidate.format); + const SizeT candidateComponentSize = GetReadbackComponentSize(candidate.type); + wide.resize(static_cast(width) * static_cast(height) * + static_cast(channels) * candidateComponentSize); + g_GLESFuncs.glReadPixels(x, y, width, height, candidate.format, candidate.type, wide.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + wideType = candidate.type; + readFormat = candidate.format; + readChannels = channels; + break; + } + } + if (wideType == GL_NONE) { + MGLOG_E_ONCE("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return true; + } + + if (normalizedCarrier.Active()) { + // GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same + // denominators, so a texel an imageStore wrote and a texel the upload seeded read back + // identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with + // the signed code recovered from the low sixteen bits of the unsigned channel. + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector floatWide(pixelCount * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(floatWide.data()); + const auto* src = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + for (SizeT channel = 0; channel < 4; ++channel) { + const Uint32 code = src[i * 4 + channel]; + const auto denominator = static_cast(normalizedCarrier.ChannelMax[channel]); + if (normalizedCarrier.SignedNormalized) { + const auto signedCode = static_cast(static_cast(code)); + dst[i * 4 + channel] = + std::max(static_cast(signedCode) / denominator, -1.0f); + } else { + dst[i * 4 + channel] = static_cast(code) / denominator; + } + } + } + wide = Move(floatWide); + wideType = GL_FLOAT; + readChannels = 4; + } + if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { + // Unpack the packed words into a float wide buffer (full 10-bit precision on e.g. + // GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV). + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector floatWide(pixelCount * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(floatWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + Uint32 word; + Memcpy(&word, wide.data() + i * 4, sizeof(word)); + dst[i * 4 + 0] = static_cast(word & 0x3FFu) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 0x3u) / 3.0f; + } + wide = std::move(floatWide); + wideType = GL_FLOAT; + readChannels = 4; + } + if (readChannels < 4) { + ExpandNarrowWideRead(wide, static_cast(width) * static_cast(height), readChannels, wideType); + } + + // Undo the three-channel widening (see the parameter's comment). Deliberately not gated on + // applyFixedPointReadClamp: that flag implements GL_CLAMP_READ_COLOR, which glGetTexImage + // is exempt from, whereas "a format without alpha reads as 1.0" is the format's own + // semantics and applies to every read. + if (forceOpaqueAlpha) { + ForceWideReadAlphaToOne(wide, static_cast(width) * static_cast(height), wideType); + } + + // GL clamps a read from a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR + // defaults to GL_FIXED_ONLY). Formats the backend substitutes with a floating-point + // one keep the out-of-range value the app stored, so apply the clamp here - a + // GL_R16_SNORM target holding -0.125 must still read back as 0. + // glReadPixels only: GL_CLAMP_READ_COLOR does not apply to glGetTexImage, which + // reaches this helper through the same scratch-framebuffer path. + if (applyFixedPointReadClamp && FramebufferImpl::IsFixedPointFallbackReadAttachment()) { + const SizeT componentSize = GetReadbackComponentSize(wideType); + const SizeT valueCount = componentSize != 0 ? wide.size() / componentSize : 0; + switch (wideType) { + case GL_FLOAT: { + auto* values = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < valueCount; ++i) values[i] = std::clamp(values[i], 0.0f, 1.0f); + break; + } + case GL_HALF_FLOAT: { + auto* values = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < valueCount; ++i) { + values[i] = MG_Util::EncodeFloatToHalfBits( + std::clamp(MG_Util::DecodeHalfBitsToFloat(values[i]), 0.0f, 1.0f)); + } + break; + } + case GL_SHORT: { + // Signed normalized: the negative half is exactly what the clamp removes. + auto* values = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < valueCount; ++i) values[i] = std::max(values[i], 0); + break; + } + case GL_BYTE: { + auto* values = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < valueCount; ++i) values[i] = std::max(values[i], 0); + break; + } + default: + break; + } + } + + if (!ClientReadback::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, + type, pixels, honorPackImageParams)) { + return false; + } + + MGLOG_D("Readback conversion: converted %s/%s from wide %s/%s", MG_Util::ConvertGLEnumToString(format).c_str(), + MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(readFormat).c_str(), + MG_Util::ConvertGLEnumToString(wideType).c_str()); + return true; + } + + // ---- Bit-exact readback of a 32-bit packed colour level --------------------------------------- + // + // glGetTexImage of a packed format read with its OWN client type owes the application the words + // the image HOLDS, and neither of the two routes above can promise that once anything other than + // a glTexImage has written the level: + // + // * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an + // RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000, same value, different bits) and + // collapses an R11F_G11F_B10F NaN to the canonical payload 1 + // (MG_Util::EncodeFloatToUnsignedSmallFloat) - and a copy-image from RGB9_E5 lands exactly + // such a NaN in the 10-bit blue field every time, because the source's shared-exponent + // field is all ones; + // * the CPU shadow only ever holds what was UPLOADED, so for a level glCopyImageSubData wrote + // it answers with the PRE-COPY contents. MirrorCopyImageIntoDestinationShadow patches that + // up for the shapes it can address texel-exactly and declines for the rest - a renderbuffer + // source (which has no shadow to mirror from at all), a cube or 1D-array endpoint, a + // self-copy - and the decline is silent, so the stale words are served as truth. + // + // glCopyImageSubData is a raw texel-block move and EXT_copy_image puts every 32-bit colour + // format in one compatibility class, so copying the level into a scratch GL_R32UI image and + // reading THAT back as unsigned integers hands over the stored words themselves, whoever wrote + // them. This is what lets the shadow stop being the authority for these formats: it is tried + // first, and every step reports rather than guesses, so a driver that turns any of it down + // simply leaves the old shadow/attachment fallbacks to run. + GLuint g_packedWordScratchTextureId = 0; + GLsizei g_packedWordScratchWidth = 0; + GLsizei g_packedWordScratchHeight = 0; + + // Grow-only, so a readback sweep over a mip chain allocates once. Zero when the driver refused + // the storage, which is a decline and not an error. + static GLuint EnsurePackedWordScratchTexture(GLsizei width, GLsizei height) { + if (g_packedWordScratchTextureId != 0 && g_packedWordScratchWidth >= width && + g_packedWordScratchHeight >= height) { + return g_packedWordScratchTextureId; + } + const GLsizei newWidth = std::max(width, g_packedWordScratchWidth); + const GLsizei newHeight = std::max(height, g_packedWordScratchHeight); + if (g_packedWordScratchTextureId != 0) { + // A scratch FBO may still name the old id, and the driver is free to hand the same + // number back for the replacement - which would false-skip the re-attach. + ScratchFBOImpl::NoteTextureIdDeleted(g_packedWordScratchTextureId); + g_GLESFuncs.glDeleteTextures(1, &g_packedWordScratchTextureId); + g_packedWordScratchTextureId = 0; + g_packedWordScratchWidth = 0; + g_packedWordScratchHeight = 0; + } + GLuint texture = 0; + g_GLESFuncs.glGenTextures(1, &texture); + if (texture == 0) return 0; + + ClearGLErrors(); + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, texture); + // Immutable single-level storage: glCopyImageSubData wants a complete image, and + // glTexStorage clamps TEXTURE_MAX_LEVEL, which is what makes a one-level texture complete + // under the default mipmapping filter. + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, newWidth, newHeight); + const GLenum storageError = g_GLESFuncs.glGetError(); + // Re-bind whatever the binding cache says lives on the temp unit, so the cache stays + // truthful without a driver query (same discipline as CopyR32FTexture2D). + auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(TextureTarget::Texture2D)]; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0); + if (storageError != GL_NO_ERROR) { + g_GLESFuncs.glDeleteTextures(1, &texture); + MGLOG_D("GetTexImage: no %dx%d GL_R32UI scratch image (%s); the verbatim word readback is unavailable", + newWidth, newHeight, MG_Util::ConvertGLEnumToString(storageError).c_str()); + return 0; + } + g_packedWordScratchTextureId = texture; + g_packedWordScratchWidth = newWidth; + g_packedWordScratchHeight = newHeight; + return texture; + } + + // One slice of `backendTarget`'s level, as width*height stored 32-bit words in `outWords`. + static Bool ReadPackedLevelWordsViaScratch(GLuint texture, GLenum backendTarget, GLint level, GLint slice, + GLsizei width, GLsizei height, Uint32* outWords) { + if (texture == 0 || outWords == nullptr || width <= 0 || height <= 0 || level < 0 || slice < 0) return false; + if (!g_GLESFuncs.glCopyImageSubData) return false; + + // Horizontal bands, so neither the scratch image nor the staging buffer scales with the + // level. The scratch is grow-only on purpose - a sweep down a mip chain must not + // reallocate per level - which without a band cap would leave a 4096x4096 readback's + // 64 MiB image parked for the rest of the process. The cap is 1 MiB of GL_R32UI, with + // 4 MiB of staging behind it because the read lands four words per texel. + constexpr SizeT kMaxScratchTexels = SizeT{1} << 18; + const GLsizei bandRows = std::max( + 1, static_cast(std::min(kMaxScratchTexels / static_cast(width), + static_cast(height)))); + const GLuint scratch = EnsurePackedWordScratchTexture(width, bandRows); + if (scratch == 0) return false; + + ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false); + auto& scratchFB = ScratchFBOImpl::BlitReadFramebuffer(); + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, ScratchFBOImpl::EnsureId(scratchFB)); + ScratchFBOImpl::EnsureColorAttachment2D(scratchFB, GL_READ_FRAMEBUFFER, scratch, GL_TEXTURE_2D, 0); + ScratchFBOImpl::EnsureReadBuffer(scratchFB, GL_COLOR_ATTACHMENT0); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_D("GetTexImage: the GL_R32UI scratch attachment is incomplete; falling back"); + return false; + } + + // GL_RGBA_INTEGER/GL_UNSIGNED_INT is the one combination ES guarantees for an integer + // colour buffer, so the read lands four words per texel and the red one is compacted out + // here. The PACK scope is the tight default rather than the application's, so a row comes + // back packed at exactly `width * 4` words. One glGetError covers the whole loop: it + // accumulates, and a failure anywhere means the caller falls back rather than trusting a + // partial result. + const SizeT wordsPerRow = static_cast(width) * 4; + Vector staging(static_cast(bandRows) * wordsPerRow); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); + ClearGLErrors(); + for (GLsizei y = 0; y < height; y += bandRows) { + const GLsizei rows = std::min(bandRows, height - y); + g_GLESFuncs.glCopyImageSubData(texture, backendTarget, level, 0, y, slice, scratch, GL_TEXTURE_2D, 0, 0, + 0, 0, width, rows, 1); + g_GLESFuncs.glReadPixels(0, 0, width, rows, GL_RGBA_INTEGER, GL_UNSIGNED_INT, staging.data()); + for (GLsizei row = 0; row < rows; ++row) { + const Uint32* srcRow = staging.data() + static_cast(row) * wordsPerRow; + Uint32* dstRow = outWords + static_cast(y + row) * static_cast(width); + for (GLsizei x = 0; x < width; ++x) dstRow[x] = srcRow[static_cast(x) * 4]; + } + } + const GLenum error = g_GLESFuncs.glGetError(); + if (error != GL_NO_ERROR) { + MGLOG_D("GetTexImage: the GL_R32UI word readback of %s was refused (%s); falling back", + MG_Util::ConvertGLEnumToString(backendTarget).c_str(), + MG_Util::ConvertGLEnumToString(error).c_str()); + return false; + } + return true; + } + + +// Included in DirectGLES.cpp after the native packed-word readback helper. +// A storage promotion needs the driver's own texels as owned tight rows, not +// client PACK layout or a fallback into a frontend texture's potentially stale +// shadow. + class ScopedWireTextureReadLevel { + public: + ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level) + : m_texture(texture), m_target(target) { + switch (target) { + case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break; + case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break; + case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break; + case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break; + case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break; + default: return; + } + GLint previous = 0; + DrainESErrors(); + g_GLESFuncs.glGetIntegerv(m_binding, &previous); + g_GLESFuncs.glBindTexture(m_target, m_texture); + g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel); + g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel); + m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR; + if (m_saved) { + // ES framebuffer completeness can reject a defined level whose + // base level or intervening mip chain is missing/inconsistent. + // Read this existing native image as the sole base level. The + // logical GL parameters and every cached copy remain unchanged. + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level); + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level); + m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + g_GLESFuncs.glBindTexture(m_target, static_cast(previous)); + } + + ~ScopedWireTextureReadLevel() { + if (!m_saved) return; + // The packed-word path can use its own texture scratch binding. + // Save the binding at restoration time too; do not assume an active + // texture unit or make the backend's binding cache forget a bind. + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(m_binding, &previous); + g_GLESFuncs.glBindTexture(m_target, m_texture); + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel); + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel); + g_GLESFuncs.glBindTexture(m_target, static_cast(previous)); + } + + Bool Valid() const { return m_valid; } + + private: + GLuint m_texture; + GLenum m_target; + GLenum m_binding = GL_NONE; + GLint m_baseLevel = 0, m_maxLevel = 0; + Bool m_saved = false, m_valid = false; + }; + + static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type, + Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier, + Vector& bytes) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) return false; + const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); + if (!pixelBytes || width <= 0 || height <= 0) return false; + const GLenum componentType = QueryReadAttachmentComponentType(); + const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT; + if (carrier.Active() ? (mapping.isInteger || !integerAttachment) + : mapping.isInteger != integerAttachment) return false; + + GLint implFormat = 0, implType = 0; + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); + struct Candidate { GLenum format, type; }; + Vector candidates; + if (carrier.Active()) { + candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT}); + } else if (mapping.isInteger) { + if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) && + (implType == GL_INT || implType == GL_UNSIGNED_INT)) + candidates.push_back({static_cast(implFormat), static_cast(implType)}); + candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)}); + } else { + if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) && + (CanDecodeWideSourceType(implType) || + (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) + candidates.push_back({static_cast(implFormat), static_cast(implType)}); + if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT}); + if (componentType == GL_SIGNED_NORMALIZED) { + candidates.push_back({GL_RGBA, GL_SHORT}); + candidates.push_back({GL_RGBA, GL_BYTE}); + } else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE}); + } + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + const SizeT pixels = static_cast(width) * static_cast(height); + Vector wide; + GLenum wideType = GL_NONE; + Int channels = 0; + DrainESErrors(); + for (const auto candidate : candidates) { + channels = GetWideReadChannelCount(candidate.format); + wide.resize(pixels * static_cast(channels) * GetReadbackComponentSize(candidate.type)); + g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; } + } + if (wideType == GL_NONE) return false; + + if (carrier.Active()) { + Vector decoded(pixels * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(decoded.data()); + const auto* src = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < pixels; ++i) { + for (SizeT c = 0; c < 4; ++c) { + const Float divisor = static_cast(carrier.ChannelMax[c]); + dst[i * 4 + c] = carrier.SignedNormalized + ? std::max(static_cast(static_cast(static_cast(src[i * 4 + c]))) / divisor, -1.0f) + : static_cast(src[i * 4 + c]) / divisor; + } + } + wide = Move(decoded); wideType = GL_FLOAT; channels = 4; + } else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { + Vector decoded(pixels * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(decoded.data()); + for (SizeT i = 0; i < pixels; ++i) { + Uint32 word = 0; + Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word)); + dst[i * 4] = static_cast(word & 1023u) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 1023u) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 1023u) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 3u) / 3.0f; + } + wide = Move(decoded); wideType = GL_FLOAT; channels = 4; + } + if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType); + if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType); + Vector converted(pixels * pixelBytes); + const SizeT sourceRowBytes = static_cast(width) * 4 * GetReadbackComponentSize(wideType); + for (GLsizei row = 0; row < height; ++row) + ClientReadback::ConvertWideReadbackRow(wide.data() + static_cast(row) * sourceRowBytes, + converted.data() + static_cast(row) * width * pixelBytes, width, wideType, mapping, type); + bytes = Move(converted); + return true; + } + + static Bool ReadWireDepthStencilLevelTight(GLuint texture, TextureTarget target, + TextureUploadTarget uploadTarget, GLint level, + const IntVec3& extent, GLenum format, GLenum type, + Vector& bytes) { + const SizeT pixelBytes = MG_Util::GetInputBytesPerPixel( + MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type)); + if (!pixelBytes) return false; + const Bool needDepth = format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL; + const Bool needStencil = format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL; + ScopedFramebufferBinding bindings(true, false); + GLuint fbo = 0; + g_GLESFuncs.glGenFramebuffers(1, &fbo); + struct DeleteFbo { GLuint id; ~DeleteFbo() { g_GLESFuncs.glDeleteFramebuffers(1, &id); } } cleanup{fbo}; + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, fbo); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + const GLenum point = needDepth ? (needStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT) + : GL_STENCIL_ATTACHMENT; + const SizeT count = static_cast(extent.x()) * extent.y(); + Vector result(count * extent.z() * pixelBytes); + if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS); + g_GLESFuncs.glFinish(); + for (GLint z = 0; z < extent.z(); ++z) { + if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || + nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY) + g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, point, texture, level, z); + else + g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, point, + TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget), texture, level); + g_GLESFuncs.glReadBuffer(GL_NONE); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false; + Vector depth; + Vector stencil; + if (needDepth && !ReadDepthValuesNative(0, 0, extent.x(), extent.y(), depth)) return false; + if (needStencil && !ReadStencilBytesNative(0, 0, extent.x(), extent.y(), stencil)) return false; + auto* dst = result.data() + count * z * pixelBytes; + for (SizeT i = 0; i < count; ++i) { + if (format == GL_DEPTH_STENCIL) { + const Uint32 s = stencil[i]; + if (type == GL_UNSIGNED_INT_24_8) { + const Uint32 packed = (NormalizedDepthToUnsigned(depth[i], 16777215.0) << 8) | s; + Memcpy(dst + i * pixelBytes, &packed, sizeof(packed)); + } else if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) { + Memcpy(dst + i * pixelBytes, &depth[i], sizeof(Float)); + Memcpy(dst + i * pixelBytes + sizeof(Float), &s, sizeof(s)); + } else return false; + } else if (type == GL_FLOAT) { + const Float value = needDepth ? depth[i] : static_cast(stencil[i]); + Memcpy(dst + i * pixelBytes, &value, sizeof(value)); + } else if (type == GL_HALF_FLOAT) { + const Uint16 value = MG_Util::EncodeFloatToHalfBits( + needDepth ? depth[i] : static_cast(stencil[i])); + Memcpy(dst + i * pixelBytes, &value, sizeof(value)); + } else { + const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT; + const Double maximum = signedType + ? (pixelBytes == 1 ? 127.0 : pixelBytes == 2 ? 32767.0 : 2147483647.0) + : (pixelBytes == 1 ? 255.0 : pixelBytes == 2 ? 65535.0 : 4294967295.0); + const Uint32 value = needDepth ? NormalizedDepthToUnsigned(depth[i], maximum) : stencil[i]; + if (pixelBytes == 1) dst[i] = static_cast(value); + else if (pixelBytes == 2) { const Uint16 v = static_cast(value); Memcpy(dst + i * 2, &v, 2); } + else if (pixelBytes == 4) Memcpy(dst + i * 4, &value, 4); + else return false; + } + } + } + bytes = Move(result); + return true; + } + + Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, + TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent, + Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector& bytes) { + if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false; + const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent); + const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level); + if (!readLevel.Valid()) return false; + if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) + return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes); + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) return false; + const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); + if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false; + const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + const auto widening = sourceUsesImageCarrier + ? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{}; + const SizeT sliceBytes = static_cast(extent.x()) * extent.y() * pixelBytes; + Vector result(sliceBytes * static_cast(extent.z())); + if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS); + g_GLESFuncs.glFinish(); + + // A raw packed word may have redundant encodings or NaN payloads. Read + // it through R32UI when the source really still uses that native layout. + const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( + logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format), + MG_Util::ConvertGLEnumToTexturePixelDataType(type)); + if (rawPacked && pixelBytes == sizeof(Uint32)) { + const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX && + uploadTarget <= TextureUploadTarget::CubeMapNegativeZ + ? static_cast(uploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX) : 0; + for (GLint layer = 0; layer < extent.z(); ++layer) + if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(), + reinterpret_cast(result.data() + static_cast(layer) * sliceBytes))) return false; + bytes = Move(result); + return true; + } + + NormalizedImageCarrierRead carrier{}; + if (widening && widening.CarriesNormalizedCodes()) { + for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c]; + carrier.SignedNormalized = widening.SignedNormalized; + } + TempFBOBinder binder(true); + auto& framebuffer = binder.Framebuffer(); + for (GLint layer = 0; layer < extent.z(); ++layer) { + if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY) + ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer); + else + ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level); + ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0); + const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x", + status, texture, level, nativeTarget); + return false; + } + Vector slice; + if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type, + TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) || + slice.size() != sliceBytes) return false; + Memcpy(result.data() + static_cast(layer) * sliceBytes, slice.data(), sliceBytes); + } + bytes = Move(result); + return true; + } + + // P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the + // twin's last sync. A re-mint asks this before reading a level back, because a level defined + // after that sync (null data, or null data plus a partial upload) is not on the driver yet and + // its FBO attachment is incomplete - which used to end the session at + // Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL. + Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, + GLint level, const IntVec3& logicalExtent) { + if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true; + const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent); + const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + GLenum binding = GL_NONE; + switch (nativeTarget) { + case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break; + case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break; + case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break; + case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break; + case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break; + default: return true; + } + // A cube's levels are per face; every other target answers for the whole level. + const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP + ? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget; + DrainESErrors(); + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(binding, &previous); + g_GLESFuncs.glBindTexture(nativeTarget, texture); + GLint width = 0, height = 0, depth = 0; + g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width); + g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height); + g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth); + const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glBindTexture(nativeTarget, static_cast(previous)); + if (!answered) return true; + const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || + nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY; + return width == extent.x() && height == extent.y() && (!layered || depth == extent.z()); + } + + // P8-E (docs/archive/notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=x, READ BY THE SERVER: + // a colour level of exactly that extent is treated as one the driver refused to attach - what an + // Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do. + // Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only + // the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it. + Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) { + static const IntVec3 s_refused = [] { + const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT"); + if (value == nullptr) return IntVec3{0, 0, 0}; + char* end = nullptr; + const long width = std::strtol(value, &end, 10); + if (end == value || *end != 'x') return IntVec3{0, 0, 0}; + const long height = std::strtol(end + 1, nullptr, 10); + if (width <= 0 || height <= 0) return IntVec3{0, 0, 0}; + return IntVec3{static_cast(width), static_cast(height), 1}; + }(); + return s_refused.x() > 0 && extent == s_refused; + } + + // P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store + // is the only CPU copy of the level on this side, and it answers only while its bytes ARE the + // level: every byte crossed (IsCovered), at the extent asked for, and the texture was never + // written by the driver except by copies the store followed (!IsDriverWritten - see + // StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is + // GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type + // takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row. + // Anything else DECLINES BY NAME: the reply is an error, and the client's + // Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has + // since overwritten. + static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat, + TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent, + GLenum format, GLenum type, Vector& bytes) { + auto& store = MG_Record::ServerStagedTexture(); + const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); + const auto target = static_cast(uploadTarget); + const auto mip = static_cast(level); + ReadbackChannelMapping mapping{}; + const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0; + const SizeT texels = static_cast(std::max(extent.x(), 0)) * static_cast(std::max(extent.y(), 0)) * + static_cast(std::max(extent.z(), 0)); + const char* declined = nullptr; + if (!store.Holds()) { + declined = "this arm keeps no staged store"; + } else if (store.IsDriverWritten(key)) { + declined = "the driver has written the texture since its bytes crossed"; + } else if (!store.IsCovered(key, target, mip)) { + declined = "not every byte of the level crossed"; + } else if (store.LevelExtentOrUndefined(key, target, mip) != extent) { + declined = "the staged level is not the extent asked for"; + } else if (pixelBytes == 0 || texels == 0) { + declined = "the format/type pair has no readback mapping"; + } + Vector out; + if (declined == nullptr) { + const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store"); + if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( + logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format), + MG_Util::ConvertGLEnumToTexturePixelDataType(type))) { + if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) { + declined = "the staged words are not the client's word size"; + } else { + out.assign(staged, staged + texels * pixelBytes); + } + } else { + Vector wide; + Bool isInteger = false; + Bool isSigned = false; + if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide, + isInteger, isSigned)) { + declined = "the staged format has no wide decoding"; + } else if (mapping.isInteger != isInteger) { + declined = "an integer read of a normalized level, or the reverse"; + } else { + const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT; + const SizeT rowTexels = static_cast(extent.x()); + const SizeT rows = texels / rowTexels; + const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType); + out.resize(texels * pixelBytes); + for (SizeT row = 0; row < rows; ++row) { + ClientReadback::ConvertWideReadbackRow(wide.data() + row * wideRowBytes, + out.data() + row * rowTexels * pixelBytes, rowTexels, + wideType, mapping, type); + } + } + } + } + if (declined != nullptr) { + MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store " + "cannot answer it (%s) - DECLINED (texture-readback-store-declined)", + res.Slot, res.Gen, level, declined); + return false; + } + MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); " + "answered from the server's staged store (texture-readback-from-store)", + res.Slot, res.Gen, level, static_cast(logicalFormat)); + bytes = Move(out); + return true; + } + + Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector& bytes) { + const auto* record = PipeTextureRecordForHandle(info.Res); + if (!record) return false; + TextureTarget target = TextureTarget::Unknown; + for (Int i = 0; i < static_cast(TextureTarget::TextureTargetCount); ++i) + if (MG_Pipe::MGPipeResourceTargetForTextureTarget(static_cast(i)) == record->Desc.Target) + target = static_cast(i); + if (target == TextureTarget::Unknown) return false; + const auto twin = TextureImpl::SyncTextureToBackendByHandle(info.Res); + if (!twin) return false; + const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP; + const IntVec3 extent{static_cast(info.Box.W), static_cast(info.Box.H), + wholeCube ? 1 : static_cast(info.Box.D)}; + const auto logicalFormat = static_cast(record->Desc.InternalFormat); + // The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is + // ReadTextureLevelTight's first choice for exactly these reads). + const Bool packedWordRead = + !twin->RequiresImageBindableStorage() && + MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( + logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), + MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) && + MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), + MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32); + const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent); + Vector result; + for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) { + const auto uploadTarget = wholeCube + ? static_cast(static_cast(TextureUploadTarget::CubeMapPositiveX) + face) + : MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target); + Vector slice; + if (refusedForTesting || + !ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level, + extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) { + if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent, + info.Format, info.Type, slice)) return false; + } + result.insert(result.end(), slice.begin(), slice.end()); + } + bytes = Move(result); + return true; + } + + static Bool IsLegacyNativeReadPixelsFormat(GLenum format) { + return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER || + format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL; + } + + static Bool IsLegacyNativeReadPixelsType(GLenum type) { + // GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV are only ever valid + // paired with GL_DEPTH_STENCIL (packed_depth_stencil.verify_read_pixels); the real + // driver already implements this readback natively. + return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV || + type == GL_INT || type == GL_FLOAT || type == GL_UNSIGNED_INT_24_8 || + type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + } + + // The (format, type) pairs the depth/stencil helpers below can service. They are not + // covered by the colour tables above - GetReadbackChannelMapping has no entry for any + // depth or stencil format, so without this gate a read the helpers CAN serve (a + // GL_UNSIGNED_SHORT depth, a GL_SHORT stencil) is turned away before it reaches them. + // The signed widths and GL_HALF_FLOAT belong in it for the same reason: the helpers scale a + // depth into 127 / 32767 / 2147483647 for the signed ones and hand GL_HALF_FLOAT the value + // as a half word, so a pair listed here is one they answer in full. + static Bool IsSupportedDepthStencilReadPixelsPair(GLenum format, GLenum type) { + switch (format) { + case GL_DEPTH_COMPONENT: + return type == GL_BYTE || type == GL_UNSIGNED_BYTE || type == GL_SHORT || + type == GL_UNSIGNED_SHORT || type == GL_INT || type == GL_UNSIGNED_INT || + type == GL_FLOAT || type == GL_HALF_FLOAT; + case GL_STENCIL_INDEX: + return type == GL_UNSIGNED_BYTE || type == GL_BYTE || type == GL_UNSIGNED_SHORT || type == GL_SHORT || + type == GL_UNSIGNED_INT || type == GL_INT || type == GL_FLOAT || type == GL_HALF_FLOAT; + case GL_DEPTH_STENCIL: + return type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + default: + return false; + } + } + + void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { + MGLOG_D("ReadPixels: x=%d y=%d w=%d h=%d format=%s type=%s pixels=%p", x, y, width, height, + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels); +#if MOBILEGL_BUILD_DISAGGREGATED + // A reset context reads back nothing it rendered: the session is latched and the read declined. + if (LatchIfDeviceLost("read-pixels")) return; +#endif + + // Combinations the ES driver has always handled directly keep the native path; other color layouts go + // through the wide-format conversion path. Anything still uncovered degrades to a logged no-op instead + // of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer. + const Bool useNativeReadback = (IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type)) || + IsSupportedDepthStencilReadPixelsPair(format, type); + ReadbackChannelMapping conversionMapping{}; + const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && + GetReadbackDstPixelSize(conversionMapping, type) != 0; + if (!useNativeReadback && !convertible) { + MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return; + } + + MGLOG_D("ReadPixels: SyncNeccessaryTextures()"); + TextureImpl::SyncNeccessaryTextures(); + + MGLOG_D("ReadPixels: SyncCurrentFBO()"); + FramebufferImpl::SyncCurrentFBO(); + + MGLOG_D("ReadPixels: BindCurrentFBO(Read)"); + BindCurrentFBO(FramebufferTarget::Read); + + MGLOG_D("ReadPixels: Applying the PACK pixel-store scope"); + ScopedPackState packParamsScope(PackStateFromContext()); + + GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); + MGLOG_D("ReadPixels: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str()); + + if (fbStatus != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E_ONCE("ReadPixels: bound READ FBO is not complete"); + return; + } + // ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the + // matching attachment class; every other convertible color layout (including GL_RGBA/GL_FLOAT + // and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type + // the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so + // it always takes the conversion path (which swaps on the CPU). + const Bool packSwapBytes = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false).SwapBytes; + // The read buffer is what glReadPixels reads, so the frontend's READ binding is exactly + // the right thing to ask here. + const Bool forceOpaqueAlpha = FramebufferImpl::IsAlphaWidenedFallbackReadAttachment(); + // An attachment widened from three channels to stay colour-renderable also has to leave + // the fast pair: only the conversion path knows to answer its alpha with the 1.0 the + // application's format implies instead of whatever the draw wrote into the added channel. + const Bool nativeFastPair = !packSwapBytes && !forceOpaqueAlpha && + ((format == GL_RGBA && type == GL_UNSIGNED_BYTE) || + (format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT))); + if (convertible && !nativeFastPair) { + if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels, + /*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true, + forceOpaqueAlpha)) { + MGLOG_D("ReadPixels: finished via client-format conversion"); + return; + } + MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); + return; + } + // Every depth and stencil read goes through the helpers, not just the widening ones: + // ES has no guaranteed readback for either aspect, so even a byte-for-byte case + // needs the fallback chain (the other native spelling, then the sampling emulation) + // on a driver without GL_NV_read_depth / GL_NV_read_stencil. + if (format == GL_DEPTH_COMPONENT && ReadPixelsDepthComponent(x, y, width, height, type, pixels)) { + MGLOG_D("ReadPixels: finished via depth readback helper"); + return; + } + if (format == GL_STENCIL_INDEX && ReadPixelsStencilViaNative(x, y, width, height, type, pixels)) { + MGLOG_D("ReadPixels: finished via stencil readback helper"); + return; + } + if (format == GL_DEPTH_STENCIL && ReadPixelsDepthStencilPacked(x, y, width, height, type, pixels)) { + MGLOG_D("ReadPixels: finished via packed depth/stencil readback helper"); + return; + } + + // Handle PBO. The pack binding is scoped: it returns to the resting 0 state + // on every exit path, so a later readback can never land in a stale PBO + // (the driver-level binding used to stay on the user PBO after this call, + // capturing subsequent client-memory readbacks into it). + auto& pixelPackBufferObject = + SplitReadbackPackBuffer(); + Bool usePBO = false; + GLuint packBufferId = 0; + // P13: SplitReadbackPackBuffer() is always null - a pack buffer reaches the readback + // by handle on the verb path - so this entry never packs into a buffer. + MGLOG_D("ReadPixels: Not using PBO"); + ScopedPixelPackBuffer packBufferBinding(packBufferId); + + MGLOG_D("ReadPixels: glReadPixels()"); + DrainESErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, format, type, pixels); + const GLenum nativeReadError = g_GLESFuncs.glGetError(); + if (nativeReadError != GL_NO_ERROR) { + // ES drivers only guarantee GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/(U)INT, float RGBA and one + // implementation-defined pair; legacy combos like GL_RED/GL_UNSIGNED_INT are rejected by e.g. + // Adreno with a GL error and an untouched destination (GL CTS packed_pixels r8_format_red). The + // failed read wrote nothing (client memory and PBO alike), so re-service the request through the + // wide-format conversion path before any PBO writeback can capture stale contents. The conversion + // helper saves/restores the ES pixel-pack binding and handles the state-layer PBO itself. + DrainESErrors(); + MGLOG_D("ReadPixels: native read of %s/%s failed (%s), retrying via client-format conversion", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), + MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); + if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels, + /*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true, + forceOpaqueAlpha)) { + MGLOG_D("ReadPixels: finished via client-format conversion after native failure"); + return; + } + MGLOG_E_ONCE("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, " + "skipping readback", + MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), + MG_Util::ConvertGLEnumToString(nativeReadError).c_str()); + return; + } + if (usePBO) { + // pull back to client memory if PBO is used + MGLOG_D("ReadPixels: PBO used, mapping buffer to client memory"); + GLvoid* pboMappedPtr = g_GLESFuncs.glMapBufferRange( + GL_PIXEL_PACK_BUFFER, 0, (GLsizeiptr)pixelPackBufferObject->GetSize(), GL_MAP_READ_BIT); + if (pboMappedPtr) { + MGLOG_D("ReadPixels: Copying data from PBO to client memory"); + SizeT size = pixelPackBufferObject->GetSize(); + pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0); + // Serial bumped with no backend op; re-open the buffer draw-clean memos. + BufferImpl::BumpBufferMutationEpoch(); + MGLOG_D("ReadPixels: Unmapping PBO"); + g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + } else { + MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr"); + } + } + MGLOG_D("ReadPixels: finished"); + } + + void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs) { + g_EGLFuncs = eglFuncs; + } + + void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs) { + g_GLESFuncs = glesFuncs; + } + + void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities) { + g_GLESCapabilities = capabilities; + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp new file mode 100644 index 00000000..c9cb8e1d --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp @@ -0,0 +1,887 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { +#if MOBILEGL_BUILD_DISAGGREGATED + // ---- SHARED IMAGES (DirectGLES.h; docs/archive/notes/anland/plan-ahb-dmabuf.md) ------ + + namespace SharedImageImpl { +#if defined(__ANDROID__) + namespace { + namespace SI = MG_Remote::Server::SharedImages; + + // EGL_ANDROID_image_native_buffer's target and EGL_KHR_image_base's attribute, spelled + // here so no extension header is needed. + constexpr EGLenum kEglNativeBufferAndroid = 0x3140; + constexpr EGLint kEglImagePreservedKhr = 0x30D2; + + using GetNativeClientBufferFn = EGLClientBuffer (*)(const AHardwareBuffer*); + using CreateImageKhrFn = void* (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*); + using DestroyImageKhrFn = EGLBoolean (*)(EGLDisplay, void*); + using ImageTargetFn = void (*)(GLenum, void*); + + // EGL_KHR_fence_sync / EGL_KHR_wait_sync / EGL_ANDROID_native_fence_sync, likewise. + constexpr EGLenum kEglSyncNativeFenceAndroid = 0x3144; + constexpr EGLint kEglSyncNativeFenceFdAndroid = 0x3145; + constexpr EGLint kEglConditionSatisfiedKhr = 0x30F6; + using CreateSyncKhrFn = void* (*)(EGLDisplay, EGLenum, const EGLint*); + using DestroySyncKhrFn = EGLBoolean (*)(EGLDisplay, void*); + using WaitSyncKhrFn = EGLint (*)(EGLDisplay, void*, EGLint); + using ClientWaitSyncKhrFn = EGLint (*)(EGLDisplay, void*, EGLint, Uint64); + using DupNativeFenceFdFn = EGLint (*)(EGLDisplay, void*); + + struct EntryPoints { + Bool Resolved = false; + Bool Usable = false; + String Why; + GetNativeClientBufferFn GetNativeClientBuffer = nullptr; + CreateImageKhrFn CreateImage = nullptr; + DestroyImageKhrFn DestroyImage = nullptr; + ImageTargetFn TargetTexture2D = nullptr; + ImageTargetFn TargetRenderbuffer = nullptr; + // Fences as sync_files: what lets one session's GPU wait for another's without a + // CPU in between. Without them every wait below is a CPU wait. + Bool FencesUsable = false; + CreateSyncKhrFn CreateSync = nullptr; + DestroySyncKhrFn DestroySync = nullptr; + WaitSyncKhrFn WaitSync = nullptr; + ClientWaitSyncKhrFn ClientWaitSync = nullptr; + DupNativeFenceFdFn DupNativeFenceFd = nullptr; + // YUV images: sampled as external textures (GL_OES_EGL_image_external_essl3), and + // through GL_EXT_YUV_target's raw Y'CbCr sampler where the driver has it, so the + // conversion is the import's own (its colour hints) rather than the driver's guess. + Bool ExternalEssl3 = false; + Bool YuvTarget = false; + }; + + // None of these is in the loader's tables, and eglGetProcAddress hands back live-looking + // stubs for entry points a driver lacks, so the extension strings decide first. Resolved + // once, from the first call that has a context current to read the strings from; the + // answer is never modified afterwards, so it is read without the lock. + const EntryPoints& ResolvedEntryPoints() { + static auto* mutex = new std::mutex(); + static auto* entry = new EntryPoints(); + const std::lock_guard lock(*mutex); + if (entry->Resolved) return *entry; + if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_GLESFuncs.glGetString) { + entry->Why = "the loader has no eglGetProcAddress/eglQueryString/glGetString"; + return *entry; + } + const char* egl = g_Display != EGL_NO_DISPLAY ? g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS) + : nullptr; + const char* gl = reinterpret_cast(g_GLESFuncs.glGetString(GL_EXTENSIONS)); + if (egl == nullptr || gl == nullptr) { + entry->Why = "no current display and context to read the extension strings from"; + return *entry; // asked again on the next call + } + entry->Resolved = true; + if (HasExtensionWord(egl, "EGL_KHR_fence_sync") && HasExtensionWord(egl, "EGL_KHR_wait_sync") && + HasExtensionWord(egl, "EGL_ANDROID_native_fence_sync")) { + entry->CreateSync = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglCreateSyncKHR")); + entry->DestroySync = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglDestroySyncKHR")); + entry->WaitSync = reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglWaitSyncKHR")); + entry->ClientWaitSync = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglClientWaitSyncKHR")); + entry->DupNativeFenceFd = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("eglDupNativeFenceFDANDROID")); + entry->FencesUsable = entry->CreateSync && entry->DestroySync && entry->WaitSync && + entry->ClientWaitSync && entry->DupNativeFenceFd; + } + if (!entry->FencesUsable) { + MGLOG_I("Shared images: no EGL native fence sync / wait sync; sessions order their access " + "to an image with CPU waits"); + } + if (!HasExtensionWord(egl, "EGL_KHR_image_base") || + !HasExtensionWord(egl, "EGL_ANDROID_image_native_buffer") || + !HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) { + entry->Why = "EGL_KHR_image_base / EGL_ANDROID_image_native_buffer / " + "EGL_ANDROID_get_native_client_buffer is not advertised"; + return *entry; + } + if (!HasExtensionWord(gl, "GL_OES_EGL_image")) { + entry->Why = "GL_OES_EGL_image is not advertised"; + return *entry; + } + entry->ExternalEssl3 = HasExtensionWord(gl, "GL_OES_EGL_image_external_essl3"); + entry->YuvTarget = HasExtensionWord(gl, "GL_EXT_YUV_target"); + entry->GetNativeClientBuffer = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID")); + entry->CreateImage = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR")); + entry->DestroyImage = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR")); + entry->TargetTexture2D = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("glEGLImageTargetTexture2DOES")); + entry->TargetRenderbuffer = reinterpret_cast( + g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES")); + if (!entry->GetNativeClientBuffer || !entry->CreateImage || !entry->DestroyImage || + !entry->TargetTexture2D || !entry->TargetRenderbuffer) { + entry->Why = "eglGetProcAddress gave no address for an advertised EGLImage entry point"; + return *entry; + } + entry->Usable = true; + return *entry; + } + + void DrainErrors() { + for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) { + } + } + } // namespace +#endif + + struct EglImage { +#if defined(__ANDROID__) + SI::ImageRef Image; + void* Handle = nullptr; + EGLDisplay Display = EGL_NO_DISPLAY; + Uint64 DisplayTerminations = 0; + DestroyImageKhrFn Destroy = nullptr; + // AcquireForSampling's memory, touched only by the session whose twin holds this: the + // write generation last waited for and the native context the wait was queued in (a + // GPU wait orders that context alone), and the read frame the image was last noted in. + Uint64 WaitedGeneration = 0; + Uint64 WaitedContext = 0; + Uint64 NotedFrame = 0; + // A YUV image (ConvertYuvForSampling): an external texture over the EGLImage, the source + // of the conversion into the texture's own RGBA storage, and the reader's frame the + // conversion last ran in. + GLuint ExternalTexture = 0; + Uint64 ConvertedFrame = 0; + + ~EglImage() { + // eglDestroyImageKHR needs the display, not a context, so a session's twin teardown + // (no context current) still releases the image; only exit() and a terminated + // display - which destroyed the image itself - skip it. + if (Handle == nullptr || Destroy == nullptr || InProcessExit()) return; + if (g_displayTerminations.load(std::memory_order_acquire) != DisplayTerminations) return; + // Texture names are the share group's: deleting one needs any context of it, and + // without one current the name goes with the display. + if (ExternalTexture != 0 && t_native.boundContext != EGL_NO_CONTEXT) + g_GLESFuncs.glDeleteTextures(1, &ExternalTexture); + Destroy(Display, Handle); + } +#endif + }; + +#if defined(__ANDROID__) + EglImageRef CreateEglImage(Uint64 id, String& why) { + const EntryPoints& entry = ResolvedEntryPoints(); + if (!entry.Usable) { + why = entry.Why; + return nullptr; + } + SI::ImageRef image = SI::Find(id); + if (image == nullptr) { + why = "no live shared image has this id"; + return nullptr; + } + if (image->Native == nullptr) { + why = "the image has no AHardwareBuffer"; + return nullptr; + } + const EGLClientBuffer buffer = + entry.GetNativeClientBuffer(static_cast(image->Native)); + if (buffer == nullptr) { + why = "eglGetNativeClientBufferANDROID returned null"; + return nullptr; + } + const EGLint attribs[] = {kEglImagePreservedKhr, EGL_TRUE, EGL_NONE}; + void* handle = entry.CreateImage(g_Display, EGL_NO_CONTEXT, kEglNativeBufferAndroid, buffer, attribs); + if (handle == nullptr) { + char text[96]; + std::snprintf(text, sizeof(text), "eglCreateImageKHR failed (EGL error 0x%04x)", + g_EGLFuncs.eglGetError ? static_cast(g_EGLFuncs.eglGetError()) : 0u); + why = text; + return nullptr; + } + auto result = MakeShared(); + result->Image = std::move(image); + result->Handle = handle; + result->Display = g_Display; + result->DisplayTerminations = g_displayTerminations.load(std::memory_order_acquire); + result->Destroy = entry.DestroyImage; + return result; + } + + Bool TargetBoundTexture2D(const EglImageRef& image) { + const EntryPoints& entry = ResolvedEntryPoints(); + if (!entry.Usable || image == nullptr || image->Handle == nullptr) return false; + DrainErrors(); + entry.TargetTexture2D(GL_TEXTURE_2D, image->Handle); + return g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + + namespace { + // ONE PRESENT TARGET PER IMAGE, PROCESS-WIDE: the EGLImage and a renderbuffer whose + // storage it is. Every native context is in one share group, so the renderbuffer is + // one object for every session's context; only the framebuffer it is attached to is + // per context. Kept until no session holds the image any more (pruned on every present + // - the cache's own reference is then the only one), with a small cap so a client that + // cycles through many images cannot pile them up between presents. + struct PresentTarget { + EglImageRef Image; + GLuint Renderbuffer = 0; + Uint64 LastUse = 0; + // A present has written the image through this target (its content is a frame). + Bool Written = false; + }; + struct PresentTargets { + std::mutex Mutex; + std::unordered_map ById; + Uint64 UseClock = 0; + }; + constexpr SizeT kMaxPresentTargets = 8; + + PresentTargets& Targets() { + static auto* targets = new PresentTargets(); + return *targets; + } + + // The framebuffer a present draws through, one per native context (container objects + // are never shared). It carries an attachment only for the duration of a present, so + // no context's framebuffer keeps a dropped target's renderbuffer alive. + PerNativeContext g_presentFramebuffers; + + Bool TargetIsCurrent(const PresentTarget& target) { + return target.Image != nullptr && + target.Image->DisplayTerminations == g_displayTerminations.load(std::memory_order_acquire); + } + + void DeleteTarget(PresentTarget& target) { + if (target.Renderbuffer != 0 && TargetIsCurrent(target)) { + g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer); + } + target.Renderbuffer = 0; + target.Image = nullptr; + } + + // Caller holds the mutex and has a context of the share group current. + void PruneLocked(PresentTargets& targets) { + for (auto it = targets.ById.begin(); it != targets.ById.end();) { + if (!TargetIsCurrent(it->second) || it->second.Image->Image.use_count() <= 1) { + DeleteTarget(it->second); + it = targets.ById.erase(it); + } else { + ++it; + } + } + } + + GLuint MakeRenderbuffer(const EglImage& image, String& why) { + GLuint renderbuffer = 0; + g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer); + if (renderbuffer == 0) { + why = "glGenRenderbuffers gave no name"; + return 0; + } + // Espryt keeps no renderbuffer-binding shadow; put the driver's binding back as found. + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); + DrainErrors(); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + ResolvedEntryPoints().TargetRenderbuffer(GL_RENDERBUFFER, image.Handle); + const GLenum error = g_GLESFuncs.glGetError(); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); + if (error != GL_NO_ERROR) { + char text[96]; + std::snprintf(text, sizeof(text), "glEGLImageTargetRenderbufferStorageOES -> 0x%04x", + static_cast(error)); + why = text; + g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer); + return 0; + } + return renderbuffer; + } + + // Everything the current context has submitted, waited out on the CPU: the fallback for + // a driver that cannot hand a fence to another session. + Bool WaitForCompletion() { + if (g_GLESFuncs.glFenceSync && g_GLESFuncs.glClientWaitSync && g_GLESFuncs.glDeleteSync) { + if (GLsync fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) { + constexpr GLuint64 kSliceNs = 1000ull * 1000 * 1000; + GLenum status = GL_TIMEOUT_EXPIRED; + GLbitfield flags = GL_SYNC_FLUSH_COMMANDS_BIT; + for (Uint slice = 0; slice < 10 && status == GL_TIMEOUT_EXPIRED; ++slice) { + status = g_GLESFuncs.glClientWaitSync(fence, flags, kSliceNs); + flags = 0; + } + g_GLESFuncs.glDeleteSync(fence); + if (status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) return true; + if (status == GL_TIMEOUT_EXPIRED) { + MGLOG_E("Shared images: the submitted work did not complete within 10 s"); + return false; + } + // GL_WAIT_FAILED: glFinish is the same promise without the object. + } + } + g_GLESFuncs.glFinish(); + return true; + } + + // How long a CPU wait on another session's fence may take before the order is given up + // on (logged): a session whose GPU work never completes must not wedge its peers. + constexpr Uint32 kFenceCpuWaitMs = 2000; + + void CloseFd(int fd) { + if (fd >= 0) ::close(fd); + } + + // The current context's LATER commands wait for `fence` (a sync_file) on the GPU; a CPU + // wait when the driver cannot. Takes ownership of `fence`: a native fence sync owns it + // from a successful create on, every other path closes it here. + void WaitForFenceBeforeLaterCommands(int fence) { + if (fence < 0) return; + const EntryPoints& entry = ResolvedEntryPoints(); + if (entry.FencesUsable) { + const EGLint attribs[] = {kEglSyncNativeFenceFdAndroid, fence, EGL_NONE}; + if (void* sync = entry.CreateSync(g_Display, kEglSyncNativeFenceAndroid, attribs)) { + if (entry.WaitSync(g_Display, sync, 0) != EGL_TRUE) { + constexpr Uint64 kWaitNs = Uint64{kFenceCpuWaitMs} * 1000 * 1000; + if (entry.ClientWaitSync(g_Display, sync, 0, kWaitNs) != kEglConditionSatisfiedKhr) { + MGLOG_E_ONCE("Shared images: a peer session's fence could not be waited for"); + } + } + // A pending server wait keeps the sync alive past this (EGL_KHR_wait_sync). + entry.DestroySync(g_Display, sync); + return; + } + } + if (!SI::SyncFile::Wait(fence, kFenceCpuWaitMs)) { + MGLOG_E_ONCE("Shared images: a peer session's fence did not signal within %u ms", + static_cast(kFenceCpuWaitMs)); + } + CloseFd(fence); + } + + // A sync_file for everything the current context has submitted so far (flushed, so it + // will signal), in `*fence`. False when the driver cannot export one; the caller then + // waits on the CPU and publishes -1. + Bool ExportFence(int* fence) { + *fence = -1; + const EntryPoints& entry = ResolvedEntryPoints(); + if (!entry.FencesUsable) return false; + const EGLint attribs[] = {EGL_NONE}; + void* sync = entry.CreateSync(g_Display, kEglSyncNativeFenceAndroid, attribs); + if (sync == nullptr) return false; + // The fence has no descriptor until its command reaches the driver's queue. + g_GLESFuncs.glFlush(); + const EGLint fd = entry.DupNativeFenceFd(g_Display, sync); + entry.DestroySync(g_Display, sync); + if (fd < 0) return false; + *fence = fd; + return true; + } + + // ONE READ FRAME PER SESSION: what the session has sampled since its last frame + // boundary. A session is served by exactly one apply thread, so the thread owns it - + // nothing about it is process-wide, and a session's end (its thread's) drops it. `Id` + // is drawn from one counter across all sessions and renewed at every boundary, so an + // EglImage's NotedFrame is a once-per-frame gate no other session's frame can match. + struct ReadFrame { + Uint64 Id = 0; + SI::ReadTracker Tracker; + // The session ends frames at publishing flushes (PublishPendingAccesses): it writes + // images, so its first use of one in a frame waits for that image's pending reads. + Bool ImplicitSync = false; + // When a YUV image is converted again (ConvertYuvForSampling): renewed at EVERY + // boundary - present, publishing flush, and each native fence whatever the session + // is - since an offscreen producer (a browser's GPU process) may have no other. + Uint64 YuvEpoch = 0; + }; + std::atomic g_readFrameIds{0}; + + ReadFrame& CurrentReadFrame() { + thread_local ReadFrame frame; + if (frame.Id == 0) frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + if (frame.YuvEpoch == 0) frame.YuvEpoch = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + return frame; + } + + void RenewYuvEpoch(ReadFrame& frame) { + frame.YuvEpoch = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + } + } // namespace + + void AcquireForSampling(const EglImageRef& image) { + if (image == nullptr || image->Image == nullptr) return; + EglImage& state = *image; + // Steady state: one lock and a compare (the write fence, once signaled, is dropped by + // the registry and nothing is duplicated any more). + Uint64 generation = 0; + int fence = SI::DupWriteFence(*state.Image, &generation); + const Uint64 context = CurrentNativeContextSerial(); + if (generation != state.WaitedGeneration || context != state.WaitedContext) { + state.WaitedGeneration = generation; + state.WaitedContext = context; + WaitForFenceBeforeLaterCommands(fence); + fence = -1; + } + CloseFd(fence); + ReadFrame& frame = CurrentReadFrame(); + if (state.NotedFrame != frame.Id) { + // A session that renders into images (it ends frames with a publishing flush) may be + // about to overwrite this one: what other sessions still sample goes first. + if (frame.ImplicitSync) { + for (const int read : SI::DupPendingReadFences(*state.Image)) WaitForFenceBeforeLaterCommands(read); + } + frame.Tracker.NoteRead(state.Image); + state.NotedFrame = frame.Id; + } + } + + Bool IsYuv(const EglImageRef& image) { + return image != nullptr && image->Image != nullptr && SI::FourccIsYuv(image->Image->Fourcc); + } + + namespace { + // THE CONVERSION PASS: one triangle over the texture, sampling the image through an + // external texture. Programs are the share group's and rebuilt with a new context + // generation; the framebuffer and vertex array are per context (never shared). + struct YuvConversion { + Uint ContextGeneration = ~0u; + GLuint ExternalProgram = 0; // the driver's conversion (samplerExternalOES) + GLuint TargetProgram = 0; // the import's own (GL_EXT_YUV_target) + GLint TargetMatrix = -1; + GLint TargetOffset = -1; + Bool Failed = false; + NativeContextName Framebuffer; + NativeContextName VertexArray; + }; + YuvConversion& Conversion() { + static auto* conversion = new YuvConversion(); + return *conversion; + } + + constexpr const char* kYuvVertexSource = + "#version 300 es\n" + "out vec2 vUv;\n" + "void main() {\n" + " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" + " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" + " vUv = p;\n" + "}\n"; + constexpr const char* kYuvExternalFragmentSource = + "#version 300 es\n" + "#extension GL_OES_EGL_image_external_essl3 : require\n" + "precision highp float;\n" + "uniform samplerExternalOES uSource;\n" + "in vec2 vUv;\n" + "out vec4 oColor;\n" + "void main() { oColor = vec4(texture(uSource, vUv).rgb, 1.0); }\n"; + constexpr const char* kYuvTargetFragmentSource = + "#version 300 es\n" + "#extension GL_EXT_YUV_target : require\n" + "precision highp float;\n" + "uniform __samplerExternal2DY2YEXT uSource;\n" + "uniform mat3 uMatrix;\n" + "uniform vec3 uOffset;\n" + "in vec2 vUv;\n" + "out vec4 oColor;\n" + "void main() {\n" + " vec3 ycbcr = texture(uSource, vUv).xyz;\n" + " oColor = vec4(clamp(uMatrix * (ycbcr - uOffset), 0.0, 1.0), 1.0);\n" + "}\n"; + + GLuint BuildYuvProgram(const char* fragmentSource) { + const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER); + const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER); + GLint linked = GL_FALSE; + GLuint program = 0; + if (vertexShader != 0 && fragmentShader != 0) { + g_GLESFuncs.glShaderSource(vertexShader, 1, &kYuvVertexSource, nullptr); + g_GLESFuncs.glCompileShader(vertexShader); + g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); + g_GLESFuncs.glCompileShader(fragmentShader); + program = g_GLESFuncs.glCreateProgram(); + if (program != 0) { + g_GLESFuncs.glAttachShader(program, vertexShader); + g_GLESFuncs.glAttachShader(program, fragmentShader); + g_GLESFuncs.glLinkProgram(program); + g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); + } + } + if (vertexShader != 0) g_GLESFuncs.glDeleteShader(vertexShader); + if (fragmentShader != 0) g_GLESFuncs.glDeleteShader(fragmentShader); + if (linked != GL_TRUE) { + if (program != 0) g_GLESFuncs.glDeleteProgram(program); + return 0; + } + return program; + } + + // MOBILEGL_YUV_DRIVER_CONVERSION=1: always the driver's conversion (samplerExternalOES), + // even where GL_EXT_YUV_target would let the import's hints decide. + Bool DriverConversionForced() { + static const Bool forced = [] { + const char* value = std::getenv("MOBILEGL_YUV_DRIVER_CONVERSION"); + return value != nullptr && value[0] == '1'; + }(); + return forced; + } + } // namespace + + void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height) { + if (!IsYuv(image) || target == 0 || width == 0 || height == 0) return; + EglImage& state = *image; + ReadFrame& frame = CurrentReadFrame(); + if (state.ConvertedFrame == frame.YuvEpoch) return; + // Whatever happens below, this frame does not try again. + state.ConvertedFrame = frame.YuvEpoch; + const EntryPoints& entry = ResolvedEntryPoints(); + if (!entry.Usable || !entry.ExternalEssl3) { + MGLOG_E_ONCE("Shared images: a YUV image cannot be sampled here (%s)", + entry.Usable ? "no GL_OES_EGL_image_external_essl3" : entry.Why.c_str()); + return; + } + // THE CPU-COPY FALLBACK, for a buffer this server did not allocate (SharedImageRegistry.h). + String why; + if (state.Image->IsForeign() && !SI::RefreshForeign(*state.Image, why)) { + MGLOG_E_ONCE("Shared images: foreign YUV image %llu could not be refreshed: %s", + static_cast(state.Image->Id), why.c_str()); + } + + YuvConversion& conversion = Conversion(); + if (conversion.ContextGeneration != g_backendContextGeneration) { + conversion.ExternalProgram = 0; + conversion.TargetProgram = 0; + conversion.TargetMatrix = conversion.TargetOffset = -1; + conversion.Failed = false; + conversion.ContextGeneration = g_backendContextGeneration; + } + if (conversion.Failed) return; + const Bool ownConversion = entry.YuvTarget && !DriverConversionForced(); + GLuint& program = ownConversion ? conversion.TargetProgram : conversion.ExternalProgram; + if (program == 0) { + program = BuildYuvProgram(ownConversion ? kYuvTargetFragmentSource : kYuvExternalFragmentSource); + if (program == 0) { + conversion.Failed = true; + MGLOG_E_ONCE("Shared images: the YUV conversion program could not be built"); + return; + } + if (ownConversion) { + conversion.TargetMatrix = g_GLESFuncs.glGetUniformLocation(program, "uMatrix"); + conversion.TargetOffset = g_GLESFuncs.glGetUniformLocation(program, "uOffset"); + } + MGLOG_I("Shared images: YUV images convert through %s", + ownConversion ? "GL_EXT_YUV_target with the import's colour hints" + : "the driver's external-sampler conversion"); + } + if (conversion.Framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &conversion.Framebuffer); + if (conversion.VertexArray == 0) g_GLESFuncs.glGenVertexArrays(1, &conversion.VertexArray); + if (conversion.Framebuffer == 0 || conversion.VertexArray == 0) return; + + // Every binding goes through Espryt's shadows or a guard that puts it back. + ScopedEmulationDrawState drawState; // also activates TextureImpl::TempTextureUnit + ScopedFramebufferBinding binding(/*saveRead=*/false, /*saveDraw=*/true); + DrainErrors(); + if (state.ExternalTexture == 0) { + g_GLESFuncs.glGenTextures(1, &state.ExternalTexture); + g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, state.ExternalTexture); + entry.TargetTexture2D(GL_TEXTURE_EXTERNAL_OES, state.Handle); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + const GLenum error = g_GLESFuncs.glGetError(); + if (error != GL_NO_ERROR) { + MGLOG_E_ONCE("Shared images: YUV image %llu cannot be bound as an external texture (0x%04x)", + static_cast(state.Image->Id), static_cast(error)); + } + } else { + g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, state.ExternalTexture); + } + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, conversion.Framebuffer); + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target, 0); + const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); + if (status == GL_FRAMEBUFFER_COMPLETE) { + VertexArrayImpl::BindBackendVAOId(conversion.VertexArray); + g_GLESFuncs.glUseProgram(program); + g_GLESFuncs.glUniform1i(g_GLESFuncs.glGetUniformLocation(program, "uSource"), + static_cast(TextureImpl::TempTextureUnit)); + if (ownConversion) { + // A Y2Y sample is the stored code normalized as the format stores it: 8 bits, or + // P010's 10 in the high end of 16. + const double sampleMax = state.Image->Fourcc == SI::kFourccP010 ? 65535.0 / 64.0 : 255.0; + const auto c = SI::Yuv::ConversionFor(SI::GetYuvHints(*state.Image), sampleMax); + g_GLESFuncs.glUniformMatrix3fv(conversion.TargetMatrix, 1, GL_FALSE, c.Matrix); + g_GLESFuncs.glUniform3fv(conversion.TargetOffset, 1, c.Offset); + } + g_GLESFuncs.glViewport(0, 0, static_cast(width), static_cast(height)); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + const GLenum error = g_GLESFuncs.glGetError(); + if (error != GL_NO_ERROR) { + MGLOG_E_ONCE("Shared images: the YUV conversion draw failed (0x%04x)", static_cast(error)); + } + } else { + MGLOG_E_ONCE("Shared images: a YUV conversion target is not renderable (0x%04x)", + static_cast(status)); + } + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); + } + + Bool PublishPendingAccesses() { + ReadFrame& frame = CurrentReadFrame(); + frame.ImplicitSync = true; + RenewYuvEpoch(frame); + // Sub-data waiting for a sync that nothing of this session would otherwise trigger. + if (t_native.boundContext != EGL_NO_CONTEXT) TextureImpl::WritePendingSharedImageUploads(); + if (frame.Tracker.Pending() == 0) return true; + int fence = -1; + if (t_native.boundContext != EGL_NO_CONTEXT && !ExportFence(&fence)) { + // No fence to publish: the work is waited out here, and published as completed. + (void)WaitForCompletion(); + } + frame.Tracker.PublishFrameAsWrite(fence); + frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + return true; + } + + Bool ExportNativeFence(int* fence) { + *fence = -1; + ReadFrame& frame = CurrentReadFrame(); + RenewYuvEpoch(frame); + // No context current: nothing of this session is in flight to fence. + if (t_native.boundContext == EGL_NO_CONTEXT) return true; + TextureImpl::WritePendingSharedImageUploads(); + int exported = -1; + if (!ExportFence(&exported)) { + // No descriptor to give: the work is waited out, so the caller may report it done. + exported = -1; + (void)WaitForCompletion(); + } + // A session that writes images (one that ends frames at flushes) ends one here too; a + // compositor's reads stay for its swap, which comes after the fence's work anyway. + if (frame.ImplicitSync && frame.Tracker.Pending() != 0) { + frame.Tracker.PublishFrameAsWrite(exported >= 0 ? ::dup(exported) : -1); + frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + } + *fence = exported; + return true; + } + + void PublishPendingReads() { + ReadFrame& frame = CurrentReadFrame(); + RenewYuvEpoch(frame); + if (frame.Tracker.Pending() == 0) return; + int fence = -1; + // No context current: the reads' context is gone (and its work with it), and there is + // nothing to fence or wait from - the frame is only forgotten. + if (t_native.boundContext != EGL_NO_CONTEXT && !ExportFence(&fence)) { + // FALLBACK: there is no fence to hand a writer, so the frame's reads are waited out + // here instead - once this session moves on (and, say, releases the buffer to its + // producer), nothing it submitted still samples the image. + (void)WaitForCompletion(); + } + // A session that writes images publishes every boundary as a write: an image it rendered + // into must not leave the frame without its generation moving. + if (frame.ImplicitSync) + frame.Tracker.PublishFrameAsWrite(fence); + else + frame.Tracker.PublishFrame(fence); + frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + } + + Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) { + if (view.Id == 0 || view.Width == 0 || view.Height == 0 || view.NativeBuffer == nullptr) return false; + if (!IsBackendContextCurrentOnThisThread()) { + MGLOG_E_ONCE("Shared image present: no backend context is current on the calling thread"); + return false; + } + if (LatchIfDeviceLost("shared-image-present")) return false; + const EGLSurface surface = CurrentContextDrawSurface(); + EGLint sourceWidth = 0; + EGLint sourceHeight = 0; + if (surface == EGL_NO_SURFACE || !g_EGLFuncs.eglQuerySurface || + !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &sourceWidth) || + !g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &sourceHeight) || sourceWidth <= 0 || + sourceHeight <= 0) { + MGLOG_E_ONCE("Shared image present: the current context has no default framebuffer to copy"); + return false; + } + const SI::ImageRef image = SI::Find(view.Id); + if (image == nullptr) { + MGLOG_E_ONCE("Shared image present: image %llu is no longer live", + static_cast(view.Id)); + return false; + } + + // WRITE AFTER READ: the frames that still sample the image finish before the copy + // overwrites it - queued on this context's GPU timeline, or waited out on the CPU when + // the driver has no fence to queue. Outside the targets' lock, which every session's + // present takes. + if (ResolvedEntryPoints().FencesUsable) { + for (const int fence : SI::DupPendingReadFences(*image)) WaitForFenceBeforeLaterCommands(fence); + } else if (!SI::WaitForReads(*image, kFenceCpuWaitMs)) { + MGLOG_E_ONCE("Shared image present: image %llu's readers did not finish within %u ms", + static_cast(view.Id), static_cast(kFenceCpuWaitMs)); + } + + Bool copied = false; + { + auto& targets = Targets(); + const std::lock_guard lock(targets.Mutex); + PruneLocked(targets); + auto it = targets.ById.find(view.Id); + if (it != targets.ById.end() && it->second.Image->Image->Native != view.NativeBuffer) { + DeleteTarget(it->second); + targets.ById.erase(it); + it = targets.ById.end(); + } + if (it == targets.ById.end()) { + String why; + PresentTarget target; + target.Image = CreateEglImage(view.Id, why); + if (target.Image != nullptr) target.Renderbuffer = MakeRenderbuffer(*target.Image, why); + if (target.Renderbuffer == 0) { + MGLOG_E_ONCE("Shared image present: image %llu cannot be rendered to: %s", + static_cast(view.Id), why.c_str()); + return false; + } + while (targets.ById.size() >= kMaxPresentTargets) { + auto oldest = targets.ById.begin(); + for (auto candidate = targets.ById.begin(); candidate != targets.ById.end(); ++candidate) { + if (candidate->second.LastUse < oldest->second.LastUse) oldest = candidate; + } + DeleteTarget(oldest->second); + targets.ById.erase(oldest); + } + it = targets.ById.emplace(view.Id, std::move(target)).first; + } + it->second.LastUse = ++targets.UseClock; + // A target written before keeps what it was given; one new to this process is + // copied whole whatever the client asked (it cannot know the image's history here). + const Bool wholeTarget = !it->second.Written; + it->second.Written = true; + + GLuint& framebuffer = g_presentFramebuffers.Current(); + if (framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &framebuffer); + if (framebuffer == 0) { + MGLOG_E_ONCE("Shared image present: glGenFramebuffers gave no name"); + return false; + } + + // Every binding goes through Espryt's shadows (BindFramebufferId, the scissor test's + // render-state shadow) and is put back by the guards, so the application's state and + // the caches that describe it are what they were. A blit is scissored like a draw. + ScopedScissorDisable scissorOff; + ScopedFramebufferBinding binding(/*saveRead=*/true, /*saveDraw=*/true); + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 0); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, framebuffer); + g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + it->second.Renderbuffer); + const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); + if (status == GL_FRAMEBUFFER_COMPLETE) { + // The default framebuffer's read buffer is context state the application may + // have set to GL_NONE; the copy reads the back buffer either way. + GLint readBuffer = GL_BACK; + g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer); + if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(GL_BACK); + DrainErrors(); + const Bool sameExtent = static_cast(sourceWidth) == view.Width && + static_cast(sourceHeight) == view.Height; + // TOP ROW FIRST: GL's top row (y = height - 1) lands in the image's first memory + // row, which an AHardwareBuffer-backed renderbuffer addresses as y = 0. + // + // Only where the image differs from the frame: the client keeps, per image, the + // damage it missed (BufferDamageTracker), and a partial blit keeps the rest of + // the renderbuffer as GL promises (checked texel for texel against the frame on + // Adreno). What once left old texels around the rectangles was the scissor + // guard above trusting another session's shadow: the copy was clipped, and + // the missed-damage bookkeeping then never brought those texels over again. + if (sameExtent && !wholeTarget && !region.IsFull()) { + // Each rectangle flipped the same way (an empty region copies nothing: the + // image already is the frame). + for (const MG_Util::Damage::FlippedCopy& copy : + MG_Util::Damage::FlippedCopies(region, sourceWidth, sourceHeight)) { + const MG_Util::Damage::Rect& from = copy.Source; + const MG_Util::Damage::Rect& to = copy.Destination; + g_GLESFuncs.glBlitFramebuffer(from.X, from.Y, from.X + from.Width, from.Y + from.Height, + to.X, to.Y + to.Height, to.X + to.Width, to.Y, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + } else { + g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, + static_cast(view.Height), static_cast(view.Width), + 0, GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR); + } + const GLenum error = g_GLESFuncs.glGetError(); + copied = error == GL_NO_ERROR; + if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(static_cast(readBuffer)); + if (!copied) { + MGLOG_E_ONCE("Shared image present: glBlitFramebuffer %dx%d -> %ux%u failed (0x%04x)", + sourceWidth, sourceHeight, view.Width, view.Height, + static_cast(error)); + } + } else { + MGLOG_E_ONCE("Shared image present: image %llu's framebuffer is incomplete (0x%04x)", + static_cast(view.Id), static_cast(status)); + } + g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); + } + if (!copied) return false; + + // The copy's completion goes to the readers as a fence and the present returns at once; + // a reader that binds the image waits for it on its own GPU. Without fence export the + // copy is waited out here and published as already complete. + int fence = -1; + if (!ExportFence(&fence) && !WaitForCompletion()) return false; + SI::PublishWrite(*image, fence); + // A session that samples images and presents into one (a nested compositor) closes its + // read frame here too. + PublishPendingReads(); + return true; + } + + void ForgetPresentTargetsAfterTerminate() { + auto& targets = Targets(); + const std::lock_guard lock(targets.Mutex); + // TargetIsCurrent is already false for every entry, so nothing reaches the driver. + for (auto& entry : targets.ById) DeleteTarget(entry.second); + targets.ById.clear(); + } +#else + EglImageRef CreateEglImage(Uint64 id, String& why) { + (void)id; + why = "shared images are imported through AHardwareBuffer, which only Android has"; + return nullptr; + } + Bool TargetBoundTexture2D(const EglImageRef& image) { + (void)image; + return false; + } + Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) { + (void)view, (void)region; + return false; + } + void AcquireForSampling(const EglImageRef& image) { (void)image; } + + Bool IsYuv(const EglImageRef& image) { + (void)image; + return false; + } + + void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height) { + (void)image, (void)target, (void)width, (void)height; + } + void PublishPendingReads() {} + Bool PublishPendingAccesses() { return true; } + Bool ExportNativeFence(int* fence) { + *fence = -1; + return true; + } + void ForgetPresentTargetsAfterTerminate() {} +#endif + } // namespace SharedImageImpl +#endif +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp new file mode 100644 index 00000000..55edb215 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp @@ -0,0 +1,1499 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESState.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + // Cached addresses of the frontend's framebuffer binding slots. The frontend + // getter linear-scans its slot array per call and the draw path asks for these + // slots several times per draw (FBO sync, FBO texture-attachment sync, the + // draw-buffer broadcast memo, the draw-time bind). The slots are by-value + // members of the GLContext, so their addresses are a pure function of the + // context's address: the cache is keyed on the raw context pointer, and a + // recreated context landing on the SAME address has its slots at the same + // addresses again - the cached pointers cannot go stale. Invalidation is + // exactly the pointer compare below. + using FbBindingSlot = + std::remove_reference_tGetFramebufferBindingSlot(FramebufferTarget::Draw))>; + // P2 step e4. On the {slot, gen} arm this is an ORDINARY read of pushed state and the cache + // above is not consulted, which closes the P1 accessor bypass: the cached raw pointer ran + // the checked accessor once per context change and then handed out the pointee forever, so + // at all five call sites the per-verb poison stamp (MGP_INPUT_CHECK) and the verify + // read-hook (MGP_INPUT_VERIFY_READ) were skipped. A verb that legitimately never fills + // GetFramebufferBindingSlot could not be caught here, and a verify build compared the field + // only where the slow accessor was used. + // + // The cache stays on the LEGACY arm, gated on the same subsystem bit as the rest of this + // slice, so that MOBILEGL_PIPE_PUSH=0 keeps being the faithful all-subsystems-pull control + // ConfigLoader.cpp documents - "reproduces P1's behaviour exactly" has to include this + // path, or the integrator's A/B measures e4 on both arms and attributes it to neither. In + // the pull build gPipeInputs is the live GLContext, there is no poison to bypass and the + // frontend getter still linear-scans, so the cache is exactly the code it was. + static inline FbBindingSlot& GetFramebufferBindingSlotChecked(FramebufferTarget target) { + if (EsprytSlotTablesEnabled()) { + return MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(target); + } + // No legacy arm compiled: EsprytSlotTablesEnabled() is unconditionally true above. + return MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(target); + } + + static Bool IsDualSourceBlendFactor(BlendFactor v) { + switch (v) { + case BlendFactor::Src1Color: + case BlendFactor::OneMinusSrc1Color: + case BlendFactor::Src1Alpha: + case BlendFactor::OneMinusSrc1Alpha: + return true; + default: + return false; + } + } + + namespace FramebufferImpl { + Array g_fboSyncedSerials{}; + + // Whether the last SyncCurrentFBO found the two records describing the two bindings. + // BindCurrentFBO's handle arm reads it instead of re-asking, because the whole value + // of that arm is that it does NOT read the binding slot - and because every entry + // point that binds runs the sync first, immediately before (PrepareForDraw, Clear, the + // blits, the DSA clears, the readbacks). It starts false and every decline clears it, + // so a bind that somehow arrives without a preceding sync takes the pre-handle arm + // rather than trusting a record nothing checked. + Bool g_fboRecordsTrusted = false; + + static void StampSyncedFramebufferSerial(FramebufferTarget target, Uint64 serial) { + auto& memo = g_fboSyncedSerials[SizeT(target)]; + memo.serial = serial; + memo.backendIdGeneration = g_attachmentBackendIdGeneration; + memo.contextGeneration = g_backendContextGeneration; + memo.valid = true; + } + + // The seam check, and it is the reason this arm can be trusted at all on a tree where + // the client half is mid-landing. The record says which framebuffer each binding + // holds; the binding slot says the same thing. If they disagree, the record is stale + // or mis-keyed and driving the driver from it would sync - or skip - the wrong + // framebuffer, which is the one failure this path could produce that no pixel test on + // this tree could see. Disagreement DECLINES the whole arm rather than fixing up one + // target, because the two records are emitted together and one of them being wrong + // says nothing good about the other. + // + // The default framebuffer is compared through IsDefault rather than through Fbo, + // because the reserved handle {0,1} is not the handle the twin table minted for the + // default FramebufferObject; comparing it against HandleOf would fail for the one case + // the reserved handle exists to make easy. This is the identity comparison D-C1 + // retires, kept here ONLY as a consistency check and only until package D's twin API + // takes the record instead of the object - at which point there is no second answer + // left to disagree with. + // + // P5e (fb, CONTRACT-P5E.md §5.4): MONOLITH-ONLY. The sentence above ends "...and only + // until package D's twin API takes the record instead of the object - at which point + // there is no second answer left to disagree with". That point is this package: the + // twin's sync takes the handle, the record was resolved from the applier's OWN bound + // handle, and the only thing this could still consult is the binding slot - a + // BARRIER_PULLED row an apply thread may not read. So under a transport the check does + // not run at all, rather than running on a value it is not allowed to have. + static Bool FramebufferRecordMatchesBinding(FramebufferTarget target, + const MG_Pipe::MGPFramebufferState& record) { + return true; + } + + static Bool SyncedFramebufferSerialIsCurrent(FramebufferTarget target, Uint64 serial) { + const auto& memo = g_fboSyncedSerials[SizeT(target)]; + return memo.valid && memo.serial == serial && + memo.backendIdGeneration == g_attachmentBackendIdGeneration && + memo.contextGeneration == g_backendContextGeneration; + } + + // The handle arm of SyncCurrentFBO. Returns false when it declined - which is not a + // failure and is the only honest answer while no client has emitted yet. + // + // NO LIVE RECORD IS A MISS, NEVER A HIT, and it is the same rule the vertex-input arm + // states at SyncVaoAttributeBuffersByHandle. What is NOT the test, and this is a trap + // worth naming: FramebufferSerial is not "has the client ever described a framebuffer". + // MGPipeApplierReset ADVANCES every P4a working-state serial unconditionally + // (PipeApply.cpp), because a cleared window is itself a change a twin has to hear + // about - so the serial is non-zero after the first make-current even though no + // set_framebuffer_state has ever been applied, and gating on it would take this arm + // with two empty records and leave every framebuffer unsynced. The record's own Fbo + // handle is the test, and BOTH bindings have to be described before this arm can run + // at all, because it answers for both in one pass: an emitter walks {Draw, Read} and a + // Target = Both record writes both, so a half-described applier is the transitional + // state of a tree whose client half has not landed yet, and that state belongs to the + // pre-handle arm. + static Bool SyncCurrentFBOByRecord() { + g_fboRecordsTrusted = false; + const auto& st = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPFramebufferState* const drawRecord = BoundFramebufferRecord(FramebufferTarget::Draw); + const MG_Pipe::MGPFramebufferState* const readRecord = BoundFramebufferRecord(FramebufferTarget::Read); + const Bool drawUnrecorded = drawRecord == nullptr || MG_Pipe::MGPipeHandleIsNull(drawRecord->Fbo); + const Bool readUnrecorded = readRecord == nullptr || MG_Pipe::MGPipeHandleIsNull(readRecord->Fbo); + // P4a decline-site F2: S - BOTH CASES ARE LOUD, the half-described one since v2 + // (ID-19) and the NEITHER-DESCRIBED one from this verification round. + // + // EXACTLY ONE recorded: an emitter walks {Draw, Read} together and a Target = Both + // record writes both, so no correct emitter can produce it. It is the shape a + // PARTIALLY LANDED emitter produces and it is invisible in a pixel test because + // the arm just declines. + // + // NEITHER recorded WAS the transitional state of a tree whose client half had not + // landed - on this branch, before B and C were integrated, every draw was that, + // which is why v2 left it silent. Package B now emits set_framebuffer_state for + // every bound framebuffer and this function only runs with bit 9 (and therefore + // 10, 11 and 7) set, so reaching a draw with NEITHER binding described means the + // record for the bound framebuffer never arrived, or arrived at a slot whose + // generation has already moved - the applier counts that second cause separately + // in StaleFramebufferRecordLookups, so the two are told apart without a third + // message. Both are seam defects and both now say so, in the words section 8's + // one grep looks for. + if (drawUnrecorded || readUnrecorded) { + if (drawUnrecorded != readUnrecorded) { + const MG_Pipe::MGPFramebufferState& described = *(drawUnrecorded ? readRecord : drawRecord); + MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the %s " + "binding has no record while %s names {slot %u, gen %u} - a " + "half-described applier; running the pre-handle framebuffer sync.", + drawUnrecorded ? "DRAW" : "READ", drawUnrecorded ? "READ" : "DRAW", + static_cast(described.Fbo.Slot), + static_cast(described.Fbo.Gen)); + } else { + MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: NEITHER " + "the DRAW nor the READ binding has one while the framebuffer " + "subsystem bit is set (bound handles {slot %u, gen %u} / " + "{slot %u, gen %u}, %llu stale-generation lookup(s) so far); " + "running the pre-handle framebuffer sync.", + static_cast( + st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)].Slot), + static_cast( + st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)].Gen), + static_cast( + st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)].Slot), + static_cast( + st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)].Gen), + static_cast(st.StaleFramebufferRecordLookups)); + } + return false; + } + // P4a decline-site F3: S - the framebuffer seam, already loud; the round keeps this + // wording VERBATIM because it is the stem the other two seams now share. + if (!FramebufferRecordMatchesBinding(FramebufferTarget::Draw, *drawRecord) || + !FramebufferRecordMatchesBinding(FramebufferTarget::Read, *readRecord)) { + MGLOG_E_ONCE("A framebuffer record does not describe the binding it names; " + "running the pre-handle framebuffer sync."); + return false; + } + g_fboRecordsTrusted = true; + + const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read}; + for (auto& target : fboTargets) { + if (SyncedFramebufferSerialIsCurrent(target, st.FramebufferSerial)) continue; + + // Not a third accessor call: these are the two pointers F2 proved non-null a + // few lines up, so nothing here dereferences an unchecked result. + const MG_Pipe::MGPFramebufferState& record = + target == FramebufferTarget::Draw ? *drawRecord : *readRecord; + if (record.IsDefault != 0) { + // The default framebuffer, said by the record rather than by comparing the + // bound object against pDefaultFramebufferInfo->defaultFBO - which is one + // of the four identity comparisons the reserved handle {0,1} exists to + // retire (MGPipeHandles.h). Nothing to sync except the widened-attachment + // masks, which only SyncToBackend ever writes and would otherwise still + // describe the user FBO that was draw-bound before. The window surface is a + // real RGBA buffer, so nothing here is ever widened. + if (target == FramebufferTarget::Draw) { + g_alphaWidenedDrawBufferMask = 0; + g_integerColorDrawBufferMask = 0; + } + StampSyncedFramebufferSerial(target, st.FramebufferSerial); + continue; + } + + // MONOLITH GLUE, and named as such: on the monolith arm the twin's SyncToBackend + // still reads the frontend object, and the bound object is still this call's + // ARGUMENT. The IDENTITY is already the handle - GetOrCreateByHandle(record.Fbo) - + // so a recycled FBO cannot be mistaken for its predecessor here. + // + // A1, TAKEN AT THE VERIFICATION ROUND AGAINST WHAT D ACTUALLY BUILT. The twin + // is resolved BY HANDLE ONLY: `BackendPtr* GetOrCreateByHandle(MGPipeHandle)` + // (Managers.h ~453) returns a POINTER rather than the reference + // GetOrCreate(StatePtr) returns, deliberately, because it has three ways to + // decline - the legacy arm, a slot past the table's sanity bound, and a + // generation BEHIND the live entry's - and every one of them has to be visible + // here rather than answered with a parked twin. + // + // P5e (fb, CONTRACT-P5E.md §5.4): AND ON THE RECORD ARM THE OBJECT IS GONE TOO. + // `SyncToBackendByHandle(record.Fbo, target)` takes the record's own handle, so + // there is no binding slot to read, no object to hand over, and no state note + // to leave behind - NoteStateForHandle was the server's last cross-record hold + // on a frontend framebuffer, and the named blit that needed it now reads the + // record too. What is left frontend under a transport is nothing at all. + // + // THE POINTER-INVALIDATION CONTRACT (Managers.h ~381-385): a handle-arm result + // is a stable array element that only a table-GROWING GetOrCreate can move. + // GetOrCreateByHandle is exactly such a call, so its result is used and dropped + // inside this iteration and never held across another registry call. + auto* const twinSlot = g_backendFramebufferObjects.GetOrCreateByHandle(record.Fbo); + // A1's third decline. Bit 9 implies bit 10 implies bit 7, so the slot tables are + // armed whenever this function runs and the legacy-arm cause cannot fire here; + // what is left is a slot past the sanity bound or a generation behind the live + // entry's, and both are seam defects - a handle the emitter minted for a + // recycled slot and never described, or one it had already retired. The whole + // arm declines rather than this one target, for F4's reason. + if (twinSlot == nullptr) { + MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the " + "%s record's handle {slot %u, gen %u} is refused by the twin slot " + "table (live generation %u); running the pre-handle framebuffer " + "sync.", + target == FramebufferTarget::Read ? "READ" : "DRAW", + static_cast(record.Fbo.Slot), + static_cast(record.Fbo.Gen), + static_cast( + g_backendFramebufferObjects.LiveGenAt(record.Fbo.Slot))); + g_fboRecordsTrusted = false; + return false; + } + auto& backendObj = *twinSlot; + if (!backendObj) { + backendObj = MakeShared(); + } + + // THE "SAME FBO AS DRAW" SKIP IS A FIELD, not a pointer comparison against the + // object the previous iteration happened to sync. Target = Both is the client + // saying one object is bound to both bindings, so the attachment and + // draw-buffer work the DRAW pass already did is not repeated - and the read + // buffer, which is READ-target-specific and is what that skip used to drop, is + // applied unconditionally on this path. + // + // ID-27 (wire review v2, MAJOR-1): THE QUESTION IS ASKED OF THE BOUND HANDLES, + // NOT OF THE RECORD'S STORED TARGET, and it is no longer possible to ask it any + // other way in this file. `MGPFramebufferState::Target` is the target of the + // LAST EMISSION that wrote the record, and since ID-19(c) that emission may be + // a `Named` one from any of B's sixteen DSA sites - so a framebuffer really + // bound to both bindings can carry `Target == Named` and a framebuffer bound to + // neither can carry `Target == Both` left over from when it was. The skip's + // real question is "is one object bound to both bindings", and the applier + // answers it directly: BoundFramebuffer[Draw] == BoundFramebuffer[Read]. Both + // are non-null here (F2 rejected a null on either), and a handle compares by + // {slot, gen}, so a recycled slot is not its predecessor. + // + // MINOR-1's correction is RETIRED by P5e: the read buffer is no longer applied + // "FROM THE FRONTEND OBJECT" on this arm - ApplyReadBufferFromRecord resolves it + // out of MGPFramebufferState::ReadSurface, which was already the record's + // answer, and the by-handle entry is what reaches it here. + const auto& boundHandles = st.BoundFramebuffer; + const Bool sameObjectOnBothBindings = + target == FramebufferTarget::Read && + boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)] == + boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)]; + + if (sameObjectOnBothBindings) { + backendObj->SyncReadBufferToBackendByHandle(record.Fbo); + } else { + backendObj->SyncToBackendByHandle(record.Fbo, target); + } + StampSyncedFramebufferSerial(target, st.FramebufferSerial); + continue; + } + + return true; + } + + void SyncCurrentFBO() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + + // The handle arm first, and it declines rather than half-running: with no record + // yet it hands the walk straight back to the pre-handle arm below, unchanged. + // P4a decline-site F1: M - the mask says this family is not switched on, and it + // IS D's FramebufferSubsystemEnabled() now. Silent, confirmed at the + // verification round. + if (SyncCurrentFBOByRecord()) return; + // P5e (fb, CONTRACT-P5E.md §5.4): AND UNDER A TRANSPORT THE DECLINE IS THE END OF + // IT. The fallback below reads GetFramebufferBindingSlot, a BARRIER_PULLED row; the + // value it would read belongs to a client this apply is no longer synchronous with, + // so "run the pre-handle sync" stops being a safe default and becomes a wrong + // picture taken from torn state. Every cause of the decline is already named by the + // MGLOG_E_ONCE that produced it (F2's half-described / neither-described applier, + // F4's unbound binding, A1's refused handle); this turns that log into the abort the + // contract asks for, with the verb in the message so the strict lane's marker table + // points at the site. + RefuseFramebufferBindingSlotRead(); + } + } // namespace FramebufferImpl + + namespace RenderStateImpl { + static Uint16 g_syncedRenderStateVersion = 0; + static Bool g_hasSyncedRenderState = false; + RenderStateParameters g_syncedRenderStateParameters; + static IntVec4 g_syncedBackendViewport = IntVec4(-1, -1, -1, -1); + // The RESOLVED scissor rectangle last pushed (see the scissor block in SyncRenderState); + // an impossible value so the first sync always pushes. + static IntVec4 g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1); + // GLES starts with sRGB framebuffer encoding on, so the first sync always has to push the + // frontend's (desktop-GL default) disabled state down. + static Bool g_syncedSrgbFramebufferWrites = true; + // Set when the shadow below stops describing the real ES context. The ES context + // OUTLIVES every MobileGL context, so a MobileGL context switch leaves it holding the + // previous context's enable state while the frontend's parameter block AND its + // version counter both restart from defaults. Every "differs from what I last pushed" + // test in SyncRenderState would then agree that nothing needs pushing, and the + // leftover state silently applies to the new context - the class of bug the CTS + // caught as GL_FRAMEBUFFER_SRGB surviving from vertex_attrib_binding into + // direct_state_access.renderbuffers_storage. One unconditional push settles the whole + // block rather than the one cap that happened to be noticed. + static Bool g_forceFullRenderStateResync = true; + // Which draw buffers' alpha channel the colour mask last pushed to the driver had forced + // OFF - i.e. the value of `appliedWidenMask` in the last SyncRenderState that reached the + // colour-mask block. NOT derivable from the frontend parameter block: it depends on the + // bound DRAW framebuffer's attachment formats and on whether the caller is a draw or a + // clear, neither of which bumps the frontend render-state version. Without it a + // clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits + // the clear's undoctored mask. + Uint32 g_syncedColorMaskAlphaWidenMask = 0; + // Scratch for the dual-source-blend decline path in the blend block below. File-scope + // rather than a local so the ordinary draw pays nothing for it: it is written only on a + // driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and + // SyncRenderState runs on the GL thread only. + static Array + g_dualSourceDeclinedBlendStates; + +#if MOBILEGL_BUILD_DISAGGREGATED + // The context epoch the shadow was last synced in (SyncRenderState). + static ContextEpoch g_syncedRenderStateEpoch{}; +#endif + void InvalidateSyncedRenderState() { + g_forceFullRenderStateResync = true; + g_hasSyncedRenderState = false; + g_syncedBackendViewport = IntVec4(-1, -1, -1, -1); + g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1); + } + + // Whether the shadow's bits are the current native context's. An invalidation (a context + // switch, the backend's turn moving to another session's thread) only raises flags - the + // bits stay, and they are then another context's. + static Bool SyncedRenderStateDescribesCurrentContext() { + if (!g_hasSyncedRenderState || g_forceFullRenderStateResync) return false; +#if MOBILEGL_BUILD_DISAGGREGATED + if (g_syncedRenderStateEpoch != CurrentContextEpoch()) return false; +#endif + return true; + } + + Bool DriverScissorTestEnabled() { + if (SyncedRenderStateDescribesCurrentContext()) + return (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; + return g_GLESFuncs.glIsEnabled != nullptr && g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE; + } + + void SetSyncedScissorTestForTesting(Bool enabled) { + g_syncedRenderStateParameters.ScissorTestEnabledMask = enabled ? 1u : 0u; + g_hasSyncedRenderState = true; + g_forceFullRenderStateResync = false; +#if MOBILEGL_BUILD_DISAGGREGATED + g_syncedRenderStateEpoch = CurrentContextEpoch(); +#endif + } + + void SyncRenderState(Bool forColorClear) { +#if MOBILEGL_BUILD_DISAGGREGATED + const auto epoch = CurrentContextEpoch(); + if (g_syncedRenderStateEpoch != epoch) { + InvalidateSyncedRenderState(); + g_syncedRenderStateEpoch = epoch; + } +#endif +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + Uint16 currentRenderStateVersion = MG_Pipe::gPipeInputs->GetRenderStateParametersVersion(); + const Bool forceFullPush = g_forceFullRenderStateResync; + g_forceFullRenderStateResync = false; + // The alpha discipline for widened colour attachments (see the header comment on + // SyncRenderState): a DRAW must not be able to move the stored alpha off 1.0, a CLEAR + // is what puts it there. So the draw path masks alpha off on every widened draw + // buffer and the clear path masks nothing. + const Uint32 appliedWidenMask = forColorClear ? 0u : FramebufferImpl::g_alphaWidenedDrawBufferMask; + const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask; + if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState && + currentRenderStateVersion == g_syncedRenderStateVersion) { + // Gate 1 of section 2.3.1: the steady-state cost of this whole function is + // the one Uint16 read above plus this compare. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/true); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); + } + return; + } + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/false); + // The version read above, the parameter-block fetch and the viewport fetch + // below - the three accessor calls this function makes unconditionally on a + // miss. The conditional sRGB capability read further down is deliberately + // NOT counted (see the inventory in PipeStats.cpp). + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); + } + + const auto& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters(); + + // The frontend has ONE version for the whole parameter block, so a per-draw blend + // toggle used to re-diff all ~40 pieces of state field by field on every draw + // (Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), making this the + // hottest thing mc_state_toggle did). Split the struct into three contiguous byte + // spans - the head (viewport/point/line/polygon-offset scalars), the blend array, + // and everything after it - and let one memcmp per span decide whether its blocks + // run at all. memcmp can false-DIFFER on padding bytes (harmless: the field-wise + // block runs and finds nothing) but can never false-match, and after the first full + // sync the tail memcpy below makes the shadow byte-identical, padding included, so + // in the steady state a span memcmp is exact. Blocks whose inputs are NOT in the + // parameter struct (the surface-size viewport fallback, the sRGB context + // capability) stay outside the gates. + static_assert(std::is_trivially_copyable_v, + "span memcmp/memcpy below treats the parameter block as raw bytes"); + constexpr SizeT kBlendSpanBegin = offsetof(RenderStateParameters, BlendStates); + constexpr SizeT kBlendSpanEnd = offsetof(RenderStateParameters, LogicOp); + const auto* currentBytes = reinterpret_cast(¶meters); + const auto* syncedBytes = reinterpret_cast(&g_syncedRenderStateParameters); + const Bool headSpanDirty = + !g_hasSyncedRenderState || std::memcmp(currentBytes, syncedBytes, kBlendSpanBegin) != 0; + const Bool blendSpanDirty = + !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanBegin, syncedBytes + kBlendSpanBegin, + kBlendSpanEnd - kBlendSpanBegin) != 0; + const Bool tailSpanDirty = + !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd, + sizeof(RenderStateParameters) - kBlendSpanEnd) != 0; + + IntVec4 backendViewport = MG_Pipe::gPipeInputs->GetViewport(); + if (backendViewport.z() <= 0 || backendViewport.w() <= 0) { + Int surfaceWidth = 0; + Int surfaceHeight = 0; + if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { + backendViewport = IntVec4(0, 0, surfaceWidth, surfaceHeight); + } + } + if (backendViewport != g_syncedBackendViewport) { + g_GLESFuncs.glViewport(backendViewport.x(), backendViewport.y(), backendViewport.z(), + backendViewport.w()); + g_syncedBackendViewport = backendViewport; + } + + // Every capability bool (and the scissor-test mask below) lives after LogicOp in the + // struct, i.e. in the tail span. + if (tailSpanDirty) { +#define SYNC_CAPABILITY(cap_mg, cap_gl) \ + if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \ + if (parameters.cap_mg##Enabled) { \ + g_GLESFuncs.glEnable(cap_gl); \ + } else { \ + g_GLESFuncs.glDisable(cap_gl); \ + } \ + } + SYNC_CAPABILITY(DepthTest, GL_DEPTH_TEST); + SYNC_CAPABILITY(ColorLogicOp, GL_COLOR_LOGIC_OP); + SYNC_CAPABILITY(Dither, GL_DITHER); + SYNC_CAPABILITY(Multisample, GL_MULTISAMPLE); + SYNC_CAPABILITY(SampleAlphaToCoverage, GL_SAMPLE_ALPHA_TO_COVERAGE); + SYNC_CAPABILITY(SampleCoverage, GL_SAMPLE_COVERAGE); + SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK); + SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL); + SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD); + SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST); + SYNC_CAPABILITY(CullFace, GL_CULL_FACE); + +#undef SYNC_CAPABILITY + + // GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a + // 16-bit mask and not a "Enabled" bool the macro above could key off. ES + // has exactly one scissor rectangle and one scissor enable, so only bit 0 - the + // index every ES draw rasterizes against - can be forwarded; a program that + // enables the test for viewport 3 alone gets viewport 0's answer here. That is + // the same limitation as the unemulated gl_ViewportIndex on this backend and is + // why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt. + { + const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0; + const Bool syncedScissorTest = + (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; + if (forceFullPush || scissorTest != syncedScissorTest) { + scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + } + } + } + + if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) { + // gl_ClipDistance clipping is per-distance enable state in GL, and ES reaches it + // only through GL_EXT_clip_cull_distance. That extension reuses the desktop enum + // values for CLIP_DISTANCE0_EXT..7_EXT, but the token is absent from the ES + // headers this file compiles against, hence the local name. Without the + // extension there is nowhere to put the state and the shader could not have + // compiled either, so the whole block is gated rather than silently no-op'ing. + constexpr GLenum kClipDistance0 = 0x3000; + constexpr Uint kClipDistanceCount = 8; + const Uint32 mask = parameters.ClipDistanceEnabledMask; + const Uint32 syncedMask = g_syncedRenderStateParameters.ClipDistanceEnabledMask; + if (forceFullPush || mask != syncedMask) { + const Uint32 changed = forceFullPush ? ~0u : (mask ^ syncedMask); + for (Uint i = 0; i < kClipDistanceCount; ++i) { + const Uint32 bit = 1u << i; + if ((changed & bit) == 0) continue; + const GLenum cap = static_cast(kClipDistance0 + i); + (mask & bit) ? g_GLESFuncs.glEnable(cap) : g_GLESFuncs.glDisable(cap); + } + } + } + + { // sRGB framebuffer writes. GLES core always encodes a write into an sRGB attachment, + // while GL_FRAMEBUFFER_SRGB is disabled by default in desktop GL and the frontend + // never turns it on, so the driver has to be told to write raw. Without this a render + // into an sRGB colour buffer comes back encoded once too often (the shader's own + // decode on the next fetch then leaves the value one conversion short). + const Bool srgbWrites = MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); + if (g_GLESCapabilities.SupportsSrgbWriteControl && + (forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) { + srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB) + : g_GLESFuncs.glDisable(GL_FRAMEBUFFER_SRGB); + g_syncedSrgbFramebufferWrites = srgbWrites; + } + } + + if (tailSpanDirty) { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed + // all-ones value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to + // it. An arbitrary non-fixed restart index is rejected at draw time (see DrawElements). + const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled; + const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled || + g_syncedRenderStateParameters.PrimitiveRestartEnabled; + if (forceFullPush || restart != syncedRestart) { + restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX) + : g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + } + } + + const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; }; + + // Which draw buffers the blend block below DECLINED (see it for why). Needed again at + // the shadow write-back at the end of this function: the span memcpy there clones the + // FRONTEND block, which for a declined draw buffer is not what the driver was handed. + Uint32 dualSourceDeclinedMask = 0; + + if (blendSpanDirty) { // Blend State + using FBO = MG_State::GLState::FramebufferObject; + auto& syncedStates = g_syncedRenderStateParameters.BlendStates; + + // Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with + // glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none. + // Detected at load and surfaced in the POST. There is no fallback that BLENDS + // correctly, so a draw that asks for a SRC1 factor on a driver without the extension + // gets the blend DECLINED: that draw buffer is pushed with blending off and neutral + // One/Zero factors, and the loss is logged once. The two rejected alternatives are + // both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise + // GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend + // against stale state), and throwing, which is what this did until now, takes the + // whole process down over one unsupported blend factor. Declining is defined, + // survivable and visible in the log. + // + // NOT gated on Enabled, deliberately, and the same way the Vulkan twin is not gated + // on effectiveBlendEnabled: what has to be kept away from the driver is the FACTOR + // ENUM, and the factor push below never consults Enabled - one glBlendFuncSeparate + // serves every draw buffer when they agree, and the per-index arm diffs factors + // alone. So `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` followed by any + // draw OR clear would otherwise hand a GL_SRC1_ALPHA to a driver that answers + // GL_INVALID_ENUM, leaving a spurious error in ITS queue for the next internal + // no-error probe to read as its own, and leaving this shadow recording factors the + // ES context rejected. Blending being off makes the picture unaffected; it does not + // make the enum acceptable. + const auto* effectiveBlendStates = ¶meters.BlendStates; + if (!g_GLESCapabilities.SupportsDualSourceBlend) { + Uint32 declinedWithBlendingOnMask = 0; + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + const auto& s = parameters.BlendStates[i]; + if (IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) || + IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha)) { + dualSourceDeclinedMask |= 1u << i; + if (s.Enabled) declinedWithBlendingOnMask |= 1u << i; + } + } + if (dualSourceDeclinedMask != 0) { + // Two masks in the message because they mean different things to whoever + // reads the log: the second one is where a PICTURE was lost. A draw buffer + // in the first mask but not the second had blending off anyway, so nothing + // was blended and nothing was dropped - only the unusable enum was kept out + // of the driver. + MGLOG_E_ONCE( + "SyncRenderState: a GL_SRC1_* (dual-source) blend factor was set on draw buffer " + "mask 0x%x, but the GLES driver does not expose GL_EXT_blend_func_extended (see " + "the dual-source blend row in the driver POST). Those draw buffers are pushed " + "with neutral One/Zero factors instead. Blending was actually ENABLED on mask " + "0x%x, and only there is anything lost: the fragment's first output is written " + "unblended and the second source is dropped.", + dualSourceDeclinedMask, declinedWithBlendingOnMask); + g_dualSourceDeclinedBlendStates = parameters.BlendStates; + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + if ((dualSourceDeclinedMask & (1u << i)) == 0) continue; + auto& s = g_dualSourceDeclinedBlendStates[i]; + // Both halves, for the same reason the Vulkan arm neutralises both: the + // enable so nothing blends against a source the driver cannot produce, + // the factors so no GL_SRC1_* enum is ever handed over. Clearing Enabled + // on a buffer that was already off is a no-op, which is what makes one + // ungated rule serve both cases. + s.Enabled = false; + s.SrcFactorRGB = BlendFactor::One; + s.DstFactorRGB = BlendFactor::Zero; + s.SrcFactorAlpha = BlendFactor::One; + s.DstFactorAlpha = BlendFactor::Zero; + } + effectiveBlendStates = &g_dualSourceDeclinedBlendStates; + } + } + // The rest of the block reads the EFFECTIVE state. The per-field writes it makes + // into `syncedStates` are provisional - the span memcpy at the end of this function + // overwrites the whole blend span with the frontend's own bytes - so the declined + // draw buffers are put back there, see the write-back below. + const auto& targetStates = *effectiveBlendStates; + + Bool allEnabled = true; + Bool allDisabled = true; + Bool anyCapDirty = forceFullPush; + + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + Bool enabled = targetStates[i].Enabled; + if (enabled) + allDisabled = false; + else + allEnabled = false; + + if (enabled != syncedStates[i].Enabled) { + anyCapDirty = true; + } + } + + if (anyCapDirty) { + if (allEnabled) { + g_GLESFuncs.glEnable(GL_BLEND); + for (auto& s : syncedStates) + s.Enabled = true; + } else if (allDisabled) { + g_GLESFuncs.glDisable(GL_BLEND); + for (auto& s : syncedStates) + s.Enabled = false; + } else { + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + if (forceFullPush || targetStates[i].Enabled != syncedStates[i].Enabled) { + syncedStates[i].Enabled = targetStates[i].Enabled; + syncedStates[i].Enabled ? g_GLESFuncs.glEnablei(GL_BLEND, i) + : g_GLESFuncs.glDisablei(GL_BLEND, i); + } + } + } + } + + Bool allFuncsSame = true; + Bool anyFuncDirty = forceFullPush; + const auto& first = targetStates[0]; + + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + const auto& cur = targetStates[i]; + const auto& syn = syncedStates[i]; + + Bool isDiffFromSyn = + (cur.SrcFactorRGB != syn.SrcFactorRGB || cur.DstFactorRGB != syn.DstFactorRGB || + cur.SrcFactorAlpha != syn.SrcFactorAlpha || cur.DstFactorAlpha != syn.DstFactorAlpha); + + if (isDiffFromSyn) anyFuncDirty = true; + + if (allFuncsSame && i > 0) { + if (cur.SrcFactorRGB != first.SrcFactorRGB || cur.DstFactorRGB != first.DstFactorRGB || + cur.SrcFactorAlpha != first.SrcFactorAlpha || cur.DstFactorAlpha != first.DstFactorAlpha) { + allFuncsSame = false; + } + } + } + + if (anyFuncDirty) { + if (allFuncsSame) { + g_GLESFuncs.glBlendFuncSeparate(MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorRGB), + MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorRGB), + MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorAlpha), + MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorAlpha)); + + for (auto& syn : syncedStates) { + syn.SrcFactorRGB = first.SrcFactorRGB; + syn.DstFactorRGB = first.DstFactorRGB; + syn.SrcFactorAlpha = first.SrcFactorAlpha; + syn.DstFactorAlpha = first.DstFactorAlpha; + } + } else { + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + const auto& cur = targetStates[i]; + auto& syn = syncedStates[i]; + + if (forceFullPush || cur.SrcFactorRGB != syn.SrcFactorRGB || + cur.DstFactorRGB != syn.DstFactorRGB || + cur.SrcFactorAlpha != syn.SrcFactorAlpha || + cur.DstFactorAlpha != syn.DstFactorAlpha) { + syn.SrcFactorRGB = cur.SrcFactorRGB; + syn.DstFactorRGB = cur.DstFactorRGB; + syn.SrcFactorAlpha = cur.SrcFactorAlpha; + syn.DstFactorAlpha = cur.DstFactorAlpha; + + g_GLESFuncs.glBlendFuncSeparatei( + i, MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorRGB), + MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorRGB), + MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorAlpha), + MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorAlpha)); + } + } + } + } + + Bool allEquationsSame = true; + Bool anyEquationDirty = forceFullPush; + + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + const auto& cur = targetStates[i]; + const auto& syn = syncedStates[i]; + + if (cur.ColorEquation != syn.ColorEquation || cur.AlphaEquation != syn.AlphaEquation) { + anyEquationDirty = true; + } + + if (allEquationsSame && i > 0) { + if (cur.ColorEquation != first.ColorEquation || cur.AlphaEquation != first.AlphaEquation) { + allEquationsSame = false; + } + } + } + + if (anyEquationDirty) { + if (allEquationsSame) { + g_GLESFuncs.glBlendEquationSeparate(MG_Util::ConvertBlendEquationToGLEnum(first.ColorEquation), + MG_Util::ConvertBlendEquationToGLEnum(first.AlphaEquation)); + + for (auto& syn : syncedStates) { + syn.ColorEquation = first.ColorEquation; + syn.AlphaEquation = first.AlphaEquation; + } + } else { + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + const auto& cur = targetStates[i]; + auto& syn = syncedStates[i]; + + if (forceFullPush || cur.ColorEquation != syn.ColorEquation || + cur.AlphaEquation != syn.AlphaEquation) { + syn.ColorEquation = cur.ColorEquation; + syn.AlphaEquation = cur.AlphaEquation; + + g_GLESFuncs.glBlendEquationSeparatei( + i, MG_Util::ConvertBlendEquationToGLEnum(cur.ColorEquation), + MG_Util::ConvertBlendEquationToGLEnum(cur.AlphaEquation)); + } + } + } + } + } + + if (tailSpanDirty) { // Depth state + if (forceFullPush || parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) { + g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc)); + } + if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) { + g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE); + } + if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) { + g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y()); + } + } + + if (tailSpanDirty) { // Stencil state + for (SizeT faceIndex = 0; faceIndex < parameters.StencilStates.size(); ++faceIndex) { + const StencilFaceState& current = parameters.StencilStates[faceIndex]; + const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex]; + const GLenum glFace = faceIndex == 0 ? GL_FRONT : GL_BACK; + + if (forceFullPush || current.Func != synced.Func || current.Ref != synced.Ref || + current.ValueMask != synced.ValueMask) { + g_GLESFuncs.glStencilFuncSeparate( + glFace, MG_Util::ConvertDepthTestFuncToGLEnum(current.Func), current.Ref, + current.ValueMask); + } + if (forceFullPush || current.WriteMask != synced.WriteMask) { + g_GLESFuncs.glStencilMaskSeparate(glFace, current.WriteMask); + } + if (forceFullPush || current.FailOp != synced.FailOp || + current.PassDepthFailOp != synced.PassDepthFailOp || + current.PassDepthPassOp != synced.PassDepthPassOp) { + g_GLESFuncs.glStencilOpSeparate( + glFace, MG_Util::ConvertStencilOperationToGLEnum(current.FailOp), + MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthFailOp), + MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthPassOp)); + } + } + } + + if (tailSpanDirty || colorMaskWidenDirty) { // Color mask. Uniform masks use the non-indexed glColorMask + // (works everywhere); divergent per-draw-buffer masks use the indexed glColorMaski when + // draw_buffers_indexed is available, otherwise fall back to broadcasting draw buffer 0. + // Mirrors the blend block. + using FBO = MG_State::GLState::FramebufferObject; + const auto& targetMasks = parameters.ColorMasks; + const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks; + + // What the DRIVER is told for draw buffer i. Identical to the application's mask + // except on a widened attachment during a draw, where alpha is forced off; the + // frontend's own array is never written, so glGet(GL_COLOR_WRITEMASK) keeps + // answering with the application's value. + const auto driverMask = [&](Uint i) -> BoolVec4 { + BoolVec4 m = targetMasks[i]; + if (i < 32 && (appliedWidenMask & (1u << i)) != 0) { + m.w() = false; + } + return m; + }; + + Bool anyDirty = forceFullPush || colorMaskWidenDirty; + Bool allSame = true; + const BoolVec4 driverMask0 = driverMask(0); + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + if (targetMasks[i] != syncedMasks[i]) anyDirty = true; + if (i > 0 && driverMask(i) != driverMask0) allSame = false; + } + + if (anyDirty) { + if (allSame || !g_GLESCapabilities.SupportsIndexedColorMask) { + // Without draw_buffers_indexed there is only one mask for the whole + // framebuffer, so a widened draw buffer 0 costs every other buffer its + // alpha writes. ES 3.2 makes glColorMaski core and ES 3.1 has it as + // EXT/OES; the only devices that reach this line are ES 3.0-class, where + // MRT with a mixed widened/native colour attachment set is already rare. + const BoolVec4& m = driverMask0; + g_GLESFuncs.glColorMask(ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()), + ToGLBoolean(m.w())); + } else { + const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski + : g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT + : g_GLESFuncs.glColorMaskiOES; + for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) { + // colorMaskWidenDirty forces every slot: the previous push may have + // been the non-indexed glColorMask above (which set all of them), and + // the per-slot diff below only knows about the application's array. + if (forceFullPush || colorMaskWidenDirty || targetMasks[i] != syncedMasks[i]) { + const BoolVec4 m = driverMask(i); + colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()), + ToGLBoolean(m.w())); + } + } + } + } + g_syncedColorMaskAlphaWidenMask = appliedWidenMask; + } + + if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present. + // Without the extension the mode stays FILL and non-FILL requests are dropped. + if ((forceFullPush || parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront) && + g_GLESCapabilities.SupportsPolygonMode) { + const auto polygonModeFn = + g_GLESFuncs.glPolygonModeNV ? g_GLESFuncs.glPolygonModeNV : g_GLESFuncs.glPolygonModeANGLE; + polygonModeFn(GL_FRONT_AND_BACK, parameters.PolygonModeFront); + } + } + + if (tailSpanDirty) { // Clear values + if (forceFullPush || parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) { + const FloatVec4& clearCol = parameters.ClearColor; + g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w()); + } + if (forceFullPush || parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) { + g_GLESFuncs.glClearDepthf(parameters.ClearDepth); + } + if (forceFullPush || parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) { + g_GLESFuncs.glClearStencil(static_cast(parameters.ClearStencil)); + } + if (forceFullPush || parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) { + const FloatVec4& blendColor = parameters.BlendColor; + g_GLESFuncs.glBlendColor(blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w()); + } + } + + if (tailSpanDirty) { // Cull face mode + if (forceFullPush || parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) { + const CullFaceMode& cfm = parameters.CullFaceModeSetting; + g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm)); + } + if (forceFullPush || parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) { + const FrontFaceMode& ffm = parameters.FrontFaceModeSetting; + g_GLESFuncs.glFrontFace(MG_Util::ConvertFrontFaceModeToGLEnum(ffm)); + } + } + + if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and + // for the same reason: what has to reach the driver is NOT simply the parameter + // field. RenderStateParameters::ScissorBoxes starts all-zero, which means "the + // application has never called glScissor" - it is not a GL scissor box. GL's + // initial box is the whole window, which the frontend has no way to spell before a + // surface exists. The pre-resync code got away with pushing the field verbatim only + // by accident: the shadow held the same default, the field never compared unequal, + // and the ES context kept its own correct default. Under the forced full push that + // accident is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle, + // and everything drawn with GL_SCISSOR_TEST enabled before the app's first + // glScissor is clipped away - Minecraft 26.2 keeps only its unscissored sky and + // hand and loses the terrain and the whole GUI. + // + // The condition is the WRITTEN FLAG, not the extent. An empty rectangle is a + // perfectly legal thing to ask for - glScissor(0,0,0,0) means "the scissor test + // rejects every fragment" - so testing `width <= 0 || height <= 0` substituted the + // whole surface for a deliberately empty box and inverted the request into "accept + // every fragment", no matter how many times the application had already called + // glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is exactly that: all 16 + // boxes zero-sized with the test enabled, requiring the draw to be clipped away + // entirely. Reading the flag preserves the Minecraft protection bit-for-bit - before + // the first glScissor the bit is clear and the surface size is still substituted - + // while an explicit empty box now reaches the driver verbatim. Negative extents + // cannot arrive here at all: all three entry points reject them with + // GL_INVALID_VALUE before storing (GL_RenderState.cpp's ValidateNonNegativeExtent). + IntVec4 backendScissorBox = parameters.ScissorBoxes[0]; + if ((parameters.ScissorBoxWrittenMask & 1u) == 0) { + Int surfaceWidth = 0; + Int surfaceHeight = 0; + if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { + backendScissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight); + } + } + // Compared against what was actually PUSHED, not against the parameter field, so + // the resolved value and the diff can never disagree. + if (backendScissorBox != g_syncedBackendScissorBox) { + g_GLESFuncs.glScissor(backendScissorBox.x(), backendScissorBox.y(), backendScissorBox.z(), + backendScissorBox.w()); + g_syncedBackendScissorBox = backendScissorBox; + } + } + + if (tailSpanDirty) { // Logic op (first field of the tail span) + // glLogicOp is GLES 1.x / EXT only - eglGetProcAddress returns null for it on a + // plain ES 3.x driver. Before the forced resync it was reached only when an app + // actually set a logic op; now every MakeCurrent would call it, so the null check + // is mandatory rather than defensive. + if (g_GLESFuncs.glLogicOp && + (forceFullPush || parameters.LogicOp != g_syncedRenderStateParameters.LogicOp)) { + g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp)); + } + } + + if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below) + if (forceFullPush || parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor || + parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits || + parameters.PolygonOffsetClamp != g_syncedRenderStateParameters.PolygonOffsetClamp) { + // glPolygonOffset IS glPolygonOffsetClampEXT(factor, units, 0), so where the + // driver has the clamp entry point it carries all three values; without it the + // clamp cannot be expressed and the offset goes through unclamped. + if (g_GLESCapabilities.SupportsPolygonOffsetClamp) { + g_GLESFuncs.glPolygonOffsetClampEXT(parameters.PolygonOffsetFactor, + parameters.PolygonOffsetUnits, + parameters.PolygonOffsetClamp); + } else { + g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits); + } + } + } + + if (headSpanDirty) { // Clip control. GL_ARB_clip_control is only advertised where the + // driver has GL_EXT_clip_control, so without it the state can only be the default. + if (g_GLESCapabilities.SupportsClipControl && + (forceFullPush || parameters.ClipOrigin != g_syncedRenderStateParameters.ClipOrigin || + parameters.ClipDepthMode != g_syncedRenderStateParameters.ClipDepthMode)) { + g_GLESFuncs.glClipControlEXT(parameters.ClipOrigin, parameters.ClipDepthMode); + } + } + + if (headSpanDirty) { // Line width + if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) { + g_GLESFuncs.glLineWidth(parameters.LineWidth); + } + } + + if (headSpanDirty) { // Point size (GLES 1.x only - ES 2+ sets it from gl_PointSize, + // so the entry point is absent on most drivers; see the glLogicOp note above) + if (g_GLESFuncs.glPointSize && + (forceFullPush || parameters.PointSize != g_syncedRenderStateParameters.PointSize)) { + g_GLESFuncs.glPointSize(parameters.PointSize); + } + } + + if (tailSpanDirty) { // Sample coverage + if (forceFullPush || parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue || + parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) { + g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue, + ToGLBoolean(parameters.SampleCoverageInvert)); + } + } + + if (tailSpanDirty) { // Sample mask + if (g_GLESFuncs.glSampleMaski && + (forceFullPush || parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue)) { + g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue); + } + } + + if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core) + // Both halves are gated on the same entry point rather than on a version check: + // GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core / + // OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would + // only push an INVALID_ENUM into the driver's queue. This is NOT part of the + // SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to + // put a guard. + if (g_GLESFuncs.glMinSampleShading) { + if (forceFullPush || + parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) { + if (parameters.SampleShadingEnabled) { + g_GLESFuncs.glEnable(GL_SAMPLE_SHADING); + } else { + g_GLESFuncs.glDisable(GL_SAMPLE_SHADING); + } + } + if (forceFullPush || parameters.MinSampleShadingValue != + g_syncedRenderStateParameters.MinSampleShadingValue) { + g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue); + } + } + } + + if (tailSpanDirty && g_GLESCapabilities.SupportsFragmentShadingRate) { // GL_EXT_fragment_shading_rate + // The frontend stores the packed rate and op indices; the driver takes the enums. + // Only forwarded where the driver has the extension, which is also the only place + // the frontend advertised it - so a non-default value cannot exist anywhere else. + if (forceFullPush || parameters.ShadingRate != g_syncedRenderStateParameters.ShadingRate) { + const GLenum rate = MGUnpackShadingRate(parameters.ShadingRate); + g_GLESFuncs.glShadingRateEXT(rate != GL_NONE ? rate : GL_SHADING_RATE_1X1_PIXELS_EXT); + } + if (forceFullPush || + parameters.ShadingRateCombinerOp0 != g_syncedRenderStateParameters.ShadingRateCombinerOp0 || + parameters.ShadingRateCombinerOp1 != g_syncedRenderStateParameters.ShadingRateCombinerOp1) { + g_GLESFuncs.glShadingRateCombinerOpsEXT(MGShadingRateCombinerOpEnum(parameters.ShadingRateCombinerOp0), + MGShadingRateCombinerOpEnum(parameters.ShadingRateCombinerOp1)); + } + } + + g_syncedRenderStateVersion = currentRenderStateVersion; + // Byte copy, not member copy: it also clones the frontend struct's padding bytes, + // which is what lets the span memcmps above answer "unchanged" exactly instead of + // tripping on indeterminate padding every draw. Only the dirty spans need copying - + // a clean span's bytes are already identical by the very memcmp that skipped it. + auto* syncedBytesMut = reinterpret_cast(&g_syncedRenderStateParameters); + if (headSpanDirty) { + std::memcpy(syncedBytesMut, currentBytes, kBlendSpanBegin); + } + if (blendSpanDirty) { + std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin, + kBlendSpanEnd - kBlendSpanBegin); + // ...except for a draw buffer whose dual-source blend was DECLINED, where the + // frontend block is precisely what did NOT reach the driver. The shadow has to hold + // what was pushed or the next diff compares against state the ES context never got: + // going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides, + // the enable block finds nothing to do, and blending stays off from the decline. + // The span stays permanently "dirty" against the frontend as a result, which costs + // one memcmp plus this block per render-state VERSION change - the top-of-function + // version early-out still skips repeat draws entirely. + for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + if ((dualSourceDeclinedMask & (1u << i)) == 0) continue; + g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i]; + } + } + if (tailSpanDirty) { + std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd, + sizeof(RenderStateParameters) - kBlendSpanEnd); + } + g_hasSyncedRenderState = true; + } + } // namespace RenderStateImpl + + namespace PrgramImpl { + // The twin SyncCurrentProgram resolved for the draw/dispatch being prepared, + // consumed by BindCurrentProgramWithResources and the post-draw draw-parameter + // updates in the same GL entry point — they all used to repeat the registry + // Find. Valid only while the keyed frontend program is the current draw + // program: every consumer compares the raw key against GetProgramForDraw() + // before trusting the twin, and SyncCurrentProgram rewrites the pair at the + // top of every PrepareForDraw/PrepareForCompute, so a recycled address can + // never be consumed (the stale pair is overwritten before any consumer runs). + static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr; + BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr; + // P5e (pg), CONTRACT-P5E.md §5.5: THE SAME STASH, KEYED ON THE HANDLE. The raw frontend + // pointer above is one of the five frontend-keyed reads this family retires: every + // consumer compared it against GetProgramForDraw().get(), which under run-ahead is a + // pointer to an object the client may already have relinked or freed. The handle arm + // compares against MGPipeApplier().DrawProgram instead - a {slot, gen} the record + // carried, which cannot be recycled behind the server's back because the gen moves with + // the slot. + // + // BOTH HALVES SURVIVE because the two arms do (ruling 1): the pointer is the monolith + // glue's key and the handle is the transport's, and SyncCurrentProgram* clears the one + // it does not use so a stale key can never be consumed. + MG_Pipe::MGPipeHandle g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle; + + // Memo of the per-draw enabled-draw-buffers walk feeding g_fragColorBroadcastCount: + // the answer is a pure function of WHICH FBO is bound and its draw-buffer edits, so + // it is keyed exactly like SyncCurrentFBO's synced trio - object pointer (identity), + // slot version (a recycled heap address cannot re-match: every real rebind bumps + // it), and object version (every attachment/draw-buffer/read-buffer edit bumps it, + // the same documented invariant the FBO sync memo already leans on). A null FBO + // keys on (nullptr, slot version, 0). + static const MG_State::GLState::FramebufferObject* g_broadcastMemoFbo = nullptr; + static Uint16 g_broadcastMemoSlotVersion = 0; + static Uint16 g_broadcastMemoObjectVersion = 0; + Bool g_broadcastMemoValid = false; + static Uint g_broadcastMemoCount = 1; + + // P4a e1 (D-C4). The handle arm's key is the framebuffer record's ContentHash, and it + // is a COMPLETE key for this answer rather than a cheaper approximation of the trio + // above: the hash is taken over every field the record carries - Fbo included, so a + // recycled framebuffer handle cannot be suppressed against its predecessor's record, + // and DrawBuffers[8] included, so an unchanged hash provably means the draw-buffer + // array did not move, which provably means the broadcast count did not move. + // + // The ORDERING that made this read come from the frontend is preserved and is what + // makes reading a record legal here: the client emits every set_* BEFORE the verb and + // the server specialises AT the verb, so by the time this runs the record already + // describes the framebuffer this draw will use - which is exactly the property the + // frontend read was buying (a program compiled against a stale count would otherwise + // not be relinked until the draw after the one that needed it). + // + // Separate statics rather than reusing the trio's: FramebufferSerial goes from 0 to + // non-zero exactly once in a process's life, so both arms can run in one run, and one + // set of fields carrying two key shapes is how a stale half is compared against a live + // one. + static Uint64 g_broadcastMemoContentHash = 0; + Bool g_broadcastMemoHandleValid = false; + static Uint g_broadcastMemoHandleCount = 1; + + // P4a e4 (D-H6, D-H7). The ShaderCso record behind a handle, WITH the composite band + // resolved: the band starts at kMGPipeShaderCsoCompositeSlotBase and the applier keeps + // it in a second dense table indexed by (slot - base), because one composite in the + // slot-indexed vector would grow it to ~983k records. The SERVER STILL NEVER LEARNS IT + // IS A COMPOSITE - a composite handle is an ordinary ShaderCso handle and every call + // names it as one; this is an indexing detail, and it lives in one place so that only + // one reader has to know it. + const MG_Pipe::MGPipeShaderCsoRecord* FindShaderCsoRecord(MG_Pipe::MGPipeHandle handle) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; + const auto& st = MG_Pipe::MGPipeApplier(); + const Vector* table = nullptr; + Uint32 index = 0; + if (MG_Pipe::MGPipeIsCompositeShaderSlot(handle.Slot)) { + table = &st.CompositeShaderCsos; + index = handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase; + } else { + table = &st.ShaderCsos; + index = handle.Slot; + } + // P4a decline-site P2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, and it + // TELLS THE TWO BANDS APART as the review requires. An out-of-range ORDINARY slot + // means the create_shader_state for this program never arrived - a client seam. + // An out-of-range COMPOSITE index is a different finding entirely: that table is + // dense and its band base is a fixed contract constant, so a handle above its end + // is a mis-decoded slot, i.e. a CONTRACT-side bug, and saying "the record has not + // arrived" about it would send the reader to the wrong package. + if (index >= table->size()) { + if (MG_Pipe::MGPipeIsCompositeShaderSlot(handle.Slot)) { + MGLOG_E_ONCE("A program record does not describe the binding it names: composite " + "ShaderCso {slot %u, gen %u} decodes to band index %u but the " + "composite table holds %llu - the band base or the decode is wrong, " + "not the emission; running the frontend's own uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + static_cast(index), + static_cast(table->size())); + } else { + MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso " + "{slot %u, gen %u} is above the applier's table of %llu record(s), so " + "no create_shader_state for it has been applied; running the " + "frontend's own uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + static_cast(table->size())); + } + return nullptr; + } + const MG_Pipe::MGPipeShaderCsoRecord& record = (*table)[index]; + // P4a decline-site P3: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, and it + // is ID-8's stale-generation refusal: every GetOrCreate(handle) in the phase owes + // one. On this tree a bound program whose record is dead, or whose generation has + // moved under the handle, is a death/recycle seam - the emitter kept handing out + // a handle it had retired, or minted a successor at the slot and never described + // it - and the two causes are named apart because they point at different halves + // of the client. + if (!record.Live || record.Gen != handle.Gen) { + MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso " + "{slot %u, gen %u} finds a %s record (generation %u); running the " + "frontend's own uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + record.Live ? "live but differently-generationed" : "dead", + static_cast(record.Gen)); + return nullptr; + } + return &record; + } + + // The record whose GLOBAL CONSTANTS may be uploaded for this program, or null. + // + // Four things have to hold before the default uniform block is taken off a record + // rather than off the frontend program, and each of them is a way the two could + // legitimately disagree on a tree whose client half is mid-landing: + // + // * the program has a handle at all, and the record at it is live; + // * the record's own Desc names that same handle - the identity check, and the seam + // where a mis-keyed emission becomes visible instead of becoming a wrong upload; + // * the version is not the ~0u NEVER-UPLOADED SENTINEL, which the client must never + // emit and which is what the record starts at; + // * the block image is at least as long as the program says the block is, because + // the upload copies GetUBOSize() bytes and a short record would read past it. + // + // Anything else and the frontend's own MapUBO answers, exactly as it does today. + // P5e (pg): `uboSize` is what the block image is checked against, and it is a PARAMETER + // now rather than a `program->GetUBOSize()` read inside the body - on the handle arm the + // size is the record's own Desc.GlobalUboSize and there is no ProgramObject to ask. + // `program` is null on that arm and is used for nothing but the monolith identity + // resolution below. + const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecordForHandle(MG_Pipe::MGPipeHandle handle, + Uint uboSize) { + const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(handle); + // P4a decline-site P4: S - FOLDED INTO P2/P3 AT THE VERIFICATION ROUND, which is + // what the review asked for and is why this one stays silent: both reasons + // FindShaderCsoRecord can answer null have just spoken for themselves, with the + // band and the generation named. A third line here would say less and fire on + // every one of them. + if (record == nullptr) return nullptr; + // P4a decline-site P5: S - THE PROGRAM SEAM, loud (MAJOR-3) and in the same + // words as the framebuffer and image seams, so one grep polices all three. No + // further flip. Unlike the image seam this one has no eager funnel to be wrong + // at: the only caller is the global-UBO upload at the draw validate point, where + // the record for THIS draw has been applied, so a descriptor that names a + // different handle than the slot it sits in is always a mis-keyed emission. + if (record->Desc.Cso != handle) { + MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso " + "record at {slot %u, gen %u} names {slot %u, gen %u} in its own " + "descriptor; running the frontend's own uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + static_cast(record->Desc.Cso.Slot), + static_cast(record->Desc.Cso.Gen)); + return nullptr; + } + // P4a decline-site P6: M, then S - FLIPPED AT THE VERIFICATION ROUND, and the + // condition the review names is already the CALLER'S, so the test is not repeated + // here. The ~0u NEVER-UPLOADED SENTINEL is what the record starts at and is + // legitimate before the first set_global_constants for a program. But the only + // caller of this function is the global-UBO upload, which runs inside + // `currentProgram->GetUBOSize() > 0 && backendProgram.HasGlobalUboBlock()` - so + // reaching this line at all means bit 12 is on, this program HAS a default + // uniform block, and it is being drawn with no global constants ever emitted for + // it. That is the seam defect, and it says so. + if (record->GlobalConstantsVersion == ~Uint32{0}) { + MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso " + "{slot %u, gen %u} still carries the never-uploaded sentinel at a draw " + "of a program with a %u-byte default uniform block; running the " + "frontend's own uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + static_cast(uboSize)); + return nullptr; + } + // P4a decline-site P7: S, and LOUD (ID-19) - this one is not a missing record, + // it is PROTOCOL CORRUPTION. The upload copies GetUBOSize() bytes out of + // GlobalConstants, so a shorter block image is a read past the end of the + // applier's own buffer; no correct emitter can produce it, and quietly handing + // the draw back to MapUBO() would file a memory-safety bug as a perf regression. + // + // THE VERDICT IS THE ONE THE CONTRACT ALREADY SPELLS, and deliberately not a new + // one: PipeApply.cpp's "the verdict of every trip wire in this file, in one + // place" says a poison or verify build STOPS and writes the Fatal{} marker G4 + // greps the retrace logs for, while a shipped push build logs at error level and + // carries on from a defined state. So the gate lanes - which is where a seam + // defect has to be caught - never reach MapUBO() at all, and a shipped build does + // not abort a running game over a client-side bug. If the integrator wants the + // stop on every arm, it is one #if away and this comment is where to say so. + if (record->GlobalConstants.size() < static_cast(uboSize)) { +#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY + MGLOG_F("MGPipe: Fatal{ProtocolCorruption} " + "A program record does not describe the binding it names: the ShaderCso " + "record {slot %u, gen %u} carries a %llu-byte global-constant block for a " + "%llu-byte default uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + static_cast(record->GlobalConstants.size()), + static_cast(uboSize)); + std::abort(); +#else + MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso " + "record {slot %u, gen %u} carries a %llu-byte global-constant block for a " + "%llu-byte default uniform block; running the frontend's own uniform block.", + static_cast(handle.Slot), static_cast(handle.Gen), + static_cast(record->GlobalConstants.size()), + static_cast(uboSize)); +#endif + return nullptr; + } + return record; + } + + // Read from the frontend rather than from the backend framebuffer sync, which + // only runs later in PrepareForDraw: a program compiled against a stale count + // would not be relinked until the draw after the one that needed it. + // + // P5e (pg): ITS OWN FUNCTION AND ITS OWN POLICY, because the three callers want three + // different answers when the record arm has nothing to say: + // + // FrontendFallback - the monolith arm, unchanged: read the draw-FBO binding slot. + // RefuseFallback - the handle arm AT A DRAW. With a live wire that binding slot is a + // BARRIER_PULLED row the client may already have moved, so a count + // read from it would compile the program against a framebuffer that + // is not the one being drawn to - silently, and only sometimes. + // The record is the answer or there is no answer (rule F). + // RecordOnly - the handle arm AT A DISPATCH. A compute program has no fragment + // stage and a dispatch has no draw framebuffer, so this count is not + // an input to what is being built; PrepareForCompute also does not + // run SyncCurrentFBO, so the framebuffer records are legitimately + // untrusted here and a refusal would abort a correct program. The + // previous resolution stands, which is what the monolith arm + // effectively does too when nothing rebound between the two calls. + enum class BroadcastCountPolicy { FrontendFallback, RefuseFallback, RecordOnly }; + + static void ResolveFragColorBroadcastCount(BroadcastCountPolicy policy) { + Bool broadcastCountResolved = false; + // The trust latch is fresh here and costs nothing: PrepareForDraw runs + // SyncCurrentFBO immediately before this, so the records have just been + // checked against the two bindings. + if (FramebufferImpl::g_fboRecordsTrusted) { + const MG_Pipe::MGPFramebufferState* const recordPtr = + BoundFramebufferRecord(FramebufferTarget::Draw); + // P4a decline-site F7: FLIPPED AT THE VERIFICATION ROUND from a silent + // decline to an assertion, because it is unreachable and a silent decline + // here is indistinguishable from a legitimate one. g_fboRecordsTrusted + // implies a record with a non-null handle on BOTH targets: + // SyncCurrentFBOByRecord sets the latch only after F2 has rejected a null + // pointer or handle on either, and it runs immediately before this in + // PrepareForDraw. Kept as a checked decline rather than deleted so a + // release build cannot dereference null if that ordering ever changes. + MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)); + if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) { + const MG_Pipe::MGPFramebufferState& record = *recordPtr; + if (!g_broadcastMemoHandleValid || g_broadcastMemoContentHash != record.ContentHash) { + Uint enabledDrawBuffers = 0; + for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + // -1 is FramebufferAttachmentType::None on the wire; every + // other value is an attachment index. + if (record.DrawBuffers[i] >= 0) { + enabledDrawBuffers = i + 1; + } + } + g_broadcastMemoContentHash = record.ContentHash; + g_broadcastMemoHandleCount = std::max(enabledDrawBuffers, 1); + g_broadcastMemoHandleValid = true; + } + g_fragColorBroadcastCount = g_broadcastMemoHandleCount; + broadcastCountResolved = true; + } + } + if (policy == BroadcastCountPolicy::RecordOnly) return; + if (policy == BroadcastCountPolicy::RefuseFallback && !broadcastCountResolved) { + // The record arm declined under a transport. g_fboRecordsTrusted is set by + // SyncCurrentFBOByRecord, which PrepareForDraw runs immediately before this, + // so reaching here means the framebuffer family's own records are missing or + // untrusted - and the frontend fallback below is exactly the read this phase + // exists to retire. + MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, " + "\"GetFramebufferBindingSlot@SyncCurrentProgram\"} - the draw " + "framebuffer's record did not answer the fragColor broadcast count on " + "the handle arm, and the frontend binding slot is a row the client owns"); + std::abort(); + } + if (!broadcastCountResolved) { + const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw); + const auto& drawFBO = drawSlot.GetBoundObject(); + const Uint16 slotVersion = drawSlot.GetVersion(); + const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0; + if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() || + g_broadcastMemoSlotVersion != slotVersion || + g_broadcastMemoObjectVersion != objectVersion) { + Uint enabledDrawBuffers = 0; + if (drawFBO) { + const auto& drawBuffers = drawFBO->GetDrawBuffers(); + for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) { + if (drawBuffers[i] != FramebufferAttachmentType::None) { + enabledDrawBuffers = i + 1; + } + } + } + g_broadcastMemoFbo = drawFBO.get(); + g_broadcastMemoSlotVersion = slotVersion; + g_broadcastMemoObjectVersion = objectVersion; + g_broadcastMemoCount = std::max(enabledDrawBuffers, 1); + g_broadcastMemoValid = true; + } + g_fragColorBroadcastCount = g_broadcastMemoCount; + } + } + + // P5e (pg), CONTRACT-P5E.md §5.5: THE SAME SYNC, FROM THE HANDLE THE RECORD CARRIED. + // Two overloads and not an `#if` inside one body (ruling 1 / ID-81): the frontend one + // above is visibly the monolith-glue half, this one names no frontend object at all. + // + // WHAT THE NINE-CLAUSE CONDITION BECOMES, and the clause COUNT does not shrink - its + // inputs move (D-H5, §5.5): + // GetLinkVersion() -> record.Serial vs GetSyncedShaderCsoSerial() + // GetImageUnitVersion() -> record.BindingsSerial vs GetSyncedBindingsSerial() + // ComputeShaderStorage...() -> record.Signature + // GetLinkStatus/SpirvStatus -> Desc.LinkStatus / Desc.SpirvStatus, above + // and the clamp masks, the broadcast count, ImageUnitFormatsStillMatch and the three + // patch clauses are unchanged - they are backend globals, fb's row and VALUE rows of the + // residual block, none of which this family owns. + void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + + g_currentDrawFrontendProgram = nullptr; + g_currentDrawBackendProgram = nullptr; + g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle; + + const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(cso); + // THE SAME THREE-PART TEST THE FRONTEND ARM MAKES, from the descriptor. LinkStatus + // is a field rather than an implication (ID-88): 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, so "a record exists" and "the program linked" are different + // statements. A program the frontend reports unlinked draws nothing, which is + // exactly what the monolith arm does with GetLinkStatus() == false. + if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) { + g_GLESFuncs.glUseProgram(0); + g_lastUsedBackendProgramId = 0; + return; + } + // REFUSED at a draw, record-only at a dispatch: see the policy's own note. + ResolveFragColorBroadcastCount(forDraw ? BroadcastCountPolicy::RefuseFallback + : BroadcastCountPolicy::RecordOnly); + + BackendProgramObjectImpl* const twin = ResolveProgramTwin(cso); + if (twin == nullptr) { + // ResolveProgramTwin has already named the handle. A draw with no twin binds + // nothing, which is the visible no-op Use() makes of an unusable program rather + // than a draw with somebody else's shader. + g_GLESFuncs.glUseProgram(0); + g_lastUsedBackendProgramId = 0; + return; + } + if (!twin->GetBackendProgramId() || + twin->GetSyncedShaderCsoSerial() != record->Serial || + twin->GetSyncedBindingsSerial() != record->BindingsSerial || + twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask || + twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask || + twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount || + twin->GetShaderStorageBlockBindingSignature() != record->Signature || + !twin->ImageUnitFormatsStillMatch() || + (twin->GetPassthroughTessControlPatchVertices() >= 0 && + (twin->GetPassthroughTessControlPatchVertices() != + static_cast(MG_Pipe::gPipeInputs->GetPatchVertices()) || + !BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(), + MG_Pipe::gPipeInputs->GetPatchDefaultOuterLevel()) || + !BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(), + MG_Pipe::gPipeInputs->GetPatchDefaultInnerLevel())))) { + twin->SyncToBackendByHandle(cso); + } + g_currentDrawProgramHandle = cso; + g_currentDrawBackendProgram = twin; + } + } // namespace PrgramImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp new file mode 100644 index 00000000..bd75f00c --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp @@ -0,0 +1,680 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESSync.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace { + // Backend fence handle: a native ES sync plus the ES context + // generation it was created under. + struct GLESSyncObject { + GLsync esSync = nullptr; + Uint contextGeneration = 0; + }; + + // Backend timer-query handle: a native GL query object name plus the + // ES context generation it was created under. Stale-generation + // handles read as available with a zero result, and deleting them + // only frees the wrapper (the dead ES context already reclaimed the + // query object). + struct GLESQueryObject { + GLuint queryId = 0; + Uint contextGeneration = 0; + // Non-zero for the core (non-timer) query targets - occlusion and transform + // feedback primitives - and then holds the glBeginQuery target, which glEndQuery + // needs back. Their results are counts reachable only through the core + // glGetQueryObjectuiv getter: GL_EXT_disjoint_timer_query's 64-bit + // glGetQueryObjectui64vEXT is timer-specific and may be entirely absent on + // drivers that otherwise fully support these core ES queries. + GLenum coreTarget = 0; + }; + } + // EGL ground-truth verification stamp. glvnd's eglGetCurrentContext performs + // fork detection with a real getpid() syscall on every call, and this + // predicate sits 2-3 deep in every draw - measured at 16% of the render + // thread on a live workload. NOT thread_local although the stamp is + // per-owner-thread state: a shared-library thread_local costs a + // __tls_get_addr call per access, which itself showed at 1.5% of the draw + // loop. Plain globals are equivalent because only the thread that passes the + // owner-identity check below can ever stamp or trust them, only one thread + // can be the owner at a time, and the ONLY writers of the owner id - + // MakeCurrent/ReleaseCurrent - reset the stamps before publishing a new + // owner, so a stamp can never leak across an ownership change. Atomics + // (relaxed) because the resets may come from a non-owner thread; the + // owner-id release/acquire pairing orders them. + std::atomic g_eglVerifiedFrameSerial{~0ull}; + std::atomic g_eglVerifiedContextGeneration{0}; + + Bool IsBackendContextCurrentOnThisThread() { + const NativeContextTuple& tuple = ActiveNativeContext(); + if (tuple.Context == EGL_NO_CONTEXT) { + return false; + } + if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) { + return false; + } + // Belt and braces: EGL itself is the ground truth. A migration that bypassed + // MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim + // standing, or GL calls would silently no-op while shadow bookkeeping (bind + // cache, synced serials) still advances. Re-verify once per (frame, context + // generation) rather than per call: an external migration is caught at the + // next frame boundary instead of the next call, which recovers the + // bookkeeping just the same, without paying a syscall on every draw. + const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed); + if (g_eglVerifiedFrameSerial.load(std::memory_order_relaxed) == frameSerial && + g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) { + return true; + } + if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != tuple.Context) { + return false; + } + g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed); + g_eglVerifiedContextGeneration.store(g_syncContextGeneration, std::memory_order_relaxed); + return true; + } + + BackendSyncHandle FenceSync() { + // ES fences can only be created on the thread that owns the ES context + // (Flywheel and friends fence on the render thread, which does). + // Returning null makes the frontend fall back to an always-signaled + // sync object. + if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glFenceSync) { + return nullptr; + } + GLsync esSync = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + if (esSync == nullptr) { + return nullptr; + } + return new GLESSyncObject{esSync, g_syncContextGeneration}; + } + + GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { + const auto* sync = static_cast(handle); + if (sync == nullptr) { + return GL_ALREADY_SIGNALED; + } + // The creating ES context is gone: its GPU work either completed or + // died with the context; waiting is meaningless either way. + if (sync->contextGeneration != g_syncContextGeneration) { + return GL_ALREADY_SIGNALED; + } + // Degraded path: a thread that does not own the ES context cannot + // issue GL calls, so report signaled instead of blocking on state we + // cannot observe. Fence waits normally arrive on the render thread, + // which owns the context. + if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) { + return GL_ALREADY_SIGNALED; + } + return g_GLESFuncs.glClientWaitSync(sync->esSync, flags & GL_SYNC_FLUSH_COMMANDS_BIT, timeout); + } + + void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { + (void)flags; + (void)timeout; + const auto* sync = static_cast(handle); + if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration || + !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glWaitSync) { + return; + } + // ES 3.0 requires flags == 0 and timeout == GL_TIMEOUT_IGNORED. + g_GLESFuncs.glWaitSync(sync->esSync, 0, GL_TIMEOUT_IGNORED); + } + + void DeleteSync(BackendSyncHandle handle) { + auto* sync = static_cast(handle); + if (sync == nullptr) { + return; + } + if (sync->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() && + g_GLESFuncs.glDeleteSync) { + g_GLESFuncs.glDeleteSync(sync->esSync); + } + // Otherwise the ES sync is abandoned; the ES context reclaims all of + // its sync objects when it is destroyed. + delete sync; + } + + Bool GetSyncStatus(BackendSyncHandle handle) { + const auto* sync = static_cast(handle); + if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration || + !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetSynciv) { + return true; + } + GLint status = GL_SIGNALED; + GLsizei length = 0; + g_GLESFuncs.glGetSynciv(sync->esSync, GL_SYNC_STATUS, 1, &length, &status); + return status == GL_SIGNALED; + } + + // GL timer queries, backed by GL_EXT_disjoint_timer_query. The desktop + // tokens from glext.h are used throughout: GL_TIME_ELAPSED (0x88BF), + // GL_TIMESTAMP (0x8E28), GL_QUERY_RESULT (0x8866) and + // GL_QUERY_RESULT_AVAILABLE (0x8867) are numerically identical to their + // _EXT counterparts. + + Bool AreTimerQueriesSupported() { + return g_GLESCapabilities.SupportsDisjointTimerQuery && g_GLESFuncs.glGenQueries && + g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery && + g_GLESFuncs.glGetQueryObjectuiv && g_GLESFuncs.glQueryCounterEXT && + g_GLESFuncs.glGetQueryObjectui64vEXT; + } + + namespace { + // The entry point the resolved tier's support ships, or null when there is none. + MG_External::GLES::glTexBuffer_PTR ResolveTexBufferEntryPoint() { + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + switch (g_GLESCapabilities.TextureBufferSupport) { + case Tier::ExtensionEXT: + return g_GLESFuncs.glTexBufferEXT ? g_GLESFuncs.glTexBufferEXT : g_GLESFuncs.glTexBuffer; + case Tier::ExtensionOES: + return g_GLESFuncs.glTexBufferOES ? g_GLESFuncs.glTexBufferOES : g_GLESFuncs.glTexBuffer; + case Tier::CoreEs32: + return g_GLESFuncs.glTexBuffer; + case Tier::None: + default: + return nullptr; + } + } + + MG_External::GLES::glTexBufferRange_PTR ResolveTexBufferRangeEntryPoint() { + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + switch (g_GLESCapabilities.TextureBufferSupport) { + case Tier::ExtensionEXT: + return g_GLESFuncs.glTexBufferRangeEXT ? g_GLESFuncs.glTexBufferRangeEXT + : g_GLESFuncs.glTexBufferRange; + case Tier::ExtensionOES: + return g_GLESFuncs.glTexBufferRangeOES ? g_GLESFuncs.glTexBufferRangeOES + : g_GLESFuncs.glTexBufferRange; + case Tier::CoreEs32: + return g_GLESFuncs.glTexBufferRange; + case Tier::None: + default: + return nullptr; + } + } + } // namespace + + Bool AreBufferTexturesSupported() { + // Both halves matter. The tier is what the driver ADVERTISES, and it is only meaningful + // once the capabilities have been filled in; the resolved pointer is what MobileGL can + // actually call, through the spelling that tier's support ships. Gating on the + // unsuffixed name alone would call an entry point an EXT/OES driver never exported. + return g_GLESCapabilities.TextureBufferSupport != + MG_External::GLESCapabilities::TextureBufferTier::None && + ResolveTexBufferEntryPoint() != nullptr; + } + + void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer) { + MG_External::GLES::glTexBuffer_PTR entryPoint = ResolveTexBufferEntryPoint(); + if (entryPoint == nullptr) { + return; + } + entryPoint(target, internalFormat, buffer); + } + + Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size) { + MG_External::GLES::glTexBufferRange_PTR entryPoint = ResolveTexBufferRangeEntryPoint(); + if (entryPoint == nullptr) { + return false; + } + entryPoint(target, internalFormat, buffer, offset, size); + return true; + } + + const char* GetBufferTextureTierName() { + using Tier = MG_External::GLESCapabilities::TextureBufferTier; + switch (g_GLESCapabilities.TextureBufferSupport) { + case Tier::CoreEs32: + return "core (ES 3.2)"; + case Tier::ExtensionEXT: + return "GL_EXT_texture_buffer"; + case Tier::ExtensionOES: + return "GL_OES_texture_buffer"; + case Tier::None: + default: + return "unsupported"; + } + } + + BackendQueryHandle BeginTimeElapsedQuery() { + // Query objects can only be created on the thread that owns the ES + // context (MC's F3 profiler queries on the render thread, which + // does). Returning null makes the frontend fall back to an + // immediately available zero result. + if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) { + return nullptr; + } + GLuint queryId = 0; + g_GLESFuncs.glGenQueries(1, &queryId); + if (queryId == 0) { + return nullptr; + } + g_GLESFuncs.glBeginQuery(GL_TIME_ELAPSED, queryId); + return new GLESQueryObject{queryId, g_syncContextGeneration}; + } + + void EndTimeElapsedQuery(BackendQueryHandle handle) { + const auto* query = static_cast(handle); + if (query == nullptr || query->contextGeneration != g_syncContextGeneration || + !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) { + return; + } + // ES tracks the active query per target, not per object, so the + // handle only guards the degraded paths above. + g_GLESFuncs.glEndQuery(GL_TIME_ELAPSED); + } + + BackendQueryHandle QueryCounterTimestamp() { + if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) { + return nullptr; + } + GLuint queryId = 0; + g_GLESFuncs.glGenQueries(1, &queryId); + if (queryId == 0) { + return nullptr; + } + g_GLESFuncs.glQueryCounterEXT(queryId, GL_TIMESTAMP); + return new GLESQueryObject{queryId, g_syncContextGeneration}; + } + + // The core (non-timer) query targets - occlusion and transform feedback primitives. + // Unlike the timer queries above these are core ES (no GL_EXT_disjoint_timer_query + // needed), so they share one begin/end pair keyed on the glBeginQuery target. + static Bool AreCoreQueriesSupported() { + return g_GLESFuncs.glGenQueries && g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && + g_GLESFuncs.glEndQuery && g_GLESFuncs.glGetQueryObjectuiv; + } + + static BackendQueryHandle BeginCoreQuery(GLenum target) { + if (!IsBackendContextCurrentOnThisThread() || !AreCoreQueriesSupported()) { + return nullptr; + } + GLuint queryId = 0; + g_GLESFuncs.glGenQueries(1, &queryId); + if (queryId == 0) { + return nullptr; + } + g_GLESFuncs.glBeginQuery(target, queryId); + return new GLESQueryObject{queryId, g_syncContextGeneration, target}; + } + + static void EndCoreQuery(BackendQueryHandle handle) { + const auto* query = static_cast(handle); + if (query == nullptr || query->coreTarget == 0 || + query->contextGeneration != g_syncContextGeneration || !IsBackendContextCurrentOnThisThread() || + !g_GLESFuncs.glEndQuery) { + return; + } + g_GLESFuncs.glEndQuery(query->coreTarget); + } + + BackendQueryHandle BeginOcclusionQuery() { + // ES only implements the boolean ANY_SAMPLES_PASSED variant, not an exact + // GL_SAMPLES_PASSED count; the frontend already coerces ANY_SAMPLES_PASSED* + // targets to boolean, and desktop GL_SAMPLES_PASSED reads a 0/1 approximation. + return BeginCoreQuery(GL_ANY_SAMPLES_PASSED); + } + + void EndOcclusionQuery(BackendQueryHandle handle) { EndCoreQuery(handle); } + + // GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED. The frontend + // otherwise counts primitives on the CPU from the draw calls, which cannot see a + // geometry shader's amplification; the real driver's counters are exact. Returning + // null keeps that CPU accounting as the fallback. + BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) { + // GL_PRIMITIVES_GENERATED is only a legal query target from ES 3.2 on (it comes + // with geometry shaders); issuing it earlier just leaves a stray GL_INVALID_ENUM + // that some later unrelated glGetError would report as its own failure. + if (generated && g_GLESCapabilities.GLESVersion.Major * 10 + g_GLESCapabilities.GLESVersion.Minor < 32) { + return nullptr; + } + return BeginCoreQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + } + + void EndXfbPrimitivesQuery(BackendQueryHandle handle) { EndCoreQuery(handle); } + + Bool IsQueryResultAvailable(BackendQueryHandle handle) { + const auto* query = static_cast(handle); + // Null/stale handles report available so the frontend proceeds to + // GetQueryResult64, which finalizes them as zero. A thread that does + // not own the ES context also reports available: GetQueryResult64 + // then returns false and the frontend keeps the handle for a later + // read from the owning thread. + if (query == nullptr || query->contextGeneration != g_syncContextGeneration || + !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetQueryObjectuiv) { + return true; + } + GLuint available = GL_FALSE; + g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available); + return available != GL_FALSE; + } + + Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) { + *outNanoseconds = 0; + const auto* query = static_cast(handle); + // Null handles never had a GL query object, handles from a + // since-destroyed ES context lost theirs, and missing entry points + // can never produce a reading (belt and braces: the creators already + // require them): zero is the FINAL result in all three cases, so + // report it as produced and let the frontend cache it and release + // the handle. + if (query == nullptr || query->contextGeneration != g_syncContextGeneration || + !g_GLESFuncs.glGetQueryObjectuiv || + (query->coreTarget == 0 && !g_GLESFuncs.glGetQueryObjectui64vEXT)) { + return true; + } + // A thread that does not own the ES context cannot issue GL calls, + // but the result still lands on the owning context eventually: report + // "not obtainable yet" so the frontend keeps the handle and a later + // availability poll / result read from the owning thread can still + // produce the real value. + if (!IsBackendContextCurrentOnThisThread()) { + return false; + } + if (wait) { + // Reading GL_QUERY_RESULT blocks in the driver until the result + // lands, but only after the commands were flushed; flush once, + // then poll availability for a bounded ~100ms before dropping to + // a glFinish as the last resort (ClientWaitSync has no polling + // loop to mirror - it delegates its timeout to the driver, which + // a query-object read cannot do). + if (g_GLESFuncs.glFlush) { + g_GLESFuncs.glFlush(); + } + constexpr Int kMaxAvailabilityPolls = 1000; // ~100ms at 100us per poll + GLuint available = GL_FALSE; + for (Int i = 0; i < kMaxAvailabilityPolls && available == GL_FALSE; ++i) { + g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available); + if (available == GL_FALSE) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + if (available == GL_FALSE && g_GLESFuncs.glFinish) { + g_GLESFuncs.glFinish(); + } + } + // GL_EXT_disjoint_timer_query's GPU_DISJOINT_EXT signal is + // deliberately ignored: after a disjoint event (power state change, + // context switch) the result may be garbage, which is tolerable for + // an F3 GPU% readout, and consuming the latched flag here could hide + // the event from another observer. + if (query->coreTarget != 0) { + GLuint result32 = 0; + g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT, &result32); + *outNanoseconds = static_cast(result32); + return true; + } + GLuint64 result = 0; + g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result); + *outNanoseconds = static_cast(result); + return true; + } + + void DeleteBackendQuery(BackendQueryHandle handle) { + auto* query = static_cast(handle); + if (query == nullptr) { + return; + } + if (query->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() && + g_GLESFuncs.glDeleteQueries) { + g_GLESFuncs.glDeleteQueries(1, &query->queryId); + } + // Otherwise the GL query object is abandoned; the ES context reclaims + // all of its query objects when it is destroyed. + delete query; + } + + Int64 GetGpuTimestampNs() { + // Synchronous GPU clock sample; 0 tells the frontend GL_TIMESTAMP + // getter to fall back. + if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported() || + !g_GLESFuncs.glGetInteger64v) { + return 0; + } + GLint64 timestamp = 0; + g_GLESFuncs.glGetInteger64v(GL_TIMESTAMP, ×tamp); + return static_cast(timestamp); + } + + Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); } + Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); } + + namespace { + // THE WATERMARK IS CONTIGUOUS. Fences signal in submission order only within ONE context, + // and the serials are handed out to every native context of every session in turn, so the + // highest signaled serial says nothing about an older fence of another context still + // pending. A serial is complete when no live fence at or below it is pending. + void PollFrameFences() { + if (!g_GLESFuncs.glGetSynciv) return; + Uint64 lowestPending = ~Uint64{0}; + for (FrameFence& slot : g_frameFenceRing) { + if (!slot.sync) continue; + if (slot.contextGeneration != g_syncContextGeneration) { + slot.sync = nullptr; // its context reclaimed it, and its work is over + continue; + } + GLint status = GL_SIGNALED; + GLsizei length = 0; + g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status); + if (status == GL_SIGNALED) { + if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); + slot.sync = nullptr; + } else if (slot.serial < lowestPending) { + lowestPending = slot.serial; + } + } + const Uint64 completed = + lowestPending == ~Uint64{0} ? g_currentFrameSerial.load(std::memory_order_relaxed) : lowestPending - 1; + if (completed > g_completedFrameSerial.load(std::memory_order_relaxed)) { + g_completedFrameSerial.store(completed, std::memory_order_relaxed); + } + } + + FrameFence* OldestPendingFrameFence() { + FrameFence* oldest = nullptr; + for (FrameFence& slot : g_frameFenceRing) { + if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; + if (!oldest || slot.serial < oldest->serial) oldest = &slot; + } + return oldest; + } + } // namespace + + Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) { + if (CompletedFrameSerial() >= serial) return true; + if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false; + // Oldest pending fence first: the watermark can only pass a serial once every older fence + // (of any context) has signaled. Each fence was flushed when it was made (Present's swap), + // so waiting from this context needs no flush of the other. + for (int guard = 0; guard < kFrameFenceRingDepth + 1; ++guard) { + PollFrameFences(); + if (CompletedFrameSerial() >= serial) return true; + FrameFence* oldest = OldestPendingFrameFence(); + if (!oldest) return false; + const GLenum status = g_GLESFuncs.glClientWaitSync(oldest->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs); + if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false; + } + PollFrameFences(); + return CompletedFrameSerial() >= serial; + } + +#if MOBILEGL_BUILD_DISAGGREGATED + namespace { + // Once per thread: a latched session declines everything after this verb anyway, and the + // apply thread is the session's own. + thread_local Bool t_deviceLossLatched = false; + + Bool LatchOnResetStatus(const char* site, GLenum status) { + if (status == GL_NO_ERROR) return false; + // ONLY THE GUILTY CONTEXT ENDS ITS SESSION. The contexts ask for reset notification + // (EnsureNativeContext) so the driver can name the context whose work hung or faulted the + // GPU; a reset it reports to an INNOCENT context - one whose work only waited behind the + // stall, the compositor's above all - is noted and the session goes on as it did before + // contexts were told. Adreno 750 measured: the guilty context reads GUILTY once, after + // which the driver drops everything it submits; innocent ones read NO_ERROR. + static thread_local Bool innocentNoted = false; + if (status == GL_INNOCENT_CONTEXT_RESET) { + if (!innocentNoted) { + innocentNoted = true; + MGLOG_W("Espryt:%s - the driver reports a GPU reset this session's context did not cause " + "(GL_INNOCENT_CONTEXT_RESET); the session goes on", + site != nullptr ? site : "?"); + } + return false; + } + t_deviceLossLatched = true; + (void)MG_Pipe::MGPipeSessionLatch(MG_Pipe::MGPipeFatalFamily::DeviceLost, + "MGPipe: Fatal{BackendDeviceLost, \"Espryt:%s\"} - this session's EGL " + "context was reset (glGetGraphicsResetStatus 0x%x); the session ends " + "and its client reads a lost context, every other session keeps running", + site != nullptr ? site : "?", static_cast(status)); + return true; + } + } // namespace + + Bool LatchIfDeviceLost(const char* site, Bool debugKnob) { + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false; + if (t_deviceLossLatched) return true; + GLenum status = GL_NO_ERROR; + if (g_GLESFuncs.glGetGraphicsResetStatus != nullptr && IsBackendContextCurrentOnThisThread()) { + status = g_GLESFuncs.glGetGraphicsResetStatus(); + } + if (status == GL_NO_ERROR && debugKnob && MG_Pipe::MGPipeDebugDeviceLossDue()) status = GL_UNKNOWN_CONTEXT_RESET; + return LatchOnResetStatus(site, status); + } + + Bool LatchIfGuiltyBeforeApply() { + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false; + if (t_deviceLossLatched) return true; + // Asked every time the apply thread wakes, so it must leave nothing behind: not + // IsBackendContextCurrentOnThisThread, whose once-per-frame EGL verification stamp it would + // spend at the top of the loop (where the session may be between two of its contexts) - only + // whether this thread holds a native context and the backend's ownership. + if (g_GLESFuncs.glGetGraphicsResetStatus == nullptr || t_native.boundContext == EGL_NO_CONTEXT || + g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) + return false; + return LatchOnResetStatus("apply", g_GLESFuncs.glGetGraphicsResetStatus()); + } +#endif + + void Present() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (LatchIfDeviceLost("present", /*debugKnob=*/false)) return; +#endif + // Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush + // makes it reachable), then non-blocking-poll prior frames' fences AFTER to + // advance the completed-frame watermark that gates buffer-pool recycling. + const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync; + Uint64 presentedSerial = 0; + if (canFence) { + const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1; + presentedSerial = serial; + FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth]; + if (slot.sync && slot.contextGeneration == g_syncContextGeneration) { + // The slot's fence is a ring-depth of serials old and may be another context's: + // dropping it unsignaled would let the watermark pass work nobody waited for. + if (g_GLESFuncs.glClientWaitSync) { + constexpr Uint64 kOldFenceWaitNs = 100ull * 1000 * 1000; + (void)g_GLESFuncs.glClientWaitSync(slot.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kOldFenceWaitNs); + } + if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); + } + slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial}; + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // The session's frame boundary: the shared images its frame sampled get the frame's fence, + // so their producers' next copies wait for it. + SharedImageImpl::PublishPendingReads(); +#endif + + // P14 S4: the session's surface, not the process's. + // + // EXT -> KHR -> plain. The loader fills eglSwapBuffersWithDamageEXT from whichever + // spelling the driver provides (Adreno exposes only eglSwapBuffersWithDamageKHR), so + // the null check below is the whole chain: a driver with neither presents with plain + // eglSwapBuffers. Passing n_rects == 0 is not a partial present - the extension + // defines it as "the entire surface is implicitly damaged and the behaviour is + // equivalent to calling eglSwapBuffers" - and the wire protocol carries no damage + // rectangles for the server to forward, so the full surface is what is meant anyway. + // + // The damage is the client's, from the present record (CurrentPresentDamage): GL window + // coordinates, which is what the native call takes. Whole surface = no rectangles. + // + // ONLY A WINDOW SURFACE TAKES RECTANGLES: the platform's swap-with-damage hands them to the + // surface's native window, and a pbuffer has none (Android's dereferences it regardless). + NativeSessionState& session = ActiveNativeSession(); // before the lock: it takes it too + const EGLSurface drawSurface = session.Draw; + Bool windowSurface = false; + if (drawSurface != EGL_NO_SURFACE) { + const std::lock_guard lock(g_nativeRegistryMutex); + if (const auto* entry = FindSessionSurfaceLocked(session, drawSurface)) + windowSurface = entry->Window != static_cast(0); + } + EGLBoolean mglSwapped = EGL_FALSE; + if (g_EGLFuncs.eglSwapBuffersWithDamageEXT != nullptr) { + const MG_Util::Damage::Region& damage = MG_Backend::CurrentPresentDamage(); + EGLint rects[MG_Util::Damage::kMaxRects * 4] = {}; + const EGLint count = static_cast(MG_Util::Damage::NativeSwapRects(windowSurface, damage, rects)); + mglSwapped = g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, count > 0 ? rects : nullptr, count); + // A DRIVER THAT NAMES THE ENTRY POINT AND THEN REFUSES THE SWAP. Some vendor EGLs + // export eglSwapBuffersWithDamageEXT and answer EGL_FALSE on a window surface without + // setting an error (measured on a 4.14 vendor kernel with an Adreno driver; the plain + // entry point presents the same surface happily). Keeping the frame on that answer + // queues nothing at all: the compositor composites into a window whose buffer queue + // never sees a buffer. Fall back to the plain call instead of dropping the frame. + if (mglSwapped == EGL_FALSE && g_EGLFuncs.eglSwapBuffers != nullptr) { + MGLOG_W_ONCE("DirectGLES: this driver's eglSwapBuffersWithDamageEXT refused a window " + "surface; presenting through eglSwapBuffers for the rest of the process"); + mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); + } + } else if (g_EGLFuncs.eglSwapBuffers != nullptr) { + mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); + } + NoteDrawSurfaceMoved(); // a window surface takes a new size at its swap + if (drawSurface != EGL_NO_SURFACE) { + const std::lock_guard lock(g_nativeRegistryMutex); + if (auto* entry = FindSessionSurfaceLocked(session, drawSurface)) ++entry->Presents; + } + // A frame boundary: the next frame's age is asked anew, and its window buffer use flushed. + t_ageAskedSurface = EGL_NO_SURFACE; + t_windowBufferFlushIssued = false; + + // P15: MOBILEGL_FRAMES_IN_FLIGHT, Espryt's half. Magma bounds the frames the GPU has + // queued with its frame contexts (frame S+1 reuses the context of frame S+1-N); this is + // the same bound on the ES driver: after presenting S, wait for S+1-N to complete. The + // platform's own queueBuffer throttle already waits for S-1, so only N = 1 is stricter + // than what Android does by itself; larger N is the same contract stated, not a change. + const Uint32 framesInFlight = MG_Config::Features.FramesInFlight; + if (presentedSerial != 0 && framesInFlight != 0 && + framesInFlight < static_cast(kFrameFenceRingDepth) && g_GLESFuncs.glClientWaitSync && + presentedSerial + 1 > framesInFlight) { + const Uint64 target = presentedSerial + 1 - framesInFlight; + const FrameFence& oldest = g_frameFenceRing[target % kFrameFenceRingDepth]; + if (oldest.sync && oldest.serial == target && oldest.contextGeneration == g_syncContextGeneration) { + constexpr Uint64 kFramesInFlightWaitNs = 1000ull * 1000 * 1000; + (void)g_GLESFuncs.glClientWaitSync(oldest.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kFramesInFlightWaitNs); + } + } + + if (canFence) PollFrameFences(); + + // After the watermark advanced: retire grown-away ring stores and record the + // frame's ring high-water marks for slot reclamation. + BufferImpl::UboRingOnPresent(); + BufferImpl::UnpackRingOnPresent(); + BufferImpl::UploadRingOnPresent(); + BufferImpl::TrimBufferPool(); + + // THE frame boundary for the MGPipe counters: publish this frame's plots, fold the + // frame into the run totals and, every 120th frame, emit the summary line. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::OnPresent(); + } + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.cpp new file mode 100644 index 00000000..cf23002e --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.cpp @@ -0,0 +1,1178 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.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 "DirectGLESInternal.h" + +namespace MobileGL::MG_Backend::DirectGLES { + namespace TextureImpl { + // Identity snapshot of what one texture unit has bound: the object in every binding + // slot plus the unit's sampler object. This - not the context's texture bind + // generation - is what the per-draw texture memos key on: the generation also bumps + // on REDUNDANT re-binds (glBindSampler of the sampler the unit already carries, which + // 26.2 issues around every texture-unit switch), so a generation-keyed memo re-derives + // everything on draws that changed nothing. The snapshot compares the bindings + // themselves, so only a real change can invalidate it. What it deliberately does NOT + // cover, and its users must key on separately: + // * membership/completeness flips with no binding moving - a default texture's image + // appearing or vanishing, any texture shape or sampler parameter change - all bump + // the sampling-resolution generation (BumpShapeVersion / SamplerObject::BumpVersion + // are their only writers and bump it unconditionally); + // * everything unit bindings say nothing about: the touched-unit high-water mark, + // the frontend context identity, the backend ES context generation, and (for the + // resolution memo) the program keys that arbitrate aliased targets. + // P2: the snapshot stops holding weak_ptrs and holds the frontend objects' LIFETIME IDs. + // The weak_ptr was here for one reason - a raw address lies once the allocator recycles + // it - and a lifetime id is a monotone per-class counter that is never handed out twice, + // so it answers the same question with an integer compare and without pinning a control + // block. 0 means "nothing bound", which no live object can collide with (the counters + // start at 1). This is not the twin table's {slot, gen}: a bound texture that has never + // been synced has no twin and therefore no handle, so a handle-keyed snapshot would read + // two never-synced textures as equal. The identity has to exist before the twin does. + // + // Split by the RUNTIME arm, not by the build, for exactly the reason g_fbSlotCache is: + // MOBILEGL_PIPE_PUSH=0 has to reproduce P1's behaviour (ConfigLoader.cpp), and a + // legacy-arm run that debounced on lifetime ids would be running P2's mechanism while + // the A/B attributed the result to P1. The two answers are equivalent - OwnerEquals on + // two empty pointers is true and LifetimeIdOf(nullptr) == 0 == 0; a live-versus-expired + // control block and two distinct lifetime ids both compare unequal - so keeping the + // legacy fields costs that arm nothing but the words, and gives the control back its + // fidelity. A build with no legacy arm compiled carries neither the fields nor the + // branch. + struct UnitBindingsSnapshot { + Array slotObjects{}; + Uint64 samplerObject = 0; + }; + + static Uint64 LifetimeIdOf(const SharedPtr& object) { + return object ? object->GetLifetimeId() : 0; + } + + static Uint64 LifetimeIdOf(const SharedPtr& object) { + return object ? object->GetLifetimeId() : 0; + } + + static void CaptureUnitBindings(Int maxTouchedUnit, Vector& out) { + out.resize(static_cast(maxTouchedUnit + 1)); + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { + auto& textureUnit = MG_Pipe::gPipeInputs->GetTextureUnitObject(unit); + auto& snapshot = out[static_cast(unit)]; + const auto& slots = textureUnit.GetAllBindingSlots(); + for (SizeT i = 0; i < slots.size(); ++i) { + snapshot.slotObjects[i] = LifetimeIdOf(slots[i].GetBoundObject()); + } + snapshot.samplerObject = LifetimeIdOf(textureUnit.GetSamplerObject()); + } + } + + static Bool UnitBindingsUnchanged(Int maxTouchedUnit, const Vector& snapshots) { + if (snapshots.size() != static_cast(maxTouchedUnit + 1)) return false; + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { + auto& textureUnit = MG_Pipe::gPipeInputs->GetTextureUnitObject(unit); + const auto& snapshot = snapshots[static_cast(unit)]; + const auto& slots = textureUnit.GetAllBindingSlots(); + for (SizeT i = 0; i < slots.size(); ++i) { + if (snapshot.slotObjects[i] != LifetimeIdOf(slots[i].GetBoundObject())) return false; + } + if (snapshot.samplerObject != LifetimeIdOf(textureUnit.GetSamplerObject())) return false; + } + return true; + } + + static Vector g_observedUnitBindings; + static Uint64 g_observedUnitBindingsContextId = 0; + static Uint64 g_observedUnitBindingsGeneration = 0; + static Int g_observedUnitBindingsMaxUnit = -1; + static Uint64 g_unitBindingsEpoch = 0; + + // Epoch of the touched units' bindings: moves exactly when WHAT is bound changes, + // never on a redundant re-bind. Memos snapshot the returned value instead of holding + // their own UnitBindingsSnapshot, so the owner-compare walk runs at most once per + // draw no matter how many memos key on it. The observation triple gates the walk: + // while (context id, bind generation, high-water mark) are unchanged nothing can + // have moved, because every path that changes a binding bumps the generation. A + // given epoch value names one observed (context, high-water mark, bindings) state - + // a context switch, a high-water-mark move or a bindings change each recapture and + // bump - so epoch equality alone proves the bindings a consumer resolved against + // are the bindings on the units now. + // P4a e2 (D-G1). THE UNIT-BINDINGS EPOCH DERIVATION, REPLACED BY THE RECORDS' SERIALS. + // + // CurrentUnitBindingsEpoch below exists to answer one question - "did WHAT the touched + // units hold change?" - without re-deriving on a redundant re-bind, and it pays for the + // answer with a per-unit snapshot walk over every binding slot plus the unit's sampler + // object, run whenever the frontend's texture bind generation moves (26.2 moves it + // around every texture-unit switch). The applier already holds the answer: + // set_sampler_views and bind_sampler_states go out through a ContentHash suppressor, + // so their serials move IF AND ONLY IF the resolved per-unit view set or the per-unit + // sampler set really moved. Two Uint64 reads replace the walk, and the suppressor is + // what makes that sound - the hash is over every entry of each tail, so suppression + // cannot hide a change. + // + // BOTH serials, because the snapshot covered both halves: slotObjects is the view set + // and samplerObject is the sampler-state set. + // + // DECLINES until both windows have arrived at least once, and the COUNT is what says + // so rather than the serial: MGPipeApplierReset advances every P4a working-state + // serial whether or not anything was ever emitted, so a serial is never "no set has + // been received". A declined answer leaves the caller on the snapshot walk, which is + // the honest thing for a tree whose client half has not landed. + // + // The two arms' epoch values share one number space during the transition (a process + // can run the walk before the first emission and the serials after it). That is safe + // by construction: consumers only ever ask "is this the same value I stamped", the + // walk's counter is small and dense and this one is a 64-bit mix, so the two cannot + // meet except with vanishing probability - and a collision costs a spare rebuild. + static Bool UnitBindingsEpochFromRecords(Uint64& out, Int maxTouchedUnit) { + const auto& st = MG_Pipe::MGPipeApplier(); + // P4a decline-site T2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, then + // the decline, and MEASURED before it was believed. Bits 10 and 11 are on (the + // caller asked SamplerSubsystemEnabled(), which refuses one without the other) + // and package C has landed, so a draw that reaches here with NEITHER window ever + // received looks like a seam defect - but the first real-path run said otherwise + // for one whole class of draw and the condition is narrowed by that measurement, + // not by argument. + // + // maxTouchedUnit < 0 IS "THIS DRAW TOUCHES NO TEXTURE UNIT AT ALL", and for such + // a draw an empty sampler-view and sampler-state window is the correct and only + // possible emission: there is nothing to describe. 161 of the integration lane's + // 477 processes are that shape, every one of them at units 0..-1, and a line that + // fires on a third of a green lane is a line nobody reads. So the refusal is + // scoped to a draw that actually touches a unit, where an empty window really + // does mean no set_sampler_views and no bind_sampler_states ever arrived. + // + // It declines to the snapshot walk rather than refusing the draw, because the + // walk answers the same question correctly and a wrong picture is not the failure + // mode here - a silent permanent fallback is, and that is what the line is for. + // The caller's MINOR-4 gate tick still counts EVERY decline, loud or not. + if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) { + // P5e (tx2), CONTRACT-P5E §5.3: UNDER A LIVE WIRE THE DECLINE IS A REFUSAL. + // + // The fall-back below is the pre-handle snapshot walk over GetTextureUnitObject, + // and on a run-ahead server that walk reads client memory for a record that has + // already been answered - there is no wait left in which the answer could be + // right. Ruling 19 makes the missing window unrepresentable at the sink; this is + // the same statement one level down, for the backend that would have papered over + // it. Under monolith the fall-back is correct and stays. + if (maxTouchedUnit >= 0) { + // The windows are the PEER's records, so in a served session this ends THAT + // session (the in-process display server serves every client from one + // process; an abort here took the compositor down with the offender). The + // answer while the latch drains is "no unit is bound": the walk the decline + // would run reads client memory, and the session stops at its next record. + // Unarmed, SessionLatch is the old abort (P13 W5: through the record latch seam, + // which is SessionLatch wherever MG_Remote is linked). + if (!MG_Pipe::MGPipeRecordLatch( + MG_Pipe::MGFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - a draw " + "touches units 0..%d and neither a sampler-view nor a sampler-state " + "window has ever been applied while the sampler subsystem bit is set; " + "the pre-handle unit-bindings snapshot walk reads client memory and is " + "refused under an active transport (CONTRACT-P5E.md §5.3)", + static_cast(maxTouchedUnit))) { + out = 0; + return true; + } + } + if (maxTouchedUnit >= 0) { + MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw " + "touches units 0..%d and neither a sampler-view nor a sampler-state " + "window has ever been applied while the sampler subsystem bit is " + "set; running the pre-handle unit-bindings snapshot walk.", + static_cast(maxTouchedUnit)); + } + return false; + } + // Local, because the tracker's MGPipeMixShutter lives in MG_Impl and no backend + // translation unit may reach for it. + const Uint64 mixed = + (st.SamplerViewsSerial * 0x9E3779B97F4A7C15ull) ^ (st.SamplerStatesSerial + 0x165667B19E3779F9ull); + out = mixed; + return true; + } + + static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) { + // P4a decline-site T1: M - the mask says this family is not switched on, and it + // IS D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification + // round: the mask's word is the one thing a decline may be quiet about. + Uint64 epochFromRecords = 0; + if (UnitBindingsEpochFromRecords(epochFromRecords, maxTouchedUnit)) { + // No accessor reads and no walk on this arm; the two PipeStats accessor + // ticks below are not counted because no accessor was called. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, + /*hit=*/true); + } + return epochFromRecords; + } + // MINOR-4. THE RECORD ARM'S DECLINE IS TICKED AS A MISS, and it has to be: + // the walk arm below ticks the SAME gate with its own hit/miss, so without + // this a fully declining tree reports the walk memo's high hit rate on + // Gate::EsprytUnitBindingsEpoch and the verification round's "hit rate goes to + // 100%" cannot tell "the record arm engaged" from "the walk arm's memo hit". + // With it, every declining call contributes at least one miss, so 100% means + // exactly one thing. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, + /*hit=*/false); + } + const Uint64 contextId = MG_Pipe::gPipeInputs->GetTextureContextId(); + const Uint64 bindGeneration = MG_Pipe::gPipeInputs->GetTextureBindGeneration(); + if (MG_Util::PipeStats::Enabled()) { + // Two accessor calls whichever way the shutter goes; only the unit WALK is + // gated, and that walk reads no GLContext accessor of its own. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2); + } + if (g_observedUnitBindingsContextId == contextId && g_observedUnitBindingsMaxUnit == maxTouchedUnit && + g_observedUnitBindingsGeneration == bindGeneration) { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/true); + } + return g_unitBindingsEpoch; + } + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/false); + } + if (g_observedUnitBindingsContextId != contextId || g_observedUnitBindingsMaxUnit != maxTouchedUnit || + !UnitBindingsUnchanged(maxTouchedUnit, g_observedUnitBindings)) { + CaptureUnitBindings(maxTouchedUnit, g_observedUnitBindings); + ++g_unitBindingsEpoch; + } + g_observedUnitBindingsContextId = contextId; + g_observedUnitBindingsMaxUnit = maxTouchedUnit; + g_observedUnitBindingsGeneration = bindGeneration; + return g_unitBindingsEpoch; + } + + // Work list behind SyncNeccessaryTextures' per-draw unit walk. WHICH textures the + // touched units hold is a pure function of the unit bindings, so the GLContext identity + // (a never-reused id, not the heap address a recreated context can land on again), the + // unit-bindings snapshot and the touched-unit high-water mark are a complete key - + // WHAT each entry then has to do is still decided per draw by the version compares + // inside the sync calls, which is why texture content, shape and parameter changes need + // no key here. + // + // Entries borrow, they never own. `slot` points at the binding slot's shared_ptr, whose + // address is fixed for the context's lifetime (TextureState holds the unit array by + // value) and whose VALUE cannot change without bumping the bind generation. `backend` is + // the registry's object for the texture in that slot; the registry only erases an entry + // once the frontend texture has expired, and a frontend texture cannot expire while a + // slot the key covers still holds a reference to it. Holding either side by shared_ptr + // instead would keep dead frontend textures alive and defeat the registry's + // weak-reference GC. + // + // `texture` records WHICH frontend object `backend` was paired with when the entry was + // built, and PairingsIntact re-checks it before any replay. The keys above are the + // primary guard, but they are all derived state: a slot swap that never reaches the + // bind generation (the DSA by-name emulation used to swap a slot silently) would leave + // every key matching while the borrowed slot pointed at a different texture, and the + // replay would then drive texture A's backend twin from texture B's frontend state - + // re-specifying A's backend storage with B's shape and destroying A's contents. A raw + // pointer compare per entry is far cheaper than the walk it guards, and a stale pairing + // costs only a list rebuild, so this stays as the structural net under the keys. + struct UnitTextureSyncEntry { + const SharedPtr* slot = nullptr; + MG_State::GLState::ITextureObject* texture = nullptr; + BackendTextureObject* backend = nullptr; + }; + // P5e (tx2), CONTRACT-P5E §5.2: THE UNIT WORK LIST WITH NO BORROWED SLOT IN IT. + // + // The pre-P5e entry borrows a pointer INTO a frontend binding slot and remembers which + // frontend object it was paired with, and PairingsIntact re-checks that pairing before + // every replay. Both exist because the list's key is derived state and a silent slot swap + // could leave a stale pairing driving texture A's twin from texture B's frontend state. + // + // A HANDLE HAS NO SUCH HAZARD. `Res` is {slot, gen}: a recycled slot arrives at a higher + // Gen, which the twin table's forward-Gen rule answers by RESETTING the twin, and a + // backward Gen is refused outright - so there is nothing for a pairing check to catch and + // PairingsIntact is not carried on this list at all. The key is the applier's own + // (ContextSerial, SamplerViewsSerial, window) plus the backend context generation, and + // SamplerViewsSerial moves IFF the resolved per-unit view set moved, which the emitter's + // content-hash suppressor is what makes sound. + // + // Its OWN list and its own key rather than a re-shape of UnitTextureSyncEntry, because + // that struct is also the draw-FBO and read-FBO attachment lists' (fb's), and one name + // carrying two shapes across two packages' worktrees is the merge trap the contract + // package exists to avoid. + struct UnitTextureSyncByHandleEntry { + MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; + BackendTextureObject* backend = nullptr; + }; + static Vector g_unitTextureSyncListByHandle; + static Bool g_unitTextureSyncListByHandleValid = false; + static Uint64 g_unitTextureSyncListByHandleContextSerial = 0; + static Uint64 g_unitTextureSyncListByHandleViewsSerial = 0; + static Uint32 g_unitTextureSyncListByHandleStart = 0; + static Uint32 g_unitTextureSyncListByHandleCount = 0; + static Uint g_unitTextureSyncListByHandleContextGeneration = 0; + static Vector g_unitTextureSyncList; + static Bool g_unitTextureSyncListValid = false; + static Uint64 g_unitTextureSyncListContextId = 0; + static Int g_unitTextureSyncListMaxUnit = -1; + static Uint g_unitTextureSyncListContextGeneration = 0; + static Uint64 g_unitTextureSyncListEpoch = 0; + static Uint64 g_unitTextureSyncListSamplingGeneration = 0; + + // P5e (fb, CONTRACT-P5E.md §5.4): THE TWO ATTACHMENT LISTS GET AN ENTRY OF THEIR OWN, + // and it borrows nothing. + // + // The unit list's entry borrows the binding slot's SharedPtr and re-checks the pairing + // before every replay, because its key (the unit-bindings epoch) is derived state that + // a silent slot swap could get past. An attachment list under the record arm has + // neither problem and cannot afford the borrow: the key IS the record - {Fbo, + // ContentHash} - and every attachment edit moves the hash by construction, so the + // pairing has nothing left to catch; and the borrowed pointer was into a frontend + // FramebufferObject, which is exactly the thing this package stops holding. So an entry + // is the texture HANDLE the record named plus the twin it resolved to, PairingsIntact + // becomes `entry.Res == surface.Res` folded into the rebuild, and the list owns no + // reference to anything the client can free. + // + // The twin pointer is still a BORROW, on the unit list's own terms: the registry keeps + // a twin alive until its object dies, an attached texture cannot die while the record + // that names it stands, and the very next thing done with the entry is a by-handle sync + // that would re-resolve it anyway. + struct FboAttachmentSyncEntry { + MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; + BackendTextureObject* backend = nullptr; + }; + + // Sibling memo for the draw FBO's texture attachments (see the use site in + // SyncNeccessaryTextures for the key derivation and the borrow rules, which are the + // unit list's). A null FBO pointer means "not stamped". + static Vector g_fboTextureSyncList; + static MG_State::GLState::FramebufferObject* g_fboTextureSyncListFbo = nullptr; + static Uint16 g_fboTextureSyncListSlotVersion = 0; + static Uint16 g_fboTextureSyncListObjectVersion = 0; + static Uint64 g_fboTextureSyncListContextId = 0; + static Uint g_fboTextureSyncListContextGeneration = 0; + // P4a e2. The record-keyed half of the same memo. The draw framebuffer record's + // ContentHash replaces the two VERSION halves of the key - the hash is taken over + // every field the record carries, so an attachment edit always moves it, and Fbo is + // inside it, so a recycled framebuffer handle cannot be suppressed against its + // predecessor. THE IDENTITY HALF IS STILL A POINTER COMPARE and deliberately stays + // one: this list borrows the attachment's own SharedPtr slots, so what it needs to + // know is that the frontend object it borrowed from is the one bound now, and that + // question has no handle in it until package D re-keys the twin tables. Held in its + // own fields with its own flag rather than reusing the versions', because a process + // runs the frontend key before the client's first emission and the record key after + // it, and one field carrying two key shapes is how a stale half gets compared against + // a live one. + static Uint64 g_fboTextureSyncListContentHash = 0; + static Bool g_fboTextureSyncListRecordKeyed = false; + // P5e (fb): the record-keyed list itself, held apart from the pre-handle one for the + // reason the two key shapes are held apart - one field carrying two shapes is how a + // stale half gets compared against a live one. Its key is + // {Fbo, ContentHash, ContextSerial, g_backendContextGeneration}: the applier's + // ContextSerial replaces GetTextureContextId (a frontend read the record arm may not + // take), and the two version halves are gone because the hash covers them. + static Vector g_fboAttachmentSyncList; + static MG_Pipe::MGPipeHandle g_fboAttachmentSyncListFbo = MG_Pipe::kMGPipeNullHandle; + static Uint64 g_fboAttachmentSyncListContentHash = 0; + static Uint64 g_fboAttachmentSyncListContextSerial = 0; + static Uint g_fboAttachmentSyncListContextGeneration = 0; + static Bool g_fboAttachmentSyncListValid = false; + + // The read framebuffer's, same shape and same reasons. Its own list because it is + // keyed on a different framebuffer and deduped against the draw one. + static Vector g_readFboAttachmentSyncList; + static MG_Pipe::MGPipeHandle g_readFboAttachmentSyncListFbo = MG_Pipe::kMGPipeNullHandle; + static Uint64 g_readFboAttachmentSyncListContentHash = 0; + static Uint64 g_readFboAttachmentSyncListContextSerial = 0; + static Uint g_readFboAttachmentSyncListContextGeneration = 0; + static Bool g_readFboAttachmentSyncListValid = false; + + // Rebuild-or-replay, shared by both lists. The record's texture surfaces ARE the + // membership (§5.4); a renderbuffer-only framebuffer - the common Minecraft frame - + // reduces to the key compare and an empty loop. + // + // ONE CALL PER ENTRY, and it is tx2's by-handle seam rather than the three the + // pre-handle arm makes: SyncTextureToBackendByHandle is the by-handle twin of + // SyncTextureObjectToBackend, which is what the REBUILD has always called here, so the + // params / builtin-sampler / mipmap work and its own clean gate are inside it. Calling + // the three separately would need by-handle forms of the first two that nothing + // declares, and would put this file's copy of the clean gate beside tx2's. + static void SyncRecordAttachmentTextures(const MG_Pipe::MGPFramebufferState& record, + Vector& list, Bool listValid) { + if (listValid) { + for (const auto& entry : list) { + // P14: the unit list's aggregate clean gate first (SyncNeccessaryTextures): an + // attachment whose storage, parameters and sampler are already the driver's + // costs two record reads instead of three nested syncs and their SharedPtr + // traffic. Same borrow rule as there - the list's key proves the twin alive. + const auto* record = PipeTextureRecordForHandle(entry.Res); + if (record != nullptr && entry.backend != nullptr && + entry.backend->IsDrawSyncCleanByRecord(entry.Res, *record)) + continue; + TextureImpl::SyncTextureToBackendByHandle(entry.Res, /*imageBindableStorageRequired=*/false); + } + return; + } + list.clear(); + const auto add = [&](const MG_Pipe::MGPSurface& surface) { + if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; + if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return; + auto& twin = TextureImpl::SyncTextureToBackendByHandle(surface.Res, + /*imageBindableStorageRequired=*/false); + list.push_back({surface.Res, twin.get()}); + }; + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) add(record.Color[i]); + add(record.Depth); + add(record.Stencil); + } + + DrawTextureSyncKeys CaptureDrawTextureSyncKeys() { + DrawTextureSyncKeys keys; + keys.contextId = MG_Pipe::gPipeInputs->GetTextureContextId(); + // Units past the frontend's high-water mark have provably-empty slots. + // + // P4a e2 DELIBERATELY DOES NOT take this off the sampler-view window's Count, and + // the reason is asymmetric risk. D-G2 makes Count the client's spelling of exactly + // this number, so the substitution looks free - but the two failure directions are + // not alike: a window that is larger than the frontend's mark costs a walk over + // provably-empty units, while one that is SMALLER silently drops the sync of a + // texture a draw is about to sample, which no gate on this tree can see because + // this tree emits no windows at all. The design assigns the high-water mark to the + // CLIENT as the count argument and says nothing about re-deriving it server-side. + // It is one accessor read per draw; the integrator can move it in one line once a + // tree exists where the two can be compared. + keys.maxTouchedUnit = MG_Pipe::gPipeInputs->GetMaxTouchedTextureUnit(); + keys.samplingGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration(); + keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit); + if (MG_Util::PipeStats::Enabled()) { + // The three reads above; CurrentUnitBindingsEpoch counts its own two when it + // takes the walk arm and none when the records answered. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); + } + return keys; + } + + // P4a e2 (D-E3). The READ framebuffer's texture attachments, and why this exists. + // + // A texture that is an attachment of the READ framebuffer ONLY reaches + // SyncAttachmentObject, which calls SyncMipmapsToBackend AND NOTHING ELSE - so its + // swizzle, its GL_DEPTH_STENCIL_TEXTURE_MODE, its LOD clamp and its border colour + // never reach the driver. The list that pushes texture PARAMETERS for an attachment is + // the one below, and it reads the DRAW binding slot alone. That is the gap + // ARCHITECTURE.md's D10 names, and the roadmap makes closing it a mandatory, + // red-before deliverable of this phase: set_texture_params is addressed by RESOURCE + // and is independent of any binding, so a texture's parameters exist the moment they + // move and an attachment - draw or read - gets them. + // + // DELIBERATELY NOT GATED ON THE FRAMEBUFFER SUBSYSTEM BIT. It is a deliverable of the + // phase rather than an arm of it, so both arms of the object A/B carry it and the A/B + // keeps measuring the arm rather than the arm plus a behaviour change. It is inside + // MOBILEGL_PIPE_PUSH, so the pull build is untouched. + // + // Its own list and its own key rather than an extension of the draw list's, because + // the draw list is keyed on the draw binding and its pull-build copy must stay + // textually what it was. + static Vector g_readFboTextureSyncList; + static MG_State::GLState::FramebufferObject* g_readFboTextureSyncListFbo = nullptr; + static Uint16 g_readFboTextureSyncListSlotVersion = 0; + static Uint16 g_readFboTextureSyncListObjectVersion = 0; + static Uint64 g_readFboTextureSyncListContextId = 0; + static Uint g_readFboTextureSyncListContextGeneration = 0; + static Uint64 g_readFboTextureSyncListContentHash = 0; + static Bool g_readFboTextureSyncListRecordKeyed = false; + + // P5e (fb, CONTRACT-P5E.md §5.4): THE RECORD ARM OF BOTH ATTACHMENT LISTS, and it is + // the package's core - this is the one unconditional per-draw live read the framebuffer + // family had, taken on every draw whether the memo hit or missed. + // + // Three frontend reads go in one move: the binding slot (twice), the FramebufferObject + // the entries borrowed their attachment slots from, and GetTextureContextId. The key is + // {Fbo, ContentHash, ContextSerial, g_backendContextGeneration} - the record's own hash + // in place of the slot/object version pair (it covers every surface, and Fbo is inside + // it, so a recycled framebuffer cannot be suppressed against its predecessor), the + // APPLIER's ContextSerial in place of the frontend context id, and the backend context + // generation unchanged because it answers a question about driver names that no + // client-side value can answer. + // + // The two lists are still two, and the dedupe is still "one object on both bindings" - + // asked of the two BOUND HANDLES rather than of two pointers, which is the same + // question with a spelling a recycled slot cannot fool. + static void SyncFramebufferAttachmentTexturesByRecord() { + const auto& st = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle drawHandle = + st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)]; + const MG_Pipe::MGPipeHandle readHandle = + st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)]; + const MG_Pipe::MGPFramebufferState* const drawRecord = + BoundFramebufferRecord(FramebufferTarget::Draw); + const MG_Pipe::MGPFramebufferState* const readRecord = + BoundFramebufferRecord(FramebufferTarget::Read); + const Uint64 contextSerial = st.ContextSerial; + + if (drawRecord == nullptr) { + // Not a decline this function can refuse on: SyncCurrentFBO runs before every + // caller of this one and is where the missing record is named and refused + // (RefuseFramebufferBindingSlotRead). Here it only means "nothing to walk". + g_fboAttachmentSyncListValid = false; + g_fboAttachmentSyncList.clear(); + } else { + const Bool listValid = g_fboAttachmentSyncListValid && + g_fboAttachmentSyncListFbo == drawRecord->Fbo && + g_fboAttachmentSyncListContentHash == drawRecord->ContentHash && + g_fboAttachmentSyncListContextSerial == contextSerial && + g_fboAttachmentSyncListContextGeneration == g_backendContextGeneration; + SyncRecordAttachmentTextures(*drawRecord, g_fboAttachmentSyncList, listValid); + g_fboAttachmentSyncListFbo = drawRecord->Fbo; + g_fboAttachmentSyncListContentHash = drawRecord->ContentHash; + g_fboAttachmentSyncListContextSerial = contextSerial; + g_fboAttachmentSyncListContextGeneration = g_backendContextGeneration; + g_fboAttachmentSyncListValid = true; + } + + if (readRecord == nullptr || readHandle == drawHandle) { + g_readFboAttachmentSyncListValid = false; + g_readFboAttachmentSyncList.clear(); + return; + } + const Bool readListValid = g_readFboAttachmentSyncListValid && + g_readFboAttachmentSyncListFbo == readRecord->Fbo && + g_readFboAttachmentSyncListContentHash == readRecord->ContentHash && + g_readFboAttachmentSyncListContextSerial == contextSerial && + g_readFboAttachmentSyncListContextGeneration == g_backendContextGeneration; + SyncRecordAttachmentTextures(*readRecord, g_readFboAttachmentSyncList, readListValid); + g_readFboAttachmentSyncListFbo = readRecord->Fbo; + g_readFboAttachmentSyncListContentHash = readRecord->ContentHash; + g_readFboAttachmentSyncListContextSerial = contextSerial; + g_readFboAttachmentSyncListContextGeneration = g_backendContextGeneration; + g_readFboAttachmentSyncListValid = true; + } + + void SyncNeccessaryTextures(const DrawTextureSyncKeys& keys) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + + // All textures we need are: + // 1. textures bound to texture units (TODO: only sync ones that are used in current program) + // 2. textures used in current FBO + // 3. textures bound to image units (TODO) + + const Int maxTouchedUnit = keys.maxTouchedUnit; + const Uint64 samplingGeneration = keys.samplingGeneration; + const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch; + // P5e (tx2), CONTRACT-P5E §5.2. THE UNIT HALF, BY HANDLE. + // + // set_sampler_views is the RESOLVED answer to the question this walk asks - which + // texture does each touched unit sample - and the client has already applied every + // drop the walk below performs by hand: an image-less default texture and a texture + // that samples as incomplete are both null in the record (SamplerEmit.h's two drops), + // and the alias contest between two real textures on one native target was arbitrated + // by the program's own sampler types. So the window IS the work list. + // + // NAMED BEHAVIOUR DELTA (P5e-5, contract §5.2): this syncs the PROGRAM-RESOLVED + // texture per unit, where the frontend walk syncs every slot of every touched unit. + // Coverage is unchanged - views UNION image units UNION the FBO attachment lists + // UNION the waited texture ops still cover every texture anything reads - so it + // narrows WORK and not what reaches the driver. + const auto& st = MG_Pipe::MGPipeApplier(); + const Bool listValid = g_unitTextureSyncListByHandleValid && + g_unitTextureSyncListByHandleContextSerial == st.ContextSerial && + g_unitTextureSyncListByHandleViewsSerial == st.SamplerViewsSerial && + g_unitTextureSyncListByHandleStart == st.SamplerViewStart && + g_unitTextureSyncListByHandleCount == st.SamplerViewCount && + g_unitTextureSyncListByHandleContextGeneration == g_backendContextGeneration; + if (listValid) { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true); + } + for (const auto& entry : g_unitTextureSyncListByHandle) { + const auto* record = PipeTextureRecordForHandle(entry.Res); + if (record == nullptr) continue; + // The aggregate gate, from the twin's own serials against the record's - + // no frontend version, no context id, no sampling generation. + if (entry.backend->IsDrawSyncCleanByRecord(entry.Res, *record)) continue; + SyncTextureToBackendByHandle(entry.Res); + } + } else { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false); + } + g_unitTextureSyncListByHandleValid = false; + g_unitTextureSyncListByHandle.clear(); + const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount; + for (Uint32 index = st.SamplerViewStart; + index < windowEnd && index < st.BoundSamplerViews.size(); ++index) { + const auto& view = st.BoundSamplerViews[index]; + if (MG_Pipe::MGPipeHandleIsNull(view.Texture)) continue; + auto& twin = SyncTextureToBackendByHandle(view.Texture); + if (!twin) continue; + g_unitTextureSyncListByHandle.push_back({view.Texture, twin.get()}); + } + g_unitTextureSyncListByHandleContextSerial = st.ContextSerial; + g_unitTextureSyncListByHandleViewsSerial = st.SamplerViewsSerial; + g_unitTextureSyncListByHandleStart = st.SamplerViewStart; + g_unitTextureSyncListByHandleCount = st.SamplerViewCount; + g_unitTextureSyncListByHandleContextGeneration = g_backendContextGeneration; + g_unitTextureSyncListByHandleValid = true; + } + // The pre-handle list must not be replayed after this arm ran: its entries borrow + // frontend slots this arm never refreshed. + g_unitTextureSyncListValid = false; + + // Texture attachments of the draw FBO, memoised like the unit list above: WHICH + // textures hang off the FBO only changes with a rebind (slot version), an + // attachment/draw-buffer edit (object version - the documented invariant + // SyncCurrentFBO's memo already leans on), a different FBO landing on a recycled + // heap address (pointer + slot version together, the StampSyncedFBO trio), another + // frontend context (context id) or a rebuilt ES context (backend generation). WHAT + // each texture then needs is still decided per draw by the version gates inside the + // three sync calls. Entry lifetime mirrors the unit list: the attachment holds the + // texture's SharedPtr at a stable address while the FBO is unchanged, and the + // registry keeps a backend object alive until its frontend texture expires, which an + // attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame - + // reduces to the key compare and an empty loop. + // P5e (fb): under a transport with the framebuffer bit set, BOTH attachment lists + // are answered from the two records and neither binding slot is touched. The + // pre-handle pair below stays compiled and stays the monolith path (ruling 1). + SyncFramebufferAttachmentTexturesByRecord(); + return; + } + + void SyncNeccessaryTextures() { SyncNeccessaryTextures(CaptureDrawTextureSyncKeys()); } + + // Whether glBindImageTexture's `layered` means anything for this target - asked of the + // target the DRIVER will see, not the one the application named. A GL_TEXTURE_1D_ARRAY + // is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking + // the state target instead answered "no" for it and pinned every 1D-array image binding + // to layer 0, whatever the application passed. + // + // `layer` travels with the answer, because GL 4.6 core 8.26 (and ES 3.2 8.22, word for + // word) makes them one rule: "If the texture identified by texture does not have + // multiple layers or faces, the entire texture level is bound, regardless of the values + // of layered and layer." REGARDLESS means ignored - not clamped, and not an error - so + // the driver must not be handed a layer index the texture has no room for. Adreno takes + // such a request literally and leaves the image unit reading zero, which is what failed + // KHR-GL42.bind_image_texture.single_layer's layer:1 rows on GL_TEXTURE_2D and on the + // GL_TEXTURE_1D that is stored as one. Normalizing here and not in the frontend shadow + // is deliberate: GL_IMAGE_BINDING_LAYER must keep echoing what the application passed. + static Bool SupportsLayeredImageBinding(TextureTarget target) { + const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target); + return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap || + backendTarget == TextureTarget::Texture2DArray || + backendTarget == TextureTarget::TextureCubeMapArray || + backendTarget == TextureTarget::Texture2DMultisampleArray; + } + + // Which image units currently hold a WRITABLE buffer texture, and how many. Kept here + // rather than recomputed per draw because the frontend tracks 192 image units and + // almost every program uses none of them: the draw path pays one integer test. + // + // Maintained by SyncImageTextureBinding, which is the single funnel for an image-unit + // change on this backend - glBindImageTextures is a frontend loop over + // glBindImageTexture, and the whole-sweep SyncImageTextureBindings goes through it too. + // Anything that clears a binding WITHOUT coming through here (a deleted texture, a + // recreated context) can only leave a bit set for a unit that no longer has one; the + // sweep re-reads the binding, finds nothing to mark, and CLEARS the bit on its way past. + // So the error is self-healing, and in the direction that costs one wasted look rather + // than one missed write. + static Array + g_writableImageBufferUnits{}; + static Uint g_writableImageBufferUnitCount = 0; + + static void TrackWritableImageBufferUnit(Uint unit, Bool writableBufferTexture) { + Bool& tracked = g_writableImageBufferUnits[unit]; + if (tracked == writableBufferTexture) return; + tracked = writableBufferTexture; + // Written as two guarded steps rather than one signed add: the count is unsigned, + // and a decrement that ever ran one time too many would not saturate at zero, it + // would wrap to four billion and defeat the early-out for the rest of the process. + if (writableBufferTexture) { + ++g_writableImageBufferUnitCount; + } else if (g_writableImageBufferUnitCount > 0) { + --g_writableImageBufferUnitCount; + } + } + + // Highest image unit that has ever been given a texture, plus one. Maintained by the + // single funnel below, so it is a sound "no draw in this context can be reading an image" + // test: nothing reaches an image unit without going through SyncImageTextureBinding. + // Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the + // draw-path staleness check below at one integer test. + Uint g_imageUnitHighWaterMark = 0; + // ---- P5e (fb, CONTRACT-P5E.md §5.4 / ruling 16 / ID-94): the image unit's bind ------ + // + // P4a e3's ResolveShaderImageRecord IS GONE, identity test, I5 seam log and all. It + // existed because there were TWO answers for one unit - the record and the frontend + // binding - and it had to decide which of them the driver was driven from; its identity + // test (`view.Res != HandleOf(boundTexture)`) was itself a client-allocator probe on the + // apply thread. Under a transport there is no second answer left: the record IS the + // unit, so the arm below reads it and nothing corroborates it. + // + // AND ACCESS IS DECODED. The comment this replaced said MGPImageView::Access carried + // "an ENCODING whose definition belongs to the client emitter and does not exist at the + // contract commit". That was STALE at the time and is what ruling 16 / ID-94 corrects: + // MGPipeEncodeImageAccess has folded the three GL names into 0/1/2 since P4a, and c0e + // moved the numbers into MG_Pipe/MGPipeValueTypes.h so one table serves both roles. + // Reading the frontend's GLenum "because it is the same value by construction" was the + // last field of this record the server declined to believe. + + // The bind itself, factored out of both arms, because the format rules below are the + // part that is easy to get subtly different in two copies - and a divergence there is + // an out-of-bounds image read on nineteen of twenty-six formats, not a wrong pixel. + // Everything it takes is a VALUE: the arm above it decides where each one came from. + // + // COMPILED IN THE PULL BUILD TOO, and that is a deliberate exception to the D-P + // discipline the renderbuffer macros keep (each arm's expression at its original site). + // A macro cannot carry a hundred lines of format reasoning, and the alternative was the + // same hundred lines twice; the function is `static` with ONE caller in the pull build, + // so it has no symbol of its own there and the code it generates is the code that was + // written inline before. Named here rather than discovered by a G1 diff. + // NOLINTNEXTLINE(readability-function-cognitive-complexity) + static void IssueImageTextureBind(Uint unit, BackendTextureObject& backendTexture, + TextureTarget textureTarget, TextureInternalFormat textureFormat, + GLenum appFormat, GLint level, Bool layeredRequested, + GLint layerRequested, GLenum access) { + const Bool layerable = SupportsLayeredImageBinding(textureTarget); + const GLboolean layered = layerable ? static_cast(layeredRequested) : GL_FALSE; + const GLint layer = layerable ? layerRequested : 0; + // The bind half of the image-format widening. The storage sync has just allocated + // this texture's storage in the core carrier of its format (the call above is the + // one that marks it image-bindable), and glBindImageTexture's `format` has to name + // the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on Adreno + // for nineteen of the twenty-six non-core formats and on both Malis for twenty-five, + // and every driver that DOES accept a narrow texture through a wide image accepts it + // silently, reading and writing out of bounds. The frontend's own ImageTextureBinding + // keeps the application's format untouched, so GL_IMAGE_BINDING_FORMAT still answers + // what was passed in. + // + // Widened from the format the APPLICATION named rather than from the texture's own, + // because GL lets the two differ inside one format class and the shader was widened + // from the class the application named too (an r32ui view of an r32f image is a legal + // reinterpretation). The two carriers always have the same texel size - every format + // in a class widens to the four-channel form of that same class - so the storage + // still describes what the bind claims. Gated on the TEXTURE having been widened, so + // a bind format that names a class the storage does not have is left alone: GL + // already calls that undefined, and inventing a carrier for it would only make the + // out-of-class read wider. + // + // A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of + // its own to widen (its texels are the application's buffer object), so + // WidenImageFormatsPass declines to widen every buffer image and the bind must decline + // with it, or the driver would be handed a carrier the shader never addressed. See the + // Dim::Buffer guard there for the 32-byte GL_RG32F measurement that pinned it. + // + // What a buffer image takes instead is the SPLIT, which is the same three-layer move + // through a different door: a private glTexBuffer view names the single-channel base + // format, the bind below names it too, and the shader subscripts it two components per + // original texel. Same gate on all three, so they cannot disagree. + // + // The split view is a SEPARATE texture name over the same buffer, and the bind has to + // name it rather than the application's own: the application's texture keeps the + // format it asked for so that a samplerBuffer reading the same buffer texture - which + // is NOT subscript-rewritten - still sees whole texels. See + // BackendTextureObject::m_bufferImageSplitViewId. + GLenum bindFormat = appFormat; + GLuint bindTextureId = backendTexture.GetBackendTextureId(); + if (textureTarget == TextureTarget::TextureBuffer) { + if (TextureImpl::GetImageBindableBufferSplitFormat(textureFormat) != GL_UNKNOWN_MGL) { + if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat( + MG_Util::ConvertGLEnumToTextureInternalFormat(appFormat)); + boundFormatSplit != GL_UNKNOWN_MGL) { + bindFormat = boundFormatSplit; + if (const Uint splitViewId = backendTexture.GetBufferImageSplitViewId(); + splitViewId != 0) { + bindTextureId = splitViewId; + } + } + } + } else if (TextureImpl::GetImageBindableStorageWidening(textureFormat)) { + const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening( + MG_Util::ConvertGLEnumToTextureInternalFormat(appFormat)); + if (boundFormatWidening) { + bindFormat = boundFormatWidening.InternalFormat; + } + } + g_GLESFuncs.glBindImageTexture(unit, bindTextureId, level, layered, layer, access, bindFormat); + } + + // MGPImageView::Access -> the GL token glBindImageTexture takes. The three numbers are + // MGPipeValueTypes.h's, so this is a spelling change and not a second table; anything + // else on the wire is Fatal{ProtocolCorruption, "ImageView.Access"} AT THE READER, + // which is what the value header's comment asks for (it may not log, so it answers the + // question and the caller owns the refusal). + static GLenum GLAccessForImageView(const MG_Pipe::MGPImageView& view) { + if (!MGPipeImageAccessIsValid(view.Access)) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ImageView.Access\"} An image record " + "carries Access=%u at unit %u, and only 0/1/2 are defined.", + static_cast(view.Access), static_cast(view.Unit)); + std::abort(); + } + switch (MGPipeDecodeImageAccess(view.Access)) { + case MGPipeImageAccess::WriteOnly: return GL_WRITE_ONLY; + case MGPipeImageAccess::ReadWrite: return GL_READ_WRITE; + case MGPipeImageAccess::ReadOnly: + case MGPipeImageAccess::Count: break; + } + return GL_READ_ONLY; + } + + // A writable buffer image, said by the record: the access byte and the resource + // descriptor's storage kind, neither of which is a frontend read. Same question + // IsWritableImageBufferTexture asks of the frontend binding. + static Bool IsWritableImageBufferView(const MG_Pipe::MGPImageView& view) { + if (MG_Pipe::MGPipeHandleIsNull(view.Res)) return false; + if (!MGPipeImageAccessIsValid(view.Access)) return false; + if (!MGPipeImageAccessWrites(MGPipeDecodeImageAccess(view.Access))) return false; + const auto* record = PipeTextureRecordForHandle(view.Res); + return record != nullptr && + static_cast(record->Desc.StorageKind) == TextureStorageType::Buffer; + } + + // GL 4.6 core 8.26: an access through an image unit whose texture does not HAVE the + // (level, layer) the unit names is INVALID - a load returns zero, a store does nothing, + // and neither is an error. The ES storage behind such a unit may well have that level + // (a mutable texture's storage is allocated for its whole chain the first time it is + // image-bound), and the driver would read whatever that undefined level holds. The + // client sends the bindings GL calls incomplete with level kMGPipeImageLevelNone + // (KHR-GL46.shader_image_load_store.incomplete_textures binds level 2 of a texture that + // defines only level 0); a level or layer past the storage is caught here. Decided from + // the applier's records alone, by the same walk the Vulkan backend makes + // (VkTextureManager::ResolveWireTextureStorage): through texture views to the root, + // against each window's levels and layers. + static Bool ImageViewNamesNoTexel(const MG_Pipe::MGPImageView& image) { + using T = MG_Pipe::MGPipeResourceTarget; + const auto& state = MG_Pipe::MGPipeApplier(); + MG_Pipe::MGPipeHandle handle = image.Res; + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= state.TextureResources.size()) return false; + const auto& first = state.TextureResources[handle.Slot]; + if (!first.Live || first.Gen != handle.Gen) return false; + // GL ignores Layer for a target with no layers, and for a layered bind. + const auto firstTarget = static_cast(first.Desc.Target); + const Bool selectsLayer = !image.Layered && + (firstTarget == T::Tex1DArray || firstTarget == T::Tex2DArray || firstTarget == T::Tex2DMSArray || + firstTarget == T::Tex3D || firstTarget == T::TexCube || firstTarget == T::TexCubeArray); + Uint32 level = image.Level; + Uint32 layer = selectsLayer ? image.Layer : 0; + for (SizeT hop = 0; hop <= state.TextureResources.size(); ++hop) { + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= state.TextureResources.size()) return false; + const auto& record = state.TextureResources[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) return false; + if (MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) { + if (static_cast(record.Desc.Target) == T::TexBuffer) return false; + if (level >= record.Desc.Levels) return true; + const Uint32 layers = static_cast(record.Desc.Target) == T::Tex3D + ? std::max(record.Desc.Depth >> level, 1u) + : std::max(record.Desc.ArrayLayers, 1u); + return layer >= layers; + } + const auto viewHandle = record.ViewCso; + if (MG_Pipe::MGPipeHandleIsNull(viewHandle) || viewHandle.Slot >= state.SamplerViewCsos.size()) + return false; + const auto& viewRecord = state.SamplerViewCsos[viewHandle.Slot]; + const auto& view = viewRecord.View; + if (!viewRecord.Live || viewRecord.Gen != viewHandle.Gen || view.Texture != handle) return false; + if (level >= view.NumLevels || layer >= view.NumLayers) return true; + level += view.MinLevel; + layer += view.MinLayer; + handle = record.Desc.ViewOf; + } + return false; + } + + // What such a unit is bound to instead, on a driver whose EMPTY unit is not enough + // (GLESCapabilities::UseInvalidImagePlaceholder): ES 3.2 8.22 leaves an invalid load's + // alpha undefined, Adreno answers 1 where GL says 0, and ANGLE faults outright. + // Elsewhere the unit stays empty, which is exact. A texel outside + // a level is invalid too, and that Adreno answers (0, 0, 0, 0) with the store dropped - so + // the unit gets a 1x1 texture of the shape and format the bind names, cleared to zero each + // time it is bound. Only texel (0, 0[, 0]) of it exists; it is private to the unit, so a + // store through one invalid unit cannot reach a load through another. Every context of the + // process shares one group (CreateContextTuple), so one set serves them all. + struct InvalidImagePlaceholder { + GLuint Id = 0; + GLenum Target = 0; + GLenum InternalFormat = 0; + }; + static Array + g_invalidImagePlaceholders{}; + + // Zero when the shape has no 1x1 ES form (multisample, buffer) or the driver refused it; + // the caller then leaves the unit empty, which is right for every channel but alpha. + static GLuint EnsureInvalidImagePlaceholder(Uint unit, TextureTarget textureTarget, GLenum internalFormat) { + GLenum target = 0; + GLsizei depth = 1; + switch (TextureImpl::MapToBackendTextureTarget(textureTarget)) { + case TextureTarget::Texture2D: target = GL_TEXTURE_2D; break; + case TextureTarget::Texture2DArray: target = GL_TEXTURE_2D_ARRAY; break; + case TextureTarget::Texture3D: target = GL_TEXTURE_3D; break; + case TextureTarget::TextureCubeMap: target = GL_TEXTURE_CUBE_MAP; break; + case TextureTarget::TextureCubeMapArray: target = GL_TEXTURE_CUBE_MAP_ARRAY; depth = 6; break; + default: return 0; + } + GLenum format = 0, type = 0; + MG_Util::TextureFormatProcessor::NormalizePixelFormat(internalFormat, Flags{}, + nullptr, &format, &type); + if (format == 0 || type == 0) return 0; + InvalidImagePlaceholder& slot = g_invalidImagePlaceholders[unit]; + if (slot.Id != 0 && (slot.Target != target || slot.InternalFormat != internalFormat)) { + g_GLESFuncs.glDeleteTextures(1, &slot.Id); + slot = {}; + } + GLint previous = 0; + const GLenum bindingQuery = target == GL_TEXTURE_2D ? GL_TEXTURE_BINDING_2D + : target == GL_TEXTURE_2D_ARRAY ? GL_TEXTURE_BINDING_2D_ARRAY + : target == GL_TEXTURE_3D ? GL_TEXTURE_BINDING_3D + : target == GL_TEXTURE_CUBE_MAP ? GL_TEXTURE_BINDING_CUBE_MAP + : GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; + g_GLESFuncs.glGetIntegerv(bindingQuery, &previous); + DebugImpl::ErrorLopper::Clear(); + if (slot.Id == 0) { + g_GLESFuncs.glGenTextures(1, &slot.Id); + g_GLESFuncs.glBindTexture(target, slot.Id); + if (depth > 1 || target == GL_TEXTURE_2D_ARRAY || target == GL_TEXTURE_3D) { + g_GLESFuncs.glTexStorage3D(target, 1, internalFormat, 1, 1, depth); + } else { + g_GLESFuncs.glTexStorage2D(target, 1, internalFormat, 1, 1); + } + slot.Target = target; + slot.InternalFormat = internalFormat; + } else { + g_GLESFuncs.glBindTexture(target, slot.Id); + } + // Six faces of the widest texel (rgba32) is the most any shape here holds. + static constexpr Uint8 kZeros[6 * 16] = {}; + // The backend unpack state rests at the ES default (Managers.cpp + // ScopedDefaultUnpackState), and a single row ignores the alignment. + BufferImpl::BindPixelUnpackBufferId(0); + if (target == GL_TEXTURE_CUBE_MAP) { + for (GLenum face = 0; face < 6; ++face) + g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 0, 0, 1, 1, format, type, + kZeros); + } else if (target == GL_TEXTURE_2D) { + g_GLESFuncs.glTexSubImage2D(target, 0, 0, 0, 1, 1, format, type, kZeros); + } else { + g_GLESFuncs.glTexSubImage3D(target, 0, 0, 0, 0, 1, 1, depth, format, type, kZeros); + } + g_GLESFuncs.glBindTexture(target, static_cast(previous)); + Bool failed = false; + DebugImpl::ErrorLopper::Loop([&failed, internalFormat](GLenum err) { + failed = true; + MGLOG_D("DirectGLES: invalid-image placeholder (%s) refused: %s", + MG_Util::ConvertGLEnumToString(internalFormat).c_str(), + MG_Util::ConvertGLEnumToString(err).c_str()); + }); + if (failed) { + g_GLESFuncs.glDeleteTextures(1, &slot.Id); + slot = {}; + } + return slot.Id; + } + + // The record form. `Res` is the texture, the four well-defined fields are the record's, + // Access is decoded, and the texture's own TARGET and STORAGE FORMAT - the two + // properties the bind rules need and MGPImageView has no room for (24 bytes, no pad) - + // come off the RESOURCE DESCRIPTOR, which is where the server already keeps them. + void SyncImageTextureBinding(const MG_Pipe::MGPImageView& view) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + const Uint unit = static_cast(view.Unit); + if (unit >= g_writableImageBufferUnits.size()) return; + TrackWritableImageBufferUnit(unit, IsWritableImageBufferView(view)); + if (MG_Pipe::MGPipeHandleIsNull(view.Res)) { + g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + return; + } + if (unit + 1 > g_imageUnitHighWaterMark) { + g_imageUnitHighWaterMark = unit + 1; + } + const auto* resource = PipeTextureRecordForHandle(view.Res); + if (resource == nullptr) { + // A record naming a texture the applier has no descriptor for is a seam defect, + // not a binding: there is nothing to allocate image-bindable storage from and + // nothing to widen against. Unbind rather than leave the unit on whatever the + // previous draw put there. + MGLOG_E_ONCE("MGPipe: image unit %u names texture {%u, %u}, which has no applier " + "resource record; unbinding the unit", + static_cast(unit), view.Res.Slot, view.Res.Gen); + g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + return; + } + if (ImageViewNamesNoTexel(view)) { + if (!g_GLESCapabilities.UseInvalidImagePlaceholder) { + g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + return; + } + // The format the shader was widened from is the one the application bound, so + // the placeholder takes that format's carrier, as the storage would have. + GLenum format = static_cast(view.InternalFormat); + if (const auto widening = TextureImpl::GetImageBindableStorageWidening( + MG_Util::ConvertGLEnumToTextureInternalFormat(format))) { + format = widening.InternalFormat; + } + const GLuint placeholder = + EnsureInvalidImagePlaceholder(unit, PipeTextureTargetForHandle(view.Res), format); + const GLboolean layered = placeholder != 0 && view.Layered != 0 ? GL_TRUE : GL_FALSE; + g_GLESFuncs.glBindImageTexture(unit, placeholder, 0, layered, 0, GLAccessForImageView(view), + placeholder != 0 ? format : GL_RGBA8); + return; + } + const auto textureFormat = static_cast(resource->Desc.InternalFormat); + const TextureTarget textureTarget = PipeTextureTargetForHandle(view.Res); + const GLenum access = GLAccessForImageView(view); + // Before the sync, for the frontend arm's reason: the note may adopt a twin, and + // `backendTexture` below is a reference into the same registry. + if (access != GL_READ_ONLY) { + NoteDriverSideTextureWriteByHandle(view.Res); + } + auto& backendTexture = SyncTextureToBackendByHandle(view.Res, /*imageBindableStorageRequired=*/true); + if (!backendTexture) return; + IssueImageTextureBind(unit, *backendTexture, textureTarget, textureFormat, + static_cast(view.InternalFormat), static_cast(view.Level), + view.Layered != 0, static_cast(view.Layer), access); + } + + // A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to + // write, and those writes land in the ES driver's buffer object - behind the frontend's + // CPU shadow, which is what MapBuffer and GetBufferSubData read. Same flag, and for the + // same reason, as MarkShaderStorageBuffersGpuWritten does for a storage block; the + // difference is only which binding the shader reaches the buffer through. A GL_READ_ONLY + // binding is left alone: marking it would make the next map wait on - and then re-read - + // a dispatch that could not have changed a byte of it. + // + // Called from the draw and dispatch preparations rather than from the eager sync + // glBindImageTexture performs: that one runs before any shader has touched the buffer, + // and flagging there would pull the driver's copy over a shadow the application may + // still be writing into. + // + // P5e (fb, CONTRACT-P5E.md §5.4): DELETED UNDER A TRANSPORT, and it is one edit paired + // with sb's deletion of the backend's storage-block GPU-write marks. The two were one + // mechanism: the backend told the frontend's buffer shadow "a shader wrote through + // this" because only the backend knew which bindings a dispatch could reach. Under a + // split the CLIENT owns that set and keeps it itself (MG_Impl/Pipe/GpuWritePending.h), + // so the mark has an owner on the side that reads it - and this body cannot run there + // anyway: every line of it is a frontend read (the image binding, the downcast to + // TextureObjectBuffer, the buffer binding slot) plus a HandleOfBuffer probe behind + // MarkBufferGpuWritten. The gate is at the top rather than at the two call sites so + // there is one statement of it, and the unit-tracking bookkeeping below stays out of + // reach of an apply thread entirely. + void MarkWritableImageBufferTexturesGpuWritten() { + // P8-C: the SERVER's twin of the buffer behind each writable buffer image, for + // this side's CPU readers (SplitHostBytesForCpuRead). Every read is a record: + // the unit set SyncImageTextureBinding(view) tracked, the applier's image view, + // and the texture descriptor's backing-store handle. + if (g_writableImageBufferUnitCount == 0) return; + const auto& st = MG_Pipe::MGPipeApplier(); + for (Uint unit = 0; unit < g_writableImageBufferUnits.size() && unit < st.BoundShaderImages.size(); + ++unit) { + if (!g_writableImageBufferUnits[unit]) continue; + const MG_Pipe::MGPImageView& view = st.BoundShaderImages[unit]; + if (!IsWritableImageBufferView(view)) continue; + const auto* record = PipeTextureRecordForHandle(view.Res); + if (record == nullptr) continue; + if (auto* buffer = BufferImpl::FindBufferResourceForHandle(record->Desc.BufferForTexBuffer)) { + buffer->serverGpuWritten = true; + } + } + return; + } + + void SyncImageTextureBindings() { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // The frontend tracks more image units than ES exposes; binding past the device + // limit raises GL_INVALID_VALUE on every dispatch. + const Uint unitCount = std::min(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS, + static_cast(std::max(g_GLESCapabilities.MaxImageUnits, 0))); + // P4a e3 (D-G2, D-J2), CORRECTED BY THE REVIEW'S MAJOR-1. + // + // THE RECORD'S WINDOW IS NOT THIS SWEEP'S MEMBERSHIP. D-J2 and PipeApply.h's MGPipeWorkingState + // are a rule about RECORD RETENTION - entries outside the var-tail window are not + // cleared in the applier - and say nothing about which DRIVER units a re-issue + // sweep has to visit. The membership this loop needs is "every unit the driver + // currently has an image on", and that is g_imageUnitHighWaterMark, which is + // server-owned by construction: SyncImageTextureBinding is the only path to + // glBindImageTexture and raises the mark on every unit it hands a texture, so the + // mark is monotonic and covers every unit a re-mint could strand. + // + // The first version of this arm walked [Start, Start + Count) and returned, which + // is the SMALLER-WINDOW failure direction DEV-3 refused for keys.maxTouchedUnit: if + // set_shader_images is the program-RESOLVED set, a program declaring only unit 0 + // pushes a window of [0,1), and unit 5's image - established eagerly by an earlier + // dispatch and never revisited - is skipped. A texture re-minted between the two + // dispatches then leaves unit 5 on a deleted driver name: a write through a freed + // allocation on Adreno, a rejected dispatch on Mali. The pre-handle sweep bound + // EVERY unit precisely so that could not happen, and that guarantee is what this + // sweep exists for. So the record's window is UNIONED with the mark, never + // substituted for it, and the walk starts at 0 rather than at Start (a unit BELOW + // Start is as undescribed as one above Start + Count). + // + // A8, which the review adds to this package's assumption list: NOTHING IN THE + // CONTRACT PINS THE WINDOW'S MEMBERSHIP. PipeApply.h's MGPipeWorkingState says only that the + // record is the last set as received, and that Start + Count above the bound is + // Fatal{ProtocolCorruption}. Until package C documents in ImageEmit.h that the + // window covers every unit the driver holds, this union is load-bearing; the + // verification round logs (ShaderImageStart, ShaderImageCount) against the mark for + // a Minecraft frame and says whether it can ever be relaxed. + // + // What the record still buys is real and is e3's actual deliverable: the four field + // VALUES come off it inside SyncImageTextureBinding, and the walk stops at the + // high-water mark instead of at the device's MaxImageUnits. The units in + // [end, unitCount) the pre-handle arm additionally visits have never been given a + // texture through the only funnel that can give one, so re-binding 0 on them is a + // provable no-op. + { + const auto& st = MG_Pipe::MGPipeApplier(); + // P4a decline-site I6: M - a set that never arrived means the full pre-handle + // sweep below, which is the SAFE (wider) direction. SILENT HERE, CONFIRMED AT + // THE VERIFICATION ROUND, and it is silent here because I2 says it once per + // process from inside that sweep, where the unit that actually holds an image + // texture can be named. Two lines for one condition is how a grep's count + // stops meaning anything. + if (st.ShaderImageCount != 0) { + // P4a decline-site I7: the window/mark UNION (MAJOR-1, fixed here); no flip + // remains at the verification round, only the A8 measurement above. + // + // P5e (fb) KEEPS THE UNION and says so again, because ruling 7 / ID-86 make + // it contract text now (§5.4: "never narrowed to the window"). Under a + // transport the walk additionally READS the applier's array rather than the + // frontend binding at each unit - a unit inside the mark but outside the + // window keeps whatever MGPImageView the last set that covered it left + // there (D-J2: entries outside the window are not cleared), which is the + // record of the very binding that raised the mark. + const Uint32 end = std::min( + std::max(st.ShaderImageStart + st.ShaderImageCount, + static_cast(g_imageUnitHighWaterMark)), + static_cast(unitCount)); + for (Uint32 unit = 0; unit < end; ++unit) { + if (unit >= st.BoundShaderImages.size()) break; + // The union can include a slot no set_shader_images + // record has described yet. Its zero-initialized Unit + // is not a binding identity: treating it as one unbinds + // real unit 0 after binding the current program's images. + // Array position identifies both retained and empty slots. + auto view = st.BoundShaderImages[unit]; + view.Unit = unit; + SyncImageTextureBinding(view); + } + return; + } + } + // The wide pre-handle sweep reads the frontend binding of every unit, which an + // apply thread may not do. Under a transport there is nothing for it to do anyway: + // g_imageUnitHighWaterMark is raised only by the funnel above, so if no set has + // ever arrived no unit has ever been given an image, and re-binding 0 on units that + // never held one is the no-op the comment above already proved. + return; + } + + // What the draw path last swept the image units against. A draw never swept them at all: + // an image unit was established once, eagerly, by glBindImageTexture and never revisited. + // That is stale the moment the texture behind it is re-specified with a new size or + // format, because ES 3.1 only allows IMMUTABLE storage on an image unit + // (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be + // redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at + // the deleted one and imageSize() reporting the old dimensions + // (KHR-GL43.shader_image_size.advanced-changeSize). + static Uint64 g_imageSweepContextId = 0; + static Uint64 g_imageSweepSamplingGeneration = 0; + } // namespace TextureImpl +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc deleted file mode 100644 index 10fba55d..00000000 --- a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc +++ /dev/null @@ -1,469 +0,0 @@ -// MobileGL - MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc -// 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 - -// Included in DirectGLES.cpp after the native packed-word readback helper. -// A storage promotion needs the driver's own texels as owned tight rows, not -// client PACK layout or a fallback into a frontend texture's potentially stale -// shadow. - class ScopedWireTextureReadLevel { - public: - ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level) - : m_texture(texture), m_target(target) { - switch (target) { - case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break; - case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break; - case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break; - case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break; - case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break; - default: return; - } - GLint previous = 0; - DrainESErrors(); - g_GLESFuncs.glGetIntegerv(m_binding, &previous); - g_GLESFuncs.glBindTexture(m_target, m_texture); - g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel); - g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel); - m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR; - if (m_saved) { - // ES framebuffer completeness can reject a defined level whose - // base level or intervening mip chain is missing/inconsistent. - // Read this existing native image as the sole base level. The - // logical GL parameters and every cached copy remain unchanged. - g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level); - g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level); - m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR; - } - g_GLESFuncs.glBindTexture(m_target, static_cast(previous)); - } - - ~ScopedWireTextureReadLevel() { - if (!m_saved) return; - // The packed-word path can use its own texture scratch binding. - // Save the binding at restoration time too; do not assume an active - // texture unit or make the backend's binding cache forget a bind. - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(m_binding, &previous); - g_GLESFuncs.glBindTexture(m_target, m_texture); - g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel); - g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel); - g_GLESFuncs.glBindTexture(m_target, static_cast(previous)); - } - - Bool Valid() const { return m_valid; } - - private: - GLuint m_texture; - GLenum m_target; - GLenum m_binding = GL_NONE; - GLint m_baseLevel = 0, m_maxLevel = 0; - Bool m_saved = false, m_valid = false; - }; - - static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type, - Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier, - Vector& bytes) { - ReadbackChannelMapping mapping{}; - if (!GetReadbackChannelMapping(format, mapping)) return false; - const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); - if (!pixelBytes || width <= 0 || height <= 0) return false; - const GLenum componentType = QueryReadAttachmentComponentType(); - const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT; - if (carrier.Active() ? (mapping.isInteger || !integerAttachment) - : mapping.isInteger != integerAttachment) return false; - - GLint implFormat = 0, implType = 0; - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); - g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); - struct Candidate { GLenum format, type; }; - Vector candidates; - if (carrier.Active()) { - candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT}); - } else if (mapping.isInteger) { - if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) && - (implType == GL_INT || implType == GL_UNSIGNED_INT)) - candidates.push_back({static_cast(implFormat), static_cast(implType)}); - candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)}); - } else { - if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) && - (CanDecodeWideSourceType(implType) || - (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) - candidates.push_back({static_cast(implFormat), static_cast(implType)}); - if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT}); - if (componentType == GL_SIGNED_NORMALIZED) { - candidates.push_back({GL_RGBA, GL_SHORT}); - candidates.push_back({GL_RGBA, GL_BYTE}); - } else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE}); - } - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); - const SizeT pixels = static_cast(width) * static_cast(height); - Vector wide; - GLenum wideType = GL_NONE; - Int channels = 0; - DrainESErrors(); - for (const auto candidate : candidates) { - channels = GetWideReadChannelCount(candidate.format); - wide.resize(pixels * static_cast(channels) * GetReadbackComponentSize(candidate.type)); - g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; } - } - if (wideType == GL_NONE) return false; - - if (carrier.Active()) { - Vector decoded(pixels * 4 * sizeof(Float)); - auto* dst = reinterpret_cast(decoded.data()); - const auto* src = reinterpret_cast(wide.data()); - for (SizeT i = 0; i < pixels; ++i) { - for (SizeT c = 0; c < 4; ++c) { - const Float divisor = static_cast(carrier.ChannelMax[c]); - dst[i * 4 + c] = carrier.SignedNormalized - ? std::max(static_cast(static_cast(static_cast(src[i * 4 + c]))) / divisor, -1.0f) - : static_cast(src[i * 4 + c]) / divisor; - } - } - wide = Move(decoded); wideType = GL_FLOAT; channels = 4; - } else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { - Vector decoded(pixels * 4 * sizeof(Float)); - auto* dst = reinterpret_cast(decoded.data()); - for (SizeT i = 0; i < pixels; ++i) { - Uint32 word = 0; - Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word)); - dst[i * 4] = static_cast(word & 1023u) / 1023.0f; - dst[i * 4 + 1] = static_cast((word >> 10) & 1023u) / 1023.0f; - dst[i * 4 + 2] = static_cast((word >> 20) & 1023u) / 1023.0f; - dst[i * 4 + 3] = static_cast((word >> 30) & 3u) / 3.0f; - } - wide = Move(decoded); wideType = GL_FLOAT; channels = 4; - } - if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType); - if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType); - Vector converted(pixels * pixelBytes); - const SizeT sourceRowBytes = static_cast(width) * 4 * GetReadbackComponentSize(wideType); - for (GLsizei row = 0; row < height; ++row) - ClientReadback::ConvertWideReadbackRow(wide.data() + static_cast(row) * sourceRowBytes, - converted.data() + static_cast(row) * width * pixelBytes, width, wideType, mapping, type); - bytes = Move(converted); - return true; - } - - static Bool ReadWireDepthStencilLevelTight(GLuint texture, TextureTarget target, - TextureUploadTarget uploadTarget, GLint level, - const IntVec3& extent, GLenum format, GLenum type, - Vector& bytes) { - const SizeT pixelBytes = MG_Util::GetInputBytesPerPixel( - MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type)); - if (!pixelBytes) return false; - const Bool needDepth = format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL; - const Bool needStencil = format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL; - ScopedFramebufferBinding bindings(true, false); - GLuint fbo = 0; - g_GLESFuncs.glGenFramebuffers(1, &fbo); - struct DeleteFbo { GLuint id; ~DeleteFbo() { g_GLESFuncs.glDeleteFramebuffers(1, &id); } } cleanup{fbo}; - FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, fbo); - ScopedPixelPackBuffer packBuffer(0); - ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); - const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); - const GLenum point = needDepth ? (needStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT) - : GL_STENCIL_ATTACHMENT; - const SizeT count = static_cast(extent.x()) * extent.y(); - Vector result(count * extent.z() * pixelBytes); - if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS); - g_GLESFuncs.glFinish(); - for (GLint z = 0; z < extent.z(); ++z) { - if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || - nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY) - g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, point, texture, level, z); - else - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, point, - TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget), texture, level); - g_GLESFuncs.glReadBuffer(GL_NONE); - if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false; - Vector depth; - Vector stencil; - if (needDepth && !ReadDepthValuesNative(0, 0, extent.x(), extent.y(), depth)) return false; - if (needStencil && !ReadStencilBytesNative(0, 0, extent.x(), extent.y(), stencil)) return false; - auto* dst = result.data() + count * z * pixelBytes; - for (SizeT i = 0; i < count; ++i) { - if (format == GL_DEPTH_STENCIL) { - const Uint32 s = stencil[i]; - if (type == GL_UNSIGNED_INT_24_8) { - const Uint32 packed = (NormalizedDepthToUnsigned(depth[i], 16777215.0) << 8) | s; - Memcpy(dst + i * pixelBytes, &packed, sizeof(packed)); - } else if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) { - Memcpy(dst + i * pixelBytes, &depth[i], sizeof(Float)); - Memcpy(dst + i * pixelBytes + sizeof(Float), &s, sizeof(s)); - } else return false; - } else if (type == GL_FLOAT) { - const Float value = needDepth ? depth[i] : static_cast(stencil[i]); - Memcpy(dst + i * pixelBytes, &value, sizeof(value)); - } else if (type == GL_HALF_FLOAT) { - const Uint16 value = MG_Util::EncodeFloatToHalfBits( - needDepth ? depth[i] : static_cast(stencil[i])); - Memcpy(dst + i * pixelBytes, &value, sizeof(value)); - } else { - const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT; - const Double maximum = signedType - ? (pixelBytes == 1 ? 127.0 : pixelBytes == 2 ? 32767.0 : 2147483647.0) - : (pixelBytes == 1 ? 255.0 : pixelBytes == 2 ? 65535.0 : 4294967295.0); - const Uint32 value = needDepth ? NormalizedDepthToUnsigned(depth[i], maximum) : stencil[i]; - if (pixelBytes == 1) dst[i] = static_cast(value); - else if (pixelBytes == 2) { const Uint16 v = static_cast(value); Memcpy(dst + i * 2, &v, 2); } - else if (pixelBytes == 4) Memcpy(dst + i * 4, &value, 4); - else return false; - } - } - } - bytes = Move(result); - return true; - } - - Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, - TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent, - Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector& bytes) { - if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false; - const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent); - const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); - ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level); - if (!readLevel.Valid()) return false; - if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) - return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes); - ReadbackChannelMapping mapping{}; - if (!GetReadbackChannelMapping(format, mapping)) return false; - const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); - if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false; - const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); - const auto widening = sourceUsesImageCarrier - ? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{}; - const SizeT sliceBytes = static_cast(extent.x()) * extent.y() * pixelBytes; - Vector result(sliceBytes * static_cast(extent.z())); - if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS); - g_GLESFuncs.glFinish(); - - // A raw packed word may have redundant encodings or NaN payloads. Read - // it through R32UI when the source really still uses that native layout. - const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( - logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format), - MG_Util::ConvertGLEnumToTexturePixelDataType(type)); - if (rawPacked && pixelBytes == sizeof(Uint32)) { - const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX && - uploadTarget <= TextureUploadTarget::CubeMapNegativeZ - ? static_cast(uploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX) : 0; - for (GLint layer = 0; layer < extent.z(); ++layer) - if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(), - reinterpret_cast(result.data() + static_cast(layer) * sliceBytes))) return false; - bytes = Move(result); - return true; - } - - NormalizedImageCarrierRead carrier{}; - if (widening && widening.CarriesNormalizedCodes()) { - for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c]; - carrier.SignedNormalized = widening.SignedNormalized; - } - TempFBOBinder binder(true); - auto& framebuffer = binder.Framebuffer(); - for (GLint layer = 0; layer < extent.z(); ++layer) { - if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY) - ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer); - else - ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level); - ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0); - const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) { - MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x", - status, texture, level, nativeTarget); - return false; - } - Vector slice; - if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type, - TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) || - slice.size() != sliceBytes) return false; - Memcpy(result.data() + static_cast(layer) * sliceBytes, slice.data(), sliceBytes); - } - bytes = Move(result); - return true; - } - - // P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the - // twin's last sync. A re-mint asks this before reading a level back, because a level defined - // after that sync (null data, or null data plus a partial upload) is not on the driver yet and - // its FBO attachment is incomplete - which used to end the session at - // Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL. - Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, - GLint level, const IntVec3& logicalExtent) { - if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true; - const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent); - const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); - GLenum binding = GL_NONE; - switch (nativeTarget) { - case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break; - case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break; - case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break; - case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break; - case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break; - default: return true; - } - // A cube's levels are per face; every other target answers for the whole level. - const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP - ? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget; - DrainESErrors(); - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(binding, &previous); - g_GLESFuncs.glBindTexture(nativeTarget, texture); - GLint width = 0, height = 0, depth = 0; - g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width); - g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height); - g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth); - const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR; - g_GLESFuncs.glBindTexture(nativeTarget, static_cast(previous)); - if (!answered) return true; - const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || - nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY; - return width == extent.x() && height == extent.y() && (!layered || depth == extent.z()); - } - - // P8-E (docs/archive/notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=x, READ BY THE SERVER: - // a colour level of exactly that extent is treated as one the driver refused to attach - what an - // Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do. - // Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only - // the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it. - static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) { - static const IntVec3 s_refused = [] { - const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT"); - if (value == nullptr) return IntVec3{0, 0, 0}; - char* end = nullptr; - const long width = std::strtol(value, &end, 10); - if (end == value || *end != 'x') return IntVec3{0, 0, 0}; - const long height = std::strtol(end + 1, nullptr, 10); - if (width <= 0 || height <= 0) return IntVec3{0, 0, 0}; - return IntVec3{static_cast(width), static_cast(height), 1}; - }(); - return s_refused.x() > 0 && extent == s_refused; - } - - // P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store - // is the only CPU copy of the level on this side, and it answers only while its bytes ARE the - // level: every byte crossed (IsCovered), at the extent asked for, and the texture was never - // written by the driver except by copies the store followed (!IsDriverWritten - see - // StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is - // GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type - // takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row. - // Anything else DECLINES BY NAME: the reply is an error, and the client's - // Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has - // since overwritten. - static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat, - TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent, - GLenum format, GLenum type, Vector& bytes) { - auto& store = MG_Record::ServerStagedTexture(); - const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res); - const auto target = static_cast(uploadTarget); - const auto mip = static_cast(level); - ReadbackChannelMapping mapping{}; - const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0; - const SizeT texels = static_cast(std::max(extent.x(), 0)) * static_cast(std::max(extent.y(), 0)) * - static_cast(std::max(extent.z(), 0)); - const char* declined = nullptr; - if (!store.Holds()) { - declined = "this arm keeps no staged store"; - } else if (store.IsDriverWritten(key)) { - declined = "the driver has written the texture since its bytes crossed"; - } else if (!store.IsCovered(key, target, mip)) { - declined = "not every byte of the level crossed"; - } else if (store.LevelExtentOrUndefined(key, target, mip) != extent) { - declined = "the staged level is not the extent asked for"; - } else if (pixelBytes == 0 || texels == 0) { - declined = "the format/type pair has no readback mapping"; - } - Vector out; - if (declined == nullptr) { - const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store"); - if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( - logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format), - MG_Util::ConvertGLEnumToTexturePixelDataType(type))) { - if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) { - declined = "the staged words are not the client's word size"; - } else { - out.assign(staged, staged + texels * pixelBytes); - } - } else { - Vector wide; - Bool isInteger = false; - Bool isSigned = false; - if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide, - isInteger, isSigned)) { - declined = "the staged format has no wide decoding"; - } else if (mapping.isInteger != isInteger) { - declined = "an integer read of a normalized level, or the reverse"; - } else { - const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT; - const SizeT rowTexels = static_cast(extent.x()); - const SizeT rows = texels / rowTexels; - const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType); - out.resize(texels * pixelBytes); - for (SizeT row = 0; row < rows; ++row) { - ClientReadback::ConvertWideReadbackRow(wide.data() + row * wideRowBytes, - out.data() + row * rowTexels * pixelBytes, rowTexels, - wideType, mapping, type); - } - } - } - } - if (declined != nullptr) { - MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store " - "cannot answer it (%s) - DECLINED (texture-readback-store-declined)", - res.Slot, res.Gen, level, declined); - return false; - } - MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); " - "answered from the server's staged store (texture-readback-from-store)", - res.Slot, res.Gen, level, static_cast(logicalFormat)); - bytes = Move(out); - return true; - } - - Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector& bytes) { - const auto* record = PipeTextureRecordForHandle(info.Res); - if (!record) return false; - TextureTarget target = TextureTarget::Unknown; - for (Int i = 0; i < static_cast(TextureTarget::TextureTargetCount); ++i) - if (MG_Pipe::MGPipeResourceTargetForTextureTarget(static_cast(i)) == record->Desc.Target) - target = static_cast(i); - if (target == TextureTarget::Unknown) return false; - const auto twin = TextureImpl::SyncTextureToBackendByHandle(info.Res); - if (!twin) return false; - const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP; - const IntVec3 extent{static_cast(info.Box.W), static_cast(info.Box.H), - wholeCube ? 1 : static_cast(info.Box.D)}; - const auto logicalFormat = static_cast(record->Desc.InternalFormat); - // The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is - // ReadTextureLevelTight's first choice for exactly these reads). - const Bool packedWordRead = - !twin->RequiresImageBindableStorage() && - MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( - logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), - MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) && - MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), - MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32); - const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent); - Vector result; - for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) { - const auto uploadTarget = wholeCube - ? static_cast(static_cast(TextureUploadTarget::CubeMapPositiveX) + face) - : MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target); - Vector slice; - if (refusedForTesting || - !ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level, - extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) { - if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent, - info.Format, info.Type, slice)) return false; - } - result.insert(result.end(), slice.begin(), slice.end()); - } - bytes = Move(result); - return true; - }