[Feat] (Pipe): land the P1 contract - PipeInputsSwitch.h with MGB_CTX, the 63-field PipeInputs block with type-identical accessors, FillPoints.def and its G5b generator, the MOBILEGL_PIPE_PUSH/VERIFY options and the three verify knobs; pull build unchanged

- MG_Pipe/PipeInputsSwitch.h is the strangler switch (ARCHITECTURE.md 9.2): MGB_CTX is the
  live GLContext in the pull build and &gPipeInputs under MOBILEGL_PIPE_PUSH, so the pull
  arm's pGLContext spelling stays outside MG_Backend/ and purity gate C's grep.
- MG_Backend/MGPipe/PipeInputs.h holds one struct with every accessor a backend reads (63:
  the 61 Coverage.def rows plus GetBoundTransformFeedbackLifetimeId and
  HasOpenTransformFeedbackSpan), each keeping its GLContext name, parameters and return
  type so the site conversion is type-neutral; V fields are copied values, O fields are
  SharedPtr copies or pointers into the context, the seven F fields forward to the live
  context from MG_Impl/Pipe/PipeFill.cpp and are the only sticky ones (Coverage.def's
  MGP_COVERAGE_STICKY_LIST argues each: argument-keyed lookups and reverse-channel writes,
  never a version or generation counter).
- MG_Pipe/FillPoints.def is the verb table: one row per GLFunctionsTable function pointer in
  declaration order (69), nine classes and the may-read field rows; gen_pipe.py parses the
  struct and refuses a row set that is not exactly its member set, then emits
  generated/PipeFillPoints.inc (verb enum, class tables, per-class field masks with the
  sticky fields OR'ed in). MGP_FILL(Verb) in MG_Impl/Pipe/PipeFill.h is the fill point;
  MGPipeFillForVerb only bumps the serial, records the verb and stamps the sticky fields
  here - the per-class copies land in the next commit, the fill points in MG_Impl after.
- MOBILEGL_PIPE_POISON is derived once in PipeInputs.h from MOBILEGL_PIPE_PUSH and the DEBUG
  level, MOBILEGL_BUILD_DISAGGREGATED or MOBILEGL_PIPE_VERIFY (the tree has no
  MOBILEGL_DEBUG); under it every accessor is a read-side freshness check that aborts with
  Fatal{UnmigratedPipeInput, "Field@Verb"}.
- CMake: MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_VERIFY options (VERIFY forces PUSH on), the two
  new sources appended only under PUSH, the compile definitions; Config.h/ConfigLoader.cpp
  gain PipeVerifyFatal / PipeVerifyCorrupt / PipePoisonOmit under #if MOBILEGL_PIPE_PUSH so
  the pull build's FeaturesTable does not change size.
- Pull build proof: symbol_report.py against the 087685d1 baseline reports 0 added / 0
  removed / 0 resized / 0 renamed and a .text delta of 0; ctest -N names unchanged; gen_pipe
  --check clean; unit tests green in the pull, push and verify builds.
This commit is contained in:
2026-09-06 01:35:45 -04:00
parent 087685d19b
commit bf86b1ede6
15 changed files with 1834 additions and 30 deletions
+28
View File
@@ -22,6 +22,11 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
@@ -451,6 +456,22 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
endif()
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
# cache write, for the same reason as the disaggregated fallback above.
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
set(MOBILEGL_PIPE_PUSH ON)
endif()
if (MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
MobileGL/MG_Impl/Pipe/PipeFill.cpp
)
endif()
if (MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
list(APPEND SOURCE_FILES
@@ -524,6 +545,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
endif()
if (MOBILEGL_PIPE_PUSH)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
endif()
if (MOBILEGL_PIPE_VERIFY)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
endif()
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
set(MOBILEGL_INCLUDE_DIR
+20
View File
@@ -330,6 +330,26 @@ namespace MobileGL::MG_Config {
// the semantic gate that replaces byte identity, and it catches the dangerous
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
Bool PipeVerify = false;
#if MOBILEGL_PIPE_PUSH
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
// counts instead, for triage and for the lane that must survive to read its own
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
// off.
Bool PipeVerifyFatal = true;
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
// green verify run goes red naming it (negative control A). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipeVerifyCorrupt;
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
// the value) of that field for that verb, an omission indistinguishable from a
// forgotten FillPoints.def row, so that verb's read of it is
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
#endif
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
+7
View File
@@ -244,6 +244,13 @@ namespace MobileGL::MG_ConfigLoader {
// that starts with MOBILEGL_ is visible to these queries by construction.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
#if MOBILEGL_PIPE_PUSH
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
#endif
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
// truthy check: unset must keep the memos, and only an explicitly falsy value may
+145
View File
@@ -0,0 +1,145 @@
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
// name lookups the runtime knobs need, and - in a verify build - the per-field equality and
// the corruption injector the comparator uses. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <cstring>
namespace MobileGL::MG_Pipe {
const char* MGPipeVerbName(MGPipeVerb verb) {
const auto index = static_cast<SizeT>(verb);
return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : "<none>";
}
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) {
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
#if MOBILEGL_PIPE_VERIFY
namespace {
// Every overload is declared up front: the array overloads recurse into their element
// type, and a call inside a template only sees what was declared before the template.
template <class T>
Bool StorageEqual(const T& a, const T& b);
template <class T>
Bool StorageEqual(T* const& a, T* const& b);
template <class T>
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b);
template <class T, SizeT N>
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
template <class T>
void CorruptStorage(T& v);
template <class T>
void CorruptStorage(T*& p);
template <class T>
void CorruptStorage(SharedPtr<T>& p);
template <class T, SizeT N>
void CorruptStorage(T (&a)[N]);
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
// ---- equality over one field's storage ----
// O-class storage compares by identity: a raw pointer into the context, or the object a
// SharedPtr owns. Everything else goes through G4's MGPipeFieldEqual, recursing through
// C arrays element-wise.
template <class T>
Bool StorageEqual(T* const& a, T* const& b) {
return a == b;
}
template <class T>
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b) {
return a.get() == b.get();
}
template <class T, SizeT N>
Bool StorageEqual(const T (&a)[N], const T (&b)[N]) {
for (SizeT i = 0; i < N; ++i) {
if (!StorageEqual(a[i], b[i])) return false;
}
return true;
}
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
}
template <class T>
Bool StorageEqual(const T& a, const T& b) {
return MGPipeFieldEqual(a, b);
}
// ---- corruption of one field's storage ----
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
// scalar or enum moves by one, a pointer becomes null, a SharedPtr is dropped, an array
// corrupts its first element, and any other struct has its first byte XOR'ed with 0x5A.
template <class T>
void CorruptStorage(T*& p) {
p = nullptr;
}
template <class T>
void CorruptStorage(SharedPtr<T>& p) {
p.reset();
}
template <class T, SizeT N>
void CorruptStorage(T (&a)[N]) {
CorruptStorage(a[0]);
}
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
CorruptStorage(c.Blend);
}
template <class T>
void CorruptStorage(T& v) {
if constexpr (std::is_same_v<T, Bool>) {
v = !v;
} else if constexpr (std::is_enum_v<T>) {
v = static_cast<T>(static_cast<std::underlying_type_t<T>>(v) + 1);
} else if constexpr (std::is_arithmetic_v<T>) {
v = static_cast<T>(v + 1);
} else {
static_assert(std::is_trivially_copyable_v<T>, "PipeInputs storage must be trivially copyable");
unsigned char first = 0;
std::memcpy(&first, &v, 1);
first ^= 0x5A;
std::memcpy(&v, &first, 1);
}
}
} // namespace
Bool MGPipeInputsFieldEqual(MGPipeInputField field, PipeInputs& a, PipeInputs& b) {
// A forwarded field has no storage and is equal by definition; VisitStorage answers
// false for it, hence the explicit sticky test first.
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
}
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
CorruptStorage(x);
return true;
});
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace MobileGL::MG_Pipe
+671
View File
@@ -0,0 +1,671 @@
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <MG_Pipe/MGPipe.h>
// The frontend types the accessors return. Allowed here: P13 keeps this include for the
// verify arm (ARCHITECTURE.md 9.5). This header spells no MG_State global - every read of
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_State/GLState/Core.h>
// MOBILEGL_PIPE_POISON: the per-verb generation stamps and the read-side
// Fatal{UnmigratedPipeInput} check. Derived here, once. The repository's debug gate is
// MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG (Defines.h); the verify CI build is
// Release/INFO with MOBILEGL_BUILD_DISAGGREGATED=OFF, so the third arm is what arms the poison
// there without dragging MG_Remote in.
#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || \
MOBILEGL_PIPE_VERIFY)
#define MOBILEGL_PIPE_POISON 1
#else
#define MOBILEGL_PIPE_POISON 0
#endif
namespace MobileGL::MG_Pipe {
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
// verb): MGLOG_F + std::abort(), live at every log level on purpose - this is not
// MOBILEGL_ASSERT, which is inert in INFO builds.
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
const char* MGPipeVerbName(MGPipeVerb verb);
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
// a read of a field whose stamp is older than the current verb serial is
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
#if MOBILEGL_PIPE_POISON
#define MGP_INPUT_CHECK(Field) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
} \
} while (0)
#else
#define MGP_INPUT_CHECK(Field) ((void)0)
#endif
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8): re-reads
// the same accessor with the same indices from the live context and compares. Armed by
// the comparator commit; until then every build's accessor is a load.
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) ((void)0)
// The V/O storage of every field that has storage, by field id. The seven F-class
// (forwarded) fields have none. PipeInputs::VisitStorage dispatches on this list, which
// is what keeps the comparator and the corruption injector one function each instead of
// two sixty-way switches.
// clang-format off
#define MGP_INPUT_STORAGE_LIST(X) \
X(GetActiveTextureUnit, m_activeTextureUnit) \
X(GetBlendColor, m_blendColor) \
X(GetBlendEquationIndexed, m_blendEquation) \
X(GetBlendFuncIndexed, m_blendFunc) \
X(GetBoundTransformFeedbackName, m_boundTransformFeedbackName) \
X(GetBoundVertexArray, m_boundVertexArray) \
X(GetBufferBindingSlot, m_bufferBindingSlot) \
X(GetBufferBindingPoint, m_bufferBindingPointBase) \
X(GetTouchedBufferBindingPointCount, m_touchedBindingPointCount) \
X(GetClampReadColor, m_clampReadColor) \
X(GetClearColor, m_clearColor) \
X(GetClearDepth, m_clearDepth) \
X(GetClearStencil, m_clearStencil) \
X(GetColorMaskIndexed, m_colorMask) \
X(GetCullFaceMode, m_cullFaceMode) \
X(GetCurrentVertexAttribute, m_currentVertexAttribute) \
X(GetDepthFunc, m_depthFunc) \
X(GetDepthMask, m_depthMask) \
X(GetDepthRangeIndexed, m_depthRange) \
X(GetFramebufferBindingSlot, m_framebufferBindingSlot) \
X(GetImageTextureBinding, m_imageTextureBindingBase) \
X(GetLineWidth, m_lineWidth) \
X(GetLogicOp, m_logicOp) \
X(GetMaxTouchedTextureUnit, m_maxTouchedTextureUnit) \
X(GetMinSampleShadingValue, m_minSampleShadingValue) \
X(GetPatchDefaultInnerLevel, m_patchDefaultInnerLevel) \
X(GetPatchDefaultOuterLevel, m_patchDefaultOuterLevel) \
X(GetPatchVertices, m_patchVertices) \
X(GetPipelineStateVersion, m_pipelineStateVersion) \
X(GetPixelStoreParameters, m_pixelStore) \
X(GetPolygonModeFront, m_polygonModeFront) \
X(GetPolygonOffsetFactor, m_polygonOffsetFactor) \
X(GetPolygonOffsetUnits, m_polygonOffsetUnits) \
X(GetPrimitiveRestartIndex, m_primitiveRestartIndex) \
X(GetProgramForDispatch, m_programForDispatch) \
X(GetProgramForDraw, m_programForDraw) \
X(GetProvokingVertexMode, m_provokingVertexMode) \
X(GetRenderStateParameters, m_renderState) \
X(GetRenderStateParametersVersion, m_renderStateParametersVersion) \
X(GetSamplingResolutionGeneration, m_samplingResolutionGeneration) \
X(GetScissorBox, m_scissorBox) \
X(GetStencilState, m_stencil) \
X(GetTextureBindGeneration, m_textureBindGeneration) \
X(GetTextureContextId, m_textureContextId) \
X(GetTextureUnitObject, m_textureUnitBase) \
X(GetTransformFeedbackCapturedVertices, m_transformFeedbackCapturedVertices) \
X(GetTransformFeedbackGeneration, m_transformFeedbackGeneration) \
X(GetTransformFeedbackPausedPrimitiveCounter, m_transformFeedbackPausedPrimitiveCounter) \
X(GetTransformFeedbackProgram, m_transformFeedbackProgram) \
X(GetViewport, m_viewport) \
X(GetViewportIndexed, m_viewportIndexed) \
X(IsCapabilityEnabled, m_capability) \
X(IsCapabilityEnabledIndexed, m_capabilityIndexed) \
X(IsTransformFeedbackActive, m_transformFeedbackActive) \
X(IsTransformFeedbackPaused, m_transformFeedbackPaused) \
X(GetBoundTransformFeedbackLifetimeId, m_boundTransformFeedbackLifetimeId)
// clang-format on
// The seven F-class fields, for the arithmetic below and for the sticky table's proof.
inline constexpr SizeT kMGPipeForwardedFieldCount = 7;
// The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief
// D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS
// and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler
// sed is type-neutral:
//
// V (value) copied out of GLContext at fill time by calling the same accessor;
// no derivation logic is re-implemented here, which is what keeps the
// copy semantically identical by construction.
// O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned
// slot/array for the accessors that return a non-const reference into
// the context. Identity is what phase C turns into a handle.
// F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of
// line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live
// context may be spelled). Sticky: stamped once by the first fill that
// sees a live context.
//
// Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ
// (compare-at-read) -> the storage. Both macros expand to nothing when their switch is
// off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load.
struct PipeInputs {
using GLContext = MG_State::GLState::GLContext;
using BufferObject = MG_State::GLState::BufferObject;
using BufferTarget = ::MobileGL::BufferTarget;
using FramebufferObject = MG_State::GLState::FramebufferObject;
using FramebufferTarget = ::MobileGL::FramebufferTarget;
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
using ProgramObject = MG_State::GLState::ProgramObject;
using ITextureObject = MG_State::GLState::ITextureObject;
using TextureUnit = MG_State::GLState::TextureUnit;
using ImageTextureBinding = MG_State::GLState::ImageTextureBinding;
using CurrentVertexAttributeValue = MG_State::GLState::CurrentVertexAttributeValue;
static constexpr SizeT kBufferTargetCount = static_cast<SizeT>(BufferTarget::BufferTargetCount);
static constexpr SizeT kFramebufferTargetCount = static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount);
static constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
static constexpr SizeT kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
static constexpr SizeT kMaxVertexAttribs = VertexArrayObject::MAX_VERTEX_ATTRIBS;
static constexpr SizeT kStencilFaceCount = static_cast<SizeT>(StencilFace::StencilFaceCount);
// IsCapabilityEnabledIndexed's two indexed capabilities, the only ones GLContext keeps
// indexed state for (RenderState::IsCapabilityEnabledIndexed).
struct IndexedCapabilities {
Bool Blend[kMGMaxDrawBuffers];
Bool ScissorTest[kMaxViewports];
};
// ---- identity / liveness (not fields) ----
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
// must be true as soon as a context exists, fill or no fill, which is what today's
// null-context guards test.
Bool IsLive() const;
// The live GLContext's address at the last fill; serves MGB_CTX_IDENTITY.
const void* ContextIdentity() const { return m_contextIdentity; }
// The verb of the last fill, kVerbCount before the first one.
MGPipeVerb CurrentVerb() const { return m_currentVerb; }
#if MOBILEGL_PIPE_POISON
const MGPipeFilledState& FilledState() const { return m_filled; }
#endif
// ---- V: values ----
Int GetActiveTextureUnit() const {
MGP_INPUT_CHECK(MGPipeInputField::GetActiveTextureUnit);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetActiveTextureUnit, 0, 0);
return m_activeTextureUnit;
}
const FloatVec4& GetBlendColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendColor, 0, 0);
return m_blendColor;
}
void GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendEquationIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendEquationIndexed, index, 0);
if (index >= kMGMaxDrawBuffers) {
MOBILEGL_ASSERT(false, "Blend equation index out of range: %u", index);
return;
}
color = m_blendEquation[index][0];
alpha = m_blendEquation[index][1];
}
void GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
BlendFactor& dstAlpha) const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendFuncIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendFuncIndexed, index, 0);
if (index >= kMGMaxDrawBuffers) {
MOBILEGL_ASSERT(false, "Blend func index out of range: %u", index);
return;
}
srcRGB = m_blendFunc[index][0];
dstRGB = m_blendFunc[index][1];
srcAlpha = m_blendFunc[index][2];
dstAlpha = m_blendFunc[index][3];
}
// Dead field: filled, read by no backend since the D21 XFB counter-slot rekey; kept so
// the vendored inventory row keeps its mapping (Coverage.def).
Uint GetBoundTransformFeedbackName() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackName);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackName, 0, 0);
return m_boundTransformFeedbackName;
}
SizeT GetTouchedBufferBindingPointCount(BufferTarget target) const {
MGP_INPUT_CHECK(MGPipeInputField::GetTouchedBufferBindingPointCount);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTouchedBufferBindingPointCount, static_cast<Uint>(target), 0);
return m_touchedBindingPointCount[static_cast<SizeT>(target)];
}
GLenum GetClampReadColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClampReadColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClampReadColor, 0, 0);
return m_clampReadColor;
}
const FloatVec4& GetClearColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearColor, 0, 0);
return m_clearColor;
}
Float GetClearDepth() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearDepth);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearDepth, 0, 0);
return m_clearDepth;
}
Uint32 GetClearStencil() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearStencil);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearStencil, 0, 0);
return m_clearStencil;
}
BoolVec4 GetColorMaskIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetColorMaskIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetColorMaskIndexed, index, 0);
return m_colorMask[index];
}
CullFaceMode GetCullFaceMode() const {
MGP_INPUT_CHECK(MGPipeInputField::GetCullFaceMode);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCullFaceMode, 0, 0);
return m_cullFaceMode;
}
const CurrentVertexAttributeValue& GetCurrentVertexAttribute(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetCurrentVertexAttribute);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCurrentVertexAttribute, index, 0);
if (index >= kMaxVertexAttribs) {
static const CurrentVertexAttributeValue defaultValue{};
MGLOG_E_ONCE("PipeInputs::GetCurrentVertexAttribute: index %u is out of range", index);
return defaultValue;
}
return m_currentVertexAttribute[index];
}
DepthTestFunc GetDepthFunc() const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthFunc);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthFunc, 0, 0);
return m_depthFunc;
}
Bool GetDepthMask() const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthMask);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthMask, 0, 0);
return m_depthMask;
}
const FloatVec2& GetDepthRangeIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthRangeIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthRangeIndexed, index, 0);
if (index >= kMaxViewports) {
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
return m_depthRange[0];
}
return m_depthRange[index];
}
Float GetLineWidth() const {
MGP_INPUT_CHECK(MGPipeInputField::GetLineWidth);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLineWidth, 0, 0);
return m_lineWidth;
}
LogicOperation GetLogicOp() const {
MGP_INPUT_CHECK(MGPipeInputField::GetLogicOp);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLogicOp, 0, 0);
return m_logicOp;
}
Int GetMaxTouchedTextureUnit() const {
MGP_INPUT_CHECK(MGPipeInputField::GetMaxTouchedTextureUnit);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMaxTouchedTextureUnit, 0, 0);
return m_maxTouchedTextureUnit;
}
Float GetMinSampleShadingValue() const {
MGP_INPUT_CHECK(MGPipeInputField::GetMinSampleShadingValue);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMinSampleShadingValue, 0, 0);
return m_minSampleShadingValue;
}
const FloatVec2& GetPatchDefaultInnerLevel() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultInnerLevel);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultInnerLevel, 0, 0);
return m_patchDefaultInnerLevel;
}
const FloatVec4& GetPatchDefaultOuterLevel() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultOuterLevel);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultOuterLevel, 0, 0);
return m_patchDefaultOuterLevel;
}
Uint GetPatchVertices() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchVertices);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchVertices, 0, 0);
return m_patchVertices;
}
Uint GetPipelineStateVersion() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPipelineStateVersion);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPipelineStateVersion, 0, 0);
return m_pipelineStateVersion;
}
Uint GetRenderStateParametersVersion() const {
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParametersVersion);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParametersVersion, 0, 0);
return m_renderStateParametersVersion;
}
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const {
MGP_INPUT_CHECK(MGPipeInputField::GetPixelStoreParameters);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u, 0);
return m_pixelStore[isUnpack ? 1 : 0];
}
GLenum GetPolygonModeFront() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonModeFront);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonModeFront, 0, 0);
return m_polygonModeFront;
}
Float GetPolygonOffsetFactor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetFactor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetFactor, 0, 0);
return m_polygonOffsetFactor;
}
Float GetPolygonOffsetUnits() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetUnits);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetUnits, 0, 0);
return m_polygonOffsetUnits;
}
Uint32 GetPrimitiveRestartIndex() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPrimitiveRestartIndex);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPrimitiveRestartIndex, 0, 0);
return m_primitiveRestartIndex;
}
ProvokingVertexMode GetProvokingVertexMode() const {
MGP_INPUT_CHECK(MGPipeInputField::GetProvokingVertexMode);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProvokingVertexMode, 0, 0);
return m_provokingVertexMode;
}
const RenderStateParameters& GetRenderStateParameters() const {
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParameters);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParameters, 0, 0);
return m_renderState;
}
Uint64 GetSamplingResolutionGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
return m_samplingResolutionGeneration;
}
const IntVec4& GetScissorBox() const {
MGP_INPUT_CHECK(MGPipeInputField::GetScissorBox);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetScissorBox, 0, 0);
return m_scissorBox;
}
const StencilFaceState& GetStencilState(StencilFace face) const {
MGP_INPUT_CHECK(MGPipeInputField::GetStencilState);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetStencilState, static_cast<Uint>(face), 0);
return m_stencil[face == StencilFace::Back ? 1 : 0];
}
Uint64 GetTextureBindGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
return m_textureBindGeneration;
}
Uint64 GetTextureContextId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
return m_textureContextId;
}
Uint64 GetTransformFeedbackCapturedVertices() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackCapturedVertices);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackCapturedVertices, 0, 0);
return m_transformFeedbackCapturedVertices;
}
Uint64 GetTransformFeedbackGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackGeneration, 0, 0);
return m_transformFeedbackGeneration;
}
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter, 0, 0);
return m_transformFeedbackPausedPrimitiveCounter;
}
Uint64 GetBoundTransformFeedbackLifetimeId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackLifetimeId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackLifetimeId, 0, 0);
return m_boundTransformFeedbackLifetimeId;
}
IntVec4 GetViewport() const {
MGP_INPUT_CHECK(MGPipeInputField::GetViewport);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewport, 0, 0);
return m_viewport;
}
const FloatVec4& GetViewportIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetViewportIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewportIndexed, index, 0);
if (index >= kMaxViewports) {
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
return m_viewportIndexed[0];
}
return m_viewportIndexed[index];
}
Bool IsCapabilityEnabled(CapabilityInput cap) const {
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabled);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabled, static_cast<Uint>(cap), 0);
const auto index = static_cast<SizeT>(cap);
return index < kCapabilityCount ? m_capability[index] : false;
}
// Blend and ScissorTest are the only indexed capabilities GLContext keeps; no backend
// asks for another (VulkanRenderer asks Blend). Any other cap is a read the fill cannot
// have served: Fatal{UnmigratedPipeInput} naming the field and the verb, the cap in a
// preceding MGLOG_E.
Bool IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabledIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabledIndexed, static_cast<Uint>(cap), index);
if (cap == CapabilityInput::Blend) {
return index < kMGMaxDrawBuffers ? m_capabilityIndexed.Blend[index] : false;
}
if (cap == CapabilityInput::ScissorTest) {
return index < kMaxViewports ? m_capabilityIndexed.ScissorTest[index] : false;
}
MGLOG_E("PipeInputs::IsCapabilityEnabledIndexed: no indexed storage for cap=%d (index=%u)",
static_cast<int>(cap), index);
MGPipeInputPoisonFatalForVerb(MGPipeInputField::IsCapabilityEnabledIndexed, m_currentVerb);
}
Bool IsTransformFeedbackActive() const {
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackActive);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackActive, 0, 0);
return m_transformFeedbackActive;
}
Bool IsTransformFeedbackPaused() const {
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackPaused);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackPaused, 0, 0);
return m_transformFeedbackPaused;
}
// ---- O: object references ----
const SharedPtr<VertexArrayObject>& GetBoundVertexArray() {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundVertexArray);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundVertexArray, 0, 0);
return m_boundVertexArray;
}
// A target the fill left null (one outside GlobalBufferTargets / BufferBindPointTargets,
// or a read before any fill) is a read the fill cannot have served: the poison Fatal,
// the target in a preceding MGLOG_E.
BindingSlot<BufferObject>& GetBufferBindingSlot(BufferTarget target) {
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingSlot);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingSlot, static_cast<Uint>(target), 0);
const auto index = static_cast<SizeT>(target);
if (index >= kBufferTargetCount || m_bufferBindingSlot[index] == nullptr) {
MGLOG_E("PipeInputs::GetBufferBindingSlot: no slot for target=%d", static_cast<int>(target));
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingSlot, m_currentVerb);
}
return *m_bufferBindingSlot[index];
}
BindingSlotRange1D<BufferObject>& GetBufferBindingPoint(BufferTarget target, Uint index) {
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingPoint);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingPoint, static_cast<Uint>(target), index);
const auto targetIndex = static_cast<SizeT>(target);
if (targetIndex >= kBufferTargetCount || m_bufferBindingPointBase[targetIndex] == nullptr) {
MGLOG_E("PipeInputs::GetBufferBindingPoint: no binding points for target=%d (index=%u)",
static_cast<int>(target), index);
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingPoint, m_currentVerb);
}
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
// (BufferState.h), so base[index] is the live slot GLContext would hand out.
return m_bufferBindingPointBase[targetIndex][index];
}
BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget target) {
MGP_INPUT_CHECK(MGPipeInputField::GetFramebufferBindingSlot);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetFramebufferBindingSlot, static_cast<Uint>(target), 0);
const auto index = static_cast<SizeT>(target);
if (index >= kFramebufferTargetCount || m_framebufferBindingSlot[index] == nullptr) {
MGLOG_E("PipeInputs::GetFramebufferBindingSlot: no slot for target=%d", static_cast<int>(target));
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetFramebufferBindingSlot, m_currentVerb);
}
return *m_framebufferBindingSlot[index];
}
ImageTextureBinding& GetImageTextureBinding(Int unit) {
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
if (m_imageTextureBindingBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
}
return m_imageTextureBindingBase[unit];
}
const ImageTextureBinding& GetImageTextureBinding(Int unit) const {
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
if (m_imageTextureBindingBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
}
return m_imageTextureBindingBase[unit];
}
const SharedPtr<ProgramObject>& GetProgramForDispatch() {
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDispatch);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDispatch, 0, 0);
return m_programForDispatch;
}
const SharedPtr<ProgramObject>& GetProgramForDraw() {
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDraw);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDraw, 0, 0);
return m_programForDraw;
}
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackProgram);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackProgram, 0, 0);
return m_transformFeedbackProgram;
}
TextureUnit& GetTextureUnitObject(Int unit) {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureUnitObject);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureUnitObject, static_cast<Uint>(unit), 0);
if (m_textureUnitBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetTextureUnitObject, m_currentVerb);
}
return m_textureUnitBase[unit];
}
// ---- F: forwarded to the live context (MG_Impl/Pipe/PipeFill.cpp); sticky ----
// Each takes an argument that is not verb state - a GL name, a lifetime id, a target -
// i.e. it is a lookup or a reverse-channel write, not a state read; there is no value
// the filler could copy and no verb whose fill could make it stale. Phase C replaces
// them with handle tables and callbacks.
SizeT GetBufferBindingPointCount(BufferTarget target) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
void InvalidateCompileEnv();
Bool ValidateProgramName(Uint index) const;
// Dropped with an MGLOG_E_ONCE when no context is live; today's guarded sites never
// reach it without one.
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
// ---- the storage visitor ----
// Calls fn(a.<member>, b.<member>) for the field's storage and returns its result; returns
// false without calling fn for a forwarded field, which has none. The comparator's
// per-field equality and the verify corruption injector are both one call of this.
template <class Fn>
static Bool VisitStorage(MGPipeInputField field, PipeInputs& a, PipeInputs& b, Fn&& fn) {
switch (field) {
#define MGP_INPUT_VISIT(Field, Member) \
case MGPipeInputField::Field: \
return fn(a.Member, b.Member);
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
#undef MGP_INPUT_VISIT
default:
return false;
}
}
private:
friend void MGPipeFillForVerb(MGPipeVerb verb);
friend void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
// ---- identity ----
const void* m_contextIdentity = nullptr;
Bool m_live = false;
MGPipeVerb m_currentVerb = MGPipeVerb::kVerbCount;
#if MOBILEGL_PIPE_POISON
MGPipeFilledState m_filled{};
#endif
// ---- V ----
Int m_activeTextureUnit = 0;
FloatVec4 m_blendColor{};
BlendEquation m_blendEquation[kMGMaxDrawBuffers][2]{};
BlendFactor m_blendFunc[kMGMaxDrawBuffers][4]{};
Uint m_boundTransformFeedbackName = 0;
SizeT m_touchedBindingPointCount[kBufferTargetCount]{};
GLenum m_clampReadColor = 0;
FloatVec4 m_clearColor{};
Float m_clearDepth = 0.f;
Uint32 m_clearStencil = 0;
BoolVec4 m_colorMask[kMGMaxDrawBuffers]{};
CullFaceMode m_cullFaceMode{};
CurrentVertexAttributeValue m_currentVertexAttribute[kMaxVertexAttribs]{};
DepthTestFunc m_depthFunc{};
Bool m_depthMask = false;
FloatVec2 m_depthRange[kMaxViewports]{};
Float m_lineWidth = 0.f;
LogicOperation m_logicOp{};
Int m_maxTouchedTextureUnit = -1;
Float m_minSampleShadingValue = 0.f;
FloatVec2 m_patchDefaultInnerLevel{};
FloatVec4 m_patchDefaultOuterLevel{};
Uint m_patchVertices = 0;
Uint m_pipelineStateVersion = 0;
Uint m_renderStateParametersVersion = 0;
PixelStoreParameters m_pixelStore[2]{}; // [0] = pack, [1] = unpack
GLenum m_polygonModeFront = 0;
Float m_polygonOffsetFactor = 0.f;
Float m_polygonOffsetUnits = 0.f;
Uint32 m_primitiveRestartIndex = 0;
ProvokingVertexMode m_provokingVertexMode{};
RenderStateParameters m_renderState{};
Uint64 m_samplingResolutionGeneration = 0;
Uint64 m_textureBindGeneration = 0;
Uint64 m_textureContextId = 0;
IntVec4 m_scissorBox{};
StencilFaceState m_stencil[kStencilFaceCount]{};
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackGeneration = 0;
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_boundTransformFeedbackLifetimeId = 0;
IntVec4 m_viewport{};
FloatVec4 m_viewportIndexed[kMaxViewports]{};
Bool m_capability[kCapabilityCount]{};
IndexedCapabilities m_capabilityIndexed{};
Bool m_transformFeedbackActive = false;
Bool m_transformFeedbackPaused = false;
// ---- O ----
SharedPtr<VertexArrayObject> m_boundVertexArray;
BindingSlot<BufferObject>* m_bufferBindingSlot[kBufferTargetCount]{};
BindingSlotRange1D<BufferObject>* m_bufferBindingPointBase[kBufferTargetCount]{};
BindingSlot<FramebufferObject>* m_framebufferBindingSlot[kFramebufferTargetCount]{};
ImageTextureBinding* m_imageTextureBindingBase = nullptr;
SharedPtr<ProgramObject> m_programForDispatch;
SharedPtr<ProgramObject> m_programForDraw;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
TextureUnit* m_textureUnitBase = nullptr;
};
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
// variable: no .cpp is needed for the definition.
inline PipeInputs gPipeInputs{};
// Every field has storage or is forwarded, and nothing else.
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
static_assert(0 MGP_INPUT_STORAGE_LIST(MGP_INPUT_COUNT_ONE) + kMGPipeForwardedFieldCount == kMGPipeInputFieldCount,
"MGP_INPUT_STORAGE_LIST plus the seven forwarded fields is not the PipeInputs field set");
#undef MGP_INPUT_COUNT_ONE
// The docs budget ~20 KB; the block is a few KB.
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
#if MOBILEGL_PIPE_VERIFY
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
// always equal (no storage).
Bool MGPipeInputsFieldEqual(MGPipeInputField field, PipeInputs& a, PipeInputs& b);
// PipeInputs.cpp. Negative control A: perturbs one field's storage (flip a Bool, +1 a
// scalar, ^0x5A the first byte of a struct, null a pointer). Returns false for a forwarded
// field, which has nothing to corrupt.
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
#endif
} // namespace MobileGL::MG_Pipe
+100
View File
@@ -0,0 +1,100 @@
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The client side of the PipeInputs block (ARCHITECTURE.md 9.2 phase A): the only place in
// the push arm that reads MG_State::pGLContext. Holds the per-verb filler, the F-class
// forwarders and IsLive. Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt appends it
// to SOURCE_FILES there).
//
// Contract commit (P1 c1): the filler bumps the verb serial, records the verb and the
// context identity, and stamps the seven sticky fields once; the per-class field copies and
// stamps land in c2, the verify snapshot and comparator in c4.
#include <MG_State/GLState/Core.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <Config.h>
namespace MobileGL::MG_Pipe {
namespace {
MG_State::GLState::GLContext* LiveContext() { return MG_State::pGLContext.get(); }
template <class T>
const SharedPtr<T>& NullShared() {
static const SharedPtr<T> null;
return null;
}
} // namespace
// ---- liveness ----
Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; }
// ---- the seven F-class forwarders ----
SizeT PipeInputs::GetBufferBindingPointCount(BufferTarget target) const {
const auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetBufferBindingPointCount(target) : 0;
}
const SharedPtr<PipeInputs::ProgramObject>& PipeInputs::GetProgramObject(Uint index) {
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared<ProgramObject>();
}
const SharedPtr<PipeInputs::ITextureObject>& PipeInputs::GetTextureObject(Uint index) {
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared<ITextureObject>();
}
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
}
void PipeInputs::InvalidateCompileEnv() {
if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
}
Bool PipeInputs::ValidateProgramName(Uint index) const {
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->ValidateProgramName(index);
}
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
auto* ctx = LiveContext();
if (ctx == nullptr) {
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
return;
}
ctx->RecordError(code, Move(info));
}
// ---- the filler ----
void MGPipeFillForVerb(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs;
#if MOBILEGL_PIPE_POISON
// Starts at 1, so FilledGen == 0 means "never filled".
++inputs.m_filled.CurrentVerbSerial;
#endif
inputs.m_currentVerb = verb;
auto* ctx = LiveContext();
if (ctx == nullptr) {
inputs.m_live = false;
inputs.m_contextIdentity = nullptr;
return;
}
inputs.m_live = true;
inputs.m_contextIdentity = ctx;
#if MOBILEGL_PIPE_POISON
// The sticky (forwarded) fields are stamped once by the first fill that sees a live
// context and stay fresh through the Sticky -> FilledGen != 0 branch of
// MGPipeInputFieldIsFresh.
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (kMGPipeInputFieldSticky[i] && inputs.m_filled.FilledGen[i] == 0) inputs.m_filled.FilledGen[i] = 1;
}
#endif
// c2: copy and stamp every field in kMGPipeClassFieldMask[kMGPipeVerbClass[verb]].
}
} // namespace MobileGL::MG_Pipe
+27
View File
@@ -0,0 +1,27 @@
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
// The fill point (ARCHITECTURE.md 9.2, P1 brief D7). MG_Impl spells MGP_FILL(Verb); as the
// statement immediately before every call through gBackendFunctionsTable.GL - after every
// early return the call is behind, inside the loop body for a call made in a loop - so the
// frontend fills the PipeInputs block for exactly the verbs that reach a backend. In the
// pull build the macro is ((void)0) and the pull build is byte-identical to a tree without
// it.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
// PipeFill.cpp. Bumps the per-verb serial, records the verb, and (from c2 on) copies
// every field in the verb class's may-read mask out of the live GLContext, stamping each
// with the new serial.
void MGPipeFillForVerb(MGPipeVerb verb);
} // namespace MobileGL::MG_Pipe
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
#else
#define MGP_FILL(Verb) ((void)0)
#endif
+28 -3
View File
@@ -31,11 +31,13 @@
X(GetBlendColor, SetDynamicState) \
X(GetBlendEquationIndexed, CreateRenderState) \
X(GetBlendFuncIndexed, CreateRenderState) \
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
X(GetBoundVertexArray, BindVertexElements) \
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
/* set_index_buffer, set_indirect_buffers and set_shader_buffers once the */ \
/* inventory carries the target argument (P1). Named for the plan's explicit */ \
/* set_index_buffer, set_indirect_buffers and set_shader_buffers when the */ \
/* inventory is re-vendored carrying the target argument (deferred out of P1: */ \
/* the extractor lives in MobileGL-CS). Named for the plan's explicit */ \
/* replacement of the DrawIndirect/Parameter pair. */ \
X(GetBufferBindingSlot, SetIndirectBuffers) \
X(GetBufferBindingPoint, SetShaderBuffers) \
@@ -94,7 +96,30 @@
X(IsTransformFeedbackPaused, PauseStreamOutput) \
X(InvalidateCompileEnv, kClientResolved) \
X(ValidateProgramName, kClientResolved) \
X(RecordError, kReverseChannel)
X(RecordError, kReverseChannel) \
/* The D21 XFB counter-slot rekey's reads (VulkanRenderer.cpp); the calls they */ \
/* map to are GetTransformFeedbackGeneration's. */ \
X(GetBoundTransformFeedbackLifetimeId, SetStreamOutputTargets) \
X(HasOpenTransformFeedbackSpan, SetStreamOutputTargets)
// X(Accessor, Reason) - the STICKY fields (P1 brief D6): the only PipeInputs fields whose
// value is valid across verbs, so the poison's per-verb generation does not apply to them.
// Exactly the seven F-class (forwarded) accessors, and the argument for each is the same:
// it takes an argument that is not verb state - a GL name, a lifetime id, a target - i.e.
// it is a lookup or a reverse-channel write, not a state read; there is no value the
// filler could copy and no verb whose fill could make it stale; phase C replaces them
// with handle tables and callbacks. None of the version/generation accessors is sticky:
// those change under verbs and are precisely what the poison must protect. The verify
// lane's Fatal{UnmigratedPipeInput} is fixed by a FillPoints.def row, never by a row here.
// gen_pipe.py refuses a name that is not an accessor above.
#define MGP_COVERAGE_STICKY_LIST(X) \
X(GetBufferBindingPointCount, "keyed by target: a constexpr capacity table, not verb state") \
X(GetProgramObject, "keyed by GL name: an object lookup, not verb state") \
X(GetTextureObject, "keyed by GL name: an object lookup, not verb state") \
X(HasOpenTransformFeedbackSpan, "keyed by lifetime id: an object lookup, not verb state") \
X(ValidateProgramName, "keyed by GL name: a name-table lookup, not verb state") \
X(InvalidateCompileEnv, "reverse channel: a write into the frontend, not a state read") \
X(RecordError, "reverse channel: a write into the frontend, not a state read")
// X(DeltaKind, PipeCall) - for inventory rows with no accessor in the member column.
// Read by gen_pipe.py ONLY, never by the C++ preprocessor: the delta kinds are the
+267
View File
@@ -0,0 +1,267 @@
// MobileGL - MobileGL/MG_Pipe/FillPoints.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The per-verb fill points of the PipeInputs strangler (ARCHITECTURE.md 9.2, phase A; the
// P1 brief D7). Three hand-maintained lists, read by scripts/gen_pipe.py (G5b) into
// generated/PipeFillPoints.inc:
//
// MGP_FILL_VERB_LIST every verb the frontend calls through GLFunctionsTable, with its class
// MGP_FILL_CLASS_LIST the verb classes
// MGP_FILL_FIELD_LIST the may-read table: which PipeInputs fields a class of verb may read
//
// The verb set IS the function-pointer member set of MG_Backend::GLFunctionsTable
// (MG_Backend/BackendObject.h), in declaration order: gen_pipe.py parses that struct and
// refuses a row set that is not exactly its member set in that order, so the MGPipeVerb enum
// and the table cannot drift apart. MG_Impl spells MGP_FILL(Verb) immediately before every
// call through the table (83 statements over these 69 verbs); Present and SetSwapInterval go
// through BackendObject virtuals and read no frontend state, so they are not verbs here.
//
// The seven sticky fields (Coverage.def, MGP_COVERAGE_STICKY_LIST) are implicit in every
// class and are not listed. The verify lane is the oracle for this table: a
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
// row, never by marking the field sticky.
//
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
//
// clang-format off
// X(Verb, Class) - one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h),
// in declaration order. gen_pipe.py parses that struct and refuses a row set that is not exactly its member set.
#define MGP_FILL_VERB_LIST(X) \
X(DrawArrays, kDraw) \
X(DrawElements, kDraw) \
X(DrawElementsBaseVertex, kDraw) \
X(MultiDrawArrays, kDraw) \
X(MultiDrawElements, kDraw) \
X(MultiDrawElementsBaseVertex, kDraw) \
X(MultiDrawElementsIndirect, kDraw) \
X(MultiDrawArraysIndirect, kDraw) \
X(MultiDrawElementsIndirectCount, kDraw) \
X(MultiDrawArraysIndirectCount, kDraw) \
X(DrawRangeElementsBaseVertex, kDraw) \
X(DrawRangeElements, kDraw) \
X(DrawElementsInstancedBaseVertexBaseInstance, kDraw) \
X(DrawElementsInstancedBaseVertex, kDraw) \
X(DrawElementsInstancedBaseInstance, kDraw) \
X(DrawElementsInstanced, kDraw) \
X(DrawArraysInstancedBaseInstance, kDraw) \
X(DrawArraysInstanced, kDraw) \
X(DrawElementsIndirect, kDraw) \
X(DrawArraysIndirect, kDraw) \
X(Clear, kClear) \
X(ClearBufferfi, kClear) \
X(ClearBufferfv, kClear) \
X(ClearBufferuiv, kClear) \
X(ClearBufferiv, kClear) \
X(ClearNamedFramebufferfv, kClear) \
X(ClearNamedFramebufferfi, kClear) \
X(ClearNamedFramebufferiv, kClear) \
X(ClearNamedFramebufferuiv, kClear) \
X(BlitFramebuffer, kBlitOrCopy) \
X(BlitNamedFramebuffer, kBlitOrCopy) \
X(CopyTexImage2D, kBlitOrCopy) \
X(CopyTexSubImage2D, kBlitOrCopy) \
X(CopyImageSubData, kBlitOrCopy) \
X(GenerateMipmap, kTextureOp) \
X(ReadPixels, kReadback) \
X(GetTexImage, kReadback) \
X(GetTextureImage, kReadback) \
X(DispatchCompute, kDispatch) \
X(DispatchComputeIndirect, kDispatch) \
X(MemoryBarrier, kQuery) \
X(MemoryBarrierByRegion, kQuery) \
X(BindImageTexture, kTextureOp) \
X(GetIntegeri_v, kQuery) \
X(ShaderStorageBlockBinding, kProgramOp) \
X(FenceSync, kQuery) \
X(ClientWaitSync, kQuery) \
X(WaitSync, kQuery) \
X(DeleteSync, kQuery) \
X(GetSyncStatus, kQuery) \
X(IsTimerQuerySupported, kQuery) \
X(BeginTimeElapsedQuery, kQuery) \
X(EndTimeElapsedQuery, kQuery) \
X(QueryCounterTimestamp, kQuery) \
X(IsQueryResultAvailable, kQuery) \
X(GetQueryResult64, kQuery) \
X(DeleteBackendQuery, kQuery) \
X(BeginOcclusionQuery, kQuery) \
X(EndOcclusionQuery, kQuery) \
X(BeginXfbPrimitivesQuery, kQuery) \
X(EndXfbPrimitivesQuery, kQuery) \
X(PatchParameteri, kQuery) \
X(BeginTransformFeedback, kXfbSpan) \
X(EndTransformFeedback, kXfbSpan) \
X(PauseTransformFeedback, kXfbSpan) \
X(ResumeTransformFeedback, kXfbSpan) \
X(BindTransformFeedback, kXfbSpan) \
X(DeleteTransformFeedback, kXfbSpan) \
X(GetGpuTimestampNs, kQuery)
// X(Class) - the nine verb classes (ARCHITECTURE.md:153 names eight; kProgramOp is split out because
// ShaderStorageBlockBinding is the one non-draw verb that syncs Espryt's render state and textures).
#define MGP_FILL_CLASS_LIST(X) \
X(kDraw) X(kDispatch) X(kClear) X(kBlitOrCopy) X(kTextureOp) X(kReadback) X(kXfbSpan) X(kProgramOp) X(kQuery)
// X(Class, Field) - the may-read table. A field named here is filled and stamped at every verb of the class;
// a read of a field NOT named here is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
// Derived from the verified reachability of every backend read (both backends, union), P1 brief D7.
#define MGP_FILL_FIELD_LIST(X) \
/* kDraw: every draw entry of both backends */ \
X(kDraw, GetBoundVertexArray) \
X(kDraw, GetProgramForDraw) \
X(kDraw, GetBufferBindingSlot) \
X(kDraw, GetBufferBindingPoint) \
X(kDraw, GetTouchedBufferBindingPointCount) \
X(kDraw, GetTextureUnitObject) \
X(kDraw, GetTextureContextId) \
X(kDraw, GetTextureBindGeneration) \
X(kDraw, GetMaxTouchedTextureUnit) \
X(kDraw, GetSamplingResolutionGeneration) \
X(kDraw, GetImageTextureBinding) \
X(kDraw, GetCurrentVertexAttribute) \
X(kDraw, GetRenderStateParameters) \
X(kDraw, GetRenderStateParametersVersion) \
X(kDraw, GetPipelineStateVersion) \
X(kDraw, GetViewport) \
X(kDraw, GetViewportIndexed) \
X(kDraw, GetDepthRangeIndexed) \
X(kDraw, GetScissorBox) \
X(kDraw, IsCapabilityEnabled) \
X(kDraw, IsCapabilityEnabledIndexed) \
X(kDraw, GetBlendColor) \
X(kDraw, GetBlendFuncIndexed) \
X(kDraw, GetBlendEquationIndexed) \
X(kDraw, GetColorMaskIndexed) \
X(kDraw, GetLogicOp) \
X(kDraw, GetDepthFunc) \
X(kDraw, GetDepthMask) \
X(kDraw, GetStencilState) \
X(kDraw, GetCullFaceMode) \
X(kDraw, GetPolygonModeFront) \
X(kDraw, GetPolygonOffsetFactor) \
X(kDraw, GetPolygonOffsetUnits) \
X(kDraw, GetLineWidth) \
X(kDraw, GetMinSampleShadingValue) \
X(kDraw, GetProvokingVertexMode) \
X(kDraw, GetPatchVertices) \
X(kDraw, GetPatchDefaultOuterLevel) \
X(kDraw, GetPatchDefaultInnerLevel) \
X(kDraw, GetPrimitiveRestartIndex) \
X(kDraw, GetFramebufferBindingSlot) \
X(kDraw, IsTransformFeedbackActive) \
X(kDraw, IsTransformFeedbackPaused) \
X(kDraw, GetTransformFeedbackProgram) \
X(kDraw, GetTransformFeedbackGeneration) \
X(kDraw, GetBoundTransformFeedbackLifetimeId) \
X(kDraw, GetTransformFeedbackCapturedVertices) \
/* kDispatch: the patch fields are Espryt's SyncCurrentProgram -> */ \
/* AttachPassthroughTessControlStage (Managers.cpp) */ \
X(kDispatch, GetProgramForDispatch) \
X(kDispatch, GetBufferBindingSlot) \
X(kDispatch, GetBufferBindingPoint) \
X(kDispatch, GetTouchedBufferBindingPointCount) \
X(kDispatch, GetTextureUnitObject) \
X(kDispatch, GetTextureContextId) \
X(kDispatch, GetTextureBindGeneration) \
X(kDispatch, GetMaxTouchedTextureUnit) \
X(kDispatch, GetSamplingResolutionGeneration) \
X(kDispatch, GetImageTextureBinding) \
X(kDispatch, GetFramebufferBindingSlot) \
X(kDispatch, GetPatchVertices) \
X(kDispatch, GetPatchDefaultOuterLevel) \
X(kDispatch, GetPatchDefaultInnerLevel) \
/* kClear */ \
X(kClear, GetRenderStateParameters) \
X(kClear, GetRenderStateParametersVersion) \
X(kClear, GetViewport) \
X(kClear, IsCapabilityEnabled) \
X(kClear, GetFramebufferBindingSlot) \
X(kClear, GetClearColor) \
X(kClear, GetClearDepth) \
X(kClear, GetClearStencil) \
X(kClear, GetScissorBox) \
X(kClear, GetColorMaskIndexed) \
X(kClear, GetDepthMask) \
X(kClear, GetStencilState) \
X(kClear, GetTextureUnitObject) \
X(kClear, GetTextureContextId) \
X(kClear, GetSamplingResolutionGeneration) \
X(kClear, GetTextureBindGeneration) \
X(kClear, GetMaxTouchedTextureUnit) \
X(kClear, GetImageTextureBinding) \
/* kBlitOrCopy */ \
X(kBlitOrCopy, GetFramebufferBindingSlot) \
X(kBlitOrCopy, IsCapabilityEnabled) \
X(kBlitOrCopy, GetScissorBox) \
X(kBlitOrCopy, IsTransformFeedbackActive) \
X(kBlitOrCopy, IsTransformFeedbackPaused) \
X(kBlitOrCopy, GetRenderStateParameters) \
X(kBlitOrCopy, GetRenderStateParametersVersion) \
X(kBlitOrCopy, GetViewport) \
X(kBlitOrCopy, GetActiveTextureUnit) \
X(kBlitOrCopy, GetTextureUnitObject) \
X(kBlitOrCopy, GetTextureContextId) \
X(kBlitOrCopy, GetSamplingResolutionGeneration) \
X(kBlitOrCopy, GetTextureBindGeneration) \
X(kBlitOrCopy, GetMaxTouchedTextureUnit) \
X(kBlitOrCopy, GetImageTextureBinding) \
X(kBlitOrCopy, GetColorMaskIndexed) \
X(kBlitOrCopy, GetDepthMask) \
X(kBlitOrCopy, GetStencilState) \
/* kTextureOp */ \
X(kTextureOp, GetActiveTextureUnit) \
X(kTextureOp, GetTextureUnitObject) \
X(kTextureOp, GetImageTextureBinding) \
X(kTextureOp, GetTextureContextId) \
X(kTextureOp, GetSamplingResolutionGeneration) \
X(kTextureOp, GetTextureBindGeneration) \
X(kTextureOp, GetMaxTouchedTextureUnit) \
/* kReadback */ \
X(kReadback, GetPixelStoreParameters) \
X(kReadback, GetBufferBindingSlot) \
X(kReadback, GetFramebufferBindingSlot) \
X(kReadback, GetActiveTextureUnit) \
X(kReadback, GetTextureUnitObject) \
X(kReadback, GetClampReadColor) \
X(kReadback, IsCapabilityEnabled) \
X(kReadback, GetRenderStateParameters) \
X(kReadback, GetRenderStateParametersVersion) \
X(kReadback, GetViewport) \
X(kReadback, GetTextureContextId) \
X(kReadback, GetSamplingResolutionGeneration) \
X(kReadback, GetTextureBindGeneration) \
X(kReadback, GetMaxTouchedTextureUnit) \
X(kReadback, GetImageTextureBinding) \
/* kXfbSpan */ \
X(kXfbSpan, GetTransformFeedbackProgram) \
X(kXfbSpan, GetBufferBindingPoint) \
X(kXfbSpan, GetTouchedBufferBindingPointCount) \
X(kXfbSpan, GetTransformFeedbackCapturedVertices) \
X(kXfbSpan, IsTransformFeedbackActive) \
X(kXfbSpan, IsTransformFeedbackPaused) \
X(kXfbSpan, GetTransformFeedbackGeneration) \
X(kXfbSpan, GetBoundTransformFeedbackLifetimeId) \
/* kProgramOp: ShaderStorageBlockBinding syncs Espryt's render state and textures */ \
X(kProgramOp, GetRenderStateParameters) \
X(kProgramOp, GetRenderStateParametersVersion) \
X(kProgramOp, GetViewport) \
X(kProgramOp, IsCapabilityEnabled) \
X(kProgramOp, GetFramebufferBindingSlot) \
X(kProgramOp, GetTextureUnitObject) \
X(kProgramOp, GetTextureContextId) \
X(kProgramOp, GetSamplingResolutionGeneration) \
X(kProgramOp, GetTextureBindGeneration) \
X(kProgramOp, GetMaxTouchedTextureUnit) \
X(kProgramOp, GetImageTextureBinding) \
/* kQuery: Magma's transform feedback query end reads the paused counter */ \
/* (DirectVulkan.cpp); every other verb in the class reads nothing and */ \
/* its fill is a serial bump */ \
X(kQuery, GetTransformFeedbackPausedPrimitiveCounter)
// clang-format on
+5
View File
@@ -85,6 +85,11 @@ namespace MobileGL::MG_Pipe {
// G5: PipeInputs field ids and the per-verb poison generations.
#include "generated/PipeFilled.inc"
// G5b: the verb enum (one per GLFunctionsTable entry), the verb classes and their
// may-read field masks - what MGPipeFillForVerb fills and what a poison build lets a
// verb read (FillPoints.def).
#include "generated/PipeFillPoints.inc"
// G6: the backend read inventory's coverage table.
#include "generated/PipeCoverage.inc"
+28
View File
@@ -0,0 +1,28 @@
// MobileGL - MobileGL/MG_Pipe/PipeInputsSwitch.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#ifndef MOBILEGL_MG_PIPE_INPUTS_SWITCH_H // belt and braces: reachable as <MG_Pipe/..> and <..> (CMakeLists.txt:531,535)
#define MOBILEGL_MG_PIPE_INPUTS_SWITCH_H
// The strangler switch (ARCHITECTURE.md 9.2). Every backend read of frontend state is spelled
// MGB_CTX->Accessor(...). Pull arm: the live GLContext, so the pull build is the tree before P1
// token for token. Push arm: the PipeInputs block the frontend fills at every verb boundary.
// The pull arm is the ONLY place under MobileGL/ outside MG_State and MG_Impl that may spell
// pGLContext; purity gate C greps MG_Backend/ for that token.
#if MOBILEGL_PIPE_PUSH
#include <MG_Backend/MGPipe/PipeInputs.h>
#define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#define MGB_CTX_LIVE (::MobileGL::MG_Pipe::gPipeInputs.IsLive())
#define MGB_CTX_IDENTITY (::MobileGL::MG_Pipe::gPipeInputs.ContextIdentity())
#else
#include <MG_State/GLState/Core.h>
#define MGB_CTX (::MobileGL::MG_State::pGLContext)
#define MGB_CTX_LIVE (::MobileGL::MG_State::pGLContext != nullptr)
#define MGB_CTX_IDENTITY (static_cast<const void*>(::MobileGL::MG_State::pGLContext.get()))
#endif
#endif
@@ -0,0 +1,300 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeFillPoints.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G5b: the verb enum, the verb classes and their may-read field masks.
//
// GENERATED by scripts/gen_pipe.py from FillPoints.def, Coverage.def and MG_Backend/BackendObject.h - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// One verb per function-pointer member of MG_Backend::GLFunctionsTable, in declaration
// order, so the enum IS the table's member list. MG_Impl spells MGP_FILL(Verb) before every
// call through the table; MGPipeFillForVerb fills exactly the fields of the verb's class
// (plus the sticky fields, OR'ed into every mask) and stamps them with the new serial. A
// read of any other field is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
enum class MGPipeVerb : Uint8 {
DrawArrays,
DrawElements,
DrawElementsBaseVertex,
MultiDrawArrays,
MultiDrawElements,
MultiDrawElementsBaseVertex,
MultiDrawElementsIndirect,
MultiDrawArraysIndirect,
MultiDrawElementsIndirectCount,
MultiDrawArraysIndirectCount,
DrawRangeElementsBaseVertex,
DrawRangeElements,
DrawElementsInstancedBaseVertexBaseInstance,
DrawElementsInstancedBaseVertex,
DrawElementsInstancedBaseInstance,
DrawElementsInstanced,
DrawArraysInstancedBaseInstance,
DrawArraysInstanced,
DrawElementsIndirect,
DrawArraysIndirect,
Clear,
ClearBufferfi,
ClearBufferfv,
ClearBufferuiv,
ClearBufferiv,
ClearNamedFramebufferfv,
ClearNamedFramebufferfi,
ClearNamedFramebufferiv,
ClearNamedFramebufferuiv,
BlitFramebuffer,
BlitNamedFramebuffer,
CopyTexImage2D,
CopyTexSubImage2D,
CopyImageSubData,
GenerateMipmap,
ReadPixels,
GetTexImage,
GetTextureImage,
DispatchCompute,
DispatchComputeIndirect,
MemoryBarrier,
MemoryBarrierByRegion,
BindImageTexture,
GetIntegeri_v,
ShaderStorageBlockBinding,
FenceSync,
ClientWaitSync,
WaitSync,
DeleteSync,
GetSyncStatus,
IsTimerQuerySupported,
BeginTimeElapsedQuery,
EndTimeElapsedQuery,
QueryCounterTimestamp,
IsQueryResultAvailable,
GetQueryResult64,
DeleteBackendQuery,
BeginOcclusionQuery,
EndOcclusionQuery,
BeginXfbPrimitivesQuery,
EndXfbPrimitivesQuery,
PatchParameteri,
BeginTransformFeedback,
EndTransformFeedback,
PauseTransformFeedback,
ResumeTransformFeedback,
BindTransformFeedback,
DeleteTransformFeedback,
GetGpuTimestampNs,
kVerbCount,
};
inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);
static_assert(kMGPipeVerbCount == 69, "the GLFunctionsTable verb set moved");
inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {
"DrawArrays",
"DrawElements",
"DrawElementsBaseVertex",
"MultiDrawArrays",
"MultiDrawElements",
"MultiDrawElementsBaseVertex",
"MultiDrawElementsIndirect",
"MultiDrawArraysIndirect",
"MultiDrawElementsIndirectCount",
"MultiDrawArraysIndirectCount",
"DrawRangeElementsBaseVertex",
"DrawRangeElements",
"DrawElementsInstancedBaseVertexBaseInstance",
"DrawElementsInstancedBaseVertex",
"DrawElementsInstancedBaseInstance",
"DrawElementsInstanced",
"DrawArraysInstancedBaseInstance",
"DrawArraysInstanced",
"DrawElementsIndirect",
"DrawArraysIndirect",
"Clear",
"ClearBufferfi",
"ClearBufferfv",
"ClearBufferuiv",
"ClearBufferiv",
"ClearNamedFramebufferfv",
"ClearNamedFramebufferfi",
"ClearNamedFramebufferiv",
"ClearNamedFramebufferuiv",
"BlitFramebuffer",
"BlitNamedFramebuffer",
"CopyTexImage2D",
"CopyTexSubImage2D",
"CopyImageSubData",
"GenerateMipmap",
"ReadPixels",
"GetTexImage",
"GetTextureImage",
"DispatchCompute",
"DispatchComputeIndirect",
"MemoryBarrier",
"MemoryBarrierByRegion",
"BindImageTexture",
"GetIntegeri_v",
"ShaderStorageBlockBinding",
"FenceSync",
"ClientWaitSync",
"WaitSync",
"DeleteSync",
"GetSyncStatus",
"IsTimerQuerySupported",
"BeginTimeElapsedQuery",
"EndTimeElapsedQuery",
"QueryCounterTimestamp",
"IsQueryResultAvailable",
"GetQueryResult64",
"DeleteBackendQuery",
"BeginOcclusionQuery",
"EndOcclusionQuery",
"BeginXfbPrimitivesQuery",
"EndXfbPrimitivesQuery",
"PatchParameteri",
"BeginTransformFeedback",
"EndTransformFeedback",
"PauseTransformFeedback",
"ResumeTransformFeedback",
"BindTransformFeedback",
"DeleteTransformFeedback",
"GetGpuTimestampNs",
};
enum class MGPipeVerbClass : Uint8 {
kDraw,
kDispatch,
kClear,
kBlitOrCopy,
kTextureOp,
kReadback,
kXfbSpan,
kProgramOp,
kQuery,
kClassCount,
};
inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);
static_assert(kMGPipeVerbClassCount == 9, "the verb class set moved");
inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {
"kDraw",
"kDispatch",
"kClear",
"kBlitOrCopy",
"kTextureOp",
"kReadback",
"kXfbSpan",
"kProgramOp",
"kQuery",
};
inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {
MGPipeVerbClass::kDraw, // DrawArrays
MGPipeVerbClass::kDraw, // DrawElements
MGPipeVerbClass::kDraw, // DrawElementsBaseVertex
MGPipeVerbClass::kDraw, // MultiDrawArrays
MGPipeVerbClass::kDraw, // MultiDrawElements
MGPipeVerbClass::kDraw, // MultiDrawElementsBaseVertex
MGPipeVerbClass::kDraw, // MultiDrawElementsIndirect
MGPipeVerbClass::kDraw, // MultiDrawArraysIndirect
MGPipeVerbClass::kDraw, // MultiDrawElementsIndirectCount
MGPipeVerbClass::kDraw, // MultiDrawArraysIndirectCount
MGPipeVerbClass::kDraw, // DrawRangeElementsBaseVertex
MGPipeVerbClass::kDraw, // DrawRangeElements
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseVertexBaseInstance
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseVertex
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseInstance
MGPipeVerbClass::kDraw, // DrawElementsInstanced
MGPipeVerbClass::kDraw, // DrawArraysInstancedBaseInstance
MGPipeVerbClass::kDraw, // DrawArraysInstanced
MGPipeVerbClass::kDraw, // DrawElementsIndirect
MGPipeVerbClass::kDraw, // DrawArraysIndirect
MGPipeVerbClass::kClear, // Clear
MGPipeVerbClass::kClear, // ClearBufferfi
MGPipeVerbClass::kClear, // ClearBufferfv
MGPipeVerbClass::kClear, // ClearBufferuiv
MGPipeVerbClass::kClear, // ClearBufferiv
MGPipeVerbClass::kClear, // ClearNamedFramebufferfv
MGPipeVerbClass::kClear, // ClearNamedFramebufferfi
MGPipeVerbClass::kClear, // ClearNamedFramebufferiv
MGPipeVerbClass::kClear, // ClearNamedFramebufferuiv
MGPipeVerbClass::kBlitOrCopy, // BlitFramebuffer
MGPipeVerbClass::kBlitOrCopy, // BlitNamedFramebuffer
MGPipeVerbClass::kBlitOrCopy, // CopyTexImage2D
MGPipeVerbClass::kBlitOrCopy, // CopyTexSubImage2D
MGPipeVerbClass::kBlitOrCopy, // CopyImageSubData
MGPipeVerbClass::kTextureOp, // GenerateMipmap
MGPipeVerbClass::kReadback, // ReadPixels
MGPipeVerbClass::kReadback, // GetTexImage
MGPipeVerbClass::kReadback, // GetTextureImage
MGPipeVerbClass::kDispatch, // DispatchCompute
MGPipeVerbClass::kDispatch, // DispatchComputeIndirect
MGPipeVerbClass::kQuery, // MemoryBarrier
MGPipeVerbClass::kQuery, // MemoryBarrierByRegion
MGPipeVerbClass::kTextureOp, // BindImageTexture
MGPipeVerbClass::kQuery, // GetIntegeri_v
MGPipeVerbClass::kProgramOp, // ShaderStorageBlockBinding
MGPipeVerbClass::kQuery, // FenceSync
MGPipeVerbClass::kQuery, // ClientWaitSync
MGPipeVerbClass::kQuery, // WaitSync
MGPipeVerbClass::kQuery, // DeleteSync
MGPipeVerbClass::kQuery, // GetSyncStatus
MGPipeVerbClass::kQuery, // IsTimerQuerySupported
MGPipeVerbClass::kQuery, // BeginTimeElapsedQuery
MGPipeVerbClass::kQuery, // EndTimeElapsedQuery
MGPipeVerbClass::kQuery, // QueryCounterTimestamp
MGPipeVerbClass::kQuery, // IsQueryResultAvailable
MGPipeVerbClass::kQuery, // GetQueryResult64
MGPipeVerbClass::kQuery, // DeleteBackendQuery
MGPipeVerbClass::kQuery, // BeginOcclusionQuery
MGPipeVerbClass::kQuery, // EndOcclusionQuery
MGPipeVerbClass::kQuery, // BeginXfbPrimitivesQuery
MGPipeVerbClass::kQuery, // EndXfbPrimitivesQuery
MGPipeVerbClass::kQuery, // PatchParameteri
MGPipeVerbClass::kXfbSpan, // BeginTransformFeedback
MGPipeVerbClass::kXfbSpan, // EndTransformFeedback
MGPipeVerbClass::kXfbSpan, // PauseTransformFeedback
MGPipeVerbClass::kXfbSpan, // ResumeTransformFeedback
MGPipeVerbClass::kXfbSpan, // BindTransformFeedback
MGPipeVerbClass::kXfbSpan, // DeleteTransformFeedback
MGPipeVerbClass::kQuery, // GetGpuTimestampNs
};
// One bit per MGPipeInputField. The 7 sticky fields are OR'ed into every class.
struct MGPipeFieldMask {
Uint64 Words[2];
};
inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
return (mask.Words[index / 64] >> (index % 64)) & 1u;
}
inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {
// kDraw: 54 fields (47 own + 7 sticky)
{{0x7ffbfff7bfffc3eeull, 0x0000000000000000ull}},
// kDispatch: 21 fields (14 own + 7 sticky)
{{0x5c00f2281d3003c0ull, 0x0000000000000000ull}},
// kClear: 25 fields (18 own + 7 sticky)
{{0x5c50ffa001347900ull, 0x0000000000000000ull}},
// kBlitOrCopy: 25 fields (18 own + 7 sticky)
{{0x5f50ffa001344101ull, 0x0000000000000000ull}},
// kTextureOp: 14 fields (7 own + 7 sticky)
{{0x5c00f22001200101ull, 0x0000000000000000ull}},
// kReadback: 22 fields (15 own + 7 sticky)
{{0x5c50f3a041300541ull, 0x0000000000000000ull}},
// kXfbSpan: 15 fields (8 own + 7 sticky)
{{0x7f0b402000000380ull, 0x0000000000000000ull}},
// kProgramOp: 18 fields (11 own + 7 sticky)
{{0x5c50f3a001300100ull, 0x0000000000000000ull}},
// kQuery: 8 fields (1 own + 7 sticky)
{{0x5c04402000000100ull, 0x0000000000000000ull}},
};
static_assert(kMGPipeInputFieldCount <= 2 * 64, "MGPipeFieldMask needs another word");
+21 -12
View File
@@ -22,8 +22,8 @@
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
enum class MGPipeInputField : Uint16 {
GetActiveTextureUnit,
@@ -87,11 +87,13 @@ enum class MGPipeInputField : Uint16 {
InvalidateCompileEnv,
ValidateProgramName,
RecordError,
GetBoundTransformFeedbackLifetimeId,
HasOpenTransformFeedbackSpan,
kFieldCount,
};
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
static_assert(kMGPipeInputFieldCount == 61, "the PipeInputs field set moved");
static_assert(kMGPipeInputFieldCount == 63, "the PipeInputs field set moved");
inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
"GetActiveTextureUnit",
@@ -155,11 +157,13 @@ inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
"InvalidateCompileEnv",
"ValidateProgramName",
"RecordError",
"GetBoundTransformFeedbackLifetimeId",
"HasOpenTransformFeedbackSpan",
};
// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each
// true has to be argued for in P1 when the fillers land: a sticky field is a field
// the poison cannot protect.
// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison
// cannot protect, so every true is argued for in Coverage.def's
// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).
inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetActiveTextureUnit
false, // GetBlendColor
@@ -169,7 +173,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetBoundVertexArray
false, // GetBufferBindingSlot
false, // GetBufferBindingPoint
false, // GetBufferBindingPointCount
true, // GetBufferBindingPointCount: keyed by target: a constexpr capacity table, not verb state
false, // GetTouchedBufferBindingPointCount
false, // GetClampReadColor
false, // GetClearColor
@@ -198,7 +202,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetPrimitiveRestartIndex
false, // GetProgramForDispatch
false, // GetProgramForDraw
false, // GetProgramObject
true, // GetProgramObject: keyed by GL name: an object lookup, not verb state
false, // GetProvokingVertexMode
false, // GetRenderStateParameters
false, // GetRenderStateParametersVersion
@@ -207,7 +211,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetStencilState
false, // GetTextureBindGeneration
false, // GetTextureContextId
false, // GetTextureObject
true, // GetTextureObject: keyed by GL name: an object lookup, not verb state
false, // GetTextureUnitObject
false, // GetTransformFeedbackCapturedVertices
false, // GetTransformFeedbackGeneration
@@ -219,10 +223,13 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // IsCapabilityEnabledIndexed
false, // IsTransformFeedbackActive
false, // IsTransformFeedbackPaused
false, // InvalidateCompileEnv
false, // ValidateProgramName
false, // RecordError
true, // InvalidateCompileEnv: reverse channel: a write into the frontend, not a state read
true, // ValidateProgramName: keyed by GL name: a name-table lookup, not verb state
true, // RecordError: reverse channel: a write into the frontend, not a state read
false, // GetBoundTransformFeedbackLifetimeId
true, // HasOpenTransformFeedbackSpan: keyed by lifetime id: an object lookup, not verb state
};
inline constexpr SizeT kMGPipeInputStickyFieldCount = 7;
// Which call is expected to have filled a field by the time a verb reads it. Names
// come from Coverage.def, so this table and the coverage table cannot disagree.
@@ -288,6 +295,8 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] =
"kClientResolved", // pseudo-call: not filled by a forward record
"kClientResolved", // pseudo-call: not filled by a forward record
"kReverseChannel", // pseudo-call: not filled by a forward record
"SetStreamOutputTargets",
"SetStreamOutputTargets",
};
struct MGPipeFilledState {
+1 -1
View File
@@ -222,7 +222,7 @@ TEST(PipeCatalogue, CoverageAccountsForEveryInventoryRow) {
// G5's field ids come from the same accessor list as the coverage table, and every field
// starts un-filled: reading one before its verb fills it is the poison's whole job.
TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
EXPECT_EQ(kMGPipeInputFieldCount, 61u);
EXPECT_EQ(kMGPipeInputFieldCount, 63u);
MGPipeFilledState state{};
state.CurrentVerbSerial = 1;
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
+186 -14
View File
@@ -8,11 +8,14 @@
# End of Source File Header
"""The seven MGPipe generators, G1..G7 (plan B section 4.1).
Reads the three hand-maintained sources of truth
Reads the four hand-maintained sources of truth
MobileGL/MG_Pipe/PipeCalls.def the call catalogue
MobileGL/MG_Pipe/PipeFields.def per-payload field lists for the verify comparator
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory,
and the sticky-field list
MobileGL/MG_Pipe/FillPoints.def verb -> class and class -> may-read field tables
(G5b), checked against MG_Backend::GLFunctionsTable
plus the vendored copy of the backend read inventory
@@ -38,6 +41,7 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PIPE_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe")
GENERATED_DIR = os.path.join(PIPE_DIR, "generated")
INVENTORY = os.path.join(REPO_ROOT, "scripts", "data", "backend_read_inventory.md")
FUNCTION_TABLE_HEADER = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend", "BackendObject.h")
GENERATED_BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
// Copyright (c) 2025-2026 MobileGL-Dev
@@ -209,7 +213,18 @@ def parse_coverage():
sys.exit("Coverage.def: MGP_COVERAGE_DELTA_LIST is missing")
for kind, call in re.findall(r"X\(([^,]+),\s*(\w+)\)", block.group(1)):
deltas.append((kind.strip(), call))
return accessors, deltas
sticky = []
block = re.search(r"#define MGP_COVERAGE_STICKY_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_STICKY_LIST is missing")
accessor_names = set(name for name, _ in accessors)
for name, reason in re.findall(r"X\((\w+)\s*,\s*\"([^\"]*)\"\)", block.group(1)):
if name not in accessor_names:
sys.exit("Coverage.def: sticky field %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST" % name)
if name in dict(sticky):
sys.exit("Coverage.def: sticky field %s is listed twice" % name)
sticky.append((name, reason))
return accessors, deltas, sticky
INVENTORY_ROW_RE = re.compile(r"^\|\s*(\d+)\s*\|([^|]*)\|([^|]*)\|([^|]*)\|")
@@ -444,8 +459,9 @@ inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
return "\n".join(out) + "\n"
def gen_filled(accessors, calls):
def gen_filled(accessors, calls, sticky):
call_names = set(c.Name for c in calls)
sticky_map = dict(sticky)
out = [banner("PipeFilled.inc", "G5: PipeInputs field ids and the per-verb poison generations.",
"Coverage.def and PipeCalls.def")]
out.append("""// One field id per GLContext accessor the backends actually read (plan B section 6.2:
@@ -458,8 +474,8 @@ def gen_filled(accessors, calls):
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
""")
out.append("enum class MGPipeInputField : Uint16 {")
for name, _ in accessors:
@@ -475,13 +491,17 @@ def gen_filled(accessors, calls):
out.append(" \"%s\"," % name)
out.append("};")
out.append("")
out.append("// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each")
out.append("// true has to be argued for in P1 when the fillers land: a sticky field is a field")
out.append("// the poison cannot protect.")
out.append("// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison")
out.append("// cannot protect, so every true is argued for in Coverage.def's")
out.append("// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).")
out.append("inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
out.append(" false, // %s" % name)
if name in sticky_map:
out.append(" true, // %s: %s" % (name, sticky_map[name]))
else:
out.append(" false, // %s" % name)
out.append("};")
out.append("inline constexpr SizeT kMGPipeInputStickyFieldCount = %d;" % len(sticky))
out.append("")
out.append("// Which call is expected to have filled a field by the time a verb reads it. Names")
out.append("// come from Coverage.def, so this table and the coverage table cannot disagree.")
@@ -510,6 +530,153 @@ inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputF
return "\n".join(out) + "\n"
FUNCTION_POINTER_MEMBER_RE = re.compile(r"\(\s*\*\s*(\w+)\s*\)\s*\(")
def parse_function_table():
"""The function-pointer members of MG_Backend::GLFunctionsTable, in declaration order.
Data members (PrefersCpuXfbPrimitiveAccounting) are not verbs and are skipped; comments
are masked line by line."""
text = read(FUNCTION_TABLE_HEADER)
start = text.find("struct GLFunctionsTable {")
if start < 0:
sys.exit("%s: struct GLFunctionsTable is missing" % FUNCTION_TABLE_HEADER)
members = []
for line in text[start:].splitlines()[1:]:
if re.match(r"^\s*};", line):
break
code = line.split("//", 1)[0]
for name in FUNCTION_POINTER_MEMBER_RE.findall(code):
members.append(name)
if not members:
sys.exit("%s: GLFunctionsTable has no function-pointer members" % FUNCTION_TABLE_HEADER)
return members
def parse_fill_points(accessors, table_members=None):
"""FillPoints.def -> (verbs, classes, fields): verbs is [(verb, class)] in file order,
classes is [class], fields is {class: [field]}. Refuses a verb set that is not exactly
GLFunctionsTable's function-pointer members in declaration order, a verb in two
classes, a class with no verbs, a field that is not an accessor, a duplicate
(class, field) row, and a class row that names no class."""
text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
if table_members is None:
table_members = parse_function_table()
def block(macro):
match = re.search(r"#define %s\(X\)(.*?)\n\n" % macro, text, re.S)
if not match:
sys.exit("FillPoints.def: %s is missing (or not followed by a blank line)" % macro)
return match.group(1)
classes = re.findall(r"X\((\w+)\)", block("MGP_FILL_CLASS_LIST"))
if len(classes) != len(set(classes)):
sys.exit("FillPoints.def: a class is listed twice in MGP_FILL_CLASS_LIST")
verbs = re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_VERB_LIST"))
seen = set()
for verb, cls in verbs:
if verb in seen:
sys.exit("FillPoints.def: verb %s is in two classes" % verb)
seen.add(verb)
if cls not in classes:
sys.exit("FillPoints.def: verb %s names unknown class %s" % (verb, cls))
verb_names = [verb for verb, _ in verbs]
missing = [m for m in table_members if m not in seen]
if missing:
sys.exit("FillPoints.def: GLFunctionsTable member(s) without a verb row: %s" % ", ".join(missing))
extra = [v for v in verb_names if v not in table_members]
if extra:
sys.exit("FillPoints.def: verb(s) that are not GLFunctionsTable members: %s" % ", ".join(extra))
if verb_names != table_members:
sys.exit("FillPoints.def: verb rows are not in GLFunctionsTable declaration order "
"(first difference at %s)" % next(a for a, b in zip(verb_names, table_members) if a != b))
for cls in classes:
if not any(c == cls for _, c in verbs):
sys.exit("FillPoints.def: class %s has no verbs" % cls)
accessor_names = set(name for name, _ in accessors)
fields = {cls: [] for cls in classes}
for cls, field in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_FIELD_LIST")):
if cls not in fields:
sys.exit("FillPoints.def: field row names unknown class %s" % cls)
if field not in accessor_names:
sys.exit("FillPoints.def: %s is not an accessor in Coverage.def" % field)
if field in fields[cls]:
sys.exit("FillPoints.def: duplicate row (%s, %s)" % (cls, field))
fields[cls].append(field)
return verbs, classes, fields
def gen_fill_points(accessors, sticky, verbs, classes, fields):
field_index = {name: i for i, (name, _) in enumerate(accessors)}
# Two words minimum (the P1 contract shape, headroom for the 64th field); grows on demand.
words = max(2, (len(accessors) + 63) // 64)
sticky_names = [name for name, _ in sticky]
out = [banner("PipeFillPoints.inc", "G5b: the verb enum, the verb classes and their may-read field masks.",
"FillPoints.def, Coverage.def and MG_Backend/BackendObject.h")]
out.append("""// One verb per function-pointer member of MG_Backend::GLFunctionsTable, in declaration
// order, so the enum IS the table's member list. MG_Impl spells MGP_FILL(Verb) before every
// call through the table; MGPipeFillForVerb fills exactly the fields of the verb's class
// (plus the sticky fields, OR'ed into every mask) and stamps them with the new serial. A
// read of any other field is Fatal{UnmigratedPipeInput, \"Field@Verb\"} in a poison build.
""")
out.append("enum class MGPipeVerb : Uint8 {")
for verb, _ in verbs:
out.append(" %s," % verb)
out.append(" kVerbCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);")
out.append("static_assert(kMGPipeVerbCount == %d, \"the GLFunctionsTable verb set moved\");" % len(verbs))
out.append("")
out.append("inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {")
for verb, _ in verbs:
out.append(" \"%s\"," % verb)
out.append("};")
out.append("")
out.append("enum class MGPipeVerbClass : Uint8 {")
for cls in classes:
out.append(" %s," % cls)
out.append(" kClassCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);")
out.append("static_assert(kMGPipeVerbClassCount == %d, \"the verb class set moved\");" % len(classes))
out.append("")
out.append("inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {")
for cls in classes:
out.append(" \"%s\"," % cls)
out.append("};")
out.append("")
out.append("inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {")
for verb, cls in verbs:
out.append(" MGPipeVerbClass::%s, // %s" % (cls, verb))
out.append("};")
out.append("")
out.append("// One bit per MGPipeInputField. The %d sticky fields are OR'ed into every class." % len(sticky))
out.append("struct MGPipeFieldMask {")
out.append(" Uint64 Words[%d];" % words)
out.append("};")
out.append("")
out.append("inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {")
out.append(" const SizeT index = static_cast<SizeT>(field);")
out.append(" return (mask.Words[index / 64] >> (index % 64)) & 1u;")
out.append("}")
out.append("")
out.append("inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {")
for cls in classes:
bits = [0] * words
names = fields[cls] + [n for n in sticky_names if n not in fields[cls]]
for name in names:
index = field_index[name]
bits[index // 64] |= 1 << (index % 64)
out.append(" // %s: %d fields (%d own + %d sticky)" % (cls, len(names), len(fields[cls]), len(names) - len(fields[cls])))
out.append(" {{%s}}," % ", ".join("0x%016xull" % b for b in bits))
out.append("};")
out.append("")
out.append("static_assert(kMGPipeInputFieldCount <= %d * 64, \"MGPipeFieldMask needs another word\");" % words)
return "\n".join(out) + "\n"
def gen_coverage(accessors, deltas, rows, calls):
call_names = set(c.Name for c in calls)
pseudo = {"kClientResolved", "kReverseChannel", "kStructuralHandle"}
@@ -635,7 +802,8 @@ def main():
calls = parse_calls()
payloads = parse_verify_payloads()
check_call_payloads_have_field_lists(calls, payloads)
accessors, deltas = parse_coverage()
accessors, deltas, sticky = parse_coverage()
verbs, classes, fields = parse_fill_points(accessors)
rows = parse_inventory()
if not os.path.isdir(GENERATED_DIR):
@@ -647,13 +815,17 @@ def main():
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWire.inc"), gen_wire(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFillPoints.inc"),
gen_fill_points(accessors, sticky, verbs, classes, fields), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeCoverage.inc"), coverage_text, args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeSpanTable.inc"), gen_span_table(), args.check, changed)
screen = sum(1 for c in calls if c.IsScreen)
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields"
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors)))
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields "
"(%d sticky), %d verbs, %d classes"
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
len(verbs), len(classes)))
print("gen_pipe: inventory %d rows: %d -> call, %d client-resolved, %d reverse-channel, "
"%d structural handle, %d UNMAPPED"
% (len(rows), mapped, pseudo["kClientResolved"], pseudo["kReverseChannel"],