From 423f9925838b4ae3fbc43c82814779b73a151173 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sat, 17 Oct 2026 15:39:14 -0500 Subject: [PATCH] More bugs fixed. Performance improvements. More tilemap features. --- LICENSES.md | 45 +- Makefile | 9 +- README.md | 14 +- docs/ApiCore.adoc | 4 +- docs/ApiDebug.adoc | 3 +- docs/ApiFile.adoc | 4 +- docs/ApiSprite.adoc | 7 +- docs/ApiTile.adoc | 141 +- docs/Assets.adoc | 77 +- docs/BuildIntegration.adoc | 60 +- docs/GettingStarted.adoc | 29 +- docs/Portability.adoc | 56 +- docs/ProgramLayout.adoc | 16 +- examples/adventure/adventure.c | 34 +- examples/adventure2/adventure2.c | 38 +- examples/scroll/scroll.c | 344 ++ examples/serloss/serloss.c | 12 +- examples/sertest/sertest.c | 9 +- examples/tilebank/tilebank.c | 36 +- examples/uber/uber.c | 861 +++- include/joey/platform.h | 79 +- include/joey/sprite.h | 7 +- include/joey/tile.h | 43 + install.sh | 6 +- installer/components/amigaGcc.sh | 11 +- installer/components/macosSdk.sh | 57 + installer/components/sdl3Macos.sh | 116 + installer/components/zig.sh | 44 + installer/distroPackages.txt | 4 + .../amigaGcc/0001-bbb-moveq-mask.patch | 21 + installer/patches/amigaGcc/README.md | 14 + joey-run | 20 +- make/amiga.mk | 34 +- make/atarist.mk | 17 +- make/be.mk | 9 +- make/beos.mk | 4 +- make/common.mk | 30 +- make/dvx.mk | 14 +- make/examples.mk | 34 +- make/haiku32.mk | 4 +- make/hosted.mk | 35 +- make/iigs.mk | 18 +- make/macos.mk | 65 + make/programs.mk | 12 +- make/support/iigs/clang-build.sh | 35 +- make/support/iigs/clang-smoke.sh | 14 +- make/support/macos/cc.sh | 106 + make/support/macos/packApp.sh | 64 + make/support/win16/owcc.sh | 13 +- make/tools.mk | 6 +- make/win16.mk | 5 + make/x68000.mk | 9 +- scripts/bench-amiga.sh | 15 +- scripts/bench-atarist.sh | 13 +- scripts/bench-dvx.sh | 2 +- scripts/bench-x68000.sh | 7 +- scripts/check-blank.sh | 2 +- scripts/check-dirtymark.sh | 2 +- scripts/check-icons.sh | 21 +- scripts/check-macos-apps.sh | 98 + scripts/check-tiled.sh | 34 + scripts/check-tilelayer.sh | 32 + scripts/lib/dvxGate.sh | 12 +- scripts/lib/joeyEnv.sh | 9 +- scripts/lib/serialLoopPeer.py | 71 +- scripts/make-iigs-disk.sh | 2 +- scripts/profile-iigs.sh | 234 + scripts/profile-x68000.sh | 97 +- scripts/verify-all.sh | 48 +- scripts/verify-amiga-display.sh | 39 + scripts/verify-atarist-display.sh | 56 + scripts/verify-be-display.sh | 2 +- scripts/verify-dos-audio.sh | 14 +- scripts/verify-dos-mouse.sh | 155 + scripts/verify-dvx-display.sh | 2 +- scripts/verify-dvx-fullscreen.sh | 4 +- scripts/verify-iigs-keys.sh | 120 + scripts/verify-win16-display.sh | 2 +- scripts/verify-win16-serial-fast.sh | 70 + scripts/verify-win32-display.sh | 2 +- scripts/verify-x68000-display.sh | 37 + scripts/verify-x68000-golden.sh | 2 +- scripts/verify-x68000-scb.sh | 18 +- scripts/verify-x68000-serial-mouse.sh | 2 +- scripts/verify-x68000-serial-scb.sh | 2 +- src/amiga/amigaCustom.h | 13 + src/amiga/amigaTileLayer.c | 199 + src/amiga/audio.c | 36 +- src/amiga/hal.c | 502 +- src/amiga/input.c | 71 +- src/amiga/libinit.c | 22 +- src/amiga/presentScan.s | 25 +- src/amiga/serial.c | 30 +- src/atarist/audio.c | 124 +- src/atarist/audio_isr.s | 28 +- src/atarist/circle.s | 18 +- src/atarist/copyRect.s | 6 +- src/atarist/fillCircle.s | 22 +- src/atarist/hal.c | 755 +-- src/atarist/input.c | 152 +- src/atarist/lineSpan.s | 445 +- src/atarist/presentSlam.s | 78 +- src/atarist/rasterIsr.s | 16 +- src/atarist/serial.c | 12 +- src/atarist/spriteAsm.s | 325 -- src/atarist/spriteWalk.s | 42 +- src/atarist/stAltLayer.c | 2 +- src/atarist/stAltLayer.h | 2 +- src/atarist/stGlyph.s | 149 + src/atarist/stTile.h | 205 +- src/atarist/stTileLayer.c | 97 + src/atarist/stTileLayer.h | 28 + src/atarist/stTileLayerBlit.s | 246 + src/atarist/ste/blitter.c | 81 +- src/atarist/ste/machine.c | 70 +- src/atarist/ste/pads.c | 81 - src/atarist/ste/steBlitter.h | 9 +- src/atarist/ste/steMachine.h | 10 +- src/atarist/ste/stePads.h | 65 +- src/atarist/superExit.s | 6 +- src/atarist/tickIsr.s | 12 +- src/atarist/tileMap.s | 26 +- src/blank/blank.c | 13 +- src/codegen/spriteCompile.c | 28 +- src/codegen/spriteEmitter.h | 24 +- src/codegen/spriteStage.c | 30 +- src/codegen/spriteStage.h | 2 +- src/core/assetLoad.c | 18 +- src/core/audio.c | 28 +- src/core/audioInternal.h | 4 +- src/core/audioSfxMix.c | 18 +- src/core/audioSfxMixInternal.h | 14 +- src/core/codegenArena.c | 50 +- src/core/codegenArenaInternal.h | 34 +- src/core/debug.c | 22 +- src/core/draw.c | 58 +- src/core/fileRoot.h | 7 +- src/core/init.c | 42 +- src/core/input.c | 232 +- src/core/inputInternal.h | 59 +- src/core/modPlayer.c | 34 +- src/core/modPlayer.h | 22 +- src/core/modStopMarker.c | 68 +- src/core/modStopMarker.h | 2 +- src/core/music.c | 160 +- src/core/musicInternal.h | 2 +- src/core/palette.c | 12 +- src/core/port.h | 305 +- src/core/present.c | 6 +- src/core/psgSynth.c | 12 +- src/core/psgSynth.h | 14 +- src/core/scb.c | 8 +- src/core/serial.c | 18 +- src/core/sprite.c | 166 +- src/core/spriteDispatch.h | 35 +- src/core/spriteInternal.h | 42 +- src/core/spritePrecompiled.h | 10 +- src/core/surface.c | 290 +- src/core/surfaceInternal.h | 115 +- src/core/tile.c | 122 +- src/core/tileLayer.c | 300 ++ src/core/tileLayerInternal.h | 74 + src/dos/audio.c | 272 +- src/dos/dosTimer.h | 12 +- src/dos/hal.c | 158 +- src/dos/input.c | 320 +- src/dos/serial.c | 8 +- src/dvx/audio.c | 18 +- src/dvx/dvxApp.c | 2 +- src/dvx/dvxPort.h | 34 +- src/dvx/hal.c | 128 +- src/dvx/input.c | 68 +- src/dvx/program.c | 30 +- src/dvx/serial.c | 4 +- src/generic/genericDraw.c | 131 +- src/generic/genericPort.c | 54 +- src/generic/genericSurface.c | 28 +- src/generic/genericTileLayer.c | 150 + src/generic/spriteEmitStub.c | 4 +- src/hosted/audio.c | 54 +- src/hosted/be/beBackend.cpp | 146 +- src/hosted/hal.c | 88 +- src/hosted/hosted.h | 97 +- src/hosted/input.c | 40 +- src/hosted/posix/posixSerial.c | 76 +- src/hosted/psp/pspKeyboard.c | 36 +- src/hosted/psp/pspKeyboard.h | 10 +- src/hosted/sdl3/sdl3Backend.c | 193 +- src/hosted/win16/spriteMergeRows.asm | 131 + src/hosted/win16/win16Backend.c | 120 +- src/hosted/win16/win16SpriteMerge.c | 61 - src/hosted/win32/win32Backend.c | 94 +- src/iigs/audio_full.c | 329 +- src/iigs/hal.c | 249 +- src/iigs/iigsLayerBlit.s | 393 ++ src/iigs/iigsTileLayer.c | 110 + src/iigs/input.c | 161 +- src/iigs/joeyDraw.s | 4512 +++++++++-------- src/iigs/peislam.s | 30 +- src/iigs/serial.c | 186 +- src/iigs/serialIrq.s | 160 +- src/iigs/sfxCopy.s | 8 +- src/iigs/spriteEmitIigs.c | 51 +- src/iigs/spriteInterp.s | 18 +- src/m68k/m68kAsm.h | 16 + src/m68k/m68kCSym.inc | 25 + src/m68k/modMix.s | 10 +- src/m68k/planarFill.s | 202 +- src/m68k/planarHash.c | 109 + src/m68k/planarLayerBlit.s | 248 + src/m68k/planarPlot.s | 26 +- src/m68k/planarShape.s | 173 + src/m68k/planarSprite.c | 16 +- src/m68k/planarSpriteWalk.s | 142 +- src/m68k/planarTile.h | 196 +- src/m68k/planarTileLayer.c | 52 + src/m68k/planarTileLayer.h | 28 + src/m68k/sfxMix.s | 10 +- src/m68k/spriteC2p.c | 22 + src/m68k/spriteC2p.h | 60 +- src/m68k/spriteEmitInterleaved68k.c | 994 ++-- src/m68k/spriteEmitPlanar68k.c | 617 ++- src/m68k/stageBands.s | 100 +- src/x68000/audio.c | 8 +- src/x68000/audioPcm.c | 83 +- src/x68000/hal.c | 361 +- src/x68000/halPlanar.c | 528 +- src/x68000/serial.c | 108 +- src/x68000/x68kPlanar.h | 2 +- src/x68000/x68kPresent.s | 109 +- src/x68000/x68kRasterIsr.s | 42 +- src/x68000/x68kSccRxIsr.s | 46 +- src/x68000/x68kTickIsr.s | 20 +- src/x68000/x68kTile.h | 7 + src/x68000/x68kTileLayer.c | 39 + src/x68000/x68kVdispIsr.s | 22 +- src/x86/spriteEmitX86.c | 6 +- src/x86/spriteMergeRows.s | 10 +- tests/goldens/sfxmix-baseline.txt | 2 +- tests/goldens/uber/amiga.txt | 7 +- tests/goldens/uber/atarist.txt | 7 +- tests/goldens/uber/dos.txt | 7 +- tests/goldens/uber/iigs.txt | 7 +- tests/host/dirtyMarkHost.c | 181 +- tests/host/inputHost.c | 74 +- tests/host/millisHost.c | 55 +- tests/host/modPlayerHost.c | 84 +- tests/host/serialHost.c | 12 +- tests/host/sfxMixHost.c | 32 +- tests/host/tileLayerHost.c | 240 + tests/host/tiledCheck.py | 229 + tools/iconBake.py | 162 +- tools/modloopend/modloopend.c | 2 +- tools/spritebake/spritebake.c | 4 +- tools/tiled2joey.py | 327 ++ 255 files changed, 16289 insertions(+), 8839 deletions(-) create mode 100644 examples/scroll/scroll.c create mode 100644 installer/components/macosSdk.sh create mode 100644 installer/components/sdl3Macos.sh create mode 100644 installer/components/zig.sh create mode 100644 installer/patches/amigaGcc/0001-bbb-moveq-mask.patch create mode 100644 installer/patches/amigaGcc/README.md create mode 100644 make/macos.mk create mode 100755 make/support/macos/cc.sh create mode 100755 make/support/macos/packApp.sh create mode 100755 scripts/check-macos-apps.sh create mode 100755 scripts/check-tiled.sh create mode 100755 scripts/check-tilelayer.sh create mode 100755 scripts/profile-iigs.sh create mode 100755 scripts/verify-amiga-display.sh create mode 100755 scripts/verify-atarist-display.sh create mode 100755 scripts/verify-dos-mouse.sh create mode 100755 scripts/verify-iigs-keys.sh create mode 100755 scripts/verify-win16-serial-fast.sh create mode 100755 scripts/verify-x68000-display.sh create mode 100644 src/amiga/amigaTileLayer.c delete mode 100644 src/atarist/spriteAsm.s create mode 100644 src/atarist/stGlyph.s create mode 100644 src/atarist/stTileLayer.c create mode 100644 src/atarist/stTileLayer.h create mode 100644 src/atarist/stTileLayerBlit.s delete mode 100644 src/atarist/ste/pads.c create mode 100644 src/core/tileLayer.c create mode 100644 src/core/tileLayerInternal.h create mode 100644 src/generic/genericTileLayer.c create mode 100644 src/hosted/win16/spriteMergeRows.asm delete mode 100644 src/hosted/win16/win16SpriteMerge.c create mode 100644 src/iigs/iigsLayerBlit.s create mode 100644 src/iigs/iigsTileLayer.c create mode 100644 src/m68k/m68kAsm.h create mode 100644 src/m68k/m68kCSym.inc create mode 100644 src/m68k/planarHash.c create mode 100644 src/m68k/planarLayerBlit.s create mode 100644 src/m68k/planarShape.s create mode 100644 src/m68k/planarTileLayer.c create mode 100644 src/m68k/planarTileLayer.h create mode 100644 src/m68k/spriteC2p.c create mode 100644 src/x68000/x68kTileLayer.c create mode 100644 tests/host/tileLayerHost.c create mode 100755 tests/host/tiledCheck.py create mode 100755 tools/tiled2joey.py diff --git a/LICENSES.md b/LICENSES.md index e22b0ac..87889d8 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -23,16 +23,17 @@ most. ### SDL 3.4.18 -- **Used on:** SDL3, PSP, web +- **Used on:** SDL3, PSP, web, macOS - **URL:** - **License:** zlib - **Role:** window, input, and audio for the hosted SDL3 code. Built as a - static library for the host, for the PSP with pspdev, and for Emscripten + static library for the host, for the PSP with pspdev, for Emscripten (with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under - SDL's zlib license), and linked into each program. + SDL's zlib license), and for macOS with zig (universal), and linked into + each program. - **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`, - `toolchains/opt/sdl3Web/` (installer components `sdl3`, `sdl3Psp`, - `sdl3Web`) + `toolchains/opt/sdl3Web/`, `toolchains/opt/sdl3Macos/` (installer + components `sdl3`, `sdl3Psp`, `sdl3Web`, `sdl3Macos`) ### NinjaTrackerPlus (NTP) @@ -52,7 +53,7 @@ most. ### PTPlayer 6.4 - **Used on:** Amiga; and, translated to C, DOS, Atari ST, X68000, SDL3, - PSP, web, Win16, Win32, Haiku, BeOS + PSP, web, macOS, Win16, Win32, Haiku, BeOS - **URL:** (author Frank Wille, ) - **License:** Public Domain (the archive's `license` file: the Unlicense) @@ -109,6 +110,8 @@ them, in brief (the entries below have the detail): terms Unknown; see the BeOS entry under the never-downloaded media below). - **Haiku:** the system's own shared libraries, at run time (MIT). +- **macOS:** the system's own libraries and frameworks, at run time + (Apple's); zig's compiler runtime, linked in (MIT). - **Every gcc port:** libgcc, under the GCC Runtime Library Exception, which puts no conditions on the programs it is linked into. @@ -303,7 +306,7 @@ above. every Win16 program -- must, by section 2.2(d), include a prominent notice, in the program and its documentation, that the source is available under the license and where to get it. Every Win16 program - carries it (`hostWin16OwplNotice`, src/hosted/win16/win16Backend.c), as + carries it (`jlHostWin16OwplNotice`, src/hosted/win16/win16Backend.c), as do KPJOY.DRV and KPCOMM.DRV, as a string never shown; docs/Portability.adoc ("Shipping") gives the same line for a program's documentation. - **Role:** the Win386 target compiles the win16 port's 32-bit programs @@ -324,6 +327,34 @@ above. - **Installed at:** `toolchains/opt/llvmMingw/` (installer component `llvmMingw`) +### zig 0.15.2 + +- **Used on:** macOS +- **URL:** +- **License:** MIT. The clang, LLVM, and lld it is built from: Apache + License 2.0 with LLVM Exceptions. The macOS C library headers and + libSystem stubs it carries: Apple's, from Apple's open-source releases + (the Apple Public Source License 2.0, among others). +- **Role:** the macos port's C compiler and Mach-O linker; it signs the + Apple silicon programs ad hoc as it links them. +- **Installed at:** `toolchains/opt/zig/` (installer component `zig`) + +### Apple MacOSX 15.5 SDK + +- **Used on:** macOS +- **URL:** Apple ships it only inside Xcode; the installer takes it from + the mirror osxcross users share, + , pinned to its SHA-256 +- **License:** Apple's Xcode and Apple SDKs Agreement. None of it is + shipped: it gives the build the frameworks' headers and stub libraries to + link against, and a program uses the Mac's own frameworks when it runs. +- **Role:** the frameworks (Cocoa, CoreAudio, IOKit, and the rest) the + macos port's programs and its SDL 3 link against. The host's LLVM tools + (`llvm-lipo`, `llvm-libtool-darwin`; Apache License 2.0 with LLVM + Exceptions) join and archive what is built for both Mac processors. +- **Installed at:** `toolchains/opt/macosSdk/` (installer component + `macosSdk`) + ### pspdev v20261001 - **Used on:** PSP diff --git a/Makefile b/Makefile index 9297a9d..d8e5bb4 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ # make dvx builds DVX (JoeyLib programs as DVX desktop programs) # make sdl3 builds SDL3 (JoeyLib programs as native windowed programs) # make web builds the web port (JoeyLib programs as web pages) +# make macos builds macOS (JoeyLib programs as universal Mac apps) # make win32 builds Win32 (Windows 95 through today, as .EXE programs) # make win16 builds Win16 (Windows 3.1 on a 386, as .EXE programs) # make psp builds the PSP port (JoeyLib programs as PSP homebrew EBOOT.PBPs) @@ -39,7 +40,7 @@ include $(dir $(firstword $(MAKEFILE_LIST)))make/common.mk ALL_TARGETS := $(JOEY_PORTS_INSTALLED) -.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos blank-check tools clean help status verify verify-audio verify-conform verify-serial docs +.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos blank-check tools clean help status verify verify-audio verify-conform verify-serial docs all: $(ALL_TARGETS) tools ifeq ($(ALL_TARGETS),) @@ -83,6 +84,9 @@ sdl3: web: @$(MAKE) -f $(REPO_DIR)/make/web.mk +macos: + @$(MAKE) -f $(REPO_DIR)/make/macos.mk + win32: @$(MAKE) -f $(REPO_DIR)/make/win32.mk @@ -154,6 +158,7 @@ status: @echo " dvx: $(if $(HAVE_DVX),yes,NO)" @echo " sdl3: $(if $(HAVE_SDL3),yes,NO)" @echo " web: $(if $(HAVE_WEB),yes,NO)" + @echo " macos: $(if $(HAVE_MACOS),yes,NO)" @echo " win32: $(if $(HAVE_WIN32),yes,NO)" @echo " win16: $(if $(HAVE_WIN16),yes,NO)" @echo " psp: $(if $(HAVE_PSP),yes,NO)" @@ -162,7 +167,7 @@ status: @echo " beos: $(if $(HAVE_BEOS),yes,NO)" help: - @echo "Targets: iigs amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos all clean status help" + @echo "Targets: iigs amiga atarist ste dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos all clean status help" @echo " iigs-disk iigs-verify (IIgs MAME visual check)" @echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)" @echo " verify (build every port, then run every gate; PORTS= subset)" diff --git a/README.md b/README.md index 53074d1..8bf92cb 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ and the PSP, from a single codebase: statically; sprites are interpreted) - Web (the SDL3 port compiled to WebAssembly with Emscripten: each program a page, its DATA packed in, saves kept in the browser) +- macOS (the SDL3 port cross-built on Linux with zig against Apple's SDK: + each program a universal app for Apple silicon and Intel Macs, macOS 13 + or newer; built and checked here, but run only on a Mac) - Win32 (Windows 95 through today: the hosted core with a backend written to the Win32 API, built with llvm-mingw against MSVCRT.DLL, tested under Wine) - Win16 (Windows 3.1 on a 386: the hosted core with a backend written to the @@ -66,20 +69,23 @@ hand, `source toolchains/env.sh` sets the same variables. `make` builds `libjoey.a` for every port whose toolchain is installed, plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`, -`sertest`, `save`, `tilebank`, `joy`, `sprite`, `audio`, `uber`, `adventure`, -`adventure2`) for each. The IIgs is the exception: +`sertest`, `save`, `tilebank`, `joy`, `sprite`, `scroll`, `audio`, `uber`, +`adventure`, `adventure2`) for each. The IIgs is the exception: `make iigs` builds the library, and `make iigs-disk` builds its examples and packages them onto a disk image. DVX needs the `dvx` installer component, which unpacks DVX and its SDK from a DVX tree's distribution, `dist/dvx.zip` and `dist/sdk.zip` (`./install.sh --dvx-dir PATH install dvx`; the default -is `../dvxgui`); its examples land in `build/dvx/bin/apps/joeylib//`, +is `../dvxgui`); its examples land in `build/dvx/bin/apps/kpunch//`, laid out as they install in DVX. SDL3 needs the `sdl3` component (`./install.sh install toolchain:sdl3`), which builds SDL 3 from its release source; its examples are native programs in `build/sdl3/bin/`, and running one takes seconds rather than an emulator's minutes. The web port needs the `web` components (`./install.sh install toolchain:web`: Emscripten through emsdk, and SDL 3 built with it); its examples are pages in -`build/web/bin//`. The Win32 port needs `toolchain:win32` (llvm-mingw) +`build/web/bin//`. The macOS port needs `toolchain:macos` (zig, +Apple's macOS SDK, and SDL 3 built with them); its examples are apps, +`build/macos/bin/.app`, which nothing here runs: copy one to a Mac. +The Win32 port needs `toolchain:win32` (llvm-mingw) and, to run its programs, `emulator:win32` (a portable Wine). The Win16 port needs `toolchain:win16` (Open Watcom) and, to run its programs, `emulator:win16` (Windows for Workgroups 3.11 in DOSBox Staging, set up from diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index 7c6d0ae..0dd27e7 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -104,8 +104,8 @@ const char *jlPlatformName(void); *Parameters:* none. *Returns:* a human-readable platform identifier, for example `"Apple IIgs"`, -`"MS-DOS"`, `"DVX"`, `"SDL3"` (the web port is the SDL3 port in a browser) or -`"Windows"`. +`"MS-DOS"`, `"DVX"`, `"SDL3"` (the web port is the SDL3 port in a browser), +`"macOS"`, or `"Windows"`. Library-owned. === jlVersionString diff --git a/docs/ApiDebug.adoc b/docs/ApiDebug.adoc index 8db23f7..8e5ac54 100644 --- a/docs/ApiDebug.adoc +++ b/docs/ApiDebug.adoc @@ -4,7 +4,8 @@ Declared in ``. A crash-tracing logger that writes `joeylog.txt` in the program's folder, beside `DATA/` and `SAVES/` (<<_files>>: on the ST, the current directory; on -DVX, the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). +DVX, the program's settings folder, `CONFIG\APPS\VENDOR\NAME\JOEYLOG.TXT`; +on macOS, its Application Support folder). Debugging a program on an emulated 1988 machine is awkward, and this is usually the fastest way to find out what it did before it stopped doing it. The headless run scripts retrieve the log automatically. The library itself writes diff --git a/docs/ApiFile.adoc b/docs/ApiFile.adoc index 1733926..db78a0b 100644 --- a/docs/ApiFile.adoc +++ b/docs/ApiFile.adoc @@ -25,8 +25,8 @@ goes there too): * *DVX* keeps the two apart, because its programs share one current directory: there `DATA/` is beside the program, in its folder under DVX's `APPS`, and `SAVES/` is in the program's own settings folder, - `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program files so - a reinstall never overwrites it. + `CONFIG\APPS\VENDOR\NAME` (`VENDOR` its maker's, <<_a_project>>), which + DVX keeps apart from the program files so a reinstall never overwrites it. * *The web* keeps them apart too: `DATA/` is packed into the program, and `SAVES/` and the log are in a folder named after the page (`games/hero.html` has `/games/hero/`), its `SAVES/` kept in the browser's diff --git a/docs/ApiSprite.adoc b/docs/ApiSprite.adoc index b5a3d92..4425fe8 100644 --- a/docs/ApiSprite.adoc +++ b/docs/ApiSprite.adoc @@ -42,11 +42,12 @@ That is often not the sprite rectangle: * `x` is rounded down to an even pixel, and `width` grows to match, on every port. -* A compiled save on the Amiga, ST, and X68000 captures the *16-pixel-aligned - region* containing the sprite. +* On the Amiga, ST, and X68000 a save captures the *16-pixel-aligned region* + containing the sprite (or its visible part). * A compiled save on the IIgs captures only the rows that hold an opaque pixel, so `y` can be lower and `height` shorter than the sprite's. -* A save of a sprite partly off the surface captures only the visible part. +* A save of a sprite partly off the surface captures only the visible part + (on the Amiga, ST, and X68000, the 16-pixel-aligned region holding it). It is the recorded region you must not overwrite before restoring. diff --git a/docs/ApiTile.adoc b/docs/ApiTile.adoc index baa99cb..feeb913 100644 --- a/docs/ApiTile.adoc +++ b/docs/ApiTile.adoc @@ -24,7 +24,8 @@ one. Fonts, terrain sets, and spritesheets are all the same thing. |`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface. |`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface. |`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character". -|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone". +|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone"; in a tile layer's map, colour 0. +|`JL_TILE_LAYER_MAX_SIDE` |4096 |Largest tile layer map side, in tiles. |=== [source,c] @@ -333,3 +334,141 @@ Any map index other than `JL_TILE_MAP_SKIP` must be a valid tileset index. The batch does not know the tileset length and does *not* range-check it -- an out-of-range index reads past the end of your array. ==== + +=== Scrolling Tile Layers + +A tile layer is a map of tile indices, up to `JL_TILE_LAYER_MAX_SIDE` tiles a +side, seen through a band of the stage: whole tile rows, the full width (rows +outside the band are yours, for a status bar or a score). `jlTileLayerDraw` +fills the band with the map as seen from a pixel origin, to the pixel both +ways, so moving the origin a little each frame scrolls it. The map wraps at its +edges: an origin past them, or a map smaller than the view, repeats it. + +The layer keeps the tiles around the view drawn, so a draw only draws the edge +tiles the move uncovered (a move of a whole view or more draws them all), then +copies the band. A draw covers the band and never reads what was there, so a +scrolling frame is: draw the layer, draw the sprites with `jlSpriteDraw` (no +save-under, since the next draw covers them), present. + +[source,c] +---- +static jlTileT tiles[64]; // from jlTileBankLoad +static uint8_t map[256 * 25]; // 256 x 25 tile indices +jlSpriteT *ship; // from jlSpriteCreate, compiled +jlTileLayerT *layer; +uint16_t x = 0; + +layer = jlTileLayerCreate(tiles, map, 256, 25, 0, JL_TILE_BLOCKS_PER_COL); +for (;;) { + jlTileLayerDraw(jlStageGet(), layer, x, 0); + jlSpriteDraw(jlStageGet(), ship, 40, 96); + jlStagePresent(); + x++; +} +---- + +The layer keeps a pointer to your tileset and map rather than copying them, so +both must outlive it. Change a cell with `jlTileLayerSetTile`, not by writing +the map: the layer would not see the write until that cell scrolled out of the +view and back. A tile whose pixels you change in the tileset is likewise only +seen where a cell showing it is set again. + +What a layer costs in memory is its view: about 1.3 KB per tile row of the band, +plus one, on the Amiga, ST, and X68000 (35 KB for a full-height band), twice that on the +others, which keep a second copy of the view shifted by a pixel so an odd +origin copies whole bytes. + +[NOTE] +==== +Every whole-band scroll redraws the band and presents it, so a scrolling game +runs at the rate of a full-screen present plus a full-band copy. On the +slowest machines JoeyLib targets that is a few frames a second (the IIgs about +5.8, the plain ST about 8, a 386SX-16 under DOS about 6.5 in the UBER +benchmark; the Amiga and STe copy the band with their blitters, about 20), so +plan scrolling games around a band no taller than the game needs. +==== + +=== jlTileLayerCreate + +[source,c] +---- +jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map, + uint16_t mapWidth, uint16_t mapHeight, + uint8_t viewBy, uint8_t viewHeight); +---- + +Makes a layer over `map`. Nothing is drawn until `jlTileLayerDraw`. + +[cols="1,1,4", options="header"] +|=== +|Parameter |Type |Description +|`tiles` |`const jlTileT *` |The tileset. Kept, not copied. +|`map` |`uint8_t *` |`mapWidth * mapHeight` tileset indices, row-major, so + `map[row * mapWidth + col]`. `JL_TILE_MAP_SKIP` draws colour 0. Kept, not + copied; `jlTileLayerSetTile` writes it. +|`mapWidth`, `mapHeight` |`uint16_t` |The map's size in tiles, 1 to + `JL_TILE_LAYER_MAX_SIDE`. +|`viewBy` |`uint8_t` |The band's first tile row on the stage. +|`viewHeight` |`uint8_t` |The band's height in tile rows, at least 1; + `viewBy + viewHeight` at most `JL_TILE_BLOCKS_PER_COL`. +|=== + +*Returns:* the layer, or `NULL` if an argument is `NULL` or out of range, or +memory runs out. + +[WARNING] +==== +Like `jlTileMapPaste`, a layer does not range-check map indices against the +tileset: any index other than `JL_TILE_MAP_SKIP` must be a valid one. +==== + +=== jlTileLayerDestroy + +[source,c] +---- +void jlTileLayerDestroy(jlTileLayerT *layer); +---- + +Frees a layer, not its tileset or map. `NULL` is a no-op. + +=== jlTileLayerDraw + +[source,c] +---- +void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer, + uint16_t x, uint16_t y); +---- + +Fills the layer's band of `dst` with the map seen from pixel `(x, y)`: the +band's top-left pixel shows map pixel `(x, y)`, each taken modulo the map's +size in pixels. Any pixel origin, with no alignment. + +[cols="1,1,4", options="header"] +|=== +|Parameter |Type |Description +|`dst` |`jlSurfaceT *` |Destination surface, normally the stage. +|`layer` |`jlTileLayerT *` |The layer. +|`x`, `y` |`uint16_t` |The origin, in map pixels. +|=== + +*Returns:* nothing. + +=== jlTileLayerSetTile + +[source,c] +---- +void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty, + uint8_t tile); +---- + +Sets map cell `(tx, ty)` to `tile`. The next draw shows it. + +[cols="1,1,4", options="header"] +|=== +|Parameter |Type |Description +|`layer` |`jlTileLayerT *` |The layer. +|`tx`, `ty` |`uint16_t` |The cell, in tiles. Out-of-range cells are ignored. +|`tile` |`uint8_t` |The tileset index, or `JL_TILE_MAP_SKIP` for colour 0. +|=== + +*Returns:* nothing. diff --git a/docs/Assets.adoc b/docs/Assets.adoc index a0fd02e..6b0e0ac 100644 --- a/docs/Assets.adoc +++ b/docs/Assets.adoc @@ -16,6 +16,11 @@ type: what you author, which tool bakes it, and which API call loads it. |`.tbk` |`jlTileBankLoad`, `jlTileBankLoadToSurface` +|A Tiled map (TMX or TMJ) with indexed tilesets +|`tiled2joey.py`, then `assetbake.py --type tile` for its sheet +|a `.map` per layer, and a `.tbk` +|`jlTileMapPaste` (the map read with `jlDataOpen`) + |Indexed PNG sprite sheet |`assetbake.py --type sprite` |`.spr` @@ -69,6 +74,12 @@ run on Linux, and all but OpenMPT are native there. JoeyLib palette holds exactly, 4 bits a gun. The sheet rules are in <<_authoring_the_png>>. +|Tile maps +|*Tiled* (GPLv2; what you make with it is yours), a map editor that places + tiles from tilesets in layers. +|Orthogonal maps of 8x8 tiles from indexed tilesets, then `tiled2joey.py` + (<<_maps_from_tiled>>). + |Program icons |Any of the above, saving an RGBA PNG with transparency. |One picture for every system (<<_icons>>). @@ -132,6 +143,61 @@ loader refuses a file baked for a different target rather than drawing garbage. `--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`, `iigs`, or `x68000`. +==== Maps from Tiled + +`tools/tiled2joey.py` turns a map made in Tiled into what a JoeyLib program +pastes: a tile sheet, which `assetbake.py` bakes into a tile bank for each +port, and a map for each tile layer, which `jlTileMapPaste` takes as it is: + +[source,console] +---- +$ python3 tools/tiled2joey.py --header build/level1.h level1.tmx \ + build/level1.png build/level1 +tiled2joey: 60x25 map, 87 tiles, 14 colours, layers: ground.map, detail.map +$ python3 tools/assetbake/assetbake.py --type tile --target iigs \ + build/level1.png build/iigs/bin/DATA/level1.tbk +---- + +* *The map* is Tiled's own TMX or its JSON (TMJ), orthogonal and finite, with + 8x8 tiles. Its tilesets, in the map or in files of their own (TSX or TSJ), + are single images: indexed PNGs whose colours make one palette of at most + 16 between them. The first tileset's colours keep their indices, so its + colour 0 stays the background; another's are matched by colour or take a + slot the first does not use. +* *The sheet* holds the tiles the map's layers use, in the order they first + appear, so the bank holds nothing the map does not place. A tile drawn + flipped or rotated is baked as a tile of its own (JoeyLib draws tiles only + as they are), so a map can use up to 255 distinct tiles and orientations + between its layers. +* *Each layer* becomes `NAME.map` in the folder given: width x height bytes, + row after row, each a tile's index in the bank or `JL_TILE_MAP_SKIP` where + the layer is empty, so pasting a layer over another leaves the one below + showing through. The files are the same on every port. +* `--header` writes the map's size and tile count as C constants, and lists + each layer's file. Object layers, image layers, tile animations, and tile + properties are not carried over. + +A map no bigger than the screen pastes in one call. A bigger one -- a level +the program shows a screen of at a time -- pastes a row of tiles a call, +starting each row at its place in the whole map: + +[source,c] +---- +for (row = 0; row < 25u; row++) { + jlTileMapPaste(jlStageGet(), 0, (uint8_t)row, 40, 1, tiles, &level[(top + row) * LEVEL1_WIDTH + left]); +} +---- + +A level that scrolls is a tile layer over the whole map (see +<<_scrolling_tile_layers>>). A layer is opaque and draws `JL_TILE_MAP_SKIP` as +colour 0, so it scrolls one map layer; flatten the scenery into one layer in +Tiled. + +[source,c] +---- +layer = jlTileLayerCreate(tiles, level, LEVEL1_WIDTH, LEVEL1_HEIGHT, 0, 25); +---- + ==== Baking Sprites [source,console] @@ -399,11 +465,12 @@ $(call programDataDir,MYGAME)/font.tbk: assets/font.png ---- Tile banks and `.spc` files differ per platform, while `.spr`, `.jym`, and -`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX -and every hosted port -- SDL3, the web, Win32, Win16, the PSP, Haiku, and -BeOS -- take the DOS bakes; DVX and Win16 run the x86 code of a `.spc`, the -others use only its cels; the STe build takes the ST's) and `SPRITEBAKE` the -matching host `spritebake`. +`.sfx` files are shared, as are the `.map` files `tiled2joey.py` writes: +`ASSET_PLATFORM` names the target a bake is for (DVX and every hosted port -- +SDL3, the web, macOS, Win32, Win16, the PSP, Haiku, and BeOS -- take the DOS +bakes; DVX and Win16 run the x86 code of a `.spc`, the others use only its +cels; the STe build takes the ST's) and `SPRITEBAKE` the matching host +`spritebake`. `examples/tilebank` is a working reference: one rule in `make/examples.mk` bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`), diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc index 1162f1f..fc1ca61 100644 --- a/docs/BuildIntegration.adoc +++ b/docs/BuildIntegration.adoc @@ -29,6 +29,10 @@ The whole Makefile for a program with no data files: JOEYLIB ?= ../joeylib include $(JOEYLIB)/make/project.mk +DVX_VENDOR := acme +DVX_AUTHOR := Acme Games +MACOS_ID_PREFIX := com.example.acme + PROGRAMS := MYGAME MYGAME_SRCS := mygame.c world.c MYGAME_TITLE := My Game @@ -39,7 +43,16 @@ include $(JOEYLIB)/make/programs.mk `make/project.mk` comes first and sets up the target being built; the description comes next and may use what it set up (see <<_data_files>>); -`make/programs.mk` comes last and makes the rules. Run make in the project's +`make/programs.mk` comes last and makes the rules. + +The first three lines say whose the programs are, which only you can say: +JoeyLib never makes it up, and building for DVX or macOS stops until they +are set. `DVX_VENDOR` is the folder your programs install in under DVX's +`APPS\` (1 to 8 letters, digits, or `_`; DVX names a package `vendor.name`) +and `DVX_AUTHOR` the author DVX shows; `MACOS_ID_PREFIX`, a reverse-DNS name +of yours, starts each Mac app's identifier, which ends in the program's name +(`com.example.acme.MYGAME`). JoeyLib's own examples carry their author's +(`kpunch`, `com.kangaroopunch.joeylib`). Run make in the project's directory -- the paths in the Makefile are relative to it -- or with `make -C`: [source,console] @@ -71,11 +84,14 @@ with `MYGAME_IIGS_GAME_DISK` the disks `build/iigs/bin/MYGAME.2mg` and (<<_the_atari_st_family>>); same toolchain and baked data as the ST |MS-DOS |`build/dos/bin/MYGAME.EXE`, with the DPMI server built in |Sharp X68000 |`build/x68000/bin/MYGAME.X` -|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp`, and its +|DVX |`build/dvx/bin/apps/acme/mygame/`: `mygame.app`, `mygame.hlp`, and its `DATA/` |SDL3 |`build/sdl3/bin/MYGAME`, a native program with SDL 3 linked in |Web |`build/web/bin/MYGAME/`: `MYGAME.html`, `.js`, `.wasm`, and `.data` (its `DATA/`, packed in), and `icon.png` +|macOS |`build/macos/bin/MYGAME.app`, a universal app: the program in +`Contents/MacOS/`, its `DATA/` and icon in `Contents/Resources/`; the program +it is packed from in `build/macos/link/MYGAME` |Win32 |`build/win32/bin/MYGAME.EXE`, a 32-bit Windows program |Win16 |`build/win16/bin/MYGAME.EXE`, a Windows 3.x program |PSP |`build/psp/bin/MYGAME/`: `EBOOT.PBP` and its `DATA/`, a folder for the @@ -134,12 +150,12 @@ probes for gates). |`ID_PORTS` |The targets it builds for. Every target when unset. |`ID_NAME` |Its file name without the extension (see <<_naming>>). |`ID_FOLDER` |Set: it ships in a folder of its own with its `DATA/` (Amiga, -ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web, and the PSP -every program is packaged apart anyway). +ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web, macOS, and +the PSP every program is packaged apart anyway). |`ID_BINDIR` |Build it there instead of `bin/`. |`ID_DATA` |Its data files (<<_data_files>>). -|`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu (by -default its name). +|`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu. +macOS: its app's display name (`CFBundleDisplayName`). By default its name. |`ID_ICON` |Its icon: a picture (a PNG, best square, transparent around the figure), baked for every target whose system shows programs by an icon (<<_icons>>). JoeyLib's kangaroo by default. @@ -160,9 +176,9 @@ MYGAME_X68000_LDFLAGS := -s MYGAME_IIGS_DATA := # nothing staged on the IIgs ---- -DVX's help and resource texts call a program "a `$(DVX_KIND)` program" and -name `$(DVX_AUTHOR)` as its author (both `JoeyLib` by default); `DVX_VENDOR` -(`joeylib`) is the folder it installs in under `APPS\`. Set them in the +DVX's help and resource texts call a program "a `$(DVX_KIND)` program" +(`JoeyLib` by default) and name `$(DVX_AUTHOR)` as its author; `DVX_VENDOR` +is the folder it installs in under `APPS\` (<<_a_project>>). Set them in the description. === Data Files @@ -179,10 +195,10 @@ set up: |`$(call programDataDir,ID)` |Where its `DATA/` files go on this target. |`$(call programOptionalDir,ID)` |Where the files go that it can run without (<<_disk_images_and_packaging>>). -|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`, `haiku`, `haiku32`, `beos`. +|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `macos`, `win32`, `win16`, `psp`, `haiku`, `haiku32`, `beos`. |`ASSET_PLATFORM` |The target its bakes are for. DVX shows the DOS stage and runs x86 code, so it takes the DOS bakes; the hosted ports' stage (SDL3, the -web, Win32, Win16, the PSP, Haiku, BeOS) is the same chunky one, so they take +web, macOS, Win32, Win16, the PSP, Haiku, BeOS) is the same chunky one, so they take them too: Win16 runs a pre-compiled sprite bank's x86 code as DVX does, and the others take its cels without their code. The STe build takes the ST's. |`SPRITEBAKE` |The host `spritebake` for this target (its rule is @@ -236,9 +252,10 @@ program's id: |Amiga |Mixed case, no extension: the id Capitalised unless `ID_AMIGA_NAME` says otherwise |`Mygame` |Sharp X68000 |Uppercase 8.3, `.X` |`MYGAME.X` -|DVX |Lowercase, 8 characters, a folder of its own |`apps/joeylib/mygame/mygame.app` +|DVX |Lowercase, 8 characters, a folder of its own |`apps/acme/mygame/mygame.app` |SDL3 |The id, no extension |`MYGAME` |Web |The id, a folder of its own |`MYGAME/MYGAME.html` +|macOS |The id, an app |`MYGAME.app` |Win32 |Uppercase 8.3, `.EXE` |`MYGAME.EXE` |Win16 |Uppercase 8.3, `.EXE` |`MYGAME.EXE` |PSP |The id, a folder of its own holding `EBOOT.PBP` |`MYGAME/EBOOT.PBP` @@ -276,10 +293,10 @@ needs to be on the disk. `Name.info` beside it is its Workbench icon, from which it starts too. *DVX.* Every program is a folder laid out as it installs in DVX: -`build/dvx/bin/apps/joeylib/mygame/` holds `mygame.app`, its help file +`build/dvx/bin/apps/acme/mygame/` holds `mygame.app`, its help file `mygame.hlp` (DVX gives every program one; the rules write a one-topic help from `MYGAME_HELP`) and its `DATA/`. Copy that folder into a DVX installation's -`APPS\JOEYLIB\` and start it from DVX, or let `joey-run` do it +`APPS\ACME\` and start it from DVX, or let `joey-run` do it (<<_dvx_programs>>). *SDL3.* A program is one native executable with SDL 3 linked in statically, @@ -299,6 +316,13 @@ Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL, and MMSYSTEM), so its folder, with its `DATA/`, is all it needs; it ships the way a DOS program does. +*macOS.* A program is its app, `build/macos/bin/MYGAME.app`, which holds +everything it needs: the program (SDL 3 linked in, for Apple silicon and +Intel Macs both), its `DATA/`, its icon, and the `Info.plist` that names +them. Copy the app to a Mac, or zip it to send it; a Mac opens it from +anywhere. Its identifier is `MACOS_ID_PREFIX` and its name +(`com.example.acme.MYGAME`, <<_a_project>>). + *PSP.* A program is its folder, `build/psp/bin/MYGAME/`: `EBOOT.PBP` (the program, SDL 3 linked in, packed as the PSPSDK packs it, with its icon and no other pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory @@ -340,13 +364,19 @@ colours, bound into the `.EXE`. |Win32 |Explorer's and the window's: 16, 32, and 48 pixels, each in 256 colours and in 32-bit, linked into the `.EXE`. |Haiku, BeOS |Tracker's and the Deskbar's: 32x32 and 16x16 in the BeOS -palette, in the program's resources. Tracker shows a file by the icons in its +palette, in the program's resources, and on Haiku the same two as one vector +icon, which draws each at its own size and the 32x32 cleanly at any larger +one (an icon with too many colour patches for the vector format goes +without). Tracker shows a file by the icons in its attributes, which a program built on another machine does not have; the program copies them across the first time it runs, so Tracker shows it by its icon from then on (or at once, after `mimeset -F NAME`). |PSP |The menu's: `ICON0` in the EBOOT, 144x80. |SDL3 |The window's, 64x64, which the desktop shows where it shows one (title bar, task bar, dock). +|macOS |Finder's and the Dock's: `icon.icns` in the app's `Contents/Resources/`, +16 to 128 pixels and twice that for Retina screens (up to 512, a picture that +big or pixel art). |Web |The page's: `icon.png`, 32x32, beside it. |=== diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc index c0f054e..f6f3715 100644 --- a/docs/GettingStarted.adoc +++ b/docs/GettingStarted.adoc @@ -5,7 +5,8 @@ JoeyLib cross-compiles for five machines and for DVX, a DOS desktop whose programs run in windows (see <<_dvx_programs>>), builds native programs for the machine you work on through SDL3 (see <<_sdl3_programs>>), builds web -pages through Emscripten (see <<_web_programs>>), and builds PSP homebrew +pages through Emscripten (see <<_web_programs>>), builds Mac apps through +zig and Apple's SDK (see <<_macos_programs>>), and builds PSP homebrew through pspdev (see <<_psp_programs>>). It builds on Linux x86-64, and on Windows inside WSL2. One script installs every cross toolchain, pinned to a known-good version and checked against a published checksum, into @@ -111,12 +112,13 @@ A target is a component name from `list`, or a group: | Everything needed to build for one port: `iigs`, `amiga`, `atarist`, `dos`, `x68000`, `dvx`, `sdl3` (SDL 3, built as a static library that every SDL3 program links in, so it runs without SDL installed), `web` (Emscripten, -through emsdk, and SDL 3 built with it), `win32` (llvm-mingw; its -emulator is a portable Wine), `win16` (Open Watcom; its emulator is Windows -for Workgroups 3.11 in DOSBox Staging, set up from your own install disc, see -<>) or `psp` (pspdev's prebuilt release, -and SDL 3 built with it; its emulator is PPSSPP's headless build, patched for -the gates). +through emsdk, and SDL 3 built with it), `macos` (zig, Apple's macOS SDK, +and SDL 3 built with them; there is no emulator: nothing here runs Mac +programs), `win32` (llvm-mingw; its emulator is a portable Wine), `win16` +(Open Watcom; its emulator is Windows for Workgroups 3.11 in DOSBox Staging, +set up from your own install disc, see <>), +or `psp` (pspdev's prebuilt release, and SDL 3 built with it; its emulator +is PPSSPP's headless build, patched for the gates). | `emulator:PORT` | Everything needed to run that port's programs: the emulator and its @@ -214,6 +216,7 @@ $ make x68000 # Sharp X68000 only $ make dvx # DVX only $ make sdl3 # native programs through SDL3 only $ make web # web pages through Emscripten only +$ make macos # universal Mac apps only $ make win32 # Windows programs only $ make win16 # Windows 3.x programs only $ make psp # PSP homebrew (EBOOT.PBP) only @@ -457,6 +460,12 @@ server. With `--headless` it runs in headless Chromium instead, at one canvas pixel per stage pixel, so a `--snapshot` is the stage. Its `DATA/` is packed in when it is built, so `--data` does not apply. +==== macOS + +Nothing here runs a Mac program: copy its app, `build/macos/bin/NAME.app`, to +a Mac and open it there (<<_macos_programs>>). `joey-run` knows one when it +sees it and says so. + ==== Win32 A Windows program runs under Wine: in a window on your display, with its @@ -665,6 +674,7 @@ built for every platform: |`keys` |Typed-character input and the key predicates. |`joy` |Joystick axes, buttons, and recalibration. |`sprite` |Sprite loading, compiling, save-under, and restore. +|`scroll` |A scrolling tile layer under a sprite, with a frame-rate bar. |`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound, and the noise generator. |`save` |Save files and the disk-space query. |`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused. @@ -679,9 +689,10 @@ game, and JoeyAGI, an interpreter for Sierra's AGI adventures. Each builds from its own directory against a JoeyLib checkout, as any program of yours would (<<_building_your_program>>). -On DVX every example is a folder of its own under `build/dvx/bin/apps/joeylib/` +On DVX every example is a folder of its own under `build/dvx/bin/apps/kpunch/` with its help file (<<_dvx_programs>>). On SDL3 every example is a native program in `build/sdl3/bin/`, beside the shared `DATA/` (<<_sdl3_programs>>), on the web a page in a folder of its own, `build/web/bin/NAME/` -(<<_web_programs>>), and on the PSP a folder for `PSP/GAME/`, +(<<_web_programs>>), on macOS an app, `build/macos/bin/NAME.app`, with its +`DATA/` inside (<<_macos_programs>>), and on the PSP a folder for `PSP/GAME/`, `build/psp/bin/NAME/` with `EBOOT.PBP` and its `DATA/` (<<_psp_programs>>). diff --git a/docs/Portability.adoc b/docs/Portability.adoc index a64b238..2f37eab 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -222,7 +222,7 @@ The API is uniform; these are the places where what you get differs. [cols="2,2,2,2,2,2,2,2,2", options="header"] |=== -|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, Win32, PSP, Haiku, BeOS |Win16 +|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, macOS, Win32, PSP, Haiku, BeOS |Win16 |Frame rate |60 Hz (50 on a PAL machine) @@ -271,8 +271,8 @@ The API is uniform; these are the places where what you get differs. |none |none |Program Manager's -|the window's (SDL3), the page's (web), Explorer's (Win32), the menu's -(PSP), Tracker's (Haiku, BeOS) +|the window's (SDL3), the page's (web), Finder's and the Dock's (macOS), +Explorer's (Win32), the menu's (PSP), Tracker's (Haiku, BeOS) |Program Manager's |Tone voices (`jlAudioTone` is voice 0) @@ -292,7 +292,7 @@ The API is uniform; these are the places where what you get differs. |analog |digital |analog (DVX calibrates) -|SDL's gamepads and joysticks (SDL3, web); Windows' joysticks (Win32); the system's joysticks +|SDL's gamepads and joysticks (SDL3, web, macOS); Windows' joysticks (Win32); the system's joysticks (Haiku, BeOS); the PSP's own controls |Windows' joysticks (Control Panel calibrates) @@ -313,7 +313,7 @@ The API is uniform; these are the places where what you get differs. |COM1..COM4 |RS-232C (needs RSDRV.SYS) |COM1..COM4 (DVX's library) -|Win32: COM1 and up; SDL3 (Linux), Haiku, BeOS: terminal devices; web, PSP: none +|Win32: COM1 and up; SDL3 (Linux), macOS, Haiku, BeOS: terminal devices; web, PSP: none |COM1..COM9 |=== @@ -612,6 +612,9 @@ names the ports, says how they differ, and gives the reason. ports that interpret sprites use only its pixels. * *Web saves are the browser's.* A browser may clear a site's stored data to make room, and a private window keeps nothing once it closes. +* *macOS: saves in Application Support.* An app's own folder may not be + writable, so `SAVES/` and the log go in the user's + `~/Library/Application Support/NAME/` (<<_macos_programs>>). [[portsDifferMemory]] ==== Sprites and Memory @@ -705,9 +708,9 @@ Nothing in the API differs between them: |Fills and copies |The CPU -|The BLiTTER, for fills of 8 or more rows (24 for one 16-pixel column) and - copies of 4 or more rows; smaller work stays on the CPU, which is faster - there +|The BLiTTER, for fills of 8 or more rows (24 for one 16-pixel column), + copies of 4 or more rows, and a tile layer's band; smaller work stays on + the CPU, which is faster there |CPU clock |As the machine is (8 MHz; TOS's setting on a Mega STe) @@ -788,7 +791,7 @@ hashes are DOS's -- and the rest behaves like this: * *Files.* DVX's programs share one current directory, so JoeyLib roots its files at run time: `DATA/` is found beside the program (its folder under `APPS`), while `SAVES/` and `joeylog.txt` go to the program's own settings - folder, `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program + folder, `CONFIG\APPS\VENDOR\NAME`, which DVX keeps apart from the program files so a reinstall never overwrites them. * *Sound* goes through DVX's sound and music libraries, mixed with every other program's: `.MOD` music, SFX in stereo, and the tone voices and the @@ -918,6 +921,41 @@ are DOS's here too. Everything in <<_sdl3_programs>> holds, except: `make web` builds every example as a page, and `./joey-run --port web NAME` serves its folder on this machine and opens it in your browser. +=== macOS Programs + +The macOS port is the SDL3 port built as Mac apps, on Linux: the same hosted +core, the same backend, the same stage (UBER's hashes are DOS's). It is +compiled with zig against Apple's macOS SDK, once for Apple silicon and once +for Intel, the two joined into one universal program that runs on either, +on macOS 13 (Ventura) or newer, with SDL 3 built the same way and linked in +(the `macos` installer components: zig, the SDK, and SDL 3). Everything in +<<_sdl3_programs>> holds, except: + +* *An app.* A program is `build/macos/bin/NAME.app`: the program in + `Contents/MacOS/`, its `DATA/` and its icon in `Contents/Resources/`, which + is its folder and the current directory, and the `Info.plist` that names + them. Copy the app to a Mac, or zip it to send it; it opens from anywhere. +* *Saves in Application Support.* `SAVES/` and `joeylog.txt` go in + `~/Library/Application Support/NAME/`, not beside the program: an app in + `/Applications` may not be writable by its user, and macOS runs a + downloaded app from a read-only copy until it is moved. `jlDiskFree` is + the free space there. +* *Retina screens.* The window draws at the screen's own resolution, so the + stage is scaled up once, sharply, and not blurred again by the system. +* *Serial* through termios, as on Linux: a Mac has no serial port of its + own, so the ports are USB adapters' call-out devices (`/dev/cu.*`, the + Mac's own Bluetooth and debug devices left out), in name order, or the + device `JOEYLIB_SERIAL` names. +* *Untested on a Mac.* Nothing here runs Mac programs: `make verify` builds + every example and checks every app is one macOS opens -- both processors' + programs, the Apple silicon one signed, for macOS 13 on, linking only the + system's own libraries, with its `Info.plist` and icon + (`scripts/check-macos-apps.sh`) -- but what an app does on a Mac has not + been seen yet. + +`make macos` builds every example as an app; `./joey-run` recognises one +and says it runs on a Mac. + === PSP Programs The PSP port is the SDL3 port built as PSP homebrew: the same hosted core diff --git a/docs/ProgramLayout.adoc b/docs/ProgramLayout.adoc index 6f6d498..7c50777 100644 --- a/docs/ProgramLayout.adoc +++ b/docs/ProgramLayout.adoc @@ -199,7 +199,7 @@ This compiles and runs on every target as-is. #define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel #define MAX_STEP_MILLIS 100 // after a stall, move no further than this -static jlSurfaceT *gStage; +static jlSurfaceT *jlgStage; static bool gRunning = true; static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT; static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT; @@ -223,8 +223,8 @@ static void buildPalette(void) { colors[2] = 0x0C0; // green colors[3] = 0x00C; // blue - jlPaletteSet(gStage, 0, colors); - jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0); + jlPaletteSet(jlgStage, 0, colors); + jlScbSetRange(jlgStage, 0, JL_SURFACE_HEIGHT - 1, 0); } @@ -278,9 +278,9 @@ static void handleInput(int32_t step) { static void renderFrame(void) { - jlSurfaceClear(gStage, 0); - jlDrawRect(gStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1); - jlFillRect(gStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2); + jlSurfaceClear(jlgStage, 0); + jlDrawRect(jlgStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1); + jlFillRect(jlgStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2); } @@ -308,8 +308,8 @@ int main(void) { return 1; } - gStage = jlStageGet(); - if (gStage == NULL) { + jlgStage = jlStageGet(); + if (jlgStage == NULL) { jlShutdown(); return 1; } diff --git a/examples/adventure/adventure.c b/examples/adventure/adventure.c index 19b1aeb..31c18a8 100644 --- a/examples/adventure/adventure.c +++ b/examples/adventure/adventure.c @@ -206,7 +206,7 @@ typedef struct { * Section 2 -- Globals (game state) * =================================================================== */ -static jlSurfaceT *gScreen; +static jlSurfaceT *jlgScreen; static jlSurfaceT *gBgScene[ROOM_COUNT]; // pristine background per room static RoomE gCurRoom; @@ -1985,7 +1985,7 @@ static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color) bits = kFontGlyph[idx][row]; for (col = 0; col < FONT_W; col++) { if (bits & (0x80 >> col)) { - jlDrawPixel(gScreen, (int16_t)(cx + col), (int16_t)(y + row), color); + jlDrawPixel(jlgScreen, (int16_t)(cx + col), (int16_t)(y + row), color); } } } @@ -2006,8 +2006,8 @@ static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color) * =================================================================== */ static void drawMessageBar(void) { - jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG); - jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG); + jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); if (gMessage[0] != '\0') { drawTextLine(4, MSG_BAR_Y + 2, gMessage, UI_INK); } @@ -2019,8 +2019,8 @@ static void drawInventoryBar(void) { int n; int16_t x; - jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG); - jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG); + jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); // Verb readout at the left. snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb)); @@ -2032,12 +2032,12 @@ static void drawInventoryBar(void) { if (gHaveItem[ITEM_LAMP]) { jlSpriteT *sp; sp = gLampIsLit ? gLampLitSprite : gItemSprite[ITEM_LAMP]; - jlSpriteDraw(gScreen, sp, x, INV_BAR_Y + 2); + jlSpriteDraw(jlgScreen, sp, x, INV_BAR_Y + 2); x = (int16_t)(x - (ITEM_ICON_W + 2)); n++; } if (gHaveItem[ITEM_KEY]) { - jlSpriteDraw(gScreen, gItemSprite[ITEM_KEY], x, INV_BAR_Y + 2); + jlSpriteDraw(jlgScreen, gItemSprite[ITEM_KEY], x, INV_BAR_Y + 2); x = (int16_t)(x - (ITEM_ICON_W + 2)); n++; } @@ -2063,10 +2063,10 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { uint8_t i; // 1. background - jlSurfaceCopy(gScreen, gBgScene[gCurRoom]); + jlSurfaceCopy(jlgScreen, gBgScene[gCurRoom]); // 2. ego - jlSpriteDraw(gScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY); + jlSpriteDraw(jlgScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY); // 3. prop priority redraw r = &gRooms[gCurRoom]; @@ -2078,7 +2078,7 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { continue; } if ((p->y + p->h) > egoBottom) { - jlSpriteDraw(gScreen, p->sp, p->x, p->y); + jlSpriteDraw(jlgScreen, p->sp, p->x, p->y); } } @@ -2100,7 +2100,7 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { cs = gCursor[gCurVerb]; } if (cs != NULL) { - jlSpriteDraw(gScreen, cs, cx, cy); + jlSpriteDraw(jlgScreen, cs, cx, cy); } } @@ -2357,7 +2357,7 @@ static void enterRoom(RoomE room, bool fromEast, bool fromWest) { gPathLen = 0; gPathStep = 0; - programSkyScb(gScreen, room); + programSkyScb(jlgScreen, room); } @@ -2413,15 +2413,15 @@ int main(void) { return 1; } - gScreen = jlStageGet(); - if (gScreen == NULL) { + jlgScreen = jlStageGet(); + if (jlgScreen == NULL) { fprintf(stderr, "jlStageGet returned NULL\n"); jlShutdown(); return 1; } - buildPalettes(gScreen); - programSkyScb(gScreen, ROOM_FOREST); + buildPalettes(jlgScreen); + programSkyScb(jlgScreen, ROOM_FOREST); if (!buildAllSprites()) { fprintf(stderr, "buildAllSprites failed: %s\n", jlLastError()); diff --git a/examples/adventure2/adventure2.c b/examples/adventure2/adventure2.c index 5427715..57705ce 100644 --- a/examples/adventure2/adventure2.c +++ b/examples/adventure2/adventure2.c @@ -180,7 +180,7 @@ typedef enum { * Section 2 -- Globals * =================================================================== */ -static jlSurfaceT *gScreen; +static jlSurfaceT *jlgScreen; static jlSurfaceT *gVisual; // room background -- "picture" buffer static jlSurfaceT *gPriority; // per-pixel priority codes static jlSurfaceT *gControl; // per-pixel control codes @@ -634,7 +634,7 @@ static void drawText(int16_t x, int16_t y, const char *text, uint8_t color) { bits = kFontGlyph[idx][row]; for (col = 0; col < FONT_W; col++) { if (bits & (0x80 >> col)) { - jlDrawPixel(gScreen, (int16_t)(cx + col), (int16_t)(y + row), color); + jlDrawPixel(jlgScreen, (int16_t)(cx + col), (int16_t)(y + row), color); } } } @@ -736,10 +736,10 @@ static void buildPalettes(void) { ui[UI_HILITE] = rgb(15, 13, 3); ui[UI_DIM] = rgb(7, 7, 8); - jlPaletteSet(gScreen, PAL_SCENE, scene); - jlPaletteSet(gScreen, PAL_DEBUG_PRI, pri); - jlPaletteSet(gScreen, PAL_DEBUG_CTRL, ctl); - jlPaletteSet(gScreen, PAL_UI, ui); + jlPaletteSet(jlgScreen, PAL_SCENE, scene); + jlPaletteSet(jlgScreen, PAL_DEBUG_PRI, pri); + jlPaletteSet(jlgScreen, PAL_DEBUG_CTRL, ctl); + jlPaletteSet(jlgScreen, PAL_UI, ui); } @@ -763,10 +763,10 @@ static void programScbForView(DebugViewE view) { break; } for (line = 0; line < PLAY_AREA_H; line++) { - jlScbSet(gScreen, (uint16_t)line, playPal); + jlScbSet(jlgScreen, (uint16_t)line, playPal); } for (line = PLAY_AREA_H; line < JL_SURFACE_HEIGHT; line++) { - jlScbSet(gScreen, (uint16_t)line, PAL_UI); + jlScbSet(jlgScreen, (uint16_t)line, PAL_UI); } } @@ -981,7 +981,7 @@ static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) { // Picture pixel is in front. Skip the actor pixel. continue; } - jlDrawPixel(gScreen, px, py, color); + jlDrawPixel(jlgScreen, px, py, color); } } } @@ -1323,15 +1323,15 @@ static void drawCursor(int16_t mx, int16_t my, VerbE v) { if (color == C_TRANS) { continue; } - jlDrawPixel(gScreen, px, py, color); + jlDrawPixel(jlgScreen, px, py, color); } } } static void drawMessageBar(void) { - jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG); - jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG); + jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); if (gMessage[0] != '\0') { drawText(4, (int16_t)(MSG_BAR_Y + 2), gMessage, UI_INK); } @@ -1342,8 +1342,8 @@ static void drawInventoryBar(void) { char buf[40]; const char *vw; - jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG); - jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG); + jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb)); drawText(4, (int16_t)(INV_BAR_Y + 2), buf, UI_HILITE); @@ -1368,7 +1368,7 @@ static void drawInventoryBar(void) { * =================================================================== */ static void composeFrameNormal(int16_t mx, int16_t my) { - jlSurfaceCopy(gScreen, gVisual); + jlSurfaceCopy(jlgScreen, gVisual); drawEgoMasked(gEgoX, gEgoY, gEgoDir, gEgoFrameIdx); drawMessageBar(); drawInventoryBar(); @@ -1381,7 +1381,7 @@ static void composeFrameDebug(int16_t mx, int16_t my) { /* The debug overlays blit the raw priority/control buffer as the * play-area picture; the palette swap (PAL_DEBUG_PRI or * PAL_DEBUG_CTRL) makes the codes visible. */ - jlSurfaceCopy(gScreen, (gDebugView == VIEW_PRIORITY) ? gPriority : gControl); + jlSurfaceCopy(jlgScreen, (gDebugView == VIEW_PRIORITY) ? gPriority : gControl); /* Show where the actor is by overlaying ego pixels in pure white * with priority always-on. */ { @@ -1404,7 +1404,7 @@ static void composeFrameDebug(int16_t mx, int16_t my) { if (pixCharToColor(rows[r][c]) == C_TRANS) { continue; } - jlDrawPixel(gScreen, px, py, 15); + jlDrawPixel(jlgScreen, px, py, 15); } } } @@ -1509,8 +1509,8 @@ int main(void) { return 1; } - gScreen = jlStageGet(); - if (gScreen == NULL) { + jlgScreen = jlStageGet(); + if (jlgScreen == NULL) { fprintf(stderr, "jlStageGet returned NULL\n"); jlShutdown(); return 1; diff --git a/examples/scroll/scroll.c b/examples/scroll/scroll.c new file mode 100644 index 0000000..0bdff79 --- /dev/null +++ b/examples/scroll/scroll.c @@ -0,0 +1,344 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Scroll demo: a tile layer (jlTileLayer*) over a landscape three screens +// wide and two tall, with a ball drawn over it every frame -- no +// save-under, since the next layer draw covers it. The arrow keys or a +// joystick move the view; left alone it drifts. The two tile rows under +// the layer are a status bar whose bar grows with the frames drawn a +// second, so the demo shows how fast each port scrolls. Escape ends it. +// +// The tiles are painted with the drawing calls and snapped, since a +// jlTileT is in each port's own format; the map is generated. + +#include + +#include + +#define MAP_W 96 +#define MAP_H 40 +#define VIEW_ROWS 23 // tile rows of the layer +#define BAR_TOP (VIEW_ROWS * JL_TILE_PIXELS_PER_SIDE) +#define BAR_H (JL_SURFACE_HEIGHT - BAR_TOP) +#define BAR_PIXELS_PER_FPS 5 // the bar's growth for each frame a second +#define BAR_INSET 4 + +#define TILE_SKY 0 +#define TILE_GRASS 1 +#define TILE_DIRT 2 +#define TILE_BRICK 3 +#define TILE_WATER 4 +#define TILE_CLOUD 5 +#define TILE_STONE 6 +#define TILE_FLOWER 7 +#define TILE_COUNT 8 + +#define COLOR_SKY 1 +#define COLOR_DIRT 2 +#define COLOR_GRASS 3 +#define COLOR_DIRT_DARK 4 +#define COLOR_BRICK 5 +#define COLOR_GREY 6 +#define COLOR_WATER 7 +#define COLOR_FOAM 8 +#define COLOR_CLOUD 9 +#define COLOR_GREY_DARK 10 +#define COLOR_PETAL 11 +#define COLOR_BALL 12 +#define COLOR_SHINE 13 +#define COLOR_BAR_BACK 14 +#define COLOR_BAR 15 + +#define GROUND_LOW 34 // the ground's highest and lowest rows +#define GROUND_HIGH 22 +#define WATER_LINE 31 // ground below this row floods +#define STONE_DEPTH 4 // dirt rows above the stone +#define LEDGES 30 // brick ledges and clouds, turn about +#define LEDGE_MIN 2 // their length in tiles +#define LEDGE_MAX 6 +#define LEDGE_TOP 4 // their highest row +#define LEDGE_GAP 4 // rows kept clear above the highest ground + +#define BALL_TILES 2 +#define BALL_SIDE (BALL_TILES * JL_TILE_PIXELS_PER_SIDE) +#define BALL_X ((JL_SURFACE_WIDTH - BALL_SIDE) / 2) +#define BALL_Y ((BAR_TOP - BALL_SIDE) / 2) + +#define SPEED 90 // pixels per second, the same on every port +#define DRIFT_X 40 +#define DRIFT_Y 12 +#define STICK_THRESHOLD 32 +#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel +#define MAX_STEP_MILLIS 100 // after a stall, move no further than this +#define MILLIS_PER_SECOND 1000 +#define MAP_PX_W ((int32_t)MAP_W * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT) +#define MAP_PX_H ((int32_t)MAP_H * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT) +#define CODEGEN_BYTES (32UL * 1024) + +static const uint16_t gPalette[16] = { + 0x000, 0x6AF, 0x852, 0x3B3, 0x531, 0xB33, 0x999, 0x24C, + 0x8CF, 0xFFF, 0x555, 0xFC2, 0xFD0, 0xFFF, 0x113, 0x4E4 +}; + +// The ball, 16x16 at 4 bits a pixel, high nibble left; colour 0 is clear. +static const uint8_t gBallRows[BALL_SIDE * BALL_SIDE / 2] = { + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, + 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, + 0x0C, 0xCC, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, + 0x0C, 0xCD, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, + 0xCC, 0xDD, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, + 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, + 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xCC, 0xCC, 0x00, 0x00, 0x00 +}; + +static jlTileT gTiles[TILE_COUNT]; +static uint8_t gMap[MAP_W * MAP_H]; +static uint8_t gBallTiles[BALL_TILES * BALL_TILES * JL_TILE_BYTES]; +static uint32_t gSeed = 2026u; + +static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond); +static void makeBall(void); +static void makeMap(void); +static void makeTiles(jlSurfaceT *screen); +static uint16_t nextRandom(uint16_t below); +static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift); +static int32_t wrap(int32_t v, int32_t size); + + +// The status bar under the layer: its bar is BAR_PIXELS_PER_FPS a frame a second. +static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond) { + uint16_t w; + + w = (uint16_t)(framesPerSecond * BAR_PIXELS_PER_FPS); + if (w > JL_SURFACE_WIDTH - 2 * BAR_INSET) { + w = JL_SURFACE_WIDTH - 2 * BAR_INSET; + } + jlFillRect(screen, 0, BAR_TOP, JL_SURFACE_WIDTH, BAR_H, COLOR_BAR_BACK); + if (w > 0) { + jlFillRect(screen, BAR_INSET, BAR_TOP + BAR_INSET, w, BAR_H - 2 * BAR_INSET, COLOR_BAR); + } +} + + +// The ball's rows, repacked into the tile-major layout jlSpriteCreate takes. +static void makeBall(void) { + uint16_t tx; + uint16_t ty; + uint16_t row; + uint16_t b; + + for (ty = 0; ty < BALL_TILES; ty++) { + for (tx = 0; tx < BALL_TILES; tx++) { + for (row = 0; row < JL_TILE_PIXELS_PER_SIDE; row++) { + for (b = 0; b < JL_TILE_BYTES_PER_ROW; b++) { + gBallTiles[(ty * BALL_TILES + tx) * JL_TILE_BYTES + row * JL_TILE_BYTES_PER_ROW + b] = gBallRows[(ty * JL_TILE_PIXELS_PER_SIDE + row) * (BALL_SIDE / 2) + tx * JL_TILE_BYTES_PER_ROW + b]; + } + } + } + } +} + + +// Rolling ground with flooded hollows and flowers, and brick ledges and +// clouds above it. +static void makeMap(void) { + uint16_t tx; + uint16_t ty; + uint16_t ground; + uint16_t len; + uint16_t i; + uint16_t k; + uint8_t t; + + ground = GROUND_HIGH + 4; + for (tx = 0; tx < MAP_W; tx++) { + if (nextRandom(3) == 0 && ground < GROUND_LOW) { + ground++; + } else if (nextRandom(3) == 0 && ground > GROUND_HIGH) { + ground--; + } + for (ty = 0; ty < MAP_H; ty++) { + if (ty < ground) { + t = (ty >= WATER_LINE) ? TILE_WATER : TILE_SKY; + } else if (ty == ground) { + t = (ty >= WATER_LINE) ? TILE_STONE : TILE_GRASS; + } else if (ty < ground + STONE_DEPTH) { + t = TILE_DIRT; + } else { + t = TILE_STONE; + } + gMap[ty * MAP_W + tx] = t; + } + if (ground < WATER_LINE && nextRandom(5) == 0) { + gMap[(ground - 1) * MAP_W + tx] = TILE_FLOWER; + } + } + for (i = 0; i < LEDGES; i++) { + tx = nextRandom(MAP_W - LEDGE_MAX); + ty = (uint16_t)(LEDGE_TOP + nextRandom(GROUND_HIGH - LEDGE_TOP - LEDGE_GAP)); + len = (uint16_t)(LEDGE_MIN + nextRandom(LEDGE_MAX - LEDGE_MIN)); + for (k = 0; k < len; k++) { + gMap[ty * MAP_W + tx + k] = ((i & 1u) != 0u) ? TILE_BRICK : TILE_CLOUD; + } + } +} + + +// Paints the eight tiles in block row 0 of the screen and snaps them. +static void makeTiles(jlSurfaceT *screen) { + jlFillRect(screen, 0, 0, 8, 8, COLOR_SKY); + jlFillRect(screen, 8, 0, 8, 8, COLOR_DIRT); + jlFillRect(screen, 8, 0, 8, 3, COLOR_GRASS); + jlDrawPixel(screen, 10, 3, COLOR_GRASS); + jlDrawPixel(screen, 13, 3, COLOR_GRASS); + jlFillRect(screen, 16, 0, 8, 8, COLOR_DIRT); + jlDrawPixel(screen, 17, 2, COLOR_DIRT_DARK); + jlDrawPixel(screen, 21, 5, COLOR_DIRT_DARK); + jlFillRect(screen, 24, 0, 8, 8, COLOR_BRICK); + jlDrawLine(screen, 24, 3, 31, 3, COLOR_GREY); + jlDrawLine(screen, 24, 7, 31, 7, COLOR_GREY); + jlDrawLine(screen, 24, 0, 24, 2, COLOR_GREY); + jlDrawLine(screen, 28, 4, 28, 6, COLOR_GREY); + jlFillRect(screen, 32, 0, 8, 8, COLOR_WATER); + jlDrawLine(screen, 32, 1, 34, 1, COLOR_FOAM); + jlDrawLine(screen, 36, 5, 38, 5, COLOR_FOAM); + jlFillRect(screen, 40, 0, 8, 8, COLOR_SKY); + jlFillRect(screen, 41, 3, 6, 3, COLOR_CLOUD); + jlFillRect(screen, 43, 2, 3, 1, COLOR_CLOUD); + jlFillRect(screen, 48, 0, 8, 8, COLOR_GREY); + jlDrawPixel(screen, 50, 2, COLOR_GREY_DARK); + jlDrawPixel(screen, 53, 5, COLOR_GREY_DARK); + jlDrawPixel(screen, 49, 6, COLOR_GREY_DARK); + jlFillRect(screen, 56, 0, 8, 8, COLOR_SKY); + jlDrawLine(screen, 59, 3, 59, 7, COLOR_GRASS); + jlFillRect(screen, 58, 1, 3, 2, COLOR_PETAL); + jlTileSnap(screen, TILE_SKY, 0, &gTiles[TILE_SKY]); + jlTileSnap(screen, TILE_GRASS, 0, &gTiles[TILE_GRASS]); + jlTileSnap(screen, TILE_DIRT, 0, &gTiles[TILE_DIRT]); + jlTileSnap(screen, TILE_BRICK, 0, &gTiles[TILE_BRICK]); + jlTileSnap(screen, TILE_WATER, 0, &gTiles[TILE_WATER]); + jlTileSnap(screen, TILE_CLOUD, 0, &gTiles[TILE_CLOUD]); + jlTileSnap(screen, TILE_STONE, 0, &gTiles[TILE_STONE]); + jlTileSnap(screen, TILE_FLOWER, 0, &gTiles[TILE_FLOWER]); +} + + +// A number in [0, below), from a fixed sequence so every port builds the +// same landscape. +static uint16_t nextRandom(uint16_t below) { + gSeed = gSeed * 1103515245u + 12345u; + return (uint16_t)((gSeed >> 16) % below); +} + + +// The view's speed along one axis, pixels per second: the stick or the two +// keys, else the drift. +static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift) { + if (minus || stick < -STICK_THRESHOLD) { + return -SPEED; + } + if (plus || stick > STICK_THRESHOLD) { + return SPEED; + } + return drift; +} + + +// v moved into [0, size). +static int32_t wrap(int32_t v, int32_t size) { + while (v < 0) { + v += size; + } + while (v >= size) { + v -= size; + } + return v; +} + + +int main(void) { + jlConfigT config; + jlSurfaceT *screen; + jlSpriteT *ball; + jlTileLayerT *layer; + int32_t x; // the view's origin, 1/256 of a pixel + int32_t y; + int32_t vx; // pixels per second + int32_t vy; + uint32_t lastMillis; + uint32_t barMillis; + uint32_t now; + uint32_t elapsed; + uint16_t frames; + + config.codegenBytes = CODEGEN_BYTES; + config.noAudio = true; + if (!jlInit(&config)) { + fprintf(stderr, "jlInit failed: %s\n", jlLastError()); + return 1; + } + screen = jlStageGet(); + jlPaletteSet(screen, 0, gPalette); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); + makeTiles(screen); + makeMap(); + makeBall(); + ball = jlSpriteCreate(gBallTiles, BALL_TILES, BALL_TILES); + layer = jlTileLayerCreate(gTiles, gMap, MAP_W, MAP_H, 0, VIEW_ROWS); + if (ball == NULL || layer == NULL) { + fprintf(stderr, "out of memory\n"); + jlShutdown(); + return 1; + } + (void)jlSpriteCompile(ball); + jlSurfaceClear(screen, 0); + drawBar(screen, 0); + + x = 0; + y = (int32_t)(GROUND_HIGH - VIEW_ROWS / 2) * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT; + frames = 0; + lastMillis = jlMillisElapsed(); + barMillis = lastMillis; + for (;;) { + jlInputPoll(); + if (jlKeyPressed(JL_KEY_ESCAPE)) { + break; + } + vx = stickAxis(jlJoystickX(JL_JOYSTICK_1), jlKeyDown(JL_KEY_LEFT), jlKeyDown(JL_KEY_RIGHT), DRIFT_X); + vy = stickAxis(jlJoystickY(JL_JOYSTICK_1), jlKeyDown(JL_KEY_UP), jlKeyDown(JL_KEY_DOWN), DRIFT_Y); + + now = jlMillisElapsed(); + elapsed = now - lastMillis; + lastMillis = now; + if (elapsed > MAX_STEP_MILLIS) { + elapsed = MAX_STEP_MILLIS; + } + x = wrap(x + (vx * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_W); + y = wrap(y + (vy * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_H); + + jlTileLayerDraw(screen, layer, (uint16_t)(x >> SUBPIXEL_SHIFT), (uint16_t)(y >> SUBPIXEL_SHIFT)); + jlSpriteDraw(screen, ball, BALL_X, BALL_Y); + frames++; + if (now - barMillis >= MILLIS_PER_SECOND) { + drawBar(screen, (uint16_t)((uint32_t)frames * MILLIS_PER_SECOND / (now - barMillis))); + frames = 0; + barMillis = now; + } + jlStagePresent(); + } + + jlTileLayerDestroy(layer); + jlSpriteDestroy(ball); + jlShutdown(); + return 0; +} diff --git a/examples/serloss/serloss.c b/examples/serloss/serloss.c index 4a36b37..f0b57ca 100644 --- a/examples/serloss/serloss.c +++ b/examples/serloss/serloss.c @@ -100,10 +100,10 @@ // shadows a card's 6551 at $C0A8). #define SLTROMSEL ((volatile uint8_t *)0x00C02DL) -extern uint16_t gIigsRxOverruns; -extern uint16_t gIigsRxDropped; +extern uint16_t jlgIigsRxOverruns; +extern uint16_t jlgIigsRxDropped; #ifdef SERLOSS_LOOSE -extern void iigsIrqLatencyRelax(void); +extern void jlIigsIrqLatencyRelax(void); #endif #ifdef SERLOSS_JOY @@ -195,8 +195,8 @@ __attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequ mailbox16(MB_BREAKS, breaks); mailbox16(MB_PRESENTS, presents); mailbox16(MB_TICKS, ticks); - mailbox16(MB_OVERRUNS, gIigsRxOverruns); - mailbox16(MB_DROPPED, gIigsRxDropped); + mailbox16(MB_OVERRUNS, jlgIigsRxOverruns); + mailbox16(MB_DROPPED, jlgIigsRxDropped); mailbox16(MB_SUM, sum); mailbox16(MB_NOW, now); } @@ -315,7 +315,7 @@ int main(void) { } } #ifdef SERLOSS_LOOSE - iigsIrqLatencyRelax(); + jlIigsIrqLatencyRelax(); #endif #ifdef SERLOSS_JOY // Calibrate on the first poll, in the latency mode the rest will run in, so a still stick diff --git a/examples/sertest/sertest.c b/examples/sertest/sertest.c index 1ea323c..0435641 100644 --- a/examples/sertest/sertest.c +++ b/examples/sertest/sertest.c @@ -4,7 +4,7 @@ // // Minimal headless serial self-test -- NO jlInit (no video/graphics), so it // runs even where the graphics HAL can't (e.g. AROS on the Amiga). Opens the -// DEFAULT port (or SERTEST_DEVICE) at 9600 8N1, sends a banner (TX +// DEFAULT port (or SERTEST_DEVICE) at 9600 8N1 (or SERTEST_BAUD), sends a banner (TX // heartbeat), then echoes every received byte forever. A host serial peer // verifies banner (TX) + echo (RX). // @@ -27,6 +27,11 @@ #ifndef SERTEST_UNIT #define SERTEST_UNIT 0u #endif +// The line rate, overridable the same way (-DSERTEST_BAUD=115200u: SERFAST, +// the Win16 throughput gate's build). +#ifndef SERTEST_BAUD +#define SERTEST_BAUD 9600u +#endif static void dtrCheck(const jlSerialConfigT *cfg); static void sendMark(const char *text); @@ -79,7 +84,7 @@ int main(void) { jlSerialConfigT cfg; static const char banner[] = "JLSERTEST-READY\r\n"; - cfg.baud = 9600u; + cfg.baud = SERTEST_BAUD; cfg.dataBits = 8u; cfg.stopBits = 1u; cfg.parity = JL_SERIAL_PARITY_NONE; diff --git a/examples/tilebank/tilebank.c b/examples/tilebank/tilebank.c index 6ff9163..1625643 100644 --- a/examples/tilebank/tilebank.c +++ b/examples/tilebank/tilebank.c @@ -101,7 +101,7 @@ static uint16_t gBankPalette[PALETTE_COLORS]; static uint16_t gExpectCount = 0u; static uint16_t gExpectPalette[PALETTE_COLORS]; static uint8_t *gExpectPixels = NULL; -static jlSurfaceT *gScreen = NULL; +static jlSurfaceT *jlgScreen = NULL; static jlTileT *gTiles = NULL; static bool *gValid = NULL; @@ -143,9 +143,9 @@ static bool loadExpected(void) { static void pasteTiles(uint16_t count) { uint16_t i; - jlSurfaceClear(gScreen, FILL_COLOR); + jlSurfaceClear(jlgScreen, FILL_COLOR); for (i = 0u; i < count; i++) { - jlTilePaste(gScreen, (uint8_t)(i % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_TILE_BLOCKS_PER_ROW), &gTiles[i]); + jlTilePaste(jlgScreen, (uint8_t)(i % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_TILE_BLOCKS_PER_ROW), &gTiles[i]); } } @@ -198,16 +198,16 @@ static uint16_t refuseCheck(uint16_t *banks) { uint16_t loaded; uint16_t streamed; - jlSurfaceClear(gScreen, FILL_COLOR); - fillHash = jlSurfaceHash(gScreen); + jlSurfaceClear(jlgScreen, FILL_COLOR); + fillHash = jlSurfaceHash(jlgScreen); refused = 0u; *banks = 0u; while (refuseBankFound(name, (uint16_t)(*banks + 1u))) { (*banks)++; - jlSurfaceClear(gScreen, FILL_COLOR); + jlSurfaceClear(jlgScreen, FILL_COLOR); loaded = jlTileBankLoad(name, gTiles, gExpectCount, gValid, NULL); - streamed = jlTileBankLoadToSurface(name, gScreen, NULL); - if (loaded == 0u && !gValid[0] && streamed == 0u && jlSurfaceHash(gScreen) == fillHash) { + streamed = jlTileBankLoadToSurface(name, jlgScreen, NULL); + if (loaded == 0u && !gValid[0] && streamed == 0u && jlSurfaceHash(jlgScreen) == fillHash) { refused++; } } @@ -231,16 +231,16 @@ static bool runChecks(void) { refused = refuseCheck(&banks); - jlSurfaceClear(gScreen, FILL_COLOR); - streamed = jlTileBankLoadToSurface(OWN_BANK, gScreen, gBankPalette); + jlSurfaceClear(jlgScreen, FILL_COLOR); + streamed = jlTileBankLoadToSurface(OWN_BANK, jlgScreen, gBankPalette); streamOk = streamed == gExpectCount && stageMatches(streamed); - streamHash = jlSurfaceHash(gScreen); + streamHash = jlSurfaceHash(jlgScreen); paletteOk = memcmp(gBankPalette, gExpectPalette, sizeof(gBankPalette)) == 0; pasted = jlTileBankLoad(OWN_BANK, gTiles, gExpectCount, gValid, NULL); pasteTiles(pasted); pasteOk = pasted == gExpectCount && stageMatches(pasted); - pasteHash = jlSurfaceHash(gScreen); + pasteHash = jlSurfaceHash(jlgScreen); pass = streamOk && pasteOk && paletteOk && streamHash == pasteHash && banks > 0u && refused == banks; jlLogF("TILEBANK: %s tiles=%u streamed=%u pasted=%u palette=%s streamHash=%08lX pasteHash=%08lX refused=%u/%u", pass ? "PASS" : "FAIL", (unsigned)gExpectCount, (unsigned)streamed, (unsigned)pasted, paletteOk ? "ok" : "wrong", (unsigned long)streamHash, (unsigned long)pasteHash, (unsigned)refused, (unsigned)banks); @@ -258,10 +258,10 @@ static void showVerdict(bool pass) { } colors[LAMP_PASS_COLOR] = LAMP_PASS_RGB; colors[LAMP_FAIL_COLOR] = LAMP_FAIL_RGB; - jlPaletteSet(gScreen, 0u, gBankPalette); - jlPaletteSet(gScreen, LAMP_PALETTE, colors); - jlScbSetRange(gScreen, LAMP_Y, LAMP_Y + LAMP_H - 1, LAMP_PALETTE); - jlFillRect(gScreen, LAMP_X, LAMP_Y, LAMP_W, LAMP_H, pass ? LAMP_PASS_COLOR : LAMP_FAIL_COLOR); + jlPaletteSet(jlgScreen, 0u, gBankPalette); + jlPaletteSet(jlgScreen, LAMP_PALETTE, colors); + jlScbSetRange(jlgScreen, LAMP_Y, LAMP_Y + LAMP_H - 1, LAMP_PALETTE); + jlFillRect(jlgScreen, LAMP_X, LAMP_Y, LAMP_W, LAMP_H, pass ? LAMP_PASS_COLOR : LAMP_FAIL_COLOR); jlStagePresent(); } @@ -282,7 +282,7 @@ static bool stageMatches(uint16_t tilesDrawn) { if (block < tilesDrawn) { want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % JL_TILE_PIXELS_PER_SIDE) * JL_TILE_PIXELS_PER_SIDE + x % JL_TILE_PIXELS_PER_SIDE]; } - if (jlSamplePixel(gScreen, x, y) != want) { + if (jlSamplePixel(jlgScreen, x, y) != want) { return false; } } @@ -301,7 +301,7 @@ int main(void) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return EXIT_FAILURE; } - gScreen = jlStageGet(); + jlgScreen = jlStageGet(); pass = loadExpected(); if (pass) { diff --git a/examples/uber/uber.c b/examples/uber/uber.c index cbf2ad2..b19632a 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -88,7 +88,7 @@ #define UBER_MB_HEARTBEAT ((volatile uint16_t *)0xE19DCAL) #define UBER_MB_TICK ((volatile uint16_t *)0xE19DCCL) // One-time layout facts for probe correlation: +8 stage pointer value, -// +12 address of uber.c's gStage static, +16 log-ring base address, +// +12 address of uber.c's jlgStage static, +16 log-ring base address, // +20 log-ring head-counter address (see src/core/debug.c's IIgs RAM // ring -- the Lua probe drains the log live through these). #define UBER_MB_MAGIC ((volatile uint16_t *)0xE19DCEL) @@ -102,13 +102,13 @@ // "JL" -- the Lua probes refuse to trust the layout words until this // appears (the SCB region holds 0x80 fill before the app writes it). #define UBER_MB_MAGIC_VAL 0x4A4Cu -extern uint8_t gJoeyLogRing[]; -extern uint16_t gJoeyLogRingHead; +extern uint8_t jlgJoeyLogRing[]; +extern uint16_t jlgJoeyLogRingHead; #define uberMbOp(_i) (*UBER_MB_OP = (uint8_t)(_i)) #define uberMbPhase(_p) (*UBER_MB_PHASE = (uint8_t)(_p)) #define uberMbBeat() (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u)) #define uberMbTick(_t) (*UBER_MB_TICK = (uint16_t)(_t)) -#define uberMbLayout() (*UBER_MB_STAGEPTR = (uint32_t)gStage, *UBER_MB_GSTAGEAT = (uint32_t)&gStage, *UBER_MB_LOGRING = (uint32_t)&gJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&gJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL) +#define uberMbLayout() (*UBER_MB_STAGEPTR = (uint32_t)jlgStage, *UBER_MB_GSTAGEAT = (uint32_t)&jlgStage, *UBER_MB_LOGRING = (uint32_t)&jlgJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&jlgJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL) #elif defined(JOEYLIB_PLATFORM_X68000) // Same mailbox words at the same addresses, for the same reason: a headless // probe needs to know WHERE the run is when the serial log has gone quiet. @@ -137,7 +137,7 @@ extern uint16_t gJoeyLogRingHead; typedef void (*OpFn)(void); -static jlSurfaceT *gStage = NULL; +static jlSurfaceT *jlgStage = NULL; static jlSpriteT *gSprite = NULL; static jlSpriteBackupT gBackup; static unsigned char gBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2))); @@ -256,7 +256,11 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t * // pixel-perfect comparison input for the cross-port validation // harness (tools/diff-uber-hashes.py). Captured against IIgs as the // golden reference; planar 68k rewrites validate by matching it. -static void timeOp(const char *name, OpFn op) { +// The window is `frames` long. settle (NULL for none) runs between the +// timing and the hash, for an op whose last call leaves a picture that +// depends on how many calls the window held (a moving origin): it redraws +// one that does not. +static void timeOpSettled(const char *name, OpFn op, OpFn settle, unsigned int frames) { unsigned long iters; unsigned long opsPerSec; unsigned long opsPerSecFr; @@ -267,7 +271,7 @@ static void timeOp(const char *name, OpFn op) { gOpIndex++; uberMbOp(gOpIndex); - iters = runForFrames(op, UBER_FRAMES, &actualFrames, &millisDelta); + iters = runForFrames(op, frames, &actualFrames, &millisDelta); if (iters == 0UL) { jlLogF("UBER: %s: 0 iters (op too slow?)", name); @@ -284,52 +288,58 @@ static void timeOp(const char *name, OpFn op) { * the jlpFrameHz fix the two agree; a divergence flags a clock bug. */ opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta; opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames; - hash = jlSurfaceHash(gStage); + if (settle != NULL) { + settle(); + } + hash = jlSurfaceHash(jlgStage); jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", name, iters, actualFrames, opsPerSec, (unsigned long)hash); jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)", name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta); } +static void timeOp(const char *name, OpFn op) { + timeOpSettled(name, op, NULL, UBER_FRAMES); +} // ----- Test ops ----- -static void op_drawPixel (void) { jlDrawPixel (gStage, 100, 100, 5); } -static void op_drawLineH (void) { jlDrawLine (gStage, 0, 50, 319, 50, 5); } -static void op_drawLineV (void) { jlDrawLine (gStage, 50, 0, 50, 199, 5); } -static void op_drawLineDiag (void) { jlDrawLine (gStage, 0, 0, 319, 199, 5); } -static void op_drawRect (void) { jlDrawRect (gStage, 10, 10, 100, 100, 5); } -static void op_drawCircleSmall (void) { jlDrawCircle (gStage, 160, 100, 16, 5); } -static void op_drawCircleLarge (void) { jlDrawCircle (gStage, 160, 100, 80, 5); } -static void op_fillRectSmall (void) { jlFillRect (gStage, 20, 20, 16, 16, 7); } -static void op_fillRectMid (void) { jlFillRect (gStage, 20, 20, 80, 80, 7); } -static void op_fillRectFull (void) { jlFillRect (gStage, 0, 0, 320, 200, 7); } -static void op_fillCircle (void) { jlFillCircle (gStage, 160, 100, 40, 7); } -static void op_samplePixel (void) { (void)jlSamplePixel(gStage, 100, 100); } -static void op_surfaceClear (void) { jlSurfaceClear (gStage, 0); } +static void op_drawPixel (void) { jlDrawPixel (jlgStage, 100, 100, 5); } +static void op_drawLineH (void) { jlDrawLine (jlgStage, 0, 50, 319, 50, 5); } +static void op_drawLineV (void) { jlDrawLine (jlgStage, 50, 0, 50, 199, 5); } +static void op_drawLineDiag (void) { jlDrawLine (jlgStage, 0, 0, 319, 199, 5); } +static void op_drawRect (void) { jlDrawRect (jlgStage, 10, 10, 100, 100, 5); } +static void op_drawCircleSmall (void) { jlDrawCircle (jlgStage, 160, 100, 16, 5); } +static void op_drawCircleLarge (void) { jlDrawCircle (jlgStage, 160, 100, 80, 5); } +static void op_fillRectSmall (void) { jlFillRect (jlgStage, 20, 20, 16, 16, 7); } +static void op_fillRectMid (void) { jlFillRect (jlgStage, 20, 20, 80, 80, 7); } +static void op_fillRectFull (void) { jlFillRect (jlgStage, 0, 0, 320, 200, 7); } +static void op_fillCircle (void) { jlFillCircle (jlgStage, 160, 100, 40, 7); } +static void op_samplePixel (void) { (void)jlSamplePixel(jlgStage, 100, 100); } +static void op_surfaceClear (void) { jlSurfaceClear (jlgStage, 0); } static void op_paletteSet(void) { static uint16_t colors[16] = { 0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF, 0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444 }; - jlPaletteSet(gStage, 0, colors); + jlPaletteSet(jlgStage, 0, colors); } -static void op_scbSetRange (void) { jlScbSetRange (gStage, 0, 199, 0); } +static void op_scbSetRange (void) { jlScbSetRange (jlgStage, 0, 199, 0); } -static void op_tileFill (void) { jlTileFill (gStage, 5, 5, 7); } -static void op_tileCopy (void) { jlTileCopy (gStage, 6, 6, gStage, 5, 5); } -static void op_tileCopyMasked (void) { jlTileCopyMasked (gStage, 7, 7, gStage, 5, 5, 0); } -static void op_tilePaste (void) { jlTilePaste (gStage, 8, 8, &gTileScratch); } -static void op_tileSnap (void) { jlTileSnap (gStage, 5, 5, &gTileScratch); } +static void op_tileFill (void) { jlTileFill (jlgStage, 5, 5, 7); } +static void op_tileCopy (void) { jlTileCopy (jlgStage, 6, 6, jlgStage, 5, 5); } +static void op_tileCopyMasked (void) { jlTileCopyMasked (jlgStage, 7, 7, jlgStage, 5, 5, 0); } +static void op_tilePaste (void) { jlTilePaste (jlgStage, 8, 8, &gTileScratch); } +static void op_tileSnap (void) { jlTileSnap (jlgStage, 5, 5, &gTileScratch); } static int16_t gSpriteX = 40; static int16_t gSpriteY = 30; -static void op_spriteSave (void) { jlSpriteSaveUnder (gStage, gSprite, gSpriteX, gSpriteY, &gBackup); } -static void op_spriteDraw (void) { jlSpriteDraw (gStage, gSprite, gSpriteX, gSpriteY); } -static void op_spriteRestore (void) { jlSpriteRestoreUnder(gStage, &gBackup); } -static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (gStage, gSprite, gSpriteX, gSpriteY, &gBackup); } +static void op_spriteSave (void) { jlSpriteSaveUnder (jlgStage, gSprite, gSpriteX, gSpriteY, &gBackup); } +static void op_spriteDraw (void) { jlSpriteDraw (jlgStage, gSprite, gSpriteX, gSpriteY); } +static void op_spriteRestore (void) { jlSpriteRestoreUnder(jlgStage, &gBackup); } +static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (jlgStage, gSprite, gSpriteX, gSpriteY, &gBackup); } static void op_stagePresent (void) { jlStagePresent(); } @@ -343,7 +353,7 @@ static void op_audioFrameTick (void) { jlAudioFrameTick(); } static void op_audioIsPlaying (void) { (void)jlAudioIsPlayingMod(); } static void op_surfaceMarkDirty(void) { // jlDrawPixel already marks; use fill instead - jlFillRect(gStage, 0, 0, 32, 32, 0); } + jlFillRect(jlgStage, 0, 0, 32, 32, 0); } // ----- Phase 0 workload ops (claudeDocs/NATIVE-PERF-PLAN.md item 3) ----- @@ -357,7 +367,7 @@ static void op_surfaceMarkDirty(void) { // jlDrawPixel already marks; use fill i #define UBER_UNALIGNED_X 71 #define UBER_UNALIGNED_Y 120 -static void op_spriteDrawUnaligned(void) { jlSpriteDraw(gStage, gSprite, UBER_UNALIGNED_X, UBER_UNALIGNED_Y); } +static void op_spriteDrawUnaligned(void) { jlSpriteDraw(jlgStage, gSprite, UBER_UNALIGNED_X, UBER_UNALIGNED_Y); } // All 16 x phases in ONE call so the op is idempotent: per-port @@ -373,7 +383,7 @@ static void op_spriteDrawSweep(void) { int16_t x; for (x = UBER_SWEEP_X0; x < (int16_t)(UBER_SWEEP_X0 + 16); x++) { - jlSpriteDraw(gStage, gSprite, x, UBER_SWEEP_Y); + jlSpriteDraw(jlgStage, gSprite, x, UBER_SWEEP_Y); } } @@ -395,7 +405,7 @@ static jlTileT gTileSet[2]; static uint8_t gTileMapIndices[UBER_TILEMAP_W * UBER_TILEMAP_H]; static void op_tileMapPasteBatch(void) { - jlTileMapPaste(gStage, UBER_TILEMAP_X0, UBER_TILEMAP_B_Y0, UBER_TILEMAP_W, UBER_TILEMAP_H, gTileSet, gTileMapIndices); + jlTileMapPaste(jlgStage, UBER_TILEMAP_X0, UBER_TILEMAP_B_Y0, UBER_TILEMAP_W, UBER_TILEMAP_H, gTileSet, gTileMapIndices); } @@ -406,9 +416,9 @@ static void op_tileMapPaste(void) { for (ty = 0; ty < UBER_TILEMAP_H; ty++) { for (tx = 0; tx < UBER_TILEMAP_W; tx++) { if (((tx + ty) & 1u) == 0u) { - jlTilePaste(gStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapA); + jlTilePaste(jlgStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapA); } else { - jlTilePaste(gStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapB); + jlTilePaste(jlgStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapB); } } } @@ -443,12 +453,12 @@ static const int16_t gFrameSpriteY[UBER_FRAME_SPRITES] = { 80, 96, 112 }; static void paintFrameStatic(void) { uint8_t t; - jlFillRect(gStage, UBER_FRAME_RX, UBER_FRAME_RY, UBER_FRAME_RW, UBER_FRAME_RH, UBER_FRAME_BG); + jlFillRect(jlgStage, UBER_FRAME_RX, UBER_FRAME_RY, UBER_FRAME_RW, UBER_FRAME_RH, UBER_FRAME_BG); for (t = 0; t < UBER_FRAME_TILES; t++) { if ((t & 1u) == 0u) { - jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapA); + jlTilePaste(jlgStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapA); } else { - jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapB); + jlTilePaste(jlgStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapB); } } } @@ -462,10 +472,10 @@ static void gameFrameBody(void) { paintFrameStatic(); for (i = 0; i < UBER_FRAME_SPRITES; i++) { - jlSpriteSaveAndDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i], &gFrameBackups[i]); + jlSpriteSaveAndDraw(jlgStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i], &gFrameBackups[i]); } for (i = UBER_FRAME_SPRITES - 1; i >= 0; i--) { - jlSpriteRestoreUnder(gStage, &gFrameBackups[i]); + jlSpriteRestoreUnder(jlgStage, &gFrameBackups[i]); } } @@ -492,15 +502,87 @@ static void op_gameFrameClean(void) { int16_t i; for (i = 0; i < UBER_FRAME_SPRITES; i++) { - jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16); + jlSurfaceCopyRect(jlgStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16); } for (i = 0; i < UBER_FRAME_SPRITES; i++) { - jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]); + jlSpriteDraw(jlgStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]); } jlStagePresent(); } +// ----- Tile layer rows (scrolling) ----- +// +// A map bigger than the view both ways, of tiles snapped from portable +// draws (a jlTileT is target-native, so only a snap round-trips), with a +// few JL_TILE_MAP_SKIP cells. The timed rows scroll it diagonally a pixel +// a call: every pixel phase, and a new tile column and row every 8 calls. +// A moving origin would make the post-window hash depend on the call +// count, so they settle on a fixed origin first. checkTileLayer (in the +// UBER-CHK block) walks moves of every size and both seams. A scroll frame +// takes several display frames on the slow ports, so the rows time over a +// longer window than the rest. +#define UBER_LAYER_FRAMES (UBER_FRAMES * 8u) +#define UBER_LAYER_MAP_W 64 +#define UBER_LAYER_MAP_H 40 +#define UBER_LAYER_TILES 8 +#define UBER_LAYER_SETTLE_X 37 +#define UBER_LAYER_SETTLE_Y 21 + +static jlTileT gLayerTiles[UBER_LAYER_TILES]; +static uint8_t gLayerMap[UBER_LAYER_MAP_W * UBER_LAYER_MAP_H]; +static jlTileLayerT *gLayer = NULL; +static uint16_t gLayerX; +static uint16_t gLayerY; + + +// Paints and snaps the tiles (block row 0 of the stage), and fills the map. +static void layerData(void) { + uint8_t t; + int16_t x; + uint16_t tx; + uint16_t ty; + + for (t = 0; t < UBER_LAYER_TILES; t++) { + x = (int16_t)(t * 8); + jlFillRect(jlgStage, x, 0, 8, 8, (uint8_t)((t * 2u + 1u) & 15u)); + jlFillRect(jlgStage, x, 0, 3, 5, (uint8_t)((t * 5u + 2u) & 15u)); + jlDrawLine(jlgStage, x, 7, (int16_t)(x + 7), 0, (uint8_t)((t + 9u) & 15u)); + jlDrawPixel(jlgStage, (int16_t)(x + 6), 1, (uint8_t)((t * 3u + 4u) & 15u)); + jlTileSnap(jlgStage, t, 0, &gLayerTiles[t]); + } + for (ty = 0; ty < UBER_LAYER_MAP_H; ty++) { + for (tx = 0; tx < UBER_LAYER_MAP_W; tx++) { + if ((tx * 7u + ty * 11u) % 23u == 0u) { + gLayerMap[ty * UBER_LAYER_MAP_W + tx] = JL_TILE_MAP_SKIP; + } else { + gLayerMap[ty * UBER_LAYER_MAP_W + tx] = (uint8_t)((tx * 5u + ty * 3u + (tx ^ ty)) % UBER_LAYER_TILES); + } + } + } +} + + +static void op_layerDraw(void) { + gLayerX++; + gLayerY++; + jlTileLayerDraw(jlgStage, gLayer, gLayerX, gLayerY); +} + + +static void op_layerFrame(void) { + gLayerX++; + gLayerY++; + jlTileLayerDraw(jlgStage, gLayer, gLayerX, gLayerY); + jlStagePresent(); +} + + +static void settleLayer(void) { + jlTileLayerDraw(jlgStage, gLayer, UBER_LAYER_SETTLE_X, UBER_LAYER_SETTLE_Y); +} + + #ifdef UBER_PROBE // NATIVE-PERF Phase 6 W0 probe rows. NOT part of the golden contract: // this block only exists in the scratch probe binary (-DUBER_PROBE, @@ -511,15 +593,22 @@ static void op_gameFrameClean(void) { // logged around each row to split O(rows) scan cost from O(bytes) // copy cost and to expose over-copy. // Unconditional since W2 item 0: the IIgs present asm now increments -// the counters too (iigsBlitStageToShr), so every port logs deltas. -extern uint32_t gPresentCopiedBytes; -extern uint32_t gPresentScannedRows; +// the counters too (jlIigsBlitStageToShr), so every port logs deltas. +extern uint32_t jlgPresentCopiedBytes; +extern uint32_t jlgPresentScannedRows; +extern uint8_t jlgPresentTelemetry; #ifdef JOEYLIB_PLATFORM_IIGS -extern void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord); -#define probeMarkRows(_y0, _y1, _w0, _w1) iigsMarkDirtyRowsInner((uint16_t)(_y0), (uint16_t)(_y1), (uint16_t)(_w0), (uint16_t)(_w1)) +extern void jlIigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord); +#define probeMarkRows(_y0, _y1, _w0, _w1) jlIigsMarkDirtyRowsInner((uint16_t)(_y0), (uint16_t)(_y1), (uint16_t)(_w0), (uint16_t)(_w1)) +#elif defined(JL_CPU_68000) +// The 68000 ports' marker is asm (src/m68k/stageBands.s), handed the rows. +extern uint8_t jlgStageMinWord[]; +extern uint8_t jlgStageMaxWord[]; +extern void jlStageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, uint8_t minWord, uint8_t maxWord); +#define probeMarkRows(_y0, _y1, _w0, _w1) jlStageBandsMark68k(&jlgStageMinWord[_y0], &jlgStageMaxWord[_y0], (uint16_t)((_y1) - (_y0)), (uint8_t)(_w0), (uint8_t)(_w1)) #else -extern void surfaceMarkDirtyRows(uint16_t yStart, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); -#define probeMarkRows(_y0, _y1, _w0, _w1) surfaceMarkDirtyRows((uint16_t)(_y0), (uint16_t)(_y1), (uint8_t)(_w0), (uint8_t)(_w1)) +extern void jlSurfaceMarkDirtyRows(uint16_t yStart, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); +#define probeMarkRows(_y0, _y1, _w0, _w1) jlSurfaceMarkDirtyRows((uint16_t)(_y0), (uint16_t)(_y1), (uint8_t)(_w0), (uint8_t)(_w1)) #endif // Same w65816 register-allocator escape hatch as DRAWSHOWCASE_ATTR: @@ -601,31 +690,31 @@ static jlSpriteBackupT gProbeSpriteBackup; static unsigned char gProbeSpriteBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2))); static void PROBE_ATTR op_probeSpriteSaveVal(void) { - jlSpriteSaveUnder(gStage, gSprite, -100, -100, &gProbeSpriteBackup); + jlSpriteSaveUnder(jlgStage, gSprite, -100, -100, &gProbeSpriteBackup); } static void PROBE_ATTR op_probeSpriteRestoreVal(void) { - jlSpriteRestoreUnder(gStage, &gProbeSpriteBackup); + jlSpriteRestoreUnder(jlgStage, &gProbeSpriteBackup); } static void PROBE_ATTR op_probeSpriteSaveG16(void) { - jlSpriteSaveUnder(gStage, gSprite, 48, 30, &gProbeSpriteBackup); + jlSpriteSaveUnder(jlgStage, gSprite, 48, 30, &gProbeSpriteBackup); } // P7-1 restore-mark cost isolation (scoping the restore-mark asm // fusion, 2026-07-11). The compiled restore fast path runs body + // dispatch identically whether the destination is the stage or a work -// surface, but spriteMarkDirty early-returns on s != gStage -- so a +// surface, but spriteMarkDirty early-returns on s != jlgStage -- so a // restore to a NON-stage surface runs the whole call MINUS the 16-row // dirty-band widen. Differencing the golden jlSpriteRestoreUnder row -// (restores gBackup to gStage -> marks) against this row (restores an +// (restores gBackup to jlgStage -> marks) against this row (restores an // identical class-1 backup to gProbeRestoreWork -> mark skipped) yields // the mark body + its call overhead: the UPPER BOUND on what fusing the // mark into the emitted asm could recover (fusion keeps the widen, only -// removes the C call boundary). Dedicated backup + surface keep gStage +// removes the C call boundary). Dedicated backup + surface keep jlgStage // and gBackup untouched, so the row is inert to the golden hashes. static jlSurfaceT *gProbeRestoreWork = NULL; static jlSpriteBackupT gProbeRestoreBackup; @@ -638,12 +727,12 @@ static void PROBE_ATTR op_probeRestoreWork(void) { // P7-1 trusted-restore lever: jlSpriteRestoreUnderTrusted skips the // geometry-validation chain. Restores the SAME valid class-1 backup to -// gStage as the golden jlSpriteRestoreUnder row (same body + inline +// jlgStage as the golden jlSpriteRestoreUnder row (same body + inline // mark), so the difference vs that row is exactly the validation cost // the trusted entry saves. Hash-inert: writes the saved-under pixels -// back to gStage (idempotent) behind UBER_PROBE. +// back to jlgStage (idempotent) behind UBER_PROBE. static void PROBE_ATTR op_probeRestoreTrusted(void) { - jlSpriteRestoreUnderTrusted(gStage, &gProbeRestoreBackup); + jlSpriteRestoreUnderTrusted(jlgStage, &gProbeRestoreBackup); } @@ -652,12 +741,12 @@ static void PROBE_ATTR op_probeRestoreTrusted(void) { // valid backup), so each difference vs its golden row is the validation // the trusted entry skips. Hash-inert behind UBER_PROBE. static void PROBE_ATTR op_probeSaveTrusted(void) { - jlSpriteSaveUnderTrusted(gStage, gSprite, 40, 30, &gProbeRestoreBackup); + jlSpriteSaveUnderTrusted(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup); } static void PROBE_ATTR op_probeSaveAndDrawTrusted(void) { - jlSpriteSaveAndDrawTrusted(gStage, gSprite, 40, 30, &gProbeRestoreBackup); + jlSpriteSaveAndDrawTrusted(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup); } @@ -665,7 +754,7 @@ static void PROBE_ATTR op_probeSaveAndDrawTrusted(void) { // (sprite at 40,30) so the difference vs that row is exactly the // validation the trusted entry skips. Hash-inert behind UBER_PROBE. static void PROBE_ATTR op_probeDrawTrusted(void) { - jlSpriteDrawTrusted(gStage, gSprite, 40, 30); + jlSpriteDrawTrusted(jlgStage, gSprite, 40, 30); } @@ -680,8 +769,10 @@ static void PROBE_ATTR op_probeDrawTrusted(void) { // The band arrays are port-neutral, so the value must also be // IDENTICAL ACROSS PORTS -- the C-mark ports are the reference any // fused asm mark must reproduce. -extern uint8_t gStageMinWord[JL_SURFACE_HEIGHT]; -extern uint8_t gStageMaxWord[JL_SURFACE_HEIGHT]; +extern uint8_t jlgStageMinWord[JL_SURFACE_HEIGHT]; +extern uint8_t jlgStageMaxWord[JL_SURFACE_HEIGHT]; +// A clean row's band minimum (surfaceInternal.h STAGE_DIRTY_CLEAN_MIN). +#define UBER_BAND_CLEAN_MIN 0xFFu static uint32_t PROBE_ATTR probeBandFold(void) { uint16_t y; @@ -689,21 +780,22 @@ static uint32_t PROBE_ATTR probeBandFold(void) { sum = 0; for (y = 0; y < JL_SURFACE_HEIGHT; y++) { - sum = (uint32_t)(sum * 33u + gStageMinWord[y]); - sum = (uint32_t)(sum * 33u + gStageMaxWord[y]); + sum = (uint32_t)(sum * 33u + jlgStageMinWord[y]); + sum = (uint32_t)(sum * 33u + jlgStageMaxWord[y]); } return sum; } + static void PROBE_ATTR op_probeSpriteBandSum(void) { uint32_t sum; jlStagePresent(); gameFrameBody(); - jlSpriteDraw(gStage, gSprite, -8, 100); - jlSpriteDraw(gStage, gSprite, 150, -8); - jlSpriteDraw(gStage, gSprite, 150, 189); - jlSpriteDraw(gStage, gSprite, 71, 120); + jlSpriteDraw(jlgStage, gSprite, -8, 100); + jlSpriteDraw(jlgStage, gSprite, 150, -8); + jlSpriteDraw(jlgStage, gSprite, 150, 189); + jlSpriteDraw(jlgStage, gSprite, 71, 120); sum = probeBandFold(); jlLogF("UBER-PROBE: spriteBandSum=%lu", (unsigned long)sum); } @@ -716,19 +808,19 @@ static void PROBE_ATTR op_probeSpriteBandSum(void) { // batched-mark callers of the NoMark twin (jlDrawRect outline, // jlFillCircle spans). Cross-port identity is the gate: the three // C-mark ports produce the reference band state the IIgs fused asm -// mark (stageRectMark) must reproduce exactly. +// mark (jlStageRectMark) must reproduce exactly. static void PROBE_ATTR op_probeFillBandSum(void) { uint32_t sum; jlStagePresent(); - jlFillRect(gStage, 13, 7, 51, 3, 5); - jlFillRect(gStage, 3, 5, 2, 4, 6); - jlFillRect(gStage, 2, 9, 1, 1, 7); - jlFillRect(gStage, 319, 0, 1, 1, 8); - jlFillRect(gStage, 1, 20, 3, 2, 9); - jlFillRect(gStage, 40, 40, 16, 16, 10); - jlDrawRect(gStage, 60, 60, 100, 100, 11); - jlFillCircle(gStage, 200, 120, 30, 12); + jlFillRect(jlgStage, 13, 7, 51, 3, 5); + jlFillRect(jlgStage, 3, 5, 2, 4, 6); + jlFillRect(jlgStage, 2, 9, 1, 1, 7); + jlFillRect(jlgStage, 319, 0, 1, 1, 8); + jlFillRect(jlgStage, 1, 20, 3, 2, 9); + jlFillRect(jlgStage, 40, 40, 16, 16, 10); + jlDrawRect(jlgStage, 60, 60, 100, 100, 11); + jlFillCircle(jlgStage, 200, 120, 30, 12); sum = probeBandFold(); jlLogF("UBER-PROBE: fillBandSum=%lu", (unsigned long)sum); } @@ -792,12 +884,12 @@ static void PROBE_ATTR probeShrTailCheck(uint16_t *badScb, uint16_t *badPal, uin *badPal = 0; *firstBadPal = SHR_TAIL_NONE; for (line = 0; line < SHR_TAIL_SCB_BYTES; line++) { - if (shrScb[line] != jlScbGet(gStage, line)) { + if (shrScb[line] != jlScbGet(jlgStage, line)) { (*badScb)++; } } for (p = 0; p < SHR_TAIL_PALETTES; p++) { - jlPaletteGet(gStage, (uint8_t)p, want); + jlPaletteGet(jlgStage, (uint8_t)p, want); for (c = 0; c < SHR_TAIL_COLORS; c++) { if (shrPal[(p * SHR_TAIL_COLORS) + c] != want[c]) { (*badPal)++; @@ -824,8 +916,8 @@ static void PROBE_ATTR probeShrTailSeed(void) { for (c = 0; c < SHR_TAIL_COLORS; c++) { colors[c] = (uint16_t)(((p & 0x0Fu) << 8) | ((c & 0x0Fu) << 4) | ((p + c) & 0x0Fu)); } - jlPaletteSet(gStage, (uint8_t)p, colors); - jlScbSetRange(gStage, (uint16_t)(p * 12u), (uint16_t)((p * 12u) + 11u), (uint8_t)p); + jlPaletteSet(jlgStage, (uint8_t)p, colors); + jlScbSetRange(jlgStage, (uint16_t)(p * 12u), (uint16_t)((p * 12u) + 11u), (uint8_t)p); } } @@ -834,6 +926,7 @@ static void PROBE_ATTR op_probeShrParity(void) { const uint8_t *stagePx; const uint8_t *shrPx; uint16_t badRows; + uint16_t staleRows; uint16_t firstBad; uint16_t badScbUp; uint16_t badPalUp; @@ -844,25 +937,29 @@ static void PROBE_ATTR op_probeShrParity(void) { uint16_t x; uint16_t y; - jlSurfaceClear(gStage, 3); + jlSurfaceClear(jlgStage, 3); jlStagePresent(); // 5 chunks x 40-row full runs probeMarkRows(0, 1, 0, 79); jlStagePresent(); // 1-row full run, top edge probeMarkRows(199, 200, 10, 60); jlStagePresent(); // 1-row computed-entry run, bottom edge - jlFillRect(gStage, 240, 50, 80, 3, 9); + jlFillRect(jlgStage, 240, 50, 80, 3, 9); jlStagePresent(); // words 60..79, W=20 -> MVN both rules - jlFillRect(gStage, 0, 55, 64, 1, 10); + jlFillRect(jlgStage, 0, 55, 64, 1, 10); jlStagePresent(); // words 0..15, W=16 -> old MVN, NEW SLAM - jlFillRect(gStage, 160, 57, 160, 1, 11); + jlFillRect(jlgStage, 160, 57, 160, 1, 11); jlStagePresent(); // words 40..79, W=40, 80 > 60 -> old slam, NEW MVN - jlFillRect(gStage, 0, 60, 144, 2, 4); + jlFillRect(jlgStage, 0, 60, 144, 2, 4); jlStagePresent(); // words 0..35 -> computed entry probeMarkRows(38, 44, 0, 79); jlStagePresent(); // run clipped at the 39/40 chunk edge - jlFillRect(gStage, 32, 100, 256, 6, 12); - jlFillRect(gStage, 300, 106, 16, 5, 13); + jlFillRect(jlgStage, 32, 100, 256, 6, 12); + jlFillRect(jlgStage, 300, 106, 16, 5, 13); jlStagePresent(); // wide run + narrow MVN in one present + jlDrawPixel(jlgStage, 77, 13, 7); + jlStagePresent(); // one pixel mid-group in a clean group (asm mark) + op_tileMapPasteBatch(); + jlStagePresent(); // a tile map (asm mark) stagePx = (const uint8_t *)0x012000L; shrPx = (const uint8_t *)0xE12000L; badRows = 0; @@ -882,6 +979,21 @@ static void PROBE_ATTR op_probeShrParity(void) { } jlLogF("UBER-PROBE: shrParity %s badRows=%u firstBad=%u", (badRows == 0u) ? "OK" : "FAIL", (unsigned int)badRows, (unsigned int)firstBad); + // Every band CLEAN after the presents: a mark that skips its group's + // flag (jlgStageDirtyGroup) is never cleared, and its rows are copied + // again at every present. + staleRows = 0; + firstBad = 0xFFFFu; + for (y = 0; y < JL_SURFACE_HEIGHT; y++) { + if (jlgStageMinWord[y] != UBER_BAND_CLEAN_MIN) { + staleRows++; + if (firstBad == 0xFFFFu) { + firstBad = y; + } + } + } + jlLogF("UBER-PROBE: shrBands %s staleRows=%u firstStale=%u", (staleRows == 0u) ? "OK" : "FAIL", (unsigned int)staleRows, (unsigned int)firstBad); + // Tail phase 1: seed all 16 palettes + the SCB bands, then present. // jlPaletteSet / jlScbSetRange set the dirty flags, so THIS present // is the one that uploads them. A mismatch here means the C-side @@ -903,7 +1015,7 @@ static void PROBE_ATTR op_probeShrParity(void) { jlStagePresent(); probeMarkRows(196, 200, 70, 79); jlStagePresent(); - jlFillRect(gStage, 0, 180, 320, 20, 6); + jlFillRect(jlgStage, 0, 180, 320, 20, 6); jlStagePresent(); probeShrTailCheck(&badScbSlam, &badPalSlam, &firstPalSlam); jlLogF("UBER-PROBE: shrTailSlam %s badScb=%u badPal=%u firstPal=%u", ((badScbSlam | badPalSlam) == 0u) ? "OK" : "FAIL", (unsigned int)badScbSlam, (unsigned int)badPalSlam, (unsigned int)firstPalSlam); @@ -929,22 +1041,22 @@ static void PROBE_ATTR op_probeDisplayCheck(void) { uint32_t afterSettle; uint32_t afterIdle; - jlFillRect(gStage, 0, 50, 320, 100, 3); + jlFillRect(jlgStage, 0, 50, 320, 100, 3); jlStagePresent(); - jlFillRect(gStage, 100, 60, 40, 10, 9); + jlFillRect(jlgStage, 100, 60, 40, 10, 9); jlStagePresent(); - afterB = jlpProbeDisplayMismatch(gStage); + afterB = jlpProbeDisplayMismatch(jlgStage); jlStagePresent(); - afterSettle = jlpProbeDisplayMismatch(gStage); + afterSettle = jlpProbeDisplayMismatch(jlgStage); jlStagePresent(); - afterIdle = jlpProbeDisplayMismatch(gStage); + afterIdle = jlpProbeDisplayMismatch(jlgStage); jlLogF("UBER-PROBE: displayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle); } #endif #ifdef JOEYLIB_PLATFORM_AMIGA -extern void amigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum); +extern void jlAmigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum); // P11 (W2): display-side present verification. After EVERY present in // a battery covering each run arm -- scattered THIN runs, a full-width @@ -961,7 +1073,7 @@ static uint8_t PROBE_ATTR probePageSumCheck(void) { uint32_t shadowSum; jlStagePresent(); - amigaProbePresentPageSums(gStage, &frontSum, &shadowSum); + jlAmigaProbePresentPageSums(jlgStage, &frontSum, &shadowSum); return (uint8_t)(frontSum != shadowSum); } @@ -970,22 +1082,22 @@ static void PROBE_ATTR op_probePresentPageSum(void) { uint8_t fails; fails = 0; - jlSurfaceClear(gStage, 3); + jlSurfaceClear(jlgStage, 3); fails = (uint8_t)(fails + probePageSumCheck()); - jlFillRect(gStage, 48, 80, 16, 16, 5); - jlFillRect(gStage, 144, 96, 16, 16, 9); - jlFillRect(gStage, 240, 112, 16, 16, 12); + jlFillRect(jlgStage, 48, 80, 16, 16, 5); + jlFillRect(jlgStage, 144, 96, 16, 16, 9); + jlFillRect(jlgStage, 240, 112, 16, 16, 12); fails = (uint8_t)(fails + probePageSumCheck()); - jlFillRect(gStage, 0, 0, 320, 1, 7); - jlFillRect(gStage, 316, 199, 4, 1, 8); - jlFillRect(gStage, 100, 150, 8, 1, 4); - jlFillRect(gStage, 108, 151, 8, 1, 5); - jlFillRect(gStage, 90, 152, 40, 1, 6); + jlFillRect(jlgStage, 0, 0, 320, 1, 7); + jlFillRect(jlgStage, 316, 199, 4, 1, 8); + jlFillRect(jlgStage, 100, 150, 8, 1, 4); + jlFillRect(jlgStage, 108, 151, 8, 1, 5); + jlFillRect(jlgStage, 90, 152, 40, 1, 6); fails = (uint8_t)(fails + probePageSumCheck()); fails = (uint8_t)(fails + probePageSumCheck()); - jlFillRect(gStage, 8, 8, 16, 16, 10); + jlFillRect(jlgStage, 8, 8, 16, 16, 10); fails = (uint8_t)(fails + probePageSumCheck()); - jlFillRect(gStage, 288, 176, 16, 16, 11); + jlFillRect(jlgStage, 288, 176, 16, 16, 11); fails = (uint8_t)(fails + probePageSumCheck()); // Narrow-tall column (P7-3 audit gap): clear + settle to isolate the // run, then a 16-px-wide x 96-row fill coalesces to ONE run whose @@ -993,10 +1105,10 @@ static void PROBE_ATTR op_probePresentPageSum(void) { // threshold with a NARROW span (blitWords ~2, modBytes ~36). The // wide present96 band (modBytes 8, blitWords 16) never exercises the // narrow modulo-blit path; this arm verifies it displays correctly. - jlSurfaceClear(gStage, 3); + jlSurfaceClear(jlgStage, 3); fails = (uint8_t)(fails + probePageSumCheck()); fails = (uint8_t)(fails + probePageSumCheck()); - jlFillRect(gStage, 48, 40, 16, 96, 13); + jlFillRect(jlgStage, 48, 40, 16, 96, 13); fails = (uint8_t)(fails + probePageSumCheck()); fails = (uint8_t)(fails + probePageSumCheck()); fails = (uint8_t)(fails + probePageSumCheck()); @@ -1006,8 +1118,55 @@ static void PROBE_ATTR op_probePresentPageSum(void) { #endif +#ifdef JOEYLIB_PLATFORM_X68000 +extern uint32_t jlX68kProbeDisplayMismatch(const jlSurfaceT *src); + +// The X68000's display-side check: the page each present draws (the one +// the next display start shows) must equal the stage's planes. It draws +// into the page not shown, which holds the frame before last, from the +// union of two frames' dirty bands, taken four rows at a time: so a tall +// band then a small rect inside it (P10's trap), one row, a staircase +// across a group boundary, both corner rows, and two windows two presents +// apart, each followed by a present and a check, then the settle and idle +// presents. Every mismatch count must be 0. +static uint32_t PROBE_ATTR probeX68kPresentCheck(void) { + jlStagePresent(); + return jlX68kProbeDisplayMismatch(jlgStage); +} + + +static void PROBE_ATTR op_probeX68kDisplayCheck(void) { + uint32_t mismatch; + + jlSurfaceClear(jlgStage, 3); + mismatch = probeX68kPresentCheck(); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 0, 50, 320, 100, 12); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 100, 60, 40, 10, 9); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 64, 33, 16, 1, 5); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 100, 150, 8, 1, 4); + jlFillRect(jlgStage, 108, 151, 8, 1, 5); + jlFillRect(jlgStage, 90, 152, 40, 1, 6); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 0, 0, 320, 1, 7); + jlFillRect(jlgStage, 316, 199, 4, 1, 8); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 8, 8, 16, 16, 10); + mismatch += probeX68kPresentCheck(); + jlFillRect(jlgStage, 288, 176, 16, 16, 11); + mismatch += probeX68kPresentCheck(); + mismatch += probeX68kPresentCheck(); + mismatch += probeX68kPresentCheck(); + jlLogF("UBER-PROBE: x68kDisplay %s (mismatch=%lu)", mismatch == 0ul ? "PASS" : "FAIL", (unsigned long)mismatch); +} +#endif + + #ifdef JOEYLIB_PLATFORM_DOS -extern uint32_t dosProbeVgaMismatch(const jlSurfaceT *src); +extern uint32_t jlDosProbeVgaMismatch(const jlSurfaceT *src); // P12 (perf-hunt-2 rank 4): DOS display-side parity -- the gate the // other three ports already had (P9/P10/P11) and DOS lacked. DOS @@ -1017,24 +1176,24 @@ extern uint32_t dosProbeVgaMismatch(const jlSurfaceT *src); // row, short expand, wrong LUT word) is invisible without this. // Battery mirrors P10: full clear, tall band, small inner rect, // settle present, idle present -- mismatch must be 0 after each. -// dosProbeVgaMismatch re-derives every byte independently of the +// jlDosProbeVgaMismatch re-derives every byte independently of the // gExpandLut fast path. static void PROBE_ATTR op_probeVgaCheck(void) { uint32_t afterB; uint32_t afterSettle; uint32_t afterIdle; - jlSurfaceClear(gStage, 3); + jlSurfaceClear(jlgStage, 3); jlStagePresent(); - jlFillRect(gStage, 0, 50, 320, 100, 3); + jlFillRect(jlgStage, 0, 50, 320, 100, 3); jlStagePresent(); - jlFillRect(gStage, 100, 60, 40, 10, 9); + jlFillRect(jlgStage, 100, 60, 40, 10, 9); jlStagePresent(); - afterB = dosProbeVgaMismatch(gStage); + afterB = jlDosProbeVgaMismatch(jlgStage); jlStagePresent(); - afterSettle = dosProbeVgaMismatch(gStage); + afterSettle = jlDosProbeVgaMismatch(jlgStage); jlStagePresent(); - afterIdle = dosProbeVgaMismatch(gStage); + afterIdle = jlDosProbeVgaMismatch(jlgStage); jlLogF("UBER-PROBE: vgaMismatch afterB=%lu afterSettle=%lu afterIdle=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle); } #endif @@ -1042,20 +1201,20 @@ static void PROBE_ATTR op_probeVgaCheck(void) { #if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE) #if defined(JOEYLIB_PLATFORM_DVX) -extern uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); -#define PROBE_WINDOW_MISMATCH(_s, _c) dvxPortProbeDisplayMismatch((_s), (_c)) +extern uint32_t jlDvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) jlDvxPortProbeDisplayMismatch((_s), (_c)) #define PROBE_WINDOW_TAG "dvxDisplayMismatch" #elif defined(JOEYLIB_HOSTED_WIN16) -extern uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); -#define PROBE_WINDOW_MISMATCH(_s, _c) hostWin16ProbeDisplayMismatch((_s), (_c)) +extern uint32_t jlHostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) jlHostWin16ProbeDisplayMismatch((_s), (_c)) #define PROBE_WINDOW_TAG "win16DisplayMismatch" #elif defined(JOEYLIB_HOSTED_WIN32) -extern uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); -#define PROBE_WINDOW_MISMATCH(_s, _c) hostWin32ProbeDisplayMismatch((_s), (_c)) +extern uint32_t jlHostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) jlHostWin32ProbeDisplayMismatch((_s), (_c)) #define PROBE_WINDOW_TAG "win32DisplayMismatch" #else -extern uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); -#define PROBE_WINDOW_MISMATCH(_s, _c) hostBeProbeDisplayMismatch((_s), (_c)) +extern uint32_t jlHostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) jlHostBeProbeDisplayMismatch((_s), (_c)) #define PROBE_WINDOW_TAG "beDisplayMismatch" #endif @@ -1063,7 +1222,7 @@ extern uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *comp // Win32, and Be presents. The golden hashes gate the stage, never what is shown. DVX // expands dirty rows through per-palette tables into the window's content // buffer (the desktop's pixel format, at the window's scale) or the -// full-screen backbuffer; dvxPortProbeDisplayMismatch derives every pixel +// full-screen backbuffer; jlDvxPortProbeDisplayMismatch derives every pixel // from the stage's own palette entries (not the present's tables) and // counts the ones that differ. The hosted backends draw only the // rectangles that changed at each show; their probes count the frame's @@ -1108,10 +1267,10 @@ static void PROBE_ATTR op_probeWindowDisplayCheck(void) { uint16_t y; for (y = 0; y < JL_SURFACE_HEIGHT; y++) { - savedScb[y] = jlScbGet(gStage, y); + savedScb[y] = jlScbGet(jlgStage, y); } - jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); - jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette); + jlPaletteGet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); + jlPaletteSet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, probePalette); // Shown before the steps, so the palette's own full redraw (Win16 // repaints its window when colours change) is behind them; and a frame // between B's presents, so each is a show of its own (Win16 shows at @@ -1119,43 +1278,43 @@ static void PROBE_ATTR op_probeWindowDisplayCheck(void) { jlStagePresent(); jlWaitVBL(); - jlSurfaceClear(gStage, 3); + jlSurfaceClear(jlgStage, 3); for (x = 0; x < JL_SURFACE_WIDTH; x++) { - jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % JL_SURFACE_COLORS_PER_PALETTE)); + jlFillRect(jlgStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % JL_SURFACE_COLORS_PER_PALETTE)); } jlStagePresent(); jlWaitVBL(); - jlFillRect(gStage, 0, 50, 320, 100, 3); + jlFillRect(jlgStage, 0, 50, 320, 100, 3); jlStagePresent(); jlWaitVBL(); - jlFillRect(gStage, 101, 61, 37, 9, 9); + jlFillRect(jlgStage, 101, 61, 37, 9, 9); jlStagePresent(); - afterB = PROBE_WINDOW_MISMATCH(gStage, &compared); + afterB = PROBE_WINDOW_MISMATCH(jlgStage, &compared); jlStagePresent(); - afterSettle = PROBE_WINDOW_MISMATCH(gStage, &compared); + afterSettle = PROBE_WINDOW_MISMATCH(jlgStage, &compared); jlStagePresent(); - afterIdle = PROBE_WINDOW_MISMATCH(gStage, &compared); - jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE); + afterIdle = PROBE_WINDOW_MISMATCH(jlgStage, &compared); + jlScbSetRange(jlgStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE); jlStagePresent(); - afterScb = PROBE_WINDOW_MISMATCH(gStage, &compared); - jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette); + afterScb = PROBE_WINDOW_MISMATCH(jlgStage, &compared); + jlPaletteSet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette); jlStagePresent(); - afterPalette = PROBE_WINDOW_MISMATCH(gStage, &compared); + afterPalette = PROBE_WINDOW_MISMATCH(jlgStage, &compared); for (y = 0; y < JL_SURFACE_HEIGHT; y++) { - jlScbSet(gStage, y, (uint8_t)(y % JL_SURFACE_PALETTE_COUNT)); + jlScbSet(jlgStage, y, (uint8_t)(y % JL_SURFACE_PALETTE_COUNT)); } jlStagePresent(); - afterAll = PROBE_WINDOW_MISMATCH(gStage, &compared); + afterAll = PROBE_WINDOW_MISMATCH(jlgStage, &compared); for (y = 0; y < JL_SURFACE_HEIGHT; y++) { - jlScbSet(gStage, y, savedScb[y]); + jlScbSet(jlgStage, y, savedScb[y]); } - jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); + jlPaletteSet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); jlStagePresent(); - afterRestore = PROBE_WINDOW_MISMATCH(gStage, &compared); - jlFillRect(gStage, 7, 21, 13, 5, 12); - jlFillRect(gStage, 251, 171, 31, 17, 6); + afterRestore = PROBE_WINDOW_MISMATCH(jlgStage, &compared); + jlFillRect(jlgStage, 7, 21, 13, 5, 12); + jlFillRect(jlgStage, 251, 171, 31, 17, 6); jlStagePresent(); - afterTwo = PROBE_WINDOW_MISMATCH(gStage, &compared); + afterTwo = PROBE_WINDOW_MISMATCH(jlgStage, &compared); jlLogF("UBER-PROBE: " PROBE_WINDOW_TAG " afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterAll=%lu afterRestore=%lu afterTwo=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterAll, (unsigned long)afterRestore, (unsigned long)afterTwo, (unsigned long)compared); } #endif @@ -1171,10 +1330,10 @@ static void PROBE_ATTR op_probeCleanNoPresent(void) { int16_t i; for (i = 0; i < UBER_FRAME_SPRITES; i++) { - jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16); + jlSurfaceCopyRect(jlgStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16); } for (i = 0; i < UBER_FRAME_SPRITES; i++) { - jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]); + jlSpriteDraw(jlgStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]); } } @@ -1183,7 +1342,7 @@ static void PROBE_ATTR op_probeCleanErases(void) { int16_t i; for (i = 0; i < UBER_FRAME_SPRITES; i++) { - jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16); + jlSurfaceCopyRect(jlgStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16); } } #endif @@ -1203,19 +1362,19 @@ static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) { uint32_t bytes0; uint32_t rows0; - bytes0 = gPresentCopiedBytes; - rows0 = gPresentScannedRows; + bytes0 = jlgPresentCopiedBytes; + rows0 = jlgPresentScannedRows; timeOp(name, fn); - jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0)); + jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu", name, (unsigned long)(jlgPresentCopiedBytes - bytes0), (unsigned long)(jlgPresentScannedRows - rows0)); // Steady-delta second pass (W2): one more call OUTSIDE the timed // window. Its single-op delta is the exact steady-state gate value // -- the timed average carries a first-iteration union carry-in on // the A/B-page ports (iteration 1 unions with the PREVIOUS row's // leftover band, e.g. P2's first present drags P1's 96-row band). - bytes0 = gPresentCopiedBytes; - rows0 = gPresentScannedRows; + bytes0 = jlgPresentCopiedBytes; + rows0 = jlgPresentScannedRows; fn(); - jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0)); + jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu", name, (unsigned long)(jlgPresentCopiedBytes - bytes0), (unsigned long)(jlgPresentScannedRows - rows0)); } #endif #endif @@ -1283,7 +1442,7 @@ static void PROBE_ATTR op_probeCompactEpoch(void) { jlSpriteT *victim; jlSpriteT *survivor; - jlSurfaceClear(gStage, 0); + jlSurfaceClear(jlgStage, 0); jlStagePresent(); victim = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); survivor = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); @@ -1297,15 +1456,15 @@ static void PROBE_ATTR op_probeCompactEpoch(void) { } return; } - jlSpriteDraw(gStage, survivor, 32, 40); + jlSpriteDraw(jlgStage, survivor, 32, 40); jlSpriteDestroy(victim); jlSpriteCompact(); - jlSpriteDraw(gStage, survivor, 96, 40); - jlSpriteDraw(gStage, survivor, 129, 60); + jlSpriteDraw(jlgStage, survivor, 96, 40); + jlSpriteDraw(jlgStage, survivor, 129, 60); jlSpriteCompact(); - jlSpriteDraw(gStage, survivor, 160, 80); + jlSpriteDraw(jlgStage, survivor, 160, 80); jlSpriteDestroy(survivor); - jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu", (unsigned long)jlSurfaceHash(gStage), (unsigned long)probeBandFold()); + jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu", (unsigned long)jlSurfaceHash(jlgStage), (unsigned long)probeBandFold()); } #endif @@ -1394,18 +1553,18 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { uint16_t held; uberMbPhase(UBER_PHASE_SHOWCASE); - jlPaletteSet(gStage, 0, gShowcasePal); - jlScbSetRange(gStage, 0, 199, 0); - jlSurfaceClear(gStage, SC_BG_COLOR); + jlPaletteSet(jlgStage, 0, gShowcasePal); + jlScbSetRange(jlgStage, 0, 199, 0); + jlSurfaceClear(jlgStage, SC_BG_COLOR); // Top strip: all 16 palette entries as swatches. for (k = 0; k < SC_PAL_COUNT; k++) { - jlFillRect(gStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k); + jlFillRect(jlgStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k); } for (cell = 0; cell < SC_CELLS; cell++) { showcaseCellRect(cell, &x, &y, &w, &h); - jlDrawRect(gStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR); + jlDrawRect(jlgStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR); ix = (int16_t)(x + SC_INSET); iy = (int16_t)(y + SC_INSET); iw = (int16_t)(w - 2 * SC_INSET); @@ -1419,51 +1578,51 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { // Pixels: a scatter of single plotted pixels. for (py = iy; py < iy + ih; py += 3) { for (px = ix; px < ix + iw; px += 3) { - jlDrawPixel(gStage, px, py, (uint8_t)(2 + ((px + py) % 14))); + jlDrawPixel(jlgStage, px, py, (uint8_t)(2 + ((px + py) % 14))); } } break; case 1: // Horizontal lines. for (k = 0; k < ih; k += 4) { - jlDrawLine(gStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14)); + jlDrawLine(jlgStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14)); } break; case 2: // Vertical lines. for (k = 0; k < iw; k += 4) { - jlDrawLine(gStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14)); + jlDrawLine(jlgStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14)); } break; case 3: // Diagonals: an X plus a fan from the center. - jlDrawLine(gStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5); - jlDrawLine(gStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6); + jlDrawLine(jlgStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5); + jlDrawLine(jlgStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6); for (k = 0; k < iw; k += 8) { - jlDrawLine(gStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8)); + jlDrawLine(jlgStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8)); } break; case 4: // Rectangle outlines, concentric. for (k = 0; 2 * k < minDim - 4; k += 5) { - jlDrawRect(gStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14)); + jlDrawRect(jlgStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14)); } break; case 5: // Filled rectangles, overlapping. - jlFillRect(gStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2); - jlFillRect(gStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4); + jlFillRect(jlgStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2); + jlFillRect(jlgStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4); break; case 6: // Circle outlines, concentric. for (r = (int16_t)(minDim / 2); r > 2; r -= 4) { - jlDrawCircle(gStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14)); + jlDrawCircle(jlgStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14)); } break; case 7: // Filled circles. - jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8); - jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 4), 5); + jlFillCircle(jlgStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8); + jlFillCircle(jlgStage, cx, cy, (uint16_t)(minDim / 4), 5); break; case 8: // Tiles: an 8x8-block checkerboard inside the cell. @@ -1473,27 +1632,27 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { byEnd = (uint8_t)((iy + ih) / 8); for (by = byStart; by < byEnd; by++) { for (bx = bxStart; bx < bxEnd; bx++) { - jlTileFill(gStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8)); + jlTileFill(jlgStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8)); } } break; case 9: // Sprite: the compiled ball at a few positions. - jlSpriteDraw(gStage, gSprite, ix, iy); - jlSpriteDraw(gStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16)); - jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8)); + jlSpriteDraw(jlgStage, gSprite, ix, iy); + jlSpriteDraw(jlgStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16)); + jlSpriteDraw(jlgStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8)); break; case 10: // Flood fill: outline a circle, then flood its interior. - jlDrawCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1); - jlFloodFill(gStage, cx, cy, 12); + jlDrawCircle(jlgStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1); + jlFloodFill(jlgStage, cx, cy, 12); break; default: // Mini scene: ground, sun, horizon, ball. - jlFillRect(gStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4); - jlFillCircle(gStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5); - jlDrawLine(gStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1); - jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16)); + jlFillRect(jlgStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4); + jlFillCircle(jlgStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5); + jlDrawLine(jlgStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1); + jlSpriteDraw(jlgStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16)); break; } } @@ -1524,7 +1683,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { // PRNG golden sequence, arena bookkeeping). static void __attribute__((noinline)) chkHash(const char *name) { - jlLogF("UBER-CHK: %s: hash=%08lX", name, (unsigned long)jlSurfaceHash(gStage)); + jlLogF("UBER-CHK: %s: hash=%08lX", name, (unsigned long)jlSurfaceHash(jlgStage)); } @@ -1539,30 +1698,30 @@ static void __attribute__((noinline)) chkPassFail(const char *name, bool pass) { // globals; Phase 1 turns these into true no-ops. static void __attribute__((noinline)) checkEdgeDraws(void) { uberMbOp(101); - jlDrawPixel(gStage, -5, 100, 5); - jlDrawPixel(gStage, 330, 100, 5); - jlDrawPixel(gStage, 100, -3, 5); - jlDrawPixel(gStage, 100, 210, 5); + jlDrawPixel(jlgStage, -5, 100, 5); + jlDrawPixel(jlgStage, 330, 100, 5); + jlDrawPixel(jlgStage, 100, -3, 5); + jlDrawPixel(jlgStage, 100, 210, 5); chkHash("edge-pixels"); // w=65535 draws phantom edges today (finding #11); h==2 exercises the // zero-height interior-edge fills; the third rect clips normally. - jlDrawRect(gStage, 10, 10, 65535u, 100, 5); - jlDrawRect(gStage, 50, 20, 30, 2, 6); - jlDrawRect(gStage, -10, 150, 340, 40, 7); + jlDrawRect(jlgStage, 10, 10, 65535u, 100, 5); + jlDrawRect(jlgStage, 50, 20, 30, 2, 6); + jlDrawRect(jlgStage, -10, 150, 340, 40, 7); chkHash("edge-drawRect"); // r=300 clips every span; r=40000 should cover the surface but draws // nothing today (finding #12). - jlFillCircle(gStage, 160, 100, 300, 4); - jlFillCircle(gStage, 160, 100, 40000u, 9); - jlDrawCircle(gStage, 10, 10, 40000u, 3); + jlFillCircle(jlgStage, 160, 100, 300, 4); + jlFillCircle(jlgStage, 160, 100, 40000u, 9); + jlDrawCircle(jlgStage, 10, 10, 40000u, 3); chkHash("edge-circles"); // Far-off-surface endpoints. The H line should fill the whole visible // row but draws nothing today (finding #72: the H/V fast path clamps // span to 320 BEFORE clipping, so a huge span anchored off-surface // clips away entirely). The diagonal clips per-pixel and is correct. - jlDrawLine(gStage, -20000, 50, 20000, 50, 2); - jlDrawLine(gStage, 60, -20000, 60, 20000, 2); - jlDrawLine(gStage, -300, -200, 620, 400, 8); + jlDrawLine(jlgStage, -20000, 50, 20000, 50, 2); + jlDrawLine(jlgStage, 60, -20000, 60, 20000, 2); + jlDrawLine(jlgStage, -300, -200, 620, 400, 8); chkHash("edge-lines"); } @@ -1576,18 +1735,18 @@ static void __attribute__((noinline)) checkClippedSprites(void) { uint32_t before; uberMbOp(102); - jlSpriteDraw(gStage, gSprite, -8, 50); - jlSpriteDraw(gStage, gSprite, 312, 50); - jlSpriteDraw(gStage, gSprite, 150, -8); - jlSpriteDraw(gStage, gSprite, 150, 192); - jlSpriteDraw(gStage, gSprite, -8, -8); - jlSpriteDraw(gStage, gSprite, 400, 100); + jlSpriteDraw(jlgStage, gSprite, -8, 50); + jlSpriteDraw(jlgStage, gSprite, 312, 50); + jlSpriteDraw(jlgStage, gSprite, 150, -8); + jlSpriteDraw(jlgStage, gSprite, 150, 192); + jlSpriteDraw(jlgStage, gSprite, -8, -8); + jlSpriteDraw(jlgStage, gSprite, 400, 100); chkHash("sprite-clipped"); - before = jlSurfaceHash(gStage); - jlSpriteSaveUnder(gStage, gSprite, -8, 100, &gBackup); - jlSpriteDraw(gStage, gSprite, -8, 100); - jlSpriteRestoreUnder(gStage, &gBackup); - chkPassFail("sprite-clip-roundtrip", jlSurfaceHash(gStage) == before); + before = jlSurfaceHash(jlgStage); + jlSpriteSaveUnder(jlgStage, gSprite, -8, 100, &gBackup); + jlSpriteDraw(jlgStage, gSprite, -8, 100); + jlSpriteRestoreUnder(jlgStage, &gBackup); + chkPassFail("sprite-clip-roundtrip", jlSurfaceHash(jlgStage) == before); } @@ -1599,11 +1758,11 @@ static void __attribute__((noinline)) checkTileMonoFlood(void) { for (i = 0; i < JL_TILE_BYTES; i++) { mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0); } - jlTilePasteMono(gStage, 10, 10, &mono, 5, 9); - jlTilePasteMono(gStage, 11, 10, &mono, 14, 0); + jlTilePasteMono(jlgStage, 10, 10, &mono, 5, 9); + jlTilePasteMono(jlgStage, 11, 10, &mono, 14, 0); chkHash("tilePasteMono"); - jlDrawRect(gStage, 240, 150, 40, 30, 1); - jlFloodFill(gStage, 250, 160, 12); + jlDrawRect(jlgStage, 240, 150, 40, 30, 1); + jlFloodFill(jlgStage, 250, 160, 12); chkHash("floodFill"); } @@ -1619,11 +1778,11 @@ static void __attribute__((noinline)) checkDrawText(void) { // 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here -- // the section-opening surface clear wiped whatever the timed tile ops // left there. - jlTileFill(gStage, 5, 5, 9); - jlTileFill(gStage, 6, 6, 3); + jlTileFill(jlgStage, 5, 5, 9); + jlTileFill(jlgStage, 6, 6, 3); asciiMap['A'] = (uint16_t)(5u | (5u << 8)); asciiMap['B'] = (uint16_t)(6u | (6u << 8)); - jlDrawText(gStage, 2, 20, gStage, asciiMap, "ABBA"); + jlDrawText(jlgStage, 2, 20, jlgStage, asciiMap, "ABBA"); chkHash("drawText"); // Out-of-range start: the entry sanitation loop (Phase 5, #22) // reproduces the historical absorb-and-wrap semantics bit-for-bit @@ -1631,7 +1790,7 @@ static void __attribute__((noinline)) checkDrawText(void) { // to (0,21) and draws) while guaranteeing jlpTileCopyMasked never // sees an out-of-range destination. Separate hash line so any // future semantic change stays isolated. - jlDrawText(gStage, 200, 20, gStage, asciiMap, "AB"); + jlDrawText(jlgStage, 200, 20, jlgStage, asciiMap, "AB"); chkHash("drawText-offgrid"); } @@ -1649,8 +1808,8 @@ static void __attribute__((noinline)) checkPalette(void) { // is forced to $000 even when the caller passes nonzero, and // conforming $0RGB entries 1..15 round-trip exactly. uberMbOp(105); - jlPaletteSet(gStage, 2, conforming); - jlPaletteGet(gStage, 2, readBack); + jlPaletteSet(jlgStage, 2, conforming); + jlPaletteGet(jlgStage, 2, readBack); ok = (readBack[0] == 0x0000u); for (i = 1; i < 16u; i++) { if (readBack[i] != conforming[i]) { @@ -1729,11 +1888,11 @@ static void __attribute__((noinline)) checkAllocator(void) { uberMbOp(117); chkPassFail("arena-churn", jlSpriteCodegenBytesUsed() == usedBefore); uberMbOp(118); - a = jlSpriteCreateFromSurface(gStage, 0, 0, 2, 2); + a = jlSpriteCreateFromSurface(jlgStage, 0, 0, 2, 2); if (a != NULL) { (void)jlSpriteCompile(a); uberMbOp(119); - jlSpriteDraw(gStage, a, 280, 20); + jlSpriteDraw(jlgStage, a, 280, 20); jlSpriteDestroy(a); } chkPassFail("sprite-from-surface", a != NULL); @@ -1758,25 +1917,146 @@ static void __attribute__((noinline)) checkTileMonoFloodExtra(void) { for (i = 0; i < JL_TILE_BYTES; i++) { monoAll.pixels[i] = comboBytes[i & 3u]; } - jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9); - jlTilePasteMono(gStage, 11, 12, &monoAll, 14, 0); + jlTilePasteMono(jlgStage, 10, 12, &monoAll, 5, 9); + jlTilePasteMono(jlgStage, 11, 12, &monoAll, 14, 0); chkHash("tilePasteMonoAll"); // Bounded flood: an enclosed color-3 box over a pre-cleared interior // so the fill stays contained and drives the bounded stop mask // (pix == boundary || pix == new) plus the planar group-skip across // several 16px groups. matchColor = boundaryColor = 3, matchEqual = 0. - jlFillRect(gStage, 40, 140, 64, 40, 0); - jlDrawRect(gStage, 44, 144, 52, 32, 3); - jlFloodFillBounded(gStage, 70, 160, 7, 3); + jlFillRect(jlgStage, 40, 140, 64, 40, 0); + jlDrawRect(jlgStage, 44, 144, 52, 32, 3); + jlFloodFillBounded(jlgStage, 70, 160, 7, 3); chkHash("floodFillBounded"); } +// Tile layers over moves of every size: a pixel, a tile, back, a jump +// across the map's seams (64x40 tiles = 512x320 pixels) both ways, a +// jump of more than a view, an origin past the map's size, and cell +// changes in and out of view. The layer is a band (rows 24-159) over a +// stage of colour 6, so the rows outside it are in every hash. One hash +// folds the stage's after each draw. +#define UBER_LAYER_CHECK_MOVES 20 + +static void __attribute__((noinline)) checkTileLayer(void) { + static const uint16_t moves[UBER_LAYER_CHECK_MOVES][2] = { + { 0, 0 }, { 1, 0 }, { 2, 1 }, { 3, 3 }, { 8, 8 }, + { 9, 15 }, { 17, 16 }, { 16, 17 }, { 15, 9 }, { 200, 5 }, + { 505, 318 }, { 511, 319 }, { 0, 0 }, { 7, 316 }, { 330, 170 }, + { 10, 10 }, { 1023, 650 }, { 4, 4 }, { 3, 2 }, { 2, 7 } + }; + jlTileLayerT *layer; + uint32_t h; + uint8_t i; + + layerData(); + jlSurfaceClear(jlgStage, 6); + layer = jlTileLayerCreate(gLayerTiles, gLayerMap, UBER_LAYER_MAP_W, UBER_LAYER_MAP_H, 3, 17); + if (layer == NULL) { + chkPassFail("tileLayer moves (layer alloc)", false); + return; + } + h = 0; + for (i = 0; i < UBER_LAYER_CHECK_MOVES; i++) { + if (i == 6) { + // In the last draw's view (origin 9,15: cells 1-41 by 1-18) and out of it. + jlTileLayerSetTile(layer, 5, 4, 3); + jlTileLayerSetTile(layer, 50, 30, JL_TILE_MAP_SKIP); + } + jlTileLayerDraw(jlgStage, layer, moves[i][0], moves[i][1]); + h = h * 31u + jlSurfaceHash(jlgStage); + } + jlLogF("UBER-CHK: tileLayer moves: hash=%08lX", (unsigned long)h); + jlTileLayerDestroy(layer); +} + + +// jlTilePasteGlyph, which nothing else here calls: every colour on both +// sides of a glyph whose rows hold every pixel pair, on the stage (each +// port's own paste, mark included) and on another surface, with colours +// above 15 (their low nibble is the colour) and the calls that must draw +// nothing -- a cell off the surface, no bits, no surface. One hash folds +// both surfaces'. +static void __attribute__((noinline)) checkTilePasteGlyph(void) { + static const uint8_t glyph[8] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u, 0x55u, 0xAAu, 0x3Cu, 0x81u }; + jlSurfaceT *other; + uint32_t h; + uint8_t c; + + uberMbOp(122); + jlSurfaceClear(jlgStage, 6); + for (c = 0; c < 16u; c++) { + jlTilePasteGlyph(jlgStage, c, 3, glyph, c, (uint8_t)(15u - c)); + jlTilePasteGlyph(jlgStage, (uint8_t)(c + 16u), 4, glyph, (uint8_t)(c | 0x30u), c); + jlTilePasteGlyph(jlgStage, (uint8_t)(c + 8u), 22, glyph, (uint8_t)(c ^ 5u), (uint8_t)(c | 0xC0u)); + } + jlTilePasteGlyph(jlgStage, 39, 24, glyph, 1, 2); + jlTilePasteGlyph(jlgStage, 40, 5, glyph, 1, 2); + jlTilePasteGlyph(jlgStage, 5, 25, glyph, 1, 2); + jlTilePasteGlyph(jlgStage, 6, 6, NULL, 1, 2); + jlTilePasteGlyph(NULL, 6, 6, glyph, 1, 2); + h = jlSurfaceHash(jlgStage); + other = jlSurfaceCreate(); + if (other == NULL) { + chkPassFail("tilePasteGlyph (surface alloc)", false); + return; + } + jlSurfaceClear(other, 9); + for (c = 0; c < 16u; c++) { + jlTilePasteGlyph(other, (uint8_t)(c * 2u), (uint8_t)(c + 2u), glyph, c, (uint8_t)(c + 7u)); + } + jlTilePasteGlyph(other, 0, 0, glyph, 15, 0); + h = h * 31u + jlSurfaceHash(other); + jlSurfaceDestroy(other); + jlLogF("UBER-CHK: tilePasteGlyph: hash=%08lX", (unsigned long)h); +} + + +// jlFillRect one row high, the shape a planar picture is pushed in, which +// UBER's timed fills never are: widths 1 to 41 at every pixel phase, all +// sixteen colours, both edges of the surface, and rows that clip or are +// empty -- on the stage and on another surface. One hash folds both. +static void __attribute__((noinline)) checkFillRectRows(void) { + jlSurfaceT *other; + uint32_t h; + uint16_t i; + + uberMbOp(123); + jlSurfaceClear(jlgStage, 5); + for (i = 0; i < 160u; i++) { + jlFillRect(jlgStage, (int16_t)((i * 37u) % 300u), (int16_t)(i + 20u), (uint16_t)(1u + (i % 41u)), 1, (uint8_t)(i & 15u)); + } + jlFillRect(jlgStage, 0, 0, 320, 1, 7); + jlFillRect(jlgStage, 316, 199, 4, 1, 8); + jlFillRect(jlgStage, 319, 10, 1, 1, 9); + jlFillRect(jlgStage, 0, 11, 1, 1, 10); + jlFillRect(jlgStage, -5, 12, 10, 1, 11); + jlFillRect(jlgStage, 310, 13, 20, 1, 12); + jlFillRect(jlgStage, 5, 200, 3, 1, 13); + jlFillRect(jlgStage, 5, 14, 0, 1, 14); + jlFillRect(jlgStage, 64, 15, 32, 1, 0x1F); + h = jlSurfaceHash(jlgStage); + other = jlSurfaceCreate(); + if (other == NULL) { + chkPassFail("fillRectRows (surface alloc)", false); + return; + } + jlSurfaceClear(other, 3); + for (i = 0; i < 64u; i++) { + jlFillRect(other, (int16_t)((i * 53u) % 310u), (int16_t)(i * 3u), (uint16_t)(1u + (i % 33u)), 1, (uint8_t)(15u - (i & 15u))); + } + h = h * 31u + jlSurfaceHash(other); + jlSurfaceDestroy(other); + jlLogF("UBER-CHK: fillRectRows: hash=%08lX", (unsigned long)h); +} + + static void __attribute__((noinline)) runCorrectnessChecks(void) { uberMbPhase(UBER_PHASE_CHECKS); jlLogF("UBER-CHK: ----- begin -----"); - jlSurfaceClear(gStage, 0); + jlSurfaceClear(jlgStage, 0); checkEdgeDraws(); checkClippedSprites(); checkTileMonoFlood(); @@ -1785,6 +2065,9 @@ static void __attribute__((noinline)) runCorrectnessChecks(void) { checkRandom(); checkAllocator(); checkTileMonoFloodExtra(); + checkTileLayer(); + checkTilePasteGlyph(); + checkFillRectRows(); // jlShutdown -> jlInit -> stale-sprite-destroy (#34) needs a teardown // of the whole library mid-run; it gets a dedicated micro-example in // Phase 1 rather than risking the benchmark's display state here. @@ -1818,8 +2101,8 @@ static void __attribute__((noinline)) runAllTests(void) { timeOp("jlSamplePixel", op_samplePixel); // Tiles. Seed scratch tile + dest cells with non-zero pixels first. - jlFillRect(gStage, 0, 0, 320, 64, 7); - jlTileSnap(gStage, 5, 5, &gTileScratch); + jlFillRect(jlgStage, 0, 0, 320, 64, 7); + jlTileSnap(jlgStage, 5, 5, &gTileScratch); timeOp("jlTileFill", op_tileFill); timeOp("jlTileCopy", op_tileCopy); timeOp("jlTileCopyMasked", op_tileCopyMasked); @@ -1828,7 +2111,7 @@ static void __attribute__((noinline)) runAllTests(void) { // Sprites. Background must be non-empty so save-under has work // to do (otherwise it's a 4 KB memset of zeros, atypical). - jlSurfaceClear(gStage, 4); + jlSurfaceClear(jlgStage, 4); timeOp("jlSpriteSaveUnder", op_spriteSave); timeOp("jlSpriteDraw", op_spriteDraw); timeOp("jlSpriteRestoreUnder", op_spriteRestore); @@ -1867,14 +2150,14 @@ static void __attribute__((noinline)) runAllTests(void) { // Seed two patterned tiles via portable draws + snap. jlTileT.pixels // is platform-private (plane bytes on ST/Amiga), so hand-written // bytes would render differently per port; only a snap round-trips. - jlFillRect(gStage, 0, 72, 4, 4, 3); - jlFillRect(gStage, 4, 72, 4, 4, 5); - jlFillRect(gStage, 0, 76, 4, 4, 5); - jlFillRect(gStage, 4, 76, 4, 4, 3); - jlTileSnap(gStage, 0, 9, &gTileMapA); - jlFillRect(gStage, 8, 72, 8, 8, 9); - jlDrawLine(gStage, 8, 72, 15, 79, 1); - jlTileSnap(gStage, 1, 9, &gTileMapB); + jlFillRect(jlgStage, 0, 72, 4, 4, 3); + jlFillRect(jlgStage, 4, 72, 4, 4, 5); + jlFillRect(jlgStage, 0, 76, 4, 4, 5); + jlFillRect(jlgStage, 4, 76, 4, 4, 3); + jlTileSnap(jlgStage, 0, 9, &gTileMapA); + jlFillRect(jlgStage, 8, 72, 8, 8, 9); + jlDrawLine(jlgStage, 8, 72, 15, 79, 1); + jlTileSnap(jlgStage, 1, 9, &gTileMapB); gTileSet[0] = gTileMapA; gTileSet[1] = gTileMapB; { @@ -1902,7 +2185,7 @@ static void __attribute__((noinline)) runAllTests(void) { gCleanSurface = jlSurfaceCreate(); if (gCleanSurface != NULL) { paintFrameStatic(); - jlSurfaceCopy(gCleanSurface, gStage); + jlSurfaceCopy(gCleanSurface, jlgStage); timeOp("gameFrameClean composite", op_gameFrameClean); jlSurfaceDestroy(gCleanSurface); gCleanSurface = NULL; @@ -1910,7 +2193,26 @@ static void __attribute__((noinline)) runAllTests(void) { jlLogF("UBER: gameFrameClean composite: SKIPPED (surface alloc failed)"); } + // Tile layer rows (appended last per the cumulative-hash safety rule). + // The first draw fills the whole ring, so it happens before the window. + layerData(); + gLayer = jlTileLayerCreate(gLayerTiles, gLayerMap, UBER_LAYER_MAP_W, UBER_LAYER_MAP_H, 0, JL_TILE_BLOCKS_PER_COL); + if (gLayer != NULL) { + gLayerX = 0; + gLayerY = 0; + jlTileLayerDraw(jlgStage, gLayer, gLayerX, gLayerY); + timeOpSettled("jlTileLayerDraw scroll", op_layerDraw, settleLayer, UBER_LAYER_FRAMES); + timeOpSettled("jlTileLayerDraw scroll + present", op_layerFrame, settleLayer, UBER_LAYER_FRAMES); + jlTileLayerDestroy(gLayer); + gLayer = NULL; + } else { + jlLogF("UBER: jlTileLayerDraw scroll: SKIPPED (layer alloc failed)"); + } + #ifdef UBER_PROBE + // The presents count from here (the IIgs and hosted ones only while + // asked). + jlgPresentTelemetry = 1u; // W0 probe rows, present-cost forensics. Order matters: the idle // present row above (timed earlier) is P3; P1/P2 mark-and-present // are self-consistent per call; P4 runs last among the TIMED rows @@ -1928,22 +2230,22 @@ static void __attribute__((noinline)) runAllTests(void) { uint32_t probeBytes0; uint32_t probeRows0; - probeBytes0 = gPresentCopiedBytes; - probeRows0 = gPresentScannedRows; + probeBytes0 = jlgPresentCopiedBytes; + probeRows0 = jlgPresentScannedRows; timeOp("probe present96", op_probePresent96); - jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); - probeBytes0 = gPresentCopiedBytes; - probeRows0 = gPresentScannedRows; + jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0)); + probeBytes0 = jlgPresentCopiedBytes; + probeRows0 = jlgPresentScannedRows; op_probePresent96(); - jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); - probeBytes0 = gPresentCopiedBytes; - probeRows0 = gPresentScannedRows; + jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0)); + probeBytes0 = jlgPresentCopiedBytes; + probeRows0 = jlgPresentScannedRows; timeOp("probe present3win", op_probePresent3win); - jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); - probeBytes0 = gPresentCopiedBytes; - probeRows0 = gPresentScannedRows; + jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0)); + probeBytes0 = jlgPresentCopiedBytes; + probeRows0 = jlgPresentScannedRows; op_probePresent3win(); - jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0)); timeOp("probe frameNoPresent", op_probeFrameNoPresent); // P7-1 IIgs trusted rows: same geometry (sprite at 40,30 into a // valid backup) as the golden jlSpriteSaveUnder / RestoreUnder / @@ -1954,21 +2256,21 @@ static void __attribute__((noinline)) runAllTests(void) { // rest of this arm (see probeOp). gProbeRestoreBackup.sprite = gSprite; gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes; - jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup); + jlSpriteSaveUnder(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup); timeOp("probe restoreTrusted", op_probeRestoreTrusted); timeOp("probe saveTrusted", op_probeSaveTrusted); timeOp("probe saveAndDrawTrusted", op_probeSaveAndDrawTrusted); timeOp("probe drawTrusted", op_probeDrawTrusted); jlStagePresent(); jlStagePresent(); - probeBytes0 = gPresentCopiedBytes; - probeRows0 = gPresentScannedRows; + probeBytes0 = jlgPresentCopiedBytes; + probeRows0 = jlgPresentScannedRows; timeOp("probe presentIdle", op_probePresentIdle); - jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); - probeBytes0 = gPresentCopiedBytes; - probeRows0 = gPresentScannedRows; + jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0)); + probeBytes0 = jlgPresentCopiedBytes; + probeRows0 = jlgPresentScannedRows; op_probePresentIdle(); - jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0)); // Hunt-3 rank 1: gameFrameClean component split on IIgs (the // 16-21 ms unattributed residual). Same recipe as the golden // row (gCleanSurface was destroyed after it): re-create with @@ -1978,7 +2280,7 @@ static void __attribute__((noinline)) runAllTests(void) { gCleanSurface = jlSurfaceCreate(); if (gCleanSurface != NULL) { paintFrameStatic(); - jlSurfaceCopy(gCleanSurface, gStage); + jlSurfaceCopy(gCleanSurface, jlgStage); timeOp("probe cleanNoPresent", op_probeCleanNoPresent); timeOp("probe cleanErases", op_probeCleanErases); jlSurfaceDestroy(gCleanSurface); @@ -2010,7 +2312,7 @@ static void __attribute__((noinline)) runAllTests(void) { gProbeRestoreWork = jlSurfaceCreate(); gProbeRestoreBackup.sprite = gSprite; gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes; - jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup); + jlSpriteSaveUnder(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup); if (gProbeRestoreWork != NULL) { probeOp("probe restoreWork", op_probeRestoreWork); } @@ -2025,7 +2327,7 @@ static void __attribute__((noinline)) runAllTests(void) { gCleanSurface = jlSurfaceCreate(); if (gCleanSurface != NULL) { paintFrameStatic(); - jlSurfaceCopy(gCleanSurface, gStage); + jlSurfaceCopy(gCleanSurface, jlgStage); probeOp("probe cleanNoPresent", op_probeCleanNoPresent); probeOp("probe cleanErases", op_probeCleanErases); jlSurfaceDestroy(gCleanSurface); @@ -2056,6 +2358,9 @@ static void __attribute__((noinline)) runAllTests(void) { #ifdef JOEYLIB_PLATFORM_DOS op_probeVgaCheck(); #endif +#ifdef JOEYLIB_PLATFORM_X68000 + op_probeX68kDisplayCheck(); +#endif #if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE) op_probeWindowDisplayCheck(); #endif @@ -2083,8 +2388,8 @@ static void __attribute__((noinline)) setupPalette(void) { pal[ 5] = 0xFF0; // yellow (test pixels) pal[ 7] = 0xFFF; // white (fills) pal[15] = 0xF00; // red - jlPaletteSet(gStage, 0, pal); - jlScbSetRange(gStage, 0, 199, 0); + jlPaletteSet(jlgStage, 0, pal); + jlScbSetRange(jlgStage, 0, 199, 0); } @@ -2156,8 +2461,8 @@ int main(void) { * cost the user is chasing; runAllTests dominates. */ startFrame = jlFrameCount(); - gStage = jlStageGet(); - if (gStage == NULL) { + jlgStage = jlStageGet(); + if (jlgStage == NULL) { jlShutdown(); return 1; } @@ -2167,8 +2472,8 @@ int main(void) { setupPalette(); // Indicate "running": red bar at top of screen. - jlSurfaceClear(gStage, 0); - jlFillRect(gStage, 0, 0, 320, 8, 1); + jlSurfaceClear(jlgStage, 0); + jlFillRect(jlgStage, 0, 0, 320, 8, 1); jlStagePresent(); if (!setupSprite()) { @@ -2203,7 +2508,7 @@ int main(void) { // Done. Green screen + waitForKey. uberMbPhase(UBER_PHASE_DONE); - jlSurfaceClear(gStage, 2); + jlSurfaceClear(jlgStage, 2); jlStagePresent(); jlLogF("UBER: press any key to exit"); diff --git a/include/joey/platform.h b/include/joey/platform.h index e687ced..9553745 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -104,7 +104,7 @@ // the Amiga's layout), and a present copies its dirty rows into the text // plane at $E00000, which is four 1bpp planes too. The text plane has one // 16-colour palette, so per-line SCB palettes are reloaded as the beam scans - // by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s). + // by a CRTC raster interrupt (src/x68000/jlX68kRasterIsr.s). #define JL_CPU_68000 #define JL_ENDIAN_BIG #define JL_NATIVE_PLANAR @@ -137,6 +137,8 @@ #endif #if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__) #define JL_PLATFORM_NAME "PSP" + #elif defined(JOEYLIB_HOSTED_SDL3) && defined(__APPLE__) + #define JL_PLATFORM_NAME "macOS" #elif defined(JOEYLIB_HOSTED_SDL3) #define JL_PLATFORM_NAME "SDL3" #elif defined(JOEYLIB_HOSTED_WIN32) @@ -175,32 +177,34 @@ // original joeylib's joey.h JL_HAS_* table. #if defined(JOEYLIB_PLATFORM_IIGS) - #define JL_HAS_SURFACE_CLEAR // iigsSurfaceClearFastInner (stage), macro - #define JL_HAS_DRAW_PIXEL // iigsDrawPixelInner, macro - #define JL_HAS_DRAW_LINE // iigsDrawLineInner, macro - #define JL_HAS_DRAW_CIRCLE // iigsDrawCircleInner, macro - #define JL_HAS_FILL_CIRCLE // iigsFillCircleInner (stage), macro -- returns bool - #define JL_HAS_FILL_RECT // iigsFillRectMark (stage, fused mark), macro -> generic else - #define JL_HAS_TILE_FILL // iigsTileFillInner, macro - #define JL_HAS_TILE_COPY // iigsTileCopyInner, macro - #define JL_HAS_TILE_COPY_MASKED // iigsTileCopyMaskedInner, macro - #define JL_HAS_TILE_PASTE // iigsTilePasteInner, macro - #define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else - #define JL_HAS_TILE_PASTE_GLYPH // iigsTilePasteGlyphMark (stage, mark fused), macro -> generic else - #define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro - #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: iigsCopyRectRows + #define JL_HAS_SURFACE_CLEAR // jlIigsSurfaceClearFastInner (stage), macro + #define JL_HAS_DRAW_PIXEL // jlIigsDrawPixelInner, macro + #define JL_HAS_DRAW_LINE // jlIigsDrawLineInner, macro + #define JL_HAS_DRAW_CIRCLE // jlIigsDrawCircleInner, macro + #define JL_HAS_FILL_CIRCLE // jlIigsFillCircleInner (stage), macro -- returns bool + #define JL_HAS_FILL_RECT // jlIigsFillRectMark (stage, fused mark), macro -> generic else + #define JL_HAS_TILE_FILL // jlIigsTileFillInner, macro + #define JL_HAS_TILE_COPY // jlIigsTileCopyInner, macro + #define JL_HAS_TILE_COPY_MASKED // jlIigsTileCopyMaskedInner, macro + #define JL_HAS_TILE_PASTE // jlIigsTilePasteInner, macro + #define JL_HAS_TILE_PASTE_MONO // jlIigsTilePasteMonoInner (stage), macro -> generic else + #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (joeyDraw.s): stage, mark fused; generic else + #define JL_HAS_TILE_SNAP // jlIigsTileSnapInner, macro + #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: jlIigsCopyRectRows // [dp],y row copies (short rows lose to // memcpy's per-call setup), function + #define JL_HAS_TILE_LAYER_BLIT // the generic rings, an unrolled stage blit (iigsTileLayer.c) + #define JL_HAS_TILE_LAYER_PUT // the generic rings, an asm put (iigsTileLayer.c) #define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro #define JL_HAS_SPRITE_SAVE // noop macro (codegen path saves) #define JL_HAS_SPRITE_RESTORE // noop macro - #define JL_HAS_FLOOD_WALK_AND_SCANS // iigsFloodWalkAndScansInner (all-in-one), macro + #define JL_HAS_FLOOD_WALK_AND_SCANS // jlIigsFloodWalkAndScansInner (all-in-one), macro #define JL_HAS_BIG_ALLOC // Memory Manager NewHandle/DisposeHandle, function #define JL_HAS_STAGE_ALLOC_PIXELS // pinned $01:2000 display memory, function #define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function #define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function #define JL_HAS_MILLIS_ELAPSED // heartbeat frames plus the beam's line - // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord, + // (frameCount: port.h #defines jlpFrameCount straight to jlIigsGetTickWord, // the VBL heartbeat tick read -- no function override to register here.) #define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function // audio: every real port implements the full engine @@ -227,7 +231,9 @@ #define JL_HAS_INPUT_INIT #define JL_HAS_INPUT_SHUTDOWN #define JL_HAS_INPUT_POLL + #define JL_HAS_INPUT_PREDICATES // jlKey/jlMouse Down/Pressed/Released themselves, asm (joeyDraw.s) #define JL_HAS_JOYSTICK_RESET + #define JL_HAS_JOYSTICK_AXES_READ // the paddles when asked for, not every poll (input.c) // serial add-on (optional; SCC modem/printer + 6551 Super Serial Card) #define JL_HAS_SERIAL_OPEN #define JL_HAS_SERIAL_CLOSE @@ -252,7 +258,7 @@ // millisElapsed: generic frameCount*1000/frameHz (no override) #elif defined(JOEYLIB_PLATFORM_AMIGA) #define JL_HAS_SURFACE_CLEAR // planar clear, function - #define JL_HAS_DRAW_PIXEL // amigaPlanarSetPixel, function + #define JL_HAS_DRAW_PIXEL // planarDrawPixel, inline (amigaTile.h) #define JL_HAS_DRAW_LINE // amigaPlanarLine, function #define JL_HAS_DRAW_CIRCLE // amiga planar circle, function #define JL_HAS_FILL_CIRCLE // amiga planar fill circle, function -- returns bool @@ -263,15 +269,16 @@ #define JL_HAS_TILE_PASTE // amiga planar tile paste, inline (amigaTile.h) #define JL_HAS_TILE_SNAP // amiga planar tile snap, inline (amigaTile.h) #define JL_HAS_TILE_PASTE_MONO // amiga planar mono colorize+paste, inline (amigaTile.h) - #define JL_HAS_TILE_PASTE_GLYPH // amiga 1bpp glyph -> planes, inline (amigaTile.h) + #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (src/m68k/planarShape.s): planes, mark fused + #define JL_HAS_FILL_RECT_ENTRY // jlFillRect itself, asm (src/m68k/planarFill.s): a row; draw.c's is jlFillRectClip #define JL_HAS_SPRITE_DRAW // amiga planar sprite draw, function - #define JL_HAS_SPRITE_SAVE // amiga planar sprite save, function - #define JL_HAS_SPRITE_RESTORE // amiga planar sprite restore, function + #define JL_HAS_SPRITE_SAVE // planarSpriteSavePlanes, inline (amigaTile.h) + #define JL_HAS_SPRITE_RESTORE // planarSpriteRestorePlanes, inline (amigaTile.h) #define JL_HAS_FLOOD_WALK_PLANES // amiga planar flood walk-out, function #define JL_HAS_FLOOD_SCAN_ROW_PLANES // amiga planar flood scan-row, function #define JL_HAS_SURFACE_COPY_PLANES // amiga blitter plane copy, function #define JL_HAS_SURFACE_COPY_RECT // amiga per-plane rect row copy, function - #define JL_HAS_SAMPLE_PIXEL // amiga planar nibble decode, function + #define JL_HAS_SAMPLE_PIXEL // planarSamplePixel, inline (amigaTile.h) #define JL_HAS_SURFACE_HASH // amiga planar hash, function #define JL_HAS_SURFACE_LOAD_FILE // amiga planar load, function #define JL_HAS_SURFACE_SAVE_FILE // amiga planar save, function @@ -279,6 +286,7 @@ #define JL_HAS_SURFACE_ALLOC_PIXELS // amiga: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, function #define JL_HAS_SURFACE_FREE_PORT_DATA // amiga FreeMem planes, function + #define JL_HAS_TILE_LAYER // the shared separate-plane ring + blit (src/m68k/planarTileLayer.c) #define JL_HAS_FRAME_HZ // 50 PAL / 60 NTSC, measured at init, function #define JL_HAS_FRAME_COUNT // VBL-server counter, function #define JL_HAS_WAIT_VBL // WaitTOF, function @@ -334,26 +342,27 @@ // millisElapsed: generic frameCount*1000/frameHz (no override) #elif defined(JOEYLIB_PLATFORM_ATARIST) #define JL_HAS_SURFACE_CLEAR // planar clear, function - #define JL_HAS_DRAW_PIXEL // stPlanarSetPixel, function + #define JL_HAS_DRAW_PIXEL // stDrawPixel, inline (stTile.h) #define JL_HAS_DRAW_LINE // stPlanarLine, function #define JL_HAS_DRAW_CIRCLE // st planar circle, function #define JL_HAS_FILL_CIRCLE // st planar fill circle, function -- returns bool - #define JL_HAS_FILL_RECT // st planar fill rect, function + #define JL_HAS_FILL_RECT // stFillRect, inline (stTile.h) + #define JL_HAS_FILL_RECT_ENTRY // jlFillRect itself, asm (lineSpan.s); draw.c's is jlFillRectClip #define JL_HAS_TILE_FILL // st planar tile fill, inline (stTile.h) #define JL_HAS_TILE_COPY // st planar tile copy, inline (stTile.h) #define JL_HAS_TILE_COPY_MASKED // st planar masked copy, inline (stTile.h) #define JL_HAS_TILE_PASTE // st planar tile paste, inline (stTile.h) #define JL_HAS_TILE_SNAP // st planar tile snap, inline (stTile.h) #define JL_HAS_TILE_PASTE_MONO // st planar mono colorize+paste, inline (stTile.h) - #define JL_HAS_TILE_PASTE_GLYPH // st 1bpp glyph -> planes, inline (stTile.h) + #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (stGlyph.s): planes, mark fused #define JL_HAS_SPRITE_DRAW // st planar sprite draw, function - #define JL_HAS_SPRITE_SAVE // st planar sprite save, function - #define JL_HAS_SPRITE_RESTORE // st planar sprite restore, function + #define JL_HAS_SPRITE_SAVE // stSpriteSavePlanes, inline (stTile.h) + #define JL_HAS_SPRITE_RESTORE // stSpriteRestorePlanes, inline (stTile.h) #define JL_HAS_FLOOD_WALK_PLANES // st planar flood walk-out, function #define JL_HAS_FLOOD_SCAN_ROW_PLANES // st planar flood scan-row, function #define JL_HAS_SURFACE_COPY_PLANES // st planar buffer memcpy, function #define JL_HAS_SURFACE_COPY_RECT // st interleaved rect row memcpy, function - #define JL_HAS_SAMPLE_PIXEL // st planar nibble decode, function + #define JL_HAS_SAMPLE_PIXEL // stSamplePixel, inline (stTile.h) #define JL_HAS_SURFACE_HASH // st planar hash, function #define JL_HAS_SURFACE_LOAD_FILE // st planar load, function #define JL_HAS_SURFACE_SAVE_FILE // st planar save, function @@ -361,6 +370,7 @@ #define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function #define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function + #define JL_HAS_TILE_LAYER // st plane-separated ring + shifting blit (stTileLayer.c) #define JL_HAS_FRAME_HZ // the machine's 50 / 60, function #define JL_HAS_FRAME_COUNT // VBL-ISR counter, function #define JL_HAS_WAIT_VBL // frame-counter wait, function @@ -448,6 +458,7 @@ #define JL_HAS_INPUT_SHUTDOWN #define JL_HAS_INPUT_POLL #define JL_HAS_JOYSTICK_RESET + #define JL_HAS_JOYSTICK_AXES_READ // the BIOS's axis read when asked for, not every poll (input.c) // serial add-on (optional; 8250/16550 UART on a COM port, IRQ RX ring) #define JL_HAS_SERIAL_OPEN #define JL_HAS_SERIAL_CLOSE @@ -483,7 +494,8 @@ #define JL_HAS_TILE_COPY_MASKED #define JL_HAS_TILE_PASTE #define JL_HAS_TILE_SNAP - #define JL_HAS_TILE_PASTE_GLYPH // x68k 1bpp glyph -> planes, inline (x68kTile.h) + #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (src/m68k/planarShape.s): planes, mark fused + #define JL_HAS_FILL_RECT_ENTRY // jlFillRect itself, asm (src/m68k/planarFill.s): a row; draw.c's is jlFillRectClip #define JL_HAS_TILE_PASTE_MONO #define JL_HAS_SPRITE_DRAW #define JL_HAS_SPRITE_SAVE @@ -500,6 +512,7 @@ #define JL_HAS_SURFACE_ALLOC_PIXELS #define JL_HAS_SURFACE_ALLOC_PORT_DATA #define JL_HAS_SURFACE_FREE_PORT_DATA + #define JL_HAS_TILE_LAYER #define JL_HAS_INIT // _iocs_crtmod display bring-up #define JL_HAS_SHUTDOWN // restore the previous CRT mode #define JL_HAS_PRESENT // copy the stage's dirty plane rows into the text plane @@ -521,7 +534,7 @@ // block (fileRoot.h). #define JL_HAS_FILE_ROOTS // Serial add-on: RSDRV.SYS transmit and line setup, receive on the - // port's own SCC interrupt (src/x68000/x68kSccRxIsr.s). + // port's own SCC interrupt (src/x68000/jlX68kSccRxIsr.s). #define JL_HAS_SERIAL_OPEN #define JL_HAS_SERIAL_CLOSE #define JL_HAS_SERIAL_POLL @@ -629,7 +642,9 @@ #define JL_HAS_SAVE_DELETE #define JL_HAS_DISK_FREE // DATA/, SAVES/ and joeylog.txt in the program's folder, from the - // executable's path (hostBackendProgramFolder, fileRoot.h). + // executable's path (jlHostBackendProgramFolder, fileRoot.h); in a browser + // SAVES/ and the log in the page's folder, and on macOS in the program's + // Application Support folder (jlHostBackendWriteFolder). #define JL_HAS_FILE_ROOTS // audio: a software mixer (MOD through the module player, the SFX slots, three // square-wave voices and YM-style noise) run on the program's thread, so @@ -687,7 +702,7 @@ #define JL_HAS_PRESENT // blit the chunky stage to your framebuffer #define JL_HAS_INPUT_INIT #define JL_HAS_INPUT_SHUTDOWN - #define JL_HAS_INPUT_POLL // read your keyboard into gKeyState[] + #define JL_HAS_INPUT_POLL // read your keyboard into jlgKeyState[] #define JL_HAS_JOYSTICK_RESET #define JL_HAS_WAIT_VBL // pace to the display #define JL_HAS_FRAME_COUNT // a monotonic frame/refresh counter diff --git a/include/joey/sprite.h b/include/joey/sprite.h index 36e79bf..d50a01d 100644 --- a/include/joey/sprite.h +++ b/include/joey/sprite.h @@ -167,9 +167,10 @@ void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); // fully in-bounds draws; for clipped draws only the visible bytes are // stored. The captured region's exact geometry is reported in // backup->x/y/width/height/sizeBytes: x is rounded down to even on every -// port, a compiled save on the Amiga, ST, and X68000 takes the 16-px- -// aligned region containing the sprite, a compiled save on the IIgs only -// the rows holding an opaque pixel, and a clipped save the visible part. +// port, a save on the Amiga, ST, and X68000 takes the 16-px-aligned region +// containing the sprite (or its visible part), a compiled save on the IIgs +// only the rows holding an opaque pixel, and a clipped save the visible +// part. // So the region a caller must not invalidate before restoring is the // RECORDED region, not just the sprite rect. On the IIgs the buffer must // not cross a 64 KB bank boundary. With backup->bytes NULL nothing is diff --git a/include/joey/tile.h b/include/joey/tile.h index f721adc..cff8a94 100644 --- a/include/joey/tile.h +++ b/include/joey/tile.h @@ -172,4 +172,47 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontS // convention. wTiles or hTiles of 0 is a no-op. void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map); + +// ----- Tile layers (scrolling) ----- +// +// A tile layer is a map of tile indices, up to JL_TILE_LAYER_MAX_SIDE +// tiles a side, seen through a band of a surface: whole tile rows +// [viewBy, viewBy + viewHeight), the full width. jlTileLayerDraw fills +// the band with the map as seen from a pixel origin, to the pixel both +// ways, so moving the origin a little each frame scrolls it. The map +// wraps at its edges (an origin past them, or a map smaller than the +// view, repeats it). An index of JL_TILE_MAP_SKIP draws colour 0. +// +// The layer keeps the tiles around the view drawn, so a draw only draws +// the edge tiles a move uncovered (a move of a whole view or more draws +// them all) before copying the band. A draw covers the band and never +// reads what was there: draw the layer, then the sprites (no save-under +// needed), then present. +// +// The layer keeps the tileset and the map you give it rather than +// copying them, so both must outlive it. Change a cell with +// jlTileLayerSetTile, not by writing the map: the layer would not see +// the write until the cell scrolled out of the view and back. +typedef struct jlTileLayerT jlTileLayerT; + +#define JL_TILE_LAYER_MAX_SIDE 4096u + +// Make a layer over `map` (mapWidth x mapHeight tile indices into `tiles`, +// row-major: map[row * mapWidth + col]) shown in tile rows [viewBy, +// viewBy + viewHeight). Returns NULL if an argument is NULL or out of +// range, or memory runs out. Nothing is drawn until jlTileLayerDraw. +jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map, uint16_t mapWidth, uint16_t mapHeight, uint8_t viewBy, uint8_t viewHeight); + +// Free a layer (not its tileset or map). NULL is a no-op. +void jlTileLayerDestroy(jlTileLayerT *layer); + +// Fill the layer's band of dst with the map seen from pixel (x, y): the +// band's top-left pixel shows map pixel (x, y), taken modulo the map's +// size in pixels. +void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer, uint16_t x, uint16_t y); + +// Set map cell (tx, ty) to `tile`; the next draw shows it. Out-of-range +// cells are ignored. +void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty, uint8_t tile); + #endif diff --git a/install.sh b/install.sh index 9203e50..5b2508f 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ # # Targets are component names (see `list`) or groups: # toolchain:PORT emulator:PORT PORT toolchains emulators all -# where PORT is iigs, amiga, atarist, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos. `all`, `toolchains` +# where PORT is iigs, amiga, atarist, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos. `all`, `toolchains` # and `emulators` leave out optional components (dvx, which unpacks a DVX # tree's dist/ zips you have); name those explicitly. # @@ -62,7 +62,7 @@ source "$JOEY_INSTALLER/lib/config.sh" # shellcheck source=installer/lib/bundle.sh source "$JOEY_INSTALLER/lib/bundle.sh" -JOEY_PORTS="iigs amiga atarist dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos" +JOEY_PORTS="iigs amiga atarist dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos" depsInstall=0 @@ -94,7 +94,7 @@ groupMembers() { printf '%s\n' "$id" fi ;; - iigs|amiga|atarist|dos|x68000|dvx|sdl3|web|win32|win16|psp) + iigs|amiga|atarist|dos|x68000|dvx|sdl3|web|macos|win32|win16|psp|haiku|haiku32|beos) if [[ "$groups" == *" toolchain:$group "* || "$groups" == *" emulator:$group "* ]]; then printf '%s\n' "$id" fi diff --git a/installer/components/amigaGcc.sh b/installer/components/amigaGcc.sh index 1ea6b3d..c7364e7 100644 --- a/installer/components/amigaGcc.sh +++ b/installer/components/amigaGcc.sh @@ -15,10 +15,12 @@ compDescription="Amiga compiler: m68k-amigaos-gcc 6.5 + NDK 3.2 + vasm (source build)" compGroups="toolchain:amiga" compRequires="host" -compHostDeps="build-tools git which bison flex m4 texinfo autoconf automake gettext rsync wget lhasa gmp-dev mpfr-dev mpc-dev ncurses-dev readline-dev" +compHostDeps="build-tools git patch which bison flex m4 texinfo autoconf automake gettext rsync wget lhasa gmp-dev mpfr-dev mpc-dev ncurses-dev readline-dev" compUrl="https://github.com/AmigaPorts/m68k-amigaos-gcc.git" compCommit="7d3e35f58e61bdf325d84b912b139f973d9b7452" -compPin="$compCommit" +patchDir="$JOEY_PATCHES/amigaGcc" +# Editing a patch rebuilds the toolchain, so the patches are part of the pin. +compPin="$compCommit $(cat "$patchDir"/*.patch | sha256sum | cut -d' ' -f1)" # name commit (the URL comes from the checkout's default-repos file) subProjects=( @@ -51,6 +53,7 @@ compInstall() { local sha local url local dir + local p src="$(freshBuildDir "$compId")" gitFetchCommit "$compUrl" "$compCommit" "$src" for entry in "${subProjects[@]}"; do @@ -59,6 +62,10 @@ compInstall() { [ -n "$url" ] || die "amiga-gcc: no URL for sub-project $name in default-repos" gitFetchCommit "$url" "$sha" "$src/projects/$name" done + for p in "$patchDir"/*.patch; do + info "applying $(basename "$p")" + patch -d "$src/projects/gcc" -p1 -s -F0 --no-backup-if-mismatch < "$p" + done rm -rf "$compDir" make -C "$src" all PREFIX="$compDir" -j"$JOEY_JOBS" # Anything cloned by the build itself is unpinned; say so. diff --git a/installer/components/macosSdk.sh b/installer/components/macosSdk.sh new file mode 100644 index 0000000..3b22f4e --- /dev/null +++ b/installer/components/macosSdk.sh @@ -0,0 +1,57 @@ +# shellcheck shell=bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# The Apple SDK for macOS 15.5, and the Mach-O tools of the host's LLVM: what +# the macOS port (make macos) links its programs against and joins them +# with. The SDK holds the frameworks (Cocoa, CoreAudio, IOKit and the rest) +# and their stub libraries; zig (component zig) brings the C library. Apple +# ships the SDK only inside Xcode and offers no download of it, so it comes +# from the mirror osxcross users share (github.com/joseluisq/macosx-sdks), +# pinned to the release's SHA-256; a copy of the same tarball put in +# toolchains/cache/downloads first (MacOSX15.5.sdk.tar.xz) is used instead. +# Its licence is Apple's (LICENSES.md). llvm-lipo joins each architecture's +# build into one universal file and llvm-libtool-darwin archives universal +# objects (make/support/macos/cc.sh, make/macos.mk). + +compDescription="macos: Apple MacOSX 15.5 SDK + LLVM's lipo and libtool" +compGroups="toolchain:macos" +compRequires="host" +compHostDeps="tar xz llvm" +compUrl="https://github.com/joseluisq/macosx-sdks/releases/download/15.5/MacOSX15.5.sdk.tar.xz" +compSha256="c15cf0f3f17d714d1aa5a642da8e118db53d79429eb015771ba816aa7c6c1cbd" +compPin="$compSha256" + + +# llvmTool NAME -- the path of LLVM's NAME: on the PATH, else the newest +# versioned LLVM's (Debian and Ubuntu keep llvm-lipo in /usr/lib/llvm-18/bin +# and the like, with only some tools linked into /usr/bin). +llvmTool() { + command -v "$1" 2>/dev/null || find /usr/lib -maxdepth 3 -path "/usr/lib/llvm-*/bin/$1" 2>/dev/null | sort -V | tail -n 1 +} + + +compInstall() { + local archive + archive="$(fetchFile "$compUrl" "$compSha256")" + rm -rf "$compDir" + extractArchive "$archive" "$compDir" 1 +} + + +compInstalled() { + [ -d "$compDir/System/Library/Frameworks/Cocoa.framework" ] +} + + +compVerify() { + compInstalled && [ -x "$(llvmTool llvm-lipo)" ] && [ -x "$(llvmTool llvm-libtool-darwin)" ] +} + + +compEnv() { + envVar MACOS_SDK "$compDir" + envVar MACOS_LIPO "$(llvmTool llvm-lipo)" + envVar MACOS_LIBTOOL "$(llvmTool llvm-libtool-darwin)" +} diff --git a/installer/components/sdl3Macos.sh b/installer/components/sdl3Macos.sh new file mode 100644 index 0000000..fa10607 --- /dev/null +++ b/installer/components/sdl3Macos.sh @@ -0,0 +1,116 @@ +# shellcheck shell=bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# SDL 3 for macOS, static and universal, for the macOS port (make macos): +# the release source the sdl3 component builds for the host (its pin is read +# from sdl3.sh, so there is one), built once for Apple silicon and once for +# Intel with the port's own compiler (make/support/macos/cc.sh, zig and the +# Apple SDK), and the two libraries joined with llvm-lipo into +# toolchains/opt/sdl3Macos. + +compDescription="macos: SDL 3 for macOS, static, universal (macos port)" +compGroups="toolchain:macos" +compRequires="host zig macosSdk" +compHostDeps="cmake pkg-config llvm" +sdl3Url="$(compQuery sdl3 compUrl)" +sdl3Sha256="$(compQuery sdl3 compSha256)" +macosCc="$JOEY_REPO/make/support/macos/cc.sh" +# How the compiler is driven is part of what is built, so the wrapper is part +# of the pin. +compPin="$sdl3Sha256:$(compQuery zig compPin):$(compQuery macosSdk compPin):$(sha256sum "$macosCc" | cut -d' ' -f1)" + + +# The tools the compiler wrapper reads from the environment, as make's +# recipes have them from toolchains/config.mk: zig's and the SDK's. +macosToolsExport() { + local kind + local name + local value + while IFS=$'\t' read -r kind name value; do + if [ "$kind" = var ]; then + export "$name=$value" + fi + done < <(compRun zig compEnv; compRun macosSdk compEnv) +} + + +compInstall() { + local archive + local src + local arch + local processor + local libs=() + archive="$(fetchFile "$sdl3Url" "$sdl3Sha256")" + src="$(freshBuildDir "$compId")" + extractArchive "$archive" "$src" 1 + macosToolsExport + for arch in aarch64 x86_64; do + export MACOS_ARCHS="$arch" + # Apple calls aarch64 arm64. + processor="${arch/aarch64/arm64}" + # Nothing here links dynamically, but CMake's Darwin support wants an + # install_name_tool all the same (LLVM's, beside its lipo); and no + # Xcode sysroot or -arch flags, since the wrapper names the SDK and + # the target itself. + cmake -S "$src" -B "$src/build-$arch" \ + -DCMAKE_SYSTEM_NAME=Darwin \ + -DCMAKE_SYSTEM_PROCESSOR="$processor" \ + -DCMAKE_C_COMPILER="$macosCc" \ + -DCMAKE_OBJC_COMPILER="$macosCc" \ + -DCMAKE_AR="$(dirname "$MACOS_LIPO")/llvm-ar" \ + -DCMAKE_RANLIB="$(dirname "$MACOS_LIPO")/llvm-ranlib" \ + -DCMAKE_INSTALL_NAME_TOOL="$(dirname "$MACOS_LIPO")/llvm-install-name-tool" \ + -DCMAKE_OSX_SYSROOT= \ + -DCMAKE_OSX_ARCHITECTURES= \ + -DCMAKE_FIND_ROOT_PATH="$MACOS_SDK" \ + -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \ + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$compDir" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DSDL_SHARED=OFF \ + -DSDL_STATIC=ON \ + -DSDL_TESTS=OFF \ + -DSDL_EXAMPLES=OFF + cmake --build "$src/build-$arch" -j"$JOEY_JOBS" + libs+=("$src/build-$arch/libSDL3.a") + done + rm -rf "$compDir" + # The headers, pkg-config file and CMake package are the same for both; + # the library is replaced by the universal one. + cmake --install "$src/build-aarch64" + "$MACOS_LIPO" -create "${libs[@]}" -output "$compDir/lib/libSDL3.a" + dropBuildDir "$compId" +} + + +compInstalled() { + [ -f "$compDir/lib/libSDL3.a" ] && [ -f "$compDir/lib/pkgconfig/sdl3.pc" ] +} + + +# Link a program against the library the way make/macos.mk does: it must +# come out universal. +compVerify() { + local tmp + local status=1 + macosToolsExport + tmp="$(mktemp -d)" + printf '#include \nint main(void) { return SDL_GetVersion() == 0; }\n' > "$tmp/t.c" + # shellcheck disable=SC2046 + if "$macosCc" "$tmp/t.c" -o "$tmp/t" $(PKG_CONFIG_LIBDIR="$compDir/lib/pkgconfig" pkg-config --cflags --static --libs sdl3) \ + && "$MACOS_LIPO" "$tmp/t" -verify_arch arm64 x86_64; then + status=0 + fi + rm -rf "$tmp" + return $status +} + + +compEnv() { + envVar SDL3_MACOS_DIR "$compDir" +} diff --git a/installer/components/zig.sh b/installer/components/zig.sh new file mode 100644 index 0000000..ecbe9e6 --- /dev/null +++ b/installer/components/zig.sh @@ -0,0 +1,44 @@ +# shellcheck shell=bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# zig 0.15.2: the C compiler the macOS port (make macos) builds with. zig is +# clang with its own Mach-O linker, the macOS C library's headers and +# libSystem's stubs, so it builds Mac programs on Linux and signs the Apple +# silicon ones (ad hoc) as it links them; the frameworks come from the Apple +# SDK (component macosSdk). Prebuilt for Linux x86-64. Its cache stays inside +# toolchains/opt/zig, not in the user's home directory. + +compDescription="macos: zig 0.15.2 (clang with a Mach-O linker)" +compGroups="toolchain:macos" +compRequires="host" +compHostDeps="tar xz" +compUrl="https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" +compSha256="02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +compPin="$compSha256" + + +compInstall() { + local archive + archive="$(fetchFile "$compUrl" "$compSha256")" + rm -rf "$compDir" + extractArchive "$archive" "$compDir" 1 +} + + +compInstalled() { + [ -x "$compDir/zig" ] +} + + +compVerify() { + ZIG_GLOBAL_CACHE_DIR="$compDir/cache" ZIG_LOCAL_CACHE_DIR="$compDir/cache" verifyCompiles "$compDir/zig" cc -target aarch64-macos +} + + +compEnv() { + envVar ZIG "$compDir/zig" + envVar ZIG_GLOBAL_CACHE_DIR "$compDir/cache" + envVar ZIG_LOCAL_CACHE_DIR "$compDir/cache" +} diff --git a/installer/distroPackages.txt b/installer/distroPackages.txt index fac6568..1cc69b0 100644 --- a/installer/distroPackages.txt +++ b/installer/distroPackages.txt @@ -91,6 +91,10 @@ gbm-dev libgbm-dev mesa-libgbm-devel egl-dev libegl-dev mesa-libEGL-devel gles-dev libgles-dev mesa-libGLES-devel +# ---- the macOS port: LLVM's Mach-O tools (llvm-lipo, llvm-libtool-darwin, +# llvm-install-name-tool), which zig does not carry +llvm llvm llvm + # ---- emulators packaged by the distribution hatari hatari hatari fs-uae fs-uae - diff --git a/installer/patches/amigaGcc/0001-bbb-moveq-mask.patch b/installer/patches/amigaGcc/0001-bbb-moveq-mask.patch new file mode 100644 index 0000000..4e7ef7f --- /dev/null +++ b/installer/patches/amigaGcc/0001-bbb-moveq-mask.patch @@ -0,0 +1,21 @@ +--- a/gcc/bbb-opts.c ++++ b/gcc/bbb-opts.c +@@ -325,7 +325,17 @@ + if (GET_CODE(src) == CONST_INT && (mode == HImode || mode == QImode)) + { + unsigned iv = UINTVAL(src); +- setMask(regno, iv, mode); ++ /* output_move_himode and output_move_qimode load a data register's ++ word or byte constant in -128..127 with moveq, which writes all 32 ++ bits sign-extended, where a strict_low_part move keeps the upper ++ ones. A negative one (but a byte's -1, which is st) may so leave ++ the upper bits set: keeping their old mask let opt_elim_dead_assign2 ++ delete a later zero-extension's clear as superfluous. */ ++ if (regno < 8 && INTVAL(src) >= -128 && INTVAL(src) < 0 ++ && !(mode == QImode && INTVAL(src) == -1)) ++ setMask(regno, iv, SImode); ++ else ++ setMask(regno, iv, mode); + value[regno] = gen_rtx_CONST_INT(mode, 0x100000000000000LL | ((long long int ) (regno) << 32) | index); + used[regno] = 1 << FIRST_PSEUDO_REGISTER; + clearRefsByMask(1<": a web program's page + cafebabe*|cffaedfe*) printf 'macos\n' ;; # a Mach-O program, universal or 64-bit 00504250*) printf 'psp\n' ;; # "\0PBP": a PSP program's EBOOT.PBP *) # Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and @@ -1108,7 +1111,7 @@ runBe() { while [ $# -gt 0 ]; do case "$1" in - --port) port="${2:?--port needs iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos}"; shift ;; + --port) port="${2:?--port needs iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos}"; shift ;; --project) [ -d "${2:-}" ] || fail "--project needs a project folder (one built on make/project.mk)" projectRoot="$(cd "$2" && pwd)" @@ -1178,7 +1181,9 @@ if [ "$serialMode" = "listen" ]; then fi # Resolve PROGRAM to a file and its machine. -if [ -d "$program" ]; then +if [ -d "$program/Contents/MacOS" ]; then + program="$(folderProgram "$program/Contents/MacOS")" +elif [ -d "$program" ]; then program="$(folderProgram "$program")" elif [ ! -e "$program" ]; then [ -n "$port" ] || fail "$program is not a file; for a program name add --port" @@ -1191,6 +1196,7 @@ detected="$(magicPort "$program")" if [ -n "$port" ] && [ "$port" != "$detected" ]; then fail "$program is a $detected program, not $port" fi +[ "$detected" != macos ] || fail "$program is a Mac app's: copy its .app to a Mac and open it there (nothing here runs macOS programs)" if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ] && [ "$detected" != "sdl3" ] && [ "$detected" != "win32" ] && [ "$detected" != "win16" ]; then fail "program arguments only work on DOS, SDL3, Win32 and Win16" fi diff --git a/make/amiga.mk b/make/amiga.mk index ad62d51..43878f4 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -20,7 +20,9 @@ $(call requireTool,$(AMIGA_PTPLAYER)/ptplayer.asm,toolchain:amiga) # independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve # from the machine-local shim alongside our code. PTPLAYER_DIR := $(AMIGA_PTPLAYER) -CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP +# -fno-common: gcc 6 merges a program's uninitialised global into a library +# one of the same name; gcc 10 on (every other port) refuses the link. +CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -fno-common -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP # OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses # CIA-B + audio.device interrupts via the OS rather than taking over # Paula directly), matching the way our HAL cooperates with Intuition. @@ -33,11 +35,7 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT # "file format not recognized" failure mode is silent at archive time # and surfaces as undefined references at every binary's link step). PTPLAYER_ASFLAGS := -Fhunk -m68000 -quiet -DOSCOMPAT=1 -# --allow-multiple-definition lets our user-space tzset stub -# (src/amiga/libinit.c) win over libnix's version in -# __gmtoffset.o. libnix's tzset dereferences a possibly-NULL -# LocaleBase; our no-op skips the deref. -LDFLAGS := -noixemul -Wl,--allow-multiple-definition +LDFLAGS := -noixemul PORT_C_SRCS := $(wildcard $(SRC_DIR)/amiga/*.c) PORT_S_SRCS := $(wildcard $(SRC_DIR)/amiga/*.s) @@ -56,7 +54,10 @@ LIB_OBJS := \ $(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \ $(BUILD)/obj/port/ptplayer.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \ + $(BUILD)/obj/68k/planarHash.o \ $(BUILD)/obj/68k/planarSprite.o \ + $(BUILD)/obj/68k/spriteC2p.o \ + $(BUILD)/obj/68k/planarTileLayer.o \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -75,10 +76,19 @@ $(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.c $(AMIGA_CC) $(CFLAGS) -c $< -o $@ # Hand-written 68k assembly: GAS syntax, fed through the gcc driver -# so the bundled m68k-amigaos-as (binutils) does the work. +# so the bundled m68k-amigaos-as (binutils) does the work. It exports every +# .equ and .set as a global absolute symbol (in a hunk object), where every +# other port's assembler keeps them local; they would be in every program's +# link, sfxMix.s's loop counter "sum" among them, so each object's are made +# local again. +AMIGA_NM := $(dir $(AMIGA_CC))m68k-amigaos-nm +AMIGA_OBJCOPY := $(dir $(AMIGA_CC))m68k-amigaos-objcopy +amigaAbsLocal = $(AMIGA_OBJCOPY) $$($(AMIGA_NM) $(1) | awk '$$2 == "A" { print "-L", $$3 }') $(1) + $(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.s @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) -c $< -o $@ + $(call amigaAbsLocal,$@) # PTPlayer is third-party Devpac/vasm-syntax assembly with no # maintained GAS port; vasm is invoked only here. @@ -90,9 +100,13 @@ $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ +# gcc-amigaos spells a C symbol with a leading underscore; the shared asm +# that names one says it through C_OP (src/m68k/m68kCSym.inc, which the +# assembler finds through -I: the gcc driver hands it no include path). $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s @mkdir -p $(dir $@) - $(AMIGA_CC) $(CFLAGS) -c $< -o $@ + $(AMIGA_CC) $(CFLAGS) -Wa,--defsym,JL_ASM_USCORE=1 -Wa,-I$(SRC_68K) -c $< -o $@ + $(call amigaAbsLocal,$@) # 68k sprite emitters (spriteEmitPlanar68k.c) live in src/m68k/; # -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h. @@ -158,3 +172,7 @@ clean-amiga: clean: clean-amiga endif + +# UBER with its probe rows, for the display gate (scripts/verify-amiga-display.sh). +.PHONY: amiga-uberprob +amiga-uberprob: $(call programTarget,UBERPROB) diff --git a/make/atarist.mk b/make/atarist.mk index b31baca..3feba9f 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -24,6 +24,7 @@ ST_SHIM := $(REPO_DIR)/make/support/atarist/include-shim ifeq ($(PLATFORM),ste) STE_SRC_DIR := $(SRC_DIR)/atarist/ste STE_CFLAGS := -DJOEYLIB_ATARI_STE -I$(STE_SRC_DIR) +STE_ASFLAGS := -Wa,--defsym,JOEYLIB_ATARI_STE=1 STE_C_SRCS := $(wildcard $(STE_SRC_DIR)/*.c) endif @@ -33,7 +34,10 @@ LDFLAGS := PORT_C_SRCS := $(wildcard $(SRC_DIR)/atarist/*.c) PORT_S_SRCS := $(wildcard $(SRC_DIR)/atarist/*.s) -SHARED_S := $(wildcard $(SRC_68K)/*.s) +# planarFill.s and planarShape.s are the separate-plane ports' (the Amiga, +# the X68000): the ST's planes interleave, and its jlFillRect and +# jlTilePasteGlyph are its own (lineSpan.s, stGlyph.s). +SHARED_S := $(filter-out $(SRC_68K)/planarFill.s $(SRC_68K)/planarShape.s,$(wildcard $(SRC_68K)/*.s)) # Sprite codegen: the ST-interleaved emitter (spriteEmitInterleaved68k.c) # and the planar sprite interpreter (planarSprite.c) live in src/m68k/ and @@ -49,7 +53,9 @@ LIB_OBJS := \ $(patsubst $(STE_SRC_DIR)/%.c,$(BUILD)/obj/ste/%.o,$(STE_C_SRCS)) \ $(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \ $(BUILD)/obj/68k/spriteEmitInterleaved68k.o \ + $(BUILD)/obj/68k/planarHash.o \ $(BUILD)/obj/68k/planarSprite.o \ + $(BUILD)/obj/68k/spriteC2p.o \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o \ $(BUILD)/obj/shim/math_shim.o @@ -76,10 +82,11 @@ endif # Hand-written 68k assembly: GAS syntax, fed through the gcc driver # so the bundled m68k-atari-mint-as (binutils) does the work. No vasm -# dependency on the ST path. +# dependency on the ST path. The STe build defines JOEYLIB_ATARI_STE for +# the port's .ifdef as well (the .s files see no preprocessor). $(BUILD)/obj/port/%.o: $(SRC_DIR)/atarist/%.s @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) -c $< -o $@ + $(ST_CC) $(CFLAGS) $(STE_ASFLAGS) -c $< -o $@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s @mkdir -p $(dir $@) @@ -147,6 +154,10 @@ clean-$(PLATFORM): # looks like success while leaving stale objects the next build links. clean: clean-$(PLATFORM) +# UBER with its probe rows, for the display gate (scripts/verify-atarist-display.sh). +.PHONY: $(PLATFORM)-uberprob +$(PLATFORM)-uberprob: $(call programTarget,UBERPROB) + # The dirty-mark check is the ST build's (DIRTYMRK_PORTS). ifeq ($(PLATFORM),atarist) .PHONY: atarist-dirtymark atarist-verify-dirtymark diff --git a/make/be.mk b/make/be.mk index c903681..cbb7f4c 100644 --- a/make/be.mk +++ b/make/be.mk @@ -28,13 +28,16 @@ PROGRAM_EXT := # Its icons, the BeOS kind Haiku shows too: 32x32 and 16x16 in the BeOS # palette, raw (tools/iconBake.py be), put into the linked program's # resources by xres (component haikuTools) as BEOS:L:STD_ICON and -# BEOS:M:STD_ICON, BE_ICON_ID each. +# BEOS:M:STD_ICON, BE_ICON_ID each; and the 32x32 one as Haiku's vector +# icon, BEOS:ICON, which Haiku draws at any size (BeOS R5 ignores it). An +# icon too busy for one bakes an empty file and goes without. BE_XRES := $(HAIKU_TOOLS)/xres $(call requireTool,$(BE_XRES),toolchain:$(PLATFORM)) BE_ICON_ID := 101 HOSTED_ICON = $(call programIconDir,$(1))/icon32.raw -HOSTED_ICON_BAKE = $(ICON_BAKE) be $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) $(call programIconDir,$(1))/icon16.raw -HOSTED_PACK = $(BE_XRES) -o $(call programBin,$(1)) -a ICON:$(BE_ICON_ID):BEOS:L:STD_ICON $(call HOSTED_ICON,$(1)) -a MICN:$(BE_ICON_ID):BEOS:M:STD_ICON $(call programIconDir,$(1))/icon16.raw +BE_ICON_VECTOR = $(call programIconDir,$(1))/icon.hvif +HOSTED_ICON_BAKE = $(ICON_BAKE) be $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) $(call programIconDir,$(1))/icon16.raw $(call BE_ICON_VECTOR,$(1)) +HOSTED_PACK = $(BE_XRES) -o $(call programBin,$(1)) -a ICON:$(BE_ICON_ID):BEOS:L:STD_ICON $(call HOSTED_ICON,$(1)) -a MICN:$(BE_ICON_ID):BEOS:M:STD_ICON $(call programIconDir,$(1))/icon16.raw $$$$([ -s $(call BE_ICON_VECTOR,$(1)) ] && echo "-a VICN:$(BE_ICON_ID):BEOS:ICON $(call BE_ICON_VECTOR,$(1))") include $(REPO_DIR)/make/hosted.mk diff --git a/make/beos.mk b/make/beos.mk index f401b2b..569c960 100644 --- a/make/beos.mk +++ b/make/beos.mk @@ -15,7 +15,9 @@ $(call requireTool,$(HAIKU32_ROOT)/cross/bin/i586-pc-haiku-g++,toolchain:beos) $(call requireTool,$(BEOS_R5_SYSROOT)/system-lib/libbe.so,toolchain:beos) BEOS_GCC2 := BE_R5_SYSROOT=$(BEOS_R5_SYSROOT) $(HAIKU32_GCC2) $(HAIKU32_ROOT) -HOSTED_CC := $(BEOS_GCC2) gcc +# -fno-common: gcc 2.95 merges a program's uninitialised global into a +# library one of the same name; every other port's compiler refuses the link. +HOSTED_CC := $(BEOS_GCC2) gcc -fno-common HOSTED_CXX := $(BEOS_GCC2) g++ HOSTED_AR := $(BEOS_GCC2) ar BE_CXX_RUNTIME := -lstdc++.r4 diff --git a/make/common.mk b/make/common.mk index fc9ce57..db1d976 100644 --- a/make/common.mk +++ b/make/common.mk @@ -20,6 +20,12 @@ REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) # `./install.sh doctor` names exactly what is wrong. requireTool = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(wildcard $(1)),,$(error $(2) is not installed; run: ./install.sh install $(2)))) +# $(call requireSet,VARIABLE,WHAT): stop with a clear message when a part of +# a program's identity is not set -- who makes it (DVX's vendor and author), +# a Mac app's identifier -- which is the project's own and never JoeyLib's. +# WHAT says what to set it to. Goals that only clean need none. +requireSet = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(strip $($(1))),,$(error $(1) is not set: $(2)))) + # Letter case, for the port tags (IIGS) and the program names each port # derives from a program's id (Hello on the Amiga, hello on DVX). lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1))))))))))))))))))))))))))) @@ -29,12 +35,14 @@ uc = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst f # $(call cQuote,TEXT) TEXT as a C string literal. shellQuote = '$(subst ','\'',$(1))' cQuote = "$(subst ",\",$(subst \,\\,$(1)))" +# A comma, for text inside a $(call) argument. +comma = , capFirst = $(patsubst a%,A%,$(patsubst b%,B%,$(patsubst c%,C%,$(patsubst d%,D%,$(patsubst e%,E%,$(patsubst f%,F%,$(patsubst g%,G%,$(patsubst h%,H%,$(patsubst i%,I%,$(patsubst j%,J%,$(patsubst k%,K%,$(patsubst l%,L%,$(patsubst m%,M%,$(patsubst n%,N%,$(patsubst o%,O%,$(patsubst p%,P%,$(patsubst q%,Q%,$(patsubst r%,R%,$(patsubst s%,S%,$(patsubst t%,T%,$(patsubst u%,U%,$(patsubst v%,V%,$(patsubst w%,W%,$(patsubst x%,X%,$(patsubst y%,Y%,$(patsubst z%,Z%,$(1))))))))))))))))))))))))))) # The ports, and which of them can build here: a port is available when its # real compiler binary is installed (DVX also needs the DVX SDK). The top # Makefile's `make` and a project's `make` build exactly these. -JOEY_PORTS := iigs amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos +JOEY_PORTS := iigs amiga atarist ste dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos HAVE_IIGS := $(if $(wildcard $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang),1) HAVE_AMIGA := $(if $(wildcard $(AMIGA_CC)),1) HAVE_ATARIST := $(if $(wildcard $(ST_CC)),1) @@ -44,6 +52,7 @@ HAVE_X68000 := $(if $(wildcard $(X68K_ROOT)/bin/m68k-xelf-gcc),1) HAVE_DVX := $(if $(and $(wildcard $(DOS_CC)),$(wildcard $(DVX_SDK)/sdk.mk)),1) HAVE_SDL3 := $(if $(wildcard $(SDL3_DIR)/lib/libSDL3.a),1) HAVE_WEB := $(if $(and $(wildcard $(EMCC)),$(wildcard $(SDL3_WEB_DIR)/lib/libSDL3.a)),1) +HAVE_MACOS := $(if $(and $(wildcard $(ZIG)),$(wildcard $(SDL3_MACOS_DIR)/lib/libSDL3.a)),1) HAVE_WIN32 := $(if $(wildcard $(WIN32_CC)),1) HAVE_WIN16 := $(if $(wildcard $(WIN16_WATCOM)/binl64/owcc),1) HAVE_PSP := $(if $(and $(wildcard $(PSP_CC)),$(wildcard $(SDL3_PSP_DIR)/lib/libSDL3.a)),1) @@ -68,8 +77,8 @@ INCLUDE_DIR := $(REPO_DIR)/include # Machine-specific code lives in src// (iigs, amiga, atarist, dos, # x68000, dvx); code shared across machines of one CPU lives in a processor # folder (src/m68k, src/x86); the hosted core and its backends in src/hosted -# (sdl3, web and psp build the same SDL3 backend, natively, for a browser -# and for the PSP; win32 and win16 their own). +# (sdl3, web, macos and psp build the same SDL3 backend, natively, for a +# browser, for the Mac and for the PSP; win32 and win16 their own). SRC_DIR := $(REPO_DIR)/src SRC_CORE := $(REPO_DIR)/src/core SRC_CG := $(REPO_DIR)/src/codegen @@ -93,7 +102,7 @@ CORE_C_SRCS_NO_MIX := $(filter-out $(CORE_MIX_C_SRCS), $(CORE_C_SRCS)) # Generic portable-C op defaults (the src/generic backend) -- present on every # target. spriteEmitStub.c is excluded: it is the no-codegen template and -# redefines jlSpriteCompile / spriteCompiledDraw, which the real +# redefines jlSpriteCompile / jlSpriteCompiledDraw, which the real # src/codegen/spriteCompile.c already provides. GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic/*.c)) @@ -113,13 +122,12 @@ BINDIR := $(PROGRAM_BUILD)/bin PORT_TAG := $(call uc,$(PLATFORM)) # Baked assets (sprite and tile banks) carry the target they were baked for. # DVX shows the DOS port's stage and runs its x86 code, so it takes the DOS -# bakes; the hosted ports' stage (SDL3, the web, Win32, Win16, the PSP, -# Haiku, BeOS) is chunky like the DOS one, so they take them too (Win16 runs a -# pre-compiled bank's x86 code, as DVX does; the rest take its cels without -# their code); the STe build is the ST port, so it -# takes the ST's (same planes, same compiled sprites); every other port its -# own. -ASSET_PLATFORM := $(if $(filter dvx sdl3 web win32 win16 psp haiku haiku32 beos,$(PLATFORM)),dos,$(if $(filter ste,$(PLATFORM)),atarist,$(PLATFORM))) +# bakes; the hosted ports' stage (SDL3, the web, macOS, Win32, Win16, the +# PSP, Haiku, BeOS) is chunky like the DOS one, so they take them too (Win16 +# runs a pre-compiled bank's x86 code, as DVX does; the rest take its cels +# without their code); the STe build is the ST port, so it takes the ST's +# (same planes, same compiled sprites); every other port its own. +ASSET_PLATFORM := $(if $(filter dvx sdl3 web macos win32 win16 psp haiku haiku32 beos,$(PLATFORM)),dos,$(if $(filter ste,$(PLATFORM)),atarist,$(PLATFORM))) # The host sprite pre-compiler for this port's bakes (.spr -> .spc); its rule # is make/programs.mk's. SPRITEBAKE := $(JOEY_BUILD)/tools/spritebake-$(ASSET_PLATFORM) diff --git a/make/dvx.mk b/make/dvx.mk index 29528bd..a4f780f 100644 --- a/make/dvx.mk +++ b/make/dvx.mk @@ -14,7 +14,7 @@ # port's entry (src/dvx/dvxApp.c), a generated one-topic help file and # resource list -- built and packaged by the SDK's own sdk.mk (KIND = app, # LIBS = libjoey.a, DATA = its data files) and copied into build/dvx/bin/ -# laid out as it goes into DVX: apps/joeylib//.app, .hlp +# laid out as it goes into DVX: apps///.app, .hlp # and DATA/. The gates and joey-run copy that tree into a DVX installation. # # This file holds the library and programRules, the DVX rules for one @@ -43,7 +43,7 @@ DVX_SDK_INCS := $(addprefix -I,$(filter-out %/basic %/net,$(patsubst %/,%,$(wild # A program's exit() and atexit() are the port's (src/dvx/program.c): the # handlers run, and JoeyLib shuts down, when the program ends rather than # when DVX does. Applied to the library and the programs, never to the port. -PROGRAM_RENAMES := -Dexit=dvxPortExit -Datexit=dvxPortAtExit +PROGRAM_RENAMES := -Dexit=jlDvxPortExit -Datexit=jlDvxPortAtExit CFLAGS := $(COMMON_CFLAGS) $(DVX_ARCH) -DJOEYLIB_PLATFORM_DVX -I$(SRC_X86) -I$(SRC_CG) -MMD -MP LIB_CFLAGS := $(CFLAGS) $(PROGRAM_RENAMES) @@ -108,13 +108,13 @@ $(LIB): $(LIB_OBJS) # under bin/: .app, .hlp and its DATA/. The name is the id in lower # case (DVX runs on DOS: an 8.3 name). DATA is staged apart, in # build/dvx/data//DATA, for the SDK to package. -# DVX_VENDOR the vendor folder (default joeylib) +# DVX_VENDOR the vendor folder: whoever makes the program, 1 to 8 +# letters, digits or _ (DVX names its package vendor.name) +# DVX_AUTHOR the resource list's author # DVX_KIND what the help and resource texts call a program: " is a # <kind> program" (default JoeyLib) -# DVX_AUTHOR the resource list's author (default JoeyLib) -DVX_VENDOR := joeylib +# The vendor and the author are the project's own: it must set them. DVX_KIND := JoeyLib -DVX_AUTHOR := JoeyLib PROGRAM_EXT := .app PROGRAM_DATA_APART := yes @@ -141,6 +141,8 @@ programRules = $(call programDvxRules,$(1),$(call programName,$(1))) # the help line) is looked up here rather than passed in, and every line # written out is shellQuote'd, so a title or help line may hold quotes. define programDvxRules +$(call requireSet,DVX_VENDOR,the project's own vendor folder$(comma) which its programs install in: 1 to 8 letters$(comma) digits or _) +$(call requireSet,DVX_AUTHOR,the author DVX names for the project's programs) .PHONY: app-$(2) app-$(2): $(LIB) $(call programVar,$(1),SRCS) $(PORT_ENTRY) $(call programVar,$(1),DATA) $(APPSDIR)/$(2)/icon32.bmp @mkdir -p $(APPSDIR)/$(2) diff --git a/make/examples.mk b/make/examples.mk index 38dadb3..948a09f 100644 --- a/make/examples.mk +++ b/make/examples.mk @@ -34,6 +34,8 @@ # of bands to check that they reach it right. # SERSLOT ST: SERTEST opening JL_SERIAL_SLOT unit 2 -- a Mega STe's SCC # channel B through Bconmap -- for verify-atarist-serial-slot.sh. +# SERFAST Win16: SERTEST at 115200 for the throughput and loss gate, +# verify-win16-serial-fast.sh. # FASTRAM Amiga: the Fast RAM gate's program: a surface made once chip # RAM has run out draws and copies as a chip one does. # WBLAUNCH Amiga: the icon gate's program: a program's icon read and @@ -43,11 +45,17 @@ # tones, voice and noise steps, sound effects, a module's end # and tick rates, which every port must play alike. -PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2 -TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS SERSLOT DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE FASTRAM WBLAUNCH +PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE SCROLL AUDIO UBER ADV ADV2 +TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS SERSLOT SERFAST DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE FASTRAM WBLAUNCH -# DVX's help and resource texts call these JoeyLib examples. -DVX_KIND := JoeyLib example +# Whose these are, as a project names its own: on DVX the vendor folder +# they install in and their author (make/dvx.mk), on macOS the start of +# their apps' identifiers (make/macos.mk). DVX's help and resource texts +# call them JoeyLib examples. +DVX_VENDOR := kpunch +DVX_AUTHOR := Scott Duensing +DVX_KIND := JoeyLib example +MACOS_ID_PREFIX := com.kangaroopunch.joeylib # The IIgs build has never included HELLO. HELLO_SRCS := $(EXAMPLES)/hello/hello.c @@ -87,6 +95,9 @@ JOY_HELP := Joysticks: shows the position and buttons of each joystick. SPRITE_SRCS := $(EXAMPLES)/sprite/sprite.c SPRITE_TITLE := Sprite SPRITE_HELP := Compiled sprites moving over a background. +SCROLL_SRCS := $(EXAMPLES)/scroll/scroll.c +SCROLL_TITLE := Scroll +SCROLL_HELP := A tile layer scrolling a landscape under a sprite, with a bar showing the frames drawn a second. AUDIO_SRCS := $(EXAMPLES)/audio/audio.c AUDIO_TITLE := Audio AUDIO_HELP := Sound: tracker music, a MOD, sound effects, a streamed tone and a noise sweep. @@ -102,7 +113,7 @@ ADV2_HELP := A second small adventure game. # Every example that ships as its own bootable IIgs volume (make # iigs-game-disks). -$(foreach p,DRAW PATTERN KEYS JOY SPRITE SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST,$(eval $(p)_IIGS_GAME_DISK := yes)) +$(foreach p,DRAW PATTERN KEYS JOY SPRITE SCROLL SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST,$(eval $(p)_IIGS_GAME_DISK := yes)) # IIgs serial probes for the gates: SCCPROBE (verify-iigs-serial.sh: does # opening the modem port leave the printer port, AppleTalk's channel, alive?) @@ -137,6 +148,10 @@ SERSLOT_SRCS := $(EXAMPLES)/sertest/sertest.c SERSLOT_PORTS := atarist SERSLOT_BINDIR := $(PROGRAM_BUILD)/test SERSLOT_CFLAGS := -DSERTEST_DEVICE=JL_SERIAL_SLOT -DSERTEST_UNIT=2u +SERFAST_SRCS := $(EXAMPLES)/sertest/sertest.c +SERFAST_PORTS := win16 +SERFAST_BINDIR := $(PROGRAM_BUILD)/test +SERFAST_CFLAGS := -DSERTEST_BAUD=115200u FASTRAM_SRCS := $(REPO_DIR)/tests/amiga/fastRam.c FASTRAM_PORTS := amiga FASTRAM_BINDIR := $(PROGRAM_BUILD)/test @@ -153,10 +168,10 @@ DIRTYMRK_CFLAGS := -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MAR # and bench-iigs.sh look for (iigs-verify-shrtail). DVX and the windowed # hosted ports build it as part of `all`, into build/<port>/probe, for # their display gates (scripts/verify-dvx-display.sh, -# verify-win16-display.sh, verify-win32-display.sh, and -# verify-be-display.sh). +# verify-win16-display.sh, verify-win32-display.sh, verify-be-display.sh, +# and verify-atarist-display.sh). UBERPROB_SRCS := $(UBER_SRCS) -UBERPROB_PORTS := iigs dvx win16 win32 haiku haiku32 beos +UBERPROB_PORTS := iigs dvx win16 win32 haiku haiku32 beos atarist ste amiga UBERPROB_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1 UBERPROB_TITLE := Uber Probe UBERPROB_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder. @@ -168,6 +183,9 @@ UBERPROB_WIN32_BINDIR := $(PROGRAM_BUILD)/probe UBERPROB_HAIKU_BINDIR := $(PROGRAM_BUILD)/probe UBERPROB_HAIKU32_BINDIR := $(PROGRAM_BUILD)/probe UBERPROB_BEOS_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_ATARIST_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_STE_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_AMIGA_BINDIR := $(PROGRAM_BUILD)/probe # AUDIO with its keys pressed on a timetable (-DAUDIO_PROBE): N once, S every # 1.5 seconds, under the tracker song. The IIgs and X68000 audio gates read diff --git a/make/haiku32.mk b/make/haiku32.mk index 9643e91..ba19b46 100644 --- a/make/haiku32.mk +++ b/make/haiku32.mk @@ -13,7 +13,9 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(HAIKU32_ROOT)/cross/bin/i586-pc-haiku-g++,toolchain:haiku32) -HOSTED_CC := $(HAIKU32_GCC2) $(HAIKU32_ROOT) gcc +# -fno-common: gcc 2.95 merges a program's uninitialised global into a +# library one of the same name; every other port's compiler refuses the link. +HOSTED_CC := $(HAIKU32_GCC2) $(HAIKU32_ROOT) gcc -fno-common HOSTED_CXX := $(HAIKU32_GCC2) $(HAIKU32_ROOT) g++ HOSTED_AR := $(HAIKU32_GCC2) $(HAIKU32_ROOT) ar BE_CXX_RUNTIME := -lstdc++.r4 diff --git a/make/hosted.mk b/make/hosted.mk index 20845ef..fef807d 100644 --- a/make/hosted.mk +++ b/make/hosted.mk @@ -7,8 +7,9 @@ # program, which make/programs.mk instantiates for every program. # Included by make/sdl3.mk (a native program), make/web.mk (the same # SDL3 backend compiled to WebAssembly), make/psp.mk (the same again, for -# the PSP), make/win32.mk, make/win16.mk and make/be.mk (Haiku), after each -# has included common.mk and set: +# the PSP), make/macos.mk (and again, for macOS), make/win32.mk, +# make/win16.mk and make/be.mk (Haiku), after each has included common.mk +# and set: # HOSTED_CC the compiler # HOSTED_AR the archiver # HOSTED_BACKEND the backend folder under src/hosted @@ -18,13 +19,16 @@ # PROGRAM_EXT the program file's extension # and, when the backend is C++ (src/hosted/<backend>/*.cpp, the Be API's), # HOSTED_CXX its C++ compiler -# and may set HOSTED_PROGRAM_DEPS, files every program is rebuilt after (the -# web page's shell), and, when a program needs its DATA at link time (the web -# port packs it into the program), HOSTED_LINK_DATA: the link flags for one -# program's DATA folder, $(1) the program's id. When the program file is -# packed from what is linked (the PSP's EBOOT.PBP, from an ELF), the port -# sets HOSTED_LINK_OUT, the file one program is linked to, and HOSTED_PACK, -# the command that packs it into the program file ($(1) the id in both). +# and may set HOSTED_ARFLAGS, what the archiver is given before the +# library's name (rcs; LLVM's libtool, which archives macOS's universal +# objects, takes -static -o), and HOSTED_PROGRAM_DEPS, files every program +# is rebuilt after (the web page's shell), and, when a program needs its +# DATA at link time (the web port packs it into the program), +# HOSTED_LINK_DATA: the link flags for one program's DATA folder, $(1) the +# program's id. When the program file is packed from what is linked (the +# PSP's EBOOT.PBP, from an ELF; a Mac app, from a program), the port sets +# HOSTED_LINK_OUT, the file one program is linked to, and HOSTED_PACK, the +# command that packs it into the program file ($(1) the id in both). # A port whose programs show an icon (make/common.mk's programIcon, baked by # tools/iconBake.py) sets HOSTED_ICON, the file one program's icon is baked # to, and HOSTED_ICON_BAKE, the command that bakes it there from the @@ -42,6 +46,7 @@ HOSTED_DIR := $(SRC_DIR)/hosted BACKEND_DIR := $(HOSTED_DIR)/$(HOSTED_BACKEND) CFLAGS := $(COMMON_CFLAGS) $(HOSTED_DEFINES) -I$(HOSTED_DIR) -MMD -MP +HOSTED_ARFLAGS ?= rcs # HOSTED_X86 (make/win16.mk): sprites are compiled to x86 code as on DOS -- # src/x86's emitter and src/codegen's dispatch in place of the stub. @@ -56,11 +61,14 @@ endif # system it runs on, built with the backend's flags; the backend includes # its headers. HOSTED_C_SRCS := $(wildcard $(HOSTED_DIR)/*.c) -# -DJOEYLIB_HOSTED_POSIX_SERIAL in HOSTED_DEFINES (make/sdl3.mk, make/be.mk): +# -DJOEYLIB_HOSTED_POSIX_SERIAL in HOSTED_DEFINES (make/sdl3.mk, make/macos.mk, +# make/be.mk): # the serial add-on through termios, src/hosted/posix. POSIX_C_SRCS := $(if $(filter -DJOEYLIB_HOSTED_POSIX_SERIAL,$(HOSTED_DEFINES)),$(wildcard $(HOSTED_DIR)/posix/*.c)) BACKEND_C_SRCS := $(wildcard $(BACKEND_DIR)/*.c) BACKEND_CXX_SRCS := $(wildcard $(BACKEND_DIR)/*.cpp) +# Assembly of the backend's own, assembled by HOSTED_AS (make/win16.mk). +BACKEND_ASM_SRCS := $(wildcard $(BACKEND_DIR)/*.asm) EXTRA_C_SRCS := $(if $(HOSTED_BACKEND_EXTRA),$(wildcard $(HOSTED_BACKEND_EXTRA)/*.c)) HOSTED_ICON_ID := 1 BACKEND_CFLAGS := $(CFLAGS) $(HOSTED_SDK_CFLAGS) $(if $(HOSTED_BACKEND_EXTRA),-I$(HOSTED_BACKEND_EXTRA)) -DHOSTED_ICON_ID=$(HOSTED_ICON_ID) @@ -73,6 +81,7 @@ LIB_OBJS := \ $(patsubst $(HOSTED_DIR)/posix/%.c,$(BUILD)/obj/posix/%.o,$(POSIX_C_SRCS)) \ $(patsubst $(BACKEND_DIR)/%.c,$(BUILD)/obj/backend/%.o,$(BACKEND_C_SRCS)) \ $(patsubst $(BACKEND_DIR)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_SRCS)) \ + $(patsubst $(BACKEND_DIR)/%.asm,$(BUILD)/obj/backend/%.o,$(BACKEND_ASM_SRCS)) \ $(patsubst $(HOSTED_BACKEND_EXTRA)/%.c,$(BUILD)/obj/backendExtra/%.o,$(EXTRA_C_SRCS)) LIB := $(LIBDIR)/libjoey.a @@ -110,6 +119,10 @@ $(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.cpp @mkdir -p $(dir $@) $(HOSTED_CXX) $(BACKEND_CFLAGS) -c $< -o $@ +$(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.asm + @mkdir -p $(dir $@) + $(HOSTED_AS) $< $@ + $(BUILD)/obj/backendExtra/%.o: $(HOSTED_BACKEND_EXTRA)/%.c @mkdir -p $(dir $@) $(HOSTED_CC) $(BACKEND_CFLAGS) -c $< -o $@ @@ -119,7 +132,7 @@ $(BUILD)/obj/backendExtra/%.o: $(HOSTED_BACKEND_EXTRA)/%.c $(LIB): $(LIB_OBJS) @mkdir -p $(dir $@) rm -f $@ - $(HOSTED_AR) rcs $@ $^ + $(HOSTED_AR) $(HOSTED_ARFLAGS) $@ $^ # Pull in per-object header-dependency files generated by -MMD/-MP. -include $(LIB_OBJS:.o=.d) diff --git a/make/iigs.mk b/make/iigs.mk index b4c99b5..fc89463 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -192,7 +192,7 @@ iigs-examples: programs # examples as ProDOS S16 apps (an 800KB floppy can't hold every example; see # make-iigs-disk.sh) with their DATA. NTP_BIN rides along so jlpAudioInit's # runtime load of "ntpplayer" finds it on the volume -- without it every -# example logs "audioInit failed". +# example logs "jlAudioOpen failed". iigs-disk: programs $(NTP_BIN) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg @@ -204,7 +204,7 @@ iigs-disk: programs $(NTP_BIN) iigs-disk-all: programs $(NTP_BIN) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ JOEY_DISK_SIZE="16MB" \ - JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV" \ + JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SCROLL SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV" \ $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg # Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW @@ -240,7 +240,11 @@ iigs-verify-serial-loss: $(call programBin,SERLOSS) $(call programBin,SERLOSSS) iigs-verify-all: iigs-disk $(REPO_DIR)/scripts/verify-iigs-all.sh -# Gate the 712 display-state bytes ABOVE the pixel block: the SCB array +# Gate what the present leaves on the screen: every $E1 pixel byte equal to +# the stage after a run of draws, the marks' asm included (shrParity), every +# dirty band CLEAN again after its present (shrBands: a mark that skips its +# 8-row group flag is never cleared, and the present can skip its rows), and +# the 712 display-state bytes ABOVE the pixel block: the SCB array # ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the # tree can see them -- the golden hash covers the STAGE, not $E1, and P9's # pixel compare stops at $9CFF -- so a wrong palette is invisible to every @@ -259,8 +263,14 @@ iigs-verify-shrtail: $(call programBin,UBERPROB) $(NTP_BIN) $(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \ BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh - @grep -E 'shrParity|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \ + @grep -E 'shrParity|shrBands|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \ { echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; } + @test "$$(grep -cE 'shrParity|shrBands|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt)" -eq 4 || \ + { echo "iigs-verify-shrtail: FAIL - not every probe row ran" >&2; exit 1; } + @grep -qE 'shrParity FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ + { echo "iigs-verify-shrtail: FAIL - \$$E1 pixels mismatched the stage" >&2; exit 1; } || true + @grep -qE 'shrBands FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ + { echo "iigs-verify-shrtail: FAIL - a band stayed dirty past its present" >&2; exit 1; } || true @grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ { echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true @echo "iigs-verify-shrtail: PASS" diff --git a/make/macos.mk b/make/macos.mk new file mode 100644 index 0000000..40b6d8c --- /dev/null +++ b/make/macos.mk @@ -0,0 +1,65 @@ +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# macOS build rules (make macos): JoeyLib programs as Mac apps, through the +# hosted core and its SDL3 backend (src/hosted, src/hosted/sdl3), built on +# Linux. The rules are make/hosted.mk's; this file says what is macOS's. A +# project reaches it through make/project.mk. +# +# Built with make/support/macos/cc.sh: zig for Apple silicon and for Intel, +# against the Apple SDK, the two joined into universal files that run on +# either (macOS 13 or newer). Programs link the universal static SDL3 the +# installer builds (SDL3_MACOS_DIR), so they run without an SDL3 on the Mac. +# A program is an app, bin/NAME.app, with the program in Contents/MacOS and +# its DATA and icon in Contents/Resources, its folder, which the port makes +# the current directory when the window opens (SAVES/ and joeylog.txt go in +# the user's Application Support folder: src/hosted/sdl3/sdl3Backend.c). The +# app's identifier is MACOS_ID_PREFIX, a reverse-DNS name of the project's +# own, and the program's name; the project must set it. The Apple silicon +# half is signed ad hoc as zig links it, which macOS needs to run it at all. + +PLATFORM := macos +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +SDL3_MACOS_LIB := $(SDL3_MACOS_DIR)/lib/libSDL3.a +$(call requireTool,$(ZIG),toolchain:macos) +$(call requireTool,$(MACOS_LIBTOOL),toolchain:macos) +$(call requireTool,$(SDL3_MACOS_LIB),toolchain:macos) + +SDL3_MACOS_PKG_CONFIG := PKG_CONFIG_LIBDIR=$(SDL3_MACOS_DIR)/lib/pkgconfig pkg-config +MACOS_SUPPORT := $(REPO_DIR)/make/support/macos + +# LLVM's libtool archives the universal objects (it warns that audio.o and +# input.o are named twice, src/core's and src/hosted's, and keeps both). +HOSTED_CC := $(MACOS_SUPPORT)/cc.sh +HOSTED_AR := $(MACOS_LIBTOOL) +HOSTED_ARFLAGS := -static -o +HOSTED_BACKEND := sdl3 +HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_SDL3 -DJOEYLIB_HOSTED_POSIX_SERIAL +HOSTED_SDK_CFLAGS := $(shell $(SDL3_MACOS_PKG_CONFIG) --cflags sdl3) +HOSTED_LINK := $(shell $(SDL3_MACOS_PKG_CONFIG) --static --libs sdl3) -Wl,-dead_strip +PROGRAM_EXT := +# A program is relinked when SDL changes, and with it repacked when the +# packing does. +HOSTED_PROGRAM_DEPS := $(SDL3_MACOS_LIB) $(MACOS_SUPPORT)/packApp.sh + +# One program's app, $(1) its id: in bin/ (or ID_BINDIR), never in a folder +# of its own (an app is one). Its folder is the app's Contents/Resources, +# where its DATA goes, and SDL's base path for it. +macosApp = $(or $(call programVar,$(1),BINDIR),$(BINDIR))/$(call programName,$(1)).app +programDir = $(call macosApp,$(1))/Contents/Resources +programBin = $(call macosApp,$(1))/Contents/MacOS/$(call programName,$(1)) + +# Linked outside the app, which then gets a copy: the dependency file stays +# out of it. By the id, which a probe build (ID_BINDIR) does not share with +# the program of its name. +HOSTED_LINK_OUT = $(PROGRAM_BUILD)/link/$(1) +# Its identifier: the project's prefix and its name. +macosId = $(call requireSet,MACOS_ID_PREFIX,the start of its apps' identifiers$(comma) a reverse-DNS name of the project's own (com.example.games))$(MACOS_ID_PREFIX).$(call programName,$(1)) +HOSTED_PACK = $(MACOS_SUPPORT)/packApp.sh $(call HOSTED_LINK_OUT,$(1)) $(call macosApp,$(1)) $(call macosId,$(1)) $(call shellQuote,$(or $(call programVar,$(1),TITLE),$(call programName,$(1)))) +# Its icon is the app's (Finder, the Dock), not the window's. +HOSTED_ICON = $(call programDir,$(1))/icon.icns +HOSTED_ICON_BAKE = $(ICON_BAKE) icns $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) + +include $(REPO_DIR)/make/hosted.mk diff --git a/make/programs.mk b/make/programs.mk index 77c264b..2f92d30 100644 --- a/make/programs.mk +++ b/make/programs.mk @@ -28,7 +28,8 @@ # on the Amiga Capitalised (Hello), on DVX lower case # ID_FOLDER non-empty: it ships in a folder of its own, bin/<name>/, # with its DATA beside it (the Amiga, ST, DOS and X68000; -# on the IIgs and DVX every program is packaged apart anyway) +# on the IIgs, DVX and macOS every program is packaged +# apart anyway) # ID_BINDIR build it here instead of bin/ (probe builds) # ID_DATA its DATA files, as the files the description's own rules # make under $(call programDataDir,ID) (or @@ -37,9 +38,12 @@ # where there is no icon), baked for each port that shows # programs by an icon -- the IIgs Finder, the Amiga's # Workbench, DVX, Win16, Win32, Haiku and BeOS, the PSP's -# menu, the SDL3 window, and the web page; by default -# JoeyLib's kangaroo (docs/BuildIntegration.adoc, Icons) -# ID_TITLE DVX: its window title (default: its Amiga-style name) +# menu, the SDL3 window, the Mac's Finder and Dock, and the +# web page; by default JoeyLib's kangaroo +# (docs/BuildIntegration.adoc, Icons) +# ID_TITLE its title: DVX's window title (default: its Amiga-style +# name), the PSP menu's, and a Mac app's display name +# (default: its name) # ID_HELP DVX: the line of its help file (no apostrophes: the help is # written through the shell in quotes) # ID_IIGS_OVERFLOW_CAP IIgs: link large overflow segments (clang-build.sh's diff --git a/make/support/iigs/clang-build.sh b/make/support/iigs/clang-build.sh index 9adc13b..1f831a1 100755 --- a/make/support/iigs/clang-build.sh +++ b/make/support/iigs/clang-build.sh @@ -148,6 +148,13 @@ linkMap="$tmpdir/link.map" linkReloc="$tmpdir/link.reloc" linkManifest="$tmpdir/link.json" linkErr="$tmpdir/link.err" +# A kept map (JOEY_LINK_MAP, below) lists the local symbols too: static +# functions, which a profile otherwise folds into the global before them. +# link816 only: omfEmit rejects the flag. +mapLocals=() +if [ -n "${JOEY_LINK_MAP:-}" ]; then + mapLocals=(--map-locals) +fi # The GS/OS load name is the output basename, uppercased to <=8 alnum chars. omfName="$(basename "$output" | tr '[:lower:]' '[:upper:]' | tr -cd 'A-Z0-9' | cut -c1-8)" @@ -171,7 +178,7 @@ emitMultiSeg() { # $1 = segment-cap, $2 = bss "$LINK" -o "$linkBin" --text-base 0 --bss-base "$2" --segment-cap "$1" \ --overflow-cap "$JOEY_OVERFLOW_CAP" --full-bank \ --manifest "$linkManifest" --map "$linkMap" --reloc-out "$linkReloc" \ - --multi-seg-expressload "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" || return 1 + --multi-seg-expressload "${mapLocals[@]}" "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" || return 1 "$OMFEMIT" --manifest "$linkManifest" --expressload --relocs "$linkReloc" \ --output "$output" } @@ -313,7 +320,7 @@ emitMultiSegFit() { if "$LINK" -o "$linkBin" --text-base 0 --bss-base 0xA000 --full-bank \ --map "$linkMap" --reloc-out "$linkReloc" \ - "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" && heapUsable; then + "${mapLocals[@]}" "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" && heapUsable; then # Single-segment AND the heap is big enough: wrap as-is. "$OMFEMIT" --input "$linkBin" --map "$linkMap" --base 0 --entry __start \ --output "$output" --name "$omfName" --expressload --relocs "$linkReloc" @@ -325,10 +332,28 @@ fi # The link map is the only record of where a symbol landed, and the OMF # carries no symbols at all, so anything that inspects a running program -# (the PC profiler, the MAME state gates) needs it kept. Off by default. +# (the PC profiler, the MAME state gates) needs it kept. Off by default: +# JOEY_LINK_MAP names a directory, which gets <output name>.map and the +# segment images beside it, <output name>.seg<N>.bin (segment 1 is +# link816's main output), with link816's segment manifest, <output +# name>.json, for a multi-segment one. The Loader puts each segment in whatever bank it +# likes, and scripts/profile-iigs.sh finds them there by their bytes. A +# directory, not a file, because one make links several programs (a game +# disk's Start launcher, say) and each keeps its own. if [ -n "${JOEY_LINK_MAP:-}" ] && [ -f "$linkMap" ]; then - mkdir -p "$(dirname "$JOEY_LINK_MAP")" - cp "$linkMap" "$JOEY_LINK_MAP" + mapBase="$JOEY_LINK_MAP/$(basename "$output")" + mkdir -p "$JOEY_LINK_MAP" + cp "$linkMap" "$mapBase.map" + rm -f "$mapBase".seg*.bin + cp "$linkBin" "$mapBase.seg1.bin" + for seg in "$tmpdir"/link.seg*.bin; do + [ -f "$seg" ] || continue + cp "$seg" "$mapBase.${seg##*/link.}" + done + rm -f "$mapBase.json" + if [ -f "$linkManifest" ]; then + cp "$linkManifest" "$mapBase.json" + fi fi # Combined header-dependency file: union of clang -MM over the C sources, diff --git a/make/support/iigs/clang-smoke.sh b/make/support/iigs/clang-smoke.sh index 3585093..c82e76a 100755 --- a/make/support/iigs/clang-smoke.sh +++ b/make/support/iigs/clang-smoke.sh @@ -5,7 +5,7 @@ # # clang-smoke.sh - End-to-end smoke test of the llvm-mos w65816 IIgs # build path. Compiles a tiny C main that calls JoeyLib's ported asm -# (iigsInitRowLut), links it with clang-build.sh, runs it under MAME's +# (jlIigsInitRowLut), links it with clang-build.sh, runs it under MAME's # apple2gs, and verifies the LUT the asm computed. # # This proves the whole toolchain end to end: clang -> llvm-mc -> @@ -32,15 +32,15 @@ export MAME_ROMPATH="$IIGS_ROMPATH" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT -# gRowOffsetLut[5] = 5 * 160 = 800 = 0x0320. Publish it to a 24-bit +# jlgRowOffsetLut[5] = 5 * 160 = 800 = 0x0320. Publish it to a 24-bit # address MAME can read back (bank 2 is clear of IIgs ROM scribbling). cat > "$tmp/smoke.c" <<'EOF' #include <stdint.h> -extern void iigsInitRowLut(void); -extern uint16_t gRowOffsetLut[]; +extern void jlIigsInitRowLut(void); +extern uint16_t jlgRowOffsetLut[]; int main(void) { - iigsInitRowLut(); - *(volatile uint16_t *)0x025000 = gRowOffsetLut[5]; + jlIigsInitRowLut(); + *(volatile uint16_t *)0x025000 = jlgRowOffsetLut[5]; return 0; } EOF @@ -63,7 +63,7 @@ LINK="$LLVM816_ROOT/tools/link816" "$RT/crt0.o" "$RT/libgcc.o" "$RT/libc.o" "$RT/extras.o" \ "$tmp/smoke.o" "$tmp/joeyDraw.o" -echo "[clang-smoke] running under MAME apple2gs, checking gRowOffsetLut[5] == 0x0320" +echo "[clang-smoke] running under MAME apple2gs, checking jlgRowOffsetLut[5] == 0x0320" bash "$RUN_IN_MAME" "$tmp/smoke.bin" --check 0x025000=0320 echo "[clang-smoke] PASS" diff --git a/make/support/macos/cc.sh b/make/support/macos/cc.sh new file mode 100755 index 0000000..d999a91 --- /dev/null +++ b/make/support/macos/cc.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# The macOS port's C compiler (make/macos.mk; the installer builds SDL with +# it too): zig cc once for each architecture in MACOS_ARCHS -- by default +# Apple silicon and Intel -- with the results joined into one universal file +# by llvm-lipo, as Apple's own clang does when given two -arch flags. It +# takes what a C compiler takes: with -c it makes a universal object, else a +# universal program. The first architecture's run is exactly the call it was +# given, so its dependency file (-MMD) comes out as a plain compiler's; the +# other runs leave theirs out. With one architecture, or with no -o (a +# preprocessing run), it is zig cc for the first architecture alone. +# +# Programs run on macOS MACOS_MIN or newer: Ventura, which every Apple +# silicon Mac runs and the Intel ones from 2017 on. +# +# From the environment (toolchains/config.mk, which make exports): ZIG and +# its cache folders, MACOS_SDK and MACOS_LIPO. +# +# Usage: cc.sh COMPILER-ARGUMENTS... +# cc.sh --minimum prints MACOS_MIN (for an Info.plist) + +set -euo pipefail + +MACOS_MIN=13.0 +read -r -a archs <<< "${MACOS_ARCHS:-aarch64 x86_64}" + +if [[ "${1:-}" == --minimum ]]; then + printf '%s\n' "$MACOS_MIN" + exit 0 +fi +if [[ -z "${ZIG:-}" || -z "${MACOS_SDK:-}" || -z "${MACOS_LIPO:-}" ]]; then + echo "$0: ZIG, MACOS_SDK or MACOS_LIPO is not set; run ./install.sh install toolchain:macos" >&2 + exit 1 +fi + +out="" +compile=0 +preprocess=0 +prev="" +for arg in "$@"; do + if [[ "$prev" == -o ]]; then + out="$arg" + fi + case "$arg" in + -c) compile=1 ;; + -E) preprocess=1 ;; + esac + prev="$arg" +done + +# zig brings the C library's headers and libSystem's stubs; the SDK the +# rest, after them (so the caller's own come first): its other headers, its +# frameworks, and, for a link, the stub libraries beside them. zig cc +# builds for UBSan unless told not to, and would then want its runtime; and +# it warns, as no other port's compiler does, at __DATE__ and __TIME__ (a +# build that uses them cannot be reproduced), which -Werror would make fatal. +flags=(-fno-sanitize=undefined -Wno-date-time -idirafter "$MACOS_SDK/usr/include" -F"$MACOS_SDK/System/Library/Frameworks") +if [[ $compile -eq 0 && $preprocess -eq 0 ]]; then + flags+=(-L"$MACOS_SDK/usr/lib") +fi + +# zig refuses a target with a version for a run that only preprocesses (a +# build system asking what the compiler defines), and the version makes no +# difference to that. +target() { + if [[ $preprocess -eq 1 ]]; then + printf '%s-macos\n' "$1" + else + printf '%s-macos.%s\n' "$1" "$MACOS_MIN" + fi +} + +if [[ ${#archs[@]} -eq 1 || -z "$out" ]]; then + exec "$ZIG" cc -target "$(target "${archs[0]}")" "$@" "${flags[@]}" +fi + +slices=() +trap 'rm -f "${slices[@]}"' EXIT +for arch in "${archs[@]}"; do + slice="$out.$arch" + slices+=("$slice") + if [[ ${#slices[@]} -eq 1 ]]; then + "$ZIG" cc -target "$(target "$arch")" "$@" "${flags[@]}" + mv -f "$out" "$slice" + continue + fi + args=() + skip=0 + for arg in "$@"; do + if [[ $skip -eq 1 ]]; then + skip=0 + continue + fi + case "$arg" in + -o) args+=(-o "$slice"); skip=1 ;; + -MD|-MMD|-MP) ;; + -MF|-MT|-MQ) skip=1 ;; + *) args+=("$arg") ;; + esac + done + "$ZIG" cc -target "$(target "$arch")" "${args[@]}" "${flags[@]}" +done +"$MACOS_LIPO" -create "${slices[@]}" -output "$out" diff --git a/make/support/macos/packApp.sh b/make/support/macos/packApp.sh new file mode 100755 index 0000000..7073166 --- /dev/null +++ b/make/support/macos/packApp.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# Pack a linked macOS program into its app, as make/macos.mk's packing step: +# the program as Contents/MacOS/NAME (NAME the app's, without .app), and +# Contents/Info.plist, which tells macOS that is the program to run, the +# app's identifier and title, its icon (Contents/Resources/icon.icns, baked +# by make beside its DATA), the oldest macOS it runs on (cc.sh's), and that +# it draws at a Retina screen's own resolution. The program's Apple silicon +# half carries the ad hoc signature zig gave it, which a copy keeps. +# +# Usage: packApp.sh PROGRAM NAME.app IDENTIFIER TITLE + +set -euo pipefail + +if [[ $# -ne 4 ]]; then + echo "usage: $0 PROGRAM NAME.app IDENTIFIER TITLE" >&2 + exit 2 +fi + +program=$1 +app=$2 +identifier=$3 +title=$4 +name=$(basename "$app" .app) +minimum=$("$(dirname "$0")/cc.sh" --minimum) + +# The title as XML text. +title=${title//&/&} +title=${title//</<} +title=${title//>/>} + +mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +cp -f "$program" "$app/Contents/MacOS/$name" +cat > "$app/Contents/Info.plist" <<EOF +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>$title</string> + <key>CFBundleExecutable</key> + <string>$name</string> + <key>CFBundleIconFile</key> + <string>icon</string> + <key>CFBundleIdentifier</key> + <string>$identifier</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$name</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>LSMinimumSystemVersion</key> + <string>$minimum</string> + <key>NSHighResolutionCapable</key> + <true/> +</dict> +</plist> +EOF diff --git a/make/support/win16/owcc.sh b/make/support/win16/owcc.sh index 1171b5a..1f04b28 100755 --- a/make/support/win16/owcc.sh +++ b/make/support/win16/owcc.sh @@ -9,6 +9,7 @@ # target, bound to its Windows 3.x supervisor. # # owcc.sh cc ARGS... compile (-c), or compile and link a program +# owcc.sh as SOURCE OBJECT assemble (Watcom's wasm: MASM syntax, OMF) # owcc.sh ar rcs LIB OBJ... make a library (Watcom's wlib) # # WIN16_WATCOM is the Open Watcom tree (make exports it from @@ -47,13 +48,18 @@ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" mode="$1" shift +if [ "$mode" = "as" ]; then + # as SOURCE OBJECT, quietly. wasm writes no object for a source that + # draws a warning, so a warning fails the build here too. + exec wasm -q -fo="$2" "$1" +fi if [ "$mode" = "ar" ]; then # ar rcs LIB OBJ...: the library is made new each time (-n), quietly, with # case-sensitive names and no .bak copy. shift exec wlib -q -n -b -c "$@" fi -[ "$mode" = "cc" ] || { echo "owcc.sh: cc or ar, not '$mode'" >&2; exit 2; } +[ "$mode" = "cc" ] || { echo "owcc.sh: cc, as or ar, not '$mode'" >&2; exit 2; } flags=(-bwin386 -fpc -I"$WATCOM/h" -include "$here/noAttributes.h" -Wc,-wcd=202) sources=() @@ -127,7 +133,10 @@ done if [ "$depFile" = 1 ]; then phonyHeaders "$deps" fi -owcc -bwin386 -o "$work/$name.rex" "${objects[@]}" "${libraries[@]}" "-Wl,option stack=$STACK_BYTES" +# noredefsok: a name the program and the library both define is an error, +# as on every other port; Watcom's linker otherwise warns and keeps the +# first, so the program's variable would silently stand in for JoeyLib's. +owcc -bwin386 -o "$work/$name.rex" "${objects[@]}" "${libraries[@]}" "-Wl,option stack=$STACK_BYTES" "-Wl,option noredefsok" if [ -n "$resources" ]; then (cd "$work" && wbind "$name" -q -s "$WATCOM/binw/win386.ext" -R -q -bt=windows "$resources" "$name.exe") else diff --git a/make/tools.mk b/make/tools.mk index 8414e1c..d26204e 100644 --- a/make/tools.mk +++ b/make/tools.mk @@ -53,17 +53,17 @@ $(BUILD_DIR)/spritebake-dos: $(SPRITEBAKE_SRC) $(SRC_X86)/spriteEmitX86.c $(SPRI @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_DOS $(SPRITEBAKE_INC) -I$(SRC_X86) $(filter %.c,$^) -o $@ -$(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SPRITEBAKE_HDRS) +$(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SRC_68K)/spriteC2p.c $(SPRITEBAKE_HDRS) @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA $(SPRITEBAKE_INC) -I$(SRC_DIR)/amiga -I$(SRC_68K) $(filter %.c,$^) -o $@ # The X68000 runs the Amiga's planar 68k emitter unchanged (see # make/x68000.mk), so its bake is the same source under its own target byte. -$(BUILD_DIR)/spritebake-x68000: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SPRITEBAKE_HDRS) +$(BUILD_DIR)/spritebake-x68000: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SRC_68K)/spriteC2p.c $(SPRITEBAKE_HDRS) @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_X68000 $(SPRITEBAKE_INC) -I$(SRC_DIR)/x68000 -I$(SRC_68K) $(filter %.c,$^) -o $@ -$(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c $(SPRITEBAKE_HDRS) +$(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c $(SRC_68K)/spriteC2p.c $(SPRITEBAKE_HDRS) @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST $(SPRITEBAKE_INC) -I$(SRC_DIR)/atarist -I$(SRC_68K) $(filter %.c,$^) -o $@ diff --git a/make/win16.mk b/make/win16.mk index b2f31ad..519933a 100644 --- a/make/win16.mk +++ b/make/win16.mk @@ -27,6 +27,7 @@ $(call requireTool,$(WIN16_WATCOM)/binl64/owcc,toolchain:win16) WIN16_OWCC := $(REPO_DIR)/make/support/win16/owcc.sh HOSTED_CC := $(WIN16_OWCC) cc +HOSTED_AS := $(WIN16_OWCC) as HOSTED_AR := $(WIN16_OWCC) ar HOSTED_BACKEND := win16 HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_WIN16 -DHOSTED_FRAME_INDEXED -DHOSTED_AUDIO_RATE=22050u -DHOSTED_AUDIO_CHANNELS=1u @@ -49,4 +50,8 @@ include $(REPO_DIR)/make/hosted.mk # (scripts/verify-win16-display.sh). ifndef JOEY_PROJECT all $(PLATFORM): $(call programTarget,UBERPROB) + +# The serial throughput gate's program (scripts/verify-win16-serial-fast.sh). +.PHONY: win16-serfast +win16-serfast: $(call programTarget,SERFAST) endif diff --git a/make/x68000.mk b/make/x68000.mk index 6fb5246..8cbf2c7 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -55,13 +55,18 @@ LIB_OBJS := \ $(patsubst $(SRC_DIR)/x68000/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/x68000/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(BUILD)/obj/68k/planarFill.o \ + $(BUILD)/obj/68k/planarShape.o \ $(BUILD)/obj/68k/planarPlot.o \ $(BUILD)/obj/68k/stageBands.o \ $(BUILD)/obj/68k/modMix.o \ $(BUILD)/obj/68k/sfxMix.o \ $(BUILD)/obj/68k/planarSpriteWalk.o \ + $(BUILD)/obj/68k/planarLayerBlit.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \ + $(BUILD)/obj/68k/planarHash.o \ $(BUILD)/obj/68k/planarSprite.o \ + $(BUILD)/obj/68k/spriteC2p.o \ + $(BUILD)/obj/68k/planarTileLayer.o \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -81,9 +86,11 @@ $(BUILD)/obj/port/%.o: $(SRC_DIR)/x68000/%.s @mkdir -p $(dir $@) $(X68K_CC) $(CFLAGS) -c $< -o $@ +# The assembler finds src/m68k/m68kCSym.inc through -I: the gcc driver hands +# it no include path. $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s @mkdir -p $(dir $@) - $(X68K_CC) $(CFLAGS) -c $< -o $@ + $(X68K_CC) $(CFLAGS) -Wa,-I$(SRC_68K) -c $< -o $@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c @mkdir -p $(dir $@) diff --git a/scripts/bench-amiga.sh b/scripts/bench-amiga.sh index 6b498c7..1b6ae21 100755 --- a/scripts/bench-amiga.sh +++ b/scripts/bench-amiga.sh @@ -14,6 +14,9 @@ # # scripts/bench-amiga.sh # default 150s timeout # TIMEOUT=240 scripts/bench-amiga.sh # override +# BENCH_PROG=Uberprob BENCH_BIN_DIR=build/amiga/probe scripts/bench-amiga.sh +# # another build of UBER (the probe: +# # scripts/verify-amiga-display.sh) # # Real Kickstart (installer component `kickstart`) is # required -- the HAL drives OCS directly and AROS is unreliable for it. @@ -25,25 +28,29 @@ source "$repo/scripts/lib/joeyEnv.sh" joeyNeed FSUAE fsuae source "$repo/scripts/lib/amigaGate.sh" amigaMachine -bin_dir=$repo/build/amiga/bin +bin_dir=${BENCH_BIN_DIR:-$repo/build/amiga/bin} +prog=${BENCH_PROG:-Uber} timeout_s=${TIMEOUT:-150} -if [[ ! -f $bin_dir/Uber ]]; then - echo "Uber not built. Run 'make amiga' first." >&2 +if [[ ! -f $bin_dir/$prog ]]; then + echo "$prog not built in $bin_dir. Run 'make amiga' first." >&2 exit 1 fi work=$(mktemp -d -t joeylib-amiga-bench.XXXXXX) mkdir -p "$work/s" find "$bin_dir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} + +# A probe build's folder has no DATA of its own: UBER's is the main build's. if [[ -d "$bin_dir/DATA" ]]; then cp -r "$bin_dir/DATA" "$work/" +elif [[ -d "$repo/build/amiga/bin/DATA" ]]; then + cp -r "$repo/build/amiga/bin/DATA" "$work/" fi # ':' anchors to the boot volume root. 32 KB task stack: the shell # default (~4 KB) left UBER one deep call chain from overflow -- the # Phase-1 sprite-compile path found it (emitter accumulator frame + # vsnprintf underneath = nondeterministic wandering crashes). -printf 'stack 32768\n:Uber\n' > "$work/s/startup-sequence" +printf 'stack 32768\n:%s\n' "$prog" > "$work/s/startup-sequence" log=$work/joeylog.txt diff --git a/scripts/bench-atarist.sh b/scripts/bench-atarist.sh index 74a0bea..7122e82 100755 --- a/scripts/bench-atarist.sh +++ b/scripts/bench-atarist.sh @@ -28,6 +28,8 @@ # scripts/bench-atarist.sh ste # the STe build on an STe # scripts/bench-atarist.sh megaste # the STe build on a Mega STe # TIMEOUT=120 scripts/bench-atarist.sh # override +# BENCH_PRG / BENCH_BIN_DIR # another program and its folder +# # (UBERPROB, verify-atarist-display.sh) # # NOTE: requires the ST supervisor-mode fix in src/atarist/hal.c # (jlpInit Super(0L)); without it UBER bus-errors on the first present. @@ -44,13 +46,14 @@ case $machine in ste|megaste) build=ste ;; *) echo "usage: scripts/bench-atarist.sh [st|megast|ste|megaste]" >&2; exit 2 ;; esac -bin_dir=$repo/build/$build/bin +bin_dir=${BENCH_BIN_DIR:-$repo/build/$build/bin} +prg=${BENCH_PRG:-UBER.PRG} tos=$EMUTOS_IMG log=$bin_dir/joeylog.txt timeout_s=${TIMEOUT:-90} -if [[ ! -f $bin_dir/UBER.PRG ]]; then - echo "UBER.PRG not built. Run 'make $build' first." >&2 +if [[ ! -f $bin_dir/$prg ]]; then + echo "$prg not built. Run 'make $build' first." >&2 exit 1 fi if [[ ! -f $tos ]]; then @@ -71,7 +74,7 @@ cleanup() { } trap cleanup EXIT -echo "bench-atarist: running the $build build's UBER headless on a $machine (timeout ${timeout_s}s)..." >&2 +echo "bench-atarist: running the $build build's $prg headless on a $machine (timeout ${timeout_s}s)..." >&2 DISPLAY=$display "$HATARI" \ --machine "$machine" \ --tos "$tos" \ @@ -81,7 +84,7 @@ DISPLAY=$display "$HATARI" \ --sound off \ --fast-forward on \ --frameskips 4 \ - --auto "C:\\UBER.PRG" >/tmp/bench-atarist-hatari.log 2>&1 & + --auto "C:\\$prg" >/tmp/bench-atarist-hatari.log 2>&1 & hat_pid=$! # Poll for the completion marker UBER flushes just before its key wait. diff --git a/scripts/bench-dvx.sh b/scripts/bench-dvx.sh index 6290879..0d89be0 100755 --- a/scripts/bench-dvx.sh +++ b/scripts/bench-dvx.sh @@ -8,7 +8,7 @@ # Stages a scratch DVX (scripts/lib/dvxGate.sh) whose desktop is UBER, boots # it in DOSBox Staging under Xvfb, and waits for UBER's "press any key to # exit" line in its joeylog.txt -- which a DVX program writes to its settings -# folder, CONFIG\APPS\JOEYLIB\UBER on the staged drive. The log is copied to +# folder, CONFIG\APPS\<VENDOR>\UBER on the staged drive. The log is copied to # build/dvx/bin/joeylog.txt (tools/diff-uber-hashes and tools/uber-perf-table # read it from there) and DOSBox is stopped. # diff --git a/scripts/bench-x68000.sh b/scripts/bench-x68000.sh index ad6a310..2599fc3 100755 --- a/scripts/bench-x68000.sh +++ b/scripts/bench-x68000.sh @@ -27,6 +27,8 @@ # for hundreds of frames rather than one. # # bash scripts/bench-x68000.sh +# X68K_BENCH_CFLAGS="-DUBER_PROBE -DUBER_FRAMES=1" UBER built with more +# flags: its probe rows (scripts/verify-x68000-display.sh) set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) @@ -52,7 +54,8 @@ trap 'rm -rf "$work"' EXIT # stock ~900 KB of Human68k utilities in SYS/ BIN/ BASIC2/ ... still allocated # and only ~330 KB usable. `xdftool empty` frees the whole 1221-cluster disk, # so the size pressure is gone -- the strip is now just tidiness.) -m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \ +# shellcheck disable=SC2086 # X68K_BENCH_CFLAGS is a list of flags +m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer ${X68K_BENCH_CFLAGS:-} \ -DJOEYLIB_PLATFORM_X68000 \ -I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \ "$repo/examples/uber/uber.c" \ @@ -67,7 +70,7 @@ x68kDataDisk "$work/data.xdf" || exit 1 x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1 # Completion-driven exit. UBER's last act before jlWaitForAnyKey is -# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM +# jlSurfaceClear(jlgStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM # is the "hashes are computed and flushed" signal. The benchmark's own # jlSurfaceClear/jlStagePresent ops can paint that colour for an instant, so the # reading only counts once it has held for GREEN_STABLE frames -- the finished diff --git a/scripts/check-blank.sh b/scripts/check-blank.sh index c04a5f1..b7ba469 100755 --- a/scripts/check-blank.sh +++ b/scripts/check-blank.sh @@ -54,7 +54,7 @@ for o in glob.glob(sys.argv[1]+"/*.o"): if len(p)>=3 and p[1] in "TDBRtdbr": defs.add(p[2]) elif len(p)>=3 and p[1]=="U": und.add(p[2]) elif len(p)==2 and p[0]=="U": und.add(p[1]) -print(" ".join(sorted(s for s in und-defs if s.startswith(("jlp","jlSprite","spriteCompiledDraw"))))) +print(" ".join(sorted(s for s in und-defs if s.startswith(("jlp","jlSprite","jlSpriteCompiledDraw"))))) PY ) if [ -n "$missing" ]; then diff --git a/scripts/check-dirtymark.sh b/scripts/check-dirtymark.sh index 1164efe..c2e6034 100755 --- a/scripts/check-dirtymark.sh +++ b/scripts/check-dirtymark.sh @@ -4,7 +4,7 @@ # SPDX-License-Identifier: MIT # # check-dirtymark.sh - Host-cc gate for the stage's dirty-band writers: the -# multi-row mark (surfaceMarkDirtyRows, whose four-row groups reuse a group's +# multi-row mark (jlSurfaceMarkDirtyRows, whose four-row groups reuse a group's # result for the next group that held the same bytes) and the whole-stage # fills, held byte for byte to the plain per-row widen over randomized band # states and marks of every height and phase (tests/host/dirtyMarkHost.c). diff --git a/scripts/check-icons.sh b/scripts/check-icons.sh index a78a927..bfc6ad0 100755 --- a/scripts/check-icons.sh +++ b/scripts/check-icons.sh @@ -13,7 +13,8 @@ # win32 a group icon, number HOSTED_ICON_ID, among its resources # (llvm-readobj) # win16 the same in its NE resource table -# sdl3 the window icon linked in (gHostedIconRgba) +# sdl3 the window icon linked in (jlgHostedIconRgba) +# macos the app's icon: Info.plist naming an ICNS file in its Resources # web icon.png beside the page, the page naming it its icon # psp the EBOOT's ICON0, the baked PNG, 144x80 # haiku, haiku32, beos BEOS:L:STD_ICON and BEOS:M:STD_ICON resources @@ -47,7 +48,7 @@ makeValue() { make -s -f "$repo/make/$1.mk" --eval "valueOf: ; @echo \$($2)" valueOf 2> /dev/null } -for port in amiga win32 win16 sdl3 web psp haiku haiku32 beos dvx iigs; do +for port in amiga win32 win16 sdl3 macos web psp haiku haiku32 beos dvx iigs; do program=$(joeyExampleProgram draw "$port" 2> /dev/null) || continue [ -f "$program" ] || continue checked="${checked:+$checked }$port" @@ -106,7 +107,21 @@ PY ;; sdl3) symbols=$(nm "$program" 2> /dev/null) - grep -q ' gHostedIconRgba$' <<< "$symbols" || fail sdl3 "DRAW links no window icon" + grep -q ' jlgHostedIconRgba$' <<< "$symbols" || fail sdl3 "DRAW links no window icon" + ;; + macos) + python3 - "$(dirname "$(dirname "$(dirname "$program")")")" <<'PY' || fail macos "DRAW.app's icon" +import plistlib +import sys + +from PIL import Image + +app = sys.argv[1] +with open(app + "/Contents/Info.plist", "rb") as f: + info = plistlib.load(f) +icon = Image.open("%s/Contents/Resources/%s.icns" % (app, info["CFBundleIconFile"])) +sys.exit(icon.format != "ICNS") +PY ;; web) folder=$(dirname "$program") diff --git a/scripts/check-macos-apps.sh b/scripts/check-macos-apps.sh new file mode 100755 index 0000000..cf4867e --- /dev/null +++ b/scripts/check-macos-apps.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# check-macos-apps.sh - Host gate for the macOS port's apps (make macos), +# which nothing here can run: every app in build/macos/bin is one macOS can +# open. Its Info.plist parses and names, as an application for the macOS +# make/support/macos/cc.sh builds for, a program that is in Contents/MacOS +# and an icon that is in Contents/Resources. The program is universal: an +# Apple silicon (arm64) and an Intel (x86_64) program, each built for that +# macOS and linking nothing but the system's own libraries, the Apple silicon +# one signed (zig signs it ad hoc as it links; macOS will not run an +# unsigned arm64 program at all). Read with LLVM's Mach-O tools. +# +# Usage: scripts/check-macos-apps.sh +# Exit: 0 PASS, 1 FAIL, 2 SKIP (the port not built, or no LLVM tools) +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +label=check-macos-apps + +joeyNeed MACOS_LIPO toolchain:macos +objdump="$(dirname "$MACOS_LIPO")/llvm-objdump" +joeyNeedFile "$objdump" toolchain:macos +minimum=$("$repo/make/support/macos/cc.sh" --minimum) +bin="$repo/build/macos/bin" +if ! compgen -G "$bin/*.app" > /dev/null; then + echo "$label: SKIP - no app in $bin (make macos)" + exit 2 +fi + +python3 - "$bin" "$MACOS_LIPO" "$objdump" "$minimum" "$label" <<'PY' +import glob +import os +import plistlib +import subprocess +import sys + +bin, lipo, objdump, minimum, label = sys.argv[1:] +ARCHS = {"arm64", "x86_64"} +SIGNED = {"arm64"} +SYSTEM = ("/System/Library/", "/usr/lib/") + + +def run(*args): + return subprocess.run(args, capture_output=True, text=True, check=True).stdout + + +def sliceProblems(program, arch): + problems = [] + headers = run(objdump, "--macho", "--private-headers", "--arch=" + arch, program).split() + if "LC_BUILD_VERSION" not in headers or headers[headers.index("minos") + 1] != minimum: + problems.append("%s not built for macOS %s" % (arch, minimum)) + if arch in SIGNED and "LC_CODE_SIGNATURE" not in headers: + problems.append("%s unsigned" % arch) + for line in run(objdump, "--macho", "--dylibs-used", "--arch=" + arch, program).splitlines()[1:]: + library = line.split()[0] + if not library.startswith(SYSTEM): + problems.append("%s links %s" % (arch, library)) + return problems + + +def appProblems(app): + contents = os.path.join(app, "Contents") + with open(os.path.join(contents, "Info.plist"), "rb") as f: + info = plistlib.load(f) + problems = [] + if info.get("CFBundlePackageType") != "APPL" or info.get("LSMinimumSystemVersion") != minimum: + problems.append("Info.plist is not an application for macOS %s" % minimum) + if not os.path.isfile(os.path.join(contents, "Resources", info.get("CFBundleIconFile", "") + ".icns")): + problems.append("no icon") + program = os.path.join(contents, "MacOS", info.get("CFBundleExecutable", "")) + if not (os.path.isfile(program) and os.access(program, os.X_OK)): + return problems + ["no program"] + archs = set(run(lipo, "-archs", program).split()) + if archs != ARCHS: + problems.append("holds %s, not %s" % (" ".join(sorted(archs)), " ".join(sorted(ARCHS)))) + for arch in sorted(archs & ARCHS): + problems += sliceProblems(program, arch) + return problems + + +apps = sorted(glob.glob(os.path.join(bin, "*.app"))) +fails = [] +for app in apps: + try: + problems = appProblems(app) + except (OSError, ValueError, KeyError, plistlib.InvalidFileException, subprocess.CalledProcessError) as error: + problems = [str(error)] + if problems: + fails.append("%s: %s" % (os.path.basename(app), ", ".join(problems))) +if fails: + print("%s: FAIL - %s" % (label, "; ".join(fails))) + sys.exit(1) +print("%s: PASS (%d app%s: universal %s, arm64 signed, macOS %s on, system libraries only)" % (label, len(apps), "" if len(apps) == 1 else "s", "+".join(sorted(ARCHS)), minimum)) +PY diff --git a/scripts/check-tiled.sh b/scripts/check-tiled.sh new file mode 100755 index 0000000..c190b3a --- /dev/null +++ b/scripts/check-tiled.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# check-tiled.sh - Host gate for tools/tiled2joey.py, which turns a map +# made in Tiled into a tile sheet (baked into tile banks by assetbake.py) and +# one jlTileMapPaste map per tile layer (docs/Assets.adoc). tests/host/ +# tiledCheck.py writes one map as TMX and as TMJ, with every flip Tiled can +# ask for, two tilesets (one external, with a margin and spacing), and every +# layer encoding the tool reads; both must convert to the sheet and maps +# worked out by hand, the sheet must bake, and maps it cannot take (16x16 +# tiles, an RGB tileset) must be refused. +# +# Usage: scripts/check-tiled.sh +# Exit: 0 PASS, 1 FAIL +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +label=check-tiled + +work=$(mktemp -d -t joeylib-tiled.XXXXXX) +trap 'rm -rf "$work"' EXIT + +out=$(python3 "$repo/tests/host/tiledCheck.py" "$repo" "$work") +status=$? +echo "$out" +checks=$(grep -c '^TILED:' <<< "$out") +if [ "$status" -ne 0 ]; then + echo "$label: FAIL ($(grep -c 'FAIL' <<< "$out") of $checks checks failed)" + exit 1 +fi +echo "$label: PASS ($checks checks: TMX and TMJ, every flip, two tilesets, three layer encodings, the bake, two refusals)" +exit 0 diff --git a/scripts/check-tilelayer.sh b/scripts/check-tilelayer.sh new file mode 100755 index 0000000..65724fa --- /dev/null +++ b/scripts/check-tilelayer.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# check-tilelayer.sh - Host gate for tile layers: the BLANK port built with +# the host cc, and tests/host/tileLayerHost.c checking every draw of 300 runs +# of random moves, bands, maps (some smaller than the view), and cell changes +# pixel for pixel against the map itself, plus the band's dirty marks, the +# rows outside it, and jlTileLayerCreate's refusals. +# +# Usage: scripts/check-tilelayer.sh +# Exit: 0 PASS, 1 FAIL +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cc=${CC:-cc} +work=$(mktemp -d -t joeylib-tilelayer.XXXXXX) +trap 'rm -rf "$work"' EXIT + +cd "$repo" +"$cc" -O2 -DJOEYLIB_PLATFORM_BLANK \ + -Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen -Wall -Wextra -Werror \ + src/core/*.c src/generic/*.c src/blank/blank.c tests/host/tileLayerHost.c \ + -lm -o "$work/tileLayerHost" + +if "$work/tileLayerHost"; then + echo "check-tilelayer: PASS (every draw matches the map pixel for pixel)" +else + echo "check-tilelayer: FAIL" + exit 1 +fi diff --git a/scripts/lib/dvxGate.sh b/scripts/lib/dvxGate.sh index d858a2d..5cca057 100644 --- a/scripts/lib/dvxGate.sh +++ b/scripts/lib/dvxGate.sh @@ -10,16 +10,16 @@ # one of them the desktop, so DVX boots straight into it; and the DOSBox # Staging run of that folder as drive C:. Nothing here writes to the DVX # project or to toolchains/opt/dvx. Source after joeyEnv.sh. A project built -# on make/project.mk sets DVX_BIN (and DVX_VENDOR, if it changed it) to its -# own before sourcing. +# on make/project.mk sets DVX_BIN and DVX_VENDOR to its own before sourcing. DVX_GATE="$(basename "$0" .sh)" DVX_BIN="${DVX_BIN:-$JOEY_REPO/build/dvx/bin}" # UBER's probe build (make dvx-probe), packaged apart from bin/. DVX_PROBE_DIR="${DVX_PROBE_DIR:-$JOEY_REPO/build/dvx/probe}" -# The folder a program installs in under apps/ (make/dvx.mk's DVX_VENDOR), and -# the name of its log. -DVX_VENDOR="${DVX_VENDOR:-joeylib}" +# The folder a program installs in under apps/ (make/dvx.mk's DVX_VENDOR; for +# JoeyLib's examples make/examples.mk's, asked of make), and the name of its +# log. +DVX_VENDOR="${DVX_VENDOR:-$(make -s -f "$JOEY_REPO/make/dvx.mk" --eval 'dvxVendor: ; @echo $(DVX_VENDOR)' dvxVendor 2> /dev/null)}" DVX_LOG_NAME=joeylog.txt # The port's own settings file in a program's settings folder, and its # full-screen and square-pixels switches (src/dvx/hal.c). @@ -68,7 +68,7 @@ dvxNeedBuild() { # dvxStage DIR APPDIR... -- stage DVX in DIR/DVX with the programs in the # APPDIRs (each a folder holding NAME.app, from dvxAppDir or anywhere else), -# installed as apps/joeylib/NAME. The first one is the desktop. DIR may hold +# installed as apps/VENDOR/NAME. The first one is the desktop. DIR may hold # an earlier staging: the runtime and the programs are copied over it, and # what DVX wrote there (its settings, the programs' CONFIG folders) stays. dvxStage() { diff --git a/scripts/lib/joeyEnv.sh b/scripts/lib/joeyEnv.sh index 5d021a3..f2cde7d 100644 --- a/scripts/lib/joeyEnv.sh +++ b/scripts/lib/joeyEnv.sh @@ -65,6 +65,11 @@ joeyExampleProgram() { printf '%s\n' "$bin/$up/EBOOT.PBP" return 0 ;; + macos) + # Every program is an app, the program inside it. + printf '%s\n' "$bin/$up.app/Contents/MacOS/$up" + return 0 + ;; amiga) # Mixed-case names. up="${name,,}" @@ -81,11 +86,11 @@ joeyExampleProgram() { # apps/<vendor>/<name>/, whatever the vendor. up="${up,,}" candidate="$(find "$bin/apps" -mindepth 3 -maxdepth 3 -type f -path "*/$up/$up.app" -print -quit 2>/dev/null)" - printf '%s\n' "${candidate:-$bin/apps/joeylib/$up/$up.app}" + printf '%s\n' "${candidate:-$bin/apps/VENDOR/$up/$up.app}" return 0 ;; *) - echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos)" >&2 + echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos)" >&2 return 2 ;; esac diff --git a/scripts/lib/serialLoopPeer.py b/scripts/lib/serialLoopPeer.py index 24c54d3..b98363d 100755 --- a/scripts/lib/serialLoopPeer.py +++ b/scripts/lib/serialLoopPeer.py @@ -25,6 +25,13 @@ CLOSE FAIL line more, and keeps the pty pair open until FILE exists or WALL runs out, so the gate can read the guest end's settings after the close (verify-sdl3-serial.sh). +--stream N --rate RATE (TCP, nullmodem transparent:1) then streams N bytes of printable +ASCII through SERTEST's echo, at most STREAM_WINDOW bytes ahead of what has +come back, and checks every byte returns once, in order. Prints one STREAM +PASS / STREAM FAIL line more, with the round trip's bytes a second; PASS +needs that to be at least STREAM_MIN_PERCENT of what the line rate RATE +(bits a second) carries. + --dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which puts the guest's modem lines in the byte stream as they change: 0xFF 0xFF is a data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs SERTEST's @@ -52,6 +59,17 @@ CONNECT_RETRY_SECONDS = 0.5 READ_SIZE = 256 SHOWN_BYTES = 80 +# --stream: what the window, the chunks, and the pass mark are, and the bytes +# it sends (printable ASCII, so never SERTEST's DTR_CHECK). +STREAM_WINDOW = 1024 +STREAM_CHUNK = 64 +STREAM_POLL_SECONDS = 0.01 +STREAM_SETTLE_SECONDS = 1.0 +STREAM_MIN_PERCENT = 75 +STREAM_FIRST_BYTE = 0x20 +STREAM_BYTE_VALUES = 95 +LINE_BITS_PER_BYTE = 10 # 8N1: a start bit, 8 data bits, a stop bit + # SERTEST's DTR check (examples/sertest/sertest.c). DTR_CHECK = b"\x04" DTR_ACK = b"\x06" @@ -239,15 +257,59 @@ def loop(send, receive, wall, start, banner): return 1, got +def stream(send, receive, wall, start, total, rate): + # The probe's last echoes go first, then the stream and its echo. + pattern = bytes(STREAM_FIRST_BYTE + (k % STREAM_BYTE_VALUES) for k in range(total)) + settle = time.time() + STREAM_SETTLE_SECONDS + while time.time() < settle: + if receive() is None: + print("STREAM FAIL: the connection closed before the stream") + return 1 + sent = 0 + got = b"" + began = time.time() + while len(got) < total and time.time() - start < wall: + if sent < total and sent - len(got) < STREAM_WINDOW: + chunk = pattern[sent:sent + STREAM_CHUNK] + send(chunk) + sent += len(chunk) + data = receive() + if data is None: + break + got += data + elapsed = time.time() - began + perSecond = len(got) / elapsed if elapsed > 0 else 0.0 + floor = rate / LINE_BITS_PER_BYTE * STREAM_MIN_PERCENT / 100.0 + if got != pattern: + at = next((k for k in range(min(len(got), total)) if got[k] != pattern[k]), min(len(got), total)) + print("STREAM FAIL: %d of %d bytes back, the first wrong at %d; %r" % (len(got), total, at, got[at:at + 16])) + return 1 + if perSecond < floor: + print("STREAM FAIL: %d bytes back in order, %.0f a second, under %.0f (%d%% of %d baud)" % (total, perSecond, floor, STREAM_MIN_PERCENT, rate)) + return 1 + print("STREAM PASS: %d bytes back in order, none lost, %.0f a second (%d baud carries %d)" % (total, perSecond, rate, rate // LINE_BITS_PER_BYTE)) + return 0 + + def main(argv): withDtrCheck = "--dtr-check" in argv argv = [arg for arg in argv if arg != "--dtr-check"] + streamBytes = 0 + rate = 0 + if "--stream" in argv and argv.index("--stream") + 1 < len(argv): + at = argv.index("--stream") + streamBytes = int(argv[at + 1]) + argv = argv[:at] + argv[at + 2:] + if "--rate" in argv and argv.index("--rate") + 1 < len(argv): + at = argv.index("--rate") + rate = int(argv[at + 1]) + argv = argv[:at] + argv[at + 2:] holdFile = None if "--close-hold" in argv and argv.index("--close-hold") + 1 < len(argv): at = argv.index("--close-hold") holdFile = argv[at + 1] argv = argv[:at] + argv[at + 2:] - if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty") or (holdFile is not None and argv[1] != "pty"): + if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty") or (holdFile is not None and argv[1] != "pty") or (streamBytes > 0 and (argv[1] == "pty" or withDtrCheck or rate <= 0)): sys.stderr.write(__doc__) return 2 mode = argv[1] @@ -294,7 +356,12 @@ def main(argv): return lines.feed(data) if withDtrCheck else data status, got = loop(conn.sendall, receive, wall, start, banner) - if status != 0 or not withDtrCheck: + if status != 0: + return status + if streamBytes > 0: + conn.settimeout(STREAM_POLL_SECONDS) + return stream(conn.sendall, receive, wall, start, streamBytes, rate) + if not withDtrCheck: return status return dtrCheck(conn.sendall, receive, wall, start, lines, got) diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh index c54ae64..5d0f251 100755 --- a/scripts/make-iigs-disk.sh +++ b/scripts/make-iigs-disk.sh @@ -56,7 +56,7 @@ VOLSIZE="${JOEY_DISK_SIZE:-800KB}" # Priority order: small primitive demos first so DRAW/PATTERN/etc. always fit; # the larger adventures land last and are dropped if the floppy is full. -order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER ADV2 ADV}) +order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER SCROLL ADV2 ADV}) work=$(mktemp -d -t joeylib-disk.XXXXXX) trap 'rm -rf "$work"' EXIT diff --git a/scripts/profile-iigs.sh b/scripts/profile-iigs.sh new file mode 100755 index 0000000..dc8fef0 --- /dev/null +++ b/scripts/profile-iigs.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# profile-iigs.sh - PC-sampling profiler for a JoeyLib program on the IIgs. +# +# Boots a self-booting program volume (make-iigs-game-disk.sh's) under +# headless MAME and samples the 65816's program bank and counter every +# PROFILE_PERIOD_US of EMULATED time, from a Lua coroutine that emu.wait()s +# between samples. A per-frame sample would land at the same point of a +# frame-synced game loop every time; a period that is not a multiple of the +# frame's spreads the samples over the whole frame. +# +# The OMF carries no symbols and the GS/OS Loader puts each bank-aligned +# segment in whatever bank it likes, so the samples are tied back to the +# link map by the segments' BYTES: at the end the script reads the start of +# every bank a sample landed in and scores it against every segment image +# (the Loader's relocations change only a few bytes). A bank that matches +# no segment is GS/OS, a tool, NTP or the ROM, and is reported by bank. +# +# The map comes from building the program with JOEY_LINK_MAP=<dir>, which +# keeps <dir>/NAME.map, NAME.seg<N>.bin and NAME.json (clang-build.sh): +# +# rm -f build/iigs/bin/STAXI && JOEY_LINK_MAP=/some/dir make iigs +# +# The map lists static functions too (JOEY_LINK_MAP links with link816's +# --map-locals), but one the compiler inlined has no code of its own: its +# time is its caller's row. +# +# Usage: scripts/profile-iigs.sh <image> <map> [seconds] [skipSeconds] +# image the program's boot volume: .po (800KB, the 3.5 +# drive) or .2mg (anything larger, a CFFA2 card) +# map NAME.map, with NAME.seg<N>.bin beside it +# seconds emulated seconds to run, default 120 +# skipSeconds ignore samples before this, default 40 (boot) +# Env: PROFILE_PERIOD_US sample period in emulated microseconds, +# default 997 +# PROFILE_TOP rows to print, default 40 +# PROFILE_RAMSIZE MAME -ramsize, default 4M +# PROFILE_SAMPLES a file to keep the raw samples in (S addr +# count, F bank bytes, T total), for a look +# inside a bank no segment matched +# Requires: ./install.sh install iigs. +set -euo pipefail + +img=${1:?usage: profile-iigs.sh <image> <map> [seconds] [skipSeconds]} +map=${2:?usage: profile-iigs.sh <image> <map> [seconds] [skipSeconds]} +seconds=${3:-120} +skip=${4:-40} +periodUs=${PROFILE_PERIOD_US:-997} +top=${PROFILE_TOP:-40} +ramsize=${PROFILE_RAMSIZE:-4M} + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +joeyNeed MAME mame +joeyNeed IIGS_ROMPATH iigsFirmware + +[ -f "$img" ] || { echo "profile-iigs: no such image $img" >&2; exit 2; } +[ -f "$map" ] || { echo "profile-iigs: no such map $map" >&2; exit 2; } +[ -f "${map%.map}.seg1.bin" ] || { echo "profile-iigs: ${map%.map}.seg1.bin missing (build with JOEY_LINK_MAP=<dir>)" >&2; exit 2; } + +# MAME opens a hard-disk image read-write: run a private copy. +work=$(mktemp -d -t joeylib-iigsprof.XXXXXX) +trap 'rm -rf "$work"' EXIT +name=$(basename "$img") +cp "$img" "$work/$name" +if [ "$(stat -c %s "$img")" -le 900000 ]; then + media=(-flop3 "$work/$name") +else + media=(-sl7 cffa2 -hard1 "$work/$name") +fi + +# Bytes of a bank compared against a segment's image. +fingerprintBytes=512 + +cat > "$work/prof.lua" <<LUA +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local out = io.open("samples.txt", "w") +local counts = {} +local banks = {} +local taken = 0 + +local function finish() + for addr, n in pairs(counts) do + out:write(string.format("S %06X %d\n", addr, n)) + end + for bank, _ in pairs(banks) do + local bytes = {} + for i = 0, $fingerprintBytes - 1 do + bytes[#bytes + 1] = string.format("%02X", mem:read_u8((bank << 16) + i)) + end + out:write(string.format("F %02X %s\n", bank, table.concat(bytes))) + end + out:write(string.format("T %d\n", taken)) + out:close() + manager.machine:exit() +end + +local sampler = coroutine.create(function() + while manager.machine.time:as_double() < $seconds do + emu.wait($periodUs / 1000000.0) + if manager.machine.time:as_double() >= $skip then + local pb = cpu.state["PB"].value + local addr = (pb << 16) | cpu.state["PC"].value + counts[addr] = (counts[addr] or 0) + 1 + banks[pb] = true + taken = taken + 1 + end + end + finish() +end) + +local started = false +emu.register_frame_done(function() + if not started then + started = true + local ok, err = coroutine.resume(sampler) + if not ok then + io.write("PROFILE-IIGS lua error: " .. tostring(err) .. "\n") + manager.machine:exit() + end + end +end) +LUA + +echo "profile-iigs: $name, ${seconds}s emulated (samples from ${skip}s, every ${periodUs} us)..." >&2 +(cd "$work" && QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout 3600 "$MAME" apple2gs \ + -ramsize "$ramsize" -rompath "$IIGS_ROMPATH" "${media[@]}" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -autoboot_script "$work/prof.lua" </dev/null >"$work/mame.log" 2>&1) || true +if [ ! -s "$work/samples.txt" ]; then + echo "profile-iigs: FAIL -- no samples (MAME output follows)" >&2 + tail -15 "$work/mame.log" >&2 + exit 1 +fi +grep 'PROFILE-IIGS' "$work/mame.log" >&2 || true +if [ -n "${PROFILE_SAMPLES:-}" ]; then + cp "$work/samples.txt" "$PROFILE_SAMPLES" +fi + +python3 - "$work/samples.txt" "$map" "$top" <<'PY' +import bisect +import json +import os +import re +import sys + +samplesPath, mapPath, top = sys.argv[1], sys.argv[2], int(sys.argv[3]) +mapBase = mapPath[:-4] if mapPath.endswith('.map') else mapPath + +# The segments: number -> (link bank, image bytes). +segments = {} +manifest = mapBase + '.json' +if os.path.exists(manifest): + for seg in json.load(open(manifest))['segments']: + segments[seg['num']] = int(seg['base'], 16) >> 16 +else: + segments[1] = 0 +images = {num: open('%s.seg%d.bin' % (mapBase, num), 'rb').read() for num in segments} + +# The symbols, by address: the globals, and the locals (static functions) +# when the map has them; a global wins an address both name. +byAddr = {} +section = None +for line in open(mapPath): + if line.startswith('#'): + section = 'global' if line.startswith('# global symbols') else ('local' if line.startswith('# local symbols') else None) + continue + m = re.match(r'^0x([0-9a-fA-F]{6})\s+(\S+)$', line.strip()) + if m and section is not None: + addr = int(m.group(1), 16) + if section == 'global' or addr not in byAddr: + byAddr[addr] = m.group(2) +syms = sorted(byAddr.items()) +symAddrs = [a for a, _ in syms] + +counts = {} +fingerprints = {} +total = 0 +for line in open(samplesPath): + parts = line.split() + if parts[0] == 'S': + counts[int(parts[1], 16)] = int(parts[2]) + elif parts[0] == 'F': + fingerprints[int(parts[1], 16)] = bytes.fromhex(parts[2]) + elif parts[0] == 'T': + total = int(parts[1]) + +# Runtime bank -> link bank, by the best-scoring segment image. +bankMap = {} +for bank, fp in fingerprints.items(): + best = None + for num, image in images.items(): + n = min(len(fp), len(image)) + if n < 16: + continue + same = sum(1 for i in range(n) if fp[i] == image[i]) + score = same / n + if best is None or score > best[0]: + best = (score, num) + if best is not None and best[0] >= 0.85: + bankMap[bank] = (segments[best[1]], best[1], best[0]) + +def where(addr): + bank = addr >> 16 + if bank in bankMap: + link = (bankMap[bank][0] << 16) | (addr & 0xFFFF) + i = bisect.bisect_right(symAddrs, link) - 1 + if i >= 0 and (symAddrs[i] >> 16) == (link >> 16): + return syms[i][1] + return 'seg%d+%04X' % (bankMap[bank][1], addr & 0xFFFF) + if bank >= 0xFC: + return 'ROM $%02X' % bank + if bank in (0xE0, 0xE1): + return 'bank $%02X (firmware/vectors)' % bank + return 'bank $%02X (GS/OS, tools, NTP)' % bank + +byName = {} +for addr, n in counts.items(): + key = where(addr) + byName[key] = byName.get(key, 0) + n + +print('profile-iigs: %d samples' % total) +for bank in sorted(bankMap): + link, num, score = bankMap[bank] + print(' bank $%02X = segment %d (link bank $%02X, %.0f%% of %d bytes match)' % (bank, num, link, score * 100, len(fingerprints[bank]))) +print('%7s %6s %s' % ('samples', 'pct', 'where')) +for key, n in sorted(byName.items(), key=lambda kv: -kv[1])[:top]: + print('%7d %5.1f%% %s' % (n, 100.0 * n / max(total, 1), key)) +PY diff --git a/scripts/profile-x68000.sh b/scripts/profile-x68000.sh index 1ca445b..6c98738 100755 --- a/scripts/profile-x68000.sh +++ b/scripts/profile-x68000.sh @@ -16,12 +16,12 @@ # separates them is that Human68k RELOCATES the copy it runs: at a known # relocation site the running image holds fileValue + base, while the disk # buffer still holds fileValue. From then on -# it samples the PC once per emulated frame, buckets by offset into .text, and -# attributes the buckets to functions with nm. -# -# Sampling once per frame is coarse (55 Hz), but this port runs a game frame in -# 150 ms or more, so each frame still gets several samples and the shape of the -# profile is clear. Read it as "which functions dominate", never as a timing. +# it samples the PC every PROFILE_PERIOD_US of emulated time (a Lua coroutine +# that emu.wait()s between samples; a period that is not a multiple of the +# frame's spreads them over the whole frame, where a sample a frame landed on +# the same instructions every time), buckets by offset into .text, and +# attributes the buckets to functions with nm. Read it as "which functions +# dominate", never as a timing. # # Usage: scripts/profile-x68000.sh program [seconds] [skipSeconds] # program the built .X: its path (a project's, say), or an @@ -29,12 +29,16 @@ # seconds emulated seconds to run, default 240 # skipSeconds ignore samples before this, default 130 (boot + menu) # Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default: the installer's toolchains/firmware/x68000) +# PROFILE_PERIOD_US sample period in emulated microseconds, default 997 +# PROFILE_SAMPLES a file to keep the raw samples in (offset count +# lines, -1 outside the program; the load base) # Requires: the program built WITH symbols (<NAME>.X.elf beside it). set -uo pipefail prog=${1:?usage: profile-x68000.sh program [seconds] [skipSeconds]} seconds=${2:-240} skip=${3:-130} +periodUs=${PROFILE_PERIOD_US:-997} repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" @@ -66,9 +70,9 @@ o = 64 + $relocSite print(int.from_bytes(d[o:o+4],'big')) ") -# gMouseX is written once per input poll, i.e. once per game frame. -mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "gMouseX" { print strtonum("0x" $1); exit }') -[[ -n ${mouseXOff:-} ]] || { echo "profile-x68000: gMouseX not found; cannot count frames" >&2; exit 2; } +# jlgMouseX is written once per input poll, i.e. once per game frame. +mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "jlgMouseX" { print strtonum("0x" $1); exit }') +[[ -n ${mouseXOff:-} ]] || { echo "profile-x68000: jlgMouseX not found; cannot count frames" >&2; exit 2; } work=$(mktemp -d -t joey-x68prof.XXXXXX) trap 'rm -rf "$work"' EXIT @@ -155,6 +159,46 @@ local function pickBase() return nil end +-- The samples come every PROFILE_PERIOD_US of emulated time, from a +-- coroutine that emu.wait()s between them, not once a frame: a frame-synced +-- game loop puts the same instructions under every frame's sample, so a +-- short routine came out as a few hot addresses or none at all. +local samplerStarted = false +local sampler = coroutine.create(function() + while true do + emu.wait($periodUs / 1000000.0) + local t = manager.machine.time:as_double() + if t >= skipUntil then + local pc = cpu.state[pcReg].value + local key + if pc >= base and pc < base + textSize then + -- The EXACT offset, not a 64-byte bucket. Buckets straddle + -- function boundaries, and a bucket credited to the symbol at + -- its start hands one function's samples to its neighbour: + -- that is how a vblank wait came out looking like an + -- expensive input poll. + key = pc - base + else + key = -1 -- ROM, IOCS, anything not ours + end + buckets[key] = (buckets[key] or 0) + 1 + total = total + 1 + end + if t >= stopAt then + out:write(string.format("fps %.2f frames %d window %.1f tapAt %08X\n", + (t > fpsStart) and (frames / (t - fpsStart)) or 0.0, frames, t - fpsStart, base + mouseXOff)) + out:write(string.format("total %d\n", total)) + for k, v in pairs(buckets) do + out:write(string.format("%d %d\n", k, v)) + end + out:write("DONE\n") + out:close() + manager.machine:exit() + return + end + end +end) + emu.register_frame_done(function() local t = manager.machine.time:as_double() if base == nil then @@ -166,7 +210,7 @@ emu.register_frame_done(function() if base ~= nil then out:write(string.format("base %08X at %.1fs (of %d candidates)\n", base, t, #cands)) out:flush() - -- gMouseX is written once per jlpInputPoll, which the game calls + -- jlgMouseX is written once per jlpInputPoll, which the game calls -- once a frame, so counting writes to it counts game frames. That -- turns "how much of the frame is X" into "and how long is a -- frame", which is the difference between a real speedup and one @@ -186,30 +230,14 @@ emu.register_frame_done(function() scanDone = false return end - if t < skipUntil then return end - local pc = cpu.state[pcReg].value - local key - if pc >= base and pc < base + textSize then - -- The EXACT offset, not a 64-byte bucket. Buckets straddle function - -- boundaries, and a bucket credited to the symbol at its start hands - -- one function's samples to its neighbour: that is how a vblank wait - -- came out looking like an expensive input poll. - key = pc - base - else - key = -1 -- ROM, IOCS, anything not ours - end - buckets[key] = (buckets[key] or 0) + 1 - total = total + 1 - if t >= stopAt then - out:write(string.format("fps %.2f frames %d window %.1f tapAt %08X\n", - (t > fpsStart) and (frames / (t - fpsStart)) or 0.0, frames, t - fpsStart, base + mouseXOff)) - out:write(string.format("total %d\n", total)) - for k, v in pairs(buckets) do - out:write(string.format("%d %d\n", k, v)) + if not samplerStarted then + samplerStarted = true + local ok, err = coroutine.resume(sampler) + if not ok then + out:write("sampler error: " .. tostring(err) .. "\n") + out:close() + manager.machine:exit() end - out:write("DONE\n") - out:close() - manager.machine:exit() end end) LUA @@ -222,6 +250,9 @@ LUA [[ -s $work/prof.txt ]] || { echo "profile-x68000: FAIL (no samples)" >&2; exit 1; } grep -q '^base ' "$work/prof.txt" || { echo "profile-x68000: FAIL (never found the load base)" >&2; exit 1; } +if [[ -n ${PROFILE_SAMPLES:-} ]]; then + cp "$work/prof.txt" "$PROFILE_SAMPLES" +fi m68k-xelf-nm -n "$elf" > "$work/syms.txt" python3 - "$work/prof.txt" "$work/syms.txt" <<'PY' diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index 2df416b..310bcb8 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -12,9 +12,10 @@ # template (blank), file roots, the dirty-band marker, the line # and circle fallbacks at the far ends of their coordinates, input, # millis, the module player the software-mixing ports share, -# save, serial, sfxmix, tilebank, and the VGM converter (with -# Furnace installed, on its demo songs too) -# golden UBER on each port, its 51 per-op surface hashes against +# save, serial, sfxmix, tilebank, tile layers against their maps, +# the Tiled map importer, and the VGM converter (with Furnace +# installed, on its demo songs too) +# golden UBER on each port, its 54 per-op surface hashes against # tests/goldens/uber (x68000 against the IIgs's, its own gate; # dvx, sdl3, web, win32, win16, psp, haiku, haiku32 and beos # against the DOS port's, whose chunky stage they share; psp under @@ -31,6 +32,13 @@ # which build runs where -- the STe build refuses a plain ST and # a Mega ST through jlLastError, the ST build runs on an STe # visual iigs: every launchable example on joey.2mg boots and draws +# iigs: also the display -- UBER's probe build's runs of draws, +# each presented, the $E1 pixels then equal to the stage, every +# dirty band clean again, and the SCB and palette blocks the +# stage's (make iigs-verify-shrtail) +# x68000 and amiga: the display -- UBER's probe build's battery +# of draws, each presented, the page or planes shown checked +# against the stage's # dvx: full screen -- UBER's hashes through a suspend and a # resume, and KEYS's row read off the 320x200 VGA screen, then # in its window, then on the whole screen again @@ -63,6 +71,12 @@ # the port's VM) # (dvx: and the present shows it, decoded from the desktop, also # at 8/15/32 bpp and 2x/3x; Ctrl+Esc must not end the program) +# (iigs: and its asm key predicates -- examples/keys lights A's +# cell only while A is held, jlKeyDown, and Escape quits it, +# jlKeyPressed) +# (dos: and the mouse through the driver's INT 33h event +# callback -- moves both ways, all four edges, every column, both +# buttons) # joy (every port but psp, whose input gate covers it, and # haiku, haiku32 and beos, as QEMU's PC has no joystick to give # them; win16 through KPJOY.DRV, JoeyLib's own joystick driver for @@ -147,9 +161,14 @@ # modem port at 9600 and a Super Serial Card at 2400 # x68000: both serial streams byte-exact and the mouse right while # the SCB changes palette on every line of a run (9600, 19200, 38400) -# disk iigs: a one-program boot volume (DRAW's) boots, launches the -# program and draws: as a 2048KB volume off a CFFA2 card, then as -# an 800KB floppy on ROM 3 and on ROM 01 +# disk programs packaged to ship: +# iigs a one-program boot volume (DRAW's) boots, launches the +# program and draws: as a 2048KB volume off a CFFA2 +# card, then as an 800KB floppy on ROM 3 and on ROM 01 +# macos scripts/check-macos-apps.sh: every app is one macOS +# opens -- universal (Apple silicon and Intel), the Apple +# silicon half signed, for macOS 13 on, its Info.plist, +# program and icon in place (no Mac runs it here) # icon a program's icon (DRAW's), where its system shows it: # host scripts/check-icons.sh: every built port's DRAW carries # its baked icon where the system looks (no emulator) @@ -191,8 +210,8 @@ # # Usage: scripts/verify-all.sh KIND|all [port ...] # with no ports, every port the kind has. Ports: host iigs -# x68000 atarist ste amiga dos dvx sdl3 web win32 win16 psp -# haiku haiku32 beos +# x68000 atarist ste amiga dos dvx sdl3 web macos win32 win16 +# psp haiku haiku32 beos # Env: VERIFY_ALL_LOG also append the full transcript to this file # Requires: whatever each selected gate requires; missing ones SKIP. set -uo pipefail @@ -203,7 +222,7 @@ shift || true label="verify-all $kind" KINDS="host golden visual tilebank input audio conform serial mouse save loss disk icon" -ALL_PORTS="host iigs x68000 atarist ste amiga dos dvx sdl3 web win32 win16 psp haiku haiku32 beos" +ALL_PORTS="host iigs x68000 atarist ste amiga dos dvx sdl3 web macos win32 win16 psp haiku haiku32 beos" # UBER on the Amiga needs longer than bench-amiga.sh's interactive default. AMIGA_UBER_SECONDS=400 @@ -220,6 +239,8 @@ host host save scripts/check-save.sh host host serial scripts/check-serial.sh host host sfxmix scripts/check-sfxmix.sh host host tilebank scripts/check-tilebank.sh +host host layer scripts/check-tilelayer.sh +host host tiled scripts/check-tiled.sh host host vgm scripts/check-vgm.sh golden iigs uber scripts/bench-iigs.sh && tools/diff-uber-hashes tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt golden x68000 uber scripts/verify-x68000-golden.sh @@ -242,6 +263,7 @@ golden haiku uber scripts/bench-be.sh haiku && tools/diff-uber-hashes te golden haiku32 uber scripts/bench-be.sh haiku32 && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/haiku32/bin/joeylog.txt golden beos uber scripts/bench-be.sh beos && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/beos/bin/joeylog.txt visual iigs examples scripts/verify-iigs-all.sh +visual iigs display make -s -f make/iigs.mk iigs-verify-shrtail || exit 1 visual dvx full scripts/verify-dvx-fullscreen.sh visual dvx display scripts/verify-dvx-display.sh visual win16 display scripts/verify-win16-display.sh @@ -250,12 +272,16 @@ visual haiku display scripts/verify-be-display.sh haiku visual haiku32 display scripts/verify-be-display.sh haiku32 visual beos display scripts/verify-be-display.sh beos visual x68000 scb scripts/verify-x68000-scb.sh +visual x68000 display scripts/verify-x68000-display.sh visual atarist palette scripts/verify-atarist-palette.sh st visual ste palette scripts/verify-atarist-palette.sh ste megaste +visual atarist display scripts/verify-atarist-display.sh st +visual ste display scripts/verify-atarist-display.sh ste megaste visual amiga palette scripts/verify-amiga-palette.sh visual amiga a1200pal JOEY_AMIGA_MODEL=A1200 scripts/verify-amiga-palette.sh visual amiga a4000pal JOEY_AMIGA_MODEL=A4000 scripts/verify-amiga-palette.sh visual amiga fastram scripts/verify-amiga-fastram.sh +visual amiga display scripts/verify-amiga-display.sh tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1 tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh tilebank atarist tilebank scripts/verify-atarist-tilebank.sh @@ -268,6 +294,7 @@ tilebank win32 tilebank scripts/verify-win32-tilebank.sh tilebank win16 tilebank scripts/verify-win16-tilebank.sh tilebank psp tilebank scripts/verify-psp-tilebank.sh input iigs input scripts/verify-iigs-input.sh +input iigs keys scripts/verify-iigs-keys.sh input x68000 input scripts/verify-x68000-input.sh input x68000 joy scripts/verify-x68000-joy.sh input iigs joy scripts/verify-iigs-joy.sh @@ -277,6 +304,7 @@ input ste joy scripts/verify-atarist-joy.sh ste input amiga input scripts/verify-amiga-input.sh input amiga joy scripts/verify-amiga-joy.sh input dos input scripts/verify-dos-input.sh +input dos mouse scripts/verify-dos-mouse.sh input dos joy scripts/verify-dos-joy.sh input dvx input scripts/verify-dvx-input.sh input dvx depths scripts/verify-dvx-input.sh 640x480x8:1 800x600x15:2 1024x768x32:3 @@ -329,6 +357,7 @@ serial dos serial scripts/verify-dos-serial.sh serial dvx serial scripts/verify-dvx-serial.sh serial win32 serial scripts/verify-win32-serial.sh serial win16 serial scripts/verify-win16-serial.sh +serial win16 fast scripts/verify-win16-serial-fast.sh serial sdl3 serial scripts/verify-sdl3-serial.sh serial haiku serial scripts/verify-be-serial.sh haiku serial haiku32 serial scripts/verify-be-serial.sh haiku32 @@ -345,6 +374,7 @@ loss x68000 scb scripts/verify-x68000-serial-scb.sh disk iigs cffa2 make -s -f make/iigs.mk iigs-verify-game-disk GAME=DRAW GAME_DISK_SIZE=2048KB || exit 1 disk iigs floppy make -s -f make/iigs.mk iigs-verify-game-disk GAME=DRAW || exit 1 disk iigs rom01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/DRAW.2mg +disk macos apps scripts/check-macos-apps.sh icon host icons scripts/check-icons.sh icon iigs finder scripts/verify-iigs-icon.sh icon amiga icon scripts/verify-amiga-icon.sh diff --git a/scripts/verify-amiga-display.sh b/scripts/verify-amiga-display.sh new file mode 100755 index 0000000..125ee86 --- /dev/null +++ b/scripts/verify-amiga-display.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# verify-amiga-display.sh - the Amiga's present, checked on the planes the +# copper shows. UBER's golden hashes are of the stage; a present that leaves +# stale rows on screen passes them. The display flips between two plane +# sets: each present draws into the one not shown, which holds the frame +# before last, from the union of two frames' dirty bands +# (src/amiga/presentScan.s). UBERPROB (UBER with UBER_PROBE, +# make/examples.mk) runs its Amiga display row (op_probePresentPageSum): a +# battery of draws, each presented and the shown planes summed against the +# stage's (jlAmigaProbePresentPageSums in src/amiga/hal.c). It must PASS. +# +# Usage: scripts/verify-amiga-display.sh +# Exit: 0 PASS, 1 FAIL +# Requires: FS-UAE and a Kickstart (install.sh install emulator:amiga); it +# builds the probe (make -f make/amiga.mk amiga-uberprob). + +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +label=verify-amiga-display +dir=$repo/build/amiga/probe + +if ! make -s -f "$repo/make/amiga.mk" amiga-uberprob; then + echo "$label: FAIL - building the UBERPROB failed" >&2 + exit 1 +fi +rm -f "$dir/joeylog.txt" +BENCH_PROG=Uberprob BENCH_BIN_DIR="$dir" TIMEOUT="${TIMEOUT:-300}" "$repo/scripts/bench-amiga.sh" > /dev/null 2>&1 +line=$(grep "UBER-PROBE: presentPageSum" "$dir/joeylog.txt" 2>/dev/null) +if [[ "$line" == *"presentPageSum PASS"* ]]; then + echo "$label: PASS - ${line#UBER-PROBE: }" + exit 0 +fi +echo "$label: FAIL - ${line:-no presentPageSum line in its log}" >&2 +exit 1 diff --git a/scripts/verify-atarist-display.sh b/scripts/verify-atarist-display.sh new file mode 100755 index 0000000..8a40e3e --- /dev/null +++ b/scripts/verify-atarist-display.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# verify-atarist-display.sh - the ST family's present, checked on the page +# the shifter shows. UBER's golden hashes are of the stage; a present that +# leaves stale rows on screen passes them. UBERPROB (UBER with UBER_PROBE, +# make/examples.mk) runs its display row (op_probeDisplayCheck): a tall band +# presented, then a small rect inside it, then a settle present and an idle +# one, comparing the shown page with the stage's planes after each +# (jlpProbeDisplayMismatch in src/atarist/hal.c). Every count must be 0. +# +# Usage: scripts/verify-atarist-display.sh [MACHINE...] (default: st ste) +# st and megast run the ST build, ste and megaste the STe build. +# Exit: 0 PASS, 1 FAIL, 2 SKIP (Hatari or EmuTOS not installed) +# Requires: Hatari + EmuTOS (install.sh install emulator:atarist); it builds +# the probe (make PLATFORM-uberprob). + +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +joeyNeed HATARI hatari +joeyNeed EMUTOS_IMG emutos + +label=verify-atarist-display +machines=("$@") +[ ${#machines[@]} -gt 0 ] || machines=(st ste) +failed=0 +for machine in "${machines[@]}"; do + case $machine in + st|megast) build=atarist ;; + ste|megaste) build=ste ;; + *) echo "$label: unknown machine '$machine' (st, megast, ste, megaste)" >&2; exit 2 ;; + esac + makefile=$repo/make/$build.mk + if ! make -s -f "$makefile" "$build-uberprob"; then + echo "$label: FAIL - building the $build UBERPROB failed" >&2 + exit 1 + fi + dir=$repo/build/$build/probe + rm -f "$dir/joeylog.txt" + BENCH_PRG=UBERPROB.PRG BENCH_BIN_DIR="$dir" TIMEOUT="${TIMEOUT:-120}" "$repo/scripts/bench-atarist.sh" "$machine" > /dev/null 2>&1 + line=$(grep "UBER-PROBE: displayMismatch" "$dir/joeylog.txt" 2>/dev/null) + if [[ "$line" =~ afterB=0\ afterSettle=0\ afterIdle=0 ]]; then + echo "$label: $build build on $machine OK - ${line#UBER-PROBE: }" + else + echo "$label: $build build on $machine FAIL - ${line:-no displayMismatch line in its log}" >&2 + failed=1 + fi +done +if [ "$failed" = 0 ]; then + echo "$label: PASS" +fi +exit "$failed" diff --git a/scripts/verify-be-display.sh b/scripts/verify-be-display.sh index 8c21538..9c383a1 100755 --- a/scripts/verify-be-display.sh +++ b/scripts/verify-be-display.sh @@ -12,7 +12,7 @@ # so a stale or misplaced rectangle would never reach a hash. UBER's probe # build (make <port> builds it, into build/<port>/probe) ends with P13, the # battery the other display gates run, which after every step asks the -# backend (hostBeProbeDisplayMismatch in src/hosted/be/beBackend.cpp) how many +# backend (jlHostBeProbeDisplayMismatch in src/hosted/be/beBackend.cpp) how many # pixels differ: the frame's against the stage, the bitmap's against the # frame, and the window as the shows left it against the window fully # repainted, both read back from the screen (BScreen::ReadBitmap). Every diff --git a/scripts/verify-dos-audio.sh b/scripts/verify-dos-audio.sh index 1847cf6..861eb20 100755 --- a/scripts/verify-dos-audio.sh +++ b/scripts/verify-dos-audio.sh @@ -32,8 +32,9 @@ # unambiguous. This also covers the overlay specifically -- SFX on DOS mix # into the module's buffer rather than a separate channel, so "SFX audible # while the module player is loaded but paused" is exactly the path that -# can break. Measured under DOSBox Staging 0.83 (whose mixer -# runs quieter than the old 0.74's): music ~1220, paused ~17, sfx ~659. +# can break. Measured under DOSBox Staging 0.83: music ~1590, paused ~17, +# sfx ~108 -- the card's mixer holds digital 16 dB under FM (the canonical +# mix, src/dos/audio.c mixerBalance), so an effect is as loud as a voice. # # Keys are HELD (keydown, pause, keyup), not tapped: the demo tests # jlKeyPressed, an edge predicate polled once a frame, and a make/break @@ -174,12 +175,13 @@ parecPid="" [ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; } -# Thresholds sit at about half the measured levels (music ~1220, paused -# ~17, sfx ~659) so ordinary mixing jitter cannot flip the verdict. +# Thresholds sit at about half the measured levels (music ~1590, sfx +# ~108; paused ~17 under its ceiling) so ordinary mixing jitter cannot +# flip the verdict. python3 "$grader" "$label" "$work/out.wav" \ "music:1:5:600:" \ - "paused:8:13::200" \ - "sfx:16:22:330:" \ + "paused:8:13::40" \ + "sfx:16:22:54:" \ "resumed:27:32:600:" status=$? [ -n "${DOS_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/dos/headless-audio.wav" 2>/dev/null diff --git a/scripts/verify-dos-mouse.sh b/scripts/verify-dos-mouse.sh new file mode 100755 index 0000000..56c3296 --- /dev/null +++ b/scripts/verify-dos-mouse.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# verify-dos-mouse.sh - Headless mouse acceptance gate for the DOS port. +# The port has the mouse driver call it on every move and button change +# (INT 33h function 0Ch, a DPMI real-mode callback, src/dos/input.c); a +# callback that never runs leaves the pointer where it started. Runs +# KEYS.EXE in DOSBox Staging under Xvfb with the mouse captured from the +# start, moves it with xdotool (real X pointer motion), and decodes KEYS's +# mouse verification scanline (row 199: 5A5A, the button flags, X, Y as +# nibble pixels) from a raw screenshot after each step. Covered: the start +# at the stage's middle, a move each way on both axes, clamping at all +# four edges, an odd column, and press and release of the left and right +# buttons. +# DOSBox scales host motion into mickeys its own way, so a move is checked +# by its direction and only the clamps by position, and a one-pixel nudge +# by landing on an odd column: in mode 13h the driver steps X by 2 in a +# 640-wide virtual screen, which the port halves, so every column counts. +# +# Usage: scripts/verify-dos-mouse.sh +# Requires: DOSBox Staging (install.sh install emulator:dos), Xvfb, xdotool, python3 + PIL; +# build/dos/bin/KEYS.EXE (make dos). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/dosboxKeys.sh" +joeyNeed DOSBOX dosbox +bin_dir=$repo/build/dos/bin +conf=$repo/scripts/dosbox-386sx16.conf +label=verify-dos-mouse + +[ -f "$bin_dir/KEYS.EXE" ] || { echo "$label: build KEYS.EXE first (make dos)" >&2; exit 2; } + +work=$(mktemp -d -t joeylib-dos-mouse.XXXXXX) +display_num=$((RANDOM % 500 + 300)) +cleanup() { + joeyStop "${dosbox_pid:-}" + [ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null + rm -rf "$work" +} +trap cleanup EXIT + +mkdir -p "$work/shots" +dosboxKeysMapper "$work/mapper.txt" + +Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 & +xvfb_pid=$! +sleep 1 + +XDG_CONFIG_HOME="$work/xdg" DISPLAY=":$display_num" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$conf" \ + --set "capture_dir=$work/shots" \ + --set "mapperfile=$work/mapper.txt" \ + --set "mouse_capture=onstart" \ + -c "MOUNT C $bin_dir" -c "C:" -c "KEYS.EXE" \ + >"$work/dosbox.log" 2>&1 & +dosbox_pid=$! + +# Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16. +sleep 12 + +win=$(joeyFindWindow ":$display_num" "$dosbox_pid") +if [ -z "$win" ]; then + echo "$label: FAIL - DOSBox window not found" >&2 + exit 1 +fi +DISPLAY=":$display_num" xdotool windowactivate --sync "$win" 2>/dev/null || true + +# The mouse row of a fresh screenshot as "flags x y" (hex flags). +readRow() { + local png="$work/$1.png" + dosboxShot ":$display_num" "$win" "$work/shots" "$png" || return 1 + python3 - "$png" <<'PY' +import sys +from PIL import Image + +img = Image.open(sys.argv[1]).convert("RGB") +w, h = img.size +sx, sy = w // 320, h // 200 +if sx < 1 or sy < 1 or w % 320 or h % 200: + sys.exit(1) + +def dac8(c4): + v6 = ((c4 << 2) | (c4 >> 2)) & 0x3F + return (v6 << 2) | (v6 >> 4) + +# KEYS palette 0 (as verify-dos-input.sh decodes it). +pal12 = [(i << 8) | (i << 4) | i for i in range(16)] +pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF +pal12[15] = 0xF00 +palRgb = [(dac8((v >> 8) & 0xF), dac8((v >> 4) & 0xF), dac8(v & 0xF)) for v in pal12] + +def pix(px): + r, g, b = img.getpixel((px * sx, 199 * sy)) + return min(range(16), key=lambda n: (r - palRgb[n][0]) ** 2 + (g - palRgb[n][1]) ** 2 + (b - palRgb[n][2]) ** 2) + +nib = [pix(i) for i in range(16)] +words = [(nib[4 * k] << 12) | (nib[4 * k + 1] << 8) | (nib[4 * k + 2] << 4) | nib[4 * k + 3] for k in range(4)] +if words[0] != 0x5A5A: + sys.exit(1) +print("%04X %d %d" % (words[1], words[2], words[3])) +PY +} + +fails=() +step() { + local tag="$1" + local row + shift + if [ $# -gt 0 ]; then + DISPLAY=":$display_num" xdotool "$@" + sleep 1 + fi + if ! row=$(readRow "$tag"); then + fails+=("$tag: no mouse row in the screenshot") + row="---- -1 -1" + fi + read -r flags mx my <<< "$row" + echo " $tag: flags=$flags x=$mx y=$my" +} + +step start +[ "$flags $mx $my" = "1000 160 100" ] || fails+=("start: expected flags 1000 at 160,100") +step upLeft mousemove_relative -- -60 -40 +[ "$flags" = "1000" ] && [ "$mx" -lt 160 ] && [ "$my" -lt 100 ] || fails+=("a move up and left did not arrive") +lastX=$mx +lastY=$my +step downRight mousemove_relative -- 120 80 +[ "$flags" = "1000" ] && [ "$mx" -gt "$lastX" ] && [ "$my" -gt "$lastY" ] || fails+=("a move down and right did not arrive") +step topLeft mousemove_relative -- -2000 -2000 +[ "$flags $mx $my" = "1000 0 0" ] || fails+=("not held at the top left (0,0)") +step nudge mousemove_relative -- 1 0 +[ "$flags" = "1000" ] && [ $((mx % 2)) -eq 1 ] || fails+=("a one-pixel nudge did not reach an odd column") +step bottomRight mousemove_relative -- 2000 2000 +[ "$flags $mx $my" = "1000 319 199" ] || fails+=("not held at the bottom right (319,199)") +step leftDown mousedown 1 +[ "$flags" = "1100" ] || fails+=("the left button down not seen") +step leftUp mouseup 1 +[ "$flags" = "1000" ] || fails+=("the left button up not seen") +step rightDown mousedown 3 +[ "$flags" = "1010" ] || fails+=("the right button down not seen") +step rightUp mouseup 3 +[ "$flags" = "1000" ] || fails+=("the right button up not seen") + +if [ ${#fails[@]} -eq 0 ]; then + echo "$label: PASS (moves both ways, all four edges, every column, both buttons down and up)" + exit 0 +fi +for f in "${fails[@]}"; do + echo "$label: FAIL - $f" >&2 +done +exit 1 diff --git a/scripts/verify-dvx-display.sh b/scripts/verify-dvx-display.sh index 490eb13..496f231 100755 --- a/scripts/verify-dvx-display.sh +++ b/scripts/verify-dvx-display.sh @@ -12,7 +12,7 @@ # (P13) that draws one-pixel stripes in all 16 colours, bands and a small rect, moves # rows to another palette and changes that palette, presenting after each, # and asks the port after every present how many pixels differ from the -# stage (dvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content +# stage (jlDvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content # buffer in the desktop's own pixel format, every screen pixel of a stage # pixel's block (scale across; down, the rows the window's shape gives it: # 4:3 stretches every fifth row one more, square pixels take scale each), diff --git a/scripts/verify-dvx-fullscreen.sh b/scripts/verify-dvx-fullscreen.sh index ad93ffb..cf069b3 100755 --- a/scripts/verify-dvx-fullscreen.sh +++ b/scripts/verify-dvx-fullscreen.sh @@ -12,7 +12,7 @@ # Manager takes the focus, the desktop comes back and UBER presents into # its window) and resumed (the Task Manager's Switch To): the # screenshots must be the 320x200 mode, the desktop, then 320x200 again; -# DVX.LOG must show the mode set twice; and UBER's 51 hashes must equal +# DVX.LOG must show the mode set twice; and UBER's 54 hashes must equal # the DOS port's (tests/goldens/uber/dos.txt): the stage is DOS's # wherever it is shown. # 2. KEYS on the whole screen: the typed string must reach jlInputGetChar @@ -227,4 +227,4 @@ if [ -n "$fails" ]; then echo "verify-dvx-fullscreen: FAIL - $fails" exit 1 fi -echo "verify-dvx-fullscreen: PASS (UBER 51/51 on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)" +echo "verify-dvx-fullscreen: PASS (UBER 54/54 on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)" diff --git a/scripts/verify-iigs-keys.sh b/scripts/verify-iigs-keys.sh new file mode 100755 index 0000000..188f437 --- /dev/null +++ b/scripts/verify-iigs-keys.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# verify-iigs-keys.sh - Headless gate for the IIgs's key predicates, the +# asm jlKeyDown and jlKeyPressed (JL_HAS_INPUT_PREDICATES, joeyDraw.s). +# Boots GS/OS under MAME and launches the KEYS example off joey.2mg, which +# lights a key's cell while jlKeyDown says it is held and quits on +# jlKeyPressed(JL_KEY_ESCAPE). Holds A on the emulated ADB keyboard and +# reads the A cell straight out of SHR memory: unlit before, lit while +# held, unlit after. Then presses Escape: KEYS must quit to the Finder, +# whose 640-mode SCB replaces KEYS' 320-mode one. +# +# Usage: scripts/verify-iigs-keys.sh +# Requires: ./install.sh install iigs; build/iigs/bin/joey.2mg built +# with the current KEYS (make iigs-disk). +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +joeyNeed MAME mame +joeyNeed IIGS_ROMPATH iigsFirmware +joeyNeed GSOS_SYSTEM_DISK iigsFirmware + +sys_disk=$GSOS_SYSTEM_DISK +data_disk=$repo/build/iigs/bin/joey.2mg + +for f in "$sys_disk" "$data_disk"; do + [ -f "$f" ] || { echo "verify-iigs-keys: missing $f (run 'make iigs-disk')" >&2; exit 2; } +done + +work=$(mktemp -d -t joeylib-keys-verify.XXXXXX) +trap 'rm -rf "$work"' EXIT +cp "$sys_disk" "$work/boot.po" +cp "$data_disk" "$work/joey.2mg" + +# Finder keystroke timeline as in verify-iigs-input.sh. The A cell is +# KEYS' row 2, column 0 (examples/keys: MARGIN 2, CELL 28x26, GAP 4); its +# middle, x 16 y 75, is the SHR byte $E1/2000 + 75*160 + 8 = $E1/4EE8: +# $11 unlit (COLOR_UNLIT), $22 lit (COLOR_LIT). SCB 0 is $E1/9D00. +cat > "$work/verify.lua" <<'LUA' +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local nat = manager.machine.natkeyboard +local frame = 0 +local idx = 1 + +local function field(port, name) + local p = manager.machine.ioport.ports[port] + if p == nil then return nil end + return p.fields[name] +end +local key_cmd = field(":macadb:KEY3", "Command / Open Apple") +local key_a = field(":macadb:KEY0", "a A") +local key_esc = field(":macadb:KEY3", "Esc") +local function press(f) if f then f:set_value(1) end end +local function release(f) if f then f:set_value(0) end end + +local function report(tag) + io.write(string.format("VERIFY-IIGS-KEYS %s scb0=%02X cell=%02X\n", tag, mem:read_u8(0xE19D00), mem:read_u8(0xE14EE8))) + io.flush() +end + +local steps = { + {3000, function() nat:post("J") end}, + {3120, function() press(key_cmd) end}, + {3126, function() nat:post("o") end}, + {3180, function() release(key_cmd) end}, + {3540, function() nat:post("KEYS") end}, + {3660, function() press(key_cmd) end}, + {3666, function() nat:post("o") end}, + {3720, function() release(key_cmd) end}, + {6600, function() report("before") end}, + {6610, function() press(key_a) end}, + {6700, function() report("held") end}, + {6710, function() release(key_a) end}, + {6800, function() report("released") end}, + {6810, function() press(key_esc) end}, + {6830, function() release(key_esc) end}, + {9000, function() report("escape"); manager.machine:exit() end}, +} + +emu.register_frame_done(function() + frame = frame + 1 + while idx <= #steps and frame >= steps[idx][1] do + steps[idx][2]() + idx = idx + 1 + end +end) +LUA + +cd "$work" +out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout 300 "$MAME" apple2gs \ + -rompath "$IIGS_ROMPATH" \ + -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/verify.lua" </dev/null 2>&1) || true + +report=$(echo "$out" | grep -E '^VERIFY-IIGS-KEYS ' || true) +echo "$report" +value() { + echo "$report" | grep " $1 " | sed -E "s/.*$2=([0-9A-Fa-f]+).*/\1/" +} +fails=() +[ "$(value before scb0)" = "00" ] || fails+=("KEYS never launched (SCB 0 $(value before scb0))") +[ "$(value before cell)" = "11" ] || fails+=("the A cell lit before A was held ($(value before cell))") +[ "$(value held cell)" = "22" ] || fails+=("jlKeyDown: the A cell unlit while A was held ($(value held cell))") +[ "$(value released cell)" = "11" ] || fails+=("jlKeyDown: the A cell still lit after A was let go ($(value released cell))") +scb=$(value escape scb0) +[ -n "$scb" ] && [ $((16#$scb)) -ge 128 ] || fails+=("jlKeyPressed: Escape did not quit KEYS (SCB 0 ${scb:-none})") +if [ ${#fails[@]} -eq 0 ]; then + echo "verify-iigs-keys: PASS (A lit only while held; Escape quit KEYS)" + exit 0 +fi +for f in "${fails[@]}"; do + echo "verify-iigs-keys: FAIL - $f" >&2 +done +exit 1 diff --git a/scripts/verify-win16-display.sh b/scripts/verify-win16-display.sh index 9406e28..4ed17e7 100755 --- a/scripts/verify-win16-display.sh +++ b/scripts/verify-win16-display.sh @@ -11,7 +11,7 @@ # so a stale or misplaced rectangle would never reach a hash. UBER's probe # build (make win16 builds it, into build/win16/probe) ends with P13, the # battery verify-dvx-display.sh runs on DVX, which after every step asks the -# backend (hostWin16ProbeDisplayMismatch in src/hosted/win16/win16Backend.c) +# backend (jlHostWin16ProbeDisplayMismatch in src/hosted/win16/win16Backend.c) # how many pixels differ: the frame's against the stage, the window as the # shows left it against the window fully repainted, and, at a whole-number # scale, that repaint against GDI's own stretch of the frame. In the diff --git a/scripts/verify-win16-serial-fast.sh b/scripts/verify-win16-serial-fast.sh new file mode 100755 index 0000000..9668514 --- /dev/null +++ b/scripts/verify-win16-serial-fast.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# verify-win16-serial-fast.sh - the Win16 port's serial throughput and loss +# gate. SERFAST (SERTEST at 115200, make/examples.mk) runs on Windows 3.11 +# under DOSBox with COM1 a nullmodem to the host peer +# (scripts/lib/serialLoopPeer.py), and the KPCOMM driver every Win16 run gets +# (win16Stage), which takes a COM port past 57600. After the banner and the +# probe's echo the peer streams STREAM_BYTES bytes through SERTEST's echo, a +# window at a time, and every one must come back once and in order, at no +# less than three quarters of what 115200 baud carries. The rate was measured +# once by hand when KPCOMM came in (11485 bytes a second echoed); this keeps +# it, and its loss, under test. +# +# Usage: scripts/verify-win16-serial-fast.sh +# Env: WIN16_SERIAL_PORT the nullmodem's TCP port (default 6805) +# Exit: 0 PASS, 1 FAIL, 2 SKIP +# Requires: Windows on DOSBox (install.sh install emulator:win16), python3; +# the win16 toolchain (it builds SERFAST.EXE, make win16-serfast). + +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/win16Gate.sh" + +label=verify-win16-serial-fast +win16Need +make -s -f "$repo/make/win16.mk" win16-serfast || { + echo "$label: FAIL - building SERFAST.EXE failed" >&2 + exit 1 +} +program=$(find "$repo/build/win16/test" -name SERFAST.EXE | head -1) +[ -f "$program" ] || { echo "$label: FAIL - no SERFAST.EXE after the build" >&2; exit 1; } +port="${WIN16_SERIAL_PORT:-6805}" +baud=115200 +streamBytes=16384 +# The peer's budget: Windows' boot, the banner and the echo, then the stream. +wall=120 + +work=$(mktemp -d -t joeylib-win16-serial-fast.XXXXXX) +peerPid="" +cleanup() { + joeyStop "$peerPid" + win16Stop + rm -rf "$work" +} +trap cleanup EXIT + +python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$wall" --stream "$streamBytes" --rate "$baud" > "$work/peer.log" 2>&1 & +peerPid=$! +sleep 1 +mkdir -p "$work/program" +cp "$program" "$work/program/SERFAST.EXE" +win16Stage "$work" "$work/program" SERFAST.EXE +# transparent:1 -- the bytes alone, no modem lines in the stream. +WIN16_DOSBOX_ARGS=(--set "serial1=nullmodem server:127.0.0.1 port:$port transparent:1") +win16Start "$work" +wait "$peerPid" +rc=$? +peerPid="" +cat "$work/peer.log" +if [ "$rc" -eq 0 ]; then + echo "$label: PASS ($streamBytes bytes through COM1 at $baud baud and back, none lost)" +else + echo "$label: FAIL" >&2 +fi +exit "$rc" diff --git a/scripts/verify-win32-display.sh b/scripts/verify-win32-display.sh index f19fb1e..26c7684 100755 --- a/scripts/verify-win32-display.sh +++ b/scripts/verify-win32-display.sh @@ -12,7 +12,7 @@ # a hash. UBER's probe build (make win32 builds it, into build/win32/probe) # ends with P13, the battery verify-dvx-display.sh and # verify-win16-display.sh run, which after every step asks the backend -# (hostWin32ProbeDisplayMismatch in src/hosted/win32/win32Backend.c) how +# (jlHostWin32ProbeDisplayMismatch in src/hosted/win32/win32Backend.c) how # many pixels differ: the frame's against the stage, the window as the # shows left it against the window fully repainted, and that repaint # against GDI stretching the core's frame itself. Every count must be 0, diff --git a/scripts/verify-x68000-display.sh b/scripts/verify-x68000-display.sh new file mode 100755 index 0000000..425df76 --- /dev/null +++ b/scripts/verify-x68000-display.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT +# +# verify-x68000-display.sh - the X68000's present, checked on the text-plane +# page it draws. UBER's golden hashes are of the stage; a present that leaves +# stale rows on screen passes them. This page flips: each present draws into +# the page not shown, which holds the frame before last, from the union of +# two frames' dirty bands. UBER built with UBER_PROBE runs its X68000 display +# row (op_probeX68kDisplayCheck): a battery of draws, each presented and the +# drawn page compared with the stage's planes (jlX68kProbeDisplayMismatch in +# src/x68000/hal.c). The total must be 0. +# +# Usage: scripts/verify-x68000-display.sh +# Exit: 0 PASS, 1 FAIL +# Requires: MAME and the X68000 firmware (install.sh install emulator:x68000), +# the library built (make x68000). + +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +label=verify-x68000-display +log=$repo/build/x68000/joeylog.txt + +rm -f "$log" +if ! X68K_BENCH_CFLAGS="-DUBER_PROBE -DUBER_FRAMES=1" "$repo/scripts/bench-x68000.sh" > /dev/null 2>&1; then + echo "$label: FAIL - the UBER_PROBE run did not finish" >&2 + exit 1 +fi +line=$(grep "UBER-PROBE: x68kDisplay" "$log" 2>/dev/null) +if [[ "$line" == *" PASS (mismatch=0)" ]]; then + echo "$label: PASS - ${line#UBER-PROBE: }" + exit 0 +fi +echo "$label: FAIL - ${line:-no x68kDisplay line in its log}" >&2 +exit 1 diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh index 39dd2d4..265a546 100755 --- a/scripts/verify-x68000-golden.sh +++ b/scripts/verify-x68000-golden.sh @@ -61,7 +61,7 @@ x68kDataDisk "$work/data.xdf" || exit 1 x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1 # Completion-driven exit. UBER's last act before jlWaitForAnyKey is -# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM +# jlSurfaceClear(jlgStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM # is the "hashes are computed and flushed" signal. The benchmark's own # jlSurfaceClear/jlStagePresent ops can paint that colour for an instant, so the # reading only counts once it has held for GREEN_STABLE frames -- the finished diff --git a/scripts/verify-x68000-scb.sh b/scripts/verify-x68000-scb.sh index 0acc99b..532ddf8 100755 --- a/scripts/verify-x68000-scb.sh +++ b/scripts/verify-x68000-scb.sh @@ -9,7 +9,7 @@ # # The X68000 port presents to the text plane, which has ONE 16-entry palette, # so it honours the SCB by reloading that palette as the beam scans: a CRTC -# raster interrupt (src/x68000/x68kRasterIsr.s) stores a band's sixteen +# raster interrupt (src/x68000/jlX68kRasterIsr.s) stores a band's sixteen # colours in the horizontal blanking before the band's first line. Each reload # has a window of about 400 dots (~18 us): after the last stage pixel of the # line above and before the first stage pixel of the band. Land early and the @@ -48,19 +48,19 @@ # taps. # exit after ESC the program exits and gives the $138 vector and MFP # channel 14 back -- and the two clock vectors it held while it -# ran, which are checked then: $114 (x68kTickIsr, chained onto -# Human68k's Timer C) and $134 (x68kVdispIsr, the frame counter +# ran, which are checked then: $114 (jlX68kTickIsr, chained onto +# Human68k's Timer C) and $134 (jlX68kVdispIsr, the frame counter # _VDISPST installs, with MFP channel 13 enabled). # # It also reports the interrupt's cost (time inside it per frame), the latency # from each raster match to the vector fetch, and the slack of the compare -# write the interrupt's timing rests on (x68kRasterIsr.s step 2). +# write the interrupt's timing rests on (jlX68kRasterIsr.s step 2). # # Human68k's Timer C tick runs throughout, and text is typed during the whole # capture so the IOCS keyboard interrupt runs too: both are MFP interrupts at # IPL 6, like the raster interrupt, and a raster match that falls inside one of # them is taken late (the Timer C dispatcher used to make reloads miss their -# line; see src/x68000/x68kTickIsr.s). The DMAC-driven ADPCM path is not +# line; see src/x68000/jlX68kTickIsr.s). The DMAC-driven ADPCM path is not # exercised: JoeyLib's audio does its work on the main thread # (src/x68000/audioPcm.c). # @@ -132,12 +132,12 @@ local TEXT_PALETTE, COLORS = 0xE82200, 16 local CRTC, CRTC_R09 = 0xE80000, 0xE80012 local MFP_IERA, MFP_IMRA, MFP_ISRA = 0xE88007, 0xE88013, 0xE8800F local RASTER_VECTOR = 0x138 -local TICK_VECTOR = 0x114 -- MFP channel 5, Timer C: x68kTickIsr -local VDISP_VECTOR = 0x134 -- MFP channel 13, Timer A: x68kVdispIsr +local TICK_VECTOR = 0x114 -- MFP channel 5, Timer C: jlX68kTickIsr +local VDISP_VECTOR = 0x134 -- MFP channel 13, Timer A: jlX68kVdispIsr local TIMER_A_BIT = 0x20 -- channel 13 in IERA / IMRA local PROGRAM_RAM_END = 0x01000000 -- IOCS's own test for "a handler is installed" local GPIP6_BIT = 0x40 -local ISRA_EXIT = 0xBF -- x68kRasterIsr's in-service ack +local ISRA_EXIT = 0xBF -- jlX68kRasterIsr's in-service ack local CPU_HZ = 10000000 -- 40 MHz / 4 (MAME x68k.cpp) local IRQ_HPOS = 2 -- x68k_crtc.cpp: R09 = N fires at (N - 1, 2) @@ -410,7 +410,7 @@ end -- Interrupt cost, latency and slack from the entry/exit/R09 events. The first -- R09 write after an entry re-points the compare at the raster of the line --- above the band (x68kRasterIsr.s step 2); it must land before that raster +-- above the band (jlX68kRasterIsr.s step 2); it must land before that raster -- begins, and its slack is how far ahead it did. The last R09 write before an -- entry is the arm that raised it. local function reportInterrupt() diff --git a/scripts/verify-x68000-serial-mouse.sh b/scripts/verify-x68000-serial-mouse.sh index dce159e..0c56f98 100755 --- a/scripts/verify-x68000-serial-mouse.sh +++ b/scripts/verify-x68000-serial-mouse.sh @@ -6,7 +6,7 @@ # verify-x68000-serial-mouse.sh - the X68000's RS-232C port and mouse at once. # # Both live on the one Z8530 SCC: RS-232C on channel A, where JoeyLib's serial -# add-on owns the receive interrupt (src/x68000/x68kSccRxIsr.s), and the mouse +# add-on owns the receive interrupt (src/x68000/jlX68kSccRxIsr.s), and the mouse # on channel B, where the IOCS ROM owns the receive interrupt and solicits a # packet every other Timer C tick. The chip has ONE register pointer for both # channels, so a select-then-access register sequence split by the other diff --git a/scripts/verify-x68000-serial-scb.sh b/scripts/verify-x68000-serial-scb.sh index 161aa49..f88870b 100755 --- a/scripts/verify-x68000-serial-scb.sh +++ b/scripts/verify-x68000-serial-scb.sh @@ -7,7 +7,7 @@ # per-line palette change: the SCB raster interrupt and the SCC together. # # The X68000 port honours the SCB by reloading the text plane's one palette as -# the beam scans (src/x68000/x68kRasterIsr.s). A palette change on every line +# the beam scans (src/x68000/jlX68kRasterIsr.s). A palette change on every line # keeps that interrupt at IPL 6 for the whole run, about one raster (31.8 us) # a change plus two rasters of lead, and the SCC is IPL 5, so every byte that # arrives in the run waits in the Z8530's receive FIFO. No handler can let the diff --git a/src/amiga/amigaCustom.h b/src/amiga/amigaCustom.h index 73c5a4c..e936698 100644 --- a/src/amiga/amigaCustom.h +++ b/src/amiga/amigaCustom.h @@ -19,4 +19,17 @@ extern struct Custom custom; static volatile struct Custom *const gCust = &custom; +// BLTSIZE packs a blit's row count in its high 10 bits (<< 6) and its word +// count in the low 6 (a width of 0 would mean 64 words). +#define AMIGA_BLT_SIZESHIFT 6 +#define AMIGA_BLT_MASK_ALL 0xFFFFu + +// Minterms that keep the destination through C. With A the constant $FFFF +// cut by the first/last-word masks (amigaBlitterMaskedFillOne), set-bits is +// D = A OR C (the masked bits on, the rest kept) and clear-bits D = (NOT A) +// AND C (the masked bits off); with A source data, D = A OR C adds A's bits +// (amigaTileLayer.c's edge words). +#define AMIGA_BLT_MINTERM_SET 0xFAu // D = A | C +#define AMIGA_BLT_MINTERM_CLEAR 0x0Au // D = ~A & C + #endif diff --git a/src/amiga/amigaTileLayer.c b/src/amiga/amigaTileLayer.c new file mode 100644 index 0000000..32d19ec --- /dev/null +++ b/src/amiga/amigaTileLayer.c @@ -0,0 +1,199 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// The Amiga's tile-layer ring and blit. The ring is the shared +// separate-plane one (src/m68k/planarTileLayer.h has the layout, +// planarTileLayer.c the Put and the CPU blit), put in chip RAM when there is +// room, so that the blitter can copy the band into a chip-RAM surface; the +// CPU blit copies it otherwise. +// +// Word k of a band plane row is (P[w + k] << s) | (P[w + k + 1] >> (16 - s)), +// P being that plane's 21 words of the ring row, w = ringX / 16, s = ringX & +// 15, and the indices wrapping at 21. Words 0..19 take their P[w + k] from P[w] +// to the ring row's end and then on from P[0]: at most two runs of whole +// source words, the second landing at band word 21 - w. A blit's channels +// walk the same number of words, and a run's last word needs one source word +// more than the run has, so a run is two blits: +// * the run itself, descending, so the blitter's shift goes left: A is +// the run's words shifted s, D the band's. Each band word takes its low +// bits from the source word to its right; the run's last band word, with +// none in the run, takes them from what the blitter shifted last. +// * its edge (s > 0): the source word after the run, ascending, shifted +// right 16 - s into the low s bits and or'd into the run's last band +// word through C. +// What a blit shifts in at a row's first word is the last A word of the row +// before, masked -- or, on its first row, of the blit before. So the masks +// keep each blit's last word to the bits the next one may take: a run's +// last-word mask clears the high s bits of each row's leftmost source word +// (the bits a run's next row would take), and an edge's masks keep only the +// high s bits it moves (an edge takes none). The runs all go first, then +// the edges, each group after a blit of a masked-off word, so its first row +// shifts in zeros too: the runs leave their last band words' low s bits +// clear for the edges to or into. +// +// The band's rows wrap at the ring's last row too, so each run and edge is a +// blit per plane per vertical stretch: at most 32 a band, each a register +// setup while the blitter moves 20 words a row per plane. + +#include <stdbool.h> +#include <stdint.h> + +#include <exec/memory.h> +#include <hardware/blit.h> +#include <proto/exec.h> +#include <proto/graphics.h> + +#include "amigaCustom.h" +#include "planarTileLayer.h" +#include "port.h" + +#define BAND_WORDS (PLANAR_TILE_BYTES_PER_ROW / 2u) +#define WORD_BITS 16u +#define SHIFT_MASK (WORD_BITS - 1u) + +static void blitBandRows(const uint8_t *srcRow, uint8_t *dstRow, uint16_t rows, uint16_t w, uint16_t s, bool edges); +static void blitEdge(const uint8_t *src, uint8_t *dst, uint16_t rows, uint16_t s); +static void blitRun(const uint8_t *src, uint8_t *dst, uint16_t words, uint16_t rows, uint16_t s); +static void blitZeroCarry(void); + + +// `rows` rows of one plane, its runs or (edges) its edges: source plane rows +// from srcRow (one ring row apart), band plane rows from dstRow. +static void blitBandRows(const uint8_t *srcRow, uint8_t *dstRow, uint16_t rows, uint16_t w, uint16_t s, bool edges) { + uint16_t words1; + uint16_t words2; + + words1 = (uint16_t)(PLANAR_RING_PLANE_WORDS - w); + if (words1 > BAND_WORDS) { + words1 = BAND_WORDS; + } + words2 = (uint16_t)(BAND_WORDS - words1); + if (edges) { + blitEdge(srcRow + ((w + words1) % PLANAR_RING_PLANE_WORDS) * 2u, dstRow + (words1 - 1u) * 2u, rows, s); + if (words2 != 0u) { + blitEdge(srcRow + words2 * 2u, dstRow + (BAND_WORDS - 1u) * 2u, rows, s); + } + return; + } + blitRun(srcRow + w * 2u, dstRow, words1, rows, s); + if (words2 != 0u) { + blitRun(srcRow, dstRow + words1 * 2u, words2, rows, s); + } +} + + +// The source word at src, shifted right 16 - s, or'd into the band word at +// dst, row after row. +static void blitEdge(const uint8_t *src, uint8_t *dst, uint16_t rows, uint16_t s) { + uint16_t keep; + + keep = (uint16_t)~(AMIGA_BLT_MASK_ALL >> s); + WaitBlit(); + gCust->bltcon0 = (uint16_t)(((WORD_BITS - s) << ASHIFTSHIFT) | SRCA | SRCC | DEST | AMIGA_BLT_MINTERM_SET); + gCust->bltcon1 = 0u; + gCust->bltafwm = keep; + gCust->bltalwm = keep; + gCust->bltamod = (int16_t)(PLANAR_RING_ROW_BYTES - 2u); + gCust->bltcmod = (int16_t)(PLANAR_TILE_BYTES_PER_ROW - 2u); + gCust->bltdmod = (int16_t)(PLANAR_TILE_BYTES_PER_ROW - 2u); + gCust->bltapt = (APTR)src; + gCust->bltcpt = (APTR)dst; + gCust->bltdpt = (APTR)dst; + gCust->bltsize = (uint16_t)((rows << AMIGA_BLT_SIZESHIFT) | 1u); +} + + +// `words` source words from src shifted left s into the band words from +// dst, row after row, descending: the pointers start at the last row's last +// word. +static void blitRun(const uint8_t *src, uint8_t *dst, uint16_t words, uint16_t rows, uint16_t s) { + uint32_t last; + + last = (uint32_t)(words - 1u) * 2u; + WaitBlit(); + gCust->bltcon0 = (uint16_t)((s << ASHIFTSHIFT) | SRCA | DEST | A_TO_D); + gCust->bltcon1 = BLITREVERSE; + gCust->bltafwm = AMIGA_BLT_MASK_ALL; + gCust->bltalwm = (uint16_t)(AMIGA_BLT_MASK_ALL >> s); + gCust->bltamod = (int16_t)(PLANAR_RING_ROW_BYTES - words * 2u); + gCust->bltdmod = (int16_t)(PLANAR_TILE_BYTES_PER_ROW - words * 2u); + gCust->bltapt = (APTR)(src + (uint32_t)(rows - 1u) * PLANAR_RING_ROW_BYTES + last); + gCust->bltdpt = (APTR)(dst + (uint32_t)(rows - 1u) * PLANAR_TILE_BYTES_PER_ROW + last); + gCust->bltsize = (uint16_t)((rows << AMIGA_BLT_SIZESHIFT) | words); +} + + +// A one-word blit of a word the masks clear, through no channel, so the next +// blit's first row shifts in zeros. +static void blitZeroCarry(void) { + WaitBlit(); + gCust->bltcon0 = 0u; + gCust->bltcon1 = 0u; + gCust->bltadat = 0u; + gCust->bltafwm = 0u; + gCust->bltalwm = 0u; + gCust->bltsize = (uint16_t)((1u << AMIGA_BLT_SIZESHIFT) | 1u); +} + + +void jlpTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) { + const AmigaPlanarT *pd; + const uint8_t *ring; + uint8_t *band; + uint16_t rows; + uint16_t rows1; + uint16_t w; + uint16_t s; + uint16_t p; + uint16_t pass; + + pd = (const AmigaPlanarT *)dst->portData; + if (!pd->chip || (TypeOfMem(layer->ring) & MEMF_CHIP) == 0u) { + jlPlanarTileLayerCpuBlit(dst, layer, ringX, ringY); + return; + } + ring = (const uint8_t *)layer->ring; + rows = (uint16_t)((uint16_t)layer->viewHeight * JL_TILE_PIXELS_PER_SIDE); + rows1 = (uint16_t)((uint16_t)layer->ringRows * JL_TILE_PIXELS_PER_SIDE - ringY); + if (rows1 > rows) { + rows1 = rows; + } + w = (uint16_t)(ringX >> 4); + s = (uint16_t)(ringX & SHIFT_MASK); + OwnBlitter(); + // Pass 0 the runs, pass 1 (s > 0) the edges. + for (pass = 0u; pass < (s != 0u ? 2u : 1u); pass++) { + blitZeroCarry(); + for (p = 0u; p < PLANAR_TILE_PLANES; p++) { + band = pd->planes[p] + (uint16_t)layer->viewBy * JL_TILE_PIXELS_PER_SIDE * PLANAR_TILE_BYTES_PER_ROW; + blitBandRows(ring + (uint32_t)ringY * PLANAR_RING_ROW_BYTES + p * PLANAR_RING_PLANE_BYTES, band, rows1, w, s, pass != 0u); + if (rows1 < rows) { + blitBandRows(ring + p * PLANAR_RING_PLANE_BYTES, band + (uint32_t)rows1 * PLANAR_TILE_BYTES_PER_ROW, (uint16_t)(rows - rows1), w, s, pass != 0u); + } + } + } + WaitBlit(); + DisownBlitter(); +} + + +// Chip RAM first, so the blitter can read it; anywhere if chip RAM is short. +bool jlpTileLayerRingAlloc(jlTileLayerT *layer) { + ULONG bytes; + + bytes = (ULONG)layer->ringRows * PLANAR_RING_SLOT_BYTES; + layer->ring = AllocMem(bytes, MEMF_CHIP | MEMF_CLEAR); + if (layer->ring == NULL) { + layer->ring = AllocMem(bytes, MEMF_ANY | MEMF_CLEAR); + } + return layer->ring != NULL; +} + + +void jlpTileLayerRingFree(jlTileLayerT *layer) { + if (layer->ring != NULL) { + FreeMem(layer->ring, (ULONG)layer->ringRows * PLANAR_RING_SLOT_BYTES); + layer->ring = NULL; + } +} diff --git a/src/amiga/audio.c b/src/amiga/audio.c index c23e87f..768081c 100644 --- a/src/amiga/audio.c +++ b/src/amiga/audio.c @@ -52,7 +52,7 @@ extern UBYTE mt_E8Trigger; #define PAULA_PERIOD_MIN 124 // Paula HW limit #define PAULA_PERIOD_MAX 65535 // The fastest rate Paula plays: an effect faster than this plays decimated -// (audioDecimate), at its pitch. +// (jlAudioDecimate), at its pitch. #define PAULA_MAX_RATE ((uint16_t)(PAULA_CLOCK_PAL / PAULA_PERIOD_MIN)) // A one-shot's head, in Chip RAM before it starts: the rest is copied while // Paula plays it (jlpAudioPlaySfx). @@ -122,7 +122,7 @@ extern UBYTE mt_E8Trigger; typedef struct { UBYTE *samples; // Chip-RAM copy of one SFX sample - ULONG sampleBytes; // its allocation + ULONG sampleBytes; // its allocation // ---- streaming (jlpAudioPlaySfxStream) ---- UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK BYTE cha; // Paula channel, CHANNEL_PICK = centred @@ -132,8 +132,8 @@ typedef struct { void *ctx; uint16_t rateHz; uint16_t dueFrame; // the VBL count the next chunk goes out at - uint8_t decimate; // every decimate-th sample plays (audioDecimate), at rateHz - uint8_t phase; // audioDecimate's phase between chunks + uint8_t decimate; // every decimate-th sample plays (jlAudioDecimate), at rateHz + uint8_t phase; // jlAudioDecimate's phase between chunks bool active; } SfxSlotT; @@ -150,11 +150,11 @@ static bool gNoiseOn = false; // never take it (maskNoiseChannel), so it is most often free. static const BYTE kLeftChannels[SIDE_CHANNELS] = { NOISE_CHANNEL, 0 }; static const BYTE kRightChannels[SIDE_CHANNELS] = { 2, 1 }; -static bool gInstalled = false; -static bool gModPlaying = false; -static bool gWantLoopOnce = false; // honor loop=false via E8 marker -static BYTE *gVoiceChip = NULL; // chip-RAM square shared by all voices -static BYTE *gNoiseChip = NULL; // chip-RAM LFSR noise for channel 3 +static bool gInstalled = false; +static bool gModPlaying = false; +static bool gWantLoopOnce = false; // honor loop=false via E8 marker +static BYTE *gVoiceChip = NULL; // chip-RAM square shared by all voices +static BYTE *gNoiseChip = NULL; // chip-RAM LFSR noise for channel 3 // Paula periods for noise pitch 0..31: the sample plays at the rate every // port gives that pitch, the YM2149's 125000 / pitch Hz (pitch 0 as 1), so @@ -403,7 +403,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { if (chip == NULL) { return; } - if (loop || !modStopMarkerAdd(data, length, chip)) { + if (loop || !jlModStopMarkerAdd(data, length, chip)) { memcpy(chip, data, length); } gModuleChip = chip; @@ -483,7 +483,7 @@ static bool streamChunk(uint8_t slot) { return false; } if (s->decimate > 1u) { - got = audioDecimate(s->chunk, s->chunk, got, s->decimate, &s->phase); + got = jlAudioDecimate(s->chunk, s->chunk, got, s->decimate, &s->phase); } } while (got < 2u); got &= ~1u; // Paula counts 16-bit words @@ -523,9 +523,9 @@ static bool tickOpen(void) { gTickPort.mp_Flags = PA_SOFTINT; gTickPort.mp_SoftInt = &gTickSoftInt; // An empty list, as amiga.lib's NewList makes one. - gTickPort.mp_MsgList.lh_Head = (struct Node *)&gTickPort.mp_MsgList.lh_Tail; - gTickPort.mp_MsgList.lh_Tail = NULL; - gTickPort.mp_MsgList.lh_TailPred = (struct Node *)&gTickPort.mp_MsgList.lh_Head; + gTickPort.mp_MsgList.lh_Head = (struct Node *)&gTickPort.mp_MsgList.lh_Tail; + gTickPort.mp_MsgList.lh_Tail = NULL; + gTickPort.mp_MsgList.lh_TailPred = (struct Node *)&gTickPort.mp_MsgList.lh_Head; gTickRequest.tr_node.io_Message.mn_Node.ln_Type = NT_REPLYMSG; gTickRequest.tr_node.io_Message.mn_ReplyPort = &gTickPort; gTickRequest.tr_node.io_Message.mn_Length = (UWORD)sizeof(gTickRequest); @@ -601,7 +601,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 return; } // Faster than Paula plays: every factor-th sample, at its pitch. - factor = audioDecimationFactor(rateHz, PAULA_MAX_RATE); + factor = jlAudioDecimationFactor(rateHz, PAULA_MAX_RATE); kept = (length + factor - 1u) / factor; if (kept < 2 || kept > 0x1FFFEUL) { return; // Paula sample length is 16 bits of words = 128KB max @@ -626,7 +626,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 // long or fast sample starts as soon as a short one. head = kept < SFX_HEAD_BYTES ? kept : SFX_HEAD_BYTES; phase = 0u; - (void)audioDecimate(chip, sample, head * factor, factor, &phase); + (void)jlAudioDecimate(chip, sample, head * factor, factor, &phase); words = (UWORD)(kept >> 1); s->samples = chip; @@ -639,7 +639,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 sfx.sfx_pri = SLOT_PRIORITY(slot); sfxPlay(slot, &sfx); if (kept > head) { - (void)audioDecimate(chip + head, sample + head * factor, length - head * factor, factor, &phase); + (void)jlAudioDecimate(chip + head, sample + head * factor, length - head * factor, factor, &phase); } } @@ -668,7 +668,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin } } // Faster than Paula plays: every decimate-th sample, at its pitch. - s->decimate = audioDecimationFactor(rateHz, PAULA_MAX_RATE); + s->decimate = jlAudioDecimationFactor(rateHz, PAULA_MAX_RATE); s->phase = 0u; s->fill = fill; s->ctx = ctx; diff --git a/src/amiga/hal.c b/src/amiga/hal.c index 49854d7..d1299e7 100644 --- a/src/amiga/hal.c +++ b/src/amiga/hal.c @@ -86,7 +86,6 @@ static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], static void amigaBlitterCopyPlanesOwned(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES], uint16_t offsetBytes, uint16_t widthWords, uint16_t height, uint16_t modBytes); static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, bool setBits); static void amigaBlitterMaskedFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, uint16_t fwm, uint16_t lwm, bool setBits); -static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color); static void amigaPresentFlushRun(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITPLANES], int16_t runFirst, int16_t runLast, uint16_t byteStart, uint16_t byteLen, bool *blitOwned); static void amigaPresentRectInner(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITPLANES], int16_t firstRow, int16_t lastRow, uint16_t byteStart, uint16_t bytesPerRow, bool *blitOwned); static uint8_t bandListUpdate(const jlSurfaceT *src); @@ -105,7 +104,7 @@ static void trampolinePoint(const uint16_t *list); // ----- Module state ----- -struct Screen *gScreen = NULL; // shared with input.c +struct Screen *jlgScreen = NULL; // shared with input.c static struct BitMap *gBitMap = NULL; // Phase 10 page-flip: two CHIP plane sets. gPlanes is set A (kept // under the original name to keep diffs minimal); gPlanesB is set B. @@ -216,7 +215,7 @@ static uint8_t gPrevStageMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))) // present scan emits these into gPresentRuns; a separate loop flushes // each (blitter or CPU per amigaPresentRectInner). Splitting scan from // flush lets the register-pressure-bound scan move to hand-rolled asm -// (amigaPresentScan68k) without touching the flush path. Field layout +// (jlAmigaPresentScan68k) without touching the flush path. Field layout // (firstRow@0, lastRow@2, byteStart@4, byteLen@6; 8 bytes) is an ABI // contract with that asm. At most one run per row, so JL_SURFACE_HEIGHT // entries always suffice. @@ -236,7 +235,7 @@ static AmigaPresentRunT gPresentRuns[JL_SURFACE_HEIGHT]; // 17. The asm hardcodes JL_SURFACE_HEIGHT, SURFACE_WORDS_PER_ROW, the clean // sentinel, and the 8-byte run record; the guards below make a mismatch a // compile error rather than a display bug. -extern uint32_t amigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax, uint8_t *prvMin, uint8_t *prvMax, AmigaPresentRunT *runList); +extern uint32_t jlAmigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax, uint8_t *prvMin, uint8_t *prvMax, AmigaPresentRunT *runList); typedef char amigaPresentScanAbiHeight[(JL_SURFACE_HEIGHT == 200) ? 1 : -1]; typedef char amigaPresentScanAbiWords[(SURFACE_WORDS_PER_ROW == 80) ? 1 : -1]; @@ -253,19 +252,15 @@ typedef char amigaPresentScanAbiRec[(sizeof(AmigaPresentRunT) == 8) ? 1 : -1]; // presents still run. static bool gPrevAnyDirty = true; -// Change detection is the core gStageScbDirty / gStagePaletteDirty flags +// Change detection is the core jlgStageScbDirty / jlgStagePaletteDirty flags // (surfaceInternal.h): the mutation APIs set them, and jlpPresent clears // them once it has written the band list they ask for (bandListUpdate). // ----- Internal helpers (alphabetical) ----- // Blitter geometry shared by every block op here: each plane row is -// AMIGA_BYTES_PER_ROW (40) bytes = 20 words. BLTSIZE packs the row -// count in the high 10 bits (<< 6) and the word count in the low 6 -// bits (a width of 0 would mean 64 words, but our rows are 20 wide). +// AMIGA_BYTES_PER_ROW (40) bytes = 20 words (amigaCustom.h has BLTSIZE). #define AMIGA_BLT_WORDS_PER_ROW (AMIGA_BYTES_PER_ROW / 2u) // 20 -#define AMIGA_BLT_SIZESHIFT 6 -#define AMIGA_BLT_MASK_ALL 0xFFFFu // BLTSIZE field guard (P7-3 audit follow-up): height occupies bits // 15..6 (10-bit == max 1024 rows) and width bits 5..0 (6-bit == max 64 @@ -275,14 +270,6 @@ static bool gPrevAnyDirty = true; // catches a future taller/wider surface before it silently truncates. typedef char amigaBltSizeFieldsFit[(JL_SURFACE_HEIGHT <= 1024 && AMIGA_BLT_WORDS_PER_ROW <= 64) ? 1 : -1]; -// Minterms for the masked solid-fill blit (amigaBlitterMaskedFillOne): -// A is the constant $FFFF cut to the rect by the first/last-word masks, -// C and D both address the destination. Set-bits writes D = A OR C -// (force the masked bits on, preserve the rest); clear-bits writes -// D = (NOT A) AND C (force the masked bits off, preserve the rest). -#define AMIGA_BLT_MINTERM_SET 0xFAu // D = A | C -#define AMIGA_BLT_MINTERM_CLEAR 0x0Au // D = ~A & C - // Blitter-vs-CPU crossover for jlpSurfaceCopyRect, in per-plane words // (height * widthWords). Below this the OwnBlitter + per-plane // WaitBlit + register-setup fixed cost (~1,300 CPU cycles) beats a @@ -424,9 +411,9 @@ static uint8_t bandListUpdate(const jlSurfaceT *src) { if (gTrampoline == NULL) { copperInstall(src); if (gCopperReady) { - gStageScbDirty = false; - gStagePaletteDirty = false; - gStagePaletteDirtyMask = 0; + jlgStageScbDirty = false; + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = 0; } return 0u; } @@ -437,7 +424,7 @@ static uint8_t bandListUpdate(const jlSurfaceT *src) { return 0u; } flip = FLIP_PLANES; - } else if (!gStageScbDirty && !gStagePaletteDirty) { + } else if (!jlgStageScbDirty && !jlgStagePaletteDirty) { return 0u; } flipWait(FLIP_LIST); @@ -445,10 +432,10 @@ static uint8_t bandListUpdate(const jlSurfaceT *src) { if (!bandListWrite(src, back)) { return flip; } - gListFront = back; - gStageScbDirty = false; - gStagePaletteDirty = false; - gStagePaletteDirtyMask = 0; + gListFront = back; + jlgStageScbDirty = false; + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = 0; return (uint8_t)(flip | FLIP_LIST); } @@ -593,12 +580,12 @@ static void copperInstall(const jlSurfaceT *src) { CMOVE(ucl, custom.copjmp2, 0); CEND(ucl); Forbid(); - if (gScreen->ViewPort.UCopIns != NULL) { - FreeVPortCopLists(&gScreen->ViewPort); + if (jlgScreen->ViewPort.UCopIns != NULL) { + FreeVPortCopLists(&jlgScreen->ViewPort); } - gScreen->ViewPort.UCopIns = ucl; + jlgScreen->ViewPort.UCopIns = ucl; Permit(); - MakeScreen(gScreen); + MakeScreen(jlgScreen); view = ViewAddress(); Forbid(); MrgCop(view); @@ -610,7 +597,7 @@ static void copperInstall(const jlSurfaceT *src) { patchTopColors(gListBand0[0]); } else { gCopStart[(gCopJump + COPPER_JUMP_STROBE) * COPPER_INSTR_WORDS] = COPPER_NOOP; - gCopperReady = false; + gCopperReady = false; } } LoadView(view); @@ -841,8 +828,8 @@ static void trampolinePoint(const uint16_t *list) { // reaching the display -- the failure class the golden stage hashes // cannot see. Reads gFrontIdx's planes while the VBL has not yet made // the flip a present asked for, and before the first copper install. Always compiled (the probe build links the stock library; -// gPresentCopiedBytes precedent). -void amigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum) { +// jlgPresentCopiedBytes precedent). +void jlAmigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum) { AmigaPlanarT *pd; const uint8_t *fp; const uint8_t *sp; @@ -897,7 +884,7 @@ bool jlpInit(const jlConfigT *config) { // defaults. gBitMap = (struct BitMap *)AllocMem((ULONG)sizeof(struct BitMap), (ULONG)(MEMF_PUBLIC | MEMF_CLEAR)); if (gBitMap == NULL) { - coreSetError("out of memory for the screen's bitmap"); + jlCoreSetError("out of memory for the screen's bitmap"); return false; } InitBitMap(gBitMap, (LONG)AMIGA_BITPLANES, (LONG)JL_SURFACE_WIDTH, (LONG)JL_SURFACE_HEIGHT); @@ -910,7 +897,7 @@ bool jlpInit(const jlConfigT *config) { gPlanesB[i] = (UBYTE *)AllocMem((ULONG)AMIGA_PLANE_SIZE, (ULONG)(MEMF_CHIP | MEMF_CLEAR)); if (gPlanes[i] == NULL || gPlanesB[i] == NULL) { screenPlanesFree(); - coreSetError("not enough chip RAM for the two screen pages"); + jlCoreSetError("not enough chip RAM for the two screen pages"); return false; } // Set A is the initial front buffer; BPL DMA fetches from it @@ -931,11 +918,11 @@ bool jlpInit(const jlConfigT *config) { // real planar screen rather than an RTG substitute. SA_BitMap // makes Intuition use OUR pre-allocated planes; CloseScreen will // not free them -- our jlpShutdown does. - gScreen = OpenScreenTags(NULL, (ULONG)SA_Width, (ULONG)JL_SURFACE_WIDTH, (ULONG)SA_Height, (ULONG)JL_SURFACE_HEIGHT, (ULONG)SA_Depth, (ULONG)AMIGA_BITPLANES, (ULONG)SA_DisplayID, (ULONG)(PAL_MONITOR_ID | LORES_KEY), (ULONG)SA_DetailPen, (ULONG)0, (ULONG)SA_BlockPen, (ULONG)1, (ULONG)SA_Title, (ULONG)"JoeyLib", (ULONG)SA_Type, (ULONG)CUSTOMSCREEN, (ULONG)SA_Quiet, (ULONG)TRUE, (ULONG)SA_BitMap, (ULONG)gBitMap, TAG_DONE); + jlgScreen = OpenScreenTags(NULL, (ULONG)SA_Width, (ULONG)JL_SURFACE_WIDTH, (ULONG)SA_Height, (ULONG)JL_SURFACE_HEIGHT, (ULONG)SA_Depth, (ULONG)AMIGA_BITPLANES, (ULONG)SA_DisplayID, (ULONG)(PAL_MONITOR_ID | LORES_KEY), (ULONG)SA_DetailPen, (ULONG)0, (ULONG)SA_BlockPen, (ULONG)1, (ULONG)SA_Title, (ULONG)"JoeyLib", (ULONG)SA_Type, (ULONG)CUSTOMSCREEN, (ULONG)SA_Quiet, (ULONG)TRUE, (ULONG)SA_BitMap, (ULONG)gBitMap, TAG_DONE); - if (gScreen == NULL) { + if (jlgScreen == NULL) { screenPlanesFree(); - coreSetError("Intuition could not open the 320x200 PAL screen"); + jlCoreSetError("Intuition could not open the 320x200 PAL screen"); return false; } // Force COLOR00 to black so the overscan/border region around the @@ -943,7 +930,7 @@ bool jlpInit(const jlConfigT *config) { // on the first jlStagePresent. Apps that paint a non-black bg need // do nothing -- the first present's band list loads their palette[0] // into COLOR00, above the screen too (patchTopColors). - SetRGB4(&gScreen->ViewPort, 0, 0, 0, 0); + SetRGB4(&jlgScreen->ViewPort, 0, 0, 0, 0); installVblServer(); frameHzMeasure(); // The millisecond clock's E-clock (jlpMillisElapsed); without it the @@ -975,7 +962,7 @@ bool jlpInit(const jlConfigT *config) { * the copy must move the counter with it. */ static void amigaPresentFlushRun(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITPLANES], int16_t runFirst, int16_t runLast, uint16_t byteStart, uint16_t byteLen, bool *blitOwned) { amigaPresentRectInner(pd, destPlanes, runFirst, runLast, byteStart, byteLen, blitOwned); - gPresentCopiedBytes += (uint32_t)(runLast - runFirst + 1) * byteLen * AMIGA_BITPLANES; + jlgPresentCopiedBytes += (uint32_t)(runLast - runFirst + 1) * byteLen * AMIGA_BITPLANES; } @@ -1093,13 +1080,18 @@ void jlpPresent(const jlSurfaceT *src) { uint8_t backIdx; uint8_t flip; - if (src == NULL || gScreen == NULL) { + // Every way out leaves the stage's bands clean (JL_PRESENT_CLEARS_BANDS): + // the union scan clears them as it reads them, the idle early-out finds + // them clean, and the returns before them clear them here. + if (src == NULL || jlgScreen == NULL) { + jlStageDirtyClearAll(); return; } flip = bandListUpdate(src); pd = (AmigaPlanarT *)src->portData; if (pd == NULL) { + jlStageDirtyClearAll(); flipRequest(flip); return; } @@ -1113,7 +1105,7 @@ void jlpPresent(const jlSurfaceT *src) { // clean row's minWord is the 0xFF CLEAN sentinel and any touched // row's is <= STAGE_DIRTY_FULL_MAX (79), so 4 clean rows read // exactly 0xFFFFFFFF -- 50 long tests replace 200 byte tests on - // the common all-clean idle frame (gStageMinWord is aligned(2), + // the common all-clean idle frame (jlgStageMinWord is aligned(2), // all the 68000 needs for move.l). A failing group byte-walks its // 4 rows with the same per-row test as before, so the first-dirty // row and the scanned-rows accounting are exactly the @@ -1123,7 +1115,7 @@ void jlpPresent(const jlSurfaceT *src) { // guards that), preserving the 2-buffer staleness convergence. if (!gPrevAnyDirty) { typedef uint32_t __attribute__((may_alias)) BandGroupT; - const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord; + const BandGroupT *minGroups = (const BandGroupT *)(const void *)jlgStageMinWord; bool anyDirty = false; uint8_t g; @@ -1143,7 +1135,7 @@ void jlpPresent(const jlSurfaceT *src) { } } } - gPresentScannedRows += (uint32_t)(anyDirty ? (y + 1) : JL_SURFACE_HEIGHT); + jlgPresentScannedRows += (uint32_t)(anyDirty ? (y + 1) : JL_SURFACE_HEIGHT); if (!anyDirty) { flipRequest(flip); return; @@ -1184,11 +1176,11 @@ void jlpPresent(const jlSurfaceT *src) { // register. Returns the run count (low 16) plus anyUnionDirty (bit // 16) and anyCurDirty (bit 17) -- the two flags the flip and // idle-early-out logic below need. - scanRes = amigaPresentScan68k(gStageMinWord, gStageMaxWord, gPrevStageMinWord, gPrevStageMaxWord, gPresentRuns); + scanRes = jlAmigaPresentScan68k(jlgStageMinWord, jlgStageMaxWord, gPrevStageMinWord, gPrevStageMaxWord, gPresentRuns); numRuns = (uint16_t)scanRes; anyUnionDirty = (scanRes & 0x00010000UL) != 0u; anyCurDirty = (scanRes & 0x00020000UL) != 0u; - gPresentScannedRows += JL_SURFACE_HEIGHT; + jlgPresentScannedRows += JL_SURFACE_HEIGHT; // Flush the collected runs. Kept separate from the scan so the // scan lives in hand-rolled asm (presentScan.s) without touching the @@ -1230,8 +1222,8 @@ void jlpPresent(const jlSurfaceT *src) { // now folded into the scan loop above, eliminating the second // 200-row pass this block used to run. gPrevAnyDirty records whether // the CURRENT frame touched any row so the next present can take the - // idle-frame early-out. jlStagePresent clears the live cur bands - // after jlpPresent returns; the fused snapshot already captured them. + // idle-frame early-out. The scan has left the live cur bands clean + // (JL_PRESENT_CLEARS_BANDS); the fused snapshot captured them first. gPrevAnyDirty = anyCurDirty; } @@ -1419,16 +1411,16 @@ void jlpShutdown(void) { // Tear down the VBL server before closing the screen so the // interrupt chain is clean if anything else is watching. removeVblServer(); - if (gScreen != NULL) { + if (jlgScreen != NULL) { // CloseScreen should free attached UCopList, but be explicit // to catch any case where the screen close path skips it. Forbid(); - if (gScreen->ViewPort.UCopIns != NULL) { - FreeVPortCopLists(&gScreen->ViewPort); + if (jlgScreen->ViewPort.UCopIns != NULL) { + FreeVPortCopLists(&jlgScreen->ViewPort); } Permit(); - CloseScreen(gScreen); - gScreen = NULL; + CloseScreen(jlgScreen); + jlgScreen = NULL; // Intuition loads a display without the screen, and its jump to // the band lists, at the next VBL: past that, nothing runs them. WaitTOF(); @@ -1447,15 +1439,14 @@ void jlpShutdown(void) { // four separate plane buffers of 40 bytes per row, so a span of pixels is the // same bytes in the same places. src/m68k/planarFill.s does the area fills, // src/m68k/planarPlot.s the pixel walkers. -extern void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); -extern void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color); +extern void jlSurface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); // src/m68k/planarPlot.s, shared with the X68000: Bresenham walkers with 16-way // colour dispatch, so every plane write is one memory read-modify-write with // the set-or-clear choice already made. Both are bit-identical to draw.c's own // walkers. Straight lines never reach jlpDrawLine -- draw.c diverts those to // jlFillRect -- so the line body walks the general diagonal case only. -extern void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); -extern void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color); +extern void jlSurface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); +extern void jlSurface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color); // Amiga is pure planar (s->pixels is NULL). Each jlp<Op> below does the planar @@ -1476,7 +1467,7 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) { colorIndex = (uint8_t)(doubled & 0x0Fu); pd = (AmigaPlanarT *)s->portData; if (!pd->chip) { - surface68kFillRect(pd->planes, 0u, 0u, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, colorIndex); + jlSurface68kFillRect(pd->planes, 0u, 0u, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, colorIndex); return; } @@ -1532,7 +1523,7 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint return; } if (!pd->chip) { - surface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex); + jlSurface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex); return; } @@ -1594,198 +1585,6 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint } -// Phase 9 sprite save/restore plane-backup hooks. -// Sprite save at (x, y, w, h) records the destination region into the -// caller's backup buffer; restore writes it back. x and w are 2-pixel -// aligned by cross-platform code. -// -// Backup layout matches the CHUNKY-packed format the cross-platform -// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see -// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte, -// high nibble = even (left) pixel, low nibble = odd (right) pixel, -// (w/2) bytes per row, (w/2)*h bytes total. This is the same format -// the Atari ST port uses and never overruns the contract buffer -- -// unlike a plane-major layout, which would round x down to an 8-pixel -// boundary and width up and then write (4*roundedW/8)*h = up to -// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each -// pixel's 4bpp color is assembled from / written to the 4 plane bits -// at byte x>>3, bit 0x80 >> (x & 7). - -void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { - const AmigaPlanarT *pd; - uint16_t pairs; - uint16_t row; - uint16_t pair; - uint8_t *dst; - - pd = (const AmigaPlanarT *)s->portData; - if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) { - return; - } - - /* Byte-aligned fast path: raw plane-byte copy, no per-pixel c2p. - * When x and w are both 8-px aligned and the rect is fully on - * surface, each sprite tile column maps to whole plane bytes, so we - * save the raw bytes -- ~8x cheaper than the chunky walker below and - * an exact round-trip on restore (the sprite fills its bytes, so - * restoring whole bytes touches no neighbor pixels). The backup row - * holds plane0..plane3, each `wb` bytes, contiguous. The chunky - * walker below stays as the arbitrary-x fallback. */ - if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - uint16_t wb; - uint16_t wb2; - uint16_t wb3; - uint16_t startByte; - uint16_t rowOff; - uint8_t *bdst; - uint16_t col; - - wb = (uint16_t)(w >> 3); - wb2 = (uint16_t)(wb << 1); - wb3 = (uint16_t)(wb2 + wb); - startByte = (uint16_t)((uint16_t)x >> 3); - rowOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + startByte); - bdst = dstPlaneBytes; - for (row = 0; row < (uint16_t)h; row++) { - const uint8_t *s0 = pd->planes[0] + rowOff; - const uint8_t *s1 = pd->planes[1] + rowOff; - const uint8_t *s2 = pd->planes[2] + rowOff; - const uint8_t *s3 = pd->planes[3] + rowOff; - for (col = 0; col < wb; col++) { - bdst[col] = s0[col]; - bdst[wb + col] = s1[col]; - bdst[wb2 + col] = s2[col]; - bdst[wb3 + col] = s3[col]; - } - bdst += (uint16_t)(wb << 2); - rowOff += AMIGA_BYTES_PER_ROW; - } - return; - } - - pairs = (uint16_t)(w >> 1); - for (row = 0; row < (uint16_t)h; row++) { - const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - dst = dstPlaneBytes + (uint16_t)(row * pairs); - for (pair = 0; pair < pairs; pair++) { - uint16_t px = (uint16_t)(x + (int16_t)(pair << 1)); - uint16_t byteOff = (uint16_t)(px >> 3); - uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u)); - uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u)); - uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3); - uint8_t hi = 0u; - uint8_t lo = 0u; - if (p0[byteOff] & maskHi) { - hi = (uint8_t)(hi | 0x01u); - } - if (p1[byteOff] & maskHi) { - hi = (uint8_t)(hi | 0x02u); - } - if (p2[byteOff] & maskHi) { - hi = (uint8_t)(hi | 0x04u); - } - if (p3[byteOff] & maskHi) { - hi = (uint8_t)(hi | 0x08u); - } - if (p0[byteOffLo] & maskLo) { - lo = (uint8_t)(lo | 0x01u); - } - if (p1[byteOffLo] & maskLo) { - lo = (uint8_t)(lo | 0x02u); - } - if (p2[byteOffLo] & maskLo) { - lo = (uint8_t)(lo | 0x04u); - } - if (p3[byteOffLo] & maskLo) { - lo = (uint8_t)(lo | 0x08u); - } - dst[pair] = (uint8_t)((hi << 4) | lo); - } - } -} - - -void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { - AmigaPlanarT *pd; - uint16_t pairs; - uint16_t row; - uint16_t pair; - const uint8_t *src; - - pd = (AmigaPlanarT *)s->portData; - if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) { - return; - } - - /* Byte-aligned fast path mirroring jlpSpriteSavePlanes: writes the - * raw plane bytes straight back (whole-byte round-trip), skipping - * the per-pixel chunky->planar masking of the walker below. */ - if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - uint16_t wb; - uint16_t wb2; - uint16_t wb3; - uint16_t startByte; - uint16_t rowOff; - const uint8_t *bsrc; - uint16_t col; - - wb = (uint16_t)(w >> 3); - wb2 = (uint16_t)(wb << 1); - wb3 = (uint16_t)(wb2 + wb); - startByte = (uint16_t)((uint16_t)x >> 3); - rowOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + startByte); - bsrc = srcPlaneBytes; - for (row = 0; row < (uint16_t)h; row++) { - uint8_t *d0 = pd->planes[0] + rowOff; - uint8_t *d1 = pd->planes[1] + rowOff; - uint8_t *d2 = pd->planes[2] + rowOff; - uint8_t *d3 = pd->planes[3] + rowOff; - for (col = 0; col < wb; col++) { - d0[col] = bsrc[col]; - d1[col] = bsrc[wb + col]; - d2[col] = bsrc[wb2 + col]; - d3[col] = bsrc[wb3 + col]; - } - bsrc += (uint16_t)(wb << 2); - rowOff += AMIGA_BYTES_PER_ROW; - } - return; - } - - pairs = (uint16_t)(w >> 1); - for (row = 0; row < (uint16_t)h; row++) { - uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; - src = srcPlaneBytes + (uint16_t)(row * pairs); - for (pair = 0; pair < pairs; pair++) { - uint8_t b = src[pair]; - uint8_t hi = (uint8_t)(b >> 4); - uint8_t lo = (uint8_t)(b & 0x0Fu); - uint16_t px = (uint16_t)(x + (int16_t)(pair << 1)); - uint16_t byteOff = (uint16_t)(px >> 3); - uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u)); - uint8_t notHi = (uint8_t)(~maskHi); - uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3); - uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u)); - uint8_t notLo = (uint8_t)(~maskLo); - p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u)); - p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u)); - p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u)); - p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u)); - p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u)); - p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u)); - p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u)); - p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u)); - } - } -} - - // The sprite interpreter (uncompiled sprites, and compiled ones drawn partly // off the surface): the planar ports' shared walker over the sprite's // planar cache, src/m68k/planarSprite.c. @@ -1796,7 +1595,7 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t if (pd == NULL) { return; } - planarSpriteDraw(pd->planes, sp, x, y); + jlPlanarSpriteDraw(pd->planes, sp, x, y); } @@ -1884,76 +1683,13 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint } -// Phase 4 planar dual-write helper: set one pixel's bit in each of -// the four shadow planes. Caller (the per-primitive walker below) has -// already validated (x, y) is on-surface. byteOff is computed once -// and reused across all four planes since stride is the same in each. - - -static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color) { - uint16_t byteOff; - uint8_t bitMask; - uint8_t notMask; - uint8_t *p0; - uint8_t *p1; - uint8_t *p2; - uint8_t *p3; - - byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3)); - bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); - notMask = (uint8_t)~bitMask; - - /* Unroll the 4-plane loop. Loop counter + array indexing inside - * the hot per-pixel path is the gating cost on circle outlines: - * UBER jlDrawCircle r=80 calls this ~640 times per call. */ - p0 = &pd->planes[0][byteOff]; - p1 = &pd->planes[1][byteOff]; - p2 = &pd->planes[2][byteOff]; - p3 = &pd->planes[3][byteOff]; - - if (color & 0x01u) { - *p0 = (uint8_t)(*p0 | bitMask); - } else { - *p0 = (uint8_t)(*p0 & notMask); - } - if (color & 0x02u) { - *p1 = (uint8_t)(*p1 | bitMask); - } else { - *p1 = (uint8_t)(*p1 & notMask); - } - if (color & 0x04u) { - *p2 = (uint8_t)(*p2 | bitMask); - } else { - *p2 = (uint8_t)(*p2 & notMask); - } - if (color & 0x08u) { - *p3 = (uint8_t)(*p3 | bitMask); - } else { - *p3 = (uint8_t)(*p3 & notMask); - } -} - - -void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { - AmigaPlanarT *pd; - uint8_t nibLo; - if (s->portData == NULL) { - jlpGenericDrawPixel(s, x, y, colorIndex); - return; - } - nibLo = (uint8_t)(colorIndex & 0x0Fu); - pd = (AmigaPlanarT *)s->portData; - amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo); -} - - void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { AmigaPlanarT *pd; if (s->portData == NULL) { return; } pd = (AmigaPlanarT *)s->portData; - surface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu)); + jlSurface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu)); } @@ -1963,7 +1699,7 @@ void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co return; } pd = (AmigaPlanarT *)s->portData; - surface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu)); + jlSurface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu)); } @@ -1988,7 +1724,7 @@ bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co if (s->portData == NULL) { return false; } - surface68kFillCircle(((AmigaPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex); + jlSurface68kFillCircle(((AmigaPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex); return true; } @@ -2144,132 +1880,12 @@ bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i } -/* ===== Phase 9 reader hooks: pure-planar Amiga implementations ===== - * - * Cross-platform code that USED to read s->pixels (chunky shadow) now - * goes through these. On Amiga the chunky shadow doesn't exist; - * pixels are derived on demand by walking the plane bits. - * - * Per-pixel assembly: for pixel (x, y), read bit (7 - x%8) from plane - * byte at row*40 + x/8 in each of the 4 planes. Color index = sum - * of (bit_p << p). */ - -uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { - AmigaPlanarT *pd; - uint16_t byteOff; - uint8_t bitMask; - uint8_t color; - uint16_t i; - - pd = (AmigaPlanarT *)s->portData; - if (pd == NULL) { - return 0u; - } - byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3)); - bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); - color = 0u; - for (i = 0; i < AMIGA_BITPLANES; i++) { - if (pd->planes[i][byteOff] & bitMask) { - color = (uint8_t)(color | (1u << i)); - } - } - return color; -} - - -/* Reverse-c2p: per row, derive 160 chunky bytes from 40 plane bytes - * (per plane, 4 planes). Used by jlpSurfaceHash to fold the planar - * surface into the same byte-stream the chunky ports hash, so cross- - * port hash comparisons stay valid. - * Walks 8 pixels per planar-byte column; per pixel assembles nibble - * from 4 plane bits. Output: 4 chunky bytes per planar-byte column - * (since 8 pixels = 4 chunky bytes at 2px/byte). */ -static void amigaPlanesToChunkyRow(const AmigaPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) { - uint16_t col; - uint16_t byteOff; - uint8_t b0; - uint8_t b1; - uint8_t b2; - uint8_t b3; - uint8_t pix; - uint8_t bitMask; - uint16_t p; - - for (col = 0; col < AMIGA_BYTES_PER_ROW; col++) { - byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + col); - b0 = pd->planes[0][byteOff]; - b1 = pd->planes[1][byteOff]; - b2 = pd->planes[2][byteOff]; - b3 = pd->planes[3][byteOff]; - /* For each of 8 pixels in this planar byte (bit 7 = leftmost), - * assemble nibble from the 4 plane bits and pack into chunky - * bytes (high nibble = even pixel, low nibble = odd pixel). */ - for (p = 0; p < 8u; p++) { - bitMask = (uint8_t)(0x80u >> p); - pix = 0u; - if (b0 & bitMask) { - pix = (uint8_t)(pix | 1u); - } - if (b1 & bitMask) { - pix = (uint8_t)(pix | 2u); - } - if (b2 & bitMask) { - pix = (uint8_t)(pix | 4u); - } - if (b3 & bitMask) { - pix = (uint8_t)(pix | 8u); - } - if ((p & 1u) == 0u) { - dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(pix << 4); - } else { - dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(dstChunkyRow[col * 4u + (p >> 1)] | pix); - } - } - } -} - - +// The shared planar hash (src/m68k/planarHash.c), over separate planes. uint32_t jlpSurfaceHash(const jlSurfaceT *s) { - AmigaPlanarT *pd; - uint16_t lo = 0xACE1u; - uint16_t hi = 0x1357u; - uint16_t n; - uint16_t v; - int16_t row; - uint8_t b; - uint8_t chunkyRow[JL_SURFACE_BYTES_PER_ROW]; - const uint16_t *w; + const AmigaPlanarT *pd; - pd = (AmigaPlanarT *)s->portData; - if (pd == NULL) { - return 0u; - } - /* Pixel hash: derive chunky one row at a time, fold byte-by-byte - * via the shared SURFACE_HASH_MIX_BYTE so cross-port hash - * matches. */ - for (row = 0; row < JL_SURFACE_HEIGHT; row++) { - amigaPlanesToChunkyRow(pd, row, chunkyRow); - for (n = 0; n < JL_SURFACE_BYTES_PER_ROW; n++) { - b = chunkyRow[n]; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - } - // SCB: byte sequence, endian-independent. - for (n = 0; n < (uint16_t)JL_SURFACE_HEIGHT; n++) { - b = s->scb[n]; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - /* Palette: read uint16 values, fold high-then-low for endian- - * independence. */ - w = &s->palette[0][0]; - for (n = 0; n < (uint16_t)JL_SURFACE_PALETTE_ENTRIES; n++) { - v = *w++; - b = (uint8_t)((v >> 8) & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - b = (uint8_t)(v & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - return ((uint32_t)hi << 16) | (uint32_t)lo; + pd = (const AmigaPlanarT *)s->portData; + return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, AMIGA_BYTES_PER_ROW, PLANAR_HASH_SEPARATE_PAIR_STEP); } @@ -2313,7 +1929,7 @@ uint8_t *jlpStageAllocPixels(void) { /* Phase 9: Amiga has no chunky shadow. The stage pixels pointer * stays NULL; cross-platform code reads pixels via jlpSamplePixel * (or the other chunky reader ops) which read from planes. NULL is - * a valid return -- cross-platform stageAlloc treats NULL as + * a valid return -- cross-platform jlStageAlloc treats NULL as * "port has no chunky storage" and skips the chunky memset. */ return NULL; } diff --git a/src/amiga/input.c b/src/amiga/input.c index e669aa6..e373446 100644 --- a/src/amiga/input.c +++ b/src/amiga/input.c @@ -76,7 +76,7 @@ struct Library *LowLevelBase = NULL; // ----- External from hal.c ----- -extern struct Screen *gScreen; +extern struct Screen *jlgScreen; // ----- Empty pointer image ----- // @@ -192,16 +192,19 @@ static bool gJoyLocked[JL_JOYSTICK_COUNT]; // lowlevel.library reports a one-button stick and a two-button one alike // (a second button is pin 9, which reads as released until it is pressed), // and most Amiga owners use one-button Atari sticks. So a joystick counts as -// one-button (gJoyOneButton: a hold of its button is button 1) until its +// one-button (jlgJoyOneButton: a hold of its button is button 1) until its // second button has been seen pressed, and again once its port has no // controller. A CD32 game controller always has both. static bool gJoySecondButton[JL_JOYSTICK_COUNT]; // Keys and mouse buttons as Intuition's messages leave them, and pressed // since the last poll: a tap between two polls still reads down for one -// (inputLatch). +// (jlInputLatch). The keys are latched only when a message changed them or +// the last latch showed a tap that must now go up. static uint8_t gKeyHeld [JL_KEY_COUNT]; static uint8_t gKeyTapped [JL_KEY_COUNT]; +static bool gKeysChanged; +static bool gKeyTapShown; static uint8_t gMouseHeld [JL_MOUSE_BUTTON_COUNT]; static uint8_t gMouseTapped [JL_MOUSE_BUTTON_COUNT]; @@ -266,23 +269,25 @@ static void drainMessages(void) { if (!isRelease) { gKeyTapped[key] = 1u; } + gKeysChanged = true; } // MapRawKey returns -1 on overflow (composition longer // than the buffer) -- nothing usable, skip. - // A Ctrl chord types nothing (inputCharTyped); Alt is a + // A Ctrl chord types nothing (jlInputCharTyped); Alt is a // layer of the keymap's characters. for (mapIdx = 0; mapIdx < mapCount; mapIdx++) { - inputCharTyped((uint8_t)mapBuf[mapIdx], mapChord); + jlInputCharTyped((uint8_t)mapBuf[mapIdx], mapChord); } break; case IDCMP_MOUSEMOVE: - inputMouseMove(msgMouseX, msgMouseY); + jlInputMouseMove(msgMouseX, msgMouseY); break; case IDCMP_INACTIVEWINDOW: // The keys and buttons it lets go elsewhere are never // reported here: let them all go now. memset(gKeyHeld, 0, sizeof(gKeyHeld)); memset(gMouseHeld, 0, sizeof(gMouseHeld)); + gKeysChanged = true; break; case IDCMP_MOUSEBUTTONS: switch (msgCode) { @@ -308,7 +313,7 @@ static void drainMessages(void) { break; } // MOUSEBUTTONS messages also carry the mouse position. - inputMouseMove(msgMouseX, msgMouseY); + jlInputMouseMove(msgMouseX, msgMouseY); break; default: break; @@ -357,40 +362,40 @@ static void pollJoysticks(void) { gJoyLocked[stick] = SetJoyPortAttrs(gAmigaPortForStick[stick], SJA_Type, SJA_TYPE_JOYSTK, TAG_DONE) != FALSE; } if (type != JP_TYPE_JOYSTK && type != JP_TYPE_GAMECTLR) { - gJoyConnected[stick] = false; - gJoyAxisX[stick] = 0; - gJoyAxisY[stick] = 0; - gJoyOneButton[stick] = false; - gJoySecondButton[stick] = false; - gJoyButtonState[stick][JL_JOY_BUTTON_0] = false; - gJoyButtonState[stick][JL_JOY_BUTTON_1] = false; + jlgJoyConnected[stick] = false; + jlgJoyAxisX[stick] = 0; + jlgJoyAxisY[stick] = 0; + jlgJoyOneButton[stick] = false; + gJoySecondButton[stick] = false; + jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = false; + jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = false; continue; } - gJoyConnected[stick] = true; + jlgJoyConnected[stick] = true; - gJoyAxisX[stick] = 0; + jlgJoyAxisX[stick] = 0; if (state & JPF_JOY_LEFT) { - gJoyAxisX[stick] = JL_JOYSTICK_AXIS_MIN; + jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MIN; } if (state & JPF_JOY_RIGHT) { - gJoyAxisX[stick] = JL_JOYSTICK_AXIS_MAX; + jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MAX; } - gJoyAxisY[stick] = 0; + jlgJoyAxisY[stick] = 0; if (state & JPF_JOY_UP) { - gJoyAxisY[stick] = JL_JOYSTICK_AXIS_MIN; + jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MIN; } if (state & JPF_JOY_DOWN) { - gJoyAxisY[stick] = JL_JOYSTICK_AXIS_MAX; + jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MAX; } // Standard 1-button stick reports JPF_BUTTON_RED only. CD32 // game controllers also report BLUE for the second face button. - gJoyButtonState[stick][JL_JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0; - gJoyButtonState[stick][JL_JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0; + jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0; + jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0; if (type == JP_TYPE_GAMECTLR || (state & JPF_BUTTON_BLUE) != 0) { gJoySecondButton[stick] = true; } - gJoyOneButton[stick] = !gJoySecondButton[stick]; + jlgJoyOneButton[stick] = !gJoySecondButton[stick]; } } @@ -405,21 +410,22 @@ void jlpJoystickReset(jlJoystickE js) { void jlpInputInit(void) { - memset(gKeyState, 0, sizeof(gKeyState)); - memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); + memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev)); memset(gKeyHeld, 0, sizeof(gKeyHeld)); memset(gKeyTapped, 0, sizeof(gKeyTapped)); + gKeysChanged = true; memset(gMouseHeld, 0, sizeof(gMouseHeld)); memset(gMouseTapped, 0, sizeof(gMouseTapped)); - if (gScreen == NULL) { + if (jlgScreen == NULL) { return; } - gWindow = OpenWindowTags(NULL, (ULONG)WA_CustomScreen, (ULONG)gScreen, (ULONG)WA_Left, (ULONG)0, (ULONG)WA_Top, (ULONG)0, (ULONG)WA_Width, (ULONG)gScreen->Width, (ULONG)WA_Height, (ULONG)gScreen->Height, (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP | WFLG_NOCAREREFRESH | WFLG_REPORTMOUSE), (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY | IDCMP_MOUSEMOVE | IDCMP_MOUSEBUTTONS | IDCMP_INACTIVEWINDOW), TAG_DONE); + gWindow = OpenWindowTags(NULL, (ULONG)WA_CustomScreen, (ULONG)jlgScreen, (ULONG)WA_Left, (ULONG)0, (ULONG)WA_Top, (ULONG)0, (ULONG)WA_Width, (ULONG)jlgScreen->Width, (ULONG)WA_Height, (ULONG)jlgScreen->Height, (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP | WFLG_NOCAREREFRESH | WFLG_REPORTMOUSE), (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY | IDCMP_MOUSEMOVE | IDCMP_MOUSEBUTTONS | IDCMP_INACTIVEWINDOW), TAG_DONE); if (gWindow != NULL) { - gMouseAttached = true; // Intuition delivers IDCMP mouse events on this window + jlgMouseAttached = true; // Intuition delivers IDCMP mouse events on this window gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES, (ULONG)(MEMF_CHIP | MEMF_CLEAR)); if (gEmptyPointer != NULL) { @@ -442,8 +448,11 @@ void jlpInputInit(void) { void jlpInputPoll(void) { drainMessages(); - inputLatch(gKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT); - inputLatch(gMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); + if (gKeysChanged || gKeyTapShown) { + gKeysChanged = false; + gKeyTapShown = jlInputLatch(jlgKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT); + } + jlInputLatch(jlgMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); pollJoysticks(); } diff --git a/src/amiga/libinit.c b/src/amiga/libinit.c index 905e159..18249ac 100644 --- a/src/amiga/libinit.c +++ b/src/amiga/libinit.c @@ -9,11 +9,12 @@ // "<name> failed to load" to the CLI and exits(20) before main() runs. // // That behavior kills the examples on AROS, which does not provide -// locale.library as a disk library. libnix unconditionally pulls the -// locale.library entry into the auto-open list because C runtime -// utilities (e.g. tzset via time()) take LocaleBase as an argument to -// ConvToLower -- even when the runtime does not actually need a real -// Locale to run. +// locale.library as a disk library, nor does a Kickstart 3.1 machine whose +// boot disk lacks LIBS:locale.library. A program that uses the time +// (time(), localtime()) has locale.library on that list: libnix's tzset, +// which its start-up runs, reads the time zone from the locale. It checks +// LocaleBase first, so with the library missing it leaves the time zone at +// GMT. // // Our replacement walks the same list but records NULL for any // library that cannot be opened, then continues. Code that actually @@ -72,14 +73,3 @@ void __exitlibraries(void) { ADD2INIT(__initlibraries, -79); ADD2EXIT(__exitlibraries, -79); - - -// libnix's tzset dereferences LocaleBase to call locale.library's -// ConvToLower. On AROS (where locale.library is unavailable), our -// tolerant __initlibraries leaves LocaleBase NULL, which would crash -// the first time time() is called (via tzset). Our pattern example -// uses time() for the 5-second display; override tzset with a no-op -// so time() can run. Difference-based timing is unaffected because -// any DST correction applied consistently cancels out. -void tzset(void) { -} diff --git a/src/amiga/presentScan.s b/src/amiga/presentScan.s index 187de66..13a68ec 100644 --- a/src/amiga/presentScan.s +++ b/src/amiga/presentScan.s @@ -17,6 +17,9 @@ | * dirty row: snapshot gPrev[y] <- cur[y], accumulate anyCurDirty, | fold union(cur,prev) into a plane-byte span, clamp, then coalesce | consecutive same-span rows into runs. +| and leaves the stage's bands clean as it reads them, so jlStagePresent +| does not clear them again (JL_PRESENT_CLEARS_BANDS): a clean group is +| clean already, and a dirty one's four rows are reset, as longs, once read. | Runs are emitted into the caller's gPresentRuns[] as 8-byte records | {firstRow.w, lastRow.w, byteStart.w, byteLen.w}; the caller flushes | them (blitter or CPU). lastRow is NOT tracked in a register: a run is @@ -30,7 +33,7 @@ | ABI: cdecl. d2-d7/a2-a6 callee-save. Byte loads use moveq #0 first | (partial-register-write hygiene -- the fold and emit do word math). | -| uint32_t amigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax, +| uint32_t jlAmigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax, | uint8_t *prvMin, uint8_t *prvMax, | AmigaPresentRunT *runList); | @@ -39,7 +42,7 @@ | bit 17 = anyCurDirty (current frame touched >=1 row). | | Register allocation across the row loop: -| a0 = gStageMinWord walker a1 = gStageMaxWord walker +| a0 = jlgStageMinWord walker a1 = jlgStageMaxWord walker | a2 = gPrevStageMinWord (r/w) a3 = gPrevStageMaxWord (r/w) | a4 = run-list write cursor a5 = run-list base (for the count) | d2 = y (row index 0..199) d4 = runFirst (-1.w == no open run) @@ -49,9 +52,9 @@ .text - .globl _amigaPresentScan68k + .globl _jlAmigaPresentScan68k -_amigaPresentScan68k: +_jlAmigaPresentScan68k: movem.l %d2-%d7/%a2-%a6,-(%sp) | 44 bytes saved move.l 48(%sp),%a0 | curMin base move.l 52(%sp),%a1 | curMax base @@ -157,10 +160,8 @@ _amigaPresentScan68k: cmp.w #200,%d2 bge .scanEnd move.l (%a0),%d0 | 4 cur min bytes - addq.l #1,%d0 | 0xFFFFFFFF+1 == 0 <=> all clean - bne .groupDirty - move.l (%a2),%d0 | 4 prev min bytes - addq.l #1,%d0 + and.l (%a2),%d0 | and the 4 prev ones + addq.l #1,%d0 | 0xFFFFFFFF+1 == 0 <=> all clean in both bne .groupDirty | all 4 rows clean-clean: close any open run, skip 4 tst.w %d4 @@ -182,11 +183,17 @@ _amigaPresentScan68k: .groupDirty: | Some row in the group is touched (cur or prev): process - | all 4 rows with the exact old one-row logic, fall-through. + | all 4 rows with the exact old one-row logic, fall-through, + | then leave the stage's four rows clean for the next frame + | (JL_PRESENT_CLEARS_BANDS), as longs. PS_ONE_ROW PS_ONE_ROW PS_ONE_ROW PS_ONE_ROW + moveq #-1,%d0 + move.l %d0,-4(%a0) + moveq #0,%d0 + move.l %d0,-4(%a1) bra .groupLoop .scanEnd: diff --git a/src/amiga/serial.c b/src/amiga/serial.c index 6129aa5..f8928c3 100644 --- a/src/amiga/serial.c +++ b/src/amiga/serial.c @@ -231,10 +231,10 @@ void jlpSerialFlush(void) { // Why the last jlpSerialOpen failed, for host-side diagnosis: a silent false was unattributable // through three swallowing layers (this file -> core gate -> the client's "non-fatal" open). // stage: 1=CreateMsgPort 2=CreateIORequest 3=OpenDevice 4=SDCMD_SETPARAMS 5=write IORequest. -uint8_t gAmigaSerialFailStage = 0u; -BYTE gAmigaSerialFailErr = 0; -BYTE gAmigaSerialFailIoErr = 0; -uint8_t gAmigaSerialFailDevice = 0u; +uint8_t jlgAmigaSerialFailStage = 0u; +BYTE jlgAmigaSerialFailErr = 0; +BYTE jlgAmigaSerialFailIoErr = 0; +uint8_t jlgAmigaSerialFailDevice = 0u; bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { @@ -245,17 +245,17 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { if (gOpen) { return false; } - gAmigaSerialFailStage = 0u; - gAmigaSerialFailErr = 0; + jlgAmigaSerialFailStage = 0u; + jlgAmigaSerialFailErr = 0; gPort = CreateMsgPort(); if (gPort == NULL) { - gAmigaSerialFailStage = 1u; + jlgAmigaSerialFailStage = 1u; return false; } gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer)); if (gReadReq == NULL) { - gAmigaSerialFailStage = 2u; + jlgAmigaSerialFailStage = 2u; serialTeardown(false); return false; } @@ -294,12 +294,12 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul); } if (err != 0) { - gAmigaSerialFailStage = 3u; - gAmigaSerialFailErr = err; + jlgAmigaSerialFailStage = 3u; + jlgAmigaSerialFailErr = err; // Distinguish a REAL open failure from a lying return value: io_Error is the device's own // verdict and io_Device is non-NULL if the device actually attached. - gAmigaSerialFailIoErr = gReadReq->IOSer.io_Error; - gAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u; + jlgAmigaSerialFailIoErr = gReadReq->IOSer.io_Error; + jlgAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u; serialTeardown(false); return false; } @@ -316,8 +316,8 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { // SERF_SHARED fallback) -- do not strip it here. gReadReq->IOSer.io_Command = SDCMD_SETPARAMS; if (DoIO((struct IORequest *)gReadReq) != 0) { - gAmigaSerialFailStage = 4u; - gAmigaSerialFailErr = gReadReq->IOSer.io_Error; + jlgAmigaSerialFailStage = 4u; + jlgAmigaSerialFailErr = gReadReq->IOSer.io_Error; serialTeardown(false); return false; } @@ -325,7 +325,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { // Second request sharing the same open device (copy carries io_Device/Unit). gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer)); if (gWriteReq == NULL) { - gAmigaSerialFailStage = 5u; + jlgAmigaSerialFailStage = 5u; serialTeardown(false); return false; } diff --git a/src/atarist/audio.c b/src/atarist/audio.c index 65fd48f..43664c6 100644 --- a/src/atarist/audio.c +++ b/src/atarist/audio.c @@ -15,7 +15,7 @@ // * jlpAudioPlayMod starts the module player at the same 12288 Hz mono // unsigned-8-bit output that the ISR consumes one byte at a time. // * Each Timer A tick the ISR pops one byte off the buffer, writes its -// YM level (gYmLevel: the YM's sixteen levels are logarithmic, so each +// YM level (jlgYmLevel: the YM's sixteen levels are logarithmic, so each // sample plays as the level nearest it on a straight line) to YM // register 8 (channel A volume), and advances the play pointer; the // frame tick sees which half it has left. The refill itself happens in @@ -101,8 +101,8 @@ // The module player and the SFX overlay mix unsigned samples centred on $80. #define MIX_SILENCE_UNSIGNED MOD_PLAYER_SILENCE_U8 // The 8-bit mix's unit (joey/audio.h's mix): a module channel at full volume -// and a full-scale sound effect each swing half a byte (modPlayerSetU8Shift, -// audioSfxSetShift). On the STe a side's two channels fill it; the plain +// and a full-scale sound effect each swing half a byte (jlModPlayerSetU8Shift, +// jlAudioSfxSetShift). On the STe a side's two channels fill it; the plain // ST's mono sums four, which clip when three or four peak together. The // voices come down to the same level (kYmVolume). #define MIX_UNIT_SHIFT 1u @@ -156,7 +156,7 @@ #define MIX_SILENCE MIX_SILENCE_UNSIGNED // The YM's channel-A volume is the DAC, and its sixteen levels are // logarithmic, about 3 dB apart: each sample is played as the level nearest -// its value on a straight line from level 0 to level 15 (gYmLevel), so the +// its value on a straight line from level 0 to level 15 (jlgYmLevel), so the // waveform keeps its shape and silence sits near the middle, level 13. The // levels' amplitudes, as the YM2149 puts them out (ST-Sound's measured // table). @@ -179,7 +179,7 @@ static const uint16_t kYmAmplitude[YM_LEVELS] = { // jlAudioTickRegister rides TOS's 200 Hz Timer C (both builds: the plain // ST's Timer A is the PWM pump's, B the per-line palettes', D the serial -// port's), chained in front of TOS's own handler by stTickIsr +// port's), chained in front of TOS's own handler by jlStTickIsr // (audio_isr.s). A carry paces the program's rate against the 200 ticks a // second, so the average is exact at any rate; at most 200 a second come // one to an interrupt, faster rates several to one. The callback runs in @@ -194,7 +194,7 @@ static const uint16_t kYmAmplitude[YM_LEVELS] = { // these must match) or the STe's DMA chip's. Even-aligned: the DMA chip can // only start a frame on an even address (2 is also mint a.out's alignment // cap). -volatile uint8_t gMixBuf[MIX_BUFFER] __attribute__((aligned(2))); +volatile uint8_t jlgMixBuf[MIX_BUFFER] __attribute__((aligned(2))); // The halves to refill: a frame tick that finds the play position has left // a half since the last one raises its flag (pollRefill), and refills it. static uint8_t gNeedRefill[2] = { 0, 0 }; @@ -203,11 +203,11 @@ static uint8_t gPlayHalf = 0u; #ifndef JOEYLIB_ATARI_STE // The PWM ISR's play pointer and the buffer's end (audio_isr.s). -volatile uint8_t *gMixPos = NULL; -volatile uint8_t *gMixEnd = NULL; +volatile uint8_t *jlgMixPos = NULL; +volatile uint8_t *jlgMixEnd = NULL; // Each sample's YM level (kYmAmplitude), built at jlpAudioInit: the ISR -// plays gYmLevel[sample], from beside its own code (audio_isr.s). -extern uint8_t gYmLevel[SAMPLE_VALUES]; +// plays jlgYmLevel[sample], from beside its own code (audio_isr.s). +extern uint8_t jlgYmLevel[SAMPLE_VALUES]; // The drain (PWM_RAMP_SAMPLES): the ramp down is written, from // PWM_DRAIN_MARGIN past gDrainFrom, where the ISR was; and the silence // before it (PWM_IDLE_MS), since gIdleSinceMs. @@ -216,7 +216,7 @@ static uint16_t gDrainFrom = 0u; static bool gIdle = false; static uint32_t gIdleSinceMs = 0u; -extern void mfpTimerAIsr(void); +extern void jlMfpTimerAIsr(void); static void (*gOldTimerAVec)(void) = NULL; @@ -227,8 +227,8 @@ static volatile uint16_t gVoiceAFreqHz; static volatile uint8_t gVoiceAAtten = 15u; #endif -// Read by stTickIsr (audio_isr.s), so not static. -void (*gStTickOldVec)(void) = NULL; +// Read by jlStTickIsr (audio_isr.s), so not static. +void (*jlgStTickOldVec)(void) = NULL; static volatile jlAudioTickFnT gStTickFn = NULL; static volatile uint32_t gStTickHz = 0u; static volatile uint32_t gStTickCarry = 0u; @@ -236,8 +236,8 @@ static bool gStTickHooked = false; // The status register at the outermost jlAudioCriticalEnter. static uint16_t gCriticalSr = 0u; -extern void stTickIsr(void); -void stTickRun(void); +extern void jlStTickIsr(void); +void jlStTickRun(void); static bool gReady = false; @@ -323,7 +323,7 @@ static long uninstallDma(void); static void ymWrite(uint8_t reg, uint8_t val) { uint16_t saved; - saved = m68kInterruptsOff(); + saved = m68kInterruptsOff(); *ST_YM_SELECT = reg; *ST_YM_DATA = val; m68kInterruptsRestore(saved); @@ -343,8 +343,8 @@ static long installTimerA(void) { // = 128) before opening the IRQ gate. silenceMixBuffer(); rampWrite(0u, 0u, MIX_SILENCE); - gMixPos = (volatile uint8_t *)gMixBuf; - gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER); + jlgMixPos = (volatile uint8_t *)jlgMixBuf; + jlgMixEnd = (volatile uint8_t *)(jlgMixBuf + MIX_BUFFER); gNeedRefill[0] = 0; gNeedRefill[1] = 0; gPlayHalf = 0u; @@ -381,7 +381,7 @@ static long installTimerA(void) { // MFP Timer A: stop, install our vector, set prescaler 200 + data // 1 (= 2.4576 MHz / 200 = 12288 Hz), then start. *ST_MFP_TACR = MFP_TACR_STOP; - gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)mfpTimerAIsr); + gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)jlMfpTimerAIsr); *ST_MFP_TADR = 1; *ST_MFP_TACR = MFP_TACR_DIV200; Jenabint(INT_TIMER_A); @@ -412,7 +412,7 @@ static long uninstallTimerA(void) { // Silence in the output stage's own sample format (MIX_SILENCE). static void silenceMixBuffer(void) { - memset((void *)gMixBuf, MIX_SILENCE, MIX_BUFFER); + memset((void *)jlgMixBuf, MIX_SILENCE, MIX_BUFFER); } @@ -432,7 +432,7 @@ static uint16_t dmaPlayOffset(void) { mid = *STE_DMA_COUNT_MID; lo = *STE_DMA_COUNT_LO; } while (hi != *STE_DMA_COUNT_HI); - return (uint16_t)((((uint32_t)hi << 16) | ((uint32_t)mid << 8) | lo) - (uint32_t)gMixBuf); + return (uint16_t)((((uint32_t)hi << 16) | ((uint32_t)mid << 8) | lo) - (uint32_t)jlgMixBuf); } @@ -471,8 +471,8 @@ static long installDma(void) { gNeedRefill[0] = 0; gNeedRefill[1] = 0; gPlayHalf = 0u; - dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)gMixBuf); - dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(gMixBuf + MIX_BUFFER)); + dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)jlgMixBuf); + dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(jlgMixBuf + MIX_BUFFER)); *STE_DMA_MODE = STE_DMA_STEREO_12K; *STE_DMA_CTRL = STE_DMA_PLAY_LOOP; return 0; @@ -494,7 +494,7 @@ static uint16_t playOffset(void) { #ifdef JOEYLIB_ATARI_STE return dmaPlayOffset(); #else - return (uint16_t)(gMixPos - gMixBuf); + return (uint16_t)(jlgMixPos - jlgMixBuf); #endif } @@ -523,10 +523,10 @@ static void refillHalf(uint8_t half) { volatile uint8_t *dst; bool silentFill; - dst = gMixBuf + (uint16_t)half * MIX_HALF; - silentFill = !modPlayerActive() && !anySfxActive(); - if (modPlayerActive()) { - if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) { + dst = jlgMixBuf + (uint16_t)half * MIX_HALF; + silentFill = !jlModPlayerActive() && !anySfxActive(); + if (jlModPlayerActive()) { + if (!jlModPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) { gModTail = true; gModTailHalf = half; } @@ -534,9 +534,9 @@ static void refillHalf(uint8_t half) { memset((void *)dst, MIX_SILENCE_UNSIGNED, MIX_HALF); } #ifdef JOEYLIB_ATARI_STE - audioSfxOverlayMixStereo(dst, MIX_HALF_FRAMES, gSfxSlots, JL_AUDIO_SFX_SLOTS, gSfxPan, AUDIO_SFX_DST_UNSIGNED); + jlAudioSfxOverlayMixStereo(dst, MIX_HALF_FRAMES, gSfxSlots, JL_AUDIO_SFX_SLOTS, gSfxPan, AUDIO_SFX_DST_UNSIGNED); #else - audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS); + jlAudioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS); #endif #ifdef JOEYLIB_ATARI_STE dmaSignBytes(dst, MIX_HALF); @@ -552,9 +552,9 @@ static void refillHalf(uint8_t half) { // mixed as they stand, in order, never flipped to unsigned and back. static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot) { #ifdef JOEYLIB_ATARI_STE - audioSfxOverlayMixStereo(gMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED); + jlAudioSfxOverlayMixStereo(jlgMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED); #else - audioSfxOverlayMix(gMixBuf + at, len, &gSfxSlots[slot], 1u); + jlAudioSfxOverlayMix(jlgMixBuf + at, len, &gSfxSlots[slot], 1u); #endif } @@ -596,7 +596,7 @@ static void sfxStartNow(uint8_t slot, bool fresh) { if (start < halfEnd) { sfxOverlayAt(start, (uint16_t)(halfEnd - start), slot); gHalfSilent[half] = false; - start = halfEnd; + start = halfEnd; } if (start < aheadEnd) { sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF + (start - halfEnd)), (uint16_t)(aheadEnd - start), slot); @@ -630,7 +630,7 @@ static void drainCancel(void) { gDraining = false; at = (uint16_t)((playOffset() + PWM_DRAIN_MARGIN) % MIX_BUFFER); for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) { - gMixBuf[(at + i) % MIX_BUFFER] = MIX_SILENCE; + jlgMixBuf[(at + i) % MIX_BUFFER] = MIX_SILENCE; } rampWrite(at, 0u, MIX_SILENCE); gHalfSilent[0] = false; @@ -655,7 +655,7 @@ static void drainStart(void) { gDrainFrom = playOffset(); at = (uint16_t)((gDrainFrom + PWM_DRAIN_MARGIN) % MIX_BUFFER); for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) { - gMixBuf[(at + i) % MIX_BUFFER] = 0u; + jlgMixBuf[(at + i) % MIX_BUFFER] = 0u; } rampWrite(at, MIX_SILENCE, 0u); gDraining = true; @@ -668,12 +668,12 @@ static void rampWrite(uint16_t at, uint8_t from, uint8_t to) { uint16_t i; for (i = 0u; i < PWM_RAMP_SAMPLES; i++) { - gMixBuf[(at + i) % MIX_BUFFER] = (uint8_t)((int16_t)from + ((int16_t)to - (int16_t)from) * (int16_t)i / (int16_t)PWM_RAMP_SAMPLES); + jlgMixBuf[(at + i) % MIX_BUFFER] = (uint8_t)((int16_t)from + ((int16_t)to - (int16_t)from) * (int16_t)i / (int16_t)PWM_RAMP_SAMPLES); } } -// gYmLevel: for each sample value, the level whose amplitude is nearest +// jlgYmLevel: for each sample value, the level whose amplitude is nearest // the value's place on the line from level 0's amplitude to level 15's. static void ymLevelsBuild(void) { uint32_t target; @@ -683,14 +683,14 @@ static void ymLevelsBuild(void) { uint8_t v; for (s = 0u; s < SAMPLE_VALUES; s++) { - target = kYmAmplitude[0] + ((uint32_t)(kYmAmplitude[YM_LEVELS - 1u] - kYmAmplitude[0]) * s) / YM_SAMPLE_MAX; - best = UINT32_MAX; - gYmLevel[s] = 0u; + target = kYmAmplitude[0] + ((uint32_t)(kYmAmplitude[YM_LEVELS - 1u] - kYmAmplitude[0]) * s) / YM_SAMPLE_MAX; + best = UINT32_MAX; + jlgYmLevel[s] = 0u; for (v = 0u; v < YM_LEVELS; v++) { gap = kYmAmplitude[v] > target ? kYmAmplitude[v] - target : target - kYmAmplitude[v]; if (gap < best) { - best = gap; - gYmLevel[s] = v; + best = gap; + jlgYmLevel[s] = v; } } } @@ -746,7 +746,7 @@ bool jlpAudioInit(void) { } #ifdef JOEYLIB_ATARI_STE // The STe build's output stage is the DMA chip: no chip, no audio. - if (!steMachineCheck()) { + if (!jlSteMachineCheck()) { return false; } #else @@ -755,8 +755,8 @@ bool jlpAudioInit(void) { #ifdef JOEYLIB_ATARI_STE memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan)); #endif - modPlayerSetU8Shift(MIX_UNIT_SHIFT); - audioSfxSetShift(MIX_UNIT_SHIFT); + jlModPlayerSetU8Shift(MIX_UNIT_SHIFT); + jlAudioSfxSetShift(MIX_UNIT_SHIFT); gReady = true; return true; } @@ -789,13 +789,13 @@ void jlpAudioShutdown(void) { return; } silenceMixBuffer(); - modPlayerStop(); + jlModPlayerStop(); gReady = false; } bool jlpAudioIsPlayingMod(void) { - return gReady && (modPlayerActive() || gModTail); + return gReady && (jlModPlayerActive() || gModTail); } @@ -804,7 +804,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { return; } gModTail = false; - if (!modPlayerStart(data, length, MIX_RATE, loop)) { + if (!jlModPlayerStart(data, length, MIX_RATE, loop)) { return; } (void)pumpStart(); @@ -815,7 +815,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE); + jlAudioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE); sfxStartNow(slot, pumpStart()); } @@ -824,7 +824,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE); + jlAudioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE); sfxStartNow(slot, pumpStart()); } @@ -965,10 +965,10 @@ static long ymPsgAllOff(void) { } -// The tick rides TOS's 200 Hz Timer C (stTickIsr chains to TOS's handler), +// The tick rides TOS's 200 Hz Timer C (jlStTickIsr chains to TOS's handler), // counting the rate against it: see the Timer C notes at the top. The hook // may stay in place for the pump (tickHookUpdate), so the tick is switched -// off while its fields change -- at rate 0 stTickRun's carry never comes +// off while its fields change -- at rate 0 jlStTickRun's carry never comes // due -- and its rate goes in last. Each store is one instruction, so this // needs no interrupts held off, and runs before jlInit's supervisor mode. bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { @@ -999,18 +999,18 @@ static void tickHookUpdate(void) { return; } if (want) { - gStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, SETEXC_READ); - (void)Setexc(VEC_MFP_TC, (long)stTickIsr); + jlgStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, SETEXC_READ); + (void)Setexc(VEC_MFP_TC, (long)jlStTickIsr); } else { - (void)Setexc(VEC_MFP_TC, (long)gStTickOldVec); + (void)Setexc(VEC_MFP_TC, (long)jlgStTickOldVec); } gStTickHooked = want; } -// Called from stTickIsr (audio_isr.s) at each Timer C interrupt: the +// Called from jlStTickIsr (audio_isr.s) at each Timer C interrupt: the // program's tick as many times as its rate has come due since the last. -void stTickRun(void) { +void jlStTickRun(void) { gStTickCarry += gStTickHz; while (gStTickCarry >= ST_TIMER_C_HZ) { gStTickCarry -= ST_TIMER_C_HZ; @@ -1019,14 +1019,14 @@ void stTickRun(void) { } -// The tick callback runs from Timer C (stTickIsr), so a section holds +// The tick callback runs from Timer C (jlStTickIsr), so a section holds // interrupts off, as ymWrite does; keep it short, as the PWM pump's Timer A // is held off with it. The core calls these for the outermost section only, // and Exit puts back the status register Enter found. void jlpAudioCriticalEnter(void) { uint16_t saved; - saved = m68kInterruptsOff(); + saved = m68kInterruptsOff(); gCriticalSr = saved; } @@ -1076,7 +1076,7 @@ void jlpAudioStopMod(void) { if (!gReady) { return; } - modPlayerStop(); + jlModPlayerStop(); gModTail = false; silenceMixBuffer(); // StopMod discards the mixed tail by design (buffer just went @@ -1103,7 +1103,7 @@ void jlpAudioStopSfx(uint8_t slot) { // Soft-real-time constraint: a half must be refilled within MIX_HALF / // MIX_RATE = 1024 / 12288 ~= 83 ms of being left, or the play position // comes back round to it stale: a tick that comes a whole buffer late -// replays the old half once. A full MOD-frame mix plus audioSfxOverlayMix +// replays the old half once. A full MOD-frame mix plus jlAudioSfxOverlayMix // can approach that budget under load, so callers should run the game loop // comfortably faster than ~12 Hz. void jlpAudioFrameTick(void) { @@ -1134,7 +1134,7 @@ void jlpAudioFrameTick(void) { // is playing, so the ISR would stream silence forever -- park it (on // the plain ST once PWM_IDLE_MS of it have gone by, after a ramp down // to level 0: drainStart). - if (!modPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) { + if (!jlModPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) { #ifdef JOEYLIB_ATARI_STE pumpStop(); #else diff --git a/src/atarist/audio_isr.s b/src/atarist/audio_isr.s index c5f9532..b12be88 100644 --- a/src/atarist/audio_isr.s +++ b/src/atarist/audio_isr.s @@ -7,7 +7,7 @@ | | Each interrupt: | 1. Reads one byte from the mix buffer (unsigned 8-bit). -| 2. Writes its YM level (_gYmLevel[byte], the level nearest the +| 2. Writes its YM level (_jlgYmLevel[byte], the level nearest the | sample's value: the YM's levels are logarithmic) to YM register 8 | (channel A volume), so the analog filter on the YM output emits | that sample for that tick. @@ -19,17 +19,17 @@ | are reached by short addresses. | | Communication with the C HAL is via three globals defined in audio.c: -| _gMixBuf -- start of the mix buffer (uint8_t array) -| _gMixPos -- volatile pointer to the next sample to play -| _gMixEnd -- end of buffer (used to detect wrap) -| and _gYmLevel, each sample value's YM level (256 bytes), defined here +| _jlgMixBuf -- start of the mix buffer (uint8_t array) +| _jlgMixPos -- volatile pointer to the next sample to play +| _jlgMixEnd -- end of buffer (used to detect wrap) +| and _jlgYmLevel, each sample value's YM level (256 bytes), defined here | and filled by audio.c. | | GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the | gcc driver. .text - .globl _mfpTimerAIsr + .globl _jlMfpTimerAIsr .equ YM_SELECT, 0xFFFF8800 .equ YM_DATA, 0xFFFF8802 @@ -37,19 +37,19 @@ .equ MFP_ISRA, 0xFFFFFA0F .equ ISRA_ACK, 0xDF | clear Timer A's in-service bit -_mfpTimerAIsr: +_jlMfpTimerAIsr: movem.l %d0/%a0,-(%sp) - movea.l _gMixPos,%a0 + movea.l _jlgMixPos,%a0 moveq #0,%d0 move.b (%a0)+,%d0 - move.b (_gYmLevel,%pc,%d0.w),%d0 + move.b (_jlgYmLevel,%pc,%d0.w),%d0 move.b #YM_VOL_A,(YM_SELECT).w move.b %d0,(YM_DATA).w - cmpa.l _gMixEnd,%a0 + cmpa.l _jlgMixEnd,%a0 bcs.s 1f - lea _gMixBuf,%a0 + lea _jlgMixBuf,%a0 1: - move.l %a0,_gMixPos + move.l %a0,_jlgMixPos move.b #ISRA_ACK,(MFP_ISRA).w movem.l (%sp)+,%d0/%a0 rte @@ -59,5 +59,5 @@ _mfpTimerAIsr: | anywhere else would cost an address register (saved and restored) and | its load every sample, and on the plain ST the mix already runs about as | fast as the interrupt plays it. TOS does not protect a program's text. - .globl _gYmLevel -_gYmLevel: .space 256 + .globl _jlgYmLevel +_jlgYmLevel: .space 256 diff --git a/src/atarist/circle.s b/src/atarist/circle.s index 102c9ba..8b1944c 100644 --- a/src/atarist/circle.s +++ b/src/atarist/circle.s @@ -17,7 +17,7 @@ | | ABI: cdecl. d2-d7/a2-a6 callee-save. | -| void surface68kStCircleOutline(uint8_t *base, +| void jlSurface68kStCircleOutline(uint8_t *base, | uint16_t cx, uint16_t cy, | uint16_t r, uint8_t color); | @@ -28,7 +28,7 @@ | d5.w = cx (cached) | a4 = cy (cached, sign-extended) | a3 = base -| a5 = _gStBitMaskWordLut +| a5 = _jlgStBitMaskWordLut | d0,d1,d6,d7 = scratch | | Scratch block (40 bytes) at sp+0..39. Each xp record is 8 bytes but @@ -201,17 +201,17 @@ .equ SP_R, SP_OFF + 12 + 2 .equ SP_COLOR, SP_OFF + 16 + 3 - .globl _surface68kStCircleOutline + .globl _jlSurface68kStCircleOutline -_surface68kStCircleOutline: +_jlSurface68kStCircleOutline: movem.l %d2-%d7/%a2-%a6,-(%sp) lea -SP_LOCAL(%sp),%sp | Load base (a3) and bitMaskLut (a5). The mask LUT is - | the shared _gStBitMaskWordLut in lineSpan.s; reference - | absolute (same pattern as _gStRowOffsetLut below). + | the shared _jlgStBitMaskWordLut in lineSpan.s; reference + | absolute (same pattern as _jlgStRowOffsetLut below). move.l SP_BASE(%sp),%a3 - lea _gStBitMaskWordLut,%a5 + lea _jlgStBitMaskWordLut,%a5 | Cache cx in d5, cy (sign-extended) in a4. move.w SP_CX(%sp),%d5 @@ -230,7 +230,7 @@ _surface68kStCircleOutline: | chain we used to do per YP_REC. Saved across all 4 | YP_RECs per Bresenham iter (~120 cyc/iter). | Shared LUT lives in lineSpan.s; reference absolute. - lea _gStRowOffsetLut,%a6 + lea _jlgStRowOffsetLut,%a6 | Dispatch on color (low 4 bits) -> one of 16 main loops. moveq #0,%d6 @@ -283,4 +283,4 @@ _surface68kStCircleOutline: | (bitMaskWordLut and yLut both live in lineSpan.s as the shared -| _gStBitMaskWordLut / _gStRowOffsetLut.) +| _jlgStBitMaskWordLut / _jlgStRowOffsetLut.) diff --git a/src/atarist/copyRect.s b/src/atarist/copyRect.s index 1ee579b..2f3e04c 100644 --- a/src/atarist/copyRect.s +++ b/src/atarist/copyRect.s @@ -13,7 +13,7 @@ | (its movem path beats a move.l loop past the crossover), so this only | ever sees short spans. | -| void stCopyRectSpans68k(uint8_t *dst, const uint8_t *src, +| void jlStCopyRectSpans68k(uint8_t *dst, const uint8_t *src, | int longsPerRow, int numRows); | Copies numRows rows of longsPerRow longs each, both pointers strided by | ST_BYTES_PER_ROW (160) between row STARTS. dst/src are long-aligned @@ -22,9 +22,9 @@ .text - .globl _stCopyRectSpans68k + .globl _jlStCopyRectSpans68k -_stCopyRectSpans68k: +_jlStCopyRectSpans68k: movem.l %d2/%d3,-(%sp) | 8 bytes saved move.l 12(%sp),%a1 | dst move.l 16(%sp),%a0 | src diff --git a/src/atarist/fillCircle.s b/src/atarist/fillCircle.s index 552ea80..be4d0d8 100644 --- a/src/atarist/fillCircle.s +++ b/src/atarist/fillCircle.s @@ -21,7 +21,7 @@ | | ABI: cdecl. d2-d7/a2-a6 callee-save. | -| void surface68kStFillCircle(uint8_t *base, +| void jlSurface68kStFillCircle(uint8_t *base, | uint16_t cx, uint16_t cy, | uint16_t r, uint8_t color); @@ -56,13 +56,13 @@ lsr.w #4,%d0 | groupLast sub.w %d1,%d0 | numGroups move.w %d0,4(%sp) - | leftMask via LUT[bitFirst]; a5 = _gStLeftMaskLut base + | leftMask via LUT[bitFirst]; a5 = _jlgStLeftMaskLut base move.w 0(%sp),%d0 and.w #15,%d0 add.w %d0,%d0 move.w (%a5,%d0.w),%d1 move.w %d1,0(%sp) - | rightMask via LUT[bitLast]; a6 = _gStRightMaskLut base + | rightMask via LUT[bitLast]; a6 = _jlgStRightMaskLut base move.w 2(%sp),%d0 and.w #15,%d0 add.w %d0,%d0 @@ -111,7 +111,7 @@ .macro SPAN_BODY color | a4 = base + y*160 + groupFirstByteOff - | y*160 via shared _gStRowOffsetLut (a2 holds lut base). + | y*160 via shared _jlgStRowOffsetLut (a2 holds lut base). | byteOff (y*160 + groupFirstByteOff) fits in 16 bits | (max 31992), so word-only ops + .w-indexed lea. add.w %d0,%d0 | y * 2 (word index) @@ -204,9 +204,9 @@ .endm - .globl _surface68kStFillCircle + .globl _jlSurface68kStFillCircle -_surface68kStFillCircle: +_jlSurface68kStFillCircle: movem.l %d2-%d7/%a2-%a6,-(%sp) lea -SP_FC_LOCAL(%sp),%sp @@ -216,12 +216,12 @@ _surface68kStFillCircle: move.b SP_FC_COLOR(%sp),%d7 | LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS). - | a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160). + | a2 = shared _jlgStRowOffsetLut (used by SPAN_BODY for y*160). | All three LUTs are the shared definitions in lineSpan.s; | reference them absolute (single source of truth). - lea _gStLeftMaskLut,%a5 - lea _gStRightMaskLut,%a6 - lea _gStRowOffsetLut,%a2 + lea _jlgStLeftMaskLut,%a5 + lea _jlgStRightMaskLut,%a6 + lea _jlgStRowOffsetLut,%a2 | loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0) moveq #0,%d5 @@ -307,4 +307,4 @@ _surface68kStFillCircle: | (leftMaskLut/rightMaskLut now live in lineSpan.s as the shared -| _gStLeftMaskLut / _gStRightMaskLut, referenced absolute above.) +| _jlgStLeftMaskLut / _jlgStRightMaskLut, referenced absolute above.) diff --git a/src/atarist/hal.c b/src/atarist/hal.c index 30b598b..8c857ca 100644 --- a/src/atarist/hal.c +++ b/src/atarist/hal.c @@ -20,14 +20,14 @@ // registers, waits for the video address counter to reach the end // of the next line's display, and stores all 16 colours in the // horizontal blank before the band, so its first line shows them -// from its first pixel; rasterBandNext then aims it at the next +// from its first pixel; jlRasterBandNext then aims it at the next // band. With 8 transitions per frame it runs 8 times instead of 313 // -- well under the ~147-HBL-fires-per-frame cap Hatari's MFP // emulation imposes on event-count mode -- and waits about a line // each time, ~3% of the CPU. // * A table set (ScreenTablesT) holds each of the 16 palettes already // in shifter format and the band table; jlpPresent requantizes only -// the palettes jlPaletteSet changed (gStagePaletteDirtyMask), and the +// the palettes jlPaletteSet changed (jlgStagePaletteDirtyMask), and the // ISRs copy a band's row straight into the 16 shifter registers. // * Two table sets: jlpPresent writes the one not shown and hands it to // the VBL ISR with the page it flips to, so colours and pixels change @@ -68,12 +68,15 @@ // (ST_BYTES_PER_ROW and ST_BYTES_PER_GROUP live in stPlanar.h so the // stTile.h inlines can see them; the rest are hal-private.) #define ST_GROUPS_PER_ROW 20 +#define ST_PLANES 4 #define ST_PLANAR_SIZE (ST_BYTES_PER_ROW * JL_SURFACE_HEIGHT) #define ST_SCREEN_ALIGN 256 -// present68kStSlam return contract: low 16 bits = bytes copied toward +// jlPresent68kStSlam return contract: low 16 bits = bytes copied toward // the back page; bit 16 = "current frame marked at least one row". #define ST_SLAM_CUR_DIRTY_BIT 0x00010000ul +// stFirstMarkedPair's step: two band longs, four rows' min bytes each. +#define ST_PAIR_ROWS 8 #ifdef JOEYLIB_ATARI_STE // STe build: where the BLiTTER beats the CPU paths, measured on an STe in @@ -81,12 +84,10 @@ // per-plane blits cost ~550 cycles to set up, so a fill spanning two or // more 16-pixel groups wins from 8 rows (even at 320x8, 1.3x at 80x16, // 1.7x at 80x80); a one-group fill, where the CPU's column RMW is cheap, -// only from about 24 rows (1.4x at 1x200); a copy from 4 rows (1.9x at -// 320x200). Shorter work stays on the ST's CPU paths. Decided by the -// rect's size alone: an STe always has the chip. -#define ST_GROUP_PIXELS 16 -#define ST_BLT_FILL_MIN_ROWS 8 -#define ST_BLT_FILL_MIN_ROWS_ONE_GROUP 24 +// only from about 24 rows (1.4x at 1x200) -- those two live with the fill +// in stTile.h -- and a copy from 4 rows (1.9x at 320x200). Shorter work +// stays on the ST's CPU paths. Decided by the rect's size alone: an STe +// always has the chip. #define ST_BLT_COPY_MIN_ROWS 4 #endif @@ -98,12 +99,19 @@ // buffer base without a cross-TU call. #include "stPlanar.h" -// present68kStSlam hardcodes the 200-row / 160-byte-row / 20-group / +// jlPresent68kStSlam hardcodes the 200-row / 160-byte-row / 20-group / // 8-byte-group / 4-rows-per-long geometry (moveq #49 group count, // lea 640 strides, 20-group copy chain, lsl.w #3 byte math). A // geometry change must fail here, not silently corrupt the display. typedef char stPresentSlamGeometryFits[(JL_SURFACE_HEIGHT == 200 && ST_BYTES_PER_ROW == 160 && SURFACE_WORDS_PER_ROW == 80 && ST_BYTES_PER_GROUP == 8) ? 1 : -1]; +// lineSpan.s's jlFillRect reads a surface's planes as SURFACE_PORT_DATA(s) +// then 0(portData), and spells the surface as 320 x 200. +typedef char stFillRectLayoutFits[(offsetof(jlSurfaceT, portData) == 4 && offsetof(StPlanarT, base) == 0 && JL_SURFACE_WIDTH == 320) ? 1 : -1]; + +// lineSpan.s: whole rows of groups as longs (planes 0:1 and 2:3). +extern void jlSurface68kStLongFill(uint8_t *dst, uint16_t numGroups, uint32_t loLong, uint32_t hiLong); + // Shifter palette registers: 16 words at $FFFF8240..$FFFF825F. Each // channel nibble holds the channel's top three bits in bits 0-2; the STe // shifter adds the channel's low bit as bit 3, which a plain ST does not @@ -198,61 +206,12 @@ static inline __attribute__((always_inline)) void stApplyMaskToGroup(uint8_t *gr } -static inline __attribute__((always_inline)) void stPlanarSetPixel(StPlanarT *pd, int16_t x, int16_t y, uint8_t color) { - uint16_t group = (uint16_t)((uint16_t)x >> 4); - uint16_t bitMask = (uint16_t)(1u << (15u - ((uint16_t)x & 15u))); - uint16_t notMask = (uint16_t)~bitMask; - uint16_t *pw = (uint16_t *)(pd->base + (uint16_t)y * ST_BYTES_PER_ROW + group * ST_BYTES_PER_GROUP); - if (color & 1u) { - pw[0] = (uint16_t)(pw[0] | bitMask); - } else { - pw[0] = (uint16_t)(pw[0] & notMask); - } - if (color & 2u) { - pw[1] = (uint16_t)(pw[1] | bitMask); - } else { - pw[1] = (uint16_t)(pw[1] & notMask); - } - if (color & 4u) { - pw[2] = (uint16_t)(pw[2] | bitMask); - } else { - pw[2] = (uint16_t)(pw[2] & notMask); - } - if (color & 8u) { - pw[3] = (uint16_t)(pw[3] | bitMask); - } else { - pw[3] = (uint16_t)(pw[3] & notMask); - } -} - - -static inline __attribute__((always_inline)) uint8_t stPlanarGetPixel(const StPlanarT *pd, int16_t x, int16_t y) { - uint16_t group = (uint16_t)((uint16_t)x >> 4); - uint16_t bitMask = (uint16_t)(1u << (15u - ((uint16_t)x & 15u))); - const uint16_t *pw = (const uint16_t *)(pd->base + (uint16_t)y * ST_BYTES_PER_ROW + group * ST_BYTES_PER_GROUP); - uint8_t c = 0u; - if (pw[0] & bitMask) { - c = (uint8_t)(c | 1u); - } - if (pw[1] & bitMask) { - c = (uint8_t)(c | 2u); - } - if (pw[2] & bitMask) { - c = (uint8_t)(c | 4u); - } - if (pw[3] & bitMask) { - c = (uint8_t)(c | 8u); - } - return c; -} - - // Flood-fill group decoder (PERF-AUDIT #4). For one 16-pixel group (4 // interleaved plane words) build the 16-bit mask whose bit // (15 - (x & 15)) is set exactly where pixel x equals color: invert // each plane word whose color bit is 0, then AND the four words. Four // word reads decode 16 pixels, vs 4 reads PER pixel through -// stPlanarGetPixel. +// stSamplePixel. static inline __attribute__((always_inline)) uint16_t stGroupEqBits(const uint16_t *gp, uint8_t color) { uint16_t p0 = gp[0]; uint16_t p1 = gp[1]; @@ -312,10 +271,11 @@ typedef struct { static void flipWait(void); static void presentShow(const uint8_t *page); static uint16_t quantizeColorToSt(uint16_t orgb); -static void quantizeDirtyPalettes(ScreenTablesT *tables, const jlSurfaceT *src); +static void quantizePalettes(ScreenTablesT *tables, const jlSurfaceT *src, uint16_t mask); static void setShifterVideoBase(const uint8_t *page); -static ScreenTablesT *tablesToWrite(void); -extern void stLeaveSuper(void *savedSsp); +static uint16_t stFirstMarkedPair(void); +static uint8_t tablesToWrite(void); +extern void jlStLeaveSuper(void *savedSsp); static long readFrameHz(void); static long readClockMs(void); @@ -324,9 +284,9 @@ static long writePrevPaletteRegs(void); static __attribute__((interrupt_handler)) void vblIsr(void); static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src); static void rasterAim(const ScreenTablesT *tables, uint16_t band); -void rasterBandNext(void); +void jlRasterBandNext(void); static void refreshPaletteStateIfNeeded(const jlSurfaceT *src); -extern void stRasterIsr(void); +extern void jlStRasterIsr(void); // ----- Module state ----- @@ -371,13 +331,13 @@ static bool gSuperEntered = false; // to "all dirty" so the first two presents do full-screen refreshes // and converge both pages to the current shadow. After that we pay // only the small per-row union(prev,current) copy. -// Non-static + aligned(2): present68kStSlam (presentSlam.s) walks both +// Non-static + aligned(2): jlPresent68kStSlam (presentSlam.s) walks both // arrays with 4-row long-word loads in its fast path; the 68000 // requires EVEN addresses for word/long reads (2 is also the mint // a.out object format's maximum alignment), and char arrays otherwise // carry no alignment guarantee. -uint8_t gPrevStageMinWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); -uint8_t gPrevStageMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); +uint8_t jlgPrevStageMinWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); +uint8_t jlgPrevStageMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); // "The previous frame's snapshot recorded at least one dirty row." // Lets jlpPresent skip the whole 200-row union scan + 200-row snapshot @@ -395,23 +355,30 @@ static bool gPrevAnyDirty = true; // frame they show, so new colours never land on the old page's pixels, nor // old colours on the new page's. #define TABLES_NONE 0xFFu +#define TABLES_ALL_PALETTES ((uint16_t)((1u << JL_SURFACE_PALETTE_COUNT) - 1u)) static ScreenTablesT gTables[2]; static volatile uint8_t gTablesShown = 0; static volatile uint8_t gTablesPending = TABLES_NONE; static uint8_t gTablesWriting = TABLES_NONE; +// What each set lacks of the stage: the palettes (a bit each) and the bands +// changed since it was last written. A set is brought up to date when it is +// next written, rather than copied whole from the other each time +// (refreshPaletteStateIfNeeded); jlpInit makes both lack everything. +static uint16_t gTablesStalePalettes[2]; +static bool gTablesStaleBands[2]; // Index of the band whose palette is on the shifter. At VBL this is 0 // (band 0 palette pre-loaded, Timer B scheduled for band 1); each Timer B -// fire loads the next band's and advances it (rasterBandNext). +// fire loads the next band's and advances it (jlRasterBandNext). static volatile uint16_t gCurrentBand = 0; // What the Timer B handler (rasterIsr.s) loads next: the band's 16 colours, // and the video address counter's low 16 bits when the display of the line // above the band ends (rasterAim), from gRasterBase, the counter at the // frame's start (the shown page's address). -const uint16_t *gStRasterColors = gTables[0].palettes[0]; -uint16_t gStRasterTarget = 0u; -static uint16_t gRasterBase = 0u; +const uint16_t *jlgStRasterColors = gTables[0].palettes[0]; +uint16_t jlgStRasterTarget = 0u; +static uint16_t gRasterBase = 0u; // The VBL count (jlpFrameCount). static volatile int16_t gFrameCount = 0; @@ -425,13 +392,13 @@ static volatile uint8_t gFlipPending = 0u; static void (*gOldVblVec)(void) = NULL; static void (*gOldTimerBVec)(void) = NULL; -// Palette/SCB change detection is the core gStageScbDirty / -// gStagePaletteDirty flags (surfaceInternal.h) -- the mutation APIs +// Palette/SCB change detection is the core jlgStageScbDirty / +// jlgStagePaletteDirty flags (surfaceInternal.h) -- the mutation APIs // set them and refreshPaletteStateIfNeeded clears them after // rebuilding, replacing a 712-byte cached-mirror memcmp per present // (NATIVE-PERF Phase 2 R9; the IIgs consumed the flags first). Each // flag rebuilds only its own table: a palette change requantizes the -// palettes in gStagePaletteDirtyMask, an SCB change rescans the bands. +// palettes in jlgStagePaletteDirtyMask, an SCB change rescans the bands. // ----- Internal helpers (alphabetical) ----- @@ -517,14 +484,11 @@ static uint16_t quantizeColorToSt(uint16_t orgb) { } -// Requantize the palettes jlPaletteSet changed since the last present -// (gStagePaletteDirtyMask; any other stage change sets every bit). -static void quantizeDirtyPalettes(ScreenTablesT *tables, const jlSurfaceT *src) { - uint16_t mask; +// Requantize the palettes in mask (a bit each) from the stage's. +static void quantizePalettes(ScreenTablesT *tables, const jlSurfaceT *src, uint16_t mask) { uint16_t pal; uint16_t col; - mask = gStagePaletteDirtyMask; for (pal = 0; mask != 0u; pal++, mask = (uint16_t)(mask >> 1)) { if ((mask & 1u) == 0u) { continue; @@ -539,32 +503,38 @@ static void quantizeDirtyPalettes(ScreenTablesT *tables, const jlSurfaceT *src) // Rebuild the shifter palettes and the band-transition table only when // their source actually changed, and each only for its own flag: in the // typical game loop neither changes, and a palette-cycling effect -// changes one palette, not the SCB. `src` is always gStage (jlpPresent's +// changes one palette, not the SCB. `src` is always jlgStage (jlpPresent's // contract), so the core dirty flags are the change signal; clear them -// only after rebuilding. +// only after rebuilding. The set written takes this change and what it +// missed while the other was written; the other is marked to take this one +// when its turn comes. static void refreshPaletteStateIfNeeded(const jlSurfaceT *src) { - ScreenTablesT *tables; + uint16_t changed; + uint8_t set; - if (!gStagePaletteDirty && !gStageScbDirty) { + if (!jlgStagePaletteDirty && !jlgStageScbDirty) { return; } - tables = tablesToWrite(); - if (gStagePaletteDirty) { - quantizeDirtyPalettes(tables, src); - gStagePaletteDirty = false; - gStagePaletteDirtyMask = 0u; - } - if (gStageScbDirty) { - buildTransitions(tables, src); - gStageScbDirty = false; + set = tablesToWrite(); + changed = jlgStagePaletteDirty ? jlgStagePaletteDirtyMask : 0u; + quantizePalettes(&gTables[set], src, (uint16_t)(changed | gTablesStalePalettes[set])); + gTablesStalePalettes[set] = 0u; + gTablesStalePalettes[set ^ 1u] |= changed; + if (jlgStageScbDirty || gTablesStaleBands[set]) { + buildTransitions(&gTables[set], src); } + gTablesStaleBands[set] = false; + gTablesStaleBands[set ^ 1u] |= jlgStageScbDirty; + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = 0u; + jlgStageScbDirty = false; } // The table set this present writes: one handed over and not yet taken // (two presents in a frame), taken back first so the VBL cannot show it -// half written; else the one not shown, made a copy of the shown one. -static ScreenTablesT *tablesToWrite(void) { +// half written; else the one not shown. +static uint8_t tablesToWrite(void) { uint16_t sr; uint8_t set; @@ -574,18 +544,17 @@ static ScreenTablesT *tablesToWrite(void) { m68kInterruptsRestore(sr); if (set == TABLES_NONE) { set = (uint8_t)(gTablesShown ^ 1u); - memcpy(&gTables[set], &gTables[gTablesShown], sizeof(gTables[set])); } gTablesWriting = set; - return &gTables[set]; + return set; } // Aim the Timer B handler at band: its colours, and the counter's value when // the display of the line above it ends. static void rasterAim(const ScreenTablesT *tables, uint16_t band) { - gStRasterColors = tables->palettes[tables->bandPalIdx[band]]; - gStRasterTarget = (uint16_t)(gRasterBase + tables->bandStart[band] * ST_BYTES_PER_ROW); + jlgStRasterColors = tables->palettes[tables->bandPalIdx[band]]; + jlgStRasterTarget = (uint16_t)(gRasterBase + tables->bandStart[band] * ST_BYTES_PER_ROW); } @@ -600,7 +569,7 @@ static void rasterAim(const ScreenTablesT *tables, uint16_t band) { // so a value written here does not take effect on the NEXT fire (that // interval was already reloaded at the underflow that triggered this // interrupt) but on the fire AFTER next: the band + 2 delta. -void rasterBandNext(void) { +void jlRasterBandNext(void) { const ScreenTablesT *tables; uint16_t band; @@ -675,7 +644,7 @@ static void vblIsr(void) { // The restart above loads the counter from TBDR (band 1's // delta), so the first fire is correctly timed. Now pre-program // TBDR with band 2's so the band-1 underflow auto-reloads with - // the right interval (one fire ahead, matching rasterBandNext). + // the right interval (one fire ahead, matching jlRasterBandNext). // Writing TBDR while counting only changes the next reload, not // the in-flight count. if (tables->bandCount > 2) { @@ -705,6 +674,26 @@ static void setShifterVideoBase(const uint8_t *page) { } +// The first pair of 4-row band groups holding a marked row (a min byte +// that is not the CLEAN sentinel), as an index of ST_PAIR_ROWS rows, or +// JL_SURFACE_HEIGHT / ST_PAIR_ROWS when none is: two longs and'ed, eight +// rows a step (jlgStageMinWord is aligned(2), all move.l needs on a 68000). +static uint16_t stFirstMarkedPair(void) { + register const uint8_t *rBands __asm__("a0") = jlgStageMinWord; + register uint16_t rLeft __asm__("d1") = (uint16_t)(JL_SURFACE_HEIGHT / ST_PAIR_ROWS - 1u); + + __asm__ volatile ( + "1: move.l (%0)+,%%d0\n\t" + " and.l (%0)+,%%d0\n\t" + " not.l %%d0\n\t" + " dbne %1,1b" + : "+a" (rBands), "+d" (rLeft) + : + : "d0", "cc"); + return (uint16_t)(JL_SURFACE_HEIGHT / ST_PAIR_ROWS - 1u - rLeft); +} + + static long writePrevPaletteRegs(void) { uint16_t i; for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { @@ -725,8 +714,8 @@ bool jlpInit(const jlConfigT *config) { #ifdef JOEYLIB_ATARI_STE // The STe build uses the BLiTTER, DMA sound and 4-bit palette without // asking again: refuse anything but an STe or Mega STe before touching - // the machine (steMachineCheck leaves the reason for jlLastError). - if (!steMachineCheck()) { + // the machine (jlSteMachineCheck leaves the reason for jlLastError). + if (!jlSteMachineCheck()) { return false; } #endif @@ -735,7 +724,7 @@ bool jlpInit(const jlConfigT *config) { // Getrez reports as the shifter's own mode number: the 16-colour // 320x200 mode cannot be shown there. if (Getrez() == ST_RES_MONO) { - coreSetError("a monochrome monitor cannot show the 16-colour 320x200 mode"); + jlCoreSetError("a monochrome monitor cannot show the 16-colour 320x200 mode"); return false; } @@ -756,7 +745,7 @@ bool jlpInit(const jlConfigT *config) { #ifdef JOEYLIB_ATARI_STE // A Mega STe runs at 16 MHz with its cache on until jlpShutdown hands // the clock back; an STe has no such switch. - steMachineSetFullSpeed(); + jlSteMachineSetFullSpeed(); #endif // Align page 0 to 256 bytes inside the static storage; page 1 @@ -769,6 +758,11 @@ bool jlpInit(const jlConfigT *config) { gScreenPage[1] = gScreenPage[0] + JL_SURFACE_PIXELS_SIZE; gFrontIdx = 0; + gTablesStalePalettes[0] = TABLES_ALL_PALETTES; + gTablesStalePalettes[1] = TABLES_ALL_PALETTES; + gTablesStaleBands[0] = true; + gTablesStaleBands[1] = true; + memset(gScreenPage[0], 0, JL_SURFACE_PIXELS_SIZE); memset(gScreenPage[1], 0, JL_SURFACE_PIXELS_SIZE); @@ -777,8 +771,8 @@ bool jlpInit(const jlConfigT *config) { // into sync with the shadow. After that we converge to the small // per-row union(prev,current) cost. for (i = 0; i < JL_SURFACE_HEIGHT; i++) { - gPrevStageMinWord[i] = 0u; - gPrevStageMaxWord[i] = STAGE_DIRTY_FULL_MAX; + jlgPrevStageMinWord[i] = 0u; + jlgPrevStageMaxWord[i] = STAGE_DIRTY_FULL_MAX; } gPrevPhysbase = Physbase(); @@ -807,11 +801,11 @@ bool jlpInit(const jlConfigT *config) { gOldTimerBVec = (void (*)(void))Setexc(VEC_MFP_TB, -1L); (void)Setexc(VEC_VBL, (long)vblIsr); - // Install MFP Timer B's vector (stRasterIsr) stopped, TBDR=1 a + // Install MFP Timer B's vector (jlStRasterIsr) stopped, TBDR=1 a // placeholder, then enable it in IMRA: the VBL ISR starts it in // event-count (HBL) mode each frame with bands, once it has aimed the // handler at the first (rasterAim). - Xbtimer(1, MFP_TBCR_STOP, 1, stRasterIsr); + Xbtimer(1, MFP_TBCR_STOP, 1, jlStRasterIsr); Jenabint(INT_TIMER_B); return true; @@ -821,7 +815,7 @@ bool jlpInit(const jlConfigT *config) { // The fused single-pass present walker (presentSlam.s). Declared here, // next to its only caller, instead of in the asm extern block below -- // that block sits after this function. -extern uint32_t present68kStSlam(const uint8_t *srcBase, uint8_t *dstBase); +extern uint32_t jlPresent68kStSlam(const uint8_t *srcBase, uint8_t *dstBase); void jlpPresent(const jlSurfaceT *src) { StPlanarT *pd; @@ -831,11 +825,16 @@ void jlpPresent(const jlSurfaceT *src) { uint32_t slam; uint16_t copied; + // Every way out leaves the bands clean (JL_PRESENT_CLEARS_BANDS): these + // two by clearing them, the idle early-out because nothing marked them, + // and the slam as it walks them. if (src == NULL || !gModeSet) { + jlStageDirtyClearAll(); return; } pd = (StPlanarT *)src->portData; if (pd == NULL) { + jlStageDirtyClearAll(); return; } refreshPaletteStateIfNeeded(src); @@ -847,42 +846,22 @@ void jlpPresent(const jlSurfaceT *src) { // ran on the first idle frame), so there is nothing to copy, no // page to flip, and the snapshot below would only copy clean->clean. // Skip the whole 200-row union scan and the 200-row snapshot copy. - // Group test, 4 rows per longword: a clean row's minWord is the - // 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_ - // FULL_MAX (79), so 4 clean rows read exactly 0xFFFFFFFF -- 50 - // long tests replace 200 byte tests on the common all-clean idle - // frame (gStageMinWord is aligned(2), all the 68000 needs for - // move.l). A failing group byte-walks its 4 rows with the same - // per-row test as before, so the first-dirty row and the - // scanned-rows accounting are exactly the byte-walker's. + // stFirstMarkedPair tests 8 rows a step (a clean row's minWord is the + // 0xFF CLEAN sentinel, any touched row's <= STAGE_DIRTY_FULL_MAX); a + // marked pair walks its rows with the per-row test, so the first-dirty + // row and the scanned-rows accounting are exactly a byte walk's. // Palette/SCB updates already ran above; presentShow hands them over. if (!gPrevAnyDirty) { - typedef uint32_t __attribute__((may_alias)) BandGroupT; - const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord; - bool anyDirty = false; - uint8_t g; - - y = JL_SURFACE_HEIGHT; - for (g = 0u; g < (uint8_t)(JL_SURFACE_HEIGHT / 4u); g++) { - if (minGroups[g] != 0xFFFFFFFFul) { - int16_t yy; - for (yy = (int16_t)((uint16_t)g * 4u); yy < (int16_t)((uint16_t)g * 4u + 4u); yy++) { - if (!STAGE_DIRTY_ROW_CLEAN(yy)) { - anyDirty = true; - y = yy; - break; - } - } - if (anyDirty) { - break; - } - } - } - gPresentScannedRows += (uint32_t)(anyDirty ? (y + 1) : JL_SURFACE_HEIGHT); - if (!anyDirty) { + y = (int16_t)(stFirstMarkedPair() * ST_PAIR_ROWS); + if (y >= JL_SURFACE_HEIGHT) { + jlgPresentScannedRows += JL_SURFACE_HEIGHT; presentShow(NULL); return; } + while (STAGE_DIRTY_ROW_CLEAN(y)) { + y++; + } + jlgPresentScannedRows += (uint32_t)(y + 1); } // PAGE FLIP: draw into the stage's shadow (pd->base), bring the @@ -901,11 +880,11 @@ void jlpPresent(const jlSurfaceT *src) { backPage = gScreenPage[backIdx]; flipWait(); - slam = present68kStSlam(pd->base, backPage); + slam = jlPresent68kStSlam(pd->base, backPage); copied = (uint16_t)slam; - gPresentCopiedBytes += copied; - gPresentScannedRows += JL_SURFACE_HEIGHT; + jlgPresentCopiedBytes += copied; + jlgPresentScannedRows += JL_SURFACE_HEIGHT; gPrevAnyDirty = (slam & ST_SLAM_CUR_DIRTY_BIT) != 0u; // Only flip when something was refreshed -- flipping on an idle @@ -927,7 +906,7 @@ void jlpPresent(const jlSurfaceT *src) { // contract is "the front page holds the complete current image after // every present", so zero mismatch == no stale rows on screen -- the // failure class the golden stage hashes cannot see. Always compiled: -// the probe build links the stock library (gPresentCopiedBytes +// the probe build links the stock library (jlgPresentCopiedBytes // precedent), so a #ifdef'd symbol would not exist at link time. uint32_t jlpProbeDisplayMismatch(const jlSurfaceT *src) { const StPlanarT *pd; @@ -987,6 +966,9 @@ uint16_t jlpFrameCount(void) { // counts from jlInit (it subtracts the reading jlInit took). static long readClockMs(void) { uint32_t ticks; + uint16_t elapsed; + uint16_t ms; + uint16_t step; uint8_t count; bool pending; @@ -998,7 +980,15 @@ static long readClockMs(void) { if (pending && count > ST_TICK_RELOAD / 2u) { ticks++; } - return (long)(ticks * ST_TICK_MS + (uint32_t)(ST_TICK_RELOAD - count) * ST_TICK_MS / ST_TICK_RELOAD); + // The milliseconds into the tick, (RELOAD - count) * MS / RELOAD, as + // the steps it has passed: a 68000 has no 32-bit divide, and gcc calls + // one for the expression. + elapsed = (uint16_t)(ST_TICK_RELOAD - count); + ms = 0u; + for (step = 1u; step < ST_TICK_MS; step++) { + ms = (uint16_t)(ms + (elapsed >= (step * ST_TICK_RELOAD + ST_TICK_MS - 1u) / ST_TICK_MS)); + } + return (long)(ticks * ST_TICK_MS + ms); } @@ -1056,7 +1046,7 @@ void jlpShutdown(void) { Setscreen((long)gPrevLogbase, (long)gPrevPhysbase, gPrevRez); Supexec(writePrevPaletteRegs); #ifdef JOEYLIB_ATARI_STE - steMachineRestoreSpeed(); + jlSteMachineRestoreSpeed(); #endif gModeSet = false; @@ -1068,7 +1058,7 @@ void jlpShutdown(void) { // reads the saved registers and the return address from the // wrong slot and rts lands in nothing. superExit.s parks the // entry SP in a GEMDOS-preserved register and puts it back. - stLeaveSuper(gSavedSsp); + jlStLeaveSuper(gSavedSsp); gSuperEntered = false; } } @@ -1077,17 +1067,10 @@ void jlpShutdown(void) { // ST word-interleaved planar 68k fast paths. The surface68kSt* // primitives are ST-specific (plane-major group layout); they live in // src/shared68k/surface68k.s alongside the Amiga-only planar helpers. -extern void surface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color); -extern void surface68kStDrawLine(uint8_t *base, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color); -extern void surface68kStFillSpan(uint8_t *base, int16_t left, int16_t right, int16_t y, uint8_t color); -extern void surface68kStFillCircle(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color); -extern void surface68kStFillRectSingleGroup(uint8_t *firstGroupPtr, uint16_t mask, uint16_t h, uint8_t color); -extern void surface68kStFillRectMulti(uint8_t *base, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t color); -extern void surface68kStLongFill(uint8_t *dst, uint16_t numGroups, uint32_t loLong, uint32_t hiLong); -extern void surface68kStSprite16x16Save(uint8_t *base, uint16_t x, uint16_t y, uint8_t *dstBuf); -extern void surface68kStSprite16x16Restore(uint8_t *base, uint16_t x, uint16_t y, const uint8_t *srcBuf); -extern void surface68kStSpriteSaveByteAligned(uint8_t *base, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes); -extern void surface68kStSpriteRestoreByteAligned(uint8_t *base, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes); +extern void jlSurface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color); +extern void jlSurface68kStDrawLine(uint8_t *base, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color); +extern void jlSurface68kStFillSpan(uint8_t *base, int16_t left, int16_t right, int16_t y, uint8_t color); +extern void jlSurface68kStFillCircle(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color); // Phase 9: clear the entire planar buffer to a 4-bit color. Build an @@ -1109,31 +1092,17 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) { color = (uint8_t)(doubled & 0x0Fu); #ifdef JOEYLIB_ATARI_STE // A whole-surface fill is far past the BLiTTER's crossover. - stBlitFillRect(pd->base, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, color); + jlStBlitFillRect(pd->base, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, color); #else /* Per-group: [p0_word][p1_word][p2_word][p3_word] = 8 bytes = 2 longs. * loLong = (p0_word << 16) | p1_word; hiLong = (p2_word << 16) | p3_word. */ loLong = ((color & 1u) ? 0xFFFF0000ul : 0ul) | ((color & 2u) ? 0x0000FFFFul : 0ul); hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul) | ((color & 8u) ? 0x0000FFFFul : 0ul); - surface68kStLongFill(pd->base, (uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP), loLong, hiLong); + jlSurface68kStLongFill(pd->base, (uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP), loLong, hiLong); #endif } -// Phase 9: planar-only. Chunky shadow is gone; only the planar buffer -// gets the pixel. -void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { - StPlanarT *pd; - - if (s->portData == NULL) { - jlpGenericDrawPixel(s, x, y, colorIndex); - return; - } - pd = (StPlanarT *)s->portData; - stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu)); -} - - // The Phase 9 C walkers (stPlanarLine / stPlanarCircleOutline / // stPlanarFillSpan / stPlanarCircleFill) were DELETED (perf-hunt-2 // rank 17): draw.c gates every jlpDrawLine / jlpDrawCircle / @@ -1141,7 +1110,7 @@ void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { // endpoint check, jlDrawCircle/jlFillCircle's bounding-box check), so // the per-call re-checks here and their clipped C fallbacks were // provably unreachable. The asm walkers own the primitives outright; -// surface68kStFillSpan stays available in surface68kSt.s. +// jlSurface68kStFillSpan stays available in surface68kSt.s. // CONTRACT (rank 17): draw.c already gates these on fully-on-surface @@ -1155,7 +1124,7 @@ void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, return; } pd = (StPlanarT *)s->portData; - surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu)); + jlSurface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu)); } @@ -1165,7 +1134,7 @@ void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co return; } pd = (StPlanarT *)s->portData; - surface68kStCircleOutline(pd->base, (uint16_t)cx, (uint16_t)cy, r, (uint8_t)(colorIndex & 0x0Fu)); + jlSurface68kStCircleOutline(pd->base, (uint16_t)cx, (uint16_t)cy, r, (uint8_t)(colorIndex & 0x0Fu)); } @@ -1175,7 +1144,7 @@ bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co return false; } pd = (StPlanarT *)s->portData; - surface68kStFillCircle(pd->base, (uint16_t)cx, (uint16_t)cy, r, (uint8_t)(colorIndex & 0x0Fu)); + jlSurface68kStFillCircle(pd->base, (uint16_t)cx, (uint16_t)cy, r, (uint8_t)(colorIndex & 0x0Fu)); return true; } @@ -1238,80 +1207,6 @@ void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) { } -// Phase 3: dual-write to the word-interleaved planar shadow buffer. -// Chunky shadow (s->pixels) is still the source-of-truth for display -// (c2p at present); the planar buffer becomes authoritative at -// Phase 9 switch flip. -// -// Per row: split [x, x+w) into a leading partial group (bits -// 15..15-bitFirst within the leading word -> mask = (1<<(16-bitFirst)) -// - 1), zero or more full groups, and a trailing partial group -// (bits 15..15-bitLast -> mask = ~((1<<(15-bitLast)) - 1)). For each -// of the 4 plane words within a group, the bit value of the color -// index controls OR-with-mask (set) vs AND-with-not-mask (clear). -// Single-group case (groupFirst == groupLast) collapses to one word -// RMW per plane with the combined mask. -// (stApplyMaskToGroup is defined inline near the top of the file.) - - -void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { - StPlanarT *pd; - uint16_t groupFirst; - uint16_t groupLast; - uint8_t *rowBase; - - if (s->portData == NULL) { - jlpGenericFillRect(s, x, y, w, h, colorIndex); - return; - } - // Callers guarantee s != NULL and positive w/h: the jlFillRect - // wrapper validates + clips before fillRectOnSurface, and the - // fill-circle span loop emits spanWidth = 2x+1 >= 1 with h = 1. - pd = (StPlanarT *)s->portData; - if (pd == NULL) { - return; - } -#ifdef JOEYLIB_ATARI_STE - // STe build: the BLiTTER, where it wins (ST_BLT_FILL_MIN_ROWS*): tall, - // or past a single 16-pixel group and at least a few rows. - if (h >= ST_BLT_FILL_MIN_ROWS_ONE_GROUP || (h >= ST_BLT_FILL_MIN_ROWS && (x & (ST_GROUP_PIXELS - 1)) + w > ST_GROUP_PIXELS)) { - stBlitFillRect(pd->base, x, y, w, h, colorIndex); - return; - } -#endif - - /* Phase 10 fast path: x == 0 AND w == JL_SURFACE_WIDTH means the rect - * spans every group on every row with no edge masks. movem.l-based - * asm long-fill batches 24 bytes per call. UBER jlFillRect 320x200 - * lands here. */ - if (x == 0 && w == JL_SURFACE_WIDTH) { - uint32_t loLong = ((colorIndex & 1u) ? 0xFFFF0000ul : 0ul) | ((colorIndex & 2u) ? 0x0000FFFFul : 0ul); - uint32_t hiLong = ((colorIndex & 4u) ? 0xFFFF0000ul : 0ul) | ((colorIndex & 8u) ? 0x0000FFFFul : 0ul); - surface68kStLongFill(pd->base + (uint16_t)y * ST_BYTES_PER_ROW, (uint16_t)((uint16_t)h * ST_GROUPS_PER_ROW), loLong, hiLong); - return; - } - - groupFirst = (uint16_t)((uint16_t)x >> 4); - groupLast = (uint16_t)(((uint16_t)x + w - 1u) >> 4); - - if (groupFirst == groupLast) { - uint16_t bitFirst = (uint16_t)((uint16_t)x & 15u); - uint16_t bitLast = (uint16_t)(((uint16_t)x + w - 1u) & 15u); - uint16_t leftMask = (uint16_t)((1ul << (16u - bitFirst)) - 1ul); - uint16_t rightMask = (uint16_t)~((1ul << (15u - bitLast)) - 1ul); - uint16_t mask = (uint16_t)(leftMask & rightMask); - rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW; - surface68kStFillRectSingleGroup(rowBase + groupFirst * ST_BYTES_PER_GROUP, mask, h, colorIndex); - return; - } - - /* Phase 10.5: multi-group case (groupFirst != groupLast) handled - * by 16-way-color-dispatched asm with hoisted mask state. ~3-5x - * faster than the C loop with inlined stApplyMaskToGroup. */ - surface68kStFillRectMulti(pd->base, x, y, w, h, colorIndex); -} - - void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { StPlanarT *dstPd; StPlanarT *srcPd; @@ -1336,7 +1231,7 @@ void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { #define ST_COPYRECT_MOVEL_MAX_BYTES 64u // Hand-rolled move.l span copier (src/atarist/copyRect.s). -void stCopyRectSpans68k(uint8_t *dst, const uint8_t *src, int longsPerRow, int numRows); +void jlStCopyRectSpans68k(uint8_t *dst, const uint8_t *src, int longsPerRow, int numRows); // Same-position group-snapped rect copy (NATIVE-PERF Phase 4 @@ -1374,12 +1269,12 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint // STe build: the BLiTTER takes the whole rect in one blit from a few // rows up (ST_BLT_COPY_MIN_ROWS), whatever the width. if (h >= ST_BLT_COPY_MIN_ROWS) { - stBlitCopyRows(d, s, rowBytes, h); + jlStBlitCopyRows(d, s, rowBytes, h); return; } #endif if (rowBytes <= ST_COPYRECT_MOVEL_MAX_BYTES) { - stCopyRectSpans68k(d, s, (int)(rowBytes >> 2), (int)h); + jlStCopyRectSpans68k(d, s, (int)(rowBytes >> 2), (int)h); } else { for (row = 0; row < h; row++) { memcpy(d, s, rowBytes); @@ -1401,11 +1296,6 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint // (DRAW hash vs IIgs reference) whether each primitive landed bits // in the right place. -// stPlanarSetPixel and stPlanarGetPixel are defined inline near the -// top of the file (between StPlanarT and the prototype block) so -// every callsite folds the 4-plane RMW into its body. - - // The sprite interpreter (uncompiled sprites, and compiled ones drawn partly // off the surface): the planar ports' shared walker over the sprite's // planar cache, src/m68k/planarSprite.c, given the screen base (it knows @@ -1417,236 +1307,7 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t if (pd == NULL) { return; } - planarSpriteDraw(&pd->base, sp, x, y); -} - - -// Phase 10: hoist y*160 to per-row, fold setPixel/getPixel bodies -// inline. Each pixel's group address differs only in (x), so we -// can compute base+row*160 once per row and just do per-pixel -// (group, bitMask, 4 plane RMW). 2x speedup over the per-pixel -// stPlanarSetPixel form. -void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { - const StPlanarT *pd; - int16_t row; - int16_t pair; - int16_t pairs; - uint8_t *pp; - const uint8_t *rowBase; - - if (s == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) { - return; - } - pd = (const StPlanarT *)s->portData; - if (pd == NULL) { - return; - } - /* NATIVE-PERF Phase 1: group-aligned raw-span format. When x AND w - * are both 16-px multiples (the compiled save/restore region - * shape) each row is a CONTIGUOUS span of interleaved-group bytes, - * so the save is a straight per-row copy -- and, critically, the - * SAME byte format the compiled emitters read and write (backup - * format is a pure function of the recorded geometry; see - * spriteInternal.h). This branch must run BEFORE the byte-aligned - * branch: a group-aligned region previously took the byte-col asm, - * whose byte ORDER differs -- save and restore switch formats - * together, atomically, on the same predicate. */ - if (((x | (int16_t)w) & 15) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - uint16_t spanBytes = (uint16_t)(w >> 1); - const uint8_t *src = pd->base + (uint16_t)y * ST_BYTES_PER_ROW + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP; - uint8_t *dst = dstPlaneBytes; - - for (row = 0; row < (int16_t)h; row++) { - memcpy(dst, src, (size_t)spanBytes); - src += ST_BYTES_PER_ROW; - dst += spanBytes; - } - return; - } - /* Phase 10.5 fast path: byte-aligned, fully on-surface. - * Specialized 16x16 (the UBER ball-sprite size) skips the asm - * walker's per-row col-init + col-loop-check overhead. Group- - * aligned regions never reach here (raw-span above owns them); - * this serves the x % 16 == 8 byte-aligned case. */ - if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - if (w == 16u && h == 16u) { - surface68kStSprite16x16Save(pd->base, (uint16_t)x, (uint16_t)y, dstPlaneBytes); - } else { - surface68kStSpriteSaveByteAligned(pd->base, (uint16_t)x, (uint16_t)y, w, h, dstPlaneBytes); - } - return; - } - - pairs = (int16_t)(w >> 1); - rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW; - for (row = 0; row < (int16_t)h; row++) { - pp = &dstPlaneBytes[(uint16_t)row * (uint16_t)pairs]; - for (pair = 0; pair < pairs; pair++) { - int16_t px; - uint16_t group; - uint16_t bitMask; - const uint16_t *pw; - uint8_t hi; - uint8_t lo; - - px = (int16_t)(x + pair * 2); - group = (uint16_t)((uint16_t)px >> 4); - bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u))); - pw = (const uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP); - hi = 0u; - if (pw[0] & bitMask) { - hi = (uint8_t)(hi | 1u); - } - if (pw[1] & bitMask) { - hi = (uint8_t)(hi | 2u); - } - if (pw[2] & bitMask) { - hi = (uint8_t)(hi | 4u); - } - if (pw[3] & bitMask) { - hi = (uint8_t)(hi | 8u); - } - - px = (int16_t)(x + pair * 2 + 1); - group = (uint16_t)((uint16_t)px >> 4); - bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u))); - pw = (const uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP); - lo = 0u; - if (pw[0] & bitMask) { - lo = (uint8_t)(lo | 1u); - } - if (pw[1] & bitMask) { - lo = (uint8_t)(lo | 2u); - } - if (pw[2] & bitMask) { - lo = (uint8_t)(lo | 4u); - } - if (pw[3] & bitMask) { - lo = (uint8_t)(lo | 8u); - } - - pp[pair] = (uint8_t)((hi << 4) | lo); - } - rowBase += ST_BYTES_PER_ROW; - } -} - - -void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { - StPlanarT *pd; - int16_t row; - int16_t pair; - int16_t pairs; - uint8_t b; - const uint8_t *pp; - uint8_t *rowBase; - - if (s == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) { - return; - } - pd = (StPlanarT *)s->portData; - if (pd == NULL) { - return; - } - /* Group-aligned raw-span restore: MUST mirror the save-side branch - * above exactly (same predicate, same byte order) -- see the save - * comment. A raw-span backup restored through the byte-col asm (or - * vice versa) silently scrambles planes. */ - if (((x | (int16_t)w) & 15) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - uint16_t spanBytes = (uint16_t)(w >> 1); - uint8_t *dst = pd->base + (uint16_t)y * ST_BYTES_PER_ROW + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP; - const uint8_t *src = srcPlaneBytes; - - for (row = 0; row < (int16_t)h; row++) { - memcpy(dst, src, (size_t)spanBytes); - dst += ST_BYTES_PER_ROW; - src += spanBytes; - } - return; - } - /* Phase 10.5 fast path: byte-aligned, fully on-surface. - * Specialized 16x16 (UBER ball-sprite) skips walker overhead. - * Serves only the x % 16 == 8 case (raw-span above owns group- - * aligned regions). */ - if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - if (w == 16u && h == 16u) { - surface68kStSprite16x16Restore(pd->base, (uint16_t)x, (uint16_t)y, srcPlaneBytes); - } else { - surface68kStSpriteRestoreByteAligned(pd->base, (uint16_t)x, (uint16_t)y, w, h, srcPlaneBytes); - } - return; - } - - pairs = (int16_t)(w >> 1); - rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW; - for (row = 0; row < (int16_t)h; row++) { - pp = &srcPlaneBytes[(uint16_t)row * (uint16_t)pairs]; - for (pair = 0; pair < pairs; pair++) { - int16_t px; - uint16_t group; - uint16_t bitMask; - uint16_t notMask; - uint16_t *pw; - uint8_t color; - - b = pp[pair]; - - px = (int16_t)(x + pair * 2); - color = (uint8_t)(b >> 4); - group = (uint16_t)((uint16_t)px >> 4); - bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u))); - notMask = (uint16_t)~bitMask; - pw = (uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP); - if (color & 1u) { - pw[0] = (uint16_t)(pw[0] | bitMask); - } else { - pw[0] = (uint16_t)(pw[0] & notMask); - } - if (color & 2u) { - pw[1] = (uint16_t)(pw[1] | bitMask); - } else { - pw[1] = (uint16_t)(pw[1] & notMask); - } - if (color & 4u) { - pw[2] = (uint16_t)(pw[2] | bitMask); - } else { - pw[2] = (uint16_t)(pw[2] & notMask); - } - if (color & 8u) { - pw[3] = (uint16_t)(pw[3] | bitMask); - } else { - pw[3] = (uint16_t)(pw[3] & notMask); - } - - px = (int16_t)(x + pair * 2 + 1); - color = (uint8_t)(b & 0x0Fu); - group = (uint16_t)((uint16_t)px >> 4); - bitMask = (uint16_t)(1u << (15u - ((uint16_t)px & 15u))); - notMask = (uint16_t)~bitMask; - pw = (uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP); - if (color & 1u) { - pw[0] = (uint16_t)(pw[0] | bitMask); - } else { - pw[0] = (uint16_t)(pw[0] & notMask); - } - if (color & 2u) { - pw[1] = (uint16_t)(pw[1] | bitMask); - } else { - pw[1] = (uint16_t)(pw[1] & notMask); - } - if (color & 4u) { - pw[2] = (uint16_t)(pw[2] | bitMask); - } else { - pw[2] = (uint16_t)(pw[2] & notMask); - } - if (color & 8u) { - pw[3] = (uint16_t)(pw[3] | bitMask); - } else { - pw[3] = (uint16_t)(pw[3] & notMask); - } - } - rowBase += ST_BYTES_PER_ROW; - } + jlPlanarSpriteDraw(&pd->base, sp, x, y); } @@ -1796,110 +1457,18 @@ bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i } -// Phase 7: pixel reader. Pre-Phase-9 reads from the chunky shadow -// (s->pixels) since that's the source-of-truth during transition. -// Once Phase 9 sets s->pixels = NULL the planar shadow becomes -// authoritative and we walk the 4 plane bits at (x, y). -uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { - if (s->pixels != NULL) { - uint8_t byte = s->pixels[(uint16_t)y * JL_SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)]; - if (x & 1) { - return (uint8_t)(byte & 0x0Fu); - } - return (uint8_t)((byte & 0xF0u) >> 4); - } - { - StPlanarT *pd = (StPlanarT *)s->portData; - if (pd == NULL) { - return 0u; - } - return stPlanarGetPixel(pd, x, y); - } -} - - -// Derive 160 chunky bytes per row from the word-interleaved planar -// buffer (20 groups x 4 plane words). Same shape as the Amiga's -// amigaPlanesToChunkyRow but per-group instead of per-byte. Used by -// jlpSurfaceHash to fold the planar surface into the same byte stream -// the chunky ports hash, so cross-port hash comparisons stay valid. -static void stPlanarToChunkyRow(const StPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) { - uint16_t group; - uint16_t p; - uint16_t bitMask; - uint8_t pix; - const uint16_t *gp; - - for (group = 0; group < ST_GROUPS_PER_ROW; group++) { - gp = (const uint16_t *)(pd->base + (uint16_t)y * ST_BYTES_PER_ROW + group * ST_BYTES_PER_GROUP); - for (p = 0; p < 16u; p++) { - bitMask = (uint16_t)(1u << (15u - p)); - pix = 0u; - if (gp[0] & bitMask) { - pix = (uint8_t)(pix | 1u); - } - if (gp[1] & bitMask) { - pix = (uint8_t)(pix | 2u); - } - if (gp[2] & bitMask) { - pix = (uint8_t)(pix | 4u); - } - if (gp[3] & bitMask) { - pix = (uint8_t)(pix | 8u); - } - if ((p & 1u) == 0u) { - dstChunkyRow[group * 8u + (p >> 1)] = (uint8_t)(pix << 4); - } else { - dstChunkyRow[group * 8u + (p >> 1)] = (uint8_t)(dstChunkyRow[group * 8u + (p >> 1)] | pix); - } - } - } -} - - +// The shared planar hash (src/m68k/planarHash.c): plane p's words sit at +// 2 * p in each group, a group (two plane-byte columns) every 8 bytes. uint32_t jlpSurfaceHash(const jlSurfaceT *s) { - StPlanarT *pd; - uint16_t lo = 0xACE1u; - uint16_t hi = 0x1357u; - uint16_t n; - uint16_t v; - int16_t row; - uint16_t col; - uint8_t b; - uint8_t chunkyRow[JL_SURFACE_BYTES_PER_ROW]; - const uint16_t *w; + const StPlanarT *pd; + const uint8_t *planes[ST_PLANES]; + uint16_t p; - pd = (StPlanarT *)s->portData; - if (pd == NULL) { - return 0u; + pd = (const StPlanarT *)s->portData; + for (p = 0u; p < ST_PLANES; p++) { + planes[p] = pd->base + p * sizeof(uint16_t); } - /* Pixel hash: derive equivalent chunky bytes from the planar - * shadow row by row, fold them into the same SURFACE_HASH_MIX_BYTE - * the chunky ports use so cross-port hash comparisons stay valid. */ - for (row = 0; row < JL_SURFACE_HEIGHT; row++) { - stPlanarToChunkyRow(pd, row, chunkyRow); - for (col = 0; col < JL_SURFACE_BYTES_PER_ROW; col++) { - b = chunkyRow[col]; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - } - // SCB + palette mix unchanged from chunky days. - { - const uint8_t *sp = s->scb; - for (n = 0; n < (uint16_t)JL_SURFACE_HEIGHT; n++) { - b = *sp++; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - } - w = &s->palette[0][0]; - for (n = 0; n < (uint16_t)JL_SURFACE_PALETTE_ENTRIES; n++) { - v = *w++; - b = (uint8_t)((v >> 8) & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - b = (uint8_t)(v & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - return ((uint32_t)hi << 16) | (uint32_t)lo; + return jlPlanarSurfaceHash(s, planes, ST_BYTES_PER_ROW, ST_BYTES_PER_GROUP); } @@ -1934,7 +1503,7 @@ uint8_t *jlpSurfaceAllocPixels(void) { } -// Phase 9: stage has no chunky shadow either. Cross-platform stageAlloc +// Phase 9: stage has no chunky shadow either. Cross-platform jlStageAlloc // stores NULL in s->pixels and skips the chunky memset. uint8_t *jlpStageAllocPixels(void) { return NULL; diff --git a/src/atarist/input.c b/src/atarist/input.c index 3c6cf75..06b76f0 100644 --- a/src/atarist/input.c +++ b/src/atarist/input.c @@ -19,9 +19,9 @@ // // The ISR writes into private gIsrState (keyboard) and gIsrMouse* // buffers; jlpInputPoll copies them into the public globals. -// jlKeyPressed edge detection requires that public gKeyState only +// jlKeyPressed edge detection requires that public jlgKeyState only // advance during jlpInputPoll, never at interrupt time -- jlInputPoll -// snapshots gKeyState into gKeyPrev before jlpInputPoll runs. +// snapshots jlgKeyState into jlgKeyPrev before jlpInputPoll runs. // // Typed characters: replacing ikbdsys means TOS's own keyboard // processing never runs, so Bconin never sees our keys. Instead the @@ -160,7 +160,7 @@ static void altLayerFind(const KeyTabT *keyTab); static void charRepeat(void); static long ikbdHandler(void); #ifdef JOEYLIB_ATARI_STE -static void padToStick(const StePadT *pad, jlJoystickE js); +static inline void padToStick(uint16_t held, uint16_t buttonA, uint16_t buttonB, jlJoystickE js); #endif static long patchIkbdVector(void); #ifdef JOEYLIB_ATARI_STE @@ -247,17 +247,23 @@ static volatile uint8_t gPacketRemaining = 0; static volatile uint8_t gPacketKind = PKT_KIND_NONE; static volatile uint8_t gMousePacketByte = 0; // bytes consumed in current packet static bool gHooked = false; -// uint8_t (not bool) so element size matches gKeyState's. See +// uint8_t (not bool) so element size matches jlgKeyState's. See // src/core/inputInternal.h for the full rationale. static volatile uint8_t gIsrState[JL_KEY_COUNT]; // Each key's presses, counted by the ISR (which alone writes them), and the // count the last poll saw: a key whose count moved was pressed since, so a -// tap between two polls still reads down for one (inputLatch's meaning). +// tap between two polls still reads down for one (jlInputLatch's meaning). // The poll writes nothing the ISR does, so it needs no interrupts held off: // held off across the keys, the plain ST's 12288 Hz PWM pump lost five // samples a poll, and digital sound played 1.4% flat. static volatile uint8_t gIsrTaps[JL_KEY_COUNT]; static uint8_t gTapsSeen[JL_KEY_COUNT]; +// Key events the ISR has taken (after it updated the key), and the count the +// last poll scanned the keys at: a poll scans them only when one came since, +// or when the last scan showed a tap that must now go up. +static volatile uint16_t gIsrKeyEvents; +static uint16_t gKeyEventsSeen; +static bool gKeyTapShown; // Mouse delta accumulator. Each ACIA mouse packet adds dx/dy here; the // poll routine clamps the running absolute position into the surface @@ -309,9 +315,26 @@ static uint32_t gRepeatDelayMs = 0u; static uint32_t gRepeatRateMs = 0u; #ifdef JOEYLIB_ATARI_STE -// Whether JL_JOYSTICK_0 is the Jaguar pad in enhanced port A, used more -// recently than port 1's stick. +// Whether the machine has the enhanced ports (jlSteMachineHasPads, kept by +// jlpInputInit), and whether JL_JOYSTICK_0 is the Jaguar pad in port A, +// used more recently than port 1's stick. +static bool gStePads = false; static bool gPadAIsStick0 = false; + +// A pad's four direction bits (STE_PAD_UP, DOWN, LEFT, RIGHT) as its axes. +// Both directions of an axis read as the positive one, as on the IKBD ports. +static const int8_t kPadAxisX[STE_PAD_DIR_MASK + 1u] = { + 0, 0, 0, 0, + JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MIN, + JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, + JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX +}; +static const int8_t kPadAxisY[STE_PAD_DIR_MASK + 1u] = { + 0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, + 0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, + 0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, + 0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX +}; #endif // ----- Internal helpers ----- @@ -343,7 +366,7 @@ static void altLayerFind(const KeyTabT *keyTab) { } else if (os->os_version >= TOS_ALT_LAYER_VERSION) { inTables = true; } else { - (void)stAltLayerFind((uint8_t)(os->os_conf >> TOS_CONF_COUNTRY_SHIFT), &gKeyTabAltUnshift, &gKeyTabAltShift, &gKeyTabAltCaps); + (void)jlStAltLayerFind((uint8_t)(os->os_conf >> TOS_CONF_COUNTRY_SHIFT), &gKeyTabAltUnshift, &gKeyTabAltShift, &gKeyTabAltCaps); return; } if (inTables) { @@ -355,11 +378,16 @@ static void altLayerFind(const KeyTabT *keyTab) { // The held key's character again, once TOS's delay has passed since it went -// down and then at TOS's rate (the ISR names the key, gIsrRepeatCode). +// down and then at TOS's rate (the ISR names the key, gIsrRepeatCode). No +// key held, the usual poll, reads no clock: a key the ISR takes just after +// the check is the next poll's. static void charRepeat(void) { uint32_t now; uint8_t n; + if (gIsrRepeatNew == 0u && gIsrRepeatCode == 0u) { + return; + } now = jlpMillisElapsed(); if (gIsrRepeatNew != 0u) { gIsrRepeatNew = 0u; @@ -480,6 +508,7 @@ static long ikbdHandler(void) { if (!isBreak) { gIsrTaps[key]++; } + gIsrKeyEvents++; } if (isBreak && code == gIsrRepeatCode) { @@ -520,14 +549,14 @@ static long ikbdHandler(void) { #ifdef JOEYLIB_ATARI_STE -// A Jaguar pad as a stick: its directions, and its A and B as the two -// buttons. -static void padToStick(const StePadT *pad, jlJoystickE js) { - gJoyAxisX[js] = pad->axisX; - gJoyAxisY[js] = pad->axisY; - gJoyButtonState[js][JL_JOY_BUTTON_0] = pad->buttonA; - gJoyButtonState[js][JL_JOY_BUTTON_1] = pad->buttonB; - gJoyOneButton[js] = false; +// A Jaguar pad as a stick: its held direction bits, and its A and B (any +// nonzero is down) as the two buttons. +static inline void padToStick(uint16_t held, uint16_t buttonA, uint16_t buttonB, jlJoystickE js) { + jlgJoyAxisX[js] = kPadAxisX[held]; + jlgJoyAxisY[js] = kPadAxisY[held]; + jlgJoyButtonState[js][JL_JOY_BUTTON_0] = buttonA != 0u; + jlgJoyButtonState[js][JL_JOY_BUTTON_1] = buttonB != 0u; + jlgJoyOneButton[js] = false; } #endif @@ -545,22 +574,23 @@ static long patchIkbdVector(void) { // last, and a pad in port B is JL_JOYSTICK_1 (port 0, the mouse's, gives // no stick in TOS's IKBD mode). static void readStePads(void) { - StePadT padA; - StePadT padB; + StePadsT pads; + uint16_t heldA; - if (!steMachineHasPads()) { + if (!gStePads) { return; } - stePadsRead(&padA, &padB); - if (padA.used) { + stePadsRead(&pads); + heldA = (uint16_t)((pads.directions >> STE_PAD_A_SHIFT) & STE_PAD_DIR_MASK); + if (heldA != 0u || ((pads.row0 | pads.row1) & STE_PAD_A_FIRE) != 0u) { gPadAIsStick0 = true; - } else if (gIsrJoyByte[ST_PORT1_AS_JS] != 0u || gJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] != 0u) { + } else if (gIsrJoyByte[ST_PORT1_AS_JS] != 0u || jlgJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] != 0u) { gPadAIsStick0 = false; } if (gPadAIsStick0) { - padToStick(&padA, ST_PORT1_AS_JS); + padToStick(heldA, (uint16_t)(pads.row0 & STE_PAD_A_FIRE), (uint16_t)(pads.row1 & STE_PAD_A_FIRE), ST_PORT1_AS_JS); } - padToStick(&padB, ST_PORT0_AS_JS); + padToStick((uint16_t)((pads.directions >> STE_PAD_B_SHIFT) & STE_PAD_DIR_MASK), (uint16_t)(pads.row0 & STE_PAD_B_FIRE), (uint16_t)(pads.row1 & STE_PAD_B_FIRE), ST_PORT0_AS_JS); } #endif @@ -588,13 +618,19 @@ void jlpInputInit(void) { KeyTabT *keyTab; uint32_t kbrate; - gMouseAttached = true; // the IKBD mouse line is always wired on an ST - memset(gKeyState, 0, sizeof(gKeyState)); - memset(gKeyPrev, 0, sizeof(gKeyPrev)); + jlgMouseAttached = true; // the IKBD mouse line is always wired on an ST + memset(jlgKeyState, 0, sizeof(jlgKeyState)); + memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrTaps, 0, sizeof(gIsrTaps)); memset(gTapsSeen, 0, sizeof(gTapsSeen)); memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte)); + gKeyEventsSeen = gIsrKeyEvents; + gKeyTapShown = false; +#ifdef JOEYLIB_ATARI_STE + gStePads = jlSteMachineHasPads(); + gPadAIsStick0 = false; +#endif // Cache the TOS keyboard translation tables (the OS keymap) for // the ISR's typed-character path. -1 leaves the tables unchanged @@ -624,16 +660,16 @@ void jlpInputInit(void) { gMouseAbsX = JL_SURFACE_WIDTH / 2; gMouseAbsY = JL_SURFACE_HEIGHT / 2; - gMouseX = gMouseAbsX; - gMouseY = gMouseAbsY; + jlgMouseX = gMouseAbsX; + jlgMouseY = gMouseAbsY; gIsrMouseDx = 0; gIsrMouseDy = 0; gIsrMouseButtons = 0; // Both ports always announce themselves on the ST -- packets // arrive even when no stick is plugged in (state stays at zero). - gJoyConnected[JL_JOYSTICK_0] = true; - gJoyConnected[JL_JOYSTICK_1] = true; + jlgJoyConnected[JL_JOYSTICK_0] = true; + jlgJoyConnected[JL_JOYSTICK_1] = true; gKbdvecs = (_KBDVECS *)Kbdvbase(); gPacketRemaining = 0; @@ -655,14 +691,26 @@ void jlpInputPoll(void) { uint8_t btn; uint8_t joy; uint8_t tail; + uint8_t held; uint8_t taps; + uint8_t tapOnly; + uint16_t events; uint16_t i; - // Down when held, or pressed since the last poll (gIsrTaps). - for (i = 0u; i < JL_KEY_COUNT; i++) { - taps = gIsrTaps[i]; - gKeyState[i] = (uint8_t)(gIsrState[i] | (taps != gTapsSeen[i])); - gTapsSeen[i] = taps; + // Down when held, or pressed since the last poll (gIsrTaps). The count + // is read first: an event the scan misses moves it for the next poll. + events = gIsrKeyEvents; + if (events != gKeyEventsSeen || gKeyTapShown) { + gKeyEventsSeen = events; + tapOnly = 0u; + for (i = 0u; i < JL_KEY_COUNT; i++) { + held = gIsrState[i]; + taps = gIsrTaps[i]; + jlgKeyState[i] = (uint8_t)(held | (taps != gTapsSeen[i])); + tapOnly = (uint8_t)(tapOnly | (jlgKeyState[i] & (uint8_t)(held == 0u))); + gTapsSeen[i] = taps; + } + gKeyTapShown = tapOnly != 0u; } // Drain ISR-translated characters into the core queue. Single- @@ -700,39 +748,39 @@ void jlpInputPoll(void) { gMouseAbsX = (int16_t)newX; gMouseAbsY = (int16_t)newY; - gMouseX = gMouseAbsX; - gMouseY = gMouseAbsY; + jlgMouseX = gMouseAbsX; + jlgMouseY = gMouseAbsY; - gMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (btn & MOUSE_HDR_LEFT_BTN) != 0; - gMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = (btn & MOUSE_HDR_RIGHT_BTN) != 0; - gMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false; + jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (btn & MOUSE_HDR_LEFT_BTN) != 0; + jlgMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = (btn & MOUSE_HDR_RIGHT_BTN) != 0; + jlgMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false; // Decode latest joystick byte for each stick. Direction is digital // on the ST, so axes snap to the JOYSTICK_AXIS_* extremes. for (i = 0; i < JL_JOYSTICK_COUNT; i++) { - joy = gIsrJoyByte[i]; - gJoyAxisX[i] = 0; - gJoyAxisY[i] = 0; + joy = gIsrJoyByte[i]; + jlgJoyAxisX[i] = 0; + jlgJoyAxisY[i] = 0; if (joy & JOY_BIT_LEFT) { - gJoyAxisX[i] = JL_JOYSTICK_AXIS_MIN; + jlgJoyAxisX[i] = JL_JOYSTICK_AXIS_MIN; } if (joy & JOY_BIT_RIGHT) { - gJoyAxisX[i] = JL_JOYSTICK_AXIS_MAX; + jlgJoyAxisX[i] = JL_JOYSTICK_AXIS_MAX; } if (joy & JOY_BIT_UP) { - gJoyAxisY[i] = JL_JOYSTICK_AXIS_MIN; + jlgJoyAxisY[i] = JL_JOYSTICK_AXIS_MIN; } if (joy & JOY_BIT_DOWN) { - gJoyAxisY[i] = JL_JOYSTICK_AXIS_MAX; + jlgJoyAxisY[i] = JL_JOYSTICK_AXIS_MAX; } - gJoyButtonState[i][JL_JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0; - gJoyOneButton[i] = true; // one fire button: a hold is button 1 + jlgJoyButtonState[i][JL_JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0; + jlgJoyOneButton[i] = true; // one fire button: a hold is button 1 } // Port 1's fire and the mouse's right button are one IKBD line: while // the IKBD reports the mouse (TOS's mode, kept here) a press on it // arrives as the right button, never in a joystick packet. if ((btn & MOUSE_HDR_RIGHT_BTN) != 0) { - gJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] = true; + jlgJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] = true; } #ifdef JOEYLIB_ATARI_STE readStePads(); diff --git a/src/atarist/lineSpan.s b/src/atarist/lineSpan.s index ba14dc9..c83205e 100644 --- a/src/atarist/lineSpan.s +++ b/src/atarist/lineSpan.s @@ -14,7 +14,7 @@ | | ABI: cdecl. d2-d7/a2-a6 callee-save. | -| void surface68kStDrawLine(uint8_t *base, +| void jlSurface68kStDrawLine(uint8_t *base, | int16_t x0, int16_t y0, | int16_t x1, int16_t y1, | uint8_t color); @@ -28,7 +28,7 @@ | d7 = sx (long; moveq #1 or #-1, low word used for .w add) | a4 = sy (long; sign-extended) | a3 = base -| a5 = _gStBitMaskWordLut +| a5 = _jlgStBitMaskWordLut | a2 = scratch (per-pixel: base + byteOff) | d0,d1 = scratch | @@ -50,7 +50,7 @@ | ---- DL_PLOT: 4-plane word RMW for hardcoded color ---- -| Inputs: d2.w = x, d3.w = y, a3 = base, a5 = _gStBitMaskWordLut +| Inputs: d2.w = x, d3.w = y, a3 = base, a5 = _jlgStBitMaskWordLut | Trashes: d0, d1, a2 .macro DL_PLOT color @@ -123,17 +123,17 @@ .endm - .globl _surface68kStDrawLine + .globl _jlSurface68kStDrawLine -_surface68kStDrawLine: +_jlSurface68kStDrawLine: movem.l %d2-%d7/%a2-%a6,-(%sp) lea -SP_LOCAL(%sp),%sp | Load base & luts. move.l SP_BASE(%sp),%a3 - lea _gStBitMaskWordLut(%pc),%a5 + lea _jlgStBitMaskWordLut(%pc),%a5 | a6 = yLut base (yp -> yp*160) for use in DL_PLOT. - lea _gStRowOffsetLut(%pc),%a6 + lea _jlgStRowOffsetLut(%pc),%a6 | x = x0, y = y0 move.w SP_X0(%sp),%d2 @@ -228,25 +228,25 @@ _surface68kStDrawLine: .align 2 -| 16 word entries: _gStBitMaskWordLut[i] = 1 << (15 - i), for i in -| 0..15. Exported (like _gStRowOffsetLut) so circle.s can reference +| 16 word entries: _jlgStBitMaskWordLut[i] = 1 << (15 - i), for i in +| 0..15. Exported (like _jlgStRowOffsetLut) so circle.s can reference | it via absolute addressing -- single source of truth for the per- | pixel bit mask shared by jlDrawLine and jlDrawCircle. - .globl _gStBitMaskWordLut -_gStBitMaskWordLut: + .globl _jlgStBitMaskWordLut +_jlgStBitMaskWordLut: .word 0x8000, 0x4000, 0x2000, 0x1000 .word 0x0800, 0x0400, 0x0200, 0x0100 .word 0x0080, 0x0040, 0x0020, 0x0010 .word 0x0008, 0x0004, 0x0002, 0x0001 -| ---- surface68kStFillSpan --------------------------------------- +| ---- jlSurface68kStFillSpan --------------------------------------- | | Single-row span fill: leading-mask group + middle long-fills + | trailing-mask group, all in one frame. Caller pre-clips so the | span is fully on-surface. | -| void surface68kStFillSpan(uint8_t *base, +| void jlSurface68kStFillSpan(uint8_t *base, | int16_t left, int16_t right, | int16_t y, uint8_t color); | @@ -272,9 +272,9 @@ _gStBitMaskWordLut: .equ SP_FS_COLOR, SP_FS_OFF + 16 + 3 - .globl _surface68kStFillSpan + .globl _jlSurface68kStFillSpan -_surface68kStFillSpan: +_jlSurface68kStFillSpan: movem.l %d2-%d7/%a2-%a6,-(%sp) move.l SP_FS_BASE(%sp),%a3 @@ -421,99 +421,79 @@ _surface68kStFillSpan: rts -| ---- surface68kStFillRectSingleGroup ----------------------------- +| ---- jlStFillRectGroupRegs ----------------------------------------- | -| Fill rect when groupFirst == groupLast (thin/single-column rect). -| Caller pre-computes firstGroupPtr = base + y*160 + groupFirst*8 -| and the mask = leftMask & rightMask. +| Fill rect when groupFirst == groupLast (thin/single-column rect), for +| jlStFillRectBase below. Register arguments and only the cdecl scratch +| registers, so the dispatch jumps here and the rts returns to its caller: +| a small rect -- JoeyAGI fills its picture a run at a time -- pays no +| cdecl frame and no movem pair. | -| void surface68kStFillRectSingleGroup(uint8_t *firstGroupPtr, -| uint16_t mask, -| uint16_t h, -| uint8_t color); +| a0 = the rect's first group (base + y*160 + groupFirst*8) +| a1 = a0 + h * 160, where the rows end +| d0.w = the mask (leftMask & rightMask) +| d1.w = color (0..15) +| Clobbers d0, d1, a0. | -| Dispatched on color (low nibble) -> 16 specialized loops with -| hardcoded OR/AND per plane. Inner loop is 4 plane word RMWs + -| advance row + branch. -| -| jlDrawLine V routes to jlFillRect 1xH which lands here. - - .equ SP_FRG_SAVED, 24 | d2-d5/a2-a3 = 6 longs - .equ SP_FRG_OFF, (SP_FRG_SAVED + 4) - .equ SP_FRG_PTR, SP_FRG_OFF + 0 - .equ SP_FRG_MASK, SP_FRG_OFF + 4 + 2 - .equ SP_FRG_H, SP_FRG_OFF + 8 + 2 - .equ SP_FRG_COLOR, SP_FRG_OFF + 12 + 3 - +| Dispatched on color -> 16 specialized loops with hardcoded OR/AND per +| plane: 4 plane word RMWs, the next row, and a compare against a1. .macro FRG_LOOP color +.Lfrg_entry_\color: + move.w %d0,%d1 + not.w %d1 | d1 = ~mask .Lfrg_loop_\color: .if ((\color) & 1) - or.w %d3,(%a3)+ + or.w %d0,(%a0)+ .else - and.w %d4,(%a3)+ + and.w %d1,(%a0)+ .endif .if ((\color) & 2) - or.w %d3,(%a3)+ + or.w %d0,(%a0)+ .else - and.w %d4,(%a3)+ + and.w %d1,(%a0)+ .endif .if ((\color) & 4) - or.w %d3,(%a3)+ + or.w %d0,(%a0)+ .else - and.w %d4,(%a3)+ + and.w %d1,(%a0)+ .endif .if ((\color) & 8) - or.w %d3,(%a3)+ + or.w %d0,(%a0)+ .else - and.w %d4,(%a3)+ + and.w %d1,(%a0)+ .endif - lea 152(%a3),%a3 | a3 now at row start; advance to next row (160-8) - subq.w #1,%d5 - bne.w .Lfrg_loop_\color - bra.w .Lfrg_done + lea 152(%a0),%a0 | a0 now at row start; advance to next row (160-8) + cmpa.l %a1,%a0 + bne.s .Lfrg_loop_\color + rts .endm - .globl _surface68kStFillRectSingleGroup + .globl _jlStFillRectGroupRegs -_surface68kStFillRectSingleGroup: - movem.l %d2-%d5/%a2-%a3,-(%sp) - - move.l SP_FRG_PTR(%sp),%a3 - move.w SP_FRG_MASK(%sp),%d3 - move.w SP_FRG_H(%sp),%d5 - tst.w %d5 - beq.w .Lfrg_done - move.w %d3,%d4 - not.w %d4 | d4 = notMask - - | Color dispatch - moveq #0,%d2 - move.b SP_FRG_COLOR(%sp),%d2 - and.w #0x0F,%d2 - add.w %d2,%d2 - add.w %d2,%d2 | * 4 for bra.w table - lea .Lfrg_table(%pc),%a2 - jmp 0(%a2,%d2.w) +_jlStFillRectGroupRegs: + add.w %d1,%d1 + add.w %d1,%d1 | * 4 for bra.w table + jmp .Lfrg_table(%pc,%d1.w) .Lfrg_table: - bra.w .Lfrg_loop_0 - bra.w .Lfrg_loop_1 - bra.w .Lfrg_loop_2 - bra.w .Lfrg_loop_3 - bra.w .Lfrg_loop_4 - bra.w .Lfrg_loop_5 - bra.w .Lfrg_loop_6 - bra.w .Lfrg_loop_7 - bra.w .Lfrg_loop_8 - bra.w .Lfrg_loop_9 - bra.w .Lfrg_loop_10 - bra.w .Lfrg_loop_11 - bra.w .Lfrg_loop_12 - bra.w .Lfrg_loop_13 - bra.w .Lfrg_loop_14 - bra.w .Lfrg_loop_15 + bra.w .Lfrg_entry_0 + bra.w .Lfrg_entry_1 + bra.w .Lfrg_entry_2 + bra.w .Lfrg_entry_3 + bra.w .Lfrg_entry_4 + bra.w .Lfrg_entry_5 + bra.w .Lfrg_entry_6 + bra.w .Lfrg_entry_7 + bra.w .Lfrg_entry_8 + bra.w .Lfrg_entry_9 + bra.w .Lfrg_entry_10 + bra.w .Lfrg_entry_11 + bra.w .Lfrg_entry_12 + bra.w .Lfrg_entry_13 + bra.w .Lfrg_entry_14 + bra.w .Lfrg_entry_15 FRG_LOOP 0 FRG_LOOP 1 @@ -532,18 +512,14 @@ _surface68kStFillRectSingleGroup: FRG_LOOP 14 FRG_LOOP 15 -.Lfrg_done: - movem.l (%sp)+,%d2-%d5/%a2-%a3 - rts - -| ---- surface68kStTileFill8x8 --------------------------------------- +| ---- jlSurface68kStTileFill8x8 --------------------------------------- | | Specialized 8x8 single-group fill: 16-way color dispatch + 8 rows | fully unrolled. Drops the per-row subq+bne overhead that the | generic FRG_LOOP pays. Used by jlpTileFill. | -| void surface68kStTileFill8x8(uint8_t *firstGroupPtr, +| void jlSurface68kStTileFill8x8(uint8_t *firstGroupPtr, | uint16_t mask, | uint8_t color); | @@ -601,9 +577,9 @@ _surface68kStFillRectSingleGroup: .endm - .globl _surface68kStTileFill8x8 + .globl _jlSurface68kStTileFill8x8 -_surface68kStTileFill8x8: +_jlSurface68kStTileFill8x8: movem.l %d3-%d4/%a2-%a3,-(%sp) move.l SP_TF_PTR(%sp),%a3 @@ -660,12 +636,12 @@ _surface68kStTileFill8x8: rts -| ---- surface68kStFillRectMulti ------------------------------------- +| ---- jlSurface68kStFillRectMulti ------------------------------------- | | Multi-group jlFillRect: groupFirst != groupLast. Caller pre-clips. | Dispatched on color (low nibble) -> 16 specialized H-row loops. | -| void surface68kStFillRectMulti(uint8_t *base, +| void jlSurface68kStFillRectMulti(uint8_t *base, | int16_t x, int16_t y, | uint16_t w, uint16_t h, | uint8_t color); @@ -763,9 +739,9 @@ _surface68kStTileFill8x8: .endm - .globl _surface68kStFillRectMulti + .globl _jlSurface68kStFillRectMulti -_surface68kStFillRectMulti: +_jlSurface68kStFillRectMulti: movem.l %d2-%d7/%a2-%a6,-(%sp) lea -SP_FRM_LOCAL(%sp),%sp @@ -803,7 +779,7 @@ _surface68kStFillRectMulti: move.w %d0,%d2 and.w #15,%d2 add.w %d2,%d2 - lea _gStLeftMaskLut(%pc),%a2 + lea _jlgStLeftMaskLut(%pc),%a2 move.w (%a2,%d2.w),%d2 | d2 = leftMask move.w %d2,%d4 not.w %d4 | d4 = notLeftMask @@ -812,7 +788,7 @@ _surface68kStFillRectMulti: move.w %d1,%d3 and.w #15,%d3 add.w %d3,%d3 - lea _gStRightMaskLut(%pc),%a2 + lea _jlgStRightMaskLut(%pc),%a2 move.w (%a2,%d3.w),%d3 | d3 = rightMask move.w %d3,%d5 not.w %d5 | d5 = notRightMask @@ -898,20 +874,20 @@ _surface68kStFillRectMulti: .align 2 -| Leading/trailing edge-mask LUTs, exported (like _gStRowOffsetLut) +| Leading/trailing edge-mask LUTs, exported (like _jlgStRowOffsetLut) | as the single source of truth shared with jlFillCircle.s via | absolute addressing. -| _gStLeftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst -| _gStRightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast - .globl _gStLeftMaskLut -_gStLeftMaskLut: +| _jlgStLeftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst +| _jlgStRightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast + .globl _jlgStLeftMaskLut +_jlgStLeftMaskLut: .word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF .word 0x0FFF, 0x07FF, 0x03FF, 0x01FF .word 0x00FF, 0x007F, 0x003F, 0x001F .word 0x000F, 0x0007, 0x0003, 0x0001 - .globl _gStRightMaskLut -_gStRightMaskLut: + .globl _jlgStRightMaskLut +_jlgStRightMaskLut: .word 0x8000, 0xC000, 0xE000, 0xF000 .word 0xF800, 0xFC00, 0xFE00, 0xFF00 .word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0 @@ -932,8 +908,8 @@ _gStRightMaskLut: | Replaces a 44-cyc lsl.w #5 + lsl.w #7 + add.w shift chain with | a 14-cyc indexed-word load. Exported so circle.s and jlFillCircle.s | can reference it via absolute addressing without duplication. - .globl _gStRowOffsetLut -_gStRowOffsetLut: + .globl _jlgStRowOffsetLut +_jlgStRowOffsetLut: .set li_y, 0 .rept 200 .word li_y * ST_BYTES_PER_ROW @@ -941,7 +917,7 @@ _gStRowOffsetLut: .endr -| ---- surface68kStLongFill ---------------------------------------- +| ---- jlSurface68kStLongFill ---------------------------------------- | | Bulk long-fill helper for full-row fills (jlSurfaceClear, jlFillRect | 320x200). Writes numGroups groups of 8 bytes (loLong, hiLong) @@ -951,7 +927,7 @@ _gStRowOffsetLut: | a1 is caller-saved scratch (free); a2-a6 and d2-d7 are callee-saved | and restored on exit. | -| void surface68kStLongFill(uint8_t *dst, +| void jlSurface68kStLongFill(uint8_t *dst, | uint16_t numGroups, | uint32_t loLong, | uint32_t hiLong); @@ -968,9 +944,9 @@ _gStRowOffsetLut: .equ SP_LF_HI, SP_LF_OFF + 12 - .globl _surface68kStLongFill + .globl _jlSurface68kStLongFill -_surface68kStLongFill: +_jlSurface68kStLongFill: movem.l %d2-%d7/%a2-%a6,-(%sp) move.l SP_LF_DST(%sp),%a0 @@ -1019,3 +995,250 @@ _surface68kStLongFill: .Llf_done: movem.l (%sp)+,%d2-%d7/%a2-%a6 rts + + +| ---- jlFillRect / jlStFillRectMark / jlStFillRectBase ------------- +| +| jlFillRect itself on the ST (JL_HAS_FILL_RECT_ENTRY). gcc 15 spent +| several times the fill's own cycles in the C wrapper (argument copies, +| 32-bit clip math, a call into the mark): JoeyAGI's ST picture fills were +| a third of its time. An on-surface rect is marked and filled here; +| anything else, a NULL surface aside, goes to draw.c's jlFillRectClip with +| the arguments as they came. +| +| void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, +| uint16_t h, uint8_t colorIndex); +| void jlStFillRectMark(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, +| uint16_t h, uint8_t colorIndex); +| void jlStFillRectBase(uint8_t *base, int16_t x, int16_t y, uint16_t w, +| uint16_t h, uint8_t colorIndex); +| +| jlStFillRectMark (stTile.h's jlpFillRect, its mark fused) and +| jlStFillRectBase (jlpFillRectNoMark) take a rect already on the surface. +| All three save d2-d5 and read the arguments once into registers, then +| share one flow: the stage's dirty bands widened (rows y .. y + h - 1 to +| words x / 4 .. (x + w - 1) / 4), the planes' base found, and a fill picked +| and jumped to once the registers are restored, so its rts returns to the +| caller: the multi-group fill and the STe's BLiTTER fill take the argument +| slots with the surface swapped for its base, the whole-row long fill its +| own four written over them, the one-group fill registers. +| +| Registers in the flow: +| d0 = x d1 = y d2 = w d3 = h d4-d5 = scratch +| a0 = the surface, then its planes' base a1 = scratch + + .equ FR_S, 4 | the surface, then its base + .equ FR_X, 8 + 2 + .equ FR_Y, 12 + 2 + .equ FR_W, 16 + 2 + .equ FR_H, 20 + 2 + .equ FR_COLOR, 24 + 3 + .equ LF_DST, 4 | jlSurface68kStLongFill's + .equ LF_NGROUPS, 8 + 2 + .equ LF_LO, 12 + .equ LF_HI, 16 + .equ FR_SAVED, 16 | d2-d5, saved for the flow + .equ FRR_S, FR_SAVED + FR_S + .equ FRR_X, FR_SAVED + FR_X + .equ FRR_Y, FR_SAVED + FR_Y + .equ FRR_W, FR_SAVED + FR_W + .equ FRR_H, FR_SAVED + FR_H + .equ FRR_COLOR, FR_SAVED + FR_COLOR + .equ SURFACE_PORT_DATA, 4 | offsetof(jlSurfaceT, portData); StPlanarT's base is at 0 (proofs in hal.c) + .equ ST_SURFACE_WIDTH, 320 + .equ ST_SURFACE_HEIGHT, 200 + .equ ST_GROUP_PIXELS, 16 + .equ ST_BYTES_PER_GROUP, 8 | a group's four plane words + .equ ST_GROUPS_PER_ROW, 20 + .equ ST_PAIR_MASK, 3 | a long's two planes of the colour + .equ ST_HI_PAIR_MASK, 12 | the high pair, already a long's offset + .equ ST_RIGHT_LUT, _jlgStRightMaskLut - _jlgStLeftMaskLut +| Where the STe's BLiTTER beats the CPU fills (measured 2026-10-06; hal.c +| has the copy's): a fill over two or more 16-px groups from 8 rows, one +| group from 24. + .equ ST_BLT_FILL_MIN_ROWS, 8 + .equ ST_BLT_FILL_MIN_ROWS_ONE_GROUP, 24 + + .globl _jlFillRect + .globl _jlStFillRectMark + .globl _jlStFillRectBase + +| The arguments into the flow's registers (a0 the first one). + .macro FR_LOAD + movea.l FRR_S(%sp),%a0 + move.w FRR_X(%sp),%d0 + move.w FRR_Y(%sp),%d1 + move.w FRR_W(%sp),%d2 + move.w FRR_H(%sp),%d3 + .endm + +_jlFillRect: + movem.l %d2-%d5,-(%sp) + FR_LOAD + move.l %a0,%d4 + beq.s .Lfr_pop | NULL: nothing + | On the surface with w and h positive, else the C clip. An + | unsigned x + w (a negative x is a big one) past the width, + | or carried out, is off it; so for y + h. + tst.w %d2 + beq.s .Lfr_pop + move.w %d0,%d4 + add.w %d2,%d4 + bcs.s .Lfr_clip + cmpi.w #ST_SURFACE_WIDTH,%d4 + bhi.s .Lfr_clip + tst.w %d3 + beq.s .Lfr_pop + move.w %d1,%d4 + add.w %d3,%d4 + bcs.s .Lfr_clip + cmpi.w #ST_SURFACE_HEIGHT,%d4 + bls.s .Lfr_mark +.Lfr_clip: + movem.l (%sp)+,%d2-%d5 + jmp _jlFillRectClip +.Lfr_pop: + movem.l (%sp)+,%d2-%d5 + rts + +_jlStFillRectMark: + movem.l %d2-%d5,-(%sp) + FR_LOAD +.Lfr_mark: + cmpa.l _jlgStage,%a0 + bne.s .Lfr_base + move.w %d0,%d4 + lsr.w #2,%d4 | minWord + move.w %d0,%d5 + add.w %d2,%d5 + subq.w #1,%d5 + lsr.w #2,%d5 | maxWord + lea _jlgStageMinWord,%a1 + adda.w %d1,%a1 + cmpi.w #1,%d3 + bne.s .Lfr_markRows + cmp.b (%a1),%d4 + bcc.s 1f + move.b %d4,(%a1) +1: + lea _jlgStageMaxWord,%a1 + adda.w %d1,%a1 + cmp.b (%a1),%d5 + bls.s .Lfr_base + move.b %d5,(%a1) + bra.s .Lfr_base +.Lfr_markRows: + | jlStageBandsMarkRegs keeps d2-d7 and takes a0 / a1 (the + | rows), d0 (their count) and d1 (maxWord : minWord); x, y + | and the surface come back from the arguments after. + movea.l %a1,%a0 + lea _jlgStageMaxWord,%a1 + adda.w %d1,%a1 + move.w %d5,%d1 + lsl.w #8,%d1 + move.b %d4,%d1 + move.w %d3,%d0 + jsr _jlStageBandsMarkRegs + movea.l FRR_S(%sp),%a0 + move.w FRR_X(%sp),%d0 + move.w FRR_Y(%sp),%d1 +.Lfr_base: + movea.l SURFACE_PORT_DATA(%a0),%a0 + movea.l (%a0),%a0 | the planes' base + bra.s .Lfrb_flow + +_jlStFillRectBase: + movem.l %d2-%d5,-(%sp) + FR_LOAD +.Lfrb_flow: +.ifdef JOEYLIB_ATARI_STE + cmpi.w #ST_BLT_FILL_MIN_ROWS_ONE_GROUP,%d3 + bcc.s .Lfrb_blit + cmpi.w #ST_BLT_FILL_MIN_ROWS,%d3 + bcs.s .Lfrb_cpu + moveq #ST_GROUP_PIXELS - 1,%d4 + and.w %d0,%d4 + add.w %d2,%d4 + cmpi.w #ST_GROUP_PIXELS,%d4 + bls.s .Lfrb_cpu +.Lfrb_blit: + move.l %a0,FRR_S(%sp) + movem.l (%sp)+,%d2-%d5 + jmp _jlStBlitFillRect +.Lfrb_cpu: +.endif + tst.w %d0 + bne.s .Lfrb_groups + cmpi.w #ST_SURFACE_WIDTH,%d2 + beq.s .Lfrb_rows +.Lfrb_groups: + move.w %d0,%d4 + add.w %d2,%d4 + subq.w #1,%d4 | the last pixel + move.w %d4,%d5 + eor.w %d0,%d5 + cmpi.w #ST_GROUP_PIXELS - 1,%d5 + bhi.s .Lfrb_multi | two groups or more + | one group: mask = left[x & 15] & right[last & 15] + moveq #ST_GROUP_PIXELS - 1,%d5 + and.w %d0,%d5 + add.w %d5,%d5 + andi.w #ST_GROUP_PIXELS - 1,%d4 + add.w %d4,%d4 + lea _jlgStLeftMaskLut(%pc),%a1 + move.w 0(%a1,%d5.w),%d5 + and.w ST_RIGHT_LUT(%a1,%d4.w),%d5 + | a0 += y * 160 + (x >> 4) * 8, a1 = a0 + h * 160 + add.w %d1,%d1 + lea _jlgStRowOffsetLut(%pc),%a1 + adda.w 0(%a1,%d1.w),%a0 + lsr.w #1,%d0 + andi.w #-ST_BYTES_PER_GROUP,%d0 + adda.w %d0,%a0 + add.w %d3,%d3 + lea _jlgStRowOffsetLut - 2(%pc),%a1 + movea.w 0(%a1,%d3.w),%a1 | (h - 1) * 160 + adda.l %a0,%a1 + lea ST_BYTES_PER_ROW(%a1),%a1 + move.w %d5,%d0 + moveq #ST_GROUP_PIXELS - 1,%d1 + and.b FRR_COLOR(%sp),%d1 + movem.l (%sp)+,%d2-%d5 + bra.w _jlStFillRectGroupRegs +.Lfrb_multi: + move.l %a0,FRR_S(%sp) + movem.l (%sp)+,%d2-%d5 + bra.w _jlSurface68kStFillRectMulti + +| Whole rows (x 0, w 320): the long fill over h * 20 groups. +.Lfrb_rows: + add.w %d1,%d1 + lea _jlgStRowOffsetLut(%pc),%a1 + adda.w 0(%a1,%d1.w),%a0 + move.l %a0,FR_SAVED + LF_DST(%sp) + move.w %d3,%d4 + lsl.w #2,%d4 + move.w %d4,%d5 + lsl.w #2,%d5 + add.w %d5,%d4 | h * 20 groups + move.w %d4,FR_SAVED + LF_NGROUPS(%sp) + lea .Lfrb_pairs(%pc),%a1 + moveq #ST_PAIR_MASK,%d4 + and.b FRR_COLOR(%sp),%d4 + add.w %d4,%d4 + add.w %d4,%d4 + move.l 0(%a1,%d4.w),FR_SAVED + LF_LO(%sp) + moveq #ST_HI_PAIR_MASK,%d4 + and.b FRR_COLOR(%sp),%d4 + move.l 0(%a1,%d4.w),FR_SAVED + LF_HI(%sp) + movem.l (%sp)+,%d2-%d5 + bra.w _jlSurface68kStLongFill + +| A long of two planes' words from two colour bits: the low bit's plane in +| the high word (it comes first in memory). +.Lfrb_pairs: + .long 0x00000000, 0xFFFF0000, 0x0000FFFF, 0xFFFFFFFF + + .if ST_GROUPS_PER_ROW * ST_BYTES_PER_GROUP - ST_BYTES_PER_ROW + .error "a row is 20 groups of 8 bytes" + .endif diff --git a/src/atarist/presentSlam.s b/src/atarist/presentSlam.s index 1a83d12..9ae26be 100644 --- a/src/atarist/presentSlam.s +++ b/src/atarist/presentSlam.s @@ -6,8 +6,11 @@ | | ONE pass over the 200 band rows replaces jlpPresent's three C passes | (union scan + per-row memcpy copy loop + snapshot loop): -| * 4-row fast path: one long compare against the CLEAN sentinel -| pattern on cur AND prev min bytes skips 4 rows in ~30 cyc/row. +| * 4-row fast path: the cur and prev min longs and'ed are all ones +| (every byte the CLEAN sentinel) only when all 8 rows are clean; +| a tight scan (one index for both arrays, ~66 cyc a group) skips +| such groups, and the walkers and cursors catch up at the next +| dirty one. | Valid under the clipped-marks invariant (surfaceInternal.h: | min != 0xFF iff touched; untouched rows hold exactly (FF,00)); | prev arrays only ever hold snapshot copies of cur, so a clean @@ -22,28 +25,30 @@ | head would cap runs at 640 B and cost ~50 memcpy dispatches on a | full-screen present); only a clean group, a partial row, a clean | row, or the end of the walk closes one. -| Cur bands are NOT cleared here -- core stageDirtyClearAll still owns -| that (jlStagePresent contract unchanged). +| The cur bands come out clean: a dirty group's are reset (two long stores) +| once its rows are walked, and the groups the scan skips were clean. So +| jlStagePresent skips its jlStageDirtyClearAll on the ST +| (JL_PRESENT_CLEARS_BANDS, port.h). | | ABI: cdecl. d2-d7/a2-a6 callee-save. | -| uint32_t present68kStSlam(const uint8_t *srcBase, uint8_t *dstBase); +| uint32_t jlPresent68kStSlam(const uint8_t *srcBase, uint8_t *dstBase); | | Return d0: low 16 bits = bytes copied (max 32,000 fits), | bit 16 = "current frame marked at least one row" | (the gPrevAnyDirty snapshot value). | | Register allocation: -| a0 = gStageMinWord walker a1 = gStageMaxWord walker -| a2 = gPrevStageMinWord walker a3 = gPrevStageMaxWord walker +| a0 = jlgStageMinWord walker a1 = jlgStageMaxWord walker +| a2 = jlgPrevStageMinWord walker a3 = jlgPrevStageMaxWord walker | a4 = src row cursor (shadow) a5 = dst row cursor (back page) | a6 = copy chain base | d3 = open full-width-run src start address (0 = no run) -| d4 = 0xFFFFFFFF fast-path constant +| d4 = 0xFFFFFFFF, four clean min bytes | d5 = AND-accumulator of cur min bytes (FF iff no row touched) | d6 = 4-row group counter (dbra) | d7 = copied-bytes accumulator -| d0-d2 = row scratch +| d0-d2 = row scratch; d2 the clean scan's index between groups | a4/a5 advance in lockstep on every path, so (a5 - a4) is the | constant dst-src delta -- run close derives its dst pointer from it. | Geometry (200 rows, 160 B/row, 20 groups, 4 rows/long) is guarded by @@ -179,48 +184,59 @@ rts - .globl _present68kStSlam + .globl _jlPresent68kStSlam -_present68kStSlam: +_jlPresent68kStSlam: movem.l %d2-%d7/%a2-%a6,-(%sp) move.l SP_PS_SRC(%sp),%a4 move.l SP_PS_DST(%sp),%a5 - lea _gStageMinWord,%a0 - lea _gStageMaxWord,%a1 - lea _gPrevStageMinWord,%a2 - lea _gPrevStageMaxWord,%a3 + lea _jlgStageMinWord,%a0 + lea _jlgStageMaxWord,%a1 + lea _jlgPrevStageMinWord,%a2 + lea _jlgPrevStageMaxWord,%a3 lea .LpsChain(%pc),%a6 - moveq #-1,%d4 | CLEAN sentinel pattern + moveq #-1,%d4 | four CLEAN min bytes moveq #-1,%d5 | AND-acc seed moveq #0,%d3 | no open run moveq #0,%d7 | copied bytes moveq #49,%d6 | 50 groups of 4 rows .LpsGroup: - move.l (%a0),%d0 | 4 cur min bytes - cmp.l %d4,%d0 - bne.s .LpsSlow | some cur row touched - move.l (%a2),%d0 | 4 prev min bytes - cmp.l %d4,%d0 - bne.s .LpsSlow | some prev row dirty - tst.l %d3 | clean group ends any open + moveq #0,%d2 | clean groups skipped * 4 +.LpsScan: + move.l 0(%a0,%d2.w),%d0 | 4 cur min bytes + and.l 0(%a2,%d2.w),%d0 | and the 4 prev: all ones + not.l %d0 | iff all 8 rows are clean + bne.s .LpsFound + addq.w #4,%d2 | both sides all-clean: + dbra %d6,.LpsScan | nothing to copy and the + bra.w .LpsEnd | snapshot is (FF,00) over + | (FF,00) -- skip 4 rows +.LpsFound: + tst.w %d2 + beq.s .LpsSlow | no clean group before it + tst.l %d3 | a clean group ends any open beq.s 1f | full-width run (a run must bsr .LpsRunClose | not bridge clean rows) 1: - addq.l #4,%a0 | both sides all-clean: - addq.l #4,%a1 | nothing to copy and the - addq.l #4,%a2 | snapshot is (FF,00) over - addq.l #4,%a3 | (FF,00) -- skip 4 rows - lea 640(%a4),%a4 - lea 640(%a5),%a5 - dbra %d6,.LpsGroup - bra.w .LpsEnd + adda.w %d2,%a0 | the walkers past the clean + adda.w %d2,%a1 | groups + adda.w %d2,%a2 + adda.w %d2,%a3 + lsl.w #5,%d2 | rows * 32 + move.w %d2,%d0 + lsl.w #2,%d0 | rows * 128 + add.w %d0,%d2 | rows * 160 + adda.w %d2,%a4 | and the cursors + adda.w %d2,%a5 .LpsSlow: PS_ROW PS_ROW PS_ROW PS_ROW + move.l %d4,-4(%a0) | the group's cur bands clean + clr.l -4(%a1) dbra %d6,.LpsGroup .LpsEnd: diff --git a/src/atarist/rasterIsr.s b/src/atarist/rasterIsr.s index 3c2c25c..df2b9ea 100644 --- a/src/atarist/rasterIsr.s +++ b/src/atarist/rasterIsr.s @@ -8,10 +8,10 @@ | 190 or so cycles between two lines' displays. Taking the interrupt and | copying 16 words takes longer than that, so Timer B fires a line early, | at the end of the display of the line two above the band (hal.c, -| buildTransitions). This handler loads the colours (_gStRasterColors) into +| buildTransitions). This handler loads the colours (_jlgStRasterColors) into | eight registers, waits for the video address counter to reach -| _gStRasterTarget -- its value at the end of the next line's display -- -| and stores all 16 with one movem.l, about 72 cycles. rasterBandNext (C) +| _jlgStRasterTarget -- its value at the end of the next line's display -- +| and stores all 16 with one movem.l, about 72 cycles. jlRasterBandNext (C) | then sets up the next band and ends the interrupt. | | It waits with the interrupt mask at 5, so the plain ST's PWM pump (Timer @@ -25,7 +25,7 @@ | gcc driver. .text - .globl _stRasterIsr + .globl _jlStRasterIsr .equ VIDEO_COUNTER_MID, 0xffff8207 | the counter's middle byte; its low byte is 2 on .equ PALETTE, 0xffff8240 @@ -34,12 +34,12 @@ .equ RASTER_NEAR, 32 | bytes of display: about 64 cycles .equ RASTER_FAR, 480 | bytes of display: three lines -_stRasterIsr: +_jlStRasterIsr: move.w #SR_ABOVE_TIMER_B,%sr movem.l %d0-%d7/%a0-%a2,-(%sp) - move.l _gStRasterColors,%a0 + move.l _jlgStRasterColors,%a0 movem.l (%a0),%d1-%d7/%a2 - move.w _gStRasterTarget,%a0 + move.w _jlgStRasterTarget,%a0 lea VIDEO_COUNTER_MID,%a1 movep.w 0(%a1),%d0 sub.w %a0,%d0 @@ -63,6 +63,6 @@ _stRasterIsr: .Lstore: movem.l %d1-%d7/%a2,PALETTE move.w #SR_ABOVE_TIMER_B,%sr - jsr _rasterBandNext + jsr _jlRasterBandNext movem.l (%sp)+,%d0-%d7/%a0-%a2 rte diff --git a/src/atarist/serial.c b/src/atarist/serial.c index f63ec9d..1c392cb 100644 --- a/src/atarist/serial.c +++ b/src/atarist/serial.c @@ -69,7 +69,7 @@ #define ST_RX_BUFFER_BYTES 4096 #define ST_RX_LOW_WATER (ST_RX_BUFFER_BYTES / 4) #define ST_RX_HIGH_WATER (ST_RX_BUFFER_BYTES / 4 * 3) -// A write's backstop on a busy transmitter, in Bcostat polls (serialTxStalled): +// A write's backstop on a busy transmitter, in Bcostat polls (jlSerialTxStalled): // the clock bounds it first, once it runs. #define ST_TX_WAIT_POLLS 200000ul @@ -199,7 +199,7 @@ static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr) { static long serialRxBufferInstall(void) { uint16_t saved; - saved = m68kInterruptsOff(); + saved = m68kInterruptsOff(); gSavedRxIorec = *gRxIorec; gRxIorec->ibuf = gRxBuffer; gRxIorec->ibufsiz = ST_RX_BUFFER_BYTES; @@ -215,7 +215,7 @@ static long serialRxBufferInstall(void) { static long serialRxBufferRestore(void) { uint16_t saved; - saved = m68kInterruptsOff(); + saved = m68kInterruptsOff(); gSavedRxIorec.ibufhd = 0; gSavedRxIorec.ibuftl = 0; *gRxIorec = gSavedRxIorec; @@ -304,7 +304,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { uint32_t previous; if (!serialAuxMap(device, config->unit)) { - coreSetError(ST_NO_SUCH_PORT); + jlCoreSetError(ST_NO_SUCH_PORT); return false; } switch (config->flow) { @@ -414,9 +414,9 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { for (n = 0u; n < len; n++) { // A bounded transmitter-ready wait (Bcostat -1 = ready), so a stalled // link (no CTS) reports a partial write instead of hanging the frame. - serialTxWaitStart(&wait, ST_TX_WAIT_POLLS); + jlSerialTxWaitStart(&wait, ST_TX_WAIT_POLLS); while (!Bcostat(ST_AUX_DEV)) { - if (serialTxStalled(&wait)) { + if (jlSerialTxStalled(&wait)) { return n; } } diff --git a/src/atarist/spriteAsm.s b/src/atarist/spriteAsm.s deleted file mode 100644 index a321f74..0000000 --- a/src/atarist/spriteAsm.s +++ /dev/null @@ -1,325 +0,0 @@ -| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. -| See LICENSE.md for license information. -| SPDX-License-Identifier: MIT -| -| ST byte-aligned sprite save / restore. Buffer holds plane-major -| bytes: per row, plane0/1/2/3 per tile col, for w/8 tile cols. The -| inner per-tile-col macro is 4 byte copies (no chunky <-> planar -| conversion since the buffer matches the surface's plane layout). -| -| ABI: cdecl. d2-d7/a2-a6 callee-save. C signatures: -| -| void surface68kStSpriteSaveByteAligned(uint8_t *base, -| uint16_t x, uint16_t y, -| uint16_t w, uint16_t h, -| uint8_t *dstPlaneBytes); -| -| void surface68kStSpriteRestoreByteAligned(uint8_t *base, -| uint16_t x, uint16_t y, -| uint16_t w, uint16_t h, -| const uint8_t *srcPlaneBytes); - - .text - - - .equ SP_SAVED, 44 - .equ SP_OFF, (SP_SAVED + 4) - .equ SP_BASE, SP_OFF + 0 - .equ SP_X, SP_OFF + 4 + 2 - .equ SP_Y, SP_OFF + 8 + 2 - .equ SP_W, SP_OFF + 12 + 2 - .equ SP_H, SP_OFF + 16 + 2 - .equ SP_BUF, SP_OFF + 20 - - -| Per-tile-col SAVE: 4 plane bytes -> 4 contiguous bytes in buffer. -| a0 -> plane 0 byte (high or low half), strides 2 to next plane -| a1 -> output planar bytes (advanced by 4) - - .macro SAVE_TILECOL - move.b (%a0),(%a1)+ | plane 0 - move.b 2(%a0),(%a1)+ | plane 1 - move.b 4(%a0),(%a1)+ | plane 2 - move.b 6(%a0),(%a1)+ | plane 3 - .endm - - - .globl _surface68kStSpriteSaveByteAligned - -_surface68kStSpriteSaveByteAligned: - movem.l %d2-%d7/%a2-%a6,-(%sp) - - move.l SP_BASE(%sp),%a3 - move.l SP_BUF(%sp),%a1 - - move.w SP_W(%sp),%d5 - lsr.w #3,%d5 | d5 = tileCols - move.w SP_H(%sp),%d6 | d6 = h - move.w SP_X(%sp),%d7 - - | a4 = base + y*160 + (x>>4)*8 - move.w SP_Y(%sp),%d0 - ext.l %d0 - move.l %d0,%d1 - lsl.l #5,%d0 | y << 5 - lsl.l #7,%d1 | y << 7 - add.l %d1,%d0 | y * 160 - lea 0(%a3,%d0.l),%a4 - moveq #0,%d0 - move.w %d7,%d0 - lsr.w #4,%d0 - lsl.w #3,%d0 - ext.l %d0 - add.l %d0,%a4 - - | Initial half offset: (x & 8) >> 3 = 0 or 1 - and.w #8,%d7 - lsr.w #3,%d7 - -.LsaveRow: - move.w %d5,%d3 | d3 = tileCols - moveq #0,%d2 - move.w %d7,%d2 - lea 0(%a4,%d2.l),%a0 | a0 = first plane-0 byte - -.LsaveCol: - SAVE_TILECOL - | Advance a0: bit 0 = 0 -> high, advance to low (+1). - | bit 0 = 1 -> low, advance to next group's high (+7). - move.l %a0,%d4 - btst #0,%d4 - bne.s .LsaveColWasLo - addq.l #1,%a0 - bra.s .LsaveColNext -.LsaveColWasLo: - lea 7(%a0),%a0 -.LsaveColNext: - subq.w #1,%d3 - bne.w .LsaveCol - - lea 160(%a4),%a4 - subq.w #1,%d6 - bne.w .LsaveRow - - movem.l (%sp)+,%d2-%d7/%a2-%a6 - rts - - -| Per-tile-col RESTORE: 4 contiguous bytes from buffer -> 4 plane bytes. -| a0 -> plane 0 byte (high or low half) -| a1 -> input planar bytes (advanced by 4) - - .macro RESTORE_TILECOL - move.b (%a1)+,(%a0) | plane 0 - move.b (%a1)+,2(%a0) | plane 1 - move.b (%a1)+,4(%a0) | plane 2 - move.b (%a1)+,6(%a0) | plane 3 - .endm - - - .globl _surface68kStSpriteRestoreByteAligned - -_surface68kStSpriteRestoreByteAligned: - movem.l %d2-%d7/%a2-%a6,-(%sp) - - move.l SP_BASE(%sp),%a3 - move.l SP_BUF(%sp),%a1 - - | tileCols is held in a5 (not d5) because the macro - | trashes d5 (uses it for pb3). - move.w SP_W(%sp),%d0 - lsr.w #3,%d0 - movea.w %d0,%a5 - move.w SP_H(%sp),%d6 - move.w SP_X(%sp),%d7 - - move.w SP_Y(%sp),%d0 - ext.l %d0 - move.l %d0,%d1 - lsl.l #5,%d0 - lsl.l #7,%d1 - add.l %d1,%d0 - lea 0(%a3,%d0.l),%a4 - moveq #0,%d0 - move.w %d7,%d0 - lsr.w #4,%d0 - lsl.w #3,%d0 - ext.l %d0 - add.l %d0,%a4 - - and.w #8,%d7 - lsr.w #3,%d7 - -.LrestoreRow: - move.w %a5,%d3 | d3 = tileCols (from a5) - moveq #0,%d2 - move.w %d7,%d2 - lea 0(%a4,%d2.l),%a0 - -.LrestoreCol: - RESTORE_TILECOL - move.l %a0,%d4 - btst #0,%d4 - bne.s .LrestoreColWasLo - addq.l #1,%a0 - bra.s .LrestoreColNext -.LrestoreColWasLo: - lea 7(%a0),%a0 -.LrestoreColNext: - subq.w #1,%d3 - bne.w .LrestoreCol - - lea 160(%a4),%a4 - subq.w #1,%d6 - bne.w .LrestoreRow - - movem.l (%sp)+,%d2-%d7/%a2-%a6 - rts - - -| ---- surface68kStSprite16x16Save / Restore ----------------------- -| -| Specialized 16x16 sprite save/restore: 16 rows fully unrolled, -| 8 byte copies per row (2 tile cols), no col loop. Drops the asm -| walker's per-row col-init + col-loop-check overhead. -| -| void surface68kStSprite16x16Save(uint8_t *base, -| uint16_t x, uint16_t y, -| uint8_t *dstBuf); -| -| void surface68kStSprite16x16Restore(uint8_t *base, -| uint16_t x, uint16_t y, -| const uint8_t *srcBuf); -| -| Caller guarantees x is byte-aligned (x mod 8 == 0). Two halfOff -| variants dispatch on (x & 8): halfOff=0 reads/writes within one -| group (offsets 0/2/4/6 high half + 1/3/5/7 low half). halfOff=1 -| spans two groups (low half of group N + high half of group N+1). - - .equ SP16_SAVED, 12 | d2/a2-a3 = 3 longs - .equ SP16_OFF, (SP16_SAVED + 4) - .equ SP16_BASE, SP16_OFF + 0 - .equ SP16_X, SP16_OFF + 4 + 2 - .equ SP16_Y, SP16_OFF + 8 + 2 - .equ SP16_BUF, SP16_OFF + 12 - - -| Macro: setup a0 = base + y*160 + group*8 + halfOff -| Trashes: d0, d1, d2; a0 left at row start - - .macro SP16_SETUP_A0 - move.l SP16_BASE(%sp),%a3 - move.w SP16_X(%sp),%d0 - move.w SP16_Y(%sp),%d1 - - | a0 = base + y*160 - ext.l %d1 - move.l %d1,%d2 - lsl.l #5,%d1 - lsl.l #7,%d2 - add.l %d2,%d1 - lea 0(%a3,%d1.l),%a0 - - | a0 += (x>>4) * 8 - move.w %d0,%d1 - lsr.w #4,%d1 - lsl.w #3,%d1 - ext.l %d1 - add.l %d1,%a0 - - | a0 += halfOff (= (x & 8) >> 3) - and.w #8,%d0 - lsr.w #3,%d0 - ext.l %d0 - add.l %d0,%a0 - | d0 = halfOff (0 or 1) for downstream dispatch - .endm - - - .globl _surface68kStSprite16x16Save - -_surface68kStSprite16x16Save: - movem.l %d2/%a2-%a3,-(%sp) - SP16_SETUP_A0 - move.l SP16_BUF(%sp),%a1 - - tst.w %d0 - bne.w .Lsp16s_low - - | halfOff=0: a0 at high half. Col 0 = high (offsets - | 0,2,4,6); col 1 = low (offsets 1,3,5,7). - .rept 16 - move.b (%a0),(%a1)+ - move.b 2(%a0),(%a1)+ - move.b 4(%a0),(%a1)+ - move.b 6(%a0),(%a1)+ - move.b 1(%a0),(%a1)+ - move.b 3(%a0),(%a1)+ - move.b 5(%a0),(%a1)+ - move.b 7(%a0),(%a1)+ - lea 160(%a0),%a0 - .endr - bra.w .Lsp16s_done - -.Lsp16s_low: - | halfOff=1: a0 at low half (group+1). Col 0 = low of - | this group, offsets 0,2,4,6 from a0. Col 1 = high of - | next group, at offsets 7,9,11,13 from a0. - .rept 16 - move.b (%a0),(%a1)+ - move.b 2(%a0),(%a1)+ - move.b 4(%a0),(%a1)+ - move.b 6(%a0),(%a1)+ - move.b 7(%a0),(%a1)+ - move.b 9(%a0),(%a1)+ - move.b 11(%a0),(%a1)+ - move.b 13(%a0),(%a1)+ - lea 160(%a0),%a0 - .endr - -.Lsp16s_done: - movem.l (%sp)+,%d2/%a2-%a3 - rts - - - .globl _surface68kStSprite16x16Restore - -_surface68kStSprite16x16Restore: - movem.l %d2/%a2-%a3,-(%sp) - SP16_SETUP_A0 - move.l SP16_BUF(%sp),%a1 - - tst.w %d0 - bne.w .Lsp16r_low - - | halfOff=0: write high half (col 0) + low half (col 1). - .rept 16 - move.b (%a1)+,(%a0) - move.b (%a1)+,2(%a0) - move.b (%a1)+,4(%a0) - move.b (%a1)+,6(%a0) - move.b (%a1)+,1(%a0) - move.b (%a1)+,3(%a0) - move.b (%a1)+,5(%a0) - move.b (%a1)+,7(%a0) - lea 160(%a0),%a0 - .endr - bra.w .Lsp16r_done - -.Lsp16r_low: - | halfOff=1 - .rept 16 - move.b (%a1)+,(%a0) - move.b (%a1)+,2(%a0) - move.b (%a1)+,4(%a0) - move.b (%a1)+,6(%a0) - move.b (%a1)+,7(%a0) - move.b (%a1)+,9(%a0) - move.b (%a1)+,11(%a0) - move.b (%a1)+,13(%a0) - lea 160(%a0),%a0 - .endr - -.Lsp16r_done: - movem.l (%sp)+,%d2/%a2-%a3 - rts diff --git a/src/atarist/spriteWalk.s b/src/atarist/spriteWalk.s index ff3d7db..e3eda82 100644 --- a/src/atarist/spriteWalk.s +++ b/src/atarist/spriteWalk.s @@ -24,7 +24,7 @@ | d1.w = groupsPerRow - 1 | The thunk jmp's (not jsr's), so sp still holds the dispatcher's | return address and the final rts returns straight to C. d0/d1/a0/a1 -| are cdecl scratch; d3-d6/a2 are callee-save and preserved here. +| are cdecl scratch; d3-d6/a2-a3 are callee-save and preserved here. | | GAS m68k syntax, assembled by m68k-atari-mint-as. ST-only: this file | must stay in src/atarist/ (src/m68k/*.s is wildcarded into the Amiga @@ -32,24 +32,46 @@ .text - .globl _stSpriteDrawShiftWalker +| Each row jumps into one unrolled run of group bodies at the entry that +| leaves exactly groupsPerRow of them before .LswEnd: no per-group loop +| count, and the stores step a2 themselves. A drawn phase starts at x = +| shift or later and ends on the surface, so a row spans at most +| SW_MAX_GROUPS groups (the emitter skips a phase that would not fit). -_stSpriteDrawShiftWalker: - movem.l %d3-%d6/%a2,-(%sp) + .equ SW_MAX_GROUPS, 21 | JL_SURFACE_WIDTH / 16 + 1 + .equ SW_GROUP_BYTES, 18 | one group body, checked below + + .globl _jlStSpriteDrawShiftWalker + +_jlStSpriteDrawShiftWalker: + movem.l %d3-%d6/%a2-%a3,-(%sp) + addq.w #1,%d1 | d1 = groupsPerRow + move.w %d1,%d3 + lsl.w #4,%d3 + add.w %d1,%d1 + add.w %d1,%d3 | d3 = groupsPerRow * SW_GROUP_BYTES + lea .LswEnd(%pc),%a3 + suba.w %d3,%a3 | a3 = entry for groupsPerRow groups .LswRow: movea.l %a0,%a2 | a2 = this row's first group - move.w %d1,%d3 | d3 = groupsPerRow - 1 -.LswGrp: + jmp (%a3) +.LswRun: + .rept SW_MAX_GROUPS movem.l (%a2),%d4-%d5 | d4 = p0w:p1w, d5 = p2w:p3w move.l (%a1)+,%d6 | notMask long and.l %d6,%d4 and.l %d6,%d5 or.l (%a1)+,%d4 | bits01 or.l (%a1)+,%d5 | bits23 - movem.l %d4-%d5,(%a2) - addq.l #8,%a2 | next group - dbra %d3,.LswGrp + move.l %d4,(%a2)+ + move.l %d5,(%a2)+ + .endr +.LswEnd: lea 160(%a0),%a0 | ST_BYTES_PER_ROW dbra %d0,.LswRow - movem.l (%sp)+,%d3-%d6/%a2 + movem.l (%sp)+,%d3-%d6/%a2-%a3 rts + + .if (.LswEnd - .LswRun) - SW_MAX_GROUPS * SW_GROUP_BYTES + .error "SW_GROUP_BYTES does not match the emitted group" + .endif diff --git a/src/atarist/stAltLayer.c b/src/atarist/stAltLayer.c index 0f922c1..ef443a0 100644 --- a/src/atarist/stAltLayer.c +++ b/src/atarist/stAltLayer.c @@ -66,7 +66,7 @@ static const AltLayerT kAltLayers[] = { }; -bool stAltLayerFind(uint8_t country, const uint8_t **unshift, const uint8_t **shift, const uint8_t **caps) { +bool jlStAltLayerFind(uint8_t country, const uint8_t **unshift, const uint8_t **shift, const uint8_t **caps) { size_t i; for (i = 0; i < sizeof(kAltLayers) / sizeof(kAltLayers[0]); i++) { diff --git a/src/atarist/stAltLayer.h b/src/atarist/stAltLayer.h index e10c545..97c40be 100644 --- a/src/atarist/stAltLayer.h +++ b/src/atarist/stAltLayer.h @@ -16,6 +16,6 @@ // three lists of (scan code, character) pairs ended by a 0, for no Shift or // Caps Lock, with Shift, and with Caps Lock. False for a country with none // (or none known), and the lists are then left alone. -bool stAltLayerFind(uint8_t country, const uint8_t **unshift, const uint8_t **shift, const uint8_t **caps); +bool jlStAltLayerFind(uint8_t country, const uint8_t **unshift, const uint8_t **shift, const uint8_t **caps); #endif diff --git a/src/atarist/stGlyph.s b/src/atarist/stGlyph.s new file mode 100644 index 0000000..b64e768 --- /dev/null +++ b/src/atarist/stGlyph.s @@ -0,0 +1,149 @@ +| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +| See LICENSE.md for license information. +| SPDX-License-Identifier: MIT +| +| void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, +| const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) +| +| The public call itself on the ST (JL_HAS_TILE_PASTE_GLYPH_ENTRY; tile.c +| leaves its C body out): an 8x8 1bpp glyph into the word-interleaved +| planes, and the stage's dirty mark, in one call. gcc 15 spent about as long +| around the paste as in it: its checks, its address arithmetic and the +| mark's set-up, with the arguments spilled and reloaded. A glyph off the +| surface, or with no bits, is nothing, as in C; a surface with no planes +| goes on to jlpGenericTilePasteGlyph, checked, with the same arguments. +| +| Plane p's byte of a row is (shape & (fg_p ^ bg_p)) ^ bg_p -- the glyph row, +| its complement, all zeros, or all ones -- so the four planes are one long +| and-ed and xor-ed with masks fixed for the glyph (plane 0 in the high +| byte), and one movep.l stores them: the planes' bytes of a group half are 2 +| apart. The row byte comes to all four bytes of the long from a table (one +| load, against a shift-and-or chain), and the eight rows are unrolled, each +| at its own displacement. +| +| cdecl, mint-gcc: every argument in a 4-byte stack slot, a uint8_t in its low +| byte; C symbols spelt with a leading underscore; d2-d7/a2-a6 callee-save. + + .equ GLYPH_SAVED, 12 | d2-d3/a2, pushed first + .equ GLYPH_DST, GLYPH_SAVED + 4 + .equ GLYPH_BX, GLYPH_SAVED + 11 + .equ GLYPH_BY, GLYPH_SAVED + 15 + .equ GLYPH_FG, GLYPH_SAVED + 23 + .equ GLYPH_BG, GLYPH_SAVED + 27 + .equ SURFACE_PORT, 4 | offsetof(jlSurfaceT, portData) (stTile.h checks it) + .equ BLOCKS_PER_ROW, 40 | JL_TILE_BLOCKS_PER_ROW (tile.c checks it) + .equ BLOCKS_PER_COL, 25 | JL_TILE_BLOCKS_PER_COL + .equ ROW_BYTES, 160 | ST_BYTES_PER_ROW + .equ GROUP_BYTES, 8 | ST_BYTES_PER_GROUP + .equ COLOR_MASK, 0x0F + + .text + .even + .globl _jlTilePasteGlyph + +| One glyph row: its byte in all four bytes of a long, masked, stored to the +| four planes' bytes of its row. + .macro GLYPH_ROW r + moveq #0,%d1 + move.b (%a0)+,%d1 + add.w %d1,%d1 + add.w %d1,%d1 + move.l 0(%a1,%d1.w),%d0 + and.l %d2,%d0 + eor.l %d3,%d0 + movep.l %d0,(\r * ROW_BYTES)(%a2) + .endm + +_jlTilePasteGlyph: + movem.l %d2-%d3/%a2,-(%sp) + movem.l GLYPH_DST(%sp),%d0-%d2/%a0 | dst, bx, by, bits + tst.l %d0 + beq.w .Lg_none + move.l %a0,%d3 + beq.w .Lg_none + cmpi.b #BLOCKS_PER_ROW,%d1 + bcc.w .Lg_none + cmpi.b #BLOCKS_PER_COL,%d2 + bcc.w .Lg_none + movea.l %d0,%a1 + move.l SURFACE_PORT(%a1),%d3 | the StPlanarT + beq.w .Lg_generic + movea.l %d3,%a2 + movea.l (%a2),%a2 | its base (first field) + andi.w #0xFF,%d2 + add.w %d2,%d2 + lea .Lg_rows(%pc),%a1 + adda.w 0(%a1,%d2.w),%a2 | the tile row + andi.w #0xFF,%d1 + moveq #1,%d0 + and.w %d1,%d0 + adda.w %d0,%a2 | the group half + lsr.w #1,%d1 + lsl.w #3,%d1 + adda.w %d1,%a2 | the group + lea .Lg_planes(%pc),%a1 + moveq #COLOR_MASK,%d0 + and.b GLYPH_FG(%sp),%d0 + moveq #COLOR_MASK,%d1 + and.b GLYPH_BG(%sp),%d1 + eor.b %d1,%d0 + add.w %d0,%d0 + add.w %d0,%d0 + move.l 0(%a1,%d0.w),%d2 | and: the planes fg and bg differ in + add.w %d1,%d1 + add.w %d1,%d1 + move.l 0(%a1,%d1.w),%d3 | xor: bg's planes + lea .Lg_rowBytes(%pc),%a1 + GLYPH_ROW 0 + GLYPH_ROW 1 + GLYPH_ROW 2 + GLYPH_ROW 3 + GLYPH_ROW 4 + GLYPH_ROW 5 + GLYPH_ROW 6 + GLYPH_ROW 7 + move.l GLYPH_DST(%sp),%d0 + cmp.l _jlgStage,%d0 + bne.s .Lg_none + moveq #0,%d0 + move.b GLYPH_BY(%sp),%d0 + lsl.w #3,%d0 | the tile's first row + lea _jlgStageMinWord,%a0 + adda.w %d0,%a0 + lea _jlgStageMaxWord,%a1 + adda.w %d0,%a1 + moveq #0,%d1 + move.b GLYPH_BX(%sp),%d1 + add.w %d1,%d1 | its first 16-bit word + jsr jlStageBandsMarkTile68k +.Lg_none: + movem.l (%sp)+,%d2-%d3/%a2 + rts +| No planes: the generic, with the same arguments, the colours masked. +.Lg_generic: + andi.b #COLOR_MASK,GLYPH_FG(%sp) + andi.b #COLOR_MASK,GLYPH_BG(%sp) + movem.l (%sp)+,%d2-%d3/%a2 + jmp _jlpGenericTilePasteGlyph + +| A tile row's offset: block row * 8 rows * 160 bytes. + .even +.Lg_rows: + .irp by, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 + .word \by * 8 * ROW_BYTES + .endr +| A colour's planes as the bytes of a long, plane 0 high: 0xFF where set. +| (The bytes add, as | starts a comment here.) +.Lg_planes: + .set color, 0 + .rept 16 + .long ((color & 1) * 0xFF000000) + ((color & 2) / 2 * 0x00FF0000) + ((color & 4) / 4 * 0x0000FF00) + ((color & 8) / 8 * 0x000000FF) + .set color, color + 1 + .endr +| A glyph row byte in each byte of a long. +.Lg_rowBytes: + .set rowByte, 0 + .rept 256 + .long rowByte * 0x01010101 + .set rowByte, rowByte + 1 + .endr diff --git a/src/atarist/stTile.h b/src/atarist/stTile.h index be0e49c..57bdaa6 100644 --- a/src/atarist/stTile.h +++ b/src/atarist/stTile.h @@ -13,22 +13,31 @@ #ifndef JOEY_ST_TILE_H #define JOEY_ST_TILE_H -#include <stdint.h> #include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> #include "joey/tile.h" #include "stPlanar.h" // 68k asm entry (surface68kSt.s side): fills one 8-px half-group in // all 4 planes. Declared here because stTileFill is its only caller. -extern void surface68kStTileFill8x8(uint8_t *firstGroupPtr, uint16_t mask, uint8_t color); +extern void jlSurface68kStTileFill8x8(uint8_t *firstGroupPtr, uint16_t mask, uint8_t color); + +// lineSpan.s's rect fills below jlFillRect, for a rect on the surface with +// w and h positive. Base picks the STe's BLiTTER, whole rows as longs, one +// 16-px group column under a mask, or groups with edge masks, and jumps to +// it; Mark widens the stage's dirty bands first. +extern void jlStFillRectBase(uint8_t *base, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex); +extern void jlStFillRectMark(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex); // 68k asm entry (tileMap.s): whole-map paste with a movep.l row body // (36 cyc/tile-row vs ~80 for compiled move.b). Requires an EVEN // tileset pointer (move.l source alignment); stTileMapPaste guards // and keeps the C pair walker as the odd-pointer fallback. Declared // here because stTileMapPaste is its only caller. -extern void stTileMapPasteMovep(uint8_t *rowBase, const uint8_t *tiles, const uint8_t *map, uint16_t wTiles, uint16_t hTiles, uint16_t headOdd); +extern void jlStTileMapPasteMovep(uint8_t *rowBase, const uint8_t *tiles, const uint8_t *map, uint16_t wTiles, uint16_t hTiles, uint16_t headOdd); // Register-args jsr shims for the single-tile movep bodies in // tileMap.s (NATIVE-PERF W3). The bodies take their pointers IN @@ -42,7 +51,7 @@ static inline __attribute__((always_inline)) void stTilePasteMovepCall(uint8_t * register const uint8_t *rSrc __asm__("a0") = tileSrc; register uint8_t *rDst __asm__("a1") = dstAddr; - __asm__ volatile ("jsr _stTilePasteMovepRegs" : "+a" (rSrc), "+a" (rDst) : : "d0", "cc", "memory"); + __asm__ volatile ("jsr _jlStTilePasteMovepRegs" : "+a" (rSrc), "+a" (rDst) : : "d0", "cc", "memory"); } @@ -50,7 +59,7 @@ static inline __attribute__((always_inline)) void stTileCopyMovepCall(uint8_t *d register const uint8_t *rSrc __asm__("a0") = srcAddr; register uint8_t *rDst __asm__("a1") = dstAddr; - __asm__ volatile ("jsr _stTileCopyMovepRegs" : "+a" (rSrc), "+a" (rDst) : : "d0", "cc", "memory"); + __asm__ volatile ("jsr _jlStTileCopyMovepRegs" : "+a" (rSrc), "+a" (rDst) : : "d0", "cc", "memory"); } @@ -58,9 +67,157 @@ static inline __attribute__((always_inline)) void stTileSnapMovepCall(uint8_t *o register const uint8_t *rSrc __asm__("a0") = srcAddr; register uint8_t *rOut __asm__("a1") = outTile; - __asm__ volatile ("jsr _stTileSnapMovepRegs" : "+a" (rSrc), "+a" (rOut) : : "d0", "cc", "memory"); + __asm__ volatile ("jsr _jlStTileSnapMovepRegs" : "+a" (rSrc), "+a" (rOut) : : "d0", "cc", "memory"); } + +// rows rows of groups 16-px groups (8 bytes, two longs; groups >= 1) from +// src to dst, both even, src stepping srcSkip and dst dstSkip more bytes +// after each row. gcc 15 calls memcpy for each row of these spans, and its +// setup outweighs the copy. +static inline __attribute__((always_inline)) void stCopyGroups(const uint8_t *src, uint16_t srcSkip, uint8_t *dst, uint16_t dstSkip, uint16_t groups, uint16_t rows) { + __asm__ volatile ( + " bra.s 3f\n" + "1: move.w %[groups],%%d0\n" + " subq.w #1,%%d0\n" + "2: move.l (%[src])+,(%[dst])+\n" + " move.l (%[src])+,(%[dst])+\n" + " dbra %%d0,2b\n" + " adda.w %[srcSkip],%[src]\n" + " adda.w %[dstSkip],%[dst]\n" + "3: subq.w #1,%[rows]\n" + " bcc.s 1b" + : [src] "+a" (src), [dst] "+a" (dst), [rows] "+d" (rows) + : [groups] "d" (groups), [srcSkip] "d" (srcSkip), [dstSkip] "d" (dstSkip) + : "d0", "cc", "memory"); +} +// Solid rects go to lineSpan.s: jlpFillRect (the line and flood spans) +// with the stage's mark fused, jlpFillRectNoMark (outlines and circle +// spans, which mark once for the whole shape) without. jlFillRect itself is +// asm there too (JL_HAS_FILL_RECT_ENTRY). Callers guarantee an on-surface +// rect with positive w and h. +static inline __attribute__((always_inline)) void stFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { + jlStFillRectBase(((StPlanarT *)s->portData)->base, x, y, (uint16_t)w, (uint16_t)h, colorIndex); +} + + +static inline __attribute__((always_inline)) void stFillRectMark(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { + jlStFillRectMark(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex); +} + + +// Single pixels (jlpDrawPixel / jlpSamplePixel), inline for the same reason +// as the tiles: jlDrawPixel, the flood fill's reads, and the planar snap call +// them per pixel. The pixel's bit is (15 - (x & 15)) of its group's four +// plane words. A planar surface always has portData (jlSurfaceCreate fails +// without it), so there is no fallback to test for. The plane writes are +// asm because gcc 15 moves each plane word through two registers instead of +// or-ing into memory: here each plane is a carry test and one or or and. +static inline __attribute__((always_inline)) void stDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { + uint16_t *pw; + uint16_t bit; + uint16_t keep; + + pw = (uint16_t *)(((StPlanarT *)s->portData)->base + y * ST_BYTES_PER_ROW + (x >> 4) * ST_BYTES_PER_GROUP); + bit = (uint16_t)(0x8000u >> (x & 15u)); + keep = (uint16_t)~bit; + __asm__ volatile ( + "lsr.b #1,%1\n\t" + "bcc.s 1f\n\t" + "or.w %2,(%0)+\n\t" + "bra.s 2f\n" + "1:\tand.w %3,(%0)+\n" + "2:\tlsr.b #1,%1\n\t" + "bcc.s 1f\n\t" + "or.w %2,(%0)+\n\t" + "bra.s 2f\n" + "1:\tand.w %3,(%0)+\n" + "2:\tlsr.b #1,%1\n\t" + "bcc.s 1f\n\t" + "or.w %2,(%0)+\n\t" + "bra.s 2f\n" + "1:\tand.w %3,(%0)+\n" + "2:\tlsr.b #1,%1\n\t" + "bcc.s 1f\n\t" + "or.w %2,(%0)\n\t" + "bra.s 2f\n" + "1:\tand.w %3,(%0)\n" + "2:" + : "+a" (pw), "+d" (colorIndex) + : "d" (bit), "d" (keep) + : "cc", "memory"); +} + + +static inline __attribute__((always_inline)) uint8_t stSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { + const uint16_t *pw; + uint16_t bit; + uint8_t color; + + pw = (const uint16_t *)(((const StPlanarT *)s->portData)->base + (uint16_t)y * ST_BYTES_PER_ROW + ((uint16_t)x >> 4) * ST_BYTES_PER_GROUP); + bit = (uint16_t)(0x8000u >> ((uint16_t)x & 15u)); + color = 0u; + if ((pw[0] & bit) != 0u) { + color = 1u; + } + if ((pw[1] & bit) != 0u) { + color = (uint8_t)(color | 2u); + } + if ((pw[2] & bit) != 0u) { + color = (uint8_t)(color | 4u); + } + if ((pw[3] & bit) != 0u) { + color = (uint8_t)(color | 8u); + } + return color; +} + + +// Sprite backups (jlpSpriteSavePlanes / jlpSpriteRestorePlanes): every save +// records the 16-px group region holding the sprite (sprite.c, +// SPRITE_BACKUP_ALIGN), so a backup row is that region's span of +// interleaved group bytes -- the compiled save's format (the format law, +// spriteInternal.h). An even buffer takes them through stCopyGroups, a group +// two longs; a caller's buffer may sit at an odd address, where the 68000 +// takes no word, so that one goes through memcpy. w is at least 16. +static inline __attribute__((always_inline)) void stSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { + const uint8_t *src; + uint16_t spanBytes; + uint16_t row; + + src = ((const StPlanarT *)s->portData)->base + (uint16_t)y * ST_BYTES_PER_ROW + ((uint16_t)x >> 4) * ST_BYTES_PER_GROUP; + spanBytes = (uint16_t)(w >> 1); + if ((((uintptr_t)dstPlaneBytes) & 1u) == 0u) { + stCopyGroups(src, (uint16_t)(ST_BYTES_PER_ROW - spanBytes), dstPlaneBytes, 0u, (uint16_t)(w >> 4), h); + return; + } + for (row = 0u; row < h; row++) { + memcpy(dstPlaneBytes, src, spanBytes); + src += ST_BYTES_PER_ROW; + dstPlaneBytes += spanBytes; + } +} + + +static inline __attribute__((always_inline)) void stSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { + uint8_t *dst; + uint16_t spanBytes; + uint16_t row; + + dst = ((StPlanarT *)s->portData)->base + (uint16_t)y * ST_BYTES_PER_ROW + ((uint16_t)x >> 4) * ST_BYTES_PER_GROUP; + spanBytes = (uint16_t)(w >> 1); + if ((((uintptr_t)srcPlaneBytes) & 1u) == 0u) { + stCopyGroups(srcPlaneBytes, 0u, dst, (uint16_t)(ST_BYTES_PER_ROW - spanBytes), (uint16_t)(w >> 4), h); + return; + } + for (row = 0u; row < h; row++) { + memcpy(dst, srcPlaneBytes, spanBytes); + dst += ST_BYTES_PER_ROW; + srcPlaneBytes += spanBytes; + } +} + + // Phase 5 tile ops. 8x8 tiles at byte position (bx, by) start at // pixel (bx*8, by*8). 8 pixels wide always covers exactly half a // 16-pixel group: high half (bits 15..8) when bx is even, low half @@ -83,7 +240,7 @@ static inline __attribute__((always_inline)) void stTileFill(jlSurfaceT *s, uint gp = pd->base + (uint16_t)by * 8u * ST_BYTES_PER_ROW + group * ST_BYTES_PER_GROUP; /* Phase 10 final: specialized 8x8 unrolled tile-fill skips the * generic FRG_LOOP's per-row subq+bne overhead. */ - surface68kStTileFill8x8(gp, halfMask, colorIndex); + jlSurface68kStTileFill8x8(gp, halfMask, colorIndex); } @@ -196,7 +353,7 @@ static inline __attribute__((always_inline)) void stTileMapPaste(jlSurfaceT *dst rowBase = pd->base + (uint16_t)by * 8u * ST_BYTES_PER_ROW + ((uint16_t)bx >> 1) * ST_BYTES_PER_GROUP; headOdd = (uint8_t)(bx & 1u); if (((uintptr_t)tileBytes & 1u) == 0u) { - stTileMapPasteMovep(rowBase, tileBytes, map, wTiles, hTiles, headOdd); + jlStTileMapPasteMovep(rowBase, tileBytes, map, wTiles, hTiles, headOdd); return; } pairs = (uint8_t)((uint8_t)(wTiles - headOdd) >> 1); @@ -297,34 +454,10 @@ static inline __attribute__((always_inline)) void stTilePasteMono(jlSurfaceT *ds } -// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape -// a mono paste has to fold out of a chunky silhouette, so this skips -// that fold entirely (and cannot be handed the wrong tile format). -static inline __attribute__((always_inline)) void stTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { - StPlanarT *pd; - uint16_t group; - uint8_t *dstAddr; - uint8_t row; - uint8_t plane; - uint8_t masksFg[4]; - uint8_t masksBg[4]; - - if (dst->portData == NULL) { - jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); - return; - } - pd = (StPlanarT *)dst->portData; - for (plane = 0u; plane < 4u; plane++) { - masksFg[plane] = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); - masksBg[plane] = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); - } - group = (uint16_t)((uint16_t)bx >> 1); - dstAddr = pd->base + (uint16_t)by * 8u * ST_BYTES_PER_ROW + group * ST_BYTES_PER_GROUP + (uint16_t)(bx & 1u); - for (row = 0u; row < JL_TILE_PIXELS_PER_SIDE; row++) { - ST_MONO_ROW(dstAddr, bits[row], masksFg, masksBg); - dstAddr += ST_BYTES_PER_ROW; - } -} +// The 1bpp glyph paste is jlTilePasteGlyph itself, in asm: stGlyph.s +// (JL_HAS_TILE_PASTE_GLYPH_ENTRY), which finds the planes at the surface's +// portData, where StPlanarT's base leads. +typedef char stGlyphEntryProof[(offsetof(jlSurfaceT, portData) == 4 && offsetof(StPlanarT, base) == 0 && ST_BYTES_PER_ROW == 160 && ST_BYTES_PER_GROUP == 8) ? 1 : -1]; static inline __attribute__((always_inline)) void stTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) { diff --git a/src/atarist/stTileLayer.c b/src/atarist/stTileLayer.c new file mode 100644 index 0000000..d7330cd --- /dev/null +++ b/src/atarist/stTileLayer.c @@ -0,0 +1,97 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Atari ST tile-layer ring (tileLayerInternal.h has the contract, +// stTileLayer.h the layout). Unlike the stage, the ring is plane-separated, +// so a plane's words are consecutive: the ST's blit shifts a pair of them as +// one long (stTileLayerBlit.s), 168 bytes a pixel row, 34944 for a +// full-height view; the STe's BLiTTER shifts them as it copies +// (ste/blitter.c), from rows 8 bytes longer. + +#include <stdint.h> +#include <string.h> + +#include "joey/tile.h" +#include "port.h" +#include "stPlanar.h" +#include "stTileLayer.h" +#include "surfaceInternal.h" +#ifdef JOEYLIB_ATARI_STE +#include "steBlitter.h" +#endif + +#ifdef JOEYLIB_ATARI_STE +// The copy of a plane row's first word holds its first two slots. +#define RING_COPIED_SLOTS ST_RING_COPY_BYTES +#else +// stTileLayerBlit.s has these numbers in it (the plane and pixel row bytes, +// and 20 groups of 8 bytes to a 160-byte row). +typedef char stTileLayerRingProof[(ST_RING_PLANE_BYTES == 42 && ST_RING_ROW_BYTES == 168 && ST_BYTES_PER_ROW == 160) ? 1 : -1]; + +extern void jlStTileLayerBlitRows(const uint8_t *ring, uint8_t *dst, uint16_t ringX, uint16_t ringY, uint16_t rows, uint16_t ringPxRows); +#endif + + +void jlpTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) { + StPlanarT *pd; + uint8_t *band; + + pd = stSurfacePlanar(dst); + band = pd->base + (uint16_t)layer->viewBy * JL_TILE_PIXELS_PER_SIDE * ST_BYTES_PER_ROW; +#ifdef JOEYLIB_ATARI_STE + jlStBlitTileLayerRows((const uint8_t *)layer->ring, band, ringX, ringY, (uint16_t)((uint16_t)layer->viewHeight << 3), (uint16_t)((uint16_t)layer->ringRows << 3)); +#else + jlStTileLayerBlitRows((const uint8_t *)layer->ring, band, ringX, ringY, (uint16_t)((uint16_t)layer->viewHeight << 3), (uint16_t)((uint16_t)layer->ringRows << 3)); +#endif +} + + +// A jlTileT row on the ST is its four plane bytes (tile.h), one to each +// plane row of the slot (and, on the STe, the first two slots to their copy +// at the plane row's end too). +void jlpTileLayerPut(jlTileLayerT *layer, uint8_t slotCol, uint8_t slotRow, const jlTileT *tile) { + uint8_t *d; + const uint8_t *s; + uint8_t r; + + d = (uint8_t *)layer->ring + (uint16_t)slotRow * ST_RING_SLOT_BYTES + slotCol; + s = tile->pixels; + for (r = 0u; r < JL_TILE_PIXELS_PER_SIDE; r++) { + d[0] = s[0]; + d[ST_RING_PLANE_BYTES] = s[1]; + d[ST_RING_PLANE_BYTES * 2] = s[2]; + d[ST_RING_PLANE_BYTES * 3] = s[3]; +#ifdef JOEYLIB_ATARI_STE + if (slotCol < RING_COPIED_SLOTS) { + d[TILE_LAYER_RING_COLS] = s[0]; + d[ST_RING_PLANE_BYTES + TILE_LAYER_RING_COLS] = s[1]; + d[ST_RING_PLANE_BYTES * 2 + TILE_LAYER_RING_COLS] = s[2]; + d[ST_RING_PLANE_BYTES * 3 + TILE_LAYER_RING_COLS] = s[3]; + } +#endif + d += ST_RING_ROW_BYTES; + s += JL_TILE_BYTES_PER_ROW; + } +} + + +bool jlpTileLayerRingAlloc(jlTileLayerT *layer) { + uint32_t bytes; + + bytes = (uint32_t)layer->ringRows * ST_RING_SLOT_BYTES; + layer->ring = jlpBigAlloc(bytes); + if (layer->ring == NULL) { + return false; + } + memset(layer->ring, 0, bytes); + return true; +} + + +void jlpTileLayerRingFree(jlTileLayerT *layer) { + if (layer->ring != NULL) { + jlpBigFree(layer->ring); + layer->ring = NULL; + } +} diff --git a/src/atarist/stTileLayer.h b/src/atarist/stTileLayer.h new file mode 100644 index 0000000..9987fec --- /dev/null +++ b/src/atarist/stTileLayer.h @@ -0,0 +1,28 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// The Atari ST family's tile-layer ring layout, shared by stTileLayer.c and +// the STe build's BLiTTER blit (ste/blitter.c). The ring is plane-separated: +// each of its ringRows * 8 pixel rows is plane 0's words, then planes 1, 2, +// and 3, a slot being one byte of each (slot c at byte c), so a plane's +// words are consecutive. A plane row is the ring's 21 words; the STe +// build's carries a copy of the first word after them, so that a run of +// source words starting anywhere reaches the word after its last without +// wrapping (the chip reads one word past a shifted run). +#ifndef JOEY_ST_TILE_LAYER_H +#define JOEY_ST_TILE_LAYER_H + +#include "tileLayerInternal.h" + +#define ST_RING_WORDS (TILE_LAYER_RING_COLS / 2) // a slot is one byte of a plane row +#ifdef JOEYLIB_ATARI_STE +#define ST_RING_COPY_BYTES 2 // the first word again +#else +#define ST_RING_COPY_BYTES 0 +#endif +#define ST_RING_PLANE_BYTES (TILE_LAYER_RING_COLS + ST_RING_COPY_BYTES) +#define ST_RING_ROW_BYTES (ST_RING_PLANE_BYTES * 4) +#define ST_RING_SLOT_BYTES (ST_RING_ROW_BYTES * JL_TILE_PIXELS_PER_SIDE) + +#endif diff --git a/src/atarist/stTileLayerBlit.s b/src/atarist/stTileLayerBlit.s new file mode 100644 index 0000000..fb359f1 --- /dev/null +++ b/src/atarist/stTileLayerBlit.s @@ -0,0 +1,246 @@ +| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +| See LICENSE.md for license information. +| SPDX-License-Identifier: MIT +| +| Atari ST tile-layer blit: `rows` rows of the plane-separated ring +| (stTileLayer.c has its layout) into the interleaved stage band, starting +| at ring pixel row ringY and column ringX, both wrapping. +| +| void jlStTileLayerBlitRows(const uint8_t *ring, uint8_t *dst, uint16_t ringX, uint16_t ringY, uint16_t rows, uint16_t ringPxRows); +| +| Output word k of a plane row is (P[w+k] << s) | (P[w+k+1] >> (16 - s)), +| with w = ringX / 16, s = ringX & 15, and word indices modulo the ring's +| 21. Each plane keeps its last word in the high half of a carry register +| (d0-d3); move.w (aP)+ brings the next into the low half, so the pair +| shifts as one long: lsl.l s and the high word for s 1-8, lsr.l 16-s and +| the low word for s 9-15 (the fewer bits either way). s 0 is a plain +| word copy. A row is two unrolled stretches of whole groups (the four +| plane words of 16 pixels): up to the ring's right edge, then on from its +| left edge, the plane pointers stepping back a plane row between them. +| A shifted row reads P[w] first, so its stretches are 20 - w and w +| groups; a plain one's are 21 - w and w - 1 (20 and 0 for w 0). +| +| ABI: cdecl, d2-d7/a2-a6 saved. Registers: a1 the band (it advances 160 +| bytes a row by itself), a2-a5 the plane pointers, a6 the ring row, d0-d3 +| the carries, d4 scratch, d5 rows left, d6 the byte offset of P[w], d7 +| the shift count. +| +| GAS m68k syntax, assembled by m68k-atari-mint-as. ST-only: this file must +| stay in src/atarist/ (src/m68k/*.s is wildcarded into the Amiga build too). + + .equ RING_PLANE_BYTES, 42 | 21 words: one byte per slot + .equ RING_ROW_BYTES, 168 | four plane rows + .equ GROUPS, 20 | 16-pixel groups in a band row + .equ RING_WORDS, 21 + .equ LEFT_GROUP_BYTES, 48 | emitted size of one LEFT_GROUP + .equ RIGHT_GROUP_BYTES, 40 + .equ PLAIN_GROUP_BYTES, 8 + .equ LEFT_MAX_SHIFT, 8 + + .equ SAVED, 44 | d2-d7/a2-a6 + .equ LOCALS, 16 + .equ L_ENTRY1, 0 | first stretch's entry + .equ L_ENTRY2, 4 | second stretch's entry + .equ L_END, 8 | ring end + .equ L_BASE, 12 | ring base + .equ ARGS, (LOCALS + SAVED + 4) + .equ A_RING, ARGS + 0 + .equ A_DST, ARGS + 4 + .equ A_RINGX, ARGS + 8 + 2 + .equ A_RINGY, ARGS + 12 + 2 + .equ A_ROWS, ARGS + 16 + 2 + .equ A_RINGPX, ARGS + 20 + 2 + + +| One plane word, shifted left (s 1-8): the high word of (prev:next) << s. + .macro LEFT_PLANE aP, dP + move.w (\aP)+,\dP + move.l \dP,%d4 + lsl.l %d7,%d4 + swap %d4 + move.w %d4,(%a1)+ + swap \dP + .endm + + +| One plane word, shifted right (s 9-15, d7 = 16 - s): the low word of +| (prev:next) >> (16 - s). + .macro RIGHT_PLANE aP, dP + move.w (\aP)+,\dP + move.l \dP,%d4 + lsr.l %d7,%d4 + move.w %d4,(%a1)+ + swap \dP + .endm + + +| The first plane pointer and the three after it for the ring row in a6. + .macro PLANE_POINTERS + lea (%a6,%d6.w),%a2 + lea RING_PLANE_BYTES(%a2),%a3 + lea 2*RING_PLANE_BYTES(%a2),%a4 + lea 3*RING_PLANE_BYTES(%a2),%a5 + .endm + + +| Both stretches of a row: the first, the plane pointers back to the ring's +| left edge, the second. + .macro STRETCHES + movea.l L_ENTRY1(%sp),%a0 + jsr (%a0) + lea -RING_PLANE_BYTES(%a2),%a2 + lea -RING_PLANE_BYTES(%a3),%a3 + lea -RING_PLANE_BYTES(%a4),%a4 + lea -RING_PLANE_BYTES(%a5),%a5 + movea.l L_ENTRY2(%sp),%a0 + jsr (%a0) + .endm + + +| The next ring row (wrapping), and loop while rows are left. + .macro NEXT_ROW loop + lea RING_ROW_BYTES(%a6),%a6 + cmpa.l L_END(%sp),%a6 + bne.s 1f + movea.l L_BASE(%sp),%a6 +1: + subq.w #1,%d5 + bne.w \loop + .endm + + +| a0 = \end - (d0 groups of \bytes each), into \slot. + .macro ENTRY end, bytes, slot + mulu.w #\bytes,%d0 + lea \end(%pc),%a0 + suba.l %d0,%a0 + move.l %a0,\slot(%sp) + .endm + + .text + + .globl _jlStTileLayerBlitRows + +_jlStTileLayerBlitRows: + movem.l %d2-%d7/%a2-%a6,-(%sp) + lea -LOCALS(%sp),%sp + + movea.l A_RING(%sp),%a0 + move.l %a0,L_BASE(%sp) + moveq #0,%d0 + move.w A_RINGPX(%sp),%d0 + mulu.w #RING_ROW_BYTES,%d0 + add.l %a0,%d0 + move.l %d0,L_END(%sp) + moveq #0,%d0 + move.w A_RINGY(%sp),%d0 + mulu.w #RING_ROW_BYTES,%d0 + lea (%a0,%d0.l),%a6 + movea.l A_DST(%sp),%a1 + move.w A_ROWS(%sp),%d5 + move.w A_RINGX(%sp),%d6 + move.w %d6,%d7 + and.w #15,%d7 | s + lsr.w #4,%d6 | w + tst.w %d7 + beq.w .Lplain + cmp.w #LEFT_MAX_SHIFT,%d7 + bls.s .Lleft + + neg.w %d7 + add.w #16,%d7 | 16 - s + moveq #GROUPS,%d0 + sub.w %d6,%d0 + ENTRY .LrightEnd, RIGHT_GROUP_BYTES, L_ENTRY1 + move.w %d6,%d0 + ENTRY .LrightEnd, RIGHT_GROUP_BYTES, L_ENTRY2 + bra.s .LshiftRows + +.Lleft: + moveq #GROUPS,%d0 + sub.w %d6,%d0 + ENTRY .LleftEnd, LEFT_GROUP_BYTES, L_ENTRY1 + move.w %d6,%d0 + ENTRY .LleftEnd, LEFT_GROUP_BYTES, L_ENTRY2 + +.LshiftRows: + add.w %d6,%d6 | byte offset of P[w] +.LshiftRow: + PLANE_POINTERS + move.w (%a2)+,%d0 + swap %d0 + move.w (%a3)+,%d1 + swap %d1 + move.w (%a4)+,%d2 + swap %d2 + move.w (%a5)+,%d3 + swap %d3 + STRETCHES + NEXT_ROW .LshiftRow + bra.w .Ldone + +.Lplain: + moveq #GROUPS,%d0 + tst.w %d6 + beq.s 1f + moveq #RING_WORDS,%d0 + sub.w %d6,%d0 | 21 - w +1: + move.w %d0,%d1 + ENTRY .LplainEnd, PLAIN_GROUP_BYTES, L_ENTRY1 + moveq #GROUPS,%d0 + sub.w %d1,%d0 + ENTRY .LplainEnd, PLAIN_GROUP_BYTES, L_ENTRY2 + add.w %d6,%d6 +.LplainRow: + PLANE_POINTERS + STRETCHES + NEXT_ROW .LplainRow + +.Ldone: + lea LOCALS(%sp),%sp + movem.l (%sp)+,%d2-%d7/%a2-%a6 + rts + + +| The unrolled stretches: a run of n groups is entered n groups before the +| rts that ends it. +.LleftRun: + .rept GROUPS + LEFT_PLANE %a2, %d0 + LEFT_PLANE %a3, %d1 + LEFT_PLANE %a4, %d2 + LEFT_PLANE %a5, %d3 + .endr +.LleftEnd: + rts + +.LrightRun: + .rept GROUPS + RIGHT_PLANE %a2, %d0 + RIGHT_PLANE %a3, %d1 + RIGHT_PLANE %a4, %d2 + RIGHT_PLANE %a5, %d3 + .endr +.LrightEnd: + rts + +.LplainRun: + .rept GROUPS + move.w (%a2)+,(%a1)+ + move.w (%a3)+,(%a1)+ + move.w (%a4)+,(%a1)+ + move.w (%a5)+,(%a1)+ + .endr +.LplainEnd: + rts + + .if (.LleftEnd - .LleftRun) - GROUPS * LEFT_GROUP_BYTES + .error "LEFT_GROUP_BYTES does not match the emitted group" + .endif + .if (.LrightEnd - .LrightRun) - GROUPS * RIGHT_GROUP_BYTES + .error "RIGHT_GROUP_BYTES does not match the emitted group" + .endif + .if (.LplainEnd - .LplainRun) - GROUPS * PLAIN_GROUP_BYTES + .error "PLAIN_GROUP_BYTES does not match the emitted group" + .endif diff --git a/src/atarist/ste/blitter.c b/src/atarist/ste/blitter.c index f0b5238..b10f62d 100644 --- a/src/atarist/ste/blitter.c +++ b/src/atarist/ste/blitter.c @@ -17,6 +17,7 @@ // burst, where a HOG-mode blit would hold them off for the whole fill. #include "stPlanar.h" +#include "stTileLayer.h" #include "steBlitter.h" // BLiTTER registers. The address and count registers are consumed as a @@ -43,11 +44,16 @@ #define BLT_OP_SOURCE 0x03u #define BLT_OP_ONES 0x0Fu #define BLT_NO_SKEW 0x00u +#define BLT_SKEW_FXSR 0x80u // read a source word more at each line's start #define BLT_MASK_ALL 0xFFFFu #define BLT_PLANES 4 #define BLT_WORD_BYTES 2 +#define BLT_WORD_BITS 16u +#define BLT_ROW_GROUPS (ST_BYTES_PER_ROW / ST_BYTES_PER_GROUP) static inline void bltRun(void); +static void bltTileLayerRun(const uint8_t *src, uint8_t *dst, uint16_t words, uint16_t rows, uint16_t extraRead); +static void bltTileLayerStretch(const uint8_t *srcRow, uint8_t *dstRow, uint16_t rows, uint16_t w, uint16_t words1, uint16_t extraRead); // Start the programmed blit (LINE_NUM $8A3C: BUSY, shared mode, halftone @@ -62,7 +68,30 @@ static inline void bltRun(void) { } -void stBlitCopyRows(uint8_t *dst, const uint8_t *src, uint16_t rowBytes, uint16_t h) { +// One run of a tile-layer band plane: `words` band words from src's words +// (one more read when extraRead primes a shifted line), rows rows. +static void bltTileLayerRun(const uint8_t *src, uint8_t *dst, uint16_t words, uint16_t rows, uint16_t extraRead) { + BLT_SRC_YINC = (int16_t)(ST_RING_ROW_BYTES - (words - 1u + extraRead) * BLT_WORD_BYTES); + BLT_DST_YINC = (int16_t)(ST_BYTES_PER_ROW - (words - 1u) * ST_BYTES_PER_GROUP); + BLT_X_COUNT = words; + BLT_SRC_ADDR = (uint32_t)src; + BLT_DST_ADDR = (uint32_t)dst; + BLT_Y_COUNT = rows; + bltRun(); +} + + +// A plane's rows of one vertical stretch: the run from P[w] (words1 band +// words), then the run from P[0] (the rest). +static void bltTileLayerStretch(const uint8_t *srcRow, uint8_t *dstRow, uint16_t rows, uint16_t w, uint16_t words1, uint16_t extraRead) { + bltTileLayerRun(srcRow + w * BLT_WORD_BYTES, dstRow, words1, rows, extraRead); + if (words1 < BLT_ROW_GROUPS) { + bltTileLayerRun(srcRow, dstRow + words1 * ST_BYTES_PER_GROUP, (uint16_t)(BLT_ROW_GROUPS - words1), rows, extraRead); + } +} + + +void jlStBlitCopyRows(uint8_t *dst, const uint8_t *src, uint16_t rowBytes, uint16_t h) { int16_t rowStep; rowStep = (int16_t)(ST_BYTES_PER_ROW - rowBytes + BLT_WORD_BYTES); @@ -84,7 +113,7 @@ void stBlitCopyRows(uint8_t *dst, const uint8_t *src, uint16_t rowBytes, uint16_ } -void stBlitFillRect(uint8_t *base, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { +void jlStBlitFillRect(uint8_t *base, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { uint16_t groupFirst; uint16_t groups; uint16_t leftMask; @@ -116,3 +145,51 @@ void stBlitFillRect(uint8_t *base, int16_t x, int16_t y, int16_t w, int16_t h, u dst += BLT_WORD_BYTES; } } + + +// Word k of a band plane row is (P[w + k] : P[w + k + 1]) >> (16 - s), P being +// that plane's ring words, w = ringX / 16, s = ringX & 15: the chip's skew +// 16 - s, with a forced extra source read priming each line so its first +// output sees two words. From P[w] the words run to the ring row's end and +// on from P[0], so a row is two runs; the copy of P[0] after each STe plane +// row (stTileLayer.h) is the word past the first run's last, so neither run +// reads across the wrap. The band's rows wrap at the ring's last row too: +// per plane, a stretch from ringY and a stretch from row 0. +void jlStBlitTileLayerRows(const uint8_t *ring, uint8_t *dst, uint16_t ringX, uint16_t ringY, uint16_t rows, uint16_t ringPxRows) { + const uint8_t *srcRow; + uint8_t *dstRow; + uint16_t w; + uint16_t s; + uint16_t words1; + uint16_t extraRead; + uint16_t rows1; + uint8_t plane; + + w = (uint16_t)(ringX >> 4); + s = (uint16_t)(ringX & (BLT_WORD_BITS - 1u)); + words1 = (uint16_t)(ST_RING_WORDS - w); + if (words1 > BLT_ROW_GROUPS) { + words1 = BLT_ROW_GROUPS; + } + rows1 = (uint16_t)(ringPxRows - ringY); + if (rows1 > rows) { + rows1 = rows; + } + extraRead = (s != 0u) ? 1u : 0u; + BLT_SRC_XINC = BLT_WORD_BYTES; + BLT_DST_XINC = ST_BYTES_PER_GROUP; + BLT_ENDMASK1 = BLT_MASK_ALL; + BLT_ENDMASK2 = BLT_MASK_ALL; + BLT_ENDMASK3 = BLT_MASK_ALL; + BLT_HOP = BLT_HOP_SOURCE; + BLT_OP = BLT_OP_SOURCE; + BLT_SKEW = (s != 0u) ? (uint8_t)((BLT_WORD_BITS - s) | BLT_SKEW_FXSR) : BLT_NO_SKEW; + for (plane = 0; plane < BLT_PLANES; plane++) { + srcRow = ring + (uint32_t)ringY * ST_RING_ROW_BYTES + plane * ST_RING_PLANE_BYTES; + dstRow = dst + plane * BLT_WORD_BYTES; + bltTileLayerStretch(srcRow, dstRow, rows1, w, words1, extraRead); + if (rows1 < rows) { + bltTileLayerStretch(ring + plane * ST_RING_PLANE_BYTES, dstRow + (uint32_t)rows1 * ST_BYTES_PER_ROW, (uint16_t)(rows - rows1), w, words1, extraRead); + } + } +} diff --git a/src/atarist/ste/machine.c b/src/atarist/ste/machine.c index 5fc687b..7dc759d 100644 --- a/src/atarist/ste/machine.c +++ b/src/atarist/ste/machine.c @@ -55,6 +55,41 @@ const char *jlpLastError(void) { } +bool jlSteMachineCheck(void) { + if (gChecked) { + return gIsSte; + } + (void)Supexec(readCookies); + gIsSte = (gCookieMch >> MCH_FAMILY_SHIFT) == MCH_FAMILY_STE && (gCookieSnd & SND_DMA_8BIT) != 0; + gMegaSte = gCookieMch == MCH_MEGA_STE; + gChecked = true; + if (!gIsSte) { + gError = "this is the STe build of the program: it needs an Atari STe or Mega STe (run the ST build here)"; + } + return gIsSte; +} + + +bool jlSteMachineHasPads(void) { + return gIsSte && !gMegaSte; +} + + +void jlSteMachineRestoreSpeed(void) { + if (gMegaSte) { + *MEGASTE_CPU_CTRL = gSavedCpuCtrl; + } +} + + +void jlSteMachineSetFullSpeed(void) { + if (gMegaSte) { + gSavedCpuCtrl = *MEGASTE_CPU_CTRL; + *MEGASTE_CPU_CTRL = MEGASTE_FULL_SPEED; + } +} + + // Supexec worker: the jar lives in supervisor-only low memory. static long readCookies(void) { const int32_t *jar; @@ -72,38 +107,3 @@ static long readCookies(void) { } return 0; } - - -bool steMachineCheck(void) { - if (gChecked) { - return gIsSte; - } - (void)Supexec(readCookies); - gIsSte = (gCookieMch >> MCH_FAMILY_SHIFT) == MCH_FAMILY_STE && (gCookieSnd & SND_DMA_8BIT) != 0; - gMegaSte = gCookieMch == MCH_MEGA_STE; - gChecked = true; - if (!gIsSte) { - gError = "this is the STe build of the program: it needs an Atari STe or Mega STe (run the ST build here)"; - } - return gIsSte; -} - - -bool steMachineHasPads(void) { - return gIsSte && !gMegaSte; -} - - -void steMachineRestoreSpeed(void) { - if (gMegaSte) { - *MEGASTE_CPU_CTRL = gSavedCpuCtrl; - } -} - - -void steMachineSetFullSpeed(void) { - if (gMegaSte) { - gSavedCpuCtrl = *MEGASTE_CPU_CTRL; - *MEGASTE_CPU_CTRL = MEGASTE_FULL_SPEED; - } -} diff --git a/src/atarist/ste/pads.c b/src/atarist/ste/pads.c deleted file mode 100644 index bfad0e5..0000000 --- a/src/atarist/ste/pads.c +++ /dev/null @@ -1,81 +0,0 @@ -// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. -// See LICENSE.md for license information. -// SPDX-License-Identifier: MIT -// -// The 1040 STe's enhanced joystick ports, read for Atari Jaguar pads (see -// stePads.h). A Jaguar pad's buttons are a matrix of rows. A write to the -// select register picks one row of each pad: a cleared bit picks it, bits -// 0-3 for pad A's rows and 4-7 for pad B's. Then, all active low, the high -// byte of the directions register holds row 0's directions (bits 8-11 for -// pad A, 12-15 for pad B: up, down, left, right), and the buttons register -// one fire line per pad (bit 1 for pad A, bit 3 for pad B), which is the -// pad's A button in row 0 and its B button in row 1. - -#include <stdbool.h> -#include <stdint.h> - -#include "joey/input.h" -#include "stePads.h" - -#define STE_PAD_BUTTONS ((volatile uint16_t *)0xFFFF9200L) -#define STE_PAD_SELECT ((volatile uint16_t *)0xFFFF9202L) -#define STE_PAD_DIRECTIONS ((volatile uint16_t *)0xFFFF9202L) - -// Select values: row 0 (directions, A) and row 1 (B) of both pads, or none. -#define STE_PAD_ROW_0 0xFFEEu -#define STE_PAD_ROW_1 0xFFDDu -#define STE_PAD_ROW_NONE 0xFFFFu - -// A pad's four direction bits, where the directions register holds them. -#define STE_PAD_A_SHIFT 8 -#define STE_PAD_B_SHIFT 12 -#define STE_PAD_DIR_MASK 0x0Fu -#define STE_PAD_UP 0x01u -#define STE_PAD_DOWN 0x02u -#define STE_PAD_LEFT 0x04u -#define STE_PAD_RIGHT 0x08u - -// Each pad's fire line in the buttons register. -#define STE_PAD_A_FIRE 0x0002u -#define STE_PAD_B_FIRE 0x0008u - -static void padSet(StePadT *pad, uint16_t held, bool buttonA, bool buttonB); - - -// One pad from its held direction bits and buttons. Holding both -// directions of an axis reads as the positive one, as on the IKBD ports. -static void padSet(StePadT *pad, uint16_t held, bool buttonA, bool buttonB) { - pad->axisX = 0; - if ((held & STE_PAD_LEFT) != 0u) { - pad->axisX = JL_JOYSTICK_AXIS_MIN; - } - if ((held & STE_PAD_RIGHT) != 0u) { - pad->axisX = JL_JOYSTICK_AXIS_MAX; - } - pad->axisY = 0; - if ((held & STE_PAD_UP) != 0u) { - pad->axisY = JL_JOYSTICK_AXIS_MIN; - } - if ((held & STE_PAD_DOWN) != 0u) { - pad->axisY = JL_JOYSTICK_AXIS_MAX; - } - pad->buttonA = buttonA; - pad->buttonB = buttonB; - pad->used = held != 0u || buttonA || buttonB; -} - - -void stePadsRead(StePadT *padA, StePadT *padB) { - uint16_t directions; - uint16_t row0; - uint16_t row1; - - *STE_PAD_SELECT = STE_PAD_ROW_0; - directions = (uint16_t)~*STE_PAD_DIRECTIONS; - row0 = (uint16_t)~*STE_PAD_BUTTONS; - *STE_PAD_SELECT = STE_PAD_ROW_1; - row1 = (uint16_t)~*STE_PAD_BUTTONS; - *STE_PAD_SELECT = STE_PAD_ROW_NONE; - padSet(padA, (uint16_t)((directions >> STE_PAD_A_SHIFT) & STE_PAD_DIR_MASK), (row0 & STE_PAD_A_FIRE) != 0u, (row1 & STE_PAD_A_FIRE) != 0u); - padSet(padB, (uint16_t)((directions >> STE_PAD_B_SHIFT) & STE_PAD_DIR_MASK), (row0 & STE_PAD_B_FIRE) != 0u, (row1 & STE_PAD_B_FIRE) != 0u); -} diff --git a/src/atarist/ste/steBlitter.h b/src/atarist/ste/steBlitter.h index 06c3860..228e7c7 100644 --- a/src/atarist/ste/steBlitter.h +++ b/src/atarist/ste/steBlitter.h @@ -12,10 +12,15 @@ // Copy h rows of rowBytes bytes (a whole number of 16-pixel groups) from // src to dst; both rows are ST_BYTES_PER_ROW apart. -void stBlitCopyRows(uint8_t *dst, const uint8_t *src, uint16_t rowBytes, uint16_t h); +void jlStBlitCopyRows(uint8_t *dst, const uint8_t *src, uint16_t rowBytes, uint16_t h); // Fill the on-surface rect (x, y, w, h), w and h >= 1, of an interleaved // planar buffer with colour index color. -void stBlitFillRect(uint8_t *base, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); +void jlStBlitFillRect(uint8_t *base, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); + +// A tile layer's band (rows rows, from ring pixel row ringY and column ringX, +// both wrapping) from its plane-separated ring into the interleaved band at +// dst; stTileLayer.c has the ring's STe layout and the contract. +void jlStBlitTileLayerRows(const uint8_t *ring, uint8_t *dst, uint16_t ringX, uint16_t ringY, uint16_t rows, uint16_t ringPxRows); #endif diff --git a/src/atarist/ste/steMachine.h b/src/atarist/ste/steMachine.h index 47d97b7..a441029 100644 --- a/src/atarist/ste/steMachine.h +++ b/src/atarist/ste/steMachine.h @@ -15,18 +15,18 @@ // True on an STe or Mega STe. Reads the TOS cookie jar once (via Supexec, // so user or supervisor mode); later calls return the same answer. Both // jlpInit and jlpAudioInit ask, in whichever order a program calls them. -bool steMachineCheck(void); +bool jlSteMachineCheck(void); // True on a 1040 STe, which has the two enhanced joystick ports the // Jaguar pads plug into (src/atarist/ste/pads.c); a Mega STe has none, so -// their registers are left alone there. Answers once steMachineCheck has +// their registers are left alone there. Answers once jlSteMachineCheck has // run, which jlpInit does first. -bool steMachineHasPads(void); +bool jlSteMachineHasPads(void); // Mega STe only (a no-op on an STe): run the CPU at 16 MHz with the cache // on for the program's lifetime, and put back what TOS had on exit. Both // need supervisor mode, which jlpInit holds between them. -void steMachineRestoreSpeed(void); -void steMachineSetFullSpeed(void); +void jlSteMachineRestoreSpeed(void); +void jlSteMachineSetFullSpeed(void); #endif diff --git a/src/atarist/ste/stePads.h b/src/atarist/ste/stePads.h index d389d76..afb8c27 100644 --- a/src/atarist/ste/stePads.h +++ b/src/atarist/ste/stePads.h @@ -3,26 +3,61 @@ // SPDX-License-Identifier: MIT // // The 1040 STe's two enhanced joystick ports, read for an Atari Jaguar pad -// in each (src/atarist/ste/pads.c). Only on a machine that has them -// (steMachineHasPads), in supervisor mode, which jlpInit holds. +// in each. Only on a machine that has them (jlSteMachineHasPads), in +// supervisor mode, which jlpInit holds. Inline: the read is a handful of +// register accesses that input.c's poll makes every frame. +// +// A Jaguar pad's buttons are a matrix of rows. A write to the select +// register picks one row of each pad: a cleared bit picks it, bits 0-3 for +// pad A's rows and 4-7 for pad B's. Then, all active low, the high byte of +// the directions register holds row 0's directions (bits 8-11 for pad A, +// 12-15 for pad B: up, down, left, right), and the buttons register one +// fire line per pad (bit 1 for pad A, bit 3 for pad B), which is the pad's +// A button in row 0 and its B button in row 1. #ifndef JOEY_STE_PADS_H #define JOEY_STE_PADS_H -#include <stdbool.h> #include <stdint.h> -// One pad as JoeyLib's two-button stick: its directions on each axis -// (JL_JOYSTICK_AXIS_MIN, 0 or JL_JOYSTICK_AXIS_MAX), its A and B buttons, -// and whether anything on it is held. -typedef struct { - int8_t axisX; - int8_t axisY; - bool buttonA; - bool buttonB; - bool used; -} StePadT; +#define STE_PAD_BUTTONS ((volatile uint16_t *)0xFFFF9200L) +#define STE_PAD_SELECT ((volatile uint16_t *)0xFFFF9202L) +#define STE_PAD_DIRECTIONS ((volatile uint16_t *)0xFFFF9202L) -// Reads the pads in both ports. -void stePadsRead(StePadT *padA, StePadT *padB); +// Select values: row 0 (directions, A) and row 1 (B) of both pads, or none. +#define STE_PAD_ROW_0 0xFFEEu +#define STE_PAD_ROW_1 0xFFDDu +#define STE_PAD_ROW_NONE 0xFFFFu + +// A pad's four direction bits, where the directions register holds them, +// and each bit in them. +#define STE_PAD_A_SHIFT 8 +#define STE_PAD_B_SHIFT 12 +#define STE_PAD_DIR_MASK 0x0Fu +#define STE_PAD_UP 0x01u +#define STE_PAD_DOWN 0x02u +#define STE_PAD_LEFT 0x04u +#define STE_PAD_RIGHT 0x08u + +// Each pad's fire line in the buttons register. +#define STE_PAD_A_FIRE 0x0002u +#define STE_PAD_B_FIRE 0x0008u + +// Both pads as read, active high: row 0's directions and fire lines (the A +// buttons) and row 1's fire lines (the B buttons). +typedef struct { + uint16_t directions; + uint16_t row0; + uint16_t row1; +} StePadsT; + + +static inline __attribute__((always_inline)) void stePadsRead(StePadsT *pads) { + *STE_PAD_SELECT = STE_PAD_ROW_0; + pads->directions = (uint16_t)~*STE_PAD_DIRECTIONS; + pads->row0 = (uint16_t)~*STE_PAD_BUTTONS; + *STE_PAD_SELECT = STE_PAD_ROW_1; + pads->row1 = (uint16_t)~*STE_PAD_BUTTONS; + *STE_PAD_SELECT = STE_PAD_ROW_NONE; +} #endif diff --git a/src/atarist/superExit.s b/src/atarist/superExit.s index 54a7fa3..5ae22f0 100644 --- a/src/atarist/superExit.s +++ b/src/atarist/superExit.s @@ -28,12 +28,12 @@ | the entry SP is parked in a3. .text - .globl _stLeaveSuper + .globl _jlStLeaveSuper -| void stLeaveSuper(void *savedSsp); +| void jlStLeaveSuper(void *savedSsp); | Must be called in supervisor mode with savedSsp as returned by | Super(0L). Returns in user mode. -_stLeaveSuper: +_jlStLeaveSuper: movel %d3,%sp@- | 8 bytes of our own movel %a3,%sp@- movel %sp@(12),%d3 | savedSsp diff --git a/src/atarist/tickIsr.s b/src/atarist/tickIsr.s index 518475c..dc2ad8b 100644 --- a/src/atarist/tickIsr.s +++ b/src/atarist/tickIsr.s @@ -6,10 +6,10 @@ | the plain ST build's alone) so the STe links it without the pump. | | MFP Timer C (200 Hz, TOS's own system tick) for jlAudioTickRegister: -| runs the C pacer (stTickRun in audio.c), which calls the program's tick +| runs the C pacer (jlStTickRun in audio.c), which calls the program's tick | as often as its rate is due, then goes on to TOS's own Timer C handler | -- which keeps the system clock and ends the interrupt -- by pushing its -| address (_gStTickOldVec) and returning into it with the exception frame +| address (_jlgStTickOldVec) and returning into it with the exception frame | untouched. The C call may use d0-d1/a0-a1, so they are kept here. | | It first lowers the interrupt mask from the 6 the MFP's interrupt raised it @@ -24,14 +24,14 @@ | gcc driver. .text - .globl _stTickIsr + .globl _jlStTickIsr .equ SR_ABOVE_TIMER_C, 0x2500 | supervisor, interrupt mask 5 -_stTickIsr: +_jlStTickIsr: move.w #SR_ABOVE_TIMER_C,%sr movem.l %d0-%d1/%a0-%a1,-(%sp) - jsr _stTickRun + jsr _jlStTickRun movem.l (%sp)+,%d0-%d1/%a0-%a1 - move.l _gStTickOldVec,-(%sp) + move.l _jlgStTickOldVec,-(%sp) rts diff --git a/src/atarist/tileMap.s b/src/atarist/tileMap.s index 31e6954..c71283e 100644 --- a/src/atarist/tileMap.s +++ b/src/atarist/tileMap.s @@ -21,7 +21,7 @@ | constants (row r at r*160), so the group pointer only moves once | per pair. | -| void stTileMapPasteMovep(uint8_t *rowBase, const uint8_t *tiles, +| void jlStTileMapPasteMovep(uint8_t *rowBase, const uint8_t *tiles, | const uint8_t *map, uint16_t wTiles, | uint16_t hTiles, uint16_t headOdd); | @@ -33,7 +33,7 @@ | walk. wTiles/hTiles >= 1 (jlTileMapPaste validated). | | cdecl args land in 4-byte slots (uint16_t value at slot+2), same -| convention as surface68kStTileFill8x8 (lineSpan.s). d2-d7/a2-a6 are +| convention as jlSurface68kStTileFill8x8 (lineSpan.s). d2-d7/a2-a6 are | callee-save; register lists (not hand masks) keep the predec movem | mask correct. | @@ -104,9 +104,9 @@ .text - .globl _stTileMapPasteMovep + .globl _jlStTileMapPasteMovep -_stTileMapPasteMovep: +_jlStTileMapPasteMovep: movem.l %d2-%d6/%a2-%a4,-(%sp) movea.l MP_BASE(%sp),%a4 | a4 = rowBase @@ -158,18 +158,18 @@ _stTileMapPasteMovep: | this dodges the Phase-10 per-call-asm 4x loss (that shape paid a | movem prologue + stacked args + in-asm derive + dbra per call). | -| stTilePasteMovepRegs: a0 = tile src (EVEN, C guards), a1 = screen +| jlStTilePasteMovepRegs: a0 = tile src (EVEN, C guards), a1 = screen | dst byte (either parity). Clobbers d0, a0. -| stTileCopyMovepRegs: a0 = screen src byte, a1 = screen dst byte +| jlStTileCopyMovepRegs: a0 = screen src byte, a1 = screen dst byte | (either parity both sides). Clobbers d0. -| stTileSnapMovepRegs: a0 = screen src byte (either parity), a1 = +| jlStTileSnapMovepRegs: a0 = screen src byte (either parity), a1 = | tile out ptr (EVEN, C guards). Clobbers d0, a1. - .globl _stTilePasteMovepRegs - .globl _stTileCopyMovepRegs - .globl _stTileSnapMovepRegs + .globl _jlStTilePasteMovepRegs + .globl _jlStTileCopyMovepRegs + .globl _jlStTileSnapMovepRegs -_stTilePasteMovepRegs: +_jlStTilePasteMovepRegs: MTR_PASTE_ROW 0 MTR_PASTE_ROW 160 MTR_PASTE_ROW 320 @@ -181,7 +181,7 @@ _stTilePasteMovepRegs: rts -_stTileCopyMovepRegs: +_jlStTileCopyMovepRegs: MTR_COPY_ROW 0 MTR_COPY_ROW 160 MTR_COPY_ROW 320 @@ -193,7 +193,7 @@ _stTileCopyMovepRegs: rts -_stTileSnapMovepRegs: +_jlStTileSnapMovepRegs: MTR_SNAP_ROW 0 MTR_SNAP_ROW 160 MTR_SNAP_ROW 320 diff --git a/src/blank/blank.c b/src/blank/blank.c index dc46819..0609e2c 100644 --- a/src/blank/blank.c +++ b/src/blank/blank.c @@ -27,10 +27,17 @@ // To ACCELERATE a specific op later (e.g. a hardware blit for fillRect), add // JL_HAS_FILL_RECT to your platform.h block and a jlpFillRect() here; the core // will call yours instead of the generic. Nothing else changes. +// +// Every function and variable the library exports starts with jl, so none +// can clash with a program's own names or a system library's: jl<Name> for +// the public API, jlp<Op> for the HAL, jl<Port><Name> for a port's own +// helpers that other files call (jlStBlitCopyRows, jlIigsSccIsr), and +// jlg<Name> for a variable other files share (jlgStage, jlgX68kTicks). Keep +// the rest static. #include "port.h" #include "surfaceInternal.h" // jlSurfaceT: ->pixels (chunky), ->scb, ->palette -#include "inputInternal.h" // gKeyState[] etc. for jlpInputPoll +#include "inputInternal.h" // jlgKeyState[] etc. for jlpInputPoll // ----- Lifecycle ----------------------------------------------------------- @@ -88,7 +95,7 @@ void jlpInputShutdown(void) { // Refresh the core-owned key-state array each frame. Write true into -// gKeyState[key] for every currently-held key (see joey/input.h for the jlKeyE +// jlgKeyState[key] for every currently-held key (see joey/input.h for the jlKeyE // codes); keys you can't map simply stay false. The cross-platform jlInputPoll // and joystick code build on this. // @@ -99,7 +106,7 @@ void jlpInputShutdown(void) { // buffer the bytes in your own volatile ring and drain it here instead -- // jlInputCharPush is not interrupt-safe (see the DOS and ST ports). void jlpInputPoll(void) { - // TODO: read your keyboard and set gKeyState[...] = true for held keys. + // TODO: read your keyboard and set jlgKeyState[...] = true for held keys. // TODO: feed translated characters to jlInputCharPush(ch). } diff --git a/src/codegen/spriteCompile.c b/src/codegen/spriteCompile.c index f658b8c..2154dc8 100644 --- a/src/codegen/spriteCompile.c +++ b/src/codegen/spriteCompile.c @@ -17,7 +17,7 @@ #include "port.h" #include "spriteInternal.h" #include "spriteStage.h" -#include "surfaceInternal.h" // coreSetError +#include "surfaceInternal.h" // jlCoreSetError @@ -30,7 +30,7 @@ * channel). It was REWRITTEN rather than patched: the emitted routines are now * clean C-ABI functions invoked through ordinary C function pointers, so the * compiler emits the call and there is no self-modifying code or manual address - * math -- see spriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c. + * math -- see jlSpriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c. * Codegen is ON by default; set JOEY_IIGS_SPRITE_CODEGEN to 0 to force the * interpreter. */ /* claudeDocs/PERF-AUDIT.md #77 -- ROOT-CAUSED 2026-07-04, FIXED toolchain-side @@ -78,10 +78,10 @@ bool jlSpriteCompile(jlSpriteT *sp) { // One greedy pass: emit every included variant straight into `staging` // and record its slot-relative offset. No separate measure pass -- the // emitter runs once per variant instead of twice. - total = spriteStageAndPlan(staging, sp, false); + total = jlSpriteStageAndPlan(staging, sp, false); if (total == 0) { /* Platforms whose emitter returns 0 for every (shift, op) have - * no compiled bytes -- spriteCompiledDraw / SaveUnder / + * no compiled bytes -- jlSpriteCompiledDraw / SaveUnder / * RestoreUnder would dereference a degenerate slot or chunky * shadow. Bail so sp->slot stays NULL and the dispatcher * routes through the interpreted jlpSprite*Planes ops path. */ @@ -89,15 +89,15 @@ bool jlSpriteCompile(jlSpriteT *sp) { return false; } - slot = codegenArenaAlloc(total); + slot = jlCodegenArenaAlloc(total); if (slot == NULL) { // Fragmented arena: total free space may suffice even though no // single hole does (sprite create/destroy churn across levels). // Honor the arena's compact-and-retry contract // (codegenArenaInternal.h) once before giving up -- failure-path // only, zero hot-path cost (#5). - codegenArenaCompact(); - slot = codegenArenaAlloc(total); + jlCodegenArenaCompact(); + slot = jlCodegenArenaAlloc(total); } if (slot == NULL) { // Degrade tier: re-stage with only the cheap byte-aligned DRAW @@ -106,12 +106,12 @@ bool jlSpriteCompile(jlSpriteT *sp) { // pre-Phase-1 builds compiled instead of losing the whole sprite to // the interpreter. Deterministic (depends only on the sprite and // the mask); the second emit only happens on this arena-full path. - total = spriteStageAndPlan(staging, sp, true); + total = jlSpriteStageAndPlan(staging, sp, true); if (total != 0) { - slot = codegenArenaAlloc(total); + slot = jlCodegenArenaAlloc(total); if (slot == NULL) { - codegenArenaCompact(); - slot = codegenArenaAlloc(total); + jlCodegenArenaCompact(); + slot = jlCodegenArenaAlloc(total); } } } @@ -120,7 +120,7 @@ bool jlSpriteCompile(jlSpriteT *sp) { // ignored is diagnosable via jlLastError. Clear the staged offsets // (sp->slot stays NULL, so the dispatcher interprets regardless). memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets)); - coreSetError("jlSpriteCompile: codegen arena full even after compaction; raise jlConfigT.codegenBytes or destroy unused sprites"); + jlCoreSetError("jlSpriteCompile: codegen arena full even after compaction; raise jlConfigT.codegenBytes or destroy unused sprites"); jlpBigFree(staging); return false; } @@ -132,10 +132,10 @@ bool jlSpriteCompile(jlSpriteT *sp) { jlpCodeWritten(codegenArenaSlotBase(slot), total); sp->slot = slot; // Registry (#34): every creation path (including jlSpriteBankLoad, - // #32) already registered sp via spriteInitFields; this idempotent + // #32) already registered sp via jlSpriteInitFields; this idempotent // re-add is a belt-and-braces guard, since compile success is the // moment sp first holds arena state that jlShutdown must reset. - spriteRegistryAdd(sp); + jlSpriteRegistryAdd(sp); jlpBigFree(staging); return true; } diff --git a/src/codegen/spriteEmitter.h b/src/codegen/spriteEmitter.h index d2d4899..8a4faf2 100644 --- a/src/codegen/spriteEmitter.h +++ b/src/codegen/spriteEmitter.h @@ -62,8 +62,8 @@ #define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K #endif -uint32_t spriteEmitDrawX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitDrawX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); // Save-under and restore-under emitters. Both copy a byte-aligned // rectangle between the destination surface and a backup buffer. The @@ -79,10 +79,10 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin // `void copy(uint32_t packed)` (packed = srcOff | (dstOff << 16)) of // unrolled MVN rows whose bank operand bytes the dispatcher patches // per call -- see spriteEmitIigs.c's header for the full contract. -uint32_t spriteEmitSaveIigs (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitSaveX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitSaveIigs (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitSaveX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); // Planar 68k emitters (Amiga). Destination addressing is across 4 // separate bitplane buffers, not a single packed-pixel surface. @@ -94,9 +94,9 @@ uint32_t spriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, // the cross-platform interpreter). SAVE/RESTORE always return 0 -- // not compiled on the Amiga yet; the interpreted jlpSpriteSavePlanes / // jlpSpriteRestorePlanes path handles them. -uint32_t spriteEmitDrawPlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitSavePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitDrawPlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitSavePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); // Word-interleaved planar 68k emitter (ST). Calling convention for // the emitted bytes: @@ -106,8 +106,8 @@ uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT // shift 1); other shifts return 0 so the cross-platform dispatcher // falls back to jlpSpriteDrawPlanes. SAVE/RESTORE always return 0 -- // not compiled on the ST; the interpreted planes path handles them. -uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); -uint32_t spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +uint32_t jlSpriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); #endif diff --git a/src/codegen/spriteStage.c b/src/codegen/spriteStage.c index cc5be55..4d70ef3 100644 --- a/src/codegen/spriteStage.c +++ b/src/codegen/spriteStage.c @@ -23,16 +23,16 @@ // Compile-time selection of the per-CPU emitter. One per-CPU // spriteEmit*.c file (src/x86, src/iigs, src/m68k) is built per // platform, but the dispatch lives in this file so jlSpriteCompile + -// spriteCompiledDraw aren't duplicated per port. +// jlSpriteCompiledDraw aren't duplicated per port. static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { #if defined(JL_CPU_X86) - return spriteEmitDrawX86(out, cap, sp, shift); + return jlSpriteEmitDrawX86(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) - return spriteEmitDrawPlanar68k(out, cap, sp, shift); + return jlSpriteEmitDrawPlanar68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_ATARIST) - return spriteEmitDrawInterleaved68k(out, cap, sp, shift); + return jlSpriteEmitDrawInterleaved68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_IIGS) - return spriteEmitDrawIigs(out, cap, sp, shift); + return jlSpriteEmitDrawIigs(out, cap, sp, shift); #else # error "jlSpriteCompile: no emitter selected for this platform" #endif @@ -46,13 +46,13 @@ static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *s // path otherwise. static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { #if defined(JL_CPU_X86) - return spriteEmitSaveX86(out, cap, sp, shift); + return jlSpriteEmitSaveX86(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) - return spriteEmitSavePlanar68k(out, cap, sp, shift); + return jlSpriteEmitSavePlanar68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_ATARIST) - return spriteEmitSaveInterleaved68k(out, cap, sp, shift); + return jlSpriteEmitSaveInterleaved68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_IIGS) - return spriteEmitSaveIigs(out, cap, sp, shift); + return jlSpriteEmitSaveIigs(out, cap, sp, shift); #else (void)out; (void)cap; @@ -65,13 +65,13 @@ static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *s static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { #if defined(JL_CPU_X86) - return spriteEmitRestoreX86(out, cap, sp, shift); + return jlSpriteEmitRestoreX86(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) - return spriteEmitRestorePlanar68k(out, cap, sp, shift); + return jlSpriteEmitRestorePlanar68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_ATARIST) - return spriteEmitRestoreInterleaved68k(out, cap, sp, shift); + return jlSpriteEmitRestoreInterleaved68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_IIGS) - return spriteEmitRestoreIigs(out, cap, sp, shift); + return jlSpriteEmitRestoreIigs(out, cap, sp, shift); #else (void)out; (void)cap; @@ -146,7 +146,7 @@ static uint32_t spriteStageOne(uint8_t *staging, uint32_t total, const jlSpriteT // image size in bytes (0 == nothing compiled). Inclusion order and caps // match the old spritePlanBuild exactly, so the staged bytes and the chosen // variant set are identical by construction (PERF-AUDIT #74). -uint32_t spriteStageAndPlan(uint8_t *staging, jlSpriteT *sp, bool alignedOnly) { +uint32_t jlSpriteStageAndPlan(uint8_t *staging, jlSpriteT *sp, bool alignedOnly) { uint32_t total; uint8_t shift; @@ -154,7 +154,7 @@ uint32_t spriteStageAndPlan(uint8_t *staging, jlSpriteT *sp, bool alignedOnly) { total = 0; #if defined(JOEYLIB_PLATFORM_IIGS) // The save/restore emitters cover these rows. - spriteContentRowsIigs(sp); + jlSpriteContentRowsIigs(sp); #endif for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) { diff --git a/src/codegen/spriteStage.h b/src/codegen/spriteStage.h index c8804bf..a2759a4 100644 --- a/src/codegen/spriteStage.h +++ b/src/codegen/spriteStage.h @@ -36,6 +36,6 @@ // total image size in bytes (0 == nothing compiled). Depends only on the // per-CPU emitters -- no arena or heap -- so it links cleanly into a host // bake tool. -uint32_t spriteStageAndPlan(uint8_t *staging, jlSpriteT *sp, bool alignedOnly); +uint32_t jlSpriteStageAndPlan(uint8_t *staging, jlSpriteT *sp, bool alignedOnly); #endif diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index b140f96..9ffe794 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -39,7 +39,7 @@ // The Atari ST shifted-phase DRAW thunk tail-jumps to this shared walker via // a baked absolute address; the .spc loader re-stamps it after load (the only // port needing any relocation -- see spritePrecompiled.h). -extern void stSpriteDrawShiftWalker(void); +extern void jlStSpriteDrawShiftWalker(void); #endif // IIgs segment placement (PERF-AUDIT #60): this TU carries NO named @@ -284,7 +284,7 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel // Shared init path (spriteInternal.h, claudeDocs/PERF-AUDIT.md #32): sets // the 0xFF/0 compiled-state sentinels and registers the sprite // for shutdown. ownsTileData true: the cel owns its buf copy. - if (!spriteInitFields(sp, buf, widthTiles, heightTiles, true)) { + if (!jlSpriteInitFields(sp, buf, widthTiles, heightTiles, true)) { jlpBigFree(sp); jlpBigFree(buf); break; @@ -343,7 +343,7 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint // One reused staging buffer for each cel's routine image (bounded to // <64 KB by the uint16 offset budget, same as the JIT). Read the image // here BEFORE building the sprite so every fallible fread happens before - // spriteInitFields registers the sprite -- no dangling registry entry. + // jlSpriteInitFields registers the sprite -- no dangling registry entry. imageBuf = (uint8_t *)jlpBigAlloc(SPC_MAX_IMAGE_BYTES); if (imageBuf == NULL) { fclose(fp); @@ -415,7 +415,7 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint jlpBigFree(buf); break; } - if (!spriteInitFields(sp, buf, widthTiles, heightTiles, true)) { + if (!jlSpriteInitFields(sp, buf, widthTiles, heightTiles, true)) { jlpBigFree(sp); jlpBigFree(buf); break; @@ -428,10 +428,10 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint slot = NULL; #if !defined(JL_SPRITE_INTERPRETED) if (imageSize > 0u) { - slot = codegenArenaAlloc(imageSize); + slot = jlCodegenArenaAlloc(imageSize); if (slot == NULL) { - codegenArenaCompact(); - slot = codegenArenaAlloc(imageSize); + jlCodegenArenaCompact(); + slot = jlCodegenArenaAlloc(imageSize); } } #endif @@ -453,7 +453,7 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint uint16_t r; for (r = 0; r < relocCount; r++) { - *(uint32_t *)(slotBase + relocOffsets[r]) = (uint32_t)(uintptr_t)stSpriteDrawShiftWalker; + *(uint32_t *)(slotBase + relocOffsets[r]) = (uint32_t)(uintptr_t)jlStSpriteDrawShiftWalker; } } #else @@ -462,7 +462,7 @@ uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint #if defined(JOEYLIB_PLATFORM_IIGS) // The rows the baked save/restore cover (the baker's staging // derived them from the same tile data). - spriteContentRowsIigs(sp); + jlSpriteContentRowsIigs(sp); #endif jlpCodeWritten(slotBase, imageSize); sp->slot = slot; diff --git a/src/core/audio.c b/src/core/audio.c index 9aba2e3..4b772e3 100644 --- a/src/core/audio.c +++ b/src/core/audio.c @@ -6,7 +6,7 @@ // gate that makes every call safe when the engine did not start. Game // code can fire-and-forget calls regardless of whether the platform // actually has audio working at runtime. jlInit starts the engine -// (audioInit) and jlShutdown stops it (audioShutdown); a program that +// (jlAudioOpen) and jlShutdown stops it (jlAudioClose); a program that // wants no audio (jlConfigT.noAudio) gets every call silent, the voice // layer's included. @@ -36,7 +36,7 @@ static uint8_t gCriticalDepth = 0u; // jlAudioCriticalEnter calls not yet e // be src itself (each sample lands at or before where it was). *phase // becomes where the next block's first sample is. Returns how many it wrote. // Factor 1 is a copy (the phase stays 0), at memmove's speed. -uint32_t audioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t factor, uint8_t *phase) { +uint32_t jlAudioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t factor, uint8_t *phase) { uint32_t i; uint32_t n; @@ -57,23 +57,14 @@ uint32_t audioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t // The smallest whole factor that brings rateHz to maxHz or under. -uint8_t audioDecimationFactor(uint16_t rateHz, uint16_t maxHz) { +uint8_t jlAudioDecimationFactor(uint16_t rateHz, uint16_t maxHz) { return (uint8_t)(((uint32_t)rateHz + maxHz - 1u) / maxHz); } // ----- Lifecycle (jlInit, jlShutdown) ----- -void audioInit(bool wanted) { - gAudioOff = !wanted; - gAudioReady = wanted && jlpAudioInit(); - if (wanted && !gAudioReady) { - jlLog("audio: the sound engine did not start; modules and effects are silent"); - } -} - - -void audioShutdown(void) { +void jlAudioClose(void) { // A playing song is stopped first, so it does not keep its voices going // and does not resume at the next jlInit. jlMusicStop(); @@ -86,6 +77,15 @@ void audioShutdown(void) { } +void jlAudioOpen(bool wanted) { + gAudioOff = !wanted; + gAudioReady = wanted && jlpAudioInit(); + if (wanted && !gAudioReady) { + jlLog("audio: the sound engine did not start; modules and effects are silent"); + } +} + + // ----- Public API ----- @@ -241,7 +241,7 @@ void jlAudioFrameTick(void) { // jlAudioVoice itself) works whether or not the engine started -- // pump it above the gAudioReady gate. With no audio wanted its // voice calls are silent and a song still runs its time. - musicPump(); + jlMusicPump(); if (!gAudioReady) { return; } diff --git a/src/core/audioInternal.h b/src/core/audioInternal.h index 2677420..eb53a22 100644 --- a/src/core/audioInternal.h +++ b/src/core/audioInternal.h @@ -14,9 +14,9 @@ // leave it off and make every audio call silent, the voice layer's too. // An engine that does not start is logged, and its modules and effects are // silent. -void audioInit(bool wanted); +void jlAudioOpen(bool wanted); // Stop it and release what the voice layer holds. Safe when it never // started. -void audioShutdown(void); +void jlAudioClose(void); #endif diff --git a/src/core/audioSfxMix.c b/src/core/audioSfxMix.c index c73055b..df68d17 100644 --- a/src/core/audioSfxMix.c +++ b/src/core/audioSfxMix.c @@ -59,13 +59,13 @@ typedef struct { uint32_t shift; // gShift } SfxRunT; -// How far under full scale a sound effect is mixed (audioSfxSetShift). +// How far under full scale a sound effect is mixed (jlAudioSfxSetShift). static uint8_t gShift = 0u; // Prototypes (file-local, alphabetical). #if defined(JL_HAS_SFX_MIX_RUN) -void audioSfxMixRun(SfxRunT *run); -#define sfxRun audioSfxMixRun +void jlAudioSfxMixRun(SfxRunT *run); +#define sfxRun jlAudioSfxMixRun #else static int interp(int8_t s0, int8_t s1, uint16_t frac); static void sfxRun(SfxRunT *run); @@ -73,8 +73,9 @@ static void sfxRun(SfxRunT *run); static void slotMix(AudioSfxSlotT *slot, volatile uint8_t *dst, uint16_t frames, RunLayoutE layout, uint8_t format); static bool streamRefill(AudioSfxSlotT *slot); +// ----- Internal (alphabetical) ----- -void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) { +void jlAudioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) { uint8_t s; for (s = 0; s < numSlots; s++) { @@ -85,7 +86,7 @@ void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slot } -void audioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSlotT *slots, uint8_t numSlots, const uint8_t *pans, uint8_t format) { +void jlAudioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSlotT *slots, uint8_t numSlots, const uint8_t *pans, uint8_t format) { uint8_t s; for (s = 0; s < numSlots; s++) { @@ -103,12 +104,12 @@ void audioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSl } -void audioSfxSetShift(uint8_t shift) { +void jlAudioSfxSetShift(uint8_t shift) { gShift = shift; } -void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) { +void jlAudioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) { if (rateHz == 0 || outputRate == 0) { return; } @@ -125,7 +126,7 @@ void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length } -void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate) { +void jlAudioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate) { if (rateHz == 0 || outputRate == 0 || fill == NULL) { return; } @@ -144,6 +145,7 @@ void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *c (void)streamRefill(slot); } +// ----- Internal helpers (alphabetical) ----- #if !defined(JL_HAS_SFX_MIX_RUN) // Linear interpolation between two adjacent signed 8-bit samples. diff --git a/src/core/audioSfxMixInternal.h b/src/core/audioSfxMixInternal.h index 59fe3dd..7cc101b 100644 --- a/src/core/audioSfxMixInternal.h +++ b/src/core/audioSfxMixInternal.h @@ -59,22 +59,22 @@ typedef struct { // Sound effects mixed shift bits under full scale from now on: a port whose // 8-bit mix holds a module's four channels at a fraction of a byte each -// (modPlayerSetU8Shift) mixes its effects at the same level, as loud as a +// (jlModPlayerSetU8Shift) mixes its effects at the same level, as loud as a // module channel (joey/audio.h's mix). 0 until a port sets it. -void audioSfxSetShift(uint8_t shift); +void jlAudioSfxSetShift(uint8_t shift); // Arm a slot for one-shot playback from a fixed PCM buffer. // `outputRate` is the HAL's mixer output rate; the helper computes // stepFp from rateHz and outputRate. Sets `active = true`. No-op // if rateHz is 0. -void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate); +void jlAudioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate); // Arm a slot for streaming playback. `fill` will be called whenever // the slot's internal prefetch buffer drains; it should fill `dst` // with up to `count` signed-8-bit samples and return the count // actually filled. Returning 0 signals end-of-stream and the slot // auto-deactivates after consuming any remaining prefetch. -void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate); +void jlAudioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate); // Overlay all active slots' contributions on top of `dst[0..len)`. // `dst` is unsigned 8-bit centered at $80. Mix is signed-to-signed @@ -86,17 +86,17 @@ void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *c // dst is `volatile` because the ST HAL's ISR consumes the buffer // from interrupt context; the DOS HAL's dst is non-volatile DMA // memory but the function accepts both via the wider qualifier. -void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots); +void jlAudioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots); // The stereo form, for a port that mixes interleaved left and right 8-bit // pairs (DOS on an SB16, the STe): each slot is added into the side its // pans[] entry reaches (both, for JL_AUDIO_PAN_CENTER), each side clamped -// as audioSfxOverlayMix clamps. pans[i] is slots[i]'s. dst holds unsigned +// as jlAudioSfxOverlayMix clamps. pans[i] is slots[i]'s. dst holds unsigned // samples (AUDIO_SFX_DST_UNSIGNED) or signed ones (AUDIO_SFX_DST_SIGNED), // each mixed in its own format and in order, a sample at a time, so a // buffer that is playing can take a sound ahead of the play position. #define AUDIO_SFX_DST_UNSIGNED 0x00u #define AUDIO_SFX_DST_SIGNED 0x80u -void audioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSlotT *slots, uint8_t numSlots, const uint8_t *pans, uint8_t format); +void jlAudioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSlotT *slots, uint8_t numSlots, const uint8_t *pans, uint8_t format); #endif diff --git a/src/core/codegenArena.c b/src/core/codegenArena.c index 70825a6..37d925f 100644 --- a/src/core/codegenArena.c +++ b/src/core/codegenArena.c @@ -16,7 +16,7 @@ // attrFixed | attrLocked | attrPage | attrNoCross gives us a fixed // page-aligned region in a single bank we can safely jump into. // The IIgs toolbox bindings live in <iigs/toolbox.h>. Handles are typed -// as void* and the owner ID is iigsMemUserId()'s (src/iigs/hal.c); the +// as void* and the owner ID is jlIigsMemUserId()'s (src/iigs/hal.c); the // Memory Manager attribute bits are port.h's. #include <iigs/toolbox.h> #endif @@ -32,14 +32,14 @@ // ----- Module state ----- // -// gCodegenBankBase / gCodegenBankAddr are non-static so the sprite +// jlgCodegenBankBase / jlgCodegenBankAddr are non-static so the sprite // dispatch reads them directly via extern instead of paying a JSL/RTL -// per draw through a wrapper. They are set once at codegenArenaInit and +// per draw through a wrapper. They are set once at jlCodegenArenaInit and // never moved (a bank is locked in place on the IIgs). Callers MUST // treat them as read-only. -uint8_t *gCodegenBankBase[CODEGEN_ARENA_BANKS_MAX]; -uint32_t gCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX]; +uint8_t *jlgCodegenBankBase[CODEGEN_ARENA_BANKS_MAX]; +uint32_t jlgCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX]; static uint32_t gBankSize[CODEGEN_ARENA_BANKS_MAX]; static ArenaSlotT *gBankFirst[CODEGEN_ARENA_BANKS_MAX]; #if defined(JOEYLIB_PLATFORM_IIGS) @@ -72,9 +72,9 @@ static bool bankAlloc(uint8_t bank, uint32_t bytes) { void *p; uint8_t raw[4]; - // The one owner ID every JoeyLib allocation shares (iigsMemUserId): + // The one owner ID every JoeyLib allocation shares (jlIigsMemUserId): // MMStartUp hands out a new one per call, and the pool runs dry. - h = NewHandle((unsigned long)bytes, iigsMemUserId(), attrFixed | attrLocked | attrPage | attrNoCross, (void *)0); + h = NewHandle((unsigned long)bytes, jlIigsMemUserId(), attrFixed | attrLocked | attrPage | attrNoCross, (void *)0); if (h == NULL) { return false; } @@ -87,17 +87,17 @@ static bool bankAlloc(uint8_t bank, uint32_t bytes) { // The 24-bit absolute address is the master pointer's raw bytes; // byte 3 (bank-high/flags) is masked off. memcpy(raw, &p, 4); - gBankHandle[bank] = h; - gCodegenBankBase[bank] = (uint8_t *)p; - gCodegenBankAddr[bank] = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2] << 16); + gBankHandle[bank] = h; + jlgCodegenBankBase[bank] = (uint8_t *)p; + jlgCodegenBankAddr[bank] = (uint32_t)raw[0] | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2] << 16); #else uint8_t *p = (uint8_t *)malloc(bytes); if (p == NULL) { return false; } - gCodegenBankBase[bank] = p; - gCodegenBankAddr[bank] = (uint32_t)(uintptr_t)p; + jlgCodegenBankBase[bank] = p; + jlgCodegenBankAddr[bank] = (uint32_t)(uintptr_t)p; #endif gBankSize[bank] = bytes; gBankFirst[bank] = newSlot((uint32_t)bank << CODEGEN_ARENA_BANK_SHIFT, bytes, false); @@ -110,7 +110,7 @@ static bool bankAlloc(uint8_t bank, uint32_t bytes) { static void bankCompact(uint8_t bank) { - uint8_t *base = gCodegenBankBase[bank]; + uint8_t *base = jlgCodegenBankBase[bank]; ArenaSlotT *slot; ArenaSlotT *next; ArenaSlotT *trailing; @@ -173,12 +173,12 @@ static void bankFree(uint8_t bank) { gBankHandle[bank] = NULL; } #else - free(gCodegenBankBase[bank]); + free(jlgCodegenBankBase[bank]); #endif - gCodegenBankBase[bank] = NULL; - gCodegenBankAddr[bank] = 0; - gBankSize[bank] = 0; - gBankFirst[bank] = NULL; + jlgCodegenBankBase[bank] = NULL; + jlgCodegenBankAddr[bank] = 0; + gBankSize[bank] = 0; + gBankFirst[bank] = NULL; } @@ -219,7 +219,7 @@ static ArenaSlotT *newSlot(uint32_t offset, uint32_t size, bool used) { // ----- Public API ----- -ArenaSlotT *codegenArenaAlloc(uint32_t bytes) { +ArenaSlotT *jlCodegenArenaAlloc(uint32_t bytes) { ArenaSlotT *slot; ArenaSlotT *remainder; uint8_t bank; @@ -254,17 +254,17 @@ ArenaSlotT *codegenArenaAlloc(uint32_t bytes) { } -uint32_t codegenArenaBytesTotal(void) { +uint32_t jlCodegenArenaBytesTotal(void) { return gTotalBytes; } -uint32_t codegenArenaBytesUsed(void) { +uint32_t jlCodegenArenaBytesUsed(void) { return gUsedBytes; } -void codegenArenaCompact(void) { +void jlCodegenArenaCompact(void) { uint8_t bank; for (bank = 0u; bank < gBankCount; bank++) { @@ -273,7 +273,7 @@ void codegenArenaCompact(void) { } -void codegenArenaFree(ArenaSlotT *slot) { +void jlCodegenArenaFree(ArenaSlotT *slot) { if (slot == NULL || gBankCount == 0u) { return; } @@ -288,7 +288,7 @@ void codegenArenaFree(ArenaSlotT *slot) { } -bool codegenArenaInit(uint32_t totalBytes) { +bool jlCodegenArenaInit(uint32_t totalBytes) { uint32_t want; if (gBankCount != 0u) { @@ -333,7 +333,7 @@ bool codegenArenaInit(uint32_t totalBytes) { } -void codegenArenaShutdown(void) { +void jlCodegenArenaShutdown(void) { uint8_t bank; for (bank = 0u; bank < gBankCount; bank++) { diff --git a/src/core/codegenArenaInternal.h b/src/core/codegenArenaInternal.h index 02d5e8f..951dc4a 100644 --- a/src/core/codegenArenaInternal.h +++ b/src/core/codegenArenaInternal.h @@ -4,7 +4,7 @@ // // Codegen arena: a pool of executable memory that holds compiled // sprite routines. Allocator is first-fit with adjacent-hole -// coalescing on free; manual compaction (codegenArenaCompact) +// coalescing on free; manual compaction (jlCodegenArenaCompact) // memmoves live slots down to consolidate free space. // // The arena is one or more BANKS. Every port but the IIgs has a single @@ -25,8 +25,8 @@ // readable, writable, AND executable. We are not running in a W^X // environment. // -// The arena is a process-singleton: codegenArenaInit creates it from -// jlConfigT.codegenBytes during jlInit; codegenArenaShutdown +// The arena is a process-singleton: jlCodegenArenaInit creates it from +// jlConfigT.codegenBytes during jlInit; jlCodegenArenaShutdown // frees it on jlShutdown. Most callers should go through the // sprite API rather than touching the arena directly. @@ -56,39 +56,39 @@ typedef struct ArenaSlotT { // Lifecycle. Returns false if totalBytes is 0 or no memory at all could be // had; a request the machine cannot fully satisfy yields what it can -// (codegenArenaBytesTotal tells), on every port. Idempotent: calling Init +// (jlCodegenArenaBytesTotal tells), on every port. Idempotent: calling Init // twice without an intervening Shutdown is a no-op. -bool codegenArenaInit(uint32_t totalBytes); -void codegenArenaShutdown(void); +bool jlCodegenArenaInit(uint32_t totalBytes); +void jlCodegenArenaShutdown(void); // First-fit allocate `bytes` of executable memory, in the first bank with // room. Returns NULL if no free slot is large enough -- the caller should -// run codegenArenaCompact and retry, or surface the failure. -ArenaSlotT *codegenArenaAlloc(uint32_t bytes); +// run jlCodegenArenaCompact and retry, or surface the failure. +ArenaSlotT *jlCodegenArenaAlloc(uint32_t bytes); // Mark the slot free and merge with adjacent free neighbors. The // caller must drop its ArenaSlotT* immediately; the struct may be // freed (if it coalesced into a neighbor). -void codegenArenaFree(ArenaSlotT *slot); +void jlCodegenArenaFree(ArenaSlotT *slot); // In every bank: walk live slots in offset order, memmove each down to // fill any preceding hole, drop all free slots from the list, and finish // with one trailing free slot covering the remaining space. Per-slot // `offset` fields are updated atomically with the memmove so any callers // resolving the slot see the new value on their next read. -void codegenArenaCompact(void); +void jlCodegenArenaCompact(void); // Where a slot's bytes are: as a pointer for the C side, and as the // 24-bit absolute address the IIgs JSLs to. The bank tables are read-only -// after codegenArenaInit and exposed directly so the per-draw hot paths +// after jlCodegenArenaInit and exposed directly so the per-draw hot paths // pay an indexed load, not a call. -extern uint8_t *gCodegenBankBase[CODEGEN_ARENA_BANKS_MAX]; -extern uint32_t gCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX]; -#define codegenArenaSlotBase(slot) (gCodegenBankBase[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK)) -#define codegenArenaSlotAddr(slot) (gCodegenBankAddr[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK)) +extern uint8_t *jlgCodegenBankBase[CODEGEN_ARENA_BANKS_MAX]; +extern uint32_t jlgCodegenBankAddr[CODEGEN_ARENA_BANKS_MAX]; +#define codegenArenaSlotBase(slot) (jlgCodegenBankBase[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK)) +#define codegenArenaSlotAddr(slot) (jlgCodegenBankAddr[(slot)->offset >> CODEGEN_ARENA_BANK_SHIFT] + ((slot)->offset & CODEGEN_ARENA_BANK_MASK)) // Occupancy across all banks. -uint32_t codegenArenaBytesUsed(void); -uint32_t codegenArenaBytesTotal(void); +uint32_t jlCodegenArenaBytesUsed(void); +uint32_t jlCodegenArenaBytesTotal(void); #endif diff --git a/src/core/debug.c b/src/core/debug.c index 3bc33b1..de086e0 100644 --- a/src/core/debug.c +++ b/src/core/debug.c @@ -15,7 +15,7 @@ // seconds of emulated floppy time, and a write can wedge indefinitely // inside SmartPort firmware retries -- one such wedge stalled the UBER // benchmark for 18+ emulated minutes. So on IIgs the lines land in a -// RAM ring instead: gJoeyLogRing/gJoeyLogRingHead are non-static so +// RAM ring instead: jlgJoeyLogRing/jlgJoeyLogRingHead are non-static so // headless-MAME Lua probes can drain the log LIVE from emulated memory // (scripts/bench-iigs.sh), and jlLogFlush() writes the accumulated ring // to joeylog.txt in one open/write/close pass (fclose is the only @@ -55,8 +55,8 @@ void jlpLogTee(const char *text); // tracks its own last-read count and detects overruns by delta > size. #define JOEY_LOG_RING_BYTES 8192u -uint8_t gJoeyLogRing[JOEY_LOG_RING_BYTES]; -uint16_t gJoeyLogRingHead = 0; +uint8_t jlgJoeyLogRing[JOEY_LOG_RING_BYTES]; +uint16_t jlgJoeyLogRingHead = 0; // Set once the ring has held JOEY_LOG_RING_BYTES: from then on it is all // valid, even after the head wraps past 65535 back to a small count. // uint8_t, not bool (ORCA-era 2-byte _Bool habit kept for the IIgs). @@ -82,17 +82,17 @@ static void ringAppend(const char *text) { #endif // Explicit read-modify-write of the global (never `++`/`+=` on a // global -- the 65816 inc-abs/DBR trap). - head = gJoeyLogRingHead; + head = jlgJoeyLogRingHead; while (*text != '\0') { - idx = (uint16_t)(head & (JOEY_LOG_RING_BYTES - 1u)); - gJoeyLogRing[idx] = (uint8_t)*text; + idx = (uint16_t)(head & (JOEY_LOG_RING_BYTES - 1u)); + jlgJoeyLogRing[idx] = (uint8_t)*text; text++; head = (uint16_t)(head + 1u); if (head == JOEY_LOG_RING_BYTES) { gJoeyLogRingFull = 1u; } } - gJoeyLogRingHead = head; + jlgJoeyLogRingHead = head; } @@ -136,7 +136,7 @@ void jlLogFlush(void) { uint16_t i; uint16_t idx; - head = gJoeyLogRingHead; + head = jlgJoeyLogRingHead; if (head == 0u && gJoeyLogRingFull == 0u) { return; } @@ -157,7 +157,7 @@ void jlLogFlush(void) { } for (i = 0; i < count; i++) { idx = (uint16_t)((uint16_t)(start + i) & (JOEY_LOG_RING_BYTES - 1u)); - fputc((int)gJoeyLogRing[idx], fp); + fputc((int)jlgJoeyLogRing[idx], fp); } fclose(fp); } @@ -173,8 +173,8 @@ void jlLogFlush(void) { // with "w" now, so the on-disk file still ends up truncated-to-current // on the next flush. void jlLogReset(void) { - gJoeyLogRingHead = 0; - gJoeyLogRingFull = 0u; + jlgJoeyLogRingHead = 0; + gJoeyLogRingFull = 0u; } #else // not JOEYLIB_PLATFORM_IIGS diff --git a/src/core/draw.c b/src/core/draw.c index f679fa0..c9b4657 100644 --- a/src/core/draw.c +++ b/src/core/draw.c @@ -49,11 +49,12 @@ // ----- Prototypes ----- -static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); -static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual); -static bool circleOnSurface(int16_t cx, int16_t cy, uint16_t r); -static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex); -static void plotPixelWide(jlSurfaceT *s, int32_t x, int32_t y, uint8_t colorIndex); +static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); +static inline void fillRectOnSurfaceInline(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); +static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual); +static bool circleOnSurface(int16_t cx, int16_t cy, uint16_t r); +static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex); +static void plotPixelWide(jlSurfaceT *s, int32_t x, int32_t y, uint8_t colorIndex); // ----- Internal helpers (alphabetical) ----- @@ -78,13 +79,21 @@ static bool circleOnSurface(int16_t cx, int16_t cy, uint16_t r) { // it per span. Pixel output and dirty-mark word bands are bit-for-bit // identical to routing the same span through jlFillRect. static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { + fillRectOnSurfaceInline(s, x, y, w, h, colorIndex); +} + + +// fillRectOnSurface's body, inlined into jlFillRect's on-surface path: a +// game drawing many small rects (JoeyAGI's runs) paid a whole call layer of +// argument shuffling for it, more than the fill. +static inline __attribute__((always_inline)) void fillRectOnSurfaceInline(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { // Compile-time-selected op: machine fill (IIgs asm stage / Amiga+ST planar) // or the generic chunky fill. jlpFillRect(s, x, y, w, h, colorIndex); #if !defined(JL_FILL_RECT_MARK_FUSED) - // On IIgs the stage arm of jlpFillRect fused this mark into the asm - // entry (port.h); non-stage surfaces never carry dirty state, so - // nothing is lost by compiling it out there. + // On the IIgs and the ST jlpFillRect fuses this mark into its asm + // (port.h); non-stage surfaces never carry dirty state, so nothing is + // lost by compiling it out there. surfaceMarkDirtyRect(s, x, y, w, h); #endif } @@ -377,9 +386,8 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col // with r >= JL_SURFACE_HEIGHT can never fit on-surface, and casting // such an r to int16_t can wrap negative (r > 32767), so reject it // here before the cast feeds the fast path / dirty-rect extents. -#if defined(JL_HAS_DRAW_CIRCLE) - // On-surface fast path: machine asm / planar. No runtime platform check - // -- the whole block is compiled out on targets without JL_HAS_DRAW_CIRCLE. + // On-surface fast path: machine asm / planar, or the chunky generic + // (genericDraw.c), compile-time selected (port.h). if (circleOnSurface(cx, cy, r)) { int16_t ir = (int16_t)r; @@ -387,7 +395,6 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); return; } -#endif // Bresenham midpoint: maintain (x, y) on the perimeter, eight- // octant symmetry plots all 8 reflections each iteration. Plots @@ -548,18 +555,17 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u // can run without per-pixel bound checks. Hand off to the port // fast path; bounding-box dirty marking happens here in C either // way. -#if defined(JL_HAS_DRAW_LINE) if (x0 >= 0 && x0 < JL_SURFACE_WIDTH && x1 >= 0 && x1 < JL_SURFACE_WIDTH && y0 >= 0 && y0 < JL_SURFACE_HEIGHT && y1 >= 0 && y1 < JL_SURFACE_HEIGHT) { int16_t bbx = (x0 < x1) ? x0 : x1; int16_t bby = (y0 < y1) ? y0 : y1; int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1); int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1); - // On-surface fast path: machine asm / planar, compiled out where unsupported. + // On-surface fast path: machine asm / planar, or the chunky + // generic (genericDraw.c), compile-time selected (port.h). jlpDrawLine(s, x0, y0, x1, y1, colorIndex); surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh); return; } -#endif // Diagonal fallback: plot through plotPixelNoMark so each pixel still // clips off-surface individually (matching jlDrawPixel), but skip the @@ -570,7 +576,7 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u // #9/#37): Bresenham walks the monotone path from (x0, y0) to // (x1, y1) and terminates exactly at (x1, y1), so the visited-point // bounding box is the endpoint bounding box -- the same four - // ternaries the JL_HAS_DRAW_LINE fast path computes. The old + // ternaries the on-surface fast path computes. The old // per-pixel accumulation folded every VISITED point (clipped or // not), so after the post-loop clamp this precomputed box marks the // identical region it did. For endpoints running off-surface the @@ -654,11 +660,11 @@ void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { // // This is ONE body for every port: jlpDrawPixel is the compile-time // selected plot (IIgs asm / planar / generic chunky RMW), and the - // surfaceMarkDirtyRect macro runs the stage compare (direct gStage + // surfaceMarkDirtyRect macro runs the stage compare (direct jlgStage // load, no jlStageGet() hop -- PERF-AUDIT #10/#17) and the // single-row widen inline with no call layers (PERF-AUDIT #20), // which is exactly the code the old hand-written IIgs branch spelled - // out. NB (IIgs): an all-C inline RMW plot here (gRowOffsetLut, no + // out. NB (IIgs): an all-C inline RMW plot here (jlgRowOffsetLut, no // multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) // -- the far-pointer code for s->pixels[...] costs more than the // cross-segment JSL into the hand-tuned asm, so the asm plot stays. @@ -670,8 +676,8 @@ void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { // R10); non-stage surfaces carry no dirty state, so the plain plot // suffices there. The batching plotters (plotPixelNoMark) keep the // unfused inner. - if (s == gStage) { - iigsDrawPixelMark((uint16_t)x, (uint16_t)y, (uint16_t)(colorIndex & 0x0Fu)); + if (s == jlgStage) { + jlIigsDrawPixelMark((uint16_t)x, (uint16_t)y, (uint16_t)(colorIndex & 0x0Fu)); return; } jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex); @@ -998,7 +1004,13 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col } +// The public jlFillRect, or, on a port with its own (JL_HAS_FILL_RECT_ENTRY), +// the clip path that port's on-surface entry hands every other rect to. +#if defined(JL_HAS_FILL_RECT_ENTRY) +void jlFillRectClip(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { +#else void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { +#endif int16_t sx; int16_t sy; int16_t sw; @@ -1027,7 +1039,7 @@ void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin // contract); they fall through to the slow path's empty-clip // return instead. if (w > 0 && h > 0 && SURFACE_RECT_ON_SURFACE(x, y, w, h)) { - fillRectOnSurface(s, x, y, (int16_t)w, (int16_t)h, colorIndex); + fillRectOnSurfaceInline(s, x, y, (int16_t)w, (int16_t)h, colorIndex); return; } @@ -1131,8 +1143,6 @@ uint8_t jlSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { } - - void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) { uint8_t nibble; uint8_t doubled; @@ -1145,5 +1155,5 @@ void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) { // Compile-time-selected op: a machine override (asm / planar) or the // portable-C generic default -- no runtime dispatch. jlpSurfaceClear(s, doubled); - surfaceMarkDirtyAll(s); + jlSurfaceMarkDirtyAll(s); } diff --git a/src/core/fileRoot.h b/src/core/fileRoot.h index 95c13a7..aeb0827 100644 --- a/src/core/fileRoot.h +++ b/src/core/fileRoot.h @@ -9,9 +9,10 @@ // the path DOS started it by, the Amiga as PROGDIR:, the X68000 from its // process block, the IIgs as GS/OS prefix 1, the hosted ports from their // executable's path. DVX keeps SAVES/ and the log apart, in the program's -// settings folder. Without the flag both roots are "": the current -// directory (the ST, whose GEMDOS tells a program nothing of where it -// was started from). +// settings folder, and so do the web port, in the page's, and macOS, in the +// program's Application Support folder. Without the flag both roots are "": +// the current directory (the ST, whose GEMDOS tells a program nothing of +// where it was started from). #ifndef JOEYLIB_FILE_ROOT_H #define JOEYLIB_FILE_ROOT_H diff --git a/src/core/init.c b/src/core/init.c index c95c9c3..7474da5 100644 --- a/src/core/init.c +++ b/src/core/init.c @@ -28,7 +28,7 @@ // IIgs (65816): ~3-4 KB per all-opaque 32x32 sprite, so 8 KB fits // the typical working set and keeps the attrNoCross Memory Manager // request small. A request above one 64 KB bank takes up to four -// (codegenArenaInit). +// (jlCodegenArenaInit). // // Amiga/ST: the all-shift compiled sprites (NATIVE-PERF Phase 1) // emit one DRAW variant per x phase (8 on Amiga, 16 on ST) plus two @@ -58,7 +58,7 @@ static void clearError(void); -// coreSetError is the shared error setter (declared in surfaceInternal.h +// jlCoreSetError is the shared error setter (declared in surfaceInternal.h // and called by surface.c etc.); its definition lives here next to the // rest of the error-reporting state. @@ -77,7 +77,7 @@ static void clearError(void) { } -void coreSetError(const char *message) { +void jlCoreSetError(const char *message) { gLastError = message; } @@ -112,46 +112,46 @@ bool jlInit(const jlConfigT *config) { clearError(); if (gInitialized) { - coreSetError("jlInit called while already initialized"); + jlCoreSetError("jlInit called while already initialized"); return false; } if (config == NULL) { - coreSetError("jlInit called with NULL config"); + jlCoreSetError("jlInit called with NULL config"); return false; } memcpy(&gConfig, config, sizeof(gConfig)); - // jlpInit must run before stageAlloc: on IIgs the stage's pixel + // jlpInit must run before jlStageAlloc: on IIgs the stage's pixel // buffer comes from jlpStageAllocPixels, which depends on shadow / // SHR setup that jlpInit performs. // A port says why its init failed through jlpLastError or by calling - // coreSetError itself; only a failure with neither gets the generic + // jlCoreSetError itself; only a failure with neither gets the generic // reason. if (!jlpInit(&gConfig)) { const char *halMsg = jlpLastError(); if (halMsg != NULL) { - coreSetError(halMsg); + jlCoreSetError(halMsg); } else if (gLastError == NULL) { - coreSetError("jlpInit failed"); + jlCoreSetError("jlpInit failed"); } return false; } gMillisBase = jlpMillisElapsed(); - if (!stageAlloc()) { - coreSetError("failed to allocate stage surface"); + if (!jlStageAlloc()) { + jlCoreSetError("failed to allocate stage surface"); jlpShutdown(); return false; } #if !defined(JL_SPRITE_INTERPRETED) - if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes : DEFAULT_CODEGEN_BYTES)) { - coreSetError("no memory for any of the compiled-sprite cache"); - stageFree(); + if (!jlCodegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes : DEFAULT_CODEGEN_BYTES)) { + jlCoreSetError("no memory for any of the compiled-sprite cache"); + jlStageFree(); jlpShutdown(); return false; } @@ -160,12 +160,12 @@ bool jlInit(const jlConfigT *config) { // Empty the typed-character queue and centre the mouse before the // port arms its input: a shutdown/re-init cycle must not hand the app // characters typed during the previous run. - inputReset(); + jlInputReset(); jlpInputInit(); // Last, so sound that does not start leaves everything else running; // audio calls are then silent. - audioInit(!gConfig.noAudio); + jlAudioOpen(!gConfig.noAudio); // A program that ends without jlShutdown (exit, or main returning) must // still leave the machine as jlInit found it: interrupts unhooked, the @@ -194,15 +194,15 @@ void jlShutdown(void) { if (!gInitialized) { return; } - audioShutdown(); + jlAudioClose(); jlpInputShutdown(); // Reset compiled-sprite state (slot pointers, routine offsets, patch // caches) in every registered sprite BEFORE the arena teardown frees - // the slot structs -- see spriteSystemShutdown in spriteInternal.h + // the slot structs -- see jlSpriteSystemShutdown in spriteInternal.h // (claudeDocs/PERF-AUDIT.md #34). - spriteSystemShutdown(); - codegenArenaShutdown(); - stageFree(); + jlSpriteSystemShutdown(); + jlCodegenArenaShutdown(); + jlStageFree(); jlpShutdown(); gInitialized = false; clearError(); diff --git a/src/core/input.c b/src/core/input.c index 458703e..6a51327 100644 --- a/src/core/input.c +++ b/src/core/input.c @@ -3,9 +3,9 @@ // SPDX-License-Identifier: MIT // // Public input API. Maintains current/previous state buffers for the -// keyboard (gKeyState/gKeyPrev), mouse buttons (gMouseButtonState/ -// gMouseButtonPrev), and joystick buttons (gJoyButtonState/ -// gJoyButtonPrev). jlInputPoll snapshots the previous state, then +// keyboard (jlgKeyState/jlgKeyPrev), mouse buttons (jlgMouseButtonState/ +// jlgMouseButtonPrev), and joystick buttons (jlgJoyButtonState/ +// jlgJoyButtonPrev). jlInputPoll snapshots the previous state, then // asks the port HAL to refresh the current state; the predicates // derive held/edge values from those two buffers. // @@ -17,9 +17,9 @@ // two: a press let go within JOY_HOLD_MILLIS is button 0, down for the one // poll after it is let go, and a press held longer is button 1, down from // then until it is let go (joyTapOrHold). Ports mark such a stick in -// gJoyOneButton. +// jlgJoyOneButton. // -// The typed-character queue (gCharQueue) is a separate path for text +// The typed-character queue (jlgCharQueue) is a separate path for text // entry: ports push OS/firmware-translated ASCII via jlInputCharPush // during jlpInputPoll, and apps pop with jlInputGetChar. The queue is // a classic ring with one empty slot (head == tail means empty), so @@ -42,6 +42,18 @@ #define ASCII_PRINTABLE_FIRST 0x20u #define ASCII_PRINTABLE_LAST 0x7Eu +// A stick's axes up to date before they are read (port.h +// jlpJoystickAxesRead): only where the poll leaves them for later. +#if defined(JL_HAS_JOYSTICK_AXES_READ) +#define JOY_AXES_FRESH(_js) do { \ + if (jlgJoyAxesStale[_js] != 0u) { \ + jlpJoystickAxesRead(_js); \ + } \ +} while (0) +#else +#define JOY_AXES_FRESH(_js) ((void)0) +#endif + #ifdef JOY_TAP_OR_HOLD // How long the one button of a one-button stick is held before it is // button 1 rather than button 0. @@ -52,24 +64,27 @@ static void joyTapOrHold(void); // See inputInternal.h for why these are uint8_t and not bool. -uint8_t gKeyState [JL_KEY_COUNT]; -uint8_t gKeyPrev [JL_KEY_COUNT]; +uint8_t jlgKeyState [JL_KEY_COUNT]; +uint8_t jlgKeyPrev [JL_KEY_COUNT]; -int16_t gMouseX = 0; -bool gMouseAttached = false; // set by jlpInputInit where the HAL confirms a mouse -int16_t gMouseY = 0; -uint8_t gMouseButtonState[JL_MOUSE_BUTTON_COUNT]; -uint8_t gMouseButtonPrev [JL_MOUSE_BUTTON_COUNT]; +int16_t jlgMouseX = 0; +bool jlgMouseAttached = false; // set by jlpInputInit where the HAL confirms a mouse +int16_t jlgMouseY = 0; +uint8_t jlgMouseButtonState[JL_MOUSE_BUTTON_COUNT]; +uint8_t jlgMouseButtonPrev [JL_MOUSE_BUTTON_COUNT]; -uint8_t gJoyConnected [JL_JOYSTICK_COUNT]; -int8_t gJoyAxisX [JL_JOYSTICK_COUNT]; -int8_t gJoyAxisY [JL_JOYSTICK_COUNT]; -uint8_t gJoyButtonState[JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; -uint8_t gJoyButtonPrev [JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; -uint8_t gJoyDeadZone [JL_JOYSTICK_COUNT]; +uint8_t jlgJoyConnected [JL_JOYSTICK_COUNT]; +int8_t jlgJoyAxisX [JL_JOYSTICK_COUNT]; +int8_t jlgJoyAxisY [JL_JOYSTICK_COUNT]; +#if defined(JL_HAS_JOYSTICK_AXES_READ) +uint8_t jlgJoyAxesStale [JL_JOYSTICK_COUNT]; +#endif +uint8_t jlgJoyButtonState[JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; +uint8_t jlgJoyButtonPrev [JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; +uint8_t jlgJoyDeadZone [JL_JOYSTICK_COUNT]; #ifdef JOY_TAP_OR_HOLD -uint8_t gJoyOneButton [JL_JOYSTICK_COUNT]; +uint8_t jlgJoyOneButton [JL_JOYSTICK_COUNT]; // A one-button stick's button as the port last reported it, and when it // went down (jlMillisElapsed). @@ -77,9 +92,9 @@ static uint8_t gJoyFireHeld [JL_JOYSTICK_COUNT]; static uint32_t gJoyFireSince[JL_JOYSTICK_COUNT]; #endif -uint8_t gCharQueue [JL_CHAR_QUEUE_SIZE]; -uint8_t gCharQueueHead = 0; -uint8_t gCharQueueTail = 0; +uint8_t jlgCharQueue [JL_CHAR_QUEUE_SIZE]; +uint8_t jlgCharQueueHead = 0; +uint8_t jlgCharQueueTail = 0; // Build-time check: the ring-index wrap masks with the size minus one, // which only wraps correctly for a power-of-two size. @@ -89,20 +104,20 @@ typedef int joey_key_second_check[(JL_KEY_COUNT <= INPUT_KEY_SECOND) ? 1 : -1]; #ifdef JOEYLIB_PLATFORM_IIGS -extern void iigsInputSnapshot(void); -// Build-time checks: iigsInputSnapshot's asm hard-codes JL_KEY_COUNT=60 -// and the small button counts, and walks every array one byte per -// element. If a future change adds/removes keys or buttons the asm -// must be updated; if anyone re-types the arrays back to bool the -// per-element size could grow past one byte and the asm reads +extern void jlIigsInputSnapshot(void); +// Build-time checks: jlIigsInputSnapshot's asm hard-codes JL_KEY_COUNT=60 +// and the small button counts, and copies every array as whole words of +// one-byte elements. If a future change adds/removes keys or buttons the +// asm must be updated; if anyone re-types the arrays back to bool the +// per-element size could grow past one byte and the asm copies // the wrong bytes. Either condition declares a zero-size array // below, which is a compile error. typedef int joey_keycount_check [(JL_KEY_COUNT == 60) ? 1 : -1]; typedef int joey_mousebtn_check [(JL_MOUSE_BUTTON_COUNT == 4) ? 1 : -1]; typedef int joey_joybtn_check [(JL_JOYSTICK_COUNT * JL_JOY_BUTTON_COUNT == 4) ? 1 : -1]; -typedef int joey_keystate_size_check [(sizeof(gKeyState) == JL_KEY_COUNT) ? 1 : -1]; -typedef int joey_mousebtn_size_check [(sizeof(gMouseButtonState) == JL_MOUSE_BUTTON_COUNT) ? 1 : -1]; -typedef int joey_joybtn_size_check [(sizeof(gJoyButtonState) == JL_JOYSTICK_COUNT * JL_JOY_BUTTON_COUNT) ? 1 : -1]; +typedef int joey_keystate_size_check [(sizeof(jlgKeyState) == JL_KEY_COUNT) ? 1 : -1]; +typedef int joey_mousebtn_size_check [(sizeof(jlgMouseButtonState) == JL_MOUSE_BUTTON_COUNT) ? 1 : -1]; +typedef int joey_joybtn_size_check [(sizeof(jlgJoyButtonState) == JL_JOYSTICK_COUNT * JL_JOY_BUTTON_COUNT) ? 1 : -1]; #endif void jlInputCharPush(uint8_t ch) { @@ -117,19 +132,33 @@ void jlInputCharPush(uint8_t ch) { if (ch < ASCII_PRINTABLE_FIRST && ch != JL_CHAR_BACKSPACE && ch != JL_CHAR_TAB && ch != JL_CHAR_RETURN && ch != JL_CHAR_ESCAPE) { return; } - head = gCharQueueHead; + head = jlgCharQueueHead; next = (uint8_t)((head + 1u) & (JL_CHAR_QUEUE_SIZE - 1u)); - if (next == gCharQueueTail) { + if (next == jlgCharQueueTail) { return; } - gCharQueue[head] = ch; - gCharQueueHead = next; + jlgCharQueue[head] = ch; + jlgCharQueueHead = next; } void jlInputCharReset(void) { - gCharQueueHead = 0; - gCharQueueTail = 0; + jlgCharQueueHead = 0; + jlgCharQueueTail = 0; +} + + +// At jlInit, before the port's own input starts: no typed characters left +// from a previous run, the default dead zone whatever a previous run set +// (jlJoystickReset), no mouse until the port finds one, and the mouse in the +// middle of the stage, where it reads on every port until a mouse reports +// (and stays, with none). +void jlInputReset(void) { + jlInputCharReset(); + memset(jlgJoyDeadZone, JOY_DEAD_ZONE_DEFAULT, sizeof(jlgJoyDeadZone)); + jlgMouseAttached = false; + jlgMouseX = JL_SURFACE_WIDTH / 2; + jlgMouseY = JL_SURFACE_HEIGHT / 2; } @@ -137,7 +166,7 @@ void jlInputCharReset(void) { // step: a centred stick reads 0 whichever middle value its driver reports // (0..65535 has two: 32767 and 32768), and the ends read // JL_JOYSTICK_AXIS_MIN and MAX. -int8_t inputAxisScale(int32_t pos, int32_t min, int32_t max) { +int8_t jlInputAxisScale(int32_t pos, int32_t min, int32_t max) { int32_t span; span = max > min ? max - min : 1; @@ -156,7 +185,7 @@ int8_t inputAxisScale(int32_t pos, int32_t min, int32_t max) { // from its own live key state: Ctrl held, and Alt where Alt is a command // key rather than a layer of characters (Windows' AltGr is Ctrl+Alt, and // types its characters). -void inputCharTyped(uint8_t ch, bool chord) { +void jlInputCharTyped(uint8_t ch, bool chord) { if (!chord) { jlInputCharPush(ch); } @@ -165,7 +194,7 @@ void inputCharTyped(uint8_t ch, bool chord) { // An analog axis within its stick's dead zone (jlJoystickReset) of centre // reads 0: |value| <= zone, the same test on every port. -int8_t inputDeadZone(int8_t value, uint8_t zone) { +int8_t jlInputDeadZone(int8_t value, uint8_t zone) { int16_t magnitude; magnitude = value < 0 ? (int16_t)-value : (int16_t)value; @@ -177,36 +206,33 @@ int8_t inputDeadZone(int8_t value, uint8_t zone) { // (events, or an interrupt) rather than reads as they are: down when held // now or pressed since the last poll, so a tap shorter than a frame reads // down for one poll. The presses are then forgotten. state may be held -// itself, for a port that reads what is held into state first. -void inputLatch(uint8_t *state, const volatile uint8_t *held, volatile uint8_t *tapped, uint16_t count) { +// itself, for a port that reads what is held into state first. Returns +// whether a tap may be all that holds any down (a tap's bit not among its +// held bits): until the next latch lets it go up, a port cannot skip a +// latch just because no event came. +bool jlInputLatch(uint8_t *state, const volatile uint8_t *held, volatile uint8_t *tapped, uint16_t count) { uint16_t i; + uint8_t h; + uint8_t t; + uint8_t tapOnly; + tapOnly = 0u; for (i = 0u; i < count; i++) { - state[i] = (uint8_t)(held[i] | tapped[i]); + h = held[i]; + t = tapped[i]; + tapOnly = (uint8_t)(tapOnly | (t & (uint8_t)~h)); + state[i] = (uint8_t)(h | t); tapped[i] = 0u; } + return tapOnly != 0u; } // A port's mouse at (x, y), held to the stage: every port reports 0..319 and // 0..199, whatever its pointer does at the edge. -void inputMouseMove(int16_t x, int16_t y) { - gMouseX = x < 0 ? 0 : (x >= JL_SURFACE_WIDTH ? (int16_t)(JL_SURFACE_WIDTH - 1) : x); - gMouseY = y < 0 ? 0 : (y >= JL_SURFACE_HEIGHT ? (int16_t)(JL_SURFACE_HEIGHT - 1) : y); -} - - -// At jlInit, before the port's own input starts: no typed characters left -// from a previous run, the default dead zone whatever a previous run set -// (jlJoystickReset), no mouse until the port finds one, and the mouse in the -// middle of the stage, where it reads on every port until a mouse reports -// (and stays, with none). -void inputReset(void) { - jlInputCharReset(); - memset(gJoyDeadZone, JOY_DEAD_ZONE_DEFAULT, sizeof(gJoyDeadZone)); - gMouseAttached = false; - gMouseX = JL_SURFACE_WIDTH / 2; - gMouseY = JL_SURFACE_HEIGHT / 2; +void jlInputMouseMove(int16_t x, int16_t y) { + jlgMouseX = x < 0 ? 0 : (x >= JL_SURFACE_WIDTH ? (int16_t)(JL_SURFACE_WIDTH - 1) : x); + jlgMouseY = y < 0 ? 0 : (y >= JL_SURFACE_HEIGHT ? (int16_t)(JL_SURFACE_HEIGHT - 1) : y); } @@ -214,12 +240,12 @@ int jlInputGetChar(void) { uint8_t tail; int ch; - tail = gCharQueueTail; - if (tail == gCharQueueHead) { + tail = jlgCharQueueTail; + if (tail == jlgCharQueueHead) { return -1; } - ch = gCharQueue[tail]; - gCharQueueTail = (uint8_t)((tail + 1u) & (JL_CHAR_QUEUE_SIZE - 1u)); + ch = jlgCharQueue[tail]; + jlgCharQueueTail = (uint8_t)((tail + 1u) & (JL_CHAR_QUEUE_SIZE - 1u)); return ch; } @@ -234,20 +260,20 @@ static void joyTapOrHold(void) { bool held; for (js = 0; js < JL_JOYSTICK_COUNT; js++) { - if (!gJoyOneButton[js]) { + if (!jlgJoyOneButton[js]) { gJoyFireHeld[js] = false; continue; } - fire = gJoyButtonState[js][JL_JOY_BUTTON_0] != 0; - held = gJoyFireHeld[js] != 0; - gJoyButtonState[js][JL_JOY_BUTTON_0] = false; - gJoyButtonState[js][JL_JOY_BUTTON_1] = false; + fire = jlgJoyButtonState[js][JL_JOY_BUTTON_0] != 0; + held = gJoyFireHeld[js] != 0; + jlgJoyButtonState[js][JL_JOY_BUTTON_0] = false; + jlgJoyButtonState[js][JL_JOY_BUTTON_1] = false; if (fire && !held) { gJoyFireSince[js] = jlMillisElapsed(); } else if (fire) { - gJoyButtonState[js][JL_JOY_BUTTON_1] = (jlMillisElapsed() - gJoyFireSince[js]) >= JOY_HOLD_MILLIS; + jlgJoyButtonState[js][JL_JOY_BUTTON_1] = (jlMillisElapsed() - gJoyFireSince[js]) >= JOY_HOLD_MILLIS; } else if (held) { - gJoyButtonState[js][JL_JOY_BUTTON_0] = gJoyButtonPrev[js][JL_JOY_BUTTON_1] == 0; + jlgJoyButtonState[js][JL_JOY_BUTTON_0] = jlgJoyButtonPrev[js][JL_JOY_BUTTON_1] == 0; } gJoyFireHeld[js] = fire; } @@ -257,14 +283,13 @@ static void joyTapOrHold(void) { void jlInputPoll(void) { #ifdef JOEYLIB_PLATFORM_IIGS - // One asm pass for: TTL decrement + key snapshot + mouse/joy - // button snapshots. Replaces 3 memcpys + the C TTL loop - // that used to live in jlpInputPoll. ~0.6 ms saved per frame. - iigsInputSnapshot(); + // One asm pass for the key and the mouse/joy button snapshots, in + // place of 3 memcpys; the keys only when they may have moved. + jlIigsInputSnapshot(); #else - memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); - memcpy(gMouseButtonPrev, gMouseButtonState, sizeof(gMouseButtonState)); - memcpy(gJoyButtonPrev, gJoyButtonState, sizeof(gJoyButtonState)); + memcpy(jlgKeyPrev, jlgKeyState, sizeof(jlgKeyState)); + memcpy(jlgMouseButtonPrev, jlgMouseButtonState, sizeof(jlgMouseButtonState)); + memcpy(jlgJoyButtonPrev, jlgJoyButtonState, sizeof(jlgJoyButtonState)); #endif jlpInputPoll(); #ifdef JOY_TAP_OR_HOLD @@ -304,7 +329,7 @@ void jlWaitForAnyKey(void) { // punctuation, which every port has a different set of. Not one // typed while a named key (not a modifier) is down, though, which // may be that key's own auto-repeat: it is dropped. - if (gCharQueueHead != gCharQueueTail) { + if (jlgCharQueueHead != jlgCharQueueTail) { jlInputCharReset(); if (!held) { return; @@ -317,20 +342,23 @@ void jlWaitForAnyKey(void) { /* All six key/mouse predicates fold the lower-bound check (`<= NONE`) * and upper-bound check (`>= COUNT`) into a single unsigned compare. * Index 0 (JL_KEY_NONE / JL_MOUSE_BUTTON_NONE) is a sentinel that no HAL - * ever writes, so reading gKeyState[0] / gMouseButtonState[0] is + * ever writes, so reading jlgKeyState[0] / jlgMouseButtonState[0] is * always 0 -- the predicate result is unchanged. * * DECIDED (PERF-AUDIT #40, Phase 8): these stay out-of-line functions. * Macro/inline forms in joey/input.h would expose the state arrays in - * a public header and multi-evaluate args, to shave call overhead off - * predicates already measured at 70K+ ops/sec post-clock-fix -- input - * is nowhere near any real game's frame profile. Revisit only if a - * profiled game shows the predicates in its frame budget. */ + * a public header and multi-evaluate args. A port whose compiled + * predicates show in a game's profile has its own, in asm + * (JL_HAS_INPUT_PREDICATES): JoeyAGI asks 42 a poll, 2.2% of its IIgs + * time, the compiled C there spending twice as long making its bool. */ +#if defined(JL_HAS_INPUT_PREDICATES) +typedef char inputPredicatesProof[(JL_KEY_COUNT == 60 && JL_MOUSE_BUTTON_COUNT == 4) ? 1 : -1]; +#else bool jlKeyDown(jlKeyE key) { if ((uint16_t)key >= (uint16_t)JL_KEY_COUNT) { return false; } - return gKeyState[key]; + return jlgKeyState[key]; } @@ -338,7 +366,7 @@ bool jlKeyPressed(jlKeyE key) { if ((uint16_t)key >= (uint16_t)JL_KEY_COUNT) { return false; } - return gKeyState[key] && !gKeyPrev[key]; + return jlgKeyState[key] && !jlgKeyPrev[key]; } @@ -346,7 +374,7 @@ bool jlKeyReleased(jlKeyE key) { if ((uint16_t)key >= (uint16_t)JL_KEY_COUNT) { return false; } - return !gKeyState[key] && gKeyPrev[key]; + return !jlgKeyState[key] && jlgKeyPrev[key]; } @@ -354,7 +382,7 @@ bool jlMouseDown(jlMouseButtonE button) { if ((uint16_t)button >= (uint16_t)JL_MOUSE_BUTTON_COUNT) { return false; } - return gMouseButtonState[button]; + return jlgMouseButtonState[button]; } @@ -362,7 +390,7 @@ bool jlMousePressed(jlMouseButtonE button) { if ((uint16_t)button >= (uint16_t)JL_MOUSE_BUTTON_COUNT) { return false; } - return gMouseButtonState[button] && !gMouseButtonPrev[button]; + return jlgMouseButtonState[button] && !jlgMouseButtonPrev[button]; } @@ -370,22 +398,23 @@ bool jlMouseReleased(jlMouseButtonE button) { if ((uint16_t)button >= (uint16_t)JL_MOUSE_BUTTON_COUNT) { return false; } - return !gMouseButtonState[button] && gMouseButtonPrev[button]; + return !jlgMouseButtonState[button] && jlgMouseButtonPrev[button]; } +#endif bool jlMousePresent(void) { - return gMouseAttached; + return jlgMouseAttached; } int16_t jlMouseX(void) { - return gMouseX; + return jlgMouseX; } int16_t jlMouseY(void) { - return gMouseY; + return jlgMouseY; } @@ -393,7 +422,8 @@ bool jlJoystickConnected(jlJoystickE js) { if ((uint16_t)js >= (uint16_t)JL_JOYSTICK_COUNT) { return false; } - return gJoyConnected[js]; + JOY_AXES_FRESH(js); + return jlgJoyConnected[js]; } @@ -401,7 +431,8 @@ int8_t jlJoystickX(jlJoystickE js) { if ((uint16_t)js >= (uint16_t)JL_JOYSTICK_COUNT) { return 0; } - return gJoyAxisX[js]; + JOY_AXES_FRESH(js); + return jlgJoyAxisX[js]; } @@ -409,7 +440,8 @@ int8_t jlJoystickY(jlJoystickE js) { if ((uint16_t)js >= (uint16_t)JL_JOYSTICK_COUNT) { return 0; } - return gJoyAxisY[js]; + JOY_AXES_FRESH(js); + return jlgJoyAxisY[js]; } @@ -425,7 +457,7 @@ bool jlJoyDown(jlJoystickE js, jlJoyButtonE button) { return false; } idx = (uint16_t)((uint16_t)js * JL_JOY_BUTTON_COUNT + (uint16_t)button); - return ((const uint8_t *)gJoyButtonState)[idx] != 0; + return ((const uint8_t *)jlgJoyButtonState)[idx] != 0; } @@ -438,7 +470,7 @@ bool jlJoyPressed(jlJoystickE js, jlJoyButtonE button) { return false; } idx = (uint16_t)((uint16_t)js * JL_JOY_BUTTON_COUNT + (uint16_t)button); - return (((const uint8_t *)gJoyButtonState)[idx] != 0) && (((const uint8_t *)gJoyButtonPrev)[idx] == 0); + return (((const uint8_t *)jlgJoyButtonState)[idx] != 0) && (((const uint8_t *)jlgJoyButtonPrev)[idx] == 0); } @@ -451,7 +483,7 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button) { return false; } idx = (uint16_t)((uint16_t)js * JL_JOY_BUTTON_COUNT + (uint16_t)button); - return (((const uint8_t *)gJoyButtonState)[idx] == 0) && (((const uint8_t *)gJoyButtonPrev)[idx] != 0); + return (((const uint8_t *)jlgJoyButtonState)[idx] == 0) && (((const uint8_t *)jlgJoyButtonPrev)[idx] != 0); } @@ -459,6 +491,6 @@ void jlJoystickReset(jlJoystickE js, uint8_t deadZone) { if ((uint16_t)js >= (uint16_t)JL_JOYSTICK_COUNT) { return; } - gJoyDeadZone[js] = deadZone; + jlgJoyDeadZone[js] = deadZone; jlpJoystickReset(js); } diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h index 3380619..10da424 100644 --- a/src/core/inputInternal.h +++ b/src/core/inputInternal.h @@ -5,7 +5,7 @@ // Internal input state shared between core and per-port HAL. // // Per-port jlpInputPoll() writes directly into the public state -// globals: gKeyState[] for keyboard, gMouse* for mouse, gJoy* for +// globals: jlgKeyState[] for keyboard, gMouse* for mouse, gJoy* for // joysticks. The core compares against the matching *Prev shadow // each jlInputPoll to derive edge events for keys/buttons. Mouse // position and joystick axes have no edge semantics, so no shadow. @@ -17,36 +17,41 @@ #include "joey/types.h" // Stored as uint8_t (not bool) because a _Bool wider than one byte -// would break the IIgs asm fast path (iigsInputSnapshot), which walks +// would break the IIgs asm fast path (jlIigsInputSnapshot), which walks // these arrays one byte per element; with a 2-byte bool element k // would sit at byte offset 2*k and the asm's per-byte clear would // never reach the live half. uint8_t pins the storage to one byte per // element on every port. Public predicates still return bool via // implicit coercion. -extern uint8_t gKeyState[JL_KEY_COUNT]; -extern uint8_t gKeyPrev [JL_KEY_COUNT]; +extern uint8_t jlgKeyState[JL_KEY_COUNT]; +extern uint8_t jlgKeyPrev [JL_KEY_COUNT]; -extern bool gMouseAttached; -extern int16_t gMouseX; -extern int16_t gMouseY; -extern uint8_t gMouseButtonState[JL_MOUSE_BUTTON_COUNT]; -extern uint8_t gMouseButtonPrev [JL_MOUSE_BUTTON_COUNT]; +extern bool jlgMouseAttached; +extern int16_t jlgMouseX; +extern int16_t jlgMouseY; +extern uint8_t jlgMouseButtonState[JL_MOUSE_BUTTON_COUNT]; +extern uint8_t jlgMouseButtonPrev [JL_MOUSE_BUTTON_COUNT]; -extern uint8_t gJoyConnected[JL_JOYSTICK_COUNT]; -extern int8_t gJoyAxisX [JL_JOYSTICK_COUNT]; -extern int8_t gJoyAxisY [JL_JOYSTICK_COUNT]; -extern uint8_t gJoyButtonState[JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; -extern uint8_t gJoyButtonPrev [JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; +extern uint8_t jlgJoyConnected[JL_JOYSTICK_COUNT]; +extern int8_t jlgJoyAxisX [JL_JOYSTICK_COUNT]; +extern int8_t jlgJoyAxisY [JL_JOYSTICK_COUNT]; +#if defined(JL_HAS_JOYSTICK_AXES_READ) +// Stick js's axes and connection not yet read since the last poll (port.h +// jlpJoystickAxesRead). +extern uint8_t jlgJoyAxesStale[JL_JOYSTICK_COUNT]; +#endif +extern uint8_t jlgJoyButtonState[JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; +extern uint8_t jlgJoyButtonPrev [JL_JOYSTICK_COUNT][JL_JOY_BUTTON_COUNT]; // Platforms whose sticks can have only one button (the ST's always do; // most Amiga owners use those same Atari sticks). There a port sets -// gJoyOneButton for such a stick and reports its button as button 0, and +// jlgJoyOneButton for such a stick and reports its button as button 0, and // jlInputPoll makes a quick press of it button 0 and a held one button 1 // (input.c, joyTapOrHold). The host BLANK build has it too, so the host // harness can test it. #if defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_BLANK) #define JOY_TAP_OR_HOLD -extern uint8_t gJoyOneButton [JL_JOYSTICK_COUNT]; +extern uint8_t jlgJoyOneButton [JL_JOYSTICK_COUNT]; #endif // A named key two physical keys give (Return and the keypad's Enter, an @@ -60,17 +65,17 @@ extern uint8_t gJoyOneButton [JL_JOYSTICK_COUNT]; // Per-stick dead zone, set by jlJoystickReset and to its default at jlInit; the // ports with analog sticks (IIgs, DOS, DVX, hosted) apply it through -// inputDeadZone. -extern uint8_t gJoyDeadZone [JL_JOYSTICK_COUNT]; +// jlInputDeadZone. +extern uint8_t jlgJoyDeadZone [JL_JOYSTICK_COUNT]; // Typed-character FIFO backing jlInputGetChar. Ports feed it with // jlInputCharPush from jlpInputPoll; the pop side is jlInputGetChar. // NOT interrupt-safe: ports whose keyboard handling runs at interrupt // time (DOS INT 9, ST ikbd) buffer translated characters in their own // volatile ring and drain it into this queue at poll time. -extern uint8_t gCharQueue [JL_CHAR_QUEUE_SIZE]; -extern uint8_t gCharQueueHead; -extern uint8_t gCharQueueTail; +extern uint8_t jlgCharQueue [JL_CHAR_QUEUE_SIZE]; +extern uint8_t jlgCharQueueHead; +extern uint8_t jlgCharQueueTail; // Append one translated character to the typed-character queue. // Filters to the documented set (printable 0x20..0x7E plus @@ -82,13 +87,13 @@ void jlInputCharPush(uint8_t ch); // Empty the typed-character queue. Called by jlInit so a re-init // never hands the app characters typed before/during a previous run. void jlInputCharReset(void); -int8_t inputAxisScale(int32_t pos, int32_t min, int32_t max); -void inputCharTyped(uint8_t ch, bool chord); -int8_t inputDeadZone(int8_t value, uint8_t zone); +void jlInputReset(void); +int8_t jlInputAxisScale(int32_t pos, int32_t min, int32_t max); +void jlInputCharTyped(uint8_t ch, bool chord); +int8_t jlInputDeadZone(int8_t value, uint8_t zone); static inline uint8_t inputKeyHeld(uint8_t held, uint8_t key, bool down); -void inputLatch(uint8_t *state, const volatile uint8_t *held, volatile uint8_t *tapped, uint16_t count); -void inputMouseMove(int16_t x, int16_t y); -void inputReset(void); +bool jlInputLatch(uint8_t *state, const volatile uint8_t *held, volatile uint8_t *tapped, uint16_t count); +void jlInputMouseMove(int16_t x, int16_t y); // (jlpJoystickReset hook -- called from jlJoystickReset to clear any auto- // disconnect state and arm a fresh center capture -- is declared with the diff --git a/src/core/modPlayer.c b/src/core/modPlayer.c index c27993d..73f9bad 100644 --- a/src/core/modPlayer.c +++ b/src/core/modPlayer.c @@ -95,7 +95,7 @@ #define VOLUME_SHIFT 6u // a sample times a volume of 64 is the sample #define MIX_CHUNK 256u // output samples mixed at a time // 8-bit output: each channel's sample times its volume, over 64, shifted down -// gU8Shift bits (modPlayerSetU8Shift), added into the output. At +// gU8Shift bits (jlModPlayerSetU8Shift), added into the output. At // U8_SHIFT_QUARTER, the default, a channel at full volume swings a quarter of // the range, -32 to 31, so the four together, -128 to 124 on the silence // byte, never wrap. At U8_SHIFT_HALF a channel swings half: a side's two @@ -225,8 +225,8 @@ static void jumpLoop(ChannelT *c, uint8_t x); static void latchLoops(void); static void mixChunkS16(int32_t *left, int32_t *right, uint32_t count); #if defined(JL_HAS_MOD_MIX_RUN) -void modMixRunU8(MixRunT *run); -#define mixRunU8 modMixRunU8 +void jlModMixRunU8(MixRunT *run); +#define mixRunU8 jlModMixRunU8 #else static void mixRunU8(MixRunT *run); #endif @@ -674,12 +674,12 @@ static void mixRunU8(MixRunT *run) { #endif -bool modPlayerActive(void) { +bool jlModPlayerActive(void) { return gPlay.active; } -bool modPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels) { +bool jlModPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels) { uint32_t run; uint32_t i; int32_t left; @@ -707,7 +707,7 @@ bool modPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels) { } -bool modPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels) { +bool jlModPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels) { uint32_t run; uint32_t f; uint8_t i; @@ -743,20 +743,20 @@ bool modPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels) { } -void modPlayerSetU8Shift(uint8_t shift) { +void jlModPlayerSetU8Shift(uint8_t shift) { gU8Shift = shift < U8_SHIFT_HALF ? U8_SHIFT_HALF : (shift > U8_SHIFT_QUARTER ? U8_SHIFT_QUARTER : shift); gVolTabBuilt = false; } -bool modPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool loop) { +bool jlModPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool loop) { const uint8_t *info; uint32_t samplesOffset; uint32_t offset; uint32_t bytes; uint8_t i; - modPlayerStop(); + jlModPlayerStop(); if (data == NULL || rate < RATE_MIN || length < MOD_HEADER_BYTES || !modFormatIsFourChannel(data)) { return false; } @@ -804,7 +804,7 @@ bool modPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool lo } -void modPlayerStop(void) { +void jlModPlayerStop(void) { if (gPlay.module != NULL) { jlpBigFree(gPlay.module); } @@ -854,8 +854,8 @@ static void patternBreak(uint8_t param) { uint8_t tens; uint8_t row; - tens = (uint8_t)(param >> NIBBLE_SHIFT); - row = (uint8_t)((param & NIBBLE_MASK) + (tens <= ROW_DECIMAL_MAX ? tens * DECIMAL_BASE : 0u)); + tens = (uint8_t)(param >> NIBBLE_SHIFT); + row = (uint8_t)((param & NIBBLE_MASK) + (tens <= ROW_DECIMAL_MAX ? tens * DECIMAL_BASE : 0u)); gPlay.pBreakPos = row > LAST_ROW ? 0u : row; gPlay.posJumpFlag = true; } @@ -977,7 +977,7 @@ static bool renderAdvance(uint32_t *run, uint32_t want) { // effect from the tick after it. length = tickLength(); if (!tick()) { - modPlayerStop(); + jlModPlayerStop(); return false; } gPlay.tickLeft = length; @@ -1485,9 +1485,9 @@ static void voiceSkip(PaulaT *a, uint32_t count) { uint32_t frac; uint32_t idx; - step = a->step; - frac = (uint32_t)a->posFrac + (step & FRAC_MASK) * count; - idx = a->posIdx + (step >> FRAC_BITS) * count + (frac >> FRAC_BITS); + step = a->step; + frac = (uint32_t)a->posFrac + (step & FRAC_MASK) * count; + idx = a->posIdx + (step >> FRAC_BITS) * count + (frac >> FRAC_BITS); a->posFrac = (uint16_t)(frac & FRAC_MASK); if (idx >= a->curLen && !voiceWrap(a, &idx)) { return; @@ -1540,7 +1540,7 @@ static void volTabBuild(void) { product = (int16_t)(-(int16_t)U8_BIAS * (int16_t)volume); for (s = 0u; s < SAMPLE_VALUES; s++) { gVolTab[volume][(uint8_t)(s + U8_BIAS)] = (int8_t)(product >> (VOLUME_SHIFT + gU8Shift)); - product = (int16_t)(product + (int16_t)volume); + product = (int16_t)(product + (int16_t)volume); } } for (s = 0u; s < U8_SUMS; s++) { diff --git a/src/core/modPlayer.h b/src/core/modPlayer.h index a7ae6f1..39a41a1 100644 --- a/src/core/modPlayer.h +++ b/src/core/modPlayer.h @@ -13,32 +13,32 @@ #include <stdbool.h> #include <stdint.h> -// The unsigned 8-bit silence modPlayerRenderU8 pads with. +// The unsigned 8-bit silence jlModPlayerRenderU8 pads with. #define MOD_PLAYER_SILENCE_U8 0x80u -// True while a module plays: from modPlayerStart until modPlayerStop or, for +// True while a module plays: from jlModPlayerStart until jlModPlayerStop or, for // a song that does not loop, its end. -bool modPlayerActive(void); +bool jlModPlayerActive(void); // Unsigned 8-bit, centred on MOD_PLAYER_SILENCE_U8: the DOS, ST and X68000 // ports' format, mono when channels is 1, and interleaved when it is 2 // (Paula's left and right channels; each channel at the level -// modPlayerSetU8Shift gives it, in mono or stereo alike). Returns +// jlModPlayerSetU8Shift gives it, in mono or stereo alike). Returns // false once the song has ended (or nothing plays): the rest of dst is // silence and the player has stopped. -bool modPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels); +bool jlModPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels); // Signed 16-bit frames, interleaved when channels is 2 (Paula's left and // right channels, as the Amiga plays them), mono when it is 1: the hosted -// ports' format. Returns false as modPlayerRenderU8 does. -bool modPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels); +// ports' format. Returns false as jlModPlayerRenderU8 does. +bool jlModPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels); // The 8-bit renders' level: a channel at full volume playing a full-scale // sample swings full scale shifted down shift bits, 1 or 2 (2 until set: // four channels fill a byte and never wrap). At 1 a side's two channels // still fit, and a mono render adds its two sides through a clamp, so four -// loud channels clip. A port sets its own before modPlayerStart. -void modPlayerSetU8Shift(uint8_t shift); +// loud channels clip. A port sets its own before jlModPlayerStart. +void jlModPlayerSetU8Shift(uint8_t shift); // Plays the module data[0..length) at rate output samples a second. The // player keeps its own copy, so data may go once this returns. loop false @@ -46,9 +46,9 @@ void modPlayerSetU8Shift(uint8_t shift); // jump back), or at an E8F (the Amiga's stop marker), or at an F00. False, with nothing // playing, for anything but a four-channel Protracker module, or when there // is no memory for it. -bool modPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool loop); +bool jlModPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool loop); // Stops the module and frees the player's copy of it. -void modPlayerStop(void); +void jlModPlayerStop(void); #endif diff --git a/src/core/modStopMarker.c b/src/core/modStopMarker.c index af51ff7..0c4fe3f 100644 --- a/src/core/modStopMarker.c +++ b/src/core/modStopMarker.c @@ -27,6 +27,40 @@ static void jumpsToMarker(uint8_t *module, uint32_t songLength, uint32_t patternCount); +bool jlModStopMarkerAdd(const uint8_t *in, uint32_t inLength, uint8_t *out) { + uint32_t songLength; + uint32_t patternCount; + uint32_t samplesOffset; + uint32_t row; + uint8_t *marker; + + if (inLength < MOD_HEADER_BYTES + MOD_PATTERN_BYTES || !modFormatIsFourChannel(in)) { + return false; + } + songLength = in[MOD_SONG_LENGTH_OFFSET]; + if (songLength == 0u || songLength >= MOD_ORDER_TABLE_LEN) { + return false; + } + patternCount = modFormatPatternCount(in); + samplesOffset = MOD_HEADER_BYTES + patternCount * MOD_PATTERN_BYTES; + if (patternCount >= MOD_MAX_PATTERNS || samplesOffset > inLength) { + return false; + } + memcpy(out, in, samplesOffset); + jumpsToMarker(out, songLength, patternCount); + marker = out + samplesOffset; + memset(marker, 0, MOD_STOP_MARKER_BYTES); + for (row = 0u; row < MOD_ROWS; row++) { + marker[row * MOD_ROW_BYTES + EFFECT_BYTE] = E8_EFFECT; + marker[row * MOD_ROW_BYTES + PARAM_BYTE] = E8_PARAM; + } + memcpy(marker + MOD_STOP_MARKER_BYTES, in + samplesOffset, inLength - samplesOffset); + out[MOD_SONG_LENGTH_OFFSET] = (uint8_t)(songLength + 1u); + out[MOD_ORDER_TABLE_OFFSET + songLength] = (uint8_t)patternCount; + return true; +} + + // A jump back -- Bxx to a position already played, or past the song's end, // which Protracker takes as its start -- loops the song where it should end, // so each one goes to the marker (at position songLength) instead, when @@ -61,37 +95,3 @@ static void jumpsToMarker(uint8_t *module, uint32_t songLength, uint32_t pattern } } } - - -bool modStopMarkerAdd(const uint8_t *in, uint32_t inLength, uint8_t *out) { - uint32_t songLength; - uint32_t patternCount; - uint32_t samplesOffset; - uint32_t row; - uint8_t *marker; - - if (inLength < MOD_HEADER_BYTES + MOD_PATTERN_BYTES || !modFormatIsFourChannel(in)) { - return false; - } - songLength = in[MOD_SONG_LENGTH_OFFSET]; - if (songLength == 0u || songLength >= MOD_ORDER_TABLE_LEN) { - return false; - } - patternCount = modFormatPatternCount(in); - samplesOffset = MOD_HEADER_BYTES + patternCount * MOD_PATTERN_BYTES; - if (patternCount >= MOD_MAX_PATTERNS || samplesOffset > inLength) { - return false; - } - memcpy(out, in, samplesOffset); - jumpsToMarker(out, songLength, patternCount); - marker = out + samplesOffset; - memset(marker, 0, MOD_STOP_MARKER_BYTES); - for (row = 0u; row < MOD_ROWS; row++) { - marker[row * MOD_ROW_BYTES + EFFECT_BYTE] = E8_EFFECT; - marker[row * MOD_ROW_BYTES + PARAM_BYTE] = E8_PARAM; - } - memcpy(marker + MOD_STOP_MARKER_BYTES, in + samplesOffset, inLength - samplesOffset); - out[MOD_SONG_LENGTH_OFFSET] = (uint8_t)(songLength + 1u); - out[MOD_ORDER_TABLE_OFFSET + songLength] = (uint8_t)patternCount; - return true; -} diff --git a/src/core/modStopMarker.h b/src/core/modStopMarker.h index 78ff5e9..d72cd7f 100644 --- a/src/core/modStopMarker.h +++ b/src/core/modStopMarker.h @@ -29,6 +29,6 @@ // hold inLength + MOD_STOP_MARKER_BYTES. False, out untouched, when the // module cannot be marked: no four-channel Protracker magic, a song length out of range, // every pattern number used, or fewer bytes than its patterns need. -bool modStopMarkerAdd(const uint8_t *in, uint32_t inLength, uint8_t *out); +bool jlModStopMarkerAdd(const uint8_t *in, uint32_t inLength, uint8_t *out); #endif diff --git a/src/core/music.c b/src/core/music.c index dcf77e0..c5c98ae 100644 --- a/src/core/music.c +++ b/src/core/music.c @@ -79,7 +79,7 @@ static bool gMusicPlaying = false; static bool gMusicPaused = false; // Sticky per-voice state, re-applied through the HAL on change (and -// on every pump on the IIgs -- see musicPump). Slot 3 is the noise +// on every pump on the IIgs -- see jlMusicPump). Slot 3 is the noise // voice: gMusicFreq holds its 0..31 pitch and gMusicOn distinguishes // "silent" from "pitch 0", which is a valid bright hiss. static bool gMusicOn[MUSIC_VOICE_SLOTS]; @@ -147,6 +147,85 @@ bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop) { } +void jlMusicPump(void) { + uint32_t now; + uint16_t eventsLeft; + uint16_t tempo; + uint8_t op; + uint8_t voice; + + if (!gMusicPlaying || gMusicPaused) { + return; + } + now = jlMillisElapsed(); + if ((int32_t)(now - gMusicNextMs) < 0) { + return; + } + if (now - gMusicNextMs > MUSIC_RESYNC_MS) { + gMusicNextMs = now; + } + + // The event budget counts DOWN at the end of the body: as a `> 0u` + // guard llvm816 compiled it to `cmp #0 / inc a / bne`, and the inc + // clobbers the compare's flags (the same backend hole that broke + // Space Taxi's collision rows on the IIgs, 2026-09-21). + eventsLeft = MUSIC_MAX_EVENTS_PER_PUMP; + while (gMusicPlaying && (int32_t)(now - gMusicNextMs) >= 0) { + if (gMusicPos >= gMusicLength) { + musicHalt(); + return; + } + op = gMusicEvents[gMusicPos]; + if ((op & MUSIC_OP_WAIT) != 0u) { + gMusicPos++; + gMusicNextMs += (uint32_t)(op & MUSIC_WAIT_MASK) * gMusicTickMs; + } else if (op >= MUSIC_OP_NOTE && op < MUSIC_OP_NOTE + MUSIC_VOICE_SLOTS) { + if (gMusicPos + MUSIC_OP_NOTE_BYTES > gMusicLength) { + musicHalt(); + return; + } + voice = (uint8_t)(op - MUSIC_OP_NOTE); + gMusicFreq[voice] = (uint16_t)(gMusicEvents[gMusicPos + 1] | ((uint16_t)gMusicEvents[gMusicPos + 2] << 8)); + gMusicAtten[voice] = gMusicEvents[gMusicPos + 3]; + gMusicOn[voice] = (voice == MUSIC_NOISE_SLOT) || gMusicFreq[voice] != 0u; + gMusicPos += MUSIC_OP_NOTE_BYTES; + musicApplyVoice(voice); + } else if (op >= MUSIC_OP_OFF && op < MUSIC_OP_OFF + MUSIC_VOICE_SLOTS) { + voice = (uint8_t)(op - MUSIC_OP_OFF); + gMusicOn[voice] = false; + gMusicPos++; + musicApplyVoice(voice); + } else if (op == MUSIC_OP_TEMPO) { + if (gMusicPos + MUSIC_OP_TEMPO_BYTES > gMusicLength) { + musicHalt(); + return; + } + tempo = (uint16_t)(gMusicEvents[gMusicPos + 1] | ((uint16_t)gMusicEvents[gMusicPos + 2] << 8)); + if (tempo == 0u || tempo > MUSIC_TICK_MS_MAX) { + musicHalt(); + return; + } + gMusicTickMs = tempo; + gMusicPos += MUSIC_OP_TEMPO_BYTES; + } else if (op == MUSIC_OP_END) { + if (gMusicLooping) { + gMusicPos = (gMusicLoop == (uint16_t)MUSIC_LOOP_NONE) ? 0u : (uint32_t)gMusicLoop; + } else { + musicHalt(); + return; + } + } else { + musicHalt(); // malformed opcode: hard stop + return; + } + eventsLeft--; + if (eventsLeft == 0u) { + break; + } + } +} + + void jlMusicResume(void) { uint8_t voice; @@ -227,82 +306,3 @@ static void musicHalt(void) { gMusicLength = 0; gMusicPos = 0; } - - -void musicPump(void) { - uint32_t now; - uint16_t eventsLeft; - uint16_t tempo; - uint8_t op; - uint8_t voice; - - if (!gMusicPlaying || gMusicPaused) { - return; - } - now = jlMillisElapsed(); - if ((int32_t)(now - gMusicNextMs) < 0) { - return; - } - if (now - gMusicNextMs > MUSIC_RESYNC_MS) { - gMusicNextMs = now; - } - - // The event budget counts DOWN at the end of the body: as a `> 0u` - // guard llvm816 compiled it to `cmp #0 / inc a / bne`, and the inc - // clobbers the compare's flags (the same backend hole that broke - // Space Taxi's collision rows on the IIgs, 2026-09-21). - eventsLeft = MUSIC_MAX_EVENTS_PER_PUMP; - while (gMusicPlaying && (int32_t)(now - gMusicNextMs) >= 0) { - if (gMusicPos >= gMusicLength) { - musicHalt(); - return; - } - op = gMusicEvents[gMusicPos]; - if ((op & MUSIC_OP_WAIT) != 0u) { - gMusicPos++; - gMusicNextMs += (uint32_t)(op & MUSIC_WAIT_MASK) * gMusicTickMs; - } else if (op >= MUSIC_OP_NOTE && op < MUSIC_OP_NOTE + MUSIC_VOICE_SLOTS) { - if (gMusicPos + MUSIC_OP_NOTE_BYTES > gMusicLength) { - musicHalt(); - return; - } - voice = (uint8_t)(op - MUSIC_OP_NOTE); - gMusicFreq[voice] = (uint16_t)(gMusicEvents[gMusicPos + 1] | ((uint16_t)gMusicEvents[gMusicPos + 2] << 8)); - gMusicAtten[voice] = gMusicEvents[gMusicPos + 3]; - gMusicOn[voice] = (voice == MUSIC_NOISE_SLOT) || gMusicFreq[voice] != 0u; - gMusicPos += MUSIC_OP_NOTE_BYTES; - musicApplyVoice(voice); - } else if (op >= MUSIC_OP_OFF && op < MUSIC_OP_OFF + MUSIC_VOICE_SLOTS) { - voice = (uint8_t)(op - MUSIC_OP_OFF); - gMusicOn[voice] = false; - gMusicPos++; - musicApplyVoice(voice); - } else if (op == MUSIC_OP_TEMPO) { - if (gMusicPos + MUSIC_OP_TEMPO_BYTES > gMusicLength) { - musicHalt(); - return; - } - tempo = (uint16_t)(gMusicEvents[gMusicPos + 1] | ((uint16_t)gMusicEvents[gMusicPos + 2] << 8)); - if (tempo == 0u || tempo > MUSIC_TICK_MS_MAX) { - musicHalt(); - return; - } - gMusicTickMs = tempo; - gMusicPos += MUSIC_OP_TEMPO_BYTES; - } else if (op == MUSIC_OP_END) { - if (gMusicLooping) { - gMusicPos = (gMusicLoop == (uint16_t)MUSIC_LOOP_NONE) ? 0u : (uint32_t)gMusicLoop; - } else { - musicHalt(); - return; - } - } else { - musicHalt(); // malformed opcode: hard stop - return; - } - eventsLeft--; - if (eventsLeft == 0u) { - break; - } - } -} diff --git a/src/core/musicInternal.h b/src/core/musicInternal.h index fe7de7f..4354b43 100644 --- a/src/core/musicInternal.h +++ b/src/core/musicInternal.h @@ -11,6 +11,6 @@ #ifndef JOEYLIB_MUSIC_INTERNAL_H #define JOEYLIB_MUSIC_INTERNAL_H -void musicPump(void); +void jlMusicPump(void); #endif diff --git a/src/core/palette.c b/src/core/palette.c index 9dec2cf..3a00fd4 100644 --- a/src/core/palette.c +++ b/src/core/palette.c @@ -24,7 +24,7 @@ typedef char paletteRowStrideMatchesShift[((1u << 5) == (JL_SURFACE_COLORS_PER_P // Standard 16-color EGA palette in IIgs $0RGB format. Used as the -// per-surface default at allocation time (paletteInitDefault) so a +// per-surface default at allocation time (jlPaletteInitDefault) so a // program that draws without first calling jlPaletteSet still gets // recognizable colors instead of an all-black palette. EGA index 6 // is the canonical "brown" hack ($0A50, half-green) so CGA monitors @@ -51,7 +51,7 @@ static const uint16_t kDefaultPaletteEga[JL_SURFACE_COLORS_PER_PALETTE] = { // ----- Internal API ----- -void paletteInitDefault(jlSurfaceT *s) { +void jlPaletteInitDefault(jlSurfaceT *s) { uint8_t i; if (s == NULL) { @@ -61,7 +61,7 @@ void paletteInitDefault(jlSurfaceT *s) { // so copy it directly into each palette row instead of routing // through jlPaletteSet 16 times (which would re-mask every entry // and re-run the stage-dirty check on a surface that is not yet - // the stage). gStagePaletteDirty already defaults to true so the + // the stage). jlgStagePaletteDirty already defaults to true so the // default palette is still uploaded on first present. for (i = 0; i < JL_SURFACE_PALETTE_COUNT; i++) { memcpy(s->palette[i], kDefaultPaletteEga, sizeof(kDefaultPaletteEga)); @@ -124,8 +124,8 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16) row[13] = colors16[13]; row[14] = colors16[14]; row[15] = colors16[15]; - if (s == gStage) { - gStagePaletteDirty = true; - gStagePaletteDirtyMask |= (uint16_t)(1u << paletteIndex); + if (s == jlgStage) { + jlgStagePaletteDirty = true; + jlgStagePaletteDirtyMask |= (uint16_t)(1u << paletteIndex); } } diff --git a/src/core/port.h b/src/core/port.h index 63563a5..b710adf 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -32,6 +32,7 @@ #include "joey/serial.h" #include "joey/sprite.h" #include "joey/surface.h" +#include "joey/tile.h" // Every op's declaration + its generic/override dispatch lives in the "Op // dispatch" section at the bottom of this header. The block immediately below @@ -68,56 +69,55 @@ // back to its C span fill). // ===================================================================== -extern void iigsDrawPixelInner (uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble); -extern void iigsDrawPixelMark (uint16_t x, uint16_t y, uint16_t nibble); -extern void iigsDrawLineInner (uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble); -extern void iigsDrawCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); -extern void iigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); -extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); -extern void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord); -extern void iigsTileFillInner (uint8_t *dstRow0, uint16_t nibble); -extern void iigsTileFillMark (uint16_t nibble, uint16_t bx, uint16_t by); -extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0); -extern void iigsTileCopyMark (const uint8_t *srcRow0, uint16_t dstBx, uint16_t dstBy); -extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent); -extern void iigsTilePasteInner (uint8_t *dstRow0, const uint8_t *srcTilePixels); -extern void iigsTilePasteMark (const uint8_t *srcTilePixels, uint16_t bx, uint16_t by); -extern void iigsTileMapPasteMark (uint16_t bx, uint16_t by, uint16_t wTiles, uint16_t hTiles, const uint8_t *tiles, const uint8_t *map); -extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor); -extern void iigsTilePasteGlyphMark(const uint8_t *bits, uint16_t bx, uint16_t by, uint16_t fgColor, uint16_t bgColor); -extern void iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0); -extern void iigsCopyRectRows (uint8_t *dst, uint16_t srcLo, uint16_t srcBank, uint16_t rowBytes, uint16_t rows); -extern void iigsFillRectMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble); -extern void iigsFillRectNoMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble); -extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp); -extern uint16_t gFloodSeedMatch; -extern uint16_t gFloodLeftX; -extern uint16_t gFloodRightX; +extern void jlIigsDrawPixelInner (uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble); +extern void jlIigsDrawPixelMark (uint16_t x, uint16_t y, uint16_t nibble); +extern void jlIigsDrawLineInner (uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble); +extern void jlIigsDrawCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); +extern void jlIigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); +extern void jlIigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); +extern void jlIigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord); +extern void jlIigsTileFillInner (uint8_t *dstRow0, uint16_t nibble); +extern void jlIigsTileFillMark (uint16_t nibble, uint16_t bx, uint16_t by); +extern void jlIigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0); +extern void jlIigsTileCopyMark (const uint8_t *srcRow0, uint16_t dstBx, uint16_t dstBy); +extern void jlIigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent); +extern void jlIigsTilePasteInner (uint8_t *dstRow0, const uint8_t *srcTilePixels); +extern void jlIigsTilePasteMark (const uint8_t *srcTilePixels, uint16_t bx, uint16_t by); +extern void jlIigsTileMapPasteMark (uint16_t bx, uint16_t by, uint16_t wTiles, uint16_t hTiles, const uint8_t *tiles, const uint8_t *map); +extern void jlIigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor); +extern void jlIigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0); +extern void jlIigsCopyRectRows (uint8_t *dst, uint16_t srcLo, uint16_t srcBank, uint16_t rowBytes, uint16_t rows); +extern void jlIigsFillRectMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble); +extern void jlIigsFillRectNoMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble); +extern void jlIigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp); +extern uint16_t jlgFloodSeedMatch; +extern uint16_t jlgFloodLeftX; +extern uint16_t jlgFloodRightX; // Stage identity for the stage-gated macros below: a direct extern -// read of surface.c's gStage (also declared in surfaceInternal.h -- +// read of surface.c's jlgStage (also declared in surfaceInternal.h -- // duplicated here because port.h is included by TUs that never pull in // surfaceInternal.h) instead of the jlStageGet() accessor, which costs // a cross-segment JSL per primitive/span (PERF-AUDIT #10/#17). Same // direct-extern precedent as codegenArenaInternal.h's arena globals. // Read-only here. -extern jlSurfaceT *gStage; +extern jlSurfaceT *jlgStage; // drawPixel: migrated to jlpDrawPixel. The asm plots into s->pixels for any // IIgs (chunky) surface; no generic defer needed. #define jlpDrawPixel(_s, _x, _y, _c) \ - iigsDrawPixelInner((_s)->pixels, (uint16_t)(_x), (uint16_t)(_y), \ + jlIigsDrawPixelInner((_s)->pixels, (uint16_t)(_x), (uint16_t)(_y), \ (uint16_t)((_c) & 0x0F)) // drawLine / drawCircle migrated to jlp<Op>: asm on any IIgs (chunky) surface, -// callers gate the on-surface fast path on JL_HAS_DRAW_LINE / _CIRCLE. +// for core/draw.c's on-surface fast paths. #define jlpDrawLine(_s, _x0, _y0, _x1, _y1, _c) \ - iigsDrawLineInner((_s)->pixels, (uint16_t)(_x0), (uint16_t)(_y0), \ + jlIigsDrawLineInner((_s)->pixels, (uint16_t)(_x0), (uint16_t)(_y0), \ (uint16_t)(_x1), (uint16_t)(_y1), \ (uint16_t)((_c) & 0x0F)) #define jlpDrawCircle(_s, _cx, _cy, _r, _c) \ - iigsDrawCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \ + jlIigsDrawCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \ (_r), (uint16_t)((_c) & 0x0F)) // fillCircle migrated to jlpFillCircle: stage-only asm, returns bool so the core @@ -127,8 +127,8 @@ extern jlSurfaceT *gStage; // to a software-multiply helper call for a runtime color (the only call // site, draw.c, always passes a variable). The jlp layer owns the & 0x0F. #define jlpFillCircle(_s, _cx, _cy, _r, _c) \ - ((_s) == gStage \ - ? (iigsFillCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \ + ((_s) == jlgStage \ + ? (jlIigsFillCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \ (_r), (uint16_t)((_c) & 0x0F)), \ true) \ : false) @@ -136,24 +136,24 @@ extern jlSurfaceT *gStage; // surfaceClear: migrated to the jlp<Op> override model. The asm clears the // stage surface; non-stage surfaces defer to the portable-C generic default. #define jlpSurfaceClear(_s, _d) \ - ((_s) == gStage \ - ? iigsSurfaceClearFastInner((_s)->pixels, \ + ((_s) == jlgStage \ + ? jlIigsSurfaceClearFastInner((_s)->pixels, \ (uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))) \ : jlpGenericSurfaceClear((_s), (_d))) // fillRect: STAGE-GATED onto the FUSED Mark entry (NATIVE-PERF Phase 6 // W1, the tile Step-D pattern): ONE JSL derives the destination in asm -// from the pinned stage base + gRowOffsetLut, fills W x H with the same +// from the pinned stage base + jlgRowOffsetLut, fills W x H with the same // leading/middle/trailing nibble handling as the old Inner, and widens -// the H-row dirty band via stageRectMark -- replacing the fill JSL plus -// the C-side surfaceMarkDirtyRect -> iigsMarkDirtyRowsInner JSL pair +// the H-row dirty band via jlStageRectMark -- replacing the fill JSL plus +// the C-side surfaceMarkDirtyRect -> jlIigsMarkDirtyRowsInner JSL pair // (draw.c compiles that mark out under JL_FILL_RECT_MARK_FUSED below). // Non-stage surfaces defer to the chunky generic and need no mark. The // asm derives the fill word from the raw nibble (PERF-AUDIT #41); the // jlp layer owns the & 0x0F. #define jlpFillRect(_s, _x, _y, _w, _h, _c) \ - ((_s) == gStage \ - ? iigsFillRectMark((uint16_t)(_x), (uint16_t)(_y), \ + ((_s) == jlgStage \ + ? jlIigsFillRectMark((uint16_t)(_x), (uint16_t)(_y), \ (uint16_t)(_w), (uint16_t)(_h), \ (uint16_t)((_c) & 0x0F)) \ : jlpGenericFillRect((_s), (_x), (_y), (_w), (_h), (_c))) @@ -162,10 +162,10 @@ extern jlSurfaceT *gStage; // on-surface fast path keeps its single whole-outline mark instead of // paying two (h-2)-row fused walks on the vertical edges; jlFillCircle // batches one bounding-square mark over its spans). Same body in asm; -// a flag skips the stageRectMark tail. +// a flag skips the jlStageRectMark tail. #define jlpFillRectNoMark(_s, _x, _y, _w, _h, _c) \ - ((_s) == gStage \ - ? iigsFillRectNoMark((uint16_t)(_x), (uint16_t)(_y), \ + ((_s) == jlgStage \ + ? jlIigsFillRectNoMark((uint16_t)(_x), (uint16_t)(_y), \ (uint16_t)(_w), (uint16_t)(_h), \ (uint16_t)((_c) & 0x0F)) \ : jlpGenericFillRect((_s), (_x), (_y), (_w), (_h), (_c))) @@ -180,17 +180,17 @@ extern jlSurfaceT *gStage; // tileFill / tileCopy / tilePaste are STAGE-GATED onto the FUSED Mark // entries (NATIVE-PERF Phase 2 Step D / R2): for the stage, ONE JSL // derives the destination in asm from the pinned stage base + the -// gRowOffsetLut, runs the op, and widens the dirty band -- replacing -// C-side pointer math plus two JSLs (op inner + iigsMarkDirtyRowsInner +// jlgRowOffsetLut, runs the op, and widens the dirty band -- replacing +// C-side pointer math plus two JSLs (op inner + jlIigsMarkDirtyRowsInner // via tile.c's tileMarkDirty, which is compiled out for these three // ops on IIgs). Non-stage surfaces keep the pointer-based inners. // tileFill: the macro passes the raw nibble (the jlp layer owns the & 0x0F); // the asm derives the $NNNN fill word (PERF-AUDIT #41 -- the old C-side // * 0x1111 lowered to a software-multiply helper for a runtime color). #define jlpTileFill(_s, _bx, _by, _c) \ - ((_s) == gStage \ - ? iigsTileFillMark((uint16_t)((_c) & 0x0F), (uint16_t)(_bx), (uint16_t)(_by)) \ - : iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \ + ((_s) == jlgStage \ + ? jlIigsTileFillMark((uint16_t)((_c) & 0x0F), (uint16_t)(_bx), (uint16_t)(_by)) \ + : jlIigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \ + ((uint16_t)(_bx) << 2)], \ (uint16_t)((_c) & 0x0F))) @@ -199,36 +199,25 @@ extern jlSurfaceT *gStage; // tileCopy's src may be ANY surface, so its row-0 pointer stays C-derived // even on the fused stage-dst arm. #define jlpTileCopy(_d, _dbx, _dby, _s, _sbx, _sby) \ - ((_d) == gStage \ - ? iigsTileCopyMark(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_sby) << 3) + ((uint16_t)(_sbx) << 2)], (uint16_t)(_dbx), (uint16_t)(_dby)) \ - : iigsTileCopyInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_dby) << 3) + ((uint16_t)(_dbx) << 2)], \ + ((_d) == jlgStage \ + ? jlIigsTileCopyMark(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_sby) << 3) + ((uint16_t)(_sbx) << 2)], (uint16_t)(_dbx), (uint16_t)(_dby)) \ + : jlIigsTileCopyInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_dby) << 3) + ((uint16_t)(_dbx) << 2)], \ &(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_sby) << 3) + ((uint16_t)(_sbx) << 2)])) #define jlpTileCopyMasked(_d, _dbx, _dby, _s, _sbx, _sby, _t) \ - iigsTileCopyMaskedInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_dby) << 3) + ((uint16_t)(_dbx) << 2)], \ + jlIigsTileCopyMaskedInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_dby) << 3) + ((uint16_t)(_dbx) << 2)], \ &(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_sby) << 3) + ((uint16_t)(_sbx) << 2)], \ (uint16_t)(_t)) // tilePaste / tileSnap: take surface+coords + the chunky tile buffer; the asm // macros derive row-0. #define jlpTilePaste(_d, _bx, _by, _ct) \ - ((_d) == gStage \ - ? iigsTilePasteMark((_ct), (uint16_t)(_bx), (uint16_t)(_by)) \ - : iigsTilePasteInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)], (_ct))) + ((_d) == jlgStage \ + ? jlIigsTilePasteMark((_ct), (uint16_t)(_bx), (uint16_t)(_by)) \ + : jlIigsTilePasteInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)], (_ct))) #define jlpTileSnap(_s, _bx, _by, _out) \ - iigsTileSnapInner((_out), &(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)]) - -// tilePasteGlyph: the 1bpp-glyph paste with the dirty mark FUSED into the -// asm (one JSL, no separate iigsMarkDirtyRowsInner call). Stage-only like -// the other stage-gated macros; other surfaces defer to the portable -// generic. fg/bg arrive pre-masked to 4 bits. -#if defined(JL_HAS_TILE_PASTE_GLYPH) -#define jlpTilePasteGlyph(_d, _bx, _by, _b, _fg, _bg) \ - ((_d) == gStage \ - ? iigsTilePasteGlyphMark((_b), (uint16_t)(_bx), (uint16_t)(_by), (uint16_t)(_fg), (uint16_t)(_bg)) \ - : jlpGenericTilePasteGlyph((_d), (_bx), (_by), (_b), (_fg), (_bg))) -#endif + jlIigsTileSnapInner((_out), &(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)]) // tilePasteMono: fused mono colorize + paste in one asm pass (PERF-AUDIT #3) // -- no intermediate jlTileT and no second paste call. Stage-only like the @@ -237,8 +226,8 @@ extern jlSurfaceT *gStage; // public jlTilePasteMono wrapper owns the & 0x0F, PERF-AUDIT #69), so they // are passed through un-re-masked per the dispatch-stanza contract below. #define jlpTilePasteMono(_d, _bx, _by, _mt, _fg, _bg) \ - ((_d) == gStage \ - ? iigsTilePasteMonoInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)], \ + ((_d) == jlgStage \ + ? jlIigsTilePasteMonoInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)], \ (_mt), (uint16_t)(_fg), (uint16_t)(_bg)) \ : jlpGenericTilePasteMono((_d), (_bx), (_by), (_mt), (_fg), (_bg))) @@ -248,19 +237,19 @@ extern jlSurfaceT *gStage; // ports (Amiga + Atari ST) and IIgs never declares them (their call sites are // #if'd out on ports without the JL_HAS_FLOOD_*_PLANES flags). // -// Tier-1 flood: multi-output. Asm sets gFloodSeedMatch / gFloodLeftX / -// gFloodRightX; macro reads those into the caller's out-ptrs. +// Tier-1 flood: multi-output. Asm sets jlgFloodSeedMatch / jlgFloodLeftX / +// jlgFloodRightX; macro reads those into the caller's out-ptrs. #define jlpFloodWalkAndScans(_pix, _x, _y, _mc, _nc, _me, _sx, _sy, _sp, _ms, _smOut, _lxOut, _rxOut) \ - (iigsFloodWalkAndScansInner((_pix), (uint16_t)(_x), (uint16_t)(_y), \ + (jlIigsFloodWalkAndScansInner((_pix), (uint16_t)(_x), (uint16_t)(_y), \ (uint16_t)((_mc) & 0x0F), \ (uint16_t)((_nc) & 0x0F), \ (uint16_t)((_me) ? 1 : 0), \ (_sx), (_sy), \ (uint16_t *)(_sp), \ (uint16_t)(_ms)), \ - *(_smOut) = (gFloodSeedMatch != 0), \ - *(_lxOut) = (int16_t)gFloodLeftX, \ - *(_rxOut) = (int16_t)gFloodRightX, \ + *(_smOut) = (jlgFloodSeedMatch != 0), \ + *(_lxOut) = (int16_t)jlgFloodLeftX, \ + *(_rxOut) = (int16_t)jlgFloodRightX, \ true) // ===================================================================== @@ -289,11 +278,11 @@ extern jlSurfaceT *gStage; // interrupt task keeps the count; the read is a same-bank word load). // The hal.c C wrapper this replaces was a pure forwarding layer on a // clock that game loops (and the UBER bench loop) poll constantly. -extern uint16_t iigsGetTickWord(void); -#define jlpFrameCount() iigsGetTickWord() +extern uint16_t jlIigsGetTickWord(void); +#define jlpFrameCount() jlIigsGetTickWord() // The one Memory Manager owner ID every IIgs allocation uses (hal.c). -uint16_t iigsMemUserId(void); +uint16_t jlIigsMemUserId(void); // Memory Manager NewHandle attributes, by Apple's names and values (Memory Manager reference). #define attrAddr 0x0002 #define attrPage 0x0004 @@ -304,11 +293,11 @@ uint16_t iigsMemUserId(void); #define IIGS_BANK_BYTES 0x10000ul // Interrupt-latency bound of the long interrupts-off paths (hal.c): a // client that cannot wait ~17 ms for its interrupt holds the tight bound -// while it needs it. Counted, so clients nest. gIigsIrqTight is nonzero +// while it needs it. Counted, so clients nest. jlgIigsIrqTight is nonzero // while any client holds it, for the C paths that bound themselves. -extern uint16_t gIigsIrqTight; -void iigsIrqLatencyRelax(void); -void iigsIrqLatencyTighten(void); +extern uint16_t jlgIigsIrqTight; +void jlIigsIrqLatencyRelax(void); +void jlIigsIrqLatencyTighten(void); // SFX streams go to NTPstreamsound as unsigned bytes with 0x00 forbidden // (joey/audio.h JL_SFX_RAW_UNSIGNED8_NOZERO): a raw producer hands @@ -320,11 +309,10 @@ void iigsIrqLatencyTighten(void); // // The dispatch stanzas below short-circuit on #if !defined(jlp<Op>) // before the JL_HAS_<OP> flag is ever tested, and the core fast-path -// gates (JL_HAS_DRAW_LINE / DRAW_CIRCLE / FILL_CIRCLE / -// FLOOD_WALK_AND_SCANS in core/draw.c) consult only the flag. So a -// macro override above whose flag is missing from the -// include/joey/platform.h registry either turns that registry into -// false documentation or silently drops a core fast path back to +// gates (JL_HAS_FILL_CIRCLE / FLOOD_WALK_AND_SCANS in core/draw.c) +// consult only the flag. So a macro override above whose flag is missing +// from the include/joey/platform.h registry either turns that registry +// into false documentation or silently drops a core fast path back to // generic C on the perf-reference machine. These checks make a // missing/removed flag a compile failure instead: one #error per // macro override defined above. (The reverse drift -- flag present, @@ -390,12 +378,14 @@ void iigsIrqLatencyTighten(void); #endif // JOEYLIB_PLATFORM_IIGS // ===================================================================== -// 68k tile ops as always-inline headers (NATIVE-PERF Phase 2 R4a). +// 68k tile and pixel ops as always-inline headers (NATIVE-PERF Phase 2 +// R4a). // // The ST tile overrides live in stTile.h as static inline functions named -// stTile*; the Amiga and X68000 share planarTile* (src/m68k/planarTile.h, -// through the amigaTile.h / x68kTile.h shims). Aliasing the -// jlpTile* dispatch names to them HERE (before the dispatch tail) +// stTile* (the pixel ops stDrawPixel and stSamplePixel); the Amiga and +// X68000 share planarTile* and planarDrawPixel / planarSamplePixel +// (src/m68k/planarTile.h, through the amigaTile.h / x68kTile.h shims). +// Aliasing the jlpTile* dispatch names to them HERE (before the dispatch tail) // makes the tail's #if !defined(jlpTile*) guards skip their extern // prototypes -- same mechanism as the IIgs macro block above -- and // every caller (tile.c wrappers, genericTile.c fallback routing) @@ -405,32 +395,52 @@ void iigsIrqLatencyTighten(void); // declared in the dispatch tail. // ===================================================================== #if defined(JOEYLIB_PLATFORM_ATARIST) - #define jlpTileFill stTileFill - #define jlpTileCopy stTileCopy - #define jlpTileCopyMasked stTileCopyMasked - #define jlpTilePaste stTilePaste - #define jlpTilePasteMono stTilePasteMono - #define jlpTilePasteGlyph stTilePasteGlyph - #define jlpTileSnap stTileSnap + #define jlpTileFill stTileFill + #define jlpTileCopy stTileCopy + #define jlpTileCopyMasked stTileCopyMasked + #define jlpTilePaste stTilePaste + #define jlpTilePasteMono stTilePasteMono + #define jlpTileSnap stTileSnap // Whole-map engine alias (W3 seam): tile.c's jlTileMapPaste // dispatches through this spelling when a port defines it, so the // union-mark stanza lives exactly twice in tile.c (shared arm + // generic) no matter how many ports bring engines. - #define jlpTileMapPaste stTileMapPaste + #define jlpTileMapPaste stTileMapPaste + #define jlpDrawPixel stDrawPixel + #define jlpFillRect stFillRectMark + #define jlpFillRectNoMark stFillRect + #define jlpSamplePixel stSamplePixel + #define jlpSpriteSavePlanes stSpriteSavePlanes + #define jlpSpriteRestorePlanes stSpriteRestorePlanes + // jlpFillRect's dirty mark is in its asm (lineSpan.s), as on the IIgs. + #define JL_FILL_RECT_MARK_FUSED 1 + // jlpPresent leaves the stage's bands clean (presentSlam.s resets each + // dirty group as it walks it), so jlStagePresent does not clear them. + #define JL_PRESENT_CLEARS_BANDS 1 #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) // One set of planar bodies for both (src/m68k/planarTile.h). - #define jlpTileFill planarTileFill - #define jlpTileCopy planarTileCopy - #define jlpTileCopyMasked planarTileCopyMasked - #define jlpTilePaste planarTilePaste - #define jlpTilePasteMono planarTilePasteMono - #define jlpTilePasteGlyph planarTilePasteGlyph - #define jlpTileSnap planarTileSnap - #define jlpTileMapPaste planarTileMapPaste + #define jlpDrawPixel planarDrawPixel + #define jlpSamplePixel planarSamplePixel + #define jlpSpriteSavePlanes planarSpriteSavePlanes + #define jlpSpriteRestorePlanes planarSpriteRestorePlanes + #define jlpTileFill planarTileFill + #define jlpTileCopy planarTileCopy + #define jlpTileCopyMasked planarTileCopyMasked + #define jlpTilePaste planarTilePaste + #define jlpTilePasteMono planarTilePasteMono + #define jlpTileSnap planarTileSnap + #define jlpTileMapPaste planarTileMapPaste + // jlpPresent leaves the stage's bands clean (the dirty-band union clears + // what it reads: src/amiga/presentScan.s, src/x68000/x68kPresent.s), so + // jlStagePresent does not clear them. + #define JL_PRESENT_CLEARS_BANDS 1 + #if defined(JOEYLIB_PLATFORM_X68000) + #define jlpFillRect x68kFillRect + #endif #elif defined(JL_CPU_X86) // DOS and DVX override only the whole-map walker (src/x86/x86Tile.h, // W3); the single-tile ops stay on the chunky generics. - #define jlpTileMapPaste x86TileMapPaste + #define jlpTileMapPaste x86TileMapPaste #endif // ===================================================================== @@ -483,15 +493,24 @@ void jlpGenericDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorInd #endif #endif -// drawLine / drawCircle have no generic default: the portable fallback is the -// clipping Bresenham in core/draw.c, and the on-surface fast path is gated by -// `#if defined(JL_HAS_DRAW_LINE/_CIRCLE)`. So here we only declare the function -// override prototype for non-macro overriders (Amiga/ST). -#if defined(JL_HAS_DRAW_LINE) && !defined(jlpDrawLine) -void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex); +// drawLine's and drawCircle's on-surface fast paths are every port's: the +// machine's, or the chunky generic (src/generic/genericDraw.c); the +// clipping walks in core/draw.c take the rest. +void jlpGenericDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex); +#if !defined(jlpDrawLine) + #if defined(JL_HAS_DRAW_LINE) + void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex); // function override + #else + #define jlpDrawLine(_s, _x0, _y0, _x1, _y1, _c) jlpGenericDrawLine((_s), (_x0), (_y0), (_x1), (_y1), (_c)) + #endif #endif -#if defined(JL_HAS_DRAW_CIRCLE) && !defined(jlpDrawCircle) -void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex); +void jlpGenericDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex); +#if !defined(jlpDrawCircle) + #if defined(JL_HAS_DRAW_CIRCLE) + void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex); // function override + #else + #define jlpDrawCircle(_s, _cx, _cy, _r, _c) jlpGenericDrawCircle((_s), (_cx), (_cy), (_r), (_c)) + #endif #endif // fillCircle returns bool (stage/portData check is inherent); the platform // dispatch is compile-time via JL_HAS_FILL_CIRCLE. @@ -508,6 +527,13 @@ void jlpGenericFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t #endif #endif +// A port whose jlFillRect is its own (JL_HAS_FILL_RECT_ENTRY) fills +// on-surface rects there and hands every other rect, unchanged, to draw.c's +// public body under this name. +#if defined(JL_HAS_FILL_RECT_ENTRY) +void jlFillRectClip(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex); +#endif + // jlpFillRectNoMark: fillRect guaranteed not to touch dirty state. On // every port but the IIgs the ops never mark, so it IS jlpFillRect; // the IIgs macro block above overrides it with the unfused asm twin. @@ -560,6 +586,32 @@ void jlpGenericTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t * #endif #endif +// Tile layers (tileLayerInternal.h): the ring's four ops. A port with a +// ring of its own overrides all four (JL_HAS_TILE_LAYER); one keeping the +// generic chunky rings may override the blit (JL_HAS_TILE_LAYER_BLIT) and +// the put (JL_HAS_TILE_LAYER_PUT). +bool jlpGenericTileLayerRingAlloc(jlTileLayerT *layer); +void jlpGenericTileLayerRingFree(jlTileLayerT *layer); +void jlpGenericTileLayerPut(jlTileLayerT *layer, uint8_t slotCol, uint8_t slotRow, const jlTileT *tile); +void jlpGenericTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY); +#if defined(JL_HAS_TILE_LAYER) + bool jlpTileLayerRingAlloc(jlTileLayerT *layer); + void jlpTileLayerRingFree(jlTileLayerT *layer); +#else + #define jlpTileLayerRingAlloc(_l) jlpGenericTileLayerRingAlloc((_l)) + #define jlpTileLayerRingFree(_l) jlpGenericTileLayerRingFree((_l)) +#endif +#if defined(JL_HAS_TILE_LAYER) || defined(JL_HAS_TILE_LAYER_PUT) + void jlpTileLayerPut(jlTileLayerT *layer, uint8_t slotCol, uint8_t slotRow, const jlTileT *tile); +#else + #define jlpTileLayerPut(_l, _c, _r, _t) jlpGenericTileLayerPut((_l), (_c), (_r), (_t)) +#endif +#if defined(JL_HAS_TILE_LAYER) || defined(JL_HAS_TILE_LAYER_BLIT) + void jlpTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY); +#else + #define jlpTileLayerBlit(_d, _l, _x, _y) jlpGenericTileLayerBlit((_d), (_l), (_x), (_y)) +#endif + // tilePasteMono color-masking contract (PERF-AUDIT #69): fgColor/bgColor // arrive already masked to 4 bits -- the public jlTilePasteMono wrapper owns // the & 0x0F. Neither the generic nor a machine override may re-mask. @@ -823,12 +875,12 @@ static inline uint32_t audioNoiseHz(uint8_t pitch) { } // An effect faster than the port's chip plays (the Amiga's Paula, which -// reads every byte it is given) plays every audioDecimationFactor-th sample +// reads every byte it is given) plays every jlAudioDecimationFactor-th sample // at rateHz over the factor, so at its own pitch (src/core/audio.c); -// audioDecimate picks them, a stream's chunks one after another with its +// jlAudioDecimate picks them, a stream's chunks one after another with its // phase carried between them. (The IIgs's DOC steps over samples itself.) -uint32_t audioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t factor, uint8_t *phase); -uint8_t audioDecimationFactor(uint16_t rateHz, uint16_t maxHz); +uint32_t jlAudioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t factor, uint8_t *phase); +uint8_t jlAudioDecimationFactor(uint16_t rateHz, uint16_t maxHz); bool jlpGenericAudioInit(void); #if !defined(jlpAudioInit) @@ -1027,6 +1079,13 @@ void jlpGenericJoystickReset(jlJoystickE js); #define jlpJoystickReset(_a) jlpGenericJoystickReset((_a)) #endif #endif +// A port whose sticks are slow to read (the IIgs's paddles, timed one +// count at a time) reads their axes and connection when one is first asked +// for after a poll, not at every poll: its poll sets jlgJoyAxesStale[js] +// (inputInternal.h), and jlpJoystickAxesRead reads stick js and clears it. +#if defined(JL_HAS_JOYSTICK_AXES_READ) + void jlpJoystickAxesRead(jlJoystickE js); +#endif // --- Serial add-on (platform-only; blank-port stub generic + JL_HAS override) --- @@ -1109,7 +1168,7 @@ void jlpGenericSerialSetRts(bool on); #endif #endif // A port that feeds its transmitter a byte at a time, polling until it takes -// each, bounds the wait with these (src/core/serial.c): serialTxStalled turns +// each, bounds the wait with these (src/core/serial.c): jlSerialTxStalled turns // true once the transmitter has taken nothing for SERIAL_TX_STALL_MS (a peer // holding transmit off, a line held), or after pollLimit polls, which is // what stops a wait on a clock that does not run yet (before jlInit). @@ -1118,8 +1177,8 @@ typedef struct { uint32_t polls; uint32_t pollLimit; } SerialTxWaitT; -bool serialTxStalled(SerialTxWaitT *wait); -void serialTxWaitStart(SerialTxWaitT *wait, uint32_t pollLimit); +bool jlSerialTxStalled(SerialTxWaitT *wait); +void jlSerialTxWaitStart(SerialTxWaitT *wait, uint32_t pollLimit); // --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) --- diff --git a/src/core/present.c b/src/core/present.c index 2415c0c..f808383 100644 --- a/src/core/present.c +++ b/src/core/present.c @@ -23,10 +23,12 @@ void jlStagePresent(void) { // Direct extern read (finding #10/#17 convention): once per present, // so this is consistency rather than a hot-path win. - stage = gStage; + stage = jlgStage; if (stage == NULL) { return; } jlpPresent(stage); - stageDirtyClearAll(); +#if !defined(JL_PRESENT_CLEARS_BANDS) + jlStageDirtyClearAll(); +#endif } diff --git a/src/core/psgSynth.c b/src/core/psgSynth.c index 20df2d2..a1bed6d 100644 --- a/src/core/psgSynth.c +++ b/src/core/psgSynth.c @@ -76,7 +76,7 @@ static void mixEveryChannel(int32_t *frame, uint8_t channels, int16_t level) { // ----- Synth API (alphabetical) ----- -void psgSynthInit(uint32_t rate) { +void jlPsgSynthInit(uint32_t rate) { gRate = rate; memset(gVoices, 0, sizeof(gVoices)); gNoisePhase = 0u; @@ -89,7 +89,7 @@ void psgSynthInit(uint32_t rate) { } -void psgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels) { +void jlPsgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels) { SquareT *sq; uint32_t i; uint32_t clocks; @@ -122,7 +122,7 @@ void psgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels) { } -void psgSynthNoise(uint8_t pitch, uint8_t atten) { +void jlPsgSynthNoise(uint8_t pitch, uint8_t atten) { uint32_t period; if (gRate == 0u) { @@ -134,7 +134,7 @@ void psgSynthNoise(uint8_t pitch, uint8_t atten) { } -bool psgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz) { +bool jlPsgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz) { if (fn != NULL && (hz == 0u || hz > gRate)) { return false; } @@ -145,7 +145,7 @@ bool psgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz) { } -uint32_t psgSynthTickRun(uint32_t frames) { +uint32_t jlPsgSynthTickRun(uint32_t frames) { uint32_t run; if (gTickFn == NULL) { @@ -169,7 +169,7 @@ uint32_t psgSynthTickRun(uint32_t frames) { } -void psgSynthVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { +void jlPsgSynthVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { if (voice >= JL_AUDIO_VOICES || gRate == 0u) { return; } diff --git a/src/core/psgSynth.h b/src/core/psgSynth.h index 898be03..142a0f4 100644 --- a/src/core/psgSynth.h +++ b/src/core/psgSynth.h @@ -11,7 +11,7 @@ // exactly the samples it falls between, however the mixing is batched, so // notes start where they should in the sound. // -// One synth a program: module state, set up by psgSynthInit at the mix +// One synth a program: module state, set up by jlPsgSynthInit at the mix // rate. Every call runs where the mixer runs (the program's thread, or // DVX's sound pump), never in an interrupt. @@ -29,20 +29,20 @@ #define PSG_SYNTH_PEAK 8191 // Silence, every voice and the noise, and no tick, at rate frames a second. -void psgSynthInit(uint32_t rate); +void jlPsgSynthInit(uint32_t rate); // Add frames of the voices and the noise to mix (channels interleaved // samples a frame, each voice in every channel). -void psgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels); +void jlPsgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels); // jlAudioNoise and jlAudioVoice (voice 0 .. JL_AUDIO_VOICES - 1). -void psgSynthNoise(uint8_t pitch, uint8_t atten); -void psgSynthVoice(uint8_t voice, uint16_t freqHz, uint8_t atten); +void jlPsgSynthNoise(uint8_t pitch, uint8_t atten); +void jlPsgSynthVoice(uint8_t voice, uint16_t freqHz, uint8_t atten); // jlAudioTickRegister: fn hz times a second of the mix (NULL: none). False // for a rate of 0 or above the mix rate. -bool psgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz); +bool jlPsgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz); // The mixer is about to mix up to frames frames: every tick due before the // first of them runs now, and the frames to mix before the next tick come // back (frames when there is none in them). The mixer mixes exactly that // many, then asks again. -uint32_t psgSynthTickRun(uint32_t frames); +uint32_t jlPsgSynthTickRun(uint32_t frames); #endif diff --git a/src/core/scb.c b/src/core/scb.c index 3634fec..3f080a1 100644 --- a/src/core/scb.c +++ b/src/core/scb.c @@ -32,8 +32,8 @@ void jlScbSet(jlSurfaceT *s, uint16_t line, uint8_t paletteIndex) { return; } s->scb[line] = paletteIndex; - if (s == gStage) { - gStageScbDirty = true; + if (s == jlgStage) { + jlgStageScbDirty = true; } } @@ -64,7 +64,7 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t // libc memset is a tight fill (the IIgs's 16-bit stores since the // 2026-10-04 runtime, 5.9 cycles a byte). memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1)); - if (s == gStage) { - gStageScbDirty = true; + if (s == jlgStage) { + jlgStageScbDirty = true; } } diff --git a/src/core/serial.c b/src/core/serial.c index 1a9771b..40d5bbf 100644 --- a/src/core/serial.c +++ b/src/core/serial.c @@ -33,7 +33,7 @@ #define SERIAL_STOP_BITS_DEF 1u #define SERIAL_BAUD_DEF 9600u -// A transmit wait gives up after this long (serialTxStalled). The clock is +// A transmit wait gives up after this long (jlSerialTxStalled). The clock is // read every SERIAL_TX_CLOCK_POLLS polls, from the first such read on, so a // byte the transmitter takes at once costs no clock read at all. #define SERIAL_TX_STALL_MS 100u @@ -169,14 +169,8 @@ bool jlSerialWriteByte(uint8_t b) { // ----- Internal ----- -// The atexit handler (see the header comment): closes a port left open. -static void serialAtExit(void) { - jlSerialClose(); -} - - // One more poll of a busy transmitter (port.h): true once the wait is over. -bool serialTxStalled(SerialTxWaitT *wait) { +bool jlSerialTxStalled(SerialTxWaitT *wait) { uint32_t now; wait->polls++; @@ -196,7 +190,13 @@ bool serialTxStalled(SerialTxWaitT *wait) { // A transmit wait begins (port.h): pollLimit is the port's backstop. -void serialTxWaitStart(SerialTxWaitT *wait, uint32_t pollLimit) { +void jlSerialTxWaitStart(SerialTxWaitT *wait, uint32_t pollLimit) { wait->polls = 0u; wait->pollLimit = pollLimit; } + + +// The atexit handler (see the header comment): closes a port left open. +static void serialAtExit(void) { + jlSerialClose(); +} diff --git a/src/core/sprite.c b/src/core/sprite.c index 1beb36d..e1a6165 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -55,11 +55,14 @@ #define COMPILED_SPRITE_WRITES_PLANES 0 #endif +// A saved region widened to SPRITE_BACKUP_ALIGN stays on the surface. +typedef char spriteBackupAlignProof[(JL_SURFACE_WIDTH % SPRITE_BACKUP_ALIGN == 0) ? 1 : -1]; + // ----- Sprite-region dirty marks (NATIVE-PERF Phase 6 W1) ----- // // Every compiled sprite draw/restore used to pay the generic -// surfaceMarkDirtyRect macro -> out-of-line surfaceMarkDirtyRows loop +// surfaceMarkDirtyRect macro -> out-of-line jlSurfaceMarkDirtyRows loop // (~800 cyc for 8 rows measured on the 68000 ports; ~1.3-1.6k for the // 16-row sprite region) -- the same per-call tax tile.c's Step-B mark // removed for tiles. spriteMarkDirty derives the word band ONCE with @@ -75,9 +78,9 @@ // spriteMarkDirtyRestore -- after a compiled restore. // On the IIgs all three spellings expand to surfaceMarkDirtyRect, // token-identical to today's path (band derive inline, one JSL into -// iigsMarkDirtyRowsInner; a C unroll of 16 long-abs RMW pairs would +// jlIigsMarkDirtyRowsInner; a C unroll of 16 long-abs RMW pairs would // REGRESS the 65816). The IIgs stage sprite entries (joeyDraw.s) take -// ownership DISPATCH-side, not here: an s == gStage arm in the compiled +// ownership DISPATCH-side, not here: an s == jlgStage arm in the compiled // path calls the asm entry (which widens the band in-section) and // returns BEFORE these C marks, so the spellings stay live only for // the non-stage compiled paths, where surfaceMarkDirtyRect's stage @@ -95,14 +98,14 @@ // ----- Sprite registry (claudeDocs/PERF-AUDIT.md #34) ----- // -// jlShutdown frees the codegen arena (codegenArenaShutdown) while +// jlShutdown frees the codegen arena (jlCodegenArenaShutdown) while // user-owned sprites may still hold ArenaSlotT pointers. If such a // sprite survives a shutdown -> re-init cycle, jlSpriteDestroy would // free a stale slot (heap corruption) and jlSpriteDraw would execute // stale offsets inside the NEW arena. The registry tracks live -// sprites so spriteSystemShutdown (called from jlShutdown BEFORE -// codegenArenaShutdown) can reset each one's compiled-path state; the -// arena bytes themselves are released by codegenArenaShutdown. +// sprites so jlSpriteSystemShutdown (called from jlShutdown BEFORE +// jlCodegenArenaShutdown) can reset each one's compiled-path state; the +// arena bytes themselves are released by jlCodegenArenaShutdown. // // A list linked through the sprites themselves (registryPrev/Next), so // every live sprite is on it, however many there are. @@ -114,7 +117,7 @@ static jlSpriteT *gSpriteRegistry = NULL; // For a source byte b (two 4bpp pixels), the destination bits it leaves // alone: a nibble of ones where that pixel is transparent (colour 0), so // *dst = (*dst & NIBBLE_KEEP(b)) | b merges both pixels with no tests. The -// IIgs looks it up in gSpriteNibbleKeep; the other chunky ports bake it into +// IIgs looks it up in jlgSpriteNibbleKeep; the other chunky ports bake it into // each sprite's phase images. #define NIBBLE_KEEP(_b) ((uint8_t)((((_b) & 0xF0u) != 0u ? 0x00u : 0xF0u) | (((_b) & 0x0Fu) != 0u ? 0x00u : 0x0Fu))) #endif @@ -129,8 +132,10 @@ static jlSpriteT *gSpriteRegistry = NULL; // loop merges bytes, so padding would only cost it). #define PHASE1_ROW_EXTRA 1u #endif -#if defined(JL_CPU_X86) && !defined(JOEYLIB_PLATFORM_HOSTED) -_Static_assert(JL_SURFACE_BYTES_PER_ROW == 160, "src/x86/spriteMergeRows.s's merge block covers 160-byte rows"); +#if defined(JL_CPU_X86) +// The merge block (src/x86/spriteMergeRows.s, src/hosted/win16/spriteMergeRows.asm) +// covers 160-byte rows. A typedef, not _Static_assert: Watcom has none. +typedef char spriteMergeRowsProof[(JL_SURFACE_BYTES_PER_ROW == 160) ? 1 : -1]; #endif #if defined(JOEYLIB_PLATFORM_IIGS) @@ -139,7 +144,7 @@ _Static_assert(JL_SURFACE_BYTES_PER_ROW == 160, "src/x86/spriteMergeRows.s's mer NIBBLE_KEEP((_h) | 0x8u), NIBBLE_KEEP((_h) | 0x9u), NIBBLE_KEEP((_h) | 0xAu), NIBBLE_KEEP((_h) | 0xBu), \ NIBBLE_KEEP((_h) | 0xCu), NIBBLE_KEEP((_h) | 0xDu), NIBBLE_KEEP((_h) | 0xEu), NIBBLE_KEEP((_h) | 0xFu) -const uint8_t gSpriteNibbleKeep[256] = { +const uint8_t jlgSpriteNibbleKeep[256] = { NIBBLE_KEEP_16(0x00u), NIBBLE_KEEP_16(0x10u), NIBBLE_KEEP_16(0x20u), NIBBLE_KEEP_16(0x30u), NIBBLE_KEEP_16(0x40u), NIBBLE_KEEP_16(0x50u), NIBBLE_KEEP_16(0x60u), NIBBLE_KEEP_16(0x70u), NIBBLE_KEEP_16(0x80u), NIBBLE_KEEP_16(0x90u), NIBBLE_KEEP_16(0xA0u), NIBBLE_KEEP_16(0xB0u), @@ -180,7 +185,7 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, in static void spriteMarkDirty(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h); #endif #if defined(JOEYLIB_PLATFORM_IIGS) -void iigsSpriteInterpRows(IigsSpriteRowsT *job); +void jlIigsSpriteInterpRows(IigsSpriteRowsT *job); #endif static void spriteRegistryRemove(jlSpriteT *sp); static void spriteResetCompiledState(jlSpriteT *sp); @@ -190,10 +195,10 @@ static uint8_t *spriteRowsBuild(const jlSpriteT *sp); static uint8_t *spritePhasesBuild(const jlSpriteT *sp); #endif #if defined(JL_CPU_X86) -// src/x86/spriteMergeRows.s (Win16: src/hosted/win16/win16SpriteMerge.c): +// src/x86/spriteMergeRows.s (Win16: src/hosted/win16/spriteMergeRows.asm): // rows (>= 1) rows of count (1..160) bytes of dst = (dst & keep) | src, the // keep bytes keepOffset past src's. -void x86SpriteMergeRows(uint8_t *dst, const uint8_t *src, uint32_t keepOffset, uint32_t rows, uint32_t count, uint32_t srcStride); +void SPRITE_CODE_CALL jlX86SpriteMergeRows(uint8_t *dst, const uint8_t *src, uint32_t keepOffset, uint32_t rows, uint32_t count, uint32_t srcStride); #endif @@ -250,7 +255,7 @@ static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t he // // It reads the sprite's row-major copy (jlSpriteT.rows), so a row is // one run of bytes indexed alongside the destination. Every destination -// byte is one merge, dst[i] = (dst[i] & gSpriteNibbleKeep[b]) | b, skipped when +// byte is one merge, dst[i] = (dst[i] & jlgSpriteNibbleKeep[b]) | b, skipped when // b is fully transparent. When source and destination share nibble // parity b is a source byte; when they differ it is one source byte's // low nibble and the next one's high nibble. An odd leading or trailing @@ -325,7 +330,7 @@ static void __attribute__((noinline)) spriteDrawInterpreted(jlSurfaceT *s, jlSpr job.trail = trailing; job.trailSrc = (uint16_t)(lastPixel >> 1); job.trailLow = (uint16_t)(lastPixel & 1u); - iigsSpriteInterpRows(&job); + jlIigsSpriteInterpRows(&job); } } #elif !defined(JL_NATIVE_PLANAR) @@ -360,7 +365,7 @@ static void __attribute__((noinline)) spriteDrawInterpreted(jlSurfaceT *s, jlSpr src = sp->phases + ((phase != 0u) ? (uint32_t)sp->heightPx * sp->widthPx : 0u) + (uint32_t)sy * rowBytes + (uint16_t)k0; dst = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)dy) + (uint16_t)(first + k0)]; #if defined(JL_CPU_X86) - x86SpriteMergeRows(dst, src, keepOffset, (uint32_t)h, (uint32_t)count, rowBytes); + jlX86SpriteMergeRows(dst, src, keepOffset, (uint32_t)h, (uint32_t)count, rowBytes); #else for (row = 0; row < h; row++) { for (i = 0; i < count; i++) { @@ -403,12 +408,12 @@ static void __attribute__((noinline)) spriteDrawInterpreted(jlSurfaceT *s, jlSpr // byte-identical to spriteMarkDirty (SURFACE_WORD_INDEX of the first // and last covered pixel) -- a HASH-INVISIBLE contract; any drift // under-marks and corrupts presents while the stage hashes stay green. -// Caller must have gated s == gStage and h == 16. +// Caller must have gated s == jlgStage and h == 16. #define SPRITE_MARK_STAGE_ROWS16(_x, _w, _y) do { \ uint8_t minWord = SURFACE_WORD_INDEX(_x); \ uint8_t maxWord = SURFACE_WORD_INDEX((int16_t)((_x) + (int16_t)(_w) - 1)); \ - uint8_t *minPtr = &gStageMinWord[(uint16_t)(_y)]; \ - uint8_t *maxPtr = &gStageMaxWord[(uint16_t)(_y)]; \ + uint8_t *minPtr = &jlgStageMinWord[(uint16_t)(_y)]; \ + uint8_t *maxPtr = &jlgStageMaxWord[(uint16_t)(_y)]; \ stageMarkDirtyGroups((uint16_t)(_y), (uint16_t)(_y) + 16u); \ SPRITE_MARK_ROW(0); \ SPRITE_MARK_ROW(1); \ @@ -441,13 +446,13 @@ static void spriteMarkDirty(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1 uint8_t minWord; uint8_t maxWord; - if (s != gStage) { + if (s != jlgStage) { return; } minWord = SURFACE_WORD_INDEX(x); maxWord = SURFACE_WORD_INDEX(x + w - 1); - minPtr = &gStageMinWord[(uint16_t)y]; - maxPtr = &gStageMaxWord[(uint16_t)y]; + minPtr = &jlgStageMinWord[(uint16_t)y]; + maxPtr = &jlgStageMaxWord[(uint16_t)y]; stageMarkDirtyGroups((uint16_t)y, (uint16_t)y + (uint16_t)h); if (h == 16) { // The dominant region: every 2-tile-high sprite (all UBER @@ -487,7 +492,7 @@ static void spriteMarkDirty(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1 // rolled loop here. h == 1 also lands here (a // sprite clipped to one row); the extra call vs the old macro's // inline single-row arm is confined to that edge case. - surfaceMarkDirtyRows((uint16_t)y, (uint16_t)(y + h), minWord, maxWord); + jlSurfaceMarkDirtyRows((uint16_t)y, (uint16_t)(y + h), minWord, maxWord); } #endif @@ -620,7 +625,7 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h if (sp == NULL) { return NULL; } - if (!spriteInitFields(sp, tileData, widthTiles, heightTiles, false)) { + if (!jlSpriteInitFields(sp, tileData, widthTiles, heightTiles, false)) { jlpBigFree(sp); return NULL; } @@ -711,7 +716,7 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y jlpBigFree(buf); return NULL; } - if (!spriteInitFields(sp, buf, widthTiles, heightTiles, true)) { + if (!jlSpriteInitFields(sp, buf, widthTiles, heightTiles, true)) { jlpBigFree(sp); jlpBigFree(buf); return NULL; @@ -726,7 +731,7 @@ void jlSpriteDestroy(jlSpriteT *sp) { } spriteRegistryRemove(sp); if (sp->slot != NULL) { - codegenArenaFree(sp->slot); + jlCodegenArenaFree(sp->slot); sp->slot = NULL; } if (sp->ownsTileData) { @@ -755,10 +760,10 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { #if defined(JOEYLIB_PLATFORM_IIGS) // Stage draw: ONE JSL runs the compiled body AND the dirty-band - // widen (iigsSpriteDrawStage). Non-stage destinations and uncompiled + // widen (jlIigsSpriteDrawStage). Non-stage destinations and uncompiled // sprites fall through to the shared arm below (its C mark is a // stage-gated no-op there). - if (s == gStage && isFullyOnSurface(x, y, widthPx, heightPx) && iigsSpriteDrawStage(sp, x, y) == 0u) { + if (s == jlgStage && isFullyOnSurface(x, y, widthPx, heightPx) && jlIigsSpriteDrawStage(sp, x, y) == 0u) { return; } #endif @@ -768,7 +773,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { // need clip math (they walk fixed offsets). The DRAW offset for // this shift is read once and gated against SPRITE_NOT_COMPILED // (#46): a slot whose DRAW emitter returned 0 bytes for this shift - // must never be jumped into (spriteCompiledDraw adds the offset to + // must never be jumped into (jlSpriteCompiledDraw adds the offset to // the slot address unchecked on IIgs/DOS). Mirrors the SaveUnder / // RestoreUnder gates. This compiled branch is the production path // on every port with a wired emitter; the interpreter is only the @@ -781,7 +786,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { shift = SPRITE_SHIFT_INDEX(x); routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW); if (routeOffset != SPRITE_NOT_COMPILED) { - spriteCompiledDraw(s, sp, x, y, routeOffset); + jlSpriteCompiledDraw(s, sp, x, y, routeOffset); if (!COMPILED_SPRITE_WRITES_PLANES) { jlpSpriteDrawPlanes(s, sp, x, y); } @@ -790,7 +795,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { // the spriteMarkDirty call boundary (see the restore arm in // jlSpriteRestoreUnder). Other heights / non-stage draws keep // the out-of-line mark. - if (s == gStage && heightPx == 16) { + if (s == jlgStage && heightPx == 16) { SPRITE_MARK_STAGE_ROWS16(x, widthPx, y); } else { spriteMarkDirtyDraw(s, x, y, (int16_t)widthPx, (int16_t)heightPx); @@ -821,11 +826,11 @@ void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { shift = SPRITE_SHIFT_INDEX(x); drawOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW); if (drawOffset != SPRITE_NOT_COMPILED) { - spriteCompiledDraw(s, sp, x, y, drawOffset); + jlSpriteCompiledDraw(s, sp, x, y, drawOffset); // Compiled 68k draws write planes directly // (COMPILED_SPRITE_WRITES_PLANES) -- no hook, exactly like // the trusted save-and-draw arm. P7-1 lever b inline mark. - if (s == gStage && sp->heightPx == 16) { + if (s == jlgStage && sp->heightPx == 16) { SPRITE_MARK_STAGE_ROWS16(x, sp->widthPx, y); } else { spriteMarkDirtyDraw(s, x, y, (int16_t)sp->widthPx, (int16_t)sp->heightPx); @@ -835,9 +840,9 @@ void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { } #elif defined(JOEYLIB_PLATFORM_IIGS) // Skip validation -> the stage draw entry (the same arm jlSpriteDraw - // gates behind isFullyOnSurface). s == gStage only (it derives the + // gates behind isFullyOnSurface). s == jlgStage only (it derives the // pinned stage base). - if (s == gStage && iigsSpriteDrawStage(sp, x, y) == 0u) { + if (s == jlgStage && jlIigsSpriteDrawStage(sp, x, y) == 0u) { return; } #endif @@ -876,11 +881,11 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS #if defined(JOEYLIB_PLATFORM_IIGS) // Stage pair: compiled save + draw and the pair's ONE dirty rect (the // same rect the shared arm marks at its tail) in one JSL - // (iigsSpriteSaveDrawStage). Non-stage destinations and uncompiled + // (jlIigsSpriteSaveDrawStage). Non-stage destinations and uncompiled // sprites fall through to the public-API fallback below (non-stage // SaveAndDraw then runs the compiled save + generic draw through // those calls). - if (slot != NULL && backup->bytes != NULL && s == gStage && isFullyOnSurface(x, y, widthPx, heightPx) && iigsSpriteSaveDrawStage(sp, x, y, backup) == 0u) { + if (slot != NULL && backup->bytes != NULL && s == jlgStage && isFullyOnSurface(x, y, widthPx, heightPx) && jlIigsSpriteSaveDrawStage(sp, x, y, backup) == 0u) { return; } #else @@ -905,14 +910,14 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS drawOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW); if (saveOffset != SPRITE_NOT_COMPILED && drawOffset != SPRITE_NOT_COMPILED) { spriteCompiledSaveUnder(s, sp, x, y, backup, saveClass, saveOffset); - spriteCompiledDraw (s, sp, x, y, drawOffset); + jlSpriteCompiledDraw (s, sp, x, y, drawOffset); if (!COMPILED_SPRITE_WRITES_PLANES) { jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); jlpSpriteDrawPlanes(s, sp, x, y); } #if defined(SPRITE_FAST_PATHS) // P7-1 lever b: inline the h==16 stage mark (see jlSpriteDraw). - if (s == gStage && heightPx == 16) { + if (s == jlgStage && heightPx == 16) { SPRITE_MARK_STAGE_ROWS16(x, widthPx, y); } else { spriteMarkDirtyDraw(s, x, y, (int16_t)widthPx, (int16_t)heightPx); @@ -961,8 +966,8 @@ void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t if (saveOffset != SPRITE_NOT_COMPILED && drawOffset != SPRITE_NOT_COMPILED) { backup->sprite = sp; spriteCompiledSaveUnder(s, sp, x, y, backup, saveClass, saveOffset); - spriteCompiledDraw (s, sp, x, y, drawOffset); - if (s == gStage && sp->heightPx == 16) { + jlSpriteCompiledDraw (s, sp, x, y, drawOffset); + if (s == jlgStage && sp->heightPx == 16) { SPRITE_MARK_STAGE_ROWS16(x, sp->widthPx, y); } else { spriteMarkDirtyDraw(s, x, y, (int16_t)sp->widthPx, (int16_t)sp->heightPx); @@ -979,17 +984,17 @@ void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t // Defragment the codegen arena (compiled sprite slots). The live .spr // on-disk format is documented beside the loader in core/assetLoad.c. void jlSpriteCompact(void) { - codegenArenaCompact(); + jlCodegenArenaCompact(); } uint32_t jlSpriteCodegenBytesTotal(void) { - return codegenArenaBytesTotal(); + return jlCodegenArenaBytesTotal(); } uint32_t jlSpriteCodegenBytesUsed(void) { - return codegenArenaBytesUsed(); + return jlCodegenArenaBytesUsed(); } @@ -1015,7 +1020,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { * in uint16_t range once bx >= 0 / by >= 0 has been checked, so * the right-edge / bottom-edge tests don't need 32-bit arithmetic * (which would invoke a costly long-compare helper). */ - if (backup->bytes == NULL || bw == 0 || bh == 0 || bx < 0 || by < 0 || bx >= JL_SURFACE_WIDTH || by >= JL_SURFACE_HEIGHT || bw > (uint16_t)(JL_SURFACE_WIDTH - bx) || bh > (uint16_t)(JL_SURFACE_HEIGHT - by) || (bx & 1) || (bw & 1)) { + if (backup->bytes == NULL || bw == 0 || bh == 0 || bx < 0 || by < 0 || bx >= JL_SURFACE_WIDTH || by >= JL_SURFACE_HEIGHT || bw > (uint16_t)(JL_SURFACE_WIDTH - bx) || bh > (uint16_t)(JL_SURFACE_HEIGHT - by) || (((uint16_t)bx | bw) & (SPRITE_BACKUP_ALIGN - 1u)) != 0u) { return; } @@ -1037,8 +1042,8 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { if (shift != 0xFFu) { #if defined(JOEYLIB_PLATFORM_IIGS) // Stage restore: compiled MVN body + dirty-band widen in one - // JSL (iigsSpriteRestoreStage). - if (s == gStage && iigsSpriteRestoreStage(backup) == 0u) { + // JSL (jlIigsSpriteRestoreStage). + if (s == jlgStage && jlIigsSpriteRestoreStage(backup) == 0u) { return; } #elif defined(SPRITE_FAST_PATHS) @@ -1057,7 +1062,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { // SPRITE_MARK_ROW, byte-identical to spriteMarkDirty (a // HASH-INVISIBLE contract). Non-h16 regions and non-stage // restores keep the split spriteMarkDirty path. - if (s == gStage && bh == 16) { + if (s == jlgStage && bh == 16) { routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_RESTORE); if (routeOffset != SPRITE_NOT_COMPILED) { spriteCompiledRestoreUnder(s, backup, shift, routeOffset); @@ -1132,7 +1137,7 @@ void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup) { bw = backup->width; bh = backup->height; shift = SPRITE_RESTORE_CLASS(sp, bx, bw, (uint16_t)(bw >> 1)); - if (shift != 0xFFu && s == gStage && bh == 16) { + if (shift != 0xFFu && s == jlgStage && bh == 16) { uint16_t routeOffset; routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_RESTORE); @@ -1179,9 +1184,9 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, shift = SPRITE_SAVE_CLASS(sp, x); routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_SAVE); #if defined(JOEYLIB_PLATFORM_IIGS) - // Stage save in one JSL (iigsSpriteSaveStage). Non-stage saves + // Stage save in one JSL (jlIigsSpriteSaveStage). Non-stage saves // keep the generic dispatch below. - if (s == gStage && iigsSpriteSaveStage(sp, x, y, backup) == 0u) { + if (s == jlgStage && jlIigsSpriteSaveStage(sp, x, y, backup) == 0u) { return; } #endif @@ -1221,13 +1226,13 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, backup->height = 0; return; } - // Round x DOWN and width UP to byte boundaries so we always grab - // entire 4bpp-packed bytes. RestoreUnder requires byte alignment. - clippedX = (int16_t)(dx & ~1); - clippedW = (int16_t)(((dx + w) - clippedX + 1) & ~1); - if (clippedX + clippedW > JL_SURFACE_WIDTH) { - clippedW = JL_SURFACE_WIDTH - clippedX; - } + // The visible part widened to SPRITE_BACKUP_ALIGN: whole 4bpp bytes, + // and on the 68000 ports the 16-px group region a compiled save + // records, which the planar hooks copy as whole plane runs (the format + // law, spriteInternal.h) at any x. The surface is a whole number of + // either wide, so the region stays on it. RestoreUnder requires it. + clippedX = (int16_t)(dx & ~(SPRITE_BACKUP_ALIGN - 1)); + clippedW = (int16_t)(((dx + w + SPRITE_BACKUP_ALIGN - 1) & ~(SPRITE_BACKUP_ALIGN - 1)) - clippedX); backup->x = clippedX; backup->y = dy; @@ -1241,24 +1246,9 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, if (backup->bytes == NULL) { // Caller didn't supply a byte buffer; we just record the // metadata so they can size their buffer for the next - // SaveUnder call. RestoreUnder will refuse to operate on a - // backup with bytes==NULL. -#if defined(SPRITE_68K) - // Planar metadata widening: a real (buffered) unclipped save on - // these ports goes through the compiled 16-px-region path, so a - // probe-then-allocate caller must be told the REGION size, not - // the chunky-rounded size -- otherwise the next buffered save - // could overrun the allocation by up to one group per row. - // X68000 takes that same compiled region path (spriteDispatch.h's - // shared planar arm), so it needs the widening for the same reason. - if (dx == x && w == (int16_t)widthPx) { - uint16_t nGroups = (uint16_t)(SPRITE_GROUPS0(widthPx) + SPRITE_SAVE_CLASS(sp, x)); - - backup->x = (int16_t)(x & ~15); - backup->width = (uint16_t)(nGroups << 4); - backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * (uint16_t)h); - } -#endif + // SaveUnder call (on the 68000 ports, the region a buffered + // save, compiled or not, records). RestoreUnder will refuse to + // operate on a backup with bytes==NULL. return; } /* Chunky save path: skip on planar ports (s->pixels NULL). @@ -1309,7 +1299,7 @@ void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int } } #elif defined(JOEYLIB_PLATFORM_IIGS) - if (s == gStage && iigsSpriteSaveStage(sp, x, y, backup) == 0u) { + if (s == jlgStage && jlIigsSpriteSaveStage(sp, x, y, backup) == 0u) { return; } #endif @@ -1320,13 +1310,13 @@ void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int // ----- Internal cross-TU entry points (spriteInternal.h) ----- // Initialize a freshly-allocated jlSpriteT's fields to the -// uncompiled/default state and register it for spriteSystemShutdown +// uncompiled/default state and register it for jlSpriteSystemShutdown // (see the registry comment near the top of this file). The single // init path for every sprite: jlSpriteCreate, jlSpriteCreateFromSurface, // and jlSpriteBankLoad (assetLoad.c) all route through here so the // 0xFF/0 sentinel rules in spriteResetCompiledState have one source // of truth (claudeDocs/PERF-AUDIT.md #32). -bool spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) { +bool jlSpriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) { sp->tileData = tileData; sp->widthTiles = widthTiles; sp->heightTiles = heightTiles; @@ -1334,7 +1324,7 @@ bool spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles sp->heightPx = (uint16_t)(heightTiles * JL_TILE_PIXELS_PER_SIDE); sp->ownsTileData = ownsTileData; #if defined(JL_NATIVE_PLANAR) - sp->planar = spritePlanarBuild(sp); + sp->planar = jlSpritePlanarBuild(sp); if (sp->planar == NULL) { return false; } @@ -1353,17 +1343,17 @@ bool spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles sp->registryPrev = NULL; sp->registryNext = NULL; sp->registered = false; - spriteRegistryAdd(sp); + jlSpriteRegistryAdd(sp); return true; } -// Register a sprite for spriteSystemShutdown. Idempotent (re-adding a +// Register a sprite for jlSpriteSystemShutdown. Idempotent (re-adding a // registered sprite is a no-op); every creation path registers via -// spriteInitFields, and jlSpriteCompile re-registers on success as a +// jlSpriteInitFields, and jlSpriteCompile re-registers on success as a // belt-and-braces guard since compile is the moment the sprite first // holds arena state that jlShutdown must reset. -void spriteRegistryAdd(jlSpriteT *sp) { +void jlSpriteRegistryAdd(jlSpriteT *sp) { if (sp->registered) { return; } @@ -1377,14 +1367,14 @@ void spriteRegistryAdd(jlSpriteT *sp) { } -// Called from jlShutdown BEFORE codegenArenaShutdown (#34). Resets +// Called from jlShutdown BEFORE jlCodegenArenaShutdown (#34). Resets // every registered sprite's compiled-path state so a sprite surviving // a shutdown -> re-init cycle can neither free a stale ArenaSlotT // (heap corruption) nor execute stale offsets inside the new arena. -// Does NOT free anything -- codegenArenaShutdown releases the slot +// Does NOT free anything -- jlCodegenArenaShutdown releases the slot // structs and arena bytes right after. Sprites stay registered: they // remain live user objects and may be recompiled after re-init. -void spriteSystemShutdown(void) { +void jlSpriteSystemShutdown(void) { jlSpriteT *sp; for (sp = gSpriteRegistry; sp != NULL; sp = sp->registryNext) { diff --git a/src/core/spriteDispatch.h b/src/core/spriteDispatch.h index 0b9d63e..5dbff40 100644 --- a/src/core/spriteDispatch.h +++ b/src/core/spriteDispatch.h @@ -22,8 +22,8 @@ #include "spriteInternal.h" #include "surfaceInternal.h" -// The generated routines are cdecl; Watcom's own default passes arguments in -// registers. +// The generated routines and the x86 asm are cdecl; Watcom's own default +// passes arguments in registers. #if defined(__WATCOMC__) #define SPRITE_CODE_CALL __cdecl #else @@ -40,7 +40,7 @@ #if defined(JOEYLIB_PLATFORM_IIGS) -// SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs; +// SURFACE_ROW_OFFSET dispatches to the jlgRowOffsetLut lookup on IIgs; // declared in surfaceInternal.h. Uses a single indexed long-mode read // for the multiply. @@ -53,7 +53,7 @@ // have only their MVN bank operands patched per call (a plain C pointer write // into the arena from the dispatcher). No self-modifying call stub, no inline // asm -- the machinery that hid the old un-instrumentable #19 corruption. -static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { +static inline __attribute__((always_inline)) void jlSpriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { uint8_t *dstRow0; uint32_t fnAddr; void (*drawFn)(uint8_t *); @@ -118,20 +118,20 @@ static inline void patchMvnBanks(uint8_t *routine, uint16_t rows, uint8_t dstBan // Stage entries (src/iigs/joeyDraw.s, .text.iigsFusedRect): a compiled -// sprite's draw, save, save-and-draw or restore on gStage in ONE JSL, +// sprite's draw, save, save-and-draw or restore on jlgStage in ONE JSL, // reading the sprite and backup structs themselves -- the arena slot, // the routine for the shift, the MVN bank-patch cache, the backup's // metadata -- then deriving the stage offset, calling the routine(s) // and widening the dirty band. They return 0 when done and non-zero, // having done nothing, when the sprite or the op at that shift is not // compiled (or, for the restore, the backup is not a whole -// compiled-format one); the caller falls back. Callers own s == gStage +// compiled-format one); the caller falls back. Callers own s == jlgStage // and, for the checked entries, the geometry validation. Only sprite.c // consumes this header, so the externs live here, not in port.h. -extern uint16_t iigsSpriteDrawStage(jlSpriteT *sp, int16_t x, int16_t y); -extern uint16_t iigsSpriteSaveStage(jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); -extern uint16_t iigsSpriteSaveDrawStage(jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); -extern uint16_t iigsSpriteRestoreStage(const jlSpriteBackupT *backup); +extern uint16_t jlIigsSpriteDrawStage(jlSpriteT *sp, int16_t x, int16_t y); +extern uint16_t jlIigsSpriteSaveStage(jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); +extern uint16_t jlIigsSpriteSaveDrawStage(jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); +extern uint16_t jlIigsSpriteRestoreStage(const jlSpriteBackupT *backup); // The stage entries read these fields at fixed offsets (SPR_* and BK_* // in joeyDraw.s); a layout change must fail the build, not the sprites. @@ -320,7 +320,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS #define JL_PLANAR_BYTES_PER_ROW AMIGA_BYTES_PER_ROW #endif -static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { +static inline __attribute__((always_inline)) void jlSpriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3); JL_PLANAR_T *pd; uint16_t byteOff; @@ -395,15 +395,14 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS // ST word-interleaved planar runtime dispatch. The JIT routine takes // one arg: groupBase = pd->base + y*160 + (x>>4)*8 (the address of -// the first 16-pixel group the sprite touches). It walks rows by -// adda.w #160 at the end of each row. Per (row, tile_col, plane) it -// emits up to one move.b / clr.b / andi.b+ori.b / ori.b chain at -// d16(a0). +// the first 16-pixel group the sprite touches). It writes each group +// the sprite covers with long ops at d16(a0), every row's offset baked +// in (spriteEmitInterleaved68k.c has the forms). // // shift selection (in spriteInternal.h SPRITE_SHIFT_INDEX): the full // intra-group phase x & 15. Phases 0/8 are the unrolled byte-aligned // JIT; the 14 sub-byte phases are thunk + pre-shifted data tables -// driven by stSpriteDrawShiftWalker (all compiled since NATIVE-PERF +// driven by jlStSpriteDrawShiftWalker (all compiled since NATIVE-PERF // Phase 1). Anything uncompiled falls back to jlpSpriteDrawPlanes via // the caller gates (#29). @@ -421,7 +420,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS #include "stPlanar.h" -static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { +static inline __attribute__((always_inline)) void jlSpriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { typedef void (*DrawFn)(uint8_t *groupBase); StPlanarT *pd; uint8_t *groupBase; @@ -490,7 +489,7 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlS #else -static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { +static inline __attribute__((always_inline)) void jlSpriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { typedef void (SPRITE_CODE_CALL *DrawFn)(uint8_t *destRow); uint8_t *destRow; DrawFn fn; diff --git a/src/core/spriteInternal.h b/src/core/spriteInternal.h index 7046d74..b5225b8 100644 --- a/src/core/spriteInternal.h +++ b/src/core/spriteInternal.h @@ -114,12 +114,18 @@ ((_copyBytes) == (uint16_t)(SPRITE_GROUPS0((_sp)->widthPx) << 3) ? 0u : \ ((_copyBytes) == (uint16_t)((SPRITE_GROUPS0((_sp)->widthPx) + 1u) << 3) ? 1u : 0xFFu)))) #define SPRITE_BACKUP_PTR_OK(_p) ((((uintptr_t)(_p)) & 1u) == 0u) +// Every save records a region of whole 16-px groups (the compiled saves' +// region, clipped to the surface when the sprite is): the planar hooks +// (jlpSpriteSavePlanes / jlpSpriteRestorePlanes) copy whole plane runs. +#define SPRITE_BACKUP_ALIGN 16 #else #define SPRITE_SAVE_CLASS(_sp, _x) ((uint8_t)((_x) & 1)) #define SPRITE_RESTORE_CLASS(_sp, _bx, _bw, _copyBytes) \ ((uint8_t)((_copyBytes) == (uint16_t)((_sp)->widthPx >> 1) ? 0u : \ ((_copyBytes) == (uint16_t)(((_sp)->widthPx >> 1) + 1u) ? 1u : 0xFFu))) #define SPRITE_BACKUP_PTR_OK(_p) (1) +// A saved region is whole 4bpp bytes: x and width even. +#define SPRITE_BACKUP_ALIGN 2 #endif // Rows a compiled save covers, and so the height a compiled-format backup @@ -168,7 +174,7 @@ struct jlSpriteT { bool ownsTileData; // true if jlSpriteDestroy must free tileData // Create-time pixel dimensions (widthTiles * 8 / heightTiles * 8), - // cached by spriteInitFields so the per-call dispatchers and + // cached by jlSpriteInitFields so the per-call dispatchers and // spriteCompiled* callees never re-derive them (#29). uint16_t widthPx; uint16_t heightPx; @@ -177,7 +183,7 @@ struct jlSpriteT { // compile failed); jlSpriteDraw falls back to the interpreter. // The fn-call address for (shift, op) is computed at draw time: // (codegenArenaSlotAddr(slot) + routineOffsets[shift][op]) - // so a codegenArenaCompact that moves the slot's bytes is + // so a jlCodegenArenaCompact that moves the slot's bytes is // transparent to the caller. ArenaSlotT *slot; uint16_t routineOffsets[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT]; @@ -209,7 +215,7 @@ struct jlSpriteT { // height when there is none). The draw never writes the transparent // rows above and below, so nothing there needs saving, restoring or // presenting -- a one-tile star cel's three rows instead of eight. Set - // with the routines (spriteContentRowsIigs) and read by the stage + // with the routines (jlSpriteContentRowsIigs) and read by the stage // entries in joeyDraw.s. uint16_t contentRow0; uint16_t contentRows; @@ -219,13 +225,13 @@ struct jlSpriteT { // The sprite converted to planes once, for the interpreter // (src/m68k/planarSprite.c): per row, per 16-pixel column, five // big-endian words -- the opaque mask, then planes 0..3. Built by - // spriteInitFields, freed by jlSpriteDestroy. + // jlSpriteInitFields, freed by jlSpriteDestroy. uint8_t *planar; #elif defined(JOEYLIB_PLATFORM_IIGS) // The sprite's pixels row-major (widthPx / 2 bytes a row, 4bpp // packed, high nibble left), for the chunky interpreter: a row is // one run of bytes instead of a 4-byte slice of each tile. Built by - // spriteInitFields, freed by jlSpriteDestroy. + // jlSpriteInitFields, freed by jlSpriteDestroy. uint8_t *rows; #else // The sprite's two phase images for the chunky interpreter, one for @@ -235,7 +241,7 @@ struct jlSpriteT { // on x86, + 4, transparent bytes padding the row to whole dwords). // Each image is its rows of bytes, then as many keep masks (a nibble // of ones where the pixel is transparent), so a draw is - // dst = (dst & keep) | byte on whole bytes. Built by spriteInitFields, + // dst = (dst & keep) | byte on whole bytes. Built by jlSpriteInitFields, // freed by jlSpriteDestroy. uint8_t *phases; #endif @@ -253,7 +259,7 @@ struct jlSpriteT { // sprite.c: for a source byte b, the destination bits the chunky // interpreter keeps (a nibble of ones where that pixel is transparent); // src/iigs/spriteInterp.s reads it too. -extern const uint8_t gSpriteNibbleKeep[256]; +extern const uint8_t jlgSpriteNibbleKeep[256]; #endif #if defined(JL_NATIVE_PLANAR) @@ -261,8 +267,8 @@ extern const uint8_t gSpriteNibbleKeep[256]; // clipped to the surface (planes[p] is plane p's first byte; on the ST, // planes[0] is the screen base and the only entry), and the cache's // builder (NULL when there is no memory for it). -void planarSpriteDraw(uint8_t *const planes[], const jlSpriteT *sp, int16_t x, int16_t y); -uint8_t *spritePlanarBuild(const jlSpriteT *sp); +void jlPlanarSpriteDraw(uint8_t *const planes[], const jlSpriteT *sp, int16_t x, int16_t y); +uint8_t *jlSpritePlanarBuild(const jlSpriteT *sp); #endif // Compiled entry points. Implemented alongside jlSpriteCompile in @@ -286,38 +292,38 @@ uint8_t *spritePlanarBuild(const jlSpriteT *sp); // Initialize a freshly-allocated jlSpriteT to the uncompiled/default // state (tile-data fields, 0xFF routine-offset fill, bank/size cache // sentinels, the interpreter's copy of the pixels) and register it for -// spriteSystemShutdown. The single init path for every sprite -- +// jlSpriteSystemShutdown. The single init path for every sprite -- // jlSpriteCreate and jlSpriteCreateFromSurface in sprite.c plus the bank // loaders in assetLoad.c -- so the sentinel rules live in one place // (claudeDocs/PERF-AUDIT.md #32). Returns false, with nothing allocated // or registered, when there is no memory for the interpreter's copy of // the pixels (planar, rows, or phases); the caller frees the sprite and // its tile data. Defined in sprite.c. -bool spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData); +bool jlSpriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData); -// Sprite registry (sprite.c, claudeDocs/PERF-AUDIT.md #34). spriteInitFields +// Sprite registry (sprite.c, claudeDocs/PERF-AUDIT.md #34). jlSpriteInitFields // registers every sprite it initializes; jlSpriteCompile re-registers // on success (idempotent) as a belt-and-braces guard, since compile // is the moment the sprite first holds an arena slot. // jlSpriteDestroy unregisters. The registry is a list threaded through // the sprites themselves (registryPrev / registryNext), so it holds every // sprite, with no cap. -void spriteRegistryAdd(jlSpriteT *sp); +void jlSpriteRegistryAdd(jlSpriteT *sp); -// Called from jlShutdown BEFORE codegenArenaShutdown: resets every +// Called from jlShutdown BEFORE jlCodegenArenaShutdown: resets every // registered sprite's compiled-path state (slot pointer, routine // offsets, patch caches) so a sprite surviving a shutdown -> re-init // cycle can neither free a stale ArenaSlotT (heap corruption) nor // execute stale offsets inside the new arena. Frees nothing -- -// codegenArenaShutdown releases the slots right after. -void spriteSystemShutdown(void); +// jlCodegenArenaShutdown releases the slots right after. +void jlSpriteSystemShutdown(void); #if defined(JOEYLIB_PLATFORM_IIGS) // Set sp->contentRow0 / contentRows from the sprite's tile data. Called -// wherever compiled routines are put in place (spriteStageAndPlan, the +// wherever compiled routines are put in place (jlSpriteStageAndPlan, the // .spc loader), so the rows always match the routines. Defined in // src/iigs/spriteEmitIigs.c, which the offline baker links too. -void spriteContentRowsIigs(jlSpriteT *sp); +void jlSpriteContentRowsIigs(jlSpriteT *sp); #endif #endif diff --git a/src/core/spritePrecompiled.h b/src/core/spritePrecompiled.h index 1182a20..266150e 100644 --- a/src/core/spritePrecompiled.h +++ b/src/core/spritePrecompiled.h @@ -8,11 +8,11 @@ // A .spc is a superset of a .spr: the same 44-byte header, then per cel the // raw chunky tile blob (as .spr) PLUS the pre-emitted native blit-routine // image + its slot-relative routineOffsets table, so the runtime skips the -// jlSpriteCompile JIT entirely -- it just codegenArenaAlloc + memcpy + stamp +// jlSpriteCompile JIT entirely -- it just jlCodegenArenaAlloc + memcpy + stamp // routineOffsets. Because the emitted routines are position-independent // (dest passed as arg; routineOffsets are slot-relative), NO load-time // relocation is needed on IIgs/DOS/Amiga; the Atari ST alone bakes an -// absolute jmp to stSpriteDrawShiftWalker, whose sites are recorded as a +// absolute jmp to jlStSpriteDrawShiftWalker, whose sites are recorded as a // per-cel reloc list and re-stamped at load. // // Header-clean (only fixed-width types + the shared header layout), so it is @@ -52,7 +52,7 @@ // target machine code and are inherently per-target): // // tileData[widthTiles*heightTiles*32] raw chunky 4bpp cel (as .spr) -- -// feeds spriteInitFields + the +// feeds jlSpriteInitFields + the // interpreter/clip fallback path. // celMeta (fixed size = 4 + shiftCount*3*2 + 2): // imageSize LE32 compiled routine-image byte count @@ -61,9 +61,9 @@ // relocCount LE16 walker-address patch sites (ST only; 0 else) // image[imageSize] the pre-emitted routines // relocOffsets[relocCount] LE16 each image offsets to patch with the -// 4-byte big-endian &stSpriteDrawShiftWalker +// 4-byte big-endian &jlStSpriteDrawShiftWalker -// Atari ST shifted-phase DRAW thunk: `jmp abs.l stSpriteDrawShiftWalker` +// Atari ST shifted-phase DRAW thunk: `jmp abs.l jlStSpriteDrawShiftWalker` // bakes the walker address (2 words, big-endian high-word-first) at // thunk-relative offset +18. The bake tool records (routineOffsets[shift][DRAW] // + this) for every shift with (shift & 7) != 0 that compiled. diff --git a/src/core/surface.c b/src/core/surface.c index 84975d5..970ceae 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -41,27 +41,29 @@ static inline void markRow(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, ui // Non-static: hot paths read it through surfaceInternal.h's extern to // skip the jlStageGet() cross-TU call (PERF-AUDIT #10/#17). Written -// only by stageAlloc/stageFree below. -jlSurfaceT *gStage = NULL; +// only by jlStageAlloc/jlStageFree below. +jlSurfaceT *jlgStage = NULL; -// aligned(2): the ST fused present (present68kStSlam) walks these with +// aligned(2): the ST fused present (jlPresent68kStSlam) walks these with // 4-row long-word loads; the 68000 requires EVEN addresses for word // and long access (2 is also the mint a.out object format's maximum // alignment), and char arrays otherwise carry no alignment guarantee. // Harmless on the other ports (plain GNU-C attribute). -uint8_t gStageMinWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); -uint8_t gStageMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); +uint8_t jlgStageMinWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); +uint8_t jlgStageMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); #if defined(JOEYLIB_PLATFORM_IIGS) // Coarse per-8-row dirty map (surfaceInternal.h): a conservative hint // that lets the PEI-slam present skip 8 rows at a time. -uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; +uint8_t jlgStageDirtyGroup[STAGE_DIRTY_GROUPS]; #endif // Present-path telemetry; see surfaceInternal.h. Written by the // ST/Amiga/DOS jlpPresent implementations, read by the UBER_PROBE -// build. Never reset by the library -- probes log deltas. -uint32_t gPresentCopiedBytes = 0; -uint32_t gPresentScannedRows = 0; +// build. Never reset by the library -- probes log deltas. The IIgs and +// hosted presents count only while jlgPresentTelemetry is set. +uint32_t jlgPresentCopiedBytes = 0; +uint32_t jlgPresentScannedRows = 0; +uint8_t jlgPresentTelemetry = 0; // "Stage SCB / palette has changed since last present-side upload." // A cheap flag check at present time lets a port skip re-uploading the @@ -73,9 +75,9 @@ uint32_t gPresentScannedRows = 0; // and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile) // re-mark dirty when the stage's data changes; the per-port present // code reads and clears the flags after uploading. -bool gStageScbDirty = true; -bool gStagePaletteDirty = true; -uint16_t gStagePaletteDirtyMask = 0xFFFFu; +bool jlgStageScbDirty = true; +bool jlgStagePaletteDirty = true; +uint16_t jlgStagePaletteDirtyMask = 0xFFFFu; // ----- Internal helpers (alphabetical) ----- @@ -91,11 +93,11 @@ static inline void markGroup(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, } -// surfaceMarkDirtyRows's group path, for at least STAGE_MARK_GROUP_MIN_ROWS +// jlSurfaceMarkDirtyRows's group path, for at least STAGE_MARK_GROUP_MIN_ROWS // rows: the C twin of src/m68k/stageBands.s -- the same even-row groups and // the same reuse of a group's result, which tests/host/dirtyMarkHost.c holds // to the plain per-row widen. A function of its own so the short-mark -// path in surfaceMarkDirtyRows stays a tight loop; gcc inlines it there +// path in jlSurfaceMarkDirtyRows stays a tight loop; gcc inlines it there // (forcing it out of line cost DOS's 16-row fills 2%). static void markGroups(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { uint8_t *minPtr; @@ -109,8 +111,8 @@ static void markGroups(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWo uint16_t rows; uint16_t groups; - minPtr = &gStageMinWord[y]; - maxPtr = &gStageMaxWord[y]; + minPtr = &jlgStageMinWord[y]; + maxPtr = &jlgStageMaxWord[y]; rows = (uint16_t)(yEnd - y); if ((y & 1u) != 0u) { markRow(minPtr++, maxPtr++, minWord, maxWord); @@ -163,7 +165,7 @@ static inline void markRow(uint8_t *minPtr, uint8_t *maxPtr, uint8_t minWord, ui // ----- Public API (alphabetical) ----- jlSurfaceT *jlStageGet(void) { - return gStage; + return jlgStage; } @@ -175,14 +177,14 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) { memcpy(dst->scb, src->scb, sizeof(src->scb)); memcpy(dst->palette, src->palette, sizeof(src->palette)); jlpSurfaceCopyPlanes(dst, src); // 4 plane memcpys on planar ports; no-op on chunky - surfaceMarkDirtyAll(dst); + jlSurfaceMarkDirtyAll(dst); // Bulk-replacing the stage's SCB/palette must re-mark them dirty so - // the per-port present re-uploads them; surfaceMarkDirtyAll only + // the per-port present re-uploads them; jlSurfaceMarkDirtyAll only // widens the pixel bands. Matches the jlScbSet / jlPaletteSet path. - if (dst == gStage) { - gStageScbDirty = true; - gStagePaletteDirty = true; - gStagePaletteDirtyMask = 0xFFFFu; + if (dst == jlgStage) { + jlgStageScbDirty = true; + jlgStagePaletteDirty = true; + jlgStagePaletteDirtyMask = 0xFFFFu; } } @@ -240,7 +242,7 @@ jlSurfaceT *jlSurfaceCreate(void) { // jlpBigAlloc to malloc, so behavior is unchanged there. s = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT)); if (s == NULL) { - coreSetError("out of memory allocating surface"); + jlCoreSetError("out of memory allocating surface"); return NULL; } memset(s, 0, sizeof(jlSurfaceT)); @@ -255,18 +257,18 @@ jlSurfaceT *jlSurfaceCreate(void) { if (s->pixels == NULL) { jlpSurfaceFreePortData(s, false, s->portData); jlpBigFree(s); - coreSetError("out of memory allocating surface pixels"); + jlCoreSetError("out of memory allocating surface pixels"); return NULL; } #else if (s->portData == NULL) { jlpSurfaceFreePixels(s->pixels); jlpBigFree(s); - coreSetError("out of memory allocating surface planes"); + jlCoreSetError("out of memory allocating surface planes"); return NULL; } #endif - paletteInitDefault(s); + jlPaletteInitDefault(s); return s; } @@ -275,7 +277,7 @@ void jlSurfaceDestroy(jlSurfaceT *s) { if (s == NULL) { return; } - if (s == gStage) { + if (s == jlgStage) { return; } jlpSurfaceFreePortData(s, false, s->portData); @@ -297,6 +299,30 @@ uint32_t jlSurfaceHash(const jlSurfaceT *s) { } +uint32_t jlSurfaceHashFinish(uint16_t lo, uint16_t hi, const jlSurfaceT *s) { + const uint8_t *p; + const uint16_t *w; + uint16_t n; + uint16_t v; + uint8_t b; + + p = s->scb; + for (n = 0; n < (uint16_t)JL_SURFACE_HEIGHT; n++) { + b = *p++; + SURFACE_HASH_MIX_BYTE(lo, hi, b); + } + w = &s->palette[0][0]; + for (n = 0; n < (uint16_t)JL_SURFACE_PALETTE_ENTRIES; n++) { + v = *w++; + b = (uint8_t)((v >> 8) & 0xFFu); + SURFACE_HASH_MIX_BYTE(lo, hi, b); + b = (uint8_t)(v & 0xFFu); + SURFACE_HASH_MIX_BYTE(lo, hi, b); + } + return ((uint32_t)hi << 16) | (uint32_t)lo; +} + + // The file is the port's pixels, then the SCBs, then the palettes. The SCBs // and palettes are read and checked first, so a file that names a palette // that is not there is refused with the surface untouched; colour 0 is @@ -334,13 +360,13 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) { } memcpy(dst->scb, scb, JL_SURFACE_HEIGHT); memcpy(dst->palette, palette, SURFACE_PALETTE_BYTES); - surfaceMarkDirtyAll(dst); + jlSurfaceMarkDirtyAll(dst); /* Loading a file into the stage overwrites its SCB/palette; re-mark * them dirty so the next present re-uploads them. */ - if (dst == gStage) { - gStageScbDirty = true; - gStagePaletteDirty = true; - gStagePaletteDirtyMask = 0xFFFFu; + if (dst == jlgStage) { + jlgStageScbDirty = true; + jlgStagePaletteDirty = true; + jlgStagePaletteDirtyMask = 0xFFFFu; } return true; } @@ -373,22 +399,110 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) { } -void surfaceMarkDirtyAll(const jlSurfaceT *s) { - if (s != gStage) { +// ----- Internal (alphabetical) ----- + +bool jlStageAlloc(void) { + if (jlgStage != NULL) { + return true; + } + // jlpBigAlloc for the same reason as jlSurfaceCreate: the IIgs C + // heap can neither hold nor protect this struct. + jlgStage = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT)); + if (jlgStage == NULL) { + return false; + } + memset(jlgStage, 0, sizeof(jlSurfaceT)); + // jlpStageAllocPixels returns NULL on planar ports (Amiga) where + // the chunky shadow doesn't exist; the planes from portData are + // the source of truth. On chunky ports NULL pixels means the + // back-buffer alloc failed (IIgs pins a fixed address and never + // returns NULL, so this is a real OOM only on DOS). + jlgStage->pixels = jlpStageAllocPixels(); + if (jlgStage->pixels != NULL) { + memset(jlgStage->pixels, 0, JL_SURFACE_PIXELS_SIZE); + } + jlgStage->portData = jlpSurfaceAllocPortData(jlgStage, true); +#ifdef JL_NATIVE_CHUNKY + if (jlgStage->pixels == NULL) { + jlpSurfaceFreePortData(jlgStage, true, jlgStage->portData); + jlpBigFree(jlgStage); + jlgStage = NULL; + return false; + } +#else + if (jlgStage->portData == NULL) { + jlpStageFreePixels(jlgStage->pixels); + jlpBigFree(jlgStage); + jlgStage = NULL; + return false; + } +#endif + jlStageDirtyClearAll(); + jlPaletteInitDefault(jlgStage); + // Fresh stage = fresh SCB/palette; force the first present after + // (re-)init to upload them. The load-time initializers cover the + // first jlInit, but a jlShutdown/jlInit cycle in one process would + // otherwise leave stale false flags and skip the upload + // (NATIVE-PERF Phase 2 R9 -- ports now trust these flags instead + // of memcmp'ing cached SCB/palette mirrors every present). + jlgStageScbDirty = true; + jlgStagePaletteDirty = true; + jlgStagePaletteDirtyMask = 0xFFFFu; + return true; +} + + +void jlStageDirtyClearAll(void) { + // Constant fill of both band arrays, same form as its sibling + // jlSurfaceMarkDirtyAll. This runs after EVERY jlStagePresent. The IIgs + // clears only the 8-row groups its group map says were marked + // (joeyDraw.s), which no fill can match on an idle present. The + // 68000 ports fill both arrays in one pass of long stores (about half + // what two libc memsets cost there); the x86 ports keep memset, whose + // rep stos is already the fastest fill they have. +#if defined(JOEYLIB_PLATFORM_IIGS) + jlIigsStageBandsClearAll(); +#elif defined(JL_CPU_68000) + jlStageBandsFill68k(jlgStageMinWord, jlgStageMaxWord, STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MIN), STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MAX)); +#else + memset(jlgStageMinWord, STAGE_DIRTY_CLEAN_MIN, JL_SURFACE_HEIGHT); + memset(jlgStageMaxWord, STAGE_DIRTY_CLEAN_MAX, JL_SURFACE_HEIGHT); +#endif + // The IIgs group bytes are NOT cleared here any more: + // jlIigsStageBandsClearAll clears each one as it clears that group's rows, + // because it has to read them anyway to decide whether those rows need + // clearing at all. Folding it in there saves this loop (~800 cycles of + // every present) outright. +} + + +void jlStageFree(void) { + if (jlgStage == NULL) { + return; + } + jlpSurfaceFreePortData(jlgStage, true, jlgStage->portData); + jlpStageFreePixels(jlgStage->pixels); + jlpBigFree(jlgStage); + jlgStage = NULL; +} + + +void jlSurfaceMarkDirtyAll(const jlSurfaceT *s) { + if (s != jlgStage) { return; } // Whole-stage mark is a constant fill of both band arrays. The 68000 - // ports share stageDirtyClearAll's one-pass fill; the x86 and IIgs + // ports share jlStageDirtyClearAll's one-pass fill; the x86 and IIgs // memsets are already tight (the IIgs's matched the asm word-store // loop it replaced, 2026-10-04). #if defined(JL_CPU_68000) - stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(0u), STAGE_BAND_FILL(STAGE_DIRTY_FULL_MAX)); + jlStageBandsFill68k(jlgStageMinWord, jlgStageMaxWord, STAGE_BAND_FILL(0u), STAGE_BAND_FILL(STAGE_DIRTY_FULL_MAX)); #else - memset(gStageMinWord, 0, JL_SURFACE_HEIGHT); - memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, JL_SURFACE_HEIGHT); + memset(jlgStageMinWord, 0, JL_SURFACE_HEIGHT); + memset(jlgStageMaxWord, STAGE_DIRTY_FULL_MAX, JL_SURFACE_HEIGHT); #endif #if defined(JOEYLIB_PLATFORM_IIGS) - memset(gStageDirtyGroup, 1, STAGE_DIRTY_GROUPS); + memset(jlgStageDirtyGroup, 1, STAGE_DIRTY_GROUPS); #endif } @@ -401,7 +515,7 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) { // old per-row loop had. (Whole-tile marks never come here: tile.c's // tileMarkDirty unrolls its constant 8 rows inline.) #if !defined(JOEYLIB_PLATFORM_IIGS) && !defined(JL_CPU_68000) -void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { +void jlSurfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { uint8_t *minPtr; uint8_t *maxPtr; uint8_t *minEnd; @@ -410,99 +524,11 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma markGroups(y, yEnd, minWord, maxWord); return; } - minPtr = &gStageMinWord[y]; - maxPtr = &gStageMaxWord[y]; - minEnd = &gStageMinWord[yEnd]; + minPtr = &jlgStageMinWord[y]; + maxPtr = &jlgStageMaxWord[y]; + minEnd = &jlgStageMinWord[yEnd]; while (minPtr < minEnd) { markRow(minPtr++, maxPtr++, minWord, maxWord); } } #endif - - -// ----- Internal (alphabetical) ----- - -bool stageAlloc(void) { - if (gStage != NULL) { - return true; - } - // jlpBigAlloc for the same reason as jlSurfaceCreate: the IIgs C - // heap can neither hold nor protect this struct. - gStage = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT)); - if (gStage == NULL) { - return false; - } - memset(gStage, 0, sizeof(jlSurfaceT)); - // jlpStageAllocPixels returns NULL on planar ports (Amiga) where - // the chunky shadow doesn't exist; the planes from portData are - // the source of truth. On chunky ports NULL pixels means the - // back-buffer alloc failed (IIgs pins a fixed address and never - // returns NULL, so this is a real OOM only on DOS). - gStage->pixels = jlpStageAllocPixels(); - if (gStage->pixels != NULL) { - memset(gStage->pixels, 0, JL_SURFACE_PIXELS_SIZE); - } - gStage->portData = jlpSurfaceAllocPortData(gStage, true); -#ifdef JL_NATIVE_CHUNKY - if (gStage->pixels == NULL) { - jlpSurfaceFreePortData(gStage, true, gStage->portData); - jlpBigFree(gStage); - gStage = NULL; - return false; - } -#else - if (gStage->portData == NULL) { - jlpStageFreePixels(gStage->pixels); - jlpBigFree(gStage); - gStage = NULL; - return false; - } -#endif - stageDirtyClearAll(); - paletteInitDefault(gStage); - // Fresh stage = fresh SCB/palette; force the first present after - // (re-)init to upload them. The load-time initializers cover the - // first jlInit, but a jlShutdown/jlInit cycle in one process would - // otherwise leave stale false flags and skip the upload - // (NATIVE-PERF Phase 2 R9 -- ports now trust these flags instead - // of memcmp'ing cached SCB/palette mirrors every present). - gStageScbDirty = true; - gStagePaletteDirty = true; - gStagePaletteDirtyMask = 0xFFFFu; - return true; -} - - -void stageDirtyClearAll(void) { - // Constant fill of both band arrays, same form as its sibling - // surfaceMarkDirtyAll. This runs after EVERY jlStagePresent. The IIgs - // clears only the 8-row groups its group map says were marked - // (joeyDraw.s), which no fill can match on an idle present. The - // 68000 ports fill both arrays in one pass of long stores (about half - // what two libc memsets cost there); the x86 ports keep memset, whose - // rep stos is already the fastest fill they have. -#if defined(JOEYLIB_PLATFORM_IIGS) - iigsStageBandsClearAll(); -#elif defined(JL_CPU_68000) - stageBandsFill68k(gStageMinWord, gStageMaxWord, STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MIN), STAGE_BAND_FILL(STAGE_DIRTY_CLEAN_MAX)); -#else - memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, JL_SURFACE_HEIGHT); - memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, JL_SURFACE_HEIGHT); -#endif - // The IIgs group bytes are NOT cleared here any more: - // iigsStageBandsClearAll clears each one as it clears that group's rows, - // because it has to read them anyway to decide whether those rows need - // clearing at all. Folding it in there saves this loop (~800 cycles of - // every present) outright. -} - - -void stageFree(void) { - if (gStage == NULL) { - return; - } - jlpSurfaceFreePortData(gStage, true, gStage->portData); - jlpStageFreePixels(gStage->pixels); - jlpBigFree(gStage); - gStage = NULL; -} diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h index 37afb17..247d367 100644 --- a/src/core/surfaceInternal.h +++ b/src/core/surfaceInternal.h @@ -58,16 +58,16 @@ struct jlSurfaceT { // True when row `y`'s dirty band is the CLEAN sentinel (min > max). // This is the canonical per-row copy test (the IIgs present asm // implements the same compare against these sentinels). -#define STAGE_DIRTY_ROW_CLEAN(_y) (gStageMinWord[_y] > gStageMaxWord[_y]) +#define STAGE_DIRTY_ROW_CLEAN(_y) (jlgStageMinWord[_y] > jlgStageMaxWord[_y]) -// True when row `y` has been marked since the last stageDirtyClearAll. +// True when row `y` has been marked since the last jlStageDirtyClearAll. // Single-load form (used by the Amiga present's idle-frame early-out // and any-dirty scan). Valid ONLY under the clipped-marks invariant: // every real mark widens minWord to <= STAGE_DIRTY_FULL_MAX (79), so // min != 0xFF exactly when the row was touched. Use // STAGE_DIRTY_ROW_CLEAN for the per-row copy decision; use this // cheaper test where one load per row matters and the invariant holds. -#define STAGE_DIRTY_ROW_TOUCHED(_y) (gStageMinWord[_y] != STAGE_DIRTY_CLEAN_MIN) +#define STAGE_DIRTY_ROW_TOUCHED(_y) (jlgStageMinWord[_y] != STAGE_DIRTY_CLEAN_MIN) // The per-row dirty bands are stored as uint8_t, which is only valid // because SURFACE_WORDS_PER_ROW - 1 (79) fits in a byte. A future width @@ -75,27 +75,27 @@ struct jlSurfaceT { // rather than silently truncate the band arrays. typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 : -1]; -// Per-row dirty word bands for the stage. gStageMinWord[y] is the -// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y] +// Per-row dirty word bands for the stage. jlgStageMinWord[y] is the +// leftmost dirty 16-bit column on row y (inclusive); jlgStageMaxWord[y] // is the rightmost (inclusive). Both default to the CLEAN sentinels -// after stageAlloc and after each jlStagePresent. -extern uint8_t gStageMinWord[JL_SURFACE_HEIGHT]; -extern uint8_t gStageMaxWord[JL_SURFACE_HEIGHT]; +// after jlStageAlloc and after each jlStagePresent. +extern uint8_t jlgStageMinWord[JL_SURFACE_HEIGHT]; +extern uint8_t jlgStageMaxWord[JL_SURFACE_HEIGHT]; // Four rows' clean test in one load, for presents on CPUs with a 32-bit // load (DOS, DVX): a clean row's min word is the CLEAN sentinel (0xFF) and a // touched row's is at most STAGE_DIRTY_FULL_MAX, so rows 4g .. 4g+3 are all // clean exactly when their four min-word bytes read 0xFFFFFFFF. It reads the // per-row bands themselves, so marking has nothing extra to keep in step -- -// unlike the IIgs's gStageDirtyGroup below, which exists only because the -// 65816 has no 32-bit load. gStageMinWord is aligned(2), which x86 (and a +// unlike the IIgs's jlgStageDirtyGroup below, which exists only because the +// 65816 has no 32-bit load. jlgStageMinWord is aligned(2), which x86 (and a // 68000 long load) accepts; the type says so (aligned(2)), so a long read at // a 2-byte boundary is defined C, not just something the CPUs tolerate. #define STAGE_DIRTY_GROUP4_SHIFT 2 #define STAGE_DIRTY_GROUP4_ROWS (1u << STAGE_DIRTY_GROUP4_SHIFT) #define STAGE_DIRTY_GROUP4_ALL_CLEAN 0xFFFFFFFFul typedef uint32_t __attribute__((may_alias, aligned(2))) StageBandGroupT; -#define STAGE_DIRTY_GROUP4_CLEAN(_g) (((const StageBandGroupT *)(const void *)gStageMinWord)[_g] == STAGE_DIRTY_GROUP4_ALL_CLEAN) +#define STAGE_DIRTY_GROUP4_CLEAN(_g) (((const StageBandGroupT *)(const void *)jlgStageMinWord)[_g] == STAGE_DIRTY_GROUP4_ALL_CLEAN) // Present-path telemetry (NATIVE-PERF Phase 6 W0, IIgs added W2). // The presents accumulate how many bytes they moved toward the @@ -104,19 +104,23 @@ typedef uint32_t __attribute__((may_alias, aligned(2))) StageBandGroupT; // O(bytes) copy cost and to detect over-copy (e.g. a bounding-box // union copying clean content). A few 32-bit adds per present buys // permanently runnable probes. All FOUR ports count: the ST/Amiga/DOS -// C presents and the IIgs present asm (iigsBlitStageToShr increments +// C presents and the IIgs present asm (jlIigsBlitStageToShr increments // copied per MVN row / per slam row and scanned by 200 per present). -extern uint32_t gPresentCopiedBytes; -extern uint32_t gPresentScannedRows; +// The IIgs and hosted presents count only while jlgPresentTelemetry is +// set (the probe build sets it): on the IIgs the counting was ~0.5% of +// Space Taxi's time, a multiply per MVN run and an add per slammed row. +extern uint32_t jlgPresentCopiedBytes; +extern uint32_t jlgPresentScannedRows; +extern uint8_t jlgPresentTelemetry; // The library-owned stage surface, defined in surface.c (written only -// by stageAlloc/stageFree). Direct extern -- matching the band arrays +// by jlStageAlloc/jlStageFree). Direct extern -- matching the band arrays // above and codegenArenaInternal.h's direct-extern globals -- so hot // paths (the port.h IIgs macros, the dirty-mark macro below, the // palette/scb stage checks) compare surface identity with a plain load // instead of a cross-TU jlStageGet() call. jlStageGet() stays as the // public accessor. Treat as read-only outside surface.c. -extern jlSurfaceT *gStage; +extern jlSurfaceT *jlgStage; // Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b. // Strength-reduced to shifts so the compiler doesn't emit a software @@ -131,17 +135,30 @@ extern jlSurfaceT *gStage; (hi_) = (uint16_t)((((hi_) << 8) - ((hi_) << 2) - (hi_)) + (b_)); \ } while (0) +// The two streams' starting values. Every jlpSurfaceHash mixes the pixels +// as chunky bytes from these, then hands them to jlSurfaceHashFinish, which +// mixes the SCBs and the palette words (high byte first, so the hash does +// not depend on byte order) and joins the streams. jlPlanarSurfaceHash +// (src/m68k/planarHash.c) is the planar ports' whole hash. +#define SURFACE_HASH_SEED_LO 0xACE1u +#define SURFACE_HASH_SEED_HI 0x1357u +// jlPlanarSurfaceHash's pairStep for separate planes: two columns, two bytes. +#define PLANAR_HASH_SEPARATE_PAIR_STEP 2u + +uint32_t jlPlanarSurfaceHash(const jlSurfaceT *s, const uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep); +uint32_t jlSurfaceHashFinish(uint16_t lo, uint16_t hi, const jlSurfaceT *s); + // Stage SCB / palette dirty flags. jlScbSet* and jlPaletteSet set them // true when the stage's data is modified; the per-port present code // checks the flags and clears after upload. Replaces a per-frame // 712-byte memcmp pair the IIgs port used to run unconditionally. -extern bool gStageScbDirty; -extern bool gStagePaletteDirty; -// Companion to gStagePaletteDirty: bit N set = palette N changed since the +extern bool jlgStageScbDirty; +extern bool jlgStagePaletteDirty; +// Companion to jlgStagePaletteDirty: bit N set = palette N changed since the // last upload. A port that can upload per palette (IIgs: 32 bytes each, // not 512) consults this; the bool stays the coarse "anything" flag the // other ports read. Both are set together and cleared together. -extern uint16_t gStagePaletteDirtyMask; +extern uint16_t jlgStagePaletteDirtyMask; // Coarse dirty map: one byte per GROUP of 8 stage rows, non-zero when // any row in the group may be dirty. It is a conservative hint, never @@ -150,10 +167,10 @@ extern uint16_t gStagePaletteDirtyMask; // all 200 rows to find the handful that changed (measured: 37 dirty // rows per present in Space Taxi gameplay), and a clean group lets the // walk step 8 rows at once. Every writer that widens a band must set -// the group byte(s) it covers; stageDirtyClearAll clears them. +// the group byte(s) it covers; jlStageDirtyClearAll clears them. // IIgs ONLY. The other ports' presents do not consult it, so they must // not pay to maintain it: the DOS present already skips four rows at a -// time by testing gStageMinWord a uint32 at a time -- a load-width +// time by testing jlgStageMinWord a uint32 at a time -- a load-width // trick that costs the marking side nothing -- and the ST/Amiga walkers // are asm. The 65816 has no 32-bit load, which is exactly why it needs // the explicit map instead. Un-gate this if another port's present @@ -161,7 +178,7 @@ extern uint16_t gStagePaletteDirtyMask; #define STAGE_DIRTY_GROUP_SHIFT 3 #define STAGE_DIRTY_GROUPS (JL_SURFACE_HEIGHT >> STAGE_DIRTY_GROUP_SHIFT) #if defined(JOEYLIB_PLATFORM_IIGS) -extern uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; +extern uint8_t jlgStageDirtyGroup[STAGE_DIRTY_GROUPS]; // Flag the groups covering rows _y .. _yEnd-1 (yEnd exclusive). Rows // are already clipped to [0, JL_SURFACE_HEIGHT) by every caller. @@ -169,7 +186,7 @@ extern uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; uint16_t grp_ = (uint16_t)(_y) >> STAGE_DIRTY_GROUP_SHIFT; \ uint16_t last_ = (uint16_t)((_yEnd) - 1u) >> STAGE_DIRTY_GROUP_SHIFT; \ for (; grp_ <= last_; grp_++) { \ - gStageDirtyGroup[grp_] = 1u; \ + jlgStageDirtyGroup[grp_] = 1u; \ } \ } while (0) #else @@ -189,20 +206,20 @@ extern uint8_t gStageDirtyGroup[STAGE_DIRTY_GROUPS]; // lets a group reuse the one before it. Below this the rows go one by one. #define STAGE_MARK_GROUP_MIN_ROWS (2u * STAGE_DIRTY_GROUP4_ROWS + 1u) #ifdef JOEYLIB_PLATFORM_IIGS -void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord); -#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) iigsMarkDirtyRowsInner((_y), (_yEnd), (uint16_t)(_min), (uint16_t)(_max)) +void jlIigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord); +#define jlSurfaceMarkDirtyRows(_y, _yEnd, _min, _max) jlIigsMarkDirtyRowsInner((_y), (_yEnd), (uint16_t)(_min), (uint16_t)(_max)) // Reset the bands of every 8-row group the group map says was marked to // the CLEAN sentinels, and clear the map (joeyDraw.s): what -// stageDirtyClearAll does after each present, skipping clean groups. -void iigsStageBandsClearAll(void); +// jlStageDirtyClearAll does after each present, skipping clean groups. +void jlIigsStageBandsClearAll(void); #elif defined(JL_CPU_68000) // src/m68k/stageBands.s. Fill stores minFill / maxFill (a byte repeated four // times) over every row of the two arrays. -void stageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, uint32_t minFill, uint32_t maxFill); -void stageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, uint8_t minWord, uint8_t maxWord); -#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) stageBandsMark68k(&gStageMinWord[_y], &gStageMaxWord[_y], (uint16_t)((_yEnd) - (_y)), (uint8_t)(_min), (uint8_t)(_max)) +void jlStageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, uint32_t minFill, uint32_t maxFill); +void jlStageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, uint8_t minWord, uint8_t maxWord); +#define jlSurfaceMarkDirtyRows(_y, _yEnd, _min, _max) jlStageBandsMark68k(&jlgStageMinWord[_y], &jlgStageMaxWord[_y], (uint16_t)((_yEnd) - (_y)), (uint8_t)(_min), (uint8_t)(_max)) #else -void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); +void jlSurfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); #endif // Drawing primitives call this with their already-clipped destination @@ -220,7 +237,7 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma // Macro, not a function (ORCA/C-era rule: no `inline` in headers the // IIgs build shares): the stage test and the h == 1 single-row widen // run inline at the call site; only multi-row marks pay a call, into -// surfaceMarkDirtyRows above. Every argument is evaluated exactly +// jlSurfaceMarkDirtyRows above. Every argument is evaluated exactly // once, but keep call-site arguments side-effect free anyway. // // Planar ports rely on the chunky shadow + c2p path through Phase 8. @@ -232,7 +249,7 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma // the plan is to avoid it entirely there too (planes become the only // source of truth). #define surfaceMarkDirtyRect(_s, _x, _y, _w, _h) do { \ - if ((_s) == gStage) { \ + if ((_s) == jlgStage) { \ int16_t markX_ = (_x); \ int16_t markY_ = (_y); \ int16_t markH_ = (_h); \ @@ -240,14 +257,14 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma uint8_t markMax_ = SURFACE_WORD_INDEX(markX_ + (_w) - 1); \ stageMarkDirtyGroups(markY_, markY_ + markH_); \ if (markH_ == 1) { \ - if (markMin_ < gStageMinWord[markY_]) { \ - gStageMinWord[markY_] = markMin_; \ + if (markMin_ < jlgStageMinWord[markY_]) { \ + jlgStageMinWord[markY_] = markMin_; \ } \ - if (markMax_ > gStageMaxWord[markY_]) { \ - gStageMaxWord[markY_] = markMax_; \ + if (markMax_ > jlgStageMaxWord[markY_]) { \ + jlgStageMaxWord[markY_] = markMax_; \ } \ } else { \ - surfaceMarkDirtyRows((uint16_t)markY_, (uint16_t)(markY_ + markH_), markMin_, markMax_); \ + jlSurfaceMarkDirtyRows((uint16_t)markY_, (uint16_t)(markY_ + markH_), markMin_, markMax_); \ } \ } \ } while (0) @@ -255,14 +272,14 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma // Shorthand for "every row, full width" -- used by jlSurfaceClear and // the bulk-replace paths (jlSurfaceCopy, jlSurfaceLoadFile). No-op if `s` // is not the stage. -void surfaceMarkDirtyAll(const jlSurfaceT *s); +void jlSurfaceMarkDirtyAll(const jlSurfaceT *s); // Reset every row to CLEAN. Called by jlStagePresent after the slam. -void stageDirtyClearAll(void); +void jlStageDirtyClearAll(void); // y -> byte offset of row y in a JL_SURFACE_BYTES_PER_ROW-strided buffer. // On IIgs this expands to a single indexed long-mode read against -// gRowOffsetLut (built once at jlpInit). +// jlgRowOffsetLut (built once at jlpInit). // // The explicit (y << 1) byte-pointer arithmetic dodges the multiply // helper that gets emitted for `uint16_t arr[N]` indexing @@ -272,9 +289,9 @@ void stageDirtyClearAll(void); // Other ports get the straight multiply -- gcc / OpenWatcom optimize // the constant 160 to a shift+add chain. #ifdef JOEYLIB_PLATFORM_IIGS -extern const uint16_t gRowOffsetLut[JL_SURFACE_HEIGHT]; +extern const uint16_t jlgRowOffsetLut[JL_SURFACE_HEIGHT]; #define SURFACE_ROW_OFFSET(_y) \ - (*((const uint16_t *)((const uint8_t *)gRowOffsetLut + ((uint16_t)(_y) << 1)))) + (*((const uint16_t *)((const uint8_t *)jlgRowOffsetLut + ((uint16_t)(_y) << 1)))) #else #define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * JL_SURFACE_BYTES_PER_ROW)) #endif @@ -324,19 +341,19 @@ extern const uint16_t gRowOffsetLut[JL_SURFACE_HEIGHT]; // Record a human-readable error string for jlLastError(). Defined in // init.c; usable by other core TUs (e.g. surface.c on allocation / // limit failure). The string must have static lifetime. -void coreSetError(const char *message); +void jlCoreSetError(const char *message); // Allocate and free the library-owned stage (the back-buffer surface // that jlStagePresent flips to the display). Called from init.c during // jlInit / jlShutdown. The stage's pixel storage is supplied by // the port HAL via jlpStageAllocPixels. -bool stageAlloc(void); -void stageFree(void); +bool jlStageAlloc(void); +void jlStageFree(void); // Fill all 16 of `s`'s palettes with the standard 16-color EGA -// palette. Called by stageAlloc and jlSurfaceCreate so a program that +// palette. Called by jlStageAlloc and jlSurfaceCreate so a program that // draws without first calling jlPaletteSet still gets recognizable // colors instead of an all-black palette. -void paletteInitDefault(jlSurfaceT *s); +void jlPaletteInitDefault(jlSurfaceT *s); #endif diff --git a/src/core/tile.c b/src/core/tile.c index d4281a3..b2fabe4 100644 --- a/src/core/tile.c +++ b/src/core/tile.c @@ -39,8 +39,8 @@ // dirty word means SURFACE_WORD_INDEX(bx*8) == bx*2 and // SURFACE_WORD_INDEX(bx*8 + 7) == bx*2 + 1. Deriving the band from bx // and widening the 8 rows inline replaces the out-of-line -// surfaceMarkDirtyRows call (~800 cycles measured steady-state on the -// 68000 ports). Callers own the `s == gStage` gate (non-stage +// jlSurfaceMarkDirtyRows call (~800 cycles measured steady-state on the +// 68000 ports). Callers own the `s == jlgStage` gate (non-stage // surfaces carry no dirty state), exactly like surfaceMarkDirtyRect. // The IIgs keeps its single-JSL asm marker -- already the cheap path // there. @@ -54,23 +54,34 @@ typedef char tileMarkBandProof[(SURFACE_WORD_INDEX(JL_TILE_PIXELS_PER_SIDE) == 2 typedef char tileMapStrideProof[(sizeof(jlTileT) == 32) ? 1 : -1]; #ifdef JOEYLIB_PLATFORM_IIGS -#define tileMarkDirty(_pixelY, _bx) iigsMarkDirtyRowsInner((uint16_t)(_pixelY), (uint16_t)((_pixelY) + JL_TILE_PIXELS_PER_SIDE), (uint16_t)((uint16_t)(_bx) << 1), (uint16_t)(((uint16_t)(_bx) << 1) + 1u)) +#define tileMarkDirty(_pixelY, _bx) jlIigsMarkDirtyRowsInner((uint16_t)(_pixelY), (uint16_t)((_pixelY) + JL_TILE_PIXELS_PER_SIDE), (uint16_t)((uint16_t)(_bx) << 1), (uint16_t)(((uint16_t)(_bx) << 1) + 1u)) // Fill/copy/paste FUSE the mark into their stage-arm asm entries on // the IIgs (port.h jlpTile* macros -> iigsTile*Mark, NATIVE-PERF // Phase 2 Step D), so their C-side mark compiles out here. -// copyMasked/pasteMono still mark separately via tileMarkDirty. +// copyMasked/pasteMono still mark separately via tileMarkDirty. The glyph +// paste is all asm there (JL_HAS_TILE_PASTE_GLYPH_ENTRY), mark included. #define tileMarkDirtyFused(_pixelY, _bx) ((void)0) -// pasteGlyph's IIgs stage arm fuses the mark the same way (iigsTilePasteGlyphMark). -#if defined(JL_HAS_TILE_PASTE_GLYPH) -#define tileMarkDirtyGlyphFused(_pixelY, _bx) ((void)0) -#else -#define tileMarkDirtyGlyphFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx)) -#endif #else // On non-IIgs ports nothing fuses the mark, so the "fused" spelling // is the plain helper. #define tileMarkDirtyFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx)) -#define tileMarkDirtyGlyphFused(_pixelY, _bx) tileMarkDirty((_pixelY), (_bx)) +// tileMarkDirty's long compares cover a tile's rows as two band groups. +typedef char tileMarkGroupProof[(JL_TILE_PIXELS_PER_SIDE == 2 * STAGE_DIRTY_GROUP4_ROWS) ? 1 : -1]; +#if defined(JL_CPU_68000) +#include "m68kAsm.h" +// The 68000 ports widen a tile's rows in src/m68k/stageBands.s +// (jlStageBandsMarkTile68k), the C below in asm: gcc 15, the ST's compiler, +// spent ~700 cycles a tile on it. Called with register arguments. +static inline __attribute__((always_inline)) void tileMarkDirty(uint16_t pixelY, uint8_t bx) { + register uint8_t *rMin __asm__("a0") = &jlgStageMinWord[pixelY]; + register uint8_t *rMax __asm__("a1") = &jlgStageMaxWord[pixelY]; + register uint16_t rWord __asm__("d1") = (uint16_t)((uint16_t)bx << 1); + + __asm__ volatile ("jsr " M68K_ASM_SYM(jlStageBandsMarkTile68k) : "+a" (rMin), "+a" (rMax), "+d" (rWord) : : "d0", "d2", "d3", "cc", "memory"); +} +#else +// A band group rotated a byte (a 68000 rol.l #8). +#define TILE_MARK_ROTATE8(_g) ((uint32_t)(((_g) << 8) | ((_g) >> 24))) // Explicit 8-row unroll: gcc-6.5 -O2 keeps the equivalent loop rolled // (8 iterations of compare + spill-reload, ~670 cycles); the d16(An) // unroll is ~450. Same widen semantics, row by row, as the loop. @@ -84,10 +95,16 @@ typedef char tileMapStrideProof[(sizeof(jlTileT) == 32) ? 1 : -1]; } while (0) static void tileMarkDirty(uint16_t pixelY, uint8_t bx) { - uint8_t *minPtr; - uint8_t *maxPtr; - uint8_t minWord; - uint8_t maxWord; + StageBandGroupT *minGroup; + StageBandGroupT *maxGroup; + uint8_t *minPtr; + uint8_t *maxPtr; + uint32_t minBand; + uint32_t maxBand; + uint8_t minWord; + uint8_t maxWord; + uint8_t rowMin; + uint8_t rowMax; minWord = (uint8_t)(bx << 1); maxWord = (uint8_t)(minWord + 1u); @@ -95,8 +112,42 @@ static void tileMarkDirty(uint16_t pixelY, uint8_t bx) { // EXACTLY one dirty group -- one store, no range loop. (This arm is // non-IIgs, where the map is not maintained; the macro is a no-op.) stageMarkDirtyGroups(pixelY, pixelY + JL_TILE_PIXELS_PER_SIDE); - minPtr = &gStageMinWord[pixelY]; - maxPtr = &gStageMaxWord[pixelY]; + minPtr = &jlgStageMinWord[pixelY]; + maxPtr = &jlgStageMaxWord[pixelY]; + // The tile's 8 rows are two groups of 4 (pixelY is a multiple of 8). + // When all 8 hold the same band -- clean after a present, whole after a + // full redraw, or what earlier tiles in the same rows left -- one band + // widens and goes back as four long stores (or nothing, if it already + // covers the tile) instead of 8 row widens. A group's bytes are all the + // same exactly when it equals itself rotated a byte. A clean row's max is + // the CLEAN sentinel too, as every mark widens both, so it widens to the + // tile's band like any other. + minGroup = (StageBandGroupT *)(void *)minPtr; + maxGroup = (StageBandGroupT *)(void *)maxPtr; + minBand = minGroup[0]; + maxBand = maxGroup[0]; + if (minBand == minGroup[1] && maxBand == maxGroup[1] && minBand == TILE_MARK_ROTATE8(minBand) && maxBand == TILE_MARK_ROTATE8(maxBand)) { + rowMin = (uint8_t)minBand; + rowMax = (uint8_t)maxBand; + if (rowMin <= minWord && rowMax >= maxWord) { + return; + } + if (minWord < rowMin) { + rowMin = minWord; + } + if (maxWord > rowMax) { + rowMax = maxWord; + } + minBand = (uint32_t)rowMin | ((uint32_t)rowMin << 8); + minBand |= minBand << 16; + maxBand = (uint32_t)rowMax | ((uint32_t)rowMax << 8); + maxBand |= maxBand << 16; + minGroup[0] = minBand; + minGroup[1] = minBand; + maxGroup[0] = maxBand; + maxGroup[1] = maxBand; + return; + } TILE_MARK_ROW(0); TILE_MARK_ROW(1); TILE_MARK_ROW(2); @@ -107,6 +158,7 @@ static void tileMarkDirty(uint16_t pixelY, uint8_t bx) { TILE_MARK_ROW(7); } #endif +#endif // Prototype (drawTextUnion is the only other static in this TU). static void drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds); @@ -223,7 +275,7 @@ void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT } // Single op: machine asm/planar override or the generic chunky default. jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); - if (dst == gStage) { + if (dst == jlgStage) { tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(dstBy), dstBx); } } @@ -237,7 +289,7 @@ void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSur return; } jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex); - if (dst == gStage) { + if (dst == jlgStage) { tileMarkDirty(TILE_BLOCK_TO_PIXEL(dstBy), dstBx); } } @@ -253,7 +305,7 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { // Single compile-time-selected op: machine asm/planar override or the // generic chunky default. jlpTileFill(s, bx, by, colorIndex); - if (s == gStage) { + if (s == jlgStage) { tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(by), bx); } } @@ -284,16 +336,16 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uin // multiply per cell, which alone erased the batch win there. #ifdef JOEYLIB_PLATFORM_IIGS // The stage test is hoisted OUT of the loop on the IIgs: the - // jlpTilePaste macro's per-cell (dst == gStage) ternary + // jlpTilePaste macro's per-cell (dst == jlgStage) ternary // materializes a boolean (~65 cycles/cell) that a 10x5 map pays // 50 times. The fused stage entry marks each pasted tile itself, // so the stage arm needs no union; non-stage surfaces carry no // dirty state. - if (dst == gStage) { + if (dst == jlgStage) { // ONE JSL for the whole map (NATIVE-PERF Phase 4): the asm // walker amortizes the per-tile JSL + arg staging + derive + // 8-row mark across the rectangle and issues one band union. - iigsTileMapPasteMark(bx, by, wTiles, hTiles, (const uint8_t *)tiles, map); + jlIigsTileMapPasteMark(bx, by, wTiles, hTiles, (const uint8_t *)tiles, map); return; } row = map; @@ -301,7 +353,7 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uin for (tx = 0; tx < wTiles; tx++) { idx = row[tx]; if (idx != JL_TILE_MAP_SKIP) { - iigsTilePasteInner(&dst->pixels[SURFACE_ROW_OFFSET((uint16_t)(by + ty) << 3) + ((uint16_t)(bx + tx) << 2)], (const uint8_t *)tiles + ((uint16_t)idx << 5)); + jlIigsTilePasteInner(&dst->pixels[SURFACE_ROW_OFFSET((uint16_t)(by + ty) << 3) + ((uint16_t)(bx + tx) << 2)], (const uint8_t *)tiles + ((uint16_t)idx << 5)); } } row += wTiles; @@ -317,8 +369,8 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uin // One rectangle-union mark for the whole map. Over-marks // JL_TILE_MAP_SKIP cells (safe: present copies extra CLEAN content, // pixels unchanged). - if (dst == gStage) { - surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u)); + if (dst == jlgStage) { + jlSurfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u)); } #else row = map; @@ -334,8 +386,8 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uin // One rectangle-union mark for the whole map. Over-marks // JL_TILE_MAP_SKIP cells (safe: present copies extra CLEAN content, // pixels unchanged). - if (dst == gStage) { - surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u)); + if (dst == jlgStage) { + jlSurfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u)); } #endif } @@ -350,12 +402,17 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) { } // Single op: machine asm/planar override or the generic chunky default. jlpTilePaste(dst, bx, by, &in->pixels[0]); - if (dst == gStage) { + if (dst == jlgStage) { tileMarkDirtyFused(TILE_BLOCK_TO_PIXEL(by), bx); } } +// A port with its own jlTilePasteGlyph (JL_HAS_TILE_PASTE_GLYPH_ENTRY, the +// IIgs's asm) makes these checks there, against these bounds. +#if defined(JL_HAS_TILE_PASTE_GLYPH_ENTRY) +typedef char tilePasteGlyphEntryProof[(JL_TILE_BLOCKS_PER_ROW == 40 && JL_TILE_BLOCKS_PER_COL == 25) ? 1 : -1]; +#else void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { if (dst == NULL || bits == NULL) { return; @@ -366,10 +423,11 @@ void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bi fgColor &= 0x0Fu; bgColor &= 0x0Fu; jlpTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); - if (dst == gStage) { - tileMarkDirtyGlyphFused(TILE_BLOCK_TO_PIXEL(by), bx); + if (dst == jlgStage) { + tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx); } } +#endif void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) { @@ -385,7 +443,7 @@ void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, // (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste. // Replaces the old runtime pixels==NULL planar/chunky branch. jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor); - if (dst == gStage) { + if (dst == jlgStage) { tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx); } } diff --git a/src/core/tileLayer.c b/src/core/tileLayer.c new file mode 100644 index 0000000..9ab86d4 --- /dev/null +++ b/src/core/tileLayer.c @@ -0,0 +1,300 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Tile layers (joey/tile.h): the ring bookkeeping every port shares. +// Which map cell each slot holds, which slots a move uncovers, and the +// band's dirty mark live here; drawing a tile into a slot and copying the +// view out are the port's (jlpTileLayer*, tileLayerInternal.h). +// +// A move of the origin by (dc, dr) whole tiles re-labels the slots: the +// view's first column moves dc slot columns on, and the dc columns that +// came into view are drawn into the slots the columns that left gave up +// (the same for rows). The pixel phase (origin & 7) costs nothing here: +// it is where the blit starts reading the ring. + +#include <stddef.h> +#include <string.h> + +#include "joey/tile.h" +#include "port.h" +#include "surfaceInternal.h" +#include "tileLayerInternal.h" + +// Tileset indexing below is idx << 5; pin it to sizeof(jlTileT). +typedef char tileLayerStrideProof[(sizeof(jlTileT) == 32) ? 1 : -1]; + +// What a JL_TILE_MAP_SKIP cell draws: colour 0, all zero bits in every +// port's tile format. +static const jlTileT gBlankTile; + +static void drawCells(jlTileLayerT *layer, uint8_t c0, uint8_t c1, uint8_t r0, uint8_t r1); +static void fill(jlTileLayerT *layer, uint16_t col, uint16_t row); +static uint16_t forward(uint16_t from, uint16_t to, uint16_t size); +static void scrollCols(jlTileLayerT *layer, uint16_t col); +static void scrollRows(jlTileLayerT *layer, uint16_t row); +static const jlTileT *tileFor(const jlTileLayerT *layer, uint8_t idx); +static uint16_t wrapAdd(uint16_t a, uint16_t b, uint16_t size); + + +// Draws the view cells in columns [c0, c1) and rows [r0, r1) into their +// slots, walking map and slot positions forward with their wraps. +static void drawCells(jlTileLayerT *layer, uint8_t c0, uint8_t c1, uint8_t r0, uint8_t r1) { + const uint8_t *mapRow; + uint16_t mapC0; + uint16_t mapC; + uint16_t mapR; + uint8_t slotC0; + uint8_t slotC; + uint8_t slotR; + uint8_t c; + uint8_t r; + + mapC0 = wrapAdd(layer->mapCol, c0, layer->mapWidth); + slotC0 = (uint8_t)wrapAdd(layer->slotCol, c0, TILE_LAYER_RING_COLS); + mapR = wrapAdd(layer->mapRow, r0, layer->mapHeight); + slotR = (uint8_t)wrapAdd(layer->slotRow, r0, layer->ringRows); + mapRow = layer->map + (uint32_t)mapR * layer->mapWidth; + for (r = r0; r < r1; r++) { + mapC = mapC0; + slotC = slotC0; + for (c = c0; c < c1; c++) { + jlpTileLayerPut(layer, slotC, slotR, tileFor(layer, mapRow[mapC])); + mapC++; + if (mapC == layer->mapWidth) { + mapC = 0u; + } + slotC++; + if (slotC == TILE_LAYER_RING_COLS) { + slotC = 0u; + } + } + mapR++; + mapRow += layer->mapWidth; + if (mapR == layer->mapHeight) { + mapR = 0u; + mapRow = layer->map; + } + slotR++; + if (slotR == layer->ringRows) { + slotR = 0u; + } + } +} + + +// Draws the whole view afresh with its first cell at map (col, row). +static void fill(jlTileLayerT *layer, uint16_t col, uint16_t row) { + layer->mapCol = col; + layer->mapRow = row; + layer->slotCol = 0u; + layer->slotRow = 0u; + drawCells(layer, 0u, TILE_LAYER_VIEW_COLS, 0u, layer->ringRows); + layer->drawn = true; +} + + +// How far `to` is ahead of `from` going forward around a ring of `size`. +static uint16_t forward(uint16_t from, uint16_t to, uint16_t size) { + return (to >= from) ? (uint16_t)(to - from) : (uint16_t)(to + size - from); +} + + +jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map, uint16_t mapWidth, uint16_t mapHeight, uint8_t viewBy, uint8_t viewHeight) { + jlTileLayerT *layer; + + if (tiles == NULL || map == NULL) { + return NULL; + } + if (mapWidth == 0u || mapWidth > JL_TILE_LAYER_MAX_SIDE || mapHeight == 0u || mapHeight > JL_TILE_LAYER_MAX_SIDE) { + return NULL; + } + if (viewHeight == 0u || (uint16_t)viewBy + viewHeight > JL_TILE_BLOCKS_PER_COL) { + return NULL; + } + // jlpBigAlloc, not the C heap, for jlSpriteCreate's reason: the IIgs's + // C heap is a few hundred bytes. + layer = (jlTileLayerT *)jlpBigAlloc((uint32_t)sizeof(jlTileLayerT)); + if (layer == NULL) { + return NULL; + } + memset(layer, 0, sizeof(jlTileLayerT)); + layer->tiles = tiles; + layer->map = map; + layer->mapWidth = mapWidth; + layer->mapHeight = mapHeight; + layer->viewBy = viewBy; + layer->viewHeight = viewHeight; + layer->ringRows = (uint8_t)(viewHeight + 1u); + if (!jlpTileLayerRingAlloc(layer)) { + jlpBigFree(layer); + return NULL; + } + return layer; +} + + +void jlTileLayerDestroy(jlTileLayerT *layer) { + if (layer == NULL) { + return; + } + jlpTileLayerRingFree(layer); + jlpBigFree(layer); +} + + +void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer, uint16_t x, uint16_t y) { + uint16_t col; + uint16_t row; + uint16_t widthPx; + uint16_t heightPx; + + if (dst == NULL || layer == NULL) { + return; + } + // The map's pixel size fits 16 bits: JL_TILE_LAYER_MAX_SIDE * 8 = 32768. + // A position already on the map, the usual one, skips the divide (a + // libcall on the 65816). + widthPx = (uint16_t)(layer->mapWidth << 3); + heightPx = (uint16_t)(layer->mapHeight << 3); + if (x >= widthPx) { + x = (uint16_t)(x % widthPx); + } + if (y >= heightPx) { + y = (uint16_t)(y % heightPx); + } + col = (uint16_t)(x >> 3); + row = (uint16_t)(y >> 3); + if (!layer->drawn) { + fill(layer, col, row); + } else { + scrollCols(layer, col); + scrollRows(layer, row); + } + jlpTileLayerBlit(dst, layer, (uint16_t)(((uint16_t)layer->slotCol << 3) + (x & 7u)), (uint16_t)(((uint16_t)layer->slotRow << 3) + (y & 7u))); + surfaceMarkDirtyRect(dst, 0, (int16_t)((uint16_t)layer->viewBy << 3), JL_SURFACE_WIDTH, (int16_t)((uint16_t)layer->viewHeight << 3)); +} + + +void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty, uint8_t tile) { + uint16_t mapC; + uint16_t mapR; + uint8_t c; + uint8_t r; + + if (layer == NULL || tx >= layer->mapWidth || ty >= layer->mapHeight) { + return; + } + layer->map[(uint32_t)ty * layer->mapWidth + tx] = tile; + if (!layer->drawn) { + return; + } + // A map smaller than the view shows a cell more than once. + mapC = layer->mapCol; + for (c = 0u; c < TILE_LAYER_VIEW_COLS; c++) { + if (mapC == tx) { + mapR = layer->mapRow; + for (r = 0u; r < layer->ringRows; r++) { + if (mapR == ty) { + drawCells(layer, c, (uint8_t)(c + 1u), r, (uint8_t)(r + 1u)); + } + mapR++; + if (mapR == layer->mapHeight) { + mapR = 0u; + } + } + } + mapC++; + if (mapC == layer->mapWidth) { + mapC = 0u; + } + } +} + + +// Moves the view's first column to map column `col` the short way round, +// drawing the columns that come into view. A move of a whole view or more +// draws everything. +static void scrollCols(jlTileLayerT *layer, uint16_t col) { + uint16_t ahead; + uint16_t back; + + ahead = forward(layer->mapCol, col, layer->mapWidth); + if (ahead == 0u) { + return; + } + back = (uint16_t)(layer->mapWidth - ahead); + if (ahead <= back) { + if (ahead >= TILE_LAYER_VIEW_COLS) { + fill(layer, col, layer->mapRow); + return; + } + layer->mapCol = col; + layer->slotCol = (uint8_t)wrapAdd(layer->slotCol, ahead, TILE_LAYER_RING_COLS); + drawCells(layer, (uint8_t)(TILE_LAYER_VIEW_COLS - ahead), TILE_LAYER_VIEW_COLS, 0u, layer->ringRows); + } else { + if (back >= TILE_LAYER_VIEW_COLS) { + fill(layer, col, layer->mapRow); + return; + } + layer->mapCol = col; + layer->slotCol = (uint8_t)wrapAdd(layer->slotCol, (uint16_t)(TILE_LAYER_RING_COLS - back), TILE_LAYER_RING_COLS); + drawCells(layer, 0u, (uint8_t)back, 0u, layer->ringRows); + } +} + + +// scrollCols for rows. +static void scrollRows(jlTileLayerT *layer, uint16_t row) { + uint16_t ahead; + uint16_t back; + + ahead = forward(layer->mapRow, row, layer->mapHeight); + if (ahead == 0u) { + return; + } + back = (uint16_t)(layer->mapHeight - ahead); + if (ahead <= back) { + if (ahead >= layer->ringRows) { + fill(layer, layer->mapCol, row); + return; + } + layer->mapRow = row; + layer->slotRow = (uint8_t)wrapAdd(layer->slotRow, ahead, layer->ringRows); + drawCells(layer, 0u, TILE_LAYER_VIEW_COLS, (uint8_t)(layer->ringRows - ahead), layer->ringRows); + } else { + if (back >= layer->ringRows) { + fill(layer, layer->mapCol, row); + return; + } + layer->mapRow = row; + layer->slotRow = (uint8_t)wrapAdd(layer->slotRow, (uint16_t)(layer->ringRows - back), layer->ringRows); + drawCells(layer, 0u, TILE_LAYER_VIEW_COLS, 0u, (uint8_t)back); + } +} + + +// The tile a map index draws. +static const jlTileT *tileFor(const jlTileLayerT *layer, uint8_t idx) { + if (idx == JL_TILE_MAP_SKIP) { + return &gBlankTile; + } + return (const jlTileT *)((const uint8_t *)layer->tiles + ((uint16_t)idx << 5)); +} + + +// (a + b) % size for an a below size; b may pass size any number of times +// (a map smaller than the view). One subtract covers the rest. Not a +// subtract loop: Loop Strength Reduction replaces one with its closed +// form, a divide and a multiply (libcalls on the 65816), for a loop that +// runs once or not at all. +static uint16_t wrapAdd(uint16_t a, uint16_t b, uint16_t size) { + a = (uint16_t)(a + b); + if (a >= size) { + a = (uint16_t)(a - size); + if (a >= size) { + a = (uint16_t)(a % size); + } + } + return a; +} diff --git a/src/core/tileLayerInternal.h b/src/core/tileLayerInternal.h new file mode 100644 index 0000000..965d8de --- /dev/null +++ b/src/core/tileLayerInternal.h @@ -0,0 +1,74 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Internal tile-layer definition shared by src/core/tileLayer.c and the +// ports' ring code (jlpTileLayer* in port.h). +// +// A layer keeps a RING of drawn tiles: TILE_LAYER_RING_COLS slot columns +// by ringRows slot rows, each slot one 8x8 tile in the port's own format. +// The view's TILE_LAYER_VIEW_COLS x ringRows tiles sit in consecutive +// slots, wrapping at the ring's edges: view column c in slot column +// (slotCol + c) % TILE_LAYER_RING_COLS, view row r in slot row +// (slotRow + r) % ringRows. A move re-labels slots instead of moving +// pixels, so only the tiles it uncovers are drawn (tileLayer.c). +// +// A port sees the ring only through four ops: +// jlpTileLayerRingAlloc allocate layer->ring for ringRows slot rows +// jlpTileLayerRingFree free it +// jlpTileLayerPut draw one tile into one slot +// jlpTileLayerBlit copy the view into a surface's band: band row +// i is ring pixel row (ringY + i) % (ringRows * 8), +// band column x ring pixel column (ringX + x) % +// TILE_LAYER_RING_PX +// The generic ops (src/generic/genericTileLayer.c) keep the chunky rings +// below. A port with a ring of its own defines JL_HAS_TILE_LAYER and all +// four; a chunky port with only a faster copy defines +// JL_HAS_TILE_LAYER_BLIT and jlpTileLayerBlit, over the chunky rings. + +#ifndef JOEYLIB_TILE_LAYER_INTERNAL_H +#define JOEYLIB_TILE_LAYER_INTERNAL_H + +#include <stdbool.h> +#include <stdint.h> + +#include "joey/tile.h" + +// A 320-pixel view at any pixel phase touches 41 tile columns; the ring +// rounds that up to 42 so it is whole 16-pixel groups (21) for the planar +// ports. The one slot the view does not use is never shown: a blit's +// shift moves its pixels out of the band. +#define TILE_LAYER_VIEW_COLS (JL_TILE_BLOCKS_PER_ROW + 1) +#define TILE_LAYER_RING_COLS (JL_TILE_BLOCKS_PER_ROW + 2) +#define TILE_LAYER_RING_PX (TILE_LAYER_RING_COLS * JL_TILE_PIXELS_PER_SIDE) + +// The chunky rings: `even`, and `odd` holding the same pixels one to the +// left, so an odd ringX copies whole bytes from `odd` instead of joining +// nibbles. A pixel row is TILE_LAYER_CHUNKY_BYTES bytes and a 2-byte guard +// repeating its first two (a 16-bit read at the last byte sees the right +// neighbour). +#define TILE_LAYER_CHUNKY_BYTES (TILE_LAYER_RING_PX / 2) +#define TILE_LAYER_CHUNKY_STRIDE (TILE_LAYER_CHUNKY_BYTES + 2) + +typedef struct { + uint8_t *even; + uint8_t *odd; +} TileLayerChunkyRingsT; + +struct jlTileLayerT { + const jlTileT *tiles; + uint8_t *map; + void *ring; // the port's slots (jlpTileLayerRingAlloc) + uint16_t mapWidth; + uint16_t mapHeight; + uint16_t mapCol; // the map column the view starts in + uint16_t mapRow; // the map row the view starts in + uint8_t slotCol; // the slot column holding the view's first column + uint8_t slotRow; // the slot row holding the view's first row + uint8_t ringRows; // viewHeight + 1: a view at any phase touches that many + uint8_t viewBy; + uint8_t viewHeight; + bool drawn; // the slots hold the view (false until the first draw) +}; + +#endif diff --git a/src/dos/audio.c b/src/dos/audio.c index c84f552..6c713ba 100644 --- a/src/dos/audio.c +++ b/src/dos/audio.c @@ -21,6 +21,11 @@ // of the DSP at once; the refills render the rest. // * jlpAudioPlaySfx arms an overlay slot, mixed over the module in the // same way. +// * jlpAudioNoise is the YM2149's noise, as psgSynth.c makes it: a 17-bit +// LFSR clocked at AUDIO_NOISE_CLOCK_HZ / pitch, mixed by the SB IRQ into +// the block after the one the DSP plays (MIX_BLOCK_FRAMES, ~12 ms), so a +// change is heard within ~23 ms however long the halves are. An OPL2 +// without a Sound Blaster plays its snare drum instead. // // Defaults match DOSBox-Staging's emulated Sound Blaster (an SB16): base // $220, IRQ 7, 8-bit DMA channel 1. BLASTER environment variable overrides @@ -140,18 +145,34 @@ static const uint8_t kDmaPagePort[DMA8_CHANNELS] = { 0x87, 0x83, 0x81, 0x82 }; // before it play mono). #define MIX_HALF_FRAMES 2048u #define MIX_CHANNELS_MAX 2u +// The DSP interrupts at the end of each block of MIX_BLOCK_FRAMES frames, a +// half being MIX_BLOCKS_PER_HALF of them: the noise is mixed a block at a +// time, NOISE_LEAD_BLOCKS past the block that just played. +#define MIX_BLOCK_FRAMES 256u +#define MIX_BLOCKS_PER_HALF (MIX_HALF_FRAMES / MIX_BLOCK_FRAMES) +#define MIX_BLOCKS (MIX_BLOCKS_PER_HALF * 2u) +#define NOISE_LEAD_BLOCKS 2u #define MIX_HALF_MAX (MIX_HALF_FRAMES * MIX_CHANNELS_MAX) #define MIX_DMA_BUFFER_MAX (MIX_HALF_MAX * 2u) #define MIX_SILENCE MOD_PLAYER_SILENCE_U8 // The 8-bit mix's unit: a module channel at full volume and a full-scale // sound effect each swing a quarter of a byte, so the module's four channels -// fit (modPlayerSetU8Shift, audioSfxSetShift); the card's mixer makes up +// fit (jlModPlayerSetU8Shift, jlAudioSfxSetShift); the card's mixer makes up // the level (above). #define MIX_UNIT_SHIFT 2u // Frames ahead of the DSP where a new sound's start-now overlay begins // (sfxStartNow): about 3 ms, so the write always lands before the read. #define SFX_START_MARGIN 64u #define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base +// The noise: its LFSR (psgSynth.c's), and its phase in LFSR clocks, 16.16. +// At least one clock a frame is white at the output rate, so pitches whose +// rate passes it are clocked once a frame. +#define NOISE_LFSR_SEED 1u +#define NOISE_LFSR_TOP_SHIFT 16 +#define NOISE_LFSR_TAP_SHIFT 3 +#define NOISE_PHASE_SHIFT 16 +#define NOISE_PHASE_ONE (1uL << NOISE_PHASE_SHIFT) +#define NOISE_HI_NEVER (1uL << 31) // above the LFSR's 17 bits: never all set #define ISR_LOCK_SIZE 4096 @@ -169,6 +190,7 @@ static bool gDspPresent = false; // the module player and the SFX resampler, so they match what the DAC // actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting. static uint16_t gEffRate = MIX_RATE; +static uint16_t gBlockBytes = MIX_BLOCK_FRAMES; // Mono or stereo (DSP 4.xx), and so a half's bytes. static uint8_t gChannels = 1u; static uint16_t gHalfBytes = MIX_HALF_FRAMES; @@ -199,12 +221,23 @@ static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS]; static uint8_t gSfxPan[JL_AUDIO_SFX_SLOTS]; // jlAudioSetPan's, stereo only // ISR-side state. volatile so the main loop sees IRQ updates promptly. -// gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised -// in the IRQ handler and lowered in jlpAudioFrameTick after the -// half has been refilled. The mix runs on the main thread, not in the -// interrupt: it is long, and the module player is not reentrant. +// gNextBlock is the block the DSP finishes next; gNextHalf the half it is +// in, flipping as a half's last block ends; gNeedRefill[i] is raised then +// and lowered in jlpAudioFrameTick after the half has been refilled. The +// mix runs on the main thread, not in the interrupt: it is long, and the +// module player is not reentrant. Only the noise is mixed in the +// interrupt, a block at a time. +static volatile uint8_t gNextBlock = 0; static volatile uint8_t gNextHalf = 0; static volatile bool gNeedRefill[2] = { false, false }; +// The noise jlpAudioNoise asked for: LFSR clocks a frame (16.16, at most +// one clock), its swing (0: none) and when it is one more (kNoiseSwing); +// and the generator, run by the IRQ. +static volatile uint32_t gNoiseStep = 0u; +static volatile uint32_t gNoiseHiMask = NOISE_HI_NEVER; +static volatile uint8_t gNoiseAmp = 0u; +static uint32_t gNoisePhase = 0u; +static uint32_t gNoiseLfsr = NOISE_LFSR_SEED; // Whether interrupts were on at the outermost jlAudioCriticalEnter. static bool gCriticalIrqWasOn = false; @@ -276,6 +309,7 @@ static void modAdd(uint8_t half, uint16_t from); static void modRemove(uint8_t half, uint16_t from); static void modRemoveAhead(void); static void modStartNow(void); +static void noiseMixBlock(uint8_t *dst); static void parseBlasterEnv(void); static void programDmaController(void); static void programDspAutoInit(void); @@ -507,12 +541,12 @@ static uint8_t dspTimeConst(uint16_t rate) { } -// An interrupt at the end of each half. The SB16 takes the rate in Hz and +// An interrupt at the end of each block. The SB16 takes the rate in Hz and // plays stereo; the cards before it take a time constant and play mono. static void programDspAutoInit(void) { uint16_t blockMinusOne; - blockMinusOne = (uint16_t)(gHalfBytes - 1u); + blockMinusOne = (uint16_t)(gBlockBytes - 1u); dspWrite(DSP_SPEAKER_ON); if (gChannels == MIX_CHANNELS_MAX) { dspWrite(DSP_OUT_RATE); @@ -595,7 +629,7 @@ static void modAdd(uint8_t half, uint16_t from) { dst = gDmaBuf + (uint32_t)half * gHalfBytes; mod = gModHalf[half]; - if (!modPlayerRenderU8(mod + from, (uint32_t)(gHalfBytes - from) / gChannels, gChannels)) { + if (!jlModPlayerRenderU8(mod + from, (uint32_t)(gHalfBytes - from) / gChannels, gChannels)) { gModTail = true; gModTailHalf = half; } @@ -655,7 +689,7 @@ static void modStartNow(void) { if (ahead.start < ahead.halfEnd) { modAdd(ahead.half, (uint16_t)(ahead.start - (uint16_t)ahead.half * gHalfBytes)); } - if (ahead.nextMixed && modPlayerActive()) { + if (ahead.nextMixed && jlModPlayerActive()) { modAdd((uint8_t)(ahead.half ^ 1u), ahead.otherFrom); } } @@ -671,8 +705,8 @@ static void refillHalf(uint8_t halfIdx) { uint8_t *dst; dst = gDmaBuf + ((uint32_t)halfIdx * gHalfBytes); - if (modPlayerActive()) { - if (!modPlayerRenderU8(gModHalf[halfIdx], MIX_HALF_FRAMES, gChannels)) { + if (jlModPlayerActive()) { + if (!jlModPlayerRenderU8(gModHalf[halfIdx], MIX_HALF_FRAMES, gChannels)) { gModTail = true; gModTailHalf = halfIdx; } @@ -684,15 +718,20 @@ static void refillHalf(uint8_t halfIdx) { } -// SB IRQ. Acks the SB (read $0E), marks the half that just played -// for refill, EOIs the PIC. The actual mixer work happens in +// SB IRQ, at the end of each block. Marks a half that just played for +// refill, acks the SB (read $0E) and EOIs the PIC; then, interrupts back +// on (a 16550 at 115200 fills in 1.4 ms), mixes the noise into the block +// after the one the DSP now plays. Everything else is mixed in // jlpAudioFrameTick on the main thread. static void sbIsr(void) { - uint8_t fillHalf; + uint8_t done; - fillHalf = gNextHalf; - gNextHalf = (uint8_t)(gNextHalf ^ 1); - gNeedRefill[fillHalf] = true; + done = gNextBlock; + gNextBlock = (uint8_t)((done + 1u) & (MIX_BLOCKS - 1u)); + if ((done & (MIX_BLOCKS_PER_HALF - 1u)) == MIX_BLOCKS_PER_HALF - 1u) { + gNeedRefill[done / MIX_BLOCKS_PER_HALF] = true; + gNextHalf = (uint8_t)(gNextHalf ^ 1u); + } (void)inportb(gSbBase + SB_DATA_AVAIL_OFFSET); @@ -700,17 +739,64 @@ static void sbIsr(void) { outportb(PIC2_CMD, PIC_EOI); } outportb(PIC1_CMD, PIC_EOI); + + if (gNoiseAmp != 0u) { + enable(); + noiseMixBlock(gDmaBuf + (uint32_t)((done + NOISE_LEAD_BLOCKS) & (MIX_BLOCKS - 1u)) * gBlockBytes); + } +} + + +// One block of the noise over what is mixed at dst: psgSynth.c's generator +// (the LFSR clocked as its phase passes a whole clock, the output its bit 0 +// as +-the swing), added to every channel and held to the byte. Runs in the +// SB IRQ: locked, with the data it touches. +static void noiseMixBlock(uint8_t *dst) { + uint32_t phase; + uint32_t step; + uint32_t lfsr; + uint32_t hiMask; + uint16_t i; + int amp; + int swing; + int level; + int value; + + amp = gNoiseAmp; + hiMask = gNoiseHiMask; + step = gNoiseStep; + phase = gNoisePhase; + lfsr = gNoiseLfsr; + swing = amp + ((lfsr & hiMask) == hiMask); + level = (lfsr & 1u) != 0u ? swing : -swing; + for (i = 0; i < MIX_BLOCK_FRAMES; i++) { + phase += step; + if (phase >= NOISE_PHASE_ONE) { + phase -= NOISE_PHASE_ONE; + lfsr = (lfsr >> 1) | (((lfsr ^ (lfsr >> NOISE_LFSR_TAP_SHIFT)) & 1u) << NOISE_LFSR_TOP_SHIFT); + swing = amp + ((lfsr & hiMask) == hiMask); + level = (lfsr & 1u) != 0u ? swing : -swing; + } + value = (int)*dst + level; + *dst++ = (uint8_t)(value < 0 ? 0 : (value > UINT8_MAX ? UINT8_MAX : value)); + if (gChannels == MIX_CHANNELS_MAX) { + value = (int)*dst + level; + *dst++ = (uint8_t)(value < 0 ? 0 : (value > UINT8_MAX ? UINT8_MAX : value)); + } + } + gNoisePhase = phase; + gNoiseLfsr = lfsr; } // Slots first .. first + count - 1 mixed over frames frames at dst: in -// stereo each on its pan's side (audioSfxOverlayMixStereo). +// stereo each on its pan's side (jlAudioSfxOverlayMixStereo). static void sfxOverlay(uint8_t *dst, uint16_t frames, uint8_t first, uint8_t count) { if (gChannels == 1u) { - audioSfxOverlayMix(dst, frames, &gSfxSlots[first], count); + jlAudioSfxOverlayMix(dst, frames, &gSfxSlots[first], count); return; } - audioSfxOverlayMixStereo(dst, frames, &gSfxSlots[first], count, &gSfxPan[first], AUDIO_SFX_DST_UNSIGNED); + jlAudioSfxOverlayMixStereo(dst, frames, &gSfxSlots[first], count, &gSfxPan[first], AUDIO_SFX_DST_UNSIGNED); } @@ -763,11 +849,12 @@ bool jlpAudioInit(void) { // receive, so the module player and the SFX resampler agree with the // hardware instead of using the nominal MIX_RATE. gChannels = gDspMajor >= DSP_MAJOR_SB16 ? MIX_CHANNELS_MAX : 1u; - gHalfBytes = (uint16_t)(MIX_HALF_FRAMES * gChannels); + gHalfBytes = (uint16_t)(MIX_HALF_FRAMES * gChannels); + gBlockBytes = (uint16_t)(MIX_BLOCK_FRAMES * gChannels); gEffRate = gChannels == MIX_CHANNELS_MAX ? (uint16_t)MIX_RATE : (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE))); memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan)); - modPlayerSetU8Shift(MIX_UNIT_SHIFT); - audioSfxSetShift(MIX_UNIT_SHIFT); + jlModPlayerSetU8Shift(MIX_UNIT_SHIFT); + jlAudioSfxSetShift(MIX_UNIT_SHIFT); if (!allocDmaBuffer()) { return false; @@ -785,18 +872,30 @@ bool jlpAudioInit(void) { return false; } - // Lock ISR code + the data sbIsr touches at IRQ time. The mix runs on - // the main thread, so refillHalf and the module player need no locking. + // Lock ISR code + the data sbIsr touches at IRQ time: the noise is mixed + // there. The rest of the mix runs on the main thread, so refillHalf and + // the module player need no locking. _go32_dpmi_lock_code(sbIsr, ISR_LOCK_SIZE); + _go32_dpmi_lock_code(noiseMixBlock, ISR_LOCK_SIZE); + _go32_dpmi_lock_data((void *)&gNextBlock, sizeof(gNextBlock)); _go32_dpmi_lock_data((void *)&gNextHalf, sizeof(gNextHalf)); _go32_dpmi_lock_data((void *)gNeedRefill, sizeof(gNeedRefill)); _go32_dpmi_lock_data((void *)&gSbBase, sizeof(gSbBase)); _go32_dpmi_lock_data((void *)&gSbIrq, sizeof(gSbIrq)); + _go32_dpmi_lock_data((void *)&gDmaBuf, sizeof(gDmaBuf)); + _go32_dpmi_lock_data((void *)&gBlockBytes, sizeof(gBlockBytes)); + _go32_dpmi_lock_data((void *)&gChannels, sizeof(gChannels)); + _go32_dpmi_lock_data((void *)&gNoiseStep, sizeof(gNoiseStep)); + _go32_dpmi_lock_data((void *)&gNoiseAmp, sizeof(gNoiseAmp)); + _go32_dpmi_lock_data((void *)&gNoiseHiMask, sizeof(gNoiseHiMask)); + _go32_dpmi_lock_data((void *)&gNoisePhase, sizeof(gNoisePhase)); + _go32_dpmi_lock_data((void *)&gNoiseLfsr, sizeof(gNoiseLfsr)); _go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec); gIrqHooked = true; - gNextHalf = 0; + gNextBlock = 0; + gNextHalf = 0; programDmaController(); programDspAutoInit(); gSbIrqWasMasked = picIrqMasked(gSbIrq); @@ -822,6 +921,7 @@ void jlpAudioShutdown(void) { // The timer, the speaker, and the OPL2 work without the Sound Blaster, // so they are let go whether or not it came up. + gNoiseAmp = 0u; tickUnhook(); speakerSet(0u); adlibShutdown(); @@ -838,7 +938,7 @@ void jlpAudioShutdown(void) { _go32_dpmi_free_iret_wrapper(&gNewIrqVec); gIrqHooked = false; } - modPlayerStop(); + jlModPlayerStop(); gModTail = false; freeDmaBuffer(); (void)dspReset(); @@ -848,7 +948,7 @@ void jlpAudioShutdown(void) { bool jlpAudioIsPlayingMod(void) { - return modPlayerActive() || gModTail; + return jlModPlayerActive() || gModTail; } @@ -857,11 +957,11 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { return; } // A song still playing goes now, not over the new one's start. - if (modPlayerActive()) { + if (jlModPlayerActive()) { modRemoveAhead(); } gModTail = false; - if (modPlayerStart(data, length, gEffRate, loop)) { + if (jlModPlayerStart(data, length, gEffRate, loop)) { modStartNow(); } } @@ -871,7 +971,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 if (!gDspPresent || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gEffRate); + jlAudioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gEffRate); sfxStartNow(slot); } @@ -880,7 +980,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin if (!gDspPresent || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate); + jlAudioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate); sfxStartNow(slot); } @@ -1012,6 +1112,23 @@ static const uint8_t kAdlibSlotMap[ADLIB_VOICE_COUNT * 2] = { // Attenuation 15 (and up) is silence (joey/audio.h). #define ATTEN_SILENT 15u +// The noise's swing (+-) a step of attenuation, 2 dB apart from the mix's +// unit: a full-swing noise carries a full-scale square's power, the square +// swinging a quarter of the byte (MIX_UNIT_SHIFT), so it is as loud as a +// voice: 32 * 10^(-atten / 10). A byte has no 1.27, so a step between two +// swings takes the larger one when the LFSR bits hiMask names are all set (a +// half or a quarter of the time), its power the step's within 0.33 dB. +typedef struct { + uint8_t amp; + uint8_t hiMask; // 0: always amp +} NoiseSwingT; + +static const NoiseSwingT kNoiseSwing[ATTEN_SILENT + 1u] = { + { 32, 0x0 }, { 25, 0x2 }, { 20, 0x6 }, { 16, 0x0 }, { 12, 0x2 }, { 10, 0x0 }, { 8, 0x0 }, { 6, 0x6 }, + { 5, 0x0 }, { 4, 0x0 }, { 3, 0x6 }, { 2, 0x2 }, { 2, 0x0 }, { 1, 0x2 }, { 1, 0x6 }, { 0, 0x0 } +}; +typedef char noiseSwingUnitProof[((128u >> MIX_UNIT_SHIFT) == 32u) ? 1 : -1]; + static bool gAdlibProbed = false; static bool gAdlibPresent = false; static bool gAdlibInited = false; @@ -1234,10 +1351,10 @@ static void adlibNoiseInit(void) { // regardless of our own IRQ rate. // // 2. jlpMillisElapsed must report wall-clock ms even though the -// PIT's sub-tick count now spans gAudioIsrDivisor, not a BIOS tick. +// PIT's sub-tick count now spans jlgAudioIsrDivisor, not a BIOS tick. // We expose an own-ISR-counter-driven ms via the gAudioIsr* // globals below; src/dos/hal.c::jlpMillisElapsed prefers it -// whenever gAudioIsrActive is true, continuing its timeline. +// whenever jlgAudioIsrActive is true, continuing its timeline. // // Literal "chain to old INT $08" doesn't work cleanly under DPMI // because the old PM handler ends in IRET, which corrupts the @@ -1254,12 +1371,12 @@ static void adlibNoiseInit(void) { // Exposed (extern in hal.c) so jlpMillisElapsed can read wall-clock // ms straight from the ISR counter while the PIT runs at its rate. -volatile uint32_t gAudioIsrFires = 0; -volatile uint16_t gAudioIsrDivisor = 0; // PIT clocks per interrupt -volatile bool gAudioIsrActive = false; +volatile uint32_t jlgAudioIsrFires = 0; +volatile uint16_t jlgAudioIsrDivisor = 0; // PIT clocks per interrupt +volatile bool jlgAudioIsrActive = false; +volatile uint32_t jlgAudioBiosTickAccum = 0; static volatile jlAudioTickFnT gAudioTickFn = NULL; -static volatile uint32_t gBiosTickAccum = 0; static volatile uint16_t gTickEvery = 1; // interrupts per callback static volatile uint16_t gTickPhase = 0; static _go32_dpmi_seginfo gOldTimerVec; @@ -1268,7 +1385,7 @@ static bool gTimerHooked = false; static void audioTickIsr(void) { - gAudioIsrFires++; + jlgAudioIsrFires++; if (gAudioTickFn != NULL && ++gTickPhase >= gTickEvery) { gTickPhase = 0; gAudioTickFn(); @@ -1276,9 +1393,9 @@ static void audioTickIsr(void) { // BIOS counter at 18.2 Hz exact via accumulator. Equivalent to // calling the original BIOS handler every PIT_default/N IRQs, // without the IRET-frame headache of an actual chain call. - gBiosTickAccum += gAudioIsrDivisor; - while (gBiosTickAccum >= BIOS_TICK_PERIOD) { - gBiosTickAccum -= BIOS_TICK_PERIOD; + jlgAudioBiosTickAccum += jlgAudioIsrDivisor; + while (jlgAudioBiosTickAccum >= BIOS_TICK_PERIOD) { + jlgAudioBiosTickAccum -= BIOS_TICK_PERIOD; _farpokel(_dos_ds, BIOS_TICK_ADDR, _farpeekl(_dos_ds, BIOS_TICK_ADDR) + 1u); } outportb(PIC1_CMD, PIC_EOI); @@ -1291,9 +1408,9 @@ static void tickUnhook(void) { return; } __asm__ __volatile__("cli" ::: "memory"); - gAudioTickFn = NULL; - gAudioIsrActive = false; - dosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR); // BIOS rate, in the mode the clock reads + gAudioTickFn = NULL; + jlgAudioIsrActive = false; + jlDosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR); // BIOS rate, in the mode the clock reads _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gOldTimerVec); __asm__ __volatile__("sti" ::: "memory"); _go32_dpmi_free_iret_wrapper(&gNewTimerVec); @@ -1304,6 +1421,7 @@ static void tickUnhook(void) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { uint32_t divisor; uint16_t every; + uint16_t count; if (fn == NULL) { tickUnhook(); @@ -1320,13 +1438,13 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { if (!gTimerHooked) { _go32_dpmi_lock_code(audioTickIsr, 4096); - _go32_dpmi_lock_data((void *)&gAudioTickFn, sizeof(gAudioTickFn)); - _go32_dpmi_lock_data((void *)&gAudioIsrFires, sizeof(gAudioIsrFires)); - _go32_dpmi_lock_data((void *)&gAudioIsrActive, sizeof(gAudioIsrActive)); - _go32_dpmi_lock_data((void *)&gAudioIsrDivisor, sizeof(gAudioIsrDivisor)); - _go32_dpmi_lock_data((void *)&gBiosTickAccum, sizeof(gBiosTickAccum)); - _go32_dpmi_lock_data((void *)&gTickEvery, sizeof(gTickEvery)); - _go32_dpmi_lock_data((void *)&gTickPhase, sizeof(gTickPhase)); + _go32_dpmi_lock_data((void *)&gAudioTickFn, sizeof(gAudioTickFn)); + _go32_dpmi_lock_data((void *)&jlgAudioIsrFires, sizeof(jlgAudioIsrFires)); + _go32_dpmi_lock_data((void *)&jlgAudioIsrActive, sizeof(jlgAudioIsrActive)); + _go32_dpmi_lock_data((void *)&jlgAudioIsrDivisor, sizeof(jlgAudioIsrDivisor)); + _go32_dpmi_lock_data((void *)&jlgAudioBiosTickAccum, sizeof(jlgAudioBiosTickAccum)); + _go32_dpmi_lock_data((void *)&gTickEvery, sizeof(gTickEvery)); + _go32_dpmi_lock_data((void *)&gTickPhase, sizeof(gTickPhase)); _go32_dpmi_get_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gOldTimerVec); gNewTimerVec.pm_offset = (unsigned long)audioTickIsr; @@ -1338,15 +1456,23 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { } __asm__ __volatile__("cli" ::: "memory"); - gAudioTickFn = fn; - gAudioIsrDivisor = (uint16_t)divisor; - gTickEvery = every; - gTickPhase = 0; - gBiosTickAccum = 0; - gAudioIsrFires = 0; - gAudioIsrActive = true; + // From the BIOS's rate, the ISR's clocks toward the next tick start + // where counter 0 is inside the current one, so the BIOS count and the + // PIT clock (hal.c pitClocksNow) go on without a step. + if (!jlgAudioIsrActive) { + outportb(PIT_CTRL_PORT, PIT_CTRL_T0_LATCH); + count = inportb(PIT_TIMER0_PORT); + count |= (uint16_t)((uint16_t)inportb(PIT_TIMER0_PORT) << 8); + jlgAudioBiosTickAccum = (uint16_t)(0u - count); + } + gAudioTickFn = fn; + jlgAudioIsrDivisor = (uint16_t)divisor; + gTickEvery = every; + gTickPhase = 0; + jlgAudioIsrFires = 0; + jlgAudioIsrActive = true; _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gNewTimerVec); - dosPitProgram(PIT_CTRL_T0_MODE2, (uint16_t)divisor); + jlDosPitProgram(PIT_CTRL_T0_MODE2, (uint16_t)divisor); __asm__ __volatile__("sti" ::: "memory"); return true; } @@ -1368,10 +1494,24 @@ void jlpAudioCriticalExit(void) { void jlpAudioNoise(uint8_t pitch, uint8_t atten) { + uint32_t hz; uint16_t freqHz; uint16_t fnum; uint8_t block; + // With a Sound Blaster, the noise every port plays: its rate and swing + // to the SB IRQ's generator (noiseMixBlock), the step first. + if (gDspPresent) { + if (atten >= ATTEN_SILENT) { + gNoiseAmp = 0u; + return; + } + hz = audioNoiseHz(pitch); + gNoiseStep = hz >= gEffRate ? (uint32_t)NOISE_PHASE_ONE : (uint32_t)((hz << NOISE_PHASE_SHIFT) / gEffRate); + gNoiseHiMask = kNoiseSwing[atten].hiMask != 0u ? kNoiseSwing[atten].hiMask : NOISE_HI_NEVER; + gNoiseAmp = kNoiseSwing[atten].amp; + return; + } if (!adlibReady()) { return; } @@ -1394,11 +1534,11 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { // ADLIB_NOISE_MIN_HZ. (Core clamps pitch to <= 31 before the HAL // sees it.) // - // The rate every other port plays (AUDIO_NOISE_CLOCK_HZ / pitch) cannot - // be matched: the OPL2's snare is a tuned drum voice rather than a - // noise source, and its band tops out near 6 kHz where that rate runs - // to 125 kHz. An even spread keeps every pitch distinct and in order, - // bright to dark. + // Only without a Sound Blaster: the rate every other port plays + // (AUDIO_NOISE_CLOCK_HZ / pitch) cannot be matched here, as the OPL2's + // snare is a tuned drum voice rather than a noise source, and its band + // tops out near 6 kHz where that rate runs to 125 kHz. An even spread + // keeps every pitch distinct and in order, bright to dark. // // It replaces MAX / (pitch + 1), a hyperbola that spent nearly all // its resolution on the first few pitches and squashed everything @@ -1460,7 +1600,7 @@ void jlpAudioStopMod(void) { if (!gDspPresent) { return; } - modPlayerStop(); + jlModPlayerStop(); modRemoveAhead(); gModTail = false; } diff --git a/src/dos/dosTimer.h b/src/dos/dosTimer.h index 82adb49..ab9b8ec 100644 --- a/src/dos/dosTimer.h +++ b/src/dos/dosTimer.h @@ -14,6 +14,7 @@ #ifndef JOEYLIB_DOS_TIMER_H #define JOEYLIB_DOS_TIMER_H +#include <stdbool.h> #include <stdint.h> #include "dosPic.h" @@ -33,6 +34,15 @@ // Program counter 0 with a PIT_CTRL_T0_* mode and a divisor (0 = 65536). // The caller keeps interrupts off around it. src/dos/hal.c. -void dosPitProgram(uint8_t ctrl, uint16_t divisor); +void jlDosPitProgram(uint8_t ctrl, uint16_t divisor); + +// While audio.c's ISR drives counter 0 at its own rate (jlgAudioIsrActive), +// each fire is jlgAudioIsrDivisor PIT clocks: jlgAudioIsrFires counts them, +// and jlgAudioBiosTickAccum holds the clocks since the last BIOS tick the +// ISR made (it adds a tick every 65536). src/dos/audio.c. +extern volatile uint32_t jlgAudioIsrFires; +extern volatile uint16_t jlgAudioIsrDivisor; +extern volatile bool jlgAudioIsrActive; +extern volatile uint32_t jlgAudioBiosTickAccum; #endif diff --git a/src/dos/hal.c b/src/dos/hal.c index a19b76e..4ccce36 100644 --- a/src/dos/hal.c +++ b/src/dos/hal.c @@ -67,6 +67,12 @@ #define VGA_CRTC_BYTE_MODE 0xE3u // mode control: byte addressing // A frame on the PIT's clock: mode 13h refreshes at 70.086 Hz. #define VGA_FRAME_PIT_CLOCKS ((uint64_t)PIT_CLOCK_HZ * 1000u / 70086u) +// flipLatched's reckoning from the last retrace seen: trusted for this many +// frames after it, and only this far either side of a retrace it predicts +// (1 ms: the time a seen retrace starts late by, plus four frames of a +// refresh 0.5% off 70.086 Hz). +#define RETRACE_TRUST_FRAMES 4u +#define RETRACE_MARGIN_CLOCKS ((uint64_t)PIT_CLOCK_HZ / 1000u) // A stage byte holds two 4-bit pixels: 256 values. #define STAGE_BYTE_VALUES 256u #define PIXEL_BITS 4u @@ -78,9 +84,10 @@ // ----- Prototypes ----- static void crashHandler(int sig); -void dosPitProgram(uint8_t ctrl, uint16_t divisor); +void jlDosPitProgram(uint8_t ctrl, uint16_t divisor); static void expandGroupPlanes(const uint32_t *lut, const uint8_t *srcBytes, uint8_t *plane0, uint16_t g); static void expandRowPlanes(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *plane0); +static bool flipLatched(void); static void flipWait(void); static void installCrashLog(void); static uint64_t pitClocksNow(void); @@ -97,7 +104,7 @@ static void uploadPalette(const jlSurfaceT *src); static uint8_t *gVgaMem = NULL; // The SCB each row of each page was last expanded with (presentRows); the // pages start cleared, every row with palette 0. A page whose rows may not -// have the stage's SCB (gStageScbDirty since it was drawn) is stale, and its +// have the stage's SCB (jlgStageScbDirty since it was drawn) is stale, and its // next present compares them. static uint8_t gPresentedScb[2][JL_SURFACE_HEIGHT]; static bool gScbStale[2] = { true, true }; @@ -106,6 +113,9 @@ static bool gScbStale[2] = { true, true }; static uint8_t gFrontPage = 0u; static bool gFlipPending = false; static uint64_t gFlipClocks = 0u; +// The last retrace start retraceWait saw, on the PIT's clock. +static bool gRetraceSeen = false; +static uint64_t gRetraceClocks = 0u; // A present that waited for a retrace to load its colours, at // gBankedClocks: the next jlpWaitVBL within a frame of it has had its // retrace, so a loop that waits and then presents keeps the refresh rate. @@ -153,7 +163,7 @@ static uint32_t gMillisLastIsr = 0u; // audio ISR fires at the last read // the poll-rate-dependent counter itself -- see jlpFrameCount.) // VGA DAC programming is ~768 outportb calls; skipped when the core -// gStagePaletteDirty flag (surfaceInternal.h) says the stage palette +// jlgStagePaletteDirty flag (surfaceInternal.h) says the stage palette // is unchanged -- see jlpPresent. // ----- Internal helpers (alphabetical) ----- @@ -230,15 +240,13 @@ static void crashHandler(int sig) { // Program PIT counter 0 (dosTimer.h). Shared with src/dos/audio.c. -void dosPitProgram(uint8_t ctrl, uint16_t divisor) { +void jlDosPitProgram(uint8_t ctrl, uint16_t divisor) { outportb(PIT_CTRL_PORT, ctrl); outportb(PIT_TIMER0_PORT, (uint8_t)(divisor & 0xFFu)); outportb(PIT_TIMER0_PORT, (uint8_t)((divisor >> 8) & 0xFFu)); } -// A flip written less than a frame ago may not have latched: the page it -// left could still be on screen, so wait for the retrace that latches it -// before drawing there. A frame or more since, a retrace has come. + // One 4-pixel group (stage bytes srcBytes[0..1]) into byte g of the four // planes: plane p is p * VGA_PAGE_BYTES past plane0. static void expandGroupPlanes(const uint32_t *lut, const uint8_t *srcBytes, uint8_t *plane0, uint16_t g) { @@ -300,8 +308,40 @@ static void expandRowPlanes(const jlSurfaceT *src, int16_t y, int16_t x, uint16_ } +// Whether the last flip has latched. A frame or more since it, a retrace +// has come. Sooner, the retraces come every VGA_FRAME_PIT_CLOCKS from the +// last one seen, so the first after the flip is known: it has latched once +// that retrace is a margin behind (a flip a margin before a retrace counts +// on the one after). Presents that come faster than jlWaitVBL would pace +// them then wait only for a retrace that has not come, not for the next. +static bool flipLatched(void) { + uint64_t now; + uint64_t sinceSeen; + uint64_t next; + + now = pitClocksNow(); + if (now - gFlipClocks >= VGA_FRAME_PIT_CLOCKS) { + return true; + } + if (!gRetraceSeen) { + return false; + } + if (gRetraceClocks > gFlipClocks) { + return true; + } + sinceSeen = gFlipClocks + RETRACE_MARGIN_CLOCKS - gRetraceClocks; + if (sinceSeen >= RETRACE_TRUST_FRAMES * VGA_FRAME_PIT_CLOCKS) { + return false; + } + next = gRetraceClocks + (sinceSeen / VGA_FRAME_PIT_CLOCKS + 1u) * VGA_FRAME_PIT_CLOCKS; + return now >= next + RETRACE_MARGIN_CLOCKS; +} + + +// A flip that may not have latched leaves the page it left possibly still +// on screen, so wait for the retrace that latches it before drawing there. static void flipWait(void) { - if (gFlipPending && pitClocksNow() - gFlipClocks < VGA_FRAME_PIT_CLOCKS) { + if (gFlipPending && !flipLatched()) { retraceWait(); } gFlipPending = false; @@ -333,23 +373,29 @@ static void installCrashLog(void) { } -// `src` is always gStage (jlpPresent's contract), so the core palette +// `src` is always jlgStage (jlpPresent's contract), so the core palette // dirty flag is the change signal, replacing a 512-byte cached-mirror // memcmp per present (NATIVE-PERF Phase 2 R9). The SCB flag is not // consumed here: mode 13h has one DAC; the per-line SCB choice is // applied in the pixel expansion, which reads src->scb directly. // PIT clocks (1.193182 MHz) since the BIOS's midnight, plus the midnights // seen: BIOS ticks * 65536 and the position inside the current tick from -// counter 0 (mode 2, dosTimer.h). All three are read with interrupts off, -// and a tick IRQ0 has already raised but the BIOS has not yet counted is -// added by hand. DJGPP's uclock() missed that case, so its time stepped -// back a tick and forward again; a frame difference taken across the step -// came out huge or short depending only on when the call landed, and UBER -// rows measured on DOS ended early by a code-layout lottery. +// counter 0 (mode 2, dosTimer.h). While the audio ISR runs counter 0 at its +// own rate, a period of the count is one fire, not a tick, so the position +// inside the tick is the clocks the ISR has added since its last tick plus +// the position inside the fire. Everything is read with interrupts off, and +// an IRQ0 the counter has already raised but its handler has not yet +// counted is added by hand. DJGPP's uclock() missed that case, so its time +// stepped back a tick and forward again; a frame difference taken across +// the step came out huge or short depending only on when the call landed, +// and UBER rows measured on DOS ended early by a code-layout lottery. static uint64_t pitClocksNow(void) { + uint64_t clocks; uint32_t ticks; + uint32_t period; + uint32_t intoTick; uint16_t count; - uint16_t intoTick; + uint16_t intoPeriod; uint8_t irr; int wasEnabled; @@ -360,23 +406,31 @@ static uint64_t pitClocksNow(void) { ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR); outportb(PIC1_CMD, PIC_OCW3_READ_IRR); irr = inportb(PIC1_CMD); + if (jlgAudioIsrActive) { + period = jlgAudioIsrDivisor; + intoTick = jlgAudioBiosTickAccum; + } else { + period = BIOS_TICK_PERIOD; + intoTick = 0u; + } if (wasEnabled) { enable(); } - // Mode 2 counts 65536 (read as 0), 65535 .. 1, so this is the PIT - // clocks since the tick began. - intoTick = (uint16_t)(0u - count); - // IRQ0 pending with the count early in a tick: the wrap came before - // the latch and the BIOS has not counted it. (Pending with the count - // late in a tick means it wrapped after the latch: nothing to add.) - if ((irr & PIC_IRQ0_BIT) != 0u && intoTick < (uint16_t)(BIOS_TICK_PERIOD / 2u)) { - ticks++; - } + // Mode 2 counts the period (65536 read as 0) down to 1, so this is the + // PIT clocks since the period began. + intoPeriod = (uint16_t)((uint16_t)period - count); if (ticks < gPitLastTicks) { gPitDayClocks += (uint64_t)BIOS_TICKS_PER_DAY * BIOS_TICK_PERIOD; } gPitLastTicks = ticks; - return gPitDayClocks + (uint64_t)ticks * BIOS_TICK_PERIOD + intoTick; + clocks = gPitDayClocks + (uint64_t)ticks * BIOS_TICK_PERIOD + intoTick + intoPeriod; + // IRQ0 pending with the count early in a period: the wrap came before + // the latch and its handler has not counted it. (Pending with the count + // late in a period means it wrapped after the latch: nothing to add.) + if ((irr & PIC_IRQ0_BIT) != 0u && intoPeriod < (uint16_t)(period / 2u)) { + clocks += period; + } + return clocks; } @@ -386,7 +440,7 @@ static void pitClockStart(void) { int wasEnabled; wasEnabled = disable(); - dosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR); + jlDosPitProgram(PIT_CTRL_T0_MODE2, PIT_BIOS_DIVISOR); if (wasEnabled) { enable(); } @@ -416,7 +470,7 @@ static void planeLutBuild(void) { static bool presentIdle(uint8_t page) { uint8_t g; - if (gPrevAnyDirty || gStagePaletteDirty || gScbStale[page]) { + if (gPrevAnyDirty || jlgStagePaletteDirty || gScbStale[page]) { return false; } for (g = 0u; g < (uint8_t)(JL_SURFACE_HEIGHT >> STAGE_DIRTY_GROUP4_SHIFT); g++) { @@ -444,10 +498,10 @@ static uint16_t presentRows(const jlSurfaceT *src, uint8_t page) { anyCur = false; for (y = 0; y < JL_SURFACE_HEIGHT; y++) { anyCur = anyCur || !STAGE_DIRTY_ROW_CLEAN(y); - minWord = gStageMinWord[y] < gPrevMinWord[y] ? gStageMinWord[y] : gPrevMinWord[y]; - maxWord = gStageMaxWord[y] > gPrevMaxWord[y] ? gStageMaxWord[y] : gPrevMaxWord[y]; - gPrevMinWord[y] = gStageMinWord[y]; - gPrevMaxWord[y] = gStageMaxWord[y]; + minWord = jlgStageMinWord[y] < gPrevMinWord[y] ? jlgStageMinWord[y] : gPrevMinWord[y]; + maxWord = jlgStageMaxWord[y] > gPrevMaxWord[y] ? jlgStageMaxWord[y] : gPrevMaxWord[y]; + gPrevMinWord[y] = jlgStageMinWord[y]; + gPrevMaxWord[y] = jlgStageMaxWord[y]; if (gScbStale[page] && src->scb[y] != gPresentedScb[page][y]) { gPresentedScb[page][y] = src->scb[y]; minWord = 0u; @@ -481,6 +535,8 @@ static void retraceWait(void) { while (!(inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT)) { /* spin until next retrace begins */; } + gRetraceClocks = pitClocksNow(); + gRetraceSeen = true; } @@ -509,7 +565,7 @@ bool jlpInit(const jlConfigT *config) { // First, so a refusal leaves the machine as it was: nothing below has // run yet to be put back. if (!__djgpp_nearptr_enable()) { - coreSetError("DOS: the DPMI host refused near pointers (the screen needs them)"); + jlCoreSetError("DOS: the DPMI host refused near pointers (the screen needs them)"); return false; } gNearEnabled = true; @@ -572,12 +628,12 @@ void jlpPresent(const jlSurfaceT *src) { } flipWait(); back = (uint8_t)(gFrontPage ^ 1u); - if (gStageScbDirty) { - gScbStale[0] = true; - gScbStale[1] = true; - gStageScbDirty = false; + if (jlgStageScbDirty) { + gScbStale[0] = true; + gScbStale[1] = true; + jlgStageScbDirty = false; } - gPresentScannedRows += JL_SURFACE_HEIGHT; + jlgPresentScannedRows += JL_SURFACE_HEIGHT; if (presentIdle(back)) { return; } @@ -585,7 +641,7 @@ void jlpPresent(const jlSurfaceT *src) { if (rows != 0u) { for (r = 0u; r < rows; r++) { expandRowPlanes(src, gRowY[r], gRowX[r], gRowW[r], &gPlaneBuf[0][(uint32_t)gRowY[r] * VGA_PLANE_ROW_BYTES]); - gPresentCopiedBytes += gRowW[r]; + jlgPresentCopiedBytes += gRowW[r]; } page = &gVgaMem[(uint32_t)back * VGA_PAGE_BYTES]; for (plane = 0u; plane < VGA_PLANES; plane++) { @@ -600,13 +656,13 @@ void jlpPresent(const jlSurfaceT *src) { gFlipPending = true; gFlipClocks = pitClocksNow(); } - if (gStagePaletteDirty) { + if (jlgStagePaletteDirty) { retraceWait(); uploadPalette(src); - gStagePaletteDirty = false; - gFlipPending = false; - gVblBanked = true; - gBankedClocks = pitClocksNow(); + jlgStagePaletteDirty = false; + gFlipPending = false; + gVblBanked = true; + gBankedClocks = pitClocksNow(); } } @@ -620,7 +676,7 @@ void jlpPresent(const jlSurfaceT *src) { // golden hashes gate the stage, not the screen, so this is DOS's only // display-side gate. Not probe-guarded (tiny, mirrors the other // ports' always-compiled probe helpers). -uint32_t dosProbeVgaMismatch(const jlSurfaceT *src) { +uint32_t jlDosProbeVgaMismatch(const jlSurfaceT *src) { uint32_t bad = 0; uint16_t x; uint16_t y; @@ -702,20 +758,16 @@ uint16_t jlpFrameHz(void) { // rate or restarts its counter -- the new source is rebased onto the last // value returned, so the clock never jumps back. // -// gAudioIsr* are owned by src/dos/audio.c. -extern volatile uint32_t gAudioIsrFires; -extern volatile uint16_t gAudioIsrDivisor; -extern volatile bool gAudioIsrActive; - +// jlgAudioIsr* are owned by src/dos/audio.c (dosTimer.h). uint32_t jlpMillisElapsed(void) { uint32_t ms; uint32_t fires; uint16_t divisor; - divisor = gAudioIsrActive ? gAudioIsrDivisor : 0u; + divisor = jlgAudioIsrActive ? jlgAudioIsrDivisor : 0u; if (divisor != 0u) { // uint64: fires * divisor * 1000 passes 32 bits within seconds. - fires = gAudioIsrFires; + fires = jlgAudioIsrFires; ms = (uint32_t)(((uint64_t)fires * divisor * 1000uL) / PIT_CLOCK_HZ); } else { // Pre-scale by 1000 before dividing by the PIT rate: dividing @@ -739,7 +791,7 @@ void jlpShutdown(void) { // Counter 0 back to the BIOS's own mode 3 at its own rate. wasEnabled = disable(); - dosPitProgram(PIT_CTRL_T0_MODE3, PIT_BIOS_DIVISOR); + jlDosPitProgram(PIT_CTRL_T0_MODE3, PIT_BIOS_DIVISOR); if (wasEnabled) { enable(); } diff --git a/src/dos/input.c b/src/dos/input.c index 4aa33f6..554f67e 100644 --- a/src/dos/input.c +++ b/src/dos/input.c @@ -7,7 +7,7 @@ // Keyboard: hooks INT 9 to capture AT set-1 scan codes from port 0x60. // The ISR reads the scan code, maps it to a jlKeyE, updates a private // gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots -// gIsrState into gKeyState with interrupts disabled. +// gIsrState into jlgKeyState with interrupts disabled. // // Typed characters: because the hook replaces the BIOS INT 9 handler // outright (no chaining, no EOI from the BIOS), INT 16h never sees our @@ -21,12 +21,12 @@ // layouts are not applied (they hook the BIOS handler we replace). // // The two-buffer split is required for jlKeyPressed edge detection. -// jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* jlpInputPoll -// runs, so whatever gKeyState holds at that moment becomes gKeyPrev. -// If the ISR wrote directly to gKeyState, press/release events that +// jlInputPoll does memcpy(jlgKeyPrev, jlgKeyState) *before* jlpInputPoll +// runs, so whatever jlgKeyState holds at that moment becomes jlgKeyPrev. +// If the ISR wrote directly to jlgKeyState, press/release events that // happened between polls would already have landed there -- the memcpy -// would copy the new value into gKeyPrev and edge detection would see -// gKeyState == gKeyPrev every frame. +// would copy the new value into jlgKeyPrev and edge detection would see +// jlgKeyState == jlgKeyPrev every frame. // // Because DJGPP runs in protected mode with paging, the ISR code and // any data it touches must be locked or a page fault at interrupt time @@ -34,10 +34,16 @@ // // Mouse: uses INT 33h, the standard MS DOS mouse driver interface. // jlpInputInit calls function 0 (reset/detect), then 7/8 to clamp the -// driver's coordinate range to the surface dimensions. jlpInputPoll -// calls function 3 once per frame to read absolute X/Y and the button -// bitmask. INT 33h is provided by every common DOS mouse driver and by -// DOSBox / DOSBox-X. +// driver's coordinate range to the surface dimensions (X in the 640-wide +// virtual screen a driver keeps in mode 13h, halved when read), 0Fh to keep +// the pointer's speed, then function 0Ch +// to have the driver call mouseCallback (a DPMI real-mode callback) with +// absolute X/Y and the button bitmask on every move and button change. +// jlpInputPoll reads what it left: a poll's own INT 33h, a real-mode +// round trip through DPMI, was ~90% of the poll (~280 us on the 386SX-16 +// reference). Without a callback, the poll calls function 3 itself. +// INT 33h is provided by every common DOS mouse driver and by DOSBox / +// DOSBox-X. #include <dos.h> #include <dpmi.h> @@ -110,6 +116,20 @@ #define MOUSE_FN_SET_X_RANGE 0x0007 #define MOUSE_FN_SET_Y_RANGE 0x0008 #define MOUSE_FN_SET_POS 0x0004 +#define MOUSE_FN_SET_HANDLER 0x000C +#define MOUSE_FN_SET_RATIO 0x000F +#define MOUSE_EVENTS_ALL 0x007F // a move, and every button down and up +#define MOUSE_EVENTS_NONE 0x0000 +#define MOUSE_CB_LOCK_SIZE 256 // mouseCallback's code, for the lock +// In mode 13h a mouse driver keeps X in a 640-wide virtual screen, two units +// a pixel, and moves it in steps of 2: the stage's 320 columns are asked +// for as 0..639 and halved (every column reachable; asked as 0..319, X only +// reached the even ones, and 318 at most). The horizontal mickey ratio is +// doubled from the default's 8 so the pointer keeps its speed; Y is left at +// the default's 16. +#define MOUSE_X_SHIFT 1 +#define MOUSE_MICKEYS_X 4 +#define MOUSE_MICKEYS_Y 16 #define MOUSE_BTN_LEFT 0x0001 #define MOUSE_BTN_RIGHT 0x0002 #define MOUSE_BTN_MIDDLE 0x0004 @@ -127,6 +147,7 @@ #define BIOS_GAMEPORT_DX_SWITCHES 0x0000 #define BIOS_GAMEPORT_DX_POS 0x0001 #define BIOS_FLAGS_CARRY 0x0001 +#define GAMEPORT_IO 0x0201 // its switches in bits 4-7: the byte AH=84h DX=0 returns #define BIOS_BTN_A1 0x10 #define BIOS_BTN_A2 0x20 #define BIOS_BTN_B1 0x40 @@ -145,6 +166,9 @@ static void joystickClear(jlJoystickE js); static bool joystickInit(void); static void joystickPoll(void); static void keyboardIsr(void); +static void mouseCallback(_go32_dpmi_registers *r); +static void mouseCallbackInstall(void); +static void mouseCallbackRemove(void); static bool mouseInit(void); static void mousePoll(void); static int8_t scaleAxis(uint16_t v, uint16_t rest); @@ -207,12 +231,17 @@ static const uint8_t gKeypadAscii[SCAN_KEYPAD_LAST - SCAN_KEYPAD_FIRST + 1] = { static _go32_dpmi_seginfo gOldHandler; static _go32_dpmi_seginfo gNewHandler; static bool gHooked = false; -// uint8_t (not bool) so element size matches gKeyState's. See +// uint8_t (not bool) so element size matches jlgKeyState's. See // src/core/inputInternal.h for the full rationale. static volatile uint8_t gIsrState[JL_KEY_COUNT]; // Keys pressed since the last poll, so a tap between two polls still reads -// down for one (inputLatch). +// down for one (jlInputLatch). static volatile uint8_t gIsrTapped[JL_KEY_COUNT]; +// A key went down or up since the last poll latched the keys; and that latch +// showed a tap that must now go up. A poll latches only on either, so an idle +// keyboard costs no pass over the keys. +static volatile uint8_t gIsrKeysChanged = 0; +static bool gKeyTapShown = false; // ISR-tracked lock/prefix state. uint8_t (not bool) so the locked-data // regions have known one-byte elements. @@ -233,6 +262,18 @@ static volatile uint8_t gIsrCharTail = 0; static bool gMousePresent = false; static bool gJoystickPresent = false; +// The mouse driver's event handler (mouseCallbackInstall): the DPMI +// real-mode callback into mouseCallback, the registers it hands it, and +// the driver's last report, kept in the callback. Locked: the driver calls +// it from its interrupt. +static _go32_dpmi_seginfo gMouseCb; +static _go32_dpmi_registers gMouseCbRegs; +static bool gMouseCbInstalled = false; +static volatile uint16_t gIsrMouseX = 0; +static volatile uint16_t gIsrMouseY = 0; +static volatile uint16_t gIsrMouseButtons = 0; +static volatile uint16_t gIsrMouseReports = 0; + // Each stick's resting position, and whether it has been taken since init // or the stick's jlJoystickReset. static uint16_t gJoyRestX [JL_JOYSTICK_COUNT]; @@ -293,6 +334,7 @@ static void keyboardIsr(void) { if (!isBreak) { gIsrTapped[key] = 1u; } + gIsrKeysChanged = 1u; } if (!isBreak) { @@ -352,10 +394,73 @@ static void keyboardIsr(void) { } +// The mouse driver's event handler, called from its interrupt through the +// DPMI callback: keep its report (CX, DX the position, BX the buttons) for +// jlpInputPoll. +static void mouseCallback(_go32_dpmi_registers *r) { + gIsrMouseX = r->x.cx; + gIsrMouseY = r->x.dx; + gIsrMouseButtons = r->x.bx; + gIsrMouseReports = (uint16_t)(gIsrMouseReports + 1u); +} + + +// Have the driver call mouseCallback on every move and button change +// (function 0Ch), then take where the pointer is now (function 3) with +// interrupts held, so no report can land between the read and the store. +// Without a callback (DPMI has none free) the poll calls function 3. +static void mouseCallbackInstall(void) { + __dpmi_regs r; + + _go32_dpmi_lock_code(mouseCallback, MOUSE_CB_LOCK_SIZE); + _go32_dpmi_lock_data((void *)&gMouseCbRegs, sizeof(gMouseCbRegs)); + _go32_dpmi_lock_data((void *)&gIsrMouseX, sizeof(gIsrMouseX)); + _go32_dpmi_lock_data((void *)&gIsrMouseY, sizeof(gIsrMouseY)); + _go32_dpmi_lock_data((void *)&gIsrMouseButtons, sizeof(gIsrMouseButtons)); + _go32_dpmi_lock_data((void *)&gIsrMouseReports, sizeof(gIsrMouseReports)); + gMouseCb.pm_offset = (unsigned long)mouseCallback; + if (_go32_dpmi_allocate_real_mode_callback_retf(&gMouseCb, &gMouseCbRegs) != 0) { + return; + } + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_SET_HANDLER; + r.x.cx = MOUSE_EVENTS_ALL; + r.x.es = gMouseCb.rm_segment; + r.x.dx = gMouseCb.rm_offset; + __dpmi_int(MOUSE_INT, &r); + disable(); + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_GET_STATE; + __dpmi_int(MOUSE_INT, &r); + gIsrMouseX = r.x.cx; + gIsrMouseY = r.x.dx; + gIsrMouseButtons = r.x.bx; + enable(); + gMouseCbInstalled = true; +} + + +// Stop the driver's calls (function 0Ch, no events) before freeing the +// callback they would land in. +static void mouseCallbackRemove(void) { + __dpmi_regs r; + + if (!gMouseCbInstalled) { + return; + } + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_SET_HANDLER; + r.x.cx = MOUSE_EVENTS_NONE; + __dpmi_int(MOUSE_INT, &r); + _go32_dpmi_free_real_mode_callback(&gMouseCb); + gMouseCbInstalled = false; +} + + // Reset the mouse driver, detect presence, clamp coordinate range to -// the surface, and center the pointer. AX=0xFFFF after reset means a -// driver is loaded; anything else means no mouse and we leave gMouse* -// at their zero defaults. +// the surface, center the pointer, and install the event handler. +// AX=0xFFFF after reset means a driver is loaded; anything else means no +// mouse and we leave gMouse* at their zero defaults. static bool mouseInit(void) { __dpmi_regs r; @@ -369,7 +474,7 @@ static bool mouseInit(void) { memset(&r, 0, sizeof(r)); r.x.ax = MOUSE_FN_SET_X_RANGE; r.x.cx = 0; - r.x.dx = JL_SURFACE_WIDTH - 1; + r.x.dx = (JL_SURFACE_WIDTH << MOUSE_X_SHIFT) - 1; __dpmi_int(MOUSE_INT, &r); memset(&r, 0, sizeof(r)); @@ -380,10 +485,17 @@ static bool mouseInit(void) { memset(&r, 0, sizeof(r)); r.x.ax = MOUSE_FN_SET_POS; - r.x.cx = JL_SURFACE_WIDTH / 2; + r.x.cx = (JL_SURFACE_WIDTH / 2) << MOUSE_X_SHIFT; r.x.dx = JL_SURFACE_HEIGHT / 2; __dpmi_int(MOUSE_INT, &r); + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_SET_RATIO; + r.x.cx = MOUSE_MICKEYS_X; + r.x.dx = MOUSE_MICKEYS_Y; + __dpmi_int(MOUSE_INT, &r); + + mouseCallbackInstall(); return true; } @@ -426,39 +538,95 @@ static bool joystickInit(void) { // No stick: nothing held, centred, not connected, its rest to be taken // again from the next stick seen. static void joystickClear(jlJoystickE js) { - gJoyConnected[js] = false; - gJoyAxisX[js] = 0; - gJoyAxisY[js] = 0; - gJoyRestValid[js] = false; - memset(gJoyButtonState[js], 0, sizeof(gJoyButtonState[js])); + jlgJoyConnected[js] = false; + jlgJoyAxisX[js] = 0; + jlgJoyAxisY[js] = 0; + gJoyRestValid[js] = false; + memset(jlgJoyButtonState[js], 0, sizeof(jlgJoyButtonState[js])); } -// Both sticks through the BIOS. A stick is there while its axes count: an -// empty port times out at 0. Its rest is its first counted reading, at -// jlInit or whenever it is plugged in, and again after jlJoystickReset. A -// read the BIOS refuses leaves no stick, rather than the last one's state. +// Both sticks' buttons, every poll, from the game port itself (its +// switches, active low: an empty port reads them released). Their axes +// wait for jlpJoystickAxesRead, when one is asked for: each BIOS read is a +// real-mode round trip through DPMI (~280 us on the 386SX-16 reference), +// and on hardware the axes are timed out of their RC circuits besides. static void joystickPoll(void) { - __dpmi_regs r; - uint16_t counts[JL_JOYSTICK_COUNT][JOY_AXES]; - uint8_t switches; - uint8_t js; - uint8_t b; + uint8_t switches; + uint8_t js; + uint8_t b; if (!gJoystickPresent) { return; } + switches = inportb(GAMEPORT_IO); + for (js = 0u; js < JL_JOYSTICK_COUNT; js++) { + for (b = 0u; b < JL_JOY_BUTTON_COUNT; b++) { + jlgJoyButtonState[js][b] = (switches & kJoyButtonBit[js][b]) == 0u; + } + jlgJoyAxesStale[js] = 1u; + } +} + + +// The driver's last report: as mouseCallback kept it, or function 3's. +// The callback's three words are read again if a report lands among them +// (it counts them), so they are one report's, with no cli/sti. +static void mousePoll(void) { + __dpmi_regs r; + uint16_t x; + uint16_t y; + uint16_t btn; + uint16_t reports; + + if (!gMousePresent) { + return; + } + if (gMouseCbInstalled) { + do { + reports = gIsrMouseReports; + x = gIsrMouseX; + y = gIsrMouseY; + btn = gIsrMouseButtons; + } while (reports != gIsrMouseReports); + } else { + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_GET_STATE; + __dpmi_int(MOUSE_INT, &r); + x = r.x.cx; + y = r.x.dx; + btn = r.x.bx; + } + // X in the driver's 640-wide virtual screen (MOUSE_X_SHIFT, above). + jlgMouseX = (int16_t)(x >> MOUSE_X_SHIFT); + jlgMouseY = (int16_t)y; + jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (btn & MOUSE_BTN_LEFT) != 0; + jlgMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = (btn & MOUSE_BTN_RIGHT) != 0; + jlgMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = (btn & MOUSE_BTN_MIDDLE) != 0; +} + + +// ----- HAL API (alphabetical) ----- + +// Probe the game port again and take the stick's next position as rest. +// Both sticks' axes through the BIOS, one read for the two. A stick is +// there while its axes count: an empty port times out at 0. Its rest is +// its first counted reading, when it is first asked for or plugged in, and +// again after jlJoystickReset. A read the BIOS refuses leaves no stick, +// rather than the last one's state. +void jlpJoystickAxesRead(jlJoystickE js) { + __dpmi_regs r; + uint16_t counts[JL_JOYSTICK_COUNT][JOY_AXES]; + uint8_t s; + + (void)js; + for (s = 0u; s < JL_JOYSTICK_COUNT; s++) { + jlgJoyAxesStale[s] = 0u; + } memset(&r, 0, sizeof(r)); r.h.ah = BIOS_FN_GAMEPORT; - r.x.dx = BIOS_GAMEPORT_DX_SWITCHES; + r.x.dx = BIOS_GAMEPORT_DX_POS; __dpmi_int(BIOS_INT, &r); - switches = r.h.al; - if ((r.x.flags & BIOS_FLAGS_CARRY) == 0u) { - memset(&r, 0, sizeof(r)); - r.h.ah = BIOS_FN_GAMEPORT; - r.x.dx = BIOS_GAMEPORT_DX_POS; - __dpmi_int(BIOS_INT, &r); - } if ((r.x.flags & BIOS_FLAGS_CARRY) != 0u) { joystickClear(JL_JOYSTICK_0); joystickClear(JL_JOYSTICK_1); @@ -468,54 +636,23 @@ static void joystickPoll(void) { counts[JL_JOYSTICK_0][1] = r.x.bx; counts[JL_JOYSTICK_1][0] = r.x.cx; counts[JL_JOYSTICK_1][1] = r.x.dx; - for (js = 0u; js < JL_JOYSTICK_COUNT; js++) { - if (counts[js][0] == 0u || counts[js][1] == 0u) { - joystickClear((jlJoystickE)js); + for (s = 0u; s < JL_JOYSTICK_COUNT; s++) { + if (counts[s][0] == 0u || counts[s][1] == 0u) { + joystickClear((jlJoystickE)s); continue; } - if (!gJoyRestValid[js]) { - gJoyRestX[js] = counts[js][0]; - gJoyRestY[js] = counts[js][1]; - gJoyRestValid[js] = true; - } - gJoyConnected[js] = true; - gJoyAxisX[js] = inputDeadZone(scaleAxis(counts[js][0], gJoyRestX[js]), gJoyDeadZone[js]); - gJoyAxisY[js] = inputDeadZone(scaleAxis(counts[js][1], gJoyRestY[js]), gJoyDeadZone[js]); - for (b = 0u; b < JL_JOY_BUTTON_COUNT; b++) { - gJoyButtonState[js][b] = (switches & kJoyButtonBit[js][b]) == 0u; + if (!gJoyRestValid[s]) { + gJoyRestX[s] = counts[s][0]; + gJoyRestY[s] = counts[s][1]; + gJoyRestValid[s] = true; } + jlgJoyConnected[s] = true; + jlgJoyAxisX[s] = jlInputDeadZone(scaleAxis(counts[s][0], gJoyRestX[s]), jlgJoyDeadZone[s]); + jlgJoyAxisY[s] = jlInputDeadZone(scaleAxis(counts[s][1], gJoyRestY[s]), jlgJoyDeadZone[s]); } } -static void mousePoll(void) { - __dpmi_regs r; - uint16_t btn; - - if (!gMousePresent) { - return; - } - memset(&r, 0, sizeof(r)); - r.x.ax = MOUSE_FN_GET_STATE; - __dpmi_int(MOUSE_INT, &r); - // Assumes a mode-13h-aware driver (DOSBox / DOSBox-X) that returns - // native 0..JL_SURFACE_WIDTH-1 X. Some real MS-compatible drivers keep - // an internal 640-wide virtual space in mode 13h where fn 7/8 only - // clamp min/max (not scale), so X comes back doubled and would need - // a >>1; that scale-detection is not done here. Y is unaffected - // (only X is virtualized in 320-wide modes). - gMouseX = (int16_t)r.x.cx; - gMouseY = (int16_t)r.x.dx; - btn = r.x.bx; - gMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (btn & MOUSE_BTN_LEFT) != 0; - gMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = (btn & MOUSE_BTN_RIGHT) != 0; - gMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = (btn & MOUSE_BTN_MIDDLE) != 0; -} - - -// ----- HAL API (alphabetical) ----- - -// Probe the game port again and take the stick's next position as rest. void jlpJoystickReset(jlJoystickE js) { gJoyRestValid[js] = false; gJoystickPresent = joystickInit(); @@ -525,10 +662,12 @@ void jlpJoystickReset(jlJoystickE js) { void jlpInputInit(void) { uint8_t kbFlags; - memset(gKeyState, 0, sizeof(gKeyState)); - memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); + memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrTapped, 0, sizeof(gIsrTapped)); + gIsrKeysChanged = 0u; + gKeyTapShown = false; // Seed the lock toggles from the BIOS shift-flag byte -- the last // state the BIOS saw before we take INT 9 away from it. @@ -546,6 +685,7 @@ void jlpInputInit(void) { _go32_dpmi_lock_data((void *)gScanSet1ToKey, sizeof(gScanSet1ToKey)); _go32_dpmi_lock_data((void *)gIsrState, sizeof(gIsrState)); _go32_dpmi_lock_data((void *)gIsrTapped, sizeof(gIsrTapped)); + _go32_dpmi_lock_data((void *)&gIsrKeysChanged, sizeof(gIsrKeysChanged)); _go32_dpmi_lock_data((void *)gScanAsciiNormal, sizeof(gScanAsciiNormal)); _go32_dpmi_lock_data((void *)gScanAsciiShift, sizeof(gScanAsciiShift)); _go32_dpmi_lock_data((void *)gKeypadAscii, sizeof(gKeypadAscii)); @@ -570,7 +710,7 @@ void jlpInputInit(void) { gHooked = true; gMousePresent = mouseInit(); - gMouseAttached = gMousePresent; + jlgMouseAttached = gMousePresent; memset(gJoyRestValid, 0, sizeof(gJoyRestValid)); gJoystickPresent = joystickInit(); } @@ -579,9 +719,16 @@ void jlpInputInit(void) { void jlpInputPoll(void) { uint8_t tail; - disable(); - inputLatch(gKeyState, gIsrState, gIsrTapped, JL_KEY_COUNT); - enable(); + // Interrupts are held only to latch: an idle keyboard, the usual + // poll, skips the cli/sti pair (each a trap to the DPMI host). A key + // the ISR takes just after the check is the next poll's, as one just + // after the latch always was. + if (gIsrKeysChanged != 0u || gKeyTapShown) { + disable(); + gIsrKeysChanged = 0u; + gKeyTapShown = jlInputLatch(jlgKeyState, gIsrState, gIsrTapped, JL_KEY_COUNT); + enable(); + } // Drain ISR-translated characters into the core queue. Single- // consumer side of the SPSC ring: only gIsrCharTail is written @@ -602,6 +749,7 @@ void jlpInputShutdown(void) { if (!gHooked) { return; } + mouseCallbackRemove(); _go32_dpmi_set_protected_mode_interrupt_vector(9, &gOldHandler); _go32_dpmi_free_iret_wrapper(&gNewHandler); gHooked = false; diff --git a/src/dos/serial.c b/src/dos/serial.c index acbb897..95be3a7 100644 --- a/src/dos/serial.c +++ b/src/dos/serial.c @@ -78,7 +78,7 @@ #define XOFF_CHAR 0x13u #define TX_CTL_NONE 0x00u // no XOFF / XON waiting to go out -// A write's backstop on a busy or held transmitter, in polls (serialTxStalled): +// A write's backstop on a busy or held transmitter, in polls (jlSerialTxStalled): // the clock bounds it first, once it runs. #define TX_WAIT_POLLS 200000ul @@ -351,7 +351,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { return false; } if (!serialUartPresent(base)) { - coreSetError(SERIAL_NO_UART); + jlCoreSetError(SERIAL_NO_UART); return false; } // A failed open puts the modem lines back the way it found them, and a @@ -477,7 +477,7 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { uint16_t n; for (n = 0u; n < len; n++) { - serialTxWaitStart(&wait, TX_WAIT_POLLS); + jlSerialTxWaitStart(&wait, TX_WAIT_POLLS); // Bounded wait for the holding register and the peer. Interrupts are off from the check to // the write, so the ISR's XOFF / XON cannot land in the register in between. for (;;) { @@ -488,7 +488,7 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { break; } enable(); - if (serialTxStalled(&wait)) { + if (jlSerialTxStalled(&wait)) { return n; } } diff --git a/src/dvx/audio.c b/src/dvx/audio.c index 1f85cc9..8159312 100644 --- a/src/dvx/audio.c +++ b/src/dvx/audio.c @@ -31,7 +31,7 @@ // None of this may run inside an interrupt (audio.lib's rule), and the // stream's fill does not: audio.lib calls it from its pump in program time. // So jlAudioTickRegister's callback runs there, between exactly the samples -// it falls between (psgSynthTickRun), as on the hosted ports: notes start +// it falls between (jlPsgSynthTickRun), as on the hosted ports: notes start // where they should in the sound. Every driver runs the mixer in real time, // the null one included, so the ticks keep time on a machine with no sound // too. A callback can do anything; jlAudioCritical* stay the generic no-ops: @@ -133,8 +133,8 @@ static int32_t voiceVolume(uint8_t atten); // ----- Module state ----- -static AudioCtxT *gCtx = NULL; -static MusicT *gMusic = NULL; +static AudioCtxT *gCtx = NULL; +static MusicT *gMusic = NULL; static bool gMusicTail = false; // the song ended on its own (musicEnded)... static uint32_t gMusicEndMs = 0u; // ... at this jlpMillisElapsed static SfxSlotT gSlots[JL_AUDIO_SFX_SLOTS]; @@ -322,10 +322,10 @@ static uint32_t synthFill(AudioStreamT *stream, void *buf, uint32_t frames, void out = (int16_t *)buf; done = 0; while (done < frames) { - run = psgSynthTickRun(frames - done < TONE_CHUNK_FRAMES ? frames - done : TONE_CHUNK_FRAMES); + run = jlPsgSynthTickRun(frames - done < TONE_CHUNK_FRAMES ? frames - done : TONE_CHUNK_FRAMES); memset(gSynthMix, 0, run * sizeof(gSynthMix[0])); if (gSynthVoices) { - psgSynthMix(gSynthMix, run, TONE_CHANNELS); + jlPsgSynthMix(gSynthMix, run, TONE_CHANNELS); } for (i = 0; i < run; i++) { out[done + i] = (int16_t)gSynthMix[i]; @@ -347,7 +347,7 @@ static void synthOpen(void) { fmt.rate = info.rate >= AUDIO_RATE_MIN && info.rate <= AUDIO_RATE_MAX ? info.rate : TONE_RATE_DEFAULT; fmt.bits = TONE_BITS; fmt.channels = TONE_CHANNELS; - psgSynthInit(fmt.rate); + jlPsgSynthInit(fmt.rate); if (audioStreamOpen(gCtx, &fmt, TONE_BUFFER_MS, synthFill, NULL, &gSynth) != AUDIO_OK) { gSynth = NULL; return; @@ -434,7 +434,7 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { uint32_t hz; if (gSynthVoices) { - psgSynthNoise(pitch, atten); + jlPsgSynthNoise(pitch, atten); return; } // Every port's curve (audioNoiseHz), as far as an audio.lib tone goes. @@ -542,13 +542,13 @@ bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { if (fn != NULL && gSynth == NULL) { return false; } - return psgSynthTickRegister(fn, hz); + return jlPsgSynthTickRegister(fn, hz); } void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { if (gSynthVoices) { - psgSynthVoice(voice, freqHz, atten); + jlPsgSynthVoice(voice, freqHz, atten); return; } toneSet(&gVoices[voice], freqHz, voiceVolume(atten)); diff --git a/src/dvx/dvxApp.c b/src/dvx/dvxApp.c index 5297bbe..a14d783 100644 --- a/src/dvx/dvxApp.c +++ b/src/dvx/dvxApp.c @@ -38,5 +38,5 @@ SHELL_APP_DESCRIPTOR( int32_t appMain(DxeAppContextT *ctx) { - return dvxPortRun(ctx); + return jlDvxPortRun(ctx); } diff --git a/src/dvx/dvxPort.h b/src/dvx/dvxPort.h index d7baa77..966f5a0 100644 --- a/src/dvx/dvxPort.h +++ b/src/dvx/dvxPort.h @@ -31,60 +31,60 @@ extern AppDescriptorT appDescriptor; // ----- program.c ----- // The running program's DVX context (appMain's argument). -extern DxeAppContextT *gDvxPortProgram; +extern DxeAppContextT *jlgDvxPortProgram; // The program's atexit() and exit(): make/dvx.mk renames the calls in the -// library and the program to these. exit() returns to dvxPortRun, so the +// library and the program to these. exit() returns to jlDvxPortRun, so the // handlers run, and JoeyLib shuts down and closes its log, when the program // ends, however it ends, and appMain returns as DVX expects. DVX's own // atexit would keep the handlers until DVX itself exits, long after the // program's code is gone. -int dvxPortAtExit(void (*fn)(void)); -void dvxPortExit(int status) __attribute__((noreturn)); +int jlDvxPortAtExit(void (*fn)(void)); +void jlDvxPortExit(int status) __attribute__((noreturn)); // appMain's body: run the program's main, then end the program; returns // main's status. -int32_t dvxPortRun(DxeAppContextT *ctx); +int32_t jlDvxPortRun(DxeAppContextT *ctx); // A timer-service callback at hz with its code (the page(s) from fn on) and // dataBytes of data pinned, as DVX requires of anything its timer interrupt // runs. Returns the timer, or DVX_PORT_NO_TIMER with nothing left pinned. -// dvxPortTimerStop removes the timer (if any) and unpins both. +// jlDvxPortTimerStop removes the timer (if any) and unpins both. #define DVX_PORT_NO_TIMER (-1) #define DVX_PORT_ISR_LOCK_BYTES 4096u -int32_t dvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes); -void dvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes); +int32_t jlDvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes); +void jlDvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes); // ----- hal.c ----- -extern WindowT *gDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown +extern WindowT *jlgDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown // End the program now if the user closed its window (the close box, // Alt+F4, Game > Exit). -void dvxPortEndIfClosed(void); +void jlDvxPortEndIfClosed(void); // Full screen on if it is off, off if it is on (Game > Full Screen, // Alt+Enter); the choice is kept for the program's next run. -void dvxPortFullScreenToggle(void); +void jlDvxPortFullScreenToggle(void); // UBER_PROBE's display parity row (examples/uber declares it itself): the // pixels where the stage is shown that differ from the stage, and how many // were compared; 0xFFFFFFFF with nowhere to show it. -uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +uint32_t jlDvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); // Called where a JoeyLib program comes back to the port every frame (the // input poll, the present): yield to the desktop if the program has not -// this virtual frame, then dvxPortEndIfClosed. -void dvxPortService(void); +// this virtual frame, then jlDvxPortEndIfClosed. +void jlDvxPortService(void); // A point in the coordinates the window's onMouse gets (content-area pixels, // or screen pixels while the program has the whole screen) to the stage // pixel under it, clamped to the stage. -void dvxPortStagePoint(int32_t *x, int32_t *y); +void jlDvxPortStagePoint(int32_t *x, int32_t *y); // ----- input.c: the window's input callbacks, installed by hal.c ----- -void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod); -void dvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); +void jlDvxPortOnKey(WindowT *win, int32_t key, int32_t mod); +void jlDvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); #endif diff --git a/src/dvx/hal.c b/src/dvx/hal.c index 16d6e9f..02bbeec 100644 --- a/src/dvx/hal.c +++ b/src/dvx/hal.c @@ -12,7 +12,7 @@ // as the hosted ports' windows are) or with square pixels, the user's // choice, under a Game menu (Full Screen, Square Pixels, Help, Exit). // Closing the window by any of those routes ends the program at its next -// frame (dvxPortEndIfClosed). +// frame (jlDvxPortEndIfClosed). // // The window's content buffer is in the desktop's own pixel format (8, 15, // 16 or 32 bits), so the present expands the stage's changed rows through @@ -56,7 +56,7 @@ // compositing of its window -- runs only while the program yields. A JoeyLib // program need not call jlWaitVBL (plenty loop on jlInputPoll alone, as a // DOS program may), so the input poll and the present yield too, at most -// once a virtual frame (dvxPortService), which keeps their cost low. +// once a virtual frame (jlDvxPortService), which keeps their cost low. #include "dvxSdk.h" @@ -95,7 +95,7 @@ #define PALETTES_NONE 0x0000u #define PALETTES_ALL 0xFFFFu -// dvxPortProbeDisplayMismatch with nowhere the stage is shown. +// jlDvxPortProbeDisplayMismatch with nowhere the stage is shown. #define PROBE_NO_TARGET 0xFFFFFFFFu // Game menu commands. @@ -177,7 +177,7 @@ static void yieldMark(uint64_t frame); // ----- Module state ----- -WindowT *gDvxPortWindow = NULL; +WindowT *jlgDvxPortWindow = NULL; static int32_t gScale = 1; // window pixels per stage pixel across static bool gSquare = false; // square pixels, not 4:3 (shapeRows) @@ -312,7 +312,7 @@ static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y, // The frame timer (DVX_FRAME_HZ, in DVX's timer interrupt): the next -// dvxPortService yields. +// jlDvxPortService yields. static void frameTickIsr(void *userData) { (void)userData; gYieldDue = 1u; @@ -325,18 +325,18 @@ static void frameTickIsr(void *userData) { static void fullSet(bool on) { AppContextT *ac; - ac = gDvxPortProgram->shellCtx; - if (on && dvxExclusiveBegin(ac, gDvxPortWindow, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, FULL_SCREEN_BPP, onFullChange, NULL) != DVX_EXCL_OK) { + ac = jlgDvxPortProgram->shellCtx; + if (on && dvxExclusiveBegin(ac, jlgDvxPortWindow, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, FULL_SCREEN_BPP, onFullChange, NULL) != DVX_EXCL_OK) { dvxLog("JoeyLib: %s: no full-screen mode", appDescriptor.name); on = false; } if (!on && gFullWanted) { - dvxExclusiveEnd(ac, gDvxPortWindow); + dvxExclusiveEnd(ac, jlgDvxPortWindow); gFullActive = false; redrawAll(); } gFullWanted = on; - wmMenuItemSetChecked(gDvxPortWindow->menuBar, CMD_GAME_FULL_SCREEN, on); + wmMenuItemSetChecked(jlgDvxPortWindow->menuBar, CMD_GAME_FULL_SCREEN, on); } @@ -384,7 +384,7 @@ static void lutBuild(const jlSurfaceT *src, uint16_t palettes) { } } if (gLutKind == LUT_INDEXED) { - dvxExclusiveSetPalette(gDvxPortProgram->shellCtx, gFullPalette, p * JL_SURFACE_COLORS_PER_PALETTE, JL_SURFACE_COLORS_PER_PALETTE); + dvxExclusiveSetPalette(jlgDvxPortProgram->shellCtx, gFullPalette, p * JL_SURFACE_COLORS_PER_PALETTE, JL_SURFACE_COLORS_PER_PALETTE); } } } @@ -439,7 +439,7 @@ static void onFullChange(WindowT *win, ExclusiveStateE state, void *userData) { gFullActive = state == ExclusiveActiveE; if (state == ExclusiveEndedE) { gFullWanted = false; - wmMenuItemSetChecked(gDvxPortWindow->menuBar, CMD_GAME_FULL_SCREEN, false); + wmMenuItemSetChecked(jlgDvxPortWindow->menuBar, CMD_GAME_FULL_SCREEN, false); } redrawAll(); } @@ -449,7 +449,7 @@ static void onMenu(WindowT *win, int32_t menuId) { (void)win; switch (menuId) { case CMD_GAME_FULL_SCREEN: - dvxPortFullScreenToggle(); + jlDvxPortFullScreenToggle(); break; case CMD_GAME_SQUARE: @@ -458,7 +458,7 @@ static void onMenu(WindowT *win, int32_t menuId) { break; case CMD_GAME_HELP: - shellOpenHelp(gDvxPortProgram->shellCtx, gDvxPortProgram->helpFile); + shellOpenHelp(jlgDvxPortProgram->shellCtx, jlgDvxPortProgram->helpFile); break; case CMD_GAME_EXIT: @@ -478,11 +478,11 @@ static void onMenu(WindowT *win, int32_t menuId) { static void onPaint(WindowT *win, RectT *dirty) { (void)win; (void)dirty; - if (gStage == NULL || gFullActive) { + if (jlgStage == NULL || gFullActive) { return; } - (void)lutSync(gStage); - presentRows(gStage, PALETTES_ALL, false); + (void)lutSync(jlgStage); + presentRows(jlgStage, PALETTES_ALL, false); } @@ -528,8 +528,8 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) { x1 = JL_SURFACE_WIDTH; } else { // Clamp a band past the row end, as the DOS present does. - maxWord = gStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? gStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1); - x0 = gStageMinWord[y] * STAGE_PIXELS_PER_WORD; + maxWord = jlgStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? jlgStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1); + x0 = jlgStageMinWord[y] * STAGE_PIXELS_PER_WORD; x1 = (maxWord + 1) * STAGE_PIXELS_PER_WORD; } x1 = x1 < t.cols ? x1 : t.cols; @@ -537,7 +537,7 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) { continue; } expandRow(&t, src, y, x0, x1); - gPresentCopiedBytes += (uint32_t)(x1 - x0); + jlgPresentCopiedBytes += (uint32_t)(x1 - x0); if (runY0 < 0) { runY0 = y; runX0 = x0; @@ -550,7 +550,7 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) { if (runY0 >= 0 && show) { targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0); } - gPresentScannedRows += JL_SURFACE_HEIGHT; + jlgPresentScannedRows += JL_SURFACE_HEIGHT; } @@ -598,11 +598,11 @@ static uint32_t probeStagePixel(const DisplayT *d, LutKindE kind, const jlSurfac // stage goes now. Before jlInit has the stage the first present does it. static void redrawAll(void) { gLutBits = 0; - if (gStage == NULL) { + if (jlgStage == NULL) { return; } - (void)lutSync(gStage); - presentRows(gStage, PALETTES_ALL, true); + (void)lutSync(jlgStage); + presentRows(jlgStage, PALETTES_ALL, true); } @@ -621,7 +621,7 @@ static bool settingLoad(const char *key) { PrefsHandleT *prefs; bool on; - shellConfigPath(gDvxPortProgram, SETTINGS_FILE, path, (int32_t)sizeof(path)); + shellConfigPath(jlgDvxPortProgram, SETTINGS_FILE, path, (int32_t)sizeof(path)); prefs = prefsLoad(path); if (prefs == NULL) { return false; @@ -636,10 +636,10 @@ static void settingSave(const char *key, bool on) { char path[DVX_MAX_PATH]; PrefsHandleT *prefs; - if (shellEnsureConfigDir(gDvxPortProgram) != 0) { + if (shellEnsureConfigDir(jlgDvxPortProgram) != 0) { return; } - shellConfigPath(gDvxPortProgram, SETTINGS_FILE, path, (int32_t)sizeof(path)); + shellConfigPath(jlgDvxPortProgram, SETTINGS_FILE, path, (int32_t)sizeof(path)); prefs = prefsLoad(path); if (prefs == NULL) { return; @@ -666,11 +666,11 @@ static void shapeSet(bool square) { WindowT *win; gSquare = square; - win = gDvxPortWindow; + win = jlgDvxPortWindow; if (win == NULL) { return; } - ac = gDvxPortProgram->shellCtx; + ac = jlgDvxPortProgram->shellCtx; gScale = windowScale(ac); dvxResizeWindow(ac, win, win->w + JL_SURFACE_WIDTH * gScale - win->contentW, win->h + shapeRows(gScale) - win->contentH); wmMenuItemSetChecked(win->menuBar, CMD_GAME_SQUARE, gSquare); @@ -681,7 +681,7 @@ static void shapeSet(bool square) { static const DisplayT *targetDisplay(void) { AppContextT *ac; - ac = gDvxPortProgram->shellCtx; + ac = jlgDvxPortProgram->shellCtx; return gFullActive ? dvxExclusiveDisplay(ac) : dvxGetDisplay(ac); } @@ -695,7 +695,7 @@ static bool targetGet(PresentTargetT *t) { int32_t scaleW; int32_t scaleH; - if (gDvxPortWindow == NULL) { + if (jlgDvxPortWindow == NULL) { return false; } d = targetDisplay(); @@ -718,12 +718,12 @@ static bool targetGet(PresentTargetT *t) { t->height = shapeRows(gScale); t->x = 0; t->y = 0; - t->pitch = gDvxPortWindow->contentPitch; - t->pixels = gDvxPortWindow->contentBuf; + t->pitch = jlgDvxPortWindow->contentPitch; + t->pixels = jlgDvxPortWindow->contentBuf; // The content area is the scaled stage unless DVX sized the window // otherwise. - t->rows = gDvxPortWindow->contentH * JL_SURFACE_HEIGHT / t->height; - t->cols = gDvxPortWindow->contentW / t->scale; + t->rows = jlgDvxPortWindow->contentH * JL_SURFACE_HEIGHT / t->height; + t->cols = jlgDvxPortWindow->contentW / t->scale; t->rows = t->rows < JL_SURFACE_HEIGHT ? t->rows : JL_SURFACE_HEIGHT; t->cols = (t->cols < JL_SURFACE_WIDTH ? t->cols : JL_SURFACE_WIDTH) & ~(STAGE_PIXELS_PER_BYTE - 1); return true; @@ -747,9 +747,9 @@ static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, r.w = w * t->scale; r.h = targetRow(t, y + h) - targetRow(t, y); if (t->full) { - dvxExclusiveFlush(gDvxPortProgram->shellCtx, &r); + dvxExclusiveFlush(jlgDvxPortProgram->shellCtx, &r); } else { - dvxInvalidateRect(gDvxPortProgram->shellCtx, gDvxPortWindow, r.x, r.y, r.w, r.h); + dvxInvalidateRect(jlgDvxPortProgram->shellCtx, jlgDvxPortWindow, r.x, r.y, r.w, r.h); } } @@ -775,9 +775,9 @@ static bool windowCreate(AppContextT *ac) { } win->gameInput = true; win->onClose = onClose; - win->onKey = dvxPortOnKey; + win->onKey = jlDvxPortOnKey; win->onMenu = onMenu; - win->onMouse = dvxPortOnMouse; + win->onMouse = jlDvxPortOnMouse; win->onPaint = onPaint; bar = wmAddMenuBar(win); @@ -795,7 +795,7 @@ static bool windowCreate(AppContextT *ac) { if (win->contentW != wantW || win->contentH != wantH) { dvxResizeWindow(ac, win, win->w + wantW - win->contentW, win->h + wantH - win->contentH); } - gDvxPortWindow = win; + jlgDvxPortWindow = win; dvxLog("JoeyLib: %s: %" PRId32 "x%" PRId32 " at %" PRId32 "x, %s, on a %" PRId32 "x%" PRId32 " %" PRId32 "-bit desktop", appDescriptor.name, win->contentW, win->contentH, gScale, gSquare ? "square" : "4:3", ac->display.width, ac->display.height, ac->display.format.bitsPerPixel); return true; } @@ -826,15 +826,15 @@ static void yieldMark(uint64_t frame) { // ----- Port API (alphabetical) ----- -void dvxPortEndIfClosed(void) { +void jlDvxPortEndIfClosed(void) { if (gCloseRequested) { - dvxPortExit(0); + jlDvxPortExit(0); } } // The user's switch (Game > Full Screen, Alt+Enter), kept for the next run. -void dvxPortFullScreenToggle(void) { +void jlDvxPortFullScreenToggle(void) { fullSet(!gFullWanted); settingSave(SETTINGS_FULL_SCREEN, gFullWanted); } @@ -851,7 +851,7 @@ void dvxPortFullScreenToggle(void) { // present's tables, so a stale or wrong table, row or scale shows; where // the stage goes is the present's own targetGet, which the screenshot // gates check. Always compiled: the probe build links the stock library. -uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { +uint32_t jlDvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { PresentTargetT t; const DisplayT *d; const uint8_t *row; @@ -908,7 +908,7 @@ uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) // clock read costs ~5 us under DOSBox (a PIT latch), paid by every present, // input poll and serial call. Without it, a clock read and a compare, the // division to the frame number once a frame (yieldMark). -void dvxPortService(void) { +void jlDvxPortService(void) { uint64_t now; if (gFrameTimer != DVX_PORT_NO_TIMER) { @@ -923,13 +923,13 @@ void dvxPortService(void) { tsYield(); } } - dvxPortEndIfClosed(); + jlDvxPortEndIfClosed(); } // Content-area pixels in the window, screen pixels on the whole screen: the // stage pixel under them, clamped to the stage. -void dvxPortStagePoint(int32_t *x, int32_t *y) { +void jlDvxPortStagePoint(int32_t *x, int32_t *y) { PresentTargetT t; if (targetGet(&t)) { @@ -958,12 +958,12 @@ bool jlpInit(const jlConfigT *config) { gCloseRequested = false; gLutBits = 0; (void)clockMicros(); - if (!windowCreate(gDvxPortProgram->shellCtx)) { - coreSetError("DVX could not open the program's window"); + if (!windowCreate(jlgDvxPortProgram->shellCtx)) { + jlCoreSetError("DVX could not open the program's window"); return false; } - // Paces dvxPortService's yields; without it the service reads the clock. - gFrameTimer = dvxPortTimerStart(DVX_FRAME_HZ, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue)); + // Paces jlDvxPortService's yields; without it the service reads the clock. + gFrameTimer = jlDvxPortTimerStart(DVX_FRAME_HZ, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue)); if (settingLoad(SETTINGS_FULL_SCREEN)) { fullSet(true); } @@ -979,27 +979,27 @@ uint32_t jlpMillisElapsed(void) { void jlpPresent(const jlSurfaceT *src) { uint16_t palettes; - if (src == NULL || gDvxPortWindow == NULL) { + if (src == NULL || jlgDvxPortWindow == NULL) { return; } palettes = PALETTES_NONE; if (lutSync(src)) { palettes = PALETTES_ALL; - } else if (gStagePaletteDirty) { - lutBuild(src, gStagePaletteDirtyMask); + } else if (jlgStagePaletteDirty) { + lutBuild(src, jlgStagePaletteDirtyMask); // On the whole screen at 8 bits the new colours are the screen's // palette: no pixel changes. - palettes = gLutKind == LUT_INDEXED ? PALETTES_NONE : gStagePaletteDirtyMask; + palettes = gLutKind == LUT_INDEXED ? PALETTES_NONE : jlgStagePaletteDirtyMask; } // An SCB change moves rows to other tables: expand them all again. - if (gStageScbDirty) { + if (jlgStageScbDirty) { palettes = PALETTES_ALL; } - gStagePaletteDirty = false; - gStagePaletteDirtyMask = PALETTES_NONE; - gStageScbDirty = false; + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = PALETTES_NONE; + jlgStageScbDirty = false; presentRows(src, palettes, true); - dvxPortService(); + jlDvxPortService(); } @@ -1007,12 +1007,12 @@ void jlpPresent(const jlSurfaceT *src) { // (DVX tells onFullChange nothing then). void jlpShutdown(void) { if (gFrameTimer != DVX_PORT_NO_TIMER) { - dvxPortTimerStop(gFrameTimer, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue)); + jlDvxPortTimerStop(gFrameTimer, frameTickIsr, (const void *)&gYieldDue, sizeof(gYieldDue)); gFrameTimer = DVX_PORT_NO_TIMER; } - if (gDvxPortWindow != NULL) { - dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow); - gDvxPortWindow = NULL; + if (jlgDvxPortWindow != NULL) { + dvxDestroyWindow(jlgDvxPortProgram->shellCtx, jlgDvxPortWindow); + jlgDvxPortWindow = NULL; } gFullWanted = false; gFullActive = false; @@ -1034,5 +1034,5 @@ void jlpWaitVBL(void) { (void)gameFrameWait(&gPacer); yieldMark(gPacer.frame); } - dvxPortEndIfClosed(); + jlDvxPortEndIfClosed(); } diff --git a/src/dvx/input.c b/src/dvx/input.c index 2cbde3e..b98d2d1 100644 --- a/src/dvx/input.c +++ b/src/dvx/input.c @@ -22,11 +22,11 @@ // screen); hal.c maps them to the stage pixel under them, and the next poll // publishes it, so the core's button edges compare two stable snapshots. // DVX can report the buttons more than once between two polls, so a click -// both made and let go there is latched (inputLatch) and reads down for one +// both made and let go there is latched (jlInputLatch) and reads down for one // poll, as on the ports that hear their buttons as events. // // Joysticks: DVX's joystick library, calibrated in DVX's Control Panel: -// axes -32768..32767 scaled to -127..127 (inputAxisScale), real analog, as +// axes -32768..32767 scaled to -127..127 (jlInputAxisScale), real analog, as // on DOS. DVX's keyboard joystick (keys standing in when no // joystick is plugged in) is left out: JoeyLib then reports no joystick, as // every port does, and programs read those keys themselves. @@ -42,7 +42,7 @@ // ----- Constants ----- -// DVX's axes run -32768..32767 (inputAxisScale takes them to JoeyLib's). +// DVX's axes run -32768..32767 (jlInputAxisScale takes them to JoeyLib's). #define JOY_AXIS_X 0 #define JOY_AXIS_Y 1 #define JOY_NO_HANDLE (-1) @@ -73,7 +73,7 @@ static uint8_t gMouseTapped[JL_MOUSE_BUTTON_COUNT]; // ----- Internal helpers (alphabetical) ----- static int8_t joyAxis(int32_t value) { - return inputAxisScale(value, INT16_MIN, INT16_MAX); + return jlInputAxisScale(value, INT16_MIN, INT16_MAX); } @@ -106,7 +106,7 @@ static void joystickOpen(void) { if (!joyGetInfo(device, &info) || (info.caps & JOY_CAP_KEYBOARD) != 0) { continue; } - gJoyHandle[js] = joyOpen(gDvxPortProgram->shellCtx, device); + gJoyHandle[js] = joyOpen(jlgDvxPortProgram->shellCtx, device); if (gJoyHandle[js] != JOY_NO_HANDLE) { js++; } @@ -121,17 +121,17 @@ static void joystickPoll(void) { for (js = 0; js < JL_JOYSTICK_COUNT; js++) { if (gJoyHandle[js] == JOY_NO_HANDLE || !joyRead(gJoyHandle[js], &st) || !st.connected) { - gJoyConnected[js] = false; - gJoyAxisX[js] = 0; - gJoyAxisY[js] = 0; - memset(gJoyButtonState[js], 0, sizeof(gJoyButtonState[js])); + jlgJoyConnected[js] = false; + jlgJoyAxisX[js] = 0; + jlgJoyAxisY[js] = 0; + memset(jlgJoyButtonState[js], 0, sizeof(jlgJoyButtonState[js])); continue; } - gJoyConnected[js] = true; - gJoyAxisX[js] = inputDeadZone(joyAxis(st.axes[JOY_AXIS_X]), gJoyDeadZone[js]); - gJoyAxisY[js] = inputDeadZone(joyAxis(st.axes[JOY_AXIS_Y]), gJoyDeadZone[js]); + jlgJoyConnected[js] = true; + jlgJoyAxisX[js] = jlInputDeadZone(joyAxis(st.axes[JOY_AXIS_X]), jlgJoyDeadZone[js]); + jlgJoyAxisY[js] = jlInputDeadZone(joyAxis(st.axes[JOY_AXIS_Y]), jlgJoyDeadZone[js]); for (b = 0; b < JL_JOY_BUTTON_COUNT; b++) { - gJoyButtonState[js][b] = (st.buttons & (1u << b)) != 0; + jlgJoyButtonState[js][b] = (st.buttons & (1u << b)) != 0; } } } @@ -139,26 +139,26 @@ static void joystickPoll(void) { // ----- Port API (alphabetical) ----- -void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod) { +void jlDvxPortOnKey(WindowT *win, int32_t key, int32_t mod) { (void)win; if (key == (SCAN_ALT_KEYPAD_ENTER | KEY_EXT_FLAG) || ((mod & KEY_MOD_ALT) != 0 && (key == KEY_ENTER || key == (KEY_SCAN_ENTER | KEY_EXT_FLAG)))) { - dvxPortFullScreenToggle(); + jlDvxPortFullScreenToggle(); return; } // Forward delete is DVX's extended Delete key; every other character is // its own code, and jlInputCharPush drops what JoeyLib does not carry. - // Ctrl and Alt make chords, which type nothing (inputCharTyped). + // Ctrl and Alt make chords, which type nothing (jlInputCharTyped). if (key == KEY_DELETE) { - inputCharTyped(JL_CHAR_DELETE, (mod & CHORD_MODIFIERS) != 0); + jlInputCharTyped(JL_CHAR_DELETE, (mod & CHORD_MODIFIERS) != 0); } else if (key < KEY_EXT_FLAG) { - inputCharTyped((uint8_t)key, (mod & CHORD_MODIFIERS) != 0); + jlInputCharTyped((uint8_t)key, (mod & CHORD_MODIFIERS) != 0); } } -void dvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { +void jlDvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { (void)win; - dvxPortStagePoint(&x, &y); + jlDvxPortStagePoint(&x, &y); gNextMouseX = (int16_t)x; gNextMouseY = (int16_t)y; gMouseHeld[JL_MOUSE_BUTTON_LEFT] = (buttons & MOUSE_LEFT) != 0; @@ -173,12 +173,12 @@ void dvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // ----- HAL API (alphabetical) ----- void jlpInputInit(void) { - memset(gKeyState, 0, sizeof(gKeyState)); - memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); + memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev)); // DVX always has a mouse; it starts in the middle of the stage. - gNextMouseX = JL_SURFACE_WIDTH / 2; - gNextMouseY = JL_SURFACE_HEIGHT / 2; - gMouseAttached = true; + gNextMouseX = JL_SURFACE_WIDTH / 2; + gNextMouseY = JL_SURFACE_HEIGHT / 2; + jlgMouseAttached = true; memset(gMouseHeld, 0, sizeof(gMouseHeld)); memset(gMouseTapped, 0, sizeof(gMouseTapped)); joystickOpen(); @@ -190,8 +190,8 @@ void jlpInputPoll(void) { uint8_t scan; uint8_t key; - dvxPortService(); - if (gDvxPortWindow != NULL && gDvxPortWindow->focused) { + jlDvxPortService(); + if (jlgDvxPortWindow != NULL && jlgDvxPortWindow->focused) { platformKeyStateGet(held); } else { memset(held, 0, sizeof(held)); @@ -199,18 +199,18 @@ void jlpInputPoll(void) { for (scan = 0; scan < SCAN_SET1_CODES; scan++) { key = gScanSet1ToKey[scan]; if (key != JL_KEY_NONE) { - gKeyState[key] = (held[scan] | held[scan | KEY_RAW_EXT]) != 0; + jlgKeyState[key] = (held[scan] | held[scan | KEY_RAW_EXT]) != 0; } } - // Alt+Enter is the full-screen switch (dvxPortOnKey), not Return, as the + // Alt+Enter is the full-screen switch (jlDvxPortOnKey), not Return, as the // hosted ports swallow it too. - if (gKeyState[JL_KEY_LALT]) { - gKeyState[JL_KEY_RETURN] = false; + if (jlgKeyState[JL_KEY_LALT]) { + jlgKeyState[JL_KEY_RETURN] = false; } - gMouseX = gNextMouseX; - gMouseY = gNextMouseY; - inputLatch(gMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); + jlgMouseX = gNextMouseX; + jlgMouseY = gNextMouseY; + jlInputLatch(jlgMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); joystickPoll(); } diff --git a/src/dvx/program.c b/src/dvx/program.c index 6899349..eebfbe0 100644 --- a/src/dvx/program.c +++ b/src/dvx/program.c @@ -5,12 +5,12 @@ // Running and ending a JoeyLib program on DVX, and the folders its files // live in. // -// dvxPortRun (appMain, in the program's own DVX task) records the program's +// jlDvxPortRun (appMain, in the program's own DVX task) records the program's // DVX context, then calls the program's main -- compiled as jlGameMain // (make/dvx.mk: -Dmain=jlGameMain) -- with argv[0] the .app's path and the // launch arguments after it. A program with `int main(void)` simply ignores // them. The program ends the same way whether main returns, it calls exit(), -// or the user closes its window (hal.c): exit() jumps back to dvxPortRun, so +// or the user closes its window (hal.c): exit() jumps back to jlDvxPortRun, so // appMain always returns, the end DVX expects of a program. Then its atexit // handlers run, then jlShutdown (nothing to do if the program already shut // down), then the log is closed, since DVX -- which owns the file handle -- @@ -54,11 +54,11 @@ static void rootSet(char *out, size_t cap, const char *dir); // ----- Module state ----- -DxeAppContextT *gDvxPortProgram = NULL; +DxeAppContextT *jlgDvxPortProgram = NULL; static void (*gAtExit[ATEXIT_MAX])(void); static int gAtExitCount = 0; -static jmp_buf gEndJump; // back to dvxPortRun from exit() +static jmp_buf gEndJump; // back to jlDvxPortRun from exit() static int gEndStatus = 0; // main's return or exit()'s status static char gArgs[SHELL_ARGS_MAX]; static char gReadRoot[ROOT_MAX]; @@ -116,7 +116,7 @@ static void rootSet(char *out, size_t cap, const char *dir) { // ----- Port API (alphabetical) ----- -int dvxPortAtExit(void (*fn)(void)) { +int jlDvxPortAtExit(void (*fn)(void)) { if (fn == NULL || gAtExitCount == ATEXIT_MAX) { return -1; } @@ -126,19 +126,19 @@ int dvxPortAtExit(void (*fn)(void)) { } -void dvxPortExit(int status) { +void jlDvxPortExit(int status) { gEndStatus = status; longjmp(gEndJump, 1); } -int32_t dvxPortRun(DxeAppContextT *ctx) { +int32_t jlDvxPortRun(DxeAppContextT *ctx) { char *argv[ARGV_MAX + 1]; int argc; - gDvxPortProgram = ctx; - gAtExitCount = 0; - gWriteRootMade = false; + jlgDvxPortProgram = ctx; + gAtExitCount = 0; + gWriteRootMade = false; rootSet(gReadRoot, sizeof(gReadRoot), ctx->appDir); rootSet(gWriteRoot, sizeof(gWriteRoot), ctx->configDir); (void)snprintf(gArgs, sizeof(gArgs), "%s", ctx->args); @@ -157,7 +157,7 @@ int32_t dvxPortRun(DxeAppContextT *ctx) { // DVX runs timer callbacks inside its timer interrupt, so their code and the // data they touch must be pinned first; DVX removes the timer itself when the // program ends. -int32_t dvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes) { +int32_t jlDvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, uint32_t dataBytes) { int32_t timer; if (platformLockRegion((const void *)fn, DVX_PORT_ISR_LOCK_BYTES, true) != 0) { @@ -167,16 +167,16 @@ int32_t dvxPortTimerStart(uint32_t hz, PlatformTimerFnT fn, const void *data, ui (void)platformUnlockRegion((const void *)fn, DVX_PORT_ISR_LOCK_BYTES, true); return DVX_PORT_NO_TIMER; } - timer = gameTimerAdd(gDvxPortProgram->shellCtx, hz, fn, NULL); + timer = gameTimerAdd(jlgDvxPortProgram->shellCtx, hz, fn, NULL); if (timer < 0) { - dvxPortTimerStop(DVX_PORT_NO_TIMER, fn, data, dataBytes); + jlDvxPortTimerStop(DVX_PORT_NO_TIMER, fn, data, dataBytes); return DVX_PORT_NO_TIMER; } return timer; } -void dvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes) { +void jlDvxPortTimerStop(int32_t timer, PlatformTimerFnT fn, const void *data, uint32_t dataBytes) { if (timer != DVX_PORT_NO_TIMER) { platformTimerRemove(timer); } @@ -196,7 +196,7 @@ const char *jlpReadRoot(void) { // leaves no folder behind. const char *jlpWriteRoot(void) { if (!gWriteRootMade) { - gWriteRootMade = shellEnsureConfigDir(gDvxPortProgram) == 0; + gWriteRootMade = shellEnsureConfigDir(jlgDvxPortProgram) == 0; } return gWriteRoot; } diff --git a/src/dvx/serial.c b/src/dvx/serial.c index 53e2d7a..d209aef 100644 --- a/src/dvx/serial.c +++ b/src/dvx/serial.c @@ -105,7 +105,7 @@ static int32_t rs232Bps(uint32_t baud) { // Let what the program wrote go out before the port does: until it should be // out at the line rate and the ring is empty, or CLOSE_MARGIN_MS past that. -// The desktop gets the time meanwhile (tsYield: not dvxPortService, which +// The desktop gets the time meanwhile (tsYield: not jlDvxPortService, which // may end the program, and this may run in the exit handler). static void txDrain(void) { uint32_t deadline; @@ -175,7 +175,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { void jlpSerialPoll(void) { - dvxPortService(); + jlDvxPortService(); } diff --git a/src/generic/genericDraw.c b/src/generic/genericDraw.c index 5b876ba..3f08fd7 100644 --- a/src/generic/genericDraw.c +++ b/src/generic/genericDraw.c @@ -17,6 +17,28 @@ #include "surfaceInternal.h" // full jlSurfaceT (s->pixels) + JL_SURFACE_PIXELS_SIZE #include "port.h" // jlpGenericSurfaceClear prototype +// ----- Prototypes ----- + +static inline void plotNibble(uint8_t *row, uint16_t x, uint8_t nibble); + + +// ----- Internal helpers ----- + +// One 4bpp pixel of a chunky row: x even is the byte's high nibble. +static inline void plotNibble(uint8_t *row, uint16_t x, uint8_t nibble) { + uint8_t *byte; + + byte = &row[x >> 1]; + if (x & 1u) { + *byte = (uint8_t)((*byte & 0xF0) | nibble); + } else { + *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4)); + } +} + + +// ----- Generic ops ----- + void jlpGenericSurfaceClear(jlSurfaceT *s, uint8_t doubled) { if (s->pixels != NULL) { @@ -76,20 +98,111 @@ void jlpGenericFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t } -// Chunky 4bpp nibble read-modify-write. Caller (plotPixelNoMark / jlDrawPixel) -// has already done the NULL + bounds check. -void jlpGenericDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { - uint8_t *byte; +// Chunky 4bpp circle outline whose whole box is on the surface (draw.c +// checks): draw.c's midpoint walk, every reflection of each step plotted +// through one of four row pointers (rows cy + y, cy - y, cy + x, and +// cy - x), each stepped a row at a time as y and x move, rather than a +// bounds-checked call and a row offset for each pixel. The same pixels as +// draw.c's clipping walk. +void jlpGenericDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { + uint8_t *belowY; + uint8_t *aboveY; + uint8_t *belowX; + uint8_t *aboveX; uint8_t nibble; + int16_t x; + int16_t y; + int16_t err; if (s->pixels == NULL) { return; } - byte = &s->pixels[SURFACE_ROW_OFFSET(y) + (x >> 1)]; nibble = colorIndex & 0x0F; - if (x & 1) { - *byte = (uint8_t)((*byte & 0xF0) | nibble); - } else { - *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4)); + x = (int16_t)r; + y = 0; + err = (int16_t)(1 - x); + belowY = &s->pixels[SURFACE_ROW_OFFSET(cy)]; + aboveY = belowY; + belowX = &s->pixels[SURFACE_ROW_OFFSET(cy + x)]; + aboveX = &s->pixels[SURFACE_ROW_OFFSET(cy - x)]; + while (x >= y) { + plotNibble(belowY, (uint16_t)(cx + x), nibble); + plotNibble(belowY, (uint16_t)(cx - x), nibble); + plotNibble(aboveY, (uint16_t)(cx + x), nibble); + plotNibble(aboveY, (uint16_t)(cx - x), nibble); + plotNibble(belowX, (uint16_t)(cx + y), nibble); + plotNibble(belowX, (uint16_t)(cx - y), nibble); + plotNibble(aboveX, (uint16_t)(cx + y), nibble); + plotNibble(aboveX, (uint16_t)(cx - y), nibble); + y++; + belowY += JL_SURFACE_BYTES_PER_ROW; + aboveY -= JL_SURFACE_BYTES_PER_ROW; + if (err <= 0) { + err = (int16_t)(err + y + y + 1); + } else { + x--; + belowX -= JL_SURFACE_BYTES_PER_ROW; + aboveX += JL_SURFACE_BYTES_PER_ROW; + err = (int16_t)(err + y + y - x - x + 1); + } } } + + +// Chunky 4bpp line, both ends on the surface (draw.c checks): draw.c's +// Bresenham walk, the row pointer stepped with y rather than a +// bounds-checked call and a row offset for each pixel. The same pixels as +// draw.c's clipping walk. +void jlpGenericDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { + uint8_t *row; + uint8_t nibble; + int16_t dx; + int16_t dy; + int16_t sx; + int16_t sy; + int16_t rowStep; + int16_t err; + int16_t e2; + int16_t x; + int16_t y; + + if (s->pixels == NULL) { + return; + } + nibble = colorIndex & 0x0F; + dx = (int16_t)((x1 > x0) ? x1 - x0 : x0 - x1); + dy = (int16_t)-((y1 > y0) ? y1 - y0 : y0 - y1); + sx = (x0 < x1) ? 1 : -1; + sy = (y0 < y1) ? 1 : -1; + rowStep = (y0 < y1) ? JL_SURFACE_BYTES_PER_ROW : -JL_SURFACE_BYTES_PER_ROW; + err = (int16_t)(dx + dy); + x = x0; + y = y0; + row = &s->pixels[SURFACE_ROW_OFFSET(y0)]; + while (1) { + plotNibble(row, (uint16_t)x, nibble); + if (x == x1 && y == y1) { + break; + } + e2 = (int16_t)(err + err); + if (e2 >= dy) { + err = (int16_t)(err + dy); + x = (int16_t)(x + sx); + } + if (e2 <= dx) { + err = (int16_t)(err + dx); + y = (int16_t)(y + sy); + row += rowStep; + } + } +} + + +// Chunky 4bpp nibble read-modify-write. Caller (plotPixelNoMark / jlDrawPixel) +// has already done the NULL + bounds check. +void jlpGenericDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { + if (s->pixels == NULL) { + return; + } + plotNibble(&s->pixels[SURFACE_ROW_OFFSET(y)], x, (uint8_t)(colorIndex & 0x0F)); +} diff --git a/src/generic/genericPort.c b/src/generic/genericPort.c index b1bb4fa..950a2ac 100644 --- a/src/generic/genericPort.c +++ b/src/generic/genericPort.c @@ -16,6 +16,15 @@ #include "surfaceInternal.h" #include "port.h" +// A 50 Hz frame is 20 ms; a 60 Hz frame 1000/60 = 50/3 ms, 50 thirds of +// a millisecond. +#define MS50_PER_FRAME 20u +#define MS60_THIRDS_PER_FRAME 50u +#define MS60_THIRDS 3u +// Frame deltas up to this (a per-frame caller sees 1) take 16-bit math, a +// 60 Hz one a frame at a time; more take the 32-bit multiply and divide. +#define MS_STEP_FRAMES 8u + // Display refresh rate in Hz. No universal value (IIgs 50/60, Amiga/ST 50, // DOS 70); every real port overrides. The stub keeps a blank port linkable. @@ -83,12 +92,30 @@ uint32_t jlpGenericMillisElapsed(void) { rem = gRemainder; if (hz == 50u) { // Exact: 20 ms per frame, no fractional remainder. - ms = ms + (uint32_t)delta * 20u; + if (delta <= MS_STEP_FRAMES) { + ms = ms + (uint16_t)(delta * MS50_PER_FRAME); + } else { + ms = ms + (uint32_t)delta * MS50_PER_FRAME; + } } else if (hz == 60u) { // Exact: 50/3 ms per frame, mod-3 remainder carried across calls. - scaled = (uint32_t)delta * 50u + (uint32_t)rem; - ms = ms + scaled / 3u; - rem = (uint16_t)(scaled % 3u); + // A few frames step one at a time, 16 ms and 2 thirds each: the + // same sum as the divide, without it. + if (delta <= MS_STEP_FRAMES) { + do { + ms = ms + MS60_THIRDS_PER_FRAME / MS60_THIRDS; + rem = (uint16_t)(rem + MS60_THIRDS_PER_FRAME % MS60_THIRDS); + if (rem >= MS60_THIRDS) { + rem = (uint16_t)(rem - MS60_THIRDS); + ms = ms + 1u; + } + delta--; + } while (delta != 0u); + } else { + scaled = (uint32_t)delta * MS60_THIRDS_PER_FRAME + (uint32_t)rem; + ms = ms + scaled / MS60_THIRDS; + rem = (uint16_t)(scaled % MS60_THIRDS); + } } else { // General fallback: 1000/hz ms per frame, mod-hz remainder carried. scaled = (uint32_t)delta * 1000u + (uint32_t)rem; @@ -108,22 +135,27 @@ bool jlpGenericAudioInit(void) { return false; } + void jlpGenericAudioShutdown(void) { } + void jlpGenericAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { (void)data; (void)length; (void)loop; } + void jlpGenericAudioStopMod(void) { } + bool jlpGenericAudioIsPlayingMod(void) { return false; } + void jlpGenericAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { (void)slot; (void)sample; @@ -131,6 +163,7 @@ void jlpGenericAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length (void)rateHz; } + void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { (void)slot; (void)fill; @@ -138,6 +171,7 @@ void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *c (void)rateHz; } + // Mono ports (DOS, ST) have nowhere to put a pan; the header says so. void jlpGenericAudioSetPan(uint8_t slot, uint8_t pan) { (void)slot; @@ -149,29 +183,35 @@ void jlpGenericAudioStopSfx(uint8_t slot) { (void)slot; } + void jlpGenericAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { (void)voice; (void)freqHz; (void)atten; } + void jlpGenericAudioNoise(uint8_t pitch, uint8_t atten) { (void)pitch; (void)atten; } + bool jlpGenericAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { (void)fn; (void)hz; return false; } + void jlpGenericAudioCriticalEnter(void) { } + void jlpGenericAudioCriticalExit(void) { } + void jlpGenericAudioFrameTick(void) { } @@ -183,22 +223,28 @@ bool jlpGenericInit(const jlConfigT *config) { return true; } + void jlpGenericShutdown(void) { } + void jlpGenericPresent(const jlSurfaceT *src) { (void)src; } + void jlpGenericInputInit(void) { } + void jlpGenericInputShutdown(void) { } + void jlpGenericInputPoll(void) { } + void jlpGenericJoystickReset(jlJoystickE js) { (void)js; } diff --git a/src/generic/genericSurface.c b/src/generic/genericSurface.c index 768b8c6..9e1fdc8 100644 --- a/src/generic/genericSurface.c +++ b/src/generic/genericSurface.c @@ -104,14 +104,11 @@ uint8_t jlpGenericSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { uint32_t jlpGenericSurfaceHash(const jlSurfaceT *s) { - uint16_t lo = 0xACE1u; - uint16_t hi = 0x1357u; - uint16_t blocks; - uint16_t n; - uint16_t v; - const uint8_t *p; - const uint16_t *w; - uint8_t b; + uint16_t lo = SURFACE_HASH_SEED_LO; + uint16_t hi = SURFACE_HASH_SEED_HI; + uint16_t blocks; + const uint8_t *p; + uint8_t b; p = s->pixels; blocks = (uint16_t)(JL_SURFACE_PIXELS_SIZE / 8); do { @@ -133,20 +130,7 @@ uint32_t jlpGenericSurfaceHash(const jlSurfaceT *s) { SURFACE_HASH_MIX_BYTE(lo, hi, b); blocks--; } while (blocks > 0u); - p = s->scb; - for (n = 0; n < (uint16_t)JL_SURFACE_HEIGHT; n++) { - b = *p++; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - w = &s->palette[0][0]; - for (n = 0; n < (uint16_t)JL_SURFACE_PALETTE_ENTRIES; n++) { - v = *w++; - b = (uint8_t)((v >> 8) & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - b = (uint8_t)(v & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - return ((uint32_t)hi << 16) | (uint32_t)lo; + return jlSurfaceHashFinish(lo, hi, s); } diff --git a/src/generic/genericTileLayer.c b/src/generic/genericTileLayer.c new file mode 100644 index 0000000..8defbdf --- /dev/null +++ b/src/generic/genericTileLayer.c @@ -0,0 +1,150 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Generic portable-C tile-layer rings: the chunky rings of +// tileLayerInternal.h, `even` and `odd`. A slot's 8x8 tile sits at byte +// slotCol * 4 of pixel rows slotRow * 8 on in `even`, and Put keeps `odd` +// in step: its byte j is even's low nibble of byte j and high nibble of +// byte j + 1, so a slot reaches odd bytes 4c-1 .. 4c+3 (the first and last +// of those also wait on a neighbouring slot, which sets them in turn when +// it is drawn). The blit copies whole bytes from whichever ring starts at +// the band's first pixel, in two runs a row: up to the ring's right edge +// and on from its left. + +#include <string.h> + +#include "joey/tile.h" +#include "port.h" +#include "surfaceInternal.h" +#include "tileLayerInternal.h" + +#define RING_SLOT_BYTES (JL_TILE_PIXELS_PER_SIDE * TILE_LAYER_CHUNKY_STRIDE) + +static uint8_t *ringAlloc(uint8_t ringRows); + + +void jlpGenericTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) { + const TileLayerChunkyRingsT *rings; + const uint8_t *ring; + const uint8_t *src; + uint8_t *d; + uint16_t ringPxRows; + uint16_t rows; + uint16_t srcRow; + uint16_t b; + uint16_t first; + uint16_t i; + + if (dst->pixels == NULL) { + return; + } + rings = (const TileLayerChunkyRingsT *)layer->ring; + ring = ((ringX & 1u) != 0u) ? rings->odd : rings->even; + b = (uint16_t)(ringX >> 1); + first = (uint16_t)(TILE_LAYER_CHUNKY_BYTES - b); + if (first > JL_SURFACE_BYTES_PER_ROW) { + first = JL_SURFACE_BYTES_PER_ROW; + } + ringPxRows = (uint16_t)((uint16_t)layer->ringRows << 3); + rows = (uint16_t)((uint16_t)layer->viewHeight << 3); + srcRow = ringY; + d = &dst->pixels[SURFACE_ROW_OFFSET((uint16_t)layer->viewBy << 3)]; + for (i = 0u; i < rows; i++) { + src = ring + (uint32_t)srcRow * TILE_LAYER_CHUNKY_STRIDE; + memcpy(d, src + b, first); + memcpy(d + first, src, (size_t)(JL_SURFACE_BYTES_PER_ROW - first)); + d += JL_SURFACE_BYTES_PER_ROW; + srcRow++; + if (srcRow == ringPxRows) { + srcRow = 0u; + } + } +} + + +void jlpGenericTileLayerPut(jlTileLayerT *layer, uint8_t slotCol, uint8_t slotRow, const jlTileT *tile) { + TileLayerChunkyRingsT *rings; + uint8_t *e; + uint8_t *o; + const uint8_t *s; + uint16_t c4; + uint16_t before; + uint16_t j; + uint8_t r; + + rings = (TileLayerChunkyRingsT *)layer->ring; + c4 = (uint16_t)((uint16_t)slotCol << 2); + before = (c4 == 0u) ? (uint16_t)(TILE_LAYER_CHUNKY_BYTES - 1u) : (uint16_t)(c4 - 1u); + e = rings->even + (uint32_t)slotRow * RING_SLOT_BYTES; + o = rings->odd + (uint32_t)slotRow * RING_SLOT_BYTES; + s = tile->pixels; + for (r = 0u; r < JL_TILE_PIXELS_PER_SIDE; r++) { + memcpy(e + c4, s, JL_TILE_BYTES_PER_ROW); + if (c4 == 0u) { + e[TILE_LAYER_CHUNKY_BYTES] = e[0]; + e[TILE_LAYER_CHUNKY_BYTES + 1u] = e[1]; + } + // The guard is e[TILE_LAYER_CHUNKY_BYTES], so j + 1 never wraps. + o[before] = (uint8_t)((uint8_t)(e[before] << 4) | (uint8_t)(e[before + 1u] >> 4)); + for (j = c4; j < c4 + JL_TILE_BYTES_PER_ROW; j++) { + o[j] = (uint8_t)((uint8_t)(e[j] << 4) | (uint8_t)(e[j + 1u] >> 4)); + } + if (c4 == 0u) { + o[TILE_LAYER_CHUNKY_BYTES] = o[0]; + o[TILE_LAYER_CHUNKY_BYTES + 1u] = o[1]; + } + e += TILE_LAYER_CHUNKY_STRIDE; + o += TILE_LAYER_CHUNKY_STRIDE; + s += JL_TILE_BYTES_PER_ROW; + } +} + + +bool jlpGenericTileLayerRingAlloc(jlTileLayerT *layer) { + TileLayerChunkyRingsT *rings; + + rings = (TileLayerChunkyRingsT *)jlpBigAlloc((uint32_t)sizeof(TileLayerChunkyRingsT)); + if (rings == NULL) { + return false; + } + rings->even = ringAlloc(layer->ringRows); + rings->odd = ringAlloc(layer->ringRows); + if (rings->even == NULL || rings->odd == NULL) { + jlpBigFree(rings->even); + jlpBigFree(rings->odd); + jlpBigFree(rings); + return false; + } + layer->ring = rings; + return true; +} + + +void jlpGenericTileLayerRingFree(jlTileLayerT *layer) { + TileLayerChunkyRingsT *rings; + + rings = (TileLayerChunkyRingsT *)layer->ring; + if (rings == NULL) { + return; + } + jlpBigFree(rings->even); + jlpBigFree(rings->odd); + jlpBigFree(rings); + layer->ring = NULL; +} + + +// One ring. On the IIgs jlpBigAlloc keeps a block under 64 KB inside one +// bank, which the blit's abs,Y reads need. +static uint8_t *ringAlloc(uint8_t ringRows) { + uint8_t *ring; + uint32_t bytes; + + bytes = (uint32_t)ringRows * RING_SLOT_BYTES; + ring = (uint8_t *)jlpBigAlloc(bytes); + if (ring != NULL) { + memset(ring, 0, (size_t)bytes); + } + return ring; +} diff --git a/src/generic/spriteEmitStub.c b/src/generic/spriteEmitStub.c index 99f06a2..ac9c659 100644 --- a/src/generic/spriteEmitStub.c +++ b/src/generic/spriteEmitStub.c @@ -4,7 +4,7 @@ // // Stub sprite codegen. Used by ports that don't yet have a per-CPU // emitter so the link still resolves. jlSpriteCompile always returns -// false (sprite stays interpreter-only); spriteCompiledDraw is +// false (sprite stays interpreter-only); jlSpriteCompiledDraw is // unreachable because the dispatcher in src/core/sprite.c gates the // call on sp->slot != NULL. @@ -19,7 +19,7 @@ bool jlSpriteCompile(jlSpriteT *sp) { } -void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { +void jlSpriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { (void)dst; (void)sp; (void)x; diff --git a/src/hosted/audio.c b/src/hosted/audio.c index d85a73c..493cfd2 100644 --- a/src/hosted/audio.c +++ b/src/hosted/audio.c @@ -19,10 +19,10 @@ // The mix is made on the program's thread, as DOS makes its own: the // library's frequent calls (present, the frame wait, the input poll, the // frame tick) keep the backend's queue AUDIO_QUEUE_FRAMES deep -// (hostedAudioService). So the stream fill callbacks run on the program's +// (jlHostedAudioService). So the stream fill callbacks run on the program's // thread as they do on DOS and the IIgs, and a tick callback // (jlAudioTickRegister) is called between the very samples it falls -// between (psgSynthTickRun): notes start where they should in the sound, +// between (jlPsgSynthTickRun): notes start where they should in the sound, // however the mixing is batched. The critical sections stay the generic // no-ops. @@ -71,7 +71,7 @@ static bool panReaches(uint8_t pan, uint8_t channel); // ----- Module state ----- static bool gReady = false; -static uint32_t gRate = 0u; // the backend's rate (hostBackendAudioOpen), 0 until open +static uint32_t gRate = 0u; // the backend's rate (jlHostBackendAudioOpen), 0 until open // A song is playing until the backend has played its last frame, not just // until the last one is mixed: frames handed to the backend, a song that // ended in the chunk being mixed, and the count at the end of that chunk. @@ -111,10 +111,10 @@ static void mixFrames(int16_t *out, uint32_t frames) { done = 0; while (done < frames) { - run = psgSynthTickRun(frames - done); + run = jlPsgSynthTickRun(frames - done); memset(gMix, 0, run * HOSTED_AUDIO_CHANNELS * sizeof(gMix[0])); mixModInto(run); - psgSynthMix(gMix, run, (uint8_t)HOSTED_AUDIO_CHANNELS); + jlPsgSynthMix(gMix, run, (uint8_t)HOSTED_AUDIO_CHANNELS); mixSfxInto(run); for (i = 0; i < run * HOSTED_AUDIO_CHANNELS; i++) { out[done * HOSTED_AUDIO_CHANNELS + i] = clampSample(gMix[i]); @@ -130,10 +130,10 @@ static void mixModInto(uint32_t frames) { int32_t right; bool ended; - if (!modPlayerActive()) { + if (!jlModPlayerActive()) { return; } - ended = !modPlayerRenderS16(gModBuf, frames, (uint8_t)MOD_SIDES); + ended = !jlModPlayerRenderS16(gModBuf, frames, (uint8_t)MOD_SIDES); for (i = 0; i < frames; i++) { left = gModBuf[i * MOD_SIDES]; right = gModBuf[i * MOD_SIDES + 1u]; @@ -145,7 +145,7 @@ static void mixModInto(uint32_t frames) { } } // A song that has ended (not looping) has stopped the player; it sounds - // on until this chunk has played (hostedAudioService). + // on until this chunk has played (jlHostedAudioService). if (ended) { gModEnding = true; } @@ -167,7 +167,7 @@ static void mixSfxInto(uint32_t frames) { continue; } memset(gSfxBuf, SFX_BIAS, frames); - audioSfxOverlayMix(gSfxBuf, (uint16_t)frames, &gSfxSlots[s], 1u); + jlAudioSfxOverlayMix(gSfxBuf, (uint16_t)frames, &gSfxSlots[s], 1u); for (c = 0; c < HOSTED_AUDIO_CHANNELS; c++) { reaches[c] = panReaches(gSfxPan[s], c); } @@ -192,21 +192,21 @@ static bool panReaches(uint8_t pan, uint8_t channel) { // ----- Hosted core API (alphabetical) ----- -void hostedAudioService(void) { +void jlHostedAudioService(void) { uint32_t queued; uint32_t frames; if (!gReady) { return; } - queued = hostBackendAudioQueued(); + queued = jlHostBackendAudioQueued(); while (queued < AUDIO_QUEUE_FRAMES) { frames = AUDIO_QUEUE_FRAMES - queued; if (frames > AUDIO_CHUNK_FRAMES) { frames = AUDIO_CHUNK_FRAMES; } mixFrames(gOut, frames); - hostBackendAudioQueue(gOut, frames); + jlHostBackendAudioQueue(gOut, frames); queued += frames; gFramesQueued += frames; if (gModEnding) { @@ -220,7 +220,7 @@ void hostedAudioService(void) { // ----- HAL API (alphabetical) ----- void jlpAudioFrameTick(void) { - hostedAudioService(); + jlHostedAudioService(); } @@ -228,11 +228,11 @@ bool jlpAudioInit(void) { if (gReady) { return true; } - gRate = hostBackendAudioOpen(); + gRate = jlHostBackendAudioOpen(); if (gRate == 0u) { return false; } - psgSynthInit(gRate); + jlPsgSynthInit(gRate); memset(gSfxSlots, 0, sizeof(gSfxSlots)); memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan)); gReady = true; @@ -241,11 +241,11 @@ bool jlpAudioInit(void) { bool jlpAudioIsPlayingMod(void) { - if (modPlayerActive()) { + if (jlModPlayerActive()) { return true; } // Ended, and playing out what is queued: until the backend has played up to the end. - if (gModTail && (int32_t)(gModEndFrame - (gFramesQueued - hostBackendAudioQueued())) > 0) { + if (gModTail && (int32_t)(gModEndFrame - (gFramesQueued - jlHostBackendAudioQueued())) > 0) { return true; } gModTail = false; @@ -254,7 +254,7 @@ bool jlpAudioIsPlayingMod(void) { void jlpAudioNoise(uint8_t pitch, uint8_t atten) { - psgSynthNoise(pitch, atten); + jlPsgSynthNoise(pitch, atten); } @@ -264,7 +264,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { } gModEnding = false; gModTail = false; - (void)modPlayerStart(data, length, gRate, loop); + (void)jlModPlayerStart(data, length, gRate, loop); } @@ -272,7 +272,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gRate); + jlAudioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gRate); } @@ -280,7 +280,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gRate); + jlAudioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gRate); } @@ -296,9 +296,9 @@ void jlpAudioShutdown(void) { if (!gReady) { return; } - modPlayerStop(); - (void)psgSynthTickRegister(NULL, 0u); - hostBackendAudioClose(); + jlModPlayerStop(); + (void)jlPsgSynthTickRegister(NULL, 0u); + jlHostBackendAudioClose(); gReady = false; } @@ -307,7 +307,7 @@ void jlpAudioStopMod(void) { if (!gReady) { return; } - modPlayerStop(); + jlModPlayerStop(); gModEnding = false; gModTail = false; } @@ -322,10 +322,10 @@ void jlpAudioStopSfx(uint8_t slot) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { - return psgSynthTickRegister(fn, hz); + return jlPsgSynthTickRegister(fn, hz); } void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { - psgSynthVoice(voice, freqHz, atten); + jlPsgSynthVoice(voice, freqHz, atten); } diff --git a/src/hosted/be/beBackend.cpp b/src/hosted/be/beBackend.cpp index 5ff6135..32fb896 100644 --- a/src/hosted/be/beBackend.cpp +++ b/src/hosted/be/beBackend.cpp @@ -7,19 +7,19 @@ // (gcc 2.95, the BeOS C++ ABI), kept to calls BeOS R5 already had. // // Threads: the program's main() owns the main thread, and every call into -// the core -- hostedKeyEvent and the rest -- is made on it. The +// the core -- jlHostedKeyEvent and the rest -- is made on it. The // BApplication runs on a thread of its own, started by the first -// hostBackendOpen, and the window on its own, as every BWindow does. The +// jlHostBackendOpen, and the window on its own, as every BWindow does. The // window's view turns key and mouse messages, and a request to close the -// window, into events in a locked queue, which hostBackendPump drains on the +// window, into events in a locked queue, which jlHostBackendPump drains on the // program's thread. Frames are drawn from the program's thread with the // window locked. Sound comes from a BSoundPlayer, whose thread takes it from -// a locked ring that hostBackendAudioQueue fills. +// a locked ring that jlHostBackendAudioQueue fills. // // The window shows the 320x200 frame from a BBitmap scaled to fill it at the // stage's shape: 4:3 by default, as the machine ports' 320x200 fills a 4:3 // monitor, or square pixels (16:10) with JOEYLIB_ASPECT=square; the rest is -// black. The backend scales the frame itself (hostedScaleMaps: each window +// black. The backend scales the frame itself (jlHostedScaleMaps: each window // pixel shows one stage pixel, so the pixels stay sharp) into a bitmap the // size of the frame's place, which the app_server only copies: its own // scaling rounds part of a bitmap differently from the whole on some @@ -116,7 +116,7 @@ extern "C" { // What one DrawBitmap's fixed cost (a round trip to the app_server) is worth // in window pixels: changed rectangles closer than that are drawn as one -// (hostedRectsMerge). An estimate -- there is no R5-era machine here to +// (jlHostedRectsMerge). An estimate -- there is no R5-era machine here to // measure it on; Win16's, measured on DOSBox's 386, is 1536. #define MERGE_CALL_OUT_PIXELS 4096u @@ -215,7 +215,7 @@ typedef enum { typedef struct { EventKindE kind; - uint8_t code; // the key (hostedKeyEvent's), character, or jlMouseButtonE + uint8_t code; // the key (jlHostedKeyEvent's), character, or jlMouseButtonE bool down; int16_t x; int16_t y; @@ -228,9 +228,9 @@ typedef struct { } KeyRowT; // The view the frame is drawn in, which takes the window's input. -class JoeyViewT : public BView { +class jlJoeyViewT : public BView { public: - JoeyViewT(BRect frame); + jlJoeyViewT(BRect frame); virtual void Draw(BRect updateRect); virtual void FrameResized(float width, float height); virtual void KeyDown(const char *bytes, int32 numBytes); @@ -244,9 +244,9 @@ public: void drawRect(const HostedRectT *rect); }; -class JoeyWindowT : public BWindow { +class jlJoeyWindowT : public BWindow { public: - JoeyWindowT(BRect frame, const char *title, window_look look); + jlJoeyWindowT(BRect frame, const char *title, window_look look); virtual bool QuitRequested(void); }; @@ -283,7 +283,7 @@ static BRect windowFrame(void); // UBER's display probe (examples/uber with UBER_PROBE). extern "C" { -uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +uint32_t jlHostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); } // ----- Module state ----- @@ -297,16 +297,16 @@ static const uint32 kMouseButtons[] = { B_PRIMARY_MOUSE static const jlMouseButtonE kMouseButtonIds[] = { JL_MOUSE_BUTTON_LEFT, JL_MOUSE_BUTTON_RIGHT, JL_MOUSE_BUTTON_MIDDLE }; static const uint8 kHiddenCursor[CURSOR_DATA_BYTES] = { CURSOR_SIZE, CURSOR_DEPTH }; -// The program, the application and the window. Set up by hostBackendOpen. -static char gProgramName[B_FILE_NAME_LENGTH]; -static char gProgramFolder[B_PATH_NAME_LENGTH]; -static char gSignature[B_MIME_TYPE_LENGTH]; -static thread_id gAppThread = -1; -static sem_id gAppReady = -1; -static status_t gAppStatus = B_NO_INIT; -static JoeyWindowT *gWindow = NULL; -static JoeyViewT *gView = NULL; -static BBitmap *gBitmap = NULL; +// The program, the application and the window. Set up by jlHostBackendOpen. +static char gProgramName[B_FILE_NAME_LENGTH]; +static char gProgramFolder[B_PATH_NAME_LENGTH]; +static char gSignature[B_MIME_TYPE_LENGTH]; +static thread_id gAppThread = -1; +static sem_id gAppReady = -1; +static status_t gAppStatus = B_NO_INIT; +static jlJoeyWindowT *gWindow = NULL; +static jlJoeyViewT *gView = NULL; +static BBitmap *gBitmap = NULL; // The core's frame the bitmap mirrors, and whether the next show copies and // draws all of it rather than what changed. static const uint32_t *gLastFrame = NULL; @@ -354,26 +354,26 @@ static bigtime_t gSticksScanned = 0; // The view paints every pixel itself (drawFrame), so the app_server is not // asked to clear it first. -JoeyViewT::JoeyViewT(BRect frame) : BView(frame, "JoeyLib", B_FOLLOW_ALL, B_WILL_DRAW | B_FRAME_EVENTS) { +jlJoeyViewT::jlJoeyViewT(BRect frame) : BView(frame, "JoeyLib", B_FOLLOW_ALL, B_WILL_DRAW | B_FRAME_EVENTS) { SetViewColor(B_TRANSPARENT_COLOR); SetViewCursor(gCursor); } -void JoeyViewT::Draw(BRect updateRect) { +void jlJoeyViewT::Draw(BRect updateRect) { (void)updateRect; drawFrame(); } -void JoeyViewT::FrameResized(float width, float height) { +void jlJoeyViewT::FrameResized(float width, float height) { (void)width; (void)height; Invalidate(); } -void JoeyViewT::KeyDown(const char *bytes, int32 numBytes) { +void jlJoeyViewT::KeyDown(const char *bytes, int32 numBytes) { BMessage *message; int32 raw; int32 mods; @@ -392,7 +392,7 @@ void JoeyViewT::KeyDown(const char *bytes, int32 numBytes) { } // Typed characters, repeats too. A character event's down is false for a // chord (Control, or Command, Be's shortcut key), which types nothing - // (inputCharTyped); Option is a layer of characters. + // (jlInputCharTyped); Option is a layer of characters. if (numBytes == 1 && (uint8_t)bytes[0] < ASCII_LIMIT) { ch = (uint8_t)bytes[0] == B_RETURN ? (uint8_t)JL_CHAR_RETURN : (uint8_t)bytes[0]; eventPush(EVENT_CHAR, ch, ((uint32)mods & (B_CONTROL_KEY | B_COMMAND_KEY)) == 0, 0, 0); @@ -401,7 +401,7 @@ void JoeyViewT::KeyDown(const char *bytes, int32 numBytes) { } -void JoeyViewT::KeyUp(const char *bytes, int32 numBytes) { +void jlJoeyViewT::KeyUp(const char *bytes, int32 numBytes) { BMessage *message; (void)bytes; @@ -414,7 +414,7 @@ void JoeyViewT::KeyUp(const char *bytes, int32 numBytes) { // Keys that give no character (Shift, Ctrl, Alt) come as unmapped keys. -void JoeyViewT::MessageReceived(BMessage *message) { +void jlJoeyViewT::MessageReceived(BMessage *message) { switch (message->what) { case B_UNMAPPED_KEY_DOWN: keyMessage(message, true); @@ -433,14 +433,14 @@ void JoeyViewT::MessageReceived(BMessage *message) { // The pointer's messages keep coming while a button is held, even off the // window. -void JoeyViewT::MouseDown(BPoint where) { +void jlJoeyViewT::MouseDown(BPoint where) { SetMouseEventMask(B_POINTER_EVENTS, 0); pointerAt(where); buttonsChanged(Window()->CurrentMessage()); } -void JoeyViewT::MouseMoved(BPoint where, uint32 transit, const BMessage *dragMessage) { +void jlJoeyViewT::MouseMoved(BPoint where, uint32 transit, const BMessage *dragMessage) { (void)transit; (void)dragMessage; pointerAt(where); @@ -448,13 +448,13 @@ void JoeyViewT::MouseMoved(BPoint where, uint32 transit, const BMessage *dragMes } -void JoeyViewT::MouseUp(BPoint where) { +void jlJoeyViewT::MouseUp(BPoint where) { pointerAt(where); buttonsChanged(Window()->CurrentMessage()); } -void JoeyViewT::WindowActivated(bool active) { +void jlJoeyViewT::WindowActivated(bool active) { if (!active) { keysRelease(); } @@ -464,7 +464,7 @@ void JoeyViewT::WindowActivated(bool active) { // The whole mirror scaled afresh into the frame's place in the view, black // around it (or, past the scaled bitmap's reach, the mirror scaled by the // app_server). The window is locked. -void JoeyViewT::drawFrame(void) { +void jlJoeyViewT::drawFrame(void) { static const HostedRectT whole = { 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT }; BRect bounds; BRect dst; @@ -487,7 +487,7 @@ void JoeyViewT::drawFrame(void) { // Stage rectangle rect of the mirror scaled into the frame's place in the // view; everything when the scaled bitmap is new. The window is locked. -void JoeyViewT::drawRect(const HostedRectT *rect) { +void jlJoeyViewT::drawRect(const HostedRectT *rect) { HostedRectT span; BRect dst; @@ -497,18 +497,18 @@ void JoeyViewT::drawRect(const HostedRectT *rect) { return; } scaledUpdate(rect); - hostedScaleRect(rect, gScaledW, gScaledH, &span); + jlHostedScaleRect(rect, gScaledW, gScaledH, &span); DrawBitmap(gScaled, BRect((float)span.x, (float)span.y, (float)(span.x + span.w - 1u), (float)(span.y + span.h - 1u)), BRect(dst.left + (float)span.x, dst.top + (float)span.y, dst.left + (float)(span.x + span.w - 1u), dst.top + (float)(span.y + span.h - 1u))); } -JoeyWindowT::JoeyWindowT(BRect frame, const char *title, window_look look) : BWindow(frame, title, look, B_NORMAL_WINDOW_FEEL, 0) { +jlJoeyWindowT::jlJoeyWindowT(BRect frame, const char *title, window_look look) : BWindow(frame, title, look, B_NORMAL_WINDOW_FEEL, 0) { } // Closing the window asks the program to end; it does at its next event // pump, which takes the window down on the way out. -bool JoeyWindowT::QuitRequested(void) { +bool jlJoeyWindowT::QuitRequested(void) { if (gEventLock.Lock()) { gQuitAsked = true; gEventLock.Unlock(); @@ -595,7 +595,7 @@ static void audioPlay(void *cookie, void *buffer, size_t size, const media_raw_a static void backendExit(void) { status_t status; - hostBackendClose(); + jlHostBackendClose(); delete gCursor; gCursor = NULL; if (gAppThread >= 0) { @@ -748,7 +748,7 @@ static void iconAttributesEnsure(void) { } -// The key a raw key code gives (hostedKeyEvent's: the second of a key's two +// The key a raw key code gives (jlHostedKeyEvent's: the second of a key's two // with INPUT_KEY_SECOND). static uint8_t keyFromRaw(int32 raw) { uint32 row; @@ -960,7 +960,7 @@ static int scaledFit(uint16_t w, uint16_t h) { gScaled = NULL; return SCALED_NONE; } - hostedScaleMaps(w, h, gColMap, gRowMap); + jlHostedScaleMaps(w, h, gColMap, gRowMap); gScaledW = w; gScaledH = h; return SCALED_NEW; @@ -980,7 +980,7 @@ static void scaledUpdate(const HostedRectT *rect) { uint16_t oy; uint16_t i; - hostedScaleRect(rect, gScaledW, gScaledH, &span); + jlHostedScaleRect(rect, gScaledW, gScaledH, &span); mirror = (const uint8_t *)gBitmap->Bits(); mirrorRowBytes = gBitmap->BytesPerRow(); scaled = (uint8_t *)gScaled->Bits(); @@ -1001,7 +1001,7 @@ static void scaledUpdate(const HostedRectT *rect) { // One stick axis (BJoystick's -32768..32767) in JoeyLib's range. static int8_t stickAxis(int16 raw) { - return inputAxisScale(raw, INT16_MIN, INT16_MAX); + return jlInputAxisScale(raw, INT16_MIN, INT16_MAX); } @@ -1011,7 +1011,7 @@ static void stickDrop(int slot) { delete gSticks[slot]; gSticks[slot] = NULL; gStickNames[slot][0] = '\0'; - hostedJoystickConnect((jlJoystickE)slot, false); + jlHostedJoystickConnect((jlJoystickE)slot, false); } @@ -1072,7 +1072,7 @@ static void sticksOpen(void) { } memcpy(gStickNames[slot], name, sizeof(gStickNames[slot])); // the same size as name gStickNames[slot][sizeof(gStickNames[slot]) - 1] = '\0'; - hostedJoystickConnect((jlJoystickE)slot, true); + jlHostedJoystickConnect((jlJoystickE)slot, true); } } @@ -1103,9 +1103,9 @@ static void sticksPoll(void) { continue; } buttons = gSticks[slot]->ButtonValues(); - hostedJoystickMove((jlJoystickE)slot, stickAxis(axes[0]), stickAxis(axes[1])); - hostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_0, (buttons & STICK_BUTTON_0_MASK) != 0); - hostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_1, (buttons & STICK_BUTTON_1_MASK) != 0); + jlHostedJoystickMove((jlJoystickE)slot, stickAxis(axes[0]), stickAxis(axes[1])); + jlHostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_0, (buttons & STICK_BUTTON_0_MASK) != 0); + jlHostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_1, (buttons & STICK_BUTTON_1_MASK) != 0); } } @@ -1134,7 +1134,7 @@ static BRect windowFrame(void) { // ----- Backend API (alphabetical) ----- -void hostBackendAudioClose(void) { +void jlHostBackendAudioClose(void) { if (gPlayer != NULL) { gPlayer->Stop(); delete gPlayer; @@ -1145,7 +1145,7 @@ void hostBackendAudioClose(void) { } -uint32_t hostBackendAudioOpen(void) { +uint32_t jlHostBackendAudioOpen(void) { media_raw_audio_format format; #if defined(__HAIKU__) @@ -1178,7 +1178,7 @@ uint32_t hostBackendAudioOpen(void) { // Into the ring, as much as fits (the core never queues more than it holds). -void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { +void jlHostBackendAudioQueue(const int16_t *frames, uint32_t count) { uint32_t room; uint32_t i; @@ -1197,7 +1197,7 @@ void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { } -uint32_t hostBackendAudioQueued(void) { +uint32_t jlHostBackendAudioQueued(void) { uint32_t queued; if (!gAudioLock.Lock()) { @@ -1209,8 +1209,8 @@ uint32_t hostBackendAudioQueued(void) { } -void hostBackendClose(void) { - hostBackendAudioClose(); +void jlHostBackendClose(void) { + jlHostBackendAudioClose(); sticksClose(); if (gWindow != NULL && gWindow->Lock()) { gWindow->Quit(); @@ -1227,23 +1227,23 @@ void hostBackendClose(void) { } -void hostBackendInputPoll(void) { +void jlHostBackendInputPoll(void) { sticksPoll(); } // The Be desktop is driven by its pointer: there is always one. -bool hostBackendMousePresent(void) { +bool jlHostBackendMousePresent(void) { return true; } -uint64_t hostBackendMicros(void) { +uint64_t jlHostBackendMicros(void) { return (uint64_t)system_time(); } -const char *hostBackendOpen(void) { +const char *jlHostBackendOpen(void) { const char *aspect; // The program's own folder is the current directory, as on every @@ -1289,9 +1289,9 @@ const char *hostBackendOpen(void) { gQuitAsked = false; gEventHead = 0; gEventTail = 0; - gWindow = new JoeyWindowT(windowFrame(), gProgramName, gFullScreen ? B_NO_BORDER_WINDOW_LOOK : B_TITLED_WINDOW_LOOK); + gWindow = new jlJoeyWindowT(windowFrame(), gProgramName, gFullScreen ? B_NO_BORDER_WINDOW_LOOK : B_TITLED_WINDOW_LOOK); gWindowedFrame = gWindow->Frame(); - gView = new JoeyViewT(gWindow->Bounds()); + gView = new jlJoeyViewT(gWindow->Bounds()); gWindow->Lock(); gWindow->AddChild(gView); gView->MakeFocus(true); @@ -1307,7 +1307,7 @@ const char *hostBackendOpen(void) { // The executable's folder, with its trailing separator, found once. -const char *hostBackendProgramFolder(void) { +const char *jlHostBackendProgramFolder(void) { static char root[B_PATH_NAME_LENGTH]; size_t length; @@ -1323,7 +1323,7 @@ const char *hostBackendProgramFolder(void) { } -void hostBackendPump(void) { +void jlHostBackendPump(void) { EventT events[EVENT_QUEUE_SIZE]; uint32_t count; uint32_t i; @@ -1333,19 +1333,19 @@ void hostBackendPump(void) { for (i = 0; i < count; i++) { switch (events[i].kind) { case EVENT_KEY: - hostedKeyEvent(events[i].code, events[i].down); + jlHostedKeyEvent(events[i].code, events[i].down); break; case EVENT_CHAR: - inputCharTyped(events[i].code, !events[i].down); + jlInputCharTyped(events[i].code, !events[i].down); break; case EVENT_MOUSE_MOVE: - hostedMouseMove(events[i].x, events[i].y); + jlHostedMouseMove(events[i].x, events[i].y); break; case EVENT_MOUSE_BUTTON: - hostedMouseButtonEvent((jlMouseButtonE)events[i].code, events[i].down); + jlHostedMouseButtonEvent((jlMouseButtonE)events[i].code, events[i].down); break; } } @@ -1360,7 +1360,7 @@ void hostBackendPump(void) { } -void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t rectCount) { +void jlHostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t rectCount) { static const HostedRectT whole = { 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT }; static HostedRectT merged[JL_SURFACE_HEIGHT]; BRect dst; @@ -1385,7 +1385,7 @@ void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t } dst = frameRect(gView->Bounds()); outPixels = (uint32_t)(dst.Width() + 1.0f) * (uint32_t)(dst.Height() + 1.0f); - count = hostedRectsMerge(rects, rectCount, outPixels == 0u ? 0u : MERGE_CALL_OUT_PIXELS * (uint32_t)(JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT) / outPixels, merged); + count = jlHostedRectsMerge(rects, rectCount, outPixels == 0u ? 0u : MERGE_CALL_OUT_PIXELS * (uint32_t)(JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT) / outPixels, merged); for (i = 0; i < count; i++) { gView->drawRect(&merged[i]); } @@ -1396,7 +1396,7 @@ void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t } -void hostBackendSleepUntil(uint64_t micros) { +void jlHostBackendSleepUntil(uint64_t micros) { if ((bigtime_t)micros > system_time()) { snooze_until((bigtime_t)micros, B_SYSTEM_TIMEBASE); } @@ -1409,7 +1409,7 @@ uint32_t jlpDiskFree(void) { fs_info info; uint64_t bytes; - if (fs_stat_dev(dev_for_path(hostBackendProgramFolder()), &info) != 0) { + if (fs_stat_dev(dev_for_path(jlHostBackendProgramFolder()), &info) != 0) { return 0u; } bytes = (uint64_t)info.free_blocks * (uint64_t)info.block_size; @@ -1440,7 +1440,7 @@ bool jlpSaveDirEnsure(const char *dir) { // after a full repaint, both read back from the screen. The window is left // fully repainted. Logs the window's size. *compared: every pixel // compared. -uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { +uint32_t jlHostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { const uint8_t *bits; BRect dst; BRect onScreen; @@ -1458,7 +1458,7 @@ uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { if (gLastFrame == NULL || gWindow == NULL || !gWindow->Lock()) { return 0; } - mismatches = hostedProbeFrameMismatch(src); + mismatches = jlHostedProbeFrameMismatch(src); bits = (const uint8_t *)gBitmap->Bits(); rowBytes = gBitmap->BytesPerRow(); for (y = 0; y < JL_SURFACE_HEIGHT; y++) { @@ -1487,7 +1487,7 @@ uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { gWindow->Unlock(); } screen.ReadBitmap(repainted, false, &onScreen); - mismatches += hostedProbeDiffer((const uint8_t *)piecemeal->Bits(), (const uint8_t *)repainted->Bits(), w, h, (uint16_t)piecemeal->BytesPerRow(), PROBE_BITS_PER_PIXEL); + mismatches += jlHostedProbeDiffer((const uint8_t *)piecemeal->Bits(), (const uint8_t *)repainted->Bits(), w, h, (uint16_t)piecemeal->BytesPerRow(), PROBE_BITS_PER_PIXEL); *compared += (uint32_t)w * h; } delete repainted; diff --git a/src/hosted/hal.c b/src/hosted/hal.c index 093a79e..0b377cf 100644 --- a/src/hosted/hal.c +++ b/src/hosted/hal.c @@ -93,7 +93,7 @@ static void show(void); #if defined(HOSTED_FRAME_INDEXED) // The stage the last present showed, which the backend's frame can be -// filled from again (hostedFrameRefresh). +// filled from again (jlHostedFrameRefresh). static const jlSurfaceT *gShownStage = NULL; #else static HostedPixelT gFrame[JL_SURFACE_HEIGHT * JL_SURFACE_WIDTH]; @@ -112,7 +112,7 @@ static uint16_t gRectCount = 0; static uint32_t gLut[JL_SURFACE_PALETTE_COUNT][JL_SURFACE_COLORS_PER_PALETTE]; #if defined(HOSTED_FRAME_INDEXED) // The palettes the SCBs used when the backend last laid its frame out for -// them (hostBackendPaletteBases; PALETTES_NONE before the first present). +// them (jlHostBackendPaletteBases; PALETTES_NONE before the first present). static uint16_t gUsedPalettes = PALETTES_NONE; #endif // The virtual frame the window last showed (FRAME_NONE: none yet). @@ -171,7 +171,7 @@ static void expandRow(const jlSurfaceT *src, uint16_t y, uint16_t x0, uint16_t x in = &src->pixels[SURFACE_ROW_OFFSET(y) + x0 / STAGE_PIXELS_PER_BYTE]; #if defined(HOSTED_FRAME_INDEXED) - hostBackendFrameRow(in, src->scb[y], y, x0, x1); + jlHostBackendFrameRow(in, src->scb[y], y, x0, x1); #else palette = (uint8_t)(src->scb[y] & NIBBLE_MASK); out = &gFrame[FRAME_ROW(y) + x0]; @@ -200,13 +200,13 @@ static void frameBoundaryWait(void) { next = (frameNow() + 1u) * US_PER_SECOND / HOSTED_FRAME_HZ; // One microsecond past the rounded-down boundary is inside the new frame. - hostBackendSleepUntil(gStartUs + next + 1u); + jlHostBackendSleepUntil(gStartUs + next + 1u); } // The virtual frame the clock is in. static uint64_t frameNow(void) { - return (hostBackendMicros() - gStartUs) * HOSTED_FRAME_HZ / US_PER_SECOND; + return (jlHostBackendMicros() - gStartUs) * HOSTED_FRAME_HZ / US_PER_SECOND; } @@ -235,7 +235,7 @@ static void lutBuild(const jlSurfaceT *src, uint16_t palettes) { } } #if defined(HOSTED_FRAME_INDEXED) - hostBackendColors(&gLut[0][0], palettes); + jlHostBackendColors(&gLut[0][0], palettes); #endif } @@ -268,19 +268,20 @@ static uint32_t rgb12To24(uint16_t rgb) { static void show(void) { - hostBackendPump(); + jlHostBackendPump(); changedRects(); #if defined(HOSTED_FRAME_INDEXED) - hostBackendShow(gRects, gRectCount); + jlHostBackendShow(gRects, gRectCount); #else - hostBackendShow(gFrame, gRects, gRectCount); + jlHostBackendShow(gFrame, gRects, gRectCount); #endif gShownFrame = frameNow(); } + // ----- Hosted core API (alphabetical) ----- -uint32_t hostedProbeDiffer(const uint8_t *a, const uint8_t *b, uint16_t w, uint16_t h, uint16_t rowBytes, uint16_t bitsPixel) { +uint32_t jlHostedProbeDiffer(const uint8_t *a, const uint8_t *b, uint16_t w, uint16_t h, uint16_t rowBytes, uint16_t bitsPixel) { uint32_t mismatches; uint16_t unitBytes; uint16_t shownBytes; @@ -304,7 +305,7 @@ uint32_t hostedProbeDiffer(const uint8_t *a, const uint8_t *b, uint16_t w, uint1 #if defined(HOSTED_FRAME_INDEXED) -uint8_t hostedProbeStagePixel(const jlSurfaceT *src, uint16_t x, uint16_t y) { +uint8_t jlHostedProbeStagePixel(const jlSurfaceT *src, uint16_t x, uint16_t y) { uint8_t pair; pair = src->pixels[SURFACE_ROW_OFFSET(y) + x / STAGE_PIXELS_PER_BYTE]; @@ -313,27 +314,27 @@ uint8_t hostedProbeStagePixel(const jlSurfaceT *src, uint16_t x, uint16_t y) { #endif -uint32_t hostedProbeStageRgb(const jlSurfaceT *src, uint8_t palette, uint8_t color) { +uint32_t jlHostedProbeStageRgb(const jlSurfaceT *src, uint8_t palette, uint8_t color) { return rgb12To24(src->palette[palette][color]); } #if defined(HOSTED_FRAME_INDEXED) -void hostedFrameRefresh(void) { +void jlHostedFrameRefresh(void) { uint16_t y; if (gShownStage == NULL) { return; } for (y = 0; y < JL_SURFACE_HEIGHT; y++) { - hostBackendFrameRow(&gShownStage->pixels[SURFACE_ROW_OFFSET(y)], gShownStage->scb[y], y, 0, JL_SURFACE_WIDTH); + jlHostBackendFrameRow(&gShownStage->pixels[SURFACE_ROW_OFFSET(y)], gShownStage->scb[y], y, 0, JL_SURFACE_WIDTH); } } #endif #if !defined(HOSTED_FRAME_INDEXED) -uint32_t hostedProbeFrameMismatch(const jlSurfaceT *src) { +uint32_t jlHostedProbeFrameMismatch(const jlSurfaceT *src) { uint32_t mismatches; uint16_t x; uint16_t y; @@ -359,7 +360,7 @@ uint32_t hostedProbeFrameMismatch(const jlSurfaceT *src) { // Greedy, top to bottom: the next rect joins the box being built when the // pixels the bigger box adds beyond both are worth less than a draw. -uint16_t hostedRectsMerge(const HostedRectT *rects, uint16_t count, uint32_t callPixels, HostedRectT *out) { +uint16_t jlHostedRectsMerge(const HostedRectT *rects, uint16_t count, uint32_t callPixels, HostedRectT *out) { HostedRectT box; const HostedRectT *next; uint32_t grown; @@ -393,7 +394,7 @@ uint16_t hostedRectsMerge(const HostedRectT *rects, uint16_t count, uint32_t cal // Output column ox shows stage column ox * W / outW (rows likewise). -void hostedScaleMaps(uint16_t outW, uint16_t outH, uint16_t *colMap, uint16_t *rowMap) { +void jlHostedScaleMaps(uint16_t outW, uint16_t outH, uint16_t *colMap, uint16_t *rowMap) { uint16_t i; for (i = 0; i < outW; i++) { @@ -407,7 +408,7 @@ void hostedScaleMaps(uint16_t outW, uint16_t outH, uint16_t *colMap, uint16_t *r // The output columns showing stage columns x..x+w-1 are ceil(x * outW / W) // up to ceil((x + w) * outW / W) (rows likewise). -void hostedScaleRect(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *out) { +void jlHostedScaleRect(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *out) { uint16_t x1; uint16_t y1; @@ -422,7 +423,7 @@ void hostedScaleRect(const HostedRectT *rect, uint16_t outW, uint16_t outH, Host // The scaling maps every periodX stage columns onto outW / gcd output ones // exactly (and rows likewise), so rect is widened to multiples of those. -void hostedRectScaled(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *src, HostedRectT *dst) { +void jlHostedRectScaled(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *src, HostedRectT *dst) { uint16_t periodX; uint16_t periodY; uint16_t x1; @@ -443,7 +444,6 @@ void hostedRectScaled(const HostedRectT *rect, uint16_t outW, uint16_t outH, Hos } - // ----- HAL API (alphabetical) ----- uint16_t jlpFrameCount(void) { @@ -458,7 +458,7 @@ uint16_t jlpFrameHz(void) { bool jlpInit(const jlConfigT *config) { (void)config; - gError = hostBackendOpen(); + gError = jlHostBackendOpen(); if (gError != NULL) { return false; } @@ -467,7 +467,7 @@ bool jlpInit(const jlConfigT *config) { gShownStage = NULL; #endif gOpen = true; - gStartUs = hostBackendMicros(); + gStartUs = jlHostBackendMicros(); gShownFrame = FRAME_NONE; return true; } @@ -479,7 +479,7 @@ const char *jlpLastError(void) { uint32_t jlpMillisElapsed(void) { - return (uint32_t)((hostBackendMicros() - gStartUs) / US_PER_MS); + return (uint32_t)((jlHostBackendMicros() - gStartUs) / US_PER_MS); } @@ -497,33 +497,33 @@ void jlpPresent(const jlSurfaceT *src) { return; } palettes = PALETTES_NONE; - if (gStagePaletteDirty) { - lutBuild(src, gStagePaletteDirtyMask); + if (jlgStagePaletteDirty) { + lutBuild(src, jlgStagePaletteDirtyMask); #if !defined(HOSTED_FRAME_INDEXED) // The rows hold colours: those in a changed palette are expanded again. - palettes = gStagePaletteDirtyMask; + palettes = jlgStagePaletteDirtyMask; #endif } // An SCB change moves rows to other tables: expand them all again. - if (gStageScbDirty) { + if (jlgStageScbDirty) { palettes = PALETTES_ALL; } #if defined(HOSTED_FRAME_INDEXED) // The backend lays its frame out by the palettes in use: when it moves // one, every pixel's index changes. - if (gStageScbDirty || gUsedPalettes == PALETTES_NONE) { + if (jlgStageScbDirty || gUsedPalettes == PALETTES_NONE) { used = palettesUsed(src); if (used != gUsedPalettes) { gUsedPalettes = used; - if (hostBackendPaletteBases(used)) { + if (jlHostBackendPaletteBases(used)) { palettes = PALETTES_ALL; } } } #endif - gStagePaletteDirty = false; - gStagePaletteDirtyMask = PALETTES_NONE; - gStageScbDirty = false; + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = PALETTES_NONE; + jlgStageScbDirty = false; for (y = 0; y < JL_SURFACE_HEIGHT; y++) { // Four clean rows at a time when no palette forces any (the DOS // present's test). @@ -538,14 +538,18 @@ void jlpPresent(const jlSurfaceT *src) { continue; } else { // Clamp a band past the row end, as the DOS present does. - maxWord = gStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? gStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1u); - x0 = (uint16_t)(gStageMinWord[y] * STAGE_PIXELS_PER_WORD); + maxWord = jlgStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? jlgStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1u); + x0 = (uint16_t)(jlgStageMinWord[y] * STAGE_PIXELS_PER_WORD); x1 = (uint16_t)((maxWord + 1u) * STAGE_PIXELS_PER_WORD); } expandRow(src, y, x0, x1); - gPresentCopiedBytes += (uint32_t)(x1 - x0) / STAGE_PIXELS_PER_BYTE; + if (jlgPresentTelemetry != 0u) { + jlgPresentCopiedBytes += (uint32_t)(x1 - x0) / STAGE_PIXELS_PER_BYTE; + } + } + if (jlgPresentTelemetry != 0u) { + jlgPresentScannedRows += JL_SURFACE_HEIGHT; } - gPresentScannedRows += JL_SURFACE_HEIGHT; #if defined(HOSTED_FRAME_INDEXED) gShownStage = src; #endif @@ -557,12 +561,12 @@ void jlpPresent(const jlSurfaceT *src) { frameBoundaryWait(); } show(); - hostedAudioService(); + jlHostedAudioService(); } const char *jlpReadRoot(void) { - return hostBackendProgramFolder(); + return jlHostBackendProgramFolder(); } @@ -570,18 +574,18 @@ const char *jlpReadRoot(void) { // before this. void jlpShutdown(void) { gOpen = false; - hostBackendClose(); + jlHostBackendClose(); } // Sleep to the clock's next frame boundary. void jlpWaitVBL(void) { frameBoundaryWait(); - hostBackendPump(); - hostedAudioService(); + jlHostBackendPump(); + jlHostedAudioService(); } const char *jlpWriteRoot(void) { - return hostBackendWriteFolder(); + return jlHostBackendWriteFolder(); } diff --git a/src/hosted/hosted.h b/src/hosted/hosted.h index 14c1b81..aa6c0a7 100644 --- a/src/hosted/hosted.h +++ b/src/hosted/hosted.h @@ -14,8 +14,8 @@ // and the file system. A new OS is a new backend; the core stays as it is. // // Everything here runs on the program's thread: the backend delivers events -// only from hostBackendPump, and the audio is mixed by the core and handed -// to the backend's queue (hostBackendAudioQueue), never pulled by an audio +// only from jlHostBackendPump, and the audio is mixed by the core and handed +// to the backend's queue (jlHostBackendAudioQueue), never pulled by an audio // thread. #ifndef JOEYLIB_HOSTED_H @@ -34,15 +34,15 @@ // after row, each 0x00RRGGBB. A backend whose build defines // HOSTED_FRAME_INDEXED (a system that shows 8-bit pictures best: Windows // 3.x on a 386) keeps a frame of its own instead: the core hands it each -// changed row as the stage holds it (hostBackendFrameRow), and it expands +// changed row as the stage holds it (jlHostBackendFrameRow), and it expands // them, each pixel base + colour -- the base it gives the row's SCB palette -// (hostBackendPaletteBases) -- shown through a colour table the core keeps it -// up to date with (hostBackendColors). Its palette changes then only touch +// (jlHostBackendPaletteBases) -- shown through a colour table the core keeps it +// up to date with (jlHostBackendColors). Its palette changes then only touch // that table. typedef uint32_t HostedPixelT; // The audio the core mixes: signed 16-bit frames of HOSTED_AUDIO_CHANNELS -// samples (left, right; or one, mono) at the rate hostBackendAudioOpen +// samples (left, right; or one, mono) at the rate jlHostBackendAudioOpen // opened, HOSTED_AUDIO_RATE as a rule. 44.1 kHz stereo unless the backend's // build asks for less (Windows 3.x: 22.05 kHz mono, what its sound drivers // and a 386 manage, or 22.222 kHz where the card plays that exactly). @@ -67,69 +67,70 @@ typedef struct { // current directory is the folder the program's executable is in, as on // every machine port that has one. NULL on success, else why the window // could not be opened. -const char *hostBackendOpen(void); +const char *jlHostBackendOpen(void); // Close the window and the audio device; safe when either is not open. -void hostBackendClose(void); +void jlHostBackendClose(void); // Show one frame (HostedPixelT above; an indexed frame is the backend's // own). rects[0..rectCount-1] cover every pixel that changed since the last // show, rows top to bottom and apart; a backend may show more (the whole // frame), and must when it has a reason of its own (its colours changed, // its window was uncovered). #if defined(HOSTED_FRAME_INDEXED) -void hostBackendShow(const HostedRectT *rects, uint16_t rectCount); +void jlHostBackendShow(const HostedRectT *rects, uint16_t rectCount); #else -void hostBackendShow(const HostedPixelT *pixels, const HostedRectT *rects, uint16_t rectCount); +void jlHostBackendShow(const HostedPixelT *pixels, const HostedRectT *rects, uint16_t rectCount); #endif #if defined(HOSTED_FRAME_INDEXED) // Pixels x0..x1-1 of stage row y changed (x0 and x1 multiples of 4): pixels // holds them from x0's byte, two a byte as the stage does, and scb is the // row's SCB. They stay the backend's to expand, now or when it shows them. -void hostBackendFrameRow(const uint8_t *pixels, uint8_t scb, uint16_t y, uint16_t x0, uint16_t x1); +void jlHostBackendFrameRow(const uint8_t *pixels, uint8_t scb, uint16_t y, uint16_t x0, uint16_t x1); // The colour table changed: colors holds all 256 entries, 0x00RRGGBB, entry // (palette << 4) | colour, and palettes has bit p set for each palette whose // sixteen are new. colors stays valid, and current, until the core shuts // down. Called before the first frame with every bit set. -void hostBackendColors(const uint32_t *colors, uint16_t palettes); +void jlHostBackendColors(const uint32_t *colors, uint16_t palettes); // The palettes the stage's SCBs use (bit p for palette p) changed, or this // is the first present: the backend gives each palette the base its colours // go at in the frame from now on (a pixel is the base + colour). True when // any base moved: the core then hands it every row again. -bool hostBackendPaletteBases(uint16_t used); +bool jlHostBackendPaletteBases(uint16_t used); #endif // The program polls its input: read what the OS reports only when asked // (the joysticks, on Windows and Be), as a machine port reads them at poll // time. -void hostBackendInputPoll(void); +void jlHostBackendInputPoll(void); // Whether the machine has a mouse (jlMousePresent): asked once, at init. -bool hostBackendMousePresent(void); -// Deliver every pending event to the core (hostedKeyEvent and the rest). A +bool jlHostBackendMousePresent(void); +// Deliver every pending event to the core (jlHostedKeyEvent and the rest). A // request to close the window ends the program (exit), as closing a machine // port's program would. -void hostBackendPump(void); +void jlHostBackendPump(void); // Microseconds since some fixed moment before the program started. -uint64_t hostBackendMicros(void); +uint64_t jlHostBackendMicros(void); // The program's own folder, with its trailing separator ("" when the system // cannot tell): the file roots (fileRoot.h), so asked before jlInit too. -const char *hostBackendProgramFolder(void); -#if defined(__EMSCRIPTEN__) -// The write root in a browser, apart from the read root: the page's own -// folder, its SAVES/ kept in IndexedDB (the SDL3 backend). Asked before +const char *jlHostBackendProgramFolder(void); +#if defined(__EMSCRIPTEN__) || defined(__APPLE__) +// The write root, apart from the read root, with its trailing separator: in +// a browser the page's own folder, its SAVES/ kept in IndexedDB; on macOS the +// program's Application Support folder (the SDL3 backend). Asked before // jlInit too. -const char *hostBackendWriteFolder(void); +const char *jlHostBackendWriteFolder(void); #else -#define hostBackendWriteFolder() hostBackendProgramFolder() +#define jlHostBackendWriteFolder() jlHostBackendProgramFolder() #endif -// Sleep until hostBackendMicros reaches micros; returns at once when it has. -void hostBackendSleepUntil(uint64_t micros); +// Sleep until jlHostBackendMicros reaches micros; returns at once when it has. +void jlHostBackendSleepUntil(uint64_t micros); // Open the audio output for the core's frames: the rate it plays them at // (HOSTED_AUDIO_RATE above, or one the device plays more exactly), 0 when // there is no audio device. The core mixes at that rate. -uint32_t hostBackendAudioOpen(void); -void hostBackendAudioClose(void); +uint32_t jlHostBackendAudioOpen(void); +void jlHostBackendAudioClose(void); // Frames queued and not yet played. -uint32_t hostBackendAudioQueued(void); +uint32_t jlHostBackendAudioQueued(void); // Queue count frames (count * HOSTED_AUDIO_CHANNELS samples). -void hostBackendAudioQueue(const int16_t *frames, uint32_t count); +void jlHostBackendAudioQueue(const int16_t *frames, uint32_t count); // ----- What the core gives the backend ----- @@ -137,60 +138,60 @@ void hostBackendAudioQueue(const int16_t *frames, uint32_t count); // is the second of two physical keys that give it: inputInternal.h), or the // window lost the keyboard and every key is up. Repeats need not be // reported. -void hostedKeyEvent(uint8_t key, bool down); -void hostedKeysRelease(void); +void jlHostedKeyEvent(uint8_t key, bool down); +void jlHostedKeysRelease(void); // Mouse: the pointer's place on the stage (already clamped to it), and its // buttons. -void hostedMouseMove(int16_t x, int16_t y); -void hostedMouseButtonEvent(jlMouseButtonE button, bool down); +void jlHostedMouseMove(int16_t x, int16_t y); +void jlHostedMouseButtonEvent(jlMouseButtonE button, bool down); // Joysticks: a driver's axis position on its min..max range as a JoeyLib // axis; a stick was attached or removed; its position (each axis // JL_JOYSTICK_AXIS_MIN..JL_JOYSTICK_AXIS_MAX); a button went down or up. -void hostedJoystickConnect(jlJoystickE js, bool connected); -void hostedJoystickMove(jlJoystickE js, int8_t x, int8_t y); -void hostedJoyButtonEvent(jlJoystickE js, jlJoyButtonE button, bool down); +void jlHostedJoystickConnect(jlJoystickE js, bool connected); +void jlHostedJoystickMove(jlJoystickE js, int8_t x, int8_t y); +void jlHostedJoyButtonEvent(jlJoystickE js, jlJoyButtonE button, bool down); // ----- Shared between the core's files ----- // Keep the audio queue full; the core calls it from every frequent library // call (present, the frame wait, the input poll and the frame tick). -void hostedAudioService(void); +void jlHostedAudioService(void); // For a display probe: colour c of the stage's palette p, worked out from // the stage's own entry as the frame's colours are, 0x00RRGGBB. -uint32_t hostedProbeStageRgb(const jlSurfaceT *src, uint8_t palette, uint8_t color); +uint32_t jlHostedProbeStageRgb(const jlSurfaceT *src, uint8_t palette, uint8_t color); #if defined(HOSTED_FRAME_INDEXED) // Every row of the stage the last present showed into the backend's frame -// again (hostBackendFrameRow), for a backend whose frame was remade; and, +// again (jlHostBackendFrameRow), for a backend whose frame was remade; and, // for its display probe, stage pixel (x, y) as (palette << 4) | colour. -void hostedFrameRefresh(void); -uint8_t hostedProbeStagePixel(const jlSurfaceT *src, uint16_t x, uint16_t y); +void jlHostedFrameRefresh(void); +uint8_t jlHostedProbeStagePixel(const jlSurfaceT *src, uint16_t x, uint16_t y); #else // A display probe's first count (UBER_PROBE's P13): the frame's pixels // that differ from the stage's, each worked out from the stage's own pixel, // SCB and palette entry rather than the present's tables. -uint32_t hostedProbeFrameMismatch(const jlSurfaceT *src); +uint32_t jlHostedProbeFrameMismatch(const jlSurfaceT *src); #endif // Its other counts: the pixels of two raw captures of a w x h window // (rowBytes a row, bitsPixel a pixel) that differ, or below 8 bits a pixel // the bytes; the rows' padding, and a 32-bit pixel's, left out. -uint32_t hostedProbeDiffer(const uint8_t *a, const uint8_t *b, uint16_t w, uint16_t h, uint16_t rowBytes, uint16_t bitsPixel); +uint32_t jlHostedProbeDiffer(const uint8_t *a, const uint8_t *b, uint16_t w, uint16_t h, uint16_t rowBytes, uint16_t bitsPixel); // rects[0..count-1] (rows top to bottom and apart, as a show hands them // over) into out, runs of them merged into their bounding box wherever // drawing it costs less than drawing them apart: callPixels is what one // draw's fixed cost is worth, in stage pixels. Returns how many out holds // (count at most). -uint16_t hostedRectsMerge(const HostedRectT *rects, uint16_t count, uint32_t callPixels, HostedRectT *out); +uint16_t jlHostedRectsMerge(const HostedRectT *rects, uint16_t count, uint32_t callPixels, HostedRectT *out); // For a backend that scales the frame to outW x outH itself, each output // pixel showing one stage pixel (so drawing part of it gives the very pixels // drawing all of it does): each output column's stage column and each // output row's stage row (colMap[outW], rowMap[outH]) ... -void hostedScaleMaps(uint16_t outW, uint16_t outH, uint16_t *colMap, uint16_t *rowMap); +void jlHostedScaleMaps(uint16_t outW, uint16_t outH, uint16_t *colMap, uint16_t *rowMap); // ... and the output pixels showing stage rectangle rect. -void hostedScaleRect(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *out); +void jlHostedScaleRect(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *out); // For a backend that has its OS scale the frame to outW x outH: rect widened to whole // periods of the scaling (src) -- where a scaler's pattern starts over, so // drawing src alone gives the very pixels drawing the whole frame does -- // and where src lands in the scaled frame (dst). -void hostedRectScaled(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *src, HostedRectT *dst); +void jlHostedRectScaled(const HostedRectT *rect, uint16_t outW, uint16_t outH, HostedRectT *src, HostedRectT *dst); #endif diff --git a/src/hosted/input.c b/src/hosted/input.c index 72841e9..c2e60de 100644 --- a/src/hosted/input.c +++ b/src/hosted/input.c @@ -31,7 +31,7 @@ static int8_t gJoyRawY [JL_JOYSTICK_COUNT]; // ----- Hosted core API (alphabetical) ----- -void hostedJoyButtonEvent(jlJoystickE js, jlJoyButtonE button, bool down) { +void jlHostedJoyButtonEvent(jlJoystickE js, jlJoyButtonE button, bool down) { if ((uint16_t)js >= JL_JOYSTICK_COUNT || (uint16_t)button >= JL_JOY_BUTTON_COUNT) { return; } @@ -42,18 +42,18 @@ void hostedJoyButtonEvent(jlJoystickE js, jlJoyButtonE button, bool down) { } -void hostedJoystickConnect(jlJoystickE js, bool connected) { +void jlHostedJoystickConnect(jlJoystickE js, bool connected) { if ((uint16_t)js >= JL_JOYSTICK_COUNT) { return; } - gJoyConnected[js] = connected; - gJoyRawX[js] = 0; - gJoyRawY[js] = 0; + jlgJoyConnected[js] = connected; + gJoyRawX[js] = 0; + gJoyRawY[js] = 0; memset(gJoyHeld[js], 0, sizeof(gJoyHeld[js])); } -void hostedJoystickMove(jlJoystickE js, int8_t x, int8_t y) { +void jlHostedJoystickMove(jlJoystickE js, int8_t x, int8_t y) { if ((uint16_t)js >= JL_JOYSTICK_COUNT) { return; } @@ -62,7 +62,7 @@ void hostedJoystickMove(jlJoystickE js, int8_t x, int8_t y) { } -void hostedKeyEvent(uint8_t key, bool down) { +void jlHostedKeyEvent(uint8_t key, bool down) { uint8_t slot; slot = (uint8_t)(key & INPUT_KEY_MASK); @@ -76,12 +76,12 @@ void hostedKeyEvent(uint8_t key, bool down) { } -void hostedKeysRelease(void) { +void jlHostedKeysRelease(void) { memset(gKeyHeld, 0, sizeof(gKeyHeld)); } -void hostedMouseButtonEvent(jlMouseButtonE button, bool down) { +void jlHostedMouseButtonEvent(jlMouseButtonE button, bool down) { if (button <= JL_MOUSE_BUTTON_NONE || button >= JL_MOUSE_BUTTON_COUNT) { return; } @@ -92,8 +92,8 @@ void hostedMouseButtonEvent(jlMouseButtonE button, bool down) { } -void hostedMouseMove(int16_t x, int16_t y) { - inputMouseMove(x, y); +void jlHostedMouseMove(int16_t x, int16_t y) { + jlInputMouseMove(x, y); } // ----- HAL API (alphabetical) ----- @@ -105,22 +105,22 @@ void jlpInputInit(void) { memset(gMouseTapped, 0, sizeof(gMouseTapped)); memset(gJoyHeld, 0, sizeof(gJoyHeld)); memset(gJoyTapped, 0, sizeof(gJoyTapped)); - gMouseAttached = hostBackendMousePresent(); + jlgMouseAttached = jlHostBackendMousePresent(); } void jlpInputPoll(void) { uint8_t js; - hostBackendPump(); - hostBackendInputPoll(); - inputLatch(gKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT); - inputLatch(gMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); - inputLatch(&gJoyButtonState[0][0], &gJoyHeld[0][0], &gJoyTapped[0][0], JL_JOYSTICK_COUNT * JL_JOY_BUTTON_COUNT); + jlHostBackendPump(); + jlHostBackendInputPoll(); + jlInputLatch(jlgKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT); + jlInputLatch(jlgMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); + jlInputLatch(&jlgJoyButtonState[0][0], &gJoyHeld[0][0], &gJoyTapped[0][0], JL_JOYSTICK_COUNT * JL_JOY_BUTTON_COUNT); for (js = 0; js < JL_JOYSTICK_COUNT; js++) { - gJoyAxisX[js] = inputDeadZone(gJoyRawX[js], gJoyDeadZone[js]); - gJoyAxisY[js] = inputDeadZone(gJoyRawY[js], gJoyDeadZone[js]); + jlgJoyAxisX[js] = jlInputDeadZone(gJoyRawX[js], jlgJoyDeadZone[js]); + jlgJoyAxisY[js] = jlInputDeadZone(gJoyRawY[js], jlgJoyDeadZone[js]); } - hostedAudioService(); + jlHostedAudioService(); } diff --git a/src/hosted/posix/posixSerial.c b/src/hosted/posix/posixSerial.c index 5a0b31c..0ecba38 100644 --- a/src/hosted/posix/posixSerial.c +++ b/src/hosted/posix/posixSerial.c @@ -3,20 +3,23 @@ // SPDX-License-Identifier: MIT // // POSIX serial HAL, for the hosted ports whose systems reach a serial port as -// a terminal device through termios: SDL3 on Linux, Haiku, and BeOS R5. The -// device is opened non-blocking in raw mode. The poll (and every read) moves -// what the driver has buffered into a ring here, since BeOS has no FIONREAD -// for jlSerialAvailable to ask how much that is, and a write waits a bounded -// time for the driver to take its bytes, as on Win32 (on BeOS R5 the -// driver's own time: see jlpSerialOpen). BeOS R5 is unverified: the gates' -// VM has no serial ports (verify-be-serial.sh). +// a terminal device through termios: SDL3 on Linux and macOS, Haiku, and +// BeOS R5. The device is opened non-blocking in raw mode. The poll (and +// every read) moves what the driver has buffered into a ring here, since +// BeOS has no FIONREAD for jlSerialAvailable to ask how much that is, and a +// write waits a bounded time for the driver to take its bytes, as on Win32 +// (on BeOS R5 the driver's own time: see jlpSerialOpen). BeOS R5 is +// unverified: the gates' VM has no serial ports (verify-be-serial.sh). macOS +// is unverified too: nothing here runs Mac programs. // // Which device: DEFAULT, MODEM and PRINTER are the one JOEYLIB_SERIAL names // in the environment when it is set (a USB adapter's /dev/ttyUSB0, say), // else the first port; SLOT is port <unit>, from 1. On Linux the ports are // /dev/ttyS0, /dev/ttyS1, and on; on Haiku and BeOS the entries of // /dev/ports in name order (BeOS's serial1, serial2, ...; Haiku's -// pc_serial0, ...). +// pc_serial0, ...); on macOS the call-out devices in name order +// (/dev/cu.usbserial-..., a USB adapter's), less the ones every Mac has that +// are not serial ports (gNotPorts). // // The open raises DTR, and RTS unless RTS/CTS handshakes (the driver then // owns it). Closing waits until the driver has had time to send what the @@ -46,6 +49,10 @@ #if defined(__HAIKU__) || defined(__BEOS__) #define PORTS_FOLDER "/dev/ports" #define PORTS_MAX 32u +#elif defined(__APPLE__) +#define PORTS_FOLDER "/dev" +#define PORTS_NAME_START "cu." +#define PORTS_MAX 32u #else #define PORT_PREFIX "/dev/ttyS" #endif @@ -87,6 +94,7 @@ typedef struct { static bool portPath(jlSerialDeviceE device, const jlSerialConfigT *config, char *out, size_t cap); #if defined(PORTS_FOLDER) +static bool portListed(const char *name); static int portNameCompare(const void *a, const void *b); #endif static speed_t rateCode(uint32_t baud, uint32_t *actual); @@ -118,9 +126,39 @@ static const BaudRateT gRates[] = { static const tcflag_t gDataBits[] = { CS5, CS6, CS7, CS8 }; +#if defined(PORTS_NAME_START) +// Call-out devices a Mac has that are not serial ports. +static const char *const gNotPorts[] = { + "cu.Bluetooth-Incoming-Port", + "cu.debug-console", + "cu.wlan-debug" +}; +#endif + // ----- Internal helpers (alphabetical) ----- #if defined(PORTS_FOLDER) +// Whether an entry of PORTS_FOLDER is a port: any but the folder's own (. +// and ..); on macOS, a call-out device not in gNotPorts. +static bool portListed(const char *name) { +#if defined(PORTS_NAME_START) + size_t i; + + if (strncmp(name, PORTS_NAME_START, strlen(PORTS_NAME_START)) != 0) { + return false; + } + for (i = 0; i < sizeof(gNotPorts) / sizeof(gNotPorts[0]); i++) { + if (strcmp(name, gNotPorts[i]) == 0) { + return false; + } + } + return true; +#else + return name[0] != '.'; +#endif +} + + static int portNameCompare(const void *a, const void *b) { return strcmp((const char *)a, (const char *)b); } @@ -157,7 +195,7 @@ static bool portPath(jlSerialDeviceE device, const jlSerialConfigT *config, char } count = 0u; while (count < PORTS_MAX && (entry = readdir(dir)) != NULL) { - if (entry->d_name[0] != '.' && strlen(entry->d_name) < PORT_NAME_BYTES) { + if (portListed(entry->d_name) && strlen(entry->d_name) < PORT_NAME_BYTES) { strcpy(names[count], entry->d_name); count++; } @@ -258,9 +296,9 @@ void jlpSerialClose(void) { if (gFd < 0) { return; } - now = hostBackendMicros(); + now = jlHostBackendMicros(); if (gTxIdleAt > now) { - hostBackendSleepUntil(gTxIdleAt + WRITE_TIMEOUT_US); + jlHostBackendSleepUntil(gTxIdleAt + WRITE_TIMEOUT_US); } gSaved.c_cflag &= ~(tcflag_t)HUPCL; (void)tcsetattr(gFd, TCSANOW, &gSaved); @@ -291,18 +329,18 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { return false; } if (!portPath(device, config, path, sizeof(path))) { - coreSetError("serial: no such port"); + jlCoreSetError("serial: no such port"); return false; } gFd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); if (gFd < 0) { snprintf(gErrorText, sizeof(gErrorText), "serial: cannot open %s: %s", path, strerror(errno)); - coreSetError(gErrorText); + jlCoreSetError(gErrorText); return false; } if (tcgetattr(gFd, &gSaved) != 0) { snprintf(gErrorText, sizeof(gErrorText), "serial: %s is not a serial port", path); - coreSetError(gErrorText); + jlCoreSetError(gErrorText); close(gFd); gFd = -1; return false; @@ -330,7 +368,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { line.c_cc[VTIME] = 0; if (cfsetispeed(&line, code) != 0 || cfsetospeed(&line, code) != 0 || tcsetattr(gFd, TCSANOW, &line) != 0) { snprintf(gErrorText, sizeof(gErrorText), "serial: %s refused the line settings", path); - coreSetError(gErrorText); + jlCoreSetError(gErrorText); close(gFd); gFd = -1; return false; @@ -400,7 +438,7 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { if (gFd < 0) { return 0u; } - now = hostBackendMicros(); + now = jlHostBackendMicros(); deadline = now + (uint64_t)len * gByteUs + WRITE_TIMEOUT_US; sent = 0u; while (sent < len) { @@ -412,13 +450,13 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { if (put < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { break; } - now = hostBackendMicros(); + now = jlHostBackendMicros(); if (now >= deadline) { break; } - hostBackendSleepUntil(now + WRITE_RETRY_US); + jlHostBackendSleepUntil(now + WRITE_RETRY_US); } - now = hostBackendMicros(); + now = jlHostBackendMicros(); gTxIdleAt = (gTxIdleAt > now ? gTxIdleAt : now) + (uint64_t)sent * gByteUs; return sent; } diff --git a/src/hosted/psp/pspKeyboard.c b/src/hosted/psp/pspKeyboard.c index e8c764b..93e57e8 100644 --- a/src/hosted/psp/pspKeyboard.c +++ b/src/hosted/psp/pspKeyboard.c @@ -16,7 +16,7 @@ // pressed again; Caps turns capital letters on and off. // // A key does what a real one does: its jlKeyE goes down and up -// (hostedKeyEvent), and it types its character (jlInputCharPush) -- the US +// (jlHostedKeyEvent), and it types its character (jlInputCharPush) -- the US // layout's, shifted with Shift (a letter with Caps too), and with Ctrl or // Alt down only the controls (Escape, Return, Tab, Backspace, Delete), as // a desktop's keyboard types. The keys are JoeyLib's (joey/input.h) and the @@ -514,22 +514,22 @@ static void keyPress(const BoardKeyT *key) { case KEY_KIND_SHIFT: shift = gShift; if (shift != JL_KEY_NONE) { - hostedKeyEvent(shift, false); + jlHostedKeyEvent(shift, false); } gShift = shift == key->key ? JL_KEY_NONE : key->key; if (gShift != JL_KEY_NONE) { - hostedKeyEvent(gShift, true); + jlHostedKeyEvent(gShift, true); } break; case KEY_KIND_CTRL: gCtrl = !gCtrl; - hostedKeyEvent(key->key, gCtrl); + jlHostedKeyEvent(key->key, gCtrl); break; case KEY_KIND_ALT: gAlt = !gAlt; - hostedKeyEvent(key->key, gAlt); + jlHostedKeyEvent(key->key, gAlt); break; case KEY_KIND_CAPS: @@ -537,16 +537,16 @@ static void keyPress(const BoardKeyT *key) { break; default: - // The keyboard's keys are their keys' second (hostedKeyEvent): + // The keyboard's keys are their keys' second (jlHostedKeyEvent): // Start and Select are Return and Escape too. gHeld = key; if (key->key != JL_KEY_NONE) { - hostedKeyEvent(key->key | INPUT_KEY_SECOND, true); + jlHostedKeyEvent(key->key | INPUT_KEY_SECOND, true); } // With CTRL or ALT latched it is a chord, which types nothing. ch = typed(key); if (ch != CHAR_NONE) { - inputCharTyped(ch, gCtrl || gAlt); + jlInputCharTyped(ch, gCtrl || gAlt); } gHeldChar = ch; gHeldChord = gCtrl || gAlt; @@ -562,7 +562,7 @@ static void keyRelease(void) { return; } if (gHeld->key != JL_KEY_NONE) { - hostedKeyEvent(gHeld->key | INPUT_KEY_SECOND, false); + jlHostedKeyEvent(gHeld->key | INPUT_KEY_SECOND, false); } gHeld = NULL; gHeldChar = CHAR_NONE; @@ -572,15 +572,15 @@ static void keyRelease(void) { static void latchesRelease(void) { if (gShift != JL_KEY_NONE) { - hostedKeyEvent(gShift, false); + jlHostedKeyEvent(gShift, false); gShift = JL_KEY_NONE; } if (gCtrl) { - hostedKeyEvent(JL_KEY_LCTRL, false); + jlHostedKeyEvent(JL_KEY_LCTRL, false); gCtrl = false; } if (gAlt) { - hostedKeyEvent(JL_KEY_LALT, false); + jlHostedKeyEvent(JL_KEY_LALT, false); gAlt = false; } } @@ -628,7 +628,7 @@ static uint8_t typed(const BoardKeyT *key) { // ----- PSP keyboard API (alphabetical) ----- -bool pspKeyboardButton(uint8_t button, bool down) { +bool jlPspKeyboardButton(uint8_t button, bool down) { if (button == SDL_GAMEPAD_BUTTON_WEST) { if (down && gOpen) { boardClose(); @@ -677,7 +677,7 @@ bool pspKeyboardButton(uint8_t button, bool down) { // The picture, then the lit keys over it, then the cursor round its key. -void pspKeyboardDraw(SDL_Renderer *renderer) { +void jlPspKeyboardDraw(SDL_Renderer *renderer) { SDL_FRect board; SDL_FRect lit; SDL_Rect box; @@ -733,7 +733,7 @@ void pspKeyboardDraw(SDL_Renderer *renderer) { } -void pspKeyboardFree(void) { +void jlPspKeyboardFree(void) { uint8_t picture; for (picture = 0; picture < BOARD_PICTURES; picture++) { @@ -745,17 +745,17 @@ void pspKeyboardFree(void) { } -bool pspKeyboardIsOpen(void) { +bool jlPspKeyboardIsOpen(void) { return gOpen; } -bool pspKeyboardTick(uint64_t ms) { +bool jlPspKeyboardTick(uint64_t ms) { if (!gOpen) { return false; } if (gHeldChar != CHAR_NONE && ms >= gHeldMs) { - inputCharTyped(gHeldChar, gHeldChord); + jlInputCharTyped(gHeldChar, gHeldChord); gHeldMs = ms + REPEAT_EVERY_MS; } if (gRepeat == REPEAT_NONE || ms < gRepeatMs) { diff --git a/src/hosted/psp/pspKeyboard.h b/src/hosted/psp/pspKeyboard.h index daace1b..f4123e5 100644 --- a/src/hosted/psp/pspKeyboard.h +++ b/src/hosted/psp/pspKeyboard.h @@ -17,16 +17,16 @@ // A gamepad button (an SDL_GamepadButton) went down or up. True when the // keyboard took it: Square, which opens and closes it, and while it is open // every button but Start and Select, which stay Return and Escape. -bool pspKeyboardButton(uint8_t button, bool down); +bool jlPspKeyboardButton(uint8_t button, bool down); // Draw it over what renderer holds, when it is open. -void pspKeyboardDraw(SDL_Renderer *renderer); +void jlPspKeyboardDraw(SDL_Renderer *renderer); // Let its pictures go, before the renderer goes. -void pspKeyboardFree(void); +void jlPspKeyboardFree(void); // True while it is on the screen. -bool pspKeyboardIsOpen(void); +bool jlPspKeyboardIsOpen(void); // A held key types its character again, and a d-pad direction held down // moves the cursor again, every so often: true when the cursor moved, now ms // (SDL_GetTicks's milliseconds). -bool pspKeyboardTick(uint64_t ms); +bool jlPspKeyboardTick(uint64_t ms); #endif diff --git a/src/hosted/sdl3/sdl3Backend.c b/src/hosted/sdl3/sdl3Backend.c index 778c4bf..3717873 100644 --- a/src/hosted/sdl3/sdl3Backend.c +++ b/src/hosted/sdl3/sdl3Backend.c @@ -34,7 +34,7 @@ // back at least once a virtual frame even when it never waits (a loop on // jlInputPoll alone, as jlWaitForAnyKey is). Its folder is the in-memory file // system's root, where its DATA is unpacked. SAVES/ and joeylog.txt go in a -// folder named after the page (webWriteFolderMount), SAVES/ there kept in +// folder named after the page (jlWebWriteFolderMount), SAVES/ there kept in // the browser's IndexedDB (IDBFS), a store of the page's own: read when the // program first asks for it and written back as each save changes (and as // the page goes away), so saves outlive it. Free space is the browser's own @@ -52,6 +52,16 @@ // program as closing a window does. The program's // folder (PSP/GAME/NAME on the memory stick) is the current directory, and // free space comes from the memory stick the program runs from. +// +// On macOS (the macos port) a program is an app, and its folder -- the +// current directory, where its DATA is -- is the app's Contents/Resources. +// SAVES/ and joeylog.txt go in the user's Application Support folder, in a +// folder named after the program (SDL's preference path), since an app's +// own folder may not be writable: one in /Applications, or one macOS runs +// from a read-only copy after it was downloaded (App Translocation). Free +// space is that folder's. The window draws at the screen's own resolution, +// two pixels a point on a Retina screen, so the frame is scaled up once, +// sharply, and not again by the system. #include <SDL3/SDL.h> @@ -88,6 +98,12 @@ #define WINDOW_SCALE_DEFAULT 3 #define WINDOW_SCALE_MAX 8 #define WINDOW_TITLE_DEFAULT "JoeyLib" +// The window's flags for one system: on macOS, the screen's own resolution. +#if defined(__APPLE__) +#define WINDOW_FLAGS_SYSTEM SDL_WINDOW_HIGH_PIXEL_DENSITY +#else +#define WINDOW_FLAGS_SYSTEM 0 +#endif // The displayed shape of the 320x200 stage: 4:3 like a CRT, or 16:10 when // its pixels are kept square. @@ -122,19 +138,19 @@ #if defined(HOSTED_WINDOW_ICON) // The program's window icon, which make bakes into it (make/sdl3.mk): -// gHostedIconSize pixels square, a byte for each of red, green, blue and +// jlgHostedIconSize pixels square, a byte for each of red, green, blue and // alpha, row after row. #define ICON_FORMAT SDL_PIXELFORMAT_RGBA32 #define ICON_PIXEL_BYTES 4 -extern const uint16_t gHostedIconSize; -extern const uint8_t gHostedIconRgba[]; +extern const uint16_t jlgHostedIconSize; +extern const uint8_t jlgHostedIconRgba[]; #endif #if defined(__EMSCRIPTEN__) #define US_PER_MS 1000u // The browser's turn at least this often: one virtual frame. #define WEB_YIELD_US (1000000u / HOSTED_FRAME_HZ) -// The longest write folder kept whole (webWriteFolderMount), its NUL in: +// The longest write folder kept whole (jlWebWriteFolderMount), its NUL in: // half the core's FILE_PATH_MAX, leaving the rest for SAVES/ and a name. #define WEB_FOLDER_BYTES 128 #endif @@ -186,6 +202,7 @@ static void padRemoved(SDL_JoystickID id); static int padSlot(SDL_JoystickID id); static void padUpdate(SDL_JoystickID id); static void pointerMove(float windowX, float windowY); +static const char *programName(void); #if defined(__PSP__) static void pspButtonKey(uint8_t button, bool down); static int pspExitCallback(int arg1, int arg2, void *common); @@ -201,7 +218,6 @@ static void webService(void); #if defined(HOSTED_WINDOW_ICON) static void windowIconSet(void); #endif -static const char *windowTitle(void); // ----- Module state ----- @@ -221,6 +237,9 @@ static char gErrorText[ERROR_TEXT_BYTES]; static char gWebWriteFolder[WEB_FOLDER_BYTES]; // "" until mounted static uint64_t gWebYieldUs = 0; // when the browser last had its turn #endif +#if defined(__APPLE__) +static char *gAppleWriteFolder = NULL; // SDL's, NULL until made +#endif #if defined(__PSP__) // The PSP loads a program by its module information. Its main thread may @@ -232,7 +251,7 @@ PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER | THREAD_ATTR_VFPU); // ----- Internal helpers (alphabetical) ----- // Whether a typed character comes as a chord, which types nothing -// (inputCharTyped): Ctrl held without Alt. AltGr is Ctrl+Alt on Windows, +// (jlInputCharTyped): Ctrl held without Alt. AltGr is Ctrl+Alt on Windows, // and types its characters. static bool charChord(void) { SDL_Keymod mods; @@ -274,7 +293,7 @@ static void charFromKey(SDL_Scancode code) { break; } if (ch != CHAR_NONE) { - inputCharTyped(ch, charChord()); + jlInputCharTyped(ch, charChord()); } } @@ -325,10 +344,10 @@ static void fullScreenToggle(void) { // cross are the keyboard's), and once it has closed the stick is read again; // either way the screen shows the keyboard as it is now. static void keyboardTook(SDL_JoystickID id) { - if (pspKeyboardIsOpen()) { - hostedJoystickMove(JL_JOYSTICK_0, 0, 0); - hostedJoyButtonEvent(JL_JOYSTICK_0, JL_JOY_BUTTON_0, false); - hostedJoyButtonEvent(JL_JOYSTICK_0, JL_JOY_BUTTON_1, false); + if (jlPspKeyboardIsOpen()) { + jlHostedJoystickMove(JL_JOYSTICK_0, 0, 0); + jlHostedJoyButtonEvent(JL_JOYSTICK_0, JL_JOY_BUTTON_0, false); + jlHostedJoyButtonEvent(JL_JOYSTICK_0, JL_JOY_BUTTON_1, false); } else { padUpdate(id); } @@ -344,12 +363,12 @@ static void keyEvent(SDL_Scancode code, bool down, bool repeat) { charFromKey(code); } if (!repeat) { - hostedKeyEvent(keyFromScancode(code), down); + jlHostedKeyEvent(keyFromScancode(code), down); } } -// The key a scancode gives (hostedKeyEvent's: the second of a key's two +// The key a scancode gives (jlHostedKeyEvent's: the second of a key's two // with INPUT_KEY_SECOND). static uint8_t keyFromScancode(SDL_Scancode code) { if (code >= SDL_SCANCODE_A && code <= SDL_SCANCODE_Z) { @@ -466,7 +485,7 @@ static void padAdded(SDL_JoystickID id, bool raw) { gPads[slot] = SDL_OpenGamepad(id); } if (gPads[slot] != NULL || gSticks[slot] != NULL) { - hostedJoystickConnect((jlJoystickE)slot, true); + jlHostedJoystickConnect((jlJoystickE)slot, true); } return; } @@ -488,7 +507,7 @@ static void padRemoved(SDL_JoystickID id) { SDL_CloseJoystick(gSticks[slot]); gSticks[slot] = NULL; } - hostedJoystickConnect((jlJoystickE)slot, false); + jlHostedJoystickConnect((jlJoystickE)slot, false); } @@ -525,9 +544,9 @@ static void padUpdate(SDL_JoystickID id) { if (pad != NULL) { x = stickAxis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX), SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT), SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)); y = stickAxis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY), SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP), SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)); - hostedJoystickMove((jlJoystickE)slot, x, y); - hostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_0, SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH)); - hostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_1, SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST)); + jlHostedJoystickMove((jlJoystickE)slot, x, y); + jlHostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_0, SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH)); + jlHostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_1, SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST)); return; } stick = gSticks[slot]; @@ -535,9 +554,9 @@ static void padUpdate(SDL_JoystickID id) { hat = SDL_GetNumJoystickHats(stick) > 0 ? SDL_GetJoystickHat(stick, 0) : SDL_HAT_CENTERED; x = stickAxis(axes > 0 ? SDL_GetJoystickAxis(stick, 0) : 0, (hat & SDL_HAT_LEFT) != 0, (hat & SDL_HAT_RIGHT) != 0); y = stickAxis(axes > 1 ? SDL_GetJoystickAxis(stick, 1) : 0, (hat & SDL_HAT_UP) != 0, (hat & SDL_HAT_DOWN) != 0); - hostedJoystickMove((jlJoystickE)slot, x, y); - hostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_0, SDL_GetJoystickButton(stick, 0)); - hostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_1, SDL_GetJoystickButton(stick, 1)); + jlHostedJoystickMove((jlJoystickE)slot, x, y); + jlHostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_0, SDL_GetJoystickButton(stick, 0)); + jlHostedJoyButtonEvent((jlJoystickE)slot, JL_JOY_BUTTON_1, SDL_GetJoystickButton(stick, 1)); } @@ -560,7 +579,22 @@ static void pointerMove(float windowX, float windowY) { stageY = (int32_t)((y - dst.y) * (float)JL_SURFACE_HEIGHT / dst.h); stageX = stageX < 0 ? 0 : (stageX >= JL_SURFACE_WIDTH ? JL_SURFACE_WIDTH - 1 : stageX); stageY = stageY < 0 ? 0 : (stageY >= JL_SURFACE_HEIGHT ? JL_SURFACE_HEIGHT - 1 : stageY); - hostedMouseMove((int16_t)stageX, (int16_t)stageY); + jlHostedMouseMove((int16_t)stageX, (int16_t)stageY); +} + + +// The program's name, its executable's where the C library knows it: the +// window's title, and on macOS the name of its settings folder. +static const char *programName(void) { +#if defined(__GLIBC__) + extern char *program_invocation_short_name; + + return program_invocation_short_name; +#elif defined(__APPLE__) + return getprogname(); +#else + return WINDOW_TITLE_DEFAULT; +#endif } @@ -617,7 +651,7 @@ static void rawStickEvent(SDL_JoystickID id) { // Draw the last frame again (the window was resized or uncovered). static void reshow(void) { if (gLastFrame != NULL) { - hostBackendShow(gLastFrame, NULL, 0); + jlHostBackendShow(gLastFrame, NULL, 0); } } @@ -628,7 +662,7 @@ static int8_t stickAxis(int16_t raw, bool negative, bool positive) { if (negative != positive) { return negative ? (int8_t)JL_JOYSTICK_AXIS_MIN : (int8_t)JL_JOYSTICK_AXIS_MAX; } - return inputAxisScale(raw, INT16_MIN, INT16_MAX); + return jlInputAxisScale(raw, INT16_MIN, INT16_MAX); } @@ -662,7 +696,7 @@ static void textureFill(const uint32_t *pixels) { // leaving the page writes back anything still due. The folder is also // Module.joeyWriteFolder, where the page's shell and the web gates find // joeylog.txt. A page's path is ASCII (the browser escapes the rest). -EM_ASYNC_JS(void, webWriteFolderMount, (char *out, int cap), { +EM_ASYNC_JS(void, jlWebWriteFolderMount, (char *out, int cap), { var path = window.location.pathname; var at = path.lastIndexOf("/"); var file = path.slice(at + 1); @@ -694,9 +728,9 @@ EM_ASYNC_JS(void, webWriteFolderMount, (char *out, int cap), { // The browser's turn when it has gone a frame without one. static void webService(void) { - if (hostBackendMicros() - gWebYieldUs >= WEB_YIELD_US) { + if (jlHostBackendMicros() - gWebYieldUs >= WEB_YIELD_US) { emscripten_sleep(0); - gWebYieldUs = hostBackendMicros(); + gWebYieldUs = jlHostBackendMicros(); } } @@ -704,7 +738,7 @@ static void webService(void) { // The room the browser lets this site's storage grow into: its quota less // what the site uses (navigator.storage.estimate), or 0 where it says // nothing. SAVES/ lives in that storage. -EM_ASYNC_JS(double, webStorageFree, (void), { +EM_ASYNC_JS(double, jlWebStorageFree, (void), { if (!navigator.storage || !navigator.storage.estimate) { return 0; } @@ -723,7 +757,7 @@ EM_ASYNC_JS(double, webStorageFree, (void), { static void windowIconSet(void) { SDL_Surface *icon; - icon = SDL_CreateSurfaceFrom((int)gHostedIconSize, (int)gHostedIconSize, ICON_FORMAT, (void *)gHostedIconRgba, (int)gHostedIconSize * ICON_PIXEL_BYTES); + icon = SDL_CreateSurfaceFrom((int)jlgHostedIconSize, (int)jlgHostedIconSize, ICON_FORMAT, (void *)jlgHostedIconRgba, (int)jlgHostedIconSize * ICON_PIXEL_BYTES); if (icon != NULL) { SDL_SetWindowIcon(gWindow, icon); SDL_DestroySurface(icon); @@ -731,21 +765,9 @@ static void windowIconSet(void) { } #endif - -// The program's name: its executable's, where the C library knows it. -static const char *windowTitle(void) { -#if defined(__GLIBC__) - extern char *program_invocation_short_name; - - return program_invocation_short_name; -#else - return WINDOW_TITLE_DEFAULT; -#endif -} - // ----- Backend API (alphabetical) ----- -void hostBackendAudioClose(void) { +void jlHostBackendAudioClose(void) { if (gAudio != NULL) { SDL_DestroyAudioStream(gAudio); gAudio = NULL; @@ -754,7 +776,7 @@ void hostBackendAudioClose(void) { } -uint32_t hostBackendAudioOpen(void) { +uint32_t jlHostBackendAudioOpen(void) { SDL_AudioSpec spec; if (gAudio != NULL) { @@ -776,12 +798,12 @@ uint32_t hostBackendAudioOpen(void) { } -void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { +void jlHostBackendAudioQueue(const int16_t *frames, uint32_t count) { SDL_PutAudioStreamData(gAudio, frames, (int)count * AUDIO_FRAME_BYTES); } -uint32_t hostBackendAudioQueued(void) { +uint32_t jlHostBackendAudioQueued(void) { int bytes; bytes = SDL_GetAudioStreamQueued(gAudio); @@ -789,12 +811,12 @@ uint32_t hostBackendAudioQueued(void) { } -void hostBackendClose(void) { +void jlHostBackendClose(void) { int slot; - hostBackendAudioClose(); + jlHostBackendAudioClose(); #if defined(__PSP__) - pspKeyboardFree(); + jlPspKeyboardFree(); #endif for (slot = 0; slot < JL_JOYSTICK_COUNT; slot++) { if (gPads[slot] != NULL) { @@ -823,14 +845,14 @@ void hostBackendClose(void) { } -// The gamepads report themselves as events (hostBackendPump). -void hostBackendInputPoll(void) { +// The gamepads report themselves as events (jlHostBackendPump). +void jlHostBackendInputPoll(void) { } // The PSP has none (its pointer is the on-screen keyboard's); elsewhere, what // SDL finds. -bool hostBackendMousePresent(void) { +bool jlHostBackendMousePresent(void) { #if defined(__PSP__) return false; #else @@ -839,12 +861,12 @@ bool hostBackendMousePresent(void) { } -uint64_t hostBackendMicros(void) { +uint64_t jlHostBackendMicros(void) { return SDL_GetTicksNS() / NS_PER_US; } -const char *hostBackendOpen(void) { +const char *jlHostBackendOpen(void) { const char *base; const char *aspect; int scale; @@ -854,8 +876,8 @@ const char *hostBackendOpen(void) { return SDL_GetError(); } // The program's own folder is the current directory from here on, as - // the folder its files are found in (hostBackendProgramFolder). - base = hostBackendProgramFolder(); + // the folder its files are found in (jlHostBackendProgramFolder). + base = jlHostBackendProgramFolder(); if (base[0] == '\0' || chdir(base) != 0) { SDL_Quit(); return "cannot make the program's folder the current directory"; @@ -864,7 +886,7 @@ const char *hostBackendOpen(void) { gSquare = aspect != NULL && SDL_strcmp(aspect, "square") == 0; scale = optionInt("JOEYLIB_SCALE", WINDOW_SCALE_DEFAULT, WINDOW_SCALE_MAX); height = gSquare ? JL_SURFACE_HEIGHT * scale : JL_SURFACE_WIDTH * scale * ASPECT_CRT_H / ASPECT_CRT_W; - gWindow = SDL_CreateWindow(windowTitle(), JL_SURFACE_WIDTH * scale, height, SDL_WINDOW_RESIZABLE | (optionInt("JOEYLIB_FULLSCREEN", 0, 1) != 0 ? SDL_WINDOW_FULLSCREEN : 0)); + gWindow = SDL_CreateWindow(programName(), JL_SURFACE_WIDTH * scale, height, SDL_WINDOW_RESIZABLE | WINDOW_FLAGS_SYSTEM | (optionInt("JOEYLIB_FULLSCREEN", 0, 1) != 0 ? SDL_WINDOW_FULLSCREEN : 0)); if (gWindow == NULL) { SDL_Quit(); return SDL_GetError(); @@ -879,7 +901,7 @@ const char *hostBackendOpen(void) { if (gTexture == NULL) { // Closing clears SDL's error: keep its text first. SDL_strlcpy(gErrorText, SDL_GetError(), sizeof(gErrorText)); - hostBackendClose(); + jlHostBackendClose(); return gErrorText; } if (!SDL_SetTextureScaleMode(gTexture, SDL_SCALEMODE_PIXELART)) { @@ -893,7 +915,7 @@ const char *hostBackendOpen(void) { // must still give the screen and the audio device back. On the PSP the // HOME menu's Exit closes the window. if (!gAtExit) { - atexit(hostBackendClose); + atexit(jlHostBackendClose); #if defined(__PSP__) sceKernelStartThread(sceKernelCreateThread(PSP_MODULE_NAME, pspExitThread, PSP_EXIT_PRIORITY, PSP_EXIT_STACK_BYTES, 0, NULL), 0, NULL); #endif @@ -904,7 +926,7 @@ const char *hostBackendOpen(void) { // SDL's base path, which ends in its separator. -const char *hostBackendProgramFolder(void) { +const char *jlHostBackendProgramFolder(void) { const char *base; base = SDL_GetBasePath(); @@ -912,14 +934,14 @@ const char *hostBackendProgramFolder(void) { } -void hostBackendPump(void) { +void jlHostBackendPump(void) { SDL_Event e; #if defined(__EMSCRIPTEN__) webService(); #endif #if defined(__PSP__) - if (pspKeyboardTick(SDL_GetTicks())) { + if (jlPspKeyboardTick(SDL_GetTicks())) { reshow(); } #endif @@ -951,7 +973,7 @@ void hostBackendPump(void) { // left out (jlInputCharPush filters the rest). for (text = e.text.text; *text != '\0'; text++) { if ((uint8_t)*text < ASCII_LIMIT) { - inputCharTyped((uint8_t)*text, charChord()); + jlInputCharTyped((uint8_t)*text, charChord()); } } } @@ -965,11 +987,11 @@ void hostBackendPump(void) { case SDL_EVENT_MOUSE_BUTTON_UP: pointerMove(e.button.x, e.button.y); if (e.button.button == SDL_BUTTON_LEFT) { - hostedMouseButtonEvent(JL_MOUSE_BUTTON_LEFT, e.button.down); + jlHostedMouseButtonEvent(JL_MOUSE_BUTTON_LEFT, e.button.down); } else if (e.button.button == SDL_BUTTON_RIGHT) { - hostedMouseButtonEvent(JL_MOUSE_BUTTON_RIGHT, e.button.down); + jlHostedMouseButtonEvent(JL_MOUSE_BUTTON_RIGHT, e.button.down); } else if (e.button.button == SDL_BUTTON_MIDDLE) { - hostedMouseButtonEvent(JL_MOUSE_BUTTON_MIDDLE, e.button.down); + jlHostedMouseButtonEvent(JL_MOUSE_BUTTON_MIDDLE, e.button.down); } break; @@ -1005,7 +1027,7 @@ void hostBackendPump(void) { case SDL_EVENT_GAMEPAD_AXIS_MOTION: #if defined(__PSP__) // The stick rests while the keyboard is open. - if (pspKeyboardIsOpen()) { + if (jlPspKeyboardIsOpen()) { break; } #endif @@ -1015,11 +1037,11 @@ void hostBackendPump(void) { case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: #if defined(__PSP__) - if (pspKeyboardButton(e.gbutton.button, e.gbutton.down)) { + if (jlPspKeyboardButton(e.gbutton.button, e.gbutton.down)) { keyboardTook(e.gbutton.which); break; } - if (!pspKeyboardIsOpen()) { + if (!jlPspKeyboardIsOpen()) { padUpdate(e.gbutton.which); } pspButtonKey(e.gbutton.button, e.gbutton.down); @@ -1042,7 +1064,7 @@ void hostBackendPump(void) { // The whole frame every time: the GPU draws it whatever changed (and on the // PSP the keyboard over it). -void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t rectCount) { +void jlHostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t rectCount) { SDL_FRect dst; (void)rects; @@ -1054,21 +1076,21 @@ void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t frameRect(&dst); SDL_RenderTexture(gRenderer, gTexture, NULL, &dst); #if defined(__PSP__) - pspKeyboardDraw(gRenderer); + jlPspKeyboardDraw(gRenderer); #endif SDL_RenderPresent(gRenderer); } -void hostBackendSleepUntil(uint64_t micros) { +void jlHostBackendSleepUntil(uint64_t micros) { uint64_t now; - now = hostBackendMicros(); + now = jlHostBackendMicros(); if (micros > now) { #if defined(__EMSCRIPTEN__) // The browser's turn, in whole milliseconds rounded up. emscripten_sleep((unsigned int)((micros - now + US_PER_MS - 1u) / US_PER_MS)); - gWebYieldUs = hostBackendMicros(); + gWebYieldUs = jlHostBackendMicros(); #else SDL_DelayPrecise((micros - now) * NS_PER_US); #endif @@ -1078,18 +1100,27 @@ void hostBackendSleepUntil(uint64_t micros) { #if defined(__EMSCRIPTEN__) // The page's folder, mounted the first time it is asked for -// (webWriteFolderMount): the program may log or save before jlInit. -const char *hostBackendWriteFolder(void) { +// (jlWebWriteFolderMount): the program may log or save before jlInit. +const char *jlHostBackendWriteFolder(void) { if (gWebWriteFolder[0] == '\0') { - webWriteFolderMount(gWebWriteFolder, (int)sizeof(gWebWriteFolder)); + jlWebWriteFolderMount(gWebWriteFolder, (int)sizeof(gWebWriteFolder)); } return gWebWriteFolder; } +#elif defined(__APPLE__) +// The program's Application Support folder, made the first time it is asked +// for (the program may log or save before jlInit); "" when it cannot be. +const char *jlHostBackendWriteFolder(void) { + if (gAppleWriteFolder == NULL) { + gAppleWriteFolder = SDL_GetPrefPath("", programName()); + } + return gAppleWriteFolder != NULL ? gAppleWriteFolder : ""; +} #endif // ----- HAL API (alphabetical) ----- -// Free bytes where the program runs, saturated at what a uint32_t holds. On +// Free bytes where the program saves, saturated at what a uint32_t holds. On // the PSP: the device its folder is on ("ms0:"), asked for its free clusters. uint32_t jlpDiskFree(void) { #if defined(__PSP__) @@ -1105,7 +1136,7 @@ uint32_t jlpDiskFree(void) { uint64_t bytes; #if defined(__PSP__) - base = hostBackendProgramFolder(); + base = jlHostBackendProgramFolder(); end = SDL_strchr(base, PSP_DEVICE_END); if (end == NULL) { return 0u; @@ -1122,9 +1153,9 @@ uint32_t jlpDiskFree(void) { } bytes = (uint64_t)size.freeClusters * size.sectorCount * size.sectorSize; #elif defined(__EMSCRIPTEN__) - bytes = (uint64_t)webStorageFree(); + bytes = (uint64_t)jlWebStorageFree(); #else - if (statvfs(hostBackendProgramFolder(), &fs) != 0) { + if (statvfs(jlHostBackendWriteFolder(), &fs) != 0) { return 0u; } bytes = (uint64_t)fs.f_bavail * (uint64_t)fs.f_frsize; diff --git a/src/hosted/win16/spriteMergeRows.asm b/src/hosted/win16/spriteMergeRows.asm new file mode 100644 index 0000000..811c9d8 --- /dev/null +++ b/src/hosted/win16/spriteMergeRows.asm @@ -0,0 +1,131 @@ +; Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +; See LICENSE.md for license information. +; SPDX-License-Identifier: MIT +; +; The Win16 port's sprite interpreter rows: src/x86/spriteMergeRows.s, the +; DOS and DVX ports', in Open Watcom's assembler, which does not read GAS. +; The same routine instruction for instruction; a change goes to both. +; +; jlX86SpriteMergeRows(dst, src, keepOffset, rows, count, srcStride) +; +; Each of rows (>= 1) rows is count (1..160) bytes of +; dst = (dst & keep) | src +; with keep read keepOffset bytes past src; rows are srcStride source and +; ROW_BYTES destination bytes apart. A row's whole dwords run in an unrolled +; block of MERGE_DWORDS merges, entered count / 4 merges from its end, then +; a clipped row's count & 3 bytes run in the tail. The merges address the +; row through byte displacements from esi and edi, which point BLOCK_BIAS +; bytes past where the block's first merge reads; no displacement is 0, so +; every merge is MERGE_SIZE bytes. +; +; Registers in the row loop: +; esi = src (biased) edi = dst (biased) ebx = keepOffset +; ebp = the block entry ecx = the tail entry or the row end +; eax, edx = the merge +; +; ABI: cdecl (sprite.c declares it SPRITE_CODE_CALL), ebx/esi/edi/ebp kept. + + .386 + .model flat + +ROW_BYTES equ 160 ; JL_SURFACE_BYTES_PER_ROW +MERGE_DWORDS equ 40 ; ROW_BYTES / 4: the widest clipped row +MERGE_SIZE equ 15 ; bytes of code a dword merge +BLOCK_BIAS equ 126 ; displacements -126..+30 + +; The arguments past the 4 saved registers and the return address. +ARG_DST equ 20 +ARG_SRC equ 24 +ARG_KEEPOFFSET equ 28 +ARG_ROWS equ 32 +ARG_COUNT equ 36 ; then the tail bytes (count & 3) +ARG_SRCSTRIDE equ 40 + + .code + public _jlX86SpriteMergeRows + + align 4 +_jlX86SpriteMergeRows proc near + push ebx + push esi + push edi + push ebp + mov eax, [esp + ARG_COUNT] + mov edx, eax + and edx, 3 + mov [esp + ARG_COUNT], edx ; the tail bytes + mov ecx, offset rowEnd + jz haveTail + mov ecx, offset tail +haveTail: + shr eax, 2 ; the whole dwords + mov edx, eax + shl edx, 4 + sub edx, eax ; * MERGE_SIZE + mov ebp, offset blockEnd + sub ebp, edx + ; The block's first merge reads 4 * (MERGE_DWORDS - dwords) + ; - BLOCK_BIAS past esi and edi; make that the row's start. + shl eax, 2 + sub eax, 4 * MERGE_DWORDS - BLOCK_BIAS + mov esi, [esp + ARG_SRC] + add esi, eax + mov edi, [esp + ARG_DST] + add edi, eax + mov ebx, [esp + ARG_KEEPOFFSET] + jmp ebp + + align 4 +block: +disp = -BLOCK_BIAS + rept MERGE_DWORDS + mov eax, [esi + disp] + mov edx, [esi + ebx + disp] + and edx, [edi + disp] + or edx, eax + mov [edi + disp], edx +disp = disp + 4 + endm +blockEnd: + jmp ecx + +; A clipped row's last 1..3 bytes, just past the block's last dword. +tail: + mov al, [esi + disp] + mov dl, [esi + ebx + disp] + and dl, [edi + disp] + or dl, al + mov [edi + disp], dl + cmp dword ptr [esp + ARG_COUNT], 1 + je rowEnd + mov al, [esi + disp + 1] + mov dl, [esi + ebx + disp + 1] + and dl, [edi + disp + 1] + or dl, al + mov [edi + disp + 1], dl + cmp dword ptr [esp + ARG_COUNT], 2 + je rowEnd + mov al, [esi + disp + 2] + mov dl, [esi + ebx + disp + 2] + and dl, [edi + disp + 2] + or dl, al + mov [edi + disp + 2], dl +rowEnd: + add esi, [esp + ARG_SRCSTRIDE] + add edi, ROW_BYTES + dec dword ptr [esp + ARG_ROWS] + jz done + jmp ebp +done: + pop ebp + pop edi + pop esi + pop ebx + ret +_jlX86SpriteMergeRows endp + + if (blockEnd - block) ne (MERGE_DWORDS * MERGE_SIZE) + .err <MERGE_SIZE does not match the emitted merge> + endif + + end diff --git a/src/hosted/win16/win16Backend.c b/src/hosted/win16/win16Backend.c index a8d28ee..f7daf7b 100644 --- a/src/hosted/win16/win16Backend.c +++ b/src/hosted/win16/win16Backend.c @@ -18,14 +18,14 @@ // base, so the code arena runs where it is. // // Everything runs on the program's thread, as on Windows 3.x it must: the -// window's messages are dispatched by hostBackendPump (and while a frame +// window's messages are dispatched by jlHostBackendPump (and while a frame // wait sleeps), and the sound goes out through waveOut buffers handed over // there, with no callback. A frame wait yields to the other programs through // the message loop. // // The frame is the window's own (HOSTED_FRAME_INDEXED): the core hands over // each presented row's changed pixels as the stage holds them -// (hostBackendFrameRow), and this backend expands them, 8 bits a pixel, +// (jlHostBackendFrameRow), and this backend expands them, 8 bits a pixel, // scaled to the window as it goes. Each pixel is its SCB palette's base and // its colour (the bases, below), through a colour // table holding all 16 palettes, so SCB palettes are exact and a palette @@ -117,7 +117,7 @@ // What a draw costs on a 386SX-16: once (us), and for each window pixel it // draws and each stage pixel it expands as it does (ns). Changed // rectangles closer than one draw's cost are drawn as one -// (hostedRectsMerge). WinGBitBlt from the canvas, which the rows were +// (jlHostedRectsMerge). WinGBitBlt from the canvas, which the rows were // expanded into as they were presented; SetBitmapBits and BitBlt; and // SetDIBitsToDevice, which matches every pixel's colour. #define DRAW_WING_US 1300u @@ -302,7 +302,7 @@ extern uint8_t farByte(uint16_t sel, uint32_t at); #pragma aux farByte = "push es" "mov es, ax" "mov al, es:[edi]" "pop es" __parm [__ax] [__edi] __value [__al] // UBER's display probe (examples/uber with UBER_PROBE). -uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +uint32_t jlHostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); // The program's own main, which WinMain calls. int main(int argc, char **argv); @@ -600,9 +600,9 @@ static int32_t gCanvasStep = 0; static uint32_t gCanvasRow[JL_SURFACE_HEIGHT]; static LogPaletteT gLogPalette; static HPALETTE gPalette = NULL; -// The core's colour table (hostBackendColors), where each palette's sixteen +// The core's colour table (jlHostBackendColors), where each palette's sixteen // are in the frame, the palettes the SCBs use, and whether the identity -// layout is in force (hostBackendPaletteBases). +// layout is in force (jlHostBackendPaletteBases). static const uint32_t *gColors = NULL; static uint8_t gBases[JL_SURFACE_PALETTE_COUNT]; static uint16_t gUsed = 0; @@ -612,10 +612,10 @@ static uint32_t gTimeWraps = 0; static uint64_t gJoyPolledUs = 0; static HWAVEOUT gWaveOut = NULL; static AudioBufferT gAudio[AUDIO_BUFFERS]; -static uint16_t gAudioNext = 0; -static uint16_t gAudioFill = 0; -static uint16_t gAudioBits = AUDIO_BITS_NARROW; // the sample size the device took -static uint32_t gAudioRate = 0u; // the rate it took +static uint16_t gAudioNext = 0; +static uint16_t gAudioFill = 0; +static uint16_t gAudioBits = AUDIO_BITS_NARROW; // the sample size the device took +static uint32_t gAudioRate = 0u; // the rate it took static const uint32_t gAudioRates[AUDIO_RATE_COUNT] = { AUDIO_RATE_SB, HOSTED_AUDIO_RATE }; // Open Watcom's license (the Sybase Open Watcom Public License, section @@ -625,7 +625,7 @@ static const uint32_t gAudioRates[AUDIO_RATE_COUNT] = { AUDIO_RATE_SB, HOST // where. This is the program's copy, never shown; it is not static, so the // compiler keeps it although nothing reads it. docs/Portability.adoc gives // the same text for a program's documentation. -const char hostWin16OwplNotice[] = "This software contains code from Open Watcom. Its source code is available under the terms of the Sybase Open Watcom Public License version 1.0 from https://github.com/open-watcom/open-watcom-v2"; +const char jlHostWin16OwplNotice[] = "This software contains code from Open Watcom. Its source code is available under the terms of the Sybase Open Watcom Public License version 1.0 from https://github.com/open-watcom/open-watcom-v2"; // ----- Internal helpers (alphabetical) ----- @@ -693,7 +693,7 @@ static void buttonsRelease(void) { for (button = 0u; button < JL_MOUSE_BUTTON_COUNT; button++) { if ((gButtonsDown & (1u << button)) != 0u) { - hostedMouseButtonEvent((jlMouseButtonE)button, false); + jlHostedMouseButtonEvent((jlMouseButtonE)button, false); } } if (gButtonsDown != 0u) { @@ -704,7 +704,7 @@ static void buttonsRelease(void) { // Whether a typed character comes as a chord, which types nothing -// (inputCharTyped): Ctrl held without Alt. AltGr is Ctrl+Alt to Windows, +// (jlInputCharTyped): Ctrl held without Alt. AltGr is Ctrl+Alt to Windows, // and types its characters. static bool charChord(void) { return (GetKeyState(VK_CONTROL) & KEY_STATE_DOWN) != 0 && (GetKeyState(VK_MENU) & KEY_STATE_DOWN) == 0; @@ -911,24 +911,24 @@ static void joysticksPoll(void) { uint64_t now; UINT js; - now = hostBackendMicros(); + now = jlHostBackendMicros(); if (now - gJoyPolledUs < JOY_POLL_US) { return; } gJoyPolledUs = now; for (js = 0; js < JL_JOYSTICK_COUNT; js++) { if (joyGetPos(JOYSTICKID1 + js, &info) != JOYERR_NOERROR || joyGetDevCaps(JOYSTICKID1 + js, &caps, sizeof(caps)) != JOYERR_NOERROR) { - if (gJoyConnected[js]) { - hostedJoystickConnect((jlJoystickE)js, false); + if (jlgJoyConnected[js]) { + jlHostedJoystickConnect((jlJoystickE)js, false); } continue; } - if (!gJoyConnected[js]) { - hostedJoystickConnect((jlJoystickE)js, true); + if (!jlgJoyConnected[js]) { + jlHostedJoystickConnect((jlJoystickE)js, true); } - hostedJoystickMove((jlJoystickE)js, inputAxisScale((int32_t)info.wXpos, (int32_t)caps.wXmin, (int32_t)caps.wXmax), inputAxisScale((int32_t)info.wYpos, (int32_t)caps.wYmin, (int32_t)caps.wYmax)); - hostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_0, (info.wButtons & JOY_BUTTON_0_BIT) != 0); - hostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_1, (info.wButtons & JOY_BUTTON_1_BIT) != 0); + jlHostedJoystickMove((jlJoystickE)js, jlInputAxisScale((int32_t)info.wXpos, (int32_t)caps.wXmin, (int32_t)caps.wXmax), jlInputAxisScale((int32_t)info.wYpos, (int32_t)caps.wYmin, (int32_t)caps.wYmax)); + jlHostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_0, (info.wButtons & JOY_BUTTON_0_BIT) != 0); + jlHostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_1, (info.wButtons & JOY_BUTTON_1_BIT) != 0); } } @@ -963,7 +963,7 @@ static void mouseButton(HWND window, jlMouseButtonE button, bool down) { ReleaseCapture(); } } - hostedMouseButtonEvent(button, down); + jlHostedMouseButtonEvent(button, down); } @@ -1072,7 +1072,7 @@ static void pointerMove(int32_t windowX, int32_t windowY) { stageY = (windowY - dst.top) * JL_SURFACE_HEIGHT / (dst.bottom - dst.top); stageX = stageX < 0 ? 0 : (stageX >= JL_SURFACE_WIDTH ? JL_SURFACE_WIDTH - 1 : stageX); stageY = stageY < 0 ? 0 : (stageY >= JL_SURFACE_HEIGHT ? JL_SURFACE_HEIGHT - 1 : stageY); - hostedMouseMove((int16_t)stageX, (int16_t)stageY); + jlHostedMouseMove((int16_t)stageX, (int16_t)stageY); } @@ -1111,7 +1111,7 @@ static void probeCapture(HDC dc, const RECT *dst, uint16_t rowBytes, uint8_t *ou // The frame's pixels that differ from the stage's, each worked out from the -// stage's own pixel and SCB (hostedProbeFrameMismatch's count, for a frame +// stage's own pixel and SCB (jlHostedProbeFrameMismatch's count, for a frame // this backend keeps). static uint32_t probeFrameMismatch(const jlSurfaceT *src) { uint32_t mismatches; @@ -1123,7 +1123,7 @@ static uint32_t probeFrameMismatch(const jlSurfaceT *src) { mismatches = 0; for (y = 0; y < JL_SURFACE_HEIGHT; y++) { for (x = 0; x < JL_SURFACE_WIDTH; x++) { - pair = hostedProbeStagePixel(src, x, y); + pair = jlHostedProbeStagePixel(src, x, y); want = (uint8_t)(gBases[pair >> NIBBLE_BITS] + (pair & NIBBLE_MASK)); if (probeFramePixel(x, y) != want) { mismatches++; @@ -1177,7 +1177,7 @@ static uint32_t probeIdentity(const jlSurfaceT *src, HDC dc, const uint8_t *scre continue; } for (c = 0; c < JL_SURFACE_COLORS_PER_PALETTE; c++) { - want = hostedProbeStageRgb(src, (uint8_t)p, (uint8_t)c); + want = jlHostedProbeStageRgb(src, (uint8_t)p, (uint8_t)c); got = &system[gBases[p] + c]; if (got->peRed != (BYTE)(want >> RGB24_RED_SHIFT) || got->peGreen != (BYTE)(want >> RGB24_GREEN_SHIFT) || got->peBlue != (BYTE)want) { mismatches++; @@ -1207,7 +1207,7 @@ static void programFolder(char *path, char **name) { static void scaleFit(uint16_t outW, uint16_t outH) { uint16_t i; - hostedScaleMaps(outW, outH, gColMap, gRowMap); + jlHostedScaleMaps(outW, outH, gColMap, gRowMap); for (i = 0; i <= JL_SURFACE_WIDTH; i++) { gColFirst[i] = (uint16_t)(((uint32_t)i * outW + JL_SURFACE_WIDTH - 1u) / JL_SURFACE_WIDTH); } @@ -1246,7 +1246,7 @@ static void show(HDC dc, const HostedRectT *rects, uint16_t rectCount) { ddbsFree(); canvasMake(outW, outH); // The canvas (or, without one, the snapshot) holds the frame again. - hostedFrameRefresh(); + jlHostedFrameRefresh(); gRepaintAll = true; } previous = NULL; @@ -1271,7 +1271,7 @@ static void show(HDC dc, const HostedRectT *rects, uint16_t rectCount) { } else { callPixels = DRAW_DIB_US * NS_PER_US / (DRAW_EXPAND_STAGE_NS + DRAW_DIB_OUT_NS * outPixels / (JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT)); } - count = hostedRectsMerge(rects, rectCount, callPixels, merged); + count = jlHostedRectsMerge(rects, rectCount, callPixels, merged); for (i = 0; i < count; i++) { spanDraw(dc, &dst, &merged[i]); } @@ -1283,7 +1283,7 @@ static void show(HDC dc, const HostedRectT *rects, uint16_t rectCount) { // Stage rectangle rect into the window, whose frame is at dst: the output -// pixels showing it (hostedScaleRect) blitted from the canvas with WinG; +// pixels showing it (jlHostedScaleRect) blitted from the canvas with WinG; // else expanded from the snapshot a band of rows at a time through gBand, // into the device bitmap that fits it and blitted (8-bit palette displays // in the identity layout), or as a bottom-up DIB of the band's own size for @@ -1300,7 +1300,7 @@ static void spanDraw(HDC dc, const RECT *dst, const HostedRectT *rect) { outW = (uint16_t)(dst->right - dst->left); outH = (uint16_t)(dst->bottom - dst->top); - hostedScaleRect(rect, outW, outH, &span); + jlHostedScaleRect(rect, outW, outH, &span); if (span.w == 0u || span.h == 0u) { return; } @@ -1397,10 +1397,10 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA return 0; } if (wParam == VK_DELETE) { - inputCharTyped(JL_CHAR_DELETE, charChord()); + jlInputCharTyped(JL_CHAR_DELETE, charChord()); } if ((lParam & KEY_REPEAT_BIT) == 0) { - hostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), true); + jlHostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), true); } // Alt and F10 would open the (absent) window menu. return 0; @@ -1408,12 +1408,12 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA case WM_KEYUP: case WM_SYSKEYUP: scan = (uint8_t)((lParam >> KEY_SCAN_SHIFT) & KEY_SCAN_MASK); - hostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), false); + jlHostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), false); return 0; case WM_CHAR: if (wParam < ASCII_LIMIT) { - inputCharTyped((uint8_t)wParam, charChord()); + jlInputCharTyped((uint8_t)wParam, charChord()); } return 0; @@ -1450,7 +1450,7 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA case WM_KILLFOCUS: // Windows 3.x has no WM_CAPTURECHANGED: losing the focus is // how a press another window took the release of shows. - hostedKeysRelease(); + jlHostedKeysRelease(); buttonsRelease(); return 0; @@ -1558,7 +1558,7 @@ static bool wingOpen(void) { // ----- Backend API (alphabetical) ----- -void hostBackendAudioClose(void) { +void jlHostBackendAudioClose(void) { uint16_t i; if (gWaveOut == NULL) { @@ -1582,7 +1582,7 @@ void hostBackendAudioClose(void) { // The device, and its buffers: each locked in place for good, as the driver // reads them while the program runs on. -uint32_t hostBackendAudioOpen(void) { +uint32_t jlHostBackendAudioOpen(void) { uint32_t rate; uint16_t r; uint16_t i; @@ -1603,7 +1603,7 @@ uint32_t hostBackendAudioOpen(void) { for (i = 0; i < AUDIO_BUFFERS; i++) { gAudio[i].memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, sizeof(AudioBlockT)); if (gAudio[i].memory == NULL) { - hostBackendAudioClose(); + jlHostBackendAudioClose(); return 0u; } gAudio[i].block16 = (DWORD)GlobalLock(gAudio[i].memory); @@ -1621,7 +1621,7 @@ uint32_t hostBackendAudioOpen(void) { // Into the buffer being filled, as the device's samples (the core's 16-bit // ones, or unsigned 8-bit); each full one is written. The core never queues // more than the buffers hold, so the next one has always played. -void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { +void jlHostBackendAudioQueue(const int16_t *frames, uint32_t count) { AudioBufferT *buffer; uint32_t i; @@ -1650,7 +1650,7 @@ void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { // Frames written and not yet played, and those waiting in the buffer being // filled. -uint32_t hostBackendAudioQueued(void) { +uint32_t jlHostBackendAudioQueued(void) { uint32_t frames; uint16_t i; @@ -1664,8 +1664,8 @@ uint32_t hostBackendAudioQueued(void) { } -void hostBackendClose(void) { - hostBackendAudioClose(); +void jlHostBackendClose(void) { + jlHostBackendAudioClose(); ddbsFree(); wingClose(); if (gWindow != NULL) { @@ -1704,7 +1704,7 @@ void hostBackendClose(void) { // In the identity layout the screen's pixels are the reserved entries // themselves: a palette in use is animated, nothing redrawn. Otherwise the // tables change and the window is redrawn in the new colours. -void hostBackendColors(const uint32_t *colors, uint16_t palettes) { +void jlHostBackendColors(const uint32_t *colors, uint16_t palettes) { uint16_t p; uint16_t c; uint16_t first; @@ -1741,7 +1741,7 @@ void hostBackendColors(const uint32_t *colors, uint16_t palettes) { // Into the canvas, expanded and scaled, as WinG will show it; else into the // snapshot as it is. -void hostBackendFrameRow(const uint8_t *pixels, uint8_t scb, uint16_t y, uint16_t x0, uint16_t x1) { +void jlHostBackendFrameRow(const uint8_t *pixels, uint8_t scb, uint16_t y, uint16_t x0, uint16_t x1) { gPresented = true; if (gCanvas != NULL) { canvasRow(pixels, scb, y, x0, x1); @@ -1752,18 +1752,18 @@ void hostBackendFrameRow(const uint8_t *pixels, uint8_t scb, uint16_t y, uint16_ } -void hostBackendInputPoll(void) { +void jlHostBackendInputPoll(void) { joysticksPoll(); } -bool hostBackendMousePresent(void) { +bool jlHostBackendMousePresent(void) { return GetSystemMetrics(SM_MOUSEPRESENT) != 0; } // timeGetTime's milliseconds, carried past its 49.7-day wrap. -uint64_t hostBackendMicros(void) { +uint64_t jlHostBackendMicros(void) { DWORD now; now = timeGetTime(); @@ -1775,7 +1775,7 @@ uint64_t hostBackendMicros(void) { } -const char *hostBackendOpen(void) { +const char *jlHostBackendOpen(void) { WNDCLASS windowClass; RECT frame; HDC dc; @@ -1881,7 +1881,7 @@ const char *hostBackendOpen(void) { // exit() from anywhere -- the program's own, or closing the window -- // must still give the sound device back. if (!gAtExit) { - atexit(hostBackendClose); + atexit(jlHostBackendClose); gAtExit = true; } return NULL; @@ -1890,7 +1890,7 @@ const char *hostBackendOpen(void) { // The identity layout on a palette display while the SCBs use few enough // palettes, each used one a block in palette order; else (palette << 4). -bool hostBackendPaletteBases(uint16_t used) { +bool jlHostBackendPaletteBases(uint16_t used) { uint8_t next[JL_SURFACE_PALETTE_COUNT]; uint16_t count; uint16_t p; @@ -1929,7 +1929,7 @@ bool hostBackendPaletteBases(uint16_t used) { // The executable's folder, with its trailing separator, found once. -const char *hostBackendProgramFolder(void) { +const char *jlHostBackendProgramFolder(void) { static char folder[PATH_BYTES]; char *name; @@ -1941,12 +1941,12 @@ const char *hostBackendProgramFolder(void) { } -void hostBackendPump(void) { +void jlHostBackendPump(void) { messagesDispatch(); } -void hostBackendShow(const HostedRectT *rects, uint16_t rectCount) { +void jlHostBackendShow(const HostedRectT *rects, uint16_t rectCount) { HDC dc; if (rectCount == 0u && !gRepaintAll) { @@ -1960,8 +1960,8 @@ void hostBackendShow(const HostedRectT *rects, uint16_t rectCount) { // Windows 3.x has no sleep: the message loop gives the time to the other // programs until the moment comes. -void hostBackendSleepUntil(uint64_t micros) { - while (hostBackendMicros() < micros) { +void jlHostBackendSleepUntil(uint64_t micros) { + while (jlHostBackendMicros() < micros) { messagesDispatch(); } } @@ -1979,7 +1979,7 @@ uint32_t jlpDiskFree(void) { unsigned drive; uint64_t bytes; - folder = hostBackendProgramFolder(); + folder = jlHostBackendProgramFolder(); drive = 0u; if (folder[0] != '\0' && folder[1] == ':') { drive = (unsigned)(toupper((unsigned char)folder[0]) - 'A' + 1); @@ -2016,7 +2016,7 @@ bool jlpSaveDirEnsure(const char *dir) { // against Windows'). Every comparison is of the screen's raw pixels, read // back the same way, so no colour matching comes into it; the window is // left fully repainted. *compared: every pixel compared. -uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { +uint32_t jlHostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { RECT dst; HDC dc; HPALETTE previous; @@ -2063,7 +2063,7 @@ uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared gRepaintAll = true; show(dc, NULL, 0); probeCapture(dc, &dst, rowBytes, repainted); - mismatches += hostedProbeDiffer(piecemeal, repainted, w, h, rowBytes, bitsPixel); + mismatches += jlHostedProbeDiffer(piecemeal, repainted, w, h, rowBytes, bitsPixel); *compared += (uint32_t)w * h; flat = (uint8_t *)malloc((size_t)JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT); if (flat != NULL && (w % JL_SURFACE_WIDTH) == 0u && (h % JL_SURFACE_HEIGHT) == 0u) { @@ -2086,7 +2086,7 @@ uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared } FreeAlias16(flat16); probeCapture(dc, &dst, rowBytes, stretched); - mismatches += hostedProbeDiffer(repainted, stretched, w, h, rowBytes, bitsPixel); + mismatches += jlHostedProbeDiffer(repainted, stretched, w, h, rowBytes, bitsPixel); *compared += (uint32_t)w * h; gRepaintAll = true; show(dc, NULL, 0); diff --git a/src/hosted/win16/win16SpriteMerge.c b/src/hosted/win16/win16SpriteMerge.c deleted file mode 100644 index ffe071a..0000000 --- a/src/hosted/win16/win16SpriteMerge.c +++ /dev/null @@ -1,61 +0,0 @@ -// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. -// See LICENSE.md for license information. -// SPDX-License-Identifier: MIT -// -// The sprite interpreter's rows on Win16 (src/core/sprite.c's -// x86SpriteMergeRows), the draw for an uncompiled sprite or one partly off -// the surface. DOS and DVX run src/x86/spriteMergeRows.s, which Watcom does -// not assemble; this is the same merge, dst = (dst & keep) | src with keep -// keepOffset bytes past src, a dword at a time while whole dwords last (an -// unclipped row is all whole dwords: sprite.c pads them), then a clipped -// row's last bytes. - -#include <stdint.h> - -#include "joey/surface.h" - -// ----- Prototypes ----- - -void x86SpriteMergeRows(uint8_t *dst, const uint8_t *src, uint32_t keepOffset, uint32_t rows, uint32_t count, uint32_t srcStride); - -// ----- Inline assembly (Watcom #pragma aux, expanded where called) ----- - -// One row of count bytes: count / 4 dwords, then count & 3 bytes. -extern void mergeRow(uint8_t *dst, const uint8_t *src, uint32_t keepOffset, uint32_t count); -#pragma aux mergeRow = \ - "mov edx, ecx" \ - "shr ecx, 2" \ - "jz mergeBytes" \ - "mergeDword: mov eax, [esi + ebx]" \ - "and eax, [edi]" \ - "or eax, [esi]" \ - "mov [edi], eax" \ - "add esi, 4" \ - "add edi, 4" \ - "dec ecx" \ - "jnz mergeDword" \ - "mergeBytes: and edx, 3" \ - "jz mergeDone" \ - "mergeByte: mov al, [esi + ebx]" \ - "and al, [edi]" \ - "or al, [esi]" \ - "mov [edi], al" \ - "inc esi" \ - "inc edi" \ - "dec edx" \ - "jnz mergeByte" \ - "mergeDone: nop" \ - __parm [__edi] [__esi] [__ebx] [__ecx] \ - __modify [__eax __ecx __edx __esi __edi] - -// ----- Sprite API ----- - -void x86SpriteMergeRows(uint8_t *dst, const uint8_t *src, uint32_t keepOffset, uint32_t rows, uint32_t count, uint32_t srcStride) { - uint32_t row; - - for (row = 0; row < rows; row++) { - mergeRow(dst, src, keepOffset, count); - dst += JL_SURFACE_BYTES_PER_ROW; - src += srcStride; - } -} diff --git a/src/hosted/win32/win32Backend.c b/src/hosted/win32/win32Backend.c index 6c984d1..f4227ee 100644 --- a/src/hosted/win32/win32Backend.c +++ b/src/hosted/win32/win32Backend.c @@ -8,7 +8,7 @@ // -- and everything since: only calls those systems have (USER, GDI, WINMM // and KERNEL32, the C library being MSVCRT.DLL). Everything runs on the // program's thread: the window is made there, so its messages queue there -// and hostBackendPump dispatches them, and the sound goes out through waveOut +// and jlHostBackendPump dispatches them, and the sound goes out through waveOut // buffers the pump hands over, with no callback. // // The window shows the 320x200 frame scaled to fill it at the stage's shape, @@ -16,7 +16,7 @@ // it. The backend keeps the frame in a DIB section (top-down 32-bit, as the // core lays it out), and a show copies into it and stretches onto the window // only the rectangles the core says changed, each widened to where the -// stretch's pattern starts over (hostedRectScaled): the whole window, at the +// stretch's pattern starts over (jlHostedRectScaled): the whole window, at the // default 3x, is most of a frame's time on a 95-era machine. It draws the // whole window when it must: uncovered or resized. JOEYLIB_SCALE=N // opens it N times 320 wide (default 3), JOEYLIB_FULLSCREEN=1 on the whole @@ -89,7 +89,7 @@ #define JOY_POLL_US (US_PER_SECOND / HOSTED_FRAME_HZ) // What one StretchBlt's fixed cost is worth in window pixels: changed -// rectangles closer than that are drawn as one (hostedRectsMerge). An +// rectangles closer than that are drawn as one (jlHostedRectsMerge). An // estimate -- there is no 95-era machine here to measure it on; Win16's, // measured on DOSBox's 386, is 1536. #define MERGE_CALL_OUT_PIXELS 4096u @@ -125,7 +125,7 @@ static void show(HDC dc, const HostedRectT *rects, uint16_t rectCount static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam); // UBER's display probe (examples/uber with UBER_PROBE). -uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +uint32_t jlHostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); // ----- Module state ----- @@ -171,7 +171,7 @@ static void blitRect(HDC dc, const RECT *dst, const HostedRectT *rect) { if (outW == 0u || outH == 0u) { return; } - hostedRectScaled(rect, outW, outH, &src, &out); + jlHostedRectScaled(rect, outW, outH, &src, &out); StretchBlt(dc, dst->left + out.x, dst->top + out.y, out.w, out.h, gDibDc, src.x, src.y, src.w, src.h, SRCCOPY); } @@ -202,7 +202,7 @@ static void buttonsRelease(void) { for (button = 0u; button < JL_MOUSE_BUTTON_COUNT; button++) { if ((gButtonsDown & (1u << button)) != 0u) { - hostedMouseButtonEvent((jlMouseButtonE)button, false); + jlHostedMouseButtonEvent((jlMouseButtonE)button, false); } } if (gButtonsDown != 0u) { @@ -213,7 +213,7 @@ static void buttonsRelease(void) { // Whether a typed character comes as a chord, which types nothing -// (inputCharTyped): Ctrl held without Alt. AltGr is Ctrl+Alt to Windows, +// (jlInputCharTyped): Ctrl held without Alt. AltGr is Ctrl+Alt to Windows, // and types its characters. static bool charChord(void) { return (GetKeyState(VK_CONTROL) & KEY_STATE_DOWN) != 0 && (GetKeyState(VK_MENU) & KEY_STATE_DOWN) == 0; @@ -288,7 +288,7 @@ static void joysticksPoll(void) { uint64_t now; UINT js; - now = hostBackendMicros(); + now = jlHostBackendMicros(); if (now - gJoyPolledUs < JOY_POLL_US) { return; } @@ -298,17 +298,17 @@ static void joysticksPoll(void) { info.dwSize = sizeof(info); info.dwFlags = JOY_RETURNX | JOY_RETURNY | JOY_RETURNBUTTONS; if (joyGetPosEx(JOYSTICKID1 + js, &info) != JOYERR_NOERROR || joyGetDevCapsA(JOYSTICKID1 + js, &caps, sizeof(caps)) != JOYERR_NOERROR) { - if (gJoyConnected[js]) { - hostedJoystickConnect((jlJoystickE)js, false); + if (jlgJoyConnected[js]) { + jlHostedJoystickConnect((jlJoystickE)js, false); } continue; } - if (!gJoyConnected[js]) { - hostedJoystickConnect((jlJoystickE)js, true); + if (!jlgJoyConnected[js]) { + jlHostedJoystickConnect((jlJoystickE)js, true); } - hostedJoystickMove((jlJoystickE)js, inputAxisScale((int32_t)info.dwXpos, (int32_t)caps.wXmin, (int32_t)caps.wXmax), inputAxisScale((int32_t)info.dwYpos, (int32_t)caps.wYmin, (int32_t)caps.wYmax)); - hostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_0, (info.dwButtons & JOY_BUTTON_0_BIT) != 0); - hostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_1, (info.dwButtons & JOY_BUTTON_1_BIT) != 0); + jlHostedJoystickMove((jlJoystickE)js, jlInputAxisScale((int32_t)info.dwXpos, (int32_t)caps.wXmin, (int32_t)caps.wXmax), jlInputAxisScale((int32_t)info.dwYpos, (int32_t)caps.wYmin, (int32_t)caps.wYmax)); + jlHostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_0, (info.dwButtons & JOY_BUTTON_0_BIT) != 0); + jlHostedJoyButtonEvent((jlJoystickE)js, JL_JOY_BUTTON_1, (info.dwButtons & JOY_BUTTON_1_BIT) != 0); } } @@ -331,7 +331,7 @@ static void mouseButton(HWND window, jlMouseButtonE button, bool down) { ReleaseCapture(); } } - hostedMouseButtonEvent(button, down); + jlHostedMouseButtonEvent(button, down); } @@ -382,7 +382,7 @@ static void pointerMove(int32_t windowX, int32_t windowY) { stageY = (windowY - dst.top) * JL_SURFACE_HEIGHT / (dst.bottom - dst.top); stageX = stageX < 0 ? 0 : (stageX >= JL_SURFACE_WIDTH ? JL_SURFACE_WIDTH - 1 : stageX); stageY = stageY < 0 ? 0 : (stageY >= JL_SURFACE_HEIGHT ? JL_SURFACE_HEIGHT - 1 : stageY); - hostedMouseMove((int16_t)stageX, (int16_t)stageY); + jlHostedMouseMove((int16_t)stageX, (int16_t)stageY); } @@ -426,7 +426,7 @@ static void show(HDC dc, const HostedRectT *rects, uint16_t rectCount) { dibCopy(&rects[i]); } outPixels = (uint32_t)(dst.right - dst.left) * (uint32_t)(dst.bottom - dst.top); - count = hostedRectsMerge(rects, rectCount, outPixels == 0u ? 0u : MERGE_CALL_OUT_PIXELS * (uint32_t)(JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT) / outPixels, merged); + count = jlHostedRectsMerge(rects, rectCount, outPixels == 0u ? 0u : MERGE_CALL_OUT_PIXELS * (uint32_t)(JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT) / outPixels, merged); for (i = 0; i < count; i++) { blitRect(dc, &dst, &merged[i]); } @@ -449,10 +449,10 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA return 0; } if (wParam == VK_DELETE) { - inputCharTyped(JL_CHAR_DELETE, charChord()); + jlInputCharTyped(JL_CHAR_DELETE, charChord()); } if ((lParam & KEY_REPEAT_BIT) == 0) { - hostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), true); + jlHostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), true); } // Alt and F10 would open the (absent) window menu. return 0; @@ -460,12 +460,12 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA case WM_KEYUP: case WM_SYSKEYUP: scan = (uint8_t)((lParam >> KEY_SCAN_SHIFT) & KEY_SCAN_MASK); - hostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), false); + jlHostedKeyEvent(scanSet1Key(scan, (lParam & KEY_EXTENDED_BIT) != 0), false); return 0; case WM_CHAR: if (wParam < ASCII_LIMIT) { - inputCharTyped((uint8_t)wParam, charChord()); + jlInputCharTyped((uint8_t)wParam, charChord()); } return 0; @@ -507,7 +507,7 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA return 0; case WM_KILLFOCUS: - hostedKeysRelease(); + jlHostedKeysRelease(); buttonsRelease(); return 0; @@ -534,7 +534,7 @@ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPA // ----- Backend API (alphabetical) ----- -void hostBackendAudioClose(void) { +void jlHostBackendAudioClose(void) { uint16_t i; if (gWaveOut == NULL) { @@ -552,7 +552,7 @@ void hostBackendAudioClose(void) { } -uint32_t hostBackendAudioOpen(void) { +uint32_t jlHostBackendAudioOpen(void) { WAVEFORMATEX format; if (gWaveOut != NULL) { @@ -577,7 +577,7 @@ uint32_t hostBackendAudioOpen(void) { // Into the next buffer, if it has played; the core never queues more than // the buffers hold, so one is always free. -void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { +void jlHostBackendAudioQueue(const int16_t *frames, uint32_t count) { AudioBufferT *buffer; buffer = &gAudio[gAudioNext]; @@ -603,7 +603,7 @@ void hostBackendAudioQueue(const int16_t *frames, uint32_t count) { // Frames in buffers that have not finished playing. -uint32_t hostBackendAudioQueued(void) { +uint32_t jlHostBackendAudioQueued(void) { uint32_t frames; uint16_t i; @@ -617,8 +617,8 @@ uint32_t hostBackendAudioQueued(void) { } -void hostBackendClose(void) { - hostBackendAudioClose(); +void jlHostBackendClose(void) { + jlHostBackendAudioClose(); if (gWindow != NULL) { DestroyWindow(gWindow); gWindow = NULL; @@ -639,19 +639,19 @@ void hostBackendClose(void) { } -void hostBackendInputPoll(void) { +void jlHostBackendInputPoll(void) { joysticksPoll(); } -bool hostBackendMousePresent(void) { +bool jlHostBackendMousePresent(void) { return GetSystemMetrics(SM_MOUSEPRESENT) != 0; } -// The counter's rate is read at hostBackendOpen, or here when the clock is +// The counter's rate is read at jlHostBackendOpen, or here when the clock is // read before jlInit; a machine with no counter reads 0. -uint64_t hostBackendMicros(void) { +uint64_t jlHostBackendMicros(void) { LARGE_INTEGER count; if (gCounterHz.QuadPart == 0 && (!QueryPerformanceFrequency(&gCounterHz) || gCounterHz.QuadPart == 0)) { @@ -662,7 +662,7 @@ uint64_t hostBackendMicros(void) { } -const char *hostBackendOpen(void) { +const char *jlHostBackendOpen(void) { WNDCLASSA windowClass; RECT frame; char folder[MAX_PATH]; @@ -687,7 +687,7 @@ const char *hostBackendOpen(void) { if (gTitle[0] == '\0') { strcpy(gTitle, WINDOW_TITLE_DEFAULT); } - if (!SetCurrentDirectoryA(hostBackendProgramFolder())) { + if (!SetCurrentDirectoryA(jlHostBackendProgramFolder())) { return "cannot make the program's folder the current directory"; } instance = GetModuleHandleA(NULL); @@ -738,7 +738,7 @@ const char *hostBackendOpen(void) { // exit() from anywhere -- the program's own, or closing the window -- // must still give the sound device back. if (!gAtExit) { - atexit(hostBackendClose); + atexit(jlHostBackendClose); gAtExit = true; } return NULL; @@ -746,7 +746,7 @@ const char *hostBackendOpen(void) { // The executable's folder, with its trailing separator, found once. -const char *hostBackendProgramFolder(void) { +const char *jlHostBackendProgramFolder(void) { static char folder[MAX_PATH]; char *name; @@ -758,7 +758,7 @@ const char *hostBackendProgramFolder(void) { } -void hostBackendPump(void) { +void jlHostBackendPump(void) { MSG message; while (PeekMessageA(&message, NULL, 0, 0, PM_REMOVE)) { @@ -768,7 +768,7 @@ void hostBackendPump(void) { } -void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t rectCount) { +void jlHostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t rectCount) { HDC dc; if (pixels != gLastFrame) { @@ -786,14 +786,14 @@ void hostBackendShow(const uint32_t *pixels, const HostedRectT *rects, uint16_t // Sleep the whole milliseconds, then give the time slice away until the // moment comes. -void hostBackendSleepUntil(uint64_t micros) { +void jlHostBackendSleepUntil(uint64_t micros) { uint64_t now; - now = hostBackendMicros(); + now = jlHostBackendMicros(); if (micros > now + US_PER_MS) { Sleep((DWORD)((micros - now) / US_PER_MS)); } - while (hostBackendMicros() < micros) { + while (jlHostBackendMicros() < micros) { Sleep(0); } } @@ -806,7 +806,7 @@ uint32_t jlpDiskFree(void) { ULARGE_INTEGER available; const char *folder; - folder = hostBackendProgramFolder(); + folder = jlHostBackendProgramFolder(); if (!GetDiskFreeSpaceExA(folder[0] != '\0' ? folder : NULL, &available, NULL, NULL)) { return 0u; } @@ -837,7 +837,7 @@ bool jlpSaveDirEnsure(const char *dir) { // stretching the core's frame itself (the DIB section checked against it). // Every comparison is of the screen's raw pixels, read back the same way; // the window is left fully repainted. *compared: every pixel compared. -uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { +uint32_t jlHostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared) { RECT dst; HDC dc; uint8_t *piecemeal; @@ -854,7 +854,7 @@ uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared if (gLastFrame == NULL) { return 0; } - mismatches = hostedProbeFrameMismatch(src); + mismatches = jlHostedProbeFrameMismatch(src); *compared = (uint32_t)JL_SURFACE_WIDTH * JL_SURFACE_HEIGHT; frameRect(&dst); w = (uint16_t)(dst.right - dst.left); @@ -875,12 +875,12 @@ uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared show(dc, NULL, 0); GdiFlush(); probeCapture(dc, &dst, size, repainted); - mismatches += hostedProbeDiffer(piecemeal, repainted, w, h, rowBytes, bitsPixel); + mismatches += jlHostedProbeDiffer(piecemeal, repainted, w, h, rowBytes, bitsPixel); SetStretchBltMode(dc, COLORONCOLOR); StretchDIBits(dc, dst.left, dst.top, w, h, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, gLastFrame, &gFrameInfo, DIB_RGB_COLORS, SRCCOPY); GdiFlush(); probeCapture(dc, &dst, size, stretched); - mismatches += hostedProbeDiffer(repainted, stretched, w, h, rowBytes, bitsPixel); + mismatches += jlHostedProbeDiffer(repainted, stretched, w, h, rowBytes, bitsPixel); *compared += 2u * (uint32_t)w * h; gRepaintAll = true; show(dc, NULL, 0); diff --git a/src/iigs/audio_full.c b/src/iigs/audio_full.c index 3a7ccf2..1b8d196 100644 --- a/src/iigs/audio_full.c +++ b/src/iigs/audio_full.c @@ -18,14 +18,14 @@ #include <string.h> // Memory Manager via <iigs/toolbox.h>. Handles/pointers are void* and the -// owner ID is the one iigsMemUserId() (hal.c) fetched from MMStartUp once +// owner ID is the one jlIigsMemUserId() (hal.c) fetched from MMStartUp once // for the whole program; _toolErr collapses to 0 so each `|| _toolErr != 0` // guard reduces to the NULL-handle failure check. The Memory Manager // attribute bits are port.h's. #include <iigs/toolbox.h> typedef void *Handle; typedef void *Pointer; -#define _ownerid (iigsMemUserId()) +#define _ownerid (jlIigsMemUserId()) #define _toolErr 0 #include "port.h" @@ -201,6 +201,7 @@ static uint16_t docFreqWord(uint16_t rateHz) { return (uint16_t)w; } + // PSG-style tone voices: each voice's SFX slot holds a RESIDENT // full-amplitude square wave (whole periods so retriggers seam), and // per-note changes are just stream-struct pokes -- pitch rides the @@ -258,23 +259,26 @@ static const uint8_t kDocVolume[AGI_ATTEN_OFF] = { // The noise's rate is the YM2149's on every port: 125000 / pitch Hz // (pitch 0 as 1), here up to the DOC's one sample byte a tick (26 kHz), // which pitches 0 to 4 ask for more than. -#define NOISE_SUSTAIN_MUL 41u // len = rate * 41 / 512 -> ~80 ms sustain #define NOISE_LFSR_SEED 0xACE1u #define NOISE_LFSR_TAPS 0xB400u // 16-bit maximal Galois LFSR, period 65535 -// How long a noise burst is left alone before a repeat call retriggers -// it, in 60 Hz GetTick units. jlpAudioNoise plays NOISE_SUSTAIN_MUL/512 -// of a second of sample (41/512 s = 80 ms = 4.8 ticks), so anything -// below that retriggers before the burst ends and the sound is gapless. -// -// It has to hold for less than HALF the burst, not just under all of it. -// At 4 ticks a caller running at 60 Hz retriggered at 4 ticks and was -// gapless, but a caller running at the ~19 fps Space Taxi manages on the -// IIgs lands at 3.2 ticks -- inside the hold, so its call did nothing and -// the burst then expired at 4.8 with the next call not due until 6.3. -// That 25 ms hole per frame was the stuttering jet. 2 ticks retriggers -// for any caller slower than 33 ms and merely costs a held music note -// one extra trigger per 33 ms. -#define NOISE_HOLD_TICKS 2u +// A retrigger's start offset rotates within the run's first 1 KB. +#define NOISE_PHASE_MASK 0x3FFu +// A burst plays the rest of the run: what the start offset and NTP's +// discarded tail (SFX_STREAM_TAIL) leave, 4080 - 1023 - 512 = 2545 bytes, +// 98 ms at the top rate and 631 ms at pitch 31. Every retrigger restarts +// NTPstreamsound, which copies the sample into DOC RAM, so the longer the +// burst, the rarer that is: a held noise costs a retrigger a frame with a +// short one. +#define NOISE_SUSTAIN_BYTES (NOISE_BYTES - NOISE_PHASE_MASK - SFX_STREAM_TAIL) +// A burst is left alone for HALF its length before a repeat call +// retriggers it (jlpAudioNoise works the ticks out per pitch), not just +// for under all of it. A caller slower than the hold retriggers on every +// call, which is gapless while it calls within the burst; a faster one +// can land just inside the hold, do nothing, and call next as much as +// one interval after the hold ends, so the hold plus that interval has to +// fit the burst. At 4 ticks of a 4.8-tick burst a caller at the ~19 fps +// Space Taxi manages on the IIgs (3.2 ticks) did exactly that: a 25 ms +// hole a frame, the stuttering jet. // ----- Streaming SFX (jlpAudioPlaySfxStream) ----- // @@ -357,11 +361,6 @@ static const uint8_t gToneMarker = 0; // makes noiseEnsure regenerate in both cases. static const uint8_t gNoiseMarker = 0; -// Retrigger phase rotation for the noise voice: restarting the same -// bytes every per-frame re-assert adds a faint 60 Hz tint to held -// noise; rotating the start offset (stride co-prime with the buffer) -// decorrelates the retriggers. Max start 1023 + max len 2082 = 3105, -// inside the 4080-byte area. // Per-slot stereo placement (jlAudioSetPan). Index is AUDIO_PAN_*; the DOC // pans per oscillator, so a centred slot sounds both of its playing // oscillators and a panned one mutes the wrong side. @@ -369,17 +368,44 @@ static uint8_t gSlotPan[JL_AUDIO_SFX_SLOTS] = { SFX_PAN_CENTER, SFX_PAN_CENTER, SFX_PAN_CENTER, SFX_PAN_CENTER, SFX_PAN_CENTER }; +// Retrigger phase rotation for the noise voice: restarting the same +// bytes every per-frame re-assert adds a faint 60 Hz tint to held +// noise; rotating the start offset (stride co-prime with the buffer) +// decorrelates the retriggers. Max start 1023 + max len 2082 = 3105, +// inside the 4080-byte area. static uint16_t gNoisePhase = 0; // GetTick deadline for the burst currently sounding on the noise slot. static uint16_t gNoiseHoldUntil = 0; // Volume the sounding burst was started at; a change forces a restart // rather than a frequency-only update. static uint8_t gNoiseVol = 0; -// The same pair per tone voice: the GetTick by which the running -// square must be restarted, and the volume it was started at. +// The freq word the sounding burst's oscillators hold, so a re-assert at +// the same pitch writes no DOC register; the GetTick it was triggered at; +// and the hold of the fastest rate it has played at since (jlpAudioNoise +// bounds what is left of the burst with them). +static uint16_t gNoiseFreqWord = 0; +static uint16_t gNoiseTrigTick = 0; +static uint16_t gNoiseHoldFast = 0; +// The same per tone voice: the GetTick by which the running square must +// be restarted, the volume it was started at, the square, and its +// oscillators' freq word. static uint16_t gToneHoldUntil[JL_AUDIO_VOICES] = { 0 }; static uint8_t gToneVol[JL_AUDIO_VOICES] = { 0 }; static uint8_t gTonePeriod[JL_AUDIO_VOICES] = { 0 }; // the square sounding: TONE_PERIOD_BYTES or TONE_LONG_PERIOD_BYTES +static uint16_t gToneFreqWord[JL_AUDIO_VOICES] = { 0 }; + +// The rate a pitch or a note plays at costs 32-bit divides, each a +// runtime helper call on the 65816, and a held sound is re-asserted every +// frame. The noise keeps each of the API's pitches' freq word and hold +// (noiseTiming), worked out at its first use; a freq word of 0 is not +// worked out yet, as docFreqWord never returns one. A tone keeps its last +// note's freq word; its 0 Hz is none, as a 0 Hz note is silence and never +// gets that far. +#define NOISE_PITCHES 32u +static uint16_t gNoiseFreqWordOf[NOISE_PITCHES] = { 0 }; +static uint8_t gNoiseHoldTicksOf[NOISE_PITCHES] = { 0 }; +static uint16_t gToneMemoHz[JL_AUDIO_VOICES] = { 0 }; +static uint16_t gToneMemoFreqWord[JL_AUDIO_VOICES] = { 0 }; // What each voice and the noise were last told, so jlpAudioFrameTick can // hold them: the DOC plays our samples once (NTPstreamsound has no loop), @@ -440,7 +466,7 @@ typedef char iigsToneLongSplit[(TONE_LONG_MAX_HZ == 346u) ? 1 : -1]; // Self-modifying call stub, executed by ntpCall. Bakes the X/Y/A // register loads AND the JSL target into the buffer, which keeps the // inline asm block free of absolute-global operands entirely -- all it -// contains is `jsl gCallStub`. +// contains is `jsl jlgCallStub`. // // The asm block declares "a", "x", "y" clobbers: the stub loads all // three and NTP is free to trash them, so a caller value the compiler @@ -462,10 +488,10 @@ typedef char iigsToneLongSplit[(TONE_LONG_MAX_HZ == 346u) ? 1 : -1]; // 15: 8F lo hi bk STA >gNtpCarry // 19: 6B RTL // GLOBAL (not static): referenced by name from ntpCall's inline -// `jsl gCallStub`; link816 resolves .text relocations only against +// `jsl jlgCallStub`; link816 resolves .text relocations only against // global symbols, so a file-local buffer links as "unresolved -// gCallStub". -unsigned char gCallStub[26]; +// jlgCallStub". +unsigned char jlgCallStub[26]; // The carry the last NTP call returned (the stub stores it): NTPprepare // sets it when it refuses a module. @@ -473,63 +499,66 @@ static volatile uint16_t gNtpCarry; // The VBL heartbeat tick (peislam.s), 60 Hz NTSC / 50 Hz PAL. Declared // here rather than shared because only the holds below need it. -extern uint16_t iigsGetTickWord(void); +extern uint16_t jlIigsGetTickWord(void); // sfxCopyUnsigned's byte loop (sfxCopy.s). -extern void iigsSfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint16_t length, uint16_t padTo); +extern void jlIigsSfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint16_t length, uint16_t padTo); // ----- Internal helpers ----- static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a); static uint16_t docFreqWord(uint16_t rateHz); +static void docRetuneOscPair(uint8_t firstOsc, uint16_t freqWord); static bool loadNTP(void); static void modEndCheck(void); static void noiseEnsure(uint8_t slot, uint32_t slotBase); +static void noiseTiming(uint8_t pitch, uint16_t *freqWord, uint16_t *holdTicks); static void ntpArm(void); static bool ntpCall(uint32_t entry, uint16_t x, uint16_t y, uint16_t a); static void ntpDisarm(void); static void ntpSilentModule(uint32_t base); static void sfxApplyPan(unsigned char *sfx, uint8_t slot, uint8_t vol); static void sfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint32_t length); -static void sfxFillStruct(uint8_t slot, uint32_t slotBase, uint32_t length, uint16_t rateHz); -static void sfxTrigger(uint32_t structAddr); static uint32_t sfxDeclaredLength(uint32_t bytes); +static void sfxFillStruct(uint8_t slot, uint32_t slotBase, uint32_t length, uint16_t rateHz); static uint32_t sfxMemoryFill(void *ctx, int8_t *dst, uint32_t count); +static void sfxTrigger(uint32_t structAddr); static bool streamChunk(uint8_t slot); static void streamStart(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, bool raw); static void toneSquareEnsure(uint8_t slot, uint32_t slotBase); static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) { - gCallStub[0] = 0xC2; - gCallStub[1] = 0x30; - gCallStub[2] = 0xA2; - gCallStub[3] = (unsigned char)(x & 0xFFu); - gCallStub[4] = (unsigned char)((x >> 8) & 0xFFu); - gCallStub[5] = 0xA0; - gCallStub[6] = (unsigned char)(y & 0xFFu); - gCallStub[7] = (unsigned char)((y >> 8) & 0xFFu); - gCallStub[8] = 0xA9; - gCallStub[9] = (unsigned char)(a & 0xFFu); - gCallStub[10] = (unsigned char)((a >> 8) & 0xFFu); - gCallStub[11] = 0x22; - gCallStub[12] = (unsigned char)(target & 0xFFu); - gCallStub[13] = (unsigned char)((target >> 8) & 0xFFu); - gCallStub[14] = (unsigned char)((target >> 16) & 0xFFu); - gCallStub[15] = 0xC2; - gCallStub[16] = 0x30; - gCallStub[17] = 0xA9; - gCallStub[18] = 0x00; - gCallStub[19] = 0x00; - gCallStub[20] = 0x2A; - gCallStub[21] = 0x8F; - gCallStub[22] = (unsigned char)((uint32_t)&gNtpCarry & 0xFFu); - gCallStub[23] = (unsigned char)(((uint32_t)&gNtpCarry >> 8) & 0xFFu); - gCallStub[24] = (unsigned char)(((uint32_t)&gNtpCarry >> 16) & 0xFFu); - gCallStub[25] = 0x6B; + jlgCallStub[0] = 0xC2; + jlgCallStub[1] = 0x30; + jlgCallStub[2] = 0xA2; + jlgCallStub[3] = (unsigned char)(x & 0xFFu); + jlgCallStub[4] = (unsigned char)((x >> 8) & 0xFFu); + jlgCallStub[5] = 0xA0; + jlgCallStub[6] = (unsigned char)(y & 0xFFu); + jlgCallStub[7] = (unsigned char)((y >> 8) & 0xFFu); + jlgCallStub[8] = 0xA9; + jlgCallStub[9] = (unsigned char)(a & 0xFFu); + jlgCallStub[10] = (unsigned char)((a >> 8) & 0xFFu); + jlgCallStub[11] = 0x22; + jlgCallStub[12] = (unsigned char)(target & 0xFFu); + jlgCallStub[13] = (unsigned char)((target >> 8) & 0xFFu); + jlgCallStub[14] = (unsigned char)((target >> 16) & 0xFFu); + jlgCallStub[15] = 0xC2; + jlgCallStub[16] = 0x30; + jlgCallStub[17] = 0xA9; + jlgCallStub[18] = 0x00; + jlgCallStub[19] = 0x00; + jlgCallStub[20] = 0x2A; + jlgCallStub[21] = 0x8F; + jlgCallStub[22] = (unsigned char)((uint32_t)&gNtpCarry & 0xFFu); + jlgCallStub[23] = (unsigned char)(((uint32_t)&gNtpCarry >> 8) & 0xFFu); + jlgCallStub[24] = (unsigned char)(((uint32_t)&gNtpCarry >> 16) & 0xFFu); + jlgCallStub[25] = 0x6B; } + // Retune the noise slot's oscillator pair without restarting the sample. // // This is what the C64 does: level H's door sweep rewrites SID voice 2's @@ -596,13 +625,6 @@ static bool loadNTP(void) { } -// Ensure slot 3's DOC sample area holds the resident noise sample and -// its stream struct's constant bytes are built. 1-bit maximal-LFSR -// noise mapped to 0xFF/0x01 (a 0x00 byte would halt the oscillator -- -// the DOC wave-end rule), so the YM-style "spectral edge = shift -// rate" semantic carries over. One-time fill, redone only after -// jlpAudioPlaySfx (or a stray tone fill) clobbers the slot and -// retargets its cache away from gNoiseMarker. // A song played once has ended when NTP's own playing flag (published by // ntpIrqWindow.py at NTP_PLAYING_ADDR) is 0 -- which gNTPPlaying, set at // the start, never sees; the flag is also 0 if NTPprepare refused the @@ -624,6 +646,13 @@ static void modEndCheck(void) { } +// Ensure slot 3's DOC sample area holds the resident noise sample and +// its stream struct's constant bytes are built. 1-bit maximal-LFSR +// noise mapped to 0xFF/0x01 (a 0x00 byte would halt the oscillator -- +// the DOC wave-end rule), so the YM-style "spectral edge = shift +// rate" semantic carries over. One-time fill, redone only after +// jlpAudioPlaySfx (or a stray tone fill) clobbers the slot and +// retargets its cache away from gNoiseMarker. static void noiseEnsure(uint8_t slot, uint32_t slotBase) { unsigned char *sfx; unsigned char *dst; @@ -663,6 +692,31 @@ static void noiseEnsure(uint8_t slot, uint32_t slotBase) { } +// A noise pitch's DOC freq word and how many beats its burst is held +// (NOISE_SUSTAIN_BYTES at its rate, halved), kept for the API's pitches. +static void noiseTiming(uint8_t pitch, uint16_t *freqWord, uint16_t *holdTicks) { + uint32_t hz; + uint16_t rate; + + if (pitch < NOISE_PITCHES && gNoiseFreqWordOf[pitch] != 0u) { + *freqWord = gNoiseFreqWordOf[pitch]; + *holdTicks = gNoiseHoldTicksOf[pitch]; + return; + } + hz = audioNoiseHz(pitch); + rate = NOISE_RATE_MAX; + if (hz < NOISE_RATE_MAX) { + rate = (uint16_t)hz; + } + *freqWord = docFreqWord(rate); + *holdTicks = (uint16_t)(((uint32_t)NOISE_SUSTAIN_BYTES * gBeatHz) / ((uint32_t)rate * 2u)); + if (pitch < NOISE_PITCHES) { + gNoiseFreqWordOf[pitch] = *freqWord; + gNoiseHoldTicksOf[pitch] = (uint8_t)*holdTicks; + } +} + + // Put NTP's DOC interrupt in place without starting any music. // // SFX streaming is not self-contained: NTPstreamsound only primes the @@ -687,7 +741,7 @@ static void ntpArm(void) { } -// Call an NTP entry point through gCallStub. +// Call an NTP entry point through jlgCallStub. // // Every site brackets the JSL with PHP/SEI ... PLP and saves DBR and // DP. NTP is an interrupt-driven third-party replayer: it may return @@ -705,7 +759,7 @@ static bool ntpCall(uint32_t entry, uint16_t x, uint16_t y, uint16_t a) { "sei\n\t" "phb\n\t" "phd\n\t" - "jsl gCallStub\n\t" + "jsl jlgCallStub\n\t" "pld\n\t" "plb\n\t" "plp" @@ -813,7 +867,7 @@ static void sfxApplyPan(unsigned char *sfx, uint8_t slot, uint8_t vol) { // whatever the slot held before, very often zeros. 0x80 is unsigned // silence and plays harmlessly if it is ever reached. // Safe in place (dst == src), which is how streamChunk uses it. The bytes -// themselves are iigsSfxCopyUnsigned's (sfxCopy.s): 57 ms for a full slot, +// themselves are jlIigsSfxCopyUnsigned's (sfxCopy.s): 57 ms for a full slot, // where the C loop it replaces took 0.73 s. // length never exceeds SFX_SAMPLE_BYTES (both callers bound it), so it // and padTo, an even page multiple or SFX_SAMPLE_BYTES itself, fit a word. @@ -824,7 +878,7 @@ static void sfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint32_t len if (padTo > (uint32_t)SFX_SAMPLE_BYTES) { padTo = (uint32_t)SFX_SAMPLE_BYTES; } - iigsSfxCopyUnsigned(dst, src, (uint16_t)length, (uint16_t)padTo); + jlIigsSfxCopyUnsigned(dst, src, (uint16_t)length, (uint16_t)padTo); } @@ -890,7 +944,7 @@ static void sfxFillStruct(uint8_t slot, uint32_t slotBase, uint32_t length, uint // would silence every SFX in an application that plays no music, which // is all of them today. // -// While hal.c's interrupt-latency bound is tight (gIigsIrqTight, e.g. a +// While hal.c's interrupt-latency bound is tight (jlgIigsIrqTight, e.g. a // serial port is open), NTP is asked to bound itself (NTP_IRQ_WINDOW), so // no stretch of the call, and no refill interrupt, masks for more than // ~1.4 ms. The word is set before every trigger, so it follows the bound @@ -899,7 +953,7 @@ static void sfxTrigger(uint32_t structAddr) { if (!gNTPArmed) { return; } - *(volatile uint16_t *)(gNTPBase + NTP_IRQ_WINDOW) = gIigsIrqTight; + *(volatile uint16_t *)(gNTPBase + NTP_IRQ_WINDOW) = jlgIigsIrqTight; (void)ntpCall(gNTPBase + NTP_ENTRY_STREAMSOUND, (uint16_t)(structAddr & 0xFFFFu), (uint16_t)((structAddr >> 16) & 0xFFu), 0); } @@ -959,7 +1013,7 @@ static void toneSquareEnsure(uint8_t slot, uint32_t slotBase) { gSfxSlotSample[slot] = &gToneMarker; gSfxSlotLength[slot] = 0; gSfxSlotRateHz[slot] = 0; - gToneHoldUntil[slot] = iigsGetTickWord(); // nothing of ours is sounding + gToneHoldUntil[slot] = jlIigsGetTickWord(); // nothing of ours is sounding gTonePeriod[slot] = TONE_PERIOD_BYTES; } @@ -1015,6 +1069,7 @@ bool jlpAudioInit(void) { gNTPReady = true; gBeatHz = jlpFrameHz(); + memset(gNoiseFreqWordOf, 0, sizeof(gNoiseFreqWordOf)); // the holds are in these beats // Lay the tone voices' square waves into their slots now: the 4 KB // fill is ~30 ms of far-pointer stores, which used to land on the @@ -1086,15 +1141,19 @@ bool jlpAudioIsPlayingMod(void) { // Noise voice: retrigger the resident slot-3 noise sample, same -// one-shot model as the tone voices (~80 ms; the sequencer re-asserts -// held noise per frame). Per call: 10 struct pokes (start pointer, -// length, freq word, volume) + one NTPstreamsound JSL. +// one-shot model as the tone voices (a burst of NOISE_SUSTAIN_BYTES, +// 98 to 631 ms by pitch; jlpAudioFrameTick re-asserts held noise every +// frame). A trigger: 10 struct pokes (start pointer, length, freq word, +// volume) + one NTPstreamsound JSL; a re-assert inside the hold at the +// same pitch: nothing. void jlpAudioNoise(uint8_t pitch, uint8_t atten) { unsigned char *sfx; uint32_t slotBase; uint32_t startAddr; - uint16_t rate; uint16_t freqWord; + uint16_t holdTicks; + uint16_t now; + uint16_t elapsed; uint16_t len; uint8_t vol; @@ -1105,41 +1164,51 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { gHeldNoiseAtten = atten; if (atten >= AGI_ATTEN_OFF) { jlpAudioStopSfx((uint8_t)NOISE_SLOT); - gNoiseHoldUntil = iigsGetTickWord(); // next call may retrigger + gNoiseHoldUntil = jlIigsGetTickWord(); // next call may retrigger return; } + noiseTiming(pitch, &freqWord, &holdTicks); + vol = kDocVolume[atten]; + + // Still sounding at the same volume, inside the burst's hold? Then + // the same pitch is a re-assert, nothing to do, and a new one is a + // pitch change on a running voice, which is exactly what the caller + // means: retune the oscillators and leave the sample alone (level H's + // door sweep rewrites the pitch every tick, and a restart would re-copy + // the sample into DOC RAM each time). What is left of the burst then + // plays at the new rate, so the hold is worked out again for it: no + // more of the burst has gone than `elapsed` beats (rounded up) of its + // 2 * gNoiseHoldFast at the fastest rate it has played at, and the + // rest lasts at least that share of twice the new rate's hold, of + // which the hold is again half. + // Wrapping compare: GetTick is a 16-bit counter. + now = jlIigsGetTickWord(); + if (vol == gNoiseVol && (int16_t)(now - gNoiseHoldUntil) < 0) { + if (freqWord != gNoiseFreqWord) { + docRetuneOscPair((uint8_t)(SFX_BASE_OSC + NOISE_SLOT * SFX_OSCS_PER_SLOT), freqWord); + gNoiseFreqWord = freqWord; + if (holdTicks < gNoiseHoldFast) { + gNoiseHoldFast = holdTicks; + } + elapsed = (uint16_t)(now - gNoiseTrigTick + 1u); + gNoiseHoldUntil = now; + if (elapsed < 2u * gNoiseHoldFast) { + gNoiseHoldUntil = (uint16_t)(now + (uint16_t)((holdTicks * (2u * gNoiseHoldFast - elapsed)) / (2u * gNoiseHoldFast))); + } + } + return; + } slotBase = gSfxBase + (uint32_t)NOISE_SLOT * (uint32_t)SFX_SLOT_BYTES; noiseEnsure((uint8_t)NOISE_SLOT, slotBase); - rate = NOISE_RATE_MAX; - if (audioNoiseHz(pitch) < NOISE_RATE_MAX) { - rate = (uint16_t)audioNoiseHz(pitch); - } - freqWord = docFreqWord(rate); - // The sustain, and the tail NTP discards (SFX_STREAM_TAIL), all noise. - len = (uint16_t)((((uint32_t)rate * NOISE_SUSTAIN_MUL) >> 9) + SFX_STREAM_TAIL); - - // Rotate the start offset so per-frame retriggers do not replay - // the same bytes (stride 509 is co-prime with the 1024 window; - // bound: 1023 + 2082 max sustain + 512 tail = 3617 <= NOISE_BYTES). - gNoisePhase = (uint16_t)((gNoisePhase + 509u) & 0x3FFu); + // Rotate the start offset so retriggers do not replay the same bytes + // (stride 509 is co-prime with the 1024 window). + gNoisePhase = (uint16_t)((gNoisePhase + 509u) & NOISE_PHASE_MASK); startAddr = slotBase + SFX_SAMPLE_OFFSET + gNoisePhase; - - vol = kDocVolume[atten]; - - // Still sounding at the same volume? Then this is a pitch change on - // a running voice, which is exactly what the caller means: retune - // the oscillators and leave the sample alone. Only a finished (or - // re-levelled) burst needs the full NTPstreamsound restart below. - // - // Wrapping compare: GetTick is a 16-bit counter, so test the signed - // difference rather than `now < deadline`. - if (vol == gNoiseVol && (int16_t)(iigsGetTickWord() - gNoiseHoldUntil) < 0) { - docRetuneOscPair((uint8_t)(SFX_BASE_OSC + NOISE_SLOT * SFX_OSCS_PER_SLOT), freqWord); - return; - } + // The sustain, and the tail NTP discards (SFX_STREAM_TAIL), all noise. + len = NOISE_SUSTAIN_BYTES + SFX_STREAM_TAIL; sfx = (unsigned char *)slotBase; sfx[0] = (unsigned char)(startAddr & 0xFFu); @@ -1154,7 +1223,10 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { sfxApplyPan(sfx, (uint8_t)NOISE_SLOT, vol); sfxTrigger(slotBase); gNoiseVol = vol; - gNoiseHoldUntil = (uint16_t)(iigsGetTickWord() + NOISE_HOLD_TICKS); + gNoiseFreqWord = freqWord; + gNoiseTrigTick = now; + gNoiseHoldFast = holdTicks; + gNoiseHoldUntil = (uint16_t)(now + holdTicks); } @@ -1228,7 +1300,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 // silence out to the declared length. uint32_t declared = sfxDeclaredLength(length); - iigsSfxCopyUnsigned((unsigned char *)sampleAddr, sample, (uint16_t)length, (uint16_t)declared); + jlIigsSfxCopyUnsigned((unsigned char *)sampleAddr, sample, (uint16_t)length, (uint16_t)declared); sfxFillStruct(slot, slotBase, declared, rateHz); gSfxSlotSample[slot] = sample; @@ -1331,7 +1403,24 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { // which uses the 512-units-per-byte scale the oscillator is actually // programmed with -- this used to inline the 65536 scale and ran every // tone 128x sharp, far above hearing. - freqWord = docFreqWord((uint16_t)(freqHz * period)); + if (freqHz != gToneMemoHz[voice]) { + gToneMemoHz[voice] = freqHz; + gToneMemoFreqWord[voice] = docFreqWord((uint16_t)(freqHz * period)); + } + freqWord = gToneMemoFreqWord[voice]; + + vol = kDocVolume[atten]; + + // Still sounding at this volume, on this square? Retune in place (the + // same pitch is then a no-op) instead of restarting the square every + // pump. Wrapping compare: GetTick is a 16-bit counter. + if (vol == gToneVol[voice] && period == gTonePeriod[voice] && (int16_t)(jlIigsGetTickWord() - gToneHoldUntil[voice]) < 0) { + if (freqWord != gToneFreqWord[voice]) { + docRetuneOscPair(firstOsc, freqWord); + gToneFreqWord[voice] = freqWord; + } + return; + } // Sustain (see TONE_SUSTAIN_DIV), rounded down to whole periods so // restarts seam. @@ -1344,16 +1433,6 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { len = (uint16_t)(squareBytes - SFX_STREAM_TAIL); } - vol = kDocVolume[atten]; - - // Still sounding at this volume, on this square? Retune in place (the - // same pitch is then a no-op) instead of restarting the square every - // pump. Wrapping compare: GetTick is a 16-bit counter. - if (vol == gToneVol[voice] && period == gTonePeriod[voice] && (int16_t)(iigsGetTickWord() - gToneHoldUntil[voice]) < 0) { - docRetuneOscPair(firstOsc, freqWord); - return; - } - // Ticks this trigger sounds for: len bytes at freqHz * period bytes/s. // NTP is told len and the tail it discards (SFX_STREAM_TAIL), all of it // square. @@ -1379,7 +1458,8 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { sfxTrigger(slotBase); gToneVol[voice] = vol; gTonePeriod[voice] = period; - gToneHoldUntil[voice] = (uint16_t)(iigsGetTickWord() + holdTicks); + gToneFreqWord[voice] = freqWord; + gToneHoldUntil[voice] = (uint16_t)(jlIigsGetTickWord() + holdTicks); } @@ -1390,7 +1470,7 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { gTickFn = fn; gTickHz = hz; - gTickLastBeat = iigsGetTickWord(); + gTickLastBeat = jlIigsGetTickWord(); gTickCarry = 0u; return true; } @@ -1474,7 +1554,7 @@ static bool streamChunk(uint8_t slot) { if (ticks > 1u) { ticks--; } - st->holdUntil = (uint16_t)(iigsGetTickWord() + (uint16_t)ticks); + st->holdUntil = (uint16_t)(jlIigsGetTickWord() + (uint16_t)ticks); } return true; } @@ -1520,7 +1600,7 @@ void jlpAudioStopSfx(uint8_t slot) { } gStream[slot].active = false; if (slot < JL_AUDIO_VOICES) { - gToneHoldUntil[slot] = iigsGetTickWord(); // next jlpAudioVoice restarts + gToneHoldUntil[slot] = jlIigsGetTickWord(); // next jlpAudioVoice restarts } firstOsc = (uint8_t)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT); @@ -1543,7 +1623,7 @@ void jlpAudioFrameTick(void) { // next chunk goes out here when the previous one is due to finish. { uint8_t slot; - uint16_t now = iigsGetTickWord(); + uint16_t now = jlIigsGetTickWord(); for (slot = 0u; slot < JL_AUDIO_SFX_SLOTS; slot++) { if (!gStream[slot].active) { @@ -1579,9 +1659,12 @@ void jlpAudioFrameTick(void) { // audio tick (jlpAudioTickRegister) is due as often as its share of // the heartbeats since the last frame says. if (gTickFn != (jlAudioTickFnT)0) { - uint16_t now = iigsGetTickWord(); + uint16_t now = jlIigsGetTickWord(); + uint16_t beats = (uint16_t)(now - gTickLastBeat); - gTickCarry += (uint32_t)(uint16_t)(now - gTickLastBeat) * gTickHz; + // One heartbeat since the last frame, the usual case, is an add: + // the 16x16->32 multiply is a libcall. + gTickCarry += (beats == 1u) ? (uint32_t)gTickHz : (uint32_t)beats * gTickHz; gTickLastBeat = now; while (gTickCarry >= gBeatHz) { gTickCarry -= gBeatHz; diff --git a/src/iigs/hal.c b/src/iigs/hal.c index 46c0810..8e9dcf1 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -37,7 +37,7 @@ // Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing). // Same idiom as src/iigs/audio_full.c: Handles/pointers are void*, -// the owner ID comes from iigsMemUserId(), and the attribute bits are +// the owner ID comes from jlIigsMemUserId(), and the attribute bits are // port.h's. // // _ownerid used to expand to MMStartUp() at EVERY call site. MMStartUp @@ -47,7 +47,7 @@ // ROM monitor inside jlSurfaceCreate, 2026-09-22). One ID, fetched once. #include <iigs/toolbox.h> typedef void *Handle; -#define _ownerid (iigsMemUserId()) +#define _ownerid (jlIigsMemUserId()) // Leading bookkeeping slot: jlAlloc/jlFree traffic in plain pointers but // DisposeHandle needs the Handle, so we over-allocate by one Handle and // stash it just before the pointer we return. @@ -55,18 +55,18 @@ typedef void *Handle; // VBL tick counter in peislam.s: a Misc Toolset heartbeat task that the // firmware VBL interrupt handler bumps every field, read by -// iigsGetTickWord as a same-bank word load. It replaced a _GetTick +// jlIigsGetTickWord as a same-bank word load. It replaced a _GetTick // toolbox call per read (hundreds of cycles, ten-odd reads per Space // Taxi frame). Polling $C019 from C missed transitions for any op over // ~1 ms; the interrupt-driven count stays accurate regardless of the // caller's polling rate. Tick rate matches the video field rate -- 60 Hz -// on NTSC, 50 Hz on PAL. iigsVblInstall/iigsVblRemove wrap SetHeartBeat/ +// on NTSC, 50 Hz on PAL. jlIigsVblInstall/jlIigsVblRemove wrap SetHeartBeat/ // DelHeartBeat for that task and return the Misc Tool Set error (0 = ok). -extern uint16_t iigsGetTickWord(void); -extern uint16_t iigsVblInstall(void); -extern uint16_t iigsVblRemove(void); +extern uint16_t jlIigsGetTickWord(void); +extern uint16_t jlIigsVblInstall(void); +extern uint16_t jlIigsVblRemove(void); // Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. -extern uint16_t iigsReadHzParam(void); +extern uint16_t jlIigsReadHzParam(void); static uint16_t gFrameHz = 60u; // hal.c is the single TU that calls into joeyDraw.asm. The IIgs jlp<Op> @@ -76,11 +76,11 @@ static uint16_t gFrameHz = 60u; // each cross-platform TU brought its own asm extern. // 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen. -extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); +extern void jlIigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); // 16 fixed-offset word stores along a 160-byte stride. Takes the raw // fill nibble; the asm derives the $NNNN fill word so C never pays the // * 0x1111 software-multiply helper (PERF-AUDIT #41). ~120 cyc per call. -extern void iigsTileFillInner(uint8_t *dstRow0, uint16_t nibble); +extern void jlIigsTileFillInner(uint8_t *dstRow0, uint16_t nibble); // Tile copy / paste / snap inner loops. All take 4-byte large- // model pointers; bank may differ between dst and src (heap // surface vs stage). Stride contracts: @@ -90,45 +90,45 @@ extern void iigsTileFillInner(uint8_t *dstRow0, uint16_t nibble); // tilePasteMonoInner is the fused mono colorize + paste (PERF-AUDIT #3): // per-byte 2-bit opacity index (static table) into four per-call fg/bg // combo bytes; fg/bg arrive pre-masked to 4 bits (PERF-AUDIT #69). -extern void iigsTileCopyInner(uint8_t *dstRow0, const uint8_t *srcRow0); -extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent); -extern void iigsTilePasteInner(uint8_t *dstRow0, const uint8_t *srcTilePixels); -extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor); -extern void iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0); +extern void jlIigsTileCopyInner(uint8_t *dstRow0, const uint8_t *srcRow0); +extern void jlIigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent); +extern void jlIigsTilePasteInner(uint8_t *dstRow0, const uint8_t *srcTilePixels); +extern void jlIigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor); +extern void jlIigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0); // Single-pixel and Bresenham line plot. jlDrawLine inner takes // pre-clipped endpoints (caller validates against surface bounds); // it does no per-pixel clipping in the loop. -extern void iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble); -extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble); +extern void jlIigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble); +extern void jlIigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble); // Bresenham midpoint circle outline. Caller has verified the entire // bbox is on-surface so no per-pixel clip. -extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); +extern void jlIigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); // Stage-to-SHR pixel upload: per-row dirty-skipped PEI slam (wide // rows) / exact-extent MVN (narrow rows), $01 -> $E1. SCB/palette // upload is NOT here: uploadScbAndPaletteIfNeeded (below) owns it via // dirty-flag-gated C memcpy, and the asm's old scbPtr/palettePtr/ // uploadFlags args plus their dead MVN upload paths were removed // (PERF-AUDIT #52). -extern void iigsBlitStageToShr(void); -// jlFloodFill walk results: written by iigsFloodWalkAndScansInner, +extern void jlIigsBlitStageToShr(void); +// jlFloodFill walk results: written by jlIigsFloodWalkAndScansInner, // read back by the jlpFloodWalkAndScans macro (core/port.h). -extern uint16_t gFloodSeedMatch; -extern uint16_t gFloodLeftX; -extern uint16_t gFloodRightX; +extern uint16_t jlgFloodSeedMatch; +extern uint16_t jlgFloodLeftX; +extern uint16_t jlgFloodRightX; // Single-call per-popped-seed worker: seed test + walk-left + walk-right // + scan-above + scan-below + push, all sharing cached row addr and -// match decoders. Outputs to gFloodSeedMatch / gFloodLeftX / gFloodRightX. -extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp); -// One-shot init for the y*160 lookup table (gRowOffsetLut, 400 bytes +// match decoders. Outputs to jlgFloodSeedMatch / jlgFloodLeftX / jlgFloodRightX. +extern void jlIigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp); +// One-shot init for the y*160 lookup table (jlgRowOffsetLut, 400 bytes // in DRAWPRIMS data). Called once from jlpInit. After this returns, // every asm primitive that needs row offset can do `lda >lut,x` instead -// of the 7-instruction shift-add. Also builds tpmNibPairIdx (256 -// bytes), iigsTilePasteMonoInner's mono-byte -> opacity-pair index +// of the 7-instruction shift-add. Also builds jlIigsTpmNibPairIdx (256 +// bytes), jlIigsTilePasteMonoInner's mono-byte -> opacity-pair index // table (PERF-AUDIT #3). -extern void iigsInitRowLut(void); +extern void jlIigsInitRowLut(void); // Filled circle, scanline-style. Takes the raw fill nibble; the asm // derives the doubled fill byte and RMW nibble bytes (PERF-AUDIT #41). -extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); +extern void jlIigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); // ----- Hardware addresses (24-bit / long pointers) ----- @@ -175,17 +175,17 @@ extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1 // in jlpInit so the visible bezel matches the cleared SHR background. #define BORDER_COLOR_MASK 0xF0 -// Interrupt windows of the two long interrupts-off paths in joeyDraw.s -// (see gIigsPresentChunkRows). Loose is what they always did and costs -// nothing; tight fits the Z8530 SCC's 3-byte receive FIFO at 9600 baud -// (~3.1 ms): 4 present rows ~1.8 ms, 12 clear blocks ~1.4 ms. +// Interrupt windows of the long interrupts-off paths in joeyDraw.s and +// iigsLayerBlit.s (see jlgIigsPresentChunkRows). Loose is what they always +// did and costs nothing; tight fits the Z8530 SCC's 3-byte receive FIFO at +// 9600 baud (~3.1 ms): 4 present rows ~1.8 ms, 12 clear blocks ~1.4 ms. #define IRQ_PRESENT_ROWS_LOOSE 40u #define IRQ_PRESENT_ROWS_TIGHT 4u #define IRQ_CLEAR_BLOCKS_LOOSE 200u #define IRQ_CLEAR_BLOCKS_TIGHT 12u // Video beam ticks a frame (VERTCNT/HORIZCNT: 65 positions a line), for -// the tight joystick read's beam stamps (iigsJoyPass) and the millisecond +// the tight joystick read's beam stamps (jlIigsJoyPass) and the millisecond // clock's fraction of a frame. The line is VERTCNT * 2 plus HORIZCNT's top // bit. #define BEAM_TICKS_PER_LINE 65u @@ -194,39 +194,48 @@ extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1 #define BEAM_HORIZ_V0_SHIFT 7u #define MS_PER_SECOND 1000u +// The millisecond clock's fraction of a frame without a divide: a line +// is under 1/16 ms in both modes (15.72 and 15.6 lines a ms), so a line +// count over 16 is the milliseconds or one short, and the line the next +// millisecond starts on settles which (gLineMsStart). +#define LINE_MS_SHIFT 4u +#define LINE_MS_STEPS (((BEAM_LINES_PAL - 1u) >> LINE_MS_SHIFT) + 2u) + // ----- Module state ----- // The beam's lines a frame, and the line the heartbeat's tick steps on, // measured in jlpInit: the millisecond clock's fraction of a frame. +// gLineMsStart[k] is the first line k milliseconds into the frame. static uint16_t gBeamLines = BEAM_LINES_NTSC; static uint16_t gVblLine = 0u; +static uint16_t gLineMsStart[LINE_MS_STEPS]; static uint8_t gPreviousNewVideo = 0; static uint8_t gPreviousBorder = 0; static uint8_t gPreviousShadow = 0; static bool gModeSet = false; -// Interrupt-latency bound, read by joeyDraw.s (so not static, and BSS -// like every other C global the asm reads DBR-relative; jlpInit sets -// the loose values). The present's chunked PEI slam re-enables -// interrupts every gIigsPresentChunkRows rows (~0.42 ms a row), the -// stage fast clear every gIigsClearChunkBlocks 160-byte blocks -// (~0.12 ms a block). A client whose interrupt cannot wait ~17 ms holds +// Interrupt-latency bound, read by joeyDraw.s and iigsLayerBlit.s (so not +// static, and BSS like every other C global the asm reads DBR-relative; +// jlpInit sets the loose values). The present's chunked PEI slam +// re-enables interrupts every jlgIigsPresentChunkRows rows (~0.42 ms a +// row), and so does the tile layer's slam (~0.3 ms a row), the stage fast +// clear every jlgIigsClearChunkBlocks 160-byte blocks (~0.12 ms a block). A client whose interrupt cannot wait ~17 ms holds // the tight bound while it needs it; the holds are counted, so clients // nest. Tight costs a full-screen present ~5% (83.1 -> 87.2 ms). -// gIigsIrqTight tells the C paths that bound themselves (input.c's +// jlgIigsIrqTight tells the C paths that bound themselves (input.c's // joystick read, audio_full.c's NinjaTrackerPlus entries) which way to // go. All three follow gIrqTightHolds, through irqLatencyApply alone. -uint16_t gIigsPresentChunkRows; -uint16_t gIigsClearChunkBlocks; -uint16_t gIigsIrqTight; +uint16_t jlgIigsPresentChunkRows; +uint16_t jlgIigsClearChunkBlocks; +uint16_t jlgIigsIrqTight; static uint16_t gIrqTightHolds = 0u; // Beam ticks a frame for the joystick asm's frame wrap (jlpInit). -uint16_t gIigsBeamFrameTicks; +uint16_t jlgIigsBeamFrameTicks; -// SCB / palette upload skipping is now driven by gStageScbDirty / -// gStagePaletteDirty (core/surface.c). The old per-frame memcmp- +// SCB / palette upload skipping is now driven by jlgStageScbDirty / +// jlgStagePaletteDirty (core/surface.c). The old per-frame memcmp- // against-cached-copy approach was costing ~7 ms / frame. // PEI slam scratch (W2 run-hoisted present). File-scope non-static so @@ -236,36 +245,36 @@ uint16_t gIigsBeamFrameTicks; // slam loop needs INSIDE a switches-on run -- the per-row SP offset -- // is parked at $E1:9DFE instead: long-addressed, so immune to the // redirects. -volatile uint16_t gPeiOrigSp; -volatile uint8_t gPeiOrigShadow; -volatile uint16_t gPeiCurRow; // scan resume: absolute row -volatile uint16_t gPeiChunkRow; // scan resume: in-chunk row -volatile uint16_t gPeiMvnCount; // narrow row: MVN byte count - 1 -volatile uint16_t gPeiRunFirst; // slam run: first row index -volatile uint16_t gPeiRunLen; // slam run: row count (1..40) -volatile uint16_t gPeiRunMax; // slam run: rightmost dirty word (low byte valid) -volatile uint16_t gPeiRunBytes; // slam run: bytes pushed per row -volatile uint16_t gPeiWidthTmp; // scan: band width scratch (low byte valid) +volatile uint16_t jlgPeiOrigSp; +volatile uint8_t jlgPeiOrigShadow; +volatile uint16_t jlgPeiCurRow; // scan resume: absolute row +volatile uint16_t jlgPeiChunkRow; // scan resume: in-chunk row +volatile uint16_t jlgPeiMvnCount; // narrow row: MVN byte count - 1 +volatile uint16_t jlgPeiRunFirst; // slam run: first row index +volatile uint16_t jlgPeiRunLen; // slam run: row count (1..40) +volatile uint16_t jlgPeiRunMax; // slam run: rightmost dirty word (low byte valid) +volatile uint16_t jlgPeiRunBytes; // slam run: bytes pushed per row +volatile uint16_t jlgPeiWidthTmp; // scan: band width scratch (low byte valid) // Narrow-row MVN RUN (rows sharing one identical band are set up once). -volatile uint16_t gPeiBandMin; // the run's band, low byte valid -volatile uint16_t gPeiBandMax; -volatile uint16_t gPeiResumeRow; // first row AFTER the run -volatile uint16_t gPeiResumeChunk; -volatile uint16_t gPeiRunOff; // stage byte offset of the run's first row -volatile uint16_t gPeiRunEnd; // offset one row past the run's last -volatile uint16_t gPeiRunStride; // 160 - bytes: advance after an MVN -volatile uint16_t gPeiRunRows; // rows in the run -volatile uint16_t gPeiMulA; // run telemetry: bytes * rows -volatile uint16_t gPeiMulB; -volatile uint16_t gPeiMulOut; -volatile uint16_t gStageGrpTmp; // asm markers: first coarse group index -volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group +volatile uint16_t jlgPeiBandMin; // the run's band, low byte valid +volatile uint16_t jlgPeiBandMax; +volatile uint16_t jlgPeiResumeRow; // first row AFTER the run +volatile uint16_t jlgPeiResumeChunk; +volatile uint16_t jlgPeiRunOff; // stage byte offset of the run's first row +volatile uint16_t jlgPeiRunEnd; // offset one row past the run's last +volatile uint16_t jlgPeiRunStride; // 160 - bytes: advance after an MVN +volatile uint16_t jlgPeiRunRows; // rows in the run +volatile uint16_t jlgPeiMulA; // run telemetry: bytes * rows +volatile uint16_t jlgPeiMulB; +volatile uint16_t jlgPeiMulOut; +volatile uint16_t jlgStageGrpTmp; // asm markers: first coarse group index +volatile uint16_t jlgPeiSkipTmp; // scan: rows skipped by a clean group // Upload SCB / palette into bank-$E1 SHR memory only when the // matching dirty flag is set. Replaces a per-frame 712-byte memcmp // pair (~7 ms / frame) with a 2-cyc flag check. -// gStageScbDirty / gStagePaletteDirty live in core/surface.c; they +// jlgStageScbDirty / jlgStagePaletteDirty live in core/surface.c; they // start true (forces the very first present to upload), get set true // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // get cleared here after upload. @@ -282,14 +291,14 @@ static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t word static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { - if (gStageScbDirty) { + if (jlgStageScbDirty) { copyWords((volatile uint16_t *)IIGS_SHR_SCB, (const uint16_t *)src->scb, JL_SURFACE_HEIGHT / 2u); - gStageScbDirty = false; + jlgStageScbDirty = false; } - if (gStagePaletteDirty) { + if (jlgStagePaletteDirty) { // Only the palettes that changed: a colour-cycling effect touches // one palette a frame, and 32 bytes beats 512 sixteen times over. - uint16_t mask = gStagePaletteDirtyMask; + uint16_t mask = jlgStagePaletteDirtyMask; uint8_t pal; for (pal = 0; mask != 0u && pal < JL_SURFACE_PALETTE_COUNT; pal++) { @@ -298,8 +307,8 @@ static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { } mask >>= 1; } - gStagePaletteDirty = false; - gStagePaletteDirtyMask = 0u; + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = 0u; } } @@ -360,21 +369,22 @@ static void irqLatencyApply(bool tight); bool jlpInit(const jlConfigT *config) { uint16_t tick; uint16_t spin = 0u; + uint16_t k; (void)config; // The heartbeat task is the frame clock; nothing else can wait for a // VBL, so prove it advances before touching anything else (a task // that never runs would spin jlpWaitVBL forever). - if (iigsVblInstall() != 0u) { - coreSetError("the Miscellaneous Tool Set refused the VBL heartbeat task"); + if (jlIigsVblInstall() != 0u) { + jlCoreSetError("the Miscellaneous Tool Set refused the VBL heartbeat task"); return false; } - tick = iigsGetTickWord(); - while (iigsGetTickWord() == tick) { + tick = jlIigsGetTickWord(); + while (jlIigsGetTickWord() == tick) { spin++; if (spin == 0u) { - iigsVblRemove(); - coreSetError("the VBL heartbeat task never ran"); + jlIigsVblRemove(); + jlCoreSetError("the VBL heartbeat task never ran"); return false; } } @@ -395,12 +405,15 @@ bool jlpInit(const jlConfigT *config) { // first present sets up SCB to 320 mode). C-side memset/memcpy to // bank $E1 is unreliable from jlpInit's calling context, so we // don't try it here. - iigsInitRowLut(); + jlIigsInitRowLut(); irqLatencyApply(gIrqTightHolds != 0u); - gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u; - gBeamLines = (gFrameHz == 50u) ? BEAM_LINES_PAL : BEAM_LINES_NTSC; - gIigsBeamFrameTicks = (uint16_t)(gBeamLines * BEAM_TICKS_PER_LINE); - gModeSet = true; + gFrameHz = (jlIigsReadHzParam() == 1u) ? 50u : 60u; + gBeamLines = (gFrameHz == 50u) ? BEAM_LINES_PAL : BEAM_LINES_NTSC; + jlgIigsBeamFrameTicks = (uint16_t)(gBeamLines * BEAM_TICKS_PER_LINE); + for (k = 0u; k < LINE_MS_STEPS; k++) { + gLineMsStart[k] = (uint16_t)(((uint32_t)k * gBeamLines * gFrameHz + MS_PER_SECOND - 1u) / MS_PER_SECOND); + } + gModeSet = true; return true; } @@ -413,14 +426,14 @@ void jlpPresent(const jlSurfaceT *src) { // (The asm's old SCB/palette MVN paths never wrote $E1:9E00 reliably and // were dead once this C path landed; PERF-AUDIT #52 removed them along // with the per-present scbPtr/palettePtr/uploadFlags marshalling, so - // iigsBlitStageToShr is pixels-only and takes no args.) + // jlIigsBlitStageToShr is pixels-only and takes no args.) // The KNOWN RESIDUAL that used to be documented here -- "the PEI-slam // corrupts $E1:9E20+ (palettes 1-15) on a DRAW" -- described correct // data, not a defect. $9E20 is not a corruption boundary: a palette // row is 16 entries x 2 bytes = 32 = $20, so $9E00+$20 = $9E20 is // exactly the palette-0 / palette-1 boundary. DRAW sets ONLY palette 0 // (draw.c's jlPaletteSet(screen, 0, ...) + jlScbSetRange(..., 0)), so - // palettes 1-15 legitimately hold paletteInitDefault's EGA table. The + // palettes 1-15 legitimately hold jlPaletteInitDefault's EGA table. The // "32 bytes intact / 480 bytes wrong" split is the app-wrote-row-0 / // library-defaulted-rows-1-15 split, and it is the only boundary in // the system that lands at $9E20 without a coincidence. @@ -443,13 +456,13 @@ void jlpPresent(const jlSurfaceT *src) { // over rows 190-199 (where a descending-push overrun would land first) // and re-compare $E1 against the stage. uploadScbAndPaletteIfNeeded(src); - iigsBlitStageToShr(); + jlIigsBlitStageToShr(); } // Release one hold on the tight interrupt-latency bound; the last one out // puts the loose bound back. -void iigsIrqLatencyRelax(void) { +void jlIigsIrqLatencyRelax(void) { if (gIrqTightHolds == 0u) { return; } @@ -459,8 +472,8 @@ void iigsIrqLatencyRelax(void) { // Take a hold on the tight interrupt-latency bound (see -// gIigsPresentChunkRows). -void iigsIrqLatencyTighten(void) { +// jlgIigsPresentChunkRows). +void jlIigsIrqLatencyTighten(void) { gIrqTightHolds++; irqLatencyApply(true); } @@ -470,7 +483,7 @@ void iigsIrqLatencyTighten(void) { // (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates // a fresh application ID per call, so it is called exactly once here // and the result cached for the life of the program. -uint16_t iigsMemUserId(void) { +uint16_t jlIigsMemUserId(void) { static uint16_t gMemUserId = 0u; static bool gMemUserIdReady = false; @@ -485,7 +498,7 @@ uint16_t iigsMemUserId(void) { void jlpShutdown(void) { // The task lives in this program's memory: pull it from the heartbeat // queue before GS/OS frees the segments. - iigsVblRemove(); + jlIigsVblRemove(); if (gModeSet) { *IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_BORDER_REG = gPreviousBorder; @@ -513,7 +526,7 @@ void jlpShutdown(void) { // painted over by surface clears (claudeDocs/PERF-AUDIT.md #85, the root of the // #84 sprite pathology). attrAddr claims the exact address; the range // is in special memory (bank $01) by definition. The claim MUST happen -// in jlpInit, BEFORE the first jlpBigAlloc: stageAlloc allocates the +// in jlpInit, BEFORE the first jlpBigAlloc: jlStageAlloc allocates the // stage struct before it calls jlpStageAllocPixels, so claiming here // would already be too late to keep that struct out of the display. static void *gStagePixelsHandle = NULL; @@ -556,7 +569,7 @@ static void claimStagePixels(void) { // proceed rather than fail init, but leave a breadcrumb for // jlLastError. (No _toolErr shim in this toolchain yet; a // NULL handle is the only failure signal.) - coreSetError("stage framebuffer MM reservation failed"); + jlCoreSetError("stage framebuffer MM reservation failed"); } } if (gBank0ScratchHandle == NULL) { @@ -567,7 +580,7 @@ static void claimStagePixels(void) { gBank0ScratchHandle = NewHandle(end - base, _ownerid, attrAddr | attrFixed | attrLocked, (void *)base); } if (gBank0ScratchHandle == NULL) { - coreSetError("bank-0 bss/heap MM reservation failed"); + jlCoreSetError("bank-0 bss/heap MM reservation failed"); } } } @@ -576,9 +589,9 @@ static void claimStagePixels(void) { // Point the chunked-SEI asm, and the C paths that bound themselves, at // the tight or the loose interrupt window. static void irqLatencyApply(bool tight) { - gIigsPresentChunkRows = tight ? IRQ_PRESENT_ROWS_TIGHT : IRQ_PRESENT_ROWS_LOOSE; - gIigsClearChunkBlocks = tight ? IRQ_CLEAR_BLOCKS_TIGHT : IRQ_CLEAR_BLOCKS_LOOSE; - gIigsIrqTight = tight ? 1u : 0u; + jlgIigsPresentChunkRows = tight ? IRQ_PRESENT_ROWS_TIGHT : IRQ_PRESENT_ROWS_LOOSE; + jlgIigsClearChunkBlocks = tight ? IRQ_CLEAR_BLOCKS_TIGHT : IRQ_CLEAR_BLOCKS_LOOSE; + jlgIigsIrqTight = tight ? 1u : 0u; } @@ -611,7 +624,7 @@ void jlpStageFreePixels(uint8_t *pixels) { // the SHIPPED build at ~5.3 ms per 16x16 erase, and the current -S // shows the compiler re-deriving BOTH 32-bit far pointers from stack // slots on EVERY word (~30+ instructions/word -- dead-lever #12's -// pattern). One JSL into iigsCopyRectRows fixes it for good: [dp],y +// pattern). One JSL into jlIigsCopyRectRows fixes it for good: [dp],y // pair copies at ~21 cyc/word with the row stride advanced in the dp // frame. rowBytes is even (the jlSurfaceCopyRect wrapper widens w to // 16-px groups) -- the asm's contract. DOS deliberately stays on the @@ -635,7 +648,7 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint srcLo = (uint16_t)(sB[0] | ((uint16_t)sB[1] << 8)); srcBank = sB[2]; } - iigsCopyRectRows(d, srcLo, (uint16_t)srcBank, (uint16_t)(w >> 1), h); + jlIigsCopyRectRows(d, srcLo, (uint16_t)srcBank, (uint16_t)(w >> 1), h); } @@ -665,14 +678,14 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint void jlpWaitVBL(void) { uint16_t tick; - tick = iigsGetTickWord(); - while (iigsGetTickWord() == tick) { + tick = jlIigsGetTickWord(); + while (jlIigsGetTickWord() == tick) { // spin until the heartbeat advances } } -// (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the +// (jlpFrameCount is a port.h macro straight to jlIigsGetTickWord -- the // heartbeat counter read in peislam.s -- so no C wrapper lives here.) @@ -686,19 +699,29 @@ uint16_t jlpFrameHz(void) { // tick steps on (gVblLine), so the clock steps every line, not every frame. // Read again if the tick steps between the reads. Never earlier than the // last reading: a tick held back while interrupts are masked would make -// the beam's fraction start again before the tick catches up. +// the beam's fraction start again before the tick catches up. The line +// count wraps by subtracting (the same as %, without its divide), and the +// fraction is gLineMsStart's: since * 1000 / (lines * Hz) exactly. uint32_t jlpMillisElapsed(void) { static uint32_t gLastMs = 0u; uint16_t tick; uint16_t since; + uint16_t frac; uint32_t ms; do { - tick = iigsGetTickWord(); + tick = jlIigsGetTickWord(); ms = jlpGenericMillisElapsed(); - since = (uint16_t)((beamLine() + gBeamLines - gVblLine) % gBeamLines); - } while (tick != iigsGetTickWord()); - ms += (uint32_t)since * MS_PER_SECOND / ((uint32_t)gBeamLines * gFrameHz); + since = (uint16_t)(beamLine() + gBeamLines - gVblLine); + } while (tick != jlIigsGetTickWord()); + while (since >= gBeamLines) { + since = (uint16_t)(since - gBeamLines); + } + frac = (uint16_t)(since >> LINE_MS_SHIFT); + if (since >= gLineMsStart[frac + 1u]) { + frac++; + } + ms += frac; if (ms < gLastMs) { ms = gLastMs; } diff --git a/src/iigs/iigsLayerBlit.s b/src/iigs/iigsLayerBlit.s new file mode 100644 index 0000000..15772c3 --- /dev/null +++ b/src/iigs/iigsLayerBlit.s @@ -0,0 +1,393 @@ +; Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +; See LICENSE.md for license information. +; SPDX-License-Identifier: MIT +; +; iigsLayerBlit.s - the IIgs tile layer: the blit of a chunky ring's rows +; (iigsTileLayer.c has the layout and works out the arguments) into the +; stage at $01:2000, and the put of a tile into the rings. +; +; GAS syntax for the llvm-mos w65816 toolchain (ABI notes: joeyDraw.s). +; +; ==================================================================== +; void jlIigsTileLayerBlitRows(const uint8_t *ring, uint16_t y1, +; uint16_t ringBytes, uint16_t n1, +; uint16_t spBottom, uint16_t spTop, +; uint16_t spWrap) +; +; A stack slam: AUXWRITE ($C005) sends bank-0 writes at $0200-$BFFF to +; bank $01, so with SP on the stage's bank-0 addresses an `lda n,y` and a +; `pha` move a ring word onto the stage, 10 cycles a word. Pushes go +; down, so the band goes bottom row first and each row right to left, and +; SP runs from the bottom row's last byte (spBottom) to the byte before +; the band (spTop) with no stepping between rows. +; +; A band row is 80 words read from a ring row at byte b, wrapping at its +; 168th; the first n1 words come before the wrap. Two unrolled runs of 80 +; copies, copy k pushing word 2 (79 - k),y, serve the two stretches of a +; row, each entered through lbRunTab at the copy that leaves its words +; to the run's end, with Y its first source byte: +; run 2, stretch 2 (words n1 .. 79, when n1 < 80): at copy n1, Y = the +; ring row's byte b + 2 n1 - 168 (0 or 1), then on to stretch 1 +; run 1, stretch 1 (words 0 .. n1 - 1): at copy 80 - n1, Y = byte b +; d12 is stretch 1's Y less the row's first stretch's (168 - 2 n1, or 0 +; with one stretch). y1 is the bottom row's stretch-1 Y, and each row +; steps Y back a ring stride; Y is a real ring address, so no read leaves +; the ring's bank. Where the band crosses the ring's first row, spWrap is +; the SP after that row (spTop when it does not): a chunk ends there too, +; and Y goes on by ringBytes, so the rows never test for the wrap. +; +; With SP on the stage any push lands there and bank-0 writes are +; redirected, so the slam only reads: its frame through D (reads are not +; redirected), the ring through DBR, and lbRunTab in the code's bank. +; Interrupts are off while it runs; every jlgIigsPresentChunkRows rows +; (hal.c; ~0.3 ms a row here) AUXWRITE goes off and the real stack comes +; back for a CLI/SEI pair. A chunk ends at the SP it stops at, so the +; slam writes no counter. +; +; ABI: arg0 ring in A:X (only its bank, X's low byte, is used); y1 @4,s, +; ringBytes @6,s, n1 @8,s, spBottom @10,s, spTop @12,s, spWrap @14,s on +; JSL entry. D stays the runtime's direct page, which the Loader +; allocates on a page, so no dp read pays the unaligned-D cycle. The +; values the rows read are in its IMG0 .. IMG5 at $D0 (llvm816: a call +; clobbers IMG0 .. IMG7; IMG8 .. IMG15 at $C0 are callee-saved, by +; W65816ImgCalleeSave); the ones only the interrupt windows read stay on +; the caller's stack, with the chunk's bytes pushed under the arguments. +; The stage is $01:2000, so the SPs are its bank-0 addresses. +; ==================================================================== + + .set LB_RING_ROW, 168 ; a ring row's bytes + .set LB_RING_STRIDE, 170 ; + the 2-byte guard + .set LB_WORDS, 80 + .set LB_COPY_BYTES, 4 ; lda n,y (3) + pha (1) + + .set LB_IMG, 0xD0 ; IMG0, the first a call clobbers + .set LB_IMG_END, 0xE0 + .set LB_CHUNK_SP, LB_IMG ; the SP this chunk stops at + .set LB_FIRST, LB_IMG + 2 ; lbRunTab offset of a row's first stretch + .set LB_E1, LB_IMG + 4 ; lbRunTab offset of stretch 1 after stretch 2 + .set LB_D12, LB_IMG + 6 ; stretch 1's Y less the first stretch's + .set LB_NEXT, LB_IMG + 8 ; stretch 1's Y to the next row's first + .set LB_REAL_SP, LB_IMG + 10 ; the caller's stack + .set LB_CHUNK_BYTES, 1 ; ,s on the caller's stack: stage bytes a chunk + .set LB_ARGS, LB_CHUNK_BYTES + 3 ; + php, phb + .set LB_A_Y1, LB_ARGS + 4 + .set LB_A_RINGBYTES, LB_ARGS + 6 + .set LB_A_N1, LB_ARGS + 8 + .set LB_A_SPBOTTOM, LB_ARGS + 10 + .set LB_A_SPTOP, LB_ARGS + 12 + .set LB_A_SPWRAP, LB_ARGS + 14 + +.section .text.jlIigsTileLayerBlitRows,"ax" + +; One run: copy k pushes word 2 (79 - k),y. +.macro LB_RUN + .set LB_D, 2 * (LB_WORDS - 1) + .rept LB_WORDS + lda LB_D,y + pha + .set LB_D, LB_D - 2 + .endr +.endm + +.globl jlIigsTileLayerBlitRows +jlIigsTileLayerBlitRows: + php + phb + rep #0x30 + .a16 + .i16 + ; a chunk's bytes, 160 a row (128 r + 32 r); DBR is still + ; the globals' bank + lda jlgIigsPresentChunkRows + asl a + asl a + asl a + asl a + asl a + pha + asl a + asl a + clc + adc LB_CHUNK_BYTES,s + sta LB_CHUNK_BYTES,s + tsc + sta LB_REAL_SP + sep #0x20 + .a8 + txa + pha + plb ; DBR = the ring's bank + rep #0x20 + .a16 + ; two stretches when n1 < 80, stretch 2 first + lda LB_A_N1,s + asl a ; 2 n1 + cmp #2 * LB_WORDS + bcs lbOneStretch + sta LB_FIRST ; run 2 at copy n1 + eor #0xFFFF + sec + adc #4 * LB_WORDS + sta LB_E1 ; run 1 at copy 80 - n1: 160 + 2 (80 - n1) + sec + sbc #4 * LB_WORDS - LB_RING_ROW ; d12 = 168 - 2 n1 + bra lbStretches +lbOneStretch: + lda #2 * LB_WORDS + sta LB_FIRST ; run 1 from copy 0 + lda #0 ; d12 +lbStretches: + sta LB_D12 + clc + adc #LB_RING_STRIDE + eor #0xFFFF + inc a + sta LB_NEXT ; -(170 + d12) + lda LB_A_Y1,s + sec + sbc LB_D12 + tay ; Y = the bottom row's first stretch + lda LB_A_SPBOTTOM,s + tax ; X = the slam's SP + sei + jmp lbChunk + +; Byte offset 2 k: run 2's copy k; 2 LB_WORDS + 2 k: run 1's copy k. The +; same section as the jmps, so the same bank. +lbRunTab: + .set LB_K, 0 + .rept LB_WORDS + .word lbRun2 + LB_K * LB_COPY_BYTES + .set LB_K, LB_K + 1 + .endr + .set LB_K, 0 + .rept LB_WORDS + .word lbRun1 + LB_K * LB_COPY_BYTES + .set LB_K, LB_K + 1 + .endr + +lbRun2: + LB_RUN +lbRun2End: + ; the row's stretch 1 + tya + clc + adc LB_D12 + tay + ldx LB_E1 + jmp (lbRunTab,x) + +lbRun1: + LB_RUN +lbRun1End: + ; the row is done and SP is the next row's last byte: Y to + ; that row's first stretch + tya + clc + adc LB_NEXT + tay + tsc + cmp LB_CHUNK_SP + beq lbChunkEnd + ldx LB_FIRST + jmp (lbRunTab,x) + +; The chunk's last row is done (A = SP): an interrupt window, or the end. +lbChunkEnd: + tax ; X = the slam's SP + sep #0x20 + .a8 + sta 0xE0C004 ; AUXWRITE off before any bank-0 write + rep #0x20 + .a16 + lda LB_REAL_SP + tcs ; the caller's stack for an interrupt + cli + txa ; a pending interrupt is taken here + cmp LB_A_SPTOP,s + beq lbDone + sei + cmp LB_A_SPWRAP,s + bne lbChunk + tya + clc + adc LB_A_RINGBYTES,s + tay ; the row above is the ring's last +; Interrupts and AUXWRITE off, the caller's stack; X = the slam's SP, Y = +; the next row's first stretch. The chunk stops LB_CHUNK_BYTES on, or at +; the band's top, or at the wrap while it is ahead. +lbChunk: + txa + sec + sbc LB_CHUNK_BYTES,s + bcs 1f + lda #0 ; below the stage +1: + cmp LB_A_SPTOP,s + bcs 2f + lda LB_A_SPTOP,s +2: + sta LB_CHUNK_SP + txa + cmp LB_A_SPWRAP,s + beq 3f + bcc 3f ; the wrap is behind + lda LB_CHUNK_SP + cmp LB_A_SPWRAP,s + bcs 3f + lda LB_A_SPWRAP,s + sta LB_CHUNK_SP +3: + sep #0x20 + .a8 + sta 0xE0C005 ; AUXWRITE on: bank-0 writes go to bank $01 + rep #0x20 + .a16 + txa + tcs ; SP = the slam + ldx LB_FIRST + jmp (lbRunTab,x) + +lbDone: + pla ; the chunk's bytes + plb + plp + rtl + +.if (lbRun2End - lbRun2) - LB_WORDS * LB_COPY_BYTES +.error "LB_COPY_BYTES does not match run 2" +.endif +.if (lbRun1End - lbRun1) - LB_WORDS * LB_COPY_BYTES +.error "LB_COPY_BYTES does not match run 1" +.endif +.if LB_REAL_SP + 2 > LB_IMG_END +.error "the frame runs past IMG7" +.endif + + +; ==================================================================== +; void jlIigsTileLayerPut(uint8_t *even, uint16_t oddLo, uint16_t oddBank, +; uint16_t tileLo, uint16_t tileBank) +; +; One tile into a slot of the chunky rings (iigsTileLayer.c): its 8 rows +; of 4 bytes into the even ring, and the odd ring's bytes that follow from +; them. even and odd point at the byte BEFORE the slot's first row (the +; byte at 4 c - 1, slot c > 0) in their rings. An odd byte j is even's low +; nibble of byte j and high nibble of byte j + 1, so for three words of +; even, W(k) at byte k, the odd word at k is, big-endian, the 24 bits +; e[k] e[k+1] e[k+2] shifted right a nibble: (xba W(k)) << 4, or'd with +; the high nibble of e[k+2], swapped back. Words 0, 2, and 4 are odd +; bytes 4 c - 1 .. 4 c + 4: the slot's own four and its neighbours' near +; bytes, which come out as they were, from the same even bytes. Each W +; feeds two odd words, so a row reads four (the last one's high byte is +; past the slot's bytes, never used). +; +; Each ring is one block inside a bank (jlpBigAlloc), so its rows step +; with a 16-bit add; the tile may straddle a bank and steps through the +; carry. +; +; ABI: arg0 even in A:X; oddLo @4,s, oddBank @6,s, tileLo @8,s, +; tileBank @10,s on JSL entry. Frame: php+phd, then the row count, a +; scratch word, odd, even, and tile pushed so that D = SP + 1 puts tile at +; dp 0, even at 4, odd at 8, the scratch at 12, and the count at 14. +; ==================================================================== + .set LTP_TILE, 0 + .set LTP_EVEN, 4 + .set LTP_ODD, 8 + .set LTP_TMP, 12 + .set LTP_ROWS, 14 + .set LTP_FRAME, 16 + .set LTP_TILE_ROW, 4 + +.section .text.jlIigsTileLayerPut,"ax" + +.macro LTP_ODD_WORD wNext, oOff + ; A = W(k): odd word k from it and W(k + 2), left in X. + xba + asl a + asl a + asl a + asl a + sta LTP_TMP + ldy #\wNext + lda [LTP_EVEN],y + tax + and #0x00F0 + lsr a + lsr a + lsr a + lsr a + ora LTP_TMP + xba +.if \oOff + ldy #\oOff + sta [LTP_ODD],y +.else + sta [LTP_ODD] +.endif +.endm + +.globl jlIigsTileLayerPut +jlIigsTileLayerPut: + php + phd + rep #0x30 + .a16 + .i16 + tay ; even lo16, held while the frame goes up + pea 8 ; the row count -> dp 14 + pha ; scratch -> dp 12 + lda 13,s ; oddBank + pha ; -> dp 10 + lda 13,s ; oddLo + pha ; -> dp 8 + phx ; even bank -> dp 6 + phy ; even lo16 -> dp 4 + lda 25,s ; tileBank + pha ; -> dp 2 + lda 25,s ; tileLo + pha ; -> dp 0 + tsc + inc a + tcd +ltpRow: + ; the tile row -> even bytes 1..4 + lda [LTP_TILE] + ldy #1 + sta [LTP_EVEN],y + ldy #2 + lda [LTP_TILE],y + ldy #3 + sta [LTP_EVEN],y + ; odd words 0, 2, 4 + lda [LTP_EVEN] + LTP_ODD_WORD 2, 0 + txa + LTP_ODD_WORD 4, 2 + txa + LTP_ODD_WORD 6, 4 + ; next row + lda LTP_EVEN + clc + adc #LB_RING_STRIDE + sta LTP_EVEN + lda LTP_ODD + clc + adc #LB_RING_STRIDE + sta LTP_ODD + lda LTP_TILE + clc + adc #LTP_TILE_ROW + sta LTP_TILE + bcc 1f + inc LTP_TILE+2 +1: + dec LTP_ROWS + bne ltpRow + tsc + clc + adc #LTP_FRAME + tcs + pld + plp + rtl diff --git a/src/iigs/iigsTileLayer.c b/src/iigs/iigsTileLayer.c new file mode 100644 index 0000000..fdfea38 --- /dev/null +++ b/src/iigs/iigsTileLayer.c @@ -0,0 +1,110 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// IIgs tile-layer blit and put. The IIgs keeps the generic chunky rings +// (tileLayerInternal.h, src/generic/genericTileLayer.c); onto the stage at +// $01:2000 it copies them with the stack slam in iigsLayerBlit.s, which +// this works the per-frame arguments out for. Any other surface takes the +// generic blit. A tile goes into the rings through the asm put there, +// except in a ring row's first slot, whose odd byte before it is the +// row's last and whose bytes the guard repeats (the generic put). + +#include <stdint.h> + +#include "joey/tile.h" +#include "port.h" +#include "surfaceInternal.h" +#include "tileLayerInternal.h" + +#define RING_SLOT_BYTES (JL_TILE_PIXELS_PER_SIDE * TILE_LAYER_CHUNKY_STRIDE) +#define BAND_WORDS (JL_SURFACE_BYTES_PER_ROW / 2) + +// iigsLayerBlit.s has these numbers in it (LB_RING_ROW, LB_RING_STRIDE, +// LB_WORDS, and a stage row's 160 bytes). +typedef char iigsTileLayerRingProof[(TILE_LAYER_CHUNKY_BYTES == 168 && TILE_LAYER_CHUNKY_STRIDE == 170 && BAND_WORDS == 80) ? 1 : -1]; + +extern void jlIigsTileLayerBlitRows(const uint8_t *ring, uint16_t y1, uint16_t ringBytes, uint16_t n1, uint16_t spBottom, uint16_t spTop, uint16_t spWrap); +extern void jlIigsTileLayerPut(uint8_t *even, uint16_t oddLo, uint16_t oddBank, uint16_t tileLo, uint16_t tileBank); + +static void farSplit(const void *p, uint16_t *lo, uint16_t *bank); + + +// A far pointer's low 16 bits and bank, through its bytes (no 32-bit shift +// libcall). +static void farSplit(const void *p, uint16_t *lo, uint16_t *bank) { + uint32_t addr; + const uint8_t *b; + + addr = (uint32_t)p; + b = (const uint8_t *)&addr; + *lo = (uint16_t)(b[0] | ((uint16_t)b[1] << 8)); + *bank = b[2]; +} + + +// The slam goes bottom row first (iigsLayerBlit.s). n1 is the band words a +// row takes before the ring's wrap, and y1 the bottom row's ring row address +// in its bank plus b. spBottom and spTop are the band's last byte and the +// byte before it, in the stage's bank-0 addresses, and spWrap the byte +// before the band row on the ring's first row when the band crosses the +// ring's end (band row ringPxRows - ringY), else spTop. +void jlpTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) { + const TileLayerChunkyRingsT *rings; + const uint8_t *ring; + uint16_t b; + uint16_t n1; + uint16_t rows; + uint16_t ringPxRows; + uint16_t bottom; + uint16_t ringB; + uint16_t top; + uint16_t stageLo; + uint16_t spTop; + uint16_t spWrap; + + if (dst != jlgStage) { + jlpGenericTileLayerBlit(dst, layer, ringX, ringY); + return; + } + rings = (const TileLayerChunkyRingsT *)layer->ring; + ring = ((ringX & 1u) != 0u) ? rings->odd : rings->even; + b = (uint16_t)(ringX >> 1); + n1 = (uint16_t)((TILE_LAYER_CHUNKY_BYTES + 1u - b) >> 1); + if (n1 > BAND_WORDS) { + n1 = BAND_WORDS; + } + rows = (uint16_t)((uint16_t)layer->viewHeight << 3); + ringPxRows = (uint16_t)((uint16_t)layer->ringRows << 3); + top = (uint16_t)((uint16_t)layer->viewBy << 3); + stageLo = (uint16_t)(uintptr_t)dst->pixels; + spTop = (uint16_t)(stageLo + SURFACE_ROW_OFFSET(top) - 1u); + spWrap = spTop; + bottom = (uint16_t)(ringY + rows - 1u); + if (bottom >= ringPxRows) { + bottom = (uint16_t)(bottom - ringPxRows); + spWrap = (uint16_t)(stageLo + SURFACE_ROW_OFFSET(top + ringPxRows - ringY) - 1u); + } + ringB = (uint16_t)((uint16_t)(uintptr_t)ring + b); + jlIigsTileLayerBlitRows(ring, (uint16_t)(ringB + bottom * TILE_LAYER_CHUNKY_STRIDE), (uint16_t)((uint16_t)layer->ringRows * RING_SLOT_BYTES), n1, (uint16_t)(stageLo + SURFACE_ROW_OFFSET(top + rows - 1u) + JL_SURFACE_BYTES_PER_ROW - 1u), spTop, spWrap); +} + + +void jlpTileLayerPut(jlTileLayerT *layer, uint8_t slotCol, uint8_t slotRow, const jlTileT *tile) { + TileLayerChunkyRingsT *rings; + uint16_t before; + uint16_t oddLo; + uint16_t oddBank; + uint16_t tileLo; + uint16_t tileBank; + + if (slotCol == 0u) { + jlpGenericTileLayerPut(layer, slotCol, slotRow, tile); + return; + } + rings = (TileLayerChunkyRingsT *)layer->ring; + before = (uint16_t)((uint16_t)slotRow * RING_SLOT_BYTES + ((uint16_t)slotCol << 2) - 1u); + farSplit(rings->odd + before, &oddLo, &oddBank); + farSplit(tile->pixels, &tileLo, &tileBank); + jlIigsTileLayerPut(rings->even + before, oddLo, oddBank, tileLo, tileBank); +} diff --git a/src/iigs/input.c b/src/iigs/input.c index 93164b8..8c51d73 100644 --- a/src/iigs/input.c +++ b/src/iigs/input.c @@ -28,7 +28,7 @@ // of the strobe). Each jlpInputPoll drains pending strobe events to // pick up presses, then samples $C010: bit 7 == 0 means no // non-modifier key is physically held, and we wholesale-clear -// gKeyState. readModifierKeys then re-asserts the modifiers from +// jlgKeyState. readModifierKeys then re-asserts the modifiers from // $C025's live state, so shift/ctrl/option stay accurate. Avoids // the inferred-release lag the old TTL-decay scheme had, and works // on every IIgs (real or stealth) without ToolBox / ADB Tool init. @@ -116,10 +116,12 @@ static const char kShiftedDigits[DIGIT_COUNT] = { ')', '!', '@', '#', '$', '%', // ----- Prototypes ----- +static int8_t analogPaddle(uint8_t v, uint8_t center, uint8_t deadZone); static void buildAsciiTable(void); -static void pollJoystick(void); +static void pollJoystickButtons(void); static uint32_t pollJoystickTight(void); static void pollMouse(void); +static void readJoystickAxes(void); static void readModifierKeys(void); static int8_t signExtend7(uint8_t raw); @@ -129,6 +131,17 @@ static int8_t signExtend7(uint8_t raw); // lookup O(1) instead of a 40-plus-case switch. static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; +// An idle keyboard leaves jlgKeyState all zero, and jlgKeyPrev with it, +// poll after poll, so neither needs touching: gKeysClear says jlgKeyState +// is all zero now (only this file writes it), gKeysClearLast that it was +// at the end of the poll before, and jlgIigsKeysMoved, set at the end of a +// poll, tells jlIigsInputSnapshot (joeyDraw.s) whether the next snapshot +// has a key state to copy: only when both were clear is jlgKeyPrev already +// equal to it. +static bool gKeysClear = true; +static bool gKeysClearLast = true; +uint8_t jlgIigsKeysMoved = 0u; + static int16_t gMouseAbsX = JL_SURFACE_WIDTH / 2; static int16_t gMouseAbsY = JL_SURFACE_HEIGHT / 2; @@ -168,11 +181,16 @@ static void readModifierKeys(void) { // $C025 has one Shift bit for both keys: it is both Shifts, as on the // X68000. - mods = *IIGS_MODIFIERS; - gKeyState[JL_KEY_LSHIFT] = (mods & MOD_SHIFT) != 0; - gKeyState[JL_KEY_RSHIFT] = gKeyState[JL_KEY_LSHIFT]; - gKeyState[JL_KEY_LCTRL] = (mods & MOD_CONTROL) != 0; - gKeyState[JL_KEY_LALT] = (mods & MOD_OPTION) != 0; + mods = *IIGS_MODIFIERS; + if ((mods & (MOD_SHIFT | MOD_CONTROL | MOD_OPTION)) != 0u) { + gKeysClear = false; + } else if (gKeysClear) { + return; // already up, with every other key + } + jlgKeyState[JL_KEY_LSHIFT] = (mods & MOD_SHIFT) != 0; + jlgKeyState[JL_KEY_RSHIFT] = jlgKeyState[JL_KEY_LSHIFT]; + jlgKeyState[JL_KEY_LCTRL] = (mods & MOD_CONTROL) != 0; + jlgKeyState[JL_KEY_LALT] = (mods & MOD_OPTION) != 0; } @@ -190,7 +208,7 @@ static int8_t signExtend7(uint8_t raw) { // Map a raw 0..255 paddle reading to JL_JOYSTICK_AXIS_MIN..MAX: its offset // from the stick's calibrated center (captured at init and by -// jlJoystickReset), clamped, then the shared dead zone (inputDeadZone). +// jlJoystickReset), clamped, then the shared dead zone (jlInputDeadZone). static int8_t analogPaddle(uint8_t v, uint8_t center, uint8_t deadZone) { int16_t delta; @@ -201,7 +219,7 @@ static int8_t analogPaddle(uint8_t v, uint8_t center, uint8_t deadZone) { if (delta > (int16_t)JL_JOYSTICK_AXIS_MAX) { delta = JL_JOYSTICK_AXIS_MAX; } - return inputDeadZone((int8_t)delta, deadZone); + return jlInputDeadZone((int8_t)delta, deadZone); } @@ -240,12 +258,19 @@ static uint8_t gJoyCenterValid [JL_JOYSTICK_COUNT]; static uint8_t gJoyRecalibrate [JL_JOYSTICK_COUNT]; +// Only stick 0 is ever marked stale (pollJoystickButtons). +void jlpJoystickAxesRead(jlJoystickE js) { + jlgJoyAxesStale[js] = 0u; + readJoystickAxes(); +} + + void jlpJoystickReset(jlJoystickE js) { if ((uint16_t)js >= (uint16_t)JL_JOYSTICK_COUNT) { return; } // Re-enable polling and arm a fresh center capture for the next - // poll. The dead-zone value lives in core's gJoyDeadZone[js]. + // paddle read. The dead-zone value lives in core's jlgJoyDeadZone[js]. gJoyConsecutiveTimeouts = 0; gJoyDisconnectLatched = false; gJoyRecalibrate[js] = true; @@ -260,13 +285,13 @@ void jlpJoystickReset(jlJoystickE js) { // resolved = ret & 0xFF (bit0: JoyX valid, bit1: JoyY valid) // px = (ret >> 8) & 0xFF (JoyX 0..255) // py = (ret >> 16) & 0xFF (JoyY 0..255) -extern uint32_t iigsPollJoystickInner(void); +extern uint32_t jlIigsPollJoystickInner(void); // Tight-bound paddle read (pollJoystickTight), used while a client holds -// hal.c's tight interrupt-latency bound (gIigsIrqTight; the serial +// hal.c's tight interrupt-latency bound (jlgIigsIrqTight; the serial // add-on does while a port is open). The classic read masks interrupts // for its whole count, ~2.8 ms at full deflection; the tight read is -// built from passes of iigsJoyPass (joeyDraw.s), none of which masks for +// built from passes of jlIigsJoyPass (joeyDraw.s), none of which masks for // more than JOY_PASS_COUNTS counts (~1.6 ms with its beam stamp): // LOW pass trigger and count at once, on the classic read's beat, so // an axis below the limit reads exactly what the classic @@ -275,7 +300,7 @@ extern uint32_t iigsPollJoystickInner(void); // HIGH pass trigger, let interrupts run for JOY_PASS_HIGH_START // counts' worth of video-beam time, then count to the end, // adding the classic samples the beam says came before -// (gIigsJoyStart), so the value can differ from the classic +// (jlgIigsJoyStart), so the value can differ from the classic // read's by a count. An axis that counts 0 timed out before // the count began: only a low pass can read it. // The last reading picks the first pass: a stick with both axes low or @@ -289,7 +314,7 @@ extern uint32_t iigsPollJoystickInner(void); #define JOY_PASS_COUNTS 144u // one pass's masked count: a multiple of 3, ~1.55 ms #define JOY_PASS_HIGH_START (JOY_COUNT_RANGE - JOY_PASS_COUNTS) // counts a high pass leaves unmasked #define JOY_PASS_ATTEMPTS 4u -// iigsJoyPass flags and mode (joeyDraw.s JOY_PASS_*). +// jlIigsJoyPass flags and mode (joeyDraw.s JOY_PASS_*). #define JOY_PASS_NO_STICK 0x01u #define JOY_PASS_X_OPEN 0x02u #define JOY_PASS_Y_OPEN 0x04u @@ -303,13 +328,29 @@ extern uint32_t iigsPollJoystickInner(void); // The classic read's "no stick" result: resolved 0, both axes 128. #define JOY_RESULT_NO_STICK 0x00808000UL -extern uint32_t iigsJoyPass(uint16_t limitAndMode, uint16_t highStart); -extern uint16_t gIigsJoyStart; +extern uint32_t jlIigsJoyPass(uint16_t limitAndMode, uint16_t highStart); +extern uint16_t jlgIigsJoyStart; // The last tight reading, for the pass order and the hold. static uint8_t gJoyTightLast[JOY_AXES] = { JOY_PADDLE_CENTER, JOY_PADDLE_CENTER }; -static void pollJoystick(void) { +// Buttons are I/O reads -- always cheap, so every poll takes them. The +// paddles are timed a count at a time (~1.4 ms at centre), so they wait +// for jlpJoystickAxesRead: the poll only marks them stale. +static void pollJoystickButtons(void) { + // Indexing through a (uint8_t *) cast collapses each + // jlgJoyButtonState[i][j] write to a literal byte offset. + ((uint8_t *)jlgJoyButtonState)[JL_JOYSTICK_0 * JL_JOY_BUTTON_COUNT + JL_JOY_BUTTON_0] + = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0; + ((uint8_t *)jlgJoyButtonState)[JL_JOYSTICK_0 * JL_JOY_BUTTON_COUNT + JL_JOY_BUTTON_1] + = (*IIGS_BTN1 & IIGS_BUTTON_BIT) != 0; + jlgJoyConnected[JL_JOYSTICK_1] = false; + jlgJoyAxesStale[JL_JOYSTICK_0] = 1u; +} + + +// The paddles: stick 0's axes and whether it is there. +static void readJoystickAxes(void) { uint32_t result; uint8_t px; uint8_t py; @@ -317,32 +358,23 @@ static void pollJoystick(void) { bool xResolved; bool yResolved; - // Buttons are I/O reads -- always cheap, do them every frame. - // Indexing through a (uint8_t *) cast collapses each - // gJoyButtonState[i][j] write to a literal byte offset. - ((uint8_t *)gJoyButtonState)[JL_JOYSTICK_0 * JL_JOY_BUTTON_COUNT + JL_JOY_BUTTON_0] - = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0; - ((uint8_t *)gJoyButtonState)[JL_JOYSTICK_0 * JL_JOY_BUTTON_COUNT + JL_JOY_BUTTON_1] - = (*IIGS_BTN1 & IIGS_BUTTON_BIT) != 0; - gJoyConnected[JL_JOYSTICK_1] = false; - // Once the stick has been latched as disconnected, only buttons // get polled. The app must call jlJoystickReset to resume axis // polling (e.g., when the user has just plugged in a stick). if (gJoyDisconnectLatched) { - gJoyAxisX[JL_JOYSTICK_0] = 0; - gJoyAxisY[JL_JOYSTICK_0] = 0; - gJoyConnected[JL_JOYSTICK_0] = false; + jlgJoyAxisX[JL_JOYSTICK_0] = 0; + jlgJoyAxisY[JL_JOYSTICK_0] = 0; + jlgJoyConnected[JL_JOYSTICK_0] = false; return; } // Asm read at 1 MHz -- result returned packed in registers (A:X). // While the interrupt-latency bound is tight, in passes that each // mask interrupts for ~1.6 ms at most (pollJoystickTight). - if (gIigsIrqTight != 0u) { + if (jlgIigsIrqTight != 0u) { result = pollJoystickTight(); } else { - result = iigsPollJoystickInner(); + result = jlIigsPollJoystickInner(); } resolvedFlags = (uint8_t)(result & 0xFFu); px = (uint8_t)((result >> 8) & 0xFFu); @@ -350,7 +382,7 @@ static void pollJoystick(void) { xResolved = (resolvedFlags & 0x01) != 0; yResolved = (resolvedFlags & 0x02) != 0; - gJoyConnected[JL_JOYSTICK_0] = xResolved || yResolved; + jlgJoyConnected[JL_JOYSTICK_0] = xResolved || yResolved; // Update auto-disconnect counter. Both axes failing => probably no // stick. One resolves => stick is present, reset the counter. @@ -372,8 +404,8 @@ static void pollJoystick(void) { if (timeouts >= JOY_DISCONNECT_THRESHOLD) { gJoyDisconnectLatched = true; } - gJoyAxisX[JL_JOYSTICK_0] = 0; - gJoyAxisY[JL_JOYSTICK_0] = 0; + jlgJoyAxisX[JL_JOYSTICK_0] = 0; + jlgJoyAxisY[JL_JOYSTICK_0] = 0; return; } @@ -389,13 +421,13 @@ static void pollJoystick(void) { // The offset from center, dead-zone clamped (analogPaddle). if (gJoyCenterValid[JL_JOYSTICK_0]) { - gJoyAxisX[JL_JOYSTICK_0] = analogPaddle(px, gJoyCenterX[JL_JOYSTICK_0], gJoyDeadZone[JL_JOYSTICK_0]); - gJoyAxisY[JL_JOYSTICK_0] = analogPaddle(py, gJoyCenterY[JL_JOYSTICK_0], gJoyDeadZone[JL_JOYSTICK_0]); + jlgJoyAxisX[JL_JOYSTICK_0] = analogPaddle(px, gJoyCenterX[JL_JOYSTICK_0], jlgJoyDeadZone[JL_JOYSTICK_0]); + jlgJoyAxisY[JL_JOYSTICK_0] = analogPaddle(py, gJoyCenterY[JL_JOYSTICK_0], jlgJoyDeadZone[JL_JOYSTICK_0]); } } -// The paddles as iigsPollJoystickInner reports them, read in passes that +// The paddles as jlIigsPollJoystickInner reports them, read in passes that // never mask interrupts for more than one pass (see JOY_PASS_COUNTS). static uint32_t pollJoystickTight(void) { uint32_t pass; @@ -423,7 +455,7 @@ static uint32_t pollJoystickTight(void) { high = gJoyTightLast[JOY_AXIS_X] > JOY_PASS_COUNTS || gJoyTightLast[JOY_AXIS_Y] > JOY_PASS_COUNTS; first = false; for (attempt = 0u; attempt < JOY_PASS_ATTEMPTS; attempt++) { - pass = iigsJoyPass(first ? (JOY_PASS_COUNTS | JOY_PASS_FIRST) : JOY_PASS_COUNTS, high ? JOY_PASS_HIGH_START : 0u); + pass = jlIigsJoyPass(first ? (JOY_PASS_COUNTS | JOY_PASS_FIRST) : JOY_PASS_COUNTS, high ? JOY_PASS_HIGH_START : 0u); flags = (uint16_t)(passBytes[2] | ((uint16_t)passBytes[3] << 8)); if ((flags & JOY_PASS_NO_STICK) != 0u) { return JOY_RESULT_NO_STICK; @@ -441,7 +473,7 @@ static uint32_t pollJoystickTight(void) { needHigh[axis] = true; } } else if (count != 0u) { - count = (uint16_t)(count + gIigsJoyStart); + count = (uint16_t)(count + jlgIigsJoyStart); if ((flags & (JOY_PASS_X_OPEN << axis)) != 0u || count > JOY_PADDLE_MAX) { count = JOY_PADDLE_MAX; } @@ -502,7 +534,7 @@ static void pollMouse(void) { } gMouseAbsY = newPos; // Button bit only meaningful on Y reads. 0 = pressed. - gMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (data & MOUSE_BUTTON_INV) == 0; + jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (data & MOUSE_BUTTON_INV) == 0; } else { newPos = (int16_t)(gMouseAbsX + delta); if (newPos < 0) { @@ -515,25 +547,28 @@ static void pollMouse(void) { } } - gMouseX = gMouseAbsX; - gMouseY = gMouseAbsY; + jlgMouseX = gMouseAbsX; + jlgMouseY = gMouseAbsY; // The ADB mouse only reports the single physical button; right // and middle stay false. - gMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = false; - gMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false; + jlgMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = false; + jlgMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false; } // ----- HAL API (alphabetical) ----- void jlpInputInit(void) { - memset(gKeyState, 0, sizeof(gKeyState)); - memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); + memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev)); + gKeysClear = true; + gKeysClearLast = true; + jlgIigsKeysMoved = 0u; buildAsciiTable(); - gMouseAttached = true; // the ADB mouse is part of every IIgs - gMouseAbsX = JL_SURFACE_WIDTH / 2; - gMouseAbsY = JL_SURFACE_HEIGHT / 2; + jlgMouseAttached = true; // the ADB mouse is part of every IIgs + gMouseAbsX = JL_SURFACE_WIDTH / 2; + gMouseAbsY = JL_SURFACE_HEIGHT / 2; // The stick's resting position is captured at the first good read, and // a stick a previous run found missing is looked for again. @@ -556,8 +591,8 @@ void jlpInputPoll(void) { bool strobeObserved; bool ctrlHeld; - // The gKeyState/gKeyPrev/gMouseButtonPrev/gJoyButtonPrev snapshots - // all happen earlier in jlInputPoll's call to iigsInputSnapshot + // The jlgKeyState/jlgKeyPrev/jlgMouseButtonPrev/jlgJoyButtonPrev snapshots + // all happen earlier in jlInputPoll's call to jlIigsInputSnapshot // (asm). We just read the live hardware state here. // Drain the keyboard FIFO, not just the head. The IIgs ADB MCU @@ -584,7 +619,8 @@ void jlpInputPoll(void) { key = gAsciiToKey[ascii]; } if (key != JL_KEY_NONE) { - gKeyState[key] = true; + jlgKeyState[key] = true; + gKeysClear = false; } // Typed-character path: the ADB firmware already applies // shift/caps and the keyboard layout, so the strobe byte IS @@ -594,11 +630,11 @@ void jlpInputPoll(void) { // are not characters -- suppress it so cursor movement never // types a backspace; the other arrows' codes are stopped by // jlInputCharPush's filter anyway. A Ctrl chord types nothing - // (inputCharTyped). + // (jlInputCharTyped). if (ascii == ASCII_DELETE) { - inputCharTyped(JL_CHAR_BACKSPACE, ctrlHeld); + jlInputCharTyped(JL_CHAR_BACKSPACE, ctrlHeld); } else if (ascii != ASCII_LEFT) { - inputCharTyped(ascii, ctrlHeld); + jlInputCharTyped(ascii, ctrlHeld); } (void)*IIGS_KBDSTRB; } @@ -606,7 +642,7 @@ void jlpInputPoll(void) { // $C010 bit 7 is the live "any non-modifier key currently held" // flag (IIe-inherited; updated by the keyboard scanner / ADB MCU // independently of the strobe). When 0 we know all non-modifier - // keys are physically released, so wholesale-clear gKeyState and + // keys are physically released, so wholesale-clear jlgKeyState and // let readModifierKeys re-assert the modifiers from $C025 below. // // strobeObserved guard: a press that arrived AND was released @@ -615,15 +651,16 @@ void jlpInputPoll(void) { // Holding the press for one poll preserves it; the next poll's // bit-7 read will clear normally. kbdStrb = *IIGS_KBDSTRB; - if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0) { - // Runs on nearly every real game frame (idle keyboard), so it is - // libc's 16-bit store fill, not a loop. Hunt-3 rank 5. - memset(gKeyState, 0, sizeof(gKeyState)); + if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0 && !gKeysClear) { + memset(jlgKeyState, 0, sizeof(jlgKeyState)); + gKeysClear = true; } readModifierKeys(); + jlgIigsKeysMoved = (uint8_t)!(gKeysClear && gKeysClearLast); + gKeysClearLast = gKeysClear; pollMouse(); - pollJoystick(); + pollJoystickButtons(); } diff --git a/src/iigs/joeyDraw.s b/src/iigs/joeyDraw.s index 76c34e7..c8314e5 100644 --- a/src/iigs/joeyDraw.s +++ b/src/iigs/joeyDraw.s @@ -31,21 +31,21 @@ .text ; ---------------------------------------------------------------- -; void iigsInitRowLut(void) +; void jlIigsInitRowLut(void) ; -; One-shot init for the y*160 lookup table at gRowOffsetLut. C calls +; One-shot init for the y*160 lookup table at jlgRowOffsetLut. C calls ; this once from jlpInit; afterwards every primitive that needs y -> -; row byte offset can do `lda gRowOffsetLut,x` (X = y*2) in a few cyc +; row byte offset can do `lda jlgRowOffsetLut,x` (X = y*2) in a few cyc ; instead of a shift-add chain. 200 entries x 2 bytes = 400 bytes. ; -; Also builds tpmNibPairIdx (256 bytes) for iigsTilePasteMonoInner: -; tpmNibPairIdx[b] = ((b & 0xF0) ? 2 : 0) | ((b & 0x0F) ? 1 : 0), the +; Also builds jlIigsTpmNibPairIdx (256 bytes) for jlIigsTilePasteMonoInner: +; jlIigsTpmNibPairIdx[b] = ((b & 0xF0) ? 2 : 0) | ((b & 0x0F) ? 1 : 0), the ; 2-bit "which nibbles are opaque" index into the per-call fg/bg combo ; bytes (PERF-AUDIT #3). ; ---------------------------------------------------------------- - .section .text.iigsInitRowLut,"ax" - .globl iigsInitRowLut -iigsInitRowLut: + .section .text.jlIigsInitRowLut,"ax" + .globl jlIigsInitRowLut +jlIigsInitRowLut: php rep #0x30 ; M=16, X=16 .a16 @@ -53,7 +53,7 @@ iigsInitRowLut: ldx #0 ; X = byte offset into LUT lda #0 ; A = current y*160 initLutLoop: - sta gRowOffsetLut, x + sta jlgRowOffsetLut, x clc adc #160 inx @@ -61,7 +61,7 @@ initLutLoop: cpx #400 bcc initLutLoop -; tpmNibPairIdx build: byte stores, so M=8 (X stays 16 for the index). +; jlIigsTpmNibPairIdx build: byte stores, so M=8 (X stays 16 for the index). sep #0x20 .a8 ldx #0 ; X = b @@ -74,13 +74,13 @@ nibLutLoop: nibLutHiZero: lda #0 nibLutHiDone: - sta tpmNibPairIdx, x + sta jlIigsTpmNibPairIdx, x txa and #0x0F beq nibLutNext - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x ora #1 - sta tpmNibPairIdx, x + sta jlIigsTpmNibPairIdx, x nibLutNext: inx cpx #256 @@ -94,7 +94,7 @@ nibLutNext: rtl ; ---------------------------------------------------------------- -; void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord) +; void jlIigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord) ; ; Fast constant fill of the 32000-byte stage at $01:$2000-$9CFF with ; fillWord (both bytes the same). Unrolled run of STA long-absolute @@ -106,9 +106,9 @@ nibLutNext: ; at $01:$2000 (the C wrapper guarantees s == jlStageGet()). fillWord ; (arg1) is at 5,s after PHP. ; ---------------------------------------------------------------- - .section .text.iigsSurfaceClearInner,"ax" - .globl iigsSurfaceClearInner -iigsSurfaceClearInner: + .section .text.jlIigsSurfaceClearInner,"ax" + .globl jlIigsSurfaceClearInner +jlIigsSurfaceClearInner: php rep #0x30 ; M=16, X=16 .a16 @@ -170,7 +170,7 @@ clrExit: rtl ; ---------------------------------------------------------------- -; void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord) +; void jlIigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord) ; ; Post-init fast clear: PHA stack-slam (4 cyc / 2 bytes) vs the STA ; long,X path (6 cyc / 2 bytes). SP is relocated to the top of the $01 @@ -179,7 +179,7 @@ clrExit: ; inhibited by jlpInit. ~1.5x faster. ; ; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI. -; The hold is chunked like the present's: every gIigsClearChunkBlocks +; The hold is chunked like the present's: every jlgIigsClearChunkBlocks ; blocks (hal.c; ~0.12 ms a block) the real stack and bank-0 writes come ; back for a CLI/SEI pair, so a pending interrupt waits at most one chunk. ; The loose bound (200) is the old single ~23 ms hold; the tight one @@ -191,18 +191,18 @@ clrExit: ; AUXWRITE stays off and the slam corrupts bank 0 instead of the $01 ; stage. `pixels` (arg0) in A:X is ignored; fillWord (arg1) at 5,s. ; ---------------------------------------------------------------- - .section .text.iigsSurfaceClearFastInner,"ax" - .globl iigsSurfaceClearFastInner -iigsSurfaceClearFastInner: + .section .text.jlIigsSurfaceClearFastInner,"ax" + .globl jlIigsSurfaceClearFastInner +jlIigsSurfaceClearFastInner: php rep #0x30 .a16 .i16 lda 5, s ; A = fillWord (read before SP/AUXWRITE change) - sta sclrFillCap + sta jlIigsSclrFillCap sei tsc - sta sclrSavedSp ; save original SP + sta jlIigsSclrSavedSp ; save original SP sep #0x20 .a8 sta 0xE0C005 ; AUXWRITE on: $00:0200-BFFF writes -> bank $01 @@ -210,9 +210,9 @@ iigsSurfaceClearFastInner: .a16 lda #0x9cff tcs ; SP = top of $01 stage region - lda sclrFillCap ; A = fillWord = the pushed value + lda jlIigsSclrFillCap ; A = fillWord = the pushed value ldx #200 ; 200 blocks x 80 PHA = 16000 pushes = 32000 bytes - ldy gIigsClearChunkBlocks ; blocks until the next interrupt window + ldy jlgIigsClearChunkBlocks ; blocks until the next interrupt window clrFastBlock: pha pha @@ -306,24 +306,24 @@ clrFastBlock: rep #0x20 .a16 tsc - sta sclrSlamSp ; where the slam resumes - lda sclrSavedSp + sta jlIigsSclrSlamSp ; where the slam resumes + lda jlIigsSclrSavedSp tcs ; the caller's stack for any interrupt cli nop ; a pending IRQ is taken here sei - lda sclrSlamSp + lda jlIigsSclrSlamSp tcs sep #0x20 .a8 sta 0xE0C005 ; AUXWRITE on rep #0x20 .a16 - lda sclrFillCap - ldy gIigsClearChunkBlocks + lda jlIigsSclrFillCap + ldy jlgIigsClearChunkBlocks brl clrFastBlock clrFastDone: - lda sclrSavedSp + lda jlIigsSclrSavedSp tcs ; restore original SP sep #0x20 .a8 @@ -337,42 +337,42 @@ clrFastDone: ; ----- DRAWPRIMS data (uninitialized; bank 0 BSS) ----- - .section .bss.gRowOffsetLut,"aw" - .globl gRowOffsetLut -gRowOffsetLut: + .section .bss.jlgRowOffsetLut,"aw" + .globl jlgRowOffsetLut +jlgRowOffsetLut: .zero 400 - .section .bss.sclrFillCap,"aw" - .globl sclrFillCap -sclrFillCap: + .section .bss.jlIigsSclrFillCap,"aw" + .globl jlIigsSclrFillCap +jlIigsSclrFillCap: .zero 2 - .section .bss.sclrSavedSp,"aw" - .globl sclrSavedSp -sclrSavedSp: + .section .bss.jlIigsSclrSavedSp,"aw" + .globl jlIigsSclrSavedSp +jlIigsSclrSavedSp: .zero 2 - .section .bss.sclrSlamSp,"aw" - .globl sclrSlamSp -sclrSlamSp: + .section .bss.jlIigsSclrSlamSp,"aw" + .globl jlIigsSclrSlamSp +jlIigsSclrSlamSp: .zero 2 ; Flood-fill seed/match scratch (asm-only data; no C definition exists). -; iigsFloodWalkAndScansInner writes the seed match result and the left/ +; jlIigsFloodWalkAndScansInner writes the seed match result and the left/ ; right span extents here for the C wrapper to read back. - .section .bss.gFloodSeedMatch,"aw" - .globl gFloodSeedMatch -gFloodSeedMatch: + .section .bss.jlgFloodSeedMatch,"aw" + .globl jlgFloodSeedMatch +jlgFloodSeedMatch: .zero 2 - .section .bss.gFloodLeftX,"aw" - .globl gFloodLeftX -gFloodLeftX: + .section .bss.jlgFloodLeftX,"aw" + .globl jlgFloodLeftX +jlgFloodLeftX: .zero 2 - .section .bss.gFloodRightX,"aw" - .globl gFloodRightX -gFloodRightX: + .section .bss.jlgFloodRightX,"aw" + .globl jlgFloodRightX +jlgFloodRightX: .zero 2 ; ===== ported: tile primitives ===== @@ -400,18 +400,18 @@ gFloodRightX: ; directly (that was the historical bug: [0],y/[4],y then read bank 0 ; while the stores wrote the load bank). The stack is bank 0, so the ; copied frame matches. -; tileScratchPtrs+0..2 = dst (low16 + bank) (staged, then -> [0],y) -; tileScratchPtrs+4..6 = src (low16 + bank) (staged, then -> [4],y) +; jlIigsTileScratchPtrs+0..2 = dst (low16 + bank) (staged, then -> [0],y) +; jlIigsTileScratchPtrs+4..6 = src (low16 + bank) (staged, then -> [4],y) ; (bytes +3 / +7 are unused padding, mirroring the 4-byte ptr layout) ; -------------------------------------------------------------------- -.section .bss.tileScratchPtrs,"aw" -.globl tileScratchPtrs -tileScratchPtrs: +.section .bss.jlIigsTileScratchPtrs,"aw" +.globl jlIigsTileScratchPtrs +jlIigsTileScratchPtrs: .zero 8 ; ==================================================================== -; iigsTileFillInner(uint8_t *dstRow0, uint16_t nibble) +; jlIigsTileFillInner(uint8_t *dstRow0, uint16_t nibble) ; ; Original: 16 STA abs,X stores at fixed offsets along a 160-byte ; stride (DBR set to the dst bank via PHB/PLB so abs,X rode the right @@ -434,12 +434,12 @@ tileScratchPtrs: ; keep the identical 16 store offsets and the identical fill word but ; reach the destination via DP-indirect-long `[dst],y` with Y = the ; offset -- the same mechanism the two-pointer tile routines use. We -; stash dst (A:X) in tileScratchPtrs and point D there ([0] = dst). -; D-frame: tileScratchPtrs+0..2 = dst (low16 + bank) -> [0],y +; stash dst (A:X) in jlIigsTileScratchPtrs and point D there ([0] = dst). +; D-frame: jlIigsTileScratchPtrs+0..2 = dst (low16 + bank) -> [0],y ; ==================================================================== -.section .text.iigsTileFillInner,"ax" -.globl iigsTileFillInner -iigsTileFillInner: +.section .text.jlIigsTileFillInner,"ax" +.globl jlIigsTileFillInner +jlIigsTileFillInner: php phb phd ; save caller D @@ -448,8 +448,8 @@ iigsTileFillInner: .i16 ; Build the dst D-frame on the STACK (bank 0). A BSS scratch - ; (tileScratchPtrs) lives in the LOAD bank, but D (the direct - ; page) is ALWAYS bank 0, so the old `tcd #tileScratchPtrs` + ; (jlIigsTileScratchPtrs) lives in the LOAD bank, but D (the direct + ; page) is ALWAYS bank 0, so the old `tcd #jlIigsTileScratchPtrs` ; pointed [0],y at bank-0:offset while the stores wrote ; loadbank:offset -- the indirect read the wrong bank entirely. ; The hardware stack is bank 0, so a stack D-frame matches. @@ -460,17 +460,17 @@ iigsTileFillInner: inc a ; A = SP+1 tcd ; D -> stacked dst; [0] = dst -; Derive fillWord = nibble * 0x1111 in asm (#41). tileScratchPtrs+4 is +; Derive fillWord = nibble * 0x1111 in asm (#41). jlIigsTileScratchPtrs+4 is ; free scratch here: tileFill stages no src pointer. - lda 12,s ; nibble (8,s + 4 just pushed) + lda 12,s ; nibble (8,s + 4 just pushed) asl a asl a asl a - asl a ; A = nibble << 4 - ora 12,s ; A = nibble * 0x11 (doubled byte) - sta tileScratchPtrs+4 - xba ; A = doubled byte << 8 - ora tileScratchPtrs+4 ; A = nibble * 0x1111 = fillWord + asl a ; A = nibble << 4 + ora 12,s ; A = nibble * 0x11 (doubled byte) + sta jlIigsTileScratchPtrs+4 + xba ; A = doubled byte << 8 + ora jlIigsTileScratchPtrs+4 ; A = nibble * 0x1111 = fillWord ldy #0 sta [0], y @@ -521,7 +521,7 @@ iigsTileFillInner: ; ==================================================================== -; iigsTileCopyInner(uint8_t *dst, const uint8_t *src) +; jlIigsTileCopyInner(uint8_t *dst, const uint8_t *src) ; ; Opaque copy of an 8x8 tile region (32 bytes) between two 4bpp ; surfaces with 160-byte row stride. Dst and src may be in any banks. @@ -529,14 +529,14 @@ iigsTileFillInner: ; ; ABI: arg0 dst in A:X (A=low16, X=high16). arg1 src (4-byte ptr) on ; the stack: @4,s on JSL entry; after PHP+PHB+PHD(+4) src low16 -; @8,s, src bank @10,s. We point D at tileScratchPtrs, store dst +; @8,s, src bank @10,s. We point D at jlIigsTileScratchPtrs, store dst ; (A:X) at +0..2 and src (from the stack) at +4..6, then run the ; original [0],y (dst) / [4],y (src) loads unchanged. PLD restores ; the caller's D (saved by our PHD). ; ==================================================================== -.section .text.iigsTileCopyInner,"ax" -.globl iigsTileCopyInner -iigsTileCopyInner: +.section .text.jlIigsTileCopyInner,"ax" +.globl jlIigsTileCopyInner +jlIigsTileCopyInner: php phb phd ; save caller D @@ -545,27 +545,27 @@ iigsTileCopyInner: .i16 ; Build the dst/src D-frame in bank-0 scratch. - sta tileScratchPtrs+0 ; dst low16 - stx tileScratchPtrs+2 ; dst high16 (bank + pad) - lda 8,s ; src low16 (post +4 push) - sta tileScratchPtrs+4 - lda 10,s ; src bank - sta tileScratchPtrs+6 + sta jlIigsTileScratchPtrs+0 ; dst low16 + stx jlIigsTileScratchPtrs+2 ; dst high16 (bank + pad) + lda 8,s ; src low16 (post +4 push) + sta jlIigsTileScratchPtrs+4 + lda 10,s ; src bank + sta jlIigsTileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the - ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the + ; jlIigsTileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via - ; `tcd #tileScratchPtrs` indirected through the wrong bank. - lda tileScratchPtrs+6 - pha ; src bank -> 7,s - lda tileScratchPtrs+4 - pha ; src low16 -> 5,s - lda tileScratchPtrs+2 - pha ; dst high16 -> 3,s - lda tileScratchPtrs+0 - pha ; dst low16 -> 1,s + ; `tcd #jlIigsTileScratchPtrs` indirected through the wrong bank. + lda jlIigsTileScratchPtrs+6 + pha ; src bank -> 7,s + lda jlIigsTileScratchPtrs+4 + pha ; src low16 -> 5,s + lda jlIigsTileScratchPtrs+2 + pha ; dst high16 -> 3,s + lda jlIigsTileScratchPtrs+0 + pha ; dst low16 -> 1,s tsc - inc a ; A = SP+1 - tcd ; D -> stacked {dst,src}; [0]=dst [4]=src + inc a ; A = SP+1 + tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ldy #0 lda [4], y @@ -632,19 +632,19 @@ iigsTileCopyInner: ; ==================================================================== -; iigsTilePasteInner(uint8_t *dst, const uint8_t *src) +; jlIigsTilePasteInner(uint8_t *dst, const uint8_t *src) ; ; Paste a packed jlTileT buffer (4 bytes/row * 8 rows = 32 bytes tight) ; onto a surface row at `dst`. Src stride 4, dst stride 160. Both args ; are 4-byte large-model pointers. ; -; ABI: identical to iigsTileCopyInner -- arg0 dst in A:X, arg1 src on +; ABI: identical to jlIigsTileCopyInner -- arg0 dst in A:X, arg1 src on ; the stack (low16 @8,s / bank @10,s after PHP+PHB+PHD). D points -; at tileScratchPtrs; [0],y = dst, [4],y = src. +; at jlIigsTileScratchPtrs; [0],y = dst, [4],y = src. ; ==================================================================== -.section .text.iigsTilePasteInner,"ax" -.globl iigsTilePasteInner -iigsTilePasteInner: +.section .text.jlIigsTilePasteInner,"ax" +.globl jlIigsTilePasteInner +jlIigsTilePasteInner: php phb phd ; save caller D @@ -652,27 +652,27 @@ iigsTilePasteInner: .a16 .i16 - sta tileScratchPtrs+0 ; dst low16 - stx tileScratchPtrs+2 ; dst high16 (bank + pad) - lda 8,s ; src low16 - sta tileScratchPtrs+4 - lda 10,s ; src bank - sta tileScratchPtrs+6 + sta jlIigsTileScratchPtrs+0 ; dst low16 + stx jlIigsTileScratchPtrs+2 ; dst high16 (bank + pad) + lda 8,s ; src low16 + sta jlIigsTileScratchPtrs+4 + lda 10,s ; src bank + sta jlIigsTileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the - ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the + ; jlIigsTileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via - ; `tcd #tileScratchPtrs` indirected through the wrong bank. - lda tileScratchPtrs+6 - pha ; src bank -> 7,s - lda tileScratchPtrs+4 - pha ; src low16 -> 5,s - lda tileScratchPtrs+2 - pha ; dst high16 -> 3,s - lda tileScratchPtrs+0 - pha ; dst low16 -> 1,s + ; `tcd #jlIigsTileScratchPtrs` indirected through the wrong bank. + lda jlIigsTileScratchPtrs+6 + pha ; src bank -> 7,s + lda jlIigsTileScratchPtrs+4 + pha ; src low16 -> 5,s + lda jlIigsTileScratchPtrs+2 + pha ; dst high16 -> 3,s + lda jlIigsTileScratchPtrs+0 + pha ; dst low16 -> 1,s tsc - inc a ; A = SP+1 - tcd ; D -> stacked {dst,src}; [0]=dst [4]=src + inc a ; A = SP+1 + tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ; Row 0: src word 0 = 0, dst word 0 = 0 ldy #0 @@ -761,7 +761,7 @@ iigsTilePasteInner: ; ==================================================================== -; iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0) +; jlIigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0) ; ; Snapshot an 8x8 region of a surface into a packed jlTileT buffer. ; Src stride 160, dst stride 4. Mirrors jlTilePaste with stride values @@ -769,11 +769,11 @@ iigsTilePasteInner: ; ; ABI: arg0 dst (the packed tile buffer) in A:X, arg1 src (the surface ; row) on the stack (low16 @8,s / bank @10,s after PHP+PHB+PHD). -; D points at tileScratchPtrs; [0],y = dst, [4],y = src. +; D points at jlIigsTileScratchPtrs; [0],y = dst, [4],y = src. ; ==================================================================== -.section .text.iigsTileSnapInner,"ax" -.globl iigsTileSnapInner -iigsTileSnapInner: +.section .text.jlIigsTileSnapInner,"ax" +.globl jlIigsTileSnapInner +jlIigsTileSnapInner: php phb phd ; save caller D @@ -781,27 +781,27 @@ iigsTileSnapInner: .a16 .i16 - sta tileScratchPtrs+0 ; dst low16 - stx tileScratchPtrs+2 ; dst high16 (bank + pad) - lda 8,s ; src low16 - sta tileScratchPtrs+4 - lda 10,s ; src bank - sta tileScratchPtrs+6 + sta jlIigsTileScratchPtrs+0 ; dst low16 + stx jlIigsTileScratchPtrs+2 ; dst high16 (bank + pad) + lda 8,s ; src low16 + sta jlIigsTileScratchPtrs+4 + lda 10,s ; src bank + sta jlIigsTileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the - ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the + ; jlIigsTileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via - ; `tcd #tileScratchPtrs` indirected through the wrong bank. - lda tileScratchPtrs+6 - pha ; src bank -> 7,s - lda tileScratchPtrs+4 - pha ; src low16 -> 5,s - lda tileScratchPtrs+2 - pha ; dst high16 -> 3,s - lda tileScratchPtrs+0 - pha ; dst low16 -> 1,s + ; `tcd #jlIigsTileScratchPtrs` indirected through the wrong bank. + lda jlIigsTileScratchPtrs+6 + pha ; src bank -> 7,s + lda jlIigsTileScratchPtrs+4 + pha ; src low16 -> 5,s + lda jlIigsTileScratchPtrs+2 + pha ; dst high16 -> 3,s + lda jlIigsTileScratchPtrs+0 + pha ; dst low16 -> 1,s tsc - inc a ; A = SP+1 - tcd ; D -> stacked {dst,src}; [0]=dst [4]=src + inc a ; A = SP+1 + tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ; Row 0: src 0/2, dst 0/2 ldy #0 @@ -890,7 +890,7 @@ iigsTileSnapInner: ; ==================================================================== -; iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, +; jlIigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, ; uint16_t fgColor, uint16_t bgColor) ; ; Fused mono colorize + paste (PERF-AUDIT #3): expands a 32-byte mono @@ -905,7 +905,7 @@ iigsTileSnapInner: ; ; Design: each source byte maps to one of only FOUR output bytes ; (bg/bg, bg/fg, fg/bg, fg/fg). A static 256-byte table -; (tpmNibPairIdx, built once in iigsInitRowLut) collapses the byte to +; (jlIigsTpmNibPairIdx, built once in jlIigsInitRowLut) collapses the byte to ; its 2-bit opacity-pair index; the four combo bytes are built per ; call (4 bytes, ~30 cyc) into the bank-0 stack D-frame. The body is ; then branch-free: load src byte -> index the static table -> index @@ -919,7 +919,7 @@ iigsTileSnapInner: ; @4,s, mono.bank @6,s, fg @8,s, bg @10,s. No PHB (DBR is never ; written), so after PHP+PHD(+3): mono.lo @7,s, mono.bank @9,s, ; fg @11,s, bg @13,s; after the comboHi push (+2): fg @13,s, -; bg @15,s. Same bank-0 stack D-frame as iigsTilePasteInner, two +; bg @15,s. Same bank-0 stack D-frame as jlIigsTilePasteInner, two ; words wider: ; D+0..3 dst far ptr -> [tpmDst],y ; D+4..7 src far ptr -> [tpmSrc],y @@ -928,9 +928,9 @@ iigsTileSnapInner: ; nibble; bit0 = lo nibble opaque -> fg in low output nibble: ; D+8 = bg:bg D+9 = bg:fg D+10 = fg:bg D+11 = fg:fg ; ==================================================================== -.section .text.iigsTilePasteMonoInner,"ax" -.globl iigsTilePasteMonoInner -iigsTilePasteMonoInner: +.section .text.jlIigsTilePasteMonoInner,"ax" +.globl jlIigsTilePasteMonoInner +jlIigsTilePasteMonoInner: tpmDst = 0 tpmSrc = 4 tpmCombo = 8 @@ -942,15 +942,15 @@ tpmCombo = 8 .i16 ; Stage dst (A:X) and src (stack) exactly like - ; iigsTilePasteInner: tileScratchPtrs is LOAD-bank BSS, so it + ; jlIigsTilePasteInner: jlIigsTileScratchPtrs is LOAD-bank BSS, so it ; is only a staging area -- the D-frame the indirects read ; through is rebuilt on the bank-0 stack below. - sta tileScratchPtrs+0 ; dst low16 - stx tileScratchPtrs+2 ; dst high16 (bank + pad) - lda 7,s ; src low16 - sta tileScratchPtrs+4 - lda 9,s ; src bank - sta tileScratchPtrs+6 + sta jlIigsTileScratchPtrs+0 ; dst low16 + stx jlIigsTileScratchPtrs+2 ; dst high16 (bank + pad) + lda 7,s ; src low16 + sta jlIigsTileScratchPtrs+4 + lda 9,s ; src bank + sta jlIigsTileScratchPtrs+6 ; Build the four combo bytes. fg/bg are pre-masked words ; (high byte 0), so every intermediate below has a clean @@ -960,36 +960,36 @@ tpmCombo = 8 asl a asl a asl a - sta tpmFgHi ; fg << 4 + sta jlIigsTpmFgHi ; fg << 4 lda 13,s ; bg asl a asl a asl a asl a - sta tpmBgHi ; bg << 4 + sta jlIigsTpmBgHi ; bg << 4 ; comboHi word: high byte fg:fg (idx 3), low byte fg:bg (idx 2) - lda tpmFgHi + lda jlIigsTpmFgHi ora 11,s ; A = fg:fg xba ; A = fg:fg << 8 - ora tpmFgHi + ora jlIigsTpmFgHi ora 13,s ; low byte = fg:bg pha ; -> D+10..11 (args now +2: fg @13,s bg @15,s) ; comboLo word: high byte bg:fg (idx 1), low byte bg:bg (idx 0) - lda tpmBgHi + lda jlIigsTpmBgHi ora 13,s ; A = bg:fg xba ; A = bg:fg << 8 - ora tpmBgHi + ora jlIigsTpmBgHi ora 15,s ; low byte = bg:bg pha ; -> D+8..9 ; Stack D-frame: {dst, src} under the combo words. - lda tileScratchPtrs+6 + lda jlIigsTileScratchPtrs+6 pha ; src bank -> D+6 - lda tileScratchPtrs+4 + lda jlIigsTileScratchPtrs+4 pha ; src low16 -> D+4 - lda tileScratchPtrs+2 + lda jlIigsTileScratchPtrs+2 pha ; dst high16 -> D+2 - lda tileScratchPtrs+0 + lda jlIigsTileScratchPtrs+0 pha ; dst low16 -> D+0 tsc inc a ; A = SP+1 @@ -1006,28 +1006,28 @@ tpmCombo = 8 ldy #0 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x sta [tpmDst], y ldy #1 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x sta [tpmDst], y ldy #2 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x sta [tpmDst], y ldy #3 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x sta [tpmDst], y @@ -1035,7 +1035,7 @@ tpmCombo = 8 ldy #4 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #160 @@ -1043,7 +1043,7 @@ tpmCombo = 8 ldy #5 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #161 @@ -1051,7 +1051,7 @@ tpmCombo = 8 ldy #6 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #162 @@ -1059,7 +1059,7 @@ tpmCombo = 8 ldy #7 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #163 @@ -1068,7 +1068,7 @@ tpmCombo = 8 ldy #8 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #320 @@ -1076,7 +1076,7 @@ tpmCombo = 8 ldy #9 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #321 @@ -1084,7 +1084,7 @@ tpmCombo = 8 ldy #10 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #322 @@ -1092,7 +1092,7 @@ tpmCombo = 8 ldy #11 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #323 @@ -1101,7 +1101,7 @@ tpmCombo = 8 ldy #12 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #480 @@ -1109,7 +1109,7 @@ tpmCombo = 8 ldy #13 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #481 @@ -1117,7 +1117,7 @@ tpmCombo = 8 ldy #14 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #482 @@ -1125,7 +1125,7 @@ tpmCombo = 8 ldy #15 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #483 @@ -1134,7 +1134,7 @@ tpmCombo = 8 ldy #16 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #640 @@ -1142,7 +1142,7 @@ tpmCombo = 8 ldy #17 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #641 @@ -1150,7 +1150,7 @@ tpmCombo = 8 ldy #18 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #642 @@ -1158,7 +1158,7 @@ tpmCombo = 8 ldy #19 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #643 @@ -1167,7 +1167,7 @@ tpmCombo = 8 ldy #20 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #800 @@ -1175,7 +1175,7 @@ tpmCombo = 8 ldy #21 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #801 @@ -1183,7 +1183,7 @@ tpmCombo = 8 ldy #22 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #802 @@ -1191,7 +1191,7 @@ tpmCombo = 8 ldy #23 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #803 @@ -1200,7 +1200,7 @@ tpmCombo = 8 ldy #24 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #960 @@ -1208,7 +1208,7 @@ tpmCombo = 8 ldy #25 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #961 @@ -1216,7 +1216,7 @@ tpmCombo = 8 ldy #26 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #962 @@ -1224,7 +1224,7 @@ tpmCombo = 8 ldy #27 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #963 @@ -1233,7 +1233,7 @@ tpmCombo = 8 ldy #28 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #1120 @@ -1241,7 +1241,7 @@ tpmCombo = 8 ldy #29 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #1121 @@ -1249,7 +1249,7 @@ tpmCombo = 8 ldy #30 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #1122 @@ -1257,7 +1257,7 @@ tpmCombo = 8 ldy #31 lda [tpmSrc], y tax - lda tpmNibPairIdx, x + lda jlIigsTpmNibPairIdx, x tax lda tpmCombo, x ldy #1123 @@ -1278,92 +1278,125 @@ tpmCombo = 8 ; ==================================================================== -; iigsTilePasteGlyphMark(const uint8_t *bits, uint16_t bx, uint16_t by, -; uint16_t fg, uint16_t bg) +; void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, +; const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) ; -; Paste an 8x8 ONE-BIT-PER-PIXEL glyph into the stage and fuse the -; dirty mark, in one JSL. This is the shape a character generator -; already has, so unlike iigsTilePasteMonoInner nothing has to expand -; the glyph into a 32-byte chunky silhouette first -- the caller hands -; over the eight glyph rows as they stand. +; The public call itself on the IIgs (JL_HAS_TILE_PASTE_GLYPH_ENTRY; +; tile.c leaves its C body out): an 8x8 ONE-BIT-PER-PIXEL glyph pasted +; into the stage with the dirty mark fused, in one JSL. The C wrapper +; that used to stand in front of the paste spent ~250 cycles a glyph, a +; third of the paste, on its checks, its colour masks and a second JSL +; with every argument pushed again. A glyph off the surface, or with no +; bits, is nothing, as in C; any surface but the stage goes on, checked, +; to jlpGenericTilePasteGlyph with the same arguments. +; +; This is the shape a character generator already has, so unlike +; jlIigsTilePasteMonoInner nothing has to expand the glyph into a +; 32-byte chunky silhouette first -- the caller hands over the eight +; glyph rows as they stand. ; ; Design: a glyph NIBBLE is four pixels = two output bytes = one word, ; so a per-call 16-entry word table turns each row into two indexed ; loads and two word stores. That halves the stores against the mono -; path's byte-at-a-time expansion AND -- because the glyph bytes are -; spread into words up front -- the whole body stays in M=16, with no -; sep/rep churn per pixel pair (the mode flips alone cost ~96 cyc a -; tile). Measured whole-call cost against the C generic it replaces: -; roughly 930 cycles versus ~2300. +; path's byte-at-a-time expansion AND -- because each 16-bit read of the +; glyph takes two rows, the second reached with xba, and a mask keeps +; only the nibble wanted -- the whole body stays in M=16, with no sep/rep +; churn per pixel pair (the mode flips alone cost ~96 cyc a tile). ; ; The table: pb[m] is the output byte for a 2-bit pixel pair (high bit -; = left pixel), so tpgWord[n] = pb[n & 3] << 8 | pb[n >> 2] -- the low +; = left pixel), so jlIigsTpgWord[n] = pb[n & 3] << 8 | pb[n >> 2] -- the low ; byte of the word lands at dst+0 (pixels 0,1) and the high byte at ; dst+1 (pixels 2,3), which is what little-endian word stores give. ; -; fg/bg arrive PRE-MASKED to 4 bits (the public jlTilePasteGlyph -; wrapper owns the & 0x0F), same contract as tilePasteMono. -; -; ABI: arg0 bits in A:X; bx @4,s by @6,s fg @8,s bg @10,s on JSL entry -; -> @8/@10/@12/@14 after php+phb+phd -> @12/@14/@16/@18 after the -; 4-byte source D-frame. Dst is $012000+off,x like -; iigsTilePasteMark. DBR is the caller's data bank throughout (no -; plb), so the absolute scratch below and gRowOffsetLut both read -; with plain abs addressing. +; ABI: dst in A:X; bx @4,s by @6,s bits @8,s fg @12,s bg @14,s on JSL +; entry, each byte argument the low byte of its word -> +1 after +; php, +4 after php+phb+phd. The checks write the byte arguments +; back masked (bx, by to 8 bits, fg, bg to 4: what the body and the +; generic take). On the stage D points at the bits argument itself, +; so [0] is the glyph. Dst is $012000+off,x like +; jlIigsTilePasteMark. DBR is the caller's data bank throughout (no +; plb), so jlgStage, the absolute scratch below and jlgRowOffsetLut +; all read with plain abs addressing. ; ==================================================================== ; Emitted into .text.iigsFusedTile, NOT a section of its own: the fused -; mark below is a 16-bit `jsr stageTileMark`, and a jsr between two +; mark below is a 16-bit `jsr jlStageTileMark`, and a jsr between two ; .section blocks silently jumps into another bank. +.equ TPG_BLOCKS_PER_ROW, 40 ; JL_TILE_BLOCKS_PER_ROW (tile.c checks it) +.equ TPG_BLOCKS_PER_COL, 25 ; JL_TILE_BLOCKS_PER_COL .section .text.iigsFusedTile,"ax" -.globl iigsTilePasteGlyphMark -iigsTilePasteGlyphMark: +.globl jlTilePasteGlyph +jlTilePasteGlyph: php - phb - phd rep #0x30 .a16 .i16 - phx ; bits high16 -> 3,s - pha ; bits low16 -> 1,s + tay ; dst low16 + lda 9,s ; no bits: nothing + ora 11,s + beq tpgNone + lda 5,s ; bx + and #0x00FF + cmp #TPG_BLOCKS_PER_ROW + bcs tpgNone + sta 5,s + lda 7,s ; by + and #0x00FF + cmp #TPG_BLOCKS_PER_COL + bcs tpgNone + sta 7,s + lda 13,s ; fg + and #0x000F + sta 13,s + lda 15,s ; bg + and #0x000F + sta 15,s + cpy jlgStage + bne tpgOther + cpx jlgStage+2 + bne tpgOther + tya ; the stage, unless there is none + bne tpgStage + txa + bne tpgStage +tpgNone: + plp + rtl +tpgOther: + tya + bne tpgGeneric + txa + beq tpgNone +tpgGeneric: + plp + ; The caller's six argument words sit at 4,s .. 15,s again; + ; push them in the same order for the generic. + lda 14,s + pha + lda 14,s + pha + lda 14,s + pha + lda 14,s + pha + lda 14,s + pha + lda 14,s + pha + tya + jsl jlpGenericTilePasteGlyph tsc - inc a + clc + adc #12 + tcs + rtl +tpgStage: + phb + phd + tsc + clc + adc #12 tcd ; [0] = bits - ; Spread the eight glyph bytes into words so the body - ; can index the table without ever leaving M=16. - ldy #0 - lda [0], y - and #0x00FF - sta tpgBitsW+0 - lda [0], y - xba - and #0x00FF - sta tpgBitsW+2 - ldy #2 - lda [0], y - and #0x00FF - sta tpgBitsW+4 - lda [0], y - xba - and #0x00FF - sta tpgBitsW+6 - ldy #4 - lda [0], y - and #0x00FF - sta tpgBitsW+8 - lda [0], y - xba - and #0x00FF - sta tpgBitsW+10 - ldy #6 - lda [0], y - and #0x00FF - sta tpgBitsW+12 - lda [0], y - xba - and #0x00FF - sta tpgBitsW+14 - ; The 16-word colour table depends only on (fg, bg), and consecutive ; cells almost always share them (a level is painted in colour runs), ; so it is kept across calls: rebuild only when the pair changes. The @@ -1375,11 +1408,11 @@ iigsTilePasteGlyphMark: asl a asl a ora 18,s ; | bg - cmp tpgLastKey + cmp jlIigsTpgLastKey bne tpgTableBuild brl tpgTableReady ; the build is past an 8-bit branch's reach tpgTableBuild: - sta tpgLastKey + sta jlIigsTpgLastKey ; pb[0..3]: the output byte for each 2-bit pixel pair, ; and the same four values pre-shifted into a high byte. @@ -1388,295 +1421,302 @@ tpgTableBuild: asl a asl a asl a - sta tpgFgHi ; fg << 4 + sta jlIigsTpgFgHi ; fg << 4 lda 18,s ; bg asl a asl a asl a asl a - sta tpgBgHi ; bg << 4 - lda tpgBgHi + sta jlIigsTpgBgHi ; bg << 4 + lda jlIigsTpgBgHi ora 18,s - sta tpgPb+0 ; bg:bg + sta jlIigsTpgPb+0 ; bg:bg xba - sta tpgPbHi+0 - lda tpgBgHi + sta jlIigsTpgPbHi+0 + lda jlIigsTpgBgHi ora 16,s - sta tpgPb+2 ; bg:fg + sta jlIigsTpgPb+2 ; bg:fg xba - sta tpgPbHi+2 - lda tpgFgHi + sta jlIigsTpgPbHi+2 + lda jlIigsTpgFgHi ora 18,s - sta tpgPb+4 ; fg:bg + sta jlIigsTpgPb+4 ; fg:bg xba - sta tpgPbHi+4 - lda tpgFgHi + sta jlIigsTpgPbHi+4 + lda jlIigsTpgFgHi ora 16,s - sta tpgPb+6 ; fg:fg + sta jlIigsTpgPb+6 ; fg:fg xba - sta tpgPbHi+6 + sta jlIigsTpgPbHi+6 - ; tpgWord[hi*4 + lo] = pb[lo] << 8 | pb[hi] - lda tpgPbHi+0 - ora tpgPb+0 - sta tpgWord+0 - lda tpgPbHi+2 - ora tpgPb+0 - sta tpgWord+2 - lda tpgPbHi+4 - ora tpgPb+0 - sta tpgWord+4 - lda tpgPbHi+6 - ora tpgPb+0 - sta tpgWord+6 - lda tpgPbHi+0 - ora tpgPb+2 - sta tpgWord+8 - lda tpgPbHi+2 - ora tpgPb+2 - sta tpgWord+10 - lda tpgPbHi+4 - ora tpgPb+2 - sta tpgWord+12 - lda tpgPbHi+6 - ora tpgPb+2 - sta tpgWord+14 - lda tpgPbHi+0 - ora tpgPb+4 - sta tpgWord+16 - lda tpgPbHi+2 - ora tpgPb+4 - sta tpgWord+18 - lda tpgPbHi+4 - ora tpgPb+4 - sta tpgWord+20 - lda tpgPbHi+6 - ora tpgPb+4 - sta tpgWord+22 - lda tpgPbHi+0 - ora tpgPb+6 - sta tpgWord+24 - lda tpgPbHi+2 - ora tpgPb+6 - sta tpgWord+26 - lda tpgPbHi+4 - ora tpgPb+6 - sta tpgWord+28 - lda tpgPbHi+6 - ora tpgPb+6 - sta tpgWord+30 + ; jlIigsTpgWord[hi*4 + lo] = pb[lo] << 8 | pb[hi] + lda jlIigsTpgPbHi+0 + ora jlIigsTpgPb+0 + sta jlIigsTpgWord+0 + lda jlIigsTpgPbHi+2 + ora jlIigsTpgPb+0 + sta jlIigsTpgWord+2 + lda jlIigsTpgPbHi+4 + ora jlIigsTpgPb+0 + sta jlIigsTpgWord+4 + lda jlIigsTpgPbHi+6 + ora jlIigsTpgPb+0 + sta jlIigsTpgWord+6 + lda jlIigsTpgPbHi+0 + ora jlIigsTpgPb+2 + sta jlIigsTpgWord+8 + lda jlIigsTpgPbHi+2 + ora jlIigsTpgPb+2 + sta jlIigsTpgWord+10 + lda jlIigsTpgPbHi+4 + ora jlIigsTpgPb+2 + sta jlIigsTpgWord+12 + lda jlIigsTpgPbHi+6 + ora jlIigsTpgPb+2 + sta jlIigsTpgWord+14 + lda jlIigsTpgPbHi+0 + ora jlIigsTpgPb+4 + sta jlIigsTpgWord+16 + lda jlIigsTpgPbHi+2 + ora jlIigsTpgPb+4 + sta jlIigsTpgWord+18 + lda jlIigsTpgPbHi+4 + ora jlIigsTpgPb+4 + sta jlIigsTpgWord+20 + lda jlIigsTpgPbHi+6 + ora jlIigsTpgPb+4 + sta jlIigsTpgWord+22 + lda jlIigsTpgPbHi+0 + ora jlIigsTpgPb+6 + sta jlIigsTpgWord+24 + lda jlIigsTpgPbHi+2 + ora jlIigsTpgPb+6 + sta jlIigsTpgWord+26 + lda jlIigsTpgPbHi+4 + ora jlIigsTpgPb+6 + sta jlIigsTpgWord+28 + lda jlIigsTpgPbHi+6 + ora jlIigsTpgPb+6 + sta jlIigsTpgWord+30 tpgTableReady: ; X = stage byte offset of the tile's row 0 - lda 14,s ; by + lda 10,s ; by asl a asl a asl a asl a tax - lda 12,s ; bx + lda 8,s ; bx asl a asl a clc - adc gRowOffsetLut,x + adc jlgRowOffsetLut,x tax - ; row 0 - lda tpgBitsW+0 + ; rows 0 and 1: one read, row 0 in the low byte + ldy #0 + lda [0], y + sta jlIigsTpgBitsW + and #0x00F0 lsr a lsr a - lsr a - and #0x001E ; (b >> 4) * 2 + lsr a ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+0,x - lda tpgBitsW+0 + lda jlIigsTpgBitsW and #0x000F asl a ; (b & 15) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+2,x - ; row 1 - lda tpgBitsW+2 + lda jlIigsTpgBitsW + xba + and #0x00F0 lsr a lsr a lsr a - and #0x001E ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+160,x - lda tpgBitsW+2 + lda jlIigsTpgBitsW + xba and #0x000F - asl a ; (b & 15) * 2 + asl a tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+162,x - ; row 2 - lda tpgBitsW+4 + ; rows 2 and 3: one read, row 2 in the low byte + ldy #2 + lda [0], y + sta jlIigsTpgBitsW + and #0x00F0 lsr a lsr a - lsr a - and #0x001E ; (b >> 4) * 2 + lsr a ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+320,x - lda tpgBitsW+4 + lda jlIigsTpgBitsW and #0x000F asl a ; (b & 15) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+322,x - ; row 3 - lda tpgBitsW+6 + lda jlIigsTpgBitsW + xba + and #0x00F0 lsr a lsr a lsr a - and #0x001E ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+480,x - lda tpgBitsW+6 + lda jlIigsTpgBitsW + xba and #0x000F - asl a ; (b & 15) * 2 + asl a tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+482,x - ; row 4 - lda tpgBitsW+8 + ; rows 4 and 5: one read, row 4 in the low byte + ldy #4 + lda [0], y + sta jlIigsTpgBitsW + and #0x00F0 lsr a lsr a - lsr a - and #0x001E ; (b >> 4) * 2 + lsr a ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+640,x - lda tpgBitsW+8 + lda jlIigsTpgBitsW and #0x000F asl a ; (b & 15) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+642,x - ; row 5 - lda tpgBitsW+10 + lda jlIigsTpgBitsW + xba + and #0x00F0 lsr a lsr a lsr a - and #0x001E ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+800,x - lda tpgBitsW+10 + lda jlIigsTpgBitsW + xba and #0x000F - asl a ; (b & 15) * 2 + asl a tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+802,x - ; row 6 - lda tpgBitsW+12 + ; rows 6 and 7: one read, row 6 in the low byte + ldy #6 + lda [0], y + sta jlIigsTpgBitsW + and #0x00F0 lsr a lsr a - lsr a - and #0x001E ; (b >> 4) * 2 + lsr a ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+960,x - lda tpgBitsW+12 + lda jlIigsTpgBitsW and #0x000F asl a ; (b & 15) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+962,x - ; row 7 - lda tpgBitsW+14 + lda jlIigsTpgBitsW + xba + and #0x00F0 lsr a lsr a lsr a - and #0x001E ; (b >> 4) * 2 tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+1120,x - lda tpgBitsW+14 + lda jlIigsTpgBitsW + xba and #0x000F - asl a ; (b & 15) * 2 + asl a tay - lda tpgWord, y + lda jlIigsTpgWord, y sta 0x012000+1122,x ; fused dirty mark - lda 14,s + lda 10,s tay ; Y = by - lda 12,s ; A = bx - jsr stageTileMark + lda 8,s ; A = bx + jsr jlStageTileMark - ; drop the source D-frame and restore - tsc - clc - adc #4 - tcs pld plb plp rtl -; iigsTilePasteGlyphMark per-call scratch. -.section .bss.tpgBitsW,"aw" -.globl tpgBitsW -tpgBitsW: -.zero 16 +; jlTilePasteGlyph's per-call scratch: the two glyph rows being pasted. +.section .bss.jlIigsTpgBitsW,"aw" +.globl jlIigsTpgBitsW +jlIigsTpgBitsW: +.zero 2 -.section .bss.tpgWord,"aw" -.globl tpgWord -tpgWord: +.section .bss.jlIigsTpgWord,"aw" +.globl jlIigsTpgWord +jlIigsTpgWord: .zero 32 -.section .bss.tpgPb,"aw" -.globl tpgPb -tpgPb: +.section .bss.jlIigsTpgPb,"aw" +.globl jlIigsTpgPb +jlIigsTpgPb: .zero 8 -.section .bss.tpgPbHi,"aw" -.globl tpgPbHi -tpgPbHi: +.section .bss.jlIigsTpgPbHi,"aw" +.globl jlIigsTpgPbHi +jlIigsTpgPbHi: .zero 8 -.section .bss.tpgLastKey,"aw" -.globl tpgLastKey -tpgLastKey: +.section .bss.jlIigsTpgLastKey,"aw" +.globl jlIigsTpgLastKey +jlIigsTpgLastKey: .zero 2 -.section .bss.tpgFgHi,"aw" -.globl tpgFgHi -tpgFgHi: +.section .bss.jlIigsTpgFgHi,"aw" +.globl jlIigsTpgFgHi +jlIigsTpgFgHi: .zero 2 -.section .bss.tpgBgHi,"aw" -.globl tpgBgHi -tpgBgHi: +.section .bss.jlIigsTpgBgHi,"aw" +.globl jlIigsTpgBgHi +jlIigsTpgBgHi: .zero 2 -; iigsTilePasteMonoInner per-call scratch (fg<<4 / bg<<4 staging). -.section .bss.tpmFgHi,"aw" -.globl tpmFgHi -tpmFgHi: +; jlIigsTilePasteMonoInner per-call scratch (fg<<4 / bg<<4 staging). +.section .bss.jlIigsTpmFgHi,"aw" +.globl jlIigsTpmFgHi +jlIigsTpmFgHi: .zero 2 -.section .bss.tpmBgHi,"aw" -.globl tpmBgHi -tpmBgHi: +.section .bss.jlIigsTpmBgHi,"aw" +.globl jlIigsTpmBgHi +jlIigsTpmBgHi: .zero 2 ; Static mono-byte -> 2-bit opacity-pair index table (built once in -; iigsInitRowLut): tpmNibPairIdx[b] = ((b&0xF0)?2:0) | ((b&0x0F)?1:0). -.section .bss.tpmNibPairIdx,"aw" -.globl tpmNibPairIdx -tpmNibPairIdx: +; jlIigsInitRowLut): jlIigsTpmNibPairIdx[b] = ((b&0xF0)?2:0) | ((b&0x0F)?1:0). +.section .bss.jlIigsTpmNibPairIdx,"aw" +.globl jlIigsTpmNibPairIdx +jlIigsTpmNibPairIdx: .zero 256 ; ==================================================================== -; iigsTileCopyMaskedInner(uint8_t *dst, const uint8_t *src, +; jlIigsTileCopyMaskedInner(uint8_t *dst, const uint8_t *src, ; uint16_t transparent) ; ; Same shape as jlTileCopy (160/160 strides, 32 bytes), but source @@ -1688,7 +1728,7 @@ tpmNibPairIdx: ; (word) on the stack. JSL entry: src @4,s, transparent @8,s. ; After PHP+PHB+PHD(+4): src low16 @8,s, src bank @10,s, ; transparent @12,s. The original read `trans` via the D-frame -; ([D+8]); here D points at tileScratchPtrs instead, so we read +; ([D+8]); here D points at jlIigsTileScratchPtrs instead, so we read ; transparent's low byte from the stack (@12,s in M=8) during ; setup. [0],y = dst, [4],y = src in the per-byte subroutine. ; ==================================================================== @@ -1699,39 +1739,39 @@ tpmNibPairIdx: ; expansion. In: M=8, X=16, Y = byte offset, [0]=dst / [4]=src frame. .macro TMASK_BYTE lda [4], y ; A = src byte - cmp tmaskDoubled + cmp jlIigsTmaskDoubled beq 9f ; both nibbles == transparent - sta tmaskSrc + sta jlIigsTmaskSrc and #0xF0 - cmp tmaskTHi + cmp jlIigsTmaskTHi beq 7f - sta tmaskOutHi ; src hi (kept in $F0 form) + sta jlIigsTmaskOutHi ; src hi (kept in $F0 form) bra 8f 7: lda [0], y and #0xF0 - sta tmaskOutHi + sta jlIigsTmaskOutHi 8: - lda tmaskSrc + lda jlIigsTmaskSrc and #0x0F - cmp tmaskTLo + cmp jlIigsTmaskTLo beq 5f - sta tmaskOutLo ; src lo + sta jlIigsTmaskOutLo ; src lo bra 6f 5: lda [0], y and #0x0F - sta tmaskOutLo + sta jlIigsTmaskOutLo 6: - lda tmaskOutHi - ora tmaskOutLo + lda jlIigsTmaskOutHi + ora jlIigsTmaskOutLo sta [0], y 9: .endm -.section .text.iigsTileCopyMaskedInner,"ax" -.globl iigsTileCopyMaskedInner -iigsTileCopyMaskedInner: +.section .text.jlIigsTileCopyMaskedInner,"ax" +.globl jlIigsTileCopyMaskedInner +jlIigsTileCopyMaskedInner: php phb phd ; save caller D @@ -1740,27 +1780,27 @@ iigsTileCopyMaskedInner: .i16 ; Build the dst/src D-frame in bank-0 scratch. - sta tileScratchPtrs+0 ; dst low16 - stx tileScratchPtrs+2 ; dst high16 (bank + pad) - lda 8,s ; src low16 - sta tileScratchPtrs+4 - lda 10,s ; src bank - sta tileScratchPtrs+6 + sta jlIigsTileScratchPtrs+0 ; dst low16 + stx jlIigsTileScratchPtrs+2 ; dst high16 (bank + pad) + lda 8,s ; src low16 + sta jlIigsTileScratchPtrs+4 + lda 10,s ; src bank + sta jlIigsTileScratchPtrs+6 ; Copy the staged dst/src into a STACK (bank-0) D-frame: the - ; tileScratchPtrs BSS scratch is in the LOAD bank, but D (the + ; jlIigsTileScratchPtrs BSS scratch is in the LOAD bank, but D (the ; direct page) is ALWAYS bank 0, so [0],y/[4],y via - ; `tcd #tileScratchPtrs` indirected through the wrong bank. - lda tileScratchPtrs+6 - pha ; src bank -> 7,s - lda tileScratchPtrs+4 - pha ; src low16 -> 5,s - lda tileScratchPtrs+2 - pha ; dst high16 -> 3,s - lda tileScratchPtrs+0 - pha ; dst low16 -> 1,s + ; `tcd #jlIigsTileScratchPtrs` indirected through the wrong bank. + lda jlIigsTileScratchPtrs+6 + pha ; src bank -> 7,s + lda jlIigsTileScratchPtrs+4 + pha ; src low16 -> 5,s + lda jlIigsTileScratchPtrs+2 + pha ; dst high16 -> 3,s + lda jlIigsTileScratchPtrs+0 + pha ; dst low16 -> 1,s tsc - inc a ; A = SP+1 - tcd ; D -> stacked {dst,src}; [0]=dst [4]=src + inc a ; A = SP+1 + tcd ; D -> stacked {dst,src}; [0]=dst [4]=src ; Pre-compute scratch values from `transparent`. ; transparent sits @12,s after the prologue; the 4-word stack @@ -1770,16 +1810,16 @@ iigsTileCopyMaskedInner: lda 20,s ; transparent (low byte) and #0x0F - sta tmaskTLo ; tLo = T + sta jlIigsTmaskTLo ; tLo = T asl a asl a asl a asl a - sta tmaskTHi ; tHi = T<<4 + sta jlIigsTmaskTHi ; tHi = T<<4 - ora tmaskTLo - sta tmaskDoubled ; doubled = (T<<4)|T + ora jlIigsTmaskTLo + sta jlIigsTmaskDoubled ; doubled = (T<<4)|T ; Walk 8 rows of 4 bytes. Y holds the byte offset; after ; the 3 in-row INYs Y is at row*160+3, so +157 lands at @@ -1833,28 +1873,28 @@ tmaskRowsDone: ; double-deref) plus TWO JSLs per stage tile op (op inner + dirty ; marker). These entries do all three in ONE JSL: derive the stage ; destination from the pinned stage base ($01:2000, IIGS_STAGE_PIXELS -; in hal.c) + gRowOffsetLut[by*8] + bx*4, run the op via +; in hal.c) + jlgRowOffsetLut[by*8] + bx*4, run the op via ; absolute-long,X stores with the stage literal folded into each -; offset, then widen the dirty band via stageTileMark below. The -; jlpTile* macros in port.h gate on s == gStage; non-stage surfaces +; offset, then widen the dirty band via jlStageTileMark below. The +; jlpTile* macros in port.h gate on s == jlgStage; non-stage surfaces ; keep taking the pointer-based inners above. ; ==================================================================== -; stageTileMark: shared fused dirty-mark tail. +; jlStageTileMark: shared fused dirty-mark tail. ; In: A = bx (word), Y = by (word); M=16, X=16. ; Out: M=16, X=16. Clobbers A, X, Y. -; Widens gStageMinWord/gStageMaxWord rows by*8..by*8+7 to the tile +; Widens jlgStageMinWord/jlgStageMaxWord rows by*8..by*8+7 to the tile ; band [bx*2, bx*2+1] -- byte compares in M=8 through the same -; DBR-inherited abs,x write path as iigsMarkDirtyRowsInner (the +; DBR-inherited abs,x write path as jlIigsMarkDirtyRowsInner (the ; proven asm-writes-C-global pattern; see the trailer note). ; ONE shared section for the helper + all three fused entries: the ; multi-seg OMF packer places sections atomically, so the 16-bit jsr -; from each entry to stageTileMark stays same-bank by construction +; from each entry to jlStageTileMark stays same-bank by construction ; (same reason tmaskByte lives inside the copyMasked section; 16-bit ; self-references only resolve intra-segment -- see clang-build.sh). .section .text.iigsFusedTile,"ax" -.globl stageTileMark -stageTileMark: +.globl jlStageTileMark +jlStageTileMark: asl a ; A = bx*2 = minWord pha ; park minWord tya @@ -1862,77 +1902,101 @@ stageTileMark: asl a asl a ; A = by*8 = first row tax + sep #0x20 + .a8 + ; The 8 rows starting at by*8 are exactly group `by`, and Y + ; still holds by. + lda jlgStageDirtyGroup,y + bne stmDirty + ; Its flag clear, the group's 8 rows are all CLEAN (every + ; mark flags its groups; the clear after a present resets a + ; group's rows and flag together), so the tile's band is + ; theirs outright: four word stores a side. + lda #1 + sta jlgStageDirtyGroup,y + rep #0x20 + .a16 + lda 1,s ; minWord, high byte 0 + xba + ora 1,s ; minWord in both bytes + ply ; drop the parked copy + sta jlgStageMinWord+0,x + sta jlgStageMinWord+2,x + sta jlgStageMinWord+4,x + sta jlgStageMinWord+6,x + clc + adc #0x0101 ; maxWord = minWord + 1, both bytes + sta jlgStageMaxWord+0,x + sta jlgStageMaxWord+2,x + sta jlgStageMaxWord+4,x + sta jlgStageMaxWord+6,x + rts +stmDirty: + ; Already flagged: widen each row, unrolled. + rep #0x20 + .a16 pla ; A = minWord (high byte 0) sep #0x20 .a8 - ; Coarse group flag: 8 rows starting at by*8 are exactly - ; group `by`, and Y still holds by. One store, no shift. - pha ; park minWord - lda #1 - sta gStageDirtyGroup,y - pla - ldy #8 ; row counter (X stays 16-bit) -stmLoop: - cmp gStageMinWord,x - bcs stmNoMin ; A >= existing -> already covered - sta gStageMinWord,x ; widen down -stmNoMin: + .irp r, 0, 1, 2, 3, 4, 5, 6, 7 + cmp jlgStageMinWord+\r,x + bcs 1f ; A >= existing -> already covered + sta jlgStageMinWord+\r,x +1: inc a ; A = maxWord = minWord + 1 - cmp gStageMaxWord,x - bcc stmNoMax ; A < existing -> already covered - beq stmNoMax - sta gStageMaxWord,x ; widen up -stmNoMax: + cmp jlgStageMaxWord+\r,x + bcc 2f ; A < existing -> already covered + beq 2f + sta jlgStageMaxWord+\r,x +2: dec a ; back to minWord - inx - dey - bne stmLoop + .endr rep #0x20 .a16 rts ; ==================================================================== -; iigsTileFillMark(uint16_t nibble, uint16_t bx, uint16_t by) +; jlIigsTileFillMark(uint16_t nibble, uint16_t bx, uint16_t by) ; ; ABI: arg0 nibble (word, pre-masked to 0x0..0xF by the jlpTileFill ; macro) in A; bx @4,s, by @6,s on JSL entry -> @8,s / @10,s ; after php+phb+phd. No D-frame: the destination is reached via ; $012000+off,x (absolute long indexed), X = tile byte offset. ; fillWord = nibble * $1111 derived in asm (PERF-AUDIT #41); -; tileScratchPtrs is free scratch (no src pointer here). +; jlIigsTileScratchPtrs is free scratch (no src pointer here). ; ==================================================================== -.globl iigsTileFillMark -iigsTileFillMark: +.globl jlIigsTileFillMark +jlIigsTileFillMark: php phb phd rep #0x30 .a16 .i16 - sta tileScratchPtrs+0 ; nibble + sta jlIigsTileScratchPtrs+0 ; nibble asl a asl a asl a - asl a ; nibble << 4 - ora tileScratchPtrs+0 ; nibble * 0x11 - sta tileScratchPtrs+2 - xba ; doubled byte << 8 - ora tileScratchPtrs+2 ; nibble * 0x1111 = fillWord - sta tileScratchPtrs+4 ; park fillWord - ; dst offset = gRowOffsetLut[by*8] + bx*4 - lda 10,s ; by + asl a ; nibble << 4 + ora jlIigsTileScratchPtrs+0 ; nibble * 0x11 + sta jlIigsTileScratchPtrs+2 + xba ; doubled byte << 8 + ora jlIigsTileScratchPtrs+2 ; nibble * 0x1111 = fillWord + sta jlIigsTileScratchPtrs+4 ; park fillWord + ; dst offset = jlgRowOffsetLut[by*8] + bx*4 + lda 10,s ; by asl a asl a asl a - asl a ; by*16 = (by*8) * 2 = LUT byte index + asl a ; by*16 = (by*8) * 2 = LUT byte index tax - lda 8,s ; bx + lda 8,s ; bx asl a - asl a ; bx*4 + asl a ; bx*4 clc - adc gRowOffsetLut,x - tax ; X = stage byte offset of tile row 0 - lda tileScratchPtrs+4 ; fillWord + adc jlgRowOffsetLut,x + tax ; X = stage byte offset of tile row 0 + lda jlIigsTileScratchPtrs+4 ; fillWord sta 0x012000+0,x sta 0x012000+2,x sta 0x012000+160,x @@ -1951,216 +2015,200 @@ iigsTileFillMark: sta 0x012000+1122,x ; fused dirty mark lda 10,s - tay ; Y = by - lda 8,s ; A = bx - jsr stageTileMark + tay ; Y = by + lda 8,s ; A = bx + jsr jlStageTileMark pld plb plp rtl ; ==================================================================== -; iigsTilePasteMark(const uint8_t *srcTilePixels, uint16_t bx, uint16_t by) +; jlIigsTilePasteMark(const uint8_t *srcTilePixels, uint16_t bx, uint16_t by) ; ; ABI: arg0 src in A:X (packed 32-byte tile, stride 4); bx @4,s, -; by @6,s on JSL entry -> @8,s / @10,s after php+phb+phd -> -; @12,s / @14,s after the 4-byte src D-frame (phx+pha). The src -; D-frame comes straight from the registers -- no tileScratchPtrs -; round-trip (the fill-style single-pointer frame). Dst is -; $012000+off,x like iigsTileFillMark. +; by @6,s on JSL entry -> @8,s / @10,s after php+phb+phx. The src +; comes straight from the registers: its offset to Y and, once the +; row offset is read under the caller's DBR, its bank to DBR, so +; its words load as n,y (12 cycles a word against 17 through a +; [0],y D-frame); the caller's DBR is back for the mark. Dst is +; $012000+off,x like jlIigsTileFillMark. ; ==================================================================== -.globl iigsTilePasteMark -iigsTilePasteMark: +.globl jlIigsTilePasteMark +jlIigsTilePasteMark: php phb - phd rep #0x30 .a16 .i16 - phx ; src high16 -> 3,s - pha ; src low16 -> 1,s - tsc - inc a - tcd ; [0] = srcTilePixels - ; dst offset = gRowOffsetLut[by*8] + bx*4 - lda 14,s ; by + phx ; src high16 (its bank in the low byte) -> 1,s + tay ; Y = src offset in its bank + ; dst offset = jlgRowOffsetLut[by*8] + bx*4 + lda 10,s ; by asl a asl a asl a asl a tax - lda 12,s ; bx + lda 8,s ; bx asl a asl a clc - adc gRowOffsetLut,x + adc jlgRowOffsetLut,x tax ; X = stage byte offset of tile row 0 - ; 8 rows: src stride 4 via [0],y; dst stride 160 folded + ; DBR = the source's bank: its words load as n,y + sep #0x20 + .a8 + lda 1,s + pha + plb + rep #0x20 + .a16 + ; 8 rows: src stride 4 at n,y; dst stride 160 folded ; into the long,x literals. - ldy #0 - lda [0], y + lda 0,y sta 0x012000+0,x - ldy #2 - lda [0], y + lda 2,y sta 0x012000+2,x - ldy #4 - lda [0], y + lda 4,y sta 0x012000+160,x - ldy #6 - lda [0], y + lda 6,y sta 0x012000+162,x - ldy #8 - lda [0], y + lda 8,y sta 0x012000+320,x - ldy #10 - lda [0], y + lda 10,y sta 0x012000+322,x - ldy #12 - lda [0], y + lda 12,y sta 0x012000+480,x - ldy #14 - lda [0], y + lda 14,y sta 0x012000+482,x - ldy #16 - lda [0], y + lda 16,y sta 0x012000+640,x - ldy #18 - lda [0], y + lda 18,y sta 0x012000+642,x - ldy #20 - lda [0], y + lda 20,y sta 0x012000+800,x - ldy #22 - lda [0], y + lda 22,y sta 0x012000+802,x - ldy #24 - lda [0], y + lda 24,y sta 0x012000+960,x - ldy #26 - lda [0], y + lda 26,y sta 0x012000+962,x - ldy #28 - lda [0], y + lda 28,y sta 0x012000+1120,x - ldy #30 - lda [0], y + lda 30,y sta 0x012000+1122,x + ; the caller's DBR back: the mark addresses the bands absolutely + sep #0x20 + .a8 + lda 3,s ; phb'd on entry + pha + plb + rep #0x20 + .a16 ; fused dirty mark - lda 14,s + lda 10,s tay ; Y = by - lda 12,s ; A = bx - jsr stageTileMark - ; drop the src D-frame, restore - tsc - clc - adc #4 - tcs - pld + lda 8,s ; A = bx + jsr jlStageTileMark + ply ; drop the src high16 plb plp rtl ; ==================================================================== -; iigsTileCopyMark(const uint8_t *srcRow0, uint16_t dstBx, uint16_t dstBy) +; jlIigsTileCopyMark(const uint8_t *srcRow0, uint16_t dstBx, uint16_t dstBy) ; -; Same shape as iigsTilePasteMark but the source is a SURFACE row +; Same shape as jlIigsTilePasteMark but the source is a SURFACE row ; (stride 160, any bank -- the jlpTileCopy macro derives it in C -; because src may be any surface), so the [0],y offsets mirror the -; dst deltas. Dst is the stage, derived in asm. +; because src may be any surface), so the n,y offsets mirror the +; dst deltas (abs,Y carries into the bank, so a source row crossing +; one still resolves). Dst is the stage, derived in asm. ; ==================================================================== -.globl iigsTileCopyMark -iigsTileCopyMark: +.globl jlIigsTileCopyMark +jlIigsTileCopyMark: php phb - phd rep #0x30 .a16 .i16 - phx ; src high16 -> 3,s - pha ; src low16 -> 1,s - tsc - inc a - tcd ; [0] = srcRow0 - ; dst offset = gRowOffsetLut[dstBy*8] + dstBx*4 - lda 14,s ; dstBy + phx ; src high16 (its bank in the low byte) -> 1,s + tay ; Y = src offset in its bank + ; dst offset = jlgRowOffsetLut[dstBy*8] + dstBx*4 + lda 10,s ; dstBy asl a asl a asl a asl a tax - lda 12,s ; dstBx + lda 8,s ; dstBx asl a asl a clc - adc gRowOffsetLut,x + adc jlgRowOffsetLut,x tax ; X = stage byte offset of tile row 0 - ; 8 rows: src stride 160 via [0],y; dst stride 160 in + ; DBR = the source's bank: its words load as n,y + sep #0x20 + .a8 + lda 1,s + pha + plb + rep #0x20 + .a16 + ; 8 rows: src stride 160 at n,y; dst stride 160 in ; the long,x literals (parallel offsets = block copy). - ldy #0 - lda [0], y + lda 0,y sta 0x012000+0,x - ldy #2 - lda [0], y + lda 2,y sta 0x012000+2,x - ldy #160 - lda [0], y + lda 160,y sta 0x012000+160,x - ldy #162 - lda [0], y + lda 162,y sta 0x012000+162,x - ldy #320 - lda [0], y + lda 320,y sta 0x012000+320,x - ldy #322 - lda [0], y + lda 322,y sta 0x012000+322,x - ldy #480 - lda [0], y + lda 480,y sta 0x012000+480,x - ldy #482 - lda [0], y + lda 482,y sta 0x012000+482,x - ldy #640 - lda [0], y + lda 640,y sta 0x012000+640,x - ldy #642 - lda [0], y + lda 642,y sta 0x012000+642,x - ldy #800 - lda [0], y + lda 800,y sta 0x012000+800,x - ldy #802 - lda [0], y + lda 802,y sta 0x012000+802,x - ldy #960 - lda [0], y + lda 960,y sta 0x012000+960,x - ldy #962 - lda [0], y + lda 962,y sta 0x012000+962,x - ldy #1120 - lda [0], y + lda 1120,y sta 0x012000+1120,x - ldy #1122 - lda [0], y + lda 1122,y sta 0x012000+1122,x + ; the caller's DBR back: the mark addresses the bands absolutely + sep #0x20 + .a8 + lda 3,s ; phb'd on entry + pha + plb + rep #0x20 + .a16 ; fused dirty mark - lda 14,s + lda 10,s tay ; Y = dstBy - lda 12,s ; A = dstBx - jsr stageTileMark - ; drop the src D-frame, restore - tsc - clc - adc #4 - tcs - pld + lda 8,s ; A = dstBx + jsr jlStageTileMark + ply ; drop the src high16 plb plp rtl ; ==================================================================== -; iigsTileMapPasteMark(uint16_t bx, uint16_t by, uint16_t wTiles, +; jlIigsTileMapPasteMark(uint16_t bx, uint16_t by, uint16_t wTiles, ; uint16_t hTiles, const jlTileT *tiles, ; const uint8_t *map) ; @@ -2168,45 +2216,49 @@ iigsTileCopyMark: ; wTiles x hTiles row-major uint8_t index map and pastes every ; non-0xFF tile to the stage, then widens the dirty band once for the ; whole rectangle -- amortizing the per-tile JSL, arg staging, derive, -; and 8-row mark that iigsTilePasteMark pays per tile. The dst offset +; and 8-row mark that jlIigsTilePasteMark pays per tile. The dst offset ; advances incrementally (+4 per tile column, +1280 per tile row = -; 8 SHR rows) from ONE gRowOffsetLut read at entry. +; 8 SHR rows) from ONE jlgRowOffsetLut read at entry. ; ; ABI: bx in A; by @4,s, wTiles @6,s, hTiles @8,s, tiles @10,s/12,s, ; map @14,s/16,s on JSL entry. Frame: php+phb+phd (+4), parked -; bx (+2), then a 22-byte reserved D-frame (+22) = +28 total; D -; = SP+1 so frame slots are D-relative and args sit at D+31..43. +; bx (+2), then an 18-byte reserved D-frame (+18) = +24 total; D +; = SP+1 so frame slots are D-relative and args sit at D+27..39. ; X survives the 16-pair copy body (sta long,x never writes X), -; so it carries the walking dst offset; Y is clobbered per cell -; and the map index lives in the frame instead. The idx << 5 -; tileset add carries into the bank byte (tmwSrc+2), so a -; tileset crossing a bank boundary still resolves. +; so it carries the walking dst offset; the map index lives in the +; frame, since Y is each tile's offset in its bank while it is +; copied: DBR is set to the tile's bank (the tileset bank plus the +; idx << 5 add's carry, so a tileset crossing a bank boundary still +; resolves; a tile straddling one does too, as abs,Y carries into +; the bank), and its words load as lda n,y -- 12 cycles a word +; against 17 for ldy #n / lda [src],y. The caller's DBR is back +; before the band union, which addresses the bands absolutely. ; ; No internal jsr (the band union is inline), so the section is ; self-contained -- no cross-section 16-bit-jsr hazard. The three ; wrapping branches (skip-cell forward, column back, row back) span ; the ~150-byte copy body and use the bne-over-brl idiom. ; ==================================================================== -.section .text.iigsTileMapPasteMark,"ax" -.globl iigsTileMapPasteMark -iigsTileMapPasteMark: -tmwSrc = 0 ; current tile src far ptr ([0],y) -tmwMap = 4 ; map far ptr ([4],y) -tmwRowBase = 8 ; dst offset of current tile row -tmwTilesLo = 10 ; tileset base low16 -tmwTilesHi = 12 ; tileset base high16 (bank) -tmwMapIdx = 14 ; running map index (reused: yEnd for the mark) -tmwTx = 16 ; columns left this row (reused: minWord) -tmwTy = 18 ; rows left (reused: maxWord) -tmwWt = 20 ; wTiles reload value -tmwBx = 22 ; parked bx -tmwByArg = 31 ; by (orig 4,s + 28 pushed, D-relative) -tmwWtArg = 33 ; wTiles (orig 6,s) -tmwHtArg = 35 ; hTiles (orig 8,s) -tmwTLoArg = 37 ; tiles low16 (orig 10,s) -tmwTHiArg = 39 ; tiles high16 (orig 12,s) -tmwMLoArg = 41 ; map low16 (orig 14,s) -tmwMHiArg = 43 ; map high16 (orig 16,s) +.section .text.jlIigsTileMapPasteMark,"ax" +.globl jlIigsTileMapPasteMark +jlIigsTileMapPasteMark: +tmwMap = 0 ; map far ptr ([0],y) +tmwRowBase = 4 ; dst offset of current tile row +tmwTilesLo = 6 ; tileset base low16 +tmwTilesHi = 8 ; tileset base high16 (bank) +tmwMapIdx = 10 ; running map index (reused: yEnd for the mark) +tmwTx = 12 ; columns left this row (reused: minWord) +tmwTy = 14 ; rows left (reused: maxWord) +tmwWt = 16 ; wTiles reload value +tmwBx = 18 ; parked bx +tmwDbr = 22 ; the caller's DBR (phb, under the phd) +tmwByArg = 27 ; by (orig 4,s + 24 pushed, D-relative) +tmwWtArg = 29 ; wTiles (orig 6,s) +tmwHtArg = 31 ; hTiles (orig 8,s) +tmwTLoArg = 33 ; tiles low16 (orig 10,s) +tmwTHiArg = 35 ; tiles high16 (orig 12,s) +tmwMLoArg = 37 ; map low16 (orig 14,s) +tmwMHiArg = 39 ; map high16 (orig 16,s) php phb @@ -2214,11 +2266,11 @@ tmwMHiArg = 43 ; map high16 (orig 16,s) rep #0x30 .a16 .i16 - pha ; park bx -> D+22 + pha ; park bx -> D+18 tsc sec - sbc #22 - tcs ; reserve the 22-byte frame + sbc #18 + tcs ; reserve the 18-byte frame inc a tcd ; D = frame base (bank-0 stack) @@ -2235,7 +2287,7 @@ tmwMHiArg = 43 ; map high16 (orig 16,s) lda tmwHtArg sta tmwTy stz tmwMapIdx - ; row-0 dst offset = gRowOffsetLut[by*8] + bx*4 (ONCE) + ; row-0 dst offset = jlgRowOffsetLut[by*8] + bx*4 (ONCE) lda tmwByArg asl a asl a @@ -2246,7 +2298,7 @@ tmwMHiArg = 43 ; map high16 (orig 16,s) asl a asl a clc - adc gRowOffsetLut,x + adc jlgRowOffsetLut,x sta tmwRowBase tmwRowTop: @@ -2275,59 +2327,48 @@ tmwDoTile: asl a ; idx * 32 (sizeof proof in tile.c) clc adc tmwTilesLo - sta tmwSrc+0 + tay ; Y = the tile's offset in its bank + sep #0x20 + .a8 lda tmwTilesHi - adc #0 ; bank carry - sta tmwSrc+2 - ; 8 rows: src stride 4 via [0],y; dst stride 160 folded - ; into the long,x literals (the iigsTilePasteMark body). - ldy #0 - lda [tmwSrc], y + adc #0 ; its bank: the add's carry + pha + plb ; DBR = the tile's bank + rep #0x20 + .a16 + ; 8 rows: src stride 4 at n,y; dst stride 160 folded + ; into the long,x literals (the jlIigsTilePasteMark body). + lda 0,y sta 0x012000+0,x - ldy #2 - lda [tmwSrc], y + lda 2,y sta 0x012000+2,x - ldy #4 - lda [tmwSrc], y + lda 4,y sta 0x012000+160,x - ldy #6 - lda [tmwSrc], y + lda 6,y sta 0x012000+162,x - ldy #8 - lda [tmwSrc], y + lda 8,y sta 0x012000+320,x - ldy #10 - lda [tmwSrc], y + lda 10,y sta 0x012000+322,x - ldy #12 - lda [tmwSrc], y + lda 12,y sta 0x012000+480,x - ldy #14 - lda [tmwSrc], y + lda 14,y sta 0x012000+482,x - ldy #16 - lda [tmwSrc], y + lda 16,y sta 0x012000+640,x - ldy #18 - lda [tmwSrc], y + lda 18,y sta 0x012000+642,x - ldy #20 - lda [tmwSrc], y + lda 20,y sta 0x012000+800,x - ldy #22 - lda [tmwSrc], y + lda 22,y sta 0x012000+802,x - ldy #24 - lda [tmwSrc], y + lda 24,y sta 0x012000+960,x - ldy #26 - lda [tmwSrc], y + lda 26,y sta 0x012000+962,x - ldy #28 - lda [tmwSrc], y + lda 28,y sta 0x012000+1120,x - ldy #30 - lda [tmwSrc], y + lda 30,y sta 0x012000+1122,x tmwCellNext: inc tmwMapIdx ; 16-bit (M=16 on both arrival paths) @@ -2352,6 +2393,15 @@ tmwMark: ; JL_TILE_MAP_SKIP cells -- safe, present just re-uploads ; clean pixels). Frame slots are dead now; reuse them: ; tmwMapIdx = yEnd, tmwTx = minWord, tmwTy = maxWord. + ; The bands are addressed absolutely: the caller's DBR + ; first, in place of the last tile's. + sep #0x20 + .a8 + lda tmwDbr + pha + plb + rep #0x20 + .a16 lda tmwByArg clc adc tmwHtArg @@ -2368,6 +2418,22 @@ tmwMark: asl a dec a sta tmwTy ; maxWord = (bx + wTiles) * 2 - 1 + ; The 8-row group flags, one per tile row (by .. by + hTiles + ; - 1, hTiles >= 1 by the C caller): the present skips a + ; group whose flag is clear and the clear after it resets + ; only flagged groups. + ldy tmwHtArg + ldx tmwByArg + sep #0x20 + .a8 + lda #1 +tmwGrpLoop: + sta jlgStageDirtyGroup,x + inx + dey + bne tmwGrpLoop + rep #0x20 + .a16 lda tmwByArg asl a asl a @@ -2377,15 +2443,15 @@ tmwMark: .a8 tmwMarkLoop: lda tmwTx - cmp gStageMinWord,x + cmp jlgStageMinWord,x bcs tmwNoMin - sta gStageMinWord,x ; widen down + sta jlgStageMinWord,x ; widen down tmwNoMin: lda tmwTy - cmp gStageMaxWord,x + cmp jlgStageMaxWord,x bcc tmwNoMax beq tmwNoMax - sta gStageMaxWord,x ; widen up + sta jlgStageMaxWord,x ; widen up tmwNoMax: inx cpx tmwMapIdx @@ -2395,7 +2461,7 @@ tmwNoMax: .i16 tsc clc - adc #24 ; drop frame (22) + parked bx (2) + adc #20 ; drop frame (18) + parked bx (2) tcs pld plb @@ -2410,65 +2476,65 @@ tmwNoMax: ; primitive's stack frame because the per-byte subroutine call would ; otherwise need to thread state through registers. (bank-0 globals) ; -------------------------------------------------------------------- -.section .bss.tmaskTLo,"aw" -.globl tmaskTLo -tmaskTLo: +.section .bss.jlIigsTmaskTLo,"aw" +.globl jlIigsTmaskTLo +jlIigsTmaskTLo: .zero 1 -.section .bss.tmaskTHi,"aw" -.globl tmaskTHi -tmaskTHi: +.section .bss.jlIigsTmaskTHi,"aw" +.globl jlIigsTmaskTHi +jlIigsTmaskTHi: .zero 1 -.section .bss.tmaskDoubled,"aw" -.globl tmaskDoubled -tmaskDoubled: +.section .bss.jlIigsTmaskDoubled,"aw" +.globl jlIigsTmaskDoubled +jlIigsTmaskDoubled: .zero 1 -.section .bss.tmaskSrc,"aw" -.globl tmaskSrc -tmaskSrc: +.section .bss.jlIigsTmaskSrc,"aw" +.globl jlIigsTmaskSrc +jlIigsTmaskSrc: .zero 1 -.section .bss.tmaskOutHi,"aw" -.globl tmaskOutHi -tmaskOutHi: +.section .bss.jlIigsTmaskOutHi,"aw" +.globl jlIigsTmaskOutHi +jlIigsTmaskOutHi: .zero 1 -.section .bss.tmaskOutLo,"aw" -.globl tmaskOutLo -tmaskOutLo: +.section .bss.jlIigsTmaskOutLo,"aw" +.globl jlIigsTmaskOutLo +jlIigsTmaskOutLo: .zero 1 ; ===== ported: fillrect / pixel / line ===== ; ==================================================================== ; JoeyLib IIgs draw primitives (llvm-mc GAS, w65816) -; Functions: iigsFillRectMark, iigsFillRectNoMark, iigsDrawPixelInner, -; iigsDrawLineInner +; Functions: jlIigsFillRectMark, jlIigsFillRectNoMark, jlIigsDrawPixelInner, +; jlIigsDrawLineInner ; ==================================================================== ; ==================================================================== ; NATIVE-PERF Phase 6 W1: fused stage fillRect entries + shared mark. ; -; stageRectMark: shared fused dirty-mark tail for rect-shaped stage +; jlStageRectMark: shared fused dirty-mark tail for rect-shaped stage ; ops (fillRect now; the W1 fused sprite entries reopen this section ; and jsr here too -- a 16-bit jsr must NEVER cross .section blocks, ; see the fused-tile trailer note at .text.iigsFusedTile). -; Generalizes stageTileMark to N rows and a computed word band. +; Generalizes jlStageTileMark to N rows and a computed word band. ; ; In: A = (maxWord << 8) | minWord (each 0..79), X = yStart (word), ; Y = rowCount (word, >= 1). M=16, X=16. ; Out: M=16, X=16. Clobbers A, X, Y. -; Widens gStageMinWord/gStageMaxWord rows yStart..yStart+rowCount-1 +; Widens jlgStageMinWord/jlgStageMaxWord rows yStart..yStart+rowCount-1 ; to cover [minWord, maxWord]. Byte compares in M=8 through the same -; DBR-inherited abs,x write path as stageTileMark / -; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; callers +; DBR-inherited abs,x write path as jlStageTileMark / +; jlIigsMarkDirtyRowsInner (caller's DBR = the C data bank; callers ; must NOT phk/plb before jsr-ing here). ; ==================================================================== .section .text.iigsFusedRect,"ax" -.globl stageRectMark -stageRectMark: +.globl jlStageRectMark +jlStageRectMark: ; Coarse group flags for rows X .. X+Y-1, before the band ; walk consumes the registers. A holds the band, X yStart, ; Y rowCount -- all three are restored below. @@ -2479,7 +2545,7 @@ stageRectMark: lsr a lsr a lsr a - sta gStageGrpTmp ; first group + sta jlgStageGrpTmp ; first group txa clc adc 1,s ; + rowCount @@ -2488,14 +2554,14 @@ stageRectMark: lsr a lsr a ; last group sec - sbc gStageGrpTmp + sbc jlgStageGrpTmp tay ; Y = group count - 1 - ldx gStageGrpTmp + ldx jlgStageGrpTmp sep #0x20 .a8 lda #1 strmGrpLoop: - sta gStageDirtyGroup,x + sta jlgStageDirtyGroup,x inx dey bpl strmGrpLoop @@ -2507,15 +2573,15 @@ strmGrpLoop: sep #0x20 .a8 strmLoop: - cmp gStageMinWord,x ; A (low byte) = minWord + cmp jlgStageMinWord,x ; A (low byte) = minWord bcs strmNoMin ; A >= existing -> already covered - sta gStageMinWord,x ; widen down + sta jlgStageMinWord,x ; widen down strmNoMin: xba ; A = maxWord - cmp gStageMaxWord,x + cmp jlgStageMaxWord,x bcc strmNoMax ; A < existing -> already covered beq strmNoMax - sta gStageMaxWord,x ; widen up + sta jlgStageMaxWord,x ; widen up strmNoMax: xba ; back to minWord for the next row inx @@ -2527,26 +2593,26 @@ strmNoMax: ; ==================================================================== -; iigsFillRectMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble) -; iigsFillRectNoMark(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble) +; jlIigsFillRectMark (uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble) +; jlIigsFillRectNoMark(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble) ; ; Fill an axis-aligned rectangle of 4bpp pixels into the IIgs SHR -; stage at bank $01 (pinned base + gRowOffsetLut row derive), then -- +; stage at bank $01 (pinned base + jlgRowOffsetLut row derive), then -- ; Mark flavor only -- widen the dirty band rows y..y+h-1 to -; [x>>2, (x+w-1)>>2] via stageRectMark, replacing the old fill JSL + -; C-side surfaceMarkDirtyRect -> iigsMarkDirtyRowsInner JSL pair with +; [x>>2, (x+w-1)>>2] via jlStageRectMark, replacing the old fill JSL + +; C-side surfaceMarkDirtyRect -> jlIigsMarkDirtyRowsInner JSL pair with ; ONE JSL. The NoMark twin (same body, flag skips the mark tail) ; serves callers that batch their own mark (jlDrawRect outline, ; jlFillCircle spans). Stage-gated by the C dispatch (port.h). ; -; ABI: clang cdecl. x arrives in A (parked in friX); y,w,h,nibble are +; ABI: clang cdecl. x arrives in A (parked in jlIigsFriX); y,w,h,nibble are ; on the stack (right-to-left). On JSL entry: y@4,s w@6,s h@8,s ; nib@10,s. After php+phb+phd (+4) and tsc+adc#8+tcd (D = SP+8), the ; equates are: ; frm_y=0 frm_w=2 frm_h=4 frm_nib=6 ; ==================================================================== -.globl iigsFillRectNoMark -iigsFillRectNoMark: +.globl jlIigsFillRectNoMark +jlIigsFillRectNoMark: frm_y = 0 frm_w = 2 frm_h = 4 @@ -2558,21 +2624,21 @@ frm_nib = 6 rep #0x30 .a16 .i16 - sta friX ; park arg0 (x) - stz friDoMark ; unfused arm (batched-mark callers) + sta jlIigsFriX ; park arg0 (x) + stz jlIigsFriDoMark ; unfused arm (batched-mark callers) bra frmBody -.globl iigsFillRectMark -iigsFillRectMark: +.globl jlIigsFillRectMark +jlIigsFillRectMark: php phb phd rep #0x30 .a16 .i16 - sta friX ; park arg0 (x) + sta jlIigsFriX ; park arg0 (x) lda #1 - sta friDoMark + sta jlIigsFriDoMark frmBody: tsc clc @@ -2580,61 +2646,61 @@ frmBody: tcd ; pxStart = x; pxEnd = x + w. - lda friX - sta friPxStart + lda jlIigsFriX + sta jlIigsFriPxStart clc adc frm_w - sta friPxEnd + sta jlIigsFriPxEnd ; leadingByte = x >> 1; hasLeading = x & 1. - lda friX + lda jlIigsFriX lsr a - sta friLeadingByte - lda friX + sta jlIigsFriLeadingByte + lda jlIigsFriX and #1 - sta friHasLeading + sta jlIigsFriHasLeading beq friStartReady - lda friPxStart + lda jlIigsFriPxStart inc a - sta friPxStart + sta jlIigsFriPxStart friStartReady: ; midStart = pxStart >> 1. - lda friPxStart + lda jlIigsFriPxStart lsr a - sta friMidStart + sta jlIigsFriMidStart ; spanLen = pxEnd - pxStart; hasTrailing = spanLen & 1; midBytes = spanLen >> 1. - lda friPxEnd + lda jlIigsFriPxEnd sec - sbc friPxStart - sta friMidBytes ; temporary hold spanLen + sbc jlIigsFriPxStart + sta jlIigsFriMidBytes ; temporary hold spanLen and #1 - sta friHasTrailing - lda friMidBytes + sta jlIigsFriHasTrailing + lda jlIigsFriMidBytes lsr a - sta friMidBytes + sta jlIigsFriMidBytes ; trailingByte = midStart + midBytes. - lda friMidStart + lda jlIigsFriMidStart clc - adc friMidBytes - sta friTrailingByte + adc jlIigsFriMidBytes + sta jlIigsFriTrailingByte -; nibble decoding: friNibLo = nib (low half), friNibHi = nib<<4 -; (high half), friDoubled = (nib<<4)|nib (full byte fill). +; nibble decoding: jlIigsFriNibLo = nib (low half), jlIigsFriNibHi = nib<<4 +; (high half), jlIigsFriDoubled = (nib<<4)|nib (full byte fill). sep #0x20 .a8 lda frm_nib and #0x0F - sta friNibLo + sta jlIigsFriNibLo asl a asl a asl a asl a - sta friNibHi - ora friNibLo - sta friDoubled + sta jlIigsFriNibHi + ora jlIigsFriNibLo + sta jlIigsFriDoubled rep #0x20 .a16 @@ -2642,67 +2708,67 @@ friStartReady: lda frm_y asl a tax - lda gRowOffsetLut,x + lda jlgRowOffsetLut,x clc adc #0x2000 - sta friCurRow + sta jlIigsFriCurRow lda frm_h - sta friRowsLeft + sta jlIigsFriRowsLeft -; friFillWord = friDoubled replicated into both bytes (16-bit pattern). - lda friDoubled +; jlIigsFriFillWord = jlIigsFriDoubled replicated into both bytes (16-bit pattern). + lda jlIigsFriDoubled and #0x00FF - sta friFillWord + sta jlIigsFriFillWord xba ; A = byte in high half - ora friFillWord - sta friFillWord + ora jlIigsFriFillWord + sta jlIigsFriFillWord ; Row-invariant hoists (Phase 6 W1): the slam entry address, the slam ; X bias, the odd-middle-byte column, and its flag never change across ; rows -- computed once here instead of the old ~64-cyc-per-row ; recompute inside friMidNonZero. Harmless when midBytes == 0 (unused). - lda friMidBytes + lda jlIigsFriMidBytes lsr a - sta friSlamWords ; nWords = midBytes >> 1 + sta jlIigsFriSlamWords ; nWords = midBytes >> 1 lda #80 sec - sbc friSlamWords + sbc jlIigsFriSlamWords asl a asl a clc adc #friMidSlamTbl-1 - sta friSlamEntry ; rts computed-goto target - 1 - lda friSlamWords + sta jlIigsFriSlamEntry ; rts computed-goto target - 1 + lda jlIigsFriSlamWords asl a clc - adc friMidStart - sta friOddOff ; midStart + 2*nWords (odd mid byte column) + adc jlIigsFriMidStart + sta jlIigsFriOddOff ; midStart + 2*nWords (odd mid byte column) sec sbc #160 - sta friSlamBias ; per row: X = friCurRow + friSlamBias - lda friMidBytes + sta jlIigsFriSlamBias ; per row: X = jlIigsFriCurRow + jlIigsFriSlamBias + lda jlIigsFriMidBytes and #1 - sta friHasOddMid + sta jlIigsFriHasOddMid friRowLoop: - lda friRowsLeft + lda jlIigsFriRowsLeft bne friDoRow brl friExit friDoRow: ; Leading partial-byte RMW. - lda friHasLeading + lda jlIigsFriHasLeading beq friNoLead - lda friCurRow + lda jlIigsFriCurRow clc - adc friLeadingByte + adc jlIigsFriLeadingByte tax sep #0x20 .a8 lda 0x010000,x and #0xF0 - ora friNibLo + ora jlIigsFriNibLo sta 0x010000,x rep #0x20 .a16 @@ -2711,7 +2777,7 @@ friNoLead: ; Middle bytes: if midBytes == 0, skip; if midBytes == 1, just seed ; one byte; if midBytes >= 2, seed first byte then MVN propagates the ; seed across the rest of the row. - lda friMidBytes + lda jlIigsFriMidBytes bne friMidNonZero brl friNoMid friMidNonZero: @@ -2726,13 +2792,13 @@ friMidNonZero: ; hazard. nWords==0 (a 1-byte middle) lands just past the table on the ; odd-byte writer. An odd trailing middle byte is written after the ; table. - lda friSlamEntry + lda jlIigsFriSlamEntry pha - lda friCurRow + lda jlIigsFriCurRow clc - adc friSlamBias + adc jlIigsFriSlamBias tax - lda friFillWord + lda jlIigsFriFillWord rts ; computed-goto into friMidSlamTbl friMidSlamTbl: sta 0x010000,x @@ -2818,15 +2884,15 @@ friMidSlamTbl: ; Odd trailing middle byte: the slam wrote 2*nWords bytes; if midBytes ; is odd, write the last middle byte at curRow + midStart + 2*nWords ; (flag + column hoisted above the row loop). - lda friHasOddMid + lda jlIigsFriHasOddMid beq friMidNoOdd - lda friCurRow + lda jlIigsFriCurRow clc - adc friOddOff + adc jlIigsFriOddOff tax sep #0x20 .a8 - lda friDoubled + lda jlIigsFriDoubled sta 0x010000,x rep #0x20 .a16 @@ -2834,57 +2900,57 @@ friMidNoOdd: friNoMid: ; Trailing partial-byte RMW. - lda friHasTrailing + lda jlIigsFriHasTrailing beq friNoTrail - lda friCurRow + lda jlIigsFriCurRow clc - adc friTrailingByte + adc jlIigsFriTrailingByte tax sep #0x20 .a8 lda 0x010000,x and #0x0F - ora friNibHi + ora jlIigsFriNibHi sta 0x010000,x rep #0x20 .a16 friNoTrail: ; Advance to next row. - lda friCurRow + lda jlIigsFriCurRow clc adc #160 - sta friCurRow - lda friRowsLeft + sta jlIigsFriCurRow + lda jlIigsFriRowsLeft dec a - sta friRowsLeft + sta jlIigsFriRowsLeft brl friRowLoop friExit: ; ----- fused dirty mark: rows y..y+h-1, band [x>>2, (x+w-1)>>2] ----- -; friPxEnd = x + w with the ORIGINAL x/w (only friPxStart was nudged +; jlIigsFriPxEnd = x + w with the ORIGINAL x/w (only jlIigsFriPxStart was nudged ; for the leading nibble), so the band covers the partial-nibble edges ; exactly like SURFACE_WORD_INDEX in the retired C-side mark. - lda friDoMark + lda jlIigsFriDoMark beq frmSkipMark - lda friPxEnd + lda jlIigsFriPxEnd dec a lsr a lsr a ; maxWord = (x + w - 1) >> 2 xba ; maxWord -> high byte (low byte 0) - sta friMarkBand - lda friX + sta jlIigsFriMarkBand + lda jlIigsFriX lsr a lsr a ; minWord = x >> 2 - ora friMarkBand ; A = (maxWord << 8) | minWord - sta friMarkBand ; park the packed band + ora jlIigsFriMarkBand ; A = (maxWord << 8) | minWord + sta jlIigsFriMarkBand ; park the packed band lda frm_y tax ; X = first row lda frm_h tay ; Y = row count (>= 1 by contract) - lda friMarkBand - jsr stageRectMark + lda jlIigsFriMarkBand + jsr jlStageRectMark frmSkipMark: pld plb @@ -2892,93 +2958,93 @@ frmSkipMark: rtl -.section .bss.friPxStart,"aw" -.globl friPxStart -friPxStart: +.section .bss.jlIigsFriPxStart,"aw" +.globl jlIigsFriPxStart +jlIigsFriPxStart: .zero 2 -.section .bss.friPxEnd,"aw" -.globl friPxEnd -friPxEnd: +.section .bss.jlIigsFriPxEnd,"aw" +.globl jlIigsFriPxEnd +jlIigsFriPxEnd: .zero 2 -.section .bss.friLeadingByte,"aw" -.globl friLeadingByte -friLeadingByte: +.section .bss.jlIigsFriLeadingByte,"aw" +.globl jlIigsFriLeadingByte +jlIigsFriLeadingByte: .zero 2 -.section .bss.friHasLeading,"aw" -.globl friHasLeading -friHasLeading: +.section .bss.jlIigsFriHasLeading,"aw" +.globl jlIigsFriHasLeading +jlIigsFriHasLeading: .zero 2 -.section .bss.friMidStart,"aw" -.globl friMidStart -friMidStart: +.section .bss.jlIigsFriMidStart,"aw" +.globl jlIigsFriMidStart +jlIigsFriMidStart: .zero 2 -.section .bss.friMidBytes,"aw" -.globl friMidBytes -friMidBytes: +.section .bss.jlIigsFriMidBytes,"aw" +.globl jlIigsFriMidBytes +jlIigsFriMidBytes: .zero 2 -.section .bss.friHasTrailing,"aw" -.globl friHasTrailing -friHasTrailing: +.section .bss.jlIigsFriHasTrailing,"aw" +.globl jlIigsFriHasTrailing +jlIigsFriHasTrailing: .zero 2 -.section .bss.friTrailingByte,"aw" -.globl friTrailingByte -friTrailingByte: +.section .bss.jlIigsFriTrailingByte,"aw" +.globl jlIigsFriTrailingByte +jlIigsFriTrailingByte: .zero 2 -.section .bss.friNibLo,"aw" -.globl friNibLo -friNibLo: +.section .bss.jlIigsFriNibLo,"aw" +.globl jlIigsFriNibLo +jlIigsFriNibLo: .zero 2 -.section .bss.friNibHi,"aw" -.globl friNibHi -friNibHi: +.section .bss.jlIigsFriNibHi,"aw" +.globl jlIigsFriNibHi +jlIigsFriNibHi: .zero 2 -.section .bss.friDoubled,"aw" -.globl friDoubled -friDoubled: +.section .bss.jlIigsFriDoubled,"aw" +.globl jlIigsFriDoubled +jlIigsFriDoubled: .zero 2 -.section .bss.friCurRow,"aw" -.globl friCurRow -friCurRow: +.section .bss.jlIigsFriCurRow,"aw" +.globl jlIigsFriCurRow +jlIigsFriCurRow: .zero 2 -.section .bss.friRowsLeft,"aw" -.globl friRowsLeft -friRowsLeft: +.section .bss.jlIigsFriRowsLeft,"aw" +.globl jlIigsFriRowsLeft +jlIigsFriRowsLeft: .zero 2 -.section .bss.friFillWord,"aw" -.globl friFillWord -friFillWord: +.section .bss.jlIigsFriFillWord,"aw" +.globl jlIigsFriFillWord +jlIigsFriFillWord: .zero 2 -.section .bss.friSlamWords,"aw" -.globl friSlamWords -friSlamWords: +.section .bss.jlIigsFriSlamWords,"aw" +.globl jlIigsFriSlamWords +jlIigsFriSlamWords: .zero 2 -.section .bss.friX,"aw" -.globl friX -friX: +.section .bss.jlIigsFriX,"aw" +.globl jlIigsFriX +jlIigsFriX: .zero 2 -.section .bss.friDoMark,"aw" -.globl friDoMark -friDoMark: +.section .bss.jlIigsFriDoMark,"aw" +.globl jlIigsFriDoMark +jlIigsFriDoMark: .zero 2 -.section .bss.friMarkBand,"aw" -.globl friMarkBand -friMarkBand: +.section .bss.jlIigsFriMarkBand,"aw" +.globl jlIigsFriMarkBand +jlIigsFriMarkBand: .zero 2 -.section .bss.friSlamEntry,"aw" -.globl friSlamEntry -friSlamEntry: +.section .bss.jlIigsFriSlamEntry,"aw" +.globl jlIigsFriSlamEntry +jlIigsFriSlamEntry: .zero 2 -.section .bss.friSlamBias,"aw" -.globl friSlamBias -friSlamBias: +.section .bss.jlIigsFriSlamBias,"aw" +.globl jlIigsFriSlamBias +jlIigsFriSlamBias: .zero 2 -.section .bss.friOddOff,"aw" -.globl friOddOff -friOddOff: +.section .bss.jlIigsFriOddOff,"aw" +.globl jlIigsFriOddOff +jlIigsFriOddOff: .zero 2 -.section .bss.friHasOddMid,"aw" -.globl friHasOddMid -friHasOddMid: +.section .bss.jlIigsFriHasOddMid,"aw" +.globl jlIigsFriHasOddMid +jlIigsFriHasOddMid: .zero 2 @@ -3003,20 +3069,20 @@ friHasOddMid: ; need be re-patches the save/restore MVN bank operands (the ; patchMvnBanks rule, spriteDispatch.h), fills the backup's metadata ; (save), derives the stage offset from the pinned base ($01:2000 + -; gRowOffsetLut[y] + x/2), long-calls the routine(s) and widens the -; dirty band (stageRectMark). The save, the restore and the band cover +; jlgRowOffsetLut[y] + x/2), long-calls the routine(s) and widens the +; dirty band (jlStageRectMark). The save, the restore and the band cover ; only the sprite's content rows (contentRow0 / contentRows, ; spriteInternal.h): the draw never writes the transparent rows above ; and below. A failed entry has done nothing and returns non-zero; the ; C caller falls back to its generic path. ; -; Callable only from C with DBR = the C data bank (gCodegenBankAddr, -; gRowOffsetLut, gStage and stageRectMark's band tables are read abs,x) -; and, for the iigsSprite*Stage entries, only for gStage, fully on it +; Callable only from C with DBR = the C data bank (jlgCodegenBankAddr, +; jlgRowOffsetLut, jlgStage and jlStageRectMark's band tables are read abs,x) +; and, for the iigsSprite*Stage entries, only for jlgStage, fully on it ; (callers own both). Nothing here changes DBR, and the compiled ; routines preserve D and DBR and return M=16/X=16 (spriteEmitIigs.c), ; so the frame survives them; C callers expect no flags back, so P is -; not saved either. In .text.iigsFusedRect: the jsr to stageRectMark +; not saved either. In .text.iigsFusedRect: the jsr to jlStageRectMark ; stays inside one section (the fused-tile trailer rule). ; ; ABI (clang cdecl): arg0 in A:X, the rest stacked from 4,s on JSL @@ -3131,9 +3197,9 @@ ISP_ARGS = ISP_LOCALS + 5 ldy #0 lda [ispFn], y ; slot->offset, low word clc - adc gCodegenBankAddr, x + adc jlgCodegenBankAddr, x sta ispBase - lda gCodegenBankAddr+2, x + lda jlgCodegenBankAddr+2, x sta ispBase+2 ldy #SPR_WPX lda [ispSpr], y @@ -3171,7 +3237,7 @@ ISP_ARGS = ISP_LOCALS + 5 lda ispX lsr a clc - adc gRowOffsetLut, x + adc jlgRowOffsetLut, x clc adc #STAGE_BASE sta ispScr @@ -3258,7 +3324,7 @@ ISP_ARGS = ISP_LOCALS + 5 ora ispTmp ldx ispY ldy ispH - jsr stageRectMark + jsr jlStageRectMark .endm ; The save, its routine at ispFn (index ispIdx), ispY on the first @@ -3398,9 +3464,9 @@ ISP_ARGS = ISP_LOCALS + 5 ; ---- iigsSprite*Stage: internal, for sprite.c's checked paths ------- -; uint16_t iigsSpriteDrawStage(jlSpriteT *sp, int16_t x, int16_t y) -.globl iigsSpriteDrawStage -iigsSpriteDrawStage: +; uint16_t jlIigsSpriteDrawStage(jlSpriteT *sp, int16_t x, int16_t y) +.globl jlIigsSpriteDrawStage +jlIigsSpriteDrawStage: ISP_ENTER sty ispSpr stx ispSpr+2 @@ -3428,10 +3494,10 @@ isdFail: ISP_LEAVE rtl -; uint16_t iigsSpriteSaveStage(jlSpriteT *sp, int16_t x, int16_t y, +; uint16_t jlIigsSpriteSaveStage(jlSpriteT *sp, int16_t x, int16_t y, ; jlSpriteBackupT *backup) -.globl iigsSpriteSaveStage -iigsSpriteSaveStage: +.globl jlIigsSpriteSaveStage +jlIigsSpriteSaveStage: ISP_ENTER sty ispSpr stx ispSpr+2 @@ -3461,10 +3527,10 @@ issFail: ISP_LEAVE rtl -; uint16_t iigsSpriteSaveDrawStage(jlSpriteT *sp, int16_t x, int16_t y, +; uint16_t jlIigsSpriteSaveDrawStage(jlSpriteT *sp, int16_t x, int16_t y, ; jlSpriteBackupT *backup) -.globl iigsSpriteSaveDrawStage -iigsSpriteSaveDrawStage: +.globl jlIigsSpriteSaveDrawStage +jlIigsSpriteSaveDrawStage: ISP_ENTER sty ispSpr stx ispSpr+2 @@ -3485,9 +3551,9 @@ isdsFail: ISP_LEAVE rtl -; uint16_t iigsSpriteRestoreStage(const jlSpriteBackupT *backup) -.globl iigsSpriteRestoreStage -iigsSpriteRestoreStage: +; uint16_t jlIigsSpriteRestoreStage(const jlSpriteBackupT *backup) +.globl jlIigsSpriteRestoreStage +jlIigsSpriteRestoreStage: ISP_ENTER sty ispBk stx ispBk+2 @@ -3510,7 +3576,7 @@ isrFail: ; The IIgs bodies of these two public calls (joey/sprite.h; sprite.c ; leaves them out here): the inner-loop pair, so they skip the C wrapper ; frame and the second JSL with its arguments pushed again that a C body -; costs (~190 cycles a call). s == gStage goes straight into the stage +; costs (~190 cycles a call). s == jlgStage goes straight into the stage ; code; anything else -- another surface, a sprite or an op that is not ; compiled, a backup that is not whole compiled-format -- goes on to the ; checked jlSpriteSaveAndDraw / jlSpriteRestoreUnder with the same @@ -3522,9 +3588,9 @@ jlSpriteSaveAndDrawTrusted: ISP_ENTER sty ispSurf stx ispSurf+2 - cpy gStage + cpy jlgStage bne istsChecked - cpx gStage+2 + cpx jlgStage+2 bne istsChecked lda ISP_ARGS sta ispSpr @@ -3573,9 +3639,9 @@ jlSpriteRestoreUnderTrusted: ISP_ENTER sty ispSurf stx ispSurf+2 - cpy gStage + cpy jlgStage bne istrChecked - cpx gStage+2 + cpx jlgStage+2 bne istrChecked lda ISP_ARGS sta ispBk @@ -3637,7 +3703,7 @@ ispbRow: rts ; ==================================================================== -; iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, +; jlIigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, ; uint16_t nibble) ; ; Plot one 4bpp pixel at (x,y) into a 320x200 surface. Caller has @@ -3666,9 +3732,9 @@ ispbRow: ; as words -> a harmless 4th filler byte in the 4-byte scratch. ; ==================================================================== -.section .text.iigsDrawPixelInner,"ax" -.globl iigsDrawPixelInner -iigsDrawPixelInner: +.section .text.jlIigsDrawPixelInner,"ax" +.globl jlIigsDrawPixelInner +jlIigsDrawPixelInner: pix = 0 ; pixels far ptr (D+0..2 = stacked copy) php @@ -3683,7 +3749,7 @@ pix = 0 ; pixels far ptr (D+0..2 = stacked ; own. Stores and the [pix],y read agreed with each other, so it looked right ; until the link layout moved that offset somewhere bank 0 would not hold it. ; The hardware stack IS bank 0. Same fix as the tile routines and -; iigsDrawLineInner. The 4 pushed bytes stay for the life of the call, so the +; jlIigsDrawLineInner. The 4 pushed bytes stay for the life of the call, so the ; N,s offsets below are 4 higher than the JSL-entry figures and the exit drops ; them before PLD. phx ; pix high16 (bank) -> 3,s @@ -3693,14 +3759,14 @@ pix = 0 ; pixels far ptr (D+0..2 = stacked tcd ; D -> stacked pix; [pix],y == [0],y ; Compute byte offset = y*160 + (x>>1) into A. Fold the LUT row directly -; into the add (adc gRowOffsetLut,x) -- no scratch round-trip (#53). +; into the add (adc jlgRowOffsetLut,x) -- no scratch round-trip (#53). lda 13,s ; y asl a ; A = y*2 (LUT byte offset) tax lda 11,s ; x lsr a ; A = x >> 1 clc - adc gRowOffsetLut,x ; A = x>>1 + y*160 + adc jlgRowOffsetLut,x ; A = x>>1 + y*160 tay sep #0x20 ; M=8 @@ -3716,10 +3782,10 @@ pix = 0 ; pixels far ptr (D+0..2 = stacked asl a asl a asl a - sta dpxlNibPart + sta jlIigsDpxlNibPart lda [pix],y and #0x0F - ora dpxlNibPart + ora jlIigsDpxlNibPart sta [pix],y bra dpxlDone @@ -3727,10 +3793,10 @@ dpxlOdd: ; Odd x -> low nibble. lda 15,s ; nibble and #0x0F - sta dpxlNibPart + sta jlIigsDpxlNibPart lda [pix],y and #0xF0 - ora dpxlNibPart + ora jlIigsDpxlNibPart sta [pix],y dpxlDone: @@ -3746,15 +3812,15 @@ dpxlDone: ; ==================================================================== -; iigsDrawPixelMark(uint16_t x, uint16_t y, uint16_t nibble) +; jlIigsDrawPixelMark(uint16_t x, uint16_t y, uint16_t nibble) ; ; FUSED stage-only jlDrawPixel (NATIVE-PERF Phase 2 R10): one JSL does ; the single-row dirty widen (word x>>2, row y -- same abs,x -; DBR-inherited write path as iigsMarkDirtyRowsInner) and the nibble -; RMW plot against the pinned stage base ($01:2000 + gRowOffsetLut[y] +; DBR-inherited write path as jlIigsMarkDirtyRowsInner) and the nibble +; RMW plot against the pinned stage base ($01:2000 + jlgRowOffsetLut[y] ; + (x>>1), long,x with the literal folded in). Caller (jlDrawPixel) -; has clipped and owns the s == gStage gate; the batching plotters -; (plotPixelNoMark for lines/circles) stay on iigsDrawPixelInner. +; has clipped and owns the s == jlgStage gate; the batching plotters +; (plotPixelNoMark for lines/circles) stay on jlIigsDrawPixelInner. ; ; ABI: x in A; y @4,s, nibble @6,s on JSL entry. Frame is php (+1) ; plus the parked x (+2): x @1,s, y @7,s, nibble @9,s. No PHB (DBR @@ -3762,67 +3828,79 @@ dpxlDone: ; No intra-asm jsr, so the section is self-contained (cross-section ; 16-bit jsr is a wild jump -- see the fused-tile block note). ; ==================================================================== -.section .text.iigsDrawPixelMark,"ax" -.globl iigsDrawPixelMark -iigsDrawPixelMark: +.section .text.jlIigsDrawPixelMark,"ax" +.globl jlIigsDrawPixelMark +jlIigsDrawPixelMark: php rep #0x30 .a16 .i16 pha ; park x; y -> 7,s nib -> 9,s - ; --- single-row widen: A still holds x --- - lsr a - lsr a - tay ; Y = wordIdx = x>>2 (<= 79) lda 7,s ; y tax ; X = row - tya ; A = wordIdx - sep #0x20 - .a8 - cmp gStageMinWord,x - bcs dpxmNoMin - sta gStageMinWord,x ; widen down -dpxmNoMin: - cmp gStageMaxWord,x - bcc dpxmNoMax - beq dpxmNoMax - sta gStageMaxWord,x ; widen up -dpxmNoMax: - rep #0x20 - .a16 - ; --- plot: byte offset = gRowOffsetLut[y] + (x>>1) --- - lda 7,s ; y - asl a - tax + lsr a + lsr a + lsr a + tay ; Y = the row's 8-row group lda 1,s ; x lsr a - clc - adc gRowOffsetLut,x + lsr a ; A = wordIdx = x>>2 (<= 79) + sep #0x20 + .a8 + ; --- single-row widen --- + cmp jlgStageMinWord,x + bcs dpxmNoMin + sta jlgStageMinWord,x ; widen down +dpxmNoMin: + cmp jlgStageMaxWord,x + bcc dpxmNoMax + beq dpxmNoMax + sta jlgStageMaxWord,x ; widen up +dpxmNoMax: + ; --- the group flag: the present skips a group whose flag + ; is clear, and the clear after it resets only flagged + ; groups --- + lda #1 + sta jlgStageDirtyGroup,y + rep #0x20 + .a16 + ; --- plot: byte offset = jlgRowOffsetLut[y] + (x>>1); the + ; halving leaves x's parity in the carry, so an odd pixel + ; adds one byte too many and is reached through the stage + ; less one --- + txa + asl a + tax ; X = the row's LUT index + lda 1,s ; x + lsr a ; A = x>>1, C = x & 1 + bcs dpxmOdd + adc jlgRowOffsetLut,x ; carry clear tax ; X = stage byte offset sep #0x20 .a8 - lda 1,s ; x low byte (parity in LSB) - and #1 - bne dpxmOdd lda 9,s ; nibble -> high half (left pixel) asl a asl a asl a asl a - sta dpxlNibPart + sta jlIigsDpxlNibPart lda 0x012000,x and #0x0F - ora dpxlNibPart + ora jlIigsDpxlNibPart sta 0x012000,x bra dpxmDone dpxmOdd: + adc jlgRowOffsetLut,x ; carry set: the offset + 1 + tax + sep #0x20 + .a8 lda 9,s ; nibble -> low half (right pixel) and #0x0F - sta dpxlNibPart - lda 0x012000,x + sta jlIigsDpxlNibPart + lda 0x011FFF,x and #0xF0 - ora dpxlNibPart - sta 0x012000,x + ora jlIigsDpxlNibPart + sta 0x011FFF,x dpxmDone: rep #0x30 .a16 @@ -3832,235 +3910,592 @@ dpxmDone: rtl -.section .bss.dpxlNibPart,"aw" -.globl dpxlNibPart -dpxlNibPart: +.section .bss.jlIigsDpxlNibPart,"aw" +.globl jlIigsDpxlNibPart +jlIigsDpxlNibPart: .zero 1 ; ==================================================================== -; iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, +; jlIigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, ; uint16_t x1, uint16_t y1, uint16_t nibble) ; ; Bresenham line plot. Caller has clipped both endpoints into ; [0..319] x [0..199], so no per-pixel bounds check. ; -; Walks (dx, dy, sx, sy, err) and plots inline (no per-pixel -; function call). Each plot does the same parity-aware nibble RMW as -; iigsDrawPixel. State stored in DRAWPRIMS scratch because registers -; are too few for the full Bresenham state plus current pixel address. +; The generic walk (core draw.c's, src/generic/genericDraw.c +; jlpGenericDrawLine) split by the major axis: an x-major line steps x +; every pixel and y when 2*err <= dx, a y-major one y every pixel and x +; when 2*err >= dy (dy negative) -- the same pixels as the generic walk +; over every delta of the surface (host model). The pixel's byte offset +; rides in Y, stepped a byte at a time and a row (+-160) at a time, and its +; nibble is which of two copies of the plot the walk is in (even x: the +; high nibble), so no pixel recomputes an address or tests a parity. The +; error term is kept doubled (lnErr2 = 2*err) and each step's compare is +; folded into its update: x-major takes lnErr2 - (dx + 1), negative when +; 2*err <= dx, and adds back dx + 1 + 2*dy (lnKeep), or that plus 2*dx +; with the y step (lnStep); y-major takes lnErr2 - dy, not negative when +; 2*err >= dy, and adds back dy + 2*dx, or that plus 2*dy with the x step. +; X counts the pixels left. ; -; ABI: clang cdecl. pixels arrives in A:X (dereferenced via [pix],y); -; x0,y0,x1,y1,nibble on the stack. Same hybrid as DrawPixel: llvm-mc -; cannot encode [dp],y from a 16-bit bss symbol, so we repoint D at a -; bank-0 struct -- and that struct is the HARDWARE STACK, not a bss slot -; (see the prologue note). We push pixels (A:X) and use [pix],y with pix=0 -; in the loop. The five word args are copied into DRAWPRIMS scratch up front -; (stack-relative), so the loop never touches the stack frame again. -; Prologue pushes: php(+1)+phb(+1)+phd(+2)+phx(+2)+pha(+2)=+8. On JSL entry -; x0@4,s y0@6,s x1@8,s y1@10,s nib@12,s; after the 8 pushes: -; x0@12,s y0@14,s x1@16,s y1@18,s nib@20,s -; pix=0 (D-relative, = the pushed far pointer) +; ABI: clang cdecl. pixels arrives in A:X (dereferenced via [lnPix],y); +; x0,y0,x1,y1,nibble on the stack. llvm-mc cannot encode [dp],y from a +; 16-bit bss symbol, and the direct page is ALWAYS bank 0 -- a bss slot's +; address is an offset in the LOAD bank, so `lda #bssSym; tcd` lands on +; somebody else's bank-0 memory (it once made the corner-to-corner diagonal +; plot nothing). So the state is a D-frame on the HARDWARE STACK, which is +; bank 0: LN_LOCALS bytes of locals below the pushed pixels pointer, D = +; SP + 1, interrupts pushing below. Prologue pushes: php(+1) phb(+1) +; phd(+2) phx(+2) pha(+2) and the locals; on JSL entry x0@4,s y0@6,s +; x1@8,s y1@10,s nib@12,s. ; ==================================================================== +.equ LN_LOCALS, 14 +.equ lnNibHi, 0 ; nibble << 4 (high byte 0) +.equ lnNibLo, 2 ; nibble (high byte 0) +.equ lnErr2, 4 ; 2 * err +.equ lnCmp, 6 ; x-major dx + 1, y-major dy +.equ lnKeep, 8 ; added back when the minor axis holds +.equ lnStep, 10 ; added back when it steps +.equ lnRow, 12 ; +-160: a row, y's way +.equ lnPix, LN_LOCALS ; the pushed pixels far pointer +.equ LN_X0, 12 + LN_LOCALS +.equ LN_Y0, 14 + LN_LOCALS +.equ LN_X1, 16 + LN_LOCALS +.equ LN_Y1, 18 + LN_LOCALS +.equ LN_NIB, 20 + LN_LOCALS -.section .text.iigsDrawLineInner,"ax" -.globl iigsDrawLineInner -iigsDrawLineInner: -pix = 0 ; pixels far ptr (D+0..2 = bank0 scratch) +; One pixel, the high or the low nibble of the byte at Y. +.macro LN_PLOT_HI + lda [lnPix],y + and #0xFF0F + ora lnNibHi + sta [lnPix],y +.endm +.macro LN_PLOT_LO + lda [lnPix],y + and #0xFFF0 + ora lnNibLo + sta [lnPix],y +.endm +; Y a row on, y's way. +.macro LN_ROW + tya + clc + adc lnRow + tay +.endm +.section .text.jlIigsDrawLineInner,"ax" +.globl jlIigsDrawLineInner +jlIigsDrawLineInner: php phb phd rep #0x30 .a16 .i16 - -; Build the pix D-frame on the STACK (bank 0) -- the same fix the tile -; routines already carry. `lda #dlnScratch; tcd` was NOT safe: the direct -; page is ALWAYS bank 0, but dlnScratch is a BSS symbol whose address is an -; offset in the LOAD bank, so D landed on bank 0 at that offset -- somebody -; else's memory. The stores and the [pix],y read agreed with each other, so -; the routine looked correct, but those four bytes lived in whatever bank 0 -; keeps at that address; once the link layout put it in the $D000+ language -; card region the pointer stopped surviving the round trip and every plot -; wrote into nowhere. That is what killed the corner-to-corner diagonal -; (0,0)-(319,199): a perfect Bresenham walk, 320 plots issued, zero pixels -; changed. The hardware stack IS bank 0, so a stack D-frame always matches. -; -; A = pix low16, X = pix high16. After the two pushes D+0 is the offset and -; D+2 the bank, so `pix = 0` still names the whole far pointer, and the two -; stores the old code needed are gone. The 4 bytes STAY on the stack for the -; life of the call, so every N,s argument offset below is 4 higher than the -; header's JSL-entry figures, and dlnExit drops them before PLD. - phx ; pix high16 (bank) -> 3,s - pha ; pix low16 (offset) -> 1,s + phx ; pixels high16 (bank) + pha ; pixels low16 tsc - inc a ; A = SP+1 - tcd ; D -> stacked pix; [pix],y == [0],y - -; dx = |x1 - x0|, sx = sign(x1 - x0) - lda 16,s ; x1 sec - sbc 12,s ; x0 - bpl dlnDxPos - eor #0xFFFF - clc - adc #1 ; A = x0 - x1 (positive |dx|) - sta dlnDx - lda #0xFFFF - sta dlnSx ; sx = -1 - bra dlnDxDone -dlnDxPos: - sta dlnDx - lda #1 - sta dlnSx -dlnDxDone: + sbc #LN_LOCALS + tcs + inc a + tcd ; D = SP + 1: locals, then lnPix -; dy = -|y1 - y0|, sy = sign(y1 - y0). Bresenham uses negative dy. - lda 18,s ; y1 - sec - sbc 14,s ; y0 - bpl dlnDyPos - sta dlnDy ; A is negative; that's dy with the sign already - lda #0xFFFF - sta dlnSy ; sy = -1 - bra dlnDyDone -dlnDyPos: -; y1 >= y0 -> raw dy positive; negate for Bresenham (-|y1-y0|). - eor #0xFFFF - clc - adc #1 - sta dlnDy - lda #1 - sta dlnSy -dlnDyDone: - -; err = dx + dy - lda dlnDx - clc - adc dlnDy - sta dlnErr - -; Copy x0, y0 to running state slots. - lda 12,s ; x0 - sta dlnX - lda 14,s ; y0 - sta dlnY - -; Cache x1, y1 so the loop's compare is fast. - lda 16,s ; x1 - sta dlnX1 - lda 18,s ; y1 - sta dlnY1 - -; Precompute both 16-bit nibble OR-values ONCE (stays in M=16 the -; whole loop -- no per-pixel SEP/REP flip). The nibble RMW reads the -; target byte plus its adjacent high byte (16-bit), masks so only the -; target nibble changes, and writes both bytes back; the high byte is -; preserved by the mask + zero OR-high-byte, so the result is identical -; to the old 8-bit single-byte RMW. dlnNibHi/dlnNibLo have low byte = -; (nib<<4) / nib and high byte = 0. - lda 20,s ; nibble + lda LN_NIB,s and #0x000F - sta dlnNibLo ; low byte = nib, high byte = 0 + sta lnNibLo asl a asl a asl a asl a - sta dlnNibHi ; low byte = nib<<4, high byte = 0 + sta lnNibHi -dlnLoop: -; --- Plot pixel at (dlnX, dlnY) --- LUT path. Stays in M=16. - lda dlnY - asl a ; A = y*2 (LUT byte offset) +; Y = the first pixel's byte: y0 * 160 + x0 / 2. + lda LN_Y0,s + asl a tax - lda gRowOffsetLut,x ; A = y*160 - sta dlnTmp - lda dlnX - lsr a ; x >> 1 + lda jlgRowOffsetLut,x + sta lnErr2 ; scratch until the walk's set-up + lda LN_X0,s + lsr a clc - adc dlnTmp ; byte offset + adc lnErr2 tay - lda dlnX ; x parity - and #1 - bne dlnPlotOdd +; lnRow = +-160 and lnCmp = dy = -|y1 - y0|. + lda LN_Y1,s + sec + sbc LN_Y0,s + bmi lnUp + eor #0xFFFF + inc a + sta lnCmp + lda #160 + bra lnRowSet +lnUp: + sta lnCmp + lda #0xFFFF - 159 ; -160 +lnRowSet: + sta lnRow -; Even x -> high nibble. Mask keeps low nibble of target byte and the -; whole adjacent high byte; OR sets nib<<4 into the high nibble. - lda [pix],y +; X = |x1 - x0|; the sign picks the walk. + lda LN_X1,s + sec + sbc LN_X0,s + bpl lnRight + eor #0xFFFF + inc a + tax + clc + adc lnCmp ; dx + dy: x-major when >= 0 + bmi lnYLeftSet + jsr lnXSet + lda LN_X0,s + lsr a + bcs lnXmOdd + brl lnXmEven +lnYLeftSet: + jsr lnYSet + lda LN_X0,s + lsr a + bcs lnYmOddJ + brl lnYmEven +lnYmOddJ: + brl lnYmOdd +lnRight: + tax + clc + adc lnCmp + bmi lnYRightSet + jsr lnXSet + lda LN_X0,s + lsr a + bcs lnXpOddJ + brl lnXpEven +lnXpOddJ: + brl lnXpOdd +lnYRightSet: + jsr lnYSet + lda LN_X0,s + lsr a + bcs lnYpOddJ + brl lnYpEven +lnYpOddJ: + brl lnYpOdd + +; x-major, x leftward (dx in X). +lnXmOdd: + LN_PLOT_LO + dex + bmi lnXmDone + lda lnErr2 + sec + sbc lnCmp + bmi lnXmOddY + clc + adc lnKeep + sta lnErr2 + bra lnXmEven +lnXmOddY: + clc + adc lnStep + sta lnErr2 + LN_ROW +lnXmEven: + LN_PLOT_HI + dex + bmi lnXmDone + dey + lda lnErr2 + sec + sbc lnCmp + bmi lnXmEvenY + clc + adc lnKeep + sta lnErr2 + bra lnXmOdd +lnXmEvenY: + clc + adc lnStep + sta lnErr2 + LN_ROW + bra lnXmOdd +lnXmDone: + brl lnDone + +; x-major, x rightward. +lnXpEven: + LN_PLOT_HI + dex + bmi lnXpDone + lda lnErr2 + sec + sbc lnCmp + bmi lnXpEvenY + clc + adc lnKeep + sta lnErr2 + bra lnXpOdd +lnXpEvenY: + clc + adc lnStep + sta lnErr2 + LN_ROW +lnXpOdd: + LN_PLOT_LO + dex + bmi lnXpDone + iny + lda lnErr2 + sec + sbc lnCmp + bmi lnXpOddY + clc + adc lnKeep + sta lnErr2 + bra lnXpEven +lnXpOddY: + clc + adc lnStep + sta lnErr2 + LN_ROW + bra lnXpEven +lnXpDone: + brl lnDone + +; y-major, x leftward (|dy| in X). +lnYmOdd: + LN_PLOT_LO + dex + bmi lnYmDone + LN_ROW + lda lnErr2 + sec + sbc lnCmp + bmi lnYmOddKeep + clc + adc lnStep + sta lnErr2 + bra lnYmEven +lnYmOddKeep: + clc + adc lnKeep + sta lnErr2 + bra lnYmOdd +lnYmEven: + LN_PLOT_HI + dex + bmi lnYmDone + LN_ROW + lda lnErr2 + sec + sbc lnCmp + bmi lnYmEvenKeep + clc + adc lnStep + sta lnErr2 + dey + bra lnYmOdd +lnYmEvenKeep: + clc + adc lnKeep + sta lnErr2 + bra lnYmEven +lnYmDone: + brl lnDone + +; y-major, x rightward. +lnYpEven: + LN_PLOT_HI + dex + bmi lnYpDone + LN_ROW + lda lnErr2 + sec + sbc lnCmp + bmi lnYpEvenKeep + clc + adc lnStep + sta lnErr2 + bra lnYpOdd +lnYpEvenKeep: + clc + adc lnKeep + sta lnErr2 + bra lnYpEven +lnYpOdd: + LN_PLOT_LO + dex + bmi lnYpDone + LN_ROW + lda lnErr2 + sec + sbc lnCmp + bmi lnYpOddKeep + clc + adc lnStep + sta lnErr2 + iny + bra lnYpEven +lnYpOddKeep: + clc + adc lnKeep + sta lnErr2 + bra lnYpOdd +lnYpDone: + brl lnDone + +; x-major set-up (dx in X, dy in lnCmp): lnErr2 = 2dx + 2dy, lnKeep = +; dx + 1 + 2dy, lnStep = lnKeep + 2dx, then lnCmp = dx + 1. X stays the +; count (dx + 1 pixels: X runs dx down to -1). +lnXSet: + lda lnCmp + asl a ; 2dy + sta lnKeep + txa + asl a ; 2dx + clc + adc lnKeep + sta lnErr2 ; 2dx + 2dy + txa + inc a ; dx + 1 + sta lnCmp + clc + adc lnKeep + sta lnKeep ; dx + 1 + 2dy + txa + asl a + clc + adc lnKeep + sta lnStep ; dx + 1 + 2dy + 2dx + rts + +; y-major set-up (dx in X, dy in lnCmp): lnErr2 = 2dx + 2dy, lnKeep = +; dy + 2dx, lnStep = lnKeep + 2dy; X becomes the count, |dy|. +lnYSet: + txa + asl a ; 2dx + clc + adc lnCmp + sta lnKeep ; dy + 2dx + clc + adc lnCmp + sta lnErr2 ; 2dx + 2dy + clc + adc lnCmp + sta lnStep ; 2dx + 3dy + lda lnCmp + eor #0xFFFF + inc a + tax ; |dy| + rts + +lnDone: + tsc + clc + adc #LN_LOCALS + 4 ; the locals and the pixels pointer + tcs + .a8 + .i8 + pld + plb + plp + rtl + +; ===== ported: circles ===== +; ================================================================ +; jlIigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, +; uint16_t r, uint16_t nibble) +; +; Bresenham midpoint circle outline. Caller has verified that the +; whole bounding box (cx-r..cx+r, cy-r..cy+r) fits inside the +; surface, so the inner loop plots all 8 octants without per-pixel +; clip checks. The four rows a step plots on, (cy +/- y)*160 and +; (cy +/- x)*160, are kept as byte offsets and stepped a row at a time. +; +; A plot's column halved by `lsr a` leaves its parity in the carry. An +; even column adds its row with the carry clear and plots the high nibble +; through pixels; an odd one adds it with the carry set -- a byte too far +; -- and plots the low nibble through pixels - 1 (ciPixM1), so no plot +; clears the carry or tests the parity again. The state is in a direct- +; page frame. +; +; ABI (llvm-mos w65816 cdecl): pixels in A:X; cx, cy, r, nibble on the +; stack, cx@4,s cy@6,s r@8,s nib@10,s on JSL entry. The direct page is +; ALWAYS bank 0 and a bss address is a LOAD-bank offset, so the frame is +; on the hardware stack (bank 0): CI_LOCALS bytes of locals below the +; pushed pixels pointer, D = SP + 1, interrupts pushing below. Prologue +; pushes php(+1) phb(+1) phd(+2) phx(+2) pha(+2) and the locals. +; ================================================================ +.equ CI_LOCALS, 24 +.equ ciNibHi, 0 ; nibble << 4 (high byte 0) +.equ ciNibLo, 2 ; nibble (high byte 0) +.equ ciX, 4 +.equ ciY, 6 +.equ ciErr, 8 +.equ ciRowYP, 10 ; (cy + y) * 160 +.equ ciRowYN, 12 ; (cy - y) * 160 +.equ ciRowXP, 14 ; (cy + x) * 160 +.equ ciRowXN, 16 ; (cy - x) * 160 +.equ ciCx, 18 +.equ ciPixM1, 20 ; pixels - 1 (far) +.equ ciPix, CI_LOCALS ; the pushed pixels far pointer +.equ CI_CX, 12 + CI_LOCALS +.equ CI_CY, 14 + CI_LOCALS +.equ CI_R, 16 + CI_LOCALS +.equ CI_NIB, 18 + CI_LOCALS + +; One reflection: column cx + or - \off (adc / sbc, with clc / sec), on +; row \row. +.macro CI_PLOT carry, op, off, row + lda ciCx + \carry + \op \off + lsr a + bcs 1f + adc \row + tay + lda [ciPix],y and #0xFF0F - ora dlnNibHi - sta [pix],y - bra dlnPlotDone -dlnPlotOdd: -; Odd x -> low nibble. 65816 is little-endian: byte[Y] is the LOW byte -; of A, so $FFF0 clears the target's low nibble and preserves the whole -; adjacent byte at Y+1; OR sets nib into the low nibble. (The old $F0FF -; had the bytes swapped: it never cleared the target nibble and erased -; the neighbor's low nibble -- claudeDocs/PERF-AUDIT.md #87.) - lda [pix],y + ora ciNibHi + sta [ciPix],y + bra 2f +1: + adc \row + tay + lda [ciPixM1],y and #0xFFF0 - ora dlnNibLo - sta [pix],y -dlnPlotDone: + ora ciNibLo + sta [ciPixM1],y +2: +.endm -; --- Loop test: if X==X1 and Y==Y1, done --- - lda dlnX - cmp dlnX1 - bne dlnStep - lda dlnY - cmp dlnY1 - bne dlnStep - brl dlnExit - -dlnStep: -; e2 = err << 1 (signed). Compare e2 against dy and dx. - lda dlnErr - asl a ; e2 = err * 2 - sta dlnE2 - -; if (e2 >= dy) { err += dy; X += sx; } -; dy is negative, so signed-compare with BMI/BPL needed. -; Test e2 - dy >= 0 via SEC; SBC dy; BPL. + .section .text.jlIigsDrawCircleInner,"ax" + .globl jlIigsDrawCircleInner +jlIigsDrawCircleInner: + php + phb + phd + rep #0x30 + .a16 + .i16 + phx ; pixels high16 (bank) + pha ; pixels low16 + tsc sec - sbc dlnDy - bmi dlnSkipX ; e2 < dy + sbc #CI_LOCALS + tcs + inc a + tcd ; D = SP + 1: locals, then ciPix - lda dlnErr - clc - adc dlnDy - sta dlnErr - - lda dlnX - clc - adc dlnSx - sta dlnX - -dlnSkipX: -; if (e2 <= dx) { err += dx; Y += sy; } -; Test dx - e2 >= 0 via dx - e2 (BPL means e2 <= dx). - lda dlnDx + lda ciPix sec - sbc dlnE2 - bmi dlnSkipY ; dx < e2 + sbc #1 + sta ciPixM1 + lda ciPix + 2 + sbc #0 + sta ciPixM1 + 2 + lda CI_NIB,s + and #0x000F + sta ciNibLo + asl a + asl a + asl a + asl a + sta ciNibHi + lda CI_CX,s + sta ciCx - lda dlnErr +; x = r, y = 0, err = 1 - r; the four rows at y = 0. + lda CI_R,s + sta ciX + stz ciY + lda #1 + sec + sbc CI_R,s + sta ciErr + lda CI_CY,s + asl a + tax + lda jlgRowOffsetLut,x + sta ciRowYP + sta ciRowYN + lda CI_CY,s clc - adc dlnDx - sta dlnErr + adc CI_R,s + asl a + tax + lda jlgRowOffsetLut,x + sta ciRowXP + lda CI_CY,s + sec + sbc CI_R,s + asl a + tax + lda jlgRowOffsetLut,x + sta ciRowXN - lda dlnY +ciLoop: + CI_PLOT clc, adc, ciX, ciRowYP + CI_PLOT sec, sbc, ciX, ciRowYP + CI_PLOT clc, adc, ciX, ciRowYN + CI_PLOT sec, sbc, ciX, ciRowYN + CI_PLOT clc, adc, ciY, ciRowXP + CI_PLOT sec, sbc, ciY, ciRowXP + CI_PLOT clc, adc, ciY, ciRowXN + CI_PLOT sec, sbc, ciY, ciRowXN + +; y++, its rows a row apart; err <= 0: err += 2y + 1, else x--, its rows a +; row nearer, err += 2(y - x) + 1. Done once x < y. + inc ciY + lda ciRowYP clc - adc dlnSy - sta dlnY + adc #160 + sta ciRowYP + lda ciRowYN + sec + sbc #160 + sta ciRowYN + lda ciErr + bmi ciErrLE + bne ciErrGT +ciErrLE: + lda ciY + asl a + sec ; + 1 + adc ciErr + sta ciErr + bra ciTest +ciErrGT: + dec ciX + lda ciRowXP + sec + sbc #160 + sta ciRowXP + lda ciRowXN + clc + adc #160 + sta ciRowXN + lda ciY + sec + sbc ciX + asl a + sec ; + 1 + adc ciErr + sta ciErr +ciTest: + lda ciX + cmp ciY + bcc ciDone + brl ciLoop -dlnSkipY: - brl dlnLoop - -dlnExit: - pla ; drop the stacked pix far pointer - pla ; (4 bytes -- see the prologue note) +ciDone: + tsc + clc + adc #CI_LOCALS + 4 ; the locals and the pixels pointer + tcs .a8 .i8 pld @@ -4069,445 +4504,8 @@ dlnExit: rtl -.section .bss.dlnDx,"aw" -.globl dlnDx -dlnDx: - .zero 2 -.section .bss.dlnDy,"aw" -.globl dlnDy -dlnDy: - .zero 2 -.section .bss.dlnSx,"aw" -.globl dlnSx -dlnSx: - .zero 2 -.section .bss.dlnSy,"aw" -.globl dlnSy -dlnSy: - .zero 2 -.section .bss.dlnErr,"aw" -.globl dlnErr -dlnErr: - .zero 2 -.section .bss.dlnE2,"aw" -.globl dlnE2 -dlnE2: - .zero 2 -.section .bss.dlnX,"aw" -.globl dlnX -dlnX: - .zero 2 -.section .bss.dlnY,"aw" -.globl dlnY -dlnY: - .zero 2 -.section .bss.dlnX1,"aw" -.globl dlnX1 -dlnX1: - .zero 2 -.section .bss.dlnY1,"aw" -.globl dlnY1 -dlnY1: - .zero 2 -.section .bss.dlnNibLo,"aw" -.globl dlnNibLo -dlnNibLo: - .zero 2 -.section .bss.dlnNibHi,"aw" -.globl dlnNibHi -dlnNibHi: - .zero 2 -.section .bss.dlnTmp,"aw" -.globl dlnTmp -dlnTmp: - .zero 2 - -; ===== ported: circles ===== ; ================================================================ -; iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, -; uint16_t r, uint16_t nibble) -; -; Bresenham midpoint circle outline. Caller has verified that the -; whole bounding box (cx-r..cx+r, cy-r..cy+r) fits inside the -; surface, so the inner loop plots all 8 octants without per-pixel -; clip checks. Each iteration computes 4 row-base byte offsets -; (cy +/- y)*160, (cy +/- x)*160 then plots 8 pixels inline. -; -; ABI (llvm-mos w65816 cdecl): -; pix -> arg0 in A:X (A=offset low16, X low byte=bank). USED via -; [pix],y, so it must live in the direct page. We PUSH it -; (offset@1,s bank@3,s) and point D at the pushed copy, so the -; original `[pix],y` (pix equ 0 -> [D+0],y) loads are -; byte-identical. The stack is bank 0; a bss address is not. -; php/phb/phd do not touch A/X, so A:X still hold pix when -; we capture them after the pushes. -; cx,cy,r,nibble -> on the stack. On JSL entry cx@4,s cy@6,s r@8,s -; nib@10,s. After php+phb+phd+phx+pha (+8): cx@12,s -; cy@14,s r@16,s nib@18,s. S never moves in the body -; (no pushes in the loop), so these N,s reads stay valid. -; They are stack reads because D hosts the pix pointer. -; ================================================================ - .section .text.iigsDrawCircleInner,"ax" - .globl iigsDrawCircleInner -pix = 0 ; D-relative: D points at the stacked far ptr -dcArgCx = 12 ; cx @ 12,s after php+phb+phd+phx+pha -dcArgCy = 14 ; cy @ 14,s -dcArgR = 16 ; r @ 16,s -dcArgNib = 18 ; nibble @ 18,s -iigsDrawCircleInner: - php - phb - phd - rep #0x30 - .a16 - .i16 - -; STACK D-frame. The direct page is ALWAYS bank 0, but dcPixPtr is a BSS -; symbol whose address is an offset in the LOAD bank, so `lda #dcPixPtr; tcd` -; aimed D at bank 0 at that offset -- memory this program does not own. The -; D-relative stores and the [pix],y reads agreed with each other, so it -; looked correct until the link layout moved that offset out of reach. The -; hardware stack IS bank 0, so park the far pointer there. Same fix as the -; tile routines and iigsDrawLineInner. The 4 pushed bytes live for the whole -; call, so the arg offsets above are 4 higher than the JSL-entry figures and -; the exit drops them before PLD. - phx ; pix high16 (bank) -> 3,s - pha ; pix low16 (offset) -> 1,s - tsc - inc a ; A = SP+1 - tcd ; D -> stacked pointer; [pix],y == [0],y - -; Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4. - lda dcArgNib, s - and #0x000F - sta dcNibLo ; low byte = nib, high byte = 0 - asl a - asl a - asl a - asl a - sta dcNibHi ; low byte = nib<<4 - -; x = r, y = 0, err = 1 - x. - lda dcArgR, s - sta dcX - lda #0 - sta dcY - lda #1 - sec - sbc dcArgR, s - sta dcErr - -; Initialize the 4 octant row bases once, then maintain them -; incrementally in the Bresenham update (instead of 4 LUT lookups per -; iteration). At entry x=r, y=0: RowYP=RowYN=acy*160, -; RowXP=(acy+r)*160, RowXN=(acy-r)*160. - lda dcArgCy, s - asl a - tax - lda gRowOffsetLut, x - sta dcRowYP - sta dcRowYN - lda dcArgCy, s - clc - adc dcArgR, s - asl a - tax - lda gRowOffsetLut, x - sta dcRowXP - lda dcArgCy, s - sec - sbc dcArgR, s - asl a - tax - lda gRowOffsetLut, x - sta dcRowXN - -dcLoop: -; Loop guard: if x < y we are done. - lda dcX - cmp dcY - bcs dcLoopBody ; x >= y -> continue - brl dcExit - -dcLoopBody: -; Row bases (dcRowYP/YN/XP/XN) are maintained incrementally in the -; Bresenham update -- no per-iteration LUT lookups here. -; -; 8 octant plots, fully inlined. Each plot: -; 1. col = (acx +/- dcX|dcY) -> A -; 2. stash col in X (free across the plots); the parity test reads it -; back with txa instead of a memory round-trip. -; 3. byteIdx = col >> 1; byte addr = byteIdx + rowBase -> Y -; 4. test col & 1 (via txa); do high or low nibble RMW -; -; All 8 octant plots stay in M=16 -- no per-plot SEP/REP flip. The -; nibble RMW reads the target byte plus its adjacent byte at Y+1 -; (16-bit load; the 65816 is little-endian so byte[Y] is the LOW byte -; of A), masks so only the target nibble changes ($FF0F clears the -; target's high nibble for even x, $FFF0 clears the target's low -; nibble for odd x -- both preserve the whole adjacent byte), then -; writes both bytes back. (The old odd mask $F0FF had its bytes -; swapped and corrupted the neighbor -- claudeDocs/PERF-AUDIT.md #87.) - -; Octants 1-4: y-row pair (cx +/- x, cy +/- y). -; Plot 1: (cx+x, cy+y) - lda dcArgCx, s - clc - adc dcX - tax - lsr a - clc - adc dcRowYP - tay - txa - and #1 - bne dcOdd1 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone1 -dcOdd1: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone1: - -; Plot 2: (cx-x, cy+y) - lda dcArgCx, s - sec - sbc dcX - tax - lsr a - clc - adc dcRowYP - tay - txa - and #1 - bne dcOdd2 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone2 -dcOdd2: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone2: - -; Plot 3: (cx+x, cy-y) - lda dcArgCx, s - clc - adc dcX - tax - lsr a - clc - adc dcRowYN - tay - txa - and #1 - bne dcOdd3 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone3 -dcOdd3: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone3: - -; Plot 4: (cx-x, cy-y) - lda dcArgCx, s - sec - sbc dcX - tax - lsr a - clc - adc dcRowYN - tay - txa - and #1 - bne dcOdd4 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone4 -dcOdd4: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone4: - -; Octants 5-8: x-row pair (cx +/- y, cy +/- x). -; Plot 5: (cx+y, cy+x) - lda dcArgCx, s - clc - adc dcY - tax - lsr a - clc - adc dcRowXP - tay - txa - and #1 - bne dcOdd5 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone5 -dcOdd5: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone5: - -; Plot 6: (cx-y, cy+x) - lda dcArgCx, s - sec - sbc dcY - tax - lsr a - clc - adc dcRowXP - tay - txa - and #1 - bne dcOdd6 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone6 -dcOdd6: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone6: - -; Plot 7: (cx+y, cy-x) - lda dcArgCx, s - clc - adc dcY - tax - lsr a - clc - adc dcRowXN - tay - txa - and #1 - bne dcOdd7 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone7 -dcOdd7: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone7: - -; Plot 8: (cx-y, cy-x) - lda dcArgCx, s - sec - sbc dcY - tax - lsr a - clc - adc dcRowXN - tay - txa - and #1 - bne dcOdd8 - lda [pix], y - and #0xFF0F - ora dcNibHi - sta [pix], y - bra dcDone8 -dcOdd8: - lda [pix], y - and #0xFFF0 - ora dcNibLo - sta [pix], y -dcDone8: - -; Update Bresenham: y++; if err<=0: err+=2y+1; else x--; err+=2(y-x)+1. - lda dcY - inc a - sta dcY ; y now = old y + 1 -; y increased by 1: RowYP moves down a row (+160), RowYN moves up (-160). - lda dcRowYP - clc - adc #160 - sta dcRowYP - lda dcRowYN - sec - sbc #160 - sta dcRowYN - - lda dcErr - bmi dcErrLE ; err < 0 -> err <= 0 - bne dcErrGT ; err > 0 -; err == 0: take the LE branch (err += 2y+1). -dcErrLE: - lda dcY - asl a - clc - adc #1 - clc - adc dcErr - sta dcErr - brl dcLoop -dcErrGT: - lda dcX - dec a - sta dcX -; x decreased by 1: RowXP moves up a row (-160), RowXN moves down (+160). - lda dcRowXP - sec - sbc #160 - sta dcRowXP - lda dcRowXN - clc - adc #160 - sta dcRowXN -; err += 2*(y-x) + 1 - lda dcY - sec - sbc dcX - asl a - clc - adc #1 - clc - adc dcErr - sta dcErr - brl dcLoop - -dcExit: - .a8 - .i8 - pla ; drop the stacked far pointer - pla ; (4 bytes -- see the prologue note) - pld - plb - plp - rtl - - -; ================================================================ -; iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, +; jlIigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, ; uint16_t r, uint16_t nibble) ; ; Filled circle with horizontal-span scanline output. Caller has @@ -4533,14 +4531,14 @@ dcExit: ; scratch globals + [fpix],y, never N,s, so the inner ; jsr-pushed return PC never disturbs an arg read. ; ================================================================ - .section .text.iigsFillCircleInner,"ax" - .globl iigsFillCircleInner + .section .text.jlIigsFillCircleInner,"ax" + .globl jlIigsFillCircleInner fpix = 0 ; D-relative: D points at the stacked far ptr fcArgCx = 12 ; cx @ 12,s after php+phb+phd+phx+pha fcArgCy = 14 ; cy @ 14,s fcArgR = 16 ; r @ 16,s fcArgFill = 18 ; nibble @ 18,s -iigsFillCircleInner: +jlIigsFillCircleInner: php phb phd @@ -4554,7 +4552,7 @@ iigsFillCircleInner: ; D-relative stores and the [fpix],y reads agreed with each other, so it ; looked correct until the link layout moved that offset out of reach. The ; hardware stack IS bank 0, so park the far pointer there. Same fix as the -; tile routines and iigsDrawLineInner. The 4 pushed bytes live for the whole +; tile routines and jlIigsDrawLineInner. The 4 pushed bytes live for the whole ; call, so the arg offsets above are 4 higher than the JSL-entry figures and ; the exit drops them before PLD. phx ; fpix high16 (bank) -> 3,s @@ -4564,106 +4562,106 @@ iigsFillCircleInner: tcd ; D -> stacked pointer; [fpix],y == [0],y ; Derive the fill bytes from the raw nibble arg (#41). -; fcFillByte = doubled byte (full byte fills + low nibble), e.g. $33. -; fcFillHi = (nibble << 4) only (high nibble RMW). -; fcFillLo = nibble only (low nibble RMW). +; jlIigsFcFillByte = doubled byte (full byte fills + low nibble), e.g. $33. +; jlIigsFcFillHi = (nibble << 4) only (high nibble RMW). +; jlIigsFcFillLo = nibble only (low nibble RMW). lda fcArgFill, s ; A = nibble (pre-masked 0x0..0xF) - sta fcFillLo + sta jlIigsFcFillLo asl a asl a asl a asl a ; A = nibble << 4 - sta fcFillHi - ora fcFillLo - sta fcFillByte ; doubled byte e.g. $33 + sta jlIigsFcFillHi + ora jlIigsFcFillLo + sta jlIigsFcFillByte ; doubled byte e.g. $33 ; Compute r*r via 16-bit shift-and-add. Only done once. lda fcArgR, s - sta fcMulA + sta jlIigsFcMulA lda fcArgR, s - sta fcMulB + sta jlIigsFcMulB jsr fcMul16 - sta fcR2 - sta fcXX ; xx = r*r initially + sta jlIigsFcR2 + sta jlIigsFcXX ; xx = r*r initially ; yy = 0, x = r, y = 0. lda #0 - sta fcYY - sta fcY + sta jlIigsFcYY + sta jlIigsFcY lda fcArgR, s - sta fcX + sta jlIigsFcX fcOuterLoop: ; Inner: while (xx + yy) > r2 then xx -= 2x - 1; x--. fcInnerLoop: - lda fcXX + lda jlIigsFcXX clc - adc fcYY - cmp fcR2 + adc jlIigsFcYY + cmp jlIigsFcR2 bcc fcInnerDone ; xx+yy < r2 beq fcInnerDone ; xx+yy == r2 (acceptable) ; xx + yy > r2 -> decrement - lda fcX + lda jlIigsFcX asl a ; A = 2x sec sbc #1 ; A = 2x - 1 - sta fcDec - lda fcXX + sta jlIigsFcDec + lda jlIigsFcXX sec - sbc fcDec - sta fcXX - lda fcX + sbc jlIigsFcDec + sta jlIigsFcXX + lda jlIigsFcX dec a - sta fcX + sta jlIigsFcX bra fcInnerLoop fcInnerDone: ; leftCol = cx - x, rightCol = cx + x for both lower and (if y>0) upper. lda fcArgCx, s sec - sbc fcX - sta fcLeftCol + sbc jlIigsFcX + sta jlIigsFcLeftCol lda fcArgCx, s clc - adc fcX - sta fcRightCol + adc jlIigsFcX + sta jlIigsFcRightCol ; Lower row: rowBase = (cy + y) * 160 lda fcArgCy, s clc - adc fcY + adc jlIigsFcY asl a ; A = y*2 (LUT byte offset) tax - lda gRowOffsetLut, x ; A = y*160 - sta fcSpanRowBase + lda jlgRowOffsetLut, x ; A = y*160 + sta jlIigsFcSpanRowBase jsr fcDoSpan ; If y > 0: upper row at (cy - y). - lda fcY + lda jlIigsFcY beq fcSkipUpper lda fcArgCy, s sec - sbc fcY + sbc jlIigsFcY asl a ; A = y*2 (LUT byte offset) tax - lda gRowOffsetLut, x ; A = y*160 - sta fcSpanRowBase + lda jlgRowOffsetLut, x ; A = y*160 + sta jlIigsFcSpanRowBase jsr fcDoSpan fcSkipUpper: ; yy += 2*y + 1 - lda fcY + lda jlIigsFcY asl a clc adc #1 clc - adc fcYY - sta fcYY + adc jlIigsFcYY + sta jlIigsFcYY ; y++ - lda fcY + lda jlIigsFcY inc a - sta fcY + sta jlIigsFcY ; if y > r: exit, else loop cmp fcArgR, s @@ -4684,135 +4682,135 @@ fcExit: rtl ; ---------------------------------------------------------------- -; fcMul16: A = fcMulA * fcMulB (16-bit unsigned, low 16 of result). +; fcMul16: A = jlIigsFcMulA * jlIigsFcMulB (16-bit unsigned, low 16 of result). ; Standard shift-and-add. 16 iterations. Caller has M=16, X=16. ; ---------------------------------------------------------------- fcMul16: .a16 .i16 lda #0 - sta fcMulRes + sta jlIigsFcMulRes ldx #16 fcMulLoop: ; result <<= 1 - lda fcMulRes + lda jlIigsFcMulRes asl a - sta fcMulRes + sta jlIigsFcMulRes ; multiplier <<= 1, MSB -> C - lda fcMulA + lda jlIigsFcMulA asl a - sta fcMulA + sta jlIigsFcMulA bcc fcMulSkip - lda fcMulRes + lda jlIigsFcMulRes clc - adc fcMulB - sta fcMulRes + adc jlIigsFcMulB + sta jlIigsFcMulRes fcMulSkip: dex bne fcMulLoop - lda fcMulRes + lda jlIigsFcMulRes rts ; ---------------------------------------------------------------- -; fcDoSpan: fill horizontal span from fcLeftCol to fcRightCol on row -; whose byte base is in fcSpanRowBase. Byte-aligned fill for the +; fcDoSpan: fill horizontal span from jlIigsFcLeftCol to jlIigsFcRightCol on row +; whose byte base is in jlIigsFcSpanRowBase. Byte-aligned fill for the ; middle bytes and per-pixel RMW for the partial-nibble leading / ; trailing bytes. ; -; Inputs: fcLeftCol, fcRightCol, fcSpanRowBase (M=16 values). +; Inputs: jlIigsFcLeftCol, jlIigsFcRightCol, jlIigsFcSpanRowBase (M=16 values). ; Trashes A, X, Y, P. Preserves D and B (relies on D = the pushed fpix). ; ---------------------------------------------------------------- fcDoSpan: .a16 .i16 ; leftByte = leftCol >> 1, leftPartial = leftCol & 1 - lda fcLeftCol + lda jlIigsFcLeftCol lsr a - sta fcLeftByte - lda fcLeftCol + sta jlIigsFcLeftByte + lda jlIigsFcLeftCol and #1 - sta fcLeftPartial + sta jlIigsFcLeftPartial ; rightByte = rightCol >> 1, rightPartial = !(rightCol & 1) - lda fcRightCol + lda jlIigsFcRightCol lsr a - sta fcRightByte - lda fcRightCol + sta jlIigsFcRightByte + lda jlIigsFcRightCol and #1 eor #1 - sta fcRightPartial + sta jlIigsFcRightPartial ; If leftByte == rightByte: single-byte case - lda fcLeftByte - cmp fcRightByte + lda jlIigsFcLeftByte + cmp jlIigsFcRightByte bne fcSpanMulti brl fcSpanSingle fcSpanMulti: ; Multi-byte case. ; Leading partial (low nibble of leftByte) if leftPartial. - lda fcLeftPartial + lda jlIigsFcLeftPartial beq fcSkipLP - lda fcSpanRowBase + lda jlIigsFcSpanRowBase clc - adc fcLeftByte + adc jlIigsFcLeftByte tay sep #0x20 .a8 lda [fpix], y and #0xF0 - ora fcFillLo + ora jlIigsFcFillLo sta [fpix], y rep #0x20 .a16 fcSkipLP: ; Trailing partial (high nibble of rightByte) if rightPartial. - lda fcRightPartial + lda jlIigsFcRightPartial beq fcSkipRP - lda fcSpanRowBase + lda jlIigsFcSpanRowBase clc - adc fcRightByte + adc jlIigsFcRightByte tay sep #0x20 .a8 lda [fpix], y and #0x0F - ora fcFillHi + ora jlIigsFcFillHi sta [fpix], y rep #0x20 .a16 fcSkipRP: ; Mid bytes: (leftByte+leftPartial) .. (rightByte-rightPartial) incl. - lda fcLeftByte + lda jlIigsFcLeftByte clc - adc fcLeftPartial - sta fcMidStart - lda fcRightByte + adc jlIigsFcLeftPartial + sta jlIigsFcMidStart + lda jlIigsFcRightByte sec - sbc fcRightPartial - sta fcMidEnd - cmp fcMidStart + sbc jlIigsFcRightPartial + sta jlIigsFcMidEnd + cmp jlIigsFcMidStart bcs fcMidGo rts ; midEnd < midStart fcMidGo: - lda fcMidEnd + lda jlIigsFcMidEnd sec - sbc fcMidStart + sbc jlIigsFcMidStart inc a - sta fcMidCount + sta jlIigsFcMidCount - lda fcSpanRowBase + lda jlIigsFcSpanRowBase clc - adc fcMidStart + adc jlIigsFcMidStart tay ; Y = byte offset - lda fcMidCount + lda jlIigsFcMidCount tax ; X = byte counter sep #0x20 .a8 - lda fcFillByte ; doubled byte + lda jlIigsFcFillByte ; doubled byte fcFillLoop: sta [fpix], y iny @@ -4827,35 +4825,35 @@ fcSpanSingle: ; leftPartial=1 -> low nibble only ; rightPartial=1 -> high nibble only ; neither -> full byte (two adjacent pixels) - lda fcSpanRowBase + lda jlIigsFcSpanRowBase clc - adc fcLeftByte + adc jlIigsFcLeftByte tay sep #0x20 .a8 - lda fcLeftPartial + lda jlIigsFcLeftPartial beq fcSbCheckRP lda [fpix], y and #0xF0 - ora fcFillLo + ora jlIigsFcFillLo sta [fpix], y rep #0x20 .a16 rts fcSbCheckRP: .a8 ; reached via BEQ from M=8 path - lda fcRightPartial + lda jlIigsFcRightPartial beq fcSbFull lda [fpix], y and #0x0F - ora fcFillHi + ora jlIigsFcFillHi sta [fpix], y rep #0x20 .a16 rts fcSbFull: .a8 ; reached via BEQ from M=8 path - lda fcFillByte + lda jlIigsFcFillByte sta [fpix], y rep #0x20 .a16 @@ -4873,168 +4871,121 @@ fcSbFull: ; ================================================================ - .section .bss.dcX,"aw" - .globl dcX -dcX: + .section .bss.jlIigsFcX,"aw" + .globl jlIigsFcX +jlIigsFcX: .zero 2 - .section .bss.dcY,"aw" - .globl dcY -dcY: + .section .bss.jlIigsFcY,"aw" + .globl jlIigsFcY +jlIigsFcY: .zero 2 - .section .bss.dcErr,"aw" - .globl dcErr -dcErr: + .section .bss.jlIigsFcXX,"aw" + .globl jlIigsFcXX +jlIigsFcXX: .zero 2 - .section .bss.dcNibLo,"aw" - .globl dcNibLo -dcNibLo: + .section .bss.jlIigsFcYY,"aw" + .globl jlIigsFcYY +jlIigsFcYY: .zero 2 - .section .bss.dcNibHi,"aw" - .globl dcNibHi -dcNibHi: + .section .bss.jlIigsFcR2,"aw" + .globl jlIigsFcR2 +jlIigsFcR2: .zero 2 - .section .bss.dcRowYP,"aw" - .globl dcRowYP -dcRowYP: + .section .bss.jlIigsFcDec,"aw" + .globl jlIigsFcDec +jlIigsFcDec: .zero 2 - .section .bss.dcRowYN,"aw" - .globl dcRowYN -dcRowYN: + .section .bss.jlIigsFcMulA,"aw" + .globl jlIigsFcMulA +jlIigsFcMulA: .zero 2 - .section .bss.dcRowXP,"aw" - .globl dcRowXP -dcRowXP: + .section .bss.jlIigsFcMulB,"aw" + .globl jlIigsFcMulB +jlIigsFcMulB: .zero 2 - .section .bss.dcRowXN,"aw" - .globl dcRowXN -dcRowXN: + .section .bss.jlIigsFcMulRes,"aw" + .globl jlIigsFcMulRes +jlIigsFcMulRes: .zero 2 - - - .section .bss.fcX,"aw" - .globl fcX -fcX: + .section .bss.jlIigsFcLeftCol,"aw" + .globl jlIigsFcLeftCol +jlIigsFcLeftCol: .zero 2 - .section .bss.fcY,"aw" - .globl fcY -fcY: + .section .bss.jlIigsFcRightCol,"aw" + .globl jlIigsFcRightCol +jlIigsFcRightCol: .zero 2 - .section .bss.fcXX,"aw" - .globl fcXX -fcXX: + .section .bss.jlIigsFcSpanRowBase,"aw" + .globl jlIigsFcSpanRowBase +jlIigsFcSpanRowBase: .zero 2 - .section .bss.fcYY,"aw" - .globl fcYY -fcYY: + .section .bss.jlIigsFcLeftByte,"aw" + .globl jlIigsFcLeftByte +jlIigsFcLeftByte: .zero 2 - .section .bss.fcR2,"aw" - .globl fcR2 -fcR2: + .section .bss.jlIigsFcRightByte,"aw" + .globl jlIigsFcRightByte +jlIigsFcRightByte: .zero 2 - .section .bss.fcDec,"aw" - .globl fcDec -fcDec: + .section .bss.jlIigsFcLeftPartial,"aw" + .globl jlIigsFcLeftPartial +jlIigsFcLeftPartial: .zero 2 - .section .bss.fcMulA,"aw" - .globl fcMulA -fcMulA: + .section .bss.jlIigsFcRightPartial,"aw" + .globl jlIigsFcRightPartial +jlIigsFcRightPartial: .zero 2 - .section .bss.fcMulB,"aw" - .globl fcMulB -fcMulB: + .section .bss.jlIigsFcMidStart,"aw" + .globl jlIigsFcMidStart +jlIigsFcMidStart: .zero 2 - .section .bss.fcMulRes,"aw" - .globl fcMulRes -fcMulRes: + .section .bss.jlIigsFcMidEnd,"aw" + .globl jlIigsFcMidEnd +jlIigsFcMidEnd: .zero 2 - .section .bss.fcLeftCol,"aw" - .globl fcLeftCol -fcLeftCol: + .section .bss.jlIigsFcMidCount,"aw" + .globl jlIigsFcMidCount +jlIigsFcMidCount: .zero 2 - .section .bss.fcRightCol,"aw" - .globl fcRightCol -fcRightCol: + .section .bss.jlIigsFcFillByte,"aw" + .globl jlIigsFcFillByte +jlIigsFcFillByte: .zero 2 - .section .bss.fcSpanRowBase,"aw" - .globl fcSpanRowBase -fcSpanRowBase: + .section .bss.jlIigsFcFillLo,"aw" + .globl jlIigsFcFillLo +jlIigsFcFillLo: .zero 2 - .section .bss.fcLeftByte,"aw" - .globl fcLeftByte -fcLeftByte: - .zero 2 - - .section .bss.fcRightByte,"aw" - .globl fcRightByte -fcRightByte: - .zero 2 - - .section .bss.fcLeftPartial,"aw" - .globl fcLeftPartial -fcLeftPartial: - .zero 2 - - .section .bss.fcRightPartial,"aw" - .globl fcRightPartial -fcRightPartial: - .zero 2 - - .section .bss.fcMidStart,"aw" - .globl fcMidStart -fcMidStart: - .zero 2 - - .section .bss.fcMidEnd,"aw" - .globl fcMidEnd -fcMidEnd: - .zero 2 - - .section .bss.fcMidCount,"aw" - .globl fcMidCount -fcMidCount: - .zero 2 - - .section .bss.fcFillByte,"aw" - .globl fcFillByte -fcFillByte: - .zero 2 - - .section .bss.fcFillLo,"aw" - .globl fcFillLo -fcFillLo: - .zero 2 - - .section .bss.fcFillHi,"aw" - .globl fcFillHi -fcFillHi: + .section .bss.jlIigsFcFillHi,"aw" + .globl jlIigsFcFillHi +jlIigsFcFillHi: .zero 2 ; ===== ported: blit / dirty / input / flood ===== ; ==================================================================== ; frag_blitflood.s - llvm-mc GAS (w65816): -; iigsBlitStageToShr, iigsMarkDirtyRowsInner, iigsPollJoystickInner, -; iigsInputSnapshot, iigsFloodWalkAndScansInner +; jlIigsBlitStageToShr, jlIigsMarkDirtyRowsInner, jlIigsPollJoystickInner, +; jlIigsInputSnapshot, jlIigsFloodWalkAndScansInner ; ; Shared ABI facts (clang w65816 cdecl, per PORT_SPEC.md): ; - arg0 (pointer/long) arrives in A=low16, X=high16; word arg0 in A. @@ -5050,7 +5001,7 @@ fcFillHi: ; ==================================================================== -; iigsBlitStageToShr(void) +; jlIigsBlitStageToShr(void) ; ; SHR pixel upload: a per-row dirty-skipped, chunked-SEI PEI slam of ; the 32000-byte pixel buffer from the bank-$01 back buffer to the $E1 @@ -5068,9 +5019,9 @@ fcFillHi: ; bases; the final PLD restores the caller's D (saved by our PHD) and ; PLB restores DBR (the narrow-row MVNs leave DBR=$E1). ; ==================================================================== -.section .text.iigsBlitStageToShr,"ax" -.globl iigsBlitStageToShr -iigsBlitStageToShr: +.section .text.jlIigsBlitStageToShr,"ax" +.globl jlIigsBlitStageToShr +jlIigsBlitStageToShr: ; Slam-run eligibility floor (NATIVE-PERF W2): a dirty row narrower ; than PEI_RUN_MIN words always takes the exact-extent MVN. With the ; run hoist in place (one softswitch set per RUN of consecutive @@ -5083,7 +5034,7 @@ iigsBlitStageToShr: ; PLAN.md W2). The old rule both UNDER-promoted left-anchored ; mid-width bands and OVER-promoted right-anchored ones. PEI_RUN_MIN = 8 -; Interrupt-latency bound: gIigsPresentChunkRows (hal.c) rows are consumed +; Interrupt-latency bound: jlgIigsPresentChunkRows (hal.c) rows are consumed ; per SEI chunk, ~0.42 ms a full-width row. Loose (40, ~17 ms) is free; ; tight (4, ~1.8 ms) fits the Z8530 SCC's 3-byte receive FIFO at 9600 baud ; (~3.1 ms; MEASURED zero loss under MAME, whose FIFO keeps only 2, where @@ -5117,7 +5068,7 @@ PEI_RUN_MIN = 8 ; Pixel blit via PEI-slam, with per-row dirty skip and chunked SEI. tsc - sta gPeiOrigSp + sta jlgPeiOrigSp sep #0x20 .a8 ; Soft switches are accessed via LONG addressing to bank $E0 @@ -5128,14 +5079,14 @@ PEI_RUN_MIN = 8 ; holds the real I/O. (Technique from the prior joeylib's ; asmSlam: >$E0C035 for shadow, >$E0C0xx for RAMRD/RAMWRT.) lda 0xE0C035 - sta gPeiOrigShadow + sta jlgPeiOrigShadow rep #0x20 .a16 ldx #0 ; X = absolute row counter ; --------------------------------------------------------------- -; Chunked-SEI scan. Contract: sei per chunk, <= gIigsPresentChunkRows rows +; Chunked-SEI scan. Contract: sei per chunk, <= jlgIigsPresentChunkRows rows ; consumed per chunk, cli at chunk end so the latched VBL IRQ catches ; up (this is what keeps the GetTick bench clock honest). Runs are ; clipped at the chunk budget, so no SEI hold got longer -- every row @@ -5152,34 +5103,34 @@ peiChunkBegin: .a8 peiRowLoop: ; TOUCHED test (surfaceInternal.h STAGE_DIRTY_ROW_TOUCHED): a row is -; CLEAN iff gStageMinWord[y] == 0xFF. Valid under the clipped-marks +; CLEAN iff jlgStageMinWord[y] == 0xFF. Valid under the clipped-marks ; invariant -- every real mark writes min <= 79 (STAGE_DIRTY_FULL_MAX), -; and stageDirtyClearAll is the only writer of 0xFF. Sentinel pin +; and jlStageDirtyClearAll is the only writer of 0xFF. Sentinel pin ; (finding #47) updated: this walk now relies on min==0xFF <=> clean; ; do not change STAGE_DIRTY_CLEAN_MIN without updating this asm. - lda gStageMinWord,x + lda jlgStageMinWord,x inc a beq peiRowClean ; 0xFF -> 0x00 -> clean ; dirty: A = W-1 = max - min - lda gStageMaxWord,x + lda jlgStageMaxWord,x sec - sbc gStageMinWord,x + sbc jlgStageMinWord,x cmp #PEI_RUN_MIN - 1 bcc peiMvnRowBr ; W < PEI_RUN_MIN -> MVN ; slam-eligibility: (max+1) <= W + W/2 inc a ; A = W - sta gPeiWidthTmp + sta jlgPeiWidthTmp lsr a clc - adc gPeiWidthTmp ; A = W + W/2 (<= 120, fits) - cmp gStageMaxWord,x + adc jlgPeiWidthTmp ; A = W + W/2 (<= 120, fits) + cmp jlgStageMaxWord,x beq peiMvnRowBr ; 1.5W == max -> max+1 > 1.5W bcs peiRunScan ; 1.5W > max -> slam run peiMvnRowBr: brl peiMvnRow peiRowClean: ; The row is clean. Before stepping one row, consult the coarse group -; flag (surfaceInternal.h gStageDirtyGroup): a clean 8-row group lets +; flag (surfaceInternal.h jlgStageDirtyGroup): a clean 8-row group lets ; the walk jump straight to the next group boundary instead of testing ; seven more bands one at a time. Measured ~37 dirty rows per present, ; so most of the 200 row visits were pure scan. The flag is only ever a @@ -5206,7 +5157,7 @@ peiRowClean: tax sep #0x20 .a8 - lda gStageDirtyGroup,x + lda jlgStageDirtyGroup,x plx ; row back in X (PLX sets flags on X) cmp #0 beq peiRowSkipGroup @@ -5218,23 +5169,23 @@ peiRowSkipGroup: rep #0x20 .a16 txa - sta gPeiSkipTmp ; old row + sta jlgPeiSkipTmp ; old row ora #7 inc a ; A = next group boundary tax sec - sbc gPeiSkipTmp ; A = rows skipped - sta gPeiSkipTmp + sbc jlgPeiSkipTmp ; A = rows skipped + sta jlgPeiSkipTmp tya clc - adc gPeiSkipTmp + adc jlgPeiSkipTmp tay ; the chunk budget counts them too sep #0x20 .a8 peiRowBounds: cpx #200 bcs peiChunkDone - cpy gIigsPresentChunkRows + cpy jlgIigsPresentChunkRows bcc peiRowLoop peiChunkDone: ; Per-chunk teardown is now JUST the CLI: runs restore SP and switches @@ -5257,11 +5208,11 @@ peiRunScan: ; a8; X = first run row rep #0x20 .a16 txa - sta gPeiRunFirst + sta jlgPeiRunFirst sep #0x20 .a8 - lda gStageMaxWord,x - sta gPeiRunMax ; low byte only (readers mask); + lda jlgStageMaxWord,x + sta jlgPeiRunMax ; low byte only (readers mask); ; never store 16-bit here -- the ; high byte is BSS-zero by ; construction @@ -5270,60 +5221,60 @@ peiRunGather: iny cpx #200 bcs peiRunGathered - cpy gIigsPresentChunkRows + cpy jlgIigsPresentChunkRows bcs peiRunGathered ; chunk budget clips the run - lda gStageMinWord,x + lda jlgStageMinWord,x inc a beq peiRunGathered ; clean row ends the run - lda gStageMaxWord,x + lda jlgStageMaxWord,x sec - sbc gStageMinWord,x + sbc jlgStageMinWord,x cmp #PEI_RUN_MIN - 1 bcc peiRunGathered ; narrow row ends the run inc a - sta gPeiWidthTmp + sta jlgPeiWidthTmp lsr a clc - adc gPeiWidthTmp - cmp gStageMaxWord,x + adc jlgPeiWidthTmp + cmp jlgStageMaxWord,x beq peiRunGathered ; fails (max+1) <= 1.5W bcc peiRunGathered - lda gStageMaxWord,x - cmp gPeiRunMax + lda jlgStageMaxWord,x + cmp jlgPeiRunMax bcc peiRunGather - sta gPeiRunMax ; widen the run's entry word + sta jlgPeiRunMax ; widen the run's entry word bra peiRunGather peiRunGathered: ; X/Y = scan resume values rep #0x30 .a16 .i16 txa - sta gPeiCurRow ; resume: absolute row + sta jlgPeiCurRow ; resume: absolute row tya - sta gPeiChunkRow ; resume: in-chunk row + sta jlgPeiChunkRow ; resume: in-chunk row txa sec - sbc gPeiRunFirst - sta gPeiRunLen ; rows in this run (1..gIigsPresentChunkRows) + sbc jlgPeiRunFirst + sta jlgPeiRunLen ; rows in this run (1..jlgIigsPresentChunkRows) ; STAGE B: computed entry. spOffset = 2*runMax + 1, parked ; at $E1:9DFE (the ONLY datum the switches-on loop loads); ; the burst enters the PEI table at word runMax, pushing ; words runMax..0 instead of always 79..0. Words left of ; the run's min are clean (stage == display there), so the ; over-push of them writes identical bytes. - lda gPeiRunMax + lda jlgPeiRunMax and #0x00FF asl a inc a ; A = 2*runMax + 1 sta 0xE19DFE inc a - sta gPeiRunBytes ; telemetry: bytes pushed/row + sta jlgPeiRunBytes ; telemetry: bytes pushed/row ; D = base of the run's LAST (highest, slammed first) row - lda gPeiCurRow + lda jlgPeiCurRow dec a asl a tax - lda gRowOffsetLut,x + lda jlgRowOffsetLut,x clc adc #0x2000 tcd ; D = last row base (A kept) @@ -5333,13 +5284,13 @@ peiRunGathered: ; X/Y = scan resume values ; X = PEI table entry byte offset = 158 - 2*runMax lda #160 sec - sbc gPeiRunBytes + sbc jlgPeiRunBytes tax - ldy gPeiRunLen ; Y = row countdown + ldy jlgPeiRunLen ; Y = row countdown ; toggles ON -- LAST bank-0 access above this line sep #0x20 .a8 - lda gPeiOrigShadow + lda jlgPeiOrigShadow and #0xF7 ; clear bit 3 -> SHR shadow ON. SHR ; shadowing mirrors $2000-9FFF for ; BOTH banks; the burst pushes into @@ -5469,33 +5420,40 @@ peiRunDone: lda #0 sta 0xE0C004 ; RAMWRT off sta 0xE0C002 ; RAMRD off - lda gPeiOrigShadow + lda jlgPeiOrigShadow sta 0xE0C035 ; shadow restored rep #0x30 .a16 .i16 - lda gPeiOrigSp + lda jlgPeiOrigSp tcs ; caller stack back - ; telemetry (W2 Stage 0): gPresentCopiedBytes += - ; runLen * bytesPerRow. Repeated add (runLen <= gIigsPresentChunkRows), - ; ~13 cyc per slammed row -- accepted for verifiability. - ; DBR is the caller's globals bank (slam never touches - ; DBR), so the absolute counter access is safe. - ldx gPeiRunLen - lda gPresentCopiedBytes + ; telemetry (W2 Stage 0), only while jlgPresentTelemetry + ; is set: jlgPresentCopiedBytes += runLen * bytesPerRow. + ; Repeated add (runLen <= jlgIigsPresentChunkRows), ~13 cyc + ; per slammed row. DBR is the caller's globals bank (slam + ; never touches DBR), so the absolute counter access is safe. + sep #0x20 + .a8 + lda jlgPresentTelemetry + rep #0x20 + .a16 + beq peiRunCntDone + ldx jlgPeiRunLen + lda jlgPresentCopiedBytes peiRunCntLoop: clc - adc gPeiRunBytes + adc jlgPeiRunBytes bcc peiRunCntNext - inc gPresentCopiedBytes+2 + inc jlgPresentCopiedBytes+2 peiRunCntNext: dex bne peiRunCntLoop - sta gPresentCopiedBytes + sta jlgPresentCopiedBytes +peiRunCntDone: ; resume the scan - lda gPeiCurRow + lda jlgPeiCurRow tax - lda gPeiChunkRow + lda jlgPeiChunkRow tay sep #0x20 .a8 @@ -5521,16 +5479,16 @@ peiMvnRow: ; a8; X = row, Y = chunk count ; inner loop is then one MVN plus a stride add, and it never touches ; a DBR-relative global (MVN leaves DBR = $E1, so the loop's three ; live values are read STACK-relative, which ignores DBR). - lda gStageMinWord,x - sta gPeiBandMin - lda gStageMaxWord,x - sta gPeiBandMax + lda jlgStageMinWord,x + sta jlgPeiBandMin + lda jlgStageMaxWord,x + sta jlgPeiBandMax rep #0x20 .a16 txa - sta gPeiCurRow ; first row of the run + sta jlgPeiCurRow ; first row of the run tya - sta gPeiChunkRow + sta jlgPeiChunkRow sep #0x20 .a8 peiMvnGather: @@ -5541,13 +5499,13 @@ peiMvnGather: iny cpx #200 bcs peiMvnRunGo - cpy gIigsPresentChunkRows + cpy jlgIigsPresentChunkRows bcs peiMvnRunGo - lda gStageMinWord,x - cmp gPeiBandMin + lda jlgStageMinWord,x + cmp jlgPeiBandMin bne peiMvnRunGo - lda gStageMaxWord,x - cmp gPeiBandMax + lda jlgStageMaxWord,x + cmp jlgPeiBandMax bne peiMvnRunGo bra peiMvnGather @@ -5556,70 +5514,70 @@ peiMvnRunGo: rep #0x20 .a16 txa - sta gPeiResumeRow + sta jlgPeiResumeRow tya - sta gPeiResumeChunk + sta jlgPeiResumeChunk sec - sbc gPeiChunkRow - sta gPeiRunRows ; rows = resumeChunk - chunkRow + sbc jlgPeiChunkRow + sta jlgPeiRunRows ; rows = resumeChunk - chunkRow ; count = (max - min) * 2 + 1 (the MVN arg is bytes - 1) sep #0x20 .a8 - lda gPeiBandMax + lda jlgPeiBandMax sec - sbc gPeiBandMin + sbc jlgPeiBandMin rep #0x20 .a16 and #0x00FF asl a inc a - sta gPeiMvnCount + sta jlgPeiMvnCount ; stride to the next row = 160 - bytes = 159 - count lda #159 sec - sbc gPeiMvnCount - sta gPeiRunStride - ; first offset = min*2 + gRowOffsetLut[firstRow] + $2000 - lda gPeiCurRow + sbc jlgPeiMvnCount + sta jlgPeiRunStride + ; first offset = min*2 + jlgRowOffsetLut[firstRow] + $2000 + lda jlgPeiCurRow asl a tay sep #0x20 .a8 - lda gPeiBandMin + lda jlgPeiBandMin rep #0x20 .a16 and #0x00FF asl a clc - adc gRowOffsetLut,y + adc jlgRowOffsetLut,y adc #0x2000 ; no carry possible: sum < $9D00 - sta gPeiRunOff + sta jlgPeiRunOff ; end = first + rows*160 (160 == 128 + 32) - lda gPeiRunRows + lda jlgPeiRunRows asl a asl a asl a asl a asl a - sta gPeiMulA ; rows * 32 + sta jlgPeiMulA ; rows * 32 asl a asl a ; rows * 128 clc - adc gPeiMulA + adc jlgPeiMulA clc - adc gPeiRunOff - sta gPeiRunEnd + adc jlgPeiRunOff + sta jlgPeiRunEnd ; Park count / stride / end on the STACK: MVN leaves DBR = $E1, so an ; absolute read inside the loop would land in the wrong bank. Stack- ; relative addressing ignores DBR. NOTE the phb copy moves to 9,s while ; these three are pushed -- the DBR restore below happens after the pulls. - ldx gPeiRunOff + ldx jlgPeiRunOff txy - lda gPeiRunEnd + lda jlgPeiRunEnd pha ; -> 5,s - lda gPeiRunStride + lda jlgPeiRunStride pha ; -> 3,s - lda gPeiMvnCount + lda jlgPeiMvnCount pha ; -> 1,s peiMvnRunLoop: lda 1,s ; count @@ -5646,37 +5604,44 @@ peiMvnRunLoop: plb rep #0x20 .a16 -; telemetry (W2 Stage 0): the run moved bytes * rows. Shift-add, once -; per run rather than a 32-bit add per row. - lda gPeiMvnCount +; telemetry (W2 Stage 0), only while jlgPresentTelemetry is set: the run +; moved bytes * rows. Shift-add, once per run rather than a 32-bit add per +; row. + sep #0x20 + .a8 + lda jlgPresentTelemetry + rep #0x20 + .a16 + beq peiMvnCntDone + lda jlgPeiMvnCount inc a - sta gPeiMulA ; multiplicand = bytes + sta jlgPeiMulA ; multiplicand = bytes lda #0 - sta gPeiMulOut - lda gPeiRunRows - sta gPeiMulB + sta jlgPeiMulOut + lda jlgPeiRunRows + sta jlgPeiMulB peiMulLoop: - lsr gPeiMulB + lsr jlgPeiMulB bcc peiMulSkip - lda gPeiMulOut + lda jlgPeiMulOut clc - adc gPeiMulA - sta gPeiMulOut + adc jlgPeiMulA + sta jlgPeiMulOut peiMulSkip: - asl gPeiMulA - lda gPeiMulB + asl jlgPeiMulA + lda jlgPeiMulB bne peiMulLoop - lda gPresentCopiedBytes + lda jlgPresentCopiedBytes clc - adc gPeiMulOut - sta gPresentCopiedBytes + adc jlgPeiMulOut + sta jlgPresentCopiedBytes bcc peiMvnCntDone - inc gPresentCopiedBytes+2 + inc jlgPresentCopiedBytes+2 peiMvnCntDone: ; Resume the scan one row past the run (X/Y were advanced by the gather). - lda gPeiResumeRow + lda jlgPeiResumeRow tax - lda gPeiResumeChunk + lda jlgPeiResumeChunk tay sep #0x20 .a8 @@ -5688,14 +5653,21 @@ peiAllDone: rep #0x30 .a16 .i16 - ; telemetry (W2 Stage 0): every present examines all 200 - ; band rows exactly once (no early-out exists on IIgs). - lda gPresentScannedRows + ; telemetry (W2 Stage 0), only while jlgPresentTelemetry + ; is set: every present examines all 200 band rows exactly + ; once (no early-out exists on IIgs). + sep #0x20 + .a8 + lda jlgPresentTelemetry + rep #0x20 + .a16 + beq peiScanCntDone + lda jlgPresentScannedRows clc adc #200 - sta gPresentScannedRows + sta jlgPresentScannedRows bcc peiScanCntDone - inc gPresentScannedRows+2 + inc jlgPresentScannedRows+2 peiScanCntDone: pld plb @@ -5704,11 +5676,11 @@ peiScanCntDone: ; ==================================================================== -; iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, +; jlIigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, ; uint16_t minWord, uint16_t maxWord) ; -; For each row in [yStart, yEnd) widen gStageMinWord[row] DOWN to -; minWord (if smaller) and gStageMaxWord[row] UP to maxWord (if larger). +; For each row in [yStart, yEnd) widen jlgStageMinWord[row] DOWN to +; minWord (if smaller) and jlgStageMaxWord[row] UP to maxWord (if larger). ; Caller clips yStart/yEnd to [0,200); no internal bounds check. ; ; Original args (D-frame, D=SP+8, all on stack): @@ -5725,9 +5697,9 @@ peiScanCntDone: ; D+0..1 would overlap the return address, but mdrYStart is never ; touched in the frame, so that is harmless. ; ==================================================================== -.section .text.iigsMarkDirtyRowsInner,"ax" -.globl iigsMarkDirtyRowsInner -iigsMarkDirtyRowsInner: +.section .text.jlIigsMarkDirtyRowsInner,"ax" +.globl jlIigsMarkDirtyRowsInner +jlIigsMarkDirtyRowsInner: mdrYEnd = 2 mdrMinWord = 4 mdrMaxWord = 6 @@ -5753,21 +5725,21 @@ mdrMaxWord = 6 lsr a lsr a lsr a - sta gStageGrpTmp ; first group + sta jlgStageGrpTmp ; first group lda mdrYEnd dec a lsr a lsr a lsr a ; last group sec - sbc gStageGrpTmp + sbc jlgStageGrpTmp tay ; Y = group count - 1 - ldx gStageGrpTmp + ldx jlgStageGrpTmp sep #0x20 .a8 lda #1 mdrGrpLoop: - sta gStageDirtyGroup,x + sta jlgStageDirtyGroup,x inx dey bpl mdrGrpLoop @@ -5780,15 +5752,15 @@ mdrGrpLoop: mdrLoop: lda mdrMinWord - cmp gStageMinWord,x + cmp jlgStageMinWord,x bcs mdrNoMin ; A >= existing -> already <= minWord - sta gStageMinWord,x ; widen down + sta jlgStageMinWord,x ; widen down mdrNoMin: lda mdrMaxWord - cmp gStageMaxWord,x + cmp jlgStageMaxWord,x bcc mdrNoMax ; A < existing -> already >= maxWord beq mdrNoMax - sta gStageMaxWord,x ; widen up + sta jlgStageMaxWord,x ; widen up mdrNoMax: inx cpx mdrYEnd @@ -5807,21 +5779,21 @@ mdrExit: ; ==================================================================== -; iigsStageBandsClearAll(void) +; jlIigsStageBandsClearAll(void) ; -; Reset the dirty bands (gStageMinWord/gStageMaxWord, 200 bytes each) to +; Reset the dirty bands (jlgStageMinWord/jlgStageMaxWord, 200 bytes each) to ; the CLEAN sentinels after a present -- min = $FF (STAGE_DIRTY_CLEAN_MIN), -; max = $00 (CLEAN_MAX) -- for only the 8-row groups gStageDirtyGroup says +; max = $00 (CLEAN_MAX) -- for only the 8-row groups jlgStageDirtyGroup says ; were marked, clearing those group bytes too. (It began as a blind fill ; when libc memset was a far-call byte loop; libc's memset is a tight ; 16-bit fill now, but no fill can skip the clean groups.) ; ; ABI: no args, no return. Same DBR-inherited abs,x/abs,y write path as -; iigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT +; jlIigsMarkDirtyRowsInner (caller's DBR = the C data bank; do NOT ; phk/plb). php/plp preserves caller widths; A/X/Y are cdecl scratch. ; ==================================================================== .section .text.iigsStageBands,"ax" -.globl iigsStageBandsClearAll +.globl jlIigsStageBandsClearAll ; GROUP-SKIPPED 2026-09-25. The blind version rewrote all 400 band bytes plus ; the 25 group bytes after EVERY present -- 200 word stores at ~12 cyc plus the ; C group loop, about 1.14 ms of the 2.20 ms an IDLE present cost, spent @@ -5838,67 +5810,67 @@ mdrExit: ; Twelve group PAIRS are walked as 16-bit reads (groups 0..23 = rows 0..191); ; group 24 (rows 192..199) is tested on its own, because a 13th pair read would ; run one byte past the 25-byte group array and clear band bytes past row 199. -iigsStageBandsClearAll: +jlIigsStageBandsClearAll: php rep #0x30 .a16 .i16 ldx #22 isbcPair: - lda gStageDirtyGroup,x + lda jlgStageDirtyGroup,x beq isbcPairNext ; 16 rows untouched: already CLEAN - stz gStageDirtyGroup,x + stz jlgStageDirtyGroup,x txa asl ; group byte offset * 8 = first row asl asl tay lda #0xFFFF ; CLEAN_MIN in both bytes - sta gStageMinWord+0,y - sta gStageMinWord+2,y - sta gStageMinWord+4,y - sta gStageMinWord+6,y - sta gStageMinWord+8,y - sta gStageMinWord+10,y - sta gStageMinWord+12,y - sta gStageMinWord+14,y + sta jlgStageMinWord+0,y + sta jlgStageMinWord+2,y + sta jlgStageMinWord+4,y + sta jlgStageMinWord+6,y + sta jlgStageMinWord+8,y + sta jlgStageMinWord+10,y + sta jlgStageMinWord+12,y + sta jlgStageMinWord+14,y lda #0 ; CLEAN_MAX in both bytes - sta gStageMaxWord+0,y - sta gStageMaxWord+2,y - sta gStageMaxWord+4,y - sta gStageMaxWord+6,y - sta gStageMaxWord+8,y - sta gStageMaxWord+10,y - sta gStageMaxWord+12,y - sta gStageMaxWord+14,y + sta jlgStageMaxWord+0,y + sta jlgStageMaxWord+2,y + sta jlgStageMaxWord+4,y + sta jlgStageMaxWord+6,y + sta jlgStageMaxWord+8,y + sta jlgStageMaxWord+10,y + sta jlgStageMaxWord+12,y + sta jlgStageMaxWord+14,y isbcPairNext: dex dex bpl isbcPair sep #0x20 .a8 - lda gStageDirtyGroup+24 + lda jlgStageDirtyGroup+24 beq isbcDone - stz gStageDirtyGroup+24 + stz jlgStageDirtyGroup+24 rep #0x20 .a16 lda #0xFFFF - sta gStageMinWord+192 - sta gStageMinWord+194 - sta gStageMinWord+196 - sta gStageMinWord+198 + sta jlgStageMinWord+192 + sta jlgStageMinWord+194 + sta jlgStageMinWord+196 + sta jlgStageMinWord+198 lda #0 - sta gStageMaxWord+192 - sta gStageMaxWord+194 - sta gStageMaxWord+196 - sta gStageMaxWord+198 + sta jlgStageMaxWord+192 + sta jlgStageMaxWord+194 + sta jlgStageMaxWord+196 + sta jlgStageMaxWord+198 isbcDone: plp rtl ; ==================================================================== -; iigsCopyRectRows(uint8_t *dst, uint16_t srcLo, uint16_t srcBank, +; jlIigsCopyRectRows(uint8_t *dst, uint16_t srcLo, uint16_t srcBank, ; uint16_t rowBytes, uint16_t rows) ; ; Copy `rows` rows of `rowBytes` bytes from src to dst, both with the @@ -5921,8 +5893,8 @@ icrSrc = 0 icrDst = 4 icrRowBytes = 19 icrRows = 21 -.globl iigsCopyRectRows -iigsCopyRectRows: +.globl jlIigsCopyRectRows +jlIigsCopyRectRows: php phb phd @@ -5987,7 +5959,7 @@ icrWordLoop: ; ==================================================================== -; iigsPollJoystickInner(void) +; jlIigsPollJoystickInner(void) ; ; Reads the IIgs paddle ports (PDL0/PDL1) at 1 MHz. No args; returns the ; packed result in registers (see ABI below) -- no output globals. @@ -6011,9 +5983,9 @@ icrWordLoop: ; (with timeout) up front avoids a mid-discharge trigger and avoids ; hanging when no stick is attached. ; ==================================================================== -.section .text.iigsPollJoystickInner,"ax" -.globl iigsPollJoystickInner -iigsPollJoystickInner: +.section .text.jlIigsPollJoystickInner,"ax" +.globl jlIigsPollJoystickInner +jlIigsPollJoystickInner: php sep #0x34 ; sei + 8-bit M/X .a8 @@ -6094,7 +6066,7 @@ gjGotXY: ; REGISTERS as the C ABI's uint32_t (A = low16, X = high16) -- NOT via ; globals: an asm WRITE of a global does not reach the C-read address in ; this toolchain (DBR/bank mismatch), whereas register return is proven -; (same path as iigsGetTickWord / iigsReadHzParam). Packing: +; (same path as jlIigsGetTickWord / jlIigsReadHzParam). Packing: ; A (low16) = (px << 8) | resolved X (high16) = py ; -> C sees: resolved = r&0xFF, px = (r>>8)&0xFF, py = (r>>16)&0xFF. pld ; restore caller DP @@ -6122,7 +6094,7 @@ gjNoJoy: ; ==================================================================== -; iigsJoyPass(uint16_t limitAndMode, uint16_t highStart) +; jlIigsJoyPass(uint16_t limitAndMode, uint16_t highStart) ; ; One pass of the tight-bound paddle read (input.c pollJoystickTight), ; used while a client holds hal.c's tight interrupt-latency bound. The @@ -6140,13 +6112,13 @@ gjNoJoy: ; time out, leaving the other OPEN. ; highStart != 0 HIGH pass: trigger, let interrupts run for ; highStart counts' worth of beam ticks, then count. -; gIigsJoyStart says how many of the classic read's +; jlgIigsJoyStart says how many of the classic read's ; samples came before the pass's first, rounded; an ; axis that timed out before that sample counts 0, as ; in the classic loop. ; ; Beam ticks: VERTCNT/HORIZCNT as line * 65 + position (0..64), i.e. -; 1 MHz cycles on any IIgs (65 to a line), wrapped at gIigsBeamFrameTicks +; 1 MHz cycles on any IIgs (65 to a line), wrapped at jlgIigsBeamFrameTicks ; (hal.c). A classic count is 11 of them, and so is one here. ; ; ABI: limitAndMode in A: the limit in counts in the low byte (a multiple @@ -6193,9 +6165,9 @@ JOY_SETTLE_TRIES = 0x0800 bne 1b .endm -.section .text.iigsJoyPass,"ax" -.globl iigsJoyPass -iigsJoyPass: +.section .text.jlIigsJoyPass,"ax" +.globl jlIigsJoyPass +jlIigsJoyPass: php rep #0x30 .a16 @@ -6392,16 +6364,16 @@ jpDone: jsr joyElapsed clc adc #JOY_START_TICKS + JOY_TICKS_PER_COUNT / 2 - sta gIigsJoyStart ; dividend, becomes the quotient + sta jlgIigsJoyStart ; dividend, becomes the quotient lda #0 ; remainder ldx #16 jpDivide: - asl gIigsJoyStart + asl jlgIigsJoyStart rol a cmp #JOY_TICKS_PER_COUNT bcc jpDivideNext sbc #JOY_TICKS_PER_COUNT - inc gIigsJoyStart + inc jlgIigsJoyStart jpDivideNext: dex bne jpDivide @@ -6474,11 +6446,11 @@ joyElapsed: sbc joyLinTrig bcs 1f clc - adc gIigsBeamFrameTicks ; the beam wrapped into the next frame + adc jlgIigsBeamFrameTicks ; the beam wrapped into the next frame 1: rts -; iigsJoyPass scratch (asm-only data) and the high pass's start output, +; jlIigsJoyPass scratch (asm-only data) and the high pass's start output, ; read by input.c. .section .bss.joyX0,"aw" joyX0: @@ -6516,68 +6488,164 @@ joyLinRaw: .section .bss.joyLinPos,"aw" joyLinPos: .zero 2 - .section .bss.gIigsJoyStart,"aw" - .globl gIigsJoyStart -gIigsJoyStart: + .section .bss.jlgIigsJoyStart,"aw" + .globl jlgIigsJoyStart +jlgIigsJoyStart: .zero 2 ; ==================================================================== -; iigsInputSnapshot(void) +; jlIigsInputSnapshot(void) ; -; Snapshot gKeyState->gKeyPrev (JL_KEY_COUNT=60 bytes) plus -; gMouseButtonState/gJoyButtonState (4 bytes each). No args. +; Snapshot jlgKeyState->jlgKeyPrev (JL_KEY_COUNT=60 bytes) plus +; jlgMouseButtonState/jlgJoyButtonState (4 bytes each). No args. +; The key state is copied only when jlgIigsKeysMoved says it may differ +; from jlgKeyPrev (src/iigs/input.c): an idle keyboard leaves both all +; zero. The copies are words, unrolled: 30 key words and 2 of each +; button set (input.c checks the sizes). ; -; ABI: void; no register/stack args. DBR stays 0; all globals via -; 16-bit abs. +; ABI: void; no register/stack args. DBR is the caller's data bank; +; all globals via 16-bit abs. ; ==================================================================== -.section .text.iigsInputSnapshot,"ax" -.globl iigsInputSnapshot -iigsInputSnapshot: +.section .text.jlIigsInputSnapshot,"ax" +.globl jlIigsInputSnapshot +jlIigsInputSnapshot: php - rep #0x30 - .a16 - .i16 sep #0x20 .a8 - -; Snapshot gKeyState -> gKeyPrev (60 bytes), long-mode loop. - ldx #59 -isnKeyLoop: - lda gKeyState,x - sta gKeyPrev,x - dex - bpl isnKeyLoop - -; Snapshot gMouseButtonState -> gMouseButtonPrev (4 bytes inline). - lda gMouseButtonState - sta gMouseButtonPrev - lda gMouseButtonState+1 - sta gMouseButtonPrev+1 - lda gMouseButtonState+2 - sta gMouseButtonPrev+2 - lda gMouseButtonState+3 - sta gMouseButtonPrev+3 - -; Snapshot gJoyButtonState -> gJoyButtonPrev (4 bytes inline). - lda gJoyButtonState - sta gJoyButtonPrev - lda gJoyButtonState+1 - sta gJoyButtonPrev+1 - lda gJoyButtonState+2 - sta gJoyButtonPrev+2 - lda gJoyButtonState+3 - sta gJoyButtonPrev+3 - + lda jlgIigsKeysMoved rep #0x30 .a16 .i16 + beq isnButtons + .irp off, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58 + lda jlgKeyState+\off + sta jlgKeyPrev+\off + .endr +isnButtons: + lda jlgMouseButtonState + sta jlgMouseButtonPrev + lda jlgMouseButtonState+2 + sta jlgMouseButtonPrev+2 + lda jlgJoyButtonState + sta jlgJoyButtonPrev + lda jlgJoyButtonState+2 + sta jlgJoyButtonPrev+2 plp rtl ; ==================================================================== -; iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, +; bool jlKeyDown(jlKeyE key), jlKeyPressed, jlKeyReleased, +; jlMouseDown(jlMouseButtonE button), jlMousePressed, jlMouseReleased +; +; The public predicates themselves (JL_HAS_INPUT_PREDICATES; input.c +; leaves its C bodies out): a game asks dozens a frame (JoeyAGI 42 a +; poll), and the compiled C spent twice as long making its bool. The same +; answers: out of range is false, a state byte counts as down when it is +; not zero, pressed is down now and not before, released the reverse. +; +; ABI: the key or button in A; 0 or 1 back in A; X clobbered. DBR is the +; caller's data bank; the state arrays via 16-bit abs,X. One section, so +; the shared false exit is a short branch from each. +; ==================================================================== +.equ IPR_KEY_COUNT, 60 ; JL_KEY_COUNT (input.c checks it) +.equ IPR_MOUSE_COUNT, 4 ; JL_MOUSE_BUTTON_COUNT + +.section .text.jlInputPredicates,"ax" + .a16 + .i16 +.globl jlKeyDown +.globl jlKeyPressed +.globl jlKeyReleased +.globl jlMouseDown +.globl jlMousePressed +.globl jlMouseReleased +jlKeyDown: + rep #0x30 + cmp #IPR_KEY_COUNT + bcs iprFalse + tax + lda jlgKeyState,x + and #0x00FF + beq iprKeyDownOut + lda #1 +iprKeyDownOut: + rtl +jlKeyPressed: + rep #0x30 + cmp #IPR_KEY_COUNT + bcs iprFalse + tax + lda jlgKeyPrev,x + and #0x00FF + bne iprFalse + lda jlgKeyState,x + and #0x00FF + beq iprKeyPressedOut + lda #1 +iprKeyPressedOut: + rtl +jlKeyReleased: + rep #0x30 + cmp #IPR_KEY_COUNT + bcs iprFalse + tax + lda jlgKeyState,x + and #0x00FF + bne iprFalse + lda jlgKeyPrev,x + and #0x00FF + beq iprKeyReleasedOut + lda #1 +iprKeyReleasedOut: + rtl +iprFalse: + lda #0 + rtl +jlMouseDown: + rep #0x30 + cmp #IPR_MOUSE_COUNT + bcs iprFalse + tax + lda jlgMouseButtonState,x + and #0x00FF + beq iprMouseDownOut + lda #1 +iprMouseDownOut: + rtl +jlMousePressed: + rep #0x30 + cmp #IPR_MOUSE_COUNT + bcs iprFalse + tax + lda jlgMouseButtonPrev,x + and #0x00FF + bne iprFalse + lda jlgMouseButtonState,x + and #0x00FF + beq iprMousePressedOut + lda #1 +iprMousePressedOut: + rtl +jlMouseReleased: + rep #0x30 + cmp #IPR_MOUSE_COUNT + bcs iprFalse + tax + lda jlgMouseButtonState,x + and #0x00FF + bne iprFalse + lda jlgMouseButtonPrev,x + and #0x00FF + beq iprMouseReleasedOut + lda #1 +iprMouseReleasedOut: + rtl + + +; ==================================================================== +; jlIigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, ; uint16_t matchColor, uint16_t newColor, ; uint16_t matchEqual, ; int16_t *stackX, int16_t *stackY, @@ -6586,7 +6654,7 @@ isnKeyLoop: ; Combined per-popped-seed flood work: seed test + walk-left + walk- ; right + span fill + scan-above + scan-below (each scan pushes new ; seeds onto the explicit stackX/stackY scan stack via *spInOut/maxSp). -; Outputs gFloodSeedMatch / gFloodLeftX / gFloodRightX. +; Outputs jlgFloodSeedMatch / jlgFloodLeftX / jlgFloodRightX. ; ; Original args (D-frame, D=SP+8, all on stack): ; pixels D+0..3, x D+4..5, y D+6..7, matchColor D+8..9, @@ -6599,23 +6667,23 @@ isnKeyLoop: ; computed row pointer and uses `[wsRow],y`, `[wsScanRow],y`, ; `[wsStackX],y`, `[wsStackY],y`, `[wsSpInOut],y` DP-indirect-long. ; The clang stack offers no such writable 28-byte window above the -; saved regs, so we point D at a private bank-0 scratch frame (wsFrame) +; saved regs, so we point D at a private bank-0 scratch frame (jlIigsWsFrame) ; and copy EVERY arg into it at the ORIGINAL D-offsets, leaving all the ; ws* equates and indirect loads unchanged. ; ; Stack source offsets (after php+phb+phd, +4): ; x@8,s y@10,s matchColor@12,s newColor@14,s matchEqual@16,s ; stackX@18,s(4) stackY@22,s(4) spInOut@26,s(4) maxSp@30,s -; Copied to: wsFrame+4 / +6 / +8 / +10 / +12 / +14..17 / +18..21 / -; +22..25 / +26 ; pixels(A:X) -> wsFrame+0..3. PLD restores caller D. +; Copied to: jlIigsWsFrame+4 / +6 / +8 / +10 / +12 / +14..17 / +18..21 / +; +22..25 / +26 ; pixels(A:X) -> jlIigsWsFrame+0..3. PLD restores caller D. ; ==================================================================== -.section .text.iigsFloodWalkAndScansInner,"ax" -.globl iigsFloodWalkAndScansInner -iigsFloodWalkAndScansInner: +.section .text.jlIigsFloodWalkAndScansInner,"ax" +.globl jlIigsFloodWalkAndScansInner +jlIigsFloodWalkAndScansInner: ; wsRow occupies D+0..3 (was pixels arg): row = pixels + y*160 is stored ; over the pixels slot so `lda [wsRow],y` reads the row directly. ; wsScanRow overlaps wsMatch+wsNew (D+8..11): those args are cached to -; wsMatchByte/wsNewByte first, then the slot is reused. +; jlIigsWsMatchByte/jlIigsWsNewByte first, then the slot is reused. wsRow = 0 ; D+0..3 (was wsPixels arg) wsX = 4 wsY = 6 @@ -6644,69 +6712,69 @@ wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit ; Rebuild the full D-frame in bank-0 scratch. ; pixels in A:X; the 9 stack args at +8/+10/.../+30,s. - sta wsFrame+0 ; pixels.lo (A) - stx wsFrame+2 ; pixels.hi (X) + sta jlIigsWsFrame+0 ; pixels.lo (A) + stx jlIigsWsFrame+2 ; pixels.hi (X) lda 8,s ; x - sta wsFrame+4 + sta jlIigsWsFrame+4 lda 10,s ; y - sta wsFrame+6 + sta jlIigsWsFrame+6 lda 12,s ; matchColor - sta wsFrame+8 + sta jlIigsWsFrame+8 lda 14,s ; newColor - sta wsFrame+10 + sta jlIigsWsFrame+10 lda 16,s ; matchEqual - sta wsFrame+12 + sta jlIigsWsFrame+12 lda 18,s ; stackX.lo - sta wsFrame+14 + sta jlIigsWsFrame+14 lda 20,s ; stackX.hi - sta wsFrame+16 + sta jlIigsWsFrame+16 lda 22,s ; stackY.lo - sta wsFrame+18 + sta jlIigsWsFrame+18 lda 24,s ; stackY.hi - sta wsFrame+20 + sta jlIigsWsFrame+20 lda 26,s ; spInOut.lo - sta wsFrame+22 + sta jlIigsWsFrame+22 lda 28,s ; spInOut.hi - sta wsFrame+24 + sta jlIigsWsFrame+24 lda 30,s ; maxSp - sta wsFrame+26 + sta jlIigsWsFrame+26 ; Copy the staged 32-byte frame into a STACK (bank-0) D-frame: - ; wsFrame is a BSS global in the LOAD bank, but D (the direct - ; page) is ALWAYS bank 0, so `tcd #wsFrame` aimed every + ; jlIigsWsFrame is a BSS global in the LOAD bank, but D (the direct + ; page) is ALWAYS bank 0, so `tcd #jlIigsWsFrame` aimed every ; D-relative read and [wsRow],y / [wsSpInOut],y indirect at ; bank-0:offset while the stores above wrote loadbank:offset. ; The hardware stack is bank 0, so a stack D-frame matches. - lda wsFrame+30 + lda jlIigsWsFrame+30 pha - lda wsFrame+28 + lda jlIigsWsFrame+28 pha - lda wsFrame+26 + lda jlIigsWsFrame+26 pha - lda wsFrame+24 + lda jlIigsWsFrame+24 pha - lda wsFrame+22 + lda jlIigsWsFrame+22 pha - lda wsFrame+20 + lda jlIigsWsFrame+20 pha - lda wsFrame+18 + lda jlIigsWsFrame+18 pha - lda wsFrame+16 + lda jlIigsWsFrame+16 pha - lda wsFrame+14 + lda jlIigsWsFrame+14 pha - lda wsFrame+12 + lda jlIigsWsFrame+12 pha - lda wsFrame+10 + lda jlIigsWsFrame+10 pha - lda wsFrame+8 + lda jlIigsWsFrame+8 pha - lda wsFrame+6 + lda jlIigsWsFrame+6 pha - lda wsFrame+4 + lda jlIigsWsFrame+4 pha - lda wsFrame+2 + lda jlIigsWsFrame+2 pha - lda wsFrame+0 + lda jlIigsWsFrame+0 pha tsc inc a ; A = SP+1 @@ -6717,22 +6785,22 @@ wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit .a8 lda wsMatch and #0x0F - sta wsMatchByte + sta jlIigsWsMatchByte lda wsNew and #0x0F - sta wsNewByte + sta jlIigsWsNewByte ora #0 pha ; save newByte for doubled compute asl a asl a asl a asl a ; A = newByte << 4 - sta wsNewHigh + sta jlIigsWsNewHigh ora 1,s ; A = (newByte<<4) | newByte - sta wsDoubledByte + sta jlIigsWsDoubledByte pla ; clean stack lda wsEq - sta wsEqByte + sta jlIigsWsEqByte rep #0x20 .a16 @@ -6740,7 +6808,7 @@ wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit lda wsY asl a ; A = y*2 (LUT byte offset) tax - lda gRowOffsetLut,x ; A = y*160 + lda jlgRowOffsetLut,x ; A = y*160 clc adc wsPixels ; A = pixels.lo + y*160 = row.lo tax ; X = row.lo (carry preserved) @@ -6753,7 +6821,7 @@ wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit ; === SEED TEST + WALK LEFT + WALK RIGHT === sep #0x20 .a8 - lda wsEqByte + lda jlIigsWsEqByte rep #0x20 .a16 bne wsWalkEqEntry @@ -6778,16 +6846,16 @@ wsSeedEqOdd: lda [wsRow],y and #0x0F wsSeedEqHave: - cmp wsMatchByte + cmp jlIigsWsMatchByte rep #0x20 .a16 bne wsSeedEqMiss lda #1 - sta gFloodSeedMatch + sta jlgFloodSeedMatch bra wsWalkEqSeedOk wsSeedEqMiss: lda #0 - sta gFloodSeedMatch + sta jlgFloodSeedMatch brl wsExit wsWalkEqSeedOk: @@ -6814,7 +6882,7 @@ wsLEqEvenEntry: lda [wsRow],y sta wsScanByte and #0x0F - cmp wsMatchByte + cmp jlIigsWsMatchByte rep #0x20 .a16 bne wsLeftEqDone @@ -6829,7 +6897,7 @@ wsLEqOddEntry: lsr a lsr a lsr a - cmp wsMatchByte + cmp jlIigsWsMatchByte rep #0x20 .a16 bne wsLeftEqDone @@ -6837,7 +6905,7 @@ wsLEqOddEntry: bra wsLEqEvenEntry wsLeftEqDone: lda wsScanCurX - sta gFloodLeftX + sta jlgFloodLeftX ; --- WALK RIGHT EQ (byte-cached) --- lda wsX @@ -6861,7 +6929,7 @@ wsREqEvenEntry: .a8 lda wsScanByte and #0x0F - cmp wsMatchByte + cmp jlIigsWsMatchByte rep #0x20 .a16 bne wsRightEqDone @@ -6879,7 +6947,7 @@ wsREqOddEntry: lsr a lsr a lsr a - cmp wsMatchByte + cmp jlIigsWsMatchByte rep #0x20 .a16 bne wsRightEqDone @@ -6887,7 +6955,7 @@ wsREqOddEntry: bra wsREqEvenEntry wsRightEqDone: lda wsScanCurX - sta gFloodRightX + sta jlgFloodRightX brl wsAfterWalk ; ***** BOUNDARY MODE WALK ***** @@ -6909,20 +6977,20 @@ wsSeedBndOdd: lda [wsRow],y and #0x0F wsSeedBndHave: - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsSeedBndMiss - cmp wsNewByte + cmp jlIigsWsNewByte beq wsSeedBndMiss rep #0x20 .a16 lda #1 - sta gFloodSeedMatch + sta jlgFloodSeedMatch bra wsWalkBndSeedOk wsSeedBndMiss: rep #0x20 .a16 lda #0 - sta gFloodSeedMatch + sta jlgFloodSeedMatch brl wsExit wsWalkBndSeedOk: @@ -6949,9 +7017,9 @@ wsLBndEvenEntry: lda [wsRow],y sta wsScanByte and #0x0F - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsLBndStop - cmp wsNewByte + cmp jlIigsWsNewByte beq wsLBndStop rep #0x20 .a16 @@ -6972,9 +7040,9 @@ wsLBndOddEntry: lsr a lsr a lsr a - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsLBndStop2 - cmp wsNewByte + cmp jlIigsWsNewByte beq wsLBndStop2 rep #0x20 .a16 @@ -6985,7 +7053,7 @@ wsLBndStop2: .a16 wsLeftBndDone: lda wsScanCurX - sta gFloodLeftX + sta jlgFloodLeftX ; --- WALK RIGHT BND (byte-cached) --- lda wsX @@ -7009,9 +7077,9 @@ wsRBndEvenEntry: .a8 lda wsScanByte and #0x0F - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsRBndStop - cmp wsNewByte + cmp jlIigsWsNewByte beq wsRBndStop rep #0x20 .a16 @@ -7035,9 +7103,9 @@ wsRBndOddEntry: lsr a lsr a lsr a - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsRBndStop2 - cmp wsNewByte + cmp jlIigsWsNewByte beq wsRBndStop2 rep #0x20 .a16 @@ -7048,22 +7116,22 @@ wsRBndStop2: .a16 wsRightBndDone: lda wsScanCurX - sta gFloodRightX + sta jlgFloodRightX wsAfterWalk: ; Cache leftX/rightX/spanLen for the scans. - lda gFloodLeftX - sta wsLeftX - lda gFloodRightX - sta wsRightX + lda jlgFloodLeftX + sta jlIigsWsLeftX + lda jlgFloodRightX + sta jlIigsWsRightX sec - sbc wsLeftX + sbc jlIigsWsLeftX inc a - sta wsSpanLen + sta jlIigsWsSpanLen ; === FILL THE SPAN === - lda wsLeftX + lda jlIigsWsLeftX lsr a ; A = leadingByte; carry = leftX & 1 tay ; Y = leadingByte bcc wsFillNoLead @@ -7072,7 +7140,7 @@ wsAfterWalk: .a8 lda [wsRow],y and #0xF0 - ora wsNewByte + ora jlIigsWsNewByte sta [wsRow],y rep #0x20 .a16 @@ -7080,22 +7148,22 @@ wsAfterWalk: wsFillNoLead: ; midEnd = (rightX + 1) >> 1; trailing partial only if rightX even. - lda wsRightX + lda jlIigsWsRightX inc a ; A = pxEnd = rightX + 1 lsr a ; A = midEnd; carry = pxEnd & 1 sta wsMidEnd ; DP store (so DP-direct CPY works) bcs wsFillSetTrail lda #0 - sta wsHasTrail + sta jlIigsWsHasTrail bra wsFillMidLoop wsFillSetTrail: lda #1 - sta wsHasTrail + sta jlIigsWsHasTrail wsFillMidLoop: sep #0x20 .a8 - lda wsDoubledByte + lda jlIigsWsDoubledByte wsFillMidIter: cpy wsMidEnd ; DP-direct CPY (X=16-bit reads 2 bytes) bcs wsFillMidDone @@ -7106,7 +7174,7 @@ wsFillMidDone: rep #0x20 .a16 - lda wsHasTrail + lda jlIigsWsHasTrail beq wsFillDone ; Trailing partial byte: high nibble of byte[wsMidEnd] = newNibble. ldy wsMidEnd ; Y = trailing byte index @@ -7114,7 +7182,7 @@ wsFillMidDone: .a8 lda [wsRow],y and #0x0F - ora wsNewHigh + ora jlIigsWsNewHigh sta [wsRow],y rep #0x20 .a16 @@ -7123,9 +7191,9 @@ wsFillDone: ; Load sp from *spInOut, cache maxSp. ldy #0 lda [wsSpInOut],y - sta wsSp + sta jlIigsWsSp lda wsMaxSp - sta wsMaxSpCache + sta jlIigsWsMaxSpCache ; === SCAN ABOVE (if y > 0) === lda wsY @@ -7141,7 +7209,7 @@ wsHasAbove: sta wsScanRow+2 lda wsY dec a - sta wsScanY + sta jlIigsWsScanY jsr wsScanAndPush wsSkipAbove: @@ -7160,13 +7228,13 @@ wsHasBelow: sta wsScanRow+2 lda wsY inc a - sta wsScanY + sta jlIigsWsScanY jsr wsScanAndPush wsSkipBelow: ; Store updated sp back to *spInOut. ldy #0 - lda wsSp + lda jlIigsWsSp sta [wsSpInOut],y wsExit: @@ -7183,15 +7251,15 @@ wsExit: rtl -; wsScanAndPush: walk wsScanRow[wsLeftX..wsRightX] for run-edge -; transitions. Pushes (curX-1, wsScanY) on falling edges and -; (rightX, wsScanY) at end of trailing run. 2 pixels per byte read. +; wsScanAndPush: walk wsScanRow[jlIigsWsLeftX..jlIigsWsRightX] for run-edge +; transitions. Pushes (curX-1, jlIigsWsScanY) on falling edges and +; (rightX, jlIigsWsScanY) at end of trailing run. 2 pixels per byte read. wsScanAndPush: sep #0x20 .a8 lda #0 sta wsScanPrevHit - lda wsEqByte + lda jlIigsWsEqByte rep #0x20 .a16 bne wsEqEntry2 @@ -7199,7 +7267,7 @@ wsScanAndPush: ; ***** EQUAL MODE ***** wsEqEntry2: - lda wsLeftX + lda jlIigsWsLeftX sta wsScanCurX ; DP write 16-bit lsr a ; A = byteIdx; carry = leftX & 1 tay ; Y = byteIdx @@ -7210,7 +7278,7 @@ wsEqEntry2: .a8 lda [wsScanRow],y and #0x0F - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsEqLeadHit2 lda #0 bra wsEqLeadStore2 @@ -7225,7 +7293,7 @@ wsEqLeadStore2: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7242,7 +7310,7 @@ wsEqAfterLead2: wsEqPairLoop2: lda wsScanCurX inc a - cmp wsRightX + cmp jlIigsWsRightX bcc wsEqDoPair2 beq wsEqDoPair2 brl wsEqTrailing2 @@ -7256,7 +7324,7 @@ wsEqDoPair2: lsr a lsr a lsr a ; A = high nibble (pixel at curX) - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsEqHi2Hit lda #0 bra wsEqHi2Store @@ -7271,7 +7339,7 @@ wsEqHi2Store: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7286,7 +7354,7 @@ wsEqHi2NoFall: .a8 lda wsScanByte and #0x0F ; A = low nibble (pixel at curX after inc) - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsEqLo2Hit lda #0 bra wsEqLo2Store @@ -7301,7 +7369,7 @@ wsEqLo2Store: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7316,7 +7384,7 @@ wsEqLo2NoFall: wsEqTrailing2: lda wsScanCurX - cmp wsRightX + cmp jlIigsWsRightX bcc wsEqDoTrail2 beq wsEqDoTrail2 brl wsScanDone @@ -7329,7 +7397,7 @@ wsEqDoTrail2: lsr a lsr a lsr a ; A = high nibble - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsEqTr2Hit lda #0 bra wsEqTr2Store @@ -7344,7 +7412,7 @@ wsEqTr2Store: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7357,7 +7425,7 @@ wsEqTr2NoFall: ; ***** BOUNDARY MODE ***** wsBoundEntry2: - lda wsLeftX + lda jlIigsWsLeftX sta wsScanCurX lsr a tay @@ -7368,9 +7436,9 @@ wsBoundEntry2: .a8 lda [wsScanRow],y and #0x0F - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsBndLeadMiss2 - cmp wsNewByte + cmp jlIigsWsNewByte beq wsBndLeadMiss2 lda #1 bra wsBndLeadStore2 @@ -7385,7 +7453,7 @@ wsBndLeadStore2: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7401,7 +7469,7 @@ wsBndAfterLead2: wsBndPairLoop2: lda wsScanCurX inc a - cmp wsRightX + cmp jlIigsWsRightX bcc wsBndDoPair2 beq wsBndDoPair2 brl wsBndTrailing2 @@ -7415,9 +7483,9 @@ wsBndDoPair2: lsr a lsr a lsr a ; A = high nibble - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsBndHi2Miss - cmp wsNewByte + cmp jlIigsWsNewByte beq wsBndHi2Miss lda #1 bra wsBndHi2Store @@ -7432,7 +7500,7 @@ wsBndHi2Store: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7447,9 +7515,9 @@ wsBndHi2NoFall: .a8 lda wsScanByte and #0x0F ; A = low nibble - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsBndLo2Miss - cmp wsNewByte + cmp jlIigsWsNewByte beq wsBndLo2Miss lda #1 bra wsBndLo2Store @@ -7464,7 +7532,7 @@ wsBndLo2Store: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7479,7 +7547,7 @@ wsBndLo2NoFall: wsBndTrailing2: lda wsScanCurX - cmp wsRightX + cmp jlIigsWsRightX bcc wsBndDoTrail2 beq wsBndDoTrail2 brl wsScanDone @@ -7492,9 +7560,9 @@ wsBndDoTrail2: lsr a lsr a lsr a ; A = high nibble - cmp wsMatchByte + cmp jlIigsWsMatchByte beq wsBndTr2Miss - cmp wsNewByte + cmp jlIigsWsNewByte beq wsBndTr2Miss lda #1 bra wsBndTr2Store @@ -7509,7 +7577,7 @@ wsBndTr2Store: .a16 lda wsScanCurX dec a - sta wsPushX + sta jlIigsWsPushX jsr wsPushXY sep #0x20 .a8 @@ -7528,30 +7596,30 @@ wsScanDone: rep #0x20 .a16 beq wsScanReturn - lda wsRightX - sta wsPushX + lda jlIigsWsRightX + sta jlIigsWsPushX jsr wsPushXY wsScanReturn: rts -; wsPushXY: push (wsPushX, wsScanY) at sp if sp<maxSp. Increments sp. +; wsPushXY: push (jlIigsWsPushX, jlIigsWsScanY) at sp if sp<maxSp. Increments sp. ; M=16 X=16 on entry/exit; preserves caller A/Y via PHA/PHY. wsPushXY: pha phy - lda wsSp - cmp wsMaxSpCache + lda jlIigsWsSp + cmp jlIigsWsMaxSpCache bcs wsPushSkip asl a tay - lda wsPushX + lda jlIigsWsPushX sta [wsStackX],y - lda wsScanY + lda jlIigsWsScanY sta [wsStackY],y - lda wsSp + lda jlIigsWsSp inc a - sta wsSp + sta jlIigsWsSp wsPushSkip: ply pla @@ -7560,86 +7628,86 @@ wsPushSkip: ; ==================================================================== ; Data symbols defined by these functions (DRAWPRIMS scratch globals). -; gRowOffsetLut / gFloodSeedMatch / gFloodLeftX / gFloodRightX, -; gStageMinWord / gStageMaxWord, gPei* and gKey/gMouse/gJoy*Button* +; jlgRowOffsetLut / jlgFloodSeedMatch / jlgFloodLeftX / jlgFloodRightX, +; jlgStageMinWord / jlgStageMaxWord, gPei* and gKey/gMouse/gJoy*Button* ; are defined ELSEWHERE (asm or C); they are left as externs. ; ==================================================================== -; Private bank-0 D-frame for iigsFloodWalkAndScansInner (28 args bytes; +; Private bank-0 D-frame for jlIigsFloodWalkAndScansInner (28 args bytes; ; pad to a clean 32). -.section .bss.wsFrame,"aw" -.globl wsFrame -wsFrame: +.section .bss.jlIigsWsFrame,"aw" +.globl jlIigsWsFrame +jlIigsWsFrame: .zero 32 -; iigsPollJoystickInner has NO output globals: it returns its result in +; jlIigsPollJoystickInner has NO output globals: it returns its result in ; registers (uint32_t in A:X). An asm WRITE of a global does not reach the ; C-read address in this toolchain (asm READING a C-defined global like -; gStageMinWord is fine; asm WRITING one that C reads is not), so the +; jlgStageMinWord is fine; asm WRITING one that C reads is not), so the ; register-return ABI is used instead. -; iigsFloodWalkAndScansInner scratch. -.section .bss.wsLeftX,"aw" -.globl wsLeftX -wsLeftX: +; jlIigsFloodWalkAndScansInner scratch. +.section .bss.jlIigsWsLeftX,"aw" +.globl jlIigsWsLeftX +jlIigsWsLeftX: .zero 2 -.section .bss.wsRightX,"aw" -.globl wsRightX -wsRightX: +.section .bss.jlIigsWsRightX,"aw" +.globl jlIigsWsRightX +jlIigsWsRightX: .zero 2 -.section .bss.wsScanY,"aw" -.globl wsScanY -wsScanY: +.section .bss.jlIigsWsScanY,"aw" +.globl jlIigsWsScanY +jlIigsWsScanY: .zero 2 -.section .bss.wsMatchByte,"aw" -.globl wsMatchByte -wsMatchByte: +.section .bss.jlIigsWsMatchByte,"aw" +.globl jlIigsWsMatchByte +jlIigsWsMatchByte: .zero 2 -.section .bss.wsNewByte,"aw" -.globl wsNewByte -wsNewByte: +.section .bss.jlIigsWsNewByte,"aw" +.globl jlIigsWsNewByte +jlIigsWsNewByte: .zero 2 -.section .bss.wsEqByte,"aw" -.globl wsEqByte -wsEqByte: +.section .bss.jlIigsWsEqByte,"aw" +.globl jlIigsWsEqByte +jlIigsWsEqByte: .zero 2 -.section .bss.wsSp,"aw" -.globl wsSp -wsSp: +.section .bss.jlIigsWsSp,"aw" +.globl jlIigsWsSp +jlIigsWsSp: .zero 2 -.section .bss.wsMaxSpCache,"aw" -.globl wsMaxSpCache -wsMaxSpCache: +.section .bss.jlIigsWsMaxSpCache,"aw" +.globl jlIigsWsMaxSpCache +jlIigsWsMaxSpCache: .zero 2 -.section .bss.wsSpanLen,"aw" -.globl wsSpanLen -wsSpanLen: +.section .bss.jlIigsWsSpanLen,"aw" +.globl jlIigsWsSpanLen +jlIigsWsSpanLen: .zero 2 -.section .bss.wsPushX,"aw" -.globl wsPushX -wsPushX: +.section .bss.jlIigsWsPushX,"aw" +.globl jlIigsWsPushX +jlIigsWsPushX: .zero 2 -.section .bss.wsNewHigh,"aw" -.globl wsNewHigh -wsNewHigh: +.section .bss.jlIigsWsNewHigh,"aw" +.globl jlIigsWsNewHigh +jlIigsWsNewHigh: .zero 2 -.section .bss.wsDoubledByte,"aw" -.globl wsDoubledByte -wsDoubledByte: +.section .bss.jlIigsWsDoubledByte,"aw" +.globl jlIigsWsDoubledByte +jlIigsWsDoubledByte: .zero 2 -.section .bss.wsHasTrail,"aw" -.globl wsHasTrail -wsHasTrail: +.section .bss.jlIigsWsHasTrail,"aw" +.globl jlIigsWsHasTrail +jlIigsWsHasTrail: .zero 2 diff --git a/src/iigs/peislam.s b/src/iigs/peislam.s index 8d2744b..6e78bab 100644 --- a/src/iigs/peislam.s +++ b/src/iigs/peislam.s @@ -5,7 +5,7 @@ ; peislam.s - VBL tick counter and ReadBParam trampoline for the IIgs. ; ; GAS syntax for the llvm-mos w65816 toolchain. The old PEI-slam present -; logic was rolled into iigsBlitStageToShr in joeyDraw; what remains is +; logic was rolled into jlIigsBlitStageToShr in joeyDraw; what remains is ; the heartbeat-driven tick counter and one toolbox trampoline. ; ; C ABI (llvm-mos w65816): caller has M=I=16. A uint16_t return value @@ -19,7 +19,7 @@ ; ; A Misc Toolset heartbeat task (SetHeartBeat, $1203) that the firmware ; VBL interrupt handler runs once per video field. It bumps a 16-bit -; counter that iigsGetTickWord returns. The old wrapper went through +; counter that jlIigsGetTickWord returns. The old wrapper went through ; the toolbox dispatcher to _GetTick ($2503) on every read -- hundreds ; of cycles, ten-odd times per Space Taxi frame (~5% of a frame). The ; rate is unchanged: both GetTick and the heartbeat queue are driven @@ -41,9 +41,9 @@ ; +8 code, ended by rtl ; ---------------------------------------------------------------- .section .text.iigsVblTask,"ax" - .globl iigsGetTickWord - .globl iigsVblInstall - .globl iigsVblRemove + .globl jlIigsGetTickWord + .globl jlIigsVblInstall + .globl jlIigsVblRemove iigsVblTask: .long 0 .word 1 @@ -64,8 +64,8 @@ iigsVblTask: vblTick: .word 0 -; uint16_t iigsGetTickWord(void) -- the current VBL count. -iigsGetTickWord: +; uint16_t jlIigsGetTickWord(void) -- the current VBL count. +jlIigsGetTickWord: phb phk plb @@ -73,8 +73,8 @@ iigsGetTickWord: plb rtl -; uint16_t iigsVblInstall(void) -- SetHeartBeat(iigsVblTask). -; uint16_t iigsVblRemove(void) -- DelHeartBeat(iigsVblTask). +; uint16_t jlIigsVblInstall(void) -- SetHeartBeat(iigsVblTask). +; uint16_t jlIigsVblRemove(void) -- DelHeartBeat(iigsVblTask). ; Both return the Misc Tool Set error code, 0 = success. The long ; pointer is pushed high word first ($00, K) then the low word (a ; same-section 16-bit relocation). The high word is built without @@ -84,10 +84,10 @@ iigsGetTickWord: ; push after sep #0x20 is what the first cut did, and llvm-mc kept ; assembling `lda #0` as a 16-bit immediate (no .a8 directive) -- the ; CPU ran the extra byte as BRK inside the call. -iigsVblInstall: +jlIigsVblInstall: ldx #0x1203 ; _SetHeartBeat bra vblTool -iigsVblRemove: +jlIigsVblRemove: ldx #0x1303 ; _DelHeartBeat vblTool: lda #0 @@ -106,7 +106,7 @@ vblToolDone: rtl ; ---------------------------------------------------------------- -; uint16_t iigsReadHzParam(void) +; uint16_t jlIigsReadHzParam(void) ; ; Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03) ; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz). @@ -116,9 +116,9 @@ vblToolDone: ; report whichever this machine actually runs so wall-clock math ; (frames * 1000 / jlpFrameHz) is correct on both. ; ---------------------------------------------------------------- - .section .text.iigsReadHzParam,"ax" - .globl iigsReadHzParam -iigsReadHzParam: + .section .text.jlIigsReadHzParam,"ax" + .globl jlIigsReadHzParam +jlIigsReadHzParam: pha ; output space (Word) pea 0x001d ; hrtz50or60 parameter ID ldx #0x0c03 ; _ReadBParam diff --git a/src/iigs/serial.c b/src/iigs/serial.c index 11df764..fda15a0 100644 --- a/src/iigs/serial.c +++ b/src/iigs/serial.c @@ -18,7 +18,7 @@ // table. // // Flow control lives in the handlers (serialIrq.s) and the transmit step -// (iigsSerialSend). XON/XOFF: the peer's XOFF and XON pause and resume +// (jlIigsSerialSend). XON/XOFF: the peer's XOFF and XON pause and resume // transmit and never reach the ring, and an XOFF goes out when the ring is // three quarters full, an XON once a read has it down to a quarter. RTS/CTS: // on the modem and printer ports transmit waits for CTS (HSKi), but the @@ -41,8 +41,8 @@ // // The one thing an interrupt cannot beat is code that masks interrupts, and a // byte that arrives then waits for the CLI. While a port is open this add-on -// holds the HAL's tight interrupt-latency bound (iigsIrqLatencyTighten), which -// cuts the present's chunked SEI (see iigsBlitStageToShr), the stage fast +// holds the HAL's tight interrupt-latency bound (jlIigsIrqLatencyTighten), which +// cuts the present's chunked SEI (see jlIigsBlitStageToShr), the stage fast // clear, the joystick read and the NinjaTrackerPlus effect calls into holds // of at most ~1.7 ms. GS/OS disk I/O and jlAudioPlayMod's instrument upload // are not JoeyLib's to cut. @@ -126,7 +126,7 @@ #define SCC_WR14_BRG_RTXC 0x01u // the BRG on, its source RTxC #define SCC_WR14_OFF 0x00u -// A write's backstop on a busy transmitter, in sends (serialTxStalled): the +// A write's backstop on a busy transmitter, in sends (jlSerialTxStalled): the // clock bounds it first, once it runs. #define IIGS_TX_POLL_LIMIT 65536ul @@ -185,39 +185,39 @@ typedef enum { // The receive ring. One producer -- the interrupt handler, or the same drain // run by the mainline with interrupts masked -- stores the byte and then -// publishes gIigsRxTail with a single 16-bit store; the mainline consumer only -// ever moves gIigsRxHead. -uint8_t gIigsRxRing[RX_RING_SIZE]; -const uint16_t gIigsRxRingMask = RX_RING_MASK; -const uint16_t gIigsRxHighWater = RX_HIGH_WATER; -volatile uint16_t gIigsRxHead = 0u; -volatile uint16_t gIigsRxTail = 0u; +// publishes jlgIigsRxTail with a single 16-bit store; the mainline consumer only +// ever moves jlgIigsRxHead. +uint8_t jlgIigsRxRing[RX_RING_SIZE]; +const uint16_t jlgIigsRxRingMask = RX_RING_MASK; +const uint16_t jlgIigsRxHighWater = RX_HIGH_WATER; +volatile uint16_t jlgIigsRxHead = 0u; +volatile uint16_t jlgIigsRxTail = 0u; // Flow control. serialIrq.s spells the jlSerialFlowE values as FLOW_RTSCTS / FLOW_XONXOFF. -uint8_t gIigsSerFlow = JL_SERIAL_FLOW_NONE; // jlSerialFlowE of the open port -volatile uint8_t gIigsRxThrottled = 0u; // a handler is holding the peer off -volatile uint8_t gIigsTxPaused = 0u; // the peer sent XOFF (nonzero) or XON (0) -volatile uint8_t gIigsTxCtl = 0u; // XOFF / XON waiting for the transmitter, or 0 -uint8_t gIigsAciaCmd = 0u; // the 6551's command register as the program +uint8_t jlgIigsSerFlow = JL_SERIAL_FLOW_NONE; // jlSerialFlowE of the open port +volatile uint8_t jlgIigsRxThrottled = 0u; // a handler is holding the peer off +volatile uint8_t jlgIigsTxPaused = 0u; // the peer sent XOFF (nonzero) or XON (0) +volatile uint8_t jlgIigsTxCtl = 0u; // XOFF / XON waiting for the transmitter, or 0 +uint8_t jlgIigsAciaCmd = 0u; // the 6551's command register as the program // set it: a throttle writes it with RTS down -uint16_t gIigsSerKind = IIGS_SER_CLOSED; // IigsSerKindE of the open port -uint16_t gIigsSerPort = 0u; // SCC command register / 6551 data register (I/O address) -uint8_t gIigsSerIpMask = 0u; // the open SCC channel's RR3 bits; 0 for the 6551 -uint32_t gIigsSerChain = 0ul; // the vector's previous owner, minus one (the handlers RTL to it) +uint16_t jlgIigsSerKind = IIGS_SER_CLOSED; // IigsSerKindE of the open port +uint16_t jlgIigsSerPort = 0u; // SCC command register / 6551 data register (I/O address) +uint8_t jlgIigsSerIpMask = 0u; // the open SCC channel's RR3 bits; 0 for the 6551 +uint32_t jlgIigsSerChain = 0ul; // the vector's previous owner, minus one (the handlers RTL to it) // Flow counters for the RetroNet RXSTAT diagnostic (read via extern): bytes moved chip->ring and // ring->caller since open. Cheap enough to keep unconditionally. -uint16_t gIigsRxPumped = 0u; -uint16_t gIigsRxRead = 0u; +uint16_t jlgIigsRxPumped = 0u; +uint16_t jlgIigsRxRead = 0u; // Receive-loss counters, same read-via-extern contract. Overruns = loss EVENTS the hardware latched // (SCC RR1 overrun, checked after every byte / 6551 status bit 2) - one latch observation may stand for // SEVERAL destroyed bytes, so this is a loss indicator, not a byte count; MAME's z80scc never surfaces // the SCC flag, so there it stays 0. Dropped = bytes the SOFT ring refused because it was full (read // from the chip, then discarded). Both saturate. -uint16_t gIigsRxOverruns = 0u; -uint16_t gIigsRxDropped = 0u; +uint16_t jlgIigsRxOverruns = 0u; +uint16_t jlgIigsRxDropped = 0u; // ----- Module state ----- @@ -234,11 +234,11 @@ static uint8_t gSerflagOld; // our channel's SERFLAG bits before // ----- Prototypes ----- -extern uint16_t iigsIrqOff(void); -extern void iigsIrqRestore(uint16_t p); -extern uint32_t iigsSerialIsrAddress(uint16_t kind); -extern uint16_t iigsSerialSend(uint16_t byte); -extern uint16_t iigsSerialService(void); +extern uint16_t jlIigsIrqOff(void); +extern void jlIigsIrqRestore(uint16_t p); +extern uint32_t jlIigsSerialIsrAddress(uint16_t kind); +extern uint16_t jlIigsSerialSend(uint16_t byte); +extern uint16_t jlIigsSerialService(void); static uint8_t aciaBaudCode(uint32_t baud); static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val); static bool serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg); @@ -257,12 +257,12 @@ static void serialSetLine(uint8_t sccBit, uint8_t aciaMask, uint8_t aciaOn, static void sccWrite(volatile uint8_t *ctrl, uint8_t reg, uint8_t val) { uint16_t p; - p = iigsIrqOff(); + p = jlIigsIrqOff(); if (reg != 0u) { *ctrl = reg; } *ctrl = val; - iigsIrqRestore(p); + jlIigsIrqRestore(p); } @@ -354,18 +354,18 @@ static void serialOpenScc(volatile uint8_t *ctrl, uint8_t reset, uint8_t xtal, u static void serialRxRelease(void) { uint16_t p; - if (gIigsRxThrottled == 0u || jlpSerialAvailable() > RX_LOW_WATER) { + if (jlgIigsRxThrottled == 0u || jlpSerialAvailable() > RX_LOW_WATER) { return; } - p = iigsIrqOff(); - gIigsRxThrottled = 0u; - if (gIigsSerFlow == JL_SERIAL_FLOW_XONXOFF) { - gIigsTxCtl = XON_CHAR; + p = jlIigsIrqOff(); + jlgIigsRxThrottled = 0u; + if (jlgIigsSerFlow == JL_SERIAL_FLOW_XONXOFF) { + jlgIigsTxCtl = XON_CHAR; } else { - *gReg = gIigsAciaCmd; + *gReg = jlgIigsAciaCmd; } - iigsIrqRestore(p); - (void)iigsSerialService(); + jlIigsIrqRestore(p); + (void)jlIigsSerialService(); } @@ -377,18 +377,18 @@ static void serialRxRelease(void) { static void serialSetLine(uint8_t sccBit, uint8_t aciaMask, uint8_t aciaOn, bool on) { uint16_t p; - if (gIigsSerKind == IIGS_SER_SCC) { + if (jlgIigsSerKind == IIGS_SER_SCC) { gWr5 = on ? (uint8_t)(gWr5 | sccBit) : (uint8_t)(gWr5 & (uint8_t)~sccBit); sccWrite(gReg, 5u, gWr5); return; } - p = iigsIrqOff(); - gIigsAciaCmd = (uint8_t)(gIigsAciaCmd & (uint8_t)~aciaMask); + p = jlIigsIrqOff(); + jlgIigsAciaCmd = (uint8_t)(jlgIigsAciaCmd & (uint8_t)~aciaMask); if (on) { - gIigsAciaCmd = (uint8_t)(gIigsAciaCmd | aciaOn); + jlgIigsAciaCmd = (uint8_t)(jlgIigsAciaCmd | aciaOn); } - *gReg = gIigsRxThrottled != 0u ? (uint8_t)(gIigsAciaCmd & (uint8_t)~ACIA_CMD_TX_MASK) : gIigsAciaCmd; - iigsIrqRestore(p); + *gReg = jlgIigsRxThrottled != 0u ? (uint8_t)(jlgIigsAciaCmd & (uint8_t)~ACIA_CMD_TX_MASK) : jlgIigsAciaCmd; + jlIigsIrqRestore(p); } @@ -432,11 +432,11 @@ static bool serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg) { } // Command: DTR on, receiver interrupt ENABLED, RTS asserted with the transmit interrupt off, parity. - gIigsAciaCmd = (uint8_t)(ACIA_CMD_DTR | ACIA_CMD_RTS_ON); + jlgIigsAciaCmd = (uint8_t)(ACIA_CMD_DTR | ACIA_CMD_RTS_ON); if (cfg->parity == JL_SERIAL_PARITY_ODD) { - gIigsAciaCmd |= ACIA_CMD_PARITY_ODD; + jlgIigsAciaCmd |= ACIA_CMD_PARITY_ODD; } else if (cfg->parity == JL_SERIAL_PARITY_EVEN) { - gIigsAciaCmd |= ACIA_CMD_PARITY_EVEN; + jlgIigsAciaCmd |= ACIA_CMD_PARITY_EVEN; } base[ACIA_STATUS] = 0x00u; // write to status register = programmed reset (DTR off: no interrupts) @@ -453,18 +453,18 @@ static bool serialInitAcia(volatile uint8_t *base, const jlSerialConfigT *cfg) { static void serialIrqInstall(uint16_t kind, volatile uint8_t *port, uint8_t rr3Bits, uint16_t vector) { uint16_t p; - p = iigsIrqOff(); - gVector = vector; - gVectorOld = (uint32_t)GetVector(vector); - gIigsSerChain = gVectorOld - 1ul; - gIigsSerPort = (uint16_t)(uint32_t)port; - gIigsSerIpMask = rr3Bits; - gSerflagOld = (uint8_t)(*IIGS_SERFLAG & rr3Bits); - *IIGS_SERFLAG = (uint8_t)(*IIGS_SERFLAG | rr3Bits); - gIigsSerKind = kind; - SetVector(vector, (void *)iigsSerialIsrAddress(kind)); - iigsIrqRestore(p); - iigsIrqLatencyTighten(); + p = jlIigsIrqOff(); + gVector = vector; + gVectorOld = (uint32_t)GetVector(vector); + jlgIigsSerChain = gVectorOld - 1ul; + jlgIigsSerPort = (uint16_t)(uint32_t)port; + jlgIigsSerIpMask = rr3Bits; + gSerflagOld = (uint8_t)(*IIGS_SERFLAG & rr3Bits); + *IIGS_SERFLAG = (uint8_t)(*IIGS_SERFLAG | rr3Bits); + jlgIigsSerKind = kind; + SetVector(vector, (void *)jlIigsSerialIsrAddress(kind)); + jlIigsIrqRestore(p); + jlIigsIrqLatencyTighten(); } @@ -473,20 +473,20 @@ static void serialIrqInstall(uint16_t kind, volatile uint8_t *port, uint8_t rr3B static void serialIrqRemove(void) { uint16_t p; - p = iigsIrqOff(); - gIigsSerKind = IIGS_SER_CLOSED; - *IIGS_SERFLAG = (uint8_t)((*IIGS_SERFLAG & (uint8_t)~gIigsSerIpMask) | gSerflagOld); - gIigsSerIpMask = 0u; + p = jlIigsIrqOff(); + jlgIigsSerKind = IIGS_SER_CLOSED; + *IIGS_SERFLAG = (uint8_t)((*IIGS_SERFLAG & (uint8_t)~jlgIigsSerIpMask) | gSerflagOld); + jlgIigsSerIpMask = 0u; SetVector(gVector, (void *)gVectorOld); - iigsIrqRestore(p); - iigsIrqLatencyRelax(); + jlIigsIrqRestore(p); + jlIigsIrqLatencyRelax(); } // ----- HAL entry points (alphabetical) ----- uint16_t jlpSerialAvailable(void) { - return (uint16_t)((gIigsRxTail - gIigsRxHead) & RX_RING_MASK); + return (uint16_t)((jlgIigsRxTail - jlgIigsRxHead) & RX_RING_MASK); } @@ -494,27 +494,27 @@ uint16_t jlpSerialAvailable(void) { void jlpSerialClose(void) { uint16_t p; - if (gIigsSerKind == IIGS_SER_CLOSED) { + if (jlgIigsSerKind == IIGS_SER_CLOSED) { return; } - p = iigsIrqOff(); - if (gIigsSerKind == IIGS_SER_SCC) { + p = jlIigsIrqOff(); + if (jlgIigsSerKind == IIGS_SER_SCC) { sccWrite(gReg, 1u, SCC_WR1_NO_INTS); } else { - gIigsAciaCmd = (uint8_t)(gIigsAciaCmd | ACIA_CMD_RX_IRQ_OFF); - *gReg = gIigsAciaCmd; + jlgIigsAciaCmd = (uint8_t)(jlgIigsAciaCmd | ACIA_CMD_RX_IRQ_OFF); + *gReg = jlgIigsAciaCmd; } // A request raised before the line went quiet is retired by taking its byte; left pending, a // level-triggered IRQ whose bits are no longer claimed would re-enter the ROM forever. - (void)iigsSerialService(); + (void)jlIigsSerialService(); serialIrqRemove(); - iigsIrqRestore(p); + jlIigsIrqRestore(p); // Otherwise the chip stays configured (the transmitter keeps running) until the next open. } void jlpSerialFlush(void) { - gIigsRxHead = gIigsRxTail; + jlgIigsRxHead = jlgIigsRxTail; serialRxRelease(); } @@ -522,12 +522,12 @@ void jlpSerialFlush(void) { bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { volatile uint8_t *base; - gIigsRxHead = 0u; - gIigsRxTail = 0u; - gIigsSerFlow = (uint8_t)config->flow; - gIigsRxThrottled = 0u; - gIigsTxPaused = 0u; - gIigsTxCtl = 0u; + jlgIigsRxHead = 0u; + jlgIigsRxTail = 0u; + jlgIigsSerFlow = (uint8_t)config->flow; + jlgIigsRxThrottled = 0u; + jlgIigsTxPaused = 0u; + jlgIigsTxCtl = 0u; switch (device) { case JL_SERIAL_PRINTER: @@ -540,11 +540,11 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { base = SSC_BASE(config->unit); gReg = &base[ACIA_COMMAND]; if (!serialInitAcia(base, config)) { - coreSetError(SSC_MISSING); + jlCoreSetError(SSC_MISSING); return false; } serialIrqInstall(IIGS_SER_ACIA, base, 0u, IIGS_VECTOR_OTHER); - *gReg = gIigsAciaCmd; // handler in: start the receiver and its interrupt + *gReg = jlgIigsAciaCmd; // handler in: start the receiver and its interrupt break; case JL_SERIAL_MODEM: case JL_SERIAL_DEFAULT: @@ -559,7 +559,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { // The interrupt keeps the ring filled; this only catches what it could not -- a Super Serial Card // whose interrupt switch is off. void jlpSerialPoll(void) { - (void)iigsSerialService(); + (void)jlIigsSerialService(); } @@ -568,14 +568,14 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { uint16_t head; uint16_t tail; - head = gIigsRxHead; - tail = gIigsRxTail; // one snapshot: the handler may append while we copy + head = jlgIigsRxHead; + tail = jlgIigsRxTail; // one snapshot: the handler may append while we copy for (n = 0u; n < max && head != tail; n++) { - buf[n] = gIigsRxRing[head]; + buf[n] = jlgIigsRxRing[head]; head = (uint16_t)((head + 1u) & RX_RING_MASK); } - gIigsRxHead = head; - gIigsRxRead = (uint16_t)(gIigsRxRead + n); + jlgIigsRxHead = head; + jlgIigsRxRead = (uint16_t)(jlgIigsRxRead + n); serialRxRelease(); return n; } @@ -601,11 +601,11 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { uint16_t n; for (n = 0u; n < len; n++) { - // iigsSerialSend drains the receiver too: a 6551 status read clears its interrupt request, so + // jlIigsSerialSend drains the receiver too: a 6551 status read clears its interrupt request, so // whoever reads it must take the byte. - serialTxWaitStart(&wait, IIGS_TX_POLL_LIMIT); - while (iigsSerialSend(buf[n]) == 0u) { - if (serialTxStalled(&wait)) { + jlSerialTxWaitStart(&wait, IIGS_TX_POLL_LIMIT); + while (jlIigsSerialSend(buf[n]) == 0u) { + if (jlSerialTxStalled(&wait)) { return n; } } diff --git a/src/iigs/serialIrq.s b/src/iigs/serialIrq.s index 1c0f6c0..1e8c9c6 100644 --- a/src/iigs/serialIrq.s +++ b/src/iigs/serialIrq.s @@ -12,52 +12,52 @@ ; each source to a Misc Tool Set vector, which serial.c points here with ; GetVector/SetVector: ; -; $0009 serial ($E1/0024) -> iigsSccIsr. Called straight from INTHNDLR's +; $0009 serial ($E1/0024) -> jlIigsSccIsr. Called straight from INTHNDLR's ; fast path, for an SCC interrupt AppleTalk refused and whose RR3 bits ; are in SERFLAG ($E1/0104), with M=X=8, DBR=$E1, D=0 and ; A = RR3 & SERFLAG -- and BEFORE the ROM has switched to its own ; stack or normalised STATEREG, so this runs on the interrupted ; program's stack with whatever RAMRD/RAMWRT it had set (the ROM's ; own serial handler, serialdrvr SERPINT, saves and clears them). -; $0017 other ($E1/005C) -> iigsSscIsr. Called last, for an interrupt no +; $0017 other ($E1/005C) -> jlIigsSscIsr. Called last, for an interrupt no ; built-in source claimed (a slot card), after the ROM has saved the ; machine and normalised it: M=X=8, DBR=0, D=0, main memory. ; ; Both return carry clear for "serviced" and M=X=8 (the ROM ROR/PLAs a byte ; right after the call). Anything that is not ours goes on to the vector's -; previous owner by an RTL to gIigsSerChain (its address minus one), with the +; previous owner by an RTL to jlgIigsSerChain (its address minus one), with the ; registers as the ROM left them. ; ; Receive ring protocol, single producer / single consumer: the producer ; (ringPut, only ever run by an interrupt handler or under SEI) stores the -; byte, then publishes gIigsRxTail with one 16-bit store; the mainline only -; ever moves gIigsRxHead. C globals are reached with long addressing, so the +; byte, then publishes jlgIigsRxTail with one 16-bit store; the mainline only +; ever moves jlgIigsRxHead. C globals are reached with long addressing, so the ; data bank does not matter; the I/O is DBR-relative through Y, and every path -; sets DBR to a bank where $C0xx is I/O ($E1 here, or $00 in iigsSscIsr where +; sets DBR to a bank where $C0xx is I/O ($E1 here, or $00 in jlIigsSscIsr where ; the ROM has I/O shadowing on). ; -; Port layout (gIigsSerPort, from serial.c): +; Port layout (jlgIigsSerPort, from serial.c): ; SCC Y = channel command register ($C038 B / $C039 A); data at 2,y. ; 6551 Y = data register ($C0n8); status at 1,y, command at 2,y. ; -; Flow control (gIigsSerFlow): under XON/XOFF ringPut takes the peer's XOFF -; and XON out of the data into gIigsTxPaused, and a ring three quarters full -; queues an XOFF in gIigsTxCtl, which the end of every drain puts in the +; Flow control (jlgIigsSerFlow): under XON/XOFF ringPut takes the peer's XOFF +; and XON out of the data into jlgIigsTxPaused, and a ring three quarters full +; queues an XOFF in jlgIigsTxCtl, which the end of every drain puts in the ; transmitter once it is free (serial.c queues the XON). Under RTS/CTS a full ; ring drops a 6551's RTS; the SCC ports have no RTS pin. .section .text.iigsSerialIrq,"ax" - .globl iigsSccIsr - .globl iigsSscIsr - .globl iigsSerialSend - .globl iigsSerialService - .globl iigsSerialIsrAddress - .globl iigsIrqOff - .globl iigsIrqRestore + .globl jlIigsSccIsr + .globl jlIigsSscIsr + .globl jlIigsSerialSend + .globl jlIigsSerialService + .globl jlIigsSerialIsrAddress + .globl jlIigsIrqOff + .globl jlIigsIrqRestore -SER_KIND_SCC = 1 ; gIigsSerKind values (IigsSerKindE in serial.c) +SER_KIND_SCC = 1 ; jlgIigsSerKind values (IigsSerKindE in serial.c) SER_KIND_ACIA = 2 -FLOW_RTSCTS = 1 ; gIigsSerFlow values (jlSerialFlowE) +FLOW_RTSCTS = 1 ; jlgIigsSerFlow values (jlSerialFlowE) FLOW_XONXOFF = 2 XON_CHAR = 0x11 XOFF_CHAR = 0x13 @@ -81,7 +81,7 @@ STATE_MAIN_MEM = 0x8F ; clears PAGE2, RAMRD, RAMWRT (SERPINT's mas ; ---------------------------------------------------------------- ; SCC receive interrupt (serial vector). ; ---------------------------------------------------------------- -iigsSccIsr: +jlIigsSccIsr: .a8 .i8 pha ; RR3 & SERFLAG, handed on if we chain @@ -92,12 +92,12 @@ iigsSccIsr: lda #3 sta SCC_A_CTRL ; point at RR3 (DBR = $E1) lda SCC_A_CTRL - and >gIigsSerIpMask + and >jlgIigsSerIpMask beq sccIsrPass ; none of ours pending rep #0x30 .a16 .i16 - lda >gIigsSerPort + lda >jlgIigsSerPort tay sep #0x20 .a8 @@ -106,8 +106,8 @@ iigsSccIsr: sta SCC_A_CTRL lda SCC_A_CTRL and >SERFLAG - ora >gIigsSerIpMask - eor >gIigsSerIpMask ; = pending bits the previous owner asked for + ora >jlgIigsSerIpMask + eor >jlgIigsSerIpMask ; = pending bits the previous owner asked for bne sccIsrPass pla sta >STATEREG @@ -127,7 +127,7 @@ sccIsrPass: ; ---------------------------------------------------------------- ; 6551 receive interrupt (other-interrupt vector). ; ---------------------------------------------------------------- -iigsSscIsr: +jlIigsSscIsr: .a8 .i8 phb @@ -137,10 +137,10 @@ iigsSscIsr: rep #0x30 .a16 .i16 - lda >gIigsSerKind + lda >jlgIigsSerKind cmp #SER_KIND_ACIA bne sscIsrPass - lda >gIigsSerPort + lda >jlgIigsSerPort tay sep #0x20 .a8 @@ -162,17 +162,17 @@ sscIsrPass: ; RTL there. A is parked in B, which 8-bit loads leave alone. chain: xba - lda >gIigsSerChain+2 + lda >jlgIigsSerChain+2 pha - lda >gIigsSerChain+1 + lda >jlgIigsSerChain+1 pha - lda >gIigsSerChain + lda >jlgIigsSerChain pha xba rtl ; ---------------------------------------------------------------- -; uint16_t iigsSerialService(void) +; uint16_t jlIigsSerialService(void) ; ; The mainline's view of the open port: under SEI, drain whatever the chip ; holds into the ring (the same drain the handlers run, which also sends a @@ -181,7 +181,7 @@ chain: ; here or in the handler, both of which then take the byte. Returns 0 when no ; port is open. ; ---------------------------------------------------------------- -iigsSerialService: +jlIigsSerialService: php sei phb @@ -191,9 +191,9 @@ iigsSerialService: rep #0x30 .a16 .i16 - lda >gIigsSerPort + lda >jlgIigsSerPort tay - lda >gIigsSerKind + lda >jlgIigsSerKind cmp #SER_KIND_SCC beq svcScc cmp #SER_KIND_ACIA @@ -221,7 +221,7 @@ svcDone: rtl ; ---------------------------------------------------------------- -; uint16_t iigsSerialSend(uint16_t byte) +; uint16_t jlIigsSerialSend(uint16_t byte) ; ; Under SEI: drain the chip (which sends a queued XOFF / XON first), then ; put the low byte of A in the transmitter if it is empty and the peer lets @@ -230,7 +230,7 @@ svcDone: ; caller retries. Check and store are one masked step, so a handler's XOFF / ; XON cannot land in the transmitter between them. ; ---------------------------------------------------------------- -iigsSerialSend: +jlIigsSerialSend: .a16 .i16 php @@ -240,9 +240,9 @@ iigsSerialSend: pea 0xE1E1 plb plb - lda >gIigsSerPort + lda >jlgIigsSerPort tay - lda >gIigsSerKind + lda >jlgIigsSerKind cmp #SER_KIND_SCC beq sendScc cmp #SER_KIND_ACIA @@ -255,7 +255,7 @@ sendScc: lda 0,y ; RR0, after any XOFF / XON went in and #SCC_RR0_TX_EMPTY beq sendNo8 - lda >gIigsSerFlow + lda >jlgIigsSerFlow cmp #FLOW_RTSCTS bne sendSccData lda 0,y @@ -273,9 +273,9 @@ sendAcia: and #ACIA_ST_TX_EMPTY beq sendNo8 sendReady: - lda >gIigsTxCtl ; a queued XOFF / XON goes first + lda >jlgIigsTxCtl ; a queued XOFF / XON goes first bne sendNo8 - lda >gIigsTxPaused + lda >jlgIigsTxPaused bne sendNo8 lda 1,s sta 0,y ; the data register @@ -324,15 +324,15 @@ sccErrReset: sta 0,y ; ext/status IP, which only this clears bra sccDrain sccDrainDone: - lda >gIigsTxCtl + lda >jlgIigsTxCtl beq sccCtlDone lda 0,y ; RR0 and #SCC_RR0_TX_EMPTY beq sccCtlDone ; busy: the next drain sends it - lda >gIigsTxCtl + lda >jlgIigsTxCtl sta 2,y lda #0 - sta >gIigsTxCtl + sta >jlgIigsTxCtl sccCtlDone: rts @@ -355,11 +355,11 @@ sscDrainDone: bit #ACIA_ST_TX_EMPTY beq sscCtlDone pha ; the status - lda >gIigsTxCtl + lda >jlgIigsTxCtl beq sscCtlNone sta 0,y lda #0 - sta >gIigsTxCtl + sta >jlgIigsTxCtl pla and #(0xFF ^ ACIA_ST_TX_EMPTY) ; the transmitter is full again rts @@ -370,14 +370,14 @@ sscCtlDone: ; A (8-bit) = received byte. Store it, then publish the tail; a full ring ; drops it and counts the drop. Under XON/XOFF the peer's XOFF / XON is -; flow, not data: it sets gIigsTxPaused (XON ^ XON = 0, XOFF ^ XON nonzero) +; flow, not data: it sets jlgIigsTxPaused (XON ^ XON = 0, XOFF ^ XON nonzero) ; and is not stored. A ring three quarters full holds the peer off ; (rxThrottle). Clobbers X. ringPut: .a8 .i16 pha ; the byte - lda >gIigsSerFlow + lda >jlgIigsSerFlow cmp #FLOW_XONXOFF bne ringPutStore lda 1,s @@ -387,35 +387,35 @@ ringPut: bne ringPutStore ringPutFlow: eor #XON_CHAR - sta >gIigsTxPaused + sta >jlgIigsTxPaused pla rts ringPutStore: rep #0x20 .a16 - lda >gIigsRxTail + lda >jlgIigsRxTail tax ; X = slot to fill inc a - and >gIigsRxRingMask - cmp >gIigsRxHead + and >jlgIigsRxRingMask + cmp >jlgIigsRxHead beq ringPutFull pha ; the next tail; the byte is now at 3,s sep #0x20 .a8 lda 3,s - sta >gIigsRxRing,x ; store BEFORE publishing + sta >jlgIigsRxRing,x ; store BEFORE publishing rep #0x20 .a16 pla - sta >gIigsRxTail - lda >gIigsRxPumped + sta >jlgIigsRxTail + lda >jlgIigsRxPumped inc a - sta >gIigsRxPumped - lda >gIigsRxTail + sta >jlgIigsRxPumped + lda >jlgIigsRxTail sec - sbc >gIigsRxHead - and >gIigsRxRingMask - cmp >gIigsRxHighWater + sbc >jlgIigsRxHead + and >jlgIigsRxRingMask + cmp >jlgIigsRxHighWater bcc ringPutDone sep #0x20 .a8 @@ -423,10 +423,10 @@ ringPutStore: bra ringPutDone ringPutFull: .a16 - lda >gIigsRxDropped + lda >jlgIigsRxDropped inc a beq ringPutDone ; saturated at 0xFFFF - sta >gIigsRxDropped + sta >jlgIigsRxDropped ringPutDone: sep #0x20 .a8 @@ -439,60 +439,60 @@ ringPutDone: rxThrottle: .a8 .i16 - lda >gIigsRxThrottled + lda >jlgIigsRxThrottled bne rxThrottleDone - lda >gIigsSerFlow + lda >jlgIigsSerFlow cmp #FLOW_XONXOFF beq rxThrottleXoff cmp #FLOW_RTSCTS bne rxThrottleDone - lda >gIigsSerKind + lda >jlgIigsSerKind cmp #SER_KIND_ACIA bne rxThrottleDone lda #1 - sta >gIigsRxThrottled - lda >gIigsAciaCmd + sta >jlgIigsRxThrottled + lda >jlgIigsAciaCmd and #ACIA_CMD_RTS_OFF sta 2,y ; 6551 command register rts rxThrottleXoff: lda #1 - sta >gIigsRxThrottled + sta >jlgIigsRxThrottled lda #XOFF_CHAR - sta >gIigsTxCtl ; the drain's end sends it + sta >jlgIigsTxCtl ; the drain's end sends it rxThrottleDone: rts -; Saturating gIigsRxOverruns++. M=8 in and out; clobbers A. +; Saturating jlgIigsRxOverruns++. M=8 in and out; clobbers A. countOverrun: .a8 rep #0x20 .a16 - lda >gIigsRxOverruns + lda >jlgIigsRxOverruns inc a beq countOverrunDone - sta >gIigsRxOverruns + sta >jlgIigsRxOverruns countOverrunDone: sep #0x20 .a8 rts ; ---------------------------------------------------------------- -; uint32_t iigsSerialIsrAddress(uint16_t kind) +; uint32_t jlIigsSerialIsrAddress(uint16_t kind) ; ; The 24-bit address of the handler for `kind`, for SetVector: the offset is ; a same-section immediate (the loader bank-aligns segments, so it equals the ; runtime offset) and the bank is K. Returned in A (low 16) : X (bank). ; ---------------------------------------------------------------- -iigsSerialIsrAddress: +jlIigsSerialIsrAddress: .a16 .i16 cmp #SER_KIND_ACIA beq isrAddrAcia - lda #iigsSccIsr + lda #jlIigsSccIsr bra isrAddrBank isrAddrAcia: - lda #iigsSscIsr + lda #jlIigsSscIsr isrAddrBank: pha phk @@ -507,15 +507,15 @@ isrAddrBank: rtl ; ---------------------------------------------------------------- -; uint16_t iigsIrqOff(void) -- SEI; returns the caller's P -; void iigsIrqRestore(uint16_t p) -- puts that P (and its I bit) back +; uint16_t jlIigsIrqOff(void) -- SEI; returns the caller's P +; void jlIigsIrqRestore(uint16_t p) -- puts that P (and its I bit) back ; ; For the mainline's two-access SCC register-pointer sequences: the ROM's ; interrupt entry reads RR3 through the same chip-wide pointer on EVERY ; interrupt, so an unmasked pointer write can land as data in the wrong ; register. ; ---------------------------------------------------------------- -iigsIrqOff: +jlIigsIrqOff: .a16 .i16 php @@ -528,7 +528,7 @@ iigsIrqOff: and #0x00FF rtl -iigsIrqRestore: +jlIigsIrqRestore: .a16 .i16 sep #0x20 diff --git a/src/iigs/sfxCopy.s b/src/iigs/sfxCopy.s index 86fdefa..8cd22db 100644 --- a/src/iigs/sfxCopy.s +++ b/src/iigs/sfxCopy.s @@ -5,7 +5,7 @@ ; ==================================================================== ; sfxCopy.s - stage a sound effect's sample for the DOC (llvm-mc w65816) ; -; iigsSfxCopyUnsigned(unsigned char *dst, const uint8_t *src, +; jlIigsSfxCopyUnsigned(unsigned char *dst, const uint8_t *src, ; uint16_t length, uint16_t padTo) ; ; The inner loop of audio_full.c's sfxCopyUnsigned, which says why: the @@ -56,9 +56,9 @@ scFixHi\n: bra scBackHi\n .endm -.section .text.iigsSfxCopyUnsigned,"ax" -.globl iigsSfxCopyUnsigned -iigsSfxCopyUnsigned: +.section .text.jlIigsSfxCopyUnsigned,"ax" +.globl jlIigsSfxCopyUnsigned +jlIigsSfxCopyUnsigned: php phd rep #0x30 diff --git a/src/iigs/spriteEmitIigs.c b/src/iigs/spriteEmitIigs.c index c8a806c..1452e77 100644 --- a/src/iigs/spriteEmitIigs.c +++ b/src/iigs/spriteEmitIigs.c @@ -30,7 +30,7 @@ // DBR is preserved via PHB/PLB (MVN leaves DBR = dst bank); // returns via RTL with M=16. // -// DRAW body shape (see spriteEmitDrawIigs for the M=8/M=16 dual-path +// DRAW body shape (see jlSpriteEmitDrawIigs for the M=8/M=16 dual-path // details): // opaque byte: lda #imm; sta abs,Y // mixed byte: lda abs,Y; and #~mask; ora #val; sta abs,Y @@ -189,7 +189,7 @@ static bool rowHasPixels(const jlSpriteT *sp, uint16_t row) { // The rows from the first with an opaque pixel through the last, which // the save and restore cover (spriteInternal.h); the whole height for a // sprite with none, so a save never covers zero rows. -void spriteContentRowsIigs(jlSpriteT *sp) { +void jlSpriteContentRowsIigs(jlSpriteT *sp) { uint16_t first; uint16_t last; uint16_t row; @@ -220,7 +220,7 @@ void spriteContentRowsIigs(jlSpriteT *sp) { // advances both by copyBytes per row. Backup rows are contiguous in // memory so Y is already correct for the next row; screen rows are // JL_SURFACE_BYTES_PER_ROW apart so X needs an explicit ADC between rows. -uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +uint32_t jlSpriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint16_t spriteBytesPerRow; uint16_t copyBytes; @@ -241,7 +241,7 @@ uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin // The prologue leaves X = backup low offset, Y = screen low offset. // Backup is contiguous so X advances correctly via MVN; screen needs // explicit advance, so Y is the one we ADC. -uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +uint32_t jlSpriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint16_t spriteBytesPerRow; uint16_t copyBytes; @@ -290,7 +290,10 @@ uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, // // Mixed bytes always run on the M=8 path because the AND/ORA in // M=16 would clobber the adjacent byte. -uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +// +// A store of the value A already holds skips its LDA #: a solid run of one +// colour costs 6c / 3 bytes a pair (M=16) or 5c / 3 bytes a byte (M=8). +uint32_t jlSpriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t cursor; uint16_t row; uint16_t col; @@ -303,6 +306,10 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin uint8_t nextValue; uint8_t nextOpaqueMask; bool wide; + bool aLowKnown; + bool aHighKnown; + uint8_t aLow; + uint8_t aHigh; if (shift > 1u) { return 0u; @@ -313,6 +320,14 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin spriteBytesPerRow = (uint16_t)(sp->widthTiles * JL_TILE_BYTES_PER_ROW); destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); // shift is 0 or 1 wide = false; + // What A holds, byte by byte, so a store of the value it already holds + // skips its load: an M=16 load sets both bytes, an M=8 one the low byte + // only, and a mixed byte's read-modify-write leaves the low byte unknown. + // Unknown on entry. + aLowKnown = false; + aHighKnown = false; + aLow = 0u; + aHigh = 0u; // C-ABI prologue. The runtime calls this routine via a normal C // function pointer -- void draw(uint8_t *dstRow0) -- so the compiler @@ -362,15 +377,18 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin out[cursor++] = 0x20; wide = true; } - { - uint16_t pair = (uint16_t)(((uint16_t)nextValue << 8) | value); + if (!aLowKnown || !aHighKnown || aLow != value || aHigh != nextValue) { out[cursor++] = 0xA9; // LDA #imm16 - out[cursor++] = (uint8_t)(pair & 0xFFu); - out[cursor++] = (uint8_t)((pair >> 8) & 0xFFu); - out[cursor++] = 0x99; // STA abs,Y - out[cursor++] = (uint8_t)(absOffset & 0xFFu); - out[cursor++] = (uint8_t)((absOffset >> 8) & 0xFFu); + out[cursor++] = value; + out[cursor++] = nextValue; + aLowKnown = true; + aHighKnown = true; + aLow = value; + aHigh = nextValue; } + out[cursor++] = 0x99; // STA abs,Y + out[cursor++] = (uint8_t)(absOffset & 0xFFu); + out[cursor++] = (uint8_t)((absOffset >> 8) & 0xFFu); col++; // consumed col+1 continue; } @@ -385,8 +403,12 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin } if (opaqueMask == 0xFFu) { - out[cursor++] = 0xA9; - out[cursor++] = value; + if (!aLowKnown || aLow != value) { + out[cursor++] = 0xA9; + out[cursor++] = value; + aLowKnown = true; + aLow = value; + } out[cursor++] = 0x99; out[cursor++] = (uint8_t)(absOffset & 0xFFu); out[cursor++] = (uint8_t)((absOffset >> 8) & 0xFFu); @@ -401,6 +423,7 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin out[cursor++] = 0x99; out[cursor++] = (uint8_t)(absOffset & 0xFFu); out[cursor++] = (uint8_t)((absOffset >> 8) & 0xFFu); + aLowKnown = false; } } } diff --git a/src/iigs/spriteInterp.s b/src/iigs/spriteInterp.s index 2983a86..907aaa0 100644 --- a/src/iigs/spriteInterp.s +++ b/src/iigs/spriteInterp.s @@ -5,7 +5,7 @@ ; ==================================================================== ; spriteInterp.s - the sprite interpreter's rows (llvm-mc w65816) ; -; iigsSpriteInterpRows(IigsSpriteRowsT *job) +; jlIigsSpriteInterpRows(IigsSpriteRowsT *job) ; ; The row loop of src/core/sprite.c's chunky interpreter, the draw for an ; uncompiled sprite or one partly off the surface. sprite.c clips the draw @@ -13,11 +13,11 @@ ; nibble into the low nibble of the first destination byte), whole ; destination bytes, and an odd trailing pixel (a source nibble into the ; high nibble of the byte after them). A whole byte is -; dst[i] = (dst[i] & gSpriteNibbleKeep[b]) | b, skipped when b == 0 +; dst[i] = (dst[i] & jlgSpriteNibbleKeep[b]) | b, skipped when b == 0 ; with b the source byte, or src[i]'s low nibble and src[i + 1]'s high one ; when the job is shifted (source and destination nibble parity differ): ; one 16-bit load reads both, and XBA with four LSRs leaves b in the low -; byte. gSpriteNibbleKeep keeps the destination nibble wherever the source +; byte. jlgSpriteNibbleKeep keeps the destination nibble wherever the source ; pixel is transparent. ; ; The C loop paid a JSL, a frame and two 24-bit pointer rebuilds a row; @@ -27,7 +27,7 @@ ; ; ABI: arg0 job in A:X (A = its bank-0 address). The job's pointers and row ; count advance in place. A, X and Y are clobbered; P, D and DBR are -; preserved. DBR is 0 (C's small model), so gSpriteNibbleKeep is +; preserved. DBR is 0 (C's small model), so jlgSpriteNibbleKeep is ; reached absolute,X. ; ==================================================================== @@ -60,9 +60,9 @@ DST_STRIDE = 160 ; JL_SURFACE_BYTES_PER_ROW 1: .endm -.section .text.iigsSpriteInterpRows,"ax" -.globl iigsSpriteInterpRows -iigsSpriteInterpRows: +.section .text.jlIigsSpriteInterpRows,"ax" +.globl jlIigsSpriteInterpRows +jlIigsSpriteInterpRows: php phd rep #0x30 @@ -110,7 +110,7 @@ srBytes: beq srBytesNext ; both pixels transparent tax lda [SR_PDST], y - and gSpriteNibbleKeep, x + and jlgSpriteNibbleKeep, x ora [SR_PSRC], y sta [SR_PDST], y srBytesNext: @@ -132,7 +132,7 @@ srShifted: tax ; X = b sep #0x20 .a8 - lda gSpriteNibbleKeep, x + lda jlgSpriteNibbleKeep, x and [SR_PDST], y sta SR_TMP txa ; A = b diff --git a/src/m68k/m68kAsm.h b/src/m68k/m68kAsm.h new file mode 100644 index 0000000..661a023 --- /dev/null +++ b/src/m68k/m68kAsm.h @@ -0,0 +1,16 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Calling the shared 68000 asm (src/m68k) from an inline asm jsr: the C name +// of a symbol takes the toolchain's prefix, __USER_LABEL_PREFIX__ (an +// underscore on mint and AmigaOS, none on the X68000's ELF), and each asm +// entry point carries both spellings. +#ifndef JOEY_M68K_ASM_H +#define JOEY_M68K_ASM_H + +#define M68K_ASM_STR2(_x) #_x +#define M68K_ASM_STR(_x) M68K_ASM_STR2(_x) +#define M68K_ASM_SYM(_name) M68K_ASM_STR(__USER_LABEL_PREFIX__) #_name + +#endif diff --git a/src/m68k/m68kCSym.inc b/src/m68k/m68kCSym.inc new file mode 100644 index 0000000..a7c0cf6 --- /dev/null +++ b/src/m68k/m68kCSym.inc @@ -0,0 +1,25 @@ +| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +| See LICENSE.md for license information. +| SPDX-License-Identifier: MIT +| +| C_OP: shared 68000 asm naming a C symbol. gcc-amigaos spells one with a +| leading underscore and elf2x68k without, and the shared asm sees no +| preprocessor, so the build says which (JL_ASM_USCORE, make/amiga.mk) and +| C_OP writes `op sym` or `op sym,reg` either way. (The asm's own entry +| points need none of this: each carries both spellings.) + + .macro C_OP op, sym, reg + .ifdef JL_ASM_USCORE + .ifb \reg + \op _\sym + .else + \op _\sym,\reg + .endif + .else + .ifb \reg + \op \sym + .else + \op \sym,\reg + .endif + .endif + .endm diff --git a/src/m68k/modMix.s b/src/m68k/modMix.s index 7fd53b1..fbaa0eb 100644 --- a/src/m68k/modMix.s +++ b/src/m68k/modMix.s @@ -26,8 +26,8 @@ .text .even - .globl modMixRunU8 - .globl _modMixRunU8 + .globl jlModMixRunU8 + .globl _jlModMixRunU8 .equ RUN_SRC, 0 .equ RUN_TAB, 4 @@ -49,10 +49,10 @@ addx.w %d6,%d5 | ... and carry the fraction's overflow .endm -| void modMixRunU8(MixRunT *run): the run starts at index 0 of its src, and +| void jlModMixRunU8(MixRunT *run): the run starts at index 0 of its src, and | its count is at most the C's MIX_CHUNK, so it fits a word. -modMixRunU8: -_modMixRunU8: +jlModMixRunU8: +_jlModMixRunU8: movem.l %d2-%d7/%a2-%a3,-(%sp) movea.l ARG_RUN(%sp),%a3 move.l RUN_COUNT(%a3),%d7 diff --git a/src/m68k/planarFill.s b/src/m68k/planarFill.s index 099102e..7b6abd5 100644 --- a/src/m68k/planarFill.s +++ b/src/m68k/planarFill.s @@ -12,8 +12,8 @@ | Home of the span-filling primitives that write a run of pixels into all | four plane buffers at once. Currently: | -| surface68kFillCircle jlFillCircle: the whole exact-disk walk -| surface68kFillRect jlFillRect and jlSurfaceClear: any on-surface rect +| jlSurface68kFillCircle jlFillCircle: the whole exact-disk walk +| jlSurface68kFillRect jlFillRect and jlSurfaceClear: any on-surface rect | | The circle's span is a partial left byte, a run of whole middle bytes, and a | partial right byte, in each of four planes. (The rect works in words and @@ -42,12 +42,16 @@ | elf2x68k does not, and one extra unused symbol is cheaper than forking the | file over a naming convention. + .include "m68kCSym.inc" + .text .even - .globl surface68kFillCircle - .globl _surface68kFillCircle - .globl surface68kFillRect - .globl _surface68kFillRect + .globl jlSurface68kFillCircle + .globl _jlSurface68kFillCircle + .globl jlSurface68kFillRect + .globl _jlSurface68kFillRect + .globl jlFillRect + .globl _jlFillRect | Bytes per plane row: JL_SURFACE_WIDTH / 8, which is AMIGA_BYTES_PER_ROW and | X68K_BYTES_PER_ROW both. Every planar surface on either port is @@ -231,10 +235,10 @@ .endm -| void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, +| void jlSurface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, | uint16_t r, uint16_t color); -surface68kFillCircle: -_surface68kFillCircle: +jlSurface68kFillCircle: +_jlSurface68kFillCircle: movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes sub.l #FC_LOCAL,%sp move.l FC_PLANES(%sp),%a0 @@ -437,13 +441,13 @@ fcDone: .endm -| void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, +| void jlSurface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, | uint16_t w, uint16_t h, uint16_t color); | | w and h must both be at least 1 and the rect on-surface -- draw.c clips | before it calls, so an empty or overhanging rect never arrives. -surface68kFillRect: -_surface68kFillRect: +jlSurface68kFillRect: +_jlSurface68kFillRect: movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes move.l FR_PLANES(%sp),%a6 move.w FR_X(%sp),%d0 | left @@ -544,3 +548,177 @@ gFillWordLut: .word 0x00FF, 0x007F, 0x003F, 0x001F, 0x000F, 0x0007, 0x0003, 0x0001 .word 0x8000, 0xC000, 0xE000, 0xF000, 0xF800, 0xFC00, 0xFE00, 0xFF00 .word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF + + +| void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, +| uint8_t colorIndex); +| +| The public call itself (JL_HAS_FILL_RECT_ENTRY; draw.c's is jlFillRectClip): +| an on-surface rect one row high is filled and marked here, and any other +| goes on, unchanged, to jlFillRectClip, which clips it and fills it the +| port's way (the Amiga's blitter, jlSurface68kFillRect on the X68000). A +| planar picture is drawn as such rows -- JoeyAGI pushes a picture a run of +| one colour at a time -- and each paid the C wrapper, an eleven-register +| fill entry, a multiply for the row and a call for the mark, several times +| the few word stores the row needs; on the Amiga a blitter set-up and wait +| besides. Here a row is its word masks, then each plane's run ORed into a +| set plane or cleared from a clear one, and the band widened in place. +| +| cdecl: every argument in a 4-byte stack slot, an int16_t / uint16_t in its +| low word and a uint8_t in its low byte. + .text + .even + .equ RECT_S, 4 + .equ RECT_X, 10 + .equ RECT_Y, 14 + .equ RECT_W, 18 + .equ RECT_H, 22 + .equ RECT_COLOR, 27 + .equ RECT_SAVED, 16 | d2-d3/a2-a3 below the arguments + .equ RECT_ROWS, 200 | JL_SURFACE_HEIGHT + .equ SURFACE_PORT, 4 | offsetof(jlSurfaceT, portData) (planarTile.h checks it) + .equ WORD_TO_BAND, 2 | x >> 2: a pixel's 16-bit stage word (its dirty band unit) + .equ COLOR_MASK, 0x0F + +| A row one word wide in colour \colour: each plane's word ORed with the +| mask (d2) where the colour has its bit, ANDed with its complement (d3) +| where not; a0-a3 = planes 0-3, d0 = the word's byte in a plane. + .macro ROWTHIN colour + .irp plane, 0, 1, 2, 3 + .if (\colour >> \plane) & 1 + or.w %d2,0(%a\plane,%d0.w) + .else + and.w %d3,0(%a\plane,%d0.w) + .endif + .endr + movem.l (%sp)+,%d2-%d3/%a2-%a3 + rts + .endm + +| One plane of a row several words wide: the left edge (d2), d1 whole words +| between, the right edge (d3); a clear plane ANDs the edges' complements and +| clears the words between. a2 walks the planes array, d4 = the colour +| (shifted down a plane a time), d5 = scratch. + .macro ROWPLANE_WIDE + movea.l (%a2)+,%a1 + adda.w %d0,%a1 + move.w %d1,%d5 + lsr.b #1,%d4 + bcc.s 4f + or.w %d2,(%a1)+ + bra.s 2f +1: move.w #0xFFFF,(%a1)+ +2: dbra %d5,1b + or.w %d3,(%a1) + bra.s 7f +4: not.w %d2 + not.w %d3 + and.w %d2,(%a1)+ + bra.s 6f +5: clr.w (%a1)+ +6: dbra %d5,5b + and.w %d3,(%a1) + not.w %d2 + not.w %d3 +7: + .endm + +| Any rect but an on-surface row: jlFillRectClip, unchanged. Here, above the +| entry, so its checks reach it with short branches. +.Lr_clip: + C_OP jmp, jlFillRectClip + +jlFillRect: +_jlFillRect: + cmpi.w #1,RECT_H(%sp) + bne.s .Lr_clip | not one row + move.w RECT_X(%sp),%d0 + cmpi.w #ROW_PIXELS,%d0 + bcc.s .Lr_clip | x off the row (negative is huge) + move.w RECT_W(%sp),%d1 + beq.s .Lr_clip + add.w %d0,%d1 + bcs.s .Lr_clip + cmpi.w #ROW_PIXELS,%d1 + bhi.s .Lr_clip | past the right edge + movea.w RECT_Y(%sp),%a1 + cmpa.w #RECT_ROWS,%a1 + bcc.s .Lr_clip | y off the surface (negative is huge) + movea.l RECT_S(%sp),%a0 + cmpa.w #0,%a0 + beq.s .Lr_clip + movem.l %d2-%d3/%a2-%a3,-(%sp) + subq.w #1,%d1 | the right pixel + C_OP cmpa.l, jlgStage, %a0 + bne.s .Lr_fill + | The stage: widen row y's band to [x >> 2, right >> 2]. + C_OP lea, jlgStageMinWord, %a2 + move.w %d0,%d2 + lsr.w #WORD_TO_BAND,%d2 + cmp.b 0(%a2,%a1.w),%d2 + bcc.s 1f + move.b %d2,0(%a2,%a1.w) +1: C_OP lea, jlgStageMaxWord, %a2 + move.w %d1,%d2 + lsr.w #WORD_TO_BAND,%d2 + cmp.b 0(%a2,%a1.w),%d2 + bls.s .Lr_fill + move.b %d2,0(%a2,%a1.w) +.Lr_fill: + movea.l SURFACE_PORT(%a0),%a2 | the planes (they lead the planar data) + lea gFillWordLut,%a0 + move.w %d0,%d2 + and.w #PIXEL_IN_WORD,%d2 + add.w %d2,%d2 + move.w 0(%a0,%d2.w),%d2 | left mask + move.w %d1,%d3 + and.w #PIXEL_IN_WORD,%d3 + add.w %d3,%d3 + move.w WORD_LUT_RIGHT(%a0,%d3.w),%d3 | right mask + lsr.w #WORD_SHIFT,%d0 | first word + lsr.w #WORD_SHIFT,%d1 | last word + sub.w %d0,%d1 | words - 1 + add.w %d0,%d0 + adda.w %a1,%a1 + lea gRowByteLut,%a0 + add.w 0(%a0,%a1.w),%d0 | y * 40 + the first word's byte + tst.w %d1 + bne.w .Lr_wide + and.w %d3,%d2 | one word: both edges + move.w %d2,%d3 + not.w %d3 + moveq #COLOR_MASK,%d1 + and.b RECT_SAVED+RECT_COLOR(%sp),%d1 + add.w %d1,%d1 + add.w %d1,%d1 + movem.l (%a2),%a0-%a3 | the four planes + jmp .Lr_thinTab(%pc,%d1.w) +.Lr_thinTab: + .irp colour, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + bra.w .Lr_thin\colour + .endr + .irp colour, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 +.Lr_thin\colour: + ROWTHIN \colour + .endr +.Lr_wide: + movem.l %d4-%d5,-(%sp) + move.b RECT_SAVED+8+RECT_COLOR(%sp),%d4 + subq.w #1,%d1 | whole middle words, then the right edge's + ROWPLANE_WIDE + ROWPLANE_WIDE + ROWPLANE_WIDE + ROWPLANE_WIDE + movem.l (%sp)+,%d4-%d5 + movem.l (%sp)+,%d2-%d3/%a2-%a3 + rts + +| A row's first byte in a plane: y * 40. + .data + .even +gRowByteLut: + .set rowY, 0 + .rept RECT_ROWS + .word rowY * ROW_BYTES + .set rowY, rowY + 1 + .endr diff --git a/src/m68k/planarHash.c b/src/m68k/planarHash.c new file mode 100644 index 0000000..0e985c3 --- /dev/null +++ b/src/m68k/planarHash.c @@ -0,0 +1,109 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// The planar ports' jlpSurfaceHash (Amiga, X68000, Atari ST): the pixels +// hashed as the chunky bytes they would be, so a planar port's hash matches +// the chunky ports' for the same picture. A plane byte holds one plane's bit +// of 8 pixels; gSpread[b] puts byte b's bits at the low bit of those pixels' +// nibbles, left pixel highest, so four lookups or'd at shifts 0 to 3 are the +// 4 chunky bytes of the 8 pixels. Decoding a pixel at a time cost four bit +// tests a pixel, and was most of an UBER run. +// +// Both layouts reach plane p's byte of column c (0..39) in a row at +// planes[p] + row * rowBytes + (c >> 1) * pairStep + (c & 1): separate +// planes are 40 bytes a row with pairStep 2, and the ST's interleaved +// groups put plane p's word at 2 * p, a group (two columns) every 8 bytes. + +#include <stdbool.h> +#include <stdint.h> + +#include "surfaceInternal.h" + +#define SPREAD_VALUES 256u +#define PIXELS_PER_PLANE_BYTE 8u +#define NIBBLE_BITS 4u +#define COLUMN_PAIRS (JL_SURFACE_WIDTH / 16) + +static inline __attribute__((always_inline)) void mixPlaneBytes(uint16_t *lo, uint16_t *hi, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3); +static void spreadBuild(void); + +static uint32_t gSpread[SPREAD_VALUES]; +static bool gSpreadBuilt = false; + + +uint32_t jlPlanarSurfaceHash(const jlSurfaceT *s, const uint8_t *const planes[], uint16_t rowBytes, uint16_t pairStep) { + const uint8_t *p0; + const uint8_t *p1; + const uint8_t *p2; + const uint8_t *p3; + uint32_t rowOff; + uint16_t lo; + uint16_t hi; + uint16_t row; + uint16_t pair; + + if (!gSpreadBuilt) { + spreadBuild(); + } + lo = SURFACE_HASH_SEED_LO; + hi = SURFACE_HASH_SEED_HI; + for (row = 0u; row < JL_SURFACE_HEIGHT; row++) { + rowOff = (uint32_t)row * rowBytes; + p0 = planes[0] + rowOff; + p1 = planes[1] + rowOff; + p2 = planes[2] + rowOff; + p3 = planes[3] + rowOff; + for (pair = 0u; pair < COLUMN_PAIRS; pair++) { + mixPlaneBytes(&lo, &hi, p0[0], p1[0], p2[0], p3[0]); + mixPlaneBytes(&lo, &hi, p0[1], p1[1], p2[1], p3[1]); + p0 += pairStep; + p1 += pairStep; + p2 += pairStep; + p3 += pairStep; + } + } + return jlSurfaceHashFinish(lo, hi, s); +} + + +// The 8 pixels whose plane bytes are b0..b3 (plane 0 first) as their 4 +// chunky bytes, mixed in order. +static inline __attribute__((always_inline)) void mixPlaneBytes(uint16_t *lo, uint16_t *hi, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) { + uint32_t chunky; + uint16_t l; + uint16_t h; + uint8_t b; + + chunky = gSpread[b0] | (gSpread[b1] << 1) | (gSpread[b2] << 2) | (gSpread[b3] << 3); + l = *lo; + h = *hi; + b = (uint8_t)(chunky >> 24); + SURFACE_HASH_MIX_BYTE(l, h, b); + b = (uint8_t)(chunky >> 16); + SURFACE_HASH_MIX_BYTE(l, h, b); + b = (uint8_t)(chunky >> 8); + SURFACE_HASH_MIX_BYTE(l, h, b); + b = (uint8_t)chunky; + SURFACE_HASH_MIX_BYTE(l, h, b); + *lo = l; + *hi = h; +} + + +static void spreadBuild(void) { + uint32_t value; + uint16_t b; + uint16_t k; + + for (b = 0u; b < SPREAD_VALUES; b++) { + value = 0u; + for (k = 0u; k < PIXELS_PER_PLANE_BYTE; k++) { + if ((b & (0x80u >> k)) != 0u) { + value |= (uint32_t)1u << ((PIXELS_PER_PLANE_BYTE - 1u - k) * NIBBLE_BITS); + } + } + gSpread[b] = value; + } + gSpreadBuilt = true; +} diff --git a/src/m68k/planarLayerBlit.s b/src/m68k/planarLayerBlit.s new file mode 100644 index 0000000..e9b2214 --- /dev/null +++ b/src/m68k/planarLayerBlit.s @@ -0,0 +1,248 @@ +| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +| See LICENSE.md for license information. +| SPDX-License-Identifier: MIT +| +| Tile-layer blit for the separate-plane 68k ports (the Amiga and the +| X68000): `rows` rows of the plane-separated ring (planarTileLayer.c has +| its layout, the ST's) into four plane buffers of 40 bytes a row, starting +| at ring pixel row ringY and column ringX, both wrapping. +| +| void jlPlanarTileLayerBlitRows(const uint8_t *ring, uint8_t *const *planes, uint16_t dstOff, uint16_t ringX, uint16_t ringY, uint16_t rows, uint16_t ringPxRows); +| +| planes is the surface's four plane pointers and dstOff the byte offset of +| the band's first row in each. The shift is the ST blit's +| (src/atarist/stTileLayerBlit.s): output word k of a plane row is +| (P[w+k] << s) | (P[w+k+1] >> (16 - s)), w = ringX / 16, s = ringX & 15, +| word indices modulo 21, each plane's last word riding in the high half +| of a carry register; lsl.l s for s 1-8, lsr.l 16-s for 9-15, a plain +| copy for 0. The ST writes one interleaved row, so it walks four planes +| at once; here every plane has its own destination, and the 68000 has +| address registers for two sources and two destinations, so a row is two +| passes of two planes. Each pass is two unrolled stretches, up to the +| ring's right edge and on from its left edge (20 - w and w steps shifted, +| 21 - w and w - 1 plain), a step being one word of each of the two +| planes. +| +| ABI: cdecl, d2-d7/a2-a6 saved. Registers: a2/a3 the pass's two plane +| sources, a4/a5 its two destinations, a6 the ring row, d0/d1 the carries, +| d2 the band row's offset, d4 scratch, d5 rows left, d6 the byte offset +| of P[w], d7 the shift count. +| +| GAS m68k syntax; every entry has both spellings (a.out/hunk "_" and ELF). + + .equ RING_PLANE_BYTES, 42 | 21 words: one byte per slot + .equ RING_ROW_BYTES, 168 | four plane rows + .equ STEPS, 20 | words in a plane's band row + .equ RING_WORDS, 21 + .equ BAND_ROW_BYTES, 40 + .equ LEFT_STEP_BYTES, 24 | emitted size of one LEFT_STEP + .equ RIGHT_STEP_BYTES, 20 + .equ PLAIN_STEP_BYTES, 4 + .equ LEFT_MAX_SHIFT, 8 + + .equ SAVED, 44 | d2-d7/a2-a6 + .equ LOCALS, 32 + .equ L_ENTRY1, 0 | first stretch's entry + .equ L_ENTRY2, 4 | second stretch's entry + .equ L_END, 8 | ring end + .equ L_BASE, 12 | ring base + .equ L_PLANE0, 16 | the four destination planes + .equ ARGS, (LOCALS + SAVED + 4) + .equ A_RING, ARGS + 0 + .equ A_PLANES, ARGS + 4 + .equ A_DSTOFF, ARGS + 8 + 2 + .equ A_RINGX, ARGS + 12 + 2 + .equ A_RINGY, ARGS + 16 + 2 + .equ A_ROWS, ARGS + 20 + 2 + .equ A_RINGPX, ARGS + 24 + 2 + + +| One plane word, shifted left (s 1-8): the high word of (prev:next) << s. + .macro LEFT_WORD aS, aD, dC + move.w (\aS)+,\dC + move.l \dC,%d4 + lsl.l %d7,%d4 + swap %d4 + move.w %d4,(\aD)+ + swap \dC + .endm + + +| One plane word, shifted right (s 9-15, d7 = 16 - s): the low word of +| (prev:next) >> (16 - s). + .macro RIGHT_WORD aS, aD, dC + move.w (\aS)+,\dC + move.l \dC,%d4 + lsr.l %d7,%d4 + move.w %d4,(\aD)+ + swap \dC + .endm + + +| One pass: planes \p and \p + 1 of the ring row in a6 into their band +| rows. \carry is 1 for the shifted variants (they read P[w] first). + .macro PASS p, carry + lea \p*RING_PLANE_BYTES(%a6,%d6.w),%a2 + lea RING_PLANE_BYTES(%a2),%a3 + movea.l L_PLANE0+\p*4(%sp),%a4 + adda.l %d2,%a4 + movea.l L_PLANE0+\p*4+4(%sp),%a5 + adda.l %d2,%a5 + .if \carry + move.w (%a2)+,%d0 + swap %d0 + move.w (%a3)+,%d1 + swap %d1 + .endif + movea.l L_ENTRY1(%sp),%a0 + jsr (%a0) + lea -RING_PLANE_BYTES(%a2),%a2 + lea -RING_PLANE_BYTES(%a3),%a3 + movea.l L_ENTRY2(%sp),%a0 + jsr (%a0) + .endm + + +| The next ring row (wrapping) and band row, and loop while rows are left. + .macro NEXT_ROW loop + lea RING_ROW_BYTES(%a6),%a6 + cmpa.l L_END(%sp),%a6 + bne.s 1f + movea.l L_BASE(%sp),%a6 +1: + moveq #BAND_ROW_BYTES,%d4 + add.l %d4,%d2 + subq.w #1,%d5 + bne.w \loop + .endm + + +| a0 = \end - (d0 steps of \bytes each), into \slot. + .macro ENTRY end, bytes, slot + mulu.w #\bytes,%d0 + lea \end(%pc),%a0 + suba.l %d0,%a0 + move.l %a0,\slot(%sp) + .endm + + .text + + .globl jlPlanarTileLayerBlitRows + .globl _jlPlanarTileLayerBlitRows + +jlPlanarTileLayerBlitRows: +_jlPlanarTileLayerBlitRows: + movem.l %d2-%d7/%a2-%a6,-(%sp) + lea -LOCALS(%sp),%sp + + movea.l A_PLANES(%sp),%a0 + move.l (%a0)+,L_PLANE0(%sp) + move.l (%a0)+,L_PLANE0+4(%sp) + move.l (%a0)+,L_PLANE0+8(%sp) + move.l (%a0),L_PLANE0+12(%sp) + movea.l A_RING(%sp),%a0 + move.l %a0,L_BASE(%sp) + moveq #0,%d0 + move.w A_RINGPX(%sp),%d0 + mulu.w #RING_ROW_BYTES,%d0 + add.l %a0,%d0 + move.l %d0,L_END(%sp) + moveq #0,%d0 + move.w A_RINGY(%sp),%d0 + mulu.w #RING_ROW_BYTES,%d0 + lea (%a0,%d0.l),%a6 + moveq #0,%d2 + move.w A_DSTOFF(%sp),%d2 + move.w A_ROWS(%sp),%d5 + move.w A_RINGX(%sp),%d6 + move.w %d6,%d7 + and.w #15,%d7 | s + lsr.w #4,%d6 | w + tst.w %d7 + beq.w .Lplain + cmp.w #LEFT_MAX_SHIFT,%d7 + bls.s .Lleft + + neg.w %d7 + add.w #16,%d7 | 16 - s + moveq #STEPS,%d0 + sub.w %d6,%d0 + ENTRY .LrightEnd, RIGHT_STEP_BYTES, L_ENTRY1 + move.w %d6,%d0 + ENTRY .LrightEnd, RIGHT_STEP_BYTES, L_ENTRY2 + bra.s .LshiftRows + +.Lleft: + moveq #STEPS,%d0 + sub.w %d6,%d0 + ENTRY .LleftEnd, LEFT_STEP_BYTES, L_ENTRY1 + move.w %d6,%d0 + ENTRY .LleftEnd, LEFT_STEP_BYTES, L_ENTRY2 + +.LshiftRows: + add.w %d6,%d6 | byte offset of P[w] +.LshiftRow: + PASS 0, 1 + PASS 2, 1 + NEXT_ROW .LshiftRow + bra.w .Ldone + +.Lplain: + moveq #STEPS,%d0 + tst.w %d6 + beq.s 1f + moveq #RING_WORDS,%d0 + sub.w %d6,%d0 | 21 - w +1: + move.w %d0,%d1 + ENTRY .LplainEnd, PLAIN_STEP_BYTES, L_ENTRY1 + moveq #STEPS,%d0 + sub.w %d1,%d0 + ENTRY .LplainEnd, PLAIN_STEP_BYTES, L_ENTRY2 + add.w %d6,%d6 +.LplainRow: + PASS 0, 0 + PASS 2, 0 + NEXT_ROW .LplainRow + +.Ldone: + lea LOCALS(%sp),%sp + movem.l (%sp)+,%d2-%d7/%a2-%a6 + rts + + +| The unrolled stretches: a run of n steps is entered n steps before the +| rts that ends it. +.LleftRun: + .rept STEPS + LEFT_WORD %a2, %a4, %d0 + LEFT_WORD %a3, %a5, %d1 + .endr +.LleftEnd: + rts + +.LrightRun: + .rept STEPS + RIGHT_WORD %a2, %a4, %d0 + RIGHT_WORD %a3, %a5, %d1 + .endr +.LrightEnd: + rts + +.LplainRun: + .rept STEPS + move.w (%a2)+,(%a4)+ + move.w (%a3)+,(%a5)+ + .endr +.LplainEnd: + rts + + .if (.LleftEnd - .LleftRun) - STEPS * LEFT_STEP_BYTES + .error "LEFT_STEP_BYTES does not match the emitted step" + .endif + .if (.LrightEnd - .LrightRun) - STEPS * RIGHT_STEP_BYTES + .error "RIGHT_STEP_BYTES does not match the emitted step" + .endif + .if (.LplainEnd - .LplainRun) - STEPS * PLAIN_STEP_BYTES + .error "PLAIN_STEP_BYTES does not match the emitted step" + .endif diff --git a/src/m68k/planarPlot.s b/src/m68k/planarPlot.s index 69211ee..e8a6f82 100644 --- a/src/m68k/planarPlot.s +++ b/src/m68k/planarPlot.s @@ -4,8 +4,8 @@ | | Shared 68000 separate-plane pixel walkers -- hand-rolled. | -| surface68kPlanarCircle jlDrawCircle: Bresenham midpoint outline -| surface68kPlanarLine jlDrawLine: Bresenham diagonal +| jlSurface68kPlanarCircle jlDrawCircle: Bresenham midpoint outline +| jlSurface68kPlanarLine jlDrawLine: Bresenham diagonal | | Used by BOTH separate-plane 68k ports, the Amiga and the X68000, which keep | four plane buffers of 40 bytes per row. Same shape as src/atarist/circle.s, @@ -41,7 +41,7 @@ | spellings are defined at each entry point, because gcc-amigaos prepends an | underscore to C names and elf2x68k does not. | -| void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, +| void jlSurface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, | uint16_t r, uint16_t color); | | Register allocation: @@ -62,10 +62,10 @@ .text .even - .globl surface68kPlanarCircle - .globl _surface68kPlanarCircle - .globl surface68kPlanarLine - .globl _surface68kPlanarLine + .globl jlSurface68kPlanarCircle + .globl _jlSurface68kPlanarCircle + .globl jlSurface68kPlanarLine + .globl _jlSurface68kPlanarLine | ---- XREC: xByte and bit mask for cx <op> <reg>, into slots ---- | Trashes d0, d6, d7. @@ -175,8 +175,8 @@ .endm -surface68kPlanarCircle: -_surface68kPlanarCircle: +jlSurface68kPlanarCircle: +_jlSurface68kPlanarCircle: movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes sub.l #24,%sp | scratch block | args: 24 scratch + 44 saved + 4 return = 72 @@ -237,7 +237,7 @@ circleDone: rts -| ---------------- surface68kPlanarLine ---------------- +| ---------------- jlSurface68kPlanarLine ---------------- | | Bresenham's diagonal line. Straight runs never arrive: src/core/draw.c | diverts horizontal and vertical lines to jlFillRect, so this walks the @@ -252,7 +252,7 @@ circleDone: | instead of the two shifts and an add a multiply-free y*40 would cost per | pixel. | -| void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, +| void jlSurface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, | int16_t x1, int16_t y1, uint16_t color); | | Register allocation: @@ -340,8 +340,8 @@ circleDone: .endm -surface68kPlanarLine: -_surface68kPlanarLine: +jlSurface68kPlanarLine: +_jlSurface68kPlanarLine: movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes sub.l #LN_LOCAL,%sp move.l LN_PLANES(%sp),%a4 diff --git a/src/m68k/planarShape.s b/src/m68k/planarShape.s new file mode 100644 index 0000000..534b1d7 --- /dev/null +++ b/src/m68k/planarShape.s @@ -0,0 +1,173 @@ +| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +| See LICENSE.md for license information. +| SPDX-License-Identifier: MIT +| +| Shared 68000 separate-plane tile shape paste (the Amiga and the X68000). +| +| planarTile.h's planarTilePasteShape: eight 1bpp shape rows (a glyph's, or +| the mono paste's folded silhouette) into an 8x8 tile of the four planes, +| set bits taking the foreground's plane bit and clear ones the +| background's. A plane's eight bytes are then only ever one of four +| things, by its two colour bits: all zeros (both 0), the shape rows +| inverted (fg 0, bg 1), the shape rows (fg 1, bg 0), or all ones (both 1). +| So each plane picks its case once and stores eight unrolled bytes, each at +| its row's displacement, against the C's and, xor and store a byte with a +| row step and a loop test (and its per-plane mask setup). +| +| a0 = the 8 shape bytes a1 = the four plane pointers +| d0.w = the tile's first byte's offset in a plane +| d1.b = fg, d2.b = bg (low nibbles) +| Register arguments from an inline asm call; clobbers d0-d4, a0-a2. +| Each entry point carries both symbol spellings (see planarFill.s). +| +| And jlTilePasteGlyph itself, which pastes a glyph through it. + + .text + .even + .globl jlPlanarPasteShape68k + .globl _jlPlanarPasteShape68k + .globl jlTilePasteGlyph + .globl _jlTilePasteGlyph + + .equ ROW_BYTES, 40 | PLANAR_TILE_BYTES_PER_ROW (planarTile.h checks it) + .equ PLANES, 4 + +| Eight rows of one plane: \op with \src at row r's byte. + .macro SHAPE_ROWS op + .irp r, 0, 1, 2, 3, 4, 5, 6, 7 + \op \r + .endr + .endm + .macro ROW_FILL r + move.b %d4,(\r * ROW_BYTES)(%a2) + .endm + .macro ROW_COPY r + move.b \r(%a0),(\r * ROW_BYTES)(%a2) + .endm + .macro ROW_INVERT r + move.b \r(%a0),%d4 + not.b %d4 + move.b %d4,(\r * ROW_BYTES)(%a2) + .endm + +jlPlanarPasteShape68k: +_jlPlanarPasteShape68k: + moveq #PLANES - 1,%d3 +.Lps_plane: + movea.l (%a1)+,%a2 + adda.w %d0,%a2 | this plane's tile byte + moveq #0,%d4 + lsr.b #1,%d2 | bg's bit for the plane + addx.b %d4,%d4 + lsr.b #1,%d1 | fg's + bcc.s 1f + addq.b #2,%d4 +1: add.w %d4,%d4 + add.w %d4,%d4 | (fg * 2 + bg) * 4 + jmp .Lps_cases(%pc,%d4.w) +.Lps_cases: + bra.w .Lps_zeros + bra.w .Lps_invert + bra.w .Lps_copy + bra.w .Lps_ones +.Lps_zeros: + moveq #0,%d4 + bra.s .Lps_fill +.Lps_ones: + moveq #-1,%d4 +.Lps_fill: + SHAPE_ROWS ROW_FILL + dbra %d3,.Lps_plane + rts +.Lps_copy: + SHAPE_ROWS ROW_COPY + dbra %d3,.Lps_plane + rts +.Lps_invert: + SHAPE_ROWS ROW_INVERT + dbra %d3,.Lps_plane + rts + + +| void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, +| const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) +| +| The public call itself (JL_HAS_TILE_PASTE_GLYPH_ENTRY; tile.c leaves its C +| body out): the checks, the shape paste above and the stage's dirty mark in +| one call. The C wrapper spent ~600 cycles a glyph around a ~650-cycle +| paste: a five-register save, every argument reloaded, a muls for the row, +| two calls. A glyph off the surface, or with no bits, is nothing, as in C; +| a surface with no planes goes on to jlpGenericTilePasteGlyph, checked, +| with the same arguments. +| +| cdecl: every argument in a 4-byte stack slot, a uint8_t in its low byte; +| the checks write fg and bg back masked to 4 bits, as the paste and the +| generic take them. The C symbols it names go through C_OP +| (m68kCSym.inc). + .include "m68kCSym.inc" + + .equ GLYPH_SAVED, 16 | d2-d4/a2, pushed first + .equ GLYPH_DST, GLYPH_SAVED + 4 + .equ GLYPH_BX, GLYPH_SAVED + 11 + .equ GLYPH_BY, GLYPH_SAVED + 15 + .equ GLYPH_FG, GLYPH_SAVED + 23 + .equ GLYPH_BG, GLYPH_SAVED + 27 + .equ SURFACE_PORT, 4 | offsetof(jlSurfaceT, portData) (planarTile.h checks it) + .equ BLOCKS_PER_ROW, 40 | JL_TILE_BLOCKS_PER_ROW (tile.c checks it) + .equ BLOCKS_PER_COL, 25 | JL_TILE_BLOCKS_PER_COL + .equ COLOR_MASK, 0x0F + +jlTilePasteGlyph: +_jlTilePasteGlyph: + movem.l %d2-%d4/%a2,-(%sp) + movem.l GLYPH_DST(%sp),%d0-%d2/%a0 | dst, bx, by, bits + tst.l %d0 + beq.s .Lg_none + move.l %a0,%d3 + beq.s .Lg_none + cmpi.b #BLOCKS_PER_ROW,%d1 + bcc.s .Lg_none + cmpi.b #BLOCKS_PER_COL,%d2 + bcc.s .Lg_none + movea.l %d0,%a1 + move.l SURFACE_PORT(%a1),%d3 | the planes (they lead the planar data) + beq.s .Lg_generic + movea.l %d3,%a1 + andi.w #0xFF,%d2 + add.w %d2,%d2 + move.w .Lg_rows(%pc,%d2.w),%d0 | the tile row's first byte + andi.w #0xFF,%d1 + add.w %d1,%d0 + moveq #COLOR_MASK,%d1 + and.b GLYPH_FG(%sp),%d1 + moveq #COLOR_MASK,%d2 + and.b GLYPH_BG(%sp),%d2 + bsr jlPlanarPasteShape68k + move.l GLYPH_DST(%sp),%d0 + C_OP cmp.l, jlgStage, %d0 + bne.s .Lg_none + moveq #0,%d0 + move.b GLYPH_BY(%sp),%d0 + lsl.w #3,%d0 | the tile's first row + C_OP lea, jlgStageMinWord, %a0 + adda.w %d0,%a0 + C_OP lea, jlgStageMaxWord, %a1 + adda.w %d0,%a1 + moveq #0,%d1 + move.b GLYPH_BX(%sp),%d1 + add.w %d1,%d1 | its first 16-bit word + jsr jlStageBandsMarkTile68k +.Lg_none: + movem.l (%sp)+,%d2-%d4/%a2 + rts +| No planes: the generic, with the same arguments, the colours masked. +.Lg_generic: + andi.b #COLOR_MASK,GLYPH_FG(%sp) + andi.b #COLOR_MASK,GLYPH_BG(%sp) + movem.l (%sp)+,%d2-%d4/%a2 + C_OP jmp, jlpGenericTilePasteGlyph +| A tile row's first byte in a plane: block row * 8 rows * 40 bytes. +.Lg_rows: + .irp by, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 + .word \by * 8 * ROW_BYTES + .endr diff --git a/src/m68k/planarSprite.c b/src/m68k/planarSprite.c index be94e78..d8f4221 100644 --- a/src/m68k/planarSprite.c +++ b/src/m68k/planarSprite.c @@ -7,10 +7,10 @@ // cannot do because the sprite is partly off the surface. // // A sprite's pixels are chunky; the screen is planar. Converting at every -// draw cost five tests a pixel, so spritePlanarBuild converts each sprite +// draw cost five tests a pixel, so jlSpritePlanarBuild converts each sprite // once, when it is created, into CACHE_COLUMN_WORDS words per 16-pixel // column per row: the opaque mask, then planes 0..3 (spriteC2p.h, the same -// conversion the code emitters bake in). planarSpriteDraw clips the draw and +// conversion the code emitters bake in). jlPlanarSpriteDraw clips the draw and // hands the rows to planarSpriteWalk.s, which shifts each word into place // and merges every destination word once. // @@ -30,11 +30,11 @@ // One pointer reaches a group's four plane words (+0 +2 +4 +6). #define PLANAR_ROW_BYTES ((uint16_t)(JL_SURFACE_WIDTH / 2)) #define PLANAR_WORD_BYTES 8u - #define planarSpriteRows(_job) planarSpriteRowsInterleaved((_job)) + #define planarSpriteRows(_job) jlPlanarSpriteRowsInterleaved((_job)) #else #define PLANAR_ROW_BYTES ((uint16_t)(JL_SURFACE_WIDTH / 8)) #define PLANAR_WORD_BYTES 2u - #define planarSpriteRows(_job) planarSpriteRowsSeparate((_job)) + #define planarSpriteRows(_job) jlPlanarSpriteRowsSeparate((_job)) #endif #define PLANAR_WORD_COLUMNS ((int16_t)(JL_SURFACE_WIDTH / 16)) @@ -58,8 +58,8 @@ typedef struct { _Static_assert(offsetof(PlanarSpriteJobT, src) == 16 && offsetof(PlanarSpriteJobT, rows) == 20 && offsetof(PlanarSpriteJobT, mainBytes) == 26 && offsetof(PlanarSpriteJobT, shift) == 32, "PlanarSpriteJobT must match planarSpriteWalk.s's PSJ_* offsets"); -void planarSpriteRowsInterleaved(PlanarSpriteJobT *job); -void planarSpriteRowsSeparate(PlanarSpriteJobT *job); +void jlPlanarSpriteRowsInterleaved(PlanarSpriteJobT *job); +void jlPlanarSpriteRowsSeparate(PlanarSpriteJobT *job); // Draw sp at (x, y), clipped to the surface. planes[p] is plane p's first @@ -67,7 +67,7 @@ void planarSpriteRowsSeparate(PlanarSpriteJobT *job); // word column k lands at destination word column first + k and, when // shift != 0, spills into first + k + 1. Screen edges fall on word // boundaries, so clipping to whole words is exact. -void planarSpriteDraw(uint8_t *const planes[], const jlSpriteT *sp, int16_t x, int16_t y) { +void jlPlanarSpriteDraw(uint8_t *const planes[], const jlSpriteT *sp, int16_t x, int16_t y) { PlanarSpriteJobT job; int16_t h; int16_t sy; @@ -136,7 +136,7 @@ void planarSpriteDraw(uint8_t *const planes[], const jlSpriteT *sp, int16_t x, i // column's left 8 pixels are the high byte. An odd last tile column is the // left half of a word whose right half is transparent. Freed with // jlpBigFree. -uint8_t *spritePlanarBuild(const jlSpriteT *sp) { +uint8_t *jlSpritePlanarBuild(const jlSpriteT *sp) { uint16_t *cache; uint16_t *out; const uint8_t *tileRow; diff --git a/src/m68k/planarSpriteWalk.s b/src/m68k/planarSpriteWalk.s index 9a77e22..322ea93 100644 --- a/src/m68k/planarSpriteWalk.s +++ b/src/m68k/planarSpriteWalk.s @@ -4,10 +4,10 @@ | | The planar ports' sprite interpreter rows (68000). | -| planarSpriteRowsSeparate(PlanarSpriteJobT *job) Amiga, X68000 -| planarSpriteRowsInterleaved(PlanarSpriteJobT *job) Atari ST +| jlPlanarSpriteRowsSeparate(PlanarSpriteJobT *job) Amiga, X68000 +| jlPlanarSpriteRowsInterleaved(PlanarSpriteJobT *job) Atari ST | -| The row loop of src/m68k/planarSprite.c's planarSpriteDraw, which clips +| The row loop of src/m68k/planarSprite.c's jlPlanarSpriteDraw, which clips | the draw and fills in the job. Each row walks the sprite's planar cache a | 16-pixel column at a time -- the opaque mask word, then planes 0..3's | words -- and merges one destination word in every plane per column: @@ -33,14 +33,19 @@ | | ABI: cdecl (gcc-amigaos, gcc-atari-mint, elf2x68k), d2-d7/a2-a6 | callee-save. Each entry point carries both symbol spellings (see -| planarFill.s). The job's fields advance a row at a time in place. +| planarFill.s). The separate walk advances the job's fields a row at a time +| in place; the interleaved one leaves them as they came. .text .even - .globl planarSpriteRowsSeparate - .globl _planarSpriteRowsSeparate - .globl planarSpriteRowsInterleaved - .globl _planarSpriteRowsInterleaved + .globl jlPlanarSpriteRowsSeparate + .globl _jlPlanarSpriteRowsSeparate + .globl jlPlanarSpriteRowsInterleaved + .globl _jlPlanarSpriteRowsInterleaved + .globl jlPlanarBackupSave68k + .globl _jlPlanarBackupSave68k + .globl jlPlanarBackupRestore68k + .globl _jlPlanarBackupRestore68k | PlanarSpriteJobT (planarSprite.c checks these offsets at build time). .equ PSJ_PLANE0, 0 | plane 0's first destination word, this row @@ -124,14 +129,29 @@ .endm | ---- ROWS: the whole draw ---- +| The interleaved walk has a2-a4 free (one plane pointer), so it keeps its +| row in registers: a2 the cache row, a3 the destination row, a4 where the +| destination rows end. The separate one keeps its row in the job. .macro ROWS interleaved movem.l %d2-%d7/%a2-%a6,-(%sp) movea.l PSJ_ARG(%sp),%a6 move.w PSJ_SHIFT(%a6),%d0 + .if \interleaved + movea.l PSJ_SRC(%a6),%a2 + movea.l PSJ_PLANE0(%a6),%a3 + moveq #0,%d6 + move.w PSJ_ROWS(%a6),%d6 + mulu.w PSJ_DSTSTRIDE(%a6),%d6 + movea.l %a3,%a4 + adda.l %d6,%a4 + .endif .Lrow\interleaved: + .if \interleaved + movea.l %a2,%a0 + movea.l %a3,%a1 + .else movea.l PSJ_SRC(%a6),%a0 movea.l PSJ_PLANE0(%a6),%a1 - .if !\interleaved movea.l PSJ_PLANE1(%a6),%a2 movea.l PSJ_PLANE2(%a6),%a3 movea.l PSJ_PLANE3(%a6),%a4 @@ -160,28 +180,118 @@ beq.s 4f NOWORDS MERGE \interleaved -4: movea.l PSJ_SRC(%a6),%a0 | the next row +4: + .if \interleaved + adda.w PSJ_SRCSTRIDE(%a6),%a2 | the next row + adda.w PSJ_DSTSTRIDE(%a6),%a3 + cmpa.l %a4,%a3 + bne.w .Lrow\interleaved + .else + movea.l PSJ_SRC(%a6),%a0 | the next row adda.w PSJ_SRCSTRIDE(%a6),%a0 move.l %a0,PSJ_SRC(%a6) moveq #0,%d6 move.w PSJ_DSTSTRIDE(%a6),%d6 add.l %d6,PSJ_PLANE0(%a6) - .if !\interleaved add.l %d6,PSJ_PLANE1(%a6) add.l %d6,PSJ_PLANE2(%a6) add.l %d6,PSJ_PLANE3(%a6) - .endif subq.w #1,PSJ_ROWS(%a6) bne.w .Lrow\interleaved + .endif movem.l (%sp)+,%d2-%d7/%a2-%a6 rts .endm -planarSpriteRowsSeparate: -_planarSpriteRowsSeparate: +jlPlanarSpriteRowsSeparate: +_jlPlanarSpriteRowsSeparate: ROWS 0 -planarSpriteRowsInterleaved: -_planarSpriteRowsInterleaved: +jlPlanarSpriteRowsInterleaved: +_jlPlanarSpriteRowsInterleaved: ROWS 1 + + +| ---- the backup copies (Amiga, X68000) ------------------------------------ +| +| jlPlanarBackupSave68k the planes' region into the backup +| jlPlanarBackupRestore68k the backup back into the planes +| +| planarTile.h's planarSpriteSavePlanes / RestorePlanes for an even backup: +| a sprite's backup is the 16-px group region holding it (sprite.c, +| SPRITE_BACKUP_ALIGN), so a plane's row of it is whole words, and a backup +| row is the four planes' rows one after another. The common one- and +| two-word regions (a 16- or 32-px sprite) take an unrolled row of a move a +| plane; wider ones a word loop. +| +| a0 = the backup (even) a1 = the four plane pointers +| d0.w = the region's first byte offset in a plane +| d1.w = words a plane row (>= 1) d2.w = rows (>= 1) +| Register arguments from an inline asm call; clobbers d0-d3, a0-a4. + + .equ BK_ROW_BYTES, 40 | PLANAR_TILE_BYTES_PER_ROW (planarTile.h checks it) + + .macro BACKUP save + movea.l (%a1)+,%a2 + adda.w %d0,%a2 + movea.l (%a1)+,%a3 + adda.w %d0,%a3 + movea.l (%a1)+,%a4 + adda.w %d0,%a4 + movea.l (%a1),%a1 | the last plane in a1 + adda.w %d0,%a1 + subq.w #1,%d2 + cmpi.w #1,%d1 + beq.s .Lbk_one\save + cmpi.w #2,%d1 + beq.s .Lbk_two\save + moveq #BK_ROW_BYTES,%d3 | wider: a word loop a plane + sub.w %d1,%d3 + sub.w %d1,%d3 | the stride past a plane's row + subq.w #1,%d1 +.Lbk_rowN\save: + .irp plane, %a2, %a3, %a4, %a1 + move.w %d1,%d0 +1: + .if \save + move.w (\plane)+,(%a0)+ + .else + move.w (%a0)+,(\plane)+ + .endif + dbra %d0,1b + adda.w %d3,\plane + .endr + dbra %d2,.Lbk_rowN\save + rts +.Lbk_one\save: + .irp plane, %a2, %a3, %a4, %a1 + .if \save + move.w (\plane),(%a0)+ + .else + move.w (%a0)+,(\plane) + .endif + lea BK_ROW_BYTES(\plane),\plane + .endr + dbra %d2,.Lbk_one\save + rts +.Lbk_two\save: + .irp plane, %a2, %a3, %a4, %a1 + .if \save + move.l (\plane),(%a0)+ + .else + move.l (%a0)+,(\plane) + .endif + lea BK_ROW_BYTES(\plane),\plane + .endr + dbra %d2,.Lbk_two\save + rts + .endm + +jlPlanarBackupSave68k: +_jlPlanarBackupSave68k: + BACKUP 1 + +jlPlanarBackupRestore68k: +_jlPlanarBackupRestore68k: + BACKUP 0 diff --git a/src/m68k/planarTile.h b/src/m68k/planarTile.h index 9738161..79d4c61 100644 --- a/src/m68k/planarTile.h +++ b/src/m68k/planarTile.h @@ -21,8 +21,10 @@ #include <stdint.h> #include <stdbool.h> +#include <stddef.h> #include "joey/tile.h" +#include "m68kAsm.h" // 8-row unrolled tile access (NATIVE-PERF Phase 2 R8): row offsets // are compile-time constants row * 40, so each row is one d16(An) @@ -33,6 +35,157 @@ // register-light per the m68k-gcc-unroll-backfire lesson. #define PLANAR_TILE_ROW_OFF(_r) ((uint16_t)((_r) * PLANAR_TILE_BYTES_PER_ROW)) +// The shared planar rect fill (src/m68k/planarFill.s): any on-surface rect, +// a run of whole rows as one movem.l run per plane, anything else as masked +// edge words and long stores between them, plane by plane -- one-pixel-wide +// rects included, which is what draw.c turns a vertical line into. +extern void jlSurface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color); + +// Single pixels (jlpDrawPixel / jlpSamplePixel), inline for the same reason: +// jlDrawPixel, the flood fill's reads, and the planar snap call them per +// pixel, where a second cdecl layer cost more than the plane writes. A +// planar surface always has portData (jlSurfaceCreate fails without it), so +// there is no fallback to test for. +static inline __attribute__((always_inline)) void planarDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { + uint8_t *const *planes; + uint16_t off; + uint8_t bit; + uint8_t keep; + + planes = ((PLANAR_TILE_DATA_T *)s->portData)->planes; + off = (uint16_t)(y * PLANAR_TILE_BYTES_PER_ROW + (x >> 3)); + bit = (uint8_t)(0x80u >> (x & 7u)); + keep = (uint8_t)~bit; + if ((colorIndex & 1u) != 0u) { + planes[0][off] = (uint8_t)(planes[0][off] | bit); + } else { + planes[0][off] = (uint8_t)(planes[0][off] & keep); + } + if ((colorIndex & 2u) != 0u) { + planes[1][off] = (uint8_t)(planes[1][off] | bit); + } else { + planes[1][off] = (uint8_t)(planes[1][off] & keep); + } + if ((colorIndex & 4u) != 0u) { + planes[2][off] = (uint8_t)(planes[2][off] | bit); + } else { + planes[2][off] = (uint8_t)(planes[2][off] & keep); + } + if ((colorIndex & 8u) != 0u) { + planes[3][off] = (uint8_t)(planes[3][off] | bit); + } else { + planes[3][off] = (uint8_t)(planes[3][off] & keep); + } +} + + +static inline __attribute__((always_inline)) uint8_t planarSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { + uint8_t *const *planes; + uint16_t off; + uint8_t bit; + uint8_t color; + + planes = ((const PLANAR_TILE_DATA_T *)s->portData)->planes; + off = (uint16_t)((uint16_t)y * PLANAR_TILE_BYTES_PER_ROW + ((uint16_t)x >> 3)); + bit = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); + color = 0u; + if ((planes[0][off] & bit) != 0u) { + color = 1u; + } + if ((planes[1][off] & bit) != 0u) { + color = (uint8_t)(color | 2u); + } + if ((planes[2][off] & bit) != 0u) { + color = (uint8_t)(color | 4u); + } + if ((planes[3][off] & bit) != 0u) { + color = (uint8_t)(color | 8u); + } + return color; +} + + +// Sprite backups (jlpSpriteSavePlanes / jlpSpriteRestorePlanes): every save +// records the 16-px group region holding the sprite (sprite.c, +// SPRITE_BACKUP_ALIGN), so a backup row is each plane's bytes of the region +// in turn -- the compiled save's format (the format law, spriteInternal.h). +// The region's bytes are whole words, so an even buffer goes through +// src/m68k/planarSpriteWalk.s's copies (register arguments); an odd one (a +// caller's, where the 68000 takes no word) bytes, here. +typedef char planarBackupGeometryProof[(PLANAR_TILE_BYTES_PER_ROW == 40 && PLANAR_TILE_PLANES == 4) ? 1 : -1]; +static inline __attribute__((always_inline)) void planarBackupCall(uint8_t *const *planes, uint16_t rowOff, uint16_t words, uint16_t rows, const uint8_t *buffer, bool save) { + register const uint8_t *rBuffer __asm__("a0") = buffer; + register uint8_t *const *rPlanes __asm__("a1") = planes; + register uint16_t rOff __asm__("d0") = rowOff; + register uint16_t rWords __asm__("d1") = words; + register uint16_t rRows __asm__("d2") = rows; + + if (save) { + __asm__ volatile ("jsr " M68K_ASM_SYM(jlPlanarBackupSave68k) : "+a" (rBuffer), "+a" (rPlanes), "+d" (rOff), "+d" (rWords), "+d" (rRows) : : "d3", "a2", "a3", "a4", "cc", "memory"); + } else { + __asm__ volatile ("jsr " M68K_ASM_SYM(jlPlanarBackupRestore68k) : "+a" (rBuffer), "+a" (rPlanes), "+d" (rOff), "+d" (rWords), "+d" (rRows) : : "d3", "a2", "a3", "a4", "cc", "memory"); + } +} + + +static inline __attribute__((always_inline)) void planarSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { + const PLANAR_TILE_DATA_T *pd; + const uint8_t *src; + uint16_t rowOff; + uint16_t span; + uint16_t row; + uint16_t i; + uint8_t plane; + + pd = (const PLANAR_TILE_DATA_T *)s->portData; + span = (uint16_t)(w >> 3); + rowOff = (uint16_t)((uint16_t)y * PLANAR_TILE_BYTES_PER_ROW + ((uint16_t)x >> 3)); + if ((((uintptr_t)dstPlaneBytes) & 1u) == 0u) { + planarBackupCall(pd->planes, rowOff, (uint16_t)(span >> 1), h, dstPlaneBytes, true); + return; + } + for (row = 0u; row < h; row++) { + for (plane = 0u; plane < PLANAR_TILE_PLANES; plane++) { + src = pd->planes[plane] + rowOff; + for (i = 0u; i < span; i++) { + dstPlaneBytes[i] = src[i]; + } + dstPlaneBytes += span; + } + rowOff = (uint16_t)(rowOff + PLANAR_TILE_BYTES_PER_ROW); + } +} + + +static inline __attribute__((always_inline)) void planarSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { + PLANAR_TILE_DATA_T *pd; + uint8_t *dst; + uint16_t rowOff; + uint16_t span; + uint16_t row; + uint16_t i; + uint8_t plane; + + pd = (PLANAR_TILE_DATA_T *)s->portData; + span = (uint16_t)(w >> 3); + rowOff = (uint16_t)((uint16_t)y * PLANAR_TILE_BYTES_PER_ROW + ((uint16_t)x >> 3)); + if ((((uintptr_t)srcPlaneBytes) & 1u) == 0u) { + planarBackupCall(pd->planes, rowOff, (uint16_t)(span >> 1), h, srcPlaneBytes, false); + return; + } + for (row = 0u; row < h; row++) { + for (plane = 0u; plane < PLANAR_TILE_PLANES; plane++) { + dst = pd->planes[plane] + rowOff; + for (i = 0u; i < span; i++) { + dst[i] = srcPlaneBytes[i]; + } + srcPlaneBytes += span; + } + rowOff = (uint16_t)(rowOff + PLANAR_TILE_BYTES_PER_ROW); + } +} + + // Phase 5 planar dual-write for tile ops, fully planar after Phase 9 // dropped the chunky shadow. All tiles are 8-pixel aligned (8x8 blocks // at multiples of 8), so plane writes are byte-aligned -- one plane @@ -313,21 +466,24 @@ static inline __attribute__((always_inline)) void planarTileMapPaste(jlSurfaceT // Eight 1bpp shape rows into the bitplanes: set bits take the // foreground's plane bit, clear bits the background's. Shared by the // mono paste (which folds a chunky silhouette into these rows first) -// and the glyph paste (whose input already IS these rows). +// and the glyph paste (whose input already IS these rows). A plane's eight +// bytes are then all zeros, the rows inverted, the rows, or all ones, by +// its two colour bits, which src/m68k/planarShape.s picks once a plane and +// stores unrolled; it is handed the rows, the plane pointers and the +// tile's byte offset in registers. +typedef char planarShapeGeometryProof[(PLANAR_TILE_BYTES_PER_ROW == 40 && PLANAR_TILE_PLANES == 4) ? 1 : -1]; +// planarShape.s's jlTilePasteGlyph (JL_HAS_TILE_PASTE_GLYPH_ENTRY) also +// pastes 1bpp glyph rows through it, as they stand: it finds the planes at +// the surface's portData, where they lead the planar data. +typedef char planarGlyphEntryProof[(offsetof(jlSurfaceT, portData) == 4 && offsetof(PLANAR_TILE_DATA_T, planes) == 0) ? 1 : -1]; static inline __attribute__((always_inline)) void planarTilePasteShape(PLANAR_TILE_DATA_T *pd, uint8_t bx, uint8_t by, const uint8_t *shape, uint8_t fgColor, uint8_t bgColor) { - uint16_t rowBase = (uint16_t)((uint16_t)by * 8u) * PLANAR_TILE_BYTES_PER_ROW + bx; - uint8_t plane; - uint8_t row; + register const uint8_t *rShape __asm__("a0") = shape; + register uint8_t **rPlanes __asm__("a1") = pd->planes; + register uint16_t rOff __asm__("d0") = (uint16_t)((uint16_t)((uint16_t)by * 8u) * PLANAR_TILE_BYTES_PER_ROW + bx); + register uint8_t rFg __asm__("d1") = fgColor; + register uint8_t rBg __asm__("d2") = bgColor; - for (plane = 0; plane < PLANAR_TILE_PLANES; plane++) { - uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t *p = pd->planes[plane] + rowBase; - for (row = 0; row < 8u; row++) { - uint8_t s = shape[row]; - p[row * PLANAR_TILE_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); - } - } + __asm__ volatile ("jsr " M68K_ASM_SYM(jlPlanarPasteShape68k) : "+a" (rShape), "+a" (rPlanes), "+d" (rOff), "+d" (rFg), "+d" (rBg) : : "d3", "d4", "a2", "cc", "memory"); } @@ -360,18 +516,4 @@ static inline __attribute__((always_inline)) void planarTilePasteMono(jlSurfaceT planarTilePasteShape(pd, bx, by, shape, fgColor, bgColor); } - -// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape -// the mono paste has to fold out of a chunky silhouette, so this skips -// the fold entirely (and cannot be handed the wrong tile format). -static inline __attribute__((always_inline)) void planarTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { - PLANAR_TILE_DATA_T *pd = (PLANAR_TILE_DATA_T *)dst->portData; - - if (pd == NULL) { - jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); - return; - } - planarTilePasteShape(pd, bx, by, bits, fgColor, bgColor); -} - #endif diff --git a/src/m68k/planarTileLayer.c b/src/m68k/planarTileLayer.c new file mode 100644 index 0000000..c164577 --- /dev/null +++ b/src/m68k/planarTileLayer.c @@ -0,0 +1,52 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// Tile-layer ring for the separate-plane 68k ports, the Amiga and the +// X68000 (planarTileLayer.h has the layout, tileLayerInternal.h the +// contract): the Put, and the CPU blit, which shifts a pair of a plane's +// words as one long (planarLayerBlit.s). A planar jlTileT is plane-major: +// plane p's 8 rows at pixels[p * 8] (planarTile.h). Each port allocates the +// ring and picks its blit (src/amiga/amigaTileLayer.c, src/x68000/ +// x68kTileLayer.c). +// +// Built for the Amiga and the X68000 only; port.h's tail brings in the +// port's tile shim, which names its planar portData type +// PLANAR_TILE_DATA_T (AmigaPlanarT / X68kPlanarT, field for field). + +#include <stdint.h> + +#include "joey/tile.h" +#include "planarTileLayer.h" +#include "port.h" +#include "surfaceInternal.h" + +// planarLayerBlit.s has these numbers in it. +typedef char planarTileLayerRingProof[(PLANAR_RING_PLANE_BYTES == 42 && PLANAR_RING_ROW_BYTES == 168 && PLANAR_TILE_BYTES_PER_ROW == 40 && PLANAR_TILE_PLANES == 4) ? 1 : -1]; + +extern void jlPlanarTileLayerBlitRows(const uint8_t *ring, uint8_t *const *planes, uint16_t dstOff, uint16_t ringX, uint16_t ringY, uint16_t rows, uint16_t ringPxRows); + + +void jlPlanarTileLayerCpuBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) { + PLANAR_TILE_DATA_T *pd; + + pd = (PLANAR_TILE_DATA_T *)dst->portData; + jlPlanarTileLayerBlitRows((const uint8_t *)layer->ring, pd->planes, (uint16_t)((uint16_t)layer->viewBy * JL_TILE_PIXELS_PER_SIDE * PLANAR_TILE_BYTES_PER_ROW), ringX, ringY, (uint16_t)((uint16_t)layer->viewHeight << 3), (uint16_t)((uint16_t)layer->ringRows << 3)); +} + + +void jlpTileLayerPut(jlTileLayerT *layer, uint8_t slotCol, uint8_t slotRow, const jlTileT *tile) { + uint8_t *d; + const uint8_t *s; + uint8_t p; + uint8_t r; + + for (p = 0u; p < PLANAR_TILE_PLANES; p++) { + d = (uint8_t *)layer->ring + (uint16_t)slotRow * PLANAR_RING_SLOT_BYTES + p * PLANAR_RING_PLANE_BYTES + slotCol; + s = tile->pixels + p * PLANAR_TILE_PLANE_STRIDE; + for (r = 0u; r < JL_TILE_PIXELS_PER_SIDE; r++) { + *d = s[r]; + d += PLANAR_RING_ROW_BYTES; + } + } +} diff --git a/src/m68k/planarTileLayer.h b/src/m68k/planarTileLayer.h new file mode 100644 index 0000000..c73e464 --- /dev/null +++ b/src/m68k/planarTileLayer.h @@ -0,0 +1,28 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// The separate-plane 68k ports' tile-layer ring (the Amiga and the X68000): +// its layout, shared by src/m68k/planarTileLayer.c (the Put and the CPU +// blit) and each port's own ring allocation and blit (src/amiga/ +// amigaTileLayer.c, src/x68000/x68kTileLayer.c). Each of the ring's +// ringRows * 8 pixel rows is plane 0's 21 words, then planes 1, 2, and 3, a +// slot being one byte of each (slot c at byte c), so a plane's words are +// consecutive. tileLayerInternal.h has the contract; PLANAR_TILE_PLANES +// comes with the port's tile shim (port.h). +#ifndef JOEY_PLANAR_TILE_LAYER_H +#define JOEY_PLANAR_TILE_LAYER_H + +#include <stdint.h> + +#include "joey/surface.h" +#include "tileLayerInternal.h" + +#define PLANAR_RING_PLANE_BYTES TILE_LAYER_RING_COLS // a slot is one byte of a plane row +#define PLANAR_RING_PLANE_WORDS (PLANAR_RING_PLANE_BYTES / 2) +#define PLANAR_RING_ROW_BYTES (PLANAR_RING_PLANE_BYTES * PLANAR_TILE_PLANES) +#define PLANAR_RING_SLOT_BYTES (PLANAR_RING_ROW_BYTES * JL_TILE_PIXELS_PER_SIDE) + +void jlPlanarTileLayerCpuBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY); + +#endif diff --git a/src/m68k/sfxMix.s b/src/m68k/sfxMix.s index 0aeefc4..8e3315e 100644 --- a/src/m68k/sfxMix.s +++ b/src/m68k/sfxMix.s @@ -28,8 +28,8 @@ .text .even - .globl audioSfxMixRun - .globl _audioSfxMixRun + .globl jlAudioSfxMixRun + .globl _jlAudioSfxMixRun .equ RUN_SRC, 0 .equ RUN_DST, 4 @@ -83,10 +83,10 @@ addx.w %d6,%d5 .endm -| void audioSfxMixRun(SfxRunT *run): count fits a word (the C's frames are a +| void jlAudioSfxMixRun(SfxRunT *run): count fits a word (the C's frames are a | uint16_t), and the run starts at index 0 of its src. -audioSfxMixRun: -_audioSfxMixRun: +jlAudioSfxMixRun: +_jlAudioSfxMixRun: movem.l %d2-%d7/%a2-%a4,-(%sp) movea.l ARG_RUN(%sp),%a3 move.l RUN_COUNT(%a3),%d7 diff --git a/src/m68k/spriteC2p.c b/src/m68k/spriteC2p.c new file mode 100644 index 0000000..ceb342a --- /dev/null +++ b/src/m68k/spriteC2p.c @@ -0,0 +1,22 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// spriteC2p.h's pair table, in a file of its own: the 68k sprite emitters +// read it on the target (with planarSprite.c's interpreter cache) and in +// the host bakers (make/tools.mk), which link no other 68k source. + +#include <stdint.h> + +#include "spriteC2p.h" + +#define C2P_ROW(_h) \ + C2P_PAIR((_h) * 16u + 0u), C2P_PAIR((_h) * 16u + 1u), C2P_PAIR((_h) * 16u + 2u), C2P_PAIR((_h) * 16u + 3u), \ + C2P_PAIR((_h) * 16u + 4u), C2P_PAIR((_h) * 16u + 5u), C2P_PAIR((_h) * 16u + 6u), C2P_PAIR((_h) * 16u + 7u), \ + C2P_PAIR((_h) * 16u + 8u), C2P_PAIR((_h) * 16u + 9u), C2P_PAIR((_h) * 16u + 10u), C2P_PAIR((_h) * 16u + 11u), \ + C2P_PAIR((_h) * 16u + 12u), C2P_PAIR((_h) * 16u + 13u), C2P_PAIR((_h) * 16u + 14u), C2P_PAIR((_h) * 16u + 15u) + +const uint32_t jlgSpriteC2pPair[256] = { + C2P_ROW(0u), C2P_ROW(1u), C2P_ROW(2u), C2P_ROW(3u), C2P_ROW(4u), C2P_ROW(5u), C2P_ROW(6u), C2P_ROW(7u), + C2P_ROW(8u), C2P_ROW(9u), C2P_ROW(10u), C2P_ROW(11u), C2P_ROW(12u), C2P_ROW(13u), C2P_ROW(14u), C2P_ROW(15u) +}; diff --git a/src/m68k/spriteC2p.h b/src/m68k/spriteC2p.h index 953d1b5..6b784bf 100644 --- a/src/m68k/spriteC2p.h +++ b/src/m68k/spriteC2p.h @@ -5,8 +5,9 @@ // The one chunky->planar conversion of a sprite's 8-pixel column, shared by // the 68k code emitters (spriteEmitC2p.h) and the planar ports' interpreter // cache (planarSprite.c), so compiled and interpreted draws read the same -// plane bits. Never on a draw hot path: emitters run at jlSpriteCompile, -// the cache is built when a sprite is created. +// plane bits. Not on a draw's path, but on every sprite a program makes or +// compiles while it runs: emitters run at jlSpriteCompile, the cache is +// built when a sprite is created. #ifndef JOEYLIB_SPRITE_C2P_H #define JOEYLIB_SPRITE_C2P_H @@ -16,45 +17,34 @@ #include "spriteInternal.h" -static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut); +static inline void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut); + +// A chunky byte's two pixels as their plane bits, one byte a plane (plane 0 +// in the top byte), the left pixel's (the high nibble) in bit 1 and the +// right's in bit 0: jlgSpriteC2pPair[b], in spriteC2p.c. +extern const uint32_t jlgSpriteC2pPair[256]; +#define C2P_PAIR_BITS(_b, _p) ((uint32_t)((((((uint32_t)(_b)) >> (4u + (_p))) & 1u) << 1) | ((((uint32_t)(_b)) >> (_p)) & 1u))) +#define C2P_PAIR(_b) ((C2P_PAIR_BITS((_b), 0u) << 24) | (C2P_PAIR_BITS((_b), 1u) << 16) | (C2P_PAIR_BITS((_b), 2u) << 8) | C2P_PAIR_BITS((_b), 3u)) + +// A transparent pixel sets no plane bit, so the opaque mask is the planes +// ORed together. +typedef char c2pTransparentProof[(TRANSPARENT_NIBBLE == 0) ? 1 : -1]; // Convert one 8-pixel sprite tile column (4 chunky 4bpp bytes) into 4 // plane bytes plus an opaque mask, bit 7 = leftmost pixel. Pixel order // is hi(b0),lo(b0),hi(b1),lo(b1),hi(b2),lo(b2),hi(b3),lo(b3) at bit -// positions 7..0. -static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut) { - uint8_t opaque; - uint8_t i; +// positions 7..0: each byte's pixel pair, looked up, lands two bits +// further right, and every plane's byte of the long fills at once. +static inline void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut) { + uint32_t planes; - plane[0] = 0u; - plane[1] = 0u; - plane[2] = 0u; - plane[3] = 0u; - opaque = 0u; - for (i = 0u; i < 8u; i++) { - uint8_t nibble; - uint8_t bit; - - nibble = (i & 1u) ? (uint8_t)(trp[i >> 1] & 0x0Fu) : (uint8_t)(trp[i >> 1] >> 4); - if (nibble != TRANSPARENT_NIBBLE) { - bit = (uint8_t)(0x80u >> i); - opaque = (uint8_t)(opaque | bit); - if (nibble & 1u) { - plane[0] = (uint8_t)(plane[0] | bit); - } - if (nibble & 2u) { - plane[1] = (uint8_t)(plane[1] | bit); - } - if (nibble & 4u) { - plane[2] = (uint8_t)(plane[2] | bit); - } - if (nibble & 8u) { - plane[3] = (uint8_t)(plane[3] | bit); - } - } - } - *opaqueOut = opaque; + planes = (jlgSpriteC2pPair[trp[0]] << 6) | (jlgSpriteC2pPair[trp[1]] << 4) | (jlgSpriteC2pPair[trp[2]] << 2) | jlgSpriteC2pPair[trp[3]]; + plane[0] = (uint8_t)(planes >> 24); + plane[1] = (uint8_t)(planes >> 16); + plane[2] = (uint8_t)(planes >> 8); + plane[3] = (uint8_t)planes; + *opaqueOut = (uint8_t)(plane[0] | plane[1] | plane[2] | plane[3]); } #endif diff --git a/src/m68k/spriteEmitInterleaved68k.c b/src/m68k/spriteEmitInterleaved68k.c index 7fd4e9a..7df570a 100644 --- a/src/m68k/spriteEmitInterleaved68k.c +++ b/src/m68k/spriteEmitInterleaved68k.c @@ -8,39 +8,67 @@ // one sprite into the ST planar buffer. The sprite's tileData is fixed // at compile time, so the chunky->planar (c2p) conversion and every // destination offset are constant-folded here; the emitted routine is a -// flat unrolled chain of GROUP-level long ops (move.l immediates for -// opaque groups, movem.l whole-group RMW, movep.l byte-lane scatter for -// lone columns -- perf-hunt-2 rank 8) with plane bytes and masks baked -// in as immediates. +// flat unrolled chain of GROUP-level long ops (perf-hunt-2 rank 8) with +// the plane bits and masks baked in. // // Calling convention (m68k cdecl, the routine is invoked through a -// C function pointer by spriteCompiledDraw in spriteCompile.c): +// C function pointer by jlSpriteCompiledDraw in spriteCompile.c): // void draw(uint8_t *groupBase); // groupBase = pd->base + y*160 + (x>>4)*8 (top-left 16-px group). // The single arg sits at 4(sp) on entry (0(sp) = return address). The -// routine loads it into a0, advances a0 by 160 (ST_BYTES_PER_ROW) at -// the start of each subsequent row, and writes groups at d16(a0). -// Only d0/d1/a0 (cdecl scratch) are touched, so no callee-save is -// needed. +// routine loads it into a0 and writes groups at d16(a0) with each row's +// offset (row*160 + group offset) baked in -- no per-row pointer advance +// (an on-surface sprite's rows stay below 200*160 = 32000, inside the +// signed 16-bit displacement). d0/d1 are the scratch; the value pool's +// d2-d7 (below) are saved and restored. // // ST planar layout: each 16-pixel group is 4 plane words (8 bytes) at // group_off + plane*2; the word's high byte (offset +0) holds the left // 8 pixels, the low byte (offset +1) the right 8. A byte-aligned sprite -// tile column writes exactly one half (one plane byte) per plane, so the -// per-plane work is a single byte RMW: -// move.b disp(a0),d0 -// andi.b #~pop,d0 ; clear the opaque pixel bits -// ori.b #planeByte,d0 ; set this plane's bits (omitted if zero) -// move.b d0,disp(a0) -// with the same plane bits the interpreter merges (both convert through -// spriteC2p.h's c2pColumn). +// tile column covers exactly one half (one plane byte per plane), so a +// group takes one or two columns, as two longs: planes 0-1 (D01) at +0 +// and planes 2-3 (D23) at +4, with the keep-mask A (the complement of +// both columns' opacity, repeated) the same for both. The plane bits are +// the ones the interpreter merges (both convert through spriteC2p.h's +// c2pColumn), and a long's data bits always lie inside the drawn bits +// (colour 0 is transparent), so a long whose data is 0 only needs the AND +// and one whose data fills every drawn bit only the OR. Group forms: +// both columns opaque -> move.l D01,d16(a0); move.l D23,d16+4(a0) +// both present, both longs need the AND and the OR +// -> movem.l d16(a0),d0-d1; and.l A,d0; or.l D01,d0; +// and.l A,d1; or.l D23,d1; movem.l d0-d1,d16(a0) +// both present, otherwise, per long +// -> and.l A,d16(a0) and/or or.l D,d16(a0) (one +// memory op, 24 cycles from a register, beats the +// movem pair's share of 30 plus the op) +// one column present -> movep.l over its byte lane (disp, +2, +4, +6; +// odd disp is fine): movep.l D,d16(a0) when the +// column is opaque, else movep.l d16(a0),d0; +// and.l A,d0; or.l D,d0; movep.l d0,d16(a0), +// dropping the AND or the OR as above +// A fully-transparent column inside a present pair contributes mask FF / +// data 00, so the pair's writes leave its bytes bit-identical -- final +// memory matches the old per-plane byte RMW form exactly (the hash gate). +// +// Value pool: every operand above is a long constant, and a few (the +// masks of a sprite's edges, solid runs) recur. A counting pass over the +// routine's planned ops weighs each value by the cycles a register would +// save -- 8 a use (move.l #,d16(a0) 24 -> move.l Dn,d16(a0) 16; andi.l +// #,d16(a0) 32 -> and.l Dn,d16(a0) 24; andi.l #,Dn 16 -> and.l Dn,Dn 8), +// or for a movep store the load into d0 it no longer needs (moveq 4 or +// move.l #imm 12) -- less the register's own load. The best go to d1 +// (free unless a movem-form group pairs it with d0) and then to d2-d7 +// while each still pays its movem save and restore (16 cycles), the +// first of those also the movem pair itself (20). // // The shift argument is the full intra-group phase x & 15 (16 slots on // the ST; see spriteInternal.h). The byte-aligned phases 0 (x mod 16 == // 0) and 8 (x mod 16 == 8) compile to inline code like the above; the -// other 14 compile to a thunk into the shared stSpriteDrawShiftWalker +// other 14 compile to a thunk into the shared jlStSpriteDrawShiftWalker // plus a pre-shifted data table (emitShiftedDraw). +#include <string.h> + #include "joey/sprite.h" #include "joey/surface.h" #include "joey/tile.h" @@ -53,93 +81,326 @@ // Tile geometry (JL_TILE_BYTES / JL_TILE_BYTES_PER_ROW / JL_TILE_PIXELS_PER_SIDE) // comes from joey/tile.h. -#define ST_PLANAR_STRIDE 160u // 320 px * 4 planes / 8 bits -#define ST_GROUP_BYTES 8u // 4 plane words per 16-px group +#define ST_PLANAR_STRIDE 160u // 320 px * 4 planes / 8 bits +#define ST_GROUP_BYTES 8u // 4 plane words per 16-px group +#define ST_LONG_BYTES 4u // the second long of a group (planes 2-3) +#define ST_ROW_OPS_MAX (6u * (JL_SURFACE_WIDTH / 16u + 1u)) // the movem form's 6 ops in every group a row can touch +#define MASK_ALL 0xFFu // every pixel of a column drawn -// 68k opcode words (big-endian), operands follow as separate words: -// movea.l 4(sp),a0 : 206F 0004 (mode 101 = (d16,An); 205F would be (An)+) -// adda.w #160,a0 : D0FC 00A0 -// move.b d16(a0),d0 : 1028 dddd -// andi.b #imm,d0 : 0200 00ii -// ori.b #imm,d0 : 0000 00ii -// move.b d0,d16(a0) : 1140 dddd -// move.b #imm,d16(a0): 117C 00ii dddd (fully-opaque column: direct store) -// rts : 4E75 -// Group forms (perf-hunt-2 rank 8 -- see the emit loop for when each -// fires; all operate on the 8-byte 16-px group or one byte lane of it): -// move.l #imm,d16(a0) : 217C hhhh llll dddd (dual-opaque half-group) -// movem.l d16(a0),d0-d1: 4CE8 0003 dddd (whole-group read) -// movem.l d0-d1,d16(a0): 48E8 0003 dddd (whole-group write; d16(An) -// is a control mode -- NO predec mask reversal) -// andi.l #imm,d0 : 0280 hhhh llll (d1: 0281) -// ori.l #imm,d0 : 0080 hhhh llll (d1: 0081) -// move.l #imm,d0 : 203C hhhh llll -// movep.l d0,d16(a0) : 01C8 dddd (byte-lane scatter, 4 bytes -// movep.l d16(a0),d0 : 0148 dddd at disp,+2,+4,+6; odd disp ok) -#define OP_MOVEA_4SP_A0 0x206Fu -#define OP_ADDA_W_A0 0xD0FCu -#define OP_MOVE_B_A0_D0 0x1028u -#define OP_ANDI_B_D0 0x0200u -#define OP_ORI_B_D0 0x0000u -#define OP_MOVE_B_D0_A0 0x1140u -#define OP_MOVE_B_IMM_A0 0x117Cu -#define OP_RTS 0x4E75u -#define OP_MOVE_L_IMM_A0 0x217Cu -#define OP_MOVEM_L_A0_D01 0x4CE8u -#define OP_MOVEM_L_D01_A0 0x48E8u -#define MOVEM_MASK_D01 0x0003u -#define OP_ANDI_L_D0 0x0280u -#define OP_ANDI_L_D1 0x0281u -#define OP_ORI_L_D0 0x0080u -#define OP_ORI_L_D1 0x0081u -#define OP_MOVE_L_IMM_D0 0x203Cu -#define OP_MOVEP_L_D0_A0 0x01C8u -#define OP_MOVEP_L_A0_D0 0x0148u +// Value pool registers: d1 (scratch, unless the movem form pairs it with +// d0), then d2-d7 (saved). +#define POOL_FIRST_SAVED 2u +#define POOL_LAST 7u +#define POOL_REGS 7u -// Emitted bytes for the trailing rts. -#define EPILOGUE_BYTES 2u +// The counting pass's tally: open addressing over value hashes. A value +// that finds no slot within TALLY_PROBES just stays an immediate. +#define TALLY_SLOTS 512u +#define TALLY_PROBES 16u + +// 68000 cycles the pool weighs (see the header comment). +#define CYC_GAIN_OP 8u // a long immediate operand -> a pool register +#define CYC_MOVEQ 4u +#define CYC_MOVE_L_IMM 12u +#define CYC_SAVED_REG 16u // movem.l save + restore, per register +#define CYC_MOVEM_BASE 20u // movem.l save (8) + restore (12), once + +// Arg displacements on entry (0(sp) = return address). +#define ARG0_DISP 0x0004u +#define ARG1_DISP 0x0008u + +// 68k opcode words (big-endian), operands follow as separate words. K is a +// pool data register; REG_HIGH_SHIFT places a register in bits 9-11. +// movea.l 4(sp),a0 : 206F 0004 (mode 101 = (d16,An); 205F would be (An)+) +// movem.l d2-dK,-(sp) : 48E7 mask (predec mask is bit-reversed: dK = 15-K) +// movem.l (sp)+,d2-dK : 4CDF mask (postinc mask: dK = K) +// moveq #v,dK : 7000|K<<9|v +// move.l #imm,dK : 203C|K<<9 hhhh llll +// move.l dK,d16(a0) : 2140|K dddd move.l #imm,d16(a0) : 217C hhhh llll dddd +// and.l dK,d16(a0) : C1A8|K<<9 dddd andi.l #imm,d16(a0) : 02A8 hhhh llll dddd +// or.l dK,d16(a0) : 81A8|K<<9 dddd ori.l #imm,d16(a0) : 00A8 hhhh llll dddd +// and.l dK,d0 : C080|K andi.l #imm,d0 : 0280 hhhh llll +// or.l dK,d0 : 8080|K ori.l #imm,d0 : 0080 hhhh llll +// and.l dK,d1 : C280|K andi.l #imm,d1 : 0281 hhhh llll +// or.l dK,d1 : 8280|K ori.l #imm,d1 : 0081 hhhh llll +// movem.l d16(a0),d0-d1 : 4CE8 0003 dddd (whole-group read) +// movem.l d0-d1,d16(a0) : 48E8 0003 dddd (whole-group write; d16(An) +// is a control mode -- NO predec mask reversal) +// movep.l d16(a0),d0 : 0148 dddd +// movep.l dK,d16(a0) : 01C8|K<<9 dddd +// rts : 4E75 +#define OP_MOVEA_4SP_A0 0x206Fu +#define OP_MOVEM_SAVE 0x48E7u +#define OP_MOVEM_SAVE_D0 0x8000u +#define OP_MOVEM_REST 0x4CDFu +#define OP_MOVEM_REST_D0 0x0001u +#define OP_MOVEQ_D0 0x7000u +#define OP_MOVE_L_IMM_D0 0x203Cu +#define OP_MOVE_L_DN_A0 0x2140u +#define OP_MOVE_L_IMM_A0 0x217Cu +#define OP_AND_L_DN_A0 0xC1A8u +#define OP_ANDI_L_A0 0x02A8u +#define OP_OR_L_DN_A0 0x81A8u +#define OP_ORI_L_A0 0x00A8u +#define OP_AND_L_DN_D0 0xC080u +#define OP_ANDI_L_D0 0x0280u +#define OP_OR_L_DN_D0 0x8080u +#define OP_ORI_L_D0 0x0080u +#define OP_AND_L_DN_D1 0xC280u +#define OP_ANDI_L_D1 0x0281u +#define OP_OR_L_DN_D1 0x8280u +#define OP_ORI_L_D1 0x0081u +#define OP_MOVEM_L_A0_D01 0x4CE8u +#define OP_MOVEM_L_D01_A0 0x48E8u +#define MOVEM_MASK_D01 0x0003u +#define OP_MOVEP_L_A0_D0 0x0148u +#define OP_MOVEP_L_D0_A0 0x01C8u +#define OP_RTS 0x4E75u +#define REG_HIGH_SHIFT 9u + +// StOpCodeT flags. +#define OP_HAS_VALUE 0x01u +#define OP_HAS_DISP 0x02u + +// Emitted bytes for the trailing rts, the arg's movea.l, and a movem.l +// of the pool's d2-d7. +#define EPILOGUE_BYTES 2u +#define MOVEA_ARG_BYTES 4u +#define MOVEM_POOL_BYTES 4u // Shifted-phase thunk opcodes (see emitShiftedDraw): // movea.l 4(sp),a0 : 206F 0004 (groupBase -- unchanged dispatcher ABI) // lea 16(pc),a1 : 43FA 0010 (data table at thunk+22; ext word at +6) // move.w #imm,d0 : 303C rrrr (rows - 1) // move.w #imm,d1 : 323C gggg (groupsPerRow - 1) -// jmp abs.l : 4EF9 hhhh llll (stSpriteDrawShiftWalker) -#define OP_LEA_16PC_A1 0x43FAu -#define LEA_PC_DISP 0x0010u -#define OP_MOVE_W_IMM_D0 0x303Cu -#define OP_MOVE_W_IMM_D1 0x323Cu -#define OP_JMP_ABSL 0x4EF9u +// jmp abs.l : 4EF9 hhhh llll (jlStSpriteDrawShiftWalker) +#define OP_LEA_16PC_A1 0x43FAu +#define LEA_PC_DISP 0x0010u +#define OP_MOVE_W_IMM_D0 0x303Cu +#define OP_MOVE_W_IMM_D1 0x323Cu +#define OP_JMP_ABSL 0x4EF9u -#define SHIFT_THUNK_BYTES 22u -#define SHIFT_GROUP_DATA_BYTES 12u // notMask long + bits01 long + bits23 long +#define SHIFT_THUNK_BYTES 22u +#define SHIFT_GROUP_DATA_BYTES 12u // notMask long + bits01 long + bits23 long + +// Compiled save/restore copier opcodes (see emitGroupSpanCopy): +// movea.l 4(sp),a0 : 206F 0004 movea.l 8(sp),a1 : 226F 0008 +// move.l (a0)+,(a1)+ : 22D8 +// lea d16(a0),a0 : 41E8 dddd lea d16(a1),a1 : 43E9 dddd +#define OP_MOVEA_8SP_A1 0x226Fu +#define OP_MOVE_L_POSTI 0x22D8u +#define OP_LEA_D16_A0 0x41E8u +#define OP_LEA_D16_A1 0x43E9u + + +// ----- Types ----- + +// Which ops a long needs (see the header comment). +typedef enum { + LONG_AND = 0, // data 0 + LONG_OR, // data fills every drawn bit + LONG_MERGE // both +} LongFormE; + +// One planned op of a row's group writes. +typedef enum { + ST_OP_STORE = 0, // move.l v,d16(a0) + ST_OP_AND_MEM, // and.l v,d16(a0) + ST_OP_OR_MEM, // or.l v,d16(a0) + ST_OP_AND_D0, // and.l v,d0 + ST_OP_OR_D0, // or.l v,d0 + ST_OP_AND_D1, // and.l v,d1 + ST_OP_OR_D1, // or.l v,d1 + ST_OP_MOVEP_STORE, // movep.l v,d16(a0), through d0 when v is not pooled + ST_OP_MOVEM_LOAD, // movem.l d16(a0),d0-d1 + ST_OP_MOVEM_SAVE, // movem.l d0-d1,d16(a0) + ST_OP_MOVEP_LOAD, // movep.l d16(a0),d0 + ST_OP_MOVEP_SAVE // movep.l d0,d16(a0) +} StOpE; + +typedef struct { + uint32_t value; + uint16_t disp; + StOpE kind; +} StOpT; + +// How an StOpE encodes. +typedef struct { + uint16_t regOp; // the pooled form (the pool register at regShift); a fixed op's only form + uint16_t immOp; // the immediate form, the long after it; a movep store's movep.l d0 after the load + uint16_t ext; // a fixed op's extension word (movem's register mask), 0 for none + uint8_t regShift; + uint8_t flags; // OP_HAS_VALUE, OP_HAS_DISP +} StOpCodeT; + + +// ----- Prototypes ----- + +static uint16_t addOp(StOpT *ops, uint16_t n, StOpE kind, uint32_t value, uint16_t disp); +static uint32_t emitGroupSpanCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool strideOnDst); +static uint32_t emitOp(uint8_t *out, uint32_t cursor, const StOpT *op); +static uint32_t emitShiftedDraw(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); +static bool fitsMoveq(uint32_t value); +static uint32_t loadBytes(uint32_t value); +static uint32_t loadCycles(uint32_t value); +static LongFormE longForm(uint32_t mask, uint32_t data); +static uint32_t opBytes(const StOpT *op); +static uint32_t opGain(const StOpT *op); +static uint32_t packLong(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3); +static uint16_t planLongToMem(StOpT *ops, uint16_t n, uint32_t mask, uint32_t data, uint16_t disp); +static uint16_t planRow(const jlSpriteT *sp, uint16_t row, uint8_t xMod16, StOpT *ops); +static void poolBuild(const jlSpriteT *sp, uint8_t xMod16, uint16_t srcH); +static uint8_t poolReg(uint32_t value); +static uint32_t putLoad(uint8_t *out, uint32_t cursor, uint8_t reg, uint32_t value); +static void tally(uint32_t value, uint32_t gain); + + +// ----- Tables and emit-time state ----- + +// Indexed by StOpE. +static const StOpCodeT ST_OP_CODES[] = { + { OP_MOVE_L_DN_A0, OP_MOVE_L_IMM_A0, 0u, 0u, OP_HAS_VALUE | OP_HAS_DISP }, // ST_OP_STORE + { OP_AND_L_DN_A0, OP_ANDI_L_A0, 0u, REG_HIGH_SHIFT, OP_HAS_VALUE | OP_HAS_DISP }, // ST_OP_AND_MEM + { OP_OR_L_DN_A0, OP_ORI_L_A0, 0u, REG_HIGH_SHIFT, OP_HAS_VALUE | OP_HAS_DISP }, // ST_OP_OR_MEM + { OP_AND_L_DN_D0, OP_ANDI_L_D0, 0u, 0u, OP_HAS_VALUE }, // ST_OP_AND_D0 + { OP_OR_L_DN_D0, OP_ORI_L_D0, 0u, 0u, OP_HAS_VALUE }, // ST_OP_OR_D0 + { OP_AND_L_DN_D1, OP_ANDI_L_D1, 0u, 0u, OP_HAS_VALUE }, // ST_OP_AND_D1 + { OP_OR_L_DN_D1, OP_ORI_L_D1, 0u, 0u, OP_HAS_VALUE }, // ST_OP_OR_D1 + { OP_MOVEP_L_D0_A0, OP_MOVEP_L_D0_A0, 0u, REG_HIGH_SHIFT, OP_HAS_VALUE | OP_HAS_DISP }, // ST_OP_MOVEP_STORE + { OP_MOVEM_L_A0_D01, 0u, MOVEM_MASK_D01, 0u, OP_HAS_DISP }, // ST_OP_MOVEM_LOAD + { OP_MOVEM_L_D01_A0, 0u, MOVEM_MASK_D01, 0u, OP_HAS_DISP }, // ST_OP_MOVEM_SAVE + { OP_MOVEP_L_A0_D0, 0u, 0u, 0u, OP_HAS_DISP }, // ST_OP_MOVEP_LOAD + { OP_MOVEP_L_D0_A0, 0u, 0u, 0u, OP_HAS_DISP } // ST_OP_MOVEP_SAVE +}; // The shared shifted-phase walker (src/atarist/spriteWalk.s). Its // absolute address is baked into each thunk's jmp at emit time -- // emission happens at runtime (post-TOS-relocation), and arena moves -// via codegenArenaCompact relocate only the thunk+data, whose one +// via jlCodegenArenaCompact relocate only the thunk+data, whose one // internal reference (lea 16(pc)) is PC-relative. -extern void stSpriteDrawShiftWalker(void); +extern void jlStSpriteDrawShiftWalker(void); -// Per-row widened accumulators for emitShiftedDraw. File statics, NOT -// stack locals: the compile path runs deep and big frames overflowed -// the Amiga shell's default 4 KB task stack (same class on ST/TOS). -// Emit-time only and single-threaded, so statics are safe. -static uint8_t gStM[JL_SURFACE_WIDTH / 8 + 1]; -static uint8_t gStBp[4][JL_SURFACE_WIDTH / 8 + 1]; +// File statics, NOT stack locals: the compile path runs deep and big +// frames overflowed the Amiga shell's default 4 KB task stack (same class +// on ST/TOS). Emit-time only and single-threaded, so statics are safe. +static uint8_t gStM[JL_SURFACE_WIDTH / 8 + 1]; // emitShiftedDraw's widened row mask +static uint8_t gStBp[4][JL_SURFACE_WIDTH / 8 + 1]; // and plane bits +static StOpT gStOps[ST_ROW_OPS_MAX]; // one row's plan +static uint32_t gTallyValue[TALLY_SLOTS]; +static uint32_t gTallyGain[TALLY_SLOTS]; // cycles a register would save; 0 = empty slot +static uint16_t gTallyUsed[TALLY_SLOTS]; // the slots in use, for the pick +static uint16_t gTallyCount; +static uint32_t gPoolValue[POOL_REGS]; // the pool: d<gPoolReg[i]> holds gPoolValue[i] +static uint8_t gPoolReg[POOL_REGS]; +static uint8_t gPoolCount; +static uint8_t gPoolSaved; // how many of the pool are d2-d7 (always the last ones) -// ----- Emit helpers ----- - -// c2pColumn (hi(b0),lo(b0),...,hi(b3),lo(b3) at bit positions 7..0, the -// interpreter's planar cache uses it too) and putWord are shared with -// the Amiga emitter via spriteEmitC2p.h. +// Appends an op to a row's plan; returns the new count. +static uint16_t addOp(StOpT *ops, uint16_t n, StOpE kind, uint32_t value, uint16_t disp) { + ops[n].kind = kind; + ops[n].value = value; + ops[n].disp = disp; + return (uint16_t)(n + 1u); +} -// ----- Emit helpers (shifted phases) ----- +// Compiled save/restore (NATIVE-PERF Phase 1): raw-span copiers over +// the 16-px-group region. A group region's rows are CONTIGUOUS spans +// of interleaved bytes, so each row is a pure move.l chain; the shift +// argument is the width CLASS (0 = ceil(w/16) groups, 1 = one more -- +// see SPRITE_SAVE_CLASS in spriteInternal.h), and only classes 0/1 +// emit. cdecl void copy(const uint8_t *src, uint8_t *dst): SAVE gets +// (screenRowStart, buf) -- a0 strided, a1 contiguous; RESTORE gets +// (buf, screenRowStart) -- a0 contiguous, a1 strided. Byte format is +// identical to jlpSpriteSavePlanes' raw-span branch (format law: +// pure function of the recorded geometry). +static uint32_t emitGroupSpanCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool strideOnDst) { + uint32_t cursor; + uint16_t nGroups; + uint16_t spanBytes; + uint16_t longsPerRow; + uint16_t strideFix; + uint16_t srcH; + int16_t row; + uint16_t i; + + if (shift > 1u) { + return 0u; + } + nGroups = (uint16_t)(((sp->widthPx + 15u) >> 4) + shift); + if ((uint16_t)(nGroups << 4) > JL_SURFACE_WIDTH) { + // Degenerate: the widened region would exceed the surface, so + // this class is unreachable through the on-surface gate. Both + // save and restore bail identically (pair provenance). + return 0u; + } + spanBytes = (uint16_t)(nGroups << 3); + longsPerRow = (uint16_t)(spanBytes >> 2); + strideFix = (uint16_t)(ST_PLANAR_STRIDE - spanBytes); + srcH = (uint16_t)(sp->heightTiles * JL_TILE_PIXELS_PER_SIDE); + + cursor = 0u; + if (cursor + 8u > cap) { + return SPRITE_EMIT_OVERFLOW; + } + cursor = putWord(out, cursor, OP_MOVEA_4SP_A0); + cursor = putWord(out, cursor, ARG0_DISP); + cursor = putWord(out, cursor, OP_MOVEA_8SP_A1); + cursor = putWord(out, cursor, ARG1_DISP); + for (row = 0; row < (int16_t)srcH; row++) { + if (cursor + (uint32_t)(longsPerRow << 1) + 4u + EPILOGUE_BYTES > cap) { + return SPRITE_EMIT_OVERFLOW; + } + for (i = 0u; i < longsPerRow; i++) { + cursor = putWord(out, cursor, OP_MOVE_L_POSTI); + } + if (row < (int16_t)(srcH - 1u)) { + // Advance the strided (screen) side to the next row; the + // buffer side is contiguous and needs no fix. + cursor = putWord(out, cursor, strideOnDst ? OP_LEA_D16_A1 : OP_LEA_D16_A0); + cursor = putWord(out, cursor, strideFix); + } + } + if (cursor + EPILOGUE_BYTES > cap) { + return SPRITE_EMIT_OVERFLOW; + } + cursor = putWord(out, cursor, OP_RTS); + return cursor; +} + + +// Emits one planned op: its pooled form when its value sits in the +// pool, else its immediate form (a movep store loads d0 first). +static uint32_t emitOp(uint8_t *out, uint32_t cursor, const StOpT *op) { + const StOpCodeT *code = &ST_OP_CODES[op->kind]; + uint8_t reg = 0u; + + if ((code->flags & OP_HAS_VALUE) != 0u) { + reg = poolReg(op->value); + } + if ((code->flags & OP_HAS_VALUE) == 0u || reg != 0u) { + cursor = putWord(out, cursor, (uint16_t)(code->regOp | (uint16_t)(reg << code->regShift))); + } else if (op->kind == ST_OP_MOVEP_STORE) { + cursor = putLoad(out, cursor, 0u, op->value); + cursor = putWord(out, cursor, code->immOp); + } else { + cursor = putWord(out, cursor, code->immOp); + cursor = putWord(out, cursor, (uint16_t)(op->value >> 16)); + cursor = putWord(out, cursor, (uint16_t)op->value); + } + if (code->ext != 0u) { + cursor = putWord(out, cursor, code->ext); + } + if ((code->flags & OP_HAS_DISP) != 0u) { + cursor = putWord(out, cursor, op->disp); + } + return cursor; +} + // Emit one sub-byte phase (shift & 7 != 0): a 22-byte thunk that tail- -// jumps into the shared stSpriteDrawShiftWalker, followed by the +// jumps into the shared jlStSpriteDrawShiftWalker, followed by the // phase's pre-shifted data table (SHIFT_GROUP_DATA_BYTES per group per // row). The table is built with the SAME c2pColumn walk the aligned // JIT uses; a source column's bits split across two adjacent widened @@ -182,9 +443,9 @@ static uint32_t emitShiftedDraw(uint8_t *out, uint32_t cap, const jlSpriteT *sp, // tool (tools/spritebake) too; on the 32-bit ST target it is a no-op. The // baked value is don't-care off-target anyway -- the .spc loader re-stamps // the real walker address at load (spritePrecompiled.h). - walker = (uint32_t)(uintptr_t)stSpriteDrawShiftWalker; + walker = (uint32_t)(uintptr_t)jlStSpriteDrawShiftWalker; cursor = putWord(out, cursor, OP_MOVEA_4SP_A0); - cursor = putWord(out, cursor, 0x0004u); + cursor = putWord(out, cursor, ARG0_DISP); cursor = putWord(out, cursor, OP_LEA_16PC_A1); cursor = putWord(out, cursor, LEA_PC_DISP); cursor = putWord(out, cursor, OP_MOVE_W_IMM_D0); @@ -255,282 +516,357 @@ static uint32_t emitShiftedDraw(uint8_t *out, uint32_t cap, const jlSpriteT *sp, } -// ----- Emit API ----- +// Whether value loads with a moveq (it sign-extends from a byte). +static bool fitsMoveq(uint32_t value) { + int32_t s = (int32_t)value; -uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { + return s >= INT8_MIN && s <= INT8_MAX; +} + + +uint32_t jlSpriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t cursor; - uint16_t wTiles; + uint32_t need; uint16_t srcH; - uint16_t xMod16; - int16_t row; - int16_t tileCol; + uint16_t saveMask; + uint16_t restMask; + uint16_t row; + uint16_t n; + uint16_t k; + uint8_t i; // Shift is the full intra-group phase x & 15 (spriteInternal.h). // The byte-aligned phases 0 (x%16==0, high byte halves) and 8 - // (x%16==8, low byte halves) emit the unrolled byte-RMW JIT below; + // (x%16==8, low byte halves) emit the unrolled group JIT below; // the 14 sub-byte phases emit a thunk + pre-shifted data table - // driven by the shared stSpriteDrawShiftWalker. + // driven by the shared jlStSpriteDrawShiftWalker. if ((shift & 7u) != 0u) { return emitShiftedDraw(out, cap, sp, shift); } - wTiles = sp->widthTiles; - srcH = (uint16_t)(sp->heightTiles * JL_TILE_PIXELS_PER_SIDE); - xMod16 = shift; // phase 0 -> 0, phase 8 -> 8 + srcH = (uint16_t)(sp->heightTiles * JL_TILE_PIXELS_PER_SIDE); + poolBuild(sp, shift, srcH); + saveMask = 0u; + restMask = 0u; + need = MOVEA_ARG_BYTES; + for (i = 0u; i < gPoolCount; i++) { + if (gPoolReg[i] >= POOL_FIRST_SAVED) { + saveMask = (uint16_t)(saveMask | (OP_MOVEM_SAVE_D0 >> gPoolReg[i])); + restMask = (uint16_t)(restMask | (OP_MOVEM_REST_D0 << gPoolReg[i])); + } + need += loadBytes(gPoolValue[i]); + } + if (saveMask != 0u) { + need += MOVEM_POOL_BYTES; + } + + // Prologue: movea.l 4(sp),a0 (before any push, so the arg is still + // at 4(sp)), save the pool's d2-d7, load the pool. cursor = 0u; - - // Prologue: movea.l 4(sp),a0 - if (cursor + 4u > cap) { + if (cursor + need > cap) { return SPRITE_EMIT_OVERFLOW; } cursor = putWord(out, cursor, OP_MOVEA_4SP_A0); - cursor = putWord(out, cursor, 0x0004u); + cursor = putWord(out, cursor, ARG0_DISP); + if (saveMask != 0u) { + cursor = putWord(out, cursor, OP_MOVEM_SAVE); + cursor = putWord(out, cursor, saveMask); + } + for (i = 0u; i < gPoolCount; i++) { + cursor = putLoad(out, cursor, gPoolReg[i], gPoolValue[i]); + } - for (row = 0; row < (int16_t)srcH; row++) { - uint16_t tileY = (uint16_t)((uint16_t)row >> 3); - uint16_t inTileY = (uint16_t)((uint16_t)row & 7u); - const uint8_t *tileRowBase = sp->tileData + (uint32_t)tileY * wTiles * JL_TILE_BYTES + (uint32_t)inTileY * JL_TILE_BYTES_PER_ROW; - - if (row > 0) { - // adda.w #160,a0 -- advance to this row's planar base. - if (cursor + 4u > cap) { + // Exact reservation per op: a conservative reserve would make the + // emit pass -- which gets exactly the size the sizing pass reported + // -- falsely overflow on trailing shorter writes. + for (row = 0u; row < srcH; row++) { + n = planRow(sp, row, shift, gStOps); + for (k = 0u; k < n; k++) { + if (cursor + opBytes(&gStOps[k]) > cap) { return SPRITE_EMIT_OVERFLOW; } - cursor = putWord(out, cursor, OP_ADDA_W_A0); - cursor = putWord(out, cursor, ST_PLANAR_STRIDE); - } - - // Group walk (perf-hunt-2 rank 8): consume columns in 16-px - // groups so the whole 8-byte group is written with long ops - // instead of per-plane byte RMWs. Phase 0 pairs columns - // (0,1)(2,3)...; phase 8 starts with a lone low half, then - // pairs (1,2)(3,4)... Forms, cheapest first: - // both columns fully opaque -> two move.l #imm,(d16,a0) - // both present, any partial -> movem.l whole-group RMW d0/d1 - // one column present -> movep.l byte-lane (pure write - // if opaque, RMW if partial) - // A fully-transparent column inside a present pair contributes - // mask FF / data 00, so the movem write-back leaves its bytes - // bit-identical -- final memory matches the old per-plane - // byte-RMW form exactly (the hash gate). d0/d1/a0 are cdecl - // scratch (the shifted-phase thunks already clobber d1). - // Exact reservation per form: a conservative reserve would make - // the emit pass -- which gets exactly the size the sizing pass - // reported -- falsely overflow on trailing shorter writes. - tileCol = 0; - while (tileCol < (int16_t)wTiles) { - uint16_t pixOff = (uint16_t)(xMod16 + (uint16_t)tileCol * 8u); - uint16_t groupByteOff = (uint16_t)((uint16_t)(pixOff >> 4) * ST_GROUP_BYTES); - uint16_t halfByte = (uint16_t)((pixOff & 8u) ? 1u : 0u); - uint8_t planeH[4]; - uint8_t planeL[4]; - uint8_t opqH = 0u; - uint8_t opqL = 0u; - - if (halfByte == 0u) { - c2pColumn(tileRowBase + (uint32_t)tileCol * JL_TILE_BYTES, planeH, &opqH); - if ((int16_t)(tileCol + 1) < (int16_t)wTiles) { - c2pColumn(tileRowBase + (uint32_t)(tileCol + 1) * JL_TILE_BYTES, planeL, &opqL); - tileCol = (int16_t)(tileCol + 2); - } else { - tileCol = (int16_t)(tileCol + 1); - } - } else { - c2pColumn(tileRowBase + (uint32_t)tileCol * JL_TILE_BYTES, planeL, &opqL); - tileCol = (int16_t)(tileCol + 1); - } - - if (opqH == 0u && opqL == 0u) { - continue; // nothing opaque in this group - } - - if (opqH == 0xFFu && opqL == 0xFFu) { - // Dual fully-opaque: all 8 group bytes are constants. - // Two move.l #imm,(d16,a0) -- 16 bytes, ~72 cyc vs the - // old 8 byte-stores' ~128. - uint32_t imm01 = ((uint32_t)planeH[0] << 24) | ((uint32_t)planeL[0] << 16) | ((uint32_t)planeH[1] << 8) | planeL[1]; - uint32_t imm23 = ((uint32_t)planeH[2] << 24) | ((uint32_t)planeL[2] << 16) | ((uint32_t)planeH[3] << 8) | planeL[3]; - - if (cursor + 16u > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVE_L_IMM_A0); - cursor = putWord(out, cursor, (uint16_t)(imm01 >> 16)); - cursor = putWord(out, cursor, (uint16_t)imm01); - cursor = putWord(out, cursor, groupByteOff); - cursor = putWord(out, cursor, OP_MOVE_L_IMM_A0); - cursor = putWord(out, cursor, (uint16_t)(imm23 >> 16)); - cursor = putWord(out, cursor, (uint16_t)imm23); - cursor = putWord(out, cursor, (uint16_t)(groupByteOff + 4u)); - } else if (opqH != 0u && opqL != 0u) { - // Both halves present, at least one partial: whole-group - // movem RMW. d0 = plane 0-1 bytes, d1 = plane 2-3; the - // AND mask is per-column opacity, identical in both regs - // (an opaque half's mask byte is 00 -> full replace). - uint8_t mH = (uint8_t)~opqH; - uint8_t mL = (uint8_t)~opqL; - uint32_t mask = ((uint32_t)mH << 24) | ((uint32_t)mL << 16) | ((uint32_t)mH << 8) | mL; - uint32_t dat01 = ((uint32_t)planeH[0] << 24) | ((uint32_t)planeL[0] << 16) | ((uint32_t)planeH[1] << 8) | planeL[1]; - uint32_t dat23 = ((uint32_t)planeH[2] << 24) | ((uint32_t)planeL[2] << 16) | ((uint32_t)planeH[3] << 8) | planeL[3]; - uint32_t need = (uint32_t)(24u + ((dat01 != 0u) ? 6u : 0u) + ((dat23 != 0u) ? 6u : 0u)); - - if (cursor + need > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVEM_L_A0_D01); - cursor = putWord(out, cursor, MOVEM_MASK_D01); - cursor = putWord(out, cursor, groupByteOff); - cursor = putWord(out, cursor, OP_ANDI_L_D0); - cursor = putWord(out, cursor, (uint16_t)(mask >> 16)); - cursor = putWord(out, cursor, (uint16_t)mask); - if (dat01 != 0u) { - cursor = putWord(out, cursor, OP_ORI_L_D0); - cursor = putWord(out, cursor, (uint16_t)(dat01 >> 16)); - cursor = putWord(out, cursor, (uint16_t)dat01); - } - cursor = putWord(out, cursor, OP_ANDI_L_D1); - cursor = putWord(out, cursor, (uint16_t)(mask >> 16)); - cursor = putWord(out, cursor, (uint16_t)mask); - if (dat23 != 0u) { - cursor = putWord(out, cursor, OP_ORI_L_D1); - cursor = putWord(out, cursor, (uint16_t)(dat23 >> 16)); - cursor = putWord(out, cursor, (uint16_t)dat23); - } - cursor = putWord(out, cursor, OP_MOVEM_L_D01_A0); - cursor = putWord(out, cursor, MOVEM_MASK_D01); - cursor = putWord(out, cursor, groupByteOff); - } else { - // Single present column: movep.l scatters d0's 4 bytes - // across this column's byte lane (disp, +2, +4, +6 = its - // 4 plane bytes; odd disp is fine, movep is byte access). - const uint8_t *pl = (opqH != 0u) ? planeH : planeL; - uint8_t opq = (opqH != 0u) ? opqH : opqL; - uint16_t disp = (uint16_t)(groupByteOff + ((opqH != 0u) ? 0u : 1u)); - uint32_t data = ((uint32_t)pl[0] << 24) | ((uint32_t)pl[1] << 16) | ((uint32_t)pl[2] << 8) | pl[3]; - - if (opq == 0xFFu) { - // move.l #data,d0 ; movep.l d0,(disp,a0) -- 10 bytes, - // ~36 cyc vs the old 4 byte-stores' ~64. - if (cursor + 10u > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVE_L_IMM_D0); - cursor = putWord(out, cursor, (uint16_t)(data >> 16)); - cursor = putWord(out, cursor, (uint16_t)data); - cursor = putWord(out, cursor, OP_MOVEP_L_D0_A0); - cursor = putWord(out, cursor, disp); - } else { - uint8_t m = (uint8_t)~opq; - uint32_t mask = ((uint32_t)m << 24) | ((uint32_t)m << 16) | ((uint32_t)m << 8) | m; - uint32_t need = (uint32_t)(14u + ((data != 0u) ? 6u : 0u)); - - if (cursor + need > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVEP_L_A0_D0); - cursor = putWord(out, cursor, disp); - cursor = putWord(out, cursor, OP_ANDI_L_D0); - cursor = putWord(out, cursor, (uint16_t)(mask >> 16)); - cursor = putWord(out, cursor, (uint16_t)mask); - if (data != 0u) { - cursor = putWord(out, cursor, OP_ORI_L_D0); - cursor = putWord(out, cursor, (uint16_t)(data >> 16)); - cursor = putWord(out, cursor, (uint16_t)data); - } - cursor = putWord(out, cursor, OP_MOVEP_L_D0_A0); - cursor = putWord(out, cursor, disp); - } - } + cursor = emitOp(out, cursor, &gStOps[k]); } } - // rts - if (cursor + EPILOGUE_BYTES > cap) { + // Epilogue: restore the pool's d2-d7, rts. + if (cursor + ((saveMask != 0u) ? MOVEM_POOL_BYTES : 0u) + EPILOGUE_BYTES > cap) { return SPRITE_EMIT_OVERFLOW; } + if (saveMask != 0u) { + cursor = putWord(out, cursor, OP_MOVEM_REST); + cursor = putWord(out, cursor, restMask); + } cursor = putWord(out, cursor, OP_RTS); return cursor; } -// Compiled save/restore (NATIVE-PERF Phase 1): raw-span copiers over -// the 16-px-group region. A group region's rows are CONTIGUOUS spans -// of interleaved bytes, so each row is a pure move.l chain; the shift -// argument is the width CLASS (0 = ceil(w/16) groups, 1 = one more -- -// see SPRITE_SAVE_CLASS in spriteInternal.h), and only classes 0/1 -// emit. cdecl void copy(const uint8_t *src, uint8_t *dst): SAVE gets -// (screenRowStart, buf) -- a0 strided, a1 contiguous; RESTORE gets -// (buf, screenRowStart) -- a0 contiguous, a1 strided. Byte format is -// identical to jlpSpriteSavePlanes' raw-span branch (format law: -// pure function of the recorded geometry). -// movea.l 4(sp),a0 : 206F 0004 movea.l 8(sp),a1 : 226F 0008 -// move.l (a0)+,(a1)+ : 22D8 -// lea d16(a0),a0 : 41E8 dddd lea d16(a1),a1 : 43E9 dddd -#define OP_MOVEA_8SP_A1 0x226Fu -#define OP_MOVE_L_POSTI 0x22D8u -#define OP_LEA_D16_A0 0x41E8u -#define OP_LEA_D16_A1 0x43E9u - -static uint32_t emitGroupSpanCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool strideOnDst) { - uint32_t cursor; - uint16_t nGroups; - uint16_t spanBytes; - uint16_t longsPerRow; - uint16_t strideFix; - uint16_t srcH; - int16_t row; - uint16_t i; - - if (shift > 1u) { - return 0u; - } - nGroups = (uint16_t)(((sp->widthPx + 15u) >> 4) + shift); - if ((uint16_t)(nGroups << 4) > JL_SURFACE_WIDTH) { - // Degenerate: the widened region would exceed the surface, so - // this class is unreachable through the on-surface gate. Both - // save and restore bail identically (pair provenance). - return 0u; - } - spanBytes = (uint16_t)(nGroups << 3); - longsPerRow = (uint16_t)(spanBytes >> 2); - strideFix = (uint16_t)(ST_PLANAR_STRIDE - spanBytes); - srcH = (uint16_t)(sp->heightTiles * JL_TILE_PIXELS_PER_SIDE); - - cursor = 0u; - if (cursor + 8u > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVEA_4SP_A0); - cursor = putWord(out, cursor, 0x0004u); - cursor = putWord(out, cursor, OP_MOVEA_8SP_A1); - cursor = putWord(out, cursor, 0x0008u); - for (row = 0; row < (int16_t)srcH; row++) { - if (cursor + (uint32_t)(longsPerRow << 1) + 4u + EPILOGUE_BYTES > cap) { - return SPRITE_EMIT_OVERFLOW; - } - for (i = 0u; i < longsPerRow; i++) { - cursor = putWord(out, cursor, OP_MOVE_L_POSTI); - } - if (row < (int16_t)(srcH - 1u)) { - // Advance the strided (screen) side to the next row; the - // buffer side is contiguous and needs no fix. - cursor = putWord(out, cursor, strideOnDst ? OP_LEA_D16_A1 : OP_LEA_D16_A0); - cursor = putWord(out, cursor, strideFix); - } - } - if (cursor + EPILOGUE_BYTES > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_RTS); - return cursor; +uint32_t jlSpriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { + return emitGroupSpanCopy(out, cap, sp, shift, true); } -uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +uint32_t jlSpriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { return emitGroupSpanCopy(out, cap, sp, shift, false); } -uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { - return emitGroupSpanCopy(out, cap, sp, shift, true); +// The bytes putLoad puts for value. +static uint32_t loadBytes(uint32_t value) { + return fitsMoveq(value) ? 2u : 6u; +} + + +// The cycles putLoad's load of value costs. +static uint32_t loadCycles(uint32_t value) { + return fitsMoveq(value) ? CYC_MOVEQ : CYC_MOVE_L_IMM; +} + + +// Which ops a long with keep-mask `mask` and plane bits `data` needs. +static LongFormE longForm(uint32_t mask, uint32_t data) { + if (data == 0u) { + return LONG_AND; + } + if (data == ~mask) { + return LONG_OR; + } + return LONG_MERGE; +} + + +// The bytes emitOp puts for op. +static uint32_t opBytes(const StOpT *op) { + const StOpCodeT *code = &ST_OP_CODES[op->kind]; + uint32_t bytes = 2u; + + if (code->ext != 0u) { + bytes += 2u; + } + if ((code->flags & OP_HAS_DISP) != 0u) { + bytes += 2u; + } + if ((code->flags & OP_HAS_VALUE) != 0u && poolReg(op->value) == 0u) { + bytes += (op->kind == ST_OP_MOVEP_STORE) ? loadBytes(op->value) : 4u; + } + return bytes; +} + + +// The cycles op saves when its value sits in the pool. +static uint32_t opGain(const StOpT *op) { + return (op->kind == ST_OP_MOVEP_STORE) ? loadCycles(op->value) : CYC_GAIN_OP; +} + + +// Four bytes as a big-endian long. +static uint32_t packLong(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) { + return ((uint32_t)b0 << 24) | ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; +} + + +// Plans one long of a group as read-modify-writes of memory: the AND, +// the OR, or both (longForm). Returns the new op count. +static uint16_t planLongToMem(StOpT *ops, uint16_t n, uint32_t mask, uint32_t data, uint16_t disp) { + LongFormE form = longForm(mask, data); + + if (form != LONG_OR) { + n = addOp(ops, n, ST_OP_AND_MEM, mask, disp); + } + if (form != LONG_AND) { + n = addOp(ops, n, ST_OP_OR_MEM, data, disp); + } + return n; +} + + +// Plans sprite row `row` at phase xMod16 (0 or 8) as the group writes of +// the header comment, displacements baked. Phase 0 pairs columns +// (0,1)(2,3)...; phase 8 starts with a lone low half, then pairs +// (1,2)(3,4).... Returns the op count. +static uint16_t planRow(const jlSpriteT *sp, uint16_t row, uint8_t xMod16, StOpT *ops) { + uint16_t wTiles = sp->widthTiles; + const uint8_t *tileRowBase = sp->tileData + (uint32_t)(row >> 3) * wTiles * JL_TILE_BYTES + (uint32_t)(row & 7u) * JL_TILE_BYTES_PER_ROW; + uint16_t rowDisp = (uint16_t)(row * ST_PLANAR_STRIDE); + uint16_t tileCol = 0u; + uint16_t n = 0u; + + while (tileCol < wTiles) { + uint16_t pixOff = (uint16_t)(xMod16 + tileCol * 8u); + uint16_t disp = (uint16_t)(rowDisp + (uint16_t)(pixOff >> 4) * ST_GROUP_BYTES); + uint8_t planeH[4]; + uint8_t planeL[4]; + uint8_t opqH = 0u; + uint8_t opqL = 0u; + + if ((pixOff & 8u) == 0u) { + c2pColumn(tileRowBase + (uint32_t)tileCol * JL_TILE_BYTES, planeH, &opqH); + if (tileCol + 1u < wTiles) { + c2pColumn(tileRowBase + (uint32_t)(tileCol + 1u) * JL_TILE_BYTES, planeL, &opqL); + tileCol = (uint16_t)(tileCol + 2u); + } else { + tileCol = (uint16_t)(tileCol + 1u); + } + } else { + c2pColumn(tileRowBase + (uint32_t)tileCol * JL_TILE_BYTES, planeL, &opqL); + tileCol = (uint16_t)(tileCol + 1u); + } + + if (opqH == 0u && opqL == 0u) { + continue; + } + if (opqH == MASK_ALL && opqL == MASK_ALL) { + n = addOp(ops, n, ST_OP_STORE, packLong(planeH[0], planeL[0], planeH[1], planeL[1]), disp); + n = addOp(ops, n, ST_OP_STORE, packLong(planeH[2], planeL[2], planeH[3], planeL[3]), (uint16_t)(disp + ST_LONG_BYTES)); + } else if (opqH != 0u && opqL != 0u) { + uint32_t mask = packLong((uint8_t)~opqH, (uint8_t)~opqL, (uint8_t)~opqH, (uint8_t)~opqL); + uint32_t dat01 = packLong(planeH[0], planeL[0], planeH[1], planeL[1]); + uint32_t dat23 = packLong(planeH[2], planeL[2], planeH[3], planeL[3]); + + if (longForm(mask, dat01) == LONG_MERGE && longForm(mask, dat23) == LONG_MERGE) { + n = addOp(ops, n, ST_OP_MOVEM_LOAD, 0u, disp); + n = addOp(ops, n, ST_OP_AND_D0, mask, 0u); + n = addOp(ops, n, ST_OP_OR_D0, dat01, 0u); + n = addOp(ops, n, ST_OP_AND_D1, mask, 0u); + n = addOp(ops, n, ST_OP_OR_D1, dat23, 0u); + n = addOp(ops, n, ST_OP_MOVEM_SAVE, 0u, disp); + } else { + n = planLongToMem(ops, n, mask, dat01, disp); + n = planLongToMem(ops, n, mask, dat23, (uint16_t)(disp + ST_LONG_BYTES)); + } + } else { + const uint8_t *pl = (opqH != 0u) ? planeH : planeL; + uint8_t opq = (opqH != 0u) ? opqH : opqL; + uint16_t lane = (uint16_t)(disp + ((opqH != 0u) ? 0u : 1u)); + uint32_t data = packLong(pl[0], pl[1], pl[2], pl[3]); + + if (opq == MASK_ALL) { + n = addOp(ops, n, ST_OP_MOVEP_STORE, data, lane); + } else { + uint8_t m = (uint8_t)~opq; + uint32_t mask = packLong(m, m, m, m); + LongFormE form = longForm(mask, data); + + n = addOp(ops, n, ST_OP_MOVEP_LOAD, 0u, lane); + if (form != LONG_OR) { + n = addOp(ops, n, ST_OP_AND_D0, mask, 0u); + } + if (form != LONG_AND) { + n = addOp(ops, n, ST_OP_OR_D0, data, 0u); + } + n = addOp(ops, n, ST_OP_MOVEP_SAVE, 0u, lane); + } + } + } + return n; +} + + +// The counting pass over every row's plan, then the pool: the value +// saving the most cycles goes to d1 (when the movem form leaves it free) +// while it beats its load, the next ones to d2-d7 while each also beats +// its movem save and restore, and the d2-d7 lot only if together they +// also pay for the movem pair. +static void poolBuild(const jlSpriteT *sp, uint8_t xMod16, uint16_t srcH) { + int32_t bestNet; + int32_t savedNet = 0; + uint16_t bestSlot; + uint16_t row; + uint16_t n; + uint16_t k; + uint8_t reg; + bool d1Free = true; + + memset(gTallyGain, 0, sizeof(gTallyGain)); + gTallyCount = 0u; + for (row = 0u; row < srcH; row++) { + n = planRow(sp, row, xMod16, gStOps); + for (k = 0u; k < n; k++) { + if (gStOps[k].kind == ST_OP_MOVEM_LOAD) { + d1Free = false; + } + if ((ST_OP_CODES[gStOps[k].kind].flags & OP_HAS_VALUE) != 0u) { + tally(gStOps[k].value, opGain(&gStOps[k])); + } + } + } + + gPoolCount = 0u; + gPoolSaved = 0u; + for (reg = d1Free ? 1u : POOL_FIRST_SAVED; reg <= POOL_LAST; reg++) { + bestNet = 0; + bestSlot = 0u; + for (k = 0u; k < gTallyCount; k++) { + uint16_t slot = gTallyUsed[k]; + int32_t net = (int32_t)gTallyGain[slot] - (int32_t)loadCycles(gTallyValue[slot]); + + if (net > bestNet) { + bestNet = net; + bestSlot = slot; + } + } + if (bestNet <= ((reg < POOL_FIRST_SAVED) ? 0 : (int32_t)CYC_SAVED_REG)) { + break; + } + gPoolReg[gPoolCount] = reg; + gPoolValue[gPoolCount] = gTallyValue[bestSlot]; + gPoolCount++; + if (reg >= POOL_FIRST_SAVED) { + gPoolSaved++; + savedNet += bestNet - (int32_t)CYC_SAVED_REG; + } + gTallyGain[bestSlot] = 0u; + } + if (gPoolSaved != 0u && savedNet <= (int32_t)CYC_MOVEM_BASE) { + gPoolCount = (uint8_t)(gPoolCount - gPoolSaved); + gPoolSaved = 0u; + } +} + + +// The pool register holding value; 0 = none (d0 is never pooled). +static uint8_t poolReg(uint32_t value) { + uint8_t i; + + for (i = 0u; i < gPoolCount; i++) { + if (gPoolValue[i] == value) { + return gPoolReg[i]; + } + } + return 0u; +} + + +// Puts the load of value into d<reg>: moveq when it sign-extends from a +// byte, else move.l #imm. +static uint32_t putLoad(uint8_t *out, uint32_t cursor, uint8_t reg, uint32_t value) { + if (fitsMoveq(value)) { + return putWord(out, cursor, (uint16_t)(OP_MOVEQ_D0 | (uint16_t)(reg << REG_HIGH_SHIFT) | (uint8_t)value)); + } + cursor = putWord(out, cursor, (uint16_t)(OP_MOVE_L_IMM_D0 | (uint16_t)(reg << REG_HIGH_SHIFT))); + cursor = putWord(out, cursor, (uint16_t)(value >> 16)); + return putWord(out, cursor, (uint16_t)value); +} + + +// Adds gain to value's tally. +static void tally(uint32_t value, uint32_t gain) { + uint16_t slot = (uint16_t)((value ^ (value >> 9) ^ (value >> 18) ^ (value >> 27)) & (TALLY_SLOTS - 1u)); + uint8_t probe; + + for (probe = 0u; probe < TALLY_PROBES; probe++) { + if (gTallyGain[slot] == 0u) { + gTallyValue[slot] = value; + gTallyGain[slot] = gain; + gTallyUsed[gTallyCount] = slot; + gTallyCount++; + return; + } + if (gTallyValue[slot] == value) { + gTallyGain[slot] += gain; + return; + } + slot = (uint16_t)((slot + 1u) & (TALLY_SLOTS - 1u)); + } } diff --git a/src/m68k/spriteEmitPlanar68k.c b/src/m68k/spriteEmitPlanar68k.c index 37c10cb..fa13184 100644 --- a/src/m68k/spriteEmitPlanar68k.c +++ b/src/m68k/spriteEmitPlanar68k.c @@ -2,25 +2,26 @@ // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // -// Planar 68k sprite codegen for the Amiga (4 separate bitplanes). +// Planar 68k sprite codegen for the Amiga and the X68000 (4 separate +// bitplanes). // // Emits a position-independent, cdecl-callable DRAW routine that blits -// one sprite into the Amiga's 4 bitplanes. The sprite's tileData is -// fixed at compile time, so the chunky->planar (c2p) conversion and -// every destination offset are constant-folded here; the emitted -// routine is a flat unrolled chain of byte writes with the plane bytes -// and masks baked in as immediates. +// one sprite into the 4 bitplanes. The sprite's tileData is fixed at +// compile time, so the chunky->planar (c2p) conversion and every +// destination offset are constant-folded here; the emitted routine is a +// flat unrolled chain of byte writes with the plane bytes and masks baked +// in. // // Calling convention (m68k cdecl, invoked through a C function pointer -// by spriteCompiledDraw in spriteCompile.c): +// by jlSpriteCompiledDraw in spriteCompile.c): // void draw(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3); // each p_i = plane_base + y*40 + x/8 (top-left byte of plane i). -// Args sit at 4/8/12/16(sp) on entry. The routine saves a2/a3, loads -// the four plane pointers into a0..a3, then writes plane bytes at -// d16(a_i) with the offset (row*40 + byteCol) baked in -- no per-row -// pointer advance (offsets stay well inside the 8000-byte plane and the -// signed 16-bit displacement range for any on-surface sprite). Only -// d0/a0/a1 are cdecl scratch; a2/a3 are saved/restored via movem. +// Args sit at 4/8/12/16(sp) on entry. The routine saves a2/a3 (and the +// value pool's d2-d7, below), loads the four plane pointers into a0..a3, +// then writes plane bytes at d16(a_i) with the offset (row*40 + byteCol) +// baked in -- no per-row pointer advance (offsets stay well inside the +// 8000-byte plane and the signed 16-bit displacement range for any +// on-surface sprite). d0 is the merge scratch. // // ALL 8 shifts (x mod 8) are compiled (NATIVE-PERF Phase 1). At shift 0 // a sprite tile column maps to exactly one plane byte; at shift s > 0 a @@ -29,14 +30,31 @@ // between two source columns receives disjoint contributions from both. // The emitter pre-shifts and OR-merges those contributions per dest // byte at emit time (destMask/destData accumulators), then emits one -// write group per non-empty dest byte -- the same opcode inventory the -// proven shift-0 path used: -// fully opaque (mask==0xFF): move.b #data,off(a_i) (direct store) -// mixed: move.b off(a_i),d0; andi.b #~mask,d0; [ori.b #data,d0;] -// move.b d0,off(a_i) +// write per plane of every non-empty dest byte, in the cheapest form the +// byte's mask and that plane's data allow (colour 0 is transparent, so a +// plane's data bits always lie inside the mask): +// mask 0xFF: move.b data,off(a_i) store +// data 0: and.b ~mask,off(a_i) clear the drawn bits +// data == mask: or.b mask,off(a_i) set the drawn bits +// otherwise: move.b off(a_i),d0; and.b ~mask,d0; or.b data,d0; +// move.b d0,off(a_i) merge +// The clear and set forms are one 20-cycle read-modify-write of memory +// (16 from a register) where the merge costs 32 and 40. // This is algebraically identical to jlpSpriteDrawPlanes' two sequential // seam RMWs (disjoint masks compose), so pixels are bit-for-bit the // walker's -- the frozen unaligned/sweep16 golden hashes gate it. +// +// Value pool: every operand above is a byte constant, and a few (0x00, +// 0xFF, the edge masks) recur all over a sprite. A counting pass over the +// whole sprite finds the most used ones and the prologue loads them into +// data registers with moveq. Each use of a register instead of an +// immediate saves 4 cycles and 2 bytes (move.b #,d16(An) 16 -> move.b +// Dn,d16(An) 12; andi.b #,d16(An) 20 -> and.b Dn,d16(An) 16; andi.b #,d0 +// 8 -> and.b Dn,d0 4). d1 is cdecl scratch, so its value only has to pay +// for its moveq (4 cycles: 2 uses); d2-d7 also have to be saved and +// restored (movem 8 + 8 more: 6 uses). + +#include <string.h> #include "joey/sprite.h" #include "joey/surface.h" @@ -50,197 +68,64 @@ // Tile geometry (JL_TILE_BYTES / JL_TILE_BYTES_PER_ROW / JL_TILE_PIXELS_PER_SIDE) // comes from joey/tile.h. -#define AMIGA_PLANE_STRIDE (JL_SURFACE_WIDTH / 8) // 40 bytes/plane-row -#define AMIGA_PLANES 4u +#define AMIGA_PLANE_STRIDE (JL_SURFACE_WIDTH / 8) // 40 bytes/plane-row +#define AMIGA_PLANES 4u +#define DEST_BYTES (JL_SURFACE_WIDTH / 8 + 1) // a shifted row spills one byte +#define BYTE_VALUES 256u +#define MASK_ALL 0xFFu // every pixel of a dest byte drawn -// 68k opcode words (big-endian). Per-plane move.b variants are indexed -// by plane (a0..a3); andi.b/ori.b only ever target d0. -// movem.l a2-a3,-(sp) : 48E7 0030 (predec mask is bit-reversed: a3=4,a2=5) -// movem.l (sp)+,a2-a3 : 4CDF 0C00 (postinc mask: a2=10,a3=11) -// movea.l d16(sp),aN : 206F/226F/246F/266F dddd -// move.b d16(aN),d0 : 1028/1029/102A/102B dddd -// andi.b #imm,d0 : 0200 00ii -// ori.b #imm,d0 : 0000 00ii -// move.b d0,d16(aN) : 1140/1340/1540/1740 dddd -// move.b #imm,d16(aN): 117C/137C/157C/177C 00ii dddd -// rts : 4E75 -#define OP_MOVEM_SAVE 0x48E7u -#define OP_MOVEM_SAVE_M 0x0030u -#define OP_MOVEM_REST 0x4CDFu -#define OP_MOVEM_REST_M 0x0C00u -#define OP_ANDI_B_D0 0x0200u -#define OP_ORI_B_D0 0x0000u -#define OP_RTS 0x4E75u +// Value pool registers: d1 (scratch), then d2-d7 (saved). +#define POOL_FIRST_SAVED 2u +#define POOL_LAST 7u +#define POOL_MIN_USES_SCRATCH 2u // moveq 4 cycles < 2 uses x 4 +#define POOL_MIN_USES_SAVED 6u // moveq 4 + movem 8 + 8 = 20 < 6 uses x 4 -static const uint16_t OP_MOVEA_SP_AN[AMIGA_PLANES] = { 0x206Fu, 0x226Fu, 0x246Fu, 0x266Fu }; -static const uint16_t OP_MOVE_B_AN_D0[AMIGA_PLANES] = { 0x1028u, 0x1029u, 0x102Au, 0x102Bu }; -static const uint16_t OP_MOVE_B_D0_AN[AMIGA_PLANES] = { 0x1140u, 0x1340u, 0x1540u, 0x1740u }; -static const uint16_t OP_MOVE_B_IMM_AN[AMIGA_PLANES] = { 0x117Cu, 0x137Cu, 0x157Cu, 0x177Cu }; +// 68k opcode words (big-endian). N is a plane's address register (a0..a3), +// K a data register; REG_HIGH_SHIFT places a register in bits 9-11. +// movem.l d2-dK/a2-a3,-(sp) : 48E7 mask (predec mask is bit-reversed: dK=15-K, a2=5, a3=4) +// movem.l (sp)+,d2-dK/a2-a3 : 4CDF mask (postinc mask: dK=K, a2=10, a3=11) +// movea.l d16(sp),aN : 206F|N<<9 dddd +// moveq #v,dK : 7000|K<<9|v +// move.b dK,d16(aN) : 1140|N<<9|K dddd (d0 is the merge's store) +// move.b #imm,d16(aN) : 117C|N<<9 00ii dddd +// and.b dK,d16(aN) : C128|K<<9|N dddd +// andi.b #imm,d16(aN) : 0228|N 00ii dddd +// or.b dK,d16(aN) : 8128|K<<9|N dddd +// ori.b #imm,d16(aN) : 0028|N 00ii dddd +// move.b d16(aN),d0 : 1028|N dddd +// and.b dK,d0 : C000|K +// andi.b #imm,d0 : 0200 00ii +// or.b dK,d0 : 8000|K +// ori.b #imm,d0 : 0000 00ii +// rts : 4E75 +#define OP_MOVEM_SAVE 0x48E7u +#define OP_MOVEM_SAVE_M 0x0030u +#define OP_MOVEM_SAVE_D0 0x8000u +#define OP_MOVEM_REST 0x4CDFu +#define OP_MOVEM_REST_M 0x0C00u +#define OP_MOVEM_REST_D0 0x0001u +#define OP_MOVEA_SP_A0 0x206Fu +#define OP_MOVEQ_D0 0x7000u +#define OP_MOVE_B_DN_A0 0x1140u +#define OP_MOVE_B_IMM_A0 0x117Cu +#define OP_AND_B_DN_A0 0xC128u +#define OP_ANDI_B_A0 0x0228u +#define OP_OR_B_DN_A0 0x8128u +#define OP_ORI_B_A0 0x0028u +#define OP_MOVE_B_A0_D0 0x1028u +#define OP_AND_B_DN_D0 0xC000u +#define OP_ANDI_B_D0 0x0200u +#define OP_OR_B_DN_D0 0x8000u +#define OP_ORI_B_D0 0x0000u +#define OP_RTS 0x4E75u +#define REG_HIGH_SHIFT 9u -// Stack displacement of arg p_i AFTER the prologue pushes a2/a3 (8 bytes): -// entry 4/8/12/16(sp) -> 12/16/20/24(sp). -#define ARG_DISP(i) (uint16_t)(12u + (uint16_t)(i) * 4u) - -#define PROLOGUE_BYTES 20u // movem(4) + 4 * movea.l(4) -#define EPILOGUE_BYTES 6u // movem(4) + rts(2) - - -// ----- Emit helpers ----- - -// c2pColumn (the conversion the interpreter's planar cache is built with) -// and putWord are shared with the ST emitter via spriteEmitC2p.h. - - -// ----- Emit API ----- - -// Per-row accumulators. File statics, NOT stack locals: the compile -// path runs deep (jlSpriteCompile -> planBuild -> measure -> emitter, -// with vsnprintf-depth logging possible underneath) and a ~200-byte -// stack frame here overflowed the Amiga shell's default 4 KB task -// stack nondeterministically. Emit-time only and single-threaded, so -// statics are safe. -static uint8_t gDestMask[JL_SURFACE_WIDTH / 8 + 1]; -static uint8_t gDestData[JL_SURFACE_WIDTH / 8 + 1][AMIGA_PLANES]; - - -uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { - uint32_t cursor; - uint16_t wTiles; - uint16_t srcH; - uint16_t lastDest; - uint8_t i; - int16_t row; - int16_t byteCol; - int16_t d; - - if (shift >= 8u) { - return 0u; // Amiga shift space is x & 7; defensive. - } - if (shift != 0u && sp->widthPx >= JL_SURFACE_WIDTH) { - // A full-width sprite can never draw unaligned on-surface (the - // dispatcher gate requires x + w <= 320 with x > 0), so a - // shifted variant would be dead bytes in the arena. - return 0u; - } - wTiles = sp->widthTiles; - srcH = (uint16_t)(sp->heightTiles * JL_TILE_PIXELS_PER_SIDE); - // Shift 0 writes wTiles dest bytes per row; shifted rows spill one. - lastDest = (shift == 0u) ? (uint16_t)(wTiles - 1u) : wTiles; - - cursor = 0u; - - // Prologue: save a2/a3, load p0..p3 into a0..a3. - if (cursor + PROLOGUE_BYTES > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVEM_SAVE); - cursor = putWord(out, cursor, OP_MOVEM_SAVE_M); - for (i = 0u; i < AMIGA_PLANES; i++) { - cursor = putWord(out, cursor, OP_MOVEA_SP_AN[i]); - cursor = putWord(out, cursor, ARG_DISP(i)); - } - - for (row = 0; row < (int16_t)srcH; row++) { - uint16_t tileY = (uint16_t)((uint16_t)row >> 3); - uint16_t inTileY = (uint16_t)((uint16_t)row & 7u); - const uint8_t *tileRowBase = sp->tileData + (uint32_t)tileY * wTiles * JL_TILE_BYTES + (uint32_t)inTileY * JL_TILE_BYTES_PER_ROW; - uint16_t rowOff = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE); - - // Phase 1: accumulate this row's per-dest-byte mask/data. A - // source column at shift s contributes its low 8-s bits to dest - // byte c (>> s) and its high s bits to dest byte c+1 (<< 8-s); - // the seam byte's two contributions occupy disjoint bit ranges, - // so OR-accumulation reproduces the walker's two sequential - // RMWs exactly. - for (d = 0; d <= (int16_t)lastDest; d++) { - gDestMask[d] = 0u; - gDestData[d][0] = 0u; - gDestData[d][1] = 0u; - gDestData[d][2] = 0u; - gDestData[d][3] = 0u; - } - for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) { - const uint8_t *trp = tileRowBase + (uint32_t)byteCol * JL_TILE_BYTES; - uint8_t plane[4]; - uint8_t opaque; - - c2pColumn(trp, plane, &opaque); - if (opaque == 0u) { - continue; - } - if (shift == 0u) { - gDestMask[byteCol] = (uint8_t)(gDestMask[byteCol] | opaque); - for (i = 0u; i < AMIGA_PLANES; i++) { - gDestData[byteCol][i] = (uint8_t)(gDestData[byteCol][i] | plane[i]); - } - } else { - gDestMask[byteCol] = (uint8_t)(gDestMask[byteCol] | (uint8_t)(opaque >> shift)); - gDestMask[byteCol + 1] = (uint8_t)(gDestMask[byteCol + 1] | (uint8_t)(opaque << (8u - shift))); - for (i = 0u; i < AMIGA_PLANES; i++) { - gDestData[byteCol][i] = (uint8_t)(gDestData[byteCol][i] | (uint8_t)(plane[i] >> shift)); - gDestData[byteCol + 1][i] = (uint8_t)(gDestData[byteCol + 1][i] | (uint8_t)(plane[i] << (8u - shift))); - } - } - } - - // Phase 2: emit one write group per non-empty dest byte. - for (d = 0; d <= (int16_t)lastDest; d++) { - uint8_t m = gDestMask[d]; - uint16_t off; - - if (m == 0u) { - continue; - } - off = (uint16_t)(rowOff + (uint16_t)d); - - for (i = 0u; i < AMIGA_PLANES; i++) { - if (m == 0xFFu) { - // Fully covered dest byte: store the data directly. - // move.b #data,off(a_i) (6 bytes) - if (cursor + 6u > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVE_B_IMM_AN[i]); - cursor = putWord(out, cursor, (uint16_t)gDestData[d][i]); - cursor = putWord(out, cursor, off); - } else { - // Exact reservation (see ST emitter): a worst-case reserve - // would falsely overflow the emit pass on trailing shorter - // writes. +ori.b only when this plane sets bits. - uint32_t planeBytes = (gDestData[d][i] != 0u) ? 16u : 12u; - - if (cursor + planeBytes > cap) { - return SPRITE_EMIT_OVERFLOW; - } - // move.b off(a_i),d0 - cursor = putWord(out, cursor, OP_MOVE_B_AN_D0[i]); - cursor = putWord(out, cursor, off); - // andi.b #~m,d0 - cursor = putWord(out, cursor, OP_ANDI_B_D0); - cursor = putWord(out, cursor, (uint16_t)(uint8_t)(~m)); - // ori.b #data,d0 (skip if no bits) - if (gDestData[d][i] != 0u) { - cursor = putWord(out, cursor, OP_ORI_B_D0); - cursor = putWord(out, cursor, (uint16_t)gDestData[d][i]); - } - // move.b d0,off(a_i) - cursor = putWord(out, cursor, OP_MOVE_B_D0_AN[i]); - cursor = putWord(out, cursor, off); - } - } - } - } - - // Epilogue: restore a2/a3, return. - if (cursor + EPILOGUE_BYTES > cap) { - return SPRITE_EMIT_OVERFLOW; - } - cursor = putWord(out, cursor, OP_MOVEM_REST); - cursor = putWord(out, cursor, OP_MOVEM_REST_M); - cursor = putWord(out, cursor, OP_RTS); - return cursor; -} +// Stack displacement of arg p_i after the prologue's movem: the return PC, +// a2/a3, and the nSaved pool registers sit below the args. +#define ARG_DISP(i, nSaved) (uint16_t)(4u * (3u + (uint16_t)(nSaved) + (uint16_t)(i))) +#define PROLOGUE_BYTES 20u // movem(4) + 4 * movea.l(4); each pool moveq adds 2 +#define EPILOGUE_BYTES 6u // movem(4) + rts(2) // Compiled save/restore (NATIVE-PERF Phase 1): plane-run copiers over // the 16-px-group region. shift is the width CLASS (0 = ceil(w/16) @@ -260,8 +145,8 @@ uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp // SAVE move.w d16(aP),(a1)+ : 32E8/32EA/32EB/32EC dddd // REST move.l (a1)+,d16(aP) : 2159/2559/2759/2959 dddd // REST move.w (a1)+,d16(aP) : 3159/3559/3759/3959 dddd -#define OP_MOVEM_SAVE_A24 0x0038u -#define OP_MOVEM_REST_A24 0x1C00u +#define OP_MOVEM_SAVE_A24 0x0038u +#define OP_MOVEM_REST_A24 0x1C00u static const uint16_t OP_COPY_ARG_AN[5] = { 0x206Fu, 0x246Fu, 0x266Fu, 0x286Fu, 0x226Fu }; static const uint16_t OP_SAVE_L_AN[AMIGA_PLANES] = { 0x22E8u, 0x22EAu, 0x22EBu, 0x22ECu }; @@ -271,7 +156,114 @@ static const uint16_t OP_REST_W_AN[AMIGA_PLANES] = { 0x3159u, 0x3559u, 0x3759u, // Arg displacements after the 12-byte movem push (+4 return PC): // p0/p1/p2/p3/buf at entry 4/8/12/16/20 -> 16/20/24/28/32. -#define COPY_ARG_DISP(i) (uint16_t)(16u + (uint16_t)(i) * 4u) +#define COPY_ARG_DISP(i) (uint16_t)(16u + (uint16_t)(i) * 4u) + + +// ----- Types ----- + +// How one plane byte is written (see the header comment). +typedef enum { + WRITE_STORE = 0, + WRITE_AND, + WRITE_OR, + WRITE_MERGE +} WriteFormE; + + +// ----- Prototypes ----- + +static void accumulateRow(const jlSpriteT *sp, uint16_t row, uint8_t shift, uint16_t lastDest); +static void countValue(uint8_t value); +static void countWrite(uint8_t mask, uint8_t data); +static uint32_t emitPlaneRunCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool isRestore); +static uint32_t emitValueToPlane(uint8_t *out, uint32_t cursor, uint32_t cap, uint16_t regOp, uint8_t regShift, uint16_t immOp, uint8_t value, uint16_t off); +static uint32_t emitWrite(uint8_t *out, uint32_t cursor, uint32_t cap, uint8_t plane, uint8_t mask, uint8_t data, uint16_t off); +static uint32_t emitWrites(uint8_t *out, uint32_t cursor, uint32_t cap, const jlSpriteT *sp, uint8_t shift, uint16_t lastDest); +static uint8_t poolBuild(const jlSpriteT *sp, uint8_t shift, uint16_t lastDest); +static uint32_t putValueOp(uint8_t *out, uint32_t cursor, uint16_t regOp, uint8_t regShift, uint16_t immOp, uint8_t value); +static uint32_t valueOpBytes(uint8_t value); +static WriteFormE writeForm(uint8_t mask, uint8_t data); + + +// ----- Emit-time state ----- + +// File statics, NOT stack locals: the compile path runs deep +// (jlSpriteCompile -> planBuild -> measure -> emitter, with +// vsnprintf-depth logging possible underneath) and a ~200-byte stack +// frame here overflowed the Amiga shell's default 4 KB task stack +// nondeterministically. Emit-time only and single-threaded, so statics +// are safe. +static uint8_t gDestMask[DEST_BYTES]; // one row's drawn bits per dest byte +static uint8_t gDestData[DEST_BYTES][AMIGA_PLANES]; // one row's plane bits per dest byte +static uint8_t gPoolValue[POOL_LAST + 1u]; // the value d1..d7 hold +static uint8_t gSeen[BYTE_VALUES]; // the distinct values counted, in first-use order +static uint16_t gSeenCount; +static uint8_t gValueReg[BYTE_VALUES]; // the data register holding a value; 0 = immediate (d0 is never pooled) +static uint32_t gValueUses[BYTE_VALUES]; // the counting pass's tally + + +// Accumulates sprite row `row` into gDestMask/gDestData. A source column +// at shift s contributes its low 8-s bits to dest byte c (>> s) and its +// high s bits to dest byte c+1 (<< 8-s); the seam byte's two +// contributions occupy disjoint bit ranges, so OR-accumulation reproduces +// the walker's two sequential RMWs exactly. At shift 0 the c+1 share is +// empty. +static void accumulateRow(const jlSpriteT *sp, uint16_t row, uint8_t shift, uint16_t lastDest) { + uint16_t wTiles = sp->widthTiles; + const uint8_t *tileRowBase = sp->tileData + (uint32_t)(row >> 3) * wTiles * JL_TILE_BYTES + (uint32_t)(row & 7u) * JL_TILE_BYTES_PER_ROW; + uint16_t byteCol; + uint8_t i; + + memset(gDestMask, 0, (size_t)lastDest + 1u); + memset(gDestData, 0, ((size_t)lastDest + 1u) * AMIGA_PLANES); + for (byteCol = 0u; byteCol < wTiles; byteCol++) { + uint8_t plane[AMIGA_PLANES]; + uint8_t opaque; + + c2pColumn(tileRowBase + (uint32_t)byteCol * JL_TILE_BYTES, plane, &opaque); + if (opaque == 0u) { + continue; + } + gDestMask[byteCol] = (uint8_t)(gDestMask[byteCol] | (uint8_t)(opaque >> shift)); + gDestMask[byteCol + 1] = (uint8_t)(gDestMask[byteCol + 1] | (uint8_t)(opaque << (8u - shift))); + for (i = 0u; i < AMIGA_PLANES; i++) { + gDestData[byteCol][i] = (uint8_t)(gDestData[byteCol][i] | (uint8_t)(plane[i] >> shift)); + gDestData[byteCol + 1][i] = (uint8_t)(gDestData[byteCol + 1][i] | (uint8_t)(plane[i] << (8u - shift))); + } + } +} + + +// Tallies one use of value, listing it in gSeen on its first. +static void countValue(uint8_t value) { + if (gValueUses[value] == 0u) { + gSeen[gSeenCount] = value; + gSeenCount++; + } + gValueUses[value]++; +} + + +// The counting pass's half of emitWrite: tallies the operand bytes the +// write will use. +static void countWrite(uint8_t mask, uint8_t data) { + switch (writeForm(mask, data)) { + case WRITE_STORE: + countValue(data); + break; + case WRITE_AND: + countValue((uint8_t)~mask); + break; + case WRITE_OR: + countValue(mask); + break; + case WRITE_MERGE: + countValue((uint8_t)~mask); + countValue(data); + break; + } +} + static uint32_t emitPlaneRunCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool isRestore) { uint32_t cursor; @@ -337,11 +329,214 @@ static uint32_t emitPlaneRunCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp } -uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +// Emits "op value,off(a_plane)": regOp/immOp already carry the plane. +// Returns the new cursor or SPRITE_EMIT_OVERFLOW. +static uint32_t emitValueToPlane(uint8_t *out, uint32_t cursor, uint32_t cap, uint16_t regOp, uint8_t regShift, uint16_t immOp, uint8_t value, uint16_t off) { + if (cursor + valueOpBytes(value) + 2u > cap) { + return SPRITE_EMIT_OVERFLOW; + } + cursor = putValueOp(out, cursor, regOp, regShift, immOp, value); + return putWord(out, cursor, off); +} + + +// Emits the write of one plane byte in its WriteFormE form. Returns the +// new cursor or SPRITE_EMIT_OVERFLOW. Reservations are exact (see the ST +// emitter): a worst-case reserve would falsely overflow the emit pass on +// trailing shorter writes. +static uint32_t emitWrite(uint8_t *out, uint32_t cursor, uint32_t cap, uint8_t plane, uint8_t mask, uint8_t data, uint16_t off) { + uint8_t notMask = (uint8_t)~mask; + uint16_t planeHigh = (uint16_t)(plane << REG_HIGH_SHIFT); + + switch (writeForm(mask, data)) { + case WRITE_STORE: + return emitValueToPlane(out, cursor, cap, (uint16_t)(OP_MOVE_B_DN_A0 | planeHigh), 0u, (uint16_t)(OP_MOVE_B_IMM_A0 | planeHigh), data, off); + case WRITE_AND: + return emitValueToPlane(out, cursor, cap, (uint16_t)(OP_AND_B_DN_A0 | plane), REG_HIGH_SHIFT, (uint16_t)(OP_ANDI_B_A0 | plane), notMask, off); + case WRITE_OR: + return emitValueToPlane(out, cursor, cap, (uint16_t)(OP_OR_B_DN_A0 | plane), REG_HIGH_SHIFT, (uint16_t)(OP_ORI_B_A0 | plane), mask, off); + case WRITE_MERGE: + break; + } + // move.b off(a_plane),d0; and.b ~mask,d0; or.b data,d0; move.b d0,off(a_plane) + if (cursor + 8u + valueOpBytes(notMask) + valueOpBytes(data) > cap) { + return SPRITE_EMIT_OVERFLOW; + } + cursor = putWord(out, cursor, (uint16_t)(OP_MOVE_B_A0_D0 | plane)); + cursor = putWord(out, cursor, off); + cursor = putValueOp(out, cursor, OP_AND_B_DN_D0, 0u, OP_ANDI_B_D0, notMask); + cursor = putValueOp(out, cursor, OP_OR_B_DN_D0, 0u, OP_ORI_B_D0, data); + cursor = putWord(out, cursor, (uint16_t)(OP_MOVE_B_DN_A0 | planeHigh)); + return putWord(out, cursor, off); +} + + +// Walks every plane byte the sprite writes, row by row. With out NULL it +// is the counting pass (countWrite); otherwise it emits each write after +// cursor and returns the new cursor or SPRITE_EMIT_OVERFLOW. +static uint32_t emitWrites(uint8_t *out, uint32_t cursor, uint32_t cap, const jlSpriteT *sp, uint8_t shift, uint16_t lastDest) { + uint16_t srcH = (uint16_t)(sp->heightTiles * JL_TILE_PIXELS_PER_SIDE); + uint16_t row; + uint16_t d; + uint8_t i; + + for (row = 0u; row < srcH; row++) { + accumulateRow(sp, row, shift, lastDest); + for (d = 0u; d <= lastDest; d++) { + uint8_t mask = gDestMask[d]; + uint16_t off = (uint16_t)(row * AMIGA_PLANE_STRIDE + d); + + if (mask == 0u) { + continue; + } + for (i = 0u; i < AMIGA_PLANES; i++) { + if (out == NULL) { + countWrite(mask, gDestData[d][i]); + } else { + cursor = emitWrite(out, cursor, cap, i, mask, gDestData[d][i], off); + if (cursor == SPRITE_EMIT_OVERFLOW) { + return SPRITE_EMIT_OVERFLOW; + } + } + } + } + } + return cursor; +} + + +uint32_t jlSpriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { + uint32_t cursor; + uint16_t lastDest; + uint16_t saveMask; + uint16_t restMask; + uint8_t nPool; + uint8_t nSaved; + uint8_t reg; + uint8_t i; + + if (shift >= 8u) { + return 0u; // Amiga shift space is x & 7; defensive. + } + if (shift != 0u && sp->widthPx >= JL_SURFACE_WIDTH) { + // A full-width sprite can never draw unaligned on-surface (the + // dispatcher gate requires x + w <= 320 with x > 0), so a + // shifted variant would be dead bytes in the arena. + return 0u; + } + // Shift 0 writes widthTiles dest bytes per row; shifted rows spill one. + lastDest = (shift == 0u) ? (uint16_t)(sp->widthTiles - 1u) : sp->widthTiles; + + nPool = poolBuild(sp, shift, lastDest); + nSaved = (nPool >= POOL_FIRST_SAVED) ? (uint8_t)(nPool - (POOL_FIRST_SAVED - 1u)) : 0u; + saveMask = OP_MOVEM_SAVE_M; + restMask = OP_MOVEM_REST_M; + for (reg = POOL_FIRST_SAVED; reg <= nPool; reg++) { + saveMask = (uint16_t)(saveMask | (OP_MOVEM_SAVE_D0 >> reg)); + restMask = (uint16_t)(restMask | (OP_MOVEM_REST_D0 << reg)); + } + + // Prologue: save a2/a3 and the pool's d2-d7, load p0..p3 into a0..a3, + // load the pool. + cursor = 0u; + if (cursor + PROLOGUE_BYTES + 2u * nPool > cap) { + return SPRITE_EMIT_OVERFLOW; + } + cursor = putWord(out, cursor, OP_MOVEM_SAVE); + cursor = putWord(out, cursor, saveMask); + for (i = 0u; i < AMIGA_PLANES; i++) { + cursor = putWord(out, cursor, (uint16_t)(OP_MOVEA_SP_A0 | (uint16_t)(i << REG_HIGH_SHIFT))); + cursor = putWord(out, cursor, ARG_DISP(i, nSaved)); + } + for (reg = 1u; reg <= nPool; reg++) { + cursor = putWord(out, cursor, (uint16_t)(OP_MOVEQ_D0 | (uint16_t)(reg << REG_HIGH_SHIFT) | gPoolValue[reg])); + } + + cursor = emitWrites(out, cursor, cap, sp, shift, lastDest); + if (cursor == SPRITE_EMIT_OVERFLOW) { + return SPRITE_EMIT_OVERFLOW; + } + + // Epilogue: restore, return. + if (cursor + EPILOGUE_BYTES > cap) { + return SPRITE_EMIT_OVERFLOW; + } + cursor = putWord(out, cursor, OP_MOVEM_REST); + cursor = putWord(out, cursor, restMask); + cursor = putWord(out, cursor, OP_RTS); + return cursor; +} + + +uint32_t jlSpriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { + return emitPlaneRunCopy(out, cap, sp, shift, true); +} + + +uint32_t jlSpriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { return emitPlaneRunCopy(out, cap, sp, shift, false); } -uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { - return emitPlaneRunCopy(out, cap, sp, shift, true); +// The counting pass, then the pool: the most used values go to d1, d2, +// ... while each still pays for its register (POOL_MIN_USES_*). Returns +// how many registers it took, always d1 up to d<returned>. +static uint8_t poolBuild(const jlSpriteT *sp, uint8_t shift, uint16_t lastDest) { + uint16_t i; + uint8_t best; + uint8_t reg; + + memset(gValueUses, 0, sizeof(gValueUses)); + memset(gValueReg, 0, sizeof(gValueReg)); + gSeenCount = 0u; + (void)emitWrites(NULL, 0u, 0u, sp, shift, lastDest); + for (reg = 1u; reg <= POOL_LAST; reg++) { + best = gSeen[0]; + for (i = 1u; i < gSeenCount; i++) { + if (gValueUses[gSeen[i]] > gValueUses[best]) { + best = gSeen[i]; + } + } + if (gSeenCount == 0u || gValueUses[best] < ((reg < POOL_FIRST_SAVED) ? POOL_MIN_USES_SCRATCH : POOL_MIN_USES_SAVED)) { + return (uint8_t)(reg - 1u); + } + gValueReg[best] = reg; + gPoolValue[reg] = best; + gValueUses[best] = 0u; + } + return POOL_LAST; +} + + +// Puts an operation's opcode word and byte operand: regOp with the pool +// register in the field at regShift when the value is pooled, else immOp +// and the immediate word. +static uint32_t putValueOp(uint8_t *out, uint32_t cursor, uint16_t regOp, uint8_t regShift, uint16_t immOp, uint8_t value) { + uint8_t reg = gValueReg[value]; + + if (reg != 0u) { + return putWord(out, cursor, (uint16_t)(regOp | (uint16_t)(reg << regShift))); + } + cursor = putWord(out, cursor, immOp); + return putWord(out, cursor, value); +} + + +// The bytes putValueOp puts for value. +static uint32_t valueOpBytes(uint8_t value) { + return (gValueReg[value] != 0u) ? 2u : 4u; +} + + +static WriteFormE writeForm(uint8_t mask, uint8_t data) { + if (mask == MASK_ALL) { + return WRITE_STORE; + } + if (data == 0u) { + return WRITE_AND; + } + if (data == mask) { + return WRITE_OR; + } + return WRITE_MERGE; } diff --git a/src/m68k/stageBands.s b/src/m68k/stageBands.s index ef4f87e..7ff0385 100644 --- a/src/m68k/stageBands.s +++ b/src/m68k/stageBands.s @@ -4,8 +4,8 @@ | | Shared 68000 stage dirty bands: the multi-row mark and the whole-stage fill. | -| Used by all three 68000 ports (Atari ST, Amiga, X68000). gStageMinWord and -| gStageMaxWord hold each stage row's first and last dirty 16-bit word, 0xFF / +| Used by all three 68000 ports (Atari ST, Amiga, X68000). jlgStageMinWord and +| jlgStageMaxWord hold each stage row's first and last dirty 16-bit word, 0xFF / | 0x00 when the row is clean (src/core/surfaceInternal.h). Both are aligned(2) | byte arrays, so a row's two bytes sit at addresses of the same parity, and | four consecutive rows starting on an even row read and write as one long. @@ -17,7 +17,7 @@ | back into two memset calls, so both are written out here. | | Both routines take the arrays as arguments rather than naming them: the C -| symbol is `_gStageMinWord` to gcc-mint and gcc-amigaos and `gStageMinWord` to +| symbol is `_jlgStageMinWord` to gcc-mint and gcc-amigaos and `jlgStageMinWord` to | elf2x68k, and this file is plain GAS with no preprocessor to choose. | | cdecl: every argument in a 4-byte stack slot, a uint16_t in its low word and @@ -33,10 +33,14 @@ .text .even - .globl stageBandsFill68k - .globl _stageBandsFill68k - .globl stageBandsMark68k - .globl _stageBandsMark68k + .globl jlStageBandsFill68k + .globl _jlStageBandsFill68k + .globl jlStageBandsMark68k + .globl _jlStageBandsMark68k + .globl jlStageBandsMarkRegs + .globl _jlStageBandsMarkRegs + .globl jlStageBandsMarkTile68k + .globl _jlStageBandsMarkTile68k | Widen the row at (a0) / (a1) to cover [d2, d3], and step both to the next. | cmp.b (An)+,Dn computes Dn - byte: carry clear means minWord >= the row's @@ -52,7 +56,7 @@ .endm -| void stageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, +| void jlStageBandsFill68k(uint8_t *minBands, uint8_t *maxBands, | uint32_t minFill, uint32_t maxFill); | | Every row's min byte from minFill and max byte from maxFill, each a byte @@ -63,8 +67,8 @@ .equ FILL_MAX_BANDS, 8 .equ FILL_MIN_FILL, 12 .equ FILL_MAX_FILL, 16 -stageBandsFill68k: -_stageBandsFill68k: +jlStageBandsFill68k: +_jlStageBandsFill68k: movea.l FILL_MIN_BANDS(%sp),%a0 movea.l FILL_MAX_BANDS(%sp),%a1 move.l FILL_MIN_FILL(%sp),%d0 @@ -76,7 +80,7 @@ _stageBandsFill68k: rts -| void stageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, +| void jlStageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, | uint8_t minWord, uint8_t maxWord); | | Widen `rows` rows (at least 2; a one-row mark is inline in C) from minRow / @@ -103,14 +107,25 @@ _stageBandsFill68k: .equ MARK_MIN_WORD, 16 + 3 .equ MARK_MAX_WORD, 20 + 3 .equ MARK_SAVED, 2 * 4 | d2-d3, pushed first -stageBandsMark68k: -_stageBandsMark68k: +| The same from registers, for asm callers (the ST's jlFillRect): a0 / a1 +| the rows, d0.w the row count, d1.w minWord in its low byte and maxWord in +| its high byte. Like the cdecl entry it keeps d2-d7/a2-a6. +jlStageBandsMarkRegs: +_jlStageBandsMarkRegs: + movem.l %d2-%d3,-(%sp) + move.b %d1,%d2 + lsr.w #8,%d1 + move.b %d1,%d3 + bra.s markBody +jlStageBandsMark68k: +_jlStageBandsMark68k: movem.l %d2-%d3,-(%sp) movea.l MARK_SAVED + MARK_MIN_ROW(%sp),%a0 movea.l MARK_SAVED + MARK_MAX_ROW(%sp),%a1 move.w MARK_SAVED + MARK_ROWS(%sp),%d0 move.b MARK_SAVED + MARK_MIN_WORD(%sp),%d2 move.b MARK_SAVED + MARK_MAX_WORD(%sp),%d3 +markBody: cmpi.w #GROUP_MIN_ROWS,%d0 bcs.w markRows movem.l %d4-%d7,-(%sp) @@ -154,3 +169,62 @@ markRow: markDone: movem.l (%sp)+,%d2-%d3 rts + + +| A tile's mark (src/core/tile.c tileMarkDirty on the 68000 ports): the 8 +| rows from a0 / a1 (an even row, so two band groups each) widened to +| [d1.b, d1.b + 1], the tile's two band words. Register arguments from an +| inline asm call; clobbers d0-d3, a0-a1. +| +| When all 8 rows hold one band -- clean after a present, whole after a full +| redraw, or what earlier tiles in these rows left -- each side that does not +| cover the tile goes back as two long stores (text along a row widens only +| its max). A +| group's four bytes are the same exactly when it equals itself rotated a +| byte. Otherwise the rows widen one by one. +jlStageBandsMarkTile68k: +_jlStageBandsMarkTile68k: + move.b %d1,%d2 | minWord + move.b %d1,%d3 + addq.b #1,%d3 | maxWord + move.l (%a0),%d0 | min bands, rows 0-3 + cmp.l 4(%a0),%d0 | rows 4-7 the same? + bne.s .LtileRows + move.l %d0,%d1 + rol.l #8,%d1 + cmp.l %d0,%d1 | the 4 bytes the same? + bne.s .LtileRows + move.l (%a1),%d1 | max bands + cmp.l 4(%a1),%d1 + bne.s .LtileRows + rol.l #8,%d1 + cmp.l (%a1),%d1 + bne.s .LtileRows + cmp.b %d2,%d0 | one band [d0.b, d1.b]: + bls.s 1f | its min covers the tile + move.b %d2,%d0 | else minWord, in all four + lsl.w #8,%d0 | bytes of both groups + move.b %d2,%d0 + swap %d0 + move.b %d2,%d0 + lsl.w #8,%d0 + move.b %d2,%d0 + move.l %d0,(%a0) + move.l %d0,4(%a0) +1: cmp.b %d3,%d1 + bcc.s 2f | its max covers the tile + move.b %d3,%d1 | else maxWord + lsl.w #8,%d1 + move.b %d3,%d1 + swap %d1 + move.b %d3,%d1 + lsl.w #8,%d1 + move.b %d3,%d1 + move.l %d1,(%a1) + move.l %d1,4(%a1) +2: rts +.LtileRows: + .rept 8 + WIDEN_ROW + .endr + rts diff --git a/src/x68000/audio.c b/src/x68000/audio.c index 62e85b5..5186b5c 100644 --- a/src/x68000/audio.c +++ b/src/x68000/audio.c @@ -30,7 +30,7 @@ void jlpAudioShutdownPcm(void); void jlpAudioPcmFrameTick(void); // Tick callbacks ride hal.c's Timer C tick (100 Hz), which owns the clock. -bool x68kAudioTickSet(jlAudioTickFnT fn, uint16_t hz); +bool jlX68kAudioTickSet(jlAudioTickFnT fn, uint16_t hz); // ----- YM2151 (OPM) ----- @@ -212,7 +212,7 @@ static void opmWrite(uint8_t reg, uint8_t value) { void jlpAudioCriticalEnter(void) { uint16_t saved; - saved = m68kInterruptsOff(); + saved = m68kInterruptsOff(); gCriticalSr = saved; } @@ -276,7 +276,7 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) { void jlpAudioShutdown(void) { uint8_t ch; - (void)x68kAudioTickSet(NULL, 0u); + (void)jlX68kAudioTickSet(NULL, 0u); if (!gAudioReady) { return; } @@ -293,7 +293,7 @@ void jlpAudioShutdown(void) { // From Timer C, 100 times a second, as the contract allows: in its // interrupt, keep it short (joey/audio.h). bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { - return x68kAudioTickSet(fn, hz); + return jlX68kAudioTickSet(fn, hz); } diff --git a/src/x68000/audioPcm.c b/src/x68000/audioPcm.c index ed0eb96..2da578c 100644 --- a/src/x68000/audioPcm.c +++ b/src/x68000/audioPcm.c @@ -80,7 +80,7 @@ #define PCM_SILENCE MOD_PLAYER_SILENCE_U8 // unsigned 8-bit // The 8-bit mix's unit: a module channel at full volume and a full-scale // sound effect each swing a quarter of a byte, so the module's four channels -// fit (modPlayerSetU8Shift, audioSfxSetShift); the encoder makes up the +// fit (jlModPlayerSetU8Shift, jlAudioSfxSetShift); the encoder makes up the // level (adpcmLevel). #define MIX_UNIT_SHIFT 2u // How far ahead of the chip a new sound starts (chainRemix): 20 ms. The @@ -136,7 +136,7 @@ static uint8_t gLastPlaying = 0; static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS]; // Each block's sound before encoding: the module renders unsigned 8-bit mono here, -// audioSfxOverlayMix adds the SFX slots in place, then adpcmEncodeBlock +// jlAudioSfxOverlayMix adds the SFX slots in place, then adpcmEncodeBlock // converts it. Kept, with the encoder's state at every ENC_CHECK_SAMPLES of // each block, so a new sound can be mixed into the blocks ahead of the chip // and they re-encoded from just before it. @@ -156,10 +156,10 @@ static uint8_t gIdleBlocks = 0; // Diagnostics: how many times each stage actually ran. Exposed (non-static) so // a probe can read them without a debugger -- silent no-ops in the refill path // are exactly the failure this port hit. -uint32_t gX68kPcmRefills = 0; -uint32_t gX68kPcmChainStarts = 0; -uint8_t gX68kPcmReady = 0; -uint8_t gX68kPcmPlaying = 0; +uint32_t jlgX68kPcmRefills = 0; +uint32_t jlgX68kPcmChainStarts = 0; +uint8_t jlgX68kPcmReady = 0; +uint8_t jlgX68kPcmPlaying = 0; // MSM6258 / OKI ADPCM encoder state. The encoder runs the exact decoder on // every nibble it emits so its predictor stays bit-identical to the chip's -- @@ -176,6 +176,17 @@ static const int16_t kStepTable[49] = { static const int8_t kIndexAdjust[8] = { -1, -1, -1, -1, 2, 4, 6, 8 }; +// Silence settles the encoder, within 26 sample pairs from any state, at +// step index 0 with a predictor of -1 to 2: the four states a silent pair +// leaves as it found them, emitting 0x80 from -1 and 0 and 0x08 from 1 and +// 2. adpcmEncodeBlock writes those bytes without encoding, every silent +// pair from then on: the chain plays eight blocks of silence after each +// sound before it stops (pcmIdle). +#define ADPCM_SETTLED_MIN (-1) +#define ADPCM_SETTLED_SPAN 3u +#define ADPCM_SETTLED_LOW 0x80u +#define ADPCM_SETTLED_HIGH 0x08u + // ----- Internal helpers (alphabetical) ----- @@ -199,6 +210,10 @@ static void adpcmEncodeBlock(uint8_t b, uint16_t from) { if (i % ENC_CHECK_SAMPLES == 0u) { gEncCheck[b][i / ENC_CHECK_SAMPLES] = state; } + if (state.stepIndex == 0 && (uint16_t)(state.predictor - ADPCM_SETTLED_MIN) <= ADPCM_SETTLED_SPAN && pcm[i] == PCM_SILENCE && pcm[i + 1u] == PCM_SILENCE) { + out[i / 2] = state.predictor <= 0 ? ADPCM_SETTLED_LOW : ADPCM_SETTLED_HIGH; + continue; + } lo = adpcmEncodeSample(adpcmLevel(pcm[i]), &state); hi = adpcmEncodeSample(adpcmLevel(pcm[i + 1u]), &state); out[i / 2] = (uint8_t)(lo | (hi << 4)); @@ -299,9 +314,9 @@ static uint8_t adpcmPlayingBlock(void) { static void blockFill(uint8_t b) { bool wasPlaying; - wasPlaying = modPlayerActive(); + wasPlaying = jlModPlayerActive(); renderBlock(b); - if (wasPlaying && !modPlayerActive()) { + if (wasPlaying && !jlModPlayerActive()) { gModTailBlocks = (uint8_t)((b + BLOCK_COUNT - gLastPlaying) % BLOCK_COUNT + 1u); } adpcmEncodeBlock(b, 0u); @@ -312,7 +327,7 @@ static void blockFill(uint8_t b) { static bool pcmIdle(void) { uint8_t s; - if (modPlayerActive()) { + if (jlModPlayerActive()) { return false; } for (s = 0u; s < JL_AUDIO_SFX_SLOTS; s++) { @@ -327,13 +342,13 @@ static bool pcmIdle(void) { // Render block b: the module (if playing, kept in gModPcm) then the SFX // overlay on top. static void renderBlock(uint8_t b) { - if (modPlayerActive()) { - (void)modPlayerRenderU8(gModPcm[b], PCM_BLOCK, 1u); + if (jlModPlayerActive()) { + (void)jlModPlayerRenderU8(gModPcm[b], PCM_BLOCK, 1u); } else { memset(gModPcm[b], PCM_SILENCE, PCM_BLOCK); } memcpy(gPcm[b], gModPcm[b], PCM_BLOCK); - audioSfxOverlayMix(gPcm[b], PCM_BLOCK, gSfxSlots, JL_AUDIO_SFX_SLOTS); + jlAudioSfxOverlayMix(gPcm[b], PCM_BLOCK, gSfxSlots, JL_AUDIO_SFX_SLOTS); } @@ -352,10 +367,10 @@ static void modMixBlock(uint8_t b, uint16_t from, bool add) { mod = gModPcm[b]; if (add) { // A song that ended in an earlier block adds nothing more. - if (!modPlayerActive()) { + if (!jlModPlayerActive()) { return; } - if (!modPlayerRenderU8(mod + from, (uint16_t)(PCM_BLOCK - from), 1u)) { + if (!jlModPlayerRenderU8(mod + from, (uint16_t)(PCM_BLOCK - from), 1u)) { gModTailBlocks = (uint8_t)((b + BLOCK_COUNT - gLastPlaying) % BLOCK_COUNT + 1u); } } @@ -408,7 +423,7 @@ static void chainRemix(RemixE what, uint8_t slot) { for (; ahead < filled; ahead++) { b = (uint8_t)((playing + ahead) % BLOCK_COUNT); if (what == REMIX_SFX) { - audioSfxOverlayMix(gPcm[b] + start, (uint16_t)(PCM_BLOCK - start), &gSfxSlots[slot], 1u); + jlAudioSfxOverlayMix(gPcm[b] + start, (uint16_t)(PCM_BLOCK - start), &gSfxSlots[slot], 1u); } else { modMixBlock(b, start, what == REMIX_MOD_ADD); } @@ -442,26 +457,26 @@ static void startChain(void) { for (b = 1u; b < BLOCK_COUNT; b++) { blockFill(b); } - gNextFill = 0u; // every block is filled; the refills start behind the chip - gPlaying = true; - gX68kPcmPlaying = 1u; - gX68kPcmChainStarts++; + gNextFill = 0u; // every block is filled; the refills start behind the chip + gPlaying = true; + jlgX68kPcmPlaying = 1u; + jlgX68kPcmChainStarts++; } // ----- HAL entry points (alphabetical) ----- bool jlpAudioInitPcm(void) { memset(gSfxSlots, 0, sizeof(gSfxSlots)); - modPlayerSetU8Shift(MIX_UNIT_SHIFT); - audioSfxSetShift(MIX_UNIT_SHIFT); - gPcmReady = true; - gX68kPcmReady = 1u; + jlModPlayerSetU8Shift(MIX_UNIT_SHIFT); + jlAudioSfxSetShift(MIX_UNIT_SHIFT); + gPcmReady = true; + jlgX68kPcmReady = 1u; return true; } bool jlpAudioIsPlayingMod(void) { - return modPlayerActive() || gModTailBlocks != 0u; + return jlModPlayerActive() || gModTailBlocks != 0u; } @@ -495,7 +510,7 @@ void jlpAudioPcmFrameTick(void) { idle = pcmIdle(); blockFill(gNextFill); gNextFill = (uint8_t)((gNextFill + 1u) % BLOCK_COUNT); - gX68kPcmRefills++; + jlgX68kPcmRefills++; if (!idle) { gIdleBlocks = 0u; } else if (gIdleBlocks < BLOCK_COUNT) { @@ -510,10 +525,10 @@ void jlpAudioPcmFrameTick(void) { // next sound restarts the chain and is heard immediately. if (gIdleBlocks >= BLOCK_COUNT) { _iocs_adpcmmod(0); - gPlaying = false; - gIdleBlocks = 0u; - gModTailBlocks = 0u; - gX68kPcmPlaying = 0u; + gPlaying = false; + gIdleBlocks = 0u; + gModTailBlocks = 0u; + jlgX68kPcmPlaying = 0u; } } @@ -523,7 +538,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { return; } jlpAudioStopMod(); - if (!modPlayerStart(data, length, MSM6258_RATE, loop)) { + if (!jlModPlayerStart(data, length, MSM6258_RATE, loop)) { return; } if (!gPlaying) { @@ -538,7 +553,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1 if (!gPcmReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MSM6258_RATE); + jlAudioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MSM6258_RATE); if (!gPlaying) { startChain(); } else { @@ -551,7 +566,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin if (!gPcmReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } - audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MSM6258_RATE); + jlAudioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MSM6258_RATE); if (!gPlaying) { startChain(); } else { @@ -579,8 +594,8 @@ void jlpAudioStopMod(void) { if (!gPcmReady) { return; } - wasPlaying = modPlayerActive() || gModTailBlocks != 0u; - modPlayerStop(); + wasPlaying = jlModPlayerActive() || gModTailBlocks != 0u; + jlModPlayerStop(); gModTailBlocks = 0u; if (gPlaying && wasPlaying) { chainRemix(REMIX_MOD_REMOVE, 0u); diff --git a/src/x68000/hal.c b/src/x68000/hal.c index 237bbcf..ce77731 100644 --- a/src/x68000/hal.c +++ b/src/x68000/hal.c @@ -6,9 +6,9 @@ // // The stage is native planar (src/x68000/halPlanar.c) and jlpPresent copies // its dirty rows into the four 1bpp TEXT planes at $E00000, which are planar -// too (x68kPresentText, src/x68000/x68kPresent.s). The text plane has one +// too (jlX68kPresentText, src/x68000/x68kPresent.s). The text plane has one // 16-colour palette, so the SCB's per-line palettes are reloaded as the beam -// scans by a CRTC raster interrupt (x68kRasterIsr.s). The graphics-plane +// scans by a CRTC raster interrupt (jlX68kRasterIsr.s). The graphics-plane // present (X68K_PRESENT_TEXT 0) is the alternative that needs no interrupt. // // VERIFIED UNDER EMULATION (MAME 0.264 + installer/patches/mame): UBER's @@ -170,12 +170,12 @@ typedef char x68kJoyPortsCoverSticks[(X68K_JOY_PORT_COUNT == JL_JOYSTICK_COUNT) #define X68K_RASTER_FIRST (X68K_CRTC_VBACK_END + 1u) // A band's reload is keyed to the raster of the line ABOVE it: the interrupt // waits out that line's display and stores in the blanking after it (see -// x68kRasterIsr.s), and arms the compare ARM_LEAD rasters before that to get +// jlX68kRasterIsr.s), and arms the compare ARM_LEAD rasters before that to get // there in time. A band starting within X68K_RASTER_STAY_MAX lines of the one // before is reached by staying in the interrupt instead: re-arming for it // would set the compare for a raster already under way. #define X68K_RASTER_ABOVE 1u -#define X68K_RASTER_ARM_LEAD 1u // == ARM_LEAD in x68kRasterIsr.s +#define X68K_RASTER_ARM_LEAD 1u // == ARM_LEAD in jlX68kRasterIsr.s #define X68K_RASTER_STAY_MAX 2u #define X68K_VBL_SPIN_LIMIT 2000000ul #define X68K_TEE_SPIN_LIMIT 200000ul @@ -233,7 +233,7 @@ typedef char x68kJoyPortsCoverSticks[(X68K_JOY_PORT_COUNT == JL_JOYSTICK_COUNT) // MILLISECONDS come from MFP Timer C, Human68k's system tick. The IPL ROM's MFP // table at $FF0EAA programs it with TCDCR $70 (prescale 200) and TCDR $C8 // (200), so on the MFP's 4 MHz clock it ticks at exactly 100 Hz, and between -// ticks its live count steps once every 50 us. x68kTickIsr.s counts the ticks +// ticks its live count steps once every 50 us. jlX68kTickIsr.s counts the ticks // and the count gives the position inside the current one, so the clock // resolves to the millisecond rather than to the 10 ms tick. // @@ -251,7 +251,7 @@ typedef char x68kJoyPortsCoverSticks[(X68K_JOY_PORT_COUNT == JL_JOYSTICK_COUNT) // // FRAMES come from the V-DISP line through IOCS _VDISPST: MFP Timer A, which // the ROM's table leaves counting V-DISP edges (TACR $08), interrupts on every -// one and x68kVdispIsr.s counts them. jlpWaitVBL waits for that count to +// one and jlX68kVdispIsr.s counts them. jlpWaitVBL waits for that count to // move, so it and jlpFrameCount agree on every edge, and no edge is lost however // long the program goes between calls. #define X68K_TICK_VEC_ADDR 0x00000114ul // MFP channel 5: vector $45 @@ -298,7 +298,7 @@ typedef char x68kTickIsWholeMs[((X68K_TICK_RELOAD % X68K_TICK_STEPS_PER_MS) == 0 #define X68K_FRAME_HZ_NOMINAL 55u // An edge seen fewer than this many steps (300 us) after a tick may have been // seen late: the tick's handler runs the IOCS timer dispatcher for up to -// ~200 us with the V-DISP interrupt masked (x68kTickIsr.s). Such an edge is +// ~200 us with the V-DISP interrupt masked (jlX68kTickIsr.s). Such an edge is // not timed; the next one is, and EDGE_TRIES bounds how many are passed over. #define X68K_EDGE_LATE_STEPS 6u #define X68K_EDGE_TRIES 8u @@ -317,12 +317,14 @@ static uint8_t gLastVdisp = 0; // stands in for still lands on the ROM code it was proven against. The key // groups invert kScanForKey: group g's keys are gKeyGroupKey[first[g] .. // first[g + 1]), each tested with its gKeyGroupMask bit. gKeyGroupBits is -// each group's byte as of the last poll, which gKeyState already reflects. +// each group's byte as of the last poll, which jlgKeyState already reflects. +// Two groups' bytes, for keyGroupsSame. +typedef uint16_t __attribute__((may_alias, aligned(2))) KeyGroupPairT; static bool gInputDirect = false; static uint8_t gKeyGroupFirst[X68K_KEYGROUP_COUNT + 1]; static uint8_t gKeyGroupKey [JL_KEY_COUNT + KEY_ALIAS_COUNT]; static uint8_t gKeyGroupMask [JL_KEY_COUNT + KEY_ALIAS_COUNT]; -static uint8_t gKeyGroupBits [X68K_KEYGROUP_COUNT]; +static uint8_t gKeyGroupBits [X68K_KEYGROUP_COUNT] __attribute__((aligned(2))); // Joysticks. gJoyDirect is set when _JOYGET is still the ROM's port read, so // the poll reads the PPI itself; a resident driver that hooked _JOYGET for // another kind of pad keeps the trap. gJoyPorts is both ports as of the last @@ -330,45 +332,44 @@ static uint8_t gKeyGroupBits [X68K_KEYGROUP_COUNT]; static bool gJoyDirect = false; static uint16_t gJoyPorts = X68K_JOY_BOTH_RELEASED; -// Clocks. gX68kTicks is bumped by x68kTickIsr.s on every Timer C interrupt, -// gX68kFrames by x68kVdispIsr.s on every V-DISP edge -- or by vdispPoll when +// Clocks. jlgX68kTicks is bumped by jlX68kTickIsr.s on every Timer C interrupt, +// jlgX68kFrames by jlX68kVdispIsr.s on every V-DISP edge -- or by vdispPoll when // _VDISPST would not take the handler. Written from interrupt context, so // volatile; each is read with a single load, which is atomic against its // handler's single add on a 68000. -volatile uint32_t gX68kTicks = 0ul; -volatile uint16_t gX68kFrames = 0u; -// jlAudioTickRegister rides the same tick (x68kAudioTickSet): x68kTickIsr.s -// calls x68kTickHook, the pacer, while a callback is set. -void (*volatile x68kTickHook)(void) = NULL; +volatile uint32_t jlgX68kTicks = 0ul; +volatile uint16_t jlgX68kFrames = 0u; +// jlAudioTickRegister rides the same tick (jlX68kAudioTickSet): jlX68kTickIsr.s +// calls jlX68kTickHook, the pacer, while a callback is set. +void (*volatile jlX68kTickHook)(void) = NULL; static volatile jlAudioTickFnT gAudioTickFn = NULL; static volatile uint32_t gAudioTickHz = 0u; static volatile uint32_t gAudioTickCarry = 0u; -extern void x68kTickIsr(void); -extern void x68kVdispIsr(void); -extern uint32_t x68kTickChainAddr; // the JMP operand inside the ISR +extern void jlX68kTickIsr(void); +extern void jlX68kVdispIsr(void); +extern uint32_t jlX68kTickChainAddr; // the JMP operand inside the ISR static bool gTickHooked = false; // Timer C is Human68k's 100 Hz and chained -static bool gVdispHooked = false; // _VDISPST took x68kVdispIsr +static bool gVdispHooked = false; // _VDISPST took jlX68kVdispIsr #if X68K_PRESENT_TEXT -// The page flip (x68kVdispIsr.s, or vdispPoll without it): a present hands +// The page flip (jlX68kVdispIsr.s, or vdispPoll without it): a present hands // over the text Y scroll of the page it drew, and the palette a one-band // screen's colours went into (NULL: none), and the next display start shows -// both together. gX68kFlipPending is set last and cleared there. -volatile uint8_t gX68kFlipPending = 0u; -volatile uint16_t gX68kFlipScroll = 0u; -const uint16_t *volatile gX68kFlipPalette = NULL; +// both together. jlgX68kFlipPending is set last and cleared there. +volatile uint8_t jlgX68kFlipPending = 0u; +volatile uint16_t jlgX68kFlipScroll = 0u; +const uint16_t *volatile jlgX68kFlipPalette = NULL; static uint8_t gFrontPage = 0u; // A one-band screen's colours, for the next flip; gPaletteQueued until then. static uint16_t gFlipPaletteBuf[JL_SURFACE_COLORS_PER_PALETTE]; static bool gPaletteQueued = false; // The page drawn into holds the frame before last, so a present copies what -// changed in either frame: the previous frame's dirty bands, and the union -// of both, which the copy walks (aligned for x68kPresentText's long reads). -// Seeded all dirty, so the first two presents fill both pages. +// changed in either frame: the previous frame's dirty bands, merged into the +// stage's for the copy (aligned for jlX68kDirtyUnion's long reads). Seeded all +// dirty, so the first two presents fill both pages. +#define X68K_BAND_GROUPS ((uint8_t)(JL_SURFACE_HEIGHT >> STAGE_DIRTY_GROUP4_SHIFT)) // four rows a long static uint8_t gPrevMinWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); static uint8_t gPrevMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); -static uint8_t gUnionMinWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); -static uint8_t gUnionMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2))); static bool gPrevAnyDirty = true; #endif static uint32_t gMillisBase = 0ul; // tickMillisNow() as jlpInit finished @@ -393,7 +394,7 @@ static uint16_t gColorLutBlue[X68K_LUT_BLUE_ENTRIES]; // // The text plane has ONE 16-entry palette, so honouring the SCB means reloading // it as the beam scans: one reload per TRANSITION between lines with different -// SCB values, worst case one per line. src/x68000/x68kRasterIsr.s does the +// SCB values, worst case one per line. src/x68000/jlX68kRasterIsr.s does the // reloads from a table built here -- see "SCB per-band palette" below. #define X68K_MAX_BANDS JL_SURFACE_HEIGHT #define X68K_NO_PALETTE 0xFFu // no line's SCB value; starts the first band @@ -401,7 +402,7 @@ static uint16_t gColorLutBlue[X68K_LUT_BLUE_ENTRIES]; #define X68K_ALL_PALETTES 0xFFFFu // a bit per palette typedef char x68kAllPalettesFit[(JL_SURFACE_PALETTE_COUNT == 16u) ? 1 : -1]; -// One band as the raster interrupt reads it. x68kRasterIsr.s addresses the +// One band as the raster interrupt reads it. jlX68kRasterIsr.s addresses the // fields by offset; the size checks below hold both sides to one layout. typedef struct { const uint16_t *palette; // its 16 colours, already in hardware format @@ -437,11 +438,12 @@ typedef char x68kBandSizeIs8[(sizeof(X68kBandT) == 8u) ? 1 : -1]; typedef char x68kTableEndAt0[(offsetof(X68kBandTableT, end) == 0u) ? 1 : -1]; typedef char x68kTableBandsAt4[(offsetof(X68kBandTableT, bands) == 4u) ? 1 : -1]; -extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s +extern void jlX68kRasterIsr(void); // src/x68000/jlX68kRasterIsr.s // The present's copy, src/x68000/x68kPresent.s. It carries the geometry as // constants of its own; these hold each one to the C definition it mirrors. -extern void x68kPresentText(uint8_t *const *planes, const uint8_t *minWord, const uint8_t *maxWord, volatile uint8_t *text); +extern bool jlX68kDirtyUnion(uint8_t *minWord, uint8_t *maxWord, uint8_t *prevMin, uint8_t *prevMax); +extern void jlX68kPresentText(uint8_t *const *planes, uint8_t *minWord, uint8_t *maxWord, volatile uint8_t *text); typedef char x68kPresentRows[(JL_SURFACE_HEIGHT == 200u) ? 1 : -1]; typedef char x68kPresentCleanMin[(STAGE_DIRTY_CLEAN_MIN == 0xFFu) ? 1 : -1]; typedef char x68kPresentFullMax[(STAGE_DIRTY_FULL_MAX == 79u) ? 1 : -1]; @@ -454,11 +456,11 @@ typedef char x68kPresentPlaneStep[(X68K_TVRAM_PLANE_STEP == 0x20000ul) ? 1 : -1] // is this far into its row. typedef char x68kPresentOriginEven[((X68K_ORIGIN_BYTE % 2u) == 0u) ? 1 : -1]; -// Shared with x68kRasterIsr.s, hence not static. Written by the main thread +// Shared with jlX68kRasterIsr.s, hence not static. Written by the main thread // only with every interrupt masked; the interrupt runs to completion. -X68kBandTableT *volatile gX68kRasterTable = NULL; // the table being walked -X68kBandTableT *volatile gX68kRasterPending = NULL; // newer, taken at the wrap -const X68kBandT *volatile gX68kRasterCursor = NULL; // the next band to reload +X68kBandTableT *volatile jlgX68kRasterTable = NULL; // the table being walked +X68kBandTableT *volatile jlgX68kRasterPending = NULL; // newer, taken at the wrap +const X68kBandT *volatile jlgX68kRasterCursor = NULL; // the next band to reload static X68kBandTableT gBandTables[X68K_BAND_TABLES]; static bool gRasterArmed = false; static uint32_t gRasterOldVec = 0ul; // $138 before we took it @@ -499,6 +501,7 @@ static uint32_t iocsHandler(uint8_t call); static uint16_t joyPortsRead(void); static void joyUpdate(jlJoystickE stick, uint8_t bits); static void keyGroupsBuild(void); +static bool keyGroupsSame(const volatile uint8_t *matrix); static void keyGroupsUpdate(const volatile uint8_t *matrix); static bool keyScanDown(uint8_t scan); static void keyTapped(uint8_t scan); @@ -568,10 +571,10 @@ bool jlpInit(const jlConfigT *config) { _iocs_g_clr_on(); clearTextPlane(); // Page 0 on screen, and both pages filled by the first two presents. - *X68K_CRTC_TEXT_Y = 0u; - gFrontPage = 0u; - gX68kFlipPending = 0u; - gPaletteQueued = false; + *X68K_CRTC_TEXT_Y = 0u; + gFrontPage = 0u; + jlgX68kFlipPending = 0u; + gPaletteQueued = false; memset(gPrevMinWord, 0, sizeof(gPrevMinWord)); memset(gPrevMaxWord, STAGE_DIRTY_FULL_MAX, sizeof(gPrevMaxWord)); gPrevAnyDirty = true; @@ -611,7 +614,7 @@ void jlpShutdown(void) { if (!gModeSet) { return; } - // Restore Human68k's Timer C vector BEFORE dropping supervisor mode: the chained x68kTickIsr + // Restore Human68k's Timer C vector BEFORE dropping supervisor mode: the chained jlX68kTickIsr // lives in this program's RAM, and leaving $114 pointing at it after exit sends the OS clock's // next tick into freed memory (the dangling-ISR trap this port already fixed for the SCC RX // vectors in jlpSerialClose). @@ -621,7 +624,7 @@ void jlpShutdown(void) { uint16_t sr; sr = m68kInterruptsOff(); - *vec = x68kTickChainAddr; + *vec = jlX68kTickChainAddr; m68kInterruptsRestore(sr); gTickHooked = false; } @@ -671,14 +674,14 @@ static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps) { uint32_t ticks; for (tries = 0u; tries < X68K_EDGE_TRIES; tries++) { - before = gX68kFrames; + before = jlgX68kFrames; jlpWaitVBL(); ticks = tickRead(&into); - if (gX68kFrames == before) { + if (jlgX68kFrames == before) { return false; } if (into >= X68K_EDGE_LATE_STEPS) { - *frame = gX68kFrames; + *frame = jlgX68kFrames; *steps = ticks * X68K_TICK_RELOAD + into; return true; } @@ -691,7 +694,7 @@ static bool frameEdgeSteps(uint16_t *frame, uint32_t *steps) { // 100 Hz tick the clock is built on: prescaler code 7 in TCDCR and a reload of // 200 caught as it happens. The previous handler is patched into the ISR's // call operand, so it still runs -- the OS clock keeps working -- with only -// the SCB raster interrupt able to cut in (see x68kTickIsr.s for why). +// the SCB raster interrupt able to cut in (see jlX68kTickIsr.s for why). // Supervisor already (jlpInit's _dos_super), so the vector write is legal. // Refuses if the vector looks unset, because chaining to nothing would fault // on the first tick. @@ -712,22 +715,22 @@ static void installTickIsr(void) { if (tickReloadCapture() != X68K_TICK_RELOAD) { return; } - x68kTickChainAddr = *vec; - *vec = (uint32_t)x68kTickIsr; - gTickHooked = true; + jlX68kTickChainAddr = *vec; + *vec = (uint32_t)jlX68kTickIsr; + gTickHooked = true; } -// Count frames in x68kVdispIsr.s. _VDISPST writes the handler straight into +// Count frames in jlX68kVdispIsr.s. _VDISPST writes the handler straight into // vector $134 (iplrom.dat $FF9DC8) with no IOCS wrapper -- which is why the C // function (RTS, free to clobber d0/d1/a0/a1) this port first handed it hung // the machine -- loads Timer A with the edges per interrupt, picks the edge in // the MFP's AER and enables the channel. It refuses, returning the vector, // when another program already holds $134; vdispPoll counts instead. static void installVdispIsr(void) { - gX68kFrames = 0u; - gLastVdisp = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); - gVdispHooked = (_iocs_vdispst(x68kVdispIsr, X68K_VDISP_DISPLAY_EDGE, X68K_VDISP_EVERY_EDGE) == 0); + jlgX68kFrames = 0u; + gLastVdisp = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); + gVdispHooked = (_iocs_vdispst(jlX68kVdispIsr, X68K_VDISP_DISPLAY_EDGE, X68K_VDISP_EVERY_EDGE) == 0); } @@ -767,7 +770,7 @@ static uint32_t tickMillisNow(void) { } -// x68kTickHook while a jlAudioTickRegister callback is set: the callback as +// jlX68kTickHook while a jlAudioTickRegister callback is set: the callback as // many times as its rate has come due since the last tick, so the average is // exact at any rate; up to the tick's 100 a second they come one to a tick. static void audioTickPace(void) { @@ -781,19 +784,19 @@ static void audioTickPace(void) { // jlAudioTickRegister's way in (src/x68000/audio.c): fn at hz, or none. // False when there is no tick to ride (Timer C was not chained). -bool x68kAudioTickSet(jlAudioTickFnT fn, uint16_t hz) { +bool jlX68kAudioTickSet(jlAudioTickFnT fn, uint16_t hz) { uint16_t sr; if (fn != NULL && !gTickHooked) { return false; } sr = m68kInterruptsOff(); - x68kTickHook = NULL; + jlX68kTickHook = NULL; gAudioTickFn = fn; gAudioTickHz = hz; gAudioTickCarry = 0u; if (fn != NULL) { - x68kTickHook = audioTickPace; + jlX68kTickHook = audioTickPace; } m68kInterruptsRestore(sr); return true; @@ -813,10 +816,10 @@ static uint32_t tickRead(uint16_t *steps) { bool pending; do { - ticks = gX68kTicks; + ticks = jlgX68kTicks; count = *X68K_MFP_TCDR; pending = (*X68K_MFP_IPRB & X68K_MFP_TIMER_C) != 0u; - } while (ticks != gX68kTicks); + } while (ticks != jlgX68kTicks); if (pending && count > X68K_TICK_RELOAD / 2u) { ticks++; } @@ -879,9 +882,9 @@ static void vdispPoll(void) { if (now != gLastVdisp) { gLastVdisp = now; if (now != 0u) { - gX68kFrames++; + jlgX68kFrames++; #if X68K_PRESENT_TEXT - if (gX68kFlipPending != 0u) { + if (jlgX68kFlipPending != 0u) { flipApply(); } #endif @@ -902,7 +905,7 @@ static void vdispPoll(void) { // between. The cost is the number of TRANSITIONS, worst case one per line. // // Timing (scripts/verify-x68000-scb.sh measures it on every transition of a -// screen with 147 of them): x68kRasterIsr.s waits out the display of the line +// screen with 147 of them): jlX68kRasterIsr.s waits out the display of the line // ABOVE a band and stores the band's sixteen colours with one movem in the // horizontal blanking after it, between the last stage pixel of one line and // the first of the next. A band that starts within X68K_RASTER_STAY_MAX lines @@ -1014,9 +1017,9 @@ static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChan } // Withdraw any queued table first, so the interrupt cannot take up the one // about to be rewritten; after that the table it walks cannot change. - sr = m68kInterruptsOff(); - gX68kRasterPending = NULL; - table = (gX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0]; + sr = m68kInterruptsOff(); + jlgX68kRasterPending = NULL; + table = (jlgX68kRasterTable == &gBandTables[0]) ? &gBandTables[1] : &gBandTables[0]; m68kInterruptsRestore(sr); buildScbPalettes(table, src); if (!table->bandsCurrent) { @@ -1033,12 +1036,12 @@ static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChan } sr = m68kInterruptsOff(); if (gRasterArmed) { - gX68kRasterPending = table; + jlgX68kRasterPending = table; m68kInterruptsRestore(sr); return; } - gX68kRasterTable = table; - gX68kRasterCursor = table->bands; + jlgX68kRasterTable = table; + jlgX68kRasterCursor = table->bands; m68kInterruptsRestore(sr); // _CRTCRAS takes the handler and the first band's raster, and owns // enabling GPIP6 in the MFP. The handler must be the raw shim, never a C @@ -1047,7 +1050,7 @@ static void installScb(const jlSurfaceT *src, uint16_t paletteMask, bool scbChan static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR; gRasterOldVec = *(volatile uint32_t *)addr; } - (void)_iocs_crtcras(x68kRasterIsr, (int)(table->bands[0].raster - X68K_RASTER_ARM_LEAD)); + (void)_iocs_crtcras(jlX68kRasterIsr, (int)(table->bands[0].raster - X68K_RASTER_ARM_LEAD)); gRasterArmed = true; } #endif // X68K_PRESENT_TEXT @@ -1184,54 +1187,46 @@ static void clearTextPlane(void) { } -// The union of this frame's dirty bands and the previous frame's, into -// gUnionMinWord / gUnionMaxWord (a clean row's 0xFF / 0x00 lose to any band), -// with this frame's kept for the next. False when both are clean: the page -// not shown already matches the stage, and there is nothing to copy. +// The union of this frame's dirty bands and the previous frame's, merged into +// the stage's bands for jlX68kPresentText (which leaves each group it copies +// clean: JL_PRESENT_CLEARS_BANDS), with this frame's kept for the next: +// jlX68kDirtyUnion (x68kPresent.s), four rows at a time. False when both are +// clean: the page not shown already matches the stage, and there is nothing +// to copy. static bool dirtyUnion(void) { - uint16_t y; - uint8_t g; - uint8_t curMin; - uint8_t curMax; - bool anyDirty; + const StageBandGroupT *curMin; + uint8_t g; - anyDirty = false; - for (g = 0u; g < (uint8_t)(JL_SURFACE_HEIGHT >> STAGE_DIRTY_GROUP4_SHIFT); g++) { - if (!STAGE_DIRTY_GROUP4_CLEAN(g)) { - anyDirty = true; - break; + if (!gPrevAnyDirty) { + curMin = (const StageBandGroupT *)(const void *)jlgStageMinWord; + for (g = 0u; g < X68K_BAND_GROUPS; g++) { + if (curMin[g] != STAGE_DIRTY_GROUP4_ALL_CLEAN) { + break; + } + } + if (g == X68K_BAND_GROUPS) { + return false; } } - if (!anyDirty && !gPrevAnyDirty) { - return false; - } - for (y = 0u; y < JL_SURFACE_HEIGHT; y++) { - curMin = gStageMinWord[y]; - curMax = gStageMaxWord[y]; - gUnionMinWord[y] = curMin < gPrevMinWord[y] ? curMin : gPrevMinWord[y]; - gUnionMaxWord[y] = curMax > gPrevMaxWord[y] ? curMax : gPrevMaxWord[y]; - gPrevMinWord[y] = curMin; - gPrevMaxWord[y] = curMax; - } - gPrevAnyDirty = anyDirty; + gPrevAnyDirty = jlX68kDirtyUnion(jlgStageMinWord, jlgStageMaxWord, gPrevMinWord, gPrevMaxWord); return true; } // Show the page handed over, and the palette with it: vdispPoll's half of -// what x68kVdispIsr.s does at each display start. +// what jlX68kVdispIsr.s does at each display start. static void flipApply(void) { const uint16_t *palette; uint16_t col; - *X68K_CRTC_TEXT_Y = gX68kFlipScroll; - palette = gX68kFlipPalette; + *X68K_CRTC_TEXT_Y = jlgX68kFlipScroll; + palette = jlgX68kFlipPalette; if (palette != NULL) { for (col = 0u; col < JL_SURFACE_COLORS_PER_PALETTE; col++) { X68K_TEXT_PALETTE[col] = palette[col]; } } - gX68kFlipPending = 0u; + jlgX68kFlipPending = 0u; } @@ -1239,10 +1234,10 @@ static void flipApply(void) { // queued colours. The pending flag goes last: the interrupt reads the rest // once it sees it. static void flipRequest(uint8_t page) { - gX68kFlipScroll = (uint16_t)(page * X68K_TEXT_PAGE_ROWS); - gX68kFlipPalette = gPaletteQueued ? gFlipPaletteBuf : NULL; - gPaletteQueued = false; - gX68kFlipPending = 1u; + jlgX68kFlipScroll = (uint16_t)(page * X68K_TEXT_PAGE_ROWS); + jlgX68kFlipPalette = gPaletteQueued ? gFlipPaletteBuf : NULL; + gPaletteQueued = false; + jlgX68kFlipPending = 1u; } @@ -1252,17 +1247,56 @@ static void flipRequest(uint8_t page) { static void flipWait(void) { uint32_t guard; - for (guard = 0ul; gX68kFlipPending != 0u && guard < X68K_VBL_SPIN_LIMIT; guard++) { + for (guard = 0ul; jlgX68kFlipPending != 0u && guard < X68K_VBL_SPIN_LIMIT; guard++) { vdispPoll(); } } +// UBER_PROBE's display row (op_probeDisplayCheck in examples/uber/uber.c): +// how many bytes of the page the last present drew -- the one the next +// display start shows -- differ from the stage's planes. The present's +// contract is that this page holds the whole current image, so zero +// mismatch is no stale row reaching the screen, which the stage hashes +// cannot see. Built with the text present it checks (the default), and +// not only into the probe build, which links the stock library (the ST's +// jlpProbeDisplayMismatch is the precedent). +uint32_t jlX68kProbeDisplayMismatch(const jlSurfaceT *src) { + const X68kPlanarT *pd; + const volatile uint8_t *page; + const uint8_t *stage; + uint32_t mismatched; + uint16_t y; + uint16_t b; + uint8_t plane; + + pd = x68kSurfacePlanar(src); + if (pd == NULL) { + return 0xFFFFFFFFul; + } + mismatched = 0ul; + for (plane = 0u; plane < X68K_BITPLANES; plane++) { + page = (const volatile uint8_t *)(X68K_TVRAM_PLANE0 + (uint32_t)plane * X68K_TVRAM_PLANE_STEP + x68kTvramOffset(X68K_ORIGIN_X, (uint16_t)(X68K_ORIGIN_Y + gFrontPage * X68K_TEXT_PAGE_ROWS))); + stage = pd->planes[plane]; + for (y = 0u; y < JL_SURFACE_HEIGHT; y++) { + for (b = 0u; b < X68K_BYTES_PER_ROW; b++) { + if (page[b] != stage[b]) { + mismatched++; + } + } + page += X68K_TVRAM_STRIDE; + stage += X68K_BYTES_PER_ROW; + } + } + return mismatched; +} + + // Present to the TEXT plane. The stage and the text plane are both four 1bpp // planes, so this is a copy -- no pixel conversion, no table, 0.5 bytes of bus // traffic per pixel. The SCB is honoured by the raster interrupt above rather // than by a per-pixel palette index. The copy, dirty-band walk included, is -// x68kPresentText (src/x68000/x68kPresent.s), whose header has the numbers. +// jlX68kPresentText (src/x68000/x68kPresent.s), whose header has the numbers. // // PAGE FLIP: the copy goes into the page not shown, which holds the frame // before last, so it takes the union of this frame's dirty rows and the last @@ -1272,29 +1306,33 @@ void jlpPresent(const jlSurfaceT *src) { X68kPlanarT *pd; uint8_t back; + // Every way out leaves the stage's bands clean (JL_PRESENT_CLEARS_BANDS): + // dirtyUnion's walk clears them, and the returns before it do here. if (src == NULL) { + jlStageDirtyClearAll(); return; } vdispPoll(); // keep the frame counter honest: see the note on vdispPoll pd = x68kSurfacePlanar(src); if (pd == NULL) { + jlStageDirtyClearAll(); return; } // The page about to be drawn may still be on screen behind a flip not yet // shown, and a queued palette with it. flipWait(); - // gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume + // jlgStagePaletteDirty and jlgStageScbDirty live in core/surface.c; consume // both here the way src/iigs/hal.c does. Either one updates the band // tables, which carry their own copies of the palettes. - if (gStagePaletteDirty || gStageScbDirty) { - installScb(src, gStagePaletteDirtyMask, gStageScbDirty); - gStagePaletteDirty = false; - gStagePaletteDirtyMask = 0u; - gStageScbDirty = false; + if (jlgStagePaletteDirty || jlgStageScbDirty) { + installScb(src, jlgStagePaletteDirtyMask, jlgStageScbDirty); + jlgStagePaletteDirty = false; + jlgStagePaletteDirtyMask = 0u; + jlgStageScbDirty = false; } if (dirtyUnion()) { back = (uint8_t)(gFrontPage ^ 1u); - x68kPresentText(pd->planes, gUnionMinWord, gUnionMaxWord, (volatile uint8_t *)(X68K_TVRAM_PLANE0 + x68kTvramOffset(X68K_ORIGIN_X, (uint16_t)(X68K_ORIGIN_Y + back * X68K_TEXT_PAGE_ROWS)))); + jlX68kPresentText(pd->planes, jlgStageMinWord, jlgStageMaxWord, (volatile uint8_t *)(X68K_TVRAM_PLANE0 + x68kTvramOffset(X68K_ORIGIN_X, (uint16_t)(X68K_ORIGIN_Y + back * X68K_TEXT_PAGE_ROWS)))); gFrontPage = back; flipRequest(back); } else if (gPaletteQueued) { @@ -1318,6 +1356,7 @@ void jlpPresent(const jlSurfaceT *src) { uint8_t palBase; if (src == NULL) { + jlStageDirtyClearAll(); return; } vdispPoll(); // keep the frame counter honest: see the note on vdispPoll @@ -1327,12 +1366,13 @@ void jlpPresent(const jlSurfaceT *src) { if (!gSpreadBuilt) { buildSpreadTables(); } - if (gStagePaletteDirty) { + if (jlgStagePaletteDirty) { uploadGfxPalette(src); - gStagePaletteDirty = false; + jlgStagePaletteDirty = false; } pd = x68kSurfacePlanar(src); if (pd == NULL) { + jlStageDirtyClearAll(); return; } sp = (const uint8_t *)&gSpreadPair[0][0][0]; @@ -1341,8 +1381,8 @@ void jlpPresent(const jlSurfaceT *src) { continue; } // Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane. - firstByte = (uint16_t)(gStageMinWord[y] >> 1); - rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); + firstByte = (uint16_t)(jlgStageMinWord[y] >> 1); + rowBytes = (uint16_t)((jlgStageMaxWord[y] >> 1) - firstByte + 1u); if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) { rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte); } @@ -1390,6 +1430,7 @@ void jlpPresent(const jlSurfaceT *src) { *wordDst++ = SPREAD_AT(sp, o0 + 4) | SPREAD_AT(sp, (o1 | 128) + 4) | SPREAD_AT(sp, (o2 | 256) + 4) | SPREAD_AT(sp, (o3 | 384) + 4); } } + jlStageDirtyClearAll(); // JL_PRESENT_CLEARS_BANDS, as the text present does } #endif // X68K_PRESENT_TEXT @@ -1590,11 +1631,11 @@ static void joyUpdate(jlJoystickE stick, uint8_t bits) { static const int8_t kAxisForPair[X68K_JOY_PAIR_MASK + 1u] = { 0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX }; uint8_t held; - held = (uint8_t)~bits; - gJoyAxisX[stick] = kAxisForPair[(held >> X68K_JOY_X_SHIFT) & X68K_JOY_PAIR_MASK]; - gJoyAxisY[stick] = kAxisForPair[(held >> X68K_JOY_Y_SHIFT) & X68K_JOY_PAIR_MASK]; - gJoyButtonState[stick][JL_JOY_BUTTON_0] = (uint8_t)((held & X68K_JOY_TRIGGER_A) != 0u); - gJoyButtonState[stick][JL_JOY_BUTTON_1] = (uint8_t)((held & X68K_JOY_TRIGGER_B) != 0u); + held = (uint8_t)~bits; + jlgJoyAxisX[stick] = kAxisForPair[(held >> X68K_JOY_X_SHIFT) & X68K_JOY_PAIR_MASK]; + jlgJoyAxisY[stick] = kAxisForPair[(held >> X68K_JOY_Y_SHIFT) & X68K_JOY_PAIR_MASK]; + jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = (uint8_t)((held & X68K_JOY_TRIGGER_A) != 0u); + jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = (uint8_t)((held & X68K_JOY_TRIGGER_B) != 0u); } @@ -1637,15 +1678,39 @@ static void keyGroupsBuild(void) { // Both states come from the bitmap, so a released key drops back to 0 (the // old set-only write latched keys down forever, hanging any second // jlWaitForAnyKey); a group whose byte did not change already matches -// gKeyState, which is what makes skipping it exact. The one other writer, +// jlgKeyState, which is what makes skipping it exact. The one other writer, // keyTapped, unsets its group's cached byte so the group is read afresh. With no key moving, a poll is fifteen byte compares instead of a // shift and mask for every mapped key. +// Whether every group's byte is the last poll's, compared a word (two +// groups) at a time: the key matrix and gKeyGroupBits are both aligned(2). +static bool keyGroupsSame(const volatile uint8_t *matrix) { + const volatile KeyGroupPairT *now; + const KeyGroupPairT *last; + uint16_t i; + + now = (const volatile KeyGroupPairT *)matrix; + last = (const KeyGroupPairT *)(const void *)gKeyGroupBits; + for (i = 0u; i < (uint16_t)(X68K_KEYGROUP_COUNT / 2u); i++) { + if (now[i] != last[i]) { + return false; + } + } + return matrix[X68K_KEYGROUP_COUNT - 1u] == gKeyGroupBits[X68K_KEYGROUP_COUNT - 1u]; +} + + +// Nothing to do while no group's byte has moved: jlgKeyState already holds +// every group, the second scancodes' keys included (they read only the group +// bytes), and an idle keyboard is the poll's commonest case. static void keyGroupsUpdate(const volatile uint8_t *matrix) { uint16_t group; uint16_t slot; uint16_t end; uint8_t bits; + if (keyGroupsSame(matrix)) { + return; + } for (group = 0u; group < X68K_KEYGROUP_COUNT; group++) { bits = matrix[group]; if (bits == gKeyGroupBits[group]) { @@ -1654,13 +1719,13 @@ static void keyGroupsUpdate(const volatile uint8_t *matrix) { gKeyGroupBits[group] = bits; end = gKeyGroupFirst[group + 1u]; for (slot = gKeyGroupFirst[group]; slot < end; slot++) { - gKeyState[gKeyGroupKey[slot]] = (uint8_t)((bits & gKeyGroupMask[slot]) != 0u); + jlgKeyState[gKeyGroupKey[slot]] = (uint8_t)((bits & gKeyGroupMask[slot]) != 0u); } } // A key with a second scancode is down while either is: the slots above // leave it to whichever of its two came last. for (slot = 0u; slot < KEY_ALIAS_COUNT; slot++) { - gKeyState[kKeyAlias[slot].key] = (uint8_t)(keyScanDown(kScanForKey[kKeyAlias[slot].key]) || keyScanDown(kKeyAlias[slot].scan)); + jlgKeyState[kKeyAlias[slot].key] = (uint8_t)(keyScanDown(kScanForKey[kKeyAlias[slot].key]) || keyScanDown(kKeyAlias[slot].scan)); } } @@ -1687,7 +1752,7 @@ static void keyTapped(uint8_t scan) { bit = (uint8_t)(1u << (scan & X68K_KEYGROUP_BIT_MASK)); for (slot = gKeyGroupFirst[group]; slot < gKeyGroupFirst[group + 1u]; slot++) { if (gKeyGroupMask[slot] == bit) { - gKeyState[gKeyGroupKey[slot]] = 1u; + jlgKeyState[gKeyGroupKey[slot]] = 1u; gKeyGroupBits[group] = (uint8_t)(gKeyGroupBits[group] ^ bit); } } @@ -1781,27 +1846,27 @@ void jlpInputInit(void) { _iocs_ms_init(); _iocs_ms_limit(0, 0, JL_SURFACE_WIDTH - 1, JL_SURFACE_HEIGHT - 1); _iocs_ms_curst(JL_SURFACE_WIDTH / 2, JL_SURFACE_HEIGHT / 2); - gMouseAttached = true; - gMouseX = JL_SURFACE_WIDTH / 2; - gMouseY = JL_SURFACE_HEIGHT / 2; + jlgMouseAttached = true; + jlgMouseX = JL_SURFACE_WIDTH / 2; + jlgMouseY = JL_SURFACE_HEIGHT / 2; // Key state: the group lists, then every key up and every group byte 0, // which agree, so the first poll rewrites exactly the groups that have a - // key held. Core never clears gKeyState, and a shutdown/re-init cycle + // key held. Core never clears jlgKeyState, and a shutdown/re-init cycle // must not leave a key reading down that the delta poll would not revisit. keyGroupsBuild(); memset(gKeyGroupBits, 0, sizeof(gKeyGroupBits)); - memset(gKeyState, 0, sizeof(gKeyState)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); gInputDirect = inputDirectUsable(); // Joysticks, on the same terms: every stick released and the cached ports // the released ones, which agree. Both ports are always wired -- an empty // port reads released -- so both report connected, as on the ST. gJoyPorts = X68K_JOY_BOTH_RELEASED; - memset(gJoyAxisX, 0, sizeof(gJoyAxisX)); - memset(gJoyAxisY, 0, sizeof(gJoyAxisY)); - memset(gJoyButtonState, 0, sizeof(gJoyButtonState)); - memset(gJoyConnected, true, sizeof(gJoyConnected)); + memset(jlgJoyAxisX, 0, sizeof(jlgJoyAxisX)); + memset(jlgJoyAxisY, 0, sizeof(jlgJoyAxisY)); + memset(jlgJoyButtonState, 0, sizeof(jlgJoyButtonState)); + memset(jlgJoyConnected, true, sizeof(jlgJoyConnected)); gJoyDirect = romMatches(iocsHandler(X68K_IOCS_JOYGET), kRomJoyget, sizeof(kRomJoyget)); } @@ -1810,7 +1875,7 @@ void jlpInputInit(void) { // be re-established AFTER the CRT mode is restored, and core's jlShutdown // calls this first. void jlpInputShutdown(void) { - gMouseAttached = false; + jlgMouseAttached = false; } @@ -1821,7 +1886,7 @@ void jlpInputShutdown(void) { // button bytes, from the mouse block or through _MS_CURGT / _MS_GETDT. // Joysticks: each port's byte, from the PPI or through _JOYGET. void jlpInputPoll(void) { - uint8_t groups[X68K_KEYGROUP_COUNT]; + uint8_t groups[X68K_KEYGROUP_COUNT] __attribute__((aligned(2))); const volatile uint8_t *matrix; const volatile uint8_t *mouse; uint16_t group; @@ -1857,7 +1922,7 @@ void jlpInputPoll(void) { } key = (uint32_t)_iocs_b_keyinp(); keyTapped((uint8_t)((key >> X68K_KEYINP_SCAN_SHIFT) & X68K_KEYINP_SCAN_MASK)); - inputCharTyped((uint8_t)(key & X68K_KEYINP_CHAR_MASK), gKeyState[JL_KEY_LCTRL]); + jlInputCharTyped((uint8_t)(key & X68K_KEYINP_CHAR_MASK), jlgKeyState[JL_KEY_LCTRL]); } } @@ -1882,12 +1947,12 @@ void jlpInputPoll(void) { rightDown = ((mouseButtons >> 8) & 0xFFu) != 0u; leftDown = (mouseButtons & 0xFFu) != 0u; } - gMouseX = (int16_t)((mousePos >> 16) & 0xFFFFu); - gMouseY = (int16_t)(mousePos & 0xFFFFu); + jlgMouseX = (int16_t)((mousePos >> 16) & 0xFFFFu); + jlgMouseY = (int16_t)(mousePos & 0xFFFFu); - gMouseButtonState[JL_MOUSE_BUTTON_LEFT] = leftDown; - gMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = rightDown; - gMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false; + jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = leftDown; + jlgMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = rightDown; + jlgMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false; // The ports are live levels: nothing latches, so a released line reads // released on the very next poll. @@ -1911,7 +1976,7 @@ void jlpJoystickReset(jlJoystickE js) { // Both clocks are described under "Clocks" near the top of this file. -// Frames since init: V-DISP edges, counted by x68kVdispIsr.s, or by +// Frames since init: V-DISP edges, counted by jlX68kVdispIsr.s, or by // vdispPoll when _VDISPST refused the handler. // // CRITICAL CONTRACT: jlpFrameCount must be monotonic ON ITS OWN, without @@ -1954,7 +2019,7 @@ void jlpCodeWritten(void *addr, uint32_t bytes) { uint16_t jlpFrameCount(void) { vdispPoll(); - return gX68kFrames; + return jlgX68kFrames; } @@ -2007,8 +2072,8 @@ void jlpWaitVBL(void) { uint16_t start; uint32_t guard; - start = gX68kFrames; - for (guard = 0ul; gX68kFrames == start && guard < X68K_VBL_SPIN_LIMIT; guard++) { + start = jlgX68kFrames; + for (guard = 0ul; jlgX68kFrames == start && guard < X68K_VBL_SPIN_LIMIT; guard++) { vdispPoll(); } } diff --git a/src/x68000/halPlanar.c b/src/x68000/halPlanar.c index c3962c6..528ac7f 100644 --- a/src/x68000/halPlanar.c +++ b/src/x68000/halPlanar.c @@ -38,8 +38,7 @@ // elf2x68k does not.) The circle OUTLINE is still per-port asm, one file // each. The pixel walkers are shared too, in src/m68k/planarPlot.s. -extern void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); -extern void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color); +extern void jlSurface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); // circle.s @@ -48,8 +47,8 @@ extern void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, u // the set-or-clear choice already made. Both are bit-identical to draw.c's own // walkers. Straight lines never reach jlpDrawLine -- draw.c diverts those to // jlFillRect -- so the line body walks the general diagonal case only. -extern void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); -extern void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color); +extern void jlSurface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color); +extern void jlSurface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color); // ----- CPU stand-ins for the Amiga blitter helpers ----- @@ -86,325 +85,7 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) { } // A whole surface is the rect fill's whole-row case: each plane is one // contiguous run, stored a row per movem.l. - surface68kFillRect(((X68kPlanarT *)s->portData)->planes, 0u, 0u, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, (uint16_t)(doubled & 0x0Fu)); -} - - -// Planar jlFillRect into the four off-screen shadow plane buffers, invisible -// until jlStagePresent. The caller (cross-platform jlFillRect) has already -// clipped the rect to the surface. src/m68k/planarFill.s does all of it: a -// rect of whole rows as one movem.l run per plane, anything else as masked -// edge words and long stores between them, plane by plane -- one-pixel-wide -// rects included, which is what draw.c turns a vertical line into. -void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { - X68kPlanarT *pd; - - // Callers guarantee s != NULL and positive w/h: the jlFillRect wrapper - // validates and clips before fillRectOnSurface. - pd = (X68kPlanarT *)s->portData; - if (pd == NULL) { - jlpGenericFillRect(s, x, y, w, h, colorIndex); - return; - } - surface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex); -} - - -// Phase 9 sprite save/restore plane-backup hooks. -// Sprite save at (x, y, w, h) records the destination region into the -// caller's backup buffer; restore writes it back. x and w are 2-pixel -// aligned by cross-platform code. -// -// Backup layout matches the CHUNKY-packed format the cross-platform -// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see -// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte, -// high nibble = even (left) pixel, low nibble = odd (right) pixel, -// (w/2) bytes per row, (w/2)*h bytes total. This is the same format -// the Atari ST port uses and never overruns the contract buffer -- -// unlike a plane-major layout, which would round x down to an 8-pixel -// boundary and width up and then write (4*roundedW/8)*h = up to -// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each -// pixel's 4bpp color is assembled from / written to the 4 plane bits -// at byte x>>3, bit 0x80 >> (x & 7). - -// Chunky pixel PAIR -> one 2-bit slice of each plane, packed so four table -// reads build all four plane bytes for eight pixels at once. -// -// Byte lane p of the entry holds the two bits plane p takes from the pair: -// the first pixel in bit 1, the second in bit 0. Accumulating with -// `acc = (acc << 2) | table[b]` four times fills every lane with its eight -// bits, and the lanes never bleed into each other because each starts empty -// and only ever takes two bits at the bottom. -// -// This replaces eight read-modify-writes PER PIXEL PAIR in the unaligned -// sprite walker with four table reads and two masked writes per plane per -// EIGHT pixels, which is what the walker cost: Space Taxi's sprites are -// almost never byte-aligned, so the walker is the path the game actually -// takes, and it measured 13% of the whole frame in restore alone. -// One plane's eight pixels starting at a sub-byte offset, spread to nibbles. -// Reading two bytes and shifting inside a 16-bit window is what makes the -// unaligned case cost the same as the aligned one. -#define X68K_PLANE_SLICE(_p, _off, _shift) \ - (gPlaneToChunky[(uint8_t)(((((uint16_t)(_p)[(_off)] << 8) \ - | (uint16_t)(_p)[(_off) + 1u]) >> (8u - (_shift))) & 0xFFu)]) - -// Merge one plane's eight pixels into the two destination bytes they can -// straddle. `bits` is the plane byte with the first pixel in bit 7; shifting -// it into a 16-bit window and masking keeps neighbouring pixels intact, which -// whole-byte stores would not. -#define X68K_MERGE_PLANE(_p, _off, _bits, _shift, _mHi, _mLo) \ - do { \ - uint16_t w16_ = (uint16_t)(((uint16_t)(_bits) << 8) >> (_shift)); \ - (_p)[(_off)] = (uint8_t)(((_p)[(_off)] & (uint8_t)~(_mHi)) \ - | (uint8_t)((w16_ >> 8) & (_mHi))); \ - if ((_mLo) != 0u) { \ - (_p)[(_off) + 1u] = (uint8_t)(((_p)[(_off) + 1u] & (uint8_t)~(_mLo)) \ - | (uint8_t)(w16_ & (_mLo))); \ - } \ - } while (0) - - -static uint32_t gPairToPlanes[256]; -// The same two pixels' transparency: bit 1 the first pixel, bit 0 the second, -// set when the pixel is not colour 0. Accumulated alongside gPairToPlanes so -// the sprite draw gets its write mask from the same four table reads. -static uint8_t gPairToMask[256]; -// The other direction, for the sprite SAVE: one plane byte (eight pixels) -// spread into eight nibbles' bit 0, packed as four bytes. Plane p's -// contribution is the same table shifted left by p, because each nibble -// carries a single bit and cannot carry into its neighbour -- so four table -// reads and three shifts rebuild eight chunky pixels from four planes. -static uint32_t gPlaneToChunky[256]; -static bool gPairTableBuilt = false; - - -static void buildPairTable(void) { - uint16_t b; - - if (gPairTableBuilt) { - return; - } - for (b = 0u; b < 256u; b++) { - uint8_t first = (uint8_t)(b >> 4); - uint8_t second = (uint8_t)(b & 0x0Fu); - uint32_t entry = 0ul; - uint16_t plane; - - for (plane = 0u; plane < X68K_BITPLANES; plane++) { - uint32_t bits = (uint32_t)((((first >> plane) & 1u) << 1) | ((second >> plane) & 1u)); - - entry |= bits << (plane * 8u); - } - gPairToPlanes[b] = entry; - gPairToMask[b] = (uint8_t)(((first != 0u) ? 2u : 0u) | ((second != 0u) ? 1u : 0u)); - } - for (b = 0u; b < 256u; b++) { - uint32_t spread = 0ul; - uint16_t px; - - // Pixel 0 is bit 7 and lands in the top nibble of the first byte. - for (px = 0u; px < 8u; px++) { - if ((b & (0x80u >> px)) != 0u) { - spread |= 1ul << ((3u - (px >> 1)) * 8u + ((px & 1u) ? 0u : 4u)); - } - } - gPlaneToChunky[b] = spread; - } - gPairTableBuilt = true; -} - - -void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { - const X68kPlanarT *pd; - uint16_t pairs; - uint16_t row; - uint16_t pair; - uint8_t *dst; - - pd = (const X68kPlanarT *)s->portData; - if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) { - return; - } - - /* Byte-aligned fast path: raw plane-byte copy, no per-pixel c2p. - * When x and w are both 8-px aligned and the rect is fully on - * surface, each sprite tile column maps to whole plane bytes, so we - * save the raw bytes -- ~8x cheaper than the chunky walker below and - * an exact round-trip on restore (the sprite fills its bytes, so - * restoring whole bytes touches no neighbor pixels). The backup row - * holds plane0..plane3, each `wb` bytes, contiguous. The chunky - * walker below stays as the arbitrary-x fallback. */ - if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - uint16_t wb; - uint16_t wb2; - uint16_t wb3; - uint16_t startByte; - uint16_t rowOff; - uint8_t *bdst; - uint16_t col; - - wb = (uint16_t)(w >> 3); - wb2 = (uint16_t)(wb << 1); - wb3 = (uint16_t)(wb2 + wb); - startByte = (uint16_t)((uint16_t)x >> 3); - rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + startByte); - bdst = dstPlaneBytes; - for (row = 0; row < (uint16_t)h; row++) { - const uint8_t *s0 = pd->planes[0] + rowOff; - const uint8_t *s1 = pd->planes[1] + rowOff; - const uint8_t *s2 = pd->planes[2] + rowOff; - const uint8_t *s3 = pd->planes[3] + rowOff; - for (col = 0; col < wb; col++) { - bdst[col] = s0[col]; - bdst[wb + col] = s1[col]; - bdst[wb2 + col] = s2[col]; - bdst[wb3 + col] = s3[col]; - } - bdst += (uint16_t)(wb << 2); - rowOff += X68K_BYTES_PER_ROW; - } - return; - } - - // Unaligned span, eight pixels at a time: take each plane's eight-bit - // slice with one 16-bit shift, then rebuild the chunky pairs from four - // table reads (see gPlaneToChunky). The old loop tested four masked - // plane bytes per PIXEL and recomputed the mask and offset each time. - buildPairTable(); - pairs = (uint16_t)(w >> 1); - { - uint16_t shift = (uint16_t)((uint16_t)x & 7u); - uint16_t firstOff = (uint16_t)((uint16_t)x >> 3); - uint16_t rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + firstOff); - - for (row = 0; row < (uint16_t)h; row++) { - const uint8_t *p0 = pd->planes[0] + rowOff; - const uint8_t *p1 = pd->planes[1] + rowOff; - const uint8_t *p2 = pd->planes[2] + rowOff; - const uint8_t *p3 = pd->planes[3] + rowOff; - uint16_t srcByte = 0u; - - dst = dstPlaneBytes + (uint16_t)(row * pairs); - for (pair = 0u; pair < pairs; pair += 4u) { - uint16_t have = (uint16_t)(pairs - pair); - uint32_t chunky; - uint16_t k; - - if (have > 4u) { - have = 4u; - } - chunky = X68K_PLANE_SLICE(p0, srcByte, shift) | (X68K_PLANE_SLICE(p1, srcByte, shift) << 1) | (X68K_PLANE_SLICE(p2, srcByte, shift) << 2) | (X68K_PLANE_SLICE(p3, srcByte, shift) << 3); - for (k = 0u; k < have; k++) { - dst[pair + k] = (uint8_t)((chunky >> ((3u - k) * 8u)) & 0xFFu); - } - srcByte++; - } - rowOff = (uint16_t)(rowOff + X68K_BYTES_PER_ROW); - } - } -} - - -void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { - X68kPlanarT *pd; - uint16_t pairs; - uint16_t row; - uint16_t pair; - const uint8_t *src; - - pd = (X68kPlanarT *)s->portData; - if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) { - return; - } - - /* Byte-aligned fast path mirroring jlpSpriteSavePlanes: writes the - * raw plane bytes straight back (whole-byte round-trip), skipping - * the per-pixel chunky->planar masking of the walker below. */ - if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) { - uint16_t wb; - uint16_t wb2; - uint16_t wb3; - uint16_t startByte; - uint16_t rowOff; - const uint8_t *bsrc; - uint16_t col; - - wb = (uint16_t)(w >> 3); - wb2 = (uint16_t)(wb << 1); - wb3 = (uint16_t)(wb2 + wb); - startByte = (uint16_t)((uint16_t)x >> 3); - rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + startByte); - bsrc = srcPlaneBytes; - for (row = 0; row < (uint16_t)h; row++) { - uint8_t *d0 = pd->planes[0] + rowOff; - uint8_t *d1 = pd->planes[1] + rowOff; - uint8_t *d2 = pd->planes[2] + rowOff; - uint8_t *d3 = pd->planes[3] + rowOff; - for (col = 0; col < wb; col++) { - d0[col] = bsrc[col]; - d1[col] = bsrc[wb + col]; - d2[col] = bsrc[wb2 + col]; - d3[col] = bsrc[wb3 + col]; - } - bsrc += (uint16_t)(wb << 2); - rowOff += X68K_BYTES_PER_ROW; - } - return; - } - - // Unaligned span. Eight pixels at a time, not one: four table reads build - // all four plane bytes at once (see gPairToPlanes), then each plane is - // shifted into place and merged with two masked writes. The old walker - // did eight read-modify-writes per PIXEL PAIR, thirty-two per eight - // pixels, and recomputed a mask and a byte offset for every one of them. - buildPairTable(); - pairs = (uint16_t)(w >> 1); - { - uint16_t shift = (uint16_t)((uint16_t)x & 7u); - uint16_t firstOff = (uint16_t)((uint16_t)x >> 3); - uint16_t rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + firstOff); - - for (row = 0; row < (uint16_t)h; row++) { - uint8_t *p0 = pd->planes[0] + rowOff; - uint8_t *p1 = pd->planes[1] + rowOff; - uint8_t *p2 = pd->planes[2] + rowOff; - uint8_t *p3 = pd->planes[3] + rowOff; - uint16_t dstByte = 0u; - - src = srcPlaneBytes + (uint16_t)(row * pairs); - for (pair = 0u; pair < pairs; pair += 4u) { - uint16_t have = (uint16_t)(pairs - pair); - uint32_t acc = 0ul; - uint16_t k; - uint16_t valid; - uint16_t maskHi; - uint16_t maskLo; - - if (have > 4u) { - have = 4u; - } - for (k = 0u; k < have; k++) { - acc = (acc << 2) | gPairToPlanes[src[pair + k]]; - } - // A short last group leaves its pixels at the bottom of each - // lane; push them up so the first pixel is always bit 7. - if (have < 4u) { - acc <<= (2u * (4u - have)); - } - valid = (uint16_t)(0xFFu << (8u - (have * 2u))); - valid &= 0xFFu; - maskHi = (uint16_t)((valid << 8) >> shift) >> 8; - maskLo = (uint16_t)((valid << 8) >> shift) & 0xFFu; - - X68K_MERGE_PLANE(p0, dstByte, (uint8_t)(acc & 0xFFu), shift, maskHi, maskLo); - X68K_MERGE_PLANE(p1, dstByte, (uint8_t)((acc >> 8) & 0xFFu), shift, maskHi, maskLo); - X68K_MERGE_PLANE(p2, dstByte, (uint8_t)((acc >> 16) & 0xFFu), shift, maskHi, maskLo); - X68K_MERGE_PLANE(p3, dstByte, (uint8_t)((acc >> 24) & 0xFFu), shift, maskHi, maskLo); - dstByte++; - } - rowOff = (uint16_t)(rowOff + X68K_BYTES_PER_ROW); - } - } + jlSurface68kFillRect(((X68kPlanarT *)s->portData)->planes, 0u, 0u, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, (uint16_t)(doubled & 0x0Fu)); } @@ -418,7 +99,7 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t if (pd == NULL) { return; } - planarSpriteDraw(pd->planes, sp, x, y); + jlPlanarSpriteDraw(pd->planes, sp, x, y); } @@ -497,76 +178,13 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint } -// Phase 4 planar dual-write helper: set one pixel's bit in each of -// the four shadow planes. Caller (the per-primitive walker below) has -// already validated (x, y) is on-surface. byteOff is computed once -// and reused across all four planes since stride is the same in each. - - -static void x68kPlanarSetPixel(X68kPlanarT *pd, int16_t x, int16_t y, uint8_t color) { - uint16_t byteOff; - uint8_t bitMask; - uint8_t notMask; - uint8_t *p0; - uint8_t *p1; - uint8_t *p2; - uint8_t *p3; - - byteOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + ((uint16_t)x >> 3)); - bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); - notMask = (uint8_t)~bitMask; - - /* Unroll the 4-plane loop. Loop counter + array indexing inside - * the hot per-pixel path is the gating cost on circle outlines: - * UBER jlDrawCircle r=80 calls this ~640 times per call. */ - p0 = &pd->planes[0][byteOff]; - p1 = &pd->planes[1][byteOff]; - p2 = &pd->planes[2][byteOff]; - p3 = &pd->planes[3][byteOff]; - - if (color & 0x01u) { - *p0 = (uint8_t)(*p0 | bitMask); - } else { - *p0 = (uint8_t)(*p0 & notMask); - } - if (color & 0x02u) { - *p1 = (uint8_t)(*p1 | bitMask); - } else { - *p1 = (uint8_t)(*p1 & notMask); - } - if (color & 0x04u) { - *p2 = (uint8_t)(*p2 | bitMask); - } else { - *p2 = (uint8_t)(*p2 & notMask); - } - if (color & 0x08u) { - *p3 = (uint8_t)(*p3 | bitMask); - } else { - *p3 = (uint8_t)(*p3 & notMask); - } -} - - -void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { - X68kPlanarT *pd; - uint8_t nibLo; - if (s->portData == NULL) { - jlpGenericDrawPixel(s, x, y, colorIndex); - return; - } - nibLo = (uint8_t)(colorIndex & 0x0Fu); - pd = (X68kPlanarT *)s->portData; - x68kPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo); -} - - void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { X68kPlanarT *pd; if (s->portData == NULL) { return; } pd = (X68kPlanarT *)s->portData; - surface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu)); + jlSurface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu)); } @@ -576,7 +194,7 @@ void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co return; } pd = (X68kPlanarT *)s->portData; - surface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu)); + jlSurface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu)); } @@ -602,7 +220,7 @@ bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co if (s->portData == NULL) { return false; } - surface68kFillCircle(((X68kPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex); + jlSurface68kFillCircle(((X68kPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex); return true; } @@ -758,132 +376,12 @@ bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i } -/* ===== Phase 9 reader hooks: pure-planar Amiga implementations ===== - * - * Cross-platform code that USED to read s->pixels (chunky shadow) now - * goes through these. On Amiga the chunky shadow doesn't exist; - * pixels are derived on demand by walking the plane bits. - * - * Per-pixel assembly: for pixel (x, y), read bit (7 - x%8) from plane - * byte at row*40 + x/8 in each of the 4 planes. Color index = sum - * of (bit_p << p). */ - -uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { - X68kPlanarT *pd; - uint16_t byteOff; - uint8_t bitMask; - uint8_t color; - uint16_t i; - - pd = (X68kPlanarT *)s->portData; - if (pd == NULL) { - return 0u; - } - byteOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + ((uint16_t)x >> 3)); - bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); - color = 0u; - for (i = 0; i < X68K_BITPLANES; i++) { - if (pd->planes[i][byteOff] & bitMask) { - color = (uint8_t)(color | (1u << i)); - } - } - return color; -} - - -/* Reverse-c2p: per row, derive 160 chunky bytes from 40 plane bytes - * (per plane, 4 planes). Used by jlpSurfaceHash to fold the planar - * surface into the same byte-stream the chunky ports hash, so cross- - * port hash comparisons stay valid. - * Walks 8 pixels per planar-byte column; per pixel assembles nibble - * from 4 plane bits. Output: 4 chunky bytes per planar-byte column - * (since 8 pixels = 4 chunky bytes at 2px/byte). */ -static void x68kPlanesToChunkyRow(const X68kPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) { - uint16_t col; - uint16_t byteOff; - uint8_t b0; - uint8_t b1; - uint8_t b2; - uint8_t b3; - uint8_t pix; - uint8_t bitMask; - uint16_t p; - - for (col = 0; col < X68K_BYTES_PER_ROW; col++) { - byteOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + col); - b0 = pd->planes[0][byteOff]; - b1 = pd->planes[1][byteOff]; - b2 = pd->planes[2][byteOff]; - b3 = pd->planes[3][byteOff]; - /* For each of 8 pixels in this planar byte (bit 7 = leftmost), - * assemble nibble from the 4 plane bits and pack into chunky - * bytes (high nibble = even pixel, low nibble = odd pixel). */ - for (p = 0; p < 8u; p++) { - bitMask = (uint8_t)(0x80u >> p); - pix = 0u; - if (b0 & bitMask) { - pix = (uint8_t)(pix | 1u); - } - if (b1 & bitMask) { - pix = (uint8_t)(pix | 2u); - } - if (b2 & bitMask) { - pix = (uint8_t)(pix | 4u); - } - if (b3 & bitMask) { - pix = (uint8_t)(pix | 8u); - } - if ((p & 1u) == 0u) { - dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(pix << 4); - } else { - dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(dstChunkyRow[col * 4u + (p >> 1)] | pix); - } - } - } -} - - +// The shared planar hash (src/m68k/planarHash.c), over separate planes. uint32_t jlpSurfaceHash(const jlSurfaceT *s) { - X68kPlanarT *pd; - uint16_t lo = 0xACE1u; - uint16_t hi = 0x1357u; - uint16_t n; - uint16_t v; - int16_t row; - uint8_t b; - uint8_t chunkyRow[JL_SURFACE_BYTES_PER_ROW]; - const uint16_t *w; + const X68kPlanarT *pd; - pd = (X68kPlanarT *)s->portData; - if (pd == NULL) { - return 0u; - } - /* Pixel hash: derive chunky one row at a time, fold byte-by-byte - * via the shared SURFACE_HASH_MIX_BYTE so cross-port hash - * matches. */ - for (row = 0; row < JL_SURFACE_HEIGHT; row++) { - x68kPlanesToChunkyRow(pd, row, chunkyRow); - for (n = 0; n < JL_SURFACE_BYTES_PER_ROW; n++) { - b = chunkyRow[n]; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - } - // SCB: byte sequence, endian-independent. - for (n = 0; n < (uint16_t)JL_SURFACE_HEIGHT; n++) { - b = s->scb[n]; - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - /* Palette: read uint16 values, fold high-then-low for endian- - * independence. */ - w = &s->palette[0][0]; - for (n = 0; n < (uint16_t)JL_SURFACE_PALETTE_ENTRIES; n++) { - v = *w++; - b = (uint8_t)((v >> 8) & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - b = (uint8_t)(v & 0xFFu); - SURFACE_HASH_MIX_BYTE(lo, hi, b); - } - return ((uint32_t)hi << 16) | (uint32_t)lo; + pd = (const X68kPlanarT *)s->portData; + return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, X68K_BYTES_PER_ROW, PLANAR_HASH_SEPARATE_PAIR_STEP); } @@ -927,7 +425,7 @@ uint8_t *jlpStageAllocPixels(void) { /* Phase 9: Amiga has no chunky shadow. The stage pixels pointer * stays NULL; cross-platform code reads pixels via jlpSamplePixel * (or the other chunky reader ops) which read from planes. NULL is - * a valid return -- cross-platform stageAlloc treats NULL as + * a valid return -- cross-platform jlStageAlloc treats NULL as * "port has no chunky storage" and skips the chunky memset. */ return NULL; } diff --git a/src/x68000/serial.c b/src/x68000/serial.c index 36250c0..a57ec22 100644 --- a/src/x68000/serial.c +++ b/src/x68000/serial.c @@ -95,7 +95,7 @@ static const uint16_t kX68kRates[X68K_RATE_COUNT] = { 75u, 150u, 300u, 600u, 120 #define X68K_WR5_BITS_SHIFT 5 #define X68K_SCC_WR5 5u -// A write's backstop on a busy transmitter, in IOCS polls (serialTxStalled): +// A write's backstop on a busy transmitter, in IOCS polls (jlSerialTxStalled): // the clock bounds it first, once it runs. #define X68K_TX_SPIN_LIMIT 200000ul @@ -114,26 +114,26 @@ static const uint16_t kX68kRates[X68K_RATE_COUNT] = { 75u, 150u, 300u, 600u, 120 // server's flowWait timeout degrade streams a whole screen without acks -- fits while the loop is // busy. It is plain RAM on a 4 MB machine; do not shrink it to save bytes. #define X68K_RX_RING_CAP 4096u -// Flow control holds the peer off at the high mark (x68kSccRxIsr.s) and lets it go at the low. +// Flow control holds the peer off at the high mark (jlX68kSccRxIsr.s) and lets it go at the low. #define X68K_RX_HIGH_WATER (X68K_RX_RING_CAP / 4u * 3u) #define X68K_RX_LOW_WATER (X68K_RX_RING_CAP / 4u) #define X68K_XON_CHAR 0x11u #define X68K_XOFF_CHAR 0x13u -// Shared with x68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer -// (writes gX68kRxTail), the mainline is the consumer (writes gX68kRxHead). The byte is stored +// Shared with jlX68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer +// (writes jlgX68kRxTail), the mainline is the consumer (writes jlgX68kRxHead). The byte is stored // before the tail is published and the 68000 is strictly in-order, so volatile indices are the // whole interlock. When the ISR could not be installed, x68kSerialPump below produces instead. -uint8_t gX68kRxRing[X68K_RX_RING_CAP]; -volatile uint16_t gX68kRxHead = 0u; // pop side -volatile uint16_t gX68kRxTail = 0u; // push side -const uint16_t gX68kRxHighWater = X68K_RX_HIGH_WATER; +uint8_t jlgX68kRxRing[X68K_RX_RING_CAP]; +volatile uint16_t jlgX68kRxHead = 0u; // pop side +volatile uint16_t jlgX68kRxTail = 0u; // push side +const uint16_t jlgX68kRxHighWater = X68K_RX_HIGH_WATER; // Flow control, shared with the ISR the same way: it reads the flow and the program's WR5 and sets -// the other two; the mainline clears gX68kRxThrottled when a read lets the peer go. -uint8_t gX68kSerFlow = JL_SERIAL_FLOW_NONE; // jlSerialFlowE of the open port -uint8_t gX68kWr5 = 0u; // channel A's WR5 as the program set it (X68K_WR5_BASE) -volatile uint8_t gX68kRxThrottled = 0u; // the ISR is holding the peer off -volatile uint8_t gX68kTxPaused = 0u; // the peer sent XOFF (nonzero) or XON (0) +// the other two; the mainline clears jlgX68kRxThrottled when a read lets the peer go. +uint8_t jlgX68kSerFlow = JL_SERIAL_FLOW_NONE; // jlSerialFlowE of the open port +uint8_t jlgX68kWr5 = 0u; // channel A's WR5 as the program set it (X68K_WR5_BASE) +volatile uint8_t jlgX68kRxThrottled = 0u; // the ISR is holding the peer off +volatile uint8_t jlgX68kTxPaused = 0u; // the peer sent XOFF (nonzero) or XON (0) // ----- SCC channel A RX interrupt takeover ----------------------------------- // @@ -141,15 +141,15 @@ volatile uint8_t gX68kTxPaused = 0u; // the peer sent XOF // target does (DOS UART IRQ ring, the 8-bit ACIA rings). serial.c reads the SCC's programmed // vector base (RR2 on channel A returns it UNmodified) and steals the two channel A receive vectors // -- base|0x0C "character available" and base|0x0E "special condition" (Z8530 status-affects-vector -// puts the source in bits 3-1) -- pointing them at the raw handlers in x68kSccRxIsr.s. RSDRV's own +// puts the source in bits 3-1) -- pointing them at the raw handlers in jlX68kSccRxIsr.s. RSDRV's own // RX handler simply never runs again; its 64-byte ring starves and the polled pump becomes a no-op. // If the base looks wrong (VIS off, unexpected layout) nothing is stolen and the polled pump keeps // carrying the port exactly as before -- the fallback is automatic. #define X68K_SCC_A_CTRL ((volatile uint8_t *)0xE98005L) #define X68K_VECTOR(v) ((volatile uint32_t *)((uint32_t)(v) << 2)) -extern void x68kSccRxIsr(void); -extern void x68kSccSpIsr(void); +extern void jlX68kSccRxIsr(void); +extern void jlX68kSccSpIsr(void); static void sccIsrInstall(void); static void sccIsrUninstall(void); @@ -204,8 +204,8 @@ static void sccIsrInstall(void) { sr = m68kInterruptsOff(); gSccOldRx = *X68K_VECTOR(gSccRxVec); gSccOldSp = *X68K_VECTOR(gSccRxVec + 2u); - *X68K_VECTOR(gSccRxVec) = (uint32_t)x68kSccRxIsr; - *X68K_VECTOR(gSccRxVec + 2u) = (uint32_t)x68kSccSpIsr; + *X68K_VECTOR(gSccRxVec) = (uint32_t)jlX68kSccRxIsr; + *X68K_VECTOR(gSccRxVec + 2u) = (uint32_t)jlX68kSccSpIsr; m68kInterruptsRestore(sr); gSccIsrInstalled = true; } @@ -238,10 +238,10 @@ static void sccIsrUninstall(void) { static void x68kFlowSend(void) { uint8_t want; - if (gX68kSerFlow != JL_SERIAL_FLOW_XONXOFF) { + if (jlgX68kSerFlow != JL_SERIAL_FLOW_XONXOFF) { return; } - want = gX68kRxThrottled != 0u ? (uint8_t)X68K_XOFF_CHAR : (uint8_t)X68K_XON_CHAR; + want = jlgX68kRxThrottled != 0u ? (uint8_t)X68K_XOFF_CHAR : (uint8_t)X68K_XON_CHAR; if (want != gFlowSent && _iocs_osns232c() != 0) { _iocs_out232c((int)want); gFlowSent = want; @@ -267,12 +267,12 @@ static bool x68kRsdrvPresent(void) { // Let a held-off peer go once a read has the ring down to the low mark: RTS back the way the // program left it, or an XON. static void x68kRxRelease(void) { - if (gX68kRxThrottled == 0u || jlpSerialAvailable() > X68K_RX_LOW_WATER) { + if (jlgX68kRxThrottled == 0u || jlpSerialAvailable() > X68K_RX_LOW_WATER) { return; } - gX68kRxThrottled = 0u; - if (gX68kSerFlow == JL_SERIAL_FLOW_RTSCTS) { - x68kWr5Write(gX68kWr5); + jlgX68kRxThrottled = 0u; + if (jlgX68kSerFlow == JL_SERIAL_FLOW_RTSCTS) { + x68kWr5Write(jlgX68kWr5); } x68kFlowSend(); } @@ -326,13 +326,13 @@ static void x68kSerialPump(void) { return; } while (_iocs_isns232c() != 0) { - uint16_t next = (uint16_t)((gX68kRxTail + 1u) % X68K_RX_RING_CAP); + uint16_t next = (uint16_t)((jlgX68kRxTail + 1u) % X68K_RX_RING_CAP); - if (next == gX68kRxHead) { + if (next == jlgX68kRxHead) { break; // soft ring full: leave the rest in the IOCS ring rather than drop it here } - gX68kRxRing[gX68kRxTail] = (uint8_t)(_iocs_inp232c() & 0xFF); - gX68kRxTail = next; + jlgX68kRxRing[jlgX68kRxTail] = (uint8_t)(_iocs_inp232c() & 0xFF); + jlgX68kRxTail = next; } } @@ -382,11 +382,11 @@ static void x68kWr5Write(uint8_t wr5) { int32_t ssp; uint16_t sr; - gX68kWr5 = wr5; + jlgX68kWr5 = wr5; ssp = x68kSuperEnter(); sr = m68kInterruptsOff(); *X68K_SCC_A_CTRL = X68K_SCC_WR5; - *X68K_SCC_A_CTRL = (gX68kRxThrottled != 0u && gX68kSerFlow == JL_SERIAL_FLOW_RTSCTS) ? (uint8_t)(wr5 & (uint8_t)~X68K_WR5_RTS) : wr5; + *X68K_SCC_A_CTRL = (jlgX68kRxThrottled != 0u && jlgX68kSerFlow == JL_SERIAL_FLOW_RTSCTS) ? (uint8_t)(wr5 & (uint8_t)~X68K_WR5_RTS) : wr5; m68kInterruptsRestore(sr); x68kSuperLeave(ssp); } @@ -396,7 +396,7 @@ static void x68kWr5Write(uint8_t wr5) { uint16_t jlpSerialAvailable(void) { x68kSerialPump(); - return (uint16_t)((gX68kRxTail + X68K_RX_RING_CAP - gX68kRxHead) % X68K_RX_RING_CAP); + return (uint16_t)((jlgX68kRxTail + X68K_RX_RING_CAP - jlgX68kRxHead) % X68K_RX_RING_CAP); } @@ -409,10 +409,10 @@ void jlpSerialClose(void) { if (!gSerialOpened) { return; } - dtrOn = (gX68kWr5 & X68K_WR5_DTR) != 0u; + dtrOn = (jlgX68kWr5 & X68K_WR5_DTR) != 0u; sccIsrUninstall(); // hand channel A RX back to RSDRV before the vectors dangle - gX68kSerFlow = JL_SERIAL_FLOW_NONE; - gX68kRxThrottled = 0u; + jlgX68kSerFlow = JL_SERIAL_FLOW_NONE; + jlgX68kRxThrottled = 0u; (void)_iocs_set232c(gPrevMode); if (!dtrOn) { x68kWr5Write((uint8_t)(x68kWr5ForMode((uint16_t)gPrevMode) & (uint8_t)~X68K_WR5_DTR)); @@ -425,7 +425,7 @@ void jlpSerialFlush(void) { while (_iocs_isns232c() != 0) { (void)_iocs_inp232c(); } - gX68kRxHead = gX68kRxTail; // the ISR may be live: only the consumer side may move + jlgX68kRxHead = jlgX68kRxTail; // the ISR may be live: only the consumer side may move x68kRxRelease(); } @@ -438,17 +438,17 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { return false; } if (!x68kRsdrvPresent()) { - coreSetError(X68K_RSDRV_MISSING); + jlCoreSetError(X68K_RSDRV_MISSING); return false; } - mode = x68kSerialMode(config); - gX68kSerFlow = (uint8_t)config->flow; - gX68kRxThrottled = 0u; - gX68kTxPaused = 0u; - gFlowSent = X68K_XON_CHAR; - gPrevMode = _iocs_set232c((int)mode); - gX68kWr5 = x68kWr5ForMode(mode); // _SET232C just wrote this, DTR and RTS on - gSerialOpened = true; + mode = x68kSerialMode(config); + jlgX68kSerFlow = (uint8_t)config->flow; + jlgX68kRxThrottled = 0u; + jlgX68kTxPaused = 0u; + gFlowSent = X68K_XON_CHAR; + gPrevMode = _iocs_set232c((int)mode); + jlgX68kWr5 = x68kWr5ForMode(mode); // _SET232C just wrote this, DTR and RTS on + gSerialOpened = true; sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong return true; } @@ -464,9 +464,9 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { x68kSerialPump(); n = 0u; - while (n < max && gX68kRxHead != gX68kRxTail) { - buf[n] = gX68kRxRing[gX68kRxHead]; - gX68kRxHead = (uint16_t)((gX68kRxHead + 1u) % X68K_RX_RING_CAP); + while (n < max && jlgX68kRxHead != jlgX68kRxTail) { + buf[n] = jlgX68kRxRing[jlgX68kRxHead]; + jlgX68kRxHead = (uint16_t)((jlgX68kRxHead + 1u) % X68K_RX_RING_CAP); n++; } x68kRxRelease(); @@ -483,9 +483,9 @@ void jlpSerialSetDtr(bool on) { return; } if (on) { - x68kWr5Write((uint8_t)(gX68kWr5 | X68K_WR5_DTR)); + x68kWr5Write((uint8_t)(jlgX68kWr5 | X68K_WR5_DTR)); } else { - x68kWr5Write((uint8_t)(gX68kWr5 & (uint8_t)~X68K_WR5_DTR)); + x68kWr5Write((uint8_t)(jlgX68kWr5 & (uint8_t)~X68K_WR5_DTR)); } } @@ -497,9 +497,9 @@ void jlpSerialSetRts(bool on) { return; } if (on) { - x68kWr5Write((uint8_t)(gX68kWr5 | X68K_WR5_RTS)); + x68kWr5Write((uint8_t)(jlgX68kWr5 | X68K_WR5_RTS)); } else { - x68kWr5Write((uint8_t)(gX68kWr5 & (uint8_t)~X68K_WR5_RTS)); + x68kWr5Write((uint8_t)(jlgX68kWr5 & (uint8_t)~X68K_WR5_RTS)); } } @@ -528,9 +528,9 @@ uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { x68kSerialPump(); for (n = 0u; n < len; n++) { - serialTxWaitStart(&wait, X68K_TX_SPIN_LIMIT); - while (gX68kTxPaused != 0u || _iocs_osns232c() == 0) { - if (serialTxStalled(&wait)) { + jlSerialTxWaitStart(&wait, X68K_TX_SPIN_LIMIT); + while (jlgX68kTxPaused != 0u || _iocs_osns232c() == 0) { + if (jlSerialTxStalled(&wait)) { return n; } } diff --git a/src/x68000/x68kPlanar.h b/src/x68000/x68kPlanar.h index 7c66c70..bed8c57 100644 --- a/src/x68000/x68kPlanar.h +++ b/src/x68000/x68kPlanar.h @@ -60,7 +60,7 @@ #define X68K_ORIGIN_BYTE (X68K_ORIGIN_X / 8u) // 12 bytes // The text plane is 1024 rows, two pages of the 512-row screen: the present // draws into the one not shown and the V-DISP interrupt flips the text Y -// scroll to it (x68kVdispIsr.s), so a frame never shows half of two. +// scroll to it (jlX68kVdispIsr.s), so a frame never shows half of two. #define X68K_TVRAM_ROWS 1024u #define X68K_TEXT_PAGE_ROWS X68K_DISPLAY_H typedef char x68kTextPagesFit[((2u * X68K_TEXT_PAGE_ROWS) <= X68K_TVRAM_ROWS) ? 1 : -1]; diff --git a/src/x68000/x68kPresent.s b/src/x68000/x68kPresent.s index 5a74819..006dff0 100644 --- a/src/x68000/x68kPresent.s +++ b/src/x68000/x68kPresent.s @@ -17,7 +17,7 @@ | straight runs of move.l (An)+,(An)+ entered part-way, so the only per-row | cost left is the band test and the jumps between the four runs. | -| BANDS. gStageMinWord / gStageMaxWord hold each row's first and last dirty +| BANDS. jlgStageMinWord / jlgStageMaxWord hold each row's first and last dirty | 16-bit stage word (4 pixels); a clean row is min 0xFF, max 0x00. A band is | copied in whole LONGS of each plane (8 words), so it can come out up to six | bytes wider than it was marked: those bytes are the surface's own and match @@ -35,14 +35,18 @@ | | GROUPS. The min-word bytes of four rows read as one long; four clean rows | are 0xFFFFFFFF and are passed over with one compare, so a present with -| nothing dirty costs ~30 cycles per four rows. +| nothing dirty costs ~30 cycles per four rows. A group it copies it leaves +| clean, min and max, for the next frame: the bands it is handed are the +| stage's own (jlX68kDirtyUnion merged the last frame's into them), and the +| core does not clear them after a present (JL_PRESENT_CLEARS_BANDS). | -| void x68kPresentText(uint8_t *const *planes, const uint8_t *minWord, -| const uint8_t *maxWord, volatile uint8_t *text); +| void jlX68kPresentText(uint8_t *const *planes, uint8_t *minWord, +| uint8_t *maxWord, volatile uint8_t *text); | | planes the stage's four plane buffers (X68kPlanarT.planes) -| minWord gStageMinWord, aligned(2) so its groups read as longs -| maxWord gStageMaxWord +| minWord jlgStageMinWord, aligned(2) so its groups read as longs; left +| clean +| maxWord jlgStageMaxWord; left clean | text plane 0 of the text plane at stage row 0, byte 0 | | The geometry below is the C's (src/x68000/hal.c holds each value against @@ -89,7 +93,7 @@ .text .even - .globl x68kPresentText + .globl jlX68kPresentText | ---- PRESENT_ROW: one row of a dirty group ---- @@ -109,7 +113,7 @@ .endm -x68kPresentText: +jlX68kPresentText: movem.l %d2-%d7/%a2-%a6,-(%sp) movea.l ARG_MIN(%sp),%a6 move.l ARG_MAX(%sp),%d7 @@ -126,6 +130,10 @@ groupRows: PRESENT_ROW PRESENT_ROW PRESENT_ROW + moveq #CLEAN_GROUP,%d0 | copied: clean for the next frame + move.l %d0,-GROUP_ROWS(%a6) + moveq #0,%d0 + move.l %d0,-GROUP_ROWS(%a6,%d7.l) dbra %d6,groupNext bra.s presentDone groupNext: @@ -236,3 +244,88 @@ copyNone: .ifne (copyPlane3 - copyPlane2) - PLANE_CODE .error "the plane copies must be the same size" .endif + + +| bool jlX68kDirtyUnion(uint8_t *minWord, uint8_t *maxWord, uint8_t *prevMin, +| uint8_t *prevMax); +| +| The page this present draws holds the frame before last, so it copies the +| union of this frame's dirty bands (the stage's: minWord / maxWord) and the +| last frame's (prevMin / prevMax): per row the lower min and the higher max, a +| clean row's 0xFF / 0x00 losing to any band. The union goes into the stage's +| bands themselves, for jlX68kPresentText, which leaves each group it copies +| clean (JL_PRESENT_CLEARS_BANDS); this frame's bands become the last frame's +| for the next present. True when any of this frame's rows was dirty. +| +| Four rows at a time, as longs (every array is aligned(2)): a group clean in +| both frames is two loads and a test and writes nothing, one the same in both +| writes nothing either, and only a group that changed takes its rows one by +| one. gcc's row loop was ~190 cycles a row, reloading its array bases for +| every byte, and wrote all 400 bytes of a separate union every present. + .equ UNION_SAVED, 36 | d2-d7/a2-a4 below the arguments + .equ UNION_ARGS, UNION_SAVED + 4 + +| One row's union, from the bytes at (a0)/(a1) (this frame's min / max) and +| (a2)/(a3) (the last frame's) into (a0)/(a1), every pointer stepped past it. +| cmp.b Dm,Dn takes Dn - Dm: "lower or same" keeps this frame's min, "carry +| clear" (same or higher) its max. + .macro UNION_ROW + move.b (%a2)+,%d4 + cmp.b (%a0)+,%d4 + bcc.s 1f + move.b %d4,-1(%a0) +1: move.b (%a3)+,%d4 + cmp.b (%a1)+,%d4 + bls.s 2f + move.b %d4,-1(%a1) +2: + .endm + +| Only the min bands are walked: a group's max bands are reached from its min +| bands, d5 bytes on in the stage's arrays and a4 in the last frame's, so a +| group clean in both costs its two loads, the test and the dbra. + .globl jlX68kDirtyUnion +jlX68kDirtyUnion: + movem.l %d2-%d7/%a2-%a4,-(%sp) + movem.l UNION_ARGS(%sp),%a0-%a3 + move.l %a1,%d5 + sub.l %a0,%d5 | stage max - stage min + movea.l %a3,%a4 + suba.l %a2,%a4 | last frame's max - min + moveq #0,%d7 | none of this frame's rows dirty yet + moveq #GROUPS - 1,%d6 +.Lu_group: + move.l (%a0)+,%d0 | this frame's min bands + move.l (%a2)+,%d1 | the last frame's + move.l %d0,%d2 + and.l %d1,%d2 + addq.l #1,%d2 + bne.s .Lu_dirty | not clean in both +.Lu_next: + dbra %d6,.Lu_group + move.l %d7,%d0 + movem.l (%sp)+,%d2-%d7/%a2-%a4 + rts +.Lu_dirty: + lea -GROUP_ROWS(%a0,%d5.l),%a1 | the group's max bands + lea -GROUP_ROWS(%a2,%a4.l),%a3 + move.l (%a1),%d2 | this frame's + move.l (%a3),%d3 | the last frame's + addq.l #1,%d0 + beq.s 1f | this frame's group was clean + moveq #1,%d7 +1: subq.l #1,%d0 + cmp.l %d0,%d1 + bne.s .Lu_rows + cmp.l %d2,%d3 + beq.s .Lu_next | the same in both: already the union +.Lu_rows: + subq.l #GROUP_ROWS,%a0 + subq.l #GROUP_ROWS,%a2 + UNION_ROW + UNION_ROW + UNION_ROW + UNION_ROW + move.l %d0,-GROUP_ROWS(%a2) | this frame's, for the next present + move.l %d2,-GROUP_ROWS(%a3) + bra.w .Lu_next diff --git a/src/x68000/x68kRasterIsr.s b/src/x68000/x68kRasterIsr.s index d148072..7884987 100644 --- a/src/x68000/x68kRasterIsr.s +++ b/src/x68000/x68kRasterIsr.s @@ -42,12 +42,12 @@ | the lines in between. Otherwise it drops the request its own step-2 edge left | pending and arms the compare for the next band. After the last band it | rewinds to band 0 for the next frame, taking up the table hal.c has queued -| in gX68kRasterPending if there is one: a new screen's bands always start on +| in jlgX68kRasterPending if there is one: a new screen's bands always start on | a frame boundary. | | LATENESS. The routine must be running before step 2's raster begins, about | one raster after the compare fires. Human68k's Timer C tick used to hold IPL -| 6 for up to ~2000 cycles and made it miss (x68kTickIsr.s now lets this +| 6 for up to ~2000 cycles and made it miss (jlX68kTickIsr.s now lets this | interrupt through). If it is ever that late anyway, step 2's raster has | already begun and GPIP6 would not fall again until the next frame: rather | than spin a whole frame at IPL 6, the wait gives up after REF_WAIT_LINES @@ -77,25 +77,25 @@ .text .even - .globl x68kRasterIsr + .globl jlX68kRasterIsr -x68kRasterIsr: +jlX68kRasterIsr: | Step 2's compare write comes first and only what the edge | waits use is saved before them, so the interrupt can be taken | late and still make it; the rest is saved while the line | above the band is drawn. move.l %a2,%sp@- - movea.l gX68kRasterCursor,%a2 + movea.l jlgX68kRasterCursor,%a2 move.w %a2@(BAND_RASTER),CRTC_R09 movem.l %d1/%a1,%sp@- lea MFP_GPIP,%a1 moveq #REF_WAIT_LINES,%d1 -rasterAbove: | the raster of the line above begins... +rasterAbove: | the raster of the line above begins... btst #GPIP_RINT_BIT,%a1@ beq.s rasterPulse tst.b %a1@ bpl.s rasterAbove -rasterAboveSync: | ...or an H-SYNC period goes by +rasterAboveSync: | ...or an H-SYNC period goes by btst #GPIP_RINT_BIT,%a1@ beq.s rasterPulse tst.b %a1@ @@ -103,30 +103,30 @@ rasterAboveSync: | ...or an H-SYNC period goes by subq.w #1,%d1 bne.s rasterAbove movem.l %d0/%d2-%d7/%a0/%a3,%sp@- - movea.l gX68kRasterTable,%a0 | too late for this frame: resync at the next + movea.l jlgX68kRasterTable,%a0 | too late for this frame: resync at the next movea.l %a0@(TABLE_END),%a2 bra.s rasterNext -rasterPulse: | its blanking (MAME) or sync pulse +rasterPulse: | its blanking (MAME) or sync pulse tst.b %a1@ bpl.s rasterPulse -rasterDisplay: | the line above the band is being drawn +rasterDisplay: | the line above the band is being drawn tst.b %a1@ bmi.s rasterDisplay movem.l %d0/%d2-%d7/%a0/%a3,%sp@- lea TEXT_PALETTE,%a3 rasterLoad: movea.l %a2@(BAND_PALETTE),%a0 - movem.l %a0@,%d0-%d7 | the colours, ready... + movem.l %a0@,%d0-%d7 | the colours, ready... rasterBlank: tst.b %a1@ bpl.s rasterBlank - movem.l %d0-%d7,%a3@ | ...stored in the line's blanking + movem.l %d0-%d7,%a3@ | ...stored in the line's blanking move.w %a2@(BAND_STAY),%d1 lea %a2@(BAND_SIZE),%a2 subq.w #1,%d1 - bmi.s rasterNext | 0: far enough away to re-arm for + bmi.s rasterNext | 0: far enough away to re-arm for bra.s rasterSkipCount -rasterSkipLine: | a line between this band and the next +rasterSkipLine: | a line between this band and the next tst.b %a1@ bmi.s rasterSkipLine rasterSkipBlank: @@ -134,27 +134,27 @@ rasterSkipBlank: bpl.s rasterSkipBlank rasterSkipCount: dbra %d1,rasterSkipLine -rasterStay: | the line above the next band +rasterStay: | the line above the next band tst.b %a1@ bmi.s rasterStay bra.s rasterLoad rasterNext: - movea.l gX68kRasterTable,%a0 + movea.l jlgX68kRasterTable,%a0 cmpa.l %a0@(TABLE_END),%a2 bne.s rasterArm - move.l gX68kRasterPending,%d0 | frame done: a newer table waiting? + move.l jlgX68kRasterPending,%d0 | frame done: a newer table waiting? beq.s rasterRewind movea.l %d0,%a0 - move.l %a0,gX68kRasterTable - clr.l gX68kRasterPending + move.l %a0,jlgX68kRasterTable + clr.l jlgX68kRasterPending rasterRewind: lea %a0@(TABLE_BANDS),%a2 rasterArm: - move.b #CH14_CLEAR,MFP_IPRA | step 2's edge, not a new request + move.b #CH14_CLEAR,MFP_IPRA | step 2's edge, not a new request move.w %a2@(BAND_RASTER),%d0 subq.w #ARM_LEAD,%d0 move.w %d0,CRTC_R09 - move.l %a2,gX68kRasterCursor + move.l %a2,jlgX68kRasterCursor movem.l %sp@+,%d0/%d2-%d7/%a0/%a3 movem.l %sp@+,%d1/%a1 movea.l %sp@+,%a2 diff --git a/src/x68000/x68kSccRxIsr.s b/src/x68000/x68kSccRxIsr.s index 3ee34b8..fb30fb8 100644 --- a/src/x68000/x68kSccRxIsr.s +++ b/src/x68000/x68kSccRxIsr.s @@ -37,16 +37,16 @@ | the chip holds it at the head of the FIFO for the special-condition handler | below, which delivers it as it came, as the other ports do. | -| Ring protocol (single consumer, single producer): the mainline reads gX68kRxHead, -| this ISR writes gX68kRxTail; each byte is stored BEFORE the tail moves, and the +| Ring protocol (single consumer, single producer): the mainline reads jlgX68kRxHead, +| this ISR writes jlgX68kRxTail; each byte is stored BEFORE the tail moves, and the | 68000 is strictly in-order, so no further interlock is needed. A full ring | drops the byte -- the frame CRC upstream turns that into a clean retransmit-less | drop rather than corruption. | -| Flow control (gX68kSerFlow, serial.c): under XON/XOFF the peer's XOFF and XON -| are taken out of the data into gX68kTxPaused, which jlpSerialWrite honours. +| Flow control (jlgX68kSerFlow, serial.c): under XON/XOFF the peer's XOFF and XON +| are taken out of the data into jlgX68kTxPaused, which jlpSerialWrite honours. | Once a drain leaves the ring three quarters full the handler holds the peer -| off, once, by setting gX68kRxThrottled: under RTS/CTS it drops RTS itself +| off, once, by setting jlgX68kRxThrottled: under RTS/CTS it drops RTS itself | (WR5, written with every interrupt masked like the RR1 read); under XON/XOFF | the mainline sends the XOFF, so nothing but the mainline ever writes the | transmitter, which IOCS drives. A read lets the peer go again. @@ -64,7 +64,7 @@ .equ SR_MASK_ALL, 0x0700 .equ SCC_WR5, 5 .equ WR5_RTS_OFF, 0xFD | WR5 without RTS (bit 1) - .equ FLOW_RTSCTS, 1 | gX68kSerFlow values (jlSerialFlowE) + .equ FLOW_RTSCTS, 1 | jlgX68kSerFlow values (jlSerialFlowE) .equ FLOW_XONXOFF, 2 .equ XON_CHAR, 0x11 .equ XOFF_CHAR, 0x13 @@ -75,25 +75,25 @@ .text .even - .globl x68kSccRxIsr - .globl x68kSccSpIsr + .globl jlX68kSccRxIsr + .globl jlX68kSccSpIsr | Both handlers' entry: the registers they use saved, the ring and its tail | in hand, and the drain's count. .macro RXENTRY movem.l %d0-%d3/%a0,-(%sp) - lea gX68kRxRing,%a0 - move.w gX68kRxTail,%d1 + lea jlgX68kRxRing,%a0 + move.w jlgX68kRxTail,%d1 moveq #DRAIN_MAX - 1,%d3 .endm | Channel A "receive character available". -x68kSccRxIsr: +jlX68kSccRxIsr: RXENTRY rxByte: move.b SCC_A_DATA,%d0 | the received byte rxGot: - cmpi.b #FLOW_XONXOFF,gX68kSerFlow + cmpi.b #FLOW_XONXOFF,jlgX68kSerFlow bne.s rxStore cmpi.b #XOFF_CHAR,%d0 beq.s rxFlowByte @@ -101,17 +101,17 @@ rxGot: bne.s rxStore rxFlowByte: eori.b #XON_CHAR,%d0 | XON -> 0 (go), XOFF -> nonzero (pause) - move.b %d0,gX68kTxPaused + move.b %d0,jlgX68kTxPaused bra.s rxFull | flow, not data: on to the next byte rxStore: move.w %d1,%d2 addq.w #1,%d2 andi.w #RING_MASK,%d2 | next tail - cmp.w gX68kRxHead,%d2 + cmp.w jlgX68kRxHead,%d2 beq.s rxFull | ring full: drop, CRC upstream recovers move.b %d0,(%a0,%d1.w) | store BEFORE publishing the new tail move.w %d2,%d1 - move.w %d1,gX68kRxTail + move.w %d1,jlgX68kRxTail rxFull: btst #RR0_RX_AVAILABLE,SCC_A_CTRL beq.s rxDone | the FIFO is empty @@ -123,19 +123,19 @@ rxFull: andi.b #RR1_ERRORS,%d2 dbne %d3,rxByte | the next byte, unless it is in error rxDone: - tst.b gX68kSerFlow + tst.b jlgX68kSerFlow beq.s rxAck | no flow control - tst.b gX68kRxThrottled + tst.b jlgX68kRxThrottled bne.s rxAck | already holding the peer off move.w %d1,%d2 - sub.w gX68kRxHead,%d2 + sub.w jlgX68kRxHead,%d2 andi.w #RING_MASK,%d2 | bytes in the ring - cmp.w gX68kRxHighWater,%d2 + cmp.w jlgX68kRxHighWater,%d2 blo.s rxAck - st gX68kRxThrottled - cmpi.b #FLOW_RTSCTS,gX68kSerFlow + st jlgX68kRxThrottled + cmpi.b #FLOW_RTSCTS,jlgX68kSerFlow bne.s rxAck | XON/XOFF: the mainline sends the XOFF - move.b gX68kWr5,%d2 + move.b jlgX68kWr5,%d2 andi.b #WR5_RTS_OFF,%d2 move.w %sr,%d0 ori.w #SR_MASK_ALL,%sr @@ -151,7 +151,7 @@ rxAck: | The byte is delivered as it came, as on the other ports: read, the error | cleared, and on through the receive handler with it, which drains whatever | follows and drops the IUS. -x68kSccSpIsr: +jlX68kSccSpIsr: RXENTRY move.b SCC_A_DATA,%d0 | the byte with the error move.b #WR0_ERROR_RESET,SCC_A_CTRL diff --git a/src/x68000/x68kTickIsr.s b/src/x68000/x68kTickIsr.s index 5b958f9..5bf6f4f 100644 --- a/src/x68000/x68kTickIsr.s +++ b/src/x68000/x68kTickIsr.s @@ -16,7 +16,7 @@ | The previous handler is IOCS's software-timer dispatcher ($FF1344 in IPL | 1.0), which runs whichever of its four timer callbacks are due and holds IPL | 6 for all of it: 700-2000 cycles, 100 times a second, measured under MAME. -| The SCB raster interrupt (x68kRasterIsr.s) is also an MFP interrupt at IPL 6 +| The SCB raster interrupt (jlX68kRasterIsr.s) is also an MFP interrupt at IPL 6 | and has about one raster (~310 cycles) to be taken, so a raster match that | fell inside that window was taken late and the band's palette reload missed | its line (scripts/verify-x68000-scb.sh). So the dispatcher is CALLED rather @@ -28,11 +28,11 @@ | (VR = $40), so IMR is the only gate: a request masked here stays pending in | IPR and is taken when the masks come back. | -| While a jlAudioTickRegister callback is set, x68kTickHook (hal.c) is its +| While a jlAudioTickRegister callback is set, jlX68kTickHook (hal.c) is its | pacer, called first at IPL 5 with the C scratch registers kept. | | The call's target is patched at install time by installTickIsr() in hal.c -| via the x68kTickChainAddr symbol below. Patching the operand rather than +| via the jlX68kTickChainAddr symbol below. Patching the operand rather than | going through a pointer is what keeps this register-free -- a 68000 has no | jmp ([abs]) addressing mode. | @@ -46,10 +46,10 @@ .text .even - .globl x68kTickIsr - .globl x68kTickChainAddr + .globl jlX68kTickIsr + .globl jlX68kTickChainAddr -x68kTickIsr: +jlX68kTickIsr: | The masks first and the IPL down at once: every cycle spent | here at IPL 6 is a cycle a raster match waits. move.b MFP_IMRA,%sp@- | a byte push keeps the stack even @@ -57,18 +57,18 @@ x68kTickIsr: andi.b #IMRA_RASTER,MFP_IMRA clr.b MFP_IMRB move.w #SR_CALL,%sr | the rest, and the dispatcher, at IPL 5 - addq.l #1,gX68kTicks - tst.l x68kTickHook | jlAudioTickRegister's pacer, if one is set + addq.l #1,jlgX68kTicks + tst.l jlX68kTickHook | jlAudioTickRegister's pacer, if one is set beq.s 1f movem.l %d0-%d1/%a0-%a1,%sp@- | what a C call may use - movea.l x68kTickHook,%a0 + movea.l jlX68kTickHook,%a0 jsr (%a0) movem.l %sp@+,%d0-%d1/%a0-%a1 1: pea x68kTickReturn | the frame the dispatcher's RTE uses move.w #SR_RETURN,%sp@- .word 0x4EF9 | jmp <abs.l> -x68kTickChainAddr: +jlX68kTickChainAddr: .long 0 | patched with the previous handler x68kTickReturn: move.b %sp@+,MFP_IMRB diff --git a/src/x68000/x68kTile.h b/src/x68000/x68kTile.h index 529c9f1..bb2fc00 100644 --- a/src/x68000/x68kTile.h +++ b/src/x68000/x68kTile.h @@ -19,4 +19,11 @@ #include "planarTile.h" +// Solid rects (jlpFillRect): the shared planar fill, straight from +// jlFillRect's on-surface path without a C layer between (a program +// filling its picture a run at a time, JoeyAGI, makes thousands a frame). +static inline __attribute__((always_inline)) void x68kFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { + jlSurface68kFillRect(((X68kPlanarT *)s->portData)->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex); +} + #endif diff --git a/src/x68000/x68kTileLayer.c b/src/x68000/x68kTileLayer.c new file mode 100644 index 0000000..ad7e910 --- /dev/null +++ b/src/x68000/x68kTileLayer.c @@ -0,0 +1,39 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// The X68000's tile-layer ring: the shared separate-plane one +// (src/m68k/planarTileLayer.c), anywhere in memory, copied by the CPU blit. + +#include <stdbool.h> +#include <stdint.h> +#include <string.h> + +#include "planarTileLayer.h" +#include "port.h" + + +void jlpTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) { + jlPlanarTileLayerCpuBlit(dst, layer, ringX, ringY); +} + + +bool jlpTileLayerRingAlloc(jlTileLayerT *layer) { + uint32_t bytes; + + bytes = (uint32_t)layer->ringRows * PLANAR_RING_SLOT_BYTES; + layer->ring = jlpBigAlloc(bytes); + if (layer->ring == NULL) { + return false; + } + memset(layer->ring, 0, bytes); + return true; +} + + +void jlpTileLayerRingFree(jlTileLayerT *layer) { + if (layer->ring != NULL) { + jlpBigFree(layer->ring); + layer->ring = NULL; + } +} diff --git a/src/x68000/x68kVdispIsr.s b/src/x68000/x68kVdispIsr.s index e65f2bc..6cbca86 100644 --- a/src/x68000/x68kVdispIsr.s +++ b/src/x68000/x68kVdispIsr.s @@ -13,12 +13,12 @@ | end-of-interrupt mode (VR = $40), so there is nothing to acknowledge. | | MFP channel 13 is below the SCB raster interrupt (channel 14) and above -| Timer C (channel 5); x68kTickIsr.s masks it while the IOCS dispatcher runs, +| Timer C (channel 5); jlX68kTickIsr.s masks it while the IOCS dispatcher runs, | and an edge in that window is counted as the mask lifts. | | THE FLIP. jlpPresent draws into the text page not shown and hands it over -| (gX68kFlipPending, set last): its text Y scroll, and the colours of a -| one-band screen (gX68kFlipPalette, NULL for none). Written here, at the +| (jlgX68kFlipPending, set last): its text Y scroll, and the colours of a +| one-band screen (jlgX68kFlipPalette, NULL for none). Written here, at the | display's start, they apply to the whole frame: the stage starts 156 lines | down and both pages are blank above and below it, so the write lands well | before the first stage line whenever the CRTC takes it. Without this handler @@ -32,17 +32,17 @@ .text .even - .globl x68kVdispIsr + .globl jlX68kVdispIsr -x68kVdispIsr: - addq.w #1,gX68kFrames - tst.b gX68kFlipPending +jlX68kVdispIsr: + addq.w #1,jlgX68kFrames + tst.b jlgX68kFlipPending beq.s .Ldone - move.w gX68kFlipScroll,CRTC_TEXT_Y - tst.l gX68kFlipPalette + move.w jlgX68kFlipScroll,CRTC_TEXT_Y + tst.l jlgX68kFlipPalette beq.s .Lflipped movem.l %d0/%a0-%a1,-(%sp) - movea.l gX68kFlipPalette,%a0 + movea.l jlgX68kFlipPalette,%a0 lea TEXT_PALETTE,%a1 moveq #PALETTE_LONGS - 1,%d0 .Lcolour: @@ -50,6 +50,6 @@ x68kVdispIsr: dbf %d0,.Lcolour movem.l (%sp)+,%d0/%a0-%a1 .Lflipped: - clr.b gX68kFlipPending + clr.b jlgX68kFlipPending .Ldone: rte diff --git a/src/x86/spriteEmitX86.c b/src/x86/spriteEmitX86.c index a3d5df1..d103944 100644 --- a/src/x86/spriteEmitX86.c +++ b/src/x86/spriteEmitX86.c @@ -168,7 +168,7 @@ static uint32_t esiOperandLen(uint16_t col) { // Emit a draw routine for one shift variant. Returns bytes written, or // SPRITE_EMIT_OVERFLOW if the routine would exceed `cap`. // Routine signature: void f(uint8_t *dst). -uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +uint32_t jlSpriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t cursor; uint16_t row; uint16_t col; @@ -322,7 +322,7 @@ uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint // RESTORE: copy backup -> screen. Destination has the screen stride. -uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +uint32_t jlSpriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t cursor; uint16_t heightPx; uint16_t copyBytes; @@ -367,7 +367,7 @@ uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, u // SAVE: copy screen -> backup. Source has the screen stride. -uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { +uint32_t jlSpriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t cursor; uint16_t heightPx; uint16_t copyBytes; diff --git a/src/x86/spriteMergeRows.s b/src/x86/spriteMergeRows.s index 7c274b0..9f0e523 100644 --- a/src/x86/spriteMergeRows.s +++ b/src/x86/spriteMergeRows.s @@ -4,7 +4,7 @@ # # The x86 ports' sprite interpreter rows (DOS, DVX). # -# x86SpriteMergeRows(dst, src, keepOffset, rows, count, srcStride) +# jlX86SpriteMergeRows(dst, src, keepOffset, rows, count, srcStride) # # The row loop of src/core/sprite.c's chunky interpreter, the draw for an # uncompiled sprite or one partly off the surface. sprite.c picks the @@ -33,8 +33,8 @@ # symbol spellings (DJGPP's leading underscore and the plain one). .text - .globl x86SpriteMergeRows - .globl _x86SpriteMergeRows + .globl jlX86SpriteMergeRows + .globl _jlX86SpriteMergeRows .equ ROW_BYTES, 160 # JL_SURFACE_BYTES_PER_ROW (sprite.c checks it) .equ MERGE_DWORDS, 40 # ROW_BYTES / 4: the widest clipped row @@ -50,8 +50,8 @@ .equ ARG_SRCSTRIDE, 40 .p2align 4 -x86SpriteMergeRows: -_x86SpriteMergeRows: +jlX86SpriteMergeRows: +_jlX86SpriteMergeRows: pushl %ebx pushl %esi pushl %edi diff --git a/tests/goldens/sfxmix-baseline.txt b/tests/goldens/sfxmix-baseline.txt index e4ce645..378e21b 100644 --- a/tests/goldens/sfxmix-baseline.txt +++ b/tests/goldens/sfxmix-baseline.txt @@ -1,4 +1,4 @@ -sfxMixHost: audioSfxOverlayMix scenarios (LCG-seeded, deterministic) +sfxMixHost: jlAudioSfxOverlayMix scenarios (LCG-seeded, deterministic) name=fixedUp8363to11025 slots=1 len=1024 hash=0x4136218D first=12 52 3F 4B CF B2 3B 1B 47 47 1E 1B 00 59 62 51 raw=12523F4BCFB23B1B47471E1B0059625100A89E0D6B00D37FEDDD814DAD8FFF5BB3FFFF2C77E186607840DA883A6CEED100578AAA25C21B7255123E153B0045FED2E6E95D0061D8A07740001F007483E94F4A02D7373010FFFFC135B000E2B19586FF5E4AB9AC7FB637CB0EFFFA4CF5E30054ABAA825D00F49980AD4A2296546A519AF665295CCBDAFFFF1F0DFEA7FFCCB137AB6EFBDD33B6AA26F3FF62AD74B3D5FF7149000070FFD6FF84C0FBEC0B4841FFEBFF84613B000027B93F6600002B0000AC86250FF5DA500F95CBFFF100FF50C4367AABA424B99E594FFF6F9FE26EFF5A3DFF7600747F005579000286FFFFDE63B57E00687ABCCE770F5AFF43AD1C4BD500466C6FB007441EB38410C1001961FF0442DAC67755ADC7685C5BFFE1F5A39343D0FF6A95D8D2FF5CCFFD80758594FFBECA000564617ABC73FF9EC7EF0000009400CB1717001600B92800E3CD0D715DB566C05DC88900DFB5039FF1A9CB2C3CC286A200EAFFC8A559FFB1FF7D9C7E87D7B602281E60A1F39661989A00D970F9729F0000553CE7F743A57800499981FFF2C7B983CAAD620000B1C8C726FF7ECEFFDC0884F7387564C75A567854E760973BE25E0883727553D3B170BB5C7BB97DFFEF5E5E63A4C7AC479CFFDF152700C0FFF9B7168067962894290098FFA14A6245F18E169C060000109C24CBCD97AE7F6B594700919FB20525DEAB009DA02B8F118C2E2E00B9FF1E0048B713C9C26F5F7B7B0044AB97A822A0EFD34D00B0FFFFDB4D00CDDE9AEE71748D6FC0FF0066E099FFD75350788F36C8FFDEB56972EC88FF6B3526E0704C333850FF3D0D7B59E569FFAAFFD30087ABB4C7002491294E0B99B60DFF17A3B776BC7229241C0044008CEC6B2860ED9C00F100823C27BCD5667F74511C4DFFFFA460FF4D493EFB189F75825F9BFF3AB4C57C656CFC08619A5E8500798CC900154BBB74DFE79895CA848316817A92FFDD7208966B0037A9AE67CCDB0B0068594D9D118700FFE8EE85533B1E652259C0002CF83A7F00A33DFF79E7C52A58370047E618A4C5B6469CC944D20D346C0093D5B5FFFF603226307FAC58940001B1937D6B77008C6C560000C51FB64B49A21F4C91E20058919800B716BBABBEFFA000420287C3FFAE0B2F4E8E3A3846FFD489473BFF64DA9962006B79615B2323394D46819E2FFFCE3D5AFF6382A79491AB04A6794AF4AADBD464C59091C977EDAE4A8184DA51599269E5FFF527BA6EB388101007E1B15A45BE5B6CDD5062B62A00892B91213730328C39B100557FFF480984DDB9D015B21070211ADC00556B648285365C1A8E187A59CADF79558A653077FF64E1A200D42B001730FF0015E4E342692CF373BCB9C0CDFF8A535BC27FD2FFEDA00023AF314754FF44A70012B2FFB856FFB6CAFF6240A1F3FD63095FC7596A84DF200000903700D2 diff --git a/tests/goldens/uber/amiga.txt b/tests/goldens/uber/amiga.txt index 2168827..6549be5 100644 --- a/tests/goldens/uber/amiga.txt +++ b/tests/goldens/uber/amiga.txt @@ -19,7 +19,7 @@ amiga: shadow plane 2 in CHIP (blitter, addr=$00019100) amiga: shadow plane 3 in CHIP (blitter, addr=$0001B040) UBER: jlSpriteCompile: 1 call in <= 1 frame -UBER: audioInit OK +UBER: jlAudioOpen OK UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene @@ -219,3 +219,8 @@ UBER: press any key to exit UBER: jlTileMapPaste 10x5: 49 iters / 16 frames = 153 ops/sec | hash=52B9646B UBER: gameFrameClean composite: 11 iters / 16 frames = 34 ops/sec | hash=C51FE171 +UBER: jlTileLayerDraw scroll: 22 iters / 132 frames = 8 ops/sec | hash=2DDFC909 +UBER: jlTileLayerDraw scroll + present: 18 iters / 131 frames = 6 ops/sec | hash=2DDFC909 +UBER-CHK: tileLayer moves: hash=59A121BE +UBER-CHK: tilePasteGlyph: hash=CA66C63E +UBER-CHK: fillRectRows: hash=FA0501E4 diff --git a/tests/goldens/uber/atarist.txt b/tests/goldens/uber/atarist.txt index ededcd8..7df4e03 100644 --- a/tests/goldens/uber/atarist.txt +++ b/tests/goldens/uber/atarist.txt @@ -15,7 +15,7 @@ # reference (DESIGN.md calls it the perf floor). UBER: jlSpriteCompile: 1 call in <= 1 frame -UBER: audioInit OK +UBER: jlAudioOpen OK UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene @@ -215,3 +215,8 @@ UBER: press any key to exit UBER: jlTileMapPaste 10x5: 49 iters / 18 frames = 163 ops/sec | hash=52B9646B UBER: gameFrameClean composite: 13 iters / 16 frames = 48 ops/sec | hash=C51FE171 +UBER: jlTileLayerDraw scroll: 23 iters / 132 frames = 10 ops/sec | hash=2DDFC909 +UBER: jlTileLayerDraw scroll + present: 18 iters / 131 frames = 8 ops/sec | hash=2DDFC909 +UBER-CHK: tileLayer moves: hash=59A121BE +UBER-CHK: tilePasteGlyph: hash=CA66C63E +UBER-CHK: fillRectRows: hash=FA0501E4 diff --git a/tests/goldens/uber/dos.txt b/tests/goldens/uber/dos.txt index 119bdec..b641468 100644 --- a/tests/goldens/uber/dos.txt +++ b/tests/goldens/uber/dos.txt @@ -15,7 +15,7 @@ # reference (DESIGN.md calls it the perf floor). UBER: jlSpriteCompile: 1 call in <= 1 frame -UBER: audioInit OK +UBER: jlAudioOpen OK UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene @@ -215,3 +215,8 @@ UBER: press any key to exit UBER: jlTileMapPaste 10x5: 113 iters / 18 frames = 450 ops/sec | hash=52B9646B UBER: gameFrameClean composite: 43 iters / 16 frames = 195 ops/sec | hash=C51FE171 +UBER: jlTileLayerDraw scroll: 93 iters / 128 frames = 50 ops/sec | hash=2DDFC909 +UBER: jlTileLayerDraw scroll + present: 13 iters / 135 frames = 6 ops/sec | hash=2DDFC909 +UBER-CHK: tileLayer moves: hash=59A121BE +UBER-CHK: tilePasteGlyph: hash=CA66C63E +UBER-CHK: fillRectRows: hash=FA0501E4 diff --git a/tests/goldens/uber/iigs.txt b/tests/goldens/uber/iigs.txt index fd9c01c..620b15c 100644 --- a/tests/goldens/uber/iigs.txt +++ b/tests/goldens/uber/iigs.txt @@ -14,7 +14,7 @@ # intended -- these hashes are what every port is held to. The IIgs is the # reference (DESIGN.md calls it the perf floor). UBER: jlSpriteCompile: 1 call in <= 1 frame -UBER: audioInit OK +UBER: jlAudioOpen OK UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene UBER: ----- begin ----- UBER: jlSurfaceClear: 9 iters / 16 frames = 33 ops/sec | hash=428CEE42 @@ -115,3 +115,8 @@ UBER: total wall time: 419016 ms (25141 frames @ 60 Hz) UBER: press any key to exit UBER: jlTileMapPaste 10x5: 15 iters / 16 frames = 56 ops/sec | hash=52B9646B UBER: gameFrameClean composite: 5 iters / 16 frames = 18 ops/sec | hash=C51FE171 +UBER: jlTileLayerDraw scroll: 20 iters / 128 frames = 9 ops/sec | hash=2DDFC909 +UBER: jlTileLayerDraw scroll + present: 10 iters / 128 frames = 4 ops/sec | hash=2DDFC909 +UBER-CHK: tileLayer moves: hash=59A121BE +UBER-CHK: tilePasteGlyph: hash=CA66C63E +UBER-CHK: fillRectRows: hash=FA0501E4 diff --git a/tests/host/dirtyMarkHost.c b/tests/host/dirtyMarkHost.c index 6155d98..1254d84 100644 --- a/tests/host/dirtyMarkHost.c +++ b/tests/host/dirtyMarkHost.c @@ -6,14 +6,18 @@ // (src/core/surface.c, src/m68k/stageBands.s) against the plain per-row // widen they must match byte for byte. // -// surfaceMarkDirtyRows takes rows four at a time and reuses a group's result +// jlSurfaceMarkDirtyRows takes rows four at a time and reuses a group's result // for the next group that held the same bytes, so its answer depends on what // the rows held: a mark of fresh rows, of rows an earlier rect marked, and of // rows every earlier mark left different all take different paths. Each // scenario seeds the bands one of several ways -- clean, per-row random, runs -// of equal bands, every row full -- then applies a few random marks of every -// height and phase, comparing all 400 band bytes with a reference after each. -// Also checks the whole-stage fills (stageDirtyClearAll, surfaceMarkDirtyAll). +// of equal bands, every row full, four rows' bands repeating -- then applies a +// few random marks of every height and phase, comparing all 400 band bytes +// with a reference after each. +// Also checks the whole-stage fills (jlStageDirtyClearAll, jlSurfaceMarkDirtyAll), +// jlFillRect's mark (the ST's is asm, src/atarist/lineSpan.s), and jlTileFill's +// whole-tile mark (src/core/tile.c), which takes clean rows and rows already +// holding the tile's band a long at a time. // // Built for the host by scripts/check-dirtymark.sh (the C marker), and for the // Atari ST by scripts/verify-atarist-dirtymark.sh, which runs it under Hatari @@ -26,6 +30,8 @@ #include <stdio.h> #include <string.h> +#include "joey/draw.h" +#include "joey/tile.h" #include "surfaceInternal.h" #ifndef DIRTY_MARK_SCENARIOS @@ -34,21 +40,32 @@ #define MARKS_PER_SCENARIO 8u #define RNG_SEED 0x2545F491ul -#define SEED_KINDS 4u +#define SEED_KINDS 5u #define SEED_CLEAN 0u #define SEED_RANDOM_ROWS 1u #define SEED_RUNS 2u #define SEED_FULL 3u +#define SEED_PERIODIC 4u +#define PERIOD_ROWS 4u #define RUN_ROWS_MAX 24u #define CLEAN_ONE_IN 3u #define HEIGHT_CLASSES 3u #define HEIGHT_SMALL_MAX 12u #define HEIGHT_MEDIUM_MAX 64u #define FAILURES_SHOWN 5u +#define TILE_SCENARIOS (DIRTY_MARK_SCENARIOS / 4ul) +#define TILE_REPEAT_ONE_IN 3u +#define TILE_FILL_COLOR 1u +#define FILL_SCENARIOS (DIRTY_MARK_SCENARIOS / 4ul) +#define FILL_OFF_EDGE 40u +#define FILL_COLOR 2u static void bandRandom(uint8_t *minWord, uint8_t *maxWord); static bool bandsMatch(const char *what); +static void checkFillRectMarks(void); +static void checkTileMarks(void); static uint16_t heightRandom(uint16_t y); +static void periodicBands(uint8_t *minWord, uint8_t *maxWord); static void refMark(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord); static uint32_t rngNext(void); static uint16_t rngBelow(uint16_t n); @@ -74,27 +91,96 @@ static void bandRandom(uint8_t *minWord, uint8_t *maxWord) { } -// Compare the stage's bands with the reference; report the first few misses. +// Compare the stage's bands with the reference; report the first few misses, +// and put the reference back in the bands after one so the next check starts +// from the same state. static bool bandsMatch(const char *what) { uint16_t y; gChecks++; - if (memcmp(gStageMinWord, gRefMin, JL_SURFACE_HEIGHT) == 0 && memcmp(gStageMaxWord, gRefMax, JL_SURFACE_HEIGHT) == 0) { + if (memcmp(jlgStageMinWord, gRefMin, JL_SURFACE_HEIGHT) == 0 && memcmp(jlgStageMaxWord, gRefMax, JL_SURFACE_HEIGHT) == 0) { return true; } gFailures++; if (gFailures <= FAILURES_SHOWN) { for (y = 0u; y < JL_SURFACE_HEIGHT; y++) { - if (gStageMinWord[y] != gRefMin[y] || gStageMaxWord[y] != gRefMax[y]) { - printf("dirtyMark: FAIL %s: row %u is [%u,%u], expected [%u,%u]\n", what, (unsigned)y, (unsigned)gStageMinWord[y], (unsigned)gStageMaxWord[y], (unsigned)gRefMin[y], (unsigned)gRefMax[y]); + if (jlgStageMinWord[y] != gRefMin[y] || jlgStageMaxWord[y] != gRefMax[y]) { + printf("dirtyMark: FAIL %s: row %u is [%u,%u], expected [%u,%u]\n", what, (unsigned)y, (unsigned)jlgStageMinWord[y], (unsigned)jlgStageMaxWord[y], (unsigned)gRefMin[y], (unsigned)gRefMax[y]); break; } } } + memcpy(jlgStageMinWord, gRefMin, JL_SURFACE_HEIGHT); + memcpy(jlgStageMaxWord, gRefMax, JL_SURFACE_HEIGHT); return false; } +// jlFillRect on the stage, from every seed: rects of every height and any +// width (now and then none), some reaching past the edges, so the clip path +// marks as well as the on-surface one. +static void checkFillRectMarks(void) { + uint32_t scenario; + uint16_t mark; + int16_t x; + int16_t y; + uint16_t w; + uint16_t h; + int16_t left; + int16_t top; + int16_t right; + int16_t bottom; + + for (scenario = 0ul; scenario < FILL_SCENARIOS; scenario++) { + seedBands((uint16_t)(scenario % SEED_KINDS)); + for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) { + x = (int16_t)((int16_t)rngBelow(JL_SURFACE_WIDTH + 2u * FILL_OFF_EDGE) - (int16_t)FILL_OFF_EDGE); + y = (int16_t)((int16_t)rngBelow(JL_SURFACE_HEIGHT + 2u * FILL_OFF_EDGE) - (int16_t)FILL_OFF_EDGE); + w = rngBelow(JL_SURFACE_WIDTH + 1u); + h = heightRandom(rngBelow(JL_SURFACE_HEIGHT)); + jlFillRect(jlgStage, x, y, w, h, FILL_COLOR); + left = (x < 0) ? 0 : x; + top = (y < 0) ? 0 : y; + right = (x + (int16_t)w > JL_SURFACE_WIDTH) ? (int16_t)JL_SURFACE_WIDTH : (int16_t)(x + (int16_t)w); + bottom = (y + (int16_t)h > JL_SURFACE_HEIGHT) ? (int16_t)JL_SURFACE_HEIGHT : (int16_t)(y + (int16_t)h); + if (right > left && bottom > top) { + refMark((uint16_t)top, (uint16_t)bottom, SURFACE_WORD_INDEX(left), SURFACE_WORD_INDEX(right - 1)); + } + if (!bandsMatch("jlFillRect") && gFailures <= FAILURES_SHOWN) { + printf("dirtyMark: scenario %lu fill %u: %d,%d %ux%u\n", (unsigned long)scenario, (unsigned)mark, (int)x, (int)y, (unsigned)w, (unsigned)h); + } + } + } +} + + +// jlTileFill on the stage, from every seed: a third of the fills repeat the +// last tile, so its rows already hold the band. +static void checkTileMarks(void) { + uint32_t scenario; + uint16_t mark; + uint8_t bx; + uint8_t by; + + bx = 0u; + by = 0u; + for (scenario = 0ul; scenario < TILE_SCENARIOS; scenario++) { + seedBands((uint16_t)(scenario % SEED_KINDS)); + for (mark = 0u; mark < MARKS_PER_SCENARIO; mark++) { + if (mark == 0u || rngBelow(TILE_REPEAT_ONE_IN) != 0u) { + bx = (uint8_t)rngBelow(JL_TILE_BLOCKS_PER_ROW); + by = (uint8_t)rngBelow(JL_TILE_BLOCKS_PER_COL); + } + jlTileFill(jlgStage, bx, by, TILE_FILL_COLOR); + refMark((uint16_t)(by * JL_TILE_PIXELS_PER_SIDE), (uint16_t)((by + 1u) * JL_TILE_PIXELS_PER_SIDE), (uint8_t)(bx * 2u), (uint8_t)(bx * 2u + 1u)); + if (!bandsMatch("jlTileFill") && gFailures <= FAILURES_SHOWN) { + printf("dirtyMark: scenario %lu fill %u: tile %u,%u\n", (unsigned long)scenario, (unsigned)mark, (unsigned)bx, (unsigned)by); + } + } + } +} + + // A mark height for rows from y: short, medium and tall about equally often, // clipped to the stage. static uint16_t heightRandom(uint16_t y) { @@ -117,6 +203,29 @@ static uint16_t heightRandom(uint16_t y) { } +// PERIOD_ROWS random bands; then, a coin each, every min made the least +// of them and every max the greatest (each band still min <= max). +static void periodicBands(uint8_t *minWord, uint8_t *maxWord) { + uint8_t least; + uint8_t greatest; + uint16_t i; + + least = STAGE_DIRTY_CLEAN_MIN; + greatest = STAGE_DIRTY_CLEAN_MAX; + for (i = 0u; i < PERIOD_ROWS; i++) { + bandRandom(&minWord[i], &maxWord[i]); + least = (minWord[i] < least) ? minWord[i] : least; + greatest = (maxWord[i] > greatest) ? maxWord[i] : greatest; + } + if (rngBelow(2u) == 0u) { + memset(minWord, least, PERIOD_ROWS); + } + if (rngBelow(2u) == 0u) { + memset(maxWord, greatest, PERIOD_ROWS); + } +} + + // The widen every marker must match: each row, one at a time. static void refMark(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { for (; y < yEnd; y++) { @@ -149,6 +258,8 @@ static void seedBands(uint16_t kind) { uint16_t run; uint8_t minWord; uint8_t maxWord; + uint8_t periodMin[PERIOD_ROWS]; + uint8_t periodMax[PERIOD_ROWS]; y = 0u; while (y < JL_SURFACE_HEIGHT) { @@ -163,6 +274,13 @@ static void seedBands(uint16_t kind) { minWord = 0u; maxWord = STAGE_DIRTY_FULL_MAX; break; + case SEED_PERIODIC: + // Four rows' bands repeating down the stage, their mins (or + // maxes) sometimes all one: band groups whose halves match + // without their bytes all matching. + periodicBands(periodMin, periodMax); + run = JL_SURFACE_HEIGHT; + break; case SEED_RUNS: run = (uint16_t)(1u + rngBelow(RUN_ROWS_MAX)); if (rngBelow(CLEAN_ONE_IN) == 0u) { @@ -183,17 +301,21 @@ static void seedBands(uint16_t kind) { break; } for (; run != 0u && y < JL_SURFACE_HEIGHT; run--, y++) { - setRow(y, minWord, maxWord); + if (kind == SEED_PERIODIC) { + setRow(y, periodMin[y % PERIOD_ROWS], periodMax[y % PERIOD_ROWS]); + } else { + setRow(y, minWord, maxWord); + } } } } static void setRow(uint16_t y, uint8_t minWord, uint8_t maxWord) { - gStageMinWord[y] = minWord; - gStageMaxWord[y] = maxWord; - gRefMin[y] = minWord; - gRefMax[y] = maxWord; + jlgStageMinWord[y] = minWord; + jlgStageMaxWord[y] = maxWord; + gRefMin[y] = minWord; + gRefMax[y] = maxWord; } @@ -208,17 +330,17 @@ int main(void) { FILE *log; #endif - // The whole-stage fills. (gStage is NULL here, which is the stage.) + // The whole-stage fills. (jlgStage is NULL here, which is the stage.) seedBands(SEED_RANDOM_ROWS); - stageDirtyClearAll(); + jlStageDirtyClearAll(); memset(gRefMin, STAGE_DIRTY_CLEAN_MIN, JL_SURFACE_HEIGHT); memset(gRefMax, STAGE_DIRTY_CLEAN_MAX, JL_SURFACE_HEIGHT); - bandsMatch("stageDirtyClearAll"); + bandsMatch("jlStageDirtyClearAll"); seedBands(SEED_RANDOM_ROWS); - surfaceMarkDirtyAll(gStage); + jlSurfaceMarkDirtyAll(jlgStage); memset(gRefMin, 0, JL_SURFACE_HEIGHT); memset(gRefMax, STAGE_DIRTY_FULL_MAX, JL_SURFACE_HEIGHT); - bandsMatch("surfaceMarkDirtyAll"); + bandsMatch("jlSurfaceMarkDirtyAll"); for (scenario = 0ul; scenario < DIRTY_MARK_SCENARIOS; scenario++) { seedBands((uint16_t)(scenario % SEED_KINDS)); @@ -226,18 +348,25 @@ int main(void) { y = rngBelow(JL_SURFACE_HEIGHT); h = heightRandom(y); bandRandom(&minWord, &maxWord); - surfaceMarkDirtyRows(y, (uint16_t)(y + h), minWord, maxWord); + jlSurfaceMarkDirtyRows(y, (uint16_t)(y + h), minWord, maxWord); refMark(y, (uint16_t)(y + h), minWord, maxWord); - if (!bandsMatch("surfaceMarkDirtyRows")) { - if (gFailures <= FAILURES_SHOWN) { - printf("dirtyMark: scenario %lu mark %u: rows %u..%u [%u,%u]\n", (unsigned long)scenario, (unsigned)mark, (unsigned)y, (unsigned)(y + h - 1u), (unsigned)minWord, (unsigned)maxWord); - } - memcpy(gStageMinWord, gRefMin, JL_SURFACE_HEIGHT); - memcpy(gStageMaxWord, gRefMax, JL_SURFACE_HEIGHT); + if (!bandsMatch("jlSurfaceMarkDirtyRows") && gFailures <= FAILURES_SHOWN) { + printf("dirtyMark: scenario %lu mark %u: rows %u..%u [%u,%u]\n", (unsigned long)scenario, (unsigned)mark, (unsigned)y, (unsigned)(y + h - 1u), (unsigned)minWord, (unsigned)maxWord); } } } + // The drawing marks, on a stage made for them. + jlgStage = jlSurfaceCreate(); + if (jlgStage == NULL) { + printf("dirtyMark: FAIL could not make a stage for the drawing marks\n"); + gFailures++; + } else { + checkFillRectMarks(); + checkTileMarks(); + jlSurfaceDestroy(jlgStage); + jlgStage = NULL; + } printf("dirtyMark: %lu passed, %lu failed\n", (unsigned long)(gChecks - gFailures), (unsigned long)gFailures); #ifdef DIRTY_MARK_LOG log = fopen(DIRTY_MARK_LOG, "w"); diff --git a/tests/host/inputHost.c b/tests/host/inputHost.c index 54e5f67..cbacb3a 100644 --- a/tests/host/inputHost.c +++ b/tests/host/inputHost.c @@ -8,10 +8,10 @@ // semantics, and the jlInputPoll refill path via a mocked // jlpInputPoll; a one-button stick's tap (button 0) and hold // (button 1), with a mocked jlMillisElapsed; the dead zone, axis scaling -// and chord rule every port shares (inputDeadZone, inputAxisScale, -// inputCharTyped); a key two physical keys give, down while either is +// and chord rule every port shares (jlInputDeadZone, jlInputAxisScale, +// jlInputCharTyped); a key two physical keys give, down while either is // held (inputKeyHeld); the mouse every port starts in the middle of the -// stage and keeps on it (inputReset, inputMouseMove); and what ends +// stage and keeps on it (jlInputReset, jlInputMouseMove); and what ends // jlWaitForAnyKey, through a scripted jlpInputPoll. // // Built by scripts/check-input.sh with -DJOEYLIB_PLATFORM_BLANK so @@ -85,27 +85,27 @@ void jlpInputPoll(void) { int i; if (gWaitScript != NULL) { - memset(gKeyState, 0, sizeof(gKeyState)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); if (gWaitAt < gWaitSteps) { - gKeyState[gWaitScript[gWaitAt].key] = 1u; - gKeyState[gWaitScript[gWaitAt].key2] = 1u; + jlgKeyState[gWaitScript[gWaitAt].key] = 1u; + jlgKeyState[gWaitScript[gWaitAt].key2] = 1u; for (c = gWaitScript[gWaitAt].chars; *c != '\0'; c++) { jlInputCharPush((uint8_t)*c); } gWaitAt++; } else { - gKeyState[JL_KEY_ESCAPE] = 1u; - gWaitOverrun = true; + jlgKeyState[JL_KEY_ESCAPE] = 1u; + gWaitOverrun = true; } - gKeyState[JL_KEY_NONE] = 0u; + jlgKeyState[JL_KEY_NONE] = 0u; return; } for (i = 0; i < gMockCount; i++) { jlInputCharPush(gMockChars[i]); } - gMockCount = 0; - gJoyButtonState[JL_JOYSTICK_0][JL_JOY_BUTTON_0] = gMockButtons[JL_JOY_BUTTON_0]; - gJoyButtonState[JL_JOYSTICK_0][JL_JOY_BUTTON_1] = gMockButtons[JL_JOY_BUTTON_1]; + gMockCount = 0; + jlgJoyButtonState[JL_JOYSTICK_0][JL_JOY_BUTTON_0] = gMockButtons[JL_JOY_BUTTON_0]; + jlgJoyButtonState[JL_JOYSTICK_0][JL_JOY_BUTTON_1] = gMockButtons[JL_JOY_BUTTON_1]; } @@ -182,13 +182,13 @@ static bool runAxisScale(void) { static const int32_t kMin[] = { INT16_MIN, INT16_MIN, INT16_MIN, INT16_MIN, 0, 0, 0, 0, 0 }; static const int32_t kMax[] = { INT16_MAX, INT16_MAX, INT16_MAX, INT16_MAX, 65535, 65535, 65535, 65535, 65535 }; static const int8_t kOut[] = { -127, 127, 0, 0, 0, 0, -127, 127, -127 }; - uint16_t i; - bool ok; + uint16_t i; + bool ok; ok = true; for (i = 0u; i < (uint16_t)(sizeof(kPos) / sizeof(kPos[0])); i++) { - if (inputAxisScale(kPos[i], kMin[i], kMax[i]) != kOut[i]) { - printf(" axis-scale: %ld in %ld..%ld gave %d, want %d\n", (long)kPos[i], (long)kMin[i], (long)kMax[i], inputAxisScale(kPos[i], kMin[i], kMax[i]), kOut[i]); + if (jlInputAxisScale(kPos[i], kMin[i], kMax[i]) != kOut[i]) { + printf(" axis-scale: %ld in %ld..%ld gave %d, want %d\n", (long)kPos[i], (long)kMin[i], (long)kMax[i], jlInputAxisScale(kPos[i], kMin[i], kMax[i]), kOut[i]); ok = false; } } @@ -203,12 +203,12 @@ static bool runChord(void) { bool ok; jlInputCharReset(); - inputCharTyped('m', true); - inputCharTyped(JL_CHAR_RETURN, true); - inputCharTyped(JL_CHAR_DELETE, true); + jlInputCharTyped('m', true); + jlInputCharTyped(JL_CHAR_RETURN, true); + jlInputCharTyped(JL_CHAR_DELETE, true); ok = jlInputGetChar() == -1; - inputCharTyped('m', false); - inputCharTyped(JL_CHAR_RETURN, false); + jlInputCharTyped('m', false); + jlInputCharTyped(JL_CHAR_RETURN, false); ok = ok && jlInputGetChar() == 'm' && jlInputGetChar() == JL_CHAR_RETURN && jlInputGetChar() == -1; printf(" chord: %s\n", ok ? "PASS" : "FAIL"); return ok; @@ -218,16 +218,16 @@ static bool runChord(void) { // A reading within the dead zone of centre is 0, at its edge too; one past // it is the reading; a zone of 0 leaves every reading but 0 alone. static bool runDeadZone(void) { - static const int8_t kIn[] = { 10, 11, -10, -11, 0, 1, -1, 127, -127 }; + static const int8_t kIn[] = { 10, 11, -10, -11, 0, 1, -1, 127, -127 }; static const uint8_t kZone[] = { 10, 10, 10, 10, 0, 0, 0, 10, 10 }; - static const int8_t kOut[] = { 0, 11, 0, -11, 0, 1, -1, 127, -127 }; - uint16_t i; - bool ok; + static const int8_t kOut[] = { 0, 11, 0, -11, 0, 1, -1, 127, -127 }; + uint16_t i; + bool ok; ok = true; for (i = 0u; i < (uint16_t)(sizeof(kIn) / sizeof(kIn[0])); i++) { - if (inputDeadZone(kIn[i], kZone[i]) != kOut[i]) { - printf(" dead-zone: %d in a zone of %u gave %d, want %d\n", kIn[i], kZone[i], inputDeadZone(kIn[i], kZone[i]), kOut[i]); + if (jlInputDeadZone(kIn[i], kZone[i]) != kOut[i]) { + printf(" dead-zone: %d in a zone of %u gave %d, want %d\n", kIn[i], kZone[i], jlInputDeadZone(kIn[i], kZone[i]), kOut[i]); ok = false; } } @@ -286,14 +286,14 @@ static bool runInterleave(void) { static bool runMouse(void) { bool ok; - inputMouseMove(5, 7); - inputReset(); + jlInputMouseMove(5, 7); + jlInputReset(); ok = jlMouseX() == JL_SURFACE_WIDTH / 2 && jlMouseY() == JL_SURFACE_HEIGHT / 2; - inputMouseMove(-5, 250); + jlInputMouseMove(-5, 250); ok = ok && jlMouseX() == 0 && jlMouseY() == JL_SURFACE_HEIGHT - 1; - inputMouseMove(400, -1); + jlInputMouseMove(400, -1); ok = ok && jlMouseX() == JL_SURFACE_WIDTH - 1 && jlMouseY() == 0; - inputMouseMove(100, 50); + jlInputMouseMove(100, 50); ok = ok && jlMouseX() == 100 && jlMouseY() == 50; printf(" mouse: %s\n", ok ? "PASS" : "FAIL"); return ok; @@ -402,7 +402,7 @@ static bool runSecondKey(void) { static bool runTapOrHold(void) { bool ok = true; - gJoyOneButton[JL_JOYSTICK_0] = true; + jlgJoyOneButton[JL_JOYSTICK_0] = true; ok &= pollJoy("tap down", 1000u, true, false, false); ok &= pollJoy("tap held", 1300u, true, false, false); ok &= pollJoy("tap let go", 1350u, false, true, false); @@ -418,8 +418,8 @@ static bool runTapOrHold(void) { ok &= jlJoyReleased(JL_JOYSTICK_0, JL_JOY_BUTTON_1); ok &= pollJoy("hold over", 2970u, false, false, false); - gJoyOneButton[JL_JOYSTICK_0] = false; - gMockButtons[JL_JOY_BUTTON_1] = true; + jlgJoyOneButton[JL_JOYSTICK_0] = false; + gMockButtons[JL_JOY_BUTTON_1] = true; ok &= pollJoy("two buttons", 3000u, true, true, true); gMockButtons[JL_JOY_BUTTON_1] = false; ok &= pollJoy("two buttons up", 3020u, false, false, false); @@ -465,14 +465,14 @@ static bool waitEndsAt(const char *name, const WaitStepT *script, int steps, int bool ok; jlInputCharReset(); - memset(gKeyState, 0, sizeof(gKeyState)); + memset(jlgKeyState, 0, sizeof(jlgKeyState)); gWaitScript = script; gWaitSteps = steps; gWaitAt = 0; gWaitOverrun = false; jlWaitForAnyKey(); gWaitScript = NULL; - ok = !gWaitOverrun && gWaitAt == expect && jlInputGetChar() == -1; + ok = !gWaitOverrun && gWaitAt == expect && jlInputGetChar() == -1; printf(" %-34s %s (ended on poll %d%s, %d expected)\n", name, ok ? "PASS" : "FAIL", gWaitAt, gWaitOverrun ? ", past the script" : "", expect); return ok; } diff --git a/tests/host/millisHost.c b/tests/host/millisHost.c index ffc57e8..a58e209 100644 --- a/tests/host/millisHost.c +++ b/tests/host/millisHost.c @@ -5,17 +5,15 @@ // millisHost.c - Host-cc harness for jlpGenericMillisElapsed (the // IIgs/Amiga millisecond clock). Drives simulated frame-count sequences, // including the 16-bit wrap at 65536 frames, and asserts the millisecond -// clock never runs backward. +// clock never runs backward (claudeDocs/PERF-AUDIT.md finding #23: it once +// jumped back every 65536 frames); and steps it a few frames at a time, +// the per-frame path and past it the divide, checking it exact at every +// step. // -// Built by scripts/check-millis.sh with -DJL_HAS_FRAME_COUNT and -// -DJL_HAS_FRAME_HZ so port.h routes jlpFrameCount/jlpFrameHz to the mock -// implementations below instead of the generic stubs. -// -// TODAY this harness FAILS: jlpGenericMillisElapsed is a pure function of -// the wrapping uint16_t frame count (claudeDocs/PERF-AUDIT.md finding #23), so the -// clock jumps backward every 65536 frames (~18 min at 60Hz). The Phase 1 -// fix makes it accumulate deltas; this harness must then PASS and becomes -// the regression gate for it. +// Built by scripts/check-millis.sh as the blank port, whose platform block +// declares JL_HAS_FRAME_COUNT and JL_HAS_FRAME_HZ, so port.h routes +// jlpFrameCount/jlpFrameHz to the mock implementations below instead of +// the generic stubs. #include <stdint.h> #include <stdbool.h> @@ -28,6 +26,7 @@ static uint16_t gMockHz = 60; // Prototypes (harness-local, alphabetical). static bool runScenario(const char *name, uint16_t hz, const uint32_t *frames, int count); +static bool runSteps(const char *name, uint16_t hz); uint16_t jlpFrameCount(void) { @@ -76,6 +75,38 @@ static bool runScenario(const char *name, uint16_t hz, const uint32_t *frames, i } +// Step the clock by 1 to STEP_DELTAS frames in turn, a few past the +// per-frame path into the divide, and check it at every step against the +// exact elapsed milliseconds: whole at 50 Hz, the thirds carried at 60. +#define STEP_DELTAS 10 +#define STEP_COUNT 2000 +#define STEP_BASE 1000u +static bool runSteps(const char *name, uint16_t hz) { + uint32_t elapsed; + uint32_t ms; + uint32_t expectMs; + bool ok; + int i; + + gMockHz = hz; + gMockFrame = (uint16_t)STEP_BASE; + ok = jlpGenericMillisElapsed() == 0u; + elapsed = 0u; + for (i = 0; i < STEP_COUNT && ok; i++) { + elapsed = elapsed + (uint32_t)(1 + i % STEP_DELTAS); + gMockFrame = (uint16_t)(STEP_BASE + elapsed); + ms = jlpGenericMillisElapsed(); + expectMs = (uint32_t)(((uint64_t)elapsed * 1000u) / hz); + if (ms != expectMs) { + printf(" %s: step %d (%lu frames): %lu ms, expected %lu ms\n", name, i, (unsigned long)elapsed, (unsigned long)ms, (unsigned long)expectMs); + ok = false; + } + } + printf(" %s: %s\n", name, ok ? "PASS" : "FAIL"); + return ok; +} + + int main(void) { // Sequences are ABSOLUTE frame counts; steps stay under 65535 frames // apart (the documented poll-at-least-once-per-wrap requirement). @@ -92,8 +123,10 @@ int main(void) { ok &= runScenario("simple-60hz", 60, simple60, (int)(sizeof(simple60) / sizeof(simple60[0]))); ok &= runScenario("wrap-50hz", 50, wrap50, (int)(sizeof(wrap50) / sizeof(wrap50[0]))); ok &= runScenario("wrap-60hz", 60, wrap60, (int)(sizeof(wrap60) / sizeof(wrap60[0]))); + ok &= runSteps("steps-50hz", 50); + ok &= runSteps("steps-60hz", 60); if (!ok) { - printf("millisHost: FAIL (finding #23 wrap bug present -- expected until Phase 1 lands)\n"); + printf("millisHost: FAIL\n"); return 1; } printf("millisHost: PASS\n"); diff --git a/tests/host/modPlayerHost.c b/tests/host/modPlayerHost.c index 8031732..14114da 100644 --- a/tests/host/modPlayerHost.c +++ b/tests/host/modPlayerHost.c @@ -153,16 +153,16 @@ static void modNote(uint8_t pattern, uint8_t row, uint8_t ch, uint8_t sample, ui static uint32_t ticksUntilEnd(const uint8_t *module, uint32_t bytes, bool loop) { uint32_t n; - if (!modPlayerStart(module, bytes, TEST_RATE, loop)) { + if (!jlModPlayerStart(module, bytes, TEST_RATE, loop)) { return 0u; } for (n = 1u; n <= TICK_LIMIT; n++) { if (!tick()) { - modPlayerStop(); + jlModPlayerStop(); return n; } } - modPlayerStop(); + jlModPlayerStop(); return 0u; } @@ -189,7 +189,7 @@ static void testEnds(void) { snprintf(detail, sizeof(detail), "B00 on position 1 ends it on tick %u (expected %u)", n, LEAD_TICKS + MOD_ROWS + 2u); check("ends at a jump back", n == LEAD_TICKS + MOD_ROWS + 2u, detail); // Marked, the jump goes to the marker, whose E8F ends it on the same tick. - marked = modStopMarkerAdd(gMod, sizeof(gMod), gMarked); + marked = jlModStopMarkerAdd(gMod, sizeof(gMod), gMarked); n = marked ? ticksUntilEnd(gMarked, sizeof(gMarked), false) : 0u; snprintf(detail, sizeof(detail), "marked, it ends on tick %u (expected %u)", n, LEAD_TICKS + MOD_ROWS + 2u); check("stop marker", n == LEAD_TICKS + MOD_ROWS + 2u, detail); @@ -247,21 +247,21 @@ static void testFuzz(void) { gMod[MOD_ORDER_TABLE_OFFSET + i] = (uint8_t)(gMod[MOD_ORDER_TABLE_OFFSET + i] % TEST_PATTERNS); } // Lengths, loops and the file's own length cut short as they come. - if (modPlayerStart(gMod, sizeof(gMod) - (run % 2u) * (RAMP_BYTES / 2u), TEST_RATE, (run & 2u) != 0u)) { + if (jlModPlayerStart(gMod, sizeof(gMod) - (run % 2u) * (RAMP_BYTES / 2u), TEST_RATE, (run & 2u) != 0u)) { started++; - (void)modPlayerRenderU8(gRenderA, FUZZ_SAMPLES, RENDER_MONO); - modPlayerStop(); + (void)jlModPlayerRenderU8(gRenderA, FUZZ_SAMPLES, RENDER_MONO); + jlModPlayerStop(); } - if (modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true)) { - (void)modPlayerRenderS16(gRenderS16, FUZZ_SAMPLES / S16_STEREO, (uint8_t)S16_STEREO); - modPlayerStop(); + if (jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true)) { + (void)jlModPlayerRenderS16(gRenderS16, FUZZ_SAMPLES / S16_STEREO, (uint8_t)S16_STEREO); + jlModPlayerStop(); } } check("fuzz", started > 0u, "mangled modules render without leaving the player's copy"); - check("refuses a short module", !modPlayerStart(gMod, MOD_HEADER_BYTES - 1u, TEST_RATE, false), "fewer bytes than a header"); + check("refuses a short module", !jlModPlayerStart(gMod, MOD_HEADER_BYTES - 1u, TEST_RATE, false), "fewer bytes than a header"); modNew(1u); memcpy(gMod + MOD_MAGIC_OFFSET, "8CHN", MOD_MAGIC_LEN); - check("refuses eight channels", !modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false), "an 8CHN module"); + check("refuses eight channels", !jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false), "an 8CHN module"); } @@ -278,7 +278,7 @@ static void testProtracker(void) { modNew(1u); modNote(0u, 0u, 0u, SAMPLE_RAMP, PERIOD_C2, 0x9u, 0x01u); modNote(0u, 1u, 0u, 0u, PERIOD_C2, 0x0u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i <= LEAD_TICKS; i++) { (void)tick(); } @@ -292,7 +292,7 @@ static void testProtracker(void) { modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x4u, 0x48u); modNote(0u, 1u, 0u, 0u, 0u, 0x6u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED; i++) { (void)tick(); } @@ -306,7 +306,7 @@ static void testProtracker(void) { // 0xy past B-3 reads on into the next finetune's periods. modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_B3, 0x0u, 0x0Cu); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i <= LEAD_TICKS + 2u; i++) { (void)tick(); } @@ -317,7 +317,7 @@ static void testProtracker(void) { modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xCu, 0x20u); modNote(0u, 1u, 0u, 0u, 0u, 0x7u, 0x84u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED + 2u; i++) { (void)tick(); } @@ -332,7 +332,7 @@ static void testProtracker(void) { modNote(0u, 1u, 0u, 0u, PERIOD_C3, 0x3u, 0x08u); modNote(0u, 2u, 0u, 0u, 0u, 0xEu, 0x31u); modNote(0u, 3u, 0u, 0u, 0u, 0x3u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED * 3u; i++) { (void)tick(); } @@ -351,7 +351,7 @@ static void testProtracker(void) { for (i = 0u; i < 3u; i++) { modNote(0u, (uint8_t)i, 3u, SAMPLE_SQUARE, kPeriods[0][i], 0x0u, 0x00u); } - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i < LEAD_TICKS; i++) { (void)tick(); } @@ -363,7 +363,7 @@ static void testProtracker(void) { check("pattern loop", ok, "rows 0, 1, 0, 1, 0, 1, 2"); modNote(0u, 0u, 1u, 0u, 0u, 0xEu, 0xE2u); modNote(0u, 1u, 1u, 0u, 0u, 0x0u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i < LEAD_TICKS; i++) { (void)tick(); } @@ -378,7 +378,7 @@ static void testProtracker(void) { modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); modNote(0u, 1u, 0u, SAMPLE_RAMP, 0u, 0x0u, 0x00u); modNote(0u, 2u, 1u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED; i++) { (void)tick(); } @@ -388,11 +388,11 @@ static void testProtracker(void) { } ok = ok && a->latch == NULL; check("sample swap", ok, "the volume at once, the loop at the next start of a sample"); - modPlayerStop(); + jlModPlayerStop(); } -// The half unit (modPlayerSetU8Shift): one channel at full volume swings half +// The half unit (jlModPlayerSetU8Shift): one channel at full volume swings half // the range; four in phase pass a byte, so the mono render clamps them where // adding them straight would wrap, and a stereo side's two still fit. static void testRenderHalf(void) { @@ -402,11 +402,11 @@ static void testRenderHalf(void) { uint8_t high; bool ok; - modPlayerSetU8Shift(U8_SHIFT_HALF); + jlModPlayerSetU8Shift(U8_SHIFT_HALF); modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); low = 0xFFu; high = 0u; for (i = 0u; i < TEST_RATE; i++) { @@ -418,21 +418,21 @@ static void testRenderHalf(void) { for (i = 0u; i < MOD_CHANNELS; i++) { modNote(0u, 0u, (uint8_t)i, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); } - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); ok = true; for (i = 0u; i < TEST_RATE; i++) { ok = ok && (gRenderA[i] == 0u || gRenderA[i] == U8_BIAS || gRenderA[i] == UINT8_MAX); } check("8-bit clamp, half", ok, "four channels in phase clamp to 0 and 255 in mono, never wrap"); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderU8(gRenderB, TEST_RATE / S16_STEREO, (uint8_t)S16_STEREO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderU8(gRenderB, TEST_RATE / S16_STEREO, (uint8_t)S16_STEREO); ok = true; for (i = 0u; i < TEST_RATE; i++) { ok = ok && (gRenderB[i] == 0u || gRenderB[i] == U8_BIAS || gRenderB[i] == U8_BIAS + 2u * (SQUARE_HIGH >> U8_SHIFT_HALF)); } check("8-bit sides, half", ok, "two channels in phase fill a stereo side, 0 to 254"); - modPlayerSetU8Shift(U8_SHIFT_QUARTER); + jlModPlayerSetU8Shift(U8_SHIFT_QUARTER); } @@ -451,8 +451,8 @@ static void testRender(void) { // One channel at full volume: a quarter of the 8-bit range each way. modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); low = 0xFFu; high = 0u; for (i = 0u; i < TEST_RATE; i++) { @@ -462,8 +462,8 @@ static void testRender(void) { snprintf(detail, sizeof(detail), "a full-volume square swings %u..%u (expected %u..%u)", low, high, U8_BIAS + (SQUARE_LOW >> U8_SHIFT_QUARTER), U8_BIAS + (SQUARE_HIGH >> U8_SHIFT_QUARTER)); check("8-bit level", low == U8_BIAS + (SQUARE_LOW >> U8_SHIFT_QUARTER) && high == U8_BIAS + (SQUARE_HIGH >> U8_SHIFT_QUARTER), detail); // Channel 0 is on the left alone, channel 1 on the right. - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderS16(gRenderS16, TEST_RATE, (uint8_t)S16_STEREO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderS16(gRenderS16, TEST_RATE, (uint8_t)S16_STEREO); ok = true; for (i = 0u; i < TEST_RATE; i++) { ok = ok && gRenderS16[i * S16_STEREO + 1u] == 0; @@ -478,10 +478,10 @@ static void testRender(void) { for (i = 0u; i < MOD_CHANNELS; i++) { modNote(0u, 0u, (uint8_t)i, (uint8_t)(i % 2u ? SAMPLE_RAMP : SAMPLE_SQUARE), kPeriods[0][i * 5u], (uint8_t)(i == 2u ? 0x4u : 0x0u), (uint8_t)(i == 2u ? 0x46u : 0x00u)); } - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderU8(gRenderA, RENDER_SAMPLES, RENDER_MONO); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); - (void)modPlayerRenderS16(gRenderS16, RENDER_SAMPLES, RENDER_MONO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderU8(gRenderA, RENDER_SAMPLES, RENDER_MONO); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerRenderS16(gRenderS16, RENDER_SAMPLES, RENDER_MONO); dot = 0.0; xx = 0.0; yy = 0.0; @@ -495,9 +495,9 @@ static void testRender(void) { // The same stream whatever the calls' sizes: 1, 7, then 1000 at a time. ok = true; for (run = 1u; run <= 1000u; run = run == 1u ? 7u : 1000u) { - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); for (done = 0u; done < RENDER_SAMPLES; done += run) { - (void)modPlayerRenderU8(gRenderB + done, done + run <= RENDER_SAMPLES ? run : RENDER_SAMPLES - done, RENDER_MONO); + (void)jlModPlayerRenderU8(gRenderB + done, done + run <= RENDER_SAMPLES ? run : RENDER_SAMPLES - done, RENDER_MONO); } ok = ok && memcmp(gRenderA, gRenderB, RENDER_SAMPLES) == 0; if (run == 1000u) { @@ -508,8 +508,8 @@ static void testRender(void) { // A song that ends: false from the call it ends in, silence after. modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u); - (void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); - ok = !modPlayerRenderU8(gRenderA, RENDER_SAMPLES, RENDER_MONO) && !modPlayerActive(); + (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); + ok = !jlModPlayerRenderU8(gRenderA, RENDER_SAMPLES, RENDER_MONO) && !jlModPlayerActive(); for (i = RENDER_SAMPLES - TEST_RATE; i < RENDER_SAMPLES; i++) { ok = ok && gRenderA[i] == MOD_PLAYER_SILENCE_U8; } diff --git a/tests/host/serialHost.c b/tests/host/serialHost.c index ec2ce89..c64c152 100644 --- a/tests/host/serialHost.c +++ b/tests/host/serialHost.c @@ -41,7 +41,7 @@ static bool gMockRts = true; // last RTS level the core hand static uint16_t gMockRtsCalls = 0u; static bool gMockDtr = true; // last DTR level the core handed the HAL static uint16_t gMockDtrCalls = 0u; -static uint32_t gMockMs = 0u; // the clock serialTxStalled reads +static uint32_t gMockMs = 0u; // the clock jlSerialTxStalled reads // ----- Mock HAL (satisfies the jlpSerial* dispatch under -DJL_HAS_SERIAL_*) ----- @@ -260,15 +260,15 @@ int main(void) { check("open: closed after HAL failure", jlSerialWriteByte('x') == false); gMockOpenResult = true; - // 10. A busy transmitter's wait (serialTxStalled): a clock that does not + // 10. A busy transmitter's wait (jlSerialTxStalled): a clock that does not // run ends it at the poll limit, a running one 100 ms on. gMockMs = 0u; - serialTxWaitStart(&wait, STALL_POLL_LIMIT); - for (polls = 1u; !serialTxStalled(&wait); polls++) { + jlSerialTxWaitStart(&wait, STALL_POLL_LIMIT); + for (polls = 1u; !jlSerialTxStalled(&wait); polls++) { } check("tx stall: a stopped clock ends the wait at the poll limit", polls == STALL_POLL_LIMIT); - serialTxWaitStart(&wait, UINT32_MAX); - for (polls = 1u; !serialTxStalled(&wait); polls++) { + jlSerialTxWaitStart(&wait, UINT32_MAX); + for (polls = 1u; !jlSerialTxStalled(&wait); polls++) { gMockMs++; } check("tx stall: a running clock ends it 100 ms after its first read", polls == STALL_POLLS_TIMED); diff --git a/tests/host/sfxMixHost.c b/tests/host/sfxMixHost.c index bed96dd..7dac030 100644 --- a/tests/host/sfxMixHost.c +++ b/tests/host/sfxMixHost.c @@ -169,8 +169,8 @@ static void runFixedScenario(const char *name, uint16_t rateHz, uint32_t outputR genSignedSamples(&state, gSampleA, sampleCount, amp); dstFill(gLongOut, mixLen, dstSeed); memset(&slot, 0, sizeof(slot)); - audioSfxSlotArm(&slot, gSampleA, sampleCount, rateHz, outputRate); - audioSfxOverlayMix(gLongOut, mixLen, &slot, 1u); + jlAudioSfxSlotArm(&slot, gSampleA, sampleCount, rateHz, outputRate); + jlAudioSfxOverlayMix(gLongOut, mixLen, &slot, 1u); reportScenario(name, 1u, gLongOut, mixLen); } @@ -201,7 +201,7 @@ int main(void) { uint32_t state; uint32_t block; - printf("sfxMixHost: audioSfxOverlayMix scenarios (LCG-seeded, deterministic)\n"); + printf("sfxMixHost: jlAudioSfxOverlayMix scenarios (LCG-seeded, deterministic)\n"); // --- Fixed-buffer slots, one slot per mix. --- // Rate conversion up: 8363 Hz source into an 11025 Hz mix @@ -239,11 +239,11 @@ int main(void) { state = 0x5EED0009u; genSignedSamples(&state, gSampleC, 2048u, 127); memset(slots, 0, sizeof(slots)); - audioSfxSlotArm(&slots[0], gSampleA, 2048u, 5000u, OUT_RATE); - audioSfxSlotArm(&slots[1], gSampleB, 900u, 11025u, OUT_RATE); - audioSfxSlotArm(&slots[2], gSampleC, 2048u, 18000u, OUT_RATE); + jlAudioSfxSlotArm(&slots[0], gSampleA, 2048u, 5000u, OUT_RATE); + jlAudioSfxSlotArm(&slots[1], gSampleB, 900u, 11025u, OUT_RATE); + jlAudioSfxSlotArm(&slots[2], gSampleC, 2048u, 18000u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B0007u); - audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 3u); + jlAudioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 3u); reportScenario("multiSlot3", 3u, gDst, MIX_LEN); // --- Streaming slots. --- @@ -254,9 +254,9 @@ int main(void) { ctx.remaining = 4000u; ctx.chunkCap = 200u; ctx.amp = 100; - audioSfxSlotArmStream(&slot, streamFill, &ctx, 8363u, OUT_RATE); + jlAudioSfxSlotArmStream(&slot, streamFill, &ctx, 8363u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B0008u); - audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u); + jlAudioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u); reportScenario("streamRefillMid", 1u, gDst, MIX_LEN); // Rate-down stream (step 2.0) that ends mid-mix: 500 source @@ -267,9 +267,9 @@ int main(void) { ctx.remaining = 500u; ctx.chunkCap = 255u; ctx.amp = 110; - audioSfxSlotArmStream(&slot, streamFill, &ctx, 22050u, OUT_RATE); + jlAudioSfxSlotArmStream(&slot, streamFill, &ctx, 22050u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B0009u); - audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u); + jlAudioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u); reportScenario("streamEndMid", 1u, gDst, MIX_LEN); // Fixed + streaming slot in the same mix call (the isStream @@ -277,14 +277,14 @@ int main(void) { state = 0x5EED000Au; genSignedSamples(&state, gSampleA, 2048u, 70); memset(slots, 0, sizeof(slots)); - audioSfxSlotArm(&slots[0], gSampleA, 2048u, 6000u, OUT_RATE); + jlAudioSfxSlotArm(&slots[0], gSampleA, 2048u, 6000u, OUT_RATE); ctx.lcgState = 0x5712EA43u; ctx.remaining = 3000u; ctx.chunkCap = 180u; ctx.amp = 95; - audioSfxSlotArmStream(&slots[1], streamFill, &ctx, 16000u, OUT_RATE); + jlAudioSfxSlotArmStream(&slots[1], streamFill, &ctx, 16000u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B000Au); - audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 2u); + jlAudioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 2u); reportScenario("mixedFixedStream", 2u, gDst, MIX_LEN); // --- Long-position fixed slot (finding #7 headroom). --- @@ -295,10 +295,10 @@ int main(void) { state = 0x5EED000Bu; genSignedSamples(&state, gLongSample, LONG_SAMPLE_LEN, 90); memset(&slot, 0, sizeof(slot)); - audioSfxSlotArm(&slot, gLongSample, LONG_SAMPLE_LEN, LONG_RATE_HZ, LONG_OUT_RATE); + jlAudioSfxSlotArm(&slot, gLongSample, LONG_SAMPLE_LEN, LONG_RATE_HZ, LONG_OUT_RATE); for (block = 0; block < LONG_BLOCKS; block++) { dstFill(gLongOut + (block * MIX_LEN), MIX_LEN, 0xD57B0100u + block); - audioSfxOverlayMix(gLongOut + (block * MIX_LEN), (uint16_t)MIX_LEN, &slot, 1u); + jlAudioSfxOverlayMix(gLongOut + (block * MIX_LEN), (uint16_t)MIX_LEN, &slot, 1u); } reportScenario("fixedLongPos64k", 1u, gLongOut, LONG_BLOCKS * MIX_LEN); diff --git a/tests/host/tileLayerHost.c b/tests/host/tileLayerHost.c new file mode 100644 index 0000000..04e35c7 --- /dev/null +++ b/tests/host/tileLayerHost.c @@ -0,0 +1,240 @@ +// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +// See LICENSE.md for license information. +// SPDX-License-Identifier: MIT +// +// tileLayerHost.c - Host-cc harness for tile layers (src/core/tileLayer.c +// and the generic ring, src/generic/genericTileLayer.c). Every draw is +// checked pixel for pixel against the map itself -- the tile a band pixel +// falls in, worked out from the origin with no ring -- so the ring's +// bookkeeping (which slots a move uncovers, wraps at the map's and the +// ring's edges) cannot hide behind a second implementation. Runs of moves +// of every size (a pixel, a tile, a view, a jump across the map's seam, +// both ways), maps smaller than the view, random bands, and cells changed +// with jlTileLayerSetTile, in and out of view. Also: rows outside the band +// keep their pixels, the band's rows are marked dirty full width, a +// JL_TILE_MAP_SKIP cell draws colour 0, and jlTileLayerCreate refuses bad +// arguments. +// +// Built by scripts/check-tilelayer.sh with -DJOEYLIB_PLATFORM_BLANK. + +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "joey/joey.h" +#include "surfaceInternal.h" + +#define TILE_COUNT 64u +#define RUNS 300u +#define STEPS_PER_RUN 40u +#define OUTSIDE_COLOR 9u +#define MAX_MAP_SIDE 300u + +static jlTileT gTiles[TILE_COUNT]; +static uint8_t gMap[MAX_MAP_SIDE * MAX_MAP_SIDE]; +static uint32_t gChecks; + +static bool checkBand(jlSurfaceT *stage, uint8_t viewBy, uint8_t viewHeight, uint16_t mapW, uint16_t mapH, uint32_t x, uint32_t y, const char *what); +static bool checkCreateRefusals(void); +static uint8_t expectedPixel(uint16_t mapW, uint16_t mapH, uint32_t mx, uint32_t my); +static uint32_t nextRand(void); +static bool runOne(jlSurfaceT *stage, uint32_t run); +static uint16_t stepFor(uint32_t mapPx); + +static uint32_t gSeed = 12345u; + + +static bool checkBand(jlSurfaceT *stage, uint8_t viewBy, uint8_t viewHeight, uint16_t mapW, uint16_t mapH, uint32_t x, uint32_t y, const char *what) { + int16_t px; + int16_t py; + int16_t top; + int16_t bottom; + uint32_t mapPxW; + uint32_t mapPxH; + uint8_t got; + uint8_t want; + + mapPxW = (uint32_t)mapW * 8u; + mapPxH = (uint32_t)mapH * 8u; + top = (int16_t)(viewBy * 8); + bottom = (int16_t)((viewBy + viewHeight) * 8); + gChecks++; + for (py = 0; py < JL_SURFACE_HEIGHT; py++) { + bool inBand = (py >= top && py < bottom); + + if (inBand && (jlgStageMinWord[py] != 0u || jlgStageMaxWord[py] != STAGE_DIRTY_FULL_MAX)) { + printf(" FAIL %s: band row %d not marked dirty full width (%u..%u)\n", what, py, jlgStageMinWord[py], jlgStageMaxWord[py]); + return false; + } + for (px = 0; px < JL_SURFACE_WIDTH; px++) { + got = jlSamplePixel(stage, px, py); + want = inBand ? expectedPixel(mapW, mapH, (x + (uint32_t)px) % mapPxW, (y + (uint32_t)(py - top)) % mapPxH) : OUTSIDE_COLOR; + if (got != want) { + printf(" FAIL %s: pixel (%d,%d) is %u, want %u (origin %lu,%lu map %ux%u band %u+%u)\n", what, px, py, got, want, (unsigned long)x, (unsigned long)y, mapW, mapH, viewBy, viewHeight); + return false; + } + } + } + return true; +} + + +static bool checkCreateRefusals(void) { + bool ok = true; + + ok = ok && jlTileLayerCreate(NULL, gMap, 10u, 10u, 0u, 25u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, NULL, 10u, 10u, 0u, 25u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, gMap, 0u, 10u, 0u, 25u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, 0u, 0u, 25u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, gMap, (uint16_t)(JL_TILE_LAYER_MAX_SIDE + 1u), 10u, 0u, 25u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, (uint16_t)(JL_TILE_LAYER_MAX_SIDE + 1u), 0u, 25u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, 10u, 0u, 0u) == NULL; + ok = ok && jlTileLayerCreate(gTiles, gMap, 10u, 10u, 1u, 25u) == NULL; + jlTileLayerDestroy(NULL); + printf(" create refusals: %s\n", ok ? "PASS" : "FAIL"); + return ok; +} + + +// The colour of map pixel (mx, my), straight from the map and the tiles. +static uint8_t expectedPixel(uint16_t mapW, uint16_t mapH, uint32_t mx, uint32_t my) { + uint8_t idx; + uint8_t b; + + (void)mapH; + idx = gMap[(my >> 3) * mapW + (mx >> 3)]; + if (idx == JL_TILE_MAP_SKIP) { + return 0u; + } + b = gTiles[idx].pixels[(my & 7u) * JL_TILE_BYTES_PER_ROW + ((mx & 7u) >> 1)]; + return (uint8_t)(((mx & 1u) == 0u) ? (b >> 4) : (b & 0x0Fu)); +} + + +static uint32_t nextRand(void) { + gSeed = gSeed * 1103515245u + 12345u; + return (gSeed >> 8) & 0xFFFFFFu; +} + + +static bool runOne(jlSurfaceT *stage, uint32_t run) { + jlTileLayerT *layer; + uint16_t mapW; + uint16_t mapH; + uint8_t viewBy; + uint8_t viewHeight; + uint32_t x; + uint32_t y; + uint32_t i; + uint32_t step; + char what[64]; + + // Every fifth run a map smaller than the view, the rest up to MAX_MAP_SIDE. + if (run % 5u == 0u) { + mapW = (uint16_t)(1u + nextRand() % 12u); + mapH = (uint16_t)(1u + nextRand() % 8u); + } else { + mapW = (uint16_t)(1u + nextRand() % MAX_MAP_SIDE); + mapH = (uint16_t)(1u + nextRand() % MAX_MAP_SIDE); + } + for (i = 0u; i < (uint32_t)mapW * mapH; i++) { + gMap[i] = (nextRand() % 17u == 0u) ? JL_TILE_MAP_SKIP : (uint8_t)(nextRand() % TILE_COUNT); + } + viewHeight = (uint8_t)((run % 3u == 0u) ? JL_TILE_BLOCKS_PER_COL : 1u + nextRand() % JL_TILE_BLOCKS_PER_COL); + viewBy = (uint8_t)(nextRand() % (uint32_t)(JL_TILE_BLOCKS_PER_COL - viewHeight + 1u)); + layer = jlTileLayerCreate(gTiles, gMap, mapW, mapH, viewBy, viewHeight); + if (layer == NULL) { + printf(" FAIL run %lu: create %ux%u band %u+%u\n", (unsigned long)run, mapW, mapH, viewBy, viewHeight); + return false; + } + jlFillRect(stage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, OUTSIDE_COLOR); + x = nextRand() % ((uint32_t)mapW * 8u); + y = nextRand() % ((uint32_t)mapH * 8u); + for (step = 0u; step < STEPS_PER_RUN; step++) { + uint32_t kind = nextRand() % 10u; + + if (kind < 7u) { + // A move: dx, dy each of stepFor's sizes, either way. + uint32_t pxW = (uint32_t)mapW * 8u; + uint32_t pxH = (uint32_t)mapH * 8u; + + x = (x + ((nextRand() & 1u) ? stepFor(pxW) : pxW - stepFor(pxW) % pxW)) % pxW; + y = (y + ((nextRand() & 1u) ? stepFor(pxH) : pxH - stepFor(pxH) % pxH)) % pxH; + } else if (kind < 9u) { + // A cell change, in view half the time. + uint16_t tx = (uint16_t)((nextRand() & 1u) ? ((x >> 3) + nextRand() % 41u) % mapW : nextRand() % mapW); + uint16_t ty = (uint16_t)((nextRand() & 1u) ? ((y >> 3) + nextRand() % 26u) % mapH : nextRand() % mapH); + + jlTileLayerSetTile(layer, tx, ty, (nextRand() % 9u == 0u) ? JL_TILE_MAP_SKIP : (uint8_t)(nextRand() % TILE_COUNT)); + } + // jlStagePresent clears the dirty marks, so the check sees only the + // draw's. + jlStagePresent(); + // Sometimes an origin past the map's size: the layer takes it modulo. + if (nextRand() % 6u == 0u) { + jlTileLayerDraw(stage, layer, (uint16_t)(x + (uint32_t)mapW * 8u * (nextRand() % 2u)), (uint16_t)(y + (uint32_t)mapH * 8u * (nextRand() % 2u))); + } else { + jlTileLayerDraw(stage, layer, (uint16_t)x, (uint16_t)y); + } + snprintf(what, sizeof(what), "run %lu step %lu", (unsigned long)run, (unsigned long)step); + if (!checkBand(stage, viewBy, viewHeight, mapW, mapH, x, y, what)) { + jlTileLayerDestroy(layer); + return false; + } + } + jlTileLayerDestroy(layer); + return true; +} + + +// A move size: a pixel or a few, about a tile, about a view, or anywhere. +static uint16_t stepFor(uint32_t mapPx) { + uint32_t pick = nextRand() % 8u; + uint32_t s; + + if (pick < 3u) { + s = nextRand() % 4u; + } else if (pick < 5u) { + s = 6u + nextRand() % 12u; + } else if (pick < 7u) { + s = 300u + nextRand() % 60u; + } else { + s = nextRand(); + } + return (uint16_t)(s % mapPx); +} + + +int main(void) { + jlConfigT config; + jlSurfaceT *stage; + uint32_t i; + uint32_t run; + bool ok; + + config.codegenBytes = 0u; + config.noAudio = true; + if (!jlInit(&config)) { + printf("tileLayerHost: FAIL -- jlInit: %s\n", jlLastError()); + return 1; + } + stage = jlStageGet(); + for (i = 0u; i < TILE_COUNT; i++) { + uint32_t b; + + for (b = 0u; b < JL_TILE_BYTES; b++) { + gTiles[i].pixels[b] = (uint8_t)nextRand(); + } + } + printf("tileLayerHost: layers against the map, pixel for pixel\n"); + ok = checkCreateRefusals(); + for (run = 0u; run < RUNS && ok; run++) { + ok = runOne(stage, run); + } + printf(" %lu draws checked over %lu runs: %s\n", (unsigned long)gChecks, (unsigned long)run, ok ? "PASS" : "FAIL"); + jlShutdown(); + printf("tileLayerHost: %s\n", ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} diff --git a/tests/host/tiledCheck.py b/tests/host/tiledCheck.py new file mode 100755 index 0000000..5364f06 --- /dev/null +++ b/tests/host/tiledCheck.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT + +"""tiledCheck.py REPO WORK - checks tools/tiled2joey.py (scripts/check-tiled.sh). + +One 4x3 map, written as TMX (tileset A embedded, B external TSX, layers in +csv and base64+zlib) and as TMJ (A embedded, B external TSJ, csv and +base64+gzip). Tileset A's four tiles and tileset B's two (with a margin and +spacing) each hold marker pixels, so every orientation Tiled can ask for +lands its markers in a known place. Both maps must give the same sheet and +maps, and those must be what is worked out here by hand: the tiles in the +order they first appear, flips baked, A's colours at their own indices and +B's new one in the first free slot, empty cells 0xFF. The sheet must bake +as a tile bank, and a map with 16x16 tiles or an RGB tileset must be refused. +Prints a line a check; exits 1 if any fails. +""" + +import base64 +import gzip +import json +import os +import struct +import subprocess +import sys +import zlib + +from PIL import Image + +BLACK = (0, 0, 0) +RED = (255, 0, 0) +GREEN = (0, 255, 0) +BLUE = (0, 0, 255) +YELLOW = (255, 255, 0) +H = 0x80000000 +V = 0x40000000 +D = 0x20000000 +SKIP = 0xFF +SIDE = 8 + +# Tileset A: four tiles in a row, markers as (x, y, palette index). +A_TILES = [ + [(1, 2, 1)], + [(1, 2, 2)], + [(1, 2, 3)], + [(1, 2, 1), (6, 6, 2)], +] +A_PALETTE = [BLACK, RED, GREEN, BLUE] +# Tileset B: two tiles, margin 1, spacing 1; its blue is A's, yellow new. +B_TILES = [ + [(0, 7, 2)], + [(7, 0, 1)], +] +B_PALETTE = [BLACK, BLUE, YELLOW] +B_MARGIN = 1 +B_SPACING = 1 + +WIDTH = 4 +HEIGHT = 3 +A_FIRST = 1 +B_FIRST = 5 +GROUND = [ + A_FIRST, A_FIRST | H, A_FIRST | V, A_FIRST | D, + A_FIRST | D | H, A_FIRST + 1, 0, A_FIRST + 2, + A_FIRST + 3, (A_FIRST + 1) | H | V, 0, 0, +] +TOP = [ + 0, 0, 0, 0, + B_FIRST, 0, B_FIRST + 1, 0, + 0, (B_FIRST + 1) | H, 0, 0, +] +TOP_NAME = "Top Layer!" + +# What the sheet must hold, worked out by hand: each tile's markers as +# (x, y, sheet palette index), in first-appearance order. A's colours keep +# their indices (red 1, green 2, blue 3); B's yellow takes slot 4. +EXPECT_TILES = [ + [(1, 2, 1)], # A0 + [(6, 2, 1)], # A0 flipped across + [(1, 5, 1)], # A0 flipped down + [(2, 1, 1)], # A0 x and y swapped + [(5, 1, 1)], # A0 swapped, then across + [(1, 2, 2)], # A1 + [(1, 2, 3)], # A2 + [(1, 2, 1), (6, 6, 2)], # A3 + [(6, 5, 2)], # A1 across and down + [(0, 7, 4)], # B0 (yellow) + [(7, 0, 3)], # B1 (blue) + [(0, 0, 3)], # B1 across +] +EXPECT_PALETTE = [BLACK, RED, GREEN, BLUE, YELLOW] +EXPECT_GROUND = bytes([0, 1, 2, 3, 4, 5, SKIP, 6, 7, 8, SKIP, SKIP]) +EXPECT_TOP = bytes([SKIP, SKIP, SKIP, SKIP, 9, SKIP, 10, SKIP, SKIP, 11, SKIP, SKIP]) +TOP_FILE = "Top_Layer.map" +TBK_COUNT_OFFSET = 6 + +failures = 0 + + +def check(name, ok, detail=""): + global failures + print("TILED: %-28s %s%s" % (name, "PASS" if ok else "FAIL", " (" + detail + ")" if detail else "")) + if not ok: + failures += 1 + + +def indexedImage(path, palette, tiles, margin, spacing): + width = 2 * margin + len(tiles) * SIDE + (len(tiles) - 1) * spacing + image = Image.new("P", (width, 2 * margin + SIDE)) + flat = [c for rgb in palette for c in rgb] + image.putpalette(flat + [0] * (3 * 256 - len(flat))) + pixels = image.load() + for k, markers in enumerate(tiles): + for x, y, index in markers: + pixels[margin + k * (SIDE + spacing) + x, margin + y] = index + image.save(path) + + +def tsx(name, image, count, margin, spacing): + return ('<tileset version="1.10" name="%s" tilewidth="8" tileheight="8" spacing="%d" margin="%d" tilecount="%d" columns="%d">\n' + ' <image source="%s"/>\n</tileset>\n' % (name, spacing, margin, count, count, image)) + + +def tilesetJson(name, image, count, margin, spacing): + return {"name": name, "tilewidth": 8, "tileheight": 8, "spacing": spacing, "margin": margin, "tilecount": count, "columns": count, "image": image} + + +def words(gids): + return struct.pack("<%dI" % len(gids), *gids) + + +def writeMaps(work): + indexedImage(os.path.join(work, "a.png"), A_PALETTE, A_TILES, 0, 0) + indexedImage(os.path.join(work, "b.png"), B_PALETTE, B_TILES, B_MARGIN, B_SPACING) + with open(os.path.join(work, "b.tsx"), "w") as f: + f.write('<?xml version="1.0" encoding="UTF-8"?>\n' + tsx("b", "b.png", len(B_TILES), B_MARGIN, B_SPACING)) + with open(os.path.join(work, "b.tsj"), "w") as f: + json.dump(tilesetJson("b", "b.png", len(B_TILES), B_MARGIN, B_SPACING), f) + tmx = ('<?xml version="1.0" encoding="UTF-8"?>\n' + '<map version="1.10" orientation="orthogonal" renderorder="right-down" width="%d" height="%d" tilewidth="8" tileheight="8" infinite="0">\n' + ' <tileset firstgid="%d" name="a" tilewidth="8" tileheight="8" tilecount="%d" columns="%d">\n <image source="a.png"/>\n </tileset>\n' + ' <tileset firstgid="%d" source="b.tsx"/>\n' + ' <layer id="1" name="ground" width="%d" height="%d">\n <data encoding="csv">%s</data>\n </layer>\n' + ' <layer id="2" name="%s" width="%d" height="%d">\n <data encoding="base64" compression="zlib">%s</data>\n </layer>\n' + '</map>\n') % (WIDTH, HEIGHT, A_FIRST, len(A_TILES), len(A_TILES), B_FIRST, WIDTH, HEIGHT, ",".join(str(g) for g in GROUND), + TOP_NAME, WIDTH, HEIGHT, base64.b64encode(zlib.compress(words(TOP))).decode()) + with open(os.path.join(work, "map.tmx"), "w") as f: + f.write(tmx) + tmj = { + "type": "map", "orientation": "orthogonal", "infinite": False, "width": WIDTH, "height": HEIGHT, "tilewidth": 8, "tileheight": 8, + "tilesets": [dict(tilesetJson("a", "a.png", len(A_TILES), 0, 0), firstgid=A_FIRST), {"firstgid": B_FIRST, "source": "b.tsj"}], + "layers": [ + {"type": "tilelayer", "name": "ground", "width": WIDTH, "height": HEIGHT, "data": GROUND}, + {"type": "group", "name": "upper", "layers": [ + {"type": "tilelayer", "name": TOP_NAME, "width": WIDTH, "height": HEIGHT, "encoding": "base64", "compression": "gzip", + "data": base64.b64encode(gzip.compress(words(TOP))).decode()}, + ]}, + ], + } + with open(os.path.join(work, "map.tmj"), "w") as f: + json.dump(tmj, f) + + +def run(repo, args): + return subprocess.run([sys.executable, os.path.join(repo, "tools", "tiled2joey.py")] + args, capture_output=True, text=True) + + +def sheetMatches(path): + image = Image.open(path) + if image.mode != "P" or image.size != (SIDE * len(EXPECT_TILES), SIDE): + return False, "mode %s, size %s" % (image.mode, image.size) + palette = image.getpalette() + for index, rgb in enumerate(EXPECT_PALETTE): + if tuple(palette[index * 3:index * 3 + 3]) != rgb: + return False, "palette index %d is %s, not %s" % (index, tuple(palette[index * 3:index * 3 + 3]), rgb) + pixels = image.load() + for k, markers in enumerate(EXPECT_TILES): + want = {(x, y): index for x, y, index in markers} + for y in range(SIDE): + for x in range(SIDE): + got = pixels[k * SIDE + x, y] + if got != want.get((x, y), 0): + return False, "tile %d pixel (%d,%d) is %d, not %d" % (k, x, y, got, want.get((x, y), 0)) + return True, "%d tiles" % len(EXPECT_TILES) + + +def main(argv): + repo, work = argv[1], argv[2] + writeMaps(work) + outputs = {} + for kind in ("tmx", "tmj"): + out = os.path.join(work, kind) + os.makedirs(out, exist_ok=True) + result = run(repo, ["--header", os.path.join(out, "map.h"), os.path.join(work, "map." + kind), os.path.join(out, "sheet.png"), os.path.join(out, "maps")]) + check("%s converts" % kind, result.returncode == 0, (result.stdout + result.stderr).strip()) + if result.returncode != 0: + continue + ok, detail = sheetMatches(os.path.join(out, "sheet.png")) + check("%s sheet" % kind, ok, detail) + with open(os.path.join(out, "maps", "ground.map"), "rb") as f: + ground = f.read() + check("%s ground layer" % kind, ground == EXPECT_GROUND, ground.hex()) + topPath = os.path.join(out, "maps", TOP_FILE) + top = open(topPath, "rb").read() if os.path.exists(topPath) else b"" + check("%s %s layer" % (kind, TOP_FILE), top == EXPECT_TOP, top.hex()) + with open(os.path.join(out, "map.h")) as f: + header = f.read() + check("%s header" % kind, "MAP_WIDTH 4u" in header and "MAP_HEIGHT 3u" in header and "MAP_TILES 12u" in header and TOP_FILE in header) + outputs[kind] = (list(Image.open(os.path.join(out, "sheet.png")).getdata()), ground, top) + if len(outputs) == 2: + check("tmx and tmj agree", outputs["tmx"] == outputs["tmj"]) + tbk = os.path.join(work, "sheet.tbk") + bake = subprocess.run([sys.executable, os.path.join(repo, "tools", "assetbake", "assetbake.py"), "--type", "tile", "--target", "dos", os.path.join(work, "tmx", "sheet.png"), tbk], capture_output=True, text=True) + count = struct.unpack_from("<H", open(tbk, "rb").read(), TBK_COUNT_OFFSET)[0] if bake.returncode == 0 else -1 + check("sheet bakes as a tile bank", count == len(EXPECT_TILES), "%d tiles" % count) + big = open(os.path.join(work, "map.tmx")).read().replace('tilewidth="8" tileheight="8" infinite', 'tilewidth="16" tileheight="16" infinite') + with open(os.path.join(work, "big.tmx"), "w") as f: + f.write(big) + refused = run(repo, [os.path.join(work, "big.tmx"), os.path.join(work, "big.png"), os.path.join(work, "bigmaps")]) + check("16x16 tiles refused", refused.returncode != 0 and "8x8" in refused.stderr, refused.stderr.strip()) + Image.open(os.path.join(work, "a.png")).convert("RGB").save(os.path.join(work, "a.png")) + refused = run(repo, [os.path.join(work, "map.tmx"), os.path.join(work, "rgb.png"), os.path.join(work, "rgbmaps")]) + check("RGB tileset refused", refused.returncode != 0 and "indexed" in refused.stderr, refused.stderr.strip()) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/iconBake.py b/tools/iconBake.py index 58c3399..dbeb16d 100755 --- a/tools/iconBake.py +++ b/tools/iconBake.py @@ -27,17 +27,25 @@ to its own 256), and a pixel at least half transparent is transparent. tool's icon in its four colours, STACK bytes of stack when started from it - iconBake.py be PICTURE OUT32 OUT16 BeOS and Haiku: 32x32 and + iconBake.py be PICTURE OUT32 OUT16 OUT.hvif + BeOS and Haiku: 32x32 and 16x16 in the BeOS palette, - raw, for xres (ICON, MICN) + raw, for xres (ICON, MICN), + and the 32x32 one as + Haiku's vector icon (VICN) iconBake.py c PICTURE OUT.c SIZE the SDL3 backend's window icon: SIZE x SIZE RGBA as C + iconBake.py icns PICTURE OUT.icns macOS: an app's icon, 16 + pixels up to 512 (or the + picture's size), and twice + that for Retina screens iconBake.py iigs PICTURE OUT NAME the Apple IIgs Finder: an icon file ($CA) giving the program NAME its icons, in the Finder's 16 colours """ +import io import math import struct import sys @@ -91,6 +99,29 @@ PRESENT = 1 BE_LARGE = 32 BE_SMALL = 16 BE_TRANSPARENT = 255 + +# Haiku's vector icon (HVIF, src/libs/icon/flat_icon in Haiku): the 16x16 +# and 32x32 icons' pixels as outlines on its 64-unit grid, a shape a colour, +# hinted so every edge falls on a pixel. The 16x16 shapes show below 32 +# pixels and the 32x32 ones from there up (scale 0.5 is 32 pixels; a level of +# detail byte is HVIF_LOD_UNITS to 1), so each size draws exactly its own +# pixels. A section holds at most 255 of anything; an icon needing more gets +# no vector icon. +HVIF_MAGIC = b"ncif" +HVIF_GRID = 64 +HVIF_STYLE_SOLID_COLOR = 1 +HVIF_PATH_CLOSED = 0x02 +HVIF_PATH_NO_CURVES = 0x08 +HVIF_SHAPE_PATH_SOURCE = 10 +HVIF_SHAPE_HINTING = 0x04 +HVIF_SHAPE_LOD_SCALE = 0x08 +HVIF_LOD_UNITS = 63.75 +HVIF_LOD_SMALL_MAX = 31 # just under scale 0.5, 32 pixels +HVIF_LOD_LARGE_MIN = 31 +HVIF_LOD_LARGE_MAX = 255 # scale 4 +HVIF_COORD_BIAS = 32 +HVIF_MAX_ITEMS = 255 +HVIF_OPAQUE = 255 BE_PALETTE = ( 0, 0, 0, 8, 8, 8, 16, 16, 16, 24, 24, 24, 32, 32, 32, 40, 40, 40, 48, 48, 48, 56, 56, 56, 64, 64, 64, 72, 72, 72, 80, 80, 80, 88, 88, 88, 96, 96, 96, 104, 104, 104, 112, 112, 112, 120, 120, 120, @@ -126,6 +157,19 @@ BE_PALETTE = ( 255, 203, 51, 255, 203, 0, 255, 255, 0, 255, 255, 51, 255, 255, 102, 255, 255, 152, 255, 255, 203, 255, 255, 255, ) +# macOS: an app's icon file (.icns) is "icns" and the file's length, then an +# element for each size: its type, its length (these 8 bytes included) and +# the picture as a PNG. The @2x types are for Retina screens, twice the +# pixels of the size they stand for. Sizes past the first at least as big as +# the picture add nothing macOS's own enlarging would not, so are left out, +# unless it is pixel art, which fit() enlarges sharply. +ICNS_MAGIC = b"icns" +ICNS_HEADER = ">4sI" +ICNS_ELEMENTS = ( + (b"icp4", 16), (b"icp5", 32), (b"ic11", 32), (b"ic12", 64), (b"ic07", 128), + (b"ic13", 256), (b"ic08", 256), (b"ic14", 512), (b"ic09", 512), +) + # The Apple IIgs Finder: an icon file (file type $CA; Apple's File Type Note # $CA, "Finder Icons File") in a volume's Icons folder, holding one icon data # record that gives a GS/OS application (file type $B3) of the program's name @@ -331,12 +375,97 @@ def bakeInfo(picture, out, stack): open(out, "wb").write(header + imageStruct + planes) -def bakeBe(picture, out32, out16): +def bakeBe(picture, out32, out16, outVector): colours = tuple(BE_PALETTE[i:i + 3] for i in range(0, len(BE_PALETTE), 3)) + lods = {BE_SMALL: (0, HVIF_LOD_SMALL_MAX), BE_LARGE: (HVIF_LOD_LARGE_MIN, HVIF_LOD_LARGE_MAX)} + sizes = [] for size, out in ((BE_LARGE, out32), (BE_SMALL, out16)): image = fit(picture, size, size) indices = toColours(image, colours[:BE_TRANSPARENT], False, (0, 0, 0)) - open(out, "wb").write(bytes(i if isOpaque else BE_TRANSPARENT for i, isOpaque in zip(indices, opaque(image)))) + pixels = bytes(i if isOpaque else BE_TRANSPARENT for i, isOpaque in zip(indices, opaque(image))) + open(out, "wb").write(pixels) + sizes.append((size, pixels, colours) + lods[size]) + vector = hvif(sizes) + if vector is None: + sys.stderr.write("iconBake.py: the icon needs more than %d paths or shapes; no vector icon\n" % HVIF_MAX_ITEMS) + vector = b"" + open(outVector, "wb").write(vector) + + +def hvifContours(pixels, size, colour): + """The outlines of colour's pixels in the size x size palette pixels, as + closed loops of corner points: each pixel side with another colour (or + the edge) beyond it, directed so the colour is on its right, chained + head to tail. Outer edges come out clockwise and holes the other way, + so a fill under either rule leaves the holes empty.""" + def at(x, y): + return 0 <= x < size and 0 <= y < size and pixels[y * size + x] == colour + + edges = {} + for y in range(size): + for x in range(size): + if not at(x, y): + continue + if not at(x, y - 1): + edges.setdefault((x, y), []).append((x + 1, y)) + if not at(x + 1, y): + edges.setdefault((x + 1, y), []).append((x + 1, y + 1)) + if not at(x, y + 1): + edges.setdefault((x + 1, y + 1), []).append((x, y + 1)) + if not at(x - 1, y): + edges.setdefault((x, y + 1), []).append((x, y)) + loops = [] + while edges: + first = next(iter(edges)) + loop = [first] + point = first + while True: + ahead = edges[point].pop() + if not edges[point]: + del edges[point] + if ahead == first: + break + loop.append(ahead) + point = ahead + # The corners alone: drop each point on a straight run. + corners = [loop[i] for i in range(len(loop)) if (loop[i - 1][0] == loop[i][0]) != (loop[i][0] == loop[(i + 1) % len(loop)][0])] + loops.append(corners) + return loops + + +def hvif(sizes): + """An HVIF of the palette pictures in sizes ((size, pixels, colours, + lodMin, lodMax), BE_TRANSPARENT clear): each colour's outlines a closed + path each, a shape a colour and size, hinted, and shown only at the + scales [lodMin, lodMax] (HVIF_LOD_UNITS a unit of icon scale, 64 pixels + being 1). None when that is more of anything than HVIF holds.""" + styles = [] + paths = [] + shapes = [] + for size, pixels, colours, lodMin, lodMax in sizes: + units = HVIF_GRID // size + for colour in sorted(set(pixels) - {BE_TRANSPARENT}): + if colours[colour] not in styles: + styles.append(colours[colour]) + first = len(paths) + for loop in hvifContours(pixels, size, colour): + if len(loop) > HVIF_MAX_ITEMS: + return None + paths.append(bytes((HVIF_PATH_CLOSED | HVIF_PATH_NO_CURVES, len(loop))) + bytes(HVIF_COORD_BIAS + v * units for point in loop for v in point)) + shapes.append(bytes((HVIF_SHAPE_PATH_SOURCE, styles.index(colours[colour]), len(paths) - first)) + bytes(range(first, len(paths))) + bytes((HVIF_SHAPE_HINTING | HVIF_SHAPE_LOD_SCALE, lodMin, lodMax))) + if max(len(styles), len(paths), len(shapes)) > HVIF_MAX_ITEMS: + return None + out = bytearray(HVIF_MAGIC) + out.append(len(styles)) + for colour in styles: + out += bytes((HVIF_STYLE_SOLID_COLOR,) + tuple(colour) + (HVIF_OPAQUE,)) + out.append(len(paths)) + for path in paths: + out += path + out.append(len(shapes)) + for shape in shapes: + out += shape + return bytes(out) def bakeC(picture, out, size): @@ -349,11 +478,29 @@ def bakeC(picture, out, size): text = ("// Generated by tools/iconBake.py: the program's window icon, %d x %d\n" "// RGBA pixels, row after row (src/hosted/sdl3/sdl3Backend.c).\n\n" "#include <stdint.h>\n\n" - "const uint16_t gHostedIconSize = %du;\n" - "const uint8_t gHostedIconRgba[] = {\n%s\n};\n") % (size, size, size, "\n".join(lines)) + "const uint16_t jlgHostedIconSize = %du;\n" + "const uint8_t jlgHostedIconRgba[] = {\n%s\n};\n") % (size, size, size, "\n".join(lines)) open(out, "w").write(text) +def bakeIcns(picture, out): + sizes = sorted({size for _, size in ICNS_ELEMENTS}) + largest = sizes[-1] + if max(picture.size) > PIXEL_ART_MAX: + largest = next((size for size in sizes if size >= max(picture.size)), largest) + pngs = {} + elements = b"" + for kind, size in ICNS_ELEMENTS: + if size > largest: + continue + if size not in pngs: + data = io.BytesIO() + fit(picture, size, size).save(data, "PNG") + pngs[size] = data.getvalue() + elements += struct.pack(ICNS_HEADER, kind, struct.calcsize(ICNS_HEADER) + len(pngs[size])) + pngs[size] + open(out, "wb").write(struct.pack(ICNS_HEADER, ICNS_MAGIC, struct.calcsize(ICNS_HEADER) + len(elements)) + elements) + + def pascal(text, size): """text as a Pascal string (a length byte first) in size bytes.""" data = text.encode("ascii")[:size - 1] @@ -444,8 +591,9 @@ BAKES = { "bmp": (bakeBmp, 0), "png": (bakePng, 1), "info": (bakeInfo, 1), - "be": (bakeBe, 1), + "be": (bakeBe, 2), "c": (bakeC, 1), + "icns": (bakeIcns, 0), "iigs": (bakeIigs, 1), } diff --git a/tools/modloopend/modloopend.c b/tools/modloopend/modloopend.c index 2d1744c..61b8483 100644 --- a/tools/modloopend/modloopend.c +++ b/tools/modloopend/modloopend.c @@ -70,7 +70,7 @@ static int process(const char *inPath, const char *outPath) { fprintf(stderr, "modloopend: out of memory\n"); } else if (fread(in, 1, (size_t)inSize, fp) != (size_t)inSize) { fprintf(stderr, "modloopend: short read on %s\n", inPath); - } else if (!modStopMarkerAdd(in, (uint32_t)inSize, out)) { + } else if (!jlModStopMarkerAdd(in, (uint32_t)inSize, out)) { fprintf(stderr, "modloopend: %s is not a Protracker module that can take a stop marker\n", inPath); } else { status = 0; diff --git a/tools/spritebake/spritebake.c b/tools/spritebake/spritebake.c index 35b745e..66c23ae 100644 --- a/tools/spritebake/spritebake.c +++ b/tools/spritebake/spritebake.c @@ -56,7 +56,7 @@ // thunk. On the host bake tool the value is don't-care (the .spc loader // re-stamps the real target address at load), so a never-called stub just // resolves the link -- the real walker is src/atarist/spriteWalk.s on target. -void stSpriteDrawShiftWalker(void) {} +void jlStSpriteDrawShiftWalker(void) {} #endif @@ -163,7 +163,7 @@ int main(int argc, char **argv) { sp.heightTiles = heightTiles; sp.widthPx = (uint16_t)(widthTiles * JL_TILE_PIXELS_PER_SIDE); sp.heightPx = (uint16_t)(heightTiles * JL_TILE_PIXELS_PER_SIDE); - total = spriteStageAndPlan(staging, &sp, false); + total = jlSpriteStageAndPlan(staging, &sp, false); // ST shifted-phase DRAW thunks bake an absolute jmp to the shared // walker; record its patch site so the loader re-stamps the runtime diff --git a/tools/tiled2joey.py b/tools/tiled2joey.py new file mode 100755 index 0000000..5678382 --- /dev/null +++ b/tools/tiled2joey.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. +# See LICENSE.md for license information. +# SPDX-License-Identifier: MIT + +"""tiled2joey.py - a Tiled map as JoeyLib tiles and tile maps. + + tiled2joey.py [--header OUT.h] MAP SHEET.png MAPDIR + +MAP is a Tiled map, TMX (XML) or TMJ (JSON): orthogonal, finite, with +8x8 tiles. Its tilesets, embedded or external (TSX or TSJ), are single +images, indexed PNGs whose colours make one palette of at most 16 between +them: the first tileset's colours keep their indices (so its index 0 +stays the background), and the others' are matched by colour or given a +slot the first does not use. + +SHEET.png gets the tiles the map's tile layers use, in a row, in the +order they first appear: what assetbake.py --type tile bakes into a tile +bank for each port. A tile Tiled draws flipped or rotated is baked as a +tile of its own (JoeyLib draws no tile flipped), so the map can use up to +255 distinct tiles and orientations between all its layers. + +MAPDIR gets one LAYER.map for each tile layer (its name, letters, digits, +- and _ kept): width x height bytes, row after row, each a tile's index +in the bank or 0xFF (JL_TILE_MAP_SKIP) where the layer is empty, as +jlTileMapPaste takes them. The files are the same on every port. + +--header writes a C header naming the map's size, its tile count, and +each layer's file. Object and image layers, tile animations, and tile +properties are left out. +""" + +import base64 +import gzip +import json +import os +import re +import struct +import sys +import xml.etree.ElementTree as ElementTree +import zlib + +from PIL import Image + +TILE_SIDE = 8 +MAX_TILES = 255 +SKIP = 0xFF +MAX_COLOURS = 16 +FLIP_H = 0x80000000 +FLIP_V = 0x40000000 +FLIP_D = 0x20000000 +ROTATE_HEX = 0x10000000 +GID_MASK = 0x0FFFFFFF + + +def fail(message): + sys.exit("tiled2joey: " + message) + + +def isXml(path): + with open(path, "rb") as f: + return f.read(64).lstrip().startswith(b"<") + + +def decodeData(text, encoding, compression): + """A layer's gids from its data: csv, or base64 of little-endian + 32-bit words, raw, zlib or gzip.""" + if encoding in (None, "", "csv"): + if isinstance(text, list): + return [int(v) for v in text] + return [int(v) for v in re.split(r"[\s,]+", text.strip()) if v] + if encoding != "base64": + fail("layer encoding %r is not supported" % encoding) + raw = base64.b64decode(text.strip()) + if compression == "zlib": + raw = zlib.decompress(raw) + elif compression == "gzip": + raw = gzip.decompress(raw) + elif compression not in (None, ""): + fail("layer compression %r is not supported (use zlib, gzip, or none)" % compression) + return list(struct.unpack("<%dI" % (len(raw) // 4), raw)) + + +def loadTilesetXml(element, base): + if element.find("image") is None: + fail("tileset %r is a collection of images; use one image" % element.get("name")) + image = element.find("image") + return { + "name": element.get("name"), + "tilewidth": int(element.get("tilewidth")), + "tileheight": int(element.get("tileheight")), + "margin": int(element.get("margin", 0)), + "spacing": int(element.get("spacing", 0)), + "columns": int(element.get("columns", 0)), + "image": os.path.join(base, image.get("source")), + "animated": any(t.find("animation") is not None for t in element.findall("tile")), + } + + +def loadTilesetJson(data, base): + if "image" not in data: + fail("tileset %r is a collection of images; use one image" % data.get("name")) + return { + "name": data.get("name"), + "tilewidth": data["tilewidth"], + "tileheight": data["tileheight"], + "margin": data.get("margin", 0), + "spacing": data.get("spacing", 0), + "columns": data.get("columns", 0), + "image": os.path.join(base, data["image"]), + "animated": any("animation" in t for t in data.get("tiles", [])), + } + + +def loadExternalTileset(path): + base = os.path.dirname(path) + if isXml(path): + return loadTilesetXml(ElementTree.parse(path).getroot(), base) + with open(path) as f: + return loadTilesetJson(json.load(f), base) + + +def loadMap(path): + """(width, height, tilesets as (firstgid, tileset), tile layers as + (name, gids)) from a TMX or TMJ map.""" + base = os.path.dirname(path) + tilesets = [] + layers = [] + if isXml(path): + root = ElementTree.parse(path).getroot() + header = {k: root.get(k) for k in ("orientation", "infinite", "tilewidth", "tileheight", "width", "height")} + for element in root.findall("tileset"): + if element.get("source"): + tileset = loadExternalTileset(os.path.join(base, element.get("source"))) + else: + tileset = loadTilesetXml(element, base) + tilesets.append((int(element.get("firstgid")), tileset)) + for element in root.iter("layer"): + data = element.find("data") + if data.find("chunk") is not None: + fail("layer %r is in chunks (an infinite map); make the map finite" % element.get("name")) + if data.find("tile") is not None: + gids = [int(t.get("gid", 0)) for t in data.findall("tile")] + else: + gids = decodeData(data.text or "", data.get("encoding"), data.get("compression")) + layers.append((element.get("name"), gids)) + else: + with open(path) as f: + root = json.load(f) + header = {k: root.get(k) for k in ("orientation", "infinite", "tilewidth", "tileheight", "width", "height")} + for entry in root.get("tilesets", []): + if "source" in entry: + tileset = loadExternalTileset(os.path.join(base, entry["source"])) + else: + tileset = loadTilesetJson(entry, base) + tilesets.append((entry["firstgid"], tileset)) + + def walk(items): + for layer in items: + if layer.get("type") == "group": + walk(layer.get("layers", [])) + elif layer.get("type") == "tilelayer": + if "chunks" in layer: + fail("layer %r is in chunks (an infinite map); make the map finite" % layer.get("name")) + layers.append((layer.get("name"), decodeData(layer["data"], layer.get("encoding"), layer.get("compression")))) + + walk(root.get("layers", [])) + if header["orientation"] != "orthogonal": + fail("the map is %s; only orthogonal maps are supported" % header["orientation"]) + if str(header["infinite"]).lower() in ("1", "true"): + fail("the map is infinite; make it finite") + if int(header["tilewidth"]) != TILE_SIDE or int(header["tileheight"]) != TILE_SIDE: + fail("the map's tiles are %sx%s; JoeyLib's are 8x8" % (header["tilewidth"], header["tileheight"])) + width = int(header["width"]) + height = int(header["height"]) + if not layers: + fail("the map has no tile layers") + for name, gids in layers: + if len(gids) != width * height: + fail("layer %r holds %d cells, not %d" % (name, len(gids), width * height)) + tilesets.sort(key=lambda entry: entry[0]) + return width, height, tilesets, layers + + +def tilesetPixels(tileset, palette, first): + """The tileset's tiles as 8x8 grids of indices into palette, a list + of MAX_COLOURS colours (None where free). The first tileset's colours + take their own indices; another's are matched by colour, or take the + first free slot.""" + if tileset["tilewidth"] != TILE_SIDE or tileset["tileheight"] != TILE_SIDE: + fail("tileset %r has %dx%d tiles; JoeyLib's are 8x8" % (tileset["name"], tileset["tilewidth"], tileset["tileheight"])) + if tileset["animated"]: + sys.stderr.write("tiled2joey: tileset %r animates tiles; only the tiles the map places are kept\n" % tileset["name"]) + image = Image.open(tileset["image"]) + if image.mode != "P": + fail("%s is mode %s; tilesets must be indexed PNGs (mode P)" % (tileset["image"], image.mode)) + source = image.getpalette() + used = sorted({index for _, index in image.getcolors(maxcolors=256)}) + remap = {} + for index in used: + rgb = tuple(source[index * 3:index * 3 + 3]) + if first: + if index >= MAX_COLOURS: + fail("%s uses palette index %d; tiles have %d colours" % (tileset["image"], index, MAX_COLOURS)) + palette[index] = rgb + elif rgb not in palette: + if None not in palette: + fail("the tilesets' colours make more than %d" % MAX_COLOURS) + palette[palette.index(None)] = rgb + remap[index] = palette.index(rgb) + columns = tileset["columns"] or (image.width - 2 * tileset["margin"] + tileset["spacing"]) // (TILE_SIDE + tileset["spacing"]) + rows = (image.height - 2 * tileset["margin"] + tileset["spacing"]) // (TILE_SIDE + tileset["spacing"]) + pixels = image.load() + tiles = [] + for row in range(rows): + for column in range(columns): + x0 = tileset["margin"] + column * (TILE_SIDE + tileset["spacing"]) + y0 = tileset["margin"] + row * (TILE_SIDE + tileset["spacing"]) + tiles.append([[remap[pixels[x0 + x, y0 + y]] for x in range(TILE_SIDE)] for y in range(TILE_SIDE)]) + return tiles + + +def orient(tile, flags): + """A tile as Tiled draws it: the diagonal flip (x and y swapped) + first, then the horizontal, then the vertical.""" + if flags & FLIP_D: + tile = [[tile[x][y] for x in range(TILE_SIDE)] for y in range(TILE_SIDE)] + if flags & FLIP_H: + tile = [list(reversed(row)) for row in tile] + if flags & FLIP_V: + tile = list(reversed(tile)) + return tile + + +def layerFileName(name, taken): + base = re.sub(r"[^A-Za-z0-9_-]+", "_", name).strip("_") or "layer" + candidate = base + n = 2 + while candidate in taken: + candidate = "%s_%d" % (base, n) + n += 1 + taken.add(candidate) + return candidate + ".map" + + +def writeHeader(path, mapPath, width, height, count, files): + guard = re.sub(r"[^A-Za-z0-9]", "_", os.path.basename(path)).upper() + prefix = re.sub(r"[^A-Za-z0-9]", "_", os.path.splitext(os.path.basename(mapPath))[0]).upper() + lines = [ + "// Generated by tools/tiled2joey.py from %s: do not edit." % os.path.basename(mapPath), + "#ifndef %s" % guard, + "#define %s" % guard, + "", + "#define %s_WIDTH %du // tiles across" % (prefix, width), + "#define %s_HEIGHT %du // tiles down" % (prefix, height), + "#define %s_TILES %du // tiles in the bank" % (prefix, count), + "", + "// The layers' map files, in the map's order:", + ] + lines += ["// %s (%s)" % (fileName, name) for name, fileName in files] + lines += ["", "#endif", ""] + with open(path, "w") as f: + f.write("\n".join(lines)) + + +def main(argv): + header = None + args = argv[1:] + if len(args) >= 2 and args[0] == "--header": + header = args[1] + args = args[2:] + if len(args) != 3: + sys.stderr.write(__doc__) + return 2 + mapPath, sheetPath, mapDir = args + width, height, tilesets, layers = loadMap(mapPath) + palette = [None] * MAX_COLOURS + sources = [(firstgid, tilesetPixels(tileset, palette, n == 0)) for n, (firstgid, tileset) in enumerate(tilesets)] + order = {} + tiles = [] + maps = [] + for name, gids in layers: + cells = bytearray() + for gid in gids: + if gid & ROTATE_HEX: + fail("layer %r rotates a tile 120 degrees (hexagonal maps only)" % name) + flags = gid & (FLIP_H | FLIP_V | FLIP_D) + gid &= GID_MASK + if gid == 0: + cells.append(SKIP) + continue + firstgid, source = max((entry for entry in sources if entry[0] <= gid), key=lambda entry: entry[0]) + local = gid - firstgid + if local >= len(source): + fail("layer %r places gid %d, past its tileset" % (name, gid)) + key = (firstgid, local, flags) + if key not in order: + if len(tiles) == MAX_TILES: + fail("the map uses more than %d distinct tiles and orientations" % MAX_TILES) + order[key] = len(tiles) + tiles.append(orient(source[local], flags)) + cells.append(order[key]) + maps.append((name, cells)) + if not tiles: + fail("the map's tile layers are empty") + sheet = Image.new("P", (TILE_SIDE * len(tiles), TILE_SIDE)) + flat = [c for rgb in palette for c in (rgb or (0, 0, 0))] + sheet.putpalette(flat + [0] * (3 * 256 - len(flat))) + sheet.putdata([tile[y][x] for y in range(TILE_SIDE) for tile in tiles for x in range(TILE_SIDE)]) + sheet.save(sheetPath) + os.makedirs(mapDir, exist_ok=True) + taken = set() + files = [] + for name, cells in maps: + fileName = layerFileName(name, taken) + with open(os.path.join(mapDir, fileName), "wb") as f: + f.write(bytes(cells)) + files.append((name, fileName)) + if header: + writeHeader(header, mapPath, width, height, len(tiles), files) + colours = sum(1 for rgb in palette if rgb is not None) + print("tiled2joey: %dx%d map, %d tile%s, %d colour%s, layers: %s" % (width, height, len(tiles), "" if len(tiles) == 1 else "s", colours, "" if colours == 1 else "s", ", ".join(f for _, f in files))) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))