mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
- The writer side recorded a member-rooted write bound to a reference (`for (auto& blendState : m_parameters.BlendStates)`) as the field alone, so seven RenderState setters read as writing nothing; the reader side resolved `render.PatchVertices` to the whole of m_parameters, so every setter that touched any byte of it "supported" NEW_PATCH_STATE and a row saying glClearColor publishes the patch state was green. Both were the same defect: the two sides did not resolve to the same token. - Both sides now carry MEM:<member> and FIELD:<member>.<leaf>; a whole-member write or read is every field. A reference, pointer or range-for alias bound to a member-rooted lvalue is followed (rebinds and aliases of aliases included), a write through a call-result lvalue and a mutating call on a member-rooted lvalue count as whole writes, a const alias cannot be written through with `.`. - A write, or a non-read-only method call, whose root the analysis cannot place - a reference parameter, a call result, a member without the m_ prefix, an unattributable assignment operator - taints the function; the taint rides the call-graph fixed point and every (row, bit) that depends on a tainted function is UNDECIDED, never a verdict. Nothing is trusted by name. - Match rule: a writer supports a bit iff the two sides share a member and, both field-resolved, their field sets intersect; a member in common with no field information on one side is COARSE, reported and never counted; UNDER-FIRING only when both sides are resolved and disjoint for every member the shutter reads. - --check counts only supported answers as derived, prints the COARSE and UNDECIDED tallies, and fails on an UNDECIDED row unless MGP_DIRTY_SURFACE_UNDECIDED_LIST in DirtySurface.def marks it; a mark on a row the derivation decides is a red gate too. The list is empty: all 8 non-render bit answers are supported at field level, and NEW_PATCH_STATE has exactly three legal carriers again. - --self-test grows from 10 to 21 negative controls, including the synthetic bodies of every shape above through the real extractor, the NEW_PATCH_STATE analogue of the value-class control, the taint, COARSE and stale-mark paths, and positive controls for SetPixelStoreParam's pasted writes and the seven alias setters.
288 lines
24 KiB
Modula-2
288 lines
24 KiB
Modula-2
// MobileGL - MobileGL/MG_Pipe/DirtySurface.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 dirty-surface mapping (ARCHITECTURE.md 5.2 corollary 4, P2 brief D16).
|
|
//
|
|
// MGPipe replaces "the backend rediscovers what changed" with "the frontend says what
|
|
// changed", which only works if EVERY frontend mutation a backend can observe has an answer
|
|
// to "what publishes this". The failure mode is silent and one-directional: a mutation that
|
|
// forgets to publish renders stale, and no purity gate can see it.
|
|
//
|
|
// So the surface is enumerated MECHANICALLY. scripts/gen_pipe_dirty_surface.py scans
|
|
// MG_Impl/GLImpl for every pGLContext-> mutator call and, with --check, fails if a scanned
|
|
// mutator has no row here or a row here names a mutator the scan no longer finds. Both
|
|
// directions, so a deleted mutator cannot leave a stale row behind either.
|
|
//
|
|
// ANSWERS. A row lists EVERY publisher that fires on EVERY path through that mutator,
|
|
// and only those; several are joined with '|'. A publisher that fires on some paths but
|
|
// not all must not appear, because a shutter built from this file would then UNDER-fire,
|
|
// and ARCHITECTURE.md 13.2 names under-firing as the dangerous direction. The one row that
|
|
// carries a bit which fires on only some paths says so in its answer - kPulledPartialShutter
|
|
// joined with that bit - because the alternative, dropping the bit, tells a reader of this
|
|
// file that a bit P2 already emits a call for has no shutter at all.
|
|
//
|
|
// "EVERY PATH" MEANS EVERY PATH THAT MUTATES. A setter that returns early because the value
|
|
// did not change publishes nothing and needs to publish nothing - there is no mutation to
|
|
// carry - so a redundant-write guard (SetColorMask's `if (changed) BumpVersions();`, the
|
|
// BitwiseEqual guards on the patch levels) does not make its publisher conditional in the
|
|
// sense this rule cares about. A publisher reached on only SOME of the paths that DO mutate
|
|
// - SetCapability's ClipDistance arms, SetStencilFunc's reference-only call - is the thing
|
|
// that must not be named.
|
|
//
|
|
// EVERY BIT ANSWER IN THIS FILE IS DERIVED AND CHECKED, in two families and one
|
|
// direction. The RenderState family (45 rows) is checked both ways against RenderState.cpp,
|
|
// below. Every other NEW_* answer is checked against the shutter Tracker.h builds for that
|
|
// bit: gen_pipe_dirty_surface.py resolves what the shutter READS and what each mutator
|
|
// transitively WRITES (through MGP_NOTE_AGGREGATE too, whose hop it reads out of
|
|
// MGPipeNoteAggregate's own switch, and through the function-like macros of MG_State, which
|
|
// it EXPANDS - sixteen of RenderState.cpp's writes exist only after the preprocessor has
|
|
// pasted them together) to the SAME two-level token, MEM:<member> plus FIELD:<member>.<leaf>,
|
|
// and fails a row that names a bit whose shutter its mutator moves on no path at all. That
|
|
// half is one-directional on purpose - "it does write something the shutter reads" cannot
|
|
// prove it does so on EVERY path - so it catches under-firing and not over-claiming.
|
|
//
|
|
// AN ABSENCE CLAIM IS ONLY WORTH THE READING BEHIND IT, and this gate learned that twice:
|
|
// its write analysis first recorded a write through a member's field as the field alone
|
|
// and never the member, so --check printed, as a fact about RenderState.cpp, that
|
|
// SetPixelStoreParam "writes nothing NEW_PIXEL_PACK's shutter reads" about a setter whose
|
|
// whole body is sixteen writes to exactly that member; then, once it read the member, it
|
|
// still could not see a write through a REFERENCE (SetBlendEquation's `for (auto&
|
|
// blendState : m_parameters.BlendStates)`) and said the same false thing about seven more
|
|
// setters - while its reader side resolved `render.PatchVertices` to the WHOLE of
|
|
// m_parameters, so every setter that touched any byte of it "supported" NEW_PATCH_STATE and
|
|
// a row saying glClearColor publishes the patch state was green. So now: a reference or
|
|
// pointer bound to a member-rooted lvalue is followed, and its writes are credited to the
|
|
// member and the field it was bound to; a write whose root the analysis cannot place (a
|
|
// reference parameter, a call result, a member without the m_ prefix, a token it could not
|
|
// expand) TAINTS the function, and every answer that depends on a tainted function is
|
|
// UNDECIDED - printed with its reason, never a verdict; a writer supports a bit only when
|
|
// the two sides share a member AND, both resolved to fields, their field sets intersect (a
|
|
// whole-member write or read is every field); a member in common with no field information
|
|
// on one side is COARSE, reported and never counted. --check counts only the supported
|
|
// answers as derived, prints the COARSE and UNDECIDED tallies, and FAILS on an UNDECIDED
|
|
// row unless MGP_DIRTY_SURFACE_UNDECIDED_LIST at the bottom of this file marks it - a
|
|
// mark that outlives its reason is a red gate too. What it still cannot claim: a shutter
|
|
// member written outside MG_State/GLState + MG_Impl/Pipe is undecided in the absence
|
|
// direction, a call is resolved by NAME to every body of that name, and a FIELD token is
|
|
// not scoped to a type - all three only widen what a mutator is credited with, and the
|
|
// second is also how a taint spreads.
|
|
//
|
|
// The prose answers (kImmediate, kExplicitDestroy, kUnpublishedDestroy, kNoBackendRead,
|
|
// kPulledEveryVerb, kPulledPartialShutter, kReverseChannel) are statements no derivation
|
|
// checks - except the bits a kPulledPartialShutter row names, which are checked like any
|
|
// other bit answer. --check prints how many rows carry a prose answer, so "all mapped" can
|
|
// never be read as "all verified".
|
|
//
|
|
// For the RenderState family that answer is not a matter of taste and it is CHECKED
|
|
// rather than asserted: scripts/gen_pipe_dirty_surface.py reads RenderState.cpp and
|
|
// derives, per setter, which of NEW_RENDER_STATE / NEW_PIPELINE_STATE moves on every
|
|
// path - BumpVersions() moves both, a bare ++m_version moves only NEW_RENDER_STATE, and
|
|
// a setter with both kinds of path therefore always-fires only NEW_RENDER_STATE - and
|
|
// --check fails when a row disagrees, in either direction. That check exists because
|
|
// this file got exactly two rows wrong: SetCapability, whose ClipDistance0..7 arms move
|
|
// only m_version, and SetStencilFunc, whose pipeline bump is conditional on Func moving.
|
|
// Both named NEW_PIPELINE_STATE, which does not fire for glEnable(GL_CLIP_DISTANCE0) or
|
|
// for a reference-only glStencilFunc.
|
|
//
|
|
// NEW_* a MGPipeDirty bit (MG_Impl/Pipe/Tracker.h). The tracker's shutter for
|
|
// that bit moves when this mutator runs, so the next verb publishes it.
|
|
// kImmediate the mutating function also reaches the backend in the same body, so the
|
|
// mutation is published inline and needs no shutter at all.
|
|
// kReverseChannel not state: a write INTO the frontend from the backend's side.
|
|
// kNoBackendRead no backend read point observes this state at all.
|
|
// kExplicitDestroy published by the delete_* / resource_destroy call the Track H slice
|
|
// emits when the object's last reference drops - an object's DEATH,
|
|
// which no generation shutters because there is no longer an object
|
|
// to carry one. Only for a kind that HAS an identity on the wire to
|
|
// destroy: the resources and CSOs of PipeCalls.def, which is what P2
|
|
// brief D13 scopes Espryt 0b's explicit destroy to.
|
|
// kUnpublishedDestroy
|
|
// the same event for a kind NOTHING publishes: a program, a program
|
|
// pipeline and a shader have no per-object handle on the wire at all
|
|
// in P2 - resource_destroy and the delete_* family name resources and
|
|
// CSOs - so their DirectGLES twins are still reclaimed by the
|
|
// backend's own registry teardown and no frontend call says they
|
|
// died. Recorded as a hole rather than dressed up as a mechanism that
|
|
// exists; naming kExplicitDestroy here would be the same defect the
|
|
// RenderState derivation above exists to stop, one class down in
|
|
// stakes. (D13's prose says 'six kinds' while the Core.cpp ranges it
|
|
// cites also cover MarkProgram/MarkShaderForDeletion; the tree
|
|
// decides, and the tree has no wire object for those three.)
|
|
// kPulledEveryVerb no shutter exists at all - no MGPipeDirty bit moves on any path through
|
|
// this mutator - and none is needed yet: the PipeInputs field it writes is
|
|
// in its verb class's may-read mask, so the residual fill copies it at
|
|
// EVERY verb of that class. A shutter here is a P3/P4 optimisation, not a
|
|
// correctness gap.
|
|
// kPulledPartialShutter
|
|
// the same pull, but a bit DOES move - on some of the paths that mutate,
|
|
// not all of them - so this row must never be read as "no shutter exists".
|
|
// The bits that move are named after the '|', which is the one place this
|
|
// file joins a prose answer with a bit, and the reason is exactly that a
|
|
// P3a shutter builder has to be able to tell "no bit covers this" from "a
|
|
// bit covers half of it". The named bits are checked the same way every
|
|
// other bit answer is - a dead one is a red gate - but they are NOT a
|
|
// licence to narrow: what holds on every mutating path is the pull.
|
|
// Which rows need this answer is a human judgement and stays one: the
|
|
// derivation's "it does move that shutter" direction over-approximates
|
|
// (a call name resolves to every body of that name, a write inside an
|
|
// `if` counts), so it can refute a named bit but cannot find the rows
|
|
// that should have named one.
|
|
//
|
|
// KNOWN BLIND SPOTS OF THE SCANNER, recorded here rather than left implicit
|
|
// (gen_pipe_dirty_surface.py's own notes plus its scan root):
|
|
// 1. it matches braced function bodies textually, so a mutator inside a LAMBDA is
|
|
// attributed to the enclosing function;
|
|
// 2. a mutation published through a HELPER the entry point calls reads as deferred here;
|
|
// 3. the scan root is MG_Impl/GLImpl only, so the four MGP_NOTE_MUTATION sites in
|
|
// MG_State/GLState/TextureState/TextureState.h are outside it entirely.
|
|
// The gate is therefore a COMPLETENESS gate over what the scanner does see. The semantic
|
|
// proof stays the MOBILEGL_PIPE_VERIFY lane, which is blind to none of the three.
|
|
//
|
|
// clang-format off
|
|
|
|
// X(Mutator, Answer)
|
|
#define MGP_DIRTY_SURFACE_LIST(X) \
|
|
/* ---- the reverse channel: 836 of the 926 calls, 90% of the surface ---- */ \
|
|
X(RecordError, kReverseChannel) \
|
|
/* ---- immediate publish points: the same body reaches the backend ---- */ \
|
|
X(SetActiveTextureUnit, kImmediate) \
|
|
X(BeginTransformFeedback, kImmediate) \
|
|
X(EndTransformFeedback, kImmediate) \
|
|
X(SetTransformFeedbackPaused, kImmediate) \
|
|
X(MarkTransformFeedbackObjectForDeletion, kImmediate) \
|
|
/* ---- the render state. Derived from RenderState.cpp and gated by --check: */ \
|
|
/* a setter that calls BumpVersions() on every path publishes BOTH counters; */ \
|
|
/* one that also has a bare ++m_version path publishes only NEW_RENDER_STATE. */ \
|
|
X(SetBlendEquation, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetBlendEquationIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetBlendFunc, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetBlendFuncIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
/* SetCapability's ClipDistance0..7 arms write ClipDistanceEnabledMask (dynamic */ \
|
|
/* chunk D7) and deliberately do NOT BumpVersions, so NEW_PIPELINE_STATE does */ \
|
|
/* not fire at all for glEnable(GL_CLIP_DISTANCE0): set_dynamic_state publishes */ \
|
|
/* it, and NEW_RENDER_STATE is the only answer that holds on every arm. */ \
|
|
X(SetCapability, NEW_RENDER_STATE) \
|
|
X(SetCapabilityIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetColorMask, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetColorMaskIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetCullFaceMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetDepthFunc, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetDepthMask, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetFrontFaceMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetLogicOp, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetMinSampleShadingValue, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetPolygonMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetProvokingVertexMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetSampleCoverage, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetSampleMaskValue, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
/* SetStencilFunc writes Func (pipeline chunk P2/P3) AND Ref/ValueMask (dynamic */ \
|
|
/* D3/D4), and ++m_pipelineStateVersion is CONDITIONAL on Func moving - which is */ \
|
|
/* what keeps a glStencilFunc that moves only the reference from evicting a */ \
|
|
/* cached pipeline, and is why only NEW_RENDER_STATE fires on every call. */ \
|
|
/* SetStencilOp is wholly pipeline, SetStencilMask wholly dynamic. */ \
|
|
X(SetStencilFunc, NEW_RENDER_STATE) \
|
|
X(SetStencilOp, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetStencilMask, NEW_RENDER_STATE) \
|
|
X(SetBlendColor, NEW_RENDER_STATE) \
|
|
X(SetClampReadColor, NEW_RENDER_STATE) \
|
|
X(SetClearColor, NEW_RENDER_STATE) \
|
|
X(SetClearDepth, NEW_RENDER_STATE) \
|
|
X(SetClearStencil, NEW_RENDER_STATE) \
|
|
X(SetClipControl, NEW_RENDER_STATE) \
|
|
X(SetDepthRange, NEW_RENDER_STATE) \
|
|
X(SetDepthRangeIndexed, NEW_RENDER_STATE) \
|
|
X(SetHint, NEW_RENDER_STATE) \
|
|
X(SetLineWidth, NEW_RENDER_STATE) \
|
|
X(SetPointFadeThresholdSize, NEW_RENDER_STATE) \
|
|
X(SetPointSize, NEW_RENDER_STATE) \
|
|
X(SetPointSpriteCoordOrigin, NEW_RENDER_STATE) \
|
|
X(SetPolygonOffset, NEW_RENDER_STATE) \
|
|
X(SetPolygonOffsetClamped, NEW_RENDER_STATE) \
|
|
X(SetPrimitiveRestartIndex, NEW_RENDER_STATE) \
|
|
X(SetScissorBox, NEW_RENDER_STATE) \
|
|
X(SetScissorBoxIndexed, NEW_RENDER_STATE) \
|
|
X(SetViewport, NEW_RENDER_STATE) \
|
|
X(SetViewportIndexed, NEW_RENDER_STATE) \
|
|
/* ---- the other value-class bits ---- */ \
|
|
/* kPulledPartialShutter, NOT kPulledEveryVerb, and NOT a bare NEW_PIXEL_PACK: */ \
|
|
/* RenderState::SetPixelStoreParam writes BOTH halves - eight Pack arms and eight */ \
|
|
/* Unpack arms - while the tracker's bit 2 is a byte compare of the PACK half alone */ \
|
|
/* (Tracker.h), because set_pixel_pack_state deliberately has no unpack counterpart */ \
|
|
/* (ARCHITECTURE.md 4.6). So glPixelStorei(GL_PACK_ALIGNMENT, 8) DOES move bit 2 and */ \
|
|
/* glPixelStorei(GL_UNPACK_ALIGNMENT, 8) moves nothing at all, and a shutter narrowed */ \
|
|
/* to bit 2 would under-fire for eight of the sixteen arms. What is true on every path */ \
|
|
/* is the pull: GetPixelStoreParameters is one of the two Coverage.def rows an emitted */ \
|
|
/* call does not supply completely (PipeFill.cpp), so the residual fill copies both */ \
|
|
/* halves at every verb of the class. The bit is named anyway because P2 already EMITS */ \
|
|
/* set_pixel_pack_state off it: a row that said "no shutter exists" about the only */ \
|
|
/* mutator behind a shipped call would be a false answer to the one question D16 hands */ \
|
|
/* P3a. Splitting this setter into a pack half and an unpack half is what would let the */ \
|
|
/* pack half answer NEW_PIXEL_PACK outright; that is P3's move, not P2's. */ \
|
|
X(SetPixelStoreParam, kPulledPartialShutter|NEW_PIXEL_PACK) \
|
|
X(SetPatchDefaultInnerLevel, NEW_PATCH_STATE|NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetPatchDefaultOuterLevel, NEW_PATCH_STATE|NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
/* Also an immediate publish point, but it has a real bit and the bit is */ \
|
|
/* the more useful answer: set_patch_state carries it whatever the caller */ \
|
|
/* does next. */ \
|
|
X(SetPatchVertices, NEW_PATCH_STATE|NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
|
X(SetCurrentVertexAttributeFloat, NEW_VERTEX_ATTRIB_DEFAULTS) \
|
|
X(SetCurrentVertexAttributeInt, NEW_VERTEX_ATTRIB_DEFAULTS) \
|
|
X(SetCurrentVertexAttributeUint, NEW_VERTEX_ATTRIB_DEFAULTS) \
|
|
/* ---- object class ---- */ \
|
|
X(BumpTextureBindGeneration, NEW_SAMPLER_VIEWS) \
|
|
/* NOT NEW_SO_TARGETS, and this one was false on EVERY path: GLContext::SetNamed */ \
|
|
/* TransformFeedbackBinding either binds a BufferState binding point (index == the */ \
|
|
/* bound XFB object) or writes a saved-bindings entry, and NEW_SO_TARGETS mixes the */ \
|
|
/* buffer-CONTENT aggregate with the transform-feedback generation - the first moves */ \
|
|
/* only at BufferObject.cpp's content sites, the second only in BeginTransformFeedback. */ \
|
|
/* A binding moves neither. It reaches the backend the same way every other buffer */ \
|
|
/* binding point does, through GetBufferBindingPoint in the verb class's may-read mask, */ \
|
|
/* so the honest answer is the pull. Narrowing it is P3b's, when it takes the subsystem */ \
|
|
/* over and the binding points get a generation of their own. */ \
|
|
X(SetNamedTransformFeedbackBinding, kPulledEveryVerb) \
|
|
/* ---- an object's death: no generation, because there is no longer an object */ \
|
|
/* to carry one. Espryt 0b's delete_* / resource_destroy publishes the kinds */ \
|
|
/* that have a handle on the wire; programs, program pipelines and shaders have */ \
|
|
/* none in P2, so nothing publishes theirs - kUnpublishedDestroy, a known hole. */ \
|
|
X(MarkBufferObjectForDeletion, kExplicitDestroy) \
|
|
X(MarkFramebufferObjectForDeletion, kExplicitDestroy) \
|
|
X(MarkProgramForDeletion, kUnpublishedDestroy) \
|
|
X(MarkProgramPipelineForDeletion, kUnpublishedDestroy) \
|
|
X(MarkRenderbufferObjectForDeletion, kExplicitDestroy) \
|
|
X(MarkSamplerObjectForDeletion, kExplicitDestroy) \
|
|
X(MarkShaderForDeletion, kUnpublishedDestroy) \
|
|
X(MarkTextureObjectForDeletion, kExplicitDestroy) \
|
|
X(MarkVertexArrayForDeletion, kExplicitDestroy) \
|
|
/* ---- no backend read point observes these at all ---- */ \
|
|
/* GL_ANY_SAMPLES_PASSED conditional rendering is resolved wholly in the */ \
|
|
/* frontend: IsConditionalRenderActive / GetConditionalRenderQuery have no */ \
|
|
/* reader under MG_Backend and no Coverage.def row. */ \
|
|
X(BeginConditionalRender, kNoBackendRead) \
|
|
X(EndConditionalRender, kNoBackendRead) \
|
|
/* ---- pulled at every verb of the class, so the next verb publishes them */ \
|
|
/* unconditionally. The transform-feedback accounting counters reach the */ \
|
|
/* backend through GetTransformFeedbackCapturedVertices and friends, which */ \
|
|
/* are in the kDraw and kXfbSpan may-read masks. */ \
|
|
X(AddTransformFeedbackAccountedCaptureDraw, kPulledEveryVerb) \
|
|
X(AddTransformFeedbackCapturedVertices, kPulledEveryVerb) \
|
|
X(AddTransformFeedbackGeometryCaptureDraw, kPulledEveryVerb) \
|
|
X(AddTransformFeedbackInputPrimitives, kPulledEveryVerb) \
|
|
X(AddTransformFeedbackPausedPrimitives, kPulledEveryVerb) \
|
|
X(AddTransformFeedbackPrimitives, kPulledEveryVerb)
|
|
|
|
// X(Mutator, Bit) - the (row, bit) pairs above whose derivation is KNOWN to come out
|
|
// UNDECIDED, each with the reason --check prints for it. Every bit answer NOT listed here
|
|
// is marked derived: --check fails when the derivation cannot decide it, and fails again
|
|
// when a mark here names a pair the derivation now decides, so this list can neither hide a
|
|
// row nor outlive its reason. Empty today: every bit answer above is supported at field
|
|
// level. The ten mutators that reach a tainted body (--check prints the count) all carry a
|
|
// prose answer, which no derivation checks.
|
|
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X)
|
|
|
|
// clang-format on
|