Files
MobileGL/scripts/gen_pipe.py
T

1737 lines
86 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 four 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,
and the sticky-field list
MobileGL/MG_Pipe/FillPoints.def verb -> class and class -> may-read field tables
(G5b), checked against MG_Backend::GLFunctionsTable
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
python3 scripts/gen_pipe.py --self-test # the negative controls: each gate below must trip
Both modes refuse a catalogue whose call payload has no field list in PipeFields.def: a
payload the G4 comparator cannot see is a payload MOBILEGL_PIPE_VERIFY is blind to. Both
also refuse a field list that does not name every data member of its struct (or names one
that is not a member), and a backend accessor read through gPipeInputs-> / pGLContext-> that
has no Coverage.def row (P1 brief D8, D12).
"""
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")
FUNCTION_TABLE_HEADER = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend", "BackendObject.h")
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, in DECLARATION order.
#
# P0 took this list from what VulkanRenderer::ComputePipelineStateHash hashed. P2 replaced
# that provenance with a RULE, and the rule is the only thing that decides membership:
#
# A member is in the pipeline subset if and only if some public RenderState setter that
# calls BumpVersions() writes it. Everything else is dynamic. There is no third set.
#
# That makes G7's invariant - the pipeline-subset hash moves IFF m_pipelineStateVersion
# moves - true by construction, and it makes the subset a strict SUPERSET of the 24 members
# the Vulkan hash read: it adds sample coverage, the front face, the provoking vertex, the
# scissor-test mask, the back polygon mode, the eleven capability bools the hash never read,
# and the three capabilities P2 gave storage to. The alternative - demoting those setters to
# ++m_version - would change MG_State semantics in the PULL build for the push path's sake.
#
# 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). StencilStates is named once and straddles:
# per face, Func and the three ops are pipeline, Ref/ValueMask/WriteMask are dynamic.
PIPELINE_STATE_MEMBERS = [
"PatchVertices",
"PatchDefaultOuterLevel",
"PatchDefaultInnerLevel",
"BlendStates",
"LogicOp",
"DepthTestEnabled",
"DepthFunc",
"DepthMask",
"ColorMasks",
"FramebufferSrgbEnabled",
"DepthClampEnabled",
"TextureCubeMapSeamlessEnabled",
"SampleCoverageValue",
"SampleCoverageInvert",
"ShadingRate",
"ShadingRateCombinerOp0",
"ShadingRateCombinerOp1",
"SampleMaskValue",
"MinSampleShadingValue",
"StencilStates",
"CullFaceEnabled",
"CullFaceModeSetting",
"FrontFaceModeSetting",
"ProvokingVertexModeSetting",
"PolygonModeFront",
"PolygonModeBack",
"ColorLogicOpEnabled",
"DebugOutputEnabled",
"DebugOutputSynchronousEnabled",
"DitherEnabled",
"LineSmoothEnabled",
"MultisampleEnabled",
"PolygonOffsetFillEnabled",
"PolygonOffsetLineEnabled",
"PolygonOffsetPointEnabled",
"PolygonSmoothEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"RasterizerDiscardEnabled",
"SampleAlphaToCoverageEnabled",
"SampleAlphaToOneEnabled",
"SampleCoverageEnabled",
"SampleMaskEnabled",
"SampleShadingEnabled",
"StencilTestEnabled",
"ProgramPointSizeEnabled",
"ScissorTestEnabledMask",
]
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, wait="kWaitNone"):
self.Index = index # 1-based; this is the wire opcode
self.Name = name
self.Payload = payload
self.Class = cls
self.Flags = flags
# P5e: PipeCalls.def's fifth column. Defaulted so the self-test's canned Calls -
# which exercise the FLAG gates and say nothing about waiting - stay one line.
self.Wait = wait
@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"]
# P5 R-17. EXACTLY the shape kVarTail already has, one flag over: a kHasBlob row's
# payload owns an MGPBlobRef, and a blobref names bytes that live somewhere the
# payload cannot reach on its own. In monolith "somewhere" is the caller's own
# memory and the companion is the raw pointer every MG_Impl call site passes today
# (CONTRACT-P5 table 1's "companion pointer today" column, nine rows); under split
# it is a SEG_STAGE run the client staged before it published. A table row that
# omitted the pair could reproduce NEITHER, which is why nine of the thirty-seven
# entry points had no routing that could be written at all.
if "kHasBlob" in self.Flags:
params.append("const void* blobBytes")
params.append("Uint64 blobByteCount")
args.append("blobBytes")
args.append("blobByteCount")
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*,"
r"\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("|"), match.group(5)))
# 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
# Types the G4 comparator may fall back to memcmp for: NONE since P1. The value structs and
# MGHostSpan have field lists of their own, and the fallback branch of MGPipeFieldEqual is a
# static_assert, so a future struct without a field list is a compile error rather than a
# padding false positive. Kept as a (deliberately empty) set so the check below keeps its
# shape.
MEMCMP_FALLBACK_TYPES = set()
# Where the structs named in PipeFields.def are declared: the payload header, the value
# header, the host-span header and - for DynamicBackendParameters, the caps block - the
# backend object header.
FIELD_LIST_STRUCT_HEADERS = [
os.path.join(PIPE_DIR, "MGPipeTypes.h"),
os.path.join(PIPE_DIR, "MGPipeValueTypes.h"),
os.path.join(PIPE_DIR, "MGPipeHostSpan.h"),
FUNCTION_TABLE_HEADER,
]
BACKEND_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend")
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
FIELD_LIST_RE = re.compile(r"#define MGP_FIELDS_(\w+)\(F\)")
def parse_field_lists(text=None):
"""PipeFields.def -> {payload: [field, ...]} for every MGP_FIELDS_<Payload>(F) macro; the
macro ends at the first line without a continuation backslash."""
if text is None:
text = read(os.path.join(PIPE_DIR, "PipeFields.def"))
lists = {}
lines = text.splitlines()
i = 0
while i < len(lines):
match = FIELD_LIST_RE.match(lines[i])
if not match:
i += 1
continue
name = match.group(1)
body = []
while i < len(lines):
body.append(lines[i])
if not lines[i].rstrip().endswith("\\"):
break
i += 1
i += 1
fields = re.findall(r"\bF\((\w+)\)", "\n".join(body))
if name in lists:
sys.exit("PipeFields.def: MGP_FIELDS_%s is defined twice" % name)
lists[name] = fields
return lists
def mask_comments_and_strings(text):
"""Replace comment and string-literal bodies with spaces, keeping every offset and
newline, so the regexes below cannot match inside a comment or a literal
(gen_pipe_dirty_surface.py's shape)."""
out = list(text)
i = 0
n = len(text)
while i < n:
c = text[i]
if c == "/" and i + 1 < n and text[i + 1] == "/":
while i < n and text[i] != "\n":
out[i] = " "
i += 1
elif c == "/" 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] = " "
if i + 1 < n:
out[i + 1] = " "
i += 2
elif c in "\"'":
quote = c
i += 1
while i < n and text[i] != quote:
if text[i] == "\\":
out[i] = " "
i += 1
if i < n and text[i] != "\n":
out[i] = " "
i += 1
if i < n:
out[i] = " "
i += 1
else:
i += 1
return "".join(out)
PADDING_MEMBER_RE = re.compile(r"^Pad\d*$")
NESTED_TYPE_RE = re.compile(r"^\s*(?:struct|class|union|enum)\b")
FUNCTION_HEAD_RE = re.compile(r"\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:=\s*(?:default|delete|0)\s*)?$")
NON_MEMBER_RE = re.compile(r"^\s*(?:static|using|typedef|friend|template|explicit|virtual|operator)\b")
def find_struct_body(masked, name):
"""The text between the braces of `struct <name> {` (an optional base list allowed), or
None. Comments and strings must already be masked."""
match = re.search(r"\bstruct\s+%s\s*(?::[^{;]*)?\{" % re.escape(name), masked)
if not match:
return None
i = match.end()
depth = 1
start = i
while i < len(masked) and depth:
if masked[i] == "{":
depth += 1
elif masked[i] == "}":
depth -= 1
i += 1
return masked[start:i - 1]
def matching_brace(text, open_index):
depth = 0
j = open_index
while j < len(text):
if text[j] == "{":
depth += 1
elif text[j] == "}":
depth -= 1
if depth == 0:
return j
j += 1
return len(text) - 1
def strip_balanced(text, open_char, close_char):
out = []
depth = 0
for c in text:
if c == open_char:
depth += 1
elif c == close_char:
depth -= 1
elif depth == 0:
out.append(c)
return "".join(out)
def member_names(statement):
"""The data-member names declared by one struct-body statement, or [] for anything
that is not a data member (a function, a static, a using, an access label...)."""
statement = re.sub(r"^\s*(?:public|private|protected)\s*:", "", statement).strip()
if not statement or NON_MEMBER_RE.match(statement):
return []
# The declarator part is what precedes the default initializer.
left = re.split(r"=|\{\.\.\.\}", statement, maxsplit=1)[0]
if "(" in left:
return [] # a function declaration
left = strip_balanced(left, "<", ">")
left = strip_balanced(left, "[", "]")
names = []
for k, chunk in enumerate(left.split(",")):
tokens = re.findall(r"[A-Za-z_]\w*", chunk)
if k == 0 and len(tokens) < 2:
return [] # no type: not a declaration
if not tokens:
return []
names.append(tokens[-1])
return [n for n in names if not PADDING_MEMBER_RE.match(n)]
def struct_data_members(body):
"""Direct data members of a struct body, in declaration order: statics, member
functions, nested types and Pad-named members excluded."""
members = []
statement = []
i = 0
n = len(body)
while i < n:
c = body[i]
if c == "{":
head = "".join(statement)
close = matching_brace(body, i)
if NESTED_TYPE_RE.match(head.strip()) or FUNCTION_HEAD_RE.search(head.rstrip()):
# A nested type or a member-function body: not a data member. Swallow the
# nested type's trailing semicolon too.
statement = []
i = close + 1
if NESTED_TYPE_RE.match(head.strip()):
while i < n and body[i] in " \t\n":
i += 1
if i < n and body[i] == ";":
i += 1
continue
statement.append("{...}") # a brace default initializer
i = close + 1
continue
if c == ";":
members.extend(member_names("".join(statement)))
statement = []
i += 1
continue
statement.append(c)
i += 1
return members
def check_field_lists_cover_struct_members(field_lists, payloads, header_texts=None):
"""Every payload in MGP_VERIFY_PAYLOAD_LIST: its MGP_FIELDS_ list must name every direct
data member of `struct <Payload> {` (Pad-named members are padding and excluded) and
nothing that is not a member. A member without an F(...) is a field MOBILEGL_PIPE_VERIFY
is blind to; an F(...) that is not a member is a list that stopped describing its
struct. Runs in both modes, --check included, so it is part of pipe-gates."""
if header_texts is None:
header_texts = [read(path) for path in FIELD_LIST_STRUCT_HEADERS]
masked = [mask_comments_and_strings(t) for t in header_texts]
problems = []
for payload in payloads:
body = None
for m in masked:
body = find_struct_body(m, payload)
if body is not None:
break
if body is None:
problems.append("%s: struct not found in %s" % (payload, ", ".join(os.path.basename(p) for p in FIELD_LIST_STRUCT_HEADERS)))
continue
members = struct_data_members(body)
listed = field_lists.get(payload, [])
missing = [m for m in members if m not in listed]
extra = [f for f in listed if f not in members]
if missing:
problems.append("%s: member(s) with no F(...) in PipeFields.def: %s" % (payload, ", ".join(missing)))
if extra:
problems.append("%s: F(...) name(s) that are not members: %s" % (payload, ", ".join(extra)))
if not members:
problems.append("%s: no data members parsed" % payload)
if problems:
sys.exit("PipeFields.def does not cover its structs:\n " + "\n ".join(problems))
ACCESSOR_READ_RE = re.compile(r"\b(?:gPipeInputs\.|pGLContext)\s*->\s*(\w+)")
# The scanner's NAMED exemptions: accessors a backend reads through gPipeInputs-> / pGLContext->
# that are NOT PipeInputs fields and never will be, each with the reason written down. A name
# here is a DEBT ENTRY, not a silence: it exists so the gate can tell "scoped, named, phased"
# from "forgot the row".
#
# P13: the last two (the G6 frontend-keyed registry's framebuffer probes, GetFramebufferObject and
# FindFramebufferObjectByLifetimeId) went with the frontend-keyed registry itself; no backend
# reads them any more.
SCAN_EXEMPT_ACCESSORS = {}
def scan_live_accessors(accessors, backend_dir=None, verbose=True):
"""Every accessor a backend reads through gPipeInputs-> or pGLContext-> (comments and
strings masked) must have a Coverage.def row - a read without a row is a PipeInputs
field that does not exist. Rows no backend reads are printed, not refused (the dead
GetBoundTransformFeedbackName row is deliberate). Returns the set of names read."""
if backend_dir is None:
backend_dir = BACKEND_DIR
known = set(name for name, _ in accessors)
read_names = {}
for root, _, files in os.walk(backend_dir):
for name in sorted(files):
if not name.endswith((".cpp", ".h")):
continue
path = os.path.join(root, name)
masked = mask_comments_and_strings(read(path))
for match in ACCESSOR_READ_RE.finditer(masked):
read_names.setdefault(match.group(1), set()).add(os.path.relpath(path, REPO_ROOT))
unknown = sorted(n for n in read_names if n not in known and n not in SCAN_EXEMPT_ACCESSORS)
if unknown:
sys.exit("Coverage.def: accessor(s) read by a backend with no row: %s"
% ", ".join("%s (%s)" % (n, ", ".join(sorted(read_names[n]))) for n in unknown))
unread = sorted(known - set(read_names))
if verbose and unread:
print("gen_pipe: %d accessor row(s) no backend reads: %s" % (len(unread), ", ".join(unread)))
return set(read_names)
def check_call_payloads_have_field_lists(calls, payloads):
"""Every payload PipeCalls.def names must have a G4 field list, or the verify comparator
is silently blind to that call. Runs in both modes, --check included."""
known = set(payloads)
missing = sorted({c.Payload for c in calls
if c.Payload not in known and c.Payload not in MEMCMP_FALLBACK_TYPES})
if missing:
sys.exit("PipeFields.def: call payload(s) with no field list, so MOBILEGL_PIPE_VERIFY "
"would be blind to them: %s" % ", ".join(missing))
FLAG_ENUM_RE = re.compile(r"enum\s+MGPipeCallFlags\s*:\s*Uint32\s*\{(.*?)\}\s*;", re.S)
FLAG_MEMBER_RE = re.compile(r"^\s*(k\w+)\s*=\s*1u\s*<<\s*(\d+)\s*,", re.M)
def parse_call_flags():
"""The MGPipeCallFlags enumerators and their bit positions, READ OUT OF MGPipe.h.
Parsed rather than hard-coded, and that is the point: this list is what the generated
kMGPipeCallFlags[] table spells into C++, and it is also what the emitted assertions pin
against MG_Remote's RingRecordFlags. A seventh flag added to the header has to reach both,
so it must reach this function - a copy here would go stale in exactly the case that
matters. kNone is deliberately absent: it is the empty set, not a flag."""
text = read(os.path.join(PIPE_DIR, "MGPipe.h"))
body = FLAG_ENUM_RE.search(text)
if not body:
sys.exit("MGPipe.h: enum MGPipeCallFlags : Uint32 is missing or has changed shape")
flags = [(name, int(bit)) for name, bit in FLAG_MEMBER_RE.findall(body.group(1))]
if not flags:
sys.exit("MGPipe.h: enum MGPipeCallFlags declares no 1u << N members")
seen = {}
for name, bit in flags:
if bit in seen:
sys.exit("MGPipe.h: %s and %s are both bit %d" % (seen[bit], name, bit))
seen[bit] = name
return flags
KNOWN_CALL_FLAGS = tuple(name for name, _ in parse_call_flags())
def check_call_flags_are_known(calls):
"""Every flag token in PipeCalls.def must be an MGPipeCallFlags enumerator, and kNone may
not be combined with one. Runs in both modes, --check included: P5 R-13.4 exports these
flags as a table six packages read, so a typo here is a wrong decode rather than a
compile error in the one consumer that used to hard-code its own copy."""
for call in calls:
for flag in call.Flags:
if flag != "kNone" and flag not in KNOWN_CALL_FLAGS:
sys.exit("PipeCalls.def: %s carries flag %s, which is not an MGPipeCallFlags "
"enumerator (%s)" % (call.Name, flag, ", ".join(KNOWN_CALL_FLAGS)))
if "kNone" in call.Flags and len(call.Flags) != 1:
sys.exit("PipeCalls.def: %s combines kNone with %s; kNone is the empty set"
% (call.Name, "|".join(f for f in call.Flags if f != "kNone")))
WAIT_ENUM_RE = re.compile(r"enum\s+MGPipeWaitClass\s*:\s*Uint8\s*\{(.*?)\}\s*;", re.S)
WAIT_MEMBER_RE = re.compile(r"^\s*(kWait\w+)\s*(?:=\s*\d+\s*)?,", re.M)
def parse_wait_classes():
"""The MGPipeWaitClass enumerators, READ OUT OF MGPipe.h, for parse_call_flags' reason:
this list is what the generated kMGPipeWaitClasses[] table spells into C++, and a copy
here would go stale in exactly the case that matters. kWaitClassCount is the terminator and
is NOT a class a row may carry - a row that named it would generate a table entry the
lookup uses to mean 'this opcode never came from the catalogue'."""
text = read(os.path.join(PIPE_DIR, "MGPipe.h"))
body = WAIT_ENUM_RE.search(text)
if not body:
sys.exit("MGPipe.h: enum MGPipeWaitClass : Uint8 is missing or has changed shape")
names = WAIT_MEMBER_RE.findall(body.group(1))
if "kWaitClassCount" not in names:
sys.exit("MGPipe.h: enum MGPipeWaitClass has no kWaitClassCount terminator")
classes = [n for n in names if n != "kWaitClassCount"]
if not classes:
sys.exit("MGPipe.h: enum MGPipeWaitClass declares no wait classes")
return classes
KNOWN_WAIT_CLASSES = tuple(parse_wait_classes())
def check_wait_classes_are_known(calls):
"""P5e (CONTRACT-P5E.md §2.2). Two gates over PipeCalls.def's fifth column, both in --check
as well, because the column is what the client's wait rule and the sink's barriered
predicate are BOTH computed from - a typo here is a wrong wait on a live wire rather than a
compile error in one consumer.
1. The token must be an MGPipeWaitClass enumerator, and never the kWaitClassCount
terminator, which the generated lookup uses for "not from this catalogue".
2. kReplySlot and kWaitReply must agree, BOTH WAYS. A reply-slot row that did not wait
would be a waiter that hangs - the client would run on and read a slot nobody had
posted into yet; a waiting row with no slot would block on an answer nobody posts.
This is the same class of defect R-13.4 found between the flag column and the payloads,
and it is stated here for the same reason: nothing else has both facts in one place."""
for call in calls:
if call.Wait not in KNOWN_WAIT_CLASSES:
sys.exit("PipeCalls.def: %s carries wait class %s, which is not an MGPipeWaitClass "
"enumerator (%s)" % (call.Name, call.Wait, ", ".join(KNOWN_WAIT_CLASSES)))
owns_slot = "kReplySlot" in call.Flags
waits_reply = call.Wait == "kWaitReply"
if owns_slot and not waits_reply:
sys.exit("PipeCalls.def: %s owns a reply slot but its wait class is %s; a "
"kReplySlot row must be kWaitReply or the client reads a slot nobody "
"posted into" % (call.Name, call.Wait))
if waits_reply and not owns_slot:
sys.exit("PipeCalls.def: %s is kWaitReply but carries no kReplySlot; it would "
"block on an answer nobody posts" % call.Name)
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))
sticky = []
block = re.search(r"#define MGP_COVERAGE_STICKY_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_STICKY_LIST is missing")
accessor_names = set(name for name, _ in accessors)
for name, reason in re.findall(r"X\((\w+)\s*,\s*\"([^\"]*)\"\)", block.group(1)):
if name not in accessor_names:
sys.exit("Coverage.def: sticky field %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST" % name)
if name in dict(sticky):
sys.exit("Coverage.def: sticky field %s is listed twice" % name)
sticky.append((name, reason))
emitted = []
block = re.search(r"#define MGP_COVERAGE_EMITTED_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_EMITTED_LIST is missing")
seen = set()
for name, call in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block.group(1)):
if name not in accessor_names:
sys.exit("Coverage.def: emitted field %s is not an accessor in "
"MGP_COVERAGE_ACCESSOR_LIST" % name)
if name in seen:
sys.exit("Coverage.def: emitted field %s is listed twice" % name)
seen.add(name)
emitted.append((name, call))
return accessors, deltas, sticky, emitted
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_wire_layout(calls, payloads):
"""Compiler-evaluated wire layout, including same-width member reorders."""
out = [banner("PipeWireLayout.inc", "P6.5: wire layout and ordered catalogue.",
"PipeFields.def and PipeCalls.def")]
out += ["struct WireLayoutMember { const char* Name; Uint64 Offset; Uint64 Size; };",
"inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {"]
fields = parse_field_lists()
for payload in payloads:
out.append(' {"%s", 0, sizeof(%s)},' % (payload, payload))
for field in fields[payload]:
out.append(' {"%s.%s", offsetof(%s, %s), sizeof(((%s*)nullptr)->%s)},' %
(payload, field, payload, field, payload, field))
for payload, names in (("MGPipeHandle", ("Slot", "Gen")),
("MGPWireRecHeader", ("Op", "Flags", "Size"))):
out.append(' {"%s", 0, sizeof(%s)},' % (payload, payload))
for field in names:
out.append(' {"%s.%s", offsetof(%s, %s), sizeof(((%s*)nullptr)->%s)},' %
(payload, field, payload, field, payload, field))
out += ["};", "struct WireCallLayout { const char* Name; const char* Payload; Uint64 Op; Uint64 Flags; Uint64 Wait; };",
"inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {"]
for call in calls:
out.append(' {"%s", "%s", static_cast<Uint64>(MGPWireOp::%s), static_cast<Uint64>(%s), static_cast<Uint64>(%s)},' %
(call.Name, call.Payload, call.Name, " | ".join(call.Flags), call.Wait))
out += ["};", ""]
return "\n".join(out)
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
// RING FRAMING FLAGS - MG_Remote::Transport::RingRecordFlags - NOT MGPipeCallFlags.
//
// This field IS RingRecordHeader::flags (Ring.h): the two structs are the same eight bytes
// and the ring writes this one. The two flag spaces OVERLAP AND DISAGREE, so stamping
// MGPipeCallFlagsFor(op) in here is a silent, data-dependent corruption:
//
// bit 0 kNeedsAck == kRecNeedsAck agree
// bit 1 kHasBlob == kRecHasBlob agree
// bit 2 kVarTail vs kRecPad DISAGREE
// bit 3 kHostSpan vs kRecBorrowSlot DISAGREE
// bit 4 kReplySlot vs kRecVarTail DISAGREE
// bit 5 kOptional vs (nothing) no ring counterpart
//
// The first two agreeing is exactly what makes this dangerous: a naive stamp looks right
// in a debugger. It is not. RingProducer::Reserve masks kRecPad out of a caller's flags,
// so bit 2 is defended for anyone who goes through it - and ONLY bit 2, and only on that
// path. A producer that writes this header itself (these are the same eight bytes, so a
// codec with its own header struct does exactly that) loses even that: Pop treats kRecPad
// as a WRAP FILLER AND SKIPS THE RECORD. Bits 3 and 4 are undefended everywhere - a
// stamped kHostSpan tells the consumer this record borrowed a slot into the GPU timeline
// and must retire late, and a stamped kReplySlot claims a tail that is not there.
//
// THE TWO SPACES ARE DISJOINT BY TRANSLATION, NOT SHARED. The encoder maps one to the
// other explicitly (MG_Remote/Wire/PipeWireCodec.cpp) and nothing else may write this
// field. The CALL's flags are not on the wire at all and do not need to be: the opcode is,
// and MGPipeCallFlagsFor(op) recovers them exactly on either side.
Uint16 Flags;
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")
out.append("""// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
// documented as "MGPipeCallFlags of the call", and until this table existed nothing
// generated said what those were: every consumer that needed to know whether a record owns
// an MGPBlobRef, a variable tail or a reply slot had to hard-code its own copy of
// PipeCalls.def's fourth column, and six of them were about to. A hard-coded copy is how
// GetCaps and CreateSamplerState came to carry an MGPBlobRef member with no kHasBlob on
// their line at all - nothing compared the two, because nothing had both in one place.
//
// Index 0 is MGPWireOp::kInvalid and is kNone: the catalogue is 1-based, and an encoder
// that reads flags for an opcode it never got from the catalogue must see the empty set
// rather than another call's flags.
//
// kHasBlob here means EXACTLY "the payload owns an MGPBlobRef member". Three calls carry
// bytes without one - resource_respecify, resource_flush_range and map_persistent, whose
// companion pointers have no carrier - and they are deliberately NOT flagged; MG_Remote's
// CONTRACT-P5.md table 1 is where those live, because a decoder that trusts kHasBlob has
// to find a member to read.""")
out.append("inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)] = {")
out.append(" /* 0 %-24s*/ static_cast<Uint32>(kNone)," % "kInvalid")
for call in calls:
out.append(" /* %2d %-24s*/ static_cast<Uint32>(%s),"
% (call.Index, call.Name, " | ".join(call.Flags)))
out.append("};")
out.append("static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==")
out.append(" static_cast<SizeT>(MGPWireOp::kOpCount),")
out.append(" \"the flags table and the opcode space disagree\");")
out.append("""
// The only supported read of the table. Out-of-range is kNone rather than undefined
// behaviour, because the one caller that can pass a bad opcode is a decoder holding bytes
// off a stream, and it must reach its own Fatal{ProtocolCorruption} rather than read past
// the array on the way there.
inline constexpr Uint32 MGPipeCallFlagsFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kMGPipeCallFlags[index]
: static_cast<Uint32>(kNone);
}
// The catalogue class of every opcode, indexed the same way; index 0 is kCallClassCount,
// never a real class, for the flags table's reason.
inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 */ static_cast<Uint8>(kCallClassCount),
""")
for call in calls:
out.append(" /* %2d %-24s*/ static_cast<Uint8>(%s)," % (call.Index, call.Name, call.Class))
out.append("};")
out.append("static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==")
out.append(" static_cast<SizeT>(MGPWireOp::kOpCount),")
out.append(" \"the class table and the opcode space disagree\");")
out.append("""
inline constexpr MGPipeCallClass MGPipeCallClassFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount)
? static_cast<MGPipeCallClass>(kMGPipeCallClasses[index])
: kCallClassCount;
}
// P5e (MG_Remote/CONTRACT-P5E.md 2.2): THE WAIT CLASS OF EVERY OPCODE, indexed the same way.
// Index 0 is kWaitClassCount - never a real class - for the flags table's reason: the one
// caller that can pass an opcode this catalogue never produced is a decoder holding bytes off
// a stream, and it must reach its own Fatal{ProtocolCorruption} rather than a wait decision.
//
// This is the STATIC HALF of MGPipeBarriered(op, payload, applierState), the predicate the emit
// table and the sink compute identically. The other half is payload-derived (an open
// transform-feedback span, a draw carrying client vertex arrays) and lives in that function,
// because it depends on state a table indexed by opcode cannot see.
inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 */ static_cast<Uint8>(kWaitClassCount),
""")
for call in calls:
out.append(" /* %2d %-24s*/ static_cast<Uint8>(%s)," % (call.Index, call.Name, call.Wait))
out.append("};")
out.append("static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==")
out.append(" static_cast<SizeT>(MGPWireOp::kOpCount),")
out.append(" \"the wait-class table and the opcode space disagree\");")
out.append("""
inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount)
? static_cast<MGPipeWaitClass>(kMGPipeWaitClasses[index])
: kWaitClassCount;
}
// THE COLUMN'S OWN INVARIANT, stated in C++ as well as in the generator: a reply-slot row
// waits for its reply and a waiting row owns a slot. gen_pipe.py refuses a catalogue where the
// two disagree, and this is the same statement where a reader of the generated table will look
// for it - a kReplySlot row that did not wait would run on and read a slot nobody had posted
// into, and a kWaitReply row with no slot would block on an answer nobody posts.
static_assert(MGPipeWaitClassFor(MGPWireOp::kInvalid) == kWaitClassCount,
"opcode 0 is not a call and has no wait class");
static_assert(MGPipeWaitClassFor(MGPWireOp::Present) == kWaitPresent,
"present is paced by the credit, not by a wait after the publish");
static_assert(MGPipeWaitClassFor(MGPWireOp::DrawVbo) == kWaitNone,
"the steady draw path is what P5e exists to stop waiting on");
static_assert(MGPipeWaitClassFor(MGPWireOp::ApplierReset) == kWaitApplied,
"a make-current is a fresh server and every verb after it must see the reset");
static_assert(MGPipeWaitClassFor(MGPWireOp::ReadPixels) == kWaitReply,
"a readback's answer is not derivable (R-5)");
// Spot checks the generator states about its own output, so that a catalogue edit that
// silently drops a flag is a build break here and not a wrong decode six packages away.
static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast<Uint32>(kNone),
"opcode 0 is not a call and carries no flags");
static_assert((MGPipeCallFlagsFor(MGPWireOp::GetCaps) & static_cast<Uint32>(kHasBlob)) != 0,
"MGPCaps owns two MGPBlobRef members; R-13.1 gave the call its flag");
static_assert((MGPipeCallFlagsFor(MGPWireOp::CreateSamplerState) & static_cast<Uint32>(kHasBlob)) != 0,
"MGPSamplerDesc owns an MGPBlobRef member; R-13.1 gave the call its flag");
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceFlushRange) & static_cast<Uint32>(kHasBlob)) == 0,
"R-13.2: resource_flush_range carries no bytes on the wire and owns no blobref");
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceRespecify) & static_cast<Uint32>(kHasBlob)) == 0,
"R-13.3: initial bytes follow as resource_subdata; MGPResourceDesc owns no blobref");
static_assert((MGPipeCallFlagsFor(MGPWireOp::DrawVbo) &
static_cast<Uint32>(kHostSpan | kVarTail)) == static_cast<Uint32>(kHostSpan | kVarTail),
"draw_vbo is the conditional-tail plus host-span shape the codec is measured on");
""")
# The bit layout, pinned from the header the generator just read. This exists because
# MGPWireRecHeader::Flags is RingRecordHeader::flags and three of these bits mean something
# ELSE over there (kVarTail/kRecPad, kHostSpan/kRecBorrowSlot, kReplySlot/kRecVarTail). The
# cross-enum table cannot live here - MG_Pipe is below MG_Remote and may not include
# Ring.h - so it lives in MG_Test/Wire/RingTest.cpp, which sees both. What CAN be stated
# here is the half this side owns: these are the flags, at these bits, and this is all of
# them. Renumber one and the translation on the other side is wrong; both ends go red.
flag_list = parse_call_flags()
out.append("")
out.append("// The bit layout of MGPipeCallFlags, read out of MGPipe.h by the generator.")
out.append("// MG_Test/Wire/RingTest.cpp holds the other half: what each of these bits means")
out.append("// in MG_Remote::Transport::RingRecordFlags, which is NOT the same thing.")
for name, bit in flag_list:
out.append("static_assert(static_cast<Uint32>(%s) == (1u << %d)," % (name, bit))
out.append(" \"MGPipeCallFlags::%s moved bit; MG_Remote's translation is keyed on it\");"
% name)
union = " | ".join(name for name, _ in flag_list)
total = 0
for _, bit in flag_list:
total |= 1 << bit
out.append("// ... and this is ALL of them. A seventh flag changes this number, which is the")
out.append("// build break that sends its author to the ring's flag space before it ships.")
out.append("inline constexpr Uint32 kMGPipeCallFlagsAllBits = static_cast<Uint32>(%s);" % union)
out.append("static_assert(kMGPipeCallFlagsAllBits == 0x%02Xu," % total)
out.append(" \"the MGPipeCallFlags bit set changed; see RingTest's cross-enum table\");")
out.append("")
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)
#if MOBILEGL_BUILD_DISAGGREGATED
// THE DECODER HOOK (P5 w1). MG_Pipe is BELOW MG_Remote and may not include it, so the real
// 71-arm decoder - MG_Remote/Wire/PipeWireCodec.cpp, which resolves the segments, cross-checks
// the variable tails and calls today's MGPipeApply* free functions - installs itself here.
// The indirection is the layering, not a policy: gMGPipeSegmentResolver
// (MGPipeHostSpan.h:47) is the same shape for the same reason.
//
// It lives behind the build option because G1 admits no symbol movement in a PULL build, and
// an inline variable that MGPipeApplyWireRecord odr-uses would be one.
using MGPipeWireRecordApplyFn = Bool (*)(MGPWireOp op, const void* record, Uint64 size,
Uint64 remaining);
inline MGPipeWireRecordApplyFn gMGPipeWireRecordApply = nullptr;
#endif
// Returns whether the record was applied.
//
// THE SWITCH IS THE PER-OPCODE BOUNDS GATE AND NOTHING ELSE, AND IT CANNOT SEE THE TAIL.
// MGP_WIRE_CHECK_BOUNDS proves `size >= sizeof(MGPWireRec_X)`, `size <= remaining` and
// 8-alignment - which is exactly the part a generator can state, because it is the part that
// follows from the opcode alone. A kVarTail record declaring Count = 4000 while carrying 8
// bytes passes every one of those, so the SECOND check - recompute the total from the
// record's own count fields and require it to EQUAL MGPWireRecHeader::Size - belongs to the
// decoder, which has the payload (MG_Remote/Wire's MGPipeWireRecordLayout, P5 w1).
//
// 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.
//
// With no decoder installed - every monolith build, and a split build before
// ClientSession::Start - this returns false, "this build does not implement it". That is the
// P0 skeleton's answer, kept deliberately: a codec that has not been installed must not look
// like one that applied the record.
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(" break;")
out.append(""" case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
MGPipeWireProtocolFatal("<unknown opcode>", size, remaining);
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (gMGPipeWireRecordApply != nullptr) {
return gMGPipeWireRecordApply(op, record, size, remaining);
}
#endif
return false;
}
#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")
out.append("""// A vector type (FloatVec4, IntVec4, BoolVec4...) is detected through its VecBase and
// compared BITWISE over its data: VecBase::operator== is IEEE ==, under which a NaN patch
// level would differ from itself. The probe rather than an overload because a
// derived-to-base conversion loses overload resolution to the exact-match generic template.
template <class Derived, class T, SizeT N>
std::true_type MGPipeVecBaseProbe(const VecBase<Derived, T, N>*);
std::false_type MGPipeVecBaseProbe(const void*);
template <class T>
inline constexpr Bool kMGPipeIsVecBase = decltype(MGPipeVecBaseProbe(static_cast<const T*>(nullptr)))::value;
template <class T>
inline Bool MGPipeFieldEqual(const T& a, const T& b);
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b);
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[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 (kMGPipeIsVecBase<T>) {
return std::memcmp(a.data.data(), b.data.data(), sizeof(a.data)) == 0;
} 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 {
// NO MEMCMP FALLBACK. Every value struct has a field list in PipeFields.def since P1
// (and gen_pipe.py asserts each list covers its struct's members); a type reaching
// this branch is one nobody gave a field list, and a memcmp would false-differ on
// its padding. A compile error is the honest answer.
static_assert(sizeof(T) == 0, "no field list in PipeFields.def for this type");
return false;
}
}
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b) {
for (SizeT i = 0; i < N; ++i) {
if (!MGPipeFieldEqual(a[i], b[i])) return false;
}
return true;
}
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_emitted_by(accessors, calls, emitted):
"""P2 brief D5: which P2 call SUPPLIES each PipeInputs field, so the per-verb residual
fill loop can skip it. Emitters are the distinct calls named in MGP_COVERAGE_EMITTED_LIST,
sorted so the enum is stable against the order rows are written in."""
call_names = set(c.Name for c in calls)
emitted_map = dict(emitted)
for name, call in emitted:
if call not in call_names:
sys.exit("Coverage.def: MGP_COVERAGE_EMITTED_LIST names %s for %s, which is not a "
"call in PipeCalls.def" % (call, name))
emitters = sorted(set(call for _, call in emitted))
out = []
out.append("// P2 brief D5: the call that now SUPPLIES a field, so the residual fill loop no")
out.append("// longer pulls it out of GLContext. kNone means the field is still pulled - which")
out.append("// is what makes MOBILEGL_PIPE_PUSH a true per-subsystem A/B instead of a single")
out.append("// switch. Rows come from Coverage.def's MGP_COVERAGE_EMITTED_LIST.")
out.append("enum class MGPipeFieldEmitter : Uint8 {")
out.append(" kNone = 0,")
for emitter in emitters:
out.append(" %s," % emitter)
out.append("};")
out.append("")
out.append("inline constexpr const char* kMGPipeFieldEmitterNames[] = {")
out.append(" \"kNone\",")
for emitter in emitters:
out.append(" \"%s\"," % emitter)
out.append("};")
out.append("")
out.append("inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
emitter = emitted_map.get(name)
if emitter is None:
out.append(" MGPipeFieldEmitter::kNone, // %s" % name)
else:
out.append(" MGPipeFieldEmitter::%s, // %s" % (emitter, name))
out.append("};")
out.append("inline constexpr SizeT kMGPipeEmittedFieldCount = %d;" % len(emitted))
out.append("")
return "\n".join(out)
def gen_filled(accessors, calls, sticky, emitted):
call_names = set(c.Name for c in calls)
sticky_map = dict(sticky)
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).
//
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
""")
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: a sticky field is a field the poison")
out.append("// cannot protect, so every true is argued for in Coverage.def's")
out.append("// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).")
out.append("inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
if name in sticky_map:
out.append(" true, // %s: %s" % (name, sticky_map[name]))
else:
out.append(" false, // %s" % name)
out.append("};")
out.append("inline constexpr SizeT kMGPipeInputStickyFieldCount = %d;" % len(sticky))
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(gen_emitted_by(accessors, calls, emitted))
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();
}
// FilledGen == 0 is "never filled" on BOTH branches: before the first MGPipeFillForVerb the
// serial is 0 as well, and a read in that window is the poison's "<Field>@<none>" case
// (P1 brief D6), never a fresh read of default-constructed storage.
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
const Uint64 gen = state.FilledGen[index];
if (gen == 0) return false;
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
}""")
return "\n".join(out) + "\n"
FUNCTION_POINTER_MEMBER_RE = re.compile(r"\(\s*\*\s*(\w+)\s*\)\s*\(")
def parse_function_table():
"""The function-pointer members of MG_Backend::GLFunctionsTable, in declaration order.
Data members (PrefersCpuXfbPrimitiveAccounting) are not verbs and are skipped; comments
are masked line by line."""
text = read(FUNCTION_TABLE_HEADER)
start = text.find("struct GLFunctionsTable {")
if start < 0:
sys.exit("%s: struct GLFunctionsTable is missing" % FUNCTION_TABLE_HEADER)
members = []
for line in text[start:].splitlines()[1:]:
if re.match(r"^\s*};", line):
break
code = line.split("//", 1)[0]
for name in FUNCTION_POINTER_MEMBER_RE.findall(code):
members.append(name)
if not members:
sys.exit("%s: GLFunctionsTable has no function-pointer members" % FUNCTION_TABLE_HEADER)
return members
def parse_fill_points(accessors, table_members=None, text=None):
"""FillPoints.def -> (verbs, classes, fields): verbs is [(verb, class)] in file order,
classes is [class], fields is {class: [field]}. Refuses a verb set that is not exactly
GLFunctionsTable's function-pointer members in declaration order, a verb in two
classes, a class with no verbs, a field that is not an accessor, a duplicate
(class, field) row, and a class row that names no class."""
if text is None:
text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
if table_members is None:
table_members = parse_function_table()
def block(macro):
match = re.search(r"#define %s\(X\)(.*?)\n\n" % macro, text, re.S)
if not match:
sys.exit("FillPoints.def: %s is missing (or not followed by a blank line)" % macro)
return match.group(1)
classes = re.findall(r"X\((\w+)\)", block("MGP_FILL_CLASS_LIST"))
if len(classes) != len(set(classes)):
sys.exit("FillPoints.def: a class is listed twice in MGP_FILL_CLASS_LIST")
verbs = re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_VERB_LIST"))
seen = set()
for verb, cls in verbs:
if verb in seen:
sys.exit("FillPoints.def: verb %s is in two classes" % verb)
seen.add(verb)
if cls not in classes:
sys.exit("FillPoints.def: verb %s names unknown class %s" % (verb, cls))
verb_names = [verb for verb, _ in verbs]
missing = [m for m in table_members if m not in seen]
if missing:
sys.exit("FillPoints.def: GLFunctionsTable member(s) without a verb row: %s" % ", ".join(missing))
extra = [v for v in verb_names if v not in table_members]
if extra:
sys.exit("FillPoints.def: verb(s) that are not GLFunctionsTable members: %s" % ", ".join(extra))
if verb_names != table_members:
sys.exit("FillPoints.def: verb rows are not in GLFunctionsTable declaration order "
"(first difference at %s)" % next(a for a, b in zip(verb_names, table_members) if a != b))
for cls in classes:
if not any(c == cls for _, c in verbs):
sys.exit("FillPoints.def: class %s has no verbs" % cls)
accessor_names = set(name for name, _ in accessors)
fields = {cls: [] for cls in classes}
for cls, field in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_FIELD_LIST")):
if cls not in fields:
sys.exit("FillPoints.def: field row names unknown class %s" % cls)
if field not in accessor_names:
sys.exit("FillPoints.def: %s is not an accessor in Coverage.def" % field)
if field in fields[cls]:
sys.exit("FillPoints.def: duplicate row (%s, %s)" % (cls, field))
fields[cls].append(field)
return verbs, classes, fields
def gen_fill_points(accessors, sticky, verbs, classes, fields):
field_index = {name: i for i, (name, _) in enumerate(accessors)}
# Two words minimum (the P1 contract shape, headroom for the 64th field); grows on demand.
words = max(2, (len(accessors) + 63) // 64)
sticky_names = [name for name, _ in sticky]
out = [banner("PipeFillPoints.inc", "G5b: the verb enum, the verb classes and their may-read field masks.",
"FillPoints.def, Coverage.def and MG_Backend/BackendObject.h")]
out.append("""// One verb per function-pointer member of MG_Backend::GLFunctionsTable, in declaration
// order, so the enum IS the table's member list. MG_Impl spells MGP_FILL(Verb) before every
// call through the table; MGPipeFillForVerb fills exactly the fields of the verb's class
// (plus the sticky fields, OR'ed into every mask) and stamps them with the new serial. A
// read of any other field is Fatal{UnmigratedPipeInput, \"Field@Verb\"} in a poison build.
""")
out.append("enum class MGPipeVerb : Uint8 {")
for verb, _ in verbs:
out.append(" %s," % verb)
out.append(" kVerbCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);")
out.append("static_assert(kMGPipeVerbCount == %d, \"the GLFunctionsTable verb set moved\");" % len(verbs))
out.append("")
out.append("inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {")
for verb, _ in verbs:
out.append(" \"%s\"," % verb)
out.append("};")
out.append("")
out.append("enum class MGPipeVerbClass : Uint8 {")
for cls in classes:
out.append(" %s," % cls)
out.append(" kClassCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);")
out.append("static_assert(kMGPipeVerbClassCount == %d, \"the verb class set moved\");" % len(classes))
out.append("")
out.append("inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {")
for cls in classes:
out.append(" \"%s\"," % cls)
out.append("};")
out.append("")
out.append("inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {")
for verb, cls in verbs:
out.append(" MGPipeVerbClass::%s, // %s" % (cls, verb))
out.append("};")
out.append("")
out.append("// One bit per MGPipeInputField. The %d sticky fields are OR'ed into every class." % len(sticky))
out.append("struct MGPipeFieldMask {")
out.append(" Uint64 Words[%d];" % words)
out.append("};")
out.append("")
out.append("inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {")
out.append(" const SizeT index = static_cast<SizeT>(field);")
out.append(" return (mask.Words[index / 64] >> (index % 64)) & 1u;")
out.append("}")
out.append("")
out.append("inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {")
for cls in classes:
bits = [0] * words
names = fields[cls] + [n for n in sticky_names if n not in fields[cls]]
for name in names:
index = field_index[name]
bits[index // 64] |= 1 << (index % 64)
out.append(" // %s: %d fields (%d own + %d sticky)" % (cls, len(names), len(fields[cls]), len(names) - len(fields[cls])))
out.append(" {{%s}}," % ", ".join("0x%016xull" % b for b in bits))
out.append("};")
out.append("")
out.append("static_assert(kMGPipeInputFieldCount <= %d * 64, \"MGPipeFieldMask needs another word\");" % words)
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
// (MG_Test/Pipe/RenderStateSpansTest.cpp).
//
// P2 replaced P0's provenance with a RULE, and the rule is the only thing that decides
// membership: a member is pipeline state IF AND ONLY IF some public RenderState setter that
// calls BumpVersions() writes it. That is what makes the G7 invariant true by construction
// rather than by inspection, and it turns the subset into a strict SUPERSET of the 24
// members VulkanRenderer::ComputePipelineStateHash used to hash.
//
// The three questions P0 left open are ANSWERED here, and the answers are in this list:
// - FramebufferSrgb, DepthClamp and TextureCubeMapSeamless had NO STORAGE at all -
// SetCapability fell to "not supported currently" and IsCapabilityEnabled answered a
// compile-time false. P2 gave all three real storage in the three padding bytes between
// ColorMasks and ClearColor, and their setters call BumpVersions(), so: pipeline state.
// - ProvokingVertexModeSetting: SetProvokingVertexMode calls BumpVersions(), so pipeline.
// - FrontFaceModeSetting likewise. ClipOrigin and ClipDepthMode do NOT (SetClipControl is
// ++m_version only), so they are dynamic, in chunk D1.
//
// 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("// Defined by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes every")
out.append("// boundary in C++ with offsetof rather than guessing it in python. 7 pipeline")
out.append("// chunks / 396 bytes and 8 dynamic chunks / 772 bytes, and the two halves")
out.append("// partition [0, sizeof(RenderStateParameters)) exactly - asserted there.")
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 expect_trip(name, because, fn, quiet=False):
"""Runs one negative control; a gate that lets it through, or trips for a reason that is
not ITS OWN, is the failure.
The first version of this function caught any SystemExit - including an unrelated
diagnostic, and even a CLEAN sys.exit(0) - and asked nothing about which one, so the
codex cross-family review's finding 9 could replace a control's callback with either and
the "nine negative controls all trip" line stayed true while the guard it named never
fired. gen_pipe_field_ownership.py's M-1 was the same defect in the sibling generator;
this mirrors that fix.
`because` is a substring the control's OWN message must contain, and an exit code of 0
can never count as a trip."""
try:
fn()
except SystemExit as trip:
code = trip.code
message = str(code) if code is not None else ""
if code != 0 and because in message:
if not quiet:
print("gen_pipe: self-test %s: tripped as expected (%s)"
% (name, message.splitlines()[0][:100]))
return 1
if not quiet:
print("gen_pipe: self-test %s: tripped for SOMEONE ELSE'S reason:\n"
" expected to contain: %s\n"
" actually said (code=%r): %s"
% (name, because, code, message.splitlines()[0][:200] if message else "<empty>"),
file=sys.stderr)
return 0
if not quiet:
print("gen_pipe: self-test %s: DID NOT TRIP" % name, file=sys.stderr)
return 0
def self_test(accessors):
"""The negative controls (check_include_closure.py's shape): each gate must go red for
its OWN reason, and zero trips is itself an error."""
canned_struct = "struct Canned {\n Uint32 A;\n Uint32 B, C;\n Uint8 Pad0[3];\n void F() { return; }\n};\n"
controls = [
("struct member without F(...)",
"member(s) with no F(...) in PipeFields.def",
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B"]}, ["Canned"], [canned_struct])),
("F(...) that is not a member",
"F(...) name(s) that are not members",
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B", "C", "D"]}, ["Canned"], [canned_struct])),
("payload with no struct",
"struct not found",
lambda: check_field_lists_cover_struct_members({"Nowhere": ["A"]}, ["Nowhere"], [canned_struct])),
]
fill_text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
verb_row = re.compile(r"X\(\s*DrawArrays\s*,\s*kDraw\s*\)")
field_row = re.compile(r"X\(\s*kDraw\s*,\s*GetBoundVertexArray\s*\)")
if not verb_row.search(fill_text) or not field_row.search(fill_text):
sys.exit("gen_pipe: self-test: FillPoints.def lost the rows the controls edit")
controls.append(("verb missing from FillPoints.def",
"GLFunctionsTable member(s) without a verb row",
lambda: parse_fill_points(
accessors, text=verb_row.sub("", fill_text, count=1))))
controls.append(("verb that is not a GLFunctionsTable member",
"verb(s) that are not GLFunctionsTable members",
lambda: parse_fill_points(
accessors, text=verb_row.sub("X(DrawArrays, kDraw) X(NotAVerb, kDraw)", fill_text, count=1))))
controls.append(("field row naming a non-accessor",
"is not an accessor in Coverage.def",
lambda: parse_fill_points(
accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1))))
# The EMITTED list's own gate: a row naming a call that is not in PipeCalls.def would
# generate an enumerator nothing can dispatch on.
calls_for_control = parse_calls()
controls.append(("emitted row naming a call that does not exist",
"which is not a call in PipeCalls.def",
lambda: gen_emitted_by(
[("GetViewport", "SetDynamicState")], calls_for_control, [("GetViewport", "NotACall")])))
# P5 R-13.4's gate. The flags are now a GENERATED TABLE six packages read instead of six
# hard-coded copies, so a token that is not an MGPipeCallFlags enumerator has to stop the
# generator rather than emit an expression that fails to compile minutes later - and
# kNone, the empty set, may not be OR'd with a real flag and quietly read as one.
flag_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kHasBlobb"])
flag_kNone = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone", "kHasBlob"])
flag_typo_because = "which is not an MGPipeCallFlags enumerator"
controls.append(("call flag that is not an MGPipeCallFlags enumerator",
flag_typo_because,
lambda: check_call_flags_are_known([flag_typo])))
controls.append(("kNone combined with a real flag",
"combines kNone with",
lambda: check_call_flags_are_known([flag_kNone])))
# P5e's two gates over the fifth column (CONTRACT-P5E.md §2.2). The column decides whether
# a live client waits after publishing, so a typo or a disagreement with the flag column is
# a wrong wait on the wire rather than a compile error in one consumer - which is exactly
# the shape R-13.4 found between the flag column and the payloads.
wait_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone"], "kWaitApplyed")
wait_slot_no_wait = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kReplySlot"], "kWaitNone")
wait_no_slot = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone"], "kWaitReply")
controls.append(("wait class that is not an MGPipeWaitClass enumerator",
"which is not an MGPipeWaitClass enumerator",
lambda: check_wait_classes_are_known([wait_typo])))
controls.append(("a kReplySlot row that does not wait for its reply",
"owns a reply slot but its wait class is",
lambda: check_wait_classes_are_known([wait_slot_no_wait])))
controls.append(("a kWaitReply row with no reply slot",
"but carries no kReplySlot",
lambda: check_wait_classes_are_known([wait_no_slot])))
# R-16 meta-control (codex review finding 9; verified by execution in
# p5-results/wave1-codex-verify.md §9). The exact perturbation there replaced the
# flag-typo control's callback with `sys.exit("unrelated parser failure")` and,
# separately, with `sys.exit(0)`; the OLD expect_trip counted both as "tripped as
# expected" because it never looked at the message or the code. Both must be REJECTED
# here, under the real control's own `because`. This is a quiet, unlisted assertion (not
# one of the nine controls below) so it cannot itself inflate the trip count.
# "Red once by doing X" = reverting expect_trip to `except SystemExit as trip: return 1`
# (accept any SystemExit) - that alone turns each of these two checks from a silent pass
# into the sys.exit below.
if expect_trip("(R-16 meta-control) unrelated diagnostic standing in for the flag-typo guard",
flag_typo_because, lambda: sys.exit("unrelated parser failure"), quiet=True) != 0:
sys.exit("gen_pipe: self-test: expect_trip counted an UNRELATED SystemExit as the "
"flag-typo guard's own trip - finding 9 / R-16 is back")
if expect_trip("(R-16 meta-control) sys.exit(0) standing in for the flag-typo guard",
flag_typo_because, lambda: sys.exit(0), quiet=True) != 0:
sys.exit("gen_pipe: self-test: expect_trip counted sys.exit(0) as a trip - "
"finding 9 / R-16 is back")
trips = 0
for name, because, fn in controls:
trips += expect_trip(name, because, fn)
# The positive control: the canned struct's exact list passes, and the parser sees the
# padding member as padding and the function as not a member.
check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct])
# ... and the real catalogue's real flags and wait classes pass the same gates.
check_call_flags_are_known(calls_for_control)
check_wait_classes_are_known(calls_for_control)
if trips == 0:
sys.exit("gen_pipe: self-test: no negative control tripped - the gates are not checking anything")
if trips != len(controls):
sys.exit("gen_pipe: self-test: %d of %d negative controls did not trip" % (len(controls) - trips, len(controls)))
print("gen_pipe: self-test: %d negative-control trip(s), positive control OK" % trips)
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()
calls = parse_calls()
payloads = parse_verify_payloads()
check_call_flags_are_known(calls)
check_wait_classes_are_known(calls)
check_call_payloads_have_field_lists(calls, payloads)
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
accessors, deltas, sticky, emitted = parse_coverage()
if args.self_test:
return self_test(accessors)
scan_live_accessors(accessors)
verbs, classes, fields = parse_fill_points(accessors)
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, "PipeWireLayout.inc"), gen_wire_layout(calls, payloads), 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, sticky, emitted),
args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFillPoints.inc"),
gen_fill_points(accessors, sticky, verbs, classes, fields), 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 "
"(%d sticky, %d emitted by a P2 call), %d verbs, %d classes"
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
len(emitted), len(verbs), len(classes)))
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())