[MG_Backend, MG_Pipe, MG_Remote, MG_IntegrationTest, docs] (P7): the XFB writeback event is sliced, so a large capture stops killing the server

P3b/P4b espryt D1 slice 2, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "XFB scatter", §3 wave 2-D).

Both XFB writeback producers posted ONE unbounded OnBufferWriteback covering the whole
bound range - ScatterCapturedRecords from its staged vector, ReadbackCapturedRanges from a
live glMapBufferRange mapping. ServerOnBufferWriteback turns each into one SEG_EVENT record
of sizeof(EventBufferWritebackHead) + bytes, and RingProducer::Reserve refuses any record
above Capacity()/2 outright - not "when full" but always, because above half a capacity a
record is placeable at some head offsets and not at others and waiting for room would be a
hang. EventRingBytes defaults to 256 KiB, so any capture target wider than ~128 KiB was
Fatal{EventRingOverflow} and the SERVER PROCESS DIED, on a buffer size, with nothing else
wrong. The producers' own Fatal text already said what to do: raise MOBILEGL_IPC_EVENT_KB
or slice the event at its producer, "as the writeback path already does" - which was true
of the client's request path and of nothing on this side.

MG_Pipe now carries the one number, published the way gMGPipeSegmentResolver is published:
gMGPipeEventRingCapacityBytes, installed by ServerSession::Accept beside the four reverse
producers and released at Close only if it is still ours, and
MGPipeBufferWritebackSliceBytes() which turns it into a per-record width - a quarter of the
ring with a 4 KiB floor, the same number and the same reasoning as the client-linkage
BufferWritebackSliceBytes one ring over. The backend can reach neither MG_Remote::Client
(wrong linkage) nor ServerSession (wrong layer), so the hook is the seam; 0 means "do not
slice", which is monolith, where the callback is a direct call with no ring under it.

Both producers loop against ONE helper, XfbWritebackSliceStep, because a second spelling of
the width is how one of them ends up still killing the server. The readback site slices
inside its mapping and still unmaps once; the scatter site slices the staged vector and
keeps the single glBufferSubData whole, since the ES store has no ring and cutting it would
change how many backend calls one application call makes. In-order delivery makes the last
slice's landing imply every earlier one, so the client needs nothing new.

Red-once (R-16): XfbAfterClipDistanceScenario.{AScatteredCapture,APlainCapture}
IntoAMegabyteRangeReachesTheApplication - a 1 MiB bound range, about eight times what one
record can ever be, with the same six capture records; glBindBufferBase over the whole
store is what makes the mirrored range a megabyte. Two cases for the two producers. With
the slicing reverted all six split entries go red on Fatal{EventRingOverflow}
("kEventBufferWriteback needs 1048600 bytes and SEG_EVENT caps ONE record at 131072") -
aborting the test process under inproc, losing the peer under spawn and tcp - while both
monolith controls stay green.

XfbAfterClipDistanceScenario and XfbRepeatedCaptureScenario now join the three split arms
in full; slice 1 could register only OrphanedCaptureTarget* because the rest could not
survive the unsliced event. XfbRepeatedCaptureScenario's gl_NextBuffer compacted-target
shape is the first thing outside the census to exercise the archive-served stride and
varyings (ProgramArchive->Link's xfb fields) on the server, and it passed on all three arms
with no further change.

Gates: integration-split 202/202, integration-spawn 118/118, integration-tcp 121/121,
ctest -L unit 2408/2408, the monolith and split XFB families 120/120, spawn_lane_parity /
fatal_census (79 sites, 0 unmarked) / link_ratchet (186, unchanged) green, G1 .text
0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
This commit is contained in:
2026-09-22 16:41:54 -04:00
parent faf0a1f902
commit 96fa569250
6 changed files with 352 additions and 23 deletions
+54 -10
View File
@@ -1734,6 +1734,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glResumeTransformFeedback != nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// How wide one OnBufferWriteback may be for a capture range of `rangeBytes`, which is
// the whole range when the transport imposes no limit (monolith, where the callback is
// a direct call with no ring under it) or when the range already fits one.
//
// BOTH XFB WRITEBACK PRODUCERS ASK THE SAME FUNCTION. They post from different sources
// - one from a live glMapBufferRange mapping, one from a staged Vector<Uint8> - but
// they post the same KIND of record into the same ring, and a second spelling of the
// width is how one of them ends up being the producer that still kills the server.
SizeT XfbWritebackSliceStep(SizeT rangeBytes) {
const Uint64 slice = MG_Pipe::MGPipeBufferWritebackSliceBytes();
if (slice == 0 || slice >= static_cast<Uint64>(rangeBytes)) return rangeBytes;
return static_cast<SizeT>(slice);
}
#endif
// Mirrors one capture span's results into the frontend CPU shadows. The GPU wrote
// the capture buffers behind the frontend's back, so the shadows that back
// MapBuffer/GetBufferSubData still hold the pre-draw bytes. Buffers whose storage
@@ -1779,11 +1795,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
res, target.start,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(mapped),
static_cast<Uint64>(size),
MG_Pipe::kMGHostSpanSegNone, 0});
// ONE EVENT PER SLICE, NOT ONE PER CAPTURE. The bytes travel
// INLINE in a SEG_EVENT record and the ring refuses any record
// above half its capacity outright, so a whole-range post of a
// capture wider than ~128 KiB was Fatal{EventRingOverflow} - the
// SERVER dying on a large buffer. The slicing shape is
// BufferObject.cpp's one ring over: in-order delivery makes the
// last slice's landing imply every earlier one, so the client
// needs nothing new to reassemble them.
//
// INSIDE THE MAPPING, deliberately: the map is the expensive part
// and the slices are reads of one live pointer, so this unmaps
// once below however many events it posts.
const SizeT step = XfbWritebackSliceStep(size);
for (SizeT off = 0; off < size; off += step) {
const SizeT bytes = std::min(step, size - off);
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
res, target.start + off,
MG_Pipe::MGPBlobRef{
reinterpret_cast<Uint64>(static_cast<Uint8*>(mapped) + off),
static_cast<Uint64>(bytes), MG_Pipe::kMGHostSpanSegNone, 0});
}
} else {
MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the "
"captured bytes of buffer {%u,%u} cannot reach the client shadow",
@@ -1992,11 +2024,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
splitRes, target.start,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(staged.data()),
static_cast<Uint64>(rangeBytes),
MG_Pipe::kMGHostSpanSegNone, 0});
// Sliced for the reason the readback path above is sliced: the bytes
// are INLINE in a SEG_EVENT record and the ring refuses a record wider
// than half its capacity outright, so one whole-range post of a
// capture over ~128 KiB was Fatal{EventRingOverflow} and the server
// died. Only the EVENT is cut; the glBufferSubData below stays one
// call over the whole `staged` vector, because the ES store has no
// ring and cutting it would change how many backend calls one
// application call makes.
const SizeT step = XfbWritebackSliceStep(rangeBytes);
for (SizeT off = 0; off < rangeBytes; off += step) {
const SizeT bytes = std::min(step, rangeBytes - off);
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
splitRes, target.start + off,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(staged.data() + off),
static_cast<Uint64>(bytes),
MG_Pipe::kMGHostSpanSegNone, 0});
}
} else {
MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the "
"scattered capture of buffer {%u,%u} cannot reach the client shadow",