mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Feat] (MG_Remote/Server): PipeApplier - attach the decoder on the apply thread, stamp/apply/clear per record, and ServerVerbSink for the five class-B verbs
This commit is contained in:
@@ -6,31 +6,26 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
// P5 c0 stubs for package v1 (with p1 for the stamp rule).
|
// P5 package v1: the applier bridge, and the consumer for contract 7's five class-B verbs.
|
||||||
|
|
||||||
#include "PipeApplier.h"
|
#include "PipeApplier.h"
|
||||||
|
|
||||||
#include "../Transport/ReplySlot.h"
|
#include "../Transport/ReplySlot.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Server {
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
#define MGP5_C0_STUB(what) \
|
|
||||||
do { \
|
|
||||||
MGLOG_F("MGPipe: Fatal{UnimplementedPipeApplier, \"%s\"} - P5 package v1 has not landed " \
|
|
||||||
"this yet; c0 shipped the signature only", \
|
|
||||||
what); \
|
|
||||||
std::abort(); \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
|
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
|
||||||
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
|
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
|
||||||
|
|
||||||
// PACKAGE s1's, not v1's, even though the class is declared in v1's header: the SEG_REPLY
|
// PACKAGE s1's, not v1's, even though the class is declared in v1's header: the SEG_REPLY
|
||||||
// slot pool is s1's deliverable (BRIEF §5) and its addressing lives in one place,
|
// slot pool is s1's deliverable (BRIEF 5) and its addressing lives in one place,
|
||||||
// Transport/ReplySlot.h, which the CLIENT reads the same slots back through. Duplicating
|
// Transport/ReplySlot.h, which the CLIENT reads the same slots back through. Duplicating
|
||||||
// `seq % slots` on this side is how the two halves come to disagree about which slot an
|
// `seq % slots` on this side is how the two halves come to disagree about which slot an
|
||||||
// answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another
|
// answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another
|
||||||
@@ -47,21 +42,324 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
|
|
||||||
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
|
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// ServerVerbSink - the five class-B verbs
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) { m_backend = backend; }
|
||||||
|
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* ServerVerbSink::Table(const char* verb) const {
|
||||||
|
if (m_backend == nullptr) {
|
||||||
|
// DECLINE BY NAME, DO NOT DEREFERENCE. A verb that arrives before
|
||||||
|
// ServerLoop::CreateBackend has run means the hook order changed under us, and the
|
||||||
|
// honest answer is "this build did not apply it" - which DecodeAndApply reports as
|
||||||
|
// false and the lane sees as a record that did not render, rather than as a crash
|
||||||
|
// with no line saying which verb was first.
|
||||||
|
MGLOG_E_ONCE("MG_Remote server: %s arrived with no backend object; the verb is "
|
||||||
|
"DECLINED. ServerLoop::CreateBackend runs from MG_Backend::Init()'s "
|
||||||
|
"hook, before ClientSession::Start",
|
||||||
|
verb);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &m_backend->GetBackendFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||||
|
|
||||||
|
// THE FBO HANDLE IS NOT RESOLVED HERE, AND THAT IS THE RULING RATHER THAN AN OMISSION.
|
||||||
|
// MGPClear::Fbo names the framebuffer the clear belongs to, but the BINDING is already
|
||||||
|
// server state: set_framebuffer_state (op 33) arrives ahead of the clear and the
|
||||||
|
// applier has bound it. Re-resolving the handle to a frontend FramebufferObject here
|
||||||
|
// would need the SharedPtr the four ClearNamedFramebuffer* entries take - a frontend
|
||||||
|
// heap reference that table 2 lists as one of the six fields with no wire carrier. So
|
||||||
|
// P5 clears THE BOUND FRAMEBUFFER, which for the reduced path (default FBO) is exactly
|
||||||
|
// right, and the named form is P7's along with the handle it needs.
|
||||||
|
switch (clear.Kind) {
|
||||||
|
case kMGPClearKindWhole:
|
||||||
|
if (gl.Clear == nullptr) return false;
|
||||||
|
gl.Clear(static_cast<GLbitfield>(clear.BufferMask));
|
||||||
|
break;
|
||||||
|
case kMGPClearKindColor:
|
||||||
|
switch (clear.ValueClass) {
|
||||||
|
case kMGPClearValueClassFloat:
|
||||||
|
if (gl.ClearBufferfv == nullptr) return false;
|
||||||
|
gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex,
|
||||||
|
reinterpret_cast<const GLfloat*>(clear.ColorValue));
|
||||||
|
break;
|
||||||
|
case kMGPClearValueClassInt:
|
||||||
|
if (gl.ClearBufferiv == nullptr) return false;
|
||||||
|
gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex,
|
||||||
|
reinterpret_cast<const GLint*>(clear.ColorValue));
|
||||||
|
break;
|
||||||
|
case kMGPClearValueClassUint:
|
||||||
|
if (gl.ClearBufferuiv == nullptr) return false;
|
||||||
|
gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex,
|
||||||
|
reinterpret_cast<const GLuint*>(clear.ColorValue));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// A value class outside the three is a wire fault, not a fallback: all three
|
||||||
|
// representations of a clear colour are numerically populated by the frontend
|
||||||
|
// and only this field says which one the backend must use, so guessing renders
|
||||||
|
// a plausible wrong colour.
|
||||||
|
Wire::WireProtocolFatalAt("MGPClear::ValueClass", clear.ValueClass, 3);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case kMGPClearKindDepth:
|
||||||
|
if (gl.ClearBufferfv == nullptr) return false;
|
||||||
|
gl.ClearBufferfv(GL_DEPTH, 0, &clear.DepthValue);
|
||||||
|
break;
|
||||||
|
case kMGPClearKindStencil:
|
||||||
|
if (gl.ClearBufferiv == nullptr) return false;
|
||||||
|
gl.ClearBufferiv(GL_STENCIL, 0, &clear.StencilValue);
|
||||||
|
break;
|
||||||
|
case kMGPClearKindDepthStencil:
|
||||||
|
if (gl.ClearBufferfi == nullptr) return false;
|
||||||
|
gl.ClearBufferfi(GL_DEPTH_STENCIL, 0, clear.DepthValue, clear.StencilValue);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Wire::WireProtocolFatalAt("MGPClear::Kind", clear.Kind, kMGPClearKindDepthStencil + 1);
|
||||||
|
}
|
||||||
|
++m_clears;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnBlit(const MG_Pipe::MGPBlit& blit) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
if (table->GL.BlitFramebuffer == nullptr) return false;
|
||||||
|
// Same ruling as OnClear's: the read and draw framebuffers are already bound by the
|
||||||
|
// set_framebuffer_state records that preceded this one, so the unnamed entry point is
|
||||||
|
// the one that matches what the server's state actually is. BlitNamedFramebuffer needs
|
||||||
|
// two frontend SharedPtrs, which table 2 lists as uncarried.
|
||||||
|
table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0,
|
||||||
|
blit.DstY0, blit.DstX1, blit.DstY1,
|
||||||
|
static_cast<GLbitfield>(blit.Mask),
|
||||||
|
static_cast<GLenum>(blit.Filter));
|
||||||
|
++m_blits;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnPresent(const MG_Pipe::MGPPresent& present) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("present");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
if (table->Present == nullptr) return false;
|
||||||
|
// Present is the ONLY frame-boundary drain the backend has (DirectGLES.cpp:12424-12470:
|
||||||
|
// the fence poll, the four ring OnPresent hooks, TrimBufferPool, PipeStats::OnPresent),
|
||||||
|
// which is why ARCHITECTURE.md:531 insists present <-> eglSwapBuffers stays strictly
|
||||||
|
// 1:1. It is NOT eglSwapBuffers itself: the swap is the client's EGL call and crosses
|
||||||
|
// as the SwapEGLBuffers control request, which runs Present on this thread through this
|
||||||
|
// same table. Both paths therefore end here and the 1:1 is structural.
|
||||||
|
table->Present();
|
||||||
|
++m_presents;
|
||||||
|
// FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side
|
||||||
|
// present credit, so the client sends 0 and the frame count on this side IS the serial.
|
||||||
|
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
||||||
|
Wire::ReplySink* replies) {
|
||||||
|
if (replies == nullptr) {
|
||||||
|
// The decoder always passes its ReplySink; a null one means the applier was built
|
||||||
|
// without a reply pool, and answering nothing would leave the client's barrier
|
||||||
|
// waiting for a slot that never gets stamped - a hang, not a wrong picture.
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - "
|
||||||
|
"the pixels' only destination in P5 is SEG_REPLY (contract table 1 row 23) "
|
||||||
|
"and a client blocked on seq %llu would never be answered",
|
||||||
|
static_cast<unsigned long long>(seq));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
|
||||||
|
if (table == nullptr || table->GL.ReadPixels == nullptr) {
|
||||||
|
// DECLINED IS A REAL ANSWER (table 0's slot-header row) and it is the RIGHT one
|
||||||
|
// here: the client is parked on this seq inside the verb barrier, so returning
|
||||||
|
// false without posting would convert "not implemented" into "never returns".
|
||||||
|
replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (info.DstSize == 0) {
|
||||||
|
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// THE CLIENT DECLARES THE BYTE COUNT AND THE SERVER DOES NOT RECOMPUTE IT. DstSize is
|
||||||
|
// sized on the client from the same GL_PACK_* state the frontend owns, and it is what
|
||||||
|
// the client will read back out of the slot; a server that recomputed from Box x
|
||||||
|
// Format x Type would be a SECOND opinion about a pack alignment the client half owns,
|
||||||
|
// and the two disagreeing is a short read with plausible pixels in it.
|
||||||
|
if (info.DstSize > m_readbackScratch.size()) {
|
||||||
|
m_readbackScratch.resize(static_cast<SizeT>(info.DstSize));
|
||||||
|
}
|
||||||
|
table->GL.ReadPixels(info.Box.X, info.Box.Y, static_cast<GLsizei>(info.Box.W),
|
||||||
|
static_cast<GLsizei>(info.Box.H), static_cast<GLenum>(info.Format),
|
||||||
|
static_cast<GLenum>(info.Type), m_readbackScratch.data());
|
||||||
|
replies->PostReply(seq, Wire::ReplySink::kStatusOk, m_readbackScratch.data(), info.DstSize);
|
||||||
|
m_readbackBytes += info.DstSize;
|
||||||
|
++m_readbacks;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
|
||||||
|
const MG_Pipe::MGPDrawRange* ranges,
|
||||||
|
const MG_Pipe::MGHostSpan* userIndices) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
|
||||||
|
if (table == nullptr) return false;
|
||||||
|
if (userIndices != nullptr) {
|
||||||
|
// kCapNeedsHostIndexBytes is 0 for the whole of P5 by ruling (table 0's cap-bit
|
||||||
|
// row) precisely so this tail never appears; a span that arrived anyway means the
|
||||||
|
// client's cap gate did not hold, and filling one is P8's.
|
||||||
|
MGLOG_E_ONCE("MG_Remote server: draw_vbo carries an MGHostSpan of user indices. P5 "
|
||||||
|
"rules kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes to 0 so that "
|
||||||
|
"no host span reaches the first IPC frame (contract table 0); filling "
|
||||||
|
"one under split is P8's. The draw is DECLINED rather than drawn from "
|
||||||
|
"a pointer that does not belong to this process");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ranges == nullptr || info.NumDraws == 0) return false;
|
||||||
|
|
||||||
|
// P5 IMPLEMENTS THE TWO SHAPES ITS REDUCED PATH USES AND DECLINES THE REST BY NAME.
|
||||||
|
// draw_vbo collapses all twenty draw entry points, and picking the right one needs the
|
||||||
|
// instancing / base-vertex / base-instance / multi-draw cross product. TriangleScenario
|
||||||
|
// is a single non-instanced array draw and OpenRA's are single indexed draws from a
|
||||||
|
// bound element buffer; the rest are P8's, together with the MGPDrawIndirect record
|
||||||
|
// that has no producer yet.
|
||||||
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||||
|
const Bool instanced = info.InstanceCount > 1 || info.StartInstance != 0;
|
||||||
|
if (info.NumDraws != 1 || instanced) {
|
||||||
|
MGLOG_E_ONCE("MG_Remote server: draw_vbo with NumDraws=%u InstanceCount=%u "
|
||||||
|
"StartInstance=%u is DECLINED - P5's reduced path is the single "
|
||||||
|
"non-instanced draw (BRIEF 4); the multi-draw and instanced arms are "
|
||||||
|
"P8's",
|
||||||
|
info.NumDraws, info.InstanceCount, info.StartInstance);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const MG_Pipe::MGPDrawRange& range = ranges[0];
|
||||||
|
if (info.IndexSize == 0) {
|
||||||
|
if (gl.DrawArrays == nullptr) return false;
|
||||||
|
gl.DrawArrays(static_cast<GLenum>(info.Mode), static_cast<GLint>(range.Start),
|
||||||
|
static_cast<GLsizei>(range.Count));
|
||||||
|
} else {
|
||||||
|
if (gl.DrawElementsBaseVertex == nullptr) return false;
|
||||||
|
GLenum indexType = GL_UNSIGNED_INT;
|
||||||
|
switch (info.IndexSize) {
|
||||||
|
case 1: indexType = GL_UNSIGNED_BYTE; break;
|
||||||
|
case 2: indexType = GL_UNSIGNED_SHORT; break;
|
||||||
|
case 4: indexType = GL_UNSIGNED_INT; break;
|
||||||
|
default:
|
||||||
|
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing
|
||||||
|
// else is a legal width; defaulting to 4 would read past the element buffer.
|
||||||
|
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
|
||||||
|
}
|
||||||
|
// Start is the FIRST INDEX, so the byte offset into the bound element buffer is
|
||||||
|
// Start * IndexSize - the same arithmetic PipeFill's emitter inverted.
|
||||||
|
const auto offset = static_cast<std::uintptr_t>(range.Start) * info.IndexSize;
|
||||||
|
gl.DrawElementsBaseVertex(static_cast<GLenum>(info.Mode),
|
||||||
|
static_cast<GLsizei>(range.Count), indexType,
|
||||||
|
reinterpret_cast<const void*>(offset), range.IndexBias);
|
||||||
|
}
|
||||||
|
++m_draws;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// PipeApplier
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
|
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
|
||||||
: m_segments(segments), m_replies(replies) {}
|
: m_segments(segments), m_replies(replies) {}
|
||||||
|
|
||||||
Bool PipeApplier::ApplyOne(const Transport::RingRecordView&) { MGP5_C0_STUB("PipeApplier::ApplyOne"); }
|
void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) {
|
||||||
|
if (control == nullptr || m_segments == nullptr) {
|
||||||
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp) {
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control "
|
||||||
MGP5_C0_STUB("PipeApplier::StampVerbBoundary");
|
"page or no segment table; ServerSession::Accept builds both before the "
|
||||||
|
"apply thread starts");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
m_verbs.SetBackend(backend);
|
||||||
|
m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies);
|
||||||
|
m_decoder.SetVerbSink(&m_verbs);
|
||||||
|
m_attached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint64 PipeApplier::ResidualPullCount() const { return m_residualPulls; }
|
void PipeApplier::Detach() {
|
||||||
|
m_decoder = Wire::PipeWireDecoder();
|
||||||
void PipeApplier::PoisonRetiredStageBytes(Uint64, Uint64) {
|
m_verbs.SetBackend(nullptr);
|
||||||
MGP5_C0_STUB("PipeApplier::PoisonRetiredStageBytes");
|
m_attached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#undef MGP5_C0_STUB
|
Bool PipeApplier::Attached() const { return m_attached; }
|
||||||
|
|
||||||
|
Bool PipeApplier::ApplyOne(const Transport::RingRecordView& record) {
|
||||||
|
if (!m_attached) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::ApplyOne before Attach\"} "
|
||||||
|
"- a record reached the applier with no decoder; the apply thread calls "
|
||||||
|
"Attach once before its first pop");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
// ORDER IS THE CONTRACT'S: stamp, then apply. The stamp is what makes any server-side
|
||||||
|
// read of gPipeInputs legal at all (PipeApplier.h's block 1), so a record applied
|
||||||
|
// before it aborts on the FIRST field inside SyncRenderState.
|
||||||
|
StampVerbBoundary(static_cast<MG_Pipe::MGPWireOp>(record.kind));
|
||||||
|
const Bool applied = m_decoder.DecodeAndApply(record);
|
||||||
|
// AND THE CLEAR IS INSIDE ApplyOne, NOT AFTER THE DRAIN BATCH. That is not tidiness,
|
||||||
|
// it is the barrier invariant. s1's SessionConsumer::ApplyOne publishes appliedSeq the
|
||||||
|
// instant this returns, and publishing appliedSeq is what makes the CLIENT runnable
|
||||||
|
// again (R-1: the barrier waits on exactly that watermark). A clear that ran after the
|
||||||
|
// batch would therefore be a second writer of gPipeInputs while the client is already
|
||||||
|
// touching it - the one thing table 3 says may not be introduced before the barrier
|
||||||
|
// retires - and the first version of this file had it there. It was caught by
|
||||||
|
// AClearRecordCrossesAndIsStampedAsAVerbBoundary failing INTERMITTENTLY, which is what
|
||||||
|
// a race looks like from the outside.
|
||||||
|
//
|
||||||
|
// THE COST, STATED: a record that is NOT a verb boundary now applies with the flag
|
||||||
|
// disarmed, so a sticky forward pulled from inside such a record's applier is not
|
||||||
|
// counted in `rsp`. Closing that needs an "enter the applier" entry point beside
|
||||||
|
// MGPipeServerStampVerbBoundary that arms the flag WITHOUT re-stamping - re-stamping on
|
||||||
|
// a non-verb op is what p1 forbids outright - and PipeInputs.cpp is p1's file. Left for
|
||||||
|
// the integrator to sequence; it makes `rsp` larger, never smaller, so the number this
|
||||||
|
// phase reports is a floor.
|
||||||
|
LeaveApplier();
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
// p1's rule verbatim (p1-v1 2). MGPipeVerbForWireOp is generated from MGP_VERB_OP_LIST in
|
||||||
|
// FieldOwnership.def and answers kVerbCount for every op that is NOT a verb boundary, so
|
||||||
|
// calling it unconditionally on every record is both correct and cheap. Four ops stamp:
|
||||||
|
// Clear -> Clear, DrawVbo -> DrawArrays, ReadPixels -> ReadPixels, Blit -> BlitFramebuffer.
|
||||||
|
//
|
||||||
|
// PRESENT IS DELIBERATELY NOT ONE, although contract 7 puts it in class B: FillPoints.def:21
|
||||||
|
// says Present and SetSwapInterval "go through BackendObject virtuals and read no frontend
|
||||||
|
// state, so they are not verbs here". There is no MGPipeVerb::Present, and stamping there
|
||||||
|
// would retire the previous verb's answers with nothing to put in their place.
|
||||||
|
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp op) {
|
||||||
|
const MG_Pipe::MGPipeVerb verb = MG_Pipe::MGPipeVerbForWireOp(op);
|
||||||
|
if (verb == MG_Pipe::MGPipeVerb::kVerbCount) return; // not a verb boundary: stamp nothing
|
||||||
|
MG_Pipe::MGPipeServerStampVerbBoundary(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PipeApplier::LeaveApplier() { MG_Pipe::MGPipeServerClearVerbBoundary(); }
|
||||||
|
|
||||||
|
Uint64 PipeApplier::ResidualPullCount() const { return MG_Pipe::MGPipeResidualPullCount(); }
|
||||||
|
|
||||||
|
// The decoder poisons EXACTLY the runs it resolved, from inside DecodeAndApply, once the
|
||||||
|
// applier has returned - so this entry point is the manual one, for a caller that knows a
|
||||||
|
// range is dead and is not the decoder. It is kept because c0's signature block declares
|
||||||
|
// it and because the R-11 copy in Managers.cpp is verified by poisoning a range by hand in
|
||||||
|
// a unit case; nothing on the live path calls it.
|
||||||
|
void PipeApplier::PoisonRetiredStageBytes(Uint64 offset, Uint64 size) {
|
||||||
|
if (size == 0 || m_segments == nullptr) return;
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
if (!MG_Config::Ipc.Audit) return;
|
||||||
|
#endif
|
||||||
|
const void* run = m_segments->Resolve(Wire::kSegStage, offset, size);
|
||||||
|
if (run == nullptr) return;
|
||||||
|
std::memset(const_cast<void*>(run), 0xDD, static_cast<SizeT>(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 PipeApplier::PoisonedStageBytes() const { return m_decoder.PoisonedStageBytes(); }
|
||||||
|
|
||||||
|
Uint64 PipeApplier::DecoderAppliedSeq() const { return m_decoder.AppliedSeq(); }
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Server
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
#include "../Transport/Ring.h"
|
#include "../Transport/Ring.h"
|
||||||
@@ -69,34 +70,147 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint32 m_slotBytes = 0;
|
Uint32 m_slotBytes = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- MGPClear's two discriminants ---------------------------------------------------
|
||||||
|
//
|
||||||
|
// MGPClear (MGPipeTypes.h:1271) names `Kind` "Whole | Color | Depth | Stencil |
|
||||||
|
// DepthStencil" and `ValueClass` "Float | Int | Uint" IN A COMMENT AND NOWHERE ELSE: the
|
||||||
|
// catalogue ships no enum for either, and the record has no producer or consumer in the
|
||||||
|
// tree, so P5 writes both halves and the two halves have to agree on a number. Declaring
|
||||||
|
// them here rather than open-coding 0..4 on each side is table 0's own rule for exactly
|
||||||
|
// this shape ("a decoder that open-codes it is the class-1 defect"), applied to a field
|
||||||
|
// table 0 did not reach.
|
||||||
|
//
|
||||||
|
// THE ORDER IS THE COMMENT'S, LEFT TO RIGHT, and ValueClass reuses the numbering
|
||||||
|
// MG_State/GLState/Core.h:39-41 already gives the identical three-way split on
|
||||||
|
// MGPAttribValue::ValueClass. c1 encodes against these constants; a disagreement is a
|
||||||
|
// clear of the wrong attachment with the wrong value type, which renders plausibly.
|
||||||
|
// FLAGGED FOR THE INTEGRATOR: this belongs in MGPipeTypes.h, which is c0's file.
|
||||||
|
inline constexpr Uint32 kMGPClearKindWhole = 0; // glClear(mask)
|
||||||
|
inline constexpr Uint32 kMGPClearKindColor = 1; // glClearBuffer{f,i,ui}v(GL_COLOR, i, v)
|
||||||
|
inline constexpr Uint32 kMGPClearKindDepth = 2; // glClearBufferfv(GL_DEPTH, 0, &d)
|
||||||
|
inline constexpr Uint32 kMGPClearKindStencil = 3; // glClearBufferiv(GL_STENCIL, 0, &s)
|
||||||
|
inline constexpr Uint32 kMGPClearKindDepthStencil = 4; // glClearBufferfi(GL_DEPTH_STENCIL,...)
|
||||||
|
inline constexpr Uint32 kMGPClearValueClassFloat = 0;
|
||||||
|
inline constexpr Uint32 kMGPClearValueClassInt = 1;
|
||||||
|
inline constexpr Uint32 kMGPClearValueClassUint = 2;
|
||||||
|
|
||||||
|
// ---- the five class-B verbs' consumer ------------------------------------------------
|
||||||
|
//
|
||||||
|
// Contract §7 class B is Clear (57), Blit (56), ReadPixels (58), DrawVbo (59) and Present
|
||||||
|
// (67), and NONE of them has an MGPipeApply* entry point - the 37 that exist are the object
|
||||||
|
// and state families. So w1's decoder validates and hands over a checked argument list and
|
||||||
|
// stops, and this is the other half: the SERVER'S OWN BACKEND CALL, through the private
|
||||||
|
// GlobalBackendFunctionsTable ServerLoop holds. It is not gBackendFunctionsTable, which in
|
||||||
|
// a split process is the client's emit table (table 3) - calling THAT here would re-emit
|
||||||
|
// the record the server is in the middle of applying, which is an infinite loop that
|
||||||
|
// renders nothing and looks like a hang.
|
||||||
|
class ServerVerbSink final : public Wire::WireVerbSink {
|
||||||
|
public:
|
||||||
|
// The server's private backend. Null until ServerLoop::CreateBackend has run, and a
|
||||||
|
// verb that arrives before then declines by name rather than dereferencing.
|
||||||
|
void SetBackend(MG_Backend::BackendObject* backend);
|
||||||
|
|
||||||
|
Bool OnClear(const MG_Pipe::MGPClear& clear) override;
|
||||||
|
Bool OnBlit(const MG_Pipe::MGPBlit& blit) override;
|
||||||
|
Bool OnPresent(const MG_Pipe::MGPPresent& present) override;
|
||||||
|
Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
||||||
|
Wire::ReplySink* replies) override;
|
||||||
|
Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges,
|
||||||
|
const MG_Pipe::MGHostSpan* userIndices) override;
|
||||||
|
|
||||||
|
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
|
||||||
|
// split build is also what a scenario that ran entirely on the monolith path looks
|
||||||
|
// like (R-16: a probe may not arm against a stub).
|
||||||
|
Uint64 Clears() const { return m_clears; }
|
||||||
|
Uint64 Draws() const { return m_draws; }
|
||||||
|
Uint64 Readbacks() const { return m_readbacks; }
|
||||||
|
Uint64 Blits() const { return m_blits; }
|
||||||
|
Uint64 Presents() const { return m_presents; }
|
||||||
|
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
|
||||||
|
Uint64 ReadbackBytes() const { return m_readbackBytes; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
|
||||||
|
|
||||||
|
MG_Backend::BackendObject* m_backend = nullptr;
|
||||||
|
Uint64 m_clears = 0;
|
||||||
|
Uint64 m_draws = 0;
|
||||||
|
Uint64 m_readbacks = 0;
|
||||||
|
Uint64 m_blits = 0;
|
||||||
|
Uint64 m_presents = 0;
|
||||||
|
Uint64 m_lastPresentSerial = 0;
|
||||||
|
Uint64 m_readbackBytes = 0;
|
||||||
|
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
||||||
|
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
||||||
|
// between them. Grown, never shrunk, and never handed out past the call.
|
||||||
|
Vector<Uint8> m_readbackScratch;
|
||||||
|
};
|
||||||
|
|
||||||
class PipeApplier {
|
class PipeApplier {
|
||||||
public:
|
public:
|
||||||
PipeApplier() = default;
|
PipeApplier() = default;
|
||||||
PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies);
|
PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies);
|
||||||
|
|
||||||
// Decode one record, stamp the verb, apply, post the reply if the call has one, then
|
// Builds the decoder over the session's control page and points it at this applier's
|
||||||
// advance appliedSeq by exactly one. P5 FORBIDS BATCHING appliedSeq (R-9): the barrier's
|
// verb sink. Separate from the constructor because ServerSession::Accept constructs the
|
||||||
// waiter reads it, and a batched watermark promises work that has not run.
|
// applier before it has decided anything about the apply thread, and the decoder needs
|
||||||
|
// the RingControl the constructor was never given.
|
||||||
|
//
|
||||||
|
// CALLED ON THE APPLY THREAD, ONCE, BEFORE THE FIRST RECORD. PipeWireDecoder is "not
|
||||||
|
// thread safe: one decoder on the apply thread, by construction", and its constructor
|
||||||
|
// installs the process-wide apply hook.
|
||||||
|
void Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend);
|
||||||
|
void Detach();
|
||||||
|
Bool Attached() const;
|
||||||
|
|
||||||
|
// Decode one record, stamp the verb, apply, post the reply if the call has one. THE
|
||||||
|
// CALLER advances appliedSeq by exactly one, through s1's SessionConsumer::ApplyOne,
|
||||||
|
// which is that watermark's single writer; P5 FORBIDS BATCHING it (R-9), because the
|
||||||
|
// barrier's waiter reads it and a batched watermark promises work that has not run.
|
||||||
Bool ApplyOne(const Transport::RingRecordView& record);
|
Bool ApplyOne(const Transport::RingRecordView& record);
|
||||||
|
|
||||||
// p1's rule, v1's call site. Called at the verb boundary, before the record's applier
|
// p1's rule, v1's call site. Called at the verb boundary, before the record's applier
|
||||||
// runs, with the verb the record belongs to.
|
// runs, with the verb the record belongs to.
|
||||||
void StampVerbBoundary(MG_Pipe::MGPWireOp op);
|
void StampVerbBoundary(MG_Pipe::MGPWireOp op);
|
||||||
|
|
||||||
|
// MANDATORY on leaving the applier (p1's M-5, PipeInputs.h's ServerStampedVerb block).
|
||||||
|
// The client's MGPipeValidateForVerb / MGPipeLeaveVerb also clear it, which is enough
|
||||||
|
// for inproc and NOT enough for a spawned server, where MG_Impl is not in the process:
|
||||||
|
// there the flag would latch TRUE for the server's life, every later read anywhere
|
||||||
|
// would be judged against the last verb's mask, and the sticky forwards would start
|
||||||
|
// aborting under strict on exactly the case their exemption exists for.
|
||||||
|
void LeaveApplier();
|
||||||
|
|
||||||
// R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side
|
// R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side
|
||||||
// increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the
|
// increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the
|
||||||
// end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
|
// end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
|
||||||
|
//
|
||||||
|
// IT FORWARDS TO MGPipeResidualPullCount() AND KEEPS NO MEMBER OF ITS OWN. The member
|
||||||
|
// c0's signature block declared is deleted rather than wired: the counter is
|
||||||
|
// process-wide in PipeInputs.cpp because the reads that increment it happen inside the
|
||||||
|
// BACKEND, arbitrarily deep under an MGPipeApply* call, with no PipeApplier in scope.
|
||||||
|
// A second copy here could only ever be a number that disagreed with the one the exit
|
||||||
|
// gate reads (p1-v1 5).
|
||||||
Uint64 ResidualPullCount() const;
|
Uint64 ResidualPullCount() const;
|
||||||
|
|
||||||
// R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with
|
// R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with
|
||||||
// 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte.
|
// 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte.
|
||||||
void PoisonRetiredStageBytes(Uint64 offset, Uint64 size);
|
void PoisonRetiredStageBytes(Uint64 offset, Uint64 size);
|
||||||
|
|
||||||
|
// How many staged bytes the decoder has poisoned, and how many records it has applied -
|
||||||
|
// the two numbers the audit lane asserts are non-zero, since an instrumentation that
|
||||||
|
// cannot be observed to have run is decoration.
|
||||||
|
Uint64 PoisonedStageBytes() const;
|
||||||
|
Uint64 DecoderAppliedSeq() const;
|
||||||
|
ServerVerbSink& Verbs() { return m_verbs; }
|
||||||
|
const ServerVerbSink& Verbs() const { return m_verbs; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Wire::SegmentTable* m_segments = nullptr;
|
Wire::SegmentTable* m_segments = nullptr;
|
||||||
ReplyPool* m_replies = nullptr;
|
ReplyPool* m_replies = nullptr;
|
||||||
Wire::PipeWireDecoder m_decoder;
|
Wire::PipeWireDecoder m_decoder;
|
||||||
Uint64 m_residualPulls = 0;
|
ServerVerbSink m_verbs;
|
||||||
|
Bool m_attached = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Server
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
|
|||||||
Reference in New Issue
Block a user