mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
CONTRACT-P7 §5.1's (A) and (D), and (D) is what makes (A) safe to have.
(A) WireDepthMipmap.{vert,frag,inc}. VK_FORMAT_FEATURE_BLIT_DST is OPTIONAL for depth
formats, so a depth chain through a shader is the ordinary case and not the exotic
one - the monolith arm has carried such a fallback since before the split. The wire
arm had none, so a depth texture the device would not blit died on the first
disjunction of `mipmap-shader-format-or-shape`, and a DEPTH|STENCIL one died a guard
earlier as `depth-stencil-mipmap@P7` whether or not it could be blitted.
The monolith's fallback is not reusable, which is the whole of (A): it builds a GL
ProgramObject, two ShaderObjects and a GL sampler, binds through the uniform manager's
global UBO and writes uSrcTexelSize as a default-block uniform - four frontend objects
and a GL compiler on the server's apply thread. The baked program is the same shader
with the same arithmetic on a push constant and its own descriptor set. The fragment
stage is a PORT and not a rewrite: §3.2 retires a refusal by making the wire arm
observably the monolith's, so the 2x2 box lands where the monolith's lands, including
the half texel its ivec2(texCoord * srcTexelSize) sits off the destination centre.
`depth-stencil-mipmap@P7` therefore splits. DEPTH-only retires into the baked program.
DEPTH|STENCIL declines - MGLOG_E_ONCE and return, before the chain is grown, so a level
this verb will not fill is not created and then recorded as defined. A decline rather
than a Fatal because the monolith does no better: its fallback asserts depth-only, and
an assert is nothing in the release build both arms ship as. Nothing in GL promises a
stencil mip either, and this pass writes gl_FragDepth - it would leave the stencil
aspect of the level it just produced undefined.
(D) MOBILEGL_BAKED_INTERNAL_SHADERS. A baked header is a compiler output committed with
its input beside it and nothing connecting the two: edit the source and the build stays
green while the shader that runs is the one from before the edit. MG_Test's
BakedInternalShadersTest recompiles all eight arrays - WireColorBlit x2,
WireDepthMipmap x2, the iterationRP witness, the three probe stages - from their
committed GLSL with the in-tree glslang and compares word for word.
THE GATE IS THE BAKER. scripts/bake_internal_shaders.py compiles nothing; it runs this
binary with MOBILEGL_BAKE_INTERNAL_SHADERS and splices the words it emits. A gate that
used the glslang LIBRARY while the baker shelled out to a glslangValidator BINARY would
be two compilers with no rule about which is right, and the day their defaults diverge
the gate reds on a header nobody can regenerate.
What the first run measured, which is the one thing nobody could know beforehand:
WireColorBlitSpirv.h and all three arrays of PrimitivesGeneratedNoXfbProbeSpv.h were
already fresh - the hand-run glslangValidator words reproduce exactly. The iterationRP
witness was stale, as its own prose said; it is regenerated here (2142 -> 2143 words),
and the witness magic 0x50323033 is still there because it is a constant in the .comp
rather than a property of the words. All three provenance comments now point at the
script and the gate instead of at a tool on somebody's PATH.
Scenarios, on the three Magma arms: GenerateMipmapDepthWithoutNativeBlitPixels runs
DEPTH_COMPONENT24 and DEPTH_COMPONENT32F under MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1 and
reads level one back through a real sampled attachment, asserting per half and leaving
row 3 - the straddle band - unpinned, because pinning it would assert the half-texel
offset rather than the mip. GenerateMipmapDepthStencilDeclinesAndKeepsTheSession takes
no knob (the decline is on the aspect, before blit support is asked about) and pins that
the session survives and level zero still holds what was cleared into it.
Red once, three of them, executed and reverted:
- a one-character edit to WireDepthMipmap.frag (no rebuild - the gate reads the source
at runtime) reds the WireDepthMipmapFragment row at word 279;
- with the depth arm disabled, the DepthMip entries die with Fatal{UnmigratedVerb,
"Magma:mipmap-shader-format-or-shape format=125 ... aspect=0x2 ...
missing=COLOR_ATTACHMENT"} - format 125 is X8_D24_UNORM_PACK32, which slice 1's
diagnostic names for us;
- with the Fatal restored, the decline case dies as
Fatal{UnmigratedVerb, "Magma:depth-stencil-mipmap@P7"}.
Gates: integration-magma-split 77 (+2), -spawn 56 (+2), -tcp 58 (+2), 0 red;
ctest -L unit 2410 (+8, the gate's rows); informational integration-magma-full-split
513, 0 red, 59 skip (base 510/0/57); fatal_census green (79 abort sites, 43 words, 0
unmarked); link_ratchet --assert-monotone unchanged at 186, p7-magma 101.
G1 holds and it was not obvious: the witness header is compiled into the pull build
too, and its word count changed. Pull .text is still 0xa52203 and the defined-symbol
name list is identical to ~/w7/p7-before/pull-syms.txt line for line (30570) - the
change lands in .rodata and widens no instruction encoding.
VulkanRenderer.h is edited outside this package's file list: the new pass needs four
private declarations next to the colour blit's. Flagged for the integrator.
185 lines
7.7 KiB
Python
185 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
# MobileGL - scripts/bake_internal_shaders.py
|
|
# Copyright (c) 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
|
|
"""Regenerate the tree's baked SPIR-V headers from their committed GLSL.
|
|
|
|
python3 scripts/bake_internal_shaders.py --build-dir build-split
|
|
python3 scripts/bake_internal_shaders.py --build-dir build-split --check
|
|
|
|
THIS SCRIPT DOES NOT COMPILE ANYTHING. It runs MG_Test's BakedInternalShadersTest with
|
|
MOBILEGL_BAKE_INTERNAL_SHADERS set, takes the words that binary emits, and splices them into
|
|
the headers. The compiler is therefore the IN-TREE glslang the gate itself uses - one compiler
|
|
and one answer.
|
|
|
|
The alternative, which is what the four headers in the tree were made with before this script
|
|
existed, is a glslangValidator somebody happened to have on their PATH. That is how the
|
|
iterationRP witness ended up carrying words its own source no longer produces: nothing named
|
|
the compiler, nothing re-ran it, and the header's prose was the only record that it was stale.
|
|
A gate whose baker is a different program from the gate has the same failure one level up - the
|
|
day the CLI's defaults and the library's diverge, the gate reds on a header nobody can
|
|
regenerate, and the fix is to edit the words by hand.
|
|
|
|
FORMATTING IS PRESERVED PER HEADER, not normalised: the headers in the tree disagree about
|
|
words per line and about the `u` suffix, and reformatting one of them would bury a one-word
|
|
change in a whole-file diff. The style is read back out of the array being replaced.
|
|
|
|
--check exits non-zero and names the headers that would change, without writing. It is the
|
|
same verdict the ctest entry gives; this mode exists so the regeneration path can be checked
|
|
from a shell without reading gtest output.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
TEST_TARGET = "BakedInternalShadersTest"
|
|
BAKE_ENV = "MOBILEGL_BAKE_INTERNAL_SHADERS"
|
|
|
|
# ` 0x07230203u, 0x00010000u, ...` - the body of one baked array.
|
|
WORD_RE = re.compile(r"0x([0-9a-fA-F]{1,8})u?")
|
|
|
|
|
|
def find_test_binary(build_dir: Path) -> Path:
|
|
"""The test executable, wherever this generator put it."""
|
|
candidates = sorted(build_dir.rglob(TEST_TARGET))
|
|
candidates = [c for c in candidates if c.is_file() and os.access(c, os.X_OK)]
|
|
if not candidates:
|
|
raise SystemExit(
|
|
f"{TEST_TARGET} not found under {build_dir}. Build it first:\n"
|
|
f" cmake --build {build_dir} --target {TEST_TARGET}"
|
|
)
|
|
return candidates[0]
|
|
|
|
|
|
def emit_words(binary: Path, out_dir: Path) -> dict:
|
|
"""Run the gate in bake mode; return {symbol: (header, [words])}."""
|
|
env = dict(os.environ)
|
|
env[BAKE_ENV] = str(out_dir)
|
|
result = subprocess.run([str(binary)], env=env, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
sys.stderr.write(result.stdout)
|
|
sys.stderr.write(result.stderr)
|
|
raise SystemExit(f"{TEST_TARGET} failed in bake mode (rc={result.returncode})")
|
|
baked = {}
|
|
for path in sorted(out_dir.glob("*.words")):
|
|
lines = path.read_text().splitlines()
|
|
if not lines:
|
|
raise SystemExit(f"{path} is empty")
|
|
header = lines[0].strip()
|
|
words = [int(line, 16) for line in lines[1:] if line.strip()]
|
|
baked[path.stem] = (header, words)
|
|
if not baked:
|
|
raise SystemExit(f"{TEST_TARGET} emitted nothing into {out_dir}")
|
|
return baked
|
|
|
|
|
|
def locate_array(text: str, symbol: str):
|
|
"""(body_start, body_end, style) for `symbol`'s array body."""
|
|
opener = re.search(re.escape(symbol) + r"\s*\[\s*\]\s*=\s*\{", text)
|
|
if not opener:
|
|
raise SystemExit(f"cannot find the array `{symbol}` to replace")
|
|
body_start = opener.end()
|
|
closer = text.find("};", body_start)
|
|
if closer < 0:
|
|
raise SystemExit("the array `" + symbol + "` has no closing `};`")
|
|
body = text[body_start:closer]
|
|
rows = [row for row in body.split("\n") if WORD_RE.search(row)]
|
|
if not rows:
|
|
raise SystemExit(f"the array `{symbol}` holds no words to learn its style from")
|
|
style = {
|
|
"indent": re.match(r"[ \t]*", rows[0]).group(0),
|
|
# The LONGEST row, not the first: a one-row array and a ragged last row would both
|
|
# teach the wrong width.
|
|
"per_line": max(len(WORD_RE.findall(row)) for row in rows),
|
|
"suffix": "u" if re.search(r"0x[0-9a-fA-F]+u", body) else "",
|
|
# The whitespace the closing brace sits behind. It is part of the SPAN being
|
|
# replaced, so a body written without it silently outdents every array it
|
|
# touches - a diff on three headers that changed no word at all.
|
|
"close_indent": re.search(r"[ \t]*$", text[:closer]).group(0),
|
|
}
|
|
return body_start, closer, style
|
|
|
|
|
|
def format_body(words, style) -> str:
|
|
per_line = style["per_line"]
|
|
out = ["\n"]
|
|
for at in range(0, len(words), per_line):
|
|
chunk = words[at:at + per_line]
|
|
out.append(style["indent"]
|
|
+ ", ".join(f"0x{word:08x}{style['suffix']}" for word in chunk) + ",\n")
|
|
out.append(style["close_indent"])
|
|
return "".join(out)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--build-dir", default="build-split",
|
|
help="a configured build tree that has BakedInternalShadersTest")
|
|
parser.add_argument("--build", action="store_true",
|
|
help="build the test target first")
|
|
parser.add_argument("--check", action="store_true",
|
|
help="report headers that would change and exit non-zero; write nothing")
|
|
args = parser.parse_args()
|
|
|
|
build_dir = Path(args.build_dir)
|
|
if not build_dir.is_absolute():
|
|
build_dir = ROOT / build_dir
|
|
if args.build:
|
|
subprocess.run(["cmake", "--build", str(build_dir), "--target", TEST_TARGET],
|
|
check=True)
|
|
|
|
binary = find_test_binary(build_dir)
|
|
scratch = Path(tempfile.mkdtemp(prefix="mgl-bake-"))
|
|
try:
|
|
baked = emit_words(binary, scratch)
|
|
finally:
|
|
shutil.rmtree(scratch, ignore_errors=True)
|
|
|
|
# One read/write per HEADER, not per symbol: two arrays share
|
|
# WireColorBlitSpirv.h and three share PrimitivesGeneratedNoXfbProbeSpv.h, and
|
|
# rewriting the file once per array would splice into a stale copy.
|
|
by_header = {}
|
|
for symbol, (header, words) in baked.items():
|
|
by_header.setdefault(header, []).append((symbol, words))
|
|
|
|
changed = []
|
|
for header, entries in sorted(by_header.items()):
|
|
path = ROOT / header
|
|
if not path.exists():
|
|
raise SystemExit(f"{header} does not exist")
|
|
original = path.read_text()
|
|
text = original
|
|
for symbol, words in sorted(entries):
|
|
start, end, style = locate_array(text, symbol)
|
|
text = text[:start] + format_body(words, style) + text[end:]
|
|
if text == original:
|
|
print(f"bake: {header} is already fresh ({len(entries)} array(s))")
|
|
continue
|
|
changed.append(header)
|
|
if args.check:
|
|
print(f"bake: {header} WOULD CHANGE ({', '.join(s for s, _ in sorted(entries))})")
|
|
else:
|
|
path.write_text(text)
|
|
print(f"bake: {header} rewritten ({', '.join(s for s, _ in sorted(entries))})")
|
|
|
|
if args.check and changed:
|
|
print(f"bake: {len(changed)} header(s) are stale against their sources", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|