"""What the 68000's own frame clock costs, out of the two clock.lua runs. ROADMAP P3. Usage: clock_cost.py THE SUBTRACTION. Both runs execute the same one-instruction loop over a window of the same number of raster frames, so the window is the same number of 68000 clocks in both. With the clock off, every clock in the window went into loop iterations: L = clocks / iters_off clocks per iteration With it armed, the interrupts took some of them: H = (clocks - iters_on * L) / ints clocks per V-DISP interrupt L is CALIBRATED rather than looked up. That is the point: this project's cost model (tools/analysis/buscost.py) says a 68000 bus cycle is 4 clocks and an instruction costs 4 * (instruction words + data accesses), and the whole reason to measure is to avoid scoring the clock against the table the table is meant to be checked by. L falling on a whole number of clocks is therefore a RESULT, not an assumption, and it is reported as one. WHAT THE FIGURE IS PER FRAME. Not H -- the interrupt fires once per refresh and a frame is several refreshes. On the hardware raster that is 31500/VTOTAL over fps interrupts per frame, and the de-skewed rate is the one to use: MAME's raster is fast by htotal/(htotal-8) (see tools/bench/clock.lua), and charging the player the emulator's extra interrupts would overstate the cost by that same 2.2%. """ import sys def read(path): d, cad = {}, {} for line in open(path): f = line.split() if f[0] == "cad": cad[int(f[1])] = int(f[2]) else: d[f[0]] = float(f[1]) d["cad"] = cad return d def main(off_path, on_path): off, on = read(off_path), read(on_path) if off["on"] != 0 or on["on"] != 1: sys.exit("FAIL: expected the calibration run first and the armed run " "second; got on=%d then on=%d" % (off["on"], on["on"])) for k in ("frames", "clocks", "fps", "vtotal"): if off[k] != on[k]: sys.exit("FAIL: the two runs do not share a window: %s is %g in " "the calibration run and %g in the armed one" % (k, off[k], on[k])) clocks = off["clocks"] L = clocks / off["iters"] ints = on["ints"] H = (clocks - on["iters"] * L) / ints # The self-check that licenses the subtraction: the interrupt count must be # the raster frame count. clock.lua already fails on this, restated here # because this file is also read on its own. if abs(ints - on["frames"]) > 1: sys.exit("FAIL: %d interrupts over %g raster frames -- not the raster" % (ints, on["frames"])) fps, skew = on["fps"], on["skew"] hw_hz = on["hw_hz"] per_frame_ints = hw_hz / fps per_frame = H * per_frame_ints FRAME_CLK = 10e6 / fps print(" calibration: %.6f clocks per loop iteration over %d iterations" % (L, off["iters"])) print(" (%s a whole number of clocks -- the loop is one " "`addq.l #1,abs.l` at 7 bus cycles plus a `bra.s`)" % ("lands on" if abs(L - round(L)) < 1e-3 else "does NOT land on")) print(" INTERRUPT: %.2f clocks per V-DISP, measured over %d of them" % (H, ints)) print(" PER FRAME: %.2f interrupts x %.2f = %.0f clocks = %.4f%% of a " "%g fps frame" % (per_frame_ints, H, per_frame, 100 * per_frame / FRAME_CLK, fps)) print(" (%.4f refreshes per frame on the HARDWARE raster of " "31500/%d = %.4f Hz, not on MAME's, which is %.4fx fast)" % (per_frame_ints, on["vtotal"], hw_hz, skew)) # THE DRIFT GATE, and it is stated in TICKS rather than in ppm on purpose. # A remainder-keeping divider emits floor() or ceil() of the exact tick # count over any window and never accumulates -- so the only honest # tolerance is one tick, and any ppm figure is that one tick divided by # however long the window happened to be. Quoting ppm would let a longer # window advertise a tighter clock for no reason. want = on["frames"] * fps * on["vtotal"] / 31500.0 ticks = on["ticks"] print(" DRIFT: %d ticks over %d refreshes; exact is %.4f, so the " "error is %+.4f ticks" % (ticks, on["frames"], want, ticks - want)) if abs(ticks - want) > 1.0: sys.exit("FAIL: %d ticks where %.4f were due -- off by %.2f, which is " "more than the one tick a remainder can hold back. The " "divider is accumulating drift." % (ticks, want, ticks - want)) cad = on["cad"] tot = sum(cad.values()) if tot: # Refreshes per frame is 31500 / (fps * VTOTAL) exactly -- the divider's # own ratio, upside down. A remainder-keeping divider can only ever # emit the two whole numbers either side of it, so anything else in the # histogram is a bug in the divider and not a rounding taste. rpf = 31500.0 / (fps * on["vtotal"]) lo, hi = int(rpf), int(rpf) + 1 print(" CADENCE: %s (%d intervals; %.4f refreshes per frame, so " "only %d and %d are possible)" % (", ".join("%dx%d (%.1f%%)" % (k, v, 100.0 * v / tot) for k, v in sorted(cad.items())), tot, rpf, lo, hi)) for k in cad: if k not in (lo, hi): sys.exit("FAIL: a frame tick waited %d refreshes, which a " "remainder-keeping divider cannot produce" % k) # The mix is forced too: lo*a + hi*b = refreshes, a + b = ticks. b = tot * rpf - lo * tot print(" expected %d:%d split %.1f%% / %.1f%%, got " "%.1f%% / %.1f%%" % (lo, hi, 100 * (tot - b) / tot, 100 * b / tot, 100.0 * cad.get(lo, 0) / tot, 100.0 * cad.get(hi, 0) / tot)) return 0 if __name__ == "__main__": if len(sys.argv) != 3: sys.exit(__doc__) sys.exit(main(sys.argv[1], sys.argv[2]))