From f15b0fdf4bab3e0cd4ee64f4385aa7d5b109741b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 6 Sep 2026 11:49:31 -0400 Subject: [PATCH] [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. --- MobileGL/MG_Pipe/DirtySurface.def | 43 +++- scripts/gen_pipe_dirty_surface.py | 372 ++++++++++++++++++++++++++++-- 2 files changed, 390 insertions(+), 25 deletions(-) diff --git a/MobileGL/MG_Pipe/DirtySurface.def b/MobileGL/MG_Pipe/DirtySurface.def index 84be476d..7964944a 100644 --- a/MobileGL/MG_Pipe/DirtySurface.def +++ b/MobileGL/MG_Pipe/DirtySurface.def @@ -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 */ \ diff --git a/scripts/gen_pipe_dirty_surface.py b/scripts/gen_pipe_dirty_surface.py index 18e6a90a..cbda9d23 100644 --- a/scripts/gen_pipe_dirty_surface.py +++ b/scripts/gen_pipe_dirty_surface.py @@ -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:, FIELD: or AGG:.""" + 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