diff --git a/CMakeLists.txt b/CMakeLists.txt index 8224159b..8f8f0ef9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 0c59110e..4a6dd7b6 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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: :; 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; diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index f4087c56..5c0aa591 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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 diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp new file mode 100644 index 00000000..0a9b5773 --- /dev/null +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp @@ -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 + +#include + +namespace MobileGL::MG_Pipe { + const char* MGPipeVerbName(MGPipeVerb verb) { + const auto index = static_cast(verb); + return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : ""; + } + + [[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) { + MGPipeInputPoisonFatal(field, MGPipeVerbName(verb)); + } + + Optional 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(i); + } + return std::nullopt; + } + + Optional 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(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 + Bool StorageEqual(const T& a, const T& b); + template + Bool StorageEqual(T* const& a, T* const& b); + template + Bool StorageEqual(const SharedPtr& a, const SharedPtr& b); + template + Bool StorageEqual(const T (&a)[N], const T (&b)[N]); + Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b); + template + void CorruptStorage(T& v); + template + void CorruptStorage(T*& p); + template + void CorruptStorage(SharedPtr& p); + template + 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 + Bool StorageEqual(T* const& a, T* const& b) { + return a == b; + } + template + Bool StorageEqual(const SharedPtr& a, const SharedPtr& b) { + return a.get() == b.get(); + } + template + 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 + 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 + void CorruptStorage(T*& p) { + p = nullptr; + } + template + void CorruptStorage(SharedPtr& p) { + p.reset(); + } + template + void CorruptStorage(T (&a)[N]) { + CorruptStorage(a[0]); + } + void CorruptStorage(PipeInputs::IndexedCapabilities& c) { + CorruptStorage(c.Blend); + } + template + void CorruptStorage(T& v) { + if constexpr (std::is_same_v) { + v = !v; + } else if constexpr (std::is_enum_v) { + v = static_cast(static_cast>(v) + 1); + } else if constexpr (std::is_arithmetic_v) { + v = static_cast(v + 1); + } else { + static_assert(std::is_trivially_copyable_v, "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(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 diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h new file mode 100644 index 00000000..ab21f4f2 --- /dev/null +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -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 +// 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 + +// 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 ("" 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 "" 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 MGPipeFindInputField(const char* name); + Optional 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(BufferTarget::BufferTargetCount); + static constexpr SizeT kFramebufferTargetCount = static_cast(FramebufferTarget::FramebufferTargetCount); + static constexpr SizeT kCapabilityCount = static_cast(CapabilityInput::CapabilityInputCount); + static constexpr SizeT kMaxViewports = RenderStateParameters::MAX_VIEWPORTS; + static constexpr SizeT kMaxVertexAttribs = VertexArrayObject::MAX_VERTEX_ATTRIBS; + static constexpr SizeT kStencilFaceCount = static_cast(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(target), 0); + return m_touchedBindingPointCount[static_cast(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(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(cap), 0); + const auto index = static_cast(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(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(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& 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& GetBufferBindingSlot(BufferTarget target) { + MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingSlot); + MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingSlot, static_cast(target), 0); + const auto index = static_cast(target); + if (index >= kBufferTargetCount || m_bufferBindingSlot[index] == nullptr) { + MGLOG_E("PipeInputs::GetBufferBindingSlot: no slot for target=%d", static_cast(target)); + MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingSlot, m_currentVerb); + } + return *m_bufferBindingSlot[index]; + } + BindingSlotRange1D& GetBufferBindingPoint(BufferTarget target, Uint index) { + MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingPoint); + MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingPoint, static_cast(target), index); + const auto targetIndex = static_cast(target); + if (targetIndex >= kBufferTargetCount || m_bufferBindingPointBase[targetIndex] == nullptr) { + MGLOG_E("PipeInputs::GetBufferBindingPoint: no binding points for target=%d (index=%u)", + static_cast(target), index); + MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingPoint, m_currentVerb); + } + // The live storage is Array, N> + // (BufferState.h), so base[index] is the live slot GLContext would hand out. + return m_bufferBindingPointBase[targetIndex][index]; + } + BindingSlot& GetFramebufferBindingSlot(FramebufferTarget target) { + MGP_INPUT_CHECK(MGPipeInputField::GetFramebufferBindingSlot); + MGP_INPUT_VERIFY_READ(MGPipeInputField::GetFramebufferBindingSlot, static_cast(target), 0); + const auto index = static_cast(target); + if (index >= kFramebufferTargetCount || m_framebufferBindingSlot[index] == nullptr) { + MGLOG_E("PipeInputs::GetFramebufferBindingSlot: no slot for target=%d", static_cast(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(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(unit), 0); + if (m_imageTextureBindingBase == nullptr) { + MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb); + } + return m_imageTextureBindingBase[unit]; + } + const SharedPtr& GetProgramForDispatch() { + MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDispatch); + MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDispatch, 0, 0); + return m_programForDispatch; + } + const SharedPtr& GetProgramForDraw() { + MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDraw); + MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDraw, 0, 0); + return m_programForDraw; + } + const SharedPtr& 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(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& GetProgramObject(Uint index); + const SharedPtr& 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 info); + + // ---- the storage visitor ---- + // Calls fn(a., b.) 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 + 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 m_boundVertexArray; + BindingSlot* m_bufferBindingSlot[kBufferTargetCount]{}; + BindingSlotRange1D* m_bufferBindingPointBase[kBufferTargetCount]{}; + BindingSlot* m_framebufferBindingSlot[kFramebufferTargetCount]{}; + ImageTextureBinding* m_imageTextureBindingBase = nullptr; + SharedPtr m_programForDispatch; + SharedPtr m_programForDraw; + SharedPtr 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 diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp new file mode 100644 index 00000000..9c73aec4 --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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 +#include +#include + +namespace MobileGL::MG_Pipe { + namespace { + MG_State::GLState::GLContext* LiveContext() { return MG_State::pGLContext.get(); } + + template + const SharedPtr& NullShared() { + static const SharedPtr 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::GetProgramObject(Uint index) { + auto* ctx = LiveContext(); + return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared(); + } + + const SharedPtr& PipeInputs::GetTextureObject(Uint index) { + auto* ctx = LiveContext(); + return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared(); + } + + 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 info) { + auto* ctx = LiveContext(); + if (ctx == nullptr) { + MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast(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 diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.h b/MobileGL/MG_Impl/Pipe/PipeFill.h new file mode 100644 index 00000000..73cea5eb --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/PipeFill.h @@ -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 +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 diff --git a/MobileGL/MG_Pipe/Coverage.def b/MobileGL/MG_Pipe/Coverage.def index f5ee58e1..583b5af0 100644 --- a/MobileGL/MG_Pipe/Coverage.def +++ b/MobileGL/MG_Pipe/Coverage.def @@ -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 diff --git a/MobileGL/MG_Pipe/FillPoints.def b/MobileGL/MG_Pipe/FillPoints.def new file mode 100644 index 00000000..ab15bbea --- /dev/null +++ b/MobileGL/MG_Pipe/FillPoints.def @@ -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 diff --git a/MobileGL/MG_Pipe/MGPipe.h b/MobileGL/MG_Pipe/MGPipe.h index 85e26d81..5a1db9df 100644 --- a/MobileGL/MG_Pipe/MGPipe.h +++ b/MobileGL/MG_Pipe/MGPipe.h @@ -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" diff --git a/MobileGL/MG_Pipe/PipeInputsSwitch.h b/MobileGL/MG_Pipe/PipeInputsSwitch.h new file mode 100644 index 00000000..317e9882 --- /dev/null +++ b/MobileGL/MG_Pipe/PipeInputsSwitch.h @@ -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 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 +#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 +#define MGB_CTX (::MobileGL::MG_State::pGLContext) +#define MGB_CTX_LIVE (::MobileGL::MG_State::pGLContext != nullptr) +#define MGB_CTX_IDENTITY (static_cast(::MobileGL::MG_State::pGLContext.get())) +#endif +#endif diff --git a/MobileGL/MG_Pipe/generated/PipeFillPoints.inc b/MobileGL/MG_Pipe/generated/PipeFillPoints.inc new file mode 100644 index 00000000..1afd3e51 --- /dev/null +++ b/MobileGL/MG_Pipe/generated/PipeFillPoints.inc @@ -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(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(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(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"); diff --git a/MobileGL/MG_Pipe/generated/PipeFilled.inc b/MobileGL/MG_Pipe/generated/PipeFilled.inc index 7dec6e86..91e82514 100644 --- a/MobileGL/MG_Pipe/generated/PipeFilled.inc +++ b/MobileGL/MG_Pipe/generated/PipeFilled.inc @@ -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(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 { diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 5ce193a4..42578b3a 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -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)); diff --git a/scripts/gen_pipe.py b/scripts/gen_pipe.py index 79588593..2a644d80 100644 --- a/scripts/gen_pipe.py +++ b/scripts/gen_pipe.py @@ -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(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(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(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"],