mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
- PipeFields.def gains MGP_FIELDS_RenderStateParameters (65 members), PixelStoreParameters (8), PerBufferBlendState (7), StencilFaceState (7), DynamicBackendParameters (85) and MGHostSpan (Ptr, Seg, Size, Offset), all appended to MGP_VERIFY_PAYLOAD_LIST: kMGPipeVerifiedPayloadCount 63 -> 69, and ResidualValueBlock / MGPPixelPackState / MGPCaps are now compared field by field all the way down.
- gen_verify: MEMCMP_FALLBACK_TYPES is empty and the generic MGPipeFieldEqual's last branch is static_assert(sizeof(T) == 0) - a struct without a field list is a compile error, not a padding false positive; Array<T, N> gets an element-wise overload, and a VecBase-derived vector (FloatVec4, IntVec4, BoolVec4...) is detected by a probe and compared bitwise over its data, because VecBase::operator== is IEEE == and a derived-to-base overload would lose resolution to the exact-match generic template.
- check_field_lists_cover_struct_members(): for every payload in MGP_VERIFY_PAYLOAD_LIST, parse `struct <Name> {` out of MGPipeTypes.h / MGPipeValueTypes.h / MGPipeHostSpan.h / BackendObject.h (comments and strings masked, statics, functions, nested types and Pad<n> members excluded) and refuse a member without an F(...) or an F(...) that is not a member; runs in both modes, so it is a pipe-gates gate.
- scan_live_accessors(): every MGB_CTX-> / pGLContext-> read under MG_Backend must have a Coverage.def row (rows nobody reads are printed: today only the dead GetBoundTransformFeedbackName).
- --self-test: six negative controls (struct member without F, F without member, payload without struct, verb missing from FillPoints.def, verb outside GLFunctionsTable, field row naming a non-accessor) that must each trip, plus a positive control; zero trips is itself an error.
1188 lines
54 KiB
Python
1188 lines
54 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();
|
|
}
|
|
|
|
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
|
|
const SizeT index = static_cast<SizeT>(field);
|
|
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
|
|
: state.FilledGen[index] == state.CurrentVerbSerial;
|
|
}""")
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
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())
|