| 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 |
|
| 36 |
|
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
|
| 44 |
|
| 45 |
from __future__ import annotations |
| 46 |
|
| 47 |
import argparse |
| 48 |
import bisect |
| 49 |
import math |
| 50 |
import collections |
| 51 |
import glob |
| 52 |
import importlib.util |
| 53 |
import os |
| 54 |
import re |
| 55 |
import statistics |
| 56 |
import sys |
| 57 |
|
| 58 |
TICKS = 1368 |
| 59 |
LEAD = 16 |
| 60 |
DEAD = 2 |
| 61 |
|
| 62 |
PAIR_EXTRA_LEAD = 2 |
| 63 |
FREE_BEFORE = [-DEAD] |
| 64 |
PERIOD_2013 = 3060 |
| 65 |
IN_PERIOD = 72 |
| 66 |
BURST = 40 |
| 67 |
|
| 68 |
_here = os.path.dirname(os.path.abspath(__file__)) |
| 69 |
|
| 70 |
|
| 71 |
def _load(name, mod): |
| 72 |
spec = importlib.util.spec_from_file_location(mod, |
| 73 |
os.path.join(_here, name)) |
| 74 |
m = importlib.util.module_from_spec(spec) |
| 75 |
spec.loader.exec_module(m) |
| 76 |
return m |
| 77 |
|
| 78 |
|
| 79 |
C = _load('2026-measurement-check.py', 'check') |
| 80 |
G = _load('vcd-slot-grid.py', 'grid') |
| 81 |
|
| 82 |
TABLES = C.TABLES |
| 83 |
PAIR = {m: {s for s in TABLES[m] if (s - 6) in set(TABLES[m])} for m in TABLES} |
| 84 |
|
| 85 |
|
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
def V(mode, t): |
| 90 |
|
| 91 |
return C.V(mode, t) |
| 92 |
|
| 93 |
|
| 94 |
def decision(mode, s): |
| 95 |
|
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
|
| 101 |
lead = LEAD + (PAIR_EXTRA_LEAD if (s % TICKS) in PAIR[mode] else 0) |
| 102 |
want = V(mode, s) - lead |
| 103 |
t = s - lead |
| 104 |
while V(mode, t) > want: |
| 105 |
t -= 1 |
| 106 |
while V(mode, t + 1) <= want: |
| 107 |
t += 1 |
| 108 |
return t |
| 109 |
|
| 110 |
|
| 111 |
def slots(mode, lo, hi): |
| 112 |
tab = TABLES[mode] |
| 113 |
out = [] |
| 114 |
line = lo // TICKS - 1 |
| 115 |
while line * TICKS <= hi: |
| 116 |
out += [line * TICKS + s for s in tab if lo <= line * TICKS + s <= hi] |
| 117 |
line += 1 |
| 118 |
return sorted(out) |
| 119 |
|
| 120 |
|
| 121 |
def simulate(mode, requests, lo, hi, drop=True): |
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
|
| 126 |
|
| 127 |
|
| 128 |
ss = slots(mode, lo - 200, hi + 200) |
| 129 |
nxt = {s: n for s, n in zip(ss, ss[1:])} |
| 130 |
|
| 131 |
|
| 132 |
|
| 133 |
|
| 134 |
ev = ([(s - FREE_BEFORE[0], 0, s) for s in ss] |
| 135 |
+ [(r, 1, r) for r in requests] |
| 136 |
+ [(decision(mode, s), 2, s) for s in ss]) |
| 137 |
ev.sort() |
| 138 |
busy = False |
| 139 |
pending = False |
| 140 |
at = None |
| 141 |
cpu, dummy, lost = [], [], 0 |
| 142 |
for t, kind, val in ev: |
| 143 |
if kind == 1: |
| 144 |
if busy: |
| 145 |
lost += 1 |
| 146 |
if not drop: |
| 147 |
pending, at = True, None |
| 148 |
else: |
| 149 |
busy, pending, at = True, True, None |
| 150 |
elif kind == 2: |
| 151 |
if pending and at is None: |
| 152 |
if (val % TICKS) in PAIR[mode]: |
| 153 |
dummy.append(val) |
| 154 |
at = nxt.get(val) |
| 155 |
else: |
| 156 |
at = val |
| 157 |
pending = False |
| 158 |
elif at == val: |
| 159 |
cpu.append(val) |
| 160 |
busy, at = False, None |
| 161 |
elif at is None and not pending: |
| 162 |
pass |
| 163 |
return cpu, dummy, lost |
| 164 |
|
| 165 |
|
| 166 |
|
| 167 |
|
| 168 |
|
| 169 |
CMDS = ('HMMM', 'HMMV', 'LMMM', 'LMMV', 'YMMM', 'HYMM', 'LINE') |
| 170 |
|
| 171 |
|
| 172 |
def meta13(path): |
| 173 |
b = os.path.basename(path)[:-4] |
| 174 |
if not b.startswith('screen') or 'wrong' in b: |
| 175 |
return None |
| 176 |
scr = re.match(r'screen(\d+)', b) |
| 177 |
if scr is None or scr[1] not in ('5', '6', '7', '8'): |
| 178 |
return None |
| 179 |
mode = ('dispOff' if 'screenoff' in b else |
| 180 |
'sprOff' if 'nosprites' in b else |
| 181 |
'sprOn' if 'sprite' in b else None) |
| 182 |
cpu = ('rd' if 'cpuread' in b else 'wr' if 'cpuwrite' in b else None) |
| 183 |
if mode is None or cpu is None: |
| 184 |
return None |
| 185 |
return {'mode': mode, 'name': b, |
| 186 |
'cmd': next((c for c in CMDS if c in b), None)} |
| 187 |
|
| 188 |
|
| 189 |
def parse13(path, mode): |
| 190 |
rel = C.RELABEL[mode] |
| 191 |
out = [] |
| 192 |
with open(path) as f: |
| 193 |
for ln in f: |
| 194 |
if ':' not in ln: |
| 195 |
continue |
| 196 |
row, rest = ln.rstrip('\n').split(':', 1) |
| 197 |
t, col = int(row), 0 |
| 198 |
while rest: |
| 199 |
cell = rest[2:13] |
| 200 |
if cell.strip(): |
| 201 |
line, c = divmod(TICKS * col + t - 1, TICKS) |
| 202 |
out.append((line * TICKS + rel.get(c, c), cell[0:3], |
| 203 |
int(cell[6:11], 16))) |
| 204 |
rest = rest[13:] |
| 205 |
col += 1 |
| 206 |
return sorted(out) |
| 207 |
|
| 208 |
|
| 209 |
def requests13(phase, lo, hi): |
| 210 |
out = [] |
| 211 |
n = (lo - phase) // PERIOD_2013 - 1 |
| 212 |
while phase + n * PERIOD_2013 <= hi: |
| 213 |
base = phase + n * PERIOD_2013 |
| 214 |
out += [base + j * IN_PERIOD for j in range(BURST) |
| 215 |
if lo <= base + j * IN_PERIOD <= hi] |
| 216 |
n += 1 |
| 217 |
return sorted(out) |
| 218 |
|
| 219 |
|
| 220 |
def run2013(repo, drop=True): |
| 221 |
print('=== 2013, Philips NMS8250: CPU clock exactly VDP/6 ===') |
| 222 |
print(f'{"capture":42} {"mode":8} {"n":>4} {"missed":>7} {"spurious":>9} ' |
| 223 |
f'{"lost":>5} {"phases":>7}') |
| 224 |
tot = collections.Counter() |
| 225 |
for path in sorted(glob.glob(os.path.join(repo, '8.final-analysis', |
| 226 |
'screen*.txt'))): |
| 227 |
m = meta13(path) |
| 228 |
if m is None: |
| 229 |
continue |
| 230 |
acc = parse13(path, m['mode']) |
| 231 |
obs = sorted(t for t, code, a in acc if code in ('R.c', 'W.c')) |
| 232 |
if len(obs) < 10: |
| 233 |
continue |
| 234 |
lo, hi = obs[0] - 100, obs[-1] + 100 |
| 235 |
best = None |
| 236 |
for phase in range(PERIOD_2013): |
| 237 |
cpu, dummy, lost = simulate(m['mode'], requests13(phase, lo, hi), |
| 238 |
lo, hi, drop) |
| 239 |
p = {x for x in cpu if obs[0] <= x <= obs[-1]} |
| 240 |
miss, extra = len(set(obs) - p), len(p - set(obs)) |
| 241 |
score = (miss + extra, ) |
| 242 |
if best is None or score < best[0]: |
| 243 |
best = (score, miss, extra, lost, [phase]) |
| 244 |
elif score == best[0]: |
| 245 |
best[4].append(phase) |
| 246 |
_, miss, extra, lost, phases = best |
| 247 |
tot['n'] += len(obs) |
| 248 |
tot['miss'] += miss |
| 249 |
tot['extra'] += extra |
| 250 |
print(f'{m["name"]:42} {m["mode"]:8} {len(obs):4} {miss:7} {extra:9} ' |
| 251 |
f'{lost:5} {len(phases):7}') |
| 252 |
print(f'\n{tot["n"]} CPU accesses: {tot["miss"]} not predicted ' |
| 253 |
f'({100.0 * tot["miss"] / tot["n"]:.2f}%), {tot["extra"]} predicted ' |
| 254 |
f'but absent\n') |
| 255 |
|
| 256 |
|
| 257 |
|
| 258 |
|
| 259 |
|
| 260 |
def timebase(ref, cyc, anchor): |
| 261 |
|
| 262 |
out = [] |
| 263 |
for t in sorted(ref): |
| 264 |
c = (t - anchor) * cyc + G.REFRESH_CYCLE |
| 265 |
line, off = divmod(c, TICKS) |
| 266 |
k = round((off - G.REFRESH_CYCLE) / 128) |
| 267 |
if 0 <= k <= 7: |
| 268 |
exact = line * TICKS + G.REFRESH_CYCLE + 128 * k |
| 269 |
if abs(exact - c) <= 3: |
| 270 |
out.append((t, exact)) |
| 271 |
return out |
| 272 |
|
| 273 |
|
| 274 |
def decode26(vcd, rng): |
| 275 |
trace = G.parse_vcd(vcd) |
| 276 |
edges = G.ras_edges(trace) |
| 277 |
g = G.geometry(edges) |
| 278 |
if g is None: |
| 279 |
return None |
| 280 |
period, _ = g |
| 281 |
cyc = TICKS / period |
| 282 |
acc = G.accesses(trace) |
| 283 |
anchor, ref = G.refresh_chain_start(acc, cyc) |
| 284 |
if anchor is None: |
| 285 |
return None |
| 286 |
fixed = timebase(ref, cyc, anchor) |
| 287 |
if len(fixed) < 4: |
| 288 |
return None |
| 289 |
xs = [t for t, _ in fixed] |
| 290 |
ys = [c for _, c in fixed] |
| 291 |
|
| 292 |
def cycle(t): |
| 293 |
i = bisect.bisect_left(xs, t) |
| 294 |
if i == 0: |
| 295 |
return ys[0] + (t - xs[0]) * cyc |
| 296 |
if i >= len(xs): |
| 297 |
return ys[-1] + (t - xs[-1]) * cyc |
| 298 |
f = (t - xs[i - 1]) / (xs[i] - xs[i - 1]) |
| 299 |
return ys[i - 1] + f * (ys[i] - ys[i - 1]) |
| 300 |
|
| 301 |
req, prev = [], {'CSR': None, 'CSW': None} |
| 302 |
for t, s in trace: |
| 303 |
for sig in ('CSR', 'CSW'): |
| 304 |
if prev[sig] == 0 and s[sig] == 1: |
| 305 |
req.append(cycle(t)) |
| 306 |
prev[sig] = s[sig] |
| 307 |
by = collections.defaultdict(list) |
| 308 |
for cas, ras, addr, rd in acc: |
| 309 |
by[ras].append(addr) |
| 310 |
cpu, dummy = [], [] |
| 311 |
for ras, addrs in sorted(by.items()): |
| 312 |
if len(addrs) != 1 or ras in ref: |
| 313 |
continue |
| 314 |
if addrs[0] == 0x1FFFF: |
| 315 |
dummy.append(cycle(ras)) |
| 316 |
elif rng[0] <= addrs[0] <= rng[1]: |
| 317 |
cpu.append(cycle(ras)) |
| 318 |
return sorted(req), sorted(cpu), sorted(dummy) |
| 319 |
|
| 320 |
|
| 321 |
def snap(mode, t): |
| 322 |
line, off = divmod(t, TICKS) |
| 323 |
return min(((line + k) * TICKS + s for k in (-1, 0, 1) |
| 324 |
for s in TABLES[mode]), key=lambda v: abs(v - t)) |
| 325 |
|
| 326 |
|
| 327 |
def fit_pace(req): |
| 328 |
|
| 329 |
|
| 330 |
|
| 331 |
|
| 332 |
|
| 333 |
|
| 334 |
|
| 335 |
|
| 336 |
|
| 337 |
|
| 338 |
req = [r for i, r in enumerate(req) if i == 0 or r - req[i - 1] > 40] |
| 339 |
g = [b - a for a, b in zip(req, req[1:])] |
| 340 |
|
| 341 |
|
| 342 |
|
| 343 |
|
| 344 |
|
| 345 |
inner = statistics.median(x for x in g if x < 300) |
| 346 |
step, tail = min(((12, 42), (22, 52), (37, 67)), |
| 347 |
key=lambda st: abs(inner / st[0] - 5.96)) |
| 348 |
s0 = inner / step |
| 349 |
x = [0] |
| 350 |
for gap in g: |
| 351 |
n = round(gap / (step * s0)) |
| 352 |
x.append(x[-1] + (tail if abs(gap - tail * s0) < 20 * s0 / step |
| 353 |
else step * max(1, n))) |
| 354 |
keep = list(range(len(req))) |
| 355 |
s, phi, rms = s0, 0.0, 0.0 |
| 356 |
for _ in range(4): |
| 357 |
n = len(keep) |
| 358 |
mx = sum(x[i] for i in keep) / n |
| 359 |
my = sum(req[i] for i in keep) / n |
| 360 |
sxx = sum((x[i] - mx) ** 2 for i in keep) |
| 361 |
sxy = sum((x[i] - mx) * (req[i] - my) for i in keep) |
| 362 |
s = sxy / sxx |
| 363 |
phi = my - s * mx |
| 364 |
res = [req[i] - (phi + s * x[i]) for i in keep] |
| 365 |
rms = (sum(e * e for e in res) / n) ** 0.5 |
| 366 |
nk = [i for i, e in zip(keep, res) if abs(e) <= max(0.4, 3 * rms)] |
| 367 |
if len(nk) == len(keep) or len(nk) < 20: |
| 368 |
break |
| 369 |
keep = nk |
| 370 |
return s, phi, rms, x, req |
| 371 |
|
| 372 |
|
| 373 |
def run2026(repo, drop=True, quantum=1): |
| 374 |
print('=== 2026, Philips NMS8280: CPU clock asynchronous to the VDP ===') |
| 375 |
vcd_dir = os.path.join(repo, 'part2', '1.vcd') |
| 376 |
txt_dir = os.path.join(repo, 'part2', '5.slots') |
| 377 |
agg = collections.defaultdict(collections.Counter) |
| 378 |
offs = collections.defaultdict(list) |
| 379 |
rates, rmss = [], [] |
| 380 |
for vcd in sorted(glob.glob(os.path.join(vcd_dir, 'scr5-*.vcd'))): |
| 381 |
name = os.path.basename(vcd)[:-4] |
| 382 |
if 'noCpu' in name: |
| 383 |
continue |
| 384 |
txt = os.path.join(txt_dir, name + '.txt') |
| 385 |
if not os.path.exists(txt) or C.meta(txt) is None: |
| 386 |
continue |
| 387 |
mode = C.meta(txt)['mode'] |
| 388 |
v = [a for t, code, a in C.parse(txt) if code in C.CPU_CODES] |
| 389 |
if not v: |
| 390 |
continue |
| 391 |
d = decode26(vcd, (min(v), max(v))) |
| 392 |
if d is None: |
| 393 |
continue |
| 394 |
req, cpu, dummy = d |
| 395 |
obs = sorted({snap(mode, t) for t in cpu}) |
| 396 |
if len(obs) < 10: |
| 397 |
continue |
| 398 |
obsd = {snap(mode, t) for t in dummy |
| 399 |
if (snap(mode, t) % TICKS) in PAIR[mode]} |
| 400 |
s, phi, rms, xs, _ = fit_pace(req) |
| 401 |
rates.append(s) |
| 402 |
rmss.append(rms) |
| 403 |
base = [phi + s * a for a in xs] |
| 404 |
lo, hi = obs[0] - 100, obs[-1] + 100 |
| 405 |
|
| 406 |
|
| 407 |
|
| 408 |
best = None |
| 409 |
o = -8.0 |
| 410 |
while o < 24.0: |
| 411 |
|
| 412 |
rq = [(quantum * math.ceil((b + o) / quantum) if quantum |
| 413 |
else b + o) for b in base if lo - 150 < b + o < hi] |
| 414 |
p, pd, lost = simulate(mode, rq, lo, hi, drop) |
| 415 |
ps = {x for x in p if obs[0] <= x <= obs[-1]} |
| 416 |
pds = {x for x in pd if obs[0] <= x <= obs[-1]} |
| 417 |
score = (len(set(obs) - ps) + len(ps - set(obs)) |
| 418 |
+ len(obsd - pds) + len(pds - obsd)) |
| 419 |
if best is None or score < best[0]: |
| 420 |
best = (score, o, ps, pds, lost, len(rq)) |
| 421 |
o += 0.05 |
| 422 |
_, off, ps, pds, lost, nreq = best |
| 423 |
pace = ('222' if 'Cpu222' in name else |
| 424 |
'132' if 'Cpu132' in name else |
| 425 |
'r/w' if 'rdwrCpu' in name else ' 72') |
| 426 |
k = agg[(pace, mode)] |
| 427 |
k['n'] += len(obs) |
| 428 |
k['miss'] += len(set(obs) - ps) |
| 429 |
k['extra'] += len(ps - set(obs)) |
| 430 |
k['dn'] += len(obsd) |
| 431 |
k['dmiss'] += len(obsd - pds) |
| 432 |
k['dextra'] += len(pds - obsd) |
| 433 |
k['lost'] += lost |
| 434 |
k['req'] += nreq |
| 435 |
k['cap'] += 1 |
| 436 |
offs[(pace, mode)].append(off) |
| 437 |
print(f'pace: {statistics.fmean(rates):.5f} +- {statistics.stdev(rates):.5f}' |
| 438 |
f' VDP cycles per Z80 T-state ({12 * statistics.fmean(rates):.4f} ' |
| 439 |
f'cycles between port accesses; 6 and 72 on a single-clock machine), ' |
| 440 |
f'fit residual {statistics.median(rmss):.3f} cycles') |
| 441 |
print(f'requests quantised to {quantum} VDP cycle(s)' |
| 442 |
if quantum else 'requests not quantised') |
| 443 |
print(f'{"pace":4} {"mode":8} {"cap":>4} {"accesses":>9} {"missed":>7} ' |
| 444 |
f'{"spurious":>9} {"dummy":>6} {"d-miss":>7} {"lost/req":>10} ' |
| 445 |
f'{"offset":>13}') |
| 446 |
for pace in (' 72', '132', '222', 'r/w'): |
| 447 |
for mode in ('dispOff', 'sprOff', 'sprOn'): |
| 448 |
k = agg[(pace, mode)] |
| 449 |
if not k['n']: |
| 450 |
continue |
| 451 |
o = offs[(pace, mode)] |
| 452 |
sd = statistics.stdev(o) if len(o) > 1 else 0.0 |
| 453 |
print(f'{pace:4} {mode:8} {k["cap"]:4} {k["n"]:9} {k["miss"]:7} ' |
| 454 |
f'{k["extra"]:9} {k["dn"]:6} {k["dmiss"]:7} ' |
| 455 |
f'{k["lost"]:5}/{k["req"]:<5} ' |
| 456 |
f'{statistics.fmean(o):6.2f} +-{sd:5.2f}') |
| 457 |
n = sum(agg[m]['n'] for m in agg) |
| 458 |
miss = sum(agg[m]['miss'] for m in agg) |
| 459 |
print(f'\n{n} CPU accesses: {miss} not predicted ' |
| 460 |
f'({100.0 * miss / n:.2f}%)') |
| 461 |
|
| 462 |
|
| 463 |
def main(): |
| 464 |
ap = argparse.ArgumentParser(description=__doc__) |
| 465 |
ap.add_argument('repo') |
| 466 |
ap.add_argument('--2013', dest='only13', action='store_true') |
| 467 |
ap.add_argument('--2026', dest='only26', action='store_true') |
| 468 |
ap.add_argument('--quantum', type=int, default=1, |
| 469 |
help='VDP cycles the request times are quantised to ' |
| 470 |
'(0 = not quantised)') |
| 471 |
ap.add_argument('--overwrite', action='store_true', |
| 472 |
help='a new request overwrites an unserved one, as openMSX ' |
| 473 |
'assumes, instead of being lost') |
| 474 |
args = ap.parse_args() |
| 475 |
if not args.only26: |
| 476 |
run2013(args.repo, not args.overwrite) |
| 477 |
if not args.only13: |
| 478 |
run2026(args.repo, not args.overwrite, args.quantum) |
| 479 |
|
| 480 |
|
| 481 |
if __name__ == '__main__': |
| 482 |
main() |