mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1948 lines
115 KiB
C++
1948 lines
115 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 "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::SetBackend(MG_Backend::BackendObject* backend) {
|
|
if (m_backend != backend) {
|
|
ReleaseQueries();
|
|
ReleaseFences();
|
|
}
|
|
m_backend = backend;
|
|
if (backend != nullptr) {
|
|
const auto& limits = backend->GetDynamicParameters();
|
|
ServerStagedTexture().SetDeviceLimits(
|
|
limits.MaxTextureSize, limits.Max3DTextureSize, limits.MaxCubeMapTextureSize,
|
|
limits.MaxArrayTextureLayers, limits.MaxTextureBufferSize);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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) {
|
|
ServerSession* session = ServerSession::Active();
|
|
return session != nullptr && session->AdoptStoreT0(handle, seq, status);
|
|
}
|
|
|
|
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"
|
|
|
|
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 named framebuffer record already crossed before this verb. Scope
|
|
// only the server's resolved draw target; the next verb still observes
|
|
// the application's unchanged binding. Both backends sync from this
|
|
// handle and never need the frontend FramebufferObject.
|
|
auto& applier = MG_Pipe::MGPipeApplier();
|
|
struct ScopedClearTarget {
|
|
MG_Pipe::MGPipeApplierState& State;
|
|
MG_Pipe::MGPipeHandle Saved;
|
|
~ScopedClearTarget() { State.BoundFramebuffer[0] = Saved; }
|
|
} clearTarget{applier, applier.BoundFramebuffer[0]};
|
|
if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo)) {
|
|
if (!applier.FramebufferRecordFor(clear.Fbo))
|
|
Wire::WireProtocolFatal("Clear.Fbo", "missing named framebuffer record");
|
|
applier.BoundFramebuffer[0] = clear.Fbo;
|
|
}
|
|
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;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.3): the record's handles cross to the backend as the verb's
|
|
// own state. The bound form carries two nulls and nothing changes; the named form's
|
|
// pair is what the backend's named-blit arm resolves - the sink no longer relies on
|
|
// "the read and draw framebuffers are already bound" (the client's ScopedBlitBindings
|
|
// staging is deleted with this), and a backend that does not consume the pair has no
|
|
// named arm, which is a loud decline rather than a blit of whatever is bound.
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbBlitReadFbo = blit.ReadFbo;
|
|
applierState.VerbBlitDrawFbo = blit.DrawFbo;
|
|
const Bool named = !MG_Pipe::MGPipeHandleIsNull(blit.ReadFbo) ||
|
|
!MG_Pipe::MGPipeHandleIsNull(blit.DrawFbo);
|
|
#endif
|
|
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));
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (named && !applierState.VerbBlitNamedConsumed) {
|
|
MGLOG_E_ONCE("MGPipe: a named blit (read {%u, %u}, draw {%u, %u}) reached a backend "
|
|
"with no named-blit arm; the verb is DECLINED rather than applied to "
|
|
"the bound framebuffers",
|
|
blit.ReadFbo.Slot, blit.ReadFbo.Gen, blit.DrawFbo.Slot, blit.DrawFbo.Gen);
|
|
applierState.VerbBlitReadFbo = MG_Pipe::kMGPipeNullHandle;
|
|
applierState.VerbBlitDrawFbo = MG_Pipe::kMGPipeNullHandle;
|
|
return false;
|
|
}
|
|
#endif
|
|
++m_blits;
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
|
Wire::ReplySink* replies) {
|
|
if (!replies) Wire::WireProtocolFatal("GetTextureImage.reply", "missing reply sink");
|
|
const SizeT bpp = MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
|
|
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
|
|
Wire::WireProtocolFatal("GetTextureImage.extent", "invalid tight image extent");
|
|
}
|
|
const Uint64 tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
|
|
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D ||
|
|
info.DstOffset > tight || !info.DstSize || info.DstSize > tight - info.DstOffset)
|
|
Wire::WireProtocolFatal("GetTextureImage.range", "invalid reply window");
|
|
auto image = info;
|
|
image.DstOffset = 0;
|
|
image.DstSize = tight;
|
|
Vector<Uint8> bytes;
|
|
const Bool ok = ReadTextureImageTight(image, bytes);
|
|
if (!ok || bytes.size() != tight) {
|
|
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
|
|
return false;
|
|
}
|
|
replies->PostReply(seq, Wire::ReplySink::kStatusOk, bytes.data() + info.DstOffset, info.DstSize);
|
|
m_readbackBytes += info.DstSize;
|
|
++m_readbacks;
|
|
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.
|
|
SessionFail(MGFatalFamily::ProtocolCorruption, "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));
|
|
}
|
|
const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type)));
|
|
// The server's LinkTerms.maxReplyBytes is copied from its attached ILink capabilities.
|
|
// Reject malformed shapes and oversized replies before growing m_readbackScratch. The
|
|
// client-side guard is not a trust boundary: a peer can write this record directly.
|
|
const auto rejectReadback = [&] {
|
|
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
|
|
return false;
|
|
};
|
|
if (bytesPerPixel == 0 || info.Box.W == 0 || info.Box.H == 0 ||
|
|
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
info.DstSize == 0 || m_maxReplyBytes == 0) {
|
|
return rejectReadback();
|
|
}
|
|
|
|
const Uint64 width = info.Box.W;
|
|
const Uint64 height = info.Box.H;
|
|
const Uint64 bpp = static_cast<Uint64>(bytesPerPixel);
|
|
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
|
|
if (width > kUint64Max / height) return rejectReadback();
|
|
const Uint64 pixels = width * height;
|
|
if (pixels > kUint64Max / bpp) return rejectReadback();
|
|
const Uint64 tight = pixels * bpp;
|
|
// PH-3's bound is the SERVER's answer size: w*h*bpp against its own maxReplyBytes,
|
|
// checked before the scratch grows. It is not a rule about DstSize.
|
|
if (tight > m_maxReplyBytes ||
|
|
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
|
|
return rejectReadback();
|
|
}
|
|
// ID-49: THE REPLY CROSSES TIGHT, WHATEVER DstSize THE CLIENT SENT. The server reads with
|
|
// neutral pack state into a w*h*bpp extent that IS the reply payload and the client
|
|
// scatters it per its own GL_PACK_* state, so a DstSize that disagrees with the tight
|
|
// extent (a client that sized its destination with pack padding) is logged and the tight
|
|
// extent this side owns is read and posted - never the client's number, so a wrong
|
|
// DstSize cannot make this a short read into uninitialised scratch. F2's first PH-3 draft
|
|
// refused the mismatch instead, which broke the ID-49 control
|
|
// (ServerLoopEglTest.AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClientSent); the
|
|
// bound above is what PH-3 needs and it does not depend on DstSize.
|
|
if (tight != info.DstSize) {
|
|
MGLOG_E_ONCE("MG_Remote server: read_pixels DstSize %llu != tight w*h*bpp %llu "
|
|
"(%ux%u, bpp %llu); reading the tight extent (ID-49)",
|
|
static_cast<unsigned long long>(info.DstSize),
|
|
static_cast<unsigned long long>(tight), info.Box.W, info.Box.H,
|
|
static_cast<unsigned long long>(bpp));
|
|
}
|
|
|
|
if (!ReadBoundFramebufferTight(info, tight)) {
|
|
// A well-formed call can still be unsupported by this server; return the
|
|
// established decline status after the peer-controlled size has been checked.
|
|
replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0);
|
|
return false;
|
|
}
|
|
|
|
// m-7: the answer is written into the slot HERE, mid-apply, while the verb stamp is still
|
|
// up - and that is safe for exactly one reason, which is the contract's and is stated so it
|
|
// is not mistaken for luck: the client reaches a reply slot ONLY through appliedSeq
|
|
// (ReplySlot.h's ORDERING clause), never by polling the slot's own stamp, and s1's
|
|
// SessionConsumer::ApplyOne publishes appliedSeq only AFTER PipeApplier::ApplyOne has run
|
|
// LeaveApplier() and (on the joint tree) dropped the ScopedApplierEntry. So by the time the
|
|
// client is allowed to look at this slot, the apply-side gPipeInputs flag is already down.
|
|
replies->PostReply(seq, Wire::ReplySink::kStatusOk, m_readbackScratch.data(), tight);
|
|
m_readbackBytes += tight;
|
|
++m_readbacks;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
|
|
if (table == nullptr || table->GL.ReadPixels == nullptr) return false;
|
|
if (tight > m_readbackScratch.size()) {
|
|
m_readbackScratch.resize(static_cast<SizeT>(tight));
|
|
}
|
|
|
|
// Save the server-visible pack state, force neutral for the read, restore. Both go through
|
|
// the applier's own set_pixel_pack_state entry point (MGPipeApplySetPixelPackState writes
|
|
// gPipeInputs.m_pixelStore[0], which the backend's ReadPixels reads via
|
|
// gPipeInputs->GetPixelStoreParameters); the read is synchronous on this thread, so the window
|
|
// in which the pack state is neutral does not outlive the call.
|
|
const MG_Pipe::PixelStoreParameters savedPack =
|
|
MG_Pipe::gPipeInputs.GetPixelStoreParameters(/*isUnpack=*/false);
|
|
// MG_Pipe owns the constant (MGPipeTypes.h): in a verify build the compare-at-read hook's
|
|
// oracle for the pack half inside this window is the same value, and a second hand-typed
|
|
// copy would drift without a build break.
|
|
const MG_Pipe::MGPPixelPackState neutralPack = MG_Pipe::MGPipeNeutralReadPixelsPack();
|
|
MG_Pipe::MGPipeApplySetPixelPackState(neutralPack);
|
|
|
|
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());
|
|
|
|
MG_Pipe::MGPPixelPackState restorePack{};
|
|
restorePack.Pack = savedPack;
|
|
MG_Pipe::MGPipeApplySetPixelPackState(restorePack);
|
|
// A backend that found its device lost latched this session and returned without filling
|
|
// the scratch: the read is declined rather than answered with stale bytes.
|
|
return !SessionLatched();
|
|
}
|
|
|
|
Bool ServerVerbSink::ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector<Uint8>& bytes) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
if (m_backend && m_backend->GetBackendType() == BackendType::DirectGLES)
|
|
return MG_Backend::DirectGLES::ReadTextureImageWire(image, bytes);
|
|
if (m_backend && m_backend->GetBackendType() == BackendType::DirectVulkan &&
|
|
MG_Backend::DirectVulkan::pVulkanRenderer)
|
|
return MG_Backend::DirectVulkan::pVulkanRenderer->ReadTextureImageWire(image, bytes);
|
|
#endif
|
|
(void)image;
|
|
(void)bytes;
|
|
return false;
|
|
}
|
|
|
|
// ---- P9 (W1, MG_Remote/CONTRACT-P9.md §1): the pack-buffer readbacks ------------------------
|
|
//
|
|
// WHY THE ROWS GO THROUGH THE APPLIER'S BUFFER-WRITE GATE AND NOT THROUGH THE DRIVER'S OWN
|
|
// PBO READ. Monolith binds the driver buffer as GL_PIXEL_PACK_BUFFER and lets glReadPixels
|
|
// write it, then maps the whole store back into the frontend shadow (DirectGLES.cpp's
|
|
// "PBO used, mapping buffer to client memory"). A server has two stores per buffer - the
|
|
// backend's (GL buffer / VkBuffer) and R-11's staged shadow that every later drain uploads
|
|
// from - and a driver-side write reaches only the first: the next queued range drained from
|
|
// the shadow would then land OLD bytes over the pixels on Espryt, and Magma has no pack path
|
|
// in its ReadPixels at all. MGPipeApplyResourceSubData is the one write both backends already
|
|
// take from the server (Ops_H_SubData adopts into the staged shadow and queues the range;
|
|
// WriteWireBuffer orders the copy against in-flight reads), so the rows are exactly what a
|
|
// client glBufferSubData of the same bytes would have been - same serial bump, same epoch
|
|
// bump, same ordering against the records that follow.
|
|
//
|
|
// THE READ IS THE REPLY FORM'S, byte for byte (ReadBoundFramebufferTight /
|
|
// ReadTextureImageTight), so the two forms cannot disagree about what a pixel is; only the
|
|
// destination moved. The PACK layout is the client's arithmetic (ReadbackLayout) carried in
|
|
// the record, and the byte swap is applied to the tight bytes first, exactly where the client
|
|
// applied it to its reply.
|
|
//
|
|
// A FAILED READ IS LOUD. The reply form's failures reach the client as ERROR / DECLINED and
|
|
// end in a named Fatal there (RequireReadbackReplyComplete: "a blocking readback has no pixels
|
|
// to return"); this form has no reply to carry them, and a pack buffer left holding its old
|
|
// bytes is the same silently wrong answer. So the refusal latches here, under the same family
|
|
// words.
|
|
|
|
// Every number here is the peer's, and they are checked BEFORE the read, so a corrupt record
|
|
// costs no GPU work - and completely enough that LandReadbackInBuffer cannot meet a range it
|
|
// would have to refuse: every run it writes starts at or before the LAST row's offset and is
|
|
// no wider than the tight extent, and both are bounded here by what one MGPSubData can name
|
|
// (MGPipeSetSubDataBufferRange: an offset up to 2^31-1, a width up to 2^32-1). Whether the
|
|
// range fits the buffer is the applier's own gate, the one every buffer write meets.
|
|
static const char* ReadbackToBufferLayoutFault(const MG_Pipe::MGPReadbackToBuffer& record,
|
|
Uint64 bytesPerPixel) {
|
|
const MG_Pipe::MGPBox& box = record.Src.Box;
|
|
const Uint64 rowBytes = static_cast<Uint64>(box.W) * bytesPerPixel;
|
|
const Uint64 tightBytes = rowBytes * box.H * box.D; // the caller proved this fits
|
|
if (record.RowStride < rowBytes) return "the row stride is narrower than a tight row";
|
|
if (box.D > 1 && record.ImageStride < record.RowStride) return "the image stride is narrower than a row";
|
|
if (record.SwapGroup != 0 && record.SwapGroup != 2 && record.SwapGroup != 4 && record.SwapGroup != 8)
|
|
return "the swap group is not a byte group GL_PACK_SWAP_BYTES reverses (2, 4 or 8)";
|
|
constexpr Uint64 kMaxOffset = 0x7FFFFFFFull;
|
|
const Uint64 lastImage = box.D - 1;
|
|
const Uint64 lastRow = box.H - 1;
|
|
if (record.DstBase > kMaxOffset) return "the first row starts past 2^31-1";
|
|
if (lastImage != 0 && record.ImageStride > (kMaxOffset - record.DstBase) / lastImage)
|
|
return "the last image starts past 2^31-1";
|
|
const Uint64 imageBase = record.DstBase + lastImage * record.ImageStride;
|
|
if (lastRow != 0 && record.RowStride > (kMaxOffset - imageBase) / lastRow)
|
|
return "the last row starts past 2^31-1";
|
|
if (tightBytes > 0xFFFFFFFFull) return "the read is wider than one buffer write can name";
|
|
return nullptr;
|
|
}
|
|
|
|
Bool ServerVerbSink::LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight,
|
|
Uint64 bytesPerPixel) {
|
|
const MG_Pipe::MGPBox& box = record.Src.Box;
|
|
const Uint64 rowBytes = static_cast<Uint64>(box.W) * bytesPerPixel;
|
|
const Uint64 rows = box.H;
|
|
const Uint64 images = box.D;
|
|
const Uint64 tightBytes = rowBytes * rows * images;
|
|
if (record.SwapGroup != 0) {
|
|
const Uint64 group = record.SwapGroup;
|
|
for (Uint64 at = 0; at + group <= tightBytes; at += group) {
|
|
for (Uint64 i = 0; i < group / 2; ++i) std::swap(tight[at + i], tight[at + group - 1 - i]);
|
|
}
|
|
}
|
|
|
|
// ONE WRITE PER CONTIGUOUS RUN: the whole read when the layout is tight (the common
|
|
// case - a neutral pack state, or an image-packed read of whole images), one per image
|
|
// when only the rows are tight, one per row otherwise. The bytes between rows are the
|
|
// application's (GL_PACK_ROW_LENGTH / ALIGNMENT padding) and are never written.
|
|
const Bool rowsTight = record.RowStride == rowBytes;
|
|
const Bool imagesTight = images == 1 || record.ImageStride == rowBytes * rows;
|
|
const Uint64 runBytes = rowsTight ? (imagesTight ? tightBytes : rowBytes * rows) : rowBytes;
|
|
const Uint64 runsPerImage = rowsTight ? 1 : rows;
|
|
const Uint64 imageRuns = rowsTight && imagesTight ? 1 : images;
|
|
for (Uint64 z = 0; z < imageRuns; ++z) {
|
|
for (Uint64 r = 0; r < runsPerImage; ++r) {
|
|
// No overflow and no refusal below: ReadbackToBufferLayoutFault bounded the last
|
|
// offset and the widest run before the read.
|
|
const Uint64 offset = record.DstBase + z * record.ImageStride + r * record.RowStride;
|
|
MG_Pipe::MGPSubData write{};
|
|
write.Res = record.Dst;
|
|
write.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
|
|
// HasLiveHostWrites stays 0 and that is the client's fact, not a default: the
|
|
// client takes this form only for a pack buffer it does not map
|
|
// (TextureReadbackEmit.inc, PackBufferReadbackTarget), so "no live host writer" is
|
|
// what a glBufferSubData of the same bytes would have said.
|
|
if (!MG_Pipe::MGPipeSetSubDataBufferRange(write, offset, runBytes)) return false;
|
|
const Uint8* bytes = tight + (z * runsPerImage + r) * runBytes;
|
|
++m_bufferReadbackWrites;
|
|
// The acceptance is the applier's (a dead handle is a counted no-op, a range
|
|
// past the store a trip wire), so a refused write ends the landing here.
|
|
if (!MG_Pipe::MGPipeApplyResourceSubData(write, bytes, nullptr)) return false;
|
|
}
|
|
}
|
|
++m_bufferReadbacks;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) {
|
|
const MG_Pipe::MGPReadbackInfo& info = record.Src;
|
|
const Uint64 bpp = static_cast<Uint64>(MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type))));
|
|
// The reply form's shape checks and its PH-3 bound: the scratch is sized from the record,
|
|
// so the SERVER's own maxReplyBytes caps it exactly as it caps an answer - the client bands
|
|
// this form by the same number (PlanReadbackBands), so no correct client is refused by it.
|
|
// ONE latch site for every shape fault (PeerLatchTest's row names it once).
|
|
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
|
|
const char* fault = nullptr;
|
|
Uint64 tight = 0;
|
|
if (bpp == 0 || info.Box.W == 0 || info.Box.H == 0 || info.Box.D != 1 ||
|
|
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max())) {
|
|
fault = "the read box or its format is invalid";
|
|
} else if (static_cast<Uint64>(info.Box.W) > kUint64Max / info.Box.H ||
|
|
static_cast<Uint64>(info.Box.W) * info.Box.H > kUint64Max / bpp) {
|
|
fault = "the tight extent overflows";
|
|
} else {
|
|
tight = static_cast<Uint64>(info.Box.W) * info.Box.H * bpp;
|
|
if (m_maxReplyBytes == 0 || tight > m_maxReplyBytes ||
|
|
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
|
|
fault = "the tight extent is past this server's maxReplyBytes (PH-3)";
|
|
} else if (info.DstOffset != 0 || info.DstSize != tight) {
|
|
fault = "Src.DstOffset / Src.DstSize are not 0 / the tight extent";
|
|
} else {
|
|
fault = ReadbackToBufferLayoutFault(record, bpp);
|
|
}
|
|
}
|
|
if (fault != nullptr) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ReadPixelsToBuffer.shape\"} %s", fault);
|
|
}
|
|
if (!ReadBoundFramebufferTight(info, tight)) {
|
|
return SessionLatch(MGFatalFamily::ReadbackDeclined, "MGPipe: Fatal{ReadbackDeclined, \"ReadPixelsToBuffer\"} - the server "
|
|
"has no GL.ReadPixels; the pack buffer would keep bytes the application is "
|
|
"about to read as pixels, and there is no reply to carry the decline");
|
|
}
|
|
return LandReadbackInBuffer(record, m_readbackScratch.data(), bpp);
|
|
}
|
|
|
|
Bool ServerVerbSink::OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) {
|
|
const MG_Pipe::MGPReadbackInfo& info = record.Src;
|
|
const Uint64 bpp = static_cast<Uint64>(MG_Util::GetInputBytesPerPixel(
|
|
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)));
|
|
const char* fault = nullptr;
|
|
Uint64 tight = 0;
|
|
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
|
|
fault = "the image extent is not a whole level's";
|
|
} else {
|
|
tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
|
|
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D) {
|
|
fault = "the tight extent overflows";
|
|
} else if (info.DstOffset != 0 || info.DstSize != tight) {
|
|
fault = "Src.DstOffset / Src.DstSize are not 0 / the tight extent";
|
|
} else {
|
|
fault = ReadbackToBufferLayoutFault(record, bpp);
|
|
}
|
|
}
|
|
if (fault != nullptr) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"GetTextureImageToBuffer.shape\"} %s", fault);
|
|
}
|
|
Vector<Uint8> bytes;
|
|
if (!ReadTextureImageTight(info, bytes) || bytes.size() != tight) {
|
|
return SessionLatch(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"GetTextureImageToBuffer\"} - the level "
|
|
"could not be read (%llu of %llu bytes); the reply form answers ERROR here, and "
|
|
"this form has no reply to carry it",
|
|
static_cast<unsigned long long>(bytes.size()),
|
|
static_cast<unsigned long long>(tight));
|
|
}
|
|
return LandReadbackInBuffer(record, bytes.data(), bpp);
|
|
}
|
|
|
|
// P8-F: A PROTOCOL GUARD, NOT AN UNMIGRATED VERB. This was P5b's server-side stub shape
|
|
// (`Fatal{UnmigratedVerb, ...}`, "ServerVerbSink has no body for it yet"), but no package is
|
|
// going to give these two multi-draw shapes a body, because our client can no longer send
|
|
// either: client index arrays become one owned element buffer before the record is built
|
|
// (EmitTables.cpp's multi-draw arm, OwnedDrawInputs.h clears kDrawHasUserIndices), and no GL
|
|
// entry point produces an instanced multi-draw. A record that carries one came from a
|
|
// foreign peer, so the family is the one every other malformed-record check uses.
|
|
//
|
|
// PH-1 (3): the shape is the PEER's (a multi-draw record it chose to send), so in an armed
|
|
// session child the refusal latches and the verb returns false; unarmed it still dies.
|
|
static Bool MultiDrawShapeLatch(const char* slot) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} a multi-draw "
|
|
"record with a shape this protocol does not carry: client indices travel as an owned "
|
|
"element buffer and no GL entry point produces an instanced multi-draw",
|
|
slot);
|
|
}
|
|
|
|
// P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record
|
|
// carries the GL call verbatim (rule D) and this body reproduces the backend call the
|
|
// monolith makes for that shape, reading only the record and the backend's own
|
|
// barrier-pulled state; the twenty GL entry points collapse onto the arms below:
|
|
//
|
|
// kDrawIsIndirect arrays: DrawArraysIndirect (DrawCount 1, Stride 0) /
|
|
// MultiDrawArraysIndirect / MultiDrawArraysIndirectCount (a parameter
|
|
// buffer named); indexed: the three Elements twins
|
|
// NumDraws != 1 MultiDrawArrays / MultiDrawElementsBaseVertex, the two arrays
|
|
// rebuilt from the ranges into bounded locals (rule C)
|
|
// arrays, one range DrawArrays / DrawArraysInstanced / DrawArraysInstancedBaseInstance
|
|
// indexed, one range DrawElementsBaseVertex (the P5 arm, unchanged, bias 0 for a plain
|
|
// DrawElements) / DrawRangeElements[BaseVertex] under
|
|
// kDrawHasIndexRange / the four DrawElementsInstanced* by whether a
|
|
// base vertex and a base instance are non-zero
|
|
// kDrawHasUserIndices the `indices` argument is the resolved SEG_STAGE run instead of an
|
|
// element-buffer offset (the client staged a client index array)
|
|
//
|
|
// "Instanced" is InstanceCount != 1 || StartInstance != 0: an instanced call with a count
|
|
// of 1 and no base instance is the plain draw it is equivalent to, and a count of 0 must
|
|
// NOT collapse onto the plain draw (it draws nothing, the plain draw would draw once).
|
|
//
|
|
// What is still refused by name, as protocol corruption (P8-F): a multi-draw that arrived
|
|
// with a span (our client stages client indices into an owned element buffer and never sets
|
|
// kDrawHasUserIndices) and a multi-draw that claims instancing (no GL entry point produces
|
|
// one; the client never sends it). Only a foreign peer reaches either.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
namespace {
|
|
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST
|
|
// COVER [0, MaxTouchedTextureUnit], AND THIS IS WHERE THAT PROMISE IS CHECKED.
|
|
//
|
|
// Everything the texture and sampler families do per draw now reads
|
|
// [SamplerViewStart, +SamplerViewCount) and [SamplerStateStart, +SamplerStateCount).
|
|
// Under run-ahead a window NARROWER than the frontend's high-water mark silently drops a
|
|
// sync of a texture the draw is about to sample, or leaves an earlier draw's sampler
|
|
// object on a unit - and a decline is exactly what run-ahead cannot take, because the
|
|
// client has already moved on and there is no wait in which to notice.
|
|
//
|
|
// A SERVER-SIDE RE-DERIVATION IS REFUSED, and that is the ruling's point: the client owns
|
|
// the high-water mark (it is the `count` argument SamplerEmit.h passes, Start=0 /
|
|
// Count=maxTouched+1) and the server's job is to check the promise, not to invent a
|
|
// second authority for it. So this is a CHECK and its failure is corruption.
|
|
//
|
|
// WHERE THE MARK COMES FROM: MGPContextValues::MaxTouchedTextureUnit, applied by
|
|
// set_context_values, which precedes the verb on the ring - the same ordering §2.1's
|
|
// XFB clause leans on. It is RECORD_SUPPLIED, so reading it is reading what a record
|
|
// put there and not client memory (rule F).
|
|
//
|
|
// WIDER IS FINE. A window larger than the mark costs a walk over provably-empty units;
|
|
// only SMALLER is unrepresentable. An empty window on a draw that touches no unit at all
|
|
// (mark 0 with nothing ever touched) is the correct and only possible emission, so a
|
|
// count of 0 is admitted exactly while the applied counters say no unit was touched.
|
|
//
|
|
// THE ONE ADMITTED SILENCE, and it is the A/B rather than a hole: a client whose sampler
|
|
// subsystem bit is CLEAR emits neither record, the backend then runs its pre-handle
|
|
// frontend walk, and there is no window to check because there is no window. That is
|
|
// "both counts are still 0", and it is distinguishable from the narrowing this refuses
|
|
// (a narrowed window carries a non-zero count that is merely too small). The moment
|
|
// either set has been received, the rule binds.
|
|
//
|
|
// PH-1 (3): both windows are the PEER's records, so the refusals latch in an armed
|
|
// session child (false = latched, the verb declines); unarmed they still die.
|
|
Bool CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
|
|
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return true;
|
|
const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit();
|
|
if (maxTouched < 0) return true;
|
|
const Uint32 required = static_cast<Uint32>(maxTouched) + 1u;
|
|
if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with "
|
|
"units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u "
|
|
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops "
|
|
"the sync of at least one texture this draw samples",
|
|
verb, static_cast<int>(maxTouched), required, st.SamplerViewStart,
|
|
st.SamplerViewCount);
|
|
}
|
|
if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) {
|
|
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with "
|
|
"units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u "
|
|
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves "
|
|
"an earlier draw's sampler object on at least one unit",
|
|
verb, static_cast<int>(maxTouched), required, st.SamplerStateStart,
|
|
st.SamplerStateCount);
|
|
}
|
|
return true;
|
|
}
|
|
} // namespace
|
|
#endif
|
|
|
|
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
|
|
const MG_Pipe::MGPDrawRange* ranges,
|
|
const MG_Pipe::MGHostSpan* userIndices,
|
|
const MG_Pipe::MGPDrawIndirect* indirect) {
|
|
if (userIndices != nullptr && !Wire::CheckDrawUserIndices(info, ranges, *userIndices)) {
|
|
return false; // PH-1 (3): latched (armed session child); unarmed it died inside
|
|
}
|
|
// The witness first, before the backend is consulted, so a unit process with no
|
|
// backend object still sees the wire's fields (PipeApplier.h LastDraw).
|
|
m_lastDraw = LastDrawRecord{};
|
|
m_lastDraw.Info = info;
|
|
if (ranges != nullptr && info.NumDraws != 0) m_lastDraw.FirstRange = ranges[0];
|
|
if (userIndices != nullptr) {
|
|
m_lastDraw.HadUserIndices = true;
|
|
m_lastDraw.UserIndexBytes = userIndices->Size;
|
|
}
|
|
if (indirect != nullptr) {
|
|
m_lastDraw.HadIndirect = true;
|
|
m_lastDraw.Indirect = *indirect;
|
|
}
|
|
++m_drawRecords;
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is
|
|
// asked to resolve anything out of the windows.
|
|
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo")) return false;
|
|
#endif
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
const auto mode = static_cast<GLenum>(info.Mode);
|
|
|
|
GLenum indexType = 0;
|
|
switch (info.IndexSize) {
|
|
case 0: break; // arrays
|
|
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) and nothing else is a
|
|
// legal width; defaulting to 4 would read past the element buffer.
|
|
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
|
|
}
|
|
|
|
// ---- the indirect family: the block is the whole description --------------------
|
|
if (indirect != nullptr) {
|
|
// The layout already refused a record that sets both flags or declares ranges
|
|
// beside the block, so NumDraws is 0 and there is no span here.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.5): the command/parameter buffer handles cross as the
|
|
// verb's own state; the backend's indirect arm resolves the buffer twins from
|
|
// them instead of reading the client's GL_DRAW_INDIRECT_BUFFER binding slot.
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbIndirectBuffer = indirect->Buffer;
|
|
applierState.VerbIndirectParameterBuffer = indirect->ParameterBuffer;
|
|
#endif
|
|
const auto offset = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(indirect->Offset));
|
|
const auto drawCount = static_cast<GLsizei>(indirect->DrawCount);
|
|
const auto stride = static_cast<GLsizei>(indirect->Stride);
|
|
const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer);
|
|
const auto parameterOffset = static_cast<GLintptr>(indirect->ParameterOffset);
|
|
// glMultiDraw*Indirect with drawcount 1 and stride 0 IS glDraw*Indirect by GL's own
|
|
// definition, so the single-draw entry point is the one the monolith reaches for
|
|
// the single-draw call and nothing is lost for the multi-draw spelling of it.
|
|
const Bool single = !counted && drawCount == 1 && stride == 0;
|
|
if (info.IndexSize == 0) {
|
|
if (counted) {
|
|
if (gl.MultiDrawArraysIndirectCount == nullptr) return false;
|
|
gl.MultiDrawArraysIndirectCount(mode, offset, parameterOffset, drawCount, stride);
|
|
} else if (single) {
|
|
if (gl.DrawArraysIndirect == nullptr) return false;
|
|
gl.DrawArraysIndirect(mode, offset);
|
|
} else {
|
|
if (gl.MultiDrawArraysIndirect == nullptr) return false;
|
|
gl.MultiDrawArraysIndirect(mode, offset, drawCount, stride);
|
|
}
|
|
} else {
|
|
if (counted) {
|
|
if (gl.MultiDrawElementsIndirectCount == nullptr) return false;
|
|
gl.MultiDrawElementsIndirectCount(mode, indexType, offset, parameterOffset,
|
|
drawCount, stride);
|
|
} else if (single) {
|
|
if (gl.DrawElementsIndirect == nullptr) return false;
|
|
gl.DrawElementsIndirect(mode, indexType, offset);
|
|
} else {
|
|
if (gl.MultiDrawElementsIndirect == nullptr) return false;
|
|
gl.MultiDrawElementsIndirect(mode, indexType, offset, drawCount, stride);
|
|
}
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
if (ranges == nullptr || info.NumDraws == 0) return false;
|
|
const Bool instanced = info.InstanceCount != 1 || info.StartInstance != 0;
|
|
const auto instanceCount = static_cast<GLsizei>(info.InstanceCount);
|
|
const GLuint baseInstance = info.StartInstance;
|
|
|
|
// ---- the multi-draws: the two arrays rebuilt from the ranges (rule C) --------------
|
|
if (info.NumDraws != 1) {
|
|
if (userIndices != nullptr) {
|
|
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
|
|
: "MultiDrawElements+CLIENT_INDICES");
|
|
}
|
|
if (instanced) {
|
|
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
|
|
: "MultiDrawElements+INSTANCED");
|
|
}
|
|
const auto n = static_cast<SizeT>(info.NumDraws);
|
|
m_multiCounts.resize(n);
|
|
if (info.IndexSize == 0) {
|
|
if (gl.MultiDrawArrays == nullptr) return false;
|
|
m_multiFirsts.resize(n);
|
|
for (SizeT i = 0; i < n; ++i) {
|
|
m_multiFirsts[i] = static_cast<GLint>(ranges[i].Start);
|
|
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
|
|
}
|
|
gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(),
|
|
static_cast<GLsizei>(n));
|
|
} else {
|
|
if (gl.MultiDrawElementsBaseVertex == nullptr) return false;
|
|
m_multiOffsets.resize(n);
|
|
m_multiBaseVertices.resize(n);
|
|
for (SizeT i = 0; i < n; ++i) {
|
|
m_multiOffsets[i] = reinterpret_cast<const void*>(
|
|
static_cast<std::uintptr_t>(ranges[i].Start) * info.IndexSize);
|
|
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
|
|
m_multiBaseVertices[i] = ranges[i].IndexBias;
|
|
}
|
|
// MultiDrawElements is the same call with every base vertex 0, which is what
|
|
// its ranges carry (CONTRACT-P5B.md d1's MultiDrawElements row).
|
|
gl.MultiDrawElementsBaseVertex(mode, m_multiCounts.data(), indexType,
|
|
m_multiOffsets.data(), static_cast<GLsizei>(n),
|
|
m_multiBaseVertices.data());
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
// ---- one range ----------------------------------------------------------------------
|
|
const MG_Pipe::MGPDrawRange& range = ranges[0];
|
|
const auto count = static_cast<GLsizei>(range.Count);
|
|
if (info.IndexSize == 0) {
|
|
if (!instanced) {
|
|
if (gl.DrawArrays == nullptr) return false;
|
|
gl.DrawArrays(mode, static_cast<GLint>(range.Start), count);
|
|
} else if (baseInstance != 0) {
|
|
if (gl.DrawArraysInstancedBaseInstance == nullptr) return false;
|
|
gl.DrawArraysInstancedBaseInstance(mode, static_cast<GLint>(range.Start), count,
|
|
instanceCount, baseInstance);
|
|
} else {
|
|
if (gl.DrawArraysInstanced == nullptr) return false;
|
|
gl.DrawArraysInstanced(mode, static_cast<GLint>(range.Start), count, instanceCount);
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
// Indexed. `indices` is the element-buffer byte offset Start * IndexSize - the same
|
|
// arithmetic PipeFill's emitter inverted - or, for a client index array, the staged run
|
|
// resolved through the process resolver the server installed (rule C: the pointer is
|
|
// used for this call only). The codec proved the span lies inside SEG_STAGE (all four
|
|
// honesty arms), so a null here is the resolver missing, which is a wiring fault.
|
|
const void* indices = nullptr;
|
|
if (userIndices != nullptr) {
|
|
indices = MG_Pipe::MGPipeHostBytes(*userIndices);
|
|
if (indices == nullptr) {
|
|
Wire::WireProtocolFatal("DrawVbo.userIndices",
|
|
"the user-index span passed the codec's four honesty arms "
|
|
"but MGPipeHostBytes resolved it to null; the server's "
|
|
"segment resolver is not installed");
|
|
}
|
|
} else {
|
|
indices = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(range.Start) *
|
|
info.IndexSize);
|
|
}
|
|
const GLint baseVertex = range.IndexBias;
|
|
const Bool ranged = (info.Flags & MG_Pipe::kDrawHasIndexRange) != 0;
|
|
// A BASE VERTEX OF 0 IS THE PLAIN ENTRY POINT, NOT THE BaseVertex ONE WITH A 0 - d1's one
|
|
// ruling against CONTRACT-P5B.md's "DrawElementsBaseVertex(..., 0), the P5 arm,
|
|
// unchanged". Espryt's DrawElementsBaseVertex slot calls glDrawElementsBaseVertex, which
|
|
// is ES 3.2 / OES_draw_elements_base_vertex and is NOT loaded on an ES 3.1 provider
|
|
// (ANGLE on D3D11: "Failed to load GLES function: glDrawElementsBaseVertex" at bring-up),
|
|
// so the P5 arm dereferenced a null function pointer on every plain glDrawElements - the
|
|
// one call every Minecraft frame is made of - while the monolith, which calls
|
|
// GL.DrawElements for glDrawElements, was fine. Rule D says the sink reproduces the call
|
|
// the monolith makes; this is that, for all three indexed families.
|
|
if (!instanced) {
|
|
if (ranged) {
|
|
if (baseVertex != 0) {
|
|
if (gl.DrawRangeElementsBaseVertex == nullptr) return false;
|
|
gl.DrawRangeElementsBaseVertex(mode, info.MinIndex, info.MaxIndex, count, indexType,
|
|
indices, baseVertex);
|
|
} else {
|
|
if (gl.DrawRangeElements == nullptr) return false;
|
|
gl.DrawRangeElements(mode, info.MinIndex, info.MaxIndex, count, indexType, indices);
|
|
}
|
|
} else if (baseVertex != 0) {
|
|
if (gl.DrawElementsBaseVertex == nullptr) return false;
|
|
gl.DrawElementsBaseVertex(mode, count, indexType, indices, baseVertex);
|
|
} else {
|
|
if (gl.DrawElements == nullptr) return false;
|
|
gl.DrawElements(mode, count, indexType, indices);
|
|
}
|
|
} else if (baseInstance != 0) {
|
|
if (baseVertex != 0) {
|
|
if (gl.DrawElementsInstancedBaseVertexBaseInstance == nullptr) return false;
|
|
gl.DrawElementsInstancedBaseVertexBaseInstance(mode, count, indexType, indices,
|
|
instanceCount, baseVertex, baseInstance);
|
|
} else {
|
|
if (gl.DrawElementsInstancedBaseInstance == nullptr) return false;
|
|
gl.DrawElementsInstancedBaseInstance(mode, count, indexType, indices, instanceCount,
|
|
baseInstance);
|
|
}
|
|
} else if (baseVertex != 0) {
|
|
if (gl.DrawElementsInstancedBaseVertex == nullptr) return false;
|
|
gl.DrawElementsInstancedBaseVertex(mode, count, indexType, indices, instanceCount, baseVertex);
|
|
} else {
|
|
if (gl.DrawElementsInstanced == nullptr) return false;
|
|
gl.DrawElementsInstanced(mode, count, indexType, indices, instanceCount);
|
|
}
|
|
++m_draws;
|
|
return true;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// P5b: the stubs the four migration packages replace (MG_Remote/CONTRACT-P5B.md).
|
|
//
|
|
// Each dies by the GL slot's own name. The record has crossed and been validated by the
|
|
// codec by the time one of these runs, so the only thing missing is the backend call, and
|
|
// the package that owns the row writes it here: `Table("<row>")`, the null-slot check
|
|
// (a backend that leaves the slot null DECLINES, which is the monolith's null-slot answer
|
|
// in the same words), the call, and a tally the lane can assert moved.
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// ---- i1 ---- (MG_Remote/CONTRACT-P5B.md §2 i1; landed by package p5b/i1)
|
|
//
|
|
// RULE D IN FIVE BODIES. Each reproduces the backend call the monolith makes, from the
|
|
// record and from server state, and NOTHING ELSE: the backend goes on reading the frontend
|
|
// fields it reads today through the BARRIER-PULLED entries of its verb class, which the
|
|
// verb stamp PipeApplier::ApplyOne put up before this sink ran is exactly what makes legal.
|
|
// That is why none of these touches a backend file and why the monolith path is byte
|
|
// identical - and it is also the honest statement of the debt, which `rsp` counts.
|
|
|
|
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does.
|
|
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid")) return false;
|
|
#endif
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
// The compute program is NOT named by THIS record and must not be - but it IS named, by
|
|
// the set_dispatch_program record that preceded it, and the backend's PrepareForCompute
|
|
// reads MGPipeApplier().DispatchProgram from there.
|
|
//
|
|
// P5e (pa): that is what this comment used to get wrong. Until P5e the backend PULLED
|
|
// GetProgramForDispatch - GetProgramForDraw's twin - inside PrepareForCompute, and the
|
|
// field's FATAL -> BARRIER_PULLED move in FieldOwnership.def (contract §6.9, i1's one
|
|
// granted row) is what made the pull legal. pg gave the sync and the link/SPIR-V gate a
|
|
// handle arm and pa retired the pull itself, so on the handle arm the row is not read at
|
|
// all; the BARRIER_PULLED class stays because the monolith arm still reads it (ID-81).
|
|
// Block* are 0 on the wire for the unchanged reason: the local size is a link artifact
|
|
// the backend reads from the program it resolved, which on that arm is the record's
|
|
// archive.
|
|
if (grid.IsIndirect != 0) {
|
|
if (gl.DispatchComputeIndirect == nullptr) return false;
|
|
// IndirectBuffer travels for P7's sake; the BINDING is server state, put there by
|
|
// the set_buffer_bindings record that preceded this one, exactly as OnClear's Fbo
|
|
// is not re-resolved here. glDispatchComputeIndirect takes only the offset.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.5): the buffer handle itself is now also the verb's own
|
|
// state, so the backend's dispatch-indirect arm resolves the twin from the record
|
|
// rather than from the client's GL_DISPATCH_INDIRECT_BUFFER binding slot.
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbDispatchIndirectBuffer = grid.IndirectBuffer;
|
|
#endif
|
|
gl.DispatchComputeIndirect(static_cast<GLintptr>(grid.IndirectOffset));
|
|
} else {
|
|
if (gl.DispatchCompute == nullptr) return false;
|
|
gl.DispatchCompute(static_cast<GLuint>(grid.GridX), static_cast<GLuint>(grid.GridY),
|
|
static_cast<GLuint>(grid.GridZ));
|
|
}
|
|
++m_dispatches;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("memory_barrier");
|
|
if (table == nullptr) return false;
|
|
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
|
// THE BITS GO OVER VERBATIM AND ARE LOWERED HERE BY NOBODY. Espryt's atomic-counter
|
|
// lowering - the counter bit implying the storage bit, because glslang lowers every
|
|
// atomic_uint onto a storage block - lives inside its own MemoryBarrier
|
|
// (DirectGLES.cpp:8837) and is a statement about the DRIVER. Repeating it on this side
|
|
// would make the split arm and the monolith arm two different calls.
|
|
if (barrier.ByRegion != 0) {
|
|
if (gl.MemoryBarrierByRegion == nullptr) return false;
|
|
gl.MemoryBarrierByRegion(static_cast<GLbitfield>(barrier.Bits));
|
|
} else {
|
|
if (gl.MemoryBarrier == nullptr) return false;
|
|
gl.MemoryBarrier(static_cast<GLbitfield>(barrier.Bits));
|
|
}
|
|
++m_memoryBarriers;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resource_copy_region");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.CopyImageSubData == nullptr) return false;
|
|
|
|
// Typed handles resolve both texture and renderbuffer storage on the server.
|
|
MG_Backend::CopyImageEndpoint src{};
|
|
MG_Backend::CopyImageEndpoint dst{};
|
|
if (copy.SrcTarget == GL_RENDERBUFFER) src.RenderbufferHandle = copy.Src;
|
|
else src.TextureHandle = copy.Src;
|
|
if (copy.DstTarget == GL_RENDERBUFFER) dst.RenderbufferHandle = copy.Dst;
|
|
else dst.TextureHandle = copy.Dst;
|
|
if (MG_Pipe::MGPipeHandleIsNull(copy.Src) || MG_Pipe::MGPipeHandleIsNull(copy.Dst)) {
|
|
// The monolith's own answer to this, in its own words (DirectGLES.cpp:9067
|
|
// "source or destination image failed to sync; declining the copy"): the frontend
|
|
// validator is what keeps it unreachable and what reports the INVALID_VALUE the
|
|
// application is owed. A decline here is a real answer, not a silent success.
|
|
MGLOG_E_ONCE("MG_Remote server: resource_copy_region named texture(s) %u -> %u that "
|
|
"the frontend no longer holds; declining the copy",
|
|
static_cast<unsigned>(copy.SrcGlName),
|
|
static_cast<unsigned>(copy.DstGlName));
|
|
return false;
|
|
}
|
|
|
|
// SrcBox is {origin, extent} and the extent is the copy's, spelled once by GL for both
|
|
// endpoints; the destination contributes only its origin.
|
|
table->GL.CopyImageSubData(src, static_cast<GLenum>(copy.SrcTarget),
|
|
static_cast<GLint>(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y,
|
|
copy.SrcBox.Z, dst, static_cast<GLenum>(copy.DstTarget),
|
|
static_cast<GLint>(copy.DstLevel), copy.DstX, copy.DstY,
|
|
copy.DstZ, static_cast<GLsizei>(copy.SrcBox.W),
|
|
static_cast<GLsizei>(copy.SrcBox.H),
|
|
static_cast<GLsizei>(copy.SrcBox.D));
|
|
++m_imageCopies;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_shader_image");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.BindImageTexture == nullptr) return false;
|
|
// THE SAME CALL IS RIGHT FOR BOTH BACKENDS, which is why the record carries the whole
|
|
// argument list although neither reads all of it today: Espryt ignores everything but
|
|
// Unit and syncs that unit from the barrier-pulled GetImageTextureBinding
|
|
// (DirectGLES.cpp:9154, :2471), and Magma's slot is a no-op (DirectVulkan.cpp:665). The
|
|
// arguments travel because rule D says a verb crosses as the CALL, and because P7 is
|
|
// what makes the backend read them instead of pulling.
|
|
table->GL.BindImageTexture(static_cast<GLuint>(bind.Unit), static_cast<GLuint>(bind.GlName),
|
|
static_cast<GLint>(bind.Level),
|
|
bind.Layered != 0 ? GL_TRUE : GL_FALSE,
|
|
static_cast<GLint>(bind.Layer),
|
|
static_cast<GLenum>(bind.Access),
|
|
static_cast<GLenum>(bind.Format));
|
|
++m_imageBinds;
|
|
// P5c (rv): an image bind moves the frontend's texture bind generation, and this verb
|
|
// carries no set_shader_images alongside it - so the server-side shutter serial the
|
|
// accessors now answer with (CONTRACT-P5C.md §5.3) moves here, at the one place the
|
|
// event reaches the applier's side.
|
|
MG_Pipe::MGPipeApplierNoteTextureStateMoved();
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
|
|
const char* name) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("set_storage_block_binding");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.ShaderStorageBlockBinding == nullptr) return false;
|
|
if (name == nullptr) return false;
|
|
// The NAME is the one coordinate the application, the frontend and both backends agree
|
|
// on (BackendObject.h:216-221), which is why the row carries a blob rather than the
|
|
// application's block INDEX. `name` points into the decoder's bounded local and is
|
|
// valid for this call only (rule C); the backend slot copies what it needs.
|
|
//
|
|
// P5f fe: publish the record's program handle for the backend consumer. Espryt
|
|
// resolves its server twin directly; Magma's consumer is the fm package's seam.
|
|
MG_Pipe::MGPipeApplier().ClearVerbHandles();
|
|
MG_Pipe::MGPipeApplier().VerbStorageBlockProgram = binding.ShaderCso;
|
|
table->GL.ShaderStorageBlockBinding(static_cast<GLuint>(binding.GlName), name,
|
|
static_cast<GLuint>(binding.Binding));
|
|
++m_storageBlockBindings;
|
|
return true;
|
|
}
|
|
|
|
// ---- t2 ---- (MG_Remote/CONTRACT-P5B.md §2 t2)
|
|
//
|
|
// SIX BODIES, SIX BACKEND CALLS, NO STATE OF THEIR OWN. Rule D: the record IS the call, and
|
|
// everything the backend reads around it - the capture program, the capture-buffer
|
|
// bindings, the bound XFB object, the patch state - it reads from gPipeInputs through its
|
|
// verb class's BARRIER-PULLED fields, which the client filled at the call site and the
|
|
// verb barrier holds still (R-1). That is why none of these touches m_backend beyond
|
|
// Table() and why not one of them caches anything across records.
|
|
//
|
|
// A NULL SLOT DECLINES, AND THE DECLINE IS THE MONOLITH'S ANSWER IN THE SAME WORDS. Magma
|
|
// (DirectVulkan) registers NO XFB slot and no PatchParameteri at all
|
|
// (BackendObject_DirectVulkan.cpp), and under monolith the frontend's own
|
|
// `if (const auto f = table.GL.X)` guard simply skips the call; `return false` here is that
|
|
// same skip, reported to DecodeAndApply as "this build did not apply it" rather than as a
|
|
// crash or as a silent success. Contract §2 t2 says so for PatchParameteri by name.
|
|
|
|
Bool ServerVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) {
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
state.BoundStreamOutputLifetimeId = begin.LifetimeId;
|
|
state.StreamOutputSpans[begin.LifetimeId] = begin;
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("begin_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.BeginTransformFeedback == nullptr) return false;
|
|
// Espryt's Begin only ARMS the span (DirectGLES.cpp:1212-1220: primitiveMode, pending,
|
|
// targets cleared); the driver glBeginTransformFeedback happens in the tail of the next
|
|
// PrepareForDraw (StartPendingTransformFeedback, :1224), where the capture program and
|
|
// the buffer bindings are read through the pulls. So this record's effect is not
|
|
// visible until a DRAW crosses - which is why an XFB scenario whose draw is still class
|
|
// C moves its first blocker to that draw rather than rendering.
|
|
table->GL.BeginTransformFeedback(static_cast<GLenum>(begin.PrimitiveMode));
|
|
++m_streamOutputSpans;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("end_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.EndTransformFeedback == nullptr) {
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
state.StreamOutputSpans.erase(state.BoundStreamOutputLifetimeId);
|
|
return false;
|
|
}
|
|
// THE THREE ACCOUNTING FIELDS ARE NOT READ, AND THAT IS THE RULING RATHER THAN AN
|
|
// OMISSION. glEndTransformFeedback takes no arguments; the numbers are the CLIENT's own
|
|
// per-span accounting (contract §2 t2's companions row) and the client is where they are
|
|
// consumed - by the primitive queries and by the capture-capacity clamp. A server that
|
|
// second-guessed them from its own driver would be publishing a second answer to a
|
|
// question the frontend already answers, and the second answer is the one that goes
|
|
// stale. They cross because the row has carried them since P4a and because P9's
|
|
// server-side scatter is what will need them.
|
|
(void)accounting;
|
|
table->GL.EndTransformFeedback();
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
state.StreamOutputSpans.erase(state.BoundStreamOutputLifetimeId);
|
|
++m_streamOutputSpans;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("pause_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.PauseTransformFeedback == nullptr) return false;
|
|
(void)control; // Reserved, and the contract says it is 0.
|
|
table->GL.PauseTransformFeedback();
|
|
++m_streamOutputControls;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resume_stream_output");
|
|
if (table == nullptr) return false;
|
|
if (table->GL.ResumeTransformFeedback == nullptr) return false;
|
|
(void)control;
|
|
table->GL.ResumeTransformFeedback();
|
|
++m_streamOutputControls;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output");
|
|
if (table == nullptr) return false;
|
|
// THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver
|
|
// objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates
|
|
// the ES object on first bind; passing the lifetime id would index a map that has never
|
|
// heard of it and silently create a second driver object per bind. The lifetime id
|
|
// identifies the P5f capture snapshot on this side. Keeping it separate preserves
|
|
// the backend GL-name argument while Begin/End find the right server-owned span.
|
|
MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId = bind.LifetimeId;
|
|
if (table->GL.BindTransformFeedback) table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
|
|
++m_streamOutputBinds;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) {
|
|
const auto* table = Table("DeleteTransformFeedback");
|
|
if (!table) return false;
|
|
if (!object.GlName || object.Pad0)
|
|
Wire::WireProtocolFatal("DeleteStreamOutput.name", "default object or reserved field");
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
if (object.LifetimeId) {
|
|
state.StreamOutputSpans.erase(object.LifetimeId);
|
|
if (state.BoundStreamOutputLifetimeId == object.LifetimeId) state.BoundStreamOutputLifetimeId = 0;
|
|
}
|
|
state.VerbDeleteStreamOutputLifetimeId = object.LifetimeId;
|
|
if (table->GL.DeleteTransformFeedback) table->GL.DeleteTransformFeedback(object.GlName);
|
|
state.VerbDeleteStreamOutputLifetimeId = 0;
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
|
|
const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter");
|
|
if (table == nullptr) return false;
|
|
// Magma registers no PatchParameteri: it compiles the patch size into its synthesized
|
|
// control stage from set_patch_state instead, so the DECLINE below is the whole of the
|
|
// right answer for that backend and not a gap (contract §2 t2).
|
|
if (table->GL.PatchParameteri == nullptr) return false;
|
|
// Pname is GL_PATCH_VERTICES and the frontend has already rejected every other spelling
|
|
// with INVALID_ENUM before the record was built, so this is a forward and not a switch.
|
|
table->GL.PatchParameteri(static_cast<GLenum>(patch.Pname), static_cast<GLint>(patch.Value));
|
|
++m_patchParameters;
|
|
return true;
|
|
}
|
|
|
|
// ---- f1 ----
|
|
Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) {
|
|
const auto* table = Table("GenerateMipmap");
|
|
if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.2): the texture the client resolved at Target on the active
|
|
// unit crosses as the verb's own state; the backend's mip-descriptor check resolves
|
|
// the record from it instead of probing the client allocator for the bound object's
|
|
// lifetime id (T2's mip half).
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbMipRes = plan.Res;
|
|
applierState.VerbMipBaseLevel = plan.BaseLevel;
|
|
applierState.VerbMipLevelCount = plan.LevelCount;
|
|
#endif
|
|
table->GL.GenerateMipmap(plan.Target);
|
|
return true;
|
|
}
|
|
|
|
Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) {
|
|
const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
|
|
if (table == nullptr) return false;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (hd, CONTRACT-P5C §3.4): the destination texture the client resolved at the
|
|
// active unit crosses as the verb's own state; the backend resolves its twin from the
|
|
// handle instead of reading the client's texture-unit binding slot (T4).
|
|
auto& applierState = MG_Pipe::MGPipeApplier();
|
|
applierState.ClearVerbHandles();
|
|
applierState.VerbCopyTexDst = copy.Dst;
|
|
#endif
|
|
if (copy.SubImage) {
|
|
if (table->GL.CopyTexSubImage2D == nullptr) return false;
|
|
table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset,
|
|
copy.X, copy.Y, copy.Width, copy.Height);
|
|
} else {
|
|
if (table->GL.CopyTexImage2D == nullptr) return false;
|
|
table->GL.CopyTexImage2D(copy.Target, copy.Level, copy.InternalFormat,
|
|
copy.X, copy.Y, copy.Width, copy.Height, 0);
|
|
}
|
|
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
|