[MG_Backend, MG_State, MG_Remote] (Disaggregated): P5e tx2 - the texture family by handle, the unit window, and the guard that notices

- SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle get their bodies
  (the c0e seams): record first, twin by GetOrCreateByHandle, three syncs from the
  record, no frontend touch and no allocator probe on the path.
- BackendTextureObject carries m_pushedSyncHandle (CONTRACT-P5E §4.1's m_handle), which
  is what the three sync prologues read instead of probing the client allocator with
  HandleOf, and what selects the by-handle arm inside them.
- IsDrawSyncCleanByRecord replaces IsDrawSyncClean(frontend) on the memo-hit path: the
  same conjunction over the record's serials and the twin's own, with BOTH halves of each
  resync pair read and neither clearing the other's (D-E2, scout R4).
- The unit work list is built from st.BoundSamplerViews[] and keyed on
  (ContextSerial, SamplerViewsSerial, window, backend context generation); PairingsIntact
  is not carried on it, because a {slot, gen} has no stale-pairing hazard to catch.
- ResolveAndBindUnitTextures is one pass over the window with the target from the view
  CSO, then an unbind driven by g_boundTexturesCache; the alias arbiter dies (the client
  arbitrated, SamplerEmit.h).
- BindCurrentTextures' memo key is ruling 6's both halves, entry selection included.
- The sampler pass reads BoundSamplerStates / the built-in sampler CSO / the view's
  InternalFormat; the apply-thread mint and ResolveUnitSamplerBackend become a named
  refusal under a transport (§4.2).
- GenerateMipmap resolves VerbMipRes and never the active unit; the detach sweep is keyed
  by the texture handle over the applier's framebuffer records. The row is NOT flipped to
  kWaitNone: the RGB16F/RGB32F CPU filter still reaches the client's level shadows, so
  ruling 14's escape applies and the flip is trailing.
- RequireImageBindableStorage gets a by-handle entry whose re-dirty is
  Fatal{UnmigratedEmulation, "image-bindable-redirty"} at the entry rather than three
  frames deep at a MarkStorageDirty guard.
- The buffer-texture arm resolves scout R5 with Desc.BufferForTexBuffer / BufOffset /
  BufSize and EnsureBufferResourceForHandle(nullptr, h) - that function is client-free
  under an active transport (kimi audit row 29).
- CheckUnitWindows(st) at draw and dispatch makes the §5.3 window promise checkable and
  checked: narrower than [0, MaxTouchedTextureUnit] is
  Fatal{ProtocolCorruption, "SetSamplerViews.Count"} / "BindSamplerStates.Count".
- MipmapStorage's guard list gains the read-only shape accessors P5c deliberately
  exempted, now that nothing on the draw path reads them.
This commit is contained in:
2026-09-18 07:56:34 -04:00
parent f6cfcbd36b
commit 72e7902080
5 changed files with 1287 additions and 167 deletions
+651 -87
View File
@@ -284,13 +284,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
return g_rawDepthFetchSamplerBackend.get();
}
Bool NeedsRawDepthFetchSampler(const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject,
// P5e (tx2), CONTRACT-P5E §5.3: the substitution's decision, over the fifteen sampler VALUES
// rather than over the object that happens to hold them. ARCHITECTURE.md §5.5 keeps the
// substitution itself on the server; this is only where its inputs come from, and the CSO
// record carries all three (MGPipeValueTypes.h). The SharedPtr overload below is the same
// three lines with one dereference in front, so the two arms cannot part.
Bool NeedsRawDepthFetchSampler(const SamplerParameters& samplerParams,
TextureInternalFormat textureFormat) {
if (!MG_Util::IsDepthFormatInternalFormat(textureFormat) || !samplerObject) {
if (!MG_Util::IsDepthFormatInternalFormat(textureFormat)) {
return false;
}
const auto& samplerParams = samplerObject->GetAllSamplerParameters();
if (samplerParams.compareMode != SamplerCompareMode::None) {
return false;
}
@@ -300,6 +303,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
samplerParams.magFilter != SamplerFilterMode::Nearest;
}
Bool NeedsRawDepthFetchSampler(const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject,
TextureInternalFormat textureFormat) {
if (!samplerObject) {
return false;
}
return NeedsRawDepthFetchSampler(samplerObject->GetAllSamplerParameters(), textureFormat);
}
// Frontend texture target a GLSL sampler uniform samples from. Only used to find
// which of a unit's bindings carries the GL_TEXTURE_LOD_BIAS the shader needs;
// targets with no mip chain map to Unknown so the lookup falls back to no bias.
@@ -2023,6 +2034,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
// mode here - a silent permanent fallback is, and that is what the line is for.
// The caller's MINOR-4 gate tick still counts EVERY decline, loud or not.
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.3: UNDER A LIVE WIRE THE DECLINE IS A REFUSAL.
//
// The fall-back below is the pre-handle snapshot walk over GetTextureUnitObject,
// and on a run-ahead server that walk reads client memory for a record that has
// already been answered - there is no wait left in which the answer could be
// right. Ruling 19 makes the missing window unrepresentable at the sink; this is
// the same statement one level down, for the backend that would have papered over
// it. Under monolith the fall-back is correct and stays.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && maxTouchedUnit >= 0) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - a draw "
"touches units 0..%d and neither a sampler-view nor a sampler-state "
"window has ever been applied while the sampler subsystem bit is set; "
"the pre-handle unit-bindings snapshot walk reads client memory and is "
"refused under an active transport (CONTRACT-P5E.md §5.3)",
static_cast<int>(maxTouchedUnit));
std::abort();
}
#endif
if (maxTouchedUnit >= 0) {
MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw "
"touches units 0..%d and neither a sampler-view nor a sampler-state "
@@ -2137,6 +2167,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.8 / ruling 1: THE ARM SELECTOR for every by-handle site in
// this family. Under ANY active transport (lockstep or run-ahead, so RUN_AHEAD=0 stays a
// pure wait-rule A/B on identical server code), and only with BOTH family bits set: a
// unit's texture comes from the sampler-view window and its storage from the
// texture-resource record, and half of each is not an arm. The push-monolith build keeps
// the frontend walk token for token - there is no server to answer from there.
static Bool UnitTexturesByHandle() {
return MG_Config::Transport != MG_Config::TransportMode::Monolith && SamplerSubsystemEnabled() &&
TextureResourceSubsystemEnabled();
}
// P5e (tx2), CONTRACT-P5E §5.2: THE UNIT WORK LIST WITH NO BORROWED SLOT IN IT.
//
// The pre-P5e entry borrows a pointer INTO a frontend binding slot and remembers which
// frontend object it was paired with, and PairingsIntact re-checks that pairing before
// every replay. Both exist because the list's key is derived state and a silent slot swap
// could leave a stale pairing driving texture A's twin from texture B's frontend state.
//
// A HANDLE HAS NO SUCH HAZARD. `Res` is {slot, gen}: a recycled slot arrives at a higher
// Gen, which the twin table's forward-Gen rule answers by RESETTING the twin, and a
// backward Gen is refused outright - so there is nothing for a pairing check to catch and
// PairingsIntact is not carried on this list at all. The key is the applier's own
// (ContextSerial, SamplerViewsSerial, window) plus the backend context generation, and
// SamplerViewsSerial moves IFF the resolved per-unit view set moved, which the emitter's
// content-hash suppressor is what makes sound.
//
// Its OWN list and its own key rather than a re-shape of UnitTextureSyncEntry, because
// that struct is also the draw-FBO and read-FBO attachment lists' (fb's), and one name
// carrying two shapes across two packages' worktrees is the merge trap the contract
// package exists to avoid.
struct UnitTextureSyncByHandleEntry {
MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle;
BackendTextureObject* backend = nullptr;
};
static Vector<UnitTextureSyncByHandleEntry> g_unitTextureSyncListByHandle;
static Bool g_unitTextureSyncListByHandleValid = false;
static Uint64 g_unitTextureSyncListByHandleContextSerial = 0;
static Uint64 g_unitTextureSyncListByHandleViewsSerial = 0;
static Uint32 g_unitTextureSyncListByHandleStart = 0;
static Uint32 g_unitTextureSyncListByHandleCount = 0;
static Uint g_unitTextureSyncListByHandleContextGeneration = 0;
#endif
static Vector<UnitTextureSyncEntry> g_unitTextureSyncList;
static Bool g_unitTextureSyncListValid = false;
static Uint64 g_unitTextureSyncListContextId = 0;
@@ -2332,6 +2405,68 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Int maxTouchedUnit = keys.maxTouchedUnit;
const Uint64 samplingGeneration = keys.samplingGeneration;
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.2. THE UNIT HALF, BY HANDLE.
//
// set_sampler_views is the RESOLVED answer to the question this walk asks - which
// texture does each touched unit sample - and the client has already applied every
// drop the walk below performs by hand: an image-less default texture and a texture
// that samples as incomplete are both null in the record (SamplerEmit.h's two drops),
// and the alias contest between two real textures on one native target was arbitrated
// by the program's own sampler types. So the window IS the work list.
//
// NAMED BEHAVIOUR DELTA (P5e-5, contract §5.2): this syncs the PROGRAM-RESOLVED
// texture per unit, where the frontend walk syncs every slot of every touched unit.
// Coverage is unchanged - views UNION image units UNION the FBO attachment lists
// UNION the waited texture ops still cover every texture anything reads - so it
// narrows WORK and not what reaches the driver.
if (UnitTexturesByHandle()) {
const auto& st = MG_Pipe::MGPipeApplier();
const Bool listValid = g_unitTextureSyncListByHandleValid &&
g_unitTextureSyncListByHandleContextSerial == st.ContextSerial &&
g_unitTextureSyncListByHandleViewsSerial == st.SamplerViewsSerial &&
g_unitTextureSyncListByHandleStart == st.SamplerViewStart &&
g_unitTextureSyncListByHandleCount == st.SamplerViewCount &&
g_unitTextureSyncListByHandleContextGeneration == g_backendContextGeneration;
if (listValid) {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true);
}
for (const auto& entry : g_unitTextureSyncListByHandle) {
const auto* record = PipeTextureRecordForHandle(entry.Res);
if (record == nullptr) continue;
// The aggregate gate, from the twin's own serials against the record's -
// no frontend version, no context id, no sampling generation.
if (entry.backend->IsDrawSyncCleanByRecord(entry.Res, *record)) continue;
SyncTextureToBackendByHandle(entry.Res);
}
} else {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false);
}
g_unitTextureSyncListByHandleValid = false;
g_unitTextureSyncListByHandle.clear();
const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount;
for (Uint32 index = st.SamplerViewStart;
index < windowEnd && index < st.BoundSamplerViews.size(); ++index) {
const auto& view = st.BoundSamplerViews[index];
if (MG_Pipe::MGPipeHandleIsNull(view.Texture)) continue;
auto& twin = SyncTextureToBackendByHandle(view.Texture);
if (!twin) continue;
g_unitTextureSyncListByHandle.push_back({view.Texture, twin.get()});
}
g_unitTextureSyncListByHandleContextSerial = st.ContextSerial;
g_unitTextureSyncListByHandleViewsSerial = st.SamplerViewsSerial;
g_unitTextureSyncListByHandleStart = st.SamplerViewStart;
g_unitTextureSyncListByHandleCount = st.SamplerViewCount;
g_unitTextureSyncListByHandleContextGeneration = g_backendContextGeneration;
g_unitTextureSyncListByHandleValid = true;
}
// The pre-handle list must not be replayed after this arm ran: its entries borrow
// frontend slots this arm never refreshed.
g_unitTextureSyncListValid = false;
} else
#endif
// The epoch survives redundant re-binds; the sampling-resolution generation
// covers the one membership input the epoch cannot see - a default texture's
// image appearing or vanishing flips IsUndefinedDefaultTexture with no binding
@@ -4839,6 +4974,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND);
#endif
Bool fullyResolved = true;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.2. ONE PASS OVER THE SAMPLER-VIEW WINDOW, AND THEN AN
// UNBIND OF EVERY TARGET THE WINDOW DID NOT CLAIM.
//
// The three-pass slot walk below exists to arbitrate: desktop 1D/1D-array targets alias
// ES 2D/2D-array, a unit can hold a real texture on one of an aliased pair and a default
// object on the other, and two REAL textures can want one native target - which the
// program's sampler types decide. THE CLIENT HAS ALREADY DECIDED ALL OF IT
// (SamplerEmit.h's per-unit resolution is keyed on exactly those sampler types), so the
// arbiter dies here rather than being re-implemented over records: one texture per unit,
// named by the record, with its frontend target in the view CSO.
//
// The unbind half keeps its shape and changes its driver: instead of walking the frontend
// unit's binding slots it walks the fixed TextureTarget enum against g_boundTexturesCache,
// which is SERVER memory and is the shadow every redundant-bind filter in this backend
// already trusts. That is what makes "the window did not claim it" mean "unbind it" - the
// same statement the pre-handle pass makes with `!boundBackendTargets[...]`.
if (TextureImpl::UnitTexturesByHandle()) {
const auto& st = MG_Pipe::MGPipeApplier();
const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount;
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
const Uint32 index = static_cast<Uint32>(unit);
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> boundBackendTargets{};
if (index >= st.SamplerViewStart && index < windowEnd && index < st.BoundSamplerViews.size()) {
const auto& view = st.BoundSamplerViews[index];
if (!MG_Pipe::MGPipeHandleIsNull(view.Texture)) {
// The TARGET is the view CSO's, which is the frontend target the client
// resolved this unit against - not a second derivation from the texture's
// descriptor, which would be a second authority for one value.
const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View);
const auto target = viewRecord != nullptr
? static_cast<TextureTarget>(viewRecord->View.Target)
: TextureTarget::Unknown;
if (viewRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: unit %d's sampler view {%u, %u} has no applier record, "
"so the target to bind texture {%u, %u} at is unknown; the unit "
"keeps what it holds",
static_cast<int>(unit), view.View.Slot, view.View.Gen,
view.Texture.Slot, view.Texture.Gen);
fullyResolved = false;
} else if (!TextureImpl::IsSupportedTextureTarget(target)) {
MGLOG_D(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(target).c_str());
} else if (auto* twin = TextureImpl::ResolveTextureTwin(view.Texture)) {
const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
twin->Bind(targetGL, unit);
boundBackendTargets[static_cast<SizeT>(
TextureImpl::MapToBackendTextureTarget(target))] = true;
} else {
// No twin yet: the sync pass that would have built it declined and
// named why. Not memoisable - a later draw would bind it without any
// key moving.
fullyResolved = false;
}
}
}
// The backend half of glBindTexture(..., 0) for every native target this unit no
// longer claims, driven by the server's own binding shadow.
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> visitedBackendTargets{};
for (SizeT t = 0; t < (SizeT)TextureTarget::TextureTargetCount; ++t) {
const auto target = static_cast<TextureTarget>(t);
if (!TextureImpl::IsSupportedTextureTarget(target)) continue;
const auto backendTargetIndex =
static_cast<SizeT>(TextureImpl::MapToBackendTextureTarget(target));
if (visitedBackendTargets[backendTargetIndex]) continue;
visitedBackendTargets[backendTargetIndex] = true;
if (boundBackendTargets[backendTargetIndex]) continue;
if (TextureImpl::g_boundTexturesCache[static_cast<SizeT>(unit)][backendTargetIndex] == nullptr) {
continue;
}
TextureImpl::UnbindTexture(unit, TextureImpl::ConvertTextureTargetToBackendGLEnum(target));
}
}
return fullyResolved;
}
#endif
// Frontend target the current program samples at a given unit; resolves an
// aliased native binding when two real textures compete for it (see below).
// Only consulted on a conflict, so the ordinary unit costs nothing.
@@ -4988,8 +5199,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend(
Int unit, const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §4.2's last paragraph: THE IDENTITY SAMPLER FAMILY IS DELETED
// UNDER A TRANSPORT AND BECOMES A NAMED REFUSAL.
//
// This function is the apply-thread MINT for a unit the record set does not cover: it
// probes the client allocator with HandleOf(samplerObject), keeps a WeakPtr to a frontend
// sampler in a per-unit memo, and its caller in the program pass creates a twin off the
// frontend object when the lookup misses. Every one of those is a rule-F violation, and
// the window rule (§5.3) makes the case it exists for UNREPRESENTABLE: a unit whose
// BoundSamplerStates[u] is null while the frontend held a sampler is a MISSING RECORD,
// not a unit to be served from the client's object.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - unit %d holds a "
"frontend sampler object that the applied bind_sampler_states window does not "
"describe. Minting a twin for it would probe the client's allocator from the "
"apply thread; CONTRACT-P5E.md §4.2 deletes the identity sampler family under a "
"transport and §5.3 makes the missing entry unrepresentable",
static_cast<int>(unit));
std::abort();
}
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
// scope - the refusal above is what retires it under a transport.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto& memo = g_unitSamplerLookupMemos[static_cast<SizeT>(unit)];
@@ -5099,6 +5329,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// empty window is the correct emission. All 161 hits of the unnarrowed line were
// that shape - the log said `units 0..-1` on every one of them.
if (st.SamplerStateCount == 0 && maxTouchedUnit >= 0) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.3: the same refusal CurrentUnitBindingsEpoch makes -
// the frontend walk below reads GetTextureUnitObject per unit, which an active
// transport may not do for a record whose client has moved on.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - a draw "
"touches units 0..%d and no bind_sampler_states has ever been applied "
"while the sampler subsystem bit is set; the pre-handle sampler walk "
"reads client memory and is refused under an active transport "
"(CONTRACT-P5E.md §5.3)",
static_cast<int>(maxTouchedUnit));
std::abort();
}
#endif
MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw touches "
"units 0..%d and no bind_sampler_states has ever been applied while the "
"sampler subsystem bit is set; running the pre-handle sampler walk.",
@@ -5222,6 +5466,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 programBackendStateVersion = 0;
Bool programLinked = false;
Uint contextGeneration = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 6 (ID-86): THE KEY IS BOTH HALVES.
//
// S2 argued SamplerViewsSerial alone - it already mixes the program-resolved opaque units,
// so a resolution that moved moves it - and S4 argued the program's rows. The integrator
// ruled BOTH, for the case neither covers alone: a RELINK whose resolution happens to be
// unchanged still needs the twin's new unit assignments, and SamplerViewsSerial does not
// move for it (the emitter's content hash is over the resolved set, which did not change).
//
// The frontend program rows above (pointer, lifetime id, backend-state version, link
// status) stay for the monolith arm and are simply not read on this one.
MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle;
Uint64 shaderCsoSerial = 0;
Uint64 bindingsSerial = 0;
Uint64 samplerViewsSerial = 0;
Uint64 contextSerial = 0;
Bool byHandle = false;
#endif
decltype(TextureImpl::g_boundTexturesCache) boundTextures{};
};
// Small per-PROGRAM memo set, not one global: the program is part of the key
@@ -5250,6 +5512,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Units past the frontend's high-water mark have provably-empty slots.
const Int maxTouchedUnit = keys.maxTouchedUnit;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2) / ruling 6: on the handle arm the key is the applier's rows and nothing
// frontend - INCLUDING the entry selection, which used the frontend program's address.
// maxTouchedUnit stays in it because the unbind sweep this memo replays is bounded by it.
const Bool byHandle = TextureImpl::UnitTexturesByHandle();
MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle;
Uint64 shaderCsoSerial = 0;
Uint64 bindingsSerial = 0;
Uint64 samplerViewsSerial = 0;
Uint64 contextSerial = 0;
if (byHandle) {
const auto& st = MG_Pipe::MGPipeApplier();
drawProgram = st.DrawProgram;
samplerViewsSerial = st.SamplerViewsSerial;
contextSerial = st.ContextSerial;
if (const auto* cso = PipeShaderCsoRecordForHandle(drawProgram)) {
shaderCsoSerial = cso->Serial;
bindingsSerial = cso->BindingsSerial;
}
}
#endif
// Entry selection by program pointer; a missing program takes the round-robin
// victim. WHICH entry is used is only a performance choice - correctness sits
// entirely in the full key + shadow compare below, unchanged from the single
@@ -5257,6 +5540,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* programKey = static_cast<const void*>(currentProgram.get());
ResolvedTextureBindingMemo* memoSlot = nullptr;
for (auto& candidate : g_resolvedTextureBindingMemos) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (byHandle) {
if (candidate.valid && candidate.byHandle && candidate.drawProgram.Slot == drawProgram.Slot &&
candidate.drawProgram.Gen == drawProgram.Gen) {
memoSlot = &candidate;
break;
}
continue;
}
#endif
if (candidate.valid && candidate.program == programKey) {
memoSlot = &candidate;
break;
@@ -5271,7 +5564,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT shadowBytes =
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
const Bool keysMatch = memo.valid && memo.glContextId == keys.contextId &&
#if MOBILEGL_BUILD_DISAGGREGATED
const Bool keysMatch =
byHandle ? (memo.valid && memo.byHandle && memo.maxTouchedUnit == maxTouchedUnit &&
memo.contextSerial == contextSerial && memo.samplerViewsSerial == samplerViewsSerial &&
memo.drawProgram.Slot == drawProgram.Slot && memo.drawProgram.Gen == drawProgram.Gen &&
memo.shaderCsoSerial == shaderCsoSerial && memo.bindingsSerial == bindingsSerial &&
memo.contextGeneration == g_backendContextGeneration)
:
#endif
(memo.valid && memo.glContextId == keys.contextId &&
memo.maxTouchedUnit == maxTouchedUnit &&
memo.unitBindingsEpoch == unitBindingsEpoch &&
memo.samplingResolutionGeneration == keys.samplingGeneration &&
@@ -5280,7 +5582,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.programBackendStateVersion ==
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
memo.contextGeneration == g_backendContextGeneration;
memo.contextGeneration == g_backendContextGeneration);
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
// snapshotted row count) matches.
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
@@ -5296,6 +5598,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
memo.contextGeneration = g_backendContextGeneration;
#if MOBILEGL_BUILD_DISAGGREGATED
memo.byHandle = byHandle;
memo.drawProgram = drawProgram;
memo.shaderCsoSerial = shaderCsoSerial;
memo.bindingsSerial = bindingsSerial;
memo.samplerViewsSerial = samplerViewsSerial;
memo.contextSerial = contextSerial;
#endif
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
memo.valid = true;
}
@@ -5559,6 +5869,84 @@ namespace MobileGL::MG_Backend::DirectGLES {
samplerBinding.lastAssignedUnit = unit;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.3. THE SAMPLER PASS, FROM RECORDS.
//
// Four frontend reads used to live in this loop body and every one of
// them has a carrier: the unit's sampler object is
// st.BoundSamplerStates[u] (null = the texture's built-in sampler, which
// is TextureResources[view.Texture].Params.BuiltinSampler); its fifteen
// values - lodBias among them - are the CSO record's
// (MGPipeValueTypes.h); and the sampled texture's FORMAT, which decides
// the raw-depth-fetch substitution, is
// SamplerViewCsos[View].View.InternalFormat.
//
// THE SUBSTITUTION ITSELF STAYS ON THE SERVER (ARCHITECTURE.md §5.5
// assigns it there by name); what moves is where its two inputs come
// from.
if (TextureImpl::UnitTexturesByHandle()) {
const auto& st = MG_Pipe::MGPipeApplier();
const auto index = static_cast<Uint32>(unit);
const Bool inViewWindow = index >= st.SamplerViewStart &&
index - st.SamplerViewStart < st.SamplerViewCount &&
index < st.BoundSamplerViews.size();
const MG_Pipe::MGPBoundView view =
inViewWindow ? st.BoundSamplerViews[index] : MG_Pipe::MGPBoundView{};
const Bool inStateWindow = index >= st.SamplerStateStart &&
index - st.SamplerStateStart < st.SamplerStateCount &&
index < st.BoundSamplerStates.size();
const MG_Pipe::MGPipeHandle unitSampler =
inStateWindow ? st.BoundSamplerStates[index] : MG_Pipe::kMGPipeNullHandle;
// The EFFECTIVE sampler of the unit: the bound CSO if there is one,
// else the sampled texture's built-in sampler CSO - exactly the
// override order GL states and the pre-handle body spells with
// `samplerObject ? ... : texture->GetSamplerObject()`.
MG_Pipe::MGPipeHandle effectiveCso = unitSampler;
if (MG_Pipe::MGPipeHandleIsNull(effectiveCso) &&
!MG_Pipe::MGPipeHandleIsNull(view.Texture)) {
if (const auto* texRecord = PipeTextureRecordForHandle(view.Texture)) {
effectiveCso = texRecord->Params.BuiltinSampler;
}
}
const MG_Pipe::MGPipeSamplerCsoRecord* effectiveCsoRecord =
PipeSamplerCsoRecordForHandle(effectiveCso);
if (samplerBinding.lodBiasLocation >= 0) {
const Float lodBias =
effectiveCsoRecord != nullptr ? effectiveCsoRecord->Params.lodBias : 0.0f;
if (lodBias != samplerBinding.lastAssignedLodBias) {
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
samplerBinding.lastAssignedLodBias = lodBias;
}
}
const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View);
const Bool sampledIsTexture2D =
viewRecord != nullptr &&
static_cast<TextureTarget>(viewRecord->View.Target) == TextureTarget::Texture2D;
if (samplerBinding.uniformType == GL_SAMPLER_2D && sampledIsTexture2D &&
effectiveCsoRecord != nullptr &&
NeedsRawDepthFetchSampler(
effectiveCsoRecord->Params,
static_cast<TextureInternalFormat>(viewRecord->View.InternalFormat))) {
GetRawDepthFetchSampler()->Bind(unit);
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
} else if (!MG_Pipe::MGPipeHandleIsNull(unitSampler)) {
// THE MINT IS GONE (§4.2): the unit's sampler is the CSO's own
// twin, the same one BindCurrentUnitSamplers binds, so the two
// arms cannot hand a unit back and forth between two driver
// samplers - and there is no arm left that creates a twin from a
// frontend SamplerObject.
if (auto* csoTwin = SamplerImpl::ResolveSamplerCsoTwin(unitSampler)) {
csoTwin->Bind(unit);
}
} else {
SamplerImpl::UnbindSampler(unit);
}
continue;
}
#endif
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
auto& samplerObject = textureUnit.GetSamplerObject();
const auto& texture2D =
@@ -8725,6 +9113,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two
// descriptor reads this file makes (Managers.cpp has the same inverse beside the storage
// sync; it is file-local there, and a header for eleven cases would be the wrong trade).
static TextureTarget TextureTargetForPipeResourceTarget(Uint8 pipeResourceTarget) {
switch (static_cast<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;
}
}
// P5c (hd), P5e (tx2): the verification half, with the RECORD passed in instead of re-read
// from the verb stash - the caller has it and the two must not disagree about which texture
// is being verified.
static void EnsureGenerateMipmapStorageDescribed(const MG_Pipe::MGPipeResourceRecord& record) {
const auto& desc = record.Desc;
if (desc.Width == 0 || desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
}
Uint maxDimension = desc.Width;
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(desc.Target);
if (target != MG_Pipe::MGPipeResourceTarget::Tex1D &&
target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) {
maxDimension = std::max(maxDimension, desc.Height);
}
if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) {
maxDimension = std::max(maxDimension, desc.Depth);
}
Uint requiredLevels = 1;
while (maxDimension > 1) {
maxDimension /= 2;
++requiredLevels;
}
if (desc.Levels < requiredLevels) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
}
}
#endif
static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
@@ -8735,27 +9172,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2).
const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes;
const auto* record = PipeTextureRecordForHandle(handle);
if (record == nullptr || record->Desc.Width == 0 || record->Desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
}
const auto& desc = record->Desc;
Uint maxDimension = desc.Width;
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(desc.Target);
if (target != MG_Pipe::MGPipeResourceTarget::Tex1D &&
target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) {
maxDimension = std::max(maxDimension, desc.Height);
}
if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) {
maxDimension = std::max(maxDimension, desc.Depth);
}
Uint requiredLevels = 1;
while (maxDimension > 1) {
maxDimension /= 2;
++requiredLevels;
}
if (desc.Levels < requiredLevels) {
if (record == nullptr) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
}
EnsureGenerateMipmapStorageDescribed(*record);
return false; // No server-side shadow allocation was necessary.
}
#endif
@@ -8825,6 +9245,80 @@ namespace MobileGL::MG_Backend::DirectGLES {
class ScopedDetachedTextureFramebufferAttachments {
public:
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2). THE SAME SWEEP, KEYED BY THE TEXTURE'S HANDLE.
//
// The frontend constructor below answers "which framebuffers attach this texture" by
// walking every live FBO twin's frontend FramebufferObject and comparing attachment
// SharedPtrs - kimi audit row 63, an unmemoised walk of client objects. The applier
// already holds the answer: every framebuffer's record carries its eleven MGPSurfaces
// and each one names its texture BY HANDLE (`MGPSurface::Res`), so the comparison is
// {slot, gen} against {slot, gen} and the level / layered / upload-target details the
// detach needs come off the same surface.
//
// This is the same reverse question fb's texture -> FBO-slot index answers for its
// attachment sync; here it is asked over the records directly, because the walk is on a
// rare path (per glGenerateMipmap) and does not need an index to be cheap.
explicit ScopedDetachedTextureFramebufferAttachments(MG_Pipe::MGPipeHandle textureHandle) {
if (MG_Pipe::MGPipeHandleIsNull(textureHandle)) return;
auto* twin = TextureImpl::ResolveTextureTwin(textureHandle);
if (twin == nullptr) return;
const GLuint backendTextureId = twin->GetBackendTextureId();
const auto& st = MG_Pipe::MGPipeApplier();
FramebufferImpl::g_backendFramebufferObjects.ForEachLive(
[&](MG_Pipe::MGPipeHandle fbo,
const SharedPtr<FramebufferImpl::BackendFramebufferObject>& backendFBO) {
if (!backendFBO) return;
const auto* record = st.FramebufferRecordFor(fbo);
if (record == nullptr || record->IsDefault != 0) return;
const auto detachPoint = [&](const MG_Pipe::MGPSurface& surface,
FramebufferAttachmentType frontendType) {
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
if (surface.Res.Slot != textureHandle.Slot || surface.Res.Gen != textureHandle.Gen) {
return;
}
GLenum backendAttachment = GL_NONE;
if (frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31) {
backendAttachment = backendFBO->GetBackendAttachmentType(frontendType);
} else {
backendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType);
}
if (backendAttachment == GL_NONE || backendAttachment == GL_UNKNOWN_MGL) return;
GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
static_cast<TextureUploadTarget>(surface.UploadTarget));
if (textureTarget == GL_UNKNOWN_MGL) {
textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(
static_cast<TextureTarget>(surface.TextureTarget));
}
const GLuint backendFBOId = backendFBO->GetBackendFramebufferId();
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId);
if (surface.Layered != 0) {
g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0);
} else {
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, backendAttachment,
textureTarget, 0, 0);
}
ClearGLErrors();
m_detachedAttachments.push_back({backendFBOId, backendAttachment, textureTarget,
backendTextureId, static_cast<GLint>(surface.Level),
surface.Layered != 0});
};
for (SizeT i = 0; i < 8; ++i) {
detachPoint(record->Color[i],
static_cast<FramebufferAttachmentType>(
static_cast<SizeT>(FramebufferAttachmentType::Color0) + i));
}
detachPoint(record->Depth, FramebufferAttachmentType::Depth);
detachPoint(record->Stencil, FramebufferAttachmentType::Stencil);
});
}
#endif
explicit ScopedDetachedTextureFramebufferAttachments(
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (texture == nullptr) {
@@ -9146,6 +9640,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBindTexture(dstTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2): the two CPU-blit mip chains, driven by the RECORD the verb named. What changes
// against the P5c arms below is only the resolution: those took the frontend texture and
// re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as
// barriered), and the handle is now simply passed in - the extent derivation, the per-level
// blits and the format question are §1's descriptor reads exactly as P5c wrote them.
static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
const auto& desc = record.Desc;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level);
BlitDepthTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()), textureId,
static_cast<GLint>(level), 0, 0, static_cast<GLsizei>(dstSize.x()),
static_cast<GLsizei>(dstSize.y()));
}
}
static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
const auto& desc = record.Desc;
const GLenum filter =
IsIntegerColorFormat(static_cast<TextureInternalFormat>(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level);
BlitColorTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0, static_cast<GLsizei>(srcSize.x()),
static_cast<GLsizei>(srcSize.y()), textureId, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()), filter);
}
}
#endif
static void GenerateDepthTexture2DMipmap(
const SharedPtr<MG_State::GLState::ITextureObject>& texture,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
@@ -9158,36 +9692,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Depth mipmap generation requires mipmap storage.");
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): the per-level extents are §1's derivation from the descriptor under an
// active transport - GetMipmapTexelSize is the client's per-level shape and the apply
// thread may not name it (rule E). Texture2D only, so x and y shrink and z stays 1.
// The record resolution is the same registry lookup texture sync and
// EnsureGenerateMipmapStorageAllocated's disaggregated arm already make.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle);
if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-shape");
}
const auto& desc = pushedRecord->Desc;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level);
BlitDepthTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()),
textureId, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()));
}
return;
}
#endif
// P5e (tx2): the transport arm that stood here - a scoped `HandleOf(texture.get())`
// probe to re-derive a handle the generate_mipmap record already carried (CONTRACT-P5E
// §4.4's site 9161) - is GenerateDepthTexture2DMipmapByRecord above, reached from
// GenerateMipmapByRecord with the handle passed in. This overload is the monolith body.
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
MOBILEGL_ASSERT(mipLevelCount > 0, "Depth mipmap generation requires allocated storage.");
@@ -9215,37 +9723,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Color mipmap generation requires mipmap storage.");
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): GenerateDepthTexture2DMipmap's descriptor arm, for the color filter path -
// same §1 extent derivation, same registry resolution, same refusal when the handle
// arm has no record to read.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle);
if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-shape");
}
const auto& desc = pushedRecord->Desc;
const GLenum filter =
IsIntegerColorFormat(static_cast<TextureInternalFormat>(desc.InternalFormat)) ? GL_NEAREST
: GL_LINEAR;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level);
BlitColorTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()),
textureId, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()), filter);
}
return;
}
#endif
// P5e (tx2): the transport arm that stood here is GenerateColorTexture2DMipmapByRecord
// above (CONTRACT-P5E §4.4's site 9216); this overload is the monolith body.
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
MOBILEGL_ASSERT(mipLevelCount > 0, "Color mipmap generation requires allocated storage.");
@@ -9560,6 +10039,65 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from
// VerbMipRes. Same four arms as the frontend body, with every shape question answered from
// the descriptor.
//
// RULING 14, AND WHY THE ROW IS NOT FLIPPED HERE. The unit read this arm retires is what the
// wait existed for, but ONE frontend contact is left on the path and it is not tx2's to
// retire: the RGB16F/RGB32F CPU filter reaches into the client's level shadows
// (GenerateThreeChannelFloatMipmapOnCpu, kimi audit row 61) and P8/P9 own the pull. So the
// brief's escape applies - the arm is not complete, the row stays kWaitApplied and the flip
// is listed as trailing - and the record stays barriered, which is what keeps the CPU arm's
// refusal a refusal rather than a wrong picture.
static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes,
const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target);
if (format == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(format) ||
format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) {
// The storage verification EnsureGenerateMipmapStorageAllocated's disaggregated arm
// already makes, with the record passed in rather than re-read from VerbMipRes: the
// frontend defined the generated chain before emitting this verb, so the server only
// checks that the descriptor it was given can carry it.
EnsureGenerateMipmapStorageDescribed(record);
}
if (format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) {
// The CPU filter reads and writes the CLIENT's level shadows. It already aborts
// three frames deep at the MipmapStorage guard under a transport; naming it here
// says which emulation, which is what the guard cannot.
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-filter");
}
if (IsDepthOnlyFormat(format)) {
GenerateDepthTexture2DMipmapByRecord(record, backendTexture);
return;
}
if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) {
GenerateColorTexture2DMipmapByRecord(record, backendTexture);
return;
}
const GLenum backendTarget =
TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target));
// TempTextureUnit, not the active unit: the scratch bind is the backend's own and
// BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit
// was never anything but the frontend's way of naming the texture, and the record names
// it now.
backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit);
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the
// source texture is detached from every framebuffer that attaches it - by handle, over
// the applier's own framebuffer records.
ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes);
ScopedCompleteFramebufferBinding completeFramebuffer;
ClearGLErrors();
g_GLESFuncs.glGenerateMipmap(backendTarget);
RecordGLError("glGenerateMipmap", backendTarget, format);
}
#endif
void PatchParameteri(GLenum pname, GLint value) {
if (g_GLESFuncs.glPatchParameteri == nullptr) return;
g_GLESFuncs.glPatchParameteri(pname, value);
@@ -9568,6 +10106,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GenerateMipmap(GLenum target) {
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
DebugImpl::OpenGLScopeMarker marker(__func__);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2), CONTRACT-P5E §5.2 / ruling 14 (ID-92's neighbour): GENERATE_MIPMAP RESOLVES
// ITS TEXTURE FROM VerbMipRes, NEVER FROM THE ACTIVE UNIT.
//
// The verb's own record names the texture (MGPMipPlan::Res, stashed as VerbMipRes) and
// EnsureGenerateMipmapStorageAllocated's disaggregated arm has read it since P5c. What
// stood here instead was GetActiveTextureUnit + GetTextureUnitObject + the target's
// binding slot - three BARRIER_PULLED reads to re-derive a handle the record carried -
// and they are the ONLY reason this row still waits: ClientSession.cpp parks the client
// so that the unit it reads is the unit the call was made on. With the unit read gone the
// wait has nothing left to protect, which is what lets PipeCalls.def's WaitClass column
// for GenerateMipmap move to kWaitNone.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes;
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes);
const auto* record = PipeTextureRecordForHandle(mipRes);
if (!backendTexture || record == nullptr) {
MGLOG_E_ONCE("MGPipe: generate_mipmap names texture {%u, %u}, which has no applier "
"record or no driver texture; the call is dropped",
mipRes.Slot, mipRes.Gen);
return;
}
GenerateMipmapByRecord(target, mipRes, *record, backendTexture);
return;
}
#endif
auto unitIndex = MGB_CTX->GetActiveTextureUnit();
auto& unit = MGB_CTX->GetTextureUnitObject(unitIndex);
+429 -79
View File
@@ -5874,6 +5874,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_backendTextureId;
}
#if MOBILEGL_PIPE_PUSH
// 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
// THE REFUSAL, AT THE ENTRY. The predicate is the SAME one the frontend overload
// hoisted to ahead of its first guarded contact - "would any level actually be
// replayed", answered from the server's own staged-texture store - so this changes
// WHERE the abort happens and not WHETHER it happens. What it buys is that the
// reason can still be stated: three frames deeper it was a MarkStorageDirty guard
// naming "texture-legacy-arm", which says nothing about image bindability.
//
// A texture with nothing defined yet is NOT refused: it is allocated image-bindable
// up front and pulls nothing, which is exactly what ImageBindableHint exists to make
// the common case.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
Bool anyLevelWouldReplay = false;
for (const auto& uploadTarget :
BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) {
for (Uint32 level = 0; level < record.Desc.Levels; ++level) {
if (store.IsLevelDefined(key, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyLevelWouldReplay = true;
break;
}
}
if (anyLevelWouldReplay) break;
}
if (anyLevelWouldReplay) {
MG_Pipe::MGPipeUnmigratedEmulation("image-bindable-redirty");
}
}
#else
(void)res;
(void)record;
#endif
// Nothing to replay, so the transition is just the two sticky flags plus the
// parameter resync the widening's swizzle needs - the whole of the frontend
// overload's first half, with its second half refused above.
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;
}
#endif
void BackendTextureObject::RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (m_imageBindableStorageRequired) {
@@ -6802,6 +6855,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
#if MOBILEGL_PIPE_PUSH
// 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 (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())
#else
#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (true)
#define MGB_TEXPARAM_TARGET(rec, obj) ((obj)->GetTarget())
#define MGB_TEXTURE_DIAG_NAME(rec, obj) ((obj)->GetExternalIndex())
#define MGB_TEXTURE_STORAGE_KIND(rec, obj) ((obj)->GetStorageType())
#endif
// 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
@@ -6918,6 +7009,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
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;
}
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 = record.Serial;
return;
}
// TRAILING (tx2 report, "left barriered"): the glTextureView CALL needs the view
// window and MGPResourceDesc has no carrier for it. The frontend object is taken when
// the monolith-glue note still reaches one - which is every case a single-process
// split has - and the refusal is NAMED rather than silent for the case that does not.
if (stateTextureObject) {
SyncTextureViewToBackend(stateTextureObject);
m_syncedResourceSerial = record.Serial;
return;
}
MGLOG_E_ONCE("MGPipe: texture view {%u, %u} (GL %u) has to be (re-)created as a view of "
"{%u, %u}, and MGPResourceDesc carries no view window - minLevel, "
"numLevels, minLayer, numLayers have no wire field - so the by-handle arm "
"cannot issue glTextureView. P5e leaves view CREATION on the frontend arm; "
"this name will sample empty until it gets one.",
res.Slot, res.Gen, record.Desc.GlNameForDiag, record.Desc.ViewOf.Slot,
record.Desc.ViewOf.Gen);
}
#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
@@ -7109,11 +7254,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) {
// 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_HANDLE_ARM_OFF(*this)) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
#if MOBILEGL_PIPE_PUSH
// 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;
}
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// 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::Transport != MG_Config::TransportMode::Monolith && pushedStorage != nullptr &&
!MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) {
SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject);
return;
}
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && 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
@@ -7138,24 +7331,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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 MOBILEGL_PIPE_PUSH
const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr;
MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle;
if (TextureResourceSubsystemEnabled()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the sync arrives holding the frontend object
// (the object-class rows) and resolves its handle by frontend identity -
// named debt inside the scope, P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
pushedRes = g_backendTextureObjects.HandleOf(stateTextureObject.get());
pushedStorage = PipeTextureRecordForHandle(pushedRes);
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})",
stateTextureObject->GetExternalIndex(), pushedRes.Slot, pushedRes.Gen);
return;
}
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
@@ -7179,6 +7355,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
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::Transport != MG_Config::TransportMode::Monolith) {
RequireImageBindableStorageByHandle(pushedRes, *pushedStorage);
} else
#endif
RequireImageBindableStorage(stateTextureObject);
}
@@ -7225,7 +7409,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject));
auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject);
@@ -7286,7 +7470,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (!anyDefined) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
stateTextureObject->GetExternalIndex());
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
} else
@@ -7916,7 +8100,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
"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",
stateTextureObject->GetExternalIndex(),
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject),
static_cast<Uint>(level), firstRegion.SrcRowStride,
firstRegion.SrcSliceStride, region.SrcRowStride,
region.SrcSliceStride);
@@ -7964,7 +8148,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
"%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",
stateTextureObject->GetExternalIndex(), static_cast<Uint>(level),
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));
@@ -8283,32 +8467,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
case TextureStorageType::Buffer: {
auto* textureBufferObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(stateTextureObject.get());
auto& slot = textureBufferObject->GetBufferBindingSlot();
auto& buffer = slot.GetBoundObject();
if (!buffer) {
MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.",
stateTextureObject->GetExternalIndex());
return;
}
#if MOBILEGL_PIPE_PUSH
// M-2: THE BUFFER-TEXTURE FIELDS ARE THE DESCRIPTOR'S TOO - BufferForTexBuffer
// names the backing store and BufOffset/BufSize name the window. The frontend
// binding slot above is still what hands over the BufferObject, because
// EnsureBufferResourceForHandle takes it as the one question P3a's record cannot
// answer (an emulated persistent map, D-A4's `frontend` argument) - but WHICH
// buffer, and which window of it, are read from the record.
// 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})",
stateTextureObject->GetExternalIndex(), pushedRes.Slot, pushedRes.Gen);
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), pushedRes.Slot, pushedRes.Gen);
return;
}
}
const Bool texBufferByRecord =
pushedStorage != nullptr && MGB_TEXTURE_HANDLE_ARM_OFF(*this) == false;
#else
const Bool texBufferByRecord = false;
#endif
auto bufferIndex = buffer->GetExternalIndex();
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;
#if MOBILEGL_PIPE_PUSH
// 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
@@ -8331,7 +8531,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
if (!backendBufferResource || backendBufferResource->id == 0) {
MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u",
stateTextureObject->GetExternalIndex());
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
@@ -8379,13 +8579,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
"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.",
stateTextureObject->GetExternalIndex(), GetBufferTextureTierName(),
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->GetSize(),
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
@@ -8400,13 +8600,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
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->GetSize()
? (buffer ? buffer->GetSize() : 0u)
: static_cast<SizeT>(pushedStorage->Desc.BufSize));
#else
const Bool wholeBufferRange = textureBufferObject->GetBufferRangeOffset() == 0 &&
textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize();
const SizeT rangeOffset = textureBufferObject->GetBufferRangeOffset();
const SizeT rangeSize = textureBufferObject->GetBufferRangeSizeInBytes();
#endif
@@ -8414,14 +8625,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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 (rangeOffset == 0 && rangeSize == buffer->GetSize()) {
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",
stateTextureObject->GetExternalIndex());
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
}
DebugImpl::ErrorLopper::Loop(
@@ -8443,10 +8654,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
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.",
stateTextureObject->GetExternalIndex());
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
} else {
g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId);
if (rangeOffset == 0 && rangeSize == buffer->GetSize()) {
if (wholeBufferRange) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId,
static_cast<GLintptr>(rangeOffset),
@@ -8470,8 +8681,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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>(stateTextureObject->GetStorageType()),
stateTextureObject->GetExternalIndex());
static_cast<int>(MGB_TEXTURE_STORAGE_KIND(pushedStorage, stateTextureObject)),
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
break;
}
@@ -8530,23 +8741,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
#if MOBILEGL_PIPE_PUSH
const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
// MONOLITH GLUE, named as such (HandleOfBuffer's shape): the Texture handle of an
// object this backend still arrives holding. Under a real split the handle rides in
// the payload; the client mints it off this object's lifetime id, so the allocator
// resolves it here through the table's own single-entry front memo.
// 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 - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
// 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
const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get());
const auto* record = PipeTextureRecordForHandle(res);
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})",
stateTextureObject->GetExternalIndex(), res.Slot, res.Gen);
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
@@ -8563,7 +8785,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (record->ParamsSerial == 0) {
MGLOG_D("Texture %u has no set_texture_params record yet, so it has no built-in "
"sampler to push.",
stateTextureObject->GetExternalIndex());
TextureDiagName(stateTextureObject, record));
return nullptr;
}
// D-E1: kMGPipeNullHandle in a record that WAS written is Fatal{ProtocolCorruption}
@@ -8576,14 +8798,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
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",
stateTextureObject->GetExternalIndex());
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",
stateTextureObject->GetExternalIndex(), builtin.Slot, builtin.Gen);
TextureDiagName(stateTextureObject, record), builtin.Slot, builtin.Gen);
return nullptr;
}
// THE HANDLE IS PART OF THE KEY, not just the serial. Sampler CSOs are
@@ -8618,7 +8840,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateTextureObject) {
// 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.
if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
@@ -8635,8 +8861,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// measured at -2 bytes on this function, which is a G1 failure.
#if MOBILEGL_PIPE_PUSH
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()) {
pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject);
pushedDesc = ResolveOwnRecord(stateTextureObject);
pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject, pushedDesc);
if (pendingSamplerParams == nullptr) return;
} else {
#if !MOBILEGL_PIPE_LEGACY_MEMOS
@@ -8675,11 +8905,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_forceSamplerResync = false;
#endif
#if MOBILEGL_PIPE_PUSH
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);
#else
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
m_backendTextureId, stateTextureObject->GetExternalIndex());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
#endif
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
@@ -8772,17 +9009,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_PIPE_PUSH
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get());
const auto* record = PipeTextureRecordForHandle(res);
// 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})",
stateTextureObject->GetExternalIndex(), res.Slot, res.Gen);
TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot,
m_pushedSyncHandle.Gen);
return nullptr;
}
// ParamsSerial + ForceResync replace m_syncedTextureParamsVersion +
@@ -8866,7 +9100,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateTextureObject) {
// 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_HANDLE_ARM_OFF(*this)) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
@@ -8899,7 +9135,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
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",
stateTextureObject->GetExternalIndex(),
TextureDiagName(stateTextureObject, pushedRecord),
pushedRecord->Params.BuiltinSampler.Slot,
pushedRecord->Params.BuiltinSampler.Gen);
}
@@ -8931,11 +9167,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_forceTextureParamsResync = false;
#endif
#if MOBILEGL_PIPE_PUSH
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);
#else
MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
#endif
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
@@ -13838,16 +14082,122 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // 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) {
(void)imageBindableStorageRequired;
MGPipeP5eSeamNotLanded("TextureImpl::SyncTextureToBackendByHandle", "tx2", texture);
#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) {
MGPipeP5eSeamNotLanded("BackendTextureObject::SyncMipmapsToBackendByHandle", "tx2",
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);
}
#if MOBILEGL_PIPE_PUSH
// 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;
// SyncMipmapsToBackend's own early-out, verbatim.
if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != record.Serial ||
!record.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;
}
#endif
} // namespace TextureImpl
namespace FramebufferImpl {
+102 -1
View File
@@ -1641,6 +1641,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// parameter and sampler halves are unchanged and run on this name as on any other.
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (tx2). THE VIEW ARM UNDER A TRANSPORT, and what it can and cannot answer.
//
// `Desc.ViewOf` names the storage owner BY HANDLE, so the STEADY question - "is my ES
// name still a view of the same storage name" - is answered entirely server-side: sync
// the storage twin by handle, compare its id against m_viewSourceBackendTextureId,
// stamp the record's Serial. That is the per-draw cost of every already-synced view
// and it reads nothing from the client.
//
// The CREATION of the view is the gap and it is named rather than papered over: the
// glTextureView call needs (minLevel, numLevels, minLayer, numLayers) and MGPResourceDesc
// carries none of the four - `ViewOf` is the only view field on the wire. So a view whose
// ES name has not been made a view YET, or whose storage was re-minted underneath it,
// still needs the frontend object and takes it when one is reachable; with no object it
// declines LOUDLY and the view samples its own (empty) name. Listed as trailing in the
// tx2 report with the wire fields it needs.
void SyncTextureViewToBackendByRecord(
MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
#endif
// The storage half of the sync for a texture created by glTextureView. Instead of
// allocating storage and replaying uploads, it makes this object's ES name BE a view
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
@@ -1654,6 +1674,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
void RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
#if MOBILEGL_PIPE_PUSH
// P5e (tx2), CONTRACT-P5E §5.2 / G-S2-5. THE SAME TRANSITION WITH NO FRONTEND
// ARGUMENT, and with the re-dirty NAMED AT THE ENTRY instead of three frames deep.
//
// The frontend overload's whole second half is the re-dirty: it walks the CLIENT's
// level shadows to re-arm every level the widened carrier owes. A server has no
// client address space to walk, so under a transport that half is not "not migrated
// yet", it is not expressible - ImageBindableHint is the prevention (a texture that
// has ever been image-bound is allocated in the carrier from the start) and P9 owns
// the pull. The refusal is raised here, at the entry, where the reason can still be
// stated, rather than at the MarkStorageDirty guard the loop would reach.
//
// A texture arriving here with NO backend storage yet pulls nothing - it is simply
// allocated image-bindable up front - so that case is not refused, it is the whole
// point of the hint.
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record);
#endif
// Whether this texture's ES storage was minted in an image carrier rather than in the
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
@@ -1702,6 +1740,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
return t->GetStorageType() == TextureStorageType::Mipmap;
}
#if MOBILEGL_PIPE_PUSH
// P5e (tx2), CONTRACT-P5E §5.2. THE SAME AGGREGATE GATE WITH NO FRONTEND OBJECT IN
// IT, and it is EXACTLY the conjunction of the three handle-arm prologues' own
// early-outs - the relation IsDrawSyncClean states above for the frontend versions,
// restated over the server's serials:
//
// SyncMipmapsToBackend m_isInitialized && m_syncedResourceSerial == Serial
// && PendingUploads.empty()
// SyncTextureParamsToBackend m_syncedParamsSerial == ParamsSerial
// && !Params.ForceResync && !m_forceTextureParamsResync
// SyncBuiltinSamplerToBackend m_syncedBuiltinSampler == Params.BuiltinSampler
// && m_syncedBuiltinSamplerSerial == that CSO's Serial
// && !Params.SamplerResync && !m_forceSamplerResync
//
// plus the storage-kind restriction the frontend gate carries, answered from
// Desc.StorageKind. No new member and no new wire field: every input already
// exists, which is the whole reason the memo-HIT path can stop being frontend-bound.
//
// BOTH SIDES OF THE RESYNC PAIR ARE READ AND NEITHER CLEARS THE OTHER'S (D-E2, now
// load-bearing for a GATE rather than only for a push, scout R4). `Params.ForceResync`
// and `Params.SamplerResync` are the CLIENT's bits: the server reads them, acts on
// them and never writes them back. `m_forceTextureParamsResync`/`m_forceSamplerResync`
// are the SERVER's: set by RecreateBackendTexture / RequireImageBindableStorage and
// cleared only by the prologue that consumed them. A gate that read one side would
// skip the sync the other side is asking for, which for the sampler half is not
// mis-filtering but an INCOMPLETE texture sampling (0,0,0,1).
//
// The contextId / samplingGeneration arguments are GONE: the applier's ContextSerial
// and TextureShutterSerial are what the caller's keys already carry.
Bool IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record) const;
#endif
private:
void RecreateBackendTexture();
@@ -1811,8 +1882,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
// over, and quietly re-reading the object would hide a missing record behind a
// picture that still looks right - which is precisely what the subsystem A/B exists
// to expose (MarkBufferGpuWritten's note, P3a).
//
// P5e (tx2): the HANDLE comes from m_pushedSyncHandle when the caller adopted this
// twin by handle, and only then from the client allocator - which is what retires
// the two `HandleOf` probes this pair used to make on every feature path. The record
// is resolved once by ResolveOwnRecord below and passed to the sampler half rather
// than looked up twice.
const MG_Pipe::MGPipeResourceRecord* ResolveOwnRecord(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) const;
const SamplerParameters* ResolvePushedBuiltinSampler(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject,
const MG_Pipe::MGPipeResourceRecord* record);
// Hands back the whole RECORD rather than its Params, because the parameter push
// reads two things from beside them: Desc.InternalFormat, which decides the two
// channel-widening swizzle compositions, and Params.BuiltinSampler, which is where
@@ -1854,6 +1934,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (two textures sharing one CSO, then one of them diverging).
Uint64 m_syncedBuiltinSamplerSerial = 0;
MG_Pipe::MGPipeHandle m_syncedBuiltinSampler = MG_Pipe::kMGPipeNullHandle;
// P5e (tx2), CONTRACT-P5E §4.1's `m_handle` (the m_pushedSyncHandle pattern the
// framebuffer twin already carries). THE HANDLE THIS TWIN WAS ADOPTED FOR.
//
// It is what replaces `g_backendTextureObjects.HandleOf(stateTextureObject.get())`
// in the three sync prologues: the by-handle entry point knows the handle before it
// knows anything else, so the prologues stop probing the client allocator to
// re-derive an answer the caller already had. It is ALSO the arm selector - a twin
// with a handle noted syncs from the record and tolerates a null frontend object,
// one without it is the monolith-glue half and reads the object as it always did.
//
// Stamped by SyncTextureToBackendByHandle / ResolveTextureTwin and never cleared:
// a slot recycled to {s, g+1} RESETS the twin (SlotTables.h's forward-Gen rule), so
// a stale handle cannot outlive the object it names.
MG_Pipe::MGPipeHandle m_pushedSyncHandle = MG_Pipe::kMGPipeNullHandle;
public:
void NotePushedSyncHandle(MG_Pipe::MGPipeHandle res) { m_pushedSyncHandle = res; }
MG_Pipe::MGPipeHandle PushedSyncHandle() const { return m_pushedSyncHandle; }
private:
#endif
};
+69
View File
@@ -427,6 +427,66 @@ namespace MobileGL::MG_Remote::Server {
// with a span (the client refuses "MultiDrawElements+CLIENT_INDICES" first; P8's
// HostResolve.cpp flattens it) and a multi-draw that claims instancing (no GL entry point
// produces one; the client never sends it).
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST
// COVER [0, MaxTouchedTextureUnit], AND THIS IS WHERE THAT PROMISE IS CHECKED.
//
// Everything the texture and sampler families do per draw now reads
// [SamplerViewStart, +SamplerViewCount) and [SamplerStateStart, +SamplerStateCount).
// Under run-ahead a window NARROWER than the frontend's high-water mark silently drops a
// sync of a texture the draw is about to sample, or leaves an earlier draw's sampler
// object on a unit - and a decline is exactly what run-ahead cannot take, because the
// client has already moved on and there is no wait in which to notice.
//
// A SERVER-SIDE RE-DERIVATION IS REFUSED, and that is the ruling's point: the client owns
// the high-water mark (it is the `count` argument SamplerEmit.h passes, Start=0 /
// Count=maxTouched+1) and the server's job is to check the promise, not to invent a
// second authority for it. So this is a CHECK and its failure is corruption.
//
// WHERE THE MARK COMES FROM: MGPContextValues::MaxTouchedTextureUnit, applied by
// set_context_values, which precedes the verb on the ring - the same ordering §2.1's
// XFB clause leans on. It is RECORD_SUPPLIED, so reading it is reading what a record
// put there and not client memory (rule F).
//
// WIDER IS FINE. A window larger than the mark costs a walk over provably-empty units;
// only SMALLER is unrepresentable. An empty window on a draw that touches no unit at all
// (mark 0 with nothing ever touched) is the correct and only possible emission, so a
// count of 0 is admitted exactly while the applied counters say no unit was touched.
//
// THE ONE ADMITTED SILENCE, and it is the A/B rather than a hole: a client whose sampler
// subsystem bit is CLEAR emits neither record, the backend then runs its pre-handle
// frontend walk, and there is no window to check because there is no window. That is
// "both counts are still 0", and it is distinguishable from the narrowing this refuses
// (a narrowed window carries a non-zero count that is merely too small). The moment
// either set has been received, the rule binds.
void CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return;
const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit();
if (maxTouched < 0) return;
const Uint32 required = static_cast<Uint32>(maxTouched) + 1u;
if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with "
"units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u "
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops "
"the sync of at least one texture this draw samples",
verb, static_cast<int>(maxTouched), required, st.SamplerViewStart,
st.SamplerViewCount);
std::abort();
}
if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with "
"units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u "
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves "
"an earlier draw's sampler object on at least one unit",
verb, static_cast<int>(maxTouched), required, st.SamplerStateStart,
st.SamplerStateCount);
std::abort();
}
}
} // namespace
#endif
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
@@ -449,6 +509,11 @@ namespace MobileGL::MG_Remote::Server {
}
++m_drawRecords;
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is
// asked to resolve anything out of the windows.
CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo");
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
@@ -668,6 +733,10 @@ namespace MobileGL::MG_Remote::Server {
// identical - and it is also the honest statement of the debt, which `rsp` counts.
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does.
CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid");
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
@@ -36,6 +36,21 @@ namespace MobileGL {
// The message names the TEXTURE-side method, not this storage's: the pinned
// surface list (MG_Test/Wire/RemoteClientTest.cpp's RemoteGuards) is written
// in TextureObjectMipmap's vocabulary.
//
// P5e (tx2): THE READ-ONLY SHAPE ACCESSORS JOIN THE LIST. P5c guarded the
// MUTATORS and deliberately exempted the shape reads, with the reason written
// into RemoteClientTest: "the per-draw binding walk reads them every draw", so
// guarding them would have aborted a lane that was still correct. P5e is the
// commit that makes that untrue - the unit work list, the clean gate and the
// three sync bodies read the resource RECORD and the server's staged-texture
// store, and nothing on the draw path asks the frontend object for a level count,
// a level extent, a level byte size or a compressed level again. So the exemption
// becomes a guard, and reverting any handle arm aborts here BY ACCESSOR NAME
// instead of rendering from client memory.
//
// A client-thread caller is unaffected (this is the frontend's own state, and the
// guard returns unless ServerLoop::OnApplyThread()); Magma's texture sync keeps
// its named scope exemption; the pull build compiles none of it.
void RefuseLegacyTextureArmFromApplyThread(const char* surface) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return;
@@ -97,6 +112,9 @@ namespace MobileGL {
#endif
SizeT MipmapStorage::GetLevelCount() const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetMipmapLevelCount");
#endif
return m_data.size();
}
@@ -171,16 +189,25 @@ namespace MobileGL {
}
GLenum MipmapStorage::GetCompressedFormat(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetCompressedFormat");
#endif
if (level >= m_compressedFormats.size()) return GL_NONE;
return m_compressedFormats[level];
}
SizeT MipmapStorage::GetCompressedByteSize(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetCompressedByteSize");
#endif
if (level >= m_compressedData.size()) return 0;
return m_compressedData[level].size();
}
const void* MipmapStorage::MapCompressedData(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("MapCompressedMipmapData");
#endif
if (level >= m_compressedData.size()) return nullptr;
return m_compressedData[level].data();
}
@@ -191,6 +218,9 @@ namespace MobileGL {
}
GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetRequestedCompressedFormat");
#endif
if (level >= m_requestedCompressedFormats.size()) return GL_NONE;
return m_requestedCompressedFormats[level];
}
@@ -239,12 +269,18 @@ namespace MobileGL {
}
IntVec3 MipmapStorage::GetTexelSize(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetMipmapTexelSize");
#endif
auto& targetTexelSizes = m_texelSizes;
if (level >= targetTexelSizes.size()) return {0, 0, 0};
return targetTexelSizes[level];
}
SizeT MipmapStorage::GetByteSize(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetMipmapByteSize");
#endif
if (level >= m_data.size()) return 0;
return m_data[level].size();
}