Repository navigation
Conversation
Convert serialized ROM records to native layouts at load boundaries and remove 32-bit address and runtime-layout assumptions. Add Cocoa/SDL build and fullscreen support, ROM-free regression fixtures, macOS CI, build documentation, and converter attribution.
Fixes the crackle/pop and slow-music report in murkantor#82, and the reload-sound latency that preceded it. The pump in 1.0.2b took its render count from the simulation's field counter (src/game/frametiming.c currentFrameCounter) and corrected an over-full queue by skipping whole rendered field frames. Both are wrong in ways that compound: - alAudioFrame is the ONLY thing that advances the game's audio clock. The sound player's event queue and the three music sequencers all move in rendered samples (src/libultra/audio/synthesizer.c:143-227). So the render count is simultaneously "how much audio" and "how fast audio time runs", and tying it to the game loop means every stalled frame (stage load, the intro swirl, a window drag, alt-tab) delivers no audio: the device drains, plays silence, and resumes with a discontinuity. That is a click. Often enough, it is continuous crackle and music that runs behind real time. It also made audio behaviour depend on maxfps, since at uncapped rates the field delta is usually 0 and only the queue-low branch rendered at all. - Dropping a rendered frame throws away ~16.7 ms of synthesised audio mid-stream. That is a step discontinuity by construction - the N64 never did it. src/audi.c:529 sized the frame from the AI's remaining buffer instead ((g_FrameSize - (osAiGetLength() >> 2)) + 16 + EXTRA_SAMPLES): a sample-level servo, inaudible correction. The port lost it because portAudioPump calls alAudioFrame directly and the shim stubs osAiGetLength() to 0. Both are fixed by splitting the one decision into the two it always was: - A LIVE DEVICE SETS THE RATE. Refill toward the target depth. The render rate then equals the DAC's consumption rate - the queue is stable, device clock drift is absorbed, and audio time is the hardware's own time. This is what amHandleFrameMessage did. The correction is a few samples of frame length, so nothing is ever dropped or duplicated. - THE WALL CLOCK SETS THE RATE ONLY WHEN THERE IS NO USABLE DEVICE, and those samples are discarded. Game audio time still advances, so a missing or wedged sink cannot freeze the sequencers or saturate the SFX queue. Also in this change: - Target raised to 40 ms (from 40.6 ms nominal, but now actually held) and it ratchets up on an underrun, easing back only in a clean 10 s window. The effective target is floored at one pump interval plus a device period: a target shallower than the interval between pumps starves by construction. - The pump-interval floor is why 40 ms is enough. At 60 fps the interval is 16.7 ms, so the floor never binds; below ~20 fps it takes over. - SINGLE-DIGIT FRAME RATES ARE SUPPORTED. Classic and demo modes can drop there, and at N fps the pump runs N times a second, so each pump must bridge 1000/N ms of audio. A fixed 16 KB cap holds only 185 ms, which is less than one interval at 5 fps, so the cap now scales with the interval (and the per-pump render budget with it). Stated plainly: at those rates the backlog IS the audio latency - 500 ms at 2 fps. That is inherent to pumping from the render loop; continuous audio at high latency beats crackle at low latency. - portAudioPumpForce no longer floods the queue during music.c's track-stop spins, which used to push seconds of audio into a 186 ms buffer and leave the trim to clean up. A forced frame advances the sequencer and is queued only when the queue is genuinely shallow. - The queue cap is checked BEFORE alAudioFrame, not after. Rendering a frame that cannot be queued is not free: alAudioFrame advances the game's audio clock, so the drop path both burned the time and left the clock ahead of what was delivered. Nothing is now synthesised that cannot be delivered. - The pacing rules moved into port/src/audio_pace.c + port/include/audio_pace.h as pure functions - no SDL, no engine headers, no globals, no clock reads - driven by port/tests/audio_pace.c under GE_PORT_TESTS=ON (ctest -R audio_pace). - GE_AUDIODBG=1 prints a one-second trace (pumps, samples rendered vs needed, queue open->close vs target, clock credit, stalled, forced) so the next audio report can be read instead of guessed at. Two bugs this change found by running the game rather than reasoning about it, both now pinned by tests named after them: - The first cut paced on the wall clock AND steered the queue from the same budget (credit time, add a bounded bias toward the target, charge it back). It compiled and passed 15 tests including a closed-loop simulated device, and it starves on hardware: if total rendered must equal wall time while the device also consumes wall time, the queue depth is a constant of integration. A bounded bias changes when samples arrive, never the running total. Measured: queue cycling 1.2-3.6 KB against a 10 KB ratcheted target, 24 dry pumps in the first 10 s. - Rounding the refill DOWN to the 16-sample alignment loses a few samples on every pump - ~11% short at 300 fps, which drains the queue whatever the target is. Round up; the "at or above the mark" early return is the hysteresis. And one caught by review after the fact, now fixed: the over-cap check sat after alAudioFrame, so a frame that did not fit was synthesised and then discarded, which is exactly what the comment above it claimed to prevent. Known limits, deliberately left for their own changes: - The device is still opened at an exact 22050 Hz with allowed_changes = 0. On Windows shared mode (48000) that puts an OS resampler and a fallback driver between us and the DAC, with mixer quanta near our 11.6 ms device period - a plausible environmental cause for murkantor#82 that we cannot reproduce on macOS. Briefing for the SDL_AudioStream fix: docs/port/rate_prompt.md. - A forced frame advances the sequencer's musical time without delivering it when the queue is not shallow, so a track-stop burst can push the playing tracks slightly ahead. Pre-existing (1.0.2b rendered and dropped the same frames) and bounded by the stop; the real fix is wall-clock pacing those spins in music.c. Verified on macOS/CoreAudio at 22050 Hz, 256-sample period, title screen (stage 90), 24 s run with GE_AUDIODBG=1: before, queue 1216->3584 B against a 10240 target with 24 dry pumps in the first window; after, queue steady at 3556 B against a 3520 target and no `ge: audio: last N s:` health line at all - zero dry pumps, zero over-cap drops, zero skipped frames. ctest: 8/8, including the 16 audio_pace cases (which cover 1-300 fps). Not verified on Windows, which is the reporter's platform; the low-frame-rate path is covered by simulation only, not by a real single-digit-fps run. Full background, including the four earlier pump designs and why each was replaced: docs/port/audio-pump-history.md.
`cl_audio_pump 1` draws the pump's state as six rows in the top-left corner:
FPS and TPS, pump calls against the ones that did work, queue against its
target and the cap, ms of audio rendered against the ms of wall time it was
worth, the four failure counters, and the stall verdict with the wall-clock
credit and device rate. `cl_audio_log 1` writes the same numbers, in the same
units, to Data/Logs/audio_pump.log once a second, flushed per line so a crash
keeps the tail. Neither is a midas.ini key: they are diagnostics, and a restart
should not leave one on because it was typed once.
The block REPLACES the FPS counter rather than sitting under it. show_fps draws
one number there and the block carries FPS and TPS itself, so with both on the
same two figures would print twice; the digits are suppressed while the block
is up, and the block takes over the corner the strip used, not a row below it.
The snapshot lives in port/include/port_audio_stats.h and is filled by the pump
in audio_pc.c, which already computed every number for its health line and
GE_AUDIODBG trace; this only publishes them. FPS and TPS are deliberately not
copied into the snapshot - the overlay reads g_PortStatFps/Tps directly, so the
block cannot disagree with the strip it displaces.
Three things the field work changed:
- UNITS, on every field. The first cut printed window totals under a "/s"
(`SND 11008/11025 /s`), which reads as a rate and is not one. Samples are
now milliseconds of audio; CLK is milliseconds rather than samples x1000;
DEV carries Hz.
- PMP. portAudioPump runs every scheduler iteration, not once per drawn
frame, so the call rate (~133k/s) dwarfs the useful work and says nothing
about sound. The row now shows PMP and RND - calls per second against the
ones that rendered anything (~130/s), which is the pair that makes an
over-serviced pump visible. The old `IN` field is gone: at sub-millisecond
intervals it read 0ms forever, a loop-speed number wearing an audio label.
- One theme per row, six rows. FPS/TPS, PMP/RND, QUE/CAP, SND, the counters,
and STALL/CLK/DEV - previously the first row mixed three unrelated
measurements and the odd `130r` suffix had no unit at all.
And one bug found by running it rather than reading it: the window accumulators
were fed only while the overlay was on, so `cl_audio_log 1` on its own wrote a
file of plausible-looking zeroes. Both consumers now feed the window.
The log then gained two things the screen deliberately keeps out:
- EXACT FIGURES ALONGSIDE THE READABLE ONES. `snd` carries the raw sample
counts as well as ms, and `que` carries ms of audio as well as bytes. One
sample is 0.045 ms, so the ms form rounds away anything under ~22 samples -
a slow leak of a few samples per pump is invisible on screen and obvious in
the counts. Queue in ms answers the other question a user actually asks
("why does the sound lag?") in the unit they experience. Both forms sit in
the line, so a log line still reads beside a screenshot.
- A WALL-CLOCK START STAMP in the header, with t= per line left relative.
Nothing else the game logs carries a clock, so without this a report says
"it happened at t=45000ms" and that cannot be tied to a crash.log mtime, a
`ge: stats` line, or a user's "about 21:40". Wall clock on every line was
rejected on purpose: repetition, and a mid-run clock correction would make
the file read out of order, whereas relative t= cannot go backwards.
The block is gated per row on dynGetFreeGfx2, using the console's own 12-Gfx-
per-glyph rule. It is ~130 glyphs where the FPS strip draws two or three; a
cramped frame now shows a short block instead of overrunning the display list.
Weak stubs in libultra_shim.c keep the headless test targets linking, since
native_player.c already links bondview2.c.
tools/console_help.txt is the user-facing manual: what the pump is, how each
field reads, which values are healthy, the FPS-counter interaction, why the log
carries more units than the screen, what the timestamps mean, and what to
attach to a sound report. It ships beside the executable like
midas_ini_help.txt (CMake copy) and is listed in the readme's file table with
the log.
Verified on macOS: ctest 8/8; live runs through the GE_CONCMD harness with both
commands enabled, the log producing one line a second with correct values
(pmp ~147k/s, rnd ~130/s, que 3556/3520B 40/40ms, snd 11008/11025smp 499/500ms,
clk 0ms, all counters zero) under a `# started 2026-10-25 00:29:24` header. The
on-screen block was checked against three screenshots, including the target
easing back from its ratcheted 4544 B to the 3520 B floor and the FPS digits
disappearing when both overlays are on. Not verified on Windows or Linux.
The in-game block publishes rates derived from engine counters, and those
rates had no test: the arithmetic lived inline in gfx_sdl2.cpp's stat pass,
where it can only be checked by looking at a running game at a known frame
rate. This lifts it into port/src/tick_stats.c as pure functions, following
the audio_pace pattern already used for the audio pump's pacing rules, and
drives them from port/tests/tick_stats.c.
Three functions:
portStatsRatePerSec(delta, dt_ms, scale)
a counter delta over a window, as a rate per second, scaled and
rounded. The numerator is widened to 64 bits before the multiply:
delta * scale * 1000 overflows 32 bits as soon as a window is long
(a debugger pause, a ten-second stall), and a wrapped numerator reads
as a plausible but wildly wrong rate.
portStatsFieldsPerTickX100(fields, ticks)
the ratio, with 0 for a window that admitted no tick rather than a
division by zero.
portStatsReseatTickBaseline(last, now)
a counter that went backwards is a reset, not a measurement.
The cases pin the three readings that matter: 60 Hz healthy (600 = 60.0
fields/s, 100 = 1.00 fields/tick); 120 Hz, where the tick rate is 120 while
fields/s is still 60.0 and fields/tick is 50; and the stalled window a
software-rendered run produced (8 fields in 479 ms = 167 and 800), which is
what identifies a simulation losing wall-clock time.
No behaviour change: nothing calls these yet. ctest grows from 8 to 9.
Verified: builds clean; ctest 9/9, tick_stats included.
…e TPS Two things in the frame-timing stats, and one is a bug. FIELDS. g_GlobalTickCount is not a field counter. lv.c advances it by one per tick that pushed the clock forward, however many video fields that tick absorbed, so its rate is min(tick rate, 60): it reads 60 at tickhz=120 and it tracks fps whenever frames overrun a field. The title bar called it "engine FIELDS/sec", which was only true while every frame fitted inside one field. The honest field counter is g_PortFieldsTicked. The first cut of this change advanced it in updateFrameCounters, from the same integer deltaFrames that becomes speedgraphframes. That was wrong, and the wrongness is why it now lives in lvlTick: updateFrameCounters runs BEFORE lvlTick decides whether the tick counts, and lvlTick zeroes g_ClockTimer when the game is paused or the controls are locked. Counting at the point of production made f/s read a healthy 60.0 through every pause - a false healthy on the one number that is supposed to be trustworthy. It is now advanced in lv.c beside `g_GlobalTimer += g_ClockTimer`, so the field count and game time are the same sum by construction, and f/s reads 0 while paused. It is a relaxed atomic because the stat pass and the hitch log read it from the render thread. With it, three new published values: g_PortStatTicks (the real tick rate, from g_PortTickSerial - NOT g_PortTicksAdmitted, which boss.c increments before its pendingGfx < 2 backpressure guard, so it counts ticks that never reached lvlTick), g_PortStatFieldsX10 (fields credited per second, x10, where 600 = 60.0 = game speed correct), and g_PortStatFieldsPerTickX100 (fields per tick, x100, where 100 = one field per tick). The rate maths is the tested tick_stats.c from the previous commit. THE BUG. lvlStageLoad zeroes g_GlobalTickCount, and this pass kept its baseline in a static in another translation unit, so it could not see the reset. The first window after a load computed 0 - last and published -2x the pre-load tick count: -60 after half a second of play, -2400 after twenty, for up to 500ms, into the title bar and the audio pump log. A reset is not a measurement, so the baseline is re-seated through portStatsReseatTickBaseline and that window reads 0. g_GlobalTickCount is snapshotted once for the arithmetic, so a reset landing mid-window cannot reproduce the publish the guard exists to prevent. g_PortStatTps survives unchanged for the log and the title bar, with its real meaning written at its definition so the next session does not re-derive it. Verified: builds clean; ctest 9/9; a software-rendered run publishes f/s 16.8 / f/t 8.00 where the credit cap is pinned, and 60.0 / 1.00 on a normal frame.
The first row of cl_audio_pump read `FPS 60fps TPS 60tps`, and TPS was not the
tick rate. It could not be: lv.c advances g_GlobalTickCount once per tick that
pushed the clock forward, so its rate is min(tick rate, 60). A 120 Hz sim
printed 60, and below 60 fps it simply tracked the frame rate - the reading
that prompted this work was `15fps 15tps` in a tickhz=hw session, which looks
like a broken counter and is in fact the console's own behaviour.
Row 1 is now the four numbers that cannot mislead:
%dfps %dtk %d.%df/s %d.%02df/t
fps presented frames, tk sim ticks per second, f/s fields credited to the sim
per second (60.0 = game speed correct, at any tick rate and any frame rate),
and f/t fields absorbed per tick (1.00 = one field per tick, above 1.00 =
frames overran a field and the tick carried the difference, below 1.00 = a
raised tick rate sub-ticking one field). tk is g_PortTickSerial, which advances
once per lvlTick call - NOT g_PortTicksAdmitted, which boss.c increments before
its pendingGfx backpressure guard and so counts ticks that never stepped.
f/s is the invariant that was missing from the display entirely; it is the
number that says whether simulated time is keeping up, and the only one that
exposes a sim losing wall-clock time.
The audio pump log gains the same two figures beside the old pair rather than
replacing them, so a log line still reads against older captures:
`FPS %d TPS %d tk %d f/s %d.%d f/t %d.%02d`. Its legend now states what TPS
actually is, and why f/s is the one to trust.
Both consumers still read the same globals the strip uses, so the block and
the file cannot disagree; port_audio_stats.h keeps saying so, now naming all
four.
Three comment-only corrections ride along, all of them fixing text that claimed
more than the code does. libultra_shim.c's weak portVideoShowFps default said
"no FPS/TPS overlay" when nothing draws TPS any more. pcsched.c and video.c
described the pump and the tick coupling in terms the counters do not support.
None changes behaviour.
Verified: builds clean; ctest 9/9. Live, the row reads 60fps 60tk 60.0f/s
1.00f/t on a healthy frame, 832fps 832tk 60.0f/s 0.07f/t uncapped, and
15fps 15tk 59.8f/s 4.00f/t in a level - the last of which is the reading that
proves no game time is lost when frames overrun fields.
tickhz=hw prices each frame at the number of video fields the console would
have needed (portHwPaceFields, 1..30) and boss.c holds the tick for that many
fields. The credit path does not have to agree: waitForNextFrame clamps a
measured tick at GE_TICKMAXSTEP, which defaults to 8 fields. Any frame the
model prices above 8 is waited for in full and then credited only 8, so the
simulation silently discards the difference and the game falls behind real
time. At the model's own ceiling of 30 fields that is 22 fields, 367ms, gone
per frame.
That is invisible at 60fps, where every frame is one field, and it is the
reason a field-rate figure was needed at all. This adds the pair to the hitch
line, where it is only printed when a frame was already long enough to log:
sim: ... 1 tick(s) 8 fld model held 12
`fld` is the fields credited to the sim since the previous frame
(g_PortFieldsTicked), `model held` is what the pacer asked for. Equal is
healthy; fld short of it is lost simulated time.
The value is READ from g_PortHwFieldsHeld, published by portHwPaceFields in
the previous commit, and deliberately not recomputed here: this runs at present
time, which is the render thread whenever render_thread=1, and
portHwPaceFields mutates the model's statics and consumes the pending load
charge through portHwTakeLoadMs (a non-atomic read-then-clear that would drop a
charge arriving between the two). Both counters are read with relaxed atomics,
as every other cross-thread counter in this file is.
`model held 0` means the model has not priced a frame yet, not that it asked
for zero fields - a frame is always at least one - so the field is omitted
until there is something to print.
Verified: builds clean; ctest 9/9; observed live in a hw-pacing run.
The block's first row publishes rates derived from engine counters, and the six source files that produce or consume them had nowhere to point for the reasoning. This is that document. docs/port/tick-and-field-accounting.md follows FPS and TPS from osGetCount to the glyph, one hop per line, naming the file and either its line or the symbol that does the work: the CPU counter, what a 60 Hz field is in its units, the main loop's admission gate, the field banking in waitForNextFrame, updateFrameCounters' split into speedgraphframes and portTickDelta, lvTick's copy into g_ClockTimer, the one-line increment that makes g_GlobalTickCount a tick counter rather than a field counter, the 500 ms stat window, and the sprintf. It ends with the sentence the whole thing exists to make unambiguous: FPS counts presents, TPS counts clock-advancing ticks, g_GlobalTimer counts fields, and all three shared one word. Then, in order: what the two digits mean (TPS is min(tick rate, 60), with the eight-case table showing fields/s at 60.00 in every row); the four tick modes; the three places the old labels were wrong; the two defects (a negative TPS published after every stage load, and a tickhz=hw frame held for up to 30 fields but credited at most 8); the warp hypotheses ranked by mechanism; the options considered and which was taken; a constants table; what is verified and what is not; and the open questions. Sections 11 and 12 were added after the implementation, from measurements rather than reasoning: that the 8-field credit cap does not fire at ordinary frame rates (a long spell at 15 fps ran f/t 4.00 with f/s 60.0 throughout); that a Dam stage load is priced at 8742.8 ms and held for all of it with no rendering, confirmed by playing it, with two thirds of the setup CPU in a single coefficient; that maxfps=-1 written beside tickhz=0 re-arms hw pacing on every launch, proven by hw forcing room_cache to 0; and that GE_STAGE launches never reach the hold. 12 indexes every published number to the code producing it. The white-blank-screen-during-the-hold question is recorded as DEFERRED rather than solved, with its two parts kept apart: whether the framebuffer should be cleared, and whether a loading indication belongs there at all. Force-added: /docs/* is gitignored because the design docs live in a private tree, but three others are tracked anyway and five source files cite this one, so a dangling reference in a fresh clone would be worse than the exception.
Two user-facing manuals shipped beside the executable, and both described the
frame and tick rates in ways the code does not behave. Audited field by field
against the code and against measured sessions (105 and 157 one-second log
lines, plus screenshots of the same build).
console_help.txt had the block's first row wrong at the root: it documented
FPS and TPS, and TPS is not the tick rate. It is min(tick rate, 60), because
lvTick advances g_GlobalTickCount by one per tick that pushed the clock
forward, so a 120 Hz sim printed 60 and below 60 fps it simply tracked the
frame rate. The row is now documented as fps, tk, f/s and f/t, with f/s named
as the number to trust: 60.0 means simulated time is keeping up, at any tick
rate and any frame rate, and below it the game is losing real time.
Three measured examples replace the single one, because one sample cannot show
which figures are supposed to move. At 60, 738 and 15 fps, f/s reads 60.0 in
all three while f/t reads 1.00, 0.08 and 4.00 - the tick rate and the fields
per tick always multiply to the field rate, which is the property the row
exists to expose.
The audit also caught errors the TPS fix did not:
- the pump described as running "once per frame" when it runs once per pass
of the game loop (177k calls/s at 738 fps, 34,712/s at 0 fps);
- PMP described as following the frame rate, which it does not;
- RND "around 60 a second is the mixer keeping up" when 60 is the floor with
the loop blocked, the observed range being 56-60 there and 120-170 in
free-running play - SND, not RND, says whether the mixer kept up;
- the log's growth rate, which this branch's own longer lines invalidated
(8 KB a minute was measured at 12).
midas_ini_help.txt said "one game tick runs per frame, so this also sets the
tick rate", which is false for -1 - the mode where it matters most, since under
N64 pacing the tick rate stays 60 while the frame rate falls. It also said
nothing about what a player is buying, so it now records that a stage load is
held for the console's load time, several seconds on a blank screen, and that
entering Dam costs about 9 seconds. maxfps=0 was ambiguous: it is a number, but
it leaves both rates uncapped rather than coupling them.
The audio_pump.log legend in audio_pc.c carried the same PMP and RND claims and
is written into every log file a user attaches to a report, so it is corrected
in the same terms.
The audio pump log carried a TPS field, inherited from the commit that added the log. It answers a question nobody asks and gives a misleading answer when they do: lv.c advances g_GlobalTickCount once per tick that pushed the clock forward, so its rate is min(tick rate, 60). A 120 Hz session printed 60, and below 60 fps it simply tracked the frame rate - which is the reading that started this whole investigation, `15fps 15tps` in a Hardware-pacing session. The two numbers that replaced it in the block answer both halves properly: `tk` is the real tick rate, and `f/s` says whether simulated time is keeping up. TPS is redundant against them and adds nothing, so it goes, along with its legend entry. Old logs and new logs differ by one field; the rest of the line is unchanged and `t=` still heads it. g_PortStatTps survives as a global and is still computed. It arrived with the original decompilation import, nothing reads it, and deleting a symbol from the imported set is not this change's business. The in-game `help` still described `cl_showfps` as "the FPS / TPS strip" - text from the original import that is now false, since no TPS is drawn anywhere - and described the pump block as sitting "under" the strip when it replaces it. docs/port/tps-the-lie.md records the whole mislabel: the four places that shipped it (the manual entry, the log legend, the stat-pass comment, and the still-live lv.c comment), the three measured rows that disprove it, the negative-TPS defect, and what replaced it. It is force-added, because docs/ is gitignored.
A third pass re-derived every claim in these files from the code and from execution rather than from the branch's own account of itself. Five sentences did not survive; this corrects them. No behaviour changes. console_help.txt TK said "three quite different things hold it lower" and named the display, the machine and Hardware pacing. All three are instances of one gate the entry never stated: a tick is taken only once the previous frame has been presented (boss.c's pendingGfx guard), so TK tracks FPS whenever drawing is what the machine is limited by. Measured across three sessions, tk equals fps in 114 of 115 one-second windows, and the user's own captures read 60/60, 738/738, 15/15, 20/20 and 832/832. The heading is now the general rule, with the three cases below it, and the "machine that cannot keep up" case says what it actually means - the renderer is behind, not the simulation, which is why f/s reads 60.0 beside a low tk. f/s said it "reads 0 during a stage load". It does not: two measured loads read 0.8 and 1.4, because a tick on one side of the hold lands inside the window. "Near 0 (under about 2.0)" is what the log shows, and the diagnostic sentence now uses the same threshold. f/s also implied 60.0 is the healthy reading. The figure is integer fields over a window that is never exactly 500 ms, so it scatters: measured healthy readings across four configurations were 59.3 to 62.9, and a run that matched every other documented value exactly still produced 62.6 once. The entry now says a few percent either side is window jitter, not drift. f/t listed "a stage load" among the cases that read 0.00. A load usually still lands one tick, so it commonly reads above zero there - measured 1.17. Corrected; 0.00 remains right for paused, controls locked and a very long frame. The log comparison section gained the caveat that FPS, tk, f/s and f/t are sampled once per drawn frame, so they repeat their last value while nothing is drawn. During the 8.7 s Dam hold the log keeps emitting lines with live pump columns beside frozen frame columns, and calling all of it "measured over the last half second" was wrong for that stretch. midas_ini_help.txt The tickhz row claimed the value is "rewritten by the game; `hw` when maxfps is -1". It is not. videoPersistDerivedTick returns early when tickhz is hw, and a run with maxfps=-1 and a hand-written tickhz=120 reached hardware pacing in a stage and left the file reading 120 afterwards. The row now says Max FPS decides the rate, gives the derivation for each numeric range, and says a number left behind under maxfps=-1 is stale and ignored. The writer fix is deferred to its own change: this branch does not touch the ini writer, and asserting a fix before it lands is the exact error this row already shipped once. audio_pc.c The legend written into every audio_pump.log carried the same TK, f/s and f/t claims, so it is corrected in the same terms, plus the sampled-per-frame note. tick-and-field-accounting.md, tps-the-lie.md §2.2 records the measured healthy band and that "60.0" throughout the document means "in that band"; §11.1 marks its narrow quoted bands as that session's readings rather than the definition. D5 gets an explicit DEFERRED section - confirmed, not fixed here, with the candidate fix and its counter-argument written down - because the clamp is a pacing decision and this branch does not touch pacing. Verified: builds clean; ctest 9/9; the regenerated legend was read back from a live Data/Logs/audio_pump.log, and line length is unchanged (174-175 bytes), so the manual's "roughly 10 KB a minute" still holds.
…s hold
The hitch line printed `fld` (fields credited to the sim since the previous
present) beside `model held N` (the field hold the hw pacer priced), and the
comment said "fld short of N is simulated time thrown away". Measured, that
comparison reports the opposite of the truth.
A hw-pacing run in Dam on a software renderer, frames ~367 ms:
sim: longest 365.6 ms, pacer 0.0 ms, 1 tick(s) 8 fld model held 4
fld (8) exceeds the hold (4), so by the stated rule the frame was healthy.
367 ms of wall time is 22 fields at 60 Hz and the sim kept 8: fourteen fields
discarded, 63% of wall time, reported as fine. Of 240 such lines, 198 read
`8 fld model held 4` and only 2 read `fld < held`, both by a single field.
The two numbers are not comparable: `model held` is what the pacer asked for,
`fld` is what the credit path allowed, and on a renderer slower than the model
the frame spans far more wall time than either. It read most favourably exactly
when the most time was being dropped.
The clamp site is the only place that knows both halves, so the report now
comes from there. waitForNextFrame calls portTickNoteClamp with the raw elapsed
counts, before it truncates them, and publishes the pair as
g_PortTickClampMeasured / g_PortTickClampCredited behind a sequence number.
The hitch line prints `credit 8 of 53` when the cap fired and nothing when it
did not, so the field's presence is the signal and the shortfall is the
difference. Measured over the whole hw-pacing run: 311 hitch lines, 241 of them
carry it, every one with a shortfall (35 to 227 fields per frame, most commonly
45-50). The same windows previously read "healthy". The 70 lines without it are
all frames under 133 ms, below the point where the cap can fire - so the field
appears exactly on the frames long enough to lose time, and on no others.
The arithmetic is two pure functions in port/src/tick_stats.c, tested beside
the existing rate maths: a 367 ms frame is 22 measured fields credited 8, a
cap of 0 means uncapped, a sub-field frame credits nothing without dividing by
zero, and a ten-second stall does not wrap.
`model held N` becomes `pacer asked N` and is documented as what it is - the
hold the model priced, not a baseline for the credit. It is still read from
g_PortHwFieldsHeld and never recomputed here: this runs at present time, on the
render thread when render_thread=1, and portHwPaceFields mutates the model and
consumes the pending load charge.
The sequence number costs one relaxed load pair in the common case and exists
because a render-thread reader could otherwise take the measured value of one
clamp and the credited value of the next, printing a shortfall that never
happened.
Verified: builds clean; ctest 9/9 with tick_stats grown; the new field observed
live in a hw-pacing Dam run (241 of 311 lines carry it, all with a shortfall;
the 70 that do not are all under the 133 ms cap threshold); and 0 of 110 hitch
lines carry it on healthy 60 fps front-end frames, where the cap never fires.
D5 itself - whether a deliberate hold should be clamped at all - is NOT fixed
here. It is a pacing change and is recorded as DEFERRED in
docs/port/tick-and-field-accounting.md §5.2.
The prior two passes on this branch were read-only and both shipped false
statements while correcting earlier ones - one replaced a false sentence in a
shipped manual with its opposite, another shipped a document whose central claim
was that its subject had not shipped yet. A third pass re-derived every claim
from the code and from execution instead. It found six things, two of them
already fixed in the preceding commits, and this commit records the whole of it
rather than only what is still broken.
NEXT-AGENT2.md
The brief the third pass was handed, kept because it is the reason that pass
found what the first two could not. It reverses the review direction - the
documents are the specification under audit and the code is the evidence -
demands four independent directions (docs to code, code to docs omissions,
message to diff to tree, and execution), and requires a prediction written
down before every run. It also states the shapes of error this branch had
already produced, so the auditor would hunt rather than confirm: a correction
that inverts the error, a number described as displayed that nothing displays,
the wrong counter named, zero conflated with "nothing happened", a document
describing its own uncommitted state, and a caveat that invents a limitation.
Its staffing rule earned itself: subagent findings must be re-verified by the
parent, because in this project a child's report had already arrived truncated
and had already contained an unverified claim about a commit's contents.
docs/port/audio-pump-audit-pass3.md
Findings with both halves each - where the claim is, where the code or the
run contradicts it, what is actually true, and a severity. The two that
mattered: the new `model held N` / `N fld` pair read "healthy" while the sim
discarded 63% of wall time, and the settings manual asserted a `tickhz`
rewrite the game does not perform. Four more are sentences execution
falsified: a load reading f/s 0 when it reads 0.8-1.4, a healthy band of
59.8-60.5 when measurements ran 59.3-62.9, a `tk` rule stated as three special
cases, and log columns that freeze during a load.
§2 lists what could not be verified and why, including three claims that
need a GPU this environment lacks. §4 is the execution record - ten rows,
each with the prediction written down before it ran, and the four that
failed. §6 adds the post-fix numbers, because otherwise they exist only in
commit messages.
It also records a mistake of its own, kept deliberately: the `tk` finding was
first written as "tk cannot exceed the presented frame rate", rated major, and
both were wrong - `pendingGfx` is tested `< 2U` and the manual's "machine that
cannot keep up" does describe what was measured. The correction is left
visible rather than edited away, because inverting an error from reading alone
is the exact failure this branch has now produced three times.
NEXT_AGENT_V3.md
The work still ahead, written as a self-contained task for a fresh session.
Two code items: the `tickhz=hw` writer (the file records a pair the loader
resolves differently) and D5 itself (hw pacing waits for up to 30 fields and
the credit path keeps 8). Then the four smaller open items - the lv.c comment
that is still false, the two cited-but-missing design documents, the three
claims needing hardware, and D4's fix never having been seen under a live
reset.
Each task carries the traps that cost this branch time: the `-2` sentinel and
the `s_CfgTickSeenInIni` guard in the writer, and the fact that the hw model's
6-field cap, the 30-field total cap and the 8-field credit cap are three caps
on one quantity. The verification sections give the command and the number to
beat, taken from §6 of the audit.
§8 is new: a fourth, read-only pass that reviewed the three commits landing
after the audit and signed the branch off. It confirmed F3 against the code
(`videoPersistDerivedTick` returns before writing when the tick is hw) and F2
against the admission gate, checked the new clamp-report concurrency by hand
(sequence start value, torn-pair re-check, ordering against the hitch
threshold, buffer bounds) and found no race, plus one honest bound: the report
is last-clamp-wins per present.
It found one gap the audit did not, and it is the reason §8 exists rather than
a one-line approval: `credit N of M` and `pacer asked N` are documented
nowhere a user will read them. `tools/console_help.txt` has no hitch section
at all - "hitch" appears in it exactly once, in an unrelated sentence. That is
not a false statement, so it is minor, but the field is the branch's only
per-frame evidence of D5 time loss and the manual does not explain it.
§8.3 records how this branch was actually verified, because the method is the
finding: four passes alternating reading and execution, each catching what the
other could not see, and every one of them fixing real defects while
introducing at least one of its own.
The audit is force-added, like the other tracked files under docs/ - without it,
the handoff's first instruction ("read the audit first") would send the next
session to a path that exists on one disk only. That was itself one of the
defects this pass logged against the tree: a reference that resolves to nothing.
This commit was amended before it was pushed, after a fifth read of its own
contents. Five statements in it were wrong and are corrected here, which is the
same pattern §8.3 describes rather than an exception to it:
- NEXT_AGENT_V3.md §0 said the audit report was untracked, while §7 of the
same file said it was force-added and this commit had in fact force-added
it. §0 now states what git ls-files shows.
- §8.1 restated the audit's retracted claim as "structural, not empirical":
one tick and one present per loop iteration. The gate is `pendingGfx < 2U`,
so two ticks per completed task are allowed and no observation here saw
even one exceeded. Corrected to the two-per-task bound, with the audit's
114-of-115 measurement carried over.
- §8.1 cited "FPS 61 tk 61" for a 120 Hz tick with vsync on, attributed to a
pass-2 session whose report is not in the tree. Pass 3's own attempt at that
configuration measured fps 30 tk 30, and the audit lists the vsync case as
unverified. It is now recorded as unverified rather than dropped.
- §8.5 said "nothing in the review found a false statement", which the two
items above falsify. The sign-off now rests on what was actually checked.
- §8.3's "twelve findings, all fixed" for pass 2 has no report in the tree to
count against, and is marked inherited.
Verified: no source behaviour changes in this commit. Builds clean and ctest 9/9
at the parent; `52807f7` and `cb98c39` were each checked out separately and
re-verified here - 0 errors, 9/9 - rather than taking the earlier review's word
for it. Every SHA cited in either document resolves to a commit, every file path
exists, and the line citations were spot-checked against their targets. The
sign-off's own numbers were checked before being written down: `video.c:929` is
the early return it claims, "hitch" occurs once in the shipped manual, and the
hitch buffer's worst case is 56 bytes inside a 64-byte array.
WHAT WAS WRONG. A session running hardware pacing (maxfps=-1, or a file's
tickhz=hw) left whatever numeric tickhz the file already had.
videoPersistDerivedTick returned early on the resolved -1, and the live
maxfps=-1 path returned before calling the writer at all. The file therefore
described a different mode from the one that ran, which is the invariant
docs/port/tick-and-field-accounting.md 11.3 states and the audit's F3.
WHAT IS TRUE NOW. The writer emits the literal `hw` for hw pacing, through
portTickIniValue() (port/src/tick_stats.c) - a pure function, NULL for the
-2 re-derive sentinel, so that state can never reach the file. A file with no
tickhz line still gets none, and a live maxfps change persists at once in both
directions.
The second half matters as much as the first: who records what the FILE says.
The tickhz handler in videoSetDisplayOption used to record it, but
videoCoupleTickToMaxFps calls that handler with "hw" while the file is still
being read, so the synthetic call could claim the file said hw when it said a
number. That is why F3 stayed invisible. The ini parse loop now records the
file's own line, for that key only, before any clamp; the handler records
nothing.
A FIRST CUT WAS WRONG and is recorded in the docs rather than in history: it
used a parse-in-progress flag set around the whole dispatch, which let the
synthetic call record hw again, and it passed only because this machine's
midas.ini lists maxfps above tickhz. With the two lines reversed the file was
still left stale. Caught by the user reading the diff before it was committed;
the second executed case below is the one it failed.
VERIFIED BY EXECUTION (macOS, Apple Software Renderer, GE_FRAMES=90, each case
starting from a backed-up midas.ini, restored afterwards - md5 unchanged):
maxfps=-1 then tickhz=120 -> tickhz=hw (logged "midas.ini 120 -> hw")
tickhz=120 then maxfps=-1 -> tickhz=hw (the order the first cut failed)
tickhz=hw then maxfps=-1 -> no write (file and running state agree)
maxfps=-1, no tickhz line -> line still absent
tickhz=hw + live cl_maxfps 120 -> maxfps=120 tickhz=120
tickhz=hw then maxfps=120, cold launch -> stays hw, nothing written (the
file and the running mode still agree; a hand-written hw line outranks
maxfps across a launch, which is pre-existing and reported, not changed)
VERIFIED BY TEST. tick_stats gains tick_ini_value: -1 -> "hw", 0 -> "0",
60 -> "60", 120 -> "120", -2 and -3 -> NULL, undersized buffer -> NULL.
ctest 9/9; build clean apart from the pre-existing __DATA,__common warning.
NOT VERIFIED. The Windows and Linux ini writers. The GE_NOINISAVE / GE_WINDOW
suppression path is reasoned (the writer now records the value only when
portIniWriteStrs reports a write) but not executed.
DOCS. tools/midas_ini_help.txt's tickhz row said a stale number "is ignored
rather than an override", which this change makes false; it now says the game
rewrites the line to the mode that ran. tick-and-field-accounting.md 11.3
records the mechanism and the six cases; the audit's section 6 records the
resolution and the first cut's failure.
WHAT WAS WRONG. tickhz=hw prices each frame at the fields the console would have needed (portHwPaceFields, 1..30) and boss.c holds the tick for that many. The credit path then clamped the measured elapsed time at portTickMaxStep(), default 8 fields. For any frame priced above 8 the loop waited the full N and kept 8, so N-8 fields of simulated time were discarded per frame - the audit measured 45-50 fields per frame in Dam. The cap cannot tell a deliberate hold from an unintended stall, so it was truncating the hold it had just been given. WHAT IS TRUE NOW. Under hardware pacing a tick is credited max(cap, hold) fields. The policy is one pure function, portStatsStepCap() in port/src/tick_stats.c; portTickCreditCap() in frametiming.c only gathers its inputs. The objection recorded in docs/port/tick-and-field-accounting.md 5.2 was that threading the expectation through boss.c adds a cross-module contract that can itself be wrong. The answer is where the hold is read, not an assertion that it is safe: boss.c is not involved. portHwPaceFields() already published the hold it priced (g_PortHwFieldsHeld, added by 6d02aa1 for the hitch log), so the clamp reads a number the pacer wrote. The flow stays one-way - the pacer publishes, the clamp reads - and no new parameter or call site exists. What keeps the cap honest, each clause a test case: - the raise is only to the hold, and portHwPaceFields caps the hold at 30 fields, so a 90-second window drag cannot become a 90-second step; - only under portTickHwPace(), which is already false in menus, in demos and outside a stage - so a drag there still clamps at 8; - an explicit GE_TICKMAXSTEP is obeyed exactly, because that knob is how this cap is measured; - 0 still means uncapped, never "cap of zero". The elapsed wall clock is still measured and still compared - the hold sets the ceiling only. A patch that trusted the hold blindly would simulate time that never elapsed, and that alternative was rejected for that reason. THE THREE CAPS (design record 10), decided together: the 8-field credit cap is the one that moved. The 6-field render cap is left alone - it bounds the fit, not the tick, and 10's open question about whether that fit saw load hitches stays open. The 30-field total cap is left alone and is now the bound on this raise as well. VERIFIED BY EXECUTION (macOS, Apple Software Renderer, GE_STAGE=dam, GE_HWSCALE=3 to price holds above the cap, cl_audio_log 1, user's midas.ini restored afterwards - md5 unchanged, Data/Saves untouched): 8 credit 8 of 52 pacer asked 6 hold under the cap: still clamps at 8 6 credit 8 of 54 pacer asked 6 1 credit 12 of 57 pacer asked 12 hold over the cap: the hold is admitted One frame in fifteen carried a hold above 8, and that frame's credit moved from 8 to 12 - the change is confined to the frames it is about. The 14 frames with a 6-field hold clamp at 8 as they should: 6 < 8, and the rest of their 52 fields is the software renderer, not a hold. GE_HWSCALE=3 also explains why holds above 8 are rare in practice - scaling the render model 3x saturates it against its own 6-field cap, so a 12-field hold is 6 render fields plus a ~100 ms load charge. The raise lands on load frames, which is the population D5 was about. VERIFIED BY TEST. tick_stats gains stall_cap_hold: hold under/over/at the cap, not under hw pacing, explicit GE_TICKMAXSTEP, uncapped. ctest 9/9; build clean apart from the pre-existing __DATA,__common warning. NOT VERIFIED. Real hardware. Every number is from a software renderer where frames cost ~890 ms, so the renderer sets the frame length; the raise is exercised but a correct hw session at the model's 30-field ceiling is not reproduced here, and the 30-field boundary is covered by the unit test only. Windows and Linux not run. Audit 2's hardware items stay unverified. DOCS. tick-and-field-accounting.md 5.2's DEFERRED section now records the decision, the rejected alternative, the three-caps ruling and these measurements; 10 and the constants list follow it. The audit's 3 bullet is annotated and 6 records the resolution. tools/console_help.txt gains a THE HITCH LINE section - closing audit 8.2, which found that credit N of M and pacer asked N, this branch's only per-frame evidence of the loss, had no shipped text anywhere. It also says which of the two to read before blaming the cap. NOTE ON AUDIO. The same run's audio columns (dry 0 over 0 skip 0 stall 0, one dry event in 23 lines, que ~880 ms against a 927 ms cap) show the pump was not starving at 1 fps; the audible artefact in that configuration is the world running at 9.2 fields/s while music is sequenced at wall rate, not this change. No claim about audio on real hardware is made here.
One page that says what the branch set out to do, what is fixed, what is not,
and which claims rest on measurement. It does not replace the audit or the
design record - it says which to open and why, which is the thing the last two
handoffs had to spell out in prose each time.
The register covers F1-F6, D4-D7 and the audit's 8.2 gap with commit IDs, and
the open items are ordered by what a reader should do next rather than by
severity:
4.1 two cited documents still do not exist (doable now; 11 of the design
record is the evidence that belongs in the first one)
4.2 lv.c's comment still asserts something false (needs a parity decision,
not a comment edit)
4.3 three manual claims need hardware, not care
4.4 D4's fix has never been seen under a live reset
4.5 the hw hold's blank screen is white
4.6 NEW, reported and not fixed: the audio pump sizes its queue to about one
frame, so a frame that overshoots the target drains it (one dry event in
23 lines at 1 fps)
4.7 Windows and Linux, and anything above 60 fps
Two things in it are corrections rather than summary. Item 4.6 is a defect found
while checking an audio complaint and not investigated; it is recorded so it
does not evaporate. And 7 states the failure mode plainly, with all three
instances on this branch, because it is the one the next pass is most likely to
repeat: a fix verified on one shape of input, reported as the mechanism working.
Force-added, as the other three docs are: docs/ is gitignored and a new file
there is otherwise on one disk and nowhere else. Same defect the doc describes
in 4.1.
WHAT WAS WRONG. The pump's queue target was max(ratcheted, interval floor).
The two are in different units of thought. The floor is a frame-RATE quantity -
one pump interval plus a 12 ms device period, ~870 ms of audio at 1 fps. The
ratchet that learns from underruns is an ABSOLUTE byte budget: +1024 bytes per
dry, ceiling TARGET_MAX = 10 KB = 116 ms. So at any frame rate whose interval
exceeds 116 ms - below about 8.6 fps - the floor won unconditionally and every
underrun the pump learned from was discarded on the next pump.
The effect in the field: the queue held exactly one interval plus 12 ms, i.e. a
12 ms tolerance for the next frame being slower. At 1 fps that is 1.4% of the
interval, and ordinary frame-to-frame variance walks through it. The pump had a
safety net for underruns and could not deploy it exactly where underruns happen.
WHAT IS TRUE NOW. portAudioPaceEffectiveTarget() (port/src/audio_pace.c) returns
max(configured target, interval floor) + learned, where `learned` is the ratchet
above the configured target. The headroom lands on top of the floor instead of
competing with it. Ordinary frame rates are unchanged - there the configured
target is the base and the learned term is what moves - and the raise is bounded
by TARGET_MAX - TARGET (76 ms), so the backlog cannot grow without limit.
WHY NO EXISTING TEST SAW IT. Every simulation in port/tests/audio_pace.c pumped
at a CONSTANT interval, and a pump that measures the elapsed interval and refills
for it cannot starve on an even cadence - it sizes for exactly the interval it
just lost. That includes the single-digit-fps test, which asserts dry == 0 at
8, 5, 2 and 1 fps and is correct at those rates only because their intervals are
identical every pump. The new simulate_jitter drains the elapsed interval BEFORE
the refill, so an interval longer than the one before it starves the queue.
VERIFIED WITHOUT A DEVICE. 1 fps, every fifth pump 70 ms longer, 120 s of
simulated time, both policies driven from the same ratchet so only the combining
rule differs:
legacy max(ratcheted, floor): 27 dry events, peak queue 952 ms
now floor + learned: 5 dry events, peak queue 1011 ms
Five and not zero because the ratchet learns 11.6 ms per underrun and the first
spikes are surprises - the mechanism working as designed, not suppressed. The
cost is ~59 ms of extra latency at 1 fps, against a documented preference for a
backlog over a gap at single-digit frame rates (audio-pump-history.md 7,
console_help.txt).
VERIFIED LIVE (Apple Software Renderer, Dam, 1 fps, cl_audio_log 1): after a
`dry` at t=43.5 s the target climbed 1041 -> 1053 -> 1064 ms, one 1024-byte step
per pump. Under the old rule it stayed pinned at interval + 12 indefinitely.
ctest 9/9; build clean apart from the pre-existing __DATA,__common warning. The
user's midas.ini is unchanged (md5 6581028b...) and Data/Saves was not touched.
NOT FIXED. A spike larger than the whole learned ceiling still empties the queue;
one was observed at the transition into Dam. No bounded ratchet covers an
unbounded spike, and covering it would mean holding an unbounded backlog.
NOT VERIFIED. Real hardware. Every number here is a software renderer, and
whether a 76 ms cushion is audibly enough is not something this machine can say.
DOCS. audio-pump-history.md gains 9 (the finding, both measurements, the cost,
and what is left), and its two statements of the old rule are corrected there and
in 8's item 2. audio_pace.h documents the combining rule and why it is a sum. The
same doc's test-count line said 16 cases and was already stale; it is 22 now.
docs/port/audio-pump-state-of-play.md 4.6 moves from "reported, not fixed" to
fixed with the numbers.
Two investigative artifacts from the last session were thrown away or nearly so,
because they were written into build_macos/ - which .gitignore:274 ignores. The
measurement behind NEXT_AGENT_V4.md section 2.1 had to be reconstructed as a
result. Both are now in the tree, in the place their kind belongs.
port/tests/ (asserts) - port/tests/audio_pace.c gains
test_harness_order_decides_whether_a_starve_is_visible. simulate() refills to
the target and THEN lets the device drain, so the queue is always topped up
before it can empty: starvation is unobservable at any frame rate, under any
jitter, with any target rule. The pump does the opposite - it runs after the
interval elapsed. The new case drives both orders with the same policy and the
same jitter (simulate_jitter gains a refill_first switch), and asserts
refill-first dry == 0 while the game's order starves. Keep it after every
harness pumps in the game's order: it is the evidence for why the order matters
and it fails loudly if refill-first returns as an optimisation.
Measured while writing it: 1 fps, every fifth pump 940 ms instead of 870, 120
pumps, legacy max(ratcheted, floor) rule in both arms - refill-then-drain dry=0,
drain-then-refill dry=24.
scripts/port/ (a human reads the output):
audio_run.sh time-bounded live run. Backs up and restores midas.ini,
compares eeprom.bin's md5, refuses to start beside another
copy, stops as soon as -w matches -n times, reports the GL
renderer. Every one of those was a rule rediscovered by
hand each session.
audio_pump_log.py reads a captured audio_pump.log: starves, queue-vs-target,
target trend.
The analyser took three wrong verdicts before it was honest, and its header now
records all three, because the next person to improve it should know they were
already tried: a target rising across a frame-rate collapse is the interval
floor moving, not the ratchet working; `dry` is a per-window counter and the
ratchet steps on the next pump, not the same one; and a 1 Hz sample cannot
distinguish "headroom was added" from "the frame rate settled lower". It now
prints the evidence and states that limit, pointing at the simulation that can
settle it. A script that guesses is worse than no script.
Distinguishing the two logs it was checked against: the pre-fix capture reads
970 -> 920 after its first starve, the post-fix one 1028 -> 1064, and the script
reports the second as consistent with the ratchet adding headroom without
claiming it proves so.
DOCS. NEXT_AGENT_V4.md gains section 7 (where a harness belongs: port/tests/ if
it asserts, scripts/port/ if a human reads it, build_macos/ for nothing), a
pointer to both scripts, and ground rule 5 - any script that produced a number a
commit message cites must be committed. docs/port/audio-pump-state-of-play.md
records the same convention under its verification section.
ctest 9/9; the standalone build (cc -std=c99 -Iport/include port/tests/audio_pace.c
port/src/audio_pace.c) passes. User's midas.ini unchanged (6581028b...) and
Data/Saves untouched across the verification run.
Nothing behavioural. A sweep of the pages the recent commits touched, looking for statements those commits falsified - the failure this branch has repeated five times, so it is worth doing before a handoff rather than after. FALSIFIED BY THE D5 FIX. tools/console_help.txt described `credit a of b` as "a here is the default cap of 8". That was true when written and stopped being true when the cap was made to yield to a hardware-pacing hold: a line can now read `credit 12 of 57`. It now says a is the cap that applied, normally 8. The paragraph below it already explained the yield, so the page contradicted itself two paragraphs apart. COUNTS. The branch is 15 commits, not 14 (NEXT_AGENT_V4.md) or 12 (audio-pump-state-of-play.md). audio_pace.c is 23 test cases, not 22 (audio-pump-history.md twice) or 16 (the same document's comparison table, which had been stale since before this session). All are now derived from the tree: git rev-list --count 3a1ab62..HEAD, and grep -c '^static void test_'. HANDOFF ACCURACY. NEXT_AGENT_V4.md was written before f04d421 landed, so two of its instructions described work that now exists: - section 2.2 step 1 said simulate_jitter "already does it this way", with a line cite of :711. It now has both orders behind a refill_first switch, at :758, and the step says so - what remains is changing simulate() itself. - section 2.4 told the next agent to build the order comparison from the shipped functions "with a flag". That flag is committed and asserted by test_harness_order_decides_whether_a_starve_is_visible; the step now says to re-derive the numbers rather than rebuild the harness. simulate() is untouched, so its line cites (:435, :465, :514) still hold and were verified rather than assumed. DOC MAP. audio-pump-state-of-play.md listed six documents as five and omitted audio-pump-history.md, which is where the pump's defect history lives. Added, with the note that counts and commit totals rot on every commit and belong derived from git rather than copied forward. ctest 9/9; build clean apart from the pre-existing __DATA,__common warning.
No code. Three things, all from auditing the branch's own history before a push. THE COUNT, AGAIN. af78f4a was committed under the subject "the counts and the cap claim the last commit left stale" and it wrote "15 commits, tip f04d421" - into the commit that added it. The branch was 16 and the tip was af78f4a. Both documents now carry the command next to the number (git rev-list --count 3a1ab62..HEAD) and NEXT_AGENT_V4.md section 0 says to re-derive rather than trust the line. This is the fifth time on this branch that a number was copied forward instead of derived, and the second time in one day by this session; the new section 8 also records that its own first draft got a related count wrong (65 citations, measured before the section's prose added more). FOLDING: NO. NEXT_AGENT_V4.md gains section 8, answering the question asked directly. The evidence, measured rather than argued: - all 16 commits were checked out in a separate worktree and built and tested individually: 16 of 16 build with zero errors and pass ctest 9/9. No broken bisect step, so no mechanical reason to fold. - the tracked docs cite commit SHAs 66 times at af78f4a, plus three in code. Rewriting any commit changes its SHA and every descendant's, so folding one commit turns dozens of references into dangling ones - the exact defect the handoff's Task B exists to fix. - four commits contain content a later commit on the same branch corrects (bbc5c99's false tickhz claim, 6d02aa1's fld-vs-model-held comparison, 17bf80b's "default cap of 8", and af78f4a's count). That is the honest case for folding; it loses because nothing is broken, because the project's own doctrine is that the record of a wrong conclusion is worth more than a tidy history, and because what must be true is the tip, not every intermediate. The window is noted rather than used: origin/audio-pump-wallclock_gen4 is still at 3a1ab62, so all 16 exist only locally and a rebase would work today. WHAT TO DO INSTEAD. docs/port/audio-pump-state-of-play.md now opens with a five-phase commit map (the numbers / documenting them / the audit and its corrections / the two deferred items / what that exposed), so a reviewer reads four units instead of sixteen. That is the reviewability folding would have bought, at no cost to the 66 citations. Verified: ctest 9/9 (no code changed). The scratch worktree and its build logs are removed; the user's midas.ini is unchanged.
Provoked by a fair question: "are we not doing any squashing, or will it just
check whether folding a couple would be nicer?" Section 8 was written as a
debate - it set out "the honest case for folding" at length and then refuted it,
under the heading "Should this history be folded? No - and the window is now",
sitting among numbered sections whose every neighbour is a task. Read cold, that
is an open question with arguments on both sides, in a brief whose job is to say
what to do next.
The decision does not change: no squashing, no reordering, no rewriting past
commit messages. What changed is the presentation.
- NEXT_AGENT_V4.md section 8 is now a decision record: "There is nothing to do
here - do not spend time on it, and do not re-litigate it", two facts a
reader would otherwise hunt for, a pointer to the full reasoning, and one
explicit reopen condition. The reopen condition is narrow and checkable -
the branch gets squashed at merge anyway, or a commit is found that does not
build - which is also the answer to "will it just check if folding would be
nicer": no, and "it would look nicer" is named as not a reason.
- The full argument moved to docs/port/audio-pump-state-of-play.md under "Why
the history is not folded", where the branch's history already lives, with
the measured evidence and the honest case for folding intact. That includes
the three commits that contain content a later commit corrects.
Also fixed while there, since the same question exposed them:
- The commit map I added in 4e731d9 was inserted BETWEEN the sentence
introducing the two long documents and the bullets listing them, orphaning
the bullets. Structure restored.
- The count is no longer written down in either document. This branch has now
miscounted itself five times, twice in one day, and every fix of the number
became the next stale number. Both pages say to run
git rev-list --count 3a1ab62..HEAD instead, and the state-of-play page says
why it is deliberately absent.
- Section 0 claimed "all 16 commits exist only here" and "the window closes the
moment it is pushed" - the latter is an invitation dressed as a warning, and
it is now gone. The commit list is marked as stopping at af78f4a so it does
not silently go stale either.
ctest 9/9 (documentation only).
The closed-loop simulation every audio case is built on refilled the queue to the target and *then* let the device drain it. portAudioPump() does the opposite: it is called after the interval has elapsed, the device has already played, and the depth it measures is the post-drain one. In the harness the queue was topped up before it could be emptied, so starvation was unobservable at any frame rate under any jitter - which is why the pump's ratchet defect (df881c9) survived every passing case and was found by ear. Measured with this harness, one scenario (1 fps, every fifth pump 940 ms instead of 870, 120 s = 137 pumps, the pre-df881c9 rule in both arms): refill-then-drain (the old harness): dry=0 drain-then-refill (the pump): dry=27 <- one per spike The shipped rule on the same scenario: dry=5 (the first spikes teach the ratchet, then its 76 ms ceiling covers the 70 ms overrun). Both numbers are printed by the test, so a later session can reproduce them without this message. What changed: - simulate() drains first, measures, then refills (now simulateScenario()). - The second harness is gone. simulate_jitter() had the right order and the constant-interval simulate() had the wrong one, so the file had two physics for one pump. Cadence, per-pump jitter, spikes, drift, the policy rule and the order are fields of one SimScenario; `refill_first` survives only as the order-sensitivity evidence. - New case: 60 fps with +/-10% jitter must stay dry==0, and the generator's bound is pinned so "bounded" is not an assumption about a hash. The low-frame-rate spike case is the old jitter case, now driving the shared core. - min_bytes/max_bytes now mean what the pump SAW and what it LEFT. Four assertions moved with that, none of them relaxed: * 20 fps: `min_bytes > TARGET/2` held only because the harness refilled first. In the pump's order a healthy run opens with the interval floor's cushion (~one device period), so that is what is asserted. * the dry-start case excludes the priming pump: a freshly opened device is empty and the ratchet's own contract says that is not an underrun. * rendered-vs-consumed was bounded by a frame of slack, which hid a real effect at low frame rates - a run that ends with a deeper queue has rendered more than it played, and that is not a rate error. Replaced by the balance equation (simUnaccounted): nothing dropped, nothing duplicated. Constant-interval cases still hold dry==0 at 60, 300, 20, 8, 5, 2 and 1 fps - verified, not assumed. Checked that the harness has teeth by mutating the shipped rule back to max(ratcheted, floor) in port/src/audio_pace.c: the pure-function case fails, and with that case disabled test_jitter_at_low_frame_rate still fails (fixed.dry == legacy.dry). Source reverted; only the two test files are here. Inventory of the other timing simulations ("this is the only one" has been wrong on this branch before): - port/tests/audio_pace.c - one model now. The manual loops in test_simulation_survives_a_stall_between_pumps, the test_clock_* cases and the drain-watchdog cases are single-concern loops that do not model a cadence. - port/tests/tick_stats.c - no loop, so "interval" there means the stat WINDOW. Audit F5's band rested on four measured readings; healthy_window_band() sweeps every window length the stat pass can close on (500-1000 ms) and pins the arithmetic: nearest-whole-field counts give 59.1-61.0, and one field of boundary skew gives 58.1-62.0 - so a reading inside the manual's 59.3-62.9 band needs no defect to explain it. - scripts/port/audio_pump_log.py and audio_run.sh - readers, not models; their headers already state what a 1 Hz sample cannot separate. - src/game/frametiming.c - no harness; decided explicitly below. frametiming.c decision: no harness in this pass, recorded rather than left as an unmentioned gap. What is tested is the policy it calls (portStatsStepCap and the field arithmetic in tick_stats.c); what a harness would need is an osGetCount stub, definitions for 42 externs from other modules, and - the reason it is real work - one process per configuration, because the file caches configuration in a dozen process-global statics fed by 30 getenv reads. A live run proves the composition (D5's `credit 8 of 52` -> `credit 12 of 57`) and cannot prove any single branch of it. The shape for a later pass is written down in docs/port/audio-pump-state-of-play.md 4.8. Verified: cmake --build build_macos (only the pre-existing __DATA,__common alignment warning) and ctest 9/9; standalone cc -std=c99 -Wall -Wextra clean.
…bols Three cited things did not exist and four citations were stale. Both are fixed. - docs/port/n64-hw-pacing.md is written, assembled from frametiming.c and the design record rather than researched: the nine render-model coefficients (two of them fitted to zero, and fillx is a surcharge not a fourth bucket), the 6-field render cap and the 30-field total, the cart-DMA and inflate rates, the stage-load CPU terms, the fit's provenance (hwfit.py is not in the tree), the near-points coefficient that is 67.7% of setup CPU and unproven, and what the hold does and does not mean. - docs/port/tick-rate-binding.md is NOT written. The binding is tick-and-field-accounting.md 13 and video.c cites that section. A second document would have restated sections 2 and 3 and then drifted from them, which is the failure the documentation map exists to prevent. - The design record's 11 stays where it is, by decision recorded in both documents: three of its four subsections are about the tick and the ini rather than the RDP model, and moving them would renumber references that are already cited. Citation policy, adopted and applied: cite the symbol or the section, and use a line number only where the line is the point. video.c and frametiming.c move constantly; four citations into video.c were stale within two commits and one of them described a branch F3-A had deleted. So: - audit: videoPersistDerivedTick's early return is recorded as gone rather than renumbered, and points at design-record 11.3 and state-of-play 2; the missing documents are marked resolved, in 2 and in 6's "what is still open"; - design record: the tickhz parse is cited by key instead of by line, 10's "neither file exists" is corrected, and 11's "belongs in n64-hw-pacing.md when that file is written" now records the decision instead; - state-of-play: 4.1 states the outcome instead of the gap, 5 carries the policy, 4.8 records the frametiming.c harness decision from the companion commit, and the documentation map lists the new document; - the three code comments now name their document's section. video.c's edit is line-neutral, so it does not shift the line citations the keybind documents still carry - those were already stale by about 30 lines from 87af349 and are left alone as their own tracking documents. Reported, not quietly renumbered. Found by reading, and not repeated in the new document: portTickHwPace()'s comment says "the model's fixed cost alone is two fields". The constant term is 13.17 ms, under one field, so the sentence is only true of the fixed cost plus an ordinary menu draw. The document states the verifiable half and says why it does not carry the other. Verified: git ls-files docs/port/ lists n64-hw-pacing.md (it needed git add -f; the directory is gitignored); build clean, only the pre-existing __DATA,__common alignment warning; ctest 9/9.
The branch is `tick-field-accounting` now. It was `audio-pump-wallclock_gen4`, which named the subject it started from; about 60% of the change is the frame and tick accounting in frametiming.c, tick_stats, video.c, hitchlog and bondview2, and the design record it is all built around is docs/port/tick-and-field-accounting.md. The pump work rides along because its ratchet defect was found by the harness this branch fixed, and it cannot be split out: four citations into code (port/tests/audio_pace.c, two; scripts/port/audio_pump_log.py, two) and dozens in the docs name commit df881c9 by SHA, and a cherry-pick would change it. A rename, not a rebase: same commits, same SHAs, so every citation into this branch's history still resolves. origin/audio-pump-wallclock_gen4 is untouched and still at 3a1ab62, so anything open against it is unaffected. Updated: the entry-point page and the audit header, which are live references. The four NEXT_AGENT*.md handoffs still say the old name and are left as written - they are dated snapshots of what the branch was called when they were written, and rewriting them would make the record agree with the present instead of with the day. The state-of-play header says so, so a reader arriving from a handoff is not left guessing. Verified: build clean (only the pre-existing __DATA,__common alignment warning), ctest 9/9. Documentation only, plus one comment-free rename in prose.
…udit found A pre-push reference audit over this branch found one hard failure, a unit error in the document this branch added, and a set of claims that a later commit made false. None of it changes code; all of it would have been read as authoritative. HARD FAILURE - two citations resolved to nothing. tps-the-lie.md cited `ecfbe07` and `853163b` as the commits that removed TPS from the overlay and the log. Neither is reachable from this branch: they are pre-rewrite twins of 85fa9f1 and 2f8a6d0 (same subjects, same file sets, different trees), left behind when the branch's early history was amended. A reviewer following either got "unknown revision" - the exact defect the branch's own Task B was about. Corrected to the reachable commits, with a note saying they read wrong for a while, because a silently fixed citation is the same failure as a silently wrong one. A UNIT ERROR IN MY OWN DOCUMENT. n64-hw-pacing.md's coefficient table labelled tris and chrs as microseconds per unit. They are milliseconds per unit: the fill and byte terms carry a * 0.001 on the count, the last two do not, so sHwModel[7] = 0.004372 ms/tri is 4.372 us and sHwModel[8] = 0.152045 ms/char is 152 us. Read as microseconds the triangle term looks 4.4 ns per triangle and vanishes, which is roughly what a reader would have concluded about the model's triangle cost. Both rows corrected, and the two conventions are now called out explicitly, since one formula carrying both is the trap that produced the error. Also softened: "each load site notes its compressed size". portHwChargeLoad takes a compressed flag and passes 0 for uncompressed reads (the bg header, the point table), so the sentence now says what it notes and which reads are DMA-only. CLAIMS A LATER COMMIT MADE FALSE. - "audit 8.2" is cited 4 times and does not exist: the audit has sections 1-6 and an appendix. The gap it means is NEXT_AGENT_V3.md 8.2, which is now what those four sites say. - state-of-play said commits after af78f4a "carry no code". 13fedc5 is the largest test change on the branch. The phase map has a sixth phase and says so. - the audit's F1 paragraph cited frametiming.c:1178-1183 and :962-970, which matched the tree it audited (2f8a6d0) and stopped matching when cb98c39 added 41 lines above them. Replaced with symbols, per the policy adopted in the previous commit, and marked as having been wrong on arrival. - the audit's verdict still read "Not safe to push as-is - two majors block it". Both blockers were fixed (F1 by cb98c39, F3 by 87af349) and section 6 records it. The verdict is kept verbatim with a dated note above it: a verdict that gets rewritten once it comes true is not a verdict. - final_report_PR.md said the branch had been pushed. It had not - the push was cancelled. It also carried 20 commits (now 21) and 38-against-main (now 39), and claimed the two deferred commits were the only behaviour changes when its own section 3 lists five. All four corrected. Verified: every 7-40 char hex string in docs/port/*.md, port/tests/*.c and scripts/port/*.py now either resolves to a commit that is an ancestor of HEAD or is not a SHA at all (an md5 prefix, 0x10000000, an image-size constant). Section references checked: design-record 13 and state-of-play 4.8 exist, no "audit 8" remains. Build clean (only the pre-existing __DATA,__common alignment warning), ctest 9/9.
CLI.md at the repo root: how to launch, how to get hardware pacing, the two
command-line flags, the startup variables worth knowing, and the console. It
answers a question the tree made hard to answer, because the answer is
counter-intuitive:
**There is no environment variable for hw mode.** GE_TICKHZ is parsed with
strtol, and strtol("hw") is 0 - so GE_TICKHZ=hw silently selects an UNCAPPED tick
rate, a different mode with different behaviour, and the only hint is the log line
that says UNCAPPED. hw pacing is reachable three ways and none of them is that:
midas.ini [display] maxfps=-1 (the reliable one)
console cl_maxfps -1 (live, persisted to the ini)
env GE_CONCMD="cl_maxfps -1" (runs at the first rendered frame)
maxfps=-1 IS tickzh=hw: videoCoupleTickToMaxFps calls the tickhz setter with "hw"
through the same path the ini uses. The doc says so, with the call chain, so the
next reader does not have to rediscover it.
Contents: quick start; the three hw routes plus the trap and what the mode costs
(the 8.7 s Dam hold, audio latency at low frame rates, active only inside a
stage); the two flags (--rom-file, --lang) and drag-and-drop, with the note that
there is no --help and unknown flags are ignored; the six-step ROM search order;
the startup variables grouped by purpose rather than listed alphabetically; the
console's cl_ verbs; and what a run writes, including that changing maxfps
rewrites midas.ini - so GE_NOINISAVE=1 or a backup, which is the same rule the
audio harnesses follow.
Every claim is read from source; provenance is a section at the end. Nothing was
launched while writing it, because a launch writes midas.ini, so the one claim
that needs a run - that GE_CONCMD="cl_maxfps -1" engages hw pacing from the first
rendered frame - is marked derived-not-observed.
Second change, on the same subject. The console's `harness` command prints a
generated table of every GE_* knob, and that table was 20 entries behind the tree:
290 of 310. Two of the missing ones were GE_HWLOAD and GE_HWLOAD_SCALE, so the
in-game listing could not show a reader the knobs that control hw-pacing load
charges - the exact question a hw-mode investigation asks. Regenerated.
The cause was the maintenance method: the header said "paste the grep output
between the braces", which is a step a person skips. There is now
scripts/port/regen_console_env.py, which rewrites the array in place, sorts
LC_ALL=C so the diff shows only additions (a locale sort reorders GUNSND_OVERLAP
and GUNSNDRATE and manufactures churn), and is idempotent - verified by running it
twice and getting a byte-identical file. The header points at it and records why.
Verified: regenerated table compiles; build clean apart from the pre-existing
__DATA,__common alignment warning; ctest 9/9; script idempotent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wow, this went much deeper than expected. What started as an overlay fix turned into a full-on deep dive — the short version is below, the long version (what was executed vs. only read, and what's knowingly still open) is in the report that follows. Provided as-is.
On the +6K diff: that's mostly tests and docs, not shipped code. Three test harnesses — and honestly a fair chunk of this branch is testing the tests. One of the harnesses was previously modelling the pump in the wrong order and could not have caught the bug it was meant to catch; the suite was mutation-checked (reintroducing the old ratchet rule makes it fail) because "a green suite that can't fail" is this branch's recurring failure mode.
Summary: replaces the lying TPS readout with trustworthy fps/tk/f/s/f/t numbers, fixes four defects that surfaced behind it (D4–D7), fixes a midas.ini mode-record bug (F3-A), and fixes an audio pump ratchet that discarded its own learning below ~8.6 fps.
PR report —
tick-field-accountingA report for whoever reviews this branch, not the PR description itself. It says
what the branch changes, what was executed, what is only read, and what is
knowingly still open.
Base:
audio-pump-wallclock_gen4at3a1ab62, notmain. The branch isstacked on the previous audio-pump work, which is 18 commits ahead of
main(4bab4e7). Diffed againstmainthis PR would be 40 commits and 234files, dragging in the macOS 64-bit port, the 32-bit ABI notes, the GCC 15 cmake
fix and 1.0.2b. Against its actual base it is the 33 files described below.
State at
ef8a6c6, the tip when this report was last touched. Every numberhere is dated on purpose: this branch has miscounted itself six times, and a
count written without a way to re-derive it rots silently. Run these before
quoting any of them.
Not pushed. The branch
tick-field-accountingexists only in the submitter'scheckout.
origin/audio-pump-wallclock_gen4is untouched and still at3a1ab62,so any PR open against it is unaffected. Push it with
git push -u origin tick-field-accounting; nothing needs rewriting first, and nocommit was rewritten by the rename — every SHA the branch cites that resolves is
still on it. (Two SHAs cited inside
tps-the-lie.mddid not resolve, from anearlier rewrite; that is fixed in the same commit as this report's correction.)
1. What the branch is for
One defect, followed down. The overlay showed
TPS, andTPSwasmin(tick rate, 60)— it tracked the frame rate below 60 and read a flat 60above it, so it described neither the tick rate nor game speed. Replacing it
with numbers that could be trusted (
fps,tk,f/s,f/t) exposed three realdefects behind it, and the audit of that work found more:
TPSfor one stat window after every stage load.tickhz=hwwaited up to 30 fields for a frame and credited 8, silentlydiscarding up to 367 ms of simulated time per frame. Measured at 45–50 fields
per frame in Dam.
f/sread a healthy 60.0 during every pause; the D4 guard coulditself publish a negative under a render-thread race.
tickhzinmidas.ini, so the file described a mode that had not run.queue target was
max(ratcheted, one interval), so below ~8.6 fps the intervalfloor won unconditionally and every underrun the ratchet learned from was
discarded on the next pump.
Two pieces of test work are the reason the branch is worth reading:
D5policy table are pure functionswith their own
ctesttargets, so the numbers a player sees and the rule thatdecides how much simulated time a frame keeps are both checked without a
window, a GPU or a ROM;
pump does not use — so starvation was unobservable by construction — now
models it correctly, and that is what let the ratchet defect be reproduced.
2. The commits, in phases
Read them as units. Every commit through
af78f4awas checked out in a separateworktree, built and run against
ctestindividually (recorded inaudio-pump-state-of-play.md; this session did not re-check each commit); thefour after it are documentation amendments to the pages below.
ca41f25,97659f6,85fa9f1. The frame and tick mathsas pure functions (
port/src/tick_stats.c),fields/spublished, the blockshowing
tk/f/s/f/tinstead ofTPS.9873f43,bbc5c99,2f8a6d0,6d02aa1. The design record, the manual claims,TPSdropped from the log,the hw field hold added to the hitch line.
52807f7,cb98c39,dbed958. Athird-pass execution audit falsified four of phase B's claims; these correct
them and replace a hitch-line comparison that read "healthy" at the moment the
most time was being dropped (F1) with a measurement at the clamp site.
87af349(F3-A, thetickhzwriter) and
17bf80b(D5, the stall cap yielding to a hardware-pacing hold).ef16465,df881c9,f04d421,af78f4a. Thestate-of-play page, the pump ratchet fix, the harnesses made durable, a
documentation sweep, and the decision not to fold the history.
13fedc5,7b99f9c,e06105f, and the citation-auditfix. The audio
simulation put in the pump's order with jitter as a scenario field, the two
missing documents resolved, the branch renamed, and a pre-push audit's findings
applied (a dead citation, a unit error in the new document, stale counts).
A five-phase reading map with the same content lives in
docs/port/audio-pump-state-of-play.md.3. What changes for a player
Five behaviour changes, all deliberate, four of them only under options a player
has to turn on:
fps,tk,f/s,f/tinstead ofFPSandTPS(
port/src/bondview2.c).TPSis gone fromaudio_pump.logtoo.f/sreads 0 while paused, instead of a false healthy 60.0 (D6).TPSafter a stage load (D4/D7) — moot now thatTPSis notdisplayed, but the guards are in the stat pass and the reset path.
midas.inirecords the tick mode. A session running hardware pacing nowwrites
tickhz=hwrather than leaving whatever number was there (F3-A). Oneconsequence worth knowing: a hand-written
tickhz=hwoutranksmaxfpsacross a launch, which is pre-existing and recorded as such.
(D5), and below ~8.6 fps the underrun ratchet actually reaches the queue
(the pump fix).
Everything else on the branch is diagnostics, documentation, or tests.
For completeness:
13fedc5and7b99f9cchange no shipped behaviour — the firstis test code only, the second is documentation plus three comment-only source
edits (verified line by line in the diff).
4. Verification — what was executed
Provenance is stated for every number, because this branch has twice shipped a
claim that a later pass falsified.
Executed in this session (2026-10-25, macOS, Apple Software Renderer):
cmake --build build_macos -j8: clean except the pre-existing__DATA,__commonalignment warning.
ctest: 9/9 (native_layout,native_collision,native_aim,native_stan,native_movement,native_prop_collision,native_player,audio_pace,tick_stats).scenario — 1 fps, every fifth pump 940 ms instead of 870, 120 s (137 pumps) —
with the pre-
df881c9rule in both arms:The same scenario under the shipped rule:
dry=5. The test prints all threecounts, so this is reproducible from the binary.
max(ratcheted, floor)inport/src/audio_pace.cmakes the pure-function case fail; with that caseskipped, the simulation case still fails. A green suite that cannot fail is
the failure mode this branch keeps hitting, so it was checked.
tick_stats: a sweep of every stat-window length the pass can close on(500–1000 ms) pins the healthy
f/sband as arithmetic — 59.1–61.0 fornearest-whole-field counts, 58.1–62.0 with one field of boundary skew. The
manual's measured 59.3–62.9 band therefore needs no defect to explain it.
Executed by earlier sessions on this branch, recorded in the docs, not re-run
here: F3-A's six
midas.inicases (each from a backup, restored,eeprom.binmd5 checked); D5's
credit 8 of 52→credit 12 of 57underGE_HWSCALE=3;the pump's live ratchet climb
target 1041 → 1053 → 1064 msafter adry; theDam stage-load price of 8.7 s.
Read and computed, not executed: the tick/field trace (
tick-and-field-accounting.md§1), the mode table, the model's coefficients, and the
frametiming.cdecisionthat no harness is built for that file yet.
Not verified anywhere: Windows and Linux. Every measurement on this branch is
macOS, most of it from a software-rendered context where frames cost ~466–890 ms.
5. What is knowingly not done
Stated so a reviewer does not have to discover them.
src/game/lv.c's comment ong_GlobalTickCountis still false below 60 fps.It says the counter "must count 60Hz fields … always true at GE_TICKHZ=60"; at
15 fps it counts ticks, each worth four fields (
f/t 4.00measured). It is thebranch's one deliberately-live mislabel, because the fix is a parity decision —
chraction.c,bondview2.c,propobj.candgunfire.call key off thecounter as if one tick were one field — not a comment edit.
paused
f/s/tkpair (consistent withlv.c, never reproduced — the consolehas no pause verb); sub-ticking,
f/tbelow 1.00 (needs a renderer faster than60 fps; this machine peaks near 38, lowest measured
f/tis 1.03); vsyncpinning
tkat 60 on a real 60 Hz panel. One machine with a working GL driversettles all three.
consecutive live log lines showed no negative, but no stage load has been
captured inside a stat window with a non-zero pre-load count.
Deferred twice; recorded in
n64-hw-pacing.md§5.76 ms and a stage load is not. Recorded in
audio-pump-history.md§9.src/game/frametiming.chas no test target. The policy it calls is tested;the glue that gathers the inputs is not. The decision, the reason, and the
shape a later pass should take are in
audio-pump-state-of-play.md§4.8.6. Where a reviewer should start
docs/port/audio-pump-state-of-play.md— one page: what the branch set outto do, the findings register with a status per defect, and the remaining work.
87af349(F3-A) and17bf80b(D5).They are the two that were held back deliberately rather than the only two
that change behaviour; §3 lists all five, and
df881c9's pump fix is the thirdcode change a player could feel. Both are small and both carry their own
objection-and-answer in the message.
port/tests/audio_pace.c— readsimulateScenario's header andtest_harness_order_decides_whether_a_starve_is_visible. If you read one thingon this branch, read why the harness that was supposed to catch the bug could
not have.
port/src/tick_stats.c— 92 lines, pure, and each rule has a case.The long references are
docs/port/audio-pump-audit-pass3.md(evidence) anddocs/port/tick-and-field-accounting.md(mechanism).docs/port/n64-hw-pacing.mdis new on this branch and covers the cost model behind
tickhz=hw.7. Risk
cap when
portTickHwPace()is true, and F3-A only changes text written tomidas.ini. The overlay and the log are the only surfaces most players see.documented failure mode is a fix verified on one shape of input and reported as
the mechanism working; it happened five times, twice while fixing an earlier
instance. Section 5 is the list of places where that is still possible.
audio-pump-state-of-play.md,"Why the history is not folded"): every commit builds and passes on its own, and
folding would break 66+ SHA citations that are the branch's evidence system.
Four commits contain content a later commit corrects; a checkout of those trees
gives a lying manual, which is the cost that decision accepts.
NEXT_AGENT*.mdhandoff files (~1,400 lines) aretracked and in this diff. They are process artifacts, not product; say if they
should come out of the PR before it is opened.
8. How to reproduce the verification