| e8555c89 Andy McCall |
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 |
from __future__ import annotations |
| 36 |
|
| 37 |
import argparse |
| 38 |
import collections |
| 39 |
import glob |
| 40 |
import os |
| 41 |
import re |
| 42 |
import sys |
| 43 |
|
| 44 |
TICKS = 1368 |
| 45 |
DUMMY_CODE, DUMMY_ADDR = 'R..', 0x1FFFF |
| 46 |
CPU_CODES = ('R.r', 'W.w') |
| 47 |
VRAM_LINE = 128 |
| 48 |
|
| 49 |
|
| 50 |
|
| 51 |
|
| 52 |
|
| 53 |
|
| 54 |
|
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
|
| 59 |
|
| 60 |
|
| 61 |
SLOTS_DISP_OFF = [ |
| 62 |
0, 8, 16, 24, 32, 40, 48, 56, 64, 72, |
| 63 |
80, 88, 96, 104, 112, 120, 164, 172, 180, 188, |
| 64 |
196, 204, 212, 220, 228, 236, 244, 252, 260, 268, |
| 65 |
276, 292, 300, 308, 316, 324, 332, 340, 348, 356, |
| 66 |
364, 372, 380, 388, 396, 404, 420, 428, 436, 444, |
| 67 |
452, 460, 468, 476, 484, 492, 500, 508, 516, 524, |
| 68 |
532, 548, 556, 564, 572, 580, 588, 596, 604, 612, |
| 69 |
620, 628, 636, 644, 652, 660, 676, 684, 692, 700, |
| 70 |
708, 716, 724, 732, 740, 748, 756, 764, 772, 780, |
| 71 |
788, 804, 812, 820, 828, 836, 844, 852, 860, 868, |
| 72 |
876, 884, 892, 900, 908, 916, 932, 940, 948, 956, |
| 73 |
964, 972, 980, 988, 996, 1004, 1012, 1020, 1028, 1036, |
| 74 |
1044, 1060, 1068, 1076, 1084, 1092, 1100, 1108, 1116, 1124, |
| 75 |
1132, 1140, 1148, 1156, 1164, 1172, 1188, 1196, 1204, 1212, |
| 76 |
1220, 1228, 1268, 1276, 1284, 1292, 1300, 1308, 1316, 1324, |
| 77 |
1334, 1344, 1352, 1360, |
| 78 |
] |
| 79 |
SLOTS_SPR_OFF = [ |
| 80 |
6, 14, 22, 30, 38, 46, 54, 62, 70, 78, |
| 81 |
86, 94, 102, 110, 118, 162, 170, 182, 188, 214, |
| 82 |
220, 246, 252, 278, 310, 316, 342, 348, 374, 380, |
| 83 |
406, 438, 444, 470, 476, 502, 508, 534, 566, 572, |
| 84 |
598, 604, 630, 636, 662, 694, 700, 726, 732, 758, |
| 85 |
764, 790, 822, 828, 854, 860, 886, 892, 918, 950, |
| 86 |
956, 982, 988, 1014, 1020, 1046, 1078, 1084, 1110, 1116, |
| 87 |
1142, 1148, 1174, 1206, 1212, 1266, 1274, 1282, 1290, 1298, |
| 88 |
1306, 1314, 1322, 1332, 1342, 1350, 1358, 1366, |
| 89 |
] |
| 90 |
SLOTS_SPR_ON = [ |
| 91 |
28, 92, 162, 170, 188, 220, 252, 316, 348, 380, |
| 92 |
444, 476, 508, 572, 604, 636, 700, 732, 764, 828, |
| 93 |
860, 892, 956, 988, 1020, 1084, 1116, 1148, 1212, 1264, |
| 94 |
1330, |
| 95 |
] |
| 96 |
TABLES = {'dispOff': SLOTS_DISP_OFF, 'sprOff': SLOTS_SPR_OFF, |
| 97 |
'sprOn': SLOTS_SPR_ON} |
| 98 |
|
| 99 |
|
| 100 |
PUBLISHED_TABLES = { |
| 101 |
'dispOff': [1325 if s == 1324 else 1335 if s == 1334 else s |
| 102 |
for s in SLOTS_DISP_OFF], |
| 103 |
'sprOff': [1323 if s == 1322 else 1333 if s == 1332 else s |
| 104 |
for s in SLOTS_SPR_OFF], |
| 105 |
'sprOn': SLOTS_SPR_ON, |
| 106 |
} |
| 107 |
|
| 108 |
|
| 109 |
def table(mode): |
| 110 |
return (PUBLISHED_TABLES if PUBLISHED[0] else TABLES)[mode] |
| 111 |
|
| 112 |
|
| 113 |
|
| 114 |
RELABEL = {'dispOff': {1325: 1324, 1335: 1334}, |
| 115 |
'sprOff': {1323: 1322, 1333: 1332}, |
| 116 |
'sprOn': {}} |
| 117 |
|
| 118 |
|
| 119 |
|
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
|
| 126 |
PAD = {'dispOff': ((1334, 2), (1344, 2)), |
| 127 |
'sprOff': ((1332, 2), (1342, 2)), |
| 128 |
'sprOn': ((1330, 2), (1337, 1), (1348, 1))} |
| 129 |
|
| 130 |
|
| 131 |
ROLES = { |
| 132 |
'hmmv': ('W',), |
| 133 |
'lmmv': ('R', 'W'), |
| 134 |
'ymmm': ('R', 'W'), |
| 135 |
'hmmm': ('R', 'W'), |
| 136 |
'line': ('R', 'W'), |
| 137 |
'lmmm': ('Rs', 'Rd', 'W'), |
| 138 |
} |
| 139 |
|
| 140 |
|
| 141 |
OPENMSX = { |
| 142 |
('hmmv', 'W->W'): 48, ('hmmv', 'newline'): 104, |
| 143 |
('lmmv', 'R->W'): 24, ('lmmv', 'W->R'): 72, ('lmmv', 'newline'): 134, |
| 144 |
('ymmm', 'R->W'): 24, ('ymmm', 'W->R'): 38, ('ymmm', 'newline'): 104, |
| 145 |
('hmmm', 'R->W'): 24, ('hmmm', 'W->R'): 64, ('hmmm', 'newline'): 128, |
| 146 |
('lmmm', 'Rd->W'): 24, ('lmmm', 'Rs->Rd'): 32, ('lmmm', 'W->Rs'): 64, |
| 147 |
('lmmm', 'newline'): 128, |
| 148 |
('line', 'R->W'): 24, ('line', 'W->R'): 88, ('line', 'newline'): 120, |
| 149 |
} |
| 150 |
OPENMSX_SPR_ON = {('lmmm', 'Rs->Rd'): 48, ('hmmm', 'newline'): 134, |
| 151 |
('lmmm', 'newline'): 134} |
| 152 |
|
| 153 |
|
| 154 |
|
| 155 |
FITTED = { |
| 156 |
('hmmv', 'W->W'): 46, ('hmmv', 'newline'): 104, |
| 157 |
('lmmv', 'R->W'): 24, ('lmmv', 'W->R'): 72, ('lmmv', 'newline'): 130, |
| 158 |
('ymmm', 'R->W'): 24, ('ymmm', 'W->R'): 36, ('ymmm', 'newline'): 104, |
| 159 |
('hmmm', 'R->W'): 24, ('hmmm', 'W->R'): 60, ('hmmm', 'newline'): 128, |
| 160 |
('lmmm', 'Rd->W'): 24, ('lmmm', 'Rs->Rd'): 32, ('lmmm', 'W->Rs'): 60, |
| 161 |
('lmmm', 'newline'): 128, |
| 162 |
('line', 'R->W'): 24, ('line', 'W->R'): 84, ('line', 'newline'): 120, |
| 163 |
} |
| 164 |
|
| 165 |
|
| 166 |
|
| 167 |
TIGHT = {m: {s for s in t if (s - 6) in set(t)} for m, t in TABLES.items()} |
| 168 |
|
| 169 |
|
| 170 |
|
| 171 |
SPR_ON_EXTRA = 1 |
| 172 |
|
| 173 |
PLAIN = [False] |
| 174 |
TIGHT_RULE = [True] |
| 175 |
PUBLISHED = [False] |
| 176 |
|
| 177 |
|
| 178 |
def V(mode, t): |
| 179 |
|
| 180 |
|
| 181 |
|
| 182 |
|
| 183 |
|
| 184 |
|
| 185 |
if PLAIN[0]: |
| 186 |
return t |
| 187 |
line, c = divmod(t, TICKS) |
| 188 |
pad = PAD[mode] |
| 189 |
return t - (sum(n for _, n in pad) * line |
| 190 |
+ sum(n for a, n in pad if c >= a)) |
| 191 |
|
| 192 |
|
| 193 |
def meta(path): |
| 194 |
|
| 195 |
name = os.path.basename(path) |
| 196 |
if not name.endswith('.txt'): |
| 197 |
return None |
| 198 |
parts = name[:-4].split('-') |
| 199 |
if len(parts) < 5 or parts[0] != 'scr5' or parts[1] not in TABLES: |
| 200 |
return None |
| 201 |
if not re.fullmatch(r'\d+[a-z]?', parts[-1]): |
| 202 |
return None |
| 203 |
return {'mode': parts[1], 'cmd': parts[2], 'cpu': parts[3], |
| 204 |
'variant': parts[4:-1], 'name': name} |
| 205 |
|
| 206 |
|
| 207 |
def parse(path): |
| 208 |
|
| 209 |
|
| 210 |
|
| 211 |
|
| 212 |
|
| 213 |
|
| 214 |
rel = {} if PUBLISHED[0] else RELABEL[meta(path)['mode']] |
| 215 |
items = [] |
| 216 |
with open(path) as f: |
| 217 |
for ln in f: |
| 218 |
if ':' not in ln: |
| 219 |
continue |
| 220 |
row, rest = ln.rstrip('\n').split(':', 1) |
| 221 |
t = int(row) |
| 222 |
col = 0 |
| 223 |
while rest: |
| 224 |
cell = rest[2:13] |
| 225 |
if cell.strip(): |
| 226 |
line, c = divmod(TICKS * col + t - 1, TICKS) |
| 227 |
items.append((line * TICKS + rel.get(c, c), |
| 228 |
cell[0:3], int(cell[6:11], 16))) |
| 229 |
rest = rest[13:] |
| 230 |
col += 1 |
| 231 |
items.sort() |
| 232 |
return items |
| 233 |
|
| 234 |
|
| 235 |
def transitions(path): |
| 236 |
|
| 237 |
info = meta(path) |
| 238 |
if info is None or info['cmd'] not in ROLES: |
| 239 |
return |
| 240 |
roles = ROLES[info['cmd']] |
| 241 |
acc = [a for a in parse(path) if a[1] not in CPU_CODES] |
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
real = any(a[2] != DUMMY_ADDR and abs(a[2] - DUMMY_ADDR) < 0x400 |
| 246 |
for a in acc) |
| 247 |
eng = [a for a in acc if real or (a[1], a[2]) != (DUMMY_CODE, DUMMY_ADDR)] |
| 248 |
writes = [i for i, a in enumerate(eng) if a[1][0] == 'W'] |
| 249 |
groups = [] |
| 250 |
for a, b in zip(writes, writes[1:]): |
| 251 |
grp = eng[a + 1:b + 1] |
| 252 |
if len(grp) == len(roles) and \ |
| 253 |
tuple(x[1][0] for x in grp) == tuple(r[0] for r in roles): |
| 254 |
groups.append(grp) |
| 255 |
for gi, grp in enumerate(groups): |
| 256 |
for i in range(len(grp) - 1): |
| 257 |
yield f'{roles[i]}->{roles[i + 1]}', grp[i][0], grp[i + 1][0] |
| 258 |
if gi + 1 < len(groups): |
| 259 |
nxt = groups[gi + 1] |
| 260 |
if (nxt[-1][2] // VRAM_LINE) == (grp[-1][2] // VRAM_LINE): |
| 261 |
yield f'{roles[-1]}->{roles[0]}', grp[-1][0], nxt[0][0] |
| 262 |
elif 0 < nxt[-1][2] - grp[-1][2] <= 2 * VRAM_LINE: |
| 263 |
yield 'newline', grp[-1][0], nxt[0][0] |
| 264 |
|
| 265 |
|
| 266 |
|
| 267 |
def next_slot(mode, p, delta, taken=frozenset()): |
| 268 |
|
| 269 |
|
| 270 |
tab = table(mode) |
| 271 |
line = p // TICKS |
| 272 |
for k in range(4): |
| 273 |
for s in tab: |
| 274 |
t = (line + k) * TICKS + s |
| 275 |
if t > p and V(mode, t) - V(mode, p) >= delta and t not in taken: |
| 276 |
return t |
| 277 |
raise AssertionError('no slot found') |
| 278 |
|
| 279 |
|
| 280 |
def prev_slot(mode, n): |
| 281 |
tab = table(mode) |
| 282 |
line, off = divmod(n, TICKS) |
| 283 |
earlier = [s for s in tab if s < off] |
| 284 |
return (line * TICKS + earlier[-1] if earlier |
| 285 |
else (line - 1) * TICKS + tab[-1]) |
| 286 |
|
| 287 |
|
| 288 |
def delta_for(table, spron, key, mode, prev=None): |
| 289 |
|
| 290 |
|
| 291 |
|
| 292 |
|
| 293 |
|
| 294 |
|
| 295 |
|
| 296 |
|
| 297 |
|
| 298 |
if (TIGHT_RULE[0] and prev is not None and |
| 299 |
(prev % TICKS) in TIGHT[mode] and not PLAIN[0]): |
| 300 |
return delta_for(table, spron, key, mode) + 1 |
| 301 |
if mode == 'sprOn': |
| 302 |
if key in spron: |
| 303 |
return spron[key] |
| 304 |
return table[key] + (0 if PLAIN[0] else SPR_ON_EXTRA) |
| 305 |
return table[key] |
| 306 |
|
| 307 |
|
| 308 |
def bands(files): |
| 309 |
out = collections.defaultdict(lambda: (1, 400)) |
| 310 |
n = collections.Counter() |
| 311 |
for f in files: |
| 312 |
mode = meta(f)['mode'] |
| 313 |
cmd = meta(f)['cmd'] |
| 314 |
for step, p, nx in transitions(f): |
| 315 |
key = (cmd, step, mode) |
| 316 |
lo = V(mode, prev_slot(mode, nx)) - V(mode, p) + 1 |
| 317 |
hi = V(mode, nx) - V(mode, p) |
| 318 |
a, b = out[key] |
| 319 |
out[key] = (max(a, lo), min(b, hi)) |
| 320 |
n[key] += 1 |
| 321 |
return out, n |
| 322 |
|
| 323 |
|
| 324 |
def count_misses(files, table, spron, cpu_traces=False, plain=None): |
| 325 |
saved = PLAIN[0] |
| 326 |
if plain is not None: |
| 327 |
PLAIN[0] = plain |
| 328 |
tot = bad = contended = 0 |
| 329 |
per = collections.Counter() |
| 330 |
for f in files: |
| 331 |
info = meta(f) |
| 332 |
|
| 333 |
|
| 334 |
|
| 335 |
|
| 336 |
taken = {a[0] for a in parse(f) |
| 337 |
if a[1] in CPU_CODES |
| 338 |
or ((a[1], a[2]) == (DUMMY_CODE, DUMMY_ADDR) |
| 339 |
and (a[0] % TICKS) in TIGHT[info['mode']])} \ |
| 340 |
if cpu_traces else frozenset() |
| 341 |
for step, p, nx in transitions(f): |
| 342 |
key = (info['cmd'], step) |
| 343 |
d = delta_for(table, spron, key, info['mode'], p) |
| 344 |
first = next_slot(info['mode'], p, d) |
| 345 |
got = next_slot(info['mode'], p, d, taken) |
| 346 |
tot += 1 |
| 347 |
contended += (got != first) |
| 348 |
if got != nx: |
| 349 |
bad += 1 |
| 350 |
per[key + (info['mode'],)] += 1 |
| 351 |
PLAIN[0] = saved |
| 352 |
return tot, bad, contended, per |
| 353 |
|
| 354 |
|
| 355 |
def main(): |
| 356 |
ap = argparse.ArgumentParser(description=__doc__) |
| 357 |
ap.add_argument('slots_dir') |
| 358 |
ap.add_argument('--cpu', action='store_true', |
| 359 |
help='use the rdCpu/wrCpu traces and test the arbitration') |
| 360 |
ap.add_argument('--plain', action='store_true', |
| 361 |
help='no memory-cycle stretch and no sprites-on +1') |
| 362 |
ap.add_argument('--published-slots', action='store_true', |
| 363 |
help="use part2/README.md's one-cycle slot correction") |
| 364 |
ap.add_argument('--no-tight', action='store_true', |
| 365 |
help='drop the extra cycle after a 6-cycle-pair slot') |
| 366 |
args = ap.parse_args() |
| 367 |
PLAIN[0] = args.plain |
| 368 |
PUBLISHED[0] = args.published_slots |
| 369 |
TIGHT_RULE[0] = not args.no_tight |
| 370 |
|
| 371 |
files = [f for f in sorted(glob.glob(os.path.join(args.slots_dir, |
| 372 |
'scr5-*.txt'))) |
| 373 |
if meta(f) and '-wrong-' not in f] |
| 374 |
files = [f for f in files |
| 375 |
if (meta(f)['cpu'] != 'noCpu') == bool(args.cpu)] |
| 376 |
if not files: |
| 377 |
sys.exit(f'no traces found in {args.slots_dir}') |
| 378 |
|
| 379 |
if args.cpu: |
| 380 |
for label, tbl, spron, pl in ( |
| 381 |
('openMSX today', OPENMSX, OPENMSX_SPR_ON, True), |
| 382 |
('fitted model', FITTED, {}, False)): |
| 383 |
tot, bad, cont, _ = count_misses(files, tbl, spron, True, pl) |
| 384 |
print(f'{len(files)} traces with CPU activity, {tot} engine ' |
| 385 |
f'transitions, the CPU took the slot the engine wanted in ' |
| 386 |
f'{100.0 * cont / tot:.1f}% of them') |
| 387 |
print(f' {label:16}: {bad} mispredicted ' |
| 388 |
f'({100.0 * bad / tot:.3f}%)') |
| 389 |
return |
| 390 |
|
| 391 |
b, n = bands(files) |
| 392 |
print(f'{len(files)} traces, {sum(n.values())} engine-to-engine transitions' |
| 393 |
+ (' [--plain: VDP cycles]' if args.plain else '')) |
| 394 |
print() |
| 395 |
print(f'{"cmd":5} {"step":9} {"dispOff":>12} {"sprOff":>12} {"sprOn":>12}' |
| 396 |
f' all modes') |
| 397 |
print('-' * 72) |
| 398 |
for cmd, step in sorted({(k[0], k[1]) for k in b}): |
| 399 |
cells, lo, hi = [], 1, 400 |
| 400 |
for mode in ('dispOff', 'sprOff', 'sprOn'): |
| 401 |
k = (cmd, step, mode) |
| 402 |
if k not in b: |
| 403 |
cells.append('-') |
| 404 |
continue |
| 405 |
a, z = b[k] |
| 406 |
cells.append(f'[{a},{z}]' + ('!' if a > z else '')) |
| 407 |
if mode == 'sprOn' and not args.plain: |
| 408 |
a, z = a - SPR_ON_EXTRA, z - SPR_ON_EXTRA |
| 409 |
lo, hi = max(lo, a), min(hi, z) |
| 410 |
allb = f'[{lo},{hi}]' + (' no single value' if lo > hi else '') |
| 411 |
print(f'{cmd:5} {step:9} ' + ' '.join(f'{c:>12}' for c in cells) |
| 412 |
+ f' {allb}') |
| 413 |
print() |
| 414 |
for label, tbl, spron, pl in ( |
| 415 |
('openMSX today', OPENMSX, OPENMSX_SPR_ON, True), |
| 416 |
('fitted model', FITTED, {}, args.plain)): |
| 417 |
tot, bad, _, per = count_misses(files, tbl, spron, plain=pl) |
| 418 |
print(f'{label:16}: {bad} of {tot} mispredicted ' |
| 419 |
f'({100.0 * bad / tot:.3f}%)') |
| 420 |
for k, v in sorted(per.items(), key=lambda kv: -kv[1])[:4]: |
| 421 |
print(f' {k[0]:5} {k[1]:9} {k[2]:8} {v}') |
| 422 |
|
| 423 |
|
| 424 |
if __name__ == '__main__': |
| 425 |
main() |