Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.
1. P6's plan and contract draft (neither started; not one line of code)
docs/Disaggregated/P6-SPAWN-PLAN.md the package plan
docs/Disaggregated/P6-CONTRACT-DRAFT.md the contract draft (moves to
MG_Remote/CONTRACT-P6.md when c6 lands)
The three findings that mattered, all written into the documents:
a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
the tree and already tested: SocketDoorbell including hangup detection and the two
lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
assembly plus a control plane plus a process, not "write a socket transport".
b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
the tree again. So the first package, a6, is a read-only audit that writes no code, and
every contract row that depends on it is marked PENDING a6 rather than decided in
advance.
c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
and protocol.fbs:186-196's own comment says Android transfers the window out of band -
that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
gate measures. A real window reaching the server is a named refusal. This gets its own
section because "P6 is done" would otherwise read as "the game runs now".
The contract adds rule G: a role may not name a process-local handle. It extends P5c's
rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
a token this contract defines or they do not cross, and a descriptor crosses only through
SCM_RIGHTS, where the kernel renumbers it.
What P5e left for P6 gets its own section. The latch semantics are already designed
(ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
client no longer waits while the peer process can DIE: the client holds records that were
published and will never be applied, and Android's freezer suspends the child without
closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
That is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122), and the two should be designed together.
Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
match integration-split's name for name, because a differing set means the lane is quietly
running less; and the arm-proof gate requires every entry to record the child pid and
transport=spawn in its OWN private log - ID-124 established that this kind of false green
really happens, and a spawn lane that actually ran monolith would pass every other gate.
S2's shape must be settled before it is written, and the document says why: E1 is the
cautionary case.
2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)
ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
is under no version control. A record cited hundreds of times that lives only locally is not
a record, so it moves in whole, with its directory structure intact.
Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
stay where they are: they are evidence DATA rather than documents - large, regenerated per
run, and mostly carrying absolute paths and device serials that mean something only on the
machine that produced them. The mapping and this split are written in
docs/Disaggregated/notes/README.md.
Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
material salvaged from failed workflow runs, which may duplicate the like-named phase
directories and is kept because a few of those were the only complete version at the time.
CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
deliberately outside it: those notes' file:line citations were written against their own
code heads, and resolving them against today's HEAD would fail wholesale - which is not an
error, it is their correct state as a historical record. To check one, use the revision it
declares (--rev).
3. Inconsistencies fixed in passing
- README.md's status line still said P5e was in progress while the stage table already said
closed.
- CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
(a) and (b) were run in the previous commit; rewritten to closed plus the one item that
actually remains (E1).
- ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
package order, the scope boundary and the two new gates.
Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
41 KiB
P3a scout — Espryt (MobileGL/MG_Backend/DirectGLES) buffer surface
Worktree: C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg, branch feat/disaggregated.
All paths below are repo-relative. Every line number was opened.
0. Map at a glance
| Thing | Where |
|---|---|
| Ops table type | MobileGL/MG_State/GLState/BufferState/BufferObject.h:76-124 |
| Ops table registry (global) | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:18,27,31 |
| Espryt implementations | MobileGL/MG_Backend/DirectGLES/Managers.cpp:624-1490 (whole BufferImpl anon namespace) |
| Espryt table instance | Managers.cpp:1482-1490 (g_glesBufferBackendOps) |
| Backend resource class | MobileGL/MG_Backend/DirectGLES/Managers.h:592-644 (GLESBufferResource) |
| Storage abstraction | MobileGL/MG_State/GLState/BufferState/PipeResource.h:62-140 |
| Draw-path entry | Managers.cpp:1602-1706 (EnsureBufferResource), 1579-1601 (IsBufferDrawClean) |
Note on the ops-table shape: every hook receives BufferObject& (a frontend heap
reference), not a handle. That is the single biggest P3a conversion fact — see §2.
1. BufferBackendOps — definition and Espryt's seven hooks
1.1 Definition (MG_State side)
MobileGL/MG_State/GLState/BufferState/BufferObject.h:76 declares struct BufferBackendOps
with exactly seven function pointers:
| # | Hook | Signature line |
|---|---|---|
| 1 | Respecify(BufferObject&) |
BufferObject.h:80 |
| 2 | SubData(BufferObject&, SizeT offset, SizeT size) |
BufferObject.h:82 |
| 3 | ResidentSubData(BufferObject&, SizeT offset, DataPtr data) |
BufferObject.h:95 |
| 4 | FlushMappedRange(BufferObject&, Range1D, Flags<BufferMappingAccessBit> appAccess) |
BufferObject.h:99-100 |
| 5 | OnDestroy(SharedPtr<BackendBufferResource>&&) |
BufferObject.h:103 |
| 6 | void* AcquirePersistentMap(BufferObject&) |
BufferObject.h:114 |
| 7 | ReadbackFromGpu(BufferObject&) |
BufferObject.h:121 |
The table is a single process-global pointer: g_bufferBackendOps at
BufferObject.cpp:18, set/read by SetBufferBackendOps (BufferObject.cpp:27) and
GetBufferBackendOps (BufferObject.cpp:31). A null table means shadow-only tracking
(unit tests, benchmarks) — see BufferObject.h:125-127.
Contract stated in the header and relied on by Espryt: every op must tolerate
GetBackendResource() == nullptr (BufferObject.h:70-74); the lazy draw-time
EnsureBufferResource full-uploads from the shadow and thereby covers everything that
happened before the resource existed. Contents are always read from the shadow, so
ops carry only ranges and flags — never bytes — with the single exception of
ResidentSubData, which carries DataPtr data valid for the duration of the call
only (BufferObject.h:85-94).
Frontend dispatchers that reach these hooks:
NotifyRespecify (BufferObject.cpp:45), NotifySubData (:53),
NotifyFlushMappedRange (:63), NotifyContentWrite (:73),
LandBytesIntoResidentStore (:371), SyncGpuWrites (:319),
TryAdoptLargeStorage (:186), EnsureGpuResidentStorage (:493).
1.2 Espryt's backend resource (GLESBufferResource)
Managers.h:592-644. Derives from MG_State::GLState::BackendBufferResource
(PipeResource.h:66-70). Fields, each of which P3a must decide client-vs-server for:
id(GL buffer name),storageSize,storageInitialized—Managers.h:598-600contextGeneration—Managers.h:603; compared againstg_bufferContextGeneration(Managers.cpp:646), bumped inOnBackendContextDestroyed(Managers.cpp:1529)std::atomic<Uint64> syncedChangeSerial—Managers.h:608; mirrors the FRONTEND'sGetChangeSerial()pendingRespecify(Managers.h:614),VecRange1D pendingRanges(Managers.h:615),Vector<PendingResidentWrite> pendingResidentWrites(Managers.h:624), guarded bystd::mutex pendingMutex(Managers.h:625)drawCleanEpoch—Managers.h:631persistentMapped,persistentPtr—Managers.h:637-638immutableStorage—Managers.h:644
1.3 Hook 1 — Ops_Respecify (Managers.cpp:1213-1264)
Wrapper Ops_RespecifyTracked at Managers.cpp:1449-1452 (calls impl then
BumpBufferMutationEpoch()).
Reads from BufferObject: GetSize(), GetUsage(), HasDefinedContent(),
MappedData(), GetChangeSerial() — all via RespecifyStorageNow
(Managers.cpp:895-936).
Behaviour:
ResourceOf(bufferObject)(Managers.cpp:878-880) → null ⇒ return (lazy create).- If
resource->immutableStorage(Managers.cpp:1224): the store came fromglBufferStorageEXTandglBufferDatacannot respecify it. ClearspersistentMapped/persistentPtr, and if on the context thread deletes the id (NoteBufferIdDeleted+glDeleteBuffers,Managers.cpp:1229-1234); otherwise leaves it flagged forEnsureBufferResourceto retire. SetspendingRespecify = trueand clears both pending queues. - Off-thread / no id / stale context (
Managers.cpp:1244): queuependingRespecify. - Size 0 (
Managers.cpp:1250): drop storage state, no GL. - Otherwise
RespecifyStorageNow(Managers.cpp:934):BindBufferId(TempBufferTarget /* = GL_ARRAY_BUFFER, Managers.h:552 */, id)glBufferData(target, size, initialData, usage), whereinitialDataisbufferObject.MappedData()only ifHasDefinedContent()— an orphaning respecify passes NULL so the driver renames without a stall (Managers.cpp:906-912).- PipeStats
StageBufferbytes accounted only wheninitialData != nullptr(Managers.cpp:913-919). - Clears
pendingRespecify,pendingRanges,pendingResidentWrites; stampssyncedChangeSerial(Managers.cpp:920-925). - If the EXTENT moved,
InvalidateIndexedBufferBindingShadowsForId(resource.id)(Managers.cpp:933) — Adreno resolves a whole-buffer indexed binding's extent at bind time, so a grown store would keep the old, smaller range.
- TODO in tree: orphan-on-respecify as an id-swap is NOT implemented
(
Managers.cpp:888-894).
1.4 Hook 2 — Ops_SubData (Managers.cpp:1266-1297)
Tracked wrapper Managers.cpp:1453-1456.
pendingRespecify⇒ return (full re-upload pending).- No context / no id / stale generation /
!StorageMatches(Managers.cpp:939-942) ⇒pendingRanges.Add({offset, offset+size})underpendingMutex. - Adopted zero-copy store (
persistentMapped && persistentPtr,Managers.cpp:1278) ⇒ bytes already there; just stampsyncedChangeSerial. No GL. MG_Config::Features.EsprytDisableUploadRing(negative control) ⇒UploadRangeNow(immediateglBufferSubData,Managers.cpp:946-955).- Default path: queue the range only (
Managers.cpp:1295-1296). This is the Mali WAR-stall fix — an immediateglBufferSubDataparks the calling thread inosup_sync_object_wait(Managers.cpp:1284-1292, andManagers.h:806-826).
1.5 Hook 3 — Ops_ResidentSubData (Managers.cpp:1304-1312)
Tracked wrapper Managers.cpp:1457-1460. Issues no GL at all — thread-agnostic.
Copies the caller's bytes into resource->pendingResidentWrites (a
{offset, Vector<Uint8> bytes} record, Managers.h:620-624) under pendingMutex.
Drained by DrainResidentWritesNow (Managers.cpp:1084-1128) at draw-time sync
(Managers.cpp:1670) or at readback (Managers.cpp:1391):
- stage into
g_uploadRing+glCopyBufferSubData(GL_COPY_READ_BUFFER → GL_COPY_WRITE_BUFFER)(Managers.cpp:1110-1117); - fallback direct
glBufferSubData(Managers.cpp:1119-1122) — legal because the adopted store carrieskDynamicStorageBit.
This hook exists because an in-place host write into the coherent mapping tears the
frames still reading the old bytes (BufferObject.h:85-94, Managers.cpp:1074-1083).
1.6 Hook 4 — Ops_FlushMappedRange (Managers.cpp:1314-1379)
Tracked wrapper Managers.cpp:1461-1465.
pendingRespecify⇒ return.- Off-thread / mismatched storage ⇒ queue the range (
Managers.cpp:1321-1325). - Adopted store ⇒ stamp serial, return (
Managers.cpp:1334-1337). The historical self-copy here mapped a buffer Espryt keeps persistently mapped — anINVALID_OPERATIONwhose fallback WAR-stalled. - Default (ring enabled) ⇒ queue the range (
Managers.cpp:1344-1348). - Kill-switch path only: honours the app's real mapping flags
(
InvalidateRange/InvalidateBuffer/Unsynchronized,Managers.cpp:1353-1356), optionallyglMapBufferRange(WRITE | INVALIDATE_RANGE? | UNSYNCHRONIZED?)+MemcpyglUnmapBuffer(Managers.cpp:1357-1375), elseUploadRangeNow.
1.7 Hook 5 — Ops_OnDestroy (Managers.cpp:1421-1447)
Tracked wrapper Managers.cpp:1466-1469. Takes ownership of the
SharedPtr<BackendBufferResource> moved out of the dying BufferObject.
Three outcomes:
- Stale
contextGeneration⇒ zeroid, drop (Managers.cpp:1424-1427). - Context current (
CanTouchGLNow(),Managers.cpp:882-884):IsPoolable(Managers.cpp:700-708: needs working fences, id != 0,!persistentMapped,!immutableStorage, live generation, initialized storage,storageSize <= kMaxPoolableBufferBytes= 8 MiBManagers.cpp:694) ⇒EnrollIntoPool(Managers.cpp:712-745);- else
InvalidateArrayBufferBindingCacheif bound,ScrubBufferBindingShadowsForId,glDeleteBuffers(Managers.cpp:1434-1441).
- Not on the context thread ⇒ push onto
g_deferredBufferReleases(Managers.cpp:669-676) underg_deferredBufferReleasesMutex, and set the lock-free emptiness flagg_hasDeferredBufferReleases(Managers.cpp:673-676).
Deferred-release / delayed-free machinery (all of this must survive P3a):
- Drain:
ProcessDeferredBufferReleases(Managers.cpp:1542-1572), called per draw fromSyncNeccessaryBuffers/SyncComputeBuffers(DirectGLES.cpp:735region andDirectGLES.cpp:717). Fast-outs on the atomic flag first. - Buffer pool (Mesa-style BO recycle),
Managers.cpp:678-762:struct PooledBuffer {id, size, contextGeneration, retireSerial}(Managers.cpp:685-690),UnorderedMap<SizeT, Vector<PooledBuffer>> g_bufferPool(Managers.cpp:691), budget constantskMaxPoolableBufferBytes8 MiB /kMaxPoolBytes64 MiB /kMaxEntriesPerBucket32 (Managers.cpp:694-696).EnrollIntoPoolstampsretireSerial = DirectGLES::CurrentFrameSerial() + 1(Managers.cpp:737-739— the+1is explained there).AcquireFromPool(size)(Managers.cpp:748-773) hands back only entries withretireSerial <= DirectGLES::CompletedFrameSerial()(DirectGLES.cpp:10859-10860).TrimBufferPool(Managers.cpp:1882) is called once per frame from Present (DirectGLES.cpp:10928);ClearBufferPool(Managers.cpp:1913) on context loss. - The pool reseed path in
EnsureBufferResource(Managers.cpp:1657-1685) does a whole-bufferglBufferSubDatafrombufferObject->MappedData().
1.8 Hook 6 — Ops_AcquirePersistentMap (Managers.cpp:1134-1210)
Tracked wrapper Managers.cpp:1470-1476 (bumps the epoch even on decline).
- Gate:
CanTouchGLNow()+glBufferStorageEXT+glMapBufferRange+glGenBuffers(Managers.cpp:1135-1138);size == 0⇒ null. - Creates the
GLESBufferResourceif absent (Managers.cpp:1142-1147) — the one op that callsbufferObject.SetBackendResource(...)besidesEnsureBufferResource. - Stale generation ⇒ wipe the resource fields BEFORE stamping the new generation
(
Managers.cpp:1151-1160), otherwise idempotency would return a dead context's pointer. - Idempotent hit:
persistentMapped && persistentPtr && storageSize == size(Managers.cpp:1163-1165). - Fresh id required:
NoteBufferIdDeleted,++g_bufferBackendIdGeneration,glDeleteBuffers,glGenBuffers(Managers.cpp:1169-1179). glBufferStorageEXT(GL_ARRAY_BUFFER, size, bufferObject.MappedData(), GL_MAP_WRITE_BIT | PERSISTENT | COHERENT | DYNAMIC_STORAGE)— bit constants defined locally atManagers.cpp:1130-1132(0x0040/0x0080/0x0100). Seeds from the shadow, which is still live at this point (Managers.cpp:1179-1185).immutableStorage = trueset as soon as the store exists, before the map result (Managers.cpp:1188).glMapBufferRange(0, size, WRITE|PERSISTENT|COHERENT); on failure logsMGLOG_E_ONCEand returns null withpersistentMapped=false(Managers.cpp:1189-1197).- Success: stamps
persistentPtr/persistentMapped/storageSize/storageInitialized, clears both pending queues, stampssyncedChangeSerial(Managers.cpp:1198-1209).
Frontend adoption then calls PipeResource::AdoptPersistentMap (PipeResource.h:113-117),
which drops the CPU shadow. Espryt itself never calls EnsureGpuResidentStorage
(only Magma does: DirectVulkan/Renderer/UniformManager.cpp:1073,1229,
VulkanRenderer.cpp:11620); Espryt reaches adoption purely through the frontend's
TryAdoptLargeStorage (BufferObject.cpp:186-196, threshold 16 MiB, kill switch
MG_Config::Features.DisableLargeBufferAdoption) and through coherent persistent maps in
AcquireMemoryRange.
1.9 Hook 7 — Ops_ReadbackFromGpu (Managers.cpp:1382-1419)
Tracked wrapper Managers.cpp:1477-1480.
- Bails on no resource / id 0 / uninitialized storage / off-thread / stale generation
(
Managers.cpp:1384-1386). - Adopted store branch (
Managers.cpp:1387-1394):DrainResidentWritesNowthenglFinish(). There is no backend copy to read back — the coherent mapping IS the data. - Otherwise:
FlushPendingRangesNowfirst (Managers.cpp:1404) so queued app writes are not reverted, thenBindBufferId+glMapBufferRange(0, size, GL_MAP_READ_BIT),bufferObject.WritebackFromBackend({mapped, size}, 0)(Managers.cpp:1410),glUnmapBuffer, then stampsyncedChangeSerialso the next draw does not re-upload the readback over itself (Managers.cpp:1413-1418).
1.10 Registration lifecycle and the mutation epoch
RegisterBufferBackendOps(Managers.cpp:1505-1510) — called fromBackendObject_DirectGLES::Initialize(BackendObject_DirectGLES.cpp:849) and from everyMakeCurrent(DirectGLES.cpp:10402).UnregisterBufferBackendOps(Managers.cpp:1512-1525) — also clears the pool and the deferred-release list.OnBackendContextDestroyed(Managers.cpp:1527-1540) — bumpsg_bufferContextGeneration, invalidates every binding cache, and resets the UBO and unpack rings. Called fromDirectGLES.cpp:10938.- Buffer-mutation epoch:
g_bufferMutationEpoch(Managers.cpp:653),CurrentBufferMutationEpoch/BumpBufferMutationEpoch(Managers.cpp:1493-1503). The exhaustive list of bump sites and the acquire/release rules is documented atManagers.h:556-590— read it before touching anything, it is the correctness contract for the draw-clean memos. g_bufferBackendIdGeneration(Managers.cpp:1502, declaredManagers.h:706): bumped whenever a LIVE resource's driver id is re-minted (persistent-map adoption atManagers.cpp:1173, immutable-store retire atManagers.cpp:1639), because VAO twins bake ids against frontend versions that do not move.
1.11 Three persistent-mapped rings + shared machinery
Documented at Managers.h:729-825; implemented Managers.cpp:764-870 (types) and
1922-2204 (create/allocate/present).
PersistentRingStore(Managers.cpp:800-819),RetiredRingStore(:823-827),RingFrameMark(:832-835),PersistentRing(:839-849).- Instances:
g_uboRing(Managers.cpp:851),g_unpackRing(:852-858),g_uploadRing(:867-873). Caps: 4 MiB initial / 64 MiB max each (Managers.cpp:770-784). CreateRingStorage(Managers.cpp:1930-1983) —glBufferStorageEXT+glMapBufferRangepersistent|coherent; retires the old store ontoring.retiredwithCurrentFrameSerial()+1.RingAvailable(Managers.cpp:1989-2000) — self-heals a stale context generation viaResetRingForNewContext(Managers.cpp:866-876).RingAllocatefast pathManagers.cpp:2016-2038, slow path from:2040.- Exported wrappers
Managers.cpp:2172-2203. Present-time upkeep is called fromDirectGLES.cpp:10925-10928(UboRingOnPresent,UnpackRingOnPresent,UploadRingOnPresent,TrimBufferPool). - Upload-ring gate for the buffer path:
UploadRingUsableNow(Managers.cpp:963-967) — kill switchFeatures.EsprytDisableUploadRing,glCopyBufferSubDatapresent, ring up. FlushPendingRangesNow(Managers.cpp:1004-1072) is the three-tier drain and the single most delicate function in the file:glMapBufferRange(WRITE | INVALIDATE_BUFFER|INVALIDATE_RANGE)+ memcpy, taken only for a whole-buffer flush or a range>= kInvalidateRangeMinBytes(128 KiB,Managers.cpp:975) —Managers.cpp:1054-1067;- upload ring +
glCopyBufferSubData—Managers.cpp:1058-1067(ring branch at:1069-1076in the listing order shown by the file:RingAllocate,Memcpy,BindBufferId(GL_COPY_READ_BUFFER/GL_COPY_WRITE_BUFFER), copy); - direct
UploadRangeNow. Ranges are flushed as queued (VecRange1D::Addalready merges near-adjacent ones); collapsing to the union re-copied whole chunk arenas —Managers.cpp:1000-1003. Kill switchFeatures.EsprytDisableInvalidateFlush(Managers.cpp:1017) forces tier 2.
- Unpack ring consumers (texture uploads, not buffers, but the same allocator):
Managers.cpp:3153,3172,3174,4484,4510,4562. - UBO ring consumer:
DirectGLES.cpp:3652-3684(global default-uniform block).
Observation, low risk: OnBackendContextDestroyed (Managers.cpp:1537-1538) resets
g_uboRing and g_unpackRing but not g_uploadRing; RingAvailable
(Managers.cpp:1996-1998) heals it on first use, so this is benign today but is an
asymmetry worth preserving-or-fixing deliberately in P3a.
2. Every place Espryt holds or dereferences a frontend BufferObject
2.1 The twin registry question — answered
The buffer twin is NOT one of the six {slot, gen} slot tables. The six tables
(SlotTables.h:1-80 explains the mechanism) are, by their MGPipeKind template argument:
| Kind | Declared | Defined |
|---|---|---|
VertexElementsCso (VAO) |
Managers.h:954 |
Managers.cpp:2877 |
Texture |
Managers.h:1275 |
Managers.cpp:5205 |
Framebuffer |
Managers.h:1366 |
Managers.cpp:5958 |
ShaderCso (program) |
Managers.h:1884 |
Managers.cpp:6269 |
SamplerCso |
Managers.h:1984 |
Managers.cpp:8787 |
Renderbuffer |
Managers.h:2011 |
Managers.cpp:8899 |
The TwinRegistry alias itself is Managers.h:544-549; StateBackendObjectRegistry is
Managers.h:300.
Buffers instead use owner-embedded twinning: the GLESBufferResource is owned by the
frontend BufferObject through PipeResource::m_backend
(PipeResource.h:127-131), reached by GetBackendResource() /
SetBackendResource() (BufferObject.cpp:611,615). Espryt's only accesses:
Managers.cpp:879—ResourceOf(BufferObject&), the ops' accessorManagers.cpp:1142,1146—Ops_AcquirePersistentMapget + createManagers.cpp:1576—GetBufferResource(BufferObject*)(no GL)Managers.cpp:1582—IsBufferDrawCleanidentity compareManagers.cpp:1608,1613—EnsureBufferResourceget + create
Consequence for P3a: there is no existing slot table to extend; the buffer kind's
{slot, gen} table has to be introduced, and SetBackendResource disappears (matching
docs/Disaggregated/ARCHITECTURE.md:284, "SetBackendResource 删除(server 拥有资源表)").
2.2 Keyed by what, today
| Key | Site | Use |
|---|---|---|
Raw BufferObject* |
DirectGLES.cpp:626 (syncedBuffers[] dedupe scratch), DirectGLES.cpp:635 (bufferKey) |
per-draw dedupe of the attribute walk |
Raw BufferObject* |
Managers.h:915-923 (ResolvedDrawBuffers::Entry::frontend, iboFrontend) |
VAO draw-buffer memo; pinned by the VAO's attribute SharedPtrs |
Raw const BufferObject* |
Managers.cpp:2452, 2636 (m_syncedIndexBufferObject) |
IBO identity compare against a wrapping Uint16 slot version |
GetLifetimeId() |
Managers.cpp:2812-2820 (stream.sourceLifetimeId) |
fp64 → fp32 converted-stream memo key |
GetChangeSerial() |
Managers.cpp:2814, Managers.h:608 (syncedChangeSerial) |
staleness of converted streams / backend storage |
SharedPtr<BufferObject> |
DirectGLES.cpp:747 (XfbCaptureTarget::buffer) |
keeps the capture target alive across a span |
SharedPtr<BufferObject> (by value/ref from PipeInputs) |
every EnsureBufferResource(...) call |
see the site list below |
2.3 EnsureBufferResource / GetBufferResource call sites (Espryt)
Managers.cpp:
2337BindAttributeBuffer(vertex attribute →GL_ARRAY_BUFFER)2393SyncZeroStrideAttribute(→glBindVertexBuffer)2622BackendVertexArrayObject::SyncToBackend, index-buffer branch (→GL_ELEMENT_ARRAY_BUFFER)4724texture-buffer (TextureStorageType::Buffer) →glTexBuffer/glTexBufferRange
DirectGLES.cpp:
411SyncBufferBindingPoints(indexed UBO/SSBO/XFB points)463regionSyncTransformFeedbackBindingPoints526SyncAtomicCounterBuffers(mapped onto reserved SSBO bindings)560SyncBoundBuffer(DrawIndirect / DispatchIndirect)612,645theResolvedDrawBuffersmemo repair and full walk681IBO memo miss4185,4247indirect-drawmg_IndirectParamsSSBO view4631BoundElementArrayBufferId()9411glReadPixelspack PBO9837glGetTexImagepack PBO
2.4 Reads of BufferObject members from backend code
| Member | Espryt sites |
|---|---|
GetSize() |
Managers.cpp:897, 1006 (limit), 1139, 1396, 1653, 1698, 1701, 2777, 4789 (via GetBufferRangeSizeInBytes compare), 4795; DirectGLES.cpp:299, 432-435 (range clamp), 480-483, 502-506, 7887, 7889, 9456, 9459, 9866, 9869; Utils.cpp:2322 |
GetUsage() |
Managers.cpp:903 |
GetChangeSerial() |
Managers.cpp:925, 1207, 1280, 1336, 1377, 1417, 1600, 1684, 1710-1714 (compare), 2814 |
GetLifetimeId() |
Managers.cpp:2813 |
MappedData() (host-visible base — shadow OR coherent map) |
Managers.cpp:911, 952, 1055, 1063, 1184, 1367, 1668, 2775; DirectGLES.cpp:301, 4965 region (restart rewrite source = indexBuffer->MappedData()), 5031-5038, 5074-5081; MultiDraw.cpp:513 |
HasDefinedContent() |
Managers.cpp:911 |
IsMapped() |
Managers.cpp:1594 (IsBufferDrawClean) |
IsBackendPersistentMapped() |
Managers.cpp:2815; DirectGLES.cpp:851 |
GetExternalIndex() (GL name, diagnostics only) |
Managers.cpp:4711; DirectGLES.cpp:9412, 9415, 9838, 9841 |
GetBackendResource() |
§2.1 list |
MarkGpuWritten() |
DirectGLES.cpp:500 (MarkShaderStorageBuffersGpuWritten), 544 (SyncAtomicCounterBuffers), 2012 (MarkWritableImageBufferTexturesGpuWritten) |
WritebackFromBackend() |
DirectGLES.cpp:868 (XFB ReadbackCapturedRanges), 976 (ScatterCapturedRecords), 7896 (StoreReadbackRowsToClient), 9460 (glReadPixels PBO), 9870 (glGetTexImage PBO); Managers.cpp:1410 (Ops_ReadbackFromGpu); Utils.cpp:2343 (ReadPixelsViaFormatConversion) |
SyncPersistentMappedRange() / SyncGpuWrites() |
§3 |
Note the "dirty ranges" question: Espryt never reads a frontend dirty-range list — the
frontend has none. Ranges arrive only as op arguments and live on the BACKEND resource
(pendingRanges).
MGB_CTX is the pushed PipeInputs block: MG_Pipe/PipeInputsSwitch.h:19 (push) /
:24 (pull). The buffer accessors that hand Espryt a SharedPtr<BufferObject> are
PipeInputs::GetBufferBindingSlot (MG_Backend/MGPipe/PipeInputs.h:485-495) and
GetBufferBindingPoint (:496-508); the fill-point coverage is
MG_Pipe/FillPoints.def:156-158, 204-206, 269, 286, 289, 310-311 and
MG_Pipe/Coverage.def:42-45.
3. SyncPersistentMappedRange and SyncGpuWrites
3.1 What they do (frontend)
SyncPersistentMappedRange — BufferObject.cpp:291-303. Returns immediately unless the
buffer is mapped, NOT GPU-resident, PERSISTENT, WRITE, not FLUSH_EXPLICIT, with a
non-empty mapped range. Then it calls NotifySubData(start, len) — i.e. it synthesises a
SubData op for a write that never went through any GL entry point. For a GPU-resident
(adopted) buffer it is a pure no-op (BufferObject.cpp:295).
SyncGpuWrites — BufferObject.cpp:319-328. If m_gpuWritePending (set by
MarkGpuWritten, BufferObject.cpp:314-317), clears the flag unconditionally and
dispatches ReadbackFromGpu.
3.2 Espryt's share of the 20 SyncPersistentMappedRange sites — 8
| Site | Context |
|---|---|
DirectGLES.cpp:297 |
ResolveIndirectCommandBytes — before reading indirect commands from the shadow |
DirectGLES.cpp:4710 |
restart-substitution: element buffer, immediately followed by SyncGpuWrites() at :4711 |
DirectGLES.cpp:4964, :4965 |
MultiDrawElementsIndirectCount — draw + parameter buffer |
DirectGLES.cpp:5066, :5067 |
MultiDrawArraysIndirectCount — draw + parameter buffer |
Managers.cpp:1700 |
EnsureBufferResource — the per-draw push, before respecify/flush |
MultiDraw.cpp:511 |
multi-draw index flattening |
(The other 11 backend sites are Magma's: DirectVulkan.cpp:281,472,796,
UniformManager.cpp:2024, VkBufferManager.cpp:628,679,
VulkanRenderer.cpp:3542,3621,4016,7431,12426,12427; plus the definition and the test
sites.)
3.3 Espryt's share of the 6 SyncGpuWrites backend sites — 3
| Site | Context |
|---|---|
DirectGLES.cpp:4711 |
restart substitution reads the index shadow |
Managers.cpp:2774 |
SyncFloat64AttributeAsFloat32 before narrowing doubles from the shadow |
MultiDraw.cpp:512 |
multi-draw index flattening |
(Magma's three: VulkanRenderer.cpp:3541, 4015, 4349. The frontend's two are
MG_Impl/GLImpl/Buffer/GL_Buffer.cpp:957, 995 — glGetBufferSubData /
glGetNamedBufferSubData.)
3.4 What a server without the frontend's address space needs instead
Both calls are the backend reaching backwards into client memory:
SyncPersistentMappedRangeexists only because the app writes into the shadow with no API call. A server cannot observe that. The design already names the replacement: a client-side trackerm_livePersistentMapspushing block-granular dirty ranges at each validate point, over the union of exactly these 20 sites, chunked byMOBILEGL_IPC_PERSISTENT_BLOCK_KB(default 64) —ARCHITECTURE.md:482. Phase 1 is conservative (whole mapped span, block-split), Phase 2 exact (64 KiB dirty bits +memcmp). AlsoARCHITECTURE.md:481:ResourceRespecify/SubDatacarry ahasLiveHostWritesbit — the one thingIsBufferDrawClean(Managers.cpp:1594) needs from the map state, with zero new record kinds.- The five Espryt sites that follow
SyncPersistentMappedRangewith a CPU read ofMappedData()(DirectGLES.cpp:301,:4965,:5031-5038,:5074-5081,MultiDraw.cpp:513) are the harder half: they are*IndirectCountcount resolution, restart rewriting and multi-draw flattening.ARCHITECTURE.md:221moves*IndirectCountcount resolution to the CLIENT (server receives resolvedMGPDrawRange[]);ARCHITECTURE.md:106addskCapNeedsHostIndexBytesso restart rewriting / multi-draw flattening get an index mirror on the server side (ARCHITECTURE.md:387, budgetMOBILEGL_PIPE_INDEX_MIRROR_MBdefault 64). SyncGpuWritesbecomes theOnGpuWritten(res, rangeCount, ranges)reverse callback (MG_Pipe/MGPipeCallbacks.h:30-31), andWritebackFromBackendbecomesOnBufferWriteback(res, offset, bytes)(MGPipeCallbacks.h:32), batched per operation, ordered with respect to the epoch bump (ARCHITECTURE.md:275,:288).- Espryt's two remaining
SyncGpuWritesconsumers (fp64 narrowing atManagers.cpp:2774, index flattening atMultiDraw.cpp:512) both readMappedData()right after — same index-mirror / client-side-resolution treatment.
4. Readback paths and write-back into the frontend
4.1 glGetBufferSubData (frontend, not Espryt)
MG_Impl/GLImpl/Buffer/GL_Buffer.cpp:957 and :995 call SyncGpuWrites() then
DownloadSubData (BufferObject.cpp:434), which reads m_resource.Bytes() — the shadow
or the coherent map. Espryt's contribution is only Ops_ReadbackFromGpu
(Managers.cpp:1382). A non-persistent mapping makes the call an INVALID_OPERATION
(GL_Buffer.cpp:948-955).
4.2 Map-read
BufferObject::AcquireMemory (BufferObject.cpp:466) and AcquireMemoryRange
(BufferObject.cpp:518) call SyncGpuWrites() (:467, :537) and hand back a pointer
into PipeResource::Bytes(). Espryt never sees a read map directly — the reconciliation
is the ReadbackFromGpu op.
4.3 Pack-PBO readback (Espryt owns these)
Four distinct writeback shapes, all landing through WritebackFromBackend and each
followed by BumpBufferMutationEpoch():
glReadPixelsnative + PBO:DirectGLES.cpp:9406-9468. Resolves the PBO viaEnsureBufferResource(:9411), scopes the driver binding withScopedPixelPackBuffer(DirectGLES.cpp:6435-6441),glReadPixels, then maps the PBO read-only andWritebackFromBackend({pboMappedPtr, size}, 0)(:9460), epoch bump (:9462).glGetTexImage+ PBO:DirectGLES.cpp:9832-9880, structurally identical (:9870).- Row-by-row conversion readbacks:
StoreReadbackRowsToClient(DirectGLES.cpp:7876-7906) — per-rowWritebackFromBackendat:7896, ONE epoch bump for the whole loop at:7903. Same shape inUtils.cpp:2297-2354(:2343writeback,:2352bump). - Transform feedback:
XfbImpl::ReadbackCapturedRanges(DirectGLES.cpp:838-878) maps each capture range read-only and writes back at:868(+ bump:872); persistently mapped targets are skipped at:851.ScatterCapturedRecords(DirectGLES.cpp:918-985) is a read-modify-write of the frontend shadow: it starts fromtarget.buffer->MappedData()(:961), patches the captured varyings in, writes back (:976), bumps (:978), and re-uploads withglBufferSubData(:980-984).ARCHITECTURE.md:310andMGPipeCallbacks.h:49-51(OnXfbScatterReady) already specify how this survives the split — the scatter loop moves to the client.
Driver-side pack/unpack binding shadows that every readback routes through:
BindPixelPackBufferId (Managers.cpp:1739), BindPixelUnpackBufferId
(Managers.cpp:1748), InvalidatePixelBufferBindingCaches (Managers.cpp:1757),
NoteBufferIdDeleted (Managers.cpp:1764). Resting state is 0; the reason is documented
at Managers.h:670-682.
5. The Adreno SIGSEGV workaround the roadmap says to keep
ROADMAP.md:19 names it as "Adreno SIGSEGV workaround 保留". ARCHITECTURE.md:316
repeats it in the server-local-invariants list ("Adreno 禁用属性 SIGSEGV workaround").
grep SIGSEGV in MobileGL/MG_Backend/DirectGLES/ returns exactly three hits, and they
are two distinct workarounds plus one cross-reference:
5.1 GL_DOUBLE / IsLong attribute with no buildable stream
Managers.cpp:2517-2540 (rationale :2516-2521, and the forward reference at
Managers.h:888-895): becoming long bumps FormatVersion, not SwitchVersion, so the
enable/disable block above does not re-run and an already-enabled array would stay enabled
with no pointer and no GL_ARRAY_BUFFER binding. That is not merely
GL_INVALID_ENUM — "the Adreno driver dereference[s] null inside the next draw and take[s]
the process with it (SIGSEGV in libGLESv2_adreno,
KHR-GL43.vertex_attrib_binding.basic-input-case4)". Fix: try
SyncFloat64AttributeAsFloat32 (Managers.cpp:2757-2874), and on failure
glDisableVertexAttribArray(attribIndex) (Managers.cpp:2537) with a MGLOG_W_ONCE.
Note the re-enable at Managers.cpp:2529 is explicitly non-redundant.
5.2 GL_BGRA vertex size refused by the driver
Managers.cpp:2568-2612 (rationale :2568-2582). ES has no GL_BGRA vertex size; the
rejection "leaves the array ENABLED with no pointer, and the Adreno driver then
dereferences null inside the next draw and kills the process rather than reporting an
error (SIGSEGV in libGLESv2_adreno,
KHR-GL43.vertex_attrib_binding.basic-input-case5)". Mechanism: drain glGetError before
the call (Managers.cpp:2585-2588), issue glVertexAttribPointer with
GL_BGRA as the size (Managers.cpp:2593-2596), and if glGetError() != GL_NO_ERROR
(Managers.cpp:2603) disable the array. Deliberately only for attrib.IsBgra — the
comment says the per-draw sync must not grow a glGetError round trip for real formats.
Both live inside BackendVertexArrayObject::SyncToBackend, i.e. in P3a's vertex-elements
half, and both depend on being able to observe a DRIVER error at attribute-declaration
time — which under a split means the check has to stay server-side and the disable has to
be a server-local decision, not a client one.
6. Test coverage that P3a must keep green
6.1 MG_IntegrationTest scenarios touching buffers on Espryt
Roadmap-named (ROADMAP.md:19): LargeArenaAdoption, StorageBufferRegrow (publishes
map-persistent-roundtrips), VertexAttribBinding, MultiDraw, PrimitiveRestart.
Full TEST_F names, by file under MobileGL/MG_IntegrationTest/Scenarios/:
LargeArenaAdoptionScenario.cpp—SubDataAfterAnInFlightDrawReachesTheNextDraw(:204),ReadbackSeesTheLatestCpuWrite(:227),GpuWriteIntoTheArenaIsReadBack(:245). File header at:13namesTryAdoptLargeStorageexplicitly.ResidentIndexScenario.cpp—PersistentCoherentEboWrittenEveryFrame(:186),EboAlsoBoundAsVertexBufferLater(:225),EboDeletedAndRecreatedAtTheSameName(:271),OneEboTwoVaosMutatedAcrossFrames(:311),PromotedDynamicEbo(:351). Header comment at:40namesBufferObject::SyncPersistentMappedRange.StorageBufferRegrowScenario.cpp—AGrownStoreIsVisibleThroughItsExistingIndexedBinding(:124). This is the case behindInvalidateIndexedBufferBindingShadowsForId(Managers.cpp:933).CrossFrameBufferScenario.cpp—VertexBufferSubData(:409),VertexMapWriteUnmap(:414),VertexPersistentMapFlush(:419),VertexPersistentCoherentWrite(:425),VertexOrphanAndReupload(:431),VertexCopyBufferSubData(:437),IndexBufferSubData(:446),IndexMapWriteUnmap(:451),IndexPersistentMapFlush(:456),IndexPersistentCoherentWrite(:481),IndexOrphanAndReupload(:487),IndexCopyBufferSubData(:493),PartialStalenessIsCaughtByWholeRegionChecks(:518); plusStreamedArenaScenarioin the same file:StreamedVertexDataSurvivesArenaRecycling(:675),StreamedIndexDataSurvivesArenaRecycling(:731) — these two are the buffer-pool recycle cases.BufferTextureScenario.cpp—VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates(:162),AnImageStoreIntoABufferTextureIsVisibleToTheCpu(:238),LevelQueriesDescribeTheAttachedBufferRange(:311). CoversManagers.cpp:4707-4800.XfbCaptureBufferReuseScenario.cpp—EverySpanIntoABufferObjectOfItsOwn(:182),EverySpanIntoOneRespecifiedBufferObject(:215),ARespecificationMayChangeTheCaptureBufferSize(:243),EverySpanIntoOneImmutableStorageBuffer(:283).AtomicCounterScenario.cpp—DispatchIncrementsTheBoundCounterBuffers(:172),CountersAccumulateAcrossDispatchesAndFollowAReseed(:201),SubDataAfterDispatchSurvivesAnImmediateReadback(:242) — theMarkGpuWritten/ReadbackFromGpuround trip.PrimitiveRestartScenario.cpp— 7 cases (:215, :236, :263, :281, :306, :332, :368); these drive theMappedData()index rewrite atDirectGLES.cpp:4964-4980.MultiDrawScenario.cpp— 10 cases (:225 … :526); driveMultiDraw.cpp:495-525.PackedWordReadbackScenario.cpp— :148, :167, :194 (PBO/readback writeback paths).VertexAttribBindingScenario.cpp,VertexArrayEnableDisableScenario.cpp,DoublePrecisionScenario.cpp— the vertex-elements half incl. the fp64 narrowing and the two SIGSEGV workarounds.
6.2 Unit tests
MobileGL/MG_Test/Buffer/BufferTest.cpp— 88TESTmacros. The ones that exercise the ops table directly use mock tables defined in-file:kZeroCopyMockOps(:1609),kResidentSubDataMockOps(:1640),ScopedBackendOpsRAII (:1651-1654). Directly ops-shaped cases:General_PersistentCoherentZeroCopyStressNoPerDrawReupload(:1658),General_PersistentCoherentFallbackSyncsPerDrawWhenBackendDeclines(:1724),General_CoherentAsFlush_*(:1778, :1809, :1819, :1855, :1890),RedefiningAnAdoptedBufferHandsTheMappingBack(:1952),RedefiningAnAdoptedBufferAtANewSizeStaysInBounds(:1990),ImmutableStorageOnAnAdoptedBufferHandsTheMappingBackToo(:2029),RedefiningAnAdoptedBufferToZeroBytesLeavesItOnTheShadow(:2058),RedefiningANonAdoptedBufferIsUnchanged(:2093), the adopted-store map/unmap family (:2165, :2204, :2248, :2279, :2316), theResidentSubDatafamily (:2364, :2412, :2557, :2616, :2698),MapBufferRangeAndUnmapBufferReseedAnAdoptedShaderStorageBuffer(:2451),ANonPersistentWriteMapOfAShadowBackedStoreStillFlushesThroughTheBackend(:2483),GlBufferSubDataIntoAnAdoptedStoreLandsInPlaceWithoutABackendTransfer(:2533),GlClearBufferSubDataRepeatsItsPatternThroughAnAdoptedStoreInPlace(:2589),GlCopyBufferSubDataIntoAnAdoptedStoreLandsAtTheDestinationOffset(:2665),AnExplicitFlushOfAPersistentMapOfAnAdoptedStoreOnlyPublishesTheChange(:2733),AZeroLengthExplicitFlushOfAnAdoptedStoreLeavesItUndefined(:2779),AWriteMapThatDiscardsWhatItMapsDoesNotReconcileAnAdoptedStoreAtMapTime(:2817),AStorageBindingDoesNotAdoptTheStoreWhileTheApplicationHoldsAMapping(:2868),RespecifyingAStoreWhileItIsMappedDoesNotLandTheStagedBytesIntoIt(:2917),RespecifyingAShadowBackedStoreWhileItIsMappedPushesNoRangeDown(:2943).MobileGL/MG_Test/SanityTest.cpp:2535—DirectGLESStateGuards.PixelPackBindingCacheSkipsRedundantBindsAndRestsAtZero, the only unit test that pokesBufferImplsymbols directly (:2539-2551).MobileGL/MG_Test/Backend/DirectGLES/contains no buffer tests (BaseInstanceInjectionTest.cpp,EsslShaderPassTest.cpp,ViewportIndexRoutingTest.cpponly).MG_Test/SelfTest/likewise has none.- No unit test covers
EnsureBufferResource,IsBufferDrawClean, the pool, the rings, or the deferred-release list — those are integration-only today. Worth knowing before P3a refactors them.
6.3 Trace cases the roadmap names (ROADMAP.md:19 — "Create/rd12/26.3/sodium trace")
Fixtures present in tools/trace_replay/fixtures/:
| Roadmap name | Fixture |
|---|---|
| Create ×2 | minecraft-1.21.1-neoforge-create-indirect-in-world.tgz (ref .0000504631.png) and minecraft-1.21.1-neoforge-create-instancing-in-world.tgz (ref .0000530333.png) |
| rd12 | minecraft-1.21.4-rd12-odinlite-in-world.tgz (ref .0004660351.png) |
| 26.3 | improved-transparency-minecraft-26.3.tgz (ref .0002667619.png) |
| sodium | minecraft-1.21.4-fabric-sodium-in-world.tgz (ref .0000923340.png) |
ARCHITECTURE.md:485 records that both Create fixtures carry coherent_as_flush: true,
so under split they must take the same buffer path as monolith for a name-by-name compare.
ROADMAP.md:90 warns that the create-indirect fixture already fails on Adreno 830 at
dev@81b17c0b (baseline APK reproduces) — it is on the P3a/P8 acceptance list but must be
fixed on dev first. ROADMAP.md:19 also demands MC 26.3 p99 unchanged on Adreno.
7. Short list of the traps a P3a implementer will hit
- Every hook takes
BufferObject&, and four of them readMappedData()— the conversion is not "rename seven functions", it is "decide who owns the bytes". EnsureBufferResource(Managers.cpp:1602) both CREATES the twin and calls back into the frontend (SyncPersistentMappedRangeat:1700). That callback is the pull the split must invert.IsBufferDrawClean(Managers.cpp:1579) reads five frontend facts —GetBackendResource(),IsMapped(),GetSize(),GetChangeSerial()— on the hot path, per draw, per buffer.ARCHITECTURE.md:481reduces it to onehasLiveHostWritesbit; the size/serial half still needs a home.- The mutation-epoch bump list (
Managers.h:556-590) is exhaustive by design, and three of its entries are backend-initiated writebacks with NO op (DirectGLES.cpp:872, 978, 7903, 9462, 9872;Utils.cpp:2352). Under split the bump becomes an ordering rule on the reverse channel (ARCHITECTURE.md:288). g_bufferBackendIdGeneration(Managers.cpp:1502) exists purely because VAO twins bake driver ids against frontend versions. It stays server-side, but the VAO half of P3a has to keep reading it (Managers.cpp:2446,:2649).PendingResidentWrite(Managers.h:620-624) is the only place Espryt COPIES app bytes into its own storage.BufferSubDataResidentis flaggedkOptionalin the catalogue (ARCHITECTURE.md:91,94) — Magma deliberately does not register it.TempBufferTargetisGL_ARRAY_BUFFER(Managers.h:552) and the redundant-bind cache for it (Managers.cpp:631-632) is invalidated from seven places. Any new code path that bindsGL_ARRAY_BUFFERbehindBindBufferId's back silently corrupts it.