{"number":1,"title":"x16emu is a riscv64 program","body":"x16emu is a riscv64 program: it builds cleanly with the Makefile's `-O3 -Wall -Werror` on a real RISC-V board, passes the control-API conformance suite there, emulates bit-for-bit as it does on x86, keeps up with a real X16 at the BASIC prompt and in programs that wait with `WAI`, and is built and booted by OxygenFlow on narvi-1 for every push.\n\n## What changed, a line a commit\n\n- `4135f58c` build: pin retro-remote-debug-controller as an oxy subrepo. The oxy tree lacked it (a nested Git checkout), so a fresh workspace failed with `retro_control.h: No such file or directory`. Pinned at the hub state whose files equal Git's pin `7b2fd15`, short of the hub's contract 0.6, which this emulator does not implement.\n- `ffa902be` video: align the framebuffer for the `uint32_t` writes into it (a `uint8_t` array cast to `uint32_t *`; in narvi's build it was 8-aligned by luck).\n- `59b66164` build: name the revision without Git where there is no Git. Outside a Git checkout every compile printed Git's usage text; now `oxy describe` names the state, else `00000000`. A Git checkout still gives the same hash as before.\n- `30ba236e` audio: work out the sample clock ratios once, not after every instruction (several 64-bit divisions by the run-time `MHZ` per instruction).\n- `74f67006` main: step the MIDI card's UARTs only when there is a MIDI card.\n- `ea204ade` main: test the KERNAL IEEE address range before calling the intercept (the call's prologue and epilogue were most of its cost).\n- `632655b7` video: keep the beam step per clock count instead of dividing each time.\n- `b97528d2` video: keep the bitmap layer's settings in registers across the line (byte stores alias everything, so every field was reloaded per pixel; `x % tilew` per pixel is now a counter).\n- `e9b7911a` main: while the CPU waits in `WAI`, step the machine to its next event at once. The big one; see below.\n- `9909b2ad` video: draw 8 bpp bitmap lines without the per-pixel shift and mask.\n- `94cbcffa` ci: build and test x16emu natively on narvi-1, OxygenHub's RISC-V runner.\n- `d9db64ba` ci: boot the R49 ROM on narvi-1 instead of the conformance suite (the syscall tier refuses TCP sockets; see below).\n- `0aa690b5` README: building and running on riscv64 Linux.\n- `c710878f` ci: point at oxygenflow#73 for the syscall tier's loopback sockets.\n- `4916c7e4` oxyignore: the `x16emu` and `makecart` a build leaves in the tree.\n\nEvery commit message quotes the warning or failure it fixes, or the measurement that justified it.\n\n## narvi\n\n- Board: OrangePi RV2, Ky X1 (8 × X60 cores, rv64imafdcv, 1.6 GHz, performance governor), 7.7 GiB.\n- Ubuntu 24.04.5 LTS, kernel 6.6.63-ky.\n- gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1), SDL2 2.30.0, zlib 1.3.\n- oxy 0.5.0 is installed there, so the bookmark was fetched from the hub (`oxy clone` then `oxy bookmark switch riscv-1`, then `oxy pull`); all history was recorded on this machine.\n- The baseline, `make` on the unchanged tree (main plus the subrepo), passed: no compiler warnings at all, 2 min 7 s. RISC-V needed no build fixes. On purpose I looked for unsigned `char` (a `-funsigned-char -Wtype-limits -Wchar-subscripts` sweep: nothing), unaligned access (`-Wcast-align=strict`: the framebuffer, fixed), endianness (`src/endian.h` keys off the compiler's `__BYTE_ORDER__`, right for riscv64) and architecture-specific code (none).\n\n## Conformance on narvi\n\n**22/22 passed**, run natively as `.github/workflows/conformance.yml` does on x86, with the final build: with the zero-filled ROM (1.35 s) and with the real R49 ROM (22/22).\n\nOne run in about 30 with the zero-filled ROM fails part-way: the emulator exits with \"CPU program counter reached $ffff\". With no ROM code, the CPU runs from `$0000` in RAM, which is randomised at power-on from `time()`. With the seed pinned (an `LD_PRELOAD` `time()`), 30 seeds give identical states at frame 300 for the base build and this one, and seed 28 reaches `$FFFF` in both. So the GitHub workflow has the same latent flake; adding `-zeroram` to its command would remove it. Not changed here.\n\nIt also boots to `READY.` headless on narvi, and was run once windowed: there is no display on narvi, so under Xvfb, with SDL's X11 driver. The screen was right (the banner, then BASIC), but by its own title figure it ran at 3-4% of a real X16, with the time in Mesa's software GL (`swrast_dri.so`) drawing the window. `SDL_RENDER_DRIVER=software` and `-quality nearest` did not change that. A board with a working GPU driver may differ; not measured.\n\n**Correctness beyond the suite.** From power-on with `-zeroram -nohostfs`, five BASIC workloads, each with sound on and off, are stepped to frame 900 over the control API, and the registers, low RAM plus all 64 RAM banks, and the screen are hashed. The workloads are: VERA scanline and VIA timer samples; LINEs on the 8 bpp bitmap; the same with FM and PSG notes; `SLEEP` in a loop; and machine code taking VIA1 Timer 1 interrupts every 16K clocks and `WAI`ting between them. All ten are identical on x86 and riscv64, and identical before and after every commit here.\n\n## Speed\n\nMeasured over the control API: boot headless (SDL dummy drivers), settle, pause, then step 600 frames in chunks of 60 and time them. One X16 frame is 1/59.52 s, so % of a real X16 = emulated seconds / wall seconds. In real-time mode that caps at 100%, so the figure that compares builds is the process's CPU seconds per emulated second, all threads included. \"idle\" is the BASIC prompt; \"busy\" is a BASIC program drawing random LINEs on the 320×240×256 bitmap and playing FM and PSG notes without pause. The narvi and x86 base and final runs were interleaved: 3 runs each on narvi, 5 each on x86, medians given.\n\n| | base | this proposal |\n|---|---|---|\n| narvi, idle, real time | 2.41 s CPU/emulated s: **42%** of a real X16 | 0.76: **100%** (131% flat out) |\n| narvi, busy, real time | 1.95: **51%** | 1.63: **61%** |\n| narvi, idle, `-warp` | 50% | 310% |\n| narvi, busy, `-warp` | 85% | 98% |\n| x86 (i7-6700K, gcc 15.2, SDL 2.32.10), idle | 0.629 | 0.207 |\n| x86, busy | 0.577 | 0.383 |\n| x86, idle, `-warp` | 183% | 1016% |\n| x86, busy, `-warp` | 291% | 374% |\n\nx86 did not get slower anywhere. That machine was loaded by other work (load average 7-10 on 8 threads), so its figures are noisier than narvi's, but the medians are consistent.\n\n**Where the time went on narvi** (perf 7.0, `cpu-clock` sampling, plus subtraction runs with `-sound none` and `-warp`). At the base, the BASIC prompt was mostly the main loop going round once per clock while the 65C02 sat in `WAI`, stepping VIA1, VERA's beam, I2C, the RTC and audio each time: about 8 million passes a second. `e9b7911a` steps them by many clocks in one pass instead, but only as far as the next clock on which something could raise an interrupt: VERA to the end of the line, audio to its next render, and VIA1 a clock at a time, stopping on the clock its interrupt rises. That keeps the result bit-for-bit (the ten-run check above includes the timer-interrupt `WAI` loop). Devices not shown to add up that way keep the clock-by-clock loop: VIA2, the serial bus, the MIDI card, an SPI byte in flight, YM2151 IRQs, the debugger, BASIC pasting, and the KERNAL hooks.\n\nThe busy workload never waits. On narvi it now goes about 40% to VERA's rendering (`render_line`'s compositing and palette, the bitmap and text layers), about 30% to the 65C02 core and memory decoding, about 8% to `video_step`, and about 8% to audio (ymfm, the PSG, mixing). To reach 100% there would need roughly another 1.6×. That means rendering at least twice as cheaply (compositing straight into the framebuffer, a line at a time, without the `col_line` round trip; per-byte text and tile rendering), a cheaper 65C02 dispatch, and stepping the devices by events rather than per instruction while the CPU runs too. That is the same idea as the `WAI` change, but with the CPU running it is a larger rework, and I did not attempt it tonight.\n\n**Tried and not kept.**\n- I2C stepped once per instruction, on its own: −0.8% idle, +1.6% busy, a wash. It went in with the `WAI` change, where it matters.\n- `-falign-functions=64 -falign-loops=16 -falign-jumps=16` in a riscv64 Makefile block: 7% faster at the prompt on one tree, 2-3% slower on the final one. On the X60, where hot code lands moves run time by several per cent: one unrelated change once slowed the prompt by 7% on its own. So every figure here is a median, and the layout-sensitive comparisons were rechecked aligned both ways.\n\nNo RISC-V assembly or intrinsics; every change is portable C and helps x86 as well.\n\n## The pipeline (`.flow/`)\n\n`.flow/pipeline.kdl` runs on narvi-1 (`runs-on \"riscv64\"`, the syscall tier):\n\n- **build**: `make -j6`, the binary kept as artifact `x16emu-linux-riscv64` with the receipt.\n- **boot**: takes that binary, boots the R49 ROM headless, and has BASIC print `SUM 333833500`.\n\nRuns: #1 build passed, conformance failed (below); #2 and #3 passed (build 124 s and 106 s, boot 6 s): https://oxygenhub.io/andymccall/x16-emulator/flow/3\n\nThe image is Ubuntu Base 24.04.5 riscv64 (checked against its SHA-256) plus build-essential, make, libsdl2-dev, zlib1g-dev, python3 and python3-pytest by apt in a chroot, and the R49 `rom.bin` from the emulator's R49 release (zip and ROM checked against their SHA-256; it is the same file as the repository's). It is built by `.flow/image/build-riscv64.sh` on narvi (no Docker on the board) and imported into narvi-1's Flow home as `cas://blob/127605ea…c157`. It has not been `flow rootfs push`ed to the hub, so only narvi-1 holds it; if its CAS is cleared, rerun the script and re-import. Importing the same tarball into a second Flow home gave a different hash, so a re-import may need a re-pin. An earlier import of the first image (`a1405289…`) is still in narvi-1's CAS, unused, if you want the space back.\n\nThe conformance suite could not be the second job. The syscall tier brings up `lo` for `network \"loopback\"`, but its seccomp filter refuses every socket family except `AF_UNIX`, so the suite's HTTP to 127.0.0.1 fails with EAFNOSUPPORT and every test skips (run #1). Filed as oxygenflow#73. Python and pytest are already in the image for when that is lifted.\n\n## For the enhanced fork (`~/development/enhanced-x16-emulator`), a later night\n\n- The `WAI` fast-forward, and the per-instruction costs before it (audio's run-time `MHZ` divisions, the MIDI step without a card, the IEEE intercept call, the beam's double divisions, the per-pixel `%` and alias reloads in the bitmap layer), are all in code it shares; the same changes should apply. Its `/perf` API will show them directly.\n- `via_step()`'s Timer 1 inaccuracy, below, is shared too.\n- Its CMake build will hit the same code-placement noise on the X60: measure in medians.\n\n## Found and left alone\n\n- **`via_step()` Timer 1**: a multi-clock step that ends exactly on the timer's −1 state consumes 1 clock where it should consume `cnt + 1`, so the timer runs `cnt` clocks ahead. Rare (only when an instruction's clocks land exactly there), the same on every architecture, and kept bit-for-bit as it is.\n- **HostFS ties emulated time to the host**: when a KERNAL call is serviced by HostFS, the CPU's clock is advanced by the wall time it took (`main.c`, \"missed_ticks\"). So any run that touches HostFS (BASIC's AUTOBOOT probe at power-on does) is not reproducible, even on one machine. `-nohostfs` makes it so.\n- **A paused emulator ignores SIGTERM**: its main loop sits in the control API's pause, so the SDL quit event is never handled.\n- **`SDL_VIDEODRIVER=wayland` without a compositor** (set in narvi's session) makes `SDL_Init` fail; noted in the README.\n\n## Not done\n\n- Busy programs are at 61% of a real X16 on narvi, not 100%; what it would take is above.\n- No GitHub Actions job for riscv64 (the optional item): the OxygenFlow pipeline on a real board covers it, and I did not add a QEMU job I had not seen pass.\n- The conformance suite does not run in CI on RISC-V until oxygenflow#73 is resolved.\n- `flow run` on narvi stopped part-way through copying the tree, with a bare `No such file or directory`, before any job ran; I tested the pipeline by pushing instead. Not investigated.","author":"andymccall","state":"landed","target":"main","source":{"kind":"bookmark","bookmark":"riscv-1"},"changes":[{"hash":"11debbbf53032edd2c9a062a0017103956232ce70971e0e688d421b6e4336e9c","message":"build: pin retro-remote-debug-controller as an oxy subrepo\n\nThe Makefile compiles src/extern/retro-remote-debug-controller/core and the\nconformance suite lives there too, but the oxy tree did not carry it: it\nwas a nested Git checkout, so `oxy status` listed it as untracked and a\nfresh workspace failed with\n\n    src/control_backend.c:5:10: fatal error: retro_control.h: No such file or directory\n\nIt is now a subrepo of doomsdayone/retro-remote-debug-controller on\nOxygenHub (public), which a Flow runner fetches into the job's workspace.\nThe pin is the state formed by its changes up to \"client+server:\nURL-encode key text\" (0.5.0), checked file for file to be the same tree as\nthe Git submodule's pin, 7b2fd15. The hub's main has since moved to\ncontract 0.6 (/perf), which this emulator does not implement, so the pin\ndeliberately stops short of it.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792540086226,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"d40fd2cac2565bcd04181aa8b04dfdb0636f8536213d6a292c178be57f7ce311","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"d40fd2cac2565bcd04181aa8b04dfdb0636f8536213d6a292c178be57f7ce311","scope":"changes,refs,seal","expires_ms":1792540692533,"revoked":false}},{"hash":"ea9efff4e5406b975d308d34a1f8ea123381cce12ed7b5f80fac3cae8792d2c3","message":"video: align the framebuffer for the uint32_t writes into it\n\nrender_line() writes the framebuffer a pixel at a time through a\nuint32_t pointer, but it is declared as a uint8_t array, which C only\npromises byte alignment. gcc -Wcast-align=strict points at it:\n\n    src/video.c:1195:41: warning: cast increases required alignment of\n    target type [-Wcast-align]\n\nOn x86 a misaligned store is merely slower. On riscv64 Linux it traps\nand the kernel emulates it, hundreds of times slower, or it faults. In\nnarvi's baseline build the linker happened to place it at 0x1b7a58,\n8-byte aligned, so nothing went wrong yet; _Alignas makes that a\nguarantee instead of luck. (The warning itself stays: it judges the\npointer's type, not the object.)","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792540356522,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"d6ea60db88d4176a8a7b28984a70c21afe862e16e1feb85100848ea48a880d7a","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"d6ea60db88d4176a8a7b28984a70c21afe862e16e1feb85100848ea48a880d7a","scope":"changes,refs,seal","expires_ms":1792541016803,"revoked":false}},{"hash":"6b218d3faaaffff04280826be759421e92e7922d09c3662a9194a316c9c5d750","message":"build: name the revision without Git where there is no Git\n\nGIT_REV asked Git unconditionally. Outside a Git checkout (an Oxygen\nworkspace, as on narvi, or a Flow job's workspace) every compile then\nprinted Git's usage text, 36 lines of it containing \"warning\", before\nfalling back:\n\n    warning: Not a git repository. Use --no-index to compare two paths outside a working tree\n    usage: git diff --no-index [<options>] <path> <path>\n\nwhich buried the compiler's own output in the build log, and the\nversion string read \"0000000+\".\n\nGit is now asked only inside a Git work tree, with the same command as\nbefore. Otherwise `oxy describe` names the state (riscv-1-ffa902be), and\nwhere neither exists the revision is 00000000. Checked in all three: a\nGit checkout still gives da994445.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792540407926,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"d6ea60db88d4176a8a7b28984a70c21afe862e16e1feb85100848ea48a880d7a","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"d6ea60db88d4176a8a7b28984a70c21afe862e16e1feb85100848ea48a880d7a","scope":"changes,refs,seal","expires_ms":1792541016803,"revoked":false}},{"hash":"9a6dd8288e688b1c07d0291a355ac1ccee4ba62e289bd7992b925997dad453d3","message":"audio: work out the sample clock ratios once, not after every instruction\n\naudio_step() runs after every emulated instruction. It used the\n*_SAMP_CLKS_PER_CPU_CLK macros, which divide by MHZ, a variable, so each\ncall did several 64-bit divisions at run time and then divided by the\nresult as well. They are constants for the run: audio_init() now works\nthem out once (MHZ is set by then), and the per-instruction division is\nreplaced by a multiply and compare, dividing only when the YM buffer is\nnearly full. min(clocks, room / ratio) is clocks exactly when\nclocks * ratio <= room, so the results are the same integers.\n\nperf on narvi (riscv64) put audio.c:242-247 at about 13% of the run\ntime at the BASIC prompt; integer division is slow there. Measured with\nthe emulator stepping 600 frames, real-time mode, SDL dummy drivers\n(CPU seconds per emulated second, median of 3; % of a real X16):\n\n    narvi  idle  2.406 -> 2.183  (42% -> 46%)\n    narvi  busy  1.974 -> 1.914  (51% -> 52%)\n    x86    idle  0.546 -> 0.393\n    x86    busy  0.574 -> 0.465\n\nEmulation is unchanged: from power-on with -zeroram -nohostfs, three\nBASIC workloads (VERA/VIA timing samples, bitmap LINE drawing, FM and\nPSG notes), each with sound on and off, give the same registers, RAM\n(low and all 64 banks) and screen at frame 900 as before, on x86 and\non riscv64.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792544544521,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","scope":"changes,refs,seal","expires_ms":1792548747025,"revoked":false}},{"hash":"51099110490bc0783baaa968ce376efa2a527a579f603f1faa563715dc437d60","message":"main: step the MIDI card's UARTs only when there is a MIDI card\n\nmidi_serial_step() ran after every instruction for both UART channels,\nwith or without -midicard. Without a card its clock decrement is zero,\nso it did nothing but cost a 64-bit multiply and a loop per channel,\nand every way the CPU could see the UARTs (reads, writes, the IRQ line)\nis already behind has_midi_card. It is now stepped only with a card.\n\nperf on narvi (riscv64) put midi.c:643-644 at about 5% of the run time\nat the BASIC prompt. Measured as before (CPU seconds per emulated\nsecond, median of 3; % of a real X16):\n\n    narvi  idle  2.183 -> 2.068  (46% -> 48%)\n    narvi  busy  1.914 -> 1.870  (52% -> 53%)\n    x86    idle  0.393 -> 0.405  (within this machine's noise)\n    x86    busy  0.465 -> 0.471  (within this machine's noise)\n\nEmulation is unchanged: the six-run determinism check (registers, all\nRAM, screen at frame 900) matches on x86 and riscv64.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792545023787,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","scope":"changes,refs,seal","expires_ms":1792548747025,"revoked":false}},{"hash":"0adc2e74f5cf3318bed4a9a1bbf89afd9c9a7e46539f08e9ec66574243cc770d","message":"main: test the KERNAL IEEE address range before calling the intercept\n\nhandle_ieee_intercept() ran after every instruction, and almost always\nreturned at once because the PC was below $FEA8. The function is large,\nso even that early return paid for a full prologue and epilogue: perf on\nnarvi (riscv64) put 66% of the function's samples on its entry and exit\nlines, about 4% of all run time at the BASIC prompt. The loop now tests\nregs.pc >= 0xFEA8, the function's own first condition after its enable\nflag, before calling it; both are side-effect free, so it is the same\ntest.\n\nMeasured as before (CPU seconds per emulated second, median of 3):\n\n    narvi  idle  2.068 -> 2.032  (48% -> 49% of a real X16)\n    narvi  busy  1.870 -> 1.884  (53%, within run-to-run spread)\n\nx86 was within this machine's noise. The determinism check matches on\nx86 and riscv64.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792546911461,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","scope":"changes,refs,seal","expires_ms":1792548747025,"revoked":false}},{"hash":"12673a41ba171e8324048662eae7ffd77c14453bb43edca023026f60f34d4978","message":"video: keep the beam step per clock count instead of dividing each time\n\nvideo_step() runs after every instruction and worked out how far the\nbeam moves, PIXEL_FREQ * steps / mhz, twice, in double precision, with\ntwo divisions. The clock count is almost always a handful, so the\nquotient for each count below 32 is now kept in a table filled with the\nsame expression whenever mhz changes, and the two counters share one\nresult. The same expression gives the same double, so the beam position\nis bit-for-bit what it was; larger counts (after a HostFS call) still\nuse the formula.\n\nMeasured as before (CPU seconds per emulated second, median of 3):\n\n    narvi  idle  2.032 -> 2.046  (49%, within run-to-run spread)\n    narvi  busy  1.884 -> 1.846  (53% -> 54% of a real X16)\n\nx86 was within this machine's noise. The determinism check matches on\nx86 and riscv64.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792546911572,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"3d452845da486e20d57a9ae4071679128d85ca843b673d411affa03074d60e16","scope":"changes,refs,seal","expires_ms":1792548747025,"revoked":false}},{"hash":"39d98c9da02ad9fc397ccdc29d897c2337c04d23457efd8b673305c75274a15b","message":"video: keep the bitmap layer's settings in registers across the line\n\nrender_layer_line_bitmap() writes layer_line[] a byte at a time, and C\nlets a byte store alias anything, so the compiler had to load every\nfield of the layer's properties, and reg_layer's palette offset, again\nfor every pixel through the pointer into the global array. It also\nworked out x % tilew, a division by a run-time value, for every pixel.\nIt now works from a local copy of the properties (whose address does\nnot escape, so no store can touch it), reads the palette offset once,\nand counts x % tilew up and wraps it. The values are the same.\n\nThe busy workload (BASIC drawing LINEs on the 320x240 bitmap layer)\nspent about 41% of narvi's run time rendering. Measured with the\nemulator stepping 600 frames, real-time mode, SDL dummy drivers, CPU\nseconds per emulated second; both builds aligned the same way\n(-falign-functions=64 -falign-loops=16 -falign-jumps=16), because\nwithout that this change moved unrelated hot code and swung the idle\nfigure by 7% on this core on its own:\n\n    narvi  busy  1.794 -> 1.737  (56% -> 58% of a real X16)\n    narvi  idle  2.012 -> 2.022  (unchanged; no bitmap layer there)\n    x86    busy  0.530 -> 0.422  (median of 3)\n\nThe determinism check matches on x86 and riscv64.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792549439202,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","scope":"changes,refs,seal","expires_ms":1792552057809,"revoked":false}},{"hash":"8e8d9cad1884b197145a28d2a55bd2177ebeeeeec53e08cbe03e6388bcb80519","message":"main: while the CPU waits in WAI, step the machine to its next event at once\n\nAt the BASIC prompt, and in any program that waits for VSYNC with WAI,\nthe 65C02 spends most of its time waiting. step6502() then advances one\nclock and returns, so the main loop went round once per clock, about\n8 million times a second, stepping VIA1, VERA's beam, I2C, the RTC and\naudio and checking for interrupts each time. That was most of narvi's\nrun time at the prompt.\n\nwai_fast_forward() steps them by many clocks in one go instead, only as\nfar as the next clock on which anything could raise an interrupt, so\nthe machine does exactly what it did clock by clock:\n\n- VERA: to the clock on which the beam passes the end of a VGA or NTSC\n  line (video_clocks_to_line_end()). That is exact while the beam moves\n  a whole number of eighths a clock (mhz divides 200, so at the X16's\n  8 MHz): every position is exact in a float, so one big step and many\n  small ones agree.\n- Audio: to the clock on which audio_step() next renders, where the CPU\n  could see a change (the PCM FIFO, VERA's AFLOW interrupt, the YM2151)\n  (audio_clocks_to_render()); the sample counters are integers.\n- VIA1: a clock at a time, stopping on the clock its interrupt rises.\n  via_step() is not stepped by the stretch: a step that ends on Timer\n  1's -1 state takes 1 clock where it should take cnt + 1, so it does\n  not add up the way single clocks do (an existing inaccuracy, left as\n  it is).\n- The RTC counts clocks; the SD card's SPI is left clock-by-clock while a\n  byte is in flight (its counter is a float).\n\nEverything not shown to add up that way keeps the clock-by-clock loop:\nVIA2, the serial bus, the MIDI card, YM2151 interrupts, the debugger,\nBASIC pasting, and the KERNAL hooks at $FEA8 and up.\n\nI2C is now stepped once per pass rather than once per clock: it acts only\nwhen the clock or data line the VIA drives changes, which happens between\ninstructions, so the second and later calls did nothing. (On its own this\nmeasured within narvi's noise; with the fast-forward it would be called\nhundreds of times per pass.)\n\nMeasured with the emulator stepping 600 frames, real-time mode, SDL dummy\ndrivers, CPU seconds per emulated second, median of 3:\n\n    narvi  idle  2.189 -> 0.698  (46% -> 100% of a real X16, 143% flat out)\n    narvi  busy  1.797 -> 1.647  (56% -> 61%)\n    x86    idle  0.412 -> 0.216\n    x86    busy  0.430 -> 0.454  (within this machine's noise)\n    x86    idle in -warp: 280% -> 1155% of a real X16\n\nEmulation is unchanged. From power-on with -zeroram -nohostfs, five BASIC\nworkloads, each with sound on and off, give the same registers, RAM (low\nand all 64 banks) and screen at frame 900 as before, on x86 and on\nriscv64. Two of them are for this change: SLEEP in a loop (the KERNAL\nwaits in WAI for VSYNC), and a machine-code loop that takes VIA1 Timer 1\ninterrupts every 16K clocks and WAIs between them, logging the scanline\nand the timer at each wake-up.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792549912260,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a","12673a41ba171e8324048662eae7ffd77c14453bb43edca023026f60f34d4978"],"head":false,"signature":{"status":"valid","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","scope":"changes,refs,seal","expires_ms":1792552057809,"revoked":false}},{"hash":"35bd422c903281cc261aa568b0d6fc798d6257121c49444ae49b760750922384","message":"video: draw 8 bpp bitmap lines without the per-pixel shift and mask\n\nAt 8 bits a pixel each VRAM byte is one pixel: first_color_pos is 0,\ncolor_fields_max 0 and color_mask $FF, so the general loop's per-pixel\nshift, mask and x_add multiply always come to the byte itself. perf on\nnarvi put that line at 4.6% of the run time drawing on the 320x240x256\nbitmap (BASIC's SCREEN $80). An 8 bpp line now takes a loop that reads\nthe byte; the palette offset is applied as before. Other depths are\nunchanged.\n\nMeasured with the emulator stepping 600 frames, real-time mode, SDL\ndummy drivers, CPU seconds per emulated second, median of 3:\n\n    narvi  busy  1.647 -> 1.579  (61% -> 63% of a real X16)\n    narvi  idle  0.698 -> 0.697  (100%, no bitmap)\n    x86    busy  0.454 -> 0.408\n\nThe determinism check (two of its workloads draw on the 8 bpp bitmap)\nmatches on x86 and riscv64.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792551040353,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a","39d98c9da02ad9fc397ccdc29d897c2337c04d23457efd8b673305c75274a15b"],"head":false,"signature":{"status":"valid","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","scope":"changes,refs,seal","expires_ms":1792552057809,"revoked":false}},{"hash":"a673804c781c66835a6e686397bb9cc24ccd999956adef71ea1c37222c4034fc","message":"ci: build and test x16emu natively on narvi-1, OxygenHub's RISC-V runner\n\nAn OxygenFlow pipeline (.flow/pipeline.kdl) with two jobs on narvi-1, an\nOrangePi RV2 (Ky X1, riscv64), in the syscall tier since the board has no\nhardware virtualisation:\n\n- build: make as README.md gives it, with the Makefile's -Werror, and the\n  riscv64 binary kept as the artifact x16emu-linux-riscv64 with the\n  runner's signed receipt;\n- conformance: that binary, taken as the job's input, against the\n  retro-remote-debug-controller conformance suite, headless, the way\n  .github/workflows/conformance.yml runs it on x86 (zero-filled ROM,\n  -warp, a loopback control port); pytest's JUnit XML is kept for the\n  hub's Tests tab, and a run that skipped every test fails.\n\nThe image is Ubuntu Base 24.04.5 riscv64, checked against its published\nSHA-256, with build-essential, make, libsdl2-dev, zlib1g-dev, python3 and\npython3-pytest installed by apt in a chroot (.flow/image/build-riscv64.sh;\nthere is no Docker on the board). It is imported on narvi-1 as\ncas://blob/a1405289def4850dade956ea6d4e4870a73bb631cc353f16f0e5fcd796fe2541.\nImporting the same tarball into another Flow home gave another hash, so\nthe pin is the image narvi-1 holds; rebuild, re-import there and re-pin\nwhen the package list changes.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792551457344,"deps":[],"head":false,"signature":{"status":"valid","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7f1108192b02c0fac8dae76a46d5a25657ba112ccd44eb352dac913449a000a1","scope":"changes,refs,seal","expires_ms":1792552057809,"revoked":false}},{"hash":"712ea2b4d3463dfffafa6ec39721181afc825a9f5fcee0f9d6532678e3147ebe","message":"ci: boot the R49 ROM on narvi-1 instead of the conformance suite\n\nThe conformance job of the first run on narvi-1 (run #1) never reached\nthe emulator; every test was skipped with\n\n    emulator control port never came up: <urlopen error [Errno 97]\n    Address family not supported by protocol>\n\nThe syscall tier's seccomp filter refuses every socket family but\nAF_UNIX, network \"loopback\" or not, so no job there can talk TCP to\n127.0.0.1, and the suite is HTTP on a loopback port. The build job\npassed (129 s).\n\nIn its place a boot job takes the built binary, boots the R49 ROM\nheadless and has BASIC run a pasted program that prints\nSUM 333833500, read back through -echo: the 65C02 core, VERA's timing,\nthe KERNAL and BASIC working on riscv64, with no network. The image now\ncarries the ROM at /opt/x16rom, from the emulator's R49 release (the zip\nand the ROM checked against their SHA-256; the ROM is the same file as\nthe repository's rom.bin), and is pinned as\ncas://blob/127605eab9812e79d7c80d99e012105ca12dc7060bec8bffb81576660981c157.\nPython and pytest stay in it for the suite, for when the tier allows it.\nThe suite passes 22/22 run by hand on narvi.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792552005383,"deps":["a673804c781c66835a6e686397bb9cc24ccd999956adef71ea1c37222c4034fc"],"head":false,"signature":{"status":"valid","key":"ee5caabc7ed3eb19b0dce0ee599b4f56c8d219b9e5d0b273a174ac4a8e2fa24f","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"ee5caabc7ed3eb19b0dce0ee599b4f56c8d219b9e5d0b273a174ac4a8e2fa24f","scope":"changes,refs,seal","expires_ms":1792552605872,"revoked":false}},{"hash":"bd65c0cc08c750c00867eb046a091f94bdd79a8aefe2c48eca51214fb0019c12","message":"README: building and running on riscv64 Linux\n\nHow to build and run it on riscv64 Linux (packages, make, the dummy SDL\ndrivers for running without a display), with what was found doing it on\nan OrangePi RV2: the speed there, how slowly a window drawn by Mesa's\nsoftware OpenGL runs (3-4% under Xvfb), SDL_Init failing when the\nsession asks for Wayland without a compositor, and that measurements on\nthe X60 move by several per cent with code placement alone.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792552505660,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"7b25c1f45c9c9b3d132b83c21e093984c8bb8d341cb4e566f6ed4094c89b6a24","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7b25c1f45c9c9b3d132b83c21e093984c8bb8d341cb4e566f6ed4094c89b6a24","scope":"changes,refs,seal","expires_ms":1792553115890,"revoked":false}},{"hash":"10b68259f181be7632ef91f8f4292946f28dbfac6d968fa6f3a95c6f46160e08","message":"ci: point at oxygenflow#73 for the syscall tier's loopback sockets","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792552505717,"deps":["712ea2b4d3463dfffafa6ec39721181afc825a9f5fcee0f9d6532678e3147ebe","a673804c781c66835a6e686397bb9cc24ccd999956adef71ea1c37222c4034fc"],"head":false,"signature":{"status":"valid","key":"7b25c1f45c9c9b3d132b83c21e093984c8bb8d341cb4e566f6ed4094c89b6a24","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7b25c1f45c9c9b3d132b83c21e093984c8bb8d341cb4e566f6ed4094c89b6a24","scope":"changes,refs,seal","expires_ms":1792553115890,"revoked":false}},{"hash":"8799a5e4565e56cd4e16934ba0ebbf365f5d7dd4a291f2eb90095f5f17efbd9a","message":"oxyignore: the x16emu and makecart a build leaves in the tree\n\n.gitignore lists them, .oxyignore did not, so after `make` every\nworkspace showed them as new files, and `oxy describe` (now the\nversion string's source outside Git) named every build -dirty.","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"4cd9916c150f426895a390868c6999314e0ef4955d657f30d2805647c747fc65","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1792552505773,"deps":["03a3459e6372147b5d16d8410463208c38a7530ff5522230ddd2dddbb8f0829a"],"head":false,"signature":{"status":"valid","key":"7b25c1f45c9c9b3d132b83c21e093984c8bb8d341cb4e566f6ed4094c89b6a24","sealed":true,"revoked":false},"via":{"name":"agent:claude@andymccall","key":"7b25c1f45c9c9b3d132b83c21e093984c8bb8d341cb4e566f6ed4094c89b6a24","scope":"changes,refs,seal","expires_ms":1792553115890,"revoked":false}}],"projected_heads":["0adc2e74f5cf3318bed4a9a1bbf89afd9c9a7e46539f08e9ec66574243cc770d","10b68259f181be7632ef91f8f4292946f28dbfac6d968fa6f3a95c6f46160e08","11debbbf53032edd2c9a062a0017103956232ce70971e0e688d421b6e4336e9c","35bd422c903281cc261aa568b0d6fc798d6257121c49444ae49b760750922384","51099110490bc0783baaa968ce376efa2a527a579f603f1faa563715dc437d60","6b218d3faaaffff04280826be759421e92e7922d09c3662a9194a316c9c5d750","8799a5e4565e56cd4e16934ba0ebbf365f5d7dd4a291f2eb90095f5f17efbd9a","8e8d9cad1884b197145a28d2a55bd2177ebeeeeec53e08cbe03e6388bcb80519","9a6dd8288e688b1c07d0291a355ac1ccee4ba62e289bd7992b925997dad453d3","bd65c0cc08c750c00867eb046a091f94bdd79a8aefe2c48eca51214fb0019c12","ea9efff4e5406b975d308d34a1f8ea123381cce12ed7b5f80fac3cae8792d2c3"],"projected_state":"3b2f79b3a205582ff07886502248a7db2cac61164e9a8c11a4c2474271d39f27","conflicted":false,"approvals":[],"landable":false,"blockers":[],"created_ms":1792554535780,"updated_ms":1792573578306,"url":"https://oxygenhub.io/andymccall/x16-emulator/proposal/1","repository_url":"https://oxygenhub.io/andymccall/x16-emulator","source_url":null,"landed":{"by":"andymccall","via":"web","when_ms":1792573578306},"auto_land":null,"requested_reviewers":[],"labels":[]}