fix(packaging): 堵两处判定假绿 + 证据账读数可复跑(M249 r1 P2-1/2/3)
P2-1 判据 2:`Selected: AdapterInfo {` 与字段不在同一行时(pretty-print Debug 漂移)
`device_type` 取不到、旧写法默认 `"?"` 而 `"?" ∉ {Cpu,Other}` ⇒ 判绿。改成取不到即判红。
P2-2 判据 1:`"wgpu" in renderers` 是「曾经出现过」,把 CPU 那次的 stderr >> 追加进来
再配 CPU 那张图会判绿。改成 `set(renderers) == {"wgpu"}`(全部 [shot] 行都声称 wgpu)。
P2-3 doc §2:A6 `--expect-refuse` 那行的「3/3」需要传 PNG 路径才成立,按表复跑是 2/2;
命令列补上 PNG,并单列「不传 PNG = 2/2」一行。
行数:判据脚本 383 → 311(自测脚手架搬到 tests/gpu_shot_selftest.py,124 行;向量
16 → 19 条,红 16 / 绿 3,新增「多行 Debug」「空体」「混入异种 renderer」三条靶子)。
This commit is contained in:
1 parent
d7a9ae15a5
commit
9aefb6a5b0
5 files changed
+191
-114
No files matched your search
+3
-2
@@ -68,8 +68,9 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
|
|||||||
| `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 |
|
| `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 |
|
||||||
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
|
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
|
||||||
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
|
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
|
||||||
| `packaging/check_gpu_shot.py` | GPU 截图证据判定(M249):`[shot] … renderer=wgpu` + `Selected: AdapterInfo` 的 `device_type ∉ {Cpu, Other}` + PNG 非空/尺寸对账/非空白,逐条点名;`--expect-refuse` 判拒绝降级出图;`--self-test` 跑 `tests/gpu_shot_vectors.json` 的 16 条向量。**尚未接进 CI**,口径与能证/不能证见 `docs/gpu-evidence.md` | ≤400 |
|
| `packaging/check_gpu_shot.py` | GPU 截图证据判定(M249):`[shot] … renderer=wgpu` + `Selected: AdapterInfo` 的 `device_type ∉ {Cpu, Other}` + PNG 非空/尺寸对账/非空白,逐条点名;`--expect-refuse` 判拒绝降级出图;`--self-test` 跑 `tests/gpu_shot_vectors.json` 的 19 条向量(脚手架 `tests/gpu_shot_selftest.py`)。**尚未接进 CI**,口径与能证/不能证见 `docs/gpu-evidence.md` | ≤400 |
|
||||||
| `packaging/tests/gpu_shot_vectors.json` | `check_gpu_shot.py --self-test` 的向量:16 条(红 13 / 绿 3),每条给出**期望的失败判据清单**,数据与代码分开 | ≤400 |
|
| `packaging/tests/gpu_shot_vectors.json` | `check_gpu_shot.py --self-test` 的向量:19 条(红 16 / 绿 3),每条给出**期望的失败判据清单**,数据与代码分开 | ≤400 |
|
||||||
|
| `packaging/tests/gpu_shot_selftest.py` | 上面那 19 条向量的执行器(按真实行格式手拼日志 + 用 `check_screenshot.encode_png` 现造 PNG):判据脚本自己守 ≤400 行,脚手架放这里 | ≤400 |
|
||||||
| `packaging/check_publish_slots.py` | 发布名单守卫(M164/M184):名单 ⇄ 产物两个方向 + `mode: ci` 槽位无产物的警告;`--self-test` 注入 11 条回归 | ≤400 |
|
| `packaging/check_publish_slots.py` | 发布名单守卫(M164/M184):名单 ⇄ 产物两个方向 + `mode: ci` 槽位无产物的警告;`--self-test` 注入 11 条回归 | ≤400 |
|
||||||
| `packaging/check_pe_subsystem.py` | PE 子系统守卫(M196):**读产物 PE 头的 `Subsystem`**(GUI 必须 2 / CLI 必须 3),非 2 即红;`--self-test` 注入 8 条回归(红 6 / 绿 2)(见 §15) | ≤400 |
|
| `packaging/check_pe_subsystem.py` | PE 子系统守卫(M196):**读产物 PE 头的 `Subsystem`**(GUI 必须 2 / CLI 必须 3),非 2 即红;`--self-test` 注入 8 条回归(红 6 / 绿 2)(见 §15) | ≤400 |
|
||||||
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 |
|
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 |
|
||||||
|
|||||||
+29
-101
@@ -25,15 +25,13 @@
|
|||||||
|
|
||||||
python packaging/check_gpu_shot.py shot.stderr.log shot.png # 正例:判定,0/1 退出
|
python packaging/check_gpu_shot.py shot.stderr.log shot.png # 正例:判定,0/1 退出
|
||||||
python packaging/check_gpu_shot.py a6.stderr.log --expect-refuse # 负例:判定拒绝是否成立
|
python packaging/check_gpu_shot.py a6.stderr.log --expect-refuse # 负例:判定拒绝是否成立
|
||||||
python packaging/check_gpu_shot.py --self-test # 16 条向量(数据在 tests/)
|
python packaging/check_gpu_shot.py --self-test # 19 条向量(数据+脚手架都在 tests/)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import zlib
|
import zlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -44,7 +42,6 @@ from check_screenshot import ( # noqa: E402
|
|||||||
DEFAULT_MIN_UNIQUE,
|
DEFAULT_MIN_UNIQUE,
|
||||||
PngError,
|
PngError,
|
||||||
decode_png,
|
decode_png,
|
||||||
encode_png,
|
|
||||||
measure,
|
measure,
|
||||||
verdict,
|
verdict,
|
||||||
)
|
)
|
||||||
@@ -122,20 +119,24 @@ def shot_size(text: str) -> tuple[int, int] | None:
|
|||||||
return (int(match.group(1)), int(match.group(2))) if match else None
|
return (int(match.group(1)), int(match.group(2))) if match else None
|
||||||
|
|
||||||
|
|
||||||
def _pick(pattern: re.Pattern[str], text: str, default: str = "?") -> str:
|
def _pick(pattern: re.Pattern[str], text: str, default: str | None = "?") -> str | None:
|
||||||
match = pattern.search(text)
|
match = pattern.search(text)
|
||||||
return match.group(1) if match else default
|
return match.group(1) if match else default
|
||||||
|
|
||||||
|
|
||||||
def selected_adapter(text: str) -> dict[str, str] | None:
|
def selected_adapter(text: str) -> dict[str, str | None] | None:
|
||||||
"""只取 `Selected:` 那一行:`Available adapters: [...]` 里有 12 条 `AdapterInfo`,取错就全错。"""
|
"""只取 `Selected:` 那一行:`Available adapters: [...]` 里有 12 条 `AdapterInfo`,取错就全错。
|
||||||
|
|
||||||
|
`device_type` 取不到时返回 `None`(**不是** `"?"`):一旦 iced 改成 pretty-print Debug,
|
||||||
|
字段就落不到 `{` 同一行,此时按「取不到 = 判红」处理,不许静默给绿(M249 r1 P2-1)。
|
||||||
|
"""
|
||||||
match = SELECTED_RE.search(text)
|
match = SELECTED_RE.search(text)
|
||||||
if match is None:
|
if match is None:
|
||||||
return None
|
return None
|
||||||
body = match.group("body")
|
body = match.group("body")
|
||||||
return {
|
return {
|
||||||
"name": _pick(ADAPTER_NAME_RE, body),
|
"name": _pick(ADAPTER_NAME_RE, body),
|
||||||
"device_type": _pick(DEVICE_TYPE_RE, body),
|
"device_type": _pick(DEVICE_TYPE_RE, body, None),
|
||||||
"backend": _pick(ADAPTER_BACKEND_RE, body),
|
"backend": _pick(ADAPTER_BACKEND_RE, body),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +171,11 @@ def judge_evidence(log_path: Path, png_path: Path) -> Report:
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
renderers = list(dict.fromkeys(shot_renderers(text)))
|
renderers = list(dict.fromkeys(shot_renderers(text)))
|
||||||
|
# 「这张图是 wgpu 出的」= 日志里**每一行** `[shot] … renderer=` 都说 wgpu。用 `in` 就退化成
|
||||||
|
# 「曾经出现过」:把 CPU 那次的 stderr `>>` 追加进来、配 CPU 那张图,会静默判绿(M249 r1 P2-2)。
|
||||||
report.add(
|
report.add(
|
||||||
1,
|
1,
|
||||||
"wgpu" in renderers,
|
set(renderers) == {"wgpu"},
|
||||||
"实际 renderer=" + "/".join(renderers) if renderers else "日志里没有 `[shot] … renderer=…` 行(没走到出图那一步?)",
|
"实际 renderer=" + "/".join(renderers) if renderers else "日志里没有 `[shot] … renderer=…` 行(没走到出图那一步?)",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -180,10 +183,15 @@ def judge_evidence(log_path: Path, png_path: Path) -> Report:
|
|||||||
if adapter is None:
|
if adapter is None:
|
||||||
report.add(2, False, "日志里没有 `Selected: AdapterInfo { … }`(wgpu 没建成设备,或只打印了 Available adapters)")
|
report.add(2, False, "日志里没有 `Selected: AdapterInfo { … }`(wgpu 没建成设备,或只打印了 Available adapters)")
|
||||||
else:
|
else:
|
||||||
|
device_type = adapter["device_type"]
|
||||||
|
# `None` = 字段不在 `{` 同一行(多行 Debug 漂移)或字段缺失:按取不到即失败处理,
|
||||||
|
# 宁可假红也不许「格式一变就静默给绿」。
|
||||||
report.add(
|
report.add(
|
||||||
2,
|
2,
|
||||||
adapter["device_type"] not in DISALLOWED_DEVICE_TYPES,
|
device_type not in (None, "?") and device_type not in DISALLOWED_DEVICE_TYPES,
|
||||||
f'device_type={adapter["device_type"]}({adapter["name"]} / {adapter["backend"]})',
|
f'device_type={device_type}({adapter["name"]} / {adapter["backend"]})'
|
||||||
|
if device_type not in (None, "?")
|
||||||
|
else "取不到 device_type:`Selected: AdapterInfo {` 与字段不在同一行(多行 Debug),或字段缺失",
|
||||||
)
|
)
|
||||||
|
|
||||||
width, height, stats = decode_shot(png_path, report)
|
width, height, stats = decode_shot(png_path, report)
|
||||||
@@ -258,7 +266,10 @@ def main(argv: list[str]) -> int:
|
|||||||
if arg == "--expect-refuse":
|
if arg == "--expect-refuse":
|
||||||
expect_refuse = True
|
expect_refuse = True
|
||||||
elif arg == "--self-test":
|
elif arg == "--self-test":
|
||||||
return self_test()
|
sys.path.insert(0, str(Path(__file__).resolve().parent / "tests"))
|
||||||
|
from gpu_shot_selftest import run_selftest # 延迟 import:脚手架反过来要拿本模块的判据函数
|
||||||
|
|
||||||
|
return run_selftest(judge_evidence, judge_refuse)
|
||||||
elif arg in ("-h", "--help"):
|
elif arg in ("-h", "--help"):
|
||||||
print(__doc__)
|
print(__doc__)
|
||||||
return 0
|
return 0
|
||||||
@@ -285,96 +296,13 @@ def main(argv: list[str]) -> int:
|
|||||||
return 1 if report.problems() else 0
|
return 1 if report.problems() else 0
|
||||||
|
|
||||||
|
|
||||||
# ────────────────────────── 自测:合成日志 + 合成 PNG ──────────────────────────
|
# ────────────────────────── 自测 ──────────────────────────
|
||||||
#
|
#
|
||||||
# 向量在 `packaging/tests/gpu_shot_vectors.json`(数据与代码分开,同 guard_vectors.json 的做法),
|
# 向量数据在 `packaging/tests/gpu_shot_vectors.json`,造样本与逐条比对的脚手架在
|
||||||
# 每条的 `expect_fail` 是**期望的失败判据清单**,逐条对:关掉一条判据、或让某条判据被别的东西
|
# `packaging/tests/gpu_shot_selftest.py`(判据脚本自己守 ≤400 行,脚手架不占这里的行数)。
|
||||||
# 掩盖,清单就对不上、本节报红。真图不在仓库里(`DevDocs/` 被 ignore),所以这颗钉子不能指望
|
# 每条的 `expect_fail` 是**期望的失败判据清单**,逐条对:关掉一条判据、或让某条判据被别的
|
||||||
# 真实产物——它守的是判据本身,真实产物由报告里的实跑退出码守。
|
# 东西掩盖,清单就对不上。真图不在仓库里(`DevDocs/` 被 ignore),所以这颗钉子守判据,真实
|
||||||
|
# 产物由报告里的实跑退出码守。
|
||||||
VECTORS_PATH = Path(__file__).resolve().parent / "tests" / "gpu_shot_vectors.json"
|
|
||||||
|
|
||||||
|
|
||||||
def synth_log(
|
|
||||||
*,
|
|
||||||
renderer: str | None = "wgpu",
|
|
||||||
device_type: str | None = "DiscreteGpu",
|
|
||||||
backend: str | None = "final=wgpu result=started",
|
|
||||||
size: tuple[int, int] | None = (200, 120),
|
|
||||||
) -> str:
|
|
||||||
"""按 M246 真实日志的**行格式**拼一份最小日志(传 `None` = 该行故意不出现)。"""
|
|
||||||
lines = [
|
|
||||||
"[probe] renderer = wgpu reason=forced-gpu 兜底=tiny-skia(失败会先打印具名错误)",
|
|
||||||
# 真实日志里永远有这一行提示文本,且它含 `Selected: AdapterInfo` 字样 —— 留着它,
|
|
||||||
# 「只有提示、没有真 Selected 行」的向量才是在测真东西。
|
|
||||||
"[probe] 取证:出现 `[gpu] iced_wgpu: …Selected: AdapterInfo…` ⇒ GPU 真生效;出现 `[gpu] wgpu 初始化失败` ⇒ 已降级 CPU",
|
|
||||||
]
|
|
||||||
if device_type is not None:
|
|
||||||
lines.append(
|
|
||||||
'[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo { name: "AMD Radeon RX 6600 XT",'
|
|
||||||
' vendor: 4098, device: 29695, device_type: %s, driver: "AMD proprietary driver",'
|
|
||||||
' driver_info: "22.8.2", backend: Vulkan, }' % device_type
|
|
||||||
)
|
|
||||||
if size is not None:
|
|
||||||
lines.append(f"[shot] {size[0]}x{size[1]} scale=1 -> selftest.png")
|
|
||||||
if renderer is not None:
|
|
||||||
lines.append(f'[shot] screen=gallery theme=light renderer={renderer} route=Detail("survival")')
|
|
||||||
if backend is not None:
|
|
||||||
lines.append(f"[backend] {backend} elapsed=1000ms evidence=selftest")
|
|
||||||
return "\n".join(lines) + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def synth_png(kind: str, size: tuple[int, int]) -> bytes:
|
|
||||||
"""`content` = 有内容的合成帧(渐变 + 8px 棋盘格);`flat` = 全白。"""
|
|
||||||
width, height = size
|
|
||||||
|
|
||||||
def content_row(y: int) -> bytes:
|
|
||||||
return b"".join(
|
|
||||||
bytes((x * 255 // max(width - 1, 1), y * 255 // max(height - 1, 1), 40))
|
|
||||||
if ((x // 8) + (y // 8)) % 2 == 0
|
|
||||||
else b"\x14\x14\x14"
|
|
||||||
for x in range(width)
|
|
||||||
)
|
|
||||||
|
|
||||||
if kind == "flat":
|
|
||||||
return encode_png(width, height, lambda _y: b"\xff" * (width * 3))
|
|
||||||
return encode_png(width, height, content_row)
|
|
||||||
|
|
||||||
|
|
||||||
def run_vector(root: Path, index: int, case: dict, default_size: tuple[int, int]) -> Report:
|
|
||||||
overrides = {key: (tuple(value) if isinstance(value, list) else value) for key, value in case.get("log", {}).items()}
|
|
||||||
log_path = root / f"case{index:02d}.log"
|
|
||||||
log_path.write_text(synth_log(**{"size": default_size, **overrides}), encoding="utf-8")
|
|
||||||
png_path = root / f"case{index:02d}.png"
|
|
||||||
png_size = tuple(case.get("png_size", default_size))
|
|
||||||
if case.get("png") == "truncated":
|
|
||||||
png_path.write_bytes(synth_png("content", png_size)[:200])
|
|
||||||
elif case.get("png") is not None:
|
|
||||||
png_path.write_bytes(synth_png(case["png"], png_size))
|
|
||||||
return judge_refuse(log_path, png_path) if case.get("mode") == "refuse" else judge_evidence(log_path, png_path)
|
|
||||||
|
|
||||||
|
|
||||||
def self_test() -> int:
|
|
||||||
"""跑 `tests/gpu_shot_vectors.json` 的全部向量,逐条对 `expect_fail`。零第三方依赖。"""
|
|
||||||
data = json.loads(VECTORS_PATH.read_text(encoding="utf-8"))
|
|
||||||
default_size = tuple(data["size"])
|
|
||||||
failures = 0
|
|
||||||
with tempfile.TemporaryDirectory(prefix="ull-gpu-shot-") as tmp:
|
|
||||||
root = Path(tmp)
|
|
||||||
for index, case in enumerate(data["cases"], start=1):
|
|
||||||
got = run_vector(root, index, case, default_size).problems()
|
|
||||||
want = case["expect_fail"]
|
|
||||||
if got != want:
|
|
||||||
print(f"::error::自测失败:{case['label']} 期望失败 {want},实际 {got}", file=sys.stderr)
|
|
||||||
failures += 1
|
|
||||||
else:
|
|
||||||
print(f"OK: {case['label']} → {'红 ' + ' / '.join(got) if got else '绿'}")
|
|
||||||
red = sum(1 for case in data["cases"] if case["expect_fail"])
|
|
||||||
if failures:
|
|
||||||
return 1
|
|
||||||
print(f"OK: GPU 截图证据判定自测通过({len(data['cases'])} 条向量:红 {red} / 绿 {len(data['cases']) - red})")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
for stream in (sys.stdout, sys.stderr):
|
for stream in (sys.stdout, sys.stderr):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# GPU 截图证据账(M249)
|
# GPU 截图证据账(M249)
|
||||||
|
|
||||||
判据的**唯一实现**是 `packaging/check_gpu_shot.py`(零第三方依赖,16 条自测向量)。
|
判据的**唯一实现**是 `packaging/check_gpu_shot.py`(零第三方依赖,19 条自测向量)。
|
||||||
样本是 M246 在本机的实测产物 `DevDocs/artifacts/gpu-probe/`(`DevDocs/` 被 `.gitignore:6` 忽略,
|
样本是 M246 在本机的实测产物 `DevDocs/artifacts/gpu-probe/`(`DevDocs/` 被 `.gitignore:6` 忽略,
|
||||||
**图不进仓库**,所以这里只引它的读数,不引它的字节)。
|
**图不进仓库**,所以这里只引它的读数,不引它的字节)。
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
```text
|
```text
|
||||||
python packaging/check_gpu_shot.py <stderr 日志> <PNG> # 正例:六条判据,0/1 退出
|
python packaging/check_gpu_shot.py <stderr 日志> <PNG> # 正例:六条判据,0/1 退出
|
||||||
python packaging/check_gpu_shot.py <stderr 日志> --expect-refuse [PNG] # 负例:拒绝降级出图是否成立
|
python packaging/check_gpu_shot.py <stderr 日志> --expect-refuse [PNG] # 负例:拒绝降级出图是否成立
|
||||||
python packaging/check_gpu_shot.py --self-test # 16 条向量(数据在 tests/gpu_shot_vectors.json)
|
python packaging/check_gpu_shot.py --self-test # 19 条向量(数据 tests/gpu_shot_vectors.json,脚手架 tests/gpu_shot_selftest.py)
|
||||||
```
|
```
|
||||||
|
|
||||||
退出码:`0` = 判据全过;`1` = 有判据没过(**逐条点名**,不只印 FAILED);`2` = 用法错(少参数 / 认不出的参数)。
|
退出码:`0` = 判据全过;`1` = 有判据没过(**逐条点名**,不只印 FAILED);`2` = 用法错(少参数 / 认不出的参数)。
|
||||||
@@ -32,7 +32,8 @@ python packaging/check_gpu_shot.py --self-test # 16 条向
|
|||||||
| A3b CPU 档重复 | `a3b-cpu-repeat.stderr.log + a3b-cpu-light.png` | **1** | 同上(重复性对照,结论一致) |
|
| A3b CPU 档重复 | `a3b-cpu-repeat.stderr.log + a3b-cpu-light.png` | **1** | 同上(重复性对照,结论一致) |
|
||||||
| A5 CPU 档 shell | `a5-cpu-shell.stderr.log + a5-cpu-shell.png` | **1** | 同上 |
|
| A5 CPU 档 shell | `a5-cpu-shell.stderr.log + a5-cpu-shell.png` | **1** | 同上 |
|
||||||
| A6 负例,按正例口径判 | `a6-negative-refuse.stderr.log + a6-negative-refuse.png` | **1** | **6 条全红**(其中 3 条是「PNG 不存在」——负例本来就没图) |
|
| A6 负例,按正例口径判 | `a6-negative-refuse.stderr.log + a6-negative-refuse.png` | **1** | **6 条全红**(其中 3 条是「PNG 不存在」——负例本来就没图) |
|
||||||
| A6 负例,`--expect-refuse` | `a6-negative-refuse.stderr.log --expect-refuse` | **0** | 3/3 过:`via=refuse`、无出图行、PNG 不存在 |
|
| A6 负例,`--expect-refuse`(传 PNG 路径) | `a6-negative-refuse.stderr.log --expect-refuse a6-negative-refuse.png` | **0** | **3/3** 过:`via=refuse`、无出图行、目标 PNG 不存在 |
|
||||||
|
| A6 负例,`--expect-refuse`(不传 PNG) | `a6-negative-refuse.stderr.log --expect-refuse` | **0** | **2/2** 过(判据 9 不登记:`--expect-refuse` 的 PNG 参数是可选的) |
|
||||||
| A1b 日志 + A1b 图 | `a1b-gpu-repeat.stderr.log + a1b-gpu-light.png` | **1** | 见 §5 第 2 条:这份日志没开 `ULL_GPU_LOG=info`,**没有** `Selected` 行 ⇒ 单独看它无法自证适配器 |
|
| A1b 日志 + A1b 图 | `a1b-gpu-repeat.stderr.log + a1b-gpu-light.png` | **1** | 见 §5 第 2 条:这份日志没开 `ULL_GPU_LOG=info`,**没有** `Selected` 行 ⇒ 单独看它无法自证适配器 |
|
||||||
| 串味:A1 日志 + A3 的 CPU 图 | `a1-gpu.stderr.log + a3-cpu-light.png` | **0** | **这是已知盲区**,见 §5 第 1 条(两张图同为 1180x760) |
|
| 串味:A1 日志 + A3 的 CPU 图 | `a1-gpu.stderr.log + a3-cpu-light.png` | **0** | **这是已知盲区**,见 §5 第 1 条(两张图同为 1180x760) |
|
||||||
|
|
||||||
@@ -45,8 +46,8 @@ A6 的负例形状(`WGPU_BACKEND=metal`):`Available adapters: []` → `wgp
|
|||||||
|
|
||||||
| # | 判据 | 抓得住 | 抓不住 |
|
| # | 判据 | 抓得住 | 抓不住 |
|
||||||
|---|------|--------|--------|
|
|---|------|--------|--------|
|
||||||
| 1 | `[shot] … renderer=wgpu` 存在 | 「其实走了 CPU 档」——CPU 档在这行写 `renderer=tiny-skia`(M97 起这行打的是**实际**后端,不是请求值,见 `crates/app-iced/src/renderer/mod.rs:52`) | 只证明这行被打印过;日志被手工拼装、或从别的进程拷来的行,它分不清(判据只读文本) |
|
| 1 | **全部** `[shot] … renderer=` 行都是 `wgpu`(`set(renderers) == {"wgpu"}`) | 「其实走了 CPU 档」——CPU 档在这行写 `renderer=tiny-skia`(M97 起这行打的是**实际**后端,不是请求值,见 `crates/app-iced/src/renderer/mod.rs:52`) | 只证明这些行被打印过;日志被手工拼装、或从别的进程拷来的行,它分不清(判据只读文本);「一次运行里混了两种 renderer」(`>>` 拼接、`via=respawn` 降级)**一律判红**,但**同种 renderer 的多次运行拼在一起**看不出来(§5 第 7 条) |
|
||||||
| 2 | `Selected: AdapterInfo { device_type: X }` 且 `X ∉ {Cpu, Other}` | 「wgpu 真建成了设备,而且选中的不是 WARP(`Cpu`) / Gl 兼容层(`Other`)」;与 `crates/app-iced/src/probe.rs:165-167` 的硬件判据同口径 | 选中的是**哪一块**卡不判(独显 vs 集显都算硬件);不看 `Available adapters` 那 12 条,只看 `Selected` 那条 —— 也就抓不住「枚举里有独显、实际选了集显」之外的更细的驱动行为 |
|
| 2 | `Selected: AdapterInfo { device_type: X }` 且 `X ∉ {Cpu, Other}`;**取不到 `X` 一律判红** | 「wgpu 真建成了设备,而且选中的不是 WARP(`Cpu`) / Gl 兼容层(`Other`)」;与 `crates/app-iced/src/probe.rs:165-167` 的硬件判据同口径 | 选中的是**哪一块**卡不判(独显 vs 集显都算硬件);不看 `Available adapters` 那 12 条,只看 `Selected` 那条 —— 也就抓不住「枚举里有独显、实际选了集显」之外的更细的驱动行为;**多行 Debug / 字段缺失**时不知道真值是啥,只按「取不到即判红」处理(宁可假红:iced 换成 pretty-print Debug 时真 GPU 图会被判红,得回来改正则) |
|
||||||
| 3 | PNG 能解码且像素数 > 0 | 0 字节 / 半截写入 / 根本不是 PNG(签名、IHDR、IDAT 逐层查) | 只管「能解出来」,不管内容对不对(那是判据 6 的事);不解调色板与隔行 PNG(沿用 `check_screenshot.py` 的同一条限制) |
|
| 3 | PNG 能解码且像素数 > 0 | 0 字节 / 半截写入 / 根本不是 PNG(签名、IHDR、IDAT 逐层查) | 只管「能解出来」,不管内容对不对(那是判据 6 的事);不解调色板与隔行 PNG(沿用 `check_screenshot.py` 的同一条限制) |
|
||||||
| 4 | PNG 宽高 = 日志 `[shot]` 声明的宽高 | 拿错文件的第一步:日志说 1180x760、图是别的尺寸 | **同尺寸互换抓不住**(见 §5 第 1 条);分辨率和内容是否同一帧也抓不住 |
|
| 4 | PNG 宽高 = 日志 `[shot]` 声明的宽高 | 拿错文件的第一步:日志说 1180x760、图是别的尺寸 | **同尺寸互换抓不住**(见 §5 第 1 条);分辨率和内容是否同一帧也抓不住 |
|
||||||
| 5 | `[backend] final=wgpu result=started` | 后端结论行与判据 1/2 自相矛盾(例如 `final=none … via=refuse`) | 需要 `ULL_ACCEPTANCE=1` 才有这行;只看这一行的话,A6 那种「一行都没出图」和「出了图但日志没写」分不清 |
|
| 5 | `[backend] final=wgpu result=started` | 后端结论行与判据 1/2 自相矛盾(例如 `final=none … via=refuse`) | 需要 `ULL_ACCEPTANCE=1` 才有这行;只看这一行的话,A6 那种「一行都没出图」和「出了图但日志没写」分不清 |
|
||||||
@@ -63,7 +64,8 @@ A6 的负例形状(`WGPU_BACKEND=metal`):`Available adapters: []` → `wgp
|
|||||||
| 变异 | 抓它的向量 | 结果 |
|
| 变异 | 抓它的向量 | 结果 |
|
||||||
|------|-----------|------|
|
|------|-----------|------|
|
||||||
| `DISALLOWED_DEVICE_TYPES` 里删掉 `Cpu`(= 允许 WARP 当 GPU) | 「renderer=wgpu 但 Selected 是 Cpu」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
| `DISALLOWED_DEVICE_TYPES` 里删掉 `Cpu`(= 允许 WARP 当 GPU) | 「renderer=wgpu 但 Selected 是 Cpu」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
||||||
| 删掉判据 1(不再要求 `renderer=wgpu`) | 「删掉 [shot] renderer 行」 | 由红转绿 ⇒ `--self-test` 退出 1 |
|
| 删掉判据 1(不再要求 `renderer=wgpu`) | 「删掉 [shot] renderer 行」、「日志里混入第二种 renderer」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
||||||
|
| 判据 2 不再把「取不到 device_type」当失败(恢复旧写法) | 「Selected 换成多行 Debug」、「`Selected: AdapterInfo {` 空体」 | 由红转绿 ⇒ 清单对不上 ⇒ `--self-test` 退出 1 |
|
||||||
|
|
||||||
## 4. 这份证据**能**证明什么
|
## 4. 这份证据**能**证明什么
|
||||||
|
|
||||||
@@ -106,9 +108,13 @@ A6 的负例形状(`WGPU_BACKEND=metal`):`Available adapters: []` → `wgp
|
|||||||
6. **换机器不成立。** 全部证据来自一台机器(RX 6600 XT / Raphael iGPU、12 个 adapter、RDP 断开会话)。
|
6. **换机器不成立。** 全部证据来自一台机器(RX 6600 XT / Raphael iGPU、12 个 adapter、RDP 断开会话)。
|
||||||
`VirtualGpu` 会不会被判成硬件只在**代码层**确认(`probe.rs:165-167` 会放行),**没在真 `VirtualGpu`
|
`VirtualGpu` 会不会被判成硬件只在**代码层**确认(`probe.rs:165-167` 会放行),**没在真 `VirtualGpu`
|
||||||
适配器上验过**(本机 12 条里一条都没有)。
|
适配器上验过**(本机 12 条里一条都没有)。
|
||||||
7. **交互启动那一支没验。** A6 验的是「取证启动(`--screenshot`)下拒绝降级出图」;交互启动的
|
7. **同种 renderer 的多次运行拼在一起,无法分辨图属于哪一次。** 判据 1 只挡住「混入**异种** renderer」
|
||||||
|
(wgpu + tiny-skia 拼一起);两次都是 `renderer=wgpu` 的日志 `>>` 到同一个文件时所有行都自洽,
|
||||||
|
判据 4 又只比尺寸 ⇒ 两张同为 1180×760 的 GPU 图互换仍然判绿。要堵它得把 PNG 的 sha256 / 运行 id
|
||||||
|
打进日志(改 Rust)。⇒ 一次取证 = 一份干净的 stderr,别往同一个文件里续写。
|
||||||
|
8. **交互启动那一支没验。** A6 验的是「取证启动(`--screenshot`)下拒绝降级出图」;交互启动的
|
||||||
`via=respawn`(换进程降级)要求真窗口,本轮没开。
|
`via=respawn`(换进程降级)要求真窗口,本轮没开。
|
||||||
8. **dark 主题、其它屏、`ULL_WINDOW_SIZE` 变尺寸、macOS / Linux** 全部没跑:只有 gallery + shell 的 light。
|
9. **dark 主题、其它屏、`ULL_WINDOW_SIZE` 变尺寸、macOS / Linux** 全部没跑:只有 gallery + shell 的 light。
|
||||||
|
|
||||||
## 6. 历史 `--diff` 基线:**永久不可复核**
|
## 6. 历史 `--diff` 基线:**永久不可复核**
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""`packaging/check_gpu_shot.py --self-test` 的执行器(M249):合成日志 + 合成 PNG + 向量比对。
|
||||||
|
|
||||||
|
为什么单独一份而不是塞进判据脚本:那一份要守 ≤400 行硬线,而这里全是「按真实行格式手拼日志、
|
||||||
|
现造 PNG、逐条对 `expect_fail`」的**测试脚手架**,与判据逻辑无关。数据另外放
|
||||||
|
`gpu_shot_vectors.json`(同 `guard_vectors.json` 的做法),被测的判据函数由调用方**以参数传入**
|
||||||
|
——本文件不反向 import `check_gpu_shot`,免得循环导入。
|
||||||
|
|
||||||
|
一颗诚实的钉子:本文件的样本是**按今天的日志格式**手拼的,所以它守**判据**,不守**格式漂移**。
|
||||||
|
产品真换了 Debug 形状时,靠的是判据里「取不到 device_type 就算失败」这类保守口径(见 doc §3)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # `check_screenshot` 在 packaging/
|
||||||
|
from check_screenshot import encode_png # noqa: E402
|
||||||
|
|
||||||
|
#: 向量数据。`log` 是 `synth_log` 的关键字覆盖(缺省键取函数默认值,`null` = 该行不出现)。
|
||||||
|
VECTORS_PATH = Path(__file__).resolve().parent / "gpu_shot_vectors.json"
|
||||||
|
|
||||||
|
SELFTEST_SIZE = (200, 120)
|
||||||
|
|
||||||
|
|
||||||
|
def synth_log(
|
||||||
|
*,
|
||||||
|
renderer: str | None = "wgpu",
|
||||||
|
device_type: str | None = "DiscreteGpu",
|
||||||
|
backend: str | None = "final=wgpu result=started",
|
||||||
|
size: tuple[int, int] | None = None,
|
||||||
|
no_size_line: bool = False,
|
||||||
|
multiline_selected: bool = False,
|
||||||
|
empty_selected: bool = False,
|
||||||
|
extra_shot_renderer: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""按 M246 真实日志的**行格式**拼一份最小日志(传 `None` = 该行故意不出现)。"""
|
||||||
|
lines = [
|
||||||
|
"[probe] renderer = wgpu reason=forced-gpu 兜底=tiny-skia(失败会先打印具名错误)",
|
||||||
|
# 真实日志里永远有这一行提示文本,且它含 `Selected: AdapterInfo` 字样 —— 留着它,
|
||||||
|
# 「只有提示、没有真 Selected 行」的向量才是在测真东西。
|
||||||
|
"[probe] 取证:出现 `[gpu] iced_wgpu: …Selected: AdapterInfo…` ⇒ GPU 真生效;出现 `[gpu] wgpu 初始化失败` ⇒ 已降级 CPU",
|
||||||
|
]
|
||||||
|
if empty_selected: # `Selected: AdapterInfo {` 之后再没有内容(字段全丢)
|
||||||
|
lines.append("[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo {")
|
||||||
|
elif device_type is not None:
|
||||||
|
fields = [
|
||||||
|
' name: "AMD Radeon RX 6600 XT",',
|
||||||
|
f" device_type: {device_type},",
|
||||||
|
' backend: Vulkan,',
|
||||||
|
]
|
||||||
|
if multiline_selected: # pretty-print Debug:字段不在 `{` 那一行(P2-1 的形状)
|
||||||
|
lines.append("[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo {")
|
||||||
|
lines.extend(fields)
|
||||||
|
lines.append("}")
|
||||||
|
else:
|
||||||
|
lines.append("[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo {" + "".join(fields) + " }")
|
||||||
|
if not no_size_line:
|
||||||
|
dims = size or SELFTEST_SIZE
|
||||||
|
lines.append(f"[shot] {dims[0]}x{dims[1]} scale=1 -> selftest.png")
|
||||||
|
for value in (renderer, extra_shot_renderer):
|
||||||
|
if value is not None:
|
||||||
|
lines.append(f'[shot] screen=gallery theme=light renderer={value} route=Detail("survival")')
|
||||||
|
if backend is not None:
|
||||||
|
lines.append(f"[backend] {backend} elapsed=1000ms evidence=selftest")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def synth_png(kind: str, size: tuple[int, int]) -> bytes:
|
||||||
|
"""`content` = 有内容的合成帧(渐变 + 8px 棋盘格);`flat` = 全白。"""
|
||||||
|
width, height = size
|
||||||
|
|
||||||
|
def content_row(y: int) -> bytes:
|
||||||
|
return b"".join(
|
||||||
|
bytes((x * 255 // max(width - 1, 1), y * 255 // max(height - 1, 1), 40))
|
||||||
|
if ((x // 8) + (y // 8)) % 2 == 0
|
||||||
|
else b"\x14\x14\x14"
|
||||||
|
for x in range(width)
|
||||||
|
)
|
||||||
|
|
||||||
|
if kind == "flat":
|
||||||
|
return encode_png(width, height, lambda _y: b"\xff" * (width * 3))
|
||||||
|
return encode_png(width, height, content_row)
|
||||||
|
|
||||||
|
|
||||||
|
def run_vector(root: Path, index: int, case: dict, default_size: tuple[int, int], judges: tuple) -> list[str]:
|
||||||
|
"""跑一条向量,返回它实际红的判据清单。`judges` = (judge_evidence, judge_refuse)。"""
|
||||||
|
judge_evidence, judge_refuse = judges
|
||||||
|
overrides = {key: (tuple(value) if isinstance(value, list) else value) for key, value in case.get("log", {}).items()}
|
||||||
|
log_path = root / f"case{index:02d}.log"
|
||||||
|
log_path.write_text(synth_log(**{"size": default_size, **overrides}), encoding="utf-8")
|
||||||
|
png_path = root / f"case{index:02d}.png"
|
||||||
|
png_size = tuple(case.get("png_size", default_size))
|
||||||
|
if case.get("png") == "truncated":
|
||||||
|
png_path.write_bytes(synth_png("content", png_size)[:200])
|
||||||
|
elif case.get("png") is not None:
|
||||||
|
png_path.write_bytes(synth_png(case["png"], png_size))
|
||||||
|
report = judge_refuse(log_path, png_path) if case.get("mode") == "refuse" else judge_evidence(log_path, png_path)
|
||||||
|
return report.problems()
|
||||||
|
|
||||||
|
|
||||||
|
def run_selftest(judge_evidence, judge_refuse) -> int:
|
||||||
|
"""跑 `gpu_shot_vectors.json` 的全部向量,逐条对 `expect_fail`(多一条少一条都算失败)。"""
|
||||||
|
data = json.loads(VECTORS_PATH.read_text(encoding="utf-8"))
|
||||||
|
default_size = tuple(data["size"])
|
||||||
|
failures = 0
|
||||||
|
with tempfile.TemporaryDirectory(prefix="ull-gpu-shot-") as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
for index, case in enumerate(data["cases"], start=1):
|
||||||
|
got = run_vector(root, index, case, default_size, (judge_evidence, judge_refuse))
|
||||||
|
want = case["expect_fail"]
|
||||||
|
if got != want:
|
||||||
|
print(f"::error::自测失败:{case['label']} 期望失败 {want},实际 {got}", file=sys.stderr)
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
print(f"OK: {case['label']} → {'红 ' + ' / '.join(got) if got else '绿'}")
|
||||||
|
red = sum(1 for case in data["cases"] if case["expect_fail"])
|
||||||
|
if failures:
|
||||||
|
return 1
|
||||||
|
print(f"OK: GPU 截图证据判定自测通过({len(data['cases'])} 条向量:红 {red} / 绿 {len(data['cases']) - red})")
|
||||||
|
return 0
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"note": "M249 check_gpu_shot.py --self-test 的向量。log = synth_log 的关键字覆盖(缺省键取脚本默认值,null = 该行不出现);png = content/flat/truncated/null(null = 不写文件);mode = evidence/refuse;expect_fail = **期望的失败判据清单(逐条对,多一条少一条都算自测失败)**——判据被关掉或被别的东西掩盖,都在这里现形。PNG 由 check_screenshot.encode_png 现造,不进仓库。",
|
"note": "M249 check_gpu_shot.py --self-test 的向量。log = synth_log 的关键字覆盖(缺省键取函数默认值;null = 该行不出现;no_size_line = 只省掉 [shot] WxH 那一行;multiline_selected / empty_selected = `Selected: AdapterInfo {` 的字段换了形状;extra_shot_renderer = 再追加一行异种 renderer 的 [shot] 行);png = content/flat/truncated/null(null = 不写文件);mode = evidence/refuse;expect_fail = **期望的失败判据清单(逐条对,多一条少一条都算自测失败)**——判据被关掉或被别的东西掩盖,都在这里现形。PNG 由 check_screenshot.encode_png 现造,不进仓库。",
|
||||||
"size": [200, 120],
|
"size": [200, 120],
|
||||||
"cases": [
|
"cases": [
|
||||||
{
|
{
|
||||||
@@ -25,12 +25,30 @@
|
|||||||
"png": "content",
|
"png": "content",
|
||||||
"expect_fail": ["Selected: AdapterInfo 的 device_type 不是 Cpu/Other"]
|
"expect_fail": ["Selected: AdapterInfo 的 device_type 不是 Cpu/Other"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "变异靶子:Selected 换成多行 Debug(字段不在 { 那一行)",
|
||||||
|
"log": { "multiline_selected": true },
|
||||||
|
"png": "content",
|
||||||
|
"expect_fail": ["Selected: AdapterInfo 的 device_type 不是 Cpu/Other"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "变异靶子:`Selected: AdapterInfo {` 空体(字段全丢)",
|
||||||
|
"log": { "device_type": null, "empty_selected": true },
|
||||||
|
"png": "content",
|
||||||
|
"expect_fail": ["Selected: AdapterInfo 的 device_type 不是 Cpu/Other"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "变异靶子:删掉 [shot] renderer 行(只剩 Selected + backend)",
|
"label": "变异靶子:删掉 [shot] renderer 行(只剩 Selected + backend)",
|
||||||
"log": { "renderer": null },
|
"log": { "renderer": null },
|
||||||
"png": "content",
|
"png": "content",
|
||||||
"expect_fail": ["截图行 renderer=wgpu"]
|
"expect_fail": ["截图行 renderer=wgpu"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "变异靶子:日志里混入第二种 renderer(CPU 那次 >> 拼进来)",
|
||||||
|
"log": { "extra_shot_renderer": "tiny-skia" },
|
||||||
|
"png": "content",
|
||||||
|
"expect_fail": ["截图行 renderer=wgpu"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "回归:CPU 档出的图(renderer=tiny-skia)",
|
"label": "回归:CPU 档出的图(renderer=tiny-skia)",
|
||||||
"log": { "renderer": "tiny-skia", "device_type": null, "backend": "final=tiny-skia result=started" },
|
"log": { "renderer": "tiny-skia", "device_type": null, "backend": "final=tiny-skia result=started" },
|
||||||
@@ -72,14 +90,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "负例基线:via=refuse + 无出图行 + 无 PNG",
|
"label": "负例基线:via=refuse + 无出图行 + 无 PNG",
|
||||||
"log": { "renderer": null, "device_type": null, "size": null, "backend": "final=none result=failed via=refuse" },
|
"log": { "renderer": null, "device_type": null, "no_size_line": true, "backend": "final=none result=failed via=refuse" },
|
||||||
"png": null,
|
"png": null,
|
||||||
"mode": "refuse",
|
"mode": "refuse",
|
||||||
"expect_fail": []
|
"expect_fail": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "负例回归:拒绝日志旁边却躺着一张 PNG",
|
"label": "负例回归:拒绝日志旁边却躺着一张 PNG",
|
||||||
"log": { "renderer": null, "device_type": null, "size": null, "backend": "final=none result=failed via=refuse" },
|
"log": { "renderer": null, "device_type": null, "no_size_line": true, "backend": "final=none result=failed via=refuse" },
|
||||||
"png": "content",
|
"png": "content",
|
||||||
"mode": "refuse",
|
"mode": "refuse",
|
||||||
"expect_fail": ["目标 PNG 不存在(拒绝出图就该一张都没有)"]
|
"expect_fail": ["目标 PNG 不存在(拒绝出图就该一张都没有)"]
|
||||||
|
|||||||
Reference in new issue
Block a user