Files
MobileGL/scripts/gen_pipe.py
T
swung0x48 1154f9a00d [Fix] (MG_Remote, Transport): give the inproc doorbell a death state so Shutdown can join a parked waiter, and bound a ring record at half the capacity so a refusal can never look like backpressure
- T-1: InProcessChannel::Close rang each CondVarDoorbell once and claimed that unparks a peer mid-frame. It does not. Doorbell::Wait consumes the one ring, re-tests a condition nothing published, finds the bell alive (CondVarDoorbell never overrode Dead(); Doorbell.cpp had no death state at all) and with kWaitForever parks again for good - so Shutdown could never join a server thread sitting in the design's own steady state (spun, set consumerParked, blocked; plan section 8.1 inheriting the earlier plan's 6.2a). CondVarDoorbell now carries an atomic death latch: Kill() sets it under the mutex and notify_all's, Dead() reports it, Park returns false at once on a dead bell (and the wait predicate includes it, so a Kill cannot slip between the test and the wait), and Close kills both bells instead of ringing them. Same shape as SocketDoorbell's EOF latch; Notify stays the ordinary wakeup.
- T-2: RingProducer::Reserve refused only total > capacity, but a record with capacity/2 < total <= capacity is unplaceable at every head offset where neither the space to the wrap boundary nor the space before it holds it - even in an EMPTY ring, because a wrap pad costs spaceToEnd bytes on top of the record. Concretely: head offset 16 of an empty 256-byte ring, a 248-byte record; FreeBytes() says 256, Reserve says nullptr, forever, and a producer waiting for FreeBytes() >= 248 stalls with nothing logged. The bound is now capacity/2, which is exact rather than conservative (worst case 2*total-8 <= capacity-8), exposed as MaxRecordBytes() for the emitter to chunk against; the minimum ring is two headers so the smallest record still fits the bound. Ring.h states Capacity()/2 as the chunking bound and the G3 header comment in gen_pipe.py now states the chunking rule plan section 8.2 asks G3 to define (PipeWire.inc regenerated).
- Tests, each shown red with only the fix site reverted and green with it: InProcessTransportTest.ShutdownUnparksAWaiterWithNoDeadline (bounded join through a shared_ptr-owned waiter: 5 s red instead of a hung job; reverted it hangs and fails at 5051 ms), RingTest.RecordLargerThanHalfTheRingIsRefused (reverted, the 248-byte record is accepted), RingTest.RecordPlaceabilityDoesNotDependOnTheHeadOffset (the offset-0 vs offset-16 negative control), RingTest.HalfCapacityRecordFitsAtEveryHeadOffset (the positive half: the maximal record at all 32 head offsets of a 256-byte ring) and RingTest.RejectsARingTooSmallForTheSmallestRecord.
2026-09-05 21:19:00 -04:00

649 lines
30 KiB
Python

#!/usr/bin/env python3
# MobileGL - scripts/gen_pipe.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
"""The seven MGPipe generators, G1..G7 (plan B section 4.1).
Reads the three hand-maintained sources of truth
MobileGL/MG_Pipe/PipeCalls.def the call catalogue
MobileGL/MG_Pipe/PipeFields.def per-payload field lists for the verify comparator
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory
plus the vendored copy of the backend read inventory
scripts/data/backend_read_inventory.md
and writes MobileGL/MG_Pipe/generated/*.inc. The outputs are COMMITTED; CI regenerates
them and fails on a diff, which is what keeps the seven generators from drifting apart
from the catalogue (they all consume the same .def).
python3 scripts/gen_pipe.py # write the generated files, print the summary
python3 scripts/gen_pipe.py --check # fail if regenerating would change anything
"""
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")
INVENTORY = os.path.join(REPO_ROOT, "scripts", "data", "backend_read_inventory.md")
GENERATED_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
// {title}
//
// GENERATED by scripts/gen_pipe.py from {sources} - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
"""
# G7. The pipeline subset of RenderStateParameters, BY MEMBER NAME, taken from the fields
# VulkanRenderer::ComputePipelineStateHash hashes today
# (MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:4805-4906 at dev@81b17c0b,
# including ResolveEffectiveSampleMask, which the hash folds in twice - once as the
# effective enable bit and once as the mask word).
#
# Names only: offsets are NOT computed here. The chunk table with real offsets is
# MGPipeRenderStateSpans.cpp, built in C++ with offsetof, because a python guess at the
# layout of a struct it cannot see is exactly the kind of drift the G7 setter-consistency
# test exists to catch (plan B section 4.5.2).
PIPELINE_STATE_MEMBERS = [
"CullFaceEnabled",
"DepthTestEnabled",
"PolygonOffsetFillEnabled",
"RasterizerDiscardEnabled",
"ColorLogicOpEnabled",
"StencilTestEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"DepthMask",
"SampleShadingEnabled",
"MultisampleEnabled",
"SampleMaskEnabled",
"SampleMaskValue",
"MinSampleShadingValue",
"PatchVertices",
"PatchDefaultOuterLevel",
"PatchDefaultInnerLevel",
"PolygonModeFront",
"CullFaceModeSetting",
"DepthFunc",
"LogicOp",
"StencilStates",
"BlendStates",
"ColorMasks",
]
def read(path):
with open(path, "r", encoding="utf-8") as handle:
return handle.read()
class Call(object):
def __init__(self, index, name, payload, cls, flags):
self.Index = index # 1-based; this is the wire opcode
self.Name = name
self.Payload = payload
self.Class = cls
self.Flags = flags
@property
def IsScreen(self):
return self.Class == "kScreen"
@property
def Signature(self):
"""(parameter declaration list, argument list) for this call."""
params = ["const %s* payload" % self.Payload]
args = ["payload"]
if "kVarTail" in self.Flags:
params.append("const void* varTail")
params.append("Uint32 varTailCount")
args.append("varTail")
args.append("varTailCount")
if "kReplySlot" in self.Flags:
params.append("MGPReplySlot* reply")
args.append("reply")
return ", ".join(params), ", ".join(args)
CALL_RE = re.compile(r"^\s*X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*([\w|]+?)\s*\)\s*\\?\s*$")
def parse_calls():
text = read(os.path.join(PIPE_DIR, "PipeCalls.def"))
documented = re.search(r"#define MGP_CALL_LIST_DOCUMENTED_COUNT (\d+)", text)
if not documented:
sys.exit("PipeCalls.def: MGP_CALL_LIST_DOCUMENTED_COUNT is missing")
calls = []
inside = False
for line in text.splitlines():
if line.startswith("#define MGP_CALL_LIST(X)"):
inside = True
continue
if not inside:
continue
match = CALL_RE.match(line)
if match:
calls.append(Call(len(calls) + 1, match.group(1), match.group(2), match.group(3),
match.group(4).split("|")))
# The macro ends at the first line without a continuation backslash.
if not line.rstrip().endswith("\\"):
inside = False
count = int(documented.group(1))
if len(calls) != count:
sys.exit("PipeCalls.def: parsed %d calls but MGP_CALL_LIST_DOCUMENTED_COUNT says %d"
% (len(calls), count))
seen = set()
for call in calls:
if call.Name in seen:
sys.exit("PipeCalls.def: duplicate call %s" % call.Name)
seen.add(call.Name)
return calls
def parse_verify_payloads():
text = read(os.path.join(PIPE_DIR, "PipeFields.def"))
match = re.search(r"#define MGP_VERIFY_PAYLOAD_LIST\(P\)(.*?)\n\n", text, re.S)
if not match:
sys.exit("PipeFields.def: MGP_VERIFY_PAYLOAD_LIST is missing")
payloads = re.findall(r"P\((\w+)\)", match.group(1))
for payload in payloads:
if ("#define MGP_FIELDS_%s(F)" % payload) not in text:
sys.exit("PipeFields.def: %s is in the payload list with no field macro" % payload)
return payloads
def parse_coverage():
text = read(os.path.join(PIPE_DIR, "Coverage.def"))
accessors = []
block = re.search(r"#define MGP_COVERAGE_ACCESSOR_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_ACCESSOR_LIST is missing")
for name, call in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block.group(1)):
accessors.append((name, call))
deltas = []
block = re.search(r"#define MGP_COVERAGE_DELTA_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_DELTA_LIST is missing")
for kind, call in re.findall(r"X\(([^,]+),\s*(\w+)\)", block.group(1)):
deltas.append((kind.strip(), call))
return accessors, deltas
INVENTORY_ROW_RE = re.compile(r"^\|\s*(\d+)\s*\|([^|]*)\|([^|]*)\|([^|]*)\|")
def parse_inventory():
if not os.path.exists(INVENTORY):
sys.exit("missing %s - copy it from MobileGL-CS/docs/CS_Refactor/" % INVENTORY)
rows = []
current_file = None
for line in read(INVENTORY).splitlines():
heading = re.match(r"^### `([^`]+)`", line)
if heading:
current_file = heading.group(1)
continue
match = INVENTORY_ROW_RE.match(line)
if match and current_file:
rows.append({
"file": current_file,
"line": int(match.group(1)),
"kind": match.group(2).strip(),
"member": match.group(3).strip(),
"delta": match.group(4).strip(),
})
return rows
def banner(name, title, sources):
return GENERATED_BANNER.format(name=name, title=title, sources=sources)
def gen_tables(calls):
screen = [c for c in calls if c.IsScreen]
context = [c for c in calls if not c.IsScreen]
out = [banner("PipeTables.inc", "G1: the two MGPipe interface tables.", "PipeCalls.def")]
for struct_name, group, what in (("MGPipeScreen", screen, "share group"),
("MGPipeContext", context, "context")):
out.append("// %s: %d calls. A null entry means the backend does not implement this\n"
"// call and the frontend keeps its own path (plan B section 4.1).\n"
"struct %s {" % (what, len(group), struct_name))
for call in group:
params, _ = call.Signature
out.append(" void (*%s)(%s);" % (call.Name, params))
out.append("};\n")
out.append("inline constexpr SizeT kMGPipeScreenCallCount = %d;" % len(screen))
out.append("inline constexpr SizeT kMGPipeContextCallCount = %d;" % len(context))
out.append("inline constexpr SizeT kMGPipeCallCount = %d;" % len(calls))
out.append("")
out.append("// A table that is not exactly its call count of function pointers has grown a")
out.append("// member that no generator knows about.")
out.append("static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()),")
out.append(" \"MGPipeScreen is not exactly its catalogue's function pointers\");")
out.append("static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()),")
out.append(" \"MGPipeContext is not exactly its catalogue's function pointers\");")
out.append("static_assert(kMGPipeScreenCallCount + kMGPipeContextCallCount == kMGPipeCallCount);")
out.append("static_assert(kMGPipeCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT,")
out.append(" \"the catalogue and its documented count disagree\");")
return "\n".join(out) + "\n"
def gen_thunks(calls):
out = [banner("PipeThunks.inc", "G2: monolith thunks over the two tables.", "PipeCalls.def")]
out.append("// One inline call through the installed table. These are the names MG_Impl call")
out.append("// sites move onto, replacing gBackendFunctionsTable.GL.* one at a time. An")
out.append("// unimplemented (null) entry is the caller's business to check, exactly as it is")
out.append("// with the table this replaces.\n")
for call in calls:
params, args = call.Signature
table = "gMGPipeScreen" if call.IsScreen else "gMGPipeContext"
out.append("inline void MGP_%s(%s) {" % (call.Name, params))
out.append(" %s.%s(%s);" % (table, call.Name, args))
out.append("}")
out.append("")
return "\n".join(out)
def gen_wire(calls):
out = [banner("PipeWire.inc", "G3: wire records, size assertions and the applier's bounds gate.",
"PipeCalls.def")]
out.append("""// Every record is a fixed header plus its payload, padded to the stream's 8-byte
// granularity. The size assertion is stated as a COMPOSITION so it fires on any padding
// the compiler inserts between the header and the payload while staying honest about the
// tail padding the alignment requires.
//
// The applier's precondition is checked BEFORE dispatch, on every record, in every build:
// a record that is shorter than its own type, longer than what is left in the buffer, or
// not a multiple of 8 is protocol corruption and is fatal. There is no recovery path -
// silently applying a truncated record is how a corrupt stream becomes a wrong picture.
//
// OVERSIZED PAYLOADS ARE CHUNKED, NEVER EMITTED WHOLE (plan section 8.2: G3 has to define
// the path for a record larger than the segment). The bound is the ring's,
// RingProducer::MaxRecordBytes() == Capacity()/2, and it is exact rather than
// conservative: a record has to be placeable at every head offset of an empty ring, the
// wrap pad in front of it costs up to total-8 bytes, and only a record of at most half the
// ring survives that at every offset. An emitter holding more than Capacity()/2 bytes of
// record (a large resource_subdata, a create_shader_state archive) splits it into several
// records of at most that size; the transport refuses a bigger one outright - nullptr plus
// an MGLOG_E - rather than let the producer wait on free bytes that can never suffice.
struct MGPWireRecHeader {
Uint16 Op; // MGPWireOp
Uint16 Flags; // MGPipeCallFlags of the call, for asserts and tracing
Uint32 Size; // bytes of this record including the header and the variable tail
};
static_assert(sizeof(MGPWireRecHeader) == 8, "the wire header is 8 bytes");
static_assert(std::is_trivially_copyable_v<MGPWireRecHeader>);
// The opcode is the call's position in PipeCalls.def. Reordering that file is a protocol
// break; appending to it is not.
enum class MGPWireOp : Uint16 {
kInvalid = 0,""")
for call in calls:
out.append(" %s = %d," % (call.Name, call.Index))
out.append(" kOpCount = %d," % (len(calls) + 1))
out.append("};\n")
for call in calls:
out.append("struct alignas(8) MGPWireRec_%s {" % call.Name)
out.append(" MGPWireRecHeader Header;")
out.append(" %s Payload;" % call.Payload)
out.append("};")
out.append("static_assert(sizeof(MGPWireRec_%s) ==" % call.Name)
out.append(" ((sizeof(MGPWireRecHeader) + sizeof(%s) + 7u) & ~SizeT(7u))," % call.Payload)
out.append(" \"MGPWireRec_%s gained padding; the wire format moved\");" % call.Name)
out.append("")
out.append("""[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
std::abort();
}
#define MGP_WIRE_CHECK_BOUNDS(RecType, CallName) \\
do { \\
if (!(size >= sizeof(RecType) && size <= remaining && (size % 8) == 0)) { \\
MGPipeWireProtocolFatal(CallName, size, remaining); \\
} \\
} while (0)
// Returns whether the record was applied. P0 is a SKELETON: every case validates its
// bounds and then reports "not applied", because no applier exists until P5 wires
// MG_Remote/Server/PipeApplier.cpp to the real backend tables. The switch and the opcode
// enum come from the same list, so a call added to the catalogue cannot be forgotten here;
// the default arm is for the opcode that never came from this catalogue at all - a byte
// off a corrupt stream - and it is fatal for the same reason the bounds check is.
inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, Uint64 remaining) {
(void)record;
switch (op) {""")
for call in calls:
out.append(" case MGPWireOp::%s:" % call.Name)
out.append(" MGP_WIRE_CHECK_BOUNDS(MGPWireRec_%s, \"%s\");" % (call.Name, call.Name))
out.append(" return false;")
out.append(""" case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
MGPipeWireProtocolFatal("<unknown opcode>", size, remaining);
}
}
#undef MGP_WIRE_CHECK_BOUNDS""")
return "\n".join(out) + "\n"
def gen_verify(payloads):
out = [banner("PipeVerify.inc", "G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.",
"PipeFields.def")]
out.append("""// Field by field, never memcmp over a whole payload: RenderStateParameters is documented
// in DirectGLES.cpp to false-DIFFER on padding under memcmp (harmlessly there, fatally
// here - a comparator with false positives is a comparator nobody reads). Each function
// reports the FIRST differing field by name, which with the draw serial is what the verify
// harness prints.
//
// Floating-point fields are compared by BITS, so a NaN patch level - which
// glPatchParameterfv accepts and ComputePipelineStateHash already hashes bitwise - equals
// itself instead of tripping every draw.
#include "../PipeFields.def"
""")
out.append("template <class T>")
out.append("struct MGPipeHasFieldVerifier : std::false_type {};\n")
for payload in payloads:
out.append("inline Bool MGPipeVerify(const %s& a, const %s& b, const char** outField);"
% (payload, payload))
out.append("")
for payload in payloads:
out.append("template <>")
out.append("struct MGPipeHasFieldVerifier<%s> : std::true_type {};" % payload)
out.append("")
out.append("""template <class T>
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
if constexpr (MGPipeHasFieldVerifier<T>::value) {
const char* unusedField = nullptr;
return MGPipeVerify(a, b, &unusedField);
} else if constexpr (std::is_floating_point_v<T>) {
return std::memcmp(&a, &b, sizeof(T)) == 0;
} else if constexpr (std::is_scalar_v<T> || std::is_enum_v<T>) {
return a == b;
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
return a == b;
} else {
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
// and this branch stops being reachable from any payload.
return std::memcmp(&a, &b, sizeof(T)) == 0;
}
}
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
for (SizeT i = 0; i < N; ++i) {
if (!MGPipeFieldEqual(a[i], b[i])) return false;
}
return true;
}
#define MGP_VERIFY_FIELD(FieldName) \\
if (!MGPipeFieldEqual(a.FieldName, b.FieldName)) { \\
if (outField != nullptr) *outField = #FieldName; \\
return false; \\
}
""")
for payload in payloads:
out.append("inline Bool MGPipeVerify(const %s& a, const %s& b, const char** outField) {"
% (payload, payload))
out.append(" MGP_FIELDS_%s(MGP_VERIFY_FIELD)" % payload)
out.append(" return true;")
out.append("}")
out.append("")
out.append("#undef MGP_VERIFY_FIELD")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerifiedPayloadCount = %d;" % len(payloads))
return "\n".join(out) + "\n"
def gen_filled(accessors, calls):
call_names = set(c.Name for c in calls)
out = [banner("PipeFilled.inc", "G5: PipeInputs field ids and the per-verb poison generations.",
"Coverage.def and PipeCalls.def")]
out.append("""// One field id per GLContext accessor the backends actually read (plan B section 6.2:
// PipeInputs is organized by MEMO KEY, not by read point, which is why the field set is
// small and stable across the whole migration).
//
// The poison is a per-verb GENERATION, not a bit. A bitmap cannot see the dangerous case:
// a field filled by the previous DRAW and then read by the glTexSubImage that follows is
// stale, and its bit is already set. So every verb bumps CurrentVerbSerial, filling a
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
""")
out.append("enum class MGPipeInputField : Uint16 {")
for name, _ in accessors:
out.append(" %s," % name)
out.append(" kFieldCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);")
out.append("static_assert(kMGPipeInputFieldCount == %d, \"the PipeInputs field set moved\");" % len(accessors))
out.append("")
out.append("inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
out.append(" \"%s\"," % name)
out.append("};")
out.append("")
out.append("// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each")
out.append("// true has to be argued for in P1 when the fillers land: a sticky field is a field")
out.append("// the poison cannot protect.")
out.append("inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
out.append(" false, // %s" % name)
out.append("};")
out.append("")
out.append("// Which call is expected to have filled a field by the time a verb reads it. Names")
out.append("// come from Coverage.def, so this table and the coverage table cannot disagree.")
out.append("inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] = {")
for name, call in accessors:
marker = "" if call in call_names else " // pseudo-call: not filled by a forward record"
out.append(" \"%s\",%s" % (call, marker))
out.append("};")
out.append("")
out.append("""struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
Uint64 FilledGen[kMGPipeInputFieldCount];
};
[[noreturn]] inline void MGPipeInputPoisonFatal(MGPipeInputField field, const char* verb) {
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \\"%s@%s\\"}",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], verb);
std::abort();
}
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
: state.FilledGen[index] == state.CurrentVerbSerial;
}""")
return "\n".join(out) + "\n"
def gen_coverage(accessors, deltas, rows, calls):
call_names = set(c.Name for c in calls)
pseudo = {"kClientResolved", "kReverseChannel", "kStructuralHandle"}
accessor_map = dict(accessors)
delta_map = dict(deltas)
for _, call in accessors + deltas:
if call not in call_names and call not in pseudo:
sys.exit("Coverage.def: %s is not a call in PipeCalls.def and not a pseudo-call" % call)
mapped = 0
by_pseudo = {name: 0 for name in pseudo}
unmapped_rows = []
per_accessor = {}
for row in rows:
call = None
if row["member"] and row["member"] != "-":
call = accessor_map.get(row["member"])
if call is None:
call = delta_map.get(row["delta"])
if call is None:
unmapped_rows.append(row)
continue
if call in pseudo:
by_pseudo[call] += 1
else:
mapped += 1
key = row["member"] if row["member"] and row["member"] != "-" else row["delta"]
per_accessor.setdefault(key, [call, 0])[1] += 1
out = [banner("PipeCoverage.inc", "G6: backend read inventory -> MGPipe call coverage.",
"Coverage.def and scripts/data/backend_read_inventory.md")]
out.append("""// The acceptance rule (plan B section 10.3-5): regenerate, `git diff --exit-code`, and
// ZERO unmapped rows. P0 permits unmapped rows and only counts them; the count below is
// the number the later gate has to drive to zero.
//
// Three pseudo-calls stand for read points that never become a forward record:
// kClientResolved (the frontend answers it), kReverseChannel (it becomes one of the ten
// MGPipeCallbacks) and kStructuralHandle (the row is a signature carrying a
// SharedPtr<MG_State...> that becomes an MGPipeHandle parameter).
""")
out.append("struct MGPipeCoverageEntry {")
out.append(" const char* Accessor;")
out.append(" const char* Call;")
out.append(" Uint32 ReadPoints;")
out.append("};")
out.append("")
out.append("inline constexpr MGPipeCoverageEntry kMGPipeCoverage[] = {")
for key in sorted(per_accessor):
call, count = per_accessor[key]
out.append(" {\"%s\", \"%s\", %d}," % (key, call, count))
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeCoverageEntryCount = %d;" % len(per_accessor))
out.append("inline constexpr Uint32 kMGPipeInventoryReadPoints = %d;" % len(rows))
out.append("inline constexpr Uint32 kMGPipeInventoryMappedToCall = %d;" % mapped)
out.append("inline constexpr Uint32 kMGPipeInventoryClientResolved = %d;" % by_pseudo["kClientResolved"])
out.append("inline constexpr Uint32 kMGPipeInventoryReverseChannel = %d;" % by_pseudo["kReverseChannel"])
out.append("inline constexpr Uint32 kMGPipeInventoryStructuralHandle = %d;"
% by_pseudo["kStructuralHandle"])
out.append("inline constexpr Uint32 kMGPipeInventoryUnmapped = %d;" % len(unmapped_rows))
out.append("static_assert(kMGPipeCoverageEntryCount == sizeof(kMGPipeCoverage) / sizeof(kMGPipeCoverage[0]));")
out.append("static_assert(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +")
out.append(" kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +")
out.append(" kMGPipeInventoryUnmapped ==")
out.append(" kMGPipeInventoryReadPoints,")
out.append(" \"every inventory row must land in exactly one bucket\");")
return "\n".join(out) + "\n", unmapped_rows, mapped, by_pseudo
def gen_span_table():
out = [banner("PipeSpanTable.inc", "G7: the render-state pipeline subset, by member name.",
"the field list in scripts/gen_pipe.py")]
out.append("""// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
// derived from a list that was reviewed rather than invented.
//
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
// table freezes:
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
// backend read points are constant false today. Pipeline state or dead capability?
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
// are handled elsewhere in the payload path rather than in the memo word.
//
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
// glViewport must not mint a new CSO.
inline constexpr const char* const kMGPipePipelineStateMembers[] = {""")
for member in PIPELINE_STATE_MEMBERS:
out.append(" \"%s\"," % member)
out.append("};")
out.append("inline constexpr SizeT kMGPipePipelineStateMemberCount = %d;" % len(PIPELINE_STATE_MEMBERS))
out.append("static_assert(kMGPipePipelineStateMemberCount ==")
out.append(" sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));")
out.append("")
out.append("// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets")
out.append("// in C++ with offsetof rather than guessing them in python.")
out.append("extern const MGPStateChunk kMGPipePipelineChunks[];")
out.append("extern const MGPStateChunk kMGPipeDynamicChunks[];")
return "\n".join(out) + "\n"
def write(path, text, check, changed):
existing = read(path) if os.path.exists(path) else None
if existing == text:
return
changed.append(os.path.relpath(path, REPO_ROOT))
if not check:
with open(path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
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")
args = parser.parse_args()
calls = parse_calls()
payloads = parse_verify_payloads()
accessors, deltas = parse_coverage()
rows = parse_inventory()
if not os.path.isdir(GENERATED_DIR):
os.makedirs(GENERATED_DIR)
coverage_text, unmapped, mapped, pseudo = gen_coverage(accessors, deltas, rows, calls)
changed = []
write(os.path.join(GENERATED_DIR, "PipeTables.inc"), gen_tables(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWire.inc"), gen_wire(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeCoverage.inc"), coverage_text, args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeSpanTable.inc"), gen_span_table(), args.check, changed)
screen = sum(1 for c in calls if c.IsScreen)
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields"
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors)))
print("gen_pipe: inventory %d rows: %d -> call, %d client-resolved, %d reverse-channel, "
"%d structural handle, %d UNMAPPED"
% (len(rows), mapped, pseudo["kClientResolved"], pseudo["kReverseChannel"],
pseudo["kStructuralHandle"], len(unmapped)))
for row in unmapped:
print("gen_pipe: UNMAPPED %s:%d %s %s" % (row["file"], row["line"], row["kind"], row["member"]))
if changed:
if args.check:
print("gen_pipe: OUT OF DATE: %s" % ", ".join(changed), file=sys.stderr)
return 1
print("gen_pipe: wrote %s" % ", ".join(changed))
else:
print("gen_pipe: generated files are up to date")
return 0
if __name__ == "__main__":
sys.exit(main())