Skip to content

Merge Fuse 1.9.1 - #65

Merged
desertkun merged 148 commits into
mainfrom
merge/fuse-1.9.1
Aug 5, 2026
Merged

Merge Fuse 1.9.1#65
desertkun merged 148 commits into
mainfrom
merge/fuse-1.9.1

Conversation

@morozov

@morozov morozov commented Aug 5, 2026

Copy link
Copy Markdown
Member

Fuse 1.9.0 and 1.9.1 have been released since the last merge (source).

bertogg and others added 30 commits May 13, 2026 17:06
Fuse supports both SDL1 and SDL2, and this library can be
independently enabled for the UI, audio and joystick support.

The problem is that for the UI and audio we have separate sdl1 and
sdl2 options in configure.ac. This is confusing and error prone
because we should be using the same SDL version for everything.

This patch leaves one single SDL UI option (--with-sdl) and one single
SDL audio driver (--with-audio-driver=sdl).

Fuse tries to use SDL2 if it's available, with SDL1 as a fallback.
This can be overridden with the existing --disable-sdl2 option.

Changes after this patch:

- $use_sdl1 and $use_sdl2 are replace by a single $use_sdl variable
  that can be set to '1', '2' or 'no'.
- $sdl1_available and $sdl2_available are also replaced with
  $sdl_available using the same logic.
- The --with-sdl2 option is removed.
- The sdl2 audio driver is removed.

The $UI variable still has separate sdl and sdl2 values because it
refers to the two different implementations under the ui/ directory.
This file must be generated when Fuse is actually compiled by the
final user or distributor with the GTK UI enabled.

The version generated during 'make dist' depends on whatever values
the local config.h file has at that moment. The result can easily be
wrong and should not be put into release tarballs.
The error can be seen with the following command:

   LC_ALL=C.UTF-8 MANROFFSEQ='' MANWIDTH=80 \
      man --warnings -E UTF-8 -l -Tutf8 -Z man/fuse.1 > /dev/null
Handle partial successful returns from snd_pcm_writei() correctly in
the ALSA backend.

The old code reduced the remaining frame count after a short write but
retried from the start of the same sample buffer. This could cause
audio corruption by repeating already-written frames and skipping later
ones.

Advance the sample pointer by the number of frames written, multiplied
by the channel count, before retrying the remaining frames.
autoconf 2.71 requires AC_PROG_LEX to be called with either 'yywrap'
or 'noyywrap'; calling it without an argument now emits:

  configure.ac:82: warning: AC_PROG_LEX without either yywrap or
  noyywrap is obsolete

The debugger's commandl.l scanner uses a single static command buffer
and never needs to switch input files, so yywrap() trivially returned 1
(no more input). Replace that idiom with the modern '%option noyywrap'
flex directive, which generates the same behaviour directly and does
not require an external yywrap symbol.
It fails every night and we are not using releases in the Fuse
GitHub mirror account yet.
Update the vendored pkg.m4 autoconf helper from serial 1 (pkg-config
0.24) to serial 12 (pkg-config-0.29.2).

The new version adds PKG_PREREQ (version guard for the macros
themselves), PKG_CHECK_MODULES_STATIC, PKG_WITH_MODULES,
PKG_HAVE_WITH_MODULES, and PKG_HAVE_DEFINE_WITH_MODULES, and improves
internal diagnostics and error messages. All existing macros used by
configure.ac (PKG_PROG_PKG_CONFIG, PKG_CHECK_MODULES, PKG_CHECK_VAR,
PKG_INSTALLDIR, PKG_NOARCH_INSTALLDIR) remain fully compatible.
libjsw is unmaintained and no longer distributed in Linux
distributions (except maybe Gentoo). Use SDL to handle joysticks
with all UIs.
When GDK_HINT_MAX_SIZE is ignored by a window manager while tiling or
maximizing the window, the UI can break if the computed size exceeds
MAX_SCALE, giving us a blank window and GTK warnings like these:

 drawing failure for widget 'GtkWindow': invalid value for stride
 drawing failure for widget 'GtkDrawingArea': invalid value for stride
 drawing failure for widget 'GtkBox': invalid value for stride

This patch limits the size to MAX_SCALE and centres the surface in the
drawing area.
Use SDL keysyms rather than scancodes for SDL2 Spectrum key events.

This makes SDL2 keyboard input follow the host keyboard layout, so
non-QWERTY layouts such as Dvorak behave more like the old SDL1 UI on
Unix-like systems. UI/native key handling is unchanged (thanks, Phil
Reynolds).
The autotools variable EXEEXT is usually empty on Unix and '.exe' on
Windows.

But on Unix it can also be used to change the name of the executable,
for example 'make EXEEXT=-sdl' to generate a 'fuse-sdl' binary.

With this change the Exec entry in the desktop file reflects the
actual name of the Fuse binary.
When the tape is asked to stop (STOP, STOP48, or end of tape) the edge
for that final event has not been played yet. tape_next_edge() stops
the tape and returns immediately, so the embedded pause at the end of
the block is ignored.

Some loaders need that final pause, so defer the stop instead and
carry it out on the next tape event.

This makes all TZX versions of 1942 work with --detect-loader, and
Chuckie Egg 2 with --detect-loader --no-accelerate-loader.

(fixes bugs #377 and #480)
Fuse only allows the GTK window to be resized in discrete steps, so
only window sizes that are a multiple of the Spectrum screen are
allowed.

This generally works on X11 (with certain exceptions, like tiling
window managers and maximized windows). However, Wayland ignores the
resize increment hint so windows can be resized freely. Fuse does not
expect this, which results in lots of display artifacts (see also
commit 70456ce for a related bug).

This patch fixes those artifacts by filling the drawing area with
black and painting the contents on top.

Additionally, when the window is manually resized (which triggers a
GTK configure_event) and a scaler is selected, activate it without
trying to force a window resize.
Fuse only repaints the parts of the screen that changed each frame and
relies on the rest staying intact.

uidisplay_frame_end() calls gdk_window_process_updates() to flush the
queued partial redraws immediately. That works on X11, but on Wayland
this produces artifacts, so drop the call (which has been deprecated
for a decade anyway) and let GTK take care of it.
The Wayland backend is now usable enough so there's no need to
override the GDK defaults.

(fixes bug #367)
(matja and Fredrick Meunier)

Filter code is derived from snes_ntsc (by Shay Green).
Also, improve Win32 graphics update performance by writing directly into
window DIB (Fredrick Meunier).
Add 16 tests covering the remaining ED-prefix instructions that were
not yet tested: LD R,A / LD A,R (complementary to existing LD I,A /
LD A,I), RRD / RLD, and all compare/IO block instructions
(CPI/CPD/CPIR/CPDR, INI/IND/INIR/INDR, OUTI/OUTD/OTIR/OTDR).

The opcodes are verified against the disassemble_ed() lookup tables
(opcode_01xxx111 and opcode_101xxxxx) in disassemble.c.
Add 26 new tests in three groups:

1. CB-prefix rotations/shifts (tests 72-80): the rotate and shift
   instructions (RLC/RRC/RL/RR/SLA/SRA/SLL/SRL) had zero test coverage.
   Adds one test per operation using register B, plus RLC (HL) to
   exercise the indirect (HL) addressing path in source_reg().

2. SBC HL,rr / ADC HL,rr — all register pairs (tests 81-86): previously
   only BC was tested. Adds DE, HL, and SP variants to give full
   coverage of the reg_pair() lookup used by cases 0x02/0x0a in
   disassemble_ed().

3. LD (nn),rr / LD rr,(nn) — all register pairs (tests 87-92):
   previously only BC was tested. Adds DE, HL, and SP variants for
   cases 0x03/0x0b in disassemble_ed().

All opcodes verified against the lookup tables in disassemble_cb()
and disassemble_ed().
Add 36 tests (93-128) covering IX and IY instruction families
that were previously untested:

- ADD IX/IY,rr for DE, IX/IY, and SP operands (completing the
  ADD IX/IY family; BC was already test3/test7)
- LD IX/IY,nn (16-bit immediate load)
- INC/DEC IX/IY
- LD (nn),IX/IY and LD IX/IY,(nn) (indirect 16-bit loads)
- PUSH/POP IX/IY
- JP (IX/IY), EX (SP),IX/IY, LD SP,IX/IY
- Undocumented IXh, IXl, IYh, IYl register forms (LD A,IXh etc)
- LD r,(IX/IY+d) and LD (IX/IY+d),r
- LD (IX/IY+d),n
fmeunier and others added 28 commits July 15, 2026 20:59
scaler_for_size() was introduced in the scaler-selection merge but had
no unit test coverage.  Add scaler_for_size_test() to unittests.c
covering:

- all four size positions within the Normal family
- lookup from a non-1x family member
- size clamping (< 1 → 1, > 4 → 4)
- scaler not in any family (SCALER_DOTMATRIX) → returned unchanged
- incomplete family registration → scaler returned unchanged
- TV family duplicate 1x/2x slot (TV2X appears in both positions)
- Timex family (SCALER_NORMAL at 2x position; requesting 1x → SCALER_HALF)
Add five deparse tests that exercise the bracketing logic for
comparison and equality operators:

- deparse-eq-non-assoc: 3 == (4 == 5) — right '==' at equal
  EQUALITY precedence: is_non_associative triggers brackets
- deparse-lt-non-assoc-right: 3 < (4 < 5) — right '<' at equal
  COMPARISON precedence: brackets
- deparse-lt-non-assoc-left: (3 < 4) < 5 — left '<' at equal
  COMPARISON precedence: outer op non-associative triggers brackets
- deparse-lt-inside-eq: (3 < 4) == 5 — COMPARISON child of EQUALITY
  parent: child has higher precedence, no brackets needed
- deparse-eq-inside-lt: 3 < (4 == 5) — EQUALITY child of COMPARISON
  parent: child has lower precedence, brackets required

The last two tests document the cross-precedence asymmetry: equality
(lower precedence) inside a comparison (higher precedence) needs
brackets, but comparison inside equality does not.
In display_write_if_dirty_timex(), when in lores (non-hires) mode the
attribute byte for the current cell is already stored in data2 (it was
read from display_get_attr_byte at the top of the function). The
subsequent call to display_get_attr() read it a second time purely to
decompose it into ink and paper components.

Move the attr decomposition into each branch so that:
- lores path: calls display_parse_attr( data2, ... ) directly, avoiding
  the second display_get_attr_byte read.
- hires path: inlines the equivalent display_parse_attr(
  display_get_attr_byte( x, y ), ... ) since hires_get_attr() must
  still be consulted.

This also removes the now-unused display_get_attr() wrapper (forward
declaration and definition), reducing indirection.
The display_dirty_xtable2 and display_dirty_ytable2 static arrays
(each 768 libspectrum_word elements = 1536 bytes, 3072 bytes combined)
were used solely by display_dirty64() to map an attribute-byte offset
to (x, y) coordinates.  Because the ZX Spectrum attribute area is
laid out linearly (row-major, 32 columns per row), the mapping is
trivially expressed as integer arithmetic:

  x = idx & (DISPLAY_WIDTH_COLS - 1)     -- idx % 32
  y = (idx / DISPLAY_WIDTH_COLS) * 8     -- row * 8

where idx = offset - DISPLAY_HEIGHT * DISPLAY_WIDTH_COLS.

GCC emits 'and' and 'sar'/'shl' for these, with no division instruction.

Benefits:
- Removes 3072 bytes of static data.
- Eliminates two data-cache lines from display_init().
- Makes the intent of the coordinate calculation self-documenting.
- Removes the loop that populated the tables from display_init().
…cal operator precedence

- debugger/event.c: add const to event_matches() first parameter; use pointer
  into the GArray instead of copying the struct in debugger_event_is_registered()
  and debugger_event().  Avoids a copy-on-every-iteration in lookup loops.
- debugger/system_variable.c: add const to system_variable_matches() first
  parameter; use a const pointer in find_system_variable() and copy only when
  the entry is found and the caller wants it.

Add five deparse tests to debugger_expression_unittest() covering precedence
interactions that were not previously exercised:
- deparse-and-inside-or: '||' with '&&' right child — no brackets needed
  because '&&' has higher precedence.
- deparse-or-inside-and: '&&' with '||' left child — brackets required
  because '||' has lower precedence than '&&'.
- deparse-cmp-inside-and: comparison operators inside '&&' — no brackets
  since comparison has higher precedence than logical AND.
- deparse-and-inside-cmp: '&&' as right operand of '<' — brackets required
  since '&&' has lower precedence than '<'.
- deparse-mul-mod-non-assoc-right: '*' parent with '%' right child — brackets
  required because '%' is non-associative even at the same precedence level as '*'.
The AY noise RNG update in sound_ay_overlay() used two conditional
branches whose outcomes are pseudo-random:

  if( ( rng & 1 ) ^ ( ( rng & 2 ) ? 1 : 0 ) )
  if( rng & 1 ) {
    rng ^= 0x24000;
  }
  rng >>= 1;

Because the LFSR output is by definition pseudo-random, a CPU branch
predictor cannot learn a pattern; both branches will mispredict ~50 %
of the time, each costing 10–20 cycles on modern microarchitectures.

Replace with equivalent branch-free expressions:

  noise_toggle ^= ( rng ^ ( rng >> 1 ) ) & 1;
  rng = ( rng >> 1 ) ^ ( 0x12000 & -( rng & 1 ) );

Correctness:
- Toggle condition: (rng & 1) XOR ((rng & 2) >> 1) == (rng ^ (rng>>1)) & 1
- LFSR step: XOR-then-shift is equivalent to shift-then-XOR-by-half the
  original mask (0x24000 >> 1 == 0x12000); the '-( rng & 1 )' idiom
  produces an all-ones mask when bit 0 is set and zero otherwise.

The inner loop runs at most once per outer tstate iteration (~109 200
iterations/second at 50 Hz).  Eliminating two branch mispredictions per
noise period crossing can save several thousand cycles per second on a
modern out-of-order CPU.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On POSIX systems (Linux, macOS), putenv(3) inserts the supplied
string pointer directly into the environment array rather than copying
it.  The previous code freed the buffer immediately after the
putenv() call, leaving the SDL_AUDIODRIVER environment variable
pointing at freed memory.  Any subsequent getenv("SDL_AUDIODRIVER")
or environ[] access (e.g. inside SDL_Init) would read deallocated
storage, causing undefined behaviour.

Fix by moving libspectrum_free() to the error path only, where
putenv() returned non-zero and the string was never inserted into the
environment.  On the success path the comment now documents that
the allocation is intentionally kept alive for the process lifetime,
consistent with POSIX requirements for putenv().
Replace the loop that sets each bit individually with a single arithmetic
expression: (1 << DISPLAY_SCREEN_WIDTH_COLS) - 1.  This is equivalent in
value and intent — a mask with one bit set for every column — but is more
concise and directly expresses the meaning without iteration.
…nstants

Replace bare hex literals 0x1800, 0x1b00, 0x3800, 0x3b00, and 6144 in
display.c with named constants derived from the existing DISPLAY_HEIGHT,
DISPLAY_HEIGHT_ROWS, and DISPLAY_WIDTH_COLS definitions.

Replace the local MONO_BITMAP_SIZE 6144 definition in screenshot.c with
DISPLAY_PIXEL_BYTES from display.h to eliminate the duplicate literal.

Also replace the 32 literals in the dirty table initialisation loop with
DISPLAY_WIDTH_COLS for consistency with the rest of the file.

No functional change.
The |& pipe operator (bash 4+) was the only reason macOS CI jobs had to
run 'brew install bash' before configuring Fuse.  Replace it with the
POSIX-compatible '2>&1 |' across all three build sub-workflows so that
the system bash (3.x on macOS) works without any Homebrew upgrade.

- build_macos_sub.yml: remove the 'brew install bash' block; replace
  '|& tee' with '2>&1 | tee' in the configure step
- build_linux_sub.yml: same '2>&1 | tee' substitution for consistency
- build_windows_sub.yml: same '2>&1 | tee' substitution for consistency

This reduces macOS CI time by eliminating one Homebrew installation.
errno is always a positive value; passing -errno to strerror() yields
"Unknown error" instead of the real OS error string. Fix both the
unix and linux implementations.
*var is initialised to 0 at the top of the function. The else branch
in the inner conditional therefore always sets *var to a value it
already holds, and can be dropped. Combine the two-level if into a
single boolean expression.
Replace 6912 / 0x1b00 / 6144 magic numbers with the newly introduced
DISPLAY_FILE_SIZE and DISPLAY_PIXEL_BYTES macros from display.h.

- screenshot.h: STANDARD_SCR_SIZE now defined in terms of
  DISPLAY_FILE_SIZE; add #include "display.h" so the constant
  resolves without requiring the includer to pull it in separately.
- slt.c: four occurrences of 6912 (SLT screen allocation / copy)
  become DISPLAY_FILE_SIZE; add #include "display.h".
- memory_pages.c: two 0x1b00 comparisons in the display-dirty helpers
  become DISPLAY_FILE_SIZE (display.h already included).
- unittests/displaytest.c: eight attribute-area index expressions
  using the literal 6144 become DISPLAY_PIXEL_BYTES (available via
  the existing machine.h → display.h include chain).

No behaviour change; the constants evaluate to the same values at
compile time.
scalerexpandtest.c previously only exercised the PAL-TV
expand_full_width_and_1_line expander.  Three other expander functions
are used by the remaining scalers but had no coverage:

- expand_1 (AdvMAME 2x/3x, HQ 2x/3x/4x): expand one pixel in every
  direction, then clip.  New test scalerexpandtest_advmame covers
  interior, top-left corner, and bottom-right corner cases.

- expand_sai (2xSaI, Super 2xSaI, SuperEagle): expand 2 pixels up/left
  and 1 pixel down/right, then clip.  New test scalerexpandtest_sai
  covers interior, top-left corner, and bottom-right corner cases.

- expand_dotmatrix (Dot Matrix): rounds the dirty rect to an even y
  co-ordinate.  New test scalerexpandtest_dotmatrix checks both the
  odd-y (rounds down, h increases) and even-y (no change) paths.

A fourth test scalerexpandtest_ntsctv verifies that the NTSC TV scalers
carry SCALER_FLAGS_FULL_REFRESH and return a NULL expander, confirming
that the dispatch code would never call a non-existent expander for them.
Three error-reporting paths passed wrong values to strerror():

1. putenv() failure (line 72): error holds putenv()'s return value
   (-1), but putenv() sets errno on failure.  The correct call is
   strerror( errno ).

2. sfifo_init() failure (line 142): sfifo uses the Linux-style
   convention of returning negative errno values (-EINVAL, -ENOMEM,
   ...).  strerror( error ) therefore received a large negative
   number and produced 'Unknown error -N'.  The correct call is
   strerror( -error ).

3. sfifo_write() failure (line 184): same negative-errno convention.
   strerror( i ) should be strerror( -i ).

All three fixes make the error messages shown to the user accurate
when SDL sound initialisation or output fails.
which supports BASIC tokens and symbols.
and update libspectrum dependency to make sure it is available.
Integrate the Fuse 1.9.1 release and its required libspectrum 1.6.3
update, together with the fork-side changes the upgrade makes
necessary: the FuseGenerator and FuseImporter submodule bumps, the
composite scaler source added to the Xcode project, the version and
release-suffix strings, and the Cocoa filter labels realigned with
the renumbered scaler enum.
@desertkun
desertkun merged commit a3728dd into main Aug 5, 2026
2 checks passed
@desertkun
desertkun deleted the merge/fuse-1.9.1 branch August 5, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants