mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Fix, Test] (MG_Remote/Client, MG_Impl/Pipe): c1 round 3 - route the five by-address delete sites (B1); observe the client arm (B3); readback requires Status=OK and the exact extent and refuses a pack-PBO read by name (M2/M8/codex11/ID-57); one ERROR->Fatal{ReplyError} rule across the two escapes and an honest ShutDown decline (M4/codex5); InitialBytesNotCarried is role-aware (M5); a routed call during teardown refuses by name not runs the applier (codex4); caps adopted on every make-current (codex12); set_dynamic_state carries an optional blob; PACK_SKIP_IMAGES ignored for a 2-D read (codex6); one tight-size function for production and test (M3/codex10); redcheck 9/9 red each on its own string (M6)
This commit is contained in:
@@ -743,7 +743,18 @@ namespace MobileGL::MG_Pipe {
|
||||
// SPLIT-ONLY, and that is load-bearing for G2: under monolith the applier reads
|
||||
// `initialBytes` directly and a second upload would be a real behaviour change in the
|
||||
// arm the split arm is measured against.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && initialBytes != nullptr) {
|
||||
//
|
||||
// ROLE-AWARE (M5). Under inproc the apply thread reaches this very emitter when the
|
||||
// server's own backend respecifies a buffer (c1-v2 §4 R-17.3: that is where
|
||||
// Fatal{BarrierTimeout, "ResourceRespecify"} from mgl-srv-apply came from). On that
|
||||
// thread this branch would emit CLIENT wire records - which the server role does not
|
||||
// produce, so ClientWireRecordsEmitted() would not move and the self-check below would
|
||||
// abort the SERVER by name; it would also run the respecify(nullptr)+follow-up shape,
|
||||
// giving the server role a path monolith does not have. So the server role takes the
|
||||
// ELSE below, exactly as monolith does. RunsAsTheServerRole() is v1's
|
||||
// ServerLoop::OnApplyThread(), false on the GL thread that owns this fill.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
!MG_Remote::Client::RunsAsTheServerRole() && initialBytes != nullptr) {
|
||||
MGPipeRouteResourceRespecify(desc, nullptr);
|
||||
// COUNTED, NOT ASSUMED, and this is the only thing that can gate the follow-up at
|
||||
// all. Dropping the walk below leaves a respecify that went out with nullptr and
|
||||
@@ -859,8 +870,11 @@ namespace MobileGL::MG_Pipe {
|
||||
//
|
||||
// EXACTLY THAT RANGE, not the whole buffer: the flush's own [offset, size) is what
|
||||
// the ladder rewrites, and staging more would be the coverage WIDENING ID-37 forbids.
|
||||
// Split-only, for the respecify's G2 reason.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && size != 0) {
|
||||
// Split-only, for the respecify's G2 reason - and ROLE-AWARE for M5's reason, the twin of
|
||||
// the respecify branch above: the apply thread flushing the server's own buffer emits no
|
||||
// client wire records, so it takes the plain route below rather than this split follow-up.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
!MG_Remote::Client::RunsAsTheServerRole() && size != 0) {
|
||||
MGPipeEmitResourceSubData(buffer, offset, size);
|
||||
}
|
||||
#endif
|
||||
@@ -1510,9 +1524,21 @@ namespace MobileGL::MG_Pipe {
|
||||
// Step 1, shared: the wire delete goes out FIRST and only for a PUBLISHED handle, and
|
||||
// the latch is cleared with it so a second death path - a composite's two, a backend's
|
||||
// redundant notice - cannot emit a second delete for a record that is already gone.
|
||||
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle, void (*apply)(const MGPHandleOnly&)) {
|
||||
//
|
||||
// `route` IS A MGPipeRoute<Name> AND NEVER A MGPipeApply<Name> (B1). R-17 converted the
|
||||
// 40 direct applier CALLS to route calls by renaming `MGPipeApply<Name>(` -> but these
|
||||
// five sites take the entry point BY ADDRESS, `&MGPipeApply<Name>`, so the call-expression
|
||||
// rename missed them and four routed rows (DeleteSamplerView, DeleteShaderState,
|
||||
// DeleteSamplerState, ResourceDestroy for textures/renderbuffers) still ran the applier
|
||||
// synchronously on the GL thread under split - two writers on g_applier with the barrier
|
||||
// not consulted, and under spawn a silent no-op that leaks every one of those objects.
|
||||
// The parameter type is the route's, which is byte-identical to the applier's
|
||||
// (const MGPHandleOnly&, void return), so the fix is `&MGPipeRoute<Name>` at the five call
|
||||
// sites; the grep gate scripts/../p5-c1 redcheck refuses any `&MGPipeApply` under MG_Impl/.
|
||||
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle,
|
||||
void (*route)(const MGPHandleOnly&)) {
|
||||
if (!MGPipeHandleIsPublished(kind, handle)) return false;
|
||||
apply(HandleOnly(kind, handle));
|
||||
route(HandleOnly(kind, handle));
|
||||
MGPipeNoteHandleUnpublished(kind, handle);
|
||||
return true;
|
||||
}
|
||||
@@ -1533,7 +1559,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeRouteDeleteSamplerView);
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
||||
@@ -1554,7 +1580,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeRouteResourceDestroy);
|
||||
// The emitter retires its entry while the handle still resolves (C-2): the drain list
|
||||
// drops the dead texture's levels, the raw pointer goes, the built-in sampler's cache
|
||||
// reference is given back, the latches and the sticky mask are cleared.
|
||||
@@ -1595,7 +1621,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeRouteResourceDestroy);
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteRenderbufferDied(handle); });
|
||||
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
||||
@@ -1632,7 +1658,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeRouteDeleteSamplerState);
|
||||
// NOTHING TO RETIRE IN AN EMITTER FOR THIS KIND, stated rather than implied: a sampler
|
||||
// CSO is content-addressed and belongs to a value, so no emitter keeps an entry under
|
||||
// a SamplerObject's handle - the cache's entries are keyed by value and reference
|
||||
@@ -1653,7 +1679,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeRouteDeleteShaderState);
|
||||
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
|
||||
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||
|
||||
@@ -210,7 +210,25 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// InitCapabilities' answer comes from a snapshot the server can only publish once its
|
||||
// own InitCapabilities has run - and ServerMakeEGLCurrent is what publishes it.
|
||||
if (!Server::ServerMakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
return MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
if (!MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
|
||||
// R-12 ARM (a) ON EVERY SUCCESSFUL MAKE-CURRENT (codex 12). ServerMakeEGLCurrent above
|
||||
// republishes the caps snapshot on every call (ServerLoop.cpp:613-628), but the base
|
||||
// class only runs InitCapabilities - the one place that pumps and refreshes - on the
|
||||
// FIRST make-current per surface (BackendObject.cpp:341-347). A repeated make-current
|
||||
// onto an already-initialised surface therefore left the client mirror one generation
|
||||
// behind while unpumped snapshots accumulated, so a cap getter or a shader compile before
|
||||
// the next Present read the prior mirror. Adopting here closes that: "a second snapshot
|
||||
// arrival IS the invalidation" (R-12) now holds AT the make-current that caused it. It is
|
||||
// idempotent - on the first make-current InitCapabilities already pumped, so this adopts
|
||||
// 0 - and a release-current (draw/ctx cleared) publishes nothing and is skipped.
|
||||
if (draw != EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
|
||||
if (ClientSession* session = ClientSession::Active()) {
|
||||
session->PumpControlPlane();
|
||||
RefreshFormatCapabilities();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
@@ -247,4 +265,18 @@ namespace MobileGL::MG_Remote::Client {
|
||||
MG_Backend::BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
// NO strong CreateRemoteBackendObject() lives here, and the reason is a link fact, not an
|
||||
// oversight. v1's Init.cpp calls MG_Remote::Client::CreateRemoteBackendObject() and ships a
|
||||
// __attribute__((weak)) placeholder for it beside ServerLoop that aborts by name; its comment
|
||||
// expects "c1's strong definition [to] displace it at link time". A strong definition here
|
||||
// does NOT: libMobileGL is linked from a static archive, ServerLoop.o (weak) is already in
|
||||
// the link and satisfies Init's reference, and nothing else references this TU's
|
||||
// CreateRemoteBackendObject - so BackendObject_Remote.o is never pulled to override it, and
|
||||
// the weak's abort fires (measured: readelf shows one local symbol, the log shows
|
||||
// Fatal{UnimplementedRemoteBackendObject}). The integrator's Init.cpp hunk works because it
|
||||
// constructs BackendObject_Remote DIRECTLY (MakeUnique<BackendObject_Remote>), which both
|
||||
// references this object - forcing its TU into the link - and bypasses the weak symbol. So
|
||||
// the merge-time construction stays v1's Init.cpp edit (or a --whole-archive / forced
|
||||
// reference the integrator adds); see c1-v3.md.
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
@@ -576,12 +576,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
|
||||
void ClientSession::Stop() {
|
||||
// FIRST, BEFORE ANYTHING ELSE GOES AWAY (R-17). Every later step here frees something
|
||||
// an emitter dereferences - the rings, the segments, the transports - so a GL call
|
||||
// that arrives during teardown must already be looking at the monolith arm. Putting
|
||||
// the monolith adapters back rather than nulling the rows is deliberate: a null row is
|
||||
// the pre-migration state and would be an undiagnosed crash, and the adapter is a
|
||||
// correct answer for a process that no longer has a session.
|
||||
// FIRST, BEFORE ANYTHING ELSE GOES AWAY (R-17 / codex 4). Every later step here frees
|
||||
// something an emitter dereferences - the rings, the segments, the transports - so a
|
||||
// routed GL-thread call that arrives during teardown must not run the applier on the
|
||||
// caller and must not reach a half-freed ring. Round 2 reinstalled the monolith adapters
|
||||
// HERE, which is running the applier on the caller - the forbidden path table 3 draws.
|
||||
// Uninstall now RAISES A FLAG and leaves the wire rows in place; the next routed call
|
||||
// aborts by name (Fatal{ClientTablesUninstalled}) inside RequireSession before it touches
|
||||
// anything. The monolith adapters go back only at the END of teardown
|
||||
// (ReinstallMonolithAfterTeardown), for the at-exit ~BufferObject deletes that reach a
|
||||
// process with no session at all - and by then the rings are gone, so the applier a
|
||||
// monolith adapter runs is a defined no-op rather than a use-after-free.
|
||||
UninstallClientWireTables();
|
||||
if (!m_started) {
|
||||
// Start's own failure paths land here with a half-built session. FIVE of them are
|
||||
@@ -599,6 +604,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
m_clientTransport.reset();
|
||||
m_serverTransport.reset();
|
||||
m_transport = nullptr;
|
||||
// The rings are gone; put the monolith adapters back for a process that will make no
|
||||
// more routed calls except, possibly, at-exit deletes (codex 4).
|
||||
ReinstallMonolithAfterTeardown();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -655,6 +663,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
if (g_active == this) {
|
||||
g_active = nullptr;
|
||||
}
|
||||
// AND ONLY NOW the monolith adapters go back (codex 4): every ring an emitter would have
|
||||
// used is freed above, so from here a routed call - an at-exit ~BufferObject delete - runs
|
||||
// the applier exactly as it does under monolith, which is the correct answer for a
|
||||
// process that no longer has a session. During the whole span above, the raised flag made
|
||||
// any routed call abort by name instead.
|
||||
ReinstallMonolithAfterTeardown();
|
||||
}
|
||||
|
||||
Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; }
|
||||
@@ -673,8 +687,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// "always accept" is ID-39's 66 lost DirectVulkan uploads with a wire in between.
|
||||
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||
const void* varTail, Uint64 varTailBytes, void* replyOut,
|
||||
Uint64 replyBytes, Int32* statusOut) {
|
||||
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) {
|
||||
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError;
|
||||
if (replySizeOut != nullptr) *replySizeOut = 0;
|
||||
if (!m_started) {
|
||||
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - EmitAndWait on a session that has "
|
||||
"not started. There is no fall-through: a record that could not be emitted "
|
||||
@@ -735,9 +750,15 @@ namespace MobileGL::MG_Remote::Client {
|
||||
if (wait == Transport::SessionWait::ShutDown) {
|
||||
// The doorbell died: the server went away. The only thing that returns from a
|
||||
// kWaitForever park, and therefore the only way a client blocked in the barrier
|
||||
// survives a server that is gone. Not a Fatal - teardown legitimately reaches here.
|
||||
// survives a server that is gone. It is teardown, not a server fault - so the answer
|
||||
// handed back is DECLINED, not the ERROR the status was pre-set to (M4): a
|
||||
// reply-owning row that saw ERROR here would abort Fatal{ReplyError} on a shutting-
|
||||
// down session, which is what the "teardown legitimately reaches here" comment
|
||||
// promised would NOT happen. DECLINED is honest - "the verb did not happen" - and the
|
||||
// acceptance rows already treat it as `false` / nullptr without aborting.
|
||||
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusDeclined;
|
||||
MGLOG_E("MG_Remote client: the barrier for %s (seq %llu) woke on a dead doorbell; the "
|
||||
"server is gone and this verb did not happen",
|
||||
"server is gone and this verb did not happen (reported as DECLINED, not ERROR)",
|
||||
Wire::WireOpName(op), static_cast<unsigned long long>(seq));
|
||||
return seq;
|
||||
}
|
||||
@@ -770,6 +791,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
std::abort();
|
||||
}
|
||||
if (statusOut != nullptr) *statusOut = status;
|
||||
if (replySizeOut != nullptr) *replySizeOut = replySize;
|
||||
if (status == Wire::ReplySink::kStatusError) {
|
||||
MGLOG_E("MG_Remote client: %s (seq %llu) answered ERROR", Wire::WireOpName(op),
|
||||
static_cast<unsigned long long>(seq));
|
||||
|
||||
@@ -96,11 +96,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// name where a kReplySlot answer lands; pass {nullptr, 0} for a call that has none.
|
||||
// Returns the record's seq, which is also its reply-slot id.
|
||||
//
|
||||
// `replySizeOut` (optional) receives the answer's OWN byte count as the server stamped
|
||||
// it - which is not always `replyBytes`: a short OK reply stamps fewer, and a DECLINE or
|
||||
// ERROR stamps 0. ReadPixels is the one caller that must know, because scattering a
|
||||
// reply that arrived short would spray stale bytes as pixels (M2 / codex 11); it reads
|
||||
// this and refuses `replySize != DstSize` by name rather than trust the copy.
|
||||
//
|
||||
// Waiting is spin(MOBILEGL_IPC_SPIN_US) then park, through Doorbell::Wait, with
|
||||
// producerParked set before blocking - the shape Doorbell.h:121 already implements.
|
||||
Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||
const void* varTail, Uint64 varTailBytes, void* replyOut,
|
||||
Uint64 replyBytes, Int32* statusOut);
|
||||
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut = nullptr);
|
||||
|
||||
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
|
||||
// red; it must be run once and the way it goes red recorded.
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -96,7 +97,11 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// last clause is what the control checks with a sentinel.
|
||||
Bool ReadbackPackStateIsTight(GLsizei width, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack) {
|
||||
if (pack.SkipRows != 0 || pack.SkipPixels != 0 || pack.SkipImages != 0) return false;
|
||||
// SKIP_IMAGES (and IMAGE_HEIGHT) are NOT consulted: glReadPixels is a 2-D read and GL
|
||||
// ignores the image-level pack parameters for it, exactly as the monolith conversion
|
||||
// path does at DirectGLES.cpp:10905 (honorPackImageParams=false). A non-zero
|
||||
// SkipImages therefore does not make the layout non-tight (codex 6).
|
||||
if (pack.SkipRows != 0 || pack.SkipPixels != 0) return false;
|
||||
if (pack.RowLength != 0 && pack.RowLength != width) return false;
|
||||
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
|
||||
const Uint64 rowBytes = static_cast<Uint64>(width) * bytesPerPixel;
|
||||
@@ -265,9 +270,62 @@ namespace MobileGL::MG_Remote::Client {
|
||||
ReadbackBytesPerPixel(format, type);
|
||||
}
|
||||
|
||||
// M2 / codex 11: the reply the server posted is COMPLETE and OK. A short OK reply, and a
|
||||
// DECLINED or ERROR reply with a zero payload, both leave the destination full of stale
|
||||
// bytes; scattering or returning it is the silently truncated picture ID-47's own comment
|
||||
// says an SSIM comparison cannot see, arriving through the status field rather than
|
||||
// through truncation. `expected` is the record's own DstSize (CONTRACT-P5 row 23: the
|
||||
// reply's exact extent). The predicate is at namespace scope (below) and exposed for the
|
||||
// control, so R-16's "drive the production predicate" holds rather than a second copy of
|
||||
// the rule in the test.
|
||||
|
||||
// The same predicate at the call site, with the named Fatal each failure mode owns.
|
||||
// status > expected cannot reach here: EmitAndWait already aborts Fatal{ReplyTooLarge} on
|
||||
// an oversize reply, so the only failures left are a wrong status or a SHORT one.
|
||||
void RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) {
|
||||
if (status == Wire::ReplySink::kStatusError) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; "
|
||||
"the destination is left untouched rather than filled with stale bytes");
|
||||
std::abort();
|
||||
}
|
||||
if (status == Wire::ReplySink::kStatusDeclined) {
|
||||
MGLOG_F("MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no "
|
||||
"GL.ReadPixels and DECLINED; a decline is a real answer for an acceptance "
|
||||
"row (R-5) but a blocking readback has no pixels to return, so it is a "
|
||||
"Fatal here rather than a buffer of stale bytes");
|
||||
std::abort();
|
||||
}
|
||||
if (replySize != expected) {
|
||||
MGLOG_F("MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK "
|
||||
"reply carried fewer bytes than the read's own DstSize (CONTRACT-P5 row "
|
||||
"23's exact extent); the missing rows would otherwise be scattered as "
|
||||
"whatever the destination held",
|
||||
static_cast<unsigned long long>(replySize),
|
||||
static_cast<unsigned long long>(expected));
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
|
||||
GLenum type, void* pixels) {
|
||||
ClientSession& session = RequireSession("ReadPixels");
|
||||
|
||||
// ID-57 / M8: A PACK-PBO DESTINATION IS REFUSED BY NAME, BEFORE ANY EMISSION AND
|
||||
// BEFORE `pixels` IS TOUCHED. With GL_PIXEL_PACK_BUFFER bound, the frontend permits
|
||||
// `pixels` to be a byte OFFSET into that buffer, not an address (GL_Framebuffer.cpp:
|
||||
// 3055 aligns it to the type size, one byte for UNSIGNED_BYTE) - and this emitter has
|
||||
// no PBO branch: it sets DstOffset = 0, hands the offset to EmitAndWait as a host
|
||||
// buffer, and the reply is memcpy'd to CPU address <offset>. Under monolith the
|
||||
// backend maps the PBO and writes the reply into the buffer (unchanged). The real
|
||||
// split form - the server writes the reply into the buffer resource and the client
|
||||
// marks it GPU-written (b1's MarkReadPixelsPackBuffer becoming the producer contract
|
||||
// §3 names) - is a P6 ROADMAP item. In P5 it is class C's shape (R-4), refused here.
|
||||
if (MG_State::pGLContext != nullptr &&
|
||||
MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack)
|
||||
.GetBoundObject()) {
|
||||
UnmigratedVerbFatal("ReadPixels+PACK_BUFFER");
|
||||
}
|
||||
|
||||
BeforeReadOnlyVerb();
|
||||
|
||||
// THE PBO HALF IS b1's DESIGN AND b1 ALREADY WIRED ITS MARK, at
|
||||
@@ -275,10 +333,16 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// ReadPixels_Backend itself. So this emitter deliberately does NOT call
|
||||
// MarkReadPixelsPackBuffer(): a second call there would be the "wire it twice"
|
||||
// shape, and the per-row counter b1's unit cases assert on would then count one
|
||||
// read as two.
|
||||
// read as two. (In P5 the refusal above means no PBO read reaches here at all; the
|
||||
// note stays for the P6 form.)
|
||||
if (width <= 0 || height <= 0) return;
|
||||
const Uint64 bytesPerPixel = ReadbackBytesPerPixel(format, type);
|
||||
const Uint64 tight = static_cast<Uint64>(width) * static_cast<Uint64>(height) * bytesPerPixel;
|
||||
// ONE tight-size function for production AND the control (M3 / codex 10a). The first
|
||||
// cut computed this inline here while the test drove TightReadbackByteCount, so a
|
||||
// `+16` on the production line stayed green - the test observed a different number.
|
||||
// Now the number the server allocates and the number the test asserts come from the
|
||||
// same body.
|
||||
const Uint64 tight = TightReadbackBytes(width, height, format, type);
|
||||
|
||||
MG_Pipe::MGPReadbackInfo info{};
|
||||
info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers"
|
||||
@@ -312,6 +376,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
|
||||
Int32 status = 0;
|
||||
Uint64 replySize = 0;
|
||||
if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) {
|
||||
// THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the
|
||||
// destination layout IS the tight layout, so the reply lands straight in the
|
||||
@@ -319,7 +384,11 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// SAME bytes, not a second rule: ScatterTightReadback below is a memcpy of the
|
||||
// whole run in exactly this case, and the control drives that function.
|
||||
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
|
||||
pixels, tight, &status);
|
||||
pixels, tight, &status, &replySize);
|
||||
// M2 / codex 11: an OK reply that arrived short, or a DECLINE/ERROR, must not be
|
||||
// handed back as pixels. The Fatal aborts before the application reads the buffer,
|
||||
// so the bytes EmitAndWait already copied into `pixels` are never observed.
|
||||
RequireReadbackReplyComplete(status, replySize, tight);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,7 +397,10 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// level out: nothing here outlives the call.
|
||||
Vector<Uint8> bounce(static_cast<SizeT>(tight));
|
||||
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
|
||||
bounce.data(), tight, &status);
|
||||
bounce.data(), tight, &status, &replySize);
|
||||
// BEFORE THE SCATTER, so a short or non-OK reply never reaches the application's
|
||||
// pointer at all (the bounce is the only thing that held the partial bytes).
|
||||
RequireReadbackReplyComplete(status, replySize, tight);
|
||||
ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel,
|
||||
pack);
|
||||
}
|
||||
@@ -590,6 +662,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
return TightReadbackBytes(width, height, format, type);
|
||||
}
|
||||
|
||||
// M2 / codex 11's predicate at namespace scope: the one RequireReadbackReplyComplete decides
|
||||
// on and the one the control drives. 0=OK / 1=DECLINED / 2=ERROR.
|
||||
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected) {
|
||||
return status == Wire::ReplySink::kStatusOk && replySize == expected;
|
||||
}
|
||||
|
||||
// ID-49's scatter. Exported for the same reason as the refusal above: the control drives
|
||||
// THIS, which is what the emitter calls, rather than a second copy of 8.4.4's arithmetic.
|
||||
void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width,
|
||||
@@ -601,15 +679,21 @@ namespace MobileGL::MG_Remote::Client {
|
||||
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
|
||||
const Uint64 strideBytes =
|
||||
((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment;
|
||||
// m6: the client re-derives GL 4.6 8.4.4's pack layout, so it honours GL_PACK_ROW_LENGTH
|
||||
// VERBATIM - including the ill-formed 0 < ROW_LENGTH < width, where the row stride is
|
||||
// narrower than a written row and consecutive rows overlap. GL leaves that case to the
|
||||
// implementation; the client reproduces exactly what the monolith backend's own scatter
|
||||
// would do with the same state rather than clamping, so the two arms stay byte-identical.
|
||||
// The fast path (ReadbackPackStateIsTight) already rejects any ROW_LENGTH != width, so
|
||||
// this only runs on the scatter path the application asked for.
|
||||
const Uint64 writtenPerRow = static_cast<Uint64>(width) * bytesPerPixel;
|
||||
// SKIP_IMAGES is in the parameter set and is meaningless for a 2D read, so it is
|
||||
// applied as GL defines it (whole images of ROW_LENGTH x IMAGE_HEIGHT) rather than
|
||||
// ignored - ignoring a non-zero one would silently write over the application's first
|
||||
// image.
|
||||
const Uint64 imageRows =
|
||||
pack.ImageHeight > 0 ? static_cast<Uint64>(pack.ImageHeight) : static_cast<Uint64>(height);
|
||||
// SKIP_IMAGES and IMAGE_HEIGHT ARE IGNORED (codex 6). glReadPixels is a 2-D read; GL
|
||||
// does not apply the image-level pack parameters to it, and the monolith conversion path
|
||||
// says so explicitly with honorPackImageParams=false (DirectGLES.cpp:10905). Applying
|
||||
// SKIP_IMAGES here shifted a read with SKIP_IMAGES=1 by a whole image and overran an
|
||||
// application buffer sized for exactly `height` rows. Only SKIP_ROWS and SKIP_PIXELS -
|
||||
// the 2-D skips - offset the first written byte.
|
||||
auto* out = static_cast<Uint8*>(destination) +
|
||||
static_cast<Uint64>(pack.SkipImages) * imageRows * strideBytes +
|
||||
static_cast<Uint64>(pack.SkipRows) * strideBytes +
|
||||
static_cast<Uint64>(pack.SkipPixels) * bytesPerPixel;
|
||||
const auto* in = static_cast<const Uint8*>(tight);
|
||||
|
||||
@@ -147,4 +147,11 @@ namespace MobileGL::MG_Remote::Client {
|
||||
Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack);
|
||||
|
||||
// M2 / codex 11. True exactly when the readback reply is OK and carries the read's own exact
|
||||
// extent (CONTRACT-P5 row 23). EmitReadPixels calls this and Fatals by name when it is false
|
||||
// - a short OK reply, or a DECLINE/ERROR with a zero payload, is refused rather than scattered
|
||||
// as pixels. Exposed so the control drives the production predicate (R-16), not a copy: pass
|
||||
// 0=OK / 1=DECLINED / 2=ERROR as `status`.
|
||||
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected);
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -48,29 +49,60 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
using MG_Pipe::MGPWireOp;
|
||||
|
||||
namespace {
|
||||
// TABLE 3's ROLE SPLIT, AS A RUNTIME CHECK. gMGPipeScreen / gMGPipeContext are PROCESS
|
||||
// globals and under `inproc` the server role is a thread in this same process, so the
|
||||
// apply thread running the server's own backend - the EGL bring-up, InitCapabilities,
|
||||
// the applier - reaches these very emitters. A record published there would be waited
|
||||
// for by the thread that is supposed to apply it: `Fatal{BarrierTimeout,
|
||||
// "ResourceRespecify"}` from `mgl-srv-apply`, thirty seconds into bring-up, which is
|
||||
// exactly how this was found.
|
||||
//
|
||||
// THE ANSWER IS NOT "SUPPRESS THE RECORD" - it is "run the server's own code", because
|
||||
// on that thread this process IS the server and the applier is one call away. It is
|
||||
// the same thing PipeWireCodec does on the decode side, where every arm calls
|
||||
// MGPipeApply* directly and never goes through a table.
|
||||
//
|
||||
// Under `spawn` (P6) the predicate is constantly false in the client process and
|
||||
// constantly true in the server's, so this costs one atomic load and changes nothing.
|
||||
//
|
||||
// NOT in the anonymous namespace, because M5 needs it from MG_Impl/Pipe/PipeFill.cpp too
|
||||
// (its split-only respecify/flush branches must not run on the apply thread). Declared in
|
||||
// WireTables.h.
|
||||
Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }
|
||||
|
||||
// TABLE 3's ROLE SPLIT, AS A RUNTIME CHECK. gMGPipeScreen / gMGPipeContext are PROCESS
|
||||
// globals and under `inproc` the server role is a thread in this same process, so the
|
||||
// apply thread running the server's own backend - the EGL bring-up, InitCapabilities,
|
||||
// the applier - reaches these very emitters. A record published there would be waited
|
||||
// for by the thread that is supposed to apply it: `Fatal{BarrierTimeout,
|
||||
// "ResourceRespecify"}` from `mgl-srv-apply`, thirty seconds into bring-up, which is
|
||||
// exactly how this was found.
|
||||
//
|
||||
// THE ANSWER IS NOT "SUPPRESS THE RECORD" - it is "run the server's own code", because
|
||||
// on that thread this process IS the server and the applier is one call away. It is
|
||||
// the same thing PipeWireCodec does on the decode side, where every arm calls
|
||||
// MGPipeApply* directly and never goes through a table.
|
||||
//
|
||||
// Under `spawn` (P6) the predicate is constantly false in the client process and
|
||||
// constantly true in the server's, so this costs one atomic load and changes nothing.
|
||||
Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }
|
||||
namespace {
|
||||
|
||||
Uint64 g_emitted = 0;
|
||||
Uint64 g_declined = 0;
|
||||
|
||||
// TEARDOWN REFUSAL (codex 4). ClientSession::Stop marks the routed tables uninstalled
|
||||
// BEFORE it frees the rings, the segments and the transport - so the window between then
|
||||
// and the moment the monolith adapters go back is one in which a routed GL-thread call
|
||||
// must not run the applier on the caller (the forbidden path table 3 draws) and must not
|
||||
// reach a half-freed ring. Round 2 reinstalled the monolith adapters at the top of Stop,
|
||||
// which is exactly running the applier on the caller; a routed mutation after
|
||||
// UninstallClientWireTables() moved no wire ordinal and never refused (the cross-family
|
||||
// verifier reproduced it). So Uninstall now RAISES THIS FLAG and leaves the wire rows in
|
||||
// place; RequireSession below reads it and refuses by name before it touches anything.
|
||||
// Atomic because the apply thread's own role check races a GL-thread teardown.
|
||||
std::atomic<Bool> g_clientTablesUninstalled{false};
|
||||
|
||||
ClientSession& RequireSession(const char* row) {
|
||||
// BEFORE the session lookup and before any ring access. A routed call that arrives
|
||||
// once Stop has begun tearing the session down is refused by name rather than run
|
||||
// on the caller's thread - the applier is server-exclusive (table 3), and a wire
|
||||
// emit into a ring being freed is a use-after-free. The monolith adapters are put
|
||||
// back only as the LAST step of teardown, for the at-exit ~BufferObject deletes that
|
||||
// legitimately reach a process with no session (see ReinstallMonolithAfterTeardown).
|
||||
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
|
||||
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - a routed call reached the "
|
||||
"client wire tables while ClientSession::Stop was tearing the session "
|
||||
"down. R-4/R-17: the applier is server-exclusive and the rings this emit "
|
||||
"would use are being freed, so the call is refused by name rather than run "
|
||||
"on the caller",
|
||||
row);
|
||||
std::abort();
|
||||
}
|
||||
ClientSession* session = ClientSession::Active();
|
||||
if (session == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the client wire tables are "
|
||||
@@ -173,7 +205,6 @@ namespace MobileGL::MG_Remote::Client {
|
||||
MGP_WIRE_BLOB(CreateRenderState, MGPRenderStateDesc, Blob)
|
||||
MGP_WIRE_BLOB(CreateVertexElements, MGPVertexElements, Blob)
|
||||
MGP_WIRE_BLOB(CreateSamplerState, MGPSamplerDesc, Parameters)
|
||||
MGP_WIRE_BLOB(SetDynamicState, MGPDynamicState, Blob)
|
||||
MGP_WIRE_BLOB(SetGlobalConstants, MGPGlobalConstants, Blob)
|
||||
|
||||
// -- context, variable tail ---------------------------------------------------
|
||||
@@ -189,6 +220,33 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
// -- the rows that fit none of the three shapes --------------------------------
|
||||
|
||||
// set_dynamic_state. AN OPTIONAL BLOB, NOT A MANDATORY ONE (round-2 regression, the
|
||||
// census's Fatal{BlobMissing, "SetDynamicState"}). EmitRenderState (PipeFill.cpp:2405-
|
||||
// 2421) sends a set_dynamic_state whenever the RenderState VERSION moved, and when the
|
||||
// chunk-level suppressor finds NO dynamic chunk changed it sends the 32-byte header with
|
||||
// ChunkMask == 0 and blobByteCount == 0 - a version-only update, which the comment there
|
||||
// calls out as deliberate. The generic MGP_WIRE_BLOB wrapper stages through StageRequired,
|
||||
// which Fatals on a zero count, so a legal header-only update aborted the process with a
|
||||
// blob-shape complaint that had nothing to do with the actual state of the pipe. The
|
||||
// DECODER already handles it: PipeWireCodec.cpp:1571-1587 takes the `ChunkMask == 0` arm
|
||||
// (CheckBlobIsHonest, no blob resolved) and applies the header alone. So the emitter is
|
||||
// the only side that was wrong; it stages OPTIONALLY, exactly like the two sub-data rows.
|
||||
// A non-empty mask still stages (blobByteCount > 0), so the reduced-path scenarios, whose
|
||||
// first draw is freshly primed with every chunk, are byte-for-byte unchanged.
|
||||
void Wire_SetDynamicState(const MG_Pipe::MGPDynamicState* payload, const void* blobBytes,
|
||||
Uint64 blobByteCount) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithContext().SetDynamicState(payload, blobBytes, blobByteCount);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("SetDynamicState");
|
||||
MG_Pipe::MGPDynamicState record = *payload;
|
||||
record.Blob = StageOptional(session, blobBytes, blobByteCount);
|
||||
session.EmitAndWait(MGPWireOp::SetDynamicState, &record, sizeof(record), nullptr, 0,
|
||||
nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// set_residual_value_state. CONTRACT-P5 table 1 row 6: the applier takes a frontend
|
||||
// `ResidualValueBlock&` and `MGPResidualValueState` is never instantiated on the live
|
||||
// path, so the encoder invents BOTH the record fill and the blob fill. The block IS
|
||||
@@ -338,6 +396,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
0, nullptr, 0, &status);
|
||||
(void)seq;
|
||||
++g_emitted;
|
||||
// ERROR IS NOT A DECLINE (M4 / codex 5 / R-5). status is 0 OK / 1 DECLINED / 2 ERROR;
|
||||
// an escape may not fold 2 into `false`, which is what a bare `return status == 0`
|
||||
// did - a transport fault then read as "the server said no". Every reply-owning row
|
||||
// - the generated acceptance rows through MGPipeTakeReplyBool, and now these two
|
||||
// escapes - answers ERROR with the same named Fatal.
|
||||
if (status == 2) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"resource_respecify\"} - the row answered "
|
||||
"ERROR, which is not an acceptance answer; folding it into accepted or "
|
||||
"refused would make a transport fault look like a resource decision");
|
||||
std::abort();
|
||||
}
|
||||
if (status == 1) ++g_declined;
|
||||
return status == 0;
|
||||
}
|
||||
@@ -376,6 +445,15 @@ namespace MobileGL::MG_Remote::Client {
|
||||
session.EmitAndWait(MGPWireOp::MapPersistent, handle, sizeof(*handle), nullptr, 0,
|
||||
nullptr, 0, &status);
|
||||
++g_emitted;
|
||||
// ERROR IS NOT A DECLINE (M4 / codex 5 / R-5), the same rule the respecify escape and
|
||||
// the generated acceptance rows obey: status 2 is a transport fault, and returning
|
||||
// nullptr for it would make it indistinguishable from R-6's legitimate decline.
|
||||
if (status == 2) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"map_persistent\"} - the row answered ERROR, "
|
||||
"which is not an acceptance answer; a transport fault is not a resource "
|
||||
"decision and may not be folded into the decline R-6 predicts");
|
||||
std::abort();
|
||||
}
|
||||
if (status == 1) ++g_declined;
|
||||
// NOT "always nullptr": the answer is READ. R-6 says the server declines, and the
|
||||
// day it stops declining this returns what it actually said rather than what the
|
||||
@@ -436,6 +514,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
void InstallClientWireTables() {
|
||||
using namespace MG_Pipe;
|
||||
|
||||
// A fresh install means the routed tables are live again: a Start after a previous
|
||||
// session's Stop clears the teardown-refusal flag so its own routed calls are not
|
||||
// refused. (Stop already puts the monolith adapters back and clears the flag; this is
|
||||
// the belt to that braces.)
|
||||
g_clientTablesUninstalled.store(false, std::memory_order_release);
|
||||
|
||||
gMGPipeScreen.ResourceCreate = &Wire_ResourceCreate;
|
||||
gMGPipeScreen.ResourceDestroy = &Wire_ResourceDestroy;
|
||||
gMGPipeScreen.UnmapPersistent = &Wire_UnmapPersistent;
|
||||
@@ -480,11 +564,28 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
|
||||
void UninstallClientWireTables() {
|
||||
// PUTS THE MONOLITH ARM BACK rather than nulling the rows. A null row is the
|
||||
// pre-migration state and would be an immediate crash with no diagnostic at whatever
|
||||
// GL call raced the teardown; the monolith adapter is at least a correct answer for a
|
||||
// process that no longer has a session.
|
||||
// MARK THE ROUTED TABLES UNINSTALLED (codex 4). It does NOT reinstall the monolith
|
||||
// adapters, and that is the whole fix: round 2 reinstalled them here, at the TOP of
|
||||
// Stop, so a routed GL-thread call arriving during teardown ran the applier on the
|
||||
// caller - the forbidden path - and moved no wire ordinal, with no refusal. Raising the
|
||||
// flag leaves the Wire_* rows in place; the next routed call reaches RequireSession,
|
||||
// reads the flag and aborts by name (Fatal{ClientTablesUninstalled}) before it touches a
|
||||
// ring that Stop is about to free. The monolith adapters are put back only once teardown
|
||||
// is complete, by ReinstallMonolithAfterTeardown, for the at-exit deletes that reach a
|
||||
// process with no session at all. Idempotent: safe to call when nothing was installed.
|
||||
g_clientTablesUninstalled.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ReinstallMonolithAfterTeardown() {
|
||||
// THE LAST STEP OF ClientSession::Stop, after the rings, segments and transport are gone
|
||||
// and the apply thread has joined. Now a routed call can only be a process that no
|
||||
// longer has a session - the canonical case is ~BufferObject running from an exit
|
||||
// handler (ID-8) - and the monolith adapter, which runs the applier synchronously, is
|
||||
// the correct answer for it, exactly as it is in a pure-monolith build. Clearing the
|
||||
// flag re-enables the (now monolith) rows. A null row here would be an undiagnosed crash
|
||||
// at whatever GL call an exit handler makes; the applier is a defined no-op-or-apply.
|
||||
MG_Pipe::MGPipeInstallMonolithTables();
|
||||
g_clientTablesUninstalled.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
Uint64 ClientWireRecordsEmitted() { return g_emitted; }
|
||||
|
||||
@@ -43,20 +43,46 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// gMGPipeRouteEscapes and records the arm. Idempotent.
|
||||
void InstallClientWireTables();
|
||||
|
||||
// Puts the monolith adapters back. Idempotent; safe to call when nothing was installed.
|
||||
// Marks the routed tables uninstalled so a routed call refuses by name
|
||||
// (Fatal{ClientTablesUninstalled, "<row>"}) rather than running the applier on the caller
|
||||
// (codex 4). It does NOT restore the monolith adapters - that is ReinstallMonolithAfterTeardown
|
||||
// below, run only once the session's rings are freed. Idempotent.
|
||||
void UninstallClientWireTables();
|
||||
|
||||
// How many records the thirty-seven emitters have published. This is t1's FOURTH arming
|
||||
// fact - "the client encoder's record ordinal actually moving during the case" - counted
|
||||
// at the only place that can count it, and it is deliberately NOT the encoder's EmitSeq:
|
||||
// EmitSeq moves for the five class-B verbs too, so a lane that armed on it would arm on a
|
||||
// Clear and call the resource path proven.
|
||||
// The LAST step of ClientSession::Stop: after the rings, segments and transport are gone,
|
||||
// puts the monolith adapters back and clears the refusal flag, so the at-exit ~BufferObject
|
||||
// deletes that reach a process with no session run the applier as they do under monolith.
|
||||
void ReinstallMonolithAfterTeardown();
|
||||
|
||||
// How many records the thirty-seven emitters have published. It counts the ROUTED rows
|
||||
// only - a resource_create, a set_vertex_buffers, a create_shader_state - and never the
|
||||
// five class-B verbs, so it is the one number that says "the resource/CSO/state path really
|
||||
// ran" as opposed to "a Clear crossed".
|
||||
//
|
||||
// WHAT IT IS NOT (M7, corrected). This is NOT what arms the split lane. WireTables.h round 2
|
||||
// claimed it was "t1's FOURTH arming fact", and that was wrong: the harness reads the
|
||||
// encoder's EmitSeq (Harness/SplitRuntimePeek.cpp:50, Harness/ScenarioFixture.h:85), which
|
||||
// moves for the class-B verbs too. So an armed Clear-only lane is green on EmitSeq while this
|
||||
// counter stays 0. Making the harness read THIS instead is t1's file (SplitRuntimePeek), so
|
||||
// the honest statement is the one here: c1 counts the routed ordinal at the only place that
|
||||
// can, PipeFill's InitialBytesNotCarried self-check reads it (PipeFill.cpp:755), and the
|
||||
// lane's own arming remains EmitSeq until t1 re-points it. c1-v3.md M7 has the full note.
|
||||
Uint64 ClientWireRecordsEmitted();
|
||||
|
||||
// How many of those were refused by the server, by acceptance row. Counted rather than
|
||||
// inferred, R-8's rule one level out.
|
||||
Uint64 ClientWireRecordsDeclined();
|
||||
|
||||
// TRUE ON THE SERVER ROLE's OWN THREAD (the apply thread), false everywhere else. It is
|
||||
// v1's ServerLoop::OnApplyThread(), exposed here because it is table 3's role split made
|
||||
// into one predicate and TWO packages read it: the wire emitters below (a routed call that
|
||||
// finds itself on the apply thread runs the monolith adapter, because on that thread this
|
||||
// process IS the server), and MG_Impl/Pipe/PipeFill.cpp's split-only respecify/flush
|
||||
// branches (M5: those branches emit CLIENT wire records and must not run on the server's
|
||||
// apply thread, which produces none - the InitialBytesNotCarried self-check would abort the
|
||||
// server otherwise). Declared here so PipeFill does not have to include a server header.
|
||||
Bool RunsAsTheServerRole();
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <MG_Remote/Transport/ReplySlot.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Remote/Client/EmitTables.h>
|
||||
#include <MG_Remote/Client/WireTables.h>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <csignal>
|
||||
@@ -521,6 +522,39 @@ TEST(RemoteReadback, TheTightRowsAreScatteredWhereThePackStateSaysAndTheGapsAreL
|
||||
"reply carried";
|
||||
}
|
||||
|
||||
TEST(RemoteReadback, PackSkipImagesIsIgnoredForATwoDimensionalRead) {
|
||||
// codex 6: SKIP_IMAGES (and IMAGE_HEIGHT) are image-level pack parameters and GL ignores
|
||||
// them for glReadPixels, a 2-D read - the monolith conversion path says so with
|
||||
// honorPackImageParams=false (DirectGLES.cpp:10905). The first cut applied SKIP_IMAGES as a
|
||||
// whole-image offset: a 4x3 RGBA8 read with SKIP_IMAGES=1 wrote bytes 48..95 of a 96-byte
|
||||
// destination sized for one image, overrunning it. With the fix the reply lands at bytes
|
||||
// 0..47 and the second image's worth of bytes keeps its sentinel.
|
||||
constexpr Uint64 kBpp = 4;
|
||||
constexpr GLsizei kW = 4;
|
||||
constexpr GLsizei kH = 3;
|
||||
constexpr Uint8 kSentinel = 0xEE;
|
||||
|
||||
PixelStoreParameters pack{};
|
||||
pack.SkipImages = 1; // the parameter under test
|
||||
pack.ImageHeight = kH; // and its companion; both must be ignored
|
||||
|
||||
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
|
||||
for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast<Uint8>(i + 1);
|
||||
|
||||
std::vector<Uint8> destination(96, kSentinel);
|
||||
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
|
||||
|
||||
for (size_t i = 0; i < tight.size(); ++i)
|
||||
EXPECT_EQ(destination[i], tight[i]) << "byte " << i << " should hold the reply at offset 0";
|
||||
for (size_t i = tight.size(); i < destination.size(); ++i)
|
||||
EXPECT_EQ(destination[i], kSentinel)
|
||||
<< "byte " << i << " is past the read's own extent and SKIP_IMAGES must not have moved "
|
||||
"the write there";
|
||||
// And the fast path takes it: an otherwise-neutral read with only SKIP_IMAGES set is tight.
|
||||
EXPECT_TRUE(ReadbackPackStateIsTightForTest(kW, kBpp, pack))
|
||||
<< "SKIP_IMAGES alone must not force the bounce path for a 2-D read";
|
||||
}
|
||||
|
||||
TEST(RemoteReadback, TheFastPathIsTakenExactlyWhenTheScatterWouldChangeNothing) {
|
||||
// EmitReadPixels reads the reply STRAIGHT into the application's pointer when
|
||||
// ReadbackPackStateIsTight says so, and pays for a bounce buffer otherwise. That is only
|
||||
@@ -644,6 +678,120 @@ TEST(PipeRouting, AnErrorStatusIsNotFoldedIntoAcceptedOrRefused) {
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// =====================================================================================
|
||||
// B3 / codex 9: the CLIENT arm's 37 rows are observed, not just the monolith install
|
||||
// =====================================================================================
|
||||
|
||||
namespace {
|
||||
// How many function-pointer cells of `a` differ from `b`, walked as a block of void* -
|
||||
// the structs ARE their function pointers (PipeCatalogueTest static_asserts that shape). A
|
||||
// routed row that stayed on the monolith adapter reads EQUAL and is not counted, which is
|
||||
// exactly the defect this measures.
|
||||
template <class T>
|
||||
SizeT CountDifferingCells(const T& a, const T& b) {
|
||||
const void* const* pa = reinterpret_cast<const void* const*>(&a);
|
||||
const void* const* pb = reinterpret_cast<const void* const*>(&b);
|
||||
SizeT n = 0;
|
||||
for (SizeT i = 0; i < sizeof(T) / sizeof(void*); ++i)
|
||||
if (pa[i] != pb[i]) ++n;
|
||||
return n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(PipeRouting, TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMoved) {
|
||||
// WHAT B3 SAYS IS MISSING. PipeCatalogueTest installs and COUNTS the monolith table, so a
|
||||
// client row that was never overwritten stays non-null and still counts - deleting one
|
||||
// `gMGPipe*.X = &Wire_X` assignment left every gate green (the cross-family verifier
|
||||
// reproduced it: catalogue 32/32, all lanes green). The only thing that catches it is
|
||||
// observing that the CLIENT install actually MOVED each routed row off the monolith adapter.
|
||||
//
|
||||
// The monolith adapters are kept beside the installed tables (MGPipeMonolith*()), so the
|
||||
// client install differs from them at exactly the routed rows and nowhere else. This reads
|
||||
// the pointers back rather than constructing them (R-16): a deleted assignment reads equal.
|
||||
InstallClientWireTables();
|
||||
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kClientWire))
|
||||
<< "InstallClientWireTables did not record the client-wire arm";
|
||||
|
||||
const SizeT movedScreen = CountDifferingCells(gMGPipeScreen, MGPipeMonolithScreen());
|
||||
const SizeT movedContext = CountDifferingCells(gMGPipeContext, MGPipeMonolithContext());
|
||||
EXPECT_EQ(movedScreen + movedContext, 33u)
|
||||
<< "exactly the 33 generated routed rows must differ from the monolith adapters; "
|
||||
<< movedScreen + movedContext
|
||||
<< " did, so a row was left on the monolith adapter (it would run the applier on the GL "
|
||||
"thread under split) or an unrouted row was overwritten";
|
||||
|
||||
const SizeT movedEscapes = CountDifferingCells(gMGPipeRouteEscapes, MGPipeMonolithEscapes());
|
||||
EXPECT_EQ(movedEscapes, 4u)
|
||||
<< "the four escape routes must move off the monolith escapes too";
|
||||
|
||||
// Restore the monolith arm for the sibling cases that assert it (and for a clean binary).
|
||||
MGPipeInstallMonolithTables();
|
||||
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kMonolith));
|
||||
}
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
TEST(PipeRouting, AClientWireRowWithNoSessionRefusesByNameRatherThanApplying) {
|
||||
// THE RUNTIME HALF of B3, and it distinguishes a Wire_* row from the monolith adapter by
|
||||
// BEHAVIOUR: with the client tables installed and no session, a routed call reaches
|
||||
// RequireSession and aborts Fatal{NoClientSession}. The monolith adapter (the deleted-
|
||||
// assignment state) would instead run MGPipeApply* and NOT abort with that string - so the
|
||||
// control goes red the moment a row falls back to monolith.
|
||||
const ChildResult child = RunInChild([] {
|
||||
InstallClientWireTables();
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = MGPipeHandle{1, 0};
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
|
||||
gMGPipeScreen.ResourceDestroy(&handle); // Wire_ResourceDestroy, no session
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "\n" << child.Log;
|
||||
EXPECT_NE(child.Log.find("Fatal{NoClientSession, \"ResourceDestroy\"}"), std::string::npos)
|
||||
<< "the installed row did not refuse by name; it may be the monolith adapter (B3)\n"
|
||||
<< child.Log;
|
||||
}
|
||||
|
||||
TEST(PipeRouting, ARoutedCallDuringTeardownRefusesByNameNotRunsTheApplier) {
|
||||
// codex 4: UninstallClientWireTables marks the tables uninstalled; a routed call in that
|
||||
// window must abort by name rather than run the applier on the caller. Reverting Uninstall
|
||||
// to reinstall the monolith adapters (round 2's behaviour) makes this call run the applier
|
||||
// and NOT abort with this string - the red-once.
|
||||
const ChildResult child = RunInChild([] {
|
||||
InstallClientWireTables();
|
||||
UninstallClientWireTables();
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = MGPipeHandle{1, 0};
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
|
||||
gMGPipeScreen.ResourceDestroy(&handle);
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "\n" << child.Log;
|
||||
EXPECT_NE(child.Log.find("Fatal{ClientTablesUninstalled, \"ResourceDestroy\"}"), std::string::npos)
|
||||
<< "a routed call after UninstallClientWireTables ran the applier on the caller instead "
|
||||
"of refusing by name (codex 4)\n"
|
||||
<< child.Log;
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// =====================================================================================
|
||||
// M2 / codex 11: a short or non-OK reply is refused, never scattered as pixels
|
||||
// =====================================================================================
|
||||
|
||||
TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) {
|
||||
// EmitReadPixels decides on ReadbackReplyIsComplete before it scatters or returns (the Fatal
|
||||
// wording each mode owns is at the call site). Driving the production predicate directly:
|
||||
// only a full OK reply is complete; a short OK reply, and a DECLINE or ERROR with a zero
|
||||
// payload, are not - and those are the shapes that would otherwise spray stale destination
|
||||
// bytes as pixels. `tight` is the read's own DstSize (CONTRACT-P5 row 23).
|
||||
constexpr Int32 kOk = 0, kDeclined = 1, kError = 2;
|
||||
const Uint64 tight = TightReadbackByteCount(4, 3, 0x1908, 0x1401); // 48
|
||||
EXPECT_TRUE(ReadbackReplyIsComplete(kOk, tight, tight)) << "a full OK reply is the only one scattered";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, tight - 16, tight))
|
||||
<< "a SHORT OK reply (one row missing) must not be scattered - the missing rows would be "
|
||||
"whatever the destination held";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kDeclined, 0, tight))
|
||||
<< "a DECLINED reply carries no pixels";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kError, 0, tight)) << "an ERROR reply carries no pixels";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, 0, tight)) << "an OK reply of zero bytes is not the extent";
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
Reference in New Issue
Block a user