Merge branch 'p5c-ev' into feat/disaggregated (P5c ev: SEG_EVENT reverse channel)

This commit is contained in:
2026-09-17 03:55:46 -04:00
12 changed files with 646 additions and 71 deletions
@@ -20,6 +20,8 @@
#if MOBILEGL_PIPE_PUSH #if MOBILEGL_PIPE_PUSH
// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against. // P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against.
#include <MG_Pipe/PipeApply.h> #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 #endif
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads. // P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads.
@@ -11664,8 +11666,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* depthTexture = defaultFBOInfo->depthAttachment.get(); auto* depthTexture = defaultFBOInfo->depthAttachment.get();
auto* stencilTexture = defaultFBOInfo->stencilAttachment.get(); auto* stencilTexture = defaultFBOInfo->stencilAttachment.get();
#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 (depthTexture) depthTexture->SetInternalFormat(depthFormat);
if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat); if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat);
}
MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment " MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment "
"formats depth=%d stencil=%d", "formats depth=%d stencil=%d",
depthBits, stencilBits, floatDepth ? 1 : 0, static_cast<int>(depthFormat), depthBits, stencilBits, floatDepth ? 1 : 0, static_cast<int>(depthFormat),
@@ -11,6 +11,11 @@
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h" #include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.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 defined(__has_include)
#if __has_include(<vulkan/vk_enum_string_helper.h>) #if __has_include(<vulkan/vk_enum_string_helper.h>)
#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.width) *
static_cast<SizeT>(defaultFramebufferExtent.height) * 4; 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; TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) { switch (m_depthStencilFormat) {
case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D24_UNORM_S8_UINT:
@@ -300,11 +300,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
depthFormat = TextureInternalFormat::Depth24Stencil8; depthFormat = TextureInternalFormat::Depth24Stencil8;
break; 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: // The default FBO's stencil attachment must track the swapchain extent:
// FramebufferObject::CheckCompleteness requires every valid attachment // FramebufferObject::CheckCompleteness requires every valid attachment
@@ -324,11 +319,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stencilFormat = depthFormat; stencilFormat = depthFormat;
break; break;
} }
#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()); auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
stencilTex->SetInternalFormat(stencilFormat); stencilTex->SetInternalFormat(stencilFormat);
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, { stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{extentWidth, extentHeight, 1}, {extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size defaultAttachmentByteSize}); // TODO: 4 is format size
}
} }
@@ -11,6 +11,10 @@
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h" #include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.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/ProgramObject.h"
#include "MG_State/GLState/TextureState/TextureObject1D.h" #include "MG_State/GLState/TextureState/TextureObject1D.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h" #include "MG_State/GLState/TextureState/TextureObject2D.h"
@@ -1072,7 +1076,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// changed a byte of it. // changed a byte of it.
bufferObject->EnsureGpuResidentStorage(); bufferObject->EnsureGpuResidentStorage();
if (imageBinding.Access != GL_READ_ONLY) { 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(); bufferObject->MarkGpuWritten();
#endif
} }
BufferSlice slice{}; BufferSlice slice{};
@@ -1228,7 +1238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// results are visible without a readback path, exactly as for a capture buffer. // results are visible without a readback path, exactly as for a capture buffer.
bufferObject->EnsureGpuResidentStorage(); bufferObject->EnsureGpuResidentStorage();
// ... and the read that follows has to wait for this draw or dispatch to retire. // ... 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(); bufferObject->MarkGpuWritten();
#endif
BufferSlice slice{}; BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) { if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
@@ -15,6 +15,10 @@
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.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/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h" #include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/SamplerState/SamplerObject.h" #include "MG_State/GLState/SamplerState/SamplerObject.h"
@@ -11675,7 +11679,13 @@ void main() {
// have happened, so the buffer is also flagged for the wait that a later CPU // 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. // read has to perform - the capture is a GPU write like any shader's.
bufferObject->EnsureGpuResidentStorage(); 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(); bufferObject->MarkGpuWritten();
#endif
BufferSlice slice{}; BufferSlice slice{};
if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) { if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) {
MGLOG_E_ONCE("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i); MGLOG_E_ONCE("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i);
+38
View File
@@ -48,6 +48,9 @@
// lanes must keep answering exactly what they answered before. // lanes must keep answering exactly what they answered before.
#include <MG_Remote/Client/CapsMirror.h> #include <MG_Remote/Client/CapsMirror.h>
#include <MG_Remote/Client/WireTables.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 #endif
#include <atomic> #include <atomic>
@@ -662,7 +665,18 @@ namespace MobileGL::MG_Pipe {
// the resource subsystem bit would make the vertex-input subsystem emit null handles // 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 // 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. // 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(); MGPipeInstallClientResourceCallbacks();
}
#else
MGPipeInstallClientResourceCallbacks();
#endif
MGPipeResourceTrackerInstance().Acquire(buffer); MGPipeResourceTrackerInstance().Acquire(buffer);
} }
@@ -1752,6 +1766,16 @@ namespace MobileGL::MG_Pipe {
void PipeInputs::InvalidateCompileEnv() { void PipeInputs::InvalidateCompileEnv() {
MGP_STICKY_FORWARD_PULL(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(); if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
} }
@@ -1763,6 +1787,20 @@ namespace MobileGL::MG_Pipe {
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) { void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
MGP_STICKY_FORWARD_PULL(RecordError); 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(); auto* ctx = LiveContext();
if (ctx == nullptr) { if (ctx == nullptr) {
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code)); MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
+131 -14
View File
@@ -49,7 +49,16 @@
#include <Config.h> #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 <cstdint>
#include <cstdlib>
namespace MobileGL::MG_Pipe { 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 // 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: // 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 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) { inline void MGPipeClientOnBufferWriteback(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes) {
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res); auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
if (buffer == nullptr) { if (buffer == nullptr) {
@@ -557,17 +579,61 @@ namespace MobileGL::MG_Pipe {
res.Slot, res.Gen); res.Slot, res.Gen);
return; return;
} }
if (bytes.Seg != kMGHostSpanSegNone) { void* bytePtr = nullptr;
MGLOG_E_ONCE("MGPipe: OnBufferWriteback carried a transport segment (%u); P3a is monolith only", 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); bytes.Seg);
return; return;
} }
// Monolith: Seg is kMGHostSpanSegNone and Offset IS the address of the backend's #endif
// mapped bytes (MGPipeTypes.h says so in as many words). Under a transport the buffer->WritebackFromBackend(DataPtr{bytePtr, static_cast<SizeT>(bytes.Size)},
// 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)); static_cast<SizeT>(offset));
} }
@@ -600,16 +666,67 @@ namespace MobileGL::MG_Pipe {
buffer->MarkGpuWritten(); buffer->MarkGpuWritten();
} }
// Installed once, and never over an entry a backend already claimed: these two are the // MONOLITH ONLY (CONTRACT-P5C §4.1): with an active transport the three reverse entries
// CLIENT's implementations of a backend -> frontend callback, so the backend installs // of gMGPipeCallbacks are the SERVER session's producer callbacks and the client
// the rest of the table and these two answer for it. // 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() { inline void MGPipeInstallClientResourceCallbacks() {
if (gMGPipeCallbacks.OnBufferWriteback == nullptr) { const auto install = [](auto& entry, auto* fn, const char* name) {
gMGPipeCallbacks.OnBufferWriteback = &MGPipeClientOnBufferWriteback; 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) { 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 } // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH #endif // MOBILEGL_PIPE_PUSH
+100 -10
View File
@@ -19,7 +19,12 @@
#include "WireTables.h" #include "WireTables.h"
#include <MGGitHash.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_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 <MG_Util/Debug/Log.h>
#include <atomic> #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 // 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 // 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). // 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) { Uint32 DrainEventRing(Transport::EventRingConsumer& events) {
if (!events.Valid()) return 0; if (!events.Valid()) return 0;
Uint32 delivered = 0; Uint32 delivered = 0;
@@ -197,19 +254,18 @@ namespace MobileGL::MG_Remote::Client {
static_cast<unsigned long long>(view.payloadSize - sizeof(*head))); static_cast<unsigned long long>(view.payloadSize - sizeof(*head)));
std::abort(); std::abort();
} }
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
// The blobref names SEG_EVENT and the IN-SEGMENT offset of those inline // 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 // bytes - never a host address (R-2's rule B), which is the whole reason
// EventRingConsumer exposes OffsetInSegment at all. // EventRingConsumer exposes OffsetInSegment at all. The consumer's
// kSegEvent arm resolves it through this session's own SegmentTable.
MG_Pipe::MGPBlobRef blob{}; MG_Pipe::MGPBlobRef blob{};
blob.Seg = Wire::kSegEvent; blob.Seg = Wire::kSegEvent;
blob.Offset = events.OffsetInSegment(bytes); blob.Offset = events.OffsetInSegment(bytes);
blob.Size = head->Size; blob.Size = head->Size;
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback( MG_Pipe::MGPipeClientOnBufferWriteback(
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen}, MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
head->Offset, blob); head->Offset, blob);
++delivered; ++delivered;
}
break; break;
} }
case Transport::kEventGpuWritten: { case Transport::kEventGpuWritten: {
@@ -224,22 +280,19 @@ namespace MobileGL::MG_Remote::Client {
static_cast<unsigned long long>(tail)); static_cast<unsigned long long>(tail));
std::abort(); std::abort();
} }
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten != nullptr) {
// EventRange and MGPRange are the same two Uint64s (EventRing.h:61-66 // 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 // 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. // a second array a later reader could get out of step with.
const auto* ranges = reinterpret_cast<const MG_Pipe::MGPRange*>( const auto* ranges = reinterpret_cast<const MG_Pipe::MGPRange*>(
static_cast<const Uint8*>(view.payload) + sizeof(*head)); static_cast<const Uint8*>(view.payload) + sizeof(*head));
MG_Pipe::gMGPipeCallbacks.OnGpuWritten( MG_Pipe::MGPipeClientOnGpuWritten(
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen}, MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
static_cast<Uint>(head->RangeCount), ranges); static_cast<Uint>(head->RangeCount), ranges);
++delivered; ++delivered;
}
break; break;
} }
case Transport::kEventSurfaceChanged: { case Transport::kEventSurfaceChanged: {
if (view.payloadSize < sizeof(Transport::EventSurfaceChangedHead)) break; if (view.payloadSize < sizeof(Transport::EventSurfaceChangedHead)) break;
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
const auto* head = const auto* head =
static_cast<const Transport::EventSurfaceChangedHead*>(view.payload); static_cast<const Transport::EventSurfaceChangedHead*>(view.payload);
MG_Pipe::MGPSurfaceInfo info{}; MG_Pipe::MGPSurfaceInfo info{};
@@ -249,14 +302,49 @@ namespace MobileGL::MG_Remote::Client {
info.Samples = head->Samples; info.Samples = head->Samples;
info.Layers = head->Layers; info.Layers = head->Layers;
info.IsDefault = head->IsDefault; info.IsDefault = head->IsDefault;
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info); ApplySurfaceChangedToClient(info);
++delivered; ++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; break;
} }
default: default:
MGLOG_W("MG_Remote client: reverse-channel record kind %u is not consumed in " 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)); static_cast<unsigned>(view.kind));
break; break;
} }
@@ -944,6 +1032,8 @@ namespace MobileGL::MG_Remote::Client {
Transport::EventRingConsumer& ClientSession::Events() { return m_events; } Transport::EventRingConsumer& ClientSession::Events() { return m_events; }
Wire::SegmentTable& ClientSession::Segments() { return m_segments; }
Transport::RingControl* ClientSession::Control() { return m_shm.CmdControl(); } Transport::RingControl* ClientSession::Control() { return m_shm.CmdControl(); }
Transport::SessionSegments& ClientSession::Shm() { return m_shm; } Transport::SessionSegments& ClientSession::Shm() { return m_shm; }
@@ -202,6 +202,11 @@ namespace MobileGL::MG_Remote::Client {
// OnSurfaceChanged. Drained by the GL thread between verbs. // OnSurfaceChanged. Drained by the GL thread between verbs.
Transport::EventRingConsumer& Events(); 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::RingControl* Control();
Transport::SessionSegments& Shm(); Transport::SessionSegments& Shm();
Transport::ITransport* Control_Plane(); Transport::ITransport* Control_Plane();
+174
View File
@@ -16,9 +16,11 @@
#include <Config.h> #include <Config.h>
#include <MGGitHash.h> #include <MGGitHash.h>
#include <MG_Pipe/MGPipeCallbacks.h>
#include <MG_Util/Debug/Log.h> #include <MG_Util/Debug/Log.h>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <vector> #include <vector>
namespace MobileGL::MG_Remote::Server { namespace MobileGL::MG_Remote::Server {
@@ -155,6 +157,120 @@ namespace MobileGL::MG_Remote::Server {
std::abort(); 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; ServerSession* g_active = nullptr;
} // namespace } // namespace
@@ -337,6 +453,19 @@ namespace MobileGL::MG_Remote::Server {
m_accepted = true; m_accepted = true;
g_active = this; 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"); LogMemory("accept");
if (!CallMaskIsSet()) { if (!CallMaskIsSet()) {
@@ -415,6 +544,17 @@ namespace MobileGL::MG_Remote::Server {
// Uninstall AFTER the apply thread has joined, never before: a record still in // 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). // flight can still resolve a segment offset (table 3's fourth column).
Wire::SegmentTable::UninstallProcessResolver(); 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_consumer.Detach();
m_commands = Transport::RingConsumer(); m_commands = Transport::RingConsumer();
@@ -491,6 +631,40 @@ namespace MobileGL::MG_Remote::Server {
m_consumer.NotifyClient(); 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 // 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. // Watermark do not ring: a client parked in WaitForPresentAck(kWaitForever) needs the pair.
void ServerSession::AdvanceCompletedFrame(Uint64 serial) { void ServerSession::AdvanceCompletedFrame(Uint64 serial) {
+12
View File
@@ -158,6 +158,18 @@ namespace MobileGL::MG_Remote::Server {
// caller and therefore compiles for every wrong pairing; the session is the thing // caller and therefore compiles for every wrong pairing; the session is the thing
// that knows which bell belongs to the client. // that knows which bell belongs to the client.
void PublishEvents(); 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(); Transport::ITransport* Control_Plane();
// completedFrameSerial / presentAckSerial: the two watermarks only the server can // completedFrameSerial / presentAckSerial: the two watermarks only the server can
+18 -1
View File
@@ -42,12 +42,14 @@
namespace MobileGL::MG_Remote::Transport { namespace MobileGL::MG_Remote::Transport {
// Record kinds on SEG_EVENT. 0 is kRingPadRecordKind and can never be an // 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 { enum EventKind : std::uint16_t {
kEventNone = 0, kEventNone = 0,
kEventBufferWriteback = 1, // MGPipeCallbacks::OnBufferWriteback kEventBufferWriteback = 1, // MGPipeCallbacks::OnBufferWriteback
kEventGpuWritten = 2, // MGPipeCallbacks::OnGpuWritten kEventGpuWritten = 2, // MGPipeCallbacks::OnGpuWritten
kEventSurfaceChanged = 3, // MGPipeCallbacks::OnSurfaceChanged kEventSurfaceChanged = 3, // MGPipeCallbacks::OnSurfaceChanged
kEventGlError = 4, // PipeInputs::RecordError, posted by the server session
}; };
// The 8-byte {slot, gen} pair, mirrored (MGPipeHandles.h:54-65). // 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"); 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. // The server's end. One producer: the apply thread, by construction.
class EventRingProducer { class EventRingProducer {
public: public:
+49
View File
@@ -822,6 +822,55 @@ TEST(SessionTest, TheEventRingCarriesTheThreeReverseCallbacks) {
EXPECT_EQ(session.eventIn.DroppedEvents(), 0u); 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) { TEST(SessionTest, AFullEventRingLatchesTheFlagRatherThanDecidingWhatToDoAboutIt) {
SessionFixture session; SessionFixture session;
ASSERT_TRUE(session.Build(TestSizes())); ASSERT_TRUE(session.Build(TestSizes()));