[MG_Backend, MG_IntegrationTest, docs] (P7): the readback family runs on the split arms, and it was already green

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

(a) THE CENSUS, AND IT CAME BACK CLEAN. Twenty-two cases per arm now run on the three split
arms: the two F1 pack-state cases, PixelStoreSweepScenario, FramebufferChurnScenario,
PackedWordReadbackScenario, LayeredTextureReadbackScenario and
DepthStencilReadbackMatrixScenario. The row expected reds and said so ("Expect reds - that
is the point"); the first run was 65/65 before any of this commit's code changes. That is
the finding: the 回读 half was assumed open because nothing had ever run it under a
transport, and the assumption was what was wrong - ScatterTightReadbackIntoPackState and the
server's neutral-pack read were already carrying it. No case needed a fix and no case needed
a named deferral.

DepthStencilReadbackMatrixScenario gets a per-arm block of its own, because the registration
macro cannot carry MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1 and without that pin the
scenario is unfalsifiable here: ES has no core depth/stencil readback, Mesa accepts the
reads anyway, and on llvmpipe every case goes green through a path the device does not have.
Its SeparateDepthAndStencilAttachmentsAreBothReadable SKIPS on all three arms and on monolith
too, naming the driver reason itself - a driver fact, not a transport one.

(b) THE TWO std::abort EMULATIONS, checked rather than assumed. copy-image-shadow-mirror
ALREADY cannot abort: MirrorCopyImageIntoDestinationShadow returns under a transport before
reaching MGPipeUnmigratedEmulation, and has since P5b package i1. get-tex-image-shadow is
retired here, and the reason is that its own premise did not hold: P4a wrote "a split server
holds no shadow to convert", but the level storage this converts IS the server's own, fed by
the staged texture store. When MapMipmapData answers, the conversion reads bytes that exist
on this side. So it converts when the shadow is there and DECLINES BY NAME when it is not
(rule I: MGLOG_E_ONCE + GL_INVALID_OPERATION, destination untouched). std::abort inside a
server answering a client's readback takes the session down for a level it happens not to
hold, and bypasses Session::Fail, which is the funnel the census gate watches. The funnel
keeps its teeth for the other four sites - generate-mipmap-storage,
generate-mipmap-cpu-fallback, generate-mipmap-cpu-filter, texture-remint-pull - which really
do reach into a client address space; fatal_census is unchanged at 79 abort sites because
the abort lives in MGPipeUnmigratedEmulation and that function stays. Stated plainly in the
notes: no case in the armed census reaches this site under a transport (the server answers
GetTextureImage with ReadTextureImageWire, never through the backend's GetTexImage slot), so
the change is defence in depth and has no integration red-once of its own.

(c) THE CASE NOTHING COVERED, and this slice's red-once:
PixelStoreSweepScenario.ReadPixelsIntoAPackBufferHonoursPackStateAndLeavesTheGapsAlone -
glReadPixels into a bound GL_PIXEL_PACK_BUFFER at a non-zero buffer offset with
PACK_ALIGNMENT/ROW_LENGTH/SKIP_ROWS/SKIP_PIXELS all non-default, read back with
glMapBufferRange. It is the only thing that drives EmitTables.cpp's `if (pbo)` branch, the
client servicing the pack PBO itself after the reply lands; the existing sweep case reads
into a client pointer, which is the other half and has unit cover. Every number makes a term
matter: ROW_LENGTH 5 at 4 bpp is 20, which ALIGNMENT 8 rounds up to a 24-byte stride;
SKIP_ROWS 1 and SKIP_PIXELS 2 put the first pixel at image byte 32; the read is issued at
buffer offset 16, so it lands at 48. The whole buffer starts 0xCD and the case asserts both
halves - the rows land where the pack state says, and every other byte is still poison.

Red-once (R-16): dropping just the buffer-offset term from EmitTables.cpp's `start` reds the
three split entries with "readback[at] Which is: '\xCD' (205)" - the destination still poison
because the rows landed 16 bytes early - while the ambient monolith entries pass, since the
driver services its own PBO there. EmitTables.cpp is not part of this commit.

(d) SplitReadbackPackBuffer's comment claimed "The client refuses PACK_BUFFER before
emission". It does not: it KEEPS the PBO and services it after the reply, and a refusal would
have been observable as a GL error. The conclusion (the server must see no pack buffer) was
right and the reason was backwards; rewritten to ID-49's actual split of the work.

Gates: integration-split 225/225 (base 186), integration-spawn 141/141 (base 102),
integration-tcp 144/144, ctest -L unit 2408/2408, integration-magma-split 73/73,
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 834a8917ff
commit ba81453f19
4 changed files with 335 additions and 8 deletions
+50 -7
View File
@@ -66,8 +66,20 @@
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// read_pixels writes server reply scratch. The client refuses PACK_BUFFER before
// emission, so no frontend pack binding can affect this destination.
// A readback on the server writes REPLY SCRATCH, so it has no pack buffer - and this
// function is how every pack-PBO lookup in this file says so under a transport.
//
// THE REASON IS NOT "THE CLIENT REFUSES PACK_BUFFER BEFORE EMISSION", which is what this
// comment used to claim and is not true of the tree (P3b/P4b espryt D1 slice 4). The client
// SERVICES the pack PBO itself, AFTER the reply lands: EmitTables.cpp uploads the reply's
// tight rows into the bound buffer with pbo->UploadSubData, one row at a time at the stride
// the application's GL_PACK_* state implies, so padding and untouched bytes survive. A
// refusal would have been visible as a GL error; what actually happens is an ordinary
// buffer upload the client performs on its own side.
//
// The server's half is ID-49's: read with NEUTRAL pack state into a tight w*h*bpp reply.
// Which is exactly why the server must see NO pack buffer here - not because the client
// forbade one, but because the client kept it.
static const SharedPtr<MG_State::GLState::BufferObject>& SplitReadbackPackBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
return none;
@@ -14917,11 +14929,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
return true;
}
const void* shadow = textureMipmapObject->MapMipmapData(uploadTarget, level);
#if MOBILEGL_PIPE_PUSH
// P4a (D-M). glGetTexImage is answered out of the frontend's own level shadow,
// converted to the requested format and type. Monolith is unchanged; a split server
// holds no shadow to convert, and the readback family as a whole is P3b/P4b's and
// P8's rather than this phase's.
#if MOBILEGL_BUILD_DISAGGREGATED
// P4a (D-M) SAID "a split server holds no shadow to convert" AND ABORTED. That is the
// premise this row of P3b/P4b re-examined, and it is wrong in the only case that can
// get here: the level storage this converts IS the server's own, fed by the staged
// texture store, so when MapMipmapData answers the conversion is reading bytes that
// exist on this side and the answer is right. There is nothing to migrate - the
// emulation was never reaching across the split, it only looked as though it must.
//
// WHEN IT DOES NOT ANSWER, THE VERB DECLINES BY NAME (rule I) RATHER THAN ABORTING.
// std::abort inside a server that is answering a client's readback takes the whole
// session down for a texture level it happens not to hold; GL's own answer for "this
// read cannot be served" is an error code and untouched destination bytes, and a
// client that gets GL_INVALID_OPERATION can carry on. The abort also bypassed
// Session::Fail entirely, which is the funnel the census gate watches.
//
// THE FUNNEL KEEPS ITS TEETH FOR THE OTHER SITES: MGPipeUnmigratedEmulation still
// aborts for generate-mipmap-storage, generate-mipmap-cpu-fallback,
// generate-mipmap-cpu-filter and texture-remint-pull, which really do reach into a
// client address space. Only this call site is retired, and only because its own
// premise did not hold.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (shadow == nullptr) {
MGLOG_E_ONCE("GetTexImage: no level shadow on this side for target=0x%x level=%d, and under an "
"active transport there is no client shadow to fall back to - the read is "
"DECLINED and the destination keeps its bytes",
static_cast<unsigned>(uploadTarget), level);
MGB_CTX->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"DirectGLES", "GetTexImage",
"the level has no shadow on the server and there is no client shadow to convert"));
return false;
}
} else
#endif
MG_Pipe::MGPipeUnmigratedEmulation("get-tex-image-shadow");
#endif
const auto& pixelPackBufferObject =
@@ -14934,7 +14978,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
const void* shadow = textureMipmapObject->MapMipmapData(uploadTarget, level);
if (!shadow) {
return false;
}
@@ -2297,6 +2297,59 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_register_split_arms("XfbAfterClipDistanceScenario.*" "Xfb.")
mgl_itest_register_split_arms("XfbRepeatedCaptureScenario.*" "Xfb.")
# ---- P3b/P4b espryt D1 slice 4: THE READBACK FAMILY ON THE SPLIT ARMS -------------------
#
# The 回读 half of the plan's "回读 / pack state" row. The PACK half is done - server reads
# with neutral pack state, the client scatters (ID-49, EmitTables.cpp) - and the READBACK half
# had no split coverage at all, so every claim about it was a reading of the code rather than
# a run of it. Arming the lanes is the first half of the work and the census is the second:
# what reds here IS the remaining worklist, and each red is either D1's to fix or excluded BY
# NAME with the Fatal quoted, the way the d1 draw block excludes
# MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws.
mgl_itest_register_split_arms("PixelStoreSweepScenario.*" "")
mgl_itest_register_split_arms("PackedWordReadbackScenario.*" "")
mgl_itest_register_split_arms("LayeredTextureReadbackScenario.*" "")
# FramebufferChurnScenario shares PixelStoreSweepScenario.cpp and is the other half of its
# story - ~3300 texture+framebuffer pairs per case, the shape that walked an Adreno driver to
# 1.2 GB of leaked objects and then answered every readback with someone else's pixels. Under
# a transport every one of those pairs is also a create/destroy record round trip, so this is
# the only entry in the readback block that says anything about the OBJECT lifetime half.
mgl_itest_register_split_arms("FramebufferChurnScenario.*" "")
# DepthStencilReadbackMatrixScenario GETS ITS OWN BLOCK, because it needs a variable the
# registration macro cannot carry: MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1.
#
# Without it this scenario is UNFALSIFIABLE on the machines this suite runs on, and the
# ambient DirectGLES.ForcedDepthStencilEmulation. lane says why: OpenGL ES has no depth or
# stencil readback in core, but Mesa accepts the reads anyway, so on llvmpipe every case goes
# green through a native path the Adreno device does not have - deleting the whole emulation
# left all of them passing. A split arm that inherited the ambient default would be measuring
# the same non-path three more times. So the pin is MIRRORED here, which is what the plan's
# row asks for.
foreach(mglItestDsArm IN LISTS MGL_ITEST_MAGMA_ARMS)
mgl_itest_magma_arm_shape(${mglItestDsArm})
string(TOLOWER "${mglItestDsArm}" mglItestDsArmLower)
mgl_itest_join_environment(mglItestDsEnvironment
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_SPLIT_LANE=1"
"MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1"
${mglItestArmTransportEnv} ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.${mglItestDsArm}.ForcedDs."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "DepthStencilReadbackMatrixScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-${mglItestDsArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${mglItestDsEnvironment}"
${mglItestArmProperties}
)
# Into the accumulated list like every other arm entry, so SplitLogPaths gives each of
# these a private MOBILEGL_LOG_FILE_PATH too - two processes append on the spawn arm.
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
# ---- P5e fix1: THE PUSH-MONOLITH ARM, INSIDE THE GATED SET (ID-107) ----------------------
#
@@ -2652,6 +2705,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
foreach(f1Slot ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D GenerateMipmap GenerateMipmapPackedFloat GenerateMipmapDepth)
mgl_itest_register_split_arms("F1WireScenario.${f1Slot}Pixels" "F1.")
endforeach()
# THE TWO PACK-STATE CASES OF F1, which are not ${slot}Pixels-shaped (P3b/P4b espryt D1
# slice 4). ID-49 splits a readback in two: the server reads with NEUTRAL pack state into a
# tight w*h*bpp reply, and the CLIENT scatters those rows into the application's destination
# per its own GL_PACK_*. Only the client half has ever been exercised by a unit case over
# ScatterTightReadbackIntoPackState; these two run the whole seam, one for the padding a
# non-default pack state must leave untouched and one for the overflow that has to be refused
# BEFORE anything is written.
mgl_itest_register_split_arms(
"F1WireScenario.TextureAndFramebufferReadsPreservePackBufferPadding" "F1.")
mgl_itest_register_split_arms(
"F1WireScenario.TextureReadbackRejectsPackedDestinationOverflowBeforeWriting" "F1.")
# P5f fm: the framebuffer/copy/mipmap verbs now consume server records on Magma too.
#
# P7 package L: THE SAME CASES ON ALL THREE TRANSPORTS. The flavour is everything this lane
@@ -31,6 +31,7 @@
// and DirectVulkan is the built-in control.
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
@@ -257,6 +258,117 @@ namespace MGITest {
glDeleteTextures(1, &texture);
}
// THE ONE READBACK DESTINATION NOTHING COVERED (P3b/P4b espryt D1 slice 4): a bound
// GL_PIXEL_PACK_BUFFER, with a non-default GL_PACK_* state, read back afterwards through
// glMapBufferRange.
//
// The case above reads into a CLIENT POINTER, which under a transport is served by
// ScatterTightReadbackIntoPackState - a pure function with unit cover of its own. A PBO
// destination is a different half of the same seam and had none: the server reads with
// NEUTRAL pack state into a tight w*h*bpp reply (ID-49), and the CLIENT then services the
// PBO itself after the reply lands, uploading the tight rows one at a time at the stride the
// application's pack state implies (EmitTables.cpp's `if (pbo)` branch). Nothing in the tree
// ran that branch, and it is arithmetic - a stride, an alignment round-up, two skips and a
// buffer offset - which is the kind of code that is either exactly right or quietly wrong.
//
// WHAT MAKES IT A TEST RATHER THAN A SMOKE CHECK is the poison. Every byte of the buffer
// starts 0xCD; the rows land where the pack state says and NOTHING ELSE MOVES. A branch that
// uploaded the reply whole, or ignored the buffer offset, or rounded the stride the other
// way, writes over poison that this asserts is still there.
//
// The numbers are chosen so each term does something: ROW_LENGTH 5 at 4 bytes per pixel is
// 20, which ALIGNMENT 8 rounds up to a 24-byte stride (a stride equal to the row length would
// not tell a missing round-up from a present one); SKIP_ROWS 1 and SKIP_PIXELS 2 move the
// first pixel to byte 24 + 8 = 32 of the image; and the read is issued at buffer offset 16,
// so the first pixel lands at 48 and a branch that dropped the offset would land at 32.
TEST_F(PixelStoreSweepScenario, ReadPixelsIntoAPackBufferHonoursPackStateAndLeavesTheGapsAlone) {
if (!Ready()) return;
ResetAllPixelStoreModes();
constexpr int kW = 2;
constexpr int kH = 2;
constexpr GLint kAlignment = 8;
constexpr GLint kRowLength = 5;
constexpr GLint kSkipRows = 1;
constexpr GLint kSkipPixels = 2;
constexpr std::size_t kBpp = 4;
constexpr std::size_t kOffset = 16;
constexpr std::size_t kPboBytes = 256;
constexpr std::uint8_t kPoison = 0xCD;
// The same arithmetic GL defines and EmitTables.cpp implements, written out here rather
// than borrowed, so the test cannot agree with the code by construction.
constexpr std::size_t kStride = ((kRowLength * kBpp) + kAlignment - 1) / kAlignment * kAlignment;
constexpr std::size_t kFirst = kOffset + kSkipRows * kStride + kSkipPixels * kBpp;
static_assert(kStride == 24, "the alignment round-up must actually round");
static_assert(kFirst == 48, "the skips and the buffer offset must all move the first pixel");
// Four distinct opaque colours, so a row or a pixel landing in the wrong place is a
// wrong VALUE and not just a wrong offset.
const std::uint8_t texels[kW * kH * kBpp] = {
0x11, 0x22, 0x33, 0xFF, 0x44, 0x55, 0x66, 0xFF,
0x77, 0x88, 0x99, 0xFF, 0xAA, 0xBB, 0xCC, 0xFF,
};
GLuint texture = 0, framebuffer = 0, pbo = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kW, kH, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels);
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
ASSERT_EQ(FirstGLError(), 0u);
const std::vector<std::uint8_t> poison(kPboBytes, kPoison);
glGenBuffers(1, &pbo);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(kPboBytes), poison.data(), GL_STREAM_READ);
ASSERT_EQ(FirstGLError(), 0u);
glPixelStorei(GL_PACK_ALIGNMENT, kAlignment);
glPixelStorei(GL_PACK_ROW_LENGTH, kRowLength);
glPixelStorei(GL_PACK_SKIP_ROWS, kSkipRows);
glPixelStorei(GL_PACK_SKIP_PIXELS, kSkipPixels);
glReadPixels(0, 0, kW, kH, GL_RGBA, GL_UNSIGNED_BYTE,
reinterpret_cast<void*>(static_cast<std::uintptr_t>(kOffset)));
EXPECT_EQ(FirstGLError(), 0u);
ResetAllPixelStoreModes();
std::vector<std::uint8_t> readback(kPboBytes, 0);
const void* mapped =
glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(kPboBytes), GL_MAP_READ_BIT);
ASSERT_NE(mapped, nullptr) << "the pack buffer could not be mapped for readback";
std::memcpy(readback.data(), mapped, kPboBytes);
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
// Every byte the pack state names carries its texel; every other byte is still poison.
std::vector<bool> written(kPboBytes, false);
for (int y = 0; y < kH; ++y) {
for (int x = 0; x < kW; ++x) {
for (std::size_t c = 0; c < kBpp; ++c) {
const std::size_t at = kFirst + static_cast<std::size_t>(y) * kStride +
static_cast<std::size_t>(x) * kBpp + c;
ASSERT_LT(at, kPboBytes);
written[at] = true;
EXPECT_EQ(readback[at], texels[(y * kW + x) * kBpp + c])
<< "pixel (" << x << ", " << y << ") component " << c << " at buffer byte " << at
<< (readback[at] == kPoison ? " is still the poison: nothing was written here" : "");
}
}
}
for (std::size_t i = 0; i < kPboBytes; ++i) {
if (written[i]) continue;
EXPECT_EQ(readback[i], kPoison)
<< "buffer byte " << i << " is outside every row the pack state names and must not have moved";
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteBuffers(1, &pbo);
glDeleteFramebuffers(1, &framebuffer);
glDeleteTextures(1, &texture);
}
// The leak regression. Each iteration is one complete CTS inner step, and every readback has
// to be exactly the gradient THIS iteration uploaded - never the previous one's. Before the
// missing destructors were added, the driver-side framebuffer count grew without bound here.
+109 -1
View File
@@ -220,7 +220,115 @@ was re-run clean at 121/121.)*
## slice 4 — readback close-out
`RESULT: (pending)`
The plan's 回读 row had four parts. The headline is part (a): **the census came back clean**, which
is not what the row expected ("Expect reds — that is the point").
### (a) the readback lane census
Registered on the three split arms (`integration-split` / `-spawn` / `-tcp`), 22 cases per arm:
| family | cases/arm | result |
|---|---|---|
| `F1WireScenario.TextureAndFramebufferReadsPreservePackBufferPadding` | 1 | green |
| `F1WireScenario.TextureReadbackRejectsPackedDestinationOverflowBeforeWriting` | 1 | green |
| `PixelStoreSweepScenario.*` | 2 (+1 new, below) | green |
| `FramebufferChurnScenario.*` | 1 | green |
| `PackedWordReadbackScenario.*` | 3 | green |
| `LayeredTextureReadbackScenario.*` | 2 | green |
| `DepthStencilReadbackMatrixScenario.*` | 12 | green |
**No case needed a fix and no case needed a named deferral.** The first run was 65/65 (63 cases +
the two TCP fixture entries), before any of this slice's code changes. That is a real finding and
it is worth stating plainly: the 回读 half was assumed open because nothing had ever run it under a
transport, and the assumption, not the code, was what was wrong. `ScatterTightReadbackIntoPackState`
and the server's neutral-pack read were already carrying it.
Two honest caveats, because a skip is not coverage:
* `DepthStencilReadbackMatrixScenario.SeparateDepthAndStencilAttachmentsAreBothReadable` **SKIPS on
all three arms** — and on monolith too. It is a driver fact, named by the case itself: *"this
driver cannot host separate DEPTH_COMPONENT24 and STENCIL_INDEX8 attachments"*. Not a transport
fact, nothing deferred.
* `DepthStencilReadbackMatrixScenario` needs `MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1`
mirrored into the split arms, which the registration macro cannot carry, so it gets its own
per-arm block (`DirectGLES.{Split,Spawn,Tcp}.ForcedDs.`). Without the pin the scenario is
unfalsifiable here: ES has no core depth/stencil readback, Mesa accepts the reads anyway, and on
llvmpipe every case goes green through a path the Adreno device does not have — deleting the
whole emulation left all of them passing. A split arm inheriting the ambient default would have
measured the same non-path three more times.
### (b) the two `std::abort` emulations
The row said both *"still `std::abort` under any non-monolith transport"*. Checked rather than
assumed, and the tree disagrees on one of them:
* **`copy-image-shadow-mirror` already cannot abort.** `MirrorCopyImageIntoDestinationShadow`
returns under `if (MG_Config::Transport != Monolith)` **before** reaching
`MGPipeUnmigratedEmulation`, and has since P5b package i1 (its own comment names the ruling and
the two backstop Fatals that bound what the skip loses). Nothing to do; the row is stale.
* **`get-tex-image-shadow` is retired here**, and the interesting part is *why* it was there. P4a's
comment reads "a split server holds no shadow to convert" — but the level storage this function
converts **is the server's own**, fed by the staged texture store. When `MapMipmapData` answers,
the conversion reads bytes that exist on this side and the answer is right; the emulation was
never reaching across the split, it only looked as though it must. So: convert when the shadow
is there, and decline by name (rule I: `MGLOG_E_ONCE` + `GL_INVALID_OPERATION`, destination
untouched) when it is not. `std::abort` inside a server answering a client's readback takes the
whole session down for a texture level it happens not to hold, and it bypasses `Session::Fail`,
which is the funnel the census gate watches.
**The funnel keeps its teeth for the other four sites** — `generate-mipmap-storage`,
`generate-mipmap-cpu-fallback`, `generate-mipmap-cpu-filter`, `texture-remint-pull` — which
really do reach into a client address space. Only this call site is retired, and only because its
own premise did not hold. `fatal_census.py` is unchanged at 79 abort sites, because the abort
lives in `MGPipeUnmigratedEmulation` and that function stays.
**Reachability, said out loud**: no case in the armed census reaches this site under a transport,
and the analysis says why — the client emits `MGPWireOp::GetTextureImage` and the server answers
it with `ReadTextureImageWire` (a scratch-FBO read), never through the backend's `GetTexImage`
slot. So this change is defence in depth and **has no integration-level red-once of its own**;
saying otherwise would be inventing one.
### (c) the case nothing covered
`PixelStoreSweepScenario.ReadPixelsIntoAPackBufferHonoursPackStateAndLeavesTheGapsAlone`:
`glReadPixels` into a bound `GL_PIXEL_PACK_BUFFER` at a non-zero buffer offset, with
`PACK_ALIGNMENT`/`ROW_LENGTH`/`SKIP_ROWS`/`SKIP_PIXELS` all non-default, read back with
`glMapBufferRange`. This is the only thing that drives `EmitTables.cpp`'s `if (pbo)` branch — the
client servicing the pack PBO itself after the reply lands, uploading tight rows one at a time at
the stride the pack state implies. The existing sweep case reads into a client POINTER, which is
`ScatterTightReadbackIntoPackState`'s half and has unit cover; the PBO half had none, and it is
pure arithmetic (stride, alignment round-up, two skips, a buffer offset).
Every number is chosen to make a term matter: `ROW_LENGTH` 5 × 4 bpp = 20, which `ALIGNMENT` 8
rounds up to a **24**-byte stride; `SKIP_ROWS` 1 + `SKIP_PIXELS` 2 put the first pixel at image
byte 32; the read is issued at buffer offset 16, so it lands at **48**. The whole buffer starts
`0xCD` and the case asserts both halves: the rows land where the pack state says, and **every other
byte is still poison**.
**RESULT — red once, then green.** Dropping just the buffer-offset term from
`EmitTables.cpp:1001` (`start = pixels + …` → `start = …`) and rebuilding:
```
3794 DirectGLES.Split.PixelStoreSweepScenario.ReadPixelsIntoAPackBuffer… (Failed)
3797 DirectGLES.Spawn.… (Failed)
3800 DirectGLES.Tcp.… (Failed)
```
`PixelStoreSweepScenario.cpp:353: readback[at] Which is: '\xCD' (205)` — the destination is still
poison because the rows landed 16 bytes early. The ambient monolith entries passed in the same run
(the driver services its own PBO there), so the three reds are a transport fact. `EmitTables.cpp`
was restored immediately and is **not** part of this commit.
### (d) the stale comment
`SplitReadbackPackBuffer`'s comment claimed *"The client refuses PACK_BUFFER before emission, so no
frontend pack binding can affect this destination."* The client does no such thing — it **keeps**
the PBO and services it after the reply. The conclusion (the server must see no pack buffer) was
right; the reason was backwards, and a refusal would have been observable as a GL error. Rewritten
to the actual reason, with ID-49's split of the work named.
**Gates.** `integration-split` 225/225 (base 186), `integration-spawn` 141/141 (base 102),
`integration-tcp` 144/144 (142 labelled + 2 fixtures; base 103 + 2), `ctest -L unit` 2408/2408,
`integration-magma-split` 73/73, parity / census (79 sites, 0 unmarked) / ratchet (186) green.
G1 `.text` `0xa52203`, both nm lists byte-identical.
## slice 5 — four transport-aware resolvers