mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
- FramebufferSrgb, DepthClamp and TextureCubeMapSeamless get real storage. All three fell
to SetCapability's "not supported currently" arm and IsCapabilityEnabled's default:
glEnable was swallowed and glIsEnabled lied, so DirectGLES' sRGB block and the
DirectVulkan read points consumed a constant. The three Bools land in the three
alignment bytes at [581, 584) between ColorMasks and ClearColor, so
sizeof(RenderStateParameters) stays 1168 and NO existing offset moves - Espryt's
kBlendSpanBegin/kBlendSpanEnd (312/536) and the whole chunk table depend on that.
- MGPipeRenderStateSpans.{h,cpp}: the pipeline/dynamic split, written in exactly one place.
The rule is the only rule - a byte is pipeline state iff a public RenderState setter that
calls BumpVersions() writes it - which makes G7's "the subset hash moves iff
m_pipelineStateVersion moves" true by construction. 16 boundaries, all offsetof or
sizeof, alternating dynamic/pipeline: 8 dynamic chunks / 772 bytes and 7 pipeline chunks
/ 396 bytes, partitioning [0, 1168) exactly, asserted at compile time.
MGPipeComputePipelineSubsetHash is XXH64 over the seven pipeline chunks, seeded with a
table version so a chunk-table change invalidates every persisted key.
- The pipeline subset is now a strict SUPERSET of the 24 members ComputePipelineStateHash
hashed: 44 members, adding sample coverage, the front face, the provoking vertex, the
scissor-test mask, the back polygon mode, eleven capability bools the hash never read and
the three above. Demoting those setters to ++m_version instead would have changed
MG_State semantics in the PULL build for the push path's sake. The hash runs only when
m_pipelineStateVersion moves, which is exactly when Magma re-hashed before.
- PipeApply.{h,cpp}: the in-process applier, the server half of the P2 calls. The server's
working RenderStateParameters IS PipeInputs::m_renderState, which is why DirectGLES'
SyncRenderState is not one line changed and why the verify comparator stops being a
tautology. Per-context CSO store indexed by slot, gen-validated; the residual block's
capability bits are compared against the assembled block, so a capability a later call
takes over and forgets to carry is Fatal{PipeResidualDiverged}.
MGPipeDeriveRenderStateFields is a declared STUB - its 29 derivations are commit c1.
- SlotAllocator.{h,cpp}: the client's per-kind {slot, gen} allocator, free list plus
high-water, first allocatable slot 1, gen bumping only on slot REUSE, a debug assert on
gen wrap, the composite ShaderCso band held back, and a lifetimeId -> slot map per kind so
a GL name never enters a key. In the contract because both Track H slices need it.
- ResidualValueBlock 1248 -> 8 bytes, one Uint64 of capability bits.
RenderStateParameters retired to create/bind_render_state and set_dynamic_state, Pack to
set_pixel_pack_state, the patch quintet to set_patch_state. gen_pipe.py now emits the
member-by-member offsetof assertions the ratchet comment always promised.
- gen_pipe.py: PIPELINE_STATE_MEMBERS grows to the 44-member set in declaration order and
PipeSpanTable.inc's "deliberately absent" block records the answers instead of the
questions; Coverage.def gains MGP_COVERAGE_EMITTED_LIST (34 rows) and PipeFilled.inc
gains kMGPipeFieldEmittedBy[], which is what lets the residual fill loop skip a field a
P2 call now supplies. One more --self-test negative control covers the new list.
- MOBILEGL_PIPE_PUSH becomes a per-subsystem bitmask with named bits (0..6 migrated at P2,
bit 63 the CSO-content-addressing negative control), defaulting to 0x7f in a push build
and staying 0 in a pull build. New CMake option MOBILEGL_PIPE_LEGACY_MEMOS, ON, forced ON
when MOBILEGL_PIPE_PUSH=OFF where it is the only arm. New Features.PipeHandleAbaControl
under MOBILEGL_PIPE_PUSH, negative control C for HandleRecycleScenario.
- PipeStats gains CallClass::{RenderStateCsoMints, RenderStateCsoBinds} (csom / csob on the
summary line), and they are PUSH-ONLY: growing the enum in the pull build would resize
the counter arrays, the name table and FormatWindowLine for two counters that could never
leave zero, and G1 admits no such resize.
- Four MG_Test/Pipe stubs plus their CMake registration, so the packages that own their
contents never touch MG_Test/Pipe/CMakeLists.txt.
G1, pull build, symbol_report --threshold 0: 0 added, 0 removed, 0 renamed, 4 resized, and
every resize is attributed:
RenderState::RenderState() 1700 -> 1848 (+148) the three {}
RenderState::SetCapability(CapabilityInput,bool) 850 -> 927 (+77) three switch arms
RenderState::IsCapabilityEnabled(CapabilityInput) 239 -> 268 (+29) three switch arms
_GLOBAL__sub_I_DirectGLES.cpp 1340 -> 1331 (-9) the static
initialiser of DirectGLES.cpp's `static RenderStateParameters
g_syncedRenderStateParameters` re-scheduling around the three new default-initialised
members. A shrink, and the only unforeseen entry; it is a direct consequence of the
struct gaining members and touches no interface.
1309 lines
61 KiB
Python
1309 lines
61 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, in DECLARATION order.
|
|
#
|
|
# P0 took this list from what VulkanRenderer::ComputePipelineStateHash hashed. P2 replaced
|
|
# that provenance with a RULE, and the rule is the only thing that decides membership:
|
|
#
|
|
# A member is in the pipeline subset if and only if some public RenderState setter that
|
|
# calls BumpVersions() writes it. Everything else is dynamic. There is no third set.
|
|
#
|
|
# That makes G7's invariant - the pipeline-subset hash moves IFF m_pipelineStateVersion
|
|
# moves - true by construction, and it makes the subset a strict SUPERSET of the 24 members
|
|
# the Vulkan hash read: it adds sample coverage, the front face, the provoking vertex, the
|
|
# scissor-test mask, the back polygon mode, the eleven capability bools the hash never read,
|
|
# and the three capabilities P2 gave storage to. The alternative - demoting those setters to
|
|
# ++m_version - would change MG_State semantics in the PULL build for the push path's sake.
|
|
#
|
|
# Names only: offsets are NOT computed here. The chunk table with real offsets is
|
|
# MGPipeRenderStateSpans.cpp, built in C++ with offsetof, because a python guess at the
|
|
# layout of a struct it cannot see is exactly the kind of drift the G7 setter-consistency
|
|
# test exists to catch (plan B section 4.5.2). StencilStates is named once and straddles:
|
|
# per face, Func and the three ops are pipeline, Ref/ValueMask/WriteMask are dynamic.
|
|
PIPELINE_STATE_MEMBERS = [
|
|
"PatchVertices",
|
|
"PatchDefaultOuterLevel",
|
|
"PatchDefaultInnerLevel",
|
|
"BlendStates",
|
|
"LogicOp",
|
|
"DepthTestEnabled",
|
|
"DepthFunc",
|
|
"DepthMask",
|
|
"ColorMasks",
|
|
"FramebufferSrgbEnabled",
|
|
"DepthClampEnabled",
|
|
"TextureCubeMapSeamlessEnabled",
|
|
"SampleCoverageValue",
|
|
"SampleCoverageInvert",
|
|
"SampleMaskValue",
|
|
"MinSampleShadingValue",
|
|
"StencilStates",
|
|
"CullFaceEnabled",
|
|
"CullFaceModeSetting",
|
|
"FrontFaceModeSetting",
|
|
"ProvokingVertexModeSetting",
|
|
"PolygonModeFront",
|
|
"PolygonModeBack",
|
|
"ColorLogicOpEnabled",
|
|
"DebugOutputEnabled",
|
|
"DebugOutputSynchronousEnabled",
|
|
"DitherEnabled",
|
|
"LineSmoothEnabled",
|
|
"MultisampleEnabled",
|
|
"PolygonOffsetFillEnabled",
|
|
"PolygonOffsetLineEnabled",
|
|
"PolygonOffsetPointEnabled",
|
|
"PolygonSmoothEnabled",
|
|
"PrimitiveRestartEnabled",
|
|
"PrimitiveRestartFixedIndexEnabled",
|
|
"RasterizerDiscardEnabled",
|
|
"SampleAlphaToCoverageEnabled",
|
|
"SampleAlphaToOneEnabled",
|
|
"SampleCoverageEnabled",
|
|
"SampleMaskEnabled",
|
|
"SampleShadingEnabled",
|
|
"StencilTestEnabled",
|
|
"ProgramPointSizeEnabled",
|
|
"ScissorTestEnabledMask",
|
|
]
|
|
|
|
|
|
def read(path):
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
return handle.read()
|
|
|
|
|
|
class Call(object):
|
|
def __init__(self, index, name, payload, cls, flags):
|
|
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))
|
|
emitted = []
|
|
block = re.search(r"#define MGP_COVERAGE_EMITTED_LIST\(X\)(.*?)\n\n", text, re.S)
|
|
if not block:
|
|
sys.exit("Coverage.def: MGP_COVERAGE_EMITTED_LIST is missing")
|
|
seen = set()
|
|
for name, call in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block.group(1)):
|
|
if name not in accessor_names:
|
|
sys.exit("Coverage.def: emitted field %s is not an accessor in "
|
|
"MGP_COVERAGE_ACCESSOR_LIST" % name)
|
|
if name in seen:
|
|
sys.exit("Coverage.def: emitted field %s is listed twice" % name)
|
|
seen.add(name)
|
|
emitted.append((name, call))
|
|
return accessors, deltas, sticky, emitted
|
|
|
|
|
|
INVENTORY_ROW_RE = re.compile(r"^\|\s*(\d+)\s*\|([^|]*)\|([^|]*)\|([^|]*)\|")
|
|
|
|
|
|
def parse_inventory():
|
|
if not os.path.exists(INVENTORY):
|
|
sys.exit("missing %s - copy it from MobileGL-CS/docs/CS_Refactor/" % INVENTORY)
|
|
rows = []
|
|
current_file = None
|
|
for line in read(INVENTORY).splitlines():
|
|
heading = re.match(r"^### `([^`]+)`", line)
|
|
if heading:
|
|
current_file = heading.group(1)
|
|
continue
|
|
match = INVENTORY_ROW_RE.match(line)
|
|
if match and current_file:
|
|
rows.append({
|
|
"file": current_file,
|
|
"line": int(match.group(1)),
|
|
"kind": match.group(2).strip(),
|
|
"member": match.group(3).strip(),
|
|
"delta": match.group(4).strip(),
|
|
})
|
|
return rows
|
|
|
|
|
|
def banner(name, title, sources):
|
|
return GENERATED_BANNER.format(name=name, title=title, sources=sources)
|
|
|
|
|
|
def gen_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, residual_fields=None):
|
|
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""")
|
|
# The migration carrier's layout, asserted MEMBER BY MEMBER and not only by sizeof
|
|
# (plan 6.3): a heterogeneous POD is where padding differs across ABIs, and the monolith
|
|
# verify harness is blind to it because both sides are the same translation unit. The
|
|
# first member is pinned at 0 and the rest are pinned to ascend, which is the strongest
|
|
# statement a generator that cannot see the layout can make; sizeof plus
|
|
# MGL_RESIDUAL_BLOCK_SIZE pins the rest, and the ratchet only ever goes DOWN.
|
|
if residual_fields:
|
|
out.append("")
|
|
out.append("// The ResidualValueBlock layout, from PipeFields.def's")
|
|
out.append("// MGP_FIELDS_ResidualValueBlock. Retiring a field without lowering")
|
|
out.append("// MGL_RESIDUAL_BLOCK_SIZE is a build break, which is the point.")
|
|
out.append("static_assert(offsetof(ResidualValueBlock, %s) == 0," % residual_fields[0])
|
|
out.append(" \"the residual block's first member must sit at offset 0\");")
|
|
for previous, member in zip(residual_fields, residual_fields[1:]):
|
|
out.append("static_assert(offsetof(ResidualValueBlock, %s) >" % member)
|
|
out.append(" offsetof(ResidualValueBlock, %s)," % previous)
|
|
out.append(" \"the residual block's members must stay in declaration order\");")
|
|
out.append("static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,")
|
|
out.append(" \"the residual ratchet only ever goes down\");")
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def gen_verify(payloads):
|
|
out = [banner("PipeVerify.inc", "G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.",
|
|
"PipeFields.def")]
|
|
out.append("""// Field by field, never memcmp over a whole payload: RenderStateParameters is documented
|
|
// in DirectGLES.cpp to false-DIFFER on padding under memcmp (harmlessly there, fatally
|
|
// here - a comparator with false positives is a comparator nobody reads). Each function
|
|
// reports the FIRST differing field by name, which with the draw serial is what the verify
|
|
// harness prints.
|
|
//
|
|
// Floating-point fields are compared by BITS, so a NaN patch level - which
|
|
// glPatchParameterfv accepts and ComputePipelineStateHash already hashes bitwise - equals
|
|
// itself instead of tripping every draw.
|
|
|
|
#include "../PipeFields.def"
|
|
""")
|
|
out.append("template <class T>")
|
|
out.append("struct MGPipeHasFieldVerifier : std::false_type {};\n")
|
|
out.append("""// A vector type (FloatVec4, IntVec4, BoolVec4...) is detected through its VecBase and
|
|
// compared BITWISE over its data: VecBase::operator== is IEEE ==, under which a NaN patch
|
|
// level would differ from itself. The probe rather than an overload because a
|
|
// derived-to-base conversion loses overload resolution to the exact-match generic template.
|
|
template <class Derived, class T, SizeT N>
|
|
std::true_type MGPipeVecBaseProbe(const VecBase<Derived, T, N>*);
|
|
std::false_type MGPipeVecBaseProbe(const void*);
|
|
template <class T>
|
|
inline constexpr Bool kMGPipeIsVecBase = decltype(MGPipeVecBaseProbe(static_cast<const T*>(nullptr)))::value;
|
|
|
|
template <class T>
|
|
inline Bool MGPipeFieldEqual(const T& a, const T& b);
|
|
template <class T, SizeT N>
|
|
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b);
|
|
template <class T, SizeT N>
|
|
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]);
|
|
""")
|
|
for payload in payloads:
|
|
out.append("inline Bool MGPipeVerify(const %s& a, const %s& b, const char** outField);"
|
|
% (payload, payload))
|
|
out.append("")
|
|
for payload in payloads:
|
|
out.append("template <>")
|
|
out.append("struct MGPipeHasFieldVerifier<%s> : std::true_type {};" % payload)
|
|
out.append("")
|
|
out.append("""template <class T>
|
|
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
|
|
if constexpr (MGPipeHasFieldVerifier<T>::value) {
|
|
const char* unusedField = nullptr;
|
|
return MGPipeVerify(a, b, &unusedField);
|
|
} else if constexpr (kMGPipeIsVecBase<T>) {
|
|
return std::memcmp(a.data.data(), b.data.data(), sizeof(a.data)) == 0;
|
|
} else if constexpr (std::is_floating_point_v<T>) {
|
|
return std::memcmp(&a, &b, sizeof(T)) == 0;
|
|
} else if constexpr (std::is_scalar_v<T> || std::is_enum_v<T>) {
|
|
return a == b;
|
|
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
|
|
return a == b;
|
|
} else {
|
|
// NO MEMCMP FALLBACK. Every value struct has a field list in PipeFields.def since P1
|
|
// (and gen_pipe.py asserts each list covers its struct's members); a type reaching
|
|
// this branch is one nobody gave a field list, and a memcmp would false-differ on
|
|
// its padding. A compile error is the honest answer.
|
|
static_assert(sizeof(T) == 0, "no field list in PipeFields.def for this type");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
template <class T, SizeT N>
|
|
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b) {
|
|
for (SizeT i = 0; i < N; ++i) {
|
|
if (!MGPipeFieldEqual(a[i], b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <class T, SizeT N>
|
|
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
|
|
for (SizeT i = 0; i < N; ++i) {
|
|
if (!MGPipeFieldEqual(a[i], b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#define MGP_VERIFY_FIELD(FieldName) \\
|
|
if (!MGPipeFieldEqual(a.FieldName, b.FieldName)) { \\
|
|
if (outField != nullptr) *outField = #FieldName; \\
|
|
return false; \\
|
|
}
|
|
""")
|
|
for payload in payloads:
|
|
out.append("inline Bool MGPipeVerify(const %s& a, const %s& b, const char** outField) {"
|
|
% (payload, payload))
|
|
out.append(" MGP_FIELDS_%s(MGP_VERIFY_FIELD)" % payload)
|
|
out.append(" return true;")
|
|
out.append("}")
|
|
out.append("")
|
|
out.append("#undef MGP_VERIFY_FIELD")
|
|
out.append("")
|
|
out.append("inline constexpr SizeT kMGPipeVerifiedPayloadCount = %d;" % len(payloads))
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def gen_emitted_by(accessors, calls, emitted):
|
|
"""P2 brief D5: which P2 call SUPPLIES each PipeInputs field, so the per-verb residual
|
|
fill loop can skip it. Emitters are the distinct calls named in MGP_COVERAGE_EMITTED_LIST,
|
|
sorted so the enum is stable against the order rows are written in."""
|
|
call_names = set(c.Name for c in calls)
|
|
emitted_map = dict(emitted)
|
|
for name, call in emitted:
|
|
if call not in call_names:
|
|
sys.exit("Coverage.def: MGP_COVERAGE_EMITTED_LIST names %s for %s, which is not a "
|
|
"call in PipeCalls.def" % (call, name))
|
|
emitters = sorted(set(call for _, call in emitted))
|
|
out = []
|
|
out.append("// P2 brief D5: the call that now SUPPLIES a field, so the residual fill loop no")
|
|
out.append("// longer pulls it out of GLContext. kNone means the field is still pulled - which")
|
|
out.append("// is what makes MOBILEGL_PIPE_PUSH a true per-subsystem A/B instead of a single")
|
|
out.append("// switch. Rows come from Coverage.def's MGP_COVERAGE_EMITTED_LIST.")
|
|
out.append("enum class MGPipeFieldEmitter : Uint8 {")
|
|
out.append(" kNone = 0,")
|
|
for emitter in emitters:
|
|
out.append(" %s," % emitter)
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr const char* kMGPipeFieldEmitterNames[] = {")
|
|
out.append(" \"kNone\",")
|
|
for emitter in emitters:
|
|
out.append(" \"%s\"," % emitter)
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount] = {")
|
|
for name, _ in accessors:
|
|
emitter = emitted_map.get(name)
|
|
if emitter is None:
|
|
out.append(" MGPipeFieldEmitter::kNone, // %s" % name)
|
|
else:
|
|
out.append(" MGPipeFieldEmitter::%s, // %s" % (emitter, name))
|
|
out.append("};")
|
|
out.append("inline constexpr SizeT kMGPipeEmittedFieldCount = %d;" % len(emitted))
|
|
out.append("")
|
|
return "\n".join(out)
|
|
|
|
|
|
def gen_filled(accessors, calls, sticky, emitted):
|
|
call_names = set(c.Name for c in calls)
|
|
sticky_map = dict(sticky)
|
|
out = [banner("PipeFilled.inc", "G5: PipeInputs field ids and the per-verb poison generations.",
|
|
"Coverage.def and PipeCalls.def")]
|
|
out.append("""// One field id per GLContext accessor the backends actually read (plan B section 6.2:
|
|
// PipeInputs is organized by MEMO KEY, not by read point, which is why the field set is
|
|
// small and stable across the whole migration).
|
|
//
|
|
// The poison is a per-verb GENERATION, not a bit. A bitmap cannot see the dangerous case:
|
|
// a field filled by the previous DRAW and then read by the glTexSubImage that follows is
|
|
// stale, and its bit is already set. So every verb bumps CurrentVerbSerial, filling a
|
|
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
|
|
// Fatal{UnmigratedPipeInput} (section 6.2.2).
|
|
//
|
|
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
|
|
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
|
|
""")
|
|
out.append("enum class MGPipeInputField : Uint16 {")
|
|
for name, _ in accessors:
|
|
out.append(" %s," % name)
|
|
out.append(" kFieldCount,")
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);")
|
|
out.append("static_assert(kMGPipeInputFieldCount == %d, \"the PipeInputs field set moved\");" % len(accessors))
|
|
out.append("")
|
|
out.append("inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {")
|
|
for name, _ in accessors:
|
|
out.append(" \"%s\"," % name)
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison")
|
|
out.append("// cannot protect, so every true is argued for in Coverage.def's")
|
|
out.append("// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).")
|
|
out.append("inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {")
|
|
for name, _ in accessors:
|
|
if name in sticky_map:
|
|
out.append(" true, // %s: %s" % (name, sticky_map[name]))
|
|
else:
|
|
out.append(" false, // %s" % name)
|
|
out.append("};")
|
|
out.append("inline constexpr SizeT kMGPipeInputStickyFieldCount = %d;" % len(sticky))
|
|
out.append("")
|
|
out.append("// Which call is expected to have filled a field by the time a verb reads it. Names")
|
|
out.append("// come from Coverage.def, so this table and the coverage table cannot disagree.")
|
|
out.append("inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] = {")
|
|
for name, call in accessors:
|
|
marker = "" if call in call_names else " // pseudo-call: not filled by a forward record"
|
|
out.append(" \"%s\",%s" % (call, marker))
|
|
out.append("};")
|
|
out.append("")
|
|
out.append(gen_emitted_by(accessors, calls, emitted))
|
|
out.append("""struct MGPipeFilledState {
|
|
Uint64 CurrentVerbSerial;
|
|
Uint64 FilledGen[kMGPipeInputFieldCount];
|
|
};
|
|
|
|
[[noreturn]] inline void MGPipeInputPoisonFatal(MGPipeInputField field, const char* verb) {
|
|
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \\"%s@%s\\"}",
|
|
kMGPipeInputFieldNames[static_cast<SizeT>(field)], verb);
|
|
std::abort();
|
|
}
|
|
|
|
// FilledGen == 0 is "never filled" on BOTH branches: before the first MGPipeFillForVerb the
|
|
// serial is 0 as well, and a read in that window is the poison's "<Field>@<none>" case
|
|
// (P1 brief D6), never a fresh read of default-constructed storage.
|
|
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
|
|
const SizeT index = static_cast<SizeT>(field);
|
|
const Uint64 gen = state.FilledGen[index];
|
|
if (gen == 0) return false;
|
|
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
|
|
}""")
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
FUNCTION_POINTER_MEMBER_RE = re.compile(r"\(\s*\*\s*(\w+)\s*\)\s*\(")
|
|
|
|
|
|
def parse_function_table():
|
|
"""The function-pointer members of MG_Backend::GLFunctionsTable, in declaration order.
|
|
Data members (PrefersCpuXfbPrimitiveAccounting) are not verbs and are skipped; comments
|
|
are masked line by line."""
|
|
text = read(FUNCTION_TABLE_HEADER)
|
|
start = text.find("struct GLFunctionsTable {")
|
|
if start < 0:
|
|
sys.exit("%s: struct GLFunctionsTable is missing" % FUNCTION_TABLE_HEADER)
|
|
members = []
|
|
for line in text[start:].splitlines()[1:]:
|
|
if re.match(r"^\s*};", line):
|
|
break
|
|
code = line.split("//", 1)[0]
|
|
for name in FUNCTION_POINTER_MEMBER_RE.findall(code):
|
|
members.append(name)
|
|
if not members:
|
|
sys.exit("%s: GLFunctionsTable has no function-pointer members" % FUNCTION_TABLE_HEADER)
|
|
return members
|
|
|
|
|
|
def parse_fill_points(accessors, table_members=None, text=None):
|
|
"""FillPoints.def -> (verbs, classes, fields): verbs is [(verb, class)] in file order,
|
|
classes is [class], fields is {class: [field]}. Refuses a verb set that is not exactly
|
|
GLFunctionsTable's function-pointer members in declaration order, a verb in two
|
|
classes, a class with no verbs, a field that is not an accessor, a duplicate
|
|
(class, field) row, and a class row that names no class."""
|
|
if text is None:
|
|
text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
|
|
if table_members is None:
|
|
table_members = parse_function_table()
|
|
|
|
def block(macro):
|
|
match = re.search(r"#define %s\(X\)(.*?)\n\n" % macro, text, re.S)
|
|
if not match:
|
|
sys.exit("FillPoints.def: %s is missing (or not followed by a blank line)" % macro)
|
|
return match.group(1)
|
|
|
|
classes = re.findall(r"X\((\w+)\)", block("MGP_FILL_CLASS_LIST"))
|
|
if len(classes) != len(set(classes)):
|
|
sys.exit("FillPoints.def: a class is listed twice in MGP_FILL_CLASS_LIST")
|
|
verbs = re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_VERB_LIST"))
|
|
seen = set()
|
|
for verb, cls in verbs:
|
|
if verb in seen:
|
|
sys.exit("FillPoints.def: verb %s is in two classes" % verb)
|
|
seen.add(verb)
|
|
if cls not in classes:
|
|
sys.exit("FillPoints.def: verb %s names unknown class %s" % (verb, cls))
|
|
verb_names = [verb for verb, _ in verbs]
|
|
missing = [m for m in table_members if m not in seen]
|
|
if missing:
|
|
sys.exit("FillPoints.def: GLFunctionsTable member(s) without a verb row: %s" % ", ".join(missing))
|
|
extra = [v for v in verb_names if v not in table_members]
|
|
if extra:
|
|
sys.exit("FillPoints.def: verb(s) that are not GLFunctionsTable members: %s" % ", ".join(extra))
|
|
if verb_names != table_members:
|
|
sys.exit("FillPoints.def: verb rows are not in GLFunctionsTable declaration order "
|
|
"(first difference at %s)" % next(a for a, b in zip(verb_names, table_members) if a != b))
|
|
for cls in classes:
|
|
if not any(c == cls for _, c in verbs):
|
|
sys.exit("FillPoints.def: class %s has no verbs" % cls)
|
|
accessor_names = set(name for name, _ in accessors)
|
|
fields = {cls: [] for cls in classes}
|
|
for cls, field in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_FIELD_LIST")):
|
|
if cls not in fields:
|
|
sys.exit("FillPoints.def: field row names unknown class %s" % cls)
|
|
if field not in accessor_names:
|
|
sys.exit("FillPoints.def: %s is not an accessor in Coverage.def" % field)
|
|
if field in fields[cls]:
|
|
sys.exit("FillPoints.def: duplicate row (%s, %s)" % (cls, field))
|
|
fields[cls].append(field)
|
|
return verbs, classes, fields
|
|
|
|
|
|
def gen_fill_points(accessors, sticky, verbs, classes, fields):
|
|
field_index = {name: i for i, (name, _) in enumerate(accessors)}
|
|
# Two words minimum (the P1 contract shape, headroom for the 64th field); grows on demand.
|
|
words = max(2, (len(accessors) + 63) // 64)
|
|
sticky_names = [name for name, _ in sticky]
|
|
out = [banner("PipeFillPoints.inc", "G5b: the verb enum, the verb classes and their may-read field masks.",
|
|
"FillPoints.def, Coverage.def and MG_Backend/BackendObject.h")]
|
|
out.append("""// One verb per function-pointer member of MG_Backend::GLFunctionsTable, in declaration
|
|
// order, so the enum IS the table's member list. MG_Impl spells MGP_FILL(Verb) before every
|
|
// call through the table; MGPipeFillForVerb fills exactly the fields of the verb's class
|
|
// (plus the sticky fields, OR'ed into every mask) and stamps them with the new serial. A
|
|
// read of any other field is Fatal{UnmigratedPipeInput, \"Field@Verb\"} in a poison build.
|
|
""")
|
|
out.append("enum class MGPipeVerb : Uint8 {")
|
|
for verb, _ in verbs:
|
|
out.append(" %s," % verb)
|
|
out.append(" kVerbCount,")
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);")
|
|
out.append("static_assert(kMGPipeVerbCount == %d, \"the GLFunctionsTable verb set moved\");" % len(verbs))
|
|
out.append("")
|
|
out.append("inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {")
|
|
for verb, _ in verbs:
|
|
out.append(" \"%s\"," % verb)
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("enum class MGPipeVerbClass : Uint8 {")
|
|
for cls in classes:
|
|
out.append(" %s," % cls)
|
|
out.append(" kClassCount,")
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);")
|
|
out.append("static_assert(kMGPipeVerbClassCount == %d, \"the verb class set moved\");" % len(classes))
|
|
out.append("")
|
|
out.append("inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {")
|
|
for cls in classes:
|
|
out.append(" \"%s\"," % cls)
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {")
|
|
for verb, cls in verbs:
|
|
out.append(" MGPipeVerbClass::%s, // %s" % (cls, verb))
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("// One bit per MGPipeInputField. The %d sticky fields are OR'ed into every class." % len(sticky))
|
|
out.append("struct MGPipeFieldMask {")
|
|
out.append(" Uint64 Words[%d];" % words)
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {")
|
|
out.append(" const SizeT index = static_cast<SizeT>(field);")
|
|
out.append(" return (mask.Words[index / 64] >> (index % 64)) & 1u;")
|
|
out.append("}")
|
|
out.append("")
|
|
out.append("inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {")
|
|
for cls in classes:
|
|
bits = [0] * words
|
|
names = fields[cls] + [n for n in sticky_names if n not in fields[cls]]
|
|
for name in names:
|
|
index = field_index[name]
|
|
bits[index // 64] |= 1 << (index % 64)
|
|
out.append(" // %s: %d fields (%d own + %d sticky)" % (cls, len(names), len(fields[cls]), len(names) - len(fields[cls])))
|
|
out.append(" {{%s}}," % ", ".join("0x%016xull" % b for b in bits))
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("static_assert(kMGPipeInputFieldCount <= %d * 64, \"MGPipeFieldMask needs another word\");" % words)
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def gen_coverage(accessors, deltas, rows, calls):
|
|
call_names = set(c.Name for c in calls)
|
|
pseudo = {"kClientResolved", "kReverseChannel", "kStructuralHandle"}
|
|
accessor_map = dict(accessors)
|
|
delta_map = dict(deltas)
|
|
for _, call in accessors + deltas:
|
|
if call not in call_names and call not in pseudo:
|
|
sys.exit("Coverage.def: %s is not a call in PipeCalls.def and not a pseudo-call" % call)
|
|
|
|
mapped = 0
|
|
by_pseudo = {name: 0 for name in pseudo}
|
|
unmapped_rows = []
|
|
per_accessor = {}
|
|
for row in rows:
|
|
call = None
|
|
if row["member"] and row["member"] != "-":
|
|
call = accessor_map.get(row["member"])
|
|
if call is None:
|
|
call = delta_map.get(row["delta"])
|
|
if call is None:
|
|
unmapped_rows.append(row)
|
|
continue
|
|
if call in pseudo:
|
|
by_pseudo[call] += 1
|
|
else:
|
|
mapped += 1
|
|
key = row["member"] if row["member"] and row["member"] != "-" else row["delta"]
|
|
per_accessor.setdefault(key, [call, 0])[1] += 1
|
|
|
|
out = [banner("PipeCoverage.inc", "G6: backend read inventory -> MGPipe call coverage.",
|
|
"Coverage.def and scripts/data/backend_read_inventory.md")]
|
|
out.append("""// The acceptance rule (plan B section 10.3-5): regenerate, `git diff --exit-code`, and
|
|
// ZERO unmapped rows. P0 permits unmapped rows and only counts them; the count below is
|
|
// the number the later gate has to drive to zero.
|
|
//
|
|
// Three pseudo-calls stand for read points that never become a forward record:
|
|
// kClientResolved (the frontend answers it), kReverseChannel (it becomes one of the ten
|
|
// MGPipeCallbacks) and kStructuralHandle (the row is a signature carrying a
|
|
// SharedPtr<MG_State...> that becomes an MGPipeHandle parameter).
|
|
""")
|
|
out.append("struct MGPipeCoverageEntry {")
|
|
out.append(" const char* Accessor;")
|
|
out.append(" const char* Call;")
|
|
out.append(" Uint32 ReadPoints;")
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr MGPipeCoverageEntry kMGPipeCoverage[] = {")
|
|
for key in sorted(per_accessor):
|
|
call, count = per_accessor[key]
|
|
out.append(" {\"%s\", \"%s\", %d}," % (key, call, count))
|
|
out.append("};")
|
|
out.append("")
|
|
out.append("inline constexpr SizeT kMGPipeCoverageEntryCount = %d;" % len(per_accessor))
|
|
out.append("inline constexpr Uint32 kMGPipeInventoryReadPoints = %d;" % len(rows))
|
|
out.append("inline constexpr Uint32 kMGPipeInventoryMappedToCall = %d;" % mapped)
|
|
out.append("inline constexpr Uint32 kMGPipeInventoryClientResolved = %d;" % by_pseudo["kClientResolved"])
|
|
out.append("inline constexpr Uint32 kMGPipeInventoryReverseChannel = %d;" % by_pseudo["kReverseChannel"])
|
|
out.append("inline constexpr Uint32 kMGPipeInventoryStructuralHandle = %d;"
|
|
% by_pseudo["kStructuralHandle"])
|
|
out.append("inline constexpr Uint32 kMGPipeInventoryUnmapped = %d;" % len(unmapped_rows))
|
|
out.append("static_assert(kMGPipeCoverageEntryCount == sizeof(kMGPipeCoverage) / sizeof(kMGPipeCoverage[0]));")
|
|
out.append("static_assert(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +")
|
|
out.append(" kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +")
|
|
out.append(" kMGPipeInventoryUnmapped ==")
|
|
out.append(" kMGPipeInventoryReadPoints,")
|
|
out.append(" \"every inventory row must land in exactly one bucket\");")
|
|
return "\n".join(out) + "\n", unmapped_rows, mapped, by_pseudo
|
|
|
|
|
|
def gen_span_table():
|
|
out = [banner("PipeSpanTable.inc", "G7: the render-state pipeline subset, by member name.",
|
|
"the field list in scripts/gen_pipe.py")]
|
|
out.append("""// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
|
|
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
|
|
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves
|
|
// (MG_Test/Pipe/RenderStateSpansTest.cpp).
|
|
//
|
|
// P2 replaced P0's provenance with a RULE, and the rule is the only thing that decides
|
|
// membership: a member is pipeline state IF AND ONLY IF some public RenderState setter that
|
|
// calls BumpVersions() writes it. That is what makes the G7 invariant true by construction
|
|
// rather than by inspection, and it turns the subset into a strict SUPERSET of the 24
|
|
// members VulkanRenderer::ComputePipelineStateHash used to hash.
|
|
//
|
|
// The three questions P0 left open are ANSWERED here, and the answers are in this list:
|
|
// - FramebufferSrgb, DepthClamp and TextureCubeMapSeamless had NO STORAGE at all -
|
|
// SetCapability fell to "not supported currently" and IsCapabilityEnabled answered a
|
|
// compile-time false. P2 gave all three real storage in the three padding bytes between
|
|
// ColorMasks and ClearColor, and their setters call BumpVersions(), so: pipeline state.
|
|
// - ProvokingVertexModeSetting: SetProvokingVertexMode calls BumpVersions(), so pipeline.
|
|
// - FrontFaceModeSetting likewise. ClipOrigin and ClipDepthMode do NOT (SetClipControl is
|
|
// ++m_version only), so they are dynamic, in chunk D1.
|
|
//
|
|
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
|
|
// glViewport must not mint a new CSO.
|
|
|
|
inline constexpr const char* const kMGPipePipelineStateMembers[] = {""")
|
|
for member in PIPELINE_STATE_MEMBERS:
|
|
out.append(" \"%s\"," % member)
|
|
out.append("};")
|
|
out.append("inline constexpr SizeT kMGPipePipelineStateMemberCount = %d;" % len(PIPELINE_STATE_MEMBERS))
|
|
out.append("static_assert(kMGPipePipelineStateMemberCount ==")
|
|
out.append(" sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));")
|
|
out.append("")
|
|
out.append("// Defined by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes every")
|
|
out.append("// boundary in C++ with offsetof rather than guessing it in python. 7 pipeline")
|
|
out.append("// chunks / 396 bytes and 8 dynamic chunks / 772 bytes, and the two halves")
|
|
out.append("// partition [0, sizeof(RenderStateParameters)) exactly - asserted there.")
|
|
out.append("extern const MGPStateChunk kMGPipePipelineChunks[];")
|
|
out.append("extern const MGPStateChunk kMGPipeDynamicChunks[];")
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def write(path, text, check, changed):
|
|
existing = read(path) if os.path.exists(path) else None
|
|
if existing == text:
|
|
return
|
|
changed.append(os.path.relpath(path, REPO_ROOT))
|
|
if not check:
|
|
with open(path, "w", encoding="utf-8", newline="\n") as handle:
|
|
handle.write(text)
|
|
|
|
|
|
def expect_trip(name, 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))))
|
|
# The EMITTED list's own gate: a row naming a call that is not in PipeCalls.def would
|
|
# generate an enumerator nothing can dispatch on.
|
|
calls_for_control = parse_calls()
|
|
controls.append(("emitted row naming a call that does not exist", lambda: gen_emitted_by(
|
|
[("GetViewport", "SetDynamicState")], calls_for_control, [("GetViewport", "NotACall")])))
|
|
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, emitted = parse_coverage()
|
|
if args.self_test:
|
|
return self_test(accessors)
|
|
scan_live_accessors(accessors)
|
|
verbs, classes, fields = parse_fill_points(accessors)
|
|
rows = parse_inventory()
|
|
|
|
if not os.path.isdir(GENERATED_DIR):
|
|
os.makedirs(GENERATED_DIR)
|
|
|
|
coverage_text, unmapped, mapped, pseudo = gen_coverage(accessors, deltas, rows, calls)
|
|
changed = []
|
|
write(os.path.join(GENERATED_DIR, "PipeTables.inc"), gen_tables(calls), args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeWire.inc"),
|
|
gen_wire(calls, parse_field_lists().get("ResidualValueBlock")), args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky, emitted),
|
|
args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeFillPoints.inc"),
|
|
gen_fill_points(accessors, sticky, verbs, classes, fields), args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeCoverage.inc"), coverage_text, args.check, changed)
|
|
write(os.path.join(GENERATED_DIR, "PipeSpanTable.inc"), gen_span_table(), args.check, changed)
|
|
|
|
screen = sum(1 for c in calls if c.IsScreen)
|
|
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields "
|
|
"(%d sticky, %d emitted by a P2 call), %d verbs, %d classes"
|
|
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
|
|
len(emitted), len(verbs), len(classes)))
|
|
print("gen_pipe: inventory %d rows: %d -> call, %d client-resolved, %d reverse-channel, "
|
|
"%d structural handle, %d UNMAPPED"
|
|
% (len(rows), mapped, pseudo["kClientResolved"], pseudo["kReverseChannel"],
|
|
pseudo["kStructuralHandle"], len(unmapped)))
|
|
for row in unmapped:
|
|
print("gen_pipe: UNMAPPED %s:%d %s %s" % (row["file"], row["line"], row["kind"], row["member"]))
|
|
|
|
if changed:
|
|
if args.check:
|
|
print("gen_pipe: OUT OF DATE: %s" % ", ".join(changed), file=sys.stderr)
|
|
return 1
|
|
print("gen_pipe: wrote %s" % ", ".join(changed))
|
|
else:
|
|
print("gen_pipe: generated files are up to date")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|