Repository navigation
Expand file tree
/
Copy pathparse.py
More file actions
236 lines (221 loc) · 10.8 KB
/
Copy pathparse.py
File metadata and controls
236 lines (221 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""
parse.py - Handles ALL PresentMon formats + MangoHud -> YAML
Supports:
1. PresentMon SUMMARY (your screenshot): Duration, Total Frames, Average FPS, Minimum FPS, 1st Percentile FPS, 5th Percentile FPS, Maximum FPS, AnimationErrorPerSecond
2. PresentMon DETAILED: MSBetweenPresents, Dropped
3. MangoHud: frametime
Driver is OPTIONAL for community PRs per your request.
form_factor supported: desktop, laptop, handheld, igpu
"""
import argparse, csv, sys
from datetime import datetime
from pathlib import Path
try:
import yaml
except ImportError:
print("pip install -r requirements.txt"); sys.exit(1)
import checks
try:
import numpy as np
HAS_NUMPY=True
except ImportError:
HAS_NUMPY=False
def percentile(data, p):
if not data: return 0
if HAS_NUMPY:
return float(np.percentile(data, p))
s=sorted(data)
k=(len(s)-1)*(p/100)
f=int(k); c=min(f+1,len(s)-1)
return float(s[f] if f==c else s[f]*(c-k)+s[c]*(k-f))
def detect_and_load(path):
with open(path, newline='', encoding='utf-8', errors='ignore') as f:
reader = csv.DictReader(f)
headers = [h.strip() for h in (reader.fieldnames or [])]
h_lower = [h.lower().strip() for h in headers]
joined = " ".join(h_lower)
# Format 1: SUMMARY - your screenshot
if "average fps" in joined and "1st percentile" in joined:
for row in reader:
# handle case insensitive keys
def get_key(target):
for h in headers:
if h.lower().strip() == target:
return row.get(h)
return None
try:
avg = float(get_key("average fps"))
p1 = float(get_key("1st percentile fps"))
p5 = None
if "5th percentile fps" in joined:
try: p5 = float(get_key("5th percentile fps"))
except: pass
min_f = None
if "minimum fps" in joined:
try: min_f = float(get_key("minimum fps"))
except: pass
max_f = None
if "maximum fps" in joined:
try: max_f = float(get_key("maximum fps"))
except: pass
duration = None
if "duration" in joined:
try: duration = float(get_key("duration"))
except: pass
total_frames = 0
if "total frames" in joined:
try: total_frames = int(float(get_key("total frames")))
except: pass
anim = None
if "animationerrorpersecond" in joined:
try: anim = float(get_key("animationerrorpersecond"))
except: pass
return {
"format": "presentmon_summary",
"avg": avg, "p1": p1, "p5": p5, "min": min_f, "max": max_f,
"duration": duration, "anim_err": anim, "frame_count": total_frames
}
except Exception as e:
raise ValueError(f"Failed to parse summary CSV: {e} Headers: {headers}")
# Format 2: Detailed PresentMon
if "msbetweenpresents" in joined:
fps_vals=[]
f.seek(0)
reader2 = csv.DictReader(f)
ms_key = next((k for k in reader2.fieldnames if "msbetweenpresents" in k.lower()), None)
drop_key = next((k for k in reader2.fieldnames if k.lower()=="dropped"), None)
for row in reader2:
try:
if drop_key and row.get(drop_key)=="1": continue
ms=float(row[ms_key])
if ms<=0 or ms>500: continue
fps_vals.append(1000.0/ms)
except: continue
return {
"format": "presentmon_detailed",
"avg": sum(fps_vals)/len(fps_vals) if fps_vals else 0,
"p1": percentile(fps_vals,1),
"p01": percentile(fps_vals,0.1),
"frame_count": len(fps_vals)
}
# Format 3: MangoHud. Logs start with an os,cpu,gpu,...,kernel,driver preamble
# (a header line and a value line) before the frametime header; older logs don't.
with open(path, newline='', encoding='utf-8', errors='ignore') as f:
rows = list(csv.reader(f))
header_index = next((i for i, row in enumerate(rows[:10]) if any(c.strip().lower() == "frametime" for c in row)), None)
if header_index is not None:
preamble = {}
if header_index >= 2:
preamble = {k.strip().lower(): v.strip() for k, v in zip(rows[0], rows[1])}
header = [c.strip().lower() for c in rows[header_index]]
ft_index = header.index("frametime")
fps_vals=[]
for row in rows[header_index+1:]:
try:
ms=float(row[ft_index])
if ms<=0 or ms>500: continue
fps_vals.append(1000.0/ms)
except: continue
return {
"format": "mangohud",
"avg": sum(fps_vals)/len(fps_vals) if fps_vals else 0,
"p1": percentile(fps_vals,1),
"p01": percentile(fps_vals,0.1),
"frame_count": len(fps_vals),
"kernel": preamble.get("kernel") or None,
"driver": preamble.get("driver") or None,
}
raise ValueError(f"Unknown CSV format. Headers: {headers}")
def main():
p=argparse.ArgumentParser(description="PresentMon/MangoHud -> YAML")
p.add_argument("csv_file")
p.add_argument("--gpu-id", required=True, help="Exact catalog ID, the file name under data/gpus/<form_factor>/<gpu_vendor>/")
p.add_argument("--game-id", required=True, help="The game's ID in data/games.yaml, such as cyberpunk_2077; add a new game there first")
p.add_argument("--submitted-by", default="", help="OPTIONAL - your GitHub username; leave it out to stay anonymous")
p.add_argument("--resolution", default="1440p")
p.add_argument("--graphics-preset", default="Ultra")
p.add_argument("--form-factor", default=None, choices=["desktop","laptop","handheld","igpu"], help="Defaults to the catalog value")
p.add_argument("--driver", default="", help="OPTIONAL - not required for PR approval")
p.add_argument("--method", default=None, choices=list(checks.PROOF_METHODS), help="Capture tool, written to proof.method. Defaults to presentmon or mangohud from the CSV format; pass it when another tool (for example capframex) made the CSV")
p.add_argument("--os", default=None, choices=list(checks.RESULT_OSES), help="Defaults to linux for MangoHud captures, windows for PresentMon")
p.add_argument("--os-detail", default="", help="OPTIONAL - full OS name, e.g. 'Windows 11 Pro 24H2' or 'SteamOS 3.6.19'")
p.add_argument("--testbench", default="", help="Official desktop runs only: the test bench ID from data/testbenches.yaml")
p.add_argument("--date", default="", help="OPTIONAL - the day you ran it, YYYY-MM-DD; defaults to today. Sets date and the file name")
p.add_argument("--output", default="")
args=p.parse_args()
catalog={entry["id"]: entry for entry in checks.load_catalog()[0]}
if args.gpu_id not in catalog:
print(f"Unknown --gpu-id '{args.gpu_id}'. Use the exact file name of a catalog entry under data/gpus/.")
sys.exit(1)
form_factor=args.form_factor or checks.spec(catalog[args.gpu_id], "classification", "form_factor")
game_ids=[game["id"] for game in checks.load_games()[0]]
if args.game_id not in game_ids:
print(f"Unknown --game-id '{args.game_id}'. Known IDs: {', '.join(game_ids)}.")
print("If it's a new game, add it to data/games.yaml (id, name, type, year) in the same pull request.")
sys.exit(1)
csv_path=Path(args.csv_file)
result=detect_and_load(csv_path)
os_name=args.os
if os_name is None:
os_name="linux" if result["format"]=="mangohud" else "windows"
capture="MangoHud" if result["format"]=="mangohud" else "PresentMon"
print(f"OS: {os_name} (from the {capture} capture; pass --os to change)")
system={"os": os_name}
if args.os_detail: system["os_detail"]=args.os_detail
driver=args.driver or result.get("driver")
if driver: system["driver"]=driver
if os_name=="linux" and result.get("kernel"): system["kernel"]=result["kernel"]
try:
today = datetime.strptime(args.date, "%Y-%m-%d") if args.date else datetime.now()
except ValueError:
print(f"--date '{args.date}' must be a real day written YYYY-MM-DD, such as 2026-10-03.")
sys.exit(1)
data={
"gpu_id": args.gpu_id,
"benchmark_type": "game",
"game_id": args.game_id,
# The file name's YYYYMMDD below; both use --date or today.
"date": today.strftime("%Y-%m-%d"),
"form_factor": form_factor,
"result": [{
"avg_fps": round(float(result["avg"]),2),
"p1_low": round(float(result["p1"]),2),
"resolution": args.resolution,
"graphics_preset": args.graphics_preset,
"p5_low": round(float(result["p5"]),2) if result.get("p5") else None,
"p01_low": round(float(result["p01"]),2) if result.get("p01") else None,
"min_fps": round(float(result["min"]),2) if result.get("min") else None,
"max_fps": round(float(result["max"]),2) if result.get("max") else None,
"duration": result.get("duration"),
"anim_error_per_sec": result.get("anim_err"),
"frame_count": result.get("frame_count")
}],
"system": system,
"overclock": {"is_oc": False, "power_limit_percent": 100},
"proof": {
"method": args.method or ("mangohud" if result["format"]=="mangohud" else "presentmon"),
"raw_log": str(csv_path.name),
"format": result["format"],
},
}
data["result"][0]={k:v for k,v in data["result"][0].items() if v is not None}
if args.submitted_by:
data={"gpu_id": data.pop("gpu_id"), "submitted_by": args.submitted_by, **data}
if args.testbench:
data={"gpu_id": data.pop("gpu_id"), "testbench_id": args.testbench, **data}
if not args.output:
date = today.strftime("%Y%m%d")
folder = Path(f"data/community/{args.gpu_id}")
out = folder / f"result_{date}_{args.game_id}.yaml"
repeat = 2
while out.exists():
out = folder / f"result_{date}_{args.game_id}_{repeat}.yaml"
repeat += 1
else:
out=Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
with open(out,"w") as f:
yaml.safe_dump(data,f,sort_keys=False)
print(f"Detected: {result['format']} | Avg: {result['avg']:.2f} | 1%: {result['p1']:.2f} -> {out}")
if __name__=="__main__":
main()