Синхронизация upstream и оптимизация постобработки MoonWell (#12)

Синхронизация с 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-проходов и запуск движка с данными и без. Проверки отсутствующих аддонов и четырёх кинороликов явно пропущены. Живой вход/выход на сервер не выполнялся.
This commit was merged in pull request #12.
This commit is contained in:
2026-11-04 12:55:09 +03:00
parent 25c0f4ced7
commit aa799a2c7d
25 changed files with 671 additions and 218 deletions
+112
View File
@@ -0,0 +1,112 @@
#!/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)