Files
MobileGL/MobileGL/MG_Pipe/MGPipeCallbacks.h
T

118 lines
6.9 KiB
C++

// MobileGL - MobileGL/MG_Pipe/MGPipeCallbacks.h
// 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
#pragma once
#include <Includes.h>
#include "MGPipeHandles.h"
#include "MGPipeTypes.h"
// The backend -> frontend reverse channel, named (plan B section 7.1).
//
// Today this traffic is 95 call sites across 17 methods poked directly into frontend
// objects. gallium has no vocabulary for shadow writeback, GPU-write notification or
// default-framebuffer geometry, because in Mesa the state tracker and the driver share an
// address space. Naming them as callbacks is the deliberate deviation (D8).
//
// SEVEN, NOT THE TEN PLAN B WROTE. Three went, each because nothing produces it.
//
// OnTexturePullRequest, the server asking the client to re-send a texture's levels after
// re-minting it (with op 50 ResourceSubDataComplete as the forward terminator). P9 W2 measured
// that no re-mint needs the client: Espryt's server reads the GPU level back and replays its own
// staged store, Magma copies GPU to GPU, on every arm (notes/p9/W2-REMINT.md). So the callback
// went; op 50 stays in the opcode table as a retired row (opcodes only ever grow) and its decoder
// still declines it.
//
// OnXfbScatterReady went with the
// design it belonged to: plan B put the XFB scatter on the CLIENT (the server would hand
// back the packed scratch and the client would run the patch loop over its own shadow), so
// there had to be a callback that told the client the layout. P5c/P5f went the other way -
// the server reads its OWN staged shadow, scatters there and returns the reconciled range
// through OnBufferWriteback, which needs no second callback - and the declaration was left
// behind with zero producers, zero consumers and no EventKind of its own. Deleted here
// (P3b/P4b espryt D1 slice 3) rather than carried: an entry that cannot fire is one every
// reader of this file has to rule out by hand, and the size assertion below made it look
// load-bearing.
//
// And OnLog, deleted in P9 W3 for the same reason: zero producers, zero consumers, no
// EventKind. Plan B pictured the server's log lines as reverse-channel records; they never
// were. They travel the CONTROL plane as LogLine frames (protocol.fbs), written by the server's
// logger through its log forward (MG_Remote/Transport/LogForward.h) - which is also where the
// severity policy this entry's comment promised ("<= WARN lossy, >= ERROR lossless and rate
// limited") now lives, on the level LogLine has always carried.
//
// Installed at context creation. In a monolith these are direct calls; under split they are
// records on the reverse channel, and their ORDER is a correctness requirement rather than
// an optimization (section 7.4).
namespace MobileGL::MG_Pipe {
struct MGPipeCallbacks {
// A driver-detected GL error that only the server could have seen. The text is
// borrowed for this synchronous call; a transport producer copies it before return.
void (*OnGlError)(Uint32 code, const char* message);
// Ranges of a resource the GPU wrote; retires MarkGpuWritten.
void (*OnGpuWritten)(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges);
void (*OnBufferWriteback)(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes);
void (*OnTextureWriteback)(MGPipeHandle res, const MGPBox* box, MGPBlobRef bytes);
// SHAPE ONLY, never bytes: the client owns the CPU shadow and allocates the levels
// itself.
void (*OnMipLevelsGenerated)(MGPipeHandle res, Uint16 base, Uint16 count);
// Retires the layering inversion where the swapchain writes into MG_Impl's
// pDefaultFramebufferInfo.
void (*OnSurfaceChanged)(const MGPSurfaceInfo* info);
void (*OnCapsInvalidated)();
};
// Seven, and the count is asserted so an eighth cannot be added without touching the
// transport's reverse-channel record table.
inline constexpr SizeT kMGPipeCallbackCount = 7;
static_assert(sizeof(MGPipeCallbacks) == kMGPipeCallbackCount * sizeof(void (*)()),
"MGPipeCallbacks gained or lost a callback");
// Null-initialized: a backend that installs nothing sends nothing.
inline MGPipeCallbacks gMGPipeCallbacks{};
#if MOBILEGL_BUILD_DISAGGREGATED
// HOW MUCH OF ITSELF ONE REVERSE-CHANNEL RECORD MAY BE, and it is not a tuning knob: a
// producer that ignores it kills the server process.
//
// OnBufferWriteback's bytes travel INLINE in one SEG_EVENT record, and a ring producer
// refuses any record above Capacity()/2 OUTRIGHT (Transport/Ring.h) - not "when full", but
// always, because above half a capacity a record is placeable at some head offsets and not
// at others, so waiting for room would be a hang. The server's answer to a refusal is
// Fatal{EventRingOverflow}, and SEG_EVENT defaults to 256 KiB, so ANY producer that posts a
// whole range wider than ~128 KiB is a server that dies on a large enough buffer.
//
// THE PRODUCERS ARE IN MG_Backend, which cannot reach MG_Remote::Client's
// BufferWritebackSliceBytes (that one is the CLIENT-linkage answer to the same question,
// for requests going the other way) and must not reach ServerSession either. So the
// transport publishes the one number through this hook, exactly as it publishes segment
// resolution through gMGPipeSegmentResolver: the SERVER role installs it at Accept and
// releases it at Close, and a monolith build leaves it null.
using MGPipeEventRingCapacityQuery = Uint64 (*)();
inline MGPipeEventRingCapacityQuery gMGPipeEventRingCapacityBytes = nullptr;
// 0 means "do not slice": no transport is installed, so the callback is a direct call with
// no ring under it and cutting the range would only multiply the calls.
//
// A QUARTER of the ring rather than the MaxRecordBytes half, which is the same number and
// the same reasoning the client side uses (MG_Remote/Client/GpuWritePending.cpp): the record
// carries its own header and an EventBufferWritebackHead beside the payload, and the ring may
// still hold small events posted earlier in the same verb's apply. The 4 KiB floor is there so
// that a pathologically small operator-supplied ring cannot make a slicing loop spin at zero
// width; such a ring is broken anyway and the producer's Fatal{EventRingOverflow} names it on
// the first post.
inline Uint64 MGPipeBufferWritebackSliceBytes() {
if (gMGPipeEventRingCapacityBytes == nullptr) return 0;
const Uint64 capacity = gMGPipeEventRingCapacityBytes();
if (capacity == 0) return 0;
const Uint64 quarter = capacity / 4;
return quarter < 4096 ? 4096 : quarter;
}
#endif
} // namespace MobileGL::MG_Pipe