Files
MobileGL/MobileGL/MG_Backend/DirectGLES/Managers.cpp
T

16208 lines
1.0 MiB
Plaintext

// MobileGL - MobileGL/MG_Backend/DirectGLES/Managers.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "Managers.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
// P3a: the handle-shaped resource op table, the applier's records and the reverse channel.
// Both headers are compiled into the library only under push, so they are included here
// under the same condition - a pull build must gain no declaration it cannot link.
#include <MG_Pipe/MGPipeCallbacks.h>
#include <MG_Pipe/MGPipeHostSpan.h>
#include <MG_Pipe/PipeApply.h>
// P5e (pg): the DEFINITION of the archive MGPipeShaderCsoRecord holds by SharedPtr. PipeApply.h
// needs only the name - the deleter is captured where the archive is constructed - but the
// program build reads through it, so the one translation unit that does needs the whole type.
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for
// why GLESBufferResource does not simply gain a member.
#include <MG_Remote/Server/StagedShadow.h>
#include <MG_Remote/Server/StagedTextureStore.h>
#include <MG_Remote/Server/ServerLoop.h>
// P5c (ct): object_death's producer (CONTRACT-P5C.md §5.2) - the death notice's split arm
// emits the record through the client's emit helper instead of hopping a stack struct to the
// apply thread.
#include <MG_Remote/Client/WireTables.h>
// P11 B2 (T0): the client's AHardwareBuffer store - its reference counting and, on Android, the
// allocation the POST self-test makes.
#include <MG_Remote/Transport/AdoptT0.h>
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#endif
#include "Utils.h"
#include "DirectGLES.h"
#include "BackendObject_DirectGLES.h"
#include <Config.h>
#include <MG_State/GLState/StateObjectDeathNotice.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/FramebufferEnumConverter.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Util/Converters/GLToMG/FramebufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <map>
#include <mutex>
#include <cstring>
#include <regex>
#include "ContextEpoch.h"
namespace MobileGL::MG_Backend::DirectGLES {
Uint g_backendContextGeneration = 1;
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
constexpr const char* BASE_VERTEX_UNIFORM_NAME = "mg_BaseVertex";
constexpr const char* BASE_INSTANCE_LOWERED_NAME = "mg_BaseInstanceLowered";
constexpr const char* BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME = "mg_BaseInstanceWordIndex";
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
// See the block comment on ForEachViewportRoutingPass in Managers.h. Auto is ON, including on
// a driver that advertises GL_OES_viewport_array: that extension gives the shader a name, not
// the driver fifteen more rectangles to rasterize against, and nothing in MobileGL has ever
// programmed the indexed state it would need.
Bool ViewportArrayEmulationEnabled() {
return MG_Config::Features.EsprytViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
}
Bool g_anyProgramRoutesViewportIndex = false;
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
// points. Those slots are taken from the TOP of the range downwards - below the one
// mg_IndirectParams already reserves - so an application binding its own SSBOs from 0 upwards
// never meets them, and the slot for GL binding N is `this - N` in every stage of the
// program without any shared state. Negative when the driver has no room left at all.
static Int AtomicCounterEsslBindingTop() {
return g_GLESCapabilities.MaxShaderStorageBufferBindings - 2;
}
static Bool IsAngleLlvmpipeRenderer() {
return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
}
static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() {
// IsAngleLlvmpipeRenderer combined with the
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle,
// both resolved in FillInGLESCapabilities.
return g_GLESCapabilities.AvoidSamplerMipmapMinFilter;
}
static Bool ShouldAvoidExplicitLodBiasOnAngleLlvmpipe() {
// IsAngleLlvmpipeRenderer combined with the MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS
// feature toggle, both resolved in FillInGLESCapabilities.
return g_GLESCapabilities.AvoidExplicitLodBias;
}
static GLenum ResolveBackendMinFilter(const SamplerParameters& samplerParams,
Bool avoidMipmapMinFilter) {
GLenum filter = MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.minFilter,
samplerParams.mipmapMode);
if (!avoidMipmapMinFilter) {
return filter;
}
switch (filter) {
case GL_NEAREST_MIPMAP_NEAREST:
case GL_NEAREST_MIPMAP_LINEAR:
return GL_NEAREST;
case GL_LINEAR_MIPMAP_NEAREST:
case GL_LINEAR_MIPMAP_LINEAR:
return GL_LINEAR;
default:
return filter;
}
}
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so this is the floor MobileGL
// advertises whatever the ES driver reports. It steers ClampMultisampleFetchesForEssl
// AND is part of the L2 translation-memo key, so the transpile and the key must read
// the same constant - hence one definition rather than two locals.
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): this is
// backend code and must not reach into the GL frontend. 4 is that translation unit's
// kFrontendMaxSamples, which is the source of truth - keep the two in step.
constexpr Int kFrontendMaxSamples = 4;
static Uint ResolveBackendEsslVersion() {
const auto& version = g_GLESCapabilities.GLESVersion;
if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) {
return 320;
}
if (version.Major == 3 && version.Minor >= 1) {
return 310;
}
return 300;
}
String ReplaceIdentifier(String source, const String& from, const String& to) {
SizeT pos = 0;
while ((pos = source.find(from, pos)) != String::npos) {
const Bool leftIsIdent = pos > 0 &&
(std::isalnum(static_cast<unsigned char>(source[pos - 1])) || source[pos - 1] == '_');
const SizeT end = pos + from.size();
const Bool rightIsIdent = end < source.size() &&
(std::isalnum(static_cast<unsigned char>(source[end])) || source[end] == '_');
if (!leftIsIdent && !rightIsIdent) {
source.replace(pos, from.size(), to);
pos += to.size();
} else {
pos = end;
}
}
return source;
}
String InjectUniformAfterVersion(String source, const String& declaration) {
const SizeT versionPos = source.find("#version");
if (versionPos == String::npos) {
return declaration + "\n" + source;
}
const SizeT lineEnd = source.find('\n', versionPos);
if (lineEnd == String::npos) {
return source + "\n" + declaration + "\n";
}
source.insert(lineEnd + 1, declaration + "\n");
return source;
}
namespace {
Bool g_processTeardown = false;
std::once_flag g_teardownSentinelOnce;
#if MOBILEGL_BUILD_DISAGGREGATED
// P12: raised only while DropEveryTwinForEndedServerSession empties the twin tables, on the
// apply thread that is the only reader of the twins (Managers.h).
Bool g_serverSessionTwinTeardown = false;
#endif
} // namespace
#if MOBILEGL_BUILD_DISAGGREGATED
Bool InProcessTeardown() { return g_processTeardown || g_serverSessionTwinTeardown; }
#else
Bool InProcessTeardown() { return g_processTeardown; }
#endif
Bool InProcessExit() { return g_processTeardown; }
void EnsureProcessTeardownSentinel() {
std::call_once(g_teardownSentinelOnce,
[] { std::atexit(+[] { g_processTeardown = true; }); });
}
namespace {
// P2 step e2's dispatcher: the frontend told us an object died, so free its slot and
// drop its twin NOW. One entry point for all six kinds, because the answer is the same
// for all six - which is why the notice carries the kind rather than there being six
// ops tables.
//
// Each arm below names the registry GLOBAL of its kind, but DestroyByLifetimeId is
// static: it is answered by every table of that kind that exists at the moment - the
// global's own and any by-value copy a fixture or a context reset is holding - and
// the slot goes back once, after all of them have let go (SlotTables.h, the holder
// list). Naming one instance here is a spelling, not a choice of holder.
//
// A notice that arrives after exit() has begun is dropped: past that point the twin's
// destructor must not call into the driver (see InProcessTeardown()), and the process
// is about to hand every GPU object back anyway. That twin is a deliberate leak, not
// garbage for a later collection - there is none on this arm.
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
if (InProcessTeardown()) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (ct), CONTRACT-P5C.md §5.2: with an active transport the death crosses AS A
// RECORD - object_death, the framebuffer family's first wire delete opcode. The GL
// thread resolves the dying object's handle in its OWN allocator inside
// EmitObjectDeathRecord: no handle means the server never saw the object and
// NOTHING crosses (which replaces the mailbox's unconditional delivery), and a
// handle means the record's EmitAndWait orders the death against in-flight verbs
// that name it - the one property the blocking control-channel hop provided and
// the only one P5c keeps. The sink (ServerVerbSink::OnObjectDeath) releases the
// kind's twin table by handle on the apply thread, so neither a lifetime-id probe
// nor the client's allocator is touched from there any more.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
// P5e (id), CONTRACT-P5E §4.4: THE RECORD IS THE ONLY DELIVERY NOW. What used
// to sit here was a mailbox hop for the ONE shape the record cannot carry - a
// transport configured with no client session at all, i.e. a ServerLoop
// fixture driving a server role with no client, never a real split (there a
// session exists whenever the server does). It posted a stack struct to the
// apply thread, which re-entered this function and ran the switch below; that
// switch's DestroyByLifetimeId probes and FREES in the CLIENT'S allocator from
// the apply thread, which is precisely the read P5e is retiring - and it is
// not a read a barrier could ever make safe, because under run-ahead there is
// no wait behind which the allocator stands still.
//
// WHAT REPLACES IT: nothing, and that is the answer rather than an omission. A
// fixture with no client session has no client allocator worth the name and no
// handle ever crossed, so there is no twin the server built from a record; its
// twins die with the backend at Stop (InProcessTeardown / the registry's own
// destruction), which is what already happened for every death raised past
// `Running() == false`. ID-96 enumerates the fixtures this changed and what
// each one drives instead; every one of them now drives `object_death` by
// handle through the applier, which is the delivery a real split uses.
// ID-96, ANSWERED BY MEASUREMENT: the set of fixtures that lose death delivery
// here is EMPTY. A probe that aborted in exactly this branch (NoSession with a
// RUNNING server loop, i.e. the only shape the deleted hop ever fired for) was
// run over the whole unit lane (2204 entries) and the whole integration-split
// lane (111) and never fired once. The one fixture family that could reach it -
// ServerLoopTest.cpp's ServerFixture / EglServerFixture, the only server role in
// the tree with no client session - has exactly one case that lets a re-keyed
// frontend object die off the apply thread
// (ServerLoopEglTest.FrontendFramebufferDeathDeletesOnTheContextOwner), and that
// case is already refused at its FIRST step by P5c's allocator guard, so its
// `framebuffer.reset()` was unreachable at 2fde7034 too.
(void)MG_Remote::Client::EmitObjectDeathRecord(kind, lifetimeId);
return;
}
// Falling through HERE under a transport means the death was raised ON the apply
// thread - the server backend destroying a frontend object it created itself
// (Magma's hidden blit/depth-mipmap resources). Those sites carry their own named
// scope (MagmaP7AllocatorDebtScope), so the allocator probe below is admitted
// there and refused everywhere else; this function no longer opens a scope of its
// own, because the one arm that needed it is the mailbox hop that just went.
#endif
switch (kind) {
case MG_Pipe::MGPipeKind::Texture:
TextureImpl::g_backendTextureObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::Framebuffer:
FramebufferImpl::g_backendFramebufferObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::Renderbuffer:
RenderbufferImpl::g_backendRenderbufferObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::SamplerCso:
SamplerImpl::g_backendSamplerObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::ShaderCso:
PrgramImpl::g_backendProgramObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::SamplerViewCso:
// P4a (D-I1, D5): the sixth kind, and the only one whose notice names a
// lifetime id that belongs to ANOTHER object - a sampler view is minted off its
// texture's id, one per ITextureObject, so ~TextureObjectBase raises this arm
// and the texture arm above from the same id. That is legal precisely because
// the two kinds have separate slot spaces, and it is why the two arms are
// written out separately rather than folded: the allocator resolves
// (kind, lifetimeId), so each finds its own slot or nothing.
//
// IDEMPOTENT, like every arm here. This is the REDUNDANT SECOND PATH: the
// client's MGPipeEmitSamplerViewCsoDestroyAndFree emits delete_sampler_view,
// raises this notice and frees the slot, in that order. Whichever of the two
// frees first wins; the other resolves nothing, because the allocator erases
// its lifetimeId -> slot mapping on Free and OnFrontendObjectDestroyed answers
// false for a handle it cannot resolve. This twin owns no driver id at all, so
// even a double release is a pointer reset.
SamplerViewImpl::BackendSamplerViewTable::OnFrontendObjectDestroyed(lifetimeId);
break;
case MG_Pipe::MGPipeKind::VertexElementsCso:
// P3a C-1: this is now the SECOND path, not the only one. The client speaks the
// whole death itself (MGPipeEmitVertexElementsDestroyAndFree: delete the
// applier record, raise this notice, free the slot), because the slot is minted
// client-side on every backend and a backend that installs no death ops - which
// Magma did not until P7 wave 2 package C, and whose table even now only EMITS
// the wire record - otherwise leaked the slot and the record per VAO for the
// life of the process. What is left here is the one thing only this
// side can do: drop the driver VAO the twin owns. It is raised while the handle
// still resolves, so OnFrontendObjectDestroyed's shared free (which the other
// five kinds still depend on) is simply the one that gets there first; the
// client's own Free right after it is then a no-op, because Free refuses a slot
// that is no longer live at that generation and the Gen bump rides the next
// handout rather than the free. Double release, no corruption, no abort.
VertexArrayImpl::g_backendVertexArrayObjects.DestroyByLifetimeId(lifetimeId);
break;
default:
// Buffer death crosses as ResourceDestroy (P3a): the catalogue has a call for
// it, so no seventh NotifyStateObjectDestroyed raiser is added. The notice
// exists for kinds that have NO such call - it carries {kind, lifetimeId} and
// not the object, and one entry point serves all of them because the answer is
// the same. The order at the buffer's death is fixed and not negotiable:
// resource_destroy first (the applier clears Live, the backend retires the
// twin), then MGPipeSlots().Free - the allocator erases its lifetimeId -> slot
// mapping on Free, so a notice resolved twice finds nothing the second time.
// On the legacy arm the signal is still BufferBackendOps::OnDestroy. Every
// other kind has no backend twin table here.
break;
}
}
const MG_State::GLState::StateObjectDeathOps g_glesStateObjectDeathOps = {
.OnDestroyed = OnFrontendStateObjectDestroyed,
};
} // namespace
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (id), CONTRACT-P5E §4.2 / §4.3: THE TWIN DEATH MOMENT, AND THE ABA ANSWER.
//
// The moment: the apply of `object_death` / the kind's own delete opcode / `resource_destroy`
// - in RING ORDER, i.e. after every verb that named the dying generation and before any
// record that names the slot's next owner - or the server backend's own destruction at Stop
// (InProcessTeardown). `applier_reset` touches neither twins nor object records
// (PipeApply.h's MGPipeApplierReset), so a make-current kills nothing.
//
// The ABA answer, which is what makes "no wait is added at any delete" (§2.7) safe: Gen
// moves ONLY on reuse (SlotAllocator.cpp), the client frees a slot only AFTER its death
// record went out (PipeFill.cpp's NotifyAndFree), and the ring is in order. So at the
// server every record naming {s, g} is applied before object_death{s, g}, which is applied
// before create{s, g+1}. THE ORDERING IS THE ARGUMENT AND THE GEN COMPARE IS THE PROOF:
// * GetOrCreate({s, g+1}) on a live {s, g} entry RE-MINTS - it resets the twin, so the
// successor never inherits the dead object's driver ids (SlotTables.h);
// * FindByHandle({s, g}) after the recycle answers NULL, so a stale handle resolves to
// nothing rather than to its successor's twin;
// * a backward {s, g-1} is REFUSED rather than adopted, so a late record cannot destroy
// the incumbent.
// Which means even a SKIPPED death (an object that never crossed emits none,
// WireTables.cpp) is answered by the forward-Gen reset alone. Pinned by
// DirectGLESSlotTable.AGenerationBehindTheLiveTwinIsRefusedRatherThanAdopted (all three
// clauses, on the table itself) and end to end by the CtWireScenario recycle cases.
Bool ReleaseTwinsForWireObjectDeath(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeKind kind) {
// The handle-keyed twin of the notice switch above, and deliberately NOT that switch:
// the record carried the handle, so no lifetime id is probed and the client's
// allocator - a client-only surface under a transport (CONTRACT-P5C.md §3.1) - is
// never touched from the apply thread. Each arm names its kind's registry GLOBAL for
// the same spelling reason as the notice arms: ReleaseByHandle /
// ReleaseTwinByHandle is static and is answered by every table of the kind that
// exists, this global's own and any by-value copy a fixture or a context reset holds.
switch (kind) {
case MG_Pipe::MGPipeKind::Texture:
return TextureImpl::g_backendTextureObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::Framebuffer:
return FramebufferImpl::g_backendFramebufferObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::Renderbuffer:
return RenderbufferImpl::g_backendRenderbufferObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::SamplerCso:
return SamplerImpl::g_backendSamplerObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::ShaderCso:
return PrgramImpl::g_backendProgramObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::SamplerViewCso:
// The notice arm's REDUNDANT SECOND PATH, keyed by the view's handle the same
// way (CONTRACT-P5C.md §5.2): the client's delete_sampler_view may already have
// released the twin, in which case the generation check inside fails and this
// answers false. A false here is idempotency, never a leak - the view twin owns
// no driver id of its own.
return SamplerViewImpl::BackendSamplerViewTable::ReleaseTwinByHandle(handle);
case MG_Pipe::MGPipeKind::VertexElementsCso:
return VertexArrayImpl::g_backendVertexArrayObjects.ReleaseByHandle(handle);
default:
// Buffer's death crosses as resource_destroy (P3a), exactly as the notice
// switch's default arm rules; every other kind has no twin table here.
return false;
}
}
// P12: see Managers.h. Every table is replaced by an empty one, holders first (views before
// the textures they view, framebuffers before their attachments, VAOs before the buffers they
// fetch from), so the last reference to each twin goes here and its destructor - answered
// InProcessTeardown() - touches neither the driver nor another TU's statics.
//
// P14 S6: "EVERY TABLE" IS NOW "EVERY BUCKET OF THIS SESSION". The tables are keyed by
// {session, share group}, so the same rebuild of the eight registries would take a NEIGHBOUR
// session's twins with it - and with S2's in-process supervisor serving several sessions at
// once that is not a hypothetical: this runs from ~BackendObject_DirectGLES, on the apply
// thread of the session that is ending. So the drop is scoped to the calling thread's
// session when it has one, and is the process-wide drop (key {0, 0} and nothing else) when it
// does not - which is the shape every unit case and the fork-worker world still has.
void DropEveryTwinForEndedServerSession() {
if (g_processTeardown) return;
g_serverSessionTwinTeardown = true;
TwinKey key;
if (CurrentTwinKey(&key) && key.SessionKey != 0) {
Uint32 buckets = 0;
buckets += SamplerViewImpl::g_backendSamplerViews.DropBucketsOfSession(key.SessionKey);
buckets += FramebufferImpl::g_backendFramebufferObjects.DropBucketsOfSession(key.SessionKey);
buckets += VertexArrayImpl::g_backendVertexArrayObjects.DropBucketsOfSession(key.SessionKey);
buckets += PrgramImpl::g_backendProgramObjects.DropBucketsOfSession(key.SessionKey);
buckets += SamplerImpl::g_backendSamplerObjects.DropBucketsOfSession(key.SessionKey);
buckets += TextureImpl::g_backendTextureObjects.DropBucketsOfSession(key.SessionKey);
buckets += RenderbufferImpl::g_backendRenderbufferObjects.DropBucketsOfSession(key.SessionKey);
buckets += BufferImpl::g_backendBufferResources.DropBucketsOfSession(key.SessionKey);
MGLOG_I("DirectGLES: the ended server session dropped %u twin group(s) across eight kinds "
"(no driver call); the other sessions in this process keep theirs",
buckets);
} else {
SamplerViewImpl::g_backendSamplerViews.ResetBuckets();
FramebufferImpl::g_backendFramebufferObjects.ResetBuckets();
VertexArrayImpl::g_backendVertexArrayObjects.ResetBuckets();
PrgramImpl::g_backendProgramObjects.ResetBuckets();
SamplerImpl::g_backendSamplerObjects.ResetBuckets();
TextureImpl::g_backendTextureObjects.ResetBuckets();
RenderbufferImpl::g_backendRenderbufferObjects.ResetBuckets();
BufferImpl::g_backendBufferResources.ResetBuckets();
}
// P12 review fix (major): AND THE UNIT SHADOWS THAT POINT AT THEM. A texture or sampler twin
// scrubs itself out of g_boundTexturesCache / g_boundSamplersCache in its destructor - but
// not under InProcessTeardown(), which answered true for every twin just dropped. Left
// behind, those raw pointers name freed twins; the next session's twin allocated at the same
// address then reads "already bound" and skips its glBindTexture / glBindSampler on a
// context where nothing is bound, so its uploads land on texture 0 and it samples black.
// The new context's active unit is GL_TEXTURE0, so the active-unit shadow goes back to 0 too.
TextureImpl::g_boundTexturesCache = {};
TextureImpl::g_activeTextureUnit = 0;
SamplerImpl::g_boundSamplersCache = {};
g_serverSessionTwinTeardown = false;
}
#endif
// The one sentence that decides the arm, written once so that a test can drive every
// combination of the two knobs and so that bring-up and first-use cannot disagree.
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled) {
// The legacy arm is not compiled, so the handle arm is the only arm and neither knob
// can produce an armless configuration.
(void)subsystemBitSet;
(void)legacyMemosEnabled;
return EsprytSlotArmVerdict::Handles;
}
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict() {
return ClassifyEsprytSlotArm(
(MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemEsprytSlots) != 0,
/*legacyMemosEnabled=*/true);
}
void DiagnoseEsprytSlotArm() {
if (CurrentEsprytSlotArmVerdict() != EsprytSlotArmVerdict::NoArm) return;
// Loud, named, and NOT a stop - see the comment on this function in SlotTables.h for
// why a stop raised from inside EGL bring-up is swallowed into a skipped lane.
MGLOG_E("MGPipe: PipeLegacyMemosDisabled - MOBILEGL_PIPE_PUSH leaves "
"kMGPipeSubsystemEsprytSlots (bit 5) clear and MOBILEGL_PIPE_LEGACY_MEMOS=0 "
"makes the legacy twin registry unreachable, so this context has no twin table "
"arm at all; the first twin lookup will stop the process");
}
Bool ResolveEsprytSlotTablesArm() {
// Resolved once and latched by the inline EsprytSlotTablesEnabled() in SlotTables.h:
// the two arms of StateBackendObjectRegistry keep their twins in different containers,
// so an answer that changed mid-run would strand every twin already built (and, for
// the driver ids those twins own, leak them).
//
// This runs at the FIRST TWIN LOOKUP, not at backend bring-up. That is deliberate and
// it is the fix for a lane that went green by skipping: bring-up runs inside
// eglMakeCurrent, which the integration harness pre-flights in a forked child, and a
// child that aborts is reported as "no usable GPU" and skips every scenario. Here the
// stop lands in the caller of the twin lookup - a scenario body, a sync path, a test -
// where ctest reports it as a failure. A process that never looks a twin up never needs
// an arm and is never stopped by this.
const EsprytSlotArmVerdict verdict = CurrentEsprytSlotArmVerdict();
if (verdict == EsprytSlotArmVerdict::NoArm) {
// The operator asked for the handle arm to be OFF and the legacy arm to be
// unreachable at the same time, which leaves no arm at all. This is a Fatal{},
// and a Fatal{} in this codebase STOPS (MG_Impl/Pipe/PipeFill.cpp's BadKnob and
// its verify trap are both MGLOG_F + abort). Returning here instead would run
// the very arm the operator disabled and hand back a green result measured on
// it - which is exactly the lever HandleRecycleScenario's arms are selected
// with, so a mis-set A/B would be scored silently against the wrong arm
// (ARCHITECTURE.md 9.6).
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled, \"MOBILEGL_PIPE_PUSH leaves "
"kMGPipeSubsystemEsprytSlots (bit 5) clear and MOBILEGL_PIPE_LEGACY_MEMOS=0 "
"makes the legacy twin registry unreachable, so there is no twin table arm "
"to run\"}");
std::abort();
}
if (verdict == EsprytSlotArmVerdict::Legacy) {
return false;
}
if ((MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemEsprytSlots) == 0) {
// The bit is clear but this build has no legacy twin registry to fall back to, so
// the bit decides nothing. Recorded so a log reader sees the mismatch.
MGLOG_D("MGPipe: kMGPipeSubsystemEsprytSlots is clear but this build has no "
"legacy twin registry; running the handle arm anyway");
}
// The notice is only consumable on the handle arm (the legacy registry keys on the
// frontend ADDRESS, which is gone by the time a destructor speaks), so it is installed
// exactly where it can be answered. Once per process, cold.
MG_State::GLState::SetStateObjectDeathOps(&g_glesStateObjectDeathOps);
return true;
}
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks) {
// One block is all the indirect-params view needs, so this is a >= 1 test and not a
// budget calculation. Negative is treated as unusable rather than clamped: a driver
// that leaves the out-param untouched is telling us nothing, and guessing "yes" here
// is what produces an unlinkable program.
return maxVertexShaderStorageBlocks >= 1;
}
static Bool CanUseVertexStageStorageBlock() {
return VertexStageStorageBlockUsable(g_GLESCapabilities.MaxVertexShaderStorageBlocks);
}
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType) {
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
return source;
}
String replaced = ReplaceIdentifier(source, "gl_BaseInstance", BASE_INSTANCE_UNIFORM_NAME);
if (replaced == source) {
// Only a substring hit (e.g. gl_BaseInstanceARB inside a SPIRV-Cross #ifdef
// fallback); nothing was rewritten, so nothing must be declared either.
return source;
}
return InjectUniformAfterVersion(std::move(replaced),
String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";");
}
// The LowerDrawParametersPass demotes gl_DrawID / gl_BaseInstance / gl_BaseVertex to plain
// Private globals (mg_DrawID / mg_BaseInstanceLowered / mg_BaseVertex); SPIRV-Cross then
// emits them as ordinary global declarations. mg_DrawID / mg_BaseVertex become uniforms fed
// per (sub-)draw. gl_BaseInstance is special: for indirect draws its value lives in the
// (possibly GPU-written) indirect command buffer, so its declaration expands into a
// std430 SSBO view of that buffer indexed by a CPU-computed word index, with the plain
// mg_BaseInstance uniform as the fallback for non-indirect draws.
//
// The word index is stored ONE-BASED, so that zero - the value every GLSL uniform starts
// at - is the "not an indirect draw" sentinel. Nothing seeds this uniform before a
// program's first draw, and the non-indirect draw entry points never write it at all, so a
// zero-based index with a negative sentinel would leave every such draw reading
// mg_indirectWords[0] out of a storage buffer no one bound. That is not a silent zero on a
// real driver: it returned garbage on Adreno, and a garbage gl_BaseInstance pushed the CTS
// shader_draw_parameters geometry clean off screen.
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType) {
if (shaderType != GL_VERTEX_SHADER) {
return source;
}
for (const char* name : {DRAW_ID_UNIFORM_NAME, BASE_VERTEX_UNIFORM_NAME}) {
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int ", "highp uint ",
"mediump uint ", "uint "}) {
const String declaration = String(declPrefix) + name + ";";
const SizeT pos = source.find(declaration);
if (pos == String::npos) {
continue;
}
// Only promote a standalone global declaration, not a uniform we already emitted.
const Bool alreadyUniform = pos >= 8 && source.compare(pos - 8, 8, "uniform ") == 0;
if (!alreadyUniform) {
const Bool hasPrecision = std::strncmp(declPrefix, "int ", 4) != 0 &&
std::strncmp(declPrefix, "uint ", 5) != 0;
const String qualifier = hasPrecision ? "uniform " : "uniform highp ";
source.replace(pos, declaration.size(), qualifier + declaration);
}
break;
}
}
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int "}) {
const String declaration = String(declPrefix) + BASE_INSTANCE_LOWERED_NAME + ";";
SizeT pos = source.find(declaration);
if (pos == String::npos) {
continue;
}
// On drivers where native indirect draws leak the command's baseInstance into
// gl_InstanceID (ANGLE-on-Vulkan; IndirectDrawInstanceIdIncludesBaseInstance),
// rebase gl_InstanceID back to zero during those draws so shaders computing
// gl_BaseInstance + gl_InstanceID don't add the base twice. Scoped to shaders
// using gl_BaseInstance: only they take the native indirect SSBO machinery.
const Bool rebaseInstanceId = g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance &&
source.find("gl_InstanceID") != String::npos;
if (rebaseInstanceId) {
source = ReplaceIdentifier(source, "gl_InstanceID", ZERO_BASED_INSTANCE_ID_NAME);
pos = source.find(declaration); // the declaration contains no gl_InstanceID
}
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
: 0;
// The whole indirect half of this machinery is a storage block read from the VERTEX
// stage, and a storage block in the vertex stage is optional in both APIs: the
// minimum for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2
// table 21.44) and ARM's GLES driver takes that allowance - a Mali-G925 reports 0.
// Emitting the block anyway does not make it work; it makes the program UNLINKABLE
// ("The number of vertex shader storage blocks (1) is greater than the maximum
// number allowed (0)"), and because the frontend's LINK_STATUS is glslang's and not
// the driver's, the application never learns: every draw with that program silently
// renders nothing. Dropping just the indirect half costs strictly less.
const Bool canReadIndirectParamsFromVertexStage = CanUseVertexStageStorageBlock();
String machinery;
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n";
}
if (!canReadIndirectParamsFromVertexStage) {
// Degraded, but contained and loud. gl_BaseInstance collapses to the plain
// mg_BaseInstance uniform, which the non-indirect draw entry points do set
// correctly - so ordinary instanced draws are unaffected. What is lost is the
// per-command baseInstance of an INDIRECT draw, which lives in the (possibly
// GPU-written) command buffer and can only be read through this block: those
// draws now see the last uniform value rather than their own command's. No
// alternative path is attempted, deliberately - there is nowhere else in the
// vertex stage to read a GPU-written buffer from.
//
// MGLOG_E_ONCE, not _D: this silently changes rendering for exactly the
// workloads (Create/Flywheel indirect instancing) whose bug reports are
// impossible to read without it, and once per process is bounded.
MGLOG_E_ONCE("gl_BaseInstance: this driver reports GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = %d, so the "
"%s storage block an indirect draw's baseInstance must be read through cannot be "
"declared in the vertex stage. Dropping indirect baseInstance support: non-indirect "
"draws are correct, indirect draws will see a stale per-command baseInstance.",
g_GLESCapabilities.MaxVertexShaderStorageBlocks, INDIRECT_PARAMS_BLOCK_NAME);
if (rebaseInstanceId) {
// Without the block there is no per-command baseInstance to subtract, and
// the uniform is the same value the define below resolves to, so rebasing
// by it would cancel the base out of gl_InstanceID twice.
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " gl_InstanceID\n";
}
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " (" + BASE_INSTANCE_UNIFORM_NAME + ")";
source.replace(pos, declaration.size(), machinery);
break;
}
machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
" { highp uint mg_indirectWords[]; };\n";
if (rebaseInstanceId) {
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " (gl_InstanceID - ((" +
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " > 0) ? int(mg_indirectWords[uint(" +
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " - 1)]) : 0))\n";
}
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " ((" +
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " > 0) ? int(mg_indirectWords[uint(" +
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " - 1)]) : " + BASE_INSTANCE_UNIFORM_NAME + ")";
source.replace(pos, declaration.size(), machinery);
break;
}
return source;
}
// ---- gl_ViewportIndex routing emulation, ESSL half ---------------------------------------
//
// LowerViewportIndexPass has already turned the BuiltIn ViewportIndex OUTPUT into a plain
// Private global, so SPIRV-Cross printed `int mg_ViewportIndex;` at file scope and the stage
// still stores the index the application asked for - it just goes nowhere. The two passes
// below give it somewhere to go WITHOUT naming a builtin the language does not have: the
// producing stage's global becomes an ordinary flat varying, and the fragment stage gets a
// gate that discards every fragment whose primitive routed to a viewport the current replay
// pass is not drawing. DirectGLES.cpp's ForEachViewportRoutingPass is the other half - it
// re-issues the draw once per distinct viewport state with the real
// glViewport/glScissor/glDepthRangef pushed for it and this uniform set to the set of
// indices that state serves.
//
// FLAT is semantics, not performance: GL takes a primitive's viewport index from its
// PROVOKING VERTEX, which is exactly what flat interpolation delivers, so a primitive whose
// vertices carry different indices routes the way the spec says with no extra machinery.
//
// NO layout(location = N) on either side, deliberately. The two stages are transpiled
// independently and neither can see the other's location assignment: the producing stage
// knows its own outputs, the fragment stage only the subset it consumes, and a number derived
// from either can disagree with the other. Leaving both unqualified hands the assignment to
// the driver's linker, which then matches them BY NAME - the ordinary GLSL rule, and the only
// one that needs no cross-stage channel. The cost is one varying slot, which a program
// already at GL_MAX_VARYING_VECTORS cannot spare.
constexpr const char* VIEWPORT_INDEX_VARYING_NAME = "mg_ViewportIndex";
constexpr const char* VIEWPORT_PASS_MASK_UNIFORM_NAME = "mg_ViewportPassMask";
constexpr const char* VIEWPORT_GATED_ENTRY_POINT_NAME = "mg_ViewportGatedMain";
constexpr const char* ESSL_ENTRY_POINT_SIGNATURE = "void main()";
static_assert(RenderStateParameters::MAX_VIEWPORTS == 16,
"the fragment gate below spells the index clamp as `& 15` and the pass mask as a "
"16-bit int; both follow MAX_VIEWPORTS and have to be respelled with it");
// Producing stage (vertex / tessellation evaluation / geometry - the three GL lets write the
// builtin). Returns whether the demoted global was found and promoted, which is also the
// answer to "does this program route viewports at all".
Bool PromoteViewportIndexGlobalToVarying(String& source) {
// The same shape PromoteDrawParameterGlobalsToUniforms matches, and for the same reason:
// SPIRV-Cross prints the demoted global with or without a precision qualifier depending
// on what the module carried. Only a declaration that starts its own line may be
// rewritten - `mg_ViewportIndex = gl_InvocationID;` in the body contains the name too and
// has to be left exactly as it is.
const String declared = String(VIEWPORT_INDEX_VARYING_NAME) + ";";
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int "}) {
const String declaration = String(declPrefix) + declared;
const SizeT pos = source.find(declaration);
if (pos == String::npos) {
continue;
}
// Column 0 of its own line is what separates the declaration from the tail of any
// other declaration or expression that ends in the same name.
if (pos != 0 && source[pos - 1] != '\n') {
continue;
}
source.replace(pos, declaration.size(),
String("flat out highp int ") + VIEWPORT_INDEX_VARYING_NAME + ";");
return true;
}
return false;
}
// Fragment stage. Returns false when the stage has no entry point to gate onto, which the
// caller reports: the program still links and still renders, it just renders every index
// with the first replay pass's state - i.e. it degrades to the pre-emulation behaviour
// rather than to a black screen.
Bool InjectViewportIndexPassGate(String& source) {
// Built beside the input and swapped in only on success, so a stage this pass declines
// reaches the driver exactly as it arrived rather than half-rewritten.
// A fragment stage that READS gl_ViewportIndex has no ESSL spelling for it either -
// LowerViewportIndexPass deliberately demotes only OUTPUTS, because a demoted INPUT would
// answer from an undefined Private global. Now that the routing varying exists and
// carries the real per-primitive value, that read has somewhere honest to go.
String gated = ReplaceIdentifier(source, "gl_ViewportIndex", VIEWPORT_INDEX_VARYING_NAME);
const SizeT entryPos = gated.find(ESSL_ENTRY_POINT_SIGNATURE);
if (entryPos == String::npos) {
return false;
}
// Declarations go immediately before the entry point rather than after #version: that
// position is already past every #extension directive (which must precede any other
// token) and past everything the body can name, so it can invalidate neither.
//
// Renaming the entry point rather than splicing a prologue into its body keeps the
// application's code byte-identical, including an early `return`.
String preamble = String("flat in highp int ") + VIEWPORT_INDEX_VARYING_NAME + ";\n";
preamble += String("uniform highp int ") + VIEWPORT_PASS_MASK_UNIFORM_NAME + ";\n";
preamble += String("void ") + VIEWPORT_GATED_ENTRY_POINT_NAME + "()";
gated.replace(entryPos, std::strlen(ESSL_ENTRY_POINT_SIGNATURE), preamble);
// `& 15` clamps the shift operand into range for MAX_VIEWPORTS = 16. GL leaves an index
// outside [0, MAX_VIEWPORTS) undefined, but an ESSL shift by >= 32 is undefined in a way
// that can take the whole draw with it, so the emulation picks a defined answer instead.
//
// The mask, not an equality test against a pass number: viewport indices whose whole
// state tuple is identical share ONE replay pass (see BeginViewportRoutingPasses), and
// the overwhelmingly common case - every index still holding what glViewport broadcast -
// is then a single pass with every bit set, i.e. a gate that discards nothing and a draw
// that is issued exactly once.
//
// PERFORMANCE NOTE: a fragment shader containing `discard` cannot take the early-Z fast
// path on a tiler, so a routed draw pays late-Z on top of its N replay passes. Accepted
// deliberately: this runs only for a program that writes gl_ViewportIndex, and that is
// why the gate is injected per program rather than into every fragment shader.
gated += "\n";
gated += String(ESSL_ENTRY_POINT_SIGNATURE) + "\n";
gated += "{\n";
gated += String(" if (((") + VIEWPORT_PASS_MASK_UNIFORM_NAME + " >> (" +
VIEWPORT_INDEX_VARYING_NAME + " & 15)) & 1) == 0)\n";
gated += " {\n";
gated += " discard;\n";
gated += " }\n";
gated += " else\n";
gated += " {\n";
gated += String(" ") + VIEWPORT_GATED_ENTRY_POINT_NAME + "();\n";
gated += " }\n";
gated += "}\n";
source = std::move(gated);
return true;
}
// The transpile pipeline invents image binding numbers: when the GL source declares
// an image uniform without layout(binding), glslang auto-assigns one (desktop GL
// allows that and lets the app pick the unit with glUniform1i, which ES forbids on
// image uniforms). The unit the app actually addresses lives in frontend state: the
// layout(binding) reflected at link time, or whatever glUniform1i stored afterwards.
// Rewrite every image uniform declaration to that unit so imageLoad/Store hits the
// unit the app bound with glBindImageTexture.
String RebindImageUniformsToFrontendUnits(String source, const PrgramImpl::ProgramBuildSource& src) {
// P5e (pg): the SOURCE, not the frontend object. The two reads below - the uniform's
// location and the unit glUniform1i put there - are exactly the pair the record's
// set_program_bindings tail carries, so on the handle arm this bakes the unit the client
// observed rather than one it would have had to dereference a live ProgramObject for.
// The null test the SharedPtr form carried moved to the caller, which returned early on
// a null program long before it reached here.
if (source.find("image") == String::npos) {
return source;
}
static const std::regex imageDeclRegex(
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
String result;
result.reserve(source.size());
SizeT lineStart = 0;
while (lineStart <= source.size()) {
const SizeT lineEnd = source.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = source.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
std::smatch match;
if (std::regex_search(line, match, imageDeclRegex)) {
const String name = match[3].str();
Int location = src.GetUniformLocation(name);
if (location < 0) {
location = src.GetUniformLocation(name + "[0]");
}
if (location >= 0) {
const Int unit = src.GetUniformSamplerOrImageUnitIndex(location);
if (unit >= 0) {
const String bindingText = "binding = " + std::to_string(unit);
if (std::regex_search(line, bindingValueRegex)) {
line = std::regex_replace(line, bindingValueRegex, bindingText);
} else if (match[1].matched) {
const SizeT layoutOpen = line.find('(', match.position(1));
line.insert(layoutOpen + 1, bindingText + ", ");
} else {
line.insert(match.position(0), "layout(" + bindingText + ") ");
}
}
}
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
namespace BufferImpl {
namespace {
using MG_State::GLState::BackendBufferResource;
using MG_State::GLState::BufferBackendOps;
using MG_State::GLState::BufferObject;
// GL_ARRAY_BUFFER redundant-bind cache (id 0 = unknown/none).
Uint g_boundArrayBufferId = 0;
Bool g_boundArrayBufferKnown = false;
// Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see
// Managers.h). Resting state between operations is 0; scopes in the
// readback/upload paths bind what they need through the cache and
// return to 0, so a stale user PBO can never capture a later
// readback that meant to target client memory.
Uint g_boundPixelPackBufferId = 0;
Bool g_boundPixelPackBufferKnown = false;
Uint g_boundPixelUnpackBufferId = 0;
Bool g_boundPixelUnpackBufferKnown = false;
// Bumped whenever the backend ES context is destroyed; resources with
// an older generation hold ids from a dead context.
Uint g_bufferContextGeneration = 1;
// Buffer-mutation epoch backing store (contract, mutation-site list and
// memory-ordering rules: Managers.h at the accessor declarations).
// Starts at 1 so the memo stamps' 0 means "never stamped". Atomic:
// frontend buffer ops may run on non-draw threads while the draw thread
// reads; the release-bump-AFTER-mutation / acquire-read-BEFORE-probes
// pairing makes a stamp taken against stale state impossible to consume.
std::atomic<Uint64> g_bufferMutationEpoch{1};
// Defined next to the indexed-binding shadow below; forward-declared so
// every glDeleteBuffers site in this namespace can scrub stale shadow
// entries (GL resets a deleted buffer's bindings - indexed and pixel
// pack/unpack alike - to 0, and a recycled name matching a stale shadow
// entry would otherwise false-skip the rebind).
void ScrubBufferBindingShadowsForId(Uint id);
// Defined next to the same shadow, and the counterpart to the scrub above for a
// buffer whose STORE was re-specified rather than deleted: the binding survives -
// nothing unbound the id - but the extent the driver resolved for it at bind time
// does not. Marks those points unknown so the next sync issues a real
// glBindBufferBase/Range instead of skipping it.
void InvalidateIndexedBufferBindingShadowsForId(Uint id);
// Resources whose owning BufferObject died; ids deleted at the next
// sync point with a current ES context.
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
std::mutex g_deferredBufferReleasesMutex;
// Cheap emptiness probe so the per-draw drain can skip the mutex and
// context check when nothing was enqueued (the overwhelmingly common
// case). Written only under the mutex; read lock-free.
std::atomic<Bool> g_hasDeferredBufferReleases{false};
// --- Buffer-storage pool (Mesa-style BO recycle) -------------------------
// Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers
// (which triggers the kgsl_sharedmem_free -> mmu_unmap -> smmu/power/bandwidth
// cascade that dominated per-frame driver cost). An id retired during frame N
// is handed back only once the GPU has completed frame N (fence watermark, see
// DirectGLES::CompletedFrameSerial), then reseeded in place with glBufferSubData
// (no glBufferData realloc). All GL access is on the ES-context-owning thread;
// the mutex only guards against off-thread deferred-release enrollment races.
struct PooledBuffer {
Uint id = 0;
SizeT size = 0;
Uint contextGeneration = 0;
Uint64 retireSerial = 0;
};
UnorderedMap<SizeT, Vector<PooledBuffer>> g_bufferPool;
SizeT g_pooledBytes = 0;
std::mutex g_poolMutex;
constexpr SizeT kMaxPoolableBufferBytes = 8u * 1024u * 1024u; // bigger buffers: delete now
constexpr SizeT kMaxPoolBytes = 64u * 1024u * 1024u; // total pool budget
constexpr SizeT kMaxEntriesPerBucket = 32;
Bool IsPoolable(const GLESBufferResource& r) {
// Require working fences: recycling is gated on the frame-completion
// watermark, which only advances if Present can insert/poll fences.
return g_GLESFuncs.glFenceSync != nullptr && g_GLESFuncs.glGetSynciv != nullptr &&
r.id != 0 && !r.persistentMapped && !r.immutableStorage &&
r.contextGeneration == g_bufferContextGeneration && r.storageInitialized &&
r.storageSize > 0 && r.storageSize <= kMaxPoolableBufferBytes;
}
// Retire a buffer id into the pool (owning thread; caller verified IsPoolable).
// Zeroes r.id to keep the single-owner invariant {live | deferred | pool}.
void EnrollIntoPool(GLESBufferResource& r) {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) {
InvalidateArrayBufferBindingCache();
}
// Pooling keeps the id alive (and thus any driver binding of it);
// drop to unknown rather than claiming the post-delete 0 state.
if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) ||
(g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) {
InvalidatePixelBufferBindingCaches();
}
const std::lock_guard<std::mutex> lock(g_poolMutex);
auto& bucket = g_bufferPool[r.storageSize];
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
ScrubBufferBindingShadowsForId(r.id);
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
r.id = 0;
return;
}
// +1: Present increments the serial at frame END, so during the frame
// now being built CurrentFrameSerial() reads (frame-1). A buffer used
// this frame is only GPU-done once THIS frame's fence (serial+1) signals.
bucket.push_back(
{r.id, r.storageSize, r.contextGeneration, DirectGLES::CurrentFrameSerial() + 1});
g_pooledBytes += r.storageSize;
r.id = 0;
}
// Hand back an idle pooled id of EXACTLY `size` whose GPU work is complete,
// else 0. Owning thread only. Drops stale-generation entries encountered.
Uint AcquireFromPool(SizeT size) {
const Uint64 completed = DirectGLES::CompletedFrameSerial();
const std::lock_guard<std::mutex> lock(g_poolMutex);
auto it = g_bufferPool.find(size);
if (it == g_bufferPool.end()) return 0;
auto& bucket = it->second;
for (SizeT i = bucket.size(); i-- > 0;) { // newest-first: hottest + most-likely-idle
PooledBuffer& e = bucket[i];
if (e.contextGeneration != g_bufferContextGeneration) {
g_pooledBytes -= e.size; // dead-context id: drop, no GL
bucket[i] = bucket.back();
bucket.pop_back();
continue;
}
if (e.retireSerial <= completed) {
const Uint id = e.id;
g_pooledBytes -= e.size;
bucket[i] = bucket.back();
bucket.pop_back();
return id;
}
}
return 0;
}
// --- Persistent-mapped bump rings (see Managers.h) -----------------------
// Shared machinery behind BOTH the global-UBO ring and the texture
// unpack-PBO ring: one EXT_buffer_storage persistent|coherent map per ring,
// monotonic head/tail cursors, and reclamation riding the Present()
// frame-fence watermark. The two rings differ only in size cap, offset
// alignment and log label - the reclamation, context-loss and
// emergency-drain rules are the part that was hard to get right, so they
// are shared rather than copied.
constexpr SizeT kUboRingInitialBytes = 4u * 1024u * 1024u;
constexpr SizeT kUboRingMaxBytes = 64u * 1024u * 1024u;
constexpr SizeT kUnpackRingInitialBytes = 4u * 1024u * 1024u;
constexpr SizeT kUnpackRingMaxBytes = 64u * 1024u * 1024u;
// A PBO-sourced glTexSubImage constrains the offset only to the pixel
// TYPE's size (GL_INVALID_OPERATION otherwise), and no ES client type is
// wider than 4 bytes - unlike a UBO bind, which owes the driver
// GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. 64 covers every type with room to
// spare and keeps consecutive staged blocks off each other's cache lines.
constexpr SizeT kUnpackRingAlignment = 64;
// glCopyBufferSubData carries no offset-alignment requirement at all; 64
// keeps staged blocks cache-line separated, same as the unpack ring.
constexpr SizeT kUploadRingInitialBytes = 4u * 1024u * 1024u;
constexpr SizeT kUploadRingMaxBytes = 64u * 1024u * 1024u;
constexpr SizeT kUploadRingAlignment = 64;
struct PersistentRingStore {
Uint id = 0;
Uint8* mappedPtr = nullptr;
SizeT size = 0;
// Monotonic linear cursors: `head` counts every byte ever allocated
// (incl. wrap padding); everything below `tail` is GPU-complete. Ring
// offset of a linear position is pos % size, so in-flight bytes are
// head - tail and must stay <= size.
Uint64 head = 0;
Uint64 tail = 0;
Uint32 generation = 0; // bumped on every (re)create/grow; 0 = never valid
Uint contextGeneration = 0;
SizeT alignment = 256;
// A hard storage-creation failure under this context; stop retrying
// per use (cleared when the context generation moves on).
Bool creationFailed = false;
};
// Grown-away ring stores: deletable only once the GPU finished the last
// frame that could reference them (same watermark as the buffer pool).
struct RetiredRingStore {
Uint id = 0;
Uint contextGeneration = 0;
Uint64 retireSerial = 0;
};
// Present()-time high-water marks: every byte below headAtPresent was
// written during frames <= frameSerial, so once frameSerial completes,
// tail may advance to headAtPresent. FIFO by construction.
struct RingFrameMark {
Uint64 frameSerial = 0;
Uint64 headAtPresent = 0;
};
// One ring: its live store, its two reclamation lists, and the immutable
// knobs that tell it apart from the other one.
struct PersistentRing {
PersistentRingStore store;
Vector<RetiredRingStore> retired;
Vector<RingFrameMark> frameMarks;
SizeT initialBytes = 0;
SizeT maxBytes = 0;
// 0: take the offset alignment from GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT
// at store-creation time (the UBO ring's binds require it).
SizeT fixedAlignment = 0;
const char* label = "";
};
// THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at
// Present, which say "everything this context wrote before its fence N is done once N is".
// One process-wide set used to be shared by every session's contexts: a fence from one
// context retired bytes another context's not-yet-executed copies still had to read
// (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's
// staging overwrote them - its pixels landed, at the wrong offsets, in the first one's
// textures. With one set per context, a context's bytes wait for that context's own
// fence. (The buffers themselves are shared objects; only who stages into them is split.)
// Staging ring for app buffer updates whose destination store may still be
// referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR
// hazard by BLOCKING in the call (osup_sync_object_wait) until every
// referencing job retires - under Minecraft 26.3's per-frame UBO and
// chunk-mesh SubData streams that serialized whole frames (~1 fps while
// chunks stream in). Staging the bytes here and issuing a
// glCopyBufferSubData instead keeps the hazard on the GPU timeline where it
// is just job ordering, and the CPU never waits.
struct RingSet {
PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"};
PersistentRing unpack{{},
{},
{},
kUnpackRingInitialBytes,
kUnpackRingMaxBytes,
kUnpackRingAlignment,
"Texture unpack ring"};
PersistentRing upload{{},
{},
{},
kUploadRingInitialBytes,
kUploadRingMaxBytes,
kUploadRingAlignment,
"Buffer upload ring"};
RingSet();
~RingSet();
RingSet(const RingSet&) = delete;
RingSet& operator=(const RingSet&) = delete;
};
// A dead context's ring buffers: shared objects, so they outlive it; deleted at the next
// Present of any context of the same share-group generation.
struct OrphanRingBuffer {
Uint id = 0;
Uint contextGeneration = 0;
};
std::mutex g_orphanRingBuffersMutex;
Vector<OrphanRingBuffer>& OrphanRingBuffers() {
static auto* orphans = new Vector<OrphanRingBuffer>();
return *orphans;
}
// Each set's generations start in a range of their own: a per-program slot cache keyed
// by (generation, offset) must never take one context's ring for another's.
std::atomic<Uint32> g_ringGenerationBases{0};
RingSet::RingSet() {
const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12;
ubo.store.generation = base;
unpack.store.generation = base;
upload.store.generation = base;
}
RingSet::~RingSet() {
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
for (const PersistentRing* ring : {&ubo, &unpack, &upload}) {
if (ring->store.id != 0) {
OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration});
}
for (const auto& retired : ring->retired) {
if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration});
}
}
}
DirectGLES::PerNativeContext<RingSet> g_ringSets;
PersistentRing& UboRing() { return g_ringSets.Current().ubo; }
PersistentRing& UnpackRing() { return g_ringSets.Current().unpack; }
PersistentRing& UploadRing() { return g_ringSets.Current().upload; }
// The ES context the ring's id/map belonged to is gone (or was never
// seen): drop every handle without GL calls and re-arm creation. The
// generation counter must survive the reset — frame serials also survive
// context recreation, so a restarted counter could revalidate a stale
// per-program slot cache against the new ring.
void ResetRingForNewContext(PersistentRing& ring) {
const Uint32 keptGeneration = ring.store.generation;
ring.store = {};
ring.store.generation = keptGeneration;
ring.store.contextGeneration = g_bufferContextGeneration;
// Keep the ring's own alignment across the wipe: the slow path rounds a
// request with it BEFORE the store that would set it exists.
if (ring.fixedAlignment != 0) ring.store.alignment = ring.fixedAlignment;
ring.retired.clear();
ring.frameMarks.clear();
}
GLESBufferResource* ResourceOf(BufferObject& bufferObject) {
return static_cast<GLESBufferResource*>(bufferObject.GetBackendResource().get());
}
Bool CanTouchGLNow() {
return DirectGLES::IsBackendContextCurrentOnThisThread();
}
// P3a: ONE body per helper, parameterised on where its host bytes and its
// extent come from, so the legacy arm and the handle arm of a push build share
// the three-tier flush, the respecify and the range upload rather than owning a
// copy each - a second copy of the tier ladder is exactly how a tier changes in
// silence. The #else below is the PULL build, whose text must stay byte-identical
// to the pre-P3a one (G1: the pull build gains no symbol and resizes none); it is
// frozen by that gate and retires with the pull path at P13.
// (Re)specify backend storage from the shadow copy: glBufferData.
// The orphaning point - the ES driver performs the actual rename.
// TODO(buffer-pool Phase 2): orphan-on-respecify is NOT yet implemented.
// When the current id is BUSY (lastUseFrameSerial > CompletedFrameSerial())
// && !persistentMapped && !noOrphan, express the orphan as an id-swap
// (retire the busy id into the pool, bind a fresh/pooled id) instead of the
// in-place glBufferData below, to avoid the driver's own rename/stall. Not
// pursued yet: glBufferData/glBufferSubData currently sit below profiler
// noise, so respecify is not a hot path in the profiled scenes.
// The body both arms run. `size`, `usage`, `initialData` and `syncedSerial` are the
// four things the legacy arm reads off the frontend object and the handle arm reads
// off the applier's stored descriptor and the shadow base the call carried; nothing
// else in here differs, so there is ONE glBufferData and one extent rule rather
// than two that can drift apart.
void RespecifyStorageWith(GLESBufferResource& resource, SizeT size, GLenum usage,
const void* initialData, Uint64 syncedSerial) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Read BEFORE the fields below are overwritten: whether this respecify changes
// the store's EXTENT is what decides if the indexed-binding shadow still
// describes the driver.
const Bool extentChanged = !resource.storageInitialized || resource.storageSize != size;
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
if (MG_Util::PipeStats::Enabled() && initialData != nullptr) {
// An ORPHANING respecify passes NULL and moves nothing, which is exactly
// why the test is on initialData rather than on size.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource.storageSize = size;
resource.storageInitialized = true;
resource.pendingRespecify = false;
resource.pendingRanges.clear();
resource.pendingResidentWrites.clear();
resource.syncedChangeSerial = syncedSerial;
// A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a
// rebind whenever the shadow already records this id at that index - so on a
// driver that resolves a whole-buffer indexed binding's extent at BIND time
// (Adreno does; Mali does not) the shader keeps seeing the old, smaller range:
// stores past it are dropped and loads return zero. Forget what the shadow
// claims for this id so the next SyncBufferBindingPoints issues the bind for
// real. Only when the extent actually moved: an orphaning respecify at the same
// size is Minecraft's per-frame hot path and its bindings are still exact.
if (extentChanged) {
InvalidateIndexedBufferBindingShadowsForId(resource.id);
}
}
void RespecifyStorageNow(GLESBufferResource& resource, BufferObject& bufferObject) {
const SizeT size = bufferObject.GetSize();
// An orphaning respecify (glBufferData with NULL, content never
// written since) stays a pure NULL reallocation: the driver renames
// the store without a stall and nothing is transferred. Uploading
// the stale shadow here turned Minecraft-style orphaning into a
// full-size synchronized upload.
const void* initialData =
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
RespecifyStorageWith(resource, size, MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage()),
initialData, bufferObject.GetChangeSerial());
}
Bool StorageMatchesSize(const GLESBufferResource& resource, SizeT size) {
return resource.storageInitialized && !resource.pendingRespecify && resource.storageSize == size;
}
Bool StorageMatches(const GLESBufferResource& resource, const BufferObject& bufferObject) {
return StorageMatchesSize(resource, bufferObject.GetSize());
}
#if MOBILEGL_BUILD_DISAGGREGATED
// -------------------------------------------------------------------------------
// R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Package v1.
//
// The rule, the evidence and the reason the storage is a side table rather than a
// member of GLESBufferResource are all in MG_Remote/Server/StagedShadow.h. This is
// only the instance and the four call sites.
//
// ONE PER PROCESS, and its copying arm is decided ONCE at first use: the two arms
// hold the authoritative bytes in DIFFERENT places, so an answer that changed
// mid-run would strand every resource already staged - the same reason
// ResolveResourceSubsystemArm latches (Managers.h). Leaked at exit like every other
// MG_Remote singleton (ID-8): ~BufferObject reaches the destroy path from exit
// handlers, after this TU's globals would already be gone.
// -------------------------------------------------------------------------------
MG_Remote::Server::StagedShadowStore& ServerStaged() {
static MG_Remote::Server::StagedShadowStore& store =
*new MG_Remote::Server::StagedShadowStore(
MG_Config::Transport != MG_Config::TransportMode::Monolith);
return store;
}
// THE COVERAGE RULE FOR THE THREE-TIER FLUSH DRAIN, ASSERTED BEFORE THE CALL AND NOT
// INSIDE IT. FlushPendingRangesFrom is a G5-pinned body (p3a_untouched_regions.sh's
// PINNED_FUNCTIONS, ID-41): the split arm CALLS it, it does not re-spell it, and it
// does not add a line to it either. Its tier-1 arm - the range-invalidating map -
// copies with its own Memcpy and never reaches UploadRangeFrom, so the one tier that
// DECLARES THE OLD BYTES DEAD is the one tier no later check can see; the only place
// left to say "every queued range is inside the staged coverage" is the instant before
// the drain takes the queue. The clamp is the drain's own (limit = the smaller of the
// frontend size and the backend store; bytes past either end have nowhere to land and
// are not this rule's subject), so the two agree about which bytes are meant.
//
// Fires only for a base that IS this resource's server shadow: RequireCoverage
// answers nothing for the legacy arm's MappedData(), which is valid for the whole
// store. `pendingMutex` is taken here and released before the drain takes it again.
void RequireStagedCoverageForPendingRanges(GLESBufferResource& resource, const Uint8* hostBase,
SizeT frontendSize, const char* site) {
if (hostBase == nullptr) return;
const SizeT limit = std::min(frontendSize, resource.storageSize);
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
for (const auto& range : resource.pendingRanges) {
const SizeT end = std::min(range.end, limit);
const SizeT start = std::min(range.start, end);
if (start == end) continue;
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
}
}
// -------------------------------------------------------------------------------
// P11 B2 (T0) - WHEN AN IMPORTED STORE'S AHardwareBuffer MAY BE LET GO.
//
// A T0 store is the client's AHardwareBuffer imported with glBufferStorageExternalEXT
// (Ops_H_ImportExternal below). The twin holds its own reference, and every path that
// ends the store - a respecify, a destroy, a re-import - deletes the GL id and then
// RETIRES the reference here: a fence is inserted after the delete, and the reference is
// released only once that fence has signalled, i.e. once every command that could still
// read or write the pages has completed on this context's GPU. The client's own
// reference ends independently (it releases on respecify/destroy); the pages live until
// the last of the two. No link watermark is involved (F-L2): the fence is this server's.
//
// Swept (non-blocking) at every import, every deferred-release drain and every ensure;
// a context loss releases the lot, because the GPU work those fences named ended with
// the context. Apply thread only, like every GL call here.
// -------------------------------------------------------------------------------
struct T0RetiredImport {
GLsync fence = nullptr;
void* ahb = nullptr;
Uint generation = 0;
};
Vector<T0RetiredImport> g_t0Retired;
// T0 twins whose destroy found no current context: Ops_OnDestroy queued them on the
// deferred-release list and keeps a second reference here, so the reference is
// retired only after that drain deleted the id (ProcessDeferredT0Retires). Guarded by
// g_deferredBufferReleasesMutex, like the list it shadows.
Vector<SharedPtr<BackendBufferResource>> g_t0DeferredDestroys;
std::atomic<Bool> g_hasT0DeferredDestroys{false};
Uint64 g_t0ImportsLive = 0;
void SweepT0Retired(Bool contextGone) {
for (SizeT i = 0; i < g_t0Retired.size();) {
T0RetiredImport& retired = g_t0Retired[i];
const Bool sameContext = !contextGone && retired.generation == g_bufferContextGeneration &&
CanTouchGLNow();
Bool done = contextGone || retired.generation != g_bufferContextGeneration ||
retired.fence == nullptr;
if (!done && sameContext && g_GLESFuncs.glClientWaitSync) {
const GLenum status = g_GLESFuncs.glClientWaitSync(retired.fence, 0, 0);
done = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED ||
status == GL_WAIT_FAILED;
}
if (!done) {
++i;
continue;
}
if (retired.fence != nullptr && sameContext && g_GLESFuncs.glDeleteSync)
g_GLESFuncs.glDeleteSync(retired.fence);
MG_Remote::Transport::AdoptT0::ReleaseImported(retired.ahb);
retired = g_t0Retired.back();
g_t0Retired.pop_back();
}
}
// Call AFTER the store's GL id is deleted (or known dead): the fence then follows every
// command that used it. Clears the twin's reference whatever the context state.
void RetireT0Import(GLESBufferResource& resource) {
if (resource.externalAhb == nullptr) return;
T0RetiredImport retired;
retired.ahb = resource.externalAhb;
retired.generation = resource.contextGeneration;
if (CanTouchGLNow() && resource.contextGeneration == g_bufferContextGeneration &&
g_GLESFuncs.glFenceSync) {
retired.fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
resource.externalAhb = nullptr;
if (g_t0ImportsLive > 0) --g_t0ImportsLive;
g_t0Retired.push_back(retired);
SweepT0Retired(false);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
// The four hostBytes sites read the same in both builds. The non-split expansion is the
// ORIGINAL EXPRESSION, character for character - `raw - offset` - so a push or verify build
// compiles exactly what it compiled before R-11 and the split arm is the only new behaviour.
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
ServerStaged().Adopt(&(res), (width), (bytes), (offset), (size))
#define MGL_SERVER_STAGED_DROP(res) ServerStaged().Drop(&(res))
#define MGL_SERVER_STAGED_DROP_ALL() ServerStaged().DropAll()
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) \
ServerStaged().RequireCoverage(&(res), (base), (start), (end), (site))
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) \
RequireStagedCoverageForPendingRanges((res), (base), (frontendSize), (site))
#else
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
(static_cast<const Uint8*>(bytes) - (offset))
#define MGL_SERVER_STAGED_DROP(res) ((void)0)
#define MGL_SERVER_STAGED_DROP_ALL() ((void)0)
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) ((void)0)
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) ((void)0)
#endif
// The host bytes a range upload/flush reads. On the legacy arm the frontend object's
// shadow; on the handle arm the base the last content-carrying call handed over.
void UploadRangeFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (start >= end || hostBase == nullptr) return;
#if MOBILEGL_BUILD_DISAGGREGATED
MGL_SERVER_STAGED_REQUIRE(resource, hostBase, start, end, "upload_range");
#endif
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start),
hostBase + start);
}
void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) {
UploadRangeFrom(resource, bufferObject.MappedData(), start, end);
}
// Ring machinery shared with the UBO/unpack rings; defined further down in
// this same unnamed namespace.
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset);
Bool RingAvailable(PersistentRing& ring);
// True when a pending-range flush can go through the staging ring right
// now: kill switch off, the ES copy entry point resolved, and the ring's
// own availability gate (EXT_buffer_storage + fences + live context) up.
Bool UploadRingUsableNow() {
if (MG_Config::Features.EsprytDisableUploadRing) return false;
if (!g_GLESFuncs.glCopyBufferSubData) return false;
return RingAvailable(UploadRing());
}
// A partial range below this goes through the staging ring instead of a
// range-invalidating map: the map's page-substitution fast path needs a
// sizeable (page-coverable) range to engage, and below it the driver
// falls back to waiting out the WAR hazard on the CPU.
constexpr SizeT kInvalidateRangeMinBytes = 128u * 1024u;
// The push arm's copy of this threshold lives in Managers.h, where a unit case can
// reach it (InvalidateFlushAccessFor). Two constants, one value, and the compiler
// is what keeps them one: the pull arm's FlushPendingRangesNow is byte-frozen
// against 5cb826b0 (ID-15), so the constant it reads may not move to a header.
static_assert(kInvalidateRangeMinBytes == kEsprytInvalidateRangeMinBytes,
"the two arms of the three-tier ladder must use the same tier-1 threshold");
// Push every queued range of `resource` from the shadow into the backend
// store, without ever letting a driver resolve the WAR hazard against
// in-flight frames at the WHOLE BUFFER's expense. Three tiers:
//
// 1. glMapBufferRange(WRITE | INVALIDATE_RANGE) + memcpy. The entire
// mapped range is rewritten from the authoritative shadow, so
// declaring its old bytes dead is exact - and it lets the driver
// swap fresh pages in for JUST that range. This is the only tier
// whose cost scales with the RANGE on this Mali driver: both the
// immediate glBufferSubData (pre-queueing) and a staged
// glCopyBufferSubData into a busy MUTABLE store ghost the whole
// destination with a worker-thread memcpy - Minecraft 26.3 streams
// ~1MB section meshes into 128MB arenas about nine times a frame
// during a camera pan, and 9 x 128MB of ghosting per frame is
// ~380ms, the measured 2-4 fps. (Backing the arenas with immutable
// stores also kills the ghost, but eagerly commits every arena's
// full extent - +hundreds of MB - which LMK'd the whole device.)
// 2. The staging ring + glCopyBufferSubData: the copy is ordered on
// the GPU timeline, no CPU wait (MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH
// forces this tier as the map path's negative control).
// 3. Direct glBufferSubData (potentially stalling) when neither the
// map entry points nor the ring exist.
//
// The ranges are flushed AS QUEUED (VecRange1D::Add already merges
// near-adjacent ones): bytes, not flush calls, are the cost axis here,
// and collapsing a scattered flush into its union re-copied nearly whole
// chunk-mesh arenas every frame.
// The caller owns syncedChangeSerial; this only drains the queue.
//
// ONE body for both arms, and deliberately so: the three-tier decision (whole-buffer
// orphan map / >= 128 KiB range-invalidating map / staged ring copy) is what the MC
// 26.3 p99 depends on, and a second copy of it for the handle arm is exactly how a
// tier silently changes. The arms differ only in where `hostBase` and `frontendSize`
// come from.
//
// R-11'S PARAMETER, AND IT IS A PARAMETER RATHER THAN AN ASSUMPTION (P5 b1).
// `hostBase` is re-read at every use today precisely because a shadow resize or an
// adoption moves what an earlier base pointed at. Under split the server may hold
// no pointer into the client's shadow at all, so the base becomes a SNAPSHOT taken
// into SEG_STAGE at emission - and a snapshot covers a RANGE, not the store. The
// two extra arguments say which range `hostBase` is good for; the default is the
// whole store, which is exactly what a live shadow is, so every caller today is
// byte-identical. The moment w1 passes a real snapshot extent, tier 1's widening
// refusal below stops being unreachable and a too-narrow snapshot is named instead
// of silently clobbering GPU-written bytes with stale ones.
constexpr SizeT kHostBaseCoversWholeStore = ~static_cast<SizeT>(0);
void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize,
SizeT hostBaseFrom = 0,
SizeT hostBaseTo = kHostBaseCoversWholeStore) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
VecRange1D ranges;
{
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
if (resource.pendingRanges.empty()) return;
ranges = std::move(resource.pendingRanges);
resource.pendingRanges.clear();
}
// No shadow to read from: only reachable on the handle arm, where the base
// arrives with the call, and only for a resource that queued a range before any
// content-carrying call named one. The queue is already drained, so the next
// full re-upload is what puts the store right.
if (hostBase == nullptr) return;
// Clamp against BOTH extents: the readback flush may run while the
// frontend size and the backend store disagree (a pending respecify
// resolves that later; bytes past either end have nowhere to land).
const SizeT limit = std::min(frontendSize, resource.storageSize);
const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush &&
g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer;
const Bool ringUsable = UploadRingUsableNow();
for (const auto& range : ranges) {
const SizeT end = std::min(range.end, limit);
const SizeT start = std::min(range.start, end);
const SizeT size = end - start;
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
// Counted once per queued range, before the three delivery shapes
// below diverge: all three move exactly these bytes, and it is the
// byte count - not the shape - that sizes SEG_STAGE.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
// The invalidating map's fast path is SHAPE-dependent on this Mali
// driver: a whole-buffer invalidation renames the store outright,
// and a large range gets fresh pages - but a small unaligned range
// of a busy store makes the map WAIT (osup_sync_object_wait, ~9%
// of a Minecraft 26.3 replay). So: whole buffer -> orphan-map;
// large range -> range-invalidating map; small range -> the staged
// ring copy, whose worst case (a whole-destination ghost) is only
// ever the small destination itself.
//
// The map covers EXACTLY the queued range: only those bytes are the
// shadow's to rewrite. Widening to page bounds looked free and was
// not - the widened bytes clobbered GPU-written data (an SSBO
// counter beside the app's SubData) with the stale shadow.
// The extent the bytes behind `hostBase` are actually good for, clamped
// to the store. With the default it IS [start, end), so the refusal below
// cannot fire and the ladder is unchanged; with a real SEG_STAGE snapshot
// it is the snapshot's window and a disagreement drops this range to the
// staging ring instead of letting a widened INVALIDATE_RANGE declare bytes
// dead that nothing is about to rewrite.
const SizeT coveredFrom = hostBaseFrom > start ? hostBaseFrom : start;
const SizeT coveredTo = hostBaseTo < end ? hostBaseTo : end;
#if MOBILEGL_PIPE_VERIFY
if (coveredFrom != start || coveredTo != end) {
MGLOG_E_ONCE("MGPipe: Fatal{StageSnapshotTooNarrow} flush_pending_ranges: the "
"staged bytes cover [%zu, %zu) and the queued range is [%zu, %zu) "
"- tiers 2 and 3 would copy from outside the snapshot",
hostBaseFrom, hostBaseTo, start, end);
}
#endif
// Read per range, like the pull arm: a ring copy staged for an earlier range of
// this same drain puts every later partial range of the store behind it
// (InvalidateFlushAccessFor's queued-copy rule, dev f973008c).
const Bool ringCopyInFlight = resource.ringCopyRetireSerial > DirectGLES::CompletedFrameSerial();
const GLbitfield access =
mapUsable ? InvalidateFlushAccessFor(start, end, coveredFrom, coveredTo, limit,
resource.storageSize, ringCopyInFlight)
: 0u;
if (access != 0) {
BindBufferId(TempBufferTarget, resource.id);
void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start,
(GLsizeiptr)size, access);
if (dst) {
Memcpy(dst, hostBase + start, size);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
continue;
}
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
(GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size);
// Commands recorded now retire with the fence the next Present inserts.
resource.ringCopyRetireSerial = DirectGLES::CurrentFrameSerial() + 1;
} else {
UploadRangeFrom(resource, hostBase, start, end);
}
}
}
// THE TWO-ARM SHAPE, AND BOTH ARMS ARE HASHED FROM HERE ON (ID-15, which
// supersedes ID-13's "FlushPendingRangesNow is defined exactly once, outside any
// `#if`").
//
// This file has TWO three-tier drains, and exactly one of them is compiled into any
// given build: `FlushPendingRangesFrom` above, inside `#if MOBILEGL_PIPE_PUSH`, is
// what a PUSH build runs - both of its call sites, the legacy one and
// Ops_H_Readback, reach it - and `FlushPendingRangesNow` inside the `#else` below,
// byte-identical to 5cb826b0, is what a PULL build runs. A push build compiles no
// FlushPendingRangesNow at all.
//
// NO FORWARDER HERE, which is what forced the two-arm shape: G5's extractor is
// preprocessor-blind, so a `FlushPendingRangesNow` that forwarded onto the ladder
// above would leave TWO definitions of that one name in the file and the gate would
// exit 2 - a gate that cannot run - rather than compare anything. (The handle arm
// cannot call FlushPendingRangesNow itself either: its signature takes a
// BufferObject&, and having no frontend object to offer is the whole point of the
// conversion.)
//
// WHAT ID-15 ADDS. The shape above is sound, but a gate keyed on the NAME
// `FlushPendingRangesNow` alone would from here on protect only the text the
// shipping build never compiles: a tier-threshold or map-access-bit edit made in
// FlushPendingRangesFrom - the ladder that actually runs, and the one MC 26.3's p99
// depends on - would satisfy both G1 (the pull text did not move) and G5 (the pull
// name still hashes the same). So scripts/p3a_untouched_regions.sh hashes BOTH
// names: eleven functions, the pull ladder compared against the P3a base ref and
// this one against a sha pinned at 3e298c9a. Neither ladder may drift, and neither
// may drift AWAY FROM THE OTHER without the gate saying so.
// Land the app bytes queued for an ADOPTED store on the GPU timeline: staged
// into the upload ring and delivered by glCopyBufferSubData. The destination
// is the IMMUTABLE persistent store, which the driver can neither rename nor
// ghost, so the copy is plain job ordering - after every in-flight reader,
// before the next consumer - which is exactly glBufferSubData's contract.
// (The in-place host write these bytes replaced tore the frames still
// reading the old vertex data: one-frame wrong geometry during fast camera
// movement.) Fallback: direct glBufferSubData - the adopted store carries
// DYNAMIC_STORAGE, and immutability again forbids the whole-store ghost.
//
// It never read the frontend object (the bytes are on the resource's own queue and
// the extent is the backend store's), so it takes none: the handle arm calls exactly
// this function with exactly these semantics.
void DrainResidentWritesNow(GLESBufferResource& resource) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
Vector<GLESBufferResource::PendingResidentWrite> writes;
{
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
if (resource.pendingResidentWrites.empty()) return;
writes = std::move(resource.pendingResidentWrites);
resource.pendingResidentWrites.clear();
}
const SizeT limit = resource.storageSize;
const Bool ringUsable = UploadRingUsableNow();
for (const auto& write : writes) {
if (write.offset >= limit) continue;
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
(GLintptr)ringOffset, (GLintptr)write.offset,
(GLsizeiptr)size);
} else {
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)write.offset, (GLsizeiptr)size,
write.bytes.data());
}
}
}
// The two-argument spelling the legacy arm's call sites use, kept so those sites
// are the SAME text in both builds (G1). The object was never read.
void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
(void)bufferObject;
DrainResidentWritesNow(resource);
}
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
// tokens); defined locally so this compiles regardless of which GLES headers
// expose the EXT tokens.
constexpr GLbitfield kMapPersistentBit = 0x0040;
constexpr GLbitfield kMapCoherentBit = 0x0080;
constexpr GLbitfield kDynamicStorageBit = 0x0100;
// Zero-copy persistent map: back the buffer with real immutable,
// persistently+coherently mapped GL storage (EXT_buffer_storage) and hand the
// app that mapped pointer (adopted by the frontend PipeResource). Returns
// nullptr when the extension is unavailable or the context is not current, in
// which case the frontend keeps its CPU-shadow model. Idempotent.
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange ||
!g_GLESFuncs.glGenBuffers) {
return nullptr;
}
const SizeT size = bufferObject.GetSize();
if (size == 0) return nullptr;
auto* resource = static_cast<GLESBufferResource*>(bufferObject.GetBackendResource().get());
if (!resource) {
auto created = MakeShared<GLESBufferResource>();
resource = created.get();
bufferObject.SetBackendResource(std::move(created));
}
// Before the generation is stamped, not after: everything on the resource
// describes a context that is gone, and the idempotency check below would
// otherwise hand the caller the dead context's mapped pointer.
if (resource->contextGeneration != g_bufferContextGeneration) {
resource->id = 0;
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
}
resource->contextGeneration = g_bufferContextGeneration;
if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) {
return resource->persistentPtr; // idempotent
}
// Need a fresh id: glBufferStorage fails on a buffer that already has
// immutable storage, and any prior mutable store is replaced anyway.
if (resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have this id baked into attribute/element bindings
// keyed on frontend versions this re-mint does not move.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) return nullptr;
// Seed from the shadow (MappedData() is still the shadow: the frontend
// adopts and drops it only after this returns).
BindBufferId(TempBufferTarget, resource->id);
const void* initial = bufferObject.MappedData();
g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast<GLsizeiptr>(size), initial,
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit |
kDynamicStorageBit);
// Set as soon as the store exists, not once the map succeeds: the failure
// path below leaves this id holding immutable storage, and whoever touches
// it next has to know that glBufferData cannot redefine it.
resource->immutableStorage = true;
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
if (!ptr) {
MGLOG_E_ONCE("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u",
resource->id);
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
return nullptr;
}
resource->persistentPtr = ptr;
resource->persistentMapped = true;
resource->storageSize = size;
resource->storageInitialized = true;
resource->pendingRespecify = false;
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
}
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
return ptr;
}
void Ops_Respecify(BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation
// The frontend hands an adopted mapping back before it redefines the store
// (BufferObject::RedefineStorage), so a resource that still carries the
// persistent state here describes the OLD store - and its storage is
// IMMUTABLE (glBufferStorageEXT), which the glBufferData below cannot
// respecify and which the driver would refuse in silence. Retire the id so
// EnsureBufferResource mints a mutable one, with a full upload from the
// shadow the frontend has just filled.
//
// Keyed on the STORAGE, not on persistentMapped: a glMapBufferRange that
// failed after its glBufferStorageEXT succeeded clears persistentMapped and
// still leaves an immutable store behind, and that one reached glBufferData.
if (resource->immutableStorage) {
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
if (resource->id != 0 && CanTouchGLNow() &&
resource->contextGeneration == g_bufferContextGeneration) {
NoteBufferIdDeleted(resource->id);
// Frontend VAO bindings survive respecification; force their
// backend twins to bind the replacement buffer name.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
// Off the context thread the id cannot be deleted here, and dropping it
// would leak an immutable, persistently mapped store. It stays put, and
// stays flagged, until EnsureBufferResource retires it on the thread
// that owns the context.
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration) {
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
if (bufferObject.GetSize() == 0) {
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = false;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
RespecifyStorageNow(*resource, bufferObject);
}
void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) {
auto* resource = ResourceOf(bufferObject);
if (!resource) return;
if (resource->pendingRespecify) return; // full re-upload pending anyway
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration ||
!StorageMatches(*resource, bufferObject)) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({offset, offset + size});
return;
}
// An adopted zero-copy persistent store already HAS the bytes (the
// frontend wrote them through the coherent mapping); a driver upload
// here would be a self-copy that re-synchronizes what coherent mapping
// made free.
if (resource->persistentMapped && resource->persistentPtr) {
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
return;
}
// An immediate glBufferSubData resolves the WAR hazard against frames
// still referencing this store on the CPU on some drivers - Mali parks
// the thread in osup_sync_object_wait until every referencing job
// retires, which serialized Minecraft 26.3's per-frame UBO/chunk-mesh
// update streams into ~1 fps. Queue the range instead (the shadow
// already holds the bytes) and let draw-time sync push the merged
// ranges through the staging ring.
if (MG_Config::Features.EsprytDisableUploadRing) {
UploadRangeNow(*resource, bufferObject, offset, offset + size);
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
return;
}
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({offset, offset + size});
}
// App bytes for an ADOPTED store: queue them untouched-by-the-mapping; the
// draw-time sync (or a readback) lands them GPU-ordered through
// DrainResidentWritesNow. No GL here, so the op is thread-agnostic.
void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) {
auto* resource = ResourceOf(bufferObject);
if (!resource || data.size == 0) return;
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* bytes = static_cast<const Uint8*>(data.data);
write.bytes.assign(bytes, bytes + data.size);
}
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
auto* resource = ResourceOf(bufferObject);
if (!resource) return;
if (resource->pendingRespecify) return;
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration ||
!StorageMatches(*resource, bufferObject)) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add(range);
return;
}
// An adopted zero-copy persistent store already HAS the bytes: the
// frontend shadow IS the coherent mapping the app (or UploadSubData)
// wrote into, so publishing is free. The self-copy that used to run
// here mapped a buffer this backend keeps persistently mapped (an
// INVALID_OPERATION whose fallback was a WAR-stalling
// glBufferSubData).
if (resource->persistentMapped && resource->persistentPtr) {
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
return;
}
// Same WAR-hazard rule as Ops_SubData: an immediate synchronized upload
// (mapped or glBufferSubData) can park the thread on Mali until the
// frames still referencing this store retire. Queue the range for the
// staged flush at draw-time sync; the negative-control kill switch
// keeps the immediate paths below.
if (!MG_Config::Features.EsprytDisableUploadRing) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add(range);
return;
}
// Honour the app's real mapping flags per call: only reach for a
// mapped upload when the app allowed invalidation/unsynchronized
// access, otherwise a plain glBufferSubData carries the exact
// synchronization semantics.
const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) ||
(appAccess & BufferMappingAccessBit::InvalidateBuffer);
const Bool unsynchronized = static_cast<Bool>(appAccess & BufferMappingAccessBit::Unsynchronized);
if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized)) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
BindBufferId(TempBufferTarget, resource->id);
void* mappedData = g_GLESFuncs.glMapBufferRange(
TempBufferTarget, (GLintptr)range.start, (GLsizeiptr)(range.end - range.start),
GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) |
(unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0));
if (mappedData) {
Memcpy(mappedData, bufferObject.MappedData() + range.start,
range.end - range.start);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
return;
}
MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
resource->id);
}
UploadRangeNow(*resource, bufferObject, range.start, range.end);
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
}
// A shader wrote this buffer through a storage/atomic-counter binding, so the ES
// driver's copy is ahead of the frontend shadow. Pull the whole thing back so
// MapBuffer/GetBufferSubData/CopyBufferSubData see the real results.
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
if (resource->persistentMapped) {
// Queued resident SubData bytes land first (GPU-ordered), then the
// finish makes them - and any shader writes already queued on this
// context - visible through the coherent mapping the reads use.
// There is no backend copy to read back in this case.
DrainResidentWritesNow(*resource, bufferObject);
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
return;
}
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
if (size == 0) return;
// Queued app writes must land in the backend store before it is read
// back, or the writeback below would revert them in the shadow.
// The shared ladder, by ID-11: a push build has no FlushPendingRangesNow (see
// the note at its would-be forwarder), so this arm and the handle arm call the
// one body between them.
FlushPendingRangesFrom(*resource, bufferObject.MappedData(), bufferObject.GetSize());
BindBufferId(TempBufferTarget, resource->id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
return;
}
bufferObject.WritebackFromBackend({mapped, size}, 0);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
// The shadow now matches the backend byte for byte; without this the next
// draw would see a newer change serial and re-upload the readback over it.
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (!resource) return;
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
if (glesResource->contextGeneration != g_bufferContextGeneration) {
glesResource->id = 0; // id belonged to a destroyed context
#if MOBILEGL_BUILD_DISAGGREGATED
RetireT0Import(*glesResource); // P11 B2: its GL store died with that context
#endif
return;
}
if (CanTouchGLNow()) {
if (IsPoolable(*glesResource)) {
EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers
return;
}
if (glesResource->id != 0) {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
InvalidateArrayBufferBindingCache();
}
ScrubBufferBindingShadowsForId(glesResource->id);
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: a T0 store's AHardwareBuffer goes only after a fence behind the delete.
RetireT0Import(*glesResource);
#endif
return;
}
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
#if MOBILEGL_BUILD_DISAGGREGATED
if (glesResource->externalAhb != nullptr) { // P11 B2: retired after the drain deletes it
g_t0DeferredDestroys.push_back(resource);
g_hasT0DeferredDestroys.store(true, std::memory_order_release);
}
#endif
g_deferredBufferReleases.push_back(std::move(resource));
g_hasDeferredBufferReleases.store(true, std::memory_order_release);
}
// Epoch-tracking wrappers: every op bumps the buffer-mutation epoch AFTER
// its impl returns (release; see Managers.h for why the order matters),
// covering every mutation branch inside - including the early returns
// that only queued pendingRanges or flagged pendingRespecify. Bumping on
// an op that turned out to be a no-op merely re-runs the probes once.
void Ops_RespecifyTracked(BufferObject& bufferObject) {
Ops_Respecify(bufferObject);
BumpBufferMutationEpoch();
}
void Ops_SubDataTracked(BufferObject& bufferObject, SizeT offset, SizeT size) {
Ops_SubData(bufferObject, offset, size);
BumpBufferMutationEpoch();
}
void Ops_ResidentSubDataTracked(BufferObject& bufferObject, SizeT offset, DataPtr data) {
Ops_ResidentSubData(bufferObject, offset, data);
BumpBufferMutationEpoch();
}
void Ops_FlushMappedRangeTracked(BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
Ops_FlushMappedRange(bufferObject, range, appAccess);
BumpBufferMutationEpoch();
}
void Ops_OnDestroyTracked(SharedPtr<BackendBufferResource>&& resource) {
Ops_OnDestroy(std::move(resource));
BumpBufferMutationEpoch();
}
void* Ops_AcquirePersistentMapTracked(BufferObject& bufferObject) {
void* result = Ops_AcquirePersistentMap(bufferObject);
// Bump even on decline: the frontend still enters a persistent map the
// per-draw probes must start seeing (IsMapped-driven range pushes).
BumpBufferMutationEpoch();
return result;
}
void Ops_ReadbackFromGpuTracked(BufferObject& bufferObject) {
Ops_ReadbackFromGpu(bufferObject);
BumpBufferMutationEpoch();
}
// ---- P3a: the same seven ops plus the create/unmap pair, BY HANDLE ------------
//
// Every body below is its Ops_* counterpart above with exactly the substitutions
// D-A2's table names and nothing else: the frontend object's GetSize() / GetUsage()
// / HasDefinedContent() / MappedData() / GetChangeSerial() become the applier
// record's Desc.Width / Desc.Usage / Desc.HasDefinedContent, the shadow base the
// call carried, and the record's server-owned Serial. Every branch survives -
// pendingRespecify early-out, the off-thread queue, the adopted zero-copy stamp,
// the upload-ring kill switch, the Mali WAR-stall queue-only default.
//
// NOTHING here reads a frontend type. The one thing a handle op cannot do is ask
// an object for its shadow, which is why GLESBufferResource::hostBytes exists: the
// three content-carrying calls hand the base over and the later drains read it.
// The applier's record for this resource, or null when the client never created it
// (or created it into a slot that has since been recycled).
const MG_Pipe::MGPipeResourceRecord* ResourceRecordOf(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
const auto& records = MG_Pipe::MGPipeApplier().Resources;
if (res.Slot >= records.size()) return nullptr;
const auto& record = records[res.Slot];
if (!record.Live || record.Gen != res.Gen) return nullptr;
return &record;
}
// The server-owned MGGen this backend mirrors in syncedChangeSerial. Zero for a
// resource with no record, which is the same "never synced" answer a frontend
// change serial of zero gave.
Uint64 ResourceSerialOf(MG_Pipe::MGPipeHandle res) {
const auto* record = ResourceRecordOf(res);
return record != nullptr ? record->Serial : 0;
}
SizeT ResourceWidthOf(MG_Pipe::MGPipeHandle res) {
const auto* record = ResourceRecordOf(res);
return record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
}
// ResourceContentIsDeclared MOVED OUT OF THIS ANONYMOUS NAMESPACE (P3b/P4b espryt
// D1 slice 1) to sit beside RequireStagedCoverage at namespace scope: the XFB
// scatter in DirectGLES.cpp has to ask the same M-3 question this file's pool-reuse
// ladder asks, and the two must not be able to answer it differently.
void Ops_H_Create(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) {
(void)res;
(void)desc;
// Nothing, and that is the row D-A2 writes: storage is defined lazily by the
// first resource_respecify, and the ensure path already tolerates a resource
// with none. Minting the twin here would only move the allocation earlier.
}
void Ops_H_Respecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const void* initialBytes) {
auto* resource = FindBufferResourceForHandle(res);
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4a (§1.4): ON MONOLITH'S RECORD ARM THE TWIN IS MINTED HERE, not lazily at
// the first draw. The record arm's readers have no frontend object to re-read a
// base from, so the base this call carries - the client's shadow, for an orphaning
// respecify too (PipeFill passes it there on this arm) - is the only way they learn
// it. Minting is GL-free; the storage stays lazy.
const Bool aliasesFrontend = MG_Config::RecordArmAliasesFrontend();
if (resource == nullptr && aliasesFrontend && initialBytes != nullptr) {
resource = GetOrCreateBufferResourceForHandle(res);
}
#else
constexpr Bool aliasesFrontend = false;
#endif
if (resource != nullptr) {
// A respecify's companion pointer IS the shadow base (offset 0) - and an
// ORPHANING one (glBufferData with NULL) carries HasDefinedContent clear and
// therefore no pointer at all. That is also the call that RESIZES the
// client's shadow (PipeResource::ResizeShadow is reserve + resize, so a grow
// past the reserve reallocates and frees the block a previous call's base
// pointed into), so the recorded base is dead exactly when no new one
// arrives. FORGET it here rather than leave a pointer nothing refreshes:
// every reader below treats a null base as "no bytes to move", which is the
// honest answer, and the ensure path re-reads the live base from the
// frontend object it still holds.
// P13 W4a: an ALIASING store records the base whether or not the content is
// defined - the pointer is the frontend shadow's new allocation either way, and
// the upload below stays gated on the descriptor (RespecifyStorageWith's
// initialData), so an orphan still uploads nothing.
if ((desc.HasDefinedContent != 0 || aliasesFrontend) && initialBytes != nullptr) {
// R-11: in monolith this is the client's shadow base, unchanged; under
// split it is copied into server-owned storage first. A respecify's
// companion pointer is the base at offset 0 and covers the whole store.
// Under split it is ALWAYS null (contract table 1 row 19 -
// initialBytes does not cross, and the content arrives as
// resource_subdata records right behind this record), so in practice
// this arm is the monolith one and the else arm is the split one.
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, static_cast<SizeT>(desc.Width),
initialBytes, 0,
static_cast<SizeT>(desc.Width));
} else {
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-C: whatever a shader wrote belonged to the store this call replaces.
resource->serverGpuWritten = false;
resource->contentSinceRespecify = false;
#endif
}
if (!resource) return; // lazy: the ensure path full-uploads on creation
if (resource->immutableStorage) {
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
if (resource->id != 0 && CanTouchGLNow() &&
resource->contextGeneration == g_bufferContextGeneration) {
NoteBufferIdDeleted(resource->id);
// Frontend VAO bindings survive respecification; force their
// backend twins to bind the replacement buffer name. (dev@d7655247,
// carried into this arm as well: the handle arm duplicates the
// immediate retire path, so a fix that lands in only one of the two
// leaves the bug alive on whichever arm the operator selects.)
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: a respecified T0 store ends here on the server's side too. Its
// AHardwareBuffer is released only after a fence behind the delete (or at once
// when the store's context is already gone) - never under a frame in flight,
// however the client's own release (at this respecify) raced it.
RetireT0Import(*resource);
#endif
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration) {
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
if (desc.Width == 0) {
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = false;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
return;
}
// Same orphaning rule, expressed on the descriptor: a NULL-data respecify has
// HasDefinedContent clear and the client sends no bytes for it.
const void* initialData = desc.HasDefinedContent != 0 ? initialBytes : nullptr;
RespecifyStorageWith(*resource, static_cast<SizeT>(desc.Width),
MG_Util::ConvertBufferUsageToGLEnum(static_cast<BufferUsage>(desc.Usage)),
initialData, ResourceSerialOf(res));
}
void Ops_H_SubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record, const void* bytes) {
const SizeT offset = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferOffset(record));
const SizeT size = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferSize(record));
auto* resource = FindBufferResourceForHandle(res);
#if MOBILEGL_BUILD_DISAGGREGATED
// R-11 / ID-52 item 3: under an ACTIVE TRANSPORT this record's bytes are the ONLY
// delivery of the buffer's content - the client sends no companion pointer and the
// server has no MappedData to fall back on. But the twin is created LAZILY at the
// first draw (Ops_H_Create is a no-op by D-A2), which is AFTER this record, so a
// subdata that finds no twin would return here and the bytes would be lost - the
// draw then uploads an empty/shifted store (TriangleScenario read a blue triangle
// before this). So under split the subdata MINTS the twin it needs to stage into.
// Monolith is untouched: the twin stays lazy there because the frontend object's
// MappedData is the source and nothing is lost by deferring the allocation (D-A2).
if (resource == nullptr && bytes != nullptr &&
MG_Config::DataArmIsRecord()) {
resource = GetOrCreateBufferResourceForHandle(res);
}
#endif
if (!resource) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: A T0 STORE TAKES A SERVER-ORIGINATED WRITE GPU-ORDERED. Its pages are the
// client's AHardwareBuffer, so there is no staged copy to refresh and the adopted
// early return below ("the coherent map already has the bytes") is false for bytes
// the SERVER produced - P9 W1's pack-buffer landing (PipeApplier.cpp
// LandReadbackInBuffer) is the one producer, and it would lose its pixels there. They
// are queued exactly as a resident SubData is (Ops_H_ResidentSubData): landed after
// every command already recorded, before the next one that reads the store.
if (resource->externalAhb != nullptr && bytes != nullptr && size != 0) {
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
// P13 W4b: A SERVER-ORIGINATED WRITE INTO AN ALIASING STORE (monolith's record arm)
// lands in the GL store, never in the base: the bytes are the sink's scratch, and the
// base is the frontend's shadow, which this side does not write. Queued uploads land
// first, so they cannot later overwrite these bytes from the stale shadow; the store
// is then marked GPU-written, which sends the frontend (its own pack-buffer mark) and
// every CPU reader here (SplitHostBytesForCpuRead) back to the GL store for them.
if (MG_Pipe::MGPipeApplyingServerOriginatedWrite() && MG_Config::RecordArmAliasesFrontend() &&
bytes != nullptr && size != 0) {
// The store must exist to take them: a pack buffer is often never drawn from, so
// nothing else has built it. Only a store that does not exist yet is ensured: the
// applier has already counted THIS write into the record's serial, so the ensure
// of a built store would read it as an untracked mutation and re-upload the
// shadow over the runs this landing wrote before it.
if (CanTouchGLNow() && (resource->id == 0 || !resource->storageInitialized ||
resource->pendingRespecify ||
resource->contextGeneration != g_bufferContextGeneration)) {
resource = EnsureBufferResourceForHandle(nullptr, res);
}
if (resource != nullptr && CanTouchGLNow() && resource->id != 0 && resource->storageInitialized &&
!resource->persistentMapped && !resource->pendingRespecify &&
resource->contextGeneration == g_bufferContextGeneration &&
StorageMatchesSize(*resource, ResourceWidthOf(res))) {
FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res));
BindBufferId(TempBufferTarget, resource->id);
g_GLESFuncs.glBufferSubData(TempBufferTarget, static_cast<GLintptr>(offset),
static_cast<GLsizeiptr>(size), bytes);
// The store holds everything the record now says (the applier bumped its
// serial for this write): without the stamp the next ensure would see a
// newer serial and re-upload the shadow over the bytes that just landed.
resource->syncedChangeSerial = ResourceSerialOf(res);
} else if (resource != nullptr) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
if (resource != nullptr) resource->serverGpuWritten = true;
return;
}
#endif
// M-2: UNDER pendingMutex, because this line runs BEFORE the CanTouchGLNow()
// test below - i.e. on the arm D-A2 deliberately keeps reachable off the render
// thread - while every reader of hostBytes (Ops_H_Readback's drain, the
// kill-switch map arm of Ops_H_FlushRange, the ensure path, the fp64 narrowing)
// is on the render thread. It was the one member of this struct that the
// off-thread path touched with neither a lock nor an atomic: pendingRanges and
// pendingResidentWrites are under this mutex and syncedChangeSerial is an
// std::atomic read with acquire. The lock it is put under is deliberately THAT
// one and not a new one - the base and the queued range are one fact ("these
// bytes, at this base"), the drain takes this mutex to lift the ranges, and a
// drain that sees a range therefore sees the base that range was queued
// against. The store is kept on both arms rather than restricted to the
// CanTouchGLNow() one because Ops_H_Readback can drain with no draw in between,
// and the ensure path's republication (the 3c55e027 fix) is what runs at a draw.
if (bytes != nullptr) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
// R-11: MONOLITH keeps the client's base; SPLIT copies [offset, offset+size)
// into server-owned storage and points hostBytes at that. Inside the SAME
// lock as the queued range, because the base and the range are one fact
// ("these bytes, at this base") and the drain takes this mutex to lift them.
resource->hostBytes =
MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size);
#if MOBILEGL_BUILD_DISAGGREGATED
if (size != 0) resource->contentSinceRespecify = true;
#endif
}
if (resource->pendingRespecify) return; // full re-upload pending anyway
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration ||
!StorageMatchesSize(*resource, ResourceWidthOf(res))) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({offset, offset + size});
return;
}
// The adopted zero-copy store already HAS the bytes; a driver upload here would
// re-synchronize what coherent mapping made free.
if (resource->persistentMapped && resource->persistentPtr) {
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
if (MG_Config::Features.EsprytDisableUploadRing) {
UploadRangeFrom(*resource, resource->hostBytes, offset, offset + size);
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
// The Mali WAR-stall fix, unchanged: queue and let draw-time sync stage the
// merged ranges through the upload ring.
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({offset, offset + size});
}
void Ops_H_ResidentSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes) {
const SizeT offset = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferOffset(record));
const SizeT size = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferSize(record));
auto* resource = FindBufferResourceForHandle(res);
if (!resource || size == 0 || bytes == nullptr) return;
// DELIBERATELY not stored as hostBytes: these are the application's staging
// store and are valid for the duration of the call only (BufferObject.h:84-85),
// which is exactly why they are COPIED here rather than referenced later.
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
void Ops_H_FlushRange(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record,
const void* bytes) {
auto* resource = FindBufferResourceForHandle(res);
if (!resource) return;
const SizeT start = static_cast<SizeT>(record.Offset);
const SizeT end = start + static_cast<SizeT>(record.Size);
// M-2, the second store site - same lock, same reason as Ops_H_SubData's, and
// the same R-11 copy. UNDER SPLIT `bytes` IS ALWAYS NULL HERE by ruling (C-6 /
// contract table 1 row 20: resource_flush_range carries no bytes at all - the
// ladder it drives rewrites its range from the authoritative shadow, which rule
// C makes server-owned, so resource_subdata is already the only way bytes reach
// it and a second carrier would be a forgeable way to say the same thing). So
// this arm keeps whatever the preceding subdata records staged, which is
// exactly what the ladder must read.
if (bytes != nullptr) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes,
start, end - start);
#if MOBILEGL_BUILD_DISAGGREGATED
if (end > start) resource->contentSinceRespecify = true;
#endif
}
if (resource->pendingRespecify) return;
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration ||
!StorageMatchesSize(*resource, ResourceWidthOf(res))) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({start, end});
return;
}
if (resource->persistentMapped && resource->persistentPtr) {
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
if (!MG_Config::Features.EsprytDisableUploadRing) {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.Add({start, end});
return;
}
// The kill-switch arm, and it reads the application's REAL flags per call -
// which is why MGPFlushRange carries them unnormalised.
const Flags<BufferMappingAccessBit> appAccess{
static_cast<std::underlying_type_t<BufferMappingAccessBit>>(record.AccessFlags)};
const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) ||
(appAccess & BufferMappingAccessBit::InvalidateBuffer);
const Bool unsynchronized = static_cast<Bool>(appAccess & BufferMappingAccessBit::Unsynchronized);
if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized) &&
resource->hostBytes != nullptr) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// The kill-switch arm's own Memcpy, and the one that passes
// GL_MAP_INVALIDATE_RANGE_BIT - i.e. the one that tells the driver the old
// bytes are dead. Bytes outside the staged coverage are exactly the ones
// that claim is false for.
MGL_SERVER_STAGED_REQUIRE(*resource, resource->hostBytes, start, end,
"flush_range_invalidate_map");
#endif
BindBufferId(TempBufferTarget, resource->id);
void* mappedData = g_GLESFuncs.glMapBufferRange(
TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start),
GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) |
(unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0));
if (mappedData) {
Memcpy(mappedData, resource->hostBytes + start, end - start);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
resource->id);
}
UploadRangeFrom(*resource, resource->hostBytes, start, end);
resource->syncedChangeSerial = ResourceSerialOf(res);
}
void Ops_H_Readback(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) {
#if MOBILEGL_BUILD_DISAGGREGATED
// An empty capture may conservatively request a writeback before
// any draw materialized this buffer. Resolve its staged resource
// exactly as a GPU consumer would, then return the real bytes.
auto* resource = MG_Config::DataArmIsRecord()
? EnsureBufferResourceForHandle(nullptr, res) : FindBufferResourceForHandle(res);
#else
auto* resource = FindBufferResourceForHandle(res);
#endif
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
if (resource->persistentMapped) {
// Queued resident SubData bytes land first (GPU-ordered), then the finish
// makes them - and any shader writes already queued on this context -
// visible through the coherent mapping the reads use. There is no backend
// copy to read back in this case.
DrainResidentWritesNow(*resource);
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
return;
}
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
// The map below starts at record.Offset where the legacy arm mapped at literal
// zero, so the clamp has to be against what is left of the BACKEND store past
// that offset: the applier's BufferRangeFault checked the range against
// Desc.Width, and a store that is smaller than the descriptor (a respecify the
// backend has not applied yet) would otherwise be mapped past its end.
const SizeT readOffset = static_cast<SizeT>(record.Offset);
if (readOffset >= resource->storageSize) return;
const SizeT size =
std::min<SizeT>(static_cast<SizeT>(record.Size), resource->storageSize - readOffset);
if (size == 0) return;
// Queued app writes must land in the backend store before it is read back, or
// the writeback below would revert them in the shadow. Under split every queued
// range must lie inside the server shadow's staged coverage first (R-11 / the
// M-6 ruling): the drain's tier-1 map would otherwise declare live GPU bytes
// dead over a range nothing staged.
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, resource->hostBytes, ResourceWidthOf(res),
"readback_flush_pending");
FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res));
BindBufferId(TempBufferTarget, resource->id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)record.Offset,
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("Ops_H_Readback: glMapBufferRange(read) failed for buffer %u", resource->id);
return;
}
// The reverse channel replaces BufferObject::WritebackFromBackend: the backend
// no longer reaches into the frontend's address space, it ANSWERS. In monolith
// the client's implementation is one call away, so SyncGpuWrites' caller still
// sees the reconciled shadow on return, exactly as before.
//
// THE ORDER FROM HERE IS A CORRECTNESS RULE, NOT A PREFERENCE
// (ARCHITECTURE.md 8.2: "写回的 epoch bump 必须在任何后续读该 handle 的命令之前被
// server 应用;反向通道需要与正向通道相同的有序保证"):
// 1. writeback - the client's shadow takes the bytes,
// 2. unmap - the read mapping goes away,
// 3. serial stamp- syncedChangeSerial catches up with the record, so the next
// draw does not re-upload the readback over itself,
// 4. epoch bump - in Ops_H_ReadbackTracked, i.e. strictly AFTER 1-3, so a
// draw-clean memo re-probed by the bump can never observe a
// half-reconciled resource. The bump must NEVER move earlier.
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
res, record.Offset,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(mapped), static_cast<Uint64>(size),
MG_Pipe::kMGHostSpanSegNone, 0});
} else {
MGLOG_E_ONCE("Ops_H_Readback: no reverse channel is installed, so the GPU-written bytes of "
"buffer %u cannot reach the client shadow",
resource->id);
}
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
// The shadow now matches the backend byte for byte; without this the next draw
// would see a newer serial and re-upload the readback over it.
resource->syncedChangeSerial = ResourceSerialOf(res);
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4a: on monolith's record arm the base this twin reads IS the shadow the
// writeback just filled, so a whole-store readback is also the refresh
// SplitHostBytesForCpuRead would owe. (A copying store's copy is not the
// client's shadow, and keeps its mark.)
if (MG_Config::RecordArmAliasesFrontend() && readOffset == 0 && size >= ResourceWidthOf(res)) {
resource->serverGpuWritten = false;
}
#endif
}
void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) {
// R-11's server copy dies with the resource, and BEFORE the twin leaves the
// table - the side map is keyed by the twin's address, so this is the last
// moment that address can be looked up.
if (GLESBufferResource* dying = FindBufferResourceForHandle(res); dying != nullptr) {
MGL_SERVER_STAGED_DROP(*dying);
}
// The twin comes OUT of the table first, so the three outcomes below are
// reached with the entry already retired. The SLOT is the client's to free,
// after this returns (D-L).
SharedPtr<BackendBufferResource> twin = g_backendBufferResources.ReleaseByHandle(res);
// Verbatim Ops_OnDestroy: stale generation -> zero the id; on-thread ->
// IsPoolable/EnrollIntoPool else scrub + glDeleteBuffers; off-thread ->
// g_deferredBufferReleases plus the lock-free flag.
Ops_OnDestroy(std::move(twin));
}
// D-E: the ONLY two changes are the signature and the three reads that went
// through the frontend object (GetSize() -> size, MappedData() -> seedBytes,
// SetBackendResource(...) -> the slot table's GetOrCreate). Every other statement
// is byte-identical to Ops_AcquirePersistentMap above - the four-way capability
// gate, the stale-generation wipe BEFORE the new stamp, the idempotency hit, the
// fresh-id sequence with ++g_bufferBackendIdGeneration, immutableStorage set as
// soon as the store exists, the MGLOG_E_ONCE decline and the success stamps.
void* Ops_H_MapPersistent(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) {
if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange ||
!g_GLESFuncs.glGenBuffers) {
return nullptr;
}
if (size == 0) return nullptr;
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (!resource) return nullptr;
// Before the generation is stamped, not after: everything on the resource
// describes a context that is gone, and the idempotency check below would
// otherwise hand the caller the dead context's mapped pointer.
if (resource->contextGeneration != g_bufferContextGeneration) {
resource->id = 0;
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
}
resource->contextGeneration = g_bufferContextGeneration;
if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) {
return resource->persistentPtr; // idempotent
}
// Need a fresh id: glBufferStorage fails on a buffer that already has
// immutable storage, and any prior mutable store is replaced anyway.
if (resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have this id baked into attribute/element bindings
// keyed on versions this re-mint does not move.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) return nullptr;
// Seed from the shadow (the client's bytes are still live at this point: it
// adopts and drops them only after this returns).
BindBufferId(TempBufferTarget, resource->id);
const void* initial = seedBytes;
g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast<GLsizeiptr>(size), initial,
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit |
kDynamicStorageBit);
// Set as soon as the store exists, not once the map succeeds: the failure
// path below leaves this id holding immutable storage, and whoever touches
// it next has to know that glBufferData cannot redefine it.
resource->immutableStorage = true;
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
if (!ptr) {
MGLOG_E_ONCE("Ops_H_MapPersistent: glMapBufferRange(persistent) failed for buffer %u",
resource->id);
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
return nullptr;
}
resource->persistentPtr = ptr;
resource->persistentMapped = true;
// The ONE statement here that is not in Ops_AcquirePersistentMap, and it is
// about a member the legacy arm does not have rather than about the map: the
// client adopts this pointer the instant we return it and
// PipeResource::AdoptPersistentMap then does m_shadow->clear() +
// shrink_to_fit(), so the shadow base any earlier content-carrying call
// recorded is a FREED allocation from here on. The live bytes are persistentPtr.
// R-11's server copy goes with it: the bytes are the coherent map's now, and a
// stale server shadow would answer a later drain with pre-map content.
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
resource->storageSize = static_cast<SizeT>(size);
resource->storageInitialized = true;
resource->pendingRespecify = false;
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
}
resource->syncedChangeSerial = ResourceSerialOf(res);
return ptr;
}
void Ops_H_UnmapPersistent(MG_Pipe::MGPipeHandle res) {
(void)res;
// P3a emits this from nowhere: the donation is permanent for the life of the
// store and is ended by the respecify / destroy paths, which already retire
// the immutable id. The entry exists so the transport has both halves of the
// pair (D-E), and giving it a body that unmapped a live coherent store would
// be the one thing D-B4 forbids.
}
// The epoch-tracking wrappers, duplicated for this table - same contract as the
// seven above, including AcquirePersistentMap's bump EVEN ON DECLINE (the client
// still enters a persistent map the per-draw probes must start seeing).
void Ops_H_RespecifyTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const void* initialBytes) {
Ops_H_Respecify(res, desc, initialBytes);
BumpBufferMutationEpoch();
}
void Ops_H_SubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes) {
Ops_H_SubData(res, record, bytes);
BumpBufferMutationEpoch();
}
void Ops_H_ResidentSubDataTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes) {
Ops_H_ResidentSubData(res, record, bytes);
BumpBufferMutationEpoch();
}
void Ops_H_FlushRangeTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPFlushRange& record,
const void* bytes) {
Ops_H_FlushRange(res, record, bytes);
BumpBufferMutationEpoch();
}
void Ops_H_ReadbackTracked(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPReadback& record) {
Ops_H_Readback(res, record);
BumpBufferMutationEpoch();
}
void Ops_H_DestroyTracked(MG_Pipe::MGPipeHandle res) {
Ops_H_Destroy(res);
BumpBufferMutationEpoch();
}
void* Ops_H_MapPersistentTracked(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes) {
void* result = Ops_H_MapPersistent(res, size, seedBytes);
// Bump even on decline: the client still enters a persistent map the per-draw
// probes must start seeing.
BumpBufferMutationEpoch();
return result;
}
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A STORE'S RESIDENT TWIN ------------
//
// The split twin of Ops_H_MapPersistent: where a monolith map_persistent mints a
// coherent persistent store (glBufferStorageEXT) and hands its pointer to the frontend,
// a T0 map_persistent IMPORTS the pages the client already holds and maps -
// glBufferStorageExternalEXT on eglGetNativeClientBufferANDROID(ahb) - and the result
// is the SAME resource shape (immutable, persistentMapped, persistentPtr = this
// server's coherent map of those pages), so every adopted-store path in this file
// applies unchanged: draws bind it with nothing to upload, resident SubData lands
// GPU-ordered (DrainResidentWritesNow), a readback drains and waits and posts no
// bytes. The R-11 staged copy is dropped: the pages are the client's.
//
// THE ENTRY POINTS ARE OPTIONAL: neither is in the loader's tables (an EGL function
// missing there is MGLOG_F), and eglGetProcAddress hands back live-looking stubs, so
// the extension strings decide and the pointers are only then taken.
using T0GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
using T0BufferStorageExternalFn = void (*)(GLenum, GLintptr, GLsizeiptr, EGLClientBuffer, GLbitfield);
T0GetNativeClientBufferFn g_t0GetNativeClientBuffer = nullptr;
T0BufferStorageExternalFn g_t0BufferStorageExternal = nullptr;
constexpr GLbitfield kT0MapReadBit = 0x0001;
constexpr GLbitfield kT0MapWriteBit = 0x0002;
constexpr GLbitfield kT0StorageFlags =
kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit | kDynamicStorageBit;
constexpr GLbitfield kT0MapFlags = kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit;
using MG_Util::BackendLoader::HasExtensionWord;
Bool ResolveT0EntryPoints(String& why) {
if (g_t0GetNativeClientBuffer != nullptr && g_t0BufferStorageExternal != nullptr) return true;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
!g_GLESFuncs.glGetString || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers) {
why = "the loader has no eglGetProcAddress/eglQueryString/glGetString/glMapBufferRange";
return false;
}
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
why = "EGL_ANDROID_get_native_client_buffer is not advertised";
return false;
}
if (!HasExtensionWord(gl, "GL_EXT_external_buffer") || !HasExtensionWord(gl, "GL_EXT_buffer_storage")) {
why = "GL_EXT_external_buffer / GL_EXT_buffer_storage is not advertised";
return false;
}
g_t0GetNativeClientBuffer = reinterpret_cast<T0GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
g_t0BufferStorageExternal = reinterpret_cast<T0BufferStorageExternalFn>(
g_EGLFuncs.eglGetProcAddress("glBufferStorageExternalEXT"));
if (g_t0GetNativeClientBuffer == nullptr || g_t0BufferStorageExternal == nullptr) {
why = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / glBufferStorageExternalEXT";
g_t0GetNativeClientBuffer = nullptr;
g_t0BufferStorageExternal = nullptr;
return false;
}
return true;
}
void DrainGLErrors() {
if (!g_GLESFuncs.glGetError) return;
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
}
}
// A fresh GL buffer whose storage IS `ahb`, mapped persistent + coherent. 0 on failure
// (with `why`); nothing on the resource is touched here. `throwaway` = the POST's own
// context is current: bind raw and keep Espryt's binding caches (which describe the
// session's context, whose name space this is not) out of it.
Uint ImportAhbAsBuffer(void* ahb, Uint64 size, void** outPtr, String& why, Bool throwaway = false) {
*outPtr = nullptr;
const EGLClientBuffer clientBuffer =
g_t0GetNativeClientBuffer(static_cast<const struct AHardwareBuffer*>(ahb));
if (clientBuffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return 0;
}
Uint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id == 0) {
why = "glGenBuffers gave no name";
return 0;
}
DrainGLErrors();
if (throwaway) g_GLESFuncs.glBindBuffer(TempBufferTarget, id);
else BindBufferId(TempBufferTarget, id);
g_t0BufferStorageExternal(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), clientBuffer,
kT0StorageFlags);
const GLenum stored = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
void* ptr = stored == GL_NO_ERROR
? g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), kT0MapFlags)
: nullptr;
if (ptr == nullptr) {
char text[160];
std::snprintf(text, sizeof(text), "glBufferStorageExternalEXT -> 0x%04x, map %s", stored,
stored == GL_NO_ERROR ? "failed" : "not attempted");
why = text;
if (!throwaway) {
NoteBufferIdDeleted(id);
++g_bufferBackendIdGeneration;
}
g_GLESFuncs.glDeleteBuffers(1, &id);
return 0;
}
*outPtr = ptr;
return id;
}
Bool T0Answer(char* out, Uint64 outBytes, Bool ok, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
std::vsnprintf(out, static_cast<SizeT>(outBytes), fmt, args);
va_end(args);
return ok;
}
// Installs an imported store on the twin. The caller has retired whatever was there.
void InstallT0Store(GLESBufferResource& resource, Uint id, void* ptr, Uint64 size) {
resource.id = id;
resource.immutableStorage = true;
resource.persistentPtr = ptr;
resource.persistentMapped = true;
resource.storageSize = static_cast<SizeT>(size);
resource.storageInitialized = true;
resource.pendingRespecify = false;
resource.contextGeneration = g_bufferContextGeneration;
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.pendingRanges.clear();
resource.pendingResidentWrites.clear();
}
Bool Ops_H_ImportExternal(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
SweepT0Retired(false);
if (!CanTouchGLNow() || size == 0 || ahb == nullptr) return false;
String why;
if (!ResolveT0EntryPoints(why)) {
MGLOG_W_ONCE("Espryt T0: no import on this context - %s", why.c_str());
return false;
}
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (resource == nullptr) return false;
// IMPORT FIRST, retire second: a refused import leaves the T2 store (and its staged
// copy) exactly as it was, so the DECLINED the client gets leaves nothing to repair.
void* ptr = nullptr;
const Uint id = ImportAhbAsBuffer(ahb, size, &ptr, why);
if (id == 0) {
MGLOG_W("Espryt T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen,
static_cast<unsigned long long>(size), why.c_str());
return false;
}
if (resource->contextGeneration == g_bufferContextGeneration && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have the old id baked into bindings keyed on versions this
// replacement does not move (the Ops_H_MapPersistent re-mint's rule).
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
}
resource->id = 0;
RetireT0Import(*resource); // a previous import of this handle, fenced
InstallT0Store(*resource, id, ptr, size);
MG_Remote::Transport::AdoptT0::AcquireImported(ahb);
resource->externalAhb = ahb;
++g_t0ImportsLive;
// The bytes are the client's pages now; R-11's copy would answer a later drain
// with pre-import content.
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
resource->syncedChangeSerial = ResourceSerialOf(res);
MGLOG_D("Espryt T0: imported {slot=%u, gen=%u} as buffer %u (%llu bytes); %llu live imports",
res.Slot, res.Gen, id, static_cast<unsigned long long>(size),
static_cast<unsigned long long>(g_t0ImportsLive));
return true;
}
Bool Ops_H_ImportExternalTracked(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
const Bool imported = Ops_H_ImportExternal(res, ahb, size);
BumpBufferMutationEpoch();
return imported;
}
// The POST's second round: the CPU writes region 1 through the held lock only now, after
// the import and a GPU use of it, and reads region 4 (the first round's fill) back into
// its cache; a second dispatch copies region 1 out and overwrites region 4. Appends its
// counts to `detail`.
Bool SelfTestExternalSteady(GLuint imported, GLuint out, void* heldPtr, char* detail, SizeT detailBytes) {
constexpr Uint32 kWords = 1024;
constexpr Uint32 kSteadyAtWord = kWords; // region 1
constexpr Uint32 kFillAtWord = 4 * kWords; // region 4
auto* held = static_cast<volatile Uint32*>(heldPtr);
Uint32 cached = 0;
for (Uint32 i = 0; i < kWords; ++i) {
held[kSteadyAtWord + i] = 0xA5000000u ^ (i * 40503u);
cached ^= held[kFillAtWord + i];
}
(void)cached;
static const char* kSource =
"#version 310 es\n"
"layout(local_size_x = 64) in;\n"
"layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n"
"layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n"
"void main() {\n"
" uint i = gl_GlobalInvocationID.x;\n"
" outb.w[i] = ext.w[1024u + i];\n"
" ext.w[4096u + i] = 0x3D5EA11Eu ^ i;\n"
"}\n";
const GLuint program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource);
GLint linked = 0;
if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
const SizeT used = std::strlen(detail);
if (!linked) {
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
std::snprintf(detail + used, detailBytes - used, "; steady: the second program did not link");
return false;
}
g_GLESFuncs.glUseProgram(program);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out);
g_GLESFuncs.glMemoryBarrier(0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT |
0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glFinish();
Uint32 steadyRead = 0, steadySaw = 0;
for (Uint32 i = 0; i < kWords; ++i)
if (held[kFillAtWord + i] == (0x3D5EA11Eu ^ i)) ++steadySaw;
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
const auto* copied = static_cast<const Uint32*>(
g_GLESFuncs.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT));
if (copied != nullptr) {
for (Uint32 i = 0; i < kWords; ++i)
if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead;
g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
}
g_GLESFuncs.glDeleteProgram(program);
std::snprintf(detail + used, detailBytes - used,
"; with the import in use, GPU read %u/%u words the CPU wrote after it, GPU write "
"seen through the held lock %u/%u words",
steadyRead, kWords, steadySaw, kWords);
return steadyRead == kWords && steadySaw == kWords;
}
// THE POST (B2, the standing rule: decide by probe, never by driver name). The AHB API
// promises CPU coherence only across an unlock/lock; T0 keeps the client's lock for the
// store's life, so the pattern it depends on is exactly what this exercises, on this
// context's own driver, before kCapAdoptT0 may be published:
// hold - a 64 KiB BLOB allocated and locked by this test, never unlocked while it runs;
// read - this server's own host map of the import reads the pattern the CPU wrote
// through the held pointer (every hostBytes fallback reads persistentPtr);
// gpu - a compute dispatch copies that pattern out of the import (the GPU read) and
// fills another region of it (the GPU write);
// back - after glFinish the fill is read through the HELD pointer and the copy is
// read back from the dispatch's output;
// steady - the same again with bytes the CPU wrote AFTER the import and its first GPU
// use (VkBufferManager::SelfTestWireImport says why the first round is not enough).
// In a throwaway ES 3.1 pbuffer context, so none of this context's state or Espryt's
// binding caches is disturbed; the caller's bind is restored before returning.
Bool Ops_H_SelfTestExternal(char* whyOut, Uint64 whyBytes) {
const auto answer = [&](Bool ok, const char* text, const char* arg = "") {
return T0Answer(whyOut, whyBytes, ok, text, arg);
};
String why;
if (!ResolveT0EntryPoints(why)) return answer(false, "%s", why.c_str());
if (!g_EGLFuncs.eglChooseConfig || !g_EGLFuncs.eglCreateContext || !g_EGLFuncs.eglMakeCurrent ||
!g_EGLFuncs.eglCreatePbufferSurface || !g_GLESFuncs.glCreateShaderProgramv ||
!g_GLESFuncs.glDispatchCompute || !g_GLESFuncs.glMemoryBarrier || !g_GLESFuncs.glFinish)
return answer(false, "the loader lacks the EGL/GLES 3.1 entry points the test needs");
const EGLDisplay display = g_EGLFuncs.eglGetCurrentDisplay();
const EGLContext savedContext = g_EGLFuncs.eglGetCurrentContext();
const EGLSurface savedDraw = g_EGLFuncs.eglGetCurrentSurface(EGL_DRAW);
const EGLSurface savedRead = g_EGLFuncs.eglGetCurrentSurface(EGL_READ);
const EGLint configAttribs[] = {EGL_RENDERABLE_TYPE, 0x0040 /*EGL_OPENGL_ES3_BIT*/, EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT, EGL_NONE};
EGLConfig config = nullptr;
EGLint configs = 0;
if (!g_EGLFuncs.eglChooseConfig(display, configAttribs, &config, 1, &configs) || configs < 1)
return answer(false, "no ES3 pbuffer config for the throwaway context");
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(display, config, surfaceAttribs);
const EGLint contextAttribs[] = {0x3098 /*EGL_CONTEXT_MAJOR_VERSION*/, 3, 0x30FB /*MINOR*/, 1, EGL_NONE};
const EGLContext context = surface != EGL_NO_SURFACE
? g_EGLFuncs.eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs)
: EGL_NO_CONTEXT;
const auto restore = [&] {
g_EGLFuncs.eglMakeCurrent(display, savedDraw, savedRead, savedContext);
if (context != EGL_NO_CONTEXT) g_EGLFuncs.eglDestroyContext(display, context);
if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(display, surface);
};
if (context == EGL_NO_CONTEXT || !g_EGLFuncs.eglMakeCurrent(display, surface, surface, context)) {
restore();
return answer(false, "the throwaway ES 3.1 context could not be made current");
}
constexpr Uint64 kBytes = 64 * 1024;
constexpr Uint32 kWords = 1024; // one 4 KiB region
constexpr Uint32 kFillAtWord = 4 * kWords; // region 4
MG_Remote::Transport::AdoptT0::HeldStore held;
Vector<Uint32> pattern(kBytes / 4);
for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast<Uint32>(i * 2654435761u);
if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why)) {
restore();
return answer(false, "hold: %s", why.c_str());
}
Bool ok = false;
GLuint out = 0, program = 0;
void* hostMap = nullptr;
const GLuint imported = ImportAhbAsBuffer(held.ahb, kBytes, &hostMap, why, /*throwaway=*/true);
char detail[512] = {};
if (imported == 0) {
std::snprintf(detail, sizeof(detail), "import: %s", why.c_str());
} else if (std::memcmp(hostMap, pattern.data(), kWords * 4) != 0) {
std::snprintf(detail, sizeof(detail), "read: the server's host map of the import does not "
"hold the CPU's pattern");
} else {
static const char* kSource =
"#version 310 es\n"
"layout(local_size_x = 64) in;\n"
"layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n"
"layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n"
"void main() {\n"
" uint i = gl_GlobalInvocationID.x;\n"
" outb.w[i] = ext.w[i];\n"
" ext.w[4096u + i] = 0x6C0FFEE0u ^ i;\n"
"}\n";
program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource);
GLint linked = 0;
if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
g_GLESFuncs.glGenBuffers(1, &out);
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
g_GLESFuncs.glBufferData(GL_SHADER_STORAGE_BUFFER, kWords * 4, nullptr, GL_DYNAMIC_READ);
if (!linked) {
std::snprintf(detail, sizeof(detail), "gpu: the test's compute program did not link");
} else {
g_GLESFuncs.glUseProgram(program);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out);
g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT |
0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glFinish();
Uint32 gpuRead = 0, heldSaw = 0;
const auto* heldWords = static_cast<const Uint32*>(held.ptr);
for (Uint32 i = 0; i < kWords; ++i)
if (heldWords[kFillAtWord + i] == (0x6C0FFEE0u ^ i)) ++heldSaw;
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
const auto* copied = static_cast<const Uint32*>(g_GLESFuncs.glMapBufferRange(
GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT));
if (copied != nullptr) {
for (Uint32 i = 0; i < kWords; ++i)
if (copied[i] == pattern[i]) ++gpuRead;
g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
}
ok = gpuRead == kWords && heldSaw == kWords;
std::snprintf(detail, sizeof(detail),
"64 KiB AHB, lock held throughout: host map read ok, GPU read %u/%u words of "
"the CPU's pattern, GPU write seen through the held lock %u/%u words",
gpuRead, kWords, heldSaw, kWords);
if (ok) ok = SelfTestExternalSteady(imported, out, held.ptr, detail, sizeof(detail));
}
}
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
if (out != 0) g_GLESFuncs.glDeleteBuffers(1, &out);
if (imported != 0) g_GLESFuncs.glDeleteBuffers(1, &imported);
g_GLESFuncs.glFinish();
restore();
MG_Remote::Transport::AdoptT0::ReleaseHeld(held);
return answer(ok, "%s", detail);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5c (tx): the TEXTURE half of the resource family, staged server-side --------
//
// The buffer half's R-11 pattern (ServerStaged() above), for texture levels: the
// staged bytes of resource_subdata's texture half are adopted into the server's
// StagedTextureStore AT APPLY TIME, because `bytes` names SEG_STAGE and is dead the
// moment the record retires (rule C). The store, its ownership and its coverage
// rules are documented in MG_Remote/Server/StagedTextureStore.h; these are only the
// three hook bodies. All three are no-ops in monolith (CopiesIntoServerStorage()
// false), which is what keeps the monolith expression character for character.
//
// NOTHING here mints or reads a texture twin: the store is keyed by the wire handle
// the record carried (StagedTextureStore.h explains why not the twin address), so
// adoption needs no GL call and no frontend object.
// GetUploadTargets() answered from the descriptor's Target: one static list per
// target, matching the frontend classes' own lists member for member
// (TextureObject*.h / TextureObjectStubs.h: a cube is six faces, a cube array is
// the single CubeMapArray target, everything else its own one).
const Vector<TextureUploadTarget>& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget) {
static const Vector<TextureUploadTarget> kUnknown{};
static const Vector<TextureUploadTarget> kTex1D{TextureUploadTarget::Texture1D};
static const Vector<TextureUploadTarget> kTex2D{TextureUploadTarget::Texture2D};
static const Vector<TextureUploadTarget> kTex3D{TextureUploadTarget::Texture3D};
static const Vector<TextureUploadTarget> kTex1DArray{TextureUploadTarget::Texture1DArray};
static const Vector<TextureUploadTarget> kTex2DArray{TextureUploadTarget::Texture2DArray};
static const Vector<TextureUploadTarget> kTexCube{
TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ};
static const Vector<TextureUploadTarget> kTexCubeArray{TextureUploadTarget::CubeMapArray};
static const Vector<TextureUploadTarget> kTex2DMS{TextureUploadTarget::Texture2DMultisample};
static const Vector<TextureUploadTarget> kTex2DMSArray{TextureUploadTarget::Texture2DMultisampleArray};
static const Vector<TextureUploadTarget> kTexRect{TextureUploadTarget::TextureRectangle};
static const Vector<TextureUploadTarget> kTexBuffer{TextureUploadTarget::TextureBuffer};
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D: return kTex1D;
case MG_Pipe::MGPipeResourceTarget::Tex2D: return kTex2D;
case MG_Pipe::MGPipeResourceTarget::Tex3D: return kTex3D;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return kTex1DArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return kTex2DArray;
case MG_Pipe::MGPipeResourceTarget::TexCube: return kTexCube;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return kTexCubeArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return kTex2DMS;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return kTex2DMSArray;
case MG_Pipe::MGPipeResourceTarget::TexRect: return kTexRect;
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return kTexBuffer;
default: return kUnknown;
}
}
// P5e (ID-124): OUT OF THE DISAGGREGATED BLOCK ON PURPOSE. This is a pure
// MGPipeResourceTarget -> TextureTarget switch with no wire, no record and no role in it, but
// it sat inside the nested `#if MOBILEGL_BUILD_DISAGGREGATED` below while its only caller,
// MGB_TEXPARAM_TARGET, is guarded by `#if MOBILEGL_PIPE_PUSH` alone - so a push build without
// disaggregation did not compile. The narrower fix (guarding the macro block too) is wrong:
// that block also carries TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push
// flavour does need. Caught by the `flavours` gate step; see ID-124.
#endif // MOBILEGL_BUILD_DISAGGREGATED
// The inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the sync's
// target reads (ConvertTextureTargetToBackendGLEnum and MapToBackendTextureTarget
// both want the frontend enum).
TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget) {
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D;
case MG_Pipe::MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D;
case MG_Pipe::MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray;
case MG_Pipe::MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray;
case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle;
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer;
default: return TextureTarget::Unknown;
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
void Ops_H_TextureSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes, const MG_Pipe::MGPSubRegion* regions) {
// The region set is the upload planner's shape and stays in the applier's
// pending set; the store's coverage is the staged run itself
// (StagedTextureStore.h's coverage ruling). It is ALSO what places that run in
// the level image when the record carries a piece of the level rather than the
// whole of it (fix A2), which is why it is read here now.
auto& store = MG_Remote::Server::ServerStagedTexture();
// P13 W4b: an aliasing store (monolith's record arm) adopts too - it records where
// the run sits in the frontend's level shadow instead of copying it.
if (!store.Holds()) return;
// The applier's gate has already faulted every shape that reaches here without
// bytes, and under split the codec declared the run's length (Blob.Size) -
// TextureEmit.h's "the bytes this record declares ARE the level shadow".
if (bytes == nullptr || record.Blob.Size == 0) return;
const auto* stored = PipeTextureRecordForHandle(res);
if (stored == nullptr) return;
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target);
const IntVec3 extent = MG_Remote::Server::StagedTextureUploadExtent(stored->Desc, record);
const SizeT runBytes = static_cast<SizeT>(record.Blob.Size);
// P8-E (notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW
// (FollowCopyImageInStagedStore). The run is the client's shadow, stale everywhere it
// did not write, so only the texels it names land - its regions, or its union box
// when it carries none (D-D3's "the box is the whole story"). A run the store cannot
// place that way is adopted as ever, and the store stops standing for the texture.
if (store.LevelHoldsFollowedCopy(key, uploadTarget, record.Level) &&
(record.RegionCount == 0 || regions != nullptr)) {
Vector<MG_Remote::Server::StagedTextureStore::ClientTexelBox> boxes;
if (record.RegionCount == 0) {
const auto& box = record.UnionBox;
boxes.push_back({box.X, box.Y, box.Z, box.W, box.H, box.D});
} else {
for (Uint32 i = 0; i < record.RegionCount; ++i) {
const auto& region = regions[i];
boxes.push_back({region.X, region.Y, region.Z, region.W, region.H, region.D});
}
}
const Uint64 imageOffset = record.RegionCount == 0
? 0 : MG_Remote::Server::StagedTextureRunImageOffset(record, regions);
if (store.AdoptClientBoxes(key, uploadTarget, record.Level, extent, imageOffset, bytes, runBytes,
boxes.data(), boxes.size())) {
return;
}
store.MarkDriverWritten(key);
}
// RegionCount == 0 IS the whole-level spelling - "the run is the level shadow",
// beginning at the level's first byte - so it is adopted with the spelling that
// replaces the level; every other record names a RUN of the level by its own box.
// A client record's run: under monolith's record arm it IS the frontend level shadow,
// the one source an aliasing store keeps by address.
if (record.RegionCount == 0) {
store.Adopt(key, uploadTarget, record.Level, extent, bytes, runBytes, /*frontendShadow=*/true);
return;
}
store.AdoptRun(key, uploadTarget, record.Level, extent,
MG_Remote::Server::StagedTextureRunImageOffset(record, regions), bytes,
runBytes, /*frontendShadow=*/true);
}
void Ops_H_TextureRespecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const MG_Pipe::MGPRespecifiedLevel* level) {
auto& store = MG_Remote::Server::ServerStagedTexture();
if (!store.Holds()) return;
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
if (level != nullptr) {
// ONE glTexImage*D redefined one level: its exact extent crossed with the
// accepted scope carrier, including a legal noncanonical mutable mip.
store.NoteLevelDefined(
key, MG_Pipe::MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level,
IntVec3{static_cast<Int>(level->Width), static_cast<Int>(level->Height),
static_cast<Int>(level->Depth)},
desc.Target, desc.InternalFormat);
return;
}
// A whole-resource redefinition: every level's old coordinate system is gone.
// glTexStorage* then defines the WHOLE chain at once (GL 4.6 core 8.19 - all six
// cube faces included), so an immutable descriptor re-marks every level of every
// upload target; a mutable whole-resource respecify (a texture view) defines
// nothing here and the store simply forgets the old levels.
store.ResetLevels(key);
if (desc.Immutable == 0 || desc.Levels == 0) return;
for (const auto& uploadTarget : StagedUploadTargetsForPipeTarget(desc.Target)) {
for (Uint32 levelIndex = 0; levelIndex < desc.Levels; ++levelIndex) {
store.NoteLevelDefined(
key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(levelIndex),
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, levelIndex),
desc.Target, desc.InternalFormat);
}
}
}
void Ops_H_TextureDestroy(MG_Pipe::MGPipeHandle res) {
// Deliberately NOT gated on CopiesIntoServerStorage(): Drop's own m_any gate
// makes the monolith call one acquire load, and an unconditional drop cannot
// strand a key the latch state was misread for.
MG_Remote::Server::ServerStagedTexture().Drop(
MG_Remote::Server::StagedTextureStore::KeyForHandle(res));
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
const MG_Pipe::MGPipeResourceOps g_glesResourceOps = {
.Create = Ops_H_Create,
.Respecify = Ops_H_RespecifyTracked,
.SubData = Ops_H_SubDataTracked,
.SubDataResident = Ops_H_ResidentSubDataTracked,
.FlushRange = Ops_H_FlushRangeTracked,
.Readback = Ops_H_ReadbackTracked,
.Destroy = Ops_H_DestroyTracked,
.MapPersistent = Ops_H_MapPersistentTracked,
.UnmapPersistent = Ops_H_UnmapPersistent,
#if MOBILEGL_BUILD_DISAGGREGATED
.TextureSubData = Ops_H_TextureSubData,
.TextureRespecify = Ops_H_TextureRespecify,
.TextureDestroy = Ops_H_TextureDestroy,
#if defined(__ANDROID__)
// P11 B2 (T0). Android only: nowhere else is there an AHardwareBuffer to import.
.ImportExternal = Ops_H_ImportExternalTracked,
.SelfTestExternal = Ops_H_SelfTestExternal,
#endif
#endif
};
const BufferBackendOps g_glesBufferBackendOps = {
.Respecify = Ops_RespecifyTracked,
.SubData = Ops_SubDataTracked,
.ResidentSubData = Ops_ResidentSubDataTracked,
.FlushMappedRange = Ops_FlushMappedRangeTracked,
.OnDestroy = Ops_OnDestroyTracked,
.AcquirePersistentMap = Ops_AcquirePersistentMapTracked,
.ReadbackFromGpu = Ops_ReadbackFromGpuTracked,
};
} // namespace
Uint64 CurrentBufferMutationEpoch() {
return g_bufferMutationEpoch.load(std::memory_order_acquire);
}
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
// P5e (vi): how many times BumpBufferMutationEpoch ran off the apply thread while
// one was running, with a live transport. See the note in the bump itself; the
// accessor is what a test can assert zero on instead of reading a call graph.
std::atomic<Uint64> g_bufferMutationEpochBumpsOffTheApplyThread{0};
} // namespace
Uint64 BufferMutationEpochBumpsOffTheApplyThread() {
return g_bufferMutationEpochBumpsOffTheApplyThread.load(std::memory_order_relaxed);
}
#endif
void BumpBufferMutationEpoch() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (vi), CONTRACT-P5E §5.1's first pin (scout S1 R2, BRIEF-P5E's open question 2):
// "does any GL-THREAD path bump this under a transport". PINNED HERE RATHER THAN
// ARGUED, because the argument is the kind that is true until someone adds a caller.
//
// It matters because this counter is the ONE non-serial input left in the record
// arm's clean gate: SyncVaoAttributeBuffersByRecord stamps the pre-pass value and
// skips every IsBufferDrawCleanByHandle probe while the stamp holds. A bump from the
// GL thread would make that stamp a cross-thread read of a value the applier never
// observed - the memo would go on reading clean over buffers the client had just
// dirtied - and under run-ahead the client is a frame ahead, so the window is a
// frame wide rather than a record wide.
//
// THE MEASURED ANSWER, and it is not the clean "no" the scout expected: there is
// EXACTLY ONE class of non-apply-thread bump under a transport, and it happens
// BEFORE THERE IS AN APPLY THREAD AT ALL. BackendObject_DirectGLES::Initialize()
// runs on the app thread by design ("the backend object is BUILT on the app thread
// while its context is never OWNED by it", ServerLoop.h) and calls
// RegisterBufferBackendOps(), whose last act is a bump. An unguarded refusal aborts
// the whole split lane in EGL bring-up, which is how this was found rather than
// reasoned.
//
// That class is harmless and the apply-thread key tells it apart from the one that
// would not be: with g_applyThreadKey == 0 no record has been applied, so no twin
// exists, so no memo holds a stamp for the bump to slide under.
//
// WHAT IS COUNTED AND WHAT IS FATAL, and why they are not the same thing. Every
// bump taken off the apply thread WHILE ONE IS RUNNING is counted and named once,
// and is Fatal UNDER THE STRICT LANE - which is where this phase's pins live, and
// which is the lane the answer is claimed for. It is not Fatal by default because
// the unit fixtures that drive the production op tables directly
// (StagedShadowProductionTest / ServerLoopTest, which register the real tables and
// call them from the test thread beside a started loop) are the server role without
// being the apply thread, and taking those down would be this pin policing a
// fixture rather than the draw path.
const Uint64 applyThreadKey =
MG_Remote::Server::Detail::g_applyThreadKey.load(std::memory_order_relaxed);
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && applyThreadKey != 0 &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
g_bufferMutationEpochBumpsOffTheApplyThread.fetch_add(1, std::memory_order_relaxed);
if (MG_Config::Ipc.StrictErrors) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"CurrentBufferMutationEpoch\"} - the "
"buffer-mutation epoch was bumped off the apply thread while an apply "
"thread was running, with a live transport. The draw-path memos stamp "
"this counter and skip their clean probes while the stamp holds "
"(CONTRACT-P5E §5.1), so a producer on the client's thread means the "
"backend is reachable from a thread that owns none of this state");
std::abort();
}
MGLOG_E_ONCE("MGPipe: the buffer-mutation epoch was bumped off the apply thread "
"with a live transport (CONTRACT-P5E §5.1's pin). Run the strict lane "
"to make this fatal at the producer");
}
#endif
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
}
// The seventh slot table. A process-lifetime global, like the other six, so it links
// itself into its own holder list from its own constructor with no initialisation
// order question to answer (SlotTables.h).
BackendBufferResourceTable g_backendBufferResources;
// The two P3a families get the three-way treatment ClassifyEsprytSlotArm established for
// bit 5, rather than reading their bit and stopping there: "the bit is clear AND the
// legacy arm has been taken away" is a real configuration an operator can ask for, and a
// family that only reads its own bit answers it by running the very arm that was
// disabled, in silence. The two families differ in whether that third verdict is
// REACHABLE, which is why the classifier takes it as a parameter rather than assuming it.
//
// A STOP, exactly like ResolveEsprytSlotTablesArm's NoArm, and M-4 is the promotion.
//
// It was a warning while the premise for the warning held: bits 7 and 8 were clear in
// every `.Handles` itest lane, which pinned MOBILEGL_PIPE_PUSH=0x7f beside
// MOBILEGL_PIPE_LEGACY_MEMOS=0, so aborting would have turned that lane red for a
// mis-pinned control rather than for a defect. That premise is gone: gates' M1 re-pinned
// MGL_ITEST_HANDLES_ARM_KNOBS to "MOBILEGL_PIPE_LEGACY_MEMOS=0" + "MOBILEGL_PIPE_PUSH=0x1ff"
// (MG_IntegrationTest/CMakeLists.txt), and the four CSO lanes went to 0x1ff /
// 0x800000000000_01ff, which is contract-review item 11 closed for all three lane
// families. The only lanes that set LEGACY_MEMOS=0 now carry an explicit vertex-input
// arm, so an armless verdict can no longer be a lane's own configuration - it can only
// be an operator who asked for both arms to be gone.
//
// Warning-and-continue is the wrong answer to that, for ResolveEsprytSlotTablesArm's
// reason verbatim: continuing runs the very arm the operator disabled and hands back a
// result measured on it, which is exactly the lever HandleRecycleScenario's arms are
// selected with, so a mis-set A/B would be scored silently against the wrong arm
// (ARCHITECTURE.md 9.6). Resolved lazily at the first use, not at bring-up, so the stop
// lands in a scenario body where ctest reports it rather than inside eglMakeCurrent
// where the harness reads an abort as "no usable GPU" and skips.
//
// This closes espryt D14 / closure row 7 and, with it, row 6's "LEGACY_MEMOS=0 +
// PUSH=0x7f must name a verdict": that combination now names NoArm and stops.
enum class PipeSubsystemArmVerdict { Handles, Legacy, NoArm };
// The one voice for an armless verdict, so the two families cannot disagree about what
// it means or about whether it stops. `what` names the family and the bit.
[[noreturn]] void StopOnArmlessPipeSubsystem(const char* what) {
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled, \"%s, so there is no arm to run\"}", what);
std::abort();
}
PipeSubsystemArmVerdict ClassifyPipeSubsystemArm(Bool subsystemBitSet, Bool legacyMemosEnabled,
Bool legacyArmSurvivesLegacyMemos) {
if (subsystemBitSet) return PipeSubsystemArmVerdict::Handles;
if (legacyArmSurvivesLegacyMemos || legacyMemosEnabled) return PipeSubsystemArmVerdict::Legacy;
return PipeSubsystemArmVerdict::NoArm;
}
Bool ResolveResourceSubsystemArm() {
const Bool bitSet = (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemResources) != 0;
// The buffer family's legacy arm is the Ops_* table and g_glesBufferBackendOps, which
// are compiled UNCONDITIONALLY - only the VAO twin's memos and the pre-handle VAO
// body live under MOBILEGL_PIPE_LEGACY_MEMOS - so this family cannot be left armless
// and NoArm is unreachable for it. Said out loud rather than assumed, because the
// knob is still answered below.
const PipeSubsystemArmVerdict verdict =
ClassifyPipeSubsystemArm(bitSet, /*legacyMemosEnabled=*/true,
/*legacyArmSurvivesLegacyMemos=*/true);
// Unreachable for this family by the argument above, and stated as a stop anyway:
// the two P3a families answer an armless verdict the same way, and an unreachable
// branch that says something different is how the reachable one drifts.
if (verdict == PipeSubsystemArmVerdict::NoArm) {
StopOnArmlessPipeSubsystem("MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemResources "
"(bit 7) clear and the pre-handle buffer arm is gone");
}
const Bool enabled = verdict == PipeSubsystemArmVerdict::Handles;
MGLOG_D("MGPipe: Espryt resource family runs the %s arm", enabled ? "handle" : "legacy");
return enabled;
}
Bool ResolveVertexInputSubsystemArm() {
const Uint64 mask = MG_Config::Features.PipePush;
const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemVertexInput) != 0;
// The vertex-input family's row, READ FROM MG_Pipe/SubsystemDeps.def (P3b/P4b R-5,
// switched over by wave 2-D package D3) rather than restated as the hand-rolled
// `bitSet && !resourcesBitSet` this function used to carry - one of the six statements
// of D-K2's rule that R-5 found with nothing comparing them. The table's own sentence
// is what the refusal prints. The mirror pair (bit 7 set, bit 8 clear) is fine - the
// legacy VAO walk calls BindAttributeBuffer -> EnsureBufferResource, which dispatches
// to the handle arm by itself - and lives with the other mirror pairs in MGPipe.h.
if (bitSet && !MG_Pipe::MGPipeSubsystemDependenciesAreSet(MG_Pipe::kMGPipeSubsystemVertexInput, mask)) {
MGLOG_E("MGPipe: kMGPipeSubsystemVertexInput (bit 8) is set but MOBILEGL_PIPE_PUSH="
"0x%llx does not carry every bit MG_Pipe/SubsystemDeps.def says it requires "
"(requires 0x%llx, missing 0x%llx): %s - REFUSING bit 8 and running the "
"legacy vertex-input arm. Set every bit of the row, or clear bit 8",
static_cast<unsigned long long>(mask),
static_cast<unsigned long long>(
MG_Pipe::MGPipeSubsystemRequires(MG_Pipe::kMGPipeSubsystemVertexInput)),
static_cast<unsigned long long>(
MG_Pipe::MGPipeSubsystemRequires(MG_Pipe::kMGPipeSubsystemVertexInput) & ~mask),
MG_Pipe::MGPipeSubsystemDependencyWhy(MG_Pipe::kMGPipeSubsystemVertexInput));
return false;
}
constexpr Bool kLegacyVaoArmCompiled = false;
// Unlike the buffer family, this one's legacy arm IS conditional: the pre-handle
// SyncToBackend body and the twin memos it reads are inside MOBILEGL_PIPE_LEGACY_MEMOS.
const PipeSubsystemArmVerdict verdict =
ClassifyPipeSubsystemArm(bitSet, /*legacyMemosEnabled=*/true,
/*legacyArmSurvivesLegacyMemos=*/false);
if (verdict != PipeSubsystemArmVerdict::Handles &&
(verdict == PipeSubsystemArmVerdict::NoArm || !kLegacyVaoArmCompiled)) {
// M-4: A STOP, not a log line. Continuing here left the process drawing through
// an UNCONFIGURED driver VAO - every attribute pointer whatever the last owner
// of that VAO name set it to - after one MGLOG_E that a lane summary does not
// read. The lanes that pin MOBILEGL_PIPE_LEGACY_MEMOS=0 all carry an explicit
// 0x1ff now, so this verdict is an operator's configuration and nothing else.
StopOnArmlessPipeSubsystem(
kLegacyVaoArmCompiled
? "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemVertexInput (bit 8) clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle VAO sync"
: "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemVertexInput (bit 8) clear and "
"the pre-handle VAO sync is not compiled into this build");
}
const Bool enabled = verdict == PipeSubsystemArmVerdict::Handles;
MGLOG_D("MGPipe: Espryt vertex-input family runs the %s arm", enabled ? "handle" : "legacy");
return enabled;
}
GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
// The table refuses both of these itself; this is the release-build VOICE for the
// refusal, because MOBILEGL_ASSERT compiles out at INFO and a resource that silently
// stops being twinned is the failure mode the refusal exists to replace.
if (res.Slot >= BackendBufferResourceTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"buffer twin request named slot %u, past this table's %u bound",
res.Slot, BackendBufferResourceTable::kMaxHandleSlot);
#else
MGLOG_E_ONCE("MGPipe: resource handle slot %u is past the backend table's %u bound - "
"refusing to twin it",
res.Slot, BackendBufferResourceTable::kMaxHandleSlot);
return nullptr;
#endif
}
const Uint32 liveGen = g_backendBufferResources.LiveGenAt(res.Slot);
if (liveGen != 0 && liveGen > res.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"buffer twin request named generation %u at slot %u, behind live generation %u",
res.Gen, res.Slot, liveGen);
#else
MGLOG_E_ONCE("MGPipe: resource handle {%u, %u} names a generation BEHIND the live twin's "
"%u - refusing rather than dropping the incumbent's driver storage",
res.Slot, res.Gen, liveGen);
return nullptr;
#endif
}
auto& twin = g_backendBufferResources.GetOrCreate(res);
if (!twin) {
twin = MakeShared<GLESBufferResource>();
// Same seed the lazy legacy path gives a resource it has just minted: nothing
// has defined storage yet, so the first sync owes a full (re)specification.
twin->pendingRespecify = true;
}
return twin.get();
}
GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res) {
auto* twin = g_backendBufferResources.FindByHandle(res);
return twin ? twin->get() : nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
SizeT end, const char* site) {
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
}
// M-3's rule for the WHOLE-STORE readers (v1 round 3). The descriptor is the
// application's own statement about the store: HasDefinedContent set means it SUPPLIED
// the content (glBufferData(size, data)), which under split arrives as resource_subdata
// records behind the respecify (table 1 row 19) - so a coverage gap at the draw is a
// MISSING RECORD and the zero-fill past the coverage is not the application's bytes.
// Clear means it ORPHANED the store (glBufferData(size, NULL), glBufferStorage(NULL)):
// every byte it has not staged since is UNDEFINED by its own declaration, the streaming
// idiom (orphan, partial glBufferSubData, draw) is the ordinary case, and uploading the
// shadow's zero-fill for the rest is exactly what the monolith arm uploads from
// MappedData(). Round 2 refused both shapes and aborted six LargeArenaAdoption /
// ResourceSubsystemControl entries on the joint by name (v1-v3.md 6).
//
// AT NAMESPACE SCOPE, not in the anonymous namespace it was written in (P3b/P4b espryt
// D1 slice 1): the XFB scatter guards ITS whole-store read with the same question, and
// a second spelling of "did the application declare these bytes" is how the pool-reuse
// ladder and the scatter end up disagreeing about one buffer.
Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res) {
const auto* record = ResourceRecordOf(res);
return record != nullptr && record->Desc.HasDefinedContent != 0;
}
// P13 W4a (§1.4): THE GPU-WRITE REFRESH OF AN ALIASING STORE. Monolith's record arm reads
// the frontend shadow in place (StagedShadowStore keeps the caller's pointer), so a store a
// shader wrote is refreshed the way the client refreshes it under split: the bytes are
// read back from this process's GL buffer and handed to OnBufferWriteback, whose consumer
// (the frontend, one call away) copies them into that shadow. The backend never writes
// frontend memory itself. Ops_H_Readback's order, for its reasons: queued uploads first,
// resident writes next, then the read, the serial stamp and - last - the epoch bump.
const Uint8* RefreshAliasedStoreFromGpu(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
const char* site) {
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized ||
resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration ||
!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) {
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read "
"back here, so the CPU reads the shadow as it is",
site, res.Slot, res.Gen);
return SplitHostBytes(resource);
}
const SizeT width = ResourceWidthOf(res);
const SizeT size = std::min(width, resource.storageSize);
if (size == 0) return SplitHostBytes(resource);
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU and no reverse channel is "
"installed to refresh the shadow it aliases",
site, res.Slot, res.Gen);
return SplitHostBytes(resource);
}
FlushPendingRangesFrom(resource, resource.hostBytes, width);
DrainResidentWritesNow(resource);
BindBufferId(TempBufferTarget, resource.id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the "
"shadow as it is",
site, resource.id);
return SplitHostBytes(resource);
}
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
res, 0,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(mapped), static_cast<Uint64>(size),
MG_Pipe::kMGHostSpanSegNone, 0});
if (resource.hostBytes == nullptr) {
// Every content-carrying record and every respecify of a store with a size hands
// its base over on this arm, so no base here means a shape nothing recorded. Keep
// the bytes in a private copy rather than answer "nothing" for GPU-written bytes.
MGLOG_W_ONCE("%s: GPU-written buffer {%u, %u} has no recorded shadow base; its refreshed "
"bytes are kept in a private copy",
site, res.Slot, res.Gen);
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.hostBytes = ServerStaged().AdoptPrivateCopy(&resource, width, mapped, size);
}
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
resource.serverGpuWritten = false;
resource.syncedChangeSerial = ResourceSerialOf(res);
BumpBufferMutationEpoch();
return resource.hostBytes;
}
// P8-C. The declaration says what; this says in what order, and it is Ops_H_Readback's
// order because it is the same question asked by the server of itself instead of by the
// client: (1) queued uploads land in the GL store first - reading back past them would
// revert them in the copy - under the same staged-coverage assertion that drain carries;
// (2) queued resident writes land (GPU-ordered, behind every command already recorded);
// (3) the whole store is read back into the staged copy, which leaves its coverage whole
// and so also answers RequireStagedCoverage for an orphaned store's first copy; (4) the
// serial stamp and the epoch bump, as Ops_H_ReadbackTracked does them.
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
const char* site) {
// A coherent store - T0's import, or (P13 W4a) a map monolith's record arm donated -
// has no copy: its bytes are read through the map once queued writes have landed.
const Bool coherent = resource.persistentMapped && resource.persistentPtr != nullptr;
Bool residentWritesQueued = false;
if (coherent) {
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
residentWritesQueued = !resource.pendingResidentWrites.empty();
}
if (!resource.serverGpuWritten && !residentWritesQueued) return SplitHostBytes(resource);
if (!ServerStaged().CopiesIntoServerStorage() && !coherent) {
// P13 W4a (§1.4): an ALIASING store (monolith's record arm) is refreshed through
// the frontend - the writeback lands in the shadow the base points into. Any other
// non-copying store has no marks to answer.
if (MG_Config::RecordArmAliasesFrontend()) return RefreshAliasedStoreFromGpu(resource, res, site);
return SplitHostBytes(resource);
}
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized ||
resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration ||
!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) {
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read "
"back here, so the CPU reads the staged copy as it is",
site, res.Slot, res.Gen);
return SplitHostBytes(resource);
}
if (coherent) {
// T0: the copy IS this server's coherent map of the client's pages. Landing the
// queued writes and finishing makes them, and every shader write already queued,
// visible through it - Ops_H_Readback's persistent-map arm, for the same reason.
DrainResidentWritesNow(resource);
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
resource.serverGpuWritten = false;
return SplitHostBytes(resource);
}
const SizeT width = ResourceWidthOf(res);
const SizeT size = std::min(width, resource.storageSize);
if (size == 0) return SplitHostBytes(resource);
MGL_SERVER_STAGED_REQUIRE_PENDING(resource, resource.hostBytes, width, "gpu_written_refresh_flush_pending");
FlushPendingRangesFrom(resource, resource.hostBytes, width);
DrainResidentWritesNow(resource);
BindBufferId(TempBufferTarget, resource.id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the "
"staged copy as it is",
site, resource.id);
return SplitHostBytes(resource);
}
{
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, width, mapped, 0, size);
}
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
resource.serverGpuWritten = false;
resource.syncedChangeSerial = ResourceSerialOf(res);
BumpBufferMutationEpoch();
MGLOG_D("%s: refreshed the staged copy of GPU-written buffer {%u, %u} (%zu bytes)", site, res.Slot,
res.Gen, size);
return resource.hostBytes;
}
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
SizeT offset, SizeT size) {
if (bytes == nullptr || size == 0 || resource.externalAhb != nullptr) return;
// Only a COPYING store may take bytes that die with a mapping: the monolith one keeps
// the caller's pointer (StagedShadow.h), which here would dangle after the unmap.
if (!ServerStaged().CopiesIntoServerStorage()) return;
// Under pendingMutex, as every other hostBytes store is (M-2).
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size);
}
#endif
MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) {
if (bufferObject == nullptr) return MG_Pipe::kMGPipeNullHandle;
// Through the table's own single-entry front memo rather than straight into
// MGPipeSlots().FindByLifetimeId, which is a hash lookup: this runs inside
// EnsureBufferResource (every SSBO / UBO / atomic / XFB / PBO / IBO bind), inside
// IsBufferDrawClean, and once per SSBO per draw through MarkBufferGpuWritten, where
// the legacy arm paid a pointer compare. HandleOf answers identically - the same
// allocator probe on a miss - and remembers the answer, which the minting overload
// was previously the only writer of. A null answer is deliberately never memoised
// (see the comment at HandleOf).
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c merge coordination (CONTRACT-P5C §3.1's named exemption): every caller of
// this function is a site whose handle-carrying records (set_shader_buffers /
// set_stream_output_targets) the client does not emit until sb, so the probe
// below runs inside the scoped exemption. The scope, not a silent guard removal,
// is what keeps the debt named and greppable.
//
// P5e (id), ruling 12: THE SCOPE CHANGED UNDER THIS SITE AND THE SITE HAD TO MOVE.
// The exemption it used to name is now MagmaP7AllocatorDebtScope and is keyed on a
// DirectVulkan server (Magma's four P7 debts); this is Espryt's, and it belongs to
// the frontend-keyed registry family with the rest of the Espryt debt. Which scope
// it names is not cosmetic: the Magma one would not exempt it here at all. It
// retires when vi/sb carry the handle into EnsureBufferResourceForHandle /
// IsBufferDrawCleanByHandle / MarkBufferGpuWrittenByHandle, which is the last
// caller set this function has.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
return g_backendBufferResources.HandleOf(bufferObject);
}
SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); }
Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res) { return ResourceSerialOf(res); }
Uint CurrentBufferContextGeneration() { return g_bufferContextGeneration; }
void MarkBufferGpuWritten(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
if (!bufferObject) return;
if (!ResourceSubsystemEnabled()) {
bufferObject->MarkGpuWritten();
return;
}
const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get());
if (MG_Pipe::MGPipeHandleIsNull(res) || MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr) {
// Deliberately NOT a fall-back to MarkGpuWritten: on this arm the resource
// family is switched over, and quietly reaching into the frontend object again
// would hide a missing handle or a missing reverse channel behind a picture
// that still looks right - which is what the subsystem A/B exists to expose.
MGLOG_E_ONCE("MGPipe: no reverse channel for the GPU-write announcement of buffer %u "
"(handle %s, OnGpuWritten %s)",
bufferObject->GetExternalIndex(),
MG_Pipe::MGPipeHandleIsNull(res) ? "missing" : "present",
MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr ? "unset" : "set");
return;
}
// WHOLE RESOURCE, stated rather than implied: the extent is spelled as one range of
// kMGPipeWholeBuffer rather than as "zero ranges", because a zero count is the
// shape a fully NARROWED announcement will legitimately have once P8/P9 build the
// client's conservative set, and the two must not be the same record.
//
// THE OTHER HALF OF THE CONTRACT, which the range shape alone does not say: what
// this replaces, BufferObject::MarkGpuWritten, sets BOTH m_hasDefinedContent and
// m_gpuWritePending. The client's OnGpuWritten must set both too, or the next
// ResourceRespecify carries HasDefinedContent = 0, Ops_H_Respecify orphans instead
// of uploading, and the shader-written contents are dropped with nothing saying so.
const MG_Pipe::MGPRange whole{0, MG_Pipe::kMGPipeWholeBuffer};
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &whole);
}
// See the declaration: re-mints of a live resource's driver id. Written only on
// the context thread (all re-mint sites run there), read only by the VAO sync.
Uint64 g_bufferBackendIdGeneration = 0;
void RegisterBufferBackendOps() {
MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);
// P3a: the handle-shaped table goes up beside it, at the same two bring-up sites
// and with the same lifetime. Registration is UNCONDITIONAL, exactly as
// BufferBackendOps' is: the subsystem bit is the FRONTEND's dispatch predicate
// (MGPipeResourceSubsystemEnabled reads the bit AND this table's presence), so a
// build with bit 7 clear registers a table nobody calls and the A/B stays a pure
// configuration question rather than a bring-up-order one.
MG_Pipe::MGPipeSetResourceOps(&g_glesResourceOps);
// Frontend writes issued while ops were unregistered advanced change
// serials with no per-op bump; re-open every draw-clean memo. Advanced directly,
// past BumpBufferMutationEpoch's apply-thread pin: a registration mutates no buffer
// and only makes every memo probe again, and with several sessions in one process
// a new session's backend registers on its own thread while another session's
// apply thread is already running - which the pin would otherwise read as a
// client-thread producer and, under the strict lane, abort the server for.
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
}
void UnregisterBufferBackendOps() {
if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) {
MG_State::GLState::SetBufferBackendOps(nullptr);
}
if (MG_Pipe::MGPipeGetResourceOps() == &g_glesResourceOps) {
MG_Pipe::MGPipeSetResourceOps(nullptr);
}
// From here on frontend writes bypass the tracked ops entirely.
BumpBufferMutationEpoch();
InvalidateArrayBufferBindingCache();
// Pooled ids belong to the dying context too; drop them without glDeleteBuffers.
ClearBufferPool();
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
// The ES context owning these ids is going away; just drop the handles.
g_deferredBufferReleases.clear();
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
}
void OnServerSessionEndedWithOthersLive() {
// Only what belongs to the ending session: its staged bytes (both stores are
// session-scoped). The rings, the ops table and the buffer generation are the
// process's and every other session's GL buffers are still alive in the shared group.
MGL_SERVER_STAGED_DROP_ALL();
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Remote::Server::ServerStagedTexture().DropAll();
#endif
}
void OnBackendContextDestroyed() {
UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch
++g_bufferContextGeneration;
// The generation moved AFTER the unregister bump above; re-open the
// memos again so no stamp can predate the generation change.
BumpBufferMutationEpoch();
InvalidateArrayBufferBindingCache();
InvalidateIndexedBufferBindingCache();
InvalidatePixelBufferBindingCaches();
// The rings' ids and persistent maps died with the context; drop the
// handles (no GL) and let the next draw / texture upload recreate them.
ResetRingForNewContext(UboRing());
ResetRingForNewContext(UnpackRing());
// R-11's server copies die with the context for the same reason the rings' ids do:
// the resource twins they are keyed by are about to be rebuilt against a new
// generation, and a shadow that outlived its twin would be looked up by a RECYCLED
// address on the next allocation - which is the quietest possible wrong answer.
MGL_SERVER_STAGED_DROP_ALL();
#if MOBILEGL_BUILD_DISAGGREGATED
// tx's texture shadows die for the same reason, keyed by handle rather than address
// but with the same recycled-identity failure mode: a new context's allocator may
// hand out a {slot, gen} the old one's store still answers for.
MG_Remote::Server::ServerStagedTexture().DropAll();
// P11 B2: every retired T0 import's fence named work on the context that just ended,
// so all of them let go now. LIVE imports keep their reference: the next ensure
// imports them again on the new context (EnsureBufferResourceForHandle).
ProcessDeferredT0Retires();
SweepT0Retired(/*contextGone=*/true);
#endif
}
void ProcessDeferredBufferReleases() {
// Runs on every draw; skip the context check, mutex and vector churn
// outright when nothing was enqueued since the last drain.
if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return;
if (!CanTouchGLNow()) return;
Vector<SharedPtr<BackendBufferResource>> releases;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
releases.swap(g_deferredBufferReleases);
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
}
for (auto& resource : releases) {
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
if (glesResource->contextGeneration != g_bufferContextGeneration) {
glesResource->id = 0;
continue;
}
if (IsPoolable(*glesResource)) {
EnrollIntoPool(*glesResource); // recycle instead of glDeleteBuffers
continue;
}
if (glesResource->id != 0) {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
InvalidateArrayBufferBindingCache();
}
ScrubBufferBindingShadowsForId(glesResource->id);
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: the T0 half of the deferred-release drain, run right after it at both sync
// points (ProcessDeferredBufferReleases itself moves verbatim, G5). Retired imports whose
// fence signalled let go of their AHardwareBuffer; a deferred T0 destroy is retired once
// the drain deleted its id (the fence then follows the delete) or its context is gone.
void ProcessDeferredT0Retires() {
if (!g_t0Retired.empty()) SweepT0Retired(false);
if (!g_hasT0DeferredDestroys.load(std::memory_order_acquire)) return;
Vector<SharedPtr<BackendBufferResource>> ready;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
for (SizeT i = 0; i < g_t0DeferredDestroys.size();) {
auto* r = static_cast<GLESBufferResource*>(g_t0DeferredDestroys[i].get());
if (r->id == 0 || r->contextGeneration != g_bufferContextGeneration) {
ready.push_back(std::move(g_t0DeferredDestroys[i]));
g_t0DeferredDestroys[i] = std::move(g_t0DeferredDestroys.back());
g_t0DeferredDestroys.pop_back();
} else {
++i;
}
}
g_hasT0DeferredDestroys.store(!g_t0DeferredDestroys.empty(), std::memory_order_release);
}
for (auto& resource : ready) RetireT0Import(*static_cast<GLESBufferResource*>(resource.get()));
}
#endif
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject) {
if (!bufferObject) return nullptr;
return static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
}
// The same five questions, asked of the applier instead of the frontend object, with
// IDENTICAL semantics (D-A4):
// identity the slot table's twin at this handle, not GetBackendResource()
// size record.Desc.Width, not GetSize()
// freshness record.Serial vs syncedChangeSerial, not GetChangeSerial()
// map state record.HasLiveHostWrites AND the frontend's IsMapped() - see below
// the rest unchanged, and server-side to begin with
//
// HasLiveHostWrites is ALWAYS FALSE in P3a and is written by nobody; it is here so the
// phase that pushes persistent-mapped host writes can set it with no new record kind,
// and the assertion below is what stops that phase landing a silent semantic change.
//
// BECAUSE it is pinned false, it CANNOT stand in for the frontend's IsMapped() yet, and
// the probe still asks the object: an EMULATED (non-adopted) persistent map - under the
// 16 MiB adoption threshold, or with DisableLargeBufferAdoption, or with no
// EXT_buffer_storage - mutates the client's shadow with no call, no serial and no epoch,
// which is the entire reason the legacy probe asks a map question instead of a serial
// one. Answering only HasLiveHostWrites made such a buffer read draw-clean forever, so
// SyncPersistentMappedRange (D-N keeps it on the ensure path for all of P3a) was never
// reached again and the frame drew the last uploaded bytes with no diagnostic anywhere.
// The frontend read retires the moment P5 gives HasLiveHostWrites a producer, and it is
// the last frontend read in this function.
Bool IsBufferDrawCleanByHandle(MG_Pipe::MGPipeHandle res, const GLESBufferResource* resource,
const MG_State::GLState::BufferObject* frontend) {
if (!resource) return false;
const auto* twin = g_backendBufferResources.FindByHandle(res);
if (twin == nullptr || twin->get() != resource) return false;
if (resource->contextGeneration != g_bufferContextGeneration) return false;
if (resource->id == 0) return false;
if (resource->persistentMapped) {
return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty();
}
const auto* record = ResourceRecordOf(res);
if (record == nullptr) return false;
#if MOBILEGL_PIPE_VERIFY && !MOBILEGL_BUILD_DISAGGREGATED
MOBILEGL_ASSERT(!record->HasLiveHostWrites,
"MGPipeResourceRecord::HasLiveHostWrites is set, but this build has no producer "
"for it - P5 b1's producer is MGPSubData::HasLiveHostWrites and is split-only");
#endif
if (record->HasLiveHostWrites) return false;
// THE LAST FRONTEND READ IN THIS FUNCTION, AND P5 b1 RETIRES IT - under split
// only, because that is the only build where it is both wrong and replaceable.
//
// It asked the object whether it was mapped because HasLiveHostWrites was pinned
// false and an emulated persistent map mutates the shadow with no call, no serial
// and no epoch; answering from the record alone made such a buffer read
// draw-CLEAN forever, SyncPersistentMappedRange was never reached again, and the
// frame drew the last uploaded bytes with no diagnostic
// (MG_Test/SanityTest.cpp's DirectGLESBufferDrawProbe is exactly that case).
//
// TWO THINGS REPLACE IT AND BOTH HAD TO LAND FIRST: the record now carries the map
// state (the line above, from MGPSubData::HasLiveHostWrites), and the client
// pushes the mapped span by block at every validate point, so the serial moves for
// a write the application made with no API call. Under a spawn there is no object
// on this side to ask, which is why this was never going to stay a choice.
//
// A null object is the "no frontend to ask" case and is treated as "not mapped",
// which is what the record already says.
const Bool askTheObjectWhetherItIsMapped =
!MG_Config::DataArmIsRecord();
if (askTheObjectWhetherItIsMapped && frontend != nullptr && frontend->IsMapped()) return false;
if (resource->pendingRespecify || !resource->storageInitialized) return false;
if (!resource->pendingRanges.empty()) return false;
if (resource->storageSize != static_cast<SizeT>(record->Desc.Width)) return false;
return resource->syncedChangeSerial.load(std::memory_order_acquire) == record->Serial;
}
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource) {
if (ResourceSubsystemEnabled()) {
return IsBufferDrawCleanByHandle(HandleOfBuffer(frontend), resource, frontend);
}
// Identity first: a respecify path can hand the frontend a NEW resource; the
// memoed pointer is then stale (and only kept alive by the caller's shadow).
if (!resource || resource != frontend->GetBackendResource().get()) return false;
if (resource->contextGeneration != g_bufferContextGeneration) return false;
if (resource->id == 0) return false;
// Zero-copy coherent persistent store: EnsureBufferResource's own early-out —
// the app writes straight into the mapped GPU storage, nothing to sync.
// Except queued resident SubData bytes, which land through the sync path
// (same unlocked emptiness probe as pendingRanges below).
if (resource->persistentMapped) {
return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty();
}
// A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push
// (persistent maps mutate the shadow without bumping the change serial).
if (frontend->IsMapped()) return false;
if (resource->pendingRespecify || !resource->storageInitialized) return false;
// Same unlocked emptiness probe EnsureBufferResource's replay branch uses.
if (!resource->pendingRanges.empty()) return false;
if (resource->storageSize != frontend->GetSize()) return false;
return resource->syncedChangeSerial.load(std::memory_order_acquire) == frontend->GetChangeSerial();
}
// The handle arm of the ensure path. Same shape, same order, same branches; the four
// frontend reads become the applier's stored descriptor, its Serial and the shadow base
// the last content-carrying call handed over.
//
// It still takes the frontend object for ONE reason, recorded rather than hidden:
// BufferObject::SyncPersistentMappedRange() is one of the eleven Espryt sites
// ROADMAP/D-N explicitly keeps where it is for P3a (the per-site attribution table is
// P8's to execute). Every other line here is handle-shaped. When P8 moves that call to
// the client this signature loses its last frontend argument.
GLESBufferResource* EnsureBufferResourceForHandle(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
MG_Pipe::MGPipeHandle res) {
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (!resource) return nullptr;
// The client's shadow base, RE-READ at every use, exactly as the legacy arm re-read
// MappedData() at every use. It is deliberately NOT resource->hostBytes here: two
// ordinary events move or free what a base recorded by an earlier call points at -
// a shadow resize (PipeResource::ResizeShadow is reserve + resize, so a grow past
// the reserve reallocates) and persistent-map adoption (the shadow is cleared and
// shrunk to fit) - and this path still holds the frontend object anyway, because
// D-N keeps SyncPersistentMappedRange here for all of P3a. hostBytes stays the
// fallback for the drains that have NO object (the readback flush), and it is
// nulled at both of those events.
const auto liveHostBase = [&resource, &bufferObject]() -> const Uint8* {
#if MOBILEGL_BUILD_DISAGGREGATED
// M-2 / codex 3 / ID-52 item 3: under an ACTIVE TRANSPORT the server's staged
// copy (resource->hostBytes, filled by MGL_SERVER_STAGED_ADOPT in Ops_H_SubData /
// Ops_H_FlushRange) is the ONLY authoritative base. Preferring the frontend
// object's MappedData() here - which under inproc is always non-null, same process
// - meant every reduced-path drain read the CLIENT's shadow and NEVER the server's,
// so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5
// 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that
// does not read the client object's memory. Under monolith nothing changes.
if (MG_Config::DataArmIsRecord()) {
return resource->hostBytes;
}
#endif
if (bufferObject) return bufferObject->MappedData();
return resource->hostBytes;
};
if (resource->contextGeneration != g_bufferContextGeneration) {
// The id (if any) belonged to a destroyed ES context.
resource->id = 0;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
resource->contextGeneration = g_bufferContextGeneration;
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// m-5 / codex 5: OnBackendContextDestroyed ran MGL_SERVER_STAGED_DROP_ALL(), which
// frees every server shadow but does NOT null the hostBytes that name them - so a
// twin that SURVIVES a context loss still carries a base into the freed allocation.
// Null it here, where a surviving twin is re-armed - but ONLY when the shadow is
// really gone. This block also runs on a twin's FIRST ensure (contextGeneration
// starts mismatched), and there the shadow a preceding resource_subdata just staged
// is still live; nulling it then would drop the reduced path's own bytes (it did,
// and TriangleScenario read the wrong VBO). HasShadow is the discriminator: false
// after DropAll, true after an ordinary Adopt.
//
// TRANSPORT-GUARDED like the other two split hunks in this file (review v2 N-7):
// under MONOLITH transport in a disaggregated build the store never copies, Adopt
// never sets m_any, HasShadow answers false for everything, and this null would
// run on every twin's first ensure - harmless there only because liveHostBase()
// prefers MappedData() under monolith, and "under monolith nothing changes" should
// be true by construction rather than by luck.
if (ServerStaged().CopiesIntoServerStorage() &&
!ServerStaged().HasShadow(resource)) {
resource->hostBytes = nullptr;
}
#if defined(__ANDROID__)
// P11 B2: A T0 STORE OUTLIVES ITS CONTEXT. Its pages are the client's
// AHardwareBuffer, which this twin still holds, so the store is imported again on
// the new context - the client keeps writing through the same pointer and nothing
// has to be re-sent. A refusal here leaves the twin without a store and says so.
if (resource->externalAhb != nullptr) {
String why;
void* ptr = nullptr;
const Uint64 size = ResourceWidthOf(res);
const Uint id = ResolveT0EntryPoints(why) ? ImportAhbAsBuffer(resource->externalAhb, size, &ptr, why)
: 0;
if (id != 0) {
InstallT0Store(*resource, id, ptr, size);
MGLOG_I("Espryt T0: store {slot=%u, gen=%u} re-imported on the new context (%llu bytes)",
res.Slot, res.Gen, static_cast<unsigned long long>(size));
} else {
MGLOG_E("Espryt T0: store {slot=%u, gen=%u} could not be re-imported after a context loss - "
"%s; the store has no GPU backing",
res.Slot, res.Gen, why.c_str());
RetireT0Import(*resource);
}
}
#endif
#endif
}
// An immutable store nothing maps any more, retired here on the thread that can.
if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
}
// Zero-copy coherent persistent buffer: nothing to (re)upload at draw time.
if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) {
DrainResidentWritesNow(*resource);
return resource;
}
if (resource->id == 0) {
const SizeT poolSize = ResourceWidthOf(res);
// The pool DECISION is the legacy one, unchanged: extent and residence only.
// An earlier cut added "and a shadow base was recorded", which silently stopped
// a buffer recycling pool ids - a change to D-F's pool behaviour, not to "the
// source of bytes" the conversion is allowed to move, and one that moves
// AcquireFromPool call counts StreamedArenaScenario observes.
const Uint reused =
(poolSize > 0 && !resource->persistentMapped) ? AcquireFromPool(poolSize) : 0;
if (reused != 0) {
resource->id = reused;
resource->storageSize = poolSize;
resource->storageInitialized = true;
resource->pendingRespecify = false;
BindBufferId(TempBufferTarget, reused);
// M-3 / codex 4: this is a WHOLE-STORE upload from the base, and under split
// the base is the server shadow (M-2). For a store whose content the
// application SUPPLIED, zero-filled bytes past the staged coverage are not the
// application's - uploading them is the silent data loss the M-6 ruling
// forbids, and a gap is a missing record: Fatal by name. For a store the
// application ORPHANED the gap is its own undefined content and the upload is
// legal (ResourceContentIsDeclared, above). RequireCoverage is a no-op for the
// legacy arm's MappedData() and for a non-copying store.
#if MOBILEGL_BUILD_DISAGGREGATED
if (ResourceContentIsDeclared(res)) {
MGL_SERVER_STAGED_REQUIRE(*resource, liveHostBase(), 0, poolSize,
"pool_reuse_whole_store");
}
#endif
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, liveHostBase());
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(poolSize));
}
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
}
resource->syncedChangeSerial = ResourceSerialOf(res);
} else {
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) {
MGLOG_E_ONCE("Failed to generate buffer object.");
MGLOG_E_ONCE("ES glGetError(): %s",
MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
return resource;
}
resource->storageInitialized = false;
resource->pendingRespecify = true;
}
}
// D-N keeps this call here for P3a. It can queue ranges and move the record, so
// everything below is read AFTER it.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.8): under an active transport the frontend accessor is a
// layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb
// persistent-map push is the only producer; the call is skipped outright. Under
// monolith D-N's placement stands.
if (bufferObject && !MG_Config::DataArmIsRecord()) {
bufferObject->SyncPersistentMappedRange();
}
#else
if (bufferObject) bufferObject->SyncPersistentMappedRange();
#endif
const auto* record = ResourceRecordOf(res);
const SizeT size = record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
if (size == 0) {
return resource;
}
const Uint64 serial = record->Serial;
const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(static_cast<BufferUsage>(record->Desc.Usage));
// Read AFTER SyncPersistentMappedRange above, and through liveHostBase for the
// reason written where it is declared.
const Uint8* const hostBase = liveHostBase();
// AND PUBLISH IT, because the readers that have NO frontend object read
// resource->hostBytes and nothing else: the readback flush (Ops_H_Readback), the
// kill-switch map arm of Ops_H_FlushRange, and the fp64 narrowing
// (SyncFloat64AttributeAsFloat32ByHandle, whose legacy counterpart reads
// bufferObject->MappedData()). A twin is created LAZILY - D-A2's row makes
// Ops_H_Create a no-op - so the very first resource_respecify of a buffer finds
// FindBufferResourceForHandle == nullptr and its base is dropped on the floor; with
// glBufferData(..., data) followed by no further content call (the ordinary static
// vertex array) hostBytes then stayed null for the object's whole life and the fp64
// narrowing refused every draw, disabling the array. This is the one place that both
// holds the object and runs before every draw that uses the store, so it is where the
// base is refreshed. Only reached for a NON-adopted resource (the adopted arm
// returned above), so C-2's rule is intact: an adopted store's hostBytes stays null
// and its bytes are read through persistentPtr.
if (hostBase != nullptr) resource->hostBytes = hostBase;
// "DOES THE SHADOW THIS PATH IS ABOUT TO UPLOAD HOLD MEANINGFUL BYTES?" - and it has
// to be asked of the SAME thing the bytes come from. The legacy arm pairs
// bufferObject.MappedData() with bufferObject.HasDefinedContent() (RespecifyStorageNow)
// and this arm pairs the same two, so the source of the bytes and the statement about
// them can never disagree.
//
// P5c (hd, CONTRACT-P5C §3.6 / B1): with an active transport the answer is the
// DESCRIPTOR's bit - which the client publishes on ResourceCreate/Respecify - OR the
// staged coverage the content records behind it delivered. The coverage half is what
// keeps the answer equal to the frontend flag's in the one case the descriptor alone
// cannot see: an ORPHANING respecify (descriptor clear) followed by glBufferSubData,
// which defines content without a new descriptor - the bytes crossed as
// resource_subdata and the staged store's coverage is exactly "content defined since
// the last orphan". With no coverage and a clear bit the store is undefined by the
// application's own declaration and the upload stays a pure NULL reallocation.
const Bool shadowHasContent =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::DataArmIsRecord()
? (record->Desc.HasDefinedContent != 0 ||
(ServerStaged().CopiesIntoServerStorage() ? ServerStaged().CoveredRunCount(resource) != 0
: resource->contentSinceRespecify))
:
#endif
(bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0));
const void* initialData = shadowHasContent ? hostBase : nullptr;
// M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE
// [0, size) upload from the base, so it owes the same coverage the pending-range drain
// below owes - the two respecify arms were the readers M-6's "never widened" rule did
// not reach. No-op for the legacy arm's MappedData() and for a non-copying store; a
// Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} under split when the
// DESCRIPTOR says the application supplied the content and the shadow's coverage does
// not span the store (a missing record), instead of uploading its zero-fill as content.
// NOT for a store the application orphaned: there `shadowHasContent` is the frontend
// object's flag, which the streaming idiom's partial glBufferSubData flips to true, and
// the bytes it did not write are undefined by its own glBufferData(NULL) - the rule at
// ResourceContentIsDeclared. Round 2 refused that idiom and aborted six joint entries.
const auto requireWholeStoreCoverage = [&]() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (initialData != nullptr && record->Desc.HasDefinedContent != 0) {
MGL_SERVER_STAGED_REQUIRE(*resource, static_cast<const Uint8*>(initialData), 0,
size, "respecify_whole_store");
}
#else
(void)initialData;
#endif
};
if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) {
requireWholeStoreCoverage();
RespecifyStorageWith(*resource, size, usage, initialData, serial);
} else if (!resource->pendingRanges.empty()) {
// Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object
// `hostBase` is the server shadow and every queued range must be staged.
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, hostBase, size, "ensure_flush_pending");
FlushPendingRangesFrom(*resource, hostBase, size);
resource->syncedChangeSerial = serial;
} else if (resource->syncedChangeSerial != serial) {
// Mutations this backend could not track (the table was unregistered between
// contexts); re-upload everything.
requireWholeStoreCoverage();
RespecifyStorageWith(*resource, size, usage, initialData, serial);
}
return resource;
}
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!bufferObject) return nullptr;
if (ResourceSubsystemEnabled()) {
const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get());
if (MG_Pipe::MGPipeHandleIsNull(res)) {
// The client never created this resource on the wire. There is deliberately
// no fall-back to the legacy arm: silently twinning it off the frontend
// object would hide a missing emission behind a working picture, which is
// exactly what the subsystem A/B exists to make visible.
MGLOG_E_ONCE("MGPipe: buffer %u has no resource handle - the resource family is switched "
"over but nothing emitted resource_create for it",
bufferObject->GetExternalIndex());
return nullptr;
}
return EnsureBufferResourceForHandle(bufferObject, res);
}
auto* resource = static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
if (!resource) {
auto newResource = MakeShared<GLESBufferResource>();
newResource->pendingRespecify = true;
resource = newResource.get();
bufferObject->SetBackendResource(std::move(newResource));
}
if (resource->contextGeneration != g_bufferContextGeneration) {
// The id (if any) belonged to a destroyed ES context.
resource->id = 0;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
resource->contextGeneration = g_bufferContextGeneration;
// The persistent map (and its pointer) died with the old context; the
// frontend re-acquires a fresh one on its next map.
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
}
// m-5: the LEGACY arm (EnsureBufferResource) is reached only under monolith/push, where
// liveHostBase reads the frontend object's MappedData rather than a server shadow, so
// there is no freed server base to null here - the split freed-base hazard lives in
// EnsureBufferResourceForHandle above, guarded by HasShadow.
// An immutable store nothing maps any more: a respecification of a buffer that
// had been persistently mapped, which Ops_Respecify could not retire because it
// ran off the context thread. glBufferData cannot redefine it, so it is retired
// here, on the thread that can, and the id is re-minted below.
if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Same as the persistent-map re-mint: the dying id may be baked into
// driver VAO bindings whose frontend versions do not move for this.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
}
// Zero-copy coherent persistent buffer: the app writes straight into the
// persistently mapped immutable store, so there is nothing to (re)upload at
// draw time. This is where the per-draw whole-buffer glBufferSubData used to run.
if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) {
DrainResidentWritesNow(*resource, *bufferObject);
return resource;
}
if (resource->id == 0) {
// Try to recycle an idle same-size buffer from the pool (GPU-complete,
// exact byte size) and reseed it in place with glBufferSubData, instead
// of glGenBuffers + fresh-storage glBufferData (the kgsl alloc path).
const SizeT poolSize = bufferObject->GetSize();
const Uint reused =
(poolSize > 0 && !resource->persistentMapped) ? AcquireFromPool(poolSize) : 0;
if (reused != 0) {
resource->id = reused;
resource->storageSize = poolSize;
resource->storageInitialized = true;
resource->pendingRespecify = false;
BindBufferId(TempBufferTarget, reused);
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize,
bufferObject->MappedData());
if (MG_Util::PipeStats::Enabled()) {
// The pool-recycle reseed is a whole-buffer upload on the hot path,
// not a bookkeeping detail: it moves the same bytes a fresh
// glBufferData would.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(poolSize));
}
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
resource->pendingResidentWrites.clear();
}
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
} else {
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) {
MGLOG_E_ONCE("Failed to generate buffer object.");
MGLOG_E_ONCE("ES glGetError(): %s",
MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
return resource;
}
resource->storageInitialized = false;
resource->pendingRespecify = true;
}
}
// Push persistently-mapped writes first; lands either as an immediate
// SubData (fresh storage) or as part of the full re-upload below.
bufferObject->SyncPersistentMappedRange();
if (bufferObject->GetSize() == 0) {
return resource;
}
if (resource->pendingRespecify || !resource->storageInitialized ||
resource->storageSize != bufferObject->GetSize()) {
RespecifyStorageNow(*resource, *bufferObject);
} else if (!resource->pendingRanges.empty()) {
// The shared ladder, by ID-11 - see the note where the forwarder would be.
FlushPendingRangesFrom(*resource, bufferObject->MappedData(), bufferObject->GetSize());
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
} else if (resource->syncedChangeSerial != bufferObject->GetChangeSerial()) {
// Ops could not track some writes (e.g. the ops table was
// unregistered between contexts); re-upload everything.
RespecifyStorageNow(*resource, *bufferObject);
}
return resource;
}
void BindBufferId(GLenum target, Uint id) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (target == GL_ARRAY_BUFFER) {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) {
return;
}
g_boundArrayBufferId = id;
g_boundArrayBufferKnown = true;
}
g_GLESFuncs.glBindBuffer(target, id);
}
void InvalidateArrayBufferBindingCache() {
g_boundArrayBufferId = 0;
g_boundArrayBufferKnown = false;
}
void BindPixelPackBufferId(Uint id) {
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
return;
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id);
g_boundPixelPackBufferId = id;
g_boundPixelPackBufferKnown = true;
}
void BindPixelUnpackBufferId(Uint id) {
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
return;
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id);
g_boundPixelUnpackBufferId = id;
g_boundPixelUnpackBufferKnown = true;
}
void InvalidatePixelBufferBindingCaches() {
g_boundPixelPackBufferId = 0;
g_boundPixelPackBufferKnown = false;
g_boundPixelUnpackBufferId = 0;
g_boundPixelUnpackBufferKnown = false;
}
void NoteBufferIdDeleted(Uint id) {
if (id == 0) {
return;
}
if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) {
InvalidateArrayBufferBindingCache();
}
ScrubBufferBindingShadowsForId(id);
}
namespace {
// Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range
// (same index + id + range) are skipped. isBase distinguishes a whole-buffer
// base bind from a sub-range bind. Fresh/reset context: every point is base(0)
// == unbound, which matches the GL default.
struct IndexedBufferBinding {
Uint id = 0;
GLintptr offset = 0;
GLsizeiptr size = 0;
Bool isBase = true;
// False when the driver's binding at this point is no longer described by the
// fields above and the next bind must be issued whatever it asks for. Set by
// InvalidateIndexedBufferBindingShadowsForId after a store was re-specified at
// a new size: the id is still bound, so the entry must NOT be scrubbed to
// base(0) (a later bind of 0 would then be false-skipped) - only distrusted.
Bool known = true;
};
constexpr SizeT kMaxIndexedBufferBindings = 64;
IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings];
IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings];
// Transform feedback gets a shadow for a reason the other two do not have: the
// capture points are synced from the application's TOUCHED high-water mark, which
// deqp/glcts permanently raises to GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by
// clearing every point after each test case. Without a shadow every capture that
// uses fewer points than that (i.e. every INTERLEAVED_ATTRIBS capture) re-issued a
// redundant glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) for the unused
// tail immediately before glBeginTransformFeedback - calls a plain GL application
// never makes there, and the only thing MobileGL does differently from one.
//
// Unlike the UBO/SSBO points these are NOT context state: they belong to the bound
// transform feedback OBJECT, so XfbImpl::BindTransformFeedback drops the whole
// shadow to unknown on every object switch (InvalidateTransformFeedbackBindingShadows).
IndexedBufferBinding g_indexedXFBBindings[kMaxIndexedBufferBindings];
IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) {
if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache
if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index];
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
if (glTarget == GL_TRANSFORM_FEEDBACK_BUFFER) return &g_indexedXFBBindings[index];
return nullptr;
}
// glDeleteBuffers resets the deleted buffer's bindings (indexed and
// pixel pack/unpack ones included) to 0 in the current context; mirror
// that in the shadows, or a later buffer recycling the same name with a
// matching shadow entry would false-skip its rebind. Default
// IndexedBufferBinding{} == base(0) == the post-delete GL state.
void ScrubBufferBindingShadowsForId(Uint id) {
if (id == 0) return;
for (auto& binding : g_indexedUBOBindings) {
if (binding.id == id) binding = {};
}
for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding = {};
}
for (auto& binding : g_indexedXFBBindings) {
if (binding.id == id) binding = {};
}
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
g_boundPixelPackBufferId = 0;
}
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
g_boundPixelUnpackBufferId = 0;
}
}
void InvalidateIndexedBufferBindingShadowsForId(Uint id) {
if (id == 0) return;
for (auto& binding : g_indexedUBOBindings) {
if (binding.id == id) binding.known = false;
}
for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding.known = false;
}
for (auto& binding : g_indexedXFBBindings) {
if (binding.id == id) binding.known = false;
}
}
} // namespace
// The capture points belong to the bound transform feedback object, so a bind (or a
// delete, which reverts to the default object) replaces all of them at once with
// state this shadow has never seen. Distrust rather than scrub: the driver's bindings
// are whatever the newly bound object holds, which is NOT necessarily base(0), and
// scrubbing would let a later bind of 0 be false-skipped.
void InvalidateTransformFeedbackBindingShadows() {
for (auto& binding : g_indexedXFBBindings) binding.known = false;
}
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
auto* s = IndexedBindingShadow(glTarget, index);
if (s && s->known && s->isBase && s->id == id) return;
g_GLESFuncs.glBindBufferBase(glTarget, index, id);
if (s) *s = {id, 0, 0, true, true};
}
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size) {
auto* s = IndexedBindingShadow(glTarget, index);
if (s && s->known && !s->isBase && s->id == id && s->offset == offset && s->size == size) return;
g_GLESFuncs.glBindBufferRange(glTarget, index, id, offset, size);
if (s) *s = {id, offset, size, false, true};
}
void InvalidateIndexedBufferBindingCache() {
for (auto& b : g_indexedUBOBindings) b = {};
for (auto& b : g_indexedSSBOBindings) b = {};
for (auto& b : g_indexedXFBBindings) b = {};
}
void TrimBufferPool() {
const std::lock_guard<std::mutex> lock(g_poolMutex);
if (g_pooledBytes <= kMaxPoolBytes) return;
// Over budget: evict oldest-retireSerial entries with real glDeleteBuffers.
while (g_pooledBytes > kMaxPoolBytes) {
SizeT oldestKey = 0, oldestIdx = 0;
Uint64 oldestSerial = ~Uint64{0};
Bool found = false;
for (auto& kv : g_bufferPool) {
for (SizeT i = 0; i < kv.second.size(); ++i) {
if (kv.second[i].retireSerial < oldestSerial) {
oldestSerial = kv.second[i].retireSerial;
oldestKey = kv.first;
oldestIdx = i;
found = true;
}
}
}
if (!found) break;
auto& bucket = g_bufferPool[oldestKey];
PooledBuffer& e = bucket[oldestIdx];
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
ScrubBufferBindingShadowsForId(e.id);
g_GLESFuncs.glDeleteBuffers(1, &e.id);
}
g_pooledBytes -= e.size;
bucket[oldestIdx] = bucket.back();
bucket.pop_back();
}
}
void ClearBufferPool() {
const std::lock_guard<std::mutex> lock(g_poolMutex);
// Ids belong to the dying context; drop without glDeleteBuffers (mirrors
// the g_deferredBufferReleases.clear() discipline).
g_bufferPool.clear();
g_pooledBytes = 0;
}
// --- Persistent-mapped bump rings (see Managers.h) ------------------------
namespace {
// (Re)create the ring store with room for at least minBytes. Any live
// store is retired (deleted once the GPU finished the last frame that
// could reference its slots), never deleted in place. Returns false and
// leaves the current store untouched when minBytes cannot fit under the
// size cap; a GL failure loses the store and latches creationFailed so
// later uses stop retrying under this context.
Bool CreateRingStorage(PersistentRing& ring, SizeT minBytes) {
SizeT newSize = ring.initialBytes;
while (newSize < minBytes) newSize *= 2;
if (newSize > ring.maxBytes) return false;
if (ring.store.id != 0) {
ring.retired.push_back(
{ring.store.id, ring.store.contextGeneration, DirectGLES::CurrentFrameSerial() + 1});
}
const Uint32 nextGeneration = ring.store.generation + 1;
ring.store.id = 0;
ring.store.mappedPtr = nullptr;
Uint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id != 0) {
BindBufferId(TempBufferTarget, id);
g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast<GLsizeiptr>(newSize), nullptr,
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(newSize),
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
if (!ptr) {
// The dying id is what the array-buffer cache has recorded as
// bound; a later buffer recycling the name would false-skip.
InvalidateArrayBufferBindingCache();
g_GLESFuncs.glDeleteBuffers(1, &id);
id = 0;
} else {
ring.store.mappedPtr = static_cast<Uint8*>(ptr);
}
}
if (id == 0) {
MGLOG_E_ONCE("%s: persistent storage creation failed (%zu bytes); falling back to the "
"client-memory upload path.",
ring.label, newSize);
ring.store.creationFailed = true;
return false;
}
SizeT alignment = ring.fixedAlignment;
if (alignment == 0) {
const GLint capsAlignment = g_GLESCapabilities.UniformBufferOffsetAlignment;
alignment = capsAlignment > 0 ? static_cast<SizeT>(capsAlignment) : 256;
}
ring.store.id = id;
ring.store.size = newSize;
ring.store.head = 0;
ring.store.tail = 0;
ring.store.generation = nextGeneration;
ring.store.alignment = alignment;
ring.frameMarks.clear();
MGLOG_D("%s: %zu MiB persistent store ready (id %u, gen %u, align %zu).", ring.label,
newSize / (1024u * 1024u), id, nextGeneration, alignment);
return true;
}
// Shared half of the availability gate (the per-ring kill switch sits in
// the exported wrappers). Reclamation rides the Present fence watermark;
// without working fences slots would never be provably GPU-idle (same rule
// as IsPoolable).
Bool RingAvailable(PersistentRing& ring) {
if (!g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers ||
!g_GLESFuncs.glFenceSync || !g_GLESFuncs.glGetSynciv) {
return false;
}
if (!CanTouchGLNow()) return false;
if (ring.store.contextGeneration != g_bufferContextGeneration) {
ResetRingForNewContext(ring);
}
return !ring.store.creationFailed;
}
// Division-based rounding fallback: the spec doesn't promise a power-of-two
// alignment. Slot offsets stay multiples of the alignment because every
// slot size is, and wrap padding restarts at ring offset 0.
inline SizeT RingAlignUp(SizeT size, SizeT alignment) {
if ((alignment & (alignment - 1)) == 0) {
return (size + alignment - 1) & ~(alignment - 1);
}
return (size + alignment - 1) / alignment * alignment;
}
Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset);
// Bump-allocate `size` bytes out of `ring`.
//
// Fast path: a live store under the current context with room before both
// the wrap boundary and the in-flight tail. Touches no GL and probes no
// frame marks - callers sit behind the ring's availability gate, so the
// context checks have already run for this draw/upload. `tail` may be stale
// here (marks are only retired on Present and on the slow path); staleness
// is conservative - the in-flight span reads too large, the check fails,
// and the slow path retires marks and re-tries.
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset) {
if (size == 0) return false;
auto& store = ring.store;
if (store.id != 0 && store.contextGeneration == g_bufferContextGeneration) {
const SizeT alignedSize = RingAlignUp(size, store.alignment);
// Ring sizes are the ring's initial size (a power of two) doubled
// some number of times, so the offset modulo reduces to a mask.
static_assert((kUboRingInitialBytes & (kUboRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
static_assert((kUploadRingInitialBytes & (kUploadRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
const SizeT offset = static_cast<SizeT>(store.head & (store.size - 1));
if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) {
store.head += alignedSize;
outOffset = offset;
return true;
}
}
return RingAllocateSlow(ring, size, outOffset);
}
Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset) {
if (!RingAvailable(ring)) return false;
auto& store = ring.store;
// Sizing the store first, so the request is rounded with the alignment
// the live store actually carries rather than the pre-creation default.
if (store.id == 0 && !CreateRingStorage(ring, RingAlignUp(size, store.alignment))) {
return false;
}
const SizeT alignedSize = RingAlignUp(size, store.alignment);
// Advance tail past every frame the GPU provably finished.
const Uint64 completed = DirectGLES::CompletedFrameSerial();
SizeT retiredMarks = 0;
for (const auto& mark : ring.frameMarks) {
if (mark.frameSerial > completed) break;
if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent;
++retiredMarks;
}
if (retiredMarks > 0) {
ring.frameMarks.erase(ring.frameMarks.begin(),
ring.frameMarks.begin() + static_cast<std::ptrdiff_t>(retiredMarks));
}
// A slot may not straddle the ring end; pad the cursor to the boundary.
SizeT offset = static_cast<SizeT>(store.head % store.size);
if (offset + alignedSize > store.size) {
store.head += store.size - offset;
offset = 0;
}
if (store.head + alignedSize - store.tail > store.size) {
// In-flight span would overrun live slots: grow instead of overwrite.
if (CreateRingStorage(ring, std::max(store.size * 2, alignedSize))) {
offset = 0;
} else if (store.creationFailed) {
return false; // store lost; callers fall back to their legacy path
} else {
// At the size cap (>maxBytes in flight). First try to free room
// by waiting for the OLDEST in-flight frames to retire - a
// bounded wait that ends as soon as enough tail space exists,
// instead of draining the entire queue.
constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame
while (!ring.frameMarks.empty() &&
store.head + alignedSize - store.tail > store.size) {
const auto& oldest = ring.frameMarks.front();
if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) {
break;
}
if (oldest.headAtPresent > store.tail) store.tail = oldest.headAtPresent;
ring.frameMarks.erase(ring.frameMarks.begin());
}
if (store.head + alignedSize - store.tail <= store.size) {
offset = static_cast<SizeT>(store.head % store.size);
if (offset + alignedSize > store.size) {
store.head += store.size - offset;
offset = 0;
}
store.head += alignedSize;
outOffset = offset;
return true;
}
// No usable fence covers the oldest frames: drain once rather than
// corrupt live slots.
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
store.tail = store.head;
ring.frameMarks.clear();
// Same-frame slots written before the drain may now be recycled by
// the very next allocations; a generation bump keeps later draws
// from rebinding those cached offsets.
++store.generation;
offset = static_cast<SizeT>(store.head % store.size);
if (offset + alignedSize > store.size) {
store.head += store.size - offset;
offset = 0;
}
}
}
store.head += alignedSize;
outOffset = offset;
return true;
}
void DeleteOrphanRingBuffers() {
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
auto& orphans = OrphanRingBuffers();
if (orphans.empty()) return;
for (const OrphanRingBuffer& orphan : orphans) {
// A buffer of a share group that is gone was reclaimed with it.
if (orphan.contextGeneration != g_bufferContextGeneration) continue;
ScrubBufferBindingShadowsForId(orphan.id);
g_GLESFuncs.glDeleteBuffers(1, &orphan.id);
}
orphans.clear();
}
// Present()-time upkeep shared by both rings.
void RingOnPresent(PersistentRing& ring) {
if (!CanTouchGLNow()) return;
DeleteOrphanRingBuffers();
// Delete grown-away stores the GPU is provably done with.
const Uint64 completed = DirectGLES::CompletedFrameSerial();
for (SizeT i = ring.retired.size(); i-- > 0;) {
RetiredRingStore& entry = ring.retired[i];
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
if (!staleContext && entry.retireSerial > completed) continue;
if (!staleContext && entry.id != 0) {
ScrubBufferBindingShadowsForId(entry.id);
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
}
ring.retired[i] = ring.retired.back();
ring.retired.pop_back();
}
auto& store = ring.store;
if (store.id == 0 || store.contextGeneration != g_bufferContextGeneration) return;
// Retire completed marks here too — RingAllocate is the main consumer,
// but frames that used the ring for nothing would otherwise let the list
// grow one entry per Present, unboundedly.
SizeT retiredMarks = 0;
for (const auto& mark : ring.frameMarks) {
if (mark.frameSerial > completed) break;
if (mark.headAtPresent > store.tail) store.tail = mark.headAtPresent;
++retiredMarks;
}
if (retiredMarks > 0) {
ring.frameMarks.erase(ring.frameMarks.begin(),
ring.frameMarks.begin() + static_cast<std::ptrdiff_t>(retiredMarks));
}
// Record this frame's high-water mark (Present just fenced the serial now
// reported by CurrentFrameSerial()). A fence-less Present repeats the
// serial; fold into the existing mark.
const Uint64 serial = DirectGLES::CurrentFrameSerial();
if (!ring.frameMarks.empty() && ring.frameMarks.back().frameSerial == serial) {
ring.frameMarks.back().headAtPresent = store.head;
} else {
ring.frameMarks.push_back({serial, store.head});
}
}
} // namespace
Bool UboRingAvailable() {
if (MG_Config::Features.EsprytDisableUboRing) return false;
return RingAvailable(UboRing());
}
Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); }
void* UboRingMappedPtr() { return UboRing().store.mappedPtr; }
Uint UboRingBufferId() { return UboRing().store.id; }
Uint32 UboRingGeneration() { return UboRing().store.generation; }
void UboRingOnPresent() { RingOnPresent(UboRing()); }
Bool UnpackRingAvailable() {
if (MG_Config::Features.EsprytDisableUnpackRing) return false;
return RingAvailable(UnpackRing());
}
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) {
// A request the ring could never satisfy even empty would otherwise walk
// the whole grow/drain ladder before failing.
if (size > kUnpackRingMaxBytes) return false;
return RingAllocate(UnpackRing(), size, outOffset);
}
void* UnpackRingMappedPtr() { return UnpackRing().store.mappedPtr; }
Uint UnpackRingBufferId() { return UnpackRing().store.id; }
SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; }
void UnpackRingOnPresent() { RingOnPresent(UnpackRing()); }
void UploadRingOnPresent() { RingOnPresent(UploadRing()); }
} // namespace BufferImpl
// ---- P4a (D-K3): the four family arm resolvers, beside BufferImpl's two ----
//
// They reuse BufferImpl's ClassifyPipeSubsystemArm / StopOnArmlessPipeSubsystem unchanged:
// one voice for "the operator left no arm at all", so the six families cannot disagree
// about what an armless verdict means or about whether it stops.
//
// Every one of the four answers legacyArmSurvivesLegacyMemos = FALSE, because every one of
// the four pre-handle arms is compiled under MOBILEGL_PIPE_LEGACY_MEMOS (Managers.h names
// them per family). So all four can reach NoArm and all four stop with the named
// Fatal{PipeLegacyMemosDisabled, ...} rather than running the very arm the operator asked
// to have taken away and handing back a result measured on it.
namespace {
// Shared by the dependent families, so the refusal reads the same way every time and a
// future family cannot invent a different wording. Returns true when the dependency is
// missing, having said so once.
//
// P3b/P4b wave 2-D package D3: THE ROWS ARE READ FROM MG_Pipe/SubsystemDeps.def AND ARE
// NO LONGER RESTATED HERE. This function used to take a single dependency BIT written out
// at each call site - the server's own copy of the rule, one of the six statements R-5
// found and two of which had drifted - and a family with two required bits needed two
// guarded calls with two hand-written sentences. It now takes the FAMILY and asks
// MGPipeSubsystemDependenciesAreSet, so the set of required bits, the subset that is
// actually missing and the reason all come from the table; `what` is only the family's
// own name. The client's gate reads the same rows
// (MG_Impl/Pipe/PipeFill.cpp P4aFamilyDependencyBits), which is what makes
// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp's comparison of the two a comparison
// of two readers rather than of two copies.
Bool PipeSubsystemDependencyMissing(Uint64 mask, Uint64 family, const char* what) {
if (MG_Pipe::MGPipeSubsystemDependenciesAreSet(family, mask)) return false;
const Uint64 required = MG_Pipe::MGPipeSubsystemRequires(family);
MGLOG_E("MGPipe: %s is set but MOBILEGL_PIPE_PUSH=0x%llx does not carry every bit "
"MG_Pipe/SubsystemDeps.def says it requires (requires 0x%llx, missing 0x%llx): "
"%s - REFUSING the dependent bit and running the legacy arm. Set every bit of "
"the row, or clear the family's own",
what, static_cast<unsigned long long>(mask),
static_cast<unsigned long long>(required),
static_cast<unsigned long long>(required & ~mask),
MG_Pipe::MGPipeSubsystemDependencyWhy(family));
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// THE SAFETY ARGUMENT P4a MADE AND NEVER RAN (P3b/P4b espryt D1 slice 5).
//
// "Running the legacy arm" is a real answer in a monolith and a POSTPONED CRASH under a
// transport. All four pre-handle arms reach frontend state through accessors that are
// BARRIER_PULLED rows (FieldOwnership.def) - GetFramebufferBindingSlot is the loudest of
// them - and under an active transport that state belongs to a client that is no longer
// parked behind this apply. The accessor's honest answer is
// Fatal{UnmigratedPipeInput, "<field>@<verb>"}, raised on the first verb that reaches it.
//
// So the verdict "Legacy" under a transport is not safe, it is merely quiet. The audit
// that called g_fboTextureSyncList "separation-safe because the record arm returns first"
// was right only while the record arm is ARMED, and nothing checked that.
//
// A FRAME-TIME FATAL IS THE WORST SPELLING OF "MOBILEGL_PIPE_PUSH LEFT BIT 9 CLEAR": it
// arrives after a session is up, inside a scenario body, naming an accessor and a verb
// rather than the mask that chose them. Stop at resolve time instead - once, before the
// first verb - where both causes are still in scope and can be said in one sentence.
//
// THROUGH StopOnArmlessPipeSubsystem AND NOT A SECOND FATAL, deliberately: it is this
// file's one voice for "the configuration left no arm to run", the census already knows
// its word, and a transport with no record arm IS that condition - the legacy arm is
// present but unreachable, which is the same "no arm" with a different cause. The cause
// is what `detail` carries.
[[noreturn]] void StopOnTransportWithoutRecordArm(const char* bit, const char* detail) {
char what[512];
std::snprintf(what, sizeof(what),
"MOBILEGL_TRANSPORT is not monolith and %s is clear (or refused), so the "
"record arm is off and the legacy arm is unreachable: %s",
bit, detail);
BufferImpl::StopOnArmlessPipeSubsystem(what);
}
#endif
// The release-build VOICE for StateBackendObjectRegistry::GetOrCreateByHandle's two
// silent refusals, shared by the kinds P4a re-keys so the wording cannot drift between
// them. It is the shape GetOrCreateBufferResourceForHandle gives P3a's buffer family,
// lifted into one template because a copy per kind is a chance per kind to write one of
// them differently.
//
// ITS CALLERS IN THIS PACKAGE ARE THE FRAMEBUFFER ATTACHMENT WALK'S TWO
// (FramebufferImpl::SyncAttachmentSurface: the texture registry and the renderbuffer
// registry), because that is where a HANDLE arrives in a payload - MGPSurface::Res -
// and an ADOPTION is what the arm owes rather than a lookup. The other three re-keyed
// kinds resolve at sites in DirectGLES.cpp (package E's SyncCurrentFBO, the lazy
// sampler mint and SyncCurrentProgram), so their first adoption is E's to write; this
// template is what they call so all five say the same thing.
//
// WHY A VOICE AT ALL: the table asserts, and MOBILEGL_ASSERT compiles out at INFO -
// which all three gate builds and every shipped build are - so an object that silently
// stops being twinned is exactly the failure mode the refusal exists to replace.
//
// Returns null on the legacy arm too, so a caller that reaches this without checking
// its family's arm gets nothing rather than a twin the legacy arm would never see.
//
// SPLIT IN TWO (review N-3), and the split is what makes the two refusals REACHABLE.
// Both of this package's call sites stand behind a cross-check that the record's Res
// equals registry.HandleOf(frontendObject) - the LIVE handle - so by the time the
// adoption ran, the slot was in bounds and the generation was current BY CONSTRUCTION
// and neither refusal below could ever fire: the letter of M-4 was closed and its
// substance was not. The record's handle is the UNTRUSTED input and the frontend is the
// corroboration, so the validity question is asked FIRST, on its own, at both sites;
// the adoption then performs the resolve. Asking it first is also side-effect free,
// which the ordering rule N-1 states for the framebuffer refusal wants here too:
// GetOrCreateByHandle can reset an incumbent twin, and a handle this predicate rejects
// must not reach it.
template <typename Registry>
Bool PipeTwinHandleIsAdoptable(Registry& registry, MG_Pipe::MGPipeHandle handle, const char* kindName) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
if (handle.Slot >= Registry::SlotTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"%s handle slot %u is past this table's %u bound",
kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot);
#else
MGLOG_E_ONCE("MGPipe: %s handle slot %u is past the backend table's %u bound - "
"refusing to twin it",
kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot);
return false;
#endif
}
const Uint32 liveGen = registry.LiveGenAt(handle.Slot);
if (liveGen != 0 && liveGen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"%s handle {%u, %u} names generation behind live generation %u",
kindName, handle.Slot, handle.Gen, liveGen);
#else
// SlotTables.h:301-321: forward is a recycle and resets the twin, BACKWARD is
// refused, because adopting it would release the incumbent LIVE twin's driver
// id and then stamp the slot back to the dead object's generation, after which
// the incumbent's own FindByHandle refuses it and it is silently handed a
// fresh, empty twin - a leak AND an object that loses its storage with no
// diagnostic. That is the shape commit d7655247 fixed on the buffer side.
MGLOG_E_ONCE("MGPipe: %s handle {%u, %u} names a generation BEHIND the live "
"twin's %u - refusing rather than dropping the incumbent's driver "
"object",
kindName, handle.Slot, handle.Gen, liveGen);
return false;
#endif
}
return true;
}
template <typename Registry>
typename Registry::BackendPtr* AdoptTwinByHandle(Registry& registry, MG_Pipe::MGPipeHandle handle,
const char* kindName) {
if (!PipeTwinHandleIsAdoptable(registry, handle, kindName)) return nullptr;
return registry.GetOrCreateByHandle(handle);
}
} // namespace
Bool ResolveFramebufferSubsystemArm() {
const Uint64 mask = MG_Config::Features.PipePush;
const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemFramebuffer) != 0;
Bool refused = false;
if (bitSet) {
// The framebuffer family's row, from MG_Pipe/SubsystemDeps.def. The mirror pair (bit
// 10 set, bit 9 clear) is FINE - the legacy FBO sync reaches the texture twin through
// SyncTextureObjectToBackend, which dispatches to the handle arm by itself - and that
// is stated with the other mirror pairs in MGPipe.h, beside the table it is the
// complement of, rather than here.
refused = PipeSubsystemDependencyMissing(mask, MG_Pipe::kMGPipeSubsystemFramebuffer,
"kMGPipeSubsystemFramebuffer (bit 9)");
// D-C3: the wire array is Color[8] and this is the driver's RAW ES cap, which is
// NOT clamped to 8 on the GLES path (ValidateColorAttachmentInRange rejects at or
// above it, and BackendObject_DirectGLES publishes it verbatim). Every campaign
// device reports 8 and ES 3.2's minimum is 8 - but a driver reporting more would
// make the record silently truncate, and truncating silently is the bug class this
// phase is closing, while widening the payload is a wire change nobody has evidence
// for. So: refuse the bit, name the cap, run the legacy arm.
if (!refused &&
static_cast<Uint32>(std::max<Int>(g_GLESCapabilities.MaxColorAttachments, 0)) >
MG_Pipe::kMGPipeMaxColorAttachments) {
MGLOG_E("MGPipe: this driver reports GL_MAX_COLOR_ATTACHMENTS = %d, above "
"MGPFramebufferState::Color[%u]'s wire width - REFUSING "
"kMGPipeSubsystemFramebuffer (bit 9) rather than truncating the record, "
"and running the legacy framebuffer arm",
g_GLESCapabilities.MaxColorAttachments, MG_Pipe::kMGPipeMaxColorAttachments);
refused = true;
}
}
const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm(
bitSet && !refused, /*legacyMemosEnabled=*/true,
/*legacyArmSurvivesLegacyMemos=*/false);
if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) {
BufferImpl::StopOnArmlessPipeSubsystem(
"MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemFramebuffer (bit 9) clear (or refuses "
"it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle g_fboSynced* arm");
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord() &&
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
StopOnTransportWithoutRecordArm(
"kMGPipeSubsystemFramebuffer (bit 9)",
"the pre-handle g_fboSynced* / g_fboTextureSyncList arm reads the framebuffer "
"binding slots, and under a transport GetFramebufferBindingSlot is a BARRIER_PULLED "
"row belonging to a client that is not parked behind this apply - so the first verb "
"to reach it would raise Fatal{UnmigratedPipeInput, "
"\"GetFramebufferBindingSlot@<verb>\"} mid-frame instead");
}
#endif
const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles;
MGLOG_D("MGPipe: Espryt framebuffer family runs the %s arm", enabled ? "handle" : "legacy");
return enabled;
}
Bool ResolveTextureResourceSubsystemArm() {
const Uint64 mask = MG_Config::Features.PipePush;
const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemTextureResources) != 0;
Bool refused = false;
if (bitSet) {
// The texture family's row, from MG_Pipe/SubsystemDeps.def. It is the ONLY row with
// two required bits, and it used to be TWO guarded calls here with the pair of
// sentences written out by hand - bit 7 for a buffer texture's BufferForTexBuffer,
// and bit 11, D-K2's FOURTH ROW (ID-14/ID-15), for MGPTextureParams::BuiltinSampler.
// One call now asks the table for both, and the refusal names whichever of the two is
// actually missing instead of always naming the first. Package F pins this arm at
// 0x5ff and CMakeLists.txt's refusal lane at 0x7ff; both are rows of the table now.
//
// The mirror pairs live in MGPipe.h with the rest. The one worth remembering at this
// call site is that bits 10 and 11 are SYMMETRIC - each row names the other - so the
// only two masks that reach the texture handle arm are "both set" and "neither set".
refused = PipeSubsystemDependencyMissing(mask, MG_Pipe::kMGPipeSubsystemTextureResources,
"kMGPipeSubsystemTextureResources (bit 10)");
}
const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm(
bitSet && !refused, /*legacyMemosEnabled=*/true,
/*legacyArmSurvivesLegacyMemos=*/false);
if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) {
BufferImpl::StopOnArmlessPipeSubsystem(
"MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemTextureResources (bit 10) clear (or "
"refuses it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle texture "
"cheap-gate trio");
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord() &&
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
StopOnTransportWithoutRecordArm(
"kMGPipeSubsystemTextureResources (bit 10)",
"the pre-handle texture cheap-gate trio resolves through the frontend texture "
"objects and their binding slots, which under a transport are client memory (rule "
"E) rather than anything this side owns");
}
#endif
const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles;
MGLOG_D("MGPipe: Espryt texture-resource family runs the %s arm", enabled ? "handle" : "legacy");
return enabled;
}
Bool ResolveSamplerSubsystemArm() {
const Uint64 mask = MG_Config::Features.PipePush;
const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemSamplers) != 0;
Bool refused = false;
if (bitSet) {
// The sampler family's row, from MG_Pipe/SubsystemDeps.def, and this is the pair G12
// drives at 0x9ff. Its mirror (bit 10 set, bit 11 clear) is NOT fine either - that is
// D-K2's fourth row, refused by ResolveTextureResourceSubsystemArm above - so the
// dependency is SYMMETRIC and the table carries both halves.
refused = PipeSubsystemDependencyMissing(mask, MG_Pipe::kMGPipeSubsystemSamplers,
"kMGPipeSubsystemSamplers (bit 11)");
}
const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm(
bitSet && !refused, /*legacyMemosEnabled=*/true,
/*legacyArmSurvivesLegacyMemos=*/false);
if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) {
BufferImpl::StopOnArmlessPipeSubsystem(
"MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemSamplers (bit 11) clear (or refuses "
"it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables UnitSamplerLookupMemo's WeakPtr "
"arm and SamplerPassMemo's raw-pointer rows");
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord() &&
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
StopOnTransportWithoutRecordArm(
"kMGPipeSubsystemSamplers (bit 11)",
"UnitSamplerLookupMemo's WeakPtr arm and SamplerPassMemo's raw-pointer rows are "
"keyed on FRONTEND objects, and under a transport there is no frontend object on "
"this side to key on - a WeakPtr that can never lock is not a slower arm, it is a "
"unit that samples through whatever it last held");
}
#endif
const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles;
MGLOG_D("MGPipe: Espryt sampler family runs the %s arm", enabled ? "handle" : "legacy");
return enabled;
}
Bool ResolveProgramSubsystemArm() {
// Bit 12 depends on NOTHING, and that is said out loud rather than left as an absence:
// a ShaderCso handle names no texture and no buffer, the archive rides beside the
// record as a companion pointer, and the eight extra inputs the server still
// specialises on (D-H5) are read from state this backend already holds.
const Bool bitSet = (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemPrograms) != 0;
const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm(
bitSet, /*legacyMemosEnabled=*/true, /*legacyArmSurvivesLegacyMemos=*/false);
if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) {
BufferImpl::StopOnArmlessPipeSubsystem(
"MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemPrograms (bit 12) clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 disables g_programTwinLookupMemo");
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord() &&
verdict != BufferImpl::PipeSubsystemArmVerdict::Handles) {
StopOnTransportWithoutRecordArm(
"kMGPipeSubsystemPrograms (bit 12)",
"g_programTwinLookupMemo is keyed on the frontend ProgramObject and the legacy arm "
"reads the current program through it; under a transport the program arrives as a "
"ShaderCso handle with its archive beside the record and there is no frontend "
"program here at all");
}
#endif
const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles;
MGLOG_D("MGPipe: Espryt program family runs the %s arm", enabled ? "handle" : "legacy");
return enabled;
}
// ---- P4a: the record readers (Managers.h documents the contract) ----
namespace {
// One shape for all five, so a kind cannot grow a different liveness rule than its
// neighbours: index the applier's own dense table, refuse a slot it has not grown to,
// refuse a record that is not Live, and refuse a generation that has moved on under
// the caller. The last of the three is the one that matters: a stale handle resolving
// to its successor's record is how a twin ends up describing another object's storage.
template <typename Record>
const Record* PipeRecordAt(const Vector<Record>& table, MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
if (handle.Slot >= table.size()) return nullptr;
const Record& record = table[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) return nullptr;
return &record;
}
} // namespace
const MG_Pipe::MGPipeResourceRecord* PipeTextureRecordForHandle(MG_Pipe::MGPipeHandle res) {
return PipeRecordAt(MG_Pipe::MGPipeApplier().TextureResources, res);
}
const MG_Pipe::MGPipeResourceRecord* PipeRenderbufferRecordForHandle(MG_Pipe::MGPipeHandle res) {
return PipeRecordAt(MG_Pipe::MGPipeApplier().RenderbufferResources, res);
}
// P3b/P4b (wave 2-D, package D3). See the declaration for why a view's storage clauses are
// asked of another record, why null means "ask again" and why the deletion order cannot
// strand this walk.
const MG_Pipe::MGPipeResourceRecord* PipeTextureStorageRecordForRecord(
const MG_Pipe::MGPipeResourceRecord& record) {
// ONE hop is the shape the client emits and the second is already generous; the bound is
// here so a cyclic chain is a null answer rather than a hang, not because a real chain
// is ever this long.
constexpr Uint32 kViewChainBound = 4;
const MG_Pipe::MGPipeResourceRecord* storage = &record;
for (Uint32 hop = 0; hop < kViewChainBound; ++hop) {
if (MG_Pipe::MGPipeHandleIsNull(storage->Desc.ViewOf)) return storage;
storage = PipeTextureRecordForHandle(storage->Desc.ViewOf);
if (storage == nullptr) return nullptr;
}
return nullptr;
}
MobileGL::TextureTarget PipeTextureTargetForHandle(MG_Pipe::MGPipeHandle res) {
const auto* record = PipeTextureRecordForHandle(res);
if (record == nullptr) return MobileGL::TextureTarget::Unknown;
// THE INVERSE OF MG_Pipe::MGPipeResourceTargetForTextureTarget, COMPUTED FROM IT rather
// than restated as a second table. MGPipeTypes.h already static_asserts that the
// forward map covers the frontend enum and that it separates the one pair anybody has
// ever folded together (2D vs rectangle), so a linear search over a dozen enumerators
// is total, cannot drift, and runs at most once per image unit per sweep. A second
// switch here is exactly how the two spellings end up disagreeing about a target that
// was added to only one of them.
for (SizeT i = 0; i < static_cast<SizeT>(MobileGL::TextureTarget::TextureTargetCount); ++i) {
const auto target = static_cast<MobileGL::TextureTarget>(i);
if (MG_Pipe::MGPipeResourceTargetForTextureTarget(target) ==
static_cast<Uint32>(record->Desc.Target)) {
return target;
}
}
return MobileGL::TextureTarget::Unknown;
}
const MG_Pipe::MGPipeSamplerCsoRecord* PipeSamplerCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) {
return PipeRecordAt(MG_Pipe::MGPipeApplier().SamplerCsos, cso);
}
const MG_Pipe::MGPipeSamplerViewRecord* PipeSamplerViewRecordForHandle(MG_Pipe::MGPipeHandle view) {
return PipeRecordAt(MG_Pipe::MGPipeApplier().SamplerViewCsos, view);
}
const MG_Pipe::MGPipeShaderCsoRecord* PipeShaderCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) {
// The composite band is a CLIENT-side indexing detail (contract D13) and the server
// must not learn about it: one reader, one answer, and the band's own table is indexed
// by (slot - base) exactly as the allocator's is.
if (MG_Pipe::MGPipeIsCompositeShaderSlot(cso.Slot)) {
const auto& band = MG_Pipe::MGPipeApplier().CompositeShaderCsos;
const Uint32 index = cso.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
if (index >= band.size()) return nullptr;
const auto& record = band[index];
if (!record.Live || record.Gen != cso.Gen) return nullptr;
return &record;
}
return PipeRecordAt(MG_Pipe::MGPipeApplier().ShaderCsos, cso);
}
// N-7: D KEYS ON THE UPLOAD HALF; WIRE (AccumulatePendingUpload, PipeApply.cpp:814) KEYS ON
// THE WHOLE PACKED FIELD. That asymmetry is resolved HERE, in D's favour and with the
// reason, rather than by widening the two comparisons: D's question is per (uploadTarget,
// level) because that is the granularity its upload loop iterates - it is handed a
// TextureUploadTarget and a level and has no second key to offer - while wire's is per
// record, where the whole field arrived. They agree because a texture's resource-target
// byte is constant for the object's life; if that ever stopped holding, wire would store
// two entries where D sees one and D's consume would strand the second in the set for ever
// (a full re-upload of that level on every sync, silently).
//
// So the low byte is CROSS-CHECKED instead of ignored. The comparand is the resource's own
// descriptor Target, which B fills from exactly the expression the sub-data's low byte
// comes from (TextureEmit.h:223 / :517 against :1037), so the two are carried and equal by
// construction and any disagreement is a seam defect, not a shape to learn. This is also
// MGPResourceDesc::Target's first reader in this package (esprytobj-v2.md §2(6) recorded it
// as having none).
static void NotePipeSubDataTargetLowByte(const MG_Pipe::MGPipeResourceRecord& record, Uint16 packedTarget,
Uint16 level) {
const Uint8 carried = MG_Pipe::MGPipeSubDataResourceTargetOf(packedTarget);
if (carried == record.Desc.Target) return;
MGLOG_E_ONCE("MGPipe: a pending upload for level %u carries resource target %u while its "
"resource's descriptor says %u - D matches on the upload half only, so this "
"entry was matched anyway; the two sides disagree about what the resource is",
static_cast<Uint>(level), static_cast<Uint>(carried),
static_cast<Uint>(record.Desc.Target));
}
const MG_Pipe::MGPipeResourceRecord::PendingUpload* FindPipeTextureUpload(
const MG_Pipe::MGPipeResourceRecord& record, Uint16 uploadTarget, Uint16 level) {
// Linear, and deliberately so: the set holds one entry per DIRTY (uploadTarget, level)
// of one texture, which is at most six faces x the level count and in practice one or
// two entries. A map would cost an allocation per texture per frame to save a walk of
// three.
//
// THE STORED KEY IS THE PACKED MGPSubData::Target (ID-12): low byte
// MGPipeResourceTarget, HIGH byte TextureUploadTarget. The applier stores the whole
// field verbatim (PipeApply.cpp's `entry.UploadTarget = record.Target`) because
// MGPRespecifiedLevel::UploadTarget has to pair with it byte for byte, so the decode is
// the reader's - here and at the consume, the only two places D compares it. `uploadTarget`
// is static_cast<Uint16>(MobileGL::TextureUploadTarget), i.e. already the half.
// Without the decode every texture upload would be dropped silently: Texture1D is 0 and
// Texture2D is 1, which collide with the resource-target byte of Buffer and Tex1D.
for (const auto& pending : record.PendingUploads) {
if (MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget) == static_cast<Uint8>(uploadTarget) &&
pending.Level == level) {
NotePipeSubDataTargetLowByte(record, pending.UploadTarget, level);
return &pending;
}
}
return nullptr;
}
void ConsumePipeTextureUpload(MG_Pipe::MGPipeHandle res, Uint16 uploadTarget, Uint16 level) {
// Re-resolved rather than taken as a reference from the caller: between the read and
// the consume the caller has issued driver work, and the applier's Vector may have been
// grown by a re-entrant call in between. Cheap - the resolve is a bounds check and two
// compares - and it is the difference between a dangling reference and a no-op.
auto& applier = MG_Pipe::MGPipeApplier();
if (MG_Pipe::MGPipeHandleIsNull(res) || res.Slot >= applier.TextureResources.size()) return;
auto& record = applier.TextureResources[res.Slot];
if (!record.Live || record.Gen != res.Gen) return;
for (SizeT index = 0; index < record.PendingUploads.size(); ++index) {
const auto& pending = record.PendingUploads[index];
// The same HIGH-byte decode FindPipeTextureUpload makes, and it has to be the same
// one: a consume that missed here after a find that hit would leave the entry in the
// set forever and re-upload the level on every sync.
if (MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget) != static_cast<Uint8>(uploadTarget) ||
pending.Level != level) {
continue;
}
NotePipeSubDataTargetLowByte(record, pending.UploadTarget, level);
// Swap-and-pop: the set is unordered by construction (it is a per-(target, level)
// accumulation, not a queue), and an ordered erase would be quadratic over a full
// cube-map drain.
record.PendingUploads[index] = std::move(record.PendingUploads.back());
record.PendingUploads.pop_back();
return;
}
}
Bool RearmPipeTextureLevelUpload(MG_Pipe::MGPipeHandle res, Uint16 packedTarget, Uint16 level,
const MG_Pipe::MGPBox& wholeLevel) {
// THE SERVER RE-DIRTYING THE CLIENT'S MODEL IS THE ONE DIRECTION D-D5's INVERSION DOES
// NOT COVER, and this is the server-side answer to it (esprytobj review M-1).
//
// D-D5 moves the rect model and the emission cursor to the CLIENT, which clears its own
// flags at emission - so on the handle arm IsStorageDirty is already false for every
// level the applier accepted, and the upload loop reads the applier's pending set
// instead. But three sites in this backend still write into the frontend dirty model,
// and the pending set cannot see them:
//
// Managers.cpp RequireImageBindableStorage - re-dirties every defined level so the
// widened image carrier is replayed rather than allocated empty (LIVE,
// and it is this helper's only caller);
// DirectGLES.cpp:7406 GenerateThreeChannelFloatMipmapOnCpu - same shape, milder
// outcome (package E's file; E's verification round calls this helper
// there, or states why not);
// DirectGLES.cpp:6735 the CLEAR half (MarkStorageDirty(..., false)) after a
// glGetTexImage shadow refresh - it clears rather than dirties, so
// nothing is owed and there is nothing to re-arm.
//
// The alternative the review offered - OR the frontend flag back into the per-level
// question - was NOT taken: it would put two authorities back on the handle arm, and
// "the server never clears the client's flag" (D-D5) would then have to grow an
// exception for the levels the OR consumed. Re-arming the set keeps ONE authority; the
// consume path is unchanged and clears exactly what it uploaded.
//
// The entry is WHOLE-LEVEL and carries no regions (RegionCount 0 = "the union box is the
// whole story", D-D3), because a replay owes every texel of the level, and it MERGES
// with an entry the client already emitted rather than duplicating it - a whole-level
// box subsumes any scatter behind it. Returns false, loudly, when the set is at its
// bound: a dropped replay is the bug this exists to stop, so it must not be silent.
auto& applier = MG_Pipe::MGPipeApplier();
if (MG_Pipe::MGPipeHandleIsNull(res) || res.Slot >= applier.TextureResources.size()) return false;
auto& record = applier.TextureResources[res.Slot];
if (!record.Live || record.Gen != res.Gen) return false;
const Uint8 uploadHalf = MG_Pipe::MGPipeSubDataUploadTargetOf(packedTarget);
for (auto& pending : record.PendingUploads) {
if (MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget) != uploadHalf ||
pending.Level != level) {
continue;
}
pending.UnionBox = wholeLevel;
pending.Regions.clear();
return true;
}
if (record.PendingUploads.size() >= MG_Pipe::kMGPipeMaxPendingUploads) {
MGLOG_E_ONCE("MGPipe: texture handle {%u, %u} already holds the applier's %u pending "
"uploads, so the server's own re-dirty of (uploadTarget=%u, level=%u) "
"cannot be armed - that level will not be replayed",
res.Slot, res.Gen, MG_Pipe::kMGPipeMaxPendingUploads, uploadHalf, level);
return false;
}
MG_Pipe::MGPipeResourceRecord::PendingUpload entry;
entry.UploadTarget = packedTarget;
entry.Level = level;
entry.UnionBox = wholeLevel;
record.PendingUploads.push_back(std::move(entry));
return true;
}
namespace SamplerViewImpl {
BackendSamplerViewTable g_backendSamplerViews;
BackendSamplerViewObject* GetOrCreateSamplerViewForHandle(MG_Pipe::MGPipeHandle view) {
if (MG_Pipe::MGPipeHandleIsNull(view)) return nullptr;
// The table refuses both of these itself; this is the release-build VOICE for the
// refusal, because MOBILEGL_ASSERT compiles out at INFO and a view that silently
// stops being twinned is the failure mode the refusal exists to replace. Same shape
// as GetOrCreateBufferResourceForHandle's.
if (view.Slot >= BackendSamplerViewTable::kMaxHandleSlot) {
MGLOG_E_ONCE("MGPipe: sampler-view handle slot %u is past the backend table's %u "
"bound - refusing to twin it",
view.Slot, BackendSamplerViewTable::kMaxHandleSlot);
return nullptr;
}
const Uint32 liveGen = g_backendSamplerViews.LiveGenAt(view.Slot);
if (liveGen != 0 && liveGen > view.Gen) {
MGLOG_E_ONCE("MGPipe: sampler-view handle {%u, %u} names a generation BEHIND the "
"live twin's %u - refusing rather than dropping the incumbent",
view.Slot, view.Gen, liveGen);
return nullptr;
}
auto& twin = g_backendSamplerViews.GetOrCreate(view);
if (!twin) twin = MakeShared<BackendSamplerViewObject>();
return twin.get();
}
BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view) {
auto* twin = g_backendSamplerViews.FindByHandle(view);
return twin ? twin->get() : nullptr;
}
MG_Pipe::MGPipeHandle HandleOfSamplerViewForTexture(
const MG_State::GLState::ITextureObject* textureObject) {
// Through the table's own single-entry front memo rather than straight into
// MGPipeSlots().FindByLifetimeId, which is a hash lookup. HandleOf answers
// identically - the same allocator probe on a miss - and remembers the answer; a
// null answer is deliberately never memoised (SlotTables.h, at HandleOf).
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the sampler-view registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
return g_backendSamplerViews.HandleOf(textureObject);
}
} // namespace SamplerViewImpl
namespace VertexArrayImpl {
namespace {
SizeT GetDataTypeSize(DataType type) {
switch (type) {
case DataType::Int8:
case DataType::Uint8:
return 1;
case DataType::Int16:
case DataType::Uint16:
case DataType::Float16:
return 2;
case DataType::Int32:
case DataType::Uint32:
case DataType::Float32:
case DataType::Fixed32:
return 4;
case DataType::Float64:
return 8;
default:
return 0;
}
}
// Tightly-packed byte size of one vertex element: 4 for the 2_10_10_10 types and GL_BGRA
// (one 32-bit word / 4 bytes), componentSize * size otherwise. 0 for unknown types.
SizeT GetAttributeByteSize(DataType type, int size, Bool isBgra) {
if (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev || isBgra) {
return 4;
}
const SizeT componentSize = GetDataTypeSize(type);
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
}
// Deinterleaves elementCount elements of componentCount doubles into a tightly packed
// float32 stream - the fetch half of the fp64 demotion the shader side already does
// unconditionally (DemoteFloat64Pass). GL byte strides and offsets are arbitrary, so
// no component carries an 8-byte alignment guarantee and each is copied out before it
// is narrowed.
void NarrowDoubleStreamToFloat32(const Uint8* sourceBase, SizeT sourceStride, SizeT componentCount,
SizeT elementCount, Vector<Float>& outData) {
outData.resize(elementCount * componentCount);
for (SizeT element = 0; element < elementCount; ++element) {
const Uint8* sourceElement = sourceBase + element * sourceStride;
Float* destinationElement = outData.data() + element * componentCount;
for (SizeT component = 0; component < componentCount; ++component) {
Double value = 0.0;
Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double));
destinationElement[component] = static_cast<Float>(value);
}
}
}
} // namespace
BackendVertexArrayObject::BackendVertexArrayObject() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
m_clientAttributeBufferIds.fill(0);
m_convertedAttributeBufferIds.fill(0);
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
if (m_backendVAOId == 0) {
MGLOG_E_ONCE("Failed to generate vertex array object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated vertex array object with ID: %u.", m_backendVAOId);
}
}
BackendVertexArrayObject::~BackendVertexArrayObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendVAOId != 0) {
NoteVAOIdDeleted(m_backendVAOId);
g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId);
m_backendVAOId = 0;
}
for (auto& bufferId : m_clientAttributeBufferIds) {
if (bufferId != 0) {
BufferImpl::NoteBufferIdDeleted(bufferId);
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
bufferId = 0;
}
}
for (auto& bufferId : m_convertedAttributeBufferIds) {
if (bufferId != 0) {
BufferImpl::NoteBufferIdDeleted(bufferId);
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
bufferId = 0;
}
}
}
namespace {
Uint g_boundBackendVAOId = 0;
Bool g_boundBackendVAOKnown = false;
} // namespace
void BindBackendVAOId(Uint id) {
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
return;
}
g_GLESFuncs.glBindVertexArray(id);
g_boundBackendVAOId = id;
g_boundBackendVAOKnown = true;
}
void InvalidateVAOBindingCache() {
g_boundBackendVAOKnown = false;
}
void NoteVAOIdDeleted(Uint id) {
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0
}
}
void BackendVertexArrayObject::Bind() const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
BindBackendVAOId(m_backendVAOId);
}
inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) {
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
return false;
}
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
return false;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id);
return true;
}
// ES 3.1 core. Queried through the loader rather than the version, because the whole
// point of using it is to express something the pointer API cannot, and falling back
// silently on a driver that lacks it is better than crashing on a null entry point.
inline Bool HasVertexBindingApi() {
return g_GLESFuncs.glBindVertexBuffer != nullptr && g_GLESFuncs.glVertexAttribFormat != nullptr &&
g_GLESFuncs.glVertexAttribIFormat != nullptr && g_GLESFuncs.glVertexAttribBinding != nullptr &&
g_GLESFuncs.glVertexBindingDivisor != nullptr;
}
Bool BackendUsesNativeBaseInstance() { return g_GLESCapabilities.SupportsBaseInstance; }
// The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift
// of the array's own offset. Only divisor'd arrays step per instance, so only they move.
//
// baseInstance is added to the ELEMENT index, not to instance/divisor - the divisor
// therefore does not appear here, and the shift is a whole number of strides.
//
// A resolved stride of zero is the binding model's "never advance" (see
// VertexAttribute::Stride), so such an array reads the same element for every instance
// and a baseInstance cannot move it. The arithmetic already yields zero for that case.
inline SizeT BaseInstanceByteShift(const MG_State::GLState::VertexAttribute& attrib, Uint32 baseInstance) {
if (baseInstance == 0 || attrib.Divisor == 0) {
return 0;
}
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(attrib.Stride);
}
// Declares one attribute through the ES binding-point API, the only spelling that can
// carry a stride of zero. Returns false when the attribute has no usable buffer, in
// which case nothing was emitted.
inline Bool SyncZeroStrideAttribute(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib) {
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
return false;
}
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
return false;
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
} else {
g_GLESFuncs.glVertexAttribIFormat(attribIndex, attrib.Size,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type), 0);
}
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
// The resolved offset goes on the binding point, not into a relative offset: the
// relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET (2047 at
// minimum) while a buffer offset is not, so anything else would break on a large
// one. BindBufferId is bypassed deliberately - glBindVertexBuffer binds into the
// VAO's binding point, not the GL_ARRAY_BUFFER target that cache tracks.
g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id,
static_cast<GLintptr>(attrib.Offset), 0);
return true;
}
// ---- the handle arm's small helpers -------------------------------------------
//
// Each one is its object-shaped counterpart above with the frontend reads replaced by
// the two wire views, and nothing else. Divisor is deliberately NOT on the attribute
// view: it is resolved per binding point and rides in MGPVertexBuffer::Divisor, which
// is where glVertexAttribDivisor reads it (D-G2).
// The entry of the applied set that feeds ATTRIBUTE `attributeIndex`.
//
// THE KEY IS THE ATTRIBUTE INDEX, NOT MGPVertexAttribWire::BindingIndex, and the two
// live in different spaces. Espryt consumes RESOLVED attributes, so the client emits
// set_vertex_buffers as one entry per attribute slot with
// MGPVertexBuffer::BindingIndex == the attribute index (VertexInputEmit.h:190-193,
// :229 - "Espryt consumes RESOLVED attributes, so the set is one entry per attribute
// slot with BindingIndex == the attribute index"), each carrying that attribute's
// already-folded buffer, stride and divisor. MGPVertexAttribWire::BindingIndex is the
// OTHER thing: the GL binding POINT the attribute was attached to by
// glVertexAttribBinding, which is the key of the binding-point view this arm never
// reads (it needs no separate view, because the resolution already happened on the
// client). The two coincide whenever the attribute was configured through
// glVertexAttribPointer, which is why every scenario that uses the pointer API stayed
// green while glVertexAttribBinding(2, 3) / (0, 5) fetched the wrong entry or none -
// KHR-GL43.vertex_attrib_binding's whole subject.
const MG_Pipe::MGPVertexBuffer* VertexBufferForAttributeIndex(const MG_Pipe::MGPipeApplierState& st,
Uint32 attributeIndex) {
if (st.VertexBufferCount == 0) return nullptr;
const Uint32 begin = st.VertexBufferStart;
const Uint32 end = begin + st.VertexBufferCount;
if (attributeIndex >= begin && attributeIndex < end &&
attributeIndex < MG_Pipe::kMGPipeMaxVertexAttribs &&
st.VertexBuffers[attributeIndex].BindingIndex == attributeIndex) {
return &st.VertexBuffers[attributeIndex];
}
for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) {
if (st.VertexBuffers[i].BindingIndex == attributeIndex) return &st.VertexBuffers[i];
}
return nullptr;
}
// BaseInstanceByteShift, on the wire views. baseInstance is added to the ELEMENT index,
// so the divisor does not appear here; a resolved stride of zero never advances and the
// arithmetic already yields zero for it.
inline SizeT BaseInstanceByteShiftWire(Int32 stride, Uint32 divisor, Uint32 baseInstance) {
if (baseInstance == 0 || divisor == 0) return 0;
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(stride);
}
// BindAttributeBuffer, resolving the driver id from the slot table instead of from the
// attribute's SharedPtr. It does NOT ensure storage: on this arm the draw's buffers
// were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw, which is
// the one place that still holds the frontend objects (a pull site P4b/P8 own).
inline Bool BindAttributeBufferByHandle(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) {
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
return false;
}
auto* backendResource = BufferImpl::FindBufferResourceForHandle(res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
return false;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id);
return true;
}
// SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved
// stride of zero, which glVertexAttribPointer's zero means the opposite of.
inline Bool SyncZeroStrideAttributeByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib,
const MG_Pipe::MGPVertexBuffer& binding) {
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
return false;
}
auto* backendResource = BufferImpl::FindBufferResourceForHandle(binding.Res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
return false;
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : static_cast<GLint>(attrib.Size);
g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize,
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
} else {
g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast<GLint>(attrib.Size),
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
0);
}
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
// The resolved offset goes on the binding point, not into a relative offset (the
// relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET, a buffer
// offset is not). BindBufferId is bypassed deliberately.
g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id,
static_cast<GLintptr>(attrib.Offset), 0);
return true;
}
// The record the applier holds for the bound vertex-elements CSO, or null.
const MG_Pipe::MGPipeVertexElementsRecord* BoundVertexElementsRecord(
const MG_Pipe::MGPipeApplierState& st) {
const MG_Pipe::MGPipeHandle cso = st.BoundVertexElements;
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
if (cso.Slot >= st.VertexElementsCsos.size()) return nullptr;
const auto& record = st.VertexElementsCsos[cso.Slot];
if (!record.Live || record.Gen != cso.Gen) return nullptr;
return &record;
}
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract; the body is the one
// SamplerImpl::ResolveSamplerCsoTwin already runs, with the sync left to vi.
BackendVertexArrayObject* ResolveVaoTwin(MG_Pipe::MGPipeHandle elements) {
if (MG_Pipe::MGPipeHandleIsNull(elements)) return nullptr;
// THE RECORD FIRST, before the table is touched: a handle with no record means the
// client named a vertex-elements CSO it never described (or one it deleted while a
// binding still names it), and adopting a slot for it would leave a twin nothing
// can ever sync. The applier's own Gen check is what turns a stale handle into this
// null rather than into its successor's record.
if (PipeRecordAt(MG_Pipe::MGPipeApplier().VertexElementsCsos, elements) == nullptr) {
MGLOG_E_ONCE("MGPipe: vertex-elements CSO {%u, %u} has no applier record on the handle "
"arm, so no driver VAO can be built for it and the draw keeps what is bound",
elements.Slot, elements.Gen);
return nullptr;
}
auto* slot = AdoptTwinByHandle(g_backendVertexArrayObjects, elements, "vertex-elements CSO");
if (slot == nullptr) return nullptr;
if (!*slot) *slot = MakeShared<BackendVertexArrayObject>();
return slot->get();
}
void BackendVertexArrayObject::SyncToBackend(
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (BufferImpl::VertexInputSubsystemEnabled()) {
// Everything this arm needs is in the applier's records; the frontend VAO is
// not read at all, which is the whole point of the conversion.
SyncToBackendFromApplier();
return;
}
// UNREACHABLE as of M-4: VertexInputSubsystemEnabled() resolves the arm at its first
// call, and an armless verdict now stops the process there rather than returning
// false into this branch. Kept, and kept loud, because it is the second lock on the
// same question: this is what an arm resolution that ever stopped stopping would
// reach, and drawing on through an unconfigured driver VAO is exactly what must not
// happen quietly.
(void)stateVAOObject;
MGLOG_E_ONCE("MGPipe: the vertex-input subsystem bit is clear and MOBILEGL_PIPE_LEGACY_MEMOS=0 "
"removed the pre-handle VAO sync, so this configuration has no arm at all");
return;
}
// The same function, driven by the applier's records (D-G4). Every branch of the walk
// survives - the enable/disable block, the fp64 narrowing with its Adreno disable, the
// zero-stride binding-API path, the attribute-buffer bind, the BGRA refusal probe, the
// pointer/IPointer at the shifted fetch offset and the divisor - and the gate is
// re-keyed onto three server-owned MGGen counters plus the CSO's {slot, gen}:
//
// m_syncedElementsHandle + m_syncedElementsSerial <- config version + the whole
// per-attribute version array
// m_syncedVertexBuffersSerial <- (new) the buffer set's own
// m_syncedIndexSerial <- wrapping Uint16 + identity patch
// m_syncedBufferIdGeneration <- UNCHANGED, server-local, and
// still the only thing that
// catches a driver-id re-mint no
// counter on either side moves
// m_hasConvertedFloat64Attribute <- UNCHANGED: a narrowed stream is
// derived from buffer CONTENT,
// which no serial covers
// m_syncedFetchBaseInstance <- no longer in the gate as a
// DIRTY input of its own (a base
// instance change is a
// ContentHash input and so moves
// VertexBuffersSerial), but kept
// as the record of what was last
// EMITTED, which is what the next
// sync has to correct
void BackendVertexArrayObject::SyncToBackendFromApplier() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const auto* rec = BoundVertexElementsRecord(st);
if (rec == nullptr) {
MGLOG_E_ONCE("MGPipe: no vertex-elements record is bound, so the driver VAO cannot be "
"configured - nothing emitted create_vertex_elements/bind_vertex_elements");
return;
}
const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration;
const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration;
// THIS GATE DEPENDS ON A SERIAL RULE THAT LIVES IN ANOTHER PACKAGE, and it is named
// here because nothing else in this file would say it: MGPipeApplierReset() runs on
// EVERY change of the current GLContext (MG_Impl/Pipe/Tracker.h's `if (m_context !=
// &ctx) Reset();`, not only on a fresh one), while this twin SURVIVES the excursion -
// BackendVertexArrayObject has no context-generation member and
// OnBackendContextDestroyed runs on destroy, not on make-current. A reset that sent
// VertexBuffersSerial and IndexBufferSerial back to ZERO would therefore walk them
// back through values this twin has already stamped, and a memo could read clean over
// state the applier had just cleared. MG_Pipe/PipeApply.cpp's reset must ADVANCE
// those two serials instead (wire's C2), which is what makes
// m_syncedVertexBuffersSerial below unable to match after a reset - and that in turn
// forces the whole AND dirty, which is also what rescues the per-record ContentSerial
// half (an applier reset is not a slot recycle, so a re-created CSO at the same
// {slot, gen} restarts its ContentSerial at 1). If that rule is ever reverted, this
// gate is unsafe on any application that changes contexts.
const Bool attributesDirty = bufferIdsRemitted || !m_hasSyncedElements ||
!(m_syncedElementsHandle == st.BoundVertexElements) ||
m_syncedElementsSerial != rec->ContentSerial ||
m_syncedVertexBuffersSerial != st.VertexBuffersSerial;
const Bool indexBufferDirty = bufferIdsRemitted || m_syncedIndexSerial != st.IndexBufferSerial;
// Emulation is server-owned: the client sends the draw's RAW base instance and never
// learns the answer. Applied here as well as in the applier so the decision is the
// same whichever side resolved it first - it is idempotent.
const Uint32 fetchBaseInstance =
BackendUsesNativeBaseInstance() ? 0u : st.VertexFetchBaseInstance;
const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance;
const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedFloat64Attribute;
if (!emitAttributes && !indexBufferDirty) {
return;
}
m_hasConvertedFloat64Attribute = false;
Bind();
// ALL 32 SLOTS, not rec->AttributeCount, and the difference is the disable arm.
// The legacy walk ran over the frontend's whole 32-slot attribute array and reached
// glDisableVertexAttribArray for every attribute that was off; a walk bounded by the
// record's count leaves an attribute the configuration has just DROPPED enabled in
// the driver VAO, pointing at whatever buffer it last held - which is the class the
// first Adreno workaround below exists to prevent. Nothing in the contract requires
// the client to emit 32 entries (D-H3 sets the precedent the other way for the
// BUFFER set: "truncated to the highest enabled attribute + 1"), so the bound was a
// requirement on B that was neither stated nor pinned. It costs nothing to drop it:
// MGPipeApplyCreateVertexElements zeroes both arrays before it unpacks, so every
// entry past AttributeCount reads Enabled = 0, Type = 0 (Int8, not Float64) and
// IsLong = 0 - i.e. exactly "disable this array", with no dependency on B at all.
for (Uint attribIndex = 0; attribIndex < MG_Pipe::kMGPipeMaxVertexAttribs && emitAttributes;
++attribIndex) {
const MGPVertexAttribWire& attrib = rec->Attributes[attribIndex];
const MG_Pipe::MGPVertexBuffer* binding =
VertexBufferForAttributeIndex(st, attribIndex);
const Uint32 divisor = binding != nullptr ? binding->Divisor : 0u;
// The enable/disable block. On this arm there is no per-attribute version to
// compare: the applier's Attributes[] IS what was last pushed, so a moved
// ContentSerial means re-emit and an unchanged one means the early-out above
// already returned.
if (attrib.Enabled) {
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
} else {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
}
// The fp64 narrowing, verbatim in behaviour including the Adreno workaround:
// when no float32 stream can be built the array is DISABLED rather than left
// enabled with no pointer, which is what the driver turns into a SIGSEGV inside
// the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case4). IsLong is
// carried separately from Type == Float64 on the wire precisely so this test
// can still tell the two apart.
if (attrib.IsLong || attrib.Type == static_cast<Uint32>(DataType::Float64)) {
if (attrib.Enabled && attrib.Type == static_cast<Uint32>(DataType::Float64) &&
binding != nullptr &&
SyncFloat64AttributeAsFloat32ByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) {
m_hasConvertedFloat64Attribute = true;
// Explicit, not redundant: an earlier walk that could not build the
// stream disabled this array.
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor);
continue;
}
if (attrib.Enabled) {
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array whose source "
"stream could not be narrowed to float32 - disabling the array",
attribIndex);
}
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
if (!attrib.Enabled || binding == nullptr) continue;
// A resolved stride of zero is the binding model's "never advance" and
// glVertexAttribPointer cannot say it - its zero means "tightly packed", i.e.
// the opposite. ES 3.1's binding-point API can.
if (attrib.Stride == 0 && HasVertexBindingApi()) {
if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) {
continue;
}
// No shift here on purpose: a zero stride never advances.
g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor);
continue;
}
if (!BindAttributeBufferByHandle(binding->Res)) {
continue;
}
// GL_BGRA as a vertex SIZE is desktop-only and ES rejects it, which leaves the
// array ENABLED with no pointer - and the Adreno driver then dereferences null
// inside the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case5). So
// the refusal is observed and the array disabled. Deliberately ONLY this
// format: the per-draw sync must not grow a glGetError round trip for the
// formats real applications use.
const Bool formatMayBeRefused = attrib.IsBgra != 0;
if (formatMayBeRefused) {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
} // start from a clean slate so the check below is about THIS call
}
const SizeT fetchOffset = static_cast<SizeT>(attrib.Offset) +
BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance);
const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type));
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : static_cast<GLint>(attrib.Size);
g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType,
attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride,
(const void*)fetchOffset);
} else {
g_GLESFuncs.glVertexAttribIPointer(attribIndex, static_cast<GLint>(attrib.Size), glType,
attrib.Stride, (const void*)fetchOffset);
}
if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
MGLOG_W_ONCE("DirectGLES: the driver refused the vertex format of attribute %u "
"(size=%d bgra=%d type=%s) - disabling the array so the draw cannot "
"fetch through a pointer the driver never accepted",
attribIndex, static_cast<int>(attrib.Size), attrib.IsBgra ? 1 : 0,
MG_Util::ConvertGLEnumToString(glType).c_str());
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor);
}
if (indexBufferDirty) {
Bool indexBufferSynced = false;
if (!MG_Pipe::MGPipeHandleIsNull(st.IndexBuffer.Res)) {
// RESOLVE ONLY, no ensure - unlike the legacy arm, which called
// EnsureBufferResource here. PrepareForDraw runs SyncNeccessaryBuffers before
// SyncCurrentVAO, so an INDEXED draw has already ensured this store; a
// non-indexed draw whose IndexBufferSerial moved has not, and then this logs
// once and leaves the binding alone. That is noisy rather than wrong: nothing
// is stamped on the miss (indexBufferSynced stays false), so the next indexed
// draw repairs it. Ensuring here would need the frontend object this arm
// deliberately does not hold.
auto* backendResource = BufferImpl::FindBufferResourceForHandle(st.IndexBuffer.Res);
if (backendResource && backendResource->id != 0) {
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id);
indexBufferSynced = true;
} else {
MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer.");
}
} else {
g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
indexBufferSynced = true;
}
if (indexBufferSynced) {
// The two element-array restore scopes (the restart substitution's and
// MultiDrawImpl's) put the DRIVER id back without touching this serial,
// which is correct: the serial records what the APPLIER last said, and
// those scopes restored exactly what the applier said.
m_syncedIndexSerial = st.IndexBufferSerial;
}
}
if (attributesDirty) {
m_syncedElementsHandle = st.BoundVertexElements;
m_syncedElementsSerial = rec->ContentSerial;
m_syncedVertexBuffersSerial = st.VertexBuffersSerial;
m_hasSyncedElements = true;
}
if (emitAttributes) {
m_syncedFetchBaseInstance = fetchBaseInstance;
}
m_syncedBufferIdGeneration = currentBufferIdGeneration;
}
// THE CLIENT-ARRAY UPLOAD FOR A DRAW WHOSE FETCHED ELEMENTS (first, count) DOES NOT
// DESCRIBE. SyncClientSideAttributesForDrawArrays below answers the non-indexed family
// exactly and nothing else: an indexed draw reads the elements its INDICES name, an
// instanced draw reads one element per instance (shifted by baseInstance and divided by
// divisor), and the indirect forms read a count and a first element that live in GPU
// memory. Until this function existed those draws took NO upload at all - the enabled
// client attribute kept whatever the ES context last held for it (normally: disabled, so
// every vertex read the generic current value), and an indexed draw over a client array
// painted nothing.
//
// `plan` is MGPipeClientFetchPlan's answer for this very draw, which is the same
// arithmetic the wire arm snapshots from (MG_Impl/Pipe/OwnedDrawInputs.h) - the two arms
// fetch the same elements by construction rather than by inspection.
//
// THE UPLOAD IS SPARSE: only the fetched elements are dereferenced, and the gaps between
// them are zero-filled. Reading the intervening application memory would be legal here -
// one thread, one address space - but it is not needed, and a draw naming element 0 and
// element 2^20 must not turn into an 8 MiB read of somebody else's heap.
//
// False means the draw must be SKIPPED: the elements it would fetch are not in the store
// the driver is about to read, and issuing it anyway is a wrong picture, not an error.
Bool BackendVertexArrayObject::SyncClientSideAttributesForDraw(
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject,
const MG_Pipe::MGPipeClientFetchPlan& plan, Uint32 fetchBaseInstance) {
if (!stateVAOObject) return true;
Bind();
const auto& allAttributes = stateVAOObject->GetAllAttributes();
Vector<Uint64> elements;
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[attribIndex];
if (!attrib.Enabled || attrib.Buffer) continue;
// The same outcome SyncToBackend reaches for a 64-bit client array it cannot
// narrow: the enabled array is DROPPED (the attribute reads its generic current
// value) rather than left enabled with no pointer behind it, which is the Adreno
// null-deref. The narrowing stream itself stays the arrays path's answer.
if (attrib.IsLong || attrib.Type == DataType::Float64) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
if (!plan.Elements(static_cast<Uint32>(attrib.Divisor), elements)) return false;
if (elements.empty()) continue;
const auto* source = reinterpret_cast<const Uint8*>(attrib.Offset);
const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra);
if (source == nullptr || elementSize == 0 || attrib.Size <= 0) {
MGLOG_E_ONCE("Client attribute %u has no readable element size (%zu); the draw is skipped",
attribIndex, elementSize);
return false;
}
const SizeT stride = attrib.Stride > 0 ? static_cast<SizeT>(attrib.Stride) : elementSize;
const Uint64 last = elements.back();
if (stride != 0 && last > (std::numeric_limits<SizeT>::max() - elementSize) / stride) return false;
const SizeT byteCount = static_cast<SizeT>(last * stride + elementSize);
auto& bufferId = m_clientAttributeBufferIds[attribIndex];
if (bufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &bufferId);
if (bufferId == 0) {
MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer.");
return false;
}
}
Vector<Uint8> snapshot(byteCount, 0);
for (const Uint64 element : elements) {
const SizeT offset = static_cast<SizeT>(element * stride);
std::memcpy(snapshot.data() + offset, source + offset, elementSize);
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(snapshot.size()),
snapshot.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(snapshot.size()));
}
// Element 0 of a client array sits at its buffer's byte 0, so a divisor'd array's
// baseInstance shift is BaseInstanceByteShift - the same arithmetic SyncToBackend
// applies to a VBO-backed array - and it rides in the pointer argument.
const void* const shift =
reinterpret_cast<const void*>(BaseInstanceByteShift(attrib, fetchBaseInstance));
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attrib.Normalized ? GL_TRUE : GL_FALSE,
static_cast<GLsizei>(stride), shift);
} else {
g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
static_cast<GLsizei>(stride), shift);
}
// The divisor is NOT optional here. SyncToBackend's own glVertexAttribDivisor sits
// behind BindAttributeBuffer, which a client-memory attribute never passes, so
// until this line the ES context's divisor for this index is whatever the last
// VBO-backed attribute left there.
g_GLESFuncs.glVertexAttribDivisor(attribIndex, attrib.Divisor);
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
}
return true;
}
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count,
Uint32 fetchBaseInstance) {
if (!stateVAOObject || count <= 0 || first < 0) {
return;
}
Bind();
const auto& allAttributes = stateVAOObject->GetAllAttributes();
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[attribIndex];
if (!attrib.Enabled || attrib.Buffer) {
continue;
}
// Same narrowing as SyncToBackend (the long note lives there), with the draw's own
// fetch range standing in for the buffer extent a client array does not have. The
// upload starts at element 0 so `first` keeps indexing the stream, exactly as the
// unconverted upload below does. The test is on the storage rather than on IsLong
// because glVertexAttribFormat(GL_DOUBLE) is 64-bit data without being long.
if (attrib.IsLong || attrib.Type == DataType::Float64) {
const auto* sourceBase = reinterpret_cast<const Uint8*>(attrib.Offset);
if (attrib.Type != DataType::Float64 || sourceBase == nullptr || attrib.Size < 1 ||
attrib.Size > 4) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
auto& bufferId = m_clientAttributeBufferIds[attribIndex];
if (bufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &bufferId);
if (bufferId == 0) {
MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer.");
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
}
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
const SizeT sourceElementSize = componentCount * sizeof(Double);
// A client array only ever reaches here through a pointer call, whose stride 0
// the frontend already resolved to the element size, so this is a guard rather
// than a case (VertexAttribute::Stride).
const SizeT sourceStride =
attrib.Stride > 0 ? static_cast<SizeT>(attrib.Stride) : sourceElementSize;
const SizeT elementCount = static_cast<SizeT>(first) + static_cast<SizeT>(count);
Vector<Float> converted;
NarrowDoubleStreamToFloat32(sourceBase, sourceStride, componentCount, elementCount, converted);
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
// GL ignores `normalized` for floating-point array types, so it is not
// forwarded here either.
g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
static_cast<GLsizei>(componentCount * sizeof(Float)),
// The converted stream is tightly packed, so a divisor'd array's
// baseInstance shift is a whole number of CONVERTED elements -
// not BaseInstanceByteShift's source-stride arithmetic.
reinterpret_cast<const void*>(
attrib.Divisor == 0 ? 0u : static_cast<SizeT>(fetchBaseInstance) *
componentCount *
sizeof(Float)));
// glVertexAttribPointer's final argument IS that shift. A client array
// declared through the pointer API keeps element 0 at the buffer's byte 0, so
// the shift rides in the pointer argument the call above passes.
// The float64 narrowing itself is shared with SyncToBackend's conversion
// branch; only the fetch RANGE differs, and the draw's is the accurate one.
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
continue;
}
const auto* clientData = reinterpret_cast<const Uint8*>(attrib.Offset);
const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra);
if (!clientData || elementSize == 0 || attrib.Size <= 0) {
continue;
}
const SizeT stride = attrib.Stride > 0 ? static_cast<SizeT>(attrib.Stride) : elementSize;
const SizeT uploadSize = static_cast<SizeT>(first + count - 1) * stride + elementSize;
auto& bufferId = m_clientAttributeBufferIds[attribIndex];
if (bufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &bufferId);
if (bufferId == 0) {
MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer.");
continue;
}
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(uploadSize), clientData,
GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(uploadSize));
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
g_GLESFuncs.glVertexAttribPointer(
attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast<GLsizei>(stride),
// A client array's element 0 sits at its buffer's byte 0, so the
// divisor'd arrays' baseInstance shift is exactly BaseInstanceByteShift -
// the same arithmetic SyncToBackend applies to a VBO-backed array.
reinterpret_cast<const void*>(BaseInstanceByteShift(attrib, fetchBaseInstance)));
} else {
g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
static_cast<GLsizei>(stride),
// See the pointer call above: the divisor'd
// arrays' baseInstance shift is the pointer.
reinterpret_cast<const void*>(
BaseInstanceByteShift(attrib, fetchBaseInstance)));
}
}
}
Bool BackendVertexArrayObject::SyncFloat64AttributeAsFloat32ByHandle(
Uint attribIndex, const MGPVertexAttribWire& attrib, const MG_Pipe::MGPVertexBuffer& binding,
Uint32 fetchBaseInstance) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (attribIndex >= m_convertedAttributeBufferIds.size() || attrib.Size < 1 || attrib.Size > 4) {
return false;
}
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
// A client-memory 64-bit array is narrowed on the draw path instead, which is
// the only place its fetch range is known.
return false;
}
auto* resource = BufferImpl::FindBufferResourceForHandle(binding.Res);
if (resource == nullptr) return false;
// WHAT IS NOT HERE, AND IT IS A RULED DEVIATION FOR P3a RATHER THAN AN OVERSIGHT
// (M-3; integrator ruling, this commit).
//
// The legacy arm opens with bufferObject->SyncGpuWrites(), one of the eleven Espryt
// SyncPersistentMappedRange / SyncGpuWrites sites D-N keeps where they are for P3a.
// D-N's wording is "no MOVE of those sites off the frontend", and this arm does not
// move it: it CANNOT MAKE IT AT ALL. The call needs a frontend BufferObject and this
// path holds only a handle, because the server has no inverse map back to a frontend
// object - that absence is the design, not a gap in it (ARCHITECTURE.md 4.2). The
// two ways to keep the site here would each break something D-N or D-J protects: a
// handle -> object map is the very thing the split removes, and pulling the bytes
// eagerly on the client at every draw is new behaviour and new cost.
//
// SO THE DEVIATION IS DECLARED, WITH ITS BLAST RADIUS. Under the default mask a
// 64-bit vertex array whose SOURCE buffer was written by a shader and not yet pulled
// back narrows STALE bytes on this arm and fresh bytes on the legacy one. That is
// the whole of it: fp64 vertex arrays, fed by a buffer a shader wrote, read without
// any intervening explicit readback. No other attribute type reads through this
// path, and a persistently mapped source is excluded separately below (the memo
// never trusts one, so those re-read every draw through the coherent map).
//
// P8-C CLOSES IT UNDER A TRANSPORT, on the server rather than by moving the pull to the
// client: a source a shader wrote on this side is read back from this server's own GL
// buffer (SplitHostBytesForCpuRead), and that write also voids the memo below, whose
// serial a GPU write never moves. The MONOLITH arm of this function keeps the
// deviation (its hostBytes is the client's shadow and its mark is the frontend
// object's); that half is dev's (ID-P8-3).
// WHERE THE BYTES ARE, and it is not one fixed place: an ADOPTED store has no client
// shadow left at all (PipeResource::AdoptPersistentMap clears and shrinks it), so
// the coherent map IS the source of truth - which is exactly the case the memo
// exclusion below makes the most frequent, since a persistently mapped source is
// never trusted and this narrowing therefore re-reads on every draw. Reading the
// recorded shadow base for such a resource read freed memory; hostBytes is nulled
// at adoption for the same reason, so the fallback below is a null and a refusal
// rather than a stale read.
#if MOBILEGL_BUILD_DISAGGREGATED
const Bool sourceGpuWritten =
MG_Config::DataArmIsRecord() && resource->serverGpuWritten;
if (sourceGpuWritten) {
BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res, "fp64_vertex_narrowing");
}
#else
constexpr Bool sourceGpuWritten = false;
#endif
const Uint8* const sourceBase =
(resource->persistentMapped && resource->persistentPtr != nullptr)
? static_cast<const Uint8*>(resource->persistentPtr)
: resource->hostBytes;
const SizeT sourceSize = BufferImpl::ResourceWidthForHandle(binding.Res);
if (sourceBase == nullptr || attrib.Offset >= sourceSize) {
return false;
}
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
const SizeT sourceElementSize = componentCount * sizeof(Double);
const SizeT available = sourceSize - static_cast<SizeT>(attrib.Offset);
if (available < sourceElementSize) {
return false;
}
const Bool neverAdvances = attrib.Stride <= 0;
const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast<SizeT>(attrib.Stride);
const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1;
const SizeT firstElement = (fetchBaseInstance != 0 && binding.Divisor != 0 && !neverAdvances)
? static_cast<SizeT>(fetchBaseInstance)
: 0;
if (firstElement >= elementCount) {
return false;
}
auto& stream = m_convertedAttributeStreams[attribIndex];
Uint& convertedBufferId = m_convertedAttributeBufferIds[attribIndex];
const Uint64 sourceSerial = BufferImpl::ResourceSerialForHandle(binding.Res);
// The memo, re-keyed: the pin is the source buffer's {slot, gen} instead of a
// frontend lifetime id, and the freshness input is the applier's server-owned
// Serial instead of the frontend change serial. A PERSISTENTLY MAPPED source is
// still never trusted - it is written through the pointer with no call at all, so
// no serial on either side can prove the converted copy is current.
const Bool memoHit = stream.valid && convertedBufferId != 0 && !resource->persistentMapped &&
!sourceGpuWritten && stream.sourceHandle == binding.Res &&
stream.sourceChangeSerial == sourceSerial &&
stream.sourceOffset == static_cast<SizeT>(attrib.Offset) &&
stream.sourceStride == sourceStride &&
stream.componentCount == componentCount && stream.elementCount == elementCount;
if (!memoHit) {
if (convertedBufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &convertedBufferId);
if (convertedBufferId == 0) {
MGLOG_E_ONCE("Failed to create the float32 scratch buffer for the 64-bit vertex array at "
"attribute %u.",
attribIndex);
return false;
}
}
Vector<Float> converted;
NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, elementCount,
converted);
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
stream.valid = true;
stream.sourceHandle = binding.Res;
stream.sourceChangeSerial = sourceSerial;
stream.sourceOffset = static_cast<SizeT>(attrib.Offset);
stream.sourceStride = sourceStride;
stream.componentCount = componentCount;
stream.elementCount = elementCount;
MGLOG_D("DirectGLES: narrowed the 64-bit vertex array at attribute %u to %zu float32 element(s).",
attribIndex, elementCount);
}
const SizeT convertedElementSize = componentCount * sizeof(Float);
if (neverAdvances) {
// Only the binding-point API can say "stride 0".
if (!HasVertexBindingApi()) {
return false;
}
g_GLESFuncs.glVertexAttribFormat(attribIndex, static_cast<GLint>(attrib.Size), GL_FLOAT, GL_FALSE, 0);
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
g_GLESFuncs.glBindVertexBuffer(attribIndex, convertedBufferId, 0, 0);
return true;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
// `normalized` is deliberately GL_FALSE rather than attrib.Normalized: GL ignores it
// for floating-point array types, and honouring it would scale the fetched values.
g_GLESFuncs.glVertexAttribPointer(attribIndex, static_cast<GLint>(attrib.Size), GL_FLOAT, GL_FALSE,
static_cast<GLsizei>(convertedElementSize),
(const void*)(firstElement * convertedElementSize));
return true;
}
TwinRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject, MG_Pipe::MGPipeKind::VertexElementsCso>
g_backendVertexArrayObjects;
} // namespace VertexArrayImpl
namespace TextureImpl {
BackendTextureObject::BackendTextureObject() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) {
MGLOG_E_ONCE("Failed to generate texture object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated texture object with ID: %u.", m_backendTextureId);
}
}
BackendTextureObject::~BackendTextureObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendTextureId == 0) {
return;
}
// Scrub every driver-state shadow that could false-skip when the name
// or this heap address is recycled - regardless of whether the id can
// still be deleted.
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
for (auto& unitCache : g_boundTexturesCache) {
for (auto& boundTexture : unitCache) {
if (boundTexture == this) {
boundTexture = nullptr;
}
}
}
// TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes.
// Discriminator for the mali-mem-purge hiccup theory: never hand the
// driver a texture free, so the purge daemon has nothing to reclaim.
static const Bool s_expLeakTextureDeletes = [] {
FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb");
if (!f) return false;
std::fclose(f);
return true;
}();
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures &&
!s_expLeakTextureDeletes) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
if (m_bufferImageSplitViewId != 0) {
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
}
}
m_backendTextureId = 0;
m_bufferImageSplitViewId = 0;
}
void BackendTextureObject::Bind(GLenum target, Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (g_activeTextureUnit != unit) {
ActivateTextureUnit(unit);
}
auto targetN = static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(target));
if (this == g_boundTexturesCache[unit][targetN]) return;
g_GLESFuncs.glBindTexture(target, m_backendTextureId);
g_boundTexturesCache[unit][targetN] = this;
}
Uint BackendTextureObject::GetBackendTextureId() const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
return m_backendTextureId;
}
// P5e (tx2), CONTRACT-P5E §5.2 / scout G-S2-5. See the declaration.
void BackendTextureObject::RequireImageBindableStorageByHandle(
MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record) {
if (m_imageBindableStorageRequired) {
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4b (defect h): monolith's record arm takes the body too; its old arm only
// cleared m_isInitialized and replayed the frontend shadow at the next sync.
if (MG_Config::DataArmIsRecord()) {
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
// ES image binding needs immutable storage, but an already
// immutable core-format allocation needs no replacement. Keep
// its GPU contents and native name; normal serial/shape checks
// below still consume pending uploads and real redefinitions.
if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) {
m_imageBindableStorageRequired = true;
m_forceTextureParamsResync = true;
return;
}
// P9 W2 (notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
// SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies,
// generated mips, image stores) and the staged store holds what the client uploaded;
// the client is never asked, so no pull protocol exists. Reading only the store would
// be wrong: a GPU-written level's store run is stale or absent - both host corpus
// re-mints (iris-photon, iris-derivative) are render targets with no staged bytes.
const Bool hadBackendStorage = m_isInitialized;
// The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be
// produced on the host (llvmpipe renders every image format), so the test lane forces
// one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
static const Bool s_forceReadbackFailure =
std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr;
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
MG_Util::ConvertTextureInternalFormatToGLEnum(format), {}, nullptr,
&canonicalFormat, &canonicalType);
for (const auto& uploadTarget :
BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) {
for (Uint32 level = 0; level < record.Desc.Levels; ++level) {
const auto targetCode = static_cast<Uint16>(uploadTarget);
const auto levelCode = static_cast<Uint16>(level);
const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode);
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue;
const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode);
const Bool covered = store.IsCovered(key, targetCode, levelCode);
const MG_Pipe::MGPBox whole{0, 0, 0, static_cast<Uint32>(extent.x()),
static_cast<Uint32>(extent.y()), static_cast<Uint32>(extent.z())};
// A genuinely full pending write owns every byte. A
// union box covering the level is not sufficient when
// its explicit rectangles leave GPU-written gaps.
const auto coversWhole = [&](const auto& box) {
return box.X == 0 && box.Y == 0 && box.Z == 0 &&
box.W == whole.W && box.H == whole.H && box.D == whole.D;
};
const Bool pendingWhole = pending && (pending->Regions.empty()
? coversWhole(pending->UnionBox)
: std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole));
// W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's
// Defined-ness follows the client's respecifies, the driver's follows this
// twin's last sync, and the two differ for a level defined (null data, or null
// data plus a partial upload) after that sync: the driver has no such level, so
// the GPU cannot have written it. A covered level then replays its staged run
// (the client's whole-level shadow, pending boxes included) and an uncovered one
// is undefined content with nothing to preserve. Reading it anyway was
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
const Bool nativeLevel =
m_isInitialized &&
NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget,
static_cast<GLint>(level), extent);
if (nativeLevel && !(covered && pendingWhole)) {
Vector<Uint8> snapshot;
const Bool readBack =
!s_forceReadbackFailure &&
ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format,
static_cast<GLint>(level), extent,
/*sourceUsesImageCarrier=*/false, canonicalFormat,
canonicalType, snapshot);
if (!readBack && !covered) {
// The GPU holds this level, the driver will not hand it back, and no
// CPU byte of it exists anywhere: the one case with no honest answer.
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u",
res.Slot, res.Gen, level);
std::abort();
}
if (!readBack) {
// W2-b: the driver refused the readback (a format it cannot attach
// to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm
// without EXT_texture_norm16, float without EXT_color_buffer_float)
// but the store covers the level. Its staged run is exactly what the
// monolith re-mint replays; texels the GPU wrote since that upload
// are not preserved, which is monolith's answer too.
MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver "
"refused the readback (internal format %u); replaying the level's "
"staged bytes instead (remint-readback-fallback)",
res.Slot, res.Gen, level, record.Desc.InternalFormat);
}
if (readBack && pending) {
// Native contents include prior draws/image
// writes; the pending stage owns ONLY its boxes.
// Merge those boxes before adopting a whole-level
// snapshot, never overwrite untouched GPU texels
// with the rest of a stale client staging run.
const SizeT pixels = static_cast<SizeT>(extent.x()) * extent.y() * extent.z();
const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode);
if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) {
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}");
std::abort();
}
const SizeT bpp = stagedSize / pixels;
const Uint8* staged = store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion");
const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W ||
Uint64(y) + h > whole.H || Uint64(z) + d > whole.D) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}");
std::abort();
}
for (Uint32 slice = 0; slice < d; ++slice) {
for (Uint32 row = 0; row < h; ++row) {
const SizeT offset = ((static_cast<SizeT>(z + slice) * whole.H + y + row) * whole.W + x) * bpp;
Memcpy(snapshot.data() + offset, staged + offset, static_cast<SizeT>(w) * bpp);
}
}
};
if (pending->Regions.empty()) {
const auto& box = pending->UnionBox;
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
} else {
for (const auto& box : pending->Regions)
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
}
}
if (readBack) {
store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size());
}
}
// Undefined contents have no byte run to preserve. A
// first allocation can still be image-bindable without
// inventing an upload or consulting the frontend.
if (!store.IsCovered(key, targetCode, levelCode)) continue;
if (!RearmPipeTextureLevelUpload(res,
MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast<Uint32>(uploadTarget)),
levelCode, whole)) {
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}");
std::abort();
}
}
}
// W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as
// on the frontend one (which counts in RequireImageBindableStorage). The keep path
// above is not a re-mint and returns before this; a first allocation holds nothing.
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
}
}
#else
(void)res;
(void)record;
#endif
// Every defined byte now belongs to the server and every required
// replay is in its pending set. The existing upload path builds and
// fills the immutable/widened carrier from those records.
m_imageBindableStorageRequired = true;
m_isInitialized = false;
// The widened carrier's swizzle override, exactly as the frontend overload's tail
// sets it and for the same reason: the parameter sync is gated on a params version
// this transition does not move.
m_forceTextureParamsResync = true;
}
// Replaces one level's shadow bytes with what the driver texture holds for it, then copies
// back the client writes the driver has not been sent yet - exactly the texels written
// (the level's dirty footprint), not the union box or the rect list, which also span texels
// the client never wrote and which only the driver's copy holds correctly. Returns false,
// leaving the shadow untouched, when the driver's copy cannot stand in for the shadow: the
// client wrote every texel of the level, the level is stored compressed, the driver has no
// image at this level and extent (defined after the last sync), or the readback cannot be
// served (a multisample image, a format the driver will not attach to a framebuffer).
//
// P11 M, `pipePending` (push builds): THE CLIENT WRITES THE DRIVER HAS NOT BEEN SENT MAY
// LIVE IN THE APPLIER'S PENDING SET INSTEAD. On the texture handle arm the client clears its
// own dirty flags when it emits a level (D-D5), so the frontend model below answers "clean"
// for a write that is still only in the shadow; the entry the applier accumulated for this
// (uploadTarget, level) names the same shadow-grid boxes. Both are merged over the
// snapshot. Only a box-only entry whose box IS the level owns every byte of it (dev's rule:
// a union box spanning the level with gaps in it is not a whole-level write); an entry with
// regions never short-circuits, since accumulated regions may overlap and their texels do
// not add up - the readback and the merge below give the same bytes when they do cover it.
// Null on the pull build's arm and the legacy arm.
static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture,
const MG_State::GLState::ITextureObject& stateTextureObject,
MG_State::GLState::TextureObjectMipmap& mipmapObject,
TextureUploadTarget uploadTarget, Uint level
,
const MG_Pipe::MGPipeResourceRecord::PendingUpload* pipePending
) {
const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level);
const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)};
const SizeT texelCount =
static_cast<SizeT>(extent.x()) * static_cast<SizeT>(extent.y()) * static_cast<SizeT>(extent.z());
Vector<MG_State::GLState::MipmapDirtyRegion> pending;
mipmapObject.GetStorageDirtyFootprint(uploadTarget, level, pending);
// Disjoint boxes, so their texels add up; a union box spanning the level is not enough.
SizeT pendingTexels = 0;
for (const auto& box : pending) {
pendingTexels += box.TexelCount();
}
if (!pending.empty() && pendingTexels >= texelCount) return false;
// Every box the entry names, checked against the level before anything is read: a box
// outside it is a corrupt record, and the answer is the pre-readback one (keep the
// shadow) rather than a copy out of bounds.
const auto pipeBoxInLevel = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
return x >= 0 && y >= 0 && z >= 0 && Uint64(x) + w <= Uint64(extent.x()) &&
Uint64(y) + h <= Uint64(extent.y()) && Uint64(z) + d <= Uint64(extent.z());
};
if (pipePending != nullptr) {
const auto& box = pipePending->UnionBox;
if (!pipeBoxInLevel(box.X, box.Y, box.Z, box.W, box.H, box.D)) {
MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's box lies outside the level; the "
"image-bindable re-mint replays the shadow as it stands",
stateTextureObject.GetExternalIndex(), level);
return false;
}
for (const auto& region : pipePending->Regions) {
if (!pipeBoxInLevel(region.X, region.Y, region.Z, region.W, region.H, region.D)) {
MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's region lies outside the "
"level; the image-bindable re-mint replays the shadow as it stands",
stateTextureObject.GetExternalIndex(), level);
return false;
}
}
if (pipePending->Regions.empty() && box.X == 0 && box.Y == 0 && box.Z == 0 &&
box.W == static_cast<Uint32>(extent.x()) && box.H == static_cast<Uint32>(extent.y()) &&
box.D == static_cast<Uint32>(extent.z())) {
return false;
}
}
if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false;
if (!g_GLESFuncs.glGetTexLevelParameteriv) return false;
const TextureTarget target = stateTextureObject.GetTarget();
const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(target);
const GLenum faceTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
const IntVec3 backendExtent = GetBackendUploadSize(target, texelSize);
GLint driverWidth = 0, driverHeight = 0, driverDepth = 0;
backendTexture.Bind(nativeTarget);
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_WIDTH, &driverWidth);
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_HEIGHT,
&driverHeight);
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_DEPTH, &driverDepth);
if (driverWidth != backendExtent.x() || driverHeight != backendExtent.y() ||
std::max(driverDepth, 1) != std::max(backendExtent.z(), 1)) {
return false;
}
const TextureInternalFormat format = stateTextureObject.GetFormat();
GLenum shadowFormat = GL_NONE, shadowType = GL_NONE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(format),
{}, nullptr, &shadowFormat, &shadowType);
Vector<Uint8> snapshot;
if (!ReadTextureLevelTight(backendTextureId, target, uploadTarget, format, static_cast<GLint>(level),
texelSize, /*sourceUsesImageCarrier=*/false, shadowFormat, shadowType,
snapshot)) {
return false;
}
const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level);
auto* shadow = static_cast<Uint8*>(mipmapObject.MapMipmapData(uploadTarget, level));
if (shadow == nullptr || snapshot.size() != shadowBytes || texelCount == 0 || shadowBytes % texelCount) {
return false;
}
if (!pending.empty()) {
// The boxes index the SHADOW's texel grid (a 1D array's layers are its rows), which
// is also the snapshot's byte order.
const SizeT bpp = shadowBytes / texelCount;
const SizeT rowBytes = static_cast<SizeT>(extent.x()) * bpp;
const SizeT sliceBytes = static_cast<SizeT>(extent.y()) * rowBytes;
for (const auto& box : pending) {
if (box.Empty()) continue;
const SizeT spanBytes = static_cast<SizeT>(box.hi.x() - box.lo.x()) * bpp;
for (Int z = box.lo.z(); z < box.hi.z(); ++z) {
for (Int y = box.lo.y(); y < box.hi.y(); ++y) {
const SizeT offset = static_cast<SizeT>(z) * sliceBytes + static_cast<SizeT>(y) * rowBytes +
static_cast<SizeT>(box.lo.x()) * bpp;
Memcpy(snapshot.data() + offset, shadow + offset, spanBytes);
}
}
}
}
if (pipePending != nullptr) {
// The same merge for the applier's entry: its regions when it carries a list, its
// union box when it does not (RegionCount 0 = "the box is the whole story", D-D3).
const SizeT bpp = shadowBytes / texelCount;
const SizeT rowBytes = static_cast<SizeT>(extent.x()) * bpp;
const SizeT sliceBytes = static_cast<SizeT>(extent.y()) * rowBytes;
const auto mergePipeBox = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
for (Uint32 slice = 0; slice < d; ++slice) {
for (Uint32 row = 0; row < h; ++row) {
const SizeT offset = static_cast<SizeT>(z + static_cast<Int32>(slice)) * sliceBytes +
static_cast<SizeT>(y + static_cast<Int32>(row)) * rowBytes +
static_cast<SizeT>(x) * bpp;
Memcpy(snapshot.data() + offset, shadow + offset, static_cast<SizeT>(w) * bpp);
}
}
};
if (pipePending->Regions.empty()) {
const auto& box = pipePending->UnionBox;
mergePipeBox(box.X, box.Y, box.Z, box.W, box.H, box.D);
} else {
for (const auto& region : pipePending->Regions) {
mergePipeBox(region.X, region.Y, region.Z, region.W, region.H, region.D);
}
}
}
Memcpy(shadow, snapshot.data(), shadowBytes);
return true;
}
void BackendTextureObject::RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (m_imageBindableStorageRequired) {
return;
}
// Read before the two flags below schedule the re-mint: whether the driver texture
// already holds this object's levels, and so may hold texels the shadow never saw.
// Also what `trp=` counts on the push builds: a transition that re-mints storage that
// ALREADY EXISTED on the backend. A texture reaching here uninitialised is allocated
// image-bindable up front and replays nothing.
const Bool hadBackendStorage = m_isInitialized && m_backendTextureId != 0 &&
m_contextGeneration == g_backendContextGeneration;
m_imageBindableStorageRequired = true;
// ES only needs the storage to be immutable, so an immutable allocation that needs no
// channel widening is already the one a re-mint would produce: keep it, and with it
// every texel the GPU wrote. Pending uploads and real redefinitions still go through
// the normal shape and dirty checks of the next sync.
if (hadBackendStorage && m_backendStorageImmutable && !stateTextureObject->IsTextureView() &&
!IsMultisampleTextureTarget(stateTextureObject->GetTarget()) &&
!GetImageBindableStorageWidening(stateTextureObject->GetFormat())) {
m_forceTextureParamsResync = true;
return;
}
m_isInitialized = false;
// A view owns no levels. Its sync hands the requirement to the storage texture
// (SyncTextureViewToBackend), whose own transition below re-mints and replays them; a
// re-dirty issued here, through the view, would reach that texture first and claim
// every level for the shadow before the driver's copy could be read back.
if (stateTextureObject->IsTextureView()) {
m_forceTextureParamsResync = true;
return;
}
// Every level this object has ALREADY uploaded has to be replayed, because the
// regeneration this transition schedules re-mints the storage in the image carrier and
// only uploads levels the shadow still calls dirty - which, for a texture that was
// synced before its first glBindImageTexture, is none of them. The new storage would
// come out ALLOCATED AND EMPTY, and every texel the application defined before that
// bind would be gone: the shader reads zeroes and the shadow still holds the data, so
// glGetTexImage (which falls back to the shadow) keeps answering correctly and only
// the image loads are wrong. Reached whenever anything syncs the texture first - a
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
// long: the scenario that binds the image immediately after uploading never sees it.
//
// And the shadow is only the CLIENT's copy. Whatever the GPU wrote into a level - a
// render or clear into it, a copy, a glGenerateMipmap - lives in the driver texture
// alone, so replaying the shadow as it stands put the pre-render bytes (or the zeroes
// of a null-data definition) into the new storage. The driver's levels are therefore
// read back into the shadow first, with the client writes still pending merged over
// them, while the old storage still exists; a level the driver cannot hand back keeps
// the shadow, which is all this used to replay. (This was the known monolith defect of
// W2-REMINT.md §6, the half of ID-P9-8 left to dev; dev's 568090f0 fixed it and P11 M
// merged it.)
//
// THIS IS THE MONOLITH ARM ONLY. Under a transport the re-mint is the server's own
// (RequireImageBindableStorageByHandle: the GPU level read back, the staged store for
// what the client uploaded), and nothing selects this function there - the image
// units sync from the record, the metadata respecify calls the by-handle overload, and
// a frontend object reaching the loop below would meet the layer-1 guard
// (Fatal{RoleViolation, "texture-legacy-arm"}) first. P9 W2 retired the
// `texture-remint-pull` marker this comment used to describe: no pull protocol exists
// because no byte of a re-mint has to come from the client (notes/p9/W2-REMINT.md).
// The two arms now answer the same way: both read the GPU's level back first.
//
// THE COUNT IS TAKEN WHERE A LEVEL IS ACTUALLY REPLAYED, not on entry (review N-4): a
// glBindImageTexture on a texture whose storage is not yet defined reaches here, finds
// no defined level and replays nothing.
// P4a (review M-1): ON THE HANDLE ARM THE RE-DIRTY HAS TO REACH THE APPLIER'S SET,
// not only the frontend's model. The client cleared its own flags when it emitted
// those levels, so MarkStorageDirty below re-arms a model the handle arm does not
// read: the pending set would stay empty, MGB_LEVEL_NEEDS_UPLOAD would answer false
// for every level, and the widened image carrier this transition schedules would be
// allocated EMPTY - the very bug the paragraph above says "survived so long",
// re-introduced one arm over. The dispatch that triggered the bind would read zeroes.
//
// Both models are written, not one: the frontend's because the legacy arm reads it
// and because the client's own NoteLevelDirty hook rides on it, the applier's
// because that is what this arm consumes.
Bool countedRemint = false;
const MG_Pipe::MGPipeHandle rearmRes =
TextureResourceSubsystemEnabled()
? [&]() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
return g_backendTextureObjects.HandleOf(stateTextureObject.get());
}()
: MG_Pipe::kMGPipeNullHandle;
const Uint32 rearmResourceTarget =
MG_Pipe::MGPipeResourceTargetForTextureTarget(stateTextureObject->GetTarget());
if (TextureResourceSubsystemEnabled() && MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
MGLOG_E_ONCE("MGPipe: texture %u needs image-bindable storage but has no handle on the "
"handle arm, so the levels it owes cannot be re-armed in the applier's "
"pending set - they would be allocated empty",
stateTextureObject->GetExternalIndex());
}
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue;
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
// This level's entry in the applier's pending set, read BEFORE the re-arm
// below turns it into a whole-level box: the client writes it still owes
// (see AdoptDriverLevelIntoShadow). Resolved per level - the re-arm may grow
// the record's set and move what an earlier pointer named.
const MG_Pipe::MGPipeResourceRecord* rearmRecord =
MG_Pipe::MGPipeHandleIsNull(rearmRes) ? nullptr : PipeTextureRecordForHandle(rearmRes);
const MG_Pipe::MGPipeResourceRecord::PendingUpload* rearmPending =
rearmRecord != nullptr ? FindPipeTextureUpload(*rearmRecord, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))
: nullptr;
if (hadBackendStorage &&
!AdoptDriverLevelIntoShadow(m_backendTextureId, *this, *stateTextureObject,
*mipmapObject, uploadTarget, level
,
rearmPending
)) {
MGLOG_D("Texture %u level %u: re-minting from the shadow, the driver's copy is not "
"readable here",
stateTextureObject->GetExternalIndex(), level);
}
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
// THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): once per
// transition that replays a level of storage the backend already held,
// not once per level.
if (!countedRemint) {
countedRemint = true;
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
}
}
if (!MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
const MG_Pipe::MGPBox wholeLevel{0,
0,
0,
static_cast<Uint32>(levelTexelSize.x()),
static_cast<Uint32>(levelTexelSize.y()),
static_cast<Uint32>(std::max(levelTexelSize.z(), 1))};
RearmPipeTextureLevelUpload(
rearmRes,
MG_Pipe::MGPipePackSubDataTarget(rearmResourceTarget,
static_cast<Uint32>(uploadTarget)),
static_cast<Uint16>(level), wholeLevel);
}
}
}
}
// The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not
// bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels),
// and a widened texture's sampled view has to answer the channels the logical format
// does not have with 0 and 1 - which is a swizzle. The parameter sync is gated on the
// frontend's params version, which this transition does not move, so without the
// override an application that never touched GL_TEXTURE_SWIZZLE_* would keep the
// driver at its defaults and sample the carrier's surplus channels raw.
m_forceTextureParamsResync = true;
}
void BackendTextureObject::RecreateBackendTexture() {
if (m_backendTextureId != 0) {
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
// Application FBO twins that attached the dying id memoize on FRONTEND
// attachment versions, which this backend-side re-mint does not move;
// without this bump their driver FBOs would keep the deleted name
// attached forever (see g_attachmentBackendIdGeneration).
++FramebufferImpl::g_attachmentBackendIdGeneration;
if (m_contextGeneration == g_backendContextGeneration) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
}
for (auto& unitCache : g_boundTexturesCache) {
for (auto& boundTexture : unitCache) {
if (boundTexture == this) {
boundTexture = nullptr;
}
}
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// A shared image's EGLImage lives exactly as long as the name it was bound to.
m_sharedImage = nullptr;
#endif
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) {
MGLOG_E_ONCE("Failed to regenerate texture object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Regenerated texture object with ID: %u.", m_backendTextureId);
}
m_isInitialized = false;
m_backendStorageImmutable = false;
m_prevTextureInfo = {};
// The new ES texture starts at the ES defaults, so every parameter this object had
// already pushed onto the old one is gone. The change-detection caches below would
// otherwise still claim those values are in force and SyncTextureParamsToBackend
// would skip the whole pass on the unchanged params version, leaving the driver
// texture at defaults for the rest of its life. Latent for swizzle, LOD range and
// border colour long before GL_DEPTH_STENCIL_TEXTURE_MODE joined them; the mode
// makes it visible because falling back to the default silently samples the wrong
// aspect rather than merely mis-filtering.
m_cacheLodRange = {0, 1000};
m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, TextureSwizzleParam::Blue,
TextureSwizzleParam::Alpha};
m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
m_forceTextureParamsResync = true;
// The filter/wrap/LOD cache belongs to the name that just went away, and its gate is
// the frontend SAMPLER's version, which a backend re-mint does not move - so without
// this the new driver texture keeps the ES defaults for life. See m_forceSamplerResync.
m_cacheSamplerParameters = SamplerParameters{};
m_forceSamplerResync = true;
}
// Sets the backend GL unpack state to MobileGL's upload default for the scope,
// then restores it. The previous state is read from a shadow instead of via
// glGetIntegerv - that query forces a driver pipeline sync and, because texture
// uploads run it per dirty texture per frame, it dominated the DirectGLES draw
// path. The backend unpack state is set ONLY by MobileGL's own save/restore
// helpers (this class and, historically, the R32F copy path), all of
// which restore to the resting default, so the shadow stays accurate; a one-time
// forced sync pins the backend to that known default up front. Apply() is
// compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op.
class ScopedDefaultUnpackState {
public:
ScopedDefaultUnpackState() {
EnsureShadowSynced();
m_prevAlignment = s_alignment;
m_prevRowLength = s_rowLength;
m_prevSkipRows = s_skipRows;
m_prevSkipPixels = s_skipPixels;
m_prevImageHeight = s_imageHeight;
m_prevSkipImages = s_skipImages;
// Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits
// width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1.
// Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride,
// shifting every row of a non-multiple-of-4 upload by one pixel.
Apply(1, 0, 0, 0, 0, 0);
}
~ScopedDefaultUnpackState() {
Apply(m_prevAlignment, m_prevRowLength, m_prevSkipRows, m_prevSkipPixels, m_prevImageHeight,
m_prevSkipImages);
}
private:
static void EnsureShadowSynced() {
#if MOBILEGL_BUILD_DISAGGREGATED
const auto epoch = CurrentContextEpoch();
if (s_epoch != epoch) {
s_synced = false;
s_epoch = epoch;
}
#endif
if (s_synced) {
return;
}
s_synced = true;
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
s_alignment = 4;
s_rowLength = 0;
s_skipRows = 0;
s_skipPixels = 0;
s_imageHeight = 0;
s_skipImages = 0;
}
static void Apply(GLint alignment, GLint rowLength, GLint skipRows, GLint skipPixels, GLint imageHeight,
GLint skipImages) {
if (alignment != s_alignment) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); s_alignment = alignment; }
if (rowLength != s_rowLength) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowLength); s_rowLength = rowLength; }
if (skipRows != s_skipRows) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, skipRows); s_skipRows = skipRows; }
if (skipPixels != s_skipPixels) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, skipPixels); s_skipPixels = skipPixels; }
if (imageHeight != s_imageHeight) { g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); s_imageHeight = imageHeight; }
if (skipImages != s_skipImages) { g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, skipImages); s_skipImages = skipImages; }
}
GLint m_prevAlignment = 4;
GLint m_prevRowLength = 0;
GLint m_prevSkipRows = 0;
GLint m_prevSkipPixels = 0;
GLint m_prevImageHeight = 0;
GLint m_prevSkipImages = 0;
// Shadow of the backend GL unpack state (GL defaults). See class comment.
static inline Bool s_synced = false;
#if MOBILEGL_BUILD_DISAGGREGATED
static inline ContextEpoch s_epoch{};
#endif
static inline GLint s_alignment = 4;
static inline GLint s_rowLength = 0;
static inline GLint s_skipRows = 0;
static inline GLint s_skipPixels = 0;
static inline GLint s_imageHeight = 0;
static inline GLint s_skipImages = 0;
};
#if MOBILEGL_BUILD_DISAGGREGATED
void ExerciseDefaultUnpackScopeForTesting() {
const ScopedDefaultUnpackState scope;
}
#endif
// --- Unpack-ring staging (see BufferImpl::UnpackRingAvailable) ---------------
// One rectangular (or whole-level) region of a level shadow, repacked TIGHTLY
// into the persistently-mapped unpack PBO. The upload that follows passes the
// returned ring offset with UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT left at the
// surrounding ScopedDefaultUnpackState's 0, so the driver reads exactly the
// bytes staged here - strictly fewer than the ROW_LENGTH-strided client-pointer
// upload it replaces, which made the driver walk the whole level's stride.
//
// `blocks` describes one or more source regions to stage back-to-back into a
// SINGLE ring allocation; the ring offset of block i lands in blocks[i].offset.
// Deliberately without default member initializers: call sites declare a
// kMaxDirtyRects-sized array of these per dirty level and fill only the entries
// they use, so the type stays trivially default-constructible and the
// declaration costs nothing on the levels that never reach the ring.
struct UnpackStagingBlock {
const Uint8* src; // top-left texel of the region in the level's upload bytes
SizeT rowBytes; // bytes per region row (region width * bpp)
SizeT rows; // region height
SizeT slices; // region depth (1 for 2D)
SizeT srcRowStride; // level row pitch
SizeT srcSliceStride; // level slice pitch
SizeT offset; // out: byte offset into the ring store
};
// False (nothing staged, ring untouched) whenever the caller must keep the
// client-pointer path: ring unavailable, a degenerate block, or a total that
// overflows / exceeds the ring's cap.
static Bool StageBlocksIntoUnpackRing(UnpackStagingBlock* blocks, SizeT blockCount) {
if (blocks == nullptr || blockCount == 0) return false;
if (!BufferImpl::UnpackRingAvailable()) return false;
const SizeT maxBytes = BufferImpl::UnpackRingMaxBytes();
SizeT total = 0;
for (SizeT i = 0; i < blockCount; ++i) {
const UnpackStagingBlock& b = blocks[i];
if (b.src == nullptr || b.rowBytes == 0 || b.rows == 0 || b.slices == 0) return false;
// Every factor is bounded by the level's own byte size, but the
// multiplications are on SizeT: check each against the cap instead of
// trusting a product that could have wrapped.
if (b.rowBytes > maxBytes || b.rows > maxBytes / b.rowBytes) return false;
const SizeT sliceBytes = b.rowBytes * b.rows;
if (b.slices > maxBytes / sliceBytes) return false;
const SizeT blockBytes = sliceBytes * b.slices;
if (blockBytes > maxBytes - total) return false;
total += blockBytes;
}
SizeT base = 0;
if (!BufferImpl::UnpackRingAllocate(total, base)) return false;
// AFTER the allocation: growing the ring replaces the store and its map.
auto* ringBytes = static_cast<Uint8*>(BufferImpl::UnpackRingMappedPtr());
if (ringBytes == nullptr) return false;
SizeT cursor = base;
for (SizeT i = 0; i < blockCount; ++i) {
UnpackStagingBlock& b = blocks[i];
b.offset = cursor;
Uint8* dst = ringBytes + cursor;
// Each block's size is a whole number of texels, so consecutive block
// offsets stay multiples of the pixel type's size just as `base` is.
if (b.rowBytes == b.srcRowStride && b.rowBytes * b.rows == b.srcSliceStride) {
Memcpy(dst, b.src, b.rowBytes * b.rows * b.slices); // region IS the level
} else {
for (SizeT z = 0; z < b.slices; ++z) {
const Uint8* srcSlice = b.src + z * b.srcSliceStride;
if (b.rowBytes == b.srcRowStride) {
Memcpy(dst, srcSlice, b.rowBytes * b.rows); // full-width rows
dst += b.rowBytes * b.rows;
continue;
}
for (SizeT y = 0; y < b.rows; ++y) {
Memcpy(dst, srcSlice + y * b.srcRowStride, b.rowBytes);
dst += b.rowBytes;
}
}
}
cursor += b.rowBytes * b.rows * b.slices;
}
return true;
}
// The `pixels` argument of a PBO-sourced glTexSubImage: a byte offset dressed
// up as a pointer.
static const void* UnpackRingPixelOffset(SizeT offset) {
return reinterpret_cast<const void*>(static_cast<std::uintptr_t>(offset));
}
static Uint GetNormFallbackComponentCount(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16:
case TextureInternalFormat::R16Snorm:
return 1;
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16:
case TextureInternalFormat::RG16Snorm:
return 2;
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB16Snorm:
return 3;
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16:
case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow)
case TextureInternalFormat::RGBA16Snorm:
return 4;
default:
return 0;
}
}
static Bool IsSnormFallbackFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA16Snorm:
return true;
default:
return false;
}
}
static Bool IsNorm8FallbackFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
return true;
default:
return false;
}
}
// Components per texel the frontend format's client data carries. Only the three-channel
// formats that can be widened to a four-channel render target need an answer (see
// PrepareChannelWidenedUpload); everything else keeps its own layout and reports 0.
Uint GetWidenableClientComponentCount(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::SRGB8:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
return 3;
default:
return 0;
}
}
// True when the widened format's client data is integer rather than normalized. The two
// classes share every narrow component type - GL_RGB8I and GL_RGB8_SNORM are both uploaded
// as GL_BYTE - but their "1.0" differs: an integer channel's one is the integer 1, a
// normalized channel's is the saturated field. The type alone cannot tell them apart, so
// the source format has to.
Bool IsIntegerWidenableFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
return true;
default:
return false;
}
}
// The bit pattern of 1.0 in an upload component type: what a format without alpha reads
// back as, and therefore what the synthetic fourth channel of a widened render target has
// to hold. Integer components carry the integer one, not a saturated field - and since
// GL_BYTE/GL_SHORT/GL_UNSIGNED_BYTE/GL_UNSIGNED_SHORT serve both classes, `integerData`
// is what decides, not the type.
static Bool GetUploadComponentOneBits(GLenum uploadType, Bool integerData, Uint8* outOneBits,
SizeT* outComponentSize) {
switch (uploadType) {
case GL_BYTE: {
const Int8 one = integerData ? Int8(1) : Int8(0x7F);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_BYTE: {
const Uint8 one = integerData ? Uint8(1) : Uint8(0xFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_SHORT: {
const Int16 one = integerData ? Int16(1) : Int16(0x7FFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_SHORT: {
const Uint16 one = integerData ? Uint16(1) : Uint16(0xFFFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_HALF_FLOAT: {
const Uint16 one = 0x3C00; // half 1.0
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_FLOAT: {
const Float one = 1.0f;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_INT: {
const Int32 one = 1;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_INT: {
const Uint32 one = 1;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
default:
return false;
}
}
// A three-channel format widened to four to keep a colour attachment renderable (see
// NormalizePixelFormat) is described to the driver as a four-component transfer, so the
// three-component client data has to be repacked with an alpha of 1.0 - otherwise the
// driver walks three texels' worth of data per four-texel row and the image shears.
// `componentCount` is the SOURCE component count and `byteSize` the source's size, so this
// runs after any type conversion (which keeps the component count) has already happened.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum uploadType,
Vector<Uint8>& widenedData, Bool integerData,
Uint32 alphaOneCodeOverride) {
Uint8 oneBits[8] = {};
SizeT componentSize = 0;
// One and two source components as well as three: the image-format widening carries
// GL_R8UI in a GL_RGBA8UI and GL_RG32F in a GL_RGBA32F (see
// TextureImpl::GetImageBindableStorageWidening), and their surplus channels take the
// same values the three-channel case gives its single added one - zeroes, and the
// format's implied 1 in alpha.
if (componentCount == 0 || componentCount > 3 || data == nullptr || byteSize == 0 ||
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
return data;
}
// ...except where the carrier holds CODES of a normalized value (GL_R16 in a
// GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that
// type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated
// 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose
// saturated code is 0x7FFF. The caller passes the channel's own maximum instead.
// Written through a value of the component's own width rather than as the low
// `componentSize` bytes of the Uint32, so the encoding does not turn on the host's
// byte order.
if (alphaOneCodeOverride != 0u) {
if (componentSize == sizeof(Uint16)) {
const auto one = static_cast<Uint16>(alphaOneCodeOverride);
Memcpy(oneBits, &one, sizeof(one));
} else if (componentSize == sizeof(Uint32)) {
Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride));
} else if (componentSize == sizeof(Uint8)) {
const auto one = static_cast<Uint8>(alphaOneCodeOverride);
Memcpy(oneBits, &one, sizeof(one));
}
}
const SizeT srcTexelBytes = componentSize * componentCount;
// Sized from the level, never from the source: the driver reads a full
// width*height*depth*4 components for the transfer it was handed, so a source that
// somehow holds fewer texels must still leave a full destination behind (its tail
// reads as transparent black with the format's implied opaque alpha) rather than a
// short buffer the driver would run off the end of.
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / srcTexelBytes);
widenedData.assign(texelCount * componentSize * 4, 0);
const auto* src = static_cast<const Uint8*>(data);
Uint8* dst = widenedData.data();
for (SizeT i = 0; i < texelCount; ++i, dst += componentSize * 4) {
if (i < copyTexelCount) {
Memcpy(dst, src, srcTexelBytes);
src += srcTexelBytes;
}
// ALWAYS at component 3, never at `componentCount`: GL's implied 1 is the ALPHA
// channel, and a one- or two-component source leaves the channels between it and
// alpha at the zero `assign` already wrote. For three components the two
// expressions coincide, which is what this used to be written as.
Memcpy(dst + componentSize * 3, oneBits, componentSize);
}
return widenedData.data();
}
static const void* PrepareNormFloatFallbackUpload(TextureInternalFormat format,
const IntVec3& texelSize,
const void* data,
SizeT byteSize,
GLenum uploadType,
Vector<Float>& convertedData) {
const Uint componentCount = GetNormFallbackComponentCount(format);
if (componentCount == 0 || uploadType != GL_FLOAT || data == nullptr || byteSize == 0) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 0));
const SizeT componentTotal = texelCount * static_cast<SizeT>(componentCount);
const SizeT sourceComponentSize = IsNorm8FallbackFormat(format) ? sizeof(Int8) : sizeof(Uint16);
const SizeT sourceComponentTotal = byteSize / sourceComponentSize;
if (componentTotal == 0 || sourceComponentTotal == 0) {
return nullptr;
}
convertedData.assign(componentTotal, 0.0f);
const SizeT copyComponentTotal = std::min(componentTotal, sourceComponentTotal);
if (IsNorm8FallbackFormat(format)) {
const Int8* src = static_cast<const Int8*>(data);
constexpr Float invMaxSnorm8 = 1.0f / 127.0f;
for (SizeT i = 0; i < copyComponentTotal; ++i) {
convertedData[i] = std::max(static_cast<Float>(src[i]) * invMaxSnorm8, -1.0f);
}
} else if (IsSnormFallbackFormat(format)) {
const Int16* src = static_cast<const Int16*>(data);
constexpr Float invMaxSnorm16 = 1.0f / 32767.0f;
for (SizeT i = 0; i < copyComponentTotal; ++i) {
convertedData[i] = std::max(static_cast<Float>(src[i]) * invMaxSnorm16, -1.0f);
}
} else {
const Uint16* src = static_cast<const Uint16*>(data);
constexpr Float invMaxUnorm16 = 1.0f / 65535.0f;
for (SizeT i = 0; i < copyComponentTotal; ++i) {
convertedData[i] = static_cast<Float>(src[i]) * invMaxUnorm16;
}
}
return convertedData.data();
}
// The two shadow -> upload conversions a fallback storage format can need, in order:
// the component type first (SNORM/UNORM shadows into the float the fallback stores), then
// the component count (three-channel client data into a four-channel widened render
// target). They compose: GL_RGB8_SNORM on a driver with no renderable three-channel
// format becomes GL_RGBA16F, so its Int8x3 shadow is converted to Float x3 and then
// repacked as Float x4 with alpha 1.0.
//
// Both scratch buffers belong to the caller so they outlive the returned pointer; the
// return value is `data` itself whenever neither conversion applies, which is what the
// sub-rect upload fast path tests for.
static const void* PrepareFallbackUpload(TextureInternalFormat format, TextureTarget target,
const IntVec3& texelSize, const void* data, SizeT byteSize,
GLenum uploadType, Vector<Float>& convertedData,
Vector<Uint8>& widenedData) {
const void* uploadData =
PrepareNormFloatFallbackUpload(format, texelSize, data, byteSize, uploadType, convertedData);
// The component-count switch first: it rules out every format that cannot be widened
// (which is nearly all of them, including GL_RGBA8) without touching the capability
// cache, so an ordinary atlas upload does not pay for a per-level cache lookup.
const Uint componentCount = GetWidenableClientComponentCount(format);
if (componentCount == 0 || !TextureImpl::BackendTextureFormatAddsAlpha(format, target)) {
return uploadData;
}
// The type conversion above rewrites the level into `convertedData` at four bytes per
// component while keeping the component count, so the widening's source size is that
// buffer's, not the shadow's.
const SizeT uploadByteSize = (!convertedData.empty() && uploadData == convertedData.data())
? convertedData.size() * sizeof(Float)
: byteSize;
return PrepareChannelWidenedUpload(componentCount, texelSize, uploadData, uploadByteSize, uploadType,
widenedData, IsIntegerWidenableFormat(format));
}
// One channel of a packed r11f_g11f_b10f word as a float. The two 11-bit channels are
// e5m6 and the 10-bit one e5m5 - IEEE-shaped but UNSIGNED, so there is no sign bit to
// read and the exponent bias is the 15 a 5-bit exponent always carries.
static Float DecodePackedUnsignedFloat(Uint32 bits, Uint mantissaBits) {
const Uint32 mantissaScale = 1u << mantissaBits;
const Uint32 mantissa = bits & (mantissaScale - 1u);
const Uint32 exponent = bits >> mantissaBits;
if (exponent == 0u) {
// Subnormal, and zero with it: no implied leading 1, and the exponent is the
// smallest NORMAL one rather than the encoded 0.
return std::ldexp(static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale), -14);
}
if (exponent == 31u) {
return mantissa == 0u ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return std::ldexp(1.0f + static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale),
static_cast<Int>(exponent) - 15);
}
// The r11f_g11f_b10f shadow decoded into the GL_RGBA / GL_FLOAT level its GL_RGBA16F
// carrier is uploaded as. Alpha is the 1 GL defines for a format that has no alpha
// channel, which is the same constant the shader-side mask writes, so a texel this
// function produced and a texel an imageStore produced are indistinguishable.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is:
// the driver reads a full width*height*depth*4 floats for the transfer it was handed.
static const void* PreparePackedFloatWidenedUpload(const IntVec3& texelSize, const void* data,
SizeT byteSize, Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Float), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Float*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Float rgb[3] = {0.0f, 0.0f, 0.0f};
if (i < copyTexelCount) {
Uint32 packed = 0;
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
rgb[0] = DecodePackedUnsignedFloat(packed & 0x7FFu, 6u);
rgb[1] = DecodePackedUnsignedFloat((packed >> 11u) & 0x7FFu, 6u);
rgb[2] = DecodePackedUnsignedFloat((packed >> 22u) & 0x3FFu, 5u);
}
dst[0] = rgb[0];
dst[1] = rgb[1];
dst[2] = rgb[2];
dst[3] = 1.0f;
}
return widenedData.data();
}
// The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its
// GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST
// component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19,
// blue 20-29 and alpha 30-31.
//
// The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an
// rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side
// unpack divides out. Neither is scaled here - the carrier holds the format's own bits.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the
// driver reads a full width*height*depth*4 shorts for the transfer it was handed.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data,
SizeT byteSize, Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Uint16), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Uint32 packed = 0;
if (i < copyTexelCount) {
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
}
dst[0] = static_cast<Uint16>(packed & 0x3FFu);
dst[1] = static_cast<Uint16>((packed >> 10u) & 0x3FFu);
dst[2] = static_cast<Uint16>((packed >> 20u) & 0x3FFu);
dst[3] = static_cast<Uint16>((packed >> 30u) & 0x3u);
}
return widenedData.data();
}
// The transfer half of the image-format widening: an image-bindable texture whose ES
// storage was widened to a core carrier is described to the driver as a four-component
// transfer, so its narrower client data has to be repacked the same way the three-channel
// colour-renderable widening repacks its own.
//
// Three shapes, because the carriers come in three kinds. Most of them keep the frontend
// format's component TYPE and only add channels, so padding the shadow out to four
// components is the whole conversion. The two PACKED formats do not: their shadow is one
// 32-bit word per texel, so the word has to be split - into four floats for
// r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such
// a word as components of the carrier's type - what the repack below would do - takes
// twelve or sixteen bytes from a four-byte texel and shears the level, which is what the
// allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel
// the upload got wrong).
//
// Composes with PrepareFallbackUpload rather than replacing it, and the composition is a
// no-op by construction: none of the widened formats is one GetWidenableClientComponentCount
// reports a count for, and the SNORM shadow-to-float conversion only fires for a GL_FLOAT
// transfer type, which the widened triple never picks for the two SNORM8 formats. So the
// shadow reaches this untouched and one conversion is all that runs.
static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening,
const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData) {
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) {
return data;
}
switch (widening.SourceEncoding) {
case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f:
return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev:
return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::Components:
break;
}
if (widening.SourceChannels == 4) {
return data;
}
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
widenedData, widening.IntegerData,
widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u);
}
// Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose
// with the widened carrier's. Deliberately unconditional on anything but the widening
// itself: whatever renderability fallback the triple carried, an image the driver refuses
// to bind is useless, so the image constraint wins.
static void ApplyImageBindableStorageWidening(const TextureImpl::ImageBindableStorageWidening& widening,
GLenum* inOutInternalFormat, GLenum* inOutFormat,
GLenum* inOutType) {
if (!widening) {
return;
}
if (inOutInternalFormat) *inOutInternalFormat = widening.InternalFormat;
if (inOutFormat) *inOutFormat = widening.Format;
if (inOutType) *inOutType = widening.Type;
}
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
// drifting mid-range texels one 5-bit step down and failing the KHR-GL3x
// pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into
// the packed 16-bit client type with round-to-nearest instead - that recovers the original
// 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the
// driver stores them verbatim with no requantization left to its discretion. 4-bit formats
// (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding.
// Always retargets *inOutType for these formats (even for null data) so every upload of a
// level uses the same client type.
static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum* inOutType,
Vector<Uint8>& packedData) {
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
return data;
}
// With the storage widened to 8-bit-per-channel (the packed16 field-order quirk)
// there is no driver requantization left for the repack to pre-empt - the shadow's
// UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg
// retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all.
if (TextureImpl::UsesWidenedPacked16NormStorage(format)) {
return data;
}
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
// the already-retargeted packed type and must still be converted.
if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) {
return data;
}
*inOutType = packedType;
if (data == nullptr || byteSize == 0) {
return data;
}
const SizeT srcPixelBytes = hasAlpha ? 4 : 3;
const SizeT texelCount = std::min(static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1)),
byteSize / srcPixelBytes);
packedData.resize(texelCount * sizeof(Uint16));
const Uint8* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(packedData.data());
for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) {
const Uint32 r = (static_cast<Uint32>(src[0]) * 31u + 127u) / 255u;
const Uint32 b = (static_cast<Uint32>(src[2]) * 31u + 127u) / 255u;
if (hasAlpha) {
const Uint32 g = (static_cast<Uint32>(src[1]) * 31u + 127u) / 255u;
dst[i] = static_cast<Uint16>((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u));
} else {
const Uint32 g = (static_cast<Uint32>(src[1]) * 63u + 127u) / 255u;
dst[i] = static_cast<Uint16>((r << 11) | (g << 5) | b);
}
}
return packedData.data();
}
// "Some level of this texture holds an image", which is all the sync gate below actually
// needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also
// answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with
// undefined lower levels - but such a texture still has to be uploaded, or the level that
// IS defined never reaches the driver at all.
static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) {
const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject);
if (mipmapObject == nullptr) return false;
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) {
return true;
}
}
}
return false;
}
// P5e (tx2): the GL name a log line wants, from whichever side of the arm can answer it.
// Desc.GlNameForDiag is the declared diagnostics carrier (MGPipeTypes.h) and it is the
// ONLY thing the by-handle arm may say about a texture's frontend name - a GL name is
// never an identity, never a memo key and never part of a hash (section 4.2.1). The
// twenty-odd GetExternalIndex() log reads this family carried (scout G-S2-4) all become
// this call.
static Uint32 TextureDiagName(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject,
const MG_Pipe::MGPipeResourceRecord* record) {
if (stateTextureObject) return static_cast<Uint32>(stateTextureObject->GetExternalIndex());
return record != nullptr ? record->Desc.GlNameForDiag : 0u;
}
// True while this twin has NO noted handle, i.e. it is the monolith-glue half and the frontend
// object below is the only thing that can answer. A macro and not a member function because the
// pull build has no such member at all and this has to fold to a constant there (D-P: the pull
// build's preprocessed text may not gain a call).
#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (MG_Pipe::MGPipeHandleIsNull((twin).PushedSyncHandle()))
// P5e (fix1, ID-81 / CONTRACT-P5E §5.8): THE NULL FRONTEND OBJECT IS A CONTRACT AND THIS
// IS WHERE IT IS CHECKED, ONCE, FOR ALL THREE SYNC BODIES.
//
// tx2's three sync bodies accept a null `stateTextureObject` because the RECORD answers
// every read they make - but only where the record arm is actually SELECTED. Two reads
// inside SyncMipmapsToBackend are gated on `Transport != Monolith` (the texture-VIEW test
// and RequireImageBindableStorage's re-dirty transition, §5.2), and every read in all
// three is gated on TextureResourceSubsystemEnabled() through `pushedStorage`. So a null
// object is servable exactly when BOTH hold, and the by-handle entries beside them
// (SyncTextureToBackendByHandle / SyncMipmapsToBackendByHandle) pass null on the strength
// of a NOTED HANDLE, which says nothing about either.
//
// The device found the gap: with MOBILEGL_TRANSPORT=monolith the push build still drives
// its framebuffer attachments from the record (P4a, D-C2), fb's SyncAttachmentSurface
// called the by-handle storage sync from that arm, and `stateTextureObject->IsTextureView()`
// dereferenced the null SharedPtr in Lightmap.<init> -> clearColorTexture -> glClear.
// The arm selection is repaired at that call site; this refusal is what makes the shape
// UNREPEATABLE rather than merely repaired - a null frontend object reaching a body that
// can still read one aborts BY NAME instead of taking a SIGSEGV three frames deep.
//
// P5e (fix1 follow-up, ID-110): THE RECORD ARM'S SELECTOR, STATED ONCE AND SPELLED WHEREVER IT
// IS RELIED ON. fix1's root cause was a seam that read "a handle was noted" as "the record arm
// is selected"; those are different statements and this is the second one. Everything a null
// frontend texture object is servable by lives behind BOTH conjuncts: the transport (the
// texture-VIEW test and RequireImageBindableStorage's re-dirty transition are
// `Transport != Monolith` arms, CONTRACT-P5E §5.2) and the subsystem bit (every `pushedStorage`
// read). Sites that were previously correct only because every writer of a handle-note happens
// to be transport-gated now say so themselves, so a future by-handle noter cannot quietly
// re-open ID-107's hole somewhere else.
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGB_TEXTURE_RECORD_ARM_SELECTED() \
(MG_Config::DataArmIsRecord() && TextureResourceSubsystemEnabled())
#else
#define MGB_TEXTURE_RECORD_ARM_SELECTED() (false)
#endif
// Returns true when the caller must decline (no handle noted: the caller's bug it always
// was, named by the caller's own log line), false when the record arm can serve the call,
// and never returns at all when a handle was noted on an arm that cannot serve it.
static Bool RefuseNullFrontendTextureOffTheHandleArm(const char* entry,
MG_Pipe::MGPipeHandle notedHandle) {
if (MG_Pipe::MGPipeHandleIsNull(notedHandle)) return true;
if (MGB_TEXTURE_RECORD_ARM_SELECTED()) return false;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"texture-handle-arm\"} - %s was handed a NULL "
"frontend texture for handle {%u, %u} on an arm that cannot answer without "
"one. The by-handle entries pass null on the strength of a noted handle, and "
"the record arm they rely on is selected by Transport != Monolith AND the "
"texture-resource subsystem bit (CONTRACT-P5E §5.8, ID-81); neither holds "
"here, so the body below would read a frontend object that does not exist",
entry, notedHandle.Slot, notedHandle.Gen);
std::abort();
}
// The guard the three sync bodies spell. In the PULL build it folds to the constant the
// pre-fix1 text folded to, so that build's preprocessed text gains no call (D-P).
#define MGB_TEXTURE_NULL_FRONTEND_REFUSED(twin, entry) \
(RefuseNullFrontendTextureOffTheHandleArm((entry), (twin).PushedSyncHandle()))
// P5e (tx2), CONTRACT-P5E §5.2: THE TEXTURE'S TARGET, from the descriptor whenever a record was
// resolved. The two live `GetTarget()` reads left INSIDE the handle arm (scout G-S2-2, the
// parameter sync and the built-in sampler sync) read Desc.Target here, through the same
// StagedTextureTargetForPipeTarget mapping the storage sync already routes through - so all
// three bodies answer the question from one carrier rather than two authorities.
#define MGB_TEXPARAM_TARGET(rec, obj) \
((rec) != nullptr ? BufferImpl::StagedTextureTargetForPipeTarget((rec)->Desc.Target) : (obj)->GetTarget())
// The ~20 GetExternalIndex() reads this family makes in LOG LINES (scout G-S2-4). The pull
// expansion is the pre-P5e expression with one pair of parentheses around the receiver.
#define MGB_TEXTURE_DIAG_NAME(rec, obj) (TextureDiagName((obj), (rec)))
// The storage KIND, from the descriptor whenever one was resolved - the backstop arm of the
// storage switch used to ask the frontend object for a value it had just read from the record.
#define MGB_TEXTURE_STORAGE_KIND(rec, obj) \
((rec) != nullptr ? static_cast<TextureStorageType>((rec)->Desc.StorageKind) : (obj)->GetStorageType())
// The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought.
// Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability
// is the extension AND the pointer, because eglGetProcAddress hands back live-looking
// stubs (see AcquireGLESFunctions).
static MG_External::GLES::glTextureViewEXT_PTR ResolveTextureViewEntryPoint() {
if (g_GLESFuncs.glTextureViewEXT != nullptr) {
return g_GLESFuncs.glTextureViewEXT;
}
return reinterpret_cast<MG_External::GLES::glTextureViewEXT_PTR>(g_GLESFuncs.glTextureViewOES);
}
// Stamps the same per-draw clean-gate keys a completed storage sync stamps, so a view
// that needs no work costs the same nothing per draw that any other synced texture does.
void BackendTextureObject::StampViewSyncKeys(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (MG_Pipe::gPipeInputs.IsLive()) {
m_syncedShapeContextId = MG_Pipe::gPipeInputs.GetTextureContextId();
m_syncedShapeGeneration = MG_Pipe::gPipeInputs.GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
}
m_syncedContentVersion = stateTextureObject->GetContentVersion();
}
void BackendTextureObject::SyncTextureViewToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
const auto& storageObject = stateTextureObject->GetViewStorageOwner();
if (!storageObject) {
MGLOG_E_ONCE("Texture %u claims to be a view but names no storage owner.",
stateTextureObject->GetExternalIndex());
return;
}
if (!g_GLESCapabilities.SupportsTextureView) {
// Unreachable through the API: the frontend refuses glTextureView with
// GL_INVALID_OPERATION when the backend does not advertise GL_ARB_texture_view,
// and DirectGLES only advertises it when this capability is set.
MGLOG_E_ONCE("Texture view %u reached the backend on a driver without "
"EXT/OES_texture_view.",
stateTextureObject->GetExternalIndex());
return;
}
// Deliberately a by-VALUE copy of the SharedPtr: SyncTextureObjectToBackend hands back
// a reference INTO the open-addressed registry map, and the params/sampler syncs below
// (plus any nested growth) can rehash it out from under a reference.
const SharedPtr<BackendTextureObject> storageBackendObject =
SyncTextureObjectToBackend(storageObject, m_imageBindableStorageRequired);
if (!storageBackendObject) {
MGLOG_E_ONCE("Failed to sync the storage texture of view %u.",
stateTextureObject->GetExternalIndex());
return;
}
const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId();
if (storageBackendTextureId == 0) {
MGLOG_D("Storage texture of view %u has no ES name yet.",
stateTextureObject->GetExternalIndex());
return;
}
if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) {
StampViewSyncKeys(stateTextureObject);
return;
}
// Either the first sync, or the storage was re-minted underneath us. A name that has
// already been through glTextureView cannot be viewed again, so start from a fresh
// one (this also scrubs the binding caches and bumps the FBO attachment generation).
RecreateBackendTexture();
GLenum glInternalFormat = 0;
GLenum glFormat = 0;
GLenum glType = 0;
TextureImpl::GenerateTextureFormatInfo(stateTextureObject->GetFormat(), &glInternalFormat, &glFormat,
&glType, stateTextureObject->GetTarget());
const GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
DebugImpl::ErrorLopper::Clear();
ResolveTextureViewEntryPoint()(m_backendTextureId, target, storageBackendTextureId, glInternalFormat,
stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewNumLayers());
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_E_ONCE("glTextureView(view=%u target=%s origtexture=%u internalformat=%s levels=[%u,%u) "
"layers=[%u,%u)) failed: %s",
m_backendTextureId, MG_Util::ConvertGLEnumToString(target).c_str(),
storageBackendTextureId, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers(),
MG_Util::ConvertGLEnumToString(error).c_str());
return;
}
m_viewSourceBackendTextureId = storageBackendTextureId;
m_isInitialized = true;
// A view's storage is immutable by construction (its origtexture had to be), which is
// what keeps the respecify paths away from this name.
m_backendStorageImmutable = true;
const auto baseSize = stateTextureObject->GetBaseSize();
m_prevTextureInfo = {stateTextureObject->GetFormat(),
static_cast<SizeT>(baseSize.x()),
static_cast<SizeT>(baseSize.y()),
static_cast<SizeT>(baseSize.z()),
static_cast<SizeT>(stateTextureObject->GetViewNumLevels()),
0,
stateTextureObject->GetSamples(),
stateTextureObject->HasFixedSampleLocations()};
MGLOG_D("Texture view %u (ES %u) now views storage texture %u (ES %u), levels [%u,%u) layers [%u,%u)",
stateTextureObject->GetExternalIndex(), m_backendTextureId, storageObject->GetExternalIndex(),
storageBackendTextureId, stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers());
StampViewSyncKeys(stateTextureObject);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2): see the declaration for the split between what the record answers and what
// the wire still cannot say about a view.
void BackendTextureObject::SyncTextureViewToBackendByRecord(
MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!g_GLESCapabilities.SupportsTextureView) {
// Unreachable through the API - the frontend refuses glTextureView without
// GL_ARB_texture_view - and kept for the reason the frontend arm keeps its twin.
MGLOG_E_ONCE("Texture view %u reached the backend on a driver without "
"EXT/OES_texture_view.",
record.Desc.GlNameForDiag);
return;
}
// The storage owner by handle, through the same by-handle entry every other draw-path
// texture takes: no frontend object, no allocator probe.
const SharedPtr<BackendTextureObject> storageBackendObject =
SyncTextureToBackendByHandle(record.Desc.ViewOf, m_imageBindableStorageRequired);
if (!storageBackendObject) {
MGLOG_E_ONCE("Failed to sync the storage texture {%u, %u} of view %u on the handle arm.",
record.Desc.ViewOf.Slot, record.Desc.ViewOf.Gen, record.Desc.GlNameForDiag);
return;
}
const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId();
if (storageBackendTextureId == 0) {
MGLOG_D("Storage texture of view %u has no ES name yet.", record.Desc.GlNameForDiag);
return;
}
// P3b/P4b D3: THE SERIAL THIS TWIN STAMPS IS THE STORAGE'S, not this view's, because
// the gate that reads it back (IsDrawSyncCleanByRecord) now asks the same record. It
// is read AFTER the storage sync above - which has just consumed the owner's pending
// uploads - and taken by VALUE, because the applier's vector may move under any later
// call and a Uint64 cannot dangle. A storage record that has gone answers 0, which
// the gate reads as "never clean": the direction that re-syncs.
const MG_Pipe::MGPipeResourceRecord* storageRecord = PipeTextureStorageRecordForRecord(record);
const Uint64 storageSerial = storageRecord != nullptr ? storageRecord->Serial : 0;
if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) {
// The steady case, and the only one this arm has to be fast at: one record read,
// one integer compare and the serial stamp that keeps the aggregate clean gate
// (IsDrawSyncCleanByRecord) answering true for the next draw.
m_syncedResourceSerial = storageSerial;
return;
}
// The view window is a sampler-view CSO, created even when this
// texture is only image-bound or attached to an FBO. It is not part
// of the resource descriptor, and needs no frontend fallback.
(void)stateTextureObject;
const auto& applier = MG_Pipe::MGPipeApplier();
const auto handle = record.ViewCso;
if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= applier.SamplerViewCsos.size()) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-cso\"} {%u,%u}", res.Slot, res.Gen);
std::abort();
}
const auto& cso = applier.SamplerViewCsos[handle.Slot];
const auto& view = cso.View;
if (!cso.Live || cso.Gen != handle.Gen || view.Texture != res || !view.NumLevels || !view.NumLayers) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-window\"} {%u,%u}", res.Slot, res.Gen);
std::abort();
}
const auto viewTarget = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(viewTarget);
GLenum internalFormat = 0, transferFormat = 0, transferType = 0;
GenerateTextureFormatInfo(static_cast<TextureInternalFormat>(view.InternalFormat),
&internalFormat, &transferFormat, &transferType, viewTarget);
// A name already bound or used as a view cannot be redefined by
// glTextureView. This also invalidates native binding/FBO caches.
RecreateBackendTexture();
DebugImpl::ErrorLopper::Clear();
ResolveTextureViewEntryPoint()(m_backendTextureId, nativeTarget, storageBackendTextureId,
internalFormat, view.MinLevel, view.NumLevels, view.MinLayer, view.NumLayers);
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"texture-view-native\"} {%u,%u} error=0x%x",
res.Slot, res.Gen, error);
std::abort();
}
m_viewSourceBackendTextureId = storageBackendTextureId;
m_isInitialized = true;
m_backendStorageImmutable = true;
m_prevTextureInfo = {static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
record.Desc.Width, record.Desc.Height, record.Desc.Depth, view.NumLevels, 0,
record.Desc.Samples, record.Desc.FixedSampleLocations != 0};
// The storage's, for the steady arm's reason above (P3b/P4b D3).
m_syncedResourceSerial = storageSerial;
}
#endif
// P4a (D-D5), the dirty-ownership inversion, applied at the two places the upload loops
// below actually ask: "does this (uploadTarget, level) owe an upload" and "it has been
// uploaded". ARCHITECTURE.md:253 puts the rect model and the emission cursor on the
// CLIENT, which clears its own flags AT EMISSION - so on the handle arm the frontend's
// IsStorageDirty is already false for every level the applier accepted, and asking it
// would upload nothing at all. The applier's pending set is what these two read instead;
// it is server-side state, it survives this function's bail arms (an incomplete texture
// returns early, a multisample target refreshes and skips), and an entry is consumed
// ONLY where the level actually uploaded.
//
// THE UPLOAD-TARGET ENCODING is static_cast<Uint16>(TextureUploadTarget), which is what
// package B must emit into MGPSubData::Target's cube-face half; it is recorded as a
// deviation because the contract left it unstated.
//
// MACROS AND NOT LAMBDAS, and that is a G1 requirement rather than a style choice:
// SyncMipmapsToBackend already holds nine ErrorLopper lambdas whose mangled names are
// ...::$_N BY POSITION, so inserting two more renumbered every one of them - measured as
// eight symbols added, eight removed and four std::function thunks resized in the PULL
// build, whose symbol set P4a may not move by one byte (D-P). A macro expands to the
// pre-P4a expression exactly when MOBILEGL_PIPE_PUSH is off, so the pull build's
// preprocessed text, and therefore its object code, is unchanged. Both are #undef'd
// immediately after the function.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): THE READS THE FOUR UPLOAD ARMS MAKE, re-sourced. With an active transport the
// apply thread may not name the client's TextureObjectMipmap at all (rule E), so on the
// handle arm:
//
// * the level TEXELS come from the server's staged-texture store, adopted at apply time
// (MGB_LEVEL_TEXELS - Fatal{StageSnapshotTooNarrow} when no record covered the level,
// which is the data-correctness refusal of the texture half);
// * the per-level EXTENT and DEFINED-NESS come from the same store (fed by the sub-data
// adoption and the respecify hook; {0,0,0} for a level nothing defined, which is exactly
// GetMipmapTexelSize's answer for one);
// * the level BYTE SIZE is the adopted run's length;
// * the texture TARGET and the UPLOAD-TARGET list come from the descriptor.
//
// Every macro keeps the P4a discipline: the non-disaggregated expansion is the original
// frontend read, character for character modulo one pair of parentheses, so the pull and
// push builds compile exactly what they compiled before tx, and every disaggregated arm
// falls back to the frontend read when there is no active transport (the legacy arm, and
// monolith). MGB_STAGED_TEXTURE_LIVE is the runtime discriminator; pushedStorage/pushedRes
// are the function's own locals. All are #undef'd with the rest after the function.
#define MGB_STAGED_TEXTURE_LIVE \
(pushedStorage != nullptr && MG_Remote::Server::ServerStagedTexture().Holds())
#define MGB_TEXTURE_TARGET(obj) \
(MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedTextureTargetForPipeTarget(pushedStorage->Desc.Target) \
: (obj)->GetTarget())
#define MGB_UPLOAD_TARGETS(obj) \
(MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedUploadTargetsForPipeTarget(pushedStorage->Desc.Target) \
: (obj)->GetUploadTargets())
#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Remote::Server::ServerStagedTexture().LevelExtentOrUndefined( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl)) \
: (obj)->GetMipmapTexelSize(tgt, lvl))
#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Remote::Server::ServerStagedTexture().LevelByteSize( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl)) \
: (obj)->GetMipmapByteSize(tgt, lvl))
#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Remote::Server::ServerStagedTexture().RequireLevelBytes( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl), site) \
: (obj)->MapMipmapData(tgt, lvl))
#else
#define MGB_TEXTURE_TARGET(obj) ((obj)->GetTarget())
#define MGB_UPLOAD_TARGETS(obj) ((obj)->GetUploadTargets())
#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) ((obj)->GetMipmapTexelSize(tgt, lvl))
#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) ((obj)->GetMipmapByteSize(tgt, lvl))
#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) ((obj)->MapMipmapData(tgt, lvl))
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): the disaggregated pair adds ONE term and ONE clear to the P4a shapes - the
// staged-texture store's GPU-dirty mark (T5: a level the GPU generated dirties the SERVER's
// shadow, and the pending set cannot see it). The mark is never set on Espryt today - its
// GenerateMipmap fills the levels on the driver - so the term is inert here and is the
// contract-shaped answer (§2.2's last row) rather than a hot-path cost: one m_any acquire
// load when the store is empty.
#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \
(pushedStorage != nullptr \
? (FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)) != nullptr || \
MG_Remote::Server::ServerStagedTexture().IsLevelGpuDirty( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl))) \
: (obj)->IsStorageDirty(tgt, lvl))
// Re-resolves the record itself, so it is safe after any amount of driver work - and it
// invalidates any PendingUpload* taken earlier for THIS texture, which is why every such pointer
// is used and dropped inside one level's iteration.
#define MGB_LEVEL_UPLOAD_DONE(obj, tgt, lvl) \
do { \
if (pushedStorage != nullptr) { \
ConsumePipeTextureUpload(pushedRes, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)); \
if (MGB_STAGED_TEXTURE_LIVE) { \
MG_Remote::Server::ServerStagedTexture().MarkLevelGpuDirty( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl), false); \
} \
} else { \
(obj)->MarkStorageDirty(tgt, lvl, false); \
} \
} while (0)
#else
#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \
(pushedStorage != nullptr \
? FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)) != nullptr \
: (obj)->IsStorageDirty(tgt, lvl))
// Re-resolves the record itself, so it is safe after any amount of driver work - and it
// invalidates any PendingUpload* taken earlier for THIS texture, which is why every such pointer
// is used and dropped inside one level's iteration.
#define MGB_LEVEL_UPLOAD_DONE(obj, tgt, lvl) \
do { \
if (pushedStorage != nullptr) { \
ConsumePipeTextureUpload(pushedRes, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)); \
} else { \
(obj)->MarkStorageDirty(tgt, lvl, false); \
} \
} while (0)
#endif
// THE STORAGE SHAPE, and on the handle arm it is the DESCRIPTOR's (C.3's d2: "the
// m_prevTextureInfo probe re-keyed onto the resource record's Serial AND Desc"; review
// M-2). v1 moved the parameters and the uploads and left the allocation reading the
// frontend object, which put TWO AUTHORITIES on one value inside one twin -
// SyncTextureParamsToBackend already answers MGB_TEXPARAM_FORMAT from
// Desc.InternalFormat while this function allocated from GetFormat(). In monolith they
// agree, so no lane can expose the split; under split the descriptor is all there is.
//
// WHAT THE DESCRIPTOR ANSWERS: internal format, base extent, level count, sample count,
// fixed sample locations, immutability, storage kind and the buffer-texture window. What
// it does NOT answer, and what still comes from the frontend shadow, is the PER-LEVEL
// extent and the level BYTES - MGPResourceDesc carries one base extent, the per-level
// sizes are derived from it, and the texels themselves ride on sub-data. A level the
// descriptor's Levels claims and the shadow has not defined reads back {0,0,0} and the
// per-level loops skip it, which is the same arm they take for a sparse chain today.
//
// AND Desc.HasDefinedContent IS DELIBERATELY NOT READ HERE (review N-8, and the
// enumeration above exists to be exhaustive, so the omission is stated rather than
// left to be noticed). c0e made it storage-defining and the BUFFER family reads it
// three times, because a buffer's content is ONE fact about the whole resource. A
// texture's is not: content is per (uploadTarget, level) and its authority is the
// pending set (D-D5), which is per level and survives a bail. A whole-resource
// "content is undefined" would say nothing the per-level loops do not already answer
// by finding no level to upload, and acting on it would mean throwing away levels the
// set still owes. The field stays a respecify CLASSIFIER (it moves the descriptor, so
// wire refuses to call such a respecify metadata-only) and nothing more on this arm.
//
// MACROS, NOT LOCALS, FOR THE SAME REASON AS THE PAIR ABOVE (DV-9, and it was measured):
// routing these through locals resized SyncTextureParamsToBackend by +9 bytes in the
// PULL build and P4a's admitted-resize set is EMPTY. Each takes the object it would
// otherwise have been spelled on, so the pull expansion is the pre-P4a expression with
// one pair of parentheses around the receiver. All are #undef'd with the pair above.
#define MGB_STORAGE_FORMAT(obj) \
(pushedStorage != nullptr ? static_cast<TextureInternalFormat>(pushedStorage->Desc.InternalFormat) \
: (obj)->GetFormat())
#define MGB_STORAGE_BASE_SIZE(obj) \
(pushedStorage != nullptr ? IntVec3{static_cast<Int>(pushedStorage->Desc.Width), \
static_cast<Int>(pushedStorage->Desc.Height), \
static_cast<Int>(pushedStorage->Desc.Depth)} \
: (obj)->GetBaseSize())
#define MGB_STORAGE_LEVELS(obj) \
(pushedStorage != nullptr ? static_cast<Uint>(pushedStorage->Desc.Levels) : (obj)->GetMipmapLevelCount())
#define MGB_STORAGE_SAMPLES(obj) \
(pushedStorage != nullptr ? static_cast<Int>(pushedStorage->Desc.Samples) : (obj)->GetSamples())
#define MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(obj) \
(pushedStorage != nullptr ? pushedStorage->Desc.FixedSampleLocations != 0 : (obj)->HasFixedSampleLocations())
#define MGB_STORAGE_IMMUTABLE(obj) \
(pushedStorage != nullptr ? pushedStorage->Desc.Immutable != 0 : (obj)->IsImmutable())
#define MGB_STORAGE_KIND(obj) \
(pushedStorage != nullptr ? static_cast<TextureStorageType>(pushedStorage->Desc.StorageKind) \
: (obj)->GetStorageType())
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). The client defined
// level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor
// still says so, but the texels are the image's, so nothing below may allocate or upload.
Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) {
const Uint64 imageId = record.SharedImageId;
if (imageId == 0) {
// Respecified away from the image. The ES name is an EGLImage sibling, which must not
// be redefined in place: the immutable flag sends the normal path through
// RecreateBackendTexture, and the EGLImage goes with the name it was bound to.
m_sharedImageId = 0;
m_isInitialized = false;
m_backendStorageImmutable = true;
return false;
}
// Steady state: bound to this image already (the serial moves with sub-data and with a
// metadata respecify, neither of which changes what the name samples). A failed bind is
// retried only once the record moves. Pending sub-data is the caller's
// (ReplaySharedImageUploads), after the image's sync state has been acquired.
if (m_sharedImageId == imageId && m_isInitialized &&
(m_sharedImage != nullptr || m_syncedResourceSerial == record.Serial)) {
m_syncedResourceSerial = record.Serial;
return true;
}
// A fresh mutable name: the current one may hold immutable storage (which no EGLImage
// can replace) or another image's sibling. RecreateBackendTexture also moves the FBO
// twins' attachment generation and forces the parameter resync the new name needs.
RecreateBackendTexture();
m_sharedImage = nullptr;
m_sharedImageId = imageId;
m_isInitialized = true;
// Whatever comes next redefines storage through a re-mint (see the imageId == 0 arm).
m_backendStorageImmutable = true;
m_syncedResourceSerial = record.Serial;
const TextureTarget target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
if (ConvertTextureTargetToBackendGLEnum(target) != GL_TEXTURE_2D) {
MGLOG_E_ONCE("Shared image texture %u: target %s cannot take an EGLImage; left empty",
record.Desc.GlNameForDiag, MG_Util::ConvertTextureTargetToString(target).c_str());
return true;
}
String why;
SharedImageImpl::EglImageRef image = SharedImageImpl::CreateEglImage(imageId, why);
if (image == nullptr) {
MGLOG_E_ONCE("Shared image texture %u: image %llu cannot be bound (%s); left empty",
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId), why.c_str());
return true;
}
Bind(GL_TEXTURE_2D);
if (SharedImageImpl::IsYuv(image)) {
// A YUV image is CONVERTED into the name's own RGBA8 level at each frame's first use
// (SharedImageImpl::ConvertYuvForSampling), never made its storage.
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
}
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, static_cast<GLsizei>(record.Desc.Width),
static_cast<GLsizei>(record.Desc.Height));
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
MGLOG_E_ONCE("Shared image texture %u: no %ux%u RGBA8 storage for YUV image %llu; left empty",
record.Desc.GlNameForDiag, record.Desc.Width, record.Desc.Height,
static_cast<unsigned long long>(imageId));
return true;
}
} else if (!SharedImageImpl::TargetBoundTexture2D(image)) {
MGLOG_E_ONCE("Shared image texture %u: glEGLImageTargetTexture2DOES refused image %llu; left empty",
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId));
return true;
}
m_sharedImage = std::move(image);
m_sharedImageUploadsRgb = false;
// The shape the name now has: the descriptor's level 0, and that one level only.
m_prevTextureInfo = {static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
record.Desc.Width,
record.Desc.Height,
record.Desc.Depth,
1,
0,
record.Desc.Samples,
record.Desc.FixedSampleLocations != 0};
return true;
}
// SUB-DATA ON A SHARED IMAGE IS WRITTEN INTO IT. An X server drawing with glamor uploads
// its clients' pixels (MIT-SHM / PutImage) into window pixmaps whose level 0 is a shared
// image, with glTexSubImage2D: the texels are in the staged store (the client defined the
// level without data, so only the uploaded boxes are covered), and they go into the
// EGLImage-backed name box by box. Every pending entry leaves the set either way, or the
// per-draw clean gate would never close. Called after AcquireForSampling, so the upload is
// ordered after the image's pending reads and writes like any other use.
void BackendTextureObject::ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record) {
if (record.PendingUploads.empty()) return;
Vector<std::pair<Uint16, Uint16>> keys;
Vector<std::pair<Uint16, MG_Pipe::MGPBox>> boxes;
keys.reserve(record.PendingUploads.size());
for (const auto& pending : record.PendingUploads) {
const Uint16 uploadHalf = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget);
keys.emplace_back(uploadHalf, pending.Level);
if (pending.Level != 0) continue; // an image has one level
if (pending.Regions.empty()) {
boxes.emplace_back(uploadHalf, pending.UnionBox);
} else {
for (const auto& region : pending.Regions)
boxes.emplace_back(uploadHalf, MG_Pipe::MGPBox{region.X, region.Y, region.Z, region.W, region.H,
region.D});
}
}
if (m_sharedImage != nullptr && !boxes.empty()) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(record.Desc.Resource);
Bind(GL_TEXTURE_2D);
ScopedDefaultUnpackState unpackState;
BufferImpl::BindPixelUnpackBufferId(0);
for (const auto& [uploadHalf, box] : boxes) {
Int rowTexels = 0;
// A shared image is RGBA8 to every client that names it (the level the client
// defined over it): four bytes a texel.
const Uint8* bytes =
store.CoveredBoxBytes(key, uploadHalf, 0, box.X, box.Y, box.W, box.H, 4, &rowTexels);
if (bytes == nullptr) {
MGLOG_E_ONCE("Shared image texture %u: an upload of %ux%u at (%d,%d) has no staged texels; "
"not written",
record.Desc.GlNameForDiag, box.W, box.H, box.X, box.Y);
continue;
}
const Uint8* origin =
bytes + (static_cast<SizeT>(box.Y) * static_cast<SizeT>(rowTexels) + static_cast<SizeT>(box.X)) * 4;
if (!m_sharedImageUploadsRgb) {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
}
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowTexels);
g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast<GLsizei>(box.W),
static_cast<GLsizei>(box.H), GL_RGBA, GL_UNSIGNED_BYTE, origin);
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
// An image of an X format (no alpha) is an RGB8 texture to the driver, which
// takes only RGB texels: this image's uploads are converted from now on.
if (g_GLESFuncs.glGetError() != GL_INVALID_OPERATION) continue;
m_sharedImageUploadsRgb = true;
}
Vector<Uint8> rgb(static_cast<SizeT>(box.W) * box.H * 3);
for (Uint32 row = 0; row < box.H; ++row) {
const Uint8* src = origin + static_cast<SizeT>(row) * static_cast<SizeT>(rowTexels) * 4;
Uint8* dst = rgb.data() + static_cast<SizeT>(row) * box.W * 3;
for (Uint32 x = 0; x < box.W; ++x) {
dst[x * 3 + 0] = src[x * 4 + 0];
dst[x * 3 + 1] = src[x * 4 + 1];
dst[x * 3 + 2] = src[x * 4 + 2];
}
}
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast<GLsizei>(box.W),
static_cast<GLsizei>(box.H), GL_RGB, GL_UNSIGNED_BYTE, rgb.data());
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
}
MGLOG_I_ONCE("Shared image texture %u: sub-data written into its image (%s texels)",
record.Desc.GlNameForDiag, m_sharedImageUploadsRgb ? "RGB" : "RGBA");
}
for (const auto& [uploadHalf, level] : keys) ConsumePipeTextureUpload(record.Desc.Resource, uploadHalf, level);
}
#endif
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
// see the twin note in SyncBuiltinSamplerToBackend. P5e (fix1): "a handle was noted"
// is not on its own the statement that the record arm will be SELECTED - the view
// test below and RequireImageBindableStorage's transition are transport-gated - so
// the check is the one shared refusal and a noted handle on an arm that cannot serve
// it aborts by name rather than falling into the frontend reads.
if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncMipmapsToBackend")) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
// P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads.
// P5e (tx2) hoists the resolution to the TOP of the function, because the view test
// below needs it before anything else does; nothing else about it moves.
//
// The record pointer stays valid for the whole function: only a create/respecify for
// a NEW slot grows MGPipeApplierState::TextureResources, and nothing this function
// calls emits one. Pointers INTO record->PendingUploads do not - the consume is a
// swap-and-pop - so every one of them is taken, used and dropped inside one level's
// iteration, and never held across a consume.
const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr;
MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle;
if (TextureResourceSubsystemEnabled()) {
pushedStorage = ResolveOwnRecord(stateTextureObject);
pushedRes = !MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)
? m_pushedSyncHandle
: (pushedStorage != nullptr ? pushedStorage->Desc.Resource
: MG_Pipe::kMGPipeNullHandle);
if (pushedStorage == nullptr) {
MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its "
"storage and uploads cannot be driven from the pushed descriptor "
"(handle {%u, %u})",
TextureDiagName(stateTextureObject, pushedStorage), pushedRes.Slot,
pushedRes.Gen);
return;
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES: ahead of the view test and of the staged-store and serial gates below,
// none of which knows the texels belong to an image - the staged store holds none of
// them, and the storage path would allocate fresh, empty storage over the image.
if (pushedStorage != nullptr && (pushedStorage->SharedImageId != 0 || m_sharedImageId != 0) &&
SyncSharedImageStorage(*pushedStorage)) {
// EVERY use, not only the attach (the draw-clean gates never pass a bound image):
// a write that landed since the last one is waited for on this context's GPU
// before the command this sync is for, and the session's frame notes the read.
if (m_sharedImage != nullptr) SharedImageImpl::AcquireForSampling(m_sharedImage);
ReplaySharedImageUploads(*pushedStorage);
// A YUV image is converted into the name at the frame's first use.
if (m_sharedImage != nullptr && SharedImageImpl::IsYuv(m_sharedImage))
SharedImageImpl::ConvertYuvForSampling(m_sharedImage, GetBackendTextureId(),
pushedStorage->Desc.Width, pushedStorage->Desc.Height);
return;
}
// P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE
// RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it
// had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage
// owner BY HANDLE, so both the question and its answer are server-owned.
//
// Ruling 1's arm: the push-monolith build keeps the frontend test below token for
// token, because there the two answers are the same answer and the verify comparator
// needs the frontend arm.
if (MG_Config::DataArmIsRecord() && pushedStorage != nullptr &&
!MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) {
SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject);
return;
}
if (MG_Config::DataArmIsRecord() && pushedStorage != nullptr) {
// Not a view: fall through to the storage body with the frontend test skipped.
} else
#endif
// A texture created by glTextureView owns no storage: the levels, the format and
// every texel belong to the texture it views, and this name only has to be made to
// ALIAS them. Everything below - storage allocation, respecification, per-level
// uploads - would be re-doing the storage texture's work on the wrong name.
if (stateTextureObject->IsTextureView()) {
SyncTextureViewToBackend(stateTextureObject);
return;
}
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads.
// Resolved once, here, because three things below want it - the cheap gate, the
// per-level dirty questions and the union box / rect list / strides the staging
// planner takes its shape from.
//
// The record pointer stays valid for the whole function: only a create/respecify
// for a NEW slot grows MGPipeApplierState::TextureResources, and nothing this
// function calls emits one. Pointers INTO record->PendingUploads do not - the
// consume is a swap-and-pop - so every one of them is taken, used and dropped
// inside one level's iteration, and never held across a consume.
if (pushedStorage != nullptr) {
// THE METADATA RESPECIFY, HONOURED (ID-18 M4, review M-2). BindMask and
// ImageBindableHint are STICKY facts the client discovers AFTER allocation - a
// texture first bound as a shader image - and an IMMUTABLE texture never has a
// later redefinition to carry them, so they arrive on a respecify that restates
// the storage the resource already has. The applier classifies such a record as a
// metadata update: no reallocation ack, no pending-upload clear, Serial moved.
// THE TWIN'S HALF IS TO RE-DERIVE ITS STORAGE FLAGS FROM THE NEW MASK AND
// RECREATE ONLY WHERE THE BACKEND NEEDS THE FLAG AT CREATION - and image
// bindability is exactly such a flag on this backend: the carrier may be channel-
// widened, which is a different allocation, not a different binding.
//
// RequireImageBindableStorage IS that path and it is idempotent, so this is the
// whole of the re-derivation: it sets the sticky flag, clears m_isInitialized so
// the storage below is regenerated, re-arms every defined level in BOTH dirty
// models (see its own comment) so the widened carrier is replayed rather than
// allocated empty, and forces the parameter resync the widening's swizzle needs.
// Arriving on the CREATE instead - the common case once B publishes the hint -
// costs nothing: m_isInitialized is already false and there are no levels to
// replay.
const Bool pushedWantsImageBindableStorage =
pushedStorage->Desc.ImageBindableHint != 0 ||
(pushedStorage->Desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0;
if (pushedWantsImageBindableStorage && !m_imageBindableStorageRequired) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), ruling 1's arm: under a transport the re-dirty half is a NAMED
// refusal at its entry (§5.2) instead of a walk of the client's level shadows;
// the push-monolith build keeps the frontend transition exactly as it is.
if (MG_Config::DataArmIsRecord()) {
RequireImageBindableStorageByHandle(pushedRes, *pushedStorage);
} else
#endif
RequireImageBindableStorage(stateTextureObject);
}
// ONE compare replaces the whole cheap-gate trio AND the content version. The
// applier bumps Serial on every respecify and every sub-data it applies to this
// resource, which is exactly the union those four keys covered - without the
// coarse behaviour the sampling-resolution generation had, where any texture's
// shape churn re-opened every other texture's gate.
//
// The pending set is the second half and it is not redundant: a previous sync
// may have BAILED (an incomplete texture returns early, a multisample target
// refreshes and skips) with the serial already stamped, and the set is what
// survives that. Empty means every level this record ever declared has been
// consumed by a sync that actually uploaded it.
if (m_isInitialized && m_syncedResourceSerial != 0 &&
m_syncedResourceSerial == pushedStorage->Serial &&
pushedStorage->PendingUploads.empty()) {
return;
}
}
// First-level clean gate (see the member comment): three version compares and no
// virtual shape walk. Every mutation the slower probe below would catch bumps one of
// the keys - shape via the context's sampling-resolution generation (coarse: any
// texture's shape churn re-opens every gate, which only costs a fall-through to the
// probe), CPU pixels via the content version, samples/fixed-locations via the params
// version - and backend-side storage resets clear m_isInitialized. Restricted to
// Mipmap storage like the probe fast path: a buffer texture's backing store can move
// without any of these keys noticing.
//
// LEGACY ARM ONLY from P4a on: the memos it reads are the pre-handle ones and the
// record above answers the same question in one compare.
if (pushedStorage == nullptr)
if (m_isInitialized && m_syncedShapeContextId != 0 && MG_Pipe::gPipeInputs.IsLive() &&
m_syncedShapeContextId == MG_Pipe::gPipeInputs.GetTextureContextId() &&
m_syncedShapeGeneration == MG_Pipe::gPipeInputs.GetSamplingResolutionGeneration() &&
m_syncedContentVersion == stateTextureObject->GetContentVersion() &&
m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() &&
stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
return;
}
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject));
auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject);
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
// The texture needs to be regenerated completely with glTexImage* calls if:
// 1. Not initialized
// 2. InternalFormat changed
// 3. Size changed
// 4. Mipmap levels changed
// IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
// undefined incomplete - which is what a top-down build (upload level N, then level 0)
// and ARB_clear_texture's conformance cases both produce. Bailing out on that shape
// left the backend name with no levels whatsoever, so the level that WAS defined could
// never be sampled or read back. Sync whenever some level holds an image; the per-level
// loops below skip the degenerate ones individually.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, CONTRACT-P5C §6 layer 1): on the staged arm the same question is answered
// out of the SERVER's staged-texture store, never out of the frontend object - the
// store's Defined-ness (fed by the respecify hook and the sub-data adoption) IS "some
// level holds an image", and IsComplete()'s sampling half is redundant with it: a
// complete mipmap chain has a defined level 0, and the gate's whole job is to let an
// incomplete-but-partly-defined chain through. The one divergence is deliberate and
// stated: a chain whose EVERY level is 0x0 is complete-by-quirk on the frontend (the
// "0x0 in last level" relaxation, TextureObject.cpp) and undefined here - and syncing
// it would upload nothing either way, because every per-level loop below skips a
// {0,0,0} level individually. A BUFFER texture never has staged levels, so its gate
// is the descriptor's format - exactly what TextureObjectBase::IsComplete() reduces
// to for that storage kind.
if (MGB_STAGED_TEXTURE_LIVE) {
const auto& stagedDesc = pushedStorage->Desc;
Bool anyDefined = false;
if (static_cast<TextureStorageType>(stagedDesc.StorageKind) == TextureStorageType::Buffer) {
// A buffer texture has no staged levels; its gate is the descriptor's
// format, which is what TextureObjectBase::IsComplete() reduces to here.
anyDefined = static_cast<TextureInternalFormat>(stagedDesc.InternalFormat) !=
TextureInternalFormat::Unknown;
} else {
auto& stagedStore = MG_Remote::Server::ServerStagedTexture();
const Uint64 stagedKey = MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes);
for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(stagedDesc.Target)) {
for (Uint32 level = 0; level < stagedDesc.Levels; ++level) {
if (stagedStore.IsLevelDefined(stagedKey, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyDefined = true;
break;
}
}
if (anyDefined) break;
}
}
if (!anyDefined) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
} else
#endif
if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
stateTextureObject->GetExternalIndex());
return;
}
// Fast path: a fully-synced mipmap texture is the common per-draw case.
// SyncNeccessaryTextures re-syncs every bound texture each draw, and the
// scratch Bind below targets the temp unit - which sequential distinct
// textures thrash, forcing a real glBindTexture per texture per draw. When
// nothing needs uploading, skip the bind + upload machinery entirely;
// BindCurrentTextures() re-establishes the real sampling bindings regardless.
// The content-version stamp short-circuits before any shape probing: it
// bumps on every CPU-side pixel mutation, so an unchanged stamp plus an
// unchanged shape means no level can be dirty. Shape stays a separate
// compare because a NULL-data glTexImage changes it without touching the
// content version.
//
// LEGACY ARM ONLY from P4a on: on the handle arm the record's Serial plus an empty
// pending set is the same statement in one compare, made above and BEFORE the
// IsComplete()/shape walk rather than after it - so this probe would only re-derive
// an answer already given, out of the very frontend state the arm exists to stop
// reading.
if (pushedStorage == nullptr)
if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap &&
m_syncedContentVersion != 0 &&
m_syncedContentVersion == stateTextureObject->GetContentVersion()) {
auto* mipmapObject =
static_cast<MG_State::GLState::TextureObjectMipmap*>(stateTextureObject.get());
const auto probeBaseSize = stateTextureObject->GetBaseSize();
StateTextureBasicInfo probe = {stateTextureObject->GetFormat(),
static_cast<SizeT>(probeBaseSize.x()),
static_cast<SizeT>(probeBaseSize.y()),
static_cast<SizeT>(probeBaseSize.z()),
static_cast<SizeT>(mipmapObject->GetMipmapLevelCount()),
0,
stateTextureObject->GetSamples(),
stateTextureObject->HasFixedSampleLocations()};
if (probe == m_prevTextureInfo) {
MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.",
m_backendTextureId);
// The probe just proved "fully synced" from the real state, so the cheap
// gate may be (re)stamped here: the coarse generation only ever goes stale
// from OTHER textures' churn, and this draw re-validated this one.
if (MG_Pipe::gPipeInputs.IsLive()) {
m_syncedShapeContextId = MG_Pipe::gPipeInputs.GetTextureContextId();
m_syncedShapeGeneration = MG_Pipe::gPipeInputs.GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
}
return;
}
}
Bind(target);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
const auto baseSize = MGB_STORAGE_BASE_SIZE(stateTextureObject);
StateTextureBasicInfo currentTextureInfo = {MGB_STORAGE_FORMAT(stateTextureObject),
static_cast<SizeT>(baseSize.x()),
static_cast<SizeT>(baseSize.y()),
static_cast<SizeT>(baseSize.z()),
0,
0,
MGB_STORAGE_SAMPLES(stateTextureObject),
MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject)};
switch (MGB_STORAGE_KIND(stateTextureObject)) {
case TextureStorageType::Mipmap: {
auto* textureMipmapObject =
static_cast<MG_State::GLState::TextureObjectMipmap*>(stateTextureObject.get());
const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject);
currentTextureInfo.mipmapLevels = mipmapCount;
Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo);
if (needsRegeneration && m_backendStorageImmutable) {
RecreateBackendTexture();
Bind(target);
}
// Only a texture that is actually image-bound pays for the widening: it doubles
// or quadruples the storage, and RequireImageBindableStorage is sticky, so a
// texture that is merely sampled keeps its narrow format for life. See
// TextureImpl::GetImageBindableStorageWidening for what widens and why.
const TextureImpl::ImageBindableStorageWidening imageWidening =
m_imageBindableStorageRequired
? TextureImpl::GetImageBindableStorageWidening(MGB_STORAGE_FORMAT(textureMipmapObject))
: TextureImpl::ImageBindableStorageWidening{};
const Bool canAppendMipmaps =
m_isInitialized &&
!m_imageBindableStorageRequired &&
!MGB_STORAGE_IMMUTABLE(stateTextureObject) &&
currentTextureInfo.internalFormat == m_prevTextureInfo.internalFormat &&
currentTextureInfo.width == m_prevTextureInfo.width &&
currentTextureInfo.height == m_prevTextureInfo.height &&
currentTextureInfo.depth == m_prevTextureInfo.depth &&
currentTextureInfo.bufferExternalIndex == m_prevTextureInfo.bufferExternalIndex &&
currentTextureInfo.samples == m_prevTextureInfo.samples &&
currentTextureInfo.fixedSampleLocations == m_prevTextureInfo.fixedSampleLocations &&
currentTextureInfo.mipmapLevels > m_prevTextureInfo.mipmapLevels &&
!TextureImpl::IsMultisampleTextureTarget(targetInternal);
MGLOG_D("%s: Got texture info: %dx%dx%d, mips %d, format %s", __func__, baseSize.x(), baseSize.y(),
baseSize.z(), mipmapCount,
MG_Util::ConvertTextureInternalFormatToString(MGB_STORAGE_FORMAT(textureMipmapObject)).c_str());
if (canAppendMipmaps) {
MGLOG_D("Texture mip count increased for backend ID %u, appending levels %zu..%zu",
m_backendTextureId, m_prevTextureInfo.mipmapLevels, mipmapCount - 1);
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
// A level the application never defined reads back as {0, 0, 0}; now that a
// sparse chain is synced rather than skipped whole, leave those undefined on
// the driver instead of giving the name a 0x0 image at that index.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
continue;
}
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"append-mips")
: nullptr;
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize =
GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), levelTexelSize);
switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
0, glFormat, glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
default:
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str());
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__,
glUploadTarget, glInternalFormat, glFormat, glType,
pData](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, format=%s, "
"type=%s, pixels=%p",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
MG_Util::ConvertGLEnumToString(glFormat).c_str(),
MG_Util::ConvertGLEnumToString(glType).c_str(), pData);
});
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
needsRegeneration = false;
}
if (needsRegeneration) {
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture with ID: %u",
m_backendTextureId);
// A REDEFINITION IN PLACE TAKES THE SAME GENERATION A RE-MINT TAKES (P4a fable
// seam F-3, the pre-handle half). Mutable driver storage is redefined on the
// SAME id below, so unlike RecreateBackendTexture nothing moves the FBO twins'
// memo: the frontend framebuffer versions do not see a texture's respecify
// and the id did not change. An attached texture whose format moved to one
// with the same carrier (GL_SRGB8 -> GL_SRGB8_ALPHA8 on a driver that widens
// the first) therefore kept the framebuffer's alpha-widening mask, and every
// draw into it stayed masked. The first definition is not a redefinition and
// bumps nothing; a redefinition that went through RecreateBackendTexture above
// has already bumped.
// PUSH BUILDS ONLY. This is Espryt code the pull build would share, and G1
// keeps the pull library byte-identical to the P4a baseline (the bump resized
// this function and the renderbuffer twin's SyncToBackend: 0/0/2/0). So the
// pull build carries the pre-P4a hole until these lines land on dev on their
// own; every arm of a push build has the fix.
if (m_isInitialized && !m_backendStorageImmutable) {
++FramebufferImpl::g_attachmentBackendIdGeneration;
}
// Regenerate all mipmap levels
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
// The frontend validates against the count MobileGL advertises, which can
// exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4,
// GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call:
// stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and
// framebuffer completeness still report what the application asked for.
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetFormatCapabilityTargetIndex(targetInternal), MGB_STORAGE_FORMAT(textureMipmapObject),
glFormat, static_cast<Int>(MGB_STORAGE_SAMPLES(stateTextureObject))));
// ES 3.1 8.19 requires width/height (and depth, for the array target) >= 1,
// so a degenerate size has nothing to allocate and must not reach the
// driver. The frontend deallocates such an image rather than defining it
// (GL 4.6 core 8.8), so this is belt and braces for any path that still
// syncs one.
const Bool hasAllocatableSize =
baseSize.x() >= 1 && baseSize.y() >= 1 &&
(targetInternal != TextureTarget::Texture2DMultisampleArray || baseSize.z() >= 1);
if (!hasAllocatableSize) {
MGLOG_D("Skipping multisample storage for texture %u: degenerate size (%d, %d, %d)",
m_backendTextureId, baseSize.x(), baseSize.y(), baseSize.z());
} else {
switch (targetInternal) {
case TextureTarget::Texture2DMultisample:
g_GLESFuncs.glTexStorage2DMultisample(
target, backendSamples, glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE);
break;
case TextureTarget::Texture2DMultisampleArray:
g_GLESFuncs.glTexStorage3DMultisample(
target, backendSamples, glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
static_cast<GLsizei>(baseSize.z()),
MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE);
break;
default:
MOBILEGL_ASSERT(false, "Unexpected multisample target: %d",
static_cast<Int>(targetInternal));
break;
}
m_backendStorageImmutable = true;
}
// The one storage branch that cleared the ES error queue without ever
// draining it again, so anything this call raised was left for an
// unrelated later query to trip over. Paired with its two siblings now.
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target,
glInternalFormat, backendSamples](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*Multisample: target=%s, internalformat=%s, "
"samples=%d",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
static_cast<Int>(backendSamples));
});
for (const auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
} else if (MGB_STORAGE_IMMUTABLE(stateTextureObject) || m_imageBindableStorageRequired) {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexStorage2D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()));
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()),
static_cast<GLsizei>(storageSize.z()));
break;
default:
MGLOG_E_ONCE("Unhandled immutable texture target %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target,
glInternalFormat](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*: target=%s, internalformat=%s", func,
file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
});
m_backendStorageImmutable = true;
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
const bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
if (levelDirty && levelByteSize != 0) {
auto levelTexelSize =
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"immutable-regen");
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
Vector<Uint8> imageWidenedUploadData;
uploadData = PrepareImageWidenedUpload(imageWidening, levelTexelSize, uploadData,
levelByteSize, imageWidenedUploadData);
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize =
GetBackendUploadSize(targetInternal, levelTexelSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat, glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType, uploadData);
break;
default:
break;
}
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__, glUploadTarget,
glFormat, glType, pData](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexSubImage*: target=%s, format=%s, "
"type=%s, pixels=%p",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(),
MG_Util::ConvertGLEnumToString(glFormat).c_str(),
MG_Util::ConvertGLEnumToString(glType).c_str(), pData);
});
}
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
} else {
m_backendStorageImmutable = false;
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
// See the append-mips loop: an undefined level stays undefined on the
// driver rather than becoming a 0x0 image.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 ||
levelTexelSize.z() <= 0) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
continue;
}
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"mutable-regen")
: nullptr;
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, "
"levelDirty = %s",
__func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
level, levelTexelSize.x(), levelTexelSize.y(), levelTexelSize.z(),
levelByteSize, pData, levelDirty ? "true" : "false");
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
auto textureTarget = MGB_TEXTURE_TARGET(stateTextureObject);
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
switch (MapToBackendTextureTarget(textureTarget)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap: {
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), 0, glFormat, glType, uploadData);
break;
}
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray: {
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
}
default: {
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
}
}
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__, glUploadTarget,
glInternalFormat, glFormat, glType, pData](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, "
"format=%s, type=%s, pixels=%p",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
MG_Util::ConvertGLEnumToString(glFormat).c_str(),
MG_Util::ConvertGLEnumToString(glType).c_str(), pData);
});
MGLOG_D("Regenerated mipmap level %d for texture with ID: %u", level,
m_backendTextureId);
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
}
m_isInitialized = true;
}
{ // Update all dirty mipmap levels
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
for (const auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
if (MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
}
break;
}
const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject);
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
// The storage this level is being written into was widened when it was minted
// (see above), so the transfer pair has to describe the carrier here too - ES
// requires glTexSubImage's `format` to match the storage's base internal
// format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION.
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
ScopedDefaultUnpackState unpackState;
// Per-level scratch of the exact-footprint path, hoisted to reuse capacity.
Vector<MG_State::GLState::MipmapDirtyRegion> dirtyFootprint;
Vector<UnpackStagingBlock> stagingBlockOverflow;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
if (!MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) {
continue;
}
auto byteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
if (byteSize == 0) {
MGLOG_D("Mipmap level %d has no data, skipping update.", level);
continue;
}
if (level > 0)
MGLOG_D("%s: Updating dirty mip %d for texture ID %u, size: %dx%d, "
"byteSize: %d",
__func__, level, m_backendTextureId,
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).x(),
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).y(), byteSize);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line,
MG_Util::ConvertGLEnumToString(err).c_str());
});
auto texelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
const void* mipData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"dirty-level");
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, texelSize, mipData, byteSize,
glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), texelSize,
uploadData, byteSize, &glType, packedUploadData);
// Leaves `uploadData` pointing at its own buffer when it fires, with
// texels four components wide where the shadow's are one or two.
Vector<Uint8> imageWidenedUploadData;
uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize,
imageWidenedUploadData);
const IntVec3 uploadSize =
GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), texelSize);
// Sub-rect upload: when only a region of the level changed (a
// 16x16 sprite in a 1024x512 atlas, the per-frame lightmap), upload
// just that region with UNPACK_ROW_LENGTH striding into the level.
// Not an optimization alone: the shadow never sees a GPU write, so
// the texels OUTSIDE the region may be stale there and must not go
// up over what the GPU drew. Every conversion above rewrites the
// whole level texel for texel into a tight buffer of its own, so the
// region is taken from whichever buffer `uploadData` now names, at
// that buffer's bytes per texel.
//
// THE SAME ON EVERY ARM (P11 M). `mipData` is the frontend shadow on
// the pull and push-monolith arms and the server's staged level on the
// split arms - the same tight client layout either way - and the
// conversions run on whichever side uploads, so the handle arm takes
// its region from the converted buffer exactly as the shadow arm
// does. What it must NOT take from the record then is the carried
// pitch and offset (below): those index the client's layout, not the
// converted one.
SizeT uploadBytes = 0;
if (uploadData == mipData) {
uploadBytes = byteSize;
} else if (!imageWidenedUploadData.empty() &&
uploadData == imageWidenedUploadData.data()) {
uploadBytes = imageWidenedUploadData.size();
} else if (!packedUploadData.empty() && uploadData == packedUploadData.data()) {
uploadBytes = packedUploadData.size();
} else if (!widenedUploadData.empty() && uploadData == widenedUploadData.data()) {
uploadBytes = widenedUploadData.size();
} else if (!convertedUploadData.empty() && uploadData == convertedUploadData.data()) {
uploadBytes = convertedUploadData.size() * sizeof(Float);
}
// A 1D array's ES image is a 2D array one texel high with the layers
// in depth, while the shadow keeps the layers as rows: same texels in
// the same order, so a region maps across by moving its y to z. (The
// target is the descriptor's on the split arms: the server may not
// name the client's texture object.)
const Bool layersAsRows =
MGB_TEXTURE_TARGET(stateTextureObject) == TextureTarget::Texture1DArray;
// P4a (D-D6, and ARCHITECTURE.md:321's ONE item moved out of the
// Espryt do-not-touch list): the DECISION below is unchanged and
// stays on the server - it is the side that pays the GPU cost, and
// the union-box-versus-N-rects choice is worth 16x the bytes on one
// axis and +6 ms/frame of Mali job cost on the other. What moves is
// the SOURCE of the shape and of the strides.
//
// On the handle arm the box and the rect list come from the record
// the client emitted; on the legacy arm they come from the frontend's
// own rect model, exactly as before. Both spellings feed the same
// three staging shapes and the same StageBlocksIntoUnpackRing, which
// is byte-identical (G5).
const MG_Pipe::MGPipeResourceRecord::PendingUpload* pendingUpload =
pushedStorage != nullptr
? FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))
: nullptr;
const auto dirtyRegion = [&]() -> MG_State::GLState::MipmapDirtyRegion {
if (pendingUpload == nullptr) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): a level this arm owes with NO pending upload behind it
// was dirtied by the GPU (T5), and the dirty answer is the
// server's own mark on the staged shadow - the whole level,
// because a generation touches all of it. The client is never
// asked (§2.2's last row).
if (MGB_STAGED_TEXTURE_LIVE) {
const IntVec3 gpuExtent =
MG_Remote::Server::ServerStagedTexture().LevelExtentOrUndefined(
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes),
static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level));
return MG_State::GLState::MipmapDirtyRegion{IntVec3{0, 0, 0}, gpuExtent};
}
#endif
return textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level);
}
// MGPBox is {origin, extent}; MipmapDirtyRegion is {lo, hi}. The
// conversion is the whole difference between the two spellings.
const auto& box = pendingUpload->UnionBox;
return MG_State::GLState::MipmapDirtyRegion{
IntVec3{box.X, box.Y, box.Z},
IntVec3{box.X + static_cast<Int32>(box.W), box.Y + static_cast<Int32>(box.H),
box.Z + static_cast<Int32>(box.D)}};
}();
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
// Whether a box of the level can be addressed at all: whole texels in
// the upload bytes, and an ES image shaped like the shadow.
const Bool regionsAddressable =
!dirtyRegion.Empty() && texelCount > 0 && uploadBytes > 0 &&
uploadBytes % texelCount == 0 &&
(layersAsRows ? std::max(texelSize.z(), 1) == 1
: uploadSize.x() == texelSize.x() && uploadSize.y() == texelSize.y() &&
std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1));
const Bool subRectEligible = regionsAddressable && !dirtyRegion.CoversWholeLevel(texelSize);
const SizeT bpp = regionsAddressable ? uploadBytes / texelCount : 0;
const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(),
dirtyRegion.hi.y() - dirtyRegion.lo.y(),
dirtyRegion.hi.z() - dirtyRegion.lo.z()};
// STRIDES ARE CARRIED, NEVER INFERRED (D-D3), once there is a record
// to carry them: a sub-rect's rows are not contiguous in the level
// shadow, so each MGPSubRegion states the level's row and slice pitch
// and 0 means "tightly packed", which is what a whole-level region
// sends. The legacy arm keeps computing them from the level extent
// and the bytes-per-texel it just derived - the same two numbers,
// from the other side of the wire.
// m-2: THE PITCH IS THE LEVEL'S (D-D3), so every region of one level
// states the same one - and v1 read Regions.front() and would have
// strided the rest at the first one's pitch, silently, if a record
// ever disagreed. This is the assert the review asked for rather than
// a per-region loop feeding per-region strides: a record whose regions
// disagree is corrupt, not a shape the upload planner should learn.
// On disagreement the carried pair is DROPPED and the level's own
// extent times bpp is used, which is what the legacy arm computes.
Bool pendingStridesAgree = true;
if (pendingUpload != nullptr && !pendingUpload->Regions.empty()) {
const auto& firstRegion = pendingUpload->Regions.front();
for (const auto& region : pendingUpload->Regions) {
if (region.SrcRowStride == firstRegion.SrcRowStride &&
region.SrcSliceStride == firstRegion.SrcSliceStride) {
continue;
}
pendingStridesAgree = false;
MGLOG_E_ONCE("MGPipe: texture %u level %u carries regions with two "
"different level pitches (%u/%u and %u/%u) - a pitch is "
"the LEVEL's, so the carried pair is dropped and the "
"extent is used instead",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject),
static_cast<Uint>(level), firstRegion.SrcRowStride,
firstRegion.SrcSliceStride, region.SrcRowStride,
region.SrcSliceStride);
break;
}
}
// P11 M: THE CARRIED PITCH AND OFFSET INDEX THE CLIENT'S LAYOUT - the
// level shadow, or the server's staged copy of it, which is `mipData`.
// A converted level's region is cut from the conversion's own tight
// buffer (above), whose texels are wider, so there the record's numbers
// name the wrong bytes and the pitch is derived from the upload
// buffer's bpp instead - which is what the shadow arm does.
const Bool carriedLayoutApplies = uploadData == mipData;
const SizeT carriedRowBytes =
(pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies &&
!pendingUpload->Regions.empty() &&
pendingUpload->Regions.front().SrcRowStride != 0)
? static_cast<SizeT>(pendingUpload->Regions.front().SrcRowStride)
: 0;
const SizeT carriedSliceBytes =
(pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies &&
!pendingUpload->Regions.empty() &&
pendingUpload->Regions.front().SrcSliceStride != 0)
? static_cast<SizeT>(pendingUpload->Regions.front().SrcSliceStride)
: 0;
const SizeT levelRowBytes =
carriedRowBytes != 0 ? carriedRowBytes : static_cast<SizeT>(texelSize.x()) * bpp;
const SizeT levelSliceBytes = carriedSliceBytes != 0
? carriedSliceBytes
: static_cast<SizeT>(texelSize.y()) * levelRowBytes;
// N-5: THE SAME DISCIPLINE FOR SrcOffset AS FOR THE STRIDES, and the
// asymmetry it removes was the review's point: the strides got a loud
// refusal in the commit that started reading them while the offset -
// which decides the ADDRESS glTexSubImage reads (w*bpp x h x d) bytes
// from - was consumed raw. The cross-check is free, because
// rectUploadPtr four lines down computes exactly the same number from
// the region origin and the two pitches. A record whose regions
// disagree with their own origins is corrupt, so on disagreement the
// carried offsets are DROPPED - every use site falls back to the
// derived pointer, which is what the legacy arm sends - and one named
// line says so. Gated on subRectEligible because bpp is 0 otherwise
// and the regions are not read at all, and on carriedLayoutApplies
// because a converted level never reads them (MGB_RECT_SRC_PTR).
//
// FIX A2 IS THE SECOND LEGITIMATE DISAGREEMENT, and the fallback is
// exactly right for it: a level too large to stage whole crosses as
// several slab records (TextureEmit.h's MGPipeForEachTextureSlab) whose
// regions carry offsets into THEIR OWN run rather than into the level
// shadow. Every piece of the level is merged into this one entry, the
// entry's rects are then read out of the SERVER'S WHOLE LEVEL IMAGE -
// which is what the derived pointer names - and those piece-relative
// offsets are precisely the ones that must not be used here.
//
// GATED ON regionsAddressable, NOT subRectEligible: the per-region arm
// for a level the GPU may have written (m_driverMayHoldGpuWrites below)
// reads MGB_RECT_SRC_PTR too, and it runs exactly when the union box is
// the whole level - which is what two or more slab pieces always add up
// to. Checked only under subRectEligible, a damage rect split across
// pieces kept the later pieces' run-relative offsets and read their
// rows from the top of the level image (KWin's wl_shm window textures:
// the panel repeated down the desktop). bpp is valid whenever the
// regions are addressable.
Bool pendingOffsetsAgree = true;
if (pendingUpload != nullptr && regionsAddressable && carriedLayoutApplies) {
for (const auto& region : pendingUpload->Regions) {
const SizeT derivedOffset = static_cast<SizeT>(region.Z) * levelSliceBytes +
static_cast<SizeT>(region.Y) * levelRowBytes +
static_cast<SizeT>(region.X) * bpp;
if (static_cast<SizeT>(region.SrcOffset) == derivedOffset) continue;
pendingOffsetsAgree = false;
MGLOG_E_ONCE("MGPipe: texture %u level %u carries a region whose SrcOffset "
"%llu is not its own origin (%d,%d,%d) at the level's pitch "
"(%llu expected) - the carried offsets are dropped and every "
"rect is read from the shadow instead. Expected once for a "
"level cut into stage-chunk pieces, whose regions index their "
"own run; otherwise this names a record whose regions and "
"origins disagree",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), static_cast<Uint>(level),
static_cast<unsigned long long>(region.SrcOffset), region.X,
region.Y, region.Z,
static_cast<unsigned long long>(derivedOffset));
break;
}
}
const Uint8* regionPtr =
static_cast<const Uint8*>(uploadData) +
static_cast<SizeT>(dirtyRegion.lo.z()) * levelSliceBytes +
static_cast<SizeT>(dirtyRegion.lo.y()) * levelRowBytes +
static_cast<SizeT>(dirtyRegion.lo.x()) * bpp;
// Scatter refinement behind the union box: ~100 sprite
// writes into an atlas leave a box that spans nearly the
// whole level while the touched texels are a few percent of
// it. The storage's bounded rect list recovers the true
// footprint; each rect is uploaded with the same
// ROW_LENGTH striding into the level as the box
// path. The storage only hands the list out when its
// summed area is materially smaller than the box (0
// otherwise), so the extra calls always move fewer bytes.
MG_State::GLState::MipmapDirtyRegion
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
SizeT dirtyRectCount = 0;
if (subRectEligible) {
if (pendingUpload != nullptr) {
// RegionCount == 0 is LEGAL and means "the union box is the
// whole story" (D-D3), which is the same statement the
// frontend's GetStorageDirtyRects makes by returning 0 - so
// both arms reach the box branch below by the same route.
//
// P11 M2: the record's list is the level's EXACT footprint now
// (TextureEmit.h), which the storage's own rect list only
// covered, and the storage withheld that list - "0 rects" - when
// it held more rects than it keeps or when they added up to 3/4
// of the union box (one driver call on nearly the same bytes).
// Both cut-offs are this side's to apply now, so the box-or-rects
// choice on a texture the GPU has not written is what it was.
// (Once the GPU may have written it, the footprint below uploads
// every region exactly, whatever this chose.) Never a partial
// list: a truncated list is dropped writes.
SizeT summedTexels = 0;
for (const auto& region : pendingUpload->Regions) {
summedTexels += static_cast<SizeT>(region.W) * static_cast<SizeT>(region.H) *
static_cast<SizeT>(std::max<Uint32>(region.D, 1));
}
const Bool listWithheld =
pendingUpload->Regions.size() < 2 ||
pendingUpload->Regions.size() > MG_State::GLState::MipmapStorage::kMaxDirtyRects ||
summedTexels * 4 >= dirtyRegion.TexelCount() * 3;
dirtyRectCount = listWithheld ? 0 : pendingUpload->Regions.size();
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& region = pendingUpload->Regions[r];
dirtyRects[r] = MG_State::GLState::MipmapDirtyRegion{
IntVec3{region.X, region.Y, region.Z},
IntVec3{region.X + static_cast<Int32>(region.W),
region.Y + static_cast<Int32>(region.H),
region.Z + static_cast<Int32>(region.D)}};
}
} else
#if MOBILEGL_BUILD_DISAGGREGATED
// tx: with an active transport the rect list is the record's (the
// pendingUpload arm above) or nothing - the server's GPU-dirty mark
// is whole-level and has no scatter refinement to hand out, and the
// frontend's rect model is not this side's to read.
if (!MGB_STAGED_TEXTURE_LIVE)
#endif
dirtyRectCount = textureMipmapObject->GetStorageDirtyRects(
uploadTarget, level, dirtyRects,
MG_State::GLState::MipmapStorage::kMaxDirtyRects);
// The scatter refinement pays only on the client-pointer path,
// where fewer bytes mean less driver-side copying. Through the
// unpack ring every glTexSubImage is a GPU copy job (Mali), so
// ~100 sprite rects become ~100 jobs whose fixed cost dwarfs
// the union box's extra bytes - measured +6 ms/frame of GPU
// time in MC's animated-atlas ticks. One box, one job.
if (BufferImpl::UnpackRingAvailable()) {
dirtyRectCount = 0;
}
}
// Everything above - the union box, the ring's one-box rule, the rect
// list with its merges, fold and 3/4 cut-off - may upload shadow texels
// the client did not write, which is only harmless while the driver
// still holds those texels as they were last uploaded. Once it may hold
// GPU writes, the level goes up as exactly the texels written, one box
// (one ring job) each, whatever that costs: more boxes, and a union box
// that spans the whole level no longer means a whole-level upload.
const MG_State::GLState::MipmapDirtyRegion* uploadRects = dirtyRects;
SizeT uploadRectCount = dirtyRectCount;
if (m_driverMayHoldGpuWrites && regionsAddressable) {
if (pendingUpload != nullptr) {
// The handle arm's footprint is the record's region list, all
// of it and in order (MGB_RECT_SRC_PTR indexes the regions by
// position); a box-only entry is its union box.
dirtyFootprint.clear();
for (const auto& region : pendingUpload->Regions) {
dirtyFootprint.push_back(MG_State::GLState::MipmapDirtyRegion{
IntVec3{region.X, region.Y, region.Z},
IntVec3{region.X + static_cast<Int32>(region.W),
region.Y + static_cast<Int32>(region.H),
region.Z + static_cast<Int32>(region.D)}});
}
} else
#if MOBILEGL_BUILD_DISAGGREGATED
// With an active transport and no record behind the level, the
// server's own whole-level GPU-dirty mark is the whole story (the
// same reason the rect list above is not read here).
if (MGB_STAGED_TEXTURE_LIVE) {
dirtyFootprint.clear();
} else
#endif
textureMipmapObject->GetStorageDirtyFootprint(uploadTarget, level, dirtyFootprint);
// One box is the union box itself, which the branches below already
// upload exactly (as the whole level when it is the whole level).
if (dirtyFootprint.size() >= 2) {
uploadRects = dirtyFootprint.data();
uploadRectCount = dirtyFootprint.size();
} else {
uploadRectCount = 0;
}
}
const Bool perRectUpload = regionsAddressable && uploadRectCount >= 2;
const auto rectUploadPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) {
return static_cast<const Uint8*>(uploadData) +
static_cast<SizeT>(rect.lo.z()) * levelSliceBytes +
static_cast<SizeT>(rect.lo.y()) * levelRowBytes +
static_cast<SizeT>(rect.lo.x()) * bpp;
};
// m-1 / D-D3, "carried, never inferred": MGPSubRegion::SrcOffset is the byte offset of that
// region's origin into the level shadow - the same number the lambda above derives from the
// origin and the two strides - and on the handle arm it is READ rather than re-derived, after
// the cross-check above has agreed that the two spellings say the same thing (N-5). It is read
// only while carriedLayoutApplies (uploadData == mipData), so the base the offset is relative to
// is the level shadow itself and never a conversion buffer: since dev's 8966d5c2 a converted
// level is sub-rect eligible too, and its rects are cut at the conversion's bpp (rectUploadPtr).
//
// THREE USE SITES, AND THE FIRST OF THEM IS UNREACHABLE (N-5's second half, and the correction
// to what esprytobj-v2.md m-1 claimed): dirtyRectCount is forced to 0 whenever
// UnpackRingAvailable(), and the staging-block loop that takes MGB_RECT_SRC_PTR(r, rect) first
// requires `ringUsable && dirtyRectCount >= 2`. The two conditions cannot both hold. That shape
// is pre-existing (the ring/scatter decision, not this package's), so the loop is left as it
// stands and the count is corrected here instead of the loop being deleted underneath it.
//
// A MACRO AND NOT A SECOND LAMBDA, for DV-9's measured reason: this function's nine ErrorLopper
// lambdas are mangled ...::$_N BY POSITION, so one more renumbers every one of them and moves
// the PULL build's symbol set, whose admitted-change set is EMPTY. The pull expansion is the
// pre-P4a call with one pair of parentheses. #undef'd with the rest.
#define MGB_RECT_SRC_PTR(idx, rect) \
((pendingUpload != nullptr && pendingOffsetsAgree && carriedLayoutApplies && \
static_cast<SizeT>(idx) < pendingUpload->Regions.size()) \
? static_cast<const Uint8*>(uploadData) + \
static_cast<SizeT>(pendingUpload->Regions[static_cast<SizeT>(idx)].SrcOffset) \
: rectUploadPtr(rect))
// A shadow-grid box as the ES image addresses it, and the image height
// that strides the source to match; they differ only for a 1D array
// (layersAsRows), whose rows are ES layers of a one-texel-high image.
struct BackendBox {
GLint x, y, z;
GLsizei w, h, d;
};
const auto ToBackendBox = [&](const MG_State::GLState::MipmapDirtyRegion& rect) {
const GLsizei w = static_cast<GLsizei>(rect.hi.x() - rect.lo.x());
const GLsizei h = static_cast<GLsizei>(rect.hi.y() - rect.lo.y());
const GLsizei d = static_cast<GLsizei>(rect.hi.z() - rect.lo.z());
return layersAsRows ? BackendBox{rect.lo.x(), 0, rect.lo.y(), w, 1, h}
: BackendBox{rect.lo.x(), rect.lo.y(), rect.lo.z(), w, h, d};
};
const GLint imageHeight = layersAsRows ? 1 : texelSize.y();
// Unpack-ring staging plan, decided ONCE for whichever branch
// below runs: either every glTexSubImage of this level sources
// from the ring or none does, so the pixel-unpack binding is
// toggled exactly once per level. The plan's three shapes line
// up 1:1 with the switch's three branches.
//
// Regions are repacked TIGHTLY (row length = the region's own
// width), which is why the ring path issues no glPixelStorei at
// all: the surrounding ScopedDefaultUnpackState already holds
// ROW_LENGTH/IMAGE_HEIGHT at 0, which is exactly what a tight
// block wants. It also stages strictly fewer bytes than the
// client-pointer path makes the driver walk, which strides over
// the whole level width.
UnpackStagingBlock
stagingBlockArray[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
UnpackStagingBlock* stagingBlocks = stagingBlockArray;
SizeT stagingBlockCount = 0;
const Bool ringUsable = BufferImpl::UnpackRingAvailable();
if (!ringUsable) {
// Nothing to plan: every branch below keeps the client
// pointer and its ROW_LENGTH striding, unchanged.
} else if (perRectUpload) {
// Only an exact footprint outgrows the array (see uploadRects).
if (uploadRectCount > MG_State::GLState::MipmapStorage::kMaxDirtyRects) {
stagingBlockOverflow.resize(uploadRectCount);
stagingBlocks = stagingBlockOverflow.data();
}
for (SizeT r = 0; r < uploadRectCount; ++r) {
const auto& rect = uploadRects[r];
stagingBlocks[r] = {
MGB_RECT_SRC_PTR(r, rect),
static_cast<SizeT>(rect.hi.x() - rect.lo.x()) * bpp,
static_cast<SizeT>(rect.hi.y() - rect.lo.y()),
static_cast<SizeT>(std::max(rect.hi.z() - rect.lo.z(), 1)),
levelRowBytes,
levelSliceBytes,
0};
}
stagingBlockCount = uploadRectCount;
} else if (subRectEligible) {
stagingBlocks[0] = {regionPtr,
static_cast<SizeT>(regionSize.x()) * bpp,
static_cast<SizeT>(regionSize.y()),
static_cast<SizeT>(std::max(regionSize.z(), 1)),
levelRowBytes,
levelSliceBytes,
0};
stagingBlockCount = 1;
} else if (uploadData == mipData && texelCount > 0 &&
byteSize % texelCount == 0) {
// Whole level, shadow bytes verbatim: `byteSize` is exactly
// what the driver would have read from the client pointer,
// and the shadow is already tightly packed. A CONVERTED
// level stays on the pointer path - its buffer's length is
// the conversion's, not the shadow's, so nothing here can
// size the driver's read of it. The whole-texels check is
// the same sanity the sub-rect path applies, and it is what
// makes "the shadow's bytes per texel == the transfer's"
// hold. (A 1D array's upload size permutes height and depth,
// but the texel count and the tight byte order are the same,
// so the flat copy still describes what the driver reads.)
stagingBlocks[0] = {static_cast<const Uint8*>(uploadData),
static_cast<SizeT>(byteSize),
1,
1,
static_cast<SizeT>(byteSize),
static_cast<SizeT>(byteSize),
0};
stagingBlockCount = 1;
}
const Bool ringStaged =
stagingBlockCount > 0 &&
StageBlocksIntoUnpackRing(stagingBlocks, stagingBlockCount);
if (ringStaged) {
BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId());
}
if (MG_Util::PipeStats::Enabled()) {
// One emission per (upload target, level) that ships texels;
// the switch below turns it into either one union-box job or
// dirtyRectCount rect jobs. The box/rect split is counted
// separately from the bytes on purpose: SSIM is blind to it
// and the +6 ms/frame Mali cliff was a shape regression, not
// a byte regression (plan section 7.3).
const Bool rectShape = subRectEligible && dirtyRectCount >= 2;
Uint64 shippedBytes = 0;
if (rectShape) {
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
shippedBytes += static_cast<Uint64>(rect.hi.x() - rect.lo.x()) *
static_cast<Uint64>(rect.hi.y() - rect.lo.y()) *
static_cast<Uint64>(std::max(rect.hi.z() - rect.lo.z(), 1)) *
static_cast<Uint64>(bpp);
}
} else if (subRectEligible) {
shippedBytes = static_cast<Uint64>(regionSize.x()) *
static_cast<Uint64>(regionSize.y()) *
static_cast<Uint64>(std::max(regionSize.z(), 1)) *
static_cast<Uint64>(bpp);
} else {
shippedBytes = static_cast<Uint64>(byteSize);
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
shippedBytes);
MG_Util::PipeStats::AddCalls(
MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1);
MG_Util::PipeStats::AddCalls(
rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions
: MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions,
1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs,
rectShape ? static_cast<Uint64>(dirtyRectCount) : 1u);
}
switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
if (perRectUpload) {
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
for (SizeT r = 0; r < uploadRectCount; ++r) {
const auto& rect = uploadRects[r];
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), rect.lo.x(),
rect.lo.y(), static_cast<GLsizei>(rect.hi.x() - rect.lo.x()),
static_cast<GLsizei>(rect.hi.y() - rect.lo.y()), glFormat,
glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset)
: static_cast<const void*>(MGB_RECT_SRC_PTR(r, rect)));
}
// The surrounding ScopedDefaultUnpackState shadow says 0.
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
} else if (subRectEligible) {
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
dirtyRegion.lo.y(), static_cast<GLsizei>(regionSize.x()),
static_cast<GLsizei>(regionSize.y()), glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset)
: static_cast<const void*>(regionPtr));
// The surrounding ScopedDefaultUnpackState shadow says 0.
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
} else {
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat,
glType,
ringStaged
? UnpackRingPixelOffset(stagingBlocks[0].offset)
: uploadData);
}
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
if (perRectUpload) {
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
}
for (SizeT r = 0; r < uploadRectCount; ++r) {
const auto box = ToBackendBox(uploadRects[r]);
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), box.x, box.y, box.z,
box.w, box.h, box.d, glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset)
: static_cast<const void*>(MGB_RECT_SRC_PTR(r, uploadRects[r])));
}
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
}
} else if (subRectEligible) {
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
}
const auto box = ToBackendBox(dirtyRegion);
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), box.x, box.y, box.z, box.w, box.h,
box.d, glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset)
: static_cast<const void*>(regionPtr));
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
}
} else {
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat,
glType,
ringStaged
? UnpackRingPixelOffset(stagingBlocks[0].offset)
: uploadData);
}
break;
default:
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str());
break;
}
if (ringStaged) {
// Back to the resting unbound state every other upload site
// in this file assumes (their BindPixelUnpackBufferId(0) is
// meant to stay a shadow no-op).
BufferImpl::BindPixelUnpackBufferId(0);
}
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
}
break;
}
case TextureStorageType::Buffer: {
auto* textureBufferObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(stateTextureObject.get());
// P5e (tx2), scout R5 RESOLVED. THE BACKING BUFFER IS THE DESCRIPTOR'S, AND SO IS
// ITS WINDOW - `Desc.BufferForTexBuffer` names the store and `Desc.BufOffset /
// BufSize` name the range, so the frontend binding slot below is not read at all
// on the handle arm.
//
// The one thing that argument had to answer is D-A4's `frontend` parameter of
// EnsureBufferResourceForHandle, which existed for the emulated-persistent-map
// question. The E note's reading is confirmed at this head: that function's three
// frontend reads (Managers.cpp's MappedData / SyncPersistentMappedRange /
// HasDefinedContent) are all inside its MONOLITH arm - under an active transport
// it is client-free (kimi audit row 29) - so `nullptr` is not a hole, it is the
// whole of the split answer, and it is what §4.2's Buffer row already prescribes
// (`EnsureBufferResourceForHandle(nullptr, h)`).
//
// WHOLE-VS-RANGE likewise stops asking the buffer object for its size: the
// sentinel kMGPipeWholeBuffer IS "the whole store", which is the question
// `rangeSize == buffer->GetSize()` was spelling the long way round.
MG_Pipe::MGPipeHandle pushedTexBuffer = MG_Pipe::kMGPipeNullHandle;
if (pushedStorage != nullptr) {
pushedTexBuffer = pushedStorage->Desc.BufferForTexBuffer;
if (MG_Pipe::MGPipeHandleIsNull(pushedTexBuffer)) {
MGLOG_E_ONCE("MGPipe: buffer texture %u's descriptor names no backing buffer on "
"the handle arm, so it cannot be backed (handle {%u, %u})",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), pushedRes.Slot, pushedRes.Gen);
return;
}
}
// P5e (fix1 follow-up, ID-110): the third conjunct is the one fix1's report
// asked for a ruling about. This arm hands the backing-buffer lookup
// kNoFrontendBuffer and leans on the record answering; it was unreachable
// under monolith only because every writer of a handle-note is
// transport-gated - safe by a chain rather than by its own statement, which
// is exactly the shape that cost the monolith arm 137 scenarios. Under a
// transport the conjunct is already implied by `pushedStorage != nullptr`, so
// this changes no behaviour and closes no door; it says the thing instead of
// depending on it.
const Bool texBufferByRecord = pushedStorage != nullptr &&
MGB_TEXTURE_HANDLE_ARM_OFF(*this) == false &&
MGB_TEXTURE_RECORD_ARM_SELECTED();
static const SharedPtr<MG_State::GLState::BufferObject> kNoFrontendBuffer{};
auto& buffer = texBufferByRecord ? kNoFrontendBuffer
: textureBufferObject->GetBufferBindingSlot().GetBoundObject();
if (!buffer && !texBufferByRecord) {
MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
auto bufferIndex = buffer ? buffer->GetExternalIndex() : 0u;
// The memo key is the HANDLE's slot on the handle arm: a GL name is never an
// identity (section 4.2.1), and the record's Serial covers a slot recycled at a
// new generation because a respecify moves it.
currentTextureInfo.bufferExternalIndex =
pushedStorage != nullptr ? static_cast<Uint>(pushedTexBuffer.Slot) : bufferIndex;
Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo);
// Need to sync texture buffer if not synced yet
auto* backendBufferResource =
pushedStorage != nullptr ? BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer)
: BufferImpl::EnsureBufferResource(buffer);
if (!backendBufferResource || backendBufferResource->id == 0) {
MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
// Bind buffer to texture
auto backendId = backendBufferResource->id;
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureBufferObject), &glInternalFormat, &glFormat,
&glType, TextureTarget::TextureBuffer);
// The view half of the buffer-image SPLIT. A buffer texture has no storage of its
// own to widen, but the VIEW its format describes can be re-described one
// component at a time over the same bytes - rg32f over N texels is r32f over 2N -
// and WidenImageFormatsPass rewrites every access to subscript it that way. Only
// for a texture that is actually image-bound: a sampled-only buffer texture keeps
// the format the application asked for (see GetImageBindableBufferSplitFormat).
//
// The split goes on a SEPARATE name (m_bufferImageSplitViewId), not on this one.
// Re-describing the application's own texture also re-describes what a
// samplerBuffer reading it sees, and the sampler side is not subscript-rewritten -
// so texelFetch(s, i) started returning component 2i of the base view instead of
// texel i. rg32f is a legal SAMPLED buffer-texture format in ES 3.2; only the
// IMAGE binding needs the split, so only the image binding's name carries it.
const GLenum bufferImageSplitFormat =
m_imageBindableStorageRequired
? TextureImpl::GetImageBindableBufferSplitFormat(MGB_STORAGE_FORMAT(textureBufferObject))
: GL_UNKNOWN_MGL;
if (needsRegeneration) {
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
// 4.x context, so glTexBuffer is a legal call the app may make on any driver -
// but ES only gained them in 3.2, and g_GLESFuncs.glTexBuffer is simply null
// below that without EXT/OES_texture_buffer. Calling it was an unconditional
// null dereference. There is no conformant way to refuse the call (it is valid
// in the context MobileGL claims), so the texture is left unbacked and the
// reason is stated once per object, latched by the flag below. It was parked
// at MGLOG_I while the level ordering compiled MGLOG_W out of INFO builds;
// W is the correct level and now survives there.
if (!AreBufferTexturesSupported()) {
if (m_bufferTextureUnsupportedReported) {
break;
}
m_bufferTextureUnsupportedReported = true;
MGLOG_W("Texture buffer %u cannot be backed: this ES driver has no buffer "
"textures (%s). Every draw sampling it will read zero and every "
"shader declaring a samplerBuffer will fail to compile. MobileGL "
"still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an "
"OpenGL 4.x context may not report 0.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), GetBufferTextureTierName(),
g_GLESCapabilities.MaxTextureBufferSize);
break;
}
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with "
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
// A texture that names a window of the buffer needs the range form; the
// whole-buffer forms report offset 0 and the buffer's current size, which
// glTexBuffer expresses more directly (and works where the range entry point
// is absent).
// The WINDOW is carried: MGPResourceDesc::BufOffset / BufSize, with
// kMGPipeWholeBuffer (~0) meaning "the whole store, resolved live" - which is
// why the whole-buffer arm still asks the BUFFER for its size. That is the
// buffer family's own object (P3a's, not this record's), and resolving it
// here is exactly what the sentinel is documented to mean.
const SizeT rangeOffset = pushedStorage != nullptr
? static_cast<SizeT>(pushedStorage->Desc.BufOffset)
: textureBufferObject->GetBufferRangeOffset();
// P5e (tx2): "the whole store" is the SENTINEL on the handle arm, not a size
// compare against a frontend BufferObject this arm may not name. The monolith
// arm keeps resolving the sentinel live, exactly as its comment says.
const Bool wholeBufferRange =
pushedStorage != nullptr
? (pushedStorage->Desc.BufOffset == 0 &&
pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer)
: (textureBufferObject->GetBufferRangeOffset() == 0 &&
textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize());
const SizeT rangeSize =
pushedStorage == nullptr
? textureBufferObject->GetBufferRangeSizeInBytes()
: (pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer
? (buffer ? buffer->GetSize() : 0u)
: static_cast<SizeT>(pushedStorage->Desc.BufSize));
// Through CallTexBuffer/CallTexBufferRange rather than g_GLESFuncs directly:
// the unsuffixed entry points are the ES 3.2 core spelling, and a driver
// whose buffer textures come from EXT/OES_texture_buffer exports the
// suffixed ones instead. The dispatchers pick whichever this tier ships.
if (wholeBufferRange) {
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize))) {
MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no "
"glTexBufferRange; binding the whole buffer instead",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
}
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) {
MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s",
func, file, line, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
backendId, MG_Util::ConvertGLEnumToString(err).c_str());
});
// The image half of the SPLIT, on its own name over the same buffer. Minted
// lazily - only a texture that is both image-bound AND holds a format with no
// ESSL image spelling ever gets one - and re-pointed here, in the same
// regeneration gate as the view above, so the two never describe different
// buffers or different windows of one.
if (bufferImageSplitFormat != GL_UNKNOWN_MGL) {
if (m_bufferImageSplitViewId == 0) {
g_GLESFuncs.glGenTextures(1, &m_bufferImageSplitViewId);
}
if (m_bufferImageSplitViewId == 0) {
MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; "
"its image binding will read the unsplit view.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
} else {
g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId);
if (wholeBufferRange) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize))) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
}
// The raw bind above went behind Bind()'s shadow, which tracks objects
// rather than names: leaving it claiming THIS object is bound would
// make the next Bind(GL_TEXTURE_BUFFER) a no-op and leave the split
// view bound in the application texture's place.
g_boundTexturesCache[g_activeTextureUnit][static_cast<SizeT>(
TextureTarget::TextureBuffer)] = nullptr;
}
}
}
break;
}
default:
// TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a
// backstop for a state object that grew a new storage kind. Skipping the upload
// renders wrong; throwing unwinds through the C GL ABI and kills the process.
MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
"skipping this sync",
static_cast<int>(MGB_TEXTURE_STORAGE_KIND(pushedStorage, stateTextureObject)),
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
m_prevTextureInfo = currentTextureInfo;
if (pushedStorage != nullptr) {
// The handle arm's one stamp. Deliberately AFTER the switch, at the same
// instant the legacy arm stamps its four keys, so a bail arm that returned
// early leaves it untouched and the next sync re-runs - which is the property
// the pending set exists to make safe (D-D5).
//
// The record pointer was resolved before any driver work and TextureResources
// has not been grown since (nothing this function calls emits a create), so
// re-reading Serial through it is sound; the PendingUploads entries it named
// have been consumed one at a time by MGB_LEVEL_UPLOAD_DONE.
m_syncedResourceSerial = pushedStorage->Serial;
return;
}
// Everything dirty at entry is uploaded (or provably has no bytes to
// upload); stamp the version so per-draw re-syncs short-circuit until
// the next CPU-side mutation.
m_syncedContentVersion = stateTextureObject->GetContentVersion();
// Same instant, so the cheap gate's keys describe exactly this synced state.
// Only Mipmap storage may arm it - the gate refuses other storage types anyway,
// but a stale trio must not linger on an object that later switches type.
if (MG_Pipe::gPipeInputs.IsLive() && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
m_syncedShapeContextId = MG_Pipe::gPipeInputs.GetTextureContextId();
m_syncedShapeGeneration = MG_Pipe::gPipeInputs.GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
} else {
m_syncedShapeContextId = 0;
}
}
#undef MGB_LEVEL_NEEDS_UPLOAD
#undef MGB_LEVEL_UPLOAD_DONE
#undef MGB_RECT_SRC_PTR
#undef MGB_STORAGE_FORMAT
#undef MGB_STORAGE_BASE_SIZE
#undef MGB_STORAGE_LEVELS
#undef MGB_STORAGE_SAMPLES
#undef MGB_STORAGE_FIXED_SAMPLE_LOCATIONS
#undef MGB_STORAGE_IMMUTABLE
#undef MGB_STORAGE_KIND
#undef MGB_TEXTURE_TARGET
#undef MGB_UPLOAD_TARGETS
#undef MGB_LEVEL_TEXEL_SIZE
#undef MGB_LEVEL_BYTE_SIZE
#undef MGB_LEVEL_TEXELS
#if MOBILEGL_BUILD_DISAGGREGATED
#undef MGB_STAGED_TEXTURE_LIVE
#endif
// P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD. On the by-handle arm the handle
// is already known - the caller resolved the record before it resolved this twin and
// noted the handle on the twin itself - so the client allocator is not probed at all.
// What is left below is the MONOLITH GLUE half (HandleOfBuffer's shape): a twin minted
// from a frontend object and never adopted by handle still re-derives one.
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolveOwnRecord(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) const {
MG_Pipe::MGPipeHandle res = m_pushedSyncHandle;
if (MG_Pipe::MGPipeHandleIsNull(res)) {
if (!stateTextureObject) return nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the
// scope - the by-handle arm above is what retires it.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
res = g_backendTextureObjects.HandleOf(stateTextureObject.get());
}
return PipeTextureRecordForHandle(res);
}
const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject,
const MG_Pipe::MGPipeResourceRecord* record) {
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its "
"built-in sampler cannot be pushed (handle {%u, %u})",
TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot,
m_pushedSyncHandle.Gen);
return nullptr;
}
// NO set_texture_params HAS BEEN APPLIED FOR THIS TEXTURE YET, which is not the same
// thing as a malformed one and must not be read as one. ParamsSerial is the
// applier's own "have I ever stored a params record here" (it is bumped on every
// set_texture_params and starts at 0), and Params is value-initialised beside it -
// so on a texture the application has never given a parameter to, BuiltinSampler is
// null because the record was never written, not because a client wrote a null.
// There is nothing to push in that state; SyncTextureParamsToBackend's own gate
// reaches the same conclusion silently (m_syncedParamsSerial starts at 0 too and the
// compare skips), and this decline is made silent for the same reason. Found by the
// verification round's census: three cases were declining loudly here on textures
// that had simply never been glTexParameter'd.
if (record->ParamsSerial == 0) {
MGLOG_D("Texture %u has no set_texture_params record yet, so it has no built-in "
"sampler to push.",
TextureDiagName(stateTextureObject, record));
return nullptr;
}
// D-E1: kMGPipeNullHandle in a record that WAS written is Fatal{ProtocolCorruption}
// territory - EVERY ITextureObject owns a SamplerObject, so a null is a malformed
// record and not "no sampler". The applier is where that verdict is raised; this
// side names it and declines, because a backend that sampled through whatever the
// driver texture last held would be the silent half of the same bug.
const MG_Pipe::MGPipeHandle builtin = record->Params.BuiltinSampler;
if (MG_Pipe::MGPipeHandleIsNull(builtin)) {
MGLOG_E_ONCE("MGPipe: texture %u's set_texture_params names the null handle as its "
"built-in sampler CSO; every texture owns a sampler object, so this "
"record is malformed - declining the built-in sampler push",
TextureDiagName(stateTextureObject, record));
return nullptr;
}
const auto* cso = PipeSamplerCsoRecordForHandle(builtin);
if (cso == nullptr) {
MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier "
"record - declining the built-in sampler push",
TextureDiagName(stateTextureObject, record), builtin.Slot, builtin.Gen);
return nullptr;
}
// THE HANDLE IS PART OF THE KEY, not just the serial. Sampler CSOs are
// content-addressed at capacity 256 (D-F1), so two textures with identical sampling
// share one CSO and a texture whose sampling diverges is re-pointed at a DIFFERENT
// CSO whose serial may well be smaller than the one it left. Keying on the serial
// alone would then skip the re-push and leave the driver texture filtering with the
// old CSO's values for ever.
//
// SamplerResync is the second half of the gate and it is the one that matters more
// than mis-filtering: ES makes a texture INCOMPLETE when its filters do not suit its
// level set, and an incomplete texture samples (0,0,0,1) rather than its contents.
// The server SETS it (RecreateBackendTexture) and the wire byte exists so the client
// can force a resync it knows about; neither side clears the other's copy (D-E2), so
// it is read here and never written back.
if (m_syncedBuiltinSampler == builtin && m_syncedBuiltinSamplerSerial == cso->Serial &&
record->Params.SamplerResync == 0 && !m_forceSamplerResync) {
MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.",
m_backendTextureId);
return nullptr;
}
m_syncedBuiltinSampler = builtin;
m_syncedBuiltinSamplerSerial = cso->Serial;
m_forceSamplerResync = false;
return &cso->Params;
}
void BackendTextureObject::SyncBuiltinSamplerToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// P5e (tx2): NULL IS THE BY-HANDLE ARM, not an error, once a handle has been noted on
// this twin - the record and the built-in sampler's CSO answer everything below and
// the frontend object is not consulted at all. Without a handle it is still the
// caller's bug it always was. P5e (fix1): and with a handle noted on an arm that
// cannot serve it, it is Fatal{RoleViolation, "texture-handle-arm"} - see the
// refusal's own comment beside MGB_TEXTURE_HANDLE_ARM_OFF.
if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncBuiltinSamplerToBackend")) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
// P4a (D-E1): the fifteen sampler values are SAMPLER state (GL 4.6 table 23.18) and
// Espryt pushes them with glTexParameter* onto the TEXTURE rather than with
// glBindSampler onto the unit - behaviour P4a preserves exactly. What changes is
// where the values and the "did they move" answer come from.
//
// The whole arm choice is a preprocessor #if/#else rather than a runtime branch
// around one shared prologue, and that is a G1 requirement (D-P): the PULL build's
// text below the #else is the pre-P4a text token for token, so its object code
// cannot move. Routing the pull path through a pointer it did not have before was
// measured at -2 bytes on this function, which is a G1 failure.
const SamplerParameters* pendingSamplerParams = nullptr;
// P5e (tx2): resolved ONCE and shared with the target read below, which used to be
// the frontend `GetTarget()` this arm was not allowed to make.
const MG_Pipe::MGPipeResourceRecord* pushedDesc = nullptr;
if (TextureResourceSubsystemEnabled()) {
pushedDesc = ResolveOwnRecord(stateTextureObject);
pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject, pushedDesc);
if (pendingSamplerParams == nullptr) return;
} else {
// UNREACHABLE: ResolveTextureResourceSubsystemArm stops the process at its first
// call when the bit is clear and the pre-handle arm is not compiled. Kept, and
// kept loud, for the reason the vertex-input arm keeps its twin - this is what
// an arm resolution that ever stopped stopping would reach, and sampling through
// stale filter state is exactly what must not happen quietly.
MGLOG_E_ONCE("MGPipe: the texture-resource subsystem bit is clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 removed the pre-handle built-in sampler "
"sync, so this configuration has no arm at all");
return;
}
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
m_backendTextureId, TextureDiagName(stateTextureObject, pushedDesc));
auto targetInternal = MGB_TEXPARAM_TARGET(pushedDesc, stateTextureObject);
GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal);
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
const SamplerParameters& samplerParams = *pendingSamplerParams;
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
m_cacheSamplerParameters = samplerParams;
return;
}
Bind(target);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
// Update built-in sampler parameters
MGLOG_D("Updating sampler parameters for texture with ID: %u", m_backendTextureId);
#define SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \
if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \
g_GLESFuncs.glTexParameteri(target, glName, \
MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \
m_cacheSamplerParameters.internalName = samplerParams.internalName; \
DebugImpl::ErrorLopper::Loop( \
[file = __FILE__, line = __LINE__, func = __func__, \
t = MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)](GLenum err) { \
MGLOG_D("%s(%s:%d) ES error %s, GL_TEXTURE_MIN_FILTER = %s", func, file, line, \
MG_Util::ConvertGLEnumToString(err).c_str(), MG_Util::ConvertGLEnumToString(t).c_str()); \
}); \
}
if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter ||
m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) {
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
(GLint)ResolveBackendMinFilter(samplerParams, IsAngleLlvmpipeRenderer()));
m_cacheSamplerParameters.minFilter = samplerParams.minFilter;
m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode;
}
if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) {
g_GLESFuncs.glTexParameteri(
target, GL_TEXTURE_MAG_FILTER,
(GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None));
m_cacheSamplerParameters.magFilter = samplerParams.magFilter;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode)
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode)
if (SupportsWrapR(targetInternal)) {
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode)
} else {
m_cacheSamplerParameters.wrapR = samplerParams.wrapR;
}
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc)
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode)
if (m_cacheSamplerParameters.minLod != samplerParams.minLod) {
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MIN_LOD, samplerParams.minLod);
m_cacheSamplerParameters.minLod = samplerParams.minLod;
}
if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) {
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod);
m_cacheSamplerParameters.maxLod = samplerParams.maxLod;
}
if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) {
if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) {
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT,
samplerParams.maxAnisotropy);
}
// Unsupported GLES backends intentionally treat anisotropy as a
// frontend-only no-op; remember the observed value so the cache
// remains coherent without issuing an illegal enum every sync.
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
#undef SYNC_TEX_SAMPLER_PARAM_IF_CHANGED
}
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
// P5e (tx2): the handle the caller adopted this twin with, and only failing that the
// client allocator - ResolveOwnRecord holds the whole of that rule.
const auto* record = ResolveOwnRecord(stateTextureObject);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its "
"parameters cannot be pushed (handle {%u, %u})",
TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot,
m_pushedSyncHandle.Gen);
return nullptr;
}
// ParamsSerial + ForceResync replace m_syncedTextureParamsVersion +
// m_forceTextureParamsResync. ForceResync exists because the widened-channel
// carrier needs a swizzle override the frontend params version does not move for;
// it is SET by the server (RequireImageBindableStorage, RecreateBackendTexture set
// m_forceTextureParamsResync on the twin) and by the client on the wire, and
// NEITHER SIDE CLEARS THE OTHER'S (D-E2) - so the wire byte is read and never
// written back, while the twin's own flag is cleared here exactly as it was before.
if (m_syncedParamsSerial == record->ParamsSerial && record->Params.ForceResync == 0 &&
!m_forceTextureParamsResync) {
MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.",
m_backendTextureId);
return nullptr;
}
m_syncedParamsSerial = record->ParamsSerial;
m_forceTextureParamsResync = false;
return record;
}
// P4a (D-E1): the six values SyncTextureParamsToBackend pushes, spelled once per arm.
// MACROS, for the reason MGB_LEVEL_NEEDS_UPLOAD gives one function up: in the PULL build
// each expands to the pre-P4a expression at its original site, so that build's tokens -
// and its object code, whose symbol sizes G1 pins to the byte - are unchanged. Reading
// them into locals instead was measured at +9 bytes on this function.
//
// Four of the six come from MGPTextureParams, which is per texture OBJECT and
// independent of any binding - that is what closes D-E3's gap, where a texture that is
// only a READ-framebuffer attachment reaches SyncMipmapsToBackend and nothing else. The
// FORMAT comes from the descriptor beside them, and the BORDER comes from the built-in
// sampler's CSO (D-F4), because a border colour is sampler state.
#define MGB_TEXPARAM_LEVEL_RANGE \
(pushedRecord != nullptr ? UintVec2{static_cast<Uint>(pushedRecord->Params.BaseLevel), \
static_cast<Uint>(pushedRecord->Params.MaxLevel)} \
: stateTextureObject->GetLevelRange())
#define MGB_TEXPARAM_FORMAT \
(pushedRecord != nullptr ? static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat) \
: stateTextureObject->GetFormat())
#define MGB_TEXPARAM_SWIZZLE \
(pushedRecord != nullptr \
? Vec4<TextureSwizzleParam>{static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[0]), \
static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[1]), \
static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[2]), \
static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[3])} \
: stateTextureObject->GetAllSwizzleParams())
// The contract now spells this encoding (c0c, ID-12 DV-2): kMGPipeDepthStencilModeDepth = 0 =
// GL_DEPTH_COMPONENT, kMGPipeDepthStencilModeStencil = 1 = GL_STENCIL_INDEX. v1 open-coded the
// direction as a bare `!= 0` and recorded it as a deviation; the constants are read here instead,
// so a future re-numbering moves both sides at once. ZERO MEANS THE DEFAULT is still what decides
// the direction: GL_DEPTH_COMPONENT is the GL and ES default and a texture that never asks for
// the stencil aspect never emits the call, so a zeroed record has to decode to exactly what such
// a texture already has.
#define MGB_TEXPARAM_DS_MODE \
(pushedRecord != nullptr \
? (pushedRecord->Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX \
: GL_DEPTH_COMPONENT) \
: stateTextureObject->GetDepthStencilTextureMode())
#define MGB_TEXPARAM_BORDER_F (pushedBorder != nullptr ? pushedBorder->borderColor : stateTextureObject->GetBorderColor())
#define MGB_TEXPARAM_BORDER_I \
(pushedBorder != nullptr ? pushedBorder->borderColorI : stateTextureObject->GetBorderColorI())
#define MGB_TEXPARAM_BORDER_UI \
(pushedBorder != nullptr ? pushedBorder->borderColorUI : stateTextureObject->GetBorderColorUI())
#define MGB_TEXPARAM_BORDER_FORM \
(pushedBorder != nullptr ? pushedBorder->borderColorForm : stateTextureObject->GetBorderColorForm())
void BackendTextureObject::SyncTextureParamsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
// see the twin note in SyncBuiltinSamplerToBackend.
if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncTextureParamsToBackend")) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
// P4a (D-E1/D-E3): where the parameter values come from, and what says they moved.
// Null on the handle arm means "nothing to do" and is NEVER a fall-back to the
// frontend; the legacy arm keeps the params-version pair verbatim, token for token,
// because the PULL build's object code may not move (D-P).
const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr;
const SamplerParameters* pushedBorder = nullptr;
// Whether the four border comparands can be READ at all this sync. On the handle arm
// a missing sampler CSO makes them unreadable and the border block below is skipped
// with the refusal already named; on the legacy arm they always come from the
// frontend object, so it is constant true and folds away.
Bool borderColorReadable = true;
if (TextureResourceSubsystemEnabled()) {
pushedRecord = ResolvePushedTextureParams(stateTextureObject);
if (pushedRecord == nullptr) return;
// D-F4: the border colour is SAMPLER state (GL 4.6 table 23.18) and lives on the
// built-in sampler's CSO, not on MGPTextureParams - P4a deliberately does not
// put SamplerParameters on the wire twice. All FOUR comparands (float, int,
// uint, form) still cross and are still all compared below, because two integer
// borders differing above 2^24 share one float and a Float -> Int transition can
// leave every number unchanged while needing a different driver entry point.
const auto* borderCso = PipeSamplerCsoRecordForHandle(pushedRecord->Params.BuiltinSampler);
if (borderCso != nullptr) {
pushedBorder = &borderCso->Params;
} else {
borderColorReadable = false;
MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier "
"record, so its border colour cannot be pushed",
TextureDiagName(stateTextureObject, pushedRecord),
pushedRecord->Params.BuiltinSampler.Slot,
pushedRecord->Params.BuiltinSampler.Gen);
}
} else {
// UNREACHABLE, and kept loud: see the twin note in SyncBuiltinSamplerToBackend.
MGLOG_E_ONCE("MGPipe: the texture-resource subsystem bit is clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 removed the pre-handle texture-parameter "
"sync, so this configuration has no arm at all");
return;
}
MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId,
TextureDiagName(stateTextureObject, pushedRecord));
auto targetInternal = MGB_TEXPARAM_TARGET(pushedRecord, stateTextureObject);
GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal);
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
// Multisample targets reject the *sampler* parameters (LOD range, border color) but
// GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them.
// Bailing out entirely used to drop every swizzle write on the floor, which is what the
// frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which
// deliberately omits the swizzle enums). Note the caches for the skipped parameters are
// still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the
// change detection below would swallow the very writes we came here to emit.
const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal);
if (isMultisampleTarget) {
m_cacheLodRange = MGB_TEXPARAM_LEVEL_RANGE;
if (borderColorReadable) m_cacheBorderColor = MGB_TEXPARAM_BORDER_F;
}
Bind(target);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
// Update texture parameters
MGLOG_D("Updating texture parameters for texture with ID: %u", m_backendTextureId);
const auto& levelRange = MGB_TEXPARAM_LEVEL_RANGE;
if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) {
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast<GLint>(levelRange.x()));
m_cacheLodRange.x() = levelRange.x();
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) {
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(levelRange.y()));
m_cacheLodRange.y() = levelRange.y();
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
// A three-channel format widened to four to keep the image colour-renderable (see
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
// whatever the draw that filled it wrote there is not what GL would report: a format
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
// promotion stays invisible, composed with the swizzle the application asked for.
Vec4<TextureSwizzleParam> swizzleParams = MGB_TEXPARAM_SWIZZLE;
if (TextureImpl::BackendTextureFormatAddsAlpha(MGB_TEXPARAM_FORMAT, targetInternal)) {
for (SizeT channel = 0; channel < 4; ++channel) {
if (swizzleParams[channel] == TextureSwizzleParam::Alpha) {
swizzleParams[channel] = TextureSwizzleParam::One;
}
}
}
// The same composition for the image-format widening, which can add TWO or THREE
// channels rather than one (GL_R8UI carried in a GL_RGBA8UI). GL reads a channel the
// format does not have as 0, except alpha, which reads as 1 - so a sampler must see
// those constants and not whatever the widened storage holds. The upload and the
// shader's own store mask already keep them at exactly these values; this covers
// storage nothing has written yet (glTexStorage with no upload), whose surplus
// channels are undefined. Composed with the application's own swizzle for the same
// reason as the alpha case above: GL_TEXTURE_SWIZZLE names a SOURCE channel of the
// logical texel, so it is the source that is substituted, never the destination.
if (const auto imageWidening =
m_imageBindableStorageRequired
? TextureImpl::GetImageBindableStorageWidening(MGB_TEXPARAM_FORMAT)
: TextureImpl::ImageBindableStorageWidening{}) {
for (SizeT channel = 0; channel < 4; ++channel) {
switch (swizzleParams[channel]) {
case TextureSwizzleParam::Green:
if (imageWidening.SourceChannels < 2) swizzleParams[channel] = TextureSwizzleParam::Zero;
break;
case TextureSwizzleParam::Blue:
if (imageWidening.SourceChannels < 3) swizzleParams[channel] = TextureSwizzleParam::Zero;
break;
case TextureSwizzleParam::Alpha:
if (imageWidening.SourceChannels < 4) swizzleParams[channel] = TextureSwizzleParam::One;
break;
default:
break;
}
}
}
if (swizzleParams != m_cacheSwizzleParams) {
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
g_GLESFuncs.glTexParameteri(target, glEnum, MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams.func)); \
m_cacheSwizzleParams.func = swizzleParams.func; \
}
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(r(), GL_TEXTURE_SWIZZLE_R);
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(g(), GL_TEXTURE_SWIZZLE_G);
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(b(), GL_TEXTURE_SWIZZLE_B);
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(a(), GL_TEXTURE_SWIZZLE_A);
#undef SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED
m_cacheSwizzleParams = swizzleParams;
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
}
// GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver
// without it every such call is INVALID_ENUM, so the parameter is simply not synced.
//
// The FORM has to be forwarded along with the value. A border colour set through
// glTexParameterIiv/Iuiv is an integer one, and an isampler2D/usampler2D fetch of the
// border returns whatever the driver's integer border register holds - so pushing it
// through glTexParameterfv handed the driver float 255.0 and the shader read back
// 1132396544, the bit pattern of that float. glTexParameterIiv/Iuiv are ES 3.2 core
// beside GL_TEXTURE_BORDER_COLOR itself, so they sit behind the same capability gate;
// the entry-point null check covers a driver that advertises the extension without them.
// The redundancy filter has to look at the AUTHORITATIVE representation, not just the
// float one: two integer borders that differ above 2^24 (16777216 and 16777217, say)
// collapse onto the same float, so a float-only comparison would skip the second sync and
// leave the driver holding the first value forever.
// m-3: GATED, because on the handle arm an unreadable border means the CSO record is
// missing and the block below is skipped with the refusal already named - so v1's
// unconditional initialiser was a live frontend read on a switched-over path whose
// value nothing then used. The value a skipped block sees is the cache's own, which
// cannot make the condition true.
const auto borderColorForm = borderColorReadable ? MGB_TEXPARAM_BORDER_FORM : m_cacheBorderColorForm;
if (!isMultisampleTarget &&
borderColorReadable &&
g_GLESCapabilities.SupportsTextureBorderClamp &&
(m_cacheBorderColor != MGB_TEXPARAM_BORDER_F ||
m_cacheBorderColorI != MGB_TEXPARAM_BORDER_I ||
m_cacheBorderColorUI != MGB_TEXPARAM_BORDER_UI ||
m_cacheBorderColorForm != borderColorForm)) {
if (borderColorForm == BorderColorForm::Int && g_GLESFuncs.glTexParameterIiv) {
const auto& borderColorI = MGB_TEXPARAM_BORDER_I;
const GLint borderColorArray[4] = {borderColorI.x(), borderColorI.y(), borderColorI.z(),
borderColorI.w()};
g_GLESFuncs.glTexParameterIiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
} else if (borderColorForm == BorderColorForm::Uint && g_GLESFuncs.glTexParameterIuiv) {
const auto& borderColorUI = MGB_TEXPARAM_BORDER_UI;
const GLuint borderColorArray[4] = {borderColorUI.x(), borderColorUI.y(), borderColorUI.z(),
borderColorUI.w()};
g_GLESFuncs.glTexParameterIuiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
} else {
const auto& borderColor = MGB_TEXPARAM_BORDER_F;
const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(),
borderColor.w()};
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
}
m_cacheBorderColor = MGB_TEXPARAM_BORDER_F;
m_cacheBorderColorI = MGB_TEXPARAM_BORDER_I;
m_cacheBorderColorUI = MGB_TEXPARAM_BORDER_UI;
m_cacheBorderColorForm = borderColorForm;
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
}
// GL_DEPTH_STENCIL_TEXTURE_MODE (GL_ARB_stencil_texturing / ES 3.1 core): which aspect
// of a packed depth/stencil image a sampler reads. Until this was forwarded the
// frontend kept the mode as a pure shadow - glGetTexParameter answered it, sampling
// ignored it - so a usampler2D bound to a D24S8 texture in STENCIL_INDEX mode read the
// depth aspect. Texture state rather than sampler state, so multisample targets take
// it too (ES 3.1 8.10 lists it among the three pnames they accept). It is only sent
// when it has moved, which for the overwhelming majority of textures is never.
const Bool supportsStencilTextureMode =
g_GLESCapabilities.GLESVersion.Major > 3 ||
(g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1);
if (supportsStencilTextureMode) {
const GLenum depthStencilTextureMode = MGB_TEXPARAM_DS_MODE;
if (m_cacheDepthStencilTextureMode != depthStencilTextureMode) {
g_GLESFuncs.glTexParameteri(target, GL_DEPTH_STENCIL_TEXTURE_MODE,
static_cast<GLint>(depthStencilTextureMode));
m_cacheDepthStencilTextureMode = depthStencilTextureMode;
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line,
MG_Util::ConvertGLEnumToString(err).c_str());
});
}
}
}
#undef MGB_TEXPARAM_LEVEL_RANGE
#undef MGB_TEXPARAM_FORMAT
#undef MGB_TEXPARAM_SWIZZLE
#undef MGB_TEXPARAM_DS_MODE
#undef MGB_TEXPARAM_BORDER_F
#undef MGB_TEXPARAM_BORDER_I
#undef MGB_TEXPARAM_BORDER_UI
#undef MGB_TEXPARAM_BORDER_FORM
void ActivateTextureUnit(Uint unit) {
if (unit == g_activeTextureUnit) {
return;
}
g_GLESFuncs.glActiveTexture(GL_TEXTURE0 + unit);
g_activeTextureUnit = unit;
}
void UnbindTexture(Uint unit, GLenum target) { // Activates `unit` when an unbind is issued
auto targetN = static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(target));
if (g_boundTexturesCache[unit][targetN] == nullptr) return;
ActivateTextureUnit(unit);
g_GLESFuncs.glBindTexture(target, 0);
g_boundTexturesCache[unit][targetN] = nullptr;
}
Uint g_activeTextureUnit = 0;
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
TwinRegistry<MG_State::GLState::ITextureObject, BackendTextureObject, MG_Pipe::MGPipeKind::Texture> g_backendTextureObjects;
#if MOBILEGL_BUILD_DISAGGREGATED
void WritePendingSharedImageUploads() {
auto& applier = MG_Pipe::MGPipeApplier();
for (SizeT slot = 0; slot < applier.TextureResources.size(); ++slot) {
const auto& record = applier.TextureResources[slot];
if (!record.Live || record.SharedImageId == 0 || record.PendingUploads.empty()) continue;
const MG_Pipe::MGPipeHandle handle{static_cast<Uint32>(slot), record.Gen};
auto* twin = g_backendTextureObjects.FindByHandle(handle);
if (twin == nullptr || !*twin) continue;
(*twin)->SyncMipmapsToBackendByHandle(handle);
}
}
#endif
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
SharedPtr<MG_State::GLState::ITextureObject> storage = textureObject;
// Bounded: GL resolves a view of a view to the original storage, so this takes one step.
for (Uint hop = 0; storage && storage->IsTextureView() && hop < 4; ++hop) {
storage = storage->GetViewStorageOwner();
}
if (!storage) return;
auto* slot = g_backendTextureObjects.Find(storage.get());
auto& backendObj = slot ? *slot : g_backendTextureObjects.GetOrCreate(storage);
if (!backendObj) {
backendObj = MakeShared<BackendTextureObject>();
}
backendObj->NoteDriverSideWrite();
}
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract.
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
// THE RECORD FIRST: a texture handle with no resource record is a seam defect - the
// client named a texture it never created, or one whose resource_destroy has
// already applied while a binding still names it - and a twin adopted for it would
// own a driver texture nothing ever uploads to.
if (PipeTextureRecordForHandle(res) == nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle "
"arm, so no driver texture can be built for it and the unit keeps what "
"it holds",
res.Slot, res.Gen);
return nullptr;
}
auto* slot = AdoptTwinByHandle(g_backendTextureObjects, res, "texture");
if (slot == nullptr) return nullptr;
if (!*slot) *slot = MakeShared<BackendTextureObject>();
return slot->get();
}
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite) {
// The storage owner's handle, one hop per view like the frontend walk above (the
// client composes a view of a view onto its root, so one hop is the real shape).
MG_Pipe::MGPipeHandle storage = res;
for (Uint hop = 0; hop < 4 && !MG_Pipe::MGPipeHandleIsNull(storage); ++hop) {
const auto* record = PipeTextureRecordForHandle(storage);
if (record == nullptr || MG_Pipe::MGPipeHandleIsNull(record->Desc.ViewOf)) break;
storage = record->Desc.ViewOf;
}
if (auto* twin = ResolveTextureTwin(storage)) twin->NoteDriverSideWrite();
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E: the staged store stops standing for this texture's content (a no-op on a
// monolith arm, where the store does not copy).
if (!storeFollowsTheWrite && !MG_Pipe::MGPipeHandleIsNull(storage)) {
MG_Remote::Server::ServerStagedTexture().MarkDriverWritten(
MG_Remote::Server::StagedTextureStore::KeyForHandle(storage));
}
#else
(void)storeFollowsTheWrite;
#endif
}
#if MOBILEGL_BUILD_DISAGGREGATED
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
const IntVec3& destinationOrigin, const IntVec3& size) {
auto& store = MG_Remote::Server::ServerStagedTexture();
// P13 W4b: an aliasing store (monolith's record arm) follows too; see FollowCopy.
if (!store.Holds()) return false;
// The store's level key for an endpoint copy-image addresses as ONE image with its
// layers on z: exactly one upload target (not a cube map, whose faces are six store
// levels) and not a 1D array (the store keeps its layers as rows). A view's own levels
// are byteless, so FollowCopy answers false for it by itself.
const auto storeTarget = [](MG_Pipe::MGPipeHandle handle, Uint16& target) {
const auto* record = PipeTextureRecordForHandle(handle);
if (record == nullptr ||
record->Desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex1DArray)) {
return false;
}
const auto& targets = BufferImpl::StagedUploadTargetsForPipeTarget(record->Desc.Target);
if (targets.size() != 1) return false;
target = static_cast<Uint16>(targets[0]);
return true;
};
Uint16 sourceTarget = 0, destinationTarget = 0;
if (!storeTarget(source, sourceTarget) || !storeTarget(destination, destinationTarget)) return false;
// ONE FORMAT BOTH ENDS. Copy-image moves the GL texel bits; the store holds each
// format's canonical shadow, which for most formats IS those bits and for some is not
// (the 16-bit packed formats are kept as UNorm8, RGB10 / RGB12 as UNorm16). Between two
// levels of one format the canonical move and the GL move agree whatever the format;
// across two compatible formats they need not, so that copy is not followed.
if (PipeTextureRecordForHandle(source)->Desc.InternalFormat !=
PipeTextureRecordForHandle(destination)->Desc.InternalFormat) {
return false;
}
return store.FollowCopy(MG_Remote::Server::StagedTextureStore::KeyForHandle(source), sourceTarget,
static_cast<Uint16>(sourceLevel), sourceOrigin,
MG_Remote::Server::StagedTextureStore::KeyForHandle(destination),
destinationTarget, static_cast<Uint16>(destinationLevel), destinationOrigin,
size);
}
#endif
} // namespace TextureImpl
namespace FramebufferImpl {
BackendFramebufferObject::BackendFramebufferObject() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Identity until a non-identity draw-buffer array forces a relocation. A framebuffer
// that is never draw-bound never runs the recompute, so the table has to start out
// matching what the attachment loop will physically do.
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
}
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendFBOId == 0) {
MGLOG_E_ONCE("Failed to generate framebuffer object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated framebuffer object with ID: %u.", m_backendFBOId);
}
}
BackendFramebufferObject::~BackendFramebufferObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendFBOId == 0) {
return;
}
// Scrub the binding shadow whether or not the id can still be deleted: a
// recycled name must never satisfy the shadow's dedup.
NoteFramebufferIdDeleted(m_backendFBOId);
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) {
g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId);
}
m_backendFBOId = 0;
}
void BackendFramebufferObject::Bind(FramebufferTarget target) const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (target == FramebufferTarget::Read)
BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId);
else
BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
}
namespace {
// Driver-level framebuffer-binding shadow (see Managers.h). Indexed by
// FramebufferTarget {Draw, Read}.
Array<Uint, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindings = {0, 0};
Array<Bool, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindingKnown = {false, false};
} // namespace
void BindFramebufferId(GLenum fbTarget, Uint id) {
const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
const SizeT drawIdx = SizeT(FramebufferTarget::Draw);
const SizeT readIdx = SizeT(FramebufferTarget::Read);
const Bool drawMatches =
!bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id);
const Bool readMatches =
!bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id);
if (drawMatches && readMatches) {
return;
}
g_GLESFuncs.glBindFramebuffer(fbTarget, id);
if (bindsDraw) {
g_driverFBOBindings[drawIdx] = id;
g_driverFBOBindingKnown[drawIdx] = true;
}
if (bindsRead) {
g_driverFBOBindings[readIdx] = id;
g_driverFBOBindingKnown[readIdx] = true;
}
}
Uint CurrentFramebufferBinding(FramebufferTarget target) {
const SizeT idx = SizeT(target);
if (!g_driverFBOBindingKnown[idx]) {
// Cold path: pin the shadow from the driver once (init probes and
// pre-shadow code bind raw but restore what they found).
GLint binding = 0;
g_GLESFuncs.glGetIntegerv(
target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING,
&binding);
g_driverFBOBindings[idx] = static_cast<Uint>(binding);
g_driverFBOBindingKnown[idx] = true;
}
return g_driverFBOBindings[idx];
}
void NoteFramebufferIdDeleted(Uint id) {
if (id == 0) {
return;
}
for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) {
if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) {
g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0
}
}
}
void InvalidateFramebufferBindingCache() {
g_driverFBOBindings = {0, 0};
g_driverFBOBindingKnown = {false, false};
// The per-target "already synced" memo describes work pushed into the ES context
// that is being left or replaced. Both callers (MakeCurrent, DestroyEGLContext)
// mean the context may have been reset under us, so claim nothing is synced:
// a null object never matches a real binding, so SyncCurrentFBO re-pushes.
g_fboSyncedSlotVersions = {0};
g_fboSyncedObjectVersions = {0};
g_fboSyncedObjects = {};
#if MOBILEGL_ESPRYT_FBO_HANDLE_ARM_MEMOS_LINKED
// E's MAJOR-4 / A9: the handle arm's own memos say the same thing about the same
// targets, so they are invalidated HERE rather than at each of the nine call sites -
// six in DirectGLES.cpp and three in SanityTest.cpp, and it was the three that were
// missed. See the declaration in Managers.h for why the call is gated and which line
// each package flips.
InvalidateFramebufferHandleArmMemos();
#endif
}
void BackendFramebufferObject::InvalidateSyncedState() {
std::fill(std::begin(m_frontendDrawBuffers), std::end(m_frontendDrawBuffers),
FramebufferAttachmentType::Unknown);
std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE);
// NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every
// attachment it had, possibly under a non-identity permutation. Declaring the table
// identity here is safe only because every gate that decides which points get
// re-issued is invalidated too - the attachment versions below, and under
// MOBILEGL_PIPE_PUSH the record memos stamped at the end of this function - so the
// next sync re-attaches all non-empty attachments at their identity points AND
// (see SyncToBackend's attachment loop) detaches any colour point whose frontend owner
// is empty. Without that detach a stale image would survive under a point the table now
// claims for a different, empty attachment.
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
}
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
m_backendReadBuffer = GL_NONE;
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
// Every attachment version is invalidated above, so the next walk re-attaches
// everything regardless; stamp the generation so it does not re-arm twice.
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
// The record memos are the gate the PIPE_PUSH arms' attachment walks actually read -
// the handle arm has nothing narrower under its key - so leaving them standing here
// would let a re-sync of the same record skip the re-attach this function exists to
// force. 0 is never a live hash, so a zeroed memo is a guaranteed miss.
m_syncedRecordHashes = {0};
m_syncedAttachmentRecordHash = 0;
}
static Bool SyncAttachmentObject(GLenum glFBOTarget,
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject,
GLenum glBackendAttachment) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
SharedPtr<TextureImpl::BackendTextureObject> backendTextureObject;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on
// a first attach), named debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get())) {
backendTextureObject = *backendTextureSlot;
} else {
auto& newTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
if (!newTextureSlot) {
newTextureSlot = MakeShared<TextureImpl::BackendTextureObject>();
}
backendTextureObject = newTextureSlot;
}
if (!backendTextureObject) {
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
return false;
}
backendTextureObject->SyncMipmapsToBackend(textureObject);
// From here on every draw, clear and blit into this framebuffer can write the
// texture behind the shadow's back.
TextureImpl::NoteDriverSideTextureWrite(textureObject);
if (attachmentObject.IsLayered()) {
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
uploadTarget == TextureUploadTarget::Texture3D ||
uploadTarget == TextureUploadTarget::Texture2DArray ||
uploadTarget == TextureUploadTarget::Texture1DArray ||
uploadTarget == TextureUploadTarget::CubeMapArray ||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
// Single slice/layer of a 3D or array texture: ES has no
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()),
static_cast<GLint>(attachmentObject.GetTextureLayer()));
} else {
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
attachmentObject.GetTextureUploadTarget());
if (glTextureTarget == GL_UNKNOWN_MGL) {
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
}
// glBindTexture rejects cube-face enums (INVALID_ENUM with no
// bind, while Bind() would still record the cube-map cache slot
// as bound): bind via the owning cube target; the attach below
// keeps the face target.
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
}
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on
// a first attach), named debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* backendRenderbufferSlot =
RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *backendRenderbufferSlot;
} else {
auto& newRenderbufferSlot =
RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
if (!newRenderbufferSlot) {
newRenderbufferSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
backendRenderbufferObject = newRenderbufferSlot;
}
backendRenderbufferObject->SyncToBackend(renderbufferObject);
backendRenderbufferObject->Bind();
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
backendRenderbufferObject->GetBackendRenderbufferId());
}
return true;
}
static Bool IsSnormFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA16Snorm:
return true;
default:
return false;
}
}
static Bool IsUnormFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R16:
case TextureInternalFormat::RG16:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGBA16:
return true;
default:
return false;
}
}
static Bool IsSnormFallbackAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsSnormFormat(textureObject->GetFormat()) &&
TextureImpl::ShouldUseCaveatTextureFormat(textureObject->GetFormat(), textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
IsSnormFormat(renderbufferObject->GetInternalFormat()) &&
TextureImpl::ShouldUseCaveatRenderbufferFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
static Bool IsUnormFallbackAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsUnormFormat(textureObject->GetFormat()) &&
TextureImpl::ShouldUseCaveatTextureFormat(textureObject->GetFormat(), textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
IsUnormFormat(renderbufferObject->GetInternalFormat()) &&
TextureImpl::ShouldUseCaveatRenderbufferFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
// The colour attachment glReadPixels/glGetTexImage would read from, or nullptr when the
// read buffer names no colour attachment at all.
static const MG_State::GLState::FramebufferAttachmentObject* GetReadColorAttachment() {
const auto& readFBO =
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return nullptr;
}
const auto readBuffer = readFBO->GetReadBuffer();
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
return nullptr;
}
return &readFBO->GetAttachment(readBuffer);
}
Bool IsAlphaWidenedColorAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(),
textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
TextureImpl::BackendRenderbufferFormatAddsAlpha(renderbufferObject->GetInternalFormat());
}
return false;
}
Uint32 g_alphaWidenedDrawBufferMask = 0;
Uint32 g_integerColorDrawBufferMask = 0;
static Bool IsIntegerColorFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32I:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32I:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return true;
default:
return false;
}
}
static Bool IsIntegerColorAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsIntegerColorFormat(textureObject->GetFormat());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject && IsIntegerColorFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
// THE FOUR CROSS-OBJECT MASKS, ANSWERED FROM THE RECORD (ID-12 DV-5, review M-3 / DV-5).
//
// All four reduce to (internal format, TEXTURE TARGET) for a texture attachment and to
// (internal format) for a renderbuffer one, and MGPSurface now carries both inline -
// InternalFormat since D-C1 and TextureTarget since c0c widened Pad0 into it. So the
// handle arm stops reading the frontend attachment objects to compute them, which is
// what D-C1's "the four cross-object masks fall out at push time with no lookup" meant.
//
// ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha / their renderbuffer
// siblings are UNTOUCHED - two of them are on D-N's byte-identical list. Only what they
// are ASKED changes, which is the whole point of carrying the target rather than
// inventing a TextureUploadTarget -> TextureTarget inverse: feeding a D-N-pinned
// function a guessed input would silently change what every texture is allocated as.
//
// Kind IS the gate (c0c: "consulted only when Kind == kMGPipeSurfaceKindTexture"), and
// kMGPipeSurfaceNoTextureTarget on a texture point is a seam defect - refused loudly and
// answered false, never guessed. False is the safe direction for all four: the mask
// turns an emulation ON, and an emulation that does not run leaves the driver's own
// (correct-for-the-real-format) behaviour, while one that runs on the wrong buffer
// clamps or overwrites texels the application wrote.
static Bool PushedSurfaceTextureTarget(const MG_Pipe::MGPSurface& surface, TextureTarget* out) {
if (surface.TextureTarget == MG_Pipe::kMGPipeSurfaceNoTextureTarget) {
MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} says Kind=texture but carries no "
"texture target - refusing to guess one for the cross-object masks",
surface.Res.Slot, surface.Res.Gen);
return false;
}
*out = static_cast<TextureTarget>(surface.TextureTarget);
return true;
}
static Bool IsSnormFallbackSurface(const MG_Pipe::MGPSurface& surface) {
const auto format = static_cast<TextureInternalFormat>(surface.InternalFormat);
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
TextureTarget target = TextureTarget::Unknown;
return PushedSurfaceTextureTarget(surface, &target) && IsSnormFormat(format) &&
TextureImpl::ShouldUseCaveatTextureFormat(format, target);
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
return IsSnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format);
}
return false;
}
static Bool IsUnormFallbackSurface(const MG_Pipe::MGPSurface& surface) {
const auto format = static_cast<TextureInternalFormat>(surface.InternalFormat);
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
TextureTarget target = TextureTarget::Unknown;
return PushedSurfaceTextureTarget(surface, &target) && IsUnormFormat(format) &&
TextureImpl::ShouldUseCaveatTextureFormat(format, target);
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
return IsUnormFormat(format) && TextureImpl::ShouldUseCaveatRenderbufferFormat(format);
}
return false;
}
static Bool IsAlphaWidenedColorSurface(const MG_Pipe::MGPSurface& surface) {
const auto format = static_cast<TextureInternalFormat>(surface.InternalFormat);
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
TextureTarget target = TextureTarget::Unknown;
return PushedSurfaceTextureTarget(surface, &target) &&
TextureImpl::BackendTextureFormatAddsAlpha(format, target);
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
return TextureImpl::BackendRenderbufferFormatAddsAlpha(format);
}
return false;
}
static Bool IsIntegerColorSurface(const MG_Pipe::MGPSurface& surface) {
// No target and no caveat table: integerness is a property of the format alone, so
// this one is the same question on both arms.
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
return false;
}
return IsIntegerColorFormat(static_cast<TextureInternalFormat>(surface.InternalFormat));
}
// The record's surface for an attachment POINT, or null when this record does not
// describe that point at all. MGPFramebufferState carries Color[8] + Depth + Stencil
// (D-C1), which is every point a framebuffer can hold on the handle arm: D-C3 refuses
// bit 9 outright on a driver reporting more than 8 colour attachments, and the
// FRONT/BACK points belong to the DEFAULT framebuffer, which has no twin at all.
const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record,
FramebufferAttachmentType point) {
if (point == FramebufferAttachmentType::Depth) return &record.Depth;
if (point == FramebufferAttachmentType::Stencil) return &record.Stencil;
if (point < FramebufferAttachmentType::Color0 || point > FramebufferAttachmentType::Color31) {
return nullptr;
}
const Int index = static_cast<Int>(point) - static_cast<Int>(FramebufferAttachmentType::Color0);
if (index >= static_cast<Int>(MG_Pipe::kMGPipeMaxColorAttachments)) return nullptr;
return &record.Color[static_cast<SizeT>(index)];
}
// SyncAttachmentObject's handle arm (review M-3). WHAT MOVES IS THE RESOLUTION: the twin
// is adopted from MGPSurface::Res through the slot table instead of being looked up by
// the frontend object's ADDRESS, and the attach SHAPE - layered, level, layer, the
// upload target and the texture target - is read off the surface instead of off the
// frontend attachment object.
//
// P5e (fb, CONTRACT-P5E.md §5.4): THE FRONTEND ATTACHMENT PARAMETER IS GONE. It was
// here for two things and P5e retires both. The storage syncs now have by-handle forms
// (SyncMipmapsToBackendByHandle / SyncToBackendByHandle), so the object is no longer
// needed to reach the texels; and the record is now the ONLY statement of what is
// attached, so the cross-checks had nothing left to corroborate against - a second
// answer that only a monolith build could produce is not a check, it is a second
// writer. With the object gone, review N-6's hole ("record empty, frontend still
// holding") is UNREPRESENTABLE rather than refused: the caller's detach and this
// function's attach now read the SAME test, `Kind == None || Res null`.
//
// The two P5c cross-checks that ran HandleOf on the apply thread go with it. The
// renderbuffer one (the one that was NOT transport-gated, unlike its texture sibling)
// is deleted by the rekey rather than gated, which is what CONTRACT-P5E §4.4 asks for:
// gating it would have left a client-allocator probe compiled into a path a run-ahead
// apply reaches, and there is no longer anything for it to say.
//
// P5e (fix1, ID-81 / §5.8): THE STORAGE SYNC IS ARM-SELECTED AND THE MONOLITH ARM KEEPS
// ITS FRONTEND OBJECT. This function has TWO callers and they are on opposite arms: the
// handle form (SyncToBackendByHandle) has no frontend object and must not acquire one,
// while the OBJECT form (SyncToBackend, the push-monolith arm - P4a drives its
// attachments from the record too, gated on FramebufferSubsystemEnabled() alone, D-C2)
// is holding the frontend attachment the whole time. fb dropped the parameter for both,
// which routed the monolith arm into tx2's by-handle storage seam; that seam passes a
// NULL SharedPtr and relies on the record arm being selected, and under
// Transport == Monolith it is not - `stateTextureObject->IsTextureView()` then
// dereferenced null (the device's Lightmap.<init> -> clearColorTexture -> glClear
// SIGSEGV). So the parameter comes back as MONOLITH GLUE, used for nothing but the
// storage sync: the attach SHAPE, the empty-point test and the twin adoption stay the
// record's on both arms, which is what keeps N-6's hole closed and the cross-checks
// deleted. It is a pointer and it is null on the handle arm, so "which arm am I on" is
// never inferred from it - MG_Config::Transport decides, as §5.8 requires, and a
// monolith arm that somehow arrives without one refuses by name instead of attaching a
// texture whose levels were never pushed.
static Bool SyncAttachmentSurface(GLenum glFBOTarget, const MG_Pipe::MGPSurface& surface,
GLenum glBackendAttachment,
const MG_State::GLState::FramebufferAttachmentObject* monolithAttachment) {
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(surface.Res)) {
// An empty point: the caller detached it on exactly this test, so attaching
// nothing here is the whole of the agreement. (The legacy arm does the same.)
return true;
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
// The record's handle is validated BEFORE the frontend is consulted (N-3): it is
// the untrusted half, the frontend cross-check below is the corroboration, and
// asking in this order is what gives the two adoption refusals a reachable
// caller at all.
if (!PipeTwinHandleIsAdoptable(TextureImpl::g_backendTextureObjects, surface.Res, "texture")) {
return false;
}
auto* twinSlot = AdoptTwinByHandle(TextureImpl::g_backendTextureObjects, surface.Res, "texture");
if (twinSlot == nullptr) {
return false; // AdoptTwinByHandle named the refusal
}
if (!*twinSlot) {
*twinSlot = MakeShared<TextureImpl::BackendTextureObject>();
}
// COPIED OUT of the table: SyncMipmapsToBackend can grow it (a re-mint adopts
// another handle), and Managers.h:381-385's pointer-invalidation warning applies
// to exactly this sequence.
SharedPtr<TextureImpl::BackendTextureObject> backendTextureObject = *twinSlot;
if (!backendTextureObject) {
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
return false;
}
// P5e (fb): the storage sync BY HANDLE (tx2's seam). The level shadow it reads
// is the applier's staged texture store, not a frontend object, which is what
// let the object parameter go - UNDER A TRANSPORT. P5e (fix1, ID-81): the
// push-monolith arm keeps the frontend sync it had at f6cfcbd3, token for token,
// because tx2's record arm inside SyncMipmapsToBackend is itself selected by
// `Transport != Monolith` and cannot answer here.
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord()) {
backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res);
// From here on every draw, clear and blit into this framebuffer can write the
// texture behind the staged shadow's back (the frontend arm's note, by handle).
TextureImpl::NoteDriverSideTextureWriteByHandle(surface.Res);
} else
#endif
{
const SharedPtr<MG_State::GLState::ITextureObject> monolithTexture =
(monolithAttachment != nullptr && monolithAttachment->IsTexture())
? monolithAttachment->GetTexture()
: nullptr;
if (!monolithTexture) {
MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} on the "
"push-monolith arm, where the storage sync needs the frontend "
"texture and this caller supplied none - refusing to attach a "
"texture whose levels were never pushed",
surface.Res.Slot, surface.Res.Gen);
return false;
}
backendTextureObject->SyncMipmapsToBackend(monolithTexture);
// From here on every draw, clear and blit into this framebuffer can write the
// texture behind the shadow's back.
TextureImpl::NoteDriverSideTextureWrite(monolithTexture);
}
const auto uploadTarget = static_cast<TextureUploadTarget>(surface.UploadTarget);
if (surface.Layered != 0) {
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(surface.Level));
} else if (uploadTarget == TextureUploadTarget::Texture3D ||
uploadTarget == TextureUploadTarget::Texture2DArray ||
uploadTarget == TextureUploadTarget::Texture1DArray ||
uploadTarget == TextureUploadTarget::CubeMapArray ||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(surface.Level),
static_cast<GLint>(surface.Layer));
} else {
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
if (glTextureTarget == GL_UNKNOWN_MGL) {
TextureTarget target = TextureTarget::Unknown;
if (!PushedSurfaceTextureTarget(surface, &target)) {
return false;
}
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
}
// Same cube-face rule as the legacy arm: glBindTexture rejects the face
// enums, so bind through the owning cube target and attach with the face.
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(surface.Level));
}
return true;
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer) {
// Same order as the texture arm above, for the same reason (N-3).
if (!PipeTwinHandleIsAdoptable(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res,
"renderbuffer")) {
return false;
}
auto* twinSlot = AdoptTwinByHandle(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res,
"renderbuffer");
if (twinSlot == nullptr) {
return false;
}
if (!*twinSlot) {
*twinSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject = *twinSlot;
if (!backendRenderbufferObject) {
MGLOG_E_ONCE("%s: No backend renderbuffer found for FBO attachment.", __func__);
return false;
}
// P5e (fb): the four allocation values were already record-supplied (D-D2); what
// moves here is the LOOKUP - the handle the surface carries, never HandleOf on
// an object the apply thread was handed.
//
// P5e (fix1, ID-81): arm-selected for the texture arm's reason, and here it is a
// BEHAVIOUR difference even though the by-handle body dereferences nothing: the
// object form reports a refused allocation to the application through the live
// GLContext (RecordError) and the by-handle form deliberately does not, because
// under a transport there is no application on this side to report it to. On the
// push-monolith arm there is, so the monolith arm keeps the object form.
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord()) {
backendRenderbufferObject->SyncToBackendByHandle(surface.Res);
} else
#endif
{
const SharedPtr<MG_State::GLState::RenderbufferObject> monolithRenderbuffer =
(monolithAttachment != nullptr && monolithAttachment->IsRenderbuffer())
? monolithAttachment->GetRenderbuffer()
: nullptr;
if (!monolithRenderbuffer) {
MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} on the "
"push-monolith arm, where the storage allocation needs the "
"frontend renderbuffer and this caller supplied none - refusing "
"to attach a renderbuffer with no storage",
surface.Res.Slot, surface.Res.Gen);
return false;
}
backendRenderbufferObject->SyncToBackend(monolithRenderbuffer);
}
backendRenderbufferObject->Bind();
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
backendRenderbufferObject->GetBackendRenderbufferId());
return true;
}
MGLOG_E_ONCE("MGPipe: attachment surface {%u, %u} carries Kind=%u, which is neither a texture "
"nor a renderbuffer nor an empty point - refusing to attach",
surface.Res.Slot, surface.Res.Gen, surface.Kind);
return false;
}
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo) {
using FBO = MG_State::GLState::FramebufferObject;
const auto& drawBuffers = fbo.GetDrawBuffers();
Uint32 mask = 0;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = drawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
if (IsAlphaWidenedColorAttachment(fbo.GetAttachment(frontendBuf))) {
mask |= (1u << i);
}
}
return mask;
}
// The read attachment's storage carries an alpha channel its frontend format does not
// (the three-channel colour-renderable widening). GL answers such a read with 1.0, but
// the storage holds whatever the draw wrote there, so the readback has to overwrite it.
Bool IsAlphaWidenedFallbackReadAttachment() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord()) {
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
return record && IsAlphaWidenedColorSurface(record->ReadSurface);
}
#endif
const auto* attachmentObject = GetReadColorAttachment();
if (attachmentObject == nullptr) {
return false;
}
return IsAlphaWidenedColorAttachment(*attachmentObject);
}
Bool IsFixedPointFallbackReadAttachment() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::DataArmIsRecord()) {
const auto* record = MG_Pipe::MGPipeApplier().ReadFramebuffer();
return record && record->ReadSurface.Kind != MG_Pipe::kMGPipeSurfaceKindNone &&
IsSnormFormat(static_cast<TextureInternalFormat>(record->ReadSurface.InternalFormat));
}
#endif
const auto& readFBO =
MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return false;
}
const auto readBuffer = readFBO->GetReadBuffer();
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
return false;
}
// Any signed-normalized attachment, not just the ones currently substituted:
// ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer
// hands back the negative half that desktop GL clamps away.
const auto& attachmentObject = readFBO->GetAttachment(readBuffer);
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsSnormFormat(textureObject->GetFormat());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
const MG_Pipe::MGPFramebufferState* PushedFramebufferRecord(MG_Pipe::MGPipeHandle fbo) {
// FramebufferRecordFor answers null for the null handle and for a slot nothing has
// described (both silent - that is every framebuffer before its first emission), and
// refuses a stale generation loudly while counting it in
// StaleFramebufferRecordLookups. So there is no second emptiness test here to get out
// of step with the applier's, and none of the three cases is a binding question.
return MG_Pipe::MGPipeApplier().FramebufferRecordFor(fbo);
}
Bool PushedFramebufferIsBoundTo(FramebufferTarget target, MG_Pipe::MGPipeHandle fbo) {
if (MG_Pipe::MGPipeHandleIsNull(fbo)) return false;
const auto binding = target == FramebufferTarget::Read ? MG_Pipe::MGPipeFramebufferTarget::Read
: MG_Pipe::MGPipeFramebufferTarget::Draw;
return MG_Pipe::MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(binding)] == fbo;
}
// The record's DrawBuffers[] is an ATTACHMENT INDEX with -1 for None (D-C1), which is
// what the frontend array holds for every framebuffer that has a twin. It deliberately
// cannot spell FrontLeft / FrontRight / BackLeft / BackRight: those are the DEFAULT
// framebuffer's own tokens, the default framebuffer has no BackendFramebufferObject at
// all (it is pDefaultFramebufferInfo->defaultFBO, which is why MGPipeHandles.h reserves
// {0,1} for it), and the legacy arm's branch for them says so in as many words - "shouldn't
// remap". So the conversion below is total for every object that reaches this twin, and
// the record's IsDefault is the thing a later phase would test if that ever changed.
static void DecodePushedDrawBuffers(const MG_Pipe::MGPFramebufferState& record,
FramebufferAttachmentType* out) {
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
const Int8 index = record.DrawBuffers[i];
out[i] = index < 0 ? FramebufferAttachmentType::None
: static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + index);
}
}
// P5e (fb, §5.4): the record half of SyncReadBufferToBackend, lifted out of it so the
// by-handle sync can run it WITHOUT a frontend object. Nothing inside reads one: the
// frontend index was only ever a diagnostic, and it is passed as a number now
// (GlNameForDiag is 0 on the handle arm, which reads as "the record did not say").
void BackendFramebufferObject::ApplyReadBufferFromRecord(const MG_Pipe::MGPFramebufferState& record,
Uint glNameForDiag) {
// D-C1 makes the NULL Res the spelling of "no attachment" and c0c's
// kMGPipeSurfaceKindNone makes Kind agree with it on a zero-initialised record.
// Res stays the test - it is the one D-C1 fixes - and Kind is CROSS-CHECKED
// rather than consulted: the two are one statement the emitter has to make
// twice, and a record where they disagree is a seam defect, not an empty point.
// Picking a winner silently is what this refuses to do.
const Bool readSurfaceEmpty = MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res);
if (readSurfaceEmpty != (record.ReadSurface.Kind == MG_Pipe::kMGPipeSurfaceKindNone)) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface says Res={%u, %u} and "
"Kind=%u, which disagree about whether it names anything - refusing "
"rather than choosing one of them",
glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen,
record.ReadSurface.Kind);
return;
}
FramebufferAttachmentType pushedReadBuf = FramebufferAttachmentType::None;
if (!readSurfaceEmpty) {
// EVERY FIELD THAT IDENTIFIES THE IMAGE, not the three v1 compared (m-4).
// The same image legally sits at two colour points - a layered attachment of
// one array and a single slice of it are different surfaces of one Res - so a
// partial comparison resolves to the lower index and the refusal branch below
// ("refusing to guess") would then be doing part of the guessing itself.
// InternalFormat and TextureTarget are properties of the RESOURCE rather than
// of the point, so they add nothing to the identity and are left out.
Bool matched = false;
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const auto& color = record.Color[i];
if (color.Res == record.ReadSurface.Res && color.Kind == record.ReadSurface.Kind &&
color.Layered == record.ReadSurface.Layered &&
color.Level == record.ReadSurface.Level && color.Layer == record.ReadSurface.Layer &&
color.UploadTarget == record.ReadSurface.UploadTarget) {
pushedReadBuf = static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
matched = true;
break;
}
}
if (!matched) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface {%u, %u} matches no "
"colour attachment of its own record - refusing to guess a read "
"buffer",
glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen);
return;
}
}
if (pushedReadBuf == m_frontendReadBuffer) {
return;
}
m_frontendReadBuffer = pushedReadBuf;
const GLenum glPushedReadBuffer = GetBackendAttachmentType(pushedReadBuf);
if (m_backendReadBuffer != glPushedReadBuffer) {
m_backendReadBuffer = glPushedReadBuffer;
// Still bound as READ first, for the reason the legacy arm gives below:
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER.
Bind(FramebufferTarget::Read);
g_GLESFuncs.glReadBuffer(glPushedReadBuffer);
}
}
// P5e (fb): the by-handle entry, for SyncCurrentFBOByRecord's "one object on both
// bindings" skip - the one path that applies a read buffer without doing the rest of
// the sync. A framebuffer with no record is the caller's seam defect, not a silent
// no-op, so it says so.
void BackendFramebufferObject::SyncReadBufferToBackendByHandle(MG_Pipe::MGPipeHandle fbo) {
const auto* record = PushedFramebufferRecord(fbo);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so its read "
"buffer cannot be pushed",
fbo.Slot, fbo.Gen);
return;
}
ApplyReadBufferFromRecord(*record, 0);
}
void BackendFramebufferObject::SyncReadBufferToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
if (!stateFBOObject) {
return;
}
// P4a (D-C2): the READ buffer is answered from the RESOLVED read surface of the READ
// framebuffer's own record, which is what structurally closes the read-buffer
// shared-FBO defect class. The comment two lines below - "when this is reached from
// SyncCurrentFBO's 'same FBO as draw' skip path" - describes a hazard that becomes
// unrepresentable: the record says which target it is, and ReadSurface was resolved
// from the read framebuffer's own read buffer before it ever crossed.
//
// The record carries the resolved SURFACE and not an index, so the attachment POINT
// is recovered by matching it against Color[] - the one place both spellings sit
// side by side. An empty surface (Res == the null handle, which D-C1 makes the
// spelling of "no attachment" independently of Kind's numbering) is
// FramebufferAttachmentType::None, i.e. GL_NONE. A surface that matches no colour
// point cannot be a legal read buffer and is refused rather than guessed.
//
// P5e (fb): THAT DECISION now lives in ApplyReadBufferFromRecord, which takes no
// frontend object; what is left in this overload is the monolith glue that finds
// the handle by identity. Under an active transport nothing reaches it -
// SyncToBackendByHandle runs the record form directly.
if (FramebufferSubsystemEnabled()) {
// P5c (hd): with an active transport the handle is the caller's
// (m_pushedSyncHandle), never the client allocator's - see SyncToBackend.
const MG_Pipe::MGPipeHandle fbo =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::DataArmIsRecord()
? m_pushedSyncHandle
:
#endif
g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
// ID-19: the OBJECT's record, not "the record of whatever is bound to READ". A
// framebuffer whose read buffer is being pushed need not be the read binding at
// all - glNamedFramebufferReadBuffer and the DSA clears reach here by name - and
// ReadSurface is resolved from THIS framebuffer's own read buffer under every
// Target, Named included (c0e's MGPFramebufferState comment).
const auto* record = PushedFramebufferRecord(fbo);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so "
"its read buffer cannot be pushed (handle {%u, %u})",
stateFBOObject->GetExternalIndex(), fbo.Slot, fbo.Gen);
return;
}
ApplyReadBufferFromRecord(*record, stateFBOObject->GetExternalIndex());
return;
}
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
if (frontendReadBuf == m_frontendReadBuffer) {
return;
}
m_frontendReadBuffer = frontendReadBuf;
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
if (m_backendReadBuffer != glBackendReadBuffer) {
m_backendReadBuffer = glBackendReadBuffer;
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER. When this is
// reached from SyncCurrentFBO's "same FBO as draw" skip path the backend FBO was
// only bound as DRAW, so bind it as READ first to route the read buffer correctly.
Bind(FramebufferTarget::Read);
g_GLESFuncs.glReadBuffer(glBackendReadBuffer);
}
}
Bool BackendFramebufferObject::RecomputeBackendColorSlots(
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) {
// Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own
// limit (ValidateColorAttachmentInRange, which reads the clamped
// GetDynamicParameters().MaxColorAttachments) is never larger than this raw ES cap, so an
// index the frontend accepted is always < slotCount. Indices at or above it can never own
// an image and stay on their identity point - never touched, never a GL error.
const Uint slotCount =
std::min<Uint>(MAX_COLOR_ATTACHMENT_SLOTS,
static_cast<Uint>(std::max<Int>(g_GLESCapabilities.MaxColorAttachments, 1)));
GLenum newSlots[MAX_COLOR_ATTACHMENT_SLOTS];
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
newSlots[i] = GL_COLOR_ATTACHMENT0 + i;
}
Bool assigned[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
Bool slotTaken[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
// 1. ES pins draw-buffer slot s to GL_COLOR_ATTACHMENTs, so an attachment named by draw
// buffer slot s has no choice: its image must sit at backend point s. This has to
// agree with the compaction the caller just pushed through glDrawBuffers.
for (Uint s = 0; s < FramebufferObject::MAX_DRAW_BUFFERS && s < slotCount; ++s) {
const auto frontendBuf = stateDrawBuffers[s];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue; // GL_NONE, or a default-framebuffer FRONT/BACK token: never relocated.
}
const Uint a =
static_cast<Uint>(frontendBuf) - static_cast<Uint>(FramebufferAttachmentType::Color0);
// Neither guard may ever fire: a duplicate draw buffer is already INVALID_OPERATION
// and an out-of-range one is rejected by ValidateColorAttachmentInRange. If one did
// fire the table would disagree with the glDrawBuffers the caller already issued,
// which is the exact non-injectivity this table exists to remove.
MOBILEGL_ASSERT(a < slotCount && !assigned[a],
"Draw buffer %u names colour attachment %u which is out of range or duplicated.", s,
a);
if (a >= slotCount || assigned[a]) {
continue;
}
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
assigned[a] = true;
slotTaken[s] = true;
}
// 2. Everything else keeps its identity point when that point survived step 1. This is
// what makes the ordinary drawBuffers[s] == COLOR_ATTACHMENTs case a strict no-op:
// the table stays identity, nothing moves, no attachment is re-issued.
for (Uint a = 0; a < slotCount; ++a) {
if (assigned[a] || slotTaken[a]) {
continue;
}
newSlots[a] = GL_COLOR_ATTACHMENT0 + a;
assigned[a] = true;
slotTaken[a] = true;
}
// 3. What is left are attachments whose identity point step 1 took away. Park them on the
// lowest free point. They are not draw buffers, so nothing is rendered through them;
// they only have to stay addressable for glReadBuffer and blits, and the map has to
// stay injective so reading one of them cannot land on another's image.
for (Uint a = 0; a < slotCount; ++a) {
if (assigned[a]) {
continue;
}
for (Uint s = 0; s < slotCount; ++s) {
if (!slotTaken[s]) {
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
assigned[a] = true;
slotTaken[s] = true;
break;
}
}
}
Bool moved = false;
for (Uint a = 0; a < MAX_COLOR_ATTACHMENT_SLOTS; ++a) {
if (m_backendColorSlots[a] == newSlots[a]) {
continue;
}
m_backendColorSlots[a] = newSlots[a];
moved = true;
// This attachment's image now belongs at a different backend point. Its frontend
// version has not changed, so the attachment loop would skip it; force it.
m_syncedFrontendAttachmentVersions[static_cast<SizeT>(FramebufferAttachmentType::Color0) + a] =
static_cast<Uint16>(~0u);
}
return moved;
}
void BackendFramebufferObject::SyncToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateFBOObject) {
MGLOG_E_ONCE("State FBO object is null, cannot sync to backend.");
return;
}
MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId,
stateFBOObject->GetExternalIndex(), (asTarget == FramebufferTarget::Draw ? "DRAW" : "READ"));
GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
// P4a (D-C2 as corrected by ID-19): THE RECORD OF THIS FRAMEBUFFER OBJECT, resolved
// BEFORE the driver framebuffer is bound.
//
// The order is the whole of C-1's fix. v1 bound first and refused afterwards, so a
// glClearNamedFramebufferfv / glBlitNamedFramebuffer on a framebuffer that is bound
// to neither target left this twin's freshly minted driver FBO bound with NO
// ATTACHMENTS and the caller then issued the clear or the blit against it -
// GL_INVALID_FRAMEBUFFER_OPERATION and nothing cleared, where the legacy arm cleared
// correctly. What the reorder buys is exactly this and no more: THE REFUSAL ITSELF
// HAS NO DRIVER EFFECT - it no longer mints and binds a driver FBO as a side effect
// of declining. It does NOT decide the driver's binding (review N-1): every caller
// binds immediately afterwards and none of them looks at a return value -
// SyncAndBindFramebufferObject is SyncToBackend then an unconditional Bind(target)
// (DirectGLES.cpp:3292-3293, the path all five DSA entry points take), and
// BindCurrentFBO binds the current FBO's twin whether or not SyncCurrentFBO synced
// it. So an unconfigured framebuffer can still end up bound; what the log line
// promises is only that this function did not put it there.
//
// The record is per FRAMEBUFFER OBJECT and is found by handle, so it describes this
// object whether it is bound to Draw, to Read, to both or to neither, and its own
// Target is NOT compared against `asTarget`: a Named record legitimately names no
// binding at all. What is still gated on `asTarget` is what reaches the DRIVER's
// bound target - glDrawBuffers and the four cross-object masks below, which stay
// Draw-only for the reason the OIT comment gives.
const MG_Pipe::MGPFramebufferState* pushedRecord = nullptr;
FramebufferObject::FramebufferAttachmentArray pushedDrawBuffers{};
if (FramebufferSubsystemEnabled()) {
// MONOLITH GLUE, named as such: the Framebuffer handle of an object this backend
// still arrives holding. A framebuffer has a handle but NO wire lifetime (D-I2) -
// there is no framebuffer create or destroy in the catalogue and none is invented
// - so the handle exists purely to key set_framebuffer_state, which is exactly
// what it is used for here.
//
// P5c (hd): with an active transport the handle is the one the caller is applying
// (m_pushedSyncHandle - the record-driven sync set it), and the client allocator
// is never probed (T2). A null there means a caller reached this arm without a
// record, which the null-record refusal below names.
const MG_Pipe::MGPipeHandle fbo =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::DataArmIsRecord()
? m_pushedSyncHandle
:
#endif
g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
pushedRecord = PushedFramebufferRecord(fbo);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so it "
"is not configured here; nothing is bound by this refusal, but the "
"caller's own bind still targets it (handle {%u, %u})",
stateFBOObject->GetExternalIndex(), fbo.Slot, fbo.Gen);
return;
}
DecodePushedDrawBuffers(*pushedRecord, pushedDrawBuffers.data());
}
Bind(asTarget);
// -------------------- Connect attachments (set buffers) -----------------------
// 1. Remap draw buffers
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers =
pushedRecord != nullptr ? pushedDrawBuffers : stateFBOObject->GetDrawBuffers();
Bool drawBufferClean = false;
if (memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0) {
drawBufferClean = true;
}
// glDrawBuffers writes the state of the FBO bound to GL_DRAW_FRAMEBUFFER.
// When this object is only bound as the READ target the call would land on
// whatever framebuffer is draw-bound AND falsely stamp this object's memo,
// so the later draw-target sync skips as "clean" while the real state is
// stale (Minecraft 26.x OIT: the scratch clear-FBO kept draw buffers NONE
// from its blit-destination configuration, silently dropping every
// offscreen color clear).
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
int nEffectiveBuffers = 0;
for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
auto& frontendBuf = stateDrawBuffers[i];
if (frontendBuf == FramebufferAttachmentType::None) {
m_backendDrawBuffers[i] = GL_NONE;
continue;
}
// Create compacted mapping
if (frontendBuf == FramebufferAttachmentType::FrontLeft ||
frontendBuf == FramebufferAttachmentType::FrontRight ||
frontendBuf == FramebufferAttachmentType::BackLeft ||
frontendBuf == FramebufferAttachmentType::BackRight) {
MGLOG_D("%s: frontend buf token found for default fbo, shouldn't remap", __func__);
m_backendDrawBuffers[i] = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendBuf);
} else {
m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i;
}
nEffectiveBuffers = i + 1;
}
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
// The line above pinned backend point s to draw-buffer slot s, so the images have to
// be moved under those points. Rebuild the whole colour map and, when anything moved,
// also drop the read-buffer memo: SyncReadBufferToBackend keys it on the frontend
// enum alone, which does not change when the point under it does.
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
}
MGLOG_D("DBAPPLY beFbo=%u target=%d n=%d db0=0x%x feDb0=%d", m_backendFBOId, (int)asTarget,
nEffectiveBuffers, m_backendDrawBuffers[0], (int)stateDrawBuffers[0]);
}
if (asTarget == FramebufferTarget::Draw) {
Uint32 snormClampOutputMask = 0;
Uint32 unormClampOutputMask = 0;
Uint32 alphaWidenedMask = 0;
Uint32 integerColorMask = 0;
for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
// ID-12 DV-5 / M-3: on the handle arm the four masks are answered from the
// record's own surface for the point this draw buffer names, and the frontend
// attachment object below is never touched - the whole arm returns here.
// A draw buffer that names a point the record cannot describe is D-C3's
// refusal arriving too late to refuse, so it is loud and contributes no bit.
if (pushedRecord != nullptr) {
const MG_Pipe::MGPSurface* pushedSurface =
PushedSurfaceForAttachment(*pushedRecord, frontendBuf);
if (pushedSurface == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's draw buffer %u names colour point %d, "
"which its record does not describe (the record carries %u "
"colour points) - contributing no fallback mask bit",
stateFBOObject->GetExternalIndex(), i,
static_cast<Int>(frontendBuf) -
static_cast<Int>(FramebufferAttachmentType::Color0),
MG_Pipe::kMGPipeMaxColorAttachments);
continue;
}
if (IsSnormFallbackSurface(*pushedSurface)) {
snormClampOutputMask |= (1u << i);
} else if (IsUnormFallbackSurface(*pushedSurface)) {
unormClampOutputMask |= (1u << i);
}
if (IsAlphaWidenedColorSurface(*pushedSurface)) {
alphaWidenedMask |= (1u << i);
}
if (IsIntegerColorSurface(*pushedSurface)) {
integerColorMask |= (1u << i);
}
continue;
}
const auto& attachmentObject = stateFBOObject->GetAttachment(frontendBuf);
if (IsSnormFallbackAttachment(attachmentObject)) {
snormClampOutputMask |= (1u << i);
} else if (IsUnormFallbackAttachment(attachmentObject)) {
unormClampOutputMask |= (1u << i);
}
// Independent of the two above: a widened attachment can be SNORM
// (GL_RGB8_SNORM -> GL_RGBA16F, which also clamps) or not (GL_SRGB8 ->
// GL_SRGB8_ALPHA8, which does not), so it gets its own bit rather than an
// `else if` branch of theirs.
if (IsAlphaWidenedColorAttachment(attachmentObject)) {
alphaWidenedMask |= (1u << i);
}
if (IsIntegerColorAttachment(attachmentObject)) {
integerColorMask |= (1u << i);
}
}
PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask;
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
g_integerColorDrawBufferMask = integerColorMask;
}
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
// only apply (and stamp the memo) when this object is bound as READ.
if (asTarget == FramebufferTarget::Read) {
SyncReadBufferToBackend(stateFBOObject);
}
// -------------------- Attach texture to backend FBO -----------------------
// A backend texture id was re-minted since this twin's last walk
// (RecreateBackendTexture): any point here may still hold the dead id while
// its frontend attachment version is unchanged, so the memo below would skip
// exactly the attachment that needs repair. Re-arm every point first.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
// P4a (D-C4): the record's ContentHash is what says this framebuffer's resolved state
// moved, and it REPLACES the frontend attachment versions AS A KEY. It covers every
// field the record carries, so an attachment set that moved, a draw-buffer array
// that moved, an extent that moved and a recycled Fbo whose successor happens to
// carry an identical attachment set are all one compare - and it moves in ONE place
// rather than in an array of 41 the twin had to walk.
//
// The re-arm is expressed through m_syncedFrontendAttachmentVersions rather than
// around it, so the attachment loop below is unchanged on both arms: a moved record
// arms every point and the loop then does exactly what it does today, including the
// empty-colour-point detach that keeps m_backendColorSlots a permutation of the
// PHYSICAL layout.
//
// OVER-FIRING IS FREE AND UNDER-FIRING IS FATAL, so the per-attachment versions stay
// as a second, narrower gate underneath: a frontend attachment that moved without
// the record moving still re-attaches. That direction is the safe one and it is the
// reason the array is re-armed rather than retired here. It is what the array is FOR
// now: the walk below resolves each attachment from the record (SyncAttachmentSurface),
// so the versions no longer say WHICH object a point holds - they say only "this point
// may have moved since I last looked", and they retire with the frontend attachment
// array itself, which is a later phase's.
if (pushedRecord != nullptr && m_syncedRecordHashes[SizeT(asTarget)] != pushedRecord->ContentHash) {
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
m_syncedRecordHashes[SizeT(asTarget)] = pushedRecord->ContentHash;
}
const auto& attachments = stateFBOObject->GetAllAttachmentObjects();
const auto& attachmentVersions = stateFBOObject->GetAllFramebufferAttachmentVersions();
for (SizeT i = 0; i < attachments.size(); ++i) {
const auto& attachmentObject = attachments[i];
auto frontendType = static_cast<FramebufferAttachmentType>(i);
GLenum glBackendAttachment = GL_NONE;
if (frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31)
glBackendAttachment = GetBackendAttachmentType(frontendType);
else
glBackendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType);
// relevant FRONTEND!!! version should be checked and updated
if (m_syncedFrontendAttachmentVersions[i] != attachmentVersions[i]) {
// SyncAttachmentObject only ever attaches: for an empty frontend attachment it
// returns true and issues nothing, so the point keeps whatever was there. That is
// what makes m_backendColorSlots a permutation of the PHYSICAL layout rather than
// a claim about one - a point handed to an attachment with no image would
// otherwise still hold the previous owner's image and glReadBuffer would return
// it. Bounded by GL_MAX_COLOR_ATTACHMENTS because GL_COLOR_ATTACHMENTn above the
// driver's limit is INVALID_ENUM, and restricted to colour points because
// FRONT_LEFT/BACK_LEFT and co. are not ES attachment points at all.
const Bool isColorPoint =
frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31 &&
(static_cast<Int>(frontendType) - static_cast<Int>(FramebufferAttachmentType::Color0)) <
g_GLESCapabilities.MaxColorAttachments;
// P5e (fb, §5.4): ON THE RECORD ARM THE EMPTY POINT IS THE RECORD'S, and it
// is the SAME test SyncAttachmentSurface attaches on - `Kind == None ||
// Res null`. Asking the frontend here and the record there is what left
// review N-6's one-directional hole; one test closes it by construction.
const Bool pointIsEmpty =
pushedRecord != nullptr
? [&] {
const MG_Pipe::MGPSurface* s =
PushedSurfaceForAttachment(*pushedRecord, frontendType);
return s == nullptr || s->Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(s->Res);
}()
: attachmentObject.IsEmpty();
if (isColorPoint && pointIsEmpty && glBackendAttachment != GL_NONE) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, 0);
}
// M-3: the ATTACHMENT RESOLUTION is the record's on the handle arm. The walk
// itself still runs over the frontend's 41 points, because that is what says
// which points EXIST and it is also what carries the per-attachment version
// memo underneath the record hash; what the record answers is WHICH object
// each point holds and in what shape.
//
// A point the record does not describe (Color8..Color31; the FRONT/BACK
// tokens) is only reachable with a non-empty attachment if D-C3's refusal
// failed to fire, so an empty one is silently fine and a live one is loud.
Bool attachmentSynced;
if (pushedRecord != nullptr) {
const MG_Pipe::MGPSurface* pushedSurface =
PushedSurfaceForAttachment(*pushedRecord, frontendType);
if (pushedSurface == nullptr) {
if (attachmentObject.IsEmpty()) {
attachmentSynced = true;
} else {
MGLOG_E_ONCE("MGPipe: framebuffer %u holds an attachment at point %s, which "
"its record does not describe - refusing to attach it",
stateFBOObject->GetExternalIndex(),
MG_Util::ConvertFramebufferAttachmentTypeToString(frontendType).c_str());
attachmentSynced = false;
}
} else {
attachmentSynced =
SyncAttachmentSurface(glFBOTarget, *pushedSurface, glBackendAttachment,
&attachmentObject);
}
} else {
attachmentSynced = SyncAttachmentObject(glFBOTarget, attachmentObject, glBackendAttachment);
}
if (attachmentSynced) {
m_syncedFrontendAttachmentVersions[i] = attachmentVersions[i];
}
}
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
else {
MGLOG_D("%s: Skipped SyncAttachmentObject(target=%s, frontendObj=(%dx%dx%d, %s), backendAtt=%s), "
"version = %u",
__func__, MG_Util::ConvertGLEnumToString(glFBOTarget).c_str(),
attachmentObject.GetSize().x(), attachmentObject.GetSize().y(),
attachmentObject.GetSize().z(),
MG_Util::ConvertFramebufferAttachmentTypeToString(frontendType).c_str(),
MG_Util::ConvertGLEnumToString(glBackendAttachment).c_str(),
m_syncedFrontendAttachmentVersions[i]);
if (!attachmentObject.IsTexture() && !attachmentObject.IsRenderbuffer()) {
continue;
}
GLint objectType = GL_NONE;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
glFBOTarget, glBackendAttachment, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType);
MOBILEGL_ASSERT((objectType == GL_NONE) ||
(attachmentObject.IsTexture() && objectType == GL_TEXTURE) ||
(attachmentObject.IsRenderbuffer() && objectType == GL_RENDERBUFFER),
"Attachment type not match!");
GLint objectName = 0;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
glFBOTarget, glBackendAttachment, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName);
// Verify that the backend object's name and parameters match the frontend attachment state
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
MOBILEGL_ASSERT(backendTextureSlot != nullptr && *backendTextureSlot != nullptr,
"No backend texture found while framebuffer reports texture attachment.");
GLuint backendTexId = (*backendTextureSlot)->GetBackendTextureId();
MOBILEGL_ASSERT(static_cast<GLint>(backendTexId) == objectName,
"Attachment texture name mismatch between GLES (%d) and backend texture object "
"(%d), frontend texture object ID=%d.",
objectName, backendTexId, textureObject->GetExternalIndex());
GLint texLevel = 0;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
glFBOTarget, glBackendAttachment, GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &texLevel);
MOBILEGL_ASSERT(texLevel == static_cast<GLint>(attachmentObject.GetTextureLevel()),
"Attachment texture level mismatch between GLES and state object.");
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendRboSlot =
RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get());
MOBILEGL_ASSERT(
backendRboSlot != nullptr && *backendRboSlot != nullptr,
"No backend renderbuffer found while framebuffer reports renderbuffer attachment.");
GLuint backendRboId = (*backendRboSlot)->GetBackendRenderbufferId();
MOBILEGL_ASSERT(static_cast<GLint>(backendRboId) == objectName,
"Attachment renderbuffer name mismatch between GLES and state object.");
}
}
#endif
}
// The walk itself can re-mint an id (SyncAttachmentObject ->
// SyncMipmapsToBackend -> RecreateBackendTexture), invalidating points this
// walk already attached or version-skipped - e.g. one texture attached at two
// points. Re-enter until the generation is quiescent: every pass syncs each
// dirty texture clean, so each repeat finds strictly fewer re-mints and the
// common case (no re-mint) never takes a second pass. The head's draw/read-
// buffer syncs are memoized against their own shadows, so a repeat re-walks
// only the attachments.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
SyncToBackend(stateFBOObject, asTarget);
}
}
// ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------
//
// WHY IT EXISTS. ScopedDetachedTextureFramebufferAttachments used to answer "which
// framebuffers currently have this texture attached" by walking every live twin and
// reading each one's FRONTEND attachment array. Under P5e that walk is the last place
// the server holds a frontend framebuffer across records - id re-typed ForEachLive and
// left the StateForHandle read greppable, naming this package as the one that replaces
// it. The records already carry the answer: every applied set_framebuffer_state names
// its eleven surfaces, so the relation is maintained where the record is consumed.
//
// KEYED ON THE TEXTURE SLOT rather than on {slot, gen}. The question a detach asks is
// "does the driver have this texture id hanging off some framebuffer", the points are
// re-derived from the record on every hit, and a stale entry left by a recycled slot
// therefore costs one record walk that matches nothing. The FRAMEBUFFER handles are
// stored whole, so a recycled framebuffer slot never answers for its predecessor
// (FindByHandle refuses a moved Gen).
//
// MAINTAINED AT THE SYNC, NOT AT THE APPLY, and that is the same set rather than a
// smaller one: a framebuffer whose record was never synced has no driver attachment to
// detach, and every path that can have one runs this sync first.
namespace {
UnorderedMap<Uint32, Vector<MG_Pipe::MGPipeHandle>> g_framebuffersByAttachedTextureSlot;
UnorderedMap<Uint32, Vector<Uint32>> g_attachedTextureSlotsByFramebufferSlot;
void ForgetFramebufferTextureAttachmentsLocked(Uint32 fboSlot) {
auto it = g_attachedTextureSlotsByFramebufferSlot.find(fboSlot);
if (it == g_attachedTextureSlotsByFramebufferSlot.end()) return;
for (const Uint32 textureSlot : it->second) {
auto listIt = g_framebuffersByAttachedTextureSlot.find(textureSlot);
if (listIt == g_framebuffersByAttachedTextureSlot.end()) continue;
auto& list = listIt->second;
list.erase(std::remove_if(list.begin(), list.end(),
[&](MG_Pipe::MGPipeHandle h) { return h.Slot == fboSlot; }),
list.end());
if (list.empty()) g_framebuffersByAttachedTextureSlot.erase(listIt);
}
g_attachedTextureSlotsByFramebufferSlot.erase(it);
}
} // namespace
void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo,
const MG_Pipe::MGPFramebufferState& record) {
if (MG_Pipe::MGPipeHandleIsNull(fbo)) return;
ForgetFramebufferTextureAttachmentsLocked(fbo.Slot);
Vector<Uint32> textureSlots;
const auto note = [&](const MG_Pipe::MGPSurface& surface) {
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return;
if (std::find(textureSlots.begin(), textureSlots.end(), surface.Res.Slot) != textureSlots.end()) {
return; // one texture at two points is one entry
}
textureSlots.push_back(surface.Res.Slot);
g_framebuffersByAttachedTextureSlot[surface.Res.Slot].push_back(fbo);
};
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) note(record.Color[i]);
note(record.Depth);
note(record.Stencil);
if (textureSlots.empty()) return; // the common Minecraft frame: renderbuffers only
g_attachedTextureSlotsByFramebufferSlot[fbo.Slot] = std::move(textureSlots);
}
// BY VALUE, and deliberately: the caller re-binds framebuffers while it walks the
// answer, and anything that syncs one re-enters NoteFramebufferTextureAttachments and
// can rehash the map underneath an iterator.
//
// NOTHING PRUNES A DEAD FRAMEBUFFER'S ENTRY and nothing needs to. A stale handle is
// refused by FindByHandle (its Gen has moved), so it contributes nothing; and the
// moment the slot is reused, the successor's first sync clears its predecessor's rows
// here. The index is therefore bounded by the live framebuffer slot count, which is
// the same bound the twin table itself carries.
Vector<MG_Pipe::MGPipeHandle> FramebuffersAttachingTexture(MG_Pipe::MGPipeHandle texture) {
if (MG_Pipe::MGPipeHandleIsNull(texture)) return {};
auto it = g_framebuffersByAttachedTextureSlot.find(texture.Slot);
if (it == g_framebuffersByAttachedTextureSlot.end()) return {};
return it->second;
}
// ---- P5e (fb, §5.4): BackendFramebufferObject::SyncToBackend BY HANDLE ---------------
//
// The record's ELEVEN SURFACES ARE THE POINT SET. The object form walks the frontend's
// 41 attachment points to learn which exist; this one does not have to, because the
// emitter REFUSES a framebuffer holding a point at or above the wire width
// (FramebufferEmit.h's two refusals) and the FRONT/BACK tokens belong to the default
// framebuffer, which has no twin. So Color[0..7] + Depth + Stencil is the whole of it,
// and a framebuffer that could not be described that way never produced a record at all.
//
// THE PER-POINT VERSION MEMO IS GONE and is not replaced. On the object form it is a
// second, narrower gate under the record hash, hedging against "a frontend attachment
// moved without the record moving" - which is unrepresentable once the record is the
// only statement of what is attached (the hash covers every surface, and Fbo is inside
// it, so a recycled handle cannot be suppressed against its predecessor). Keeping it
// would have meant keeping the frontend array that carries it, which is the whole point
// of this arm. The two SERVER-owned gates stay, because no client-side value can answer
// what they answer: g_attachmentBackendIdGeneration ("did I re-mint a driver id") and
// the re-entry it drives.
void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo,
FramebufferTarget asTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPFramebufferState* const recordPtr = PushedFramebufferRecord(fbo);
if (recordPtr == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so it is not "
"configured here; nothing is bound by this refusal, but the caller's own "
"bind still targets it",
fbo.Slot, fbo.Gen);
return;
}
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
if (record.IsDefault != 0) {
// The default framebuffer has no twin (pDefaultFramebufferInfo->defaultFBO is the
// frontend object and MGPipeHandles.h reserves {0,1} for it), so reaching this
// body with one is a caller that skipped its own IsDefault branch.
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} describes the DEFAULT framebuffer, "
"which has no twin to configure",
fbo.Slot, fbo.Gen);
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Kept in step for the monolith-glue overloads that still resolve their record
// through it (SyncReadBufferToBackend's push arm).
m_pushedSyncHandle = fbo;
#endif
const GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
Bind(asTarget);
FramebufferObject::FramebufferAttachmentArray stateDrawBuffers{};
DecodePushedDrawBuffers(record, stateDrawBuffers.data());
// 1. Remap draw buffers. Same rule as the object form: glDrawBuffers writes the
// DRAW-bound framebuffer's state, so a READ-only sync must neither issue it nor
// stamp the memo (the Minecraft 26.x OIT bug the object form names).
Bool attachmentPointsMoved = false;
const Bool drawBufferClean =
memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0;
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
int nEffectiveBuffers = 0;
for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf == FramebufferAttachmentType::None) {
m_backendDrawBuffers[i] = GL_NONE;
continue;
}
// DecodePushedDrawBuffers cannot produce a FRONT/BACK token - those are the
// default framebuffer's own and it has no twin - so the object form's
// "shouldn't remap" branch has no counterpart here.
m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i;
nEffectiveBuffers = i + 1;
}
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
attachmentPointsMoved = true;
}
}
// The four cross-object masks, off the record's own surfaces (ID-12 DV-5).
if (asTarget == FramebufferTarget::Draw) {
Uint32 snormClampOutputMask = 0;
Uint32 unormClampOutputMask = 0;
Uint32 alphaWidenedMask = 0;
Uint32 integerColorMask = 0;
for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
const MG_Pipe::MGPSurface* surface = PushedSurfaceForAttachment(record, frontendBuf);
if (surface == nullptr) continue; // unrepresentable: DecodePushedDrawBuffers' range
if (IsSnormFallbackSurface(*surface)) {
snormClampOutputMask |= (1u << i);
} else if (IsUnormFallbackSurface(*surface)) {
unormClampOutputMask |= (1u << i);
}
if (IsAlphaWidenedColorSurface(*surface)) alphaWidenedMask |= (1u << i);
if (IsIntegerColorSurface(*surface)) integerColorMask |= (1u << i);
}
PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask;
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
g_integerColorDrawBufferMask = integerColorMask;
}
// 2. Remap read buffer, READ-target only, for the reason the object form gives.
if (asTarget == FramebufferTarget::Read) {
ApplyReadBufferFromRecord(record, 0);
}
// 3. The attachments. Three gates, and the key is the object's rather than the
// binding's: idGenerationMoved ("I re-minted a driver id, so a point may hold a
// dead one"), attachmentPointsMoved ("the colour permutation moved, so a point
// whose record did not change still has to be re-issued") and the record hash.
// NOT m_syncedRecordHashes: the per-target array is the object arm's re-arm
// trigger, and the points written below belong to the driver FBO rather than to
// the binding they were walked for - see m_syncedAttachmentRecordHash.
const Bool idGenerationMoved = m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration;
if (idGenerationMoved) {
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
if (idGenerationMoved || attachmentPointsMoved ||
m_syncedAttachmentRecordHash != record.ContentHash) {
const auto applyPoint = [&](FramebufferAttachmentType point,
const MG_Pipe::MGPSurface& surface) {
const Bool isColorPoint = point >= FramebufferAttachmentType::Color0 &&
point <= FramebufferAttachmentType::Color31;
const GLenum glBackendAttachment =
isColorPoint ? GetBackendAttachmentType(point)
: MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(point);
if (glBackendAttachment == GL_NONE || glBackendAttachment == GL_UNKNOWN_MGL) return;
const Bool empty = surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(surface.Res);
// The detach that keeps m_backendColorSlots a permutation of the PHYSICAL
// layout: a colour point whose record says empty must not keep the previous
// owner's image, or glReadBuffer returns it. Bounded by the driver's cap,
// because GL_COLOR_ATTACHMENTn above it is INVALID_ENUM.
if (isColorPoint && empty) {
const Int index =
static_cast<Int>(point) - static_cast<Int>(FramebufferAttachmentType::Color0);
if (index < g_GLESCapabilities.MaxColorAttachments) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment,
GL_RENDERBUFFER, 0);
}
return;
}
// No frontend attachment on this arm, by construction: the record IS the
// point set here (§5.4). Null is what selects nothing - the transport test
// inside is what selects the by-handle storage syncs.
SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment,
/*monolithAttachment=*/nullptr);
};
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
applyPoint(static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i)),
record.Color[i]);
}
applyPoint(FramebufferAttachmentType::Depth, record.Depth);
applyPoint(FramebufferAttachmentType::Stencil, record.Stencil);
m_syncedAttachmentRecordHash = record.ContentHash;
NoteFramebufferTextureAttachments(fbo, record);
}
// The walk itself can re-mint a driver id (the storage sync behind a texture
// surface), invalidating points this walk already attached. Re-enter until the
// generation is quiescent, exactly as the object form does - every pass syncs each
// dirty texture clean, so each repeat finds strictly fewer re-mints.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
SyncToBackendByHandle(fbo, asTarget);
}
}
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
// Only colour attachments are ever relocated; depth/stencil, the default framebuffer's
// FRONT/BACK names and None map straight through.
if (frontendAtt < FramebufferAttachmentType::Color0 || frontendAtt > FramebufferAttachmentType::Color31) {
return MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt);
}
// The table is a permutation of the backend colour points, so this is the one point that
// owns this attachment. Searching the draw-buffer array instead returned the identity
// point for every attachment that was not a draw buffer - which is exactly the point a
// relocated draw buffer had just taken over, so COLOR_ATTACHMENT0 read back the image of
// whatever attachment was last made the draw buffer.
const Uint index = static_cast<Uint>(frontendAtt) - static_cast<Uint>(FramebufferAttachmentType::Color0);
return m_backendColorSlots[index];
}
TwinRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject, MG_Pipe::MGPipeKind::Framebuffer>
g_backendFramebufferObjects;
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSlotVersions = {0};
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
// must re-sync it even when the binding-slot version has not moved.
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions = {0};
Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects = {};
Uint64 g_attachmentBackendIdGeneration = 0;
Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations = {0};
} // namespace FramebufferImpl
namespace ScratchFBOImpl {
namespace {
// ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it
// (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it.
struct ScratchSet {
ScratchFramebuffer temp;
ScratchFramebuffer blitRead;
ScratchFramebuffer blitDraw;
Uint completeTinyFBOId = 0;
Uint completeTinyRBOId = 0;
};
PerNativeContext<ScratchSet> g_scratchSets;
// Detach every point the shadow no longer vouches for. Used when the
// shadow is unknown (context reset, texture id deleted while attached).
void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
fb.colorTex = 0;
fb.colorTarget = 0;
fb.colorLevel = 0;
fb.colorLayer = -1;
fb.depthTex = 0;
fb.depthTarget = 0;
fb.depthLevel = 0;
fb.depthLayer = -1;
fb.depthHasStencil = false;
fb.depthIsStencilOnly = false;
fb.attachmentsKnown = true;
}
void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) {
if (!fb.attachmentsKnown) {
ScrubAllAttachments(fb, fbTarget);
}
}
// The post-attach glGetError probe below must not misread an error some
// earlier operation left queued; drain before attaching (rare path -
// only runs when the attachment actually changes).
void DrainPendingGLErrors() {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
}
}
// Record the color point as detached when the shadow said something was
// there; the actual detach call is the caller's (it may be replaced by
// the new attach directly when the point is being overwritten).
void RecordNoColor(ScratchFramebuffer& fb) {
fb.colorTex = 0;
fb.colorTarget = 0;
fb.colorLevel = 0;
fb.colorLayer = -1;
}
void RecordNoDepth(ScratchFramebuffer& fb) {
fb.depthTex = 0;
fb.depthTarget = 0;
fb.depthLevel = 0;
fb.depthLayer = -1;
fb.depthHasStencil = false;
fb.depthIsStencilOnly = false;
}
} // namespace
ScratchFramebuffer& TempFramebuffer() {
return g_scratchSets.Current().temp;
}
ScratchFramebuffer& BlitReadFramebuffer() {
return g_scratchSets.Current().blitRead;
}
ScratchFramebuffer& BlitDrawFramebuffer() {
return g_scratchSets.Current().blitDraw;
}
Uint EnsureId(ScratchFramebuffer& fb) {
if (fb.id == 0) {
g_GLESFuncs.glGenFramebuffers(1, &fb.id);
// A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw
// buffers (the ES defaults for a non-default framebuffer).
fb.attachmentsKnown = true;
RecordNoColor(fb);
RecordNoDepth(fb);
fb.readBuffer = GL_COLOR_ATTACHMENT0;
fb.drawBuffer = GL_COLOR_ATTACHMENT0;
}
return fb.id;
}
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget,
GLint level) {
PrepareForUse(fb, fbTarget);
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
RecordNoDepth(fb);
}
if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) {
return;
}
if (fb.colorTex != 0) {
// Detach first: if the new attach fails, the point must read as
// missing (incomplete FBO), not silently keep the old texture.
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoColor(fb);
return;
}
fb.colorTex = tex;
fb.colorTarget = texTarget;
fb.colorLevel = level;
fb.colorLayer = -1;
}
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
PrepareForUse(fb, fbTarget);
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
RecordNoDepth(fb);
}
if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) {
return;
}
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoColor(fb);
return;
}
fb.colorTex = tex;
fb.colorTarget = 0;
fb.colorLevel = level;
fb.colorLayer = layer;
}
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
Bool withStencil, Bool stencilOnly) {
PrepareForUse(fb, fbTarget);
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
RecordNoColor(fb);
}
if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level &&
fb.depthLayer < 0 && fb.depthHasStencil == withStencil &&
fb.depthIsStencilOnly == stencilOnly) {
return;
}
if (fb.depthTex != 0) {
// One call clears both depth and stencil points regardless of how
// the previous attachment was made.
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
// A stencil-only texture has no depth point to attach at, and a depth one has no
// stencil point; GL_DEPTH_STENCIL_ATTACHMENT is only the combined texture's form.
g_GLESFuncs.glFramebufferTexture2D(fbTarget,
stencilOnly ? GL_STENCIL_ATTACHMENT
: (withStencil ? GL_DEPTH_STENCIL_ATTACHMENT
: GL_DEPTH_ATTACHMENT),
texTarget, tex, level);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoDepth(fb);
return;
}
fb.depthTex = tex;
fb.depthTarget = texTarget;
fb.depthLayer = -1;
fb.depthLevel = level;
fb.depthHasStencil = withStencil;
fb.depthIsStencilOnly = stencilOnly;
}
// The layer-point sibling of EnsureColorAttachmentLayer above, for a view's mip chain in
// a depth-only storage owner. depthTarget == 0 is the marker that this point was made
// with glFramebufferTextureLayer, exactly as colorTarget == 0 is on the color side, so a
// later 2D attach to the same level is never deduped against it.
void EnsureDepthAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
PrepareForUse(fb, fbTarget);
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
RecordNoColor(fb);
}
if (fb.depthTex == tex && fb.depthTarget == 0 && fb.depthLevel == level && fb.depthLayer == layer &&
!fb.depthHasStencil && !fb.depthIsStencilOnly) {
return;
}
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_DEPTH_ATTACHMENT, tex, level, layer);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoDepth(fb);
return;
}
fb.depthTex = tex;
fb.depthTarget = 0;
fb.depthLevel = level;
fb.depthLayer = layer;
fb.depthHasStencil = false;
fb.depthIsStencilOnly = false;
}
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
PrepareForUse(fb, fbTarget);
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
RecordNoColor(fb);
}
}
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
PrepareForUse(fb, fbTarget);
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
RecordNoDepth(fb);
}
}
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) {
if (fb.readBuffer == readBuffer) {
return;
}
g_GLESFuncs.glReadBuffer(readBuffer);
fb.readBuffer = readBuffer;
}
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) {
if (fb.drawBuffer == drawBuffer) {
return;
}
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
fb.drawBuffer = drawBuffer;
}
Uint EnsureCompleteTinyFramebufferId() {
ScratchSet& set = g_scratchSets.Current();
Uint& g_completeTinyFBOId = set.completeTinyFBOId;
Uint& g_completeTinyRBOId = set.completeTinyRBOId;
if (g_completeTinyFBOId != 0) {
return g_completeTinyFBOId;
}
// One-time creation: the renderbuffer binding is context state with no
// shadow, so save/restore it by query here (cold path only).
GLint prevRenderbuffer = 0;
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId);
g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId);
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId);
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
g_completeTinyRBOId);
const GLenum drawBuffer = GL_COLOR_ATTACHMENT0;
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
"Scratch 1x1 framebuffer is incomplete.");
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<Uint>(prevRenderbuffer));
return g_completeTinyFBOId;
}
void NoteTextureIdDeleted(Uint textureId) {
if (textureId == 0) {
return;
}
// Textures ARE shared, so the deleted id may sit in any context's scratch FBO.
g_scratchSets.ForEach([textureId](ScratchSet& set) {
for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) {
if (fb->colorTex == textureId || fb->depthTex == textureId) {
fb->attachmentsKnown = false;
}
}
});
}
void OnBackendContextDestroyed() {
g_scratchSets.Clear();
}
} // namespace ScratchFBOImpl
namespace PixelStoreImpl {
namespace {
PackState g_packState;
Bool g_packStateKnown = false;
void PinPackState(const PackState& value) {
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment);
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels);
g_packState = value;
g_packStateKnown = true;
}
} // namespace
void ApplyPackState(const PackState& desired) {
if (!g_packStateKnown) {
PinPackState(desired);
return;
}
if (desired.Alignment != g_packState.Alignment) {
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment);
g_packState.Alignment = desired.Alignment;
}
if (desired.RowLength != g_packState.RowLength) {
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength);
g_packState.RowLength = desired.RowLength;
}
if (desired.SkipRows != g_packState.SkipRows) {
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows);
g_packState.SkipRows = desired.SkipRows;
}
if (desired.SkipPixels != g_packState.SkipPixels) {
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels);
g_packState.SkipPixels = desired.SkipPixels;
}
}
PackState CurrentPackState() {
if (!g_packStateKnown) {
// Fresh/unknown context: pin to the GL defaults (what a new context
// starts with; writing them makes the shadow authoritative either way).
PinPackState(PackState{});
}
return g_packState;
}
void InvalidatePackStateCache() {
g_packStateKnown = false;
}
} // namespace PixelStoreImpl
namespace PrgramImpl {
Uint32 g_snormFallbackClampOutputMask = 0;
Uint g_fragColorBroadcastCount = 1;
Uint32 g_unormFallbackClampOutputMask = 0;
Uint g_lastUsedBackendProgramId = 0;
// Every error-queue drain in the program build path is bounded by this: a lost
// context never answers GL_NO_ERROR, and the build runs on the thread that would
// then spin forever.
constexpr Int kMaxDrainedProgramErrors = 32;
TwinRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl, MG_Pipe::MGPipeKind::ShaderCso> g_backendProgramObjects;
// ---- P5e (pg): the source, both ways round ---------------------------------------
Int ProgramArchiveSource::GetUniformLocation(const String& name) const {
// ProgramObject::GetUniformLocation over the archive. Reproduced rather than called
// because the frontend's form is a non-static member over Artifacts() and this arm
// has no ProgramObject to call it on; the ARRAY RULES are the whole body and they
// are the part a paraphrase would get wrong, so they are transcribed exactly.
const auto it = Link->uniformLocations.find(name);
if (it != Link->uniformLocations.end()) return static_cast<Int>(it->second);
if (name.empty()) return -1;
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" at its base
// location, so a bare "arr" resolves to that entry.
if (name.back() != ']') {
const auto suffixed = Link->uniformLocations.find(name + "[0]");
if (suffixed != Link->uniformLocations.end()) return static_cast<Int>(suffixed->second);
return -1;
}
if (name.length() < 4) return -1;
// An ARRAY OF ARRAYS is keyed by its full "[0]"-terminated spelling ("a[2][1][0]"),
// so a query that already ends in a subscript may still be the NAME of an array
// rather than an element of one. That reading is tried FIRST; only then is the
// trailing subscript read as an element index.
{
const auto arrayOfArrays = Link->uniformLocations.find(name + "[0]");
if (arrayOfArrays != Link->uniformLocations.end()) {
return static_cast<Int>(arrayOfArrays->second);
}
}
const SizeT bracket = name.rfind('[');
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
Uint element = 0;
for (SizeT i = bracket + 1; i < name.length() - 1; ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) return -1;
}
auto base = Link->uniformLocations.find(name.substr(0, bracket) + "[0]");
if (base == Link->uniformLocations.end()) {
base = Link->uniformLocations.find(name.substr(0, bracket));
if (base == Link->uniformLocations.end()) return -1;
}
const Int baseLocation = static_cast<Int>(base->second);
if (!IsValidUniformLocation(baseLocation)) return -1;
const Int index = Link->uniformIndexInTProgram[baseLocation];
if (!UniformAt(index).type.isArray) return -1;
if (static_cast<GLint>(element) >=
MG_State::GLState::ProgramObject::GetUniformArraySizeByTIndex(*Link, index)) {
return -1;
}
const Int location = baseLocation + static_cast<Int>(element);
if (!UniformLocationsAliasSameUniform(baseLocation, location)) return -1;
return location;
}
const String& ProgramArchiveSource::GetUniformBlockName(Uint index) const {
static const String kEmpty;
if (index >= Link->glBlockIndexToTProgram.size()) return kEmpty;
const Int tBlockIndex = Link->glBlockIndexToTProgram[index];
if (tBlockIndex < 0 || static_cast<SizeT>(tBlockIndex) >= Link->blockReflection.size()) {
return kEmpty;
}
return Link->blockReflection[tBlockIndex].name;
}
Uint ProgramArchiveSource::GetUniformBlockBinding(Uint index) const {
// THE OVERLAY WINS WHEN THERE IS ONE, and the archive's link-time value stands when
// there is not. glUniformBlockBinding moves this AFTER the link that produced the
// archive, so a twin answering from the archive alone would bind the program's
// blocks to the points it was LINKED with rather than the ones it is BOUND with.
if (OverlayGoverns && BlockBindingOverlay != nullptr) {
if (index < BlockBindingOverlay->size()) {
return static_cast<Uint>((*BlockBindingOverlay)[index]);
}
// A block past the tail the record declared. The tail is dense over
// GetActiveUniformBlocksCount(), so this is a record and an archive that
// disagree about how many blocks the program has - which can only happen if a
// create and a bindings record crossed. 0 is GL's own default and is the safe
// answer; the mismatch is the emitter's to fix, not this read's to hide.
return 0;
}
if (index >= Link->uniformBlockBinding.size()) return 0;
return static_cast<Uint>(Link->uniformBlockBinding[index]);
}
Int ProgramArchiveSource::GetUniformSamplerOrImageUnitIndex(Uint location) const {
if (OverlayGoverns) {
// THE TAIL IS THE WHOLE SET, so a location it does not name has no unit - NOT
// whatever the archive's link-time snapshot said. The client emits every
// assigned unit it can see, including the ones the link seeded from
// layout(binding=N), so "absent" really does mean -1.
//
// Sparse and ASCENDING BY LOCATION on the wire (CONTRACT-P5E §1), which is what
// lets this be a binary search: the build walks every location from 0 to
// maxUniformLocation, and a linear scan per location would be quadratic on a
// program with a thousand locations and a handful of samplers.
if (SamplerUnitOverlay == nullptr) return -1;
const auto it = std::lower_bound(
SamplerUnitOverlay->begin(), SamplerUnitOverlay->end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) {
return entry.Location < value;
});
if (it == SamplerUnitOverlay->end() || it->Location != location) return -1;
return static_cast<Int>(it->Unit);
}
if (location >= Link->uniformSamplerOrImageUnitIndex.size()) return -1;
return Link->uniformSamplerOrImageUnitIndex[location];
}
ProgramArchiveSource ProgramArchiveSource::FromFrontend(
const MG_State::GLState::ProgramObject& program) {
// THE MONOLITH-GLUE HALF (ruling 1 / ID-81). The "archive" is the frontend's own
// live tables, which is what makes this arm byte-for-byte what it was: there is no
// overlay because the three mutable fields ARE those tables' members, and the
// program object is pinned by the draw that is reading it.
ProgramArchiveSource source;
source.Link = &program.GetLinkReflection();
source.Spirv = &program.GetSpirvReflection();
source.Identity = program.GetExternalIndex();
source.IdentityIsHandleSlot = false;
source.Linked = program.GetLinkStatus();
source.SpirvUsable = program.GetSpirvStatus();
source.SpirvValidationEnabled = program.GetSpirvValidationEnabled();
source.PointSizeWasDemoted = program.PointSizeDemoted();
source.GlobalUboSize = program.GetUBOSize();
source.LinkedStages = program.GetLinkedShaderStages();
source.OverlayGoverns = false;
source.StorageOverrides = program.GetShaderStorageBlockBindingOverrides();
// The function this source's ComputeShaderStorageBlockBindingSignature overload
// replaces, so the monolith answer is unchanged and there is still exactly one
// place the number comes from on this arm.
source.StorageOverrideSignature = ComputeShaderStorageBlockBindingSignatureOf(program);
return source;
}
ProgramArchiveSource ProgramArchiveSource::FromRecord(
MG_Pipe::MGPipeHandle cso, const MG_Pipe::MGPipeShaderCsoRecord& record) {
// THE HANDLE ARM. Every field below is server-owned: the archive the create adopted,
// the descriptor the create stored, and the three tails set_program_bindings
// carried. Nothing here touches a ProgramObject, which is the property the strict
// lane measures.
ProgramArchiveSource source;
source.Link = &record.Archive->Link;
source.Spirv = &record.Archive->Spirv;
source.Identity = cso.Slot;
source.IdentityIsHandleSlot = true;
// LinkStatus IS A FIELD NOW, and that is the whole point of c0e adding it: before
// P5e "linked" was only implied by the create existing, which is a different
// statement - create_shader_state is re-issued at every link that moves the link
// version, and a FAILED relink of a bound program moves it too (ID-88).
source.Linked = record.Desc.LinkStatus != 0;
source.SpirvUsable = record.Desc.SpirvStatus != 0;
source.SpirvValidationEnabled = record.Desc.EnableSpirvValidation != 0;
source.PointSizeWasDemoted = record.Desc.PointSizeDemoted != 0;
source.GlobalUboSize = record.Desc.GlobalUboSize;
source.LinkedStages.reserve(record.Archive->LinkedStages.size());
for (const Uint32 stage : record.Archive->LinkedStages) {
source.LinkedStages.push_back(static_cast<ShaderStage>(stage));
}
// "HAS A BINDINGS RECORD EVER BEEN APPLIED TO THIS RECORD" - not "is the tail
// non-empty". A program with no uniform blocks, no sampler and no override emits a
// record with three empty tails, and that record is the statement that the archive's
// link-time values are no longer the answer.
source.OverlayGoverns = record.BindingsSerial != 0;
source.BlockBindingOverlay = &record.BlockBindings;
source.SamplerUnitOverlay = &record.SamplerUnits;
if (source.OverlayGoverns) {
source.StorageOverrides.reserve(record.StorageOverrides.size());
for (const auto& entry : record.StorageOverrides) {
source.StorageOverrides[entry.Name] = entry.Binding;
}
} else {
source.StorageOverrides = record.Archive->Link.shaderStorageBlockBinding;
}
// THE RECORD'S NUMBER EITHER WAY, and deliberately not a recomputation when the
// overlay is absent. This is the value the twin STAMPS and the draw path's clean
// clause COMPARES (CONTRACT-P5E §5.5 names record.Signature as the input), so the
// two have to be the same field or every draw of a program with an override
// rebuilds. A record with no bindings applied carries 0 while the archive may hold
// a link-time override map - the next set_program_bindings then produces a non-zero
// signature and forces exactly one rebuild, which is correct and, since the emitter
// sends the bindings in the same breath as the create, not a state a draw reaches.
source.StorageOverrideSignature = record.Signature;
return source;
}
Uint ProgramBlockBindingFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Int blockIndex) {
if (blockIndex < 0) return 0;
const SizeT index = static_cast<SizeT>(blockIndex);
if (record.BindingsSerial != 0) {
// The tail is the whole set; a block past it is a record and an archive that
// disagree about the block count, and GL's own default of 0 is the safe answer.
return index < record.BlockBindings.size()
? static_cast<Uint>(record.BlockBindings[index])
: 0;
}
if (!record.Archive || index >= record.Archive->Link.uniformBlockBinding.size()) return 0;
return static_cast<Uint>(record.Archive->Link.uniformBlockBinding[index]);
}
Int ProgramSamplerUnitFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Uint location) {
if (record.BindingsSerial != 0) {
const auto it = std::lower_bound(
record.SamplerUnits.begin(), record.SamplerUnits.end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) {
return entry.Location < value;
});
if (it == record.SamplerUnits.end() || it->Location != location) return -1;
return static_cast<Int>(it->Unit);
}
if (!record.Archive ||
location >= record.Archive->Link.uniformSamplerOrImageUnitIndex.size()) {
return -1;
}
return record.Archive->Link.uniformSamplerOrImageUnitIndex[location];
}
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract - in particular why the
// composite band had to land in the same package as this function.
BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso) {
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
// THE RECORD FIRST, and through the BAND-AWARE reader: a composite pipeline's CSO
// lives in the applier's CompositeShaderCsos table, and asking the ordinary one for
// it would answer null for a program that is perfectly live.
if (PipeShaderCsoRecordForHandle(cso) == nullptr) {
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record on the handle arm, so "
"no driver program can be built for it and the draw keeps what is bound",
cso.Slot, cso.Gen);
return nullptr;
}
auto* slot = AdoptTwinByHandle(g_backendProgramObjects, cso, "shader CSO");
if (slot == nullptr) return nullptr;
if (!*slot) *slot = MakeShared<BackendProgramObjectImpl>();
return slot->get();
}
BackendProgramObjectImpl::BackendProgramObjectImpl() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
m_backendProgramId = g_GLESFuncs.glCreateProgram();
if (m_backendProgramId == 0) {
MGLOG_E_ONCE("Failed to create program object in backend.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Created backend program object with ID: %u", m_backendProgramId);
}
}
BackendProgramObjectImpl::~BackendProgramObjectImpl() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendProgramId != 0) {
MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId);
g_GLESFuncs.glDeleteProgram(m_backendProgramId);
// The driver may recycle this GL name for a future program; a stale
// guard entry would then wrongly skip the glUseProgram for it.
if (g_lastUsedBackendProgramId == m_backendProgramId) {
g_lastUsedBackendProgramId = 0;
}
}
}
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding) {
if (backendProgramId == 0 || blockName.empty()) return false;
if (!g_GLESFuncs.glGetProgramResourceIndex || !g_GLESFuncs.glShaderStorageBlockBinding) return false;
GLuint driverIndex =
g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, blockName.c_str());
if (driverIndex == GL_INVALID_INDEX) {
// An arrayed block is enumerated per element by GL but declared once; the
// generated ESSL carries the bare block name.
const auto bracket = blockName.rfind('[');
if (bracket == String::npos || blockName.back() != ']') return false;
driverIndex = g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK,
blockName.substr(0, bracket).c_str());
if (driverIndex == GL_INVALID_INDEX) return false;
}
g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, driverIndex, binding);
return true;
}
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
const ProgramBuildSource& src) {
const auto& overrides = src.GetShaderStorageBlockBindingOverrides();
if (overrides.empty()) return; // the overwhelming majority of programs
for (const auto& [blockName, binding] : overrides) {
if (binding < 0) continue;
ApplyShaderStorageBlockBinding(backendProgramId, blockName, static_cast<Uint>(binding));
}
}
// P5e (pg). ON A PUSH BUILD THE SIGNATURE IS THE SOURCE'S, TAKEN VERBATIM, and it is not
// recomputed here for a reason worth stating: on the handle arm the number the DRAW PATH
// compares against is MGPipeShaderCsoRecord::Signature - the CLIENT's commutative hash,
// computed in MG_Impl/Pipe/ProgramEmit.h. If the server recomputed its own from the
// record's override tail, the two formulas would have to stay bit-identical for ever or
// every draw of every program with an override would rebuild. Taking the client's number
// verbatim removes that coupling: there is one authority, and the twin's stamp and the
// clean clause read the same field. The monolith arm's source carries the answer of the
// function this replaces, computed from the frontend map, so its behaviour is unchanged.
Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src) {
return src.StorageOverrideSignature;
}
// The COMPUTATION, still here and still the only one on this side: FromFrontend seeds a
// monolith source with it, and the monolith arm of SyncCurrentProgram's clean condition
// asks it per draw exactly as it always did. In a pull build this function does not
// exist and the overload above IS this body (see the #else), so the pull build gains no
// symbol - G1's rule for every new name in this phase.
Uint64 ComputeShaderStorageBlockBindingSignatureOf(
const MG_State::GLState::ProgramObject& program) {
const auto& overrides = program.GetShaderStorageBlockBindingOverrides();
if (overrides.empty()) return 0; // the overwhelming majority of programs
// Order-independent on purpose: the source is an UnorderedMap, so any signature that
// depended on iteration order would differ between two identical override sets and
// rebuild the program for nothing. Built from the VALUES, not from a change counter,
// so re-setting a block to the binding it already carries forces no rebuild - which
// is what the pipeline composite's per-draw uniform mirror depends on.
//
// MG_Impl/Pipe/ProgramEmit.h's MGPipeStorageOverrideSignatureEntry is this entry
// mix, verbatim, on the client side; the two are twins with a named pointer at each
// other because MG_Backend includes nothing from MG_Impl.
Uint64 signature = 0;
for (const auto& [blockName, binding] : overrides) {
if (binding < 0) continue; // never rebound; the declared qualifier still stands
Uint64 entry = std::hash<String>{}(blockName);
entry ^= (static_cast<Uint64>(static_cast<Uint32>(binding)) + 0x9e3779b97f4a7c15ull +
(entry << 6) + (entry >> 2));
signature += entry; // commutative combine
}
return signature;
}
namespace {
// The GL internal format bound to an image unit right now. GL_NONE for a unit
// outside the frontend's array, which cannot be addressed at all.
Uint BoundImageUnitFormat(Int unit) {
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, §5.4): the format-less image bake asks what format the APPLICATION
// named at this unit, and under a transport the record is the only statement of
// it - MGPImageView::InternalFormat is that same GLenum, copied by the client
// out of the unit's own binding. A unit the applier has never been told about
// answers 0, which is what an unbound unit answers on the frontend arm too.
if (MG_Config::DataArmIsRecord()) {
const auto& st = MG_Pipe::MGPipeApplier();
if (static_cast<SizeT>(unit) >= st.BoundShaderImages.size()) return 0;
return static_cast<Uint>(st.BoundShaderImages[static_cast<SizeT>(unit)].InternalFormat);
}
#endif
return static_cast<Uint>(MG_Pipe::gPipeInputs.GetImageTextureBinding(unit).Format);
}
// Combines one (unit, format) pair into a running digest. Commutative, so the order
// the uniforms are walked in cannot change the answer, and mixed rather than summed
// so a unit and a format cannot trade places between two pairs and cancel out.
Uint64 MixImageUnitFormat(Uint64 signature, Int unit, Uint format) {
Uint64 entry = static_cast<Uint64>(static_cast<Uint32>(unit)) + 0x9e3779b97f4a7c15ull;
entry ^= static_cast<Uint64>(format) + 0xbf58476d1ce4e5b9ull + (entry << 6) + (entry >> 2);
return signature + entry;
}
// Pipeline position of a shader stage. Names the PRODUCER of an inter-stage
// interface block: a block one stage consumes was written by the stage before it.
// ShaderStage is declared in pipeline order, so the enum value IS the position;
// compute has no inter-stage interface at all and is reported as -1.
Int InterStagePipelineIndex(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex:
case ShaderStage::TessControl:
case ShaderStage::TessEval:
case ShaderStage::Geometry:
case ShaderStage::Fragment:
return static_cast<Int>(stage);
default:
return -1;
}
}
// Whether a stage can declare interface blocks in BOTH directions at once, i.e.
// whether one block name can name two different blocks inside it. A vertex INPUT
// and a fragment OUTPUT cannot be blocks and compute has neither, so only these
// three can. This is what keeps the module probe off every program without
// tessellation or geometry - which is every program Minecraft and its shader packs
// build.
Bool CanDeclareBlocksInBothDirections(ShaderStage stage) {
return stage == ShaderStage::TessControl || stage == ShaderStage::TessEval ||
stage == ShaderStage::Geometry;
}
// Reflection names an array uniform after its first element ("g_image[0]") at every
// location it spans; SPIR-V names the variable once, without the subscript. This is
// the name both sides agree on.
String ImageUniformBaseName(const String& reflectionName) {
if (reflectionName.size() >= 3 && reflectionName.compare(reflectionName.size() - 3, 3, "[0]") == 0) {
return reflectionName.substr(0, reflectionName.size() - 3);
}
return reflectionName;
}
// GL_MAX_<stage>_IMAGE_UNIFORMS as the ES driver reports it, which is also exactly what
// MobileGL advertises for it (GL_Getter answers from the same DynamicBackendParameters).
// -1 for a stage the ES side has no such limit for, which is the "cannot say" answer
// the diagnostic that reads it prints rather than a made-up number. [[maybe_unused]]
// because its only caller is an MGLOG_E argument, and MGLOG_E compiles to nothing in a
// build whose MOBILEGL_LOG_ACTIVE_LEVEL is above ERROR.
[[maybe_unused]] Int AdvertisedStageImageUniformLimit(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex: return g_GLESCapabilities.MaxVertexImageUniforms;
case ShaderStage::Geometry: return g_GLESCapabilities.MaxGeometryImageUniforms;
case ShaderStage::Fragment: return g_GLESCapabilities.MaxFragmentImageUniforms;
case ShaderStage::Compute: return g_GLESCapabilities.MaxComputeImageUniforms;
default: return -1;
}
}
// Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only
// through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes
// through untouched - the emitted ESSL still has to be legal for the driver.
Bool IsCoreEsslLayoutFormat(glslang::TLayoutFormat format) {
switch (format) {
case glslang::ElfRgba32f:
case glslang::ElfRgba16f:
case glslang::ElfR32f:
case glslang::ElfRgba8:
case glslang::ElfRgba8Snorm:
case glslang::ElfRgba32i:
case glslang::ElfRgba16i:
case glslang::ElfRgba8i:
case glslang::ElfR32i:
case glslang::ElfRgba32ui:
case glslang::ElfRgba16ui:
case glslang::ElfRgba8ui:
case glslang::ElfR32ui:
return true;
default:
return false;
}
}
// The GL internal format a glslang layout format names, for the eighteen non-core
// formats WidenImageFormatsForEssl carries losslessly plus nothing else: the only
// question asked of it is "does this DECLARED format widen", and answering 0 for
// everything else is the same "no" a non-widenable format gets. Kept as its own
// switch rather than routed through the frontend's enum converters because a
// TLayoutFormat is a glslang value and the reflection snapshot stores it raw.
//
// IT MUST LIST EXACTLY WHAT WideningOfSpirvImageFormat DOES. This table is what arms
// the pass (ImageFormatWillBeWidened -> declaresWidenableImageFormat), so a format the
// pass would carry but this switch answers 0 for never gets the chance: the module
// reaches SPIRV-Cross with its original qualifier, the throw takes the stage, and the
// only visible symptom is the "no GLSL ES spelling" diagnostic for a format that has
// one. That is exactly what r11f_g11f_b10f did until it was added here.
Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) {
switch (format) {
case glslang::ElfRg32f: return 0x8230; // GL_RG32F
case glslang::ElfRg16f: return 0x822F; // GL_RG16F
case glslang::ElfR16f: return 0x822D; // GL_R16F
case glslang::ElfRg8: return 0x822B; // GL_RG8
case glslang::ElfR8: return 0x8229; // GL_R8
case glslang::ElfRg8Snorm: return 0x8F95; // GL_RG8_SNORM
case glslang::ElfR8Snorm: return 0x8F94; // GL_R8_SNORM
case glslang::ElfRg32i: return 0x823B; // GL_RG32I
case glslang::ElfRg16i: return 0x8239; // GL_RG16I
case glslang::ElfR16i: return 0x8233; // GL_R16I
case glslang::ElfRg8i: return 0x8237; // GL_RG8I
case glslang::ElfR8i: return 0x8231; // GL_R8I
case glslang::ElfRg32ui: return 0x823C; // GL_RG32UI
case glslang::ElfRg16ui: return 0x823A; // GL_RG16UI
case glslang::ElfR16ui: return 0x8234; // GL_R16UI
case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI
case glslang::ElfR8ui: return 0x8232; // GL_R8UI
// Not a channel widening but a lossless re-encoding into rgba16f - the one entry
// here whose carrier has a different per-channel layout. See
// WidenImageFormatsPass.h.
case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F
// 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same
// channel count, every value representable. Only the transfer is re-encoded.
case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI
// The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES.
// These are the entries whose carrier changes the shader-visible type as well as
// the qualifier (image2D becomes uimage2D), so every access through them is
// wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h.
case glslang::ElfRgba16: return 0x805B; // GL_RGBA16
case glslang::ElfRg16: return 0x822C; // GL_RG16
case glslang::ElfR16: return 0x822A; // GL_R16
case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2
case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM
case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM
case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM
default:
return 0;
}
}
// Whether the ESSL chain will re-declare an image of this format in a core carrier
// and mask its accesses (WidenImageFormatsForEssl). The same rule
// TextureImpl::GetImageBindableStorageWidening applies to the storage and the bind -
// the three layers move together or the shader addresses a texel size the storage
// does not have.
//
// Without GL_NV_image_formats there is no legal spelling for any non-core format, so
// everything carriable widens. WITH the extension only the formats SPIRV-Cross
// refuses to print do: it throws for its is_desktop_only_format set instead of
// emitting a token, and the throw loses the stage however willing the driver was.
Bool ImageFormatWillBeWidened(Uint glInternalFormat) {
if (glInternalFormat == 0) return false;
if (MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) == 0) {
return false;
}
return !g_GLESCapabilities.SupportsExtendedImageFormats ||
!MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(
glInternalFormat);
}
} // namespace
// What the format bake needs from the frontend, collected in one walk of the uniform
// reflection: which image uniforms declared NO format (the only ones a bake may touch -
// a declared format is authoritative and stays), what the units they address currently
// hold, and whether any format in play - declared or baked - is outside the ES core set.
ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src) {
ImageFormatBakeInputs inputs;
// A format GLSL ES cannot spell on a driver with no GL_NV_image_formats to spell it
// with. There is no legal ESSL for such a shader at all, so the stage will not
// compile and the program is lost - a failure that used to leave nothing behind but
// a draw that rendered nothing. Recorded and reported ONCE per program build rather
// than per uniform: an image array reaches this decision once per element.
String unspellableUniform;
String unspellableFormat;
Uint unspellableCount = 0;
const auto recordUnspellableFormat = [&](const String& uniformName, String formatSpelling) {
if (unspellableCount == 0) {
unspellableUniform = uniformName;
unspellableFormat = Move(formatSpelling);
}
++unspellableCount;
};
const Uint maxUniformLoc = src.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = src.GetUniformName(loc);
if (name.empty()) continue;
if (!IsImageUniformType(src.GetUniformType(loc))) continue;
const auto& type = src.GetUniformTypeFacts(loc);
if (type.hasFormat) {
// Declared, and therefore never overridden by the BAKE - but a non-core
// spelling still has to become legal ESSL somehow.
const auto declaredFormat = static_cast<glslang::TLayoutFormat>(type.layoutFormat);
if (!IsCoreEsslLayoutFormat(declaredFormat)) {
// Eighteen of the twenty-six non-core formats are re-declared in a core
// format that carries them losslessly, with every access masked back to
// the channels GL says they have (WidenImageFormatsForEssl, and the
// matching storage/bind widening in TextureImpl). Those need neither the
// extension nor the diagnostic: there IS a legal spelling for them now.
if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) {
inputs.declaresWidenableImageFormat = true;
} else {
inputs.needsExtendedImageFormats = true;
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
// From the OWNED TypeFacts, not from a live TType: the reflection
// snapshot already carries the declared layout format, and there
// is no glslang object to ask on a translation-cache L1 hit.
recordUnspellableFormat(
name, glslang::TQualifier::getLayoutFormatString(declaredFormat));
}
}
}
continue;
}
const Int unit = src.GetUniformSamplerOrImageUnitIndex(loc);
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue;
const Uint boundFormat = BoundImageUnitFormat(unit);
// Every format-less uniform contributes to the rebuild key, including one whose
// unit holds nothing yet: an image bound for the first time AFTER the link has
// to move the key, or the program built against "nothing bound" would never be
// rebuilt against the real format.
inputs.units.push_back(unit);
inputs.signature = MixImageUnitFormat(inputs.signature, unit, boundFormat);
if (boundFormat == 0) continue;
if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) {
// The same three-way split the DECLARED branch above makes, and it has to be
// the same one: a format-less image is baked with the bound format, so from
// WidenImageFormatsForEssl's point of view the two routes hand it identical
// modules and must arm it identically.
if (ImageFormatWillBeWidened(boundFormat)) {
// The bake writes this format INTO the module, so the widening that runs
// straight after has to be armed for it even though nothing DECLARED it -
// and armed WHETHER OR NOT the driver has GL_NV_image_formats. SPIRV-Cross
// throws for its is_desktop_only_format set the moment it targets ESSL,
// however willing the driver was, so the extension decides HOW MUCH gets
// widened (widenOnlyUnprintableImageFormats) and never WHETHER. Arming
// this only on the no-extension path left the shader half of the widening
// switched off while TextureImpl's storage/bind half - which keys on
// SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran:
// the stage threw, the program linked without it, and every dispatch
// silently did nothing. That is the whole of
// KHR-GL43.stencil_texturing.functional's compute half, whose uni_image is
// a format-less uimage2D bound to an R8UI texture.
inputs.declaresWidenableImageFormat = true;
} else if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
// Outside the GLSL ES core set, with no GL_NV_image_formats to spell it
// and no core format that carries it exactly: there is no legal ESSL for
// this stage at all. Leaving the image format-LESS is NOT a softer
// failure: all three test devices reject a format-less image declaration
// outright ("all images have to define layout format"), readonly and
// writeonly alike, so it trades one hard compile error for another. The
// unit stays in the rebuild key either way, so a rebind to a spellable
// format still rebuilds and works.
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which "
"GLSL ES core cannot spell, this driver has no GL_NV_image_formats for, and "
"no core format carries exactly.",
name.c_str(), unit, boundFormat);
recordUnspellableFormat(
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
continue;
} else {
inputs.needsExtendedImageFormats = true;
}
}
const String baseName = ImageUniformBaseName(name);
const auto existing = inputs.glFormatByUniformName.find(baseName);
if (existing == inputs.glFormatByUniformName.end()) {
inputs.glFormatByUniformName.emplace(baseName, boundFormat);
} else if (existing->second != boundFormat) {
// An ARRAY whose elements were pointed at units holding different formats.
// One declaration carries one qualifier, so there is no spelling for it, and
// the uniform is left format-less rather than given a format that is wrong
// for all but one element. Marked in place with GL_NONE and swept below -
// never by erasing here, because the entry is reached again by the array's
// remaining elements and a flat hash map must not be mutated structurally
// while an iterator into it is live.
existing->second = 0;
}
}
for (const auto& entry : inputs.glFormatByUniformName) {
if (entry.second == 0) inputs.conflictedNames.push_back(entry.first);
}
for (const auto& conflicted : inputs.conflictedNames) {
inputs.glFormatByUniformName.erase(conflicted);
}
// Split off the ones SPIRV-Cross will not print. They cannot go through the module -
// it throws for them when targeting ESSL, and the stage is lost - so they are spelled
// into the emitted text instead. Collected first, erased after, because a flat hash
// map must not be restructured while it is being walked.
Vector<String> textCompleted;
for (const auto& entry : inputs.glFormatByUniformName) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) {
continue;
}
// A format the widening carries stays on the module route even though SPIRV-Cross
// would not print it: by the time SPIRV-Cross sees the declaration it names the
// core carrier, which it does print. Writing the narrow spelling into the text
// instead would put back exactly the token the driver rejects.
if (ImageFormatWillBeWidened(entry.second)) {
continue;
}
String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second);
if (spelling.empty()) continue; // no image-format spelling at all; nothing to write
inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling));
textCompleted.push_back(entry.first);
}
for (const auto& name : textCompleted) {
inputs.glFormatByUniformName.erase(name);
}
// Unlatched MGLOG_E, like the transpile- and link-failure diagnostics in SyncToBackend:
// one line per failing program build, and naming the uniform and the format is the
// whole diagnostic value. Left as a log rather than a link failure on purpose - the
// frontend has already reported LINK_STATUS = true and GL cannot retract it, and the
// program stays queryable exactly as the "linked but not drawable" exit leaves it.
if (unspellableCount != 0) {
MGLOG_E("Image format '%s' on uniform '%s' has no GLSL ES spelling and this driver does not expose "
"GL_NV_image_formats%s; the stage using it cannot compile and the program will draw "
"nothing.",
unspellableFormat.empty() ? "(none)" : unspellableFormat.c_str(), unspellableUniform.c_str(),
unspellableCount > 1 ? " (and it is not the only image uniform affected)" : "");
}
return inputs;
}
// Every image ARRAY whose elements the application did NOT leave on units consecutive from
// element zero - the only shape ESSL can spell, since an image unit there comes solely
// from the one layout(binding=N) an array declaration carries. Desktop GL assigns them
// per element with glUniform1i, which ES makes an INVALID_OPERATION on an image uniform,
// so there is nothing to fix at the API end and the emitted text has to carry it
// (RemapImageArrayElementUnits). Empty for every program that does not do this, which is
// very nearly all of them - one walk of the reflection and no allocation in that case.
Vector<ImageArrayUnitPlan> CollectNonConsecutiveImageArrayPlans(const ProgramBuildSource& src) {
Vector<ImageArrayUnitPlan> plans;
const Uint maxUniformLoc = src.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = src.GetUniformName(loc);
if (name.empty()) continue;
if (!IsImageUniformType(src.GetUniformType(loc))) continue;
// Reflection repeats the array's "g_image[0]" spelling at EVERY location the array
// spans, so only the location that name resolves back to is the array itself.
if (src.GetUniformLocation(name) != static_cast<Int>(loc)) continue;
const String baseName = ImageUniformBaseName(name);
if (baseName == name) continue; // a scalar image: one binding says it all
ImageArrayUnitPlan plan;
plan.name = baseName;
for (Uint element = loc; element <= maxUniformLoc &&
src.UniformLocationsAliasSameUniform(
static_cast<Int>(loc), static_cast<Int>(element));
++element) {
plan.units.push_back(src.GetUniformSamplerOrImageUnitIndex(element));
}
if (plan.units.size() < 2) continue;
Bool consecutive = true;
for (SizeT element = 0; element < plan.units.size(); ++element) {
if (plan.units[element] != plan.units[0] + static_cast<Int>(element)) {
consecutive = false;
break;
}
}
// What ESSL does unaided is already right; leaving these out is what keeps the
// emitted text of every ordinary image shader byte-identical to before.
if (consecutive) continue;
plans.push_back(Move(plan));
}
return plans;
}
Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const {
if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs
Uint64 signature = 0;
for (const Int unit : m_formatlessImageUnits) {
signature = MixImageUnitFormat(signature, unit, BoundImageUnitFormat(unit));
}
return signature;
}
Bool BackendProgramObjectImpl::ImageUnitFormatsStillMatch() const {
if (m_formatlessImageUnits.empty()) return m_imageUnitFormatSignature == 0;
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
}
// ===== THE MEMOIZED SEGMENT (shader translation memo, level 2) =====
//
// One stage's sanitized SPIR-V turned into the ESSL SPIRV-Cross emits, through the
// DirectGLES-specific pass chain. Extracted out of SyncToBackend's loop so that the
// boundary the L2 memo keys on is a function signature rather than a comment: every
// input this reads is either an argument below or a process-global capability bit,
// and EVERY ONE OF THEM IS IN EsslTranslationKeyInputs. If you add a read here, add
// it to BuildEsslTranslationKey too - an under-specified key here is a silently
// miscompiled shader.
//
// Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray,
// MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples,
// SupportsNoperspectiveInterpolation, SupportsExtendedImageFormats, GLESVersion}
// (the last via ResolveBackendEsslVersion); and m_backendProgramId, for a log line only.
//
// Deliberately NOT in here, and therefore NOT in the key: the text-level passes that
// follow in SyncToBackend. They are cheap string work and they read a long tail of
// live per-program state (RebindImageUniformsToFrontendUnits walks the ProgramObject
// reflection, the norm-clamp masks and the fragColor broadcast count are live
// globals, the buffer-texture tier retargets an #extension line) whose inclusion
// would make the key both enormous and fragile for no measurable saving.
//
// Returns false when SPIRV-Cross refused the module; `outError` then holds its
// message and nothing is memoized.
Bool BackendProgramObjectImpl::TranspileSpirvToEssl(
const Vector<unsigned int>& spirvCode, const GLenum glShaderType,
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const std::map<String, String>& inputBlockRenames,
const std::map<String, String>& outputBlockRenames, const Bool stripInputBlockLocations,
const Bool stripOutputBlockLocations,
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
String& outError) const {
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
// plain globals (mg_*) before handing the module to SPIRV-Cross.
Vector<unsigned int> loweredSpirv;
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
!loweredSpirv.empty()) {
effectiveSpirv = &loweredSpirv;
}
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
// there IS an extension that provides it - so this runs only when the driver does
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
// gets the `#extension` request added to the decompiled source below instead.
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
// viewport 0), which is the degradation ViewportArrayScenario already documents
// for this backend; NOT demoting it costs the whole program, because the stage
// fails to compile and every draw made with it silently renders nothing.
// Gated on the module actually declaring the output, so no other stage pays an
// optimizer round trip for it.
// One parse of the module answers every armed pass gate below. The per-gate
// Declares* probes each cost a BuildModule per stage, and on a driver where both
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
// Probing the pre-lowering module is sound for both gates: demoting
// gl_ViewportIndex neither adds nor removes multisampled image types.
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
// this is backend code and must not reach into the GL frontend. 4 is that
// translation unit's kFrontendMaxSamples, which is the source of truth -
// keep the two in step.
const Int advertisedMaxSamples =
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
// Armed by the EMULATION as well as by the missing extension, and the emulation is on
// by default (MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a
// reason to keep the builtin: it only ever gave the SHADER a compilable name, while
// the driver's INDEXED viewport state was never programmed by anything in MobileGL
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
// rasterized every index as index 0 exactly like a driver without it. Lowering here
// is what lets the ESSL passes downstream turn the builtin into the flat varying the
// replay gates on.
//
// Restricted to the three stages GL lets WRITE the builtin (4.1 core gives it to the
// geometry stage, ARB_shader_viewport_layer_array adds vertex and tessellation
// evaluation). A fragment stage's gl_ViewportIndex is an INPUT, which the pass
// declines anyway, and a compute stage has none - so arming those two only ever
// bought them the shared probe's BuildModule for nothing.
const Bool stageCanWriteViewportIndex = glShaderType == GL_VERTEX_SHADER ||
glShaderType == GL_TESS_EVALUATION_SHADER ||
glShaderType == GL_GEOMETRY_SHADER;
const Bool viewportLoweringArmed =
stageCanWriteViewportIndex &&
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
const Bool sampleClampArmed =
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
// The image-format widening is armed on EVERY driver, so its probe has to ride the
// shared parse rather than add one: it is asked of every stage of every program, and
// a BuildModule per stage per gate is exactly what cost compile-heavy CTS cases ~10%
// before this struct existed. What differs per driver is only HOW MUCH it widens -
// everything carriable where there is no GL_NV_image_formats to spell the narrow
// format, and only the formats SPIRV-Cross refuses to print where there is.
const Bool widenOnlyUnprintableImageFormats = g_GLESCapabilities.SupportsExtendedImageFormats;
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
if (viewportLoweringArmed || sampleClampArmed) {
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
*effectiveSpirv);
}
Vector<unsigned int> loweredViewportSpirv;
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
!loweredViewportSpirv.empty()) {
effectiveSpirv = &loweredViewportSpirv;
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which ESSL has no core "
"spelling for. The builtin was demoted to a plain global; %s.",
m_backendProgramId,
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
ViewportArrayEmulationEnabled()
? "the ESSL passes below promote it to a routing varying"
: "every invocation renders into viewport 0");
}
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
// reports - but the realised allocation cannot be, and
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
// texture the ONE sample Adreno and Mali actually support for it. A shader
// written against the advertised ceiling then fetches a sample that storage does
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
// the Sample operand to the backend-real per-category maximum so the fetch lands
// inside the allocation. Gated on some category actually being squeezed AND the
// module actually declaring a multisampled image, so no other stage pays an
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
// allocates the sample count it was asked for, so its modules are already right.
Vector<unsigned int> clampedSampleSpirv;
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
*effectiveSpirv, clampedSampleSpirv,
g_GLESCapabilities.MaxColorTextureSamples,
g_GLESCapabilities.MaxIntegerSamples,
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
enableSpirvValidation) &&
!clampedSampleSpirv.empty()) {
effectiveSpirv = &clampedSampleSpirv;
}
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
// rather than emulating them, so this has to happen before it sees the binary.
Vector<unsigned int> splitArrayInputSpirv;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
// Only when the pass ACTUALLY split something. The optimizer hands back a
// re-serialised copy either way, and adopting that copy for every vertex
// shader would put every one of them through a round trip they do not need
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
// first time this gate was missing (the original capture; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024).
effectiveSpirv = &splitArrayInputSpirv;
}
// Adopt the rewritten module only when THIS stage actually had one of the
// blocks - the optimizer hands back a re-serialised copy either way, and taking
// that copy for a module it did not rewrite is not free (it cost the
// create-indirect retrace 0.15 SSIM when the array-input split first missed
// this gate; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). The report has to be
// per stage, not cumulative: a fragment shader consuming the same block reports a
// name the vertex stage already reported,
// and its own rewrite must still be taken or the two stages stop matching.
Vector<unsigned int> flattenedXfbSpirv;
if (!xfbCaptureBlockNames.empty()) {
// Reported into a local first, and published only if the module is really
// adopted. The caller unions the published set unconditionally (so that a
// cache HIT contributes its names too), so publishing a name for a rewrite
// that was declined would rename a capture the emitted ESSL never renamed.
std::set<String> flattenedNames;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
*effectiveSpirv, xfbCaptureBlockNames, flattenedNames, flattenedXfbSpirv,
enableSpirvValidation) &&
!flattenedXfbSpirv.empty() && !flattenedNames.empty()) {
effectiveSpirv = &flattenedXfbSpirv;
outFlattenedXfbBlockNames = Move(flattenedNames);
}
}
// The producer-keyed interface-block rename, planned program-wide by the caller and
// applied to this stage: the blocks it CONSUMES are spelled after the previous stage
// present in the program and the ones it PRODUCES after itself, so a tessellation
// evaluation stage's two TCSOutputBlocks stop being one name and every other stage
// still agrees with it. See UniquifyIoBlockNamesPass for why Mali needs it.
//
// INSIDE THE MEMOIZED SEGMENT, and at exactly the position it was written in - after
// the XFB flatten, before the UBO precision strip. Both halves of that matter:
// * INSIDE, because it rewrites the MODULE and the emitted ESSL carries the result.
// Left outside, a second program sharing this stage's key would be served ESSL
// with the blocks un-renamed and the repair would silently stop working - the
// same trap SetAtomicCounterBlockBindings sets one screen down.
// * AT THIS POSITION, because moving a SPIR-V pass in a chain is a behavioural
// change, and this one arrived device-verified on Mali. Hoisting it above the
// cache probe instead would have needed no key material at all (the rename would
// already be in the module bytes the key hashes) and was rejected for that
// reason: it reorders the chain, and it would re-serialise the module on every
// build including the ones the memo is there to make free.
// Its two rename maps are therefore KEY MATERIAL - see the caller.
Vector<unsigned int> uniquifiedIoBlockSpirv;
if (!inputBlockRenames.empty() || !outputBlockRenames.empty()) {
std::set<String> stageRenamedIoBlockNames;
if (MG_Util::ShaderTranspiler::ShaderCompiler::UniquifyIoBlockNamesForEssl(
*effectiveSpirv, inputBlockRenames, outputBlockRenames,
stageRenamedIoBlockNames, uniquifiedIoBlockSpirv, enableSpirvValidation) &&
!uniquifiedIoBlockSpirv.empty() && !stageRenamedIoBlockNames.empty()) {
effectiveSpirv = &uniquifiedIoBlockSpirv;
MGLOG_D("Program %u stage %s: %zu inter-stage interface block(s) renamed per "
"producing stage, because some stage of this program declares the same "
"block name in both directions and the ES driver may alias the two.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
stageRenamedIoBlockNames.size());
}
}
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
// ForceSupporterOutput swaps the fragment header to highp, that member reads
// back as highp and the ES driver refuses to link ("definitions of uniform
// block ... do not match"). Strip the hint from block structs so both stages
// declare the member highp; nothing else about emission changes.
Vector<unsigned int> uboPrecisionSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
!uboPrecisionSpirv.empty()) {
effectiveSpirv = &uboPrecisionSpirv;
}
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
// rejects the require. So on such devices emulate screen-linear interpolation instead
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
// the decoration - exact, extension-free. Devices that have the extension keep the
// decoration and let the hardware do it natively.
Vector<unsigned int> noperspectiveSpirv;
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
!noperspectiveSpirv.empty()) {
effectiveSpirv = &noperspectiveSpirv;
}
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
// than approximating one. The shared pass turns the type into the 2D one and
// divides the coordinate of every normalized-coordinate lookup by the texture
// size, which is the whole of the difference between the two.
Vector<unsigned int> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
!rectLoweredSpirv.empty()) {
effectiveSpirv = &rectLoweredSpirv;
}
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
// as 2D for images without ever asking whether the type is arrayed, so a
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
// two-component constructor, which every driver rejects, taking the whole
// program with it. The pass does the conversion properly - type to 2D array,
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
Vector<unsigned int> arrayImageSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
arrayImageSpirv, enableSpirvValidation) &&
!arrayImageSpirv.empty()) {
effectiveSpirv = &arrayImageSpirv;
}
// The SAMPLER half of the same 1D story, and a defect one layer deeper than the one
// above. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - it just prints the
// OFFSET and the two GRADIENT operands with the arity the desktop shader spelled, so
// textureLodOffset(sampler1DArray, vec2, float, int) is emitted against a
// sampler2DArray and the driver answers "no matching overloaded function found",
// losing the stage and silently no-oping every dispatch that used it. Widening the
// operands alone would be an INVALID module (the validator derives the required arity
// from the image's own Dim), so the pass moves the type to 2D and widens coordinate,
// offset and gradients together.
//
// NO KEY MATERIAL, by the same test LegalizeResourceArrayIndexingForEssl passes:
// it takes the module and nothing else, no capability bit arms it, and it self-gates
// on the module's own content (BinaryHasOffsetOrGrad1DSampledImage). The module is
// already the largest thing in the L2 key, so it is covered completely.
Vector<unsigned int> sampled1DSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DSampledImagesForEssl(
*effectiveSpirv, sampled1DSpirv, enableSpirvValidation) &&
!sampled1DSpirv.empty()) {
effectiveSpirv = &sampled1DSpirv;
}
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
// format"), which loses the whole program. Give each such image the format the
// application bound to its unit - the one GL's format-class rules make correct -
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
// also rewrites image types, so this one is looking at the final shapes.
//
// Gated on the module actually declaring one: the map is empty for every program
// whose images all declare formats, and the cheap probe keeps a program that has
// an unbound format-less image from paying an optimizer round trip per stage.
Vector<unsigned int> imageFormatSpirv;
if (!imageFormatBake.glFormatByUniformName.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
enableSpirvValidation) &&
!imageFormatSpirv.empty()) {
effectiveSpirv = &imageFormatSpirv;
}
// GL has forty image formats and GLSL ES core has thirteen; the other twenty-seven
// reach ES only through GL_NV_image_formats, which no tested driver advertises. A
// shader declaring one of them has NO legal ESSL spelling at all - SPIRV-Cross throws
// for some of them and the driver rejects the token for the rest ("'rg32f' : not a
// legal layout qualifier id"), and dropping the qualifier is refused too ("all images
// have to define layout format") - so the stage is lost and every draw with the
// program silently renders nothing. Seventeen of them widen EXACTLY into a core format
// of the same per-channel width, and this rewrites those declarations to the carrier
// and masks every access back to the channels GL says the format has. The other nine
// have no exact carrier and keep the honest diagnostic
// CollectImageFormatBakeInputs emits.
//
// A driver that HAS GL_NV_image_formats still needs part of this. SPIRV-Cross throws
// for its is_desktop_only_format set when it targets ESSL rather than printing a
// token, and the throw loses the stage however willing the driver was - Mesa
// advertises the extension and `layout(r8ui) uimage2D` lost its whole program there
// until the widening ran for it too. So the driver bit decides HOW MUCH is widened,
// never WHETHER.
//
// AFTER the bake above, deliberately: a format-less image whose unit holds a non-core
// format is baked with that format and widened here, so both routes end in the same
// place and there is no second widening rule for baked declarations.
//
// KEY MATERIAL: g_GLESCapabilities.SupportsExtendedImageFormats, which selects the
// mode - see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes
// no other input: what it rewrites is a pure function of the module's own declared
// formats and that mode, and the module is already the largest thing in the L2 key.
// The ARMING flag is deliberately NOT key material: it only decides whether the pass
// runs, and the module below is adopted only when the pass actually changed the bytes
// - so a program-wide flag that over-arms a stage costs an optimizer round trip and
// changes no output.
//
// DirectVulkan is deliberately not given this: it takes the declared format natively
// and resolves the descriptor's view format from the same bind state.
Vector<unsigned int> widenedImageFormatSpirv;
if (imageFormatBake.declaresWidenableImageFormat &&
MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl(
*effectiveSpirv, widenedImageFormatSpirv, widenOnlyUnprintableImageFormats,
enableSpirvValidation) &&
!widenedImageFormatSpirv.empty() && widenedImageFormatSpirv != *effectiveSpirv) {
effectiveSpirv = &widenedImageFormatSpirv;
}
// GLSL ES demands a constant integral expression to index a fragment output
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
// outright ("array indexes for fragment outputs must be constant integral
// expressions"), linking no program and silently no-oping every draw that
// uses it. Mesa accepts it, ANGLE does not - which is the whole of the
// improved-transparency-minecraft-26.3 failure. Fold or lower the index here,
// on the ESSL path only: the same module is legal for DirectVulkan.
Vector<unsigned int> outputIndexSpirv;
if (glShaderType == GL_FRAGMENT_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
!outputIndexSpirv.empty()) {
effectiveSpirv = &outputIndexSpirv;
}
// Same rule, two more resources, every stage: desktop GL lets an array of
// storage blocks and an array of image uniforms be indexed with any
// dynamically-uniform expression, GLSL ES keeps the ES 3.1
// constant-expression rule for both, and the drivers enforce it - Qualcomm
// ("indexing into an SSBO array using a non-constant expression is not
// permitted"), Mesa ("image arrays indexed with non-constant expressions are
// forbidden in GLSL ES") - losing the stage, the program, and every draw or
// dispatch that used it, while the frontend keeps reporting the link glslang
// performed. Fold or lower the index here, on the ESSL path only: the same
// module is legal for DirectVulkan, which binds the array as one descriptor
// array.
//
// The image half is also what makes RemapImageArrayElementUnits below possible
// at all: that pass rewrites `g_image[k]` into a per-element declaration, and it
// can only do that once every k the emitted ESSL spells is a literal.
//
// NO KEY MATERIAL, and that is a conclusion rather than an omission: this takes the
// module and nothing else - no capability bit arms it, no per-program plan steers
// it - and it self-gates on the module's own content
// (BinaryHasDynamicResourceArrayIndexing). The module is already the largest
// thing in the L2 key, so it is fully covered. Contrast LowerViewportIndexForEssl,
// whose signature is equally module-only but which SupportsViewportArray ARMS -
// that bit is in the key precisely because of it.
Vector<unsigned int> blockArrayIndexSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeResourceArrayIndexingForEssl(
*effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) &&
!blockArrayIndexSpirv.empty()) {
effectiveSpirv = &blockArrayIndexSpirv;
}
// glslang kept the application's layout(offset = N) on the atomic counters it
// lowered onto gl_AtomicCounterBlock_<N>, and no std140/std430 layout can put
// member 0 anywhere but offset 0 - so SPIRV-Cross throws ("cannot be expressed as
// neither std430 nor std140") and the stage never reaches the driver. Collapse the
// block into one uint array at offset 0 and re-index each counter to the element
// that used to be at its byte offset; the buffer then stays bound whole, which it
// has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 64 on Adreno 830 and 32 or
// more everywhere else, so an 8-byte bind offset is not expressible on any of them).
// BEFORE SetAtomicCounterBlockBindings
// below, which only moves the block's BINDING and needs the block intact.
//
// NO KEY MATERIAL either, for the same reason - and note where the application's
// layout(offset = N) values live: glslang already baked them into the module as
// member Offset decorations, so they are in the key as MODULE BYTES. There is no
// separate offset input to carry.
Vector<unsigned int> atomicCounterSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(
*effectiveSpirv, atomicCounterSpirv, enableSpirvValidation) &&
!atomicCounterSpirv.empty()) {
effectiveSpirv = &atomicCounterSpirv;
}
// The second half of the inter-stage interface-block repair, and the one that
// actually closes the 420pack group: this driver drops the payload of a block that
// carries an explicit layout(location=) whenever a tessellation or geometry stage
// is in the pipeline, so the qualifier comes off and ES matches the block by name
// and member sequence instead. The names those two sides agree on are the ones the
// rename above just fixed, which is why this runs AFTER it and not before.
//
// The caller arms the two directions; both are false unless the driver POST
// measured the defect AND this program has a stage that can hit it. Adopted only
// when this stage really had a located block, for the reason the array-input split
// documents: the optimizer hands back a re-serialised copy either way.
//
// LAST IN THE CHAIN, and that position is load-bearing. Vulkan SPIR-V REQUIRES a
// Location on every user-defined Input/Output variable
// ([VUID-StandaloneSpirv-Location-04915]), so the module this produces is
// deliberately no longer valid Vulkan SPIR-V - it is an ESSL-emission intermediate
// that goes straight into SPIRV-Cross and reaches no driver as SPIR-V. Running it
// here means no later pass validates what it produced; the pass itself skips
// validation for the same reason (see StripIoBlockLocationsForEssl). Anywhere
// earlier and every remaining pass would latch a validation failure on a module
// that is doing exactly what it was asked to.
Vector<unsigned int> strippedIoBlockLocationSpirv;
if (stripInputBlockLocations || stripOutputBlockLocations) {
Bool strippedAny = false;
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripIoBlockLocationsForEssl(
*effectiveSpirv, stripInputBlockLocations, stripOutputBlockLocations,
strippedAny, strippedIoBlockLocationSpirv, enableSpirvValidation) &&
!strippedIoBlockLocationSpirv.empty() && strippedAny) {
effectiveSpirv = &strippedIoBlockLocationSpirv;
// THE ARMING SIGNAL, and it is INFO on purpose: the per-stage line below is
// MGLOG_D, which is compiled out of every build CI and the device runs, so
// nothing outside a debug build could tell an armed repair from a silently
// un-armed one. Latched, so it costs one line per process rather than one
// per stage of every program. The integration lane that pins the emulation
// on asserts on exactly this line - see UnlocatedIoBlockScenario.
MGLOG_I_ONCE("DirectGLES is emitting inter-stage interface blocks WITHOUT their "
"layout(location) qualifier, because this driver loses a located "
"block's payload across a tessellation or geometry boundary.");
MGLOG_D("Program %u stage %s: interface-block location qualifiers dropped "
"(%s), because this driver loses a located block's payload across a "
"tessellation or geometry boundary.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
stripInputBlockLocations
? (stripOutputBlockLocations ? "consumed and produced" : "consumed")
: "produced");
}
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION,
ResolveBackendEsslVersion());
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
// then deliberately preserves the qualifier for `buffer` declarations.
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
}
// Atomic counters, same mechanism for the same reason. glslang already turned
// every atomic_uint into a member of gl_AtomicCounterBlock_<N> and let the IO
// mapper pick that block's binding, which has no relation to the GL binding point
// N the application bound its counter buffer to - and can alias an SSBO the
// application binds itself. Move each block to its reserved slot and record N, so
// the draw path knows which GL_ATOMIC_COUNTER_BUFFER points to re-issue as
// storage-buffer bindings.
//
// BOTH HALVES ARE MEMO STATE. `atomicCounterEsslBindingTop` decides the binding
// this prints into the ESSL, so it is in the L2 key; `outAtomicCounterGlBindings`
// is an OUTPUT this stage produces and the draw path consumes, so it is in the L2
// payload. A hit that replayed only the text would leave the bindings empty and
// every counter buffer unbound - the same class of silent loss the flattened XFB
// block names would have been.
spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop,
outAtomicCounterGlBindings);
// `layout(index = 0)` is the GL default spelled out loud, and GLSL ES has no such
// qualifier in core - a stage that prints it is refused with "index layout
// qualifier requires EXT_blend_func_extended" and the whole program then draws
// nothing. Drop the decoration when it carries the default; a REAL dual-source
// index (1) is left alone, because that one genuinely needs the extension and the
// driver has to see it. Fragment stage only: no other stage can carry it.
if (glShaderType == GL_FRAGMENT_SHADER) {
spvcSession.DropDefaultFragmentOutputColorIndex();
}
// `readonly writeonly` together says the buffer variable can only be asked its
// .length(), which the frontend has already enforced - so the pair is inert, and
// printing it is not. Mesa's ES compiler refuses a block spelled that way and the
// stage never reaches the program.
spvcSession.RelaxReadWriteExclusiveStorageBuffers();
const char* result = nullptr;
spvcSession.Compile(&result);
if (!result) {
// The caller owns the diagnostic: it is the one that knows the
// frontend program id, and a failed transpile must NOT be memoized -
// the message names the stage and is worth re-emitting every time.
const char* lastError = spvcSession.GetLastErrorString();
outError = lastError ? lastError : "";
return false;
}
outSource = result;
return true;
}
// GL 4.6 core 11.2.2 lets a program have a tessellation EVALUATION shader and no CONTROL
// shader: the input patch is passed through unmodified and the levels come from the
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. OpenGL ES 3.2 has no such
// state and no such allowance - it rejects the program at link, and with an EMPTY info
// log, which was verified on an Adreno 830 with no MobileGL in the process (TES-only:
// link=0, log empty; the same shaders plus any TCS: link=1, with or without the SSBO and
// atomic counter the failing conformance case also declares). The frontend's own glslang
// link succeeds, so GL_LINK_STATUS reads TRUE, program 0 is bound in its place, and every
// draw silently renders nothing - a black framebuffer, an atomic counter still at 0 and
// an untouched SSBO, with no error anywhere.
//
// So the missing stage is synthesized and attached here, alongside the program's own.
// DirectVulkan already does exactly this for the same structural reason
// (ProgramFactory::BuildPassthroughTessControlSource), so this completes the pair rather
// than inventing an approach.
//
// Nothing that works today can be harmed by it: it fires ONLY for a program that has an
// evaluation stage and no control stage, and every such program fails to link on ES right
// now. The worst case is that the synthesized stage fails to compile or link, which leaves
// the program exactly as dead as it already was - but with a driver log that says why,
// where today there is an empty one.
void BackendProgramObjectImpl::AttachPassthroughTessControlStage(
const ProgramBuildSource& src, const Int tessEvalShaderIndex,
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
const String& tessEvalStageEssl) {
// PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output
// patch size - so a program built for one value is stale for another. Recorded here
// and compared on the draw path (SyncCurrentProgram), the same shape as the
// storage-block and image-format signatures next to it.
const Uint patchVertices = MG_Pipe::gPipeInputs.IsLive()
? MG_Pipe::gPipeInputs.GetPatchVertices()
: 3u;
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
// PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked
// into the same stage (ES has no such state and no entry point to forward them to), so
// they are recorded and compared alongside the patch size - the two move together, as
// BuildPassthroughTessControlEssl's contract says.
m_passthroughTessControlOuterLevel = MG_Pipe::gPipeInputs.IsLive()
? MG_Pipe::gPipeInputs.GetPatchDefaultOuterLevel()
: FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = MG_Pipe::gPipeInputs.IsLive()
? MG_Pipe::gPipeInputs.GetPatchDefaultInnerLevel()
: FloatVec2(1.0f, 1.0f);
if (tessEvalShaderIndex < 0 ||
static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no "
"SPIR-V for it; the pass-through control stage GL describes cannot be checked, so "
"the program is left to fail its ES link.",
src.GetExternalIndex());
m_backendProgramUsable = false;
return;
}
// The one shape the pass-through cannot stand in for. It forwards gl_Position and
// nothing else, so an evaluation stage that reads a user-defined varying or a
// per-patch input - both of which carry a Location, where every built-in it needs
// does not - would start reading undefined values the moment a control stage sat
// between it and the vertex stage. Declining keeps that from being silent; it is the
// identical rule DirectVulkan applies in ReflectPassthroughTessControlNeed.
if (MG_Util::ShaderTranspiler::ShaderCompiler::ModuleReadsLocatedInput(
shaderSpirvs[static_cast<SizeT>(tessEvalShaderIndex)])) {
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a "
"user-defined input through it; a synthesized pass-through control stage cannot "
"forward that, so the program is declined rather than fed an undefined varying.",
src.GetExternalIndex());
m_backendProgramUsable = false;
return;
}
// Mirrored from the neighbours rather than fixed: whether SPIRV-Cross redeclares
// gl_PerVertex, and with which members, depends on what the application's shaders
// touched, and a synthesized stage that redeclares a DIFFERENT shape than the stage
// it feeds is an ES link error against a program with no other problem. gl_in copies
// the vertex stage's OUT block (that is what arrives) and gl_out the evaluation
// stage's IN block (that is what is expected). A neighbour that redeclared nothing
// yields an empty list, which leaves the driver's own built-in declaration in place -
// which is exactly what matching it requires.
const String inMembers =
ExtractPerVertexBlockMembers(vertexStageEssl, /*input=*/false).value_or(String());
const String outMembers =
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
inMembers, outMembers,
m_passthroughTessControlOuterLevel,
m_passthroughTessControlInnerLevel);
// The mirrored member lists can carry gl_PointSize - the neighbour stage declared it,
// so matching it is the whole point - and a redeclaration is exactly as illegal as a
// reference in ESSL without the extension. Same directive, same never-speculative
// rule as the per-stage loop; a driver with neither spelling gets nothing added and
// fails below with its own message, which is the honest outcome for a shape it
// cannot express.
const char* passthroughPointSizeExtension =
source.find("gl_PointSize") != String::npos
? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true)
: nullptr;
source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension);
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
if (backendShaderId == 0) {
MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for "
"program %u.",
src.GetExternalIndex());
m_backendProgramUsable = false;
return;
}
const char* sourceCStr = source.c_str();
MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices "
"%u):\n%s",
src.GetExternalIndex(), patchVertices, sourceCStr);
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
g_GLESFuncs.glCompileShader(backendShaderId);
// GL_FALSE, not GL_TRUE, for the reason the per-stage loop states: an unwritten
// out-param must read as "compile failed" and never as a silent success.
GLint compileStatus = GL_FALSE;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
GLint logLength = 0;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
if (logLength < 0) logLength = 0;
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
log.back() = '\0';
MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for "
"program %u. Driver log: %s\nSource:\n%s",
src.GetExternalIndex(), log.data(), sourceCStr);
m_backendProgramUsable = false;
g_GLESFuncs.glDeleteShader(backendShaderId);
return;
}
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
// Same ownership handover as every other stage: glDeleteShader only FLAGS, so this is
// what makes the program own it and what keeps a relink from leaking it.
g_GLESFuncs.glDeleteShader(backendShaderId);
}
// ---- THE PROGRAM BUILD, AND THE ONE SOURCE IT READS (P5e pg) ----------------------
//
// ONE BODY, TWO HEADS, AND THE PULL BUILD'S HEAD IS THE ORIGINAL ONE. In the pull build
// `ProgramBuildSource` IS `MG_State::GLState::ProgramObject`, this function keeps its
// name, its signature and its null guard, and `src` is a reference to the object it was
// already dereferencing on every line - so the pull build compiles the same reads
// through the same accessors and G1's byte identity survives a rename and nothing else.
//
// In a PUSH build the body is a private worker that the two public overloads feed:
// SyncToBackend(program) - the monolith-glue half, ruling 1 - wraps the frontend's own
// archive, and SyncToBackendByHandle(cso) wraps the RECORD's. That is the whole of this
// family's rule F: after this, nothing in a program build dereferences a frontend
// object, and the forty accessor calls below read memory the server owns.
void BackendProgramObjectImpl::SyncToBackendFromSource(const ProgramBuildSource& src) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Recorded before either early return below, so Use() can always name the GL
// program a no-op draw belongs to - including the "linked but not drawable" exit.
m_frontendProgramId = src.GetExternalIndex();
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
// queryable, but has no SPIR-V to build a driver program out of. GL cannot retract
// a LINK_STATUS it already reported true, so "linked but not drawable" is the
// answer, and this is where the ES backend expresses it.
if (!src.GetLinkStatus() || !src.GetSpirvStatus()) {
MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
"program ID: %u",
src.GetExternalIndex());
return;
}
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
src.GetExternalIndex(), m_backendProgramId);
// Every link-derived cache below (incl. m_samplerUniformBindings and its
// lastAssignedUnit/lastAssignedLodBias program-state mirrors) is rebuilt;
// the sampler-pass memo keyed on them must not survive.
m_samplerPassMemo.valid = false;
m_backendProgramUsable = true;
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
// The generated ESSL bakes these in (see the SetShaderStorageBlockBinding call in the
// transpile loop below), so the set they were generated against is part of what makes
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = src.GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(src);
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
// The top is snapshotted here so every stage of this program - and the draw path
// reading it afterwards - resolves the same slot for the same GL binding.
m_atomicCounterGlBindings.clear();
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// Re-established by AttachPassthroughTessControlStage below when this program needs
// one; cleared first so a program that stops needing one (a relink that now attaches
// a real control stage) does not keep comparing against a stale patch size. The
// default levels are re-established from the same call and gated on the same -1.
m_passthroughTessControlPatchVertices = -1;
m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
// The same shape again for image FORMATS: what a format-less image declaration
// compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
// Taken BEFORE the transpile loop so both the bake and the key see one snapshot.
const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(src);
m_formatlessImageUnits = imageFormatBake.units;
m_imageUnitFormatSignature = imageFormatBake.signature;
for (const auto& conflicted : imageFormatBake.conflictedNames) {
MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with "
"different bound formats; left format-less.",
conflicted.c_str(), src.GetExternalIndex());
}
// ...and once more for image ARRAYS whose per-element units are not consecutive, which
// ESSL has no way to express in one declaration. Program-wide, like the bake, and read
// from the same snapshot of the reflection; the per-stage rewrite happens below.
const Vector<ImageArrayUnitPlan> nonConsecutiveImageArrays =
CollectNonConsecutiveImageArrayPlans(src);
// Detach all existing shaders
GLint attachedCount = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_ATTACHED_SHADERS, &attachedCount);
MGLOG_D("Currently attached shaders count: %d", attachedCount);
if (attachedCount > 0) {
Vector<GLuint> attachedShaders(attachedCount);
// Every GL out-param in this function is pre-initialized and every count is
// re-clamped after the query. A driver that returns without writing the
// out-param (no current context, a lost context, a stubbed entry point) would
// otherwise leak an uninitialized stack value straight into a container size
// or a loop bound - which is exactly how this path used to throw
// length_error out of a Vector fill-ctor.
GLsizei actualCount = 0;
g_GLESFuncs.glGetAttachedShaders(m_backendProgramId, attachedCount, &actualCount,
attachedShaders.data());
actualCount = std::clamp<GLsizei>(actualCount, 0, static_cast<GLsizei>(attachedShaders.size()));
MGLOG_D("Detaching %d existing shaders from program %u", actualCount, m_backendProgramId);
for (GLsizei i = 0; i < actualCount; ++i) {
MGLOG_D("Detaching shader ID: %u from program %u", attachedShaders[i], m_backendProgramId);
g_GLESFuncs.glDetachShader(m_backendProgramId, attachedShaders[i]);
}
}
// Attach current shaders.
//
// The EXECUTABLE's stage list, not GetAttachedShaders(): this loop indexes
// shaderSpirvs by the same running index, and the generated SPIR-V is a link
// artifact while the attach list is live. GL 4.6 core 7.3 makes glAttachShader take
// effect only at the next link, so a program that is attached to after it linked has
// MORE entries in the attach list than there are modules - and pairing the two read
// straight off the end of shaderSpirvs (a std::vector copy from garbage, which is
// how this crashed). GetLinkedShaderStages() is the list the modules were generated
// from, one entry per module, in module order.
const Vector<ShaderStage> linkedStages = src.GetLinkedShaderStages();
auto& shaderSpirvs = src.GetGeneratedSpirv();
// Both come from the same Link(), so they agree by construction. If they ever did
// not there would be no index this function could safely use for EITHER array, so
// this refuses the build instead of picking one and hoping.
if (linkedStages.size() != shaderSpirvs.size()) {
MGLOG_E_ONCE("Program %u: %zu linked stage(s) but %zu generated SPIR-V module(s); refusing to "
"build a backend program from mismatched link artifacts.",
src.GetExternalIndex(), linkedStages.size(), shaderSpirvs.size());
m_backendProgramUsable = false;
return;
}
if (src.PointSizeDemoted()) {
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that
// pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because
// every rendering assertion stays green on a healthy driver whether the
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
MGLOG_I_ONCE("DirectGLES is building programs whose tessellation/geometry gl_PointSize was "
"demoted to an ordinary varying, because this driver cannot host the built-in "
"in those stages.");
}
MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId);
for (const auto& ref : src.GetLinkedShaderSnapshot()) {
if (!ref.shader) continue;
const auto& stage =
MG_Util::ConvertGLEnumToString(MG_Util::ConvertShaderStageToGLEnum(ref.shader->GetShaderStage()));
// The source THIS link consumed, which a later glShaderSource does not disturb.
const String& src = ref.source ? *ref.source : ref.shader->GetShaderSource();
MGLOG_D("Original src @ %s: \n", stage.c_str());
MGLOG_D("%s:", src.empty() ? "" : src.c_str());
}
const Bool enableSpirvValidation = src.GetSpirvValidationEnabled();
// Blocks a transform-feedback capture request names a member of ("StageData" of
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
// then captures nothing at all for it, so those blocks - and ONLY those - get
// flattened into per-member variables below, in EVERY stage, so a producer and its
// consumer keep matching. gl_PerVertex members ("gl_Position") carry no block
// prefix and so never enter this set.
std::set<String> xfbCaptureBlockNames;
for (const auto& xfbVarying : src.GetTransformFeedbackVaryings()) {
const SizeT dot = xfbVarying.name.find('.');
if (dot != String::npos && dot > 0) {
xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot));
}
}
std::set<String> flattenedXfbBlockNames;
// Stages whose ESSL had a read+write image declaration doubled into a coherent
// read/write pair, and by how many. Empty for every program but a handful; consulted
// ONLY when the link then fails, because the doubling spends the driver's per-stage
// GL_MAX_*_IMAGE_UNIFORMS budget that MobileGL keeps advertising unadjusted (halving
// the advertised value would fail basic-api and NotSupported-out cases that never pay
// the doubling, so the limit must stay honest and the connection has to be made here
// instead). See the budget note on SplitReadWriteImageUniforms.
struct SplitImageUniformStage {
ShaderStage stage;
Uint splitCount;
};
Vector<SplitImageUniformStage> splitImageUniformStages;
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface
// blocks, so ONE stage may legally declare `in FOO {...}` and `out FOO {...}` at
// the same time - which the tessellation evaluation stage of both interface-block
// tests in KHR-GL42/43.shading_language_420pack does ("in TCSOutputBlock ... out
// TCSOutputBlock"). SPIRV-Cross keeps the same split (block_input_names vs
// block_output_names) and re-emits BOTH under the name FOO, so the generated ESSL
// declares two different blocks called FOO in one shader. Adreno's ES compiler
// keeps them apart; Mali's does not - the stage compiles, the program links, and
// the output block's payload never reaches the next stage. All 22 of that group's
// Mali failures are exactly the two tests that write this shape, and every one of
// them passes on Adreno and on DirectVulkan.
//
// The repair is a rename keyed on the PRODUCING stage, planned here and applied
// per stage below so a producer and its consumer keep naming the same block.
// Gated twice over, because a re-serialised module is not free (it cost the
// create-indirect retrace 0.15 SSIM the first time the array-input split missed
// its gate; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024): only a tessellation
// or geometry stage can declare blocks in both directions at all, and even then the
// probe has to FIND a collision before any
// stage is rewritten.
std::set<String> collidingIoBlockNames;
std::set<String> declaredIoBlockNames;
Vector<Int> stagePipelineIndices(linkedStages.size(), -1);
Bool anyStageCanDeclareBlocksInBothDirections = false;
for (SizeT index = 0; index < linkedStages.size(); ++index) {
const ShaderStage stage = linkedStages[index];
stagePipelineIndices[index] = InterStagePipelineIndex(stage);
if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true;
}
// A SECOND, INDEPENDENT interface-block repair riding the same gate, because it
// needs the same question answered: "does this program have a stage where an
// inter-stage block can go wrong?". CanDeclareBlocksInBothDirections is true for
// exactly the tessellation and geometry stages, which is also exactly the set of
// stages whose presence makes this driver drop a LOCATED block's payload (a
// vertex-to-fragment located block is fine on the same driver, measured). The two
// repairs are otherwise unrelated: the rename fixes a name collision inside ONE
// stage, this drops a qualifier from EVERY block of the program - so it does not
// wait for the collision probe to find anything.
const Bool ioBlockLocationStripArmed =
!g_GLESCapabilities.SupportsLocatedInterStageIoBlocks &&
anyStageCanDeclareBlocksInBothDirections;
if (anyStageCanDeclareBlocksInBothDirections) {
for (SizeT index = 0; index < shaderSpirvs.size(); ++index) {
MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl(
shaderSpirvs[index], collidingIoBlockNames, declaredIoBlockNames);
}
// A block a capture request names is resolved BY NAME at
// glTransformFeedbackVaryings time - and flattened away entirely by the pass
// below - so renaming one would ask the driver for a block the request does
// not spell.
for (const auto& xfbCaptureBlockName : xfbCaptureBlockNames) {
collidingIoBlockNames.erase(xfbCaptureBlockName);
}
}
// The one spelling every stage of THIS program agrees on for `blockName` as written
// by pipeline stage `producerPipelineIndex`. "__" is reserved in GLSL, so a name
// already ending in '_' does not get another one, and the digit-suffix loop steps
// off any name the program already spells.
const auto uniqueIoBlockName = [&declaredIoBlockNames](const String& blockName,
Int producerPipelineIndex) {
const char* separator = (!blockName.empty() && blockName.back() == '_') ? "" : "_";
String candidate = blockName + separator + "mgio" + std::to_string(producerPipelineIndex);
while (declaredIoBlockNames.find(candidate) != declaredIoBlockNames.end()) {
candidate += "0";
}
return candidate;
};
// Desktop GL makes the tessellation CONTROL stage optional; OpenGL ES 3.2 rejects a
// program that has an evaluation stage without one, with an empty info log. When that
// is this program's shape, one is synthesized below - and it has to be spelled to
// MATCH the two stages it sits between, so their emitted ESSL is kept here as it is
// produced. Empty for every program that has a control stage of its own, which is
// all but a handful.
Bool hasTessEvalStage = false;
Bool hasTessControlStage = false;
Int tessEvalShaderIndex = -1;
String vertexStageEssl;
String tessEvalStageEssl;
//
// Asked of the executable for the same reason the loop below indexes it: a
// tessellation evaluation shader merely ATTACHED to a linked vertex+fragment program
// is not part of what this program runs, and synthesizing a control stage for it
// would build a tessellating driver program for an executable that does not
// tessellate (and would take tessEvalShaderIndex past the end of shaderSpirvs).
for (SizeT index = 0; index < linkedStages.size(); ++index) {
const ShaderStage stage = linkedStages[index];
if (stage == ShaderStage::TessControl) hasTessControlStage = true;
if (stage == ShaderStage::TessEval) {
hasTessEvalStage = true;
tessEvalShaderIndex = static_cast<Int>(index);
}
}
const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage;
// The stage order the loop below walks, with every FRAGMENT stage moved to the end.
// The viewport-routing gate is the reason: whether a fragment stage needs one is a
// question about the OTHER stages ("does any of them still write gl_ViewportIndex?"),
// and the honest, free answer to it is the promotion the producing stage's own text
// pass just performed. Answering it any other way costs a BuildModule per
// pre-rasterization stage of every program - the parse the shared SpirvGateFeatures
// probe exists to avoid. Nothing else in the loop is order-sensitive: the two
// passthrough-tessellation sources it captures are a vertex and an evaluation stage,
// and the three sets it accumulates are unions.
Vector<SizeT> stageOrder;
stageOrder.reserve(linkedStages.size());
for (SizeT index = 0; index < linkedStages.size(); ++index) {
if (linkedStages[index] != ShaderStage::Fragment) stageOrder.push_back(index);
}
for (SizeT index = 0; index < linkedStages.size(); ++index) {
if (linkedStages[index] == ShaderStage::Fragment) stageOrder.push_back(index);
}
// Set by whichever pre-rasterization stage's demoted mg_ViewportIndex global the text
// pass turned into a varying; read by the fragment stage to decide whether to inject
// the gate that consumes it.
Bool programRoutesViewportIndex = false;
// No fragment stage, no gate - and without a gate the promotion below would only add
// an output nothing can read. That is not merely useless: in a separable program
// pipeline the fragment stage lives in a DIFFERENT program, which never saw this
// build and cannot be given a gate, so promoting there would hang an unmatched
// varying off a program to buy nothing. Both cases keep the pre-emulation behaviour,
// which is what a program with no fragment stage had anyway.
const Bool programHasFragmentStage =
std::find(linkedStages.begin(), linkedStages.end(), ShaderStage::Fragment) !=
linkedStages.end();
const Bool viewportEmulationForThisProgram =
ViewportArrayEmulationEnabled() && programHasFragmentStage;
for (const SizeT index : stageOrder) {
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(linkedStages[index]);
GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType);
if (backendShaderId == 0) {
MGLOG_E_ONCE("Failed to create backend shader for attachment.");
continue;
}
String source;
auto& spirvCode = shaderSpirvs[index];
// A samplerBuffer is core in the OpenGL 3.1+ context MobileGL advertises but needs
// ES 3.2 or EXT/OES_texture_buffer on the host. Without it SPIRV-Cross emits
// `#extension GL_EXT_texture_buffer : require` and the driver rejects both that
// and the isamplerBuffer keyword - the program never links and every draw using it
// becomes a silent no-op. Say so here, naming the stage. Deliberately unlatched:
// this is bounded by program count, and which stage failed is the whole point.
// Gated on the capability so the module walk never runs on a healthy driver.
if (!AreBufferTexturesSupported() &&
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirvCode)) {
MGLOG_E("Program %u stage %s samples a buffer texture, which this ES driver "
"cannot provide (%s). The shader will not compile and the program will "
"not link; every draw using it is a no-op.",
m_backendProgramId,
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
GetBufferTextureTierName());
m_backendProgramUsable = false;
g_GLESFuncs.glDeleteShader(backendShaderId);
continue;
}
// ---- L2 of the shader translation memo -------------------------------
// The whole DirectGLES SPIR-V pass chain plus SPIRV-Cross for this stage,
// memoized on the module bytes and on every capability bit and per-program
// input that steers them. See TranslationCache.h for the key inventory and
// for why the text-level passes below stay outside the boundary.
MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs;
esslKeyInputs.spirv = &spirvCode;
esslKeyInputs.shaderType = glShaderType;
// The EFFECTIVE arming, computed the same way TranspileSpirvToEssl computes it.
// Duplicated rather than shared because the two live on opposite sides of the
// memo boundary - and a key that disagrees with the pass it is keying is the one
// failure mode of this cache that renders wrong pixels instead of being slow.
esslKeyInputs.viewportIndexLoweringArmed =
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER ||
glShaderType == GL_GEOMETRY_SHADER) &&
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
esslKeyInputs.supportsNoperspectiveInterpolation =
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
esslKeyInputs.supportsExtendedImageFormats =
g_GLESCapabilities.SupportsExtendedImageFormats;
esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples;
esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples;
esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples;
esslKeyInputs.advertisedMaxSamples =
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
esslKeyInputs.xfbCaptureBlockNames = &xfbCaptureBlockNames;
esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName;
esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides;
esslKeyInputs.esslVersion = ResolveBackendEsslVersion();
esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop;
// THIS STAGE's share of the program-wide interface-block rename plan built above
// the loop. Resolved here, outside the memoized segment, because it is planning
// and not translation - exactly like the image-format bake map - and because that
// makes the two maps a plain function argument the L2 key can carry.
//
// KEYING ON THE RESOLVED MAPS rather than on what they were derived from
// (collidingIoBlockNames, declaredIoBlockNames, stagePipelineIndices, this
// stage's index) is deliberate: the maps ARE the pass's arguments, so they are
// exactly as fine as the pass's behaviour and no finer. Two programs whose
// collision plans differ but whose maps for THIS stage come out identical really
// do produce the same ESSL and should share the entry.
//
// A block whose other end is NOT in this program is deliberately left out of the
// plan: in a separate-shader-objects pipeline the interface it matches across
// lives in another program that never saw this plan, and renaming one side of
// THAT would break a program pipeline to repair a driver quirk. That is what the
// producer/consumer presence tests below are for - in a monolithic program both
// are trivially satisfied for every interface the collision can touch.
std::map<String, String> inputBlockRenames;
std::map<String, String> outputBlockRenames;
if (!collidingIoBlockNames.empty() && stagePipelineIndices[index] >= 0) {
const Int myPipelineIndex = stagePipelineIndices[index];
Int producerPipelineIndex = -1;
Bool hasConsumerStage = false;
for (const Int otherPipelineIndex : stagePipelineIndices) {
if (otherPipelineIndex < 0) continue;
if (otherPipelineIndex < myPipelineIndex &&
otherPipelineIndex > producerPipelineIndex) {
producerPipelineIndex = otherPipelineIndex;
}
if (otherPipelineIndex > myPipelineIndex) hasConsumerStage = true;
}
for (const auto& collidingBlockName : collidingIoBlockNames) {
if (producerPipelineIndex >= 0) {
inputBlockRenames[collidingBlockName] =
uniqueIoBlockName(collidingBlockName, producerPipelineIndex);
}
if (hasConsumerStage) {
outputBlockRenames[collidingBlockName] =
uniqueIoBlockName(collidingBlockName, myPipelineIndex);
}
}
}
esslKeyInputs.inputBlockRenames = &inputBlockRenames;
esslKeyInputs.outputBlockRenames = &outputBlockRenames;
// ...and THIS STAGE's share of the interface-block LOCATION strip, planned the
// same way and for the same reason. The gate has three parts, all of which have
// to hold before a single block loses its qualifier:
// * the driver POST measured the defect (never a renderer-string quirk list);
// * this program has a stage that can hit it - a located block between a
// vertex and a fragment stage works on the affected driver, so a program
// with neither tessellation nor geometry keeps its ESSL byte for byte;
// * for THIS stage and THIS direction, this program HAS a stage on that side
// of it. That is the same test the rename plan above makes, and the same
// approximation: it asks "is some stage of this program earlier/later than
// me", not "is the exact partner of every one of my blocks here". The two
// coincide for every program MobileGL builds, because a separable pipeline
// is flattened into one composite carrying every stage that has a shader
// (GLContext::GetProgramForDraw) and a program bound with glUseProgram has
// no partner program at all - so a stage set with a gap in it does not
// arise. Should one ever arise, this must become the nearest-stage
// resolution the rename plan computes, or the two ends of the gap would
// disagree about the qualifier.
// The direction tests deliberately mirror that plan rather than inventing a
// second rule for the same question.
Bool stripInputBlockLocations = false;
Bool stripOutputBlockLocations = false;
if (ioBlockLocationStripArmed && stagePipelineIndices[index] >= 0) {
const Int myPipelineIndex = stagePipelineIndices[index];
for (const Int otherPipelineIndex : stagePipelineIndices) {
if (otherPipelineIndex < 0) continue;
if (otherPipelineIndex < myPipelineIndex) stripInputBlockLocations = true;
if (otherPipelineIndex > myPipelineIndex) stripOutputBlockLocations = true;
}
}
esslKeyInputs.stripInputBlockLocations = stripInputBlockLocations;
esslKeyInputs.stripOutputBlockLocations = stripOutputBlockLocations;
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey;
if (MG_Util::ShaderTranspiler::ShaderTranslationCacheEnabled()) {
esslCacheKey = MG_Util::ShaderTranspiler::BuildEsslTranslationKey(esslKeyInputs);
}
std::set<String> stageFlattenedXfbBlockNames;
// Per stage, and NOT m_atomicCounterGlBindings directly: on a miss the
// transpile appends to this, on a hit the payload supplies it, and only then
// is it folded into the program-wide vector. Pointing the transpile straight
// at the member would have made the miss path and the hit path disagree about
// who owns the append.
Vector<Int> stageAtomicCounterGlBindings;
const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit =
esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr;
if (esslHit) {
source = esslHit->essl;
stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames;
stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings;
} else {
String transpileError;
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
imageFormatBake, storageBlockBindingOverrides,
inputBlockRenames, outputBlockRenames,
stripInputBlockLocations, stripOutputBlockLocations,
m_atomicCounterEsslBindingTop,
enableSpirvValidation, source,
stageFlattenedXfbBlockNames,
stageAtomicCounterGlBindings, transpileError)) {
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics
// below: one line per failing stage is bounded by program count and
// naming the stage is the entire diagnostic value. A stage that never
// reaches the driver leaves the program short of that stage, so the link
// fails with an EMPTY driver info log - the least debuggable failure
// MobileGL can produce, and what hid the whole
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
"SPIRV-Cross error: %s",
src.GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
transpileError.c_str());
m_backendProgramUsable = false;
continue;
}
if (esslCacheKey.Valid()) {
auto payload = MakeShared<MG_Util::ShaderTranspiler::EsslTranslationResult>();
payload->essl = source;
payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames;
payload->atomicCounterGlBindings = stageAtomicCounterGlBindings;
const SizeT payloadBytes =
MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload);
esslCache.Insert(
esslCacheKey,
MG_Util::ShaderTranspiler::EsslTranslationResultPtr(Move(payload)),
payloadBytes);
}
}
// Per stage, never cumulative: a fragment shader consuming the same block
// reports a name the vertex stage already reported, and its own rewrite must
// still be taken or the two stages stop matching. Done here rather than inside
// the transpile so a cache HIT contributes its names too.
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
stageFlattenedXfbBlockNames.end());
// Same rule for the atomic-counter bindings this stage declared, and for the
// same reason: the loop below de-duplicates across stages, so a hit that
// contributed nothing would silently drop a counter buffer the draw path has
// to bind.
m_atomicCounterGlBindings.insert(m_atomicCounterGlBindings.end(),
stageAtomicCounterGlBindings.begin(),
stageAtomicCounterGlBindings.end());
// Position in the chain is arbitrary: this is the only header-level rewrite, it
// edits #extension directives and never the body, and the replacement is the
// same length and stays an #extension line - so it commutes with every pass
// below, including ForceSupporterOutput's scan for the last directive. First,
// because a header concern reads better before the body ones.
source = RetargetTextureBufferExtension(std::move(source),
g_GLESCapabilities.TextureBufferSupport);
// The other header-level rewrite, and next to that one for the same reason. The
// formats it covers are both the ones the bake above put into the module and the
// ones the application declared itself - either can be outside the thirteen GLSL
// ES has in core, and neither reaches the driver without this directive.
source = RequestExtendedImageFormats(std::move(source),
imageFormatBake.needsExtendedImageFormats &&
g_GLESCapabilities.SupportsExtendedImageFormats);
// The third header-level rewrite, for the builtin SPIRV-Cross prints bare:
// gl_ViewportIndex is in no version of ESSL core, so without this directive the
// stage does not compile and the whole program - not just its viewport routing -
// is lost. The token probe keeps the line off every other program and the
// capability gate keeps it off drivers that would hard-error on an unadvertised
// name; a driver without the extension took the LowerViewportIndexPass fallback
// above and its source no longer names the builtin at all, so the two are mutually
// exclusive by construction. Read `source` BEFORE it is moved from.
// The routing emulation is the third way this can be reached and the only one
// that needs no directive: it renames the fragment stage's read onto the varying
// the producing stage now writes, a few passes below.
const Bool needsViewportArrayExtension =
g_GLESCapabilities.SupportsViewportArray &&
!(ViewportArrayEmulationEnabled() && programRoutesViewportIndex) &&
source.find("gl_ViewportIndex") != String::npos;
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
// The fourth header-level rewrite, and the same shape as the third: ESSL has no
// gl_PointSize in a tessellation or geometry stage at ANY version - 320 makes the
// stages core and still leaves the built-in behind EXT/OES_..._point_size - while
// SPIRV-Cross prints it bare. Without the directive the stage fails to compile
// with "`gl_PointSize' undeclared", which takes the whole program to program 0:
// the draw renders nothing AND glBeginTransformFeedback is rejected, so a capture
// of anything at all off that program silently comes back empty. The token probe
// keeps the line off every other program and PointSizeExtensionName returns
// nullptr - i.e. nothing is emitted - on a driver advertising neither spelling.
if (source.find("gl_PointSize") != String::npos) {
const Bool tessellationStage = glShaderType == GL_TESS_CONTROL_SHADER ||
glShaderType == GL_TESS_EVALUATION_SHADER;
if (tessellationStage || glShaderType == GL_GEOMETRY_SHADER) {
const auto tier = tessellationStage ? g_GLESCapabilities.TessellationPointSizeSupport
: g_GLESCapabilities.GeometryPointSizeSupport;
const char* pointSizeExtension = PointSizeExtensionName(tier, tessellationStage);
if (pointSizeExtension == nullptr) {
// Latched, and an ERROR rather than a warning: what follows is a
// driver compile failure whose text names a built-in the application
// never mis-spelled, and the reason is a missing driver capability
// rather than anything in the shader. Saying so here is the whole
// difference between a legible skip and an unexplained black draw.
MGLOG_E_ONCE("This driver advertises neither the EXT nor the OES %s_point_size "
"extension, so its ESSL has no gl_PointSize in a %s stage; program %u "
"will fail to compile. Point size from a non-vertex stage is not "
"available on this device.",
tessellationStage ? "tessellation" : "geometry",
tessellationStage ? "tessellation" : "geometry",
src.GetExternalIndex());
}
source = RequestPointSizeExtension(std::move(source), pointSizeExtension);
}
}
source = RebindImageUniformsToFrontendUnits(std::move(source), src);
// The completion half of the format bake, for the formats SPIRV-Cross throws on
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every
// program whose format-less images bound a format the module could carry, which
// is the normal case - those were baked into the SPIR-V above and this pass finds
// their declarations already qualified. AFTER the rebind, so the layout qualifier
// it edits is the one that already exists; BEFORE the split and the binding
// strip, so both halves of a split image inherit the format.
source = BakeImageFormatQualifiers(std::move(source),
imageFormatBake.esslFormatQualifierByUniformName);
// An image ARRAY whose elements do not sit on consecutive units cannot be spelled
// by the single layout(binding=N) the rebind above stamped: ESSL gives element k
// the unit N+k and there is no glUniform1i to correct it with. Split the array
// into one scalar declaration per element, each with its own binding. AFTER the
// rebind and the format bake, both of which look the array up by its GL uniform
// name and need the binding already there; BEFORE the read+write split, so an
// element that is both read and written is split with its own binding on it.
if (!nonConsecutiveImageArrays.empty()) {
Vector<String> declinedImageArrays;
source = RemapImageArrayElementUnits(source, nonConsecutiveImageArrays,
&declinedImageArrays);
for (const auto& declined : declinedImageArrays) {
// MGLOG_E, unlatched, like the transpile- and compile-failure diagnostics
// around it: this is the "linked, drew, produced wrong numbers, said
// nothing" shape that cost earlier waves whole days, and one line per
// declined array is bounded by program count. There is no honest GL answer
// to give instead - the frontend has already reported LINK_STATUS = true.
MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach "
"from one declaration, so this stage will read and write the WRONG units. State "
"program ID: %u, stage: %s.",
declined.c_str(), src.GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
}
}
// Wedged between those two on purpose:
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
// both halves of a split image is already the frontend texture unit (and so
// that pass never has to reason about the alias it introduces);
// * BEFORE RemoveLayoutBinding, whose keepBindingRegex recognises an image
// declaration and preserves its binding - an image unit cannot be set from
// the API in ES, so the qualifier is the only binding mechanism there is,
// and both halves of the pair have to still be carrying theirs when it runs.
// Takes no stage: the qualifier it adds is a decision about THIS text's accesses
// and the rename that keeps two stages from declaring one image uniform
// differently is keyed on that same decision, so two stages that agree still
// share one uniform (see the location-budget note on the pass).
Uint splitImageUniformCount = 0;
source = SplitReadWriteImageUniforms(source, &splitImageUniformCount);
if (splitImageUniformCount != 0) {
splitImageUniformStages.push_back({linkedStages[index], splitImageUniformCount});
}
source = RemoveLayoutBinding(source);
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe());
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
// The two halves of the gl_ViewportIndex routing emulation, next to the draw-
// parameter promotion because they are the same shape: a builtin ESSL cannot
// spell, demoted to a plain global by a SPIR-V pass, given a real interface here.
// BEFORE ForceSupporterOutput, so the `precision highp` statements it hoists to
// the top land above the declarations these inject; AFTER
// ForceFlatIntegerVaryings, which matches only declarations carrying a
// layout(...) qualifier and so cannot touch either of them.
if (viewportEmulationForThisProgram) {
if (glShaderType == GL_FRAGMENT_SHADER) {
if (programRoutesViewportIndex && !InjectViewportIndexPassGate(source)) {
// MGLOG_E, unlatched, like the transpile- and compile-failure
// diagnostics around it: the program still links and still draws, so
// nothing else in the process will ever say that its viewport routing
// silently collapsed back to one rectangle.
MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no "
"entry point to gate, so the routing cannot be emulated: every "
"index will rasterize against viewport 0. State program ID: %u.",
m_backendProgramId, src.GetExternalIndex());
}
} else if (PromoteViewportIndexGlobalToVarying(source)) {
programRoutesViewportIndex = true;
}
}
source = ForceSupporterOutput(source);
source = ClampNormFallbackOutputs(std::move(source), glShaderType,
m_snormFallbackClampOutputMask,
m_unormFallbackClampOutputMask);
// Patch for Photon compiler precision issue
String findStr = "1000000.0";
String replaceStr = "65500.0";
auto pos = source.find(findStr);
while (pos != String::npos) {
MGLOG_D("Applying patch #2 to Photon...");
source.replace(pos, findStr.length(), replaceStr);
pos = source.find(findStr, pos);
}
const char* sourceCStr = source.c_str();
MGLOG_D("Setting shader source for backend shader ID: %u\nsrc:\n%s", backendShaderId, sourceCStr);
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
g_GLESFuncs.glCompileShader(backendShaderId);
// GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed"
// and take the diagnostic path, never as a silent success that attaches an
// uncompiled shader.
GLint compileStatus = GL_FALSE;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
GLint logLength = 0;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
if (logLength < 0) logLength = 0;
// +1 and zero-filled: GL_INFO_LOG_LENGTH already counts the terminator,
// but a driver that reports 0 (or fails the query) must still leave
// log.data() a readable empty C string for the %s below.
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
log.back() = '\0';
// MGLOG_E, unlatched. This was parked at MGLOG_I while the level ordering
// compiled E and W out of every INFO build: the Android retrace artifact
// carried 294 INFO lines and zero ERROR lines while two generated shaders
// were being rejected outright, and the lane could not say why it was
// rendering an empty translucent layer. A shader the driver refuses is
// never noise.
//
// A BOUNDED EXCERPT of the source goes with it. The driver log names a line
// and a column in text that exists nowhere but here, so without any source at
// all the only way to read "`gl_PointSize' undeclared" is to rebuild the whole
// library at DEBUG - but the full dump cannot go at E either. This is not
// "one line per refused shader": SyncToBackend's rebuild gate keys on
// per-draw state (the enabled-draw-buffer count among it), so a program used
// across passes with different draw-buffer counts re-transpiles, re-compiles
// and re-fails on every alternation, i.e. per frame. At E - live at the
// production INFO level - each of those records would push the whole
// post-SPIRV-Cross ESSL through the global log mutex with a forced flush onto
// /sdcard/MG/latest.log, the file users are asked to share. The excerpt keeps
// the record O(1); the full text is still there at D, printed against this
// same backend shader id by the "Setting shader source" line above, so
// nothing needs to be dumped twice.
constexpr SizeT kMaxLoggedSourceBytes = 2048;
String truncatedSource;
const char* sourceForLog = source.c_str();
if (source.size() > kMaxLoggedSourceBytes) {
// Back up to a line boundary when there is one inside the window, so the
// excerpt ends on a whole statement rather than mid-token. Built only on
// this branch: a stage that fits keeps its own buffer and is not copied.
SizeT cut = kMaxLoggedSourceBytes;
if (const SizeT lastNewline = source.rfind('\n', cut);
lastNewline != String::npos && lastNewline > 0) {
cut = lastNewline + 1;
}
truncatedSource = source.substr(0, cut);
truncatedSource += "... [" + std::to_string(source.size() - cut) +
" more bytes; the whole stage is printed at the DEBUG level]\n";
sourceForLog = truncatedSource.c_str();
}
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
"%u, driver log: %s\nSource:\n%s",
src.GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId,
log.data(), sourceForLog);
m_backendProgramUsable = false;
// Nothing will ever attach this one, so nothing else can free it.
g_GLESFuncs.glDeleteShader(backendShaderId);
continue;
}
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
// Hand the shader's lifetime to the program, immediately and unconditionally.
//
// glDeleteShader only FLAGS a shader; the driver frees it when it is attached to
// nothing. Flagging it here is what makes the program own it, so deleting the
// program (or the detach loop above, on a relink) is what actually frees it.
// Without this call every program build leaked its shader objects for the process
// lifetime, and a relink leaked them twice - the detach loop above dropped the
// program's reference to shaders nothing had flagged, so they became unreachable
// AND undeletable. The GL swizzle conformance test builds 1,296 programs per case,
// so a handful of cases left tens of thousands of live driver shaders behind and
// the driver started mis-serving them (KHR-GL33/GL40.texture_swizzle.smoke_*).
// Same class of defect as the missing framebuffer/renderbuffer/sampler destructors
// fixed in Wave 1, and the last of that family: this is the one backend GL object
// MobileGL creates without an owning wrapper to destroy it.
g_GLESFuncs.glDeleteShader(backendShaderId);
// Kept AFTER every text-level pass, so what the synthesized control stage mirrors
// is the text the driver actually sees, not an intermediate form.
if (needsPassthroughTessControl) {
if (glShaderType == GL_VERTEX_SHADER) {
vertexStageEssl = source;
} else if (glShaderType == GL_TESS_EVALUATION_SHADER) {
tessEvalStageEssl = source;
}
}
MGLOG_D("Processed shader source length: %zu", source.length());
}
if (needsPassthroughTessControl) {
AttachPassthroughTessControlStage(src, tessEvalShaderIndex, shaderSpirvs,
vertexStageEssl, tessEvalStageEssl);
}
// A counter buffer declared by several stages was recorded once per stage; the draw
// path binds per GL binding point, so collapse the duplicates here rather than
// re-issuing the same glBindBufferBase two or three times every draw.
if (!m_atomicCounterGlBindings.empty()) {
std::sort(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end());
m_atomicCounterGlBindings.erase(
std::unique(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()),
m_atomicCounterGlBindings.end());
}
// Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend
// program before it links. SPIRV-Cross keeps user output names verbatim in
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
// frontend's requested names carry over unchanged.
SizeT declaredXfbVaryingCount = 0;
if (src.GetTransformFeedbackVaryingCount() > 0 &&
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
const auto& xfbVaryings = src.GetTransformFeedbackVaryings();
Vector<const GLchar*> xfbNames;
xfbNames.reserve(xfbVaryings.size());
// A block this build flattened no longer HAS the member the application asked
// for; it has the variable that replaced it. Everything else - including a
// member of a block that was left alone - keeps the application's spelling.
// Storage first, pointers after: xfbNames holds pointers into these strings.
//
// Same rule for a demoted gl_PointSize: the capture stage's ESSL no longer
// spells the built-in at all - the value lives in the carrier the demotion
// named - so the driver-side request has to follow it there. Only when the
// capture stage IS a demoted one (geometry, else evaluation): a program whose
// capture stage is the vertex shader keeps the built-in and its spelling,
// whatever happened to a control stage behind it.
Bool captureStageDemoted = false;
if (src.PointSizeDemoted()) {
for (const ShaderStage linkedStage : linkedStages) {
if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) {
captureStageDemoted = true;
break;
}
}
}
Vector<String> rewrittenXfbNames(xfbVaryings.size());
for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) {
String flatName;
if (!flattenedXfbBlockNames.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) {
rewrittenXfbNames[nameIndex] = std::move(flatName);
} else if (captureStageDemoted && xfbVaryings[nameIndex].name == "gl_PointSize") {
rewrittenXfbNames[nameIndex] =
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME;
} else {
rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name;
}
}
for (const auto& xfbName : rewrittenXfbNames) {
xfbNames.push_back(xfbName.c_str());
}
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
m_backendProgramId);
// Bounded: a lost context never answers GL_NO_ERROR, and this runs on the
// thread that would then spin forever.
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
xfbNames.data(),
src.GetTransformFeedbackBufferMode());
// Unchecked before. A rejected capture set leaves the program linking happily
// with NO capture set at all, and then every draw of every span records
// nothing while the application reads its buffer's pre-draw bytes and
// GL_NO_ERROR - the signature four conformance families were stuck on.
if (const GLenum xfbError = g_GLESFuncs.glGetError(); xfbError != GL_NO_ERROR) {
String declared;
for (const auto& xfbName : rewrittenXfbNames) {
if (!declared.empty()) declared += ", ";
declared += xfbName;
}
MGLOG_E("The ES driver REJECTED the transform feedback capture set for backend program %u with "
"%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(),
MG_Util::ConvertGLEnumToString(
src.GetTransformFeedbackBufferMode()).c_str(),
declared.c_str(), src.GetExternalIndex());
}
declaredXfbVaryingCount = xfbNames.size();
}
// Link program
MGLOG_D("Linking program %u", m_backendProgramId);
g_GLESFuncs.glLinkProgram(m_backendProgramId);
GLint linkStatus = GL_FALSE;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
if (linkStatus != GL_TRUE) {
GLint logLength = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
if (logLength < 0) logLength = 0;
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetProgramInfoLog(m_backendProgramId, logLength, nullptr, log.data());
log.back() = '\0';
// MGLOG_I for the same reason as the compile failure above: a program that
// links nothing no-ops every draw that uses it, and that has to be readable
// in an INFO-level artifact.
MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
src.GetExternalIndex(), m_backendProgramId, log.data());
// The one link failure MobileGL can name a cause for that the driver's log never
// will: ESSL has no legal single declaration for a read+write image outside
// r32f/r32i/r32ui, so those are split into a coherent pair and the stage ends up
// declaring more image uniforms than the application did - against a
// GL_MAX_*_IMAGE_UNIFORMS that is still the driver's raw number, because lowering
// it would fail basic-api and NotSupported-out every case that only ever uses
// readonly/writeonly images. A shader declaring more than half a stage's budget in
// read+write images therefore links here and nowhere else, and without this line
// the next reader has only a generic driver message to go on.
for (const SplitImageUniformStage& split : splitImageUniformStages) {
MGLOG_E("...and %u read+write image uniform(s) in stage %s were split into coherent "
"read/write pairs, so that stage declares %u image uniform(s) more than the "
"program did; GL_MAX_*_IMAGE_UNIFORMS for it is %d. If the driver log names "
"image uniforms, that is the cause.",
split.splitCount,
MG_Util::ConvertGLEnumToString(
MG_Util::ConvertShaderStageToGLEnum(split.stage)).c_str(),
split.splitCount, AdvertisedStageImageUniformLimit(split.stage));
}
} else {
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
// A link that SUCCEEDS can still have dropped the capture set: ESSL rejects a
// requested name the transpiled shader does not actually declare by simply not
// capturing it, and a program whose last vertex-processing stage was rewritten
// by a SPIR-V pass (viewport-index lowering, gl_PerVertex handling, the
// synthesized pass-through tessellation control stage) can end up spelling its
// outputs differently from the frontend's request. Asking the driver what it
// ACTUALLY linked is the only way to tell that apart from a driver that just
// captures nothing - which is the whole ambiguity the empty-capture failures
// across geometry_shader / tessellation_shader / gpu_shader5 / DSA sat on.
if (declaredXfbVaryingCount > 0) {
GLint linkedXfbVaryings = 0;
GLint linkedXfbBufferMode = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_VARYINGS,
&linkedXfbVaryings);
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_BUFFER_MODE,
&linkedXfbBufferMode);
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
const GLenum requestedMode = src.GetTransformFeedbackBufferMode();
if (static_cast<SizeT>(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount ||
static_cast<GLenum>(linkedXfbBufferMode) != requestedMode) {
MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not "
"agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) "
"in mode %s. Captures made with it will be empty or wrongly laid out.",
m_backendProgramId, src.GetExternalIndex(), declaredXfbVaryingCount,
MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings,
MG_Util::ConvertGLEnumToString(
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
} else {
MGLOG_D("Backend program %u capture set confirmed by the driver: %d varying(s), mode %s",
m_backendProgramId, linkedXfbVaryings,
MG_Util::ConvertGLEnumToString(
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
}
}
}
// The driver program was relinked IN PLACE, so its GL name no longer identifies
// the executable behind it - and that name is exactly what Use()'s
// g_lastUsedBackendProgramId early-out treats as identifying it. Without this,
// a rebuild of the program that is already bound issues no glUseProgram at all
// and the driver keeps running whatever the last one installed.
//
// GL 4.6 core 7.3 does promise that a successful re-link of a program in use
// installs the new executable - but only "for all shader stages where the program
// is active", and a stage the previous link did not produce is not active for
// anything. So a relink that ADDS a stage is precisely the case the promise does
// not cover. Verified with no MobileGL in the process (bare EGL + GLES 3.2, Mesa
// 26.1.4 llvmpipe): vertex+fragment linked, used and drawn renders; a geometry
// shader attached and relinked reports LINK_STATUS true with an empty info log,
// and the next draw renders NOTHING and raises no error - while the same draw
// after a fresh glUseProgram of the same name renders again.
//
// A flag rather than zeroing the guard: 0 is also the id Use() binds for a build
// that did NOT come out usable, and a zeroed guard would make it skip that
// glUseProgram(0) and leave the failed program's previous executable running -
// the silent wrong-shader draw Use() exists to prevent.
m_rebindAfterRelink = true;
m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
BASE_INSTANCE_UNIFORM_NAME);
m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME);
m_baseVertexUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
BASE_VERTEX_UNIFORM_NAME);
m_baseInstanceWordIndexUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME);
// Asked of the DRIVER rather than remembered from the injection, deliberately: the
// gate is only real if the uniform survived compilation and linking, and this is the
// one question whose answer covers both. A gate the driver optimized away would
// otherwise leave the draw path replaying passes whose mask reaches nothing, which
// renders every index's primitives in every pass.
m_viewportPassMaskUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, VIEWPORT_PASS_MASK_UNIFORM_NAME);
if (m_viewportPassMaskUniformLocation >= 0) {
// Sticky, and never cleared on a relink: it only ever short-circuits a per-draw
// check, so being late to go false costs a pointer compare and being late to go
// true would cost correctness.
g_anyProgramRoutesViewportIndex = true;
}
// The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind
// SSBO blocks after compile); record it so draws bind the indirect buffer there.
m_indirectParamsBinding = -1;
if (m_baseInstanceWordIndexUniformLocation >= 0 && g_GLESFuncs.glGetProgramResourceIndex) {
const GLuint blockIndex = g_GLESFuncs.glGetProgramResourceIndex(
m_backendProgramId, GL_SHADER_STORAGE_BLOCK, INDIRECT_PARAMS_BLOCK_NAME);
if (blockIndex != GL_INVALID_INDEX && g_GLESCapabilities.MaxShaderStorageBufferBindings > 0) {
m_indirectParamsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings - 1;
}
}
// Create global UBO
if (src.GetUBOSize() > 0) {
g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId);
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId);
g_GLESFuncs.glBufferData(GL_UNIFORM_BUFFER, src.GetUBOSize(), nullptr, GL_STREAM_DRAW);
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
} else {
m_backendGlobalUBOId = 0;
}
CacheResourceLocations(src);
// NOT the mechanism that makes a rebinding work - the transpiled qualifier above is.
// glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes,
// so this replay is a no-op almost everywhere; it stays because it is still correct
// (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL
// driver used as the ES backend. AFTER the link either way, because it needs the
// driver's linked interface.
ReseedShaderStorageBlockBindings(m_backendProgramId, src);
m_isInitialized = true;
MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId);
}
// ---- the two public heads (P5e pg) -----------------------------------------------
//
// THE CLEAN KEYS ARE STAMPED HERE AND NOT IN THE WORKER, because which key is
// authoritative is exactly what distinguishes the two arms - and a worker that stamped
// both would be claiming to know something it was not told. The monolith half keeps the
// frontend's two versions (and the record serial P4a added beside them); the handle half
// keeps the record's two serials and leaves the frontend versions at their
// never-matched sentinel, which is honest: on that arm nothing may ask a ProgramObject
// what its link version is.
void BackendProgramObjectImpl::SyncToBackend(
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateProgramObject) {
MGLOG_E_ONCE("State program object is null, skipping backend sync.");
return;
}
SyncToBackendFromSource(ProgramArchiveSource::FromFrontend(*stateProgramObject));
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
m_syncedImageUnitVersion = stateProgramObject->GetImageUnitVersion();
// P4a (D-B3): the ShaderCso record's Serial, stamped in the same breath as the two
// frontend versions it replaces. GetSyncedShaderCsoSerial() beside
// GetSyncedLinkVersion()/GetSyncedImageUnitVersion() is what the draw path's
// nine-clause rebuild condition reads on the handle arm; the clause COUNT does not
// shrink, its inputs move (D-H5).
if (ProgramSubsystemEnabled()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): MONOLITH GLUE ONLY. This is the frontend-keyed
// lookup the phase exists to retire, and it survives here for the reason
// ruling 1 (ID-81) gives: under Transport=monolith the push build keeps its
// frontend arms token for token, and this arm is only ever reached from them.
// The handle half below names no frontend identity at all.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// The reader hides the composite band, so a program-pipeline composite - which
// the server must never learn is one - resolves through the same call.
const MG_Pipe::MGPipeHandle cso = g_backendProgramObjects.HandleOf(stateProgramObject.get());
const auto* record = PipeShaderCsoRecordForHandle(cso);
if (record != nullptr) {
m_syncedShaderCsoSerial = record->Serial;
m_syncedBindingsSerial = record->BindingsSerial;
} else {
// NOT a fall-back and not a silent zero: a stamped 0 would make every later
// serial compare fire for ever, which is the safe direction but hides the
// missing record. Name it and leave the memo where it was.
MGLOG_E_ONCE("MGPipe: program %u has no shader-CSO applier record on the handle "
"arm, so its synced serial cannot be stamped (handle {%u, %u})",
stateProgramObject->GetExternalIndex(), cso.Slot, cso.Gen);
}
}
}
// P5e (pg), CONTRACT-P5E §5.5: THE BUILD THAT NAMES NO CLIENT MEMORY. Everything it
// reads is the record: the archive create_shader_state carried, the three binding tails
// set_program_bindings carried, and the descriptor. Nothing here resolves a frontend
// identity, probes the client allocator or holds a SharedPtr to a frontend object - the
// three absences rule F is made of.
void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const auto* record = PipeShaderCsoRecordForHandle(cso);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record, so no driver "
"program can be built for it",
cso.Slot, cso.Gen);
return;
}
// THE ARCHIVE IS THE RECORD'S OR THE BUILD DOES NOT HAPPEN. A record with no archive
// is one the transport arm never filled - a monolith create that reached a handle
// arm - and building from an empty reflection would produce a program with no
// uniforms and no modules, i.e. a black screen with no marker anywhere. The
// create's own trip wire already refuses a record that declares no blobs AND
// carries no artefacts (PipeApply.cpp); this is the same statement one level later,
// where the twin can name the handle.
if (!record->Archive) {
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} carries no server-owned archive; its "
"create_shader_state was emitted by the monolith arm and this is "
"the handle arm, so there is nothing to build from",
cso.Slot, cso.Gen);
m_backendProgramUsable = false;
return;
}
SyncToBackendFromSource(ProgramArchiveSource::FromRecord(cso, *record));
// The two server-owned keys the draw path's clean condition reads instead of
// GetLinkVersion()/GetImageUnitVersion(). BindingsSerial is the second because a
// glUniform1i on an IMAGE uniform is baked into the generated ESSL rather than
// re-issued per draw - the record that carries it must therefore force a rebuild,
// exactly as m_imageUnitVersion used to.
m_syncedShaderCsoSerial = record->Serial;
m_syncedBindingsSerial = record->BindingsSerial;
}
namespace {
// The GL name of the array element that lives at `location`, given the reflection
// name reported for it. Reflection reports one name per UNIFORM ("goku[0]") but
// one location per ELEMENT, so a caller walking locations sees the same name
// repeatedly; this turns it back into "goku[k]". Anything that is not an array
// (or whose base location cannot be resolved) comes back unchanged, so the only
// behaviour that moves is the array case.
String SubscriptUniformNameForElement(const ProgramBuildSource& program, const String& name,
Uint location) {
if (name.size() < 3 || name.compare(name.size() - 3, 3, "[0]") != 0) return name;
const Int base = program.GetUniformLocation(name);
if (base < 0 || static_cast<Uint>(base) > location) return name;
const Uint element = location - static_cast<Uint>(base);
if (element == 0) return name;
return name.substr(0, name.size() - 3) + "[" + std::to_string(element) + "]";
}
} // namespace
// Resolves every name-based resource lookup once per link so the per-draw path
// (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex /
// glGetUniformLocation string queries; block-to-binding-point assignments are
// program state and only need to be established here.
void BackendProgramObjectImpl::CacheResourceLocations(const ProgramBuildSource& src) {
m_globalUboBackendBlockIndex = -1;
m_globalUboBackendBlockSize = 0;
m_lastUploadedGlobalUboVersion = ~0u;
m_globalUboRingAllocation = {};
if (src.GetUBOSize() > 0) {
const Uint blockIndex =
g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
if (blockIndex != GL_INVALID_INDEX) {
m_globalUboBackendBlockIndex = static_cast<Int>(blockIndex);
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0);
// Ring bindings are ranges and must span the block as the backend
// compiled it (its std140 padding may exceed the frontend's
// SPIR-V-reflected size).
if (g_GLESFuncs.glGetActiveUniformBlockiv) {
GLint blockDataSize = 0;
g_GLESFuncs.glGetActiveUniformBlockiv(m_backendProgramId, blockIndex,
GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize);
m_globalUboBackendBlockSize = static_cast<Int>(blockDataSize);
}
} else {
MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.",
src.GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
}
}
const Int uboCount = src.GetActiveUniformBlocksCount();
m_uniformBlockBackendIndices.assign(static_cast<SizeT>(std::max(uboCount, 0)), -1);
Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO
for (Int i = 0; i < uboCount; ++i) {
++lastUBOBinding;
const auto& name = src.GetUniformBlockName(static_cast<Uint>(i));
const GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, name.c_str());
if (backendBlkIdx == GL_INVALID_INDEX) {
// Either eliminated as unused, or an SSBO block (frontend reflection
// lists those among uniform blocks); SSBO bindings are baked into the ESSL.
continue;
}
m_uniformBlockBackendIndices[static_cast<SizeT>(i)] = static_cast<Int>(backendBlkIdx);
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding);
MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u",
src.GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx,
lastUBOBinding);
}
m_samplerUniformBindings.clear();
const Uint maxUniformLoc = src.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = src.GetUniformName(loc);
if (name.empty()) continue;
const GLenum uniformType = src.GetUniformType(loc);
if (IsImageUniformType(uniformType)) {
// ES image units come exclusively from the layout(binding=N) qualifier
// (preserved in the transpiled ESSL); glUniform1i on an image uniform
// is an INVALID_OPERATION.
continue;
}
// Reflection names an array uniform after its FIRST element ("goku[0]") at
// every location the array spans, so asking the driver for that one name
// once per location hands back the same backend location N times. The
// per-draw pass then issues N glUniform1i calls against it and only the
// last element's unit survives - "layout(binding = 1) uniform sampler2D
// goku[7]" ended up with goku[0] on unit 7 and goku[1..6] still on 0.
// Address each element by its own name instead; the frontend already
// reserves one location per element, so the element index is the distance
// from the array's base location.
const String elementName = SubscriptUniformNameForElement(src, name, loc);
const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, elementName.c_str());
if (backendLoc < 0) continue;
SamplerUniformBinding binding;
binding.frontendLocation = loc;
binding.backendLocation = backendLoc;
binding.uniformType = uniformType;
binding.lastAssignedUnit = -1;
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation(
m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str());
binding.lastAssignedLodBias = 0.0f;
m_samplerUniformBindings.push_back(binding);
}
}
void BackendProgramObjectImpl::Use() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// glUseProgram on a program that did not link is an INVALID_OPERATION and
// leaves the *previous* program current, so the draw would silently render
// with an unrelated shader (KHR-GL3x.texture_size_promotion read another
// test case's alpha that way once a sampler2DRect stage failed to
// transpile). Bind nothing instead: the draw is then a visible no-op.
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
// ...unless SyncToBackend relinked this program since the last bind, in which case
// the id matching proves nothing about the executable behind it.
if (g_lastUsedBackendProgramId == programToBind && !m_rebindAfterRelink) {
return;
}
m_rebindAfterRelink = false;
if (!m_backendProgramUsable) {
// Every draw made with this program renders nothing and raises no GL error, so
// without this line the only symptom is a framebuffer that kept its clear
// colour. Latched: the early return above only dedupes CONSECUTIVE binds, so an
// app alternating a healthy and a broken program would otherwise log every
// single draw. Parked at MGLOG_I until the level ordering was fixed.
MGLOG_E_ONCE("Backend program for GL program %u is unusable (a shader failed to transpile, "
"compile or link); binding program 0 - draws with it will render nothing",
m_frontendProgramId);
}
MGLOG_D("Using program %u", programToBind);
g_GLESFuncs.glUseProgram(programToBind);
g_lastUsedBackendProgramId = programToBind;
}
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
if (m_baseInstanceUniformLocation >= 0) {
g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast<GLint>(baseInstance));
}
// A direct value disables the indirect-command-buffer read.
SetBaseInstanceWordIndex(-1);
}
// The uniform is written one-based so that its GLSL initial value, zero, already reads
// as "no indirect command" - see PromoteDrawParameterGlobalsToUniforms.
void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const {
if (m_baseInstanceWordIndexUniformLocation >= 0) {
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation,
wordIndex < 0 ? 0 : wordIndex + 1);
}
}
void BackendProgramObjectImpl::SetBaseVertex(Int32 baseVertex) const {
if (m_baseVertexUniformLocation >= 0) {
g_GLESFuncs.glUniform1i(m_baseVertexUniformLocation, baseVertex);
}
}
void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const {
if (m_drawIdUniformLocation < 0) {
return;
}
g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast<GLint>(drawId));
}
void BackendProgramObjectImpl::SetViewportPassMask(Uint32 indexMask) const {
if (m_viewportPassMaskUniformLocation < 0) {
return;
}
g_GLESFuncs.glUniform1i(m_viewportPassMaskUniformLocation, static_cast<GLint>(indexMask));
}
} // namespace PrgramImpl
namespace SamplerImpl {
BackendSamplerObject::BackendSamplerObject() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendSamplerId == 0) {
MGLOG_E_ONCE("Failed to generate sampler object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated sampler object with ID: %u.", m_backendSamplerId);
}
}
BackendSamplerObject::~BackendSamplerObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendSamplerId == 0) {
return;
}
// Scrub the unit shadow whether or not the id can still be deleted - the next
// twin can land on this heap address and would otherwise false-skip its Bind.
for (auto& boundSampler : g_boundSamplersCache) {
if (boundSampler == this) {
boundSampler = nullptr; // glDeleteSamplers unbinds from every unit
}
}
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) {
g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId);
}
m_backendSamplerId = 0;
}
void BackendSamplerObject::SyncToBackend(
const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject,
MG_Pipe::MGPipeHandle pushedCso) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateSamplerObject) {
// A CSO TWIN HAS NO FRONTEND OBJECT (P4a fable seam F-4, ResolveSamplerCsoTwin):
// the content-addressed handle is its identity and the applier's record its
// only authority, so a null object beside a live handle is the record arm and
// not the pre-P4a error. Everything below that names the object is guarded on
// it; the pull build's text is the three lines the #if brackets.
if (MG_Pipe::MGPipeHandleIsNull(pushedCso)) {
MGLOG_E_ONCE("State sampler object is null, cannot sync to backend.");
return;
}
}
// P4a (D-F1): a SamplerObject is a pure 100-byte value with no driver-side per-object
// binding state, so its CSO is CONTENT-ADDRESSED on the client at capacity 256 and
// two identical samplers share one record. What that means here is that the record's
// server-owned Serial, not the frontend object's version, is what says the values
// moved - and that the values themselves cross byte for byte INCLUDING
// borderColorForm, which is why all four border comparands below are still compared.
//
// The whole arm choice is a preprocessor #if/#else so the PULL build's text is the
// pre-P4a text token for token (D-P).
const SamplerParameters* pushedParams = nullptr;
// The GL name for the two log lines below, or 0 for a CSO twin, which has no object
// to name (F-4): the handle in the same line is its name.
const Uint samplerName = stateSamplerObject ? stateSamplerObject->GetExternalIndex() : 0u;
if (SamplerSubsystemEnabled()) {
// THE HANDLE COMES FROM THE CALLER, NOT FROM THIS TWIN'S REGISTRY, and the
// difference is the seam D's verification round found on the integrated tree.
//
// v2 asked g_backendSamplerObjects.HandleOf(object) - the twin's own identity
// handle, minted off the frontend lifetime id - and looked the record up by it.
// That can only ever miss: a SamplerCso is CONTENT-ADDRESSED on the client
// (D-F1, ID-14/ID-17), its handles come out of MGPipeSlots().Allocate keyed on a
// parameter hash, and nothing ever emits a create_sampler_state at an identity
// handle. On the integrated tree every glBindSampler-driven parameter set was
// therefore refused and the driver sampler kept its defaults - two integration
// scenarios red, and the twin's own comment below (content-addressed, the
// record's Serial is the authority) already said why it could not work.
//
// The carried fact is MGPipeApplier().BoundSamplerStates[unit], written by the
// client at bind_sampler_states; the caller that knows the unit passes it as
// `pushedCso`. See the declaration for why the parameter is push-only.
if (!MG_Pipe::MGPipeHandleIsNull(pushedCso)) {
const auto* record = PipeSamplerCsoRecordForHandle(pushedCso);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: sampler %u has no applier record on the handle arm, so its "
"parameters cannot be pushed (handle {%u, %u})",
samplerName, pushedCso.Slot, pushedCso.Gen);
return;
}
if (m_isInitialized && m_syncedSamplerSerial != 0 && m_syncedSamplerSerial == record->Serial) {
MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.",
samplerName);
return;
}
m_syncedSamplerSerial = record->Serial;
pushedParams = &record->Params;
} else {
// NO HANDLE CARRIED. Two shapes, and they are told apart by whether this
// object is in the twin registry at all:
//
// * not registered -> a sampler this BACKEND minted for its own use (the
// raw-depth-fetch sampler, DirectGLES.cpp:207-218). The client has never
// seen it, no record can exist for it now or after any package lands,
// and the object IS the authority for server-owned state. Not a seam.
// * registered -> an application sampler whose caller did not carry the
// unit's handle. That is the E-side call-site gap; it is named once and
// the values are taken from the object, which in monolith are the very
// values the client content-addressed, so the picture stays right while
// the gap is visible rather than silent.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the registration probe below resolves the
// sampler's handle by frontend identity - frontend-keyed twin resolution,
// named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (!MG_Pipe::MGPipeHandleIsNull(
g_backendSamplerObjects.HandleOf(stateSamplerObject.get()))) {
MGLOG_E_ONCE("MGPipe: sampler %u was synced without its unit's SamplerCso handle, so "
"the content-addressed record cannot be named and the object's own "
"parameters are used - the caller must pass "
"MGPipeApplier().BoundSamplerStates[unit]",
stateSamplerObject->GetExternalIndex());
}
// Spelled exactly as the pre-handle arm below spells it (compare widened,
// assign narrowed) so the two cannot drift on a version past 65535.
const Uint currentSamplerVersion = stateSamplerObject->GetVersion();
if (m_isInitialized && m_syncedSamplerVersion == currentSamplerVersion) {
MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.",
stateSamplerObject->GetExternalIndex());
return;
}
m_syncedSamplerVersion = currentSamplerVersion;
pushedParams = &stateSamplerObject->GetAllSamplerParameters();
}
} else {
// A CSO twin is only ever resolved on the handle arm (ResolveSamplerCsoTwin gates
// on SamplerSubsystemEnabled()), so a null object cannot reach the legacy body
// below; stated as a return rather than assumed, because that body dereferences
// it.
if (!stateSamplerObject) return;
// UNREACHABLE: ResolveSamplerSubsystemArm stops at its first call when the bit is
// clear and the pre-handle arm is not compiled. Kept, and kept loud.
MGLOG_E_ONCE("MGPipe: the sampler subsystem bit is clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 removed the pre-handle sampler-version "
"memo, so this configuration has no arm at all");
return;
}
MGLOG_D("Syncing sampler with backend ID %u to backend for state ID %u", m_backendSamplerId, samplerName);
const SamplerParameters& samplerParams = *pushedParams;
#define SYNC_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \
if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \
g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, glName, \
(GLint)MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \
m_cacheSamplerParameters.internalName = samplerParams.internalName; \
}
if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter ||
m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) {
g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, GL_TEXTURE_MIN_FILTER,
(GLint)ResolveBackendMinFilter(
samplerParams,
ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe()));
m_cacheSamplerParameters.minFilter = samplerParams.minFilter;
m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode;
}
if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) {
g_GLESFuncs.glSamplerParameteri(
m_backendSamplerId, GL_TEXTURE_MAG_FILTER,
(GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None));
m_cacheSamplerParameters.magFilter = samplerParams.magFilter;
}
SYNC_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode)
SYNC_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode)
SYNC_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode)
SYNC_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc)
SYNC_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode)
if (m_cacheSamplerParameters.minLod != samplerParams.minLod) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MIN_LOD, samplerParams.minLod);
m_cacheSamplerParameters.minLod = samplerParams.minLod;
}
if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod);
m_cacheSamplerParameters.maxLod = samplerParams.maxLod;
}
if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) {
if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT,
samplerParams.maxAnisotropy);
}
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor ||
m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI ||
m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI ||
m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) {
// Same gate as the texture-side border colour above, and the same reason for
// branching on the form: an integer border colour must reach the driver through
// glSamplerParameterIiv/Iuiv or an integer sampler reads the float's bit pattern
// back instead of the value.
if (g_GLESCapabilities.SupportsTextureBorderClamp) {
if (samplerParams.borderColorForm == BorderColorForm::Int &&
g_GLESFuncs.glSamplerParameterIiv) {
const GLint borderColorArray[4] = {
samplerParams.borderColorI.x(), samplerParams.borderColorI.y(),
samplerParams.borderColorI.z(), samplerParams.borderColorI.w()};
g_GLESFuncs.glSamplerParameterIiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
} else if (samplerParams.borderColorForm == BorderColorForm::Uint &&
g_GLESFuncs.glSamplerParameterIuiv) {
const GLuint borderColorArray[4] = {
samplerParams.borderColorUI.x(), samplerParams.borderColorUI.y(),
samplerParams.borderColorUI.z(), samplerParams.borderColorUI.w()};
g_GLESFuncs.glSamplerParameterIuiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
} else if (g_GLESFuncs.glSamplerParameterfv) {
const GLfloat borderColorArray[4] = {
samplerParams.borderColor.x(), samplerParams.borderColor.y(),
samplerParams.borderColor.z(), samplerParams.borderColor.w()};
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
}
}
m_cacheSamplerParameters.borderColor = samplerParams.borderColor;
m_cacheSamplerParameters.borderColorI = samplerParams.borderColorI;
m_cacheSamplerParameters.borderColorUI = samplerParams.borderColorUI;
m_cacheSamplerParameters.borderColorForm = samplerParams.borderColorForm;
}
#undef SYNC_SAMPLER_PARAM_IF_CHANGED
m_isInitialized = true;
}
void BackendSamplerObject::Bind(Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (g_boundSamplersCache[unit] == this) return;
g_GLESFuncs.glBindSampler(static_cast<GLenum>(unit), m_backendSamplerId);
g_boundSamplersCache[unit] = this;
}
Uint BackendSamplerObject::GetBackendSamplerId() const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
return m_backendSamplerId;
}
void UnbindSampler(Uint unit) {
if (g_boundSamplersCache[unit] == nullptr) return;
g_GLESFuncs.glBindSampler(static_cast<GLenum>(unit), 0);
g_boundSamplersCache[unit] = nullptr;
}
Array<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundSamplersCache;
TwinRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject, MG_Pipe::MGPipeKind::SamplerCso> g_backendSamplerObjects;
// P4a fable seam F-4. THE TWIN FOR A CONTENT-ADDRESSED SamplerCso HANDLE, keyed by that
// handle and synced from its record - see the declaration for why the identity-keyed
// lookup it replaces could never hit.
BackendSamplerObject* ResolveSamplerCsoTwin(MG_Pipe::MGPipeHandle cso) {
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
// THE RECORD FIRST, before the table is touched: a handle with no record is a seam
// defect (the client minted and named a CSO it never described, or evicted one a
// standing set still names), and adopting a slot for it would leave a twin that
// syncs nothing. The census stem is the sampler family's.
const auto* record = PipeSamplerCsoRecordForHandle(cso);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} has no applier record on the handle arm, so no "
"driver sampler can be built for it and the unit keeps what it holds",
cso.Slot, cso.Gen);
return nullptr;
}
// The same slot table the identity twins live in: one allocator serves both handle
// families of this kind, so a content-addressed slot and an identity-minted slot can
// never coincide, and the generation discipline (forward = recycle, backward =
// refused) is what retires a twin whose CSO the client's LRU evicted and re-minted.
auto* slot = g_backendSamplerObjects.GetOrCreateByHandle(cso);
if (slot == nullptr) {
MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} cannot be adopted on the handle arm (the slot's "
"live generation is %u), so no driver sampler is built for it",
cso.Slot, cso.Gen, g_backendSamplerObjects.LiveGenAt(cso.Slot));
return nullptr;
}
if (!*slot) *slot = MakeShared<BackendSamplerObject>();
// Serial-gated inside: a CSO whose record did not move since this twin last synced
// costs the record lookup above and one compare.
(*slot)->SyncToBackend(nullptr, cso);
return slot->get();
}
} // namespace SamplerImpl
namespace RenderbufferImpl {
BackendRenderbufferObject::BackendRenderbufferObject() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendRBOId == 0) {
MGLOG_E_ONCE("Failed to generate renderbuffer object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
}
}
BackendRenderbufferObject::~BackendRenderbufferObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendRBOId == 0) {
return;
}
// No driver-level renderbuffer-binding shadow exists (Bind() always issues the
// call), so there is nothing to scrub here - only the id to release.
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) {
g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId);
}
m_backendRBOId = 0;
}
void BackendRenderbufferObject::Bind() const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, m_backendRBOId);
}
// P4a (D-D2): the four values BackendRenderbufferObject::SyncToBackend allocates from,
// spelled once per arm. Macros for the reason the texture-parameter block gives: in the
// PULL build each expands to the pre-P4a expression at its original site, so that
// build's tokens and its symbol sizes are unchanged (D-P).
//
// ARCHITECTURE.md:130 keeps the renderbuffer an INDEPENDENT frontend class while giving
// it the same discriminated MGPResourceDesc a texture gets, and this is where that pays:
// the same four fields, read out of the same record type, with Desc.Target telling the
// applier which of the three per-kind tables the record lives in.
#define MGB_RBO_FORMAT \
(pushedRecord != nullptr ? static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat) \
: stateRBOObject->GetInternalFormat())
#define MGB_RBO_WIDTH (pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Width) : static_cast<Int>(stateRBOObject->GetWidth()))
#define MGB_RBO_HEIGHT \
(pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Height) : static_cast<Int>(stateRBOObject->GetHeight()))
#define MGB_RBO_SAMPLES \
(pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Samples) : static_cast<Int>(stateRBOObject->GetSamples()))
void BackendRenderbufferObject::SyncToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateRBOObject) {
MGLOG_E_ONCE("State RBO object is null, cannot sync to backend.");
return;
}
MGLOG_D("Syncing RBO with backend ID %u to backend for state ID %u", m_backendRBOId,
stateRBOObject->GetExternalIndex());
const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr;
if (TextureResourceSubsystemEnabled()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the renderbuffer's handle is resolved by
// frontend identity below - frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// MONOLITH GLUE, named as such: the Renderbuffer handle of an object this
// backend still arrives holding. Under a real split it rides in the payload.
const MG_Pipe::MGPipeHandle res = g_backendRenderbufferObjects.HandleOf(stateRBOObject.get());
pushedRecord = PipeRenderbufferRecordForHandle(res);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: renderbuffer %u has no applier record on the handle arm, so "
"its storage cannot be allocated from the pushed descriptor "
"(handle {%u, %u})",
stateRBOObject->GetExternalIndex(), res.Slot, res.Gen);
return;
}
if (m_isInitialized && m_syncedResourceSerial != 0 &&
m_syncedResourceSerial == pushedRecord->Serial) {
MGLOG_D("RBO %u already initialized with matching parameters, skipping re-allocation.",
stateRBOObject->GetExternalIndex());
return;
}
} else {
// UNREACHABLE: ResolveTextureResourceSubsystemArm stops at its first call when
// the bit is clear and the pre-handle arm is not compiled. Kept, and kept loud.
MGLOG_E_ONCE("MGPipe: the texture-resource subsystem bit is clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 removed the pre-handle renderbuffer "
"four-field cache, so this configuration has no arm at all");
return;
}
// A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3, the
// pre-handle half): glRenderbufferStorage below re-allocates behind the name the
// framebuffer twins already attached, the frontend's renderbuffer setters bump no
// version and no framebuffer version sees them, so without this the FBO memo kept
// the widening masks of the storage the renderbuffer was attached with. Same
// generation a texture re-mint takes, for the same reason; the first allocation is
// not a redefinition.
// PUSH BUILDS ONLY, for the texture twin's reason (G1: the pull library stays
// byte-identical to the P4a baseline).
if (m_isInitialized) {
++FramebufferImpl::g_attachmentBackendIdGeneration;
}
Bind();
// Allocate storage
TextureInternalFormat internalFormat = MGB_RBO_FORMAT;
Int width = MGB_RBO_WIDTH;
Int height = MGB_RBO_HEIGHT;
Int samples = MGB_RBO_SAMPLES;
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
// The allocation is deferred to here, so an ES driver that refuses it (a
// multi-gigabyte renderbuffer is refused routinely) used to leave m_isInitialized
// true over a renderbuffer with no storage and say nothing at all: the attachment
// then rendered nowhere. Drain first so the check cannot pick up an unrelated stale
// flag, and report GL_OUT_OF_MEMORY to the application. The error lands on whatever
// entry point triggered the sync rather than on glRenderbufferStorage itself, which
// is where the deferred model puts it - still far better than silence.
DebugImpl::ErrorLopper::Clear();
if (samples > 0) {
// Same clamp as the multisample texture path: the frontend accepts the count it
// advertised, the driver only takes the count it supports for this format, and
// the state object keeps reporting the requested one.
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
} else {
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
}
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s",
stateRBOObject->GetExternalIndex(), width, height, samples,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
if (MG_Pipe::gPipeInputs.IsLive()) {
MG_Pipe::gPipeInputs.RecordError(
ErrorCode::OutOfMemory,
MakeUnique<GenericErrorInfo>("DirectGLES", "BackendRenderbufferObject::SyncToBackend",
"The ES driver could not allocate the renderbuffer storage."));
}
}
DebugImpl::ErrorLopper::Clear();
m_cacheInternalFormat = internalFormat;
m_cacheWidth = width;
m_cacheHeight = height;
m_cacheSamples = samples;
// Stamped in the same breath as the four cache members and AFTER the allocation, so
// an allocation the driver refused with GL_OUT_OF_MEMORY above leaves the twin
// describing what it actually holds. The deferred OOM report and its RecordError are
// untouched: the error still lands on whatever entry point triggered the sync, which
// is where the deferred model puts it.
if (pushedRecord != nullptr) m_syncedResourceSerial = pushedRecord->Serial;
m_isInitialized = true;
MGLOG_D("RBO %u sync completed. backend ID %u", stateRBOObject->GetExternalIndex(), m_backendRBOId);
}
#undef MGB_RBO_FORMAT
#undef MGB_RBO_WIDTH
#undef MGB_RBO_HEIGHT
#undef MGB_RBO_SAMPLES
// P5e (fb, CONTRACT-P5E.md §5.4): the same allocation, keyed on the renderbuffer HANDLE.
//
// THE FOUR VALUES WERE ALREADY THE RECORD'S at P4a (D-D2) - what the object form still
// needed the frontend for was the LOOKUP, `HandleOf(stateRBOObject.get())`, a
// client-allocator probe on the apply thread. On this arm the handle arrives from the
// attachment surface that named it, so nothing is probed and nothing is dereferenced.
// An OVERLOAD rather than a changed signature (the monolith glue keeps its tokens).
void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPipeResourceRecord* const pushedRecord =
PipeRenderbufferRecordForHandle(renderbuffer);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: renderbuffer handle {%u, %u} has no applier record, so its "
"storage cannot be allocated from the pushed descriptor",
renderbuffer.Slot, renderbuffer.Gen);
return;
}
if (m_isInitialized && m_syncedResourceSerial != 0 &&
m_syncedResourceSerial == pushedRecord->Serial) {
return;
}
// A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3): the
// allocation below re-defines the storage behind the name the framebuffer twins
// already attached, and no record of THEIRS moves for it, so the attachment memo
// has to be re-armed the same way a texture re-mint arms it. The first allocation
// is not a redefinition.
if (m_isInitialized) {
++FramebufferImpl::g_attachmentBackendIdGeneration;
}
Bind();
const auto internalFormat = static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat);
const Int width = static_cast<Int>(pushedRecord->Desc.Width);
const Int height = static_cast<Int>(pushedRecord->Desc.Height);
const Int samples = static_cast<Int>(pushedRecord->Desc.Samples);
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
// Drain first, for the object form's reason: a driver that refuses the allocation
// (a multi-gigabyte renderbuffer is refused routinely) used to leave the twin
// claiming storage it does not have, and the attachment then rendered nowhere.
DebugImpl::ErrorLopper::Clear();
if (samples > 0) {
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
} else {
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
}
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
MGLOG_E_ONCE("Renderbuffer {%u, %u} storage allocation ran out of memory: %dx%d, "
"samples=%d, format=%s",
renderbuffer.Slot, renderbuffer.Gen, width, height, samples,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
// NO RecordError HERE. The object form reports the OOM to the application
// through the live GLContext; on this arm there is no application on this side
// of the wire to report it to, and gPipeInputs->RecordError is itself one of the
// BARRIER-PULLED rows (FieldOwnership.def) that rule F forbids an unbarriered
// apply to touch. The client raises its own errors from its own validator.
}
DebugImpl::ErrorLopper::Clear();
m_cacheInternalFormat = internalFormat;
m_cacheWidth = width;
m_cacheHeight = height;
m_cacheSamples = samples;
m_syncedResourceSerial = pushedRecord->Serial;
m_isInitialized = true;
}
TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
// =====================================================================================
// P5e SEAMS: DECLARED AND REFUSED HERE, BODIED BY THE FAMILY PACKAGES
// (MG_Remote/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature
// in c0e so both sides compile from day one")
// =====================================================================================
//
// WHY THEY ARE ALL IN ONE BLOCK AND WHY THE BODIES ABORT. P5e lands as eight packages in
// eight parallel worktrees, and five of them are on opposite sides of these calls: tx2
// writes SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle while fb's
// attachment sync and image sweep CALL them; fb writes the two SyncToBackendByHandle
// overloads while tx2's detach walk needs the reverse index behind them; id rekeys the
// registries the three resolvers read. A package that had to add its own declaration would
// collide with the package that added the other half, which is the merge trap P4a's
// contract package was written to avoid.
//
// So the signature is fixed HERE, once, and each family's commit replaces a body. The body
// ABORTS BY NAME rather than returning null or doing nothing: a resolver that answered null
// would render a blank draw and a sync that did nothing would render stale pixels, and both
// would be green lanes. Nothing calls any of them at this commit - every call site is still
// on the frontend overload beside it - so the abort is a link-time seam, not a runtime one.
//
// A package REPLACES the body in place and deletes the matching comment; it does not add a
// second definition elsewhere, or the linker's answer depends on link order.
namespace {
[[noreturn]] void MGPipeP5eSeamNotLanded(const char* name, const char* owner,
MG_Pipe::MGPipeHandle handle) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"} - the P5e by-handle seam is declared by "
"package c0e and bodied by package %s; it was called for handle {%u, %u} "
"before that package landed",
name, owner, handle.Slot, handle.Gen);
std::abort();
}
} // namespace
namespace TextureImpl {
// ---- P5e (tx2): THE TEXTURE FAMILY'S BY-HANDLE ENTRY, CONTRACT-P5E §5.2 --------------
//
// SamplerImpl::ResolveSamplerCsoTwin is the shape (Managers.cpp, the sampler half was
// already run-ahead clean on its record arm and this is that shape brought to the texture
// half): RECORD FIRST, twin by GetOrCreateByHandle, three syncs driven from the record,
// no frontend touch and no allocator probe anywhere on the path.
//
// WHAT THE FRONTEND ENTRY PAYS AND THIS ONE DOES NOT. SyncTextureObjectToBackend takes a
// by-VALUE copy of the twin and does a second Find at the tail, because its registry's
// Find returns a reference INTO an open-addressed map that a NESTED sync (a view syncing
// its storage texture) can rehash out from under it. A slot-table entry is an ARRAY
// ELEMENT: a nested GetOrCreateByHandle can only grow the vector, so the answer is
// re-INDEXED at the tail rather than pinned by a refcount, and the copy is a refcount
// this arm does not pay.
//
// A null return is a NAMED decline and never a mint: no record (the client named a
// texture it never created, or one whose resource_destroy has already applied), or a slot
// whose live generation is ahead of the handle's. A caller binds nothing and the unit
// keeps what it holds.
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(MG_Pipe::MGPipeHandle texture,
Bool imageBindableStorageRequired) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The one storage every decline arm can return a reference to. Never written.
static SharedPtr<BackendTextureObject> s_noTwin{};
s_noTwin = nullptr;
const auto* record = PipeTextureRecordForHandle(texture);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle "
"arm, so no driver texture can be built for it and the caller keeps "
"what it holds",
texture.Slot, texture.Gen);
return s_noTwin;
}
auto* slot = g_backendTextureObjects.GetOrCreateByHandle(texture);
if (slot == nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u, %u} cannot be adopted on the handle arm (the slot's "
"live generation is %u), so no driver texture is built for it",
texture.Slot, texture.Gen, g_backendTextureObjects.LiveGenAt(texture.Slot));
return s_noTwin;
}
if (!*slot) *slot = MakeShared<BackendTextureObject>();
// THE HANDLE IS NOTED BEFORE ANY SYNC RUNS. It is what the three prologues read
// instead of probing the client allocator, and what selects the by-handle arm inside
// them, so it has to be in place before the first of them is called.
(*slot)->NotePushedSyncHandle(texture);
const SharedPtr<BackendTextureObject> twin = *slot;
if (imageBindableStorageRequired) {
twin->RequireImageBindableStorageByHandle(texture, *record);
}
static const SharedPtr<MG_State::GLState::ITextureObject> kNoFrontendTexture{};
twin->SyncTextureParamsToBackend(kNoFrontendTexture);
twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture);
twin->SyncMipmapsToBackend(kNoFrontendTexture);
// The storage sync may RE-MINT the driver texture, which discards every parameter the
// two calls above just pushed - the frontend entry's argument, unchanged.
if (twin->NeedsParameterResync()) {
twin->SyncTextureParamsToBackend(kNoFrontendTexture);
twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture);
}
// ONE re-index, and only because a nested adopt may have GROWN the vector; nothing on
// this arm erases a live slot, so the entry itself cannot have gone.
auto* refreshed = g_backendTextureObjects.FindByHandle(texture);
if (refreshed != nullptr && *refreshed) {
return *refreshed;
}
s_noTwin = twin;
return s_noTwin;
}
void BackendTextureObject::SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture) {
// The STORAGE half alone, for fb's attachment sync and the image sweep: an attachment
// needs its levels on the driver, not its sampler parameters (§5.4). The handle note
// is what routes the body below onto the record.
NotePushedSyncHandle(texture);
static const SharedPtr<MG_State::GLState::ITextureObject> kNoFrontendTexture{};
SyncMipmapsToBackend(kNoFrontendTexture);
}
// P5e (tx2), CONTRACT-P5E §5.2. See the declaration for the clause-by-clause derivation
// and for why BOTH halves of each resync pair are read here.
Bool BackendTextureObject::IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record) const {
(void)res;
// P3b/P4b D3: THE TWO STORAGE CLAUSES ARE ASKED OF THE STORAGE RECORD, which is this
// record for every texture that owns its texels and the record named by Desc.ViewOf
// for a glTextureView. A view's own Serial is moved by nothing an upload does - the
// client keys every resource_subdata on the storage owner, and the applier moves that
// record's Serial and PendingUploads alone - so reading the view's here answered
// "clean" for every owner write after the view's first sample. The three PARAMETER
// clauses below stay this record's: a view has its own texture parameters and its own
// built-in sampler, which is the whole point of the name existing. See
// PipeTextureStorageRecordForRecord and CONTRACT-P5E.md §5.2.
const MG_Pipe::MGPipeResourceRecord* storage = PipeTextureStorageRecordForRecord(record);
if (storage == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
// Never clean while bound to a shared image: see IsDrawSyncClean.
if (m_sharedImage != nullptr) return false;
#endif
// SyncMipmapsToBackend's own early-out, verbatim.
if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != storage->Serial ||
!storage->PendingUploads.empty()) {
return false;
}
// SyncTextureParamsToBackend's (ResolvePushedTextureParams' gate).
if (m_syncedParamsSerial != record.ParamsSerial || record.Params.ForceResync != 0 ||
m_forceTextureParamsResync) {
return false;
}
// SyncBuiltinSamplerToBackend's (ResolvePushedBuiltinSampler's gate). A texture whose
// params have never been set has no built-in sampler to push and that prologue
// declines SILENTLY - so a zero ParamsSerial is clean here for the same reason, and
// the CSO lookup below is only made when there is a CSO to look up.
if (record.ParamsSerial != 0) {
const MG_Pipe::MGPipeHandle builtin = record.Params.BuiltinSampler;
if (m_syncedBuiltinSampler != builtin) return false;
const auto* cso = PipeSamplerCsoRecordForHandle(builtin);
if (cso == nullptr || m_syncedBuiltinSamplerSerial != cso->Serial) return false;
if (record.Params.SamplerResync != 0 || m_forceSamplerResync) return false;
} else if (m_forceSamplerResync) {
return false;
}
// The storage-kind restriction the frontend gate carries, from the descriptor: a
// buffer texture's backing store can move without any serial above noticing.
return static_cast<TextureStorageType>(record.Desc.StorageKind) == TextureStorageType::Mipmap;
}
} // namespace TextureImpl
// FramebufferImpl::BackendFramebufferObject::SyncToBackendByHandle and
// RenderbufferImpl::BackendRenderbufferObject::SyncToBackendByHandle LANDED with package fb
// (P5e); their bodies are beside the object forms they overload, where the attachment walk
// and the storage allocation they share can be read side by side.
// PrgramImpl's seam is GONE, not stubbed: package pg wrote
// BackendProgramObjectImpl::SyncToBackendByHandle's real body beside the frontend overload
// it is the twin of (above, with the two ProgramArchiveSource heads). A second definition
// here would make the linker's answer depend on link order, which is the one failure this
// block's own comment was written to prevent.
} // namespace MobileGL::MG_Backend::DirectGLES