mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 05:08:31 +09:00
1192 lines
69 KiB
C++
1192 lines
69 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 emulation uniforms. Both are
|
|
// no-ops when the program does not read the corresponding builtin.
|
|
void SetCurrentBaseInstance(Uint32 baseInstance);
|
|
void SetCurrentDrawID(Uint32 drawId);
|
|
// 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();
|
|
|
|
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.
|
|
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;
|
|
};
|
|
|
|
// 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);
|
|
// 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;
|
|
// 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;
|
|
};
|
|
|
|
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);
|
|
} // 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;
|
|
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};
|
|
Uint16 m_syncedSamplerVersion = 0;
|
|
Uint16 m_syncedTextureParamsVersion = 0;
|
|
};
|
|
|
|
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};
|
|
};
|
|
|
|
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;
|
|
|
|
// 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;
|
|
// 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; }
|
|
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; }
|
|
|
|
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_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;
|
|
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);
|
|
} // 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
|