#!/usr/bin/env bash # 渲染闸正主(迭代 19 卡① 自动化验收补齐批 2026-10-08 落码——T1/T2)——窗口化自动闸。 # 迭代 15③ 挂账「渲染五闸」机器补跑半随本闸落籍(shot_probe/golden_compare/board_shot 三件入闸; # fx_probe 逻辑层归 probe_gate --full——归宿对账见卡①任务书 §一; # ui_layout_probe 原列此处,2026-10-15 用户裁「无使用的探针删除」随批退役,1920 档布局机检面待补)。 # 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 # Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。 # 段: # ① shot_probe 七屏窗口化截图(menu/settings/career/camp/level/battle/status——迭代 19 卡① T1 扩屏) # → tools/shot_checker.py A-D 语义断言自动判定(A 屏级骨架全屏/B 文字对比度/C 明度三档带/ # D 文字禁压纹理;E 焦点预算=语义面仍归人闸不机检)。首跑存量 FAIL 按 S1 停机点全量呈报登记 06。 # ② 取证段(Q7:超时/零字节/文件缺失任一即 FAIL——「渲染真实发生」必须成立,不做像素判定): # fx_shot_probe 三时刻抓帧(行到达/结算排播/轮末连帧)+ board_shot_probe L1-L3 棋盘抓帧。 # ③ 棋盘 golden 逐像素(T2):board 整屏帧按 board_meta.json 取样坐标裁剪棋盘区 # (tools/golden_board_crop.py——禁硬编码像素)→ tools/golden_compare.py 对 # game/tests/golden/ 三张基线(容差 provisional:差 ≤16/changed ≤2%,S2 停机点口径)。 # 素材缺位=判红(2026-10-17 改;此前是 SKIP 不判红——但缺盘时这一段根本不对比,闸末仍打 # 「全绿」exit 0,等于空跑判绿。基线生成期素材缺位=L3 停机,运行期同样无权出验收结论)。 # ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比 # 1.35×+左右黑边各 96px,构图与基准档一致(迭代 9 分辨率案甲)——分辨率维度不重复建闸。 # (原由 ui_layout_probe 两档断言承担,探针已随 2026-10-15 裁「无使用的探针删除」退役, # 1920 布局机检面现无承载,待补。) # 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git; # 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。 # 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径,2026-10-07 用户令);禁改 --headless 跑本闸 # (headless 无渲染面,探针内 fail-fast 护栏会红——那是护栏生效不是闸红)。 # 用法: tools/render_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree) # 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell + python(PIL);Godot 路径可用环境变量 GODOT_BIN 覆盖。 set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" GAME="${1:-$REPO/game}" GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式 TMP="$(mktemp -d /tmp/rendergate_XXXXXX)" trap 'rm -rf "$TMP"' EXIT _rc=0 orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算) printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r' } kill_orphans() { local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done } # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 kill_orphans fi pre="$(orphans | wc -l | tr -d ' ')" if [ "$pre" -gt 0 ]; then echo "[render_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi echo "======== shot_probe 七屏截图 → shot_checker A-D ========" SHOT="$TMP/shots" timeout 180 "$G" --path "$GAME_WIN" res://smoke/shot_probe.tscn -- "$SHOT" >"$TMP/shot.log" 2>&1 st=$?; kill_orphans grep -E "\[shot_probe\]" "$TMP/shot.log" | tail -12 if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then echo "[render_gate] FAIL — shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1 elif [ "$st" -ne 0 ] || ! grep -q "问题=0" "$TMP/shot.log"; then echo "[render_gate] FAIL — shot_probe 未干净完成(exit=$st,缺失屏/空文件/路径断裂见上)"; _rc=1 fi if [ "$_rc" -eq 0 ]; then python tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /' _rc="${PIPESTATUS[0]}" [ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记 06)" fi echo "======== fx_shot_probe 三时刻取证(Q7) ========" FX="$TMP/fxshots" timeout 180 "$G" --path "$GAME_WIN" res://smoke/fx_shot_probe.tscn -- "$FX" >"$TMP/fx.log" 2>&1 st=$?; kill_orphans grep -E "\[fx_shot_probe\]" "$TMP/fx.log" | tail -4 if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then echo "[render_gate] FAIL — fx_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1 elif [ "$st" -ne 0 ]; then echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 fi fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7) local pat="$1" min="$2" n=0 f for f in "$FX"/$pat; do [ -f "$f" ] || continue [ -s "$f" ] || { echo "[render_gate] FAIL — 取证零字节: $f"; _rc=1; return; } n=$((n+1)) done if [ "$n" -lt "$min" ]; then echo "[render_gate] FAIL — 取证文件缺失: $pat 仅 $n/<$min"; _rc=1; fi } fx_evidence "fx_dig*.png" 1 fx_evidence "fx_settle*.png" 1 fx_evidence "fx_round*.png" 6 fx_evidence "fx_final.png" 1 echo "======== board_shot_probe L1-L3 取证(Q7) ========" BD="$TMP/boardshots" timeout 180 "$G" --path "$GAME_WIN" res://smoke/board_shot_probe.tscn -- "$BD" >"$TMP/bd.log" 2>&1 st=$?; kill_orphans grep -E "\[board_shot\]" "$TMP/bd.log" | tail -6 if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then echo "[render_gate] FAIL — board_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1 elif [ "$st" -ne 0 ]; then echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 fi for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi done echo "======== 棋盘 golden 逐像素(T2——裁剪→对比,容差 provisional) ========" if [ ! -d "$REPO/game/tests/golden" ]; then echo "[render_gate] FAIL — game/tests/golden/ 基线目录缺失(基线在库=验收断言②)"; _rc=1 elif [ ! -s "$BD/board_meta.json" ]; then echo "[render_gate] SKIP — board_meta.json 缺失(取证段已红),golden 对比不判" else # 素材缺位=判红(2026-10-17 由 SKIP 改):缺盘态下这一段一个像素都不比,而闸末照样打印 # 「全绿」退出码 0——那是空跑判绿,比误红危险得多(2026-10-16 菜单废基线事故正是靠它蒙过 # worktree 自检的)。基线锚在素材齐盘态(Q5),缺盘态本就无权出验收结论。 _asset_miss=0 for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ assets/art/board/flag_seal.png \ assets/art/character/character_hero_idle.png \ assets/art/board/terrain_wall_n_dungeon.png assets/art/board/terrain_floor_dungeon_1.png \ assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \ assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do [ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: $a(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; } done if [ "$_asset_miss" -eq 0 ]; then CROP="$TMP/crop" python tools/golden_board_crop.py "$BD/board_meta.json" "$BD" "$CROP" | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — 棋盘区裁剪失败"; _rc=1 else python tools/golden_compare.py "$REPO/game/tests/golden" "$CROP" \ board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — golden 逐像素超容差(S2 停机点:字体/平台误红呈报后定容差)"; _rc=1 fi fi fi fi echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========" if [ ! -s "$REPO/game/tests/golden/menu_boot.png" ]; then echo "[render_gate] FAIL — game/tests/golden/menu_boot.png 基线缺失(菜单 golden 未建——tools/golden_baseline_refresh.sh --menu)"; _rc=1 elif [ ! -s "$SHOT/shot_menu.png" ]; then echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判" elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then echo "[render_gate] FAIL — 菜单素材缺位(字体/按钮石纹):基线锚素材齐盘态,缺盘不得出验收结论(2026-10-17 由 SKIP 改判红)"; _rc=1 else MENUG="$TMP/menugolden" mkdir -p "$MENUG" cp "$SHOT/shot_menu.png" "$MENUG/menu_boot.png" python tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — 菜单 golden 超容差(S2 停机点口径:核对热区——按钮换墨/布局变更属预期面=显式重制基线+呈报;超预期面=L3)"; _rc=1 fi fi echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)" echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)" if [ "$left" -ne 0 ]; then echo "[render_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi if [ "$_rc" -eq 0 ]; then echo "[render_gate] 全绿"; else echo "[render_gate] 存在 FAIL,见上"; fi exit "$_rc"