mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
Audit of every memoization implementation; sixteen verified defects fixed: DirectGLES backend: - Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its sibling shadows; its identity+version key is only monotonic within one GLContext, so a library teardown + re-init could false-hit on a recycled FBO address. - Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment generation that the SyncCurrentFBO gate and every FBO twin compare, so driver FBOs re-attach instead of keeping the deleted texture name; the attachment walk re-enters until the generation is quiescent (a walk itself can re-mint). - Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a generation the VAO twin sync compares, forcing a full re-emit of the baked glVertexAttribPointer / element-array bindings that frontend versions cannot see. - VAO element-array sync memo: bound-object identity joins the wrapping Uint16 slot version (same pairing the ResolvedDrawBuffers IBO memo already uses). DirectVulkan backend: - EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a shadow-backed persistent map mutates with no epoch bump, so a hit must decline. - VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float. - GetOrCreateComputePipeline no longer memoizes a failed creation (same contract as PipelineFactory): a transient driver failure was permanently disabling every dispatch of that program. - Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler filter/aniso/LOD setters bump only that counter, so the old key served a stale verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes. - SetupDraw fast path declines instead of re-arming on a moved sampling-resolution generation (the snapshot bakes the LOD verdict into its pipeline), and recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback cannot bind the undecorated variant and silently capture nothing. - VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO state-pointer memos outlive the factory across renderer recreation, and a fresh factory restarting at epoch 1 would dereference a dead factory's entry. - Cached render passes re-read the live renderbuffer clear payload at begin (the clear VALUE is not in the pass hash; the entry's inline snapshot replayed the creation-time color and dropped the newly queued one). - FramebufferObject gains a never-reused lifetime id, keyed into the render-pass fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16 version pair, which address reuse plus fresh version counts could equal. - SyncTextureResource's preserved-content image goes through the deferred-release ring on both failure paths instead of a synchronous destructor under the GPU. MG_State frontend: - Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers glCompileShader forever once the environment's content changes. - Pipeline composite cache rebuilds from each stage program's last-link shader snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live attach list and current compile nodes: post-link glAttachShader/glCompileShader must not leak into the composite while the (lifetimeId, linkVersion) signature still hits - GL's "as last linked" rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
1358 lines
82 KiB
C++
1358 lines
82 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/Managers.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 <Includes.h>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include "DirectGLES.h"
|
|
#include "MG_State/GLState/SamplerState/SamplerObject.h"
|
|
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
|
#include <MG_State/GLState/TextureState/TextureObject.h>
|
|
#include <MG_State/GLState/Core.h>
|
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
|
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
|
|
|
// True once the process has entered exit(): past that point the EGL library and
|
|
// the driver may already be unloaded, so a backend twin's destructor must not
|
|
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
|
// pointer from __run_exit_handlers) nor touch statics in other TUs (cross-TU
|
|
// destruction order is unspecified). Deliberate leak: the process is exiting and
|
|
// the driver reclaims GPU objects. The flag is set by a std::atexit handler that
|
|
// EnsureProcessTeardownSentinel() registers lazily on first registry use - by
|
|
// then every static everywhere has finished constructing, so this handler is
|
|
// guaranteed to run BEFORE any static destructor (atexit is LIFO). A destructor
|
|
// hook on the registry itself was tried first and is WRONG: tests and cache
|
|
// resets destroy temporary registry instances mid-run, which would latch the
|
|
// flag while the process is very much alive.
|
|
Bool InProcessTeardown();
|
|
void EnsureProcessTeardownSentinel();
|
|
|
|
// Generation of the backend ES context that owns the driver ids currently handed
|
|
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
|
|
// driver name (texture, framebuffer, renderbuffer, sampler) stamps this at
|
|
// construction and compares it in its destructor: a twin outliving its context
|
|
// must NOT glDelete* its id, because a successor context may already have recycled
|
|
// that name and the delete would take out a live object of the new context.
|
|
extern Uint g_backendContextGeneration;
|
|
|
|
// Which optional pieces of state a draw needs synchronized before it is issued.
|
|
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
|
// draws that provably cannot read them.
|
|
enum class DrawSyncBit : Uint32 {
|
|
None = 0,
|
|
IndexBuffer = 1 << 0,
|
|
IndirectBuffer = 1 << 1,
|
|
Instancing = 1 << 2
|
|
};
|
|
// Deliberately the shared Flags<> rather than hand-written operators for this enum:
|
|
// a namespace-local operator| here would hide MobileGL::operator|(Bit, Bit) from
|
|
// every other scoped-enum flag set used inside this namespace.
|
|
using DrawSyncFlags = Flags<DrawSyncBit>;
|
|
|
|
// The GL-defined indirect command layouts, byte-identical to what the driver reads
|
|
// out of a GL_DRAW_INDIRECT_BUFFER. Also the staging layout the multi-draw emulation
|
|
// synthesizes commands into.
|
|
struct DrawElementsIndirectCommand {
|
|
Uint32 count = 0;
|
|
Uint32 instanceCount = 0;
|
|
Uint32 firstIndex = 0;
|
|
Int32 baseVertex = 0;
|
|
Uint32 baseInstance = 0;
|
|
};
|
|
|
|
struct DrawArraysIndirectCommand {
|
|
Uint32 count = 0;
|
|
Uint32 instanceCount = 0;
|
|
Uint32 first = 0;
|
|
Uint32 baseInstance = 0;
|
|
};
|
|
|
|
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
|
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
|
void PrepareForDraw(DrawSyncFlags syncBits);
|
|
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
|
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
|
void CheckPrimitiveRestartSupported(GLenum indexType);
|
|
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
|
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
|
void SetCurrentBaseInstance(Uint32 baseInstance);
|
|
void SetCurrentDrawID(Uint32 drawId);
|
|
// GL's gl_BaseVertex is the base-vertex parameter of an indexed draw and zero for every
|
|
// command that has none - including all the DrawArrays forms - so every draw path that
|
|
// does not carry one must leave this at zero rather than inherit the last draw's value.
|
|
void SetCurrentBaseVertex(Int32 baseVertex);
|
|
// True when the current program actually reads gl_DrawID, i.e. when a batched
|
|
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
|
// batch and would therefore be wrong.
|
|
Bool CurrentProgramReadsDrawID();
|
|
// Same question for gl_BaseVertex: a batched multi-draw tier cannot give each sub-draw
|
|
// its own base vertex through a uniform either.
|
|
Bool CurrentProgramReadsBaseVertex();
|
|
// Both of the above, conservatively, for a caller that must decide BEFORE PrepareForDraw
|
|
// has synced the program - where "does not read it" is indistinguishable from "cannot be
|
|
// asked yet". Answers true whenever the backend twin is missing or predates the current
|
|
// link.
|
|
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
|
|
|
template <typename StateObject, typename BackendObject>
|
|
class StateBackendObjectRegistry {
|
|
public:
|
|
|
|
using StatePtr = SharedPtr<StateObject>;
|
|
using StateWeakPtr = std::weak_ptr<StateObject>;
|
|
using BackendPtr = SharedPtr<BackendObject>;
|
|
|
|
// The backend twin and the weak reference that decides whether the raw key still
|
|
// names the state object the twin was built for. Both live in one entry: a
|
|
// separate liveness map answered nothing the backend probe had not already found
|
|
// and cost a second hash lookup on every Find, which the draw path runs ~10 times.
|
|
struct Entry {
|
|
BackendPtr backend;
|
|
StateWeakPtr stateRef;
|
|
};
|
|
using BackendMap = UnorderedMap<StateObject*, Entry>;
|
|
using iterator = typename BackendMap::iterator;
|
|
using const_iterator = typename BackendMap::const_iterator;
|
|
|
|
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
|
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
|
|
|
|
// Twin creation is the moment a driver-owned id starts needing a guarded
|
|
// destructor; cold path, so the once-guard costs nothing per draw.
|
|
EnsureProcessTeardownSentinel();
|
|
auto& entry = m_entries[stateObj.get()];
|
|
if (entry.stateRef.expired()) {
|
|
// The previous owner of this address is gone and the allocator handed it
|
|
// to a new object: its twin describes ids the new state object never made.
|
|
entry.backend.reset();
|
|
}
|
|
entry.stateRef = stateObj;
|
|
return entry.backend;
|
|
}
|
|
|
|
// Null when no live state object owns this key. The result points into the map, so
|
|
// it stays valid only until the next GetOrCreate/Find/CollectGarbage on this registry.
|
|
// Take that literally, including for Find: the map is open-addressed and erases by
|
|
// shifting the rest of the probe cluster into the hole, so an erase relocates entries
|
|
// OTHER than the erased one - and Find erases, whenever it lands on a key whose state
|
|
// object has expired. Callers that need the twin across another registry call must copy
|
|
// the BackendPtr out (or keep only the pointee, which is heap-allocated and never moves).
|
|
BackendPtr* Find(StateObject* stateObj) {
|
|
const auto entryIt = m_entries.find(stateObj);
|
|
if (entryIt == m_entries.end()) {
|
|
return nullptr;
|
|
}
|
|
if (entryIt->second.stateRef.expired()) {
|
|
m_entries.erase(entryIt);
|
|
return nullptr;
|
|
}
|
|
return &entryIt->second.backend;
|
|
}
|
|
|
|
const BackendPtr* Find(StateObject* stateObj) const {
|
|
return const_cast<StateBackendObjectRegistry*>(this)->Find(stateObj);
|
|
}
|
|
|
|
iterator begin() { return m_entries.begin(); }
|
|
const_iterator begin() const { return m_entries.begin(); }
|
|
iterator end() { return m_entries.end(); }
|
|
const_iterator end() const { return m_entries.end(); }
|
|
|
|
void CollectGarbageIfNeeded() {
|
|
++m_gcTick;
|
|
if (m_gcTick < kGCInterval) {
|
|
return;
|
|
}
|
|
CollectGarbage();
|
|
m_gcTick = 0;
|
|
}
|
|
|
|
void CollectGarbageNow() { CollectGarbage(); }
|
|
|
|
private:
|
|
void CollectGarbage() {
|
|
if (m_isCollecting) {
|
|
return;
|
|
}
|
|
|
|
m_isCollecting = true;
|
|
|
|
Vector<StateObject*> staleKeys;
|
|
staleKeys.reserve(m_entries.size());
|
|
for (const auto& [stateKey, entry] : m_entries) {
|
|
if (entry.stateRef.expired()) {
|
|
staleKeys.push_back(stateKey);
|
|
}
|
|
}
|
|
|
|
for (auto* stateKey : staleKeys) {
|
|
m_entries.erase(stateKey);
|
|
}
|
|
|
|
m_isCollecting = false;
|
|
}
|
|
|
|
private:
|
|
static constexpr Uint32 kGCInterval = 1024;
|
|
BackendMap m_entries;
|
|
Uint32 m_gcTick = 0;
|
|
Bool m_isCollecting = false;
|
|
};
|
|
|
|
namespace BufferImpl {
|
|
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
|
|
|
|
// --- Buffer-mutation epoch -------------------------------------------------
|
|
// Manager-wide monotonic counter: it moves whenever ANY buffer resource may
|
|
// have gone from draw-clean to dirty. Draw-path memos read it once per pass
|
|
// (CurrentBufferMutationEpoch, acquire), re-run their IsBufferDrawClean
|
|
// probes only when it moved, and stamp the PRE-pass value after a pass in
|
|
// which every probe came up clean - so a concurrent bump lands strictly
|
|
// after the stamped value and forces a re-probe on the next pass no matter
|
|
// how the probe interleaved with the mutation. Conservative-correct: a bump
|
|
// never skips work, it only re-runs the probes once.
|
|
//
|
|
// Every clean->dirty transition path bumps it (BumpBufferMutationEpoch,
|
|
// release, AFTER the mutation lands so an acquire reader that still sees
|
|
// the old epoch cannot have missed the mutation):
|
|
// * the frontend BufferBackendOps table - Respecify, SubData,
|
|
// FlushMappedRange, AcquirePersistentMap, ReadbackFromGpu, OnDestroy -
|
|
// which every frontend change-serial bump and every pending-range
|
|
// queueing reaches while ops are registered (upload, orphan/respecify,
|
|
// map flush/unmap writeback, persistent-map adoption, delete/pooling);
|
|
// * backend-initiated shadow writebacks that bump the frontend change
|
|
// serial without an op: transform-feedback capture readback
|
|
// (XfbImpl::ReadbackCapturedRanges and the scatter path) and every
|
|
// pack-PBO WritebackFromBackend site (glReadPixels/glGetTexImage);
|
|
// * RegisterBufferBackendOps/UnregisterBufferBackendOps - while ops are
|
|
// unregistered, frontend writes advance serials silently, so both edges
|
|
// of that window re-open every memo;
|
|
// * OnBackendContextDestroyed - the buffer context generation moved, so
|
|
// every previously clean resource is invalid.
|
|
// NOT bumped (cleanliness provably unchanged): MarkGpuWritten (the backend
|
|
// copy is authoritative; IsBufferDrawClean does not consult it),
|
|
// NotifyContentWrite on a GPU-resident buffer (persistent-mapped resources
|
|
// are clean by construction), and EnsureBufferResource itself (it only
|
|
// repairs toward clean). A non-persistent map (draws on it are GL errors
|
|
// the frontend rejects) sets IsMapped without an op; persistent maps reach
|
|
// AcquirePersistentMap or (FLUSH_EXPLICIT) publish only via FlushMappedRange.
|
|
Uint64 CurrentBufferMutationEpoch();
|
|
void BumpBufferMutationEpoch();
|
|
|
|
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
|
|
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
|
|
// draw-time "sync" reduces to ensuring the storage exists.
|
|
class GLESBufferResource : public MG_State::GLState::BackendBufferResource {
|
|
public:
|
|
~GLESBufferResource() override = default;
|
|
|
|
Uint id = 0;
|
|
SizeT storageSize = 0;
|
|
Bool storageInitialized = false;
|
|
// ES context generation this resource's id belongs to; ids from a
|
|
// destroyed context are invalid and must not be deleted or reused.
|
|
Uint contextGeneration = 0;
|
|
// Frontend change serial the backend storage reflects. When immediate
|
|
// ops cannot run (ops unregistered, no current context), this lags and
|
|
// EnsureBufferResource falls back to a full re-upload. Atomic: read on
|
|
// the context-owning thread while ops on other threads may update it.
|
|
std::atomic<Uint64> syncedChangeSerial{0};
|
|
// Ops that arrived while no ES context was current on the calling thread
|
|
// (or before storage existed); replayed by EnsureBufferResource. The ES
|
|
// context migrates between app threads, so deferring ops can race with
|
|
// the owning thread replaying them: guard both fields with pendingMutex.
|
|
Bool pendingRespecify = false;
|
|
VecRange1D pendingRanges;
|
|
std::mutex pendingMutex;
|
|
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
|
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
|
// at 1). Written only on the draw thread; per-draw resource consumers
|
|
// (the UBO binding walk) skip the probe while their pre-pass epoch read
|
|
// matches, exactly like the per-VAO memo stamps.
|
|
Uint64 drawCleanEpoch = 0;
|
|
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
|
|
// immutable, persistently+coherently mapped, and persistentPtr is what the app
|
|
// (and the frontend PipeResource) write into directly. While set, draw-time
|
|
// sync is a no-op and no per-draw glBufferSubData is issued. Cleared on ES
|
|
// context loss.
|
|
Bool persistentMapped = false;
|
|
void* persistentPtr = nullptr;
|
|
// The GL store behind `id` was created with glBufferStorageEXT and is
|
|
// therefore IMMUTABLE - glBufferData cannot respecify it and it must never be
|
|
// recycled through the size-keyed buffer pool. Tracked separately from
|
|
// persistentMapped because the two come apart: a glMapBufferRange that fails
|
|
// after its glBufferStorageEXT succeeded leaves immutable storage behind with
|
|
// no map, and a respecification then has to retire the id rather than hand it
|
|
// to glBufferData, which the driver would silently refuse.
|
|
Bool immutableStorage = false;
|
|
};
|
|
|
|
// Registered as the frontend's BufferBackendOps at backend init and on
|
|
// every MakeCurrent (the ES context can be destroyed and recreated, e.g.
|
|
// by the trace replayer's probe context).
|
|
void RegisterBufferBackendOps();
|
|
void UnregisterBufferBackendOps();
|
|
// The ES context died: unregister ops, invalidate all outstanding GL ids
|
|
// (they belonged to the dead context) and drop deferred deletes.
|
|
void OnBackendContextDestroyed();
|
|
|
|
// Get-or-create the backend resource and bring its storage up to date
|
|
// (creates the GL buffer, replays pending ops, pushes persistent-mapped
|
|
// ranges). Requires the ES context to be current. Returns nullptr only
|
|
// for null input.
|
|
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
|
// Existing resource or nullptr; performs no GL calls.
|
|
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
|
// True when EnsureBufferResource(frontend) would provably fall straight through
|
|
// every branch and do no work — i.e. `resource` is still the frontend's own
|
|
// resource, its id belongs to the live ES context, and either it is the
|
|
// zero-copy coherent persistent store (draw-time sync is a no-op by design) or
|
|
// the storage is initialized at the right size with no pending ops and a synced
|
|
// change serial while the buffer is not mapped (an active map may owe a
|
|
// per-draw persistent-range push, so it always takes the full path).
|
|
// `frontend` must be non-null and alive; the caller guarantees that by holding
|
|
// (or shadowing something that holds) a SharedPtr to it. Enables the per-VAO
|
|
// resolved-buffers memo to skip EnsureBufferResource on clean static buffers.
|
|
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource);
|
|
|
|
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
|
// thread without a current ES context). Called from draw-time sync.
|
|
void ProcessDeferredBufferReleases();
|
|
|
|
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
|
void BindBufferId(GLenum target, Uint id);
|
|
void InvalidateArrayBufferBindingCache();
|
|
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
|
|
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
|
|
// upload site routes its binding through these so the shadow always matches
|
|
// the driver; the resting state between operations is 0, which keeps any
|
|
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
|
|
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
|
|
// and a recycled name matching the shadow would false-skip the rebind) and
|
|
// invalidated on MakeCurrent (context may reset).
|
|
void BindPixelPackBufferId(Uint id);
|
|
void BindPixelUnpackBufferId(Uint id);
|
|
void InvalidatePixelBufferBindingCaches();
|
|
// A GL buffer id is being deleted by code outside BufferImpl (e.g. the VAO
|
|
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
|
// could false-skip when the name is recycled.
|
|
void NoteBufferIdDeleted(Uint id);
|
|
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
|
|
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
|
|
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
|
|
// key on FRONTEND versions, which a backend-side re-mint does not move - without
|
|
// this generation the driver VAO would keep fetching through the deleted id (or
|
|
// its retained store) forever. Compared and stamped by
|
|
// BackendVertexArrayObject::SyncToBackend.
|
|
extern Uint64 g_bufferBackendIdGeneration;
|
|
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
|
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
|
// (id, range) already at that index matches, like the array-buffer/texture/
|
|
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
|
|
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
|
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
|
void InvalidateIndexedBufferBindingCache();
|
|
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
|
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
|
// without glDeleteBuffers (called when the ES context is going away).
|
|
void TrimBufferPool();
|
|
void ClearBufferPool();
|
|
|
|
// --- Global-UBO ring ------------------------------------------------------
|
|
// One persistently+coherently mapped buffer (EXT_buffer_storage) shared by
|
|
// every program's lowered default-uniform block. Each content change is
|
|
// bump-allocated into a fresh slot and bound with glBindBufferRange, so the
|
|
// CPU never rewrites bytes the GPU may still be reading — the per-draw
|
|
// glBufferSubData into one static UBO forced Adreno to resolve that
|
|
// write-after-read hazard on every uniform-dirtying draw (MC dirties
|
|
// uniforms every draw). Reclamation rides the Present() frame-fence
|
|
// watermark; no ring bytes are recycled before their frame's GPU work
|
|
// completed.
|
|
//
|
|
// A program's cached slot, reusable within one frame while the frontend UBO
|
|
// content version is unchanged. Cross-frame reuse is intentionally not
|
|
// attempted: later same-frame allocations may recycle bytes of completed
|
|
// frames, so re-referencing them would need per-bind pinning — rewriting
|
|
// GetUBOSize() bytes once per program per frame is far cheaper.
|
|
struct UboRingAllocation {
|
|
Uint32 contentVersion = ~0u; // frontend UBO content version held at `offset`
|
|
Uint32 ringGeneration = 0; // ring identity the slot lives in (0 = never valid)
|
|
Uint64 frameSerial = ~Uint64{0}; // frame the slot was written in
|
|
SizeT offset = 0;
|
|
};
|
|
// False when the feature is disabled, EXT_buffer_storage / fences are
|
|
// missing, the ES context is not current, or ring creation already failed
|
|
// under this context (callers then take the legacy glBufferSubData path).
|
|
Bool UboRingAvailable();
|
|
// Bump-allocate `size` bytes aligned to GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT.
|
|
// Grows the ring (new GL store, generation bump) when the in-flight span
|
|
// would be overrun. Returns false when storage (re)creation fails.
|
|
Bool UboRingAllocate(SizeT size, SizeT& outOffset);
|
|
void* UboRingMappedPtr();
|
|
Uint UboRingBufferId();
|
|
Uint32 UboRingGeneration();
|
|
// Present()-time upkeep: records the frame's high-water mark for reclamation
|
|
// and deletes grown-away ring stores once the GPU is done with them.
|
|
void UboRingOnPresent();
|
|
} // namespace BufferImpl
|
|
|
|
namespace VertexArrayImpl {
|
|
class BackendVertexArrayObject {
|
|
public:
|
|
BackendVertexArrayObject();
|
|
~BackendVertexArrayObject();
|
|
void SyncToBackend(const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject);
|
|
void SyncClientSideAttributesForDrawArrays(
|
|
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
|
|
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
|
void Bind() const;
|
|
|
|
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
|
// distinct enabled-attribute buffers (deduped) and the index buffer, resolved
|
|
// to their backend resources once. Valid while the VAO's config version is
|
|
// unchanged — every attach/enable/disable/format mutation bumps it (the same
|
|
// invariant SyncToBackend's gate already leans on), and the VAO's attribute
|
|
// SharedPtrs pin each memoed frontend buffer for exactly that long, so the raw
|
|
// pointers cannot dangle on a hit. Per-buffer cleanliness is NOT memoed here:
|
|
// each hit re-checks IsBufferDrawClean (resource identity, context generation,
|
|
// pending ops, change serial) and falls back to EnsureBufferResource for just
|
|
// the dirty entries via their attribute index. The IBO entry is keyed on the
|
|
// slot's bound-object identity instead (its slot version is a wrapping Uint16
|
|
// and is not covered by the config version).
|
|
struct ResolvedDrawBuffers {
|
|
struct Entry {
|
|
MG_State::GLState::BufferObject* frontend = nullptr;
|
|
BufferImpl::GLESBufferResource* resource = nullptr;
|
|
Uint8 attribIndex = 0;
|
|
};
|
|
Bool valid = false;
|
|
Uint32 configVersion = 0;
|
|
Uint count = 0;
|
|
Array<Entry, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> entries;
|
|
MG_State::GLState::BufferObject* iboFrontend = nullptr;
|
|
BufferImpl::GLESBufferResource* iboResource = nullptr;
|
|
// Buffer-mutation epoch (BufferImpl::CurrentBufferMutationEpoch) at which
|
|
// the LAST probe pass found every entry / the IBO clean; 0 = not stamped
|
|
// (epochs start at 1). While a stamp matches the pre-pass epoch read, the
|
|
// probes are skipped outright: any path that can dirty ANY buffer bumps
|
|
// the epoch (the exhaustive site list lives at the epoch declaration).
|
|
// The IBO stamp is only trusted together with the bound-object identity
|
|
// compare - the VAO's index slot can rebind with no epoch or config move.
|
|
Uint64 vboCleanEpoch = 0;
|
|
Uint64 iboCleanEpoch = 0;
|
|
};
|
|
ResolvedDrawBuffers& GetResolvedDrawBuffersMemo() { return m_resolvedDrawBuffers; }
|
|
|
|
// Memo for SyncCurrentVertexAttributeValues: which of a program's ACTIVE
|
|
// attribute locations lack an enabled array in this VAO (those read the
|
|
// context's current generic value instead of a buffer). Keyed on the VAO
|
|
// config version (enable/disable bumps it) and the program's active-location
|
|
// mask. Hosted per twin — the former function-static single entry missed on
|
|
// every draw once the app cycled VAOs, re-reading the cold attribute slots.
|
|
struct PendingAttribValueMask {
|
|
Bool valid = false;
|
|
Uint32 configVersion = 0;
|
|
Uint32 activeMask = 0;
|
|
Uint32 pendingMask = 0;
|
|
};
|
|
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
|
|
|
private:
|
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
|
PendingAttribValueMask m_pendingAttribValueMask;
|
|
Uint m_backendVAOId = 0;
|
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
|
Bool m_isInitialized = false;
|
|
Uint16 m_syncedIndexBufferVersion = 0;
|
|
// Identity of the buffer the version above was stamped against. Raw and never
|
|
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
|
|
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
|
|
// alone would read a wrapped-back count with a different buffer bound as clean.
|
|
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
|
|
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
|
// version on every per-attribute version bump (the three Bump*Version functions are
|
|
// its only writers), so an unchanged config version proves every per-attribute
|
|
// compare in SyncToBackend would come up clean. The index-buffer slot has its own
|
|
// version and is NOT covered. The Bool (not a sentinel value) marks "never synced".
|
|
Bool m_hasSyncedConfigVersion = false;
|
|
Uint32 m_syncedConfigVersion = 0;
|
|
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
|
m_syncedAttributeVersions;
|
|
// Byte shift currently baked into the instanced arrays' offsets by the baseInstance
|
|
// emulation (see SetPendingFetchBaseInstance). It is draw state, not VAO state, so it
|
|
// is deliberately NOT covered by the config version: the frontend never bumps for it.
|
|
// Kept here because it describes what was last EMITTED, which is what the next sync
|
|
// has to correct.
|
|
Uint32 m_syncedFetchBaseInstance = 0;
|
|
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
|
|
// mismatch means some live buffer's driver id was re-minted since; the ids
|
|
// baked into the driver VAO's attribute/element bindings may be dead even
|
|
// though every frontend version matches, so the next sync re-emits them all.
|
|
Uint64 m_syncedBufferIdGeneration = 0;
|
|
};
|
|
|
|
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
|
g_backendVertexArrayObjects;
|
|
|
|
// Shadowed glBindVertexArray: every backend VAO bind goes through here so a
|
|
// draw's second bind of the same VAO (SyncToBackend, then PrepareForDraw's
|
|
// re-bind) reaches the driver once. Invalidate whenever the ES context is
|
|
// replaced - ids restart and the resting binding is 0 again.
|
|
void BindBackendVAOId(Uint id);
|
|
void InvalidateVAOBindingCache();
|
|
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
|
void NoteVAOIdDeleted(Uint id);
|
|
|
|
// baseInstance emulation for drivers without GL_EXT_base_instance. GL fetches an
|
|
// instanced array at element "floor(instance / divisor) + baseInstance", and ES has no
|
|
// way to say the "+ baseInstance" part - so it is folded into the attribute's own byte
|
|
// offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent.
|
|
// Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw
|
|
// so the next one refetches from element 0; ScopedFetchBaseInstance does both.
|
|
void SetPendingFetchBaseInstance(Uint32 baseInstance);
|
|
Uint32 GetPendingFetchBaseInstance();
|
|
|
|
class ScopedFetchBaseInstance {
|
|
public:
|
|
explicit ScopedFetchBaseInstance(Uint32 baseInstance) { SetPendingFetchBaseInstance(baseInstance); }
|
|
~ScopedFetchBaseInstance() { SetPendingFetchBaseInstance(0); }
|
|
ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete;
|
|
ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete;
|
|
};
|
|
} // namespace VertexArrayImpl
|
|
|
|
namespace TextureImpl {
|
|
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
|
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
|
|
(void)target;
|
|
return true;
|
|
}
|
|
|
|
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
|
|
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
|
|
// they are single-level and already clamp, so only the non-normalized coordinates differ.
|
|
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
|
|
// ShaderCompiler::LowerRectImages rewrites rectangle images (declining any module
|
|
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
|
|
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
|
switch (target) {
|
|
case TextureTarget::Texture1D:
|
|
case TextureTarget::TextureRectangle:
|
|
return TextureTarget::Texture2D;
|
|
case TextureTarget::Texture1DArray:
|
|
return TextureTarget::Texture2DArray;
|
|
default:
|
|
return target;
|
|
}
|
|
}
|
|
|
|
inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) {
|
|
return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target));
|
|
}
|
|
|
|
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
|
switch (uploadTarget) {
|
|
case TextureUploadTarget::Texture1D:
|
|
case TextureUploadTarget::TextureRectangle:
|
|
return GL_TEXTURE_2D;
|
|
case TextureUploadTarget::Texture1DArray:
|
|
return GL_TEXTURE_2D_ARRAY;
|
|
default:
|
|
return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
|
}
|
|
}
|
|
|
|
// 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and
|
|
// moves the layer count into depth.
|
|
inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) {
|
|
if (stateTarget == TextureTarget::Texture1DArray) {
|
|
return {texelSize.x(), 1, texelSize.y()};
|
|
}
|
|
return texelSize;
|
|
}
|
|
|
|
inline Bool IsMultisampleTextureTarget(TextureTarget target) {
|
|
return target == TextureTarget::Texture2DMultisample ||
|
|
target == TextureTarget::Texture2DMultisampleArray;
|
|
}
|
|
|
|
inline Bool SupportsWrapR(TextureTarget target) {
|
|
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
|
|
}
|
|
|
|
// Components per texel the frontend format's client data carries, for the three-channel
|
|
// formats that can be widened to a four-channel colour-renderable target; 0 for everything
|
|
// else. See PrepareChannelWidenedUpload.
|
|
Uint GetWidenableClientComponentCount(TextureInternalFormat format);
|
|
|
|
// True when a widenable format's components are integer rather than normalized, which is
|
|
// what decides the synthetic alpha's value: GL_RGB8I and GL_RGB8_SNORM are both uploaded
|
|
// as GL_BYTE, but their 1.0 is 1 and 0x7F respectively.
|
|
Bool IsIntegerWidenableFormat(TextureInternalFormat format);
|
|
|
|
// Repacks three-component client data as four components with an alpha of 1.0 in
|
|
// `uploadType`, for a format the backend widened to keep a colour attachment renderable.
|
|
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
|
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
|
// scratch buffer and has to outlive the returned pointer.
|
|
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
|
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
|
Bool integerData = false);
|
|
|
|
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
|
SizeT width = 0;
|
|
SizeT height = 0;
|
|
SizeT depth = 0;
|
|
SizeT mipmapLevels = 0;
|
|
Uint bufferExternalIndex = 0;
|
|
Int samples = 0;
|
|
Bool fixedSampleLocations = true;
|
|
|
|
bool operator==(const StateTextureBasicInfo& other) const {
|
|
return internalFormat == other.internalFormat && width == other.width && height == other.height &&
|
|
depth == other.depth && mipmapLevels == other.mipmapLevels &&
|
|
bufferExternalIndex == other.bufferExternalIndex && samples == other.samples &&
|
|
fixedSampleLocations == other.fixedSampleLocations;
|
|
}
|
|
|
|
bool operator!=(const StateTextureBasicInfo& other) const { return !(*this == other); }
|
|
};
|
|
|
|
inline const Uint TempTextureUnit = 0;
|
|
class BackendTextureObject {
|
|
public:
|
|
BackendTextureObject();
|
|
// Deletes the GL texture (frontend glDeleteTextures used to leak every
|
|
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
|
|
// shadows so a recycled name or heap address cannot false-skip a rebind.
|
|
~BackendTextureObject();
|
|
BackendTextureObject(const BackendTextureObject&) = delete;
|
|
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
|
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
|
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
|
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
|
void RequireImageBindableStorage();
|
|
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
|
Uint GetBackendTextureId() const;
|
|
|
|
// Aggregate first-level clean gate for the per-draw trio
|
|
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
|
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
|
// (params version == synced params version; builtin-sampler version ==
|
|
// synced sampler version; and SyncMipmapsToBackend's cheap gate - stamped
|
|
// trio + content version + Mipmap storage). True means each of the three
|
|
// would provably return without work, so the caller may skip the calls;
|
|
// false only falls through to the three calls, whose own gates re-decide
|
|
// individually - this gate must never be MORE permissive than they are.
|
|
// `contextId`/`samplingGeneration` are the frontend context's current
|
|
// values, hoisted by the caller so a per-draw list walk reads them once
|
|
// instead of per texture. `t` must be the live frontend texture.
|
|
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
|
Uint64 samplingGeneration) const {
|
|
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
|
m_syncedShapeGeneration != samplingGeneration) {
|
|
return false;
|
|
}
|
|
const Uint16 paramsVersion = t->GetTextureParamsVersion();
|
|
if (m_syncedShapeParamsVersion != paramsVersion || m_syncedTextureParamsVersion != paramsVersion) {
|
|
return false;
|
|
}
|
|
if (m_syncedContentVersion == 0 || m_syncedContentVersion != t->GetContentVersion()) {
|
|
return false;
|
|
}
|
|
const auto& samplerObject = t->GetSamplerObject();
|
|
if (!samplerObject || m_syncedSamplerVersion != samplerObject->GetVersion()) {
|
|
return false;
|
|
}
|
|
return t->GetStorageType() == TextureStorageType::Mipmap;
|
|
}
|
|
|
|
private:
|
|
void RecreateBackendTexture();
|
|
|
|
Uint m_backendTextureId = 0;
|
|
// ES context generation the id was created under; a dtor running after
|
|
// that context died must not delete a foreign (recycled) name.
|
|
Uint m_contextGeneration = 0;
|
|
Bool m_isInitialized = false;
|
|
Bool m_imageBindableStorageRequired = false;
|
|
Bool m_backendStorageImmutable = false;
|
|
// Latches the "this driver has no buffer textures" report to once per texture. The
|
|
// report is emitted from the respecify path, which bails before recording the state
|
|
// it was asked to apply - so without the latch the texture stays permanently dirty
|
|
// and every draw of every frame logs the same line.
|
|
Bool m_bufferTextureUnsupportedReported = false;
|
|
StateTextureBasicInfo m_prevTextureInfo;
|
|
// Frontend content version at the last completed mipmap sync. The per-draw
|
|
// clean probe compares this before rebuilding shape info and scanning
|
|
// per-level dirty flags; 0 never matches a real version (they start at 1).
|
|
Uint64 m_syncedContentVersion = 0;
|
|
// First-level clean gate for SyncMipmapsToBackend, checked before even the
|
|
// IsComplete()/shape-probe walk. Valid only as a trio with the content and
|
|
// texture-params versions: the context's sampling-resolution generation moves on
|
|
// EVERY texture-shape mutation (BumpShapeVersion is the only writer of shape and
|
|
// unconditionally bumps it), the content version on every CPU pixel mutation, and
|
|
// the params version covers SetSamples/SetFixedSampleLocations, which bump neither
|
|
// of the other two but feed the shape probe. The context id pins the generation to
|
|
// the context that produced it - generations restart at 0 with a new context, and a
|
|
// texture is owned by exactly one context (share groups are not implemented), so a
|
|
// mutation can never happen under a context this key does not name. 0 = never
|
|
// stamped (real context ids start at 1). Backend-side invalidation rides on
|
|
// m_isInitialized: RequireImageBindableStorage and RecreateBackendTexture clear it.
|
|
Uint64 m_syncedShapeContextId = 0;
|
|
Uint64 m_syncedShapeGeneration = 0;
|
|
Uint16 m_syncedShapeParamsVersion = 0;
|
|
SamplerParameters m_cacheSamplerParameters;
|
|
UintVec2 m_cacheLodRange = {0, 1000};
|
|
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
|
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
|
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
|
// texture that never asks for the stencil aspect never emits the call. The
|
|
// depth/stencil readback and replicate-blit emulations also write this parameter
|
|
// raw, but only ever on their own scratch textures (never on an application
|
|
// texture), so they cannot desynchronise this cache.
|
|
GLenum m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
|
Uint16 m_syncedSamplerVersion = 0;
|
|
Uint16 m_syncedTextureParamsVersion = 0;
|
|
// Set when the driver texture underneath was regenerated and has therefore lost every
|
|
// parameter already pushed onto it: the params-version early-out has to be overridden
|
|
// once, or an unchanged version would skip the re-push forever.
|
|
Bool m_forceTextureParamsResync = false;
|
|
};
|
|
|
|
void ActivateTextureUnit(Uint unit);
|
|
void UnbindTexture(Uint unit, GLenum target);
|
|
extern StateBackendObjectRegistry<MG_State::GLState::ITextureObject, BackendTextureObject>
|
|
g_backendTextureObjects;
|
|
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
|
Bool imageBindableStorageRequired = false);
|
|
// Brings every texture the next draw reads - the touched units' bindings and the draw
|
|
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
|
|
// documented at their definitions. Declared here so tests can drive those memos directly.
|
|
void SyncNeccessaryTextures();
|
|
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
|
g_boundTexturesCache;
|
|
extern Uint g_activeTextureUnit;
|
|
} // namespace TextureImpl
|
|
|
|
namespace FramebufferImpl {
|
|
class BackendFramebufferObject {
|
|
public:
|
|
BackendFramebufferObject();
|
|
// Deletes the driver framebuffer and scrubs the binding shadow. Without it every
|
|
// frontend glDeleteFramebuffers leaked one ES framebuffer for the process lifetime;
|
|
// an app that creates a framebuffer per readback (GL CTS packed_pixels does ~3300
|
|
// per case) walked the driver into hundreds of megabytes of dead framebuffers and
|
|
// out of the resources a later attachment needs.
|
|
~BackendFramebufferObject();
|
|
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
|
|
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
|
|
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
|
FramebufferTarget asTarget);
|
|
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
|
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
|
|
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
|
|
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
|
|
void InvalidateSyncedState();
|
|
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
|
|
void Bind(FramebufferTarget target) const;
|
|
// FramebufferAttachmentType GetCompactedAttachmentTypeAtDrawBufferIndex(Int index);
|
|
GLenum GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const;
|
|
|
|
private:
|
|
Uint m_backendFBOId = 0;
|
|
Uint m_contextGeneration = 0;
|
|
|
|
/* this will save buffers in its original form,
|
|
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
|
i.e. it could be like [COLOR_ATTACHMENT0, COLOR_ATTACHMENT5, NONE, COLOR_ATTACHMENT4]
|
|
Probably useful to re-link shader output according to this.
|
|
aka. realizing `glBindFragDataLocation`
|
|
*/
|
|
FramebufferAttachmentType m_frontendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {
|
|
FramebufferAttachmentType::None};
|
|
/* this will save buffers in stricter ES rules
|
|
reversion, absence or not consecutive are not allowed, according to ES spec
|
|
i.e. it could be like [COLOR_ATTACHMENT0, COLOR_ATTACHMENT1, NONE, COLOR_ATTACHMENT3, ...]
|
|
this array could be provided as data directly to ES `glDrawBuffers` function
|
|
*/
|
|
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
|
|
|
static constexpr Uint MAX_COLOR_ATTACHMENT_SLOTS =
|
|
static_cast<Uint>(FramebufferAttachmentType::Color31) -
|
|
static_cast<Uint>(FramebufferAttachmentType::Color0) + 1;
|
|
/* Where each frontend GL_COLOR_ATTACHMENTn image physically lives in the backend ES
|
|
framebuffer, as a GL_COLOR_ATTACHMENTm enum. ES only accepts glDrawBuffers bufs[s] ==
|
|
GL_COLOR_ATTACHMENTs, so a GL draw-buffer slot s naming attachment a forces a's image
|
|
under backend slot s. This table is the single owner of that decision and is kept a
|
|
PERMUTATION of the backend colour slots: every other attachment keeps its identity
|
|
slot when that slot survived, and is parked on the lowest free slot when it did not.
|
|
Deriving the point per-query from the draw-buffer array instead handed the identity
|
|
point to any attachment that was not a draw buffer - i.e. exactly the point a
|
|
relocated draw buffer had just taken over. The permutation is only true of the
|
|
PHYSICAL framebuffer because the attachment loop detaches a point whose frontend
|
|
owner is empty; do not remove that detach. */
|
|
GLenum m_backendColorSlots[MAX_COLOR_ATTACHMENT_SLOTS] = {GL_NONE};
|
|
/* Rebuild m_backendColorSlots from the frontend draw-buffer array. Returns true when any
|
|
attachment moved, i.e. when the physical attachments and the memoised read buffer have
|
|
to be re-applied. */
|
|
Bool RecomputeBackendColorSlots(
|
|
const MG_State::GLState::FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers);
|
|
|
|
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
|
|
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
|
|
|
|
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
|
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
|
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
|
|
// mismatch means some backend texture id was re-minted since, and any of this
|
|
// twin's attachment points may still hold the dead id even though the frontend
|
|
// attachment versions match - so the walk re-attaches everything first.
|
|
Uint64 m_syncedBackendIdGeneration = 0;
|
|
};
|
|
|
|
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
|
g_backendFramebufferObjects;
|
|
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
|
|
// backend actually stores in a floating-point format. GL clamps a read from a
|
|
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
|
|
// GL_FIXED_ONLY); the substituted float storage would not, so the readback path
|
|
// has to apply the clamp itself.
|
|
Bool IsFixedPointFallbackReadAttachment();
|
|
|
|
// True when the read buffer names a three-channel attachment the backend actually stores
|
|
// in a four-channel format (the colour-renderable widening). A format without alpha reads
|
|
// back as 1.0, so the readback path has to overwrite the alpha the draw left behind -
|
|
// unconditionally, since this is the format's own semantics rather than the
|
|
// GL_CLAMP_READ_COLOR rule the clamp above implements.
|
|
Bool IsAlphaWidenedFallbackReadAttachment();
|
|
|
|
// True when this attachment's storage carries an alpha channel its frontend format does
|
|
// not (the three-channel colour-renderable widening).
|
|
Bool IsAlphaWidenedColorAttachment(const MG_State::GLState::FramebufferAttachmentObject& attachmentObject);
|
|
|
|
// Bit i set = DRAW BUFFER i of `fbo` resolves to a colour attachment the backend widened
|
|
// from three channels to four. Indexed by draw-buffer slot, not by attachment point,
|
|
// because that is what glColorMaski / glClearBufferfv address.
|
|
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo);
|
|
|
|
// The same mask for whatever is currently bound to GL_DRAW_FRAMEBUFFER, recomputed by
|
|
// SyncCurrentFBO (BackendFramebufferObject::SyncToBackend for the DRAW target, and reset
|
|
// to 0 on the default framebuffer). Read by the draw/clear state sync, so it is only
|
|
// trustworthy after SyncCurrentFBO has run in the same entry point.
|
|
//
|
|
// WHY IT EXISTS (the dst-alpha discipline). A widened attachment has a real alpha channel
|
|
// the application's format does not, and GL says a missing channel reads as 1.0. Readback
|
|
// can paper over that (ForceWideReadAlphaToOne), but GL_DST_ALPHA /
|
|
// GL_ONE_MINUS_DST_ALPHA blending and glBlitFramebuffer read the STORED alpha inside the
|
|
// driver where no interception is possible. So the stored alpha is kept at 1.0 instead:
|
|
// a clear touching a widened buffer writes alpha 1.0, and every draw into it has its
|
|
// alpha write mask forced off, so nothing can ever move it again. The application's own
|
|
// colour mask is untouched - glGet(GL_COLOR_WRITEMASK) still reports what it set.
|
|
extern Uint32 g_alphaWidenedDrawBufferMask;
|
|
|
|
// Bit i set = DRAW BUFFER i of the framebuffer bound as DRAW resolves to a colour
|
|
// attachment with an INTEGER format. Recomputed beside the mask above and for its sake:
|
|
// glClearBufferfv on an integer colour buffer is GL_INVALID_OPERATION, so the
|
|
// per-draw-buffer clear route the widening needs has to stand down when one is present.
|
|
// (glClear on an integer colour buffer is left undefined by ES in the first place, and
|
|
// an application that wants a defined answer has to call glClearBufferuiv/iv - which does
|
|
// carry the widened alpha substitution.)
|
|
extern Uint32 g_integerColorDrawBufferMask;
|
|
|
|
// The colour a clear has to hand the driver for one draw buffer: the application's value,
|
|
// except that a widened attachment's alpha is replaced by the 1.0 its three-channel
|
|
// format implies. `one` is 1.0 encoded in the clear call's own component type - the
|
|
// integer clears carry the integer 1, the float clear carries 1.0f.
|
|
//
|
|
// Returns `value` itself when nothing is substituted, so the ordinary path allocates and
|
|
// copies nothing; `scratch` is the caller's buffer and has to outlive the returned
|
|
// pointer. Free of GL state on purpose, so the substitution can be unit-tested exactly as
|
|
// the driver sees it.
|
|
template <typename T>
|
|
const T* SubstituteWidenedClearAlpha(const T* value, Bool widened, T one, T (&scratch)[4]) {
|
|
if (!widened || value == nullptr) {
|
|
return value;
|
|
}
|
|
scratch[0] = value[0];
|
|
scratch[1] = value[1];
|
|
scratch[2] = value[2];
|
|
scratch[3] = one;
|
|
return scratch;
|
|
}
|
|
|
|
// What SyncCurrentFBO last pushed for each target, as a (binding, object, revision)
|
|
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
|
|
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
|
|
// only meaningful together - see SyncCurrentFBO.
|
|
//
|
|
// The binding slot's own version, which changes whenever a different object is bound
|
|
// to this target. Distinguishes a rebind from an in-place edit, and keeps the raw
|
|
// pointer below from matching an address the allocator recycled for a new FBO.
|
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSlotVersions;
|
|
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
|
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
|
// must re-sync it even when the binding-slot version has not moved.
|
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
|
// Which object was synced. Raw and never dereferenced: only compared for identity.
|
|
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
|
g_fboSyncedObjects;
|
|
|
|
// Bumped whenever a live backend texture's driver id is re-minted while its
|
|
// frontend texture may still be attached to application FBOs
|
|
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
|
|
// whose backend storage went immutable). The FBO twins' attachment memos key on
|
|
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
|
|
// the driver FBO would keep the deleted texture name attached forever. The
|
|
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
|
|
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
|
|
extern Uint64 g_attachmentBackendIdGeneration;
|
|
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
|
|
// target; part of the synced tuple above.
|
|
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
|
|
|
|
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
|
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
|
// save/restore the current binding without a glGetIntegerv round-trip (that
|
|
// query forces a driver pipeline sync) and so redundant rebinds no-op.
|
|
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
|
|
// the driver once. Invalidated on MakeCurrent (context may reset).
|
|
// GL_FRAMEBUFFER binds both targets.
|
|
void BindFramebufferId(GLenum fbTarget, Uint id);
|
|
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
|
void InvalidateFramebufferBindingCache();
|
|
// A driver framebuffer id is about to be deleted: ES reverts every target that
|
|
// currently binds it to 0, so the binding shadow has to follow or the next
|
|
// BindFramebufferId(0) would be deduped away and leave the deleted name bound.
|
|
void NoteFramebufferIdDeleted(Uint id);
|
|
} // namespace FramebufferImpl
|
|
|
|
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
|
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
|
|
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
|
|
// texture) is detached exactly when a later use of another aspect would
|
|
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
|
|
// incomplete and silently degraded later readbacks).
|
|
namespace ScratchFBOImpl {
|
|
struct ScratchFramebuffer {
|
|
Uint id = 0;
|
|
// false => attachment state unknown; scrub every point on next use.
|
|
// A fresh FBO starts with nothing attached, so creation sets it true.
|
|
Bool attachmentsKnown = false;
|
|
Uint colorTex = 0;
|
|
GLenum colorTarget = 0;
|
|
GLint colorLevel = 0;
|
|
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
|
|
Uint depthTex = 0;
|
|
GLenum depthTarget = 0;
|
|
GLint depthLevel = 0;
|
|
Bool depthHasStencil = false;
|
|
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
|
|
GLenum readBuffer = 0;
|
|
GLenum drawBuffer = 0;
|
|
};
|
|
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
|
|
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
|
|
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
|
|
// Returns the GL id, generating it if needed (requires a current ES context).
|
|
Uint EnsureId(ScratchFramebuffer& fb);
|
|
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
|
|
// target the READ/DRAW binding respectively). Each Ensure* performs the
|
|
// minimal detach/attach set and keeps the shadow in sync; a failed attach
|
|
// records the point as detached so the completeness check fails instead of
|
|
// silently reading a stale attachment.
|
|
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
|
|
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
|
|
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
|
Bool withStencil);
|
|
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
|
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
|
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
|
|
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
|
|
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
|
|
// binding while respecifying texture storage). Attachment is set once at
|
|
// creation and never changes.
|
|
Uint EnsureCompleteTinyFramebufferId();
|
|
// A backend texture id is being deleted or respecified: a scratch FBO still
|
|
// referencing it would hold a dangling attachment (ES only auto-detaches
|
|
// from the *bound* framebuffer), and a recycled name could false-skip a
|
|
// re-attach; force a full scrub on next use.
|
|
void NoteTextureIdDeleted(Uint textureId);
|
|
// The ES context (and the scratch FBO ids with it) is going away.
|
|
void OnBackendContextDestroyed();
|
|
} // namespace ScratchFBOImpl
|
|
|
|
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
|
|
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
|
|
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
|
|
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
|
|
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
|
|
// pins the driver to the shadow by writing all fields once. Invalidated on
|
|
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
|
|
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
|
|
// frontend context state instead.
|
|
namespace PixelStoreImpl {
|
|
struct PackState {
|
|
GLint Alignment = 4;
|
|
GLint RowLength = 0;
|
|
GLint SkipRows = 0;
|
|
GLint SkipPixels = 0;
|
|
Bool operator==(const PackState& o) const {
|
|
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
|
|
SkipPixels == o.SkipPixels;
|
|
}
|
|
};
|
|
void ApplyPackState(const PackState& desired);
|
|
PackState CurrentPackState();
|
|
void InvalidatePackStateCache();
|
|
} // namespace PixelStoreImpl
|
|
|
|
namespace SamplerImpl {
|
|
class BackendSamplerObject; // for PrgramImpl's sampler-pass memo rows below
|
|
}
|
|
|
|
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
|
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
|
// assigned through glUniform1i.
|
|
inline Bool IsImageUniformType(GLenum type) {
|
|
switch (type) {
|
|
case 0x904D: /*GL_IMAGE_2D*/
|
|
case 0x904E: /*GL_IMAGE_3D*/
|
|
case 0x9050: /*GL_IMAGE_CUBE*/
|
|
case 0x9051: /*GL_IMAGE_BUFFER*/
|
|
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
|
case 0x9058: /*GL_INT_IMAGE_2D*/
|
|
case 0x9059: /*GL_INT_IMAGE_3D*/
|
|
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
|
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
|
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
|
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
|
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
|
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
|
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
|
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
namespace PrgramImpl {
|
|
class BackendProgramObjectImpl {
|
|
public:
|
|
// Per-link cache of a sampler-style uniform's backend location: built once in
|
|
// SyncToBackend so draws stop issuing glGetUniformLocation string queries.
|
|
// lastAssignedUnit mirrors the program-state value set through glUniform1i
|
|
// (program state persists across binds, so caching per program is exact).
|
|
struct SamplerUniformBinding {
|
|
Uint frontendLocation = 0;
|
|
Int backendLocation = -1;
|
|
GLenum uniformType = 0;
|
|
Int lastAssignedUnit = -1;
|
|
// Location of this sampler's emulated GL_TEXTURE_LOD_BIAS uniform
|
|
// (PrgramImpl::EmulateTextureLodBias), -1 when the shader has none.
|
|
// lastAssignedLodBias mirrors the value the program currently holds,
|
|
// so an unbiased shader issues no per-draw glUniform1f at all.
|
|
Int lodBiasLocation = -1;
|
|
Float lastAssignedLodBias = 0.0f;
|
|
};
|
|
|
|
// Memo of the whole per-draw sampler-uniform pass (glUniform1i unit
|
|
// assignments, lod-bias uniform, raw-depth-fetch substitution and the
|
|
// per-unit sampler-object binds) in BindCurrentProgramWithResources.
|
|
// The pass is a pure function of the keys below, and its only driver-side
|
|
// effect is the sampler binding of each sampled unit, so replaying it as
|
|
// "do nothing" additionally requires those bindings to still be on the
|
|
// driver - the per-entry row compare against g_boundSamplersCache (the
|
|
// shadow every sampler bind in this backend already routes through).
|
|
//
|
|
// Invalidation enumeration:
|
|
// * sampler-uniform unit assignment (glUniform1i) and uniform-block
|
|
// binding edits -> frontend backendStateVersion;
|
|
// * any texture/sampler bind moving on any unit (incl. the high-water
|
|
// mark moving) -> unitBindingsEpoch;
|
|
// * any sampler parameter (incl. lod bias, compare mode) or texture
|
|
// shape/format change -> samplingGeneration;
|
|
// * another frontend context -> contextId (never-reused id);
|
|
// * ES context recreation -> textureContextGeneration;
|
|
// * relink / backend program rebuild -> SyncToBackend resets `valid`
|
|
// (it rebuilds m_samplerUniformBindings, whose lastAssignedUnit /
|
|
// lastAssignedLodBias dedup state this memo leans on);
|
|
// * any other writer moving a sampled unit's sampler binding
|
|
// (BindCurrentUnitSamplers on a unit-sampler change, scratch binds)
|
|
// -> the row snapshot compare.
|
|
struct SamplerPassMemo {
|
|
static constexpr SizeT kMaxEntries = 16;
|
|
Bool valid = false;
|
|
Uint8 count = 0;
|
|
Uint64 contextId = 0;
|
|
Uint64 unitBindingsEpoch = 0;
|
|
Uint64 samplingGeneration = 0;
|
|
Uint32 backendStateVersion = 0;
|
|
Uint textureContextGeneration = 0;
|
|
Array<Uint8, kMaxEntries> units{};
|
|
Array<SamplerImpl::BackendSamplerObject*, kMaxEntries> rows{};
|
|
};
|
|
|
|
BackendProgramObjectImpl();
|
|
~BackendProgramObjectImpl();
|
|
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
|
void Use() const;
|
|
void SetBaseInstance(Uint32 baseInstance) const;
|
|
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
|
void SetDrawID(Uint32 drawId) const;
|
|
void SetBaseVertex(Int32 baseVertex) const;
|
|
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
|
// actually reaches a shader read rather than being discarded.
|
|
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
|
|
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
|
// uniform write, and only such a program needs the reset after one.
|
|
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
|
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
|
// False when the last SyncToBackend could not produce a usable program (a
|
|
// shader failed to transpile or compile, or the link itself failed). Use()
|
|
// must not leave the previously bound program current in that case.
|
|
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
|
|
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
|
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
|
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
|
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
|
// Signature of the glShaderStorageBlockBinding override set the generated ESSL was
|
|
// transpiled against (ES can only express a storage-block binding as the declared
|
|
// qualifier, so the overrides are baked into the source). A mismatch means the
|
|
// program is stale exactly like the clamp masks above.
|
|
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
|
|
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
|
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
|
Vector<SamplerUniformBinding>& GetSamplerUniformBindings() { return m_samplerUniformBindings; }
|
|
Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; }
|
|
void SetLastUploadedGlobalUboVersion(Uint32 version) { m_lastUploadedGlobalUboVersion = version; }
|
|
// Backend-reported GL_UNIFORM_BLOCK_DATA_SIZE of the global block; ring
|
|
// bindings must span at least this much (may exceed the frontend's
|
|
// reflected size when the transpiled block pads differently).
|
|
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
|
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
|
SamplerPassMemo& GetSamplerPassMemo() { return m_samplerPassMemo; }
|
|
// Frontend link version this backend program (and its resource caches) was
|
|
// built from; a mismatch means every link-derived cache here is stale.
|
|
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
|
// Image-uniform unit generation this backend program was GENERATED against.
|
|
// Separate from the link version because it is not link state: ES forbids
|
|
// glUniform1i on an image uniform, so RebindImageUniformsToFrontendUnits bakes the
|
|
// unit into the ESSL, and a program built before glUniform1i moved that unit is as
|
|
// stale as one built before a relink - while the sampler half, which really is
|
|
// re-issued per draw, needs nothing of the sort.
|
|
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
|
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
|
// resolve to are still the ones its ESSL was generated against.
|
|
//
|
|
// A fourth condition of the same family as the three above, and the only one that
|
|
// reads live state rather than a program-side counter, because that is where the
|
|
// dependency actually is. GLSL ES requires a format layout qualifier on every image
|
|
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
|
|
// qualifier is whatever glBindImageTexture named - so a declaration with no format
|
|
// is compiled against the BINDING, and a rebind to a different format makes the
|
|
// built program wrong. Keyed on the units the program's own images address (cached
|
|
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
|
|
// version above and forces a re-sync anyway), so the cost on a program with no
|
|
// format-less image - which is all but a handful - is one empty-vector test.
|
|
//
|
|
// Deliberately NOT reached from glBindImageTexture: that entry point must never
|
|
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
|
|
// state and this comparison notices at the next Prepare, which is also what makes
|
|
// an image first bound AFTER link work.
|
|
Bool ImageUnitFormatsStillMatch() const;
|
|
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
|
|
// image-unit state. 0 when the program has no format-less image uniform.
|
|
Uint64 ComputeImageUnitFormatSignature() const;
|
|
|
|
private:
|
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
|
|
|
Uint m_backendProgramId = 0;
|
|
// GL name of the frontend program this was last synced from; diagnostics only, so
|
|
// an unusable backend program can be traced back to the glCreateProgram id the app
|
|
// knows it by.
|
|
Uint m_frontendProgramId = 0;
|
|
Uint m_backendGlobalUBOId = 0;
|
|
Int m_baseInstanceUniformLocation = -1;
|
|
Int m_drawIdUniformLocation = -1;
|
|
Int m_baseVertexUniformLocation = -1;
|
|
Int m_baseInstanceWordIndexUniformLocation = -1;
|
|
Int m_indirectParamsBinding = -1;
|
|
Uint32 m_snormFallbackClampOutputMask = 0;
|
|
Uint32 m_unormFallbackClampOutputMask = 0;
|
|
// Draw buffers a legacy gl_FragColor write has to reach (see
|
|
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
|
Uint m_fragColorBroadcastCount = 1;
|
|
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
|
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
|
Bool m_isInitialized = false;
|
|
Bool m_backendProgramUsable = false;
|
|
|
|
Int m_globalUboBackendBlockIndex = -1;
|
|
Int m_globalUboBackendBlockSize = 0;
|
|
Vector<Int> m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent)
|
|
Vector<SamplerUniformBinding> m_samplerUniformBindings;
|
|
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
|
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
|
Uint32 m_syncedLinkVersion = ~0u;
|
|
Uint32 m_syncedImageUnitVersion = ~0u;
|
|
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
|
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
|
// that declares a format on all of its images, which is the overwhelming majority -
|
|
// and what keeps the per-draw comparison free for them.
|
|
Vector<Int> m_formatlessImageUnits;
|
|
Uint64 m_imageUnitFormatSignature = 0;
|
|
SamplerPassMemo m_samplerPassMemo;
|
|
};
|
|
|
|
extern Uint32 g_snormFallbackClampOutputMask;
|
|
extern Uint32 g_unormFallbackClampOutputMask;
|
|
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
|
|
// is framebuffer state that the shader has to be compiled against, so a program
|
|
// whose snapshot no longer matches is relinked.
|
|
extern Uint g_fragColorBroadcastCount;
|
|
// Backend id of the last glUseProgram issued through this backend; lets Use()
|
|
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
|
// ES context is recreated.
|
|
extern Uint g_lastUsedBackendProgramId;
|
|
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
|
g_backendProgramObjects;
|
|
|
|
// Points one shader storage block of an ALREADY-LINKED backend program at
|
|
// `binding`. `blockName` is the frontend interface-query spelling; the real
|
|
// driver's own index for it is looked up here, because the transpiled ESSL's
|
|
// block order is not the frontend's. Returns false when the block does not exist
|
|
// on the backend program (eliminated as unused, or the driver lacks the entry
|
|
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
|
// record either way.
|
|
//
|
|
// NOT how a rebinding reaches the shader. glShaderStorageBlockBinding has no ES
|
|
// equivalent and is absent from every real ES driver, so this is a no-op there;
|
|
// SyncToBackend bakes the effective binding into the ESSL it generates instead
|
|
// (SpvcSession::SetShaderStorageBlockBinding). This is kept as the cheaper path on
|
|
// a driver that does happen to expose the entry point.
|
|
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
|
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
|
// program that was just built - best effort, on the same "only where the driver has
|
|
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
|
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
|
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
|
// Order-independent digest of the program's glShaderStorageBlockBinding overrides.
|
|
// The generated ESSL carries them (ES has no way to move a storage block's binding
|
|
// after link), so a program built against a different set is stale and the draw path
|
|
// has to rebuild it. Computed from the values, so re-setting a block to the binding it
|
|
// already has costs nothing. 0 when nothing was ever rebound.
|
|
Uint64 ComputeShaderStorageBlockBindingSignature(
|
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
|
|
|
// Everything the image-format bake needs from one walk of a program's uniform
|
|
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
|
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
|
|
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
|
|
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
|
|
// on it.
|
|
struct ImageFormatBakeInputs {
|
|
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
|
|
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
|
|
// one is authoritative and is never overridden.
|
|
UnorderedMap<String, Uint> glFormatByUniformName;
|
|
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
|
|
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
|
|
// write into the emitted declaration instead. Disjoint from the map above by
|
|
// construction: a format is baked into the module or completed in the text, never
|
|
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
|
|
UnorderedMap<String, String> esslFormatQualifierByUniformName;
|
|
// Units those uniforms address, kept so the draw path can re-read their formats
|
|
// without walking the reflection again.
|
|
Vector<Int> units;
|
|
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
|
|
// image uniform, which is all but a handful.
|
|
Uint64 signature = 0;
|
|
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
|
|
// declaration carries one qualifier, so there is nothing correct to bake and they
|
|
// are dropped from the map above. Kept for diagnostics.
|
|
Vector<String> conflictedNames;
|
|
// Some format in play - declared or baked - is outside the GLSL ES core image
|
|
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
|
Bool needsExtendedImageFormats = false;
|
|
};
|
|
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
|
} // namespace PrgramImpl
|
|
|
|
namespace SamplerImpl {
|
|
class BackendSamplerObject {
|
|
public:
|
|
BackendSamplerObject();
|
|
// Deletes the driver sampler and clears the units whose binding shadow still names
|
|
// this twin (a recycled heap address would otherwise false-skip a later Bind).
|
|
// Frontend glDeleteSamplers used to leak the backend id for the process lifetime.
|
|
~BackendSamplerObject();
|
|
BackendSamplerObject(const BackendSamplerObject&) = delete;
|
|
BackendSamplerObject& operator=(const BackendSamplerObject&) = delete;
|
|
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
|
void Bind(Uint unit);
|
|
Uint GetBackendSamplerId() const;
|
|
|
|
private:
|
|
Uint m_backendSamplerId = 0;
|
|
Uint m_contextGeneration = 0;
|
|
Bool m_isInitialized = false;
|
|
SamplerParameters m_cacheSamplerParameters;
|
|
Uint16 m_syncedSamplerVersion = 0;
|
|
};
|
|
|
|
void UnbindSampler(Uint unit);
|
|
|
|
extern Array<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
|
g_boundSamplersCache;
|
|
extern StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject>
|
|
g_backendSamplerObjects;
|
|
} // namespace SamplerImpl
|
|
|
|
namespace RenderbufferImpl {
|
|
class BackendRenderbufferObject {
|
|
public:
|
|
BackendRenderbufferObject();
|
|
// Deletes the driver renderbuffer; frontend glDeleteRenderbuffers used to leak it
|
|
// (with its whole image allocation) for the process lifetime.
|
|
~BackendRenderbufferObject();
|
|
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
|
|
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
|
|
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
|
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
|
void Bind() const;
|
|
|
|
private:
|
|
Uint m_backendRBOId = 0;
|
|
Uint m_contextGeneration = 0;
|
|
Bool m_isInitialized = false;
|
|
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
|
Int m_cacheWidth = 0;
|
|
Int m_cacheHeight = 0;
|
|
Int m_cacheSamples = 0;
|
|
};
|
|
|
|
extern StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject>
|
|
g_backendRenderbufferObjects;
|
|
} // namespace RenderbufferImpl
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|