[Fix] (MG_State, BufferObject): publish the live-host-writes bit on the RISING edge too - the only content record a coherent persistent map emits is the push, and a draw-clean answer skips the ensure the push lives in and then latches it

This commit is contained in:
2026-09-11 15:48:28 -04:00
parent db1a5dbc27
commit 3a5e26215d
2 changed files with 82 additions and 47 deletions
@@ -259,6 +259,14 @@ namespace MobileGL::MG_State::GLState {
if (void* base = MG_Pipe::MGPipeEmitMapPersistent(*this)) m_resource.AdoptPersistentMap(base);
return;
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// R-6 IS "ALWAYS", AND THIS IS ITS SECOND DOOR. MGPipeApplyMapPersistent declines
// every acquisition under split - but a split build whose backend registered no
// MGPipe resource ops falls through to the LEGACY hook below, which would mint a real
// pointer and adopt it. A donated address is meaningless across a process, and an
// inproc lane that adopted would be green for a reason spawn cannot reproduce.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
#endif
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
@@ -403,12 +411,16 @@ namespace MobileGL::MG_State::GLState {
if (size == 0) return;
NotifySubData(offset, size);
if (MG_Util::PipeStats::Enabled()) {
// THE persistent-map-push SITE. It is the bytes an application wrote through a
// map with no API call and that a split build therefore has to ship - the bytes
// PipeStats.cpp's inventory says stage-buffer explicitly does NOT cover, so the
// two classes do not double-count: stage-buffer is what a BACKEND stages out of
// its own shadow, this is what the CLIENT ships because it has no shadow on the
// far side.
// THE persistent-map-push SITE: the bytes an application wrote through a map with
// no API call, which a split build therefore has to ship.
//
// THESE BYTES ARE ALSO COUNTED AS stage-buffer, and that overlap is stated in the
// inventory rather than avoided. At tier T2 a pushed block IS an ordinary
// resource_subdata, so it reaches Ops_H_SubData, is queued into pendingRanges and
// is staged like any other write. Subtracting it at the staging site would make
// stage-buffer under-report what the BACKEND actually moves, which is the question
// that class exists to answer; the two counters are different questions about the
// same bytes and PipeStats.cpp:33-35 now says so.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush,
static_cast<Uint64>(size));
}
@@ -418,45 +430,48 @@ namespace MobileGL::MG_State::GLState {
if (!MG_Remote::Client::PersistentMapTracker::PushIsArmed()) return;
MG_Remote::Client::PersistentMapTracker::Instance().NoteMapStateChanged(*this);
// THE LIVE-HOST-WRITES BIT (ARCHITECTURE.md 12, CONTRACT-P5.md table 1 row 19's
// neighbour). A live WRITE map - persistent or not - mutates the shadow with no call,
// no serial and no epoch, which is exactly why IsBufferDrawCleanByHandle had to ask
// the frontend object whether it was mapped. Under a spawn there is no object on that
// side, so the fact has to cross; it rides MGPResourceDesc's last pad byte as a
// METADATA field (ID-18 M4), so a respecify that moves only it allocates nothing,
// acks nothing and clears no pending upload.
// THE LIVE-HOST-WRITES BIT (ARCHITECTURE.md 12, CONTRACT-P5.md section 3). A live
// WRITE map - persistent or not - mutates the shadow with no call, no serial and no
// epoch, which is exactly why IsBufferDrawCleanByHandle had to ask the frontend object
// whether it was mapped. Under a spawn there is no object on that side, so the fact
// has to cross; it rides MGPSubData's pad (see MGPipeTypes.h, and b1-v1.md 3.1 for
// why not MGPResourceDesc's).
//
// PUBLISHED ON THE EDGE, not per map: the record is a state announcement, and
// re-announcing an unchanged state on every map of a streaming buffer would be new
// traffic for no new information.
// BOTH EDGES EMIT A RECORD, AND THE RISING ONE IS NOT OPTIONAL. An earlier cut let the
// rising edge emit nothing on the grounds that "the next content record carries the
// bit anyway" - and for the single idiom this feature exists to serve, a persistently
// mapped streaming arena behind a static VAO, there IS no next content record. The
// only one is the block push, the push for a vertex/uniform/SSBO map runs inside
// EnsureBufferResourceForHandle, and a draw-clean answer SKIPS that ensure
// (DirectGLES.cpp:691) and then LATCHES it (:697, vboCleanEpoch - and a coherent
// persistent map bumps no mutation epoch, which is its whole point). So: draw 1
// pushes, draw 2 probes clean, draw 3 onward never probes again, and the frame draws
// frame 1's bytes for ever with no diagnostic. That is Managers.cpp:2650-2655's
// regression one layer down, and the rising-edge record is what breaks the cycle: the
// server learns a host writer is live BEFORE the first probe, answers dirty while the
// map lives, and the ensure - and with it the push - runs every draw.
const Bool live = m_isMapped && (m_mappingAccess & BufferMappingAccessBit::Write) &&
!m_resource.IsGpuResident();
if (live == m_publishedLiveHostWrites) return;
m_publishedLiveHostWrites = live;
if (live) {
// RISING EDGE COSTS NOTHING. The bit rides every content record this buffer
// emits, and a live write map's first record is the first block push - or, for a
// non-persistent map, the landing at unmap. Emitting one here would be a record
// whose only content is a bit the very next record carries anyway.
return;
}
// FALLING EDGE NEEDS A CARRIER, because after an unmap there may be no further
// content record for the lifetime of the buffer, and a record stuck at "a host writer
// is live" reads draw-DIRTY forever - the mirror image of the C-1 regression, costing
// a re-upload per draw instead of losing bytes. So one block goes out with the bit
// already cleared above.
//
// ONE BLOCK AND NOT THE SPAN: the bytes of the span have already shipped on this same
// path - ReleaseMemory lands the staged writes and then calls NotifyFlushMappedRange
// for every non-resident write map that is not FLUSH_EXPLICIT - so this record exists
// to carry the STATE, and the bytes it carries are real, current and the cheapest
// honest ones there are. A zero-length record would have been the alternative and it
// is illegal by contract rule A.
// ONE BLOCK, NOT THE SPAN. On the falling edge the span's bytes have already shipped
// on this same path (ReleaseMemory lands the staged writes and calls
// NotifyFlushMappedRange before this runs); on the rising edge the span has not been
// written yet. Either way the record exists to carry the STATE, and the bytes it
// carries are real, current and the cheapest honest ones there are. A zero-length
// record would have been the alternative and it is illegal by contract rule A.
if (m_size == 0) return;
const Uint64 blockBytes = MG_Remote::Client::PersistentMapTracker::BlockBytes();
const SizeT length = blockBytes == 0 || blockBytes >= static_cast<Uint64>(m_size)
? m_size
: static_cast<SizeT>(blockBytes);
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 TURNS THE WHOLE MECHANISM OFF, STATE RECORD
// INCLUDED (E3(a)). An earlier cut skipped the blocks and still shipped a whole buffer
// here, which meant an unmap delivered the bytes the control exists to withhold - and
// a negative control that still delivers them is not a control. With the knob at 0
// nothing is published and nothing is pushed, so the probe answers clean, the frame
// draws the last uploaded bytes, and PersistentCoherentMapScenario goes red.
if (blockBytes == 0) return;
const SizeT length = blockBytes >= static_cast<Uint64>(m_size) ? m_size
: static_cast<SizeT>(blockBytes);
NotifySubData(0, length);
}
@@ -775,6 +790,10 @@ namespace MobileGL::MG_State::GLState {
m_resource.AdoptPersistentMap(pushedBase);
return true;
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// R-6's second door, as in TryAdoptLargeStorage above.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return false;
#endif
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
return false;
@@ -814,9 +833,10 @@ namespace MobileGL::MG_State::GLState {
#if MOBILEGL_BUILD_DISAGGREGATED
// BEFORE the adoption attempt below, deliberately. Under R-6 the acquisition always
// declines, so the predicate this publishes is already final; if a later phase ever
// mints one, the adoption path notes the change itself and this entry is withdrawn
// there rather than never having been made - which is the order that keeps the set
// conservative under both answers.
// mints one, the adoption path notes the change itself (it does now - the call is
// beside AdoptPersistentMap) and the entry is withdrawn there rather than never
// having been made, which is the order that keeps the set conservative under both
// answers.
NotePersistentMapStateChanged();
#endif
@@ -834,13 +854,27 @@ namespace MobileGL::MG_State::GLState {
MG_Pipe::MGPipeResourceSubsystemEnabled()) {
if (void* pushedBase = MG_Pipe::MGPipeEmitMapPersistent(*this)) {
m_resource.AdoptPersistentMap(pushedBase);
#if MOBILEGL_BUILD_DISAGGREGATED
// An adoption takes the buffer OUT of the push set and out of the
// live-host-writes state: the application now writes coherent GPU memory
// and there is nothing to ship. Under R-6 this is unreachable; it is here
// because the comment below used to claim the adoption path withdrew the
// entry and nothing did, which would have left the published bit true
// across an adoption until unmap (latent for P11).
NotePersistentMapStateChanged();
#endif
}
return m_resource.Bytes() + range.start;
}
#endif
if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) &&
!(access & BufferMappingAccessBit::FlushExplicit) && g_bufferBackendOps &&
g_bufferBackendOps->AcquirePersistentMap) {
!(access & BufferMappingAccessBit::FlushExplicit) &&
#if MOBILEGL_BUILD_DISAGGREGATED
// R-6's second door, as in TryAdoptLargeStorage: no legacy mint under a
// transport, whatever the backend registered.
MG_Config::Transport == MG_Config::TransportMode::Monolith &&
#endif
g_bufferBackendOps && g_bufferBackendOps->AcquirePersistentMap) {
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
m_resource.AdoptPersistentMap(base);
}
@@ -215,10 +215,11 @@ namespace MobileGL {
// no longer ask this object (there is no object on that side of a spawn).
void NotePersistentMapStateChanged();
// What MGPipeBuildResourceDesc writes into MGPResourceDesc::HasLiveHostWrites.
// The published value, not the live predicate: the two are the same by the time
// any descriptor is built, and reading the published one is what makes a
// descriptor and the record that announced it agree by construction.
// What MGPipeEmitResourceSubData and MGPipeEmitBufferSubDataResident write into
// MGPSubData::HasLiveHostWrites. The PUBLISHED value, not the live predicate: the
// two are the same by the time any content record is built, and reading the
// published one is what makes a record and the edge that announced it agree by
// construction.
Bool HasLiveHostWritesForWire() const;
// Is a GPU write still unreconciled? Under split this is the THIRD STATE made