[Merge] (P5b): integrate i1 alongside f1 and r2, preserving the combined slot partition

This commit is contained in:
2026-09-16 13:25:44 -04:00
10 changed files with 639 additions and 63 deletions
+262 -36
View File
@@ -10,8 +10,8 @@
//
// THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12):
// class A 2 slots answered locally from the caps mirror, never emitted, never Fatal
// class B 5 slots emitted
// class C 64 slots Fatal{UnmigratedVerb, "<slot>"}
// class B 5 slots emitted (+ 7 flipped by P5b package i1, below)
// class C 64 slots Fatal{UnmigratedVerb, "<slot>"} (- the same 7)
// The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that
// changes class without changing the arithmetic is a build break rather than a behaviour
// change nobody reviewed.
@@ -35,6 +35,13 @@
#include <MG_State/GLState/BufferState/BufferObject.h>
#include <MG_State/GLState/Core.h>
// P5b i1: the emitters below name a texture's, a buffer's and a program's HANDLE beside the GL
// arguments (rule D). The handle comes from the client's own slot allocator, which is
// MG_Impl/Pipe's - the same table MG_Impl/Pipe/ImageEmit.h's set_shader_images reads, so the
// two records name one identity rather than two.
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#include <MG_State/GLState/TextureState/TextureState.h>
#include <cstdlib>
#include <cstring>
@@ -665,6 +672,231 @@ namespace MobileGL::MG_Remote::Client {
LogE2ControlLine(g_presentOrdinal);
}
// =============================================================================
// CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block
// (MG_Remote/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows.
// =============================================================================
//
// RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record
// carries the GL arguments verbatim - the enums as tokens, the GL names the backend
// keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's
// ServerVerbSink reproduces the backend call the monolith makes. The backend keeps
// reading the frontend state it reads today through the BARRIER-PULLED fields of its
// verb class, which the record's own verb stamp is what makes legal. So a migration is
// one emitter here plus one sink body there, and nothing in the backend moves.
//
// THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks
// below dispatch on the GL NAME - that is the whole point of carrying it - so a handle
// is carried for P7's sake only. FindByLifetimeId answers the handle the resource
// subsystem has already published for this object and kMGPipeNullHandle when it has
// published none; Acquire would MINT one here instead, at a call site that emits no
// create record, and the server would then be handed an identity it has never seen.
// A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a
// true statement, rather than a slot nobody allocated.
MG_Pipe::MGPipeHandle PublishedTextureHandle(
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (!texture) return MG_Pipe::kMGPipeNullHandle;
return MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture,
texture->GetLifetimeId());
}
// glBindImageTexture. Emitted AT THE CALL, after the frontend has written the unit's
// ImageTextureBinding and MGP_FILL(BindImageTexture) has run - the record's verb
// boundary is what makes the server's read of that binding legal. set_shader_images
// (the draw-prep set) still travels at the next validate, untouched: that record
// describes a resolved unit for the draw, this one reproduces a call.
//
// NO PRE-VERB HOOK, AND THAT IS DELIBERATE. b1's two hooks describe "the work the
// record is ABOUT TO START" - PushPersistentMapsBeforeVerb publishes bytes an
// application wrote through a coherent map, MarkGpuWrites* builds the GPU-write set.
// A bind starts no shader and reads no buffer; the dispatch that later reads this image
// is the verb that carries both hooks, and running them here as well would push the
// same maps twice per dispatch and inflate b1's per-row counters.
void EmitBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered,
GLint layer, GLenum access, GLenum format) {
ClientSession& session = RequireSession("BindImageTexture");
MG_Pipe::MGPImageBind record{};
// The unit's binding is the frontend's and has just been written by the caller, so
// the texture this record names is the one the server's SyncImageTextureBinding
// will pull for the same unit. Read from MG_State::pGLContext and NOT through
// MGB_CTX: on this side of a split MGB_CTX is gPipeInputs, which is the SERVER's
// view, and the client asking it a question is how the two halves come to disagree.
if (MG_State::pGLContext != nullptr) {
record.Res = PublishedTextureHandle(
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit)).Texture);
}
record.Unit = static_cast<Uint32>(unit);
// The GL name the application passed, verbatim - what the ES slot is handed as
// `texture` and currently ignores. Never an identity (ARCHITECTURE 4.2.1).
record.GlName = static_cast<Uint32>(texture);
record.Level = static_cast<Int32>(level);
record.Layer = static_cast<Int32>(layer);
// THE GL ACCESS TOKEN, not MGPImageView::Access's three-value encoding (table 0's
// MGPImageBind::Access row). Two records, two jobs.
record.Access = static_cast<Uint32>(access);
record.Format = static_cast<Uint32>(format);
record.Layered = layered != GL_FALSE ? 1 : 0;
session.EmitAndWait(MG_Pipe::MGPWireOp::BindShaderImage, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
// glDispatchCompute. THE HOOK ORDER IS b1's AND IS INHERITED FROM THE CLASS-C STUB
// VERBATIM: push the persistent maps (they produce resource_subdata records that must
// precede the verb on SEG_CMD), then the dispatch mark walk, then the record. The stub
// carried both calls before its Fatal precisely so that the package which flipped this
// slot would inherit a call site that was already correct.
void EmitDispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
ClientSession& session = RequireSession("DispatchCompute");
PushPersistentMapsBeforeVerb();
MarkGpuWritesForDispatch();
MG_Pipe::MGPGridInfo record{};
record.GridX = static_cast<Uint32>(numGroupsX);
record.GridY = static_cast<Uint32>(numGroupsY);
record.GridZ = static_cast<Uint32>(numGroupsZ);
// Block* STAY 0 IN P5b (contract i1): the local size is a link artifact the backend
// reads from its own program, and a client-minted copy would be a second statement
// of it. P7's Magma may fill it from the reflection archive.
record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle;
record.IndirectOffset = 0;
record.IsIndirect = 0;
session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
}
void EmitDispatchComputeIndirect(GLintptr indirect) {
ClientSession& session = RequireSession("DispatchComputeIndirect");
PushPersistentMapsBeforeVerb();
MarkGpuWritesForDispatch();
MG_Pipe::MGPGridInfo record{};
// The counts come from the GL_DISPATCH_INDIRECT_BUFFER, so the three grid fields are
// 0 and IsIndirect is what says so; the sink dispatches on it and calls
// glDispatchComputeIndirect with the offset the application spelled.
record.IsIndirect = 1;
record.IndirectOffset = static_cast<Uint64>(indirect);
record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle;
if (MG_State::pGLContext != nullptr) {
const auto& bound =
MG_State::pGLContext
->GetBufferBindingSlot(::MobileGL::BufferTarget::DispatchIndirect)
.GetBoundObject();
if (bound) {
record.IndirectBuffer = MG_Pipe::MGPipeSlots().FindByLifetimeId(
MG_Pipe::MGPipeKind::Buffer, bound->GetLifetimeId());
}
}
session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
}
// glMemoryBarrier / glMemoryBarrierByRegion. The bits cross VERBATIM: the frontend has
// already validated them and already folds glTextureBarrier onto the same field
// (GL_Drawing.cpp:920-937), and Espryt's atomic-counter lowering - the counter bit
// implying the storage bit - stays inside the backend where the reason for it lives
// (DirectGLES.cpp:8837). A client that pre-lowered would be answering a driver question
// from the wrong side and the two arms would stop being byte-identical.
//
// No pre-verb hook: a barrier orders memory the GPU already holds. It starts no shader
// and reads no mapped buffer.
void EmitMemoryBarrier(GLbitfield barriers) {
ClientSession& session = RequireSession("MemoryBarrier");
MG_Pipe::MGPMemoryBarrier record{};
record.Bits = static_cast<Uint32>(barriers);
record.ByRegion = 0;
session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr,
0, nullptr, 0, nullptr);
}
void EmitMemoryBarrierByRegion(GLbitfield barriers) {
ClientSession& session = RequireSession("MemoryBarrierByRegion");
MG_Pipe::MGPMemoryBarrier record{};
record.Bits = static_cast<Uint32>(barriers);
record.ByRegion = 1;
session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr,
0, nullptr, 0, nullptr);
}
// glCopyImageSubData -> resource_copy_region (53), which P5b rules is glCopyImageSubData
// ONLY (contract §6.4; the framebuffer-sourced copies are f1's row 76).
void EmitCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
ClientSession& session = RequireSession("CopyImageSubData");
// ID-57's SHAPE: REFUSED BY NAME, BEFORE ANY EMISSION. GL 4.6 core 18.3.2 accepts
// GL_RENDERBUFFER as either endpoint, and an endpoint is a sum type for exactly that
// reason - but no sticky forward hands out a renderbuffer object, so the sink has no
// way to rebuild one from a name, and none was measured. P7 is where the backend
// takes handles and this arm becomes ordinary. The sink refuses the same shape by
// the same name if a record ever reaches it (defence on both sides of one wire).
if (src.IsRenderbuffer() || dst.IsRenderbuffer()) {
UnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER");
}
MG_Pipe::MGPCopyRegion record{};
record.Src = PublishedTextureHandle(src.Texture);
record.Dst = PublishedTextureHandle(dst.Texture);
// The GL names beside the handles: the key MGB_CTX->GetTextureObject(name) takes on
// the far side (a BARRIER-PULLED sticky forward, counted in `rsp`, retired by P7).
record.SrcGlName =
src.Texture ? static_cast<Uint32>(src.Texture->GetExternalIndex()) : 0u;
record.DstGlName =
dst.Texture ? static_cast<Uint32>(dst.Texture->GetExternalIndex()) : 0u;
// The GL targets verbatim, in a Uint16 - every GL texture target fits one. NOT
// MGPipeResourceTarget: the sink only ever forwards these to a slot that takes GL
// enums, and the tree has no resource-target -> GL-enum inverse to spend on them.
record.SrcTarget = static_cast<Uint16>(srcTarget);
record.DstTarget = static_cast<Uint16>(dstTarget);
record.SrcLevel = static_cast<Uint16>(srcLevel);
record.DstLevel = static_cast<Uint16>(dstLevel);
// SrcBox is {srcX, srcY, srcZ, w, h, d}: the source origin AND the extent, which is
// one extent for both endpoints (GL spells the copy's size once).
record.SrcBox = MG_Pipe::MGPBox{srcX, srcY, srcZ, static_cast<Uint32>(srcWidth),
static_cast<Uint32>(srcHeight),
static_cast<Uint32>(srcDepth)};
record.DstX = dstX;
record.DstY = dstY;
record.DstZ = dstZ;
session.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCopyRegion, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
// glShaderStorageBlockBinding -> set_storage_block_binding (75), the ONE content-carrying
// row P5b adds. The block is NAMED, not indexed, because the application's index is the
// frontend interface-query enumeration's and no backend shares that index space
// (BackendObject.h:216-221) - the name is the one coordinate all three agree on.
void EmitShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName,
GLuint storageBlockBinding) {
ClientSession& session = RequireSession("ShaderStorageBlockBinding");
// The backend slot's own first line (DirectGLES.cpp:9201), kept here so a null name
// never becomes a zero-size blob - which rule A forbids spelling at all.
if (storageBlockName == nullptr) return;
MG_Pipe::MGPStorageBlockBinding record{};
record.GlName = static_cast<Uint32>(program);
record.Binding = static_cast<Uint32>(storageBlockBinding);
record.ShaderCso = MG_Pipe::kMGPipeNullHandle;
if (MG_State::pGLContext != nullptr) {
const auto& programObject = MG_State::pGLContext->GetProgramObject(program);
if (programObject) {
record.ShaderCso = MG_Pipe::MGPipeSlots().FindByLifetimeId(
MG_Pipe::MGPipeKind::ShaderCso, programObject->GetLifetimeId());
}
}
// Size = strlen + 1: THE NUL TRAVELS (contract table 0's block-name row). The
// decoder re-terminates into a bounded local and refuses a run whose last byte is
// not NUL, so the two sides agree on where the name ends.
const Uint64 nameBytes = static_cast<Uint64>(std::strlen(storageBlockName)) + 1ull;
record.Name = session.Encoder().StageBytes(storageBlockName, nameBytes);
session.EmitAndWait(MG_Pipe::MGPWireOp::SetStorageBlockBinding, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
// =============================================================================
// CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER.
// =============================================================================
@@ -704,7 +936,8 @@ namespace MobileGL::MG_Remote::Client {
}
// =============================================================================
// CLASS C - Fatal{UnmigratedVerb}. 64 slots: 63 in GLFunctionsTable + SetSwapInterval.
// CLASS C - Fatal{UnmigratedVerb}. 57 slots: 56 in GLFunctionsTable + SetSwapInterval
// (64 at the P5b contract commit, less the seven package i1 flipped to class B).
// =============================================================================
//
// PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md,
@@ -755,17 +988,12 @@ namespace MobileGL::MG_Remote::Client {
X(DrawElementsIndirect, void, (GLenum, GLenum, const void*)) \
X(DrawArraysIndirect, void, (GLenum, const void*))
// DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below
// because they carry b1's dispatch hook before the Fatal.
#define MGR_UNMIGRATED_I1_SLOTS(X) \
X(BindImageTexture, void, (GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum)) \
X(CopyImageSubData, void, \
(const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, \
const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, GLsizei, GLsizei, \
GLsizei)) \
X(MemoryBarrier, void, (GLbitfield)) \
X(MemoryBarrierByRegion, void, (GLbitfield)) \
X(ShaderStorageBlockBinding, void, (GLuint, const GLchar*, GLuint))
// i1 HAS LANDED: the list is EMPTY and all seven slots are class B (the five emitters
// above plus the two compute ones). It is kept as an empty macro rather than deleted so
// that MGR_UNMIGRATED_GL_SLOTS' union, kUnmigratedI1's arithmetic and the ownership
// static_assert below all keep their shape - and so the next package to need a row here
// (a P5b wave-3 image/compute slot) has the partition to put it in.
#define MGR_UNMIGRATED_I1_SLOTS(X)
#define MGR_UNMIGRATED_T2_SLOTS(X) \
X(PatchParameteri, void, (GLenum, GLint)) \
@@ -829,31 +1057,18 @@ namespace MobileGL::MG_Remote::Client {
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_DEFINE_UNMIGRATED)
#undef MGR_DEFINE_UNMIGRATED
// THE TWO COMPUTE SLOTS CARRY b1's DISPATCH HOOK BEFORE THE FATAL, and this is stated
// rather than hidden. MarkGpuWritesForDispatch() belongs immediately before the
// dispatch record, and the dispatch record is class C until i1 lands - so the call site
// is here, in the right place, and is UNREACHABLE-IN-EFFECT: the abort follows it. The
// package that moves DispatchCompute into class B (i1: launch_grid, opcode 60) replaces
// the Fatal and inherits a call site that is already correct rather than discovering
// that the mark walk was never wired.
void DispatchCompute_Unmigrated(GLuint, GLuint, GLuint) {
PushPersistentMapsBeforeVerb();
MarkGpuWritesForDispatch();
UnmigratedVerbFatal("DispatchCompute");
}
void DispatchComputeIndirect_Unmigrated(GLintptr) {
PushPersistentMapsBeforeVerb();
MarkGpuWritesForDispatch();
UnmigratedVerbFatal("DispatchComputeIndirect");
}
// THE TWO COMPUTE SLOTS' class-C stubs are GONE (P5b i1): they carried b1's dispatch
// hook before their Fatal so that the package flipping them would inherit a call site
// that was already correct, and EmitDispatchCompute / EmitDispatchComputeIndirect above
// are that inheritance - same two calls, same order, the record where the Fatal was.
void SetSwapInterval_Unmigrated(Int) { UnmigratedVerbFatal("SetSwapInterval"); }
// The counts, as arithmetic. MGR_COUNT_ONE expands to `+ 1` per row.
#define MGR_COUNT_ONE(Name, Ret, Sig) +1
constexpr Uint32 kUnmigratedD1 = 0 MGR_UNMIGRATED_D1_SLOTS(MGR_COUNT_ONE);
// + DispatchCompute, DispatchComputeIndirect, written out by hand.
constexpr Uint32 kUnmigratedI1 = 0 MGR_UNMIGRATED_I1_SLOTS(MGR_COUNT_ONE) + 2;
// i1 landed: the list is empty and the two hand-written compute stubs are gone with it.
constexpr Uint32 kUnmigratedI1 = 0 MGR_UNMIGRATED_I1_SLOTS(MGR_COUNT_ONE);
constexpr Uint32 kUnmigratedT2 = 0 MGR_UNMIGRATED_T2_SLOTS(MGR_COUNT_ONE);
constexpr Uint32 kUnmigratedF1 = 0 MGR_UNMIGRATED_F1_SLOTS(MGR_COUNT_ONE);
// + SetSwapInterval, written out by hand.
@@ -866,7 +1081,9 @@ namespace MobileGL::MG_Remote::Client {
// The emitted counts, PER OWNER. P5's five are c1's; each P5b package raises its own.
constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present
constexpr Uint32 kEmittedSlotsD1 = 0;
constexpr Uint32 kEmittedSlotsI1 = 0;
// P5b i1: BindImageTexture, DispatchCompute, DispatchComputeIndirect, MemoryBarrier,
// MemoryBarrierByRegion, CopyImageSubData, ShaderStorageBlockBinding.
constexpr Uint32 kEmittedSlotsI1 = 7;
constexpr Uint32 kEmittedSlotsT2 = 0;
constexpr Uint32 kEmittedSlotsF1 = 11;
constexpr Uint32 kEmittedSlots =
@@ -899,8 +1116,6 @@ namespace MobileGL::MG_Remote::Client {
MGR_UNMIGRATED_GL_SLOTS(MGR_ASSIGN_UNMIGRATED)
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_ASSIGN_UNMIGRATED)
#undef MGR_ASSIGN_UNMIGRATED
table.GL.DispatchCompute = &DispatchCompute_Unmigrated;
table.GL.DispatchComputeIndirect = &DispatchComputeIndirect_Unmigrated;
table.SetSwapInterval = &SetSwapInterval_Unmigrated;
// ---- class A
@@ -920,6 +1135,7 @@ namespace MobileGL::MG_Remote::Client {
table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
table.Present = &EmitPresent;
<<<<<<< HEAD
// ---- f1 ----
table.GL.ClearBufferfi = &EmitClearBufferfi;
table.GL.ClearBufferfv = &EmitClearBufferfv;
@@ -932,6 +1148,16 @@ namespace MobileGL::MG_Remote::Client {
table.GL.CopyTexImage2D = &EmitCopyTexImage2D;
table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D;
table.GL.GenerateMipmap = &EmitGenerateMipmap;
=======
// ---- class B, P5b package i1 (kEmittedSlotsI1 = 7)
table.GL.BindImageTexture = &EmitBindImageTexture;
table.GL.DispatchCompute = &EmitDispatchCompute;
table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect;
table.GL.MemoryBarrier = &EmitMemoryBarrier;
table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion;
table.GL.CopyImageSubData = &EmitCopyImageSubData;
table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding;
>>>>>>> p5b/i1
return table;
}