[Fix] (Pipe): derive the dirty-surface map's object-class and value-class answers too, and correct the two rows that named a shutter their mutator never moves

- X(SetNamedTransformFeedbackBinding, NEW_SO_TARGETS) was false on EVERY path: that
  mutator binds a BufferState binding point or writes a saved-bindings entry, while the
  bit's shutter mixes the buffer-CONTENT aggregate with the transform-feedback
  generation, and a binding moves neither. It answers kPulledEveryVerb, which is what
  reaches the backend today (GetBufferBindingPoint, in the class's may-read mask).
- X(SetPixelStoreParam, NEW_PIXEL_PACK) was false on the eight Unpack arms: the setter
  writes both halves and the tracker's bit 2 is a byte compare of the PACK half alone,
  because set_pixel_pack_state deliberately has no unpack counterpart. It answers
  kPulledEveryVerb, the one publisher every arm has.
- --check no longer rubber-stamps the 28 rows the RenderState derivation cannot reach.
  It reads Tracker.h's Update() for what each bit's shutter READS, resolves those
  accessors through MG_State's getters to the members behind them, computes what every
  mutator transitively WRITES as a fixed point over MG_State/GLState and MG_Impl/Pipe
  (expanding MGP_NOTE_AGGREGATE through MGPipeNoteAggregate's own switch rather than
  assuming the hop), and fails a row naming a bit whose shutter its mutator moves on no
  path. One-directional by construction: the write analysis over-approximates, so it can
  prove absence and not presence, and absence is the under-firing direction.
- the enumerator spelling and the NEW_* spelling are paired BY POSITION out of Tracker.h,
  so the enum and kMGPipeDirtyNames drifting apart is itself a gate failure.
- two more self-test negative controls, one per family, both built from the defect that
  was really in the file; 7 controls now, all tripping.
- --check prints what it did NOT check: how many rows carry a prose answer, and every row
  the derivation declined, so "all mapped" cannot be read as "all verified".
- render_state_publishers() folds the bodies of one name with INTERSECTION, so two
  overloads - one BumpVersions, one bare ++m_version - can no longer derive as "both
  always fire" and bless an under-firing row.
- the header states what "every path" means: every path that MUTATES, so a redundant-write
  guard does not make its publisher conditional, while a publisher reached on only some
  mutating paths must not be named.
This commit is contained in:
2026-09-07 23:18:09 -04:00
parent 8f66c374aa
commit f15b0fdf4b
2 changed files with 390 additions and 25 deletions
+41 -2
View File
@@ -23,6 +23,27 @@
// 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.
//
// "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 to the members behind it,
// computes what each mutator transitively WRITES (through MGP_NOTE_AGGREGATE too, whose hop
// it reads out of MGPipeNoteAggregate's own switch), 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. The prose answers (kImmediate, kExplicitDestroy,
// kUnpublishedDestroy, kNoBackendRead, kPulledEveryVerb, kReverseChannel) are statements no
// derivation checks; --check prints how many rows carry one, and prints every row it had to
// decline, 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
@@ -139,7 +160,16 @@
X(SetViewport, NEW_RENDER_STATE) \
X(SetViewportIndexed, NEW_RENDER_STATE) \
/* ---- the other value-class bits ---- */ \
X(SetPixelStoreParam, NEW_PIXEL_PACK) \
/* NOT NEW_PIXEL_PACK, though half of it does move that bit: RenderState::SetPixelStore */ \
/* Param 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_UNPACK_ALIGNMENT, 8) and its seven siblings move NOTHING that */ \
/* bit reads, and naming it here would be an under-firing shutter 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. */ \
X(SetPixelStoreParam, kPulledEveryVerb) \
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 */ \
@@ -151,7 +181,16 @@
X(SetCurrentVertexAttributeUint, NEW_VERTEX_ATTRIB_DEFAULTS) \
/* ---- object class ---- */ \
X(BumpTextureBindGeneration, NEW_SAMPLER_VIEWS) \
X(SetNamedTransformFeedbackBinding, NEW_SO_TARGETS) \
/* 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 */ \
+349 -23
View File
@@ -194,36 +194,271 @@ def render_state_publishers():
if name.startswith("Set"):
bodies.setdefault(name, []).append(masked[start:end])
def direct(name):
def direct(body):
publishers = set()
for body in bodies[name]:
bump = BUMP_VERSIONS_RE.search(body) is not None
bare_version = BARE_VERSION_RE.search(body) is not None
bare_pipeline = BARE_PIPELINE_RE.search(body) is not None
if bump or bare_version:
publishers.add(RENDER_STATE_BIT)
if bump and not bare_version and not bare_pipeline:
publishers.add(PIPELINE_STATE_BIT)
bump = BUMP_VERSIONS_RE.search(body) is not None
bare_version = BARE_VERSION_RE.search(body) is not None
bare_pipeline = BARE_PIPELINE_RE.search(body) is not None
if bump or bare_version:
publishers.add(RENDER_STATE_BIT)
if bump and not bare_version and not bare_pipeline:
publishers.add(PIPELINE_STATE_BIT)
return publishers
def resolve_body(name, body, seen):
publishers = direct(body)
if publishers:
return publishers
# No bump of its own: whatever the setter it delegates to publishes.
for match in SETTER_CALL_RE.finditer(body):
callee = match.group(1)
if callee != name and callee in bodies:
publishers |= resolve(callee, seen)
return publishers
def resolve(name, seen):
if name in seen:
return set()
seen.add(name)
publishers = direct(name)
if publishers:
return publishers
# No bump of its own: whatever the setter it delegates to publishes.
for body in bodies[name]:
for match in SETTER_CALL_RE.finditer(body):
callee = match.group(1)
if callee != name and callee in bodies:
publishers |= resolve(callee, seen)
return publishers
# INTERSECTION, not union, across the bodies of one name. A union would let two
# overloads - one calling BumpVersions(), one bumping m_version alone - derive as
# "both counters always fire" and bless the exact under-firing row this derivation
# exists to catch. Every Set* name in RenderState.cpp has exactly one body today, so
# this changes no answer; it is the fold that stays right when one does not.
answers = [resolve_body(name, body, seen) for body in bodies[name]]
return set.intersection(*answers) if answers else set()
return {name: resolve(name, set()) for name in bodies}
# ---- the OTHER answers, derived from the shutter each bit is built out of ---------------
# The render-state derivation above covers 45 of the 73 rows. For the rest, "does this
# mutator move the shutter it names" is still a mechanical question, just one asked of a
# different pair of files: MG_Impl/Pipe/Tracker.h says which counters and which bytes each
# MGPipeDirty bit compares, and MG_State says who moves those. So:
#
# 1. read Tracker.h's Update() and, per bit, collect what its shutter READS -
# ctx.GetXxx() accessors and `render.Field` reads, with the walk's own locals expanded;
# 2. resolve each accessor, through MG_State's one-line getters, to the MEMBER it returns;
# 3. walk every function body under MG_State/GLState and MG_Impl/Pipe and compute, as a
# fixed point over call names, which members and struct fields each one transitively
# WRITES - including through MGP_NOTE_AGGREGATE, whose per-aggregate hop is read out of
# MGPipeNoteAggregate's own switch rather than assumed;
# 4. a row claiming bit B for mutator M is UNDER-FIRING when M writes nothing B reads.
#
# It is deliberately ONE-DIRECTIONAL. Step 3 is an over-approximation (a call name resolves
# to every body of that name, and a write inside an `if` counts), so "M does write something
# B reads" is not proof that it does so on every path and cannot be turned into a MISSING
# check without false reds. "M writes NOTHING B reads" needs no such assumption, and it is
# the under-firing direction ARCHITECTURE.md 13.2 calls the dangerous one - which is what
# was wrong in this file: X(SetNamedTransformFeedbackBinding, NEW_SO_TARGETS) named a
# shutter that moves on NO path through that mutator.
STATE_ROOTS = (os.path.join(REPO_ROOT, "MobileGL", "MG_State", "GLState"),
os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "Pipe"))
UPDATE_RE = re.compile(r"Uint32\s+Update\s*\(")
NOW_RE = re.compile(r"now\[Index\(MGPipeDirty::(\w+)\)\]\s*=\s*([^;]*);")
DIRTY_OR_RE = re.compile(r"dirty\s*\|=\s*MGPipeDirtyBit\(MGPipeDirty::(\w+)\)")
DIRTY_ANY_RE = re.compile(r"dirty\s*\|=")
LOCAL_RE = re.compile(r"(\w+)\s*=\s*([^;]*);")
CTX_READ_RE = re.compile(r"\bctx\.(\w+)\s*\(")
ARROW_READ_RE = re.compile(r"\b\w+\s*->\s*(\w+)\s*\(")
FIELD_READ_RE = re.compile(r"\brender\.(\w+)")
RETURN_RE = re.compile(r"\breturn\s+([^;]*);")
MEMBER_RE = re.compile(r"\b(m_\w+)\b")
MEMBER_CALL_RE = re.compile(r"\b(m_\w+)\s*\.\s*(\w+)\s*\(")
CALL_RE = re.compile(r"\b(\w+)\s*\(")
WORD_RE = re.compile(r"\b(\w+)\b")
AGGREGATE_RE = re.compile(r"MGP_NOTE_AGGREGATE\(\s*(\w+)\s*\)")
MEMBER_WRITE_RE = re.compile(r"\+\+\s*(m_\w+)|\b(m_\w+)\s*(?:\+\+|\+=|=(?!=))")
FIELD_WRITE_RE = re.compile(r"\.\s*(\w+)\s*(?:\[[^\]]*\])?\s*(?:\+\+|\+=|=(?!=))")
AGGREGATE_CASE_RE = re.compile(r"case\s+MGPipeAggregate::(\w+)\s*:\s*([^;]*);")
def state_bodies():
"""{function name: [body text]} over MG_State/GLState and MG_Impl/Pipe."""
bodies = {}
for root in STATE_ROOTS:
for directory, _, files in os.walk(root):
for name in sorted(files):
if not name.endswith((".h", ".cpp")):
continue
path = os.path.join(directory, name)
with open(path, "r", encoding="utf-8", errors="replace") as handle:
masked = mask_comments_and_strings(handle.read())
for fn, start, end in function_bodies(masked):
bodies.setdefault(fn, []).append(masked[start:end])
return bodies
def written_tokens(bodies):
"""{function name: set of tokens it transitively WRITES}, a fixed point over call names.
A token is MEM:<member>, FIELD:<struct field> or AGG:<MGPipeAggregate enumerator>."""
reach = {}
for name, bodylist in bodies.items():
tokens = set()
for body in bodylist:
tokens |= set("AGG:" + m.group(1) for m in AGGREGATE_RE.finditer(body))
for match in MEMBER_WRITE_RE.finditer(body):
tokens.add("MEM:" + (match.group(1) or match.group(2)))
tokens |= set("FIELD:" + m.group(1) for m in FIELD_WRITE_RE.finditer(body))
reach[name] = tokens
changed = True
rounds = 0
while changed and rounds < 16:
changed = False
rounds += 1
for name, bodylist in bodies.items():
before = len(reach[name])
for body in bodylist:
for match in CALL_RE.finditer(body):
callee = match.group(1)
if callee != name and callee in reach:
reach[name] |= reach[callee]
if len(reach[name]) != before:
changed = True
return reach
def aggregate_tokens(bodies, reach):
"""{MGPipeAggregate enumerator: the tokens its notice writes}, read out of
MGPipeNoteAggregate's own switch rather than assumed."""
out = {}
for body in bodies.get("MGPipeNoteAggregate", []):
for match in AGGREGATE_CASE_RE.finditer(body):
aggregate, statement = match.group(1), match.group(2)
tokens = set()
for call in CALL_RE.finditer(statement):
tokens |= reach.get(call.group(1), set())
out.setdefault(aggregate, set())
out[aggregate] |= tokens
return out
def expand_aggregates(tokens, aggregates):
"""AGG:X stands for whatever X's notice writes."""
out = set()
for token in tokens:
if token.startswith("AGG:"):
out |= aggregates.get(token[4:], set())
else:
out.add(token)
return out
def resolve_reader(name, bodies, seen=None):
"""The members an accessor returns, through however many one-line getters it delegates
to. An empty answer means the derivation could not follow it, which is reported as
UNVERIFIED rather than treated as "moves nothing"."""
seen = seen if seen is not None else set()
if name in seen or name not in bodies:
return set()
seen.add(name)
members = set()
for body in bodies[name]:
for match in RETURN_RE.finditer(body):
expression = match.group(1)
delegated = set()
for call in MEMBER_CALL_RE.finditer(expression):
delegated.add(call.group(1))
members |= resolve_reader(call.group(2), bodies, seen)
for member in MEMBER_RE.finditer(expression):
if member.group(1) not in delegated:
members.add("MEM:" + member.group(1))
return members
def shutter_readers():
"""{MGPipeDirty bit name: set of reader tokens} out of Tracker.h's Update()."""
with open(TRACKER_PATH, "r", encoding="utf-8", errors="replace") as handle:
masked = mask_comments_and_strings(handle.read())
body = None
for name, start, end in function_bodies(masked):
if name == "Update" and UPDATE_RE.search(masked[max(0, start - 200):start]):
body = masked[start:end]
break
if body is None:
return {}
assignments = {}
for match in LOCAL_RE.finditer(body):
assignments.setdefault(match.group(1), set()).add(match.group(2))
def readers_of(expression, depth=0):
found = set()
if depth > 4:
return found
found |= set("CTX:" + m.group(1) for m in CTX_READ_RE.finditer(expression))
found |= set("CTX:" + m.group(1) for m in ARROW_READ_RE.finditer(expression))
found |= set("FIELD:" + m.group(1) for m in FIELD_READ_RE.finditer(expression))
for word in WORD_RE.findall(expression):
if word in assignments and word not in ("now", "dirty"):
for assigned in assignments[word]:
if assigned != expression:
found |= readers_of(assigned, depth + 1)
return found
out = {}
for match in NOW_RE.finditer(body):
out.setdefault(match.group(1), set())
out[match.group(1)] |= readers_of(match.group(2))
# The two BitwiseEqual bits have no `now[]` entry: their shutter is the byte compare
# itself. The window is the text since the previous `dirty |=`, which is the block that
# builds the value being compared.
for match in DIRTY_OR_RE.finditer(body):
# Since the previous `dirty |=` of ANY form - the counter loop's included, or the
# window would start at the top of the walk and inherit every other bit's readers.
previous = 0
for boundary in DIRTY_ANY_RE.finditer(body, 0, match.start()):
previous = boundary.end()
window = body[previous:match.start()]
out.setdefault(match.group(1), set())
out[match.group(1)] |= readers_of(window)
return out
ENUM_BODY_RE = re.compile(r"enum\s+class\s+MGPipeDirty\s*:\s*Uint32\s*\{([^}]*)\}")
ENUMERATOR_RE = re.compile(r"^\s*(\w+)\s*(?:=\s*\d+\s*)?,", re.M)
def dirty_bit_aliases():
"""{MGPipeDirty enumerator: the NEW_* name a row spells}, paired BY POSITION with
kMGPipeDirtyNames. Tracker.h's Update() names the enumerators and DirtySurface.def names
the strings, so the two spellings have to be tied together somewhere; doing it by
position also checks that the enum and its name table have not drifted apart."""
with open(TRACKER_PATH, "r", encoding="utf-8", errors="replace") as handle:
text = handle.read()
match = ENUM_BODY_RE.search(mask_comments_and_strings(text))
if not match:
return {}
enumerators = [name for name in ENUMERATOR_RE.findall(match.group(1)) if name != "Count"]
names = DIRTY_NAME_RE.findall(text)
if len(enumerators) != len(names):
return {}
return dict(zip(enumerators, names))
def shutter_movers(readers, bodies, aliases):
"""{NEW_* bit name: (tokens whose write moves that bit's shutter, every reader resolved?)}"""
out = {}
for enumerator, tokens in readers.items():
bit = aliases.get(enumerator)
if bit is None:
continue
movers = set()
resolved = True
for token in tokens:
if token.startswith("FIELD:"):
movers.add(token)
continue
members = resolve_reader(token[4:], bodies)
if not members:
resolved = False
movers |= members
out[bit] = (movers, resolved and bool(movers))
return out
def dirty_bit_names():
"""The MGPipeDirty bit names, read out of Tracker.h's kMGPipeDirtyNames so a row cannot
name a bit that does not exist and a bit cannot be renamed out from under a row. Read
@@ -253,7 +488,41 @@ def answer_set(answer):
return {part.strip() for part in answer.split("|") if part.strip()}
def check_mapping(mapping, duplicates, scanned, bits, publishers=None):
def object_class_problems(mapping, bits, movers, moved):
"""The under-firing check for every answer the RenderState derivation cannot reach.
Returns (problems, verified, unverified) - `unverified` names the rows the derivation
had to decline, with the reason, so --check reports its own coverage instead of letting
a row it never looked at read as checked."""
problems = []
verified = 0
unverified = []
render_bits = {RENDER_STATE_BIT, PIPELINE_STATE_BIT}
for mutator in sorted(mapping):
claimed = (answer_set(mapping[mutator]) & bits) - render_bits
if not claimed:
continue
if mutator not in moved:
unverified.append("%s (no body found under MG_State/GLState or MG_Impl/Pipe to "
"derive from)" % mutator)
continue
for bit in sorted(claimed):
shutter, resolved = movers.get(bit, (set(), False))
if not resolved:
unverified.append("%s <- %s (Tracker.h's shutter for that bit reads something "
"this script cannot resolve to a member)" % (mutator, bit))
continue
if moved[mutator] & shutter:
verified += 1
continue
problems.append(
"UNDER-FIRING answer %s for %s - it writes nothing %s's shutter reads "
"(shutter: %s), so a mutation through it publishes nothing"
% (bit, mutator, bit, ", ".join(sorted(t.split(":", 1)[1] for t in shutter))))
return problems, verified, unverified
def check_mapping(mapping, duplicates, scanned, bits, publishers=None, movers=None, moved=None):
"""Every problem the gate fails on, as a list of human-readable lines. BOTH directions:
an unmapped mutator renders stale, and a row naming a mutator the scan no longer finds is
a stale row that would keep a real hole looking covered. `publishers` is
@@ -281,6 +550,10 @@ def check_mapping(mapping, duplicates, scanned, bits, publishers=None):
problems.append("BAD answer %s for %s - a non-bit answer stands alone"
% (mapping[mutator], mutator))
if movers is not None and moved is not None:
object_problems, _, _ = object_class_problems(mapping, bits, movers, moved)
problems += object_problems
if publishers is None:
return problems
@@ -342,7 +615,7 @@ def scan_all():
return sources, per_file, distinct_all
def self_test(scanned, bits, publishers):
def self_test(scanned, bits, publishers, movers, moved):
"""Canned negative controls. Each MUST trip; trips == 0 is an error, which is the shape
check_include_closure.py and gen_pipe.py --self-test already use."""
trips = 0
@@ -397,6 +670,34 @@ def self_test(scanned, bits, publishers):
else:
failures.append("negative control 5 (a dropped render-state publisher) did NOT trip")
# 6. THE CONTROL FOR THE OBJECT-CLASS HALF, and it is again the shape of a defect that
# was actually in this file: X(SetNamedTransformFeedbackBinding, NEW_SO_TARGETS)
# named a shutter (the buffer-content aggregate mixed with the transform-feedback
# generation) that GLContext::SetNamedTransformFeedbackBinding moves on no path - it
# binds a BufferState binding point or writes a saved-bindings entry, and neither is
# a buffer CONTENT write or a BeginTransformFeedback.
with_dead_shutter = dict(real)
with_dead_shutter["SetNamedTransformFeedbackBinding"] = "NEW_SO_TARGETS"
problems = check_mapping(with_dead_shutter, real_duplicates, scanned, bits, publishers,
movers, moved)
if any("UNDER-FIRING answer NEW_SO_TARGETS" in p for p in problems):
trips += 1
else:
failures.append("negative control 6 (an object-class answer whose shutter the mutator "
"never moves) did NOT trip")
# 7. the same check pointed at a value-class bit, so one passing control cannot stand in
# for the whole family: a vertex-attribute default does not move the pixel-store bytes.
with_wrong_bit = dict(real)
with_wrong_bit["SetCurrentVertexAttributeInt"] = "NEW_PIXEL_PACK"
problems = check_mapping(with_wrong_bit, real_duplicates, scanned, bits, publishers,
movers, moved)
if any("UNDER-FIRING answer NEW_PIXEL_PACK" in p for p in problems):
trips += 1
else:
failures.append("negative control 7 (a value-class answer whose shutter the mutator "
"never moves) did NOT trip")
for failure in failures:
print("dirty-surface self-test: %s" % failure)
if trips == 0:
@@ -433,14 +734,28 @@ def main():
publishers = render_state_publishers()
if not publishers:
sys.exit("could not derive any RenderState setter out of %s" % RENDER_STATE_PATH)
bodies = state_bodies()
reach = written_tokens(bodies)
aggregates = aggregate_tokens(bodies, reach)
moved = {name: expand_aggregates(tokens, aggregates) for name, tokens in reach.items()}
readers = shutter_readers()
if not readers:
sys.exit("could not read the dirty shutters out of %s - has MGPipeTracker::Update been "
"renamed?" % TRACKER_PATH)
aliases = dirty_bit_aliases()
if not aliases:
sys.exit("could not pair MGPipeDirty's enumerators with kMGPipeDirtyNames in %s - the "
"enum and its name table have drifted apart" % TRACKER_PATH)
movers = shutter_movers(readers, bodies, aliases)
if args.self_test:
return self_test(distinct_all, bits, publishers)
return self_test(distinct_all, bits, publishers, movers, moved)
mapping, duplicates = load_mapping()
if args.check:
problems = check_mapping(mapping, duplicates, distinct_all, bits, publishers)
problems = check_mapping(mapping, duplicates, distinct_all, bits, publishers, movers,
moved)
for problem in problems:
print("dirty-surface: %s" % problem)
if problems:
@@ -449,8 +764,19 @@ def main():
"RenderState.cpp actually publishes" % len(problems))
return 1
derived = sum(1 for m in mapping if m in publishers)
_, verified, unverified = object_class_problems(mapping, bits, movers, moved)
prose = sorted(m for m in mapping if not (answer_set(mapping[m]) & bits))
print("dirty-surface: %d mutators, all mapped, no stale rows; %d render-state answers "
"derived from RenderState.cpp and matching" % (len(mapping), derived))
# What the gate did NOT check is part of its output, or "all mapped" reads as "all
# verified" - which it is not, and was not for two rows through a whole review.
print("dirty-surface: %d other bit answers derived from their shutter in Tracker.h "
"(under-firing only); %d declined; %d rows carry a prose answer (%s) that no "
"derivation checks"
% (verified, len(unverified), len(prose),
", ".join(sorted(set(a for m in prose for a in answer_set(mapping[m]))))))
for row in unverified:
print("dirty-surface: not derived: %s" % row)
return 0
total_functions = 0