EnsureGeneratedMipmapStorageAllocated branched on the transport. The split arm
planned the generation window from BASE_LEVEL/MAX_LEVEL and refused to touch
immutable storage (70c8230b); the monolith arm rebuilt the chain from the bound
object's level-0 extent and then truncated it. Through a view that is
destructive in a way the split arm had already been fixed for: a view's level-0
extent is one slice of the owner's layers, so a two-layer view of a four-layer
array resized every owner level the generation touched to two layers - the
storage behind the owner's other layers was gone - and redefined the owner's
levels out of the view's window as well.
Both arms now take one path. An immutable texture or a view returns before any
allocation: it already owns every level generation can write. A mutable texture
gets levels from the plan's Base..End window and nothing else moves - the chain
is not truncated, because neither MAX_LEVEL nor a generate is a truncation (a
level-0 respecification drops the stale tail itself, in
DiscardMipmapChainOnBaseRespecification). MipmapGenerationPlan.h is no longer
behind MOBILEGL_BUILD_DISAGGREGATED for the same reason: the window is a
property of the GL call, not of how the backend is reached, and the plan the
wire carrier publishes (MGPMipPlan::LevelCount, end-exclusive) is this one.
The window rule was asserted only by the split-only F1WireScenario, whose
fixture skips under every other transport - so the monolith arm, which is what
a non-disaggregated library runs for every application, had no coverage at all.
TextureViewScenario.GenerateMipmapThroughAViewLeavesTheOwnerShapeAlone runs in
an integration-gpu lane under both transports and pins the owner's layer counts,
its own level extents, and the pixels of the levels and layers outside the
view's window. It is DirectGLES-only, and the skip names why: Magma reallocates
the chain through the bound object in its own generate path
(DirectVulkan/Renderer/VulkanRenderer.cpp's EnsureGenerateMipmapStorageAllocated),
and a CPU sub-image into the layers of mip 2 or 3 does not read back through
Magma at all - a run of the same scenario with the generate removed fails
identically - so the per-level colours this case is built on are unobservable on
that backend rather than merely wrong.
DirectVulkan resolved every default-framebuffer role against
m_imageIndexAcquired. Present() advances that to the image it acquires for
the frame that is about to start, and a present record is kWaitPresent - the
client does not wait for the apply - so a readback belonging to the frame
being presented (the retrace snapshot, taken at the swap call) can be applied
after the acquire and then resolve an image nothing had written. That is the
pure-black sundial-lite DirectVulkan retrace frame; DirectGLES is immune
because the driver keeps handing back the buffer the app drew into.
Split the GL-visible index out (VulkanRenderer::m_defaultFramebufferImageIndex)
and keep it out of the present/acquire path: it only re-points at the acquired
image when something WRITES the default framebuffer (DefaultFramebufferWriteIndex,
called at the FBO-0 render-pass creation, the deferred-clear-free direct writes
and the wire draw/clear/blit resolutions) and is reset on RecreateSwapchain.
Reads - glReadPixels both arms, the parked-clear materialization, a blit or copy
SOURCED from FBO 0, ResolveWireImage's read role (carrying the resolved index in
WireImage) - resolve DefaultFramebufferReadIndex instead, i.e. whatever that is.
A readback that resolves a swapchain image with no recorded write now says so
once (MGLOG_W_ONCE, both arms) instead of silently handing back raw pixels.
New integration scenario DirectVulkan|DirectGLES.DefaultFramebufferAcrossSwap
pins both halves: a readback after a swap still sees the drawn frame, and the
first draw after a swap lands in the image the readback then resolves.
glGenerateMipmap's native arm carries BASE_LEVEL/MAX_LEVEL of the view's own
ES name, so the LEVEL axis was already clipped the way the view describes, but
the layer set it writes is every layer of the storage the view aliases. A
generation through a windowed texture view therefore rewrote the owner's layers
outside the window (F1WireScenario
.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow).
Both arms now read the window and generate the chain with per-layer blits
between the owner's levels MinLevel + L, over layers
[MinLayer, MinLayer + NumLayers), and leave the native call for the shapes it
already gets right: no view, a view whose layer window covers the whole
storage, a multisample view, and aspects the layer blits do not carry. The
record arm reads the window from the sampler view CSO with the same validation
SyncTextureViewToBackendByRecord uses (Fatal{ProtocolCorruption} on a stale or
foreign record); the monolith arm reads the same numbers off the frontend view
object. ScratchFBOImpl gains EnsureDepthAttachmentLayer for the depth-only
aspect.
PipeFill.cpp reaches MG_Remote::Client::MGPipeStageChunkBytes through
GpuWritePending.h, which the TU did not include: the declaration was visible to
the helper's own TU and to no other, so both MobileGL translation units for
PipeFill.cpp failed to compile while the shared objects it defines built fine.
PipeFill.cpp.
ResourceEmit.AWideBufferSubDataSplitsAtTheStageChunkBytes: the walk driven at
MGPipeStageChunkBytesFor's clamp (a quarter of the segment, floored at 4096 and
never above the segment), with the trace's own 128 MiB arena against the default
32 MiB segment - sixteen contiguous ascending pieces, each encodable by the
record builder, covering the range exactly once. The no-session answer (0 = keep
the record's own bound) is asserted beside it.
ResourceEmit.AZeroByteRespecifyDefinesTheStoreAndCarriesNoContent: armed the way
the split-only unit cases are (Transport = InProcess, an op table, and the caps
mirror R-8's liveness gate reads), BufferObject::Respecify(0, nullptr) must not
abort, must land Width == 0 && HasDefinedContent == 1 on the applier, and must
emit no resource_subdata at all. The drive runs in a child because the defect's
verdict is std::abort() and a case that aborts its own binary reports nothing.
MG_Test/Pipe/ResourceEmitTest.cpp.
glBufferData(target, 0, NULL, usage) DEFINES its store - that is not the
orphaning idiom - so HasDefinedContent is set, and MappedData() answers a
non-null pointer for it because the shadow reserves one byte whatever the size
(PipeResource.h:140-143). Reading MappedData() alone therefore put the call on
the split arm's respecify(nullptr)-plus-follow-up shape, whose walk emits
nothing for a zero-length range (ResourceTracker.h:253), and the
InitialBytesNotCarried self-check aborted by name on the first such call in the
bsl-esc-menu trace.
The predicate is 'HasDefinedContent && GetSize() > 0': there are bytes here,
not merely a defined store. The split arm's own condition is untouched - moving
it instead would send the zero-byte case to the UncarriedInitialBytes refusal
in the wire escape.
PipeFill.cpp.
One resource_subdata record's blob is staged whole in SEG_STAGE, so a range
larger than that arena is Fatal{RingOverrun, SEG_STAGE} at the encoder rather
than a split: the CI traces abort there on a 128 MiB arena's whole-buffer
follow-up against a 32 MiB segment.
- ClientSession::StageCapacityBytes(), the twin of EventRingCapacityBytes, so
the emitter can size against the arena the codec allocates from.
- MGPipeStageChunkBytes(): clamp(segment/4, 4096, segment), 0 when there is
nothing to fit (monolith, the server role's own uploads, no session).
- PipeFill.cpp's two buffer content walks take that cap instead of the
record's own 2^32-1 bound, so an over-long range becomes several contiguous
ascending records. StagedShadowStore::Adopt already accumulates coverage.
ClientSession.h/.cpp, GpuWritePending.h/.cpp, PipeFill.cpp.
The remove-artifact-clutter jobs deleted trace fixtures and results while
reading job/artifact inventories through unchecked process substitution, so
a failed GitHub API page looked like 'no failed cases' and fixtures for a
red retrace could be deleted. test.yml cleanup also did not wait for the
retrace-split lane even though those jobs download the same fixtures.
Read both paginated inventories into files before any deletion so an API
failure fails the step, add retrace-split to the cleanup dependencies, and
retain fixtures plus result artifacts for failed cases in all three lanes.