mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
- MGPipeInputFieldIsFresh (gen_pipe.py gen_filled, emitted into PipeFilled.inc) compared FilledGen == CurrentVerbSerial for a non-sticky field; before the first MGPipeFillForVerb both are 0, so 55 of the 63 fields read as fresh and served their default-constructed storage silently, with no log line and no abort. Only the four raw-pointer O-class accessors tripped, through their null-base checks.
- D6 says the serial starts at 1 so that FilledGen == 0 means never filled, and names the window "<Field>@<none>"; the predicate now refuses a stamp of 0 before consulting the sticky branch or the serial, so the window is the poison's case as documented. This is the window E's risk table expects the verify lane to find (an init-time read, the first link, anything reached from eglMakeCurrent).
- Reproduced with the reviewer's pre-fill program (a live context with line width 7, no fill, gPipeInputs.GetLineWidth()): stored=0, rc=0 before; rc=134 with exactly Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"} after, with and without Features.PipeVerify set first.
- The compare-at-read hook arms at the first fill and cannot see this window either; a static_assert next to it pins that a verify build always carries the poison, which is what covers the reads before arming.
1192 lines
55 KiB
Python
1192 lines
55 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 MGB_CTX-> / 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, 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
|
|
|
|
|
|
# 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(?:MGB_CTX|pGLContext)\s*->\s*(\w+)")
|
|
|
|
|
|
def scan_live_accessors(accessors, backend_dir=None, verbose=True):
|
|
"""Every accessor a backend reads through MGB_CTX-> 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)
|
|
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))
|
|
|
|
|
|
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))
|
|
return accessors, deltas, sticky
|
|
|
|
|
|
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")
|
|
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_filled(accessors, calls, sticky):
|
|
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("""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. 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 expect_trip(name, fn):
|
|
"""Runs one negative control; a gate that lets it through is the failure."""
|
|
try:
|
|
fn()
|
|
except SystemExit as trip:
|
|
print("gen_pipe: self-test %s: tripped as expected (%s)" % (name, str(trip).splitlines()[0][:100]))
|
|
return 1
|
|
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 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(...)",
|
|
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B"]}, ["Canned"], [canned_struct])),
|
|
("F(...) that is not a member",
|
|
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B", "C", "D"]}, ["Canned"], [canned_struct])),
|
|
("payload with no struct",
|
|
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", lambda: parse_fill_points(
|
|
accessors, text=verb_row.sub("", fill_text, count=1))))
|
|
controls.append(("verb that is not a GLFunctionsTable member", 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", lambda: parse_fill_points(
|
|
accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1))))
|
|
trips = 0
|
|
for name, fn in controls:
|
|
trips += expect_trip(name, 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])
|
|
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_payloads_have_field_lists(calls, payloads)
|
|
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
|
|
accessors, deltas, sticky = 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, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky), 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 verbs, %d classes"
|
|
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
|
|
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())
|