Put the frame clock on the 68000, and find that the 12 fps frame does not exist
ROADMAP P3 said "needs MFP timer or VBL" and neither can do it. The MFP's timer clock is 16 MHz/4, its prescalers stop at 200 and its data register is 8 bits, so the slowest tick any single timer can make is 78.125 Hz -- 6.5x faster than a frame -- and 4e6/12 is not an integer, so no setting reaches 12 Hz at all. The raster has no whole divide near 12 either: 4 refreshes is 13.86 fps and 5 is 11.09. tools/analysis/23_frame_clock.py walks all 7x256 timer settings rather than asserting it. src/player/clock.i takes the V-DISP falling edge on MFP GPIP4 -- the start of vertical blanking, which is when a player would present -- and adds fps*VTOTAL per edge to a 16-bit accumulator, emitting a tick at 31,500 and keeping the remainder. The long-run rate is fps*VTOTAL/VTOTAL = 12.000000 fps exactly, and both constants are read out of the CRTC at init, so the clock is derived from the registers that generate the raster it counts. Measured over 3,000 refreshes: 3,000 interrupts, 649 ticks where 649.1429 were due. It costs 181.35 clocks per V-DISP, 838 per frame, 0.1006% of the budget -- timed by the 68000 itself, because the host's granularity is 17.64 ms and the interrupt is microseconds. The loop's own cost was calibrated rather than looked up and landed on 38.000002 clocks, which both licenses the subtraction and confirms buscost.py's model; the 181.35 then decomposes exactly, leaving 43.99 clocks for the interrupt exception -- the textbook 44, measured. THE ONE THAT MOVES SOMETHING: 12 fps on a 55.4577 Hz raster is 4.6215 refreshes, so a frame is shown for 4 refreshes (72.13 ms) or 5 (90.16 ms), 37.9% of them short. The 833,333-clock budget every figure in this project is priced against is the MEAN slot, and the short one is 13.4% under it. The cadence was already in the tree unnamed: stream.lua's tick is sampled at frame boundaries, so its gaps were always 4 or 5, and every host-paced result in FINDINGS 49/51 carried it. P3 moved who produces it onto the machine and made it visible. It is not a dropped frame -- the pace gate lets an overrun eat the next frame's idle -- and on the gate container it costs 4 frames of 120 their idle against 1 for the nominal model, most of that the frame-0 transient at 111% of budget. stream.s counts it now, and the rig matches an offline model of the divider exactly. Also struck: MAME's raster runs 2.22% fast. refresh_mode() builds the frame period from scr.max_x*scr.max_y with scr.max_x = m_htotal - 8, one character cell short and an inclusive bound used as a count, so it runs at 56.6901 Hz where the registers say 55.4577 -- agreeing to six digits with the arithmetic. Every "1/55.46 s granularity" note in this tree was wrong and is 1/56.69 s, corrected in six files with the derivation put once in crtc_mode.lua. No conclusion changes and no 68000 cycle figure moves; the CPU clock is unrelated to the screen. But anything paced by the raster runs fast under MAME, so the rig reports both rates and prices the interrupt against the hardware's. decode.s and frame.i are unchanged; decode.bin is still 1,296 B at the same MD5. The pace gate's wait loop is byte-for-byte the one FINDINGS 51 measured and the free-running path executes none of the new code. check.sh gains two stages: the clock's own measurement, and 120 frames decoded pixel-exact with nothing outside the machine deciding when a frame may start. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
-- a LOWER BOUND, not a prediction. Interrupts are masked (SR=$2700) so the
|
||||
-- IPL's timer and VBL handlers cannot steal cycles into the measurement.
|
||||
--
|
||||
-- Timing resolution is one video frame (1/55.46 s = 18.03 ms), because Lua
|
||||
-- Timing resolution is one video frame (1/56.69 s = 17.64 ms -- MAME's, not
|
||||
-- the hardware's 55.46; see crtc_mode.lua), because Lua
|
||||
-- gets no cycle counter -- luaengine.cpp exposes machine.time and nothing
|
||||
-- from device_execute_interface. Each variant therefore loops enough times
|
||||
-- to run ~4 emulated seconds, putting the granularity error near 0.4%.
|
||||
|
||||
+45
-1
@@ -286,7 +286,7 @@ echo "--- session 21: the 68000 builds its own codebooks and palette (FINDINGS 5
|
||||
#
|
||||
# NOT gated on the cycle counts, and the reason is NOT the one blit.s has. These
|
||||
# are emulated time and reproduce exactly run to run; what they are not is
|
||||
# sharp, because MAME samples them on a 1/55.46 s clock and the job takes
|
||||
# sharp, because MAME samples them on a 1/56.69 s clock and the job takes
|
||||
# milliseconds. Nothing in the tree's cost models depends on them either. A
|
||||
# change in them is a re-derivation in FINDINGS 53, not a red light here.
|
||||
bash tools/bench/load_run.sh "$DLX" > tmp/load_gate.log 2>&1 || {
|
||||
@@ -294,4 +294,48 @@ bash tools/bench/load_run.sh "$DLX" > tmp/load_gate.log 2>&1 || {
|
||||
exit 1; }
|
||||
grep -aE "^ *OK|both CPU cores|SCENE CHANGE" tmp/load_gate.log | sed 's/^ *//;s/^/ /'
|
||||
|
||||
echo "--- session 22: the 68000 keeps its own frame clock (FINDINGS 54) ---"
|
||||
# ROADMAP P3. Until now the 12 fps tick came from tools/bench/stream.lua -- a
|
||||
# host writing a word into emulated RAM. A player has no host. src/player/
|
||||
# clock.i derives the tick from the CRTC's own V-DISP output through the MFP,
|
||||
# with a remainder-keeping divider whose two constants are READ OUT OF THE CRTC
|
||||
# at init, so the clock and the raster it counts cannot disagree.
|
||||
#
|
||||
# WHAT IS GATED, and it is deliberately structural rather than numeric:
|
||||
# * the interrupt count equals the raster frame count -- the tick IS the
|
||||
# raster, not something that merely resembles it;
|
||||
# * the divider does not accumulate drift, stated in TICKS (a remainder can
|
||||
# hold back at most one) rather than in ppm, which would let a longer
|
||||
# window advertise a tighter clock for free;
|
||||
# * every frame tick waits 4 or 5 refreshes and nothing else, which is what a
|
||||
# remainder-keeping divider can produce and a broken one cannot.
|
||||
# The interrupt COST is printed and not gated, for the same reason FINDINGS 53's
|
||||
# cycle counts are not: it is a measurement, and a change in it is a
|
||||
# re-derivation in FINDINGS 54 rather than a red light here.
|
||||
bash tools/bench/clock_run.sh 3000 12 > tmp/clock_gate.log 2>&1 || {
|
||||
echo "FAIL: the frame clock did not pass."; tail -12 tmp/clock_gate.log
|
||||
exit 1; }
|
||||
grep -aE "INTERRUPT:|PER FRAME:|DRIFT:|CADENCE:" tmp/clock_gate.log
|
||||
grep -q "V-DISP interrupts 3000" tmp/clock_gate.log || {
|
||||
echo "FAIL: the tick is not the raster -- the interrupt count and the frame"
|
||||
echo " count disagree. Everything else in FINDINGS 54 rests on that."
|
||||
exit 1; }
|
||||
|
||||
echo "--- session 22: 120 frames decoded on the machine's own clock (FINDINGS 54) ---"
|
||||
# The strongest form of the claim: the same pixel-exact 120-frame decode out of
|
||||
# the same 256 KB ring, with NOTHING outside the machine deciding when a frame
|
||||
# may start. The pace gate in src/player/stream.s is byte-for-byte the one
|
||||
# FINDINGS 51 measured -- it cannot tell a host-written tick from a machine-
|
||||
# written one, which is why this is a test of the clock and not of a new rig.
|
||||
DLX_PACE=2 bash tools/bench/pace_run.sh 256 0 > tmp/selfpace_check.log 2>&1 || {
|
||||
echo "FAIL: the self-paced pass did not complete."; tail -8 tmp/selfpace_check.log
|
||||
exit 1; }
|
||||
grep -aE "decoder SELF-PACED|FRAME CLOCK|UNDERRUNS|NO IDLE" tmp/selfpace_check.log
|
||||
grep -q "UNDERRUNS: 0/120" tmp/selfpace_check.log || {
|
||||
echo "FAIL: the self-paced decoder underran."; exit 1; }
|
||||
grep -q "^OK" tmp/selfpace_check.log || {
|
||||
echo "FAIL: the self-paced pass was not pixel-exact. The clock changed WHEN"
|
||||
echo " frames start; if it changed WHAT they draw, the interrupt is"
|
||||
echo " corrupting decoder state."; tail -4 tmp/selfpace_check.log; exit 1; }
|
||||
|
||||
echo "ALL GREEN"
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
-- Drive src/player/clockgate.s: measure the 68000's own FRAME CLOCK.
|
||||
-- ROADMAP P3.
|
||||
--
|
||||
-- Two things are being measured and they need different instruments.
|
||||
--
|
||||
-- THE RATE AND THE CADENCE are counted, not timed. The clock's tick is a
|
||||
-- V-DISP interrupt, and MAME's Lua sees the machine once per screen frame --
|
||||
-- which is once per V-DISP. So the host's sampling granularity is exactly the
|
||||
-- clock's own granularity, and the cadence comes out as integers: how many
|
||||
-- refreshes each frame tick waited. There is no timing error to argue about
|
||||
-- in a count of 4s and 5s.
|
||||
--
|
||||
-- THE COST IS TIMED BY THE 68000, because the host cannot. 1/55.46 s of host
|
||||
-- granularity is 18 ms and the interrupt costs microseconds. So the 68000 runs
|
||||
-- a one-instruction loop for a window of thousands of refreshes and the host
|
||||
-- reads the iteration count at both ends; the interrupt cost falls out of the
|
||||
-- difference between a run with the clock armed and one without. See the head
|
||||
-- of src/player/clockgate.s for the arithmetic. This script emits the raw
|
||||
-- counts; tools/bench/clock_cost.py does the subtraction, so that the two runs
|
||||
-- it needs can be separate MAME invocations.
|
||||
--
|
||||
-- MEASUREMENT SCOPE. This is MAME 0.277's emulated X68000, not real hardware.
|
||||
-- What is being priced is the interrupt sequence of MAME's cycle-accurate
|
||||
-- M68000 core (src/devices/cpu/m68000, the `M68000` device x68k.cpp:1133 asks
|
||||
-- for) against zero-wait-state RAM. Real DRAM adds wait states to the six bus
|
||||
-- cycles of the exception and the four of the handler alike, so this is a LOWER
|
||||
-- BOUND in the same way every other 68000 figure in this project is.
|
||||
--
|
||||
-- Env:
|
||||
-- DLX_CLK_ON 1 = arm the frame clock, 0 = leave it off (the calibration
|
||||
-- run). REQUIRED -- the two runs are not interchangeable and a
|
||||
-- default would let one be reported as the other.
|
||||
-- DLX_CLK_FPS frame rate to ask clk_init for (default 12)
|
||||
-- DLX_CLK_WIN measurement window, in raster frames (default 3000 = 54.1 s)
|
||||
-- DLX_CLK_OUT where to write the raw counts (default tmp/clock_run.txt)
|
||||
|
||||
M = manager.machine
|
||||
SP = M.devices[":maincpu"].spaces["program"]
|
||||
|
||||
local function findfile(n)
|
||||
for _,p in ipairs{"../tools/bench/"..n, "tools/bench/"..n, n} do
|
||||
local f = io.open(p,"rb"); if f then f:close(); return p end
|
||||
end
|
||||
error(n.." not found")
|
||||
end
|
||||
local MODE = loadfile(findfile("crtc_mode.lua"))()
|
||||
|
||||
local CGFLAG, CGON, CGCNT = 0x18070, 0x18074, 0x18078
|
||||
local CLK_PACE = 0x18034
|
||||
local CLK_ACC, CLK_INCR = 0x18060, 0x18062
|
||||
local CLK_VDISP, CLK_FPS = 0x18064, 0x18068
|
||||
local CLK_ERR = 0x1806C
|
||||
local CPUHZ = 10000000
|
||||
|
||||
local ONS = os.getenv("DLX_CLK_ON")
|
||||
local FPS = tonumber(os.getenv("DLX_CLK_FPS") or "") or 12
|
||||
local WIN = tonumber(os.getenv("DLX_CLK_WIN") or "") or 3000
|
||||
local OUT = os.getenv("DLX_CLK_OUT") or "clock_run.txt"
|
||||
|
||||
local function P(s) print("[CLK] "..s) end
|
||||
|
||||
if ONS ~= "0" and ONS ~= "1" then
|
||||
P("DLX_CLK_ON must be 0 (calibration, clock off) or 1 (clock armed). The "
|
||||
.."cost figure is the DIFFERENCE between the two runs, so neither is "
|
||||
.."meaningful alone and neither gets to be the default.")
|
||||
M:exit()
|
||||
return
|
||||
end
|
||||
local ON = (ONS == "1")
|
||||
|
||||
local code do local f=io.open("clockgate.bin","rb"); code=f:read("a"); f:close() end
|
||||
|
||||
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
|
||||
|
||||
-- Settling frames between the gate reporting `running` and the window opening.
|
||||
-- The CPU may still be inside clk_init when the host first sees CGFLAG=1, and
|
||||
-- the first V-DISP edge after arming lands wherever the raster happens to be.
|
||||
-- Two frames puts the window entirely inside the steady state.
|
||||
local SETTLE = 2
|
||||
|
||||
local st, n = "boot", 0
|
||||
local f_ready, f0, f1 = nil, nil, nil
|
||||
local c0, c1, v0, v1, p0, p1, t0, t1
|
||||
-- Cadence: refreshes between consecutive frame ticks. Recorded as a histogram
|
||||
-- and as the raw first few, because the interesting claim is not the mean (the
|
||||
-- divider makes that exact by construction) but that the SPREAD is only ever
|
||||
-- the two values either side of fps*VTOTAL/HFREQ.
|
||||
local last_pace, last_pace_f, cad, seen_tick = nil, nil, {}, false
|
||||
|
||||
SUB = emu.add_machine_frame_notifier(function()
|
||||
local ok, err = pcall(function()
|
||||
n = n + 1
|
||||
if st == "boot" then
|
||||
if T() < 3.0 then return end
|
||||
MODE.apply(SP)
|
||||
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
|
||||
SP:write_u32(CGFLAG, 0)
|
||||
SP:write_u32(CGON, ON and 1 or 0)
|
||||
SP:write_u32(CLK_FPS, FPS)
|
||||
local cpu = M.devices[":maincpu"]
|
||||
cpu.state["SR"].value = 0x2700 -- clk_init lowers it to $2500 itself
|
||||
cpu.state["SP"].value = 0x8000
|
||||
cpu.state["PC"].value = 0x10000
|
||||
P(string.format("clockgate.bin=%d B, clock %s, asking for %d fps, "
|
||||
.."window %d raster frames", #code,
|
||||
ON and "ARMED" or "OFF (calibration run)", FPS, WIN))
|
||||
st = "wait"; return
|
||||
end
|
||||
if st == "wait" then
|
||||
local fl = SP:read_u32(CGFLAG)
|
||||
if fl == 0xEE then
|
||||
local e = SP:read_u32(CLK_ERR)
|
||||
P("clk_init REFUSED: CLK_ERR="..e..(e == 1 and
|
||||
" (CRTC is not in a 31.5 kHz mode, so HFREQ=31500 would be wrong)" or
|
||||
e == 2 and " (fps*VTOTAL does not fit the 16-bit accumulator)" or ""))
|
||||
M:exit(); return
|
||||
end
|
||||
if fl ~= 1 then
|
||||
if T() > 60 then P("TIMEOUT: the gate never started"); M:exit() end
|
||||
return
|
||||
end
|
||||
f_ready = n; st = "settle"; return
|
||||
end
|
||||
if st == "settle" then
|
||||
if n < f_ready + SETTLE then return end
|
||||
f0, t0 = n, T()
|
||||
c0 = SP:read_u32(CGCNT)
|
||||
v0 = SP:read_u32(CLK_VDISP)
|
||||
p0 = SP:read_u32(CLK_PACE)
|
||||
last_pace, last_pace_f = p0, n
|
||||
if ON then
|
||||
P(string.format("armed: incr=%d (fps*VTOTAL), acc=%d, first tick "
|
||||
.."pending", SP:read_u16(CLK_INCR),
|
||||
SP:read_u16(CLK_ACC)))
|
||||
end
|
||||
st = "run"; return
|
||||
end
|
||||
if st == "run" then
|
||||
if ON then
|
||||
local pc = SP:read_u32(CLK_PACE)
|
||||
if pc ~= last_pace then
|
||||
-- The FIRST change is dropped. Its interval runs from the window
|
||||
-- opening rather than from a tick, so it measures where the window
|
||||
-- happened to start and would show up as a spurious short bucket.
|
||||
if seen_tick then
|
||||
-- More than one tick in a single refresh would mean fps above the
|
||||
-- raster rate; give it its own bucket rather than averaging it in.
|
||||
local gap = n - last_pace_f
|
||||
if pc - last_pace > 1 then gap = 0 end
|
||||
cad[gap] = (cad[gap] or 0) + 1
|
||||
end
|
||||
seen_tick = true
|
||||
last_pace, last_pace_f = pc, n
|
||||
end
|
||||
end
|
||||
if n < f0 + WIN then return end
|
||||
f1, t1 = n, T()
|
||||
c1 = SP:read_u32(CGCNT)
|
||||
v1 = SP:read_u32(CLK_VDISP)
|
||||
p1 = SP:read_u32(CLK_PACE)
|
||||
st = "done"
|
||||
|
||||
local frames = f1 - f0
|
||||
local secs = t1 - t0
|
||||
local clocks = secs * CPUHZ
|
||||
local iters = c1 - c0
|
||||
local ints = v1 - v0
|
||||
local ticks = p1 - p0
|
||||
P(string.format("window: %d raster frames, %.6f s emulated -> %.0f "
|
||||
.."68000 clocks", frames, secs, clocks))
|
||||
-- THE INSTRUMENT IS 2.22% FAST AND IT IS WORTH SAYING SO EVERY RUN.
|
||||
-- The CRTC registers describe a 31,500 lines/s raster of VTOTAL lines.
|
||||
-- MAME does not run it at that rate: x68k_crtc.cpp refresh_mode()
|
||||
-- computes the frame period as (scr.max_x * scr.max_y) dots with
|
||||
-- scr.max_x = m_htotal - 8, one character cell short and an INCLUSIVE
|
||||
-- rectangle bound used as a count. So the emulated raster is fast by
|
||||
-- htotal/(htotal-8) -- 368/360 in this mode -- and every rate derived
|
||||
-- from it here is fast by the same factor. The divider under test is
|
||||
-- built on the registers, so its HARDWARE rate is the asked-for one and
|
||||
-- what this rig can check is that it tracks whatever raster it is given.
|
||||
local vtotal = SP:read_u16(0xE80008) + 1
|
||||
local htotal = (SP:read_u16(0xE80000) + 1) * 8
|
||||
local hw_hz = 31500 / vtotal
|
||||
local skew = htotal / (htotal - 8)
|
||||
P(string.format(" raster period %.4f ms = %.4f Hz", 1000*secs/frames,
|
||||
frames/secs))
|
||||
P(string.format(" the CRTC registers describe 31500/%d = %.4f Hz; "
|
||||
.."MAME is fast by htotal/(htotal-8) = %d/%d = %.4f",
|
||||
vtotal, hw_hz, htotal, htotal-8, skew))
|
||||
P(string.format(" loop iterations %d", iters))
|
||||
if ON then
|
||||
P(string.format(" V-DISP interrupts %d, frame ticks %d", ints,
|
||||
ticks))
|
||||
-- The self-check that makes the rest of it worth reading: the interrupt
|
||||
-- count and the host's screen-frame count are supposed to be the SAME
|
||||
-- clock seen from two sides. If they disagree by more than the one
|
||||
-- edge the window boundaries can straddle, the tick is not the raster.
|
||||
if math.abs(ints - frames) > 1 then
|
||||
P(string.format("FAIL: %d V-DISP interrupts over %d raster frames. "
|
||||
.."The tick is not coming from the raster.", ints,
|
||||
frames))
|
||||
M:exit(); return
|
||||
end
|
||||
-- Two numbers, and confusing them is the whole trap. The measured rate
|
||||
-- is against MAME's fast raster; dividing the skew out gives the rate
|
||||
-- the same code produces on a machine whose raster matches its own
|
||||
-- registers, which is the number the player is judged on.
|
||||
local meas = ticks/secs
|
||||
P(string.format(" measured rate %.6f fps against MAME's raster "
|
||||
.."(%+.0f ppm vs the asked %d)", meas,
|
||||
1e6*(meas/FPS - 1), FPS))
|
||||
P(string.format(" de-skewed %.6f fps -> %+.1f ppm from %d, "
|
||||
.."which is the tick quantisation of %d ticks and not "
|
||||
.."drift", meas/skew, 1e6*(meas/skew/FPS - 1), FPS,
|
||||
ticks))
|
||||
local ks = {}
|
||||
for k in pairs(cad) do ks[#ks+1] = k end
|
||||
table.sort(ks)
|
||||
local s = ""
|
||||
for _,k in ipairs(ks) do
|
||||
s = s .. string.format("%d:%d ", k, cad[k])
|
||||
end
|
||||
P(" cadence, refreshes per frame tick: "..s)
|
||||
end
|
||||
|
||||
local fh = io.open(OUT, "w")
|
||||
fh:write(string.format("on %d\nfps %d\nframes %d\nsecs %.15g\n"
|
||||
.."clocks %.15g\niters %d\nints %d\nticks %d\n"
|
||||
.."vtotal %d\nhtotal %d\nhw_hz %.15g\nskew %.15g\n",
|
||||
ON and 1 or 0, FPS, frames, secs, clocks, iters,
|
||||
ints, ticks, vtotal, htotal, hw_hz, skew))
|
||||
for k, v in pairs(cad) do fh:write(string.format("cad %d %d\n", k, v)) end
|
||||
fh:close()
|
||||
P("counts -> "..OUT)
|
||||
P("done")
|
||||
M:exit(); return
|
||||
end
|
||||
end)
|
||||
if not ok then print("[CLK] LUA ERROR: "..tostring(err)); M:exit() end
|
||||
end)
|
||||
@@ -0,0 +1,132 @@
|
||||
"""What the 68000's own frame clock costs, out of the two clock.lua runs.
|
||||
|
||||
ROADMAP P3. Usage: clock_cost.py <off-run.txt> <on-run.txt>
|
||||
|
||||
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]))
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# One frame-clock run: the 68000 derives its own 12 fps tick from the raster
|
||||
# (ROADMAP P3, FINDINGS 54).
|
||||
#
|
||||
# tools/bench/clock_run.sh [window-in-raster-frames] [fps]
|
||||
#
|
||||
# TWO MAME INVOCATIONS, and they are not interchangeable. The first leaves the
|
||||
# clock off and calibrates the cost of the gate's own loop; the second arms it.
|
||||
# The interrupt cost is the difference, so a run that reported only the second
|
||||
# would be reporting a number it cannot compute. See src/player/clockgate.s.
|
||||
#
|
||||
# Only MAME can run this: the frame clock is an MFP interrupt driven by the
|
||||
# CRTC's V-DISP output, and tools/bench/c68k has neither device. That is why
|
||||
# this stage has no second-core half, unlike load_run.sh.
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
WIN=${1:-3000}
|
||||
FPS=${2:-12}
|
||||
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/clockgate.bin src/player/clockgate.s > /dev/null
|
||||
|
||||
for ON in 0 1; do
|
||||
# stdbuf -oL: without it a long MAME run is unobservable until it exits, and
|
||||
# a run that is merely finishing looks exactly like one that is wedged (34.1).
|
||||
( cd tmp && DLX_CLK_ON=$ON DLX_CLK_FPS=$FPS DLX_CLK_WIN=$WIN \
|
||||
DLX_CLK_OUT=clock_$ON.txt SDL_VIDEODRIVER=dummy stdbuf -oL \
|
||||
timeout -k 5 600 mame x68000 -bios ipl10 -ramsize 2M -video soft -window \
|
||||
-sound none -nothrottle -plugins -autoboot_script ../tools/bench/clock.lua \
|
||||
-seconds_to_run 240 > clock_$ON.log 2>&1 )
|
||||
# A run that never reached the counts must fail as that, not as bad arithmetic.
|
||||
grep -q "^\[CLK\] done" tmp/clock_$ON.log || {
|
||||
echo "FAIL: the clock rig did not finish run ON=$ON -- no completion marker."
|
||||
tail -8 tmp/clock_$ON.log; exit 1; }
|
||||
done
|
||||
grep -a "^\[CLK\]" tmp/clock_1.log | sed -n '/window:/,/cadence/p' | sed 's/\[CLK\] / /'
|
||||
python3 tools/bench/clock_cost.py tmp/clock_0.txt tmp/clock_1.txt
|
||||
@@ -23,6 +23,26 @@
|
||||
-- Total blanking time is identical to the 768 mode (112 dots @ 11.592MHz =
|
||||
-- 336 dots @ 34.776MHz = 9.66us), which is what a real monitor needs.
|
||||
--
|
||||
-- THE EMULATOR DOES NOT RUN THE RASTER THESE REGISTERS DESCRIBE, and every
|
||||
-- rig in this tree samples the machine at ITS rate, not at the hardware's.
|
||||
-- x68k_crtc.cpp refresh_mode() builds the frame period as
|
||||
--
|
||||
-- (scr.max_x * scr.max_y) dots / dotclock, scr.max_x = m_htotal - 8
|
||||
--
|
||||
-- which is one character cell short AND uses an inclusive rectangle bound as a
|
||||
-- count. So MAME's refresh is fast by htotal/(htotal-8) = 368/360 = 1.02222:
|
||||
-- 56.6901 Hz where the registers say 55.4577. MEASURED, not read off the
|
||||
-- source alone -- tools/bench/clock.lua reports both every run, and they agree
|
||||
-- to six digits (FINDINGS 54.5).
|
||||
--
|
||||
-- It matters in exactly two places and is harmless in the rest. Anything timed
|
||||
-- by counting host frames has 1/56.69 s of granularity, not 1/55.46; and
|
||||
-- anything PACED by the raster runs 2.22% fast under MAME. It does NOT touch
|
||||
-- 68000 cycle figures: the CPU clock is 40 MHz/4 and has nothing to do with the
|
||||
-- screen. Do not "correct" the 55.4577 below to match a measurement -- it is
|
||||
-- the hardware's, derived from the dot clocks above, and it is what
|
||||
-- src/player/clock.i builds its divider on.
|
||||
--
|
||||
-- VERTICAL registers are NOT halved. The CRTC still generates a 568-line
|
||||
-- 31.5kHz raster (31500/568 = 55.46 Hz); "256 lines" is a graphics-layer
|
||||
-- double-scan (draw_gfx() halves gfxrect, x68k_v.cpp:401). Halving them would
|
||||
|
||||
@@ -93,7 +93,7 @@ end
|
||||
|
||||
-- The plan: one sequential correctness pass, then the cost anchors, then a
|
||||
-- full pass timed. Iteration counts target ~4 emulated seconds each so the
|
||||
-- 1/55.46 s timing granularity costs under 0.5%.
|
||||
-- 1/56.69 s timing granularity (crtc_mode.lua) costs under 0.5%.
|
||||
-- DLX_VERIFY_ONLY=1 drops the cost anchors and runs only the correctness pass,
|
||||
-- so tools/bench/check.sh can gate the decoder without paying for ~2 minutes of
|
||||
-- timing runs that would make the green light sensitive to host load anyway.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
--
|
||||
-- MEASUREMENT SCOPE, unchanged from decode.lua: MAME's memory carries no wait
|
||||
-- states, so these are pure 68000 instruction cycles -- a LOWER BOUND on real
|
||||
-- hardware. Interrupts are masked (SR=$2700). The host clock has 1/55.46 s
|
||||
-- hardware. Interrupts are masked (SR=$2700). The host clock has 1/56.69 s
|
||||
-- granularity and the job takes milliseconds, so each configuration is repeated
|
||||
-- LITER times and divided; repeating is honest because do_load is not
|
||||
-- temporally recursive -- every pass rewrites what the last one wrote, from the
|
||||
|
||||
+13
-2
@@ -15,12 +15,23 @@
|
||||
#
|
||||
# kbps 0 = unlimited pipe. There is no default rate anywhere in this tree
|
||||
# (FINDINGS 50) and there is none here either.
|
||||
#
|
||||
# DLX_PACE selects WHO KEEPS THE TIME: 1 (default) is the host writing the tick,
|
||||
# 2 is the 68000 writing it off the CRTC's V-DISP (ROADMAP P3, FINDINGS 54).
|
||||
# Everything else about the run is identical, which is the whole point -- the
|
||||
# pace gate in src/player/stream.s cannot tell them apart, so a difference in
|
||||
# the result is a difference in the CLOCK and not in the rig.
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
RING=${1:?ring KB}; KBPS=${2:?pipe KB/s, or 0 for unlimited}
|
||||
CUT_AT=$3; CUT_FR=${4:-1}
|
||||
DLX=${DLX:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
PACE=${DLX_PACE:-1}
|
||||
TAG="r${RING}_k${KBPS}${CUT_AT:+_cut${CUT_AT}x${CUT_FR}}"
|
||||
# The default tag is left ALONE when the host keeps the time: tools/bench/
|
||||
# pace_sweep.sh reads tmp/pace_r<ring>_k<kbps>.log by name, and renaming the
|
||||
# host-paced logs would break a sweep that has nothing to do with this option.
|
||||
if [ "$PACE" != 1 ]; then TAG="${TAG}_p$PACE"; fi
|
||||
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/stream.bin src/player/stream.s > /dev/null
|
||||
[ -f tmp/stream_disk.bin ] || python3 tools/bench/prep_stream.py "$DLX" > tmp/prep_stream.log
|
||||
@@ -29,7 +40,7 @@ mkdir -p "tmp/snap_pace_$TAG"; rm -f "tmp/snap_pace_$TAG/x68000"/*.png
|
||||
# of a prefix is not an assignment token, so bash takes the next word as the
|
||||
# command and the run dies with "SDL_VIDEODRIVER=dummy: command not found".
|
||||
CUTENV=(); [ -n "$CUT_AT" ] && CUTENV=(DLX_CUT_AT="$CUT_AT" DLX_CUT_FR="$CUT_FR")
|
||||
( cd tmp && env DLX_PACE=1 DLX_RING_KB=$RING DLX_STREAM_KBPS=$KBPS \
|
||||
( cd tmp && env DLX_PACE=$PACE DLX_RING_KB=$RING DLX_STREAM_KBPS=$KBPS \
|
||||
"${CUTENV[@]}" DLX_SLACK_CSV="slack_$TAG.csv" \
|
||||
SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 900 \
|
||||
mame x68000 -bios ipl10 -ramsize 2M -video soft -window -sound none \
|
||||
@@ -42,6 +53,6 @@ grep -q "snapshot taken" "tmp/pace_$TAG.log" || {
|
||||
echo "FAIL($TAG): no snapshot marker -- the pass did not complete."
|
||||
tail -6 "tmp/pace_$TAG.log"; exit 1; }
|
||||
echo "=== $TAG"
|
||||
grep -aE "decoder (PACED|FREE)|ring: |UNDERRUNS|SEEK SLACK|RING-BOUND|RATE-BOUND|BUILD TIME|PIPE CUT|DEADLINE|REQUIRED" \
|
||||
grep -aE "decoder (SELF-PACED|PACED|FREE)|FRAME CLOCK|ring: |UNDERRUNS|NO IDLE|SEEK SLACK|RING-BOUND|RATE-BOUND|BUILD TIME|PIPE CUT|DEADLINE|REQUIRED" \
|
||||
"tmp/pace_$TAG.log" | sed "s/\[STR\] / /"
|
||||
python3 tools/bench/verify_decode.py "$DLX" --snap "tmp/snap_pace_$TAG" | tail -2
|
||||
|
||||
@@ -107,7 +107,8 @@ end
|
||||
local function launch(cfg)
|
||||
push(STREAM, blob, cfg.off+1, cfg.len)
|
||||
clear_picture()
|
||||
-- ~4 emulated seconds per config: 1/55.46 s granularity costs under 0.5%.
|
||||
-- ~4 emulated seconds per config: 1/56.69 s granularity (crtc_mode.lua)
|
||||
-- costs under 0.5%.
|
||||
local est = cfg.nspans*(cfg.var == 5 and 60 or (cfg.var == 6 and 50 or 70))
|
||||
+ cfg.npix*10
|
||||
cfg.iter = math.max(4, math.floor(4*CPUHZ/est))
|
||||
|
||||
+84
-7
@@ -53,7 +53,17 @@
|
||||
-- (FINDINGS 42.1). A default is how a folklore number ends up
|
||||
-- silently underneath a table nobody restates it in.
|
||||
-- DLX_PREFILL_KB bytes to deliver before releasing the CPU (default 0)
|
||||
-- DLX_PACE 1 = hold the decoder to META.fps (default 0 = free-run)
|
||||
-- DLX_PACE 1 = hold the decoder to META.fps, with the HOST writing
|
||||
-- the tick (default 0 = free-run)
|
||||
-- 2 = hold it to META.fps with the 68000 writing its own
|
||||
-- tick, off the CRTC's V-DISP (src/player/clock.i, ROADMAP
|
||||
-- P3). Same gate, same ring, same deadlines; what changes
|
||||
-- is that nothing outside the machine decides when a frame
|
||||
-- may start. Deadlines are then taken from the ticks the
|
||||
-- machine actually emitted rather than from a host model of
|
||||
-- 12 fps -- which matters, because MAME's raster runs 2.22%
|
||||
-- fast (see tools/bench/clock.lua) and a host model would
|
||||
-- quietly grade every arrival against the wrong clock.
|
||||
-- DLX_CUT_AT frame tick at which the pipe stops dead (a seek). Needs
|
||||
-- DLX_PACE; unset = no cut.
|
||||
-- DLX_CUT_FR how many frame times the cut lasts (default 1)
|
||||
@@ -88,7 +98,9 @@ local META = loadfile("stream_meta.lua")()
|
||||
local FLAG, ITER, NFR = 0x18000, 0x18008, 0x1800C
|
||||
local RD_PTR, FR_HEAD, FR_TAIL = 0x18020, 0x18024, 0x18028
|
||||
local STALLS, SPINS, DESC = 0x1802C, 0x18030, 0x18100
|
||||
local PACE, PACEON = 0x18034, 0x18038
|
||||
local PACE, PACEON, CLKON = 0x18034, 0x18038, 0x1803C
|
||||
local CLK_VDISP, CLK_FPS, CLK_ERR = 0x18064, 0x18068, 0x1806C
|
||||
local LATEFR, LATEMAX, LATE1ST = 0x18080, 0x18084, 0x18088
|
||||
local DESCN = 64
|
||||
local CB1, CB4 = 0x20000, 0x22000
|
||||
local RING = 0x40000
|
||||
@@ -108,7 +120,8 @@ if KBPS == nil then
|
||||
return
|
||||
end
|
||||
local PREFILL = (tonumber(os.getenv("DLX_PREFILL_KB") or "") or 0) * 1024
|
||||
local PACED = (os.getenv("DLX_PACE") == "1")
|
||||
local SELFCLK = (os.getenv("DLX_PACE") == "2")
|
||||
local PACED = (os.getenv("DLX_PACE") == "1") or SELFCLK
|
||||
local CUT_AT = tonumber(os.getenv("DLX_CUT_AT") or "")
|
||||
local CUT_FR = tonumber(os.getenv("DLX_CUT_FR") or "") or 1
|
||||
local SLACK_CSV = os.getenv("DLX_SLACK_CSV")
|
||||
@@ -269,9 +282,13 @@ local function setup()
|
||||
SP:write_u32(FLAG, 0); SP:write_u32(FR_HEAD, 0); SP:write_u32(FR_TAIL, 0)
|
||||
SP:write_u32(ITER, 1); SP:write_u32(NFR, META.nframes)
|
||||
SP:write_u32(PACE, 0); SP:write_u32(PACEON, PACED and 1 or 0)
|
||||
SP:write_u32(CLKON, SELFCLK and 1 or 0)
|
||||
SP:write_u32(CLK_FPS, META.fps)
|
||||
P(string.format("stream.bin=%d B, codebooks %d+%d B, disk %d B, %d frames",
|
||||
#code, META.cb1_len, META.cb4_len, META.disk_len, META.nframes))
|
||||
P(string.format("decoder %s%s", PACED and ("PACED at "..META.fps.." fps")
|
||||
P(string.format("decoder %s%s", SELFCLK
|
||||
and ("SELF-PACED at "..META.fps.." fps off V-DISP")
|
||||
or PACED and ("PACED at "..META.fps.." fps by the host")
|
||||
or "FREE-RUNNING (tests wrap, not buffering -- 49.7.2)",
|
||||
CUT_AT and string.format(", pipe cut at tick %d for %.2f fr",
|
||||
CUT_AT, CUT_FR) or ""))
|
||||
@@ -292,6 +309,12 @@ local function launch()
|
||||
end
|
||||
|
||||
local st, t0, t_rel = "boot", nil, 0
|
||||
-- tick_t[i+1] = emulated time of frame tick i, filled in as they are observed.
|
||||
-- Under a self-clock this replaces the host's `t_rel + i/fps` as the deadline:
|
||||
-- the machine's clock is the one the player is actually held to, and MAME's
|
||||
-- raster is 2.22% fast, so grading arrivals against a host model of 12 fps
|
||||
-- would flatter every record by that much.
|
||||
local tick_t = {}
|
||||
local pace, min_ahead, min_at = -1, math.huge, -1
|
||||
local sum_ahead, n_ahead = 0, 0
|
||||
local slack_series = {}
|
||||
@@ -314,10 +337,21 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
end
|
||||
if st == "running" then
|
||||
if PACED then
|
||||
local tick = math.floor((t - t_rel) * META.fps)
|
||||
-- WHO WRITES THE TICK. Host-paced, the tick is a host model of
|
||||
-- META.fps and this script advances it. Self-paced, the 68000 has
|
||||
-- already advanced it off the raster and this script only READS it --
|
||||
-- the frame gate in src/player/stream.s cannot tell the two apart,
|
||||
-- which is the point: the same bytes are gated either way.
|
||||
local tick = SELFCLK and SP:read_u32(PACE)
|
||||
or math.floor((t - t_rel) * META.fps)
|
||||
if tick > pace then
|
||||
pace = tick
|
||||
SP:write_u32(PACE, pace)
|
||||
if not SELFCLK then SP:write_u32(PACE, pace) end
|
||||
-- The emulated time this tick actually happened, which is what a
|
||||
-- record's deadline is measured against under a self-clock. Ticks
|
||||
-- can arrive more than one apart if the host misses a frame, so the
|
||||
-- whole run is filled rather than just the newest.
|
||||
for k = #tick_t, tick do tick_t[k+1] = t end
|
||||
-- Taken BEFORE this tick's frame is decoded, so snapshot n is the
|
||||
-- finished picture of frame n-1. tick 0 is skipped: nothing has been
|
||||
-- drawn yet and it would record a black screen as a decoded frame.
|
||||
@@ -358,6 +392,14 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
.."or described wrongly, not that the codec changed.")
|
||||
M:exit(); return
|
||||
end
|
||||
if fl == 0xE2 then
|
||||
local e = SP:read_u32(CLK_ERR)
|
||||
P("FRAME CLOCK REFUSED: CLK_ERR="..e..(e == 1 and
|
||||
" (the CRTC is not in a 31.5 kHz mode, so the divider's 31500 "
|
||||
.."lines/s would be wrong)" or e == 2 and
|
||||
" (fps*VTOTAL does not fit the 16-bit accumulator)" or ""))
|
||||
M:exit(); return
|
||||
end
|
||||
if fl == 0xE1 then
|
||||
P("PRODUCER STALLED OUT -- stream.s spun SPINMAX times with no new "
|
||||
.."record. Delivered "..nsent.."/"..META.nframes..".")
|
||||
@@ -382,6 +424,25 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
META.nframes, dt, dt*CPUHZ/META.nframes,
|
||||
100*(dt*CPUHZ/META.nframes)/FRAME12, META.fps))
|
||||
end
|
||||
if SELFCLK then
|
||||
-- The clock's own report, in the run where it actually paced a
|
||||
-- decoder rather than a busy loop. The rate is against MAME's fast
|
||||
-- raster; tools/bench/clock_run.sh is where it gets de-skewed.
|
||||
local vd = SP:read_u32(CLK_VDISP)
|
||||
local vt = SP:read_u16(0xE80008) + 1 -- VTOTAL, as clk_init read it
|
||||
-- DELIBERATELY NOT A RATE. 120 ticks is far too short a window to
|
||||
-- quote fps from: the run's start and end each straddle a tick, so
|
||||
-- +/-1 on 119 intervals is +/-8000 ppm and would read as drift the
|
||||
-- clock does not have. What IS exact here is a count of refreshes
|
||||
-- against a count of ticks. The rate figure comes from
|
||||
-- tools/bench/clock_run.sh, over thousands of them.
|
||||
P(string.format("FRAME CLOCK: %d V-DISP interrupts drove %d ticks "
|
||||
.."(%.4f refreshes/frame; the divider's own ratio is "
|
||||
.."31500/(%d*%d) = %.4f), %.0f clocks/frame of "
|
||||
.."interrupt at FINDINGS 54's 181.35 each",
|
||||
vd, pace + 1, vd/(pace + 1), META.fps, vt,
|
||||
31500/(META.fps*vt), 181.35*vd/(pace+1)))
|
||||
end
|
||||
P(string.format("ring: %d wraps, %d B of hole (mean %.1f KB, %.1f%% of "
|
||||
.."the ring)", holes, hole_bytes,
|
||||
holes > 0 and hole_bytes/holes/1024 or 0,
|
||||
@@ -395,6 +456,20 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
-- opposite thing, so the two are never printed in the same words.
|
||||
P(string.format("UNDERRUNS: %d/%d frames waited past their %d fps slot "
|
||||
.."(%d polls)", stalls, META.nframes, META.fps, spins))
|
||||
-- The other way a paced frame can go wrong, and it is not the same
|
||||
-- failure. An UNDERRUN is the pipe: the record was not there. This
|
||||
-- is the CPU: the record was there, the slot was already open, and
|
||||
-- the decoder had no idle left in the frame before. Under a
|
||||
-- host-written tick every slot is 83.33 ms; under the machine's own
|
||||
-- clock they alternate 72.13 and 90.16 ms, 37.9% of them short, and
|
||||
-- this is what the short ones cost (FINDINGS 54).
|
||||
local late, latemax = SP:read_u32(LATEFR), SP:read_u32(LATEMAX)
|
||||
local late1 = SP:read_u32(LATE1ST)
|
||||
P(string.format("NO IDLE: %d/%d frames found their slot already open "
|
||||
.."-- the frame before used all of it; worst overrun "
|
||||
.."%d whole tick%s, first at frame %d", late,
|
||||
META.nframes, latemax, latemax == 1 and "" or "s",
|
||||
late1 == 0xFFFFFFFF and -1 or late1))
|
||||
-- WHAT THE MINIMUM OVER A RUN IS, AND IS NOT. At release the ring
|
||||
-- holds only what the prefill put there, so the early ticks report a
|
||||
-- buffer that has not been built yet, not a ring or a pipe that
|
||||
@@ -455,7 +530,9 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
for i = 0, META.nframes-1 do
|
||||
local a = arrival[i+1]
|
||||
if a then
|
||||
local late = a - (t_rel + i / META.fps)
|
||||
local due = SELFCLK and tick_t[i+1] or (t_rel + i / META.fps)
|
||||
if not due then due = t_rel + i / META.fps end
|
||||
local late = a - due
|
||||
if late > 0 then
|
||||
misses = misses + 1
|
||||
if late > worst then worst = late end
|
||||
|
||||
Reference in New Issue
Block a user