mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Feat] (MG_Pipe, scripts): generate the PipeInputs field-ownership table - 63 fields plus the seven sticky forwards in exactly one of four classes, RECORD-SUPPLIED derived from the emitted list rather than asserted, and a field in none of them stops the generator
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
#!/usr/bin/env python3
|
||||
# MobileGL - scripts/gen_pipe_field_ownership.py
|
||||
# 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
|
||||
"""TABLE 2 - where every PipeInputs field's value comes from once the backend is a server.
|
||||
|
||||
CONTRACT-P5.md section 3 / BRIEF-P5 R-7. Reads
|
||||
|
||||
MobileGL/MG_Pipe/Coverage.def the 63 accessors, the 7 sticky, the 40 emitted
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp EmittedCallSuppliesTheWholeField's refusals
|
||||
MobileGL/MG_Pipe/FieldOwnership.def the hand-maintained half
|
||||
|
||||
and writes MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc. The output is COMMITTED; CI
|
||||
regenerates it and fails on a diff, exactly as gen_pipe.py's seven outputs do.
|
||||
|
||||
ONE CLASS PER ROW, AND THE BUILD FAILS OTHERWISE. 63 field rows + the 7 sticky forwards =
|
||||
70, each in exactly one of RECORD_SUPPLIED / APPLIER_DERIVED / BARRIER_PULLED / FATAL. A
|
||||
field in none of them stops this script, so the committed header can never contain an
|
||||
unclassified row and --check is what catches a stale one. RECORD_SUPPLIED is DERIVED rather
|
||||
than listed - it is Coverage.def's emitted list minus PipeFill.cpp's refusals - so the
|
||||
mapping file cannot drift away from the emitters it describes without a red gate.
|
||||
|
||||
python3 scripts/gen_pipe_field_ownership.py # write it, print the summary
|
||||
python3 scripts/gen_pipe_field_ownership.py --check # THE GATE: rc 1 on any hole
|
||||
python3 scripts/gen_pipe_field_ownership.py --self-test # the gate's own negative controls
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PIPE_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe")
|
||||
GENERATED_DIR = os.path.join(PIPE_DIR, "generated")
|
||||
COVERAGE_DEF = os.path.join(PIPE_DIR, "Coverage.def")
|
||||
OWNERSHIP_DEF = os.path.join(PIPE_DIR, "FieldOwnership.def")
|
||||
PIPE_FILL = os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "Pipe", "PipeFill.cpp")
|
||||
PIPE_CALLS = os.path.join(PIPE_DIR, "PipeCalls.def")
|
||||
FILL_POINTS = os.path.join(PIPE_DIR, "FillPoints.def")
|
||||
OUT_NAME = "PipeFieldOwnership.inc"
|
||||
|
||||
CLASSES = ("RECORD_SUPPLIED", "APPLIER_DERIVED", "BARRIER_PULLED", "FATAL")
|
||||
ENUMERATOR = {
|
||||
"RECORD_SUPPLIED": "kRecordSupplied",
|
||||
"APPLIER_DERIVED": "kApplierDerived",
|
||||
"BARRIER_PULLED": "kBarrierPulled",
|
||||
"FATAL": "kFatal",
|
||||
}
|
||||
|
||||
BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
|
||||
// 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
|
||||
|
||||
// TABLE 2: PipeInputs field ownership (CONTRACT-P5.md section 3, R-7).
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe_field_ownership.py from Coverage.def, FieldOwnership.def and
|
||||
// MG_Impl/Pipe/PipeFill.cpp - DO NOT EDIT. Regenerate with
|
||||
// `python3 scripts/gen_pipe_field_ownership.py`; CI runs it and diffs the result.
|
||||
//
|
||||
// Included from MG_Backend/MGPipe/PipeInputs.h inside namespace MobileGL::MG_Pipe, which is
|
||||
// the one header that both the poison check and the server's verb stamp already see. It is
|
||||
// NOT included from MG_Pipe/MGPipe.h with the other seven generated files, on purpose: that
|
||||
// header is in the PULL build's include closure and G1 admits no symbol motion there.
|
||||
"""
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def mask_comments(text):
|
||||
"""Blank comment bodies, keeping every offset and newline. String literals are LEFT
|
||||
ALONE - unlike gen_pipe_dirty_surface.py's masker - because this file's rows carry their
|
||||
retiring phase and their reason as quoted arguments."""
|
||||
out = list(text)
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
if text[i] == "/" and i + 1 < n and text[i + 1] == "/":
|
||||
while i < n and text[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
elif text[i] == "/" and i + 1 < n and text[i + 1] == "*":
|
||||
out[i] = out[i + 1] = " "
|
||||
i += 2
|
||||
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
|
||||
if text[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
if i < n:
|
||||
out[i] = out[i + 1] = " "
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def macro_block(text, name):
|
||||
"""The body of a `#define <name>(X) ...` continued-line macro, comments blanked."""
|
||||
masked = mask_comments(text)
|
||||
start = masked.find("#define %s(X)" % name)
|
||||
if start < 0:
|
||||
sys.exit("gen_pipe_field_ownership: %s is not in the file" % name)
|
||||
end = start
|
||||
while True:
|
||||
line_end = masked.find("\n", end)
|
||||
if line_end < 0:
|
||||
body = masked[start:]
|
||||
break
|
||||
if not masked[end:line_end].rstrip().endswith("\\"):
|
||||
body = masked[start:line_end]
|
||||
break
|
||||
end = line_end + 1
|
||||
# The rows wrap, so the continuation backslashes have to go before a row regex can see a
|
||||
# row as one thing. Offsets do not matter here; only the token sequence does.
|
||||
return body.replace("\\\n", "\n")
|
||||
|
||||
|
||||
def parse_coverage(text=None):
|
||||
"""The ordered 63 accessors, the 7 sticky and the 40 emitted, out of Coverage.def."""
|
||||
text = read(COVERAGE_DEF) if text is None else text
|
||||
accessors = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*\)",
|
||||
macro_block(text, "MGP_COVERAGE_ACCESSOR_LIST"))
|
||||
sticky = re.findall(r"X\(\s*(\w+)\s*,", macro_block(text, "MGP_COVERAGE_STICKY_LIST"))
|
||||
emitted = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*\)",
|
||||
macro_block(text, "MGP_COVERAGE_EMITTED_LIST"))
|
||||
if not accessors:
|
||||
sys.exit("gen_pipe_field_ownership: Coverage.def's accessor list did not parse")
|
||||
seen = set()
|
||||
for name in accessors:
|
||||
if name in seen:
|
||||
sys.exit("gen_pipe_field_ownership: %s appears twice in the accessor list" % name)
|
||||
seen.add(name)
|
||||
for name in sticky + emitted:
|
||||
if name not in seen:
|
||||
sys.exit("gen_pipe_field_ownership: %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST"
|
||||
% name)
|
||||
return accessors, sticky, emitted
|
||||
|
||||
|
||||
def parse_supplies_whole_field(text=None):
|
||||
"""The fields EmittedCallSuppliesTheWholeField REFUSES, read out of PipeFill.cpp rather
|
||||
than restated here - the derivation that keeps RECORD_SUPPLIED honest is only as good as
|
||||
its source, so the source is the function itself."""
|
||||
text = read(PIPE_FILL) if text is None else text
|
||||
masked = mask_comments(text)
|
||||
start = masked.find("Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field)")
|
||||
if start < 0:
|
||||
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField is not in %s"
|
||||
% os.path.basename(PIPE_FILL))
|
||||
false_at = masked.find("return false;", start)
|
||||
if false_at < 0:
|
||||
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField has no `return false` arm")
|
||||
refused = re.findall(r"case\s+MGPipeInputField::(\w+)\s*:", masked[start:false_at])
|
||||
if not refused:
|
||||
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField refuses nothing - "
|
||||
"either the function moved or the parse broke; a silently empty refusal set "
|
||||
"would make every emitted field RECORD_SUPPLIED")
|
||||
return refused
|
||||
|
||||
|
||||
ROW_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*\"([^\"]*)\"\s*,\s*\"([^\"]*)\"\s*\)")
|
||||
ARG_ROW_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\d+)\s*,\s*(\w+)\s*,\s*\"([^\"]*)\"\s*\)")
|
||||
PAIR_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\w+)\s*\)")
|
||||
|
||||
|
||||
def parse_ownership(text=None):
|
||||
text = read(OWNERSHIP_DEF) if text is None else text
|
||||
fields = ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_LIST"))
|
||||
forwards = ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_FORWARD_LIST"))
|
||||
args = ARG_ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_ARG_LIST"))
|
||||
verb_ops = PAIR_RE.findall(macro_block(text, "MGP_VERB_OP_LIST"))
|
||||
if not fields:
|
||||
sys.exit("gen_pipe_field_ownership: FieldOwnership.def's field list did not parse")
|
||||
return fields, forwards, args, verb_ops
|
||||
|
||||
|
||||
def parse_ops_and_verbs(calls_text=None, fill_points_text=None):
|
||||
"""The two name spaces the stamp map joins, read from the files that define them: the
|
||||
catalogue's calls (PipeCalls.def, which IS the MGPWireOp enum) and the verb set
|
||||
(FillPoints.def, which IS MG_Backend::GLFunctionsTable's member list)."""
|
||||
calls_text = read(PIPE_CALLS) if calls_text is None else calls_text
|
||||
fill_points_text = read(FILL_POINTS) if fill_points_text is None else fill_points_text
|
||||
ops = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*,\s*\w+\s*,",
|
||||
macro_block(calls_text, "MGP_CALL_LIST"))
|
||||
verbs = re.findall(r"X\(\s*(\w+)\s*,\s*(k\w+)\s*\)",
|
||||
macro_block(fill_points_text, "MGP_FILL_VERB_LIST"))
|
||||
if not ops:
|
||||
sys.exit("gen_pipe_field_ownership: PipeCalls.def's call list did not parse")
|
||||
if not verbs:
|
||||
sys.exit("gen_pipe_field_ownership: FillPoints.def's verb list did not parse")
|
||||
return ops, [v for v, _ in verbs]
|
||||
|
||||
|
||||
def check_verb_ops(verb_ops, ops, verbs):
|
||||
"""The stamp map, against both name spaces. A row whose op or verb does not exist would
|
||||
otherwise become a switch arm that fails to compile minutes later - or, worse, a silently
|
||||
absent stamp point."""
|
||||
if not verb_ops:
|
||||
sys.exit("gen_pipe_field_ownership: MGP_VERB_OP_LIST is empty - the server would have "
|
||||
"no verb boundary to stamp at and every field would read @<none>")
|
||||
seen = set()
|
||||
for op, verb in verb_ops:
|
||||
if op not in ops:
|
||||
sys.exit("gen_pipe_field_ownership: the stamp map names op %s, which is not a call "
|
||||
"in PipeCalls.def" % op)
|
||||
if verb not in verbs:
|
||||
sys.exit("gen_pipe_field_ownership: the stamp map names verb %s, which is not a "
|
||||
"verb in FillPoints.def" % verb)
|
||||
if op in seen:
|
||||
sys.exit("gen_pipe_field_ownership: op %s has two stamp rows" % op)
|
||||
seen.add(op)
|
||||
return verb_ops
|
||||
|
||||
|
||||
def build(accessors, sticky, emitted, refused, rows, forwards, args):
|
||||
"""The join, and every gate it is allowed to fail on. Returns
|
||||
(ownership, phase, why, forward rows, argument rows, counts)."""
|
||||
supplied = [f for f in emitted if f not in set(refused)]
|
||||
ownership = {}
|
||||
phase = {}
|
||||
why = {}
|
||||
for field in supplied:
|
||||
ownership[field] = "RECORD_SUPPLIED"
|
||||
phase[field] = "-"
|
||||
why[field] = "derived: Coverage.def's emitted list, and PipeFill.cpp does not refuse it"
|
||||
|
||||
for field, cls, retires, reason in rows:
|
||||
if field not in accessors:
|
||||
sys.exit("gen_pipe_field_ownership: FieldOwnership.def names %s, which is not a "
|
||||
"PipeInputs field" % field)
|
||||
if cls not in CLASSES:
|
||||
sys.exit("gen_pipe_field_ownership: %s is in class %s, which is not one of %s"
|
||||
% (field, cls, "/".join(CLASSES)))
|
||||
if cls == "RECORD_SUPPLIED":
|
||||
sys.exit("gen_pipe_field_ownership: %s claims RECORD_SUPPLIED, which is DERIVED and "
|
||||
"may not be asserted by hand" % field)
|
||||
if field in ownership:
|
||||
sys.exit("gen_pipe_field_ownership: %s is in TWO classes - the derivation says "
|
||||
"RECORD_SUPPLIED and FieldOwnership.def says %s" % (field, cls))
|
||||
if cls == "BARRIER_PULLED" and retires.strip() in ("", "-"):
|
||||
sys.exit("gen_pipe_field_ownership: %s is BARRIER_PULLED and names no retiring "
|
||||
"phase - P5's debt is only sized if every row says who pays it" % field)
|
||||
if cls != "BARRIER_PULLED" and retires.strip() != "-":
|
||||
sys.exit("gen_pipe_field_ownership: %s is %s and names a retiring phase; only "
|
||||
"BARRIER_PULLED rows have one" % (field, cls))
|
||||
ownership[field] = cls
|
||||
phase[field] = retires
|
||||
why[field] = reason
|
||||
|
||||
missing = [f for f in accessors if f not in ownership]
|
||||
if missing:
|
||||
sys.exit("gen_pipe_field_ownership: %d field(s) in NO class, which is a build failure "
|
||||
"(R-7.1): %s" % (len(missing), ", ".join(missing)))
|
||||
|
||||
# The seven sticky forwards' own rows. They are the seven that hand the server a frontend
|
||||
# object or write into the frontend, so the gate is structurally blind on them without a
|
||||
# row of their own - and the row has to agree with the field row, or the two halves of the
|
||||
# same accessor would be documented as different things.
|
||||
forward_map = {}
|
||||
for field, cls, retires, mechanism in forwards:
|
||||
if field not in sticky:
|
||||
sys.exit("gen_pipe_field_ownership: %s has a FORWARD row but is not sticky in "
|
||||
"Coverage.def" % field)
|
||||
if field in forward_map:
|
||||
sys.exit("gen_pipe_field_ownership: %s has two FORWARD rows" % field)
|
||||
if cls not in CLASSES:
|
||||
sys.exit("gen_pipe_field_ownership: forward %s is in class %s, which is not one of "
|
||||
"%s" % (field, cls, "/".join(CLASSES)))
|
||||
if ownership[field] != cls:
|
||||
sys.exit("gen_pipe_field_ownership: %s's field row says %s and its forward row says "
|
||||
"%s; a read of a sticky field IS a call of its forward" % (field, ownership[field], cls))
|
||||
forward_map[field] = (cls, retires, mechanism)
|
||||
absent = [f for f in sticky if f not in forward_map]
|
||||
if absent:
|
||||
sys.exit("gen_pipe_field_ownership: sticky forward(s) with no row: %s" % ", ".join(absent))
|
||||
|
||||
arg_rows = []
|
||||
for field, arg0, cls, reason in args:
|
||||
if field not in accessors:
|
||||
sys.exit("gen_pipe_field_ownership: argument exception names %s, which is not a "
|
||||
"PipeInputs field" % field)
|
||||
if cls not in CLASSES:
|
||||
sys.exit("gen_pipe_field_ownership: argument exception %s(%s) is in class %s"
|
||||
% (field, arg0, cls))
|
||||
if cls == ownership[field]:
|
||||
sys.exit("gen_pipe_field_ownership: argument exception %s(%s) repeats the field's "
|
||||
"own class (%s) and narrows nothing" % (field, arg0, cls))
|
||||
arg_rows.append((field, int(arg0), cls, reason))
|
||||
|
||||
counts = {cls: sum(1 for f in accessors if ownership[f] == cls) for cls in CLASSES}
|
||||
return ownership, phase, why, forward_map, arg_rows, counts
|
||||
|
||||
|
||||
def emit(accessors, sticky, ownership, phase, why, forward_map, arg_rows, counts, verb_ops):
|
||||
out = [BANNER.format(name=OUT_NAME)]
|
||||
add = out.append
|
||||
add("""
|
||||
// The four classes. kUnclassified exists so the static_assert below has something to refuse;
|
||||
// the generator never emits it, which is what makes "a field in no class fails the build"
|
||||
// true at two independent points rather than one.
|
||||
enum class MGPipeFieldOwnership : Uint8 {
|
||||
kUnclassified = 0,
|
||||
kRecordSupplied, // a pushed record supplies the WHOLE field
|
||||
kApplierDerived, // the applier writes it out of records it already applies
|
||||
kBarrierPulled, // P5's debt: read out of the client's residual fill under the verb barrier
|
||||
kFatal, // no carrier and the reduced path never reads it
|
||||
};
|
||||
|
||||
inline constexpr const char* kMGPipeFieldOwnershipNames[] = {
|
||||
"UNCLASSIFIED", "RECORD-SUPPLIED", "APPLIER-DERIVED", "BARRIER-PULLED", "FATAL",
|
||||
};
|
||||
""")
|
||||
add("inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnership[kMGPipeInputFieldCount] = {")
|
||||
for field in accessors:
|
||||
add(" MGPipeFieldOwnership::%s, // %s" % (ENUMERATOR[ownership[field]], field))
|
||||
add("};\n")
|
||||
|
||||
add("// The ROADMAP phase whose row retires the pull. \"-\" for every class but BARRIER-PULLED.")
|
||||
add("inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] = {")
|
||||
for field in accessors:
|
||||
add(" \"%s\", // %s" % (phase[field], field))
|
||||
add("};\n")
|
||||
|
||||
add("// The seven sticky forwards, which are among the 63 above and need a row of their own:")
|
||||
add("// they are the ones that hand the server a raw frontend object or write into the")
|
||||
add("// frontend, so the exit gate is structurally blind on them without one.")
|
||||
add("inline constexpr SizeT kMGPipeFieldOwnershipForwardCount = %d;" % len(sticky))
|
||||
add("static_assert(kMGPipeFieldOwnershipForwardCount == kMGPipeInputStickyFieldCount,")
|
||||
add(" \"the forward rows and Coverage.def's sticky set are the same seven\");")
|
||||
add("inline constexpr MGPipeInputField kMGPipeFieldOwnershipForwardField[kMGPipeFieldOwnershipForwardCount] = {")
|
||||
for field in sticky:
|
||||
add(" MGPipeInputField::%s," % field)
|
||||
add("};")
|
||||
add("inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnershipForward[kMGPipeFieldOwnershipForwardCount] = {")
|
||||
for field in sticky:
|
||||
add(" MGPipeFieldOwnership::%s, // %s" % (ENUMERATOR[forward_map[field][0]], field))
|
||||
add("};")
|
||||
add("inline constexpr const char* kMGPipeFieldOwnershipForwardMechanism[kMGPipeFieldOwnershipForwardCount] = {")
|
||||
for field in sticky:
|
||||
add(" \"%s\", // %s, retires in %s" % (forward_map[field][2], field, forward_map[field][1]))
|
||||
add("};\n")
|
||||
|
||||
add("// CONTRACT-P5.md section 3: \"70 rows, each in exactly one class\".")
|
||||
add("inline constexpr SizeT kMGPipeFieldOwnershipRowCount =")
|
||||
add(" kMGPipeInputFieldCount + kMGPipeFieldOwnershipForwardCount;")
|
||||
add("static_assert(kMGPipeFieldOwnershipRowCount == 70, \"table 2's row count moved\");\n")
|
||||
|
||||
add("""// An ARGUMENT-KEYED narrowing of one field. The field keeps its single row above; this
|
||||
// says that one argument value of it belongs to a different class. Coverage.def:62-69 already
|
||||
// rules the shape for GetBufferBindingSlot - "THE ROW STAYS ONE ROW ... the field is ONE array
|
||||
// that a second row of the same name could only duplicate" - and m_pixelStore[2] is the same
|
||||
// shape indexed by its own isUnpack argument.
|
||||
struct MGPipeFieldArgumentOwnership {
|
||||
MGPipeInputField Field;
|
||||
Uint32 Arg0;
|
||||
MGPipeFieldOwnership Class;
|
||||
};""")
|
||||
add("inline constexpr SizeT kMGPipeFieldArgumentOwnershipCount = %d;" % len(arg_rows))
|
||||
add("inline constexpr MGPipeFieldArgumentOwnership")
|
||||
add(" kMGPipeFieldArgumentOwnership[kMGPipeFieldArgumentOwnershipCount] = {")
|
||||
for field, arg0, cls, reason in arg_rows:
|
||||
add(" {MGPipeInputField::%s, %du, MGPipeFieldOwnership::%s}, // %s"
|
||||
% (field, arg0, ENUMERATOR[cls], reason))
|
||||
add("};\n")
|
||||
|
||||
add("""constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field) {
|
||||
return kMGPipeFieldOwnership[static_cast<SizeT>(field)];
|
||||
}
|
||||
|
||||
// The same answer, narrowed by the accessor's first argument. Every accessor that takes one
|
||||
// may call this; only the fields with a row above answer differently from the field's class.
|
||||
constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field, Uint32 arg0) {
|
||||
for (SizeT i = 0; i < kMGPipeFieldArgumentOwnershipCount; ++i) {
|
||||
if (kMGPipeFieldArgumentOwnership[i].Field == field &&
|
||||
kMGPipeFieldArgumentOwnership[i].Arg0 == arg0) {
|
||||
return kMGPipeFieldArgumentOwnership[i].Class;
|
||||
}
|
||||
}
|
||||
return MGPipeFieldOwnershipOf(field);
|
||||
}
|
||||
|
||||
constexpr const char* MGPipeFieldOwnershipName(MGPipeFieldOwnership ownership) {
|
||||
return kMGPipeFieldOwnershipNames[static_cast<SizeT>(ownership)];
|
||||
}
|
||||
|
||||
// THE BUILD FAILURE R-7.1 ASKS FOR. The generator refuses to emit an unclassified row, so
|
||||
// this can only fire on a hand-edited header - which is exactly the edit the DO NOT EDIT
|
||||
// banner cannot prevent on its own.
|
||||
constexpr Bool MGPipeEveryFieldIsClassified() {
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
if (kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kUnclassified) return false;
|
||||
}
|
||||
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
|
||||
if (kMGPipeFieldOwnershipForward[i] == MGPipeFieldOwnership::kUnclassified) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static_assert(MGPipeEveryFieldIsClassified(),
|
||||
"a PipeInputs field is in none of the four ownership classes (CONTRACT-P5 table 2, R-7.1)");
|
||||
""")
|
||||
add("""// WHERE THE SERVER STAMPS. The wire's op and the fill's verb are different name spaces
|
||||
// and do not line up by name (draw_vbo is DrawArrays, blit is BlitFramebuffer), so this is the
|
||||
// join. An op with no row is NOT a verb boundary and the applier must not stamp on it.
|
||||
// Present is deliberately absent: FillPoints.def:21 - "Present and SetSwapInterval go through
|
||||
// BackendObject virtuals and read no frontend state, so they are not verbs here".
|
||||
constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) {
|
||||
switch (op) {""")
|
||||
for op, verb in verb_ops:
|
||||
add(" case MGPWireOp::%s: return MGPipeVerb::%s;" % (op, verb))
|
||||
add(""" default:
|
||||
return MGPipeVerb::kVerbCount;
|
||||
}
|
||||
}
|
||||
""")
|
||||
add("inline constexpr SizeT kMGPipeVerbBoundaryOpCount = %d;" % len(verb_ops))
|
||||
add("")
|
||||
add("// The class sizes, as constants a test can pin without recounting the table.")
|
||||
for cls in CLASSES:
|
||||
add("inline constexpr SizeT kMGPipe%sFieldCount = %d;"
|
||||
% ("".join(p.capitalize() for p in cls.split("_")), counts[cls]))
|
||||
add("static_assert(%s == kMGPipeInputFieldCount, \"the four class sizes do not partition the field set\");"
|
||||
% " + ".join("kMGPipe%sFieldCount" % "".join(p.capitalize() for p in cls.split("_"))
|
||||
for cls in CLASSES))
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def write(path, text, check_only, changed):
|
||||
existing = read(path) if os.path.exists(path) else None
|
||||
if existing == text:
|
||||
return
|
||||
changed.append(os.path.basename(path))
|
||||
if not check_only:
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(text)
|
||||
|
||||
|
||||
def expect_trip(name, fn):
|
||||
"""A control that must exit. Returns 1 when it did, and says so when it did not - a
|
||||
silent pass here is the gate not checking anything."""
|
||||
try:
|
||||
fn()
|
||||
except SystemExit:
|
||||
return 1
|
||||
print("gen_pipe_field_ownership: self-test: control did NOT trip: %s" % name, file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def self_test():
|
||||
"""The negative controls (gen_pipe.py --self-test's shape): each gate must go red for its
|
||||
own reason, and zero trips is itself an error."""
|
||||
coverage = read(COVERAGE_DEF)
|
||||
ownership_text = read(OWNERSHIP_DEF)
|
||||
fill = read(PIPE_FILL)
|
||||
accessors, sticky, emitted = parse_coverage(coverage)
|
||||
refused = parse_supplies_whole_field(fill)
|
||||
|
||||
ops, verbs = parse_ops_and_verbs()
|
||||
|
||||
def run(own_text=None, cov=None, fill_text=None):
|
||||
acc, stk, emt = parse_coverage(cov if cov is not None else coverage)
|
||||
ref = parse_supplies_whole_field(fill_text if fill_text is not None else fill)
|
||||
rows, fwd, args, verb_ops = parse_ownership(own_text if own_text is not None else ownership_text)
|
||||
check_verb_ops(verb_ops, ops, verbs)
|
||||
return build(acc, stk, emt, ref, rows, fwd, args)
|
||||
|
||||
# The rows wrap over two lines with a trailing backslash, so every control below edits
|
||||
# them through a regex whose gaps tolerate that rather than through a literal that would
|
||||
# silently stop matching the day someone re-aligns the file.
|
||||
GAP = r"[\s\\]*"
|
||||
|
||||
def edit(pattern, replacement, what, text=None):
|
||||
text = ownership_text if text is None else text
|
||||
edited, count = re.subn(pattern, replacement, text, count=1)
|
||||
if count != 1:
|
||||
sys.exit("gen_pipe_field_ownership: self-test: could not %s - the control's own "
|
||||
"edit no longer matches the file it is supposed to break" % what)
|
||||
return edited
|
||||
|
||||
# 1. THE HEADLINE CONTROL (exit gate E4): take one field out of the table. It is in no
|
||||
# class, and that is a build failure rather than a silent default.
|
||||
dropped = edit(r"X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED," + GAP + r"\"[^\"]*\","
|
||||
+ GAP + r"\"[^\"]*\"\)", "", "remove GetActiveTextureUnit's row")
|
||||
controls = [("a field in NO class (GetActiveTextureUnit's row removed)",
|
||||
lambda: run(own_text=dropped))]
|
||||
|
||||
# 2. The other direction: a field the derivation already placed in RECORD_SUPPLIED, also
|
||||
# claimed by hand. E4's negative control is "move a field from supplied to FATAL".
|
||||
doubled = ownership_text.replace(
|
||||
"#define MGP_FIELD_OWNERSHIP_LIST(X)",
|
||||
"#define MGP_FIELD_OWNERSHIP_LIST(X) X(GetClearColor, FATAL, \"-\", \"moved by hand\") \\\n", 1)
|
||||
controls.append(("a RECORD_SUPPLIED field claimed by hand (supplied -> FATAL)",
|
||||
lambda: run(own_text=doubled)))
|
||||
|
||||
# 3. A row naming something that is not a field at all.
|
||||
typo = edit(r"X\(GetActiveTextureUnit,", "X(GetActiveTextureUnitt,", "misspell a field name")
|
||||
controls.append(("a row naming a non-field", lambda: run(own_text=typo)))
|
||||
|
||||
# 4. A BARRIER_PULLED row with no retiring phase: the debt is only sized if every row
|
||||
# says who pays it, which is the whole of R-7.2's "rsp IS the size of the debt".
|
||||
unphased = edit(r"(X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED,)" + GAP + r"\"[^\"]*\",",
|
||||
r"\1 \"-\",", "blank a BARRIER_PULLED row's retiring phase")
|
||||
controls.append(("a BARRIER_PULLED row with no retiring phase", lambda: run(own_text=unphased)))
|
||||
|
||||
# 5. A class that is not one of the four.
|
||||
bogus = edit(r"(X\(GetActiveTextureUnit,)" + GAP + r"BARRIER_PULLED,",
|
||||
r"\1 SOMEHOW_FINE,", "introduce a fifth class")
|
||||
controls.append(("a fifth class", lambda: run(own_text=bogus)))
|
||||
|
||||
# 6. A sticky forward that lost its own row - the seven most dangerous fields are exactly
|
||||
# the ones a table without forward rows is blind to.
|
||||
no_forward = edit(r"X\(RecordError," + GAP + r"BARRIER_PULLED," + GAP + r"\"P9\"," + GAP
|
||||
+ r"\"OnGlError[^\"]*\"\)", "", "remove RecordError's forward row")
|
||||
controls.append(("a sticky forward with no row", lambda: run(own_text=no_forward)))
|
||||
|
||||
# 7. A forward row that disagrees with its field row.
|
||||
disagree = edit(r"X\(GetTextureObject," + GAP + r"BARRIER_PULLED," + GAP + r"\"P7\"," + GAP
|
||||
+ r"\"a server-side texture handle table\"\)",
|
||||
"X(GetTextureObject, FATAL, \"-\", \"a server-side texture handle table\")",
|
||||
"contradict GetTextureObject's field row")
|
||||
controls.append(("a forward row that contradicts its field row", lambda: run(own_text=disagree)))
|
||||
|
||||
# 8. An argument exception that narrows nothing.
|
||||
same = edit(r"X\(GetPixelStoreParameters, 1, FATAL,",
|
||||
"X(GetPixelStoreParameters, 1, APPLIER_DERIVED,",
|
||||
"make the argument exception repeat the field's class")
|
||||
controls.append(("an argument exception that repeats the field's class",
|
||||
lambda: run(own_text=same)))
|
||||
|
||||
# 9. THE DERIVATION'S OWN SOURCE. If EmittedCallSuppliesTheWholeField's refusals stop
|
||||
# parsing, every emitted field silently becomes RECORD_SUPPLIED and eight rows of this
|
||||
# table quietly contradict themselves - so an empty refusal set has to stop the script
|
||||
# rather than produce a plausible table.
|
||||
blinded = fill.replace("Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field)",
|
||||
"Bool EmittedCallSuppliesTheWholeFieldXX(MGPipeInputField field)", 1)
|
||||
controls.append(("the derivation's source function renamed away",
|
||||
lambda: run(fill_text=blinded)))
|
||||
|
||||
# 10. THE STAMP MAP against both name spaces. A row naming an op that is not a call, or a
|
||||
# verb that is not a verb, would otherwise be an arm that fails to compile - or an
|
||||
# absent stamp point, which is silent.
|
||||
bad_op = edit(r"X\(Clear,\s*Clear\)", "X(Klear, Clear)", "misspell a stamp map op")
|
||||
controls.append(("a stamp row naming an op that does not exist", lambda: run(own_text=bad_op)))
|
||||
bad_verb = edit(r"X\(DrawVbo,\s*DrawArrays\)", "X(DrawVbo, DrawArrayz)",
|
||||
"misspell a stamp map verb")
|
||||
controls.append(("a stamp row naming a verb that does not exist", lambda: run(own_text=bad_verb)))
|
||||
|
||||
trips = 0
|
||||
for name, fn in controls:
|
||||
trips += expect_trip(name, fn)
|
||||
|
||||
# The positive control: the real tables pass, and they partition the real field set.
|
||||
_, _, _, _, _, counts = run()
|
||||
total = sum(counts.values())
|
||||
if total != len(accessors):
|
||||
sys.exit("gen_pipe_field_ownership: self-test: the positive control does not partition "
|
||||
"the field set (%d of %d)" % (total, len(accessors)))
|
||||
if len(sticky) != 7:
|
||||
sys.exit("gen_pipe_field_ownership: self-test: Coverage.def no longer has seven sticky fields")
|
||||
if len(refused) != 9:
|
||||
sys.exit("gen_pipe_field_ownership: self-test: EmittedCallSuppliesTheWholeField refuses %d "
|
||||
"fields, not the nine the contract's derivation is written against" % len(refused))
|
||||
if trips == 0:
|
||||
sys.exit("gen_pipe_field_ownership: self-test: no negative control tripped - the gates are "
|
||||
"not checking anything")
|
||||
if trips != len(controls):
|
||||
sys.exit("gen_pipe_field_ownership: self-test: %d of %d negative controls did not trip"
|
||||
% (len(controls) - trips, len(controls)))
|
||||
print("gen_pipe_field_ownership: self-test: %d negative-control trip(s), positive control OK "
|
||||
"(%d fields partitioned, 7 sticky forwards, 9 refusals)" % (trips, total))
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="do not write; exit 1 if regenerating would change anything")
|
||||
parser.add_argument("--self-test", action="store_true",
|
||||
help="run the negative controls (each gate must trip) and exit")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.self_test:
|
||||
return self_test()
|
||||
|
||||
accessors, sticky, emitted = parse_coverage()
|
||||
refused = parse_supplies_whole_field()
|
||||
rows, forwards, arg_rows, verb_ops = parse_ownership()
|
||||
ops, verbs = parse_ops_and_verbs()
|
||||
check_verb_ops(verb_ops, ops, verbs)
|
||||
ownership, phase, why, forward_map, arg_list, counts = build(
|
||||
accessors, sticky, emitted, refused, rows, forwards, arg_rows)
|
||||
|
||||
if not os.path.isdir(GENERATED_DIR):
|
||||
os.makedirs(GENERATED_DIR)
|
||||
changed = []
|
||||
write(os.path.join(GENERATED_DIR, OUT_NAME),
|
||||
emit(accessors, sticky, ownership, phase, why, forward_map, arg_list, counts, verb_ops),
|
||||
args.check, changed)
|
||||
|
||||
print("gen_pipe_field_ownership: %d fields + %d sticky forwards = %d rows; "
|
||||
"%d record-supplied (derived), %d applier-derived, %d barrier-pulled, %d fatal, "
|
||||
"%d argument exception(s), %d verb-boundary op(s)"
|
||||
% (len(accessors), len(sticky), len(accessors) + len(sticky),
|
||||
counts["RECORD_SUPPLIED"], counts["APPLIER_DERIVED"], counts["BARRIER_PULLED"],
|
||||
counts["FATAL"], len(arg_list), len(verb_ops)))
|
||||
pulled = [(f, phase[f]) for f in accessors if ownership[f] == "BARRIER_PULLED"]
|
||||
print("gen_pipe_field_ownership: the debt, by retiring phase:")
|
||||
by_phase = {}
|
||||
for field, retires in pulled:
|
||||
by_phase.setdefault(retires, []).append(field)
|
||||
for retires in sorted(by_phase):
|
||||
print("gen_pipe_field_ownership: %-42s %d" % (retires, len(by_phase[retires])))
|
||||
|
||||
if changed:
|
||||
if args.check:
|
||||
print("gen_pipe_field_ownership: OUT OF DATE: %s" % ", ".join(changed), file=sys.stderr)
|
||||
return 1
|
||||
print("gen_pipe_field_ownership: wrote %s" % ", ".join(changed))
|
||||
else:
|
||||
print("gen_pipe_field_ownership: generated file is up to date")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user