Files
MobileGL/MobileGL/MG_Impl/Pipe/PipeFill.cpp
T
swung0x48 62a7786184 [Fix] (Pipe, Purity): end a declared verb honestly, and gate the header MG_State now includes
- MGPipeLeaveVerb() bumps the serial and puts the current verb back to none, so a test that
  drives a backend helper directly stops declaring where it says it stops and a later
  unguarded read aborts as "<Field>@<none>" instead of naming an unrelated verb
- check_include_closure.py gains a fourth probe: F2 put MGP_NOTE_MUTATION into frontend
  mutators, so MG_State includes MG_Pipe/PipeMutation.h and that header must never reach
  back into MG_State, MG_Impl or a backend
2026-09-06 06:15:56 -04:00

615 lines
31 KiB
C++

// 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, IsLive, the MOBILEGL_PIPE_POISON_OMIT knob and - in a verify build - the
// second arm (SnapshotFromGLContext), the entry compare, the compare-at-read hook and the
// MOBILEGL_PIPE_VERIFY_CORRUPT / _FATAL knobs. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there).
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Pipe/PipeMutation.h>
#include <Config.h>
#include <atomic>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Pipe {
using GLContext = MG_State::GLState::GLContext;
// The one door into PipeInputs' storage on the client side. A struct rather than a
// list of friend functions so the header names exactly one friend.
struct MGPipeFillAccess {
// Copies ONE field's storage out of the live context by calling the GLContext
// accessor of the same name (P1 brief D4: no derivation logic is re-implemented
// here, which is what keeps the copy semantically identical by construction).
// A forwarded field has no storage and copies nothing.
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
using F = MGPipeInputField;
using MG_State::GLState::BufferBindPointTargets;
using MG_State::GLState::GlobalBufferTargets;
switch (field) {
case F::GetActiveTextureUnit:
dst.m_activeTextureUnit = ctx.GetActiveTextureUnit();
break;
case F::GetBlendColor:
dst.m_blendColor = ctx.GetBlendColor();
break;
case F::GetBlendEquationIndexed:
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
ctx.GetBlendEquationIndexed(i, dst.m_blendEquation[i][0], dst.m_blendEquation[i][1]);
}
break;
case F::GetBlendFuncIndexed:
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
ctx.GetBlendFuncIndexed(i, dst.m_blendFunc[i][0], dst.m_blendFunc[i][1], dst.m_blendFunc[i][2],
dst.m_blendFunc[i][3]);
}
break;
case F::GetBoundTransformFeedbackName:
dst.m_boundTransformFeedbackName = ctx.GetBoundTransformFeedbackName();
break;
case F::GetBoundVertexArray:
dst.m_boundVertexArray = ctx.GetBoundVertexArray();
break;
case F::GetBufferBindingSlot:
// Every global target has a slot; Index stays null - GLContext resolves it
// through the bound VAO's element-buffer slot (Core.cpp), a derivation no
// FillPoints.def row can copy - and a read of it is the poison Fatal in the
// accessor. No backend reads it today (every slot read is DrawIndirect,
// DispatchIndirect, Parameter or PixelPack).
for (const auto target : GlobalBufferTargets) {
dst.m_bufferBindingSlot[static_cast<SizeT>(target)] = &ctx.GetBufferBindingSlot(target);
}
break;
case F::GetBufferBindingPoint:
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
// (BufferState.h), so the address of point 0 is the base of that target's row.
for (const auto target : BufferBindPointTargets) {
dst.m_bufferBindingPointBase[static_cast<SizeT>(target)] = &ctx.GetBufferBindingPoint(target, 0);
}
break;
case F::GetTouchedBufferBindingPointCount:
for (const auto target : BufferBindPointTargets) {
dst.m_touchedBindingPointCount[static_cast<SizeT>(target)] =
ctx.GetTouchedBufferBindingPointCount(target);
}
break;
case F::GetClampReadColor:
dst.m_clampReadColor = ctx.GetClampReadColor();
break;
case F::GetClearColor:
dst.m_clearColor = ctx.GetClearColor();
break;
case F::GetClearDepth:
dst.m_clearDepth = ctx.GetClearDepth();
break;
case F::GetClearStencil:
dst.m_clearStencil = ctx.GetClearStencil();
break;
case F::GetColorMaskIndexed:
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
dst.m_colorMask[i] = ctx.GetColorMaskIndexed(i);
}
break;
case F::GetCullFaceMode:
dst.m_cullFaceMode = ctx.GetCullFaceMode();
break;
case F::GetCurrentVertexAttribute:
for (Uint i = 0; i < PipeInputs::kMaxVertexAttribs; ++i) {
dst.m_currentVertexAttribute[i] = ctx.GetCurrentVertexAttribute(i);
}
break;
case F::GetDepthFunc:
dst.m_depthFunc = ctx.GetDepthFunc();
break;
case F::GetDepthMask:
dst.m_depthMask = ctx.GetDepthMask();
break;
case F::GetDepthRangeIndexed:
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
dst.m_depthRange[i] = ctx.GetDepthRangeIndexed(i);
}
break;
case F::GetFramebufferBindingSlot:
for (SizeT i = 0; i < PipeInputs::kFramebufferTargetCount; ++i) {
dst.m_framebufferBindingSlot[i] =
&ctx.GetFramebufferBindingSlot(static_cast<PipeInputs::FramebufferTarget>(i));
}
break;
case F::GetImageTextureBinding:
// Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> (TextureState.h): unit 0's
// address is the base.
dst.m_imageTextureBindingBase = &ctx.GetImageTextureBinding(0);
break;
case F::GetLineWidth:
dst.m_lineWidth = ctx.GetLineWidth();
break;
case F::GetLogicOp:
dst.m_logicOp = ctx.GetLogicOp();
break;
case F::GetMaxTouchedTextureUnit:
dst.m_maxTouchedTextureUnit = ctx.GetMaxTouchedTextureUnit();
break;
case F::GetMinSampleShadingValue:
dst.m_minSampleShadingValue = ctx.GetMinSampleShadingValue();
break;
case F::GetPatchDefaultInnerLevel:
dst.m_patchDefaultInnerLevel = ctx.GetPatchDefaultInnerLevel();
break;
case F::GetPatchDefaultOuterLevel:
dst.m_patchDefaultOuterLevel = ctx.GetPatchDefaultOuterLevel();
break;
case F::GetPatchVertices:
dst.m_patchVertices = ctx.GetPatchVertices();
break;
case F::GetPipelineStateVersion:
dst.m_pipelineStateVersion = ctx.GetPipelineStateVersion();
break;
case F::GetPixelStoreParameters:
dst.m_pixelStore[0] = ctx.GetPixelStoreParameters(false);
dst.m_pixelStore[1] = ctx.GetPixelStoreParameters(true);
break;
case F::GetPolygonModeFront:
dst.m_polygonModeFront = ctx.GetPolygonModeFront();
break;
case F::GetPolygonOffsetFactor:
dst.m_polygonOffsetFactor = ctx.GetPolygonOffsetFactor();
break;
case F::GetPolygonOffsetUnits:
dst.m_polygonOffsetUnits = ctx.GetPolygonOffsetUnits();
break;
case F::GetPrimitiveRestartIndex:
dst.m_primitiveRestartIndex = ctx.GetPrimitiveRestartIndex();
break;
case F::GetProgramForDispatch:
dst.m_programForDispatch = ctx.GetProgramForDispatch();
break;
case F::GetProgramForDraw:
dst.m_programForDraw = ctx.GetProgramForDraw();
break;
case F::GetProvokingVertexMode:
dst.m_provokingVertexMode = ctx.GetProvokingVertexMode();
break;
case F::GetRenderStateParameters:
dst.m_renderState = ctx.GetRenderStateParameters();
break;
case F::GetRenderStateParametersVersion:
dst.m_renderStateParametersVersion = ctx.GetRenderStateParametersVersion();
break;
case F::GetSamplingResolutionGeneration:
dst.m_samplingResolutionGeneration = ctx.GetSamplingResolutionGeneration();
break;
case F::GetScissorBox:
dst.m_scissorBox = ctx.GetScissorBox();
break;
case F::GetStencilState:
dst.m_stencil[0] = ctx.GetStencilState(StencilFace::Front);
dst.m_stencil[1] = ctx.GetStencilState(StencilFace::Back);
break;
case F::GetTextureBindGeneration:
dst.m_textureBindGeneration = ctx.GetTextureBindGeneration();
break;
case F::GetTextureContextId:
dst.m_textureContextId = ctx.GetTextureContextId();
break;
case F::GetTextureUnitObject:
// Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> (TextureState.h): unit 0 is the base.
dst.m_textureUnitBase = &ctx.GetTextureUnitObject(0);
break;
case F::GetTransformFeedbackCapturedVertices:
dst.m_transformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices();
break;
case F::GetTransformFeedbackGeneration:
dst.m_transformFeedbackGeneration = ctx.GetTransformFeedbackGeneration();
break;
case F::GetTransformFeedbackPausedPrimitiveCounter:
dst.m_transformFeedbackPausedPrimitiveCounter = ctx.GetTransformFeedbackPausedPrimitiveCounter();
break;
case F::GetTransformFeedbackProgram:
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
break;
case F::GetViewport:
dst.m_viewport = ctx.GetViewport();
break;
case F::GetViewportIndexed:
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
dst.m_viewportIndexed[i] = ctx.GetViewportIndexed(i);
}
break;
case F::IsCapabilityEnabled:
// Every capability, FramebufferSrgb included: it copies today's constant false
// (MEASUREMENTS.md), so no value changes.
for (SizeT i = 0; i < PipeInputs::kCapabilityCount; ++i) {
dst.m_capability[i] = ctx.IsCapabilityEnabled(static_cast<CapabilityInput>(i));
}
break;
case F::IsCapabilityEnabledIndexed:
// The only two indexed capabilities GLContext keeps (RenderState).
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
dst.m_capabilityIndexed.Blend[i] = ctx.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
}
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
dst.m_capabilityIndexed.ScissorTest[i] =
ctx.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i);
}
break;
case F::IsTransformFeedbackActive:
dst.m_transformFeedbackActive = ctx.IsTransformFeedbackActive();
break;
case F::IsTransformFeedbackPaused:
dst.m_transformFeedbackPaused = ctx.IsTransformFeedbackPaused();
break;
case F::GetBoundTransformFeedbackLifetimeId:
dst.m_boundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId();
break;
// The seven forwarded fields: nothing to copy.
case F::GetBufferBindingPointCount:
case F::GetProgramObject:
case F::GetTextureObject:
case F::HasOpenTransformFeedbackSpan:
case F::InvalidateCompileEnv:
case F::ValidateProgramName:
case F::RecordError:
case F::kFieldCount:
break;
}
}
static void SetIdentity(PipeInputs& inputs, GLContext* ctx) {
inputs.m_live = ctx != nullptr;
inputs.m_contextIdentity = ctx;
}
static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; }
#if MOBILEGL_PIPE_POISON
static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; }
#endif
};
namespace {
GLContext* LiveContext() { return MG_State::pGLContext.get(); }
template <class T>
const SharedPtr<T>& NullShared() {
static const SharedPtr<T> null;
return null;
}
[[noreturn]] void BadKnob(const char* knob, const char* value, const char* why) {
MGLOG_F("MGPipe: Fatal{PipeVerifyBadKnob, \"%s=%s\": %s}", knob, value, why);
std::abort();
}
// ---- MOBILEGL_PIPE_POISON_OMIT (negative control B, P1 brief D6) ----
// The filler skips the STAMP (never the value) of one (verb, field) pair: an omission
// indistinguishable from a forgotten FillPoints.def row, so that verb's read of the
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected.
struct PoisonOmission {
Bool Armed = false;
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
MGPipeInputField Field = MGPipeInputField::kFieldCount;
};
PoisonOmission g_omission;
Bool g_omissionKnobParsed = false;
String g_omissionKnobValue; // the value the last parse saw
// Parsed on the first fill and again only when the value changes. A lane loads
// Features once, before any fill, so that is one parse per process there; a forked
// test child that sets Features after its parent already filled gets its own parse,
// which is what puts the parser and its Fatal{PipeVerifyBadKnob} under a unit test.
// An empty value never clears an omission a test armed through MGPipeSetPoisonOmission.
void ParsePoisonOmissionKnob() {
const String& knob = MG_Config::Features.PipePoisonOmit;
if (g_omissionKnobParsed && knob == g_omissionKnobValue) return;
g_omissionKnobParsed = true;
g_omissionKnobValue = knob;
if (knob.empty()) return;
const auto colon = knob.find(':');
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) {
BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "expected <Verb>:<FieldName>");
}
const String verbName = knob.substr(0, colon);
const String fieldName = knob.substr(colon + 1);
const auto verb = MGPipeFindVerb(verbName.c_str());
if (!verb) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such verb in kMGPipeVerbNames");
const auto field = MGPipeFindInputField(fieldName.c_str());
if (!field) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such field in kMGPipeInputFieldNames");
MGPipeSetPoisonOmission(verbName.c_str(), fieldName.c_str());
}
[[maybe_unused]] Bool IsOmitted(MGPipeVerb verb, MGPipeInputField field) {
return g_omission.Armed && g_omission.Verb == verb && g_omission.Field == field;
}
#if MOBILEGL_PIPE_VERIFY
// ---- the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8) ----
// Two mechanisms, both active only when Features.PipeVerify is set: the ENTRY compare
// once per verb (the pushed block against a second snapshot of the live context,
// taken at the same instant - tautological until P2 gives the first arm a real
// filler, and kept falsifiable by MOBILEGL_PIPE_VERIFY_CORRUPT), and the
// COMPARE-AT-READ in every accessor (the stored value against a fresh read of the
// live context at the moment the backend reads it - the arm that is real in P1: it
// catches a value that changed between the verb boundary and the read).
PipeInputs g_snapshot{}; // the second arm
PipeInputs g_readScratch{}; // where the compare-at-read re-read lands
// The read hook arms at the first fill (ArmVerify below), so it cannot see a read
// made before that. That window is covered by the poison instead: MGP_INPUT_CHECK
// precedes MGP_INPUT_VERIFY_READ in every accessor and a stamp of 0 is never fresh,
// so such a read is Fatal{UnmigratedPipeInput, "<Field>@<none>"} before the hook
// could matter - which holds only while a verify build always carries the poison.
static_assert(MOBILEGL_PIPE_POISON, "the compare-at-read hook relies on the poison for reads before the first fill");
struct VerifyState {
Bool Parsed = false;
Bool Enabled = false;
Bool Fatal = true;
Bool InHook = false; // a re-read that re-enters an accessor is not re-verified
Optional<MGPipeInputField> Corrupt;
String CorruptKnob; // the MOBILEGL_PIPE_VERIFY_CORRUPT value the last arm saw
std::atomic<Uint64> Divergences{0};
~VerifyState() {
const Uint64 count = Divergences.load(std::memory_order_relaxed);
if (count != 0) {
MGLOG_E("MGPipe: verify summary - %llu divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0",
static_cast<unsigned long long>(count));
}
}
};
VerifyState g_verify;
// Armed on the first fill and re-armed when any of the three verify knobs'
// Features value (PipeVerify, PipeVerifyFatal, PipeVerifyCorrupt) differs from what
// the last arm latched (the same reason as ParsePoisonOmissionKnob: one arm per lane
// process, a fresh arm for a forked test child that turns a knob after its parent
// filled). Cost: two Bool compares and one String compare per fill, verify builds only.
void ArmVerify() {
const auto& features = MG_Config::Features;
if (g_verify.Parsed && g_verify.Enabled == features.PipeVerify && g_verify.Fatal == features.PipeVerifyFatal &&
g_verify.CorruptKnob == features.PipeVerifyCorrupt) {
return;
}
g_verify.Parsed = true;
g_verify.Enabled = features.PipeVerify;
g_verify.Fatal = features.PipeVerifyFatal;
g_verify.CorruptKnob = features.PipeVerifyCorrupt;
g_verify.Corrupt = Optional<MGPipeInputField>{};
if (!g_verify.Enabled) return;
const String& corrupt = g_verify.CorruptKnob;
if (!corrupt.empty()) {
const auto field = MGPipeFindInputField(corrupt.c_str());
if (!field) {
BadKnob("MOBILEGL_PIPE_VERIFY_CORRUPT", corrupt.c_str(), "no such field in kMGPipeInputFieldNames");
}
g_verify.Corrupt = field;
}
// The lanes grep for this line: a verify run whose log lacks it never armed.
MGLOG_I("MGPipe: verify armed - %u fields, %u verbs, fatal=%d", static_cast<unsigned>(kMGPipeInputFieldCount),
static_cast<unsigned>(kMGPipeVerbCount), g_verify.Fatal ? 1 : 0);
if (g_verify.Corrupt) {
MGLOG_I("MGPipe: verify corruption armed - %s", kMGPipeInputFieldNames[static_cast<SizeT>(*g_verify.Corrupt)]);
}
}
void ReportDivergence(MGPipeInputField field, const char* where) {
const Uint64 serial = MGPipeFillAccess::Filled(gPipeInputs).CurrentVerbSerial;
MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(gPipeInputs.CurrentVerb()),
static_cast<unsigned long long>(serial), where);
if (g_verify.Fatal) std::abort();
g_verify.Divergences.fetch_add(1, std::memory_order_relaxed);
}
void EntryCompare(PipeInputs& inputs, const MGPipeFieldMask& mask) {
if (!g_verify.Enabled) return;
SnapshotFromGLContext(g_snapshot, mask);
// Negative control A: perturb the SNAPSHOT arm, so a green run goes red naming the
// field. A field outside this verb's mask is not compared and stays untouched.
if (g_verify.Corrupt && MGPipeFieldMaskHas(mask, *g_verify.Corrupt)) {
MGPipeApplyVerifyCorruption(g_snapshot, *g_verify.Corrupt);
}
MGPipeInputField differing = MGPipeInputField::kFieldCount;
if (!MGPipeVerifyInputs(inputs, g_snapshot, mask, &differing)) ReportDivergence(differing, "entry");
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace
#if MOBILEGL_PIPE_VERIFY
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask) {
auto* ctx = LiveContext();
MGPipeFillAccess::SetIdentity(snapshot, ctx);
MGPipeFillAccess::SetVerb(snapshot, gPipeInputs.CurrentVerb());
if (ctx == nullptr) return;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field) || kMGPipeInputFieldSticky[i]) continue;
MGPipeFillAccess::CopyField(snapshot, *ctx, field);
}
}
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1) {
if (&self != &gPipeInputs || !g_verify.Enabled || g_verify.InHook) return;
const auto index = static_cast<SizeT>(field);
if (kMGPipeInputFieldSticky[index]) return;
auto* ctx = LiveContext();
if (ctx == nullptr) return;
// The whole field is re-read and compared - a superset of "the same indices", so a
// divergence in an index the backend did not ask for is still a divergence between
// the boundary value and the live value. The indices only decorate the report. The
// cost is per backend read (GetRenderStateParameters re-copies and compares the whole
// struct; GetProgramForDraw re-joins the pending link), inside the verify budget and
// to be kept in mind when reading the verify lane's wall time.
// InHook: the re-read calls the same GLContext accessor the filler calls, and
// GetProgramForDraw's join can re-enter a backend and with it another gPipeInputs
// accessor; that inner read is a plain load rather than a second hook, so the hook
// never recurses (and never reports the inner read against a half-copied scratch).
g_verify.InHook = true;
MGPipeFillAccess::CopyField(g_readScratch, *ctx, field);
const Bool equal = MGPipeInputsFieldEqual(field, self, g_readScratch);
g_verify.InHook = false;
if (equal) return;
MGLOG_E("MGPipe: verify read of %s (index %u, %u) differs from the live context", kMGPipeInputFieldNames[index],
index0, index1);
ReportDivergence(field, "read");
}
#endif // MOBILEGL_PIPE_VERIFY
// ---- push on mutation (P1 lane finding F2) ----
// A backend that writes a frontend object inside its own verb moves a value the verb
// boundary already copied: Magma's ResolveSamplerDescriptor synthesises a fallback
// texture for an unbound sampler and its AllocateStorage/SetInternalFormat bump the
// context's sampling-resolution generation, so every read of that field after the
// fallback differs from the live context (the two SampledSetStaleness / six
// UnboundImageDescriptor entries the verify lane aborted on). The frontend mutator
// spells MGP_NOTE_MUTATION(Field) at the point of the move and lands here.
//
// Only the value is refreshed. The stamp is deliberately left alone: a field whose stamp
// this verb withheld (negative control B) must stay stale, and a field the verb never
// filled must stay Fatal{UnmigratedPipeInput} on the next read rather than be healed by
// an unrelated frontend write.
void MGPipeNoteFrontendMutation(MGPipeInputField field) {
PipeInputs& inputs = gPipeInputs;
auto* ctx = LiveContext();
if (ctx == nullptr) return;
const auto verb = inputs.CurrentVerb();
if (verb == MGPipeVerb::kVerbCount) return; // nothing has filled the block yet
const auto index = static_cast<SizeT>(field);
if (kMGPipeInputFieldSticky[index]) return; // forwarded: no storage to refresh
const MGPipeFieldMask& mask =
kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
if (!MGPipeFieldMaskHas(mask, field)) return; // this verb never pushed it
MGPipeFillAccess::CopyField(inputs, *ctx, field);
}
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
if (verb == nullptr || field == nullptr) {
g_omission = PoisonOmission{};
return;
}
const auto v = MGPipeFindVerb(verb);
const auto f = MGPipeFindInputField(field);
if (!v || !f) BadKnob("MOBILEGL_PIPE_POISON_OMIT", verb, "unknown verb or field");
g_omission.Armed = true;
g_omission.Verb = *v;
g_omission.Field = *f;
#if MOBILEGL_PIPE_POISON
MGLOG_I("MGPipe: poison omission armed - %s@%s", field, verb);
#else
MGLOG_W_ONCE("MGPipe: poison omission %s@%s requested but the poison is not compiled in "
"(MOBILEGL_PIPE_POISON=0): no stamp exists to omit",
field, verb);
#endif
}
// ---- liveness ----
Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; }
// ---- the seven F-class forwarders ----
SizeT PipeInputs::GetBufferBindingPointCount(BufferTarget target) const {
const auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetBufferBindingPointCount(target) : 0;
}
const SharedPtr<PipeInputs::ProgramObject>& PipeInputs::GetProgramObject(Uint index) {
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared<ProgramObject>();
}
const SharedPtr<PipeInputs::ITextureObject>& PipeInputs::GetTextureObject(Uint index) {
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared<ITextureObject>();
}
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
}
void PipeInputs::InvalidateCompileEnv() {
if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
}
Bool PipeInputs::ValidateProgramName(Uint index) const {
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->ValidateProgramName(index);
}
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
auto* ctx = LiveContext();
if (ctx == nullptr) {
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
return;
}
ctx->RecordError(code, Move(info));
}
void MGPipeLeaveVerb() {
PipeInputs& inputs = gPipeInputs;
#if MOBILEGL_PIPE_POISON
// Same bump the next fill would make, without a verb to fill from: no field is
// stamped, so every stamp this verb made falls behind the serial.
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
#endif
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
}
// ---- the filler ----
void MGPipeFillForVerb(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs;
ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY
ArmVerify();
#else
// The runtime knob without the compiled comparator is a no-op that would look green;
// this warning is what a lane's arming assertion turns into red.
if (MG_Config::Features.PipeVerify) {
MGLOG_W_ONCE("MGPipe: MOBILEGL_PIPE_VERIFY=1 requested but the comparator is not compiled in "
"(configure with -DMOBILEGL_PIPE_VERIFY=ON)");
}
#endif
#if MOBILEGL_PIPE_POISON
MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs);
// Starts at 1: FilledGen == 0 is "never filled", and MGPipeInputFieldIsFresh refuses
// it on both branches, so a read before this first bump is
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
++filled.CurrentVerbSerial;
#endif
MGPipeFillAccess::SetVerb(inputs, verb);
auto* ctx = LiveContext();
MGPipeFillAccess::SetIdentity(inputs, ctx);
if (ctx == nullptr) return;
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field)) continue;
#if MOBILEGL_PIPE_POISON
if (kMGPipeInputFieldSticky[i]) {
// Stamped once by the first fill that sees a live context; fresh through the
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
continue;
}
#else
if (kMGPipeInputFieldSticky[i]) continue;
#endif
MGPipeFillAccess::CopyField(inputs, *ctx, field);
#if MOBILEGL_PIPE_POISON
// The value is copied either way; only the stamp is withheld for the omitted pair.
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
#endif
}
#if MOBILEGL_PIPE_VERIFY
EntryCompare(inputs, mask);
#endif
}
} // namespace MobileGL::MG_Pipe