mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
829 lines
48 KiB
C++
829 lines
48 KiB
C++
// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
// P5 package v1: the applier bridge, and the consumer for contract 7's five class-B verbs.
|
|
|
|
#include "PipeApplier.h"
|
|
#include <MG_Remote/FatalFunnel.h>
|
|
|
|
#include "ServerSession.h"
|
|
#include "ServerLoop.h"
|
|
#include "../Transport/ReplySlot.h"
|
|
#include <MG_Backend/Record/StagedTextureStore.h>
|
|
|
|
#include <Config.h>
|
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
|
// P5c ct: object_death's per-kind release names the Espryt twin tables (CONTRACT-P5C.md
|
|
// §5.2). The same dependency ServerLoop.cpp already takes for CreateBackend; a server built
|
|
// on Magma simply holds no twins in these tables and every release resolves to nothing.
|
|
#include <MG_Backend/DirectGLES/Managers.h>
|
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
|
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
|
|
#include <MG_Remote/Client/ClientSession.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
|
|
|
#include <cstdlib>
|
|
#if !defined(_WIN32)
|
|
#include <unistd.h>
|
|
#endif
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
namespace MobileGL::MG_Remote::Server {
|
|
|
|
// P5e (ra, CONTRACT-P5E §1 / §6): DOES THIS SERVER PUBLISH kCapRunAheadApply? The
|
|
// question is asked of the server's OWN CallMask and not of a build constant, because
|
|
// "the client may run ahead" is exactly what that bit says and Magma never sets it.
|
|
// A session with no CallMask yet (the bring-up window, a fixture that never called
|
|
// SetCapabilityBits) answers false: no client can have latched run-ahead against a
|
|
// snapshot that was never published.
|
|
//
|
|
// P5e (gl, ID-111): PROMOTED OUT OF THE ANONYMOUS NAMESPACE, unchanged in body. It is now
|
|
// the second conjunct of the barriered stamp as well as Present's frame-serial gate, and
|
|
// the red-once has to be able to assert what it answers for the session it built.
|
|
Bool MGPipeServerPublishesRunAhead() {
|
|
const ServerSession* session = ServerSession::Active();
|
|
if (session == nullptr || !session->CallMaskIsSet()) return false;
|
|
return (session->CallMask() & static_cast<Uint64>(MG_Pipe::kCapRunAheadApply)) != 0;
|
|
}
|
|
|
|
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) {}
|
|
|
|
// 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,
|
|
// 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
|
|
// answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another
|
|
// call's answer instead of failing.
|
|
//
|
|
// The view is rebuilt per call rather than stored, so that this body does not change
|
|
// ReplyPool's four members and therefore does not touch v1's header at all.
|
|
void ReplyPool::PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) {
|
|
if (m_link) {
|
|
const auto result = m_link->PostReply(seq, status, bytes, size);
|
|
if (result == MOBILEGL_OK) {
|
|
++m_posted;
|
|
|
|
return;
|
|
}
|
|
++m_failed;
|
|
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL)
|
|
SessionFail(MGFatalFamily::ReplyTooLarge, "MGPipe: Fatal{ReplyTooLarge, stream reply}");
|
|
// EVERY OTHER FAILURE IS NAMED AND COUNTED RATHER THAN SWALLOWED (P12). This used to
|
|
// `return` on anything that was not BUFFER_TOO_SMALL, so a TRANSPORT_CLOSED reply
|
|
// vanished with no line, no counter and no Fatal - and the client's only symptom is a
|
|
// deferred answer that never arrives, which is indistinguishable from a buffer that
|
|
// overwrote it. Once per call site rather than once per reply: a broken link fails
|
|
// every time, and a log per answer would drown the run it is there to explain.
|
|
MGLOG_E_ONCE("MGPipe: reply seq %llu was NOT delivered (%d); the client will not read"
|
|
" this answer. FailedReplies() counts them",
|
|
static_cast<unsigned long long>(seq), static_cast<int>(result));
|
|
return;
|
|
}
|
|
Transport::ReplySlotPool pool(m_base, m_size, m_slots);
|
|
// Fatal inside Post when the answer does not fit a slot: P5 does not chunk replies,
|
|
// and the client knows an answer's size before it emits the record.
|
|
pool.Post(seq, status, bytes, size);
|
|
++m_posted;
|
|
}
|
|
|
|
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// ServerVerbSink - the five class-B verbs
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
void ServerVerbSink::OnBackendChanged() {
|
|
ReleaseQueries();
|
|
ReleaseFences();
|
|
}
|
|
|
|
ServerVerbSink::FenceEntry& ServerVerbSink::FindFence(MG_Pipe::MGPipeHandle handle) {
|
|
const auto it = m_fences.find(handle.Slot);
|
|
if (handle.Slot == 0 || it == m_fences.end() ||
|
|
!it->second.Live || it->second.Gen != handle.Gen) {
|
|
Wire::WireProtocolFatal("Fence.handle", "missing, destroyed or stale fence handle");
|
|
}
|
|
return it->second;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status,
|
|
Uint64& inProcessBase) {
|
|
ServerSession* session = ServerSession::Active();
|
|
return session != nullptr && session->AdoptStoreT0(handle, seq, status, inProcessBase);
|
|
}
|
|
|
|
Bool ServerVerbSink::OnFenceCreate(const MG_Pipe::MGPHandleOnly& desc) {
|
|
if (desc.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence))
|
|
Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace");
|
|
const auto* table = Table("FenceCreate");
|
|
if (table == nullptr) return false;
|
|
const auto handle = desc.Handle;
|
|
if (handle.Slot == 0) {
|
|
Wire::WireProtocolFatal("FenceCreate.handle", "reserved fence handle");
|
|
}
|
|
const Bool seen = m_fences.find(handle.Slot) != m_fences.end();
|
|
auto& entry = m_fences[handle.Slot];
|
|
if (entry.Live || (seen && handle.Gen <= entry.Gen)) {
|
|
Wire::WireProtocolFatal("FenceCreate.handle", "duplicate or stale fence generation");
|
|
}
|
|
entry.Gen = handle.Gen;
|
|
entry.Live = true;
|
|
// GL_Sync.cpp treats an absent slot or a null creation result as always signaled.
|
|
entry.Native = table->GL.FenceSync == nullptr ? nullptr : table->GL.FenceSync();
|
|
// P10: in creation order, for ReportSignaledFences - the order the GPU finishes them in.
|
|
m_unreportedFences.push_back(handle);
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnFenceDestroy(const MG_Pipe::MGPHandleOnly& desc) {
|
|
if (desc.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence))
|
|
Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace");
|
|
auto& entry = FindFence(desc.Handle);
|
|
const auto* table = Table("FenceDestroy");
|
|
if (table == nullptr) return false;
|
|
if (entry.Native != nullptr && table->GL.DeleteSync != nullptr) table->GL.DeleteSync(entry.Native);
|
|
entry.Native = nullptr;
|
|
entry.Live = false;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnFenceStatus(const MG_Pipe::MGPHandleOnly& desc, Uint32& result) {
|
|
if (desc.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence))
|
|
Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace");
|
|
auto& entry = FindFence(desc.Handle);
|
|
const auto* table = Table("FenceStatus");
|
|
if (table == nullptr) return false;
|
|
result = entry.Native == nullptr || table->GL.GetSyncStatus == nullptr ||
|
|
table->GL.GetSyncStatus(entry.Native);
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnFenceWait(const MG_Pipe::MGPFenceWait& request, Uint32& result) {
|
|
if ((request.Flags & ~static_cast<Uint32>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
|
|
Wire::WireProtocolFatal("FenceWait.Flags", "unknown client-wait flag");
|
|
}
|
|
auto& entry = FindFence(request.Fence);
|
|
const auto* table = Table("FenceWait");
|
|
if (table == nullptr) return false;
|
|
result = entry.Native == nullptr || table->GL.ClientWaitSync == nullptr
|
|
? GL_ALREADY_SIGNALED
|
|
: table->GL.ClientWaitSync(entry.Native, request.Flags, request.TimeoutNs);
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnFenceWaitServer(const MG_Pipe::MGPFenceWait& request) {
|
|
if (request.Flags != 0 || request.TimeoutNs != GL_TIMEOUT_IGNORED) {
|
|
Wire::WireProtocolFatal("FenceWaitServer.arguments", "invalid server wait arguments");
|
|
}
|
|
auto& entry = FindFence(request.Fence);
|
|
const auto* table = Table("FenceWaitServer");
|
|
if (table == nullptr) return false;
|
|
if (entry.Native != nullptr && table->GL.WaitSync != nullptr)
|
|
table->GL.WaitSync(entry.Native, request.Flags, request.TimeoutNs);
|
|
return true;
|
|
}
|
|
|
|
void ServerVerbSink::ReleaseFences() {
|
|
// Detach runs on the apply thread before its private backend/context is destroyed.
|
|
if (m_backend != nullptr) {
|
|
const auto destroy = m_backend->GetBackendFunctions().GL.DeleteSync;
|
|
if (destroy != nullptr) {
|
|
for (auto& [slot, entry] : m_fences)
|
|
if (entry.Live && entry.Native != nullptr) destroy(entry.Native);
|
|
}
|
|
}
|
|
m_fences.clear();
|
|
m_unreportedFences.clear();
|
|
}
|
|
|
|
// P10 (CONTRACT-P10.md §1): WHICH OF THE CLIENT'S FENCES HAS THE GPU FINISHED.
|
|
//
|
|
// The client answers a poll of a fence it knows is signaled without asking - that is the
|
|
// whole of package A - and it learns what is signaled only from here. Walked in CREATION
|
|
// order and stopped at the first one still pending: one context's GPU work completes in
|
|
// the order it was submitted, so a later fence cannot be done while an earlier one is not,
|
|
// and the steady-state cost is one status query per drain batch. (Were a backend ever to
|
|
// finish them out of order, the later fence would only be reported late - an answer the
|
|
// client then gets by its own round trip - never early.)
|
|
//
|
|
// `flush`: the apply thread is about to go idle. A fence whose commands the driver still
|
|
// holds never signals on its own, and an idle server submits nothing more, so the first
|
|
// pending fence is asked through ClientWaitSync(GL_SYNC_FLUSH_COMMANDS_BIT, 0) - the one
|
|
// backend-neutral way both backends take to submit what they hold - instead of the plain
|
|
// status query. Only when idle: mid-stream the next records flush soon enough, and a
|
|
// submit per batch would cost Magma its command-buffer batching.
|
|
Uint32 ServerVerbSink::ReportSignaledFences(Bool flush) {
|
|
if (m_unreportedFences.empty()) return 0;
|
|
ServerSession* session = ServerSession::Active();
|
|
if (session == nullptr || m_backend == nullptr) return 0;
|
|
const MG_Backend::GLFunctionsTable& gl = m_backend->GetBackendFunctions().GL;
|
|
Uint32 reported = 0;
|
|
while (!m_unreportedFences.empty()) {
|
|
const MG_Pipe::MGPipeHandle handle = m_unreportedFences.front();
|
|
const auto it = m_fences.find(handle.Slot);
|
|
if (it == m_fences.end() || !it->second.Live || it->second.Gen != handle.Gen) {
|
|
// Deleted before it was seen signaled: nobody can poll it any more.
|
|
m_unreportedFences.pop_front();
|
|
continue;
|
|
}
|
|
const FenceEntry& entry = it->second;
|
|
Bool signaled = entry.Native == nullptr;
|
|
if (!signaled) {
|
|
if (flush && gl.ClientWaitSync != nullptr) {
|
|
const GLenum status = gl.ClientWaitSync(entry.Native, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
|
|
signaled = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED;
|
|
flush = false; // one submit is all an idle server owes
|
|
++m_fenceIdleFlushes;
|
|
} else {
|
|
signaled = gl.GetSyncStatus == nullptr || gl.GetSyncStatus(entry.Native);
|
|
}
|
|
}
|
|
if (!signaled) break;
|
|
m_unreportedFences.pop_front();
|
|
session->PostFenceSignaled(handle);
|
|
++reported;
|
|
}
|
|
m_fencesReported += reported;
|
|
return reported;
|
|
}
|
|
|
|
#include "QueryServer.inc"
|
|
|
|
namespace {
|
|
// A record's damage rectangles as a region; 0 rectangles is the whole surface. A count past
|
|
// what the record holds is clamped (the rectangles it names are all there are).
|
|
MG_Util::Damage::Region DamageOfRecord(Uint32 count, const Int32* rects) {
|
|
const Int32 n = static_cast<Int32>(std::min<Uint32>(count, MG_Pipe::kMGPMaxDamageRects));
|
|
return MG_Util::Damage::Region::FromEglRects(rects, n);
|
|
}
|
|
} // namespace
|
|
|
|
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 wants present <-> eglSwapBuffers to stay 1:1.
|
|
//
|
|
// m-1: THAT 1:1 IS A CONVENTION c1 UPHOLDS, NOT A STRUCTURAL GUARANTEE, and the earlier
|
|
// claim that it was structural is wrong. This Present() is reached ONLY from a present
|
|
// RECORD (ServerVerbSink::OnPresent). ServerSwapEGLBuffers does NOT reach it - it calls
|
|
// backend->SwapEGLBuffers -> BackendObject::SwapEGLBuffers -> eglSwapBuffers, and never
|
|
// Present() - so the two paths do NOT both end here. The frame count staying in step with
|
|
// the swap count rests entirely on c1 emitting exactly one present record per swap;
|
|
// nothing here compares Presents() to a swap count. If that drifts, the frame fence and
|
|
// TrimBufferPool's recycle watermark stop tracking frames - which is the reason the 1:1
|
|
// was wanted, recorded here so a future swap-without-present is looked for rather than
|
|
// assumed impossible. Presents() is exposed for a lane that wants to make the comparison.
|
|
// The frame's damage rides to the native present for the length of this call.
|
|
MG_Backend::SetCurrentPresentDamage(DamageOfRecord(present.DamageCount, present.Damage));
|
|
table->Present();
|
|
MG_Backend::SetCurrentPresentDamage(MG_Util::Damage::Region::Full());
|
|
++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.
|
|
//
|
|
// P5e (ra, CONTRACT-P5E §1, §2.4): IT IS THE CLIENT'S NOW, 1-BASED AND MINTED BY THE
|
|
// PAYER. A credit can only be paced in an id space the waiter advances, and the waiter
|
|
// is the client (`WaitForPresentAck(m_presentsSent + 1 - credit)`), so a server-stamped
|
|
// serial would be the server acknowledging its own count. The 0 arm survives for a
|
|
// peer that has not been re-built - and on a server that PUBLISHES the run-ahead cap it
|
|
// is a protocol fault, because such a client is pacing on this answer and a 0 would
|
|
// acknowledge a frame nobody asked about.
|
|
if (present.FrameSerial == 0 && MGPipeServerPublishesRunAhead()) {
|
|
// PH-1 (3): latches in an armed session child - no credit is returned for it, the
|
|
// session closes and the peer's credit wait ends on the hang-up.
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"Present.FrameSerial\"} - a run-ahead "
|
|
"server was handed present serial 0. The client mints this 1-based and "
|
|
"waits on it for its credit (CONTRACT-P5E §2.4); returning a credit for "
|
|
"serial 0 would release a wait that is asking about frame N");
|
|
}
|
|
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
|
|
// §2.4's other half, and the reason ServerSession::ReturnPresentCredit has had no
|
|
// production caller since v1 wrote it: ONE CREDIT PER SWAP, returned after Present()
|
|
// has returned rather than before it, because what the client is waiting for is the
|
|
// swap and not the record's apply. It advances presentAckSerial AND rings the client's
|
|
// bell - a client parked in WaitForPresentAck(kWaitForever) needs the pair.
|
|
if (ServerSession* session = ServerSession::Active()) {
|
|
// P10 (CONTRACT-P10.md §1): the fences finished by now are reported BEFORE the credit
|
|
// goes back - a client released by the credit polls or waits on the fence of a frame
|
|
// or two ago at once, and a report posted only after this batch would race it. A
|
|
// status query, no submit: the swap has just submitted.
|
|
if (session->EventRingHasRoom()) ReportSignaledFences(/*flush=*/false);
|
|
session->ReturnPresentCredit(m_lastPresentSerial);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5) ------------------------------------------
|
|
//
|
|
// TWO CONTROL RECORDS, NO BACKEND TABLE AND NO DECLINE ARM. Neither body consults
|
|
// Table(): the reset belongs to the applier this process owns, and the death release
|
|
// belongs to the twin tables - a backend that registered no slots (Magma's XFB shape)
|
|
// still has an applier to reset and still answers a death with the same generation
|
|
// check. A record that cannot be proved is Fatal, not declined: both refusals are
|
|
// ProtocolCorruption because by the time the sink runs the codec has already proved the
|
|
// record's SHAPE, and what is left to check are the contract facts about the peer (§1).
|
|
|
|
Bool ServerVerbSink::OnApplierReset(const MG_Pipe::MGPApplierReset& reset) {
|
|
// §1: the serial is ASSERTED, never dispatched on. P5c has exactly one context per
|
|
// session, so the only legal sequence is 0, 1, 2, ... and the session's own count of
|
|
// accepted resets IS the expected value; anything else means the two ends disagree
|
|
// about how many make-current edges have crossed, which no backend answer can fix.
|
|
// PH-1 (3): latches in an armed session child (the peer wrote the serial); dies unarmed.
|
|
if (reset.ContextSerial != m_applierResetSerial) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the "
|
|
"record carries %llu and this session has accepted %llu reset(s); the "
|
|
"serial is asserted against the session's own count, not dispatched on "
|
|
"(one context per session in P5c)",
|
|
static_cast<unsigned long long>(reset.ContextSerial),
|
|
static_cast<unsigned long long>(m_applierResetSerial));
|
|
}
|
|
++m_applierResetSerial;
|
|
// THE WHOLE POINT OF THE RECORD: the reset runs HERE, on the apply thread, against
|
|
// the g_applier this role owns (PipeApply.cpp:409). The layer-2 guard inside
|
|
// MGPipeApplierReset passes because this IS the apply thread; the GL-thread direct
|
|
// call it replaced is the Fatal arm.
|
|
MG_Pipe::MGPipeApplierReset();
|
|
++m_applierResets;
|
|
return true;
|
|
}
|
|
|
|
ServerVerbSink::OwnedFd& ServerVerbSink::OwnedFd::operator=(OwnedFd&& other) noexcept {
|
|
if (this != &other) Reset(std::exchange(other.fd, -1));
|
|
return *this;
|
|
}
|
|
|
|
ServerVerbSink::OwnedFd::~OwnedFd() { Reset(-1); }
|
|
|
|
void ServerVerbSink::OwnedFd::Reset(int next) {
|
|
#if !defined(_WIN32)
|
|
if (fd >= 0) ::close(fd);
|
|
#endif
|
|
fd = next;
|
|
}
|
|
|
|
// P14 S1. THE RING'S DOOR INTO THE SESSION'S CONTEXT TABLE.
|
|
//
|
|
// The record carries the client's current context token and this is where a session's
|
|
// attribution moves. It is deliberately NOT an assertion like OnApplierReset's serial: the
|
|
// client's EGL layer mints the token and the server has no second count to compare against,
|
|
// so what is checked is a FACT ABOUT THIS SESSION'S OWN TABLE - a token that CreateContext
|
|
// never created is corruption, not a late record. Token 0 is the release edge and is always
|
|
// legal (a session whose client released its context has no context, which is exactly the
|
|
// value a session that never bound one already answers).
|
|
Bool ServerVerbSink::OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
|
|
MG_Pipe::MGPSharedImageReply& reply) {
|
|
namespace SI = SharedImages;
|
|
ServerSession* session = ServerSession::Active();
|
|
if (session == nullptr) return false;
|
|
SI::SessionHolder& holder = session->SharedImageHolder();
|
|
const auto fill = [&reply](const SI::Image& image) {
|
|
reply.ImageId = image.Id;
|
|
reply.Modifier = image.Modifier;
|
|
reply.Width = image.Width;
|
|
reply.Height = image.Height;
|
|
reply.Format = image.Fourcc;
|
|
reply.Stride = image.Stride;
|
|
reply.Offset = image.Offset;
|
|
reply.Plane1Stride = image.Plane1Stride;
|
|
reply.Plane1Offset = image.Plane1Offset;
|
|
};
|
|
switch (op.Op) {
|
|
case MG_Pipe::kMGPSharedImageAllocate: {
|
|
std::string why;
|
|
SI::ImageRef image = SI::Allocate(op.Width, op.Height, op.Format, why);
|
|
if (image == nullptr) {
|
|
MGLOG_E("MG_Remote server: shared image %ux%u (fourcc 0x%08x) not allocated: %s", op.Width, op.Height,
|
|
op.Format, why.c_str());
|
|
return false;
|
|
}
|
|
Transport::ITransport* transport = session->ControlTransport();
|
|
// The descriptor goes out BEFORE the reply is posted, so the client finds it queued.
|
|
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, seq, image->Id, 0};
|
|
const MobileGLResult shared = transport != nullptr
|
|
? transport->ShareFd(image->Fd, MobileGLByteSpan{&offer, sizeof(offer)})
|
|
: MOBILEGL_ERR_NOT_INITIALIZED;
|
|
if (shared != MOBILEGL_OK) {
|
|
MGLOG_E("MG_Remote server: shared image %llu could not be sent on the aux socket (rc=%d)",
|
|
static_cast<unsigned long long>(image->Id), static_cast<int>(shared));
|
|
return false;
|
|
}
|
|
holder.Hold(image);
|
|
fill(*image);
|
|
MGLOG_D("MG_Remote server: shared image %llu allocated, %ux%u stride %u",
|
|
static_cast<unsigned long long>(image->Id), image->Width, image->Height, image->Stride);
|
|
return true;
|
|
}
|
|
case MG_Pipe::kMGPSharedImageImport: {
|
|
int fd = -1;
|
|
std::string why;
|
|
if (session->T0Inbox().TakeSharedImageFd(seq, Transport::AdoptT0::kOfferWaitMs, &fd, why) !=
|
|
AdoptInbox::Outcome::Taken) {
|
|
MGLOG_E("MG_Remote server: shared-image import (record %llu) has no descriptor: %s",
|
|
static_cast<unsigned long long>(seq), why.c_str());
|
|
return false;
|
|
}
|
|
SI::ImageRef image = SI::Identify(fd, why);
|
|
// YUV: the planes and colour hints ride in Damage (MGPImportPlaneWord). A buffer this
|
|
// server did not allocate is taken too, through the labelled CPU-copy fallback
|
|
// (SharedImageRegistry.h, Image::ForeignSource); RGBA ones never are.
|
|
const Bool yuv = SI::FourccIsYuv(op.Format) && op.DamageCount == MG_Pipe::kMGPImportPlaneWords;
|
|
if (image == nullptr && yuv) {
|
|
SI::PlaneLayout layout;
|
|
layout.Count = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportPlaneCount]);
|
|
layout.Offset[0] = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportPlane0Offset]);
|
|
layout.Offset[1] = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportPlane1Offset]);
|
|
layout.Pitch[0] = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportPlane0Pitch]);
|
|
layout.Pitch[1] = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportPlane1Pitch]);
|
|
// The copy reads rows: only a buffer said to be linear (or said nothing) is one.
|
|
const Uint64 modifier =
|
|
static_cast<Uint64>(static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportModifierLo])) |
|
|
(static_cast<Uint64>(static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportModifierHi])) << 32);
|
|
if (op.Damage[MG_Pipe::kMGPImportHasModifier] != 0 && modifier != SI::kModifierLinear &&
|
|
modifier != SI::kModifierInvalid) {
|
|
char text[96];
|
|
std::snprintf(text, sizeof(text), "; a foreign buffer of modifier 0x%016llx is not linear",
|
|
static_cast<unsigned long long>(modifier));
|
|
why += text;
|
|
} else {
|
|
std::string foreignWhy;
|
|
image = SI::ImportForeignYuv(fd, op.Width, op.Height, op.Format, layout, foreignWhy);
|
|
if (image == nullptr) why += "; as a foreign YUV buffer: " + foreignWhy;
|
|
}
|
|
}
|
|
#if !defined(_WIN32)
|
|
::close(fd);
|
|
#endif
|
|
if (image == nullptr) {
|
|
MGLOG_W_ONCE("MG_Remote server: a dma-buf import was refused: %s", why.c_str());
|
|
return false;
|
|
}
|
|
if (SI::FourccIsYuv(image->Fourcc) != SI::FourccIsYuv(op.Format) ||
|
|
(SI::FourccIsYuv(op.Format) && image->Fourcc != op.Format)) {
|
|
MGLOG_W("MG_Remote server: a dma-buf import of image %llu (fourcc 0x%08x) as fourcc 0x%08x was refused",
|
|
static_cast<unsigned long long>(image->Id), image->Fourcc, op.Format);
|
|
return false;
|
|
}
|
|
if (yuv) {
|
|
SI::Yuv::Hints hints;
|
|
hints.ColorSpace = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportYuvColorSpace]);
|
|
hints.Range = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportYuvRange]);
|
|
hints.SitingX = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportYuvSiting]) & 0xffffu;
|
|
hints.SitingY = static_cast<Uint32>(op.Damage[MG_Pipe::kMGPImportYuvSiting]) >> 16;
|
|
SI::SetYuvHints(*image, hints);
|
|
}
|
|
if (image->Width != op.Width || image->Height != op.Height || !SI::FourccSupported(op.Format)) {
|
|
MGLOG_W("MG_Remote server: a dma-buf import of image %llu (%ux%u) as %ux%u fourcc 0x%08x was refused",
|
|
static_cast<unsigned long long>(image->Id), image->Width, image->Height, op.Width, op.Height,
|
|
op.Format);
|
|
return false;
|
|
}
|
|
holder.Hold(image);
|
|
fill(*image);
|
|
return true;
|
|
}
|
|
case MG_Pipe::kMGPSharedImageRelease:
|
|
reply.ImageId = op.ImageId;
|
|
return holder.Release(op.ImageId);
|
|
case MG_Pipe::kMGPSharedImagePresent: {
|
|
SI::ImageRef image = holder.Get(op.ImageId);
|
|
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
|
|
if (image == nullptr || backend == nullptr) return false;
|
|
MG_Backend::SharedImageView view;
|
|
view.Id = image->Id;
|
|
view.Width = image->Width;
|
|
view.Height = image->Height;
|
|
view.Fourcc = image->Fourcc;
|
|
view.NativeBuffer = image->Native;
|
|
if (!backend->BlitDefaultFramebufferToSharedImage(view, DamageOfRecord(op.DamageCount, op.Damage)))
|
|
return false;
|
|
fill(*image);
|
|
return true;
|
|
}
|
|
case MG_Pipe::kMGPSharedImageQueryBufferAge: {
|
|
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
|
|
if (backend == nullptr) return false;
|
|
reply.BufferAge = backend->QueryCurrentBufferAge((op.Format & MG_Pipe::kMGPBufferAgeDamageRegionFollows) != 0);
|
|
return true;
|
|
}
|
|
case MG_Pipe::kMGPSharedImageSetDamageRegion: {
|
|
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
|
|
if (backend == nullptr) return false;
|
|
return backend->SetCurrentDamageRegion(DamageOfRecord(op.DamageCount, op.Damage));
|
|
}
|
|
case MG_Pipe::kMGPSharedImageFlush: {
|
|
// The answer is posted after the fence is published: the client commits the buffer to
|
|
// its compositor only after this returns, so the compositor's session finds the write.
|
|
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
|
|
if (backend == nullptr) return false;
|
|
return backend->PublishSharedImageAccesses();
|
|
}
|
|
case MG_Pipe::kMGPSharedImageNativeFence: {
|
|
// A descriptor of the session's work, and an image writer's flush (MGPipeTypes.h). Like
|
|
// an Allocate's, the descriptor goes out BEFORE the reply is posted.
|
|
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
|
|
if (backend == nullptr) return false;
|
|
int fence = -1;
|
|
if (!backend->ExportNativeFence(&fence)) return false;
|
|
#if !defined(_WIN32)
|
|
if (fence >= 0) {
|
|
m_lastNativeFence.Reset(::dup(fence));
|
|
} else if (m_lastNativeFence.fd >= 0) {
|
|
fence = ::dup(m_lastNativeFence.fd);
|
|
}
|
|
#endif
|
|
reply.Format = 0;
|
|
if (fence < 0) return true;
|
|
Transport::ITransport* transport = session->ControlTransport();
|
|
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, seq, 0, 0};
|
|
const MobileGLResult shared = transport != nullptr
|
|
? transport->ShareFd(fence, MobileGLByteSpan{&offer, sizeof(offer)})
|
|
: MOBILEGL_ERR_NOT_INITIALIZED;
|
|
#if !defined(_WIN32)
|
|
::close(fence);
|
|
#endif
|
|
if (shared != MOBILEGL_OK) {
|
|
MGLOG_E_ONCE("MG_Remote server: a native fence could not be sent on the aux socket (rc=%d)",
|
|
static_cast<int>(shared));
|
|
return false;
|
|
}
|
|
reply.Format = MG_Pipe::kMGPNativeFenceFdFollows;
|
|
return true;
|
|
}
|
|
case MG_Pipe::kMGPSharedImageAttach: {
|
|
SI::ImageRef image = holder.Get(op.ImageId);
|
|
if (image == nullptr) return false;
|
|
if (!MG_Pipe::MGPipeApplyAttachSharedImage(op.Texture, image->Id)) {
|
|
MGLOG_E("MG_Remote server: shared image %llu could not be attached to texture {slot=%u, gen=%u}: the "
|
|
"texture has no record",
|
|
static_cast<unsigned long long>(image->Id), op.Texture.Slot, op.Texture.Gen);
|
|
return false;
|
|
}
|
|
fill(*image);
|
|
return true;
|
|
}
|
|
default:
|
|
MGLOG_E("MG_Remote server: shared_image operation %u is not one this server knows", op.Op);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Bool ServerVerbSink::OnBindContext(const MG_Pipe::MGPBindContext& bind) {
|
|
// NO SESSION IS A DECLINE, NOT A FAULT, and it is the one case here that a peer's bytes
|
|
// cannot cause: a bind is answered by the session's own table, so with no accepted
|
|
// session there is nothing to attribute the record to and nothing to refuse. (The window
|
|
// is Close() clearing the active session while the apply thread is still draining.)
|
|
ServerSession* session = ServerSession::Active();
|
|
if (session == nullptr) return false;
|
|
// PH-1 (3): the refusal latches in an armed session child (the peer wrote the token);
|
|
// unarmed it dies, like every other contract fact about a peer.
|
|
if (!session->BindContext(bind.ClientContextToken)) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the "
|
|
"record binds token %llu, which this session never created; a bind_context "
|
|
"names a context CreateContext put in the session's table, and 0 (the "
|
|
"release) is the only token that needs no entry",
|
|
static_cast<unsigned long long>(bind.ClientContextToken));
|
|
}
|
|
++m_bindContexts;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE HALF OF THE SWITCH.
|
|
//
|
|
// `bind_context` is where the session's context attribution moves, and it is also the ONLY
|
|
// point at which the apply thread's native context may move: the MakeCurrent control frame
|
|
// that precedes it still carries the OLD context (the client emits the binding after the
|
|
// binding has taken effect, which is the ordering S1 chose on purpose), so a native switch
|
|
// driven from there would bind the context the client just left. Frontend GL state is the
|
|
// backend's per-tuple business; this call makes the tuple the resolver now names current.
|
|
//
|
|
// It is a no-op for the shapes S4 did not change: with no resolver installed (monolith, a
|
|
// unit case, the inproc one-process shape) the key is {0,0}, and the single-context world's
|
|
// bind does not move the native context - MakeNativeContextCurrentForBoundToken returns
|
|
// without a driver call when the context it resolves to is already the one bound here.
|
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
|
(void)MG_Backend::DirectGLES::MakeNativeContextCurrentForBoundToken();
|
|
// The backend half for one that keeps a single native surface binding (Magma).
|
|
if (MG_Backend::BackendObject* backend = ServerLoopInstance().Backend()) {
|
|
backend->OnClientContextBound(bind.ClientContextToken);
|
|
}
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
|
|
// §1's zero ruling: a null handle means "the object never crossed", and the client
|
|
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
|
|
// not a no-op.
|
|
// PH-1 (3): both refusals latch in an armed session child; unarmed they die as before.
|
|
if (MG_Pipe::MGPipeHandleIsNull(death.Handle)) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null "
|
|
"handle never crosses: the client emits nothing for an object its own "
|
|
"allocator cannot resolve (CONTRACT-P5C.md §5.2)");
|
|
}
|
|
if (death.Kind >= static_cast<Uint32>(MG_Pipe::MGPipeKind::KindCount)) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an "
|
|
"MGPipeKind",
|
|
static_cast<unsigned>(death.Kind));
|
|
}
|
|
// The per-kind release, keyed by the handle the record carried. A false answer is
|
|
// NOT a decline: the kind's own delete opcode may already have released the twin
|
|
// (the idempotent second path every notice arm documents), and a kind this backend
|
|
// does not twin (Buffer, whose death crosses as resource_destroy) legally resolves
|
|
// to nothing.
|
|
MG_Backend::DirectGLES::ReleaseTwinsForWireObjectDeath(
|
|
death.Handle, static_cast<MG_Pipe::MGPipeKind>(death.Kind));
|
|
++m_objectDeaths;
|
|
return true;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// PipeApplier
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
|
|
: m_segments(segments), m_replies(replies) {}
|
|
|
|
void PipeApplier::Attach(Transport::ILink* link, MG_Backend::BackendObject* backend) {
|
|
if (!link || !link->Attached() || !m_segments)
|
|
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, PipeApplier::Attach missing link}");
|
|
m_verbs.SetBackend(backend);
|
|
m_verbs.SetMaxReplyBytes(link->Capabilities().MaxReplyBytes);
|
|
m_decoder = Wire::PipeWireDecoder(link, m_segments, m_replies);
|
|
m_decoder.SetVerbSink(&m_verbs);
|
|
MG_Pipe::MGPipeServerBlockNoteIdentity(); m_attached = true;
|
|
}
|
|
|
|
void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) {
|
|
if (control == nullptr || m_segments == nullptr) {
|
|
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control "
|
|
"page or no segment table; ServerSession::Accept builds both before the "
|
|
"apply thread starts");
|
|
}
|
|
m_verbs.SetBackend(backend);
|
|
m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies);
|
|
m_decoder.SetVerbSink(&m_verbs);
|
|
// P5f (f1), CONTRACT-P5E §3.2: the server block's identity, once per session (the
|
|
// per-verb stamp refreshes it against the served-context serial). A no-op unless the
|
|
// dual-block rehearsal is armed; without it the server block's ContextIdentity() stays
|
|
// nullptr and the backend's identity-keyed memo caches read that as a HIT on their
|
|
// zero-initialised slot - an unnamed null dereference instead of a named marker.
|
|
MG_Pipe::MGPipeServerBlockNoteIdentity();
|
|
m_attached = true;
|
|
}
|
|
|
|
void PipeApplier::Detach() {
|
|
m_decoder = Wire::PipeWireDecoder();
|
|
m_verbs.SetBackend(nullptr);
|
|
m_attached = false;
|
|
}
|
|
|
|
Bool PipeApplier::Attached() const { return m_attached; }
|
|
|
|
Bool PipeApplier::ApplyOne(const Transport::RingRecordView& record) {
|
|
if (!m_attached) {
|
|
SessionFail(MGFatalFamily::ProtocolCorruption, "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");
|
|
}
|
|
// R-1's INVARIANT, THE SERVER'S HALF (table 3's gPipeInputs row, c1-v1 8.1). The flag
|
|
// is raised for the WHOLE of this function and not only around DecodeAndApply: the
|
|
// stamp below and LeaveApplier at the end are both writes to gPipeInputs, and the client
|
|
// asserts the flag is down before it publishes (ClientSession::EmitAndWait), so a
|
|
// bracket that excluded either would leave a real write outside the check. It is
|
|
// dropped before this function returns, and s1's SessionConsumer::ApplyOne publishes
|
|
// appliedSeq only after that - so by the time the client is runnable the flag is down.
|
|
const Client::ClientSession::ScopedApplierEntry insideApplier;
|
|
// P5e (id), CONTRACT-P5E §2.1 / §4.4: STAMP WHETHER THE CLIENT IS PARKED BEHIND THIS
|
|
// RECORD, before anything can ask. It is the input to the allocator guard's exemption
|
|
// (SlotAllocator.cpp) and to every frontend-keyed twin member that survives as monolith
|
|
// glue (SlotTables.h), and it has to be up before DecodeAndApply because the sinks those
|
|
// reach are exactly the askers. MGPipeBarriered answers true for every record until ra
|
|
// lands the wait rule, so this line changes nothing this phase and is the line ra
|
|
// rebases onto rather than adds.
|
|
//
|
|
// AND THE STAMP IS `true` UNTIL ra LANDS THE CLIENT'S HALF (ID-103). The predicate
|
|
// describes what the client WILL do once EmitAndWaitTails follows the wait classes;
|
|
// today it still blocks after every record, so a `false` stamp here would withdraw the
|
|
// §4.4 exemptions from a probe the client's own wait still makes safe - a refusal with
|
|
// no defect behind it. The predicate is computed on every record all the same, so it is
|
|
// exercised for the whole phase rather than first run on the day it starts deciding.
|
|
//
|
|
// AND THE SERVER'S OWN CAPABILITY IS THE SECOND CONJUNCT (P5e gl, ID-111). The constant
|
|
// above says what the CLIENT will do once ra's wait rule is live; it says nothing about
|
|
// which server this is. kCapRunAheadApply is never published on DirectVulkan (ID-90,
|
|
// MGPipeRunAheadCapBitsFor), yet draw_vbo / blit / clear / launch_grid are all
|
|
// kWaitNone - so a stamp that read the build constant alone would, on the day it flips,
|
|
// have a MAGMA server mark every draw record UNBARRIERED while its client is still
|
|
// lockstep. CountBarrierPull (PipeInputs.cpp) is an unconditional Fatal on an
|
|
// unbarriered pull, no knob involved, and Magma's residual fill is its ONLY source for
|
|
// the seven pointer-backed fields: Magma would die on its first draw and take
|
|
// MagmaP7AllocatorDebtScope's exemption with it.
|
|
//
|
|
// BOTH PREDICATES ARE COMPUTED UNCONDITIONALLY, as arguments rather than as the arms of
|
|
// a short-circuit, which is ID-103's reason extended to the capability probe: they are
|
|
// exercised on every record for the whole phase rather than first running on the day
|
|
// they start deciding.
|
|
//
|
|
// PH-1 (3): BUT NOT BEFORE THE RECORD HAS BEEN ADMITTED. MGPipeBarriered reads payload
|
|
// fields (a DrawVbo's MGPDrawInfo::Flags) and the stamps index per-opcode tables, so a
|
|
// record whose ring header is shorter than its own type - or names no opcode - would be
|
|
// read past its end here, before DecodeAndApply's pre-gate could refuse it. In an armed
|
|
// session child the pre-gate therefore runs FIRST: a refused record latches by name and
|
|
// is declined with nothing stamped (ServerLoopTest's short-record case reads the stamp).
|
|
// Unarmed it admits everything and the generated gate keeps its death, as before.
|
|
if (!m_decoder.AdmitOrDecline(record)) return false;
|
|
const Bool wireSaysBarriered = MG_Pipe::MGPipeBarriered(
|
|
static_cast<MG_Pipe::MGPWireOp>(record.kind), record.payload, MG_Pipe::MGPipeApplier());
|
|
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(
|
|
MGPipeApplierStampsBarriered(MG_Pipe::kMGPipeP5eClientWaitRuleLanded,
|
|
MGPipeServerPublishesRunAhead(), wireSaysBarriered));
|
|
// P5e (gl), ID-128: AND WHETHER IT WAS BARRIERED BY ESCALATION RATHER THAN BY ITS CLASS.
|
|
// The predicate above is the static wait class plus two payload-derived escalations
|
|
// (ID-83: an open transform-feedback span, a draw carrying client vertex arrays), so the
|
|
// difference between it and the table IS the escalation - both halves are already
|
|
// computed here, and the second flag costs one compare. The strict knob is the only
|
|
// reader: a pull on an escalated record is a debt the phase that owns XFB (§5.7) or
|
|
// client arrays (ID-82) owes, neither of which is this one.
|
|
MG_Pipe::MGPipeApplierSetCurrentRecordBarrieredByEscalation(
|
|
wireSaysBarriered &&
|
|
MG_Pipe::MGPipeWaitClassFor(static_cast<MG_Pipe::MGPWireOp>(record.kind)) ==
|
|
MG_Pipe::kWaitNone);
|
|
// 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
|