Changelog
[1.0.0-beta2] - 2026-09-02
Added
- Preferences ▸ Advanced ▸ Real-time clock fitted, so a Next with no RTC can be set permanently rather than only per run with
--no-rtcor by hand-editing the config. The tab now says that these settings take effect at the next machine start. - Preferences ▸ Display ▸ Hide the status bar in full screen, off by default. Leaving full screen restores the setting you had rather than always showing it.
- The toolbar carries Open, Reset, Pause/Resume and Debugger. Screenshot, the three tape controls and Cycle Scanlines have left it: each already had a keyboard shortcut, and tape is rarely the way software is loaded on a Next.
- View ▸ Toolbar shows and hides the icon toolbar, as View ▸ Status Bar does for the status bar. Remembered between sessions, and also on the Preferences Display tab.
--sld <path>loads a FantASM SLD, and the debugger’ssymcommand and a new ADPsldcommand take one too. A plain symbol export is accepted anywhere an SLD is, told apart by its contents. Loading one older than the program warns, the usual cause being a rebuild without a regenerate. Nothing reads the symbols yet.--esp espanswers the guest’s AT commands itself and services connections with host sockets, so.httpand friends work with no extra hardware and no shell plumbing. Every connection is logged; an unreachable host reportsDNS FailorCONNECT FAILwithin eight seconds rather than hanging. It reports itself joined to an access point namedBizmuthon10.0.2.15, so software that checks for a network before using one finds it.--esp <endpoint>attaches something to the Next’s ESP UART for one run:null(nothing attached — the default),loopback, which returns everything the guest sends paced by the wire, orbridge, which exposes the wire on a host TCP port. Persisted asesp.endpointin the config; nothing is attached unless you ask.--log-file <path>writes the log to a file as well as the terminal, including the startup lines. On Windows there was previously no way to capture a log at all: redirecting output to a file produced an empty one.- An
assertwritten in FantASM source stops the machine where it was written when the claim is false, reporting the source line and the message beside it: assert failed at main.asm:88 — HL survived the copy (hl==4). Loading the SLD arms one breakpoint per annotation, carrying the negation of what was claimed; loading another replaces them. An ADP client seesreason=assert, DeZog shows the claim and its message as the break reason, and a script reads it withannotation()— returning"continue"fromon_breakpointto break only on the failures it chooses. Both work in a headless--screenshotrun as well as at the window: a failed claim is reported and the run carries on, so one capture reports every broken claim rather than the first. Adebugannotation is armed the same way and never stops: it logs a quoted operand verbatim, or calls an unquoted one as a script function, every time its address is reached. bp add … ifwarns when a condition can never be true —a & 0x0F == 0x20,im == 5,mem[hl] == 0x1234— and arms the breakpoint anyway. An SLD’s!assertand!debugannotations are checked the same way at load, against their source lines.- Breakpoint conditions take arithmetic and shifts,
phys[addr]for memory whatever is paged in,port[addr], a width asmem[hl:2](little-endian, 1 to 4 bytes), and symbol names from a loaded SLD. A symbol is its window address everywhere except insidephys[…].&binds tighter than comparison, sonr[7] & 3 == 2reads as it looks. - The debugger injects serial faults — an ADP
serial drop|stall|delay|framing-error|break|clearcommand and matching Rhaiesp_drop_next(),esp_stall_ms(),esp_delay_ms(),esp_framing_error(),esp_break(),esp_faults_clear(). These are the link failures a real ESP cannot be asked for on demand: a dropped byte, a truncated delivery, a receiver stalled until it overruns, a far end gone quiet. They act on the UART’s receive path, so each works with--esp loopback,bridgeandespalike; nothing is armed unless you arm it, and a save state carries none of it. - The debugger drives the mouse: an ADP
mouse <dx> <dy> [buttons] [wheel]command and a matching Rhaimouse(), so mouse-reading code can be exercised headlessly with nothing at the window. watch_io(port)in a debugger script boundson_io_read/on_io_writeto the ports named, aswatch_memalready bounded memory writes; a barewatch_io()takes every port. Filtering inside the handler saves nothing, because the cost is reaching it. Breaking for existing scripts: an I/O handler with nowatch_ionow receives nothing, and says so at load — as anon_mem_writewith nowatch_memalways has, silently.--mouse "120:8,-4,0 130:0,0,1 134:0,0,0"schedules mouse motion and clicks against frame numbers for a headless run, as--keysdoes for the keyboard, and names any entry it cannot parse instead of dropping it.s.portraw[addr]in a debugger script gives the paging state behind a write-only port —$7FFD,$1FFD,$DFFD,$EFF7,$BF3B— in that port’s bit layout.s.port[addr]still reports what a guest read returns, which for these is the open bus; the pair mirrorsnragainstnrraw. A bit the hardware discards on write reads 0.- A debugger script reads every port the CPU can read, where before only
$FFand the mouse counters answered — the keyboard, both Kempston joysticks, the CTC, sprite status, the AY, ULA+, I²C, Layer 2, the nextreg pair, the UART, the SD card and the DMA. Watching a port no longer disturbs it: the UART keeps the bytes the program was waiting for and the errors it was about to report, the SD card stays in step with the program driving it, and the DMA read sequence stays where the program left it. - ADP commands that take an address take a symbol name instead —
bp add pc main_loop,mem,disasm,runto,bp add mem. A number still wins where a label is also valid hex.sym <name>resolves one by hand, andbp listshows the name beside the address. - Rhai scripts get
sym("name"), giving both the CPU-window address and the physical one, so a script can reach a symbol in a bank that is not mapped; andsrc(addr)for the reverse. An unknown name gives an empty map and says whether nothing was loaded or the name was misspelt. - With an SLD loaded,
bt()names the source position of each address in the backtrace,disasmlabels aCALL/JP/JR/DJNZ/RSTtarget, and a stop reports the file and line it landed on — in the ADP*stoppedevent as a trailingat=<file>:<line>, and in the debugger window’s status line. --esp bridgeputs the UART on localhost so anything can be the far end — a real ESP8266 on a USB serial adapter viasocat TCP:localhost:11002 /dev/ttyUSB0,b115200,raw, or a test script.--esp-portsets the port (esp.port, default 11002).
Fixed
--auto-keysmeans a press frame and nothing else. With--loadit silently became a cap on the boot instead, so one flag meant two unrelated things depending on another flag; the cap is now--max-boot-frames, and takes the number given rather than deriving one. Passing--auto-keyswith--loadsays it is ignored instead of quietly changing the timeout.--auto-keysstopped pressing Y some time ago, but its own--helpstill said “ENTER then Y” — as diddocs/cli-surface.mdand a comment that had drifted onto an unrelated constant. It presses ENTER once; repeated keys drove the file browser off its own listing.- A debugger script asked a 128K, +2A or +3 for its interrupt mode two ways and got two answers:
reg("im")read the CPU whileget_machine_state().imreported 0 whatever the machine was doing. Both now read one source, on every machine, along withiff1,iff2andhalt. portraw[$7FFD]— and$1FFDon a +2A/+3 — answers on a classic machine, so a script can ask which bank is at$C000. On a machine with no nextregs the paging is those ports, and they were unreadable. A machine with no such port still refuses rather than answering 0, which would read as “bank 0 is paged”.- Nextreg
$0Bmoves a UART onto a joystick port instead of being stored and ignored. With it set, the selected channel stops hearing the ESP module: receive and CTS come from the pad’sCandBbuttons, transmitted bytes go to joystick pin 7, and the module is told to hold off. HoldingCpulls the receive line low, which reads as a break. The pin-7 output modes (a static level, and the CTC-driven divider) are still unmodelled — nothing in the emulator is connected to that pin. - The UART acts on hardware flow control (
$163Bbit 5) rather than storing it: the Next holds the far end off two bytes short of a full receive FIFO, and will not start a frame while CTS is withheld. With the bit clear, as it is at reset, CTS is ignored and the far end is never held off. - View ▸ Full Screen fills the screen, hiding the title bar, the menu bar and the desktop. It maximised the window instead, which left all three in place. F11 leaves again from the emulator display, which is the only way out once the menu bar is hidden. A maximised window and a full-screen one are now separate states, and only full screen is remembered as such.
- A Preferences tab taller than the dialog was clipped, with no way to reach the controls below the fold. The dialog is now sized to its tallest tab.
- Breaking — a breakpoint condition reads memory as
mem[addr], not(addr).( )now groups and nothing else, so rewrite(hl) > $40asmem[hl] > $40. - Mouse capture hides the pointer on Windows builds that were made from a clean tree, where wxWidgets’ compiled-in blank cursor can be missing. A drawn one stands in for it.
- The Next loads tapes. Its Tape menu was live and it offered
.tapand.tzxin File ▸ Open, then answered load_tape not supported by ZX Spectrum Next to whatever was picked. A real Next has an EAR socket and NextZXOS loads tapes; playback drives that pin, so it works whatever is paged in. Real-time (edge) playback only — the fast ROM-trap loader needs the 48K ROM mapped, which a Next only is in a state the user does not choose, so Tape ▸ Fast is refused there rather than accepted and ignored. - Breaking — savestate format is v15, the +3 floppy controller’s, the +2A/+3 bus’s and the Next’s serialised shapes having changed. Save states written by an earlier build are refused with a message saying so; take a fresh one.
- A panic anywhere in the window no longer disables the whole UI. The state all the windows share sat behind a mutex that poisons on panic, so the first fault made every later attempt to read or write that state panic as well — a one-frame problem became a dead window, failing somewhere unrelated to the cause.
- A second Bizmuth started while one is already running says so, and says what it means: port 11001 is already in use — another Bizmuth is probably running, and this one has no adp server. It previously logged an errno and then announced that its debug servers were running, when it had none — so every debugger command silently reached the first process.
- A second debugger client is told the server is busy and disconnected, instead of completing the TCP handshake and then receiving nothing. Both servers take one client at a time and now say so; an ADP client gets
*busy -- ADP serves one client at a time, and a DZRP one is closed without a reply, that protocol being binary. - The disassembly gutter marks a breakpoint set from the ADP debugger or from DeZog, not only one set in the debugger window — and double-clicking such a line now removes it instead of silently adding a second breakpoint at the same address. The gutter read the address back out of the label, and three of the four things that set a breakpoint word their label differently.
- A debugger
machine [16k|48k|128k|plus2a|plus3|next]command reports the current machine or switches to another, taking effect at the next frame. A Rhai script reaches it by returning the command, so a scripted session can now exercise a machine switch — which previously only the Machine menu could do. Not available in a--screenshotcapture, which builds one machine and exits; asking there says so. - On Windows,
bizmuth.exe > logcaptures the output. Reattaching to the parent console rebound the standard handles unconditionally, so a redirection or a pipe the shell had already set up was discarded and the file came out empty. A plain terminal launch still prints to the terminal. - A breakpoint condition takes
$as a hex prefix, which the documentation has always said it does and the parser refused with unexpected character ‘$’. !(hl)in a breakpoint condition tests the byte at HL, as(hl)does everywhere else. It tested the HL register instead — silently, and the two readings agree often enough to look right.!hlstill tests the register, and!(a == 1)is still a negated comparison.- A disk image whose sector-size code is out of range is described rather than killing the emulator as the image is opened. One byte of a DSK set above 56 was enough.
- FORMAT TRACK on a +3 formats the track. It collected the sector descriptors the program sent and threw them away, so a formatted track read back as whatever was there before — and a freshly formatted blank track read back as sector not found. Formatting now also refuses a write-protected disk, which it previously ignored, and a head the disk has not got.
- A +3 write spanning several sectors writes them all. It accepted the first sector’s bytes, wrote it, and reported success with the rest of the transfer still to come — so a program writing a whole track wrote one sector of it. A write with the multi-track flag now crosses onto the second head as a read does, and both end reporting end-of-cylinder.
- A +3 write naming a sector that is not on the track reports sector not found, as a read does, instead of accepting the data and discarding it while reporting success.
- A 128K
.snaof 147487 bytes loads. That is the ordinary length when the paged bank is 2 or 5, since the file stores that bank twice — Bizmuth offered to load one and then refused it with a complaint about its size. - A malformed
.dskno longer shifts every track after it. A track’s sectors are now bounded by the size its own table entry declares, its sector-info entries by the track header, and the size table by the 256-byte disk header — each of which was bounded only by the length of the file. - Breaking — savestate format is v16, the +2A/+3 bus having gained a field. States written by an earlier build are refused with a message saying so; take a fresh one.
- A +2A or +3 floats on
$0FFDand its neighbours, the one port window that does. It reads the byte the ULA is fetching with bit 0 forced high, the last byte a contended access left on the bus while the ULA fetches nothing, and $FF once a program has locked paging. Every other unattached port still reads $FF. - Nextreg
$68bit 2 shifts a Timex hi-res screen by a pixel. Its half-pixel is a whole pixel at hi-res resolution, and was dropped as sub-pixel along with the standard modes, where it still is. - The floating bus returns what the ULA is actually fetching. It was anchored at the start of the scanline rather than at the ULA’s first fetch, so a read landed 60 T-states from where it should and the right half of every line read $FF; and it never went idle, where the real ULA leaves the bus alone for half of each eight-T-state group. A 48K’s first byte now lands 14338 T-states after the frame interrupt. Affects the 16K, 48K, 128K and the Next; the +2A/+3 has no floating bus.
- Single-stepping across a frame boundary no longer kills the emulator. A machine that had only ever been stepped — attach the debugger, set the program counter, step — rendered its first frame against a border timeline nothing had seeded, and a machine restored from a savestate did the same. The border is black for that one frame and correct from the next.
- A 16K, 48K, 128K or +2 charges contention on the T-states an instruction spends working with a contended address on the bus, not only on its reads and writes. Those went uncharged, so contended-memory code ran fast — a block copy into contended RAM by about a quarter. The +2A/+3 does not contend them and is unchanged, nor is the Next except under 48K or 128K machine timing.
bt()works on the 16K, 48K, 128K and +2A/+3, where it previously returned nothing on every machine but the Next — and returned it silently, so an empty backtrace could not be told from an unsupported one. A machine that records no history now says so instead of logging nothing.- The Next contends the CPU while the expansion bus holds the clock at 3.5 MHz, whatever speed nextreg
$07asks for. Contention was decided from the register rather than the speed the CPU is actually running at, so an expansion-bus session ran uncontended.--max-speednow governs it too, matching the clock instructions are timed at. OUT ($1FFD),Aon the Next costs no I/O contention. The port was contended from a decode the core has commented out; the live one covers the ULA,$7FFDand the two ULA+ ports only.- The Next’s memory contention follows the machine timing nextreg
$03selects, instead of always using the 128K ULA’s pattern. Under +3 timing — which is what the machine resets into and what the boot ROM sets — it stalls at seven of every eight T-states rather than six, and contends no I/O access at all. Pentagon timing now disables contention as it should; the$8FPentagon memory mapping, a different register, no longer does. bt()in a debugger script sees the instructions you stepped. The execution history was recorded by the frame loop only, so stopping and stepping into a bad jump — the casebt()exists for — showed the path up to the last full frame and none of the steps, with nothing saying so.- An SD card whose directory tree loops back on itself is reported rather than killing the emulator. Verifying such a card exhausted the stack, which aborts the process outright — no dialog, no log line.
--pc-traceand--esxdos-traceon a machine that cannot trace — anything but the Next — warn and name the file that will not be written, instead of logging that tracing started. The debugger’spctracecommand says the same rather than reporting success. A missing trace file read as a broken feature.- A corrupt savestate no longer crashes the emulator on the next SD clock, and a debugger script reading the SD port on one no longer takes the process down with it. The multi-block read stream recovers on the following sector instead.
- Timex hi-res (512×192) is clipped by the ULA clip window and scrolls with the ULA X scroll, as every other ULA mode does. It ignored both, so a clip set around a hi-res screen did nothing and the screen would not scroll horizontally.
- A +2A or +3 contends memory at seven of every eight T-states, where it stalled at one. Contended-memory code ran far faster here than on hardware, so anything timed against the beam — border stripes, multicolour, a loader’s timing loop — drifted. The 48K, 128K and +2 are unaffected.
- An unattached port on a +2A or +3 reads $FF rather than the screen byte the beam is fetching. That family’s ASIC has no general floating bus, so software using the 48K/128K floating-bus trick to sync to the display read plausible rubbish and appeared to work.
- No I/O access on a +2A or +3 is contended. The 128K’s I/O contention pattern was applied there, so an
INfrom a contended port cost up to six T-states more depending on where the beam was, and timing-critical code tuned on a +3 ran late. Memory contention is unaffected. - A +2A no longer answers the floppy-controller ports. Software probing for a controller found one and could then wait on a drive that is not fitted; a real +2A has the sockets empty. The disk motor bit drives nothing there either.
- A frame that starts with the ROM waiting on a tape block runs, instead of ending immediately having executed nothing. Between blocks of a fast (trap) load that is exactly where the ROM sits, so a load could stall for a frame at a time.
- Stepping through code on a 16K, 48K, 128K or +2A/+3 shows the border effects the code produces. A stepped
OUTwas timestamped against the last full frame instead of against itself, so every border change made while stepping landed at one point in the frame, and often out of order. - Stepping across a frame boundary starts the next frame, so a long debugging session no longer accumulates one frame’s worth of border and attribute changes per instruction stepped.
- Resetting a +3 recovers its floppy controller and stops the motor. The controller sits on the system reset line on hardware; here a reset left it in whatever phase it was in, and a reset was the obvious thing to try after a command had wedged it.
- Resetting a 48K clears ULA+ palette mode, as resetting a 128K already did. Enabling ULAplus and resetting left BASIC rendering through the leftover palette. The palette registers themselves survive the reset, as they do on hardware.
- Resetting a 128K, +2A or +3 no longer ejects the tape. The cassette is not on the reset line, and clearing the queue left the emulator showing a tape inserted with nothing left to load from it — so
LOAD ""after a reset failed on those machines and worked on a 16K or 48K. OUT ($0FFD),Aand its neighbours no longer repage a +2A or +3. The paging port was decoded without the address line the +3 ASIC requires, so any port with A15 and A1 clear that was not one of three named exceptions moved the ROM and RAM banks.- The +2A/+3 paging lock holds
$1FFDas well as$7FFD. After a program locked paging, a write to$1FFDcould still change the ROM bank or switch the machine into special all-RAM paging. The disk motor bit still answers, as it does on hardware. - A
$7FFDwrite while a +2A or +3 is in special all-RAM paging is recorded rather than thrown away, so the shadow screen can be selected there — it previously could not be at all — and the bank and ROM bits chosen there take effect on the return to normal paging. - A +3 command naming a side the disk hasn’t got is answered sector not found. On a single-sided disk it read the next cylinder’s data instead, and only failed at all on the last cylinder.
- READ ID on a double-sided disk names the track the head is actually over. Past cylinder 0 it named the wrong track entirely, and the command’s head byte was never consulted.
- FORMAT TRACK’s result reports the command’s sector-size code and the last sector descriptor written. It reported the filler byte as the size, over whatever cylinder/head/sector the previous read or write had left behind.
- A read or write with a sector-size code of 0 transfers the number of bytes DTL asks for. DTL was stored and never applied, so 512 bytes went where the hardware sends 128 — enough to break a loader that relies on the truncation.
- A multi-track (MT) read carries on onto the second head at the end of a cylinder instead of stopping there, and a read with SK set steps over a deleted-data sector instead of reading it. Those three flag bits were masked off the command byte and never consulted.
--keysand--mouseschedule against the same frame number in every run mode. A--screenshotcapture counted its own loop, so a schedule written for one mode fired at different frames in another once--loador--loadstatehad consumed frames first.--mouseand a script’smouse()in the same frame no longer overflow when both are large, a--keysframe near the top of the 32-bit range no longer panics, and--load’s automatic ENTER is held for the eight frames it says rather than nine.- A cancelled or failed starter-pack download leaves nothing behind. The temp zip stayed in the temp directory, and a part-written file stayed in the install directory where the completeness check counted it as having arrived. Two copies of Bizmuth downloading at once no longer share one temp file either.
- Input ▸ Next Navigation (
Ctrl+N) selects the Next’s arrow-key mode, which had no menu item at all. It is what the Next starts in, so the Input menu showed Sinclair Joystick ticked while the machine was in Next Nav — and choosing any of the three items on offer left no way back short of switching machines. --log-filecaptures a debugger script’slog()output and the replies to commands it issues. Both went straight to stderr, so the file held the startup lines and nothing else — and on Windows, where that flag is the only way to get a log at all, they were unreachable.- Pausing from the ADP debugger, or from a script, tells the other debug clients. A DeZog session waiting on its
CONTINUEwas never told the machine had stopped and hung until something else stopped it. - A debugger script’s breakpoints survive a machine switch. They were armed on the old machine and silently dropped, and the script could not put them back — a fresh
breakpoint(addr)was discarded as a duplicate of one that no longer existed. - A debugger script reading
s.im2_stuck_at/s.im2_stuck_stateoutside an event is told so, instead of getting the-1that also means “the interrupt chain is idle”. - A DeZog client connecting mid-session is no longer sent every stop that happened before it arrived, and stops raised with no client attached no longer accumulate for the life of the process. The default run has the DZRP server off, so that was every run.
bt()still reports a backtrace after switching machines with a debugger script loaded. Execution history was enabled only on the machine present at startup, so a switch left the stack empty with no way to tell that from a genuinely empty one.- A breakpoint removed over DZRP no longer leaves its label in
bp listand the debugger panel, wherebp clearcould then delete an unrelated live breakpoint that had been given the same id. Its one-shot flag is dropped too, which had made a later breakpoint on that id report as a step — silently discarded by DeZog rather than stopping. - Layer 2’s X scroll in 640×256 mode moves the picture two pixels per unit, as the hardware does, instead of one. Odd values also swapped which half of each byte reached the screen, drawing the whole line from the wrong nibbles — an artefact the hardware cannot produce.
- Layer 2 maps a bank of
$80or above to the bank the hardware maps it to. The logical-to-physical bank step used four bits where the hardware uses three, so every bank from$80up read and wrote the wrong 16K, and$F0wrapped to bank 0. - LoRes at a high
$33scroll no longer reads pixels from outside the screen bank. They >= 96address bump carried past the end of the bank instead of wrapping within it, so the top of the display was drawn from unrelated memory. - Radastan honours ULA+: with
$68bit 3 set the palette offset narrows to its low two bits under a fixed11, as the hardware does. Colours came from the wrong quarter of the palette. - On a 128K, +2 or +3 displaying the shadow screen, writing to the off-screen normal screen’s attributes no longer changes the picture. A program using bank 7 for display and bank 5 as a work area had its work painted over the display, timed to the beam.
- Mid-frame attribute effects now work on the shadow screen too, where writes to it were not tracked at all.
- Conditional breakpoints work on the 16K, 48K, 128K and +2A/+3. The condition was parsed, stored and listed back, then ignored — every conditional breakpoint on those machines stopped unconditionally. Scripts and conditions on them can now read the registers, the flags and memory as they can on the Next.
- The Sprite RAM viewer’s image is rendered outside the UI state lock, which its own canvas takes on every repaint — in live mode the two contended once per frame.
- Bizmuth starts even if the SD-card status icons cannot be drawn, dropping the indicators instead of the emulator. They were built during window construction, so a failure took the process down before there was a window to report it in — from a desktop launch, with no terminal, the app simply failed to start.
- Menu items for hardware the machine hasn’t got are greyed out: the disk items away from the +3, and the SD-card and sprite items away from the Next. Every item was live on every machine, so Media ▸ Disk ▸ Insert on a 48K opened a file dialog for a drive that was not there.
- A +3 floppy command carrying a head byte above 1 no longer kills the emulator with a two-sided disk in the drive, and FORMAT TRACK with no sectors ends in the result phase instead of leaving the controller reporting “send me more” until the motor is switched off.
- An SD image whose FAT geometry does not fit a 32-bit count is described rather than killing the emulator as the card is opened. The probe runs on every
--sdopen, so a 3 MiB crafted image took the emulator down before it booted. - A TZX carrying a Custom Info block — which is where archived tapes keep loading instructions and POKEs — loads instead of being refused as truncated.
- A TZX using Jump to Block, Call Sequence, Return from Sequence, Select Block or Emulation Info loads. All five were read as though they carried a body length they do not have, so the file was refused as truncated when nothing was wrong with it.
- Playback follows a jump and a call sequence rather than playing straight past them, so a tape whose blocks are not in playing order loads the blocks it names, in the order it names them.
- A TZX whose flow-control blocks form a cycle is refused instead of hanging the emulator on load. A jump to itself generates no tape edges, so the existing edge limit never saw it.
- A malformed TZX is refused with a message naming the field, instead of killing the emulator. Five separate faults, each reachable from a file under 35 bytes: a one-symbol data alphabet, a pilot-symbol count larger than the file, a zero loop-repeat count, a
used_bitsoutside 1-8, and nested loops whose product exhausts memory while every field is individually legal. --keys,--mouse,--pc-trace,--pc-trace-from/--pc-trace-toand--esxdos-tracetake effect in the windowed and--headlessruns, and the audio levels, mouse and joystick settings take effect in a--screenshotcapture. Each was accepted and silently ignored on the paths that did not apply it — a--headless --keysrun exited having pressed nothing.- Switching machines keeps the loaded symbol file, instead of leaving
bt(), the disassembler’s labels and every symbol lookup empty for the rest of the session with no diagnostic. - The Scanlines preference is kept. Choosing one on a machine with no scandoubler was undone within a frame and saved as Off on exit; the setting now stands until a machine that drives the register overrides it, and is restored at startup like the other view settings.
--esp bridgesurvives a machine switch. Switching machines left a thread spinning a core at 100% and holding the port, so the ESP was dead for the rest of the session.- Media ▸ Disk ▸ Write Protect A now protects the disk. The menu item ticked and did nothing, so a guest write still reached the image; +3DOS is also told the tab is set, rather than only being refused when it tries.
- Layer 2 blanks when pointed past addressable memory, as the hardware does, instead of reading somewhere else — or, on a 2 MB machine, taking the emulator down.
- A compressed SD-card image is bounded as it decompresses instead of trusting the size it declares about itself. A small crafted archive could claim a huge expansion and take the emulator down with it.
- Memory above
$BFFFis contended on the 128K, +2A and +3, where it previously ran untimed however the RAM was paged. Any effect written against a contended top bank was out of time. - The +2A/+3 contends the banks its ASIC contends (4–7) and stalls by the one T-state it stalls, rather than borrowing the 128K’s odd-bank rule and six-T-state ramp.
- The floating bus on a 128K, +2A or +3 reads the screen actually being displayed. With the shadow screen selected it returned the other bank’s bytes, so bus-synchronisation tricks read the wrong data.
- The 16K, 48K, 128K and +3 no longer drop a frame interrupt when a long instruction steps over the point it fires at. The loss was intermittent and alignment-dependent, showing up as stuttering music and slipping frame counters.
- Resetting a 128K, +2A or +3 silences the AY. A note playing when the reset arrived carried on sounding through the boot screen.
- A CTC interrupt-vector write is taken only from channel 0, as the hardware wires it. A vector-shaped word sent to any other channel silently moved where all four channels’ interrupts vectored to.
- DeZog’s sprite view shows the right sprite. Every sprite but the first reported a mix of the previous sprite’s padding and its own attributes, because the reply was read with the packed record length where the store is 8 bytes apart. Asking for a sprite past the end now pads the reply instead of killing the emulator.
- A breakpoint condition cut short after
nr[ornrraw[is rejected instead of killing the emulator — including one arriving from DeZog, where a parse failure is meant to be caught and downgraded. - Selecting a floppy drive the machine does not have reports not-ready instead of killing the emulator. A +3 program addressing drive 2 or 3 — which the command’s two-bit drive field allows — crashed it.
- A truncated or malformed
.z80snapshot is reported as an error instead of killing the emulator. A file that stated an extra-header length it did not contain, or whose compressed data ended part-way through a repeat, read past its own end. - Scripts and conditional breakpoints can read the interrupt mode and the interrupt/halt flip-flops by name —
reg("im"), andim,iff1,iff2,haltin a breakpoint condition.reg("im")previously returned the same value that means “called outside an event”. reg(),flag(),dm()andrompg()say when they are given a name they do not know, listing the ones they accept. A misspelt name still returns the documented sentinel, so it previously read as a measurement — a script cross-checking the interrupt mode reported the emulator wrong on the strength of one.- The Bizmuth icon in the About and update dialogs is drawn whole and at the right size, instead of showing an enlarged crop of its middle.
- The same icon is drawn at full size on a display scaled above 100%, where it appeared half size in the corner of its box.
- Reading a ULA+ palette entry back through
$FF3Breturns the entry that was written there. It previously returned whatever the nextreg palette pointer happened to address, with red and green swapped. - The palette index register (
$40) reports where the pointer has reached, so a program that writes entries and reads the index back is told the truth. It previously reported the value last written to it. - The mouse pointer no longer jumps and jitters on a display server that does not allow the pointer to be moved programmatically — Wayland, where the emulator now says so once and names
GDK_BACKEND=x11as the workaround. Mouse travel is limited to the window in that case. - The mouse resolution setting (
$0A) starts at the pass-through value the hardware starts at, instead of the doubler, so the pointer no longer moves at twice the speed a program asked for. - Writing 7 to the video-timing register (
$11) reads back 0, as the hardware reports: there are seven profiles and the eighth code selects the default. A program that wrote 7 was previously told it was on a profile the machine was not on. - Mouse DPI scaled every setting by half what the hardware does, so a program reading the mouse moved the pointer at half speed at each of the four settings.
- The UART select and framing registers (
$153B,$163B) read back what was written; both returned$FFas undecoded ports. A$153Bread reports the ESP/Pi select in bit 6 beside the prescaler. - The UART reports a break, a framing or parity error, and whether the byte being read arrived after one — three status bits that read 0 whatever happened, so a guest checking for a corrupted transfer found the check always passed.
- Reading the UART receive port with nothing buffered returns 0 instead of handing back the last byte received, over and over.
- The UART’s receive-overflow bit stays set until the status port is read, so a program can check once at the end of a transfer whether anything was lost. It previously reported only whether the buffer happened to be full at that instant.
- A full UART receive buffer keeps what it holds and discards the arriving byte; the arriving byte used to overwrite the oldest and drop the other 511.
- The UART’s near-full flag stays set when the receive buffer is completely full, rather than reading empty at the one point it matters most.
- The UART powers up at 115200 baud with 8 data bits, no parity and one stop bit, as the hardware does. Both reset values were 0 — a zero baud divisor and a five-bit frame — so a program relying on them, as it is entitled to, was misconfigured from the start.
- The UART buffers transmitted bytes in its 64-byte transmit FIFO, and reports tx-full and tx-empty from it rather than as fixed values. Asserting break now holds tx-empty low as the hardware does, so the two bits are no longer complements.
- Clearing the UART FIFOs (
$163Bbit 7) clears the transmit side as well as the receive side; one reset line drives both.
Changed
- Breaking —
--wavand--wav-sourceswrite WAVE files instead of headerless raw floats, and the per-source files are nowbeeper.wav/ay.wav/dac.wav. Anything reading them as baref32needs a WAVE reader; the rate now travels with the data, which it did not before, and it differs between a headless capture and a windowed one. - Breaking — save-state format is now v12, and earlier states no longer load. A mismatched state is rejected with an older/newer message rather than mis-loaded.
- Load Symbols… describes what it accepts — an SLD or a symbol export — where it previously named one assembler and a file extension the loader does not read.
- The UART transmits at the baud rate it is configured for, rather than instantly. A program that writes faster than the wire carries now sees tx-full and has to wait, as it does on hardware, so throughput figures and request-response timings are achievable ones.
- Mouse motion reaches the guest at the PS/2 rate of 80 packets a second rather than one lump a frame, so a program polling several times a frame — on a CTC timer, say — sees the movement spread across its polls instead of arriving all at once.
- Mouse motion comes from the compositor rather than from where the pointer has moved to, so travel is no longer limited by the window — the emulated pointer keeps going as long as your hand does. Where the compositor cannot supply it, the input device is read instead, which needs membership of the
inputgroup. Linux only. - While the mouse is captured under Wayland the cursor is now held in place and hidden, instead of wandering out of the window and clicking whatever is behind it.
[1.0.0-beta1] - 2026-08-23
Added
--loadstarts a program from a cached post-boot state instead of booting NextZXOS first — ScrollNutter runs in 1.3 s rather than 3.3 s. The state is captured on the first slow load and keyed to your card, boot ROM and machine;--no-boot-cacheforces the boot.--config <path>uses a different config file for both reading and writing, so a scratch or scripted run cannot disturb your saved settings.--no-rtcruns the Next as a board with no DS1307 fitted, which not every Next has; NextZXOS then leaves the clock off its boot menu. Persisted asemulation.rtcin the config.- Status bar shows the CPU clock beside the speed percentage (
100% 28MHz), with a CPU icon labelling the cell. --wav-sourcesworks headless, so per-source audio (beeper / AY / DAC, unity-gain mono, raw f32) can be captured without a window — the basis for an audio regression check, which the project had no equivalent of.- Headless/debugger key injection covers the whole ZX keyboard, with
+chords (key sym+pis the double-quote) — a script can now typeLOAD "", not just press menu keys. --tape-mode fast|realselects tape loading from the command line; the Tape menu’s choice now persists across tape loads instead of being reset by the next one.- TurboSound per-chip stereo enables: bits 6 and 5 of the
$FFFDchip-select write mute that AY’s left or right output, latched independently per chip. Previously decoded as part of the select and discarded, so a chip panned to one side played in both. - The ULA frame interrupt no longer masks off when the Z80 is not in IM2 under hardware mode — it is the one source that still pulses there.
- The IM2 daisy’s ULA slot can no longer miss the 32-pixel interrupt window when a single instruction steps clean over it.
Changed
- The CPU is now hg80, our own Z80 and Z80N core, replacing the LGPL-licensed
z80emu. hg80 is MIT OR Apache-2.0, so the separately shipped, user-replaceable CPU library —z80_ffi.dll,libz80_ffi.so,libz80_ffi.dylib— is gone, and with it the LGPL relinking obligation. Anyone who relinked that library against 0.9.5-beta5 or earlier cannot do so here. --nex/--snapshotare now--load(it takes.nex,.sna,.snxand.z80, so neither old name was right), and--dump-snapshotis--inspect-snapshot— it was one keystroke from--snapshot, which did the opposite. Every old spelling still works.--selectimplies--auto-keysrather than silently doing nothing on its own, and says so when--loadmakes it inapplicable.- Status bar fields adapt to the machine: the disk cell appears only on the +3, and the SD cells only on the Next, instead of every machine carrying cells that can never apply.
--nexinjects the program once NextZXOS settles rather than after a fixed 1300 frames — about 10 s instead of 26 on a card that boots to a quiet menu. A card that never goes idle (blinking cursor, on-screen clock) falls back to the old wait and now warns that it did.- Breaking — save-state format is now v8, and states written by 0.9.5-beta5 or earlier no longer load. A mismatched state is rejected with an older/newer message rather than mis-loaded.
--loadstateno longer changes which hardware you are running: a state captured on a board with no RTC fitted left the clock unfitted afterwards, even on a machine that has one. Board fitment now follows the run, like the--no-mem-waitand--no-im2-daisytoggles beside it.
Fixed
- Video-timing changes take effect at the frame boundary as the hardware does, not on the write: flipping
$03machine timing or$0550/60 Hz mid-frame no longer alters that frame’s length.$05and$09report the pending value until it lands. --keysaccepts a+-joined chord such ascaps+0, and names any token it cannot use instead of dropping it silently. A missing colon, a non-numeric frame or an unknown key each left a headless run exiting 0 having pressed nothing.- A
HALTis woken by a pending interrupt in IM2 hardware mode; it previously waited forever on an interrupt that could never be asserted. - Nextreg
$C0bits 2-1 report the CPU’s interrupt mode, which is the only way a program can ask what mode it is in — the Z80 has no instruction for it. They read 0 always, so a dot command saving and restoring the mode would hand NextZXOS back in IM 0 instead of IM 1. --sd-rono longer turns the saved card read-only for good: one run with the flag made every later launch discard guest writes, so saves silently stopped persisting. Toggling read-only during the run still persists as before.--helpno longer advertises defaults the program doesn’t use:--machineand--debug-protocolclaimed48kandbothwhile a fresh install actually runs the Next with ADP, and--roms-dirclaimed “nearest ancestorroms/” rather than~/Documents/Bizmuth.--helpstates, for every flag backed by the config, whether it persists or applies to the run only.- Nextreg
$68bit 4 cancels the extended keys’ contribution to the$FEkeyboard matrix, as the hardware does, instead of blanking the$B0/$B1readback — software that set it lost the extended keys entirely and still saw the composite it was suppressing. - CTC control-word bit 3 holds a timer channel until a trigger edge arrives instead of starting it the moment the time constant is written, so a channel armed to wait no longer fires early.
- CTC control-word bit 4 selects the trigger edge, and a control word that changes it generates an edge — starting a waiting timer or decrementing a counter-mode channel.
- The CTC channel state machine models the hardware’s five states, so a soft reset written while a channel awaits its time constant is consumed as the constant, and a channel put into the hard reset state stays there until a control word promising a time constant arrives.
- CTC channel 0’s trigger is channel 3’s zero-cross: the cascade is a ring, and channel 0 previously received no trigger at all.
- Tilemap 512-tile mode applies in text mode, so glyph indices above 255 select the right glyph; previously the index stayed 8 bits there and only 256 were reachable.
- A tile in 512-tile mode renders below the ULA unless
$6Bbit0 is set, matching the hardware; previously 512-tile mode suppressed the below-ULA rule instead of forcing it. --esxdos-tracenames the esxDOS function codes from the NextZXOS API documentation: six were wrong ($89,$8B,$90,$91,$92,$94) and eight absent ($88,$8C,$93,$A2,$A5,$A6,$B0,$B1).--esxdos-tracereports a successfulM_P3DOSasokrather thanERR:$94dispatches a +3DOS/IDEDOS/NextZXOS call, where carry reset — not set — means an error.- The CPU’s actual clock is modelled separately from the programmed one: nextreg
$07read-back reports the real speed in bits[5:4], and enabling the expansion bus pins the CPU to 3.5 MHz as the hardware does, instead of the two being assumed identical. --sd/--sd0/--sd1attach a card for that run only rather than overwriting the saved card path, so a run against a temporary image no longer leaves every later launch booting from it. Cards chosen through Preferences, the Media menu or first-run setup persist as before.- Fast tape loading works again, on every Spectrum:
--tapewas ignored on the 128K, loading a tape forced real-time mode, and the ROM trap was gated on the 48K ROM being paged. - A failed File ▸ Open now says so in a dialog instead of only in the terminal, and a successful one is logged; “nothing happened” was indistinguishable from a silent error.
- Interrupts in IM2-hardware mode (nextreg
$C0bit 0) are now a level held until the CPU acknowledges, not a 32-cycle pulse; a program whoseDIwindow outlasted the pulse silently lost the interrupt, which ran Captain Squarejaw’s game logic at a third of its rate. - The IM2 daisy is clocked once per CPU T-state rather than once per machine cycle; it could previously stick in service and mask every lower priority, killing TX-1696’s streaming audio.
- An interrupt is no longer delivered twice when the CPU acknowledges it in the clock before the daisy registers the request; the duplicate put ~4% jitter into TX-1696’s 16 kHz DAC stream and made it audibly choppy.
[0.9.5-beta5] - 2026-07-28
Added
- SD-card activity indicator — status-bar icon per Next slot: empty / read / write / unsaved-changes; tooltip shows file name + RO/RW mode.
- Corrupt-card warning — a corrupt/unformatted Next SD card raises a dialog (+ red status icon) instead of a blank “Error Mounting” guest screen.
- Scan / Fix SD Card — Media ▸ SD0/SD1 ▸ Scan / Fix…: FAT consistency check, with reformat-to-fix for an unreadable card.
Fixed
- Z80N flag accuracy:
ADD HL/DE/BC,Anow clear the carry andLDWScomputes it, matching the FPGA — programs branching on the carry after these no longer take the wrong path. - LGPL corresponding-source artifacts regenerated — the published
z80emupatch andz80-ffibridge source had drifted from the shipping library (missingZ80::set_haltand thez80ffi_imexport); both now reproduce it, verified by rebuilding the library from them alone. - First-run setup no longer recurs each launch — onboarding’s installed SD path is now persisted to the config.
- DZRP debugging fixed — the previous implementation was non-conformant and could not connect; DeZog now attaches, reads registers and memory, and stops at breakpoints.
--nexwas silently ignored outside--screenshot; the windowed and--headlesspaths now load it too.- IM2 daisy controller was gated on a stale interrupt mode in default (dynamic-link) builds — the bus mirrored
Cpu::inner()’s mode, so a program’sIM nwent unseen; a cheapim()read was added to the CPU library and the bus now mirrors the live mode. - Debugger reported stale (≈reset) CPU registers while the machine was running, in default (dynamic-link) builds only —
Cpu::inner()never pulled from the dlopen’dz80-ffilibrary; ADPreg/state/get cpu.*and Rhaireg()now read live state.
Changed
- Emulation is substantially faster — measured end to end on two machines (AMD x86_64 / Apple M4 Pro): TX-1696 +163% / +112%, NXModPlayer +111% / +81%, scrollnutter +92% / +66%. Interrupt-heavy and sprite-heavy programs gain most; output is byte-identical in every case.
- Save states now use the
.bzstextension (was.azst); the file format is unchanged, so existing states load under either name.
[0.9.4-beta4] - 2026-07-23
Added
- Save State / Load State — the File menu now saves the full machine state to a file and restores it later (the
--savestate/--loadstatecapability, now available interactively). - Re-runnable setup — Help ▸ Run Setup… (re-)downloads or locates the ROMs and Next SD card at any time, not just on first launch.
Changed
- The executable is now named
bizmuth(wasazimuth), matching the app name — so the Linux command and the application-menu title read “Bizmuth” consistently. - The Z80/Z80N CPU core now ships as a separate, replaceable library installed alongside Bizmuth (
z80_ffi.dllon Windows,libz80_ffi.soon Linux,libz80_ffi.dylibon macOS) instead of being compiled in — so the open-source (LGPL) CPU core can be swapped for your own build. If that library is missing from the install, Bizmuth will not start. - Faster interrupt-heavy programs — the IM2 daisy-chain interrupt controller no longer re-evaluates every peripheral on every bus cycle, giving roughly a 20% speed-up on interrupt-driven programs such as NXModPlayer and TX-1696 (output is byte-identical).
- Calmer idle screen — the no-machine screen is now a softer “No Input” backdrop with a bottom-left caption (was a bright “No ROMs!” test pattern), shown from launch until a machine boots so the window is never a plain black rectangle.
Fixed
- No more silent black screen when a Next can’t boot — a Next needs a bootable SD card in slot 0; if it’s missing or misconfigured (or ROMs are absent), Bizmuth now shows a clear dialog with Download / Preferences… / Quit and boots in place once you fix it, instead of a blank or near-blank screen. Config paths (ROM folder, SD images) are now stored and resolved as absolute, so launching from Finder or a different folder no longer fails to find them. A missing or unreadable boot ROM surfaces an error rather than a black screen.
- Menu tick marks stay correct — the Disk Write-Protect, Sprite RAM, Capture Mouse and SD Read-Only menu items now keep their check marks in sync with the actual state (previously only some menus reconciled, so these could show the wrong state after a change made outside the menu).
- Save-state restore fidelity — restoring a state while a program was running no longer leaves the machine slow or flickery: the CPU-speed, memory-wait and IM2-daisy settings, and any in-flight DMA→DAC audio stream, are now carried across the load. (The save-state format is now v2; states written by earlier builds can no longer be loaded.)
- Audio on high-rate USB DACs — the audio buffer is now sized by time rather than a fixed sample count, so it no longer under-runs (crackle/dropouts under load) on output devices running at high sample rates (96/192/384 kHz).
- Windows: no longer opens a console window on launch (runs as a GUI app); logs still print when started from a terminal.
[0.9.3-beta3] - 2026-07-20
Added
- In-app update check — on startup Bizmuth checks for a newer release and shows a notification (with a Preferences toggle to turn it off). The About box and
--versionnow report the exact build version, date and commit. - ADP debugger: page-qualified breakpoints (
page:off) for debugging banked memory, plus a headless server mode and apausestop-event.
Changed
- Reworked the About box into a custom dialog (logo, tagline, version, and a Check-for-updates link).
[0.9.1-beta2] - 2026-07-17
Added
- Emulates the 16K Spectrum (Machine menu /
--machine 16k). - ADP debugger: asynchronous
*stoppedevents (breakpoint hits reported without polling),mem write(poke), and step-over (next); command paths are now quote-aware.
Changed
- Every distribution now bundles a
THIRD-PARTY-LICENSESfile.
[0.9.0-beta1] - 2026-07-15
First public beta — a ZX Spectrum Next emulator built for developers.
Added
- Emulates the 48K, 128K, +2A and +3 Spectrums, and the full ZX Spectrum Next core (Z80N, Layer 2, sprites, tilemap, copper, DMA, CTC, audio) — verified against the FPGA.
- Source-level debugging: the ADP protocol (TCP :11001), DZRP for DeZog, a GUI debugger window, conditional breakpoints, memory watchpoints and Rhai event scripting.
- Loads
.nex,.sna/.snx/.z80snapshots,.tap/.tzxtapes and.dskdisks; NextZXOS SD-card images; full save-states; headless capture and scripting. - First-run setup assistant (fetches the ROM pack + SD image); keyboard, Kempston mouse and gamepad input.
- macOS (signed + notarised
.dmg), Windows (installer + portable zip) and Linux (AppImage /.deb/ Arch package).