aa799a2c7d
Синхронизация с upstream до e2aeaecf: исправления AccessKit на Windows, прокрутки трекпада на Linux и анимации моделей в меню.
MoonWell: расчёт рассеяния, вторичного освещения и AO перенесён в половинное разрешение с восстановлением по глубине. В контрольной сцене RTX 5070 суммарное время постобработки снизилось с 6,41 до 3,47 мс. При выключенном расширении изображение совпадает с исходным. Добавлен анализатор диагностических логов GPU.
Исправлена подготовка тестов интерфейса и обработка отсутствующих кинороликов в сокращённой установке. Уточнены причины существующих отклонений русского пакета и HD-атласа.
Проверки: 46 тестов графического модуля и сравнения дневных/ночных кадров пройдены; полный цикл scripts/gates.sh пройден: fmt, Clippy workspace и player, reference, тесты с установкой и без неё, документация, каталог сообщений, проверка GPU-проходов и запуск движка с данными и без. Проверки отсутствующих аддонов и четырёх кинороликов явно пропущены. Живой вход/выход на сервер не выполнялся.
113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Summarize GPU readback means by session and settings epoch, without mixing toggles.
|
|
|
|
Usage: python scripts/analyze-gfx-diagnostics.py Diagnostics [another/Diagnostics]
|
|
Pass times are weighted by readback frames, not by wall-clock seconds. CPU schedule
|
|
phases overlap worker/render work and must not be summed into a frame time.
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import csv
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
def percentile(values, fraction):
|
|
values = sorted(values)
|
|
return values[int((len(values) - 1) * fraction)] if values else None
|
|
|
|
|
|
def summarize(root):
|
|
text = (root / "gpu-passes.log").read_text(encoding="utf-8-sig")
|
|
epochs = []
|
|
current = {}
|
|
for line in text.splitlines():
|
|
session = re.search(r"session=(\S+)", line)
|
|
if not session:
|
|
continue
|
|
session = session[1]
|
|
if " tag mwgfx=" in line:
|
|
settings = dict(re.findall(r"([\w.]+)=([^\s]+)", line.split("tag mwgfx=", 1)[1]))
|
|
epoch = {"session": session, "settings": settings, "samples": []}
|
|
current[session] = epoch
|
|
epochs.append(epoch)
|
|
if " GPU_PASSES " not in line:
|
|
continue
|
|
pairs = dict(re.findall(r"(\w+)=([\d.]+)", line.split("GPU_PASSES", 1)[1]))
|
|
frames = int(pairs.pop("frames", 0))
|
|
if frames <= 0:
|
|
continue
|
|
if session not in current:
|
|
current[session] = {"session": session, "settings": {}, "samples": []}
|
|
epochs.append(current[session])
|
|
current[session]["samples"].append((frames, {k: float(v) for k, v in pairs.items()}))
|
|
|
|
result = {"source": str(root), "adapters": sorted(set(re.findall(
|
|
r"# benilla fps journal[^\n]+", text))), "epochs": []}
|
|
for epoch in epochs:
|
|
samples = epoch.pop("samples")
|
|
if not samples:
|
|
continue
|
|
n = sum(w for w, _ in samples)
|
|
totals = collections.defaultdict(float)
|
|
for weight, passes in samples:
|
|
for name, ms in passes.items():
|
|
totals[name] += weight * ms
|
|
mean = {k: v / n for k, v in totals.items()}
|
|
epoch.update(readbacks=len(samples), frames=n,
|
|
mean_ms=dict(sorted(mean.items(), key=lambda item: -item[1])),
|
|
total_measured_ms=sum(mean.values()))
|
|
result["epochs"].append(epoch)
|
|
|
|
# CSV may contain repeated headers and adapter banners from multiple launches.
|
|
header = None
|
|
fps = collections.defaultdict(list)
|
|
path = root / "fps-journal.csv"
|
|
if path.exists():
|
|
for line in path.read_text(encoding="utf-8-sig").splitlines():
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
row = next(csv.reader([line]))
|
|
if row[0] == "t":
|
|
header = row
|
|
continue
|
|
if header is None:
|
|
continue
|
|
for key, value in zip(header, row):
|
|
if key not in ("mean_ms", "main_ms", "gpu_ms", "p95_ms"):
|
|
continue
|
|
try:
|
|
fps[key].append(float(value))
|
|
except ValueError:
|
|
pass
|
|
result["journal_ms"] = {key: {"samples": len(values),
|
|
"median": percentile(values, .5), "p95": percentile(values, .95)}
|
|
for key, values in fps.items()}
|
|
phases = collections.defaultdict(list)
|
|
path = root / "cpu-phases.csv"
|
|
if path.exists():
|
|
lines = (line for line in path.read_text(encoding="utf-8-sig").splitlines()
|
|
if line and not line.startswith("#"))
|
|
for row in csv.DictReader(lines):
|
|
try:
|
|
phases[row["phase"]].append(float(row["wall_ms"]))
|
|
except (ValueError, KeyError, TypeError):
|
|
pass
|
|
result["cpu_phase_wall_ms"] = {key: {"median": percentile(values, .5),
|
|
"p95": percentile(values, .95)} for key, values in phases.items()}
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("folders", nargs="+", type=Path)
|
|
parser.add_argument("--output", type=Path, help="Write a UTF-8 JSON report")
|
|
args = parser.parse_args()
|
|
report = json.dumps([summarize(root) for root in args.folders], indent=2)
|
|
if args.output:
|
|
args.output.write_text(report + "\n", encoding="utf-8")
|
|
else:
|
|
print(report)
|