diff --git a/.agents/skills/README.md b/.agents/skills/README.md new file mode 100644 index 000000000..fc4af2843 --- /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 000000000..453e2c5b9 --- /dev/null +++ b/.agents/skills/update-changelog/SKILL.md @@ -0,0 +1,117 @@ +--- +name: update-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 + +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` 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: + `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. 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 + +### 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 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/.beads/issues.jsonl b/.beads/issues.jsonl index 5851aa42d..be0b92ee8 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,6 +11,30 @@ {"_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-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} +{"_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} {"_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} @@ -42,8 +68,9 @@ {"_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} {"_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/.github/workflows/build_libspectrum_macos_sub.yml b/.github/workflows/build_libspectrum_macos_sub.yml index ea4f4c4bd..da528bc86 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 @@ -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: diff --git a/.github/workflows/build_libspectrum_windows_sub.yml b/.github/workflows/build_libspectrum_windows_sub.yml index 4aa6aea4c..69be2f771 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 @@ -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_windows_sub.yml b/.github/workflows/build_windows_sub.yml index c8775899d..d787f9aa5 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 @@ -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) diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml deleted file mode 100755 index aa1e5bd2e..000000000 --- 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 }}" diff --git a/.github/workflows/windows_release.yml b/.github/workflows/windows_release.yml index e66d502c3..c935bcc1b 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: @@ -74,20 +73,20 @@ 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: 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" + dependencies: "mingw64-i686-SDL2 mingw64-i686-libxml2" + 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_libxml2: "yes" + verify_audio: "sdl" + verify_other: "Using SDL: 2" uses: ./.github/workflows/build_windows_sub.yml with: key: ${{ matrix.build.key }} diff --git a/.gitignore b/.gitignore index d5c605251..b1e3a4faf 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ tags /unittests/sdl2scalerstatetest /unittests/sdl2joysticktest /unittests/sdl2mousetest +/unittests/*.exe /ylwrap /z80/coretest /z80/coretest.exe diff --git a/3rdparty/FuseGenerator b/3rdparty/FuseGenerator index 82bf742b4..cc0b25d18 160000 --- a/3rdparty/FuseGenerator +++ b/3rdparty/FuseGenerator @@ -1 +1 @@ -Subproject commit 82bf742b419c0d9bfd982bf4f8deaa6c7b0d9d42 +Subproject commit cc0b25d18fb9bfef24cf9f0f2a1f721d7874293d diff --git a/3rdparty/FuseImporter b/3rdparty/FuseImporter index 324f1e52c..9deb360c9 160000 --- a/3rdparty/FuseImporter +++ b/3rdparty/FuseImporter @@ -1 +1 @@ -Subproject commit 324f1e52ced0f9949da4190df9302e9a5b5a1208 +Subproject commit 9deb360c9ac6c1c121c18c4fdf6dc99fbe75906c diff --git a/3rdparty/libspectrum b/3rdparty/libspectrum index e4a2d6e12..282b61099 160000 --- a/3rdparty/libspectrum +++ b/3rdparty/libspectrum @@ -1 +1 @@ -Subproject commit e4a2d6e12482e0e4098cde71a0fe2abb373946d5 +Subproject commit 282b61099a2b7e171b0a13fc39821adb452e08ab diff --git a/AUTHORS b/AUTHORS index cffbc1ee9..04bc5df24 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/ChangeLog b/ChangeLog index b609b963b..0f1d7c935 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,97 @@ +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. + + * 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/Makefile.am b/Makefile.am index 68bc22e51..94bb6f4e2 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 @@ -159,7 +160,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 \ @@ -169,7 +169,10 @@ EXTRA_DIST = AUTHORS \ settings.dat \ settings.pl \ settings-win32.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 \ @@ -228,7 +231,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 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 @@ -236,3 +239,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/README b/README index dd2d5bae0..ec69b224a 100644 --- a/README +++ b/README @@ -1,10 +1,10 @@ -The Free Unix Spectrum Emulator (Fuse) 1.8.0 +The Free Unix Spectrum Emulator (Fuse) 1.9.1 ============================================ 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. @@ -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,13 +67,13 @@ 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. -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 @@ -92,26 +92,28 @@ 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'. For Spectrum discussions not directly related to Fuse, visit either the Usenet newsgroup `comp.sys.sinclair' or the World of Spectrum forums . Philip Kendall -11th May, 2026 +25th July, 2026 diff --git a/compat/linux/relative_paths.c b/compat/linux/relative_paths.c index d1f983630..9b18db365 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 b63c55d86..2c97e1ecc 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 ); diff --git a/configure.ac b/configure.ac index a55bf826e..687fb8fbd 100644 --- a/configure.ac +++ b/configure.ac @@ -20,20 +20,20 @@ dnl E-mail: philip-fuse@shadowmagic.org.uk dnl Package version (user-visible: tarballs, fuse --version, NSIS text). dnl Keep README, man/fuse.1 (.TH third field), metainfo.xml, and fusepb MARKETING_VERSION in sync. -m4_define([fuse_version], [1.8.0-fusex-0.7]) +m4_define([fuse_version], [1.9.1-fusex-0.1]) dnl Win32 FILEVERSION / NSIS VIProductVersion require four integers only; map fusex-0.1 -> fourth field 1. m4_define([fuse_major_version], [1]) -m4_define([fuse_minor_version], [8]) -m4_define([fuse_micro_version], [0]) +m4_define([fuse_minor_version], [9]) +m4_define([fuse_micro_version], [1]) m4_define([fuse_nano_version], [1]) 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]) 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([fusex],[fuse_version],[fuse_bugreport],[fusex],[fuse_url]) AC_CONFIG_SRCDIR(fuse.c) @@ -77,10 +77,9 @@ fi dnl Checks for programs. 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 @@ -113,10 +112,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]) @@ -135,7 +130,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) @@ -273,17 +268,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, @@ -296,54 +281,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 @@ -565,20 +540,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" @@ -673,13 +637,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' @@ -748,62 +712,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 "$sdl1_available" = yes; then - AC_MSG_RESULT(yes) - AC_DEFINE([USE_JOYSTICK], 1, [Defined if we're using hardware joysticks]) - use_sdl1=yes - else - AC_MSG_WARN([SDL version $SDL1_VERSION not 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) @@ -1020,8 +952,8 @@ AC_SUBST(WINSPARKLE_CFLAGS) AC_SUBST(WINSPARKLE_LIBS) 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, @@ -1127,19 +1059,11 @@ 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 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/Makefile.am b/data/Makefile.am index 872ef9cf8..97ff07dd1 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 @@ -75,11 +76,18 @@ 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|@FUSE_BIN_EXTENSION@|$(EXEEXT)|' \ + -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 \ @@ -115,6 +123,9 @@ 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/check_win32_dll_deps.sh \ data/win32/fuse.manifest.in \ data/win32/installer.nsi.in \ data/win32/winfuse.ico \ diff --git a/data/net.sourceforge.fuse_emulator.Fuse.desktop.in b/data/net.sourceforge.fuse_emulator.Fuse.desktop.in index ecd66540a..f6485a6e8 100644 --- a/data/net.sourceforge.fuse_emulator.Fuse.desktop.in +++ b/data/net.sourceforge.fuse_emulator.Fuse.desktop.in @@ -1,11 +1,11 @@ [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 MimeType=@MIME_TYPES@ -Exec=fuse %f +Exec=fuse@FUSE_BIN_EXTENSION@ %f Icon=net.sourceforge.fuse_emulator.Fuse Keywords=sinclair;zx;game;retro; diff --git a/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml b/data/net.sourceforge.fuse_emulator.Fuse.metainfo.xml index 575c56bd0..169dcd7fb 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 @@ -17,10 +17,33 @@ Emulator - +
      -
    • FuseX fork release (packaging, Win32 UI, tooling)
    • +
    • FuseX fork release (macOS app, CLI options, headless mode, debugger)
    • +
    +
    +
    + + +
      +
    • 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
    • +
    +
    +
    + + +
      +
    • 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/data/shell-completion/bash/fuse b/data/shell-completion/bash/fuse index f93e07956..2e1e28097 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 @@ -91,7 +92,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,8 +163,13 @@ _fuse() _filedir '@(txt)' return 0 ;; - --competition-code|--debugger-command| \ - --drive-40-max-track|--drive-80-max-track|--joystick-[12]|-j| \ + --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| \ --joystick-keyboard-fire|--joystick-keyboard-left| \ @@ -182,7 +188,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 @@ -198,14 +210,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 @@ -237,17 +249,17 @@ _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 --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 - --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 @@ -272,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/data/shell-completion/diff_options.sh b/data/shell-completion/diff_options.sh index 7b3a7338e..c739e7f21 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/data/win32/check_win32_dll_deps.sh b/data/win32/check_win32_dll_deps.sh new file mode 100644 index 000000000..7597bcaf4 --- /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 diff --git a/debugger/breakpoint.c b/debugger/breakpoint.c index 412e1bc05..dda32add5 100644 --- a/debugger/breakpoint.c +++ b/debugger/breakpoint.c @@ -76,8 +76,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 ); diff --git a/debugger/command.c b/debugger/command.c index 4b35673cd..65dd5d353 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 @@ -69,14 +70,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 6cc2586c8..09d9a727c 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 @@ -50,7 +51,7 @@ ID [a-z]([a-z0-9']|\\[ ])* -%option caseless +%option caseless noyywrap %s COMMANDSTATE1 %x COMMANDSTATE2 @@ -99,6 +100,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 e3789fd6d..ab6bc9d8c 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 @@ -69,6 +70,8 @@ %token COMPARISON /* < > <= >= */ %token EQUALITY /* == != */ %token NEGATE /* ! ~ */ +%token LSHIFT /* << */ +%token RSHIFT /* >> */ %token BASE %token BREAK @@ -131,8 +134,9 @@ %left '&' %left EQUALITY %left COMPARISON +%left LSHIFT RSHIFT %left '+' '-' -%left '*' '/' +%left '*' '/' '%' %right NEGATE /* Unary minus, unary plus, !, ~ */ /* High precedence */ @@ -274,6 +278,22 @@ 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 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.h b/debugger/debugger.h index c77d8e867..0f811122e 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 @@ -103,5 +104,7 @@ 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/debugger_internals.h b/debugger/debugger_internals.h index 9ded57704..fb9bef940 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 @@ -72,8 +73,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; @@ -92,11 +95,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/disassemble.c b/debugger/disassemble.c index 42767fdd6..10ffbb7ce 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 @@ -36,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 }; @@ -108,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 ); @@ -129,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 */ @@ -146,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 ); @@ -167,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 ); @@ -177,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; @@ -198,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; @@ -240,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 ) { @@ -251,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; } @@ -268,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 ) { @@ -279,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; } @@ -296,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 ) { @@ -310,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; @@ -320,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; @@ -330,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; @@ -373,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; @@ -395,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; @@ -428,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 ) { @@ -438,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; @@ -465,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 ); @@ -486,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" @@ -515,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; @@ -524,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; @@ -535,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; @@ -568,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; @@ -589,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; } @@ -717,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 ) ); @@ -726,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 { @@ -854,6 +858,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 +911,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 */ @@ -915,17 +944,421 @@ 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 */ + +/* 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 */ + +/* 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 */ + +/* 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 */ + +/* 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 */ + +/* 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 */ + +/* 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) */ + +/* 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) */ + +/* 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 */ + +/* 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) */ + +/* 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 */ + +/* 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 */ + +/* 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 ) { - char disassembly[40]; + char disassembly[DISASM_BUF_LEN]; size_t length; memcpy( memory_map_read[8].page, data, data_length ); @@ -972,11 +1405,30 @@ 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" ); 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)" ); @@ -1012,13 +1464,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" ); @@ -1030,15 +1494,411 @@ 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" ); + + /* 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" ); + + /* 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" ); + + 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" ); + + /* 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)" ); + + /* 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)" ); + + /* 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" ); + + /* 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)" ); + + /* 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" ); + + /* 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; +} + +/* 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; + + /* 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; } diff --git a/debugger/event.c b/debugger/event.c index 273265750..1f6e87951 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; @@ -109,7 +110,7 @@ void debugger_track_frame( libspectrum_dword frame_length ) void debugger_event( int event_code ) { - debugger_event_t event; + const debugger_event_t *event; debugger_breakpoint *bp; GSList *ptr, *ptr_next; @@ -121,7 +122,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 ) { @@ -130,7 +131,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 3a8e3c160..5e4142ff1 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 @@ -53,6 +54,7 @@ enum precedence_t { PRECEDENCE_BITWISE_AND, PRECEDENCE_EQUALITY, PRECEDENCE_COMPARISON, + PRECEDENCE_SHIFT, PRECEDENCE_ADDITION, PRECEDENCE_MULTIPLICATION, PRECEDENCE_NEGATE, @@ -92,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 ); @@ -130,7 +132,11 @@ 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_LEFT_SHIFT: + case DEBUGGER_TOKEN_RIGHT_SHIFT: + return PRECEDENCE_SHIFT; case DEBUGGER_TOKEN_EQUAL_TO: case DEBUGGER_TOKEN_NOT_EQUAL_TO: @@ -260,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; @@ -315,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 ) { @@ -341,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 ) { @@ -359,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 ) { @@ -381,6 +387,27 @@ 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_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: { + 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 ) == debugger_expression_evaluate( binary->op2 ); @@ -434,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 ); } @@ -530,6 +557,9 @@ 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_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; @@ -619,7 +649,11 @@ 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; + case DEBUGGER_TOKEN_RIGHT_SHIFT: return 1; /* None of the comparison operators are associative due to them returning truth values */ @@ -647,3 +681,785 @@ 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_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 ) +{ + 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; +} + +/* 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; +} + +static libspectrum_dword +get_sysvar_sentinel( void ) +{ + return 42; +} + +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_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" ); + 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" ); + + /* 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; + + 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" ); + + /* 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" ); + + /* 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( '&', + 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" ); + + /* 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( '-', + 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" ); + + /* '/' 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" ); + + /* '>>' 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( + 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( '-', + 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" ); + + /* 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" ); + + 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" ); + + /* 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++; + } + } + + /* 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" ); + + /* 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; + + 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; +} + diff --git a/debugger/system_variable.c b/debugger/system_variable.c index 314b180fd..e64943ffa 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; } } diff --git a/display.c b/display.c index 2674e4507..68bed9fac 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 @@ -69,14 +69,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 @@ -102,6 +94,14 @@ 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. + 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_screen_x, display_cached_screen_y; + /* The border colour changes which have occurred in this frame. `t` is the T-state offset within the scanline at which the change becomes visible (post end-of-OUT alignment), measured from the T-state where this @@ -123,9 +123,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; @@ -160,10 +157,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++) @@ -172,7 +167,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 ); @@ -242,8 +231,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 ); @@ -252,9 +242,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; @@ -263,7 +253,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; } @@ -272,11 +263,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 ); } } @@ -284,8 +275,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 ); @@ -421,11 +412,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 ); } @@ -560,6 +555,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 */ @@ -637,18 +638,18 @@ copy_critical_region( int beam_x, int beam_y ) } static inline void -get_beam_position( libspectrum_dword t, int *x, int *y ) +get_beam_position( int *x, int *y ) { - if( t < machine_current->line_times[ 0 ] ) { + if( tstates < machine_current->line_times[ 0 ] ) { *x = *y = -1; return; } - *y = ( t - machine_current->line_times[ 0 ] ) / + *y = ( tstates - machine_current->line_times[ 0 ] ) / machine_current->timings.tstates_per_line; if( *y >= 0 && *y <= DISPLAY_SCREEN_HEIGHT ) - *x = ( t - machine_current->line_times[ *y ] ) / 4; + *x = ( tstates - machine_current->line_times[ *y ] ) / 4; else *x = 0; } @@ -657,27 +658,33 @@ update_critical_internal( int x, int y ) { int beam_x, beam_y; - get_beam_position( tstates, &beam_x, &beam_y ); + if( tstates != display_cached_beam_tstates ) { + get_beam_position( &beam_x, &beam_y ); + display_cached_beam_tstates = tstates; - 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 ); } void @@ -717,9 +724,14 @@ static void display_dirty64( libspectrum_word offset ) { int i, x, y; + int idx = offset - ( DISPLAY_HEIGHT * DISPLAY_WIDTH_COLS ); - x=display_dirty_xtable2[ offset - 0x1800 ]; - y=display_dirty_ytable2[ offset - 0x1800 ]; + /* The attribute area is laid out linearly: column x, row y, so: + x = idx % DISPLAY_WIDTH_COLS + y = (idx / DISPLAY_WIDTH_COLS) * 8 + Both divisions are exact powers of two and optimise to shifts/masks. */ + x = idx & ( DISPLAY_WIDTH_COLS - 1 ); + y = ( idx / DISPLAY_WIDTH_COLS ) * 8; for( i = 0; i < 8; i++ ) display_dirty_chunk( x, y + i ); } @@ -981,6 +993,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; @@ -990,10 +1006,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; @@ -1015,14 +1031,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 ); } @@ -1050,7 +1070,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 ); } @@ -1136,9 +1156,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 a3fa2cb58..61f382cbb 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 @@ -63,6 +64,19 @@ /* 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 + +/* 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/fuse.c b/fuse.c index 12e1f10b0..cbeee2177 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 @@ -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 @@ -508,7 +508,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" @@ -564,7 +564,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" ); } diff --git a/fusepb/English.lproj/InfoPlist.strings b/fusepb/English.lproj/InfoPlist.strings index c96ebe67b..c930f70fe 100644 Binary files a/fusepb/English.lproj/InfoPlist.strings and b/fusepb/English.lproj/InfoPlist.strings differ diff --git a/fusepb/FuseX.xcodeproj/project.pbxproj b/fusepb/FuseX.xcodeproj/project.pbxproj index bbefa6458..d0e6922ff 100644 --- a/fusepb/FuseX.xcodeproj/project.pbxproj +++ b/fusepb/FuseX.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ B61F466009121DF100C8096C /* DebuggerController.m in Sources */ = {isa = PBXBuildFile; fileRef = B632C6AF03E5368700A864FD /* DebuggerController.m */; }; B61F466209121DF100C8096C /* FuseMenus.m in Sources */ = {isa = PBXBuildFile; fileRef = B66EA7840401075300A864FD /* FuseMenus.m */; }; B61F466309121DF100C8096C /* scaler.c in Sources */ = {isa = PBXBuildFile; fileRef = B63ABD8D042F175200A864FD /* scaler.c */; }; + FEC5107A347A105100000001 /* snes_ntsc.c in Sources */ = {isa = PBXBuildFile; fileRef = FEC51079347A105100000001 /* snes_ntsc.c */; }; B61F466409121DF100C8096C /* tc2068.c in Sources */ = {isa = PBXBuildFile; fileRef = B6FEA44F0444C3370013916D /* tc2068.c */; }; B61F466509121DF100C8096C /* dck.c in Sources */ = {isa = PBXBuildFile; fileRef = B65E4C600445DB7D00A864FD /* dck.c */; }; B61F466709121DF100C8096C /* psg.c in Sources */ = {isa = PBXBuildFile; fileRef = B6CA304C049CEC410037E9F2 /* psg.c */; }; @@ -533,6 +534,7 @@ B639B7670A6BAFCF00927E24 /* csw.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = csw.icns; sourceTree = ""; }; B639B7D00A6BB45600927E24 /* raw.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = raw.icns; sourceTree = ""; }; B63ABD8D042F175200A864FD /* scaler.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; name = scaler.c; path = ../ui/scaler/scaler.c; sourceTree = SOURCE_ROOT; }; + FEC51079347A105100000001 /* snes_ntsc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = snes_ntsc.c; path = ../ui/scaler/snes_ntsc.c; sourceTree = SOURCE_ROOT; }; B63F9949077182B4004D6DFA /* RollbackController.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = RollbackController.h; path = controllers/RollbackController.h; sourceTree = SOURCE_ROOT; }; B63F994A077182B4004D6DFA /* RollbackController.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; name = RollbackController.m; path = controllers/RollbackController.m; sourceTree = SOURCE_ROOT; }; B6403FD60A7E4B1A00E00B11 /* loader.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; path = loader.c; sourceTree = ""; }; @@ -1711,6 +1713,7 @@ F541FB5E03B0B33401FF8235 /* scaler.h */, B60A6A3A042BEECE00D41533 /* scaler_internals.h */, B63ABD8D042F175200A864FD /* scaler.c */, + FEC51079347A105100000001 /* snes_ntsc.c */, B62B19E00DD31DF500D42AAF /* scalers16.c */, ); path = scaler; @@ -2188,6 +2191,7 @@ B61F466009121DF100C8096C /* DebuggerController.m in Sources */, B61F466209121DF100C8096C /* FuseMenus.m in Sources */, B61F466309121DF100C8096C /* scaler.c in Sources */, + FEC5107A347A105100000001 /* snes_ntsc.c in Sources */, B61F466409121DF100C8096C /* tc2068.c in Sources */, B61F466509121DF100C8096C /* dck.c in Sources */, B61F466709121DF100C8096C /* psg.c in Sources */, @@ -2449,7 +2453,7 @@ ); LIBRARY_SEARCH_PATHS = .; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = "1.8.0-fusex-0.7"; + MARKETING_VERSION = "1.9.1-fusex-0.1"; OTHER_CFLAGS = ""; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( @@ -2521,7 +2525,7 @@ ); LIBRARY_SEARCH_PATHS = .; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = "1.8.0-fusex-0.7"; + MARKETING_VERSION = "1.9.1-fusex-0.1"; OTHER_CFLAGS = ""; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( @@ -2592,7 +2596,7 @@ ); LIBRARY_SEARCH_PATHS = .; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = "1.8.0-fusex-0.7"; + MARKETING_VERSION = "1.9.1-fusex-0.1"; OTHER_CFLAGS = ""; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( diff --git a/fusepb/config.h b/fusepb/config.h index 15f74b9d3..483aa62f3 100644 --- a/fusepb/config.h +++ b/fusepb/config.h @@ -167,7 +167,7 @@ /* #undef USE_WIDGET */ /* Version number of libspectrum package (encoded in snapshots etc.)*/ -#define VERSION "1.4.4" +#define VERSION "1.9.1-fusex-0.1" /* Define to 1 if your processor stores words with the most significant byte first (like Motorola and SPARC, unlike Intel and VAX). */ diff --git a/fusepb/libspectrum.h b/fusepb/libspectrum.h index 2bc90f4b1..9b70d0a5a 100644 --- a/fusepb/libspectrum.h +++ b/fusepb/libspectrum.h @@ -1275,7 +1275,7 @@ libspectrum_tape_block_alloc( libspectrum_tape_type type ); LIBSPECTRUM_API libspectrum_error libspectrum_tape_block_free( libspectrum_tape_block *block ); LIBSPECTRUM_API libspectrum_tape_type -libspectrum_tape_block_type( libspectrum_tape_block *block ); +libspectrum_tape_block_type( const libspectrum_tape_block *block ); LIBSPECTRUM_API libspectrum_error libspectrum_tape_block_set_type( libspectrum_tape_block *block, libspectrum_tape_type type ); @@ -1286,10 +1286,10 @@ libspectrum_tape_block_init( libspectrum_tape_block *block, LIBSPECTRUM_API libspectrum_error libspectrum_tape_block_description( char *buffer, size_t length, - libspectrum_tape_block *block ); + const libspectrum_tape_block *block ); LIBSPECTRUM_API int -libspectrum_tape_block_metadata( libspectrum_tape_block *block ); +libspectrum_tape_block_metadata( const libspectrum_tape_block *block ); LIBSPECTRUM_API libspectrum_dword libspectrum_tape_block_length( libspectrum_tape_block *block ); @@ -1476,6 +1476,10 @@ LIBSPECTRUM_API libspectrum_tape_generalised_data_symbol* libspectrum_tape_gener LIBSPECTRUM_API libspectrum_tape_generalised_data_symbol_edge_type libspectrum_tape_generalised_data_symbol_type( const libspectrum_tape_generalised_data_symbol *symbol ); LIBSPECTRUM_API libspectrum_word libspectrum_tape_generalised_data_symbol_pulse( const libspectrum_tape_generalised_data_symbol *symbol, size_t which ); +LIBSPECTRUM_API libspectrum_error +libspectrum_zx_string_to_utf8( char *buffer, size_t length, + const libspectrum_byte *src, size_t src_length ); + /* * Input recording routines diff --git a/fusepb/xibs/Preferences.xib b/fusepb/xibs/Preferences.xib index 2afc2f485..ec1847c8d 100644 --- a/fusepb/xibs/Preferences.xib +++ b/fusepb/xibs/Preferences.xib @@ -2047,11 +2047,11 @@ - + - + @@ -2062,7 +2062,7 @@ - + @@ -2073,7 +2073,7 @@ - + @@ -2084,7 +2084,7 @@ - + @@ -2095,7 +2095,7 @@ - + @@ -2106,7 +2106,7 @@ - + diff --git a/hacking/Makefile.am b/hacking/Makefile.am index ba50db4e7..96f322833 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 @@ -27,6 +28,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/hacking/releasing.md b/hacking/releasing.md new file mode 100644 index 000000000..8b623e634 --- /dev/null +++ b/hacking/releasing.md @@ -0,0 +1,101 @@ +# 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. 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 + +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. diff --git a/hacking/unittests.txt b/hacking/unittests.txt index 4d824158e..b2f86f566 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'. diff --git a/keyboard.c b/keyboard.c index e493892f4..c281c47fb 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 @@ -36,7 +37,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; @@ -336,7 +337,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]; } @@ -380,7 +381,7 @@ int keyboard_release_all( void ) { int i; - for( i=0; i<8; i++ ) keyboard_return_values[i] = 0xff; + for( i=0; i \&wii_keysym }, diff --git a/m4/ax_create_stdint_h.m4 b/m4/ax_create_stdint_h.m4 deleted file mode 100644 index 33a21f8ef..000000000 --- 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" -]) -]) diff --git a/m4/ax_pthread.m4 b/m4/ax_pthread.m4 index d383ad5c6..9f35d1391 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 diff --git a/m4/ax_strings_strcasecmp.m4 b/m4/ax_strings_strcasecmp.m4 new file mode 100644 index 000000000..b78966579 --- /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 diff --git a/m4/pkg.m4 b/m4/pkg.m4 index c5b26b52e..c88ef6ffe 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 diff --git a/man/fuse.1 b/man/fuse.1 index 2261e9f84..dbeeebdfc 100644 --- a/man/fuse.1 +++ b/man/fuse.1 @@ -1,9 +1,11 @@ +'\" t .\" -*- 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 @@ -24,7 +26,7 @@ .\" E-mail: philip-fuse@shadowmagic.org.uk .\" .\" -.TH fuse 1 "14th May, 2026" "Version 1.8.0-fusex-0.1" "Emulators" +.TH fuse 1 "4th August, 2026" "Version 1.9.1-fusex-0.1" "Emulators" .\" .\"------------------------------------------------------------------ .\" @@ -97,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 @@ -189,6 +186,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 @@ -481,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. @@ -538,11 +582,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. @@ -593,32 +639,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 @@ -1006,15 +1026,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 @@ -1857,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 @@ -2482,13 +2480,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 @@ -3299,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..." @@ -4342,18 +4333,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 ) @@ -5231,12 +5246,13 @@ 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 `!=', -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. +debugger variables, parentheses, the standard five numeric operations +(`+', `\-', `*', `/' 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 ' diff --git a/memory_pages.c b/memory_pages.c index acc36af09..eabffa24b 100644 --- a/memory_pages.c +++ b/memory_pages.c @@ -233,9 +233,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; @@ -469,7 +469,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 ); } @@ -489,7 +489,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/menu.h b/menu.h index c9c6339b6..1ed7a6074 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 @@ -78,6 +79,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 39686d650..b9a88e5be 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 @@ -134,7 +135,15 @@ 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_GTK +Options/_Filter options..., Item +#endif + +#ifdef UI_SDL +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 diff --git a/peripherals/ula.c b/peripherals/ula.c index 420df962e..d4427a1d8 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 ce11bd562..f0eaeaf50 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? */ diff --git a/psg.c b/psg.c index c00013a01..3052b9c3c 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 @@ -70,7 +71,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 +102,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; } @@ -130,13 +131,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 8241e5d47..571e520d2 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 @@ -26,6 +27,20 @@ #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 + +/* 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; diff --git a/rzx.c b/rzx.c index 3ed05a25d..bd2bc89ef 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/screenshot.c b/screenshot.c index 521c0b2fa..965bf7269 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 @@ -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 @@ -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/screenshot.h b/screenshot.h index c8ed31986..ee3700481 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/settings.dat b/settings.dat index c151f909d..8dddad889 100644 --- a/settings.dat +++ b/settings.dat @@ -1,4 +1,5 @@ # settings.dat: configuration options for Fuse +# Copyright (c) 2026 Fredrick Meunier # Copyright (c) 2002-2018 Philip Kendall, Alistair Cree # This program is free software; you can redistribute it and/or modify @@ -66,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 @@ -110,7 +111,6 @@ volume_uspeech, numeric, 100 joy_prompt, boolean, 1,,, joyprompt joy_kempston, boolean, 0,,, kempston joy_keyboard, boolean, 0,,, keyboard -joystick_1, string, NULL,, 'j' joystick_1_output, numeric, 0 joystick_2_output, numeric, 0 joystick_keyboard_output, numeric, 0 @@ -129,7 +129,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 joy1_xaxis, numeric, 0,,, joy1x joystick_2_fire_1, numeric, 4096 joystick_2_fire_2, numeric, 4096 @@ -212,12 +211,22 @@ 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, string, NULL sdl_fullscreen_mode, string, NULL doublescan_mode, numeric, 1,, 'D', doublescan-mode + +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 start_scaler_mode, string, "2x",, 'g', graphics-filter speccyboot_tap, string, "tap0", diff --git a/slt.c b/slt.c index 418a2a083..ee34bf857 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/snapshot.c b/snapshot.c index 38176b94a..a22d4c3c2 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 @@ -26,6 +27,7 @@ #include "libspectrum.h" #include "fuse.h" +#include "keyboard.h" #include "machine.h" #include "memory_pages.h" #include "module.h" @@ -102,6 +104,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/sound.c b/sound.c index d651da4ba..eaba36a56 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 @@ -63,20 +63,24 @@ 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 +/* 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[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 { @@ -161,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, @@ -170,13 +174,13 @@ 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; 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; @@ -416,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 */ @@ -443,14 +442,14 @@ 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]; + 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; 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; @@ -497,28 +496,26 @@ 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; } } - /* the tone level if no enveloping is being used */ - for( g = 0; g < 3; 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 < 3; 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; 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 ) { @@ -533,13 +530,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 ) ) @@ -547,13 +544,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; } } @@ -622,16 +619,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 ) @@ -666,9 +661,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; } diff --git a/sound/alsasound.c b/sound/alsasound.c index 841b2040e..84d66a14e 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 @@ -384,7 +385,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; } } } diff --git a/sound/sdlsound.c b/sound/sdlsound.c index fb553d5c2..3a6c13935 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 ) ); + strerror( errno ) ); 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__ */ @@ -134,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; } @@ -176,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 ) { diff --git a/svg.c b/svg.c index 2082f1f42..33bd7947e 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, @@ -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; diff --git a/tape.c b/tape.c index 65d7c0c72..5f16b5dac 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 @@ -52,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; @@ -61,6 +76,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; @@ -88,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 ); @@ -133,6 +150,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; @@ -229,8 +247,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 @@ -452,6 +470,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; } @@ -604,7 +627,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 ); @@ -632,6 +655,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 +720,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(); @@ -747,9 +772,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; @@ -859,6 +884,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 +918,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 +930,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 ); @@ -957,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[ 10 * 9 + 1 ]; int offset; size_t i; unsigned long total_pulses; @@ -970,12 +1004,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; @@ -985,7 +1019,8 @@ tape_block_details( char *buffer, size_t length, default: goto normal; } - make_name( name, &data[2] ); + if( libspectrum_zx_string_to_utf8( name, sizeof( name ), &data[2], 10 ) ) + goto normal; snprintf( buffer, length, "%s: \"%s\"", type, name ); @@ -1075,19 +1110,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'; -} diff --git a/timer/timer.c b/timer/timer.c index 66b67ae61..c432adf6b 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 @@ -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; diff --git a/ui/cocoa/cocoadisplay.m b/ui/cocoa/cocoadisplay.m index d28d9e48b..66b003fbd 100755 --- a/ui/cocoa/cocoadisplay.m +++ b/ui/cocoa/cocoadisplay.m @@ -103,7 +103,6 @@ scaler_register_clear(); scaler_register( SCALER_NORMAL ); - scaler_register( SCALER_PALTV ); if( machine_current->timex ) { scaler_register( SCALER_TIMEXTV ); } else { @@ -122,6 +121,9 @@ scaler_register( SCALER_HQ2X ); 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 ) ) { diff --git a/ui/gtk3/Makefile.am b/ui/gtk3/Makefile.am index dd8f5d515..bdbaad08f 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 @@ -29,7 +31,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,7 +43,10 @@ gtkui_resource_files = \ ui/gtk3/icons/tape_inactive.png \ ui/gtk3/icons/tape_marker.png -CLEANFILES += $(ui_gtk_built) +gtkui_built_resource_files = \ + ui/gtk3/menu_data.ui + +CLEANFILES += $(ui_gtk_built) ui/gtk3/options.stamp ui_gtk_files = \ ui/gtk3/binary.c \ @@ -50,6 +54,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 \ @@ -97,16 +102,16 @@ 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) +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 $< diff --git a/ui/gtk3/filter_options.c b/ui/gtk3/filter_options.c new file mode 100644 index 000000000..1b128dacb --- /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 9a7d47b64..ef3a5be5c 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 @@ -23,6 +25,7 @@ #include "config.h" +#include #include #include #include @@ -47,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; @@ -103,20 +102,32 @@ typedef enum { FORMAT_x8b8g8r8 /* GdkRGB */ } colour_format_t; -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 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 */ 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 */ @@ -193,6 +204,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]; @@ -201,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 ); @@ -224,8 +236,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 active scaler */ + float scale = scaler_get_scaling_factor( current_scaler ); if( surface ) cairo_surface_destroy( surface ); surface = @@ -234,6 +246,8 @@ ensure_appropriate_surface( void ) scale * image_width, scale * image_height, scaled_pitch ); + + surface_scaler = current_scaler; } static int @@ -245,10 +259,14 @@ drawing_area_resize( int width, int height, int force_scaler ) if( size > height / DISPLAY_SCREEN_HEIGHT ) size = height / DISPLAY_SCREEN_HEIGHT; - /* If we're the same size as before, no need to do anything else */ - if( size == gtkdisplay_current_size ) return 0; + if( size > MAX_SCALE ) size = MAX_SCALE; + if( size < 1 ) size = 1; + + /* 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_current_size = size; + gtkdisplay_surface_size = size; register_scalers( force_scaler ); @@ -265,7 +283,6 @@ static void register_scalers( int force_scaler ) { scaler_type scaler; - float drawing_area_scale, scaling_factor; scaler_register_clear(); @@ -294,52 +311,39 @@ 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; - drawing_area_scale = (float)gtkdisplay_current_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 ) { - - switch( gtkdisplay_current_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 ); - scaler_select_scaler( scaler ); + /* Activate the scaler without trying to resize the GTK window */ + scaler_activate_scaler( scaler ); } void uidisplay_frame_end( void ) { - if( display_updated ) { - gdk_window_process_updates( gtk_widget_get_window( gtkui_drawing_area ), - FALSE ); - display_updated = 0; + if( scaler_flags & SCALER_FLAGS_FULL_REFRESH ) { + uidisplay_area( 0, 0, image_width, image_height ); } - return; + /* If the user changed the full screen option, apply it now */ + gtkui_fullscreen_apply(); } void uidisplay_area( int x, int y, int w, int h ) { - float scale = (float)gtkdisplay_current_size / image_scale; + float scale = scaler_get_scaling_factor( current_scaler ); int scaled_x, scaled_y, i, yy; libspectrum_dword *palette; @@ -377,16 +381,58 @@ 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, 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 ); + + /* 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 = ( avail_width - (int)( surface_width * s ) ) / 2; + *offset_y = origin_y + ( avail_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; - - display_updated = 1; + 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. */ @@ -395,7 +441,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++; - gtk_widget_queue_draw_area( gtkui_drawing_area, x, y, width, height ); + /* Map the surface onto the drawing area */ + get_surface_placement( &scale, &offset_x, &offset_y ); + + /* 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 @@ -414,6 +469,8 @@ uidisplay_hotswap_gfx_mode( void ) int uidisplay_end( void ) { + cancel_pending_resize(); + return 0; } @@ -507,27 +564,85 @@ 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 offset_x, offset_y; + double scale; + /* Create a new surface for this gfx mode */ if( !surface ) ensure_appropriate_surface(); - /* Repaint the drawing area */ - cairo_set_source_surface( cr, surface, 0, 0 ); - cairo_set_operator( cr, CAIRO_OPERATOR_SOURCE ); + /* 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 */ + cairo_set_source_rgb( cr, 0, 0, 0 ); cairo_paint( cr ); + /* Repaint the surface on top */ + cairo_translate( cr, offset_x, offset_y ); + cairo_scale( cr, scale, scale ); + cairo_set_source_surface( cr, surface, 0, 0 ); + + /* 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; } +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; } @@ -538,14 +653,10 @@ 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 | GDK_HINT_MAX_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] */ @@ -559,37 +670,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.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; - geometry.height_inc = DISPLAY_SCREEN_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, @@ -605,9 +687,23 @@ 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 ); /* 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 bf4357d44..31925fa66 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,6 +69,8 @@ int gtkui_picture( const char *filename, int border ); extern void gtkui_popup_menu(void); +void gtkui_fullscreen_apply( void ); + GtkAccelGroup* gtkstock_add_accel_group( GtkWidget *widget ); /* Set modifier=0 to use the first default accel key. @@ -120,6 +123,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/gtkjoystick.c b/ui/gtk3/gtkjoystick.c index 751a13caf..6d745585b 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 @@ -36,15 +37,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 { @@ -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 diff --git a/ui/gtk3/gtkkeyboard.c b/ui/gtk3/gtkkeyboard.c index da7567ea5..bbde51ab6 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 @@ -82,8 +83,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 ); + } + } 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 f68125e1c..52b2a5963 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 @@ -42,6 +43,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 +161,96 @@ 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; + 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 */ + if( settings_current.full_screen ) { + 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 +259,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 +308,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 +342,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 +363,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 a4b2ded39..a5cf0271f 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 @@ -62,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; @@ -91,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 ); @@ -109,40 +118,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 @@ -152,9 +164,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() ); @@ -177,6 +186,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", @@ -229,6 +240,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; @@ -370,6 +389,66 @@ gtkui_gain_focus( GtkWidget *widget GCC_UNUSED, return TRUE; } +void +gtkui_fullscreen_apply( void ) +{ + GdkWindow *window = gtk_widget_get_window( gtkui_window ); + int state; + + /* 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, @@ -983,3 +1062,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 ); +} diff --git a/ui/options.dat b/ui/options.dat index fe6f6ac27..7ef2da3c5 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 @@ -62,11 +63,12 @@ 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 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 diff --git a/ui/scaler/Makefile.am b/ui/scaler/Makefile.am index b8784c8ae..a7bfd764d 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 @@ -20,7 +21,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,7 +40,10 @@ 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 \ @@ -48,4 +53,5 @@ EXTRA_DIST += \ 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 e28e1707e..bd7603f88 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 @@ -70,10 +70,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_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 ); @@ -81,17 +79,17 @@ static void expand_dotmatrix( int *x, int *y, int *w, int *h, in the same order as scaler.h:scaler_type */ static struct scaler_info available_scalers[] = { - { "Timex Half (smoothed)", "half", SCALER_FLAGS_NONE, SCALE_FACTOR_HALF, + { "Timex Half (smoothed)", "half", SCALER_FLAGS_NONE, SCALE_FACTOR_HALF, scaler_Half_16, SCALER32( scaler_Half ), NULL }, { "Timex Half (skipping)", "halfskip", SCALER_FLAGS_NONE, SCALE_FACTOR_HALF, scaler_HalfSkip_16, SCALER32( scaler_HalfSkip ), NULL }, - { "Normal", "normal", SCALER_FLAGS_NONE, SCALE_FACTOR_ONE, + { "Normal", "normal", SCALER_FLAGS_NONE, SCALE_FACTOR_ONE, scaler_Normal1x_16, SCALER32( scaler_Normal1x ), NULL }, - { "Double size", "2x", SCALER_FLAGS_NONE, SCALE_FACTOR_TWO, + { "Double size", "2x", SCALER_FLAGS_NONE, SCALE_FACTOR_TWO, scaler_Normal2x_16, SCALER32( scaler_Normal2x ), NULL }, - { "Triple size", "3x", SCALER_FLAGS_NONE, SCALE_FACTOR_THREE, + { "Triple size", "3x", SCALER_FLAGS_NONE, SCALE_FACTOR_THREE, scaler_Normal3x_16, SCALER32( scaler_Normal3x ), NULL }, - { "Quadruple size", "4x", SCALER_FLAGS_NONE, SCALE_FACTOR_FOUR, + { "Quadruple size", "4x", SCALER_FLAGS_NONE, SCALE_FACTOR_FOUR, scaler_Normal4x_16, SCALER32( scaler_Normal4x ), NULL }, { "2xSaI", "2xsai", SCALER_FLAGS_EXPAND, SCALE_FACTOR_TWO, scaler_2xSaI_16, SCALER32( scaler_2xSaI ), expand_sai }, @@ -117,20 +115,48 @@ static struct scaler_info available_scalers[] = { scaler_Timex1_5x_16, SCALER32( scaler_Timex1_5x ), NULL }, { "Timex 2x", "timex2x", SCALER_FLAGS_NONE, SCALE_FACTOR_TWO, scaler_Normal2x_16, SCALER32( scaler_Normal2x ), NULL }, - { "PAL TV", "paltv", SCALER_FLAGS_EXPAND, SCALE_FACTOR_ONE, - scaler_PalTV_16, SCALER32( scaler_PalTV ), expand_pal1 }, { "PAL TV 2x", "paltv2x", SCALER_FLAGS_EXPAND, SCALE_FACTOR_TWO, - scaler_PalTV2x_16, SCALER32( scaler_PalTV2x ), expand_pal }, + scaler_PalTV2x_16, SCALER32( scaler_PalTV2x ), + expand_full_width_and_1_line }, { "PAL TV 3x", "paltv3x", SCALER_FLAGS_EXPAND, SCALE_FACTOR_THREE, - scaler_PalTV3x_16, SCALER32( scaler_PalTV3x ), expand_pal }, + scaler_PalTV3x_16, SCALER32( scaler_PalTV3x ), + expand_full_width_and_1_line }, { "PAL TV 4x", "paltv4x", SCALER_FLAGS_EXPAND, SCALE_FACTOR_FOUR, - scaler_PalTV4x_16, SCALER32( scaler_PalTV4x ), expand_pal }, + scaler_PalTV4x_16, SCALER32( scaler_PalTV4x ), + expand_full_width_and_1_line }, { "HQ 2x", "hq2x", SCALER_FLAGS_EXPAND, SCALE_FACTOR_TWO, scaler_HQ2x_16, SCALER32( scaler_HQ2x ), expand_1 }, { "HQ 3x", "hq3x", SCALER_FLAGS_EXPAND, SCALE_FACTOR_THREE, scaler_HQ3x_16, SCALER32( scaler_HQ3x ), expand_1 }, { "HQ 4x", "hq4x", SCALER_FLAGS_EXPAND, SCALE_FACTOR_FOUR, scaler_HQ4x_16, SCALER32( scaler_HQ4x ), expand_1 }, + { "NTSC TV 2x", "ntsctv2x", SCALER_FLAGS_FULL_REFRESH, SCALE_FACTOR_TWO, + scaler_blargg_NTSC_2x_16, SCALER32( scaler_blargg_NTSC_2x ), NULL }, + { "NTSC TV 3x", "ntsctv3x", SCALER_FLAGS_FULL_REFRESH, SCALE_FACTOR_THREE, + scaler_blargg_NTSC_3x_16, SCALER32( scaler_blargg_NTSC_3x ), NULL }, + { "NTSC TV 4x", "ntsctv4x", SCALER_FLAGS_FULL_REFRESH, SCALE_FACTOR_FOUR, + scaler_blargg_NTSC_4x_16, SCALER32( scaler_blargg_NTSC_4x ), 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; @@ -168,6 +194,24 @@ 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 ) { @@ -187,6 +231,12 @@ scaler_get_type( 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 ) ) { return i; @@ -283,6 +333,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 */ @@ -311,21 +396,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 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 and the following line. */ static void -expand_pal( int *x, int *y, int *w, int *h, int image_width, int image_height ) +expand_full_width_and_1_line( int *x, int *y, int *w, int *h, + int image_width, int image_height ) { - (*x)-=1; (*w)+=2; + *x = 0; + *w = image_width; + (*h)++; clip( x, y, w, h, image_width, image_height ); } diff --git a/ui/scaler/scaler.h b/ui/scaler/scaler.h index ff61f4764..377113cb3 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) @@ -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 enum scale_factor_t { @@ -86,6 +89,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 ); @@ -102,7 +112,8 @@ 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( 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 a9245e77f..e95ade2ca 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 @@ -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 3ad209cfa..fd3e38d5e 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 @@ -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,39 @@ 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; + int pal_delay; + 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 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, + libspectrum_byte *dstPtr, + libspectrum_dword dstPitch, + int width, int height ); void FUNCTION( scaler_PalTV2x )( const libspectrum_byte *srcPtr, @@ -1623,129 +1544,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; - -/* - 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; + static const composite_scaler_config config = { + 2, 0, 0, 1, 7.0 / 6.0, { 1.0, 0.75, 0.0, 0.0 } + }; -/* 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 +1559,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; + static const composite_scaler_config config = { + 3, 0, 0, 1, ( 7.0 / 9.0 ) * 1.005, { 0.8, 1.0, 0.5, 0.0 } + }; - 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; - - 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 +1574,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; + static const composite_scaler_config config = { + 4, 0, 0, 1, 7.0 / 12.0, { 0.9, 1.0, 0.75, 0.5 } + }; -/* - 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 -/* - 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 +1893,321 @@ 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 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, + 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]; + 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 ) ); + + burst_phase = config->cycle_phase ? + ( 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 ? + ( 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]; + libspectrum_signed_dword y; + libspectrum_signed_dword u; + libspectrum_signed_dword v; + + 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 ] ); + + 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; + } + + 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, 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, 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, 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 000000000..9c506f251 --- /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 000000000..f98b61876 --- /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 000000000..d6c2877f5 --- /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 000000000..c4275d30e --- /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 52ef79131..2149f95c8 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 @@ -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/sdl/sdljoystick.c b/ui/sdl/sdljoystick.c index e7439b0fc..db2a05924 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 @@ -24,6 +24,7 @@ #include "config.h" + #include #include diff --git a/ui/sdl/sdlui.c b/ui/sdl/sdlui.c index 050db5d90..56cd83238 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 @@ -106,7 +106,7 @@ ui_event( void ) } break; -#if defined USE_JOYSTICK && !defined HAVE_JSW_H +#ifdef USE_JOYSTICK case SDL_JOYBUTTONDOWN: sdljoystick_buttonpress( &(event.jbutton) ); @@ -121,7 +121,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_display.c b/ui/sdl2/sdl2_display.c index e4b55face..d9e6fca57 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_activate_scaler( current_scaler ) ) @@ -246,8 +248,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; } @@ -290,6 +291,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 ); @@ -804,8 +810,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; } @@ -839,7 +844,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_joystick.c b/ui/sdl2/sdl2_joystick.c index 5e4e7e677..abcdc5249 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 e1a80191e..ee8519786 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_keyboard.c b/ui/sdl2/sdl2_keyboard.c index 03dd8b8d3..9da67b316 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 ) { @@ -186,19 +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 = sdl2keyboard_physical_map( keyevent ); - if( spectrum_keysym == INPUT_KEY_NONE ) 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 ); } diff --git a/ui/sdl2/sdl2_scaler_state.c b/ui/sdl2/sdl2_scaler_state.c index 70f008e2e..114b09d23 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/sdl2/sdl2_ui.c b/ui/sdl2/sdl2_ui.c index 3915b7a83..151eb4e6a 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 8b0489351..eed6ee47d 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/wii/wiidisplay.c b/ui/wii/wiidisplay.c index 940da2e07..d09a3bd46 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 @@ -215,7 +216,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 +246,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 84bb3bd86..622cb38b7 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 @@ -51,10 +52,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 +126,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 +185,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 +216,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 +277,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 +307,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 +353,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 +366,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/win32/win32joystick.c b/ui/win32/win32joystick.c index 1fd002ea5..b7a317217 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 @@ -39,11 +40,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 +165,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 dd91412a1..d3033c029 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 @@ -261,7 +262,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 ); @@ -287,7 +288,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/xdisplay.c b/ui/xlib/xdisplay.c index 60c0f03e1..7318c69ca 100644 --- a/ui/xlib/xdisplay.c +++ b/ui/xlib/xdisplay.c @@ -1,7 +1,8 @@ /* xdisplay.c: Routines for dealing with drawing the Speccy's screen via Xlib - Copyright (c) 2000-2021 Philip Kendall, Darren Salt, Gergely Szász + 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� + 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 @@ -613,7 +614,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 +649,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 +670,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 +695,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 +734,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 +773,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 +885,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/ui/xlib/xjoystick.c b/ui/xlib/xjoystick.c index b9c327291..0e82d8adc 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 @@ -26,11 +27,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" diff --git a/unittests/Makefile.am b/unittests/Makefile.am index 40e7c3c0a..0acf3448a 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/displaytest.c b/unittests/displaytest.c index 0e4654a7d..4578ce4b7 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 ); @@ -486,6 +486,27 @@ no_write_if_modified_area_ahead_of_critical_region( void ) return 0; } +static int +attribute_write_marks_correct_cell( void ) +{ + int y; + + /* Arrange: the final attribute byte is column 31, character row 23. */ + + /* Act */ + display_dirty_sinclair( 0x1aff ); + + /* Assert: mark column 31 in each of the cell's eight pixel rows only. */ + for( y = 0; y < DISPLAY_HEIGHT; y++ ) { + libspectrum_dword expected = + 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 @@ -692,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 ); @@ -715,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; @@ -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 }, diff --git a/unittests/palcompositetest.c b/unittests/palcompositetest.c new file mode 100644 index 000000000..380b326b6 --- /dev/null +++ b/unittests/palcompositetest.c @@ -0,0 +1,509 @@ +/* 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 ); +} + +#ifdef main +/* SDL headers redefine main on Windows, but this test needs a normal entry point. */ +#undef main +#endif + +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 000000000..8197256e4 --- /dev/null +++ b/unittests/scalerexpandtest.c @@ -0,0 +1,225 @@ +/* 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; + + /* 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; +} + +#ifdef main +/* SDL headers redefine main on Windows, but this test needs a normal entry point. */ +#undef main +#endif + +int +main( void ) +{ + int r = 0; + + r += scalerexpandtest_paltv(); + r += scalerexpandtest_advmame(); + r += scalerexpandtest_sai(); + r += scalerexpandtest_dotmatrix(); + r += scalerexpandtest_ntsctv(); + + return r; +} diff --git a/unittests/sdl2joysticktest.c b/unittests/sdl2joysticktest.c index ff15097b6..1d69b2d59 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 } }; diff --git a/unittests/sdl2scalerstatetest.c b/unittests/sdl2scalerstatetest.c index 931991fc2..0dd17f054 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; @@ -46,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; } @@ -438,6 +440,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 ) { @@ -460,6 +523,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 { @@ -472,17 +698,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", @@ -495,6 +733,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 } }; diff --git a/unittests/unittests.c b/unittests/unittests.c index a67c4e961..a99249008 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 @@ -30,6 +31,7 @@ #include "debugger/debugger.h" #include "fuse.h" +#include "keyboard.h" #include "machine.h" #include "mempool.h" #include "periph.h" @@ -51,8 +53,10 @@ #include "peripherals/ula.h" #include "peripherals/usource.h" #include "settings.h" +#include "snapshot.h" #include "bitmap.h" #include "rectangle.h" +#include "ui/scaler/scaler.h" #include "unittests.h" #include "utils.h" @@ -327,6 +331,84 @@ 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 +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 ) { @@ -441,6 +523,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; } @@ -1082,6 +1187,168 @@ 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; +} + +/* 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 ) { @@ -1090,12 +1357,18 @@ unittests_run( void ) r += contention_test(); 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(); r += paging_test(); r += debugger_disassemble_unittest(); + 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); diff --git a/update-release.py b/update-release.py new file mode 100755 index 000000000..0360b568f --- /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() diff --git a/z80/coretest.c b/z80/coretest.c index 0750d9deb..829b25533 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 5851c7cad..93c8b0dfb 100644 --- a/z80/z80.c +++ b/z80/z80.c @@ -324,7 +324,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 25ad799ff..406bb1abf 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 39dc1bbe2..0d6d2bf06 100644 --- a/z80/z80_macros.h +++ b/z80/z80_macros.h @@ -81,11 +81,15 @@ #define CLOCKH z80.clockh #define CLOCKL z80.clockl +/* 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