wip(assembler): round L30 -- FI floor ring defect fixed; transition cost recovered

The Gradient interface left native solid density in the air-side gap and snapped the face to
voxel levels (concentric ring steps on the FI floor). The owner Gradient path now writes signed
distance both sides; match error RMS 0.36 -> 0.11 m. Band and fade-tile culling cut transition
cost: whole view vs L24 SW +2.7%, FI +0.2%. Evaluator 16. Deterministic across compilers and
workers; interface and passage audits pass. Found: on the real FloatingIslands recipe, the
rock-column connected-ends audit fails (all styles); earlier matrices ran SurfaceWorld twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-21 11:40:27 +02:00
1 parent 63c7e563d0
commit 103d8b471e
15 files changed
+809 -8

No files matched your search

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Rasterize upward mesh triangles into a top-down field height map."""
import argparse
import array
import json
import math
import struct
import sys
Q16 = 1 << 16
MASK64 = (1 << 64) - 1
def trunc_div(numerator, denominator):
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
def transition_hash(seed, interface, x, y, z, salt):
value = 14695981039346656037
for word in (seed & 0xffffffff, interface & 0xffffffff,
x & MASK64, y & MASK64, z & MASK64, salt):
for byte in range(8):
value ^= (word >> (byte * 8)) & 0xff
value = (value * 1099511628211) & MASK64
value ^= value >> 30
value = (value * 0xbf58476d1ce4e5b9) & MASK64
value ^= value >> 27
value = (value * 0x94d049bb133111eb) & MASK64
value ^= value >> 31
return ((value >> 32) >> 15) - Q16
def transition_fade(value):
value = max(0, min(Q16, value))
square = value * value // Q16
return square * (3 * Q16 - 2 * value) // Q16
def gradient_height_um(layout, x_um, y_um):
upper = next(item for item in layout["strates"] if item["order"] == 0)
lower = next(item for item in layout["strates"] if item["order"] == 1)
width = upper["transition_width_um"]
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
ix = x_um // cell
iy = y_um // cell
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
def at(dx, dy):
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
0x47524144)
return noise
def lerp(a, b, t):
return a + trunc_div((b - a) * t, Q16)
low = lerp(lerp(at(0, 0), at(1, 0), tx),
lerp(at(0, 1), at(1, 1), tx), ty)
displacement = trunc_div(low * (width // 8), Q16)
seam = upper["bedrock"]["bottom_um"]
thickness = upper["bedrock"]["top_um"] - seam
return seam + displacement + thickness
def read_scene(path):
triangles = []
with open(path, "rb") as stream:
if stream.read(8) != b"VFMSH001":
raise SystemExit(f"{path}: invalid scene header")
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
stream.read(40)
if version != 1 or count > 100000:
raise SystemExit("unsupported scene")
for _ in range(count):
key = struct.unpack("<4i", stream.read(16))
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
"<5I", stream.read(20))
positions = array.array("f")
positions.frombytes(stream.read(vertex_count * 12))
normals = array.array("f")
normals.frombytes(stream.read(vertex_count * 12))
indices = array.array("I")
indices.frombytes(stream.read(index_count * 4))
masks = stream.read(mask_count)
if sys.byteorder != "little":
positions.byteswap()
normals.byteswap()
indices.byteswap()
for index in range(0, index_count, 3):
if masks and masks[index // 3]:
continue
a, b, c = indices[index:index + 3]
if max(a, b, c) >= surface_vertices:
continue
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
+ normals[c * 3 + 2]) / 3.0
if nz < 0.35:
continue
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
positions[v * 3 + 2]) for v in (a, b, c))
triangles.append(vertices)
return voxel_um, triangles
def rasterize(triangles, bounds, width, height):
x0, x1, y0, y1 = bounds
values = [None] * (width * height)
sx, sy = width / (x1 - x0), height / (y1 - y0)
for tri in triangles:
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
if abs(den) < 1.0e-12:
continue
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
for py in range(top, bottom + 1):
y = y1 - (py + 0.5) / sy
row = py * width
for px in range(left, right + 1):
x = x0 + (px + 0.5) / sx
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
w = 1.0 - u - v
if min(u, v, w) < -1.0e-6:
continue
z = u * az + v * bz + w * cz
idx = row + px
if values[idx] is None or z > values[idx]:
values[idx] = z
return values
def main():
parser = argparse.ArgumentParser()
parser.add_argument("scene")
parser.add_argument("output_ppm")
parser.add_argument("--bounds", nargs=4, type=float, required=True,
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
metavar=("ZMIN", "ZMAX"))
parser.add_argument("--ring-center", nargs=2, type=float,
metavar=("X", "Y"))
parser.add_argument("--layout")
args = parser.parse_args()
voxel_um, triangles = read_scene(args.scene)
width, height = args.size
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
values = rasterize(triangles, args.bounds, width, height)
finite = sorted(value for value in values if value is not None)
if not finite:
raise SystemExit("no upward-facing field samples in bounds")
low, high = args.range
if high <= low:
raise SystemExit("height range must increase")
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
(1.00, (181, 71, 49)))
with open(args.output_ppm, "wb") as output:
output.write(f"P6\n{width} {height}\n255\n".encode())
for value in values:
if value is None:
output.write(bytes((18, 22, 30)))
continue
t = max(0.0, min(1.0, (value - low) / (high - low)))
for index in range(len(stops) - 1):
if t <= stops[index + 1][0]:
ta, ca = stops[index]
tb, cb = stops[index + 1]
blend = (t - ta) / (tb - ta)
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
for c in range(3))
break
else:
rgb = stops[-1][1]
output.write(bytes(rgb))
def quantile(p):
return finite[round((len(finite) - 1) * p)]
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
f"height_range_m={finite[-1]-finite[0]:.6f} "
f"map={args.output_ppm}")
if args.ring_center:
cx, cy = args.ring_center
def at_world(x, y):
fx = (x - args.bounds[0]) * sx - 0.5
fy = (args.bounds[3] - y) * sy - 0.5
ix, iy = math.floor(fx), math.floor(fy)
tx, ty = fx - ix, fy - iy
samples = []
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
if not (0 <= px < width and 0 <= py < height):
return None
value = values[py * width + px]
if value is None:
return None
samples.append(value)
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
"radius_m mean_m angular_sd_m min_m max_m samples")
for radius in range(1, 17):
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
cy + radius * math.sin(a * math.tau / 32))
for a in range(32)]
row = [value for value in row if value is not None]
if row:
mean = sum(row) / len(row)
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
f"{max(row):.6f} {len(row)}")
if args.layout:
layout = json.load(open(args.layout, encoding="utf-8"))
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
raise SystemExit("--layout must describe a Gradient owner interface")
errors = []
for index, actual in enumerate(values):
if actual is None:
continue
px, py = index % width, index // width
x = args.bounds[0] + (px + 0.5) / sx
y = args.bounds[3] - (py + 0.5) / sy
expected = gradient_height_um(layout, round(x * 1_000_000),
round(y * 1_000_000)) / 1_000_000.0
errors.append(actual - expected)
absolute = sorted(abs(error) for error in errors)
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
f"max_abs_m={absolute[-1]:.6f} "
"expected=owner-resolved-2d-hash-value-noise")
if __name__ == "__main__":
main()
@@ -0,0 +1,28 @@
#include "CoreNamespacePreamble.h"
#include "ResolvedLayoutInternal.h"
int main()
{
VoxelForgeCore::ResolvedLayoutStratumV2 upper, lower;
upper.strateIndex=0;upper.order=0;upper.transitionMode="Gradient";
upper.transitionWidthMicrometres=16000000;
upper.bedrock.bottomMicrometres=-120000000;
upper.bedrock.topMicrometres=-119500000;
upper.bedrock.presentInCurrentField=true;
lower.strateIndex=1;lower.order=1;
lower.ceiling.bottomMicrometres=-126250000;
lower.ceiling.topMicrometres=-120000000;
lower.ceiling.presentInCurrentField=true;
for(const auto& point:std::array<std::array<std::int64_t,2>,5>{
std::array<std::int64_t,2>{64000000,-32000000},
{65000000,-32000000},{64000000,-31000000},{70000000,-32000000},
{80000000,-35000000}})
{
const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(
upper,lower,0,8000000,point[0],point[1]);
std::printf("x_m=%.3f y_m=%.3f top_m=%.6f bottom_m=%.6f\n",
point[0]/1000000.0,point[1]/1000000.0,
bands.upperBedrockTopMicrometres/1000000.0,
bands.upperBedrockBottomMicrometres/1000000.0);
}
}
@@ -0,0 +1,121 @@
# VoxelForge L30
Work is on `assembler/world`. The speed and look patches are saved separately under
`Saved/Linux/l30/`; evaluator version is now 16 for the field change. No Unreal files were edited.
Nothing was committed, pushed, or stashed.
## Ring measurement and fix
I rasterized upward-facing triangles from the L29 mesh (skirt triangles excluded) over the same
409,600 pixels as the after field. At `(64, -32)`, the L29 field stayed almost flat at `-120.063 m` from
radii 1–11 m, then broke at 12 m. The expected Gradient at radius 1 m is `-121.360 m`. This is a
field defect, not shading: the probe is about 67 m from the passage mouth, and the raster has no
skirts or readable-shading tint. The passage column flare is at that distant mouth as well.
The upper FloatingIslands generator could leave native solid density in the air-side gap above a
negatively displaced Gradient face. The constant band densities also snapped the face to Core
voxel levels. The owner Gradient path now clears that gap with positive signed distance and writes
negative signed distance below the face, allowing the mesh to interpolate its height. The after
profile follows the resolved hash surface at every sampled radius. Match error fell from RMS
`0.361765 m`, p95 `1.016178 m`, max `1.304169 m` to RMS `0.110357 m`, p95 `0.218531 m`, max
`0.248818 m` (one Core voxel is `0.25 m`).
Before/after captures use the same color ramp and bounds:
- [L29 height map](captures/fi-floor-gradient-before-heightmap.jpg), [fixed height map](captures/fi-floor-gradient-after-heightmap.jpg)
- [L29 readable render](captures/fi-floor-gradient-before.jpg), [fixed readable render](captures/fi-floor-gradient-after.jpg)
## Transition captures
The maps cover the full 640×640 surface and are all below 300 KB. Gradient is the owner world
without a mode override. Hard and Interleaved use fixture overrides; the resolved layouts record
those modes.
- [Hard](captures/transition-hard-heightmap.jpg) — flat interface.
- [Gradient](captures/transition-gradient-heightmap.jpg) — owner field.
- [Interleaved](captures/transition-interleaved-heightmap.jpg) — fixture override.
## Cost
Five alternating W12 pairs, hashing off and `cache_hits=0`. First table is the separable speed-only
build, whose hashes remain L29-identical. Second table is the final version-16 build.
| Recipe | L24 | L29 | L30 speed-only | vs L24 | L30 final | vs L24 |
|---|---:|---:|---:|---:|---:|---:|
| SurfaceWorld | 1.566 s | 1.653 s | 1.606 s | +2.55% | 1.530 s | +2.68% |
| FloatingIslands | 16.625 s | 16.779 s | 16.470 s | −0.93% | 16.543 s | +0.21% |
Counter-enabled runs on the same plain W12 workload locate the saved work:
| Recipe | Boundary calls | Window checks → evaluations | Fade calls | Boundary tile / point culls | Column Z tests → guards → radial rejects | SDF calls |
|---|---:|---:|---:|---:|---:|---:|
| SurfaceWorld | 19.95 M | 3.54 M → 739 k | 8.2 k | 8.99 M / 8.96 M | 1.87 M → 1.85 M → 1.58 M | 18.4 k |
| FloatingIslands | 66.33 M | 8.00 M → 2.68 M | 8.6 k | 38.25 M / 23.63 M | 8.17 M → 8.14 M → 6.82 M | 83.4 k |
The fade-tile cull skips 731 k SurfaceWorld and 2.67 M FloatingIslands fade evaluations; the
column guard exits on nonpositive field values 272 k and 1.27 M times. Timing comes from
uninstrumented binaries; the counts above use counter logging.
## Hashes and audits
The speed-only v15 build matches all 12 L29 hash records for each recipe/compiler/worker matrix
(W1/W8/W12), and all 12 records for each 24 m crossing matrix. That is 24 exact comparisons;
all pass. The final v16 build is deterministic across both compilers and all worker counts, in the
normal and 24 m crossing runs. Representative W12 hashes (`density / mesh / payload / topology`):
| Recipe | Full detail | Whole view |
|---|---|---|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `4ee19ea6b6406ca6 / be648f94c899b47e / 310b525f31604f98 / 10366eb6dfce2384` |
Exact-query hashes also match across v16 runs: SurfaceWorld
`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849`; FloatingIslands
`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`.
- Interface audit: all 12 compiler/recipe/mode combinations pass; 4,221 columns each, zero holes
and air samples, both negative controls reject.
- The 16 L29 fixture-matrix replays pass passage safety/open-air, validator controls (10/10), L13
route parity, L9 proof checks (zero violations), and walkability. `FloorProfileBlend` still
reports 2.544 vox in style 1 and 3.645 vox in style 3, unchanged from L29.
- Audit scope caveat: the L29 files named `floating-islands` were run with
`--recipe=surface-world`. With the actual `--recipe=floating-islands` option, the rock-column
audit fails on all four styles on both L29 and L30: connected ends are 1/2, 3/4, 1/2, 3/4.
The open-air audit passes. This is inherited, but means the L29 report's “two recipes” matrix did
not exercise the FloatingIslands CLI recipe.
## Failed and retried runs
- Broad air-gap experiments first lost a SurfaceWorld style-2 support sample. Route/radius masks
and support restoration then caused open-air violations. Widening the fade restored that case,
but measurement placed the ring far outside the fade, so those variants were discarded. The
final owner-only gate and signed-distance face pass the targeted support audit.
- Early speed variants missed the target: tile-only `+4.04%`, Z-point `+3.11%`, scalar path
`+4.33%` vs L24. The final band and fade-tile culls meet the target.
- The first counter-instrumentation compile failed; the counter hook was corrected and rebuilt
with Clang and GCC. One duplicated timing sequence was discarded; only the clean five-pair runs
are reported.
- The first 24 m crossing comparison supplied an extra camera override and mismatched. Rerunning
with the L29 camera setup and only `--camera-step=24,0,0` matches all hashes.
- An early Hard export hit `std::bad_alloc`; retry passed. The first EGL render used an unwritable
cache; retry with `XDG_CACHE_HOME` under `Saved/Linux/l30` passed. The first map conversion also
stopped before the Hard/Interleaved maps; both were rerun individually and have full raster
coverage.
- The first passage-matrix summary script assumed a different marker order and reported failure;
checking each audit marker by name confirms the core matrix passes. A `check=True` true-recipe
control runner stopped on the expected L29 exit code 10; rerunning while recording return codes
confirmed the same four inherited rock-column failures on L29 and L30.
No build or run exceeded 30 minutes or remained stuck for 15 minutes. Builds and runs used CPUs
0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap. No Unreal build was
attempted.
## Orchestrator review (Claude)
- Merged: the ring was a real field defect (air-side gap left solid, face snapped to voxel levels), now fixed
(RMS 0.36 → 0.11 m). Cost back to SW +2.7% / FI +0.2% vs L24. The speed part is bit-identical; final v16 is
deterministic.
- Still visible: `fi-floor-gradient-after.jpg` shows faint concentric lines. Unknown whether that is residual
geometry (≤0.25 m steps) or the readable shader's height-tint banding; L31 measures it.
- **Important finding (thanks to Luna's caveat):** L29's "both recipes" passage matrix ran SurfaceWorld twice. On
the real `--recipe=floating-islands`, the rock-column connected-ends audit fails in all 4 styles (1/2 or 3/4).
Since L27, FI column results were never really tested. L31 priority.
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Interleave five W12 hash-off whole-view cost triplets for L24/L29/L30."""
import re
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l30"
OUT.mkdir(parents=True, exist_ok=True)
BINARIES = {
"l24": ROOT / "Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19",
"l29": ROOT / "Saved/Linux/bin/VoxelForgeCoreHeadless-linux29-clang",
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
}
ORDERS = (
("l24", "l29", "l30"),
("l29", "l30", "l24"),
("l30", "l24", "l29"),
("l24", "l30", "l29"),
("l29", "l24", "l30"),
)
WHOLE_READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
rows = []
for recipe in ("surface-world", "floating-islands"):
for pair, order in enumerate(ORDERS, 1):
for build in order:
name = f"cost-speedfade-{recipe}-pair{pair}-{build}"
logfile = OUT / f"{name}.log"
command = [
"prlimit", "--as=6442450944", "--", str(BINARIES[build]),
str(ROOT / "Standalone/data/owner_world.json"),
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
"--stream-pipeline", "--force-generate", "--clip-radius=3",
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
]
started = time.monotonic()
with logfile.open("w", encoding="utf-8") as stream:
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
check=True, timeout=1800)
elapsed = time.monotonic() - started
text = logfile.read_text(encoding="utf-8", errors="replace")
matches = WHOLE_READY.findall(text)
if len(matches) != 1 or "hash=off" not in text or "cache_hits=0" not in text:
raise SystemExit(f"invalid cost run: {name}; see {logfile}")
ready = float(matches[0])
rows.append((recipe, pair, build, ready, elapsed))
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
f"process_elapsed={elapsed:.3f}s", flush=True)
summary = OUT / "cost-runs-speedfade.csv"
summary.write_text("recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds\n" +
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.6f}\n"
for recipe, pair, build, ready, elapsed in rows), encoding="utf-8")
print(f"wrote {summary}", flush=True)
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Verify speed-only L30 hashes on the 24 m crossing against L29."""
import re
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l30"
OUT.mkdir(parents=True, exist_ok=True)
BINARIES = {
"clang": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
"gcc": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade-gcc",
}
PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
passed = True
for compiler, binary in BINARIES.items():
for recipe in ("surface-world", "floating-islands"):
reference_path = ROOT / f"Saved/Linux/linux29-final-crossing24-{compiler}-{recipe}.log"
reference = PATTERN.findall(reference_path.read_text(encoding="utf-8"))
for workers in (1, 8, 12):
name = f"crossing-speedonly-default-{compiler}-{recipe}-w{workers}"
logfile = OUT / f"{name}.log"
command = [
"prlimit", "--as=6442450944", "--", str(binary),
str(ROOT / "Standalone/data/owner_world.json"),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
"--stream-hash", "--force-generate", "--clip-radius=3",
"--max-clip-level=8", f"--workers={workers}",
"--camera-step=24,0,0",
"--region-crossing", f"--recipe={recipe}",
]
started = time.monotonic()
with logfile.open("w", encoding="utf-8") as stream:
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
check=True, timeout=1800)
elapsed = time.monotonic() - started
rows = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace"))
ok = len(reference) == 12 and rows == reference
passed &= ok
print(f"{compiler} {recipe} W{workers} elapsed={elapsed:.3f}s "
f"hash_rows={len(rows)} {'MATCH' if ok else 'MISMATCH'}", flush=True)
print(f"[CrossingSpeedOnly] runs=12 status={'PASS' if passed else 'FAIL'}", flush=True)
raise SystemExit(0 if passed else 1)
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB