OXYGENHUB

main: while the CPU waits in WAI, step the machine to its next event at once 8e8d9cad1884

At the BASIC prompt, and in any program that waits for VSYNC with WAI,
the 65C02 spends most of its time waiting. step6502() then advances one
clock and returns, so the main loop went round once per clock, about
8 million times a second, stepping VIA1, VERA's beam, I2C, the RTC and
audio and checking for interrupts each time. That was most of narvi's
run time at the prompt.

wai_fast_forward() steps them by many clocks in one go instead, only as
far as the next clock on which anything could raise an interrupt, so
the machine does exactly what it did clock by clock:

- VERA: to the clock on which the beam passes the end of a VGA or NTSC
  line (video_clocks_to_line_end()). That is exact while the beam moves
  a whole number of eighths a clock (mhz divides 200, so at the X16's
  8 MHz): every position is exact in a float, so one big step and many
  small ones agree.
- Audio: to the clock on which audio_step() next renders, where the CPU
  could see a change (the PCM FIFO, VERA's AFLOW interrupt, the YM2151)
  (audio_clocks_to_render()); the sample counters are integers.
- VIA1: a clock at a time, stopping on the clock its interrupt rises.
  via_step() is not stepped by the stretch: a step that ends on Timer
  1's -1 state takes 1 clock where it should take cnt + 1, so it does
  not add up the way single clocks do (an existing inaccuracy, left as
  it is).
- The RTC counts clocks; the SD card's SPI is left clock-by-clock while a
  byte is in flight (its counter is a float).

Everything not shown to add up that way keeps the clock-by-clock loop:
VIA2, the serial bus, the MIDI card, YM2151 interrupts, the debugger,
BASIC pasting, and the KERNAL hooks at $FEA8 and up.

I2C is now stepped once per pass rather than once per clock: it acts only
when the clock or data line the VIA drives changes, which happens between
instructions, so the second and later calls did nothing. (On its own this
measured within narvi's noise; with the fast-forward it would be called
hundreds of times per pass.)

Measured with the emulator stepping 600 frames, real-time mode, SDL dummy
drivers, CPU seconds per emulated second, median of 3:

    narvi  idle  2.189 -> 0.698  (46% -> 100% of a real X16, 143% flat out)
    narvi  busy  1.797 -> 1.647  (56% -> 61%)
    x86    idle  0.412 -> 0.216
    x86    busy  0.430 -> 0.454  (within this machine's noise)
    x86    idle in -warp: 280% -> 1155% of a real X16

Emulation is unchanged. From power-on with -zeroram -nohostfs, five BASIC
workloads, each with sound on and off, give the same registers, RAM (low
and all 64 banks) and screen at frame 900 as before, on x86 and on
riscv64. Two of them are for this change: SLEEP in a loop (the KERNAL
waits in WAI for VSYNC), and a machine-code loop that takes VIA1 Timer 1
interrupts every 16K clocks and WAIs between them, logging the scanline
and the timer at each wake-up.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 7f1108192b02
When
· 2026-10-21 02:31 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
8e8d9cad1884b197145a28d2a55bd2177ebeeeeec53e08cbe03e6388bcb80519
Depends on
03a3459e6372 12673a41ba17
Depended on by
nothing yet
In bookmarks
main riscv-1
Edits
13 atoms, 0 tree operations, 7 files

Browse the tree at this change

modified src/audio.c +14 −0
…
248 unchanged lines
249 249
	}
250 250
}
251 251
252 +
// How many CPU clocks audio_step() can take in one call and still do
253 +
// exactly what that many one-clock calls would: up to and including the
254 +
// clock on which it next has to render, which is when the CPU can see a
255 +
// change (the PCM FIFO, VERA's AFLOW interrupt, the YM2151's state).
256 +
uint32_t
257 +
audio_clocks_to_render(void)
258 +
{
259 +
	if (audio_dev == 0) {
260 +
		return UINT32_MAX;
261 +
	}
262 +
	uint32_t ym_room = (ym_samp_pos_rd - ym_samp_pos_hd - (1 << SAMP_POS_FRAC_BITS)) & SAMP_POS_MASK_FRAC;
263 +
	return (uint32_t)(ym_room / ym_samp_clks_per_cpu_clk) + 1;
264 +
}
265 +
252 266
void
253 267
audio_render()
254 268
{
…
197 unchanged lines
modified src/audio.h +1 −0
…
10 unchanged lines
11 11
void audio_init(const char *dev_name, int num_audio_buffers);
12 12
void audio_close(void);
13 13
void audio_step(int cpu_clocks);
14 +
uint32_t audio_clocks_to_render(void);
14 15
void audio_render();
15 16
16 17
void audio_usage(void);
modified src/main.c +47 −3
…
1626 unchanged lines
1627 1627
	return handled;
1628 1628
}
1629 1629
1630 +
// While the CPU waits in WAI, nothing it can see changes until an
1631 +
// interrupt, yet the loop below would go round once a clock, stepping every
1632 +
// device each time: at the BASIC prompt, most of the run time. This steps
1633 +
// them by many clocks at once instead, but only as far as the next clock on
1634 +
// which something could raise an interrupt, so the machine does exactly
1635 +
// what it did a clock at a time: VERA to the end of the line, audio to its
1636 +
// next render, and VIA1 a clock at a time here, stopping on the clock its
1637 +
// interrupt rises (via_step() does not take a stretch that ends on its
1638 +
// timer's -1 state the way it takes single clocks). Everything not shown to
1639 +
// add up that way keeps the clock-by-clock loop: VIA2, the serial bus, the
1640 +
// MIDI card, an SPI byte in flight, YM2151 interrupts, the debugger, BASIC
1641 +
// pasting, the KERNAL hooks at $FEA8 and up.
1642 +
// Returns the clocks taken, with VIA1 stepped through them, or 0.
1643 +
static uint32_t
1644 +
wai_fast_forward(void)
1645 +
{
1646 +
	if (!waiting || headless || debugger_enabled || has_serial || has_via2 || has_midi_card ||
1647 +
	    ym2151_irq_support || pasting_bas || regs.pc >= 0xFEA8 || vera_spi_busy()) {
1648 +
		return 0;
1649 +
	}
1650 +
	uint32_t limit = video_clocks_to_line_end(MHZ);
1651 +
	uint32_t audio = audio_clocks_to_render();
1652 +
	if (audio < limit) {
1653 +
		limit = audio;
1654 +
	}
1655 +
	if (limit < 2) {
1656 +
		return 0;
1657 +
	}
1658 +
	uint32_t n = 0;
1659 +
	do {
1660 +
		via1_step(1);
1661 +
		n++;
1662 +
	} while (n < limit && !via1_irq());
1663 +
	clockticks6502 += n;
1664 +
	return n;
1665 +
}
1666 +
1630 1667
void
1631 1668
emscripten_main_loop(void) {
1632 1669
	emulator_loop(NULL);
…
143 unchanged lines
1776 1813
		if (coverage_enabled && !waiting) {
1777 1814
			coverage_mark(regs.pc);
1778 1815
		}
1779 -
		step6502();
1816 +
		uint32_t skipped = wai_fast_forward();
1817 +
		if (!skipped) {
1818 +
			step6502();
1819 +
		}
1780 1820
		uint32_t clocks = clockticks6502 - old_clockticks6502;
1781 1821
		old_clockticks6502 = clockticks6502;
1782 1822
		bool new_frame = false;
1783 -
		via1_step(clocks);
1823 +
		if (!skipped) {
1824 +
			via1_step(clocks);
1825 +
		}
1784 1826
		vera_spi_step(MHZ, clocks);
1785 1827
		if (has_serial) {
1786 1828
			serial_step(clocks);
…
12 unchanged lines
1799 1841
			}
1800 1842
		}
1801 1843
1802 -
		for (uint32_t i = 0; i < clocks; i++) {
1844 +
		// The I2C bus acts only on a change of the clock and data lines the
1845 +
		// VIA drives, which happen between instructions, so once is enough.
1846 +
		if (clocks) {
1803 1847
			i2c_step();
1804 1848
		}
1805 1849
		rtc_step(clocks);
…
155 unchanged lines
modified src/vera_spi.c +7 −0
…
41 unchanged lines
42 42
	}
43 43
}
44 44
45 +
// A byte is on its way: vera_spi_step() is counting clocks toward its end.
46 +
bool
47 +
vera_spi_busy(void)
48 +
{
49 +
	return busy;
50 +
}
51 +
45 52
uint8_t
46 53
vera_spi_read(uint8_t reg)
47 54
{
…
37 unchanged lines
modified src/vera_spi.h +2 −0
…
2 unchanged lines
3 3
// All rights reserved. License: 2-clause BSD
4 4
5 5
#include <inttypes.h>
6 +
#include <stdbool.h>
6 7
7 8
void vera_spi_init();
8 9
void vera_spi_step(int mhz, int clocks);
10 +
bool vera_spi_busy(void);
9 11
uint8_t vera_spi_read(uint8_t address);
10 12
void vera_spi_write(uint8_t address, uint8_t value);
modified src/video.c +32 −0
…
17 unchanged lines
18 18
#include "i2c.h"
19 19
#include "audio.h"
20 20
21 +
#include <math.h>
21 22
#include <stdbool.h>
22 23
#include <limits.h>
23 24
#include <stdint.h>
…
1237 unchanged lines
1261 1262
	return PIXEL_FREQ * steps / mhz;
1262 1263
}
1263 1264
1265 +
// The least n for which pos + n * step passes end, for a pos on the grid
1266 +
// of eighths; 1 for one off it.
1267 +
static uint32_t
1268 +
clocks_to_pass(float pos, float end, double step)
1269 +
{
1270 +
	if (pos > end || pos * 8 != floorf(pos * 8)) {
1271 +
		return 1;
1272 +
	}
1273 +
	return (uint32_t)((end - pos) / step) + 1;
1274 +
}
1275 +
1276 +
// How many CPU clocks video_step() can take in one call and still do
1277 +
// exactly what that many one-clock calls would: up to and including the
1278 +
// clock on which the beam passes the end of a VGA or NTSC line, the first
1279 +
// time it can raise an interrupt or end a frame. That holds only while the
1280 +
// beam moves by a whole number of eighths a clock (25 MHz / mhz, so mhz
1281 +
// divides 200, as the X16's 8 does): then every position is exact in a
1282 +
// float, and one big step adds up to the same as many small ones.
1283 +
// Otherwise the answer is 1.
1284 +
uint32_t
1285 +
video_clocks_to_line_end(uint8_t mhz)
1286 +
{
1287 +
	if (mhz == 0 || 200 % mhz) {
1288 +
		return 1;
1289 +
	}
1290 +
	const double step = scan_pixels(mhz, 1);
1291 +
	const uint32_t vga = clocks_to_pass(vga_scan_pos_x, VGA_SCAN_WIDTH, step);
1292 +
	const uint32_t ntsc = clocks_to_pass(ntsc_half_cnt, NTSC_HALF_SCAN_WIDTH, step);
1293 +
	return vga < ntsc ? vga : ntsc;
1294 +
}
1295 +
1264 1296
bool
1265 1297
video_step(float mhz, float steps, bool midline)
1266 1298
{
…
1216 unchanged lines
modified src/video.h +1 −0
…
13 unchanged lines
14 14
bool video_init(int window_scale, float screen_x_scale, char *quality, bool fullscreen, float opacity);
15 15
void video_reset(void);
16 16
bool video_step(float mhz, float steps, bool midline);
17 +
uint32_t video_clocks_to_line_end(uint8_t mhz);
17 18
bool video_update(void);
18 19
void video_end(void);
19 20
bool video_get_irq_out(void);
…
23 unchanged lines