From 2236600112f97ddd2fa637b6efe5ae206416e25b Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Wed, 13 May 2026 17:06:56 +0200 Subject: [PATCH 001/142] Rework the SDL detection in configure.ac 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. --- configure.ac | 114 ++++++++++++++++++--------------------------------- 1 file changed, 39 insertions(+), 75 deletions(-) diff --git a/configure.ac b/configure.ac index 9c0d6b6e..15ffa448 100644 --- a/configure.ac +++ b/configure.ac @@ -272,17 +272,7 @@ if test -z "$UI"; then fi fi -dnl Check if the user wants the SDL 2 UI -if test -z "$UI"; then - AC_MSG_CHECKING(whether SDL 2 UI requested) - AC_ARG_WITH(sdl2, - [ --with-sdl2 use SDL 2 for user interface], - if test "$withval" = no; then sdl2_ui=no; else sdl2_ui=yes; fi, - sdl2_ui=no) - AC_MSG_RESULT($sdl2_ui) -fi - -dnl Check if the user wants the SDL 1 UI +dnl Check if the user wants the SDL UI if test -z "$UI"; then AC_MSG_CHECKING(whether SDL UI requested) AC_ARG_WITH(sdl, @@ -295,54 +285,44 @@ fi dnl Check if the user prefers SDL 1 over SDL 2 AC_ARG_ENABLE(sdl2, AS_HELP_STRING([--disable-sdl2],[disable SDL 2 support]), - if test "$enableval" = yes; then sdl2=yes; else sdl2=no; fi, - sdl2=yes + if test "$enableval" = yes; then enable_sdl2=yes; else enable_sdl2=no; fi, + enable_sdl2=yes ) -if test "$sdl_ui" = yes -a "$sdl2_ui" = yes; then - AC_MSG_ERROR([please choose either --with-sdl or --with-sdl2, not both]) +dnl Look for SDL 2 +sdl_available=no +if test "$enable_sdl2" = yes; then + PKG_CHECK_MODULES([SDL2], [sdl2], [sdl_available=2], + [AC_MSG_WARN([SDL 2 not found, looking for SDL 1])]) fi -dnl Look for SDL 1 -SDL1_VERSION=1.2.4 -sdl1_available=no -AM_PATH_SDL($SDL1_VERSION, sdl1_available=yes, sdl1_available=no) -SDL1_CFLAGS="$SDL_CFLAGS" -SDL1_LIBS="$SDL_LIBS" -AC_SUBST(SDL1_CFLAGS) -AC_SUBST(SDL1_LIBS) - -dnl Look for SDL 2 -sdl2_available=no -if test "$sdl2" = yes; then - PKG_CHECK_MODULES([SDL2], [sdl2], [sdl2_available=yes], [ - AC_MSG_WARN([SDL 2 not found, looking for SDL 1]) - sdl2=no]) +dnl Look for SDL 1 if SDL 2 was not found or not requested +if test "$sdl_available" = no; then + SDL1_VERSION=1.2.4 + AM_PATH_SDL($SDL1_VERSION, sdl_available=1, sdl_available=no) + SDL1_CFLAGS="$SDL_CFLAGS" + SDL1_LIBS="$SDL_LIBS" + AC_SUBST(SDL1_CFLAGS) + AC_SUBST(SDL1_LIBS) fi -dnl Enable the SDL 2 UI if all conditions are met -if test "$sdl2_ui" = yes; then - if test "$sdl2_available" = yes; then +dnl Enable the SDL UI if all conditions are met. +use_sdl=no +if test "$sdl_ui" = yes; then + if test "$sdl_available" = 2; then AC_DEFINE([UI_SDL2], 1, [Defined if the SDL 2 UI is in use]) AC_DEFINE([USE_WIDGET], 1, [Defined if we're using a widget-based UI]) UI=sdl2 WIDGET=widget - use_sdl2=yes - else - AC_MSG_ERROR([SDL 2 support requested for the UI, but SDL 2 was not found]) - fi -fi - -dnl Enable the SDL 1 UI if all conditions are met -if test "$sdl_ui" = yes; then - if test "$sdl1_available" = yes; then + use_sdl=2 + elif test "$sdl_available" = 1; then AC_DEFINE([UI_SDL], 1, [Defined if the SDL UI in use]) AC_DEFINE([USE_WIDGET], 1, [Defined if we're using a widget-based UI]) UI=sdl WIDGET=widget - use_sdl1=yes + use_sdl=1 else - AC_MSG_ERROR([SDL version $SDL1_VERSION not found]) + AC_MSG_ERROR([SDL support requested for the UI, but SDL was not found]) fi fi @@ -564,20 +544,9 @@ AC_CHECK_HEADER( ) dnl SDL audio -if test "$sdl2_available" = "yes"; then - dnl Default to SDL2 audio if we're using the SDL2 UI - if test "$UI" = sdl2; then - audio_driver_list="sdl2 $audio_driver_list" - else - audio_driver_list="$audio_driver_list sdl2" - fi -fi - -if test "$sdl1_available" = "yes"; then - dnl Default to SDL audio if we're using the SDL 1 UI - if test "$UI" = sdl2; then - : - elif test "$UI" = sdl; then +if test "$sdl_available" != "no"; then + dnl Default to SDL audio if we're using the SDL UI + if test "$UI" = sdl || test "$UI" = sdl2; then audio_driver_list="sdl $audio_driver_list" else audio_driver_list="$audio_driver_list sdl" @@ -672,13 +641,13 @@ dnl dnl Add the necessary options for the selected audio driver dnl case $audio_driver in - sdl2) - SOUND_LIBADD='sound/sdl2sound.$(OBJEXT)' SOUND_LIBS='' sound_fifo=yes - use_sdl2=yes - ;; sdl) - SOUND_LIBADD='sound/sdlsound.$(OBJEXT)' SOUND_LIBS='' sound_fifo=yes - use_sdl1=yes + if test "$sdl_available" = 2; then + SOUND_LIBADD='sound/sdl2sound.$(OBJEXT)' SOUND_LIBS='' sound_fifo=yes + else + SOUND_LIBADD='sound/sdlsound.$(OBJEXT)' SOUND_LIBS='' sound_fifo=yes + fi + use_sdl=$sdl_available ;; directsound) SOUND_LIBADD='sound/dxsound.$(OBJEXT)' SOUND_LIBS='-ldsound -lole32 -ldxguid' @@ -774,12 +743,12 @@ if test "$stick" = yes; then if test "$libjsw" = yes; then AC_MSG_RESULT([no, use libjsw]) else - if test "$sdl1_available" = yes; then + if test "$sdl_available" != no; then AC_MSG_RESULT(yes) AC_DEFINE([USE_JOYSTICK], 1, [Defined if we're using hardware joysticks]) - use_sdl1=yes + use_sdl=$sdl_available else - AC_MSG_WARN([SDL version $SDL1_VERSION not found - joystick support disabled]) + AC_MSG_WARN([No suitable SDL version found - joystick support disabled]) stick=no fi fi @@ -966,8 +935,8 @@ AM_CONDITIONAL(COMPAT_WII, test "$COMPAT_OSNAME" = 'wii') AM_CONDITIONAL(COMPAT_WIN32, test "$COMPAT_OSNAME" = 'win32') dnl These tell the Makefile to use SDL 1 and SDL 2 flags during the build -AM_CONDITIONAL(USE_SDL1, test "$use_sdl1" = "yes") -AM_CONDITIONAL(USE_SDL2, test "$use_sdl2" = "yes") +AM_CONDITIONAL(USE_SDL1, test "$use_sdl" = "1") +AM_CONDITIONAL(USE_SDL2, test "$use_sdl" = "2") dnl Decide whether to install desktop and mime files AC_ARG_ENABLE(desktop-integration, @@ -1073,12 +1042,7 @@ echo "User interface: ${UI}" if test "${UI}" = "gtk3"; then echo "Using GTK 3: ${gtk}" fi -if test "${use_sdl1}" = "yes"; then - echo "Using SDL 1: yes" -fi -if test "${use_sdl2}" = "yes"; then - echo "Using SDL 2: yes" -fi +echo "Using SDL: ${use_sdl}" if test x"${gpm}" != "x"; then echo "libgpm support: ${gpm}" fi From 0851387f0effd1e2d54e1ef8c3bc25493ff25fbf Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sat, 16 May 2026 19:44:23 +0200 Subject: [PATCH 002/142] Don't ship ui/gtk3/menu_data.ui in release tarballs 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. --- ui/gtk3/Makefile.am | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/gtk3/Makefile.am b/ui/gtk3/Makefile.am index dd8f5d51..58670555 100644 --- a/ui/gtk3/Makefile.am +++ b/ui/gtk3/Makefile.am @@ -29,7 +29,6 @@ BUILT_SOURCES += $(ui_gtk_built) endif gtkui_resource_files = \ - ui/gtk3/menu_data.ui \ ui/gtk3/icons/disk_active.png \ ui/gtk3/icons/disk_inactive.png \ ui/gtk3/icons/mdr_active.png \ @@ -42,6 +41,9 @@ gtkui_resource_files = \ ui/gtk3/icons/tape_inactive.png \ ui/gtk3/icons/tape_marker.png +gtkui_built_resource_files = \ + ui/gtk3/menu_data.ui + CLEANFILES += $(ui_gtk_built) ui_gtk_files = \ @@ -105,8 +107,8 @@ ui/gtk3/options_internals.h: $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/gt @$(MKDIR_P) ui/gtk3 $(AM_V_GEN)$(PERL) $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/options.dat | $(PERL) -I$(srcdir)/perl $(srcdir)/ui/gtk3/options-header.pl - > $@.tmp && mv $@.tmp $@ -ui/gtk3/resources.c: ui/gtk3/resources.xml $(gtkui_resource_files) +ui/gtk3/resources.c: ui/gtk3/resources.xml $(gtkui_resource_files) $(gtkui_built_resource_files) $(AM_V_GEN)glib-compile-resources --sourcedir=$(srcdir) --sourcedir=$(builddir) --target=$@ --internal --generate-source --c-name=gtkui $< -ui/gtk3/resources.h: ui/gtk3/resources.xml $(gtkui_resource_files) +ui/gtk3/resources.h: ui/gtk3/resources.xml $(gtkui_resource_files) $(gtkui_built_resource_files) $(AM_V_GEN)glib-compile-resources --sourcedir=$(srcdir) --sourcedir=$(builddir) --target=$@ --internal --generate-header --c-name=gtkui $< From da89fc1f70973e93c6c049cfa0f99e297adeb327 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sat, 16 May 2026 19:57:03 +0200 Subject: [PATCH 003/142] Fix error in manpage 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 --- man/fuse.1 | 1 + 1 file changed, 1 insertion(+) diff --git a/man/fuse.1 b/man/fuse.1 index fbf101cc..7fe53845 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -1,3 +1,4 @@ +'\" t .\" -*- nroff -*- .\" .\" fuse.1: Fuse man page From 05a0535693a0a9f28280e9f7db3090859341ead1 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sat, 16 May 2026 20:10:33 +0200 Subject: [PATCH 004/142] desktop file: set the GTK category in GTK builds only https://specifications.freedesktop.org/menu/latest/category-registry.html --- data/Makefile.am | 8 +++++++- data/net.sourceforge.fuse_emulator.Fuse.desktop.in | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/data/Makefile.am b/data/Makefile.am index 7057b811..50debfb6 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -75,11 +75,17 @@ data/fuse.xml: $(srcdir)/data/fuse.xml.in fi; \ $(SED) -e "$$mime_zlib" -e "$$mime_bz2" < $(srcdir)/data/fuse.xml.in > $@ +if UI_GTK +EXTRA_DESKTOP_CATEGORIES = GTK; +endif + # Generate net.sourceforge.fuse_emulator.Fuse.desktop from fuse.xml to keep mime types list in sync data/net.sourceforge.fuse_emulator.Fuse.desktop: $(srcdir)/data/net.sourceforge.fuse_emulator.Fuse.desktop.in data/fuse.xml $(AM_V_GEN)$(MKDIR_P) data; \ list=`$(SED) -e '/.*/\1;/' -e t -e d < data/fuse.xml | tr -d '\n'` && \ - $(SED) -e "s|@MIME_TYPES@|$$list|" < $(srcdir)/data/net.sourceforge.fuse_emulator.Fuse.desktop.in > $@ + $(SED) -e "s|@MIME_TYPES@|$$list|" \ + -e 's|@EXTRA_DESKTOP_CATEGORIES@|$(EXTRA_DESKTOP_CATEGORIES)|' \ + < $(srcdir)/data/net.sourceforge.fuse_emulator.Fuse.desktop.in > $@ update-database-cache: if test -z "$(DESTDIR)"; then \ diff --git a/data/net.sourceforge.fuse_emulator.Fuse.desktop.in b/data/net.sourceforge.fuse_emulator.Fuse.desktop.in index ecd66540..5d358939 100644 --- a/data/net.sourceforge.fuse_emulator.Fuse.desktop.in +++ b/data/net.sourceforge.fuse_emulator.Fuse.desktop.in @@ -1,7 +1,7 @@ [Desktop Entry] Version=1.0 Type=Application -Categories=Game;Emulator;GTK; +Categories=Game;Emulator;@EXTRA_DESKTOP_CATEGORIES@ Name=Fuse GenericName=Spectrum Emulator Comment=Emulator of the 1980s ZX Spectrum home computer and its various clones From 48d1d2305b0cf67936c0e2c4ea09fb5c87b0aae5 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 17 May 2026 19:53:05 +1000 Subject: [PATCH 005/142] Correct Options level full screen toggle for SDL 2 (thanks, Blerkotron) --- .beads/issues.jsonl | 1 - menu_data.dat | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5851aa42..7eaf7787 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -46,4 +46,3 @@ {"_type":"issue","id":"fuse-qqg","title":"Final SDL2 parity cleanup","description":"Track remaining SDL2 backend parity gaps and cleanup once build/configure, minimal UI, SDL2 sound, display, fullscreen/window, keyboard, joystick, and mouse/grab work are all complete. Use this to capture residual polish rather than front-loading modernization.","acceptance_criteria":"Remaining SDL2 parity gaps are enumerated and cleaned up after the earlier milestones finish; no major known parity blocker remains for the SDL2 UI and SDL2 sound backends; renderer/texture modernization remains out of scope unless separately justified.","status":"closed","priority":3,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:53Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T00:55:53Z","closed_at":"2026-04-12T00:55:53Z","close_reason":"Cleaned up the remaining practical SDL2 parity gaps by restoring mouse grab across window recreation and allowing held-key repeat through the widget UI. No major known SDL2 parity blocker remains; SDL1-style fixed fullscreen mode selection remains out of scope for this cleanup.","dependencies":[{"issue_id":"fuse-qqg","depends_on_id":"fuse-4vn","type":"blocks","created_at":"2026-04-11T21:57:11Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-qqg","depends_on_id":"fuse-e8p","type":"blocks","created_at":"2026-04-11T21:57:12Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"fuse-e8p","title":"Mouse and grab behavior for SDL2 UI","description":"Add SDL2 mouse handling and grab/capture behavior only after the core SDL2 milestones are stable. Treat this as deferred follow-up work after build/configure, minimal UI, SDL2 sound, display cleanup, fullscreen/window management, and keyboard fidelity.","acceptance_criteria":"SDL2 mouse input and grab/capture behavior are implemented after the prerequisite SDL2 UI milestones are complete; behavior is usable without requiring a hybrid SDL1/SDL2 path or unrelated renderer modernization.","status":"closed","priority":3,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:47Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T00:44:36Z","closed_at":"2026-04-12T00:44:36Z","close_reason":"Implemented SDL2 mouse grab/release behavior with relative mode and window grab, added unit tests for mouse grab policy, fixed fullscreen status icon null-safety, and verified the runtime behavior locally with acceptable residual macOS cursor-hide quirks.","dependencies":[{"issue_id":"fuse-e8p","depends_on_id":"fuse-3tt","type":"blocks","created_at":"2026-04-11T21:57:10Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-e8p","depends_on_id":"fuse-l6g","type":"blocks","created_at":"2026-04-11T21:57:10Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"fuse-4vn","title":"Joystick support for SDL2 UI","description":"Add SDL2 joystick/game-controller support only after build/configure, minimal UI, SDL2 sound, display cleanup, fullscreen/window management, and keyboard fidelity are all in place. Keep this as a follow-up integration task rather than broadening earlier milestones.","acceptance_criteria":"SDL2 joystick support is implemented after the prerequisite SDL2 UI and input/display milestones are complete; it integrates cleanly with the native SDL2 backend without reopening earlier architectural decisions.","status":"closed","priority":3,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:40Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T23:41:02Z","closed_at":"2026-04-11T23:41:02Z","close_reason":"Added native SDL2 joystick support with button/axis/hat event handling, fixed configure-time SDL2 joystick enablement, added unit tests for joystick event translation, and verified behavior with a real joystick.","dependencies":[{"issue_id":"fuse-4vn","depends_on_id":"fuse-3tt","type":"blocks","created_at":"2026-04-11T21:57:09Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-4vn","depends_on_id":"fuse-l6g","type":"blocks","created_at":"2026-04-11T21:57:09Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"_type":"memory","key":"sdl2-renderer-backend-guidance","value":"Future SDL2 renderer/texture backend guidance: keep current surface/scaler parity backend as the stable path; if revisiting a renderer path, use it as a separate follow-up rather than replacing the parity backend wholesale. Practical lessons from historical SDL2 attempts and this branch: (1) renderer backbuffers must be explicitly cleared every frame before drawing because SDL render backbuffers are invalid after present; assume no preserved contents across frames. (2) Dirty-rect updates interact badly with non-integer SDL scaling/filtering and can leave interpolation artifacts at update borders; if using renderer scaling with non-integer factors, prefer full texture updates or be extremely conservative about dirty regions. (3) Relative mouse mode is the right SDL2 approach for grab/ungrab; keep emulator input on key events and treat any SDL_TEXTINPUT support as widget-text-entry-only, not Spectrum input. (4) Fixed software scalers and free SDL scaling become conceptually messy when mixed; for parity/default behavior preserve fixed Fuse scaler semantics, and if adding a freely resizeable SDL scaler mode, make it an explicit alternative mode rather than silently changing existing scaler behavior. (5) Avoid relying on WM-specific aspect-hint logic as a core behavior; it proved fragile and WM-dependent historically. (6) If using textures, RGB565 remains a sensible source format, but be careful about assumptions around filtering, clipping, and fullscreen border clearing. (7) If pursuing renderer fullscreen/window logic, test resize, maximized window, fullscreen transitions, and border clearing across multiple platforms/GPUs early, especially Intel/X11-style cases historically prone to stale backbuffer artifacts. (8) The current branch intentionally retained SDL1-style fixed fullscreen mode selection because it is a valued user feature; do not drop that behavior casually in a renderer redesign."} diff --git a/menu_data.dat b/menu_data.dat index 9f899ce8..eac0162c 100644 --- a/menu_data.dat +++ b/menu_data.dat @@ -131,7 +131,9 @@ Options/Select ROMs/Peripheral ROMs/uS_peech..., Item,, menu_options_selectroms_ Options/_Filter..., Item,,, menu_filter_detail -#if defined UI_SDL || defined UI_SDL2 +#ifdef UI_SDL +Options/F_ull screen, Item, F11 +#elif defined UI_SDL2 Options/F_ull screen, Item, F11 #endif From fe1d30390bca1a7c8b6016dbd176b37cecc40352 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 17 May 2026 20:25:17 +1000 Subject: [PATCH 006/142] Add release process documentation and distribute maintainer helpers --- .agents/skills/README.md | 16 ++ .agents/skills/update-changelog/SKILL.md | 109 +++++++++++ Makefile.am | 5 +- README | 2 + hacking/Makefile.am | 1 + update-release.py | 237 +++++++++++++++++++++++ 6 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/README.md create mode 100644 .agents/skills/update-changelog/SKILL.md create mode 100755 update-release.py diff --git a/.agents/skills/README.md b/.agents/skills/README.md new file mode 100644 index 00000000..fc4af284 --- /dev/null +++ b/.agents/skills/README.md @@ -0,0 +1,16 @@ +Repository skills +================= + +This directory contains optional maintainer automation helpers for +agent environments that support repository skills. + +These files are not required to build or use Fuse. They exist as +convenience helpers for contributor and release workflows. + +Currently included: + +* `update-changelog` - drafts a new top-of-file `ChangeLog` release + entry using the existing Fuse project format. + +If your agent environment does not support repository skills, follow the +normal documented maintainer process instead. diff --git a/.agents/skills/update-changelog/SKILL.md b/.agents/skills/update-changelog/SKILL.md new file mode 100644 index 00000000..2e739611 --- /dev/null +++ b/.agents/skills/update-changelog/SKILL.md @@ -0,0 +1,109 @@ +--- +name: update-changelog +description: Drafts a new top-of-file Fuse release entry in ChangeLog using unrecorded commits and the project’s existing grouped format. Drafts first, then waits for confirmation before editing ChangeLog. +--- + +# Update ChangeLog + +This is a project-specific skill for preparing a Fuse release ChangeLog entry. + +Use this skill when the user wants to update `ChangeLog` for a release. + +## Rules + +- Follow the existing style in `ChangeLog` exactly. +- Preserve all existing entries unchanged. +- Draft first. +- Do not edit `ChangeLog` until the user confirms. +- If the release version is not yet known, ask the user for it before drafting the new entry. +- Use the most recent unrecorded commit date for the new heading. +- Use Philip Kendall’s maintainer identity in the heading: + `YYYY-MM-DD Philip Kendall ` + +## Workflow + +Update the ChangeLog file in the current directory `ChangeLog` with any commits that are not yet recorded. + +1. Use `git log ChangeLog` to find the most recent modification git hash. +2. Run `git log --format="%H %ai %s" ..HEAD` to find unrecorded commits. +3. Run `git log ..HEAD` to inspect commit bodies when needed. +4. Filter out noise commits: merges, "revert accidental commit", "add ignore", "update for version", + "first updates for", "bring up to date", "tidy up commit text", and similar meta-commits that don't + describe user-visible changes. +5. For each remaining commit, determine its category according to the principles below. +6. Add a new date entry at the top of the `ChangeLog` after the first line of the file, using the most recent + unrecorded commit's date, with Philip Kendall's name/email as maintainer: + `YYYY-MM-DD Philip Kendall ` +7. Under that date, add a `* Fuse X.Y.Z released.` entry. +8. Under that date, add entries grouped by category, 8-space indented, matching the existing format. +9. Show the proposed new entry to the user and ask for confirmation. +10. Only after confirmation, write the updated `ChangeLog`. + +## ChangeLog grouping structure + +### Top-level structure per release + +Each release entry has: +1. Date line (`YYYY-MM-DD`) +2. Author line (`Philip Kendall` with email) +3. Release announcement (`* Fuse X.Y.Z released.`) +4. Categorized bullet groups (see below) +5. Catch-all footer (`* Various other minor bugfixes.` or similar) + +### Category groups + +The ChangeLog uses a fixed set of category headings, each as a bullet followed by a colon. Sub-items are indented +bullets under the heading: + +- `* New features:` User-facing additions: new hardware emulation, new file formats, new UI backends, new capabilities. +- `* Emulation core improvements:` Z80 accuracy, timing fixes, peripheral emulation, disk/tape handling, sound accuracy. +- `* Machine specific improvements:` Fixes scoped to a particular machine type (`+3`, Timex, Pentagon, etc.). +- `* UI improvements:` GTK/Win32/SDL/Xlib UI changes, dialogs, menus, display rendering, redraw fixes. +- `* Debugger improvements:` Breakpoints, disassembly, debugger variables, memory browser. +- `* Profiler improvements:` Code profiler fixes. +- `* Scaler improvements:` Graphics scaler additions/fixes. +- `* RZX improvements:` Recording/playback features. +- `* Deprecated features removed:` Removed UI backends, removed syntax, dropped dependencies. +- `* Miscellaneous improvements:` Build system, compiler warnings, docs, platform portability, memory leaks, anything that doesn't fit above. +- `* Testing improvements:` Unit tests, CI workflows, test coverage. +- `* Bugfixes:` / `* Minor bugfixes:` Small standalone fixes used in older releases. + +## Key patterns + +1. Not all categories appear in every release — only categories that have relevant changes are included. +2. Each bullet is a single sentence describing the change, often with: + - the what + - the impact + - credits in parentheses at the end: `(Author Name)` or `(thanks, Contributor) (Author Name)` + - bug references: `(fixes bug #NNN)` +3. Credits are consistent — the primary author of the commit is in the final parentheses. `thanks, X` acknowledges + upstream or community input. +4. Bullet hierarchy is two levels deep — category heading to specific changes. No deeper nesting. +5. Most releases end with `* Various other minor bugfixes.` as a catch-all for small fixes not worth itemizing. +6. Order is roughly consistent: New features, Emulation core, Machine specific, UI, Debugger, optional categories, + Deprecated, Miscellaneous, Testing, footer. +7. Indentation: 8 spaces for the category heading line, 10 spaces for sub-bullets, using spaces and not tabs. +8. Older releases use a simpler flat bullet list; follow the current grouped format used at the top of the file. + +## When generating entries + +- Classify each commit into one of the standard categories. +- Write a single-sentence bullet per change. +- Include impact or context where available, including bug numbers and affected games. +- Include credits in parentheses. +- Use the standard category ordering. +- Skip categories with no entries. +- Add `Various other minor bugfixes` as a footer if there are unclassified small fixes. + +## Drafting instructions + +When presenting the draft: + +- show only the proposed new ChangeLog entry +- do not edit the file yet +- ask the user to confirm or request changes + +When the user confirms: + +- insert the new entry at the top of `ChangeLog` after the first line +- do not alter older entries diff --git a/Makefile.am b/Makefile.am index adc1b00a..47b9510f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -154,7 +154,10 @@ EXTRA_DIST = AUTHORS \ menu_data.pl \ settings.dat \ settings.pl \ - settings-header.pl + settings-header.pl \ + update-release.py \ + .agents/skills/README.md \ + .agents/skills/update-changelog/SKILL.md CLEANFILES = options.h \ settings.c \ diff --git a/README b/README index dd2d5bae..678227c4 100644 --- a/README +++ b/README @@ -109,6 +109,8 @@ http://lists.sourceforge.net/lists/listinfo/fuse-emulator-devel and the Fuse project page on SourceForge, http://sourceforge.net/projects/fuse-emulator/ +Maintainer release notes are in `hacking/releasing.md'. + For Spectrum discussions not directly related to Fuse, visit either the Usenet newsgroup `comp.sys.sinclair' or the World of Spectrum forums . diff --git a/hacking/Makefile.am b/hacking/Makefile.am index ba50db4e..80737e29 100644 --- a/hacking/Makefile.am +++ b/hacking/Makefile.am @@ -27,6 +27,7 @@ EXTRA_DIST += \ hacking/implementation_notes.txt \ hacking/input.txt \ hacking/peripheral_tests.txt \ + hacking/releasing.md \ hacking/sound.txt \ hacking/spectranet.txt \ hacking/tc2048_tech_notes.txt \ diff --git a/update-release.py b/update-release.py new file mode 100755 index 00000000..0360b568 --- /dev/null +++ b/update-release.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +update-release.py — bump the release date (and optionally version) in +man pages, README, and configure.ac. + +Usage: + ./update-release.py --date "12th June, 2026" + ./update-release.py --date "12th June, 2026" --version "1.4.6" + ./update-release.py --date "12th June, 2026" --version "1.4.6.1" + ./update-release.py --date "12th June, 2026" --dry-run + +Only touches the .TH line in man/*.1, the title and last line of README, +and the version defines in configure.ac. Copyright lines are never modified. +""" + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +MAN_DIR = ROOT / "man" +README = ROOT / "README" +CONFIGURE_AC = ROOT / "configure.ac" + +# .TH line example: +# .TH tzxlist 1 "10th March, 2026" "Version 1.4.5" "Emulators" +TH_RE = re.compile( + r'^(\s*\.TH\s+\S+\s+\d+\s+)' # prefix up to date + r'"([^"]*)"' # old date in quotes + r'(\s+)' # whitespace before version + r'"([^"]*)"' # old version in quotes + r'(.*)', # rest of line + re.MULTILINE, +) + + +def update_man_page(path, new_date, new_version, dry_run): + """Return True if the file was (or would be) changed.""" + text = path.read_text() + new_text, count = TH_RE.subn( + lambda m: (m.group(1) + + f'"{new_date}"' + + m.group(3) + + (f'"{new_version}"' if new_version else f'"{m.group(4)}"') + + m.group(5)), + text, + ) + if count == 0: + print(f" skip {path.name} (no .TH match)") + return False + if dry_run: + print(f" would update {path.name}") + else: + path.write_text(new_text) + print(f" updated {path.name}") + return True + + +def update_readme(new_date, new_version, dry_run): + """Update README release metadata. + + Updates the title line version when requested and the last line date. + The last line is expected to be just the date, e.g. "10th March, 2026". + """ + if not README.exists(): + print(f" skip README (not found)") + return False + + text = README.read_text() + lines = text.splitlines(keepends=True) + changed = False + updates = [] + + if new_version: + title_pattern = re.compile( + r'^(The Free Unix Spectrum Emulator \(Fuse\) )\S+(\r?\n?)$' + ) + match = title_pattern.match(lines[0]) if lines else None + if not match: + print(f" skip README (title line doesn't match expected format)") + return False + new_title = f"{match.group(1)}{new_version}{match.group(2)}" + if lines[0] != new_title: + lines[0] = new_title + changed = True + updates.append(f"title -> {new_version}") + + # The last non-empty line should be the date + last_line = lines[-1].strip() + if not last_line: + print(f" skip README (last line is blank)") + return False + + # Only update if the last line looks like a date (no other content) + date_pattern = re.compile( + r'^\d+(?:st|nd|rd|th)\s+\w+,\s+\d{4}$' + ) + if not date_pattern.match(last_line): + print(f" skip README (last line '{last_line}' doesn't look like a date)") + return False + + new_last_line = new_date + "\n" + if lines[-1] != new_last_line: + lines[-1] = new_last_line + changed = True + updates.append(f"last line -> {new_date}") + + if not changed: + print(f" skip README (already up to date)") + return False + + new_text = "".join(lines) + detail = ", ".join(updates) + if dry_run: + print(f" would update README ({detail})") + else: + README.write_text(new_text) + print(f" updated README ({detail})") + return True + + +def parse_version(version_str): + """Parse a version string like '1.4.6' or '1.4.6.0' into components. + + Returns (major, minor, micro, nano) — nano defaults to 0. + """ + parts = version_str.split(".") + if len(parts) < 3 or len(parts) > 4: + print(f"error: version '{version_str}' must be X.Y.Z or X.Y.Z.N", + file=sys.stderr) + sys.exit(1) + major = parts[0] + minor = parts[1] + micro = parts[2] + nano = parts[3] if len(parts) == 4 else "0" + return major, minor, micro, nano + + +def update_configure_ac(version_str, dry_run): + """Update the version defines in configure.ac.""" + if not CONFIGURE_AC.exists(): + print(f" skip configure.ac (not found)") + return False + + major, minor, micro, nano = parse_version(version_str) + + text = CONFIGURE_AC.read_text() + + replacements = { + r'm4_define\(\[fuse_version\],\s*\[\S+\]\)': + f'm4_define([fuse_version], [{version_str}])', + r'm4_define\(\[fuse_major_version\],\s*\[\S+\]\)': + f'm4_define([fuse_major_version], [{major}])', + r'm4_define\(\[fuse_minor_version\],\s*\[\S+\]\)': + f'm4_define([fuse_minor_version], [{minor}])', + r'm4_define\(\[fuse_micro_version\],\s*\[\S+\]\)': + f'm4_define([fuse_micro_version], [{micro}])', + r'm4_define\(\[fuse_nano_version\],\s*\[\S+\]\)': + f'm4_define([fuse_nano_version], [{nano}])', + } + + changed = False + for pattern, replacement in replacements.items(): + new_text, count = re.subn(pattern, replacement, text) + if count: + changed = True + text = new_text + + if not changed: + print(f" skip configure.ac (no version defines found)") + return False + + if dry_run: + print(f" would update configure.ac (version -> {version_str})") + else: + CONFIGURE_AC.write_text(text) + print(f" updated configure.ac (version -> {version_str})") + return True + + +def main(): + parser = argparse.ArgumentParser( + description="Update release date (and optionally version) in man pages, README, and configure.ac.", + ) + parser.add_argument( + "--date", + required=True, + help='New release date string, e.g. "12th June, 2026"', + ) + parser.add_argument( + "--version", + default=None, + help='New version string, e.g. "1.4.6" or "1.4.6.1". If omitted, version is left unchanged.', + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would change without writing files.", + ) + args = parser.parse_args() + + if not MAN_DIR.is_dir(): + print(f"error: man directory not found at {MAN_DIR}", file=sys.stderr) + sys.exit(1) + + man_pages = sorted(MAN_DIR.glob("*.1")) + if not man_pages: + print(f"error: no *.1 files found in {MAN_DIR}", file=sys.stderr) + sys.exit(1) + + print(f"Scanning {len(man_pages)} man page(s), README, and configure.ac ...") + print(f" new date : {args.date}") + if args.version: + print(f" new version : {args.version}") + print() + + changed = 0 + for mp in man_pages: + if update_man_page(mp, args.date, args.version, args.dry_run): + changed += 1 + + if update_readme(args.date, args.version, args.dry_run): + changed += 1 + + if args.version and update_configure_ac(args.version, args.dry_run): + changed += 1 + + print() + if args.dry_run: + print(f"Dry run — {changed} file(s) would be updated.") + else: + print(f"Done — {changed} file(s) updated.") + + +if __name__ == "__main__": + main() From e6990dea40df51e13439c0733b7a40a942a16507 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 17 May 2026 20:29:22 +1000 Subject: [PATCH 007/142] Fix SDL2 Timex tape icon placement --- .beads/issues.jsonl | 1 + ui/sdl2/sdl2_display.c | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7eaf7787..f698d0d0 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -9,6 +9,7 @@ {"_type":"issue","id":"fuse-wgi","title":"Minimal native SDL2 UI backend","description":"Introduce ui/sdl2/ as a native SDL2 frontend that reaches basic emulator usability using the existing software rendering and scaler flow. Focus this milestone on starting the emulator, running a responsive event loop, presenting frames in a window, and supporting essential keyboard-driven emulator use, not on text/layout-perfect fidelity or later modernization.","design":"This backend should stay native to ui/sdl2/ and reuse the existing software framebuffer and scaler flow for now. Prioritize dependable physical-key emulator usability over text-entry or layout fidelity in this early milestone.","acceptance_criteria":"Building with UI=sdl2 produces a working native SDL2 frontend under ui/sdl2/. Launching the emulator with UI=sdl2 starts successfully, opens an SDL2 window, and redraws visible emulator frames through the existing software/scaler path. The SDL2 event loop remains responsive during normal emulator use, including routine input and window interaction. Essential keyboard-driven emulator use works well enough for routine control and testing. Quit and shutdown paths exit cleanly without leaving the process hung or the SDL2 UI in a broken state.","status":"closed","priority":1,"issue_type":"feature","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:12Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T12:41:30Z","closed_at":"2026-04-11T12:41:30Z","close_reason":"SDL2 backend now presents visible emulator frames correctly; verified by user after fixing the 16-bit presentation path.","dependencies":[{"issue_id":"fuse-wgi","depends_on_id":"fuse-pir","type":"blocks","created_at":"2026-04-11T21:57:05Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"fuse-pir","title":"Build/configure groundwork for UI=sdl2 and SDL2 sound selection","description":"Add configure and build-system groundwork for a distinct UI=sdl2 backend and a separate SDL2 sound backend without regressing existing SDL1 codepaths. This task is only about selection, dependency plumbing, and default resolution rules, while keeping UI and audio independently selectable. When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override.","design":"Keep this limited to configure/build and backend selection plumbing. Do not introduce a hybrid SDL1/SDL2 runtime path or an internal SDL1 compatibility layer just to simplify option handling.","acceptance_criteria":"Configure/build logic exposes UI=sdl2 as a distinct selectable UI backend rather than folding it into the SDL1 path. Build/dependency handling distinguishes SDL1 and SDL2 requirements so SDL2-enabled targets can be built without rewriting the SDL1 backend path. UI and audio selection remain independent, so UI=sdl2 can be paired with a non-SDL2 audio backend and SDL2 sound can be selected without requiring UI=sdl2. When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. Existing SDL1 UI and audio builds continue to configure and build unchanged when SDL2 support is disabled or not selected.","status":"closed","priority":1,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:06Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T12:17:46Z","closed_at":"2026-04-11T12:17:46Z","close_reason":"Implemented SDL2 build/configure groundwork with UI=sdl2 selection, SDL2 sound selection/defaulting, temporary scaffolding sources, and SDL2 UI exclusion of SDL1 sound.","dependency_count":0,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fuse-7en","title":"Epic: native SDL2 UI and SDL2 sound backends","description":"Track the SDL2 migration as separate native backends in ui/sdl2/ and sound/sdl2sound.c. Sequence the work as build/configure groundwork first, then minimal SDL2 UI and SDL2 sound, then display cleanup, fullscreen/window behavior, and keyboard fidelity, with joystick, mouse/grab, and final parity cleanup staying as later follow-up. Keep UI and audio independently selectable, preserve the existing software rendering and scaler flow for this tranche, and clarify the default sound rule as: When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. Explicitly exclude any hybrid SDL1/SDL2 backend, internal SDL1 compatibility layer, or renderer/texture modernization in this epic.","design":"Architectural boundaries for this epic: add native SDL2 implementations as separate modules in ui/sdl2/ and sound/sdl2sound.c, keep backend selection independent between UI and audio, and treat renderer/texture modernization as separate future work rather than a prerequisite.","acceptance_criteria":"The epic description and child issue set explicitly cover build/configure groundwork, minimal SDL2 UI, SDL2 sound, display cleanup, fullscreen/window management, keyboard fidelity, deferred joystick, deferred mouse/grab, and final parity cleanup. The documented sequencing matches the current dependency graph, including fuse-pir blocking fuse-wgi and fuse-k8e, fuse-n5p depending on both fuse-wgi and fuse-k8e, and later input/window follow-up work remaining downstream. The epic text explicitly states that UI and audio stay independently selectable, the existing software/scaler flow stays in scope for this tranche, and the default sound rule is: When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. The epic text explicitly excludes any hybrid SDL1/SDL2 backend, internal SDL1 compatibility layer, or renderer/texture modernization in this tranche.","status":"closed","priority":1,"issue_type":"feature","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:55:31Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T01:58:45Z","closed_at":"2026-04-12T01:58:45Z","close_reason":"All SDL2 UI and SDL2 sound backend milestone work is now complete, documented, formatted, and locally verified; remaining future improvements can be tracked separately from this epic.","dependencies":[{"issue_id":"fuse-7en","depends_on_id":"fuse-3tt","type":"blocks","created_at":"2026-04-11T21:57:15Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-4vn","type":"blocks","created_at":"2026-04-11T21:57:15Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-e8p","type":"blocks","created_at":"2026-04-11T21:57:16Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-k8e","type":"blocks","created_at":"2026-04-11T21:57:13Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-l6g","type":"blocks","created_at":"2026-04-11T21:57:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-n5p","type":"blocks","created_at":"2026-04-11T21:57:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-pir","type":"blocks","created_at":"2026-04-11T21:57:12Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-qqg","type":"blocks","created_at":"2026-04-11T21:57:17Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-wgi","type":"blocks","created_at":"2026-04-11T21:57:13Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":9,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-rlo","title":"Fix SDL2 tape activity icon position on Timex machines","description":"SDL2 UI draws the tape activity icon around the middle of the screen after switching to a Timex machine such as the TC2048. Investigate how the icon position is computed for Timex display layouts and update the SDL2 path so the indicator is drawn in the proper screen position.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-17T10:27:54Z","created_by":"Fredrick Meunier","updated_at":"2026-05-17T10:29:21Z","started_at":"2026-05-17T10:27:58Z","closed_at":"2026-05-17T10:29:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-0yb","title":"Model SDL2 fullscreen scaler transitions explicitly","description":"Implement a small SDL2 display scaler state model so fullscreen/windowed transitions match SDL1 behavior more closely while keeping the options dialog truthful. Current SDL2 fullscreen code uses ad hoc windowed_scaler preservation and current_scaler inference, which causes edge cases when filters/scalers are changed in fullscreen. Desired behavior: current_scaler remains the actual scaler in use; entering fullscreen keeps the current scaler if it fits; if substitution is needed, prefer a scaler in the same family/class (TV, PAL TV, AdvMAME, HQ, normal, Timex) before generic fallback; exiting fullscreen restores only when the scaler was automatically substituted; explicit user scaler choices while fullscreen should be respected and become the actual scaler shown in options. Consider states like WINDOWED, FULLSCREEN_NATIVE, FULLSCREEN_AUTO and explicit transition events for fullscreen toggle, user scaler change, machine change, and startup/init.","notes":"Reworked the explicit-scaler-change signaling to avoid the earlier ui/scaler/scaler.[ch] hook. Removed scaler_selecting_scaler() entirely. Added a UI-level one-shot hotswap reason in ui/uidisplay.h + ui.c: uidisplay_set_next_hotswap_reason() / uidisplay_take_next_hotswap_reason(). menu.c marks explicit scaler selections before calling scaler_select_scaler(); SDL2 consumes that reason inside uidisplay_hotswap_gfx_mode(). Reverified with make -j4 fuse unittests/sdl2displaytest unittests/sdl2scalerstatetest \u0026\u0026 ./unittests/sdl2displaytest \u0026\u0026 ./unittests/sdl2scalerstatetest \u0026\u0026 make check.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-12T08:53:52Z","created_by":"Fredrick Meunier","updated_at":"2026-05-12T11:38:33Z","started_at":"2026-05-12T09:04:09Z","dependencies":[{"issue_id":"fuse-0yb","depends_on_id":"fuse-8k3","type":"blocks","created_at":"2026-05-12T18:53:57Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-so3.6","title":"Hide SDL2 cursor while fullscreen","description":"SDL2 desktop fullscreen currently leaves the host mouse pointer visible when entering fullscreen if the mouse was not already grabbed. SDL1 hides the cursor while fullscreen and restores it when leaving. Match that behaviour for SDL2 fullscreen transitions.","acceptance_criteria":"Entering SDL2 fullscreen hides the host cursor even when the mouse was not previously grabbed; leaving fullscreen restores the cursor unless another grab state requires it hidden; SDL1 behaviour is documented as the reference.","notes":"SDL1 reference: ui/sdl/sdldisplay.c hides the cursor after fullscreen hotswap when settings_current.full_screen or ui_mouse_grabbed is true, and shows it otherwise. SDL2 fix exposes the existing cursor visibility helper and applies the same fullscreen-or-grabbed visibility rule after SDL2 window recreation.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-10T00:24:49Z","created_by":"Fredrick Meunier","updated_at":"2026-05-10T00:28:05Z","started_at":"2026-05-10T00:24:51Z","closed_at":"2026-05-10T00:28:05Z","close_reason":"Implemented SDL2 desktop fullscreen unit coverage/manual notes in bead and matched SDL1 cursor hiding behaviour; verified focused SDL2 display/mouse tests and fuse build.","labels":["fullscreen","sdl2"],"dependencies":[{"issue_id":"fuse-so3.6","depends_on_id":"fuse-so3","type":"parent-child","created_at":"2026-05-10T10:24:49Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-so3.5","title":"Remove obsolete SDL2 fullscreen mode scanning and selection","description":"After desktop fullscreen is working, remove or greatly simplify native SDL2 code that exists only to enumerate, rank, and choose real fullscreen display modes. Keep only code needed for any explicit legacy/fallback mode, if the project decides to retain one.","acceptance_criteria":"Unused SDL2 mode ranking/listing helpers are removed or isolated behind an explicit legacy option; dead fields such as stored fullscreen width/height/refresh/format are removed where no longer needed; code no longer computes a default real fullscreen mode for normal --full-screen.","notes":"Removed SDL2 fixed fullscreen mode enumeration/selection path in working tree: deleted native SDL2 mode list/ranking helpers, fixed-mode globals, SDL_SetWindowDisplayMode fullscreen branch, and related unit tests while keeping desktop fullscreen scaler tests. Verified with make unittests/sdl2displaytest, ./unittests/sdl2displaytest, and make fuse. Not committed per user request.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-09T22:57:22Z","created_by":"Fredrick Meunier","updated_at":"2026-05-10T00:50:50Z","started_at":"2026-05-10T00:40:03Z","closed_at":"2026-05-10T00:50:50Z","close_reason":"Removed SDL2 fixed fullscreen mode enumeration/ranking, fixed-mode globals, SDL_SetWindowDisplayMode path, and related tests; SDL2 fullscreen now uses desktop fullscreen only. Verified with make unittests/sdl2displaytest, ./unittests/sdl2displaytest, and make fuse.","labels":["cleanup","fullscreen","sdl2"],"dependencies":[{"issue_id":"fuse-so3.5","depends_on_id":"fuse-so3","type":"parent-child","created_at":"2026-05-10T08:57:21Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-so3.5","depends_on_id":"fuse-so3.4","type":"blocks","created_at":"2026-05-10T08:57:28Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} diff --git a/ui/sdl2/sdl2_display.c b/ui/sdl2/sdl2_display.c index 65410f94..c304a8a5 100644 --- a/ui/sdl2/sdl2_display.c +++ b/ui/sdl2/sdl2_display.c @@ -293,6 +293,11 @@ sdl2display_status_icon( SDL_Surface **icon, int x, int y ) if( !surface ) return; + if( machine_current->timex ) { + x <<= 1; + y <<= 1; + } + sdl2display_icon_rect( x, y, surface->w, surface->h, sdl2display_current_size, fullscreen_x_off, fullscreen_y_off, &rect ); From b54d59aa98354dbcd6f6abb66607859a7e41e8d1 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 19:41:18 +1000 Subject: [PATCH 008/142] Advance ALSA buffer pointer after partial writes 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. --- sound/alsasound.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sound/alsasound.c b/sound/alsasound.c index 841b2040..ad4c2d0b 100644 --- a/sound/alsasound.c +++ b/sound/alsasound.c @@ -384,7 +384,8 @@ sound_lowlevel_frame( libspectrum_signed_word *data, int len ) if( verb ) fprintf( stderr, "ALSA: *buffer underrun*!\n" ); } else { - len -= ret; + data += ret * ch; + len -= ret; } } } From b02f4065360919e238b86c43ae196aee8b6ca257 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 20:04:40 +1000 Subject: [PATCH 009/142] Suppress AC_PROG_LEX deprecation warning with noyywrap 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. --- configure.ac | 2 +- debugger/command.c | 9 --------- debugger/commandl.l | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/configure.ac b/configure.ac index 15ffa448..6d15b975 100644 --- a/configure.ac +++ b/configure.ac @@ -79,7 +79,7 @@ AC_PROG_CC AM_PROG_CC_C_O AC_PATH_PROG(PERL, perl) AC_SUBST(PERL) -AM_PROG_LEX +AC_PROG_LEX([noyywrap]) AC_PROG_YACC LT_INIT diff --git a/debugger/command.c b/debugger/command.c index 4e7f4fd3..e1e849b4 100644 --- a/debugger/command.c +++ b/debugger/command.c @@ -42,7 +42,6 @@ static char *command_buffer = NULL; static char *command_ptr; int yyparse( void ); -int yywrap( void ); /* Evaluate the debugger command given in 'command' */ void @@ -66,14 +65,6 @@ debugger_command_evaluate( const char *command ) ui_debugger_update(); } -/* Utility functions called from the flex scanner */ - -int -yywrap( void ) -{ - return 1; -} - /* Called to get up to 'max_size' bytes of the command to be parsed */ int debugger_command_input( char *buf, int *result, int max_size ) diff --git a/debugger/commandl.l b/debugger/commandl.l index 2685ffc6..94725f81 100644 --- a/debugger/commandl.l +++ b/debugger/commandl.l @@ -46,7 +46,7 @@ ID [a-z]([a-z0-9']|\\[ ])* -%option caseless +%option caseless noyywrap %s COMMANDSTATE1 %x COMMANDSTATE2 From d31999c0774b8c46e20d76304814d73a7192d560 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 20:37:09 +1000 Subject: [PATCH 010/142] Fix duplicate step (7) numbering in build_linux_sub.yml --- .github/workflows/build_linux_sub.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_linux_sub.yml b/.github/workflows/build_linux_sub.yml index c1186561..c0eb8a5c 100644 --- a/.github/workflows/build_linux_sub.yml +++ b/.github/workflows/build_linux_sub.yml @@ -94,16 +94,16 @@ jobs: run: | .github/scripts/in_config.sh "${{ inputs.verify_other }}" - - name: (7) Make + - name: (8) Make run: make - - name: (8) Install + - name: (9) Install run: sudo make install - - name: (9) Run unit tests + - name: (10) Run unit tests if: ${{ inputs.run_check }} run: make check - - name: (10) Finish + - name: (11) Finish run: | echo "🍏 Finishing with status ${{ job.status }}." From cf3e86542b05025273d4da98a973c062c146e291 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 20:48:29 +1000 Subject: [PATCH 011/142] Delete nightly build for now It fails every night and we are not using releases in the Fuse GitHub mirror account yet. --- .github/workflows/nightly_build.yml | 45 ----------------------------- 1 file changed, 45 deletions(-) delete mode 100755 .github/workflows/nightly_build.yml diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml deleted file mode 100755 index aa1e5bd2..00000000 --- a/.github/workflows/nightly_build.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Nightly build -run-name: Automated nightly build and pre-release - -# Executed every night (1 AM) + button -on: - schedule: - - cron: '0 1 * * *' - workflow_dispatch: - -jobs: - # Merge master from defined repo (usually official fuse repo) - catchup: - name: "Merge foreign master" - runs-on: ubuntu-latest - if: ${{ vars.PARENT_FUSE_BRANCH }} && ${{ vars.PARENT_FUSE_REPO }} - permissions: - contents: write - steps: - - name: "Checkout code" - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: "Merge fresh ${{ vars.PARENT_FUSE_BRANCH }} from ${{ vars.PARENT_FUSE_REPO }}" - run: | - git fetch --prune - git remote add repo ${{ vars.PARENT_FUSE_REPO }} - git fetch repo - git merge repo/${{ vars.PARENT_FUSE_BRANCH }} - git push - - # Build and release nightly prerelease version from master (no draft) - # Add current date and time to the tag, keep latest 5 - master: - needs: [catchup] - name: "Nightly build from master" - uses: ./.github/workflows/test_release.yml - with: - reason: "Nightly master build" - draft: false - generateReleaseNotes: true - libspectrum_branch: "master" - add_date_time: true - keep_latest: 5 - secrets: - RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" From 9542015f5a502e79dd841bdf53b7c0c0bd7624d9 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 20:56:12 +1000 Subject: [PATCH 012/142] Update m4/pkg.m4 from pkg-config 0.24 to 0.29.2 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. --- m4/pkg.m4 | 311 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 220 insertions(+), 91 deletions(-) diff --git a/m4/pkg.m4 b/m4/pkg.m4 index c5b26b52..c88ef6ff 100644 --- a/m4/pkg.m4 +++ b/m4/pkg.m4 @@ -1,29 +1,60 @@ -# pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*- -# serial 1 (pkg-config-0.24) -# -# Copyright © 2004 Scott James Remnant . -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# As a special exception to the GNU General Public License, if you -# distribute this file as part of a program that contains a -# configuration script generated by Autoconf, you may include it under -# the same distribution terms that you use for the rest of that program. - -# PKG_PROG_PKG_CONFIG([MIN-VERSION]) -# ---------------------------------- +# pkg.m4 - Macros to locate and use pkg-config. -*- Autoconf -*- +# serial 12 (pkg-config-0.29.2) + +dnl Copyright © 2004 Scott James Remnant . +dnl Copyright © 2012-2015 Dan Nicholson +dnl +dnl This program is free software; you can redistribute it and/or modify +dnl it under the terms of the GNU General Public License as published by +dnl the Free Software Foundation; either version 2 of the License, or +dnl (at your option) any later version. +dnl +dnl This program is distributed in the hope that it will be useful, but +dnl WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +dnl General Public License for more details. +dnl +dnl You should have received a copy of the GNU General Public License +dnl along with this program; if not, write to the Free Software +dnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +dnl 02111-1307, USA. +dnl +dnl As a special exception to the GNU General Public License, if you +dnl distribute this file as part of a program that contains a +dnl configuration script generated by Autoconf, you may include it under +dnl the same distribution terms that you use for the rest of that +dnl program. + +dnl PKG_PREREQ(MIN-VERSION) +dnl ----------------------- +dnl Since: 0.29 +dnl +dnl Verify that the version of the pkg-config macros are at least +dnl MIN-VERSION. Unlike PKG_PROG_PKG_CONFIG, which checks the user's +dnl installed version of pkg-config, this checks the developer's version +dnl of pkg.m4 when generating configure. +dnl +dnl To ensure that this macro is defined, also add: +dnl m4_ifndef([PKG_PREREQ], +dnl [m4_fatal([must install pkg-config 0.29 or later before running autoconf/autogen])]) +dnl +dnl See the "Since" comment for each macro you use to see what version +dnl of the macros you require. +m4_defun([PKG_PREREQ], +[m4_define([PKG_MACROS_VERSION], [0.29.2]) +m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1, + [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])]) +])dnl PKG_PREREQ + +dnl PKG_PROG_PKG_CONFIG([MIN-VERSION]) +dnl ---------------------------------- +dnl Since: 0.16 +dnl +dnl Search for the pkg-config tool and set the PKG_CONFIG variable to +dnl first found in the path. Checks that the version of pkg-config found +dnl is at least MIN-VERSION. If MIN-VERSION is not specified, 0.9.0 is +dnl used since that's the first version where most current features of +dnl pkg-config existed. AC_DEFUN([PKG_PROG_PKG_CONFIG], [m4_pattern_forbid([^_?PKG_[A-Z_]+$]) m4_pattern_allow([^PKG_CONFIG(_(PATH|LIBDIR|SYSROOT_DIR|ALLOW_SYSTEM_(CFLAGS|LIBS)))?$]) @@ -45,18 +76,19 @@ if test -n "$PKG_CONFIG"; then PKG_CONFIG="" fi fi[]dnl -])# PKG_PROG_PKG_CONFIG - -# PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -# -# Check to see whether a particular set of modules exists. Similar -# to PKG_CHECK_MODULES(), but does not set variables or print errors. -# -# Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG]) -# only at the first occurence in configure.ac, so if the first place -# it's called might be skipped (such as if it is within an "if", you -# have to call PKG_CHECK_EXISTS manually -# -------------------------------------------------------------- +])dnl PKG_PROG_PKG_CONFIG + +dnl PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) +dnl ------------------------------------------------------------------- +dnl Since: 0.18 +dnl +dnl Check to see whether a particular set of modules exists. Similar to +dnl PKG_CHECK_MODULES(), but does not set variables or print errors. +dnl +dnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG]) +dnl only at the first occurrence in configure.ac, so if the first place +dnl it's called might be skipped (such as if it is within an "if", you +dnl have to call PKG_CHECK_EXISTS manually AC_DEFUN([PKG_CHECK_EXISTS], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl if test -n "$PKG_CONFIG" && \ @@ -66,8 +98,10 @@ m4_ifvaln([$3], [else $3])dnl fi]) -# _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES]) -# --------------------------------------------- +dnl _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES]) +dnl --------------------------------------------- +dnl Internal wrapper calling pkg-config via PKG_CONFIG and setting +dnl pkg_failed based on the result. m4_define([_PKG_CONFIG], [if test -n "$$1"; then pkg_cv_[]$1="$$1" @@ -79,10 +113,11 @@ m4_define([_PKG_CONFIG], else pkg_failed=untried fi[]dnl -])# _PKG_CONFIG +])dnl _PKG_CONFIG -# _PKG_SHORT_ERRORS_SUPPORTED -# ----------------------------- +dnl _PKG_SHORT_ERRORS_SUPPORTED +dnl --------------------------- +dnl Internal check to see if pkg-config supports short errors. AC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED], [AC_REQUIRE([PKG_PROG_PKG_CONFIG]) if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then @@ -90,26 +125,24 @@ if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then else _pkg_short_errors_supported=no fi[]dnl -])# _PKG_SHORT_ERRORS_SUPPORTED - - -# PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], -# [ACTION-IF-NOT-FOUND]) -# -# -# Note that if there is a possibility the first call to -# PKG_CHECK_MODULES might not happen, you should be sure to include an -# explicit call to PKG_PROG_PKG_CONFIG in your configure.ac -# -# -# -------------------------------------------------------------- +])dnl _PKG_SHORT_ERRORS_SUPPORTED + + +dnl PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], +dnl [ACTION-IF-NOT-FOUND]) +dnl -------------------------------------------------------------- +dnl Since: 0.4.0 +dnl +dnl Note that if there is a possibility the first call to +dnl PKG_CHECK_MODULES might not happen, you should be sure to include an +dnl explicit call to PKG_PROG_PKG_CONFIG in your configure.ac AC_DEFUN([PKG_CHECK_MODULES], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl pkg_failed=no -AC_MSG_CHECKING([for $1]) +AC_MSG_CHECKING([for $2]) _PKG_CONFIG([$1][_CFLAGS], [cflags], [$2]) _PKG_CONFIG([$1][_LIBS], [libs], [$2]) @@ -119,17 +152,17 @@ and $1[]_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details.]) if test $pkg_failed = yes; then - AC_MSG_RESULT([no]) + AC_MSG_RESULT([no]) _PKG_SHORT_ERRORS_SUPPORTED if test $_pkg_short_errors_supported = yes; then - $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` - else - $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` + $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` + else + $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` fi - # Put the nasty error message in config.log where it belongs - echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD + # Put the nasty error message in config.log where it belongs + echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD - m4_default([$4], [AC_MSG_ERROR( + m4_default([$4], [AC_MSG_ERROR( [Package requirements ($2) were not met: $$1_PKG_ERRORS @@ -140,8 +173,8 @@ installed software in a non-standard prefix. _PKG_TEXT])[]dnl ]) elif test $pkg_failed = untried; then - AC_MSG_RESULT([no]) - m4_default([$4], [AC_MSG_FAILURE( + AC_MSG_RESULT([no]) + m4_default([$4], [AC_MSG_FAILURE( [The pkg-config script could not be found or is too old. Make sure it is in your PATH or set the PKG_CONFIG environment variable to the full path to pkg-config. @@ -151,21 +184,45 @@ _PKG_TEXT To get pkg-config, see .])[]dnl ]) else - $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS - $1[]_LIBS=$pkg_cv_[]$1[]_LIBS + $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS + $1[]_LIBS=$pkg_cv_[]$1[]_LIBS AC_MSG_RESULT([yes]) - $3 + $3 fi[]dnl -])# PKG_CHECK_MODULES +])dnl PKG_CHECK_MODULES -# PKG_INSTALLDIR(DIRECTORY) -# ------------------------- -# Substitutes the variable pkgconfigdir as the location where a module -# should install pkg-config .pc files. By default the directory is -# $libdir/pkgconfig, but the default can be changed by passing -# DIRECTORY. The user can override through the --with-pkgconfigdir -# parameter. +dnl PKG_CHECK_MODULES_STATIC(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], +dnl [ACTION-IF-NOT-FOUND]) +dnl --------------------------------------------------------------------- +dnl Since: 0.29 +dnl +dnl Checks for existence of MODULES and gathers its build flags with +dnl static libraries enabled. Sets VARIABLE-PREFIX_CFLAGS from --cflags +dnl and VARIABLE-PREFIX_LIBS from --libs. +dnl +dnl Note that if there is a possibility the first call to +dnl PKG_CHECK_MODULES_STATIC might not happen, you should be sure to +dnl include an explicit call to PKG_PROG_PKG_CONFIG in your +dnl configure.ac. +AC_DEFUN([PKG_CHECK_MODULES_STATIC], +[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl +_save_PKG_CONFIG=$PKG_CONFIG +PKG_CONFIG="$PKG_CONFIG --static" +PKG_CHECK_MODULES($@) +PKG_CONFIG=$_save_PKG_CONFIG[]dnl +])dnl PKG_CHECK_MODULES_STATIC + + +dnl PKG_INSTALLDIR([DIRECTORY]) +dnl ------------------------- +dnl Since: 0.27 +dnl +dnl Substitutes the variable pkgconfigdir as the location where a module +dnl should install pkg-config .pc files. By default the directory is +dnl $libdir/pkgconfig, but the default can be changed by passing +dnl DIRECTORY. The user can override through the --with-pkgconfigdir +dnl parameter. AC_DEFUN([PKG_INSTALLDIR], [m4_pushdef([pkg_default], [m4_default([$1], ['${libdir}/pkgconfig'])]) m4_pushdef([pkg_description], @@ -176,16 +233,18 @@ AC_ARG_WITH([pkgconfigdir], AC_SUBST([pkgconfigdir], [$with_pkgconfigdir]) m4_popdef([pkg_default]) m4_popdef([pkg_description]) -]) dnl PKG_INSTALLDIR +])dnl PKG_INSTALLDIR -# PKG_NOARCH_INSTALLDIR(DIRECTORY) -# ------------------------- -# Substitutes the variable noarch_pkgconfigdir as the location where a -# module should install arch-independent pkg-config .pc files. By -# default the directory is $datadir/pkgconfig, but the default can be -# changed by passing DIRECTORY. The user can override through the -# --with-noarch-pkgconfigdir parameter. +dnl PKG_NOARCH_INSTALLDIR([DIRECTORY]) +dnl -------------------------------- +dnl Since: 0.27 +dnl +dnl Substitutes the variable noarch_pkgconfigdir as the location where a +dnl module should install arch-independent pkg-config .pc files. By +dnl default the directory is $datadir/pkgconfig, but the default can be +dnl changed by passing DIRECTORY. The user can override through the +dnl --with-noarch-pkgconfigdir parameter. AC_DEFUN([PKG_NOARCH_INSTALLDIR], [m4_pushdef([pkg_default], [m4_default([$1], ['${datadir}/pkgconfig'])]) m4_pushdef([pkg_description], @@ -196,13 +255,15 @@ AC_ARG_WITH([noarch-pkgconfigdir], AC_SUBST([noarch_pkgconfigdir], [$with_noarch_pkgconfigdir]) m4_popdef([pkg_default]) m4_popdef([pkg_description]) -]) dnl PKG_NOARCH_INSTALLDIR +])dnl PKG_NOARCH_INSTALLDIR -# PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE, -# [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -# ------------------------------------------- -# Retrieves the value of the pkg-config variable for the given module. +dnl PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE, +dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) +dnl ------------------------------------------- +dnl Since: 0.28 +dnl +dnl Retrieves the value of the pkg-config variable for the given module. AC_DEFUN([PKG_CHECK_VAR], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl @@ -211,4 +272,72 @@ _PKG_CONFIG([$1], [variable="][$3]["], [$2]) AS_VAR_COPY([$1], [pkg_cv_][$1]) AS_VAR_IF([$1], [""], [$5], [$4])dnl -])# PKG_CHECK_VAR +])dnl PKG_CHECK_VAR + +dnl PKG_WITH_MODULES(VARIABLE-PREFIX, MODULES, +dnl [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND], +dnl [DESCRIPTION], [DEFAULT]) +dnl ------------------------------------------ +dnl +dnl Prepare a "--with-" configure option using the lowercase +dnl [VARIABLE-PREFIX] name, merging the behaviour of AC_ARG_WITH and +dnl PKG_CHECK_MODULES in a single macro. +AC_DEFUN([PKG_WITH_MODULES], +[ +m4_pushdef([with_arg], m4_tolower([$1])) + +m4_pushdef([description], + [m4_default([$5], [build with ]with_arg[ support])]) + +m4_pushdef([def_arg], [m4_default([$6], [auto])]) +m4_pushdef([def_action_if_found], [AS_TR_SH([with_]with_arg)=yes]) +m4_pushdef([def_action_if_not_found], [AS_TR_SH([with_]with_arg)=no]) + +m4_case(def_arg, + [yes],[m4_pushdef([with_without], [--without-]with_arg)], + [m4_pushdef([with_without],[--with-]with_arg)]) + +AC_ARG_WITH(with_arg, + AS_HELP_STRING(with_without, description[ @<:@default=]def_arg[@:>@]),, + [AS_TR_SH([with_]with_arg)=def_arg]) + +AS_CASE([$AS_TR_SH([with_]with_arg)], + [yes],[PKG_CHECK_MODULES([$1],[$2],$3,$4)], + [auto],[PKG_CHECK_MODULES([$1],[$2], + [m4_n([def_action_if_found]) $3], + [m4_n([def_action_if_not_found]) $4])]) + +m4_popdef([with_arg]) +m4_popdef([description]) +m4_popdef([def_arg]) + +])dnl PKG_WITH_MODULES + +dnl PKG_HAVE_WITH_MODULES(VARIABLE-PREFIX, MODULES, +dnl [DESCRIPTION], [DEFAULT]) +dnl ----------------------------------------------- +dnl +dnl Convenience macro to trigger AM_CONDITIONAL after PKG_WITH_MODULES +dnl check._[VARIABLE-PREFIX] is exported as make variable. +AC_DEFUN([PKG_HAVE_WITH_MODULES], +[ +PKG_WITH_MODULES([$1],[$2],,,[$3],[$4]) + +AM_CONDITIONAL([HAVE_][$1], + [test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"]) +])dnl PKG_HAVE_WITH_MODULES + +dnl PKG_HAVE_DEFINE_WITH_MODULES(VARIABLE-PREFIX, MODULES, +dnl [DESCRIPTION], [DEFAULT]) +dnl ------------------------------------------------------ +dnl +dnl Convenience macro to run AM_CONDITIONAL and AC_DEFINE after +dnl PKG_WITH_MODULES check. HAVE_[VARIABLE-PREFIX] is exported as make +dnl and preprocessor variable. +AC_DEFUN([PKG_HAVE_DEFINE_WITH_MODULES], +[ +PKG_HAVE_WITH_MODULES([$1],[$2],[$3],[$4]) + +AS_IF([test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"], + [AC_DEFINE([HAVE_][$1], 1, [Enable ]m4_tolower([$1])[ support])]) +])dnl PKG_HAVE_DEFINE_WITH_MODULES From 3ec60b1c3e1091ebcfdaa06533d283c0941054a1 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 21:13:25 +1000 Subject: [PATCH 013/142] Use ARRAY_SIZE macro instead of manual sizeof division in SDL2 --- ui/sdl2/sdl2_display.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/sdl2/sdl2_display.c b/ui/sdl2/sdl2_display.c index c304a8a5..b0e2b0a3 100644 --- a/ui/sdl2/sdl2_display.c +++ b/ui/sdl2/sdl2_display.c @@ -249,8 +249,7 @@ sdl2display_add_scaled_rect( int x, int y, int w, int h ) { if( sdl2display_force_full_refresh ) return; - if( num_rects == - (int)( sizeof( updated_rects ) / sizeof( updated_rects[0] ) ) ){ + if( num_rects == (int)ARRAY_SIZE( updated_rects ) ){ sdl2display_force_full_refresh = 1; return; } @@ -812,8 +811,7 @@ uidisplay_area( int x, int y, int width, int height ) { if( sdl2display_force_full_refresh ) return; - if( num_rects == - (int)( sizeof( updated_rects ) / sizeof( updated_rects[0] ) ) ){ + if( num_rects == (int)ARRAY_SIZE( updated_rects ) ){ sdl2display_force_full_refresh = 1; return; } From 5dad7aec27c0af9de126d979ef7793bf027fe47c Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 18 May 2026 22:28:22 +1000 Subject: [PATCH 014/142] =?UTF-8?q?Remove=20support=20for=20libjsw=20(fixe?= =?UTF-8?q?s=20bug=20#21)=20(thanks,=20Sergio=20Baldov=C3=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libjsw is unmaintained and no longer distributed in Linux distributions (except maybe Gentoo). Use SDL to handle joysticks with all UIs. --- README | 8 +- configure.ac | 55 ++---- data/shell-completion/bash/fuse | 6 +- data/shell-completion/diff_options.sh | 3 +- fuse.c | 4 +- man/fuse.1 | 26 --- settings.dat | 2 - ui/gtk3/gtkjoystick.c | 7 +- ui/sdl/sdljoystick.c | 7 +- ui/sdl/sdlui.c | 4 +- ui/sdl2/sdl2_joystick.c | 2 +- ui/sdl2/sdl2_joystick.h | 2 +- ui/sdl2/sdl2_ui.c | 2 +- ui/uijoystick.c | 243 +------------------------- ui/win32/win32joystick.c | 6 +- ui/win32/win32ui.c | 4 +- ui/xlib/xjoystick.c | 5 +- 17 files changed, 39 insertions(+), 347 deletions(-) diff --git a/README b/README index 678227c4..2ce7483e 100644 --- a/README +++ b/README @@ -4,7 +4,7 @@ The Free Unix Spectrum Emulator (Fuse) 1.8.0 Fuse (the Free Unix Spectrum Emulator) was originally, and somewhat unsurprisingly, an emulator of the ZX Spectrum (a popular 1980s home computer, especially in the UK) for Unix. However, it has now also -been ported to Mac OS X, which may or may not count as a Unix variant +been ported to macOS, which may or may not count as a Unix variant depending on your advocacy position and Windows which definitely isn't a Unix variant. Fuse also emulates some of the better-known ZX Spectrum clones as well. @@ -69,11 +69,11 @@ If you've used Fuse prior to version 0.5.0, note that the external utilities (tzxlist, etc) are now available separately from Fuse itself. See http://fuse-emulator.sourceforge.net/ for details. -Mac OS X +macOS * Either the native port by Fredrick Meunier, or the original version - will compile on OS X 10.3 (Panther) or later. -* On Mac OS X Lion you will need to use clang as gcc-llvm-4.2.1 fails to + will compile on macOS 10.3 (Panther) or later. +* On macOS Lion you will need to use clang as gcc-llvm-4.2.1 fails to correctly compile z80_ops.c. Windows diff --git a/configure.ac b/configure.ac index 6d15b975..05c85efb 100644 --- a/configure.ac +++ b/configure.ac @@ -716,62 +716,30 @@ AC_MSG_CHECKING(whether hardware joystick support requested) stick=yes) AC_MSG_RESULT($stick) if test "$stick" = yes; then - dnl We prefer to use UI-specific joystick code. (Default.) - dnl If there is none, or you override this here, we use libjsw. case "$UI" in sdl|sdl2|win32) - dnl Libraries provide their own joystick code - AC_MSG_CHECKING([whether to use $UI's joystick support]) - AC_ARG_ENABLE(ui-joystick, - AS_HELP_STRING([--disable-ui-joystick], [use libjsw joystick code (where supported)]), - if test "$enableval" = no; then libjsw=yes; else libjsw=no; fi, - libjsw=no) - if test "$libjsw" = yes; then - AC_MSG_RESULT([no, use libjsw]) - else - AC_MSG_RESULT(yes) - AC_DEFINE([USE_JOYSTICK], 1, [Defined if we're using hardware joysticks]) - fi + dnl These UIs provide their own joystick code. + AC_DEFINE([USE_JOYSTICK], 1, [Defined if we're using hardware joysticks]) ;; gtk3|xlib) - dnl These UIs could use SDL library for handling joysticks - AC_MSG_CHECKING([whether to use $UI's joystick support]) - AC_ARG_ENABLE(ui-joystick, - AS_HELP_STRING([--disable-ui-joystick], [use libjsw joystick code (where supported)]), - if test "$enableval" = no; then libjsw=yes; else libjsw=no; fi, - libjsw=no) - if test "$libjsw" = yes; then - AC_MSG_RESULT([no, use libjsw]) + dnl These UIs use SDL library for handling joysticks. + if test "$sdl_available" != no; then + AC_DEFINE([USE_JOYSTICK], 1, [Defined if we're using hardware joysticks]) + use_sdl=$sdl_available else - if test "$sdl_available" != no; then - AC_MSG_RESULT(yes) - AC_DEFINE([USE_JOYSTICK], 1, [Defined if we're using hardware joysticks]) - use_sdl=$sdl_available - else - AC_MSG_WARN([No suitable SDL version found - joystick support disabled]) - stick=no - fi + AC_MSG_WARN([No suitable SDL version found - joystick support disabled]) + stick=no fi ;; wii) dnl Wii UI check this elsewhere AC_MSG_RESULT(yes) - libjsw=no ;; *) - dnl Our only option is libjsw. - libjsw=yes + dnl No joystick backend available for this UI. + stick=no ;; esac - if test "$libjsw" = yes; then - dnl Look for libjsw. If missing, use QAOP. - AC_CHECK_HEADERS(jsw.h, - LIBS="$LIBS -ljsw"; - AC_DEFINE([USE_JOYSTICK], 1, - [Defined if we're using hardware joysticks]), - AC_MSG_WARN(jsw.h not found - joystick support disabled) - stick=no) - fi fi dnl See if libxml2 is available (for the config file handling) @@ -1047,9 +1015,6 @@ if test x"${gpm}" != "x"; then echo "libgpm support: ${gpm}" fi echo "Joystick support: ${stick}" -if test "${stick}" = "yes"; then - echo "Using libjsw for joystick support: ${libjsw}" -fi echo "zlib support: ${zlib}" echo "libxml2 support: ${libxml2}" echo "libpng support: ${libpng}" diff --git a/data/shell-completion/bash/fuse b/data/shell-completion/bash/fuse index f93e0795..24c08586 100644 --- a/data/shell-completion/bash/fuse +++ b/data/shell-completion/bash/fuse @@ -163,7 +163,7 @@ _fuse() return 0 ;; --competition-code|--debugger-command| \ - --drive-40-max-track|--drive-80-max-track|--joystick-[12]|-j| \ + --drive-40-max-track|--drive-80-max-track| \ --joystick-[12]-fire-[1-9]|--joystick-[12]-fire-1[0-5]| \ --joystick-[12]-output|--joystick-keyboard-down| \ --joystick-keyboard-fire|--joystick-keyboard-left| \ @@ -198,14 +198,14 @@ _fuse() --drive-plusd2-type --embed-snapshot --fastload --fbmode --fuller --full-screen --graphicsfile --graphics-filter --help --if2cart --interface1 --interface2 --issue2 - --joystick-1 --joystick-1-fire-1 --joystick-1-fire-2 + --joystick-1-fire-1 --joystick-1-fire-2 --joystick-1-fire-3 --joystick-1-fire-4 --joystick-1-fire-5 --joystick-1-fire-6 --joystick-1-fire-7 --joystick-1-fire-8 --joystick-1-fire-9 --joystick-1-fire-10 --joystick-1-fire-11 --joystick-1-fire-12 --joystick-1-fire-13 --joystick-1-fire-14 --joystick-1-fire-15 --joystick-1-output - --joystick-2 --joystick-2-fire-1 --joystick-2-fire-2 + --joystick-2-fire-1 --joystick-2-fire-2 --joystick-2-fire-3 --joystick-2-fire-4 --joystick-2-fire-5 --joystick-2-fire-6 --joystick-2-fire-7 --joystick-2-fire-8 --joystick-2-fire-9 --joystick-2-fire-10 diff --git a/data/shell-completion/diff_options.sh b/data/shell-completion/diff_options.sh index 7b3a7338..c739e7f2 100755 --- a/data/shell-completion/diff_options.sh +++ b/data/shell-completion/diff_options.sh @@ -32,7 +32,6 @@ echo === grep -o -E '\\\-\\\-[a-Za-z0-9\\\-]+' ../../man/fuse.1 | \ sed -e 's/\\\-/\-/g' -e 's/\-\-no\-/\-\-/g' | sort | uniq | \ - grep -v -e "\-\-disable\-ui\-joystick" -e "\-\-foo" -e "\-\-help" \ - -e "\-\-version" > man.txt + grep -v -e "\-\-foo" -e "\-\-help" -e "\-\-version" > man.txt grep -v -e "\-\-no\-" settings.txt | diff -U 0 man.txt - diff --git a/fuse.c b/fuse.c index 79c743d7..47af23cc 100644 --- a/fuse.c +++ b/fuse.c @@ -41,9 +41,9 @@ bootstrapping by redefining main. As we now allow SDL joystick code to be used in the GTK and Xlib UIs we need to also do the magic when that code is in use, feel free to look away for the next line */ -#if defined UI_SDL || defined UI_SDL2 || (defined USE_JOYSTICK && !defined HAVE_JSW_H && (defined UI_X || defined UI_GTK) ) +#if defined UI_SDL || defined UI_SDL2 || (defined USE_JOYSTICK && (defined UI_X || defined UI_GTK) ) #include /* Needed on MacOS X and Windows */ -#endif /* #if defined UI_SDL || defined UI_SDL2 || (defined USE_JOYSTICK && !defined HAVE_JSW_H && (defined UI_X || defined UI_GTK) ) */ +#endif /* #if defined UI_SDL || defined UI_SDL2 || (defined USE_JOYSTICK && (defined UI_X || defined UI_GTK) ) */ #ifdef GEKKO /* #include diff --git a/man/fuse.1 b/man/fuse.1 index 7fe53845..34611fe6 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -594,32 +594,6 @@ Emulate an issue\ 2 keyboard. Same as the General Options dialog's option. .RE .PP -.B \-j -.I device -.br -.B \-\-joystick\-1 -.I device -.RS -Read from -.I device -to emulate the first joystick. Fuse will use either -.RI ` /dev/input/js0 ' -or -.RI ` /dev/js0 ' -by default. -.RE -.PP -.B \-\-joystick\-2 -.I device -.RS -As for -.B \-\-joystick\-1 -but for the second joystick; the default here is either -.RI ` /dev/input/js1 ' -or -.RI ` /dev/js1 "'." -.RE -.PP .B \-\-joystick\-1\-output .I type .br diff --git a/settings.dat b/settings.dat index 0bd533f7..f51f852b 100644 --- a/settings.dat +++ b/settings.dat @@ -105,7 +105,6 @@ volume_specdrum, numeric, 100 volume_covox, numeric, 100 volume_uspeech, numeric, 100 -joystick_1, string, NULL, 'j' joystick_1_output, numeric, 0 joystick_1_fire_1, numeric, 4096 joystick_1_fire_2, numeric, 4096 @@ -122,7 +121,6 @@ joystick_1_fire_12, numeric, 4096 joystick_1_fire_13, numeric, 4096 joystick_1_fire_14, numeric, 4096 joystick_1_fire_15, numeric, 4096 -joystick_2, string, NULL joystick_2_output, numeric, 0 joystick_2_fire_1, numeric, 4096 joystick_2_fire_2, numeric, 4096 diff --git a/ui/gtk3/gtkjoystick.c b/ui/gtk3/gtkjoystick.c index 751a13ca..da303289 100644 --- a/ui/gtk3/gtkjoystick.c +++ b/ui/gtk3/gtkjoystick.c @@ -36,15 +36,14 @@ #include "menu.h" #include "settings.h" -#if !defined USE_JOYSTICK || defined HAVE_JSW_H -/* Fake joystick, or override UI-specific handling */ +#ifndef USE_JOYSTICK #include "../uijoystick.c" -#else /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#else #include "../sdl/sdljoystick.c" -#endif /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#endif enum { diff --git a/ui/sdl/sdljoystick.c b/ui/sdl/sdljoystick.c index f9ce08d3..993e9fde 100644 --- a/ui/sdl/sdljoystick.c +++ b/ui/sdl/sdljoystick.c @@ -26,11 +26,10 @@ #include "config.h" -#if !defined USE_JOYSTICK || defined HAVE_JSW_H -/* Fake joystick, or override UI-specific handling */ +#ifndef USE_JOYSTICK #include "../uijoystick.c" -#else /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#else #include @@ -252,4 +251,4 @@ ui_joystick_end( void ) #endif } -#endif /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#endif diff --git a/ui/sdl/sdlui.c b/ui/sdl/sdlui.c index fed74b2f..ab130813 100644 --- a/ui/sdl/sdlui.c +++ b/ui/sdl/sdlui.c @@ -99,7 +99,7 @@ ui_event( void ) } break; -#if defined USE_JOYSTICK && !defined HAVE_JSW_H +#ifdef USE_JOYSTICK case SDL_JOYBUTTONDOWN: sdljoystick_buttonpress( &(event.jbutton) ); @@ -114,7 +114,7 @@ ui_event( void ) sdljoystick_hatmove( &(event.jhat) ); break; -#endif /* if defined USE_JOYSTICK && !defined HAVE_JSW_H */ +#endif /* #ifdef USE_JOYSTICK */ case SDL_QUIT: fuse_emulation_pause(); diff --git a/ui/sdl2/sdl2_joystick.c b/ui/sdl2/sdl2_joystick.c index 5e4e7e67..abcdc524 100644 --- a/ui/sdl2/sdl2_joystick.c +++ b/ui/sdl2/sdl2_joystick.c @@ -9,7 +9,7 @@ #include "config.h" -#if !defined USE_JOYSTICK || defined HAVE_JSW_H +#ifndef USE_JOYSTICK #include "../uijoystick.c" #else diff --git a/ui/sdl2/sdl2_joystick.h b/ui/sdl2/sdl2_joystick.h index e1a80191..ee851978 100644 --- a/ui/sdl2/sdl2_joystick.h +++ b/ui/sdl2/sdl2_joystick.h @@ -14,7 +14,7 @@ #include -#if defined USE_JOYSTICK && !defined HAVE_JSW_H +#ifdef USE_JOYSTICK void sdl2joystick_buttonpress( SDL_JoyButtonEvent *buttonevent ); void sdl2joystick_buttonrelease( SDL_JoyButtonEvent *buttonevent ); void sdl2joystick_axismove( SDL_JoyAxisEvent *axisevent ); diff --git a/ui/sdl2/sdl2_ui.c b/ui/sdl2/sdl2_ui.c index 3915b7a8..151eb4e6 100644 --- a/ui/sdl2/sdl2_ui.c +++ b/ui/sdl2/sdl2_ui.c @@ -128,7 +128,7 @@ ui_event( void ) } break; -#if defined USE_JOYSTICK && !defined HAVE_JSW_H +#ifdef USE_JOYSTICK case SDL_JOYBUTTONDOWN: sdl2joystick_buttonpress( &event.jbutton ); break; diff --git a/ui/uijoystick.c b/ui/uijoystick.c index 8b048935..eed6ee47 100644 --- a/ui/uijoystick.c +++ b/ui/uijoystick.c @@ -1,4 +1,4 @@ -/* uijoystick.c: Joystick emulation (using libjsw) +/* uijoystick.c: Fake joystick emulation support Copyright (c) 2003-2015 Darren Salt, Philip Kendall Copyright (c) 2015 UB880D @@ -24,248 +24,9 @@ */ -/* Usage note: build this from within a specific UI unless that UI implements - * its own joystick support using some other library. - * Inclusion as follows: - * #if !defined USE_JOYSTICK || defined HAVE_JSW_H - * # include "../uijoystick.c" - * #else - * // UI-specific code implementing the following (exported) functions - * #endif - */ - #include "config.h" -#include "input.h" #include "uijoystick.h" -#include "peripherals/joystick.h" - -#if defined USE_JOYSTICK && defined HAVE_JSW_H - -#include -#include -#include - -#include - -#include "libspectrum.h" - -#include "fuse.h" -#include "keyboard.h" -#include "settings.h" -#include "spectrum.h" -#include "machine.h" -#include "ui/ui.h" -#include "utils.h" - -static js_data_struct jsd[2]; - -static int js_button_states[2][NUM_JOY_BUTTONS]; - -static void poll_joystick( int which ); -static void do_axis( int which, double position, input_key negative, - input_key positive ); - -static int -init_stick( int which, const char *const device, - const char *const calibration ) -{ - switch( JSInit( &jsd[which], device, calibration, JSFlagNonBlocking ) ) { - - case JSSuccess: - if( JSLoadCalibrationUNIX( &jsd[which] ) && errno != ENOENT ) { - ui_error( UI_ERROR_ERROR, - "failed to read calibration for joystick %i: %s", which + 1, - strerror( errno ) ); - break; - } - - if( jsd[which].total_axises < 2 || jsd[which].total_buttons < 1 ) - { - ui_error( UI_ERROR_ERROR, "sorry, joystick %i (%s) is inadequate!", - which + 1, device ); - break; - } - return 0; - - case JSBadValue: - ui_error( UI_ERROR_ERROR, "failed to initialise joystick %i: %s", - which + 1, "invalid parameter/value"); - break; - - case JSNoAccess: - - /* FIXME: why is this commented out? */ -/* - ui_error (UI_ERROR_ERROR, - "failed to initialise joystick %i: %s", - which + 1, "cannot access device"); -*/ - break; - - case JSNoBuffers: - ui_error( UI_ERROR_ERROR, "failed to initialise joystick %i: %s", - which + 1, "not enough memory" ); - break; - - default: - ui_error( UI_ERROR_ERROR, "failed to initialise joystick %i", which + 1 ); - break; - - } - - JSClose( &jsd[which] ); - - return 1; -} - -static -int open_joystick( int which, const char *device, const char *calibration ) -{ - char path[ PATH_MAX ]; - - /* If we were given an explicit device to use for this joystick, try - only that */ - if( device && device[0] ) return init_stick( which, device, calibration ); - - /* Otherwise try /dev/input/js and /dev/js */ - snprintf( path, PATH_MAX, "/dev/input/js%d", which ); - if( !init_stick( which, path, calibration ) ) return 0; - - snprintf( path, PATH_MAX, "/dev/js%d", which ); - if( !init_stick( which, path, calibration ) ) return 0; - - /* Couldn't find this joystick */ - return 1; -} - -int -ui_joystick_init( void ) -{ - const char *cfgdir; - char *calibration; - int error; - size_t i, j; - - cfgdir = compat_get_config_path(); if( !cfgdir ) return 1; - - /* Default calibration file is ~/.joystick */ - calibration = libspectrum_new( char, strlen( cfgdir ) + - strlen( JSDefaultCalibration ) + 2 ); - - sprintf( calibration, "%s/%s", cfgdir, JSDefaultCalibration ); - - for( i = 0; i < 2; i++ ) { - for( j = 0; j < NUM_JOY_BUTTONS; j++ ) { - js_button_states[i][j] = 0; - } - } - - /* If we can't init the first, don't try the second */ - error = open_joystick( 0, settings_current.joystick_1, calibration ); - if( error ) { - libspectrum_free( calibration ); - return 0; - } - - error = open_joystick( 1, settings_current.joystick_2, calibration ); - if( error ) { - libspectrum_free( calibration ); - return 1; - } - - libspectrum_free( calibration ); - - return 2; -} - -void -ui_joystick_end( void ) -{ - int i; - for( i = 0; i < joysticks_supported; i++ ) JSClose( &jsd[i] ); -} - -void -ui_joystick_poll( void ) -{ - int i; - - for( i = 0; i < joysticks_supported; i++ ) poll_joystick( i ); -} - -static void -poll_joystick( int which ) -{ - js_data_struct *joystick; - double position; - int fire, buttons; - input_event_t event; - size_t i; - - joystick = &jsd[which]; - - if( JSUpdate( joystick ) != JSGotEvent ) return; - - position = JSGetAxisCoeffNZ( joystick, 0 ); - do_axis( which, position, INPUT_JOYSTICK_LEFT, INPUT_JOYSTICK_RIGHT ); - - position = JSGetAxisCoeffNZ( joystick, 1 ); - do_axis( which, position, INPUT_JOYSTICK_UP, INPUT_JOYSTICK_DOWN ); - - event.types.joystick.which = which; - - buttons = joystick->total_buttons; - if( buttons > NUM_JOY_BUTTONS ) buttons = NUM_JOY_BUTTONS; /* We support 'only' NUM_JOY_BUTTONS (15 as defined in ui/uijoystick.h) fire buttons */ - - for( i = 0; i < buttons; i++ ) { - - fire = JSGetButtonState( joystick, i ); - if( fire == JSButtonStateOn ) { - event.type = INPUT_EVENT_JOYSTICK_PRESS; - } else { - event.type = INPUT_EVENT_JOYSTICK_RELEASE; - } - - event.types.joystick.button = INPUT_JOYSTICK_FIRE_1 + i; - - if( js_button_states[which][i] != fire ) { - js_button_states[which][i] = fire; - input_event( &event ); - } - - } - -} - -static void -do_axis( int which, double position, input_key negative, input_key positive ) -{ - input_event_t event1, event2; - - event1.types.joystick.which = event2.types.joystick.which = which; - - event1.types.joystick.button = positive; - event2.types.joystick.button = negative; - - if( position == 0.0 ) { - event1.type = INPUT_EVENT_JOYSTICK_RELEASE; - event2.type = INPUT_EVENT_JOYSTICK_RELEASE; - } else if( position > 0.0 ) { - event1.type = INPUT_EVENT_JOYSTICK_PRESS; - event2.type = INPUT_EVENT_JOYSTICK_RELEASE; - } else { - event1.type = INPUT_EVENT_JOYSTICK_RELEASE; - event2.type = INPUT_EVENT_JOYSTICK_PRESS; - } - - input_event( &event1 ); - input_event( &event2 ); -} - -#else /* #if defined USE_JOYSTICK && defined HAVE_JSW_H */ - -/* No joystick library */ int ui_joystick_init( void ) @@ -282,5 +43,3 @@ void ui_joystick_poll( void ) { } - -#endif /* #if defined USE_JOYSTICK && defined HAVE_JSW_H */ diff --git a/ui/win32/win32joystick.c b/ui/win32/win32joystick.c index 1fd002ea..781520bf 100644 --- a/ui/win32/win32joystick.c +++ b/ui/win32/win32joystick.c @@ -39,11 +39,11 @@ #include "win32joystick.h" -#if !defined USE_JOYSTICK || defined HAVE_JSW_H +#ifndef USE_JOYSTICK #include "../uijoystick.c" -#else /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#else /* Functions to handle Joystick events */ #include "ui/ui.h" @@ -164,7 +164,7 @@ ui_joystick_end( void ) /* Initialization and unitialization is handled by MS Windows */ } -#endif /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#endif /* Win32 UI functions to handle Joystick options menus */ struct button_info { diff --git a/ui/win32/win32ui.c b/ui/win32/win32ui.c index c0141a70..122d48ec 100755 --- a/ui/win32/win32ui.c +++ b/ui/win32/win32ui.c @@ -256,7 +256,7 @@ fuse_window_proc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam ) another window */ return TRUE; -#if defined USE_JOYSTICK && !defined HAVE_JSW_H +#ifdef USE_JOYSTICK case MM_JOY1BUTTONDOWN: win32joystick_buttonevent( 0, 1, wParam ); @@ -282,7 +282,7 @@ fuse_window_proc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam ) win32joystick_move( 1, LOWORD( lParam ), HIWORD( lParam ) ); break; -#endif /* if defined USE_JOYSTICK && !defined HAVE_JSW_H */ +#endif /* #ifdef USE_JOYSTICK */ } return( DefWindowProc( hWnd, msg, wParam, lParam ) ); diff --git a/ui/xlib/xjoystick.c b/ui/xlib/xjoystick.c index b9c32729..c10694f5 100644 --- a/ui/xlib/xjoystick.c +++ b/ui/xlib/xjoystick.c @@ -26,11 +26,10 @@ #include "config.h" #include "peripherals/joystick.h" -#if !defined USE_JOYSTICK || defined HAVE_JSW_H -/* Fake joystick, or override UI-specific handling */ +#ifndef USE_JOYSTICK #include "../uijoystick.c" -#else /* #if !defined USE_JOYSTICK || defined HAVE_JSW_H */ +#else #include "../sdl/sdljoystick.c" From 70456ce9730328f52345c03ac87704df9becef84 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Mon, 18 May 2026 17:50:10 +0200 Subject: [PATCH 015/142] Fix GTK display on maximized windows 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. --- ui/gtk3/gtkdisplay.c | 57 ++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 9a7d47b6..7174f01d 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -107,8 +107,9 @@ static int display_updated = 0; static cairo_surface_t *surface = NULL; -/* The current size of the window (in units of DISPLAY_SCREEN_*) */ -static int gtkdisplay_current_size=1; +/* The current size of the cairo surface (in units of DISPLAY_SCREEN_*). + It may be smaller than the GTK drawing area if the window is maximized. */ +static int gtkdisplay_surface_size=1; /* Extra height used for menu and status bars */ static int extra_height = 0; @@ -224,8 +225,8 @@ uidisplay_init( int width, int height ) static void ensure_appropriate_surface( void ) { - /* Create a bigger surface for the new display size */ - float scale = (float)gtkdisplay_current_size / image_scale; + /* Recreate the cairo surface to match the new size */ + float scale = (float)gtkdisplay_surface_size / image_scale; if( surface ) cairo_surface_destroy( surface ); surface = @@ -245,10 +246,15 @@ drawing_area_resize( int width, int height, int force_scaler ) if( size > height / DISPLAY_SCREEN_HEIGHT ) size = height / DISPLAY_SCREEN_HEIGHT; + /* Clamp the size of the cairo surface. Window managers can ignore + GDK_HINT_MAX_SIZE when tiling or maximizing the window, which + would otherwise limit us to MAX_SCALE. */ + if( size > MAX_SCALE ) size = MAX_SCALE; + /* If we're the same size as before, no need to do anything else */ - if( size == gtkdisplay_current_size ) return 0; + if( size == gtkdisplay_surface_size ) return 0; - gtkdisplay_current_size = size; + gtkdisplay_surface_size = size; register_scalers( force_scaler ); @@ -265,7 +271,7 @@ static void register_scalers( int force_scaler ) { scaler_type scaler; - float drawing_area_scale, scaling_factor; + float surface_scale, scaling_factor; scaler_register_clear(); @@ -301,13 +307,13 @@ register_scalers( int force_scaler ) scaler = scaler_is_supported( current_scaler ) ? current_scaler : SCALER_NORMAL; - drawing_area_scale = (float)gtkdisplay_current_size / image_scale; + surface_scale = (float)gtkdisplay_surface_size / image_scale; scaling_factor = scaler_get_scaling_factor( current_scaler ); - /* Override scaler if the image doesn't fit well in the drawing area */ - if( force_scaler && drawing_area_scale != scaling_factor ) { + /* Override scaler if it doesn't match the surface scale */ + if( force_scaler && surface_scale != scaling_factor ) { - switch( gtkdisplay_current_size ) { + switch( gtkdisplay_surface_size ) { case 1: scaler = machine_current->timex ? SCALER_HALF : SCALER_NORMAL; break; case 2: scaler = machine_current->timex ? SCALER_NORMAL : SCALER_DOUBLESIZE; @@ -339,7 +345,7 @@ uidisplay_frame_end( void ) void uidisplay_area( int x, int y, int w, int h ) { - float scale = (float)gtkdisplay_current_size / image_scale; + float scale = (float)gtkdisplay_surface_size / image_scale; int scaled_x, scaled_y, i, yy; libspectrum_dword *palette; @@ -379,7 +385,8 @@ uidisplay_area( int x, int y, int w, int h ) static void gtkdisplay_area(int x, int y, int width, int height) { - int max_width, max_height; + int max_width, max_height, widget_width, widget_height; + int offset_x, offset_y; display_updated = 1; @@ -395,7 +402,15 @@ static void gtkdisplay_area(int x, int y, int width, int height) if( x + width < max_width ) width++; if( y + height < max_height ) height++; - gtk_widget_queue_draw_area( gtkui_drawing_area, x, y, width, height ); + /* Translate surface coordinates into widget coordinates. The surface is + centred when the drawing area is larger than the cairo surface. */ + widget_width = gtk_widget_get_allocated_width( gtkui_drawing_area ); + widget_height = gtk_widget_get_allocated_height( gtkui_drawing_area ); + offset_x = ( widget_width - max_width ) / 2; + offset_y = ( widget_height - max_height ) / 2; + + gtk_widget_queue_draw_area( gtkui_drawing_area, x + offset_x, y + offset_y, + width, height ); } int @@ -509,11 +524,23 @@ uidisplay_plot16( int x, int y, libspectrum_word data, static gboolean gtkdisplay_draw( GtkWidget *widget, cairo_t *cr, gpointer user_data ) { + int surface_width, surface_height, widget_width, widget_height; + int offset_x, offset_y; + /* Create a new surface for this gfx mode */ if( !surface ) ensure_appropriate_surface(); + /* Centre the surface in the drawing area. */ + surface_width = cairo_image_surface_get_width( surface ); + surface_height = cairo_image_surface_get_height( surface ); + widget_width = gtk_widget_get_allocated_width( widget ); + widget_height = gtk_widget_get_allocated_height( widget ); + + offset_x = ( widget_width - surface_width ) / 2; + offset_y = ( widget_height - surface_height ) / 2; + /* Repaint the drawing area */ - cairo_set_source_surface( cr, surface, 0, 0 ); + cairo_set_source_surface( cr, surface, offset_x, offset_y ); cairo_set_operator( cr, CAIRO_OPERATOR_SOURCE ); cairo_paint( cr ); From ab05875f030f877ef4695eb05bc054506359f529 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 19 May 2026 21:59:20 +1000 Subject: [PATCH 016/142] Use layout-aware key mapping in SDL2 keyboard handling 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). --- ui/sdl2/sdl2_keyboard.c | 58 +---------------------------------------- 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/ui/sdl2/sdl2_keyboard.c b/ui/sdl2/sdl2_keyboard.c index 03dd8b8d..e7168aa6 100644 --- a/ui/sdl2/sdl2_keyboard.c +++ b/ui/sdl2/sdl2_keyboard.c @@ -122,61 +122,6 @@ keysyms_map_t keysyms_map[] = { { 0, 0 } }; -static input_key -sdl2keyboard_physical_map( const SDL_KeyboardEvent *keyevent ) -{ - switch( keyevent->keysym.scancode ) { - case SDL_SCANCODE_A: return INPUT_KEY_a; - case SDL_SCANCODE_B: return INPUT_KEY_b; - case SDL_SCANCODE_C: return INPUT_KEY_c; - case SDL_SCANCODE_D: return INPUT_KEY_d; - case SDL_SCANCODE_E: return INPUT_KEY_e; - case SDL_SCANCODE_F: return INPUT_KEY_f; - case SDL_SCANCODE_G: return INPUT_KEY_g; - case SDL_SCANCODE_H: return INPUT_KEY_h; - case SDL_SCANCODE_I: return INPUT_KEY_i; - case SDL_SCANCODE_J: return INPUT_KEY_j; - case SDL_SCANCODE_K: return INPUT_KEY_k; - case SDL_SCANCODE_L: return INPUT_KEY_l; - case SDL_SCANCODE_M: return INPUT_KEY_m; - case SDL_SCANCODE_N: return INPUT_KEY_n; - case SDL_SCANCODE_O: return INPUT_KEY_o; - case SDL_SCANCODE_P: return INPUT_KEY_p; - case SDL_SCANCODE_Q: return INPUT_KEY_q; - case SDL_SCANCODE_R: return INPUT_KEY_r; - case SDL_SCANCODE_S: return INPUT_KEY_s; - case SDL_SCANCODE_T: return INPUT_KEY_t; - case SDL_SCANCODE_U: return INPUT_KEY_u; - case SDL_SCANCODE_V: return INPUT_KEY_v; - case SDL_SCANCODE_W: return INPUT_KEY_w; - case SDL_SCANCODE_X: return INPUT_KEY_x; - case SDL_SCANCODE_Y: return INPUT_KEY_y; - case SDL_SCANCODE_Z: return INPUT_KEY_z; - case SDL_SCANCODE_1: return INPUT_KEY_1; - case SDL_SCANCODE_2: return INPUT_KEY_2; - case SDL_SCANCODE_3: return INPUT_KEY_3; - case SDL_SCANCODE_4: return INPUT_KEY_4; - case SDL_SCANCODE_5: return INPUT_KEY_5; - case SDL_SCANCODE_6: return INPUT_KEY_6; - case SDL_SCANCODE_7: return INPUT_KEY_7; - case SDL_SCANCODE_8: return INPUT_KEY_8; - case SDL_SCANCODE_9: return INPUT_KEY_9; - case SDL_SCANCODE_0: return INPUT_KEY_0; - case SDL_SCANCODE_MINUS: return INPUT_KEY_minus; - case SDL_SCANCODE_EQUALS: return INPUT_KEY_equal; - case SDL_SCANCODE_LEFTBRACKET: return INPUT_KEY_bracketleft; - case SDL_SCANCODE_RIGHTBRACKET: return INPUT_KEY_bracketright; - case SDL_SCANCODE_BACKSLASH: return INPUT_KEY_backslash; - case SDL_SCANCODE_SEMICOLON: return INPUT_KEY_semicolon; - case SDL_SCANCODE_APOSTROPHE: return INPUT_KEY_apostrophe; - case SDL_SCANCODE_GRAVE: return INPUT_KEY_asciitilde; - case SDL_SCANCODE_COMMA: return INPUT_KEY_comma; - case SDL_SCANCODE_PERIOD: return INPUT_KEY_period; - case SDL_SCANCODE_SLASH: return INPUT_KEY_slash; - default: return INPUT_KEY_NONE; - } -} - static input_key sdl2keyboard_native_map( const SDL_KeyboardEvent *keyevent ) { @@ -190,8 +135,7 @@ sdl2keyboard_dispatch( input_event_type type, SDL_KeyboardEvent *keyevent ) input_event_t fuse_event; native_keysym = sdl2keyboard_native_map( keyevent ); - spectrum_keysym = sdl2keyboard_physical_map( keyevent ); - if( spectrum_keysym == INPUT_KEY_NONE ) spectrum_keysym = native_keysym; + spectrum_keysym = native_keysym; if( native_keysym == INPUT_KEY_NONE && spectrum_keysym == INPUT_KEY_NONE ) return; From d0ca391af716ba015634f160c2a55b445358d669 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 19 May 2026 22:27:45 +1000 Subject: [PATCH 017/142] Release pressed keys after snapshot restore (fixes bug #77) (thanks, jdaw1) --- snapshot.c | 3 +++ unittests/unittests.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/snapshot.c b/snapshot.c index 38176b94..55686bd0 100644 --- a/snapshot.c +++ b/snapshot.c @@ -26,6 +26,7 @@ #include "libspectrum.h" #include "fuse.h" +#include "keyboard.h" #include "machine.h" #include "memory_pages.h" #include "module.h" @@ -102,6 +103,8 @@ snapshot_copy_from( libspectrum_snap *snap ) machine_reset( 0 ); } + keyboard_release_all(); + module_snapshot_from( snap ); /* Need to reset memory_map_[read|write] after all modules have had a turn diff --git a/unittests/unittests.c b/unittests/unittests.c index a67c4e96..fd4147b7 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -30,6 +30,7 @@ #include "debugger/debugger.h" #include "fuse.h" +#include "keyboard.h" #include "machine.h" #include "mempool.h" #include "periph.h" @@ -51,6 +52,7 @@ #include "peripherals/ula.h" #include "peripherals/usource.h" #include "settings.h" +#include "snapshot.h" #include "bitmap.h" #include "rectangle.h" #include "unittests.h" @@ -327,6 +329,34 @@ bitmap_ops_test( void ) return 0; } +static int +snapshot_copy_from_releases_keyboard_test( void ) +{ + libspectrum_snap *snap; + int i; + + snap = libspectrum_snap_alloc(); + TEST_ASSERT( snap != NULL ); + TEST_ASSERT( snapshot_copy_to( snap ) == 0 ); + + keyboard_press( KEYBOARD_a ); + + for( i = 0; i < 8; i++ ) { + if( keyboard_return_values[i] != 0xff ) break; + } + TEST_ASSERT( i != 8 ); + + TEST_ASSERT( snapshot_copy_from( snap ) == 0 ); + + for( i = 0; i < 8; i++ ) { + TEST_ASSERT( keyboard_return_values[i] == 0xff ); + } + + TEST_ASSERT( libspectrum_snap_free( snap ) == 0 ); + + return 0; +} + static int utils_safe_strdup_test( void ) { @@ -1090,6 +1120,7 @@ unittests_run( void ) r += contention_test(); r += floating_bus_test(); r += floating_bus_merge_test(); + r += snapshot_copy_from_releases_keyboard_test(); r += utils_safe_strdup_test(); r += bitmap_ops_test(); r += mempool_test(); From 079a77e8a3a65edde41365c1ec381d476fb36bab Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Wed, 20 May 2026 10:30:44 +0200 Subject: [PATCH 018/142] desktop file: include the value of EXEEXT in the Exec field 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. --- data/Makefile.am | 1 + data/net.sourceforge.fuse_emulator.Fuse.desktop.in | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/data/Makefile.am b/data/Makefile.am index 50debfb6..c8ef07b4 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -84,6 +84,7 @@ data/net.sourceforge.fuse_emulator.Fuse.desktop: $(srcdir)/data/net.sourceforge. $(AM_V_GEN)$(MKDIR_P) data; \ list=`$(SED) -e '/.*/\1;/' -e t -e d < data/fuse.xml | tr -d '\n'` && \ $(SED) -e "s|@MIME_TYPES@|$$list|" \ + -e 's|@FUSE_BIN_EXTENSION@|$(EXEEXT)|' \ -e 's|@EXTRA_DESKTOP_CATEGORIES@|$(EXTRA_DESKTOP_CATEGORIES)|' \ < $(srcdir)/data/net.sourceforge.fuse_emulator.Fuse.desktop.in > $@ diff --git a/data/net.sourceforge.fuse_emulator.Fuse.desktop.in b/data/net.sourceforge.fuse_emulator.Fuse.desktop.in index 5d358939..f6485a6e 100644 --- a/data/net.sourceforge.fuse_emulator.Fuse.desktop.in +++ b/data/net.sourceforge.fuse_emulator.Fuse.desktop.in @@ -6,6 +6,6 @@ Name=Fuse GenericName=Spectrum Emulator Comment=Emulator of the 1980s ZX Spectrum home computer and its various clones MimeType=@MIME_TYPES@ -Exec=fuse %f +Exec=fuse@FUSE_BIN_EXTENSION@ %f Icon=net.sourceforge.fuse_emulator.Fuse Keywords=sinclair;zx;game;retro; From b1feba0b3555906edec3ff7dcd033a4ab65635ee Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 20 May 2026 21:00:51 +1000 Subject: [PATCH 019/142] Simplify sdl2keyboard_dispatch after removal of physical map --- .beads/issues.jsonl | 9 +++++++++ ui/sdl2/sdl2_keyboard.c | 12 +++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f698d0d0..5bc7587e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -9,6 +9,14 @@ {"_type":"issue","id":"fuse-wgi","title":"Minimal native SDL2 UI backend","description":"Introduce ui/sdl2/ as a native SDL2 frontend that reaches basic emulator usability using the existing software rendering and scaler flow. Focus this milestone on starting the emulator, running a responsive event loop, presenting frames in a window, and supporting essential keyboard-driven emulator use, not on text/layout-perfect fidelity or later modernization.","design":"This backend should stay native to ui/sdl2/ and reuse the existing software framebuffer and scaler flow for now. Prioritize dependable physical-key emulator usability over text-entry or layout fidelity in this early milestone.","acceptance_criteria":"Building with UI=sdl2 produces a working native SDL2 frontend under ui/sdl2/. Launching the emulator with UI=sdl2 starts successfully, opens an SDL2 window, and redraws visible emulator frames through the existing software/scaler path. The SDL2 event loop remains responsive during normal emulator use, including routine input and window interaction. Essential keyboard-driven emulator use works well enough for routine control and testing. Quit and shutdown paths exit cleanly without leaving the process hung or the SDL2 UI in a broken state.","status":"closed","priority":1,"issue_type":"feature","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:12Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T12:41:30Z","closed_at":"2026-04-11T12:41:30Z","close_reason":"SDL2 backend now presents visible emulator frames correctly; verified by user after fixing the 16-bit presentation path.","dependencies":[{"issue_id":"fuse-wgi","depends_on_id":"fuse-pir","type":"blocks","created_at":"2026-04-11T21:57:05Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"fuse-pir","title":"Build/configure groundwork for UI=sdl2 and SDL2 sound selection","description":"Add configure and build-system groundwork for a distinct UI=sdl2 backend and a separate SDL2 sound backend without regressing existing SDL1 codepaths. This task is only about selection, dependency plumbing, and default resolution rules, while keeping UI and audio independently selectable. When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override.","design":"Keep this limited to configure/build and backend selection plumbing. Do not introduce a hybrid SDL1/SDL2 runtime path or an internal SDL1 compatibility layer just to simplify option handling.","acceptance_criteria":"Configure/build logic exposes UI=sdl2 as a distinct selectable UI backend rather than folding it into the SDL1 path. Build/dependency handling distinguishes SDL1 and SDL2 requirements so SDL2-enabled targets can be built without rewriting the SDL1 backend path. UI and audio selection remain independent, so UI=sdl2 can be paired with a non-SDL2 audio backend and SDL2 sound can be selected without requiring UI=sdl2. When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. Existing SDL1 UI and audio builds continue to configure and build unchanged when SDL2 support is disabled or not selected.","status":"closed","priority":1,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:06Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T12:17:46Z","closed_at":"2026-04-11T12:17:46Z","close_reason":"Implemented SDL2 build/configure groundwork with UI=sdl2 selection, SDL2 sound selection/defaulting, temporary scaffolding sources, and SDL2 UI exclusion of SDL1 sound.","dependency_count":0,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fuse-7en","title":"Epic: native SDL2 UI and SDL2 sound backends","description":"Track the SDL2 migration as separate native backends in ui/sdl2/ and sound/sdl2sound.c. Sequence the work as build/configure groundwork first, then minimal SDL2 UI and SDL2 sound, then display cleanup, fullscreen/window behavior, and keyboard fidelity, with joystick, mouse/grab, and final parity cleanup staying as later follow-up. Keep UI and audio independently selectable, preserve the existing software rendering and scaler flow for this tranche, and clarify the default sound rule as: When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. Explicitly exclude any hybrid SDL1/SDL2 backend, internal SDL1 compatibility layer, or renderer/texture modernization in this epic.","design":"Architectural boundaries for this epic: add native SDL2 implementations as separate modules in ui/sdl2/ and sound/sdl2sound.c, keep backend selection independent between UI and audio, and treat renderer/texture modernization as separate future work rather than a prerequisite.","acceptance_criteria":"The epic description and child issue set explicitly cover build/configure groundwork, minimal SDL2 UI, SDL2 sound, display cleanup, fullscreen/window management, keyboard fidelity, deferred joystick, deferred mouse/grab, and final parity cleanup. The documented sequencing matches the current dependency graph, including fuse-pir blocking fuse-wgi and fuse-k8e, fuse-n5p depending on both fuse-wgi and fuse-k8e, and later input/window follow-up work remaining downstream. The epic text explicitly states that UI and audio stay independently selectable, the existing software/scaler flow stays in scope for this tranche, and the default sound rule is: When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. The epic text explicitly excludes any hybrid SDL1/SDL2 backend, internal SDL1 compatibility layer, or renderer/texture modernization in this tranche.","status":"closed","priority":1,"issue_type":"feature","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:55:31Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T01:58:45Z","closed_at":"2026-04-12T01:58:45Z","close_reason":"All SDL2 UI and SDL2 sound backend milestone work is now complete, documented, formatted, and locally verified; remaining future improvements can be tracked separately from this epic.","dependencies":[{"issue_id":"fuse-7en","depends_on_id":"fuse-3tt","type":"blocks","created_at":"2026-04-11T21:57:15Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-4vn","type":"blocks","created_at":"2026-04-11T21:57:15Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-e8p","type":"blocks","created_at":"2026-04-11T21:57:16Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-k8e","type":"blocks","created_at":"2026-04-11T21:57:13Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-l6g","type":"blocks","created_at":"2026-04-11T21:57:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-n5p","type":"blocks","created_at":"2026-04-11T21:57:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-pir","type":"blocks","created_at":"2026-04-11T21:57:12Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-qqg","type":"blocks","created_at":"2026-04-11T21:57:17Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-wgi","type":"blocks","created_at":"2026-04-11T21:57:13Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":9,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-88t","title":"Plan PAL TV enhancement branch inspired by Blargg work","description":"Create a concrete implementation plan for a new branch that enhances the PAL TV scalers using ideas and lessons from the Blargg NTSC work, without carrying NTSC-specific code or GUI tuning UI.\n\nWhy:\n- We want to keep the useful lessons from the Blargg branch while avoiding NTSC-specific artifact modelling for mainstream Spectrum use.\n- PAL TV enhancement should stand on its own as a cleaner, more historically appropriate path for normal Spectrum machines.\n- Command-line/settings exposure is sufficient; a GUI filter-options dialog is intentionally out of scope.\n\nScope / plan:\n1. Start a fresh branch from latest master rather than building directly on the Blargg branch.\n2. Reuse concepts, not the NTSC implementation:\n - generic scaler capability flags if needed\n - robust 16-bit/32-bit filter input handling patterns\n - settings.dat-based command-line parameter exposure\n3. Do not carry forward these NTSC-specific pieces:\n - ui/scaler/snes_ntsc.c\n - ui/scaler/snes_ntsc.h\n - ui/scaler/snes_ntsc_impl.h\n - ui/scaler/snes_ntsc_config.h\n - ui/scaler/snes_ntsc.txt\n - ui/gtk3/filter_options.c\n - menu_data.dat filter-options menu additions\n - menu.h filter-options callback additions\n - widget/menu fallback handling for filter-options UI\n4. Focus the PAL work on improving existing PAL TV scaler family behavior:\n - PAL TV\n - PAL TV 2x\n - PAL TV 3x\n - PAL TV 4x\n5. Keep the configuration surface small and command-line oriented. Candidate tunables:\n - paltv_brightness\n - paltv_contrast\n - paltv_saturation\n - paltv_sharpness\n - paltv_bleed\n - optionally paltv_artifacts if justified\n Avoid NTSC-leaning parameters unless the PAL model clearly benefits from them.\n6. Preferred implementation direction:\n - preserve frame-stable output if possible\n - avoid any full-refresh requirement unless testing proves it is genuinely needed\n - improve PAL-style blur/bleed/fringing/crosstalk rather than simulating NTSC frame-phase behaviour\n7. File-by-file starting checklist for the future branch:\n - inspect ui/scaler/scalers.c for reusable helper structure and input-conversion patterns\n - inspect ui/scaler/scaler.h and ui/scaler/scaler.c for any generic flag/integration ideas worth porting\n - update settings.dat with only PAL-oriented parameters\n - implement PAL filter changes inside the existing PAL TV scaler family\n - verify across 16-bit and 32-bit UI paths with make and make check\n\nAcceptance:\n- There is a clear branch plan for PAL TV enhancement that can be followed later.\n- The plan explicitly excludes NTSC code and GUI tuning UI.\n- The plan identifies the most valuable reusable ideas from the Blargg branch and where to apply them.","status":"open","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T10:24:11Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T10:24:11Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-nyt","title":"Move full-refresh scaler handling into UI frame_end","description":"Keep SCALER_FLAGS_FULL_REFRESH but remove display-layer policy. Have UIs synthesize a full-screen update in uidisplay_frame_end() when needed.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T09:46:50Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T09:47:44Z","started_at":"2026-05-20T09:47:01Z","closed_at":"2026-05-20T09:47:44Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-07s","title":"Generalise Blargg full-refresh handling with scaler flag","description":"Replace the Blargg-specific redraw logic with a generic scaler flag, while keeping the frame scheduling in the display pipeline where it belongs.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T09:38:19Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T09:39:01Z","started_at":"2026-05-20T09:38:39Z","closed_at":"2026-05-20T09:39:01Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-nws","title":"Correct Blargg NTSC channel extraction","description":"Follow-up: the first SDL2 colour conversion fix changed the failure mode but output is still wrong. Re-check snes_ntsc output packing and extract channels correctly.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:24:55Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:26:16Z","started_at":"2026-05-20T08:25:08Z","closed_at":"2026-05-20T08:26:16Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-1t7","title":"Fix SDL2 Blargg NTSC colour mapping","description":"Investigate incorrect colour mapping when using the Blargg NTSC scaler on SDL2 and correct the pixel format/channel handling.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:21:05Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:23:28Z","started_at":"2026-05-20T08:21:13Z","closed_at":"2026-05-20T08:23:28Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-hmu","title":"Expose Blargg NTSC scalers in widget UI","description":"Investigate why the ported Blargg NTSC scalers do not appear in the widget UI scaler list and fix the registration or selection logic.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:18:53Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:19:41Z","started_at":"2026-05-20T08:19:07Z","closed_at":"2026-05-20T08:19:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-e0n","title":"Fix build fallout from Blargg NTSC port","description":"Resolve build warnings and unit test link failure introduced while porting patches-374-blargg-ntsc-filter-patch onto latest master.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:16:43Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:17:52Z","started_at":"2026-05-20T08:17:17Z","closed_at":"2026-05-20T08:17:52Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-ebn","title":"Port patches-374-blargg-ntsc-filter-patch to latest master","description":"Port the changes from branch patches-374-blargg-ntsc-filter-patch onto a new branch created from the latest master without pushing anything.","status":"closed","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:03:44Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:10:12Z","closed_at":"2026-05-20T08:10:12Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-rlo","title":"Fix SDL2 tape activity icon position on Timex machines","description":"SDL2 UI draws the tape activity icon around the middle of the screen after switching to a Timex machine such as the TC2048. Investigate how the icon position is computed for Timex display layouts and update the SDL2 path so the indicator is drawn in the proper screen position.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-17T10:27:54Z","created_by":"Fredrick Meunier","updated_at":"2026-05-17T10:29:21Z","started_at":"2026-05-17T10:27:58Z","closed_at":"2026-05-17T10:29:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-0yb","title":"Model SDL2 fullscreen scaler transitions explicitly","description":"Implement a small SDL2 display scaler state model so fullscreen/windowed transitions match SDL1 behavior more closely while keeping the options dialog truthful. Current SDL2 fullscreen code uses ad hoc windowed_scaler preservation and current_scaler inference, which causes edge cases when filters/scalers are changed in fullscreen. Desired behavior: current_scaler remains the actual scaler in use; entering fullscreen keeps the current scaler if it fits; if substitution is needed, prefer a scaler in the same family/class (TV, PAL TV, AdvMAME, HQ, normal, Timex) before generic fallback; exiting fullscreen restores only when the scaler was automatically substituted; explicit user scaler choices while fullscreen should be respected and become the actual scaler shown in options. Consider states like WINDOWED, FULLSCREEN_NATIVE, FULLSCREEN_AUTO and explicit transition events for fullscreen toggle, user scaler change, machine change, and startup/init.","notes":"Reworked the explicit-scaler-change signaling to avoid the earlier ui/scaler/scaler.[ch] hook. Removed scaler_selecting_scaler() entirely. Added a UI-level one-shot hotswap reason in ui/uidisplay.h + ui.c: uidisplay_set_next_hotswap_reason() / uidisplay_take_next_hotswap_reason(). menu.c marks explicit scaler selections before calling scaler_select_scaler(); SDL2 consumes that reason inside uidisplay_hotswap_gfx_mode(). Reverified with make -j4 fuse unittests/sdl2displaytest unittests/sdl2scalerstatetest \u0026\u0026 ./unittests/sdl2displaytest \u0026\u0026 ./unittests/sdl2scalerstatetest \u0026\u0026 make check.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-12T08:53:52Z","created_by":"Fredrick Meunier","updated_at":"2026-05-12T11:38:33Z","started_at":"2026-05-12T09:04:09Z","dependencies":[{"issue_id":"fuse-0yb","depends_on_id":"fuse-8k3","type":"blocks","created_at":"2026-05-12T18:53:57Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-so3.6","title":"Hide SDL2 cursor while fullscreen","description":"SDL2 desktop fullscreen currently leaves the host mouse pointer visible when entering fullscreen if the mouse was not already grabbed. SDL1 hides the cursor while fullscreen and restores it when leaving. Match that behaviour for SDL2 fullscreen transitions.","acceptance_criteria":"Entering SDL2 fullscreen hides the host cursor even when the mouse was not previously grabbed; leaving fullscreen restores the cursor unless another grab state requires it hidden; SDL1 behaviour is documented as the reference.","notes":"SDL1 reference: ui/sdl/sdldisplay.c hides the cursor after fullscreen hotswap when settings_current.full_screen or ui_mouse_grabbed is true, and shows it otherwise. SDL2 fix exposes the existing cursor visibility helper and applies the same fullscreen-or-grabbed visibility rule after SDL2 window recreation.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-10T00:24:49Z","created_by":"Fredrick Meunier","updated_at":"2026-05-10T00:28:05Z","started_at":"2026-05-10T00:24:51Z","closed_at":"2026-05-10T00:28:05Z","close_reason":"Implemented SDL2 desktop fullscreen unit coverage/manual notes in bead and matched SDL1 cursor hiding behaviour; verified focused SDL2 display/mouse tests and fuse build.","labels":["fullscreen","sdl2"],"dependencies":[{"issue_id":"fuse-so3.6","depends_on_id":"fuse-so3","type":"parent-child","created_at":"2026-05-10T10:24:49Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -43,6 +51,7 @@ {"_type":"issue","id":"fuse-h6s","title":"Add SDL2 presentation helper test seam","description":"Create a minimal, C89-friendly test seam for SDL2 presentation logic by extracting pure helper functions and simple inline view structs without abstracting hot-path rendering through indirect calls. Use it to add unit coverage for fullscreen offset/layout and scaler-selection decisions that currently live inside the SDL2 display backend.","status":"closed","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T23:09:12Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T23:23:45Z","closed_at":"2026-04-11T23:23:45Z","close_reason":"Added a C89-friendly SDL2 presentation helper seam with unit coverage for fullscreen offsets, rect transforms, icon placement, and fullscreen scaler selection without introducing hot-path indirection.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-l6g","title":"Fullscreen and window management for SDL2 UI","description":"Implement SDL2-native fullscreen transitions and window management behavior once core presentation is correct. Cover sizing, mode changes, and window lifecycle behavior needed for routine use, without expanding scope into deferred joystick or mouse/grab work.","acceptance_criteria":"The SDL2 UI supports usable fullscreen and windowed operation with correct transitions, sizing, and window lifecycle behavior on top of the cleaned-up display path; remaining joystick and mouse/grab concerns stay deferred.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:29Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T14:12:31Z","closed_at":"2026-04-11T14:12:31Z","close_reason":"SDL2 fullscreen/window handling now supports the General options fullscreen toggle, centers fullscreen presentation, adjusts scaler choice for fullscreen use, restores the windowed scaler on exit, and was verified locally by the user.","dependencies":[{"issue_id":"fuse-l6g","depends_on_id":"fuse-n5p","type":"blocks","created_at":"2026-04-11T21:57:08Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fuse-n5p","title":"Display correctness and presentation cleanup for SDL2 UI","description":"Clean up SDL2 display presentation after the minimal SDL2 UI and SDL2 sound milestones are in place, while explicitly keeping the existing software rendering and scaler flow for this tranche. Focus on concrete correctness issues such as pixel format handling, redraw/expose paths, presentation stability, and scaling behavior before fullscreen or broader polish.","design":"This is a display-correctness pass on top of the existing software/scaler path, not a rendering-architecture rewrite. Keep SDL2 presentation behavior compatible with the current frame production flow so later fullscreen and input work build on a stable base.","acceptance_criteria":"SDL2 presentation handles the intended pixel formats correctly without obvious channel swaps, corruption, or format-mismatch artifacts in normal emulator use. Frame presentation is stable over time, without obvious flicker, runaway redraw behavior, or stale-frame display during routine operation. Redraw and expose/update paths refresh the window correctly after events that require repainting. Scaling and final presentation match the expected behavior of the current software rendering and scaler flow rather than introducing a new renderer/texture pipeline. The task leaves the software/scaler flow in place and does not expand scope into renderer/texture modernization.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:24Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T13:23:46Z","closed_at":"2026-04-11T13:23:46Z","close_reason":"SDL2 display presentation now handles redraw/expose correctly, keeps status/media overlays in sync, and was verified locally by the user.","dependencies":[{"issue_id":"fuse-n5p","depends_on_id":"fuse-k8e","type":"blocks","created_at":"2026-04-11T21:57:07Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-n5p","depends_on_id":"fuse-wgi","type":"blocks","created_at":"2026-04-11T21:57:06Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":3,"comment_count":0} +{"_type":"issue","id":"fuse-5lz","title":"Audit Blargg NTSC UI-specific behaviour","description":"Review UI-specific integration points for the Blargg NTSC scaler across SDL, SDL2, Xlib, GTK3, Win32, and others to identify any remaining quirks beyond the shared 16-bit input fix.","status":"closed","priority":3,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:33:36Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:33:53Z","closed_at":"2026-05-20T08:33:53Z","close_reason":"Audited UI integration. Shared 16-bit input conversion in ui/scaler/scalers.c covers SDL1 and Xlib; GTK3, Win32, and Wii use 32-bit scaler paths and do not have the same format mismatch. No additional code changes needed from this audit.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-nqf","title":"Use libspectrum_new0 in SDL2 display allocations","description":"Replace the remaining direct calloc usage in the SDL2 display backend with the project-standard typed libspectrum_new0 allocator and matching libspectrum_free cleanup for consistency with the rest of the codebase.","status":"closed","priority":3,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-12T03:20:30Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T03:21:27Z","closed_at":"2026-04-12T03:21:27Z","close_reason":"Replaced the remaining direct calloc usage in the SDL2 display backend with libspectrum_new0 and paired tmp_screen buffer cleanup with libspectrum_free for allocator consistency.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-qqg","title":"Final SDL2 parity cleanup","description":"Track remaining SDL2 backend parity gaps and cleanup once build/configure, minimal UI, SDL2 sound, display, fullscreen/window, keyboard, joystick, and mouse/grab work are all complete. Use this to capture residual polish rather than front-loading modernization.","acceptance_criteria":"Remaining SDL2 parity gaps are enumerated and cleaned up after the earlier milestones finish; no major known parity blocker remains for the SDL2 UI and SDL2 sound backends; renderer/texture modernization remains out of scope unless separately justified.","status":"closed","priority":3,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:53Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T00:55:53Z","closed_at":"2026-04-12T00:55:53Z","close_reason":"Cleaned up the remaining practical SDL2 parity gaps by restoring mouse grab across window recreation and allowing held-key repeat through the widget UI. No major known SDL2 parity blocker remains; SDL1-style fixed fullscreen mode selection remains out of scope for this cleanup.","dependencies":[{"issue_id":"fuse-qqg","depends_on_id":"fuse-4vn","type":"blocks","created_at":"2026-04-11T21:57:11Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-qqg","depends_on_id":"fuse-e8p","type":"blocks","created_at":"2026-04-11T21:57:12Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"fuse-e8p","title":"Mouse and grab behavior for SDL2 UI","description":"Add SDL2 mouse handling and grab/capture behavior only after the core SDL2 milestones are stable. Treat this as deferred follow-up work after build/configure, minimal UI, SDL2 sound, display cleanup, fullscreen/window management, and keyboard fidelity.","acceptance_criteria":"SDL2 mouse input and grab/capture behavior are implemented after the prerequisite SDL2 UI milestones are complete; behavior is usable without requiring a hybrid SDL1/SDL2 path or unrelated renderer modernization.","status":"closed","priority":3,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:47Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T00:44:36Z","closed_at":"2026-04-12T00:44:36Z","close_reason":"Implemented SDL2 mouse grab/release behavior with relative mode and window grab, added unit tests for mouse grab policy, fixed fullscreen status icon null-safety, and verified the runtime behavior locally with acceptable residual macOS cursor-hide quirks.","dependencies":[{"issue_id":"fuse-e8p","depends_on_id":"fuse-3tt","type":"blocks","created_at":"2026-04-11T21:57:10Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-e8p","depends_on_id":"fuse-l6g","type":"blocks","created_at":"2026-04-11T21:57:10Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} diff --git a/ui/sdl2/sdl2_keyboard.c b/ui/sdl2/sdl2_keyboard.c index e7168aa6..9da67b31 100644 --- a/ui/sdl2/sdl2_keyboard.c +++ b/ui/sdl2/sdl2_keyboard.c @@ -131,18 +131,16 @@ sdl2keyboard_native_map( const SDL_KeyboardEvent *keyevent ) static void sdl2keyboard_dispatch( input_event_type type, SDL_KeyboardEvent *keyevent ) { - input_key native_keysym, spectrum_keysym; + input_key keysym; input_event_t fuse_event; - native_keysym = sdl2keyboard_native_map( keyevent ); - spectrum_keysym = native_keysym; + keysym = sdl2keyboard_native_map( keyevent ); - if( native_keysym == INPUT_KEY_NONE && - spectrum_keysym == INPUT_KEY_NONE ) return; + if( keysym == INPUT_KEY_NONE ) return; fuse_event.type = type; - fuse_event.types.key.native_key = native_keysym; - fuse_event.types.key.spectrum_key = spectrum_keysym; + fuse_event.types.key.native_key = keysym; + fuse_event.types.key.spectrum_key = keysym; input_event( &fuse_event ); } From 8afbb79b1835ba654d4ac8fd7744e080a37f021f Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Wed, 27 May 2026 16:38:23 +0200 Subject: [PATCH 020/142] Don't ignore the embedded pause at the end of a block 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) --- tape.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tape.c b/tape.c index 9a8530da..d91218a1 100644 --- a/tape.c +++ b/tape.c @@ -61,6 +61,9 @@ int tape_modified; /* Is the emulated tape deck playing? */ int tape_playing; +/* Do we have to stop the tape on the next edge? */ +static int tape_stop_pending = 0; + /* Was the tape playing started automatically? */ static int tape_autoplay; @@ -133,6 +136,7 @@ tape_init( void *context ) so we can't update the statusbar */ tape_playing = 0; tape_microphone = 0; + tape_stop_pending = 0; next_tape_edge_tstates = 0; @@ -632,6 +636,7 @@ tape_play( int autoplay ) tape_playing = 1; tape_autoplay = autoplay; tape_microphone = 0; + tape_stop_pending = 0; event_remove_type( tape_mic_off_event ); @@ -696,6 +701,7 @@ tape_stop( void ) if( tape_playing ) { tape_playing = 0; + tape_stop_pending = 0; ui_statusbar_update( UI_STATUSBAR_ITEM_TAPE, UI_STATUSBAR_STATE_INACTIVE ); loader_tape_stop(); @@ -859,6 +865,12 @@ tape_next_edge( libspectrum_dword last_tstates, int from_acceleration ) libspectrum_dword edge_tstates; int flags; + /* If a stop was deferred, carry it out now */ + if( tape_stop_pending ) { + tape_stop(); + return; + } + /* If the tape's not playing, just return */ if( ! tape_playing ) return; @@ -887,8 +899,9 @@ tape_next_edge( libspectrum_dword last_tstates, int from_acceleration ) sound_beeper( last_tstates, tape_microphone ); - /* If we've been requested to stop the tape, do so and then - return without stacking another edge */ + /* If we've been requested to stop the tape, do it on the next tape + event so that this final edge (e.g. the embedded pause at the end + of the tape) is still played */ if( ( flags & LIBSPECTRUM_TAPE_FLAGS_STOP ) || ( ( flags & LIBSPECTRUM_TAPE_FLAGS_STOP48 ) && ( !( libspectrum_machine_capabilities( machine_current->machine ) & @@ -898,12 +911,14 @@ tape_next_edge( libspectrum_dword last_tstates, int from_acceleration ) ) ) { - tape_stop(); - return; + tape_stop_pending = 1; } - /* If that was the end of a block, update the browser */ - if( flags & LIBSPECTRUM_TAPE_FLAGS_BLOCK ) { + /* If that was the end of a block, update the browser. This is skipped + if tape_stop_pending was set above: at the end of the tape both + STOP and BLOCK are set. The trap check below could undo the deferred + stop and drop the final edge. */ + if( ( flags & LIBSPECTRUM_TAPE_FLAGS_BLOCK ) && !tape_stop_pending ) { ui_tape_browser_update( UI_TAPE_BROWSER_SELECT_BLOCK, NULL ); From 1df203a18200e06ed30ca72bc0d438031939d2e9 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Fri, 29 May 2026 11:24:14 +0200 Subject: [PATCH 021/142] gtk3: Fix display artifacts when resizing windows on Wayland 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 70456ce9730328f52345c03ac87704df9becef84 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. --- ui/gtk3/gtkdisplay.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 7174f01d..9eeb64ca 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -250,6 +250,7 @@ drawing_area_resize( int width, int height, int force_scaler ) GDK_HINT_MAX_SIZE when tiling or maximizing the window, which would otherwise limit us to MAX_SCALE. */ if( size > MAX_SCALE ) size = MAX_SCALE; + if( size < 1 ) size = 1; /* If we're the same size as before, no need to do anything else */ if( size == gtkdisplay_surface_size ) return 0; @@ -327,7 +328,8 @@ register_scalers( int force_scaler ) } } - scaler_select_scaler( scaler ); + /* Activate the scaler without trying to resize the GTK window */ + scaler_activate_scaler( scaler ); } void @@ -539,9 +541,13 @@ gtkdisplay_draw( GtkWidget *widget, cairo_t *cr, gpointer user_data ) offset_x = ( widget_width - surface_width ) / 2; offset_y = ( widget_height - surface_height ) / 2; - /* Repaint the drawing area */ + /* Fill the drawing area with black. This clears the margins around + the source if the drawing area is larger */ + cairo_set_source_rgb( cr, 0, 0, 0 ); + cairo_paint( cr ); + + /* Repaint the surface on top */ cairo_set_source_surface( cr, surface, offset_x, offset_y ); - cairo_set_operator( cr, CAIRO_OPERATOR_SOURCE ); cairo_paint( cr ); return FALSE; From 6932086f7394e452d7638d0188d4f00f483aa220 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Fri, 29 May 2026 12:12:36 +0200 Subject: [PATCH 022/142] gtk3: Stop forcing synchronous redraws to fix Wayland artifacts 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. --- ui/gtk3/gtkdisplay.c | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 9eeb64ca..46ebe362 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -103,8 +103,6 @@ typedef enum { FORMAT_x8b8g8r8 /* GdkRGB */ } colour_format_t; -static int display_updated = 0; - static cairo_surface_t *surface = NULL; /* The current size of the cairo surface (in units of DISPLAY_SCREEN_*). @@ -335,13 +333,6 @@ register_scalers( int force_scaler ) void uidisplay_frame_end( void ) { - if( display_updated ) { - gdk_window_process_updates( gtk_widget_get_window( gtkui_drawing_area ), - FALSE ); - display_updated = 0; - } - - return; } void @@ -390,8 +381,6 @@ static void gtkdisplay_area(int x, int y, int width, int height) int max_width, max_height, widget_width, widget_height; int offset_x, offset_y; - display_updated = 1; - if( width <= 0 || height <= 0 ) return; max_width = surface ? cairo_image_surface_get_width( surface ) : width; From 6b91805f458a540b804ff034b0bb774bd28d4039 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Fri, 29 May 2026 12:35:21 +0200 Subject: [PATCH 023/142] gtk3: Stop forcing X11 over Wayland The Wayland backend is now usable enough so there's no need to override the GDK defaults. (fixes bug #367) --- ui/gtk3/gtkui.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/gtk3/gtkui.c b/ui/gtk3/gtkui.c index a4b2ded3..0ab736f3 100644 --- a/ui/gtk3/gtkui.c +++ b/ui/gtk3/gtkui.c @@ -152,9 +152,6 @@ ui_init( int *argc, char ***argv ) GtkAccelGroup *accel_group; GtkSettings *settings; - /* The Wayland output is buggy, see #367 */ - gdk_set_allowed_backends( "quartz,win32,mir,x11,*" ); - gtk_init(argc,argv); g_resources_register( gtkui_get_resource() ); From f7f561f1e40fde46a020c8b756de535495d8c35a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 30 May 2026 12:19:40 +1000 Subject: [PATCH 024/142] Fixes for GitHub actions builds with new SDL autoconf --- .beads/issues.jsonl | 20 ++++++- .../build_libspectrum_windows_sub.yml | 42 +++++++-------- .github/workflows/build_linux.yml | 8 +-- .github/workflows/build_macos.yml | 10 ++-- .github/workflows/build_windows.yml | 8 +-- .github/workflows/build_windows_sub.yml | 52 +++++++++---------- .github/workflows/windows_release.yml | 37 +++++++------ 7 files changed, 97 insertions(+), 80 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5bc7587e..be0b92ee 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,5 @@ +{"_type":"issue","id":"fuse-z7i","title":"Add regression coverage for PAL TV scaler tuning output","description":"Why:\\n- Manual screenshot checks show the PAL TV tuning work is not visibly affecting output as expected.\\n- We need automated validation of scaler output so future tuning changes can be checked against concrete expected pixel results.\\n\\nWhat:\\n- Inspect existing test infrastructure for display/scaler coverage.\\n- Add focused regression tests or a small test harness for PAL TV scaler output, preferably covering neutral/default and non-default tuning settings.\\n- Use the tests to confirm whether the PAL TV tuning path is active and whether settings change output.\\n\\nAcceptance:\\n- There is automated coverage that exercises PAL TV scaler output.\\n- Tests distinguish at least one tuned case from the baseline case.\\n- The new coverage is documented by the verification command used.","notes":"Added unittests/paltvscalertest.c plus unittests/Makefile.am wiring for a focused PAL TV scaler regression test. The harness builds a synthetic colourful input row, checks that zero PAL tuning matches a copied legacy PAL TV 1x reference exactly, and checks that Blargg-inspired tuned settings differ from that baseline. While writing the test it exposed that the neutral path no longer matched legacy output; fixed scaler_PalTV so zero tuning uses the exact legacy path while non-zero tuning uses the cached tuned path. Verified with: gcc -std=gnu23 -DHAVE_CONFIG_H -I. -I/Users/fred/Documents/Code/HomeGrown/fuse-emulator-trunk-vanilla/ls/include -DFUSEDATADIR='\"/usr/local/share/fuse\"' -I/opt/homebrew/opt/libpng/include/libpng16 -I/opt/homebrew/include -I/opt/homebrew/include/SDL2 -D_THREAD_SAFE -Wall -Wwrite-strings -pthread -g -O2 -DSCALER_DATA_SIZE=4 unittests/paltvscalertest.c ui/scaler/scalers.c -L/Users/fred/Documents/Code/HomeGrown/fuse-emulator-trunk-vanilla/ls/lib -lspectrum -o unittests/paltvscalertest \u0026\u0026 ./unittests/paltvscalertest ; also rebuilt scalers with make ui/scaler/scalers16.o ui/scaler/scalers32.o V=1","status":"closed","priority":1,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T12:12:24Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T12:21:03Z","started_at":"2026-05-20T12:12:35Z","closed_at":"2026-05-20T12:21:03Z","close_reason":"Added PAL TV scaler regression coverage and used it to fix the neutral PAL tuning path semantics.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-ymo","title":"PAL TV 1x tuning defaults are visually too strong","description":"Why:\\n- After implementing fuse-vzv, the PAL TV 1x tuning path appears noticeably stronger than intended in normal use.\\n- The new tuning model should preserve conservative near-legacy behaviour at default settings.\\n\\nWhat:\\n- Inspect the PAL TV 1x tuning math added in ui/scaler/scalers.c.\\n- Reduce the effective strength of the default/neutral path so PAL TV 1x stays close to prior appearance.\\n- Keep the cached tuning structure, but tune the coefficient mapping or neutral behaviour as needed.\\n\\nAcceptance:\\n- Default PAL TV 1x output stays visually close to the pre-tuning implementation.\\n- Tuning controls still have an effect, but not an exaggerated one at typical values.\\n- Diff remains focused to PAL TV scaler internals.","notes":"Added a focused PAL TV scaler regression harness that proved the earlier 'neutral' tuned path still differed from legacy PAL TV output. Adjusted scaler_PalTV so all-zero PAL tuning now takes the exact legacy PAL TV 1x path, while non-zero settings use the cached tuned path; kept the Blargg-inspired defaults non-zero so normal startup still exercises tuning. Verification used the new paltvscalertest harness plus make ui/scaler/scalers16.o ui/scaler/scalers32.o V=1.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T11:58:53Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T12:21:08Z","started_at":"2026-05-20T11:59:01Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-8k3","title":"SDL2 fullscreen startup recreates/resizes window visibly","description":"Address native SDL2 UI startup behaviour where ./fuse --full-screen first maps a windowed SDL window and/or recreates the SDL window several times before settling into fullscreen. Analysis points to ui/sdl2/sdl2_display.c: sdl2display_create_window() creates SDL_WINDOW_SHOWN then calls SDL_SetWindowFullscreen(), and startup scaler selection can call scaler_select_scaler() -\u003e uidisplay_hotswap_gfx_mode() -\u003e sdl2display_recreate() before the final display state. SDL1 via sdl12-compat appears to pass fullscreen flags directly to SDL_CreateWindow() for initial fullscreen and tries to reuse the SDL2 window on later SetVideoMode calls, which likely explains why it does not visibly reinitialise. Acceptance: startup with --full-screen should avoid visible windowed-to-fullscreen transition and avoid unnecessary SDL window destroy/create cycles; toggling fullscreen from options should keep existing behaviour; verify at least SDL2 build starts cleanly.","notes":"Reverted last attempted SDL2 auto_fullscreen_scaler tweak per testing feedback. Current uncommitted state is back to the simpler windowed_scaler preservation plus the separate current-fits fullscreen scaler chooser change. Note user preference: options dialog should always show actual scaler in use, not a separate desired scaler. Verified with make -j10 fuse unittests/sdl2displaytest \u0026\u0026 ./unittests/sdl2displaytest.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-11T11:42:16Z","created_by":"Fredrick Meunier","updated_at":"2026-05-12T08:41:13Z","started_at":"2026-05-11T12:00:39Z","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"fuse-73b","title":"SDL2 fullscreen has scaling artefacts on Ubuntu","description":"Address native SDL2 UI scaling artefacts seen after ./fuse --full-screen on Ubuntu. Analysis suggests the SDL2 renderer path renders a Fuse software-scaled RGB565 texture at image_width * current_scaler, sets SDL_RenderSetLogicalSize() to that size, and lets SDL desktop fullscreen scale it again to the actual renderer output. On common 16:9 desktops this can produce non-integer final scaling. sdl12-compat also uses FULLSCREEN_DESKTOP and logical size, but explicitly sets SDL_HINT_RENDER_SCALE_QUALITY around texture creation, while native SDL2 does not, so backend/default filter differences may explain why SDL1 via sdl12-compat hides or avoids the artefacts. Acceptance: fullscreen output on Ubuntu should avoid obvious uneven/interpolation artefacts while preserving SDL1-style fixed Fuse scaler semantics where possible; test macOS regression if available; document any chosen scale-quality/integer-scaling policy in code comments if non-obvious.","notes":"Do not implement yet. Useful tests: run SDL1/sdl12-compat with SDL12COMPAT_SCALE_METHOD=nearest to see whether artefacts appear; try native SDL2 with SDL_HINT_RENDER_SCALE_QUALITY=0/1 and/or explicit destination rect/integer scaling experiments. Remember project memory sdl2-renderer-backend-guidance: avoid silently replacing fixed scaler semantics with free SDL scaling.","status":"closed","priority":1,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-11T11:42:14Z","created_by":"Fredrick Meunier","updated_at":"2026-05-11T11:59:45Z","started_at":"2026-05-11T11:44:00Z","closed_at":"2026-05-11T11:59:45Z","close_reason":"Implemented SDL2 render scale quality hint for fullscreen scaling artefacts; left commit for maintainer review","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-so3.4","title":"Implement SDL2 desktop fullscreen presentation","description":"Change native SDL2 fullscreen startup and toggling so the default fullscreen path uses SDL_WINDOW_FULLSCREEN_DESKTOP. Present the emulator output in an aspect-correct fullscreen rectangle that fills as much of the desktop fullscreen surface as possible, with clear decisions about how this interacts with Fuse's existing scalers and status icons.","acceptance_criteria":"./fuse --full-screen in the SDL2 UI uses SDL_WINDOW_FULLSCREEN_DESKTOP by default; no real display mode is selected during default fullscreen; emulator output is aspect-correct and visibly fills the desktop fullscreen area rather than remaining a small centered rectangle; fullscreen toggle/recreate paths continue to work.","notes":"Audit-driven scope update from fuse-so3.2:\n- Default SDL2 fullscreen must use SDL_WINDOW_FULLSCREEN_DESKTOP. Do not call SDL_SetWindowDisplayMode() in the normal SDL2 fullscreen path.\n- Create the SDL2 window at the normal image*Fuse-scaler size, enter desktop fullscreen, then sync presentation from SDL_GetWindowSurface()/actual window surface size.\n- Preserve fixed Fuse scaler semantics: choose the largest supported Fuse scaler that fits the desktop fullscreen area and avoid arbitrary SDL free scaling unless it is introduced later as an explicit separate mode.\n- Keep centering offsets, rect scaling/update helpers, status icon placement, and fullscreen toggle/recreate semantics.\n- Update sdl2display_find_best_fullscreen_scaler() to use desktop/actual fullscreen surface height rather than a selected fixed-mode height.\n- SDL1 ui/sdl/sdldisplay.c real mode selection is out of scope for this SDL2 implementation task.\nImplementation direction update:\n- Rework the SDL2 fullscreen presentation toward the sdl12-compat-style path: SDL_WINDOW_FULLSCREEN_DESKTOP plus SDL_RenderSetLogicalSize().\n- Keep Fuse's fixed scaler output as the renderer logical target, upload it to a streaming RGB565 texture, clear the renderer to black, RenderCopy the texture, and Present. SDL handles aspect-correct pillarbox/letterbox.\n- This replaces the prior window-surface blit presentation experiment and makes fullscreen fill as much of the desktop as possible while preserving aspect.","status":"closed","priority":1,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-09T22:57:19Z","created_by":"Fredrick Meunier","updated_at":"2026-05-10T00:15:31Z","started_at":"2026-05-09T23:08:00Z","closed_at":"2026-05-10T00:15:31Z","close_reason":"SDL2 renderer logical fullscreen implementation verified manually: fullscreen toggle and screen updates look OK.","labels":["fullscreen","sdl2"],"dependencies":[{"issue_id":"fuse-so3.4","depends_on_id":"fuse-so3","type":"parent-child","created_at":"2026-05-10T08:57:19Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-so3.4","depends_on_id":"fuse-so3.2","type":"blocks","created_at":"2026-05-10T08:57:27Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} @@ -9,7 +11,22 @@ {"_type":"issue","id":"fuse-wgi","title":"Minimal native SDL2 UI backend","description":"Introduce ui/sdl2/ as a native SDL2 frontend that reaches basic emulator usability using the existing software rendering and scaler flow. Focus this milestone on starting the emulator, running a responsive event loop, presenting frames in a window, and supporting essential keyboard-driven emulator use, not on text/layout-perfect fidelity or later modernization.","design":"This backend should stay native to ui/sdl2/ and reuse the existing software framebuffer and scaler flow for now. Prioritize dependable physical-key emulator usability over text-entry or layout fidelity in this early milestone.","acceptance_criteria":"Building with UI=sdl2 produces a working native SDL2 frontend under ui/sdl2/. Launching the emulator with UI=sdl2 starts successfully, opens an SDL2 window, and redraws visible emulator frames through the existing software/scaler path. The SDL2 event loop remains responsive during normal emulator use, including routine input and window interaction. Essential keyboard-driven emulator use works well enough for routine control and testing. Quit and shutdown paths exit cleanly without leaving the process hung or the SDL2 UI in a broken state.","status":"closed","priority":1,"issue_type":"feature","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:12Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T12:41:30Z","closed_at":"2026-04-11T12:41:30Z","close_reason":"SDL2 backend now presents visible emulator frames correctly; verified by user after fixing the 16-bit presentation path.","dependencies":[{"issue_id":"fuse-wgi","depends_on_id":"fuse-pir","type":"blocks","created_at":"2026-04-11T21:57:05Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"fuse-pir","title":"Build/configure groundwork for UI=sdl2 and SDL2 sound selection","description":"Add configure and build-system groundwork for a distinct UI=sdl2 backend and a separate SDL2 sound backend without regressing existing SDL1 codepaths. This task is only about selection, dependency plumbing, and default resolution rules, while keeping UI and audio independently selectable. When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override.","design":"Keep this limited to configure/build and backend selection plumbing. Do not introduce a hybrid SDL1/SDL2 runtime path or an internal SDL1 compatibility layer just to simplify option handling.","acceptance_criteria":"Configure/build logic exposes UI=sdl2 as a distinct selectable UI backend rather than folding it into the SDL1 path. Build/dependency handling distinguishes SDL1 and SDL2 requirements so SDL2-enabled targets can be built without rewriting the SDL1 backend path. UI and audio selection remain independent, so UI=sdl2 can be paired with a non-SDL2 audio backend and SDL2 sound can be selected without requiring UI=sdl2. When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. Existing SDL1 UI and audio builds continue to configure and build unchanged when SDL2 support is disabled or not selected.","status":"closed","priority":1,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:06Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T12:17:46Z","closed_at":"2026-04-11T12:17:46Z","close_reason":"Implemented SDL2 build/configure groundwork with UI=sdl2 selection, SDL2 sound selection/defaulting, temporary scaffolding sources, and SDL2 UI exclusion of SDL1 sound.","dependency_count":0,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fuse-7en","title":"Epic: native SDL2 UI and SDL2 sound backends","description":"Track the SDL2 migration as separate native backends in ui/sdl2/ and sound/sdl2sound.c. Sequence the work as build/configure groundwork first, then minimal SDL2 UI and SDL2 sound, then display cleanup, fullscreen/window behavior, and keyboard fidelity, with joystick, mouse/grab, and final parity cleanup staying as later follow-up. Keep UI and audio independently selectable, preserve the existing software rendering and scaler flow for this tranche, and clarify the default sound rule as: When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. Explicitly exclude any hybrid SDL1/SDL2 backend, internal SDL1 compatibility layer, or renderer/texture modernization in this epic.","design":"Architectural boundaries for this epic: add native SDL2 implementations as separate modules in ui/sdl2/ and sound/sdl2sound.c, keep backend selection independent between UI and audio, and treat renderer/texture modernization as separate future work rather than a prerequisite.","acceptance_criteria":"The epic description and child issue set explicitly cover build/configure groundwork, minimal SDL2 UI, SDL2 sound, display cleanup, fullscreen/window management, keyboard fidelity, deferred joystick, deferred mouse/grab, and final parity cleanup. The documented sequencing matches the current dependency graph, including fuse-pir blocking fuse-wgi and fuse-k8e, fuse-n5p depending on both fuse-wgi and fuse-k8e, and later input/window follow-up work remaining downstream. The epic text explicitly states that UI and audio stay independently selectable, the existing software/scaler flow stays in scope for this tranche, and the default sound rule is: When UI=sdl2 and SDL2 sound is available, default the sound backend to SDL2 sound while still allowing manual override. The epic text explicitly excludes any hybrid SDL1/SDL2 backend, internal SDL1 compatibility layer, or renderer/texture modernization in this tranche.","status":"closed","priority":1,"issue_type":"feature","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:55:31Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T01:58:45Z","closed_at":"2026-04-12T01:58:45Z","close_reason":"All SDL2 UI and SDL2 sound backend milestone work is now complete, documented, formatted, and locally verified; remaining future improvements can be tracked separately from this epic.","dependencies":[{"issue_id":"fuse-7en","depends_on_id":"fuse-3tt","type":"blocks","created_at":"2026-04-11T21:57:15Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-4vn","type":"blocks","created_at":"2026-04-11T21:57:15Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-e8p","type":"blocks","created_at":"2026-04-11T21:57:16Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-k8e","type":"blocks","created_at":"2026-04-11T21:57:13Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-l6g","type":"blocks","created_at":"2026-04-11T21:57:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-n5p","type":"blocks","created_at":"2026-04-11T21:57:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-pir","type":"blocks","created_at":"2026-04-11T21:57:12Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-qqg","type":"blocks","created_at":"2026-04-11T21:57:17Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-7en","depends_on_id":"fuse-wgi","type":"blocks","created_at":"2026-04-11T21:57:13Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":9,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"fuse-88t","title":"Plan PAL TV enhancement branch inspired by Blargg work","description":"Create a concrete implementation plan for a new branch that enhances the PAL TV scalers using ideas and lessons from the Blargg NTSC work, without carrying NTSC-specific code or GUI tuning UI.\n\nWhy:\n- We want to keep the useful lessons from the Blargg branch while avoiding NTSC-specific artifact modelling for mainstream Spectrum use.\n- PAL TV enhancement should stand on its own as a cleaner, more historically appropriate path for normal Spectrum machines.\n- Command-line/settings exposure is sufficient; a GUI filter-options dialog is intentionally out of scope.\n\nScope / plan:\n1. Start a fresh branch from latest master rather than building directly on the Blargg branch.\n2. Reuse concepts, not the NTSC implementation:\n - generic scaler capability flags if needed\n - robust 16-bit/32-bit filter input handling patterns\n - settings.dat-based command-line parameter exposure\n3. Do not carry forward these NTSC-specific pieces:\n - ui/scaler/snes_ntsc.c\n - ui/scaler/snes_ntsc.h\n - ui/scaler/snes_ntsc_impl.h\n - ui/scaler/snes_ntsc_config.h\n - ui/scaler/snes_ntsc.txt\n - ui/gtk3/filter_options.c\n - menu_data.dat filter-options menu additions\n - menu.h filter-options callback additions\n - widget/menu fallback handling for filter-options UI\n4. Focus the PAL work on improving existing PAL TV scaler family behavior:\n - PAL TV\n - PAL TV 2x\n - PAL TV 3x\n - PAL TV 4x\n5. Keep the configuration surface small and command-line oriented. Candidate tunables:\n - paltv_brightness\n - paltv_contrast\n - paltv_saturation\n - paltv_sharpness\n - paltv_bleed\n - optionally paltv_artifacts if justified\n Avoid NTSC-leaning parameters unless the PAL model clearly benefits from them.\n6. Preferred implementation direction:\n - preserve frame-stable output if possible\n - avoid any full-refresh requirement unless testing proves it is genuinely needed\n - improve PAL-style blur/bleed/fringing/crosstalk rather than simulating NTSC frame-phase behaviour\n7. File-by-file starting checklist for the future branch:\n - inspect ui/scaler/scalers.c for reusable helper structure and input-conversion patterns\n - inspect ui/scaler/scaler.h and ui/scaler/scaler.c for any generic flag/integration ideas worth porting\n - update settings.dat with only PAL-oriented parameters\n - implement PAL filter changes inside the existing PAL TV scaler family\n - verify across 16-bit and 32-bit UI paths with make and make check\n\nAcceptance:\n- There is a clear branch plan for PAL TV enhancement that can be followed later.\n- The plan explicitly excludes NTSC code and GUI tuning UI.\n- The plan identifies the most valuable reusable ideas from the Blargg branch and where to apply them.","status":"open","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T10:24:11Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T10:24:11Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-x5g","title":"Refactor GTK composite colour handling into scaler core","description":"Follow up the GTK PAL/NTSC composite filter fix by moving 32-bit byte-order handling out of ui/gtk3/gtkdisplay.c and into the scaler layer. Preserve behaviour while making the composite scalers explicitly support Cairo's RGB24/x8r8g8b8 memory layout without frontend-local swizzling.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-25T12:51:16Z","created_by":"Fredrick Meunier","updated_at":"2026-05-25T12:51:20Z","started_at":"2026-05-25T12:51:20Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-78g","title":"Fix PAL TV filter regressions on pal_ntsc branch","description":"Feedback on the pal_ntsc branch reports two regressions to investigate: PAL TV colours appear incorrect (notably red/blue swapped), and GTK builds need a GTK4-style cast fix around gtk_range_set_value. Also evaluate whether GTK should keep the extra filter-parameters dialog or leave tuning to command-line flags only, while preserving existing SDL2 behaviour.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-25T12:40:23Z","created_by":"Fredrick Meunier","updated_at":"2026-05-25T12:43:24Z","started_at":"2026-05-25T12:40:26Z","closed_at":"2026-05-25T12:43:24Z","close_reason":"Fixed GTK build cast error and corrected GTK composite filter byte-order handling for PAL/NTSC TV scalers.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-hrp","title":"Use full-line dirty expansion for PAL composite 2x/3x scalers","description":"Why:\\n- Conservative local dirty expansion for the new PAL composite scalers still leaves artifacts because the scaler is sensitive to horizontal origin/alignment.\\n- Redrawing whole source lines is a simpler middle ground than full-screen refresh and avoids the PAL line-origin mismatch.\\n\\nWhat:\\n- Add a full-line dirty expander and use it for PAL TV 2x/3x.\\n- Keep NTSC composite scalers on full refresh semantics.\\n\\nAcceptance:\\n- PAL TV 2x/3x dirty updates expand to full source-line width.\\n- NTSC composite scalers are unchanged.\\n- Build/tests pass.","notes":"Changed PAL TV 2x/3x to use a new expand_full_width helper that expands dirty regions to the full source-line width. NTSC composite 2x/3x remain full-refresh only. Verification: make check -j1 passed. A standalone make ui/scaler/scaler.o hit the existing transient .Tpo/.Po rename issue, but the object rebuilt successfully during make check -j1.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-24T09:32:29Z","created_by":"Fredrick Meunier","updated_at":"2026-05-24T09:43:21Z","started_at":"2026-05-24T09:32:34Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-vbt","title":"Add composite scaler dirty-region expander for new PAL/NTSC TV scalers","description":"Why:\\n- The new shared composite scalers have wider horizontal influence than the legacy PAL expanders captured.\\n- We want a conservative expander matching the new composite scaler footprint, even if full-refresh remains in use.\\n\\nWhat:\\n- Add a scaler expander for the shared composite PAL/NTSC 2x/3x scalers with a conservative horizontal expansion radius based on the blargg-derived filter footprint.\\n- Wire it to the relevant scaler registrations.\\n\\nAcceptance:\\n- A dedicated expander exists for the shared composite PAL/NTSC 2x/3x scalers.\\n- It conservatively covers neighboring source pixels affected by the filter.\\n- Build/tests pass.","notes":"Added a conservative expand_composite dirty-region helper for the shared composite PAL TV 2x/3x and blargg NTSC 2x/3x scalers. It expands source dirty rectangles by seven pixels left and right, matching the approximate horizontal footprint of the shared composite filter, while leaving vertical coverage unchanged. Left full-refresh enabled. Verified with make ui/scaler/scaler.o and make check -j1.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-24T09:05:50Z","created_by":"Fredrick Meunier","updated_at":"2026-05-24T09:11:45Z","started_at":"2026-05-24T09:06:00Z","closed_at":"2026-05-24T09:11:45Z","close_reason":"Added a conservative composite dirty-region expander for the shared PAL/NTSC 2x/3x scalers and verified with make check -j1.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-2o0","title":"Squash fuse-ymo-blargg-base onto pal_ntsc","description":"Why: user requested taking the changes from branch fuse-ymo-blargg-base and squashing them onto the current branch.\n\nWhat: inspect the branch-only commits and apply them as a single squashed commit on the current branch, resolving any conflicts without disturbing unrelated worktree changes.","notes":"Applied the final squashed diff from fuse-ymo-blargg-base onto pal_ntsc using the branch state from f8b938cd^..cad927d3, excluding beads metadata. Verified with: make check","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-23T22:33:12Z","created_by":"Fredrick Meunier","updated_at":"2026-05-23T22:35:14Z","started_at":"2026-05-23T22:33:17Z","closed_at":"2026-05-23T22:35:14Z","close_reason":"Squashed the Blargg NTSC scaler branch changes onto pal_ntsc and verified with make check.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-5zl","title":"Restore NTSC frame-to-frame burst phase drift in shared composite scaler","description":"Why:\\n- The shared composite backend refactor for fuse-b3l likely lost NTSC frame-to-frame burst phase persistence, which can reduce visible dot crawl compared with port-374-blargg-ntsc-filter-patch.\\n\\nWhat:\\n- Restore persistent NTSC frame phase state in the shared composite backend so NTSC 2x/3x keep the old per-frame phase drift while PAL variants remain fixed-phase.\\n\\nAcceptance:\\n- NTSC 2x/3x preserve frame-to-frame phase drift equivalent to the pre-refactor branch behavior.\\n- PAL TV 2x/3x remain fixed-phase.\\n- Build and tests pass.","notes":"Restored NTSC frame-to-frame burst phase persistence in the shared composite backend. NTSC now keeps a cached per-frame phase and computes per-line phase from that frame phase plus the current row, matching the old port-374-blargg-ntsc-filter-patch behaviour more closely. PAL variants remain fixed-phase.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-21T10:27:40Z","created_by":"Fredrick Meunier","updated_at":"2026-05-21T10:29:17Z","started_at":"2026-05-21T10:27:49Z","closed_at":"2026-05-21T10:29:17Z","close_reason":"Restored NTSC frame-to-frame burst phase persistence in the shared composite backend while keeping PAL TV variants fixed-phase; verified with make check.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-b3l","title":"Refactor shared composite scaler backend for PAL TV 2x/3x and NTSC 2x/3x","description":"Why:\\n- The new direction is one shared blargg-derived composite scaler stack rather than separate PAL TV and NTSC code paths.\\n- The current branch already shows the blargg-derived path can produce acceptable output once full refresh is handled correctly.\\n\\nWhat:\\n- Refactor the current blargg-derived scaler code into a shared backend with explicit phase behaviour controls.\\n- Provide PAL TV 2x/3x variants using fixed/no NTSC phase cycling.\\n- Provide NTSC 2x/3x variants using the original NTSC phase cycling.\\n- Update scaler registrations and UI-visible scaler options accordingly while keeping the work incremental and testable.\\n\\nAcceptance:\\n- Shared backend is used by PAL TV 2x/3x and NTSC 2x/3x scaler variants.\\n- PAL and NTSC variants differ by controlled phase behaviour rather than separate scaler implementations.\\n- The resulting scaler options are selectable and build successfully.","notes":"Started shared composite backend refactor. Replaced PAL TV 2x/3x with shared blargg-derived helper reused by NTSC 2x/3x, using fixed phase for PAL and cycling phase for NTSC. Removed pal_tv2x setting/option and made PAL TV 4x always use the dark scanline effect. Marked PAL TV 2x/3x as full-refresh scalers and grouped blargg NTSC into the SDL2 PALTV scaler family. Regenerated settings.c, settings.h, and options.h. Partial verification: make settings.c settings.h options.h; make ui/scaler/scalers32.o ui/scaler/scalers16.o ui/scaler/scaler.o ui/sdl2/sdl2_scaler_state.o; make -j4","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-21T09:34:51Z","created_by":"Fredrick Meunier","updated_at":"2026-05-21T10:23:54Z","started_at":"2026-05-21T09:56:29Z","closed_at":"2026-05-21T10:23:54Z","close_reason":"Refactored PAL TV 2x/3x and blargg NTSC 2x/3x onto a shared composite backend with phase-controlled PAL/NTSC behaviour, removed the PAL TV scanline toggle, and verified with make and make check.","dependencies":[{"issue_id":"fuse-b3l","depends_on_id":"fuse-czv","type":"parent-child","created_at":"2026-05-21T19:35:01Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"fuse-d58","title":"Remove obsolete PAL TV 1x and PAL scanline-specific mode after shared scaler rollout","description":"Why:\\n- Once the shared composite scaler stack is in place, the old PAL TV 1x path and PAL scanline-specific behaviour should be retired to avoid overlapping scaler models.\\n\\nWhat:\\n- Remove the PAL TV 1x scaler path.\\n- Remove the PAL-specific scanline mode/setting once replacement PAL TV 2x/3x modes are established.\\n- Update any related registrations, menus, and settings handling.\\n\\nAcceptance:\\n- PAL TV 1x is removed.\\n- The old PAL scanline-specific mode/setting is removed.\\n- Remaining PAL/NTSC composite scaler options are coherent and build successfully.","status":"open","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-21T09:34:50Z","created_by":"Fredrick Meunier","updated_at":"2026-05-21T09:34:50Z","dependencies":[{"issue_id":"fuse-d58","depends_on_id":"fuse-b3l","type":"blocks","created_at":"2026-05-21T19:35:03Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-d58","depends_on_id":"fuse-czv","type":"parent-child","created_at":"2026-05-21T19:35:01Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-czv","title":"Unify composite TV scalers around shared blargg-derived PAL/NTSC stack","description":"Why:\\n- The current direction is to replace the split PAL TV and blargg NTSC experiments with one composite/TV scaler stack.\\n- PAL TV 2x/3x should use the shared composite path with NTSC phase disabled, while NTSC 2x/3x should use the same path with NTSC phase enabled.\\n- PAL TV 1x and the old PAL scanline behaviour should be removed as part of this simplification.\\n\\nWhat:\\n- Incrementally refactor the scaler implementation and registrations so PAL and NTSC scaler variants share one composite backend.\\n- Deliver PAL TV 2x/3x and NTSC 2x/3x as the supported variants in the unified stack.\\n- Remove obsolete PAL TV 1x and PAL scanline-specific behaviour once replacements are in place.\\n\\nAcceptance:\\n- Shared composite scaler backend drives PAL and NTSC scaler variants.\\n- PAL TV 2x/3x run with fixed/no NTSC phase cycling.\\n- NTSC 2x/3x run with NTSC phase cycling enabled.\\n- PAL TV 1x and the old PAL scanline mode are removed.","status":"open","priority":2,"issue_type":"epic","owner":"fredm@spamcop.net","created_at":"2026-05-21T09:34:29Z","created_by":"Fredrick Meunier","updated_at":"2026-05-21T09:34:29Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-t7b","title":"Replan PAL TV colour work around blargg-style scaler path","description":"Why:\\n- Current PAL TV/fuse-ymo colour-transform work is not producing appropriate colours.\\n- The known-good reference is the historical blargg NTSC scaler branch, adapted without the NTSC phase behaviour.\\n- The current unpushed commit 043a602782a445453de05e16ae272e8c492d25ce and related local changes must be preserved on a new local-only branch and must not be pushed to origin yet.\\n\\nWhat:\\n- Move the current local work onto a dedicated local branch.\\n- Replan the implementation around port-374-blargg-ntsc-filter-patch as the reference path.\\n- Keep the work unpushed while the new approach is explored.\\n\\nAcceptance:\\n- A dedicated local branch contains commit 043a602782a445453de05e16ae272e8c492d25ce and current local changes.\\n- origin/master remains untouched by this experimental work.\\n- Follow-up implementation can proceed from the new branch using the blargg branch as reference.","notes":"Corrected full-refresh behaviour on fuse-ymo-blargg-base: when SCALER_FLAGS_FULL_REFRESH is set, frame_end now redraws the whole screen regardless of any dirty rectangles already queued. Updated SDL2, SDL1, Xlib, GTK3, and Win32 frontend frame-end logic accordingly. Verified with 'make -j2'.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-21T08:15:47Z","created_by":"Fredrick Meunier","updated_at":"2026-05-21T09:37:33Z","started_at":"2026-05-21T08:15:55Z","closed_at":"2026-05-21T09:37:33Z","close_reason":"Replan completed: preserved the earlier experimental work locally, moved active implementation to a new local branch based on port-374-blargg-ntsc-filter-patch, validated the blargg-derived path with fixed-phase and corrected full-refresh behaviour, and opened the follow-up epic/tasks for the incremental shared PAL/NTSC composite scaler work.","dependencies":[{"issue_id":"fuse-t7b","depends_on_id":"fuse-czv","type":"parent-child","created_at":"2026-05-21T19:35:03Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"fuse-yq8","title":"PAL TV: validate redraw behaviour and decide on refresh flag need","description":"Why:\n- The Blargg port needed a full-refresh capability flag and UI frame-end handling.\n- PAL work should avoid that unless testing shows dirty-rect updates are insufficient.\n\nWhat:\n- Test the tuned PAL TV family with normal dirty-area redraw behaviour.\n- If redraw artifacts appear, add the smallest acceptable scaler capability/UI integration needed to force or expand refreshes.\n- Verify with make and make check, and note the exact commands used.\n\nAcceptance:\n- There is an explicit decision on whether PAL TV needs a refresh-related scaler flag.\n- If no flag is needed, that is documented by verification.\n- If a flag is needed, the UI-side integration is minimal and justified.","status":"open","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T11:31:15Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T11:31:15Z","dependencies":[{"issue_id":"fuse-yq8","depends_on_id":"fuse-88t","type":"discovered-from","created_at":"2026-05-20T21:31:14Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-yq8","depends_on_id":"fuse-8gd","type":"blocks","created_at":"2026-05-20T21:31:24Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"fuse-8gd","title":"PAL TV: propagate tuning model to 2x/3x/4x variants","description":"Why:\n- After PAL TV 1x proves the tuning model, the scaled PAL TV family should behave consistently.\n\nWhat:\n- Apply the same PAL tuning model to PAL TV 2x, 3x, and 4x in ui/scaler/scalers.c.\n- Preserve current scaled-mode semantics such as existing pal_tv2x line-darkening behaviour unless intentionally changed.\n- Keep differences between 1x and scaled variants limited to scaling/layout behaviour rather than separate tuning models.\n\nAcceptance:\n- PAL TV 2x/3x/4x honour the PAL tuning settings consistently.\n- Existing scaled-mode semantics are preserved unless deliberately adjusted and documented.\n- The implementation remains PAL-only.","notes":"Requirement clarified during PAL tuning review: there should be a single shared set of PAL tuning parameters (paltv_*) that applies consistently across all PAL TV scaler variants rather than per-model/per-variant controls. Propagation work should keep one common tuning model and one settings surface for PAL TV, PAL TV 2x, PAL TV 3x, and PAL TV 4x.","status":"open","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T11:31:14Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T12:09:02Z","dependencies":[{"issue_id":"fuse-8gd","depends_on_id":"fuse-88t","type":"discovered-from","created_at":"2026-05-20T21:31:13Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-8gd","depends_on_id":"fuse-vzv","type":"blocks","created_at":"2026-05-20T21:31:23Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"fuse-vzv","title":"PAL TV: implement cached tuning core for 1x scaler","description":"Why:\n- The Blargg port shows a valuable pattern: cache derived filter state and rebuild only when settings change.\n- PAL TV 1x is the best place to prove the tuning model before propagating it to scaled variants.\n\nWhat:\n- In ui/scaler/scalers.c, add a PAL tuning/cache structure derived from settings_current.\n- Apply the tuning model to the existing PAL TV 1x scaler path.\n- Reuse 16-bit/32-bit conversion patterns only as needed for PAL math; do not import NTSC implementation details.\n- Preserve frame-stable PAL behaviour where possible.\n\nAcceptance:\n- PAL TV 1x honours the new PAL tuning settings.\n- Cached/derived tuning state is recomputed only when inputs change.\n- No NTSC code or GUI tuning UI is added.","notes":"Implemented a cached PAL tuning structure in ui/scaler/scalers.c for the 1x PAL TV path. The scaler now lazily recomputes derived brightness/contrast/saturation/sharpness/bleed coefficients only when the PAL tuning settings change, applies brightness/contrast/sharpness to luma, uses saturation on chroma, and uses bleed-adjusted chroma weighting while keeping the work local to the existing PAL scaler family. Verified with: make ui/scaler/scalers16.o ui/scaler/scalers32.o V=1","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T11:31:13Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T11:53:22Z","started_at":"2026-05-20T11:50:14Z","closed_at":"2026-05-20T11:53:22Z","close_reason":"Added a cached PAL tuning core for the 1x scaler in ui/scaler/scalers.c and verified both 16-bit and 32-bit scaler objects rebuild cleanly.","dependencies":[{"issue_id":"fuse-vzv","depends_on_id":"fuse-1tw","type":"blocks","created_at":"2026-05-20T21:31:22Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-vzv","depends_on_id":"fuse-3qq","type":"blocks","created_at":"2026-05-20T21:31:22Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-vzv","depends_on_id":"fuse-88t","type":"discovered-from","created_at":"2026-05-20T21:31:12Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"fuse-3qq","title":"PAL TV: add command-line tunables in settings.dat","description":"Why:\n- The Blargg port demonstrates a useful settings.dat pattern for scaler-specific numeric tuning.\n- PAL TV enhancement should expose only PAL-oriented command-line/settings parameters.\n\nWhat:\n- Add PAL-oriented numeric settings in settings.dat.\n- Candidate parameters: paltv_brightness, paltv_contrast, paltv_saturation, paltv_sharpness, paltv_bleed, with paltv_artifacts only if justified.\n- Keep the configuration surface small and command-line oriented.\n- Preserve existing pal_tv2x behaviour/settings unless there is a strong reason to change it.\n\nAcceptance:\n- PAL-specific tuning options exist in settings.dat.\n- No NTSC-specific or GUI-only options are introduced.\n- Defaults are conservative and suitable for current behaviour compatibility.","notes":"Implemented by adding PAL-only numeric settings to settings.dat: paltv_brightness, paltv_contrast, paltv_saturation, paltv_sharpness, and paltv_bleed. Chose conservative default value 0 for all new tunables and intentionally did not add paltv_artifacts yet. Regenerated generated settings outputs with: make settings.c settings.h options.h","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T11:31:12Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T11:33:58Z","started_at":"2026-05-20T11:33:14Z","closed_at":"2026-05-20T11:33:58Z","close_reason":"Added PAL-only command-line tunables in settings.dat with conservative defaults; omitted paltv_artifacts pending justification.","dependencies":[{"issue_id":"fuse-3qq","depends_on_id":"fuse-88t","type":"discovered-from","created_at":"2026-05-20T21:31:12Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"fuse-1tw","title":"PAL TV: refactor current scaler family and identify shared tuning hooks","description":"Why:\n- We need a small preparatory refactor before adding PAL-specific tunables.\n- The existing PAL TV 1x/2x/3x/4x code in ui/scaler/scalers.c should expose clear shared helper points for later tuning work.\n\nWhat:\n- Inspect and lightly refactor the existing PAL TV scaler family in ui/scaler/scalers.c.\n- Isolate common RGB extraction / YUV conversion / reconstruction logic where practical.\n- Do not change visible behaviour beyond mechanical refactoring unless needed for safety.\n- Do not add NTSC code or GUI tuning UI.\n\nAcceptance:\n- The PAL TV family has clearer shared helper points for later tuning work.\n- Behaviour remains effectively unchanged apart from any preparatory cleanup.\n- The diff stays focused to PAL scaler internals.","notes":"Refactored the PAL TV scaler family in ui/scaler/scalers.c by extracting shared helper hooks for source unpacking, RGB packing, and darkened scanline generation: paltv_unpack_source_pixel, paltv_pack_rgb, and paltv_darkened_pixel. Updated PAL TV 1x/2x/3x/4x to use those helpers without changing the PAL algorithm itself. Verified with: rm -f ui/scaler/scalers16.o ui/scaler/scalers32.o \u0026\u0026 make ui/scaler/scalers16.o ui/scaler/scalers32.o","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T11:31:11Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T11:41:20Z","started_at":"2026-05-20T11:34:27Z","closed_at":"2026-05-20T11:41:20Z","close_reason":"Added shared PAL TV helper hooks and mechanically rewired the PAL scaler family to use them; verified 16-bit and 32-bit scaler objects rebuild cleanly.","dependencies":[{"issue_id":"fuse-1tw","depends_on_id":"fuse-88t","type":"discovered-from","created_at":"2026-05-20T21:31:10Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"fuse-88t","title":"Plan PAL TV enhancement branch inspired by Blargg work","description":"Create a concrete implementation plan for a new branch that enhances the PAL TV scalers using ideas and lessons from the Blargg NTSC work, without carrying NTSC-specific code or GUI tuning UI.\n\nWhy:\n- We want to keep the useful lessons from the Blargg branch while avoiding NTSC-specific artifact modelling for mainstream Spectrum use.\n- PAL TV enhancement should stand on its own as a cleaner, more historically appropriate path for normal Spectrum machines.\n- Command-line/settings exposure is sufficient; a GUI filter-options dialog is intentionally out of scope.\n\nScope / plan:\n1. Start a fresh branch from latest master rather than building directly on the Blargg branch.\n2. Reuse concepts, not the NTSC implementation:\n - generic scaler capability flags if needed\n - robust 16-bit/32-bit filter input handling patterns\n - settings.dat-based command-line parameter exposure\n3. Do not carry forward these NTSC-specific pieces:\n - ui/scaler/snes_ntsc.c\n - ui/scaler/snes_ntsc.h\n - ui/scaler/snes_ntsc_impl.h\n - ui/scaler/snes_ntsc_config.h\n - ui/scaler/snes_ntsc.txt\n - ui/gtk3/filter_options.c\n - menu_data.dat filter-options menu additions\n - menu.h filter-options callback additions\n - widget/menu fallback handling for filter-options UI\n4. Focus the PAL work on improving existing PAL TV scaler family behavior:\n - PAL TV\n - PAL TV 2x\n - PAL TV 3x\n - PAL TV 4x\n5. Keep the configuration surface small and command-line oriented. Candidate tunables:\n - paltv_brightness\n - paltv_contrast\n - paltv_saturation\n - paltv_sharpness\n - paltv_bleed\n - optionally paltv_artifacts if justified\n Avoid NTSC-leaning parameters unless the PAL model clearly benefits from them.\n6. Preferred implementation direction:\n - preserve frame-stable output if possible\n - avoid any full-refresh requirement unless testing proves it is genuinely needed\n - improve PAL-style blur/bleed/fringing/crosstalk rather than simulating NTSC frame-phase behaviour\n7. File-by-file starting checklist for the future branch:\n - inspect ui/scaler/scalers.c for reusable helper structure and input-conversion patterns\n - inspect ui/scaler/scaler.h and ui/scaler/scaler.c for any generic flag/integration ideas worth porting\n - update settings.dat with only PAL-oriented parameters\n - implement PAL filter changes inside the existing PAL TV scaler family\n - verify across 16-bit and 32-bit UI paths with make and make check\n\nAcceptance:\n- There is a clear branch plan for PAL TV enhancement that can be followed later.\n- The plan explicitly excludes NTSC code and GUI tuning UI.\n- The plan identifies the most valuable reusable ideas from the Blargg branch and where to apply them.","status":"open","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T10:24:11Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T10:24:11Z","dependencies":[{"issue_id":"fuse-88t","depends_on_id":"fuse-1tw","type":"blocks","created_at":"2026-05-20T21:31:25Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-88t","depends_on_id":"fuse-3qq","type":"blocks","created_at":"2026-05-20T21:31:26Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-88t","depends_on_id":"fuse-8gd","type":"blocks","created_at":"2026-05-20T21:31:27Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-88t","depends_on_id":"fuse-vzv","type":"blocks","created_at":"2026-05-20T21:31:26Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-88t","depends_on_id":"fuse-yq8","type":"blocks","created_at":"2026-05-20T21:31:28Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-nyt","title":"Move full-refresh scaler handling into UI frame_end","description":"Keep SCALER_FLAGS_FULL_REFRESH but remove display-layer policy. Have UIs synthesize a full-screen update in uidisplay_frame_end() when needed.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T09:46:50Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T09:47:44Z","started_at":"2026-05-20T09:47:01Z","closed_at":"2026-05-20T09:47:44Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-07s","title":"Generalise Blargg full-refresh handling with scaler flag","description":"Replace the Blargg-specific redraw logic with a generic scaler flag, while keeping the frame scheduling in the display pipeline where it belongs.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T09:38:19Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T09:39:01Z","started_at":"2026-05-20T09:38:39Z","closed_at":"2026-05-20T09:39:01Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-nws","title":"Correct Blargg NTSC channel extraction","description":"Follow-up: the first SDL2 colour conversion fix changed the failure mode but output is still wrong. Re-check snes_ntsc output packing and extract channels correctly.","status":"closed","priority":2,"issue_type":"bug","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:24:55Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:26:16Z","started_at":"2026-05-20T08:25:08Z","closed_at":"2026-05-20T08:26:16Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -51,6 +68,7 @@ {"_type":"issue","id":"fuse-h6s","title":"Add SDL2 presentation helper test seam","description":"Create a minimal, C89-friendly test seam for SDL2 presentation logic by extracting pure helper functions and simple inline view structs without abstracting hot-path rendering through indirect calls. Use it to add unit coverage for fullscreen offset/layout and scaler-selection decisions that currently live inside the SDL2 display backend.","status":"closed","priority":2,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-11T23:09:12Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T23:23:45Z","closed_at":"2026-04-11T23:23:45Z","close_reason":"Added a C89-friendly SDL2 presentation helper seam with unit coverage for fullscreen offsets, rect transforms, icon placement, and fullscreen scaler selection without introducing hot-path indirection.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-l6g","title":"Fullscreen and window management for SDL2 UI","description":"Implement SDL2-native fullscreen transitions and window management behavior once core presentation is correct. Cover sizing, mode changes, and window lifecycle behavior needed for routine use, without expanding scope into deferred joystick or mouse/grab work.","acceptance_criteria":"The SDL2 UI supports usable fullscreen and windowed operation with correct transitions, sizing, and window lifecycle behavior on top of the cleaned-up display path; remaining joystick and mouse/grab concerns stay deferred.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:29Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T14:12:31Z","closed_at":"2026-04-11T14:12:31Z","close_reason":"SDL2 fullscreen/window handling now supports the General options fullscreen toggle, centers fullscreen presentation, adjusts scaler choice for fullscreen use, restores the windowed scaler on exit, and was verified locally by the user.","dependencies":[{"issue_id":"fuse-l6g","depends_on_id":"fuse-n5p","type":"blocks","created_at":"2026-04-11T21:57:08Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"fuse-n5p","title":"Display correctness and presentation cleanup for SDL2 UI","description":"Clean up SDL2 display presentation after the minimal SDL2 UI and SDL2 sound milestones are in place, while explicitly keeping the existing software rendering and scaler flow for this tranche. Focus on concrete correctness issues such as pixel format handling, redraw/expose paths, presentation stability, and scaling behavior before fullscreen or broader polish.","design":"This is a display-correctness pass on top of the existing software/scaler path, not a rendering-architecture rewrite. Keep SDL2 presentation behavior compatible with the current frame production flow so later fullscreen and input work build on a stable base.","acceptance_criteria":"SDL2 presentation handles the intended pixel formats correctly without obvious channel swaps, corruption, or format-mismatch artifacts in normal emulator use. Frame presentation is stable over time, without obvious flicker, runaway redraw behavior, or stale-frame display during routine operation. Redraw and expose/update paths refresh the window correctly after events that require repainting. Scaling and final presentation match the expected behavior of the current software rendering and scaler flow rather than introducing a new renderer/texture pipeline. The task leaves the software/scaler flow in place and does not expand scope into renderer/texture modernization.","status":"closed","priority":2,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:24Z","created_by":"Fredrick Meunier","updated_at":"2026-04-11T13:23:46Z","closed_at":"2026-04-11T13:23:46Z","close_reason":"SDL2 display presentation now handles redraw/expose correctly, keeps status/media overlays in sync, and was verified locally by the user.","dependencies":[{"issue_id":"fuse-n5p","depends_on_id":"fuse-k8e","type":"blocks","created_at":"2026-04-11T21:57:07Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-n5p","depends_on_id":"fuse-wgi","type":"blocks","created_at":"2026-04-11T21:57:06Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":3,"comment_count":0} +{"_type":"issue","id":"fuse-dq7","title":"Investigate 4x composite scaler variants based on the 2x/3x shared path","description":"Why:\\n- After the shared PAL/NTSC composite scaler stack is working for 2x and 3x, a 4x mode may be desirable if it can be derived cleanly from the same approach without introducing poor scaling artefacts or undue complexity.\\n\\nWhat:\\n- Evaluate whether a viable 4x PAL/NTSC composite scaler can be derived from the shared 2x/3x composite path.\\n- Prototype or design the approach if promising, otherwise document why it should be deferred or avoided.\\n\\nAcceptance:\\n- The viability of 4x composite scaler variants is assessed against image quality and implementation complexity.\\n- If viable, there is a concrete follow-up plan or prototype direction.\\n- If not viable, the reasons are documented clearly.","notes":"Implemented a concrete 4x prototype direction on the shared composite backend. Generalized the shared PAL/NTSC composite config/cache from 3 to 4 rows, moved PAL TV 4x onto the shared backend, and added a new blargg NTSC 4x scaler using dsxd 7.0/12.0 with a four-row brightness profile { 0.9, 1.0, 0.75, 0.5 }. PAL TV 4x now uses the same full-line dirty expansion policy as PAL TV 2x/3x, while NTSC 4x remains full-refresh. Registered NTSC 4x in GTK3/SDL/SDL2/Win32/Xlib and added it to the SDL2 PALTV scaler family. Verified with make -j4 and make check -j1.","status":"closed","priority":3,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-05-21T09:34:50Z","created_by":"Fredrick Meunier","updated_at":"2026-05-24T11:35:42Z","started_at":"2026-05-24T11:22:05Z","closed_at":"2026-05-24T11:35:42Z","close_reason":"Implemented a shared-backend 4x composite prototype for PAL and NTSC and verified it builds/tests cleanly.","dependencies":[{"issue_id":"fuse-dq7","depends_on_id":"fuse-b3l","type":"blocks","created_at":"2026-05-21T19:35:04Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-dq7","depends_on_id":"fuse-czv","type":"parent-child","created_at":"2026-05-21T19:35:02Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-5lz","title":"Audit Blargg NTSC UI-specific behaviour","description":"Review UI-specific integration points for the Blargg NTSC scaler across SDL, SDL2, Xlib, GTK3, Win32, and others to identify any remaining quirks beyond the shared 16-bit input fix.","status":"closed","priority":3,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-05-20T08:33:36Z","created_by":"Fredrick Meunier","updated_at":"2026-05-20T08:33:53Z","closed_at":"2026-05-20T08:33:53Z","close_reason":"Audited UI integration. Shared 16-bit input conversion in ui/scaler/scalers.c covers SDL1 and Xlib; GTK3, Win32, and Wii use 32-bit scaler paths and do not have the same format mismatch. No additional code changes needed from this audit.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-nqf","title":"Use libspectrum_new0 in SDL2 display allocations","description":"Replace the remaining direct calloc usage in the SDL2 display backend with the project-standard typed libspectrum_new0 allocator and matching libspectrum_free cleanup for consistency with the rest of the codebase.","status":"closed","priority":3,"issue_type":"task","owner":"fredm@spamcop.net","created_at":"2026-04-12T03:20:30Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T03:21:27Z","closed_at":"2026-04-12T03:21:27Z","close_reason":"Replaced the remaining direct calloc usage in the SDL2 display backend with libspectrum_new0 and paired tmp_screen buffer cleanup with libspectrum_free for allocator consistency.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"fuse-qqg","title":"Final SDL2 parity cleanup","description":"Track remaining SDL2 backend parity gaps and cleanup once build/configure, minimal UI, SDL2 sound, display, fullscreen/window, keyboard, joystick, and mouse/grab work are all complete. Use this to capture residual polish rather than front-loading modernization.","acceptance_criteria":"Remaining SDL2 parity gaps are enumerated and cleaned up after the earlier milestones finish; no major known parity blocker remains for the SDL2 UI and SDL2 sound backends; renderer/texture modernization remains out of scope unless separately justified.","status":"closed","priority":3,"issue_type":"task","assignee":"Fredrick Meunier","owner":"fredm@spamcop.net","created_at":"2026-04-11T11:56:53Z","created_by":"Fredrick Meunier","updated_at":"2026-04-12T00:55:53Z","closed_at":"2026-04-12T00:55:53Z","close_reason":"Cleaned up the remaining practical SDL2 parity gaps by restoring mouse grab across window recreation and allowing held-key repeat through the widget UI. No major known SDL2 parity blocker remains; SDL1-style fixed fullscreen mode selection remains out of scope for this cleanup.","dependencies":[{"issue_id":"fuse-qqg","depends_on_id":"fuse-4vn","type":"blocks","created_at":"2026-04-11T21:57:11Z","created_by":"Fredrick Meunier","metadata":"{}"},{"issue_id":"fuse-qqg","depends_on_id":"fuse-e8p","type":"blocks","created_at":"2026-04-11T21:57:12Z","created_by":"Fredrick Meunier","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} diff --git a/.github/workflows/build_libspectrum_windows_sub.yml b/.github/workflows/build_libspectrum_windows_sub.yml index 4aa6aea4..1689ec79 100644 --- a/.github/workflows/build_libspectrum_windows_sub.yml +++ b/.github/workflows/build_libspectrum_windows_sub.yml @@ -97,27 +97,27 @@ jobs: work-vol: ${{ steps.cygwin-cache-path.outputs.work-vol }} platform: x86 packages: | - audiofile \ - autoconf \ - automake \ - bison \ - flex \ - gcc-core \ - gcc-g++ \ - gettext-devel \ - libtool \ - libaudiofile-devel \ - make \ - mingw64-i686-audiofile \ - mingw64-i686-bzip2 \ - mingw64-i686-gcc-core \ - mingw64-i686-gcc-g++ \ - mingw64-i686-libgcrypt \ - mingw64-i686-libgpg-error \ - mingw64-i686-libpng \ - mingw64-i686-win-iconv \ - mingw64-i686-winpthread \ - mingw64-i686-zlib \ + audiofile + autoconf + automake + bison + flex + gcc-core + gcc-g++ + gettext-devel + libtool + libaudiofile-devel + make + mingw64-i686-audiofile + mingw64-i686-bzip2 + mingw64-i686-gcc-core + mingw64-i686-gcc-g++ + mingw64-i686-libgcrypt + mingw64-i686-libgpg-error + mingw64-i686-libpng + mingw64-i686-win-iconv + mingw64-i686-winpthreads + mingw64-i686-zlib pkg-config - name: (5) Save Cygwin packages diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 3888c1a8..4c88dd1d 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -73,7 +73,7 @@ jobs: configure_params: "--with-sdl --disable-sdl2" verify_ui: "sdl" verify_audio: "sdl" - verify_other: "Using SDL 1: yes" + verify_other: "Using SDL: 1" # SDL 2 UI + Sound sdl2-ui: @@ -83,10 +83,10 @@ jobs: with: name: SDL2 UI dependencies: "libaudiofile-dev libgcrypt-dev libsdl2-dev libpng-dev" - configure_params: "--with-sdl2" + configure_params: "--with-sdl" verify_ui: "sdl2" - verify_audio: "sdl2" - verify_other: "Using SDL 2: yes" + verify_audio: "sdl" + verify_other: "Using SDL: 2" ########################################## ### DEPRECATED Linux UI configurations ### diff --git a/.github/workflows/build_macos.yml b/.github/workflows/build_macos.yml index 17bc0a10..e763fec7 100644 --- a/.github/workflows/build_macos.yml +++ b/.github/workflows/build_macos.yml @@ -24,7 +24,7 @@ jobs: configure_params: "--with-sdl --disable-sdl2" verify_ui: "sdl" verify_audio: "sdl" - verify_other: "Using SDL 1: yes" + verify_other: "Using SDL: 1" ######################################### ### SUPPORTED MacOS UI configurations ### @@ -66,7 +66,7 @@ jobs: configure_params: "--with-sdl --disable-sdl2" verify_ui: "sdl" verify_audio: "sdl" - verify_other: "Using SDL 1: yes" + verify_other: "Using SDL: 1" # SDL 2 UI + Sound sdl2-ui: @@ -76,7 +76,7 @@ jobs: with: name: SDL2 UI dependencies: "sdl2" - configure_params: "--with-sdl2" + configure_params: "--with-sdl" verify_ui: "sdl2" - verify_audio: "sdl2" - verify_other: "Using SDL 2: yes" + verify_audio: "sdl" + verify_other: "Using SDL: 2" diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index 54986121..a5725a0f 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -81,7 +81,7 @@ jobs: verify_ui: "sdl" verify_libxml2: "no" verify_audio: "sdl" - verify_other: "Using SDL 1: yes" + verify_other: "Using SDL: 1" # SDL 2 UI + Sound sdl2-ui-sdl2-sound: @@ -91,8 +91,8 @@ jobs: with: key: "sdl2-ui-sdl2-sound" dependencies: "mingw64-i686-SDL2" - configure_params: "--without-win32 --with-sdl2 --with-audio-driver=sdl2" + configure_params: "--without-win32 --with-sdl --with-audio-driver=sdl" verify_ui: "sdl2" verify_libxml2: "no" - verify_audio: "sdl2" - verify_other: "Using SDL 2: yes" + verify_audio: "sdl" + verify_other: "Using SDL: 2" diff --git a/.github/workflows/build_windows_sub.yml b/.github/workflows/build_windows_sub.yml index c8775899..ec6e3b3f 100644 --- a/.github/workflows/build_windows_sub.yml +++ b/.github/workflows/build_windows_sub.yml @@ -126,32 +126,32 @@ jobs: work-vol: ${{ steps.cygwin-cache-path.outputs.work-vol }} platform: x86 packages: | - autoconf \ - automake \ - bison \ - dos2unix \ - flex \ - gcc-core \ - gcc-g++ \ - gettext-devel \ - ghostscript \ - groff \ - libtool \ - make \ - netpbm \ - psutils \ - zip \ - mingw64-i686-bzip2 \ - mingw64-i686-gcc-core \ - mingw64-i686-gcc-g++ \ - mingw64-i686-libgcrypt \ - mingw64-i686-libgpg-error \ - mingw64-i686-libpng \ - mingw64-i686-win-iconv \ - mingw64-i686-winpthreads \ - mingw64-i686-zlib \ - mingw64-i686-audiofile \ - pkg-config \ + autoconf + automake + bison + dos2unix + flex + gcc-core + gcc-g++ + gettext-devel + ghostscript + groff + libtool + make + netpbm + psutils + zip + mingw64-i686-bzip2 + mingw64-i686-gcc-core + mingw64-i686-gcc-g++ + mingw64-i686-libgcrypt + mingw64-i686-libgpg-error + mingw64-i686-libpng + mingw64-i686-win-iconv + mingw64-i686-winpthreads + mingw64-i686-zlib + mingw64-i686-audiofile + pkg-config ${{ inputs.dependencies }} - name: (3a) Mark Cygwin workspace as safe for git diff --git a/.github/workflows/windows_release.yml b/.github/workflows/windows_release.yml index e66d502c..b574509f 100644 --- a/.github/workflows/windows_release.yml +++ b/.github/workflows/windows_release.yml @@ -4,16 +4,16 @@ run-name: Build Windows release / ${{ github.actor }} / on: workflow_dispatch: inputs: - fuse_tag: + fuse_ref: type: string required: false default: "" - description: Fuse tag to build. Leave empty to use the current workflow revision. - libspectrum_tag: + description: Fuse ref to build (branch, tag, or commit). Leave empty to use the selected workflow revision. + libspectrum_ref: type: string required: false default: "" - description: Libspectrum tag to build. Leave empty to use master from the canonical SourceForge repository. + description: Libspectrum ref to build (branch, tag, or commit). Leave empty to use master from the canonical SourceForge repository. jobs: resolve-refs: @@ -26,27 +26,26 @@ jobs: - name: Resolve selected refs id: resolve run: | - if [ -n "${{ inputs.fuse_tag }}" ]; then - fuse_ref="refs/tags/${{ inputs.fuse_tag }}" + if [ -n "${{ inputs.fuse_ref }}" ]; then + fuse_ref="${{ inputs.fuse_ref }}" else - fuse_ref="current workflow revision (${{ github.sha }})" + fuse_ref="" fi - if [ -n "${{ inputs.libspectrum_tag }}" ]; then - libspectrum_ref="refs/tags/${{ inputs.libspectrum_tag }}" + if [ -n "${{ inputs.libspectrum_ref }}" ]; then + libspectrum_ref="${{ inputs.libspectrum_ref }}" else libspectrum_ref="master" fi - echo "Fuse ref: ${fuse_ref}" - echo "Libspectrum ref: ${libspectrum_ref}" - - if [ -n "${{ inputs.fuse_tag }}" ]; then - echo "fuse_ref=${fuse_ref}" >> "$GITHUB_OUTPUT" + if [ -n "$fuse_ref" ]; then + echo "Fuse ref: ${fuse_ref}" else - echo "fuse_ref=" >> "$GITHUB_OUTPUT" + echo "Fuse ref: current workflow revision (${{ github.sha }})" fi + echo "Libspectrum ref: ${libspectrum_ref}" + echo "fuse_ref=${fuse_ref}" >> "$GITHUB_OUTPUT" echo "libspectrum_ref=${libspectrum_ref}" >> "$GITHUB_OUTPUT" libspectrum: @@ -79,15 +78,15 @@ jobs: verify_ui: "sdl" verify_libxml2: "no" verify_audio: "sdl" - verify_other: "Using SDL 1: yes" + verify_other: "Using SDL: 1" - name: "Fuse SDL 2 build" key: "sdl2-ui-sdl2-sound" dependencies: "mingw64-i686-SDL2" - configure_params: "--without-win32 --with-sdl2 --with-audio-driver=sdl2" + configure_params: "--without-win32 --with-sdl --with-audio-driver=sdl" verify_ui: "sdl2" verify_libxml2: "no" - verify_audio: "sdl2" - verify_other: "Using SDL 2: yes" + verify_audio: "sdl" + verify_other: "Using SDL: 2" uses: ./.github/workflows/build_windows_sub.yml with: key: ${{ matrix.build.key }} From 90b2d09e18929d6841cd9abc00f8911175a35dd6 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 30 May 2026 16:10:36 +1000 Subject: [PATCH 025/142] Add NTSC TV and replace PAL TV filters (patch #374) (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). --- AUTHORS | 4 +- data/shell-completion/bash/fuse | 21 +- man/fuse.1 | 105 +++- menu.h | 4 + menu_data.dat | 4 + screenshot.c | 4 +- settings.dat | 13 +- ui/gtk3/Makefile.am | 1 + ui/gtk3/filter_options.c | 150 ++++++ ui/gtk3/gtkdisplay.c | 9 +- ui/options.dat | 1 - ui/scaler/Makefile.am | 14 +- ui/scaler/scaler.c | 68 ++- ui/scaler/scaler.h | 18 +- ui/scaler/scaler_internals.h | 9 +- ui/scaler/scalers.c | 862 ++++++++++++-------------------- ui/scaler/snes_ntsc.c | 242 +++++++++ ui/scaler/snes_ntsc.h | 226 +++++++++ ui/scaler/snes_ntsc_config.h | 29 ++ ui/scaler/snes_ntsc_impl.h | 441 ++++++++++++++++ ui/sdl/sdldisplay.c | 7 +- ui/sdl2/sdl2_display.c | 7 +- ui/sdl2/sdl2_scaler_state.c | 9 +- ui/wii/wiidisplay.c | 2 - ui/win32/win32display.c | 40 +- ui/xlib/xdisplay.c | 24 +- unittests/sdl2scalerstatetest.c | 1 - 27 files changed, 1649 insertions(+), 666 deletions(-) create mode 100644 ui/gtk3/filter_options.c create mode 100644 ui/scaler/snes_ntsc.c create mode 100644 ui/scaler/snes_ntsc.h create mode 100644 ui/scaler/snes_ntsc_config.h create mode 100644 ui/scaler/snes_ntsc_impl.h diff --git a/AUTHORS b/AUTHORS index cffbc1ee..04bc5df2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -46,7 +46,9 @@ Fuse has been brought to you by: * Bjoern Giesler: the original Wii port. -* Shay Green: the Blip_Buffer band pass filter for beeper and AY output. +* Shay Green: the Blip_Buffer band pass filter for beeper and AY output, + and the snes_ntsc composite-video filter library used by the NTSC TV + and PAL TV scalers. * Michael D Wynne: the Opus Discovery emulation (for EightyOne). diff --git a/data/shell-completion/bash/fuse b/data/shell-completion/bash/fuse index 24c08586..1ec049b9 100644 --- a/data/shell-completion/bash/fuse +++ b/data/shell-completion/bash/fuse @@ -91,7 +91,7 @@ _fuse() COMPREPLY=( $( compgen -W '2x 2xsai 3x 4x advmame2x advmame3x dotmatrix half halfskip hq2x hq3x hq4x normal super2xsai supereagle timex15x timex2x timextv tv2x tv3x tv4x - paltv paltv2x paltv3x paltv4x' -- "$cur" ) ) + paltv2x paltv3x paltv4x' -- "$cur" ) ) return 0 ;; --graphicsfile) @@ -162,7 +162,12 @@ _fuse() _filedir '@(txt)' return 0 ;; - --competition-code|--debugger-command| \ + --competition-code|--composite-filter-artifacts| \ + --composite-filter-bleed|--composite-filter-brightness| \ + --composite-filter-contrast|--composite-filter-fringing| \ + --composite-filter-gamma|--composite-filter-hue| \ + --composite-filter-resolution|--composite-filter-saturation| \ + --composite-filter-sharpness|--debugger-command| \ --drive-40-max-track|--drive-80-max-track| \ --joystick-[12]-fire-[1-9]|--joystick-[12]-fire-1[0-5]| \ --joystick-[12]-output|--joystick-keyboard-down| \ @@ -182,7 +187,13 @@ _fuse() COMPREPLY=( $( compgen -W '--accelerate-loader --aspect-hint --auto-load --autosave-settings --beta128 --beta128-48boot --betadisk --bw-tv --cmos-z80 --competition-code - --competition-mode --compress-rzx --confirm-actions --covox + --competition-mode --composite-filter-artifacts + --composite-filter-bleed --composite-filter-brightness + --composite-filter-contrast --composite-filter-fringing + --composite-filter-gamma --composite-filter-hue + --composite-filter-resolution --composite-filter-saturation + --composite-filter-sharpness --compress-rzx + --confirm-actions --covox --debugger-command --detect-loader --didaktik80 --didaktik80disk --disciple --discipledisk --disk-ask-merge --disk-try-merge --divide --divide-masterfile @@ -237,7 +248,7 @@ _fuse() --no-late-timings --no-loading-sound --no-mdr-random-len --no-melodik --no-mouse-swap-buttons --no-movie-stop-after-rzx --no-multiface1 --no-multiface128 --no-multiface3 - --no-multiface1-stealth --no-opus --no-pal-tv2x + --no-multiface1-stealth --no-opus --no-plus3-detect-speedlock --no-plusd --no-printer --no-raw-s-net --no-recreated-spectrum --no-rs232-handshake --no-rzx-autosaves --no-simpleide --no-slt --no-sound @@ -247,7 +258,7 @@ _fuse() --no-usource --no-uspeech --no-writable-roms --no-zxatasp --no-zxatasp-upload --no-zxatasp-write-protect --no-zxcf --no-zxcf-upload --no-zxmmc - --no-zxprinter --opus --opusdisk --pal-tv2x --phantom-typist-mode + --no-zxprinter --opus --opusdisk --phantom-typist-mode --playback --plus3-detect-speedlock --plus3disk --plusd --plusddisk --printer --rate --raw-s-net --record --recreated-spectrum --rom-128-0 --rom-128-1 diff --git a/man/fuse.1 b/man/fuse.1 index 34611fe6..374ea495 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -190,6 +190,53 @@ The same as the RZX Options dialog's option. .RE .PP +.B \-\-composite\-filter\-artifacts +.I value +.br +.B \-\-composite\-filter\-bleed +.I value +.br +.B \-\-composite\-filter\-brightness +.I value +.br +.B \-\-composite\-filter\-contrast +.I value +.br +.B \-\-composite\-filter\-fringing +.I value +.br +.B \-\-composite\-filter\-gamma +.I value +.br +.B \-\-composite\-filter\-hue +.I value +.br +.B \-\-composite\-filter\-resolution +.I value +.br +.B \-\-composite\-filter\-saturation +.I value +.br +.B \-\-composite\-filter\-sharpness +.I value +.RS +Set the tuning parameters used by the PAL TV and NTSC TV composite +filters. Each value is an integer in the range \-100 to 100. These +options correspond to the Filter Options dialog's +.IR Artifacts , +.IR Bleed , +.IR Brightness , +.IR Contrast , +.IR Fringing , +.IR Gamma , +.IR Hue , +.IR Resolution , +.IR Saturation +and +.I Sharpness +controls respectively. +.RE +.PP .B \-\-compress\-rzx .RS Specify whether RZX files should be written out compressed. (Enabled @@ -539,11 +586,13 @@ which uses no filtering. The available options are .IR tv2x , .IR tv3x , .IR tv4x , -.IR paltv , .IR paltv2x , .IR paltv3x , +.IR paltv4x , +.IR ntsctv2x , +.IR ntsctv3x , and -.IR paltv4x . +.IR ntsctv4x . See the .B "GRAPHICS FILTERS" section for more details. @@ -981,15 +1030,6 @@ option. Insert the specified file into the emulated Opus Discovery's drive\ 1. .RE .PP -.B \-\-pal\-tv2x -.RS -Specify whether the PAL\ TV\ 2x, PAL\ TV\ 3x and PAL\ TV\ 4x scalers should -also produce scanlines along the lines of the TV\ 2x and Timex\ TV scalers. -The same as the General Options dialog's -.I "PAL-TV use TV2x effect" -option. -.RE -.PP .B \-\-phantom\-typist\-mode .I mode .RS @@ -2457,13 +2497,6 @@ and white television. This is effective only under the GTK, Win32, Xlib and SDL user interfaces: the others will always simulate a colour TV. .RE .PP -.I "PAL-TV use TV2x effect" -.RS -This option allows you to choose whether the PAL\ TV\ 2x and higher scalers -also reproduce scanlines in the same way as the TV\ 2x, TV\ 3x and Timex\ TV -scalers. -.RE -.PP .I "Show statusbar" .RS For the GTK and Win32 UI, enables the statusbar beneath the display. For the @@ -4317,18 +4350,42 @@ is a single-sizing filter for Timex machines (note that this means TV\ 2X and Timex\ TV produce the same size output). .RE .PP -.IR "PAL\ TV " ( paltv ) -.br .IR "PAL\ TV\ 2x " ( paltv2x ) .br .IR "PAL\ TV\ 3x " ( paltv3x ) .br .IR "PAL\ TV\ 4x " ( paltv4x ) .RS -Four filters which attempt to emulate the effect of the PAL TV system -which layers a lower-resolution colour image over the top of a -higher-resolution black-and-white image. The filters can also optionally -add scanlines like the other TV series scalers. +Three filters which emulate a PAL television picture rather than a +simple RGB-style pixel enlargement. They model composite-video style +behaviour such as colour blending, reduced chroma resolution and +softening between neighbouring pixels, while scaling the screen by +2\(mu, 3\(mu and 4\(mu respectively. +.PP +These filters use Fuse's composite filter controls, allowing the image +to be tuned with settings such as hue, saturation, contrast, +brightness, sharpness, gamma, resolution, artifacts, fringing and +bleed. +.RE +.PP +.IR "NTSC\ TV\ 2x " ( ntsctv2x ) +.br +.IR "NTSC\ TV\ 3x " ( ntsctv3x ) +.br +.IR "NTSC\ TV\ 4x " ( ntsctv4x ) +.RS +Three filters which emulate an NTSC television picture rather than a +simple RGB-style pixel enlargement. They model composite-video effects +such as colour bleeding, fringing, artifacting and softening between +neighbouring pixels, while scaling the screen by 2\(mu, 3\(mu and 4\(mu +respectively. +.PP +These filters use the bundled +.I snes_ntsc +composite-video library by Shay Green ("blargg"). As with the PAL TV +filters, the image can be tuned with the composite filter controls for +hue, saturation, contrast, brightness, sharpness, gamma, resolution, +artifacts, fringing and bleed. .RE .PP .IR "Dot matrix " ( dotmatrix ) diff --git a/menu.h b/menu.h index 4740a81e..a1492d66 100644 --- a/menu.h +++ b/menu.h @@ -76,6 +76,10 @@ MENU_CALLBACK_WITH_ACTION( menu_options_selectroms_machine_select ); MENU_CALLBACK_WITH_ACTION( menu_options_selectroms_peripheral_select ); MENU_CALLBACK( menu_options_filter ); MENU_DETAIL( menu_filter_detail ); +#ifdef UI_GTK +MENU_CALLBACK( menu_options_filteroptions ); +#endif + MENU_CALLBACK( menu_options_fullscreen ); MENU_CALLBACK( menu_options_save ); diff --git a/menu_data.dat b/menu_data.dat index eac0162c..55bee937 100644 --- a/menu_data.dat +++ b/menu_data.dat @@ -131,6 +131,10 @@ Options/Select ROMs/Peripheral ROMs/uS_peech..., Item,, menu_options_selectroms_ Options/_Filter..., Item,,, menu_filter_detail +#ifdef UI_GTK +Options/_Filter options..., Item +#endif + #ifdef UI_SDL Options/F_ull screen, Item, F11 #elif defined UI_SDL2 diff --git a/screenshot.c b/screenshot.c index 521c0b2f..c6e41514 100644 --- a/screenshot.c +++ b/screenshot.c @@ -361,7 +361,7 @@ screenshot_available_scalers( scaler_type scaler ) case SCALER_HALF: case SCALER_HALFSKIP: case SCALER_NORMAL: case SCALER_TIMEX1_5X: case SCALER_TIMEX2X: - case SCALER_TIMEXTV: case SCALER_PALTV: + case SCALER_TIMEXTV: return 1; default: return 0; @@ -376,7 +376,7 @@ screenshot_available_scalers( scaler_type scaler ) case SCALER_2XSAI: case SCALER_SUPER2XSAI: case SCALER_SUPEREAGLE: case SCALER_ADVMAME2X: case SCALER_ADVMAME3X: case SCALER_TV2X: case SCALER_TV3X: case SCALER_TV4X: case SCALER_DOTMATRIX: - case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: + case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: case SCALER_HQ2X: case SCALER_HQ3X: case SCALER_HQ4X: return 1; default: diff --git a/settings.dat b/settings.dat index f51f852b..4ac863b8 100644 --- a/settings.dat +++ b/settings.dat @@ -67,7 +67,7 @@ multiface3, boolean, 0 multiface1_stealth, boolean, 0 snapsasz80, null, 0 opus, boolean, 0 -pal_tv2x, boolean, 0 +pal_tv2x, null, 0 movie_compr, string, NULL movie_start, string, NULL movie_stop_after_rzx, boolean, 1 @@ -207,6 +207,17 @@ doublescan_mode, numeric, 1, 'D', doublescan-mode start_scaler_mode, string, "2x", 'g', graphics-filter +composite_filter_hue, numeric, 0 +composite_filter_saturation, numeric, -27 +composite_filter_contrast, numeric, 21 +composite_filter_brightness, numeric, 10 +composite_filter_sharpness, numeric, -10 +composite_filter_gamma, numeric, 0 +composite_filter_resolution, numeric, -16 +composite_filter_artifacts, numeric, -59 +composite_filter_fringing, numeric, 14 +composite_filter_bleed, numeric, -71 + speccyboot_tap, string, "tap0", rom_16, string, "48.rom", diff --git a/ui/gtk3/Makefile.am b/ui/gtk3/Makefile.am index 58670555..bf98a997 100644 --- a/ui/gtk3/Makefile.am +++ b/ui/gtk3/Makefile.am @@ -52,6 +52,7 @@ ui_gtk_files = \ ui/gtk3/confirm.c \ ui/gtk3/debugger.c \ ui/gtk3/fileselector.c \ + ui/gtk3/filter_options.c \ ui/gtk3/gtkdisplay.c \ ui/gtk3/gtkinternals.h \ ui/gtk3/gtkjoystick.c \ diff --git a/ui/gtk3/filter_options.c b/ui/gtk3/filter_options.c new file mode 100644 index 00000000..1b128dac --- /dev/null +++ b/ui/gtk3/filter_options.c @@ -0,0 +1,150 @@ +/* filter_options.c: options for current filter (scaler) +*/ + +#include + +#include +#include + +#include +#include + +#include "compat.h" +#include "fuse.h" +#include "gtkinternals.h" +#include "menu.h" +#include "settings.h" +#include "ui/ui.h" + +static int create_dialog( void ); +static void browse_done( GtkWidget *widget, gpointer data ); +static gboolean delete_dialog( GtkWidget *widget, GdkEvent *event, + gpointer user_data ); + +static GtkWidget + *dialog; /* The dialog box itself */ + +static int dialog_created; /* Have we created the dialog box yet? */ + +void +menu_options_filteroptions( GtkAction *gtk_action GCC_UNUSED, + gpointer data GCC_UNUSED ) +{ + /* Firstly, stop emulation */ + fuse_emulation_pause(); + + if( !dialog_created ) + if( create_dialog() ) { fuse_emulation_unpause(); return; } + + gtk_widget_show_all( dialog ); + + /* Carry on with emulation */ + fuse_emulation_unpause(); +} + +struct option_t { + const char *label; + int *value; +}; + +gboolean value_change(GtkRange *range, + GtkScrollType scroll, + gdouble value, + gpointer user_data +) { + *((int *)user_data) = (int)value; + return FALSE; +} + +static int +create_dialog( void ) +{ + GtkWidget *content_area, *range, *label, *box; + struct option_t options[] = { + { + .label = "Hue", + .value = &settings_current.composite_filter_hue + }, + { + .label = "Saturation", + .value = &settings_current.composite_filter_saturation + }, + { + .label = "Contrast", + .value = &settings_current.composite_filter_contrast + }, + { + .label = "Brightness", + .value = &settings_current.composite_filter_brightness + }, + { + .label = "Sharpness", + .value = &settings_current.composite_filter_sharpness + }, + { + .label = "Gamma", + .value = &settings_current.composite_filter_gamma + }, + { + .label = "Resolution", + .value = &settings_current.composite_filter_resolution + }, + { + .label = "Artifacts", + .value = &settings_current.composite_filter_artifacts + }, + { + .label = "Fringing", + .value = &settings_current.composite_filter_fringing + }, + { + .label = "Bleed", + .value = &settings_current.composite_filter_bleed + } + }; + const struct option_t *option; + + /* Give me a new dialog box */ + dialog = gtkstock_dialog_new( "Fuse - Filter Options", + G_CALLBACK( delete_dialog ) ); + +gtk_widget_set_size_request (dialog, 300, 600); + content_area = gtk_dialog_get_content_area( GTK_DIALOG( dialog ) ); + + /* Create the OK button */ + gtkstock_create_close( dialog, NULL, G_CALLBACK( browse_done ), FALSE ); + + /* Make the window big enough to show at least some data */ + gtk_window_set_default_size( GTK_WINDOW( dialog ), -1, 250 ); + + for (option = options; option != options + sizeof(options)/sizeof(*options); ++option) { + label = gtk_label_new(option->label); + range = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, -100, 100, 1); + gtk_range_set_value( GTK_RANGE( range ), *(option->value) ); + box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + gtk_box_pack_start( GTK_BOX( box ), label, TRUE, TRUE, 5 ); + gtk_box_pack_start( GTK_BOX( box ), range, TRUE, TRUE, 5 ); + gtk_box_pack_start( GTK_BOX( content_area ), box, TRUE, TRUE, 5 ); + g_signal_connect(range, "change-value", G_CALLBACK(value_change), option->value); + } + + dialog_created = 1; + + return 0; +} + +/* Called if the OK button is clicked */ +static void +browse_done( GtkWidget *widget GCC_UNUSED, gpointer data GCC_UNUSED ) +{ + gtk_widget_hide( dialog ); +} + +/* Catch attempts to delete the window and just hide it instead */ +static gboolean +delete_dialog( GtkWidget *widget, GdkEvent *event GCC_UNUSED, + gpointer user_data GCC_UNUSED ) +{ + gtk_widget_hide( widget ); + return TRUE; +} diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 46ebe362..6c6a70db 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -192,6 +192,8 @@ uidisplay_init( int width, int height ) G_CALLBACK( drawing_area_resize_callback ), NULL ); error = init_colours( colour_format ); if( error ) return error; + error = scaler_select_bitformat( BITFORMAT_X8R8G8B8 ); + if( error ) return error; black = settings_current.bw_tv ? bw_colours[0] : gtkdisplay_colours[0]; @@ -299,9 +301,11 @@ register_scalers( int force_scaler ) scaler_register( SCALER_SUPER2XSAI ); scaler_register( SCALER_SUPEREAGLE ); scaler_register( SCALER_DOTMATRIX ); + scaler_register( SCALER_NTSC2X ); + scaler_register( SCALER_NTSC3X ); + scaler_register( SCALER_NTSC4X ); } scaler_register( SCALER_NORMAL ); - scaler_register( SCALER_PALTV ); scaler = scaler_is_supported( current_scaler ) ? current_scaler : SCALER_NORMAL; @@ -333,6 +337,9 @@ register_scalers( int force_scaler ) void uidisplay_frame_end( void ) { + if( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) { + uidisplay_area( 0, 0, image_width, image_height ); + } } void diff --git a/ui/options.dat b/ui/options.dat index 2641ce24..1199be15 100644 --- a/ui/options.dat +++ b/ui/options.dat @@ -62,7 +62,6 @@ Checkbox, RS-232 (h)andshake, rs232_handshake, INPUT_KEY_h Checkbox, Raw Sinclair(N)ET, raw_s_net, INPUT_KEY_n #endif Checkbox, Black and white T(V), bw_tv, INPUT_KEY_v -Checkbox, (P)AL-TV use TV2x effect, pal_tv2x, INPUT_KEY_p #ifdef UI_SDL Checkbox, Full (s)creen, full_screen, INPUT_KEY_s #elif defined UI_SDL2 diff --git a/ui/scaler/Makefile.am b/ui/scaler/Makefile.am index b8784c8a..5ded8ca5 100644 --- a/ui/scaler/Makefile.am +++ b/ui/scaler/Makefile.am @@ -20,7 +20,8 @@ ## ## E-mail: philip-fuse@shadowmagic.org.uk -fuse_SOURCES += ui/scaler/scaler.c +fuse_SOURCES += ui/scaler/scaler.c \ + ui/scaler/snes_ntsc.c fuse_LDADD += \ ui/scaler/scalers16.o \ @@ -38,14 +39,19 @@ ui/scaler/scalers32.o: $(srcdir)/ui/scaler/scalers.c noinst_HEADERS += \ ui/scaler/scaler.h \ - ui/scaler/scaler_internals.h + ui/scaler/scaler_internals.h \ + ui/scaler/snes_ntsc.h \ + ui/scaler/snes_ntsc_config.h \ + ui/scaler/snes_ntsc_impl.h EXTRA_DIST += \ ui/scaler/scalers.c \ ui/scaler/scaler_hq2x.c \ ui/scaler/scaler_hq3x.c \ - ui/scaler/scaler_hq4x.c + ui/scaler/scaler_hq4x.c \ + ui/scaler/snes_ntsc.txt CLEANFILES += \ ui/scaler/scalers16.o \ - ui/scaler/scalers32.o + ui/scaler/scalers32.o \ + ui/scaler/snes_ntsc.o diff --git a/ui/scaler/scaler.c b/ui/scaler/scaler.c index e7a05e0b..3dfb800c 100644 --- a/ui/scaler/scaler.c +++ b/ui/scaler/scaler.c @@ -63,10 +63,8 @@ static void expand_1( int *x, int *y, int *w, int *h, int image_width, int image_height ); static void expand_sai( int *x, int *y, int *w, int *h, int image_width, int image_height ); -static void expand_pal1( int *x, int *y, int *w, int *h, - int image_width, int image_height ); -static void expand_pal( int *x, int *y, int *w, int *h, - int image_width, int image_height ); +static void expand_full_width( int *x, int *y, int *w, int *h, + int image_width, int image_height ); static void expand_dotmatrix( int *x, int *y, int *w, int *h, int image_width, int image_height ); @@ -110,20 +108,24 @@ static const struct scaler_info available_scalers[] = { scaler_Timex1_5x_16, scaler_Timex1_5x_32, NULL }, { "Timex 2x", "timex2x", SCALER_FLAGS_NONE, 2.0, scaler_Normal2x_16, scaler_Normal2x_32, NULL }, - { "PAL TV", "paltv", SCALER_FLAGS_EXPAND, 1.0, - scaler_PalTV_16, scaler_PalTV_32, expand_pal1 }, - { "PAL TV 2x", "paltv2x", SCALER_FLAGS_EXPAND, 2.0, - scaler_PalTV2x_16, scaler_PalTV2x_32, expand_pal }, - { "PAL TV 3x", "paltv3x", SCALER_FLAGS_EXPAND, 3.0, - scaler_PalTV3x_16, scaler_PalTV3x_32, expand_pal }, - { "PAL TV 4x", "paltv4x", SCALER_FLAGS_EXPAND, 4.0, - scaler_PalTV4x_16, scaler_PalTV4x_32, expand_pal }, + { "PAL TV 2x", "paltv2x", SCALER_FLAGS_EXPAND, 2.0, + scaler_PalTV2x_16, scaler_PalTV2x_32, expand_full_width }, + { "PAL TV 3x", "paltv3x", SCALER_FLAGS_EXPAND, 3.0, + scaler_PalTV3x_16, scaler_PalTV3x_32, expand_full_width }, + { "PAL TV 4x", "paltv4x", SCALER_FLAGS_EXPAND, 4.0, + scaler_PalTV4x_16, scaler_PalTV4x_32, expand_full_width }, { "HQ 2x", "hq2x", SCALER_FLAGS_EXPAND, 2.0, scaler_HQ2x_16, scaler_HQ2x_32, expand_1 }, { "HQ 3x", "hq3x", SCALER_FLAGS_EXPAND, 3.0, scaler_HQ3x_16, scaler_HQ3x_32, expand_1 }, { "HQ 4x", "hq4x", SCALER_FLAGS_EXPAND, 4.0, scaler_HQ4x_16, scaler_HQ4x_32, expand_1 }, + { "NTSC TV 2x", "ntsctv2x", SCALER_FLAGS_FULL_REFRESH, 2.0, + scaler_blargg_NTSC_2x_16, scaler_blargg_NTSC_2x_32, NULL }, + { "NTSC TV 3x", "ntsctv3x", SCALER_FLAGS_FULL_REFRESH, 3.0, + scaler_blargg_NTSC_3x_16, scaler_blargg_NTSC_3x_32, NULL }, + { "NTSC TV 4x", "ntsctv4x", SCALER_FLAGS_FULL_REFRESH, 4.0, + scaler_blargg_NTSC_4x_16, scaler_blargg_NTSC_4x_32, NULL }, }; scaler_type current_scaler = SCALER_NUM; @@ -160,11 +162,35 @@ scaler_select_scaler( scaler_type scaler ) return uidisplay_hotswap_gfx_mode(); } +int +scaler_select_bitformat( scaler_bitformat_t bitformat ) +{ + switch( bitformat ) { + case BITFORMAT_555: + case BITFORMAT_565: + return scaler_select_bitformat_16( bitformat ); + + case BITFORMAT_X8B8G8R8: + case BITFORMAT_X8R8G8B8: + return scaler_select_bitformat_32( bitformat ); + + default: + ui_error( UI_ERROR_ERROR, "unknown bitformat %d", bitformat ); + return 1; + } +} + int scaler_select_id( const char *id ) { scaler_type i; + if( !strcmp( id, "paltv" ) ) { + ui_error( UI_ERROR_WARNING, + "Scaler id '%s' has been removed; using 'normal'", id ); + return scaler_select_scaler( SCALER_NORMAL ); + } + for( i=0; i < SCALER_NUM; i++ ) { if( ! strcmp( available_scalers[i].id, id ) ) { scaler_select_scaler( i ); @@ -261,21 +287,13 @@ expand_sai( int *x, int *y, int *w, int *h, int image_width, int image_height ) clip( x, y, w, h, image_width, image_height ); } -/* Expand two pixels left and right */ -static void -expand_pal1( int *x, int *y, int *w, int *h, int image_width, int image_height ) -{ - int w_mod = (*w) % 2; - (*x)-=2; (*w)+=4; - (*w)+=w_mod; /* expand to even*/ - clip( x, y, w, h, image_width, image_height ); -} - -/* Expand one pixels left and right */ +/* Expand to the full source-line width */ static void -expand_pal( int *x, int *y, int *w, int *h, int image_width, int image_height ) +expand_full_width( int *x, int *y, int *w, int *h, + int image_width, int image_height ) { - (*x)-=1; (*w)+=2; + *x = 0; + *w = image_width; clip( x, y, w, h, image_width, image_height ); } diff --git a/ui/scaler/scaler.h b/ui/scaler/scaler.h index a0669e5c..36702dcd 100644 --- a/ui/scaler/scaler.h +++ b/ui/scaler/scaler.h @@ -46,20 +46,23 @@ typedef enum scaler_type { SCALER_DOTMATRIX, SCALER_TIMEX1_5X, SCALER_TIMEX2X, - SCALER_PALTV, SCALER_PALTV2X, SCALER_PALTV3X, SCALER_PALTV4X, SCALER_HQ2X, SCALER_HQ3X, SCALER_HQ4X, + SCALER_NTSC2X, + SCALER_NTSC3X, + SCALER_NTSC4X, SCALER_NUM /* End marker; do not remove */ } scaler_type; typedef enum scaler_flags_t { - SCALER_FLAGS_NONE = 0, - SCALER_FLAGS_EXPAND = 1 << 0, + SCALER_FLAGS_NONE = 0, + SCALER_FLAGS_EXPAND = 1 << 0, + SCALER_FLAGS_FULL_REFRESH = 1 << 1, } scaler_flags_t; typedef void ScalerProc( const libspectrum_byte *srcPtr, @@ -77,6 +80,13 @@ extern scaler_flags_t scaler_flags; extern scaler_expand_fn *scaler_expander; extern int scalers_registered; +typedef enum scaler_bitformat_t { + BITFORMAT_555 = 555, + BITFORMAT_565 = 565, + BITFORMAT_X8B8G8R8, + BITFORMAT_X8R8G8B8, +} scaler_bitformat_t; + typedef int (*scaler_available_fn)( scaler_type scaler ); int scaler_select_id( const char *scaler_mode ); @@ -92,6 +102,6 @@ scaler_flags_t scaler_get_flags( scaler_type scaler ); float scaler_get_scaling_factor( scaler_type scaler ); scaler_expand_fn* scaler_get_expander( scaler_type scaler ); -int scaler_select_bitformat( libspectrum_dword BitFormat ); +int scaler_select_bitformat( scaler_bitformat_t bitformat ); #endif diff --git a/ui/scaler/scaler_internals.h b/ui/scaler/scaler_internals.h index a9245e77..5cce8c03 100644 --- a/ui/scaler/scaler_internals.h +++ b/ui/scaler/scaler_internals.h @@ -31,6 +31,8 @@ #ifndef FUSE_SCALER_INTERNALS_H #define FUSE_SCALER_INTERNALS_H +#include "scaler.h" + #define DECLARE_SCALER( name ) \ extern void scaler_##name##_16( const libspectrum_byte *srcPtr, \ libspectrum_dword srcPitch, \ @@ -60,12 +62,17 @@ DECLARE_SCALER(TV3x); DECLARE_SCALER(TV4x); DECLARE_SCALER(TimexTV); DECLARE_SCALER(DotMatrix); -DECLARE_SCALER(PalTV); DECLARE_SCALER(PalTV2x); DECLARE_SCALER(PalTV3x); DECLARE_SCALER(PalTV4x); DECLARE_SCALER(HQ2x); DECLARE_SCALER(HQ3x); DECLARE_SCALER(HQ4x); +DECLARE_SCALER(blargg_NTSC_2x); +DECLARE_SCALER(blargg_NTSC_3x); +DECLARE_SCALER(blargg_NTSC_4x); + +extern int scaler_select_bitformat_16( scaler_bitformat_t bitformat ); +extern int scaler_select_bitformat_32( scaler_bitformat_t bitformat ); #endif /* #ifndef FUSE_SCALER_INTERNALS_H */ diff --git a/ui/scaler/scalers.c b/ui/scaler/scalers.c index 3ad209cf..242ecbc8 100644 --- a/ui/scaler/scalers.c +++ b/ui/scaler/scalers.c @@ -27,14 +27,17 @@ #include "config.h" #include +#include #include "libspectrum.h" +#include "display.h" #include "scaler.h" #include "scaler_internals.h" #include "settings.h" #include "ui/ui.h" #include "ui/uidisplay.h" +#include "snes_ntsc.h" #ifndef MIN #define MIN(a,b) (((a) < (b)) ? (a) : (b)) @@ -80,9 +83,9 @@ static const libspectrum_word dotmatrix_555[16] = { static const libspectrum_word *dotmatrix; int -scaler_select_bitformat( libspectrum_dword BitFormat ) +scaler_select_bitformat_16( scaler_bitformat_t bitformat ) { - switch( BitFormat ) { + switch( bitformat ) { /* FIXME(?): there is an assumption here that our colour fields are (*) xxxx|xyyy|yyyz|zzzz for the 565 mode @@ -129,7 +132,7 @@ scaler_select_bitformat( libspectrum_dword BitFormat ) break; default: - ui_error( UI_ERROR_ERROR, "unknown bitformat %d", BitFormat ); + ui_error( UI_ERROR_ERROR, "unknown bitformat %d", bitformat ); return 1; } @@ -142,10 +145,12 @@ scaler_select_bitformat( libspectrum_dword BitFormat ) typedef libspectrum_dword scaler_data_type; #define FUNCTION( name ) name##_32 -/* The assumption here is that the colour fields are laid out in - memory as (LSB) red|green|blue|padding (MSB). We wish to access - these as 32-bit entities, so make sure we get our masks the right - way round. */ +/* The default 32-bit scaler format stores pixels in memory as + red|green|blue|padding. Some frontends, such as GTK/Cairo RGB24, + instead use native-endian x8r8g8b8, which is blue|green|red|padding on + little-endian hosts and padding|red|green|blue on big-endian hosts. + The composite scalers need explicit channel-aware handling for both. */ +static scaler_bitformat_t scaler_32bit_bitformat = BITFORMAT_X8B8G8R8; #ifdef WORDS_BIGENDIAN @@ -193,6 +198,21 @@ static const libspectrum_dword dotmatrix[16] = { #endif /* #ifdef WORDS_BIGENDIAN */ +int +scaler_select_bitformat_32( scaler_bitformat_t bitformat ) +{ + switch( bitformat ) { + case BITFORMAT_X8B8G8R8: + case BITFORMAT_X8R8G8B8: + scaler_32bit_bitformat = bitformat; + return 0; + + default: + ui_error( UI_ERROR_ERROR, "unknown bitformat %d", bitformat ); + return 1; + } +} + #else /* #if SCALER_DATA_SIZE == 2 or 4 */ #error Unknown SCALER_DATA_SIZE #endif /* #if SCALER_DATA_SIZE == 2 or 4 */ @@ -1483,138 +1503,32 @@ FUNCTION( scaler_DotMatrix )( const libspectrum_byte *srcPtr, ( ( ( (b) & blueMask ) >> 11 ) * 8424 ) >> 10 : \ ( ( ( (b) & blueMask ) >> 10 ) * 8424 ) >> 10 ) -void -FUNCTION( scaler_PalTV )( const libspectrum_byte *srcPtr, - libspectrum_dword srcPitch, - libspectrum_byte *dstPtr, - libspectrum_dword dstPitch, - int width, int height ) -{ -/* - 1.a. RGB => 255,255,255 RGB - 1.b. RGB => YUV - 2. 422 interstricial color subsampling - 3.a. YUV => RGB - 3.b 255,255,255 RGB => RGB -*/ - int i, j; - unsigned int nextlineSrc = srcPitch / sizeof( scaler_data_type ); - const scaler_data_type *p, *p0 = (const scaler_data_type *)srcPtr; - - unsigned int nextlineDst = dstPitch / sizeof( scaler_data_type ); - scaler_data_type *q, *q0 = (scaler_data_type *)dstPtr; - - libspectrum_byte r0, g0, b0, - r1, g1, b1, - r2, g2, b2, - r3, g3, b3; - libspectrum_signed_word y1, y2, u1, u2, v1, v2; - -/* - 422 cosited - # + # + + only Y - # Y and Cb Cr - # + # + - - abcd... => 1/2a + a + 1/2b / 2; ...; 1/2a + b + 1/2c; ... ; ... - always 3 sample/proc - -*/ - for( j = height; j; j-- ) { - p = p0 - 1; q = q0; -#if SCALER_DATA_SIZE == 2 - /* 1.a. RGB => RGB */ - r2 = R_TO_R( *p ); - g2 = G_TO_G( *p ); - b2 = B_TO_B( *p ); - p++; - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); - p++; - r1 = R_TO_R( *p ); - g1 = G_TO_G( *p ); - b1 = B_TO_B( *p ); - p++; -#else - r2 = (*p & redMask); - g2 = (*p & greenMask) >> 8; - b2 = (*p & blueMask) >> 16; - p++; - r0 = (*p & redMask); - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; - p++; - r1 = (*p & redMask); - g1 = (*p & greenMask) >> 8; - b1 = (*p & blueMask) >> 16; - p++; -#endif - u1 = ( RGB_TO_U( r2, g2, b2 ) + 2 * RGB_TO_U( r0, g0, b0 ) + - RGB_TO_U( r1, g1, b1 ) ) >> 2; - v1 = ( RGB_TO_V( r2, g2, b2 ) + 2 * RGB_TO_V( r0, g0, b0 ) + - RGB_TO_V( r1, g1, b1 ) ) >> 2; - for( i = width; i; i -= 2 ) { -#if SCALER_DATA_SIZE == 2 - /* 1.a. RGB => RGB */ - r2 = R_TO_R( *p ); - g2 = G_TO_G( *p ); - b2 = B_TO_B( *p ); - p++; - r3 = R_TO_R( *p ); - g3 = G_TO_G( *p ); - b3 = B_TO_B( *p ); - p++; -#else - r2 = (*p & redMask); - g2 = (*p & greenMask) >> 8; - b2 = (*p & blueMask) >> 16; - p++; - r3 = (*p & redMask); - g3 = (*p & greenMask) >> 8; - b3 = (*p & blueMask) >> 16; - p++; -#endif -/* 1.b. RGB => YUV && 2. YUV subsampling */ - y1 = RGB_TO_Y( r0, g0, b0 ); - y2 = RGB_TO_Y( r1, g1, b1 ); - - u2 = ( RGB_TO_U( r1, g1, b1 ) + 2 * RGB_TO_U( r2, g2, b2 ) + - RGB_TO_U( r3, g3, b3 ) ) >> 2; - v2 = ( RGB_TO_V( r1, g1, b1 ) + 2 * RGB_TO_V( r2, g2, b2 ) + - RGB_TO_V( r3, g3, b3 ) ) >> 2; -/* 3.a. YUV => RGB */ - r0 = YUV_TO_R(y1, u1, v1); - g0 = YUV_TO_G(y1, u1, v1); - b0 = YUV_TO_B(y1, u1, v1); - - u1 = (u1 + u2) >> 1; - v1 = (v1 + v2) >> 1; - - r1 = YUV_TO_R(y2, u1, v1); - g1 = YUV_TO_G(y2, u1, v1); - b1 = YUV_TO_B(y2, u1, v1); -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q++ = RGB_TO_PIXEL_565( r0, g0, b0 ); - *q++ = RGB_TO_PIXEL_565( r1, g1, b1 ); - } else { - *q++ = RGB_TO_PIXEL_555( r0, g0, b0 ); - *q++ = RGB_TO_PIXEL_555( r1, g1, b1 ); - } -#else - *q++ = r0 + (g0 << 8) + (b0 << 16); - *q++ = r1 + (g1 << 8) + (b1 << 16); -#endif - u1 = u2; v1 = v2; - r0 = r2; g0 = g2; b0 = b2; - r1 = r3; g1 = g3; b1 = b3; - } - p0 += nextlineSrc; - q0 += nextlineDst; - } -} +typedef struct composite_scaler_config { + int scale; + int cycle_phase; + int burst_phase; + double dsxd; + double brightness[4]; +} composite_scaler_config; + +typedef struct composite_scaler_cache { + int snes_init; + int burst_phase; + snes_ntsc_setup_t last_setup; + snes_ntsc_t ntsc[4]; +} composite_scaler_cache; + +static void composite_scaler_make_setup( snes_ntsc_setup_t *setup ); +static double composite_scaler_adjust_brightness( double brightness, + double factor ); +static void composite_scaler_init( const composite_scaler_config *config, + composite_scaler_cache *cache ); +static void composite_scaler_blit( const composite_scaler_config *config, + const libspectrum_byte *srcPtr, + libspectrum_dword srcPitch, + libspectrum_byte *dstPtr, + libspectrum_dword dstPitch, + int width, int height ); void FUNCTION( scaler_PalTV2x )( const libspectrum_byte *srcPtr, @@ -1623,129 +1537,12 @@ FUNCTION( scaler_PalTV2x )( const libspectrum_byte *srcPtr, libspectrum_dword dstPitch, int width, int height ) { -/* - 1.a. RGB => 255,255,255 RGB - 1.b. RGB => YUV - 2. 4:2:2 cosited color subsampling - 3.a. YUV => RGB - 3.b 255,255,255 RGB => RGB -*/ - int i, j; - unsigned int nextlineSrc = srcPitch / sizeof( scaler_data_type ); - const scaler_data_type *p, *p0 = (const scaler_data_type *)srcPtr; - - unsigned int nextlineDst = dstPitch / sizeof( scaler_data_type ); - scaler_data_type *q, *q0 = (scaler_data_type *)dstPtr; - - libspectrum_byte r0, g0, b0, - r1, g1, b1, - rx, gx, bx; - libspectrum_signed_dword y1, y2, u1, v1, u2, v2; + static const composite_scaler_config config = { + 2, 0, 0, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } + }; -/* - 422 cosited - # + # + + only Y - # Y and Cb Cr - # + # + - - abcd... => 1/2a + a + 1/2b / 2; ...; 1/2a + b + 1/2c; ... ; ... - always 3 sample/proc - -*/ - for( j = height; j; j-- ) { - p = p0 - 1; q = q0; -#if SCALER_DATA_SIZE == 2 - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); - p++; - r1 = R_TO_R( *p ); - g1 = G_TO_G( *p ); - b1 = B_TO_B( *p ); -#else - r0 = *p & redMask; - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; - p++; - r1 = *(p) & redMask; - g1 = (*(p) & greenMask) >> 8; - b1 = (*(p) & blueMask) >> 16; -#endif - y1 = RGB_TO_Y( r1, g1, b1 ); - u1 = ( RGB_TO_U( r0, g0, b0 ) + 3 * RGB_TO_U( r1, g1, b1 ) ) >> 2; - v1 = ( RGB_TO_V( r0, g0, b0 ) + 3 * RGB_TO_V( r1, g1, b1 ) ) >> 2; - for( i = width; i; i-- ) { - p++; /* next point */ -#if SCALER_DATA_SIZE == 2 - /* 1.a. RGB => RGB */ - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); -#else - r0 = (*p & redMask); - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; -#endif -/* 1.b. RGB => YUV && 2. YUV subsampling */ - y2 = RGB_TO_Y( r0, g0, b0 ); - u2 = ( RGB_TO_U( r1, g1, b1 ) + 3 * RGB_TO_U( r0, g0, b0 ) ) >> 2; - v2 = ( RGB_TO_V( r1, g1, b1 ) + 3 * RGB_TO_V( r0, g0, b0 ) ) >> 2; - -/* 3.a. YUV => RGB */ - rx = YUV_TO_R( y1, u1, v1 ); /* [x0][ ]*/ - gx = YUV_TO_G( y1, u1, v1 ); - bx = YUV_TO_B( y1, u1, v1 ); - - u1 = ( u1 + u2 ) >> 1; - v1 = ( v1 + v2 ) >> 1; - - r1 = YUV_TO_R( y1, u1, v1 ); - g1 = YUV_TO_G( y1, u1, v1 ); - b1 = YUV_TO_B( y1, u1, v1 ); - -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = RGB_TO_PIXEL_565( rx, gx, bx ); - } else { - *q = RGB_TO_PIXEL_555( rx, gx, bx ); - } -#else - *q = rx + ( gx << 8 ) + ( bx << 16 ); -#endif - - if( settings_current.pal_tv2x ) - *(q + nextlineDst) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + nextlineDst) = *q; - - q++; -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = RGB_TO_PIXEL_565( r1, g1, b1 ); - } else { - *q = RGB_TO_PIXEL_555( r1, g1, b1 ); - } -#else - *q = r1 + ( g1 << 8 ) + ( b1 << 16 ); -#endif - if( settings_current.pal_tv2x ) - *(q + nextlineDst) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + nextlineDst) = *q; - - q++; - y1 = y2; u1 = u2; v1 = v2; /* save for next point */ - r1 = r0; g1 = g0; b1 = b0; - } - p0 += nextlineSrc; - q0 += nextlineDst << 1; - } + composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, + height ); } void @@ -1755,161 +1552,12 @@ FUNCTION( scaler_PalTV3x )( const libspectrum_byte *srcPtr, libspectrum_dword dstPitch, int width, int height ) { -/* - 1.a. RGB => 255,255,255 RGB - 1.b. RGB => YUV - 2. 4:2:2 cosited color subsampling - 3.a. YUV => RGB - 3.b 255,255,255 RGB => RGB -*/ - int i, j; - unsigned int nextlineSrc = srcPitch / sizeof( scaler_data_type ); - const scaler_data_type *p, *p0 = (const scaler_data_type *)srcPtr; - - unsigned int nextlineDst = dstPitch / sizeof( scaler_data_type ); - scaler_data_type *q, *q0 = (scaler_data_type *)dstPtr; - - libspectrum_byte r0, g0, b0, - r1, g1, b1, - r2, g2, b2, - rx, gx, bx; - libspectrum_signed_dword y1, y2, u1, v1, u2, v2; - -/* - 422 cosited - # + # + + only Y - # Y and Cb Cr - # + # + - - abcd... => 1/2a + a + 1/2b / 2; ...; 1/2a + b + 1/2c; ... ; ... - always 3 sample/proc - -*/ - for( j = height; j; j-- ) { - p = p0 - 1; q = q0; -#if SCALER_DATA_SIZE == 2 - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); - p++; - r1 = R_TO_R( *p ); - g1 = G_TO_G( *p ); - b1 = B_TO_B( *p ); -#else - r0 = *p & redMask; - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; - p++; /* next point */ - r1 = *(p) & redMask; - g1 = (*(p) & greenMask) >> 8; - b1 = (*(p) & blueMask) >> 16; -#endif - y1 = RGB_TO_Y( r1, g1, b1 ); - u1 = ( RGB_TO_U( r0, g0, b0 ) + 3 * RGB_TO_U( r1, g1, b1 ) ) >> 2; - v1 = ( RGB_TO_V( r0, g0, b0 ) + 3 * RGB_TO_V( r1, g1, b1 ) ) >> 2; - for( i = width; i; i-- ) { - p++; -#if SCALER_DATA_SIZE == 2 - /* 1.a. RGB => RGB */ - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); -#else - r0 = (*p & redMask); - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; -#endif -/* 1.b. RGB => YUV && 2. YUV subsampling */ - y2 = RGB_TO_Y( r0, g0, b0 ); - u2 = ( RGB_TO_U( r1, g1, b1 ) + 3 * RGB_TO_U( r0, g0, b0 ) ) >> 2; - v2 = ( RGB_TO_V( r1, g1, b1 ) + 3 * RGB_TO_V( r0, g0, b0 ) ) >> 2; - -/* 3.a. YUV => RGB */ - rx = YUV_TO_R( y1, u1, v1 ); /* [x0][ ]*/ - gx = YUV_TO_G( y1, u1, v1 ); - bx = YUV_TO_B( y1, u1, v1 ); - - u1 = ( u1 + u2 ) >> 1; - v1 = ( v1 + v2 ) >> 1; - - r1 = YUV_TO_R( y1, u1, v1 ); - g1 = YUV_TO_G( y1, u1, v1 ); - b1 = YUV_TO_B( y1, u1, v1 ); - -/* - ab => EFG - ab EFG - efg -*/ - r2 = ((int)rx + r1) >> 1; /* F */ - g2 = ((int)gx + g1) >> 1; - b2 = ((int)bx + b1) >> 1; - -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = RGB_TO_PIXEL_565( rx, gx, bx); - } else { - *q = RGB_TO_PIXEL_555( rx, gx, bx); - } -#else - *q = rx + ( gx << 8 ) + ( bx << 16 ); /* E, E, e */ -#endif - *(q + nextlineDst) = *q; - - if( settings_current.pal_tv2x ) - *(q + (nextlineDst << 1)) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + (nextlineDst << 1)) = *q; - - q++; -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = RGB_TO_PIXEL_565( r2, g2, b2 ); - } else { - *q = RGB_TO_PIXEL_555( r2, g2, b2 ); - } -#else - *q = r2 + ( g2 << 8 ) + ( b2 << 16 ); /* F, F, f*/ -#endif - *(q + nextlineDst) = *q; - - if( settings_current.pal_tv2x ) - *(q + (nextlineDst << 1)) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + (nextlineDst << 1)) = *q; + static const composite_scaler_config config = { + 3, 0, 0, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } + }; - q++; -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = RGB_TO_PIXEL_565( r1, g1, b1 ); - } else { - *q = RGB_TO_PIXEL_555( r1, g1, b1 ); - } -#else - *q = r1 + ( g1 << 8 ) + ( b1 << 16 ); /* G, G, g*/ -#endif - *(q + nextlineDst) = *q; - if( settings_current.pal_tv2x ) - *(q + (nextlineDst << 1)) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + (nextlineDst << 1)) = *q; - - q++; - y1 = y2; u1 = u2; v1 = v2; /* save for next point */ - r1 = r0; g1 = g0; b1 = b0; - } - p0 += nextlineSrc; - q0 += (nextlineDst << 1) + nextlineDst; - } + composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, + height ); } void @@ -1919,143 +1567,12 @@ FUNCTION( scaler_PalTV4x )( const libspectrum_byte *srcPtr, libspectrum_dword dstPitch, int width, int height ) { -/* - 1.a. RGB => 255,255,255 RGB - 1.b. RGB => YUV - 2. 4:2:2 cosited color subsampling - 3.a. YUV => RGB - 3.b 255,255,255 RGB => RGB -*/ - int i, j; - unsigned int nextlineSrc = srcPitch / sizeof( scaler_data_type ); - const scaler_data_type *p, *p0 = (const scaler_data_type *)srcPtr; - - unsigned int nextlineDst = dstPitch / sizeof( scaler_data_type ); - scaler_data_type *q, *q0 = (scaler_data_type *)dstPtr; - - libspectrum_byte r0, g0, b0, - r1, g1, b1, - rx, gx, bx; - libspectrum_signed_dword y1, y2, u1, v1, u2, v2; - -/* - 422 cosited - # + # + + only Y - # Y and Cb Cr - # + # + - - abcd... => 1/2a + a + 1/2b / 2; ...; 1/2a + b + 1/2c; ... ; ... - always 3 sample/proc - -*/ - for( j = height; j; j-- ) { - p = p0 - 1; q = q0; -#if SCALER_DATA_SIZE == 2 - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); - p++; - r1 = R_TO_R( *p ); - g1 = G_TO_G( *p ); - b1 = B_TO_B( *p ); -#else - r0 = *p & redMask; - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; - p++; - r1 = *(p) & redMask; - g1 = (*(p) & greenMask) >> 8; - b1 = (*(p) & blueMask) >> 16; -#endif - y1 = RGB_TO_Y( r1, g1, b1 ); - u1 = ( RGB_TO_U( r0, g0, b0 ) + 3 * RGB_TO_U( r1, g1, b1 ) ) >> 2; - v1 = ( RGB_TO_V( r0, g0, b0 ) + 3 * RGB_TO_V( r1, g1, b1 ) ) >> 2; - for( i = width; i; i-- ) { - p++; /* next point */ -#if SCALER_DATA_SIZE == 2 - /* 1.a. RGB => RGB */ - r0 = R_TO_R( *p ); - g0 = G_TO_G( *p ); - b0 = B_TO_B( *p ); -#else - r0 = (*p & redMask); - g0 = (*p & greenMask) >> 8; - b0 = (*p & blueMask) >> 16; -#endif -/* 1.b. RGB => YUV && 2. YUV subsampling */ - y2 = RGB_TO_Y( r0, g0, b0 ); - u2 = ( RGB_TO_U( r1, g1, b1 ) + 3 * RGB_TO_U( r0, g0, b0 ) ) >> 2; - v2 = ( RGB_TO_V( r1, g1, b1 ) + 3 * RGB_TO_V( r0, g0, b0 ) ) >> 2; - -/* 3.a. YUV => RGB */ - rx = YUV_TO_R( y1, u1, v1 ); /* [x0][ ]*/ - gx = YUV_TO_G( y1, u1, v1 ); - bx = YUV_TO_B( y1, u1, v1 ); - - u1 = ( u1 + u2 ) >> 1; - v1 = ( v1 + v2 ) >> 1; - - r1 = YUV_TO_R( y1, u1, v1 ); - g1 = YUV_TO_G( y1, u1, v1 ); - b1 = YUV_TO_B( y1, u1, v1 ); + static const composite_scaler_config config = { + 4, 0, 0, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } + }; -#if SCALER_DATA_SIZE == 2 -/* - q q+1 | q+2 q+3 - q q+1 | q+2 q+3 - _______________ - -*/ -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = *(q+1) = RGB_TO_PIXEL_565( rx, gx, bx ); - } else { - *q = *(q+1) = RGB_TO_PIXEL_555( rx, gx, bx ); - } -#else - *q = *(q+1) = rx + ( gx << 8 ) + ( bx << 16 ); -#endif - - if( settings_current.pal_tv2x ) - *(q + nextlineDst) = *(q + nextlineDst + 1) = - *(q + 2 * nextlineDst) = *(q + 2 * nextlineDst + 1) = - *(q + 3 * nextlineDst) = *(q + 3 * nextlineDst + 1) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + nextlineDst) = *(q + nextlineDst + 1) = - *(q + 2 * nextlineDst) = *(q + 2 * nextlineDst + 1) = - *(q + 3 * nextlineDst) = *(q + 3 * nextlineDst + 1) = *q; - - q++; q++; -#if SCALER_DATA_SIZE == 2 -/* 3.b. RGB => RGB */ - if( green6bit ) { - *q = *(q + 1) = RGB_TO_PIXEL_565( r1, g1, b1 ); - } else { - *q = *(q + 1) = RGB_TO_PIXEL_555( r1, g1, b1 ); - } -#else - *q = *(q + 1) = r1 + ( g1 << 8 ) + ( b1 << 16 ); -#endif - if( settings_current.pal_tv2x ) - *(q + nextlineDst) = *(q + nextlineDst + 1) = - *(q + 2 * nextlineDst) = *(q + 2 * nextlineDst + 1) = - *(q + 3 * nextlineDst) = *(q + 3 * nextlineDst + 1) = - ((((*q & redblueMask) * 7) >> 3) & redblueMask) | - ((((*q & greenMask ) * 7) >> 3) & greenMask); - else - *(q + nextlineDst) = *(q + nextlineDst + 1) = - *(q + 2 * nextlineDst) = *(q + 2 * nextlineDst + 1) = - *(q + 3 * nextlineDst) = *(q + 3 * nextlineDst + 1) = *q; - - q++; q++; - y1 = y2; u1 = u2; v1 = v2; /* save for next point */ - r1 = r0; g1 = g0; b1 = b0; - } - p0 += nextlineSrc; - q0 += nextlineDst << 2; - } + composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, + height ); } #define prevline (-nextlineSrc) @@ -2369,3 +1886,246 @@ FUNCTION( scaler_HQ4x ) ( const libspectrum_byte *srcPtr, q0 += ( nextlineDst << 2 ); } } + +static inline scaler_data_type +blargg_ntsc_rgb_to_pixel( const uint8_t *rgb ) +{ +#if SCALER_DATA_SIZE == 2 + const uint8_t blue = rgb[0]; + const uint8_t green = rgb[1]; + const uint8_t red = rgb[2]; + + return green6bit ? RGB_TO_PIXEL_565( red, green, blue ) : + RGB_TO_PIXEL_555( red, green, blue ); +#else + const uint8_t red = rgb[0]; + const uint8_t green = rgb[1]; + const uint8_t blue = rgb[2]; + +#ifdef WORDS_BIGENDIAN + return scaler_32bit_bitformat == BITFORMAT_X8R8G8B8 ? + green << 8 | red << 16 | blue : + blue << 8 | green << 16 | red << 24; +#else + return scaler_32bit_bitformat == BITFORMAT_X8R8G8B8 ? + blue | green << 8 | red << 16 : + red | green << 8 | blue << 16; +#endif +#endif +} + +#if SCALER_DATA_SIZE == 2 +static const SNES_NTSC_IN_T* +blargg_ntsc_input_row( const libspectrum_byte *srcPtr, int width ) +{ + static SNES_NTSC_IN_T input_buffer[ DISPLAY_SCREEN_WIDTH ]; + const scaler_data_type *src = (const scaler_data_type *)srcPtr; + int x; + + for( x = 0; x < width; x++ ) { + libspectrum_byte red = R_TO_R( src[x] ); + libspectrum_byte green = G_TO_G( src[x] ); + libspectrum_byte blue = B_TO_B( src[x] ); + + input_buffer[x] = red << 16 | green << 8 | blue; + } + + return input_buffer; +} +#else +static const SNES_NTSC_IN_T* +blargg_ntsc_input_row( const libspectrum_byte *srcPtr, int width ) +{ + if( scaler_32bit_bitformat == BITFORMAT_X8B8G8R8 ) + return (const SNES_NTSC_IN_T *)srcPtr; + + static SNES_NTSC_IN_T input_buffer[ DISPLAY_SCREEN_WIDTH ]; + const scaler_data_type *src = (const scaler_data_type *)srcPtr; + int x; + + for( x = 0; x < width; x++ ) { + libspectrum_dword pixel = src[x]; + libspectrum_byte red = ( pixel >> 16 ) & 0xff; + libspectrum_byte green = ( pixel >> 8 ) & 0xff; + libspectrum_byte blue = pixel & 0xff; + + input_buffer[x] = red | green << 8 | blue << 16; + } + + return input_buffer; +} +#endif + +static void +composite_scaler_make_setup( snes_ntsc_setup_t *setup ) +{ + memset( setup, 0, sizeof( *setup ) ); + + setup->hue = (double)settings_current.composite_filter_hue / 100.0; + setup->saturation = + (double)settings_current.composite_filter_saturation / 100.0; + setup->contrast = + (double)settings_current.composite_filter_contrast / 100.0; + setup->brightness = + (double)settings_current.composite_filter_brightness / 100.0; + setup->sharpness = + (double)settings_current.composite_filter_sharpness / 100.0; + setup->gamma = + (double)settings_current.composite_filter_gamma / 100.0; + setup->resolution = + (double)settings_current.composite_filter_resolution / 100.0; + setup->artifacts = + (double)settings_current.composite_filter_artifacts / 100.0; + setup->fringing = + (double)settings_current.composite_filter_fringing / 100.0; + setup->bleed = + (double)settings_current.composite_filter_bleed / 100.0; + setup->merge_fields = 1; + setup->decoder_matrix = 0; + setup->bsnes_colortbl = 0; +} + +static double +composite_scaler_adjust_brightness( double brightness, double factor ) +{ + return ( ( brightness + 1 ) * factor ) - 1; +} + +static void +composite_scaler_init( const composite_scaler_config *config, + composite_scaler_cache *cache ) +{ + snes_ntsc_setup_t setup; + uint32_t n; + + composite_scaler_make_setup( &setup ); + + if( cache->snes_init && + !memcmp( &cache->last_setup, &setup, sizeof( cache->last_setup ) ) ) + return; + + memcpy( &cache->last_setup, &setup, sizeof( cache->last_setup ) ); + + for( n = 0; n < config->scale; n++ ) { + snes_ntsc_setup_t row_setup; + + row_setup = setup; + row_setup.brightness = + composite_scaler_adjust_brightness( setup.brightness, + config->brightness[n] ); + snes_ntsc_init( &cache->ntsc[n], &row_setup ); + } + + cache->snes_init = 1; +} + +static void +composite_scaler_blit( const composite_scaler_config *config, + const libspectrum_byte *srcPtr, + libspectrum_dword srcPitch, + libspectrum_byte *dstPtr, + libspectrum_dword dstPitch, + int width, int height ) +{ + static composite_scaler_cache cache_2x; + static composite_scaler_cache cache_3x; + static composite_scaler_cache cache_4x; + static uint8_t buffer[4 * SNES_NTSC_OUT_WIDTH( DISPLAY_SCREEN_WIDTH ) + 8]; + composite_scaler_cache *cache = + config->scale == 2 ? &cache_2x : + config->scale == 3 ? &cache_3x : &cache_4x; + int burst_phase; + + composite_scaler_init( config, cache ); + memset( buffer, 0, sizeof( buffer ) ); + + burst_phase = config->cycle_phase ? + ( cache->burst_phase + 1 ) % snes_ntsc_burst_count : + config->burst_phase; + + while( height-- ) { + const SNES_NTSC_IN_T *input = blargg_ntsc_input_row( srcPtr, width ); + int line_phase = config->cycle_phase ? + ( burst_phase + height ) % snes_ntsc_burst_count : + burst_phase; + uint32_t n; + + for( n = 0; n < config->scale; n++ ) { + scaler_data_type *out = (scaler_data_type*)( dstPtr + dstPitch * n ); + uint32_t x; + double dsx; + + snes_ntsc_blit( &cache->ntsc[n], input, width, line_phase, width, 1, + buffer, 4 * SNES_NTSC_OUT_WIDTH( width ) ); + + dsx = 0; + for( x = 0; x < width * config->scale; x++ ) { + uint32_t isx; + double fsx; + uint8_t rgb[4]; + + isx = (int)floor( dsx ); + fsx = dsx - floor( dsx ); + rgb[0] = ( ( 1 - fsx ) * buffer[ isx * 4 + 0 ] ) + + ( fsx * buffer[ isx * 4 + 4 ] ); + rgb[1] = ( ( 1 - fsx ) * buffer[ isx * 4 + 1 ] ) + + ( fsx * buffer[ isx * 4 + 5 ] ); + rgb[2] = ( ( 1 - fsx ) * buffer[ isx * 4 + 2 ] ) + + ( fsx * buffer[ isx * 4 + 6 ] ); + out[0] = blargg_ntsc_rgb_to_pixel( rgb ); + out++; + dsx += config->dsxd; + } + } + + srcPtr += srcPitch; + dstPtr += dstPitch * config->scale; + } + + if( config->cycle_phase ) cache->burst_phase = burst_phase; +} + +void +FUNCTION( scaler_blargg_NTSC_2x )( const libspectrum_byte *srcPtr, + libspectrum_dword srcPitch, + libspectrum_byte *dstPtr, + libspectrum_dword dstPitch, + int width, int height ) +{ + static const composite_scaler_config config = { + 2, 1, 0, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } + }; + + composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, + height ); +} + +void +FUNCTION( scaler_blargg_NTSC_3x )( const libspectrum_byte *srcPtr, + libspectrum_dword srcPitch, + libspectrum_byte *dstPtr, + libspectrum_dword dstPitch, + int width, int height ) +{ + static const composite_scaler_config config = { + 3, 1, 0, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } + }; + + composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, + height ); +} + +void +FUNCTION( scaler_blargg_NTSC_4x )( const libspectrum_byte *srcPtr, + libspectrum_dword srcPitch, + libspectrum_byte *dstPtr, + libspectrum_dword dstPitch, + int width, int height ) +{ + static const composite_scaler_config config = { + 4, 1, 0, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } + }; + + composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, + height ); +} diff --git a/ui/scaler/snes_ntsc.c b/ui/scaler/snes_ntsc.c new file mode 100644 index 00000000..9c506f25 --- /dev/null +++ b/ui/scaler/snes_ntsc.c @@ -0,0 +1,242 @@ +/* snes_ntsc 0.2.2. http://www.slack.net/~ant/ */ + +#include "snes_ntsc.h" + +/* Copyright (C) 2006-2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +snes_ntsc_setup_t const snes_ntsc_monochrome = { 0,-1, 0, 0,.2, 0,.2,-.2,-.2,-1, 1, 0, 0 }; +snes_ntsc_setup_t const snes_ntsc_composite = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 }; +snes_ntsc_setup_t const snes_ntsc_svideo = { 0, 0, 0, 0,.2, 0,.2, -1, -1, 0, 1, 0, 0 }; +snes_ntsc_setup_t const snes_ntsc_rgb = { 0, 0, 0, 0,.2, 0,.7, -1, -1,-1, 1, 0, 0 }; + +#define alignment_count 3 +#define burst_count 3 +#define rescale_in 8 +#define rescale_out 7 + +#define artifacts_mid 1.0f +#define fringing_mid 1.0f +#define std_decoder_hue 0 + +#define rgb_bits 7 /* half normal range to allow for doubled hires pixels */ +#define gamma_size 32 + +#include "snes_ntsc_impl.h" + +/* 3 input pixels -> 8 composite samples */ +pixel_info_t const snes_ntsc_pixels [alignment_count] = { + { PIXEL_OFFSET( -4, -9 ), { 1, 1, .6667f, 0 } }, + { PIXEL_OFFSET( -2, -7 ), { .3333f, 1, 1, .3333f } }, + { PIXEL_OFFSET( 0, -5 ), { 0, .6667f, 1, 1 } }, +}; + +static void merge_kernel_fields( snes_ntsc_rgb_t* io ) +{ + int n; + for ( n = burst_size; n; --n ) + { + snes_ntsc_rgb_t p0 = io [burst_size * 0] + rgb_bias; + snes_ntsc_rgb_t p1 = io [burst_size * 1] + rgb_bias; + snes_ntsc_rgb_t p2 = io [burst_size * 2] + rgb_bias; + /* merge colors without losing precision */ + io [burst_size * 0] = + ((p0 + p1 - ((p0 ^ p1) & snes_ntsc_rgb_builder)) >> 1) - rgb_bias; + io [burst_size * 1] = + ((p1 + p2 - ((p1 ^ p2) & snes_ntsc_rgb_builder)) >> 1) - rgb_bias; + io [burst_size * 2] = + ((p2 + p0 - ((p2 ^ p0) & snes_ntsc_rgb_builder)) >> 1) - rgb_bias; + ++io; + } +} + +static void correct_errors( snes_ntsc_rgb_t color, snes_ntsc_rgb_t* out ) +{ + int n; + for ( n = burst_count; n; --n ) + { + unsigned i; + for ( i = 0; i < rgb_kernel_size / 2; i++ ) + { + snes_ntsc_rgb_t error = color - + out [i ] - out [(i+12)%14+14] - out [(i+10)%14+28] - + out [i + 7] - out [i + 5 +14] - out [i + 3 +28]; + DISTRIBUTE_ERROR( i+3+28, i+5+14, i+7 ); + } + out += alignment_count * rgb_kernel_size; + } +} + +void snes_ntsc_init( snes_ntsc_t* ntsc, snes_ntsc_setup_t const* setup ) +{ + int merge_fields; + int entry; + init_t impl; + if ( !setup ) + setup = &snes_ntsc_composite; + init( &impl, setup ); + + merge_fields = setup->merge_fields; + if ( setup->artifacts <= -1 && setup->fringing <= -1 ) + merge_fields = 1; + + for ( entry = 0; entry < snes_ntsc_palette_size; entry++ ) + { + /* Reduce number of significant bits of source color. Clearing the + low bits of R and B were least notictable. Modifying green was too + noticeable. */ + int ir = entry >> 8 & 0x1E; + int ig = entry >> 4 & 0x1F; + int ib = entry << 1 & 0x1E; + + { + float rr = impl.to_float [ir]; + float gg = impl.to_float [ig]; + float bb = impl.to_float [ib]; + + float y, i, q = RGB_TO_YIQ( rr, gg, bb, y, i ); + + int r, g, b = YIQ_TO_RGB( y, i, q, impl.to_rgb, int, r, g ); + snes_ntsc_rgb_t rgb = PACK_RGB( r, g, b ); + + snes_ntsc_rgb_t* out = ntsc->table [entry]; + gen_kernel( &impl, y, i, q, out ); + if ( merge_fields ) + merge_kernel_fields( out ); + correct_errors( rgb, out ); + } + } +} + +#ifndef SNES_NTSC_NO_BLITTERS + +void snes_ntsc_blit( snes_ntsc_t const* ntsc, SNES_NTSC_IN_T const* input, + long in_row_width, + int burst_phase, int in_width, int in_height, void* rgb_out, + long out_pitch ) +{ + int chunk_count = (in_width - 1) / snes_ntsc_in_chunk; + for ( ; in_height; --in_height ) + { + SNES_NTSC_IN_T const* line_in = input; + SNES_NTSC_BEGIN_ROW( ntsc, burst_phase, + snes_ntsc_black, snes_ntsc_black, SNES_NTSC_ADJ_IN( *line_in ) ); + snes_ntsc_out_t* restrict line_out = (snes_ntsc_out_t*) rgb_out; + int n; + ++line_in; + + for ( n = chunk_count; n; --n ) + { + /* order of input and output pixels must not be altered */ + SNES_NTSC_COLOR_IN( 0, SNES_NTSC_ADJ_IN( line_in [0] ) ); + SNES_NTSC_RGB_OUT( 0, line_out [0], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 1, line_out [1], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 1, SNES_NTSC_ADJ_IN( line_in [1] ) ); + SNES_NTSC_RGB_OUT( 2, line_out [2], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 3, line_out [3], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 2, SNES_NTSC_ADJ_IN( line_in [2] ) ); + SNES_NTSC_RGB_OUT( 4, line_out [4], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 5, line_out [5], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 6, line_out [6], SNES_NTSC_OUT_DEPTH ); + + line_in += 3; + line_out += 7; + } + + /* finish final pixels */ + SNES_NTSC_COLOR_IN( 0, snes_ntsc_black ); + SNES_NTSC_RGB_OUT( 0, line_out [0], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 1, line_out [1], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 1, snes_ntsc_black ); + SNES_NTSC_RGB_OUT( 2, line_out [2], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 3, line_out [3], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 2, snes_ntsc_black ); + SNES_NTSC_RGB_OUT( 4, line_out [4], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 5, line_out [5], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_RGB_OUT( 6, line_out [6], SNES_NTSC_OUT_DEPTH ); + + burst_phase = (burst_phase + 1) % snes_ntsc_burst_count; + input += in_row_width; + rgb_out = (char*) rgb_out + out_pitch; + } +} + +void snes_ntsc_blit_hires( snes_ntsc_t const* ntsc, SNES_NTSC_IN_T const* input, long in_row_width, + int burst_phase, int in_width, int in_height, void* rgb_out, long out_pitch ) +{ + int chunk_count = (in_width - 2) / (snes_ntsc_in_chunk * 2); + for ( ; in_height; --in_height ) + { + SNES_NTSC_IN_T const* line_in = input; + SNES_NTSC_HIRES_ROW( ntsc, burst_phase, + snes_ntsc_black, snes_ntsc_black, snes_ntsc_black, + SNES_NTSC_ADJ_IN( line_in [0] ), + SNES_NTSC_ADJ_IN( line_in [1] ) ); + snes_ntsc_out_t* restrict line_out = (snes_ntsc_out_t*) rgb_out; + int n; + line_in += 2; + + for ( n = chunk_count; n; --n ) + { + /* twice as many input pixels per chunk */ + SNES_NTSC_COLOR_IN( 0, SNES_NTSC_ADJ_IN( line_in [0] ) ); + SNES_NTSC_HIRES_OUT( 0, line_out [0], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 1, SNES_NTSC_ADJ_IN( line_in [1] ) ); + SNES_NTSC_HIRES_OUT( 1, line_out [1], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 2, SNES_NTSC_ADJ_IN( line_in [2] ) ); + SNES_NTSC_HIRES_OUT( 2, line_out [2], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 3, SNES_NTSC_ADJ_IN( line_in [3] ) ); + SNES_NTSC_HIRES_OUT( 3, line_out [3], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 4, SNES_NTSC_ADJ_IN( line_in [4] ) ); + SNES_NTSC_HIRES_OUT( 4, line_out [4], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 5, SNES_NTSC_ADJ_IN( line_in [5] ) ); + SNES_NTSC_HIRES_OUT( 5, line_out [5], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_HIRES_OUT( 6, line_out [6], SNES_NTSC_OUT_DEPTH ); + + line_in += 6; + line_out += 7; + } + + SNES_NTSC_COLOR_IN( 0, snes_ntsc_black ); + SNES_NTSC_HIRES_OUT( 0, line_out [0], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 1, snes_ntsc_black ); + SNES_NTSC_HIRES_OUT( 1, line_out [1], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 2, snes_ntsc_black ); + SNES_NTSC_HIRES_OUT( 2, line_out [2], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 3, snes_ntsc_black ); + SNES_NTSC_HIRES_OUT( 3, line_out [3], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 4, snes_ntsc_black ); + SNES_NTSC_HIRES_OUT( 4, line_out [4], SNES_NTSC_OUT_DEPTH ); + + SNES_NTSC_COLOR_IN( 5, snes_ntsc_black ); + SNES_NTSC_HIRES_OUT( 5, line_out [5], SNES_NTSC_OUT_DEPTH ); + SNES_NTSC_HIRES_OUT( 6, line_out [6], SNES_NTSC_OUT_DEPTH ); + + burst_phase = (burst_phase + 1) % snes_ntsc_burst_count; + input += in_row_width; + rgb_out = (char*) rgb_out + out_pitch; + } +} + +#endif diff --git a/ui/scaler/snes_ntsc.h b/ui/scaler/snes_ntsc.h new file mode 100644 index 00000000..f98b6187 --- /dev/null +++ b/ui/scaler/snes_ntsc.h @@ -0,0 +1,226 @@ +/* SNES NTSC video filter */ + +/* snes_ntsc 0.2.2 */ +#ifndef SNES_NTSC_H +#define SNES_NTSC_H + +#include "snes_ntsc_config.h" + +#ifdef __cplusplus + extern "C" { +#endif + +/* Image parameters, ranging from -1.0 to 1.0. Actual internal values shown +in parenthesis and should remain fairly stable in future versions. */ +typedef struct snes_ntsc_setup_t +{ + /* Basic parameters */ + double hue; /* -1 = -180 degrees +1 = +180 degrees */ + double saturation; /* -1 = grayscale (0.0) +1 = oversaturated colors (2.0) */ + double contrast; /* -1 = dark (0.5) +1 = light (1.5) */ + double brightness; /* -1 = dark (0.5) +1 = light (1.5) */ + double sharpness; /* edge contrast enhancement/blurring */ + + /* Advanced parameters */ + double gamma; /* -1 = dark (1.5) +1 = light (0.5) */ + double resolution; /* image resolution */ + double artifacts; /* artifacts caused by color changes */ + double fringing; /* color artifacts caused by brightness changes */ + double bleed; /* color bleed (color resolution reduction) */ + int merge_fields; /* if 1, merges even and odd fields together to reduce flicker */ + float const* decoder_matrix; /* optional RGB decoder matrix, 6 elements */ + + unsigned long const* bsnes_colortbl; /* undocumented; set to 0 */ +} snes_ntsc_setup_t; + + + +/* Video format presets */ +extern snes_ntsc_setup_t const snes_ntsc_composite; /* color bleeding + artifacts */ +extern snes_ntsc_setup_t const snes_ntsc_svideo; /* color bleeding only */ +extern snes_ntsc_setup_t const snes_ntsc_rgb; /* crisp image */ +extern snes_ntsc_setup_t const snes_ntsc_monochrome;/* desaturated + artifacts */ + +/* Initializes and adjusts parameters. Can be called multiple times on the same +snes_ntsc_t object. Can pass NULL for either parameter. */ +typedef struct snes_ntsc_t snes_ntsc_t; +void snes_ntsc_init( snes_ntsc_t* ntsc, snes_ntsc_setup_t const* setup ); + +/* Filters one or more rows of pixels. Input pixel format is set by SNES_NTSC_IN_FORMAT +and output RGB depth is set by SNES_NTSC_OUT_DEPTH. Both default to 16-bit RGB. +In_row_width is the number of pixels to get to the next input row. Out_pitch +is the number of *bytes* to get to the next output row. */ +void snes_ntsc_blit( snes_ntsc_t const* ntsc, SNES_NTSC_IN_T const* input, + long in_row_width, int burst_phase, int in_width, int in_height, + void* rgb_out, long out_pitch ); + +void snes_ntsc_blit_hires( snes_ntsc_t const* ntsc, SNES_NTSC_IN_T const* input, + long in_row_width, int burst_phase, int in_width, int in_height, + void* rgb_out, long out_pitch ); + +/* Number of output pixels written by low-res blitter for given input width. Width +might be rounded down slightly; use SNES_NTSC_IN_WIDTH() on result to find rounded +value. Guaranteed not to round 256 down at all. */ +#define SNES_NTSC_OUT_WIDTH( in_width ) \ + ((((in_width) - 1) / snes_ntsc_in_chunk + 1) * snes_ntsc_out_chunk) + +/* Number of low-res input pixels that will fit within given output width. Might be +rounded down slightly; use SNES_NTSC_OUT_WIDTH() on result to find rounded +value. */ +#define SNES_NTSC_IN_WIDTH( out_width ) \ + (((out_width) / snes_ntsc_out_chunk - 1) * snes_ntsc_in_chunk + 1) + + +/* Interface for user-defined custom blitters */ + +enum { snes_ntsc_in_chunk = 3 }; /* number of input pixels read per chunk */ +enum { snes_ntsc_out_chunk = 7 }; /* number of output pixels generated per chunk */ +enum { snes_ntsc_black = 0 }; /* palette index for black */ +enum { snes_ntsc_burst_count = 3 }; /* burst phase cycles through 0, 1, and 2 */ + +/* Begins outputting row and starts three pixels. First pixel will be cut off a bit. +Use snes_ntsc_black for unused pixels. Declares variables, so must be before first +statement in a block (unless you're using C++). */ +#define SNES_NTSC_BEGIN_ROW( ntsc, burst, pixel0, pixel1, pixel2 ) \ + char const* ktable = \ + (char const*) (ntsc)->table + burst * (snes_ntsc_burst_size * sizeof (snes_ntsc_rgb_t));\ + SNES_NTSC_BEGIN_ROW_6_( pixel0, pixel1, pixel2, SNES_NTSC_IN_FORMAT, ktable ) + +/* Begins input pixel */ +#define SNES_NTSC_COLOR_IN( index, color ) \ + SNES_NTSC_COLOR_IN_( index, color, SNES_NTSC_IN_FORMAT, ktable ) + +/* Generates output pixel. Bits can be 24, 16, 15, 14, 32 (treated as 24), or 0: +24: RRRRRRRR GGGGGGGG BBBBBBBB (8-8-8 RGB) +16: RRRRRGGG GGGBBBBB (5-6-5 RGB) +15: RRRRRGG GGGBBBBB (5-5-5 RGB) +14: BBBBBGG GGGRRRRR (5-5-5 BGR, native SNES format) + 0: xxxRRRRR RRRxxGGG GGGGGxxB BBBBBBBx (native internal format; x = junk bits) */ +#define SNES_NTSC_RGB_OUT( index, rgb_out, bits ) \ + SNES_NTSC_RGB_OUT_14_( index, rgb_out, bits, 1 ) + +/* Hires equivalents */ +#define SNES_NTSC_HIRES_ROW( ntsc, burst, pixel1, pixel2, pixel3, pixel4, pixel5 ) \ + char const* ktable = \ + (char const*) (ntsc)->table + burst * (snes_ntsc_burst_size * sizeof (snes_ntsc_rgb_t));\ + unsigned const snes_ntsc_pixel1_ = (pixel1);\ + snes_ntsc_rgb_t const* kernel1 = SNES_NTSC_IN_FORMAT( ktable, snes_ntsc_pixel1_ );\ + unsigned const snes_ntsc_pixel2_ = (pixel2);\ + snes_ntsc_rgb_t const* kernel2 = SNES_NTSC_IN_FORMAT( ktable, snes_ntsc_pixel2_ );\ + unsigned const snes_ntsc_pixel3_ = (pixel3);\ + snes_ntsc_rgb_t const* kernel3 = SNES_NTSC_IN_FORMAT( ktable, snes_ntsc_pixel3_ );\ + unsigned const snes_ntsc_pixel4_ = (pixel4);\ + snes_ntsc_rgb_t const* kernel4 = SNES_NTSC_IN_FORMAT( ktable, snes_ntsc_pixel4_ );\ + unsigned const snes_ntsc_pixel5_ = (pixel5);\ + snes_ntsc_rgb_t const* kernel5 = SNES_NTSC_IN_FORMAT( ktable, snes_ntsc_pixel5_ );\ + snes_ntsc_rgb_t const* kernel0 = kernel1;\ + snes_ntsc_rgb_t const* kernelx0;\ + snes_ntsc_rgb_t const* kernelx1 = kernel1;\ + snes_ntsc_rgb_t const* kernelx2 = kernel1;\ + snes_ntsc_rgb_t const* kernelx3 = kernel1;\ + snes_ntsc_rgb_t const* kernelx4 = kernel1;\ + snes_ntsc_rgb_t const* kernelx5 = kernel1 + +#define SNES_NTSC_HIRES_OUT( x, rgb_out, bits ) {\ + snes_ntsc_rgb_t raw_ =\ + kernel0 [ x ] + kernel2 [(x+5)%7+14] + kernel4 [(x+3)%7+28] +\ + kernelx0 [(x+7)%7+7] + kernelx2 [(x+5)%7+21] + kernelx4 [(x+3)%7+35] +\ + kernel1 [(x+6)%7 ] + kernel3 [(x+4)%7+14] + kernel5 [(x+2)%7+28] +\ + kernelx1 [(x+6)%7+7] + kernelx3 [(x+4)%7+21] + kernelx5 [(x+2)%7+35];\ + SNES_NTSC_CLAMP_( raw_, 0 );\ + SNES_NTSC_RGB_OUT_( rgb_out, (bits), 0 );\ +} + + +/* private */ +enum { snes_ntsc_entry_size = 128 }; +enum { snes_ntsc_palette_size = 0x2000 }; +typedef unsigned long snes_ntsc_rgb_t; +struct snes_ntsc_t { + snes_ntsc_rgb_t table [snes_ntsc_palette_size] [snes_ntsc_entry_size]; +}; +enum { snes_ntsc_burst_size = snes_ntsc_entry_size / snes_ntsc_burst_count }; + +#define SNES_NTSC_RGB32( ktable, n ) \ + (snes_ntsc_rgb_t const*) (ktable + ( \ + ((n >> 3) & 0x001E) | \ + ((n >> 6) & 0x03E0) | \ + ((n >> 10) & 0x3C00) \ + ) * \ + (snes_ntsc_entry_size / 2 * sizeof (snes_ntsc_rgb_t))) + + +/* + +bbbbbbbbggggggggrrrrrrrr +b bbbbbbbggggggggrrrrrrrr + bbbbbbbbggggggggrrrrrrrr + + 0000000000000000 + --bbbbgggggrrrr- +*/ +#define SNES_NTSC_RGB16( ktable, n ) \ + (snes_ntsc_rgb_t const*) (ktable + ((n & 0x001E) | (n >> 1 & 0x03E0) | (n >> 2 & 0x3C00)) * \ + (snes_ntsc_entry_size / 2 * sizeof (snes_ntsc_rgb_t))) + +#define SNES_NTSC_BGR15( ktable, n ) \ + (snes_ntsc_rgb_t const*) (ktable + ((n << 9 & 0x3C00) | (n & 0x03E0) | (n >> 10 & 0x001E)) * \ + (snes_ntsc_entry_size / 2 * sizeof (snes_ntsc_rgb_t))) + +/* common 3->7 ntsc macros */ +#define SNES_NTSC_BEGIN_ROW_6_( pixel0, pixel1, pixel2, ENTRY, table ) \ + unsigned const snes_ntsc_pixel0_ = (pixel0);\ + snes_ntsc_rgb_t const* kernel0 = ENTRY( table, snes_ntsc_pixel0_ );\ + unsigned const snes_ntsc_pixel1_ = (pixel1);\ + snes_ntsc_rgb_t const* kernel1 = ENTRY( table, snes_ntsc_pixel1_ );\ + unsigned const snes_ntsc_pixel2_ = (pixel2);\ + snes_ntsc_rgb_t const* kernel2 = ENTRY( table, snes_ntsc_pixel2_ );\ + snes_ntsc_rgb_t const* kernelx0;\ + snes_ntsc_rgb_t const* kernelx1 = kernel0;\ + snes_ntsc_rgb_t const* kernelx2 = kernel0 + +#define SNES_NTSC_RGB_OUT_14_( x, rgb_out, bits, shift ) {\ + snes_ntsc_rgb_t raw_ =\ + kernel0 [x ] + kernel1 [(x+12)%7+14] + kernel2 [(x+10)%7+28] +\ + kernelx0 [(x+7)%14] + kernelx1 [(x+ 5)%7+21] + kernelx2 [(x+ 3)%7+35];\ + SNES_NTSC_CLAMP_( raw_, shift );\ + SNES_NTSC_RGB_OUT_( rgb_out, bits, shift );\ +} + +/* common ntsc macros */ +#define snes_ntsc_rgb_builder ((1L << 21) | (1 << 11) | (1 << 1)) +#define snes_ntsc_clamp_mask (snes_ntsc_rgb_builder * 3 / 2) +#define snes_ntsc_clamp_add (snes_ntsc_rgb_builder * 0x101) +#define SNES_NTSC_CLAMP_( io, shift ) {\ + snes_ntsc_rgb_t sub = (io) >> (9-(shift)) & snes_ntsc_clamp_mask;\ + snes_ntsc_rgb_t clamp = snes_ntsc_clamp_add - sub;\ + io |= clamp;\ + clamp -= sub;\ + io &= clamp;\ +} + +#define SNES_NTSC_COLOR_IN_( index, color, ENTRY, table ) {\ + unsigned color_;\ + kernelx##index = kernel##index;\ + kernel##index = (color_ = (color), ENTRY( table, color_ ));\ +} + +/* x is always zero except in snes_ntsc library */ +#define SNES_NTSC_RGB_OUT_( rgb_out, bits, x ) {\ + if ( bits == 16 )\ + rgb_out = (raw_>>(13-x)& 0xF800)|(raw_>>(8-x)&0x07E0)|(raw_>>(4-x)&0x001F);\ + if ( bits == 24 || bits == 32 )\ + rgb_out = (raw_>>(5-x)&0xFF0000)|(raw_>>(3-x)&0xFF00)|(raw_>>(1-x)&0xFF);\ + if ( bits == 15 )\ + rgb_out = (raw_>>(14-x)& 0x7C00)|(raw_>>(9-x)&0x03E0)|(raw_>>(4-x)&0x001F);\ + if ( bits == 14 )\ + rgb_out = (raw_>>(24-x)& 0x001F)|(raw_>>(9-x)&0x03E0)|(raw_<<(6+x)&0x7C00);\ + if ( bits == 0 )\ + rgb_out = raw_ << x;\ +} + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/ui/scaler/snes_ntsc_config.h b/ui/scaler/snes_ntsc_config.h new file mode 100644 index 00000000..d6c2877f --- /dev/null +++ b/ui/scaler/snes_ntsc_config.h @@ -0,0 +1,29 @@ +/* Configure library by modifying this file */ + +#ifndef SNES_NTSC_CONFIG_H +#define SNES_NTSC_CONFIG_H + +#include + + +/* Format of source pixels */ +#define SNES_NTSC_IN_FORMAT SNES_NTSC_RGB32 +/* #define SNES_NTSC_IN_FORMAT SNES_NTSC_BGR15 */ + +/* The following affect the built-in blitter only; a custom blitter can +handle things however it wants. */ + +/* Bits per pixel of output. Can be 15, 16, 32, or 24 (same as 32). */ +#define SNES_NTSC_OUT_DEPTH 32 + +/* Type of input pixel values */ +#define SNES_NTSC_IN_T uint32_t + +/* Each raw pixel input value is passed through this. You might want to mask +the pixel index if you use the high bits as flags, etc. */ +#define SNES_NTSC_ADJ_IN( in ) in + +/* For each pixel, this is the basic operation: +output_color = SNES_NTSC_ADJ_IN( SNES_NTSC_IN_T ) */ + +#endif diff --git a/ui/scaler/snes_ntsc_impl.h b/ui/scaler/snes_ntsc_impl.h new file mode 100644 index 00000000..c4275d30 --- /dev/null +++ b/ui/scaler/snes_ntsc_impl.h @@ -0,0 +1,441 @@ +/* snes_ntsc 0.2.2. http://www.slack.net/~ant/ */ + +/* Common implementation of NTSC filters */ + +#include +#include +#include + + +/* Copyright (C) 2006 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +#define DISABLE_CORRECTION 0 + +#undef PI +#define PI 3.14159265358979323846f + +#ifndef LUMA_CUTOFF + #define LUMA_CUTOFF 0.20 +#endif +#ifndef gamma_size + #define gamma_size 1 +#endif +#ifndef rgb_bits + #define rgb_bits 8 +#endif +#ifndef artifacts_max + #define artifacts_max (artifacts_mid * 1.5f) +#endif +#ifndef fringing_max + #define fringing_max (fringing_mid * 2) +#endif +#ifndef STD_HUE_CONDITION + #define STD_HUE_CONDITION( setup ) 1 +#endif + +#define ext_decoder_hue (std_decoder_hue + 15) +#define rgb_unit (1 << rgb_bits) +#define rgb_offset (rgb_unit * 2 + 0.5f) + +enum { burst_size = snes_ntsc_entry_size / burst_count }; +enum { kernel_half = 16 }; +enum { kernel_size = kernel_half * 2 + 1 }; + +typedef struct init_t +{ + float to_rgb [burst_count * 6]; + float to_float [gamma_size]; + float contrast; + float brightness; + float artifacts; + float fringing; + float kernel [rescale_out * kernel_size * 2]; +} init_t; + +#define ROTATE_IQ( i, q, sin_b, cos_b ) {\ + float t;\ + t = i * cos_b - q * sin_b;\ + q = i * sin_b + q * cos_b;\ + i = t;\ +} + +static void init_filters( init_t* impl, snes_ntsc_setup_t const* setup ) +{ +#if rescale_out > 1 + float kernels [kernel_size * 2]; +#else + float* const kernels = impl->kernel; +#endif + + /* generate luma (y) filter using sinc kernel */ + { + /* sinc with rolloff (dsf) */ + float const rolloff = 1 + (float) setup->sharpness * (float) 0.032; + float const maxh = 32; + float const pow_a_n = (float) pow( rolloff, maxh ); + float sum; + int i; + /* quadratic mapping to reduce negative (blurring) range */ + float to_angle = (float) setup->resolution + 1; + to_angle = PI / maxh * (float) LUMA_CUTOFF * (to_angle * to_angle + 1); + + kernels [kernel_size * 3 / 2] = maxh; /* default center value */ + for ( i = 0; i < kernel_half * 2 + 1; i++ ) + { + int x = i - kernel_half; + float angle = x * to_angle; + /* instability occurs at center point with rolloff very close to 1.0 */ + if ( x || pow_a_n > (float) 1.056 || pow_a_n < (float) 0.981 ) + { + float rolloff_cos_a = rolloff * (float) cos( angle ); + float num = 1 - rolloff_cos_a - + pow_a_n * (float) cos( maxh * angle ) + + pow_a_n * rolloff * (float) cos( (maxh - 1) * angle ); + float den = 1 - rolloff_cos_a - rolloff_cos_a + rolloff * rolloff; + float dsf = num / den; + kernels [kernel_size * 3 / 2 - kernel_half + i] = dsf - (float) 0.5; + } + } + + /* apply blackman window and find sum */ + sum = 0; + for ( i = 0; i < kernel_half * 2 + 1; i++ ) + { + float x = PI * 2 / (kernel_half * 2) * i; + float blackman = 0.42f - 0.5f * (float) cos( x ) + 0.08f * (float) cos( x * 2 ); + sum += (kernels [kernel_size * 3 / 2 - kernel_half + i] *= blackman); + } + + /* normalize kernel */ + sum = 1.0f / sum; + for ( i = 0; i < kernel_half * 2 + 1; i++ ) + { + int x = kernel_size * 3 / 2 - kernel_half + i; + kernels [x] *= sum; + assert( kernels [x] == kernels [x] ); /* catch numerical instability */ + } + } + + /* generate chroma (iq) filter using gaussian kernel */ + { + float const cutoff_factor = -0.03125f; + float cutoff = (float) setup->bleed; + int i; + + if ( cutoff < 0 ) + { + /* keep extreme value accessible only near upper end of scale (1.0) */ + cutoff *= cutoff; + cutoff *= cutoff; + cutoff *= cutoff; + cutoff *= -30.0f / 0.65f; + } + cutoff = cutoff_factor - 0.65f * cutoff_factor * cutoff; + + for ( i = -kernel_half; i <= kernel_half; i++ ) + kernels [kernel_size / 2 + i] = (float) exp( i * i * cutoff ); + + /* normalize even and odd phases separately */ + for ( i = 0; i < 2; i++ ) + { + float sum = 0; + int x; + for ( x = i; x < kernel_size; x += 2 ) + sum += kernels [x]; + + sum = 1.0f / sum; + for ( x = i; x < kernel_size; x += 2 ) + { + kernels [x] *= sum; + assert( kernels [x] == kernels [x] ); /* catch numerical instability */ + } + } + } + + /* + printf( "luma:\n" ); + for ( i = kernel_size; i < kernel_size * 2; i++ ) + printf( "%f\n", kernels [i] ); + printf( "chroma:\n" ); + for ( i = 0; i < kernel_size; i++ ) + printf( "%f\n", kernels [i] ); + */ + + /* generate linear rescale kernels */ + #if rescale_out > 1 + { + float weight = 1.0f; + float* out = impl->kernel; + int n = rescale_out; + do + { + float remain = 0; + int i; + weight -= 1.0f / rescale_in; + for ( i = 0; i < kernel_size * 2; i++ ) + { + float cur = kernels [i]; + float m = cur * weight; + *out++ = m + remain; + remain = cur - m; + } + } + while ( --n ); + } + #endif +} + +static float const default_decoder [6] = + { 0.956f, 0.621f, -0.272f, -0.647f, -1.105f, 1.702f }; + +static void init( init_t* impl, snes_ntsc_setup_t const* setup ) +{ + impl->brightness = (float) setup->brightness * (0.5f * rgb_unit) + rgb_offset; + impl->contrast = (float) setup->contrast * (0.5f * rgb_unit) + rgb_unit; + #ifdef default_palette_contrast + if ( !setup->palette ) + impl->contrast *= default_palette_contrast; + #endif + + impl->artifacts = (float) setup->artifacts; + if ( impl->artifacts > 0 ) + impl->artifacts *= artifacts_max - artifacts_mid; + impl->artifacts = impl->artifacts * artifacts_mid + artifacts_mid; + + impl->fringing = (float) setup->fringing; + if ( impl->fringing > 0 ) + impl->fringing *= fringing_max - fringing_mid; + impl->fringing = impl->fringing * fringing_mid + fringing_mid; + + init_filters( impl, setup ); + + /* generate gamma table */ + if ( gamma_size > 1 ) + { + float const to_float = 1.0f / (gamma_size - (gamma_size > 1)); + float const gamma = 1.1333f - (float) setup->gamma * 0.5f; + /* match common PC's 2.2 gamma to TV's 2.65 gamma */ + int i; + for ( i = 0; i < gamma_size; i++ ) + impl->to_float [i] = + (float) pow( i * to_float, gamma ) * impl->contrast + impl->brightness; + } + + /* setup decoder matricies */ + { + float hue = (float) setup->hue * PI + PI / 180 * ext_decoder_hue; + float sat = (float) setup->saturation + 1; + float const* decoder = setup->decoder_matrix; + if ( !decoder ) + { + decoder = default_decoder; + if ( STD_HUE_CONDITION( setup ) ) + hue += PI / 180 * (std_decoder_hue - ext_decoder_hue); + } + + { + float s = (float) sin( hue ) * sat; + float c = (float) cos( hue ) * sat; + float* out = impl->to_rgb; + int n; + + n = burst_count; + do + { + float const* in = decoder; + int n = 3; + do + { + float i = *in++; + float q = *in++; + *out++ = i * c - q * s; + *out++ = i * s + q * c; + } + while ( --n ); + if ( burst_count <= 1 ) + break; + ROTATE_IQ( s, c, 0.866025f, -0.5f ); /* +120 degrees */ + } + while ( --n ); + } + } +} + +/* kernel generation */ + +#define RGB_TO_YIQ( r, g, b, y, i ) (\ + (y = (r) * 0.299f + (g) * 0.587f + (b) * 0.114f),\ + (i = (r) * 0.596f - (g) * 0.275f - (b) * 0.321f),\ + ((r) * 0.212f - (g) * 0.523f + (b) * 0.311f)\ +) + +#define YIQ_TO_RGB( y, i, q, to_rgb, type, r, g ) (\ + r = (type) (y + to_rgb [0] * i + to_rgb [1] * q),\ + g = (type) (y + to_rgb [2] * i + to_rgb [3] * q),\ + (type) (y + to_rgb [4] * i + to_rgb [5] * q)\ +) + +#define PACK_RGB( r, g, b ) ((r) << 21 | (g) << 11 | (b) << 1) + +enum { rgb_kernel_size = burst_size / alignment_count }; +enum { rgb_bias = rgb_unit * 2 * snes_ntsc_rgb_builder }; + +typedef struct pixel_info_t +{ + int offset; + float negate; + float kernel [4]; +} pixel_info_t; + +#if rescale_in > 1 + #define PIXEL_OFFSET_( ntsc, scaled ) \ + (kernel_size / 2 + ntsc + (scaled != 0) + (rescale_out - scaled) % rescale_out + \ + (kernel_size * 2 * scaled)) + + #define PIXEL_OFFSET( ntsc, scaled ) \ + PIXEL_OFFSET_( ((ntsc) - (scaled) / rescale_out * rescale_in),\ + (((scaled) + rescale_out * 10) % rescale_out) ),\ + (1.0f - (((ntsc) + 100) & 2)) +#else + #define PIXEL_OFFSET( ntsc, scaled ) \ + (kernel_size / 2 + (ntsc) - (scaled)),\ + (1.0f - (((ntsc) + 100) & 2)) +#endif + +extern pixel_info_t const snes_ntsc_pixels [alignment_count]; + +/* Generate pixel at all burst phases and column alignments */ +static void gen_kernel( init_t* impl, float y, float i, float q, snes_ntsc_rgb_t* out ) +{ + /* generate for each scanline burst phase */ + float const* to_rgb = impl->to_rgb; + int burst_remain = burst_count; + y -= rgb_offset; + do + { + /* Encode yiq into *two* composite signals (to allow control over artifacting). + Convolve these with kernels which: filter respective components, apply + sharpening, and rescale horizontally. Convert resulting yiq to rgb and pack + into integer. Based on algorithm by NewRisingSun. */ + pixel_info_t const* pixel = snes_ntsc_pixels; + int alignment_remain = alignment_count; + do + { + /* negate is -1 when composite starts at odd multiple of 2 */ + float const yy = y * impl->fringing * pixel->negate; + float const ic0 = (i + yy) * pixel->kernel [0]; + float const qc1 = (q + yy) * pixel->kernel [1]; + float const ic2 = (i - yy) * pixel->kernel [2]; + float const qc3 = (q - yy) * pixel->kernel [3]; + + float const factor = impl->artifacts * pixel->negate; + float const ii = i * factor; + float const yc0 = (y + ii) * pixel->kernel [0]; + float const yc2 = (y - ii) * pixel->kernel [2]; + + float const qq = q * factor; + float const yc1 = (y + qq) * pixel->kernel [1]; + float const yc3 = (y - qq) * pixel->kernel [3]; + + float const* k = &impl->kernel [pixel->offset]; + int n; + ++pixel; + for ( n = rgb_kernel_size; n; --n ) + { + float i = k[0]*ic0 + k[2]*ic2; + float q = k[1]*qc1 + k[3]*qc3; + float y = k[kernel_size+0]*yc0 + k[kernel_size+1]*yc1 + + k[kernel_size+2]*yc2 + k[kernel_size+3]*yc3 + rgb_offset; + if ( rescale_out <= 1 ) + k--; + else if ( k < &impl->kernel [kernel_size * 2 * (rescale_out - 1)] ) + k += kernel_size * 2 - 1; + else + k -= kernel_size * 2 * (rescale_out - 1) + 2; + { + int r, g, b = YIQ_TO_RGB( y, i, q, to_rgb, int, r, g ); + *out++ = PACK_RGB( r, g, b ) - rgb_bias; + } + } + } + while ( alignment_count > 1 && --alignment_remain ); + + if ( burst_count <= 1 ) + break; + + to_rgb += 6; + + ROTATE_IQ( i, q, -0.866025f, -0.5f ); /* -120 degrees */ + } + while ( --burst_remain ); +} + +static void correct_errors( snes_ntsc_rgb_t color, snes_ntsc_rgb_t* out ); + +#if DISABLE_CORRECTION + #define CORRECT_ERROR( a ) { out [i] += rgb_bias; } + #define DISTRIBUTE_ERROR( a, b, c ) { out [i] += rgb_bias; } +#else + #define CORRECT_ERROR( a ) { out [a] += error; } + #define DISTRIBUTE_ERROR( a, b, c ) {\ + snes_ntsc_rgb_t fourth = (error + 2 * snes_ntsc_rgb_builder) >> 2;\ + fourth &= (rgb_bias >> 1) - snes_ntsc_rgb_builder;\ + fourth -= rgb_bias >> 2;\ + out [a] += fourth;\ + out [b] += fourth;\ + out [c] += fourth;\ + out [i] += error - (fourth * 3);\ + } +#endif + +#define RGB_PALETTE_OUT( rgb, out_ )\ +{\ + unsigned char* out = (out_);\ + snes_ntsc_rgb_t clamped = (rgb);\ + SNES_NTSC_CLAMP_( clamped, (8 - rgb_bits) );\ + out [0] = (unsigned char) (clamped >> 21);\ + out [1] = (unsigned char) (clamped >> 11);\ + out [2] = (unsigned char) (clamped >> 1);\ +} + +/* blitter related */ + +#ifndef restrict + #if defined (__GNUC__) + #define restrict __restrict__ + #elif defined (_MSC_VER) && _MSC_VER > 1300 + #define restrict __restrict + #else + /* no support for restricted pointers */ + #define restrict + #endif +#endif + +#include + +#if SNES_NTSC_OUT_DEPTH <= 16 + #if USHRT_MAX == 0xFFFF + typedef unsigned short snes_ntsc_out_t; + #else + #error "Need 16-bit int type" + #endif + +#else + #if UINT_MAX == 0xFFFFFFFF + typedef unsigned int snes_ntsc_out_t; + #elif ULONG_MAX == 0xFFFFFFFF + typedef unsigned long snes_ntsc_out_t; + #else + #error "Need 32-bit int type" + #endif + +#endif diff --git a/ui/sdl/sdldisplay.c b/ui/sdl/sdldisplay.c index 52ef7913..bd870331 100644 --- a/ui/sdl/sdldisplay.c +++ b/ui/sdl/sdldisplay.c @@ -118,7 +118,6 @@ init_scalers( void ) scaler_register( SCALER_ADVMAME2X ); scaler_register( SCALER_ADVMAME3X ); scaler_register( SCALER_DOTMATRIX ); - scaler_register( SCALER_PALTV ); scaler_register( SCALER_HQ2X ); if( machine_current->timex ) { scaler_register( SCALER_HALF ); @@ -138,6 +137,9 @@ init_scalers( void ) scaler_register( SCALER_PALTV4X ); scaler_register( SCALER_HQ3X ); scaler_register( SCALER_HQ4X ); + scaler_register( SCALER_NTSC2X ); + scaler_register( SCALER_NTSC3X ); + scaler_register( SCALER_NTSC4X ); } if( scaler_is_supported( current_scaler ) ) { @@ -771,7 +773,8 @@ uidisplay_frame_end( void ) } /* Force a full redraw if requested */ - if ( sdldisplay_force_full_refresh ) { + if( sdldisplay_force_full_refresh || + ( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) ) { num_rects = 1; updated_rects[0].x = 0; diff --git a/ui/sdl2/sdl2_display.c b/ui/sdl2/sdl2_display.c index b0e2b0a3..a7edb5ce 100644 --- a/ui/sdl2/sdl2_display.c +++ b/ui/sdl2/sdl2_display.c @@ -89,7 +89,6 @@ init_scalers( void ) scaler_register( SCALER_ADVMAME2X ); scaler_register( SCALER_ADVMAME3X ); scaler_register( SCALER_DOTMATRIX ); - scaler_register( SCALER_PALTV ); scaler_register( SCALER_HQ2X ); if( machine_current->timex ) { scaler_register( SCALER_HALF ); @@ -109,6 +108,9 @@ init_scalers( void ) scaler_register( SCALER_PALTV4X ); scaler_register( SCALER_HQ3X ); scaler_register( SCALER_HQ4X ); + scaler_register( SCALER_NTSC2X ); + scaler_register( SCALER_NTSC3X ); + scaler_register( SCALER_NTSC4X ); } if( scaler_is_supported( current_scaler ) ) { @@ -845,7 +847,8 @@ uidisplay_frame_end( void ) if( sdl2_status_updated ) sdl2display_queue_status_rects(); - if( sdl2display_force_full_refresh ) { + if( sdl2display_force_full_refresh || + ( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) ) { num_rects = 1; updated_rects[0].x = 0; updated_rects[0].y = 0; diff --git a/ui/sdl2/sdl2_scaler_state.c b/ui/sdl2/sdl2_scaler_state.c index 70f008e2..114b09d2 100644 --- a/ui/sdl2/sdl2_scaler_state.c +++ b/ui/sdl2/sdl2_scaler_state.c @@ -22,7 +22,8 @@ typedef enum sdl2_scaler_family { SDL2_SCALER_FAMILY_HQ, SDL2_SCALER_FAMILY_TIMEX, SDL2_SCALER_FAMILY_SPECIAL2X, - SDL2_SCALER_FAMILY_DOTMATRIX + SDL2_SCALER_FAMILY_DOTMATRIX, + SDL2_SCALER_FAMILY_NTSCTV } sdl2_scaler_family; static sdl2_scaler_family scaler_family( scaler_type scaler ); @@ -172,7 +173,6 @@ scaler_family( scaler_type scaler ) case SCALER_TV4X: return SDL2_SCALER_FAMILY_TV; - case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: @@ -201,6 +201,11 @@ scaler_family( scaler_type scaler ) case SCALER_DOTMATRIX: return SDL2_SCALER_FAMILY_DOTMATRIX; + case SCALER_NTSC2X: + case SCALER_NTSC3X: + case SCALER_NTSC4X: + return SDL2_SCALER_FAMILY_NTSCTV; + default: return SDL2_SCALER_FAMILY_NONE; } diff --git a/ui/wii/wiidisplay.c b/ui/wii/wiidisplay.c index 940da2e0..2c0f6e26 100644 --- a/ui/wii/wiidisplay.c +++ b/ui/wii/wiidisplay.c @@ -215,7 +215,6 @@ register_scalers( void ) switch( image_scale ) { case 1: scaler_register( SCALER_NORMAL ); - scaler_register( SCALER_PALTV ); if( !scaler_is_supported( current_scaler ) ) scaler_select_scaler( SCALER_NORMAL ); return 0; @@ -246,7 +245,6 @@ register_scalers( void ) case 2: scaler_register( SCALER_NORMAL ); scaler_register( SCALER_TIMEXTV ); - scaler_register( SCALER_PALTV ); if( !scaler_is_supported( current_scaler ) ) scaler_select_scaler( SCALER_NORMAL ); return 0; diff --git a/ui/win32/win32display.c b/ui/win32/win32display.c index 84bb3bd8..e09869b9 100644 --- a/ui/win32/win32display.c +++ b/ui/win32/win32display.c @@ -51,10 +51,6 @@ ptrdiff_t win32display_pitch = DISPLAY_SCREEN_WIDTH * static unsigned char rgb_image[ 4 * 2 * ( DISPLAY_SCREEN_HEIGHT + 4 ) * ( DISPLAY_SCREEN_WIDTH + 3 ) ]; static const int rgb_pitch = ( DISPLAY_SCREEN_WIDTH + 3 ) * 4; - -/* The scaled image */ -static unsigned char scaled_image[ MAX_SCALE * DISPLAY_SCREEN_HEIGHT * - MAX_SCALE * DISPLAY_SCREEN_WIDTH * 2 ]; static const ptrdiff_t scaled_pitch = MAX_SCALE * DISPLAY_SCREEN_WIDTH * 2; /* Win32 specific variables */ @@ -129,6 +125,8 @@ win32display_init( void ) libspectrum_dword black; error = init_colours(); if( error ) return error; + error = scaler_select_bitformat( BITFORMAT_X8R8G8B8 ); + if( error ) return error; black = settings_current.bw_tv ? bw_colours[0] : win32display_colours[0]; @@ -186,13 +184,13 @@ init_colours( void ) #ifdef WORDS_BIGENDIAN - win32display_colours[i] = red << 24 | green << 16 | blue << 8; - bw_colours[i] = grey << 24 | grey << 16 | grey << 8; + win32display_colours[i] = blue << 24 | green << 16 | red << 8; + bw_colours[i] = grey << 24 | grey << 16 | grey << 8; #else /* #ifdef WORDS_BIGENDIAN */ - win32display_colours[i] = red | green << 8 | blue << 16; - bw_colours[i] = grey | grey << 8 | grey << 16; + win32display_colours[i] = blue | green << 8 | red << 16; + bw_colours[i] = grey | grey << 8 | grey << 16; #endif /* #ifdef WORDS_BIGENDIAN */ @@ -217,7 +215,6 @@ win32display_drawing_area_resize( int width, int height, int force_scaler ) register_scalers( force_scaler ); - memset( scaled_image, 0, sizeof( scaled_image ) ); display_refresh_all(); return 0; @@ -279,9 +276,11 @@ register_scalers( int force_scaler ) scaler_register( SCALER_SUPER2XSAI ); scaler_register( SCALER_SUPEREAGLE ); scaler_register( SCALER_DOTMATRIX ); + scaler_register( SCALER_NTSC2X ); + scaler_register( SCALER_NTSC3X ); + scaler_register( SCALER_NTSC4X ); } scaler_register( SCALER_NORMAL ); - scaler_register( SCALER_PALTV ); scaler = scaler_is_supported( current_scaler ) ? current_scaler : SCALER_NORMAL; @@ -307,6 +306,10 @@ register_scalers( int force_scaler ) void uidisplay_frame_end( void ) { + if( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) { + uidisplay_area( 0, 0, image_width, image_height ); + } + if( !IsRectEmpty( &invalidated_area ) ) { InvalidateRect( fuse_hWnd, &invalidated_area, FALSE ); @@ -349,7 +352,8 @@ uidisplay_area( int x, int y, int w, int h ) /* Create scaled image */ scaler_proc32( &rgb_image[ ( y + 2 ) * rgb_pitch + 4 * ( x + 1 ) ], rgb_pitch, - &scaled_image[ scaled_y * scaled_pitch + 4 * scaled_x ], + (unsigned char *)win32_pixdata + scaled_y * scaled_pitch + + 4 * scaled_x, scaled_pitch, w, h ); w *= scale; h *= scale; @@ -361,26 +365,12 @@ uidisplay_area( int x, int y, int w, int h ) void win32display_area(int x, int y, int width, int height) { - int disp_x,disp_y; int bottom, right; - long ofs; RECT r; - char *pixdata = win32_pixdata; bottom = y + height; right = x + width; - for( disp_y = y; disp_y < bottom; disp_y++ ) { - for( disp_x = x; disp_x < right; disp_x++ ) { - ofs = ( disp_x << 2 ) + ( disp_y * scaled_pitch ); - - pixdata[ ofs + 0 ] = scaled_image[ ofs + 2 ]; /* blue */ - pixdata[ ofs + 1 ] = scaled_image[ ofs + 1 ]; /* green */ - pixdata[ ofs + 2 ] = scaled_image[ ofs + 0 ]; /* red */ - pixdata[ ofs + 3 ] = 0; /* unused */ - } - } - /* Mark area for updating */ SetRect( &r, x, y, right, bottom ); UnionRect( &invalidated_area, &invalidated_area, &r ); diff --git a/ui/xlib/xdisplay.c b/ui/xlib/xdisplay.c index 60c0f03e..a7ece696 100644 --- a/ui/xlib/xdisplay.c +++ b/ui/xlib/xdisplay.c @@ -1,7 +1,7 @@ /* xdisplay.c: Routines for dealing with drawing the Speccy's screen via Xlib - Copyright (c) 2000-2021 Philip Kendall, Darren Salt, Gergely Szsz + Copyright (c) 2000-2021 Philip Kendall, Darren Salt, Gergely Sz�sz Copyright (c) 2015 Stuart Brady - Copyright (c) 2015-2024 Sergio Baldov + Copyright (c) 2015-2024 Sergio Baldov� This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -613,7 +613,6 @@ register_scalers( void ) } } else { scaler_register( SCALER_NORMAL ); - scaler_register( SCALER_PALTV ); if( machine_current->timex ) { scaler_register( SCALER_HALF ); scaler_register( SCALER_HALFSKIP ); @@ -649,6 +648,10 @@ register_scalers( void ) scaler_register( SCALER_TV4X ); scaler_register( SCALER_PALTV4X ); scaler_register( SCALER_HQ4X ); + + scaler_register( SCALER_NTSC2X ); + scaler_register( SCALER_NTSC3X ); + scaler_register( SCALER_NTSC4X ); } } if( current_scaler != SCALER_NUM ) @@ -666,17 +669,16 @@ register_scalers( void ) new_timex_scaler = SCALER_HALFSKIP; } else { switch( current_scaler ) { - case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: - new_scaler = SCALER_PALTV; + new_scaler = SCALER_NORMAL; new_timex_scaler = SCALER_HALF; break; case SCALER_TV2X: case SCALER_TV3X: case SCALER_TV4X: - new_scaler = SCALER_PALTV; + new_scaler = SCALER_NORMAL; new_timex_scaler = SCALER_HALF; break; default: @@ -692,18 +694,17 @@ register_scalers( void ) new_timex_scaler = SCALER_NORMAL; } else { switch( current_scaler ) { - case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: new_scaler = SCALER_PALTV2X; - new_timex_scaler = SCALER_PALTV; + new_timex_scaler = SCALER_NORMAL; break; case SCALER_TV2X: case SCALER_TV3X: case SCALER_TV4X: new_scaler = SCALER_TV2X; - new_timex_scaler = SCALER_PALTV; + new_timex_scaler = SCALER_NORMAL; break; case SCALER_2XSAI: case SCALER_SUPER2XSAI: @@ -732,7 +733,6 @@ register_scalers( void ) new_timex_scaler = SCALER_TIMEX1_5X; } else { switch( current_scaler ) { - case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: @@ -772,7 +772,6 @@ register_scalers( void ) new_timex_scaler = SCALER_TIMEX2X; } else { switch( current_scaler ) { - case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X: @@ -885,7 +884,8 @@ uidisplay_frame_end( void ) X_Rect *r, *last_rect; /* Force a full redraw if requested */ - if ( xdisplay_force_full_refresh ) { + if( xdisplay_force_full_refresh || + ( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) ) { num_rects = 1; updated_rects[0].x = 0; diff --git a/unittests/sdl2scalerstatetest.c b/unittests/sdl2scalerstatetest.c index 931991fc..209d52c8 100644 --- a/unittests/sdl2scalerstatetest.c +++ b/unittests/sdl2scalerstatetest.c @@ -26,7 +26,6 @@ fill_test_scales( float *scales ) scales[ SCALER_HALFSKIP ] = 0.5f; scales[ SCALER_NORMAL ] = 1.0f; scales[ SCALER_TIMEXTV ] = 1.0f; - scales[ SCALER_PALTV ] = 1.0f; scales[ SCALER_TIMEX1_5X ] = 1.5f; scales[ SCALER_DOUBLESIZE ] = 2.0f; scales[ SCALER_2XSAI ] = 2.0f; From ec8fe52c41fdb7f5cc226c156d5b777d99b7dfbd Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 1 Jun 2026 22:12:19 +1000 Subject: [PATCH 026/142] Add remaining ED-prefix disassembler coverage 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. --- debugger/disassemble.c | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 42767fdd..1ceb91ad 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -915,12 +915,28 @@ libspectrum_byte test49_data[] = { 0xed, 0x5e }; /* IM 2 */ /* ED prefix: LD I,A and LD A,I */ libspectrum_byte test50_data[] = { 0xed, 0x47 }; /* LD I,A */ libspectrum_byte test51_data[] = { 0xed, 0x57 }; /* LD A,I */ +libspectrum_byte test56_data[] = { 0xed, 0x4f }; /* LD R,A */ +libspectrum_byte test57_data[] = { 0xed, 0x5f }; /* LD A,R */ +libspectrum_byte test58_data[] = { 0xed, 0x67 }; /* RRD */ +libspectrum_byte test59_data[] = { 0xed, 0x6f }; /* RLD */ /* ED prefix: block instructions */ libspectrum_byte test52_data[] = { 0xed, 0xa0 }; /* LDI */ libspectrum_byte test53_data[] = { 0xed, 0xb0 }; /* LDIR */ libspectrum_byte test54_data[] = { 0xed, 0xa8 }; /* LDD */ libspectrum_byte test55_data[] = { 0xed, 0xb8 }; /* LDDR */ +libspectrum_byte test60_data[] = { 0xed, 0xa1 }; /* CPI */ +libspectrum_byte test61_data[] = { 0xed, 0xa9 }; /* CPD */ +libspectrum_byte test62_data[] = { 0xed, 0xb1 }; /* CPIR */ +libspectrum_byte test63_data[] = { 0xed, 0xb9 }; /* CPDR */ +libspectrum_byte test64_data[] = { 0xed, 0xa2 }; /* INI */ +libspectrum_byte test65_data[] = { 0xed, 0xaa }; /* IND */ +libspectrum_byte test66_data[] = { 0xed, 0xb2 }; /* INIR */ +libspectrum_byte test67_data[] = { 0xed, 0xba }; /* INDR */ +libspectrum_byte test68_data[] = { 0xed, 0xa3 }; /* OUTI */ +libspectrum_byte test69_data[] = { 0xed, 0xab }; /* OUTD */ +libspectrum_byte test70_data[] = { 0xed, 0xb3 }; /* OTIR */ +libspectrum_byte test71_data[] = { 0xed, 0xbb }; /* OTDR */ static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) @@ -1030,15 +1046,31 @@ debugger_disassemble_unittest( void ) r += run_test( test48_data, sizeof( test48_data ), "IM 1" ); r += run_test( test49_data, sizeof( test49_data ), "IM 2" ); - /* ED prefix: LD I,A and LD A,I */ + /* ED prefix: LD I,A, LD R,A, LD A,I, LD A,R, RRD, RLD */ r += run_test( test50_data, sizeof( test50_data ), "LD I,A" ); r += run_test( test51_data, sizeof( test51_data ), "LD A,I" ); + r += run_test( test56_data, sizeof( test56_data ), "LD R,A" ); + r += run_test( test57_data, sizeof( test57_data ), "LD A,R" ); + r += run_test( test58_data, sizeof( test58_data ), "RRD" ); + r += run_test( test59_data, sizeof( test59_data ), "RLD" ); /* ED prefix: block instructions */ r += run_test( test52_data, sizeof( test52_data ), "LDI" ); r += run_test( test53_data, sizeof( test53_data ), "LDIR" ); r += run_test( test54_data, sizeof( test54_data ), "LDD" ); r += run_test( test55_data, sizeof( test55_data ), "LDDR" ); + r += run_test( test60_data, sizeof( test60_data ), "CPI" ); + r += run_test( test61_data, sizeof( test61_data ), "CPD" ); + r += run_test( test62_data, sizeof( test62_data ), "CPIR" ); + r += run_test( test63_data, sizeof( test63_data ), "CPDR" ); + r += run_test( test64_data, sizeof( test64_data ), "INI" ); + r += run_test( test65_data, sizeof( test65_data ), "IND" ); + r += run_test( test66_data, sizeof( test66_data ), "INIR" ); + r += run_test( test67_data, sizeof( test67_data ), "INDR" ); + r += run_test( test68_data, sizeof( test68_data ), "OUTI" ); + r += run_test( test69_data, sizeof( test69_data ), "OUTD" ); + r += run_test( test70_data, sizeof( test70_data ), "OTIR" ); + r += run_test( test71_data, sizeof( test71_data ), "OTDR" ); return r; } From 2242ed34121d2e604c84e89edca59f5ef2bb69ce Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 1 Jun 2026 22:18:04 +1000 Subject: [PATCH 027/142] Extend disassembler coverage for CB rotations and register pairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(). --- debugger/disassemble.c | 60 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 1ceb91ad..497dcddb 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -854,6 +854,19 @@ libspectrum_byte test13_data[] = { 0xfd, 0xdd, 0xfd, 0xdd, 0xfd, 0xdd, 0xfd, libspectrum_byte test14_data[] = { 0x7e }; libspectrum_byte test15_data[] = { 0xdd, 0x7e, 0x55 }; +/* CB prefix rotation/shift tests (one per operation, using register B) */ +libspectrum_byte test72_data[] = { 0xcb, 0x00 }; /* RLC B */ +libspectrum_byte test73_data[] = { 0xcb, 0x08 }; /* RRC B */ +libspectrum_byte test74_data[] = { 0xcb, 0x10 }; /* RL B */ +libspectrum_byte test75_data[] = { 0xcb, 0x18 }; /* RR B */ +libspectrum_byte test76_data[] = { 0xcb, 0x20 }; /* SLA B */ +libspectrum_byte test77_data[] = { 0xcb, 0x28 }; /* SRA B */ +libspectrum_byte test78_data[] = { 0xcb, 0x30 }; /* SLL B (undocumented) */ +libspectrum_byte test79_data[] = { 0xcb, 0x38 }; /* SRL B */ + +/* CB prefix rotation on (HL) — exercises (HL) indirect addressing */ +libspectrum_byte test80_data[] = { 0xcb, 0x06 }; /* RLC (HL) */ + /* CB prefix BIT/RES/SET tests */ libspectrum_byte test16_data[] = { 0xcb, 0x47 }; /* BIT 0,A */ libspectrum_byte test17_data[] = { 0xcb, 0x87 }; /* RES 0,A */ @@ -894,13 +907,25 @@ libspectrum_byte test37_data[] = { 0xed, 0x70 }; /* IN F,(C) — special case * libspectrum_byte test38_data[] = { 0xed, 0x41 }; /* OUT (C),B */ libspectrum_byte test39_data[] = { 0xed, 0x71 }; /* OUT (C),0 — special case */ -/* ED prefix: SBC HL,rr and ADC HL,rr */ +/* ED prefix: SBC HL,rr and ADC HL,rr — all register pairs */ libspectrum_byte test40_data[] = { 0xed, 0x42 }; /* SBC HL,BC */ libspectrum_byte test41_data[] = { 0xed, 0x4a }; /* ADC HL,BC */ - -/* ED prefix: LD (nn),rr and LD rr,(nn) */ +libspectrum_byte test81_data[] = { 0xed, 0x52 }; /* SBC HL,DE */ +libspectrum_byte test82_data[] = { 0xed, 0x5a }; /* ADC HL,DE */ +libspectrum_byte test83_data[] = { 0xed, 0x62 }; /* SBC HL,HL */ +libspectrum_byte test84_data[] = { 0xed, 0x6a }; /* ADC HL,HL */ +libspectrum_byte test85_data[] = { 0xed, 0x72 }; /* SBC HL,SP */ +libspectrum_byte test86_data[] = { 0xed, 0x7a }; /* ADC HL,SP */ + +/* ED prefix: LD (nn),rr and LD rr,(nn) — all register pairs */ libspectrum_byte test42_data[] = { 0xed, 0x43, 0x56, 0x34 }; /* LD (3456),BC */ libspectrum_byte test43_data[] = { 0xed, 0x4b, 0x56, 0x34 }; /* LD BC,(3456) */ +libspectrum_byte test87_data[] = { 0xed, 0x53, 0x56, 0x34 }; /* LD (3456),DE */ +libspectrum_byte test88_data[] = { 0xed, 0x5b, 0x56, 0x34 }; /* LD DE,(3456) */ +libspectrum_byte test89_data[] = { 0xed, 0x63, 0x56, 0x34 }; /* LD (3456),HL */ +libspectrum_byte test90_data[] = { 0xed, 0x6b, 0x56, 0x34 }; /* LD HL,(3456) */ +libspectrum_byte test91_data[] = { 0xed, 0x73, 0x56, 0x34 }; /* LD (3456),SP */ +libspectrum_byte test92_data[] = { 0xed, 0x7b, 0x56, 0x34 }; /* LD SP,(3456) */ /* ED prefix: NEG, RETN, RETI */ libspectrum_byte test44_data[] = { 0xed, 0x44 }; /* NEG */ @@ -988,6 +1013,17 @@ debugger_disassemble_unittest( void ) r += run_test( test14_data, sizeof( test14_data ), "LD A,(HL)" ); r += run_test( test15_data, sizeof( test15_data ), "LD A,(IX+55)" ); + /* CB prefix rotation/shift (one per operation) */ + r += run_test( test72_data, sizeof( test72_data ), "RLC B" ); + r += run_test( test73_data, sizeof( test73_data ), "RRC B" ); + r += run_test( test74_data, sizeof( test74_data ), "RL B" ); + r += run_test( test75_data, sizeof( test75_data ), "RR B" ); + r += run_test( test76_data, sizeof( test76_data ), "SLA B" ); + r += run_test( test77_data, sizeof( test77_data ), "SRA B" ); + r += run_test( test78_data, sizeof( test78_data ), "SLL B" ); + r += run_test( test79_data, sizeof( test79_data ), "SRL B" ); + r += run_test( test80_data, sizeof( test80_data ), "RLC (HL)" ); + /* CB prefix BIT/RES/SET */ r += run_test( test16_data, sizeof( test16_data ), "BIT 0,A" ); r += run_test( test17_data, sizeof( test17_data ), "RES 0,A" ); @@ -1028,13 +1064,25 @@ debugger_disassemble_unittest( void ) r += run_test( test38_data, sizeof( test38_data ), "OUT (C),B" ); r += run_test( test39_data, sizeof( test39_data ), "OUT (C),0" ); - /* ED prefix: SBC HL,rr and ADC HL,rr */ + /* ED prefix: SBC HL,rr and ADC HL,rr — all register pairs */ r += run_test( test40_data, sizeof( test40_data ), "SBC HL,BC" ); r += run_test( test41_data, sizeof( test41_data ), "ADC HL,BC" ); - - /* ED prefix: LD (nn),rr and LD rr,(nn) */ + r += run_test( test81_data, sizeof( test81_data ), "SBC HL,DE" ); + r += run_test( test82_data, sizeof( test82_data ), "ADC HL,DE" ); + r += run_test( test83_data, sizeof( test83_data ), "SBC HL,HL" ); + r += run_test( test84_data, sizeof( test84_data ), "ADC HL,HL" ); + r += run_test( test85_data, sizeof( test85_data ), "SBC HL,SP" ); + r += run_test( test86_data, sizeof( test86_data ), "ADC HL,SP" ); + + /* ED prefix: LD (nn),rr and LD rr,(nn) — all register pairs */ r += run_test( test42_data, sizeof( test42_data ), "LD (3456),BC" ); r += run_test( test43_data, sizeof( test43_data ), "LD BC,(3456)" ); + r += run_test( test87_data, sizeof( test87_data ), "LD (3456),DE" ); + r += run_test( test88_data, sizeof( test88_data ), "LD DE,(3456)" ); + r += run_test( test89_data, sizeof( test89_data ), "LD (3456),HL" ); + r += run_test( test90_data, sizeof( test90_data ), "LD HL,(3456)" ); + r += run_test( test91_data, sizeof( test91_data ), "LD (3456),SP" ); + r += run_test( test92_data, sizeof( test92_data ), "LD SP,(3456)" ); /* ED prefix: NEG, RETN, RETI */ r += run_test( test44_data, sizeof( test44_data ), "NEG" ); From 60a9807342917da82c9e7ab057b74115ede3c77e Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 1 Jun 2026 22:39:05 +1000 Subject: [PATCH 028/142] Add DD/FD prefix (IX/IY) disassembler coverage 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 --- debugger/disassemble.c | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 497dcddb..7b25513e 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -963,6 +963,64 @@ libspectrum_byte test69_data[] = { 0xed, 0xab }; /* OUTD */ libspectrum_byte test70_data[] = { 0xed, 0xb3 }; /* OTIR */ libspectrum_byte test71_data[] = { 0xed, 0xbb }; /* OTDR */ +/* DD prefix: ADD IX,rr completeness */ +libspectrum_byte test93_data[] = { 0xdd, 0x19 }; /* ADD IX,DE */ +libspectrum_byte test94_data[] = { 0xdd, 0x29 }; /* ADD IX,IX */ +libspectrum_byte test95_data[] = { 0xdd, 0x39 }; /* ADD IX,SP */ + +/* FD prefix: ADD IY,rr completeness */ +libspectrum_byte test96_data[] = { 0xfd, 0x19 }; /* ADD IY,DE */ +libspectrum_byte test97_data[] = { 0xfd, 0x29 }; /* ADD IY,IY */ +libspectrum_byte test98_data[] = { 0xfd, 0x39 }; /* ADD IY,SP */ + +/* DD/FD prefix: LD IX/IY,nn */ +libspectrum_byte test99_data[] = { 0xdd, 0x21, 0x34, 0x12 }; /* LD IX,1234 */ +libspectrum_byte test100_data[] = { 0xfd, 0x21, 0x34, 0x12 }; /* LD IY,1234 */ + +/* DD/FD prefix: INC/DEC IX/IY */ +libspectrum_byte test101_data[] = { 0xdd, 0x23 }; /* INC IX */ +libspectrum_byte test102_data[] = { 0xdd, 0x2b }; /* DEC IX */ +libspectrum_byte test103_data[] = { 0xfd, 0x23 }; /* INC IY */ +libspectrum_byte test104_data[] = { 0xfd, 0x2b }; /* DEC IY */ + +/* DD/FD prefix: LD (nn),IX/IY and LD IX/IY,(nn) */ +libspectrum_byte test105_data[] = { 0xdd, 0x22, 0x34, 0x12 }; /* LD (1234),IX */ +libspectrum_byte test106_data[] = { 0xdd, 0x2a, 0x34, 0x12 }; /* LD IX,(1234) */ +libspectrum_byte test107_data[] = { 0xfd, 0x22, 0x34, 0x12 }; /* LD (1234),IY */ +libspectrum_byte test108_data[] = { 0xfd, 0x2a, 0x34, 0x12 }; /* LD IY,(1234) */ + +/* DD/FD prefix: PUSH/POP IX/IY */ +libspectrum_byte test109_data[] = { 0xdd, 0xe5 }; /* PUSH IX */ +libspectrum_byte test110_data[] = { 0xdd, 0xe1 }; /* POP IX */ +libspectrum_byte test111_data[] = { 0xfd, 0xe5 }; /* PUSH IY */ +libspectrum_byte test112_data[] = { 0xfd, 0xe1 }; /* POP IY */ + +/* DD/FD prefix: JP (IX/IY), EX (SP),IX/IY, LD SP,IX/IY */ +libspectrum_byte test113_data[] = { 0xdd, 0xe9 }; /* JP (IX) */ +libspectrum_byte test114_data[] = { 0xdd, 0xe3 }; /* EX (SP),IX */ +libspectrum_byte test115_data[] = { 0xdd, 0xf9 }; /* LD SP,IX */ +libspectrum_byte test116_data[] = { 0xfd, 0xe9 }; /* JP (IY) */ +libspectrum_byte test117_data[] = { 0xfd, 0xe3 }; /* EX (SP),IY */ +libspectrum_byte test118_data[] = { 0xfd, 0xf9 }; /* LD SP,IY */ + +/* DD/FD prefix: undocumented IXh, IXl, IYh, IYl registers */ +libspectrum_byte test119_data[] = { 0xdd, 0x7c }; /* LD A,IXh */ +libspectrum_byte test120_data[] = { 0xdd, 0x7d }; /* LD A,IXl */ +libspectrum_byte test121_data[] = { 0xfd, 0x7c }; /* LD A,IYh */ +libspectrum_byte test122_data[] = { 0xfd, 0x7d }; /* LD A,IYl */ + +/* DD/FD prefix: LD r,(IX/IY+d) */ +libspectrum_byte test123_data[] = { 0xdd, 0x46, 0x05 }; /* LD B,(IX+05) */ +libspectrum_byte test124_data[] = { 0xfd, 0x46, 0x05 }; /* LD B,(IY+05) */ + +/* DD/FD prefix: LD (IX/IY+d),r */ +libspectrum_byte test125_data[] = { 0xdd, 0x70, 0x05 }; /* LD (IX+05),B */ +libspectrum_byte test126_data[] = { 0xfd, 0x70, 0x05 }; /* LD (IY+05),B */ + +/* DD/FD prefix: LD (IX/IY+d),n */ +libspectrum_byte test127_data[] = { 0xdd, 0x36, 0x05, 0x07 }; /* LD (IX+05),07 */ +libspectrum_byte test128_data[] = { 0xfd, 0x36, 0x05, 0x07 }; /* LD (IY+05),07 */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1120,5 +1178,63 @@ debugger_disassemble_unittest( void ) r += run_test( test70_data, sizeof( test70_data ), "OTIR" ); r += run_test( test71_data, sizeof( test71_data ), "OTDR" ); + /* DD prefix: ADD IX,rr completeness */ + r += run_test( test93_data, sizeof( test93_data ), "ADD IX,DE" ); + r += run_test( test94_data, sizeof( test94_data ), "ADD IX,IX" ); + r += run_test( test95_data, sizeof( test95_data ), "ADD IX,SP" ); + + /* FD prefix: ADD IY,rr completeness */ + r += run_test( test96_data, sizeof( test96_data ), "ADD IY,DE" ); + r += run_test( test97_data, sizeof( test97_data ), "ADD IY,IY" ); + r += run_test( test98_data, sizeof( test98_data ), "ADD IY,SP" ); + + /* DD/FD prefix: LD IX/IY,nn */ + r += run_test( test99_data, sizeof( test99_data ), "LD IX,1234" ); + r += run_test( test100_data, sizeof( test100_data ), "LD IY,1234" ); + + /* DD/FD prefix: INC/DEC IX/IY */ + r += run_test( test101_data, sizeof( test101_data ), "INC IX" ); + r += run_test( test102_data, sizeof( test102_data ), "DEC IX" ); + r += run_test( test103_data, sizeof( test103_data ), "INC IY" ); + r += run_test( test104_data, sizeof( test104_data ), "DEC IY" ); + + /* DD/FD prefix: LD (nn),IX/IY and LD IX/IY,(nn) */ + r += run_test( test105_data, sizeof( test105_data ), "LD (1234),IX" ); + r += run_test( test106_data, sizeof( test106_data ), "LD IX,(1234)" ); + r += run_test( test107_data, sizeof( test107_data ), "LD (1234),IY" ); + r += run_test( test108_data, sizeof( test108_data ), "LD IY,(1234)" ); + + /* DD/FD prefix: PUSH/POP IX/IY */ + r += run_test( test109_data, sizeof( test109_data ), "PUSH IX" ); + r += run_test( test110_data, sizeof( test110_data ), "POP IX" ); + r += run_test( test111_data, sizeof( test111_data ), "PUSH IY" ); + r += run_test( test112_data, sizeof( test112_data ), "POP IY" ); + + /* DD/FD prefix: JP (IX/IY), EX (SP),IX/IY, LD SP,IX/IY */ + r += run_test( test113_data, sizeof( test113_data ), "JP (IX)" ); + r += run_test( test114_data, sizeof( test114_data ), "EX (SP),IX" ); + r += run_test( test115_data, sizeof( test115_data ), "LD SP,IX" ); + r += run_test( test116_data, sizeof( test116_data ), "JP (IY)" ); + r += run_test( test117_data, sizeof( test117_data ), "EX (SP),IY" ); + r += run_test( test118_data, sizeof( test118_data ), "LD SP,IY" ); + + /* DD/FD prefix: undocumented IXh, IXl, IYh, IYl registers */ + r += run_test( test119_data, sizeof( test119_data ), "LD A,IXh" ); + r += run_test( test120_data, sizeof( test120_data ), "LD A,IXl" ); + r += run_test( test121_data, sizeof( test121_data ), "LD A,IYh" ); + r += run_test( test122_data, sizeof( test122_data ), "LD A,IYl" ); + + /* DD/FD prefix: LD r,(IX/IY+d) */ + r += run_test( test123_data, sizeof( test123_data ), "LD B,(IX+05)" ); + r += run_test( test124_data, sizeof( test124_data ), "LD B,(IY+05)" ); + + /* DD/FD prefix: LD (IX/IY+d),r */ + r += run_test( test125_data, sizeof( test125_data ), "LD (IX+05),B" ); + r += run_test( test126_data, sizeof( test126_data ), "LD (IY+05),B" ); + + /* DD/FD prefix: LD (IX/IY+d),n */ + r += run_test( test127_data, sizeof( test127_data ), "LD (IX+05),07" ); + r += run_test( test128_data, sizeof( test128_data ), "LD (IY+05),07" ); + return r; } From 13b5bd2275e11a7a57772074652e56a90d26ce6a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 2 Jun 2026 12:47:47 +1000 Subject: [PATCH 029/142] Add disassembler unit tests for main (unprefixed) Z80 opcodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 54 new tests (test129–test182) to debugger_disassemble_unittest() covering previously untested main (unprefixed) Z80 instruction groups: - EX AF,AF' (0x08) - Accumulator/flag ops: RLCA, RRCA, RLA, RRA, DAA, CPL, SCF, CCF - 16-bit ops: LD rr,nn (BC), INC BC, DEC BC, ADD HL,DE - 8-bit ops: INC B, DEC B, LD B,07 - Store: LD (BC),A, LD (DE),A, LD (1234),HL, LD (1234),A - Load: LD A,(BC), LD A,(DE), LD HL,(1234), LD A,(1234) - HALT and LD B,C - ALU: ADD A,B, SUB C, AND D, XOR E, OR H, CP L - Immediate ALU: ADD A,07 - Conditional RET (NZ), POP/PUSH (BC, AF), RET, EXX, JP (HL), LD SP,HL - JP NZ/1234, OUT (07),A, IN A,(07), EX (SP),HL, EX DE,HL, DI, EI - CALL NZ/1234, PUSH (DE) - RST 0, RST 28 --- debugger/disassemble.c | 164 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 7b25513e..b9fd1f8c 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1021,6 +1021,86 @@ libspectrum_byte test126_data[] = { 0xfd, 0x70, 0x05 }; /* LD (IY+05),B */ libspectrum_byte test127_data[] = { 0xdd, 0x36, 0x05, 0x07 }; /* LD (IX+05),07 */ libspectrum_byte test128_data[] = { 0xfd, 0x36, 0x05, 0x07 }; /* LD (IY+05),07 */ +/* Main (unprefixed) opcodes: 00xxx000 group */ +libspectrum_byte test129_data[] = { 0x08 }; /* EX AF,AF' */ + +/* 00xxx111 group: rotate/accumulator flags */ +libspectrum_byte test130_data[] = { 0x07 }; /* RLCA */ +libspectrum_byte test131_data[] = { 0x0f }; /* RRCA */ +libspectrum_byte test132_data[] = { 0x17 }; /* RLA */ +libspectrum_byte test133_data[] = { 0x1f }; /* RRA */ +libspectrum_byte test134_data[] = { 0x27 }; /* DAA */ +libspectrum_byte test135_data[] = { 0x2f }; /* CPL */ +libspectrum_byte test136_data[] = { 0x37 }; /* SCF */ +libspectrum_byte test137_data[] = { 0x3f }; /* CCF */ + +/* 00xxx001: LD rr,nn; 00xxx011/00xxx1011: INC/DEC rr; 00xxx1001: ADD HL,rr */ +libspectrum_byte test138_data[] = { 0x01, 0x34, 0x12 }; /* LD BC,1234 */ +libspectrum_byte test139_data[] = { 0x03 }; /* INC BC */ +libspectrum_byte test140_data[] = { 0x0b }; /* DEC BC */ +libspectrum_byte test141_data[] = { 0x19 }; /* ADD HL,DE */ + +/* 00xxx100/101/110: INC r, DEC r, LD r,n */ +libspectrum_byte test142_data[] = { 0x04 }; /* INC B */ +libspectrum_byte test143_data[] = { 0x05 }; /* DEC B */ +libspectrum_byte test144_data[] = { 0x06, 0x07 }; /* LD B,07 */ + +/* 00xxx010: LD (rr),A / LD (nn),HL / LD (nn),A */ +libspectrum_byte test145_data[] = { 0x02 }; /* LD (BC),A */ +libspectrum_byte test146_data[] = { 0x12 }; /* LD (DE),A */ +libspectrum_byte test147_data[] = { 0x22, 0x34, 0x12 }; /* LD (1234),HL */ +libspectrum_byte test148_data[] = { 0x32, 0x34, 0x12 }; /* LD (1234),A */ + +/* 00xxx110 (0x0a column): LD A,(rr) / LD HL,(nn) / LD A,(nn) */ +libspectrum_byte test149_data[] = { 0x0a }; /* LD A,(BC) */ +libspectrum_byte test150_data[] = { 0x1a }; /* LD A,(DE) */ +libspectrum_byte test151_data[] = { 0x2a, 0x34, 0x12 }; /* LD HL,(1234) */ +libspectrum_byte test152_data[] = { 0x3a, 0x34, 0x12 }; /* LD A,(1234) */ + +/* 01xxxxxx: LD r,r' and HALT */ +libspectrum_byte test153_data[] = { 0x76 }; /* HALT */ +libspectrum_byte test154_data[] = { 0x41 }; /* LD B,C */ + +/* 10xxxxxx: arithmetic/logic on registers */ +libspectrum_byte test155_data[] = { 0x80 }; /* ADD A,B */ +libspectrum_byte test156_data[] = { 0x91 }; /* SUB C */ +libspectrum_byte test157_data[] = { 0xa2 }; /* AND D */ +libspectrum_byte test158_data[] = { 0xab }; /* XOR E */ +libspectrum_byte test159_data[] = { 0xb4 }; /* OR H */ +libspectrum_byte test160_data[] = { 0xbd }; /* CP L */ + +/* 11xxx110: immediate arithmetic/logic */ +libspectrum_byte test161_data[] = { 0xc6, 0x07 }; /* ADD A,07 */ + +/* 11xxx000: conditional RET; 11xxx001: POP/RET/EXX/JP(HL)/PUSH/CALL */ +libspectrum_byte test162_data[] = { 0xc0 }; /* RET NZ */ +libspectrum_byte test163_data[] = { 0xc1 }; /* POP BC */ +libspectrum_byte test164_data[] = { 0xc9 }; /* RET */ +libspectrum_byte test165_data[] = { 0xd9 }; /* EXX */ +libspectrum_byte test166_data[] = { 0xe9 }; /* JP (HL) */ +libspectrum_byte test167_data[] = { 0xf1 }; /* POP AF */ +libspectrum_byte test168_data[] = { 0xf9 }; /* LD SP,HL */ + +/* 11xxx010: conditional JP; 11xxx011: JP/CB/OUT/IN/EX/DI/EI */ +libspectrum_byte test169_data[] = { 0xc2, 0x34, 0x12 }; /* JP NZ,1234 */ +libspectrum_byte test170_data[] = { 0xc3, 0x34, 0x12 }; /* JP 1234 */ +libspectrum_byte test171_data[] = { 0xd3, 0x07 }; /* OUT (07),A */ +libspectrum_byte test172_data[] = { 0xdb, 0x07 }; /* IN A,(07) */ +libspectrum_byte test173_data[] = { 0xe3 }; /* EX (SP),HL */ +libspectrum_byte test174_data[] = { 0xeb }; /* EX DE,HL */ +libspectrum_byte test175_data[] = { 0xf3 }; /* DI */ +libspectrum_byte test176_data[] = { 0xfb }; /* EI */ + +/* 11xxx100: conditional CALL; 11xxx101: PUSH/CALL */ +libspectrum_byte test177_data[] = { 0xc4, 0x34, 0x12 }; /* CALL NZ,1234 */ +libspectrum_byte test178_data[] = { 0xc5 }; /* PUSH BC */ +libspectrum_byte test179_data[] = { 0xcd, 0x34, 0x12 }; /* CALL 1234 */ +libspectrum_byte test180_data[] = { 0xf5 }; /* PUSH AF */ + +/* 11xxx111: RST */ +libspectrum_byte test181_data[] = { 0xc7 }; /* RST 0 */ +libspectrum_byte test182_data[] = { 0xef }; /* RST 28 */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1236,5 +1316,89 @@ debugger_disassemble_unittest( void ) r += run_test( test127_data, sizeof( test127_data ), "LD (IX+05),07" ); r += run_test( test128_data, sizeof( test128_data ), "LD (IY+05),07" ); + /* Main (unprefixed) opcodes */ + + /* 00xxx000 group: NOP already covered by test1; EX AF,AF' */ + r += run_test( test129_data, sizeof( test129_data ), "EX AF,AF'" ); + + /* 00xxx111 group: rotate/accumulator flags */ + r += run_test( test130_data, sizeof( test130_data ), "RLCA" ); + r += run_test( test131_data, sizeof( test131_data ), "RRCA" ); + r += run_test( test132_data, sizeof( test132_data ), "RLA" ); + r += run_test( test133_data, sizeof( test133_data ), "RRA" ); + r += run_test( test134_data, sizeof( test134_data ), "DAA" ); + r += run_test( test135_data, sizeof( test135_data ), "CPL" ); + r += run_test( test136_data, sizeof( test136_data ), "SCF" ); + r += run_test( test137_data, sizeof( test137_data ), "CCF" ); + + /* 00xxx001: LD rr,nn; 00xxx011/00xxx1011: INC/DEC rr; ADD HL,rr */ + r += run_test( test138_data, sizeof( test138_data ), "LD BC,1234" ); + r += run_test( test139_data, sizeof( test139_data ), "INC BC" ); + r += run_test( test140_data, sizeof( test140_data ), "DEC BC" ); + r += run_test( test141_data, sizeof( test141_data ), "ADD HL,DE" ); + + /* 00xxx100/101/110: INC r, DEC r, LD r,n */ + r += run_test( test142_data, sizeof( test142_data ), "INC B" ); + r += run_test( test143_data, sizeof( test143_data ), "DEC B" ); + r += run_test( test144_data, sizeof( test144_data ), "LD B,07" ); + + /* 00xxx010: LD (rr),A / LD (nn),HL / LD (nn),A */ + r += run_test( test145_data, sizeof( test145_data ), "LD (BC),A" ); + r += run_test( test146_data, sizeof( test146_data ), "LD (DE),A" ); + r += run_test( test147_data, sizeof( test147_data ), "LD (1234),HL" ); + r += run_test( test148_data, sizeof( test148_data ), "LD (1234),A" ); + + /* 00xxx110 (0x0a column): LD A,(rr) / LD HL,(nn) / LD A,(nn) */ + r += run_test( test149_data, sizeof( test149_data ), "LD A,(BC)" ); + r += run_test( test150_data, sizeof( test150_data ), "LD A,(DE)" ); + r += run_test( test151_data, sizeof( test151_data ), "LD HL,(1234)" ); + r += run_test( test152_data, sizeof( test152_data ), "LD A,(1234)" ); + + /* 01xxxxxx: LD r,r' and HALT */ + r += run_test( test153_data, sizeof( test153_data ), "HALT" ); + r += run_test( test154_data, sizeof( test154_data ), "LD B,C" ); + + /* 10xxxxxx: arithmetic/logic on registers */ + r += run_test( test155_data, sizeof( test155_data ), "ADD A,B" ); + r += run_test( test156_data, sizeof( test156_data ), "SUB C" ); + r += run_test( test157_data, sizeof( test157_data ), "AND D" ); + r += run_test( test158_data, sizeof( test158_data ), "XOR E" ); + r += run_test( test159_data, sizeof( test159_data ), "OR H" ); + r += run_test( test160_data, sizeof( test160_data ), "CP L" ); + + /* 11xxx110: immediate arithmetic */ + r += run_test( test161_data, sizeof( test161_data ), "ADD A,07" ); + + /* 11xxx000: conditional RET */ + r += run_test( test162_data, sizeof( test162_data ), "RET NZ" ); + + /* 11xxx001: POP, RET, EXX, JP(HL), POP AF, LD SP,HL */ + r += run_test( test163_data, sizeof( test163_data ), "POP BC" ); + r += run_test( test164_data, sizeof( test164_data ), "RET" ); + r += run_test( test165_data, sizeof( test165_data ), "EXX" ); + r += run_test( test166_data, sizeof( test166_data ), "JP (HL)" ); + r += run_test( test167_data, sizeof( test167_data ), "POP AF" ); + r += run_test( test168_data, sizeof( test168_data ), "LD SP,HL" ); + + /* 11xxx010: conditional JP; 11xxx011: JP/OUT/IN/EX/DI/EI */ + r += run_test( test169_data, sizeof( test169_data ), "JP NZ,1234" ); + r += run_test( test170_data, sizeof( test170_data ), "JP 1234" ); + r += run_test( test171_data, sizeof( test171_data ), "OUT (07),A" ); + r += run_test( test172_data, sizeof( test172_data ), "IN A,(07)" ); + r += run_test( test173_data, sizeof( test173_data ), "EX (SP),HL" ); + r += run_test( test174_data, sizeof( test174_data ), "EX DE,HL" ); + r += run_test( test175_data, sizeof( test175_data ), "DI" ); + r += run_test( test176_data, sizeof( test176_data ), "EI" ); + + /* 11xxx100: conditional CALL; 11xxx101: PUSH/CALL */ + r += run_test( test177_data, sizeof( test177_data ), "CALL NZ,1234" ); + r += run_test( test178_data, sizeof( test178_data ), "PUSH BC" ); + r += run_test( test179_data, sizeof( test179_data ), "CALL 1234" ); + r += run_test( test180_data, sizeof( test180_data ), "PUSH AF" ); + + /* 11xxx111: RST */ + r += run_test( test181_data, sizeof( test181_data ), "RST 0" ); + r += run_test( test182_data, sizeof( test182_data ), "RST 28" ); + return r; } From af848ab66299a5fe6229a7797da0fe1c950d5602 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 14:02:07 +1000 Subject: [PATCH 030/142] Update SourceForge HTTP URLs to HTTPS --- README | 14 +++++++------- configure.ac | 4 ++-- fuse.c | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README b/README index 2ce7483e..7544fe93 100644 --- a/README +++ b/README @@ -42,7 +42,7 @@ Help! doesn't work If you're having a problem using/running/building Fuse, the two places you're most likely to get help are the development mailing list or the official forums at -. +. What you'll need to run Fuse ---------------------------- @@ -54,7 +54,7 @@ Required: * X, SDL or framebuffer support. If you have GTK, you'll get a (much) nicer user interface under X. * libspectrum: this is available from - http://fuse-emulator.sourceforge.net/libspectrum.php + https://fuse-emulator.sourceforge.net/libspectrum.php Optional: @@ -67,7 +67,7 @@ Optional: If you've used Fuse prior to version 0.5.0, note that the external utilities (tzxlist, etc) are now available separately from Fuse -itself. See http://fuse-emulator.sourceforge.net/ for details. +itself. See https://fuse-emulator.sourceforge.net/ for details. macOS @@ -92,22 +92,22 @@ Closing comments Fuse has its own home page, which you can find at: -http://fuse-emulator.sourceforge.net/ +https://fuse-emulator.sourceforge.net/ and contains much of the information listed here. News of new versions of Fuse (and other important Fuse-related announcements) are distributed via the fuse-emulator-announce mailing list on SourceForge; see -http://lists.sourceforge.net/lists/listinfo/fuse-emulator-announce +https://lists.sourceforge.net/lists/listinfo/fuse-emulator-announce for details on how to subscribe and the like. If you've got any bug reports, suggestions or the like for Fuse, or just want to get involved in the development, this is coordinated via the fuse-emulator-devel mailing list, -http://lists.sourceforge.net/lists/listinfo/fuse-emulator-devel +https://lists.sourceforge.net/lists/listinfo/fuse-emulator-devel and the Fuse project page on SourceForge, -http://sourceforge.net/projects/fuse-emulator/ +https://sourceforge.net/projects/fuse-emulator/ Maintainer release notes are in `hacking/releasing.md'. diff --git a/configure.ac b/configure.ac index 05c85efb..55b860af 100644 --- a/configure.ac +++ b/configure.ac @@ -31,8 +31,8 @@ m4_define([fuse_rc_version], [fuse_major_version,fuse_minor_version,fuse_micro_v dnl Package info m4_define([fuse_copyright], ["(c) 1999-2026 Philip Kendall and others"]) -m4_define([fuse_url], [http://fuse-emulator.sourceforge.net/]) -m4_define([fuse_bugreport], [http://sourceforge.net/p/fuse-emulator/bugs/]) +m4_define([fuse_url], [https://fuse-emulator.sourceforge.net/]) +m4_define([fuse_bugreport], [https://sourceforge.net/p/fuse-emulator/bugs/]) AC_INIT([fuse],[fuse_version],[fuse_bugreport],[fuse],[fuse_url]) AC_CONFIG_SRCDIR(fuse.c) diff --git a/fuse.c b/fuse.c index 47af23cc..1acdb0d8 100644 --- a/fuse.c +++ b/fuse.c @@ -501,7 +501,7 @@ static void fuse_show_copyright(void) "'AUTHORS' for more details.\n" "\n" "For help, please mail or use\n" - "the forums at .\n" + "the forums at .\n" "\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" @@ -557,7 +557,7 @@ static void fuse_show_help( void ) "--version Print version number and exit.\n" "\n" "For help, please mail or use\n" - "the forums at .\n" + "the forums at .\n" "For complete documentation, see the manual page of Fuse.\n\n" ); } From c334a4f50735d578e2c45b9c3b424aa2f4f20106 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 14:07:31 +1000 Subject: [PATCH 031/142] Remove redundant autoreconf and name anonymous workflow steps build_macos_sub.yml ran autoreconf -i at the end of the 'Install dependencies' step, but the very next autogen-related step (step 6) already runs ./autogen.sh which invokes autoreconf -f -i -I m4. The earlier call is both redundant and weaker (no -f, no -I m4), so remove it. Also add explicit names to the anonymous 'verify_other' conditional steps in build_linux_sub.yml and build_macos_sub.yml so they appear with readable labels in the GitHub Actions UI. --- .github/workflows/build_linux_sub.yml | 3 ++- .github/workflows/build_macos_sub.yml | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_linux_sub.yml b/.github/workflows/build_linux_sub.yml index c0eb8a5c..fb6cdf75 100644 --- a/.github/workflows/build_linux_sub.yml +++ b/.github/workflows/build_linux_sub.yml @@ -90,7 +90,8 @@ jobs: .github/scripts/in_config.sh "User interface: ${{ inputs.verify_ui }}" .github/scripts/in_config.sh "Selected audio driver: ${{ inputs.verify_audio }}" - - if: ${{ inputs.verify_other }} + - name: (7b) Verify additional configure output + if: ${{ inputs.verify_other }} run: | .github/scripts/in_config.sh "${{ inputs.verify_other }}" diff --git a/.github/workflows/build_macos_sub.yml b/.github/workflows/build_macos_sub.yml index 68099f64..e2b7f42a 100644 --- a/.github/workflows/build_macos_sub.yml +++ b/.github/workflows/build_macos_sub.yml @@ -75,8 +75,6 @@ jobs: echo "LDFLAGS=-L$(brew --prefix)/lib -L$(brew --prefix libgcrypt)/lib -L$(brew --prefix libxml2)/lib" >> "$GITHUB_ENV" echo "PKG_CONFIG_PATH=$(brew --prefix)/lib/pkgconfig:$(brew --prefix libgcrypt)/lib/pkgconfig:$(brew --prefix libxml2)/lib/pkgconfig" >> "$GITHUB_ENV" - autoreconf -i - - name: (4) Download libspectrum artefact uses: actions/download-artifact@v8 with: @@ -112,7 +110,8 @@ jobs: .github/scripts/in_config.sh "User interface: ${{ inputs.verify_ui }}" .github/scripts/in_config.sh "Selected audio driver: ${{ inputs.verify_audio }}" - - if: ${{ inputs.verify_other }} + - name: (8b) Verify additional configure output + if: ${{ inputs.verify_other }} run: | .github/scripts/in_config.sh "${{ inputs.verify_other }}" From fae3af4e9d2fdda0c535eb028b2da59e82baffb0 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 14:12:08 +1000 Subject: [PATCH 032/142] Add decimal-mode disassembler unit tests The disassembler supports two output bases: hex (default) and decimal (debugger_output_base == 10). All 55 existing tests ran with base 16, leaving six code paths in get_byte(), get_word(), get_offset(), and ix_iy_offset() completely untested in decimal mode. Add debugger_disassemble_decimal_unittest() that: - saves and restores debugger_output_base around the tests so the hex suite is unaffected - exercises each numeric-formatting function with base 10 Tests added (all at address 0x4000): testd1 LD B,07h -> "LD B,7" (get_byte, 00xxx110 path) testd2 LD BC,1234h -> "LD BC,4660" (get_word, 00xx0001 path) testd3 JR +0 -> "JR 16386" (get_offset, 18h path) testd4 LD A,(IX+55h) -> "LD A,(IX+85)" (ix_iy_offset positive) testd5 BIT 0,(IX-01h) -> "BIT 0,(IX-1)" (ix_iy_offset negative) testd6 ADD A,07h -> "ADD A,7" (get_byte, 11xxx110 path) --- debugger/debugger.h | 1 + debugger/disassemble.c | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/debugger/debugger.h b/debugger/debugger.h index dca6c438..fe5f7a4a 100644 --- a/debugger/debugger.h +++ b/debugger/debugger.h @@ -96,5 +96,6 @@ void debugger_system_variable_register( /* Unit tests */ int debugger_disassemble_unittest( void ); +int debugger_disassemble_decimal_unittest( void ); #endif /* #ifndef FUSE_DEBUGGER_H */ diff --git a/debugger/disassemble.c b/debugger/disassemble.c index b9fd1f8c..86ee99c5 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1101,6 +1101,26 @@ libspectrum_byte test180_data[] = { 0xf5 }; /* PUSH AF */ libspectrum_byte test181_data[] = { 0xc7 }; /* RST 0 */ libspectrum_byte test182_data[] = { 0xef }; /* RST 28 */ +/* Decimal-mode tests: each exercises a distinct numeric-formatting path */ + +/* testd1: get_byte() in decimal mode -- LD B,n */ +libspectrum_byte testd1_data[] = { 0x06, 0x07 }; /* LD B,07h */ + +/* testd2: get_word() in decimal mode -- LD rr,nn */ +libspectrum_byte testd2_data[] = { 0x01, 0x34, 0x12 }; /* LD BC,1234h */ + +/* testd3: get_offset() in decimal mode -- JR e */ +libspectrum_byte testd3_data[] = { 0x18, 0x00 }; /* JR +0 -> 4002h */ + +/* testd4: ix_iy_offset() positive in decimal mode -- LD A,(IX+d) */ +libspectrum_byte testd4_data[] = { 0xdd, 0x7e, 0x55 }; /* LD A,(IX+55h) */ + +/* testd5: ix_iy_offset() negative in decimal mode -- BIT b,(IX+d) */ +libspectrum_byte testd5_data[] = { 0xdd, 0xcb, 0xff, 0x46 }; /* BIT 0,(IX-01h) */ + +/* testd6: get_byte() via 11xxx110 path -- ADD A,n */ +libspectrum_byte testd6_data[] = { 0xc6, 0x07 }; /* ADD A,07h */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1400,5 +1420,40 @@ debugger_disassemble_unittest( void ) r += run_test( test181_data, sizeof( test181_data ), "RST 0" ); r += run_test( test182_data, sizeof( test182_data ), "RST 28" ); + r += debugger_disassemble_decimal_unittest(); + + return r; +} + +/* Tests for decimal output mode (debugger_output_base == 10). + Each test exercises a distinct numeric-formatting code path. */ +int +debugger_disassemble_decimal_unittest( void ) +{ + int r = 0; + int saved_base = debugger_output_base; + + debugger_output_base = 10; + + /* get_byte() via 00xxx110 path: LD B,07h -> "LD B,7" */ + r += run_test( testd1_data, sizeof( testd1_data ), "LD B,7" ); + + /* get_word() via 00xxx001 path: LD BC,1234h -> "LD BC,4660" */ + r += run_test( testd2_data, sizeof( testd2_data ), "LD BC,4660" ); + + /* get_offset() via JR: offset 0 from 4000h -> 4002h = 16386 decimal */ + r += run_test( testd3_data, sizeof( testd3_data ), "JR 16386" ); + + /* ix_iy_offset() positive: LD A,(IX+55h) -> "LD A,(IX+85)" */ + r += run_test( testd4_data, sizeof( testd4_data ), "LD A,(IX+85)" ); + + /* ix_iy_offset() negative: BIT 0,(IX-01h) -> "BIT 0,(IX-1)" */ + r += run_test( testd5_data, sizeof( testd5_data ), "BIT 0,(IX-1)" ); + + /* get_byte() via 11xxx110 path: ADD A,07h -> "ADD A,7" */ + r += run_test( testd6_data, sizeof( testd6_data ), "ADD A,7" ); + + debugger_output_base = saved_base; + return r; } From 287039863225731ae567e4eb27250e11aef6988d Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 14:15:17 +1000 Subject: [PATCH 033/142] Add NOPD boundary tests for ED-prefix disassembler Add 6 unit tests (test183-test188) covering all three NOPD ranges in the ED-prefix disassembler (disassemble_ed): - 0xED 0x00-0x3F (b < 0x40): lower NOPD range [tests 183-184] - 0xED 0x80-0x9F (b in else, < 0xa0): mid NOPD range [tests 185-186] - 0xED 0xBC-0xFF (b > 0xBB): upper NOPD range [tests 187-188] Each test verifies both the 'NOPD' disassembly string and the 2-byte instruction length. The three code paths that produce 'NOPD' were previously exercised by no tests at all; this closes the gap. --- debugger/disassemble.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 86ee99c5..c7a16a6d 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1121,6 +1121,16 @@ libspectrum_byte testd5_data[] = { 0xdd, 0xcb, 0xff, 0x46 }; /* BIT 0,(IX-01h) /* testd6: get_byte() via 11xxx110 path -- ADD A,n */ libspectrum_byte testd6_data[] = { 0xc6, 0x07 }; /* ADD A,07h */ +/* ED NOPD boundary tests: opcodes that fall outside the valid ED range + (b < 0x40 or b > 0xbb) are decoded as NOPD (2-byte instruction). + The 0x80-0x9f sub-range inside the ED handler also decodes as NOPD. */ +libspectrum_byte test183_data[] = { 0xed, 0x00 }; /* NOPD: lower range start */ +libspectrum_byte test184_data[] = { 0xed, 0x3f }; /* NOPD: lower range end */ +libspectrum_byte test185_data[] = { 0xed, 0x80 }; /* NOPD: mid-range 0x80–0x9f start */ +libspectrum_byte test186_data[] = { 0xed, 0x9f }; /* NOPD: mid-range 0x80–0x9f end */ +libspectrum_byte test187_data[] = { 0xed, 0xbc }; /* NOPD: upper range start */ +libspectrum_byte test188_data[] = { 0xed, 0xff }; /* NOPD: upper range end */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1455,5 +1465,15 @@ debugger_disassemble_decimal_unittest( void ) debugger_output_base = saved_base; + /* ED prefix: NOPD boundary — b < 0x40 (lower range) and b > 0xbb (upper range) */ + r += run_test( test183_data, sizeof( test183_data ), "NOPD" ); + r += run_test( test184_data, sizeof( test184_data ), "NOPD" ); + /* ED prefix: NOPD mid-range 0x80–0x9f */ + r += run_test( test185_data, sizeof( test185_data ), "NOPD" ); + r += run_test( test186_data, sizeof( test186_data ), "NOPD" ); + /* ED prefix: NOPD upper range 0xbc–0xff */ + r += run_test( test187_data, sizeof( test187_data ), "NOPD" ); + r += run_test( test188_data, sizeof( test188_data ), "NOPD" ); + return r; } From 800101b39237a51b599f20cbb6494ff94423d95d Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 14:19:17 +1000 Subject: [PATCH 034/142] Add missing path coverage for sdl2_scaler_state Add 6 tests that exercise previously uncovered code paths in sdl2_scaler_state.c: - startup_windowed_returns_requested_as_windowed: verifies that sdl2_scaler_state_startup() with fullscreen=0 returns the requested scaler unchanged with SDL2_SCALER_WINDOWED state. - leave_fullscreen_from_native_state_keeps_current: verifies that sdl2_scaler_state_leave_fullscreen() from SDL2_SCALER_FULLSCREEN_NATIVE (not AUTO) keeps the current scaler and transitions to WINDOWED without attempting a restore. - leave_fullscreen_auto_with_no_restore_keeps_current: verifies that sdl2_scaler_state_leave_fullscreen() from AUTO state with restore_scaler==SCALER_NUM also keeps the current scaler. - user_select_windowed_returns_requested_as_windowed: verifies that sdl2_scaler_state_user_select() with fullscreen=0 returns the requested scaler with SDL2_SCALER_WINDOWED state. - reevaluate_windowed_keeps_current: verifies that sdl2_scaler_state_reevaluate() with fullscreen=0 returns the current scaler unchanged with SDL2_SCALER_WINDOWED state. - reevaluate_native_with_fitting_scaler_keeps_state: verifies that sdl2_scaler_state_reevaluate() in NATIVE state keeps the current scaler and state unchanged when the scaler still fits. --- unittests/sdl2scalerstatetest.c | 175 ++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/unittests/sdl2scalerstatetest.c b/unittests/sdl2scalerstatetest.c index 209d52c8..1da1d3fb 100644 --- a/unittests/sdl2scalerstatetest.c +++ b/unittests/sdl2scalerstatetest.c @@ -459,6 +459,169 @@ fullscreen_unknown_display_keeps_current_scaler( void ) return 0; } +static int +startup_windowed_returns_requested_as_windowed( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + fill_supported( supported ); + fill_env( &env, supported, scales, 1920, 1080 ); + + decision = sdl2_scaler_state_startup( SCALER_DOUBLESIZE, 0, &env ); + if( decision.scaler != SCALER_DOUBLESIZE || + decision.next.state != SDL2_SCALER_WINDOWED || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "startup windowed: expected keep 2x windowed, no restore\n" ); + return 1; + } + + return 0; +} + +static int +leave_fullscreen_from_native_state_keeps_current( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_state state; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + fill_supported( supported ); + fill_env( &env, supported, scales, 1920, 1080 ); + + state.state = SDL2_SCALER_FULLSCREEN_NATIVE; + state.restore_scaler = SCALER_NUM; + + decision = sdl2_scaler_state_leave_fullscreen( &state, SCALER_DOUBLESIZE, + &env ); + if( decision.scaler != SCALER_DOUBLESIZE || + decision.next.state != SDL2_SCALER_WINDOWED || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "leave native: expected keep current and transition to windowed\n" ); + return 1; + } + + return 0; +} + +static int +leave_fullscreen_auto_with_no_restore_keeps_current( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_state state; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + fill_supported( supported ); + fill_env( &env, supported, scales, 1920, 1080 ); + + state.state = SDL2_SCALER_FULLSCREEN_AUTO; + state.restore_scaler = SCALER_NUM; + + decision = sdl2_scaler_state_leave_fullscreen( &state, SCALER_NORMAL, &env ); + if( decision.scaler != SCALER_NORMAL || + decision.next.state != SDL2_SCALER_WINDOWED || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "leave auto no restore: expected keep current and windowed\n" ); + return 1; + } + + return 0; +} + +static int +user_select_windowed_returns_requested_as_windowed( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + fill_supported( supported ); + fill_env( &env, supported, scales, 1920, 1080 ); + + decision = sdl2_scaler_state_user_select( SCALER_TRIPLESIZE, 0, &env ); + if( decision.scaler != SCALER_TRIPLESIZE || + decision.next.state != SDL2_SCALER_WINDOWED || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "user select windowed: expected keep 3x windowed, no restore\n" ); + return 1; + } + + return 0; +} + +static int +reevaluate_windowed_keeps_current( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_state state; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + fill_supported( supported ); + fill_env( &env, supported, scales, 1920, 1080 ); + + state.state = SDL2_SCALER_WINDOWED; + state.restore_scaler = SCALER_NUM; + + decision = sdl2_scaler_state_reevaluate( &state, SCALER_TRIPLESIZE, 0, + &env ); + if( decision.scaler != SCALER_TRIPLESIZE || + decision.next.state != SDL2_SCALER_WINDOWED || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "reevaluate windowed: expected keep current scaler unchanged\n" ); + return 1; + } + + return 0; +} + +static int +reevaluate_native_with_fitting_scaler_keeps_state( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_state state; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + fill_supported( supported ); + fill_env( &env, supported, scales, 1920, 1080 ); + + state.state = SDL2_SCALER_FULLSCREEN_NATIVE; + state.restore_scaler = SCALER_NUM; + + decision = sdl2_scaler_state_reevaluate( &state, SCALER_DOUBLESIZE, 1, + &env ); + if( decision.scaler != SCALER_DOUBLESIZE || + decision.next.state != SDL2_SCALER_FULLSCREEN_NATIVE || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "reevaluate native fits: expected keep 2x native unchanged\n" ); + return 1; + } + + return 0; +} + typedef int (*test_fn_t)( void ); struct test_t { @@ -471,17 +634,29 @@ static const struct test_t tests[] = { startup_fullscreen_auto_restores_on_exit }, { "startup_fullscreen_keeps_requested_when_it_fits", startup_fullscreen_keeps_requested_when_it_fits }, + { "startup_windowed_returns_requested_as_windowed", + startup_windowed_returns_requested_as_windowed }, { "enter_fullscreen_prefers_same_family", enter_fullscreen_prefers_same_family }, { "tv_family_includes_timextv", tv_family_includes_timextv }, { "user_select_clears_auto_restore_state", user_select_clears_auto_restore_state }, + { "user_select_windowed_returns_requested_as_windowed", + user_select_windowed_returns_requested_as_windowed }, { "reevaluate_native_becomes_auto_when_current_no_longer_fits", reevaluate_native_becomes_auto_when_current_no_longer_fits }, { "reevaluate_auto_preserves_supported_restore_scaler", reevaluate_auto_preserves_supported_restore_scaler }, + { "reevaluate_native_with_fitting_scaler_keeps_state", + reevaluate_native_with_fitting_scaler_keeps_state }, + { "reevaluate_windowed_keeps_current", + reevaluate_windowed_keeps_current }, { "leave_fullscreen_restores_supported_equivalent_when_original_invalid", leave_fullscreen_restores_supported_equivalent_when_original_invalid }, + { "leave_fullscreen_from_native_state_keeps_current", + leave_fullscreen_from_native_state_keeps_current }, + { "leave_fullscreen_auto_with_no_restore_keeps_current", + leave_fullscreen_auto_with_no_restore_keeps_current }, { "transition_dispatches_startup_branch", transition_dispatches_startup_branch }, { "transition_dispatches_enter_branch", From 4ef41eb4c3ea7595da4ecf69163b22916463c142 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 14:45:44 +1000 Subject: [PATCH 035/142] Add tests for SDL2 reevaluate-auto and user-select-unfitting paths Cover two previously untested branches in sdl2_scaler_state: - reevaluate_auto_adjusts_when_current_no_longer_fits: verifies that when reevaluate() is called in FULLSCREEN_AUTO state and the active scaler no longer fits the display, the state machine falls back to the best fitting scaler while preserving the stored restore_scaler. - user_select_fullscreen_adjusts_to_fitting_scaler: verifies that when the user explicitly selects a scaler that is too large for the current fullscreen display, user_select() resolves to the best fitting alternative and returns FULLSCREEN_NATIVE state with no restore_scaler (the selection is treated as a deliberate user choice, not an automatic substitution). --- unittests/sdl2scalerstatetest.c | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/unittests/sdl2scalerstatetest.c b/unittests/sdl2scalerstatetest.c index 1da1d3fb..1f0b111a 100644 --- a/unittests/sdl2scalerstatetest.c +++ b/unittests/sdl2scalerstatetest.c @@ -437,6 +437,67 @@ transition_dispatches_reevaluate_branch( void ) return 0; } +static int +reevaluate_auto_adjusts_when_current_no_longer_fits( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_state state; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + memset( supported, 0, sizeof( supported ) ); + supported[ SCALER_NORMAL ] = 1; + supported[ SCALER_DOUBLESIZE ] = 1; + supported[ SCALER_TRIPLESIZE ] = 1; + fill_env( &env, supported, scales, 500, 350 ); + + state.state = SDL2_SCALER_FULLSCREEN_AUTO; + state.restore_scaler = SCALER_TRIPLESIZE; + + /* DOUBLESIZE (2x = 640x480) doesn't fit a 500x350 display; expect fallback + to NORMAL (1x = 320x240) while preserving the restore scaler. */ + decision = sdl2_scaler_state_reevaluate( &state, SCALER_DOUBLESIZE, 1, &env ); + if( decision.scaler != SCALER_NORMAL || + decision.next.state != SDL2_SCALER_FULLSCREEN_AUTO || + decision.next.restore_scaler != SCALER_TRIPLESIZE ) { + fprintf( stderr, + "reevaluate auto unfitting: expected normal/auto restore 3x\n" ); + return 1; + } + + return 0; +} + +static int +user_select_fullscreen_adjusts_to_fitting_scaler( void ) +{ + unsigned char supported[ SCALER_NUM ]; + float scales[ SCALER_NUM ]; + sdl2_scaler_env env; + sdl2_scaler_decision decision; + + fill_test_scales( scales ); + memset( supported, 0, sizeof( supported ) ); + supported[ SCALER_NORMAL ] = 1; + supported[ SCALER_QUADSIZE ] = 1; + fill_env( &env, supported, scales, 400, 300 ); + + /* QUADSIZE (4x = 1280x960) doesn't fit a 400x300 display; expect fallback + to NORMAL (1x = 320x240) with NATIVE state and no restore scaler. */ + decision = sdl2_scaler_state_user_select( SCALER_QUADSIZE, 1, &env ); + if( decision.scaler != SCALER_NORMAL || + decision.next.state != SDL2_SCALER_FULLSCREEN_NATIVE || + decision.next.restore_scaler != SCALER_NUM ) { + fprintf( stderr, + "user select unfitting: expected normal/native no restore\n" ); + return 1; + } + + return 0; +} + static int fullscreen_unknown_display_keeps_current_scaler( void ) { @@ -669,6 +730,10 @@ static const struct test_t tests[] = { transition_dispatches_reevaluate_branch }, { "fullscreen_unknown_display_keeps_current_scaler", fullscreen_unknown_display_keeps_current_scaler }, + { "reevaluate_auto_adjusts_when_current_no_longer_fits", + reevaluate_auto_adjusts_when_current_no_longer_fits }, + { "user_select_fullscreen_adjusts_to_fitting_scaler", + user_select_fullscreen_adjusts_to_fitting_scaler }, { NULL, NULL } }; From ed996e4beb629b7d316924793f53ffad90b1f76a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 22:43:01 +1000 Subject: [PATCH 036/142] Complete disassembler test coverage for conditional instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 37 new tests (test189–test225) covering: - Conditional JR: JR Z, JR NC, JR C (3 tests) - Conditional RET: RET Z/NC/C/PO/PE/P/M (7 tests) - Conditional JP: JP Z/NC/C/PO/PE/P/M nn (7 tests) - Conditional CALL: CALL Z/NC/C/PO/PE/P/M nn (7 tests) - RST: RST 8/10/18/20/30/38 (6 tests) - Immediate arithmetic/logic: ADC/SUB/SBC/AND/XOR/OR/CP n (7 tests) These complete the 11xxx group coverage begun in the main opcode tests (test162–test182) and ensure every conditional variant and every RST target is verified by the disassembler unit test suite. --- debugger/disassemble.c | 98 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index c7a16a6d..74a1471f 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1131,6 +1131,55 @@ libspectrum_byte test186_data[] = { 0xed, 0x9f }; /* NOPD: mid-range 0x80–0x9 libspectrum_byte test187_data[] = { 0xed, 0xbc }; /* NOPD: upper range start */ libspectrum_byte test188_data[] = { 0xed, 0xff }; /* NOPD: upper range end */ +/* 00xxx000: conditional JR — remaining conditions (NZ already covered) */ +libspectrum_byte test189_data[] = { 0x28, 0x04 }; /* JR Z,+4 -> 4006 */ +libspectrum_byte test190_data[] = { 0x30, 0x04 }; /* JR NC,+4 -> 4006 */ +libspectrum_byte test191_data[] = { 0x38, 0x04 }; /* JR C,+4 -> 4006 */ + +/* 11xxx000: conditional RET — remaining seven conditions */ +libspectrum_byte test192_data[] = { 0xc8 }; /* RET Z */ +libspectrum_byte test193_data[] = { 0xd0 }; /* RET NC */ +libspectrum_byte test194_data[] = { 0xd8 }; /* RET C */ +libspectrum_byte test195_data[] = { 0xe0 }; /* RET PO */ +libspectrum_byte test196_data[] = { 0xe8 }; /* RET PE */ +libspectrum_byte test197_data[] = { 0xf0 }; /* RET P */ +libspectrum_byte test198_data[] = { 0xf8 }; /* RET M */ + +/* 11xxx010: conditional JP — remaining seven conditions */ +libspectrum_byte test199_data[] = { 0xca, 0x34, 0x12 }; /* JP Z,1234 */ +libspectrum_byte test200_data[] = { 0xd2, 0x34, 0x12 }; /* JP NC,1234 */ +libspectrum_byte test201_data[] = { 0xda, 0x34, 0x12 }; /* JP C,1234 */ +libspectrum_byte test202_data[] = { 0xe2, 0x34, 0x12 }; /* JP PO,1234 */ +libspectrum_byte test203_data[] = { 0xea, 0x34, 0x12 }; /* JP PE,1234 */ +libspectrum_byte test204_data[] = { 0xf2, 0x34, 0x12 }; /* JP P,1234 */ +libspectrum_byte test205_data[] = { 0xfa, 0x34, 0x12 }; /* JP M,1234 */ + +/* 11xxx100: conditional CALL — remaining seven conditions */ +libspectrum_byte test206_data[] = { 0xcc, 0x34, 0x12 }; /* CALL Z,1234 */ +libspectrum_byte test207_data[] = { 0xd4, 0x34, 0x12 }; /* CALL NC,1234 */ +libspectrum_byte test208_data[] = { 0xdc, 0x34, 0x12 }; /* CALL C,1234 */ +libspectrum_byte test209_data[] = { 0xe4, 0x34, 0x12 }; /* CALL PO,1234 */ +libspectrum_byte test210_data[] = { 0xec, 0x34, 0x12 }; /* CALL PE,1234 */ +libspectrum_byte test211_data[] = { 0xf4, 0x34, 0x12 }; /* CALL P,1234 */ +libspectrum_byte test212_data[] = { 0xfc, 0x34, 0x12 }; /* CALL M,1234 */ + +/* 11xxx111: RST — remaining six targets */ +libspectrum_byte test213_data[] = { 0xcf }; /* RST 8 */ +libspectrum_byte test214_data[] = { 0xd7 }; /* RST 10 */ +libspectrum_byte test215_data[] = { 0xdf }; /* RST 18 */ +libspectrum_byte test216_data[] = { 0xe7 }; /* RST 20 */ +libspectrum_byte test217_data[] = { 0xf7 }; /* RST 30 */ +libspectrum_byte test218_data[] = { 0xff }; /* RST 38 */ + +/* 11xxx110: immediate arithmetic/logic — remaining seven operations */ +libspectrum_byte test219_data[] = { 0xce, 0x07 }; /* ADC A,07 */ +libspectrum_byte test220_data[] = { 0xd6, 0x07 }; /* SUB 07 */ +libspectrum_byte test221_data[] = { 0xde, 0x07 }; /* SBC A,07 */ +libspectrum_byte test222_data[] = { 0xe6, 0x07 }; /* AND 07 */ +libspectrum_byte test223_data[] = { 0xee, 0x07 }; /* XOR 07 */ +libspectrum_byte test224_data[] = { 0xf6, 0x07 }; /* OR 07 */ +libspectrum_byte test225_data[] = { 0xfe, 0x07 }; /* CP 07 */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1432,6 +1481,55 @@ debugger_disassemble_unittest( void ) r += debugger_disassemble_decimal_unittest(); + /* Conditional JR — remaining conditions */ + r += run_test( test189_data, sizeof( test189_data ), "JR Z,4006" ); + r += run_test( test190_data, sizeof( test190_data ), "JR NC,4006" ); + r += run_test( test191_data, sizeof( test191_data ), "JR C,4006" ); + + /* Conditional RET — remaining seven conditions */ + r += run_test( test192_data, sizeof( test192_data ), "RET Z" ); + r += run_test( test193_data, sizeof( test193_data ), "RET NC" ); + r += run_test( test194_data, sizeof( test194_data ), "RET C" ); + r += run_test( test195_data, sizeof( test195_data ), "RET PO" ); + r += run_test( test196_data, sizeof( test196_data ), "RET PE" ); + r += run_test( test197_data, sizeof( test197_data ), "RET P" ); + r += run_test( test198_data, sizeof( test198_data ), "RET M" ); + + /* Conditional JP — remaining seven conditions */ + r += run_test( test199_data, sizeof( test199_data ), "JP Z,1234" ); + r += run_test( test200_data, sizeof( test200_data ), "JP NC,1234" ); + r += run_test( test201_data, sizeof( test201_data ), "JP C,1234" ); + r += run_test( test202_data, sizeof( test202_data ), "JP PO,1234" ); + r += run_test( test203_data, sizeof( test203_data ), "JP PE,1234" ); + r += run_test( test204_data, sizeof( test204_data ), "JP P,1234" ); + r += run_test( test205_data, sizeof( test205_data ), "JP M,1234" ); + + /* Conditional CALL — remaining seven conditions */ + r += run_test( test206_data, sizeof( test206_data ), "CALL Z,1234" ); + r += run_test( test207_data, sizeof( test207_data ), "CALL NC,1234" ); + r += run_test( test208_data, sizeof( test208_data ), "CALL C,1234" ); + r += run_test( test209_data, sizeof( test209_data ), "CALL PO,1234" ); + r += run_test( test210_data, sizeof( test210_data ), "CALL PE,1234" ); + r += run_test( test211_data, sizeof( test211_data ), "CALL P,1234" ); + r += run_test( test212_data, sizeof( test212_data ), "CALL M,1234" ); + + /* RST — remaining six targets */ + r += run_test( test213_data, sizeof( test213_data ), "RST 8" ); + r += run_test( test214_data, sizeof( test214_data ), "RST 10" ); + r += run_test( test215_data, sizeof( test215_data ), "RST 18" ); + r += run_test( test216_data, sizeof( test216_data ), "RST 20" ); + r += run_test( test217_data, sizeof( test217_data ), "RST 30" ); + r += run_test( test218_data, sizeof( test218_data ), "RST 38" ); + + /* Immediate arithmetic/logic — remaining seven operations */ + r += run_test( test219_data, sizeof( test219_data ), "ADC A,07" ); + r += run_test( test220_data, sizeof( test220_data ), "SUB 07" ); + r += run_test( test221_data, sizeof( test221_data ), "SBC A,07" ); + r += run_test( test222_data, sizeof( test222_data ), "AND 07" ); + r += run_test( test223_data, sizeof( test223_data ), "XOR 07" ); + r += run_test( test224_data, sizeof( test224_data ), "OR 07" ); + r += run_test( test225_data, sizeof( test225_data ), "CP 07" ); + return r; } From 3db5eede9e665fda4f69e1780e11cf9c8d3da205 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 22:50:20 +1000 Subject: [PATCH 037/142] Remove deprecated AM_PROG_CC_C_O Remove deprecated AM_PROG_CC_C_O call. Since autoconf 2.64 its functionality is absorbed into AC_PROG_CC, and automake 1.13 deprecated it explicitly. With subdir-objects already enabled in AM_INIT_AUTOMAKE the macro has been a no-op for years. --- configure.ac | 1 - 1 file changed, 1 deletion(-) diff --git a/configure.ac b/configure.ac index 55b860af..70440efe 100644 --- a/configure.ac +++ b/configure.ac @@ -76,7 +76,6 @@ fi dnl Checks for programs. AC_PROG_CC -AM_PROG_CC_C_O AC_PATH_PROG(PERL, perl) AC_SUBST(PERL) AC_PROG_LEX([noyywrap]) From c26118617bca23e09a365b5302cb291fc26689c9 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 6 Jun 2026 22:54:52 +1000 Subject: [PATCH 038/142] Add NTSC scaler entries to fill_test_scales() helper --- unittests/sdl2scalerstatetest.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unittests/sdl2scalerstatetest.c b/unittests/sdl2scalerstatetest.c index 1f0b111a..0dd17f05 100644 --- a/unittests/sdl2scalerstatetest.c +++ b/unittests/sdl2scalerstatetest.c @@ -45,6 +45,9 @@ fill_test_scales( float *scales ) scales[ SCALER_QUADSIZE ] = 4.0f; scales[ SCALER_TV4X ] = 4.0f; scales[ SCALER_PALTV4X ] = 4.0f; + scales[ SCALER_NTSC2X ] = 2.0f; + scales[ SCALER_NTSC3X ] = 3.0f; + scales[ SCALER_NTSC4X ] = 4.0f; scales[ SCALER_HQ4X ] = 4.0f; } From c9fe127981d6a1ec71cd6d144880bc04f08efb11 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:28:34 +1000 Subject: [PATCH 039/142] Add missing boundary-condition tests to sdl2joysticktest Add three tests that exercise boundary cases not previously covered: - button_event_maps_first_fire_button: verifies button index 0 maps to INPUT_JOYSTICK_FIRE_1 (the base case of the index arithmetic). - button_event_maps_last_fire_button: verifies button index NUM_JOY_BUTTONS-1 maps to INPUT_JOYSTICK_FIRE_15 (the upper boundary of valid button indices). - axis_just_below_negative_threshold_presses_negative: verifies that a value of -16385 (one below the -16384 threshold) sets event1 to PRESS and event2 to RELEASE, the symmetric counterpart of the existing axis_just_above_threshold_presses_positive test. --- unittests/sdl2joysticktest.c | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/unittests/sdl2joysticktest.c b/unittests/sdl2joysticktest.c index ff15097b..1d69b2d5 100644 --- a/unittests/sdl2joysticktest.c +++ b/unittests/sdl2joysticktest.c @@ -140,6 +140,51 @@ axis_just_above_threshold_presses_positive( void ) return 0; } +static int +button_event_maps_first_fire_button( void ) +{ + input_event_t event; + + if( !sdl2joystick_button_event( 0, 0, INPUT_EVENT_JOYSTICK_PRESS, &event ) ) + return 1; + + if( event.type != INPUT_EVENT_JOYSTICK_PRESS ) return 1; + if( event.types.joystick.which != 0 ) return 1; + if( event.types.joystick.button != INPUT_JOYSTICK_FIRE_1 ) return 1; + + return 0; +} + +static int +button_event_maps_last_fire_button( void ) +{ + input_event_t event; + + if( !sdl2joystick_button_event( 0, NUM_JOY_BUTTONS - 1, + INPUT_EVENT_JOYSTICK_PRESS, &event ) ) + return 1; + + if( event.type != INPUT_EVENT_JOYSTICK_PRESS ) return 1; + if( event.types.joystick.button != INPUT_JOYSTICK_FIRE_15 ) return 1; + + return 0; +} + +static int +axis_just_below_negative_threshold_presses_negative( void ) +{ + input_event_t event1, event2; + + /* value == -16385: just below threshold, should press negative direction */ + sdl2joystick_axis_events( 0, -16385, INPUT_JOYSTICK_UP, + INPUT_JOYSTICK_DOWN, &event1, &event2 ); + + if( event1.type != INPUT_EVENT_JOYSTICK_PRESS ) return 1; + if( event2.type != INPUT_EVENT_JOYSTICK_RELEASE ) return 1; + + return 0; +} + static int hat_event_sets_press_and_release( void ) { @@ -166,6 +211,10 @@ struct test_t { static const struct test_t tests[] = { { "lookup_maps_instance_ids", lookup_maps_instance_ids }, { "button_event_maps_fire_button", button_event_maps_fire_button }, + { "button_event_maps_first_fire_button", + button_event_maps_first_fire_button }, + { "button_event_maps_last_fire_button", + button_event_maps_last_fire_button }, { "button_event_rejects_out_of_range_button", button_event_rejects_out_of_range_button }, { "axis_positive_presses_positive_direction", @@ -180,6 +229,8 @@ static const struct test_t tests[] = { axis_at_negative_threshold_is_neutral }, { "axis_just_above_threshold_presses_positive", axis_just_above_threshold_presses_positive }, + { "axis_just_below_negative_threshold_presses_negative", + axis_just_below_negative_threshold_presses_negative }, { "hat_event_sets_press_and_release", hat_event_sets_press_and_release }, { NULL, NULL } }; From bb4f3c81d1a0eeb5ed3a018b46585694dab2bf9e Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:31:56 +1000 Subject: [PATCH 040/142] Update m4/ax_pthread.m4 from serial 21 to serial 31 Fetch the latest ax_pthread.m4 from the autoconf-archive project (https://github.com/autoconf-archive/autoconf-archive). Notable changes between serial 21 and serial 31: - Updated URL from http:// to https:// - Added PTHREAD_CXX support for C++ programs - Added CXXFLAGS documentation example - Improved compiler detection for modern systems - Added copyright for Marc Stevens (2019) - Various robustness and platform-compatibility improvements --- m4/ax_pthread.m4 | 444 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 317 insertions(+), 127 deletions(-) diff --git a/m4/ax_pthread.m4 b/m4/ax_pthread.m4 index d383ad5c..9f35d139 100644 --- a/m4/ax_pthread.m4 +++ b/m4/ax_pthread.m4 @@ -1,5 +1,5 @@ # =========================================================================== -# http://www.gnu.org/software/autoconf-archive/ax_pthread.html +# https://www.gnu.org/software/autoconf-archive/ax_pthread.html # =========================================================================== # # SYNOPSIS @@ -14,24 +14,28 @@ # flags that are needed. (The user can also force certain compiler # flags/libs to be tested by setting these environment variables.) # -# Also sets PTHREAD_CC to any special C compiler that is needed for -# multi-threaded programs (defaults to the value of CC otherwise). (This -# is necessary on AIX to use the special cc_r compiler alias.) +# Also sets PTHREAD_CC and PTHREAD_CXX to any special C compiler that is +# needed for multi-threaded programs (defaults to the value of CC +# respectively CXX otherwise). (This is necessary on e.g. AIX to use the +# special cc_r/CC_r compiler alias.) # # NOTE: You are assumed to not only compile your program with these flags, -# but also link it with them as well. e.g. you should link with +# but also to link with them as well. For example, you might link with # $PTHREAD_CC $CFLAGS $PTHREAD_CFLAGS $LDFLAGS ... $PTHREAD_LIBS $LIBS +# $PTHREAD_CXX $CXXFLAGS $PTHREAD_CFLAGS $LDFLAGS ... $PTHREAD_LIBS $LIBS # -# If you are only building threads programs, you may wish to use these +# If you are only building threaded programs, you may wish to use these # variables in your default LIBS, CFLAGS, and CC: # # LIBS="$PTHREAD_LIBS $LIBS" # CFLAGS="$CFLAGS $PTHREAD_CFLAGS" +# CXXFLAGS="$CXXFLAGS $PTHREAD_CFLAGS" # CC="$PTHREAD_CC" +# CXX="$PTHREAD_CXX" # # In addition, if the PTHREAD_CREATE_JOINABLE thread-attribute constant -# has a nonstandard name, defines PTHREAD_CREATE_JOINABLE to that name -# (e.g. PTHREAD_CREATE_UNDETACHED on AIX). +# has a nonstandard name, this macro defines PTHREAD_CREATE_JOINABLE to +# that name (e.g. PTHREAD_CREATE_UNDETACHED on AIX). # # Also HAVE_PTHREAD_PRIO_INHERIT is defined if pthread is found and the # PTHREAD_PRIO_INHERIT symbol is defined when compiling with @@ -55,6 +59,7 @@ # # Copyright (c) 2008 Steven G. Johnson # Copyright (c) 2011 Daniel Richard G. +# Copyright (c) 2019 Marc Stevens # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the @@ -67,7 +72,7 @@ # Public License for more details. # # You should have received a copy of the GNU General Public License along -# with this program. If not, see . +# with this program. If not, see . # # As a special exception, the respective Autoconf Macro's copyright owner # gives unlimited permission to copy, distribute and modify the configure @@ -82,35 +87,41 @@ # modified version of the Autoconf Macro, you may extend this special # exception to the GPL to apply to your modified version as well. -#serial 21 +#serial 31 AU_ALIAS([ACX_PTHREAD], [AX_PTHREAD]) AC_DEFUN([AX_PTHREAD], [ AC_REQUIRE([AC_CANONICAL_HOST]) +AC_REQUIRE([AC_PROG_CC]) +AC_REQUIRE([AC_PROG_SED]) AC_LANG_PUSH([C]) ax_pthread_ok=no # We used to check for pthread.h first, but this fails if pthread.h -# requires special compiler flags (e.g. on True64 or Sequent). +# requires special compiler flags (e.g. on Tru64 or Sequent). # It gets checked for in the link test anyway. # First of all, check if the user has set any of the PTHREAD_LIBS, # etcetera environment variables, and if threads linking works using # them: -if test x"$PTHREAD_LIBS$PTHREAD_CFLAGS" != x; then - save_CFLAGS="$CFLAGS" +if test "x$PTHREAD_CFLAGS$PTHREAD_LIBS" != "x"; then + ax_pthread_save_CC="$CC" + ax_pthread_save_CFLAGS="$CFLAGS" + ax_pthread_save_LIBS="$LIBS" + AS_IF([test "x$PTHREAD_CC" != "x"], [CC="$PTHREAD_CC"]) + AS_IF([test "x$PTHREAD_CXX" != "x"], [CXX="$PTHREAD_CXX"]) CFLAGS="$CFLAGS $PTHREAD_CFLAGS" - save_LIBS="$LIBS" LIBS="$PTHREAD_LIBS $LIBS" - AC_MSG_CHECKING([for pthread_join in LIBS=$PTHREAD_LIBS with CFLAGS=$PTHREAD_CFLAGS]) - AC_TRY_LINK_FUNC([pthread_join], [ax_pthread_ok=yes]) + AC_MSG_CHECKING([for pthread_join using $CC $PTHREAD_CFLAGS $PTHREAD_LIBS]) + AC_LINK_IFELSE([AC_LANG_CALL([], [pthread_join])], [ax_pthread_ok=yes]) AC_MSG_RESULT([$ax_pthread_ok]) - if test x"$ax_pthread_ok" = xno; then + if test "x$ax_pthread_ok" = "xno"; then PTHREAD_LIBS="" PTHREAD_CFLAGS="" fi - LIBS="$save_LIBS" - CFLAGS="$save_CFLAGS" + CC="$ax_pthread_save_CC" + CFLAGS="$ax_pthread_save_CFLAGS" + LIBS="$ax_pthread_save_LIBS" fi # We must check for the threads library under a number of different @@ -118,12 +129,14 @@ fi # (e.g. DEC) have both -lpthread and -lpthreads, where one of the # libraries is broken (non-POSIX). -# Create a list of thread flags to try. Items starting with a "-" are -# C compiler flags, and other items are library names, except for "none" -# which indicates that we try without any flags at all, and "pthread-config" -# which is a program returning the flags for the Pth emulation library. +# Create a list of thread flags to try. Items with a "," contain both +# C compiler flags (before ",") and linker flags (after ","). Other items +# starting with a "-" are C compiler flags, and remaining items are +# library names, except for "none" which indicates that we try without +# any flags at all, and "pthread-config" which is a program returning +# the flags for the Pth emulation library. -ax_pthread_flags="pthreads none -Kthread -kthread lthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config" +ax_pthread_flags="pthreads none -Kthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config" # The ordering *is* (sometimes) important. Some notes on the # individual items follow: @@ -132,82 +145,163 @@ ax_pthread_flags="pthreads none -Kthread -kthread lthread -pthread -pthreads -mt # none: in case threads are in libc; should be tried before -Kthread and # other compiler flags to prevent continual compiler warnings # -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h) -# -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) -# lthread: LinuxThreads port on FreeBSD (also preferred to -pthread) -# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads) -# -pthreads: Solaris/gcc -# -mthreads: Mingw32/gcc, Lynx/gcc +# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads), Tru64 +# (Note: HP C rejects this with "bad form for `-t' option") +# -pthreads: Solaris/gcc (Note: HP C also rejects) # -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it -# doesn't hurt to check since this sometimes defines pthreads too; -# also defines -D_REENTRANT) -# ... -mt is also the pthreads flag for HP/aCC +# doesn't hurt to check since this sometimes defines pthreads and +# -D_REENTRANT too), HP C (must be checked before -lpthread, which +# is present but should not be used directly; and before -mthreads, +# because the compiler interprets this as "-mt" + "-hreads") +# -mthreads: Mingw32/gcc, Lynx/gcc # pthread: Linux, etcetera # --thread-safe: KAI C++ # pthread-config: use pthread-config program (for GNU Pth library) -case ${host_os} in +case $host_os in + + freebsd*) + + # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) + # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread) + + ax_pthread_flags="-kthread lthread $ax_pthread_flags" + ;; + + hpux*) + + # From the cc(1) man page: "[-mt] Sets various -D flags to enable + # multi-threading and also sets -lpthread." + + ax_pthread_flags="-mt -pthread pthread $ax_pthread_flags" + ;; + + openedition*) + + # IBM z/OS requires a feature-test macro to be defined in order to + # enable POSIX threads at all, so give the user a hint if this is + # not set. (We don't define these ourselves, as they can affect + # other portions of the system API in unpredictable ways.) + + AC_EGREP_CPP([AX_PTHREAD_ZOS_MISSING], + [ +# if !defined(_OPEN_THREADS) && !defined(_UNIX03_THREADS) + AX_PTHREAD_ZOS_MISSING +# endif + ], + [AC_MSG_WARN([IBM z/OS requires -D_OPEN_THREADS or -D_UNIX03_THREADS to enable pthreads support.])]) + ;; + solaris*) # On Solaris (at least, for some versions), libc contains stubbed # (non-functional) versions of the pthreads routines, so link-based - # tests will erroneously succeed. (We need to link with -pthreads/-mt/ - # -lpthread.) (The stubs are missing pthread_cleanup_push, or rather - # a function called by this macro, so we could check for that, but - # who knows whether they'll stub that too in a future libc.) So, - # we'll just look for -pthreads and -lpthread first: + # tests will erroneously succeed. (N.B.: The stubs are missing + # pthread_cleanup_push, or rather a function called by this macro, + # so we could check for that, but who knows whether they'll stub + # that too in a future libc.) So we'll check first for the + # standard Solaris way of linking pthreads (-mt -lpthread). + + ax_pthread_flags="-mt,-lpthread pthread $ax_pthread_flags" + ;; +esac + +# Are we compiling with Clang? + +AC_CACHE_CHECK([whether $CC is Clang], + [ax_cv_PTHREAD_CLANG], + [ax_cv_PTHREAD_CLANG=no + # Note that Autoconf sets GCC=yes for Clang as well as GCC + if test "x$GCC" = "xyes"; then + AC_EGREP_CPP([AX_PTHREAD_CC_IS_CLANG], + [/* Note: Clang 2.7 lacks __clang_[a-z]+__ */ +# if defined(__clang__) && defined(__llvm__) + AX_PTHREAD_CC_IS_CLANG +# endif + ], + [ax_cv_PTHREAD_CLANG=yes]) + fi + ]) +ax_pthread_clang="$ax_cv_PTHREAD_CLANG" + + +# GCC generally uses -pthread, or -pthreads on some platforms (e.g. SPARC) + +# Note that for GCC and Clang -pthread generally implies -lpthread, +# except when -nostdlib is passed. +# This is problematic using libtool to build C++ shared libraries with pthread: +# [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25460 +# [2] https://bugzilla.redhat.com/show_bug.cgi?id=661333 +# [3] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=468555 +# To solve this, first try -pthread together with -lpthread for GCC + +AS_IF([test "x$GCC" = "xyes"], + [ax_pthread_flags="-pthread,-lpthread -pthread -pthreads $ax_pthread_flags"]) + +# Clang takes -pthread (never supported any other flag), but we'll try with -lpthread first + +AS_IF([test "x$ax_pthread_clang" = "xyes"], + [ax_pthread_flags="-pthread,-lpthread -pthread"]) - ax_pthread_flags="-pthreads pthread -mt -pthread $ax_pthread_flags" + +# The presence of a feature test macro requesting re-entrant function +# definitions is, on some systems, a strong hint that pthreads support is +# correctly enabled + +case $host_os in + darwin* | hpux* | linux* | osf* | solaris*) + ax_pthread_check_macro="_REENTRANT" ;; - darwin*) - ax_pthread_flags="-pthread $ax_pthread_flags" + aix*) + ax_pthread_check_macro="_THREAD_SAFE" ;; -esac -# Clang doesn't consider unrecognized options an error unless we specify -# -Werror. We throw in some extra Clang-specific options to ensure that -# this doesn't happen for GCC, which also accepts -Werror. + *) + ax_pthread_check_macro="--" + ;; +esac +AS_IF([test "x$ax_pthread_check_macro" = "x--"], + [ax_pthread_check_cond=0], + [ax_pthread_check_cond="!defined($ax_pthread_check_macro)"]) -AC_MSG_CHECKING([if compiler needs -Werror to reject unknown flags]) -save_CFLAGS="$CFLAGS" -ax_pthread_extra_flags="-Werror" -CFLAGS="$CFLAGS $ax_pthread_extra_flags -Wunknown-warning-option -Wsizeof-array-argument" -AC_COMPILE_IFELSE([AC_LANG_PROGRAM([int foo(void);],[foo()])], - [AC_MSG_RESULT([yes])], - [ax_pthread_extra_flags= - AC_MSG_RESULT([no])]) -CFLAGS="$save_CFLAGS" -if test x"$ax_pthread_ok" = xno; then -for flag in $ax_pthread_flags; do +if test "x$ax_pthread_ok" = "xno"; then +for ax_pthread_try_flag in $ax_pthread_flags; do - case $flag in + case $ax_pthread_try_flag in none) AC_MSG_CHECKING([whether pthreads work without any flags]) ;; + *,*) + PTHREAD_CFLAGS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\1/"` + PTHREAD_LIBS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\2/"` + AC_MSG_CHECKING([whether pthreads work with "$PTHREAD_CFLAGS" and "$PTHREAD_LIBS"]) + ;; + -*) - AC_MSG_CHECKING([whether pthreads work with $flag]) - PTHREAD_CFLAGS="$flag" + AC_MSG_CHECKING([whether pthreads work with $ax_pthread_try_flag]) + PTHREAD_CFLAGS="$ax_pthread_try_flag" ;; pthread-config) AC_CHECK_PROG([ax_pthread_config], [pthread-config], [yes], [no]) - if test x"$ax_pthread_config" = xno; then continue; fi + AS_IF([test "x$ax_pthread_config" = "xno"], [continue]) PTHREAD_CFLAGS="`pthread-config --cflags`" PTHREAD_LIBS="`pthread-config --ldflags` `pthread-config --libs`" ;; *) - AC_MSG_CHECKING([for the pthreads library -l$flag]) - PTHREAD_LIBS="-l$flag" + AC_MSG_CHECKING([for the pthreads library -l$ax_pthread_try_flag]) + PTHREAD_LIBS="-l$ax_pthread_try_flag" ;; esac - save_LIBS="$LIBS" - save_CFLAGS="$CFLAGS" + ax_pthread_save_CFLAGS="$CFLAGS" + ax_pthread_save_LIBS="$LIBS" + CFLAGS="$CFLAGS $PTHREAD_CFLAGS" LIBS="$PTHREAD_LIBS $LIBS" - CFLAGS="$CFLAGS $PTHREAD_CFLAGS $ax_pthread_extra_flags" # Check for various functions. We must include pthread.h, # since some functions may be macros. (On the Sequent, we @@ -218,8 +312,18 @@ for flag in $ax_pthread_flags; do # pthread_cleanup_push because it is one of the few pthread # functions on Solaris that doesn't have a non-functional libc stub. # We try pthread_create on general principles. + AC_LINK_IFELSE([AC_LANG_PROGRAM([#include - static void routine(void *a) { a = 0; } +# if $ax_pthread_check_cond +# error "$ax_pthread_check_macro must be defined" +# endif + static void *some_global = NULL; + static void routine(void *a) + { + /* To avoid any unused-parameter or + unused-but-set-parameter warning. */ + some_global = a; + } static void *start_routine(void *a) { return a; }], [pthread_t th; pthread_attr_t attr; pthread_create(&th, 0, start_routine, 0); @@ -227,101 +331,187 @@ for flag in $ax_pthread_flags; do pthread_attr_init(&attr); pthread_cleanup_push(routine, 0); pthread_cleanup_pop(0) /* ; */])], - [ax_pthread_ok=yes], - []) + [ax_pthread_ok=yes], + []) - LIBS="$save_LIBS" - CFLAGS="$save_CFLAGS" + CFLAGS="$ax_pthread_save_CFLAGS" + LIBS="$ax_pthread_save_LIBS" AC_MSG_RESULT([$ax_pthread_ok]) - if test "x$ax_pthread_ok" = xyes; then - break; - fi + AS_IF([test "x$ax_pthread_ok" = "xyes"], [break]) PTHREAD_LIBS="" PTHREAD_CFLAGS="" done fi + +# Clang needs special handling, because older versions handle the -pthread +# option in a rather... idiosyncratic way + +if test "x$ax_pthread_clang" = "xyes"; then + + # Clang takes -pthread; it has never supported any other flag + + # (Note 1: This will need to be revisited if a system that Clang + # supports has POSIX threads in a separate library. This tends not + # to be the way of modern systems, but it's conceivable.) + + # (Note 2: On some systems, notably Darwin, -pthread is not needed + # to get POSIX threads support; the API is always present and + # active. We could reasonably leave PTHREAD_CFLAGS empty. But + # -pthread does define _REENTRANT, and while the Darwin headers + # ignore this macro, third-party headers might not.) + + # However, older versions of Clang make a point of warning the user + # that, in an invocation where only linking and no compilation is + # taking place, the -pthread option has no effect ("argument unused + # during compilation"). They expect -pthread to be passed in only + # when source code is being compiled. + # + # Problem is, this is at odds with the way Automake and most other + # C build frameworks function, which is that the same flags used in + # compilation (CFLAGS) are also used in linking. Many systems + # supported by AX_PTHREAD require exactly this for POSIX threads + # support, and in fact it is often not straightforward to specify a + # flag that is used only in the compilation phase and not in + # linking. Such a scenario is extremely rare in practice. + # + # Even though use of the -pthread flag in linking would only print + # a warning, this can be a nuisance for well-run software projects + # that build with -Werror. So if the active version of Clang has + # this misfeature, we search for an option to squash it. + + AC_CACHE_CHECK([whether Clang needs flag to prevent "argument unused" warning when linking with -pthread], + [ax_cv_PTHREAD_CLANG_NO_WARN_FLAG], + [ax_cv_PTHREAD_CLANG_NO_WARN_FLAG=unknown + # Create an alternate version of $ac_link that compiles and + # links in two steps (.c -> .o, .o -> exe) instead of one + # (.c -> exe), because the warning occurs only in the second + # step + ax_pthread_save_ac_link="$ac_link" + ax_pthread_sed='s/conftest\.\$ac_ext/conftest.$ac_objext/g' + ax_pthread_link_step=`AS_ECHO(["$ac_link"]) | sed "$ax_pthread_sed"` + ax_pthread_2step_ac_link="($ac_compile) && (echo ==== >&5) && ($ax_pthread_link_step)" + ax_pthread_save_CFLAGS="$CFLAGS" + for ax_pthread_try in '' -Qunused-arguments -Wno-unused-command-line-argument unknown; do + AS_IF([test "x$ax_pthread_try" = "xunknown"], [break]) + CFLAGS="-Werror -Wunknown-warning-option $ax_pthread_try -pthread $ax_pthread_save_CFLAGS" + ac_link="$ax_pthread_save_ac_link" + AC_LINK_IFELSE([AC_LANG_SOURCE([[int main(void){return 0;}]])], + [ac_link="$ax_pthread_2step_ac_link" + AC_LINK_IFELSE([AC_LANG_SOURCE([[int main(void){return 0;}]])], + [break]) + ]) + done + ac_link="$ax_pthread_save_ac_link" + CFLAGS="$ax_pthread_save_CFLAGS" + AS_IF([test "x$ax_pthread_try" = "x"], [ax_pthread_try=no]) + ax_cv_PTHREAD_CLANG_NO_WARN_FLAG="$ax_pthread_try" + ]) + + case "$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG" in + no | unknown) ;; + *) PTHREAD_CFLAGS="$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG $PTHREAD_CFLAGS" ;; + esac + +fi # $ax_pthread_clang = yes + + + # Various other checks: -if test "x$ax_pthread_ok" = xyes; then - save_LIBS="$LIBS" - LIBS="$PTHREAD_LIBS $LIBS" - save_CFLAGS="$CFLAGS" +if test "x$ax_pthread_ok" = "xyes"; then + ax_pthread_save_CFLAGS="$CFLAGS" + ax_pthread_save_LIBS="$LIBS" CFLAGS="$CFLAGS $PTHREAD_CFLAGS" + LIBS="$PTHREAD_LIBS $LIBS" # Detect AIX lossage: JOINABLE attribute is called UNDETACHED. - AC_MSG_CHECKING([for joinable pthread attribute]) - attr_name=unknown - for attr in PTHREAD_CREATE_JOINABLE PTHREAD_CREATE_UNDETACHED; do - AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], - [int attr = $attr; return attr /* ; */])], - [attr_name=$attr; break], - []) - done - AC_MSG_RESULT([$attr_name]) - if test "$attr_name" != PTHREAD_CREATE_JOINABLE; then - AC_DEFINE_UNQUOTED([PTHREAD_CREATE_JOINABLE], [$attr_name], - [Define to necessary symbol if this constant - uses a non-standard name on your system.]) - fi - - AC_MSG_CHECKING([if more special flags are required for pthreads]) - flag=no - case ${host_os} in - aix* | freebsd* | darwin*) flag="-D_THREAD_SAFE";; - osf* | hpux*) flag="-D_REENTRANT";; - solaris*) - if test "$GCC" = "yes"; then - flag="-D_REENTRANT" - else - # TODO: What about Clang on Solaris? - flag="-mt -D_REENTRANT" - fi - ;; - esac - AC_MSG_RESULT([$flag]) - if test "x$flag" != xno; then - PTHREAD_CFLAGS="$flag $PTHREAD_CFLAGS" - fi + AC_CACHE_CHECK([for joinable pthread attribute], + [ax_cv_PTHREAD_JOINABLE_ATTR], + [ax_cv_PTHREAD_JOINABLE_ATTR=unknown + for ax_pthread_attr in PTHREAD_CREATE_JOINABLE PTHREAD_CREATE_UNDETACHED; do + AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], + [int attr = $ax_pthread_attr; return attr /* ; */])], + [ax_cv_PTHREAD_JOINABLE_ATTR=$ax_pthread_attr; break], + []) + done + ]) + AS_IF([test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xunknown" && \ + test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xPTHREAD_CREATE_JOINABLE" && \ + test "x$ax_pthread_joinable_attr_defined" != "xyes"], + [AC_DEFINE_UNQUOTED([PTHREAD_CREATE_JOINABLE], + [$ax_cv_PTHREAD_JOINABLE_ATTR], + [Define to necessary symbol if this constant + uses a non-standard name on your system.]) + ax_pthread_joinable_attr_defined=yes + ]) + + AC_CACHE_CHECK([whether more special flags are required for pthreads], + [ax_cv_PTHREAD_SPECIAL_FLAGS], + [ax_cv_PTHREAD_SPECIAL_FLAGS=no + case $host_os in + solaris*) + ax_cv_PTHREAD_SPECIAL_FLAGS="-D_POSIX_PTHREAD_SEMANTICS" + ;; + esac + ]) + AS_IF([test "x$ax_cv_PTHREAD_SPECIAL_FLAGS" != "xno" && \ + test "x$ax_pthread_special_flags_added" != "xyes"], + [PTHREAD_CFLAGS="$ax_cv_PTHREAD_SPECIAL_FLAGS $PTHREAD_CFLAGS" + ax_pthread_special_flags_added=yes]) AC_CACHE_CHECK([for PTHREAD_PRIO_INHERIT], - [ax_cv_PTHREAD_PRIO_INHERIT], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include ]], - [[int i = PTHREAD_PRIO_INHERIT;]])], - [ax_cv_PTHREAD_PRIO_INHERIT=yes], - [ax_cv_PTHREAD_PRIO_INHERIT=no]) + [ax_cv_PTHREAD_PRIO_INHERIT], + [AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include ]], + [[int i = PTHREAD_PRIO_INHERIT; + return i;]])], + [ax_cv_PTHREAD_PRIO_INHERIT=yes], + [ax_cv_PTHREAD_PRIO_INHERIT=no]) ]) - AS_IF([test "x$ax_cv_PTHREAD_PRIO_INHERIT" = "xyes"], - [AC_DEFINE([HAVE_PTHREAD_PRIO_INHERIT], [1], [Have PTHREAD_PRIO_INHERIT.])]) + AS_IF([test "x$ax_cv_PTHREAD_PRIO_INHERIT" = "xyes" && \ + test "x$ax_pthread_prio_inherit_defined" != "xyes"], + [AC_DEFINE([HAVE_PTHREAD_PRIO_INHERIT], [1], [Have PTHREAD_PRIO_INHERIT.]) + ax_pthread_prio_inherit_defined=yes + ]) - LIBS="$save_LIBS" - CFLAGS="$save_CFLAGS" + CFLAGS="$ax_pthread_save_CFLAGS" + LIBS="$ax_pthread_save_LIBS" # More AIX lossage: compile with *_r variant - if test "x$GCC" != xyes; then + if test "x$GCC" != "xyes"; then case $host_os in aix*) AS_CASE(["x/$CC"], - [x*/c89|x*/c89_128|x*/c99|x*/c99_128|x*/cc|x*/cc128|x*/xlc|x*/xlc_v6|x*/xlc128|x*/xlc128_v6], - [#handle absolute path differently from PATH based program lookup - AS_CASE(["x$CC"], - [x/*], - [AS_IF([AS_EXECUTABLE_P([${CC}_r])],[PTHREAD_CC="${CC}_r"])], - [AC_CHECK_PROGS([PTHREAD_CC],[${CC}_r],[$CC])])]) + [x*/c89|x*/c89_128|x*/c99|x*/c99_128|x*/cc|x*/cc128|x*/xlc|x*/xlc_v6|x*/xlc128|x*/xlc128_v6], + [#handle absolute path differently from PATH based program lookup + AS_CASE(["x$CC"], + [x/*], + [ + AS_IF([AS_EXECUTABLE_P([${CC}_r])],[PTHREAD_CC="${CC}_r"]) + AS_IF([test "x${CXX}" != "x"], [AS_IF([AS_EXECUTABLE_P([${CXX}_r])],[PTHREAD_CXX="${CXX}_r"])]) + ], + [ + AC_CHECK_PROGS([PTHREAD_CC],[${CC}_r],[$CC]) + AS_IF([test "x${CXX}" != "x"], [AC_CHECK_PROGS([PTHREAD_CXX],[${CXX}_r],[$CXX])]) + ] + ) + ]) ;; esac fi fi test -n "$PTHREAD_CC" || PTHREAD_CC="$CC" +test -n "$PTHREAD_CXX" || PTHREAD_CXX="$CXX" AC_SUBST([PTHREAD_LIBS]) AC_SUBST([PTHREAD_CFLAGS]) AC_SUBST([PTHREAD_CC]) +AC_SUBST([PTHREAD_CXX]) # Finally, execute ACTION-IF-FOUND/ACTION-IF-NOT-FOUND: -if test x"$ax_pthread_ok" = xyes; then +if test "x$ax_pthread_ok" = "xyes"; then ifelse([$1],,[AC_DEFINE([HAVE_PTHREAD],[1],[Define if you have POSIX threads libraries and header files.])],[$1]) : else From 8e06e579ff11dc9fcdf9b517e80153e83b47f355 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:34:57 +1000 Subject: [PATCH 041/142] Add missing m4/ax_strings_strcasecmp.m4 companion macro configure.ac calls AX_STRINGS_STRCASECMP as a fallback when AX_STRING_STRCASECMP reports that strcasecmp() is not found in . The companion macro file ax_strings_strcasecmp.m4 was never added to m4/, so autoconf left AX_STRINGS_STRCASECMP unexpanded in the generated configure script. On any platform where strcasecmp() lives in rather than (e.g. very old BSDs or strict POSIX-only environments), running ./configure would fail with a "command not found" error. Add the companion macro (autoconf-archive serial 9, same author and licence as ax_string_strcasecmp.m4) so that the fallback path is correctly handled and HAVE_STRINGS_STRCASECMP is defined when needed. --- m4/ax_strings_strcasecmp.m4 | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 m4/ax_strings_strcasecmp.m4 diff --git a/m4/ax_strings_strcasecmp.m4 b/m4/ax_strings_strcasecmp.m4 new file mode 100644 index 00000000..b7896657 --- /dev/null +++ b/m4/ax_strings_strcasecmp.m4 @@ -0,0 +1,52 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_strings_strcasecmp.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_STRINGS_STRCASECMP +# +# DESCRIPTION +# +# This macro tries to find strcasecmp() in strings.h. +# +# Use this macro in conjunction with AX_STRING_STRCASECMP in your +# configure.in like so: +# +# AX_STRING_STRCASECMP +# if test x"$ac_cv_string_strcasecmp" = "xno" ; then +# AX_STRINGS_STRCASECMP +# fi +# +# This will cause either HAVE_STRING_STRCASECMP or HAVE_STRINGS_STRCASECMP +# to be defined in config.h, which will tell your code what header to +# include to get strcasecmp()'s prototype. +# +# LICENSE +# +# Copyright (c) 2008 Warren Young +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 9 + +AU_ALIAS([ETR_STRINGS_STRCASECMP], [AX_STRINGS_STRCASECMP]) +AC_DEFUN([AX_STRINGS_STRCASECMP], +[ +AC_CACHE_CHECK([for strcasecmp() in strings.h], ac_cv_strings_strcasecmp, [ + AC_LINK_IFELSE([AC_LANG_PROGRAM( + [[#include ]], + [[strcasecmp("foo", "bar");]])], + [ac_cv_strings_strcasecmp=yes], + [ac_cv_strings_strcasecmp=no]) +]) + + if test x"$ac_cv_strings_strcasecmp" = "xyes" + then + AC_DEFINE(HAVE_STRINGS_STRCASECMP, 1, + [ Define if your system has strcasecmp() in strings.h ]) + fi +]) dnl AX_STRINGS_STRCASECMP From b9cb70244d2a18ef44ef99adaaddb16fa0a528a2 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:38:21 +1000 Subject: [PATCH 042/142] Add unit tests for keyboard_read and keyboard_simulate_keypress keyboard_read() is the core function the ULA uses to read keyboard state; it ANDs together the half-rows selected by the high byte of the I/O port address. keyboard_simulate_keypress() computes what the ULA would return for a single key press given a particular port high byte. Neither function had dedicated unit tests before. The new tests verify: - keyboard_read returns 0xff with no keys pressed for any port selection - keyboard_read reflects a pressed key only when its half-row is selected - keyboard_read returns 0xff after a key is released - keyboard_simulate_keypress clears the key bit when the half-row is selected - keyboard_simulate_keypress returns 0xff when the half-row is not selected - keyboard_simulate_keypress returns 0xff for an unmapped key (KEYBOARD_NONE) --- unittests/unittests.c | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/unittests/unittests.c b/unittests/unittests.c index fd4147b7..580d230d 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -357,6 +357,56 @@ snapshot_copy_from_releases_keyboard_test( void ) return 0; } +static int +keyboard_read_test( void ) +{ + /* No keys pressed: all half-rows are 0xff, keyboard_read returns 0xff + regardless of which half-rows are selected. */ + keyboard_release_all(); + /* Select all half-rows (porth = 0x00 means every bit is low → select all) */ + TEST_ASSERT( keyboard_read( 0x00 ) == 0xff ); + /* Select no half-rows (porth = 0xff means every bit is high → select none) */ + TEST_ASSERT( keyboard_read( 0xff ) == 0xff ); + + /* Press 'a': sits in half-row 1, bit 0x01. + keyboard_read shifts porth right once per iteration and checks bit 0 each + time, so half-row N is selected when bit N of porth is 0. + 0xfd = 11111101b has bit 1 low → selects only half-row 1. */ + keyboard_press( KEYBOARD_a ); + TEST_ASSERT( keyboard_read( 0xfd ) == 0xfe ); /* bit 0 cleared */ + /* Selecting a different half-row should not show the pressed key. */ + TEST_ASSERT( keyboard_read( 0xfe ) == 0xff ); /* half-row 0, 'a' not there */ + /* Selecting all half-rows still shows the pressed key. */ + TEST_ASSERT( keyboard_read( 0x00 ) == 0xfe ); + keyboard_release( KEYBOARD_a ); + + /* After release the bit is restored. */ + TEST_ASSERT( keyboard_read( 0xfd ) == 0xff ); + + return 0; +} + +static int +keyboard_simulate_keypress_test( void ) +{ + /* 'a' is in half-row 1, bit 0x01. keyboard_simulate_keypress checks + whether half-row 1's bit (mask = 1<<1 = 0x02) is low in porth. */ + + /* porth = 0xfd (bit 1 low) → half-row 1 selected → bit 0x01 cleared */ + TEST_ASSERT( keyboard_simulate_keypress( 0xfd, KEYBOARD_a ) == 0xfe ); + + /* porth = 0xff (bit 1 high) → half-row 1 not selected → 0xff returned */ + TEST_ASSERT( keyboard_simulate_keypress( 0xff, KEYBOARD_a ) == 0xff ); + + /* porth = 0x00 (all bits low) → all half-rows selected → bit cleared */ + TEST_ASSERT( keyboard_simulate_keypress( 0x00, KEYBOARD_a ) == 0xfe ); + + /* An unknown/unmapped key should return 0xff unchanged. */ + TEST_ASSERT( keyboard_simulate_keypress( 0x00, KEYBOARD_NONE ) == 0xff ); + + return 0; +} + static int utils_safe_strdup_test( void ) { @@ -1121,6 +1171,8 @@ unittests_run( void ) r += floating_bus_test(); r += floating_bus_merge_test(); r += snapshot_copy_from_releases_keyboard_test(); + r += keyboard_read_test(); + r += keyboard_simulate_keypress_test(); r += utils_safe_strdup_test(); r += bitmap_ops_test(); r += mempool_test(); From b761ec9035e4441e82891b17253bd0b8666fa920 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:40:47 +1000 Subject: [PATCH 043/142] Add MEMPOOL_UNTRACKED coverage to mempool_test MEMPOOL_UNTRACKED (-1) is a special sentinel that bypasses pool tracking and allocates directly via libspectrum_malloc/malloc_n. The existing tests verified out-of-range pool IDs (-2, too large) return NULL, but never exercised the MEMPOOL_UNTRACKED code path. Add three assertions covering: - mempool_malloc( MEMPOOL_UNTRACKED, n ) returns non-NULL memory that does not affect any pool's tracked count - mempool_malloc_n( MEMPOOL_UNTRACKED, nmemb, size ) likewise - mempool_strdup( MEMPOOL_UNTRACKED, str ) returns a valid copy that does not affect any pool's tracked count Each allocation is freed with libspectrum_free to avoid leaks. --- unittests/unittests.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/unittests/unittests.c b/unittests/unittests.c index 580d230d..9c3827d1 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -521,6 +521,29 @@ mempool_test( void ) TEST_ASSERT( mempool_malloc( mempool_get_pools(), 23 ) == NULL ); TEST_ASSERT( mempool_malloc( -2, 23 ) == NULL ); + /* Test MEMPOOL_UNTRACKED: allocations succeed but bypass pool tracking */ + { + void *p = mempool_malloc( MEMPOOL_UNTRACKED, 16 ); + TEST_ASSERT( p != NULL ); + TEST_ASSERT( mempool_get_pool_size( pool1 ) == 0 ); + libspectrum_free( p ); + } + + { + void *p = mempool_malloc_n( MEMPOOL_UNTRACKED, 4, 8 ); + TEST_ASSERT( p != NULL ); + TEST_ASSERT( mempool_get_pool_size( pool1 ) == 0 ); + libspectrum_free( p ); + } + + { + char *s = mempool_strdup( MEMPOOL_UNTRACKED, "untracked" ); + TEST_ASSERT( s != NULL ); + TEST_ASSERT( strcmp( s, "untracked" ) == 0 ); + TEST_ASSERT( mempool_get_pool_size( pool1 ) == 0 ); + libspectrum_free( s ); + } + return 0; } From 2f2939f8edf8c7e8fa6f4cdc79b77d705410e94a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:43:34 +1000 Subject: [PATCH 044/142] Add rectangle active/inactive reallocation path coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add rectangle_realloc_test() to unittests_run() to verify the doubly-expanding allocation paths in rectangle_add() and rectangle_end_line(): - Test 1: Add 9 distinct (x,w) pairs on the same scan line, forcing the active-list to grow from the initial allocation of 8 to 16. - Test 2: Continue to 17 entries, triggering a second doubling to 32. - Test 3: Flush all 17 stale active rects via rectangle_end_line(300), exercising the inactive-list reallocation (0→8→16→32). - Test 4: Flush a second batch of 9 non-overlapping rects to confirm the inactive list grows correctly beyond 17 entries. These paths were previously untested; the existing rectangle_test() only exercised lists with up to three entries. --- unittests/unittests.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/unittests/unittests.c b/unittests/unittests.c index 9c3827d1..a3a42ddb 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -1185,6 +1185,41 @@ rectangle_test( void ) return 0; } +static int +rectangle_realloc_test( void ) +{ + int i; + int saved_frame_rate = settings_current.frame_rate; + + /* --- Test 1: force active-list reallocation by adding > 8 distinct rects --- */ + /* Initial active allocation is 8; the 9th unique (x,w) pair triggers doubling. */ + rectangle_reset(); + settings_current.frame_rate = 1; + for( i = 0; i < 9; i++ ) + rectangle_add( 0, i * 10, 5 ); + TEST_ASSERT( rectangle_get_active_count() == 9 ); + + /* --- Test 2: continue past 16 to trigger a second doubling (8->16->32) --- */ + for( i = 9; i < 17; i++ ) + rectangle_add( 0, i * 10, 5 ); + TEST_ASSERT( rectangle_get_active_count() == 17 ); + + /* --- Test 3: flushing > 8 rects forces inactive-list reallocation --- */ + /* All 17 active rects are stale (line 300 > line 0); they move to inactive. */ + rectangle_end_line( 300 ); + TEST_ASSERT( rectangle_get_active_count() == 0 ); + TEST_ASSERT( rectangle_inactive_count == 17 ); + + /* --- Test 4: a second flush of > 8 non-overlapping rects grows inactive further --- */ + for( i = 0; i < 9; i++ ) + rectangle_add( 1, i * 10 + 5, 3 ); + rectangle_end_line( 300 ); + TEST_ASSERT( rectangle_inactive_count == 26 ); + + settings_current.frame_rate = saved_frame_rate; + return 0; +} + int unittests_run( void ) { @@ -1202,6 +1237,7 @@ unittests_run( void ) r += paging_test(); r += debugger_disassemble_unittest(); r += rectangle_test(); + r += rectangle_realloc_test(); printf("Final return value: %d (should be 0)\n", r); From 1408862d2273ce3b9f3d6d72dae334b9005e0d8a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:48:38 +1000 Subject: [PATCH 045/142] Add modulo operator % to expression evaluator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debugger expression evaluator supported +, -, *, / but had no modulo operator. Add % with the same precedence as * and /, guarded against modulo-by-zero with a clear error message. Useful for breakpoint conditions such as (PC % 16 == 0) to fire on 16-byte-aligned addresses, or (counter % 256 == 0) to break every 256th iteration. Changes: - commandy.y: add '%' to the %left '*' '/' precedence line and add the grammar rule expression '%' expression - expression.c (binaryop_precedence): add case '%' alongside * and / - expression.c (evaluate_binaryop): add case '%' with zero check - expression.c (deparse_binaryop): add case '%' → "%" --- debugger/commandy.y | 6 +++++- debugger/expression.c | 12 +++++++++++- man/fuse.1 | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/debugger/commandy.y b/debugger/commandy.y index e3789fd6..ec18d85e 100644 --- a/debugger/commandy.y +++ b/debugger/commandy.y @@ -132,7 +132,7 @@ %left EQUALITY %left COMPARISON %left '+' '-' -%left '*' '/' +%left '*' '/' '%' %right NEGATE /* Unary minus, unary plus, !, ~ */ /* High precedence */ @@ -274,6 +274,10 @@ expression: NUMBER { $$ = debugger_expression_new_number( $1, debugger_memory_ $$ = debugger_expression_new_binaryop( '/', $1, $3, debugger_memory_pool ); if( !$$ ) YYABORT; } + | expression '%' expression { + $$ = debugger_expression_new_binaryop( '%', $1, $3, debugger_memory_pool ); + if( !$$ ) YYABORT; + } | expression EQUALITY expression { $$ = debugger_expression_new_binaryop( $2, $1, $3, debugger_memory_pool ); if( !$$ ) YYABORT; diff --git a/debugger/expression.c b/debugger/expression.c index 3a8e3c16..dbfa5101 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -130,7 +130,7 @@ binaryop_precedence( int operation ) case '^': return PRECEDENCE_BITWISE_XOR; case '&': return PRECEDENCE_BITWISE_AND; case '+': case '-': return PRECEDENCE_ADDITION; - case '*': case '/': return PRECEDENCE_MULTIPLICATION; + case '*': case '/': case '%': return PRECEDENCE_MULTIPLICATION; case DEBUGGER_TOKEN_EQUAL_TO: case DEBUGGER_TOKEN_NOT_EQUAL_TO: @@ -381,6 +381,15 @@ evaluate_binaryop( struct binaryop_type *binary ) return debugger_expression_evaluate( binary->op1 ) / op2; } + case '%': { + libspectrum_dword op2 = debugger_expression_evaluate( binary->op2 ); + if( op2 == 0 ) { + ui_error( UI_ERROR_ERROR, "modulo by 0" ); + return 0; + } + return debugger_expression_evaluate( binary->op1 ) % op2; + } + case DEBUGGER_TOKEN_EQUAL_TO: return debugger_expression_evaluate( binary->op1 ) == debugger_expression_evaluate( binary->op2 ); @@ -530,6 +539,7 @@ deparse_binaryop( char *buffer, size_t length, case '-': operation_string = "-"; break; case '*': operation_string = "*"; break; case '/': operation_string = "/"; break; + case '%': operation_string = "%"; break; case DEBUGGER_TOKEN_EQUAL_TO: operation_string = "=="; break; case DEBUGGER_TOKEN_NOT_EQUAL_TO: operation_string = "!="; break; case '<': operation_string = "<"; break; diff --git a/man/fuse.1 b/man/fuse.1 index 374ea495..6fb7446b 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -5263,8 +5263,8 @@ which uses a restricted version of C's syntax; exactly the same syntax is used for conditional breakpoints, with `0' being false and any other value being true. In numeric expressions, you can use integer constants (all calculations are done in integers), system variables, -debugger variables, parentheses, the standard four numeric operations -(`+', `\-', `*' and `/'), the (non-)equality operators `==' and `!=', +debugger variables, parentheses, the standard five numeric operations +(`+', `\-', `*', `/' and `%'), the (non-)equality operators `==' and `!=', the comparison operators `>', `<', `>=' and `<=', bitwise and (`&'), or (`|') and exclusive or (`^') and logical and (`&&') and or (`||'). Square brackets (`[' and `]') can be used to dereference a value; for From 8b45ceb2ac67077493f6d33c0efbd8685218ea46 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 15:56:56 +1000 Subject: [PATCH 046/142] Guard against undefined behaviour when end <= x in copy_critical_region_line() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the electron beam enters the display area at column 0 (beam_x == 0), copy_critical_region() calls copy_critical_region_line() with end == 0 and x == 0. The existing bit-mask construction then performed: bit_mask <<= x + (32 - end); /* shift by 32 — UB */ bit_mask >>= (32 - end); /* shift by 32 — UB */ Shifting a 32-bit value by its full width is undefined behaviour in C. On x86 the shift count is taken modulo 32, so the shifts become no-ops and bit_mask stays 0xffffffff. The subsequent dirty = ( display_maybe_dirty[y] & bit_mask ) >> x; then reads all dirty bits for the line and processes them before the beam has reached them, which can cause pixels to be written to display_last_screen prematurely, leading to missed updates in later frames. Fix: return early when end <= x (empty range). This is the minimal correct guard: it handles both the beam-at-column-0 case (end == x == 0) and any hypothetical end < x call. --- display.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/display.c b/display.c index 4bdc138c..c67d7b66 100644 --- a/display.c +++ b/display.c @@ -559,6 +559,12 @@ copy_critical_region_line( int y, int x, int end ) { libspectrum_dword bit_mask, dirty; + /* Nothing to do for an empty range; also guards against undefined + behaviour in the shift expressions below when end <= x (which can + occur legitimately when the beam is at column 0 at the start of a + display line). */ + if( end <= x ) return; + if( x < DISPLAY_WIDTH_COLS ) { /* Build a mask for the bits we're interested in */ From fbed9f7166d2f4b600ed53e26c6df18788336102 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 7 Jun 2026 20:47:57 +1000 Subject: [PATCH 047/142] Add << and >> bit shift operators to expression evaluator The debugger expression evaluator already supports bitwise &, |, ^, ~ and the modulo operator %. Shift operators are a natural complement and genuinely useful for Z80 debugging: BC >> 8 ; extract register B from the BC pair 1 << 3 ; mask for bit 3 (SP >> 12) == 4 ; break when stack pointer is in page 4 (PC >> 8) == 0x40 ; break in 0x4000..0x40ff range Precedence follows the C standard: << and >> sit above +/- but below comparison operators. Both are treated as non-associative (like /), so a << b << c requires explicit parentheses. Implementation: - debugger_internals.h: add DEBUGGER_TOKEN_LEFT_SHIFT and DEBUGGER_TOKEN_RIGHT_SHIFT constants (0x1001, 0x1002) - commandl.l: add "<<" -> LSHIFT and ">>" -> RSHIFT lexer rules, placed before the single-character < and > rules so longest-match wins - commandy.y: declare LSHIFT/RSHIFT tokens, add %left LSHIFT RSHIFT precedence level, add grammar rules - expression.c: add PRECEDENCE_SHIFT between PRECEDENCE_COMPARISON and PRECEDENCE_ADDITION; handle new tokens in binaryop_precedence(), evaluate_binaryop(), deparse_binaryop(), and is_non_associative() --- debugger/commandl.l | 3 +++ debugger/commandy.y | 15 +++++++++++++++ debugger/debugger_internals.h | 4 +++- debugger/expression.c | 19 +++++++++++++++++++ man/fuse.1 | 11 ++++++----- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/debugger/commandl.l b/debugger/commandl.l index 94725f81..66066dd6 100644 --- a/debugger/commandl.l +++ b/debugger/commandl.l @@ -95,6 +95,9 @@ w|wr|wri|writ|write { return WRITE; } "!=" { yylval.token = DEBUGGER_TOKEN_NOT_EQUAL_TO; return EQUALITY; } +"<<" { return LSHIFT; } +">>" { return RSHIFT; } + "<" { yylval.token = '<'; return COMPARISON; } ">" { yylval.token = '>'; return COMPARISON; } "<=" { yylval.token = DEBUGGER_TOKEN_LESS_THAN_OR_EQUAL_TO; diff --git a/debugger/commandy.y b/debugger/commandy.y index ec18d85e..4526d024 100644 --- a/debugger/commandy.y +++ b/debugger/commandy.y @@ -69,6 +69,8 @@ %token COMPARISON /* < > <= >= */ %token EQUALITY /* == != */ %token NEGATE /* ! ~ */ +%token LSHIFT /* << */ +%token RSHIFT /* >> */ %token BASE %token BREAK @@ -131,6 +133,7 @@ %left '&' %left EQUALITY %left COMPARISON +%left LSHIFT RSHIFT %left '+' '-' %left '*' '/' '%' %right NEGATE /* Unary minus, unary plus, !, ~ */ @@ -278,6 +281,18 @@ expression: NUMBER { $$ = debugger_expression_new_number( $1, debugger_memory_ $$ = debugger_expression_new_binaryop( '%', $1, $3, debugger_memory_pool ); if( !$$ ) YYABORT; } + | expression LSHIFT expression { + $$ = debugger_expression_new_binaryop( + DEBUGGER_TOKEN_LEFT_SHIFT, $1, $3, debugger_memory_pool + ); + if( !$$ ) YYABORT; + } + | expression RSHIFT expression { + $$ = debugger_expression_new_binaryop( + DEBUGGER_TOKEN_RIGHT_SHIFT, $1, $3, debugger_memory_pool + ); + if( !$$ ) YYABORT; + } | expression EQUALITY expression { $$ = debugger_expression_new_binaryop( $2, $1, $3, debugger_memory_pool ); if( !$$ ) YYABORT; diff --git a/debugger/debugger_internals.h b/debugger/debugger_internals.h index 0231742a..6659a297 100644 --- a/debugger/debugger_internals.h +++ b/debugger/debugger_internals.h @@ -67,8 +67,10 @@ typedef enum debugger_token { DEBUGGER_TOKEN_LESS_THAN_OR_EQUAL_TO = 0x2264, DEBUGGER_TOKEN_GREATER_THAN_OR_EQUAL_TO = 0x2265, - /* No real significance to this value */ + /* No real significance to these values */ DEBUGGER_TOKEN_DEREFERENCE = 0x1000, + DEBUGGER_TOKEN_LEFT_SHIFT = 0x1001, + DEBUGGER_TOKEN_RIGHT_SHIFT = 0x1002, } debugger_token; diff --git a/debugger/expression.c b/debugger/expression.c index dbfa5101..6d4eb17a 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -53,6 +53,7 @@ enum precedence_t { PRECEDENCE_BITWISE_AND, PRECEDENCE_EQUALITY, PRECEDENCE_COMPARISON, + PRECEDENCE_SHIFT, PRECEDENCE_ADDITION, PRECEDENCE_MULTIPLICATION, PRECEDENCE_NEGATE, @@ -132,6 +133,10 @@ binaryop_precedence( int operation ) case '+': case '-': return PRECEDENCE_ADDITION; case '*': case '/': case '%': return PRECEDENCE_MULTIPLICATION; + case DEBUGGER_TOKEN_LEFT_SHIFT: + case DEBUGGER_TOKEN_RIGHT_SHIFT: + return PRECEDENCE_SHIFT; + case DEBUGGER_TOKEN_EQUAL_TO: case DEBUGGER_TOKEN_NOT_EQUAL_TO: return PRECEDENCE_EQUALITY; @@ -390,6 +395,14 @@ evaluate_binaryop( struct binaryop_type *binary ) return debugger_expression_evaluate( binary->op1 ) % op2; } + case DEBUGGER_TOKEN_LEFT_SHIFT: + return debugger_expression_evaluate( binary->op1 ) << + debugger_expression_evaluate( binary->op2 ); + + case DEBUGGER_TOKEN_RIGHT_SHIFT: + return debugger_expression_evaluate( binary->op1 ) >> + debugger_expression_evaluate( binary->op2 ); + case DEBUGGER_TOKEN_EQUAL_TO: return debugger_expression_evaluate( binary->op1 ) == debugger_expression_evaluate( binary->op2 ); @@ -540,6 +553,8 @@ deparse_binaryop( char *buffer, size_t length, case '*': operation_string = "*"; break; case '/': operation_string = "/"; break; case '%': operation_string = "%"; break; + case DEBUGGER_TOKEN_LEFT_SHIFT: operation_string = "<<"; break; + case DEBUGGER_TOKEN_RIGHT_SHIFT: operation_string = ">>"; break; case DEBUGGER_TOKEN_EQUAL_TO: operation_string = "=="; break; case DEBUGGER_TOKEN_NOT_EQUAL_TO: operation_string = "!="; break; case '<': operation_string = "<"; break; @@ -631,6 +646,10 @@ is_non_associative( int operation ) case '+': case '*': return 0; case '-': case '/': return 1; + /* Shift operators are non-associative */ + case DEBUGGER_TOKEN_LEFT_SHIFT: return 1; + case DEBUGGER_TOKEN_RIGHT_SHIFT: return 1; + /* None of the comparison operators are associative due to them returning truth values */ case DEBUGGER_TOKEN_EQUAL_TO: diff --git a/man/fuse.1 b/man/fuse.1 index 6fb7446b..fcae080e 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -5264,11 +5264,12 @@ is used for conditional breakpoints, with `0' being false and any other value being true. In numeric expressions, you can use integer constants (all calculations are done in integers), system variables, debugger variables, parentheses, the standard five numeric operations -(`+', `\-', `*', `/' and `%'), the (non-)equality operators `==' and `!=', -the comparison operators `>', `<', `>=' and `<=', bitwise and (`&'), or -(`|') and exclusive or (`^') and logical and (`&&') and or (`||'). -Square brackets (`[' and `]') can be used to dereference a value; for -example `[0x4000]' will give the value of the first byte of the screen. +(`+', `\-', `*', `/' and `%'), the shift operators `<<' and `>>', the +(non-)equality operators `==' and `!=', the comparison operators `>', +`<', `>=' and `<=', bitwise and (`&'), or (`|') and exclusive or (`^') +and logical and (`&&') and or (`||'). Square brackets (`[' and `]') can +be used to dereference a value; for example `[0x4000]' will give the +value of the first byte of the screen. .PP System variables are specified via an .RI ` area : detail ' From 82d022b04b95a43607026a55a87ddf3de27e0a36 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Mon, 8 Jun 2026 13:26:01 +0200 Subject: [PATCH 048/142] Set the pulse level after loading a ROM block with tape traps A standard ROM block starts with a low pulse level and has an odd number of edges (due to the pilot tone), so it always ends with the level high. However, trap_load_block() loads the data but leaves tape_microphone untouched. This can break some polarity-sensitive loaders when the ROM block is followed by a custom block. This patch sets tape_microphone accordingly so that the following block starts with the expected low level. This fixes the following games using trap traps: - Action Force II - Gryzor - World Class Leader Board - Match Day II - Out Run (fixes bugs #398, #401, #402 and #411) --- tape.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tape.c b/tape.c index d91218a1..008a554d 100644 --- a/tape.c +++ b/tape.c @@ -456,6 +456,11 @@ tape_load_trap( void ) next thing to occur is the pause at the end of the current block */ libspectrum_tape_set_state( tape, LIBSPECTRUM_TAPE_STATE_PAUSE ); + /* Standard ROM blocks start with a low pulse level and have an odd + * number of pulses (due to the pilot tone), so at the end the level + * is always high. */ + tape_microphone = 1; + return 0; } From b99abc9673c467ee374e2469f9200fd16216687c Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 10 Jun 2026 23:28:47 +1000 Subject: [PATCH 049/142] Fix missing '%' case in is_non_associative() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The modulo operator '%' was added to binaryop_precedence() with PRECEDENCE_MULTIPLICATION, sharing that level with '*' and '/'. However, is_non_associative() — which is called by brackets_necessary() when two operators share the same precedence — had no case for '%'. Reaching the default path in is_non_associative() calls fuse_abort(), so any expression mixing '%' with another PRECEDENCE_MULTIPLICATION operator (e.g. '2 * 3 % 5', '10 % 4 % 3') would crash Fuse. Fix: add '%' to the non-associative group alongside '-' and '/'. Modulo is non-associative for the same reason as division: '10 % 4 % 3 = 2' but '10 % (4 % 3) = 0'. --- debugger/expression.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debugger/expression.c b/debugger/expression.c index 6d4eb17a..e80c95da 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -644,7 +644,7 @@ is_non_associative( int operation ) /* Simple cases */ case '+': case '*': return 0; - case '-': case '/': return 1; + case '-': case '/': case '%': return 1; /* Shift operators are non-associative */ case DEBUGGER_TOKEN_LEFT_SHIFT: return 1; From f6244c5bcc84be796fad943764e492e7de18ffa4 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 11 Jun 2026 19:54:43 +1000 Subject: [PATCH 050/142] Replace magic number 14 with PSG_REGISTERS constant --- psg.c | 4 ++-- psg.h | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/psg.c b/psg.c index c00013a0..688baa1e 100644 --- a/psg.c +++ b/psg.c @@ -130,13 +130,13 @@ psg_frame( void ) /* check if any AY sound events have happened this frame */ ay_updated = 0; - for( i = 0; i < 14 && !ay_updated; i++ ) + for( i = 0; i < PSG_REGISTERS && !ay_updated; i++ ) ay_updated = psg_registers_written[i]; if( ay_updated ) { write_frame_separator(); - for( i = 0; i < 14; i++ ) { + for( i = 0; i < PSG_REGISTERS; i++ ) { if( psg_registers_written[i] ) { putc( i, psg_file ); putc( psg_register_values[i], psg_file ); diff --git a/psg.h b/psg.h index 8241e5d4..cf2b9eef 100644 --- a/psg.h +++ b/psg.h @@ -26,6 +26,10 @@ #include "libspectrum.h" +/* Number of AY registers stored in a .psg file (registers 0-13; registers + 14 and 15 are I/O ports and are not part of the PSG file format) */ +#define PSG_REGISTERS 14 + /* Are we currently recording a .psg file? */ extern int psg_recording; From dfba113a88a4e2e35e7230d74840bcf3fa1ff802 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 11 Jun 2026 20:13:58 +1000 Subject: [PATCH 051/142] Replace magic number 10 with TIMER_SPEED_HISTORY_SAMPLES The rolling speed estimator in timer.c kept its history window size as bare literal 10 in three places: the array declaration, the guard that switches from 'not enough data' to live estimation, and the ring-buffer modulo wrap. Introduce TIMER_SPEED_HISTORY_SAMPLES = 10 so that all three sites name the same constant, making the relationship obvious and allowing a future change to the window size in one place. --- timer/timer.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/timer/timer.c b/timer/timer.c index 66b67ae6..3f1b108c 100644 --- a/timer/timer.c +++ b/timer/timer.c @@ -39,8 +39,12 @@ static void timer_frame_callback_sound( libspectrum_dword last_tstates ); * Routines for estimating emulation speed */ -/* The actual time at the end of each of the last 10 emulated seconds */ -static double stored_times[10]; +/* Number of one-second speed samples kept for the rolling speed estimate */ +#define TIMER_SPEED_HISTORY_SAMPLES 10 + +/* The actual time at the end of each of the last TIMER_SPEED_HISTORY_SAMPLES + emulated seconds */ +static double stored_times[TIMER_SPEED_HISTORY_SAMPLES]; /* Which is the next entry in 'stored_times' that we will update */ static size_t next_stored_time; @@ -72,14 +76,14 @@ timer_estimate_speed( void ) current_time = timer_get_time(); if( current_time < 0 ) return 1; - if( samples < 10 ) { + if( samples < TIMER_SPEED_HISTORY_SAMPLES ) { /* If we don't have enough data, assume we're running at the desired speed :-) */ current_speed = settings_current.emulation_speed; } else { - current_speed = 10 * 100 / + current_speed = TIMER_SPEED_HISTORY_SAMPLES * 100 / ( current_time - stored_times[ next_stored_time ] ); } @@ -87,7 +91,7 @@ timer_estimate_speed( void ) stored_times[ next_stored_time ] = current_time; - next_stored_time = ( next_stored_time + 1 ) % 10; + next_stored_time = ( next_stored_time + 1 ) % TIMER_SPEED_HISTORY_SAMPLES; frames_until_update = ( machine_current->timings.processor_speed / machine_current->timings.tstates_per_frame ) - 1; From 40206eaea31ec8cba8d680d811bc9bf701beeb55 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 11 Jun 2026 20:17:02 +1000 Subject: [PATCH 052/142] Replace magic numbers with AY_CHANNELS and AY_REGISTERS constants The AY sound emulation in sound.c used three bare magic numbers: - 3 for the number of AY tone channels - 16 for the AY register file size (in sound_ay_registers and the reset loop) Introduce #define AY_CHANNELS 3 in sound.c to name the channel count, and use the existing AY_REGISTERS constant (defined in peripherals/sound/ay.h, already transitively included via machine.h) for the register array size. --- sound.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/sound.c b/sound.c index d651da4b..25a70c58 100644 --- a/sound.c +++ b/sound.c @@ -63,6 +63,7 @@ int sound_stereo_ay = SOUND_STEREO_AY_NONE; /* local copy of settings_current.st * 50th I think this should be plenty. */ #define AY_CHANGE_MAX 8000 +#define AY_CHANNELS 3 int sound_framesiz; @@ -70,13 +71,13 @@ static int sound_channels; static unsigned int ay_tone_levels[16]; -static unsigned int ay_tone_tick[3], ay_tone_high[3], ay_noise_tick; +static unsigned int ay_tone_tick[AY_CHANNELS], ay_tone_high[AY_CHANNELS], ay_noise_tick; static unsigned int ay_tone_cycles, ay_env_cycles; static unsigned int ay_env_internal_tick, ay_env_tick; -static unsigned int ay_tone_period[3], ay_noise_period, ay_env_period; +static unsigned int ay_tone_period[AY_CHANNELS], ay_noise_period, ay_env_period; /* Local copy of the AY registers */ -static libspectrum_byte sound_ay_registers[16]; +static libspectrum_byte sound_ay_registers[AY_REGISTERS]; struct ay_change_tag { @@ -176,7 +177,7 @@ sound_ay_init( void ) ay_noise_tick = ay_noise_period = 0; ay_env_internal_tick = ay_env_tick = ay_env_period = 0; ay_tone_cycles = ay_env_cycles = 0; - for( f = 0; f < 3; f++ ) + for( f = 0; f < AY_CHANNELS; f++ ) ay_tone_tick[f] = ay_tone_high[f] = 0, ay_tone_period[f] = 1; ay_change_count = 0; @@ -444,7 +445,7 @@ sound_ay_overlay( void ) static int rng = 1; static int noise_toggle = 0; static int env_first = 1, env_rev = 0, env_counter = 15; - int tone_level[3]; + int tone_level[AY_CHANNELS]; int mixer, envshape; int g, level; libspectrum_dword f; @@ -503,14 +504,14 @@ sound_ay_overlay( void ) } /* the tone level if no enveloping is being used */ - for( g = 0; g < 3; g++ ) + for( g = 0; g < AY_CHANNELS; g++ ) tone_level[g] = ay_tone_levels[ sound_ay_registers[ 8 + g ] & 15 ]; /* envelope */ envshape = sound_ay_registers[13]; level = ay_tone_levels[ env_counter ]; - for( g = 0; g < 3; g++ ) + for( g = 0; g < AY_CHANNELS; g++ ) if( sound_ay_registers[ 8 + g ] & 16 ) tone_level[g] = level; @@ -666,9 +667,9 @@ sound_ay_reset( void ) sound_ay_init(); ay_change_count = 0; - for( f = 0; f < 16; f++ ) + for( f = 0; f < AY_REGISTERS; f++ ) sound_ay_write( f, 0, 0 ); - for( f = 0; f < 3; f++ ) + for( f = 0; f < AY_CHANNELS; f++ ) ay_tone_high[f] = 0; ay_tone_cycles = ay_env_cycles = 0; } From 10824aed6bc866f8a8c1b0ae6e9be965ede99e66 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 11 Jun 2026 20:20:15 +1000 Subject: [PATCH 053/142] Add named constants for flash attribute period The FLASH attribute logic in display_frame() used bare integer literals 16 and 32 for the flash half-period and full-period respectively. The helper display_reset_frame_count() used the magic value 31 (= 32 - 1). Introduce DISPLAY_FLASH_HALF_PERIOD (16) and DISPLAY_FLASH_PERIOD (32) in display.h alongside the other screen geometry constants. Replace all three occurrences in display.c. --- display.c | 11 ++++++----- display.h | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/display.c b/display.c index c67d7b66..399cb04d 100644 --- a/display.c +++ b/display.c @@ -966,10 +966,10 @@ display_frame( void ) update_ui_screen(); display_frame_count++; - if(display_frame_count==16) { + if(display_frame_count==DISPLAY_FLASH_HALF_PERIOD) { display_flash_reversed=1; display_dirty_flashing(); - } else if(display_frame_count==32) { + } else if(display_frame_count==DISPLAY_FLASH_PERIOD) { display_flash_reversed=0; display_dirty_flashing(); display_frame_count=0; @@ -1108,9 +1108,10 @@ display_getpixel( int x, int y ) void display_reset_frame_count( void ) { - /* We set the frame count to 31 so the next call to display_frame() - pushes us back to zero and resets display_flash_reversed */ - display_frame_count = 31; + /* We set the frame count to DISPLAY_FLASH_PERIOD - 1 so the next call + to display_frame() pushes us back to zero and resets + display_flash_reversed */ + display_frame_count = DISPLAY_FLASH_PERIOD - 1; } void diff --git a/display.h b/display.h index 1ff4bfae..31c838de 100644 --- a/display.h +++ b/display.h @@ -60,6 +60,11 @@ /* The aspect ratio corrected display width */ #define DISPLAY_ASPECT_WIDTH ( DISPLAY_SCREEN_WIDTH / 2 ) +/* The Spectrum's FLASH attribute toggles every 16 frames. One complete + flash cycle (normal → reversed → normal) spans 32 frames. */ +#define DISPLAY_FLASH_HALF_PERIOD 16 +#define DISPLAY_FLASH_PERIOD 32 + extern int display_ui_initialised; extern libspectrum_byte display_lores_border; From 80417d4177c6115df5ada9799a1ca3ef37fec8c5 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 11 Jun 2026 20:28:51 +1000 Subject: [PATCH 054/142] Add KEYBOARD_HALFROWS constant for half-row count The ZX Spectrum has 8 keyboard half-rows, and this count appeared as a bare literal 8 in three places in keyboard.c and keyboard.h: - the keyboard_return_values array size declaration (keyboard.h and .c) - the scanning loop in keyboard_read() - the reset loop in keyboard_release_all() Introduce KEYBOARD_HALFROWS = 8 in keyboard.h with a brief comment and replace all three magic literals. --- keyboard.c | 6 +++--- keyboard.h | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/keyboard.c b/keyboard.c index 1f235c05..eda42fa9 100644 --- a/keyboard.c +++ b/keyboard.c @@ -36,7 +36,7 @@ /* Bit masks for each of the eight keyboard half-rows; `AND' the selected ones of these to get the value to return */ -libspectrum_byte keyboard_return_values[8]; +libspectrum_byte keyboard_return_values[KEYBOARD_HALFROWS]; /* The hash used for storing the UI -> Fuse input layer key mappings */ static GHashTable *keysyms_hash; @@ -327,7 +327,7 @@ keyboard_read( libspectrum_byte porth ) { libspectrum_byte data = 0xff; int i; - for( i=0; i<8; i++,porth>>=1 ) { + for( i=0; i>=1 ) { if(! (porth&0x01) ) data &= keyboard_return_values[i]; } @@ -359,7 +359,7 @@ int keyboard_release_all( void ) { int i; - for( i=0; i<8; i++ ) keyboard_return_values[i] = 0xff; + for( i=0; i Date: Sat, 13 Jun 2026 18:29:35 +1000 Subject: [PATCH 055/142] Add AY_ENV_STEPS constant for envelope step count Replace all bare 16 literals that represent the AY-3-8910 envelope step count with the new AY_ENV_STEPS named constant. This parallels the existing AY_CLOCK_DIVISOR constant used for clock division and makes the two logically distinct values explicit: although both happen to equal 16 for the AY-3-8910, AY_ENV_STEPS (amplitude steps per envelope period / distinct volume levels) and AY_CLOCK_DIVISOR (tone/ noise clock prescaler) have different meanings and could diverge in a variant chip. --- sound.c | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/sound.c b/sound.c index 25a70c58..cab523ce 100644 --- a/sound.c +++ b/sound.c @@ -64,12 +64,15 @@ int sound_stereo_ay = SOUND_STEREO_AY_NONE; /* local copy of settings_current.st */ #define AY_CHANGE_MAX 8000 #define AY_CHANNELS 3 +/* the AY envelope generator cycles through 16 amplitude steps per + envelope period; the chip also has 16 distinct output volume levels */ +#define AY_ENV_STEPS 16 int sound_framesiz; static int sound_channels; -static unsigned int ay_tone_levels[16]; +static unsigned int ay_tone_levels[AY_ENV_STEPS]; static unsigned int ay_tone_tick[AY_CHANNELS], ay_tone_high[AY_CHANNELS], ay_noise_tick; static unsigned int ay_tone_cycles, ay_env_cycles; @@ -162,7 +165,7 @@ sound_ay_init( void ) * Matthew Westcott, adjusted as I described in a followup to his post, * then scaled to 0..0xffff. */ - static const int levels[16] = { + static const int levels[AY_ENV_STEPS] = { 0x0000, 0x0385, 0x053D, 0x0770, 0x0AD7, 0x0FD5, 0x15B0, 0x230C, 0x2B4C, 0x43C1, 0x5A4B, 0x732F, @@ -171,7 +174,7 @@ sound_ay_init( void ) int f; /* scale the values down to fit */ - for( f = 0; f < 16; f++ ) + for( f = 0; f < AY_ENV_STEPS; f++ ) ay_tone_levels[f] = ( levels[f] * AMPL_AY_TONE + 0x8000 ) / 0xffff; ay_noise_tick = ay_noise_period = 0; @@ -444,7 +447,7 @@ sound_ay_overlay( void ) { static int rng = 1; static int noise_toggle = 0; - static int env_first = 1, env_rev = 0, env_counter = 15; + static int env_first = 1, env_rev = 0, env_counter = AY_ENV_STEPS - 1; int tone_level[AY_CHANNELS]; int mixer, envshape; int g, level; @@ -498,7 +501,7 @@ sound_ay_overlay( void ) ay_env_internal_tick = ay_env_tick = ay_env_cycles = 0; env_first = 1; env_rev = 0; - env_counter = ( sound_ay_registers[13] & AY_ENV_ATTACK ) ? 0 : 15; + env_counter = ( sound_ay_registers[13] & AY_ENV_ATTACK ) ? 0 : AY_ENV_STEPS - 1; break; } } @@ -518,8 +521,8 @@ sound_ay_overlay( void ) /* envelope output counter gets incr'd every 16 AY cycles. */ ay_env_cycles += AY_CLOCK_DIVISOR; noise_count = 0; - while( ay_env_cycles >= 16 ) { - ay_env_cycles -= 16; + while( ay_env_cycles >= AY_CLOCK_DIVISOR ) { + ay_env_cycles -= AY_CLOCK_DIVISOR; noise_count++; ay_env_tick++; while( ay_env_tick >= ay_env_period ) { @@ -534,13 +537,13 @@ sound_ay_overlay( void ) env_counter += ( envshape & AY_ENV_ATTACK ) ? 1 : -1; if( env_counter < 0 ) env_counter = 0; - if( env_counter > 15 ) - env_counter = 15; + if( env_counter > AY_ENV_STEPS - 1 ) + env_counter = AY_ENV_STEPS - 1; } ay_env_internal_tick++; - while( ay_env_internal_tick >= 16 ) { - ay_env_internal_tick -= 16; + while( ay_env_internal_tick >= AY_ENV_STEPS ) { + ay_env_internal_tick -= AY_ENV_STEPS; /* end of cycle */ if( !( envshape & AY_ENV_CONT ) ) @@ -548,13 +551,13 @@ sound_ay_overlay( void ) else { if( envshape & AY_ENV_HOLD ) { if( env_first && ( envshape & AY_ENV_ALT ) ) - env_counter = ( env_counter ? 0 : 15 ); + env_counter = ( env_counter ? 0 : AY_ENV_STEPS - 1 ); } else { /* non-hold */ if( envshape & AY_ENV_ALT ) env_rev = !env_rev; else - env_counter = ( envshape & AY_ENV_ATTACK ) ? 0 : 15; + env_counter = ( envshape & AY_ENV_ATTACK ) ? 0 : AY_ENV_STEPS - 1; } } From b8aa5ff3f02e9274cb70f582766f90af4096eccd Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 13 Jun 2026 18:34:24 +1000 Subject: [PATCH 056/142] Name PSG file-format magic numbers as constants The write_frame_separator() helper and psg_start_recording() used bare integer literals whose meaning is only apparent from the PSG file-format specification: 12 -- number of reserved (zero) bytes that follow the 4-byte "PSG\x1a" magic in the file header 0xff -- byte marker for a single empty (no-AY-event) frame 0xfe -- byte marker for a multi-frame block (followed by a count byte) 4 -- number of empty frames encoded by each count unit in a multi-frame block Introduce four named constants in psg.h: PSG_HEADER_PADDING 12 PSG_SINGLE_FRAME_MARKER 0xff PSG_MULTI_FRAME_MARKER 0xfe PSG_FRAMES_PER_BLOCK 4 and replace every occurrence of the bare literals in psg.c. --- psg.c | 12 ++++++------ psg.h | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/psg.c b/psg.c index 688baa1e..0e738b0c 100644 --- a/psg.c +++ b/psg.c @@ -70,7 +70,7 @@ psg_start_recording( const char *filename ) ui_error( UI_ERROR_ERROR, "unable to write PSG file header" ); return 1; } - for( i = 0; i < 12; i++ ) putc( 0, psg_file ); + for( i = 0; i < PSG_HEADER_PADDING; i++ ) putc( 0, psg_file ); /* begin with no registers written */ for( i = 0; i < AY_REGISTERS; i++ ) psg_registers_written[i] = 0; @@ -101,21 +101,21 @@ psg_stop_recording( void ) static int write_frame_separator( void ) { - while( psg_empty_frame_count >= 4 ) { + while( psg_empty_frame_count >= PSG_FRAMES_PER_BLOCK ) { int count; - count = psg_empty_frame_count / 4; + count = psg_empty_frame_count / PSG_FRAMES_PER_BLOCK; if( count > 0xff ) count = 0xff; - putc( 0xfe, psg_file ); + putc( PSG_MULTI_FRAME_MARKER, psg_file ); putc( count, psg_file ); - psg_empty_frame_count -= 4 * count; + psg_empty_frame_count -= PSG_FRAMES_PER_BLOCK * count; } for( ; psg_empty_frame_count; psg_empty_frame_count-- ) - putc( 0xff, psg_file ); + putc( PSG_SINGLE_FRAME_MARKER, psg_file ); return 0; } diff --git a/psg.h b/psg.h index cf2b9eef..276704d8 100644 --- a/psg.h +++ b/psg.h @@ -30,6 +30,16 @@ 14 and 15 are I/O ports and are not part of the PSG file format) */ #define PSG_REGISTERS 14 +/* PSG file format constants */ +/* Reserved (zero) bytes that follow the 4-byte "PSG\x1a" magic in the header */ +#define PSG_HEADER_PADDING 12 +/* Byte marker for a single empty frame */ +#define PSG_SINGLE_FRAME_MARKER 0xff +/* Byte marker for a block of PSG_FRAMES_PER_BLOCK empty frames (followed by count) */ +#define PSG_MULTI_FRAME_MARKER 0xfe +/* Number of empty frames encoded per count unit in a multi-frame block */ +#define PSG_FRAMES_PER_BLOCK 4 + /* Are we currently recording a .psg file? */ extern int psg_recording; From 8537ec52d522ba53be81c6283f021dd17c2b84cd Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 14 Jun 2026 21:42:28 +1000 Subject: [PATCH 057/142] Add unit tests for expression evaluator Add debugger_expression_unittest() covering: - Integer literal evaluation - All binary arithmetic operators (+, -, *, /) - Division by zero (returns 0, no crash) - Modulo operator % (added in commit 6d7cea50) - Modulo by zero (returns 0; bug fixed in commit 4748449b) - Bit-shift operators << and >> (added in commit aced0fd4) - Bitwise operators (&, |, ^) - Comparison operators (==, !=, <, >, <=, >=) - Logical operators (&& and ||) - Deparse output for %, <<, >> operators - is_non_associative('%') fix: deparsing (3*4)%5 must bracket the left operand; before the fix this called fuse_abort() Tests use MEMPOOL_UNTRACKED so no debugger pool initialisation is required; expressions are freed via debugger_expression_delete. --- debugger/debugger.h | 1 + debugger/expression.c | 204 ++++++++++++++++++++++++++++++++++++++++++ unittests/unittests.c | 1 + 3 files changed, 206 insertions(+) diff --git a/debugger/debugger.h b/debugger/debugger.h index fe5f7a4a..f40103e6 100644 --- a/debugger/debugger.h +++ b/debugger/debugger.h @@ -97,5 +97,6 @@ void debugger_system_variable_register( /* Unit tests */ int debugger_disassemble_unittest( void ); int debugger_disassemble_decimal_unittest( void ); +int debugger_expression_unittest( void ); #endif /* #ifndef FUSE_DEBUGGER_H */ diff --git a/debugger/expression.c b/debugger/expression.c index e80c95da..6af859e3 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -676,3 +676,207 @@ is_non_associative( int operation ) fuse_abort(); } +/* Unit tests for the debugger expression evaluator */ + +static int +eval_integer_test( libspectrum_dword value, libspectrum_dword expected ) +{ + debugger_expression *expr; + libspectrum_dword result; + + expr = debugger_expression_new_number( value, MEMPOOL_UNTRACKED ); + result = debugger_expression_evaluate( expr ); + debugger_expression_delete( expr ); + + if( result != expected ) { + printf( "expression eval 'integer': expected %u, got %u\n", + (unsigned)expected, (unsigned)result ); + return 1; + } + + return 0; +} + +static int +eval_binary_test( int op, libspectrum_dword v1, libspectrum_dword v2, + libspectrum_dword expected, const char *label ) +{ + debugger_expression *left, *right, *expr; + libspectrum_dword result; + + left = debugger_expression_new_number( v1, MEMPOOL_UNTRACKED ); + right = debugger_expression_new_number( v2, MEMPOOL_UNTRACKED ); + expr = debugger_expression_new_binaryop( op, left, right, MEMPOOL_UNTRACKED ); + result = debugger_expression_evaluate( expr ); + debugger_expression_delete( expr ); + + if( result != expected ) { + printf( "expression eval '%s': expected %u, got %u\n", + label, (unsigned)expected, (unsigned)result ); + return 1; + } + + return 0; +} + +static int +eval_unary_test( int op, libspectrum_dword v, libspectrum_dword expected, + const char *label ) +{ + debugger_expression *operand, *expr; + libspectrum_dword result; + + operand = debugger_expression_new_number( v, MEMPOOL_UNTRACKED ); + expr = debugger_expression_new_unaryop( op, operand, MEMPOOL_UNTRACKED ); + result = debugger_expression_evaluate( expr ); + debugger_expression_delete( expr ); + + if( result != expected ) { + printf( "expression eval '%s': expected %u, got %u\n", + label, (unsigned)expected, (unsigned)result ); + return 1; + } + + return 0; +} + +/* Deparse helper: takes ownership of expr and frees it after the check */ +static int +deparse_test( debugger_expression *expr, const char *expected, + const char *label ) +{ + char buf[64]; + + debugger_expression_deparse( buf, sizeof( buf ), expr ); + debugger_expression_delete( expr ); + + if( strcmp( buf, expected ) ) { + printf( "expression deparse '%s': expected '%s', got '%s'\n", + label, expected, buf ); + return 1; + } + + return 0; +} + +int +debugger_expression_unittest( void ) +{ + int r = 0; + int saved_base; + + /* Integer literals */ + r += eval_integer_test( 0, 0 ); + r += eval_integer_test( 42, 42 ); + + /* Arithmetic */ + r += eval_binary_test( '+', 3, 4, 7, "add" ); + r += eval_binary_test( '-', 10, 3, 7, "sub" ); + r += eval_binary_test( '*', 3, 4, 12, "mul" ); + r += eval_binary_test( '/', 12, 4, 3, "div" ); + +/* Only run on null UI as it creates a popup */ +#ifdef UI_NULL + /* Division by zero returns 0 without crashing */ + r += eval_binary_test( '/', 5, 0, 0, "div-by-zero" ); +#endif + + /* Modulo operator */ + r += eval_binary_test( '%', 10, 3, 1, "mod" ); + r += eval_binary_test( '%', 9, 3, 0, "mod-exact" ); + +/* Only run on null UI as it creates a popup */ +#ifdef UI_NULL + /* Modulo by zero returns 0 without crashing */ + r += eval_binary_test( '%', 10, 0, 0, "mod-by-zero" ); +#endif + + /* Bit-shift operators */ + r += eval_binary_test( DEBUGGER_TOKEN_LEFT_SHIFT, 1, 3, 8, "lshift" ); + r += eval_binary_test( DEBUGGER_TOKEN_LEFT_SHIFT, 1, 7, 128, "lshift-7" ); + r += eval_binary_test( DEBUGGER_TOKEN_RIGHT_SHIFT, 16, 2, 4, "rshift" ); + r += eval_binary_test( DEBUGGER_TOKEN_RIGHT_SHIFT, 8, 3, 1, "rshift-3" ); + + /* Bitwise operators */ + r += eval_binary_test( '&', 0xF0, 0xFF, 0xF0, "bitwise-and" ); + r += eval_binary_test( '|', 0x0F, 0xF0, 0xFF, "bitwise-or" ); + r += eval_binary_test( '^', 0xFF, 0x0F, 0xF0, "bitwise-xor" ); + + /* Comparison operators */ + r += eval_binary_test( DEBUGGER_TOKEN_EQUAL_TO, 5, 5, 1, "eq-true" ); + r += eval_binary_test( DEBUGGER_TOKEN_EQUAL_TO, 5, 6, 0, "eq-false" ); + r += eval_binary_test( DEBUGGER_TOKEN_NOT_EQUAL_TO, 5, 6, 1, "ne-true" ); + r += eval_binary_test( DEBUGGER_TOKEN_NOT_EQUAL_TO, 5, 5, 0, "ne-false" ); + r += eval_binary_test( '<', 3, 5, 1, "lt-true" ); + r += eval_binary_test( '<', 5, 3, 0, "lt-false" ); + r += eval_binary_test( '>', 5, 3, 1, "gt-true" ); + r += eval_binary_test( '>', 3, 5, 0, "gt-false" ); + r += eval_binary_test( DEBUGGER_TOKEN_LESS_THAN_OR_EQUAL_TO, 5, 5, 1, "lte-eq" ); + r += eval_binary_test( DEBUGGER_TOKEN_LESS_THAN_OR_EQUAL_TO, 4, 5, 1, "lte-lt" ); + r += eval_binary_test( DEBUGGER_TOKEN_LESS_THAN_OR_EQUAL_TO, 6, 5, 0, "lte-false" ); + r += eval_binary_test( DEBUGGER_TOKEN_GREATER_THAN_OR_EQUAL_TO, 5, 5, 1, "gte-eq" ); + r += eval_binary_test( DEBUGGER_TOKEN_GREATER_THAN_OR_EQUAL_TO, 6, 5, 1, "gte-gt" ); + r += eval_binary_test( DEBUGGER_TOKEN_GREATER_THAN_OR_EQUAL_TO, 4, 5, 0, "gte-false" ); + + /* Logical operators */ + r += eval_binary_test( DEBUGGER_TOKEN_LOGICAL_AND, 1, 1, 1, "logical-and-true" ); + r += eval_binary_test( DEBUGGER_TOKEN_LOGICAL_AND, 1, 0, 0, "logical-and-false" ); + r += eval_binary_test( DEBUGGER_TOKEN_LOGICAL_OR, 1, 0, 1, "logical-or-true" ); + r += eval_binary_test( DEBUGGER_TOKEN_LOGICAL_OR, 0, 0, 0, "logical-or-false" ); + + /* Unary operators */ + r += eval_unary_test( '!', 0, 1, "logical-not-zero" ); + r += eval_unary_test( '!', 1, 0, "logical-not-one" ); + r += eval_unary_test( '~', 0, 0xFFFFFFFF, "bitwise-not-zero" ); + r += eval_unary_test( '~', 0xFFFFFFFF, 0, "bitwise-not-ones" ); + + /* Deparse tests: save and restore output base */ + saved_base = debugger_output_base; + debugger_output_base = 16; + + r += deparse_test( + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 + 0x4", "deparse-add" ); + + r += deparse_test( + debugger_expression_new_binaryop( '%', + debugger_expression_new_number( 10, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0xa % 0x3", "deparse-mod" ); + + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LEFT_SHIFT, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x1 << 0x3", "deparse-lshift" ); + + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_RIGHT_SHIFT, + debugger_expression_new_number( 16, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x10 >> 0x2", "deparse-rshift" ); + + /* is_non_associative('%') fix: deparsing (3 * 4) % 5 must bracket the + left operand since % is non-associative and equal-precedence to *. + Before the fix the missing '%' case in is_non_associative() would abort. */ + r += deparse_test( + debugger_expression_new_binaryop( '%', + debugger_expression_new_binaryop( '*', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "( 0x3 * 0x4 ) % 0x5", "deparse-mod-non-assoc" ); + + debugger_output_base = saved_base; + + return r; +} + diff --git a/unittests/unittests.c b/unittests/unittests.c index a3a42ddb..f82bed1d 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -1236,6 +1236,7 @@ unittests_run( void ) r += mempool_test(); r += paging_test(); r += debugger_disassemble_unittest(); + r += debugger_expression_unittest(); r += rectangle_test(); r += rectangle_realloc_test(); From f4512cba6decc9dd67c9fe7e9ded7b749c929523 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 14 Jun 2026 21:51:30 +1000 Subject: [PATCH 058/142] Add DDCB/FDCB rotate/shift disassembler tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests 226–239 cover the seven documented rotate/shift operations (RRC, RL, RR, SLA, SRA, SLL, SRL) on (IX+d) and (IY+d) via the DD CB and FD CB prefixes. Previously only RLC was tested (tests 5 and 9), leaving the remaining seven operations completely unverified. Tests 240–241 cover the undocumented LD reg,rotate (IX/IY+d) form (b < 0x40, b & 7 != 6), exercising the code path in disassemble_ddfd_cb() that was previously untested. --- debugger/disassemble.c | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 74a1471f..224f3ad7 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1180,6 +1180,30 @@ libspectrum_byte test223_data[] = { 0xee, 0x07 }; /* XOR 07 */ libspectrum_byte test224_data[] = { 0xf6, 0x07 }; /* OR 07 */ libspectrum_byte test225_data[] = { 0xfe, 0x07 }; /* CP 07 */ +/* DD CB prefix: remaining documented rotate/shift ops on (IX+d) */ +libspectrum_byte test226_data[] = { 0xdd, 0xcb, 0x55, 0x0e }; /* RRC (IX+55) */ +libspectrum_byte test227_data[] = { 0xdd, 0xcb, 0x55, 0x16 }; /* RL (IX+55) */ +libspectrum_byte test228_data[] = { 0xdd, 0xcb, 0x55, 0x1e }; /* RR (IX+55) */ +libspectrum_byte test229_data[] = { 0xdd, 0xcb, 0x55, 0x26 }; /* SLA (IX+55) */ +libspectrum_byte test230_data[] = { 0xdd, 0xcb, 0x55, 0x2e }; /* SRA (IX+55) */ +libspectrum_byte test231_data[] = { 0xdd, 0xcb, 0x55, 0x36 }; /* SLL (IX+55) */ +libspectrum_byte test232_data[] = { 0xdd, 0xcb, 0x55, 0x3e }; /* SRL (IX+55) */ + +/* FD CB prefix: remaining documented rotate/shift ops on (IY+d) */ +libspectrum_byte test233_data[] = { 0xfd, 0xcb, 0x55, 0x0e }; /* RRC (IY+55) */ +libspectrum_byte test234_data[] = { 0xfd, 0xcb, 0x55, 0x16 }; /* RL (IY+55) */ +libspectrum_byte test235_data[] = { 0xfd, 0xcb, 0x55, 0x1e }; /* RR (IY+55) */ +libspectrum_byte test236_data[] = { 0xfd, 0xcb, 0x55, 0x26 }; /* SLA (IY+55) */ +libspectrum_byte test237_data[] = { 0xfd, 0xcb, 0x55, 0x2e }; /* SRA (IY+55) */ +libspectrum_byte test238_data[] = { 0xfd, 0xcb, 0x55, 0x36 }; /* SLL (IY+55) */ +libspectrum_byte test239_data[] = { 0xfd, 0xcb, 0x55, 0x3e }; /* SRL (IY+55) */ + +/* DD CB undocumented: LD reg,rotate (IX+d) — exercises the b<0x40, b&7!=6 branch */ +libspectrum_byte test240_data[] = { 0xdd, 0xcb, 0x55, 0x00 }; /* LD B,RLC (IX+55) */ + +/* FD CB undocumented: LD reg,rotate (IY+d) */ +libspectrum_byte test241_data[] = { 0xfd, 0xcb, 0x55, 0x00 }; /* LD B,RLC (IY+55) */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1530,6 +1554,28 @@ debugger_disassemble_unittest( void ) r += run_test( test224_data, sizeof( test224_data ), "OR 07" ); r += run_test( test225_data, sizeof( test225_data ), "CP 07" ); + /* DD CB prefix: remaining documented rotate/shift ops on (IX+d) */ + r += run_test( test226_data, sizeof( test226_data ), "RRC (IX+55)" ); + r += run_test( test227_data, sizeof( test227_data ), "RL (IX+55)" ); + r += run_test( test228_data, sizeof( test228_data ), "RR (IX+55)" ); + r += run_test( test229_data, sizeof( test229_data ), "SLA (IX+55)" ); + r += run_test( test230_data, sizeof( test230_data ), "SRA (IX+55)" ); + r += run_test( test231_data, sizeof( test231_data ), "SLL (IX+55)" ); + r += run_test( test232_data, sizeof( test232_data ), "SRL (IX+55)" ); + + /* FD CB prefix: remaining documented rotate/shift ops on (IY+d) */ + r += run_test( test233_data, sizeof( test233_data ), "RRC (IY+55)" ); + r += run_test( test234_data, sizeof( test234_data ), "RL (IY+55)" ); + r += run_test( test235_data, sizeof( test235_data ), "RR (IY+55)" ); + r += run_test( test236_data, sizeof( test236_data ), "SLA (IY+55)" ); + r += run_test( test237_data, sizeof( test237_data ), "SRA (IY+55)" ); + r += run_test( test238_data, sizeof( test238_data ), "SLL (IY+55)" ); + r += run_test( test239_data, sizeof( test239_data ), "SRL (IY+55)" ); + + /* DD CB / FD CB undocumented: LD reg,rotate (IX/IY+d) */ + r += run_test( test240_data, sizeof( test240_data ), "LD B,RLC (IX+55)" ); + r += run_test( test241_data, sizeof( test241_data ), "LD B,RLC (IY+55)" ); + return r; } From 9b394c5d35554f7b598117dc97de2bc5463e5594 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:41:58 +0000 Subject: [PATCH 059/142] Merge two-pass AY tone-level loops into one The AY chip output synthesis in sound_ay_overlay() previously computed per-channel tone levels in two separate passes: 1. A first loop set tone_level[g] from the volume register entry in ay_tone_levels[]. 2. A second loop overrode tone_level[g] with the current envelope output level when bit 4 of the channel's volume register was set. Merge these into a single pass that reads sound_ay_registers[8+g] once per channel per step instead of twice. This halves the number of register-array reads in the innermost synthesis loop, which runs once every 32 T-states (~2184 iterations per 50 Hz frame, ~109 000 times per second at 50 fps). The combined expression is equivalent to the original two-loop sequence: - bit 4 clear: use ay_tone_levels[reg & 15] (volume register) - bit 4 set: use level (envelope output) --- sound.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sound.c b/sound.c index cab523ce..d2d61456 100644 --- a/sound.c +++ b/sound.c @@ -454,7 +454,7 @@ sound_ay_overlay( void ) libspectrum_dword f; struct ay_change_tag *change_ptr = ay_change; int changes_left = ay_change_count; - int reg, r; + int reg, r, ch_vol; int chan1, chan2, chan3; int last_chan1 = 0, last_chan2 = 0, last_chan3 = 0; unsigned int tone_count, noise_count; @@ -506,17 +506,15 @@ sound_ay_overlay( void ) } } - /* the tone level if no enveloping is being used */ - for( g = 0; g < AY_CHANNELS; g++ ) - tone_level[g] = ay_tone_levels[ sound_ay_registers[ 8 + g ] & 15 ]; - - /* envelope */ + /* Per-channel tone level: use the envelope output if bit 4 of the + channel's volume register is set, otherwise use the volume table. */ envshape = sound_ay_registers[13]; level = ay_tone_levels[ env_counter ]; - for( g = 0; g < AY_CHANNELS; g++ ) - if( sound_ay_registers[ 8 + g ] & 16 ) - tone_level[g] = level; + for( g = 0; g < AY_CHANNELS; g++ ) { + ch_vol = sound_ay_registers[ 8 + g ]; + tone_level[g] = ( ch_vol & 16 ) ? level : ay_tone_levels[ ch_vol & 15 ]; + } /* envelope output counter gets incr'd every 16 AY cycles. */ ay_env_cycles += AY_CLOCK_DIVISOR; From 74134a122a5a96cf24b6d26dd48c69382a2f717b Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 15 Jun 2026 20:56:30 +1000 Subject: [PATCH 060/142] Expand undocumented DDCB/FDCB LD reg,rotate test coverage Tests 240-241 covered only LD B,RLC (IX+55) and LD B,RLC (IY+55). The disassemble_ddfd_cb() undocumented path (b<0x40, b&7!=6) decodes any of six destination registers (B/C/D/E/H/L/A) and eight rotation operations (RLC/RRC/RL/RR/SLA/SRA/SLL/SRL). Add tests 242-254 to verify: - all six remaining destination registers for IX (tests 242-247) - all six remaining destination registers for IY (tests 248-253) - a non-RLC rotation op (SRL) to confirm rotate_op() selection is independent of the destination register (test254) --- debugger/disassemble.c | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 224f3ad7..1978eed4 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1204,6 +1204,25 @@ libspectrum_byte test240_data[] = { 0xdd, 0xcb, 0x55, 0x00 }; /* LD B,RLC (IX+5 /* FD CB undocumented: LD reg,rotate (IY+d) */ libspectrum_byte test241_data[] = { 0xfd, 0xcb, 0x55, 0x00 }; /* LD B,RLC (IY+55) */ +/* DD CB undocumented: remaining destination registers with RLC (IX+d) */ +libspectrum_byte test242_data[] = { 0xdd, 0xcb, 0x55, 0x01 }; /* LD C,RLC (IX+55) */ +libspectrum_byte test243_data[] = { 0xdd, 0xcb, 0x55, 0x02 }; /* LD D,RLC (IX+55) */ +libspectrum_byte test244_data[] = { 0xdd, 0xcb, 0x55, 0x03 }; /* LD E,RLC (IX+55) */ +libspectrum_byte test245_data[] = { 0xdd, 0xcb, 0x55, 0x04 }; /* LD H,RLC (IX+55) */ +libspectrum_byte test246_data[] = { 0xdd, 0xcb, 0x55, 0x05 }; /* LD L,RLC (IX+55) */ +libspectrum_byte test247_data[] = { 0xdd, 0xcb, 0x55, 0x07 }; /* LD A,RLC (IX+55) */ + +/* FD CB undocumented: remaining destination registers with RLC (IY+d) */ +libspectrum_byte test248_data[] = { 0xfd, 0xcb, 0x55, 0x01 }; /* LD C,RLC (IY+55) */ +libspectrum_byte test249_data[] = { 0xfd, 0xcb, 0x55, 0x02 }; /* LD D,RLC (IY+55) */ +libspectrum_byte test250_data[] = { 0xfd, 0xcb, 0x55, 0x03 }; /* LD E,RLC (IY+55) */ +libspectrum_byte test251_data[] = { 0xfd, 0xcb, 0x55, 0x04 }; /* LD H,RLC (IY+55) */ +libspectrum_byte test252_data[] = { 0xfd, 0xcb, 0x55, 0x05 }; /* LD L,RLC (IY+55) */ +libspectrum_byte test253_data[] = { 0xfd, 0xcb, 0x55, 0x07 }; /* LD A,RLC (IY+55) */ + +/* DD CB undocumented: non-RLC rotation op to verify rotate_op() selection */ +libspectrum_byte test254_data[] = { 0xdd, 0xcb, 0x55, 0x38 }; /* LD B,SRL (IX+55) */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1576,6 +1595,25 @@ debugger_disassemble_unittest( void ) r += run_test( test240_data, sizeof( test240_data ), "LD B,RLC (IX+55)" ); r += run_test( test241_data, sizeof( test241_data ), "LD B,RLC (IY+55)" ); + /* DD CB: remaining destination registers — C, D, E, H, L, A */ + r += run_test( test242_data, sizeof( test242_data ), "LD C,RLC (IX+55)" ); + r += run_test( test243_data, sizeof( test243_data ), "LD D,RLC (IX+55)" ); + r += run_test( test244_data, sizeof( test244_data ), "LD E,RLC (IX+55)" ); + r += run_test( test245_data, sizeof( test245_data ), "LD H,RLC (IX+55)" ); + r += run_test( test246_data, sizeof( test246_data ), "LD L,RLC (IX+55)" ); + r += run_test( test247_data, sizeof( test247_data ), "LD A,RLC (IX+55)" ); + + /* FD CB: remaining destination registers — C, D, E, H, L, A */ + r += run_test( test248_data, sizeof( test248_data ), "LD C,RLC (IY+55)" ); + r += run_test( test249_data, sizeof( test249_data ), "LD D,RLC (IY+55)" ); + r += run_test( test250_data, sizeof( test250_data ), "LD E,RLC (IY+55)" ); + r += run_test( test251_data, sizeof( test251_data ), "LD H,RLC (IY+55)" ); + r += run_test( test252_data, sizeof( test252_data ), "LD L,RLC (IY+55)" ); + r += run_test( test253_data, sizeof( test253_data ), "LD A,RLC (IY+55)" ); + + /* DD CB: verify rotation-op selection (SRL) independent of destination */ + r += run_test( test254_data, sizeof( test254_data ), "LD B,SRL (IX+55)" ); + return r; } From b25b4cafa03fcb20bd73cf84c52de5708656f8c1 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 15 Jun 2026 21:02:10 +1000 Subject: [PATCH 061/142] Guard shift operators against count >= 32 Shifting a libspectrum_dword (uint32_t) by 32 or more bits is undefined behaviour in C. On x86 the hardware masks the count to 5 bits, so '1 << 32' silently returns 1 instead of 0; on other architectures the result is unpredictable. Apply the same guard pattern used by division and modulo: evaluate the count first, return 0 immediately when count >= 32, then perform the shift. Add two unit tests (lshift-large, rshift-large) to document and enforce the new boundary behaviour. --- debugger/expression.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/debugger/expression.c b/debugger/expression.c index 6af859e3..9e508565 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -395,13 +395,17 @@ evaluate_binaryop( struct binaryop_type *binary ) return debugger_expression_evaluate( binary->op1 ) % op2; } - case DEBUGGER_TOKEN_LEFT_SHIFT: - return debugger_expression_evaluate( binary->op1 ) << - debugger_expression_evaluate( binary->op2 ); + case DEBUGGER_TOKEN_LEFT_SHIFT: { + libspectrum_dword count = debugger_expression_evaluate( binary->op2 ); + if( count >= 32 ) return 0; + return debugger_expression_evaluate( binary->op1 ) << count; + } - case DEBUGGER_TOKEN_RIGHT_SHIFT: - return debugger_expression_evaluate( binary->op1 ) >> - debugger_expression_evaluate( binary->op2 ); + case DEBUGGER_TOKEN_RIGHT_SHIFT: { + libspectrum_dword count = debugger_expression_evaluate( binary->op2 ); + if( count >= 32 ) return 0; + return debugger_expression_evaluate( binary->op1 ) >> count; + } case DEBUGGER_TOKEN_EQUAL_TO: return debugger_expression_evaluate( binary->op1 ) == @@ -794,8 +798,10 @@ debugger_expression_unittest( void ) /* Bit-shift operators */ r += eval_binary_test( DEBUGGER_TOKEN_LEFT_SHIFT, 1, 3, 8, "lshift" ); r += eval_binary_test( DEBUGGER_TOKEN_LEFT_SHIFT, 1, 7, 128, "lshift-7" ); + r += eval_binary_test( DEBUGGER_TOKEN_LEFT_SHIFT, 1, 32, 0, "lshift-large" ); r += eval_binary_test( DEBUGGER_TOKEN_RIGHT_SHIFT, 16, 2, 4, "rshift" ); r += eval_binary_test( DEBUGGER_TOKEN_RIGHT_SHIFT, 8, 3, 1, "rshift-3" ); + r += eval_binary_test( DEBUGGER_TOKEN_RIGHT_SHIFT, 1, 32, 0, "rshift-large" ); /* Bitwise operators */ r += eval_binary_test( '&', 0xF0, 0xFF, 0xF0, "bitwise-and" ); From 64e16a7c5591b09d33c9ba8f74462811a68f598d Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 15 Jun 2026 21:40:18 +1000 Subject: [PATCH 062/142] Updates for a "1.9.0" version --- ChangeLog | 59 +++++++++++++++++++ README | 4 +- configure.ac | 4 +- ...ourceforge.fuse_emulator.Fuse.metainfo.xml | 11 ++++ man/fuse.1 | 2 +- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index b609b963..94667451 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,62 @@ +2026-06-15 Fredrick Meunier + + * Fuse 1.9.0 released. + + * New features: + * Add NTSC TV and replace the PAL TV filters (derived from + snes_ntsc by Shay Green) (patch #374) (matja and Fredrick + Meunier). + + * Emulation core improvements: + * Tape traps: set the pulse level correctly after loading a ROM + block (fixes bugs #398, #401, #402 and #411) (Alberto Garcia). + * TZX handling: don't ignore the embedded pause at the end of a + block (fixes bugs #377 and #480) (Alberto Garcia). + * Release pressed keys after snapshot restore (fixes bug #77) + (thanks, jdaw1) (Fredrick Meunier). + + * UI improvements: + * GTK3: stop forcing X11 over Wayland (Alberto Garcia). + * SDL2: use layout-aware key mapping in keyboard handling + (Fredrick Meunier). + * SDL2: correct the Options-level fullscreen toggle (thanks, + Blerkotron) (Fredrick Meunier). + * SDL2: fix tape icon placement on Timex machines (Fredrick + Meunier). + * Win32: improve graphics update performance by writing directly + into window DIB (Fredrick Meunier). + + * Debugger improvements: + * Add modulo (%) and bit-shift (<<, >>) operators to the expression + evaluator (Fredrick Meunier). + + * Deprecated features removed: + * Remove support for libjsw (fixes bug #21) (thanks, Sergio + Baldoví) (Fredrick Meunier). + + * Miscellaneous improvements: + * Rework SDL detection in configure.ac (Alberto Garcia). + * Update SourceForge HTTP URLs to HTTPS (Fredrick Meunier). + * desktop file: include EXEEXT in the Exec field (Alberto Garcia). + * desktop file: set the GTK category only in GTK builds (Alberto + Garcia). + * Fix an error in the manpage (Alberto Garcia). + * Advance the ALSA buffer pointer after partial writes to improve + sound output handling (Fredrick Meunier). + + * Testing improvements: + * Add unit tests for the expression evaluator (Fredrick Meunier). + * Expand disassembler test coverage for unprefixed, CB, ED, DD/FD + and undocumented DDCB/FDCB opcodes (Fredrick Meunier). + * Add unit tests for keyboard_read and + keyboard_simulate_keypress (Fredrick Meunier). + * Add unit test coverage for SDL2 scaler selection and joystick + boundary cases (Fredrick Meunier). + * Add unit test coverage for rectangle reallocation and + MEMPOOL_UNTRACKED handling (Fredrick Meunier). + + * Various other minor bugfixes. + 2026-05-11 Fredrick Meunier * Fuse 1.8.0 released. diff --git a/README b/README index 7544fe93..fe32e406 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -The Free Unix Spectrum Emulator (Fuse) 1.8.0 +The Free Unix Spectrum Emulator (Fuse) 1.9.0 ============================================ Fuse (the Free Unix Spectrum Emulator) was originally, and somewhat @@ -116,4 +116,4 @@ Usenet newsgroup `comp.sys.sinclair' or the World of Spectrum forums . Philip Kendall -11th May, 2026 +15th June, 2026 diff --git a/configure.ac b/configure.ac index 70440efe..548c8881 100644 --- a/configure.ac +++ b/configure.ac @@ -19,11 +19,11 @@ dnl dnl E-mail: philip-fuse@shadowmagic.org.uk dnl Package version -m4_define([fuse_version], [1.8.0]) +m4_define([fuse_version], [1.9.0]) dnl Product full version m4_define([fuse_major_version], [1]) -m4_define([fuse_minor_version], [8]) +m4_define([fuse_minor_version], [9]) m4_define([fuse_micro_version], [0]) m4_define([fuse_nano_version], [0]) m4_define([fuse_full_version], [fuse_major_version.fuse_minor_version.fuse_micro_version.fuse_nano_version]) diff --git a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml index 3f657929..d09f0976 100644 --- a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml +++ b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml @@ -17,6 +17,17 @@ Emulator + + +
    +
  • Added NTSC TV output and replaced the PAL TV filters
  • +
  • GTK3 improved Wayland support
  • +
  • SDL2 switched to layout-aware keyboard mapping
  • +
  • Debugger: added modulo (%) and bit-shift (<<, >>) operators to the expression evaluator
  • +
  • Removed support for libjsw
  • +
+
+
    diff --git a/man/fuse.1 b/man/fuse.1 index fcae080e..56a70e0c 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -25,7 +25,7 @@ .\" E-mail: philip-fuse@shadowmagic.org.uk .\" .\" -.TH fuse 1 "11th May, 2026" "1.8.0" "Emulators" +.TH fuse 1 "15th June, 2026" "1.9.0" "Emulators" .\" .\"------------------------------------------------------------------ .\" From 1c9767a9ad630e7b644e1df6ef6ec55b9589672e Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 15 Jun 2026 22:04:19 +1000 Subject: [PATCH 063/142] Updates for copyright headers --- Makefile.am | 1 + data/Makefile.am | 1 + data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml | 2 +- data/shell-completion/bash/fuse | 1 + debugger/command.c | 1 + debugger/commandl.l | 1 + debugger/commandy.y | 1 + debugger/debugger.h | 1 + debugger/debugger_internals.h | 1 + debugger/disassemble.c | 1 + debugger/expression.c | 1 + display.c | 2 +- display.h | 1 + fuse.c | 2 +- hacking/Makefile.am | 1 + keyboard.c | 1 + keyboard.h | 1 + man/fuse.1 | 3 ++- menu.h | 1 + menu_data.dat | 1 + psg.c | 1 + psg.h | 1 + screenshot.c | 2 +- settings.dat | 1 + snapshot.c | 1 + sound.c | 2 +- sound/alsasound.c | 1 + tape.c | 1 + timer/timer.c | 2 +- ui/gtk3/Makefile.am | 2 ++ ui/gtk3/gtkdisplay.c | 2 ++ ui/gtk3/gtkjoystick.c | 1 + ui/gtk3/gtkui.c | 1 + ui/options.dat | 1 + ui/scaler/Makefile.am | 1 + ui/scaler/scaler.c | 2 +- ui/scaler/scaler.h | 2 +- ui/scaler/scaler_internals.h | 2 +- ui/scaler/scalers.c | 2 +- ui/sdl/sdldisplay.c | 2 +- ui/sdl/sdljoystick.c | 2 +- ui/sdl/sdlui.c | 2 +- ui/wii/wiidisplay.c | 1 + ui/win32/win32display.c | 1 + ui/win32/win32joystick.c | 1 + ui/win32/win32ui.c | 1 + ui/xlib/xdisplay.c | 1 + ui/xlib/xjoystick.c | 1 + unittests/unittests.c | 1 + 49 files changed, 52 insertions(+), 14 deletions(-) diff --git a/Makefile.am b/Makefile.am index 47b9510f..674b75d1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,6 @@ ## Process this file with automake to produce Makefile.in ## Copyright (c) 1999-2017 Philip Kendall +## Copyright (c) 2026 Fredrick Meunier ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by diff --git a/data/Makefile.am b/data/Makefile.am index c8ef07b4..0ff47a00 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -1,6 +1,7 @@ ## Process this file with automake to produce Makefile.in ## Copyright (c) 2011 Philip Kendall ## Copyright (c) 2013-2015 Sergio Baldoví +## Copyright (c) 2026 Alberto Garcia ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by diff --git a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml index d09f0976..0a1700d8 100644 --- a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml +++ b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml @@ -1,5 +1,5 @@ - + net.sourceforge.fuse_emulator.Fuse diff --git a/data/shell-completion/bash/fuse b/data/shell-completion/bash/fuse index 1ec049b9..e3e68ab7 100644 --- a/data/shell-completion/bash/fuse +++ b/data/shell-completion/bash/fuse @@ -1,6 +1,7 @@ # fuse: bash completion for fuse-emulator # Copyright (c) 2015-2023 Sergio Baldoví +# Copyright (c) 2026 Fredrick Meunier # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/debugger/command.c b/debugger/command.c index e1e849b4..391a9457 100644 --- a/debugger/command.c +++ b/debugger/command.c @@ -1,5 +1,6 @@ /* command.c: Parse a debugger command Copyright (c) 2002-2017 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/debugger/commandl.l b/debugger/commandl.l index 66066dd6..4bc2ef00 100644 --- a/debugger/commandl.l +++ b/debugger/commandl.l @@ -1,5 +1,6 @@ /* commandl.l: Debugger command lexical scanner Copyright (c) 2002-2017 Philip Kendall, Sergio Baldoví + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/debugger/commandy.y b/debugger/commandy.y index 4526d024..ab6bc9d8 100644 --- a/debugger/commandy.y +++ b/debugger/commandy.y @@ -1,6 +1,7 @@ /* commandy.y: Parse a debugger command Copyright (c) 2002-2017 Philip Kendall Copyright (c) 2015 Sergio Baldoví + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/debugger/debugger.h b/debugger/debugger.h index f40103e6..ae729527 100644 --- a/debugger/debugger.h +++ b/debugger/debugger.h @@ -1,5 +1,6 @@ /* debugger.h: Fuse's monitor/debugger Copyright (c) 2002-2018 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/debugger/debugger_internals.h b/debugger/debugger_internals.h index 6659a297..3d7a8d95 100644 --- a/debugger/debugger_internals.h +++ b/debugger/debugger_internals.h @@ -1,5 +1,6 @@ /* debugger_internals.h: The internals of Fuse's monitor/debugger Copyright (c) 2002-2016 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 1978eed4..6a033c8d 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1,6 +1,7 @@ /* disassemble.c: Fuse's disassembler Copyright (c) 2002-2018 Darren Salt, Philip Kendall Copyright (c) 2016 BogDan Vatra + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/debugger/expression.c b/debugger/expression.c index 9e508565..d2af59ac 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -1,5 +1,6 @@ /* expression.c: A numeric expression Copyright (c) 2003-2017 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/display.c b/display.c index 399cb04d..59a0414a 100644 --- a/display.c +++ b/display.c @@ -1,5 +1,5 @@ /* display.c: Routines for printing the Spectrum screen - Copyright (c) 1999-2016 Philip Kendall, Thomas Harte, Witold Filipczyk + Copyright (c) 1999-2026 Philip Kendall, Thomas Harte, Witold Filipczyk and Fredrick Meunier This program is free software; you can redistribute it and/or modify diff --git a/display.h b/display.h index 31c838de..4ef6b3ef 100644 --- a/display.h +++ b/display.h @@ -1,5 +1,6 @@ /* display.h: Routines for printing the Spectrum's screen Copyright (c) 1999-2016 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/fuse.c b/fuse.c index 1acdb0d8..f145545e 100644 --- a/fuse.c +++ b/fuse.c @@ -1,5 +1,5 @@ /* fuse.c: The Free Unix Spectrum Emulator - Copyright (c) 1999-2018 Philip Kendall and others + Copyright (c) 1999-2026 Philip Kendall and others This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/hacking/Makefile.am b/hacking/Makefile.am index 80737e29..96f32283 100644 --- a/hacking/Makefile.am +++ b/hacking/Makefile.am @@ -1,6 +1,7 @@ ## Process this file with automake to produce Makefile.in ## Copyright (c) 2002-2011 Philip Kendall ## Copyright (c) 2015 Sergio Baldoví +## Copyright (c) 2026 Fredrick Meunier ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by diff --git a/keyboard.c b/keyboard.c index eda42fa9..448e0ac0 100644 --- a/keyboard.c +++ b/keyboard.c @@ -1,5 +1,6 @@ /* keyboard.c: Routines for dealing with the Spectrum's keyboard Copyright (c) 1999-2017 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/keyboard.h b/keyboard.h index 48538c8f..ab6e2102 100644 --- a/keyboard.h +++ b/keyboard.h @@ -1,5 +1,6 @@ /* keyboard.h: Routines for dealing with the Spectrum's keyboard Copyright (c) 1999-2017 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/man/fuse.1 b/man/fuse.1 index 56a70e0c..fb3b8862 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -2,9 +2,10 @@ .\" -*- nroff -*- .\" .\" fuse.1: Fuse man page -.\" Copyright (c) 2001-2018 Russell Marks, Philip Kendall, Darren Salt, +.\" Copyright (c) 2001-2026 Russell Marks, Philip Kendall, Darren Salt, .\" Fredrick Meunier, Stuart Brady .\" Copyright (c) 2021-2026 Sergio Baldoví +.\" Copyright (c) 2026 Alberto Garcia .\" .\" This program is free software; you can redistribute it and/or modify .\" it under the terms of the GNU General Public License as published by diff --git a/menu.h b/menu.h index a1492d66..8ccb1388 100644 --- a/menu.h +++ b/menu.h @@ -1,5 +1,6 @@ /* menu.h: general menu callbacks Copyright (c) 2004-2015 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/menu_data.dat b/menu_data.dat index 55bee937..dec6e1f3 100644 --- a/menu_data.dat +++ b/menu_data.dat @@ -1,5 +1,6 @@ # menu_data.dat: Menu structure for Fuse # Copyright (c) 2004-2018 Philip Kendall, Alistair Cree +# Copyright (c) 2026 Fredrick Meunier # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/psg.c b/psg.c index 0e738b0c..3052b9c3 100644 --- a/psg.c +++ b/psg.c @@ -1,5 +1,6 @@ /* psg.c: recording AY chip output to .psg files Copyright (c) 2003-2016 Matthew Westcott, Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/psg.h b/psg.h index 276704d8..571e520d 100644 --- a/psg.h +++ b/psg.h @@ -1,5 +1,6 @@ /* psg.h: recording AY chip output to .psg files Copyright (c) 2003-2016 Matthew Westcott, Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/screenshot.c b/screenshot.c index c6e41514..ed24ff33 100644 --- a/screenshot.c +++ b/screenshot.c @@ -1,5 +1,5 @@ /* screenshot.c: Routines for handling .png and .scr screenshots - Copyright (c) 2002-2021 Philip Kendall, Fredrick Meunier + Copyright (c) 2002-2026 Philip Kendall, Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/settings.dat b/settings.dat index 4ac863b8..c7f7cd45 100644 --- a/settings.dat +++ b/settings.dat @@ -1,5 +1,6 @@ # settings.dat: configuration options for Fuse # Copyright (c) 2002-2018 Philip Kendall, Alistair Cree +# Copyright (c) 2026 Fredrick Meunier # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/snapshot.c b/snapshot.c index 55686bd0..a22d4c3c 100644 --- a/snapshot.c +++ b/snapshot.c @@ -1,5 +1,6 @@ /* snapshot.c: snapshot handling routines Copyright (c) 1999-2012 Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/sound.c b/sound.c index d2d61456..24b0bfaa 100644 --- a/sound.c +++ b/sound.c @@ -1,5 +1,5 @@ /* sound.c: Sound support - Copyright (c) 2000-2016 Russell Marks, Matan Ziv-Av, Philip Kendall, + Copyright (c) 2000-2026 Russell Marks, Matan Ziv-Av, Philip Kendall, Fredrick Meunier, Patrik Rak This program is free software; you can redistribute it and/or modify diff --git a/sound/alsasound.c b/sound/alsasound.c index ad4c2d0b..84d66a14 100644 --- a/sound/alsasound.c +++ b/sound/alsasound.c @@ -1,5 +1,6 @@ /* alsasound.c: ALSA (Linux) sound I/O Copyright (c) 2006-2008 Gergely Szasz + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/tape.c b/tape.c index 008a554d..f1eb7586 100644 --- a/tape.c +++ b/tape.c @@ -2,6 +2,7 @@ Copyright (c) 1999-2017 Philip Kendall, Darren Salt, Witold Filipczyk Copyright (c) 2015-2018 UB880D Copyright (c) 2016-2021 Fredrick Meunier + Copyright (c) 2026 Alberto Garcia This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/timer/timer.c b/timer/timer.c index 3f1b108c..c432adf6 100644 --- a/timer/timer.c +++ b/timer/timer.c @@ -1,5 +1,5 @@ /* timer.c: Speed routines for Fuse - Copyright (c) 1999-2024 Philip Kendall, Marek Januszewski, Fredrick Meunier + Copyright (c) 1999-2026 Philip Kendall, Marek Januszewski, Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/gtk3/Makefile.am b/ui/gtk3/Makefile.am index bf98a997..57e75a90 100644 --- a/ui/gtk3/Makefile.am +++ b/ui/gtk3/Makefile.am @@ -1,6 +1,8 @@ ## Process this file with automake to produce Makefile.in ## Copyright (c) 2001-2016 Philip Kendall ## Copyright (c) 2015-2018 Sergio Baldoví +## Copyright (c) 2026 Alberto Garcia +## Copyright (c) 2026 Fredrick Meunier ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 6c6a70db..6566c386 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -1,5 +1,7 @@ /* gtkdisplay.c: GTK routines for dealing with the Speccy screen Copyright (c) 2000-2018 Philip Kendall + Copyright (c) 2026 Alberto Garcia + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/gtk3/gtkjoystick.c b/ui/gtk3/gtkjoystick.c index da303289..4ddf87af 100644 --- a/ui/gtk3/gtkjoystick.c +++ b/ui/gtk3/gtkjoystick.c @@ -1,5 +1,6 @@ /* gtkjoystick.c: Joystick emulation Copyright (c) 2003-2011 Darren Salt, Philip Kendall + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/gtk3/gtkui.c b/ui/gtk3/gtkui.c index 0ab736f3..30097c98 100644 --- a/ui/gtk3/gtkui.c +++ b/ui/gtk3/gtkui.c @@ -1,5 +1,6 @@ /* gtkui.c: GTK routines for dealing with the user interface Copyright (c) 2000-2021 Philip Kendall, Russell Marks, Sergio Baldoví + Copyright (c) 2026 Alberto Garcia This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/options.dat b/ui/options.dat index 1199be15..cfe61bdb 100644 --- a/ui/options.dat +++ b/ui/options.dat @@ -1,5 +1,6 @@ # options.dat: Data file for options dialog boxes # Copyright (c) 2001-2023 Philip Kendall, Marek Januszewski, Stuart Brady +# Copyright (c) 2026 Fredrick Meunier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/ui/scaler/Makefile.am b/ui/scaler/Makefile.am index 5ded8ca5..ec0db4d2 100644 --- a/ui/scaler/Makefile.am +++ b/ui/scaler/Makefile.am @@ -1,6 +1,7 @@ ## Process this file with automake to produce Makefile.in ## Copyright (c) 2001 Philip Kendall ## Copyright (c) 2015 Sergio Baldoví +## Copyright (c) 2026 Fredrick Meunier ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by diff --git a/ui/scaler/scaler.c b/ui/scaler/scaler.c index 3dfb800c..00621f77 100644 --- a/ui/scaler/scaler.c +++ b/ui/scaler/scaler.c @@ -1,5 +1,5 @@ /* scaler.c: code for selecting (etc) scalers - * Copyright (C) 2003-2021 Fredrick Meunier, Philip Kendall + * Copyright (C) 2003-2026 Fredrick Meunier, Philip Kendall * Copyright (c) 2015-2019 Sergio Baldoví * * Originally taken from ScummVM - Scumm Interpreter diff --git a/ui/scaler/scaler.h b/ui/scaler/scaler.h index 36702dcd..9100d966 100644 --- a/ui/scaler/scaler.h +++ b/ui/scaler/scaler.h @@ -1,5 +1,5 @@ /* ScummVM - Scumm Interpreter - * Copyright (C) 2002-2021 The ScummVM project, Fredrick Meunier and + * Copyright (C) 2002-2026 The ScummVM project, Fredrick Meunier and * Philip Kendall * * HQ2x and HQ3x scalers taken from HiEnd3D Demos (http://www.hiend3d.com) diff --git a/ui/scaler/scaler_internals.h b/ui/scaler/scaler_internals.h index 5cce8c03..e95ade2c 100644 --- a/ui/scaler/scaler_internals.h +++ b/ui/scaler/scaler_internals.h @@ -1,5 +1,5 @@ /* scaler_internals.h: functions internal to the scaler code - Copyright (c) 2003 Fredrick Meunier, Philip Kendall + Copyright (c) 2003,2026 Fredrick Meunier, Philip Kendall Originally taken from ScummVM - Scumm Interpreter Copyright (C) 2001 Ludvig Strigeus diff --git a/ui/scaler/scalers.c b/ui/scaler/scalers.c index 242ecbc8..f4ab04fb 100644 --- a/ui/scaler/scalers.c +++ b/ui/scaler/scalers.c @@ -1,5 +1,5 @@ /* scalers.c: the actual graphics scalers - * Copyright (C) 2003-2021 Fredrick Meunier, Philip Kendall, Gergely Szasz + * Copyright (C) 2003-2026 Fredrick Meunier, Philip Kendall, Gergely Szasz * * Originally taken from ScummVM - Scumm Interpreter * Copyright (C) 2001 Ludvig Strigeus diff --git a/ui/sdl/sdldisplay.c b/ui/sdl/sdldisplay.c index bd870331..2149f95c 100644 --- a/ui/sdl/sdldisplay.c +++ b/ui/sdl/sdldisplay.c @@ -1,5 +1,5 @@ /* sdldisplay.c: Routines for dealing with the SDL display - Copyright (c) 2000-2021 Philip Kendall, Matan Ziv-Av, Fredrick Meunier + Copyright (c) 2000-2026 Philip Kendall, Matan Ziv-Av, Fredrick Meunier Copyright (c) 2015 Adrien Destugues This program is free software; you can redistribute it and/or modify diff --git a/ui/sdl/sdljoystick.c b/ui/sdl/sdljoystick.c index 993e9fde..08e2d718 100644 --- a/ui/sdl/sdljoystick.c +++ b/ui/sdl/sdljoystick.c @@ -1,5 +1,5 @@ /* sdljoystick.c: routines for dealing with the SDL joystick - Copyright (c) 2003-2021 Darren Salt, Fredrick Meunier, Philip Kendall + Copyright (c) 2003-2026 Darren Salt, Fredrick Meunier, Philip Kendall Copyright (c) 2015 UB880D This program is free software; you can redistribute it and/or modify diff --git a/ui/sdl/sdlui.c b/ui/sdl/sdlui.c index ab130813..3d2358ec 100644 --- a/ui/sdl/sdlui.c +++ b/ui/sdl/sdlui.c @@ -1,5 +1,5 @@ /* sdlui.c: Routines for dealing with the SDL user interface - Copyright (c) 2000-2021 Philip Kendall, Matan Ziv-Av, Fredrick Meunier + Copyright (c) 2000-2026 Philip Kendall, Matan Ziv-Av, Fredrick Meunier Copyright (c) 2015 Stuart Brady This program is free software; you can redistribute it and/or modify diff --git a/ui/wii/wiidisplay.c b/ui/wii/wiidisplay.c index 2c0f6e26..d09a3bd4 100644 --- a/ui/wii/wiidisplay.c +++ b/ui/wii/wiidisplay.c @@ -1,5 +1,6 @@ /* wiidisplay.c: Routines for dealing with the Wii's framebuffer display Copyright (c) 2008-2009 Bjoern Giesler, Marek Januszewski + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/win32/win32display.c b/ui/win32/win32display.c index e09869b9..622cb38b 100644 --- a/ui/win32/win32display.c +++ b/ui/win32/win32display.c @@ -1,5 +1,6 @@ /* win32display.c: Routines for dealing with the Win32 GDI display Copyright (c) 2003-2011 Philip Kendall, Marek Januszewski, Stuart Brady + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/win32/win32joystick.c b/ui/win32/win32joystick.c index 781520bf..b7a31721 100644 --- a/ui/win32/win32joystick.c +++ b/ui/win32/win32joystick.c @@ -1,6 +1,7 @@ /* win32joystick.c: Joystick emulation Copyright (c) 2003-2011 Darren Salt, Philip Kendall, Marek Januszewski Copyright (c) 2015 UB880D + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/win32/win32ui.c b/ui/win32/win32ui.c index 122d48ec..6497b009 100755 --- a/ui/win32/win32ui.c +++ b/ui/win32/win32ui.c @@ -2,6 +2,7 @@ Copyright (c) 2003-2015 Marek Januszewski, Philip Kendall, Stuart Brady Copyright (c) 2015 Kirben Copyright (c) 2016 lordhoto + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/xlib/xdisplay.c b/ui/xlib/xdisplay.c index a7ece696..7318c69c 100644 --- a/ui/xlib/xdisplay.c +++ b/ui/xlib/xdisplay.c @@ -2,6 +2,7 @@ Copyright (c) 2000-2021 Philip Kendall, Darren Salt, Gergely Sz�sz Copyright (c) 2015 Stuart Brady Copyright (c) 2015-2024 Sergio Baldov� + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/ui/xlib/xjoystick.c b/ui/xlib/xjoystick.c index c10694f5..0e82d8ad 100644 --- a/ui/xlib/xjoystick.c +++ b/ui/xlib/xjoystick.c @@ -1,5 +1,6 @@ /* xjoystick.c: Joystick emulation Copyright (c) 2003 Darren Salt + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/unittests/unittests.c b/unittests/unittests.c index f82bed1d..67979978 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -1,6 +1,7 @@ /* unittests.c: unit testing framework for Fuse Copyright (c) 2008-2018 Philip Kendall Copyright (c) 2015 Stuart Brady + Copyright (c) 2026 Fredrick Meunier This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From 929737312e776edd70712cdbffd23ea4c092c196 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 15 Jun 2026 22:12:41 +1000 Subject: [PATCH 064/142] Add hacking/releasing.md --- hacking/releasing.md | 99 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 hacking/releasing.md diff --git a/hacking/releasing.md b/hacking/releasing.md new file mode 100644 index 00000000..f9b39219 --- /dev/null +++ b/hacking/releasing.md @@ -0,0 +1,99 @@ +# Releasing Fuse + +## Overview + +This document describes the manual steps used to prepare a Fuse release. +The `update-release.py` helper updates release metadata in the main +documentation files and in `configure.ac`, but it does not perform the +full release process by itself. + +## Release metadata helper + +The repository includes `update-release.py` in the top-level directory. + +### Usage + +Preview changes: + +```sh +./update-release.py --date "12th June, 2026" --version "1.8.1" --dry-run +``` + +Update the release date only: + +```sh +./update-release.py --date "12th June, 2026" +``` + +Update both release date and version: + +```sh +./update-release.py --date "12th June, 2026" --version "1.8.1" +``` + +### Files updated + +When given `--date`, the script updates: + +- the `.TH` line in `man/fuse.1` +- the final date line in `README` + +When given `--version`, it also updates: + +- the top version line in `README` +- the version definitions in `configure.ac` + +### Files not updated + +The script does not update: + +- `ChangeLog` +- generated files +- release tarballs, tags, or announcements + +## Optional agent skill for ChangeLog updates + +The repository also includes an optional maintainer skill at +`.agents/skills/update-changelog/SKILL.md`. If your agent environment +supports repository skills, it can use that skill to draft a new release +entry for `ChangeLog` in the existing project format. + +The skill is a convenience helper only. It is not required to build or +use Fuse. If it is unavailable, update `ChangeLog` manually in the +existing style. + +## Typical release workflow + +1. Update `ChangeLog`. If your agent environment supports repository + skills, use `.agents/skills/update-changelog/SKILL.md` to draft the + new top-of-file release entry, review it, and then apply it after + confirmation. Otherwise, update `ChangeLog` manually in the existing + format. +2. Run `update-release.py` with `--dry-run`. +3. Run it again without `--dry-run` once the changes look correct. +4. Review the resulting diff. +5. If `configure.ac` changed, regenerate the build system as needed. +6. Build the project. +7. Run the test suite. +8. Create the release tarball, tag, and publish using the normal + maintainer process. + +## Verification + +After updating release metadata, verify with: + +```sh +./configure +make +make check +``` + +Also inspect the diff manually to confirm the release date and version +were updated in the expected files. + +## Caveats + +- The date should be passed in the format used by the existing docs, for + example `12th June, 2026`. +- The script is intended as a maintainer convenience tool. +- Review all changes before creating a release. From b72c83e85786f5f84375d42c104952cc5453bbaa Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 15 Jun 2026 22:14:42 +1000 Subject: [PATCH 065/142] Remove reference to ui/scaler/snes_ntsc.txt --- ui/scaler/Makefile.am | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/scaler/Makefile.am b/ui/scaler/Makefile.am index ec0db4d2..a7bfd764 100644 --- a/ui/scaler/Makefile.am +++ b/ui/scaler/Makefile.am @@ -49,8 +49,7 @@ EXTRA_DIST += \ ui/scaler/scalers.c \ ui/scaler/scaler_hq2x.c \ ui/scaler/scaler_hq3x.c \ - ui/scaler/scaler_hq4x.c \ - ui/scaler/snes_ntsc.txt + ui/scaler/scaler_hq4x.c CLEANFILES += \ ui/scaler/scalers16.o \ From e9dddc2fa5e45b1188aab85bbe9912b8a774ccaa Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Fri, 29 May 2026 00:57:33 +0200 Subject: [PATCH 066/142] gtk3: Scale the Spectrum display to fit the window Scale the cairo surface to fit the drawing area while preserving the aspect ratio. The surface is still rendered using the best integer scaler size, and cairo only handles the fractional remainder. On Wayland, where the window can be resized freely, this makes Fuse use as much of the drawing area as the aspect ratio allows. On X11, where the window is resized in discrete steps, the cairo scaling factor is always 1.0 so the result is unchanged. (part of feature request #135) --- ui/gtk3/gtkdisplay.c | 73 +++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 6566c386..4d8f4675 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -25,6 +25,7 @@ #include "config.h" +#include #include #include #include @@ -385,15 +386,45 @@ uidisplay_area( int x, int y, int w, int h ) gtkdisplay_area( scaled_x, scaled_y, w, h ); } +/* Map the cairo surface onto the drawing area. + 'scale' is set so the contents fit the drawing area. + 'offset_x' and 'offset_y' are set so the contents are centred. */ +static void +get_surface_placement( double *scale, int *offset_x, int *offset_y ) +{ + int surface_width, surface_height, widget_width, widget_height; + double scale_x, scale_y, s; + + surface_width = cairo_image_surface_get_width( surface ); + surface_height = cairo_image_surface_get_height( surface ); + widget_width = gtk_widget_get_allocated_width( gtkui_drawing_area ); + widget_height = gtk_widget_get_allocated_height( gtkui_drawing_area ); + + scale_x = (double)widget_width / surface_width; + scale_y = (double)widget_height / surface_height; + s = scale_x < scale_y ? scale_x : scale_y; + + *offset_x = ( widget_width - (int)( surface_width * s ) ) / 2; + *offset_y = ( widget_height - (int)( surface_height * s ) ) / 2; + *scale = s; +} + static void gtkdisplay_area(int x, int y, int width, int height) { - int max_width, max_height, widget_width, widget_height; + int max_width, max_height; int offset_x, offset_y; + int wx, wy, ww, wh; + double scale; if( width <= 0 || height <= 0 ) return; - max_width = surface ? cairo_image_surface_get_width( surface ) : width; - max_height = surface ? cairo_image_surface_get_height( surface ) : height; + if( !surface ) { + gtk_widget_queue_draw_area( gtkui_drawing_area, x, y, width, height ); + return; + } + + max_width = cairo_image_surface_get_width( surface ); + max_height = cairo_image_surface_get_height( surface ); /* Expand the invalidated area slightly to avoid thin seams on scaled GTK redraws where Cairo clips right on a dirty-rect edge. */ @@ -402,15 +433,16 @@ static void gtkdisplay_area(int x, int y, int width, int height) if( x + width < max_width ) width++; if( y + height < max_height ) height++; - /* Translate surface coordinates into widget coordinates. The surface is - centred when the drawing area is larger than the cairo surface. */ - widget_width = gtk_widget_get_allocated_width( gtkui_drawing_area ); - widget_height = gtk_widget_get_allocated_height( gtkui_drawing_area ); - offset_x = ( widget_width - max_width ) / 2; - offset_y = ( widget_height - max_height ) / 2; + /* Map the surface onto the drawing area */ + get_surface_placement( &scale, &offset_x, &offset_y ); - gtk_widget_queue_draw_area( gtkui_drawing_area, x + offset_x, y + offset_y, - width, height ); + /* Adjust the values according to the scale factor */ + wx = (int)( x * scale ) + offset_x; + wy = (int)( y * scale ) + offset_y; + ww = (int)( ceil( ( x + width ) * scale ) ) - (int)( x * scale ); + wh = (int)( ceil( ( y + height ) * scale ) ) - (int)( y * scale ); + + gtk_widget_queue_draw_area( gtkui_drawing_area, wx, wy, ww, wh ); } int @@ -522,22 +554,17 @@ uidisplay_plot16( int x, int y, libspectrum_word data, /* Called by gtkui_drawing_area on "draw" event */ static gboolean -gtkdisplay_draw( GtkWidget *widget, cairo_t *cr, gpointer user_data ) +gtkdisplay_draw( GtkWidget *widget GCC_UNUSED, cairo_t *cr, + gpointer user_data ) { - int surface_width, surface_height, widget_width, widget_height; int offset_x, offset_y; + double scale; /* Create a new surface for this gfx mode */ if( !surface ) ensure_appropriate_surface(); - /* Centre the surface in the drawing area. */ - surface_width = cairo_image_surface_get_width( surface ); - surface_height = cairo_image_surface_get_height( surface ); - widget_width = gtk_widget_get_allocated_width( widget ); - widget_height = gtk_widget_get_allocated_height( widget ); - - offset_x = ( widget_width - surface_width ) / 2; - offset_y = ( widget_height - surface_height ) / 2; + /* Map the surface onto the drawing area */ + get_surface_placement( &scale, &offset_x, &offset_y ); /* Fill the drawing area with black. This clears the margins around the source if the drawing area is larger */ @@ -545,7 +572,9 @@ gtkdisplay_draw( GtkWidget *widget, cairo_t *cr, gpointer user_data ) cairo_paint( cr ); /* Repaint the surface on top */ - cairo_set_source_surface( cr, surface, offset_x, offset_y ); + cairo_translate( cr, offset_x, offset_y ); + cairo_scale( cr, scale, scale ); + cairo_set_source_surface( cr, surface, 0, 0 ); cairo_paint( cr ); return FALSE; From 134d884a1233077fd954ac32dae9b72e22c95a2d Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sun, 31 May 2026 23:16:40 +0200 Subject: [PATCH 067/142] gtk3: Allow the window to grow beyond MAX_SCALE We no longer need to limit the window size now that the cairo surface is scaled to fit the drawing area. The scalers are still used normally, and the surface is limited to MAX_SCALE. Anything beyond that is scaled by cairo. (part of feature request #135) --- ui/gtk3/gtkdisplay.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 4d8f4675..a1852c37 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -249,9 +249,6 @@ drawing_area_resize( int width, int height, int force_scaler ) if( size > height / DISPLAY_SCREEN_HEIGHT ) size = height / DISPLAY_SCREEN_HEIGHT; - /* Clamp the size of the cairo surface. Window managers can ignore - GDK_HINT_MAX_SIZE when tiling or maximizing the window, which - would otherwise limit us to MAX_SCALE. */ if( size > MAX_SCALE ) size = MAX_SCALE; if( size < 1 ) size = 1; @@ -604,8 +601,7 @@ gtkdisplay_update_geometry( void ) scale = scaler_get_scaling_factor( current_scaler ); - hints = GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE | - GDK_HINT_BASE_SIZE | GDK_HINT_RESIZE_INC; + hints = GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE | GDK_HINT_RESIZE_INC; /* Since GTK 3.20 it is intended that gtk_window_set_geometry_hints don't set geometry of widgets. See [bugs:#344] */ @@ -631,8 +627,6 @@ gtkdisplay_update_geometry( void ) geometry.min_width = DISPLAY_ASPECT_WIDTH; geometry.min_height = DISPLAY_SCREEN_HEIGHT + extra_height; - geometry.max_width = MAX_SCALE * DISPLAY_ASPECT_WIDTH; - geometry.max_height = MAX_SCALE * DISPLAY_SCREEN_HEIGHT + extra_height; geometry.base_width = scale * image_width; geometry.base_height = scale * image_height + extra_height; geometry.width_inc = DISPLAY_ASPECT_WIDTH; From fc725bc0e4a8e767c8d81ce82109e8edd094e8f4 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sun, 31 May 2026 23:16:57 +0200 Subject: [PATCH 068/142] gtk3: Allow the user to resize the window freely on X11 Now that Fuse can scale the contents of the Spectrum display to match the actual window size there's no need to restrict the X11 backend to allow only discrete steps. Stop using the BASE_SIZE and RESIZE_INC hints because they are no longer needed. (part of feature request #135) --- ui/gtk3/gtkdisplay.c | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index a1852c37..327c0c51 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -601,7 +601,7 @@ gtkdisplay_update_geometry( void ) scale = scaler_get_scaling_factor( current_scaler ); - hints = GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE | GDK_HINT_RESIZE_INC; + hints = GDK_HINT_MIN_SIZE; /* Since GTK 3.20 it is intended that gtk_window_set_geometry_hints don't set geometry of widgets. See [bugs:#344] */ @@ -615,22 +615,8 @@ gtkdisplay_update_geometry( void ) extra_height += gtkstatusbar_get_height(); } -#ifdef GDK_WINDOWING_WAYLAND - /* We don't calculate the window size enough accurately on wayland - backend to force the window geometry (bug #367) */ - GdkDisplay *display = gdk_display_get_default(); - - if( GDK_IS_WAYLAND_DISPLAY( display ) ) { - hints &= ~GDK_HINT_RESIZE_INC; - } -#endif /* #ifdef GDK_WINDOWING_WAYLAND */ - geometry.min_width = DISPLAY_ASPECT_WIDTH; geometry.min_height = DISPLAY_SCREEN_HEIGHT + extra_height; - geometry.base_width = scale * image_width; - geometry.base_height = scale * image_height + extra_height; - geometry.width_inc = DISPLAY_ASPECT_WIDTH; - geometry.height_inc = DISPLAY_SCREEN_HEIGHT; if( settings_current.aspect_hint ) { hints |= GDK_HINT_ASPECT; From e5b0e170dc13035b930a8655931c66b629773a36 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 9 Jun 2026 16:11:18 +0200 Subject: [PATCH 069/142] gtk3: Toggle fullscreen mode with F11 Add an F11 handler so fullscreen can be toggled from within Fuse, without relying on a window manager shortcut. (part of feature request #135) --- ui/gtk3/gtkinternals.h | 2 ++ ui/gtk3/gtkkeyboard.c | 5 +++++ ui/gtk3/gtkui.c | 12 ++++++++++++ 3 files changed, 19 insertions(+) diff --git a/ui/gtk3/gtkinternals.h b/ui/gtk3/gtkinternals.h index bf4357d4..cf2352aa 100644 --- a/ui/gtk3/gtkinternals.h +++ b/ui/gtk3/gtkinternals.h @@ -68,6 +68,8 @@ int gtkui_picture( const char *filename, int border ); extern void gtkui_popup_menu(void); +void gtkui_fullscreen_toggle( void ); + GtkAccelGroup* gtkstock_add_accel_group( GtkWidget *widget ); /* Set modifier=0 to use the first default accel key. diff --git a/ui/gtk3/gtkkeyboard.c b/ui/gtk3/gtkkeyboard.c index da7567ea..cb2fe89c 100644 --- a/ui/gtk3/gtkkeyboard.c +++ b/ui/gtk3/gtkkeyboard.c @@ -85,6 +85,11 @@ gtkkeyboard_keypress( GtkWidget *widget GCC_UNUSED, GdkEvent *event, if( event->key.keyval == GDK_KEY_F1 && event->key.state == 0 ) ui_mouse_suspend(); + if( event->key.keyval == GDK_KEY_F11 && event->key.state == 0 ) { + gtkui_fullscreen_toggle(); + return TRUE; + } + fuse_event.type = INPUT_EVENT_KEYPRESS; get_keysyms( &fuse_event, event->key.hardware_keycode, event->key.keyval, event->key.group ); diff --git a/ui/gtk3/gtkui.c b/ui/gtk3/gtkui.c index 30097c98..e512948f 100644 --- a/ui/gtk3/gtkui.c +++ b/ui/gtk3/gtkui.c @@ -368,6 +368,18 @@ gtkui_gain_focus( GtkWidget *widget GCC_UNUSED, return TRUE; } +void +gtkui_fullscreen_toggle( void ) +{ + GdkWindow *window = gtk_widget_get_window( gtkui_window ); + + if( window && ( gdk_window_get_state( window ) & GDK_WINDOW_STATE_FULLSCREEN ) ) { + gtk_window_unfullscreen( GTK_WINDOW( gtkui_window ) ); + } else { + gtk_window_fullscreen( GTK_WINDOW( gtkui_window ) ); + } +} + /* Called by the main window on a "delete-event" */ static gboolean gtkui_delete( GtkWidget *widget GCC_UNUSED, GdkEvent *event GCC_UNUSED, From 1215cf18f64b595c5568dc57ed4f61b73af7fb93 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 9 Jun 2026 17:06:55 +0200 Subject: [PATCH 070/142] gtk3: Don't set the window aspect hint Now that the window can be resized freely there's no need to set the aspect hint, since Fuse guarantees that the Spectrum display will be displayed with the correct aspect ratio regardless of the window size. Removing the hint also fixes issues when coming back fullscreen with the Wayland backend. The --aspect-hint command line option and setting is kept for the Xlib UI, but --strict-aspect-hint is removed since the GTK UI was the only one using it. (part of feature request #135) --- data/shell-completion/bash/fuse | 4 ++-- man/fuse.1 | 24 +++--------------------- settings.dat | 1 - ui/gtk3/gtkdisplay.c | 16 ---------------- 4 files changed, 5 insertions(+), 40 deletions(-) diff --git a/data/shell-completion/bash/fuse b/data/shell-completion/bash/fuse index e3e68ab7..2e1e2809 100644 --- a/data/shell-completion/bash/fuse +++ b/data/shell-completion/bash/fuse @@ -255,7 +255,7 @@ _fuse() --no-rzx-autosaves --no-simpleide --no-slt --no-sound --no-sound-force-8bit --no-speccyboot --no-specdrum --no-spectranet --no-spectranet-disable --no-statusbar - --no-strict-aspect-hint --no-traps --no-ttx2000s --no-unittests + --no-traps --no-ttx2000s --no-unittests --no-usource --no-uspeech --no-writable-roms --no-zxatasp --no-zxatasp-upload --no-zxatasp-write-protect --no-zxcf --no-zxcf-upload --no-zxmmc @@ -284,7 +284,7 @@ _fuse() --snapshot --snet --sound --sound-device --sound-force-8bit --sound-freq --speaker-type --speccyboot --speccyboot-tap --specdrum --spectranet --spectranet-disable --speed - --statusbar --strict-aspect-hint --tape + --statusbar --tape --teletext-addr-1 --teletext-addr-2 --teletext-addr-3 --teletext-addr-4 --teletext-port-1 --teletext-port-2 --teletext-port-3 --teletext-port-4 --textfile --traps --ttx2000s diff --git a/man/fuse.1 b/man/fuse.1 index fb3b8862..001e5bbb 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -99,17 +99,12 @@ option. .PP .B \-\-aspect\-hint .RS -Specify whether the GTK and Xlib user interfaces should `hint' to the +Specify whether the Xlib user interface should `hint' to the window manager about the preferred aspect ratio for the graphics window, thus preventing resizing to non-square sizes which lead to -Fuse not displaying correctly. This option has been observed to cause -problems with some window managers when using the GTK UI which can -prevent the window from being resized or moved at all. (Enabled by -default, but you can use +Fuse not displaying correctly. (Enabled by default, but you can use .RB ` \-\-no\-aspect\-hint ' -to disable). See also the -.RB ` \-\-strict\-aspect\-hint ' -option. +to disable). .RE .PP .B \-\-autosave\-settings @@ -1873,19 +1868,6 @@ being accessed. Same as the General Options dialog's option. .RE .PP -.B \-\-strict\-aspect\-hint -.RS -For the GTK UI, use stricter limits for the aspect ratio limits set -by the -.RB ` \-\-aspect\-hint ' -option. This can cause some window managers (for example, -.IR metacity (1)) -to not allow the window to be resized and moved, but is necessary to -prevent others (for example, -.IR fvwm (1)) -from being able resize the window away from square. -.RE -.PP .B \-t .I file .br diff --git a/settings.dat b/settings.dat index c7f7cd45..a18d283c 100644 --- a/settings.dat +++ b/settings.dat @@ -200,7 +200,6 @@ printer_graphics_filename, string, "printout.pbm",, graphicsfile printer_text_filename, string, "printout.txt",, textfile aspect_hint, boolean, 1 -strict_aspect_hint, boolean, 0 fb_mode, numeric, 320, 'v', fbmode svga_modes, null, 0 sdl_fullscreen_mode, string, NULL diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 327c0c51..d278431f 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -595,12 +595,9 @@ gtkdisplay_update_geometry( void ) GdkGeometry geometry; GdkWindowHints hints; GtkWidget *geometry_widget; - float scale; if( !scalers_registered ) return; - scale = scaler_get_scaling_factor( current_scaler ); - hints = GDK_HINT_MIN_SIZE; /* Since GTK 3.20 it is intended that gtk_window_set_geometry_hints @@ -618,19 +615,6 @@ gtkdisplay_update_geometry( void ) geometry.min_width = DISPLAY_ASPECT_WIDTH; geometry.min_height = DISPLAY_SCREEN_HEIGHT + extra_height; - if( settings_current.aspect_hint ) { - hints |= GDK_HINT_ASPECT; - - geometry.min_aspect = geometry.max_aspect = - ( scale * DISPLAY_ASPECT_WIDTH ) / - ( scale * DISPLAY_SCREEN_HEIGHT + extra_height ); - - if( !settings_current.strict_aspect_hint ) { - geometry.min_aspect *= 0.9; - geometry.max_aspect *= 1.125; - } - } - gtk_window_set_geometry_hints( GTK_WINDOW( gtkui_window ), geometry_widget, &geometry, hints ); From 38b98d446358783e8df3710e63e52a7c92417364 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sun, 14 Jun 2026 19:14:15 +0200 Subject: [PATCH 071/142] gtk3: Don't switch scalers when the window is resized Traditionally the Fuse window could only be resized in discrete steps, and each time that happened a new scaler was calculated to match the new window size. With the introduction of freely resizable windows and fractional scaling the Spectrum display is continually adjusted to match the new window size, so we don't really have to look for a new scaler. This has two advantages: - The scaler that the user selected is always preserved. Until now, a TV 2x scaler would turn into a normal 3x scaler if the window was enlarged, losing the TV filter in the process. This also happens if the window is put in fullscreen mode. - Without this change, changing the scaler while the window is being manually resized causes the screen to blink, resulting in an unpleasant visual artifact. The only downside of preserving the scaler is that if Fuse is opened with for example '-g normal' and then maximized then you get the fullscreen version of the 1x display (maximized by cairo) instead of the 4x version. This results in a blurrier image. (part of feature request #135) --- ui/gtk3/gtkdisplay.c | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index d278431f..dd6dd775 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -125,9 +125,6 @@ static void gtkdisplay_load_gfx_mode( void ); static gboolean gtkdisplay_draw( GtkWidget *widget, cairo_t *cr, gpointer user_data ); -static gint drawing_area_resize_callback( GtkWidget *widget, GdkEvent *event, - gpointer data ); - static int init_colours( colour_format_t format ) { @@ -191,9 +188,6 @@ uidisplay_init( int width, int height ) colour_format = FORMAT_x8r8g8b8; - g_signal_connect( G_OBJECT( gtkui_window ), "configure_event", - G_CALLBACK( drawing_area_resize_callback ), NULL ); - error = init_colours( colour_format ); if( error ) return error; error = scaler_select_bitformat( BITFORMAT_X8R8G8B8 ); if( error ) return error; @@ -577,18 +571,6 @@ gtkdisplay_draw( GtkWidget *widget GCC_UNUSED, cairo_t *cr, return FALSE; } -/* Called by gtkui_window on "configure_event". - On GTK 3 the window determines the size of the drawing area */ -static gint -drawing_area_resize_callback( GtkWidget *widget GCC_UNUSED, GdkEvent *event, - gpointer data GCC_UNUSED ) -{ - drawing_area_resize( event->configure.width, - event->configure.height - extra_height, 1 ); - - return FALSE; -} - void gtkdisplay_update_geometry( void ) { @@ -624,13 +606,19 @@ static void gtkdisplay_load_gfx_mode( void ) { float scale; + int surface_width, surface_height; scale = scaler_get_scaling_factor( current_scaler ); gtkdisplay_update_geometry(); - gtk_window_resize( GTK_WINDOW( gtkui_window ), scale * image_width, - scale * image_height + extra_height ); + /* Rebuild the cairo surface for the new scaler */ + surface_width = scale * image_width; + surface_height = scale * image_height; + drawing_area_resize( surface_width, surface_height, 0 ); + + gtk_window_resize( GTK_WINDOW( gtkui_window ), surface_width, + surface_height + extra_height ); /* Redraw the entire screen... */ display_refresh_all(); From 257a1bd790042e7f42c7ad1167eed3c02b63fe09 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Sun, 14 Jun 2026 19:31:27 +0200 Subject: [PATCH 072/142] gtk3: Remove the force_scaler parameter from register_scalers() After the last commit, this function only rebuilds the list of available scalers for the current machine and reactivates the current scaler, or falls back to normal. This means that we can get rid of the force_scaler parameter and the related code and variables that are now dead. And because of that there's also no need to call register_scalers() just to rebuild the cairo surface. (part of feature request #135) --- ui/gtk3/gtkdisplay.c | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index dd6dd775..c8f1a447 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -117,7 +117,7 @@ static int extra_height = 0; static int init_colours( colour_format_t format ); static void gtkdisplay_area(int x, int y, int width, int height); -static void register_scalers( int force_scaler ); +static void register_scalers( void ); static void gtkdisplay_load_gfx_mode( void ); /* Callbacks */ @@ -201,7 +201,7 @@ uidisplay_init( int width, int height ) image_width = width; image_height = height; image_scale = width / DISPLAY_ASPECT_WIDTH; - register_scalers( 0 ); + register_scalers(); display_refresh_all(); @@ -235,7 +235,7 @@ ensure_appropriate_surface( void ) } static int -drawing_area_resize( int width, int height, int force_scaler ) +drawing_area_resize( int width, int height ) { int size; @@ -251,8 +251,6 @@ drawing_area_resize( int width, int height, int force_scaler ) gtkdisplay_surface_size = size; - register_scalers( force_scaler ); - memset( scaled_image, 0, sizeof( scaled_image ) ); ensure_appropriate_surface(); @@ -263,10 +261,9 @@ drawing_area_resize( int width, int height, int force_scaler ) } static void -register_scalers( int force_scaler ) +register_scalers( void ) { scaler_type scaler; - float surface_scale, scaling_factor; scaler_register_clear(); @@ -304,26 +301,6 @@ register_scalers( int force_scaler ) scaler = scaler_is_supported( current_scaler ) ? current_scaler : SCALER_NORMAL; - surface_scale = (float)gtkdisplay_surface_size / image_scale; - scaling_factor = scaler_get_scaling_factor( current_scaler ); - - /* Override scaler if it doesn't match the surface scale */ - if( force_scaler && surface_scale != scaling_factor ) { - - switch( gtkdisplay_surface_size ) { - case 1: scaler = machine_current->timex ? SCALER_HALF : SCALER_NORMAL; - break; - case 2: scaler = machine_current->timex ? SCALER_NORMAL : SCALER_DOUBLESIZE; - break; - case 3: scaler = machine_current->timex ? SCALER_TIMEX1_5X : - SCALER_TRIPLESIZE; - break; - case 4: scaler = machine_current->timex ? SCALER_TIMEX2X : - SCALER_QUADSIZE; - break; - } - } - /* Activate the scaler without trying to resize the GTK window */ scaler_activate_scaler( scaler ); } @@ -615,7 +592,7 @@ gtkdisplay_load_gfx_mode( void ) /* Rebuild the cairo surface for the new scaler */ surface_width = scale * image_width; surface_height = scale * image_height; - drawing_area_resize( surface_width, surface_height, 0 ); + drawing_area_resize( surface_width, surface_height ); gtk_window_resize( GTK_WINDOW( gtkui_window ), surface_width, surface_height + extra_height ); From ca0ac97d5e705608143277633b7226ebb0067855 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Mon, 15 Jun 2026 14:43:47 +0200 Subject: [PATCH 073/142] gtk3: Hide the pointer and bars when the mouse is idle Hide the pointer when the mouse is idle over the GTK drawing area for a couple of seconds. Additionally, if the window is in full screen mode, hide the menu and status bars too, so that only the Spectrum display can be seen. If users want to keep the bars visible at all times they can do it by leaving the mouse pointer over the menu or status bars, outside of the GTK drawing area. Alternatively, they can also maximise the window instead of using full screen mode. (part of feature request #135) --- ui/gtk3/gtkinternals.h | 3 + ui/gtk3/gtkkeyboard.c | 6 +- ui/gtk3/gtkmouse.c | 132 ++++++++++++++++++++++++++++++++++++++++- ui/gtk3/gtkui.c | 8 +++ 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/ui/gtk3/gtkinternals.h b/ui/gtk3/gtkinternals.h index cf2352aa..b61d8483 100644 --- a/ui/gtk3/gtkinternals.h +++ b/ui/gtk3/gtkinternals.h @@ -122,6 +122,9 @@ void gtkui_free_font( PangoFontDescription *font ); int gtkui_menubar_get_height( void ); +/* Show/hide the menu bar and status bar */ +void gtkui_set_bars_visible( int visible ); + /* * The menu data (menu_data.c) */ diff --git a/ui/gtk3/gtkkeyboard.c b/ui/gtk3/gtkkeyboard.c index cb2fe89c..862b6aab 100644 --- a/ui/gtk3/gtkkeyboard.c +++ b/ui/gtk3/gtkkeyboard.c @@ -82,8 +82,12 @@ gtkkeyboard_keypress( GtkWidget *widget GCC_UNUSED, GdkEvent *event, { input_event_t fuse_event; - if( event->key.keyval == GDK_KEY_F1 && event->key.state == 0 ) + if( event->key.keyval == GDK_KEY_F1 && event->key.state == 0 ) { ui_mouse_suspend(); + if( settings_current.full_screen ) { + gtkui_set_bars_visible( 1 ); + } + } if( event->key.keyval == GDK_KEY_F11 && event->key.state == 0 ) { gtkui_fullscreen_toggle(); diff --git a/ui/gtk3/gtkmouse.c b/ui/gtk3/gtkmouse.c index f68125e1..a25d77cd 100644 --- a/ui/gtk3/gtkmouse.c +++ b/ui/gtk3/gtkmouse.c @@ -42,6 +42,15 @@ static GdkCursor *nullpointer = NULL; +/* Auto-hide the mouse pointer (and, in fullscreen, the menu and status bars) + after this many seconds of inactivity over the drawing area */ +#define POINTER_HIDE_TIMEOUT 2 +static guint pointer_timeout_id = 0; +static gint64 pointer_last_activity = 0; +static gboolean pointer_hidden = FALSE; +static gboolean bars_hidden = FALSE; +static gboolean pointer_over_drawing_area = FALSE; + /* The widget we base our events, grabs, warping etc on */ static GtkWidget *mouse_widget = NULL; @@ -151,13 +160,99 @@ gtkmouse_reset_pointer( void ) } +/* Show the mouse pointer (and the bars) again if we hid them */ +static void +show_pointer_and_bars( void ) +{ + GdkWindow *window = gtk_widget_get_window( gtkui_drawing_area ); + + if( pointer_hidden && window ) { + gdk_window_set_cursor( window, NULL ); + pointer_hidden = FALSE; + } + + if( bars_hidden ) { + gtkui_set_bars_visible( 1 ); + bars_hidden = FALSE; + } +} + +/* Inactivity timer: hide the pointer once it has been idle long enough */ +static gboolean +hide_pointer( gpointer data GCC_UNUSED ) +{ + GdkWindow *window; + GdkWindow *toplevel; + gint64 timeout = (gint64)POINTER_HIDE_TIMEOUT * G_USEC_PER_SEC; + gint64 idle = g_get_monotonic_time() - pointer_last_activity; + + /* Rearm the timer if the pointer hasn't been idle long enough */ + if( idle < timeout ) { + pointer_timeout_id = g_timeout_add( ( timeout - idle ) / 1000, + hide_pointer, NULL ); + return G_SOURCE_REMOVE; + } + + pointer_timeout_id = 0; + + /* If the pointer is idle, hide it */ + window = gtk_widget_get_window( gtkui_drawing_area ); + if( window ) { + if( !nullpointer ) + nullpointer = gdk_cursor_new_for_display( gdk_window_get_display( window ), + GDK_BLANK_CURSOR ); + gdk_window_set_cursor( window, nullpointer ); + pointer_hidden = TRUE; + } + + /* In fullscreen also hide the menu and status bars */ + toplevel = gtk_widget_get_window( gtkui_window ); + if( toplevel && + ( gdk_window_get_state( toplevel ) & GDK_WINDOW_STATE_FULLSCREEN ) ) { + gtkui_set_bars_visible( 0 ); + bars_hidden = TRUE; + } + + return G_SOURCE_REMOVE; +} + +/* Pointer over the drawing area: track its idle status */ +static void +pointer_activity( void ) +{ + pointer_last_activity = g_get_monotonic_time(); + + if( !pointer_timeout_id ) + pointer_timeout_id = + g_timeout_add( POINTER_HIDE_TIMEOUT * 1000, hide_pointer, NULL ); + + show_pointer_and_bars(); +} + +/* Pointer left the drawing area: stop tracking its idle status */ +static void +pointer_reset( void ) +{ + if( pointer_timeout_id ) { + g_source_remove( pointer_timeout_id ); + pointer_timeout_id = 0; + } + + show_pointer_and_bars(); +} + static gboolean motion_event( GtkWidget *widget GCC_UNUSED, GdkEventMotion *event, gpointer data GCC_UNUSED ) { int rel_x, rel_y; - if( !ui_mouse_grabbed ) return FALSE; + /* When the mouse isn't grabbed, keep the pointer visible and track its + inactivity */ + if( !ui_mouse_grabbed ) { + if( pointer_over_drawing_area ) pointer_activity(); + return FALSE; + } /* Get relative movement from last position */ (*mouse_motion_fn)( event->x, event->y, &rel_x, &rel_y ); @@ -166,6 +261,28 @@ motion_event( GtkWidget *widget GCC_UNUSED, GdkEventMotion *event, return FALSE; } +/* The pointer entered the drawing area: start tracking inactivity */ +static gboolean +enter_event( GtkWidget *widget GCC_UNUSED, GdkEventCrossing *event GCC_UNUSED, + gpointer data GCC_UNUSED ) +{ + pointer_over_drawing_area = TRUE; + if( !ui_mouse_grabbed ) pointer_activity(); + + return FALSE; +} + +/* The pointer left the drawing area: make it visible */ +static gboolean +leave_event( GtkWidget *widget GCC_UNUSED, GdkEventCrossing *event GCC_UNUSED, + gpointer data GCC_UNUSED ) +{ + pointer_over_drawing_area = FALSE; + pointer_reset(); + + return FALSE; +} + static gboolean button_event( GtkWidget *widget GCC_UNUSED, GdkEventButton *event, gpointer data GCC_UNUSED ) @@ -193,6 +310,14 @@ gtkmouse_init( void ) G_CALLBACK( button_event ), NULL ); g_signal_connect( G_OBJECT( mouse_widget ), "button-release-event", G_CALLBACK( button_event ), NULL ); + + /* Track when the pointer is over the drawing area so we can hide it */ + gtk_widget_add_events( gtkui_drawing_area, + GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK ); + g_signal_connect( G_OBJECT( gtkui_drawing_area ), "enter-notify-event", + G_CALLBACK( enter_event ), NULL ); + g_signal_connect( G_OBJECT( gtkui_drawing_area ), "leave-notify-event", + G_CALLBACK( leave_event ), NULL ); } int @@ -219,6 +344,8 @@ ui_mouse_grab( int startup ) FALSE, nullpointer, NULL, NULL, NULL ); if( status == GDK_GRAB_SUCCESS ) { + /* The grab hides the pointer, so stop tracking its activity */ + pointer_reset(); gtkmouse_reset_pointer(); ui_statusbar_update( UI_STATUSBAR_ITEM_MOUSE, UI_STATUSBAR_STATE_ACTIVE ); return 1; @@ -238,6 +365,9 @@ ui_mouse_release( int suspend GCC_UNUSED ) seat = gdk_display_get_default_seat( display ); gdk_seat_ungrab( seat ); + /* The grab finished, show the pointer again */ + pointer_reset(); + ui_statusbar_update( UI_STATUSBAR_ITEM_MOUSE, UI_STATUSBAR_STATE_INACTIVE ); return 0; } diff --git a/ui/gtk3/gtkui.c b/ui/gtk3/gtkui.c index e512948f..ad1ceade 100644 --- a/ui/gtk3/gtkui.c +++ b/ui/gtk3/gtkui.c @@ -993,3 +993,11 @@ gtkui_menubar_get_height( void ) return alloc.height; } + +void +gtkui_set_bars_visible( int visible ) +{ + gtk_widget_set_visible( menu_bar, visible ); + /* The status bar is only shown if the user has it enabled */ + gtkstatusbar_set_visibility( visible ? settings_current.statusbar : 0 ); +} From 62241ba990a80943b7043e3ebde7cd14d7deaaee Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Mon, 15 Jun 2026 17:03:34 +0200 Subject: [PATCH 074/142] gtk3: Enable the full_screen setting in the GTK UI With this patch, the 'Full screen' option is enabled in the settings dialog and the options menu, and its value tracks the actual full screen state of the window. On startup, that option is honored and Fuse starts in full screen mode if it's enabled. (part of feature request #135) --- man/fuse.1 | 4 +-- menu_data.dat | 2 ++ ui/gtk3/gtkdisplay.c | 13 ++++++++ ui/gtk3/gtkinternals.h | 3 +- ui/gtk3/gtkkeyboard.c | 6 +--- ui/gtk3/gtkmouse.c | 6 ++-- ui/gtk3/gtkui.c | 74 +++++++++++++++++++++++++++++++++++++++--- ui/options.dat | 2 ++ 8 files changed, 94 insertions(+), 16 deletions(-) diff --git a/man/fuse.1 b/man/fuse.1 index 001e5bbb..beb46b7e 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -525,7 +525,7 @@ option. .B \-\-full\-screen .RS Specify whether Fuse should run in full screen mode. -This option is effective under the SDL and SDL2 UIs. +This option is effective under the GTK, SDL and SDL2 UIs. .PP Under SDL2, Fuse uses desktop fullscreen: the current desktop display mode is kept and Fuse scales the emulated display to fit it. @@ -3290,7 +3290,7 @@ section for more details. .I "Options, Full Screen" .RS Switch Fuse between full screen and windowed mode. -This menu is only available under the SDL UI. +This menu is only available under the GTK and SDL UIs. .RE .PP .I "Options, Disk Options..." diff --git a/menu_data.dat b/menu_data.dat index dec6e1f3..f5771004 100644 --- a/menu_data.dat +++ b/menu_data.dat @@ -140,6 +140,8 @@ Options/_Filter options..., Item Options/F_ull screen, Item, F11 #elif defined UI_SDL2 Options/F_ull screen, Item, F11 +#elif defined UI_GTK +Options/F_ull screen, Item, F11 #endif Options/_Disk options..., Item diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index c8f1a447..a57427f1 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -311,6 +311,9 @@ uidisplay_frame_end( void ) if( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) { uidisplay_area( 0, 0, image_width, image_height ); } + + /* If the user changed the full screen option, apply it now */ + gtkui_fullscreen_apply(); } void @@ -600,3 +603,13 @@ gtkdisplay_load_gfx_mode( void ) /* Redraw the entire screen... */ display_refresh_all(); } + +/* The window's natural size for the current scaler */ +void +gtkdisplay_get_window_size( int *width, int *height ) +{ + float scale = scaler_get_scaling_factor( current_scaler ); + + *width = scale * image_width; + *height = scale * image_height + extra_height; +} diff --git a/ui/gtk3/gtkinternals.h b/ui/gtk3/gtkinternals.h index b61d8483..31925fa6 100644 --- a/ui/gtk3/gtkinternals.h +++ b/ui/gtk3/gtkinternals.h @@ -35,6 +35,7 @@ extern libspectrum_dword gtkdisplay_colours[ 16 ]; void gtkdisplay_update_geometry( void ); +void gtkdisplay_get_window_size( int *width, int *height ); /* * Keyboard routines (gtkkeyboard.c) @@ -68,7 +69,7 @@ int gtkui_picture( const char *filename, int border ); extern void gtkui_popup_menu(void); -void gtkui_fullscreen_toggle( void ); +void gtkui_fullscreen_apply( void ); GtkAccelGroup* gtkstock_add_accel_group( GtkWidget *widget ); diff --git a/ui/gtk3/gtkkeyboard.c b/ui/gtk3/gtkkeyboard.c index 862b6aab..bbde51ab 100644 --- a/ui/gtk3/gtkkeyboard.c +++ b/ui/gtk3/gtkkeyboard.c @@ -31,6 +31,7 @@ #include "gtkinternals.h" #include "input.h" #include "keyboard.h" +#include "settings.h" #include "ui/ui.h" /* Given a hardware keycode, return the keyval which would have been returned if @@ -89,11 +90,6 @@ gtkkeyboard_keypress( GtkWidget *widget GCC_UNUSED, GdkEvent *event, } } - if( event->key.keyval == GDK_KEY_F11 && event->key.state == 0 ) { - gtkui_fullscreen_toggle(); - return TRUE; - } - fuse_event.type = INPUT_EVENT_KEYPRESS; get_keysyms( &fuse_event, event->key.hardware_keycode, event->key.keyval, event->key.group ); diff --git a/ui/gtk3/gtkmouse.c b/ui/gtk3/gtkmouse.c index a25d77cd..52b2a596 100644 --- a/ui/gtk3/gtkmouse.c +++ b/ui/gtk3/gtkmouse.c @@ -28,6 +28,7 @@ #include #include "gtkinternals.h" +#include "settings.h" #include "ui/ui.h" #ifdef GDK_WINDOWING_WAYLAND @@ -182,7 +183,6 @@ static gboolean hide_pointer( gpointer data GCC_UNUSED ) { GdkWindow *window; - GdkWindow *toplevel; gint64 timeout = (gint64)POINTER_HIDE_TIMEOUT * G_USEC_PER_SEC; gint64 idle = g_get_monotonic_time() - pointer_last_activity; @@ -206,9 +206,7 @@ hide_pointer( gpointer data GCC_UNUSED ) } /* In fullscreen also hide the menu and status bars */ - toplevel = gtk_widget_get_window( gtkui_window ); - if( toplevel && - ( gdk_window_get_state( toplevel ) & GDK_WINDOW_STATE_FULLSCREEN ) ) { + if( settings_current.full_screen ) { gtkui_set_bars_visible( 0 ); bars_hidden = TRUE; } diff --git a/ui/gtk3/gtkui.c b/ui/gtk3/gtkui.c index ad1ceade..e097e8ad 100644 --- a/ui/gtk3/gtkui.c +++ b/ui/gtk3/gtkui.c @@ -63,6 +63,10 @@ GtkWidget *gtkui_drawing_area; static GtkWidget *menu_bar; +/* Wait until the window has reached its final size before applying + fullscreen changes. See gtkui_window_configure() for more details */ +static int fullscreen_ready = 0; + /* The UIManager used to create the menu bar */ GtkUIManager *ui_manager_menu = NULL; @@ -92,6 +96,10 @@ static gboolean gtkui_make_menu(GtkAccelGroup **accel_group, static gboolean gtkui_lose_focus( GtkWidget*, GdkEvent*, gpointer ); static gboolean gtkui_gain_focus( GtkWidget*, GdkEvent*, gpointer ); +static gboolean gtkui_window_state ( GtkWidget *, GdkEventWindowState *, + gpointer ); +static gboolean gtkui_window_configure ( GtkWidget *, + GdkEventConfigure *, gpointer ); static gboolean gtkui_delete( GtkWidget *widget, GdkEvent *event, gpointer data ); @@ -175,6 +183,8 @@ ui_init( int *argc, char ***argv ) gtk_widget_add_events( gtkui_window, GDK_KEY_RELEASE_MASK ); g_signal_connect(G_OBJECT(gtkui_window), "key-release-event", G_CALLBACK(gtkkeyboard_keyrelease), NULL); + g_signal_connect(G_OBJECT(gtkui_window), "window-state-event", + G_CALLBACK(gtkui_window_state), NULL); /* If we lose the focus, disable all keys */ g_signal_connect( G_OBJECT( gtkui_window ), "focus-out-event", @@ -227,6 +237,14 @@ ui_init( int *argc, char ***argv ) gtk_widget_show_all( gtkui_window ); gtkstatusbar_set_visibility( settings_current.statusbar ); + /* Wait until the window gets its final size before going fullscreen, + so it can later restore its expected windowed size correctly. */ + fullscreen_ready = !settings_current.full_screen; + if( settings_current.full_screen ) { + g_signal_connect( G_OBJECT( gtkui_window ), "configure-event", + G_CALLBACK( gtkui_window_configure ), NULL ); + } + ui_mouse_present = 1; return 0; @@ -369,17 +387,65 @@ gtkui_gain_focus( GtkWidget *widget GCC_UNUSED, } void -gtkui_fullscreen_toggle( void ) +gtkui_fullscreen_apply( void ) { GdkWindow *window = gtk_widget_get_window( gtkui_window ); + int state; - if( window && ( gdk_window_get_state( window ) & GDK_WINDOW_STATE_FULLSCREEN ) ) { - gtk_window_unfullscreen( GTK_WINDOW( gtkui_window ) ); - } else { + /* Don't touch the window until it has reached its final size at startup */ + if( !fullscreen_ready || !window ) return; + + /* Nothing to do if the window is already in the requested state */ + state = gdk_window_get_state( window ) & GDK_WINDOW_STATE_FULLSCREEN; + if( !!state == !!settings_current.full_screen ) + return; + + if( settings_current.full_screen ) { gtk_window_fullscreen( GTK_WINDOW( gtkui_window ) ); + } else { + gtk_window_unfullscreen( GTK_WINDOW( gtkui_window ) ); } } +static gboolean +gtkui_window_state ( GtkWidget *widget, GdkEventWindowState *event, + gpointer data GCC_UNUSED ) +{ + if( event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN ) + settings_current.full_screen = + !!( event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN ); + + return FALSE; +} + +/* Go fullscreen at startup once the window has reached its final size. + This is necessary so, when we leave fullscreen, the window is restored + to the expected size according to the selected scaler, and not to + the initial size before the scaler was applied. */ +static gboolean +gtkui_window_configure( GtkWidget *widget, GdkEventConfigure *event, + gpointer data GCC_UNUSED ) +{ + int width, height; + + /* Fuse resizes the window a few times during startup so we receive + several configure events. Wait until one where the window is as big + as it's supposed to according to the selected scaler. */ + gtkdisplay_get_window_size( &width, &height ); + if( event->width < width || event->height < height ) + return FALSE; + + /* Once the window has the final size we can stop listening to these + * events. */ + g_signal_handlers_disconnect_by_func( + widget, G_CALLBACK( gtkui_window_configure ), NULL ); + + fullscreen_ready = 1; + gtkui_fullscreen_apply(); + + return FALSE; +} + /* Called by the main window on a "delete-event" */ static gboolean gtkui_delete( GtkWidget *widget GCC_UNUSED, GdkEvent *event GCC_UNUSED, diff --git a/ui/options.dat b/ui/options.dat index cfe61bdb..4f4929f7 100644 --- a/ui/options.dat +++ b/ui/options.dat @@ -67,6 +67,8 @@ Checkbox, Black and white T(V), bw_tv, INPUT_KEY_v Checkbox, Full (s)creen, full_screen, INPUT_KEY_s #elif defined UI_SDL2 Checkbox, Full (s)creen, full_screen, INPUT_KEY_s +#elif defined UI_GTK +Checkbox, Full (s)creen, full_screen, INPUT_KEY_s #endif Checkbox, Show status(b)ar, statusbar, INPUT_KEY_b Checkbox, Snap (j)oystick prompt, joy_prompt, INPUT_KEY_j From ed7b18adc51f5417057406e924b074ce90132c43 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 16 Jun 2026 00:04:58 +0200 Subject: [PATCH 075/142] gtk3: Don't resize the display when the bars are hidden in full screen In full screen mode the menu and status bars are hidden when the mouse pointer is idle. This makes the drawing area taller, so the Spectrum image is suddenly scaled to fill the extra space, producing an unpleasant visual effect. What we do instead is draw everything as if the bars were always hidden, and when they appear we simply clip the image. This looks as if the menu and status bar were painted as an overlay on top of the Spectrum display, producing a more natural result. The area that is left out belongs to the border of the Spectrum display, and the effect only lasts until the pointer is idle again, so the content loss is negligible. (part of feature request #135) --- ui/gtk3/gtkdisplay.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index a57427f1..792bc310 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -363,20 +363,33 @@ uidisplay_area( int x, int y, int w, int h ) static void get_surface_placement( double *scale, int *offset_x, int *offset_y ) { - int surface_width, surface_height, widget_width, widget_height; + int surface_width, surface_height, avail_width, avail_height, origin_y; double scale_x, scale_y, s; + GtkAllocation alloc; surface_width = cairo_image_surface_get_width( surface ); surface_height = cairo_image_surface_get_height( surface ); - widget_width = gtk_widget_get_allocated_width( gtkui_drawing_area ); - widget_height = gtk_widget_get_allocated_height( gtkui_drawing_area ); - scale_x = (double)widget_width / surface_width; - scale_y = (double)widget_height / surface_height; + /* By default fit the image into the drawing area */ + gtk_widget_get_allocation( gtkui_drawing_area, &alloc ); + avail_width = alloc.width; + avail_height = alloc.height; + origin_y = 0; + + /* In fullscreen fit the image into the whole window instead. + If the menu and status bars are hidden then we see the whole content. + If they are visible then those bars cover the edges of the image. */ + if( settings_current.full_screen ) { + avail_height = gtk_widget_get_allocated_height( gtkui_window ); + origin_y = -alloc.y; + } + + scale_x = (double)avail_width / surface_width; + scale_y = (double)avail_height / surface_height; s = scale_x < scale_y ? scale_x : scale_y; - *offset_x = ( widget_width - (int)( surface_width * s ) ) / 2; - *offset_y = ( widget_height - (int)( surface_height * s ) ) / 2; + *offset_x = ( avail_width - (int)( surface_width * s ) ) / 2; + *offset_y = origin_y + ( avail_height - (int)( surface_height * s ) ) / 2; *scale = s; } From 22e8e6c7de8b55ddbdda2be7a21397b722335318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Baldov=C3=AD?= Date: Sun, 14 Jun 2026 21:45:47 +0200 Subject: [PATCH 076/142] Ignore .exe files in unittests directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 31db84f6..f975916f 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ tags /unittests/sdl2scalerstatetest /unittests/sdl2joysticktest /unittests/sdl2mousetest +/unittests/*.exe /ylwrap /z80/coretest /z80/coretest.exe From 4340e207626fccda977414a85f59c5ad426247e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Baldov=C3=AD?= Date: Sun, 14 Jun 2026 22:17:49 +0200 Subject: [PATCH 077/142] Use xmlBufferContent to extract XML data accessing xmlBuffer struct members like content is deprecated --- svg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg.c b/svg.c index 2082f1f4..b506123c 100644 --- a/svg.c +++ b/svg.c @@ -299,7 +299,7 @@ svg_closefile( void ) svg_fnameroot, svg_filecount++ ); if( ( fp = fopen( svg_fname, "w" ) ) != NULL ) { - fprintf( fp, "%s", buffer->content ); + fprintf( fp, "%s", xmlBufferContent( buffer ) ); if( fclose( fp ) != 0 ) { ui_error( UI_ERROR_ERROR, "error closing SVG file '%s': %s", svg_fname, From d4114c1e335ef7726b5665dd6649d779f0156e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Baldov=C3=AD?= Date: Fri, 19 Jun 2026 22:23:58 +0200 Subject: [PATCH 078/142] Fix drag-and-drop files to GTK3 UI in Windows backend Paths on Windows use backslashes and differ from unix paths. --- ui/gtk3/gtkui.c | 59 ++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/ui/gtk3/gtkui.c b/ui/gtk3/gtkui.c index 30097c98..659f72e6 100644 --- a/ui/gtk3/gtkui.c +++ b/ui/gtk3/gtkui.c @@ -110,40 +110,43 @@ static void gtkui_drag_data_received( GtkWidget *widget GCC_UNUSED, GtkSelectionData *data, guint info GCC_UNUSED, guint timestamp ) { - static char uri_prefix[] = "file://"; - char *filename, *selection_filename; - const guchar *selection_data, *data_begin, *data_end, *p; + gchar *filename, **uris; gint selection_length; + GdkAtom selection_target; + gboolean success = FALSE; - selection_length = gtk_selection_data_get_length( data ); + if( !data ) { + gtk_drag_finish( drag_context, FALSE, FALSE, timestamp ); + return; + } - if ( data && selection_length > (gint) sizeof( uri_prefix ) ) { - selection_data = gtk_selection_data_get_data( data ); - data_begin = selection_data + sizeof( uri_prefix ) - 1; - data_end = selection_data + selection_length; - p = data_begin; - do { - if ( *p == '\r' || *p == '\n' ) { - data_end = p; - break; + selection_length = gtk_selection_data_get_length( data ); + selection_target = gtk_selection_data_get_target( data ); + + if ( selection_length > 0 && + selection_target == gdk_atom_intern( "text/uri-list", FALSE ) ) { + + uris = gtk_selection_data_get_uris( data ); + if( uris ) { + if( uris[0] ) { + /* Convert URI to a local path (handles %20 spaces and translates + to the backend format) */ + filename = g_filename_from_uri( uris[0], NULL, NULL ); + + if( filename ) { + fuse_emulation_pause(); + utils_open_file( filename, settings_current.auto_load, NULL ); + g_free( filename ); + display_refresh_all(); + fuse_emulation_unpause(); + success = TRUE; + } } - } while ( p++ != data_end ); - - selection_filename = g_strndup( (const gchar *)data_begin, - data_end - data_begin ); - - filename = g_uri_unescape_string( selection_filename, NULL ); - if ( filename ) { - fuse_emulation_pause(); - utils_open_file( filename, settings_current.auto_load, NULL ); - free( filename ); - display_refresh_all(); - fuse_emulation_unpause(); + g_strfreev( uris ); } - - g_free( selection_filename ); } - gtk_drag_finish( drag_context, FALSE, FALSE, timestamp ); + + gtk_drag_finish( drag_context, success, FALSE, timestamp ); } int From ec0e3ffa33c28d46eff7be14d252953a649bb91f Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Fri, 26 Jun 2026 11:42:02 +0200 Subject: [PATCH 079/142] gtk3: Don't leave a thin border around the scaled display When the scaling factor is not an integer (e.g. in full screen) cairo can paint about one pixel around the Spectrum image, into the surrounding black margins. When the emulation refreshes the screen with partial redraws those margin pixels are never touched, potentially leaving a stale thin border that is only cleared on full redraws (for example when the window is resized). The solution is to paint the Spectrum image only over its own area so it never touches the margins in the first place. --- ui/gtk3/gtkdisplay.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 792bc310..308455b0 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -559,7 +559,14 @@ gtkdisplay_draw( GtkWidget *widget GCC_UNUSED, cairo_t *cr, cairo_translate( cr, offset_x, offset_y ); cairo_scale( cr, scale, scale ); cairo_set_source_surface( cr, surface, 0, 0 ); - cairo_paint( cr ); + + /* Paint the image over its own area and not the whole drawing area. + Otherwise it can bleed into the surrounding margins when the + scaling factor is not an integer, leaving a stale thin border. */ + cairo_rectangle( cr, 0, 0, + cairo_image_surface_get_width( surface ), + cairo_image_surface_get_height( surface ) ); + cairo_fill( cr ); return FALSE; } From ac13d4c12c74c5a452e7c14f6c8b23657199f304 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 11:20:23 +1000 Subject: [PATCH 080/142] Always provide initialised memory in memory_pool_allocate_persistent() (fixes bug #232) --- memory_pages.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/memory_pages.c b/memory_pages.c index 35fa5136..0dda1d73 100644 --- a/memory_pages.c +++ b/memory_pages.c @@ -231,9 +231,9 @@ memory_pool_allocate_persistent( size_t length, int persistent ) memory_pool_entry_t *entry; libspectrum_byte *memory; - memory = libspectrum_new( libspectrum_byte, length ); + memory = libspectrum_new0( libspectrum_byte, length ); - entry = libspectrum_new( memory_pool_entry_t, 1 ); + entry = libspectrum_new0( memory_pool_entry_t, 1 ); entry->persistent = persistent; entry->memory = memory; From 116da8c548fda67a94acdc4070e65f25cdb179ee Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 14:36:08 +1000 Subject: [PATCH 081/142] Include z80/coretest in make check The Z80 CPU core tests (z80/coretest) were only reachable via the separate 'make test' target, not via 'make check'. This meant a plain 'make check' silently skipped the Z80 opcode correctness suite. --- Makefile.am | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 674b75d1..971c0526 100644 --- a/Makefile.am +++ b/Makefile.am @@ -216,7 +216,7 @@ include ui/xlib/Makefile.am include unittests/Makefile.am include z80/Makefile.am -check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest +check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest z80/coretest @test -e $(top_builddir)/roms || ln -sf "$(abs_top_srcdir)/roms" $(top_builddir)/roms $(top_builddir)/fuse --unittests $(top_builddir)/unittests/displaytest @@ -224,3 +224,5 @@ check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2 $(top_builddir)/unittests/sdl2scalerstatetest $(top_builddir)/unittests/sdl2joysticktest $(top_builddir)/unittests/sdl2mousetest + $(top_builddir)/z80/coretest $(srcdir)/z80/tests/tests.in > $(top_builddir)/z80/tests.actual + cmp $(top_builddir)/z80/tests.actual $(srcdir)/z80/tests/tests.expected From 5180cc9aed9d26d6ba0f105da7d631d825d76b00 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 14:42:15 +1000 Subject: [PATCH 082/142] Complete LD r,(IX/IY+d) and LD (IX/IY+d),r disassembler test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests 123-128 covered only the B register for DD/FD-prefix load instructions. Add tests 255-277 to verify the disassembler handles all remaining destination registers for: LD r,(IX+d) / LD r,(IY+d) — registers C, D, E, H, L (A already at test15) LD (IX+d),r / LD (IY+d),r — registers C, D, E, H, L, A The DD/FD prefix LD instructions use the 01xxxxxx opcode range with two distinct code paths in the disassembler: - source == 6 (register 6 = (IX+d)): the "LD r,(IX+d)" path forces dest_reg() to use USE_HL so H/L render as H/L (not IXh/IXl) - dest == 6 (register 6 = (IX+d)): the "LD (IX+d),r" path forces source_reg() to use USE_HL for the same reason --- debugger/disassemble.c | 54 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 6a033c8d..8930b931 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1224,6 +1224,33 @@ libspectrum_byte test253_data[] = { 0xfd, 0xcb, 0x55, 0x07 }; /* LD A,RLC (IY+5 /* DD CB undocumented: non-RLC rotation op to verify rotate_op() selection */ libspectrum_byte test254_data[] = { 0xdd, 0xcb, 0x55, 0x38 }; /* LD B,SRL (IX+55) */ +/* DD/FD prefix: LD r,(IX/IY+d) — remaining registers C, D, E, H, L */ +libspectrum_byte test255_data[] = { 0xdd, 0x4e, 0x05 }; /* LD C,(IX+05) */ +libspectrum_byte test256_data[] = { 0xdd, 0x56, 0x05 }; /* LD D,(IX+05) */ +libspectrum_byte test257_data[] = { 0xdd, 0x5e, 0x05 }; /* LD E,(IX+05) */ +libspectrum_byte test258_data[] = { 0xdd, 0x66, 0x05 }; /* LD H,(IX+05) */ +libspectrum_byte test259_data[] = { 0xdd, 0x6e, 0x05 }; /* LD L,(IX+05) */ +libspectrum_byte test260_data[] = { 0xfd, 0x4e, 0x05 }; /* LD C,(IY+05) */ +libspectrum_byte test261_data[] = { 0xfd, 0x56, 0x05 }; /* LD D,(IY+05) */ +libspectrum_byte test262_data[] = { 0xfd, 0x5e, 0x05 }; /* LD E,(IY+05) */ +libspectrum_byte test263_data[] = { 0xfd, 0x66, 0x05 }; /* LD H,(IY+05) */ +libspectrum_byte test264_data[] = { 0xfd, 0x6e, 0x05 }; /* LD L,(IY+05) */ +libspectrum_byte test265_data[] = { 0xfd, 0x7e, 0x05 }; /* LD A,(IY+05) */ + +/* DD/FD prefix: LD (IX/IY+d),r — remaining registers C, D, E, H, L, A */ +libspectrum_byte test266_data[] = { 0xdd, 0x71, 0x05 }; /* LD (IX+05),C */ +libspectrum_byte test267_data[] = { 0xdd, 0x72, 0x05 }; /* LD (IX+05),D */ +libspectrum_byte test268_data[] = { 0xdd, 0x73, 0x05 }; /* LD (IX+05),E */ +libspectrum_byte test269_data[] = { 0xdd, 0x74, 0x05 }; /* LD (IX+05),H */ +libspectrum_byte test270_data[] = { 0xdd, 0x75, 0x05 }; /* LD (IX+05),L */ +libspectrum_byte test271_data[] = { 0xdd, 0x77, 0x05 }; /* LD (IX+05),A */ +libspectrum_byte test272_data[] = { 0xfd, 0x71, 0x05 }; /* LD (IY+05),C */ +libspectrum_byte test273_data[] = { 0xfd, 0x72, 0x05 }; /* LD (IY+05),D */ +libspectrum_byte test274_data[] = { 0xfd, 0x73, 0x05 }; /* LD (IY+05),E */ +libspectrum_byte test275_data[] = { 0xfd, 0x74, 0x05 }; /* LD (IY+05),H */ +libspectrum_byte test276_data[] = { 0xfd, 0x75, 0x05 }; /* LD (IY+05),L */ +libspectrum_byte test277_data[] = { 0xfd, 0x77, 0x05 }; /* LD (IY+05),A */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1615,6 +1642,33 @@ debugger_disassemble_unittest( void ) /* DD CB: verify rotation-op selection (SRL) independent of destination */ r += run_test( test254_data, sizeof( test254_data ), "LD B,SRL (IX+55)" ); + /* DD/FD prefix: LD r,(IX/IY+d) — remaining registers C, D, E, H, L */ + r += run_test( test255_data, sizeof( test255_data ), "LD C,(IX+05)" ); + r += run_test( test256_data, sizeof( test256_data ), "LD D,(IX+05)" ); + r += run_test( test257_data, sizeof( test257_data ), "LD E,(IX+05)" ); + r += run_test( test258_data, sizeof( test258_data ), "LD H,(IX+05)" ); + r += run_test( test259_data, sizeof( test259_data ), "LD L,(IX+05)" ); + r += run_test( test260_data, sizeof( test260_data ), "LD C,(IY+05)" ); + r += run_test( test261_data, sizeof( test261_data ), "LD D,(IY+05)" ); + r += run_test( test262_data, sizeof( test262_data ), "LD E,(IY+05)" ); + r += run_test( test263_data, sizeof( test263_data ), "LD H,(IY+05)" ); + r += run_test( test264_data, sizeof( test264_data ), "LD L,(IY+05)" ); + r += run_test( test265_data, sizeof( test265_data ), "LD A,(IY+05)" ); + + /* DD/FD prefix: LD (IX/IY+d),r — remaining registers C, D, E, H, L, A */ + r += run_test( test266_data, sizeof( test266_data ), "LD (IX+05),C" ); + r += run_test( test267_data, sizeof( test267_data ), "LD (IX+05),D" ); + r += run_test( test268_data, sizeof( test268_data ), "LD (IX+05),E" ); + r += run_test( test269_data, sizeof( test269_data ), "LD (IX+05),H" ); + r += run_test( test270_data, sizeof( test270_data ), "LD (IX+05),L" ); + r += run_test( test271_data, sizeof( test271_data ), "LD (IX+05),A" ); + r += run_test( test272_data, sizeof( test272_data ), "LD (IY+05),C" ); + r += run_test( test273_data, sizeof( test273_data ), "LD (IY+05),D" ); + r += run_test( test274_data, sizeof( test274_data ), "LD (IY+05),E" ); + r += run_test( test275_data, sizeof( test275_data ), "LD (IY+05),H" ); + r += run_test( test276_data, sizeof( test276_data ), "LD (IY+05),L" ); + r += run_test( test277_data, sizeof( test277_data ), "LD (IY+05),A" ); + return r; } From 2f54f977b347fe45b16d2d39d54bccf17c92cab8 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 14:52:38 +1000 Subject: [PATCH 083/142] Replace R register magic numbers with named constants Add Z80_R_LOWER_BITS (0x7f) and Z80_R7_MASK (0x80) to z80_macros.h to replace the bare hex literals used when reconstructing the R register from its two storage fields (z80.r and z80.r7). The pattern (R7 & 0x80) | (R & 0x7f) appears in five places; named constants make the intent clearer without changing any behaviour. --- rzx.c | 2 +- z80/coretest.c | 2 +- z80/z80.c | 2 +- z80/z80_debugger_variables.c | 2 +- z80/z80_macros.h | 6 +++++- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rzx.c b/rzx.c index 303b3ab2..a2178699 100644 --- a/rzx.c +++ b/rzx.c @@ -722,7 +722,7 @@ static int playback_frame( void ) R register */ static int counter_reset( void ) { - R &= 0x7f; /* Clear all but the 7 lowest bits of the R register */ + R &= Z80_R_LOWER_BITS; /* Normalise R to its lower seven bits */ rzx_instructions_offset = -R; /* Gives us a zero count */ return 0; diff --git a/z80/coretest.c b/z80/coretest.c index 8698a9f5..a1dfd561 100644 --- a/z80/coretest.c +++ b/z80/coretest.c @@ -329,7 +329,7 @@ dump_z80_state( void ) { printf( "%04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x %04x\n", AF, BC, DE, HL, AF_, BC_, DE_, HL_, IX, IY, SP, PC, z80.memptr.w ); - printf( "%02x %02x %d %d %d %d %d\n", I, ( R7 & 0x80 ) | ( R & 0x7f ), + printf( "%02x %02x %d %d %d %d %d\n", I, ( R7 & Z80_R7_MASK ) | ( R & Z80_R_LOWER_BITS ), IFF1, IFF2, IM, z80.halted, tstates ); } diff --git a/z80/z80.c b/z80/z80.c index 2f47ffe3..490f4c55 100644 --- a/z80/z80.c +++ b/z80/z80.c @@ -322,7 +322,7 @@ z80_to_snapshot( libspectrum_snap *snap ) { libspectrum_byte r_register; - r_register = ( R7 & 0x80 ) | ( R & 0x7f ); + r_register = ( R7 & Z80_R7_MASK ) | ( R & Z80_R_LOWER_BITS ); libspectrum_snap_set_a ( snap, A ); libspectrum_snap_set_f ( snap, F ); libspectrum_snap_set_a_ ( snap, A_ ); libspectrum_snap_set_f_ ( snap, F_ ); diff --git a/z80/z80_debugger_variables.c b/z80/z80_debugger_variables.c index 25ad799f..406bb1ab 100644 --- a/z80/z80_debugger_variables.c +++ b/z80/z80_debugger_variables.c @@ -129,7 +129,7 @@ DEBUGGER_CALLBACKS(Q) static libspectrum_dword get_R( void ) { - return ( R7 & 0x80 ) | ( R & 0x7f ); + return ( R7 & Z80_R7_MASK ) | ( R & Z80_R_LOWER_BITS ); } static void diff --git a/z80/z80_macros.h b/z80/z80_macros.h index 2511a523..faf5a676 100644 --- a/z80/z80_macros.h +++ b/z80/z80_macros.h @@ -78,11 +78,15 @@ #define R z80.r #define R7 z80.r7 +/* Masks for the Z80 R register: low seven bits auto-increment, bit 7 is static */ +#define Z80_R_LOWER_BITS 0x7f +#define Z80_R7_MASK 0x80 + #define IFF1 z80.iff1 #define IFF2 z80.iff2 #define IM z80.im -#define IR ( ( z80.i ) << 8 | ( z80.r7 & 0x80 ) | ( z80.r & 0x7f ) ) +#define IR ( ( z80.i ) << 8 | ( z80.r7 & Z80_R7_MASK ) | ( z80.r & Z80_R_LOWER_BITS ) ) #define IS_CMOS settings_current.z80_is_cmos From d4f27311121586028bdc6111962114752ef79e1b Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 15:01:18 +1000 Subject: [PATCH 084/142] Cache beam position in display_update_critical() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit display_dirty64() calls display_update_critical() eight times in a tight loop (once per pixel row in the 8-row attribute block) all at the same value of tstates. Each call previously invoked get_beam_position() which performs an integer division by tstates_per_line (~228) to compute the current scanline. On x86 integer division costs 20-40 clock cycles. Introduce three file-scope statics: display_cached_beam_tstates – last tstates for which the position was computed (initialised to -1) display_cached_beam_x/y – the cached result display_update_critical() checks the cache first; if tstates is unchanged the cached values are reused directly. The cache is invalidated at the start of display_frame() alongside the existing critical-region reset, so a machine change or reset never causes a stale hit. The benefit is most pronounced during attribute writes (display_dirty64) where 7 of 8 divisions are eliminated per call. Pixel writes (display_dirty8) also benefit whenever the same instruction triggers multiple screen writes at identical tstates. --- display.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/display.c b/display.c index 59a0414a..9e9ccc7c 100644 --- a/display.c +++ b/display.c @@ -101,6 +101,13 @@ static int display_redraw_all; /* The last point at which we updated the screen display */ static int critical_region_x = 0, critical_region_y = 0; +/* Cache the most-recently computed beam position keyed on tstates. + get_beam_position() is called once per dirty screen write, and attribute + writes trigger eight consecutive calls all at the same tstates value. + Skipping the division when tstates hasn't changed saves ~7/8 of those. */ +static libspectrum_dword display_cached_beam_tstates = (libspectrum_dword)-1; +static int display_cached_beam_x, display_cached_beam_y; + /* The border colour changes which have occurred in this frame */ struct border_change_t { int x, y; @@ -662,7 +669,12 @@ display_update_critical( int x, int y ) { int beam_x, beam_y; - get_beam_position( &beam_x, &beam_y ); + if( tstates != display_cached_beam_tstates ) { + get_beam_position( &display_cached_beam_x, &display_cached_beam_y ); + display_cached_beam_tstates = tstates; + } + beam_x = display_cached_beam_x; + beam_y = display_cached_beam_y; beam_x -= DISPLAY_BORDER_WIDTH_COLS; beam_y -= DISPLAY_BORDER_HEIGHT; @@ -957,6 +969,10 @@ update_ui_screen( void ) int display_frame( void ) { + /* Invalidate the beam position cache so get_beam_position() is called + fresh in the new frame (machine timing may have changed on reset). */ + display_cached_beam_tstates = (libspectrum_dword)-1; + /* Copy all the critical region to the display */ copy_critical_region( DISPLAY_WIDTH_COLS, DISPLAY_HEIGHT - 1 ); critical_region_x = critical_region_y = 0; From a67a18e50824e4ea1640f5174c705d8cda63680a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 15:07:38 +1000 Subject: [PATCH 085/142] Add unary negation and binary subtraction non-associativity tests The expression evaluator supports the unary '-' (negation) operator but it had no unit tests. Add four evaluation tests to document the unsigned wrap-around semantics and two deparse tests to verify the rendered form. Also add a non-associativity deparse test for binary '-'. The existing test coverage already verified '%' non-associativity, but '-' (the most common non-associative operator) was not exercised by a deparse test. --- debugger/expression.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index d2af59ac..37cf694d 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -837,6 +837,12 @@ debugger_expression_unittest( void ) r += eval_unary_test( '~', 0, 0xFFFFFFFF, "bitwise-not-zero" ); r += eval_unary_test( '~', 0xFFFFFFFF, 0, "bitwise-not-ones" ); + /* Unary negation: unsigned arithmetic wraps modulo 2^32 */ + r += eval_unary_test( '-', 0, 0, "negate-zero" ); + r += eval_unary_test( '-', 1, 0xFFFFFFFF, "negate-one" ); + r += eval_unary_test( '-', 0xFFFFFFFF, 1, "negate-maxuint" ); + r += eval_unary_test( '-', 0x80000000, 0x80000000, "negate-midpoint" ); + /* Deparse tests: save and restore output base */ saved_base = debugger_output_base; debugger_output_base = 16; @@ -882,6 +888,33 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "( 0x3 * 0x4 ) % 0x5", "deparse-mod-non-assoc" ); + /* Binary '-' is non-associative: 3-(4-2) must bracket the right operand */ + r += deparse_test( + debugger_expression_new_binaryop( '-', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( '-', + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 - ( 0x4 - 0x2 )", "deparse-sub-non-assoc" ); + + /* Unary negation deparse: plain integer and expression requiring brackets */ + r += deparse_test( + debugger_expression_new_unaryop( '-', + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "-0x5", "deparse-negate" ); + + r += deparse_test( + debugger_expression_new_unaryop( '-', + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "-( 0x3 + 0x4 )", "deparse-negate-expr" ); + debugger_output_base = saved_base; return r; From b6955e55b2c68cdbe7b0cd4b81cd31096d66972e Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 15:12:31 +1000 Subject: [PATCH 086/142] Remove duplicate find_breakpoint_by_id() forward declaration --- debugger/breakpoint.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/debugger/breakpoint.c b/debugger/breakpoint.c index 59597b79..70733877 100644 --- a/debugger/breakpoint.c +++ b/debugger/breakpoint.c @@ -72,8 +72,6 @@ static debugger_breakpoint* get_breakpoint_by_id( size_t id ); static gint find_breakpoint_by_id( gconstpointer data, gconstpointer user_data ); static void remove_time( gpointer data, gpointer user_data ); -static gint find_breakpoint_by_id( gconstpointer data, - gconstpointer user_data ); static gint find_breakpoint_by_address( gconstpointer data, gconstpointer user_data ); static void free_breakpoint( gpointer data, gpointer user_data ); From f1298b1975cb03d24a5e81914690a39b382df229 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 15:16:03 +1000 Subject: [PATCH 087/142] Replace magic number 40 with DISASM_BUF_LEN constant All internal string buffers in disassemble.c used the literal value 40 for their size, which appears in 50 places across declarations, local variables, and function calls. Introduce DISASM_BUF_LEN to name this value, making the intent clear and ensuring any future resize only requires a single change. --- debugger/disassemble.c | 101 +++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 8930b931..849f9180 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -37,6 +37,9 @@ #include "memory_pages.h" #include "ui/ui.h" +/* Size of temporary string buffers used throughout the disassembler */ +#define DISASM_BUF_LEN 40 + /* Used to flag whether we're after a DD or FD prefix */ enum hl_type { USE_HL, USE_IX, USE_IY }; @@ -109,7 +112,7 @@ disassemble_main( libspectrum_word address, char *buffer, size_t buflen, size_t *length, enum hl_type use_hl ) { libspectrum_byte b; - char buffer2[40], buffer3[40]; + char buffer2[DISASM_BUF_LEN], buffer3[DISASM_BUF_LEN]; size_t prefix_length = 0; b = readbyte_internal( address ); @@ -130,16 +133,16 @@ disassemble_main( libspectrum_word address, char *buffer, size_t buflen, } else if( b < 0x80 ) { if( ( b & 0x07 ) == 0x06 ) { /* LD something,(HL) */ - dest_reg( address, USE_HL, buffer2, 40 ); - source_reg( address, use_hl, buffer3, 40 ); + dest_reg( address, USE_HL, buffer2, DISASM_BUF_LEN ); + source_reg( address, use_hl, buffer3, DISASM_BUF_LEN ); *length = ( use_hl == USE_HL ? 1 : 2 ); } else if( ( ( b >> 3 ) & 0x07 ) == 0x06 ) { /* LD (HL),something */ - dest_reg( address, use_hl, buffer2, 40 ); - source_reg( address, USE_HL, buffer3, 40 ); + dest_reg( address, use_hl, buffer2, DISASM_BUF_LEN ); + source_reg( address, USE_HL, buffer3, DISASM_BUF_LEN ); *length = ( use_hl == USE_HL ? 1 : 2 ); } else { /* Does not involve (HL) at all */ - dest_reg( address, use_hl, buffer2, 40 ); - source_reg( address, use_hl, buffer3, 40 ); + dest_reg( address, use_hl, buffer2, DISASM_BUF_LEN ); + source_reg( address, use_hl, buffer3, DISASM_BUF_LEN ); *length = 1; } /* Note LD (HL),(HL) does not exist */ @@ -147,7 +150,7 @@ disassemble_main( libspectrum_word address, char *buffer, size_t buflen, snprintf( buffer, buflen, "LD %s,%s", buffer2, buffer3 ); } else if( b < 0xc0 ) { - *length = 1 + source_reg( address, use_hl, buffer2, 40 ); + *length = 1 + source_reg( address, use_hl, buffer2, DISASM_BUF_LEN ); snprintf( buffer, buflen, addition_op( b ), buffer2 ); } else { disassemble_11xxxxxx( address, buffer, buflen, length, use_hl ); @@ -168,7 +171,7 @@ disassemble_00xxxxxx( libspectrum_word address, char *buffer, size_t buflen, const char *opcode_00xxx111[] = { "RLCA", "RRCA", "RLA", "RRA", "DAA", "CPL", "SCF", "CCF" }; - char buffer2[40], buffer3[40]; + char buffer2[DISASM_BUF_LEN], buffer3[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); @@ -178,14 +181,14 @@ disassemble_00xxxxxx( libspectrum_word address, char *buffer, size_t buflen, if( b <= 0x08 ) { snprintf( buffer, buflen, "%s", opcode_00xxx000[ b >> 3 ] ); *length = 1; } else { - get_offset( buffer2, 40, address + 2, readbyte_internal( address + 1 ) ); + get_offset( buffer2, DISASM_BUF_LEN, address + 2, readbyte_internal( address + 1 ) ); snprintf( buffer, buflen, "%s%s", opcode_00xxx000[ b >> 3 ], buffer2 ); *length = 2; } break; case 0x01: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD %s,%s", reg_pair( b, use_hl ), buffer2 ); *length = 3; break; @@ -199,18 +202,18 @@ disassemble_00xxxxxx( libspectrum_word address, char *buffer, size_t buflen, break; case 0x04: case 0x0c: - *length = 1 + dest_reg( address, use_hl, buffer2, 40 ); + *length = 1 + dest_reg( address, use_hl, buffer2, DISASM_BUF_LEN ); snprintf( buffer, buflen, "INC %s", buffer2 ); break; case 0x05: case 0x0d: - *length = 1 + dest_reg( address, use_hl, buffer2, 40 ); + *length = 1 + dest_reg( address, use_hl, buffer2, DISASM_BUF_LEN ); snprintf( buffer, buflen, "DEC %s", buffer2 ); break; case 0x06: case 0x0e: - *length = 2 + dest_reg( address, use_hl, buffer2, 40 ); - get_byte( buffer3, 40, readbyte_internal( address + *length - 1 ) ); + *length = 2 + dest_reg( address, use_hl, buffer2, DISASM_BUF_LEN ); + get_byte( buffer3, DISASM_BUF_LEN, readbyte_internal( address + *length - 1 ) ); snprintf( buffer, buflen, "LD %s,%s", buffer2, buffer3 ); break; @@ -241,7 +244,7 @@ static void disassemble_00xxx010( libspectrum_word address, char *buffer, size_t buflen, size_t *length, enum hl_type use_hl ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); switch( b >> 4 ) { @@ -252,13 +255,13 @@ disassemble_00xxx010( libspectrum_word address, char *buffer, size_t buflen, break; case 2: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD (%s),%s", buffer2, hl_ix_iy( use_hl ) ); *length = 3; break; case 3: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD (%s),A", buffer2 ); *length = 3; break; } @@ -269,7 +272,7 @@ static void disassemble_00xxx110( libspectrum_word address, char *buffer, size_t buflen, size_t *length, enum hl_type use_hl ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); switch( b >> 4 ) { @@ -280,13 +283,13 @@ disassemble_00xxx110( libspectrum_word address, char *buffer, size_t buflen, break; case 2: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD %s,(%s)", hl_ix_iy( use_hl ), buffer2 ); *length = 3; break; case 3: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD A,(%s)", buffer2 ); *length = 3; break; } @@ -297,7 +300,7 @@ static void disassemble_11xxxxxx( libspectrum_word address, char *buffer, size_t buflen, size_t *length, enum hl_type use_hl ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); switch( b & 0x07 ) { @@ -311,7 +314,7 @@ disassemble_11xxxxxx( libspectrum_word address, char *buffer, size_t buflen, break; case 0x02: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "JP %s,%s", condition( b ), buffer2 ); *length = 3; break; @@ -321,7 +324,7 @@ disassemble_11xxxxxx( libspectrum_word address, char *buffer, size_t buflen, break; case 0x04: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "CALL %s,%s", condition( b ), buffer2 ); *length = 3; break; @@ -331,7 +334,7 @@ disassemble_11xxxxxx( libspectrum_word address, char *buffer, size_t buflen, break; case 0x06: - get_byte( buffer2, 40, readbyte_internal( address + 1 ) ); + get_byte( buffer2, DISASM_BUF_LEN, readbyte_internal( address + 1 ) ); snprintf( buffer, buflen, addition_op( b ), buffer2 ); *length = 2; break; @@ -374,13 +377,13 @@ static void disassemble_11xxx011( libspectrum_word address, char *buffer, size_t buflen, size_t *length, enum hl_type use_hl ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); switch( ( b >> 3 ) - 0x18 ) { case 0x00: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "JP %s", buffer2 ); *length = 3; break; @@ -396,12 +399,12 @@ disassemble_11xxx011( libspectrum_word address, char *buffer, size_t buflen, break; case 0x02: - get_byte( buffer2, 40, readbyte_internal( address + 1 ) ); + get_byte( buffer2, DISASM_BUF_LEN, readbyte_internal( address + 1 ) ); snprintf( buffer, buflen, "OUT (%s),A", buffer2 ); *length = 2; break; case 0x03: - get_byte( buffer2, 40, readbyte_internal( address + 1 ) ); + get_byte( buffer2, DISASM_BUF_LEN, readbyte_internal( address + 1 ) ); snprintf( buffer, buflen, "IN A,(%s)", buffer2 ); *length = 2; break; @@ -429,7 +432,7 @@ static void disassemble_11xxx101( libspectrum_word address, char *buffer, size_t buflen, size_t *length, enum hl_type use_hl ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); switch( ( b >> 3 ) - 0x18 ) { @@ -439,7 +442,7 @@ disassemble_11xxx101( libspectrum_word address, char *buffer, size_t buflen, break; case 0x01: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "CALL %s", buffer2 ); *length = 3; break; @@ -466,10 +469,10 @@ static void disassemble_cb( libspectrum_word address, char *buffer, size_t buflen, size_t *length ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; libspectrum_byte b = readbyte_internal( address ); - source_reg( address, USE_HL, buffer2, 40 ); + source_reg( address, USE_HL, buffer2, DISASM_BUF_LEN ); if( b < 0x40 ) { snprintf( buffer, buflen, "%s %s", rotate_op( b ), buffer2 ); @@ -487,7 +490,7 @@ disassemble_ed( libspectrum_word address, char *buffer, size_t buflen, size_t *length ) { libspectrum_byte b; - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; const char *opcode_01xxx111[] = { "LD I,A", "LD R,A", "LD A,I", "LD A,R", "RRD", "RLD", "NOPD", "NOPD" @@ -516,7 +519,7 @@ disassemble_ed( libspectrum_word address, char *buffer, size_t buflen, if( b == 0x70 ) { snprintf( buffer, buflen, "IN F,(C)" ); *length = 1; } else { - dest_reg( address, USE_HL, buffer2, 40 ); + dest_reg( address, USE_HL, buffer2, DISASM_BUF_LEN ); snprintf( buffer, buflen, "IN %s,(C)", buffer2 ); *length = 1; } break; @@ -525,7 +528,7 @@ disassemble_ed( libspectrum_word address, char *buffer, size_t buflen, if( b == 0x71 ) { snprintf( buffer, buflen, "OUT (C),0" ); *length = 1; } else { - dest_reg( address, USE_HL, buffer2, 40 ); + dest_reg( address, USE_HL, buffer2, DISASM_BUF_LEN ); snprintf( buffer, buflen, "OUT (C),%s", buffer2 ); *length = 1; } break; @@ -536,7 +539,7 @@ disassemble_ed( libspectrum_word address, char *buffer, size_t buflen, break; case 0x03: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD (%s),%s", buffer2, reg_pair( b, USE_HL ) ); *length = 3; break; @@ -569,7 +572,7 @@ disassemble_ed( libspectrum_word address, char *buffer, size_t buflen, break; case 0x0b: - get_word( buffer2, 40, address + 1 ); + get_word( buffer2, DISASM_BUF_LEN, address + 1 ); snprintf( buffer, buflen, "LD %s,(%s)", reg_pair( b, USE_HL ), buffer2 ); *length = 3; break; @@ -590,33 +593,33 @@ disassemble_ddfd_cb( libspectrum_word address, char offset, size_t *length ) { libspectrum_byte b = readbyte_internal( address ); - char buffer2[40], buffer3[40]; + char buffer2[DISASM_BUF_LEN], buffer3[DISASM_BUF_LEN]; if( b < 0x40 ) { if( ( b & 0x07 ) == 0x06 ) { - ix_iy_offset( buffer2, 40, use_hl, offset ); + ix_iy_offset( buffer2, DISASM_BUF_LEN, use_hl, offset ); snprintf( buffer, buflen, "%s %s", rotate_op( b ), buffer2 ); *length = 1; } else { - source_reg( address, USE_HL, buffer2, 40 ); - ix_iy_offset( buffer3, 40, use_hl, offset ); + source_reg( address, USE_HL, buffer2, DISASM_BUF_LEN ); + ix_iy_offset( buffer3, DISASM_BUF_LEN, use_hl, offset ); snprintf( buffer, buflen, "LD %s,%s %s", buffer2, rotate_op( b ), buffer3 ); *length = 1; } } else if( b < 0x80 ) { - ix_iy_offset( buffer2, 40, use_hl, offset ); + ix_iy_offset( buffer2, DISASM_BUF_LEN, use_hl, offset ); snprintf( buffer, buflen, "%s %d,%s", bit_op( b ), bit_op_bit( b ), buffer2 ); *length = 1; } else { if( ( b & 0x07 ) == 0x06 ) { - ix_iy_offset( buffer2, 40, use_hl, offset ); + ix_iy_offset( buffer2, DISASM_BUF_LEN, use_hl, offset ); snprintf( buffer, buflen, "%s %d,%s", bit_op( b ), bit_op_bit( b ), buffer2 ); *length = 1; } else { - source_reg( address, USE_HL, buffer2, 40 ); - ix_iy_offset( buffer3, 40, use_hl, offset ); + source_reg( address, USE_HL, buffer2, DISASM_BUF_LEN ); + ix_iy_offset( buffer3, DISASM_BUF_LEN, use_hl, offset ); snprintf( buffer, buflen, "LD %s,%s %d,%s", buffer2, bit_op( b ), bit_op_bit( b ), buffer3 ); *length = 1; } @@ -718,7 +721,7 @@ static int single_reg( int i, enum hl_type use_hl, libspectrum_byte offset, char *buffer, size_t buflen ) { - char buffer2[40]; + char buffer2[DISASM_BUF_LEN]; if( i == 0x04 && use_hl != USE_HL ) { snprintf( buffer, buflen, "%sh", hl_ix_iy( use_hl ) ); @@ -727,7 +730,7 @@ single_reg( int i, enum hl_type use_hl, libspectrum_byte offset, snprintf( buffer, buflen, "%sl", hl_ix_iy( use_hl ) ); return 0; } else if( i == 0x06 && use_hl != USE_HL ) { - ix_iy_offset( buffer2, 40, use_hl, offset ); + ix_iy_offset( buffer2, DISASM_BUF_LEN, use_hl, offset ); snprintf( buffer, buflen, "%s", buffer2 ); return 1; } else { @@ -1254,7 +1257,7 @@ libspectrum_byte test277_data[] = { 0xfd, 0x77, 0x05 }; /* LD (IY+05),A */ static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { - char disassembly[40]; + char disassembly[DISASM_BUF_LEN]; size_t length; memcpy( memory_map_read[8].page, data, data_length ); From c4dfb104de1eacbee76c2478ce2bdcb6705497a3 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 15:26:25 +1000 Subject: [PATCH 088/142] Use GitHub mirror URL for libspectrum clone The libspectrum CI workflows were cloning from SourceForge at https://git.code.sf.net/p/fuse-emulator/libspectrum. The project also maintains a mirror at https://github.com/fuse-emulator/libspectrum which is already used when checking out the repo in the dev environment. Switch all three libspectrum build sub-workflows to use the GitHub URL. GitHub's infrastructure is typically more reliable and faster than SourceForge for shallow git clones, which should reduce sporadic CI failures caused by SourceForge availability issues. --- .github/workflows/build_libspectrum_linux_sub.yml | 2 +- .github/workflows/build_libspectrum_macos_sub.yml | 2 +- .github/workflows/build_libspectrum_windows_sub.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_libspectrum_linux_sub.yml b/.github/workflows/build_libspectrum_linux_sub.yml index f905d8e0..b2c3ae88 100644 --- a/.github/workflows/build_libspectrum_linux_sub.yml +++ b/.github/workflows/build_libspectrum_linux_sub.yml @@ -42,7 +42,7 @@ jobs: shell: bash env: LIBSPECTRUM_DIR: libspectrum - LIBSPECTRUM_GIT_URL: https://git.code.sf.net/p/fuse-emulator/libspectrum + LIBSPECTRUM_GIT_URL: https://github.com/fuse-emulator/libspectrum steps: - name: (1) Prepare environment diff --git a/.github/workflows/build_libspectrum_macos_sub.yml b/.github/workflows/build_libspectrum_macos_sub.yml index ea4f4c4b..db3ab346 100644 --- a/.github/workflows/build_libspectrum_macos_sub.yml +++ b/.github/workflows/build_libspectrum_macos_sub.yml @@ -38,7 +38,7 @@ jobs: shell: bash env: LIBSPECTRUM_DIR: libspectrum - LIBSPECTRUM_GIT_URL: https://git.code.sf.net/p/fuse-emulator/libspectrum + LIBSPECTRUM_GIT_URL: https://github.com/fuse-emulator/libspectrum steps: - name: (1) Prepare environment diff --git a/.github/workflows/build_libspectrum_windows_sub.yml b/.github/workflows/build_libspectrum_windows_sub.yml index 1689ec79..69be2f77 100644 --- a/.github/workflows/build_libspectrum_windows_sub.yml +++ b/.github/workflows/build_libspectrum_windows_sub.yml @@ -43,7 +43,7 @@ jobs: PKG_CONFIG_LIBDIR: "/usr/local/i686-w64-mingw32/lib/pkgconfig:/usr/i686-w64-mingw32/sys-root/mingw/lib/pkgconfig" CYGWIN_LIBDIR: "/usr/i686-w64-mingw32/sys-root/mingw/bin/" LIBSPECTRUM_DIR: libspectrum - LIBSPECTRUM_GIT_URL: https://git.code.sf.net/p/fuse-emulator/libspectrum + LIBSPECTRUM_GIT_URL: https://github.com/fuse-emulator/libspectrum steps: - name: (1) Prepare environment From 54de01dc76719982337aa1ca04f64461d8f8e6f4 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 15:51:43 +1000 Subject: [PATCH 089/142] Remove deprecated AC_C_CONST and AC_C_INLINE macros from configure.ac Both AC_C_CONST and AC_C_INLINE were deprecated in autoconf 2.70 (November 2020). They provided compatibility shims for compilers lacking C99 and earlier 'const' and 'inline' keywords respectively. No HAVE_C_CONST or HAVE_C_INLINE guard is tested anywhere in the Fuse codebase. C99 guarantees both keywords, and every platform targeted by Fuse (Linux, macOS, Windows/Cygwin/MinGW, Wii) uses a C99-capable compiler, making these macros no-ops in practice. --- configure.ac | 4 ---- 1 file changed, 4 deletions(-) diff --git a/configure.ac b/configure.ac index 548c8881..b5b7ba8b 100644 --- a/configure.ac +++ b/configure.ac @@ -111,10 +111,6 @@ AC_CHECK_HEADERS( sys/audioio.h ) -dnl Checks for typedefs, structures, and compiler characteristics. -AC_C_CONST -AC_C_INLINE - dnl Checks for library functions. AC_CHECK_FUNCS(dirname geteuid getopt_long fsync) AC_CHECK_LIB([m],[cos]) From dc3f1fd12cadd583444615b6275b409c075870ea Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 16:04:57 +1000 Subject: [PATCH 090/142] Remove unused AX_CREATE_STDINT_H macro file AX_CREATE_STDINT_H was added in 2015 but AX_CREATE_STDINT_H has never been called from configure.ac. The macro synthesises a _stdint.h for platforms that lack - a pre-C99 concern. All platforms Fuse targets today (Linux, macOS, Windows/Cygwin/MinGW, Wii) provide as part of C99. Removes the 727-line m4/ax_create_stdint_h.m4 file and its entry from EXTRA_DIST in Makefile.am. --- Makefile.am | 1 - m4/ax_create_stdint_h.m4 | 727 --------------------------------------- 2 files changed, 728 deletions(-) delete mode 100644 m4/ax_create_stdint_h.m4 diff --git a/Makefile.am b/Makefile.am index 971c0526..96543304 100644 --- a/Makefile.am +++ b/Makefile.am @@ -146,7 +146,6 @@ EXTRA_DIST = AUTHORS \ THANKS \ keysyms.dat \ keysyms.pl \ - m4/ax_create_stdint_h.m4 \ m4/ax_pthread.m4 \ m4/ax_string_strcasecmp.m4 \ m4/pkg.m4 \ diff --git a/m4/ax_create_stdint_h.m4 b/m4/ax_create_stdint_h.m4 deleted file mode 100644 index 33a21f8e..00000000 --- a/m4/ax_create_stdint_h.m4 +++ /dev/null @@ -1,727 +0,0 @@ -# =========================================================================== -# http://www.gnu.org/software/autoconf-archive/ax_create_stdint_h.html -# =========================================================================== -# -# SYNOPSIS -# -# AX_CREATE_STDINT_H [( HEADER-TO-GENERATE [, HEDERS-TO-CHECK])] -# -# DESCRIPTION -# -# the "ISO C9X: 7.18 Integer types " section requires the -# existence of an include file that defines a set of typedefs, -# especially uint8_t,int32_t,uintptr_t. Many older installations will not -# provide this file, but some will have the very same definitions in -# . In other enviroments we can use the inet-types in -# which would define the typedefs int8_t and u_int8_t -# respectivly. -# -# This macros will create a local "_stdint.h" or the headerfile given as -# an argument. In many cases that file will just "#include " or -# "#include ", while in other environments it will provide the -# set of basic 'stdint's definitions/typedefs: -# -# int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t,intptr_t,uintptr_t -# int_least32_t.. int_fast32_t.. intmax_t -# -# which may or may not rely on the definitions of other files, or using -# the AC_CHECK_SIZEOF macro to determine the actual sizeof each type. -# -# if your header files require the stdint-types you will want to create an -# installable file mylib-int.h that all your other installable header may -# include. So if you have a library package named "mylib", just use -# -# AX_CREATE_STDINT_H(mylib-int.h) -# -# in configure.ac and go to install that very header file in Makefile.am -# along with the other headers (mylib.h) - and the mylib-specific headers -# can simply use "#include " to obtain the stdint-types. -# -# Remember, if the system already had a valid , the generated -# file will include it directly. No need for fuzzy HAVE_STDINT_H things... -# (oops, GCC 4.2.x has deliberatly disabled its stdint.h for non-c99 -# compilation and the c99-mode is not the default. Therefore this macro -# will not use the compiler's stdint.h - please complain to the GCC -# developers). -# -# LICENSE -# -# Copyright (c) 2008 Guido U. Draheim -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 3 of the License, or (at your -# option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see . -# -# As a special exception, the respective Autoconf Macro's copyright owner -# gives unlimited permission to copy, distribute and modify the configure -# scripts that are the output of Autoconf when processing the Macro. You -# need not follow the terms of the GNU General Public License when using -# or distributing such scripts, even though portions of the text of the -# Macro appear in them. The GNU General Public License (GPL) does govern -# all other use of the material that constitutes the Autoconf Macro. -# -# This special exception to the GPL applies to versions of the Autoconf -# Macro released by the Autoconf Archive. When you make and distribute a -# modified version of the Autoconf Macro, you may extend this special -# exception to the GPL to apply to your modified version as well. - -#serial 10 - -AC_DEFUN([AX_CHECK_DATA_MODEL],[ - AC_CHECK_SIZEOF(char) - AC_CHECK_SIZEOF(short) - AC_CHECK_SIZEOF(int) - AC_CHECK_SIZEOF(long) - AC_CHECK_SIZEOF(void*) - ac_cv_char_data_model="" - ac_cv_char_data_model="$ac_cv_char_data_model$ac_cv_sizeof_char" - ac_cv_char_data_model="$ac_cv_char_data_model$ac_cv_sizeof_short" - ac_cv_char_data_model="$ac_cv_char_data_model$ac_cv_sizeof_int" - ac_cv_long_data_model="" - ac_cv_long_data_model="$ac_cv_long_data_model$ac_cv_sizeof_int" - ac_cv_long_data_model="$ac_cv_long_data_model$ac_cv_sizeof_long" - ac_cv_long_data_model="$ac_cv_long_data_model$ac_cv_sizeof_voidp" - AC_MSG_CHECKING([data model]) - case "$ac_cv_char_data_model/$ac_cv_long_data_model" in - 122/242) ac_cv_data_model="IP16" ; n="standard 16bit machine" ;; - 122/244) ac_cv_data_model="LP32" ; n="standard 32bit machine" ;; - 122/*) ac_cv_data_model="i16" ; n="unusual int16 model" ;; - 124/444) ac_cv_data_model="ILP32" ; n="standard 32bit unixish" ;; - 124/488) ac_cv_data_model="LP64" ; n="standard 64bit unixish" ;; - 124/448) ac_cv_data_model="LLP64" ; n="unusual 64bit unixish" ;; - 124/*) ac_cv_data_model="i32" ; n="unusual int32 model" ;; - 128/888) ac_cv_data_model="ILP64" ; n="unusual 64bit numeric" ;; - 128/*) ac_cv_data_model="i64" ; n="unusual int64 model" ;; - 222/*2) ac_cv_data_model="DSP16" ; n="strict 16bit dsptype" ;; - 333/*3) ac_cv_data_model="DSP24" ; n="strict 24bit dsptype" ;; - 444/*4) ac_cv_data_model="DSP32" ; n="strict 32bit dsptype" ;; - 666/*6) ac_cv_data_model="DSP48" ; n="strict 48bit dsptype" ;; - 888/*8) ac_cv_data_model="DSP64" ; n="strict 64bit dsptype" ;; - 222/*|333/*|444/*|666/*|888/*) : - ac_cv_data_model="iDSP" ; n="unusual dsptype" ;; - *) ac_cv_data_model="none" ; n="very unusual model" ;; - esac - AC_MSG_RESULT([$ac_cv_data_model ($ac_cv_long_data_model, $n)]) -]) - -dnl AX_CHECK_HEADER_STDINT_X([HEADERLIST][,ACTION-IF]) -AC_DEFUN([AX_CHECK_HEADER_STDINT_X],[ -AC_CACHE_CHECK([for stdint uintptr_t], [ac_cv_header_stdint_x],[ - ac_cv_header_stdint_x="" # the 1997 typedefs (inttypes.h) - AC_MSG_RESULT([(..)]) - for i in m4_ifval([$1],[$1],[stdint.h inttypes.h sys/inttypes.h sys/types.h]) - do - unset ac_cv_type_uintptr_t - unset ac_cv_type_uint64_t - AC_CHECK_TYPE(uintptr_t,[ac_cv_header_stdint_x=$i],continue,[#include <$i>]) - AC_CHECK_TYPE(uint64_t,[and64="/uint64_t"],[and64=""],[#include<$i>]) - m4_ifvaln([$2],[$2]) break - done - AC_MSG_CHECKING([for stdint uintptr_t]) - ]) -]) - -AC_DEFUN([AX_CHECK_HEADER_STDINT_O],[ -AC_CACHE_CHECK([for stdint uint32_t], [ac_cv_header_stdint_o],[ - ac_cv_header_stdint_o="" # the 1995 typedefs (sys/inttypes.h) - AC_MSG_RESULT([(..)]) - for i in m4_ifval([$1],[$1],[inttypes.h sys/inttypes.h sys/types.h stdint.h]) - do - unset ac_cv_type_uint32_t - unset ac_cv_type_uint64_t - AC_CHECK_TYPE(uint32_t,[ac_cv_header_stdint_o=$i],continue,[#include <$i>]) - AC_CHECK_TYPE(uint64_t,[and64="/uint64_t"],[and64=""],[#include<$i>]) - m4_ifvaln([$2],[$2]) break - break; - done - AC_MSG_CHECKING([for stdint uint32_t]) - ]) -]) - -AC_DEFUN([AX_CHECK_HEADER_STDINT_U],[ -AC_CACHE_CHECK([for stdint u_int32_t], [ac_cv_header_stdint_u],[ - ac_cv_header_stdint_u="" # the BSD typedefs (sys/types.h) - AC_MSG_RESULT([(..)]) - for i in m4_ifval([$1],[$1],[sys/types.h inttypes.h sys/inttypes.h]) ; do - unset ac_cv_type_u_int32_t - unset ac_cv_type_u_int64_t - AC_CHECK_TYPE(u_int32_t,[ac_cv_header_stdint_u=$i],continue,[#include <$i>]) - AC_CHECK_TYPE(u_int64_t,[and64="/u_int64_t"],[and64=""],[#include<$i>]) - m4_ifvaln([$2],[$2]) break - break; - done - AC_MSG_CHECKING([for stdint u_int32_t]) - ]) -]) - -AC_DEFUN([AX_CREATE_STDINT_H], -[# ------ AX CREATE STDINT H ------------------------------------- -AC_MSG_CHECKING([for stdint types]) -ac_stdint_h=`echo ifelse($1, , _stdint.h, $1)` -# try to shortcircuit - if the default include path of the compiler -# can find a "stdint.h" header then we assume that all compilers can. -AC_CACHE_VAL([ac_cv_header_stdint_t],[ -old_CXXFLAGS="$CXXFLAGS" ; CXXFLAGS="" -old_CPPFLAGS="$CPPFLAGS" ; CPPFLAGS="" -old_CFLAGS="$CFLAGS" ; CFLAGS="" -AC_TRY_COMPILE([#include ],[int_least32_t v = 0;], -[ac_cv_stdint_result="(assuming C99 compatible system)" - ac_cv_header_stdint_t="stdint.h"; ], -[ac_cv_header_stdint_t=""]) -if test "$GCC" = "yes" && test ".$ac_cv_header_stdint_t" = "."; then -CFLAGS="-std=c99" -AC_TRY_COMPILE([#include ],[int_least32_t v = 0;], -[AC_MSG_WARN(your GCC compiler has a defunct stdint.h for its default-mode)]) -fi -CXXFLAGS="$old_CXXFLAGS" -CPPFLAGS="$old_CPPFLAGS" -CFLAGS="$old_CFLAGS" ]) - -v="... $ac_cv_header_stdint_h" -if test "$ac_stdint_h" = "stdint.h" ; then - AC_MSG_RESULT([(are you sure you want them in ./stdint.h?)]) -elif test "$ac_stdint_h" = "inttypes.h" ; then - AC_MSG_RESULT([(are you sure you want them in ./inttypes.h?)]) -elif test "_$ac_cv_header_stdint_t" = "_" ; then - AC_MSG_RESULT([(putting them into $ac_stdint_h)$v]) -else - ac_cv_header_stdint="$ac_cv_header_stdint_t" - AC_MSG_RESULT([$ac_cv_header_stdint (shortcircuit)]) -fi - -if test "_$ac_cv_header_stdint_t" = "_" ; then # cannot shortcircuit.. - -dnl .....intro message done, now do a few system checks..... -dnl btw, all old CHECK_TYPE macros do automatically "DEFINE" a type, -dnl therefore we use the autoconf implementation detail CHECK_TYPE_NEW -dnl instead that is triggered with 3 or more arguments (see types.m4) - -inttype_headers=`echo $2 | sed -e 's/,/ /g'` - -ac_cv_stdint_result="(no helpful system typedefs seen)" -AX_CHECK_HEADER_STDINT_X(dnl - stdint.h inttypes.h sys/inttypes.h $inttype_headers, - ac_cv_stdint_result="(seen uintptr_t$and64 in $i)") - -if test "_$ac_cv_header_stdint_x" = "_" ; then -AX_CHECK_HEADER_STDINT_O(dnl, - inttypes.h sys/inttypes.h stdint.h $inttype_headers, - ac_cv_stdint_result="(seen uint32_t$and64 in $i)") -fi - -if test "_$ac_cv_header_stdint_x" = "_" ; then -if test "_$ac_cv_header_stdint_o" = "_" ; then -AX_CHECK_HEADER_STDINT_U(dnl, - sys/types.h inttypes.h sys/inttypes.h $inttype_headers, - ac_cv_stdint_result="(seen u_int32_t$and64 in $i)") -fi fi - -dnl if there was no good C99 header file, do some typedef checks... -if test "_$ac_cv_header_stdint_x" = "_" ; then - AC_MSG_CHECKING([for stdint datatype model]) - AC_MSG_RESULT([(..)]) - AX_CHECK_DATA_MODEL -fi - -if test "_$ac_cv_header_stdint_x" != "_" ; then - ac_cv_header_stdint="$ac_cv_header_stdint_x" -elif test "_$ac_cv_header_stdint_o" != "_" ; then - ac_cv_header_stdint="$ac_cv_header_stdint_o" -elif test "_$ac_cv_header_stdint_u" != "_" ; then - ac_cv_header_stdint="$ac_cv_header_stdint_u" -else - ac_cv_header_stdint="stddef.h" -fi - -AC_MSG_CHECKING([for extra inttypes in chosen header]) -AC_MSG_RESULT([($ac_cv_header_stdint)]) -dnl see if int_least and int_fast types are present in _this_ header. -unset ac_cv_type_int_least32_t -unset ac_cv_type_int_fast32_t -AC_CHECK_TYPE(int_least32_t,,,[#include <$ac_cv_header_stdint>]) -AC_CHECK_TYPE(int_fast32_t,,,[#include<$ac_cv_header_stdint>]) -AC_CHECK_TYPE(intmax_t,,,[#include <$ac_cv_header_stdint>]) - -fi # shortcircut to system "stdint.h" -# ------------------ PREPARE VARIABLES ------------------------------ -if test "$GCC" = "yes" ; then -ac_cv_stdint_message="using gnu compiler "`$CC --version | head -1` -else -ac_cv_stdint_message="using $CC" -fi - -AC_MSG_RESULT([make use of $ac_cv_header_stdint in $ac_stdint_h dnl -$ac_cv_stdint_result]) - -dnl ----------------------------------------------------------------- -# ----------------- DONE inttypes.h checks START header ------------- -AC_CONFIG_COMMANDS([$ac_stdint_h],[ -AC_MSG_NOTICE(creating $ac_stdint_h : $_ac_stdint_h) -ac_stdint=$tmp/_stdint.h - -echo "#ifndef" $_ac_stdint_h >$ac_stdint -echo "#define" $_ac_stdint_h "1" >>$ac_stdint -echo "#ifndef" _GENERATED_STDINT_H >>$ac_stdint -echo "#define" _GENERATED_STDINT_H '"'$PACKAGE $VERSION'"' >>$ac_stdint -echo "/* generated $ac_cv_stdint_message */" >>$ac_stdint -if test "_$ac_cv_header_stdint_t" != "_" ; then -echo "#define _STDINT_HAVE_STDINT_H" "1" >>$ac_stdint -echo "#include " >>$ac_stdint -echo "#endif" >>$ac_stdint -echo "#endif" >>$ac_stdint -else - -cat >>$ac_stdint < -#else -#include - -/* .................... configured part ............................ */ - -STDINT_EOF - -echo "/* whether we have a C99 compatible stdint header file */" >>$ac_stdint -if test "_$ac_cv_header_stdint_x" != "_" ; then - ac_header="$ac_cv_header_stdint_x" - echo "#define _STDINT_HEADER_INTPTR" '"'"$ac_header"'"' >>$ac_stdint -else - echo "/* #undef _STDINT_HEADER_INTPTR */" >>$ac_stdint -fi - -echo "/* whether we have a C96 compatible inttypes header file */" >>$ac_stdint -if test "_$ac_cv_header_stdint_o" != "_" ; then - ac_header="$ac_cv_header_stdint_o" - echo "#define _STDINT_HEADER_UINT32" '"'"$ac_header"'"' >>$ac_stdint -else - echo "/* #undef _STDINT_HEADER_UINT32 */" >>$ac_stdint -fi - -echo "/* whether we have a BSD compatible inet types header */" >>$ac_stdint -if test "_$ac_cv_header_stdint_u" != "_" ; then - ac_header="$ac_cv_header_stdint_u" - echo "#define _STDINT_HEADER_U_INT32" '"'"$ac_header"'"' >>$ac_stdint -else - echo "/* #undef _STDINT_HEADER_U_INT32 */" >>$ac_stdint -fi - -echo "" >>$ac_stdint - -if test "_$ac_header" != "_" ; then if test "$ac_header" != "stddef.h" ; then - echo "#include <$ac_header>" >>$ac_stdint - echo "" >>$ac_stdint -fi fi - -echo "/* which 64bit typedef has been found */" >>$ac_stdint -if test "$ac_cv_type_uint64_t" = "yes" ; then -echo "#define _STDINT_HAVE_UINT64_T" "1" >>$ac_stdint -else -echo "/* #undef _STDINT_HAVE_UINT64_T */" >>$ac_stdint -fi -if test "$ac_cv_type_u_int64_t" = "yes" ; then -echo "#define _STDINT_HAVE_U_INT64_T" "1" >>$ac_stdint -else -echo "/* #undef _STDINT_HAVE_U_INT64_T */" >>$ac_stdint -fi -echo "" >>$ac_stdint - -echo "/* which type model has been detected */" >>$ac_stdint -if test "_$ac_cv_char_data_model" != "_" ; then -echo "#define _STDINT_CHAR_MODEL" "$ac_cv_char_data_model" >>$ac_stdint -echo "#define _STDINT_LONG_MODEL" "$ac_cv_long_data_model" >>$ac_stdint -else -echo "/* #undef _STDINT_CHAR_MODEL // skipped */" >>$ac_stdint -echo "/* #undef _STDINT_LONG_MODEL // skipped */" >>$ac_stdint -fi -echo "" >>$ac_stdint - -echo "/* whether int_least types were detected */" >>$ac_stdint -if test "$ac_cv_type_int_least32_t" = "yes"; then -echo "#define _STDINT_HAVE_INT_LEAST32_T" "1" >>$ac_stdint -else -echo "/* #undef _STDINT_HAVE_INT_LEAST32_T */" >>$ac_stdint -fi -echo "/* whether int_fast types were detected */" >>$ac_stdint -if test "$ac_cv_type_int_fast32_t" = "yes"; then -echo "#define _STDINT_HAVE_INT_FAST32_T" "1" >>$ac_stdint -else -echo "/* #undef _STDINT_HAVE_INT_FAST32_T */" >>$ac_stdint -fi -echo "/* whether intmax_t type was detected */" >>$ac_stdint -if test "$ac_cv_type_intmax_t" = "yes"; then -echo "#define _STDINT_HAVE_INTMAX_T" "1" >>$ac_stdint -else -echo "/* #undef _STDINT_HAVE_INTMAX_T */" >>$ac_stdint -fi -echo "" >>$ac_stdint - - cat >>$ac_stdint <= 199901L -#define _HAVE_UINT64_T -#define _HAVE_LONGLONG_UINT64_T -typedef long long int64_t; -typedef unsigned long long uint64_t; - -#elif !defined __STRICT_ANSI__ -#if defined _MSC_VER || defined __WATCOMC__ || defined __BORLANDC__ -#define _HAVE_UINT64_T -typedef __int64 int64_t; -typedef unsigned __int64 uint64_t; - -#elif defined __GNUC__ || defined __MWERKS__ || defined __ELF__ -/* note: all ELF-systems seem to have loff-support which needs 64-bit */ -#if !defined _NO_LONGLONG -#define _HAVE_UINT64_T -#define _HAVE_LONGLONG_UINT64_T -typedef long long int64_t; -typedef unsigned long long uint64_t; -#endif - -#elif defined __alpha || (defined __mips && defined _ABIN32) -#if !defined _NO_LONGLONG -typedef long int64_t; -typedef unsigned long uint64_t; -#endif - /* compiler/cpu type to define int64_t */ -#endif -#endif -#endif - -#if defined _STDINT_HAVE_U_INT_TYPES -/* int8_t int16_t int32_t defined by inet code, redeclare the u_intXX types */ -typedef u_int8_t uint8_t; -typedef u_int16_t uint16_t; -typedef u_int32_t uint32_t; - -/* glibc compatibility */ -#ifndef __int8_t_defined -#define __int8_t_defined -#endif -#endif - -#ifdef _STDINT_NEED_INT_MODEL_T -/* we must guess all the basic types. Apart from byte-adressable system, */ -/* there a few 32-bit-only dsp-systems that we guard with BYTE_MODEL 8-} */ -/* (btw, those nibble-addressable systems are way off, or so we assume) */ - -dnl /* have a look at "64bit and data size neutrality" at */ -dnl /* http://unix.org/version2/whatsnew/login_64bit.html */ -dnl /* (the shorthand "ILP" types always have a "P" part) */ - -#if defined _STDINT_BYTE_MODEL -#if _STDINT_LONG_MODEL+0 == 242 -/* 2:4:2 = IP16 = a normal 16-bit system */ -typedef unsigned char uint8_t; -typedef unsigned short uint16_t; -typedef unsigned long uint32_t; -#ifndef __int8_t_defined -#define __int8_t_defined -typedef char int8_t; -typedef short int16_t; -typedef long int32_t; -#endif -#elif _STDINT_LONG_MODEL+0 == 244 || _STDINT_LONG_MODEL == 444 -/* 2:4:4 = LP32 = a 32-bit system derived from a 16-bit */ -/* 4:4:4 = ILP32 = a normal 32-bit system */ -typedef unsigned char uint8_t; -typedef unsigned short uint16_t; -typedef unsigned int uint32_t; -#ifndef __int8_t_defined -#define __int8_t_defined -typedef char int8_t; -typedef short int16_t; -typedef int int32_t; -#endif -#elif _STDINT_LONG_MODEL+0 == 484 || _STDINT_LONG_MODEL+0 == 488 -/* 4:8:4 = IP32 = a 32-bit system prepared for 64-bit */ -/* 4:8:8 = LP64 = a normal 64-bit system */ -typedef unsigned char uint8_t; -typedef unsigned short uint16_t; -typedef unsigned int uint32_t; -#ifndef __int8_t_defined -#define __int8_t_defined -typedef char int8_t; -typedef short int16_t; -typedef int int32_t; -#endif -/* this system has a "long" of 64bit */ -#ifndef _HAVE_UINT64_T -#define _HAVE_UINT64_T -typedef unsigned long uint64_t; -typedef long int64_t; -#endif -#elif _STDINT_LONG_MODEL+0 == 448 -/* LLP64 a 64-bit system derived from a 32-bit system */ -typedef unsigned char uint8_t; -typedef unsigned short uint16_t; -typedef unsigned int uint32_t; -#ifndef __int8_t_defined -#define __int8_t_defined -typedef char int8_t; -typedef short int16_t; -typedef int int32_t; -#endif -/* assuming the system has a "long long" */ -#ifndef _HAVE_UINT64_T -#define _HAVE_UINT64_T -#define _HAVE_LONGLONG_UINT64_T -typedef unsigned long long uint64_t; -typedef long long int64_t; -#endif -#else -#define _STDINT_NO_INT32_T -#endif -#else -#define _STDINT_NO_INT8_T -#define _STDINT_NO_INT32_T -#endif -#endif - -/* - * quote from SunOS-5.8 sys/inttypes.h: - * Use at your own risk. As of February 1996, the committee is squarely - * behind the fixed sized types; the "least" and "fast" types are still being - * discussed. The probability that the "fast" types may be removed before - * the standard is finalized is high enough that they are not currently - * implemented. - */ - -#if defined _STDINT_NEED_INT_LEAST_T -typedef int8_t int_least8_t; -typedef int16_t int_least16_t; -typedef int32_t int_least32_t; -#ifdef _HAVE_UINT64_T -typedef int64_t int_least64_t; -#endif - -typedef uint8_t uint_least8_t; -typedef uint16_t uint_least16_t; -typedef uint32_t uint_least32_t; -#ifdef _HAVE_UINT64_T -typedef uint64_t uint_least64_t; -#endif - /* least types */ -#endif - -#if defined _STDINT_NEED_INT_FAST_T -typedef int8_t int_fast8_t; -typedef int int_fast16_t; -typedef int32_t int_fast32_t; -#ifdef _HAVE_UINT64_T -typedef int64_t int_fast64_t; -#endif - -typedef uint8_t uint_fast8_t; -typedef unsigned uint_fast16_t; -typedef uint32_t uint_fast32_t; -#ifdef _HAVE_UINT64_T -typedef uint64_t uint_fast64_t; -#endif - /* fast types */ -#endif - -#ifdef _STDINT_NEED_INTMAX_T -#ifdef _HAVE_UINT64_T -typedef int64_t intmax_t; -typedef uint64_t uintmax_t; -#else -typedef long intmax_t; -typedef unsigned long uintmax_t; -#endif -#endif - -#ifdef _STDINT_NEED_INTPTR_T -#ifndef __intptr_t_defined -#define __intptr_t_defined -/* we encourage using "long" to store pointer values, never use "int" ! */ -#if _STDINT_LONG_MODEL+0 == 242 || _STDINT_LONG_MODEL+0 == 484 -typedef unsigned int uintptr_t; -typedef int intptr_t; -#elif _STDINT_LONG_MODEL+0 == 244 || _STDINT_LONG_MODEL+0 == 444 -typedef unsigned long uintptr_t; -typedef long intptr_t; -#elif _STDINT_LONG_MODEL+0 == 448 && defined _HAVE_UINT64_T -typedef uint64_t uintptr_t; -typedef int64_t intptr_t; -#else /* matches typical system types ILP32 and LP64 - but not IP16 or LLP64 */ -typedef unsigned long uintptr_t; -typedef long intptr_t; -#endif -#endif -#endif - -/* The ISO C99 standard specifies that in C++ implementations these - should only be defined if explicitly requested. */ -#if !defined __cplusplus || defined __STDC_CONSTANT_MACROS -#ifndef UINT32_C - -/* Signed. */ -# define INT8_C(c) c -# define INT16_C(c) c -# define INT32_C(c) c -# ifdef _HAVE_LONGLONG_UINT64_T -# define INT64_C(c) c ## L -# else -# define INT64_C(c) c ## LL -# endif - -/* Unsigned. */ -# define UINT8_C(c) c ## U -# define UINT16_C(c) c ## U -# define UINT32_C(c) c ## U -# ifdef _HAVE_LONGLONG_UINT64_T -# define UINT64_C(c) c ## UL -# else -# define UINT64_C(c) c ## ULL -# endif - -/* Maximal type. */ -# ifdef _HAVE_LONGLONG_UINT64_T -# define INTMAX_C(c) c ## L -# define UINTMAX_C(c) c ## UL -# else -# define INTMAX_C(c) c ## LL -# define UINTMAX_C(c) c ## ULL -# endif - - /* literalnumbers */ -#endif -#endif - -/* These limits are merily those of a two complement byte-oriented system */ - -/* Minimum of signed integral types. */ -# define INT8_MIN (-128) -# define INT16_MIN (-32767-1) -# define INT32_MIN (-2147483647-1) -# define INT64_MIN (-__INT64_C(9223372036854775807)-1) -/* Maximum of signed integral types. */ -# define INT8_MAX (127) -# define INT16_MAX (32767) -# define INT32_MAX (2147483647) -# define INT64_MAX (__INT64_C(9223372036854775807)) - -/* Maximum of unsigned integral types. */ -# define UINT8_MAX (255) -# define UINT16_MAX (65535) -# define UINT32_MAX (4294967295U) -# define UINT64_MAX (__UINT64_C(18446744073709551615)) - -/* Minimum of signed integral types having a minimum size. */ -# define INT_LEAST8_MIN INT8_MIN -# define INT_LEAST16_MIN INT16_MIN -# define INT_LEAST32_MIN INT32_MIN -# define INT_LEAST64_MIN INT64_MIN -/* Maximum of signed integral types having a minimum size. */ -# define INT_LEAST8_MAX INT8_MAX -# define INT_LEAST16_MAX INT16_MAX -# define INT_LEAST32_MAX INT32_MAX -# define INT_LEAST64_MAX INT64_MAX - -/* Maximum of unsigned integral types having a minimum size. */ -# define UINT_LEAST8_MAX UINT8_MAX -# define UINT_LEAST16_MAX UINT16_MAX -# define UINT_LEAST32_MAX UINT32_MAX -# define UINT_LEAST64_MAX UINT64_MAX - - /* shortcircuit*/ -#endif - /* once */ -#endif -#endif -STDINT_EOF -fi - if cmp -s $ac_stdint_h $ac_stdint 2>/dev/null; then - AC_MSG_NOTICE([$ac_stdint_h is unchanged]) - else - ac_dir=`AS_DIRNAME(["$ac_stdint_h"])` - AS_MKDIR_P(["$ac_dir"]) - rm -f $ac_stdint_h - mv $ac_stdint $ac_stdint_h - fi -],[# variables for create stdint.h replacement -PACKAGE="$PACKAGE" -VERSION="$VERSION" -ac_stdint_h="$ac_stdint_h" -_ac_stdint_h=AS_TR_CPP(_$PACKAGE-$ac_stdint_h) -ac_cv_stdint_message="$ac_cv_stdint_message" -ac_cv_header_stdint_t="$ac_cv_header_stdint_t" -ac_cv_header_stdint_x="$ac_cv_header_stdint_x" -ac_cv_header_stdint_o="$ac_cv_header_stdint_o" -ac_cv_header_stdint_u="$ac_cv_header_stdint_u" -ac_cv_type_uint64_t="$ac_cv_type_uint64_t" -ac_cv_type_u_int64_t="$ac_cv_type_u_int64_t" -ac_cv_char_data_model="$ac_cv_char_data_model" -ac_cv_long_data_model="$ac_cv_long_data_model" -ac_cv_type_int_least32_t="$ac_cv_type_int_least32_t" -ac_cv_type_int_fast32_t="$ac_cv_type_int_fast32_t" -ac_cv_type_intmax_t="$ac_cv_type_intmax_t" -]) -]) From 54c562af6fdeb40cd2ee07e0bc89abd14b329e6d Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 22:30:44 +1000 Subject: [PATCH 091/142] Replace magic numbers with named constants in tape.c Replace hard-coded literals in tape.c with self-documenting named constants: TAPE_ROM_HEADER_LEN - length of a ROM tape header block (19 bytes: 1 flag + 1 type + 10 name + 2 data-len + 2 param1 + 2 param2 + 1 parity) TAPE_ROM_HEADER_FLAG - flag byte that identifies a header block (0x00) TAPE_ROM_SAVE_PAUSE_MS - pause after each ROM-save block (1000 ms) TAPE_RECORDING_SAMPLE_RATE - tape recording sample rate (44100 Hz) TAPE_RECORDING_BUFFER_SIZE - initial tape recording buffer (8192 bytes) --- tape.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tape.c b/tape.c index f1eb7586..c70ed754 100644 --- a/tape.c +++ b/tape.c @@ -53,6 +53,20 @@ #include "z80/z80.h" #include "z80/z80_macros.h" +/* Length of a standard ZX Spectrum ROM tape header block (flag byte + + 1 type + 10 name + 2 length + 2 param1 + 2 param2 + 1 parity = 19 bytes) */ +#define TAPE_ROM_HEADER_LEN 19 + +/* Flag byte for a ZX Spectrum ROM tape header block */ +#define TAPE_ROM_HEADER_FLAG 0x00 + +/* Pause appended after each ROM-routine tape-save block (milliseconds) */ +#define TAPE_ROM_SAVE_PAUSE_MS 1000 + +/* Sample rate and initial buffer size for tape recording */ +#define TAPE_RECORDING_SAMPLE_RATE 44100 +#define TAPE_RECORDING_BUFFER_SIZE 8192 + /* The current tape */ static libspectrum_tape *tape; @@ -234,8 +248,8 @@ does_tape_load_with_code( void ) block_length = libspectrum_tape_block_data_length( block ); data = libspectrum_tape_block_data( block ); needs_code = - (block_length == 19) && - (data[0] == 0x00) && + (block_length == TAPE_ROM_HEADER_LEN) && + (data[0] == TAPE_ROM_HEADER_FLAG) && (data[1] == 0x03); /* Stop looking now - either we found an appropriate block or we found @@ -614,7 +628,7 @@ tape_save_trap( void ) data[ DE+1 ] = parity; /* Give a 1 second pause after this block */ - libspectrum_tape_block_set_pause( block, 1000 ); + libspectrum_tape_block_set_pause( block, TAPE_ROM_SAVE_PAUSE_MS ); libspectrum_tape_append_block( tape, block ); @@ -759,9 +773,9 @@ tape_record_start( void ) { /* sample rate will be 44.1KHz */ rec_state.tstates_per_sample = - machine_current->timings.processor_speed/44100; + machine_current->timings.processor_speed/TAPE_RECORDING_SAMPLE_RATE; - rec_state.tape_buffer_size = 8192; + rec_state.tape_buffer_size = TAPE_RECORDING_BUFFER_SIZE; rec_state.tape_buffer = libspectrum_new(libspectrum_byte, rec_state.tape_buffer_size); rec_state.tape_buffer_used = 0; @@ -991,12 +1005,12 @@ tape_block_details( char *buffer, size_t length, case LIBSPECTRUM_TAPE_BLOCK_DATA_BLOCK: /* See if this looks like a standard Spectrum header and if so display some extra data */ - if( libspectrum_tape_block_data_length( block ) != 19 ) goto normal; + if( libspectrum_tape_block_data_length( block ) != TAPE_ROM_HEADER_LEN ) goto normal; data = libspectrum_tape_block_data( block ); - /* Flag byte is 0x00 for headers */ - if( data[0] != 0x00 ) goto normal; + /* Flag byte is TAPE_ROM_HEADER_FLAG (0x00) for headers */ + if( data[0] != TAPE_ROM_HEADER_FLAG ) goto normal; switch( data[1] ) { case 0x00: type = "Program"; break; From 71ee8af410eabf644d1dc8f0ca5d5acbbc785189 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 22:43:10 +1000 Subject: [PATCH 092/142] Cache adjusted/clamped beam coords in display_update_critical() The existing beam-position cache keyed on tstates avoids the get_beam_position() division on cache hits, but the border-offset subtractions and clamping branches were still performed on every call. Move the adjustment and clamping inside the cache-miss branch and store the final screen coordinates in display_cached_screen_x/y. Cache hits (roughly 7 of every 8 attribute-write calls, all with the same tstates) now read the pre-computed values directly. --- display.c | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/display.c b/display.c index 9e9ccc7c..154e31cd 100644 --- a/display.c +++ b/display.c @@ -104,9 +104,10 @@ static int critical_region_x = 0, critical_region_y = 0; /* Cache the most-recently computed beam position keyed on tstates. get_beam_position() is called once per dirty screen write, and attribute writes trigger eight consecutive calls all at the same tstates value. - Skipping the division when tstates hasn't changed saves ~7/8 of those. */ + Caching the fully adjusted/clamped screen coordinates avoids the border + subtraction and clamping branches on each cache hit (~7/8 of calls). */ static libspectrum_dword display_cached_beam_tstates = (libspectrum_dword)-1; -static int display_cached_beam_x, display_cached_beam_y; +static int display_cached_screen_x, display_cached_screen_y; /* The border colour changes which have occurred in this frame */ struct border_change_t { @@ -670,31 +671,32 @@ display_update_critical( int x, int y ) int beam_x, beam_y; if( tstates != display_cached_beam_tstates ) { - get_beam_position( &display_cached_beam_x, &display_cached_beam_y ); + get_beam_position( &beam_x, &beam_y ); display_cached_beam_tstates = tstates; - } - beam_x = display_cached_beam_x; - beam_y = display_cached_beam_y; - beam_x -= DISPLAY_BORDER_WIDTH_COLS; - beam_y -= DISPLAY_BORDER_HEIGHT; + beam_x -= DISPLAY_BORDER_WIDTH_COLS; + beam_y -= DISPLAY_BORDER_HEIGHT; - if( beam_y < 0 ) { - beam_x = beam_y = 0; - } else if( beam_y >= DISPLAY_HEIGHT ) { - beam_x = DISPLAY_WIDTH_COLS; - beam_y = DISPLAY_HEIGHT - 1; - } + if( beam_y < 0 ) { + beam_x = beam_y = 0; + } else if( beam_y >= DISPLAY_HEIGHT ) { + beam_x = DISPLAY_WIDTH_COLS; + beam_y = DISPLAY_HEIGHT - 1; + } + + if( beam_x < 0 ) { + beam_x = 0; + } else if( beam_x > DISPLAY_WIDTH_COLS ) { + beam_x = DISPLAY_WIDTH_COLS; + } - if( beam_x < 0 ) { - beam_x = 0; - } else if( beam_x > DISPLAY_WIDTH_COLS ) { - beam_x = DISPLAY_WIDTH_COLS; + display_cached_screen_x = beam_x; + display_cached_screen_y = beam_y; } - if( y < beam_y || - ( y == beam_y && x < beam_x ) ) - copy_critical_region( beam_x, beam_y ); + if( y < display_cached_screen_y || + ( y == display_cached_screen_y && x < display_cached_screen_x ) ) + copy_critical_region( display_cached_screen_x, display_cached_screen_y ); } /* Mark the 8-pixel chunk at (x,y) as maybe dirty and update the critical From c5c109c5ffea9f7c1bb38d12091f31788f67c73b Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 23:24:04 +1000 Subject: [PATCH 093/142] Fix decimal deparse of libspectrum_dword values > INT_MAX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit debugger_expression_deparse() used %d to format libspectrum_dword (uint32_t) when debugger_output_base == 10. Using a signed format specifier for an unsigned type is undefined behaviour; on all common platforms it prints a negative number for values >= 0x80000000 (e.g. ~0 prints as "-1" instead of "4294967295"). Fix: replace %d with %u. Add two deparse tests to debugger_expression_unittest(): deparse-decimal-small (42 → "42") deparse-decimal-large (0x80000000 → "2147483648") The second test would fail with the old %d format, catching any future regression. --- debugger/expression.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/debugger/expression.c b/debugger/expression.c index 37cf694d..76fe3ba7 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -461,7 +461,7 @@ debugger_expression_deparse( char *buffer, size_t length, case DEBUGGER_EXPRESSION_TYPE_INTEGER: if( debugger_output_base == 10 ) { - snprintf( buffer, length, "%d", exp->types.integer ); + snprintf( buffer, length, "%u", exp->types.integer ); } else { snprintf( buffer, length, "0x%x", exp->types.integer ); } @@ -915,6 +915,19 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "-( 0x3 + 0x4 )", "deparse-negate-expr" ); + /* Deparse in decimal base: small value and a value > INT_MAX */ + debugger_output_base = 10; + + r += deparse_test( + debugger_expression_new_number( 42, MEMPOOL_UNTRACKED ), + "42", "deparse-decimal-small" ); + + /* 0x80000000 is 2147483648 as uint32_t but -2147483648 as int32_t. + Using %d (signed) would produce the wrong negative result; %u is correct. */ + r += deparse_test( + debugger_expression_new_number( 0x80000000, MEMPOOL_UNTRACKED ), + "2147483648", "deparse-decimal-large" ); + debugger_output_base = saved_base; return r; From d56e342f328dafa7026420d5b130ea864ecbe884 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 27 Jun 2026 23:57:40 +1000 Subject: [PATCH 094/142] Cache libspectrum Linux and macOS builds to speed up CI Add GitHub Actions cache restore/save around the libspectrum build steps in the Linux and macOS sub-workflows. The cache key encodes the libspectrum commit SHA and all feature flags (zlib, bzip2, libgcrypt, libaudiofile, fake_glib), so a cache hit is only used when the inputs match exactly. On a cache hit, steps (3)-(11) are skipped entirely; the packed tar file is restored from cache and uploaded as an artifact as usual. The Linux build is invoked twice per PR (for fuse-app and fuse-app-gtk, which differ in use_fake_glib), so both benefit from separate cache entries. The macOS workflow follows the same pattern. This mirrors the caching already present in the Windows/Cygwin workflow (actions/cache/restore@v5 / actions/cache/save@v5). --- .../workflows/build_libspectrum_linux_sub.yml | 31 ++++++++++++++++++- .../workflows/build_libspectrum_macos_sub.yml | 31 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_libspectrum_linux_sub.yml b/.github/workflows/build_libspectrum_linux_sub.yml index b2c3ae88..bdd021e8 100644 --- a/.github/workflows/build_libspectrum_linux_sub.yml +++ b/.github/workflows/build_libspectrum_linux_sub.yml @@ -67,7 +67,22 @@ jobs: git -C "$LIBSPECTRUM_DIR" fetch --depth 1 origin "${{ inputs.libspectrum_ref }}" git -C "$LIBSPECTRUM_DIR" checkout FETCH_HEAD + - name: (2b) Capture libspectrum commit SHA for cache key + id: libspectrum-sha + run: | + sha=$(git -C "$LIBSPECTRUM_DIR" rev-parse HEAD) + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "Libspectrum SHA: $sha" + + - name: (2c) Restore libspectrum build cache + id: libspectrum-cache + uses: actions/cache/restore@v5 + with: + key: libspectrum-linux-${{ steps.libspectrum-sha.outputs.sha }}-z${{ inputs.use_zlib && '1' || '0' }}-b${{ inputs.use_bzip2 && '1' || '0' }}-g${{ inputs.use_libgcrypt && '1' || '0' }}-a${{ inputs.use_libaudiofile && '1' || '0' }}-f${{ inputs.use_fake_glib && '1' || '0' }} + path: ${{ env.LIBSPECTRUM_DIR }}/libspectrum-installed-linux.tar + - name: (3) Install dependencies + if: steps.libspectrum-cache.outputs.cache-hit != 'true' run: | sudo apt-get update sudo apt-get -y install --no-install-recommends \ @@ -77,12 +92,14 @@ jobs: libglib2.0-dev - name: (4) Autogen.sh + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running autogen.sh .." ./autogen.sh - name: (5) Configure for Linux + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running configure .." @@ -95,6 +112,7 @@ jobs: | tee ./configure.out - name: (6) Verify output from configure + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | .github/scripts/in_config.sh "libspectrum is ready to be compiled" @@ -105,18 +123,21 @@ jobs: .github/scripts/in_config.sh "Internal GLib replacement: ${{ inputs.use_fake_glib && 'yes' || 'no' }}" - name: (7) Make + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running make .." make - name: (8) Install + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running make install .." sudo make install - name: (9) Run tests + if: steps.libspectrum-cache.outputs.cache-hit != 'true' id: run-tests working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | @@ -124,13 +145,14 @@ jobs: make check - name: (10) Verbose check tests (on failure) - if: failure() && steps.run-tests.outcome != 'success' + if: steps.libspectrum-cache.outputs.cache-hit != 'true' && failure() && steps.run-tests.outcome != 'success' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Listing all tests individually .." test/test - name: (11) Pack installed library files + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | find /usr/local | grep libspectrum > .tar_files @@ -138,6 +160,13 @@ jobs: libspectrum-installed-linux.tar \ -C /usr/local/ -T.tar_files + - name: (11b) Save libspectrum build cache + if: steps.libspectrum-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + key: libspectrum-linux-${{ steps.libspectrum-sha.outputs.sha }}-z${{ inputs.use_zlib && '1' || '0' }}-b${{ inputs.use_bzip2 && '1' || '0' }}-g${{ inputs.use_libgcrypt && '1' || '0' }}-a${{ inputs.use_libaudiofile && '1' || '0' }}-f${{ inputs.use_fake_glib && '1' || '0' }} + path: ${{ env.LIBSPECTRUM_DIR }}/libspectrum-installed-linux.tar + - name: (12) Upload generated tar file uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/build_libspectrum_macos_sub.yml b/.github/workflows/build_libspectrum_macos_sub.yml index db3ab346..da528bc8 100644 --- a/.github/workflows/build_libspectrum_macos_sub.yml +++ b/.github/workflows/build_libspectrum_macos_sub.yml @@ -63,7 +63,22 @@ jobs: git -C "$LIBSPECTRUM_DIR" fetch --depth 1 origin "${{ inputs.libspectrum_ref }}" git -C "$LIBSPECTRUM_DIR" checkout FETCH_HEAD + - name: (2b) Capture libspectrum commit SHA for cache key + id: libspectrum-sha + run: | + sha=$(git -C "$LIBSPECTRUM_DIR" rev-parse HEAD) + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "Libspectrum SHA: $sha" + + - name: (2c) Restore libspectrum build cache + id: libspectrum-cache + uses: actions/cache/restore@v5 + with: + key: libspectrum-macos-${{ steps.libspectrum-sha.outputs.sha }}-z${{ inputs.use_zlib && '1' || '0' }}-b${{ inputs.use_bzip2 && '1' || '0' }}-g${{ inputs.use_libgcrypt && '1' || '0' }}-f${{ inputs.use_fake_glib && '1' || '0' }} + path: ${{ env.LIBSPECTRUM_DIR }}/libspectrum-installed-macos.tar + - name: (3) Install dependencies + if: steps.libspectrum-cache.outputs.cache-hit != 'true' run: | echo "Inspect if brew installed .." brew doctor || true @@ -82,12 +97,14 @@ jobs: echo "PKG_CONFIG_PATH=$(brew --prefix)/lib/pkgconfig:$(brew --prefix bzip2)/lib/pkgconfig:$(brew --prefix libgcrypt)/lib/pkgconfig:$(brew --prefix glib)/lib/pkgconfig" >> "$GITHUB_ENV" - name: (4) Autogen.sh + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running autogen.sh .." ./autogen.sh - name: (5) Configure for MacOS + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running configure .." @@ -100,6 +117,7 @@ jobs: | tee ./configure.out - name: (6) Verify output from configure + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | .github/scripts/in_config.sh "libspectrum is ready to be compiled" @@ -110,18 +128,21 @@ jobs: .github/scripts/in_config.sh "Internal GLib replacement: ${{ inputs.use_fake_glib && 'yes' || 'no' }}" - name: (7) Make + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running make .." make - name: (8) Install + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Running make install .." sudo make install - name: (9) Run tests + if: steps.libspectrum-cache.outputs.cache-hit != 'true' id: run-tests working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | @@ -129,13 +150,14 @@ jobs: make check - name: (10) Verbose check tests (on failure) - if: failure() && steps.run-tests.outcome != 'success' + if: steps.libspectrum-cache.outputs.cache-hit != 'true' && failure() && steps.run-tests.outcome != 'success' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | echo "Listing all tests individually .." test/test - name: (11) Pack installed library files + if: steps.libspectrum-cache.outputs.cache-hit != 'true' working-directory: ${{ env.LIBSPECTRUM_DIR }} run: | find /usr/local | grep libspectrum > .tar_files @@ -143,6 +165,13 @@ jobs: libspectrum-installed-macos.tar \ -C /usr/local/ -T.tar_files + - name: (11b) Save libspectrum build cache + if: steps.libspectrum-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + key: libspectrum-macos-${{ steps.libspectrum-sha.outputs.sha }}-z${{ inputs.use_zlib && '1' || '0' }}-b${{ inputs.use_bzip2 && '1' || '0' }}-g${{ inputs.use_libgcrypt && '1' || '0' }}-f${{ inputs.use_fake_glib && '1' || '0' }} + path: ${{ env.LIBSPECTRUM_DIR }}/libspectrum-installed-macos.tar + - name: (12) Upload generated tar file uses: actions/upload-artifact@v7 with: From 016b0de6e5e8e3e968cfaf9448d1f0db18f13213 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 28 Jun 2026 21:43:23 +1000 Subject: [PATCH 095/142] Replace magic ULA bit masks with named constants Add ULA_PORT_BORDER_BITS, ULA_PORT_MIC_BIT, ULA_PORT_EAR_BIT, and ULA_PORT_EAR_IN_BIT to peripherals/ula.h and use them throughout peripherals/ula.c instead of the raw hex values 0x07, 0x08, 0x10, 0x18, 0x40. --- peripherals/ula.c | 12 ++++++------ peripherals/ula.h | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/peripherals/ula.c b/peripherals/ula.c index 420df962..d4427a1d 100644 --- a/peripherals/ula.c +++ b/peripherals/ula.c @@ -189,7 +189,7 @@ ula_read( libspectrum_word port, libspectrum_byte *attached ) r &= phantom_typist_ula_read( port ); r &= keyboard_read( port >> 8 ); - if( tape_microphone ) r ^= 0x40; + if( tape_microphone ) r ^= ULA_PORT_EAR_IN_BIT; return r; } @@ -200,9 +200,9 @@ ula_write( libspectrum_word port GCC_UNUSED, libspectrum_byte b ) { last_byte = b; - display_set_lores_border( b & 0x07 ); + display_set_lores_border( b & ULA_PORT_BORDER_BITS ); sound_beeper( tstates, - (!!(b & 0x10) << 1) + ( (!(b & 0x8)) | tape_microphone ) ); + (!!(b & ULA_PORT_EAR_BIT) << 1) + ( (!(b & ULA_PORT_MIC_BIT)) | tape_microphone ) ); /* FIXME: shouldn't really be using the memory capabilities here */ @@ -217,12 +217,12 @@ ula_write( libspectrum_word port GCC_UNUSED, libspectrum_byte b ) } else if( machine_current->capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_128_MEMORY || !settings_current.issue2 ) { /* 128K always acts like an Issue 3 */ - ula_default_value = b & 0x10 ? 0xff : 0xbf; + ula_default_value = b & ULA_PORT_EAR_BIT ? 0xff : 0xbf; } else { /* Issue 2 */ - ula_default_value = b & 0x18 ? 0xff : 0xbf; + ula_default_value = b & ( ULA_PORT_EAR_BIT | ULA_PORT_MIC_BIT ) ? 0xff : 0xbf; } @@ -237,7 +237,7 @@ ula_last_byte( void ) libspectrum_byte ula_tape_level( void ) { - return last_byte & 0x8; + return last_byte & ULA_PORT_MIC_BIT; } static void diff --git a/peripherals/ula.h b/peripherals/ula.h index ce11bd56..f0eaeaf5 100644 --- a/peripherals/ula.h +++ b/peripherals/ula.h @@ -24,6 +24,12 @@ #ifndef FUSE_ULA_H #define FUSE_ULA_H +/* ULA port 0xFE bit masks */ +#define ULA_PORT_BORDER_BITS 0x07 /* bits 2-0: border colour */ +#define ULA_PORT_MIC_BIT 0x08 /* bit 3: MIC output (tape recording) */ +#define ULA_PORT_EAR_BIT 0x10 /* bit 4: EAR output (speaker) */ +#define ULA_PORT_EAR_IN_BIT 0x40 /* bit 6: EAR input (tape signal) */ + #define ULA_CONTENTION_SIZE 80000 /* How much contention do we get at every tstate when MREQ is active? */ From 918493cf5aebb6881b7270ae4706cef317f5f729 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 28 Jun 2026 22:00:52 +1000 Subject: [PATCH 096/142] Use libXML for config files in all Windows release build variants --- .github/workflows/windows_release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows_release.yml b/.github/workflows/windows_release.yml index b574509f..c935bcc1 100644 --- a/.github/workflows/windows_release.yml +++ b/.github/workflows/windows_release.yml @@ -73,18 +73,18 @@ jobs: verify_other: "" - name: "Fuse SDL 1 build" key: "sdl1-ui-sdl1-sound" - dependencies: "mingw64-i686-SDL" + dependencies: "mingw64-i686-SDL mingw64-i686-libxml2" configure_params: "--without-win32 --with-sdl --disable-sdl2 --with-audio-driver=sdl" verify_ui: "sdl" - verify_libxml2: "no" + verify_libxml2: "yes" verify_audio: "sdl" verify_other: "Using SDL: 1" - name: "Fuse SDL 2 build" key: "sdl2-ui-sdl2-sound" - dependencies: "mingw64-i686-SDL2" + dependencies: "mingw64-i686-SDL2 mingw64-i686-libxml2" configure_params: "--without-win32 --with-sdl --with-audio-driver=sdl" verify_ui: "sdl2" - verify_libxml2: "no" + verify_libxml2: "yes" verify_audio: "sdl" verify_other: "Using SDL: 2" uses: ./.github/workflows/build_windows_sub.yml From 21411d2946825513bf58e8fcac0f6e9d999f619b Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 28 Jun 2026 22:12:14 +1000 Subject: [PATCH 097/142] Fall back to defaults if loading a settings file fails --- settings.pl | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/settings.pl b/settings.pl index 61663f6d..98d9dc8c 100644 --- a/settings.pl +++ b/settings.pl @@ -168,6 +168,7 @@ read_config_file( settings_info *settings ) { const char *cfgdir; char path[ PATH_MAX ]; + settings_info new_settings; xmlDocPtr doc; @@ -184,19 +185,30 @@ if( !compat_file_exists( path ) ) return 0; } + memset( &new_settings, 0, sizeof( new_settings ) ); + settings_copy_internal( &new_settings, settings ); + doc = xmlReadFile( path, NULL, 0 ); if( !doc ) { - ui_error( UI_ERROR_ERROR, "error reading config file" ); - return 1; + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; } - if( parse_xml( doc, settings ) ) { + if( parse_xml( doc, &new_settings ) ) { xmlFreeDoc( doc ); - return 1; + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; } xmlFreeDoc( doc ); + settings_copy_internal( settings, &new_settings ); + settings_free( &new_settings ); + return 0; } @@ -334,6 +346,7 @@ { const char *cfgdir; char path[ PATH_MAX ]; int error; + settings_info new_settings; utils_file file; @@ -350,16 +363,30 @@ if( !compat_file_exists( path ) ) return 0; } + memset( &new_settings, 0, sizeof( new_settings ) ); + settings_copy_internal( &new_settings, settings ); + error = utils_read_file( path, &file ); if( error ) { - ui_error( UI_ERROR_ERROR, "error reading config file" ); - return 1; + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; } - if( parse_ini( &file, settings ) ) { utils_close_file( &file ); return 1; } + if( parse_ini( &file, &new_settings ) ) { + utils_close_file( &file ); + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; + } utils_close_file( &file ); + settings_copy_internal( settings, &new_settings ); + settings_free( &new_settings ); + return 0; } From 36f5b6ddfcc44d0c5173510eec92cd414d07b558 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 23 Jun 2026 00:13:10 +0200 Subject: [PATCH 098/142] Revert "gtk3: Remove the force_scaler parameter from register_scalers()" This reverts commit 257a1bd790042e7f42c7ad1167eed3c02b63fe09. --- ui/gtk3/gtkdisplay.c | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 308455b0..c6f5e79b 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -117,7 +117,7 @@ static int extra_height = 0; static int init_colours( colour_format_t format ); static void gtkdisplay_area(int x, int y, int width, int height); -static void register_scalers( void ); +static void register_scalers( int force_scaler ); static void gtkdisplay_load_gfx_mode( void ); /* Callbacks */ @@ -201,7 +201,7 @@ uidisplay_init( int width, int height ) image_width = width; image_height = height; image_scale = width / DISPLAY_ASPECT_WIDTH; - register_scalers(); + register_scalers( 0 ); display_refresh_all(); @@ -235,7 +235,7 @@ ensure_appropriate_surface( void ) } static int -drawing_area_resize( int width, int height ) +drawing_area_resize( int width, int height, int force_scaler ) { int size; @@ -251,6 +251,8 @@ drawing_area_resize( int width, int height ) gtkdisplay_surface_size = size; + register_scalers( force_scaler ); + memset( scaled_image, 0, sizeof( scaled_image ) ); ensure_appropriate_surface(); @@ -261,9 +263,10 @@ drawing_area_resize( int width, int height ) } static void -register_scalers( void ) +register_scalers( int force_scaler ) { scaler_type scaler; + float surface_scale, scaling_factor; scaler_register_clear(); @@ -301,6 +304,26 @@ register_scalers( void ) scaler = scaler_is_supported( current_scaler ) ? current_scaler : SCALER_NORMAL; + surface_scale = (float)gtkdisplay_surface_size / image_scale; + scaling_factor = scaler_get_scaling_factor( current_scaler ); + + /* Override scaler if it doesn't match the surface scale */ + if( force_scaler && surface_scale != scaling_factor ) { + + switch( gtkdisplay_surface_size ) { + case 1: scaler = machine_current->timex ? SCALER_HALF : SCALER_NORMAL; + break; + case 2: scaler = machine_current->timex ? SCALER_NORMAL : SCALER_DOUBLESIZE; + break; + case 3: scaler = machine_current->timex ? SCALER_TIMEX1_5X : + SCALER_TRIPLESIZE; + break; + case 4: scaler = machine_current->timex ? SCALER_TIMEX2X : + SCALER_QUADSIZE; + break; + } + } + /* Activate the scaler without trying to resize the GTK window */ scaler_activate_scaler( scaler ); } @@ -615,7 +638,7 @@ gtkdisplay_load_gfx_mode( void ) /* Rebuild the cairo surface for the new scaler */ surface_width = scale * image_width; surface_height = scale * image_height; - drawing_area_resize( surface_width, surface_height ); + drawing_area_resize( surface_width, surface_height, 0 ); gtk_window_resize( GTK_WINDOW( gtkui_window ), surface_width, surface_height + extra_height ); From 5c576188eb3eeb6ca9cd62fccac7db40805dbc0e Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 23 Jun 2026 00:20:34 +0200 Subject: [PATCH 099/142] Revert "gtk3: Don't switch scalers when the window is resized" This reverts commit 38b98d446358783e8df3710e63e52a7c92417364. --- ui/gtk3/gtkdisplay.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index c6f5e79b..43f92666 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -125,6 +125,9 @@ static void gtkdisplay_load_gfx_mode( void ); static gboolean gtkdisplay_draw( GtkWidget *widget, cairo_t *cr, gpointer user_data ); +static gint drawing_area_resize_callback( GtkWidget *widget, GdkEvent *event, + gpointer data ); + static int init_colours( colour_format_t format ) { @@ -188,6 +191,9 @@ uidisplay_init( int width, int height ) colour_format = FORMAT_x8r8g8b8; + g_signal_connect( G_OBJECT( gtkui_window ), "configure_event", + G_CALLBACK( drawing_area_resize_callback ), NULL ); + error = init_colours( colour_format ); if( error ) return error; error = scaler_select_bitformat( BITFORMAT_X8R8G8B8 ); if( error ) return error; @@ -594,6 +600,18 @@ gtkdisplay_draw( GtkWidget *widget GCC_UNUSED, cairo_t *cr, return FALSE; } +/* Called by gtkui_window on "configure_event". + On GTK 3 the window determines the size of the drawing area */ +static gint +drawing_area_resize_callback( GtkWidget *widget GCC_UNUSED, GdkEvent *event, + gpointer data GCC_UNUSED ) +{ + drawing_area_resize( event->configure.width, + event->configure.height - extra_height, 1 ); + + return FALSE; +} + void gtkdisplay_update_geometry( void ) { @@ -629,19 +647,13 @@ static void gtkdisplay_load_gfx_mode( void ) { float scale; - int surface_width, surface_height; scale = scaler_get_scaling_factor( current_scaler ); gtkdisplay_update_geometry(); - /* Rebuild the cairo surface for the new scaler */ - surface_width = scale * image_width; - surface_height = scale * image_height; - drawing_area_resize( surface_width, surface_height, 0 ); - - gtk_window_resize( GTK_WINDOW( gtkui_window ), surface_width, - surface_height + extra_height ); + gtk_window_resize( GTK_WINDOW( gtkui_window ), scale * image_width, + scale * image_height + extra_height ); /* Redraw the entire screen... */ display_refresh_all(); From 03941c5a9a4cf2f4a524d6146828c5406ad9a5ca Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 23 Jun 2026 14:42:13 +0200 Subject: [PATCH 100/142] gtk3: Don't switch scalers while the user is resizing the window Commit 38b98d44 (now reverted) disabled the automatic switching of scalers when a window was being resized because one of the effects was that it would produce an unpleasant flickering effect. This patch takes a different approach to solve that flickering: it waits until the user has finished resizing the window before calling drawing_area_resize(). --- ui/gtk3/gtkdisplay.c | 58 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 43f92666..3caf4d70 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -115,10 +115,18 @@ static int gtkdisplay_surface_size=1; /* Extra height used for menu and status bars */ static int extra_height = 0; +/* If the user resizes the window, switch the scaler only after this + period of inactivity (i.e. without configure events). */ +#define RESIZE_TIMEOUT_MS 250 +static guint resize_timeout_id = 0; +static gint64 resize_last_activity = 0; +static int pending_width, pending_height; + static int init_colours( colour_format_t format ); static void gtkdisplay_area(int x, int y, int width, int height); static void register_scalers( int force_scaler ); static void gtkdisplay_load_gfx_mode( void ); +static void cancel_pending_resize( void ); /* Callbacks */ @@ -474,6 +482,8 @@ uidisplay_hotswap_gfx_mode( void ) int uidisplay_end( void ) { + cancel_pending_resize(); + return 0; } @@ -600,14 +610,52 @@ gtkdisplay_draw( GtkWidget *widget GCC_UNUSED, cairo_t *cr, return FALSE; } +static void +cancel_pending_resize( void ) +{ + if( resize_timeout_id ) { + g_source_remove( resize_timeout_id ); + resize_timeout_id = 0; + } +} + +/* Resize the drawing area when the user has finished resizing the window */ +static gboolean +drawing_area_resize_timeout( gpointer data GCC_UNUSED ) +{ + gint64 timeout = (gint64)RESIZE_TIMEOUT_MS * 1000; + gint64 idle = g_get_monotonic_time() - resize_last_activity; + + /* Rearm the timer if more configure events arrived in the meantime */ + if( idle < timeout ) { + resize_timeout_id = g_timeout_add( ( timeout - idle ) / 1000, + drawing_area_resize_timeout, NULL ); + return G_SOURCE_REMOVE; + } + + resize_timeout_id = 0; + + drawing_area_resize( pending_width, pending_height, 1 ); + + return G_SOURCE_REMOVE; +} + /* Called by gtkui_window on "configure_event". - On GTK 3 the window determines the size of the drawing area */ + On GTK 3 the window determines the size of the drawing area. + + Wait for RESIZE_TIMEOUT_MS before changing the scaler to prevent + the window from flickering while it is being resized. */ static gint drawing_area_resize_callback( GtkWidget *widget GCC_UNUSED, GdkEvent *event, gpointer data GCC_UNUSED ) { - drawing_area_resize( event->configure.width, - event->configure.height - extra_height, 1 ); + pending_width = event->configure.width; + pending_height = event->configure.height - extra_height; + resize_last_activity = g_get_monotonic_time(); + + if( !resize_timeout_id ) + resize_timeout_id = + g_timeout_add( RESIZE_TIMEOUT_MS, drawing_area_resize_timeout, NULL ); return FALSE; } @@ -652,6 +700,10 @@ gtkdisplay_load_gfx_mode( void ) gtkdisplay_update_geometry(); + /* This is a programmatic resize for a new scaler so apply it right away */ + cancel_pending_resize(); + drawing_area_resize( scale * image_width, scale * image_height, 0 ); + gtk_window_resize( GTK_WINDOW( gtkui_window ), scale * image_width, scale * image_height + extra_height ); From a254c9e4b5a370a51579b074264c42cc4455e766 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Date: Tue, 23 Jun 2026 18:36:45 +0200 Subject: [PATCH 101/142] gtk3: Select scalers from the same family when resizing the window If during a window resize the scaler has to change, select the new scaler from the same family (e.g. go from TV2X to TV3X and not to TRIPLESIZE). If there's no available scaler of the requested size (for example, there is no ADVMAME1X or ADVMAME4X), then select the closest one. Because of this, the check in drawing_area_resize() must be updated. --- ui/gtk3/gtkdisplay.c | 51 +++++++++++++++------------------------- ui/scaler/scaler.c | 56 ++++++++++++++++++++++++++++++++++++++++++++ ui/scaler/scaler.h | 1 + 3 files changed, 76 insertions(+), 32 deletions(-) diff --git a/ui/gtk3/gtkdisplay.c b/ui/gtk3/gtkdisplay.c index 3caf4d70..ef3a5be5 100644 --- a/ui/gtk3/gtkdisplay.c +++ b/ui/gtk3/gtkdisplay.c @@ -50,10 +50,6 @@ creating via the scalers */ #define MAX_SCALE 4 -/* The size of a 1x1 image in units of - DISPLAY_ASPECT WIDTH x DISPLAY_SCREEN_HEIGHT */ -int image_scale; - /* The height and width of a 1x1 image in pixels */ int image_width, image_height; @@ -108,8 +104,13 @@ typedef enum { static cairo_surface_t *surface = NULL; -/* The current size of the cairo surface (in units of DISPLAY_SCREEN_*). - It may be smaller than the GTK drawing area if the window is maximized. */ +/* The scaler the current cairo surface was created for */ +static scaler_type surface_scaler = SCALER_NUM; + +/* The size the window has been resized to (in units of DISPLAY_SCREEN_*), + used to choose the scaler. The cairo surface is sized to the active + scaler instead, which may be smaller or larger than this when the + scaler's family has no variant for this size (see scaler_family_table). */ static int gtkdisplay_surface_size=1; /* Extra height used for menu and status bars */ @@ -213,7 +214,6 @@ uidisplay_init( int width, int height ) *(libspectrum_dword*)( rgb_image + y * rgb_pitch + 4 * x ) = black; image_width = width; image_height = height; - image_scale = width / DISPLAY_ASPECT_WIDTH; register_scalers( 0 ); @@ -236,8 +236,8 @@ uidisplay_init( int width, int height ) static void ensure_appropriate_surface( void ) { - /* Recreate the cairo surface to match the new size */ - float scale = (float)gtkdisplay_surface_size / image_scale; + /* Recreate the cairo surface to match the active scaler */ + float scale = scaler_get_scaling_factor( current_scaler ); if( surface ) cairo_surface_destroy( surface ); surface = @@ -246,6 +246,8 @@ ensure_appropriate_surface( void ) scale * image_width, scale * image_height, scaled_pitch ); + + surface_scaler = current_scaler; } static int @@ -260,8 +262,9 @@ drawing_area_resize( int width, int height, int force_scaler ) if( size > MAX_SCALE ) size = MAX_SCALE; if( size < 1 ) size = 1; - /* If we're the same size as before, no need to do anything else */ - if( size == gtkdisplay_surface_size ) return 0; + /* If we're the same size and scaler as before, no need to do anything else */ + if( size == gtkdisplay_surface_size && current_scaler == surface_scaler ) + return 0; gtkdisplay_surface_size = size; @@ -280,7 +283,6 @@ static void register_scalers( int force_scaler ) { scaler_type scaler; - float surface_scale, scaling_factor; scaler_register_clear(); @@ -318,25 +320,10 @@ register_scalers( int force_scaler ) scaler = scaler_is_supported( current_scaler ) ? current_scaler : SCALER_NORMAL; - surface_scale = (float)gtkdisplay_surface_size / image_scale; - scaling_factor = scaler_get_scaling_factor( current_scaler ); - - /* Override scaler if it doesn't match the surface scale */ - if( force_scaler && surface_scale != scaling_factor ) { - - switch( gtkdisplay_surface_size ) { - case 1: scaler = machine_current->timex ? SCALER_HALF : SCALER_NORMAL; - break; - case 2: scaler = machine_current->timex ? SCALER_NORMAL : SCALER_DOUBLESIZE; - break; - case 3: scaler = machine_current->timex ? SCALER_TIMEX1_5X : - SCALER_TRIPLESIZE; - break; - case 4: scaler = machine_current->timex ? SCALER_TIMEX2X : - SCALER_QUADSIZE; - break; - } - } + /* When the window is resized switch to the scaler of the same family + (e.g. PAL TV, HQ) that fits the new size */ + if( force_scaler ) + scaler = scaler_for_size( scaler, gtkdisplay_surface_size ); /* Activate the scaler without trying to resize the GTK window */ scaler_activate_scaler( scaler ); @@ -356,7 +343,7 @@ uidisplay_frame_end( void ) void uidisplay_area( int x, int y, int w, int h ) { - float scale = (float)gtkdisplay_surface_size / image_scale; + float scale = scaler_get_scaling_factor( current_scaler ); int scaled_x, scaled_y, i, yy; libspectrum_dword *palette; diff --git a/ui/scaler/scaler.c b/ui/scaler/scaler.c index 00621f77..606d7b0d 100644 --- a/ui/scaler/scaler.c +++ b/ui/scaler/scaler.c @@ -128,6 +128,27 @@ static const struct scaler_info available_scalers[] = { scaler_blargg_NTSC_4x_16, scaler_blargg_NTSC_4x_32, NULL }, }; +/* Scalers that share the same look but render the image at a different scale + belong to the same family. Single-size scalers (2xSaI, Dot Matrix, ...) are + not listed here and are left untouched when the display is resized. + + Important: a machine cannot have a scaler listed in more than one family, + that is, a scaler cannot be in two different rows unless those two rows + are for different machines. In this case SCALER_NORMAL appears twice + precisely because no machine can use both families of scalers. */ +#define SCALER_SIZES 4 +static const scaler_type scaler_family_table[][ SCALER_SIZES ] = { + /* 1x 2x 3x 4x */ + { SCALER_NORMAL, SCALER_DOUBLESIZE, SCALER_TRIPLESIZE, SCALER_QUADSIZE }, + { SCALER_TV2X, SCALER_TV2X, SCALER_TV3X, SCALER_TV4X }, + { SCALER_PALTV2X, SCALER_PALTV2X, SCALER_PALTV3X, SCALER_PALTV4X }, + { SCALER_HQ2X, SCALER_HQ2X, SCALER_HQ3X, SCALER_HQ4X }, + { SCALER_NTSC2X, SCALER_NTSC2X, SCALER_NTSC3X, SCALER_NTSC4X }, + { SCALER_ADVMAME2X, SCALER_ADVMAME2X, SCALER_ADVMAME3X, SCALER_ADVMAME3X }, + /* Timex sizes 1x-4x correspond to scaling factors 0.5x, 1x, 1.5x and 2x */ + { SCALER_HALF, SCALER_NORMAL, SCALER_TIMEX1_5X, SCALER_TIMEX2X }, +}; + scaler_type current_scaler = SCALER_NUM; ScalerProc *scaler_proc16, *scaler_proc32; scaler_flags_t scaler_flags; @@ -259,6 +280,41 @@ scaler_get_expander( scaler_type scaler ) return available_scalers[scaler].expander; } +/* Return the scaler that should be used at the given size (1x to 4x) + from the same family as 'scaler' (e.g. PAL TV, HQ, etc.). + + SCALER_NORMAL belongs to two families (the regular one and the Timex one), + so we only consider families whose scalers are all currently registered. + That is the family for the current machine. */ +scaler_type +scaler_for_size( scaler_type scaler, int size ) +{ + size_t family, i; + + if( size < 1 ) size = 1; + if( size > SCALER_SIZES ) size = SCALER_SIZES; + + for( family = 0; family < ARRAY_SIZE( scaler_family_table ); family++ ) { + int found = 0, supported = 1; + + /* For every row of scalers, check if: + - The current one ('scaler') is there. + - All four scalers are supported by the current machine */ + for( i = 0; i < SCALER_SIZES; i++ ) { + if( scaler_family_table[family][i] == scaler ) found = 1; + if( !scaler_is_supported( scaler_family_table[family][i] ) ) + supported = 0; + } + + /* If that's the case, select the best scaler for the given size */ + if( found && supported ) + return scaler_family_table[family][size - 1]; + } + + /* Single-size scaler (or not in any family): leave it as it is */ + return scaler; +} + /* The expansion functions */ /* Clip after expansion */ diff --git a/ui/scaler/scaler.h b/ui/scaler/scaler.h index 9100d966..cad77add 100644 --- a/ui/scaler/scaler.h +++ b/ui/scaler/scaler.h @@ -101,6 +101,7 @@ ScalerProc *scaler_get_proc32( scaler_type scaler ); scaler_flags_t scaler_get_flags( scaler_type scaler ); float scaler_get_scaling_factor( scaler_type scaler ); scaler_expand_fn* scaler_get_expander( scaler_type scaler ); +scaler_type scaler_for_size( scaler_type scaler, int size ); int scaler_select_bitformat( scaler_bitformat_t bitformat ); From 0bc61e0d19d913ec9e842ae09241f04a73d4234e Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 29 Jun 2026 22:50:24 +1000 Subject: [PATCH 102/142] Add CB prefix rotate/shift test coverage for register A and (HL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 18 new disassembler unit tests (test278–test295) covering two gaps in the CB-prefix test suite. The existing tests 72–79 only verify B as the operand for RLC/RRC/RL/RR/ SLA/SRA/SLL/SRL. Adds one test per opcode with register A (rrr = 111), the furthest end of the eight-entry register table, so both endpoints of source_reg() are exercised for every rotation/shift mnemonic. test80 already verifies RLC (HL). The other seven ops — RRC, RL, RR, SLA, SRA, SLL, SRL — were untested. Adds one test per missing op to ensure rotate_op() selects the right mnemonic when the register field is (HL) (rrr = 110). The existing tests 16–18 exercise BIT/RES/SET with register A and bit numbers 0 and 1. Adds BIT 7,(HL), RES 7,(HL), and SET 7,(HL) to verify bit_op_bit() with the maximum bit index (bbb = 111) and source_reg() with (HL) in the >= 0x40 branch of disassemble_cb(). --- debugger/disassemble.c | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 849f9180..a4c3f2c5 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1254,6 +1254,37 @@ libspectrum_byte test275_data[] = { 0xfd, 0x74, 0x05 }; /* LD (IY+05),H */ libspectrum_byte test276_data[] = { 0xfd, 0x75, 0x05 }; /* LD (IY+05),L */ libspectrum_byte test277_data[] = { 0xfd, 0x77, 0x05 }; /* LD (IY+05),A */ +/* CB prefix: all 8 rotate/shift operations with register A (rrr = 111). + Together with the existing B tests (test72-79) these verify that + source_reg() returns the correct register name at both ends of + the register range used by disassemble_cb(). */ +static libspectrum_byte test278_data[] = { 0xcb, 0x07 }; /* RLC A */ +static libspectrum_byte test279_data[] = { 0xcb, 0x0f }; /* RRC A */ +static libspectrum_byte test280_data[] = { 0xcb, 0x17 }; /* RL A */ +static libspectrum_byte test281_data[] = { 0xcb, 0x1f }; /* RR A */ +static libspectrum_byte test282_data[] = { 0xcb, 0x27 }; /* SLA A */ +static libspectrum_byte test283_data[] = { 0xcb, 0x2f }; /* SRA A */ +static libspectrum_byte test284_data[] = { 0xcb, 0x37 }; /* SLL A (undocumented) */ +static libspectrum_byte test285_data[] = { 0xcb, 0x3f }; /* SRL A */ + +/* CB prefix: all remaining rotate/shift operations with (HL) (rrr = 110). + RLC (HL) is already covered by test80; these add the other seven ops + to verify rotate_op() selects each mnemonic when the register is (HL). */ +static libspectrum_byte test286_data[] = { 0xcb, 0x0e }; /* RRC (HL) */ +static libspectrum_byte test287_data[] = { 0xcb, 0x16 }; /* RL (HL) */ +static libspectrum_byte test288_data[] = { 0xcb, 0x1e }; /* RR (HL) */ +static libspectrum_byte test289_data[] = { 0xcb, 0x26 }; /* SLA (HL) */ +static libspectrum_byte test290_data[] = { 0xcb, 0x2e }; /* SRA (HL) */ +static libspectrum_byte test291_data[] = { 0xcb, 0x36 }; /* SLL (HL) (undocumented) */ +static libspectrum_byte test292_data[] = { 0xcb, 0x3e }; /* SRL (HL) */ + +/* CB prefix: BIT, RES and SET with bit 7 and (HL) (rrr = 110). + Exercises bit_op(), bit_op_bit() with the maximum bit number, + and source_reg() with the (HL) register in the >= 0x40 branch. */ +static libspectrum_byte test293_data[] = { 0xcb, 0x7e }; /* BIT 7,(HL) */ +static libspectrum_byte test294_data[] = { 0xcb, 0xbe }; /* RES 7,(HL) */ +static libspectrum_byte test295_data[] = { 0xcb, 0xfe }; /* SET 7,(HL) */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1672,6 +1703,30 @@ debugger_disassemble_unittest( void ) r += run_test( test276_data, sizeof( test276_data ), "LD (IY+05),L" ); r += run_test( test277_data, sizeof( test277_data ), "LD (IY+05),A" ); + /* CB prefix: all rotate/shift ops with register A */ + r += run_test( test278_data, sizeof( test278_data ), "RLC A" ); + r += run_test( test279_data, sizeof( test279_data ), "RRC A" ); + r += run_test( test280_data, sizeof( test280_data ), "RL A" ); + r += run_test( test281_data, sizeof( test281_data ), "RR A" ); + r += run_test( test282_data, sizeof( test282_data ), "SLA A" ); + r += run_test( test283_data, sizeof( test283_data ), "SRA A" ); + r += run_test( test284_data, sizeof( test284_data ), "SLL A" ); + r += run_test( test285_data, sizeof( test285_data ), "SRL A" ); + + /* CB prefix: remaining rotate/shift ops with (HL) */ + r += run_test( test286_data, sizeof( test286_data ), "RRC (HL)" ); + r += run_test( test287_data, sizeof( test287_data ), "RL (HL)" ); + r += run_test( test288_data, sizeof( test288_data ), "RR (HL)" ); + r += run_test( test289_data, sizeof( test289_data ), "SLA (HL)" ); + r += run_test( test290_data, sizeof( test290_data ), "SRA (HL)" ); + r += run_test( test291_data, sizeof( test291_data ), "SLL (HL)" ); + r += run_test( test292_data, sizeof( test292_data ), "SRL (HL)" ); + + /* CB prefix: BIT, RES, SET with bit 7 and (HL) */ + r += run_test( test293_data, sizeof( test293_data ), "BIT 7,(HL)" ); + r += run_test( test294_data, sizeof( test294_data ), "RES 7,(HL)" ); + r += run_test( test295_data, sizeof( test295_data ), "SET 7,(HL)" ); + return r; } From 5c57e3a6341a0b2a66f6cd162c80a36cb43bf943 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 30 Jun 2026 18:19:10 +1000 Subject: [PATCH 103/142] Add CB prefix rotation/shift tests for registers C, D, E, H, L Currently the CB prefix rotation/shift disassembler tests cover only register B (test72-79), register A (test278-285), and (HL) (test80, test286-292), leaving registers C, D, E, H, and L completely untested. Add 12 new test vectors (test296-test307): - All eight rotation/shift opcodes for register C (rrr=001): RLC C (CB 0x01), RRC C (CB 0x09), RL C (CB 0x11), RR C (CB 0x19), SLA C (CB 0x21), SRA C (CB 0x29), SLL C (CB 0x31), SRL C (CB 0x39) This gives full coverage of every shift-group op path for rrr=001, mirroring the complete sets already present for rrr=000 (B) and rrr=110/111 ((HL)/A). - One RLC test each for D (rrr=010), E (rrr=011), H (rrr=100), and L (rrr=101), confirming that single_reg() returns the correct register name for all five previously untested rrr values. --- debugger/disassemble.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index a4c3f2c5..682ad5ea 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1285,6 +1285,25 @@ static libspectrum_byte test293_data[] = { 0xcb, 0x7e }; /* BIT 7,(HL) */ static libspectrum_byte test294_data[] = { 0xcb, 0xbe }; /* RES 7,(HL) */ static libspectrum_byte test295_data[] = { 0xcb, 0xfe }; /* SET 7,(HL) */ +/* CB prefix: all eight rotation/shift ops for register C (rrr = 001). + Verifies that the dest_reg()/source_reg() lookup returns "C" for + every shift-group opcode. Opcode = (op << 3) | 001. */ +static libspectrum_byte test296_data[] = { 0xcb, 0x01 }; /* RLC C */ +static libspectrum_byte test297_data[] = { 0xcb, 0x09 }; /* RRC C */ +static libspectrum_byte test298_data[] = { 0xcb, 0x11 }; /* RL C */ +static libspectrum_byte test299_data[] = { 0xcb, 0x19 }; /* RR C */ +static libspectrum_byte test300_data[] = { 0xcb, 0x21 }; /* SLA C */ +static libspectrum_byte test301_data[] = { 0xcb, 0x29 }; /* SRA C */ +static libspectrum_byte test302_data[] = { 0xcb, 0x31 }; /* SLL C (undocumented) */ +static libspectrum_byte test303_data[] = { 0xcb, 0x39 }; /* SRL C */ + +/* CB prefix: one rotation op per remaining register to verify register + name lookup for rrr = 010 (D), 011 (E), 100 (H), 101 (L). */ +static libspectrum_byte test304_data[] = { 0xcb, 0x02 }; /* RLC D */ +static libspectrum_byte test305_data[] = { 0xcb, 0x03 }; /* RLC E */ +static libspectrum_byte test306_data[] = { 0xcb, 0x04 }; /* RLC H */ +static libspectrum_byte test307_data[] = { 0xcb, 0x05 }; /* RLC L */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1727,6 +1746,22 @@ debugger_disassemble_unittest( void ) r += run_test( test294_data, sizeof( test294_data ), "RES 7,(HL)" ); r += run_test( test295_data, sizeof( test295_data ), "SET 7,(HL)" ); + /* CB prefix: all rotation/shift ops for register C (rrr = 001) */ + r += run_test( test296_data, sizeof( test296_data ), "RLC C" ); + r += run_test( test297_data, sizeof( test297_data ), "RRC C" ); + r += run_test( test298_data, sizeof( test298_data ), "RL C" ); + r += run_test( test299_data, sizeof( test299_data ), "RR C" ); + r += run_test( test300_data, sizeof( test300_data ), "SLA C" ); + r += run_test( test301_data, sizeof( test301_data ), "SRA C" ); + r += run_test( test302_data, sizeof( test302_data ), "SLL C" ); + r += run_test( test303_data, sizeof( test303_data ), "SRL C" ); + + /* CB prefix: RLC for registers D, E, H, L (rrr = 010, 011, 100, 101) */ + r += run_test( test304_data, sizeof( test304_data ), "RLC D" ); + r += run_test( test305_data, sizeof( test305_data ), "RLC E" ); + r += run_test( test306_data, sizeof( test306_data ), "RLC H" ); + r += run_test( test307_data, sizeof( test307_data ), "RLC L" ); + return r; } From 28442f7710bb7476a798a266728285ee43290d02 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 30 Jun 2026 18:49:07 +1000 Subject: [PATCH 104/142] Add deparse tests for all remaining binary operators Add deparse tests for the 14 binary operators that had no deparse coverage: -, *, /, ==, !=, <, >, <=, >=, &, ^, |, &&, ||. Also add two cross-precedence tests showing that brackets are inserted exactly when required: - deparse-add-inside-mul: (3 + 4) * 5 is deparsed as '( 0x3 + 0x4 ) * 0x5' because + (ADDITION) has lower precedence than * (MULTIPLICATION). - deparse-mul-inside-add: 3 + (4 * 5) is deparsed as '0x3 + 0x4 * 0x5' with no brackets because * (MULTIPLICATION) already has higher precedence than + (ADDITION). These tests complement the existing deparse tests for + (add), % (mod), << (lshift), >> (rshift), and the mod non-associativity fix, giving every operator in deparse_binaryop() a direct test. --- debugger/expression.c | 125 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index 76fe3ba7..45267560 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -888,6 +888,108 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "( 0x3 * 0x4 ) % 0x5", "deparse-mod-non-assoc" ); + /* Plain arithmetic operators without deparse coverage yet */ + r += deparse_test( + debugger_expression_new_binaryop( '-', + debugger_expression_new_number( 7, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x7 - 0x3", "deparse-sub" ); + + r += deparse_test( + debugger_expression_new_binaryop( '*', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 * 0x4", "deparse-mul" ); + + r += deparse_test( + debugger_expression_new_binaryop( '/', + debugger_expression_new_number( 12, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0xc / 0x4", "deparse-div" ); + + /* Comparison operators */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_EQUAL_TO, + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 6, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x5 == 0x6", "deparse-eq" ); + + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_NOT_EQUAL_TO, + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 6, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x5 != 0x6", "deparse-ne" ); + + r += deparse_test( + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 < 0x5", "deparse-lt" ); + + r += deparse_test( + debugger_expression_new_binaryop( '>', + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x5 > 0x3", "deparse-gt" ); + + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LESS_THAN_OR_EQUAL_TO, + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x4 <= 0x5", "deparse-lte" ); + + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_GREATER_THAN_OR_EQUAL_TO, + debugger_expression_new_number( 6, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x6 >= 0x5", "deparse-gte" ); + + /* Bitwise operators */ + r += deparse_test( + debugger_expression_new_binaryop( '&', + debugger_expression_new_number( 0xF0, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0xFF, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0xf0 & 0xff", "deparse-bitwise-and" ); + + r += deparse_test( + debugger_expression_new_binaryop( '^', + debugger_expression_new_number( 0xFF, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0x0F, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0xff ^ 0xf", "deparse-bitwise-xor" ); + + r += deparse_test( + debugger_expression_new_binaryop( '|', + debugger_expression_new_number( 0x0F, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0xF0, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0xf | 0xf0", "deparse-bitwise-or" ); + + /* Logical operators */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_AND, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x1 && 0x1", "deparse-logical-and" ); + + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_OR, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x1 || 0x0", "deparse-logical-or" ); + /* Binary '-' is non-associative: 3-(4-2) must bracket the right operand */ r += deparse_test( debugger_expression_new_binaryop( '-', @@ -899,6 +1001,29 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "0x3 - ( 0x4 - 0x2 )", "deparse-sub-non-assoc" ); + /* Cross-precedence: lower-precedence child is bracketed inside a + higher-precedence parent (add inside mul) */ + r += deparse_test( + debugger_expression_new_binaryop( '*', + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "( 0x3 + 0x4 ) * 0x5", "deparse-add-inside-mul" ); + + /* Higher-precedence child of lower-precedence parent needs no brackets */ + r += deparse_test( + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( '*', + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 + 0x4 * 0x5", "deparse-mul-inside-add" ); + /* Unary negation deparse: plain integer and expression requiring brackets */ r += deparse_test( debugger_expression_new_unaryop( '-', From 9f42b65eb63811f9c070e05aea3ab4fc6f154526 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 30 Jun 2026 19:31:16 +1000 Subject: [PATCH 105/142] Add deparse tests for unary !, ~, and non-assoc /, << Add 7 new deparse tests to debugger_expression_unittest(): - deparse-div-non-assoc: 12/(4/2) brackets the right-hand operand since '/' is non-associative and equal-precedence to the outer '/' - deparse-lshift-non-assoc: 1<<(2<<3) brackets the inner '<<' since '<<' is non-associative; mirrors the sub and mod tests for shifts - deparse-logical-not and deparse-logical-not-expr: exercise '!' unary deparse with and without expression operand requiring brackets - deparse-bitwise-not and deparse-bitwise-not-expr: exercise '~' unary deparse with and without expression operand requiring brackets --- debugger/expression.c | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index 45267560..0a05cc02 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -1001,6 +1001,29 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "0x3 - ( 0x4 - 0x2 )", "deparse-sub-non-assoc" ); + /* '/' is non-associative: the right operand '/' at equal precedence always + needs brackets so 12/(4/2) is not confused with (12/4)/2 */ + r += deparse_test( + debugger_expression_new_binaryop( '/', + debugger_expression_new_number( 12, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( '/', + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0xc / ( 0x4 / 0x2 )", "deparse-div-non-assoc" ); + + /* '<<' is non-associative: 1 << (2 << 3) brackets the right operand */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LEFT_SHIFT, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LEFT_SHIFT, + debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x1 << ( 0x2 << 0x3 )", "deparse-lshift-non-assoc" ); + /* Cross-precedence: lower-precedence child is bracketed inside a higher-precedence parent (add inside mul) */ r += deparse_test( @@ -1040,6 +1063,40 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "-( 0x3 + 0x4 )", "deparse-negate-expr" ); + /* Unary '!' deparse: plain integer (no brackets) and expression (brackets + needed because '+' has lower precedence than '!') */ + r += deparse_test( + debugger_expression_new_unaryop( '!', + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "!0x5", "deparse-logical-not" ); + + r += deparse_test( + debugger_expression_new_unaryop( '!', + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "!( 0x3 + 0x4 )", "deparse-logical-not-expr" ); + + /* Unary '~' deparse: plain integer (no brackets) and expression (brackets + needed because '|' has lower precedence than '~') */ + r += deparse_test( + debugger_expression_new_unaryop( '~', + debugger_expression_new_number( 15, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "~0xf", "deparse-bitwise-not" ); + + r += deparse_test( + debugger_expression_new_unaryop( '~', + debugger_expression_new_binaryop( '|', + debugger_expression_new_number( 0xF0, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0x0F, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "~( 0xf0 | 0xf )", "deparse-bitwise-not-expr" ); + /* Deparse in decimal base: small value and a value > INT_MAX */ debugger_output_base = 10; From 09618d85d501279e5e7b3361637b3de7ec5ac1fa Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Tue, 30 Jun 2026 19:36:52 +1000 Subject: [PATCH 106/142] Add CB BIT/RES/SET disassembler tests for all registers The existing CB prefix BIT/RES/SET tests (test16-18) only cover register A (rrr=111) with bit numbers 0 and 1. The single_reg() helper is a table-driven lookup for all eight 8-bit registers; these six new tests exercise it for every non-A, non-(HL) register (B, C, D, E, H, L) and sample several mid-range bit numbers (2, 3, 4, 5, 6), verifying that: - bit_op() returns the correct mnemonic (BIT, RES, SET) - bit_op_bit() extracts the right bit number from the opcode - source_reg() returns the correct register name for each rrr value Opcodes verified: CB 0x58 -> BIT 3,B CB 0x69 -> BIT 5,C CB 0xA2 -> RES 4,D CB 0x94 -> RES 2,H CB 0xF5 -> SET 6,L CB 0xDB -> SET 3,E --- debugger/disassemble.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 682ad5ea..8677d1cb 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1304,6 +1304,18 @@ static libspectrum_byte test305_data[] = { 0xcb, 0x03 }; /* RLC E */ static libspectrum_byte test306_data[] = { 0xcb, 0x04 }; /* RLC H */ static libspectrum_byte test307_data[] = { 0xcb, 0x05 }; /* RLC L */ +/* CB prefix BIT/RES/SET: cover all non-A, non-(HL) register names. + The existing tests (test16-18) cover register A; test293-295 add (HL). + These six tests exercise source_reg() for B(0), C(1), D(2), E(3), H(4), L(5) + and bit_op_bit() for several mid-range bit numbers. */ +static libspectrum_byte test308_data[] = { 0xcb, 0x58 }; /* BIT 3,B */ +static libspectrum_byte test309_data[] = { 0xcb, 0x69 }; /* BIT 5,C */ +static libspectrum_byte test310_data[] = { 0xcb, 0xa2 }; /* RES 4,D */ +static libspectrum_byte test311_data[] = { 0xcb, 0x94 }; /* RES 2,H */ +static libspectrum_byte test312_data[] = { 0xcb, 0xf5 }; /* SET 6,L */ +static libspectrum_byte test313_data[] = { 0xcb, 0xdb }; /* SET 3,E */ + + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) { @@ -1370,6 +1382,14 @@ debugger_disassemble_unittest( void ) r += run_test( test17_data, sizeof( test17_data ), "RES 0,A" ); r += run_test( test18_data, sizeof( test18_data ), "SET 1,A" ); + /* CB prefix BIT/RES/SET — remaining registers B, C, D, E, H, L */ + r += run_test( test308_data, sizeof( test308_data ), "BIT 3,B" ); + r += run_test( test309_data, sizeof( test309_data ), "BIT 5,C" ); + r += run_test( test310_data, sizeof( test310_data ), "RES 4,D" ); + r += run_test( test311_data, sizeof( test311_data ), "RES 2,H" ); + r += run_test( test312_data, sizeof( test312_data ), "SET 6,L" ); + r += run_test( test313_data, sizeof( test313_data ), "SET 3,E" ); + /* DD CB prefix BIT/RES/SET on (IX+d) */ r += run_test( test19_data, sizeof( test19_data ), "BIT 0,(IX+55)" ); r += run_test( test20_data, sizeof( test20_data ), "RES 0,(IX+55)" ); From 4b91a65a38c4785bc1e332b19e6c601b95667f81 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 1 Jul 2026 22:53:48 +1000 Subject: [PATCH 107/142] Add deparse tests for DEBUGGER_TOKEN_DEREFERENCE The deparse_unaryop() function handles DEBUGGER_TOKEN_DEREFERENCE by wrapping the operand in square brackets, but this code path had no unit-test coverage. Add two tests: - deparse-dereference: [0x5] (simple number operand) - deparse-dereference-expr: [0x100 + 0xf] (expression operand) --- debugger/expression.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index 0a05cc02..343d45d5 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -1097,6 +1097,21 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "~( 0xf0 | 0xf )", "deparse-bitwise-not-expr" ); + r += deparse_test( + debugger_expression_new_unaryop( DEBUGGER_TOKEN_DEREFERENCE, + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "[0x5]", "deparse-dereference" ); + + r += deparse_test( + debugger_expression_new_unaryop( DEBUGGER_TOKEN_DEREFERENCE, + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 0x100, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0x0f, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "[0x100 + 0xf]", "deparse-dereference-expr" ); + /* Deparse in decimal base: small value and a value > INT_MAX */ debugger_output_base = 10; From 2426cdd922f6ac21b67b417478f949799ffba68f Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 1 Jul 2026 22:55:41 +1000 Subject: [PATCH 108/142] Add remaining CB rotation/shift tests for registers D, E, H, L MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers D, E, H, L each had only RLC tested (test304-307). This commit adds the other seven rotation/shift operations for each register, completing single-register CB-prefix coverage across the entire 8×8 op×register matrix. New tests (28 total, test314-test341): D (rrr=010): RRC, RL, RR, SLA, SRA, SLL, SRL (test314-320) E (rrr=011): RRC, RL, RR, SLA, SRA, SLL, SRL (test321-327) H (rrr=100): RRC, RL, RR, SLA, SRA, SLL, SRL (test328-334) L (rrr=101): RRC, RL, RR, SLA, SRA, SLL, SRL (test335-341) --- debugger/disassemble.c | 75 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/debugger/disassemble.c b/debugger/disassemble.c index 8677d1cb..10ffbb7c 100644 --- a/debugger/disassemble.c +++ b/debugger/disassemble.c @@ -1315,6 +1315,45 @@ static libspectrum_byte test311_data[] = { 0xcb, 0x94 }; /* RES 2,H */ static libspectrum_byte test312_data[] = { 0xcb, 0xf5 }; /* SET 6,L */ static libspectrum_byte test313_data[] = { 0xcb, 0xdb }; /* SET 3,E */ +/* CB prefix: remaining 7 rotation/shift ops for each of D, E, H, L. + Completes single-register coverage so every op×register combination + in the rotate_op()/source_reg() dispatch path is exercised. + Opcode = (op_index << 3) | register_index, with: + op: 0=RLC 1=RRC 2=RL 3=RR 4=SLA 5=SRA 6=SLL 7=SRL + reg: 2=D 3=E 4=H 5=L */ +/* D (rrr = 010) */ +static libspectrum_byte test314_data[] = { 0xcb, 0x0a }; /* RRC D */ +static libspectrum_byte test315_data[] = { 0xcb, 0x12 }; /* RL D */ +static libspectrum_byte test316_data[] = { 0xcb, 0x1a }; /* RR D */ +static libspectrum_byte test317_data[] = { 0xcb, 0x22 }; /* SLA D */ +static libspectrum_byte test318_data[] = { 0xcb, 0x2a }; /* SRA D */ +static libspectrum_byte test319_data[] = { 0xcb, 0x32 }; /* SLL D (undocumented) */ +static libspectrum_byte test320_data[] = { 0xcb, 0x3a }; /* SRL D */ +/* E (rrr = 011) */ +static libspectrum_byte test321_data[] = { 0xcb, 0x0b }; /* RRC E */ +static libspectrum_byte test322_data[] = { 0xcb, 0x13 }; /* RL E */ +static libspectrum_byte test323_data[] = { 0xcb, 0x1b }; /* RR E */ +static libspectrum_byte test324_data[] = { 0xcb, 0x23 }; /* SLA E */ +static libspectrum_byte test325_data[] = { 0xcb, 0x2b }; /* SRA E */ +static libspectrum_byte test326_data[] = { 0xcb, 0x33 }; /* SLL E (undocumented) */ +static libspectrum_byte test327_data[] = { 0xcb, 0x3b }; /* SRL E */ +/* H (rrr = 100) */ +static libspectrum_byte test328_data[] = { 0xcb, 0x0c }; /* RRC H */ +static libspectrum_byte test329_data[] = { 0xcb, 0x14 }; /* RL H */ +static libspectrum_byte test330_data[] = { 0xcb, 0x1c }; /* RR H */ +static libspectrum_byte test331_data[] = { 0xcb, 0x24 }; /* SLA H */ +static libspectrum_byte test332_data[] = { 0xcb, 0x2c }; /* SRA H */ +static libspectrum_byte test333_data[] = { 0xcb, 0x34 }; /* SLL H (undocumented) */ +static libspectrum_byte test334_data[] = { 0xcb, 0x3c }; /* SRL H */ +/* L (rrr = 101) */ +static libspectrum_byte test335_data[] = { 0xcb, 0x0d }; /* RRC L */ +static libspectrum_byte test336_data[] = { 0xcb, 0x15 }; /* RL L */ +static libspectrum_byte test337_data[] = { 0xcb, 0x1d }; /* RR L */ +static libspectrum_byte test338_data[] = { 0xcb, 0x25 }; /* SLA L */ +static libspectrum_byte test339_data[] = { 0xcb, 0x2d }; /* SRA L */ +static libspectrum_byte test340_data[] = { 0xcb, 0x35 }; /* SLL L (undocumented) */ +static libspectrum_byte test341_data[] = { 0xcb, 0x3d }; /* SRL L */ + static int run_test( libspectrum_byte *data, size_t data_length, const char *expected ) @@ -1782,6 +1821,42 @@ debugger_disassemble_unittest( void ) r += run_test( test306_data, sizeof( test306_data ), "RLC H" ); r += run_test( test307_data, sizeof( test307_data ), "RLC L" ); + /* CB prefix: remaining rotation/shift ops for D (rrr = 010) */ + r += run_test( test314_data, sizeof( test314_data ), "RRC D" ); + r += run_test( test315_data, sizeof( test315_data ), "RL D" ); + r += run_test( test316_data, sizeof( test316_data ), "RR D" ); + r += run_test( test317_data, sizeof( test317_data ), "SLA D" ); + r += run_test( test318_data, sizeof( test318_data ), "SRA D" ); + r += run_test( test319_data, sizeof( test319_data ), "SLL D" ); + r += run_test( test320_data, sizeof( test320_data ), "SRL D" ); + + /* CB prefix: remaining rotation/shift ops for E (rrr = 011) */ + r += run_test( test321_data, sizeof( test321_data ), "RRC E" ); + r += run_test( test322_data, sizeof( test322_data ), "RL E" ); + r += run_test( test323_data, sizeof( test323_data ), "RR E" ); + r += run_test( test324_data, sizeof( test324_data ), "SLA E" ); + r += run_test( test325_data, sizeof( test325_data ), "SRA E" ); + r += run_test( test326_data, sizeof( test326_data ), "SLL E" ); + r += run_test( test327_data, sizeof( test327_data ), "SRL E" ); + + /* CB prefix: remaining rotation/shift ops for H (rrr = 100) */ + r += run_test( test328_data, sizeof( test328_data ), "RRC H" ); + r += run_test( test329_data, sizeof( test329_data ), "RL H" ); + r += run_test( test330_data, sizeof( test330_data ), "RR H" ); + r += run_test( test331_data, sizeof( test331_data ), "SLA H" ); + r += run_test( test332_data, sizeof( test332_data ), "SRA H" ); + r += run_test( test333_data, sizeof( test333_data ), "SLL H" ); + r += run_test( test334_data, sizeof( test334_data ), "SRL H" ); + + /* CB prefix: remaining rotation/shift ops for L (rrr = 101) */ + r += run_test( test335_data, sizeof( test335_data ), "RRC L" ); + r += run_test( test336_data, sizeof( test336_data ), "RL L" ); + r += run_test( test337_data, sizeof( test337_data ), "RR L" ); + r += run_test( test338_data, sizeof( test338_data ), "SLA L" ); + r += run_test( test339_data, sizeof( test339_data ), "SRA L" ); + r += run_test( test340_data, sizeof( test340_data ), "SLL L" ); + r += run_test( test341_data, sizeof( test341_data ), "SRL L" ); + return r; } From 4f5982b560cb5b65c21fb5512a3bff3201b3a668 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 2 Jul 2026 23:12:04 +1000 Subject: [PATCH 109/142] Add unit tests for user-defined variable expressions Add eval_variable_test() helper and six new tests exercising the DEBUGGER_EXPRESSION_TYPE_VARIABLE code path in expression.c: - deparse-variable: $counter deparsed to "$counter" - deparse-variable-in-binop: $counter + 0x1 deparsed correctly - variable-undefined-returns-zero: unset variable evaluates to 0 - variable-get-after-set: debugger_variable_set/get round-trip - variable-in-binop-eval: $zz_x * 2 == 20 after setting $zz_x = 10 Previously the variable expression type was exercised only through the command parser; the evaluator and deparser had no direct unit test coverage. --- debugger/expression.c | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index 343d45d5..9a115966 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -702,6 +702,26 @@ eval_integer_test( libspectrum_dword value, libspectrum_dword expected ) return 0; } +static int +eval_variable_test( const char *name, libspectrum_dword expected, + const char *label ) +{ + debugger_expression *expr; + libspectrum_dword result; + + expr = debugger_expression_new_variable( name, MEMPOOL_UNTRACKED ); + result = debugger_expression_evaluate( expr ); + debugger_expression_delete( expr ); + + if( result != expected ) { + printf( "expression eval '%s': expected %u, got %u\n", + label, (unsigned)expected, (unsigned)result ); + return 1; + } + + return 0; +} + static int eval_binary_test( int op, libspectrum_dword v1, libspectrum_dword v2, libspectrum_dword expected, const char *label ) @@ -1112,6 +1132,44 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "[0x100 + 0xf]", "deparse-dereference-expr" ); + /* User-defined variable: deparse emits the name prefixed with '$' */ + r += deparse_test( + debugger_expression_new_variable( "counter", MEMPOOL_UNTRACKED ), + "$counter", "deparse-variable" ); + + /* Variable inside a binary expression */ + r += deparse_test( + debugger_expression_new_binaryop( '+', + debugger_expression_new_variable( "counter", MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "$counter + 0x1", "deparse-variable-in-binop" ); + + /* Variable evaluation: an unset variable returns 0 */ + r += eval_variable_test( "zz_undefined", 0, "variable-undefined-returns-zero" ); + + /* Variable evaluation: set then get */ + debugger_variable_set( "zz_counter", 42 ); + r += eval_variable_test( "zz_counter", 42, "variable-get-after-set" ); + + /* Variable inside an arithmetic expression: $zz_x * 2 == 20 */ + debugger_variable_set( "zz_x", 10 ); + { + debugger_expression *var, *two, *prod; + libspectrum_dword result; + + var = debugger_expression_new_variable( "zz_x", MEMPOOL_UNTRACKED ); + two = debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ); + prod = debugger_expression_new_binaryop( '*', var, two, MEMPOOL_UNTRACKED ); + result = debugger_expression_evaluate( prod ); + debugger_expression_delete( prod ); + if( result != 20 ) { + printf( "expression eval 'variable-in-binop-eval': expected 20, got %u\n", + (unsigned)result ); + r++; + } + } + /* Deparse in decimal base: small value and a value > INT_MAX */ debugger_output_base = 10; From 79b224e89e1a6be34c30bba08717f8c1b6c46e70 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Fri, 3 Jul 2026 20:38:39 +1000 Subject: [PATCH 110/142] Add const to expression evaluate/copy functions Both debugger_expression_evaluate() and debugger_expression_copy() only read their expression arguments. Mark the parameters const to make that explicit and allow callers to pass const expressions. The two static helpers evaluate_unaryop() and evaluate_binaryop() are updated consistently. --- debugger/debugger_internals.h | 4 ++-- debugger/expression.c | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/debugger/debugger_internals.h b/debugger/debugger_internals.h index 3d7a8d95..e41988c4 100644 --- a/debugger/debugger_internals.h +++ b/debugger/debugger_internals.h @@ -90,11 +90,11 @@ debugger_expression_new_system_variable( const char *type, const char *detail, debugger_expression* debugger_expression_new_variable( const char *name, int pool ); -debugger_expression* debugger_expression_copy( debugger_expression *src ); +debugger_expression* debugger_expression_copy( const debugger_expression *src ); void debugger_expression_delete( debugger_expression* expression ); libspectrum_dword -debugger_expression_evaluate( debugger_expression* expression ); +debugger_expression_evaluate( const debugger_expression* expression ); /* Event handling */ diff --git a/debugger/expression.c b/debugger/expression.c index 9a115966..c079d719 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -94,8 +94,8 @@ struct debugger_expression { }; -static libspectrum_dword evaluate_unaryop( struct unaryop_type *unaryop ); -static libspectrum_dword evaluate_binaryop( struct binaryop_type *binary ); +static libspectrum_dword evaluate_unaryop( const struct unaryop_type *unaryop ); +static libspectrum_dword evaluate_binaryop( const struct binaryop_type *binary ); static int deparse_unaryop( char *buffer, size_t length, const struct unaryop_type *unaryop ); @@ -266,7 +266,7 @@ debugger_expression_delete( debugger_expression *exp ) } debugger_expression* -debugger_expression_copy( debugger_expression *src ) +debugger_expression_copy( const debugger_expression *src ) { debugger_expression *dest; @@ -321,7 +321,7 @@ debugger_expression_copy( debugger_expression *src ) } libspectrum_dword -debugger_expression_evaluate( debugger_expression *exp ) +debugger_expression_evaluate( const debugger_expression *exp ) { switch( exp->type ) { @@ -347,7 +347,7 @@ debugger_expression_evaluate( debugger_expression *exp ) } static libspectrum_dword -evaluate_unaryop( struct unaryop_type *unary ) +evaluate_unaryop( const struct unaryop_type *unary ) { switch( unary->operation ) { @@ -365,7 +365,7 @@ evaluate_unaryop( struct unaryop_type *unary ) } static libspectrum_dword -evaluate_binaryop( struct binaryop_type *binary ) +evaluate_binaryop( const struct binaryop_type *binary ) { switch( binary->operation ) { From becbb7f8c1f805ea01945f520a2a85c1f25eb49d Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Fri, 3 Jul 2026 20:42:02 +1000 Subject: [PATCH 111/142] Add copy and rshift-non-assoc tests - Add copy_eval_test() and copy_deparse_test() helpers that delete the original expression before using the copy, catching any shallow-copy bugs as use-after-free errors. - Add tests for all four expression types (integer, unaryop, binaryop, variable) through copy_eval_test and copy_deparse_test; previously debugger_expression_copy() had zero test coverage. - Add deparse-rshift-non-assoc to complement the existing deparse-lshift-non-assoc test: 16 >> (4 >> 2) must bracket the right operand because '>>' is non-associative. --- debugger/expression.c | 107 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index c079d719..7d152b6e 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -784,6 +784,52 @@ deparse_test( debugger_expression *expr, const char *expected, return 0; } +/* Copy-and-eval helper: takes ownership of expr, copies it, deletes + the original, evaluates the copy, and checks the result. */ +static int +copy_eval_test( debugger_expression *expr, libspectrum_dword expected, + const char *label ) +{ + debugger_expression *copy; + libspectrum_dword result; + + copy = debugger_expression_copy( expr ); + debugger_expression_delete( expr ); + result = debugger_expression_evaluate( copy ); + debugger_expression_delete( copy ); + + if( result != expected ) { + printf( "expression copy-eval '%s': expected %u, got %u\n", + label, (unsigned)expected, (unsigned)result ); + return 1; + } + + return 0; +} + +/* Copy-and-deparse helper: takes ownership of expr, copies it, deletes + the original, deparses the copy, and checks the result. */ +static int +copy_deparse_test( debugger_expression *expr, const char *expected, + const char *label ) +{ + debugger_expression *copy; + char buf[64]; + + copy = debugger_expression_copy( expr ); + debugger_expression_delete( expr ); + debugger_expression_deparse( buf, sizeof( buf ), copy ); + debugger_expression_delete( copy ); + + if( strcmp( buf, expected ) ) { + printf( "expression copy-deparse '%s': expected '%s', got '%s'\n", + label, expected, buf ); + return 1; + } + + return 0; +} + int debugger_expression_unittest( void ) { @@ -1044,6 +1090,17 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "0x1 << ( 0x2 << 0x3 )", "deparse-lshift-non-assoc" ); + /* '>>' is non-associative: 16 >> (4 >> 2) brackets the right operand */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_RIGHT_SHIFT, + debugger_expression_new_number( 16, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( DEBUGGER_TOKEN_RIGHT_SHIFT, + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x10 >> ( 0x4 >> 0x2 )", "deparse-rshift-non-assoc" ); + /* Cross-precedence: lower-precedence child is bracketed inside a higher-precedence parent (add inside mul) */ r += deparse_test( @@ -1170,6 +1227,56 @@ debugger_expression_unittest( void ) } } + /* debugger_expression_copy() tests: verify each expression type is deep- + copied correctly. The original is deleted before the copy is used so + a shallow copy would produce garbage or a use-after-free error. */ + + /* INTEGER copy: eval and deparse */ + r += copy_eval_test( + debugger_expression_new_number( 99, MEMPOOL_UNTRACKED ), + 99, "copy-eval-integer" ); + + r += copy_deparse_test( + debugger_expression_new_number( 7, MEMPOOL_UNTRACKED ), + "0x7", "copy-deparse-integer" ); + + /* UNARYOP copy: negate 5 → 0xFFFFFFFB */ + r += copy_eval_test( + debugger_expression_new_unaryop( '-', + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + (libspectrum_dword)-5, "copy-eval-unaryop" ); + + r += copy_deparse_test( + debugger_expression_new_unaryop( '-', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "-0x3", "copy-deparse-unaryop" ); + + /* BINARYOP copy: 6 + 7 = 13 */ + r += copy_eval_test( + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 6, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 7, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + 13, "copy-eval-binaryop" ); + + r += copy_deparse_test( + debugger_expression_new_binaryop( '+', + debugger_expression_new_number( 2, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x2 + 0x3", "copy-deparse-binaryop" ); + + /* VARIABLE copy: zz_counter was set to 42 above */ + r += copy_eval_test( + debugger_expression_new_variable( "zz_counter", MEMPOOL_UNTRACKED ), + 42, "copy-eval-variable" ); + + r += copy_deparse_test( + debugger_expression_new_variable( "zz_counter", MEMPOOL_UNTRACKED ), + "$zz_counter", "copy-deparse-variable" ); + /* Deparse in decimal base: small value and a value > INT_MAX */ debugger_output_base = 10; From a365e1a678ce7f4d1d38677924cf95ffcf5dda1b Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 5 Jul 2026 09:40:55 +1000 Subject: [PATCH 112/142] Add unit tests for DEBUGGER_EXPRESSION_TYPE_SYSVAR expressions Cover the four untested paths in SYSVAR handling: deparse, evaluate, copy-and-evaluate, and copy-and-deparse. A static getter get_sysvar_sentinel() returns 42 and is registered as the test:sentinel system variable so the tests run without depending on Z80 register state. --- debugger/expression.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index 7d152b6e..5957a007 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -830,6 +830,12 @@ copy_deparse_test( debugger_expression *expr, const char *expected, return 0; } +static libspectrum_dword +get_sysvar_sentinel( void ) +{ + return 42; +} + int debugger_expression_unittest( void ) { @@ -1277,6 +1283,42 @@ debugger_expression_unittest( void ) debugger_expression_new_variable( "zz_counter", MEMPOOL_UNTRACKED ), "$zz_counter", "copy-deparse-variable" ); + /* SYSVAR: register a read-only test sentinel that returns 42 */ + debugger_system_variable_register( "test", "sentinel", get_sysvar_sentinel, + NULL ); + + r += deparse_test( + debugger_expression_new_system_variable( "test", "sentinel", + MEMPOOL_UNTRACKED ), + "test:sentinel", "deparse-sysvar" ); + + /* System variable evaluation */ + { + debugger_expression *expr; + libspectrum_dword result; + + expr = debugger_expression_new_system_variable( "test", "sentinel", + MEMPOOL_UNTRACKED ); + result = debugger_expression_evaluate( expr ); + debugger_expression_delete( expr ); + if( result != 42 ) { + printf( "expression eval 'sysvar-eval': expected 42, got %u\n", + (unsigned)result ); + r++; + } + } + + /* SYSVAR copy: original deleted before copy is used */ + r += copy_eval_test( + debugger_expression_new_system_variable( "test", "sentinel", + MEMPOOL_UNTRACKED ), + 42, "copy-eval-sysvar" ); + + r += copy_deparse_test( + debugger_expression_new_system_variable( "test", "sentinel", + MEMPOOL_UNTRACKED ), + "test:sentinel", "copy-deparse-sysvar" ); + /* Deparse in decimal base: small value and a value > INT_MAX */ debugger_output_base = 10; From 00c19cd79853d21a62c2cb8e9169cf1e08a361ad Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 5 Jul 2026 22:14:47 +1000 Subject: [PATCH 113/142] =?UTF-8?q?Skip=20symbol=20characters=20on=20Wii?= =?UTF-8?q?=20UI=20(fixes=20bug=20#266)=20(Sergio=20Baldov=C3=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keysyms.pl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keysyms.pl b/keysyms.pl index 15569f0d..e1aefaaa 100755 --- a/keysyms.pl +++ b/keysyms.pl @@ -124,7 +124,11 @@ ($) 'Hyper_L','Hyper_R', 'Super_L','Super_R', 'KP_Enter', - 'Mode_switch' ) }, + 'Mode_switch', + 'dollar','less','greater','exclam', + 'ampersand','parenleft','parenright', + 'asterisk','plus','colon','bar', + 'asciicircum','dead_circumflex' ) }, function => \&wii_keysym }, From 27282b74e5f5f8ce9852d1b3366c8a0ce98f3493 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 15 Jul 2026 20:29:56 +1000 Subject: [PATCH 114/142] Remove redundant labels to simplify gtk3 joystick mapping dialog --- ui/gtk3/Makefile.am | 12 ++++---- ui/gtk3/gtkjoystick.c | 67 +++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/ui/gtk3/Makefile.am b/ui/gtk3/Makefile.am index 57e75a90..bdbaad08 100644 --- a/ui/gtk3/Makefile.am +++ b/ui/gtk3/Makefile.am @@ -46,7 +46,7 @@ gtkui_resource_files = \ gtkui_built_resource_files = \ ui/gtk3/menu_data.ui -CLEANFILES += $(ui_gtk_built) +CLEANFILES += $(ui_gtk_built) ui/gtk3/options.stamp ui_gtk_files = \ ui/gtk3/binary.c \ @@ -102,13 +102,13 @@ ui/gtk3/menu_data.ui: $(srcdir)/perl/cpp-perl.pl $(srcdir)/menu_data.pl $(srcdir @$(MKDIR_P) ui/gtk3 $(AM_V_GEN)$(PERL) $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/menu_data.dat | $(PERL) -I$(srcdir)/perl $(srcdir)/menu_data.pl gtk ui > $@.tmp && mv $@.tmp $@ -ui/gtk3/options.c: $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/gtk3/options.pl $(srcdir)/ui/options.dat $(srcdir)/perl/Fuse.pm $(srcdir)/perl/Fuse/Dialog.pm +ui/gtk3/options.stamp: $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/gtk3/options.pl $(srcdir)/ui/gtk3/options-header.pl $(srcdir)/ui/options.dat $(srcdir)/perl/Fuse.pm $(srcdir)/perl/Fuse/Dialog.pm @$(MKDIR_P) ui/gtk3 - $(AM_V_GEN)$(PERL) $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/options.dat | $(PERL) -I$(srcdir)/perl $(srcdir)/ui/gtk3/options.pl - > $@.tmp && mv $@.tmp $@ + $(AM_V_GEN)$(PERL) $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/options.dat | $(PERL) -I$(srcdir)/perl $(srcdir)/ui/gtk3/options.pl - > ui/gtk3/options.c.tmp && mv ui/gtk3/options.c.tmp ui/gtk3/options.c + $(AM_V_GEN)$(PERL) $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/options.dat | $(PERL) -I$(srcdir)/perl $(srcdir)/ui/gtk3/options-header.pl - > ui/gtk3/options_internals.h.tmp && mv ui/gtk3/options_internals.h.tmp ui/gtk3/options_internals.h + @touch $@ -ui/gtk3/options_internals.h: $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/gtk3/options-header.pl $(srcdir)/ui/options.dat $(srcdir)/perl/Fuse.pm $(srcdir)/perl/Fuse/Dialog.pm - @$(MKDIR_P) ui/gtk3 - $(AM_V_GEN)$(PERL) $(srcdir)/perl/cpp-perl.pl config.h $(srcdir)/ui/options.dat | $(PERL) -I$(srcdir)/perl $(srcdir)/ui/gtk3/options-header.pl - > $@.tmp && mv $@.tmp $@ +ui/gtk3/options.c ui/gtk3/options_internals.h: ui/gtk3/options.stamp ui/gtk3/resources.c: ui/gtk3/resources.xml $(gtkui_resource_files) $(gtkui_built_resource_files) $(AM_V_GEN)glib-compile-resources --sourcedir=$(srcdir) --sourcedir=$(builddir) --target=$@ --internal --generate-source --c-name=gtkui $< diff --git a/ui/gtk3/gtkjoystick.c b/ui/gtk3/gtkjoystick.c index 4ddf87af..6d745585 100644 --- a/ui/gtk3/gtkjoystick.c +++ b/ui/gtk3/gtkjoystick.c @@ -56,7 +56,6 @@ enum struct button_info { int *setting; char name[80]; - GtkWidget *label; keyboard_key_name key; }; @@ -86,7 +85,8 @@ static void create_joystick_type_selector( struct joystick_info *info, static void create_fire_button_selector( const char *title, struct button_info *info, GtkBox *parent, GtkTreeModel *model ); -static void set_key_text( GtkWidget *label, keyboard_key_name key ); +static gboolean find_key_iter( GtkTreeModel *model, GtkTreeIter *iter, + GtkTreeIter *match, keyboard_key_name key ); static void key_callback( GtkComboBox *widget, gpointer user_data ); static void joystick_done( GtkButton *button, gpointer user_data ); @@ -353,9 +353,7 @@ create_fire_button_selector( const char *title, struct button_info *info, { GtkWidget *frame, *box, *combo; GtkCellRenderer *renderer; - GtkTreeIter iter; - GtkTreePath *path; - size_t i; + GtkTreeIter iter, selected; frame = gtk_frame_new( title ); gtk_box_pack_start( parent, frame, TRUE, TRUE, 0 ); @@ -364,27 +362,10 @@ create_fire_button_selector( const char *title, struct button_info *info, gtk_container_set_border_width( GTK_CONTAINER( box ), 2 ); gtk_container_add( GTK_CONTAINER( frame ), box ); - /* Create label */ info->key = *info->setting; - info->label = gtk_label_new( "" ); - - for( i = 0; i < ARRAY_SIZE( key_menu ); i++ ) { - - keyboard_key_name key; - - key = key_menu[i].key; - - if( key_menu[i].item != KEY_GROUP && key == (unsigned int)*info->setting ) { - set_key_text( info->label, key ); - break; - } - - } - - gtk_box_pack_start( GTK_BOX( box ), info->label, TRUE, TRUE, 0 ); /* Create combobox */ - combo = gtk_combo_box_new_with_model( model ); + combo = gtk_combo_box_new_with_model( model ); renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( combo ), renderer, TRUE ); gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( combo ), renderer, @@ -392,11 +373,9 @@ create_fire_button_selector( const char *title, struct button_info *info, gtk_cell_layout_set_cell_data_func( GTK_CELL_LAYOUT( combo ), renderer, set_entry_properties, NULL, NULL ); - /* Select first item */ - path = gtk_tree_path_new_from_indices( 0, -1 ); - gtk_tree_model_get_iter( model, &iter, path ); - gtk_tree_path_free( path ); - gtk_combo_box_set_active_iter( GTK_COMBO_BOX( combo ), &iter ); + if( gtk_tree_model_get_iter_first( model, &iter ) && + find_key_iter( model, &iter, &selected, info->key ) ) + gtk_combo_box_set_active_iter( GTK_COMBO_BOX( combo ), &selected ); gtk_box_pack_start( GTK_BOX( box ), combo, TRUE, TRUE, 0 ); @@ -404,17 +383,32 @@ create_fire_button_selector( const char *title, struct button_info *info, info ); } -static void -set_key_text( GtkWidget *label, keyboard_key_name key ) +static gboolean +find_key_iter( GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *match, + keyboard_key_name key ) { - const char *text; - char buffer[40]; + do { + GValue value = G_VALUE_INIT; + GtkTreeIter child; + + gtk_tree_model_get_value( model, iter, COL_KEY, &value ); + + if( g_value_get_int( &value ) == key && + !gtk_tree_model_iter_has_child( model, iter ) ) { + *match = *iter; + g_value_unset( &value ); + return TRUE; + } + + g_value_unset( &value ); - text = keyboard_key_text( key ); + if( gtk_tree_model_iter_children( model, &child, iter ) && + find_key_iter( model, &child, match, key ) ) + return TRUE; - snprintf( buffer, 40, "%s", text ); + } while( gtk_tree_model_iter_next( model, iter ) ); - gtk_label_set_text( GTK_LABEL( label ), buffer ); + return FALSE; } static void @@ -434,9 +428,8 @@ key_callback( GtkComboBox *widget, gpointer user_data ) key = g_value_get_int( &value ); g_value_unset( &value ); - /* Store and display selection */ + /* Store selection */ info->key = key; - set_key_text( info->label, info->key ); } static void From 5f82f247da871cbd7d2ff8208b69e69416dd0b9a Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 15 Jul 2026 20:52:29 +1000 Subject: [PATCH 115/142] Fix off-by-one in svg_startcapture allocation strlen() returns the length without the null terminator, so libspectrum_new( char, strlen(name) ) allocates one byte too few for the string copy. The subsequent strcpy() then writes one byte past the end of the allocation, causing heap corruption whenever svg_startcapture() is called with a non-empty name. Fix by allocating strlen(name) + 1 bytes so the terminator fits. --- svg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg.c b/svg.c index b506123c..33bd7947 100644 --- a/svg.c +++ b/svg.c @@ -323,7 +323,7 @@ svg_startcapture( const char *name, svg_capture_type mode ) if( name == NULL || *name == '\0' ) name = "fuse"; - svg_fnameroot = libspectrum_new( char, strlen ( name ) ); + svg_fnameroot = libspectrum_new( char, strlen( name ) + 1 ); strcpy( svg_fnameroot, name ); svg_filecount = 0; From d117c3add66591c569893c4fd088db3bdde35a5e Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 15 Jul 2026 20:59:28 +1000 Subject: [PATCH 116/142] Add scaler_for_size unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- unittests/unittests.c | 129 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/unittests/unittests.c b/unittests/unittests.c index 67979978..a9924900 100644 --- a/unittests/unittests.c +++ b/unittests/unittests.c @@ -56,6 +56,7 @@ #include "snapshot.h" #include "bitmap.h" #include "rectangle.h" +#include "ui/scaler/scaler.h" #include "unittests.h" #include "utils.h" @@ -1221,6 +1222,133 @@ rectangle_realloc_test( void ) return 0; } +/* Unit tests for scaler_for_size(). + The function looks up which family a scaler belongs to and returns the + scaler in that family matching the requested 1x–4x size. Tests cover: + - all four size positions in the Normal family + - lookup starting from a non-1x family member + - size clamping (< 1 → 1, > 4 → 4) + - scaler not in any fully-registered family returns unchanged + - family with fewer than all four members registered returns unchanged + - the TV family's duplicate 1x/2x slot (TV2X appears twice) + - the Timex family's remapping (SCALER_NORMAL maps to position 2) */ +static int +scaler_for_size_test( void ) +{ + int r = 0; + + /* --- Normal family: NORMAL(1x), DOUBLESIZE(2x), TRIPLESIZE(3x), QUADSIZE(4x) --- */ + scaler_register_clear(); + scaler_register( SCALER_NORMAL ); + scaler_register( SCALER_DOUBLESIZE ); + scaler_register( SCALER_TRIPLESIZE ); + scaler_register( SCALER_QUADSIZE ); + + if( scaler_for_size( SCALER_NORMAL, 1 ) != SCALER_NORMAL ) { + printf( "scaler_for_size: normal-at-1x: expected SCALER_NORMAL\n" ); + r++; + } + if( scaler_for_size( SCALER_NORMAL, 2 ) != SCALER_DOUBLESIZE ) { + printf( "scaler_for_size: normal-at-2x: expected SCALER_DOUBLESIZE\n" ); + r++; + } + if( scaler_for_size( SCALER_NORMAL, 3 ) != SCALER_TRIPLESIZE ) { + printf( "scaler_for_size: normal-at-3x: expected SCALER_TRIPLESIZE\n" ); + r++; + } + if( scaler_for_size( SCALER_NORMAL, 4 ) != SCALER_QUADSIZE ) { + printf( "scaler_for_size: normal-at-4x: expected SCALER_QUADSIZE\n" ); + r++; + } + + /* Lookup from a non-1x family member still resolves the correct size */ + if( scaler_for_size( SCALER_DOUBLESIZE, 1 ) != SCALER_NORMAL ) { + printf( "scaler_for_size: doublesize-at-1x: expected SCALER_NORMAL\n" ); + r++; + } + if( scaler_for_size( SCALER_TRIPLESIZE, 4 ) != SCALER_QUADSIZE ) { + printf( "scaler_for_size: triplesize-at-4x: expected SCALER_QUADSIZE\n" ); + r++; + } + + /* Size clamping: values < 1 and > 4 are clamped */ + if( scaler_for_size( SCALER_NORMAL, 0 ) != SCALER_NORMAL ) { + printf( "scaler_for_size: normal-at-0: expected SCALER_NORMAL (clamp to 1)\n" ); + r++; + } + if( scaler_for_size( SCALER_NORMAL, 5 ) != SCALER_QUADSIZE ) { + printf( "scaler_for_size: normal-at-5: expected SCALER_QUADSIZE (clamp to 4)\n" ); + r++; + } + + /* Scaler not in any family with all members registered: returned unchanged. + SCALER_DOTMATRIX is a single-size scaler and not listed in any family row. */ + scaler_register( SCALER_DOTMATRIX ); + if( scaler_for_size( SCALER_DOTMATRIX, 2 ) != SCALER_DOTMATRIX ) { + printf( "scaler_for_size: dotmatrix-at-2x: expected SCALER_DOTMATRIX (no family)\n" ); + r++; + } + + /* --- Incomplete family: only PALTV2X registered, not PALTV3X or PALTV4X --- */ + scaler_register_clear(); + scaler_register( SCALER_PALTV2X ); + if( scaler_for_size( SCALER_PALTV2X, 3 ) != SCALER_PALTV2X ) { + printf( "scaler_for_size: paltv2x-incomplete: expected SCALER_PALTV2X\n" ); + r++; + } + + /* --- TV family: TV2X appears in both the 1x and 2x slot --- */ + scaler_register_clear(); + scaler_register( SCALER_TV2X ); + scaler_register( SCALER_TV3X ); + scaler_register( SCALER_TV4X ); + + if( scaler_for_size( SCALER_TV2X, 1 ) != SCALER_TV2X ) { + printf( "scaler_for_size: tv2x-at-1x: expected SCALER_TV2X\n" ); + r++; + } + if( scaler_for_size( SCALER_TV2X, 2 ) != SCALER_TV2X ) { + printf( "scaler_for_size: tv2x-at-2x: expected SCALER_TV2X\n" ); + r++; + } + if( scaler_for_size( SCALER_TV2X, 3 ) != SCALER_TV3X ) { + printf( "scaler_for_size: tv2x-at-3x: expected SCALER_TV3X\n" ); + r++; + } + if( scaler_for_size( SCALER_TV2X, 4 ) != SCALER_TV4X ) { + printf( "scaler_for_size: tv2x-at-4x: expected SCALER_TV4X\n" ); + r++; + } + + /* --- Timex family: HALF(1x), NORMAL(2x), TIMEX1_5X(3x), TIMEX2X(4x). + With this family registered, SCALER_NORMAL is at position 2 (size 2), so + requesting size 1 from SCALER_NORMAL returns SCALER_HALF. --- */ + scaler_register_clear(); + scaler_register( SCALER_HALF ); + scaler_register( SCALER_NORMAL ); + scaler_register( SCALER_TIMEX1_5X ); + scaler_register( SCALER_TIMEX2X ); + + if( scaler_for_size( SCALER_NORMAL, 1 ) != SCALER_HALF ) { + printf( "scaler_for_size: timex-normal-at-1x: expected SCALER_HALF\n" ); + r++; + } + if( scaler_for_size( SCALER_NORMAL, 2 ) != SCALER_NORMAL ) { + printf( "scaler_for_size: timex-normal-at-2x: expected SCALER_NORMAL\n" ); + r++; + } + if( scaler_for_size( SCALER_HALF, 3 ) != SCALER_TIMEX1_5X ) { + printf( "scaler_for_size: timex-half-at-3x: expected SCALER_TIMEX1_5X\n" ); + r++; + } + if( scaler_for_size( SCALER_TIMEX2X, 1 ) != SCALER_HALF ) { + printf( "scaler_for_size: timex-timex2x-at-1x: expected SCALER_HALF\n" ); + r++; + } + + return r; +} + int unittests_run( void ) { @@ -1240,6 +1368,7 @@ unittests_run( void ) r += debugger_expression_unittest(); r += rectangle_test(); r += rectangle_realloc_test(); + r += scaler_for_size_test(); printf("Final return value: %d (should be 0)\n", r); From e788f18a033a238fcec34918d92cff7a26b22262 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 15 Jul 2026 21:03:02 +1000 Subject: [PATCH 117/142] Add comparison non-associativity deparse tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- debugger/expression.c | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/debugger/expression.c b/debugger/expression.c index 5957a007..07b8b093 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -1025,6 +1025,67 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "0x6 >= 0x5", "deparse-gte" ); + /* Comparison non-associativity: at equal precedence the result is always + bracketed (regardless of position) because comparison operators return + truth values and chaining them is almost always a mistake. */ + + /* 3 == (4 == 5): right operand at equal EQUALITY precedence — brackets */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_EQUAL_TO, + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( DEBUGGER_TOKEN_EQUAL_TO, + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 == ( 0x4 == 0x5 )", "deparse-eq-non-assoc" ); + + /* 3 < (4 < 5): right operand at equal COMPARISON precedence — brackets */ + r += deparse_test( + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 < ( 0x4 < 0x5 )", "deparse-lt-non-assoc-right" ); + + /* (3 < 4) < 5: left operand at equal COMPARISON precedence — brackets */ + r += deparse_test( + debugger_expression_new_binaryop( '<', + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "( 0x3 < 0x4 ) < 0x5", "deparse-lt-non-assoc-left" ); + + /* (3 < 4) == 5: COMPARISON child of EQUALITY parent — higher prec, no + brackets needed; re-parsing preserves the original evaluation order */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_EQUAL_TO, + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 < 0x4 == 0x5", "deparse-lt-inside-eq" ); + + /* 3 < (4 == 5): EQUALITY child of COMPARISON parent — lower prec forces + brackets so re-parsing does not swap to (3 < 4) == 5 */ + r += deparse_test( + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( DEBUGGER_TOKEN_EQUAL_TO, + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 < ( 0x4 == 0x5 )", "deparse-eq-inside-lt" ); + /* Bitwise operators */ r += deparse_test( debugger_expression_new_binaryop( '&', From 9bba793a1b0b4d25ab8386b5b35c6ec76ebea4e9 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 15 Jul 2026 21:06:02 +1000 Subject: [PATCH 118/142] Eliminate redundant attr lookup in display_write_if_dirty_timex() 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. --- display.c | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/display.c b/display.c index 154e31cd..dee716c2 100644 --- a/display.c +++ b/display.c @@ -127,9 +127,6 @@ int current_border[ DISPLAY_SCREEN_HEIGHT ][ DISPLAY_SCREEN_WIDTH_COLS ]; static void display_dirty8( libspectrum_word address ); static void display_dirty64( libspectrum_word address ); -static void display_get_attr( int x, int y, - libspectrum_byte *ink, libspectrum_byte *paper); - static int border_changes_last = 0; static struct border_change_t *border_changes = NULL; @@ -428,11 +425,15 @@ display_write_if_dirty_timex( int x, int y ) index = beam_x + beam_y * DISPLAY_SCREEN_WIDTH_COLS; if( display_last_screen[ index ] != last_chunk_detail ) { libspectrum_byte ink, paper; - display_get_attr( x, y, &ink, &paper ); if( scld_last_dec.name.hires ) { + /* In hires mode the attr byte is not in data2, so we must look it up. */ + display_parse_attr( display_get_attr_byte( x, y ), &ink, &paper ); libspectrum_word hires_data = (data << 8) + data2; uidisplay_plot16( beam_x, beam_y, hires_data, ink, paper ); } else { + /* In lores mode data2 already holds the attr byte (set above), so + parse it directly instead of reading it a second time. */ + display_parse_attr( data2, &ink, &paper ); uidisplay_plot8( beam_x, beam_y, data, ink, paper ); } @@ -737,15 +738,6 @@ display_dirty64( libspectrum_word offset ) for( i = 0; i < 8; i++ ) display_dirty_chunk( x, y + i ); } -/* Get the attributes for the eight pixels starting at - ( (8*x) , y ) */ -static void -display_get_attr( int x, int y, - libspectrum_byte *ink, libspectrum_byte *paper ) -{ - display_parse_attr( display_get_attr_byte( x, y ), ink, paper ); -} - void display_parse_attr( libspectrum_byte attr, libspectrum_byte *ink, libspectrum_byte *paper ) From f9a1659d701996a9aeb9fdd291c9569859e3556b Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Wed, 15 Jul 2026 21:11:03 +1000 Subject: [PATCH 119/142] Update hacking/unittests.txt to reflect current test suite --- hacking/unittests.txt | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/hacking/unittests.txt b/hacking/unittests.txt index 4d824158..b2f86f56 100644 --- a/hacking/unittests.txt +++ b/hacking/unittests.txt @@ -1,23 +1,41 @@ Running Fuse's unit tests ========================= -Fuse's unit tests are built into the main `fuse' binary rather than -being exposed as separate Automake `TESTS' executables. - -The normal developer entry point is: +Fuse's test suite is split across several test binaries. The normal +developer entry point is: make check -This builds `fuse' and runs the embedded unit test framework via -`./fuse --unittests'. Contributors should run it before submitting -changes. +This builds all test binaries and runs them in sequence: -If you want to run the test framework directly, use: + fuse --unittests + Embedded tests for the debugger expression evaluator, + disassembler, memory paging, and other core subsystems. + Exits 0 on success or non-zero on failure. - ./fuse --unittests + unittests/displaytest + Display write/dirty tests covering Sinclair, Timex, and + Pentagon screen modes. + + unittests/sdl2displaytest + unittests/sdl2scalerstatetest + unittests/sdl2joysticktest + unittests/sdl2mousetest + SDL2 UI subsystem tests (display pipeline, scaler state + machine, joystick mapping, mouse input). + + z80/coretest z80/tests/tests.in + Z80 CPU core regression suite. Output is written to + z80/tests.actual and compared against + z80/tests/tests.expected using cmp(1). -The `--unittests' option runs the embedded test framework, prints any -failures to stdout, and exits with status 0 on success or a non-zero -status on failure. +Contributors should run `make check' before submitting changes. + +To run a single test binary directly (from the build directory), use +e.g.: + + ./fuse --unittests + ./unittests/displaytest + ./z80/coretest z80/tests/tests.in For manual peripheral smoke tests, see `hacking/peripheral_tests.txt'. From 8b832800f98ff070082f0385aae07f585b22ef07 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Fri, 17 Jul 2026 21:42:08 +1000 Subject: [PATCH 120/142] Replace attribute dirty-tracking lookup tables with arithmetic 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(). --- display.c | 25 ++++++++----------------- unittests/displaytest.c | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/display.c b/display.c index dee716c2..1d408ebf 100644 --- a/display.c +++ b/display.c @@ -68,14 +68,6 @@ static libspectrum_word static libspectrum_word display_dirty_xtable[ DISPLAY_WIDTH_COLS * DISPLAY_HEIGHT ]; -/* If you write to the byte at display_dirty_?table2[n+0x5800], then - the 64 pixels starting at (8*xtable2[n],ytable2[n]) must be - replotted */ -static libspectrum_word - display_dirty_ytable2[ DISPLAY_WIDTH_COLS * DISPLAY_HEIGHT_ROWS ]; -static libspectrum_word - display_dirty_xtable2[ DISPLAY_WIDTH_COLS * DISPLAY_HEIGHT_ROWS ]; - /* The number of frames mod 32 that have elapsed. 0<=d_f_c<16 => Flashing characters are normal 16<=d_f_c<32 => Flashing characters are reversed @@ -185,12 +177,6 @@ display_init( int *argc, char ***argv ) display_dirty_xtable[ display_line_start[y]+x ] = x; } - for(y=0;y= 184 ? ( (libspectrum_dword)1 << 31 ) : 0; + + if( display_get_maybe_dirty( y ) != expected ) return 1; + } + + return 0; +} + /* display_dirty_flashing_sinclair() tests */ static int @@ -869,6 +890,7 @@ static const struct test_t tests[] = { no_write_if_dirty_area_ahead_of_beam }, { "no_write_if_modified_area_ahead_of_critical_region", no_write_if_modified_area_ahead_of_critical_region }, + { "attribute_write_marks_correct_cell", attribute_write_marks_correct_cell }, /* display_dirty_flashing_sinclair() tests */ { "flash_dirty_no_flash_attrs", flash_dirty_no_flash_attrs }, From b360274875a347d7728c4f73bf6717d7ae2fcd91 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Fri, 17 Jul 2026 21:46:09 +1000 Subject: [PATCH 121/142] Add const to event/sysvar helper functions and deparse tests for logical operator precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 '*'. --- debugger/event.c | 15 +++++---- debugger/expression.c | 65 ++++++++++++++++++++++++++++++++++++++ debugger/system_variable.c | 14 ++++---- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/debugger/event.c b/debugger/event.c index c745f37e..5c151ca1 100644 --- a/debugger/event.c +++ b/debugger/event.c @@ -62,7 +62,8 @@ debugger_event_register( const char *type, const char *detail ) } static int -event_matches( debugger_event_t *event, const char *type, const char *detail ) +event_matches( const debugger_event_t *event, const char *type, + const char *detail ) { if( strcasecmp( type, event->type ) ) return 0; if( strcmp( detail, "*" ) == 0 ) return 1; @@ -76,10 +77,10 @@ debugger_event_is_registered( const char *type, const char *detail ) size_t i; for( i = 0; i < registered_events->len; i++ ) { - debugger_event_t event = - g_array_index( registered_events, debugger_event_t, i ); + const debugger_event_t *event = + &g_array_index( registered_events, debugger_event_t, i ); - if( event_matches( &event, type, detail ) ) return 1; + if( event_matches( event, type, detail ) ) return 1; } return 0; @@ -88,7 +89,7 @@ debugger_event_is_registered( const char *type, const char *detail ) void debugger_event( int event_code ) { - debugger_event_t event; + const debugger_event_t *event; debugger_breakpoint *bp; GSList *ptr, *ptr_next; @@ -100,7 +101,7 @@ debugger_event( int event_code ) fuse_abort(); } - event = g_array_index( registered_events, debugger_event_t, event_code ); + event = &g_array_index( registered_events, debugger_event_t, event_code ); for( ptr = debugger_breakpoints; ptr; ptr = ptr_next ) { @@ -109,7 +110,7 @@ debugger_event( int event_code ) if( bp->type != DEBUGGER_BREAKPOINT_TYPE_EVENT ) continue; - if( event_matches( &bp->value.event, event.type, event.detail ) && + if( event_matches( &bp->value.event, event->type, event->detail ) && debugger_breakpoint_trigger( bp ) ) { debugger_mode = DEBUGGER_MODE_HALTED; debugger_command_evaluate( bp->commands ); diff --git a/debugger/expression.c b/debugger/expression.c index 07b8b093..5e4142ff 100644 --- a/debugger/expression.c +++ b/debugger/expression.c @@ -1123,6 +1123,71 @@ debugger_expression_unittest( void ) MEMPOOL_UNTRACKED ), "0x1 || 0x0", "deparse-logical-or" ); + /* Precedence interactions involving logical operators */ + + /* && has higher precedence than ||: '||' parent with '&&' right child + needs no brackets because && binds more tightly */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_OR, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_AND, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x1 || 0x1 && 0x0", "deparse-and-inside-or" ); + + /* '&&' parent with '||' left child: || has lower precedence than && + so the left operand must be bracketed */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_AND, + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_OR, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 0, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "( 0x1 || 0x0 ) && 0x1", "deparse-or-inside-and" ); + + /* Comparison operators have higher precedence than &&: 'a < b && c > d' + needs no brackets around either comparison */ + r += deparse_test( + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_AND, + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( '>', + debugger_expression_new_number( 6, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 < 0x5 && 0x6 > 0x4", "deparse-cmp-inside-and" ); + + /* '&&' inside a comparison: && has lower precedence than '<' so the + right operand must be bracketed */ + r += deparse_test( + debugger_expression_new_binaryop( '<', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( DEBUGGER_TOKEN_LOGICAL_AND, + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 1, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 < ( 0x1 && 0x1 )", "deparse-and-inside-cmp" ); + + /* '*' parent with '%' child: % is non-associative so even though both + have the same precedence, the child must be bracketed */ + r += deparse_test( + debugger_expression_new_binaryop( '*', + debugger_expression_new_number( 3, MEMPOOL_UNTRACKED ), + debugger_expression_new_binaryop( '%', + debugger_expression_new_number( 4, MEMPOOL_UNTRACKED ), + debugger_expression_new_number( 5, MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + MEMPOOL_UNTRACKED ), + "0x3 * ( 0x4 % 0x5 )", "deparse-mul-mod-non-assoc-right" ); + /* Binary '-' is non-associative: 3-(4-2) must bracket the right operand */ r += deparse_test( debugger_expression_new_binaryop( '-', diff --git a/debugger/system_variable.c b/debugger/system_variable.c index 314b180f..e64943ff 100644 --- a/debugger/system_variable.c +++ b/debugger/system_variable.c @@ -70,23 +70,25 @@ debugger_system_variable_register( const char *type, const char *detail, } static int -system_variable_matches( system_variable_t *sysvar, const char *type, const char *detail ) +system_variable_matches( const system_variable_t *sysvar, const char *type, + const char *detail ) { return strcasecmp( type, sysvar->type ) == 0 && strcasecmp( detail, sysvar->detail ) == 0; } static int -find_system_variable( const char *type, const char *detail, system_variable_t *out ) +find_system_variable( const char *type, const char *detail, + system_variable_t *out ) { size_t i; for( i = 0; i < system_variables->len; i++ ) { - system_variable_t sysvar = - g_array_index( system_variables, system_variable_t, i ); + const system_variable_t *sysvar = + &g_array_index( system_variables, system_variable_t, i ); - if( system_variable_matches( &sysvar, type, detail ) ) { - if( out != NULL ) *out = sysvar; + if( system_variable_matches( sysvar, type, detail ) ) { + if( out != NULL ) *out = *sysvar; return i; } } From e70b37c0e6b44836d4b773a89ae4f4430a9b8064 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:25:26 +0000 Subject: [PATCH 122/142] sound: replace AY noise RNG conditionals with branch-free bit operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- sound.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sound.c b/sound.c index 24b0bfaa..e0782d86 100644 --- a/sound.c +++ b/sound.c @@ -624,16 +624,14 @@ sound_ay_overlay( void ) while( ay_noise_tick >= ay_noise_period ) { ay_noise_tick -= ay_noise_period; - if( ( rng & 1 ) ^ ( ( rng & 2 ) ? 1 : 0 ) ) - noise_toggle = !noise_toggle; - - /* rng is 17-bit shift reg, bit 0 is output. - * input is bit 0 xor bit 3. + /* rng is 17-bit LFSR, bit 0 is output. + * Feedback input is bit 0 XOR bit 1 (drives noise_toggle). + * Shift taps at bits 14 and 17 produce the next state. + * Rewritten branch-free: XOR/mask replaces two conditional branches + * whose outcomes are pseudo-random and therefore poorly predicted. */ - if( rng & 1 ) { - rng ^= 0x24000; - } - rng >>= 1; + noise_toggle ^= ( rng ^ ( rng >> 1 ) ) & 1; + rng = ( rng >> 1 ) ^ ( 0x12000 & -( rng & 1 ) ); /* don't keep trying if period is zero */ if( !ay_noise_period ) From 7f543aeaa1aa795135b174152cac28f3a8201bb3 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Fri, 17 Jul 2026 21:55:06 +1000 Subject: [PATCH 123/142] tests: compare AY noise RNG update implementations --- Makefile.am | 6 ++-- unittests/Makefile.am | 4 +++ unittests/aynoisetest.c | 70 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 unittests/aynoisetest.c diff --git a/Makefile.am b/Makefile.am index 96543304..8c4faae7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -167,10 +167,11 @@ DISTCLEANFILES = pkgdata_DATA = -test: z80/coretest unittests/displaytest +test: z80/coretest unittests/displaytest unittests/aynoisetest z80/coretest $(srcdir)/z80/tests/tests.in > z80/tests.actual cmp z80/tests.actual $(srcdir)/z80/tests/tests.expected ./unittests/displaytest + ./unittests/aynoisetest ## Resources for Windows executables @@ -215,7 +216,7 @@ include ui/xlib/Makefile.am include unittests/Makefile.am include z80/Makefile.am -check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest z80/coretest +check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest unittests/aynoisetest z80/coretest @test -e $(top_builddir)/roms || ln -sf "$(abs_top_srcdir)/roms" $(top_builddir)/roms $(top_builddir)/fuse --unittests $(top_builddir)/unittests/displaytest @@ -223,5 +224,6 @@ check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2 $(top_builddir)/unittests/sdl2scalerstatetest $(top_builddir)/unittests/sdl2joysticktest $(top_builddir)/unittests/sdl2mousetest + $(top_builddir)/unittests/aynoisetest $(top_builddir)/z80/coretest $(srcdir)/z80/tests/tests.in > $(top_builddir)/z80/tests.actual cmp $(top_builddir)/z80/tests.actual $(srcdir)/z80/tests/tests.expected diff --git a/unittests/Makefile.am b/unittests/Makefile.am index 40e7c3c0..04bae276 100644 --- a/unittests/Makefile.am +++ b/unittests/Makefile.am @@ -31,6 +31,7 @@ noinst_PROGRAMS += unittests/sdl2displaytest noinst_PROGRAMS += unittests/sdl2scalerstatetest noinst_PROGRAMS += unittests/sdl2joysticktest noinst_PROGRAMS += unittests/sdl2mousetest +noinst_PROGRAMS += unittests/aynoisetest unittests_displaytest_SOURCES = unittests/displaytest.c display.c unittests_displaytest_LDADD = $(LIBSPECTRUM_LIBS) $(GLIB_LIBS) @@ -59,3 +60,6 @@ unittests_sdl2mousetest_SOURCES = \ ui/sdl2/sdl2_mouse_internal.c unittests_sdl2mousetest_LDADD = $(LIBSPECTRUM_LIBS) unittests_sdl2mousetest_CPPFLAGS = $(AM_CPPFLAGS) + +unittests_aynoisetest_SOURCES = unittests/aynoisetest.c +unittests_aynoisetest_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/unittests/aynoisetest.c b/unittests/aynoisetest.c new file mode 100644 index 00000000..2535e9b8 --- /dev/null +++ b/unittests/aynoisetest.c @@ -0,0 +1,70 @@ +/* aynoisetest.c: Tests for AY noise generator state updates + Copyright (c) 2026 Fredrick Meunier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include + +static void +conditional_noise_step( int *rng, int *noise_toggle ) +{ + if( ( *rng & 1 ) ^ ( ( *rng & 2 ) ? 1 : 0 ) ) + *noise_toggle = !*noise_toggle; + + if( *rng & 1 ) + *rng ^= 0x24000; + *rng >>= 1; +} + +static void +branch_free_noise_step( int *rng, int *noise_toggle ) +{ + *noise_toggle ^= ( *rng ^ ( *rng >> 1 ) ) & 1; + *rng = ( *rng >> 1 ) ^ ( 0x12000 & -( *rng & 1 ) ); +} + +int +main( void ) +{ + int initial_rng, initial_noise_toggle; + int conditional_rng, conditional_toggle; + int branch_free_rng, branch_free_toggle; + + for( initial_rng = 0; initial_rng < 0x20000; initial_rng++ ) { + for( initial_noise_toggle = 0; initial_noise_toggle < 2; + initial_noise_toggle++ ) { + conditional_rng = initial_rng; + conditional_toggle = initial_noise_toggle; + branch_free_rng = initial_rng; + branch_free_toggle = initial_noise_toggle; + + conditional_noise_step( &conditional_rng, &conditional_toggle ); + branch_free_noise_step( &branch_free_rng, &branch_free_toggle ); + + if( conditional_rng != branch_free_rng || + conditional_toggle != branch_free_toggle ) { + fprintf( stderr, + "Mismatch for rng %#x, noise toggle %d: " + "conditional %#x/%d, branch-free %#x/%d\n", + initial_rng, initial_noise_toggle, conditional_rng, + conditional_toggle, branch_free_rng, branch_free_toggle ); + return 1; + } + } + } + + return 0; +} From 01d4ccecb3799c9fabaf272ccb0d34fd1ee61fff Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Fri, 17 Jul 2026 21:58:53 +1000 Subject: [PATCH 124/142] Revert accidental commit "compare AY noise RNG update implementations" This reverts commit 7f543aeaa1aa795135b174152cac28f3a8201bb3. --- Makefile.am | 6 ++-- unittests/Makefile.am | 4 --- unittests/aynoisetest.c | 70 ----------------------------------------- 3 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 unittests/aynoisetest.c diff --git a/Makefile.am b/Makefile.am index 8c4faae7..96543304 100644 --- a/Makefile.am +++ b/Makefile.am @@ -167,11 +167,10 @@ DISTCLEANFILES = pkgdata_DATA = -test: z80/coretest unittests/displaytest unittests/aynoisetest +test: z80/coretest unittests/displaytest z80/coretest $(srcdir)/z80/tests/tests.in > z80/tests.actual cmp z80/tests.actual $(srcdir)/z80/tests/tests.expected ./unittests/displaytest - ./unittests/aynoisetest ## Resources for Windows executables @@ -216,7 +215,7 @@ include ui/xlib/Makefile.am include unittests/Makefile.am include z80/Makefile.am -check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest unittests/aynoisetest z80/coretest +check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest z80/coretest @test -e $(top_builddir)/roms || ln -sf "$(abs_top_srcdir)/roms" $(top_builddir)/roms $(top_builddir)/fuse --unittests $(top_builddir)/unittests/displaytest @@ -224,6 +223,5 @@ check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2 $(top_builddir)/unittests/sdl2scalerstatetest $(top_builddir)/unittests/sdl2joysticktest $(top_builddir)/unittests/sdl2mousetest - $(top_builddir)/unittests/aynoisetest $(top_builddir)/z80/coretest $(srcdir)/z80/tests/tests.in > $(top_builddir)/z80/tests.actual cmp $(top_builddir)/z80/tests.actual $(srcdir)/z80/tests/tests.expected diff --git a/unittests/Makefile.am b/unittests/Makefile.am index 04bae276..40e7c3c0 100644 --- a/unittests/Makefile.am +++ b/unittests/Makefile.am @@ -31,7 +31,6 @@ noinst_PROGRAMS += unittests/sdl2displaytest noinst_PROGRAMS += unittests/sdl2scalerstatetest noinst_PROGRAMS += unittests/sdl2joysticktest noinst_PROGRAMS += unittests/sdl2mousetest -noinst_PROGRAMS += unittests/aynoisetest unittests_displaytest_SOURCES = unittests/displaytest.c display.c unittests_displaytest_LDADD = $(LIBSPECTRUM_LIBS) $(GLIB_LIBS) @@ -60,6 +59,3 @@ unittests_sdl2mousetest_SOURCES = \ ui/sdl2/sdl2_mouse_internal.c unittests_sdl2mousetest_LDADD = $(LIBSPECTRUM_LIBS) unittests_sdl2mousetest_CPPFLAGS = $(AM_CPPFLAGS) - -unittests_aynoisetest_SOURCES = unittests/aynoisetest.c -unittests_aynoisetest_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/unittests/aynoisetest.c b/unittests/aynoisetest.c deleted file mode 100644 index 2535e9b8..00000000 --- a/unittests/aynoisetest.c +++ /dev/null @@ -1,70 +0,0 @@ -/* aynoisetest.c: Tests for AY noise generator state updates - Copyright (c) 2026 Fredrick Meunier - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include - -static void -conditional_noise_step( int *rng, int *noise_toggle ) -{ - if( ( *rng & 1 ) ^ ( ( *rng & 2 ) ? 1 : 0 ) ) - *noise_toggle = !*noise_toggle; - - if( *rng & 1 ) - *rng ^= 0x24000; - *rng >>= 1; -} - -static void -branch_free_noise_step( int *rng, int *noise_toggle ) -{ - *noise_toggle ^= ( *rng ^ ( *rng >> 1 ) ) & 1; - *rng = ( *rng >> 1 ) ^ ( 0x12000 & -( *rng & 1 ) ); -} - -int -main( void ) -{ - int initial_rng, initial_noise_toggle; - int conditional_rng, conditional_toggle; - int branch_free_rng, branch_free_toggle; - - for( initial_rng = 0; initial_rng < 0x20000; initial_rng++ ) { - for( initial_noise_toggle = 0; initial_noise_toggle < 2; - initial_noise_toggle++ ) { - conditional_rng = initial_rng; - conditional_toggle = initial_noise_toggle; - branch_free_rng = initial_rng; - branch_free_toggle = initial_noise_toggle; - - conditional_noise_step( &conditional_rng, &conditional_toggle ); - branch_free_noise_step( &branch_free_rng, &branch_free_toggle ); - - if( conditional_rng != branch_free_rng || - conditional_toggle != branch_free_toggle ) { - fprintf( stderr, - "Mismatch for rng %#x, noise toggle %d: " - "conditional %#x/%d, branch-free %#x/%d\n", - initial_rng, initial_noise_toggle, conditional_rng, - conditional_toggle, branch_free_rng, branch_free_toggle ); - return 1; - } - } - } - - return 0; -} From 8cdd16ddf7e893110dfc5e51e0e8092e833040c7 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sun, 19 Jul 2026 20:09:40 +1000 Subject: [PATCH 125/142] Blend PAL composite scaler chroma across lines --- Makefile.am | 4 +- ui/scaler/scaler.c | 18 +- ui/scaler/scalers.c | 94 ++++++- unittests/Makefile.am | 16 ++ unittests/palcompositetest.c | 504 +++++++++++++++++++++++++++++++++++ unittests/scalerexpandtest.c | 80 ++++++ 6 files changed, 702 insertions(+), 14 deletions(-) create mode 100644 unittests/palcompositetest.c create mode 100644 unittests/scalerexpandtest.c diff --git a/Makefile.am b/Makefile.am index 96543304..d95be661 100644 --- a/Makefile.am +++ b/Makefile.am @@ -215,7 +215,7 @@ include ui/xlib/Makefile.am include unittests/Makefile.am include z80/Makefile.am -check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest z80/coretest +check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2scalerstatetest unittests/sdl2joysticktest unittests/sdl2mousetest unittests/palcompositetest unittests/scalerexpandtest z80/coretest @test -e $(top_builddir)/roms || ln -sf "$(abs_top_srcdir)/roms" $(top_builddir)/roms $(top_builddir)/fuse --unittests $(top_builddir)/unittests/displaytest @@ -223,5 +223,7 @@ check-local: fuse unittests/displaytest unittests/sdl2displaytest unittests/sdl2 $(top_builddir)/unittests/sdl2scalerstatetest $(top_builddir)/unittests/sdl2joysticktest $(top_builddir)/unittests/sdl2mousetest + $(top_builddir)/unittests/palcompositetest + $(top_builddir)/unittests/scalerexpandtest $(top_builddir)/z80/coretest $(srcdir)/z80/tests/tests.in > $(top_builddir)/z80/tests.actual cmp $(top_builddir)/z80/tests.actual $(srcdir)/z80/tests/tests.expected diff --git a/ui/scaler/scaler.c b/ui/scaler/scaler.c index 606d7b0d..3ed0c7d2 100644 --- a/ui/scaler/scaler.c +++ b/ui/scaler/scaler.c @@ -63,8 +63,8 @@ static void expand_1( int *x, int *y, int *w, int *h, int image_width, int image_height ); static void expand_sai( int *x, int *y, int *w, int *h, int image_width, int image_height ); -static void expand_full_width( int *x, int *y, int *w, int *h, - int image_width, int image_height ); +static void expand_full_width_and_1_line( int *x, int *y, int *w, int *h, + int image_width, int image_height ); static void expand_dotmatrix( int *x, int *y, int *w, int *h, int image_width, int image_height ); @@ -109,11 +109,14 @@ static const struct scaler_info available_scalers[] = { { "Timex 2x", "timex2x", SCALER_FLAGS_NONE, 2.0, scaler_Normal2x_16, scaler_Normal2x_32, NULL }, { "PAL TV 2x", "paltv2x", SCALER_FLAGS_EXPAND, 2.0, - scaler_PalTV2x_16, scaler_PalTV2x_32, expand_full_width }, + scaler_PalTV2x_16, scaler_PalTV2x_32, + expand_full_width_and_1_line }, { "PAL TV 3x", "paltv3x", SCALER_FLAGS_EXPAND, 3.0, - scaler_PalTV3x_16, scaler_PalTV3x_32, expand_full_width }, + scaler_PalTV3x_16, scaler_PalTV3x_32, + expand_full_width_and_1_line }, { "PAL TV 4x", "paltv4x", SCALER_FLAGS_EXPAND, 4.0, - scaler_PalTV4x_16, scaler_PalTV4x_32, expand_full_width }, + scaler_PalTV4x_16, scaler_PalTV4x_32, + expand_full_width_and_1_line }, { "HQ 2x", "hq2x", SCALER_FLAGS_EXPAND, 2.0, scaler_HQ2x_16, scaler_HQ2x_32, expand_1 }, { "HQ 3x", "hq3x", SCALER_FLAGS_EXPAND, 3.0, @@ -343,13 +346,14 @@ expand_sai( int *x, int *y, int *w, int *h, int image_width, int image_height ) clip( x, y, w, h, image_width, image_height ); } -/* Expand to the full source-line width */ +/* Expand to the full source-line width and the following line. */ static void -expand_full_width( int *x, int *y, int *w, int *h, +expand_full_width_and_1_line( int *x, int *y, int *w, int *h, int image_width, int image_height ) { *x = 0; *w = image_width; + (*h)++; clip( x, y, w, h, image_width, image_height ); } diff --git a/ui/scaler/scalers.c b/ui/scaler/scalers.c index f4ab04fb..fd3e38d5 100644 --- a/ui/scaler/scalers.c +++ b/ui/scaler/scalers.c @@ -1507,6 +1507,7 @@ typedef struct composite_scaler_config { int scale; int cycle_phase; int burst_phase; + int pal_delay; double dsxd; double brightness[4]; } composite_scaler_config; @@ -1523,6 +1524,12 @@ static double composite_scaler_adjust_brightness( double brightness, double factor ); static void composite_scaler_init( const composite_scaler_config *config, composite_scaler_cache *cache ); +static libspectrum_byte composite_scaler_clamp_colour( + libspectrum_signed_dword colour ); +static void composite_scaler_yuv_to_rgb( libspectrum_signed_dword y, + libspectrum_signed_dword u, + libspectrum_signed_dword v, + uint8_t rgb[4] ); static void composite_scaler_blit( const composite_scaler_config *config, const libspectrum_byte *srcPtr, libspectrum_dword srcPitch, @@ -1538,7 +1545,7 @@ FUNCTION( scaler_PalTV2x )( const libspectrum_byte *srcPtr, int width, int height ) { static const composite_scaler_config config = { - 2, 0, 0, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } + 2, 0, 0, 1, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } }; composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, @@ -1553,7 +1560,7 @@ FUNCTION( scaler_PalTV3x )( const libspectrum_byte *srcPtr, int width, int height ) { static const composite_scaler_config config = { - 3, 0, 0, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } + 3, 0, 0, 1, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } }; composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, @@ -1568,7 +1575,7 @@ FUNCTION( scaler_PalTV4x )( const libspectrum_byte *srcPtr, int width, int height ) { static const composite_scaler_config config = { - 4, 0, 0, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } + 4, 0, 0, 1, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } }; composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, @@ -2019,6 +2026,28 @@ composite_scaler_init( const composite_scaler_config *config, cache->snes_init = 1; } +static libspectrum_byte +composite_scaler_clamp_colour( libspectrum_signed_dword colour ) +{ + if( colour < 0 ) return 0; + if( colour > 255 ) return 255; + + return colour; +} + +static void +composite_scaler_yuv_to_rgb( libspectrum_signed_dword y, + libspectrum_signed_dword u, + libspectrum_signed_dword v, uint8_t rgb[4] ) +{ + rgb[0] = composite_scaler_clamp_colour( + ( 8192L * y + 11485L * v + 16384 ) >> 15 ); + rgb[1] = composite_scaler_clamp_colour( + ( 8192L * y - 2819L * u - 5850L * v + 16384 ) >> 15 ); + rgb[2] = composite_scaler_clamp_colour( + ( 8192L * y + 14516L * u + 16384 ) >> 15 ); +} + static void composite_scaler_blit( const composite_scaler_config *config, const libspectrum_byte *srcPtr, @@ -2031,10 +2060,13 @@ composite_scaler_blit( const composite_scaler_config *config, static composite_scaler_cache cache_3x; static composite_scaler_cache cache_4x; static uint8_t buffer[4 * SNES_NTSC_OUT_WIDTH( DISPLAY_SCREEN_WIDTH ) + 8]; + static libspectrum_signed_dword previous_u[4][ DISPLAY_SCREEN_WIDTH * 4 ]; + static libspectrum_signed_dword previous_v[4][ DISPLAY_SCREEN_WIDTH * 4 ]; composite_scaler_cache *cache = config->scale == 2 ? &cache_2x : config->scale == 3 ? &cache_3x : &cache_4x; int burst_phase; + int have_previous = 0; composite_scaler_init( config, cache ); memset( buffer, 0, sizeof( buffer ) ); @@ -2043,6 +2075,38 @@ composite_scaler_blit( const composite_scaler_config *config, ( cache->burst_phase + 1 ) % snes_ntsc_burst_count : config->burst_phase; + if( config->pal_delay ) { + const SNES_NTSC_IN_T *input = blargg_ntsc_input_row( srcPtr - srcPitch, + width ); + uint32_t n; + + for( n = 0; n < config->scale; n++ ) { + uint32_t x; + double dsx = 0; + + snes_ntsc_blit( &cache->ntsc[n], input, width, burst_phase, width, 1, + buffer, 4 * SNES_NTSC_OUT_WIDTH( width ) ); + + for( x = 0; x < width * config->scale; x++ ) { + uint32_t isx = (int)floor( dsx ); + double fsx = dsx - floor( dsx ); + uint8_t rgb[4]; + + rgb[0] = ( ( 1 - fsx ) * buffer[ isx * 4 + 0 ] ) + + ( fsx * buffer[ isx * 4 + 4 ] ); + rgb[1] = ( ( 1 - fsx ) * buffer[ isx * 4 + 1 ] ) + + ( fsx * buffer[ isx * 4 + 5 ] ); + rgb[2] = ( ( 1 - fsx ) * buffer[ isx * 4 + 2 ] ) + + ( fsx * buffer[ isx * 4 + 6 ] ); + previous_u[n][x] = RGB_TO_U( rgb[0], rgb[1], rgb[2] ); + previous_v[n][x] = RGB_TO_V( rgb[0], rgb[1], rgb[2] ); + dsx += config->dsxd; + } + } + + have_previous = 1; + } + while( height-- ) { const SNES_NTSC_IN_T *input = blargg_ntsc_input_row( srcPtr, width ); int line_phase = config->cycle_phase ? @@ -2063,6 +2127,9 @@ composite_scaler_blit( const composite_scaler_config *config, uint32_t isx; double fsx; uint8_t rgb[4]; + libspectrum_signed_dword y; + libspectrum_signed_dword u; + libspectrum_signed_dword v; isx = (int)floor( dsx ); fsx = dsx - floor( dsx ); @@ -2072,12 +2139,27 @@ composite_scaler_blit( const composite_scaler_config *config, ( fsx * buffer[ isx * 4 + 5 ] ); rgb[2] = ( ( 1 - fsx ) * buffer[ isx * 4 + 2 ] ) + ( fsx * buffer[ isx * 4 + 6 ] ); + + if( config->pal_delay ) { + y = RGB_TO_Y( rgb[0], rgb[1], rgb[2] ); + u = RGB_TO_U( rgb[0], rgb[1], rgb[2] ); + v = RGB_TO_V( rgb[0], rgb[1], rgb[2] ); + + if( have_previous ) { + composite_scaler_yuv_to_rgb( y, ( u + previous_u[n][x] ) / 2, + ( v + previous_v[n][x] ) / 2, rgb ); + } + previous_u[n][x] = u; + previous_v[n][x] = v; + } + out[0] = blargg_ntsc_rgb_to_pixel( rgb ); out++; dsx += config->dsxd; } } + have_previous = 1; srcPtr += srcPitch; dstPtr += dstPitch * config->scale; } @@ -2093,7 +2175,7 @@ FUNCTION( scaler_blargg_NTSC_2x )( const libspectrum_byte *srcPtr, int width, int height ) { static const composite_scaler_config config = { - 2, 1, 0, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } + 2, 1, 0, 0, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } }; composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, @@ -2108,7 +2190,7 @@ FUNCTION( scaler_blargg_NTSC_3x )( const libspectrum_byte *srcPtr, int width, int height ) { static const composite_scaler_config config = { - 3, 1, 0, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } + 3, 1, 0, 0, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } }; composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, @@ -2123,7 +2205,7 @@ FUNCTION( scaler_blargg_NTSC_4x )( const libspectrum_byte *srcPtr, int width, int height ) { static const composite_scaler_config config = { - 4, 1, 0, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } + 4, 1, 0, 0, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } }; composite_scaler_blit( &config, srcPtr, srcPitch, dstPtr, dstPitch, width, diff --git a/unittests/Makefile.am b/unittests/Makefile.am index 40e7c3c0..0acf3448 100644 --- a/unittests/Makefile.am +++ b/unittests/Makefile.am @@ -31,6 +31,8 @@ noinst_PROGRAMS += unittests/sdl2displaytest noinst_PROGRAMS += unittests/sdl2scalerstatetest noinst_PROGRAMS += unittests/sdl2joysticktest noinst_PROGRAMS += unittests/sdl2mousetest +noinst_PROGRAMS += unittests/palcompositetest +noinst_PROGRAMS += unittests/scalerexpandtest unittests_displaytest_SOURCES = unittests/displaytest.c display.c unittests_displaytest_LDADD = $(LIBSPECTRUM_LIBS) $(GLIB_LIBS) @@ -59,3 +61,17 @@ unittests_sdl2mousetest_SOURCES = \ ui/sdl2/sdl2_mouse_internal.c unittests_sdl2mousetest_LDADD = $(LIBSPECTRUM_LIBS) unittests_sdl2mousetest_CPPFLAGS = $(AM_CPPFLAGS) + +unittests_palcompositetest_SOURCES = \ + unittests/palcompositetest.c \ + ui/scaler/snes_ntsc.c +unittests_palcompositetest_LDADD = $(LIBSPECTRUM_LIBS) \ + ui/scaler/scalers16.o ui/scaler/scalers32.o +unittests_palcompositetest_CPPFLAGS = $(AM_CPPFLAGS) + +unittests_scalerexpandtest_SOURCES = \ + unittests/scalerexpandtest.c \ + ui/scaler/scaler.c +unittests_scalerexpandtest_LDADD = $(LIBSPECTRUM_LIBS) \ + ui/scaler/scalers16.o ui/scaler/scalers32.o ui/scaler/snes_ntsc.o +unittests_scalerexpandtest_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/unittests/palcompositetest.c b/unittests/palcompositetest.c new file mode 100644 index 00000000..087e7997 --- /dev/null +++ b/unittests/palcompositetest.c @@ -0,0 +1,504 @@ +/* palcompositetest.c: regression tests for PAL composite scaler output + Copyright (c) 2026 Fredrick Meunier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. +*/ + +#include "config.h" + +#include +#include + +#include "libspectrum.h" + +#include "settings.h" +#include "ui/scaler/scaler_internals.h" +#include "ui/ui.h" + +#define TEST_WIDTH 8 +#define TEST_HEIGHT 2 +#define TEST_MAX_INPUT_HEIGHT 3 +#define TEST_ROW_PIXELS ( TEST_WIDTH + 3 ) +#define TEST_MAX_SCALE 4 +#define TEST_OUTPUT_WIDTH ( TEST_WIDTH * TEST_MAX_SCALE ) +#define TEST_OUTPUT_HEIGHT ( TEST_HEIGHT * TEST_MAX_SCALE ) + +typedef struct test_rgb { + libspectrum_byte red; + libspectrum_byte green; + libspectrum_byte blue; +} test_rgb; + +settings_info settings_current; + +int +ui_error( ui_error_level severity GCC_UNUSED, const char *format GCC_UNUSED, ... ) +{ + return 0; +} + +static const test_rgb source_colours[ TEST_HEIGHT ][ TEST_WIDTH ] = { + { { 0, 0, 0 }, { 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 }, + { 255, 255, 0 }, { 0, 255, 255 }, { 255, 0, 255 }, + { 255, 255, 255 } }, + { { 16, 32, 48 }, { 64, 80, 96 }, { 112, 128, 144 }, + { 160, 176, 192 }, { 208, 224, 240 }, { 240, 224, 208 }, + { 192, 176, 160 }, { 144, 128, 112 } } +}; + +static libspectrum_dword +pack_32( scaler_bitformat_t format, test_rgb colour ) +{ +#ifdef WORDS_BIGENDIAN + if( format == BITFORMAT_X8R8G8B8 ) + return colour.green << 8 | colour.red << 16 | colour.blue; + return colour.red << 24 | colour.green << 16 | colour.blue << 8; +#else + if( format == BITFORMAT_X8R8G8B8 ) + return colour.blue | colour.green << 8 | colour.red << 16; + return colour.red | colour.green << 8 | colour.blue << 16; +#endif +} + +static test_rgb +unpack_32( scaler_bitformat_t format, libspectrum_dword pixel ) +{ + test_rgb colour; + +#ifdef WORDS_BIGENDIAN + if( format == BITFORMAT_X8R8G8B8 ) { + colour.red = pixel >> 16; + colour.green = pixel >> 8; + colour.blue = pixel; + } else { + colour.red = pixel >> 24; + colour.green = pixel >> 16; + colour.blue = pixel >> 8; + } +#else + if( format == BITFORMAT_X8R8G8B8 ) { + colour.red = pixel >> 16; + colour.green = pixel >> 8; + colour.blue = pixel; + } else { + colour.red = pixel; + colour.green = pixel >> 8; + colour.blue = pixel >> 16; + } +#endif + + return colour; +} + +static libspectrum_word +pack_16( scaler_bitformat_t format, test_rgb colour ) +{ + if( format == BITFORMAT_565 ) + return ( colour.red >> 3 ) << 11 | ( colour.green >> 2 ) << 5 | + ( colour.blue >> 3 ); + + return ( colour.red >> 3 ) << 10 | ( colour.green >> 3 ) << 5 | + ( colour.blue >> 3 ); +} + +static void +build_32_source( scaler_bitformat_t format, + libspectrum_dword source[ TEST_HEIGHT ][ TEST_ROW_PIXELS ] ) +{ + int row, column; + + for( row = 0; row < TEST_HEIGHT; row++ ) { + for( column = 0; column < TEST_WIDTH; column++ ) + source[row][column + 1] = pack_32( format, + source_colours[row][column] ); + source[row][0] = source[row][1]; + source[row][TEST_WIDTH + 1] = source[row][TEST_WIDTH]; + source[row][TEST_WIDTH + 2] = source[row][TEST_WIDTH]; + } +} + +static void +build_16_source( scaler_bitformat_t format, + libspectrum_word source[ TEST_HEIGHT ][ TEST_ROW_PIXELS ] ) +{ + int row, column; + + for( row = 0; row < TEST_HEIGHT; row++ ) { + for( column = 0; column < TEST_WIDTH; column++ ) + source[row][column + 1] = pack_16( format, + source_colours[row][column] ); + source[row][0] = source[row][1]; + source[row][TEST_WIDTH + 1] = source[row][TEST_WIDTH]; + source[row][TEST_WIDTH + 2] = source[row][TEST_WIDTH]; + } +} + +static void +run_32( scaler_bitformat_t format, ScalerProc *scaler, int scale, + const libspectrum_dword *source, int height, + libspectrum_dword output[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ] ) +{ + libspectrum_dword padded[ TEST_MAX_INPUT_HEIGHT + 1 ][ TEST_ROW_PIXELS ]; + + memcpy( padded[1], source, height * sizeof( padded[0] ) ); + memcpy( padded[0], padded[1], sizeof( padded[0] ) ); + memset( output, 0, TEST_OUTPUT_HEIGHT * TEST_OUTPUT_WIDTH * + sizeof( output[0][0] ) ); + scaler_select_bitformat_32( format ); + scaler( (const libspectrum_byte*)&padded[1][1], + TEST_ROW_PIXELS * sizeof( padded[0][0] ), (libspectrum_byte*)output, + TEST_OUTPUT_WIDTH * sizeof( output[0][0] ), TEST_WIDTH, height ); + (void)scale; +} + +static void +run_16( scaler_bitformat_t format, ScalerProc *scaler, int scale, + const libspectrum_word *source, int height, + libspectrum_word output[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ] ) +{ + libspectrum_word padded[ TEST_MAX_INPUT_HEIGHT + 1 ][ TEST_ROW_PIXELS ]; + + memcpy( padded[1], source, height * sizeof( padded[0] ) ); + memcpy( padded[0], padded[1], sizeof( padded[0] ) ); + memset( output, 0, TEST_OUTPUT_HEIGHT * TEST_OUTPUT_WIDTH * + sizeof( output[0][0] ) ); + scaler_select_bitformat_16( format ); + scaler( (const libspectrum_byte*)&padded[1][1], + TEST_ROW_PIXELS * sizeof( padded[0][0] ), (libspectrum_byte*)output, + TEST_OUTPUT_WIDTH * sizeof( output[0][0] ), TEST_WIDTH, height ); + (void)scale; +} + +static libspectrum_signed_dword +rgb_to_y( test_rgb colour ) +{ + return ( 2449L * colour.red + 4809L * colour.green + 934L * colour.blue + + 1024 ) >> 11; +} + +static libspectrum_signed_dword +rgb_to_u( test_rgb colour ) +{ + return ( 4096L * colour.blue - 1383L * colour.red - 2713L * colour.green + + 1024 ) >> 11; +} + +static libspectrum_signed_dword +rgb_to_v( test_rgb colour ) +{ + return ( 4096L * colour.red - 3430L * colour.green - 666L * colour.blue + + 1024 ) >> 11; +} + +static libspectrum_byte +clamp_colour( libspectrum_signed_dword colour ) +{ + if( colour < 0 ) return 0; + if( colour > 255 ) return 255; + return colour; +} + +static test_rgb +chroma_average( test_rgb previous, test_rgb current ) +{ + libspectrum_signed_dword y = rgb_to_y( current ); + libspectrum_signed_dword u = ( rgb_to_u( current ) + rgb_to_u( previous ) ) / 2; + libspectrum_signed_dword v = ( rgb_to_v( current ) + rgb_to_v( previous ) ) / 2; + test_rgb result; + + result.red = clamp_colour( ( 8192L * y + 11485L * v + 16384 ) >> 15 ); + result.green = clamp_colour( ( 8192L * y - 2819L * u - 5850L * v + + 16384 ) >> 15 ); + result.blue = clamp_colour( ( 8192L * y + 14516L * u + 16384 ) >> 15 ); + return result; +} + +static int +same_colour( test_rgb a, test_rgb b ) +{ + return a.red == b.red && a.green == b.green && a.blue == b.blue; +} + +static int +check_32_delay_line( scaler_bitformat_t format, ScalerProc *scaler, + int scale, const char *name ) +{ + libspectrum_dword source[ TEST_HEIGHT ][ TEST_ROW_PIXELS ]; + libspectrum_dword top[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_dword bottom[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_dword combined[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + int row, column; + int differs_from_rgb_average = 0; + + build_32_source( format, source ); + run_32( format, scaler, scale, &source[0][0], 1, top ); + run_32( format, scaler, scale, &source[1][0], 1, bottom ); + run_32( format, scaler, scale, &source[0][0], TEST_HEIGHT, combined ); + + for( row = 0; row < scale; row++ ) { + for( column = 0; column < TEST_WIDTH * scale; column++ ) { + test_rgb first = unpack_32( format, combined[row][column] ); + test_rgb expected = unpack_32( format, top[row][column] ); + test_rgb previous = unpack_32( format, top[row][column] ); + test_rgb current = unpack_32( format, bottom[row][column] ); + test_rgb mixed = unpack_32( format, combined[row + scale][column] ); + test_rgb chroma = chroma_average( previous, current ); + test_rgb rgb_average; + + if( !same_colour( first, expected ) ) { + fprintf( stderr, "%s changed first-line output at row %d, pixel %d\n", + name, row, column ); + return 1; + } + + if( !same_colour( mixed, chroma ) ) { + fprintf( stderr, "%s did not apply chroma-only delay at row %d, pixel %d\n", + name, row, column ); + return 1; + } + + rgb_average.red = ( previous.red + current.red ) / 2; + rgb_average.green = ( previous.green + current.green ) / 2; + rgb_average.blue = ( previous.blue + current.blue ) / 2; + if( !same_colour( mixed, rgb_average ) ) differs_from_rgb_average = 1; + } + } + + if( !differs_from_rgb_average ) { + fprintf( stderr, "%s appears to average whole RGB values\n", name ); + return 1; + } + + return 0; +} + +static int +check_colourmixer_delay_line_32( void ) +{ + /* Colour code 22 followed by the alternating green/magenta code 34. */ + static const test_rgb colours[ 3 ] = { + { 255, 0, 0 }, { 0, 255, 0 }, { 255, 0, 255 } + }; + libspectrum_dword source[ 3 ][ TEST_ROW_PIXELS ]; + libspectrum_dword first[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_dword second[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_dword third[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_dword combined[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + int row, column; + + for( row = 0; row < 3; row++ ) { + for( column = 0; column < TEST_WIDTH; column++ ) + source[row][column + 1] = pack_32( BITFORMAT_X8B8G8R8, + colours[row] ); + source[row][0] = source[row][1]; + source[row][TEST_WIDTH + 1] = source[row][TEST_WIDTH]; + source[row][TEST_WIDTH + 2] = source[row][TEST_WIDTH]; + } + + run_32( BITFORMAT_X8B8G8R8, scaler_PalTV2x_32, 2, &source[0][0], 1, + first ); + run_32( BITFORMAT_X8B8G8R8, scaler_PalTV2x_32, 2, &source[1][0], 1, + second ); + run_32( BITFORMAT_X8B8G8R8, scaler_PalTV2x_32, 2, &source[2][0], 1, + third ); + run_32( BITFORMAT_X8B8G8R8, scaler_PalTV2x_32, 2, &source[0][0], 3, + combined ); + + for( row = 0; row < 2; row++ ) { + for( column = 0; column < TEST_WIDTH * 2; column++ ) { + test_rgb first_colour = unpack_32( BITFORMAT_X8B8G8R8, + first[row][column] ); + test_rgb second_colour = unpack_32( BITFORMAT_X8B8G8R8, + second[row][column] ); + test_rgb third_colour = unpack_32( BITFORMAT_X8B8G8R8, + third[row][column] ); + test_rgb second_actual = unpack_32( BITFORMAT_X8B8G8R8, + combined[row + 2][column] ); + test_rgb third_actual = unpack_32( BITFORMAT_X8B8G8R8, + combined[row + 4][column] ); + test_rgb second_expected = chroma_average( first_colour, second_colour ); + test_rgb third_expected = chroma_average( second_colour, third_colour ); + test_rgb recursive = chroma_average( second_expected, third_colour ); + + if( !same_colour( second_actual, second_expected ) ) { + fprintf( stderr, "ColourMixer transition did not blend line 2\n" ); + return 1; + } + + if( !same_colour( third_actual, third_expected ) ) { + fprintf( stderr, + "ColourMixer transition did not use raw line 2 chroma\n" ); + return 1; + } + + if( same_colour( third_expected, recursive ) ) { + fprintf( stderr, "ColourMixer fixture cannot detect recursive blending\n" ); + return 1; + } + + if( same_colour( third_expected, third_colour ) ) { + fprintf( stderr, "ColourMixer fixture cannot detect paired blending\n" ); + return 1; + } + } + } + + return 0; +} + +static int +check_16_delay_line( scaler_bitformat_t format, ScalerProc *scaler, + int scale, const char *name ) +{ + libspectrum_word source[ TEST_HEIGHT ][ TEST_ROW_PIXELS ]; + libspectrum_word top[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_word bottom[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + libspectrum_word combined[ TEST_OUTPUT_HEIGHT ][ TEST_OUTPUT_WIDTH ]; + int row, column; + int changed = 0; + + build_16_source( format, source ); + run_16( format, scaler, scale, &source[0][0], 1, top ); + run_16( format, scaler, scale, &source[1][0], 1, bottom ); + run_16( format, scaler, scale, &source[0][0], TEST_HEIGHT, combined ); + + for( row = 0; row < scale; row++ ) { + for( column = 0; column < TEST_WIDTH * scale; column++ ) { + if( combined[row][column] != top[row][column] ) { + fprintf( stderr, "%s changed 16-bit first-line output at row %d, pixel %d\n", + name, row, column ); + return 1; + } + if( combined[row + scale][column] != bottom[row][column] ) changed = 1; + } + } + + if( !changed ) { + fprintf( stderr, "%s did not blend 16-bit second-line chroma\n", name ); + return 1; + } + + return 0; +} + +static int +check_partial_redraw_32( void ) +{ + libspectrum_dword old_source[ 4 ][ TEST_ROW_PIXELS ]; + libspectrum_dword new_source[ 4 ][ TEST_ROW_PIXELS ]; + libspectrum_dword full[ 3 * 2 ][ TEST_WIDTH * 2 ]; + libspectrum_dword partial[ 3 * 2 ][ TEST_WIDTH * 2 ]; + int column; + + build_32_source( BITFORMAT_X8B8G8R8, &old_source[1] ); + memcpy( old_source[2], old_source[1], sizeof( old_source[0] ) ); + memcpy( old_source[3], old_source[1], sizeof( old_source[0] ) ); + memcpy( old_source[0], old_source[1], sizeof( old_source[0] ) ); + memcpy( new_source, old_source, sizeof( new_source ) ); + + for( column = 0; column < TEST_WIDTH; column++ ) + new_source[2][column + 1] = pack_32( BITFORMAT_X8B8G8R8, + source_colours[1][column] ); + new_source[2][0] = new_source[2][1]; + new_source[2][TEST_WIDTH + 1] = new_source[2][TEST_WIDTH]; + new_source[2][TEST_WIDTH + 2] = new_source[2][TEST_WIDTH]; + + scaler_select_bitformat_32( BITFORMAT_X8B8G8R8 ); + scaler_PalTV2x_32( (const libspectrum_byte*)&old_source[1][1], + TEST_ROW_PIXELS * sizeof( old_source[0][0] ), + (libspectrum_byte*)partial, sizeof( partial[0] ), + TEST_WIDTH, 3 ); + scaler_PalTV2x_32( (const libspectrum_byte*)&new_source[1][1], + TEST_ROW_PIXELS * sizeof( new_source[0][0] ), + (libspectrum_byte*)full, sizeof( full[0] ), + TEST_WIDTH, 3 ); + + /* Redraw the changed row and its successor without overwriting its + predecessor. The scaler reads the preceding source row for chroma. */ + scaler_PalTV2x_32( (const libspectrum_byte*)&new_source[2][1], + TEST_ROW_PIXELS * sizeof( new_source[0][0] ), + (libspectrum_byte*)&partial[2][0], sizeof( partial[0] ), + TEST_WIDTH, 2 ); + + if( memcmp( &partial[2][0], &full[2][0], + 4 * TEST_WIDTH * sizeof( full[0][0] ) ) ) { + fprintf( stderr, "PAL TV partial redraw differs from full redraw\n" ); + return 1; + } + + return 0; +} + +static libspectrum_dword +output_checksum( const libspectrum_dword *output, int width, int height ) +{ + libspectrum_dword checksum = 2166136261UL; + int row, column; + + for( row = 0; row < height; row++ ) { + for( column = 0; column < width; column++ ) { + test_rgb colour = unpack_32( BITFORMAT_X8B8G8R8, + output[row * width + column] ); + + checksum = ( checksum ^ colour.red ) * 16777619UL; + checksum = ( checksum ^ colour.green ) * 16777619UL; + checksum = ( checksum ^ colour.blue ) * 16777619UL; + } + } + + return checksum; +} + +static int +check_ntsc_regression( void ) +{ + libspectrum_dword source[ TEST_HEIGHT ][ TEST_ROW_PIXELS ]; + libspectrum_dword output[ TEST_HEIGHT * 2 ][ TEST_WIDTH * 2 ]; + + build_32_source( BITFORMAT_X8B8G8R8, source ); + memset( output, 0, sizeof( output ) ); + scaler_select_bitformat_32( BITFORMAT_X8B8G8R8 ); + scaler_blargg_NTSC_2x_32( (const libspectrum_byte*)&source[0][1], + TEST_ROW_PIXELS * sizeof( source[0][0] ), + (libspectrum_byte*)output, + sizeof( output[0] ), TEST_WIDTH, TEST_HEIGHT ); + + if( output_checksum( &output[0][0], TEST_WIDTH * 2, TEST_HEIGHT * 2 ) != + 0x2b785bb3UL ) { + fprintf( stderr, "NTSC composite checksum mismatch: got %08x\n", + output_checksum( &output[0][0], TEST_WIDTH * 2, + TEST_HEIGHT * 2 ) ); + return 1; + } + + return 0; +} + +static int +check_scaler( ScalerProc *scaler_32, ScalerProc *scaler_16, int scale, + const char *name ) +{ + if( check_32_delay_line( BITFORMAT_X8B8G8R8, scaler_32, scale, name ) ) return 1; + if( check_32_delay_line( BITFORMAT_X8R8G8B8, scaler_32, scale, name ) ) return 1; + if( check_16_delay_line( BITFORMAT_565, scaler_16, scale, name ) ) return 1; + return check_16_delay_line( BITFORMAT_555, scaler_16, scale, name ); +} + +int +main( void ) +{ + memset( &settings_current, 0, sizeof( settings_current ) ); + + if( check_scaler( scaler_PalTV2x_32, scaler_PalTV2x_16, 2, + "PAL TV 2x" ) ) return 1; + if( check_scaler( scaler_PalTV3x_32, scaler_PalTV3x_16, 3, + "PAL TV 3x" ) ) return 1; + if( check_scaler( scaler_PalTV4x_32, scaler_PalTV4x_16, 4, + "PAL TV 4x" ) ) return 1; + if( check_colourmixer_delay_line_32() ) return 1; + if( check_partial_redraw_32() ) return 1; + return check_ntsc_regression(); +} diff --git a/unittests/scalerexpandtest.c b/unittests/scalerexpandtest.c new file mode 100644 index 00000000..9d3966cc --- /dev/null +++ b/unittests/scalerexpandtest.c @@ -0,0 +1,80 @@ +/* scalerexpandtest.c: regression tests for scaler dirty-region expansion + Copyright (c) 2026 Fredrick Meunier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. +*/ + +#include "config.h" + +#include + +#include "settings.h" +#include "ui/scaler/scaler.h" +#include "ui/ui.h" +#include "utils.h" + +settings_info settings_current; + +int +ui_error( ui_error_level severity GCC_UNUSED, const char *format GCC_UNUSED, ... ) +{ + return 0; +} + +int +uidisplay_hotswap_gfx_mode( void ) +{ + return 0; +} + +char* +utils_safe_strdup( const char *src GCC_UNUSED ) +{ + return NULL; +} + +static int +scalerexpandtest_expect( int x, int y, int w, int h, int expected_x, + int expected_y, int expected_w, int expected_h ) +{ + if( x == expected_x && y == expected_y && w == expected_w && + h == expected_h ) + return 0; + + fprintf( stderr, "got (%d, %d, %d, %d), expected (%d, %d, %d, %d)\n", + x, y, w, h, expected_x, expected_y, expected_w, expected_h ); + return 1; +} + +static int +scalerexpandtest_paltv( void ) +{ + scaler_expand_fn *expander = scaler_get_expander( SCALER_PALTV2X ); + int x = 12, y = 8, w = 3, h = 1; + + if( !expander ) { + fprintf( stderr, "PAL TV scaler has no dirty-region expander\n" ); + return 1; + } + + if( scaler_get_flags( SCALER_PALTV2X ) & SCALER_FLAGS_FULL_REFRESH ) { + fprintf( stderr, "PAL TV scaler unexpectedly requires full refresh\n" ); + return 1; + } + + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 0, 8, 320, 2 ) ) return 1; + + x = 12; y = 239; w = 3; h = 1; + expander( &x, &y, &w, &h, 320, 240 ); + return scalerexpandtest_expect( x, y, w, h, 0, 239, 320, 1 ); +} + +int +main( void ) +{ + return scalerexpandtest_paltv(); +} From bc66e4d9976c222c1503c9af5dd758263cf99c2f Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 20 Jul 2026 20:36:06 +1000 Subject: [PATCH 126/142] Fix use-after-free in SDL audio device setup 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(). --- sound/sdlsound.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sound/sdlsound.c b/sound/sdlsound.c index fb553d5c..348e3ded 100644 --- a/sound/sdlsound.c +++ b/sound/sdlsound.c @@ -63,13 +63,18 @@ sound_lowlevel_init( const char *device, int *freqptr, int *stereoptr ) strcpy( command, environment ); strcat( command, device ); error = putenv( command ); - libspectrum_free( command ); - if( error ) { + if( error ) { + /* putenv() failed: the string was not inserted into the environment, + so it is safe to free here. */ + libspectrum_free( command ); settings_current.sound = 0; ui_error( UI_ERROR_ERROR, "Couldn't set SDL_AUDIODRIVER: %s", strerror ( error ) ); return 1; } + /* On POSIX, putenv() stores the pointer directly in the environment + rather than copying it, so command must remain allocated for the + lifetime of the process. Do not free it here. */ } #endif /* #ifndef __MORPHOS__ */ From 35b6f8a79e747182c11b1d2b8b4f012dee4cc8c8 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 20 Jul 2026 20:41:58 +1000 Subject: [PATCH 127/142] Simplify display_all_dirty initialisation to a bitmask expression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- display.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/display.c b/display.c index 1d408ebf..17525434 100644 --- a/display.c +++ b/display.c @@ -156,10 +156,8 @@ display_init( int *argc, char ***argv ) if(ui_init(argc, argv)) return 1; - /* Set up the 'all pixels must be refreshed' marker */ - display_all_dirty = 0; - for( i = 0; i < DISPLAY_SCREEN_WIDTH_COLS; i++ ) - display_all_dirty = ( display_all_dirty << 1 ) | 0x01; + /* Set up the 'all pixels must be refreshed' marker: one bit per column */ + display_all_dirty = ( (libspectrum_qword)1 << DISPLAY_SCREEN_WIDTH_COLS ) - 1; for(i=0;i<3;i++) for(j=0;j<8;j++) From 0c36516018e74814067ddb2fca74fc1c698e3007 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 20 Jul 2026 21:05:30 +1000 Subject: [PATCH 128/142] Add DISPLAY_PIXEL_BYTES, DISPLAY_ATTR_BYTES, and DISPLAY_FILE_SIZE constants 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. --- display.c | 42 ++++++++++++++++++++++++------------------ display.h | 8 ++++++++ screenshot.c | 2 +- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/display.c b/display.c index 17525434..2f5f0ec3 100644 --- a/display.c +++ b/display.c @@ -166,7 +166,7 @@ display_init( int *argc, char ***argv ) 32 * ( (64*i) + j + (k*8) ); for(y=0;y= 0x1b00 ) break; - if( offset < 0x1800 ) { /* 0x1800 = first attributes byte */ + if( offset >= DISPLAY_FILE_SIZE ) break; + if( offset < DISPLAY_PIXEL_BYTES ) { display_dirty8( offset ); } else { display_dirty64( offset ); @@ -230,8 +230,9 @@ display_dirty_timex( libspectrum_word offset ) case ALTDFILE: /* second screen */ case HIRESATTRALTD: /* strange mode using second screen */ - if( offset < 0x2000 || offset >= 0x3b00 ) break; - if( offset < 0x3800 ) { /* 0x3800 = first attributes byte */ + if( offset < ALTDFILE_OFFSET || + offset >= ALTDFILE_OFFSET + DISPLAY_FILE_SIZE ) break; + if( offset < ALTDFILE_OFFSET + DISPLAY_PIXEL_BYTES ) { display_dirty8( offset - ALTDFILE_OFFSET ); } else { display_dirty64( offset - ALTDFILE_OFFSET ); @@ -240,9 +241,9 @@ display_dirty_timex( libspectrum_word offset ) case EXTCOLOUR: /* extended colours */ case HIRES: /* hires mode */ - if( offset >= 0x3800 ) break; - if( offset >= 0x1800 && offset < 0x2000 ) break; - if( offset >= 0x2000 ) offset -= ALTDFILE_OFFSET; + if( offset >= ALTDFILE_OFFSET + DISPLAY_PIXEL_BYTES ) break; + if( offset >= DISPLAY_PIXEL_BYTES && offset < ALTDFILE_OFFSET ) break; + if( offset >= ALTDFILE_OFFSET ) offset -= ALTDFILE_OFFSET; display_dirty8( offset ); break; @@ -251,7 +252,8 @@ display_dirty_timex( libspectrum_word offset ) taken from second screen */ /* case HIRESDOUBLECOL: hires mode, but data taken only from second screen */ - if( offset >= 0x2000 && offset < 0x3800 ) + if( offset >= ALTDFILE_OFFSET && + offset < ALTDFILE_OFFSET + DISPLAY_PIXEL_BYTES ) display_dirty8( offset - ALTDFILE_OFFSET ); break; } @@ -260,11 +262,11 @@ display_dirty_timex( libspectrum_word offset ) void display_dirty_pentagon_16_col( libspectrum_word offset ) { - /* The only relevant sections of the page will be the two 6144 byte sections - separated by ALTDFILE_OFFSET, which have the same display offset */ - if( offset >= 0x2000 ) offset -= ALTDFILE_OFFSET; + /* The only relevant sections of the page will be the two DISPLAY_PIXEL_BYTES + sections separated by ALTDFILE_OFFSET, which have the same display offset */ + if( offset >= ALTDFILE_OFFSET ) offset -= ALTDFILE_OFFSET; /* No attributes are relevent in this mode */ - if( offset < 0x1800 ) { /* 0x1800 = first attributes byte */ + if( offset < DISPLAY_PIXEL_BYTES ) { display_dirty8( offset ); } } @@ -272,8 +274,8 @@ display_dirty_pentagon_16_col( libspectrum_word offset ) void display_dirty_sinclair( libspectrum_word offset ) { - if( offset >= 0x1b00 ) return; - if( offset < 0x1800 ) { /* 0x1800 = first attributes byte */ + if( offset >= DISPLAY_FILE_SIZE ) return; + if( offset < DISPLAY_PIXEL_BYTES ) { display_dirty8( offset ); } else { display_dirty64( offset ); @@ -990,14 +992,18 @@ display_dirty_flashing_timex(void) if( !scld_last_dec.name.hires ) { if( scld_last_dec.name.b1 ) { - for( offset = ALTDFILE_OFFSET; offset < 0x3800; offset++ ) { + for( offset = ALTDFILE_OFFSET; + offset < ALTDFILE_OFFSET + DISPLAY_PIXEL_BYTES; + offset++ ) { attr = screen[ offset ]; if( attr & 0x80 ) display_dirty8( offset - ALTDFILE_OFFSET ); } } else if( scld_last_dec.name.altdfile ) { - for( offset= 0x3800; offset < 0x3b00; offset++ ) { + for( offset= ALTDFILE_OFFSET + DISPLAY_PIXEL_BYTES; + offset < ALTDFILE_OFFSET + DISPLAY_FILE_SIZE; + offset++ ) { attr = screen[ offset ]; if( attr & 0x80 ) display_dirty64( offset - ALTDFILE_OFFSET ); } @@ -1025,7 +1031,7 @@ display_dirty_flashing_sinclair(void) screen = RAM[ memory_current_screen ]; /* Standard Speccy screen */ - for( offset = 0x1800; offset < 0x1b00; offset++ ) { + for( offset = DISPLAY_PIXEL_BYTES; offset < DISPLAY_FILE_SIZE; offset++ ) { attr = screen[ offset ]; if( attr & 0x80 ) display_dirty64( offset ); } diff --git a/display.h b/display.h index 4ef6b3ef..8716e08b 100644 --- a/display.h +++ b/display.h @@ -66,6 +66,14 @@ #define DISPLAY_FLASH_HALF_PERIOD 16 #define DISPLAY_FLASH_PERIOD 32 +/* Byte sizes of the ZX Spectrum display file areas. + The pixel data area holds 192 rows × 32 bytes = 6144 bytes (0x1800). + The attribute area holds 24 rows × 32 bytes = 768 bytes (0x300). + The combined display file is 6912 bytes (0x1b00). */ +#define DISPLAY_PIXEL_BYTES ( DISPLAY_HEIGHT * DISPLAY_WIDTH_COLS ) +#define DISPLAY_ATTR_BYTES ( DISPLAY_HEIGHT_ROWS * DISPLAY_WIDTH_COLS ) +#define DISPLAY_FILE_SIZE ( DISPLAY_PIXEL_BYTES + DISPLAY_ATTR_BYTES ) + extern int display_ui_initialised; extern libspectrum_byte display_lores_border; diff --git a/screenshot.c b/screenshot.c index ed24ff33..965bf726 100644 --- a/screenshot.c +++ b/screenshot.c @@ -39,7 +39,7 @@ #include "ui/ui.h" #include "utils.h" -#define MONO_BITMAP_SIZE 6144 +#define MONO_BITMAP_SIZE DISPLAY_PIXEL_BYTES #define HICOLOUR_SCR_SIZE (2 * MONO_BITMAP_SIZE) #define MLT_SIZE (2 * MONO_BITMAP_SIZE) #define HIRES_ATTR HICOLOUR_SCR_SIZE From fa36b03ace1c8a779601f1684fbd80cc864d801d Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 20 Jul 2026 21:10:34 +1000 Subject: [PATCH 129/142] Replace |& with 2>&1 | and drop brew install bash on macOS 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. --- .github/workflows/build_linux_sub.yml | 2 +- .github/workflows/build_macos_sub.yml | 8 +------- .github/workflows/build_windows_sub.yml | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_linux_sub.yml b/.github/workflows/build_linux_sub.yml index fb6cdf75..2fa4d5b4 100644 --- a/.github/workflows/build_linux_sub.yml +++ b/.github/workflows/build_linux_sub.yml @@ -73,7 +73,7 @@ jobs: - name: (6) Configure for Linux run: | ./configure ${{ inputs.configure_params }} \ - |& tee ./configure.out + 2>&1 | tee ./configure.out - name: (7) Verify output from configure # Most of the tested strings are common for all Linux builds diff --git a/.github/workflows/build_macos_sub.yml b/.github/workflows/build_macos_sub.yml index e2b7f42a..9fff419f 100644 --- a/.github/workflows/build_macos_sub.yml +++ b/.github/workflows/build_macos_sub.yml @@ -55,12 +55,6 @@ jobs: echo "Inspect if brew installed .." brew doctor || true - echo "Upgrade bash (fix |& ) .." - brew install bash - which -a bash - bash --version - sudo sh -c 'echo /usr/local/bin/bash >> /etc/shells' - echo "Installing dependencies .." brew install \ automake \ @@ -93,7 +87,7 @@ jobs: - name: (7) Configure for MacOS run: | ./configure ${{ inputs.configure_params }} \ - |& tee ./configure.out + 2>&1 | tee ./configure.out - name: (8) Verify output from configure # Most of the tested strings are common for all MacOS builds diff --git a/.github/workflows/build_windows_sub.yml b/.github/workflows/build_windows_sub.yml index ec6e3b3f..d787f9aa 100644 --- a/.github/workflows/build_windows_sub.yml +++ b/.github/workflows/build_windows_sub.yml @@ -233,7 +233,7 @@ jobs: --prefix=/usr/local/i686-w64-mingw32 \ --disable-static \ ${{ inputs.configure_params }} \ - |& tee ./configure.out + 2>&1 | tee ./configure.out shell: bash.exe --login --norc -eo pipefail -o igncr '{0}' - name: (6b) Check config.log (on configure failure) From 17b86edac06f7fd605555d9ceeb3baab72dce298 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 23 Jul 2026 21:24:16 +1000 Subject: [PATCH 130/142] Fix strerror( -errno ) to strerror( errno ) in relative_paths 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. --- compat/linux/relative_paths.c | 2 +- compat/unix/relative_paths.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compat/linux/relative_paths.c b/compat/linux/relative_paths.c index d1f98363..9b18db36 100644 --- a/compat/linux/relative_paths.c +++ b/compat/linux/relative_paths.c @@ -36,7 +36,7 @@ get_relative_directory( char *buffer, size_t bufsize ) ssize_t retval = readlink( "/proc/self/exe", buffer, bufsize - 1 ); if( retval < 0 ) { ui_error( UI_ERROR_ERROR, "error getting current working directory: %s", - strerror( -errno ) ); + strerror( errno ) ); fuse_abort(); } buffer[ retval ] = '\0'; diff --git a/compat/unix/relative_paths.c b/compat/unix/relative_paths.c index b63c55d8..2c97e1ec 100644 --- a/compat/unix/relative_paths.c +++ b/compat/unix/relative_paths.c @@ -36,7 +36,7 @@ get_relative_directory( char *buffer, size_t bufsize ) size_t len = bufsize - strlen( fuse_progname ) - strlen( FUSE_DIR_SEP_STR ); if( !getcwd( buffer, len ) ) { ui_error( UI_ERROR_ERROR, "error getting current working directory: %s", - strerror( -errno ) ); + strerror( errno ) ); fuse_abort(); } strcat( buffer, FUSE_DIR_SEP_STR ); From 42a44397d5802b1d1b210ca4733010eecc8c53f9 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 23 Jul 2026 21:31:16 +1000 Subject: [PATCH 131/142] Simplify ay_do_tone() by removing redundant else branch *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. --- sound.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sound.c b/sound.c index e0782d86..eaba36a5 100644 --- a/sound.c +++ b/sound.c @@ -420,13 +420,8 @@ ay_do_tone( int level, unsigned int tone_count, int *var, int chan ) ay_tone_high[ chan ] = !ay_tone_high[ chan ]; } - if( level ) { - if( ay_tone_high[ chan ] ) - *var = level; - else { - *var = 0; - } - } + if( level && ay_tone_high[ chan ] ) + *var = level; } /* bitmasks for envelope */ From c80090613fa5adb654a02d153ec56614cb326698 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 23 Jul 2026 21:37:19 +1000 Subject: [PATCH 132/142] Replace magic screen size literals with named constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- memory_pages.c | 4 ++-- screenshot.h | 4 +++- slt.c | 9 +++++---- unittests/displaytest.c | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/memory_pages.c b/memory_pages.c index 0dda1d73..e5e5b34a 100644 --- a/memory_pages.c +++ b/memory_pages.c @@ -459,7 +459,7 @@ memory_display_dirty_pentagon_16_col( libspectrum_word address, ( mapping->page_num == 5 || mapping->page_num == 4 ) ) || ( memory_current_screen == 7 && ( mapping->page_num == 7 || mapping->page_num == 6 ) ) ) && - ( offset2 & 0xdfff ) < 0x1b00 && + ( offset2 & 0xdfff ) < DISPLAY_FILE_SIZE && memory[ offset ] != b ) display_dirty_pentagon_16_col( offset2 ); } @@ -479,7 +479,7 @@ memory_display_dirty_sinclair( libspectrum_word address, libspectrum_byte b ) \ the destination), redraw that bit */ if( mapping->source == memory_source_ram && mapping->page_num == memory_current_screen && - ( offset2 & memory_screen_mask ) < 0x1b00 && + ( offset2 & memory_screen_mask ) < DISPLAY_FILE_SIZE && memory[ offset ] != b ) display_dirty( offset2 ); } diff --git a/screenshot.h b/screenshot.h index c8ed3198..ee370048 100644 --- a/screenshot.h +++ b/screenshot.h @@ -24,6 +24,8 @@ #ifndef FUSE_SCREENSHOT_H #define FUSE_SCREENSHOT_H +#include "display.h" + #ifndef SCALER_H #include "ui/scaler/scaler.h" #endif /* #ifndef SCALER_H */ @@ -43,6 +45,6 @@ int screenshot_scr_read( const char *filename ); int screenshot_mlt_write( const char *filename ); int screenshot_mlt_read( const char *filename ); -#define STANDARD_SCR_SIZE 6912 +#define STANDARD_SCR_SIZE DISPLAY_FILE_SIZE #endif /* #ifndef FUSE_SCREENSHOT_H */ diff --git a/slt.c b/slt.c index 418a2a08..ee34bf85 100644 --- a/slt.c +++ b/slt.c @@ -30,6 +30,7 @@ #include "libspectrum.h" #include "infrastructure/startup_manager.h" +#include "display.h" #include "module.h" #include "settings.h" #include "slt.h" @@ -115,9 +116,9 @@ slt_from_snapshot( libspectrum_snap *snap ) if( libspectrum_snap_slt_screen( snap ) ) { - slt_screen = memory_pool_allocate( 6912 * sizeof( libspectrum_byte ) ); + slt_screen = memory_pool_allocate( DISPLAY_FILE_SIZE * sizeof( libspectrum_byte ) ); - memcpy( slt_screen, libspectrum_snap_slt_screen( snap ), 6912 ); + memcpy( slt_screen, libspectrum_snap_slt_screen( snap ), DISPLAY_FILE_SIZE ); slt_screen_level = libspectrum_snap_slt_screen_level( snap ); } } @@ -143,9 +144,9 @@ slt_to_snapshot( libspectrum_snap *snap ) if( slt_screen ) { - buffer = libspectrum_new( libspectrum_byte, 6912 ); + buffer = libspectrum_new( libspectrum_byte, DISPLAY_FILE_SIZE ); - memcpy( buffer, slt_screen, 6912 ); + memcpy( buffer, slt_screen, DISPLAY_FILE_SIZE ); libspectrum_snap_set_slt_screen( snap, buffer ); libspectrum_snap_set_slt_screen_level( snap, slt_screen_level ); } diff --git a/unittests/displaytest.c b/unittests/displaytest.c index 47555bb1..4578ce4b 100644 --- a/unittests/displaytest.c +++ b/unittests/displaytest.c @@ -322,7 +322,7 @@ no_write_if_data_unchanged( void ) { /* Arrange */ RAM[0][0] = 0; - RAM[0][6144] = 0; + RAM[0][DISPLAY_PIXEL_BYTES] = 0; /* Act */ display_write_if_dirty_sinclair( 0, 0 ); @@ -338,7 +338,7 @@ write_called_for_new_data( void ) { /* Arrange */ RAM[0][0] = 0x01; - RAM[0][6144] = 0x02; + RAM[0][DISPLAY_PIXEL_BYTES] = 0x02; /* Act */ display_write_if_dirty_sinclair( 0, 0 ); @@ -348,7 +348,7 @@ write_called_for_new_data( void ) if( display_last_screen[ 964 ] != 0x201 ) { fprintf( stderr, "display_last_screen[964]: expected 0x201, got 0x%x (attr=0x%02x, scld=0x%02x)\n", - display_last_screen[ 964 ], RAM[0][6144], scld_last_dec.byte ); + display_last_screen[ 964 ], RAM[0][DISPLAY_PIXEL_BYTES], scld_last_dec.byte ); return 1; } if( display_get_is_dirty( 24 ) != ( (libspectrum_qword)1 << 4 ) ) { @@ -366,7 +366,7 @@ write_reads_from_appropriate_x( void ) { /* Arrange */ RAM[0][31] = 0x12; - RAM[0][6144 + 31] = 0x34; + RAM[0][DISPLAY_PIXEL_BYTES + 31] = 0x34; /* Act */ display_write_if_dirty_sinclair( 31, 0 ); @@ -384,7 +384,7 @@ write_reads_from_appropriate_y( void ) { /* Arrange */ RAM[0][32] = 0x56; - RAM[0][6144 + 32] = 0x78; + RAM[0][DISPLAY_PIXEL_BYTES + 32] = 0x78; /* Act */ display_write_if_dirty_sinclair( 0, 8 ); @@ -402,7 +402,7 @@ flash_inverts_colours( void ) { /* Arrange */ RAM[0][0] = 0x01; - RAM[0][6144] = 0x82; + RAM[0][DISPLAY_PIXEL_BYTES] = 0x82; display_set_flash_reversed( 1 ); @@ -713,7 +713,7 @@ timex_lores_write_called_for_new_data( void ) /* Arrange: STANDARD mode, non-zero pixel and attribute data */ timex_test_before( STANDARD ); RAM[0][0] = 0x01; - RAM[0][6144] = 0x02; /* ink=2, paper=0 */ + RAM[0][DISPLAY_PIXEL_BYTES] = 0x02; /* ink=2, paper=0 */ /* Act */ display_write_if_dirty_timex( 0, 0 ); @@ -736,7 +736,7 @@ timex_mode_change_causes_redraw( void ) /* Arrange: draw once in STANDARD mode to prime the cache */ timex_test_before( STANDARD ); RAM[0][0] = 0x01; - RAM[0][6144] = 0x02; + RAM[0][DISPLAY_PIXEL_BYTES] = 0x02; display_write_if_dirty_timex( 0, 0 ); plot8_count = 0; From 6c7c5aa72e550d08cd8001c0c02514f9f04bb474 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 23 Jul 2026 21:40:19 +1000 Subject: [PATCH 133/142] Extend scalerexpandtest to cover all four expander types 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. --- unittests/scalerexpandtest.c | 142 ++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/unittests/scalerexpandtest.c b/unittests/scalerexpandtest.c index 9d3966cc..46240dc9 100644 --- a/unittests/scalerexpandtest.c +++ b/unittests/scalerexpandtest.c @@ -68,13 +68,153 @@ scalerexpandtest_paltv( void ) expander( &x, &y, &w, &h, 320, 240 ); if( scalerexpandtest_expect( x, y, w, h, 0, 8, 320, 2 ) ) return 1; + /* Last row: h clipped so it does not exceed the image height */ x = 12; y = 239; w = 3; h = 1; expander( &x, &y, &w, &h, 320, 240 ); return scalerexpandtest_expect( x, y, w, h, 0, 239, 320, 1 ); } +/* expand_1: used by AdvMAME 2x/3x and HQ scalers. + Expands the dirty rectangle by one pixel in every direction, then clips. */ +static int +scalerexpandtest_advmame( void ) +{ + scaler_expand_fn *expander = scaler_get_expander( SCALER_ADVMAME2X ); + int x, y, w, h; + + if( !expander ) { + fprintf( stderr, "AdvMAME 2x scaler has no dirty-region expander\n" ); + return 1; + } + + if( scaler_get_flags( SCALER_ADVMAME2X ) & SCALER_FLAGS_FULL_REFRESH ) { + fprintf( stderr, "AdvMAME 2x scaler unexpectedly requires full refresh\n" ); + return 1; + } + + /* Interior point: expands by 1 in every direction */ + x = 10; y = 10; w = 5; h = 3; + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 9, 9, 7, 5 ) ) return 1; + + /* Top-left corner: negative coordinates are clipped to 0 */ + x = 0; y = 0; w = 4; h = 2; + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 0, 0, 5, 3 ) ) return 1; + + /* Bottom-right corner: width/height clipped to image bounds */ + x = 316; y = 237; w = 4; h = 3; + expander( &x, &y, &w, &h, 320, 240 ); + return scalerexpandtest_expect( x, y, w, h, 315, 236, 5, 4 ); +} + +/* expand_sai: used by 2xSaI, Super 2xSaI, SuperEagle, and HQ scalers. + Expands 2 pixels up and left, 1 pixel down and right, then clips. */ +static int +scalerexpandtest_sai( void ) +{ + scaler_expand_fn *expander = scaler_get_expander( SCALER_2XSAI ); + int x, y, w, h; + + if( !expander ) { + fprintf( stderr, "2xSaI scaler has no dirty-region expander\n" ); + return 1; + } + + if( scaler_get_flags( SCALER_2XSAI ) & SCALER_FLAGS_FULL_REFRESH ) { + fprintf( stderr, "2xSaI scaler unexpectedly requires full refresh\n" ); + return 1; + } + + /* Interior point: x-=2, y-=2, w+=3, h+=3 */ + x = 10; y = 10; w = 5; h = 3; + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 8, 8, 8, 6 ) ) return 1; + + /* Top-left corner: negative coordinates clipped to 0, w/h reduced by clip */ + x = 1; y = 1; w = 4; h = 4; + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 0, 0, 6, 6 ) ) return 1; + + /* Bottom-right corner: w/h clipped to image bounds */ + x = 316; y = 237; w = 4; h = 3; + expander( &x, &y, &w, &h, 320, 240 ); + return scalerexpandtest_expect( x, y, w, h, 314, 235, 6, 5 ); +} + +/* expand_dotmatrix: rounds dirty-rect y down to the nearest even row. */ +static int +scalerexpandtest_dotmatrix( void ) +{ + scaler_expand_fn *expander = scaler_get_expander( SCALER_DOTMATRIX ); + int x, y, w, h; + + if( !expander ) { + fprintf( stderr, "Dot Matrix scaler has no dirty-region expander\n" ); + return 1; + } + + if( scaler_get_flags( SCALER_DOTMATRIX ) & SCALER_FLAGS_FULL_REFRESH ) { + fprintf( stderr, + "Dot Matrix scaler unexpectedly requires full refresh\n" ); + return 1; + } + + /* Odd y: rounds down to even, h gains one extra row */ + x = 12; y = 9; w = 3; h = 1; + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 12, 8, 3, 2 ) ) return 1; + + /* Even y: no change to x, y, w, or h */ + x = 12; y = 8; w = 3; h = 1; + expander( &x, &y, &w, &h, 320, 240 ); + if( scalerexpandtest_expect( x, y, w, h, 12, 8, 3, 1 ) ) return 1; + + /* y=0 (even): no rounding needed */ + x = 5; y = 0; w = 10; h = 2; + expander( &x, &y, &w, &h, 320, 240 ); + return scalerexpandtest_expect( x, y, w, h, 5, 0, 10, 2 ); +} + +/* NTSC TV scalers require a full-screen refresh and have no expander. */ +static int +scalerexpandtest_ntsctv( void ) +{ + if( scaler_get_expander( SCALER_NTSC2X ) != NULL ) { + fprintf( stderr, "NTSC TV 2x scaler unexpectedly has an expander\n" ); + return 1; + } + + if( !( scaler_get_flags( SCALER_NTSC2X ) & SCALER_FLAGS_FULL_REFRESH ) ) { + fprintf( stderr, + "NTSC TV 2x scaler should have SCALER_FLAGS_FULL_REFRESH\n" ); + return 1; + } + + if( scaler_get_expander( SCALER_NTSC3X ) != NULL ) { + fprintf( stderr, "NTSC TV 3x scaler unexpectedly has an expander\n" ); + return 1; + } + + if( !( scaler_get_flags( SCALER_NTSC3X ) & SCALER_FLAGS_FULL_REFRESH ) ) { + fprintf( stderr, + "NTSC TV 3x scaler should have SCALER_FLAGS_FULL_REFRESH\n" ); + return 1; + } + + return 0; +} + int main( void ) { - return scalerexpandtest_paltv(); + int r = 0; + + r += scalerexpandtest_paltv(); + r += scalerexpandtest_advmame(); + r += scalerexpandtest_sai(); + r += scalerexpandtest_dotmatrix(); + r += scalerexpandtest_ntsctv(); + + return r; } From 3c9bbf52dc76ae381fbf5e420e85dee463e720a8 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 23 Jul 2026 21:42:35 +1000 Subject: [PATCH 134/142] Fix strerror() calls with wrong arguments in sdlsound.c 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. --- sound/sdlsound.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sound/sdlsound.c b/sound/sdlsound.c index 348e3ded..3a6c1393 100644 --- a/sound/sdlsound.c +++ b/sound/sdlsound.c @@ -69,7 +69,7 @@ sound_lowlevel_init( const char *device, int *freqptr, int *stereoptr ) libspectrum_free( command ); settings_current.sound = 0; ui_error( UI_ERROR_ERROR, "Couldn't set SDL_AUDIODRIVER: %s", - strerror ( error ) ); + strerror( errno ) ); return 1; } /* On POSIX, putenv() stores the pointer directly in the environment @@ -139,7 +139,7 @@ sound_lowlevel_init( const char *device, int *freqptr, int *stereoptr ) * received.channels * sound_framesiz + 1 ) ) ) { ui_error( UI_ERROR_ERROR, "Problem initialising sound fifo: %s", - strerror ( error ) ); + strerror( -error ) ); return 1; } @@ -181,7 +181,7 @@ sound_lowlevel_frame( libspectrum_signed_word *data, int len ) } if( i < 0 ) { ui_error( UI_ERROR_ERROR, "Couldn't write sound fifo: %s", - strerror( i ) ); + strerror( -i ) ); } if( !audio_output_started ) { From 57cbfc9caa06942fb8b3030d01bd0f50bc510ae3 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Thu, 23 Jul 2026 21:54:30 +1000 Subject: [PATCH 135/142] Decode tape header names with libspectrum_zx_string_to_utf8() which supports BASIC tokens and symbols. --- tape.c | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/tape.c b/tape.c index c70ed754..225c5a7f 100644 --- a/tape.c +++ b/tape.c @@ -106,7 +106,6 @@ static libspectrum_dword next_tape_edge_tstates; static int tape_autoload( libspectrum_machine hardware ); static int trap_load_block( libspectrum_tape_block *block ); static int tape_play( int autoplay ); -static void make_name( unsigned char *name, const unsigned char *data ); static void tape_event_record_sample( libspectrum_dword last_tstates, int type, void *user_data ); @@ -992,7 +991,7 @@ tape_block_details( char *buffer, size_t length, libspectrum_tape_block *block ) { libspectrum_byte *data; - const char *type; unsigned char name[11]; + const char *type; char *name; int offset; size_t i; unsigned long total_pulses; @@ -1020,9 +1019,10 @@ tape_block_details( char *buffer, size_t length, default: goto normal; } - make_name( name, &data[2] ); + name = libspectrum_zx_string_to_utf8( &data[2], 10 ); snprintf( buffer, length, "%s: \"%s\"", type, name ); + libspectrum_free( name ); break; @@ -1108,19 +1108,3 @@ tape_block_details( char *buffer, size_t length, return 0; } - -static void -make_name( unsigned char *name, const unsigned char *data ) -{ - size_t i; - - for( i = 0; i < 10; i++, name++, data++ ) { - if( *data >= 32 && *data < 127 ) { - *name = *data; - } else { - *name = '?'; - } - } - - *name = '\0'; -} From 57ca1e20da0bdbd62ce847845bda90a673cefe33 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 25 Jul 2026 11:23:29 +1000 Subject: [PATCH 136/142] Use updated libspectrum_zx_string_to_utf8() and update libspectrum dependency to make sure it is available. --- configure.ac | 2 +- tape.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index b5b7ba8b..447a8e51 100644 --- a/configure.ac +++ b/configure.ac @@ -129,7 +129,7 @@ if test "$prefix" != "NONE"; then fi) dnl Check that libspectrum is available and that it is new enough -PKG_CHECK_MODULES([LIBSPECTRUM], [libspectrum >= 1.6.0]) +PKG_CHECK_MODULES([LIBSPECTRUM], [libspectrum >= 1.6.3]) dnl Check if supplied libspectrum has its own internal GLib replacement AC_MSG_CHECKING(whether libspectrum has its own internal GLib replacement) diff --git a/tape.c b/tape.c index 225c5a7f..a3f295e5 100644 --- a/tape.c +++ b/tape.c @@ -991,7 +991,7 @@ tape_block_details( char *buffer, size_t length, libspectrum_tape_block *block ) { libspectrum_byte *data; - const char *type; char *name; + const char *type; char name[ 10 * 9 + 1 ]; int offset; size_t i; unsigned long total_pulses; @@ -1019,10 +1019,10 @@ tape_block_details( char *buffer, size_t length, default: goto normal; } - name = libspectrum_zx_string_to_utf8( &data[2], 10 ); + if( libspectrum_zx_string_to_utf8( name, sizeof( name ), &data[2], 10 ) ) + goto normal; snprintf( buffer, length, "%s: \"%s\"", type, name ); - libspectrum_free( name ); break; From 05ce3603b277725351f7dbf4ad8406d9d67090f7 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 25 Jul 2026 12:04:03 +1000 Subject: [PATCH 137/142] Updates for a "1.9.1" version --- ChangeLog | 35 +++++++++++++++++++++++++++++++++++ README | 4 ++-- configure.ac | 4 ++-- man/fuse.1 | 2 +- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 94667451..0f1d7c93 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,38 @@ +2026-07-25 Fredrick Meunier + + * Fuse 1.9.1 released. + + * UI improvements: + * GTK3: add freely resizable, aspect-ratio-preserving display + scaling, including improved fullscreen behaviour (part of feature + request #135) (Alberto Garcia). + * GTK3: preserve scaler families when resizing and prevent stale + display borders (Alberto Garcia). + * GTK3/Windows: fix drag-and-drop file paths (Sergio Baldoví). + * Wii: skip symbol characters (fixes bug #266) (Sergio Baldoví). + + * Debugger improvements: + * Correct decimal rendering of values greater than INT_MAX + (Fredrick Meunier). + + * Scaler improvements: + * Implement PAL composite scaler chroma blending across lines + (Fredrick Meunier). + + * Miscellaneous improvements: + * Fall back to default settings when loading a settings file fails + (Fredrick Meunier). + * Decode BASIC tokens and symbols in tape header names (Fredrick + Meunier). + * Always initialise persistent memory-pool allocations (fixes bug + #232) (Fredrick Meunier). + + * Testing improvements: + * Include Z80 core tests in `make check` and expand disassembler, + debugger-expression and scaler test coverage (Fredrick Meunier). + + * Various other minor bugfixes. + 2026-06-15 Fredrick Meunier * Fuse 1.9.0 released. diff --git a/README b/README index fe32e406..ec69b224 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -The Free Unix Spectrum Emulator (Fuse) 1.9.0 +The Free Unix Spectrum Emulator (Fuse) 1.9.1 ============================================ Fuse (the Free Unix Spectrum Emulator) was originally, and somewhat @@ -116,4 +116,4 @@ Usenet newsgroup `comp.sys.sinclair' or the World of Spectrum forums . Philip Kendall -15th June, 2026 +25th July, 2026 diff --git a/configure.ac b/configure.ac index 447a8e51..41ee88b9 100644 --- a/configure.ac +++ b/configure.ac @@ -19,12 +19,12 @@ dnl dnl E-mail: philip-fuse@shadowmagic.org.uk dnl Package version -m4_define([fuse_version], [1.9.0]) +m4_define([fuse_version], [1.9.1]) dnl Product full version m4_define([fuse_major_version], [1]) m4_define([fuse_minor_version], [9]) -m4_define([fuse_micro_version], [0]) +m4_define([fuse_micro_version], [1]) m4_define([fuse_nano_version], [0]) m4_define([fuse_full_version], [fuse_major_version.fuse_minor_version.fuse_micro_version.fuse_nano_version]) m4_define([fuse_rc_version], [fuse_major_version,fuse_minor_version,fuse_micro_version,fuse_nano_version]) diff --git a/man/fuse.1 b/man/fuse.1 index beb46b7e..1c8ed387 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -26,7 +26,7 @@ .\" E-mail: philip-fuse@shadowmagic.org.uk .\" .\" -.TH fuse 1 "15th June, 2026" "1.9.0" "Emulators" +.TH fuse 1 "25th July, 2026" "1.9.1" "Emulators" .\" .\"------------------------------------------------------------------ .\" From f3d754ee2cc63032ff2643c9a3d732218ed1d849 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 25 Jul 2026 21:29:33 +1000 Subject: [PATCH 138/142] Don't redefine main on Windows for palcompositetest --- unittests/palcompositetest.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unittests/palcompositetest.c b/unittests/palcompositetest.c index 087e7997..380b326b 100644 --- a/unittests/palcompositetest.c +++ b/unittests/palcompositetest.c @@ -487,6 +487,11 @@ check_scaler( ScalerProc *scaler_32, ScalerProc *scaler_16, int scale, return check_16_delay_line( BITFORMAT_555, scaler_16, scale, name ); } +#ifdef main +/* SDL headers redefine main on Windows, but this test needs a normal entry point. */ +#undef main +#endif + int main( void ) { From 51775b86f73de43569ffa871884b3be1fed27603 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Sat, 25 Jul 2026 21:48:05 +1000 Subject: [PATCH 139/142] Don't redefine main on Windows for scalerexpandtest --- unittests/scalerexpandtest.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unittests/scalerexpandtest.c b/unittests/scalerexpandtest.c index 46240dc9..8197256e 100644 --- a/unittests/scalerexpandtest.c +++ b/unittests/scalerexpandtest.c @@ -205,6 +205,11 @@ scalerexpandtest_ntsctv( void ) return 0; } +#ifdef main +/* SDL headers redefine main on Windows, but this test needs a normal entry point. */ +#undef main +#endif + int main( void ) { From 8e5be95de48aaff32efd7715f0ee42af55ebff04 Mon Sep 17 00:00:00 2001 From: Fredrick Meunier Date: Mon, 27 Jul 2026 20:08:23 +1000 Subject: [PATCH 140/142] Updates for a "1.9.1" version --- .agents/skills/update-changelog/SKILL.md | 20 +++++++++++++------ ...ourceforge.fuse_emulator.Fuse.metainfo.xml | 12 +++++++++++ hacking/releasing.md | 16 ++++++++------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.agents/skills/update-changelog/SKILL.md b/.agents/skills/update-changelog/SKILL.md index 2e739611..453e2c5b 100644 --- a/.agents/skills/update-changelog/SKILL.md +++ b/.agents/skills/update-changelog/SKILL.md @@ -1,6 +1,6 @@ --- name: update-changelog -description: Drafts a new top-of-file Fuse release entry in ChangeLog using unrecorded commits and the project’s existing grouped format. Drafts first, then waits for confirmation before editing ChangeLog. +description: Drafts a new top-of-file Fuse release entry in ChangeLog and a high-level AppStream release entry using unrecorded commits and the project’s existing formats. Drafts first, then waits for confirmation before editing release notes. --- # Update ChangeLog @@ -14,7 +14,7 @@ Use this skill when the user wants to update `ChangeLog` for a release. - Follow the existing style in `ChangeLog` exactly. - Preserve all existing entries unchanged. - Draft first. -- Do not edit `ChangeLog` until the user confirms. +- Do not edit `ChangeLog` or `data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml` until the user confirms. - If the release version is not yet known, ask the user for it before drafting the new entry. - Use the most recent unrecorded commit date for the new heading. - Use Philip Kendall’s maintainer identity in the heading: @@ -36,8 +36,13 @@ Update the ChangeLog file in the current directory `ChangeLog` with any commits `YYYY-MM-DD Philip Kendall ` 7. Under that date, add a `* Fuse X.Y.Z released.` entry. 8. Under that date, add entries grouped by category, 8-space indented, matching the existing format. -9. Show the proposed new entry to the user and ask for confirmation. -10. Only after confirmation, write the updated `ChangeLog`. +9. Draft a corresponding high-level `` entry for + `data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml`. Use the same version and date, insert it first in + ``, and follow the existing XML format. Summarise only the most notable user-facing changes in a + concise `
      `; do not duplicate every ChangeLog item. +10. Show both proposed entries to the user and ask for confirmation. +11. Only after confirmation, update both `ChangeLog` and + `data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml`. ## ChangeLog grouping structure @@ -99,11 +104,14 @@ bullets under the heading: When presenting the draft: -- show only the proposed new ChangeLog entry -- do not edit the file yet +- show only the proposed new ChangeLog and AppStream release entries +- do not edit either file yet - ask the user to confirm or request changes When the user confirms: - insert the new entry at the top of `ChangeLog` after the first line +- insert the matching high-level release entry first in `` in + `data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml` - do not alter older entries +- validate the AppStream XML after editing diff --git a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml index 0a1700d8..00763d9e 100644 --- a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml +++ b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml @@ -17,6 +17,18 @@ Emulator + + +
        +
      • GTK3: added freely resizable, aspect-ratio-preserving display scaling and improved fullscreen behaviour
      • +
      • Improved PAL composite scaler chroma blending across lines
      • +
      • Improved tape header names by decoding BASIC tokens and symbols
      • +
      • Fall back to default settings if loading the settings file fails
      • +
      • Debugger: corrected decimal rendering of large values
      • +
      • Various other minor bug fixes and improvements
      • +
      +
      +
        diff --git a/hacking/releasing.md b/hacking/releasing.md index f9b39219..8b623e63 100644 --- a/hacking/releasing.md +++ b/hacking/releasing.md @@ -69,13 +69,15 @@ existing style. new top-of-file release entry, review it, and then apply it after confirmation. Otherwise, update `ChangeLog` manually in the existing format. -2. Run `update-release.py` with `--dry-run`. -3. Run it again without `--dry-run` once the changes look correct. -4. Review the resulting diff. -5. If `configure.ac` changed, regenerate the build system as needed. -6. Build the project. -7. Run the test suite. -8. Create the release tarball, tag, and publish using the normal +2. Update `data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml` with + the high level changes in the release. +3. Run `update-release.py` with `--dry-run`. +4. Run it again without `--dry-run` once the changes look correct. +5. Review the resulting diff. +6. If `configure.ac` changed, regenerate the build system as needed. +7. Build the project. +8. Run the test suite. +9. Create the release tarball, tag, and publish using the normal maintainer process. ## Verification From b782ba1fde1149d5cd99c7ce81ff664676a49423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Baldov=C3=AD?= Date: Thu, 30 Jul 2026 23:38:51 +0200 Subject: [PATCH 141/142] Distribute data/win32/LICENSES.txt and data/win32/README-win32.txt --- data/Makefile.am | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/Makefile.am b/data/Makefile.am index 0ff47a00..627c0669 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -123,6 +123,8 @@ EXTRA_DIST += \ data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml \ data/fuse.xml.in \ data/shell-completion/diff_options.sh \ + data/win32/LICENSES.txt \ + data/win32/README-win32.txt \ data/win32/fuse.manifest.in \ data/win32/installer.nsi.in \ data/win32/winfuse.ico From 3135b77a69070baf42482f11fcf4b647822c3e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Baldov=C3=AD?= Date: Fri, 31 Jul 2026 00:04:31 +0200 Subject: [PATCH 142/142] Extract transitive dependencies for Windows distribution --- data/Makefile.am | 1 + data/win32/check_win32_dll_deps.sh | 87 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 data/win32/check_win32_dll_deps.sh diff --git a/data/Makefile.am b/data/Makefile.am index 627c0669..56422f59 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -125,6 +125,7 @@ EXTRA_DIST += \ data/shell-completion/diff_options.sh \ data/win32/LICENSES.txt \ data/win32/README-win32.txt \ + data/win32/check_win32_dll_deps.sh \ data/win32/fuse.manifest.in \ data/win32/installer.nsi.in \ data/win32/winfuse.ico diff --git a/data/win32/check_win32_dll_deps.sh b/data/win32/check_win32_dll_deps.sh new file mode 100644 index 00000000..7597bcaf --- /dev/null +++ b/data/win32/check_win32_dll_deps.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -u + +root_dir=${1:-} +if [ -z "$root_dir" ] || [ ! -d "$root_dir" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +is_system_dll() { + case "$1" in + kernel32.dll|user32.dll|gdi32.dll|advapi32.dll|shell32.dll|ole32.dll| \ + oleaut32.dll|uuid.dll|ws2_32.dll|comdlg32.dll|winmm.dll|version.dll| \ + msvcrt.dll|bcrypt.dll|ntdll.dll|shlwapi.dll|crypt32.dll|dbghelp.dll| \ + psapi.dll|iphlpapi.dll|secur32.dll|setupapi.dll|imm32.dll|mpr.dll| \ + winspool.drv|api-ms-win-*|ext-ms-*|comctl32.dll|dwmapi.dll|dnsapi.dll| \ + dwrite.dll|gdiplus.dll|hid.dll|msimg32.dll|opengl32.dll|rpcrt4.dll|usp10.dll) return 0 ;; + esac + return 1 +} + +search_dirs=( + "$root_dir" + /usr/local/i686-w64-mingw32/bin + /usr/i686-w64-mingw32/sys-root/mingw/bin + /usr/i686-w64-mingw32/sys-root/mingw/system32 + /usr/local/i686-w64-mingw32/system32 + /usr/i686-pc-mingw32/sys-root/mingw/bin + /usr/i686-pc-mingw32/sys-root/mingw/system32 +) + +if [ -n "${CYGWIN_LIBDIR:-}" ]; then + search_dirs+=("${CYGWIN_LIBDIR%/}") +fi + +# MSYS2 toolchain +if [ -n "${MSYSTEM_PREFIX:-}" ]; then + search_dirs+=("${MSYSTEM_PREFIX}/bin") +fi + +if [ -n "${WIN32_DLL_SEARCH_DIRS:-}" ]; then + # shellcheck disable=SC2206 + extra_dirs=(${WIN32_DLL_SEARCH_DIRS}) + search_dirs+=("${extra_dirs[@]}") +fi + +find_binaries() { + if [ "${WIN32_DLL_DEPS_SCAN_EXES:-1}" = 0 ]; then + find "$root_dir" -maxdepth 1 -type f -name '*.dll' -print + else + find "$root_dir" -maxdepth 1 -type f \( -name '*.exe' -o -name '*.dll' \) -print + fi +} + +changed=1 +while [ "$changed" -ne 0 ]; do + changed=0 + while IFS= read -r file; do + while IFS= read -r dll; do + dll_lc=$(printf '%s' "$dll" | tr '[:upper:]' '[:lower:]') + if is_system_dll "$dll_lc"; then + continue + fi + + if [ -f "$root_dir/$dll" ]; then + continue + fi + + found=0 + for searchdir in "${search_dirs[@]}"; do + if [ -f "$searchdir/$dll" ]; then + cp -f "$searchdir/$dll" "$root_dir/" + changed=1 + found=1 + break + fi + done + + if [ "$found" -eq 0 ]; then + echo "Missing dependency for ${file}: ${dll}" >&2 + exit 1 + fi + done < <( + objdump -p "$file" 2>/dev/null | grep 'DLL Name' | sed 's/.*DLL Name: //' + ) + done < <(find_binaries) +done