Files
Sylpheed/tools/port/verify-capture
MechaCat02 c3758e3850 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00

376 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# Diff the port's render against a CAPTURE OF THE REAL GAME.
#
# tools/port/verify-capture main_menu
# tools/port/verify-capture # every screen with a capture
#
# THIS IS THE CORRECTNESS CHECK. `verify-screen` is the consistency one, and its
# own header has pointed at this file since P1 -- `tools/port/verify-capture` -- while
# this file did not exist. The port has had a harness comparing two renderers
# that share its assumptions, and none comparing it to the game.
#
# 🔴 WHAT THE `diff` COLUMN DOES NOT SAY. It counts pixels surviving
# `-threshold 25%` -- differing by more than ~64 levels. That is deliberate: it
# detects a missing or MISPLACED element, which is a large connected blob. It is
# blind to sub-threshold spatial error -- a one-pixel offset, a soft edge in a
# slightly wrong place, an antialiasing difference -- because none of that moves
# a pixel 64 levels.
#
# So `main_menu 0.06%` means NO GROSS DISPLACEMENT. It does NOT mean the
# geometry is right, and it has already been read that way by another agent:
# `docs/re/structures/title-residual-tone-vs-geometry.md` uses this screen as a
# tone-only positive control, citing this number as "geometry is essentially
# right". Measured 2026-08-31 against that capture: after fitting a per-level LUT
# -- the most general tone model there is -- the remaining residual is 6.94 on
# edge pixels against 2.20 on flat ones, a 3.2x concentration. A purely tonal
# residual leaves a per-level LUT exactly 0.00 (checked, by construction). The
# menu carries spatial error this column cannot see.
#
# ⚠️ That gap is not academic. `docs/re/captures/ORACLE-CAPTURES.md`: two
# renderers agreeing proves nothing, and this corpus has been bitten three times
# -- the dropped `pteff05` background, the scale-0 rect, and `rest()`. Every one
# was invisible to a render-vs-render diff and obvious against a capture.
#
# WHAT IT CAN CONCLUDE, and what it cannot:
#
# * ✅ STRUCTURE. Something drawn that should not be, or missing that should be,
# shows as a large connected region of difference. That is the failure mode
# the three above were, and it is what this tool is for.
# * 🔴 NOT a pixel score. The captures are NOT gamma-neutral:
# `capture ~= 255*(render/255)^g` with g ~ 1.34-1.49, and that ramp is THE
# GAME'S, not the capture path's (`docs/re/structures/ui-render-tone-curve.md`).
# So RMSE has a floor and driving it lower is fitting the ramp. This reports
# the raw difference AND the gamma-compensated one, and neither is a target.
# * ⚠️ A capture is ONE MOMENT. Several screens are still animating -- the
# title's two `ptloop` sweeps never stop -- and the focused button in a
# capture may not be the one the port focuses. Differences confined to a
# button or a moving element are expected; say which before calling anything.
#
# Geometry needs no correction: the corpus cross-correlated a render against
# `live-main-menu.png` over +/-6 px and the best alignment is exactly (0,0) at
# 0.9466. The captures are a 1279x675 top-left crop of the 1280x720 guest
# surface, so the render is cropped to match and nothing is scaled.
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
export DISPLAY="${DISPLAY:-:97}"
OUT="${OUT:-$(mktemp -d)}"; mkdir -p "$OUT"
CAPS=docs/re/captures/title-builds
# screen : capture : how to pose it
# 🔴 A MENU CAPTURE HAS A BUTTON FOCUSED, AND THE FIRST VERSION OF THIS TOOL
# RENDERED WITH NONE. `--screen=` draws no focus record at all, so `main_menu`
# was being compared to the oracle in a state the oracle was never in: 2 159
# differing pixels, of which 74 % sat inside the focus signature. Rendered with
# focus it is 531 -- 0.06 % of the frame, a 4x improvement that was entirely my
# harness posing the port wrong.
#
# `--menu=` applies `authored/flow.json`'s initial focus and `--script=wait`
# shoots one settled frame and exits.
MAP=(
"main_menu:$CAPS/live-main-menu.png:menu"
"extras:$CAPS/live-extras.png:menu"
# The only capture of a MEASURED focus state, and it was unusable until
# `--focus=` was made to work on the `--menu` path (it parsed, was stored, and
# was overwritten by the authored initial focus on every `_menu_enter`).
#
# It discriminates: rendering each of the five buttons focused against this
# capture gives 0.1355 % for `ptbtn04` and 0.70-0.82 % for the other four. The
# port's focus rendering identifies the right button by a factor of five.
"main_menu_options:$CAPS/live-main-menu-options-focused.png:focus:ptbtn04"
# ⚠️ THE TITLE IS POSED AT t=357.7 UNITS, NOT AT ITS SETTLE, and the time is
# MEASURED rather than chosen. The two `ptloop` sweeps are a continuous
# animation whose leaf group ends at t=600 with the quads parked off-screen at
# x=1521, so posing at the settle compares a still frame against a capture
# taken mid-sweep and simply omits them.
#
# t=357.7 is the Decoder's REFINED fit, and the refinement is worth knowing.
# Its first value, 355, came from the two per-draw alpha bytes alone and left
# an 11.5 px residual that looked like a pivot problem. Solving the same
# instant on the vertex POSITIONS instead gives t=357.88 and 357.58 to
# +/-0.12 units, against +/-1.54 and +/-1.89 from the alphas -- alpha moves
# only 0.27-0.33 levels per unit, so one byte of quantisation is worth 6-8 px
# of sweep. At 357.7 the centres land within 0.70 px and both alphas inside one
# level. THE 11.5 px WAS THE FIT'S RESOLUTION, NOT GEOMETRY.
#
# ⚠️ And there is no pivot correction: the leaf pivot is (200, 90) on a 399x180
# sprite, so it is the centre to within half a pixel -- checked here against
# the export rather than taken.
#
# ⚠️ It is NOT the time that minimises the difference: t=390 measures 1.65 %
# against 1.82 % here. Picking that one would be fitting the pose to the
# score, which is the thing this harness exists not to do.
"title:$CAPS/live-title-build4-no-plate.png:t357"
# The Japanese title at rest, Decoder 310bf86. Settled pose by omission --
# the capture is demonstrated at rest (five frames over 6 s, 0 px change in
# the logo block while 5-8 % of the frame moves).
#
# 🔴 THIS ROW EXISTS BECAUSE SCORING THE WRONG FRAME COST A WRONG CONCLUSION.
# `verify-screen` poses at `--pose=rest`, which for this screen lights every
# `ptlogo_back2eff*` sparkle at its own peak simultaneously -- `rest` for those
# elements IS the peak of a 4-unit flash. That frame is fine for the
# consistency check it was built for and must never be scored against a
# capture: doing so put the port at r +0.7462 against the reference's +0.8727
# and I wrote up that the port had moved away from the game. Posed as it
# SHIPS, the same block scores **+0.9994**.
"title_jp:$CAPS/live-title-jp-at-rest.png:settled::1279x675+1+45"
# A SECOND title comparison, and the most sensitive row this tool has.
#
# `live-title-press-a.png` is the title WITH the plate. Posed at t=237 -- inside
# the plate's own 8-unit opaque window, t=236-238 -- the port matches it at
# **0.00093 %**, two orders below every other row. That makes it the best
# regression detector here: anything structural that moves will show.
#
# ⚠️ The instant is FITTED, not measured: 237 is where this capture's content
# places it, found by sweeping. That is legitimate for choosing which frame to
# compare against -- every row does it -- but it is not a claim about the game,
# and the 0.00093 % is therefore a floor for THIS pose, not a general accuracy.
#
# It also closes the systematic-error question the leaf sweep left open. Two
# independent captures fit at two DIFFERENT phases -- this one at 237, the
# no-plate one at ~400 -- and both to 0.01 % or better. A geometry error in how
# the sweeps are drawn would leave a floor in both. Neither has one.
# 🔴 THIS ROW IS POSED AT THE PLATE'S BLIND PHASE, and it cannot see the plate's
# highlight at all. `--loop-phase=0` pins the looping-focus clock, and
# `ptbtn00f` -- the plate's own highlight, which the GAME draws ADDITIVE
# (docs/re/data/blend-bit-vs-oracle.txt, entry 2) -- contributes EXACTLY 0 px at
# phase 0 and 22 000-29 000 px at phases 20..100. Measured 2026-08-31 by
# shadowing its sprite and diffing.
#
# So switching that element to its measured additive blend moved 26 319 px at
# phase 20 and reported ZERO here. This row's 0.09 % is real and unaffected; it
# simply says nothing about the pulse. A capture at a NON-ZERO loop phase is
# what would let this row see it, and none exists -- filed in BLOCKED.md.
"title_plate:$CAPS/live-title-press-a.png:plate"
# A BANDED row -- the capture is 1279x120, not a full frame, and the harness
# could not compare one until now. That was the only reason this capture sat
# unused; nothing about it was unusable.
#
# Its y offset is MEASURED, not guessed: sliding it down the render, the
# structural difference is 0.354 % at y=520 against 8.9-9.1 % five pixels
# either side and 17-52 % further out. A 25x drop over five pixels.
#
# ⚠️ Its residual is NOT the port's error. The port reproduces the same band of
# `live-title-press-a.png` EXACTLY (0.000 %), and the two captures differ from
# each other by 0.301 % -- two thin horizontal strips, 248x5 px and 206x1 px,
# the shape of a sub-pixel edge difference rather than a state difference. So
# 0.354 % is very nearly the oracle-to-oracle gap and this row's job is to stay
# near it, not to reach zero.
"title_band:$CAPS/live-attract-title-press-a-band.png:band"
"publisher_logo:$CAPS/live-splash-publisher.png:screen"
"developer_logos:$CAPS/live-splash-developer.png:screen"
)
CURVE=""
if [ "${1:-}" = "--curve" ]; then CURVE=1; shift; fi
want=("$@")
echo "RMSE is reported and is NOT a target: the capture carries the game's own"
echo "tone ramp, so it has a floor. What finds a real defect is the DIFFERING"
echo "REGION -- a missing or misplaced element is a large connected blob."
echo
# 🔴 THE METRIC'S OWN ZERO, asserted before any row is printed.
#
# Every number below is "small is good", and this file already says the RMSE has
# a floor from the game's tone ramp. What was never established is the floor of
# the COMPARISON ITSELF. A control that only bounds error from above cannot tell
# an exact instrument from a slightly wrong one -- and slightly wrong is the
# failure that passes. The Decoder reached that form of it after their coherence
# estimator's positive control read 0.94 for two reasons at once.
#
# Measured here rather than assumed: a capture against itself, and against a PNG
# round-trip of itself, must both be EXACTLY 0. If they are not, the metric has a
# bias and no row below means what it says.
_ctl=""
for row in "${MAP[@]}"; do
IFS=: read -r _n _c _rest <<<"$row"; [ -f "$_c" ] && { _ctl="$_c"; break; }
done
if [ -n "$_ctl" ]; then
_rt="${TMPDIR:-/tmp}/verify-capture-rt.png"; convert "$_ctl" -quality 100 "$_rt"
for _pair in "$_ctl|$_ctl|identity" "$_ctl|$_rt|PNG round-trip"; do
IFS='|' read -r _a _b _lab <<<"$_pair"
_d=$(convert "$_a" "$_b" -metric RMSE -compare -format "%[distortion]" info: 2>&1 | tail -1)
_v=$(python3 -c "print('%.4f' % (float('$_d')*255))" 2>/dev/null || echo "?")
if [ "$_v" = "0.0000" ]; then
printf ' metric control, %-16s RMSE %s -- exact\n' "$_lab:" "$_v"
else
printf ' 🔴 metric control, %-13s RMSE %s -- NOT ZERO. The comparison is\n' "$_lab:" "$_v"
echo " biased and every row below is unreadable. Refusing."
exit 3
fi
done
echo
fi
printf '%-17s %-9s %-7s %-22s %s\n' screen raw-rmse diff region note
for row in "${MAP[@]}"; do
IFS=: read -r name cap pose forced capcrop <<<"$row"
if [ ${#want[@]} -gt 0 ] && ! printf '%s\n' "${want[@]}" | grep -qx "$name"; then continue; fi
[ -f "$cap" ] || { printf '%-17s %s\n' "$name" "no capture"; continue; }
if [ "$pose" = band ]; then
godot --path port --resolution 1280x720 -- --loop-phase=0 --leaf-time=0 "--screen=title" --overlay=press_start \
--time=3.95 "--capture=$OUT/$name.full.png" >"$OUT/$name.log" 2>&1 || true
[ -f "$OUT/$name.full.png" ] && convert "$OUT/$name.full.png" \
-crop 1279x120+0+520 +repage "$OUT/$name.render.png"
elif [ "$pose" = focus ]; then
godot --path port --resolution 1280x720 -- --loop-phase=0 --leaf-time=0 "--menu=main_menu" "--focus=$forced" \
--script=wait "--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true
[ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png"
elif [ "$pose" = menu ]; then
godot --path port --resolution 1280x720 -- --loop-phase=0 --leaf-time=0 "--menu=$name" --script=wait \
"--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true
[ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png"
elif [ "$pose" = plate ]; then
godot --path port --resolution 1280x720 -- --loop-phase=0 --leaf-time=0 "--screen=title" --overlay=press_start \
--time=3.95 "--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true
elif [ "$pose" = t357 ]; then
# 🔴 NO `--time` HERE EITHER, and the row's note used to claim otherwise.
#
# It passed `--time=5.9617` (t=357.7 units, the Decoder's refined sweep fit)
# and that value was NEVER APPLIED: `pose_at` replaced it with the screen's
# settle instant, t=198, on every run. Every title figure this tool has ever
# printed -- including the 0.26 % the port has quoted repeatedly -- was
# measured at the SETTLE, under a note saying t=357.7.
#
# Honouring it now makes that visible: t=357.7 is PAST the title's own group,
# which ends at t=269, so the whole screen poses at its faded-out final
# keyframes and the disagreement goes to 30.97 %. The instant was only ever
# meant for the `ptloop` LEAF, which runs to t=600 and is looped separately
# by `loop_leaf` (authored/rendering.json). Applying it to the whole screen
# was always wrong; it was harmless only while it was ignored.
#
# So: pose at the settle, which is what was actually being measured, and let
# the leaf loop carry the sweeps' phase.
godot --path port --resolution 1280x720 -- --loop-phase=0 --leaf-time=0 "--screen=$name" \
"--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true
else
# NO `--time`. It used to pass `--time=99` as an idiom for "settled", and
# that worked only because `--time` was SILENTLY IGNORED on a screen with a
# settle window: `pose_at` overwrote the requested instant with
# `settle_instant` whenever `holding` was true. The tool asked for t=5940
# units and was handed the settle instant, which is the pose it actually
# wants -- and the 0.01 % agreements on both splashes were measured through
# that accident. Now that `--time` is honoured, asking for it explicitly
# would pose past the end of every group, so the request is simply dropped
# and the settled pose asked for by omission.
godot --path port --resolution 1280x720 -- --loop-phase=0 --leaf-time=0 "--screen=$name" \
"--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true
fi
[ -f "$OUT/$name.render.png" ] || { printf '%-17s %s\n' "$name" "render failed"; continue; }
# Crop the render to the capture's frame. The capture is the crop, not a scale.
if [ "$pose" = band ]; then
cp "$OUT/$name.render.png" "$OUT/$name.crop.png"
else
convert "$OUT/$name.render.png" -crop 1279x675+0+0 +repage "$OUT/$name.crop.png"
fi
# ⚠️ A FIFTH FIELD, because not every capture is pre-cropped to the game
# surface. Every capture in `$CAPS` until now was already 1279x675, so the
# render was cropped and the capture used as-is. The JP title capture is a
# full 1280x720 DISPLAY frame with the surface at +0+45 -- comparing it whole
# would score the port against a 45px shift and report a catastrophe.
#
# The offset is MEASURED, not inherited from the earlier submenu capture:
# row/column profile correlation against the port, with the English pair as a
# control, gives (0,0) for the control at r 0.994 and dy=-45 for this frame.
if [ -n "$capcrop" ]; then
convert "$cap" -crop "$capcrop" +repage "$OUT/$name.cap.png"
cap="$OUT/$name.cap.png"
fi
raw=$(convert "$OUT/$name.crop.png" "$cap" -metric RMSE -compare -format "%[distortion]" info: 2>&1 | tail -1)
raw=$(python3 -c "print('%.2f' % (float('$raw')*255))" 2>/dev/null || echo "?")
note=""
# 🔴 EVERY ROW WITH A SWEEPING LEAF CARRIES A CAPTURE-PHASE TERM, AND THIS
# TOOL USED TO PRINT THE NUMBER WITHOUT IT.
#
# `ptloop01`/`ptloop02` free-run on a settled screen -- a settled screen is not
# a static screen -- so a capture froze them wherever the shutter fell, and the
# render is pinned at `--leaf-time=0` by CONVENTION, not because 0 is the
# game's phase. Measured by sweeping the phase against each capture:
#
# title 5.56 main_menu 3.78 extras 3.73 splashes 0.00
#
# Those are RMSE, in this tool's own metric, and larger than most margins
# anyone has quoted from these rows. So: usable for REGRESSION at a fixed pin,
# run to run; NOT usable as an absolute against anything measured differently.
#
# ✅ The two splash rows carry no free-running element at all. They are the
# only absolutes here that mean what they say.
case "$name" in
title) note="settle t=198; +/-5.56 capture-phase term -- regression only" ;;
title_jp) note="+/-5.6 capture-phase term (same leaves as build 4)" ;;
# 🔴 The old note said "rendered with AUTHORED initial focus", and it was
# stale twice over. The value became MEASURED on 2026-08-31 (NEW GAME, 2/2
# fresh boots, first entry) -- and the capture's OWN focus state, which had
# never been established, is now identified by exclusion: rendering all five
# candidates against this capture gives ptbtn01 13.06 and every alternative
# 15.96-16.59, ~22 % worse. So the residual below is NOT a focus mismatch.
#
# ⚠️ It does not re-establish "the menu opens on NEW GAME". Focus persists on
# this screen, so a capture of the running menu could show any item; what is
# established is that THIS capture shows NEW GAME and the port renders the
# same state.
main_menu) note="focus ptbtn01 confirmed by exclusion (next best +22%); +/-3.78 capture-phase term" ;;
extras) note="rendered with authored initial focus; +/-3.73 capture-phase term" ;;
publisher_logo|developer_logos) note="no free-running element -- absolute, means what it says" ;;
esac
# Where the difference lives. This comes FIRST because it is what the gamma
# sweep has to be protected from.
convert "$OUT/$name.crop.png" "$cap" -compose difference -composite \
-colorspace Gray -threshold 25% "$OUT/$name.mask.png"
# THE TONE RELATIONSHIP IS REPORTED AS A CURVE, NOT AS A BEST EXPONENT, and
# two earlier versions of this tool reported an exponent and were wrong twice.
#
# `docs/re/structures/ui-render-tone-curve.md` models it as
# `capture = 255*(render/255)^g`, g ~ 1.34-1.49, measured on dark flat patches
# and explicitly not constrained above render ~60. Binning every structurally
# matched pixel of `main_menu` by render level and averaging the capture gives:
#
# render capture implied g pixels
# 8 4.04 1.20 183 026
# 16 7.89 1.26 227 630
# 24 15.57 1.18 100 945
# 32 26.15 1.10 87 474
# 40 38.07 1.03 86 094
# 48 53.96 0.93 85 255
# 64 78.52 0.85 6 509
# 96 130.44 0.69 1 682
#
# ⚠️ **The implied exponent is not constant. It falls monotonically and crosses
# 1.0 near render ~44**, so the capture is DARKER than the render in the darks
# and BRIGHTER in the midtones. A single power law cannot express that, which
# is exactly why a whole-frame fit returns 1.00: the two halves cancel. The
# corpus's reach -- "nothing constrains midtones or highlights" -- was a real
# limit and this is what lies past it.
#
# So: no best-g is printed. The table above is the instrument that can actually
# be argued with; `tools/port/verify-capture --curve SCREEN` regenerates it.
frac=$(convert "$OUT/$name.mask.png" -format "%[fx:mean*100]" info:)
box=$(convert "$OUT/$name.mask.png" -trim -format "%wx%h%X%Y" info: 2>/dev/null || echo "-")
printf '%-17s %-9s %6.2f%% %-22s %s\n' "$name" "$raw" "$frac" "$box" "$note"
done
if [ -n "$CURVE" ]; then
for row in "${MAP[@]}"; do
IFS=: read -r name cap pose <<<"$row"
if [ ${#want[@]} -gt 0 ] && ! printf '%s\n' "${want[@]}" | grep -qx "$name"; then continue; fi
[ -f "$OUT/$name.mask.png" ] || continue
convert "$OUT/$name.crop.png" -colorspace Gray -depth 8 "gray:$OUT/$name.r.gray"
convert "$cap" -colorspace Gray -depth 8 "gray:$OUT/$name.c.gray"
convert "$OUT/$name.mask.png" -colorspace Gray -depth 8 "gray:$OUT/$name.m.gray"
echo; echo "transfer curve, $name -- structurally matched pixels only"
python3 - "$OUT/$name" <<'PYEOF'
import sys, math
b = sys.argv[1]
r = open(b+".r.gray","rb").read(); c = open(b+".c.gray","rb").read(); m = open(b+".m.gray","rb").read()
n = min(len(r), len(c), len(m)); bins = {}
for i in range(n):
if m[i]: continue
s = bins.setdefault(r[i]//8*8, [0,0]); s[0] += c[i]; s[1] += 1
print(" %-8s %-9s %-9s %s" % ("render","capture","implied g","pixels"))
for k in sorted(bins):
tot, cnt = bins[k]
if cnt < 500 or k < 8: continue
cap = tot/cnt
g = math.log(max(cap,0.5)/255.0)/math.log(k/255.0)
print(" %-8d %-9.2f %-9.2f %d" % (k, cap, g, cnt))
PYEOF
done
fi
echo "artifacts in $OUT"