mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user