mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
[MG_Remote, MG_Impl, MG_Backend] (Disaggregated): P5c ev - SEG_EVENT is the only reverse channel
The three reverse MGPipeCallbacks entries become the SERVER session's producer callbacks
(Reserve + fill the head + copy the payload inline + PublishEvents; the host pointer never
reaches the wire), installed at Accept and uninstalled at Close, a second installation
over a claimed entry being Fatal{RoleViolation, callback-double-install}. Producers wired:
buffer writeback (the mapped bytes are copied into the record's inline tail instead of a
raw pointer in MGPBlobRef::Offset), GPU-written (Magma's three direct MarkGpuWritten
bypasses are routed through the callback), surface-changed (the backend posts
MGPSurfaceInfo; the client consumer replays the allocate/format writes against
pDefaultFramebufferInfo on the GL thread, where R3's ownership always was), and
kEventGlError = 4 for PipeInputs::RecordError (ordering stays P9's).
PipeInputs::InvalidateCompileEnv's forward is deleted with an active transport - R-12's
caps re-publication already does its job. The consumer's monolith guard is replaced by
the three segment arms (kSegEvent resolves through the session's SegmentTable;
kMGHostSpanSegNone is monolith-only and Fatal{ProtocolCorruption} on the wire; anything
else the same), and DrainEventRing calls the client consumers BY NAME, never through the
global table. Overflow is Fatal{EventRingOverflow} - P5c's events are lossless, P9 owns
the drop policy. Monolith arms keep the pre-P5c expressions character for character.
Evidence: unit 2139/2139 (incl. the new kEventGlError round-trip), integration-split
107/107, monolith GPU subset 24/24; negative control executed - a writeback blobref
reverted to the raw-pointer shape turns DirectGLES.Split.AtomicCounterScenario.
SubDataAfterDispatchSurvivesAnImmediateReadback red with Fatal{ProtocolCorruption,
OnBufferWriteback.Seg}. CONTRACT-P5C sections 1, 4.
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against.
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// P5c ev: the surface-changed event's producer callback, installed by the server session.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#endif
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
@@ -11601,8 +11603,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
auto* depthTexture = defaultFBOInfo->depthAttachment.get();
|
||||
auto* stencilTexture = defaultFBOInfo->stencilAttachment.get();
|
||||
if (depthTexture) depthTexture->SetInternalFormat(depthFormat);
|
||||
if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the attachments are CLIENT
|
||||
// memory and this thread may not write them - the backend fills MGPSurfaceInfo
|
||||
// and posts, and the client applies it to its own pDefaultFramebufferInfo on the
|
||||
// GL thread. FORMAT ONLY, exactly as the monolith arm below: Width/Height stay 0,
|
||||
// which is the consumer's cue to leave the placeholder extent untouched. The
|
||||
// callback's presence IS the transport probe - the server session installs it at
|
||||
// Accept, and under monolith nobody ever does.
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.InternalFormat = static_cast<Uint32>(depthFormat);
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (depthTexture) depthTexture->SetInternalFormat(depthFormat);
|
||||
if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat);
|
||||
}
|
||||
MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment "
|
||||
"formats depth=%d stencil=%d",
|
||||
depthBits, stencilBits, floatDepth ? 1 : 0, static_cast<int>(depthFormat),
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the surface-changed event's producer callback, installed by the server session.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#endif
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<vulkan/vk_enum_string_helper.h>)
|
||||
#include <vulkan/vk_enum_string_helper.h>
|
||||
@@ -280,11 +285,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<SizeT>(defaultFramebufferExtent.width) *
|
||||
static_cast<SizeT>(defaultFramebufferExtent.height) * 4;
|
||||
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
switch (m_depthStencilFormat) {
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
@@ -300,11 +300,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
break;
|
||||
}
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
// The default FBO's stencil attachment must track the swapchain extent:
|
||||
// FramebufferObject::CheckCompleteness requires every valid attachment
|
||||
@@ -324,11 +319,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stencilFormat = depthFormat;
|
||||
break;
|
||||
}
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the default FBO's
|
||||
// attachments are CLIENT memory and this thread may not write them - the backend
|
||||
// fills MGPSurfaceInfo and posts, and the client's consumer replays exactly the
|
||||
// allocate/format writes of the monolith arm below on the GL thread. The two
|
||||
// formats are always equal by the switches above, so one InternalFormat carries
|
||||
// both. The callback's presence IS the transport probe - the server session
|
||||
// installs it at Accept, and under monolith nobody ever does.
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.Width = defaultFramebufferExtent.width;
|
||||
info.Height = defaultFramebufferExtent.height;
|
||||
info.InternalFormat = static_cast<Uint32>(depthFormat);
|
||||
info.Samples = 1;
|
||||
info.Layers = 1;
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the GPU-write announcement routes through the reverse channel (R2).
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#endif
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject1D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
@@ -1072,7 +1076,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// changed a byte of it.
|
||||
bufferObject->EnsureGpuResidentStorage();
|
||||
if (imageBinding.Access != GL_READ_ONLY) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev (R2, CONTRACT-P5C §4.2): the announcement goes through the reverse
|
||||
// channel - the apply thread may not poke the client object directly.
|
||||
MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject);
|
||||
#else
|
||||
bufferObject->MarkGpuWritten();
|
||||
#endif
|
||||
}
|
||||
|
||||
BufferSlice slice{};
|
||||
@@ -1228,7 +1238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// results are visible without a readback path, exactly as for a capture buffer.
|
||||
bufferObject->EnsureGpuResidentStorage();
|
||||
// ... and the read that follows has to wait for this draw or dispatch to retire.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev (R2, CONTRACT-P5C §4.2): through the reverse channel, not a direct poke of
|
||||
// the client object from the apply thread.
|
||||
MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject);
|
||||
#else
|
||||
bufferObject->MarkGpuWritten();
|
||||
#endif
|
||||
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the GPU-write announcement routes through the reverse channel (R2).
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#endif
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/SamplerState/SamplerObject.h"
|
||||
@@ -11615,7 +11619,13 @@ void main() {
|
||||
// have happened, so the buffer is also flagged for the wait that a later CPU
|
||||
// read has to perform - the capture is a GPU write like any shader's.
|
||||
bufferObject->EnsureGpuResidentStorage();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev (R2, CONTRACT-P5C §4.2): through the reverse channel, not a direct poke
|
||||
// of the client object from the apply thread.
|
||||
MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject);
|
||||
#else
|
||||
bufferObject->MarkGpuWritten();
|
||||
#endif
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) {
|
||||
MGLOG_E_ONCE("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i);
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
// lanes must keep answering exactly what they answered before.
|
||||
#include <MG_Remote/Client/CapsMirror.h>
|
||||
#include <MG_Remote/Client/WireTables.h>
|
||||
// P5c ev (CONTRACT-P5C §4.2): RecordError's transport arm posts kEventGlError through the
|
||||
// server session, and InvalidateCompileEnv's forward is deleted with an active transport.
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
@@ -662,7 +665,18 @@ namespace MobileGL::MG_Pipe {
|
||||
// the resource subsystem bit would make the vertex-input subsystem emit null handles
|
||||
// in exactly the A/B arm that exists to isolate the two. It costs one free-list pop
|
||||
// and one map insert per buffer object and emits nothing.
|
||||
//
|
||||
// The client resource callbacks are MONOLITH-ONLY (CONTRACT-P5C §4.1): with an
|
||||
// active transport the server session installs its producer callbacks at Accept and
|
||||
// the client's consumers are invoked by name from DrainEventRing, so installing
|
||||
// them here would be the double installation the check now aborts on.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) {
|
||||
MGPipeInstallClientResourceCallbacks();
|
||||
}
|
||||
#else
|
||||
MGPipeInstallClientResourceCallbacks();
|
||||
#endif
|
||||
MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
}
|
||||
|
||||
@@ -1752,6 +1766,16 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void PipeInputs::InvalidateCompileEnv() {
|
||||
MGP_STICKY_FORWARD_PULL(InvalidateCompileEnv);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// DELETED with an active transport (CONTRACT-P5C §4.2): R-12's caps
|
||||
// re-publication already invalidates the CLIENT's compile environment when the
|
||||
// second snapshot arrives (CapsMirror.cpp:78-80), and this forward is a write
|
||||
// into the frontend with no wire shape. A caller that still needs it under a
|
||||
// transport is a defect to fix, not a pull to serve.
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
|
||||
}
|
||||
|
||||
@@ -1763,6 +1787,20 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
MGP_STICKY_FORWARD_PULL(RecordError);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// The error queue is CLIENT state and the apply thread may not write it (R4).
|
||||
// kEventGlError carries the code and the message; the client records it into its
|
||||
// own queue at the next drain point. ORDERING IS P9's (CONTRACT-P5C §4.2): the
|
||||
// event is observed at the next EmitAndWait drain, which preserves per-thread
|
||||
// program order of error-then-read but not cross-verb interleaving - the
|
||||
// accepted P5c shape, stated in the contract rather than discovered in P6.
|
||||
const String message = info != nullptr ? info->toString() : String{};
|
||||
MG_Remote::Server::ServerSessionInstance().PostGlError(static_cast<Uint32>(code),
|
||||
message.c_str());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) {
|
||||
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
|
||||
|
||||
@@ -49,7 +49,16 @@
|
||||
|
||||
#include <Config.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c ev (CONTRACT-P5C §4.3): the writeback consumer's SEG_EVENT arm resolves the blobref
|
||||
// through the client session's own SegmentTable, and the transport check below is the same
|
||||
// MG_Config::Transport probe PipeFill.cpp uses. Behind the build option for G1's reason -
|
||||
// nothing under MG_Remote may be reachable from a pull build.
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
@@ -550,6 +559,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// The client resolves the handle to its own object and writes the shadow; the epoch bump
|
||||
// stays SERVER-side and happens AFTER this returns, never before (ARCHITECTURE.md 7.4:
|
||||
// the reverse channel needs the same ordering guarantee as the forward one).
|
||||
//
|
||||
// THE BLOBREF'S THREE ARMS (CONTRACT-P5C §4.3, replacing the P3a monolith guard that
|
||||
// rejected every Seg != kMGHostSpanSegNone and would have dropped every writeback EVENT
|
||||
// on arrival):
|
||||
// Seg == kSegEvent -> the wire shape (CONTRACT-P5C §1's reverse-channel row):
|
||||
// Offset is the byte offset of the inline payload inside
|
||||
// SEG_EVENT, resolved through the client session's OWN
|
||||
// SegmentTable, bounds-checked against the announced size;
|
||||
// Seg == kMGHostSpanSegNone -> monolith only: Offset IS the backend's mapped address
|
||||
// (MGPipeTypes.h says so in as many words). With an active
|
||||
// transport this is rule B in the reverse direction and is
|
||||
// Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"};
|
||||
// anything else -> the same Fatal.
|
||||
inline void MGPipeClientOnBufferWriteback(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes) {
|
||||
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
|
||||
if (buffer == nullptr) {
|
||||
@@ -557,18 +579,62 @@ namespace MobileGL::MG_Pipe {
|
||||
res.Slot, res.Gen);
|
||||
return;
|
||||
}
|
||||
if (bytes.Seg != kMGHostSpanSegNone) {
|
||||
MGLOG_E_ONCE("MGPipe: OnBufferWriteback carried a transport segment (%u); P3a is monolith only",
|
||||
void* bytePtr = nullptr;
|
||||
if (bytes.Seg == kMGHostSpanSegNone) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Seg\"} - a "
|
||||
"writeback blobref carried Seg = kMGHostSpanSegNone (a raw host "
|
||||
"address) with an active transport. Rule B binds the reverse "
|
||||
"direction exactly as it binds MGHostSpan: on the wire the blobref "
|
||||
"names SEG_EVENT and an in-segment offset, never a host address");
|
||||
std::abort();
|
||||
}
|
||||
#endif
|
||||
bytePtr = reinterpret_cast<void*>(static_cast<std::uintptr_t>(bytes.Offset));
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
else if (bytes.Seg == MG_Remote::Wire::kSegEvent) {
|
||||
// Size == 0 with a live resource is legal - a zero-length writeback
|
||||
// (CONTRACT-P5C §1) - and SegmentTable::Resolve answers nullptr for a zero size,
|
||||
// so only a non-empty run goes through the bounds-checked resolve.
|
||||
if (bytes.Size != 0) {
|
||||
auto* session = MG_Remote::Client::ClientSession::Active();
|
||||
const void* resolved =
|
||||
session == nullptr
|
||||
? nullptr
|
||||
: session->Segments().Resolve(MG_Remote::Wire::kSegEvent, bytes.Offset,
|
||||
bytes.Size);
|
||||
if (resolved == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Offset\"} - "
|
||||
"the writeback blobref's {%llu + %llu} does not resolve inside the "
|
||||
"session's SEG_EVENT segment",
|
||||
static_cast<unsigned long long>(bytes.Offset),
|
||||
static_cast<unsigned long long>(bytes.Size));
|
||||
std::abort();
|
||||
}
|
||||
bytePtr = const_cast<void*>(resolved);
|
||||
}
|
||||
} else {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Seg\"} - a writeback "
|
||||
"blobref carried Seg %u, which is neither kMGHostSpanSegNone (monolith) nor "
|
||||
"kSegEvent (the wire shape)",
|
||||
bytes.Seg);
|
||||
std::abort();
|
||||
}
|
||||
#else
|
||||
else {
|
||||
// The pre-P5c guard's monolith spelling, kept for builds with no transport layer:
|
||||
// a segment tag cannot legitimately arrive here and the write must not be made
|
||||
// from a null pointer.
|
||||
MGLOG_E_ONCE("MGPipe: OnBufferWriteback carried a transport segment (%u) in a build "
|
||||
"with no transport; the writeback is dropped",
|
||||
bytes.Seg);
|
||||
return;
|
||||
}
|
||||
// Monolith: Seg is kMGHostSpanSegNone and Offset IS the address of the backend's
|
||||
// mapped bytes (MGPipeTypes.h says so in as many words). Under a transport the
|
||||
// segment resolves first, and that is the phase's edit, not this one's.
|
||||
buffer->WritebackFromBackend(
|
||||
DataPtr{reinterpret_cast<void*>(static_cast<std::uintptr_t>(bytes.Offset)),
|
||||
static_cast<SizeT>(bytes.Size)},
|
||||
static_cast<SizeT>(offset));
|
||||
#endif
|
||||
buffer->WritebackFromBackend(DataPtr{bytePtr, static_cast<SizeT>(bytes.Size)},
|
||||
static_cast<SizeT>(offset));
|
||||
}
|
||||
|
||||
// A draw or dispatch wrote these ranges. ARCHITECTURE.md 7.1 calls this a NARROWING
|
||||
@@ -600,16 +666,67 @@ namespace MobileGL::MG_Pipe {
|
||||
buffer->MarkGpuWritten();
|
||||
}
|
||||
|
||||
// Installed once, and never over an entry a backend already claimed: these two are the
|
||||
// CLIENT's implementations of a backend -> frontend callback, so the backend installs
|
||||
// the rest of the table and these two answer for it.
|
||||
// MONOLITH ONLY (CONTRACT-P5C §4.1): with an active transport the three reverse entries
|
||||
// of gMGPipeCallbacks are the SERVER session's producer callbacks and the client
|
||||
// consumers are invoked BY NAME from DrainEventRing - the global table is a
|
||||
// producer-side surface under split. The caller (MGPipeMintResourceHandle) gates on the
|
||||
// resolved transport, exactly as PipeFill.cpp's other transport arms do.
|
||||
//
|
||||
// Installed over only an EMPTY or OUR OWN entry: writing over one somebody else claimed
|
||||
// is Fatal{RoleViolation, "callback-double-install"} - the "never over an entry a
|
||||
// backend already claimed" comment, made a check. Finding our own function is the
|
||||
// idempotent repeat this helper runs once per buffer mint, not a second installation.
|
||||
inline void MGPipeInstallClientResourceCallbacks() {
|
||||
if (gMGPipeCallbacks.OnBufferWriteback == nullptr) {
|
||||
gMGPipeCallbacks.OnBufferWriteback = &MGPipeClientOnBufferWriteback;
|
||||
}
|
||||
const auto install = [](auto& entry, auto* fn, const char* name) {
|
||||
if (entry != nullptr && entry != fn) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - %s is "
|
||||
"already claimed by a different function. With an active transport "
|
||||
"the reverse entries are the server session's producers; the client "
|
||||
"installs them under monolith only",
|
||||
name);
|
||||
std::abort();
|
||||
}
|
||||
entry = fn;
|
||||
};
|
||||
install(gMGPipeCallbacks.OnBufferWriteback, &MGPipeClientOnBufferWriteback,
|
||||
"OnBufferWriteback");
|
||||
install(gMGPipeCallbacks.OnGpuWritten, &MGPipeClientOnGpuWritten, "OnGpuWritten");
|
||||
}
|
||||
|
||||
// The GPU-write announcement for a buffer the backend holds only a FRONTEND POINTER to -
|
||||
// Magma's barrier-pulled binding-point reads (UniformManager / VulkanRenderer), whose
|
||||
// direct bufferObject->MarkGpuWritten() was the apply thread poking client memory (R2).
|
||||
// Routed through the reverse channel exactly as DirectGLES' MarkBufferGpuWritten routes
|
||||
// its own sites: ONE whole-buffer range, stated rather than implied (zero ranges is the
|
||||
// shape a fully narrowed announcement will legitimately have at P8/P9, and the two must
|
||||
// not be the same record). The handle comes from the client allocator's lifetime-id
|
||||
// probe - the mint is unconditional at the BufferObject constructor
|
||||
// (MGPipeMintResourceHandle), so a live buffer always has one.
|
||||
//
|
||||
// The fallback is the monolith shape, preserved byte for byte: no reverse channel
|
||||
// installed (a pull-arm build, or a process with no session) pokes the object directly,
|
||||
// which is exactly what these sites did before. A MISSING HANDLE with a live channel is
|
||||
// loud rather than a silent drop, the same choice MarkBufferGpuWritten makes.
|
||||
inline void MGPipeAnnounceBufferGpuWritten(
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
||||
if (bufferObject == nullptr) return;
|
||||
if (gMGPipeCallbacks.OnGpuWritten == nullptr) {
|
||||
gMGPipeCallbacks.OnGpuWritten = &MGPipeClientOnGpuWritten;
|
||||
bufferObject->MarkGpuWritten();
|
||||
return;
|
||||
}
|
||||
const MGPipeHandle res =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, bufferObject->GetLifetimeId());
|
||||
if (MGPipeHandleIsNull(res)) {
|
||||
MGLOG_E_ONCE("MGPipe: no handle for the GPU-write announcement of buffer %u - the "
|
||||
"reverse channel is installed but the mint is missing, so the mark "
|
||||
"would be dropped at the consumer; poking the object directly rather "
|
||||
"than losing it",
|
||||
bufferObject->GetExternalIndex());
|
||||
bufferObject->MarkGpuWritten();
|
||||
return;
|
||||
}
|
||||
const MGPRange whole{0, kMGPipeWholeBuffer};
|
||||
gMGPipeCallbacks.OnGpuWritten(res, 1, &whole);
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
#include "WireTables.h"
|
||||
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
@@ -173,6 +178,58 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// THE BYTES LIVE IN THE RING ITSELF, so Drained() is called only after every payload
|
||||
// pointer popped here has been consumed: retiring earlier is R-11's violation one level
|
||||
// down (EventRing.h:168-171 says so in as many words).
|
||||
//
|
||||
// THE CONSUMERS ARE CALLED BY NAME, NEVER THROUGH gMGPipeCallbacks (CONTRACT-P5C
|
||||
// §4.1): with an active transport the global table's reverse entries are the SERVER
|
||||
// session's producer callbacks, so routing the drain through it would call the
|
||||
// producer back from the GL thread - the layer-2 violation the contract names.
|
||||
//
|
||||
// THE SURFACE-CHANGED CONSUMER IS WHERE R3's OWNERSHIP ALWAYS WAS: the backend
|
||||
// posts the MGPSurfaceInfo, and the allocate/format writes against
|
||||
// pDefaultFramebufferInfo happen HERE, on the GL thread, against client memory.
|
||||
void ApplySurfaceChangedToClient(const MG_Pipe::MGPSurfaceInfo& info) {
|
||||
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
|
||||
if (!defaultFBOInfo) return;
|
||||
const auto format = static_cast<TextureInternalFormat>(info.InternalFormat);
|
||||
auto* colorTex =
|
||||
static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
auto* depthTex =
|
||||
static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
auto* stencilTex =
|
||||
static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
if (info.Width != 0 && info.Height != 0) {
|
||||
// The SWAPCHAIN's publication shape: an extent-carrying event. Reproduces
|
||||
// SwapchainObject's monolith writes statement for statement - colour storage
|
||||
// at the extent, depth/stencil format then storage - because
|
||||
// FramebufferObject::CheckCompleteness requires every attachment to agree.
|
||||
const Int extentWidth = static_cast<Int>(info.Width);
|
||||
const Int extentHeight = static_cast<Int>(info.Height);
|
||||
const SizeT attachmentByteSize =
|
||||
static_cast<SizeT>(info.Width) * static_cast<SizeT>(info.Height) * 4;
|
||||
if (colorTex != nullptr) {
|
||||
colorTex->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
||||
{{extentWidth, extentHeight, 1}, attachmentByteSize});
|
||||
}
|
||||
if (depthTex != nullptr) {
|
||||
depthTex->SetInternalFormat(format);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
||||
{{extentWidth, extentHeight, 1}, attachmentByteSize});
|
||||
}
|
||||
if (stencilTex != nullptr) {
|
||||
stencilTex->SetInternalFormat(format);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
||||
{{extentWidth, extentHeight, 1}, attachmentByteSize});
|
||||
}
|
||||
} else {
|
||||
// The DirectGLES publication shape: FORMAT ONLY, Width/Height == 0. The
|
||||
// placeholder's 512x512 extent is deliberately left alone - all three
|
||||
// attachments share it, and resizing depth/stencil without colour would
|
||||
// report the default framebuffer incomplete.
|
||||
if (depthTex != nullptr) depthTex->SetInternalFormat(format);
|
||||
if (stencilTex != nullptr) stencilTex->SetInternalFormat(format);
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 DrainEventRing(Transport::EventRingConsumer& events) {
|
||||
if (!events.Valid()) return 0;
|
||||
Uint32 delivered = 0;
|
||||
@@ -197,19 +254,18 @@ namespace MobileGL::MG_Remote::Client {
|
||||
static_cast<unsigned long long>(view.payloadSize - sizeof(*head)));
|
||||
std::abort();
|
||||
}
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
|
||||
// The blobref names SEG_EVENT and the IN-SEGMENT offset of those inline
|
||||
// bytes - never a host address (R-2's rule B), which is the whole reason
|
||||
// EventRingConsumer exposes OffsetInSegment at all.
|
||||
MG_Pipe::MGPBlobRef blob{};
|
||||
blob.Seg = Wire::kSegEvent;
|
||||
blob.Offset = events.OffsetInSegment(bytes);
|
||||
blob.Size = head->Size;
|
||||
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
|
||||
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
|
||||
head->Offset, blob);
|
||||
++delivered;
|
||||
}
|
||||
// The blobref names SEG_EVENT and the IN-SEGMENT offset of those inline
|
||||
// bytes - never a host address (R-2's rule B), which is the whole reason
|
||||
// EventRingConsumer exposes OffsetInSegment at all. The consumer's
|
||||
// kSegEvent arm resolves it through this session's own SegmentTable.
|
||||
MG_Pipe::MGPBlobRef blob{};
|
||||
blob.Seg = Wire::kSegEvent;
|
||||
blob.Offset = events.OffsetInSegment(bytes);
|
||||
blob.Size = head->Size;
|
||||
MG_Pipe::MGPipeClientOnBufferWriteback(
|
||||
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
|
||||
head->Offset, blob);
|
||||
++delivered;
|
||||
break;
|
||||
}
|
||||
case Transport::kEventGpuWritten: {
|
||||
@@ -224,39 +280,71 @@ namespace MobileGL::MG_Remote::Client {
|
||||
static_cast<unsigned long long>(tail));
|
||||
std::abort();
|
||||
}
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten != nullptr) {
|
||||
// EventRange and MGPRange are the same two Uint64s (EventRing.h:61-66
|
||||
// asserts it), so the tail is handed over as-is rather than copied into
|
||||
// a second array a later reader could get out of step with.
|
||||
const auto* ranges = reinterpret_cast<const MG_Pipe::MGPRange*>(
|
||||
static_cast<const Uint8*>(view.payload) + sizeof(*head));
|
||||
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(
|
||||
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
|
||||
static_cast<Uint>(head->RangeCount), ranges);
|
||||
++delivered;
|
||||
}
|
||||
// EventRange and MGPRange are the same two Uint64s (EventRing.h:61-66
|
||||
// asserts it), so the tail is handed over as-is rather than copied into
|
||||
// a second array a later reader could get out of step with.
|
||||
const auto* ranges = reinterpret_cast<const MG_Pipe::MGPRange*>(
|
||||
static_cast<const Uint8*>(view.payload) + sizeof(*head));
|
||||
MG_Pipe::MGPipeClientOnGpuWritten(
|
||||
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
|
||||
static_cast<Uint>(head->RangeCount), ranges);
|
||||
++delivered;
|
||||
break;
|
||||
}
|
||||
case Transport::kEventSurfaceChanged: {
|
||||
if (view.payloadSize < sizeof(Transport::EventSurfaceChangedHead)) break;
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
const auto* head =
|
||||
static_cast<const Transport::EventSurfaceChangedHead*>(view.payload);
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.Width = head->Width;
|
||||
info.Height = head->Height;
|
||||
info.InternalFormat = head->InternalFormat;
|
||||
info.Samples = head->Samples;
|
||||
info.Layers = head->Layers;
|
||||
info.IsDefault = head->IsDefault;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
++delivered;
|
||||
const auto* head =
|
||||
static_cast<const Transport::EventSurfaceChangedHead*>(view.payload);
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.Width = head->Width;
|
||||
info.Height = head->Height;
|
||||
info.InternalFormat = head->InternalFormat;
|
||||
info.Samples = head->Samples;
|
||||
info.Layers = head->Layers;
|
||||
info.IsDefault = head->IsDefault;
|
||||
ApplySurfaceChangedToClient(info);
|
||||
++delivered;
|
||||
break;
|
||||
}
|
||||
case Transport::kEventGlError: {
|
||||
if (view.payloadSize < sizeof(Transport::EventGlErrorHead)) break;
|
||||
const auto* head =
|
||||
static_cast<const Transport::EventGlErrorHead*>(view.payload);
|
||||
const char* message =
|
||||
reinterpret_cast<const char*>(static_cast<const Uint8*>(view.payload) +
|
||||
sizeof(*head));
|
||||
const Uint64 tail = view.payloadSize - sizeof(*head);
|
||||
// MessageBytes == 0 is the corrupt shape, never "no message": the NUL
|
||||
// travels (CONTRACT-P5C §1, rule A's twin), so a legal record carries at
|
||||
// least one byte and that byte terminates the string.
|
||||
if (head->MessageBytes == 0 || tail < head->MessageBytes ||
|
||||
message[head->MessageBytes - 1] != '\0') {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"kEventGlError\"} - "
|
||||
"MessageBytes %u against a %llu-byte tail, or the terminating "
|
||||
"NUL is missing",
|
||||
static_cast<unsigned>(head->MessageBytes),
|
||||
static_cast<unsigned long long>(tail));
|
||||
std::abort();
|
||||
}
|
||||
// The error queue is CLIENT state, written here on the GL thread. The
|
||||
// observation point is the next drain after the post - P9 owns the
|
||||
// ordering (CONTRACT-P5C §4.2).
|
||||
if (MG_State::pGLContext != nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
static_cast<ErrorCode>(head->Code),
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
String(message, static_cast<SizeT>(head->MessageBytes - 1))));
|
||||
} else {
|
||||
MGLOG_E_ONCE("MG_Remote client: a kEventGlError (code %u) arrived with "
|
||||
"no live context and is dropped",
|
||||
static_cast<unsigned>(head->Code));
|
||||
}
|
||||
++delivered;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_W("MG_Remote client: reverse-channel record kind %u is not consumed in "
|
||||
"P5 (R-12 takes three and a half of the ten callbacks)",
|
||||
"P5c (four of the ten callbacks are armed; the rest are P9's)",
|
||||
static_cast<unsigned>(view.kind));
|
||||
break;
|
||||
}
|
||||
@@ -944,6 +1032,8 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
Transport::EventRingConsumer& ClientSession::Events() { return m_events; }
|
||||
|
||||
Wire::SegmentTable& ClientSession::Segments() { return m_segments; }
|
||||
|
||||
Transport::RingControl* ClientSession::Control() { return m_shm.CmdControl(); }
|
||||
|
||||
Transport::SessionSegments& ClientSession::Shm() { return m_shm; }
|
||||
|
||||
@@ -202,6 +202,11 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// OnSurfaceChanged. Drained by the GL thread between verbs.
|
||||
Transport::EventRingConsumer& Events();
|
||||
|
||||
// The CLIENT's own segment table (P5c ev, CONTRACT-P5C §4.3): the writeback
|
||||
// consumer resolves a SEG_EVENT blobref through it. Never the process resolver -
|
||||
// table 3 installs that one on the server role only.
|
||||
Wire::SegmentTable& Segments();
|
||||
|
||||
Transport::RingControl* Control();
|
||||
Transport::SessionSegments& Shm();
|
||||
Transport::ITransport* Control_Plane();
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
@@ -155,6 +157,120 @@ namespace MobileGL::MG_Remote::Server {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// ---- P5c ev: the reverse channel's PRODUCER callbacks (CONTRACT-P5C §4.1) --------
|
||||
//
|
||||
// With an active transport the three reverse entries of gMGPipeCallbacks belong to
|
||||
// the SERVER session, never to the client: the backend calls them with exactly the
|
||||
// arguments it always passed (a writeback blobref whose Offset IS the backend's own
|
||||
// mapped pointer, a range array, a surface info), and what the callback does with
|
||||
// the call is Reserve + fill the head + copy the payload + PublishEvents. The host
|
||||
// pointer NEVER reaches the wire - what crosses is the inline copy inside the
|
||||
// SEG_EVENT record, which is rule B binding the reverse direction exactly as it
|
||||
// binds MGHostSpan (R-2).
|
||||
//
|
||||
// OVERFLOW IS A DEFECT, NOT A DROP (§4.4): all four events are lossless in P5c, so
|
||||
// a Reserve that returns nullptr is Fatal{EventRingOverflow}. CountDrop and the
|
||||
// eventRingFull latch stay built and stay unused-by-policy; P9 owns the policy.
|
||||
[[noreturn]] void FatalEventRingOverflow(const char* eventName, Uint64 payloadBytes) {
|
||||
MGLOG_F("MGPipe: Fatal{EventRingOverflow} - the producer of %s could not reserve "
|
||||
"%llu bytes on SEG_EVENT. P5c's events are lossless and a full ring under "
|
||||
"lockstep is a producer burst no measured workload has, so this is a "
|
||||
"defect, not a drop (CONTRACT-P5C §4.4; the drop policy is P9's)",
|
||||
eventName, static_cast<unsigned long long>(payloadBytes));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
void ServerOnBufferWriteback(MG_Pipe::MGPipeHandle res, Uint64 offset,
|
||||
MG_Pipe::MGPBlobRef bytes) {
|
||||
if (bytes.Seg != MG_Pipe::kMGHostSpanSegNone) {
|
||||
// The backend handed over a segment-tagged blobref. The ONLY legal shape at
|
||||
// this boundary is the monolith one - Offset is the mapped address, valid
|
||||
// for this call - because the segment copy is THIS function's own job.
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Seg\"} - the "
|
||||
"writeback producer was handed Seg %u; at the backend boundary the "
|
||||
"blobref names the backend's own mapped bytes (Seg = "
|
||||
"kMGHostSpanSegNone) and the copy into SEG_EVENT is the producer's",
|
||||
bytes.Seg);
|
||||
std::abort();
|
||||
}
|
||||
ServerSession& session = ServerSessionInstance();
|
||||
const Uint64 payloadBytes = sizeof(Transport::EventBufferWritebackHead) + bytes.Size;
|
||||
void* slot = session.Events().Reserve(Transport::kEventBufferWriteback, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventBufferWriteback", payloadBytes);
|
||||
}
|
||||
Transport::EventBufferWritebackHead head{};
|
||||
head.Resource = Transport::EventHandle{res.Slot, res.Gen};
|
||||
head.Offset = offset;
|
||||
head.Size = bytes.Size;
|
||||
std::memcpy(slot, &head, sizeof(head));
|
||||
if (bytes.Size != 0) {
|
||||
std::memcpy(static_cast<Uint8*>(slot) + sizeof(head),
|
||||
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(bytes.Offset)),
|
||||
static_cast<SizeT>(bytes.Size));
|
||||
}
|
||||
session.PublishEvents();
|
||||
}
|
||||
|
||||
void ServerOnGpuWritten(MG_Pipe::MGPipeHandle res, Uint rangeCount,
|
||||
const MG_Pipe::MGPRange* ranges) {
|
||||
ServerSession& session = ServerSessionInstance();
|
||||
const Uint64 tailBytes = static_cast<Uint64>(rangeCount) * sizeof(Transport::EventRange);
|
||||
const Uint64 payloadBytes = sizeof(Transport::EventGpuWrittenHead) + tailBytes;
|
||||
void* slot = session.Events().Reserve(Transport::kEventGpuWritten, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventGpuWritten", payloadBytes);
|
||||
}
|
||||
Transport::EventGpuWrittenHead head{};
|
||||
head.Resource = Transport::EventHandle{res.Slot, res.Gen};
|
||||
head.RangeCount = static_cast<std::uint32_t>(rangeCount);
|
||||
std::memcpy(slot, &head, sizeof(head));
|
||||
if (tailBytes != 0) {
|
||||
// EventRange and MGPRange are the same two Uint64s (EventRing.h:61-66 asserts
|
||||
// it), so the tail is a plain copy rather than a per-element conversion.
|
||||
std::memcpy(static_cast<Uint8*>(slot) + sizeof(head), ranges,
|
||||
static_cast<SizeT>(tailBytes));
|
||||
}
|
||||
session.PublishEvents();
|
||||
}
|
||||
|
||||
void ServerOnSurfaceChanged(const MG_Pipe::MGPSurfaceInfo* info) {
|
||||
ServerSession& session = ServerSessionInstance();
|
||||
constexpr Uint64 payloadBytes = sizeof(Transport::EventSurfaceChangedHead);
|
||||
void* slot = session.Events().Reserve(Transport::kEventSurfaceChanged, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventSurfaceChanged", payloadBytes);
|
||||
}
|
||||
Transport::EventSurfaceChangedHead head{};
|
||||
if (info != nullptr) {
|
||||
head.Width = info->Width;
|
||||
head.Height = info->Height;
|
||||
head.InternalFormat = info->InternalFormat;
|
||||
head.Samples = info->Samples;
|
||||
head.Layers = info->Layers;
|
||||
head.IsDefault = info->IsDefault;
|
||||
}
|
||||
std::memcpy(slot, &head, sizeof(head));
|
||||
session.PublishEvents();
|
||||
}
|
||||
|
||||
// One installer for both roles' tables, so the check exists in exactly one spelling:
|
||||
// writing over an entry somebody else claimed is Fatal{RoleViolation,
|
||||
// "callback-double-install"} - MGPipeCallbacks' "never over an entry a backend
|
||||
// already claimed" comment, made a check (CONTRACT-P5C §4.1). Finding OUR OWN
|
||||
// function there is the idempotent repeat, not a second installation.
|
||||
template <typename Fn>
|
||||
void InstallReverseCallback(Fn& entry, Fn producer) {
|
||||
if (entry != nullptr && entry != producer) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"callback-double-install\"} - a reverse "
|
||||
"MGPipeCallbacks entry is already claimed by a different function. With "
|
||||
"an active transport the three reverse entries are the server "
|
||||
"session's producers; the client installs them under monolith only");
|
||||
std::abort();
|
||||
}
|
||||
entry = producer;
|
||||
}
|
||||
|
||||
ServerSession* g_active = nullptr;
|
||||
|
||||
} // namespace
|
||||
@@ -337,6 +453,19 @@ namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
m_accepted = true;
|
||||
g_active = this;
|
||||
|
||||
// ---- P5c ev: the three reverse-channel producers are THIS session's (CONTRACT-P5C
|
||||
// §4.1), installed before the apply thread can apply a record that produces one and
|
||||
// uninstalled at Close after the join. Under monolith these entries are the
|
||||
// client's (MGPipeInstallClientResourceCallbacks, monolith-only now); a session is
|
||||
// by definition not monolith, so finding one of them claimed here is the double
|
||||
// installation the check exists to name.
|
||||
InstallReverseCallback(MG_Pipe::gMGPipeCallbacks.OnBufferWriteback,
|
||||
&ServerOnBufferWriteback);
|
||||
InstallReverseCallback(MG_Pipe::gMGPipeCallbacks.OnGpuWritten, &ServerOnGpuWritten);
|
||||
InstallReverseCallback(MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged,
|
||||
&ServerOnSurfaceChanged);
|
||||
|
||||
LogMemory("accept");
|
||||
|
||||
if (!CallMaskIsSet()) {
|
||||
@@ -415,6 +544,17 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// Uninstall AFTER the apply thread has joined, never before: a record still in
|
||||
// flight can still resolve a segment offset (table 3's fourth column).
|
||||
Wire::SegmentTable::UninstallProcessResolver();
|
||||
// The reverse-channel producers go with the session that owns them - release
|
||||
// only the entries that are still OURS, never one a later owner installed.
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == &ServerOnBufferWriteback) {
|
||||
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback = nullptr;
|
||||
}
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == &ServerOnGpuWritten) {
|
||||
MG_Pipe::gMGPipeCallbacks.OnGpuWritten = nullptr;
|
||||
}
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == &ServerOnSurfaceChanged) {
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged = nullptr;
|
||||
}
|
||||
}
|
||||
m_consumer.Detach();
|
||||
m_commands = Transport::RingConsumer();
|
||||
@@ -491,6 +631,40 @@ namespace MobileGL::MG_Remote::Server {
|
||||
m_consumer.NotifyClient();
|
||||
}
|
||||
|
||||
void ServerSession::PostGlError(Uint32 code, const char* message) {
|
||||
if (!m_accepted) {
|
||||
// The same answer PipeInputs::RecordError's own no-live-context arm gives: a
|
||||
// dropped driver error is loud, never silent.
|
||||
MGLOG_E_ONCE("MG_Remote server: PostGlError (code %u) before Accept - the error is "
|
||||
"dropped, because there is no SEG_EVENT to carry it on",
|
||||
static_cast<unsigned>(code));
|
||||
return;
|
||||
}
|
||||
// The message rides INLINE, NUL-terminated, MessageBytes = strlen + 1 (CONTRACT-P5C
|
||||
// §1), truncated to the cap at the producer - which is here, and is why the cap is a
|
||||
// constant of the wire header rather than a negotiated value.
|
||||
SizeT length = message == nullptr ? 0 : std::strlen(message);
|
||||
if (length >= Transport::kEventGlErrorMaxMessageBytes) {
|
||||
length = Transport::kEventGlErrorMaxMessageBytes - 1;
|
||||
}
|
||||
const Uint32 messageBytes = static_cast<Uint32>(length) + 1;
|
||||
const Uint64 payloadBytes = sizeof(Transport::EventGlErrorHead) + messageBytes;
|
||||
void* slot = m_events.Reserve(Transport::kEventGlError, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventGlError", payloadBytes);
|
||||
}
|
||||
Transport::EventGlErrorHead head{};
|
||||
head.Code = code;
|
||||
head.MessageBytes = messageBytes;
|
||||
std::memcpy(slot, &head, sizeof(head));
|
||||
auto* tail = reinterpret_cast<char*>(static_cast<Uint8*>(slot) + sizeof(head));
|
||||
if (length != 0) {
|
||||
std::memcpy(tail, message, length);
|
||||
}
|
||||
tail[length] = '\0';
|
||||
PublishEvents();
|
||||
}
|
||||
|
||||
// Both of these advance AND ring, through SessionConsumer. The free functions in namespace
|
||||
// Watermark do not ring: a client parked in WaitForPresentAck(kWaitForever) needs the pair.
|
||||
void ServerSession::AdvanceCompletedFrame(Uint64 serial) {
|
||||
|
||||
@@ -158,6 +158,18 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// caller and therefore compiles for every wrong pairing; the session is the thing
|
||||
// that knows which bell belongs to the client.
|
||||
void PublishEvents();
|
||||
|
||||
// ---- P5c ev: the reverse channel's producers (CONTRACT-P5C §4) -------------------
|
||||
//
|
||||
// With an active transport the three reverse MGPipeCallbacks entries are THIS
|
||||
// session's producer callbacks (Accept installs them, Close uninstalls them), and
|
||||
// the fourth event kind is posted through here: PipeInputs::RecordError's transport
|
||||
// arm calls PostGlError with the frontend ErrorCode widened and the ErrorInfo's
|
||||
// message. `message` may be null; a longer message than
|
||||
// Transport::kEventGlErrorMaxMessageBytes is truncated, which is the contract's
|
||||
// ruling (§1), not a check. A session that has not accepted drops the error with a
|
||||
// loud line, exactly as RecordError's own no-live-context arm does.
|
||||
void PostGlError(Uint32 code, const char* message);
|
||||
Transport::ITransport* Control_Plane();
|
||||
|
||||
// completedFrameSerial / presentAckSerial: the two watermarks only the server can
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// Record kinds on SEG_EVENT. 0 is kRingPadRecordKind and can never be an
|
||||
// event, which is why the list starts at 1.
|
||||
// event, which is why the list starts at 1. APPEND-ONLY, for the same reason
|
||||
// the opcode space is (CONTRACT-P5C §7.7): kEventGlError is the first addition.
|
||||
enum EventKind : std::uint16_t {
|
||||
kEventNone = 0,
|
||||
kEventBufferWriteback = 1, // MGPipeCallbacks::OnBufferWriteback
|
||||
kEventGpuWritten = 2, // MGPipeCallbacks::OnGpuWritten
|
||||
kEventSurfaceChanged = 3, // MGPipeCallbacks::OnSurfaceChanged
|
||||
kEventGlError = 4, // PipeInputs::RecordError, posted by the server session
|
||||
};
|
||||
|
||||
// The 8-byte {slot, gen} pair, mirrored (MGPipeHandles.h:54-65).
|
||||
@@ -97,6 +99,21 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
};
|
||||
static_assert(sizeof(EventSurfaceChangedHead) == 24, "MGPSurfaceInfo is 24 bytes on the wire");
|
||||
|
||||
// PipeInputs::RecordError(code, info) (CONTRACT-P5C §1). The NUL-terminated
|
||||
// message follows this head INSIDE THE RECORD: MessageBytes = strlen + 1 and
|
||||
// the NUL travels, so MessageBytes == 0 is the corrupt shape, never "no
|
||||
// message". A longer message is TRUNCATED AT THE PRODUCER to the cap; the cap
|
||||
// is a static_assert here, not a runtime check, because it is part of the
|
||||
// wire's shape rather than a policy a peer may disagree about.
|
||||
struct EventGlErrorHead {
|
||||
std::uint32_t Code; // the frontend ErrorCode, widened
|
||||
std::uint32_t MessageBytes; // strlen + 1, the NUL included
|
||||
};
|
||||
static_assert(sizeof(EventGlErrorHead) == 8, "wire shape");
|
||||
inline constexpr std::uint32_t kEventGlErrorMaxMessageBytes = 1024;
|
||||
static_assert(kEventGlErrorMaxMessageBytes == 1024,
|
||||
"CONTRACT-P5C §1 caps an inline GL-error message at 1024 bytes, NUL included");
|
||||
|
||||
// The server's end. One producer: the apply thread, by construction.
|
||||
class EventRingProducer {
|
||||
public:
|
||||
|
||||
@@ -822,6 +822,55 @@ TEST(SessionTest, TheEventRingCarriesTheThreeReverseCallbacks) {
|
||||
EXPECT_EQ(session.eventIn.DroppedEvents(), 0u);
|
||||
}
|
||||
|
||||
// P5c ev (CONTRACT-P5C §1, §4.5): the fourth event kind's unit round-trip, in the same shape
|
||||
// as the three above - the producer writes the head plus the inline NUL-terminated message,
|
||||
// the consumer reads every field back, the in-segment offset of the message stays inside
|
||||
// SEG_EVENT, and a drained ring reports zero drops.
|
||||
TEST(SessionTest, TheEventRingCarriesAGlError) {
|
||||
SessionFixture session;
|
||||
ASSERT_TRUE(session.Build(TestSizes()));
|
||||
|
||||
const char message[] = "DirectVulkan: vkCreateGraphicsPipelines failed";
|
||||
const std::uint32_t messageBytes = static_cast<std::uint32_t>(sizeof(message)); // NUL included
|
||||
ASSERT_LT(messageBytes, kEventGlErrorMaxMessageBytes);
|
||||
{
|
||||
void* slot = session.eventOut.Reserve(kEventGlError,
|
||||
sizeof(EventGlErrorHead) + messageBytes);
|
||||
ASSERT_NE(slot, nullptr);
|
||||
EventGlErrorHead head{};
|
||||
head.Code = 4; // ErrorCode::InvalidOperation, widened
|
||||
head.MessageBytes = messageBytes;
|
||||
std::memcpy(slot, &head, sizeof(head));
|
||||
std::memcpy(static_cast<std::uint8_t*>(slot) + sizeof(head), message, messageBytes);
|
||||
}
|
||||
session.eventOut.PublishAndNotify(session.clientTransport->SelfDoorbell(),
|
||||
session.Control().producerParked);
|
||||
|
||||
RingRecordView view{};
|
||||
ASSERT_TRUE(session.eventIn.Pop(view));
|
||||
EXPECT_EQ(view.kind, kEventGlError);
|
||||
// The record's payload is rounded up to the ring's 8-byte alignment; the head's
|
||||
// MessageBytes is the authoritative inline length.
|
||||
ASSERT_GE(view.payloadSize, sizeof(EventGlErrorHead) + messageBytes);
|
||||
EventGlErrorHead head{};
|
||||
std::memcpy(&head, view.payload, sizeof(head));
|
||||
EXPECT_EQ(head.Code, 4u);
|
||||
EXPECT_EQ(head.MessageBytes, messageBytes);
|
||||
const char* inlineMessage =
|
||||
reinterpret_cast<const char*>(static_cast<const std::uint8_t*>(view.payload) + sizeof(head));
|
||||
EXPECT_STREQ(inlineMessage, message);
|
||||
// The offset a consumer would resolve the inline message at: inside SEG_EVENT and past
|
||||
// its control page, never a host address.
|
||||
const std::uint64_t offset = session.eventIn.OffsetInSegment(inlineMessage);
|
||||
EXPECT_GE(offset, sizeof(RingControl));
|
||||
EXPECT_LT(offset, session.clientSegments.AnnouncedSize(SessionSegmentSlot::Event));
|
||||
|
||||
EXPECT_FALSE(session.eventIn.Pop(view));
|
||||
session.eventIn.Drained();
|
||||
EXPECT_FALSE(session.eventIn.RingIsFull());
|
||||
EXPECT_EQ(session.eventIn.DroppedEvents(), 0u);
|
||||
}
|
||||
|
||||
TEST(SessionTest, AFullEventRingLatchesTheFlagRatherThanDecidingWhatToDoAboutIt) {
|
||||
SessionFixture session;
|
||||
ASSERT_TRUE(session.Build(TestSizes()));
|
||||
|
||||
Reference in New Issue
Block a user