Major refactoring

This commit is contained in:
Scott Duensing 2026-06-30 18:07:12 -05:00
parent 1972f6df90
commit add3f3dd0d
68 changed files with 3455 additions and 2241 deletions

View file

@ -35,7 +35,7 @@ ifeq ($(HAVE_DOS),1)
ALL_TARGETS += dos ALL_TARGETS += dos
endif endif
.PHONY: all iigs iigs-disk amiga atarist dos tools clean help status .PHONY: all iigs iigs-disk iigs-verify amiga atarist dos blank-check tools clean help status
all: $(ALL_TARGETS) tools all: $(ALL_TARGETS) tools
ifeq ($(ALL_TARGETS),) ifeq ($(ALL_TARGETS),)
@ -52,6 +52,9 @@ iigs:
iigs-disk: iigs-disk:
@$(MAKE) -f $(REPO_DIR)/make/iigs.mk iigs-disk @$(MAKE) -f $(REPO_DIR)/make/iigs.mk iigs-disk
iigs-verify:
@$(MAKE) -f $(REPO_DIR)/make/iigs.mk iigs-verify
amiga: amiga:
@$(MAKE) -f $(REPO_DIR)/make/amiga.mk @$(MAKE) -f $(REPO_DIR)/make/amiga.mk
@ -61,6 +64,11 @@ atarist:
dos: dos:
@$(MAKE) -f $(REPO_DIR)/make/dos.mk @$(MAKE) -f $(REPO_DIR)/make/dos.mk
# Compile + link-check the new-port template (src/blank/blank.c) with the host
# cc. No toolchain needed; keeps the template from bit-rotting as the lib grows.
blank-check:
@$(REPO_DIR)/scripts/check-blank.sh
clean: clean:
rm -rf $(REPO_DIR)/build rm -rf $(REPO_DIR)/build
@ -73,4 +81,6 @@ status:
help: help:
@echo "Targets: iigs amiga atarist dos all clean status help" @echo "Targets: iigs amiga atarist dos all clean status help"
@echo "Source toolchains/env.sh first." @echo " iigs-disk iigs-verify (IIgs MAME visual check)"
@echo " blank-check (host-cc smoke test of the new-port template)"
@echo "Source toolchains/env.sh first (blank-check needs no toolchain)."

View file

@ -49,7 +49,7 @@ void jlWaitVBL(void);
// Monotonic 16-bit frame counter. Caller must poll faster than // Monotonic 16-bit frame counter. Caller must poll faster than
// 2 * jlFrameHz() so no edge is missed. Used by benchmarks and // 2 * jlFrameHz() so no edge is missed. Used by benchmarks and
// frame-rate-independent animation. (Per-port edge-detect / VBL-ISR // frame-rate-independent animation. (Per-port edge-detect / VBL-ISR
// mechanism is documented at halFrameCount in src/core/hal.h.) // mechanism is documented at jlpFrameCount in src/core/port.h.)
uint16_t jlFrameCount(void); uint16_t jlFrameCount(void);
// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST // Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST
@ -62,7 +62,7 @@ uint16_t jlFrameHz(void);
// from frame rate -- two consecutive calls separated by a long // from frame rate -- two consecutive calls separated by a long
// render report the true elapsed time, not the number of VBLs that // render report the true elapsed time, not the number of VBLs that
// fired. (Per-port millisecond timer sources are documented at // fired. (Per-port millisecond timer sources are documented at
// halMillisElapsed in src/core/hal.h.) // jlpMillisElapsed in src/core/port.h.)
uint32_t jlMillisElapsed(void); uint32_t jlMillisElapsed(void);
// Deterministic, seedable pseudo-random generator. Bit-identical // Deterministic, seedable pseudo-random generator. Bit-identical

View file

@ -90,7 +90,7 @@ typedef enum {
void jlInputPoll(void); void jlInputPoll(void);
// Block until the user presses any key. Internally polls via // Block until the user presses any key. Internally polls via
// jlInputPoll, so per-port halInputPoll machinery (including // jlInputPoll, so per-port jlpInputPoll machinery (including
// audio-friendly IRQ-driven samplers) keeps working while the // audio-friendly IRQ-driven samplers) keeps working while the
// wait loop runs. // wait loop runs.
void jlWaitForAnyKey(void); void jlWaitForAnyKey(void);

View file

@ -12,7 +12,8 @@
#if !defined(JOEYLIB_PLATFORM_IIGS) && \ #if !defined(JOEYLIB_PLATFORM_IIGS) && \
!defined(JOEYLIB_PLATFORM_AMIGA) && \ !defined(JOEYLIB_PLATFORM_AMIGA) && \
!defined(JOEYLIB_PLATFORM_ATARIST) && \ !defined(JOEYLIB_PLATFORM_ATARIST) && \
!defined(JOEYLIB_PLATFORM_DOS) !defined(JOEYLIB_PLATFORM_DOS) && \
!defined(JOEYLIB_PLATFORM_BLANK)
#if defined(__DJGPP__) || defined(__MSDOS__) #if defined(__DJGPP__) || defined(__MSDOS__)
#define JOEYLIB_PLATFORM_DOS #define JOEYLIB_PLATFORM_DOS
@ -28,12 +29,17 @@
#endif #endif
// JOEYLIB_PLATFORM_BLANK is the copy-to-start template for a new port (see
// src/blank/blank.c). It has no auto-detect macro -- bring it up by building
// with -DJOEYLIB_PLATFORM_BLANK.
// ----- Validate exactly one platform is defined ----- // ----- Validate exactly one platform is defined -----
#if (defined(JOEYLIB_PLATFORM_IIGS) + \ #if (defined(JOEYLIB_PLATFORM_IIGS) + \
defined(JOEYLIB_PLATFORM_AMIGA) + \ defined(JOEYLIB_PLATFORM_AMIGA) + \
defined(JOEYLIB_PLATFORM_ATARIST) + \ defined(JOEYLIB_PLATFORM_ATARIST) + \
defined(JOEYLIB_PLATFORM_DOS)) != 1 defined(JOEYLIB_PLATFORM_DOS) + \
defined(JOEYLIB_PLATFORM_BLANK)) != 1
#error "JoeyLib: exactly one JOEYLIB_PLATFORM_* must be defined" #error "JoeyLib: exactly one JOEYLIB_PLATFORM_* must be defined"
#endif #endif
@ -61,6 +67,227 @@
#define JOEYLIB_ENDIAN_LITTLE #define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY #define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "MS-DOS" #define JOEYLIB_PLATFORM_NAME "MS-DOS"
#elif defined(JOEYLIB_PLATFORM_BLANK)
// Chunky little-endian is the simplest baseline for a new port: the generic
// backend gives you fully-working software rendering out of the box. Adjust
// these (or switch to JOEYLIB_NATIVE_PLANAR) once you know your hardware.
#define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "Blank Template"
#endif
// ----- Per-machine op override registry (JL_HAS_<OP>) -----
//
// A machine #define-s JL_HAS_<OP> for each overridable platform op it replaces
// with its own (asm / native) implementation in src/<machine>/. Ops a machine
// does NOT claim fall back to the portable-C default in src/generic/, selected
// at compile time (no runtime dispatch). The jlp<Op> macros/prototypes in
// src/core/port.h read this table. This is the new lib's equivalent of the
// 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 // iigsFillRectInner (stage), 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_SNAP // iigsTileSnapInner, macro
#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_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_FRAME_COUNT // GetTick word, function
#define JL_HAS_WAIT_VBL // $C019 VBL poll, function
// audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK
// lifecycle / present / input (every real port implements)
#define JL_HAS_INIT
#define JL_HAS_SHUTDOWN
#define JL_HAS_PRESENT
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL
#define JL_HAS_JOYSTICK_RESET
// 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_LINE // amigaPlanarLine, function
#define JL_HAS_DRAW_CIRCLE // amiga planar circle, function
#define JL_HAS_FILL_CIRCLE // amiga planar fill circle, function -- returns bool
#define JL_HAS_FILL_RECT // amiga planar fill rect, function
#define JL_HAS_TILE_FILL // amiga planar tile fill, function
#define JL_HAS_TILE_COPY // amiga planar tile copy, function
#define JL_HAS_TILE_COPY_MASKED // amiga planar masked copy, function
#define JL_HAS_TILE_PASTE // amiga planar tile paste, function
#define JL_HAS_TILE_SNAP // amiga planar tile snap, function
#define JL_HAS_TILE_PASTE_MONO // amiga planar mono colorize+paste, function
#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_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_SAMPLE_PIXEL // amiga planar nibble decode, function
#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
#define JL_HAS_STAGE_ALLOC_PIXELS // amiga: NULL (no chunky stage shadow), function
#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_SURFACE_PLANE_PTR // amiga plane base pointer, function
#define JL_HAS_FRAME_HZ // PAL 50, function
#define JL_HAS_FRAME_COUNT // VBL-server counter, function
#define JL_HAS_WAIT_VBL // WaitTOF, function
// audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK
// lifecycle / present / input (every real port implements)
#define JL_HAS_INIT
#define JL_HAS_SHUTDOWN
#define JL_HAS_PRESENT
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL
#define JL_HAS_JOYSTICK_RESET
// 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_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_TILE_FILL // st planar tile fill, function
#define JL_HAS_TILE_COPY // st planar tile copy, function
#define JL_HAS_TILE_COPY_MASKED // st planar masked copy, function
#define JL_HAS_TILE_PASTE // st planar tile paste, function
#define JL_HAS_TILE_SNAP // st planar tile snap, function
#define JL_HAS_TILE_PASTE_MONO // st planar mono colorize+paste, function
#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_SURFACE_COPY_PLANES // st planar buffer memcpy, function
#define JL_HAS_SAMPLE_PIXEL // st planar nibble decode, function
#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
#define JL_HAS_STAGE_ALLOC_PIXELS // st: NULL (no chunky stage shadow), function
#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_SURFACE_PLANE_PTR // st interleaved buffer base, function
#define JL_HAS_FRAME_HZ // PAL 50, function
#define JL_HAS_FRAME_COUNT // VBL-ISR counter, function
#define JL_HAS_WAIT_VBL // frame-counter wait, function
#define JL_HAS_MILLIS_ELAPSED // Supexec 200 Hz tick, function
// audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK
// lifecycle / present / input (every real port implements)
#define JL_HAS_INIT
#define JL_HAS_SHUTDOWN
#define JL_HAS_PRESENT
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL
#define JL_HAS_JOYSTICK_RESET
#elif defined(JOEYLIB_PLATFORM_DOS)
// chunky generics: surface clear, draw pixel/line/circle, fill circle/rect,
// tiles, surface readers, allocation -- DOS overrides only the timing services.
#define JL_HAS_FRAME_HZ // VGA ~70, function
#define JL_HAS_FRAME_COUNT // $3DA retrace edge counter, function
#define JL_HAS_WAIT_VBL // $3DA retrace poll, function
#define JL_HAS_MILLIS_ELAPSED // uclock / audio-ISR tick, function
// audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK
// lifecycle / present / input (every real port implements)
#define JL_HAS_INIT
#define JL_HAS_SHUTDOWN
#define JL_HAS_PRESENT
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL
#define JL_HAS_JOYSTICK_RESET
#elif defined(JOEYLIB_PLATFORM_BLANK)
// Copy-to-start template. It overrides ONLY the platform SERVICES (TODO
// stubs in src/blank/blank.c). Everything graphical -- draw / tile / sprite
// primitives, surface readers, allocation, bigAlloc, millisElapsed -- comes
// from the portable-C generics for free, so a chunky port renders correctly
// before you write a line of graphics code. Add a JL_HAS_<OP> here + an
// override in blank.c only when you want to accelerate a specific op.
#define JL_HAS_INIT // bring up your display mode
#define JL_HAS_SHUTDOWN // restore it
#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_JOYSTICK_RESET
#define JL_HAS_WAIT_VBL // pace to the display
#define JL_HAS_FRAME_COUNT // a monotonic frame/refresh counter
#define JL_HAS_FRAME_HZ // your refresh rate in Hz
// audio: left to the no-op generics (your port links + runs silent). Add
// JL_HAS_AUDIO_* + overrides in blank.c when you wire up sound.
#endif #endif
// ----- Library version ----- // ----- Library version -----

View file

@ -10,10 +10,10 @@ BINDIR := $(BUILD)/bin
# PTPlayer is staged by toolchains/install.sh into PTPLAYER_DIR; we # PTPlayer is staged by toolchains/install.sh into PTPLAYER_DIR; we
# reference its ptplayer.asm + header from there rather than copying # reference its ptplayer.asm + header from there rather than copying
# them into the source tree, so install.sh can refresh / version it # them into the source tree, so install.sh can refresh / version it
# independently. -I on $(SRC_PORT)/amiga lets ptplayer.h resolve # independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve
# <SDI_compiler.h> from the port-local shim alongside our HAL code. # <SDI_compiler.h> from the machine-local shim alongside our code.
PTPLAYER_DIR := $(REPO_DIR)/toolchains/amiga/ptplayer PTPLAYER_DIR := $(REPO_DIR)/toolchains/amiga/ptplayer
CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_PORT)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP $(CFLAGS_EXTRA) CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP $(CFLAGS_EXTRA)
# OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses # OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses
# CIA-B + audio.device interrupts via the OS rather than taking over # CIA-B + audio.device interrupts via the OS rather than taking over
# Paula directly), matching the way our HAL cooperates with Intuition. # Paula directly), matching the way our HAL cooperates with Intuition.
@ -27,13 +27,13 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT
# and surfaces as undefined references at every binary's link step). # and surfaces as undefined references at every binary's link step).
PTPLAYER_ASFLAGS := -Fhunk -m68000 -quiet -DOSCOMPAT=1 PTPLAYER_ASFLAGS := -Fhunk -m68000 -quiet -DOSCOMPAT=1
# --allow-multiple-definition lets our user-space tzset stub # --allow-multiple-definition lets our user-space tzset stub
# (src/port/amiga/libinit.c) win over libnix's version in # (src/amiga/libinit.c) win over libnix's version in
# __gmtoffset.o. libnix's tzset dereferences a possibly-NULL # __gmtoffset.o. libnix's tzset dereferences a possibly-NULL
# LocaleBase; our no-op skips the deref. # LocaleBase; our no-op skips the deref.
LDFLAGS := -noixemul -Wl,--allow-multiple-definition LDFLAGS := -noixemul -Wl,--allow-multiple-definition
PORT_C_SRCS := $(wildcard $(SRC_PORT)/amiga/*.c) PORT_C_SRCS := $(wildcard $(SRC_DIR)/amiga/*.c)
PORT_S_SRCS := $(wildcard $(SRC_PORT)/amiga/*.s) PORT_S_SRCS := $(wildcard $(SRC_DIR)/amiga/*.s)
SHARED_S := $(wildcard $(SRC_68K)/*.s) SHARED_S := $(wildcard $(SRC_68K)/*.s)
# Amiga uses PTPlayer's mt_playfx for SFX, not the libxmp+overlay # Amiga uses PTPlayer's mt_playfx for SFX, not the libxmp+overlay
@ -42,17 +42,20 @@ SHARED_S := $(wildcard $(SRC_68K)/*.s)
# code out of every Amiga binary. # code out of every Amiga binary.
CORE_C_SRCS_AMIGA := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS)) CORE_C_SRCS_AMIGA := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
# Sprite codegen: 68k emitter shared with the ST port. # Sprite codegen: the 68k emitters (spriteEmit68k.c shared with ST, plus the
# Amiga-planar spriteEmitPlanar68k.c) live in src/m68k/ and build into obj/68k/;
# only the CPU-independent dispatch stays in src/codegen.
CODEGEN_DIR := $(REPO_DIR)/src/codegen CODEGEN_DIR := $(REPO_DIR)/src/codegen
LIB_OBJS := \ LIB_OBJS := \
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS_AMIGA)) \ $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS_AMIGA)) \
$(patsubst $(SRC_PORT)/amiga/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \
$(patsubst $(SRC_PORT)/amiga/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(patsubst $(SRC_DIR)/amiga/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
$(patsubst $(SRC_DIR)/amiga/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \ $(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/port/ptplayer.o \ $(BUILD)/obj/port/ptplayer.o \
$(BUILD)/obj/codegen/spriteEmit68k.o \ $(BUILD)/obj/68k/spriteEmit68k.o \
$(BUILD)/obj/codegen/spriteEmitPlanar68k.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/codegen/spriteCompile.o $(BUILD)/obj/codegen/spriteCompile.o
LIB := $(LIBDIR)/libjoey.a LIB := $(LIBDIR)/libjoey.a
@ -129,13 +132,17 @@ $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@ $(AMIGA_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_PORT)/amiga/%.c $(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@ $(AMIGA_CC) $(CFLAGS) -c $< -o $@
# Hand-written 68k assembly: GAS syntax, fed through the gcc driver # 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.
$(BUILD)/obj/port/%.o: $(SRC_PORT)/amiga/%.s $(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.s
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@ $(AMIGA_CC) $(CFLAGS) -c $< -o $@
@ -153,6 +160,12 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@ $(AMIGA_CC) $(CFLAGS) -c $< -o $@
# 68k sprite emitters (spriteEmit68k.c, spriteEmitPlanar68k.c) now live in
# src/m68k/; -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@
$(LIB): $(LIB_OBJS) $(LIB): $(LIB_OBJS)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_AR) rcs $@ $^ $(AMIGA_AR) rcs $@ $^

View file

@ -24,20 +24,23 @@ LIBXMP_AR := $(LIBDIR)/libxmplite.a
# support has no impact on JoeyLib's audio. # support has no impact on JoeyLib's audio.
LIBXMP_CFLAGS := -DLIBXMP_CORE_PLAYER -DLIBXMP_CORE_DISABLE_IT -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w LIBXMP_CFLAGS := -DLIBXMP_CORE_PLAYER -DLIBXMP_CORE_DISABLE_IT -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w
PORT_C_SRCS := $(wildcard $(SRC_PORT)/atarist/*.c) PORT_C_SRCS := $(wildcard $(SRC_DIR)/atarist/*.c)
PORT_S_SRCS := $(wildcard $(SRC_PORT)/atarist/*.s) PORT_S_SRCS := $(wildcard $(SRC_DIR)/atarist/*.s)
SHARED_S := $(wildcard $(SRC_68K)/*.s) SHARED_S := $(wildcard $(SRC_68K)/*.s)
# Sprite codegen: 68k emitter shared with the Amiga port. # Sprite codegen: the 68k emitters (spriteEmit68k.c shared with Amiga, plus the
# ST-interleaved spriteEmitInterleaved68k.c) live in src/m68k/ and build into
# obj/68k/; only the CPU-independent dispatch stays in src/codegen.
CODEGEN_DIR := $(REPO_DIR)/src/codegen CODEGEN_DIR := $(REPO_DIR)/src/codegen
LIB_OBJS := \ LIB_OBJS := \
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \
$(patsubst $(SRC_PORT)/atarist/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \
$(patsubst $(SRC_PORT)/atarist/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(patsubst $(SRC_DIR)/atarist/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
$(patsubst $(SRC_DIR)/atarist/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \ $(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/codegen/spriteEmit68k.o \ $(BUILD)/obj/68k/spriteEmit68k.o \
$(BUILD)/obj/codegen/spriteEmitInterleaved68k.o \ $(BUILD)/obj/68k/spriteEmitInterleaved68k.o \
$(BUILD)/obj/codegen/spriteCompile.o $(BUILD)/obj/codegen/spriteCompile.o
LIB := $(LIBDIR)/libjoey.a LIB := $(LIBDIR)/libjoey.a
@ -105,14 +108,18 @@ $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@ $(ST_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_PORT)/atarist/%.c $(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_DIR)/atarist/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@ $(ST_CC) $(CFLAGS) -c $< -o $@
# Hand-written 68k assembly: GAS syntax, fed through the gcc driver # Hand-written 68k assembly: GAS syntax, fed through the gcc driver
# so the bundled m68k-atari-mint-as (binutils) does the work. No vasm # so the bundled m68k-atari-mint-as (binutils) does the work. No vasm
# dependency on the ST path. # dependency on the ST path.
$(BUILD)/obj/port/%.o: $(SRC_PORT)/atarist/%.s $(BUILD)/obj/port/%.o: $(SRC_DIR)/atarist/%.s
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@ $(ST_CC) $(CFLAGS) -c $< -o $@
@ -120,6 +127,12 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@ $(ST_CC) $(CFLAGS) -c $< -o $@
# 68k sprite emitters (spriteEmit68k.c, spriteEmitInterleaved68k.c) now live in
# src/m68k/; -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@
$(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ $(ST_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@

View file

@ -4,15 +4,23 @@
REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..)
INCLUDE_DIR := $(REPO_DIR)/include INCLUDE_DIR := $(REPO_DIR)/include
# Machine-specific code lives in src/<machine>/ (iigs, amiga, atarist, dos);
# code shared across machines of one CPU lives in a processor folder (src/m68k).
SRC_DIR := $(REPO_DIR)/src
SRC_CORE := $(REPO_DIR)/src/core SRC_CORE := $(REPO_DIR)/src/core
SRC_PORT := $(REPO_DIR)/src/port
SRC_CG := $(REPO_DIR)/src/codegen SRC_CG := $(REPO_DIR)/src/codegen
SRC_68K := $(REPO_DIR)/src/shared68k SRC_68K := $(REPO_DIR)/src/m68k
EXAMPLES := $(REPO_DIR)/examples EXAMPLES := $(REPO_DIR)/examples
# Portable C sources for libjoey -- present on every target. # Portable C sources for libjoey -- present on every target.
CORE_C_SRCS := $(wildcard $(SRC_CORE)/*.c) CORE_C_SRCS := $(wildcard $(SRC_CORE)/*.c)
# Common include flags. Per-port code can include hal.h / surfaceInternal.h # 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
# src/codegen/spriteCompile.c already provides.
GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic/*.c))
# Common include flags. Per-port code can include port.h / surfaceInternal.h
# directly because SRC_CORE is in the include path. # directly because SRC_CORE is in the include path.
COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2

View file

@ -22,15 +22,17 @@ LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP
LIBXMP_AR := $(LIBDIR)/libxmplite.a LIBXMP_AR := $(LIBDIR)/libxmplite.a
LIBXMP_CFLAGS := -DJOEY_LIBXMP_LITE -DLIBXMP_CORE_PLAYER -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w LIBXMP_CFLAGS := -DJOEY_LIBXMP_LITE -DLIBXMP_CORE_PLAYER -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w
PORT_C_SRCS := $(wildcard $(SRC_PORT)/dos/*.c) # The x86 sprite emitter (spriteEmitX86.c) now lives in src/dos/ and is built
PORT_S_SRCS := $(wildcard $(SRC_PORT)/dos/*.asm) # by the port rule below; only the CPU-independent dispatch stays in src/codegen.
PORT_C_SRCS := $(wildcard $(SRC_DIR)/dos/*.c)
PORT_S_SRCS := $(wildcard $(SRC_DIR)/dos/*.asm)
CODEGEN_DIR := $(REPO_DIR)/src/codegen CODEGEN_DIR := $(REPO_DIR)/src/codegen
LIB_OBJS := \ LIB_OBJS := \
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \
$(patsubst $(SRC_PORT)/dos/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \
$(patsubst $(SRC_PORT)/dos/%.asm,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(patsubst $(SRC_DIR)/dos/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
$(BUILD)/obj/codegen/spriteEmitX86.o \ $(patsubst $(SRC_DIR)/dos/%.asm,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(BUILD)/obj/codegen/spriteCompile.o $(BUILD)/obj/codegen/spriteCompile.o
LIB := $(LIBDIR)/libjoey.a LIB := $(LIBDIR)/libjoey.a
@ -119,11 +121,15 @@ $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(DOS_CC) $(CFLAGS) -c $< -o $@ $(DOS_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_PORT)/dos/%.c $(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(DOS_CC) $(CFLAGS) -c $< -o $@ $(DOS_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_PORT)/dos/%.asm $(BUILD)/obj/port/%.o: $(SRC_DIR)/dos/%.c
@mkdir -p $(dir $@)
$(DOS_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@
$(BUILD)/obj/port/%.o: $(SRC_DIR)/dos/%.asm
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(DOS_AS) $(ASFLAGS) $< -o $@ $(DOS_AS) $(ASFLAGS) $< -o $@

View file

@ -2,7 +2,7 @@
# #
# toolchains/iigs/clang-build.sh drives compile (clang) / assemble # toolchains/iigs/clang-build.sh drives compile (clang) / assemble
# (llvm-mc) / link (link816) against the prebuilt w65816 runtime. # (llvm-mc) / link (link816) against the prebuilt w65816 runtime.
# Hand-rolled asm lives in src/port/iigs/*.s (llvm-mc GAS syntax); # Hand-rolled asm lives in src/iigs/*.s (llvm-mc GAS syntax);
# bank placement is handled by -ffunction-sections + link816 # bank placement is handled by -ffunction-sections + link816
# dead-stripping. # dead-stripping.
# #
@ -26,19 +26,20 @@ RT_INC := $(LLVM816_ROOT)/runtime/include
CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections
INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen
PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c) PORT_C_SRCS := $(wildcard $(SRC_DIR)/iigs/*.c)
# Hand-rolled asm, migrated to llvm-mc GAS .s. # Hand-rolled asm, migrated to llvm-mc GAS .s.
PORT_ASM_SRCS := $(wildcard $(SRC_PORT)/iigs/*.s) PORT_ASM_SRCS := $(wildcard $(SRC_DIR)/iigs/*.s)
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that # IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
# DOS and ST share, so src/core/audioSfxMix.c is unused here. # DOS and ST share, so src/core/audioSfxMix.c is unused here.
CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS)) CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
# Sprite codegen: 65816 emitter + cross-platform compile dispatch. # Sprite codegen: the 65816 emitter (spriteEmitIigs.c) now lives in src/iigs/
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \ # and is picked up by PORT_C_SRCS; only the CPU-independent compile dispatch
$(REPO_DIR)/src/codegen/spriteCompile.c # remains in src/codegen.
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteCompile.c
LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(CODEGEN_SRCS) LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(GENERIC_C_SRCS) $(PORT_C_SRCS) $(CODEGEN_SRCS)
LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS) LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS)
# Example sources (single-TU unless noted). # Example sources (single-TU unless noted).
@ -64,7 +65,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin
NTP_ASM := $(BUILD)/audio/ntpdata.s NTP_ASM := $(BUILD)/audio/ntpdata.s
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32 IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples clean-iigs .PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify clean-iigs
# Default: compile-check the library + run the end-to-end smoke test. # Default: compile-check the library + run the end-to-end smoke test.
all iigs: iigs-lib iigs-clang-smoke all iigs: iigs-lib iigs-clang-smoke
@ -76,6 +77,19 @@ iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/JOY \
$(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \ $(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \
$(BINDIR)/AGI $(BINDIR)/AGI
# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that run-iigs-mame.sh /
# run-iigs.sh mount on flop4. Packs the launchable examples as ProDOS S16 apps
# (an 800KB floppy can't hold every example; see make-iigs-disk.sh).
iigs-disk: iigs-examples
BINDIR="$(BINDIR)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg
# Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW
# example off joey.2mg, and assert the SHR framebuffer was actually rendered
# (many distinct colors -> pixels/lines/circles/tiles/flood all drew). Pass
# VERIFY_EXAMPLE=name to check a different example.
iigs-verify: iigs-disk
$(REPO_DIR)/scripts/verify-iigs.sh $(or $(VERIFY_EXAMPLE),draw)
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN) $(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
@mkdir -p $(BUILD)/audio @mkdir -p $(BUILD)/audio
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s @cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s

92
scripts/bench-amiga.sh Executable file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Headless UBER perf capture for the Amiga (FS-UAE).
#
# Stages the built Amiga binaries + DATA into a scratch directory-HD
# with an s/startup-sequence that auto-runs Uber, boots A500/OCS from
# the real Kickstart on a private Xvfb display, polls the staged
# joeylog.txt until UBER prints its "press any key to exit" completion
# marker (or a wall-clock timeout), then kills the emulator and copies
# the log to build/amiga/bin/joeylog.txt for tools/uber-perf-table.
#
# scripts/bench-amiga.sh # default 150s timeout
# TIMEOUT=240 scripts/bench-amiga.sh # override
#
# Real Kickstart (toolchains/emulators/support/kickstart.rom) is
# required -- the HAL drives OCS directly and AROS is unreliable for it.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/amiga/bin
support=$repo/toolchains/emulators/support
kickstart=$support/kickstart.rom
timeout_s=${TIMEOUT:-150}
if [[ ! -f $bin_dir/Uber ]]; then
echo "Uber not built. Run 'make amiga' first." >&2
exit 1
fi
if [[ ! -f $kickstart ]]; then
echo "Kickstart ROM missing: $kickstart" >&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/" {} +
if [[ -d "$bin_dir/DATA" ]]; then
cp -r "$bin_dir/DATA" "$work/"
fi
# ':' anchors to the boot volume root.
echo ":Uber" > "$work/s/startup-sequence"
log=$work/joeylog.txt
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$!
sleep 0.4
cleanup() {
kill "$fs_pid" 2>/dev/null || true
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
rm -rf "$work"
}
trap cleanup EXIT
echo "bench-amiga: running UBER headless (timeout ${timeout_s}s)..." >&2
DISPLAY=$display fs-uae \
--amiga_model=A500 \
--fast_memory=2048 \
--hard_drive_0="$work" \
--hard_drive_0_label=JOEYLIB \
--kickstart_file="$kickstart" \
--automatic_input_grab=0 \
--fullscreen=0 \
--fade_out_duration=0 \
>/tmp/bench-amiga-fsuae.log 2>&1 &
fs_pid=$!
waited=0
while (( waited < timeout_s )); do
if [[ -f $log ]] && grep -q "press any key to exit" "$log" 2>/dev/null; then
break
fi
sleep 2
(( waited += 2 )) || true
done
if [[ ! -f $log ]]; then
echo "bench-amiga: FAIL -- no joeylog.txt produced" >&2
exit 1
fi
cp "$log" "$bin_dir/joeylog.txt"
ops=$(grep -c "ops/sec" "$log" || true)
done_marker=$(grep -c "press any key to exit" "$log" || true)
if (( done_marker == 0 )); then
echo "bench-amiga: WARNING -- timed out before completion marker (partial capture)" >&2
fi
echo "bench-amiga: captured $ops op lines (complete=$done_marker) in ${waited}s" >&2
echo " -> $bin_dir/joeylog.txt" >&2
grep "UBER:" "$log" | sed 's/ | hash=.*//' || true

84
scripts/bench-atarist.sh Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Headless UBER perf capture for the Atari ST (Hatari).
#
# Runs build/atarist/bin/UBER.PRG under Hatari (fast-forward, no sound)
# on a private Xvfb display, polling joeylog.txt until UBER prints its
# "press any key to exit" completion marker (or a wall-clock timeout),
# then kills the emulator.
#
# UBER writes "UBER: <op>: N iters / M frames = K ops/sec" lines to
# joeylog.txt in its CWD == the GEMDOS drive C: == the bin dir, so the
# log lands directly in build/atarist/bin/joeylog.txt where
# tools/uber-perf-table expects it.
#
# scripts/bench-atarist.sh # default 90s timeout
# TIMEOUT=120 scripts/bench-atarist.sh # override
#
# NOTE: requires the ST supervisor-mode fix in src/atarist/hal.c
# (jlpInit Super(0L)); without it UBER bus-errors on the first present.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/atarist/bin
tos=$repo/toolchains/emulators/support/emutos-512k.img
log=$bin_dir/joeylog.txt
timeout_s=${TIMEOUT:-90}
if [[ ! -f $bin_dir/UBER.PRG ]]; then
echo "UBER.PRG not built. Run 'make atarist' first." >&2
exit 1
fi
if [[ ! -f $tos ]]; then
echo "TOS ROM missing: $tos" >&2
exit 1
fi
rm -f "$log"
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$!
sleep 0.4
cleanup() {
kill "$hat_pid" 2>/dev/null || true
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup EXIT
echo "bench-atarist: running UBER headless (timeout ${timeout_s}s)..." >&2
DISPLAY=$display hatari \
--tos "$tos" \
--harddrive "$bin_dir" \
--gemdos-drive C \
--drive-a off --drive-b off \
--sound off \
--fast-forward on \
--frameskips 4 \
--auto "C:\\UBER.PRG" >/tmp/bench-atarist-hatari.log 2>&1 &
hat_pid=$!
# Poll for the completion marker UBER flushes just before its key wait.
waited=0
while (( waited < timeout_s )); do
if [[ -f $log ]] && grep -q "press any key to exit" "$log"; then
break
fi
sleep 1
(( waited++ )) || true
done
if [[ ! -f $log ]]; then
echo "bench-atarist: FAIL -- no joeylog.txt produced" >&2
exit 1
fi
ops=$(grep -c "ops/sec" "$log" || true)
done_marker=$(grep -c "press any key to exit" "$log" || true)
if (( done_marker == 0 )); then
echo "bench-atarist: WARNING -- timed out before completion marker (partial capture)" >&2
fi
echo "bench-atarist: captured $ops op lines (complete=$done_marker) in ${waited}s" >&2
echo " -> $log" >&2
grep "UBER:" "$log" | sed 's/ | hash=.*//' || true

71
scripts/bench-dos.sh Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Headless UBER perf capture for DOS (DOSBox, 386SX-16 timing).
#
# Mounts build/dos/bin as C: in a headless DOSBox (Xvfb), runs UBER.EXE
# (which writes JOEYLOG.TXT to its CWD == C: == the host bin dir), polls
# that log for UBER's "press any key to exit" completion marker, then
# kills DOSBox. tools/uber-perf-table reads build/dos/bin/JOEYLOG.TXT.
#
# Uses the same dosbox-386sx16.conf as run-dos.sh so the cycle budget
# matches the PERF.md reference machine.
#
# scripts/bench-dos.sh # default 90s timeout
# TIMEOUT=120 scripts/bench-dos.sh # override
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
log=$bin_dir/JOEYLOG.TXT
timeout_s=${TIMEOUT:-90}
if [[ ! -f $bin_dir/UBER.EXE ]]; then
echo "UBER.EXE not built. Run 'make dos' first." >&2
exit 1
fi
rm -f "$log" "$bin_dir/joeylog.txt"
display=:$((90 + RANDOM % 9))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
xvfb_pid=$!
sleep 0.4
cleanup() {
kill "$dosbox_pid" 2>/dev/null || true
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup EXIT
echo "bench-dos: running UBER headless (timeout ${timeout_s}s)..." >&2
DISPLAY=$display dosbox \
-conf "$conf" \
-c "C:" \
-c "UBER.EXE" \
"$bin_dir" >/tmp/bench-dos-dosbox.log 2>&1 &
dosbox_pid=$!
waited=0
while (( waited < timeout_s )); do
if [[ -f $log ]] && grep -q "press any key to exit" "$log" 2>/dev/null; then
break
fi
sleep 1
(( waited++ )) || true
done
if [[ ! -f $log ]]; then
echo "bench-dos: FAIL -- no JOEYLOG.TXT produced" >&2
exit 1
fi
# uber-perf-table reads build/dos/bin/JOEYLOG.TXT directly (uppercase).
ops=$(grep -c "ops/sec" "$log" || true)
done_marker=$(grep -c "press any key to exit" "$log" || true)
if (( done_marker == 0 )); then
echo "bench-dos: WARNING -- timed out before completion marker (partial capture)" >&2
fi
echo "bench-dos: captured $ops op lines (complete=$done_marker) in ${waited}s" >&2
echo " -> $log" >&2
grep "UBER:" "$log" | sed 's/ | hash=.*//' || true

57
scripts/check-blank.sh Executable file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# check-blank.sh - Smoke-test the new-port template (src/blank/blank.c).
#
# A blank port has no toolchain, so we compile it with the HOST cc just to
# prove two things stay true as the library evolves:
# 1. The template + the JOEYLIB_PLATFORM_BLANK blocks in platform.h still
# compile (catches a new platform-#if dispatch point that forgot a BLANK
# case -- e.g. assetLoad.c's tile target, the sprite emitter selector).
# 2. blank.c + the generics LINK with no unresolved jlp* ops -- i.e. a new
# chunky port really does get everything except the platform services for
# free from src/generic/.
#
# Source set mirrors a real blank-port build: core + ALL of src/generic
# (including the stub sprite emitter) + blank.c; NOT src/codegen/spriteCompile.c
# (that's only for ports with a per-CPU JIT emitter).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-blank.XXXXXX)
trap 'rm -rf "$work"' EXIT
inc=(-Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen)
cd "$repo"
fail=0
for f in src/core/*.c src/generic/*.c src/blank/blank.c; do
o="$work/$(echo "$f" | tr / _).o"
if ! "$cc" -DJOEYLIB_PLATFORM_BLANK "${inc[@]}" -Wall -c "$f" -o "$o" 2>"$work/err"; then
echo "check-blank: COMPILE FAIL: $f" >&2
sed 's/^/ /' "$work/err" >&2
fail=1
fi
done
[ "$fail" = 0 ] || { echo "check-blank: FAIL (template does not compile for BLANK)" >&2; exit 1; }
# Link-completeness: any JoeyLib symbol undefined across the whole object set
# (and not defined by any of them) is a real gap in blank.c or the generics.
missing=$(python3 - "$work" <<'PY'
import subprocess, sys, glob
defs=set(); und=set()
for o in glob.glob(sys.argv[1]+"/*.o"):
for line in subprocess.run(["nm",o],capture_output=True,text=True).stdout.splitlines():
p=line.split()
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")))))
PY
)
if [ -n "$missing" ]; then
echo "check-blank: FAIL - unresolved JoeyLib symbols (blank.c / generics gap):" >&2
echo " $missing" >&2
exit 1
fi
echo "check-blank: PASS (blank template compiles + links; all ops resolve via blank.c + generics)"

68
scripts/make-iigs-disk.sh Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
# make-iigs-disk.sh - Build the JOEYLIB ProDOS disk image (joey.2mg) that
# run-iigs-mame.sh and run-iigs.sh mount as the data disk (flop4). It holds
# the launchable example binaries as ProDOS type $B3 (S16 / GS-OS apps) in a
# volume named JOEYLIB, so GS/OS Finder lists and launches them.
#
# A 3.5" floppy caps at 800KB, which cannot hold every example at once.
# Examples are added in priority order (small primitive demos first); any that
# would overflow the volume are skipped with a warning rather than corrupting
# the image. Override the set with JOEY_DISK_EXAMPLES="DRAW SPRITE ...".
#
# Usage: scripts/make-iigs-disk.sh [output.2mg]
# Requires: toolchains/env.sh sourced (LLVM816_ROOT), built IIgs examples.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?make-iigs-disk.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
BINDIR="${BINDIR:-$repo/build/iigs/bin}"
OUT="${1:-$BINDIR/joey.2mg}"
VOL=JOEYLIB
[ -x "$CADIUS" ] || { echo "make-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
# Priority order: small primitive demos first so DRAW/PATTERN/etc. always fit;
# the large AGI build lands last and is dropped if the floppy is full.
order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER ADV2 ADV AGI})
work=$(mktemp -d -t joeylib-disk.XXXXXX)
trap 'rm -rf "$work"' EXIT
rm -f "$OUT"
"$CADIUS" CREATEVOLUME "$OUT" "$VOL" 800KB >/dev/null
freeBlocks() {
"$CADIUS" CATALOG "$OUT" 2>/dev/null \
| grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1
}
added=()
skipped=()
for name in "${order[@]}"; do
bin="$BINDIR/$name"
[ -f "$bin" ] || continue
sz=$(stat -c%s "$bin")
# data blocks + one index block per 256 data blocks + a little dir slack
need=$(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 ))
if [ "$need" -gt "$(freeBlocks)" ]; then
skipped+=("$name")
continue
fi
# ProDOS type $B3 (S16/GS-OS application), aux $0000.
cp "$bin" "$work/$name#B30000"
"$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/$name#B30000" >/dev/null
rm -f "$work/$name#B30000"
if "$CADIUS" CATALOG "$OUT" 2>/dev/null | grep -qE "^ $name +S16"; then
added+=("$name")
else
skipped+=("$name")
fi
done
echo "iigs-disk: $OUT (volume /$VOL)"
echo " added: ${added[*]:-none}"
if [ ${#skipped[@]} -gt 0 ]; then
echo " SKIPPED (800KB floppy full): ${skipped[*]}"
echo " -> mount a single-example disk via JOEY_DISK_EXAMPLES=\"NAME\" $0"
fi

View file

@ -251,17 +251,32 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
# Visible by default. Set MAME_HEADLESS=1 to suppress the video window # Visible by default. Set MAME_HEADLESS=1 to suppress the video window
# (CI / batch runs that only care about crash.txt). # (CI / batch runs that only care about crash.txt / snapshots). The crash
# hook drives MAME's debugger, which needs a Qt platform; headless boxes
# have no X display, so force Qt's offscreen plugin and SDL's dummy
# video/audio drivers so MAME never tries to open a window.
video_arg="-window" video_arg="-window"
if [[ "${MAME_HEADLESS:-0}" = "1" ]]; then if [[ "${MAME_HEADLESS:-0}" = "1" ]]; then
video_arg="-video none" # Use the software renderer (not "-video none"): MAME's video:snapshot()
# needs a real render target, so "-video none" silently drops snapshots.
# "-video soft" renders to an offscreen surface that snapshot() can read.
video_arg="-video soft"
export QT_QPA_PLATFORM=offscreen
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
fi fi
# apple2gs ROM set. Default to the user's MAME roms; override via MAME_ROMPATH.
# (A bare `mame apple2gs` only finds ROMs if rompath is already configured;
# passing it explicitly keeps this script self-contained.)
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
# Clear the done-marker the Lua hook uses to signal shutdown. # Clear the done-marker the Lua hook uses to signal shutdown.
rm -f "$out/.done" rm -f "$out/.done"
cd "$work" cd "$work"
mame apple2gs \ mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" \ -flop3 "$work/boot.po" \
-flop4 "$work/joey.2mg" \ -flop4 "$work/joey.2mg" \
$video_arg -sound none \ $video_arg -sound none \

127
scripts/verify-iigs.sh Executable file
View file

@ -0,0 +1,127 @@
#!/usr/bin/env bash
# verify-iigs.sh - Headless, deterministic visual-verification gate for the
# IIgs port. Boots GS/OS under MAME, drives the Finder to launch one example
# off the JOEYLIB data disk, then reads the Super Hi-Res framebuffer directly
# out of emulated memory ($E1/2000..$E1/9CFF) and reports how many distinct
# 4-bit pixel values were drawn plus a checksum. A blank/Finder screen yields
# a tiny distinct count; a real rendered demo (pixels, lines, circles, flood
# fills) lights up many of the 16 colors.
#
# Why memory reads instead of PNG snapshots: MAME's video:snapshot() needs a
# real render target, which a headless `-video none` run does not have. Reading
# the SHR bytes the asm actually wrote is render-target-independent and exact.
#
# Usage: scripts/verify-iigs.sh [example] [minDistinct]
# example lowercase name (default: draw). Must be on joey.2mg (run
# `make iigs-disk` first). The volume is JOEYLIB.
# minDistinct PASS threshold for distinct nibble values (default: 4).
#
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
example=${1:-draw}
minDistinct=${2:-4}
NAME=${example^^}
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
readFrame="${MAME_READ_FRAME:-9000}"
for f in "$sys_disk" "$data_disk"; do
[ -f "$f" ] || { echo "verify-iigs: missing $f (run 'make iigs-disk')" >&2; exit 2; }
done
work=$(mktemp -d -t joeylib-verify.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Finder keystroke timeline (frame counts calibrated against gsos-system.po):
# select JOEYLIB volume (J), Cmd-O to open it, type the program name, Cmd-O to
# launch. Then at readFrame, sum the SHR framebuffer and report. No -debug: we
# drive input with natkeyboard and read memory via the program space directly,
# and exit cleanly from Lua (so MAME never lingers).
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 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()
-- SHR pixels: \$E1/2000 .. \$E1/9CFF = 32000 bytes, 2 pixels/byte (4bpp).
local base = 0xE12000
local count = 32000
local seen = {}
local distinct = 0
local nonzero = 0
local sum = 0
for i = 0, count - 1 do
local b = mem:read_u8(base + i)
if b ~= 0 then nonzero = nonzero + 1 end
sum = (sum + b) & 0xFFFFFF
local hi = (b >> 4) & 0x0F
local lo = b & 0x0F
if not seen[hi] then seen[hi] = true; distinct = distinct + 1 end
if not seen[lo] then seen[lo] = true; distinct = distinct + 1 end
end
local scb0 = mem:read_u8(0xE19D00)
io.write(string.format("VERIFY-IIGS name=$NAME frame=%d distinctNibbles=%d nonZeroBytes=%d checksum=%06X scb0=%02X\n",
frame, distinct, nonzero, sum, scb0))
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("$NAME") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
{$readFrame, function() report(); 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 "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/verify.lua" </dev/null 2>&1) || true
line=$(echo "$out" | grep -E '^VERIFY-IIGS ' | tail -1)
echo "$line"
if [ -z "$line" ]; then
echo "verify-iigs: FAIL - no framebuffer report (boot/launch failed)" >&2
echo "$out" | tail -15 >&2
exit 1
fi
distinct=$(echo "$line" | sed -E 's/.*distinctNibbles=([0-9]+).*/\1/')
if [ "$distinct" -ge "$minDistinct" ]; then
echo "verify-iigs: PASS ($NAME rendered, distinctNibbles=$distinct >= $minDistinct)"
else
echo "verify-iigs: FAIL ($NAME distinctNibbles=$distinct < $minDistinct - screen looks blank)" >&2
exit 1
fi

View file

@ -22,7 +22,7 @@
#include <proto/exec.h> #include <proto/exec.h>
#include "ptplayer.h" #include "ptplayer.h"
#include "hal.h" #include "port.h"
#include "joey/audio.h" #include "joey/audio.h"
extern struct Custom custom; extern struct Custom custom;
@ -48,7 +48,7 @@ extern UBYTE mt_E8Trigger;
// unless the author placed an `E8FF` effect at song end. mt_E8Trigger // unless the author placed an `E8FF` effect at song end. mt_E8Trigger
// reflects the most-recently-seen E8 value; mt_init resets it to 0. // reflects the most-recently-seen E8 value; mt_init resets it to 0.
// When loop=false on PlayMod, we poll for this sentinel in // When loop=false on PlayMod, we poll for this sentinel in
// halAudioFrameTick and clear mt_Enable when seen. // jlpAudioFrameTick and clear mt_Enable when seen.
#define PTPLAYER_LOOP_END_MARKER 0xFF #define PTPLAYER_LOOP_END_MARKER 0xFF
// ----- Module state ----- // ----- Module state -----
@ -86,7 +86,7 @@ static UWORD periodForRate(uint16_t rateHz) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halAudioInit(void) { bool jlpAudioInit(void) {
int i; int i;
if (gInstalled) { if (gInstalled) {
@ -109,7 +109,7 @@ bool halAudioInit(void) {
} }
void halAudioShutdown(void) { void jlpAudioShutdown(void) {
int i; int i;
if (!gInstalled) { if (!gInstalled) {
@ -136,12 +136,12 @@ void halAudioShutdown(void) {
} }
bool halAudioIsPlayingMod(void) { bool jlpAudioIsPlayingMod(void) {
return gModPlaying && mt_Enable != 0; return gModPlaying && mt_Enable != 0;
} }
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
UBYTE *chip; UBYTE *chip;
if (!gInstalled) { if (!gInstalled) {
@ -167,7 +167,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
// PTPlayer takes the module pointer in a0, sample-data pointer in // PTPlayer takes the module pointer in a0, sample-data pointer in
// a1 (NULL = samples follow the module header), song position in // a1 (NULL = samples follow the module header), song position in
// d0. mt_init resets mt_E8Trigger to 0 so subsequent halAudioFrame // d0. mt_init resets mt_E8Trigger to 0 so subsequent jlpAudioFrame
// Tick polling sees a clean state. // Tick polling sees a clean state.
mt_init((void *)&custom, chip, NULL, 0); mt_init((void *)&custom, chip, NULL, 0);
mt_Enable = 1; mt_Enable = 1;
@ -176,7 +176,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
SfxStructure sfx; SfxStructure sfx;
SfxSlotT *s; SfxSlotT *s;
UBYTE *chip; UBYTE *chip;
@ -221,7 +221,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
// need a per-slot Paula buffer with mid-playback refill from the // need a per-slot Paula buffer with mid-playback refill from the
// audio IRQ. No-op until then; callers that depend on streaming // audio IRQ. No-op until then; callers that depend on streaming
// will hear silence on Amiga. // will hear silence on Amiga.
void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
(void)slot; (void)slot;
(void)fill; (void)fill;
(void)ctx; (void)ctx;
@ -233,7 +233,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
// keep a single-period square sample in chip RAM and re-program // keep a single-period square sample in chip RAM and re-program
// Paula's period register per note. Not wired yet -- AGI-on-Amiga // Paula's period register per note. Not wired yet -- AGI-on-Amiga
// will be silent until this is filled in. // will be silent until this is filled in.
void halAudioTone(uint16_t freqHz) { void jlpAudioTone(uint16_t freqHz) {
(void)freqHz; (void)freqHz;
} }
@ -246,14 +246,14 @@ void halAudioTone(uint16_t freqHz) {
// AGI 0..15 attenuation. Stubbed for now -- AGI-on-Amiga is silent // AGI 0..15 attenuation. Stubbed for now -- AGI-on-Amiga is silent
// until the chip buffer + AllocAudio + direct Paula programming // until the chip buffer + AllocAudio + direct Paula programming
// lands. See feedback_amiga_audio_voice memory when it does. // lands. See feedback_amiga_audio_voice memory when it does.
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
(void)voice; (void)voice;
(void)freqHz; (void)freqHz;
(void)atten; (void)atten;
} }
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
// CIA-B Timer B at hz could drive this; not wired until the // CIA-B Timer B at hz could drive this; not wired until the
// Paula voice path lands. // Paula voice path lands.
(void)fn; (void)fn;
@ -262,15 +262,15 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
} }
void halAudioCriticalEnter(void) { void jlpAudioCriticalEnter(void) {
} }
void halAudioCriticalExit(void) { void jlpAudioCriticalExit(void) {
} }
void halAudioStopMod(void) { void jlpAudioStopMod(void) {
if (!gInstalled) { if (!gInstalled) {
return; return;
} }
@ -281,7 +281,7 @@ void halAudioStopMod(void) {
} }
void halAudioStopSfx(uint8_t slot) { void jlpAudioStopSfx(uint8_t slot) {
UBYTE i; UBYTE i;
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
@ -298,7 +298,7 @@ void halAudioStopSfx(uint8_t slot) {
} }
void halAudioFrameTick(void) { void jlpAudioFrameTick(void) {
// PTPlayer drives itself off CIA-B; the only host-loop work is the // PTPlayer drives itself off CIA-B; the only host-loop work is the
// play-once watchdog. When the song author placed an `E8FF` at // play-once watchdog. When the song author placed an `E8FF` at
// song end, mt_E8Trigger latches to 0xFF -- if the caller passed // song end, mt_E8Trigger latches to 0xFF -- if the caller passed

View file

@ -46,7 +46,7 @@
#include <proto/intuition.h> #include <proto/intuition.h>
#include <proto/graphics.h> #include <proto/graphics.h>
#include "hal.h" #include "port.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -54,7 +54,7 @@ extern struct Custom custom;
// Frame-counter VBL server lives at end of file; forward-declare so // Frame-counter VBL server lives at end of file; forward-declare so
// halInit / halShutdown can install / remove it without C inferring // jlpInit / jlpShutdown can install / remove it without C inferring
// implicit non-static linkage at the call sites. // implicit non-static linkage at the call sites.
static void installVblServer(void); static void installVblServer(void);
static void removeVblServer(void); static void removeVblServer(void);
@ -103,7 +103,7 @@ static struct BitMap *gBitMap = NULL;
// Phase 10 page-flip: two CHIP plane sets. gPlanes is set A (kept // 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. // under the original name to keep diffs minimal); gPlanesB is set B.
// gFrontIdx selects which is currently displayed via BPL DMA. The // gFrontIdx selects which is currently displayed via BPL DMA. The
// off-screen set is the destination for halPresent's shadow->CHIP // off-screen set is the destination for jlpPresent's shadow->CHIP
// copy, so the beam never catches the writes. // copy, so the beam never catches the writes.
static UBYTE *gPlanes [AMIGA_BITPLANES]; static UBYTE *gPlanes [AMIGA_BITPLANES];
static UBYTE *gPlanesB[AMIGA_BITPLANES]; static UBYTE *gPlanesB[AMIGA_BITPLANES];
@ -114,7 +114,7 @@ static struct UCopList *gNewUCL = NULL; // built but not yet installed
// each BPLnPTH/PTL move instruction. scanBplCopperPokes finds them // each BPLnPTH/PTL move instruction. scanBplCopperPokes finds them
// after every installCopperList; patchBplCopperPokes writes new plane // after every installCopperList; patchBplCopperPokes writes new plane
// addresses into them per flip. NULL until the first copper list is // addresses into them per flip. NULL until the first copper list is
// installed (i.e. before the first halPresent). // installed (i.e. before the first jlpPresent).
static volatile uint16_t *gBplPokes[AMIGA_BITPLANES][2]; // [plane][0=PTH,1=PTL] static volatile uint16_t *gBplPokes[AMIGA_BITPLANES][2]; // [plane][0=PTH,1=PTL]
// Snapshot of the previous frame's dirty bands. With N=2 buffers, // Snapshot of the previous frame's dirty bands. With N=2 buffers,
@ -127,7 +127,7 @@ static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT]; static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
// "The previous frame's snapshot recorded at least one dirty row." // "The previous frame's snapshot recorded at least one dirty row."
// Lets halPresent early-out of the whole union scan + 400-byte // Lets jlpPresent early-out of the whole union scan + 400-byte
// snapshot copy on consecutive idle frames: if neither the previous // snapshot copy on consecutive idle frames: if neither the previous
// frame nor the current frame touched any row, union(prev,current) is // frame nor the current frame touched any row, union(prev,current) is
// empty AND the snapshot would copy clean->clean (a no-op), so there // empty AND the snapshot would copy clean->clean (a no-op), so there
@ -136,7 +136,7 @@ static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
// presents still run. // presents still run.
static bool gPrevAnyDirty = true; static bool gPrevAnyDirty = true;
// Cached SCB + palettes from the last present. halPresent* only needs // Cached SCB + palettes from the last present. jlpPresent* only needs
// to rebuild/install the copper list when SCB assignments or palette // to rebuild/install the copper list when SCB assignments or palette
// RGB values differ from what is already on screen; pure pixel updates // RGB values differ from what is already on screen; pure pixel updates
// (which dominate a typical game loop and every frame of the keys // (which dominate a typical game loop and every frame of the keys
@ -335,7 +335,7 @@ static void installCopperList(void) {
// MrgCop rebuilt the hardware copper list from scratch, including // MrgCop rebuilt the hardware copper list from scratch, including
// fresh BPL pointer pokes derived from gBitMap->Planes[]. Re-cache // fresh BPL pointer pokes derived from gBitMap->Planes[]. Re-cache
// pointers to those pokes so the per-frame flip in halPresent can // pointers to those pokes so the per-frame flip in jlpPresent can
// patch them in place without another MrgCop pass. // patch them in place without another MrgCop pass.
scanBplCopperPokes(); scanBplCopperPokes();
} }
@ -402,7 +402,7 @@ static void scanBplCopperPokes(void) {
// of CHIP plane buffers. Takes effect at the next vblank when the // of CHIP plane buffers. Takes effect at the next vblank when the
// copper next runs through these pokes. Disable() guards against an // copper next runs through these pokes. Disable() guards against an
// interrupt landing between word writes; the copper itself is past // interrupt landing between word writes; the copper itself is past
// the BPL section by the time halPresent runs (the app calls // the BPL section by the time jlpPresent runs (the app calls
// jlWaitVBL before jlStagePresent, so we're already in the visible // jlWaitVBL before jlStagePresent, so we're already in the visible
// scan region by here), so we can't actually tear a poke in flight. // scan region by here), so we can't actually tear a poke in flight.
static void patchBplCopperPokes(UBYTE *planes[AMIGA_BITPLANES]) { static void patchBplCopperPokes(UBYTE *planes[AMIGA_BITPLANES]) {
@ -518,7 +518,7 @@ static void uploadFirstBandPalette(const jlSurfaceT *src) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halInit(const jlConfigT *config) { bool jlpInit(const jlConfigT *config) {
uint16_t i; uint16_t i;
uint16_t j; uint16_t j;
@ -545,7 +545,7 @@ bool halInit(const jlConfigT *config) {
return false; return false;
} }
InitBitMap(gBitMap, (LONG)AMIGA_BITPLANES, (LONG)SURFACE_WIDTH, (LONG)SURFACE_HEIGHT); InitBitMap(gBitMap, (LONG)AMIGA_BITPLANES, (LONG)SURFACE_WIDTH, (LONG)SURFACE_HEIGHT);
// Phase 10 page-flip needs TWO CHIP plane sets so halPresent can // Phase 10 page-flip needs TWO CHIP plane sets so jlpPresent can
// write into the off-screen one and an atomic copper-list patch // write into the off-screen one and an atomic copper-list patch
// swaps which one BPL DMA fetches from. Allocate both up front; // swaps which one BPL DMA fetches from. Allocate both up front;
// bail with full rollback if either runs out of chip RAM. // bail with full rollback if either runs out of chip RAM.
@ -574,7 +574,7 @@ bool halInit(const jlConfigT *config) {
return false; return false;
} }
// Set A is the initial front buffer; BPL DMA fetches from it // Set A is the initial front buffer; BPL DMA fetches from it
// until the first halPresent flips to B. // until the first jlpPresent flips to B.
gBitMap->Planes[i] = gPlanes[i]; gBitMap->Planes[i] = gPlanes[i];
} }
gFrontIdx = 0; gFrontIdx = 0;
@ -590,7 +590,7 @@ bool halInit(const jlConfigT *config) {
// SA_DisplayID pins us to OCS PAL low-res so Intuition opens a // SA_DisplayID pins us to OCS PAL low-res so Intuition opens a
// real planar screen rather than an RTG substitute. SA_BitMap // real planar screen rather than an RTG substitute. SA_BitMap
// makes Intuition use OUR pre-allocated planes; CloseScreen will // makes Intuition use OUR pre-allocated planes; CloseScreen will
// not free them -- our halShutdown does. // not free them -- our jlpShutdown does.
gScreen = OpenScreenTags(NULL, gScreen = OpenScreenTags(NULL,
(ULONG)SA_Width, (ULONG)SURFACE_WIDTH, (ULONG)SA_Width, (ULONG)SURFACE_WIDTH,
(ULONG)SA_Height, (ULONG)SURFACE_HEIGHT, (ULONG)SA_Height, (ULONG)SURFACE_HEIGHT,
@ -630,16 +630,11 @@ bool halInit(const jlConfigT *config) {
} }
const char *halLastError(void) {
return NULL;
}
// Phase 9 switch flip: present is now always a per-row planar memcpy // Phase 9 switch flip: present is now always a per-row planar memcpy
// from the off-screen shadow planes (where every drawing primitive // from the off-screen shadow planes (where every drawing primitive
// dual-writes today) into gPlanes[] (the displayed BitMap). c2p is // dual-writes today) into gPlanes[] (the displayed BitMap). c2p is
// gone; the chunky `s->pixels` shadow is still maintained by the // gone; the chunky `s->pixels` shadow is still maintained by the
// halFast* primitives but no longer drives display. Phase 10 will // jlp<Op> primitives but no longer drives display. Phase 10 will
// either (a) BPLPTR-swap shadow <-> display planes (zero-copy) or // either (a) BPLPTR-swap shadow <-> display planes (zero-copy) or
// (b) stop writing chunky in the fast paths to recover the dual- // (b) stop writing chunky in the fast paths to recover the dual-
// write cost. Per-row dirty tracking is reused: only dirty bands // write cost. Per-row dirty tracking is reused: only dirty bands
@ -710,7 +705,7 @@ static void amigaPresentRectInner(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITP
} }
void halPresent(const jlSurfaceT *src) { void jlpPresent(const jlSurfaceT *src) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
int16_t y; int16_t y;
int16_t firstRow; int16_t firstRow;
@ -836,7 +831,7 @@ void halPresent(const jlSurfaceT *src) {
} }
// Snapshot current dirty bands as "previous" for next present. // Snapshot current dirty bands as "previous" for next present.
// jlStagePresent clears the live bands after halPresent returns, // jlStagePresent clears the live bands after jlpPresent returns,
// so we have to grab them now or they're gone. Record whether any // so we have to grab them now or they're gone. Record whether any
// row was dirty so the next present can take the idle-frame early- // row was dirty so the next present can take the idle-frame early-
// out above. A row is dirty iff its min word stayed below the clean // out above. A row is dirty iff its min word stayed below the clean
@ -859,7 +854,7 @@ void halPresent(const jlSurfaceT *src) {
// WaitTOF() blocks the calling task until the next "top of frame" // WaitTOF() blocks the calling task until the next "top of frame"
// VBlank interrupt -- 50 Hz on PAL, 60 Hz on NTSC. graphics.library // VBlank interrupt -- 50 Hz on PAL, 60 Hz on NTSC. graphics.library
// is auto-opened by libnix so no extra plumbing is needed. // is auto-opened by libnix so no extra plumbing is needed.
void halWaitVBL(void) { void jlpWaitVBL(void) {
WaitTOF(); WaitTOF();
} }
@ -871,7 +866,7 @@ void halWaitVBL(void) {
// when the caller's loop body is long. The interrupt server fires // when the caller's loop body is long. The interrupt server fires
// exactly once per VBlank regardless of caller cadence. // exactly once per VBlank regardless of caller cadence.
// //
// halFrameCount just reads the volatile counter -- no edge detection // jlpFrameCount just reads the volatile counter -- no edge detection
// needed in the polling path. // needed in the polling path.
static volatile uint16_t gFrameCount = 0; static volatile uint16_t gFrameCount = 0;
@ -915,12 +910,12 @@ static void removeVblServer(void) {
} }
uint16_t halFrameCount(void) { uint16_t jlpFrameCount(void) {
return gFrameCount; return gFrameCount;
} }
uint16_t halFrameHz(void) { uint16_t jlpFrameHz(void) {
/* PAL by default. The toolchain doesn't currently switch modes /* PAL by default. The toolchain doesn't currently switch modes
* at runtime; if we ever expose NTSC this returns 60. */ * at runtime; if we ever expose NTSC this returns 60. */
return 50u; return 50u;
@ -931,17 +926,14 @@ uint16_t halFrameHz(void) {
// IIgs. A timer.device-based ReadEClock impl would give microsecond // IIgs. A timer.device-based ReadEClock impl would give microsecond
// resolution and decouple from VBL load, but adds a 7-line library // resolution and decouple from VBL load, but adds a 7-line library
// open / close dance for what AGI doesn't notice yet (audio is // open / close dance for what AGI doesn't notice yet (audio is
// stubbed on Amiga -- see halAudioVoice). Revisit when 3-voice // stubbed on Amiga -- see jlpAudioVoice). Revisit when 3-voice
// audio actually lands on Amiga. // audio actually lands on Amiga.
uint32_t halMillisElapsed(void) {
return (uint32_t)((uint32_t)halFrameCount() * 1000u / 50u);
}
void halShutdown(void) { void jlpShutdown(void) {
uint16_t i; uint16_t i;
amigaDumpPresentCounters("halShutdown"); amigaDumpPresentCounters("jlpShutdown");
// Tear down the VBL server before closing the screen so the // Tear down the VBL server before closing the screen so the
// interrupt chain is clean if anything else is watching. // interrupt chain is clean if anything else is watching.
removeVblServer(); removeVblServer();
@ -956,7 +948,7 @@ void halShutdown(void) {
CloseScreen(gScreen); CloseScreen(gScreen);
gScreen = NULL; gScreen = NULL;
} }
// We allocated the BitMap and its planes manually (see halInit) // We allocated the BitMap and its planes manually (see jlpInit)
// so SA_BitMap could pin Intuition to non-interleaved layout. // so SA_BitMap could pin Intuition to non-interleaved layout.
// CloseScreen with an SA_BitMap'd screen does NOT free our // CloseScreen with an SA_BitMap'd screen does NOT free our
// BitMap or planes -- we own them and must clean up here. // BitMap or planes -- we own them and must clean up here.
@ -994,27 +986,20 @@ extern void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2,
uint16_t cx, uint16_t cy, uint16_t r, uint8_t color); uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
// Phase 3 planar dual-write: write the bitplanes alongside the // Amiga is pure planar (s->pixels is NULL). Each jlp<Op> below does the planar
// Phase 9: Amiga is pure planar. Every halFast* below returns true // work directly on the bitplanes in s->portData (AmigaPlanarT); the degenerate
// for any surface that has planar storage (s->portData != NULL) to // portData == NULL case defers to the chunky-C generic (a no-op there since
// suppress the cross-platform chunky fallback path -- there is no // s->pixels is NULL). The gate is portData presence, not stage identity, so
// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The gate // non-stage surfaces created by jlSurfaceCreate use their own planes.
// is portData presence, NOT stage identity, so non-stage surfaces void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
// created by jlSurfaceCreate use their OWN planes instead of falling
// into the chunky C fallback (which would deref s->pixels == NULL).
// The actual planar work happens either inline here or in dedicated
// halXxxPlanes hooks called by cross-platform code AFTER each halFast*.
//
// halFastSurfaceClear inlines the planar fill (no separate hook).
// All other halFast* are short-circuit stubs that return true.
bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t colorIndex; uint8_t colorIndex;
uint16_t i; uint16_t i;
bool setBits; bool setBits;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; jlpGenericSurfaceClear(s, doubled);
return;
} }
colorIndex = (uint8_t)(doubled & 0x0Fu); colorIndex = (uint8_t)(doubled & 0x0Fu);
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
@ -1022,7 +1007,7 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
// Blitter DEST-only block fill of every whole plane to $00 (clear) // Blitter DEST-only block fill of every whole plane to $00 (clear)
// or $FF (set). Each plane is 200 rows x 20 words, contiguous, so // or $FF (set). Each plane is 200 rows x 20 words, contiguous, so
// BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now // BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now
// CHIP RAM (see halSurfaceAllocPortData) so the blitter can reach // CHIP RAM (see jlpSurfaceAllocPortData) so the blitter can reach
// them. Own the blitter once and fill all four planes; the final // them. Own the blitter once and fill all four planes; the final
// WaitBlit before DisownBlitter guarantees the planes are fully // WaitBlit before DisownBlitter guarantees the planes are fully
// written before any CPU read (a subsequent draw or the present // written before any CPU read (a subsequent draw or the present
@ -1034,27 +1019,13 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
} }
WaitBlit(); WaitBlit();
DisownBlitter(); DisownBlitter();
return true;
}
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
/* Pure short-circuit: halFillRectPlanes (called by cross-platform
* jlFillRect after this) does the actual planar fill with full
* partial-byte mask handling. We just claim ownership so the C
* chunky fallback never runs. */
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
if (s->portData == NULL) {
return false;
}
return true;
} }
// Phase 3 planar dual-write for jlFillRect: writes the four off-screen // Phase 3 planar dual-write for jlFillRect: writes the four off-screen
// shadow plane buffers alongside the chunky shadow. Caller (cross- // shadow plane buffers alongside the chunky shadow. Caller (cross-
// platform jlFillRect) has already done the chunky write via // platform jlFillRect) has already done the chunky write via
// halFastFillRect or fillRectClipped. The shadow planes are off- // the chunky generic. The shadow planes are off-
// screen so this is invisible until jlStagePresent. // screen so this is invisible until jlStagePresent.
// //
// Layout reminder (see docs/amiga_planar.md): each plane byte covers // Layout reminder (see docs/amiga_planar.md): each plane byte covers
@ -1070,7 +1041,7 @@ bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h
// For each plane, the bit value at every pixel in the rect is // For each plane, the bit value at every pixel in the rect is
// constant: (colorIndex >> plane) & 1. Set bit -> OR with mask; // constant: (colorIndex >> plane) & 1. Set bit -> OR with mask;
// clear bit -> AND with ~mask. // clear bit -> AND with ~mask.
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t byteFirst; uint16_t byteFirst;
uint16_t byteLast; uint16_t byteLast;
@ -1090,13 +1061,14 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
} }
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd == NULL) { if (pd == NULL) {
jlpGenericFillRect(s, x, y, w, h, colorIndex);
return; return;
} }
/* Variable shifts on 68000 cost 8 cyc per bit shifted -- for shifts /* Variable shifts on 68000 cost 8 cyc per bit shifted -- for shifts
* by up to 7 that's ~30 cyc per mask, ~60 cyc both. LUTs compile * by up to 7 that's ~30 cyc per mask, ~60 cyc both. LUTs compile
* to a single byte load. jlFillCircle r=40 calls halFillRectPlanes * to a single byte load. jlFillCircle r=40 fills 160 spans, so
* 160 times, so this saves ~160*60=9600 cyc per jlFillCircle. */ * this saves ~160*60=9600 cyc per jlFillCircle. */
static const uint8_t kLeftMaskLut[8] = { static const uint8_t kLeftMaskLut[8] = {
0xFFu, 0x7Fu, 0x3Fu, 0x1Fu, 0x0Fu, 0x07u, 0x03u, 0x01u 0xFFu, 0x7Fu, 0x3Fu, 0x1Fu, 0x0Fu, 0x07u, 0x03u, 0x01u
}; };
@ -1339,7 +1311,7 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
// byte per row, 8 rows per tile, no edge masks. Stride between rows // byte per row, 8 rows per tile, no edge masks. Stride between rows
// in a plane is AMIGA_BYTES_PER_ROW (40). // in a plane is AMIGA_BYTES_PER_ROW (40).
void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { void jlpTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t plane; uint16_t plane;
uint8_t fillByte; uint8_t fillByte;
@ -1348,6 +1320,7 @@ void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd == NULL) { if (pd == NULL) {
jlpGenericTileFill(s, bx, by, colorIndex);
return; return;
} }
for (plane = 0; plane < AMIGA_BITPLANES; plane++) { for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
@ -1361,7 +1334,7 @@ void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex
} }
void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { void jlpTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
AmigaPlanarT *dstPd; AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd; AmigaPlanarT *srcPd;
uint16_t plane; uint16_t plane;
@ -1372,6 +1345,7 @@ void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu
dstPd = (AmigaPlanarT *)dst->portData; dstPd = (AmigaPlanarT *)dst->portData;
srcPd = (AmigaPlanarT *)src->portData; srcPd = (AmigaPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) { if (dstPd == NULL || srcPd == NULL) {
jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
return; return;
} }
for (plane = 0; plane < AMIGA_BITPLANES; plane++) { for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
@ -1393,10 +1367,9 @@ void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu
// bits = OR of 4 XOR'd bytes is 0 in that bit). Then for each plane, // bits = OR of 4 XOR'd bytes is 0 in that bit). Then for each plane,
// dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set // dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set
// positions, preserve dst bits elsewhere. // positions, preserve dst bits elsewhere.
void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) { void jlpTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) {
AmigaPlanarT *dstPd; AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd; AmigaPlanarT *srcPd;
uint8_t transparent;
uint8_t transBitByte[AMIGA_BITPLANES]; uint8_t transBitByte[AMIGA_BITPLANES];
uint16_t i; uint16_t i;
uint8_t row; uint8_t row;
@ -1408,9 +1381,10 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
dstPd = (AmigaPlanarT *)dst->portData; dstPd = (AmigaPlanarT *)dst->portData;
srcPd = (AmigaPlanarT *)src->portData; srcPd = (AmigaPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) { if (dstPd == NULL || srcPd == NULL) {
jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent);
return; return;
} }
transparent = (uint8_t)(transparentIndex & 0x0Fu); transparent = (uint8_t)(transparent & 0x0Fu);
/* Per-plane "all bits set if transparent's bit at this plane is 1 /* Per-plane "all bits set if transparent's bit at this plane is 1
* else all 0" -- so XOR gives bit set where pixel differs from * else all 0" -- so XOR gives bit set where pixel differs from
* transparent in that plane. */ * transparent in that plane. */
@ -1457,7 +1431,7 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
// pixel's 4bpp color is assembled from / written to the 4 plane bits // pixel's 4bpp color is assembled from / written to the 4 plane bits
// at byte x>>3, bit 0x80 >> (x & 7). // at byte x>>3, bit 0x80 >> (x & 7).
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const AmigaPlanarT *pd; const AmigaPlanarT *pd;
uint16_t pairs; uint16_t pairs;
uint16_t row; uint16_t row;
@ -1468,6 +1442,49 @@ void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) { if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
return; 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) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= 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); pairs = (uint16_t)(w >> 1);
for (row = 0; row < (uint16_t)h; row++) { 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 *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
@ -1497,7 +1514,7 @@ void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
} }
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t pairs; uint16_t pairs;
uint16_t row; uint16_t row;
@ -1508,6 +1525,44 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) { if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
return; 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) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= 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); pairs = (uint16_t)(w >> 1);
for (row = 0; row < (uint16_t)h; row++) { for (row = 0; row < (uint16_t)h; row++) {
uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW; uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
@ -1578,7 +1633,7 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
* one static routine in lib code handles every sprite cel, so the * one static routine in lib code handles every sprite cel, so the
* codegen arena no longer needs the ~17-23 KB of unrolled per-shift * codegen arena no longer needs the ~17-23 KB of unrolled per-shift
* code per cel that the old emitter generated. */ * code per cel that the old emitter generated. */
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) { void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
int16_t dy; int16_t dy;
int16_t sy; int16_t sy;
@ -1703,7 +1758,7 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
// <-> planar conversion at all. The previous c2p-based path paid // <-> planar conversion at all. The previous c2p-based path paid
// 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper. // 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper.
#define AMIGA_TILE_PLANE_STRIDE 8 #define AMIGA_TILE_PLANE_STRIDE 8
void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *tileOut) { void jlpTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t row; uint16_t row;
uint16_t rowBase; uint16_t rowBase;
@ -1711,12 +1766,13 @@ void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *t
pd = (AmigaPlanarT *)src->portData; pd = (AmigaPlanarT *)src->portData;
if (pd == NULL) { if (pd == NULL) {
jlpGenericTileSnap(src, bx, by, chunkyOut);
return; return;
} }
rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx;
for (plane = 0; plane < AMIGA_BITPLANES; plane++) { for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
const uint8_t *p = pd->planes[plane] + rowBase; const uint8_t *p = pd->planes[plane] + rowBase;
uint8_t *q = tileOut + plane * AMIGA_TILE_PLANE_STRIDE; uint8_t *q = chunkyOut + plane * AMIGA_TILE_PLANE_STRIDE;
for (row = 0; row < 8u; row++) { for (row = 0; row < 8u; row++) {
q[row] = p[row * AMIGA_BYTES_PER_ROW]; q[row] = p[row * AMIGA_BYTES_PER_ROW];
} }
@ -1724,7 +1780,7 @@ void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *t
} }
void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *tileBytes) { void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t row; uint8_t row;
uint8_t plane; uint8_t plane;
@ -1732,14 +1788,15 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *
pd = (AmigaPlanarT *)dst->portData; pd = (AmigaPlanarT *)dst->portData;
if (pd == NULL) { if (pd == NULL) {
jlpGenericTilePaste(dst, bx, by, chunkyTile);
return; return;
} }
/* jlTileT.pixels[] holds plane-major planar bytes (see /* jlTileT.pixels[] holds plane-major planar bytes (see
* halTileSnapPlanes header for layout). Paste = 32 byte stores * jlpTileSnap header for layout). Paste = 32 byte stores
* with no chunky -> planar conversion. */ * with no chunky -> planar conversion. */
rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx;
for (plane = 0; plane < AMIGA_BITPLANES; plane++) { for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
const uint8_t *q = tileBytes + plane * AMIGA_TILE_PLANE_STRIDE; const uint8_t *q = chunkyTile + plane * AMIGA_TILE_PLANE_STRIDE;
uint8_t *p = pd->planes[plane] + rowBase; uint8_t *p = pd->planes[plane] + rowBase;
for (row = 0; row < 8u; row++) { for (row = 0; row < 8u; row++) {
p[row * AMIGA_BYTES_PER_ROW] = q[row]; p[row * AMIGA_BYTES_PER_ROW] = q[row];
@ -1749,14 +1806,14 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *
/* Planar monochrome paste. monoTile is plane-major (matches the /* Planar monochrome paste. monoTile is plane-major (matches the
* Amiga halTileSnapPlanes output): bytes [0..7] = plane 0 = shape * Amiga jlpTileSnap output): bytes [0..7] = plane 0 = shape
* bits, bytes [8..15] = plane 1 (zero for monochrome tiles), etc. * bits, bytes [8..15] = plane 1 (zero for monochrome tiles), etc.
* Each pixel's output color: fgColor where shape bit is 1, bgColor * Each pixel's output color: fgColor where shape bit is 1, bgColor
* where 0. Output is written directly to the dst planar buffers via * where 0. Output is written directly to the dst planar buffers via
* the per-plane formula * the per-plane formula
* out_plane_k = (shape & mask_fg_k) | (~shape & mask_bg_k) * out_plane_k = (shape & mask_fg_k) | (~shape & mask_bg_k)
* where mask_X_k = $FF if (X & (1<<k)) else $00. */ * where mask_X_k = $FF if (X & (1<<k)) else $00. */
void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t plane; uint8_t plane;
uint8_t row; uint8_t row;
@ -1764,6 +1821,7 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
pd = (AmigaPlanarT *)dst->portData; pd = (AmigaPlanarT *)dst->portData;
if (pd == NULL) { if (pd == NULL) {
jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor);
return; return;
} }
rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx;
@ -1786,7 +1844,7 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
// the cross-platform chunky pixel memcpy. Both src and dst planes // the cross-platform chunky pixel memcpy. Both src and dst planes
// are off-screen shadow buffers; the displayed gPlanes[] is updated // are off-screen shadow buffers; the displayed gPlanes[] is updated
// only at jlStagePresent. // only at jlStagePresent.
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
AmigaPlanarT *dstPd; AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd; AmigaPlanarT *srcPd;
@ -1797,7 +1855,7 @@ void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
} }
// Straight blitter A->D copy of every whole plane (200 rows x 20 // Straight blitter A->D copy of every whole plane (200 rows x 20
// words, contiguous). Both source and dest planes are CHIP RAM now // words, contiguous). Both source and dest planes are CHIP RAM now
// (halSurfaceAllocPortData) so the blitter can reach both. The // (jlpSurfaceAllocPortData) so the blitter can reach both. The
// helper owns the blitter, copies all four planes, and WaitBlit's // helper owns the blitter, copies all four planes, and WaitBlit's
// before DisownBlitter so the dest planes are complete before any // before DisownBlitter so the dest planes are complete before any
// CPU read. // CPU read.
@ -1806,43 +1864,6 @@ void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
} }
/* Phase 9: tile halFast hooks return true to suppress chunky fallback.
* The actual planar work happens in halTileFillPlanes / halTileCopyPlanes
* / etc. (called by cross-platform tile.c after each halFast). jlTileSnap
* outputs a chunky jlTileT -- see halTileSnapPlanes-style derivation
* inside the snap planar work added below if needed. For now jlTileSnap
* skips its output (jlTileT will be all-zeros) when called on Amiga;
* apps that depend on jlTileSnap on Amiga need a planar-to-chunky
* derivation hook (TODO if any UBER/demo path actually exercises it). */
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
(void)dstRow0;
(void)srcRow0;
return true;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
(void)dstRow0;
(void)srcRow0;
(void)transparent;
return true;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
(void)dstRow0;
(void)srcTilePixels;
return true;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
(void)dstTilePixels;
(void)srcRow0;
return true;
}
// Phase 4 planar dual-write helper: set one pixel's bit in each of // 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 // the four shadow planes. Caller (the per-primitive walker below) has
// already validated (x, y) is on-surface. byteOff is computed once // already validated (x, y) is on-surface. byteOff is computed once
@ -1903,16 +1924,16 @@ static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t
} }
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t nibLo; uint8_t nibLo;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; jlpGenericDrawPixel(s, x, y, colorIndex);
return;
} }
nibLo = (uint8_t)(colorIndex & 0x0Fu); nibLo = (uint8_t)(colorIndex & 0x0Fu);
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo); amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
return true;
} }
@ -1959,28 +1980,26 @@ static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1
} }
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; return;
} }
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu)); amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
return true;
} }
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; return;
} }
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1], surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
pd->planes[2], pd->planes[3], pd->planes[2], pd->planes[3],
(uint16_t)cx, (uint16_t)cy, r, (uint16_t)cx, (uint16_t)cy, r,
(uint8_t)(colorIndex & 0x0Fu)); (uint8_t)(colorIndex & 0x0Fu));
return true;
} }
@ -2038,7 +2057,7 @@ void amigaFillSpanInline(AmigaPlanarT *pd, int16_t spanX, int16_t spanY,
} }
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
int16_t x; int16_t x;
int16_t y; int16_t y;
@ -2050,7 +2069,7 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
return false; return false;
} }
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
/* Inline per-span plane fill -- avoids 4 halFillRectPlanes /* Inline per-span plane fill -- avoids 4 jlpFillRect
* dispatches per midpoint iter (~320 dispatches for r=40). */ * dispatches per midpoint iter (~320 dispatches for r=40). */
x = (int16_t)r; x = (int16_t)r;
y = 0; y = 0;
@ -2076,65 +2095,6 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
} }
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)row;
(void)startX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)markBuf;
return false;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)scanY;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
return false;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)pixels;
(void)x;
(void)y;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
/* Plane-aware nibble at (x, y) given the 4 plane row bases. Reads one /* Plane-aware nibble at (x, y) given the 4 plane row bases. Reads one
* byte per plane and assembles the 4-bit color from a single bit * byte per plane and assembles the 4-bit color from a single bit
* position. Caller is responsible for valid (x, y). */ * position. Caller is responsible for valid (x, y). */
@ -2168,7 +2128,7 @@ static void amigaPlaneRowPtrs(const jlSurfaceT *s, int16_t y, uint8_t **out /* [
} }
bool halFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) { bool jlpFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t *rowPlanes[AMIGA_BITPLANES]; uint8_t *rowPlanes[AMIGA_BITPLANES];
int16_t leftX; int16_t leftX;
@ -2218,7 +2178,7 @@ bool halFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t
} }
bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) { bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t *rowPlanes[AMIGA_BITPLANES]; uint8_t *rowPlanes[AMIGA_BITPLANES];
int16_t byteCol; int16_t byteCol;
@ -2269,17 +2229,6 @@ bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i
} }
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
/* Phase 9: chunky write skipped; halTileFillPlanes (called by
* cross-platform tile.c after this) does the planar fill. */
(void)s;
(void)bx;
(void)by;
(void)fillWord;
return true;
}
/* ===== Phase 9 reader hooks: pure-planar Amiga implementations ===== /* ===== Phase 9 reader hooks: pure-planar Amiga implementations =====
* *
* Cross-platform code that USED to read s->pixels (chunky shadow) now * Cross-platform code that USED to read s->pixels (chunky shadow) now
@ -2290,7 +2239,7 @@ bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
* byte at row*40 + x/8 in each of the 4 planes. Color index = sum * byte at row*40 + x/8 in each of the 4 planes. Color index = sum
* of (bit_p << p). */ * of (bit_p << p). */
uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t byteOff; uint16_t byteOff;
uint8_t bitMask; uint8_t bitMask;
@ -2314,7 +2263,7 @@ uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
/* Reverse-c2p: per row, derive 160 chunky bytes from 40 plane bytes /* Reverse-c2p: per row, derive 160 chunky bytes from 40 plane bytes
* (per plane, 4 planes). Used by halSurfaceHash to fold the planar * (per plane, 4 planes). Used by jlpSurfaceHash to fold the planar
* surface into the same byte-stream the chunky ports hash, so cross- * surface into the same byte-stream the chunky ports hash, so cross-
* port hash comparisons stay valid. * port hash comparisons stay valid.
* Walks 8 pixels per planar-byte column; per pixel assembles nibble * Walks 8 pixels per planar-byte column; per pixel assembles nibble
@ -2354,7 +2303,7 @@ static void amigaPlanesToChunkyRow(const AmigaPlanarT *pd, int16_t y, uint8_t *d
} }
uint32_t halSurfaceHash(const jlSurfaceT *s) { uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t lo = 0xACE1u, hi = 0x1357u; uint16_t lo = 0xACE1u, hi = 0x1357u;
uint16_t n, v; uint16_t n, v;
@ -2394,17 +2343,9 @@ uint32_t halSurfaceHash(const jlSurfaceT *s) {
} }
void halSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
/* Amiga has no chunky shadow. Plane copy happens in
* halSurfaceCopyPlanes (called separately by jlSurfaceCopy). */
(void)dst;
(void)src;
}
// On-disk format is the Amiga's native plane-major buffer: planes // On-disk format is the Amiga's native plane-major buffer: planes
// 0..3 written sequentially, AMIGA_PLANE_SIZE bytes each. // 0..3 written sequentially, AMIGA_PLANE_SIZE bytes each.
bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t i; uint8_t i;
@ -2421,7 +2362,7 @@ bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
} }
bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) { bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t i; uint8_t i;
@ -2438,36 +2379,23 @@ bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
} }
uint8_t *halStageAllocPixels(void) { uint8_t *jlpStageAllocPixels(void) {
/* Phase 9: Amiga has no chunky shadow. The stage pixels pointer /* Phase 9: Amiga has no chunky shadow. The stage pixels pointer
* stays NULL; cross-platform code reads pixels via halSamplePixel * stays NULL; cross-platform code reads pixels via jlpSamplePixel
* (or other halXxxChunky hooks) which read from planes. NULL is * (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 stageAlloc treats NULL as
* "port has no chunky storage" and skips the chunky memset. */ * "port has no chunky storage" and skips the chunky memset. */
return NULL; return NULL;
} }
void halStageFreePixels(uint8_t *pixels) { uint8_t *jlpSurfaceAllocPixels(void) {
/* halStageAllocPixels returned NULL on Amiga, so this is always /* Same rationale as jlpStageAllocPixels: no chunky on Amiga. */
* NULL (free(NULL) is well-defined no-op). Symmetric for any
* future port that does allocate stage pixels. */
free(pixels);
}
uint8_t *halSurfaceAllocPixels(void) {
/* Same rationale as halStageAllocPixels: no chunky on Amiga. */
return NULL; return NULL;
} }
void halSurfaceFreePixels(uint8_t *pixels) { uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
free(pixels);
}
uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
if (planeIdx >= AMIGA_BITPLANES) { if (planeIdx >= AMIGA_BITPLANES) {
return NULL; return NULL;
@ -2495,7 +2423,7 @@ uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
// Returns NULL on allocation failure. Cross-platform code stores the // Returns NULL on allocation failure. Cross-platform code stores the
// result in s->portData; primitives access via (AmigaPlanarT *) // result in s->portData; primitives access via (AmigaPlanarT *)
// s->portData. // s->portData.
void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t i; uint16_t i;
@ -2512,7 +2440,7 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
/* Both stage and non-stage: AllocMem fresh planes, MEMF_CLEAR /* Both stage and non-stage: AllocMem fresh planes, MEMF_CLEAR
* for the JoeyLib contract that color 0 = black at surface * for the JoeyLib contract that color 0 = black at surface
* allocation. Stage uses these as off-screen back planes (display * allocation. Stage uses these as off-screen back planes (display
* is gPlanes[], CHIP, updated only by halPresent). Non-stage uses * is gPlanes[], CHIP, updated only by jlpPresent). Non-stage uses
* these as the surface's only planes. * these as the surface's only planes.
* *
* MEMF_CHIP because the bulk plane ops (clear, full-row fill, * MEMF_CHIP because the bulk plane ops (clear, full-row fill,
@ -2544,7 +2472,7 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
} }
void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) { void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t i; uint16_t i;

View file

@ -30,7 +30,7 @@
#include <proto/intuition.h> #include <proto/intuition.h>
#include <proto/lowlevel.h> #include <proto/lowlevel.h>
#include "hal.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
// ----- Constants ----- // ----- Constants -----
@ -242,13 +242,13 @@ static void pollJoysticks(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
void halJoystickReset(jlJoystickE js) { void jlpJoystickReset(jlJoystickE js) {
// Amiga sticks are digital -- no calibration to do. // Amiga sticks are digital -- no calibration to do.
(void)js; (void)js;
} }
void halInputInit(void) { void jlpInputInit(void) {
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
@ -291,13 +291,13 @@ void halInputInit(void) {
} }
void halInputPoll(void) { void jlpInputPoll(void) {
drainMessages(); drainMessages();
pollJoysticks(); pollJoysticks();
} }
void halInputShutdown(void) { void jlpInputShutdown(void) {
if (gLowLevelBase != NULL) { if (gLowLevelBase != NULL) {
CloseLibrary(gLowLevelBase); CloseLibrary(gLowLevelBase);
gLowLevelBase = NULL; gLowLevelBase = NULL;

View file

@ -2,16 +2,16 @@
// running PWM through YM2149 channel-A volume register. // running PWM through YM2149 channel-A volume register.
// //
// Pipeline: // Pipeline:
// * halAudioInit creates an xmp_context, allocates a 2 KB mix buffer // * jlpAudioInit creates an xmp_context, allocates a 2 KB mix buffer
// (in BSS), parks the Timer A vector at our 68k ISR (audio_isr.s), // (in BSS), parks the Timer A vector at our 68k ISR (audio_isr.s),
// and starts Timer A at ~12.3 kHz (MFP prescaler 200, data 1). // and starts Timer A at ~12.3 kHz (MFP prescaler 200, data 1).
// * halAudioPlayMod loads a .MOD via xmp_load_module_from_memory and // * jlpAudioPlayMod loads a .MOD via xmp_load_module_from_memory and
// calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit // calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit
// output that the ISR consumes one byte at a time. // output that the ISR consumes one byte at a time.
// * Each Timer A tick the ISR pops one byte off the buffer, writes // * Each Timer A tick the ISR pops one byte off the buffer, writes
// its high 4 bits to YM register 8 (channel A volume), advances // its high 4 bits to YM register 8 (channel A volume), advances
// the play pointer, raises a refill flag at half/end. The refill // the play pointer, raises a refill flag at half/end. The refill
// itself happens in halAudioFrameTick on the main thread because // itself happens in jlpAudioFrameTick on the main thread because
// libxmp-lite is not interrupt-safe. // libxmp-lite is not interrupt-safe.
// //
// Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by // Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by
@ -25,7 +25,7 @@
#include <libxmp-lite/xmp.h> #include <libxmp-lite/xmp.h>
#include "hal.h" #include "port.h"
#include "audioSfxMixInternal.h" #include "audioSfxMixInternal.h"
#include "joey/audio.h" #include "joey/audio.h"
@ -84,7 +84,7 @@ static int gXmpLoopCount = 0;
static void (*gOldTimerAVec)(void) = NULL; static void (*gOldTimerAVec)(void) = NULL;
// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c). // SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
// Mixed in over libxmp's MOD output during halAudioFrameTick. // Mixed in over libxmp's MOD output during jlpAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS]; static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables). // Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
@ -180,7 +180,7 @@ static void silenceMixBuffer(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halAudioInit(void) { bool jlpAudioInit(void) {
if (gReady) { if (gReady) {
return true; return true;
} }
@ -194,7 +194,7 @@ bool halAudioInit(void) {
} }
void halAudioShutdown(void) { void jlpAudioShutdown(void) {
if (!gReady) { if (!gReady) {
return; return;
} }
@ -217,12 +217,12 @@ void halAudioShutdown(void) {
} }
bool halAudioIsPlayingMod(void) { bool jlpAudioIsPlayingMod(void) {
return gReady && gXmpStarted; return gReady && gXmpStarted;
} }
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gReady || gXmpCtx == NULL) { if (!gReady || gXmpCtx == NULL) {
return; return;
} }
@ -249,7 +249,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
@ -257,7 +257,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
} }
void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
@ -268,7 +268,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
// YM2149 has three PSG voices; a proper tone-mode driver would // YM2149 has three PSG voices; a proper tone-mode driver would
// program voice A here. Not wired yet -- AGI-on-ST will be silent // program voice A here. Not wired yet -- AGI-on-ST will be silent
// until this is filled in. // until this is filled in.
void halAudioTone(uint16_t freqHz) { void jlpAudioTone(uint16_t freqHz) {
(void)freqHz; (void)freqHz;
} }
@ -342,30 +342,30 @@ static long applyYmVoice(void) {
// driving 12 kHz PWM for MOD output (see installTimerA). A future // driving 12 kHz PWM for MOD output (see installTimerA). A future
// AGI-on-ST sound path could lift the YM2149 tone driver onto // AGI-on-ST sound path could lift the YM2149 tone driver onto
// Timer B or C and register it here. // Timer B or C and register it here.
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
(void)fn; (void)fn;
(void)hz; (void)hz;
return false; return false;
} }
void halAudioCriticalEnter(void) { void jlpAudioCriticalEnter(void) {
// No tick-callback ISR is registered (halAudioTickRegister returns // No tick-callback ISR is registered (jlpAudioTickRegister returns
// false). The Timer A MOD ISR synchronizes with the producer via // false). The Timer A MOD ISR synchronizes with the producer via
// the gNeedRefill flags, not this critical section, so masking // the gNeedRefill flags, not this critical section, so masking
// here would only starve that ~12 kHz audio ISR for no benefit. // here would only starve that ~12 kHz audio ISR for no benefit.
} }
void halAudioCriticalExit(void) { void jlpAudioCriticalExit(void) {
} }
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
if (voice >= JOEY_AUDIO_VOICES) { if (voice >= JOEY_AUDIO_VOICES) {
return; return;
} }
// The PWM-via-volume engine in halAudioInit hammers YM reg 8 // The PWM-via-volume engine in jlpAudioInit hammers YM reg 8
// (channel A volume) at 12 kHz for MOD output. If that IRQ is // (channel A volume) at 12 kHz for MOD output. If that IRQ is
// running, it will fight the per-voice writes here. AGI does not // running, it will fight the per-voice writes here. AGI does not
// call jlAudioInit, so Timer A is not installed and the YM is // call jlAudioInit, so Timer A is not installed and the YM is
@ -378,7 +378,7 @@ void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
} }
void halAudioStopMod(void) { void jlpAudioStopMod(void) {
if (!gReady || gXmpCtx == NULL) { if (!gReady || gXmpCtx == NULL) {
return; return;
} }
@ -394,7 +394,7 @@ void halAudioStopMod(void) {
} }
void halAudioStopSfx(uint8_t slot) { void jlpAudioStopSfx(uint8_t slot) {
if (slot >= JOEY_AUDIO_SFX_SLOTS) { if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
@ -412,7 +412,7 @@ void halAudioStopSfx(uint8_t slot) {
// being rewritten (audible tearing). A full MOD-frame mix plus // being rewritten (audible tearing). A full MOD-frame mix plus
// audioSfxOverlayMix can approach that budget under load, so callers // audioSfxOverlayMix can approach that budget under load, so callers
// should run the game loop comfortably faster than ~12 Hz. // should run the game loop comfortably faster than ~12 Hz.
void halAudioFrameTick(void) { void jlpAudioFrameTick(void) {
if (!gReady) { if (!gReady) {
return; return;
} }

View file

@ -1,6 +1,6 @@
| Atari ST word-interleaved planar circle outline -- 68000 hand-rolled. | Atari ST word-interleaved planar circle outline -- 68000 hand-rolled.
| |
| Mirrors src/port/amiga/circle.s in spirit but for ST's single | Mirrors src/amiga/circle.s in spirit but for ST's single
| word-interleaved planar buffer: | word-interleaved planar buffer:
| * Per scanline: 20 groups of 8 bytes; each group is 4 plane | * Per scanline: 20 groups of 8 bytes; each group is 4 plane
| words back-to-back (p0_word, p1_word, p2_word, p3_word). | words back-to-back (p0_word, p1_word, p2_word, p3_word).

View file

@ -5,7 +5,7 @@
// * Word-interleaved ST planar buffer copied to the screen at present. // * Word-interleaved ST planar buffer copied to the screen at present.
// //
// M2.5 scope (per-band palette / SCB emulation): // M2.5 scope (per-band palette / SCB emulation):
// * halPresent scans the jlSurfaceT's SCB array and builds a compact // * jlpPresent scans the jlSurfaceT's SCB array and builds a compact
// transitions table: each entry is (start_line, palette_index) // transitions table: each entry is (start_line, palette_index)
// for a new palette region. For pattern.c's 8 uniform bands this // for a new palette region. For pattern.c's 8 uniform bands this
// is 8 entries; in the worst case it is 200 (one per scanline). // is 8 entries; in the worst case it is 200 (one per scanline).
@ -20,7 +20,7 @@
// ~147-HBL-fires-per-frame cap Hatari's MFP emulation imposes on // ~147-HBL-fires-per-frame cap Hatari's MFP emulation imposes on
// event-count mode, and ~0.2% CPU overhead vs ~60% for per-HBL. // event-count mode, and ~0.2% CPU overhead vs ~60% for per-HBL.
// * gLinePalettes is a flat pre-quantized (line, color)->$0RGB // * gLinePalettes is a flat pre-quantized (line, color)->$0RGB
// table built in halPresent by flattenScbPalettes; the ISR uses // table built in jlpPresent by flattenScbPalettes; the ISR uses
// its first row per band as the source of 16 shifter writes. // its first row per band as the source of 16 shifter writes.
// //
// Deferred: // Deferred:
@ -34,7 +34,7 @@
#include <mint/osbind.h> #include <mint/osbind.h>
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "joey/tile.h" #include "joey/tile.h"
@ -105,7 +105,7 @@ typedef struct {
// Phase 10: planar primitive helpers must be visible everywhere they // Phase 10: planar primitive helpers must be visible everywhere they
// could inline. Defined up here (between StPlanarT and the rest of // could inline. Defined up here (between StPlanarT and the rest of
// the prototype block) so every halFast* / fillSpan / circle walker // the prototype block) so every jlp<Op> / fillSpan / circle walker
// can fold the 4-plane RMW directly into its body. always_inline // can fold the 4-plane RMW directly into its body. always_inline
// hammers the point home for gcc-mint's conservative inliner. // hammers the point home for gcc-mint's conservative inliner.
@ -163,7 +163,7 @@ static void refreshPaletteStateIfNeeded(const
// Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus // Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus
// padding for runtime 256-byte alignment. TOS .PRG format only supports // padding for runtime 256-byte alignment. TOS .PRG format only supports
// 2-byte object-file alignment, so we overallocate and align the first // 2-byte object-file alignment, so we overallocate and align the first
// page pointer manually in halInit; the second page sits exactly one // page pointer manually in jlpInit; the second page sits exactly one
// page (SURFACE_PIXELS_SIZE = 32000, itself a multiple of 256) after // page (SURFACE_PIXELS_SIZE = 32000, itself a multiple of 256) after
// the first, so it stays 256-byte aligned as the shifter requires. // the first, so it stays 256-byte aligned as the shifter requires.
static uint8_t gScreenBuffer[(2 * SURFACE_PIXELS_SIZE) + ST_SCREEN_ALIGN]; static uint8_t gScreenBuffer[(2 * SURFACE_PIXELS_SIZE) + ST_SCREEN_ALIGN];
@ -184,6 +184,15 @@ static int16_t gPrevRez = 0;
static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE]; static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE];
static bool gModeSet = false; static bool gModeSet = false;
// Supervisor-stack token returned by Super(0L) in jlpInit, used to drop
// back to user mode in jlpShutdown. The ST maps the entire $FF8000..
// $FFFFFF I/O region (shifter video-base, palette, MFP) as supervisor-
// only; a GEMDOS .PRG starts in user mode, so the direct register pokes
// in jlpPresent (setShifterVideoBase) bus-error until we enter
// supervisor for the program's lifetime.
static void *gSavedSsp = NULL;
static bool gSuperEntered = false;
// Snapshot of the previous frame's dirty bands. With two physical // Snapshot of the previous frame's dirty bands. With two physical
// pages the off-screen page holds content from TWO frames ago, so each // pages the off-screen page holds content from TWO frames ago, so each
// present must refresh the union(prev, current) of dirty rows from the // present must refresh the union(prev, current) of dirty rows from the
@ -196,7 +205,7 @@ static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT]; static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
// "The previous frame's snapshot recorded at least one dirty row." // "The previous frame's snapshot recorded at least one dirty row."
// Lets halPresent skip the whole 200-row union scan + 200-row snapshot // Lets jlpPresent skip the whole 200-row union scan + 200-row snapshot
// copy on consecutive idle frames: if neither the previous frame nor // copy on consecutive idle frames: if neither the previous frame nor
// the current frame touched any row, union(prev,current) is empty, the // the current frame touched any row, union(prev,current) is empty, the
// off-screen page already matches the displayed page (the settling // off-screen page already matches the displayed page (the settling
@ -213,7 +222,7 @@ static uint16_t gLinePalettes[SURFACE_HEIGHT][SURFACE_COLORS_PER_PALETTE];
// Band-transition table. Each entry is one palette change: at // Band-transition table. Each entry is one palette change: at
// display line gBandStart[i], load palette indexed by gBandPalIdx[i]. // display line gBandStart[i], load palette indexed by gBandPalIdx[i].
// Built once per halPresent from the jlSurfaceT's SCB array. // Built once per jlpPresent from the jlSurfaceT's SCB array.
#define MAX_BANDS SURFACE_HEIGHT #define MAX_BANDS SURFACE_HEIGHT
static uint16_t gBandStart [MAX_BANDS]; static uint16_t gBandStart [MAX_BANDS];
static uint8_t gBandPalIdx[MAX_BANDS]; static uint8_t gBandPalIdx[MAX_BANDS];
@ -277,7 +286,7 @@ static void buildTransitions(const jlSurfaceT *src) {
// Pre-quantize every palette row indexed by scanline through the SCB // Pre-quantize every palette row indexed by scanline through the SCB
// into gLinePalettes, so the Timer B ISR can do a flat indexed copy // into gLinePalettes, so the Timer B ISR can do a flat indexed copy
// without any surface-level lookups. Called once per halPresent. // without any surface-level lookups. Called once per jlpPresent.
static void flattenScbPalettes(const jlSurfaceT *src) { static void flattenScbPalettes(const jlSurfaceT *src) {
uint16_t line; uint16_t line;
uint16_t col; uint16_t col;
@ -490,7 +499,7 @@ static void vblIsr(void) {
// bytes. The low byte does not exist on STF (base is 256-aligned), so // bytes. The low byte does not exist on STF (base is 256-aligned), so
// two byte writes fully specify the address. The shifter latches the // two byte writes fully specify the address. The shifter latches the
// base at the next frame start, so calling this mid-visible-frame (as // base at the next frame start, so calling this mid-visible-frame (as
// halPresent does, after the app's jlWaitVBL) makes the flip take // jlpPresent does, after the app's jlWaitVBL) makes the flip take
// effect on the following frame with no tearing. No TOS / Setscreen // effect on the following frame with no tearing. No TOS / Setscreen
// involvement, which matters because our VBL ISR replaced TOS's and // involvement, which matters because our VBL ISR replaced TOS's and
// does not run TOS's Setscreen-latch path. // does not run TOS's Setscreen-latch path.
@ -532,12 +541,26 @@ static void writeDiagnostics(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halInit(const jlConfigT *config) { bool jlpInit(const jlConfigT *config) {
uintptr_t addr; uintptr_t addr;
uint16_t i; uint16_t i;
(void)config; (void)config;
// Enter supervisor mode for the program's lifetime. The shifter
// video-base, palette, and MFP registers live in the supervisor-only
// $FF8000..$FFFFFF I/O region; jlpPresent pokes the video-base bytes
// directly every flip, and a .PRG launched by GEMDOS starts in user
// mode, so without this the first present bus-errors. Staying super
// for the whole run (vs a per-present Supexec trap) also keeps the
// present hot path free of trap overhead. jlpShutdown restores user
// mode via the saved stack token. GEMDOS/XBIOS/BIOS traps (including
// the stdio used by joeylog) work fine from supervisor.
if (!gSuperEntered) {
gSavedSsp = (void *)Super(0L);
gSuperEntered = true;
}
// Align page 0 to 256 bytes inside the static storage; page 1 // Align page 0 to 256 bytes inside the static storage; page 1
// follows exactly one 32000-byte (256-aligned) page later, so it // follows exactly one 32000-byte (256-aligned) page later, so it
// is 256-aligned too. The shifter only displays 256-byte-aligned // is 256-aligned too. The shifter only displays 256-byte-aligned
@ -570,7 +593,7 @@ bool halInit(const jlConfigT *config) {
} }
// Switch to ST low-res: 320x200x16, mode 0. Display page 0 first; // Switch to ST low-res: 320x200x16, mode 0. Display page 0 first;
// halPresent flips to the off-screen page via the shifter base // jlpPresent flips to the off-screen page via the shifter base
// registers from here on. // registers from here on.
Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0); Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0);
gModeSet = true; gModeSet = true;
@ -596,12 +619,7 @@ bool halInit(const jlConfigT *config) {
} }
const char *halLastError(void) { void jlpPresent(const jlSurfaceT *src) {
return NULL;
}
void halPresent(const jlSurfaceT *src) {
StPlanarT *pd; StPlanarT *pd;
uint8_t *backPage; uint8_t *backPage;
uint8_t backIdx; uint8_t backIdx;
@ -778,7 +796,7 @@ void halPresent(const jlSurfaceT *src) {
// Snapshot this frame's dirty bands as "previous" for the next // Snapshot this frame's dirty bands as "previous" for the next
// present. jlStagePresent clears the live bands right after // present. jlStagePresent clears the live bands right after
// halPresent returns, so we must grab them now. Next present unions // jlpPresent returns, so we must grab them now. Next present unions
// these with that frame's dirty rows to refresh whatever the other // these with that frame's dirty rows to refresh whatever the other
// page is stale on. Record whether any row was dirty so the next // page is stale on. Record whether any row was dirty so the next
// present can take the idle-frame early-out above. // present can take the idle-frame early-out above.
@ -798,7 +816,7 @@ void halPresent(const jlSurfaceT *src) {
// Vsync() is XBIOS opcode 37; mintlib exposes it directly. It blocks // Vsync() is XBIOS opcode 37; mintlib exposes it directly. It blocks
// until the next 50 Hz (PAL) or 60 Hz (NTSC) vertical blank. // until the next 50 Hz (PAL) or 60 Hz (NTSC) vertical blank.
void halWaitVBL(void) { void jlpWaitVBL(void) {
int16_t before; int16_t before;
// Can't use Vsync(): TOS's Vsync increments _vblsem inside its // Can't use Vsync(): TOS's Vsync increments _vblsem inside its
@ -815,12 +833,12 @@ void halWaitVBL(void) {
// gFrameCount is already maintained by our VBL ISR; just narrow to // gFrameCount is already maintained by our VBL ISR; just narrow to
// uint16_t for the cross-port HAL contract. // uint16_t for the cross-port HAL contract.
uint16_t halFrameCount(void) { uint16_t jlpFrameCount(void) {
return (uint16_t)gFrameCount; return (uint16_t)gFrameCount;
} }
uint16_t halFrameHz(void) { uint16_t jlpFrameHz(void) {
/* PAL ST is 50 Hz; NTSC ST and SM124 mono are ~60 / ~70. We /* PAL ST is 50 Hz; NTSC ST and SM124 mono are ~60 / ~70. We
* report 50 as the baseline -- close enough for ops/sec scaling, * report 50 as the baseline -- close enough for ops/sec scaling,
* and the actual frame rate is still observable via iter counts. */ * and the actual frame rate is still observable via iter counts. */
@ -839,7 +857,7 @@ static long readHz200(void) {
return (long)*((volatile uint32_t *)0x4BAUL); return (long)*((volatile uint32_t *)0x4BAUL);
} }
uint32_t halMillisElapsed(void) { uint32_t jlpMillisElapsed(void) {
uint32_t ticks; uint32_t ticks;
ticks = (uint32_t)Supexec(readHz200); ticks = (uint32_t)Supexec(readHz200);
// 0 is a legal counter value (power-on tick 0 and every ~248-day // 0 is a legal counter value (power-on tick 0 and every ~248-day
@ -855,19 +873,19 @@ uint32_t halMillisElapsed(void) {
} }
void halShutdown(void) { void jlpShutdown(void) {
if (!gModeSet) { if (!gModeSet) {
return; return;
} }
// Stop the audio Timer A first. The audio HAL has its own // Stop the audio Timer A first. The audio HAL has its own
// halAudioShutdown that disables Timer A and restores the vector, // jlpAudioShutdown that disables Timer A and restores the vector,
// but cross-platform jlShutdown doesn't call it -- if a sketch // but cross-platform jlShutdown doesn't call it -- if a sketch
// forgets jlAudioShutdown(), Timer A keeps firing after our // forgets jlAudioShutdown(), Timer A keeps firing after our
// code unloads and TOS panics on the first dangling vector hit. // code unloads and TOS panics on the first dangling vector hit.
// Calling halAudioShutdown here is idempotent (gReady guard), // Calling jlpAudioShutdown here is idempotent (gReady guard),
// so explicit-shutdown sketches still work. // so explicit-shutdown sketches still work.
halAudioShutdown(); jlpAudioShutdown();
// Disable MFP Timer B and restore the exception vectors before // Disable MFP Timer B and restore the exception vectors before
// changing the screen -- a late ISR firing mid-Setscreen would // changing the screen -- a late ISR firing mid-Setscreen would
@ -884,6 +902,13 @@ void halShutdown(void) {
Supexec(writePrevPaletteRegs); Supexec(writePrevPaletteRegs);
writeDiagnostics(); writeDiagnostics();
gModeSet = false; gModeSet = false;
// Drop back to user mode so control returns to GEMDOS the way it
// launched us (mirrors the Super(0L) in jlpInit).
if (gSuperEntered) {
(void)Super(gSavedSsp);
gSuperEntered = false;
}
} }
@ -907,14 +932,15 @@ extern void surface68kStSpriteRestoreByteAligned(uint8_t *base, uint16_t x, uint
// Phase 9: clear the entire planar buffer to a 4-bit color. Build an // Phase 9: clear the entire planar buffer to a 4-bit color. Build an
// 8-byte group template (4 plane words: 0xFFFF or 0x0000 each by // 8-byte group template (4 plane words: 0xFFFF or 0x0000 each by
// color bit) then stream it across all 4000 groups via long stores. // color bit) then stream it across all 4000 groups via long stores.
bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) { void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
StPlanarT *pd; StPlanarT *pd;
uint8_t color; uint8_t color;
uint32_t loLong; uint32_t loLong;
uint32_t hiLong; uint32_t hiLong;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; jlpGenericSurfaceClear(s, doubled);
return;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
color = (uint8_t)(doubled & 0x0Fu); color = (uint8_t)(doubled & 0x0Fu);
@ -927,62 +953,20 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
surface68kStLongFill(pd->base, surface68kStLongFill(pd->base,
(uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP), (uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP),
loLong, hiLong); loLong, hiLong);
return true;
}
// Phase 9: pure short-circuit. halFillRectPlanes (called by cross-
// platform jlFillRect right after this) does the actual planar fill;
// we just claim ownership so the chunky fillRectClipped fallback
// never runs.
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
if (s->portData == NULL) {
return false;
}
return true;
}
// Phase 9: claim every halFastTile* call so the cross-platform chunky
// fallback (which would dereference NULL s->pixels) never fires. The
// halTileXxxPlanes hook called separately by tile.c does the planar
// work.
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
(void)dstRow0; (void)srcRow0;
return true;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
(void)dstRow0; (void)srcRow0; (void)transparent;
return true;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
(void)dstRow0; (void)srcTilePixels;
return true;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
(void)dstTilePixels; (void)srcRow0;
return true;
} }
// Phase 9: planar-only. Chunky shadow is gone; only the planar buffer // Phase 9: planar-only. Chunky shadow is gone; only the planar buffer
// gets the pixel. // gets the pixel.
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; jlpGenericDrawPixel(s, x, y, colorIndex);
return;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu)); stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
return true;
} }
@ -1050,7 +1034,7 @@ static void stPlanarCircleOutline(StPlanarT *pd, int16_t cx, int16_t cy, uint16_
// Phase 10: group-aware span fill -- the same leading-mask / // Phase 10: group-aware span fill -- the same leading-mask /
// full-group / trailing-mask decomposition halFillRectPlanes uses, // full-group / trailing-mask decomposition jlpFillRect uses,
// but for one row. Replaces the per-pixel walk that gave jlFillCircle // but for one row. Replaces the per-pixel walk that gave jlFillCircle
// r=40 ~1 ops/sec. // r=40 ~1 ops/sec.
static void stPlanarFillSpan(StPlanarT *pd, int16_t x0, int16_t x1, int16_t y, uint8_t color) { static void stPlanarFillSpan(StPlanarT *pd, int16_t x0, int16_t x1, int16_t y, uint8_t color) {
@ -1092,10 +1076,10 @@ static void stPlanarCircleFill(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r
} }
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; return;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
// Asm walker assumes fully on-surface; partial-clip lines fall // Asm walker assumes fully on-surface; partial-clip lines fall
@ -1106,14 +1090,13 @@ bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t
} else { } else {
stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu)); stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
} }
return true;
} }
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; return;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
/* Off-surface circles fall back to the per-pixel C walker which /* Off-surface circles fall back to the per-pixel C walker which
@ -1128,11 +1111,10 @@ bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
surface68kStCircleOutline(pd->base, (uint16_t)cx, (uint16_t)cy, r, surface68kStCircleOutline(pd->base, (uint16_t)cx, (uint16_t)cy, r,
(uint8_t)(colorIndex & 0x0Fu)); (uint8_t)(colorIndex & 0x0Fu));
} }
return true;
} }
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s->portData == NULL) { if (s->portData == NULL) {
return false; return false;
@ -1153,91 +1135,6 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
} }
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)row;
(void)startX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)s; (void)startX; (void)y; (void)matchColor; (void)newColor; (void)matchEqual;
(void)seedMatched; (void)leftXOut; (void)rightXOut;
return false;
}
bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)s; (void)leftX; (void)rightX; (void)scanY; (void)matchColor; (void)newColor; (void)matchEqual;
(void)markBuf;
return false;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)markBuf;
return false;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)scanY;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
return false;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)pixels;
(void)x;
(void)y;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
// Phase 9: short-circuit. halTileFillPlanes does the planar work
// after this returns true; the chunky fallback that would run on
// false would dereference NULL s->pixels.
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)bx; (void)by; (void)fillWord;
if (s->portData == NULL) {
return false;
}
return true;
}
// Phase 2: allocate a shadow word-interleaved planar buffer per // Phase 2: allocate a shadow word-interleaved planar buffer per
// surface. Both stage and non-stage get their own buffer; the two // surface. Both stage and non-stage get their own buffer; the two
// displayed pages (gScreenPage[0/1]) are the flip targets, kept // displayed pages (gScreenPage[0/1]) are the flip targets, kept
@ -1252,7 +1149,7 @@ bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
// Symptom of getting this wrong: intermittent return-to-desktop // Symptom of getting this wrong: intermittent return-to-desktop
// after the red startup paint as the first long write hits an // after the red startup paint as the first long write hits an
// odd-by-2 base. // odd-by-2 base.
void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
StPlanarT *pd; StPlanarT *pd;
uint8_t *raw; uint8_t *raw;
uintptr_t addr; uintptr_t addr;
@ -1280,7 +1177,7 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
} }
void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) { void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
StPlanarT *pd; StPlanarT *pd;
(void)s; (void)s;
@ -1312,12 +1209,16 @@ void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
// (stApplyMaskToGroup is defined inline near the top of the file.) // (stApplyMaskToGroup is defined inline near the top of the file.)
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
uint16_t groupFirst; uint16_t groupFirst;
uint16_t groupLast; uint16_t groupLast;
uint8_t *rowBase; uint8_t *rowBase;
if (s->portData == NULL) {
jlpGenericFillRect(s, x, y, w, h, colorIndex);
return;
}
if (s == NULL || w == 0u || h == 0u) { if (s == NULL || w == 0u || h == 0u) {
return; return;
} }
@ -1363,7 +1264,7 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
} }
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
StPlanarT *dstPd; StPlanarT *dstPd;
StPlanarT *srcPd; StPlanarT *srcPd;
@ -1399,12 +1300,16 @@ void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
// pixel (bx*8, by*8). 8 pixels wide always covers exactly half a // 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 // 16-pixel group: high half (bits 15..8) when bx is even, low half
// (bits 7..0) when bx is odd. Per-row work is 4 plane half-word RMWs. // (bits 7..0) when bx is odd. Per-row work is 4 plane half-word RMWs.
void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { void jlpTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
uint16_t group; uint16_t group;
uint16_t halfMask; uint16_t halfMask;
uint8_t *gp; uint8_t *gp;
if (s->portData == NULL) {
jlpGenericTileFill(s, bx, by, colorIndex);
return;
}
if (s == NULL) { if (s == NULL) {
return; return;
} }
@ -1426,13 +1331,17 @@ void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex
// positions. Width 8 = single tile column = single half-group // positions. Width 8 = single tile column = single half-group
// write per plane. The asm walker handles 8 rows just as well // write per plane. The asm walker handles 8 rows just as well
// as a sprite's variable height. // as a sprite's variable height.
void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *tileBytes) { void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
StPlanarT *pd; StPlanarT *pd;
uint16_t group; uint16_t group;
uint8_t *dstAddr; uint8_t *dstAddr;
int16_t row; int16_t row;
if (dst == NULL || tileBytes == NULL) { if (dst->portData == NULL) {
jlpGenericTilePaste(dst, bx, by, chunkyTile);
return;
}
if (dst == NULL || chunkyTile == NULL) {
return; return;
} }
pd = (StPlanarT *)dst->portData; pd = (StPlanarT *)dst->portData;
@ -1442,7 +1351,7 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *
/* Phase 10.5: jlTileT.pixels holds plane-major bytes (4 plane bytes /* Phase 10.5: jlTileT.pixels holds plane-major bytes (4 plane bytes
* per row * 8 rows = 32 bytes). Direct byte copy to the planar * per row * 8 rows = 32 bytes). Direct byte copy to the planar
* buffer; no chunky <-> planar conversion. Mirrors the sibling * buffer; no chunky <-> planar conversion. Mirrors the sibling
* halTileCopyPlanes pattern but reads from the contiguous tile * jlpTileCopy pattern but reads from the contiguous tile
* buffer. Drops the asm-walker entry/exit overhead. */ * buffer. Drops the asm-walker entry/exit overhead. */
group = (uint16_t)((uint16_t)bx >> 1); group = (uint16_t)((uint16_t)bx >> 1);
dstAddr = pd->base dstAddr = pd->base
@ -1452,12 +1361,12 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *
(void)row; (void)row;
#define ST_TILE_PASTE_ROW \ #define ST_TILE_PASTE_ROW \
do { \ do { \
dstAddr[0] = tileBytes[0]; \ dstAddr[0] = chunkyTile[0]; \
dstAddr[2] = tileBytes[1]; \ dstAddr[2] = chunkyTile[1]; \
dstAddr[4] = tileBytes[2]; \ dstAddr[4] = chunkyTile[2]; \
dstAddr[6] = tileBytes[3]; \ dstAddr[6] = chunkyTile[3]; \
dstAddr += ST_BYTES_PER_ROW; \ dstAddr += ST_BYTES_PER_ROW; \
tileBytes += TILE_BYTES_PER_ROW; \ chunkyTile += TILE_BYTES_PER_ROW; \
} while (0) } while (0)
ST_TILE_PASTE_ROW; ST_TILE_PASTE_ROW;
ST_TILE_PASTE_ROW; ST_TILE_PASTE_ROW;
@ -1472,7 +1381,7 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *
/* Planar monochrome paste. monoTile follows the ST tile layout from /* Planar monochrome paste. monoTile follows the ST tile layout from
* halTileSnapPlanes: row-major-with-planes-interleaved-per-row, i.e. * jlpTileSnap: row-major-with-planes-interleaved-per-row, i.e.
* row 0 plane 0 at byte 0 * row 0 plane 0 at byte 0
* row 0 plane 1 at byte 1 * row 0 plane 1 at byte 1
* row 0 plane 2 at byte 2 * row 0 plane 2 at byte 2
@ -1489,7 +1398,7 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *
* inside the 16-pixel group starting at (bx/2)*8 bytes from the * inside the 16-pixel group starting at (bx/2)*8 bytes from the
* row's base. bx odd selects the second byte of the plane word * row's base. bx odd selects the second byte of the plane word
* (low 8 pixels of the 16-pixel group). */ * (low 8 pixels of the 16-pixel group). */
void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
StPlanarT *pd; StPlanarT *pd;
uint16_t group; uint16_t group;
uint8_t *dstAddr; uint8_t *dstAddr;
@ -1498,6 +1407,10 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
uint8_t masks_fg[4]; uint8_t masks_fg[4];
uint8_t masks_bg[4]; uint8_t masks_bg[4];
if (dst->portData == NULL) {
jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor);
return;
}
if (dst == NULL || monoTile == NULL) { if (dst == NULL || monoTile == NULL) {
return; return;
} }
@ -1532,13 +1445,17 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
} }
void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *tileOut) { void jlpTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) {
const StPlanarT *pd; const StPlanarT *pd;
uint16_t group; uint16_t group;
const uint8_t *srcAddr; const uint8_t *srcAddr;
int16_t row; int16_t row;
if (src == NULL || tileOut == NULL) { if (src->portData == NULL) {
jlpGenericTileSnap(src, bx, by, chunkyOut);
return;
}
if (src == NULL || chunkyOut == NULL) {
return; return;
} }
pd = (const StPlanarT *)src->portData; pd = (const StPlanarT *)src->portData;
@ -1554,12 +1471,12 @@ void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *t
(void)row; (void)row;
#define ST_TILE_SNAP_ROW \ #define ST_TILE_SNAP_ROW \
do { \ do { \
tileOut[0] = srcAddr[0]; \ chunkyOut[0] = srcAddr[0]; \
tileOut[1] = srcAddr[2]; \ chunkyOut[1] = srcAddr[2]; \
tileOut[2] = srcAddr[4]; \ chunkyOut[2] = srcAddr[4]; \
tileOut[3] = srcAddr[6]; \ chunkyOut[3] = srcAddr[6]; \
srcAddr += ST_BYTES_PER_ROW; \ srcAddr += ST_BYTES_PER_ROW; \
tileOut += TILE_BYTES_PER_ROW; \ chunkyOut += TILE_BYTES_PER_ROW; \
} while (0) } while (0)
ST_TILE_SNAP_ROW; ST_TILE_SNAP_ROW;
ST_TILE_SNAP_ROW; ST_TILE_SNAP_ROW;
@ -1578,7 +1495,7 @@ void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *t
// We just byte-copy 4 plane bytes per row -- no chunky scratch, no // We just byte-copy 4 plane bytes per row -- no chunky scratch, no
// bit transpose, no LUT. ~640 cyc per tile vs ~5000 cyc for the // bit transpose, no LUT. ~640 cyc per tile vs ~5000 cyc for the
// snap+paste path. // snap+paste path.
void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { void jlpTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
StPlanarT *dstPd; StPlanarT *dstPd;
const StPlanarT *srcPd; const StPlanarT *srcPd;
uint8_t *dstAddr; uint8_t *dstAddr;
@ -1587,6 +1504,10 @@ void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu
uint16_t dstGroup; uint16_t dstGroup;
int16_t row; int16_t row;
if (dst->portData == NULL || src->portData == NULL) {
jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
return;
}
if (dst == NULL || src == NULL) { if (dst == NULL || src == NULL) {
return; return;
} }
@ -1630,7 +1551,7 @@ void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu
} }
void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) { void jlpTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) {
StPlanarT *dstPd; StPlanarT *dstPd;
uint8_t scratch[TILE_BYTES]; uint8_t scratch[TILE_BYTES];
int16_t row; int16_t row;
@ -1650,6 +1571,10 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
uint8_t mask; uint8_t mask;
uint8_t notMask; uint8_t notMask;
if (dst->portData == NULL) {
jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent);
return;
}
if (dst == NULL || src == NULL) { if (dst == NULL || src == NULL) {
return; return;
} }
@ -1665,7 +1590,7 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
* *
* For transparent=0 this collapses to mask = p0|p1|p2|p3. * For transparent=0 this collapses to mask = p0|p1|p2|p3.
* Replaces the prior 64-iteration per-pixel SetPixel walker. */ * Replaces the prior 64-iteration per-pixel SetPixel walker. */
halTileSnapPlanes(src, srcBx, srcBy, scratch); jlpTileSnap(src, srcBx, srcBy, scratch);
dstX0 = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE); dstX0 = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstY0 = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE); dstY0 = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
group = (uint16_t)(dstX0 >> 4); group = (uint16_t)(dstX0 >> 4);
@ -1673,10 +1598,10 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
dstByte = dstPd->base + dstY0 * ST_BYTES_PER_ROW dstByte = dstPd->base + dstY0 * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP + halfOff; + group * ST_BYTES_PER_GROUP + halfOff;
xK0 = (transparentIndex & 1u) ? 0xFFu : 0u; xK0 = (transparent & 1u) ? 0xFFu : 0u;
xK1 = (transparentIndex & 2u) ? 0xFFu : 0u; xK1 = (transparent & 2u) ? 0xFFu : 0u;
xK2 = (transparentIndex & 4u) ? 0xFFu : 0u; xK2 = (transparent & 4u) ? 0xFFu : 0u;
xK3 = (transparentIndex & 8u) ? 0xFFu : 0u; xK3 = (transparent & 8u) ? 0xFFu : 0u;
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) { for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
p0 = scratch[row * 4 + 0]; p0 = scratch[row * 4 + 0];
@ -1791,7 +1716,7 @@ static void stSpriteDrawByteAligned(StPlanarT *pd, const jlSpriteT *sp, int16_t
// stPlanarSetPixel body so there's no nested function entry / y*160 // stPlanarSetPixel body so there's no nested function entry / y*160
// re-derivation. Major rewrite of the dispatcher path that drove // re-derivation. Major rewrite of the dispatcher path that drove
// the 0.06x sprite gap before this commit. // the 0.06x sprite gap before this commit.
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) { void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
StPlanarT *pd; StPlanarT *pd;
int16_t spritePxStart; int16_t spritePxStart;
int16_t spritePyStart; int16_t spritePyStart;
@ -1943,7 +1868,7 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
// can compute base+row*160 once per row and just do per-pixel // can compute base+row*160 once per row and just do per-pixel
// (group, bitMask, 4 plane RMW). 2x speedup over the per-pixel // (group, bitMask, 4 plane RMW). 2x speedup over the per-pixel
// stPlanarSetPixel form. // stPlanarSetPixel form.
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const StPlanarT *pd; const StPlanarT *pd;
int16_t row; int16_t row;
int16_t pair; int16_t pair;
@ -2011,7 +1936,7 @@ void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
} }
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
StPlanarT *pd; StPlanarT *pd;
int16_t row; int16_t row;
int16_t pair; int16_t pair;
@ -2085,7 +2010,7 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
// (s->pixels) since that's the source-of-truth during transition. // (s->pixels) since that's the source-of-truth during transition.
// Once Phase 9 sets s->pixels = NULL the planar shadow becomes // Once Phase 9 sets s->pixels = NULL the planar shadow becomes
// authoritative and we walk the 4 plane bits at (x, y). // authoritative and we walk the 4 plane bits at (x, y).
uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
if (s->pixels != NULL) { if (s->pixels != NULL) {
uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)]; uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
if (x & 1) return (uint8_t)(byte & 0x0Fu); if (x & 1) return (uint8_t)(byte & 0x0Fu);
@ -2104,7 +2029,7 @@ uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
// Derive 160 chunky bytes per row from the word-interleaved planar // Derive 160 chunky bytes per row from the word-interleaved planar
// buffer (20 groups x 4 plane words). Same shape as the Amiga's // buffer (20 groups x 4 plane words). Same shape as the Amiga's
// amigaPlanesToChunkyRow but per-group instead of per-byte. Used by // amigaPlanesToChunkyRow but per-group instead of per-byte. Used by
// halSurfaceHash to fold the planar surface into the same byte stream // jlpSurfaceHash to fold the planar surface into the same byte stream
// the chunky ports hash, so cross-port hash comparisons stay valid. // the chunky ports hash, so cross-port hash comparisons stay valid.
static void stPlanarToChunkyRow(const StPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) { static void stPlanarToChunkyRow(const StPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) {
uint16_t group; uint16_t group;
@ -2134,7 +2059,7 @@ static void stPlanarToChunkyRow(const StPlanarT *pd, int16_t y, uint8_t *dstChun
} }
uint32_t halSurfaceHash(const jlSurfaceT *s) { uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
StPlanarT *pd; StPlanarT *pd;
uint16_t lo = 0xACE1u; uint16_t lo = 0xACE1u;
uint16_t hi = 0x1357u; uint16_t hi = 0x1357u;
@ -2177,19 +2102,9 @@ uint32_t halSurfaceHash(const jlSurfaceT *s) {
} }
// Phase 9: planar-only. The chunky shadow is gone; surface copy is
// 32000 bytes of planar data. halSurfaceCopyPlanes already handles
// the planar copy via memcpy of pd->base. This stub only guards the
// pre-Phase-9 contract; cross-platform jlSurfaceCopy still calls both
// halSurfaceCopyChunky and halSurfaceCopyPlanes.
void halSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
(void)dst; (void)src;
}
// On-disk format is the ST's native interleaved planar buffer; one // On-disk format is the ST's native interleaved planar buffer; one
// fread fills it directly, no chunky scratch or c2p step. // fread fills it directly, no chunky scratch or c2p step.
bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
StPlanarT *pd; StPlanarT *pd;
pd = (StPlanarT *)dst->portData; pd = (StPlanarT *)dst->portData;
@ -2200,7 +2115,7 @@ bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
} }
bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) { bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
StPlanarT *pd; StPlanarT *pd;
pd = (StPlanarT *)src->portData; pd = (StPlanarT *)src->portData;
@ -2213,16 +2128,11 @@ bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
// Phase 9: no chunky storage on the ST. Cross-platform code treats // Phase 9: no chunky storage on the ST. Cross-platform code treats
// NULL as "port has no chunky shadow" (same contract Amiga uses). // NULL as "port has no chunky shadow" (same contract Amiga uses).
uint8_t *halSurfaceAllocPixels(void) { uint8_t *jlpSurfaceAllocPixels(void) {
return NULL; return NULL;
} }
void halSurfaceFreePixels(uint8_t *pixels) {
free(pixels); /* free(NULL) is a no-op; symmetric for non-planar ports. */
}
// ST is word-interleaved: one buffer holds all 4 planes per group // ST is word-interleaved: one buffer holds all 4 planes per group
// back-to-back. There's no per-plane base, but we overload planeIdx // back-to-back. There's no per-plane base, but we overload planeIdx
// 0 to return the single buffer base so the cross-platform sprite // 0 to return the single buffer base so the cross-platform sprite
@ -2230,7 +2140,7 @@ void halSurfaceFreePixels(uint8_t *pixels) {
// routine, which computes plane offsets internally via d16(a0) // routine, which computes plane offsets internally via d16(a0)
// chains. planeIdx >= 1 returns NULL since they don't make sense // chains. planeIdx >= 1 returns NULL since they don't make sense
// in interleaved layout. // in interleaved layout.
uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) { uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
StPlanarT *pd; StPlanarT *pd;
if (s == NULL || planeIdx != 0u) { if (s == NULL || planeIdx != 0u) {
@ -2246,11 +2156,7 @@ uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
// Phase 9: stage has no chunky shadow either. Cross-platform stageAlloc // Phase 9: stage has no chunky shadow either. Cross-platform stageAlloc
// stores NULL in s->pixels and skips the chunky memset. // stores NULL in s->pixels and skips the chunky memset.
uint8_t *halStageAllocPixels(void) { uint8_t *jlpStageAllocPixels(void) {
return NULL; return NULL;
} }
void halStageFreePixels(uint8_t *pixels) {
free(pixels);
}

View file

@ -14,17 +14,17 @@
// consume the documented byte count to stay in sync. // consume the documented byte count to stay in sync.
// //
// The ISR writes into private gIsrState (keyboard) and gIsrMouse* // The ISR writes into private gIsrState (keyboard) and gIsrMouse*
// buffers; halInputPoll copies them into the public globals. // buffers; jlpInputPoll copies them into the public globals.
// jlKeyPressed edge detection requires that public gKeyState only // jlKeyPressed edge detection requires that public gKeyState only
// advance during halInputPoll, never at interrupt time -- jlInputPoll // advance during jlpInputPoll, never at interrupt time -- jlInputPoll
// snapshots gKeyState into gKeyPrev before halInputPoll runs. // snapshots gKeyState into gKeyPrev before jlpInputPoll runs.
#include <string.h> #include <string.h>
#include <mint/osbind.h> #include <mint/osbind.h>
#include <mint/ostruct.h> #include <mint/ostruct.h>
#include "hal.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -166,7 +166,7 @@ static int16_t gMouseAbsY = SURFACE_HEIGHT / 2;
// Latched joystick state, written by the IKBD ISR. The byte stays // Latched joystick state, written by the IKBD ISR. The byte stays
// unchanged between packets while a direction or fire is held, so // unchanged between packets while a direction or fire is held, so
// halInputPoll can simply read the latest value. // jlpInputPoll can simply read the latest value.
static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT]; static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT];
// ----- Internal helpers ----- // ----- Internal helpers -----
@ -283,13 +283,13 @@ static long restoreIkbdVector(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
void halJoystickReset(jlJoystickE js) { void jlpJoystickReset(jlJoystickE js) {
// Atari ST sticks are digital -- no calibration to do. // Atari ST sticks are digital -- no calibration to do.
(void)js; (void)js;
} }
void halInputInit(void) { void jlpInputInit(void) {
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrState, 0, sizeof(gIsrState));
@ -320,7 +320,7 @@ void halInputInit(void) {
// gIsrJoyByte[] at ~100 Hz max; the ~60-byte memcpy is essentially // gIsrJoyByte[] at ~100 Hz max; the ~60-byte memcpy is essentially
// never racing a write. Worst case is a single key or one packet // never racing a write. Worst case is a single key or one packet
// lagging one frame -- well under perceptible. // lagging one frame -- well under perceptible.
void halInputPoll(void) { void jlpInputPoll(void) {
int32_t dx; int32_t dx;
int32_t dy; int32_t dy;
int32_t newX; int32_t newX;
@ -370,7 +370,7 @@ void halInputPoll(void) {
} }
void halInputShutdown(void) { void jlpInputShutdown(void) {
if (!gHooked) { if (!gHooked) {
return; return;
} }

View file

@ -537,7 +537,7 @@ _surface68kStFillRectSingleGroup:
| |
| Specialized 8x8 single-group fill: 16-way color dispatch + 8 rows | Specialized 8x8 single-group fill: 16-way color dispatch + 8 rows
| fully unrolled. Drops the per-row subq+bne overhead that the | fully unrolled. Drops the per-row subq+bne overhead that the
| generic FRG_LOOP pays. Used by halTileFillPlanes. | generic FRG_LOOP pays. Used by jlpTileFill.
| |
| void surface68kStTileFill8x8(uint8_t *firstGroupPtr, | void surface68kStTileFill8x8(uint8_t *firstGroupPtr,
| uint16_t mask, | uint16_t mask,

135
src/blank/blank.c Normal file
View file

@ -0,0 +1,135 @@
// JoeyLib new-port template ("jBlank").
//
// Copy this directory to src/<yourplatform>/, set JOEYLIB_PLATFORM_<YOURS> in
// include/joey/platform.h (mirror the JOEYLIB_PLATFORM_BLANK blocks there), and
// fill in the TODOs below. Build with -DJOEYLIB_PLATFORM_<YOURS>.
//
// HOW THE PORT MODEL WORKS
// -----------------------
// JoeyLib's public API (jlDrawPixel, jlFillRect, jlSpriteDraw, ...) is
// implemented ONCE in src/core/*.c. Each overridable operation is a jlp<Op>()
// resolved at COMPILE TIME: a portable-C default lives in src/generic/ and your
// port supplies its own version ONLY when it defines JL_HAS_<OP> in platform.h.
// There is no runtime dispatch.
//
// The payoff for a new port: you get ALL of the graphics for free. The generic
// backend (src/generic/genericDraw|Tile|Sprite|Surface.c) is a complete chunky
// 4bpp software renderer -- draw primitives, tiles, sprites, surface readers,
// allocation. A new CHUNKY port renders correctly before you write a single
// line of graphics code. You only have to implement the platform SERVICES below
// (whose generics are no-op stubs): bring up the display, blit the stage to it,
// read input, and report timing. Wire up audio whenever you like.
//
// 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.
#include "port.h"
#include "surfaceInternal.h" // jlSurfaceT: ->pixels (chunky), ->scb, ->palette
#include "inputInternal.h" // gKeyState[] etc. for jlpInputPoll
// ----- Lifecycle -----------------------------------------------------------
// Called once from jlInit after config is stored, before any surfaces exist.
// Bring up your video mode and allocate any hardware-adjacent buffers. Return
// true on success (false aborts jlInit). jlpLastError may be set on failure.
bool jlpInit(const jlConfigT *config) {
(void)config;
// TODO: program your display into a 320x200, 16-colors-per-scanline mode
// (or the closest your hardware offers) and prepare to present.
return true;
}
// Called from jlShutdown. Undo jlpInit: restore the prior video mode and free
// whatever you allocated.
void jlpShutdown(void) {
// TODO: restore the display.
}
// ----- Present -------------------------------------------------------------
// Copy the library-owned stage surface to the visible display. This is the one
// service every port MUST get right. The stage is chunky 4bpp:
// src->pixels : SURFACE_PIXELS_SIZE bytes, 2 pixels per byte (high nibble is
// the LEFT pixel), SURFACE_BYTES_PER_ROW per scanline.
// src->scb[y] : palette index 0..15 selecting which of the 16 palettes row
// y uses (SCB = scanline control byte).
// src->palette[p][c] : 16 palettes x 16 colors, each a $0RGB 4-bit-channel
// value. Expand to your hardware's color depth.
// jlStagePresent has already walked the per-row dirty bands; for a first cut
// just blit the whole frame, then optimize to dirty rows later (see the dirty
// arrays in surfaceInternal.h).
void jlpPresent(const jlSurfaceT *src) {
(void)src;
// TODO: for each scanline y, look up src->scb[y] to pick the palette, then
// expand src->pixels nibbles -> your framebuffer pixels.
}
// ----- Input ---------------------------------------------------------------
// Set up your keyboard/joystick at the end of jlInit.
void jlpInputInit(void) {
// TODO: install / enable your input hardware.
}
// Tear input down (from jlShutdown, before jlpShutdown).
void jlpInputShutdown(void) {
// TODO: restore input hardware.
}
// 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
// codes); keys you can't map simply stay false. The cross-platform jlInputPoll
// and joystick code build on this.
void jlpInputPoll(void) {
// TODO: read your keyboard and set gKeyState[...] = true for held keys.
}
// Clear any auto-disconnect / calibration state for a joystick and arm a fresh
// center capture on the next poll. Digital-stick ports can leave this empty.
void jlpJoystickReset(jlJoystickE js) {
(void)js;
// TODO (analog sticks only): reset calibration for js.
}
// ----- Timing --------------------------------------------------------------
// Block until the next display vertical blank (frame pacing).
void jlpWaitVBL(void) {
// TODO: spin/sleep until the next vblank.
}
// A monotonic 16-bit frame/refresh counter. Either edge-detect a vblank flag
// inside this call or return a counter maintained by a vblank interrupt. Used
// for animation cadence and the generic millisElapsed (frameCount*1000/frameHz).
uint16_t jlpFrameCount(void) {
// TODO: return your running frame count.
return 0u;
}
// Nominal display refresh rate in Hz (50 PAL, 60 NTSC, 70 VGA, ...). Reported
// only; the generic millisElapsed divides by it.
uint16_t jlpFrameHz(void) {
// TODO: return your real refresh rate.
return 60u;
}
// ----- Audio (optional) ----------------------------------------------------
//
// Left entirely to the no-op generics: your port links and runs SILENT until
// you wire up sound. When you do, add JL_HAS_AUDIO_<OP> to your platform.h
// block and implement the matching jlpAudio<Op>() here (see src/<port>/audio.c
// in the shipping ports for the full op set: Init/Shutdown, PlayMod/StopMod/
// IsPlayingMod, PlaySfx/PlaySfxStream/StopSfx, Tone/Voice, TickRegister,
// CriticalEnter/Exit, FrameTick).

View file

@ -14,7 +14,7 @@
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/surface.h" #include "joey/surface.h"
#include "codegenArenaInternal.h" #include "codegenArenaInternal.h"
#include "hal.h" #include "port.h"
#include "spriteEmitter.h" #include "spriteEmitter.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -30,16 +30,16 @@
// `cap` argument and returns SPRITE_EMIT_OVERFLOW when a routine would // `cap` argument and returns SPRITE_EMIT_OVERFLOW when a routine would
// not fit. jlSpriteCompile then bails cleanly (no arena alloc, // not fit. jlSpriteCompile then bails cleanly (no arena alloc,
// sp->slot stays NULL, the dispatcher falls back to the interpreted // sp->slot stays NULL, the dispatcher falls back to the interpreted
// halSpriteXxxPlanes path) instead of corrupting the heap. The // the jlpSprite*Planes ops path) instead of corrupting the heap. The
// constant is a long literal so it never wraps the 16-bit int the // constant is a long literal so it never wraps the 16-bit int the
// IIgs uses for plain int arithmetic. // IIgs uses for plain int arithmetic.
// //
// The scratch is taken from halBigAlloc (jlAlloc's backing), NOT plain // The scratch is taken from jlpBigAlloc (jlAlloc's backing), NOT plain
// malloc: 16 KB exceeds the per-allocation / total ceiling of the stock // malloc: 16 KB exceeds the per-allocation / total ceiling of the stock
// C heap on some ports (the IIgs C heap lives in bank 0 and is only a // C heap on some ports (the IIgs C heap lives in bank 0 and is only a
// few KB), where a malloc that large either fails outright or -- with a // few KB), where a malloc that large either fails outright or -- with a
// bump allocator that does not bound-check -- hands back an out-of-heap // bump allocator that does not bound-check -- hands back an out-of-heap
// pointer that corrupts memory. halBigAlloc routes to the platform's // pointer that corrupts memory. jlpBigAlloc routes to the platform's
// native allocator (IIgs Memory Manager; plain malloc elsewhere). // native allocator (IIgs Memory Manager; plain malloc elsewhere).
#define SPRITE_EMIT_SCRATCH_BYTES (16384ul) #define SPRITE_EMIT_SCRATCH_BYTES (16384ul)
@ -177,7 +177,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
* bytes per row). The post-emit pass below aliases slots 2..7 * bytes per row). The post-emit pass below aliases slots 2..7
* for save/restore to slot 1's bytes. */ * for save/restore to slot 1's bytes. */
scratch = (uint8_t *)halBigAlloc(SPRITE_EMIT_SCRATCH_BYTES); scratch = (uint8_t *)jlpBigAlloc(SPRITE_EMIT_SCRATCH_BYTES);
if (scratch == NULL) { if (scratch == NULL) {
return false; return false;
} }
@ -186,7 +186,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
/* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single /* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single
* routine that did not fit the scratch is rejected here as well. */ * routine that did not fit the scratch is rejected here as well. */
if (totalSize > 0xFFFFu) { if (totalSize > 0xFFFFu) {
halBigFree(scratch); jlpBigFree(scratch);
return false; return false;
} }
if (totalSize == 0) { if (totalSize == 0) {
@ -194,14 +194,14 @@ bool jlSpriteCompile(jlSpriteT *sp) {
* no compiled bytes -- spriteCompiledDraw / SaveUnder / * no compiled bytes -- spriteCompiledDraw / SaveUnder /
* RestoreUnder would dereference a degenerate slot or chunky * RestoreUnder would dereference a degenerate slot or chunky
* shadow. Bail so sp->slot stays NULL and the dispatcher * shadow. Bail so sp->slot stays NULL and the dispatcher
* routes through the interpreted halSpriteXxxPlanes path. */ * routes through the interpreted the jlpSprite*Planes ops path. */
halBigFree(scratch); jlpBigFree(scratch);
return false; return false;
} }
slot = codegenArenaAlloc(totalSize); slot = codegenArenaAlloc(totalSize);
if (slot == NULL) { if (slot == NULL) {
halBigFree(scratch); jlpBigFree(scratch);
return false; return false;
} }
@ -222,7 +222,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
/* The second pass disagreed with the sizing pass (must /* The second pass disagreed with the sizing pass (must
* not happen, but never write past the slot). Roll back. */ * not happen, but never write past the slot). Roll back. */
codegenArenaFree(slot); codegenArenaFree(slot);
halBigFree(scratch); jlpBigFree(scratch);
return false; return false;
} }
/* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED) /* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED)
@ -249,7 +249,7 @@ bool jlSpriteCompile(jlSpriteT *sp) {
} }
#endif #endif
sp->slot = slot; sp->slot = slot;
halBigFree(scratch); jlpBigFree(scratch);
return true; return true;
} }
@ -516,8 +516,8 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* For non-zero shifts (x not 8-px-aligned), the dispatcher in * For non-zero shifts (x not 8-px-aligned), the dispatcher in
* src/core/sprite.c (jlSpriteDraw / jlSpriteSaveUnder / jlSpriteRestoreUnder) * src/core/sprite.c (jlSpriteDraw / jlSpriteSaveUnder / jlSpriteRestoreUnder)
* sees SPRITE_NOT_COMPILED for the shift and falls back to the * sees SPRITE_NOT_COMPILED for the shift and falls back to the
* interpreter, which handles arbitrary x via halSpriteDrawPlanes / * interpreter, which handles arbitrary x via jlpSpriteDrawPlanes /
* halSpriteSavePlanes / halSpriteRestorePlanes. */ * jlpSpriteSavePlanes / jlpSpriteRestorePlanes. */
// Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40. // Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40.
// Derived from surface geometry so it cannot drift if the fixed // Derived from surface geometry so it cannot drift if the fixed
@ -535,11 +535,20 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
DrawFn fn; DrawFn fn;
shift = (uint8_t)(x & 7); shift = (uint8_t)(x & 7);
/* Only shift 0 (byte-aligned x) is compiled; shifts 1..7 stay
* SPRITE_NOT_COMPILED. jlSpriteDraw gates only on sp->slot != NULL,
* so guard the per-shift offset here and delegate a non-aligned draw
* to the interpreter (COMPILED_SPRITE_WRITES_PLANES==1 means
* jlSpriteDraw suppresses its own planes hook, so we must run it). */
if (sp->routineOffsets[shift][SPRITE_OP_DRAW] == SPRITE_NOT_COMPILED) {
jlpSpriteDrawPlanes(dst, sp, x, y);
return;
}
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3)); byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3));
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return; p0 = jlpSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1); p1 = jlpSurfacePlanePtr(dst, 1);
p2 = halSurfacePlanePtr(dst, 2); p2 = jlpSurfacePlanePtr(dst, 2);
p3 = halSurfacePlanePtr(dst, 3); p3 = jlpSurfacePlanePtr(dst, 3);
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_DRAW]); fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_DRAW]);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff); fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff);
} }
@ -576,10 +585,10 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
/* 4 planes * h * (widthPx/8) bytes = h * widthPx/2. */ /* 4 planes * h * (widthPx/8) bytes = h * widthPx/2. */
backup->sizeBytes = (uint16_t)((uint16_t)heightPx * (widthPx >> 1)); backup->sizeBytes = (uint16_t)((uint16_t)heightPx * (widthPx >> 1));
p0 = halSurfacePlanePtr(src, 0); if (p0 == NULL) return; p0 = jlpSurfacePlanePtr(src, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(src, 1); p1 = jlpSurfacePlanePtr(src, 1);
p2 = halSurfacePlanePtr(src, 2); p2 = jlpSurfacePlanePtr(src, 2);
p3 = halSurfacePlanePtr(src, 3); p3 = jlpSurfacePlanePtr(src, 3);
fn = (SaveFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_SAVE]); fn = (SaveFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_SAVE]);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff, backup->bytes); fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff, backup->bytes);
} }
@ -605,10 +614,10 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
shift = (uint8_t)(backup->width > (uint16_t)(sp->widthTiles * 8) ? 1u : 0u); shift = (uint8_t)(backup->width > (uint16_t)(sp->widthTiles * 8) ? 1u : 0u);
byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)backup->x >> 3)); byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return; p0 = jlpSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1); p1 = jlpSurfacePlanePtr(dst, 1);
p2 = halSurfacePlanePtr(dst, 2); p2 = jlpSurfacePlanePtr(dst, 2);
p3 = halSurfacePlanePtr(dst, 3); p3 = jlpSurfacePlanePtr(dst, 3);
fn = (RestoreFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_RESTORE]); fn = (RestoreFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_RESTORE]);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff, backup->bytes); fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff, backup->bytes);
} }
@ -627,7 +636,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* 1 : byte-aligned x with x mod 16 == 8 (first tile col low half) * 1 : byte-aligned x with x mod 16 == 8 (first tile col low half)
* 2+ : non-byte-aligned x, never compiled (emitter returns 0); the * 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
* per-shift offset is SPRITE_NOT_COMPILED so the dispatcher * per-shift offset is SPRITE_NOT_COMPILED so the dispatcher
* falls back to halSpriteDrawPlanes. */ * falls back to jlpSpriteDrawPlanes. */
// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160 // ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct // bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
@ -648,16 +657,16 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
routeOffset = sp->routineOffsets[shift][SPRITE_OP_DRAW]; routeOffset = sp->routineOffsets[shift][SPRITE_OP_DRAW];
if (routeOffset == SPRITE_NOT_COMPILED) { if (routeOffset == SPRITE_NOT_COMPILED) {
/* Non-byte-aligned x: cross-platform jlSpriteDraw will call /* Non-byte-aligned x: cross-platform jlSpriteDraw will call
* halSpriteDrawPlanes after this returns (since the dispatcher * jlpSpriteDrawPlanes after this returns (since the dispatcher
* already chose the compiled path based on sp->slot != NULL, * already chose the compiled path based on sp->slot != NULL,
* but COMPILED_SPRITE_WRITES_PLANES is 1 on ST so it normally * but COMPILED_SPRITE_WRITES_PLANES is 1 on ST so it normally
* suppresses the planes hook). For non-aligned shifts we * suppresses the planes hook). For non-aligned shifts we
* deliberately want the interpreted planes hook to run, so * deliberately want the interpreted planes hook to run, so
* delegate via halSpriteDrawPlanes here. */ * delegate via jlpSpriteDrawPlanes here. */
halSpriteDrawPlanes(dst, sp, x, y); jlpSpriteDrawPlanes(dst, sp, x, y);
return; return;
} }
base = halSurfacePlanePtr(dst, 0); base = jlpSurfacePlanePtr(dst, 0);
if (base == NULL) { if (base == NULL) {
return; return;
} }
@ -672,7 +681,7 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
/* Save/Restore aren't compiled on ST yet (emitter returns 0). The /* Save/Restore aren't compiled on ST yet (emitter returns 0). The
* dispatcher's check on sp->routineOffsets[shift][SPRITE_OP_SAVE/_RESTORE] * dispatcher's check on sp->routineOffsets[shift][SPRITE_OP_SAVE/_RESTORE]
* == SPRITE_NOT_COMPILED already routes those through the * == SPRITE_NOT_COMPILED already routes those through the
* interpreted halSpriteSavePlanes / halSpriteRestorePlanes. These * interpreted jlpSpriteSavePlanes / jlpSpriteRestorePlanes. These
* stubs exist only to satisfy the linker. */ * stubs exist only to satisfy the linker. */
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) { void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
(void)src; (void)sp; (void)x; (void)y; (void)backup; (void)src; (void)sp; (void)x; (void)y; (void)backup;

View file

@ -1,37 +0,0 @@
// 68k sprite codegen for ST word-interleaved planar layout.
//
// Phase 11 (shared-walker): the per-shift unrolled emitter was
// retired. halSpriteDrawPlanes in src/port/atarist/hal.c now handles
// all shifts byte-wide at runtime, including the formerly fast shifts
// 0 and 1 -- per-cel arena cost drops to zero. The byte-aligned fast
// path stSpriteDrawByteAligned is still called from halSpriteDrawPlanes
// for shift==0 sprites that are fully on surface, so byte-aligned UBER
// perf does not regress. The entry points below are kept so the
// dispatcher API stays uniform across ports; they emit zero bytes, so
// jlSpriteCompile takes the "totalSize == 0 -> bail" path and sp->slot
// stays NULL.
#include "joey/sprite.h"
#include "joey/surface.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Emit API -----
uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}

View file

@ -1,42 +0,0 @@
// Planar 68k sprite codegen for Amiga.
//
// Phase 11 (shared-walker): the Amiga port no longer emits per-cel
// unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
// 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
// src/core/sprite.c routes through halSpriteXxxPlanes in
// src/port/amiga/hal.c when sp->slot is NULL, and those HAL hooks are
// byte-wide walkers that read sp->tileData on the fly and synthesize
// plane bytes at draw time. Same per-byte work, shared as one static
// lib routine instead of 21 inlined copies; the pre-shift plane-byte
// decomposition (and its shift/transparency math) now lives ONLY in
// halSpriteDrawPlanes, so there is one source of truth.
//
// The entry points below are kept so the dispatcher API stays uniform
// across ports. Returning 0 tells emitTotalSize() the emitter has no
// bytes for this op, so jlSpriteCompile takes the "totalSize == 0 ->
// bail" path and sp->slot stays NULL.
#include "joey/sprite.h"
#include "joey/surface.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Public API -----
uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}

View file

@ -4,7 +4,7 @@
// function. jlSpriteCompile.c picks the right one at compile time // function. jlSpriteCompile.c picks the right one at compile time
// (via #ifdef on JOEYLIB_PLATFORM_*) for the runtime build. The // (via #ifdef on JOEYLIB_PLATFORM_*) for the runtime build. The
// planar 68k emitters now return 0 bytes after Phase 11 -- the // planar 68k emitters now return 0 bytes after Phase 11 -- the
// halSpriteDrawPlanes walker reads chunky tile data and synthesizes // jlpSpriteDrawPlanes walker reads chunky tile data and synthesizes
// plane bytes at draw time -- but the entry points are kept so the // plane bytes at draw time -- but the entry points are kept so the
// dispatcher API stays uniform across ports. // dispatcher API stays uniform across ports.
// //
@ -104,7 +104,7 @@ uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT
// where groupBase = pd->base + y*160 + (x>>4)*8. Shifts 0 and 1 emit // where groupBase = pd->base + y*160 + (x>>4)*8. Shifts 0 and 1 emit
// real bytes (x mod 16 == 0 for shift 0, x mod 16 == 8 for shift 1); // real bytes (x mod 16 == 0 for shift 0, x mod 16 == 8 for shift 1);
// other shifts return 0 so the cross-platform dispatcher falls back // other shifts return 0 so the cross-platform dispatcher falls back
// to halSpriteDrawPlanes. // to jlpSpriteDrawPlanes.
uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); 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 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 spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);

View file

@ -58,6 +58,8 @@
#define EXPECTED_TILE_TARGET 3u #define EXPECTED_TILE_TARGET 3u
#elif defined(JOEYLIB_PLATFORM_IIGS) #elif defined(JOEYLIB_PLATFORM_IIGS)
#define EXPECTED_TILE_TARGET 4u #define EXPECTED_TILE_TARGET 4u
#elif defined(JOEYLIB_PLATFORM_BLANK)
#define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops)
#else #else
#error "Unknown platform for asset loader" #error "Unknown platform for asset loader"
#endif #endif

View file

@ -6,7 +6,7 @@
#include <stddef.h> #include <stddef.h>
#include "joey/audio.h" #include "joey/audio.h"
#include "hal.h" #include "port.h"
static bool gAudioReady = false; static bool gAudioReady = false;
@ -16,7 +16,7 @@ bool jlAudioInit(void) {
if (gAudioReady) { if (gAudioReady) {
return true; return true;
} }
gAudioReady = halAudioInit(); gAudioReady = jlpAudioInit();
return gAudioReady; return gAudioReady;
} }
@ -25,7 +25,7 @@ void jlAudioShutdown(void) {
if (!gAudioReady) { if (!gAudioReady) {
return; return;
} }
halAudioShutdown(); jlpAudioShutdown();
gAudioReady = false; gAudioReady = false;
} }
@ -34,7 +34,7 @@ void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gAudioReady || data == NULL || length == 0) { if (!gAudioReady || data == NULL || length == 0) {
return; return;
} }
halAudioPlayMod(data, length, loop); jlpAudioPlayMod(data, length, loop);
} }
@ -42,7 +42,7 @@ void jlAudioStopMod(void) {
if (!gAudioReady) { if (!gAudioReady) {
return; return;
} }
halAudioStopMod(); jlpAudioStopMod();
} }
@ -50,7 +50,7 @@ bool jlAudioIsPlayingMod(void) {
if (!gAudioReady) { if (!gAudioReady) {
return false; return false;
} }
return halAudioIsPlayingMod(); return jlpAudioIsPlayingMod();
} }
@ -61,7 +61,7 @@ void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16
if (slot >= JOEY_AUDIO_SFX_SLOTS) { if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
halAudioPlaySfx(slot, sample, length, rateHz); jlpAudioPlaySfx(slot, sample, length, rateHz);
} }
@ -72,7 +72,7 @@ void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint
if (slot >= JOEY_AUDIO_SFX_SLOTS) { if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
halAudioPlaySfxStream(slot, fill, ctx, rateHz); jlpAudioPlaySfxStream(slot, fill, ctx, rateHz);
} }
@ -81,7 +81,7 @@ void jlAudioTone(uint16_t freqHz) {
// PCM/SFX paths that need DMA + libxmp wiring. The PC speaker // PCM/SFX paths that need DMA + libxmp wiring. The PC speaker
// (and other simple hardware tone generators) is direct port // (and other simple hardware tone generators) is direct port
// I/O and works whether the DSP came up or not. // I/O and works whether the DSP came up or not.
halAudioTone(freqHz); jlpAudioTone(freqHz);
} }
@ -92,7 +92,7 @@ void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
if (voice >= JOEY_AUDIO_VOICES) { if (voice >= JOEY_AUDIO_VOICES) {
return; return;
} }
halAudioVoice(voice, freqHz, atten); jlpAudioVoice(voice, freqHz, atten);
} }
@ -100,17 +100,17 @@ bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
if (fn != NULL && hz == 0u) { if (fn != NULL && hz == 0u) {
return false; return false;
} }
return halAudioTickRegister(fn, hz); return jlpAudioTickRegister(fn, hz);
} }
void jlAudioCriticalEnter(void) { void jlAudioCriticalEnter(void) {
halAudioCriticalEnter(); jlpAudioCriticalEnter();
} }
void jlAudioCriticalExit(void) { void jlAudioCriticalExit(void) {
halAudioCriticalExit(); jlpAudioCriticalExit();
} }
@ -118,7 +118,7 @@ void jlAudioStopSfx(uint8_t slot) {
if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
halAudioStopSfx(slot); jlpAudioStopSfx(slot);
} }
@ -127,9 +127,9 @@ void jlAudioFrameTick(void) {
return; return;
} }
#ifndef JOEYLIB_PLATFORM_IIGS #ifndef JOEYLIB_PLATFORM_IIGS
// IIgs: NTPstreamsound is fully DOC-IRQ-driven, halAudioFrameTick // IIgs: NTPstreamsound is fully DOC-IRQ-driven, jlpAudioFrameTick
// is an empty no-op there. Skip the wrapper JSL entirely on IIgs // is an empty no-op there. Skip the wrapper JSL entirely on IIgs
// so per-frame audio cost stays at the gAudioReady branch above. // so per-frame audio cost stays at the gAudioReady branch above.
halAudioFrameTick(); jlpAudioFrameTick();
#endif #endif
} }

View file

@ -9,7 +9,7 @@
#include "joey/draw.h" #include "joey/draw.h"
#include "joey/debug.h" #include "joey/debug.h"
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -30,54 +30,14 @@
// ----- Prototypes ----- // ----- Prototypes -----
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); 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 void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual);
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex); static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { // (fillRectClipped moved to src/generic/genericDraw.c as jlpGenericFillRect,
uint8_t nibble = colorIndex & 0x0F; // the chunky default for the migrated jlpFillRect op.)
uint8_t doubled = (uint8_t)((nibble << 4) | nibble);
int16_t row;
uint16_t pxStart;
uint16_t pxEnd;
uint16_t midBytes;
uint8_t *line;
// Planar ports have NULL s->pixels; the planar dual-write hook
// handles the actual plane fill. Guarding here keeps the chunky
// fallback from dereferencing NULL when a port has no chunky store.
if (s->pixels == NULL) {
return;
}
/* px* and midBytes are uint16_t (clipped values are non-negative)
* so `>>1` lowers to a single LSR instead of a signed-shift
* helper. Same with `<<1` for midBytes. */
for (row = 0; row < h; row++) {
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
pxStart = (uint16_t)x;
pxEnd = (uint16_t)(x + w);
if (pxStart & 1u) {
line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0xF0) | nibble);
pxStart++;
}
midBytes = (uint16_t)((pxEnd - pxStart) >> 1);
if (midBytes > 0u) {
memset(&line[pxStart >> 1], doubled, (size_t)midBytes);
pxStart = (uint16_t)(pxStart + (midBytes << 1));
}
if (pxStart < pxEnd) {
line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0x0F) | (nibble << 4));
}
}
}
// Fill an axis-aligned span that the CALLER has already proven to be // Fill an axis-aligned span that the CALLER has already proven to be
// fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <= // fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <=
@ -88,10 +48,9 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
// it per span. Pixel output and dirty-mark word bands are bit-for-bit // it per span. Pixel output and dirty-mark word bands are bit-for-bit
// identical to routing the same span through jlFillRect. // 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) { static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
if (!halFastFillRect(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex)) { // Compile-time-selected op: machine fill (IIgs asm stage / Amiga+ST planar)
fillRectClipped(s, x, y, w, h, colorIndex); // or the generic chunky fill.
} jlpFillRect(s, x, y, w, h, colorIndex);
halFillRectPlanes(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex);
surfaceMarkDirtyRect(s, x, y, w, h); surfaceMarkDirtyRect(s, x, y, w, h);
} }
@ -128,7 +87,6 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
int16_t y; int16_t y;
int16_t leftX; int16_t leftX;
int16_t rightX; int16_t rightX;
uint8_t *row;
uint8_t pix; uint8_t pix;
bool pixMatch; bool pixMatch;
uint8_t newNibble; uint8_t newNibble;
@ -150,19 +108,17 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
continue; continue;
} }
/* Phase 9: planar ports have NULL s->pixels and the asm fast #if defined(JL_HAS_FLOOD_WALK_AND_SCANS)
* paths take a chunky-row pointer. Skip them on planar; the C // Highest-tier asm fast path (IIgs): seed-test + walk-left + walk-right
* fallback below uses halSamplePixel which works on both // + 1-row fill + scan-above + scan-below + push, all in one
* storage layouts. */ // cross-segment call. The asm caches row addr / match decoder
// across every sub-operation. C just pops and dispatches; this
// path completes the entire per-seed work and computes the row
// address itself, so we don't pay y*160 in C. On this platform it
// always handles the pop, so the C fallback below is never reached.
if (s->pixels != NULL) { if (s->pixels != NULL) {
// Highest-tier asm fast path: seed-test + walk-left + walk-right
// + 1-row fill + scan-above + scan-below + push, all in one
// cross-segment call. The asm caches row addr / match decoder
// across every sub-operation. C just pops and dispatches; this
// path completes the entire per-seed work and computes the row
// address itself, so we don't pay y*160 in C unless we fall back.
bool seedMatched; bool seedMatched;
if (halFastFloodWalkAndScans(s->pixels, x, y, if (jlpFloodWalkAndScans(s->pixels, x, y,
matchColor, newNibble, matchEqual, matchColor, newNibble, matchEqual,
stackX, stackY, stackX, stackY,
&sp, FLOOD_STACK_SIZE, &sp, FLOOD_STACK_SIZE,
@ -176,28 +132,23 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
continue; continue;
} }
} }
#endif
/* Fallback path: compute row only if chunky; halFastFloodWalk // Walk-out: find the matching run containing the seed. Planar ports
* needs it but isn't implemented on Amiga. */ // (Amiga) override with a plane-aware walk; everyone else uses the
row = (s->pixels != NULL) ? &s->pixels[SURFACE_ROW_OFFSET(y)] : NULL; // portable jlpSamplePixel walk, which works on chunky and planar.
// Tier-2 asm fast path: combined seed test + walk-left +
// walk-right in one cross-segment call. Falls back to the
// pure-C walks below on ports without an asm implementation.
{ {
#if defined(JL_HAS_FLOOD_WALK_PLANES)
bool seedMatched; bool seedMatched;
if (row != NULL && halFastFloodWalk(row, x, matchColor, newNibble, matchEqual, if (jlpFloodWalkPlanes(s, x, y, matchColor, newNibble, matchEqual,
&seedMatched, &leftX, &rightX)) {
if (!seedMatched) {
continue;
}
} else if (halFloodWalkPlanes(s, x, y, matchColor, newNibble, matchEqual,
&seedMatched, &leftX, &rightX)) { &seedMatched, &leftX, &rightX)) {
if (!seedMatched) { if (!seedMatched) {
continue; continue;
} }
} else { } else
pix = halSamplePixel(s, x, y); #endif
{
pix = jlpSamplePixel(s, x, y);
pixMatch = (pix == matchColor); pixMatch = (pix == matchColor);
if (matchEqual) { if (matchEqual) {
if (!pixMatch) { if (!pixMatch) {
@ -212,7 +163,7 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
// Walk left to find the start of the matching run. // Walk left to find the start of the matching run.
leftX = x; leftX = x;
while (leftX > 0) { while (leftX > 0) {
pix = halSamplePixel(s, (int16_t)(leftX - 1), y); pix = jlpSamplePixel(s, (int16_t)(leftX - 1), y);
pixMatch = (pix == matchColor); pixMatch = (pix == matchColor);
if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) { if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) {
break; break;
@ -223,7 +174,7 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
// Walk right to find the end. // Walk right to find the end.
rightX = x; rightX = x;
while (rightX < SURFACE_WIDTH - 1) { while (rightX < SURFACE_WIDTH - 1) {
pix = halSamplePixel(s, (int16_t)(rightX + 1), y); pix = jlpSamplePixel(s, (int16_t)(rightX + 1), y);
pixMatch = (pix == matchColor); pixMatch = (pix == matchColor);
if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) { if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) {
break; break;
@ -246,15 +197,14 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
fillRectOnSurface(s, leftX, y, spanW, 1, newNibble); fillRectOnSurface(s, leftX, y, spanW, 1, newNibble);
} }
// Scan rows above and below for run boundaries. The hot // Scan rows above and below for run boundaries: fill markBuf[] with
// per-pixel match check goes through halFastFloodScanRow on // 1/0 per pixel, then walk it for run-edge transitions and push a
// ports that have it (IIgs); fills markBuf[] with 1/0 per // seed at each run's right edge. Planar ports (Amiga) override the
// pixel so the run-edge walk below is array-only -- no // markBuf fill with a plane-aware scan; everyone else fills it via
// function call, no nibble extract. // the portable jlpSamplePixel walk.
{ {
int16_t i; int16_t i;
int16_t spanLen; int16_t spanLen;
uint8_t *scanRow;
int16_t scanY; int16_t scanY;
int16_t side; int16_t side;
bool curHit; bool curHit;
@ -273,52 +223,43 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
} }
scanY = (int16_t)(y + 1); scanY = (int16_t)(y + 1);
} }
scanRow = (s->pixels != NULL) ? &s->pixels[SURFACE_ROW_OFFSET(scanY)] : NULL; if (
// Prefer the combined scan+push asm path (one call per #if defined(JL_HAS_FLOOD_SCAN_ROW_PLANES)
// scan, no markBuf and no per-pixel C edge walk). Skip !jlpFloodScanRowPlanes(s, leftX, rightX, scanY,
// the asm tiers if we don't have a chunky row pointer matchColor, newNibble, matchEqual,
// (Phase 9 planar ports). floodMarkBuf)
if (scanRow == NULL || #else
!halFastFloodScanAndPush(scanRow, leftX, rightX, true
matchColor, newNibble, matchEqual, #endif
scanY, stackX, stackY, ) {
&sp, FLOOD_STACK_SIZE)) { // C fallback: fill markBuf the slow way.
if ((scanRow == NULL ||
!halFastFloodScanRow(scanRow, leftX, rightX,
matchColor, newNibble, matchEqual,
floodMarkBuf)) &&
!halFloodScanRowPlanes(s, leftX, rightX, scanY,
matchColor, newNibble, matchEqual,
floodMarkBuf)) {
// C fallback: fill markBuf the slow way.
for (i = 0; i < spanLen; i++) {
pix = halSamplePixel(s, (int16_t)(leftX + i), scanY);
pixMatch = (pix == matchColor);
floodMarkBuf[i] = (uint8_t)(matchEqual
? (pixMatch ? 1 : 0)
: ((!pixMatch && pix != newNibble) ? 1 : 0));
}
}
// Walk markBuf for run-edge transitions.
prevHit = false;
for (i = 0; i < spanLen; i++) { for (i = 0; i < spanLen; i++) {
curHit = floodMarkBuf[i] != 0; pix = jlpSamplePixel(s, (int16_t)(leftX + i), scanY);
if (!curHit && prevHit) { pixMatch = (pix == matchColor);
if (sp < FLOOD_STACK_SIZE) { floodMarkBuf[i] = (uint8_t)(matchEqual
stackX[sp] = (int16_t)(leftX + i - 1); ? (pixMatch ? 1 : 0)
stackY[sp] = scanY; : ((!pixMatch && pix != newNibble) ? 1 : 0));
sp++;
}
}
prevHit = curHit;
} }
if (prevHit) { }
// Walk markBuf for run-edge transitions.
prevHit = false;
for (i = 0; i < spanLen; i++) {
curHit = floodMarkBuf[i] != 0;
if (!curHit && prevHit) {
if (sp < FLOOD_STACK_SIZE) { if (sp < FLOOD_STACK_SIZE) {
stackX[sp] = rightX; stackX[sp] = (int16_t)(leftX + i - 1);
stackY[sp] = scanY; stackY[sp] = scanY;
sp++; sp++;
} }
} }
prevHit = curHit;
}
if (prevHit) {
if (sp < FLOOD_STACK_SIZE) {
stackX[sp] = rightX;
stackY[sp] = scanY;
sp++;
}
} }
} }
} }
@ -330,29 +271,17 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
// the public jlDrawPixel (which marks per call afterward) and the // the public jlDrawPixel (which marks per call afterward) and the
// Bresenham line/circle fallbacks (which accumulate one bounding box and // Bresenham line/circle fallbacks (which accumulate one bounding box and
// mark it once after their loop). Mirrors jlDrawPixel's NULL + bounds // mark it once after their loop). Mirrors jlDrawPixel's NULL + bounds
// check, halFastDrawPixel dispatch, and chunky nibble RMW fallback. // check, jlpDrawPixel dispatch, and chunky nibble RMW fallback.
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
uint8_t *byte;
uint8_t nibble;
if (s == NULL) { if (s == NULL) {
return; return;
} }
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return; return;
} }
// Compile-time-selected op: machine plot (asm/planar) or the generic chunky
if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex) && s->pixels != NULL) { // nibble RMW (src/generic/genericDraw.c). No runtime dispatch.
/* Cast to uint16_t before shift -- already validated x >= 0, jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
* so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
nibble = colorIndex & 0x0F;
if (x & 1) {
*byte = (uint8_t)((*byte & 0xF0) | nibble);
} else {
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
}
}
} }
@ -362,7 +291,6 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
int16_t x; int16_t x;
int16_t y; int16_t y;
int16_t err; int16_t err;
int16_t ir;
int16_t minX; int16_t minX;
int16_t minY; int16_t minY;
int16_t maxX; int16_t maxX;
@ -382,16 +310,20 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
// with r >= SURFACE_HEIGHT can never fit on-surface, and casting // with r >= SURFACE_HEIGHT can never fit on-surface, and casting
// such an r to int16_t can wrap negative (r > 32767), so reject it // 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. // here before the cast feeds the fast path / dirty-rect extents.
#if defined(JL_HAS_DRAW_CIRCLE)
if (r < SURFACE_HEIGHT) { if (r < SURFACE_HEIGHT) {
ir = (int16_t)r; int16_t ir = (int16_t)r;
// On-surface fast path: machine asm / planar. No runtime platform check
// -- the whole block is compiled out on targets without JL_HAS_DRAW_CIRCLE.
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH && if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT && cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) {
halFastDrawCircle(s, cx, cy, r, colorIndex)) { jlpDrawCircle(s, cx, cy, r, colorIndex);
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
return; return;
} }
} }
#endif
// Bresenham midpoint: maintain (x, y) on the perimeter, eight- // Bresenham midpoint: maintain (x, y) on the perimeter, eight-
// octant symmetry plots all 8 reflections each iteration. Plots // octant symmetry plots all 8 reflections each iteration. Plots
@ -521,16 +453,19 @@ 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 // can run without per-pixel bound checks. Hand off to the port
// fast path; bounding-box dirty marking happens here in C either // fast path; bounding-box dirty marking happens here in C either
// way. // way.
#if defined(JL_HAS_DRAW_LINE)
if (x0 >= 0 && x0 < SURFACE_WIDTH && x1 >= 0 && x1 < SURFACE_WIDTH && if (x0 >= 0 && x0 < SURFACE_WIDTH && x1 >= 0 && x1 < SURFACE_WIDTH &&
y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT && y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT) {
halFastDrawLine(s, x0, y0, x1, y1, colorIndex)) {
int16_t bbx = (x0 < x1) ? x0 : x1; int16_t bbx = (x0 < x1) ? x0 : x1;
int16_t bby = (y0 < y1) ? y0 : y1; int16_t bby = (y0 < y1) ? y0 : y1;
int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1); int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1);
int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1); int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1);
// On-surface fast path: machine asm / planar, compiled out where unsupported.
jlpDrawLine(s, x0, y0, x1, y1, colorIndex);
surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh); surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh);
return; return;
} }
#endif
// Diagonal fallback: plot through plotPixelNoMark so each pixel still // Diagonal fallback: plot through plotPixelNoMark so each pixel still
// clips off-surface individually (matching jlDrawPixel), but skip the // clips off-surface individually (matching jlDrawPixel), but skip the
@ -584,7 +519,7 @@ void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
return; return;
} }
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
// Hot per-pixel path: the asm plot (halFastDrawPixel -> iigsDrawPixelInner) // Hot per-pixel path: the asm plot (jlpDrawPixel -> iigsDrawPixelInner)
// plus an inline dirty mark, with NO plotPixelNoMark / surfaceMarkDirtyRect // plus an inline dirty mark, with NO plotPixelNoMark / surfaceMarkDirtyRect
// call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table, // call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table,
// no multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) -- // no multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) --
@ -594,7 +529,7 @@ void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) { if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) {
return; return;
} }
(void)halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex); jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
if (s == jlStageGet()) { if (s == jlStageGet()) {
uint8_t word = SURFACE_WORD_INDEX(x); uint8_t word = SURFACE_WORD_INDEX(x);
@ -692,11 +627,15 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
// route spans through the lighter on-surface fill instead of // route spans through the lighter on-surface fill instead of
// re-clipping each one through jlFillRect. // re-clipping each one through jlFillRect.
onSurface = true; onSurface = true;
if (halFastFillCircle(s, cx, cy, r, colorIndex)) { #if defined(JL_HAS_FILL_CIRCLE)
// Stage/portData-gated fast path (returns bool). Compiled out where
// unsupported; the span fill below is the universal fallback.
if (jlpFillCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
return; return;
} }
#endif
} }
} }
@ -792,9 +731,9 @@ void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor) {
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return; return;
} }
/* halSamplePixel reads from whichever storage the port uses -- /* jlpSamplePixel reads from whichever storage the port uses --
* works on both chunky (s->pixels) and planar (s->portData) ports. */ * works on both chunky (s->pixels) and planar (s->portData) ports. */
seedColor = halSamplePixel(s, x, y); seedColor = jlpSamplePixel(s, x, y);
if ((seedColor & 0x0F) == (newColor & 0x0F)) { if ((seedColor & 0x0F) == (newColor & 0x0F)) {
return; return;
} }
@ -811,7 +750,7 @@ void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor, u
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return; return;
} }
pix = halSamplePixel(s, x, y); pix = jlpSamplePixel(s, x, y);
// Starting on a boundary pixel or already-filled pixel: nothing // Starting on a boundary pixel or already-filled pixel: nothing
// to do. // to do.
if ((pix & 0x0F) == (boundaryColor & 0x0F)) { if ((pix & 0x0F) == (boundaryColor & 0x0F)) {
@ -831,10 +770,10 @@ uint8_t jlSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return 0; return 0;
} }
/* halSamplePixel reads from whichever storage the port uses -- /* jlpSamplePixel reads from whichever storage the port uses --
* chunky ports return a nibble extracted from s->pixels; planar * chunky ports return a nibble extracted from s->pixels; planar
* ports read 4 plane bits and assemble the nibble. */ * ports read 4 plane bits and assemble the nibble. */
return halSamplePixel(s, x, y); return jlpSamplePixel(s, x, y);
} }
@ -849,8 +788,8 @@ void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) {
} }
nibble = colorIndex & 0x0F; nibble = colorIndex & 0x0F;
doubled = (uint8_t)((nibble << 4) | nibble); doubled = (uint8_t)((nibble << 4) | nibble);
if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) { // Compile-time-selected op: a machine override (asm / planar) or the
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE); // portable-C generic default -- no runtime dispatch.
} jlpSurfaceClear(s, doubled);
surfaceMarkDirtyAll(s); surfaceMarkDirtyAll(s);
} }

View file

@ -1,572 +0,0 @@
// Internal HAL (hardware abstraction layer) interface.
//
// This header is included by src/core/*.c and by per-port source under
// src/port/<platform>/. It is NOT part of the public API and must not
// be installed or exposed to game code.
//
// Each port must implement every function declared here.
#ifndef JOEYLIB_HAL_H
#define JOEYLIB_HAL_H
#include <stdio.h>
#include "joey/audio.h"
#include "joey/core.h"
#include "joey/input.h"
#include "joey/sprite.h"
#include "joey/surface.h"
// Per-port one-shot initialization. Called from jlInit after config
// has been stored but before any surfaces are created. The port sets up
// the display mode, allocates any HW-adjacent buffers (chip RAM on
// Amiga, VGA mode on DOS, SHR on IIgs), and prepares for presents.
// Returns true on success. On failure, halLastError may be set.
bool halInit(const jlConfigT *config);
// Per-port teardown. Restores display mode, frees HW-adjacent buffers.
void halShutdown(void);
// Large / persistent allocation backing for the public jlAlloc / jlFree.
// Ports with a tiny or per-allocation-capped C heap (IIgs: bank-0 heap,
// ~32 KB malloc cap) implement these with their native allocator (IIgs:
// Memory Manager NewHandle / DisposeHandle). Ports with a normal heap
// use the shared malloc / free default defined in core/init.c, so only
// the constrained ports need a per-port override. halBigAlloc returns
// NULL on failure; the returned memory is not zero-filled. halBigFree
// accepts NULL.
void *halBigAlloc(uint32_t bytes);
void halBigFree(void *p);
// Allocate / release the SURFACE_PIXELS_SIZE-byte pixel buffer that
// backs the library-owned stage surface. Ports that have a
// hardware-friendly pin location for the back buffer (IIgs $01/2000
// with SHR shadow inhibited) return that address here; ports with no
// such constraint just malloc/free. Planar 68k ports may return NULL
// if the surface is planar-only and has no chunky shadow.
uint8_t *halStageAllocPixels(void);
void halStageFreePixels(uint8_t *pixels);
// Allocate / release the per-surface portData blob (see jlSurfaceT in
// surfaceInternal.h). Chunky ports return NULL from Init -- they keep
// portData unused and operate on the chunky `pixels` buffer. Planar
// 68k ports allocate a per-surface struct here describing the
// bitplane storage (Amiga: 4 separate plane buffers + stride; ST: one
// interleaved buffer + stride). Called by jlSurfaceCreate / stageAlloc
// after pixels is allocated; freed by jlSurfaceDestroy / stageFree
// before pixels is freed. `isStage` lets the port short-circuit for
// the stage if its planes are display-owned (e.g. Amiga's BitMap
// planes from OpenScreen) rather than allocated per surface.
void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage);
void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData);
// Phase 3 planar dual-write: called from cross-platform jlFillRect AFTER
// the chunky shadow has been written, with the same already-clipped
// (x, y, w, h) and the raw color index 0..15. Planar ports update
// the bitplanes with the rect's bit pattern (per-plane bit value =
// (color >> plane) & 1). Chunky ports (DOS, IIgs) provide a no-op
// stub. Called unconditionally so cross-platform code doesn't have
// to know the port is planar; the per-port stub is the cheapest
// possible thing on chunky ports.
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex);
// Phase 3 planar dual-write for jlSurfaceCopy: called from cross-platform
// jlSurfaceCopy AFTER the chunky pixel buffer is memcpy'd. Planar ports
// also memcpy the bitplanes from src to dst so JOEYLIB_PLANAR_PRESENT
// builds see correct planes. dst and src are non-NULL and distinct
// (caller's no-op guards already passed).
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src);
// Phase 5 planar dual-write for tile ops. Called from cross-platform
// tile.c AFTER the chunky path completes. (bx, by) are tile-grid
// coords (0..39 horiz, 0..24 vert; surface is 40x25 tiles).
// transparentIndex for jlTileCopyMasked: pixel value to skip. jlTilePaste
// reads from a packed 32-byte chunky jlTileT (4 bytes/row x 8 rows).
// All Amiga impls operate on the off-screen shadow planes via
// AmigaPlanarT; chunky-port stubs are no-ops. jlTileSnap is read-only
// so has no planar dual-write hook.
void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex);
void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile);
// halTilePasteMonoPlanes: planar-port jlTilePasteMono. Input tile holds
// a 1bpp monochrome shape (typically loaded from a font / charset
// asset) in the port-specific layout produced by halTileSnapPlanes;
// plane 0 carries the shape bytes, planes 1-3 are zero. Writes a
// colorized copy to the dst surface at (bx, by): fgColor where each
// shape bit is set, bgColor where clear, in the dst's native plane
// layout. Chunky ports stub this out -- the cross-platform code in
// jlTilePasteMono handles them via its chunky-shadow path.
void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor);
// jlTileSnap: cross-platform code reads s->pixels chunky bytes into a
// 32-byte jlTileT. On planar ports (s->pixels NULL) the chunky read
// crashes -- this hook is the planar derivation: reads bitplane bits
// for the tile rect and assembles 32 chunky bytes (4 bytes/row x 8
// rows) into chunkyTileOut. Chunky ports (s->pixels valid) implement
// this as a no-op since the cross-platform fallback already filled
// chunkyTileOut from s->pixels.
void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyTileOut);
// Phase 6 planar dual-write for jlSpriteDraw. Called from cross-platform
// sprite.c AFTER spriteCompiledDraw or spriteDrawInterpreted has
// updated the chunky shadow. (x, y) is the destination top-left in
// surface pixels (may be partially off-surface; the hook does its own
// clipping). Walks the sprite's chunky tile data and updates dst
// surface planes for every non-transparent pixel (nibble != 0).
// Save/restore have NO planar dual-write yet -- after jlSpriteSaveUnder
// + jlSpriteDraw + jlSpriteRestoreUnder under JOEYLIB_PLANAR_PRESENT, the
// planes still show the sprite (chunky restored, planes unchanged).
// Workable approach for that needs a parallel plane backup buffer;
// deferred until apps actually depend on PLANAR_PRESENT save/restore.
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y);
// Phase 9 sprite save/restore plane data. Chunky ports already hold
// pixel data in backup->bytes via the cross-platform memcpy. Planar
// ports (Amiga) DO have chunky NULL, so backup->bytes is unused by
// the chunky path -- we repurpose it to hold per-plane bytes. Layout:
// 4 plane stripes of (h * bytesPerPlaneRow) bytes each, where
// bytesPerPlaneRow = w/8 (sprite x and w are guaranteed 2-pixel
// aligned by jlSpriteSaveUnder; planar requires further 8-pixel
// rounding -- see Amiga impl notes). Total bytes:
// 4 * h * w/8 = h * w/2 = same as chunky. backup->sizeBytes capacity
// works on both ports. Chunky-port impls are no-ops; Amiga writes /
// reads plane bytes via AmigaPlanarT.
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes);
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes);
// Phase 9 reader hooks. Cross-platform code calls these instead of
// reading from s->pixels directly so it works regardless of whether
// the port stores chunky or planar as the source of truth. Chunky
// ports (DOS, IIgs) implement these reading from s->pixels (cheap);
// Amiga reads from the bitplanes in AmigaPlanarT. (x, y) bounds are
// already validated by the caller.
//
// halSamplePixel: returns the 0..15 nibble at (x, y).
// halSurfaceHash: returns the FNV-style hash of pixel + scb + palette
// that jlSurfaceHash currently computes by walking s->pixels. Allows
// ports to use their native pixel storage instead.
// halSurfaceCopyChunky: cross-platform jlSurfaceCopy used to memcpy
// s->pixels src->dst; on planar ports there is no chunky to copy
// (planes already covered by halSurfaceCopyPlanes). Chunky ports
// do the memcpy here; Amiga is a no-op.
// halSurfaceLoadFile / halSurfaceSaveFile wrap fread / fwrite of the
// pixel data using each port's native pixel format (chunky on
// IIgs/DOS, interleaved planar on ST, plane-major on Amiga). Files
// written by one port are NOT loadable by another -- conversion is
// the asset pipeline's job.
uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y);
uint32_t halSurfaceHash(const jlSurfaceT *s);
void halSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src);
bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp);
bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp);
// Present the dirty regions of the source surface to the display.
// The cross-platform jlStagePresent walks the dirty arrays before
// calling this; ports may use the dirty arrays themselves to skip
// untouched rows.
void halPresent(const jlSurfaceT *src);
// Optional: returns a port-specific error message string for the last
// HAL failure, or NULL if none. Ports may return NULL always.
const char *halLastError(void);
// Input: per-port keyboard setup, polling, and teardown.
// halInputInit is called at the end of jlInit; halInputShutdown
// from jlShutdown before halShutdown. halInputPoll refreshes the
// core-owned key-state array (declared in core/inputInternal.h) --
// the port writes true into gKeyState[key] for held keys. Keys the
// port does not recognize simply stay zero.
void halInputInit(void);
void halInputShutdown(void);
void halInputPoll(void);
// Block until the next display vertical blank. Each port implements
// this with whatever native wait the hardware provides (VGA $3DA,
// graphics.library WaitTOF, XBIOS Vsync, $C019 polling).
void halWaitVBL(void);
// Monotonic 16-bit frame counter. Caller polls; ports either detect
// the rising edge inside this call (IIgs $C019 / DOS $3DA / Amiga
// VPOSR) or return a counter maintained by a VBL ISR (ST). Required
// caller invariant: poll faster than 2 * halFrameHz() so no edge is
// missed. Used by benchmarks; cheap enough for animation cadence too.
uint16_t halFrameCount(void);
// Nominal display frame rate in Hz (50 PAL Amiga, 60 NTSC IIgs / ST,
// ~70 VGA mode 13h). Reported only -- no API contract that VBLs
// arrive at exactly this rate. Benchmarks divide by it to convert
// iters-per-N-frames to ops/sec.
uint16_t halFrameHz(void);
// Wall-clock millisecond counter since halInit. Source of truth
// for time-based scheduling (sound event timing, animation cadence
// on platforms where rendering can outpace VBL). Per-port impl uses
// the cheapest sub-frame timer available on that hardware: DOS uses
// uclock() (PIT counter 0, ~1 us resolution); ST reads the 200 Hz
// Timer C counter at _hz_200; Amiga uses ReadEClock (~1.4 us PAL);
// IIgs falls back to frame_count * 1000 / 60 since pulling in time()
// bursts the stdio cluster bank.
uint32_t halMillisElapsed(void);
// Audio: per-port engine setup, module + SFX playback, teardown.
// halAudioInit returns true if the platform has a working engine.
// Per-surface chunky pixel allocation. Chunky ports (DOS, IIgs, ST
// while still chunky) allocate SURFACE_PIXELS_SIZE bytes (calloc-
// style, zero-filled). Pure-planar Amiga returns NULL -- there's no
// chunky shadow; cross-platform code that previously read s->pixels
// goes through halSamplePixel / halSurfaceCopyChunky / etc. instead.
// halSurfaceFreePixels mirrors free(); NULL is a valid input on
// planar ports.
uint8_t *halSurfaceAllocPixels(void);
void halSurfaceFreePixels(uint8_t *pixels);
// Get a pointer to the start of bitplane `planeIdx` (0..3) for surface
// `s`. Returns NULL on chunky ports (no planes). On Amiga returns
// pd->planes[planeIdx] from the AmigaPlanarT struct in portData.
// Used by the planar sprite codegen dispatcher to compute the 4
// plane addresses to hand the emitted asm.
uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx);
bool halAudioInit(void);
void halAudioShutdown(void);
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
void halAudioStopMod(void);
bool halAudioIsPlayingMod(void);
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
void halAudioStopSfx(uint8_t slot);
void halAudioTone(uint16_t freqHz);
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
void halAudioCriticalEnter(void);
void halAudioCriticalExit(void);
void halAudioFrameTick(void);
// Optional fast-path hooks. Each returns true if the port handled the
// operation in a port-specific accelerated path; false means the
// caller should fall back to the platform-agnostic C implementation.
//
// Funneling all asm dispatches through hal.c (one TU per port) avoids
// the cumulative linker "Expression too complex" failure that
// hits when multiple cross-platform TUs each call into a named load
// segment full of asm primitives. Cross-platform code in src/core/
// only ever calls into HAL, so the link-time expression cost is paid
// once per binary -- not once per TU that wants speed.
//
// Each port must provide all of these; ports without an accelerated
// path simply return false from every hook.
bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled);
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex);
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord);
// Tile primitives operate on already-computed row-0 pointers from
// the C wrapper. dstRow0 / srcRow0 point at the first byte of the
// 8x8 region within their respective surfaces (stride 160). For
// jlTilePaste / jlTileSnap the jlTileT side is a packed 32-byte buffer
// (stride 4); the corresponding pointer points at byte 0 of that
// buffer.
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0);
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent);
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels);
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0);
// jlDrawPixel inner: caller has already done NULL + bounds checks.
// (x, y) are guaranteed in [0..SURFACE_WIDTH-1] x [0..SURFACE_HEIGHT-1].
// colorIndex is the 0..15 nibble. Surface dirty marking happens in
// the C wrapper after this returns.
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex);
// jlDrawLine inner for the diagonal case. Caller ensures both endpoints
// are inside the surface bounds, so the inner loop runs without
// per-pixel clip checks. The C wrapper still routes pure horizontal
// and vertical lines through jlFillRect (which has its own fast path).
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex);
// jlDrawCircle / jlFillCircle inner. Caller has already validated that
// the entire bounding circle (cx-r .. cx+r, cy-r .. cy+r) fits inside
// the surface bounds, so the inner loop plots every octant pixel
// unconditionally. r is guaranteed > 0; the cx == 0 / r == 0 cases
// stay in the C wrapper.
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
// jlFloodFill helper: combined seed test + walk-left + walk-right for
// one row. Returns true if the port handled it (asm path taken). The
// out-param seedMatched tells the caller whether the seed pixel
// satisfied the match criterion -- if false, caller skips this pop;
// if true, leftXOut/rightXOut hold the run boundaries.
// Returns false if no asm path; caller falls back to C walks.
bool halFastFloodWalk(uint8_t *row, int16_t startX,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
bool *seedMatched,
int16_t *leftXOut, int16_t *rightXOut);
// jlFloodFill helper for the row-above / row-below run-detection scans.
// Walks pixels [leftX..rightX] inclusive of `row`, writing 1 byte per
// pixel into markBuf (1 = qualifies for flood, 0 = does not). The C
// side then walks markBuf for run-edge transitions, replacing the
// per-pixel srcPixel + match check inside the inner loop.
// Returns true if the port handled it; false to fall back to C.
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
uint8_t *markBuf);
// Combined per-pixel scan + run-edge walk + seed push. Higher-level
// than halFastFloodScanRow: replaces both the markBuf fill AND the C
// loop that walks markBuf for falling edges. *spInOut is read on entry
// and updated with the new top-of-stack on return. Returns true if
// the port handled it (caller skips the C run-edge walk entirely);
// false to fall back to halFastFloodScanRow + C walk.
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
int16_t scanY,
int16_t *stackX, int16_t *stackY,
int16_t *spInOut, int16_t maxSp);
// Highest-level flood helper: combined seed-test + walk-left + walk-right
// + scan-above + scan-below + push for ONE popped seed. Replaces three
// cross-segment HAL calls (halFastFloodWalk + 2x halFastFloodScanAndPush)
// per dispatch loop iteration with one. The asm internally caches row
// addr / matchByte / nibble decoder across all three sub-operations.
//
// pixels is the surface base (s->pixels). On return, leftXOut / rightXOut
// hold the matching-run boundaries (only valid if seedMatched != 0); the
// caller does the 1-row halFastFillRect using those bounds. *spInOut is
// updated with any new seeds the asm pushed for the row above/below.
//
// Returns true if the port handled it; false to fall back to
// halFastFloodWalk + the per-side halFastFloodScanAndPush calls.
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
int16_t *stackX, int16_t *stackY,
int16_t *spInOut, int16_t maxSp,
bool *seedMatched,
int16_t *leftXOut, int16_t *rightXOut);
// Planar variants of halFastFloodWalk / halFastFloodScanRow. Take a
// jlSurfaceT* instead of a chunky-row pointer so they work on planar
// ports (Amiga post-Phase 9) where s->pixels is NULL. Same semantics;
// chunky ports return false (the chunky variants above are faster
// when a chunky row is available). Replace the per-pixel
// halSamplePixel walk on planar ports.
bool halFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
bool *seedMatched,
int16_t *leftXOut, int16_t *rightXOut);
bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY,
uint8_t matchColor, uint8_t newColor, bool matchEqual,
uint8_t *markBuf);
#ifdef JOEYLIB_PLATFORM_IIGS
// =====================================================================
// IIgs direct-dispatch macros.
//
// The halFast* function declarations above are the cross-platform API.
// On IIgs, those wrappers were ~60-80 cyc/call of pure plumbing on top
// of the asm itself: wrapper prologue (PHB/PHD/TCD), redundant arg
// re-push for the inner JSL, then wrapper epilogue. The macros below
// take effect at preprocess time and inline the asm call at the call
// site, eliminating the wrapper layer entirely.
//
// Cross-platform code in src/core/*.c is unchanged -- it still calls
// halFastDrawPixel(...) etc. On IIgs the preprocessor swaps that for
// the macro expansion before the file is compiled. The matching
// halFast* C definitions in src/port/iigs/hal.c are deleted, since
// nothing references them once the macros take effect.
//
// Macros use comma-expression form so they evaluate to a `bool` value
// (most halFast* return true on IIgs since the asm always succeeds).
// =====================================================================
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 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 fillWord);
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 fillWord);
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 iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0);
extern void iigsFillRectInner (uint8_t *pixels, 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;
#undef halFastDrawPixel
#define halFastDrawPixel(_s, _x, _y, _c) \
(iigsDrawPixelInner((_s)->pixels, (uint16_t)(_x), (uint16_t)(_y), \
(uint16_t)((_c) & 0x0F)), \
true)
#undef halFastDrawLine
#define halFastDrawLine(_s, _x0, _y0, _x1, _y1, _c) \
(iigsDrawLineInner((_s)->pixels, (uint16_t)(_x0), (uint16_t)(_y0), \
(uint16_t)(_x1), (uint16_t)(_y1), \
(uint16_t)((_c) & 0x0F)), \
true)
#undef halFastDrawCircle
#define halFastDrawCircle(_s, _cx, _cy, _r, _c) \
(iigsDrawCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \
(_r), (uint16_t)((_c) & 0x0F)), \
true)
// fillWord = doubled byte * $0101 = (nib*$11) * $101 = nib * $1111.
// Compile-time arithmetic when caller passes a constant; at most a
// single multiply when the nibble is variable (still cheaper than
// the wrapper's three sequential ORs / shifts).
#undef halFastFillCircle
#define halFastFillCircle(_s, _cx, _cy, _r, _c) \
((_s) == jlStageGet() \
? (iigsFillCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \
(_r), (uint16_t)(((_c) & 0x0F) * 0x1111)), \
true) \
: false)
#undef halFastSurfaceClear
#define halFastSurfaceClear(_s, _d) \
((_s) == jlStageGet() \
? (iigsSurfaceClearFastInner((_s)->pixels, \
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))), \
true) \
: false)
// halFastFillRect: macro form, same shape as the others. Builds
// clean now that _ROOT has been thinned out via the CORESYS load
// segment migration -- previous attempts shrank _ROOT enough to
// retrip the bank-packing fragility, but with most core .c files
// out of _ROOT that's no longer reactive. Saves ~80 cyc/call.
#undef halFastFillRect
#define halFastFillRect(_s, _x, _y, _w, _h, _c) \
((_s) == jlStageGet() \
? (iigsFillRectInner((_s)->pixels, (uint16_t)(_x), (uint16_t)(_y), \
(uint16_t)(_w), (uint16_t)(_h), \
(uint16_t)((_c) & 0x0F)), \
true) \
: false)
// Tile primitives operate on caller-computed row pointers; just
// forward the args. by/bx are tile coords -> bx*4 + by*8*160 byte
// offset within the surface. Use SURFACE_ROW_OFFSET (LUT lookup) to
// dodge a software multiply helper for the *160 multiply.
#undef halFastTileFill
#define halFastTileFill(_s, _bx, _by, _fw) \
(iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \
+ ((uint16_t)(_bx) << 2)], \
(_fw)), \
true)
#undef halFastTileCopy
#define halFastTileCopy(_d, _s) (iigsTileCopyInner((_d), (_s)), true)
#undef halFastTileCopyMasked
#define halFastTileCopyMasked(_d, _s, _t) \
(iigsTileCopyMaskedInner((_d), (_s), (uint16_t)(_t)), true)
#undef halFastTilePaste
#define halFastTilePaste(_d, _s) (iigsTilePasteInner((_d), (_s)), true)
#undef halFastTileSnap
#define halFastTileSnap(_d, _s) (iigsTileSnapInner((_d), (_s)), true)
// Tier 2/3 flood fallbacks always returned false on IIgs (the asm
// impls were deleted as unreachable). Macros to constant false so
// the compiler dead-code-eliminates the never-taken fallback branches
// in floodFillInternal.
#undef halFastFloodWalk
#define halFastFloodWalk(_row, _sx, _mc, _nc, _me, _sm, _lx, _rx) (false)
#undef halFastFloodScanRow
#define halFastFloodScanRow(_row, _lx, _rx, _mc, _nc, _me, _mb) (false)
#undef halFastFloodScanAndPush
#define halFastFloodScanAndPush(_row, _lx, _rx, _mc, _nc, _me, _sy, _sx, _syA, _sp, _ms) (false)
// IIgs is chunky; the planar flood hooks are never reachable.
#undef halFloodWalkPlanes
#define halFloodWalkPlanes(_s, _sx, _y, _mc, _nc, _me, _sm, _lx, _rx) (false)
#undef halFloodScanRowPlanes
#define halFloodScanRowPlanes(_s, _lx, _rx, _sy, _mc, _nc, _me, _mb) (false)
// Tier-1 flood: multi-output. Asm sets gFloodSeedMatch / gFloodLeftX /
// gFloodRightX; macro reads those into the caller's out-ptrs.
#undef halFastFloodWalkAndScans
#define halFastFloodWalkAndScans(_pix, _x, _y, _mc, _nc, _me, _sx, _sy, _sp, _ms, _smOut, _lxOut, _rxOut) \
(iigsFloodWalkAndScansInner((_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, \
true)
// =====================================================================
// IIgs planar dual-write elision.
//
// Every cross-platform primitive calls a halXxxPlanes() hook
// unconditionally so planar ports (Amiga, ST) can mirror each draw
// into their bitplanes. IIgs is chunky-native, so those hooks are
// pure no-op stubs -- but the call site still costs a cross-segment
// JSL per primitive (PHB/PHD setup + RTL) for zero work. These macros
// expand each call to ((void)0) at preprocess time so the call site is
// deleted entirely, exactly as the halFast* macros above eliminate the
// wrapper layer. The matching no-op stub definitions in
// src/port/iigs/hal.c are deleted, since nothing references them once
// the macros take effect.
//
// Gated on JOEYLIB_PLATFORM_IIGS specifically (NOT JOEYLIB_NATIVE_CHUNKY):
// DOS is also chunky-native and keeps its real stub functions, because
// its stubs are cheap near-calls and DOS is not the perf reference.
//
// Arg lists match each hook's arity so the preprocessor parses and
// discards the exact call expression; arguments with side effects are
// not present at any halXxxPlanes call site (all args are plain lvalues
// / field reads), so dropping them changes no behavior.
// =====================================================================
#undef halFillRectPlanes
#define halFillRectPlanes(_s, _x, _y, _w, _h, _c) ((void)0)
#undef halSurfaceCopyPlanes
#define halSurfaceCopyPlanes(_dst, _src) ((void)0)
#undef halTileFillPlanes
#define halTileFillPlanes(_s, _bx, _by, _c) ((void)0)
#undef halTileCopyPlanes
#define halTileCopyPlanes(_dst, _dbx, _dby, _src, _sbx, _sby) ((void)0)
#undef halTileCopyMaskedPlanes
#define halTileCopyMaskedPlanes(_dst, _dbx, _dby, _src, _sbx, _sby, _t) ((void)0)
#undef halTilePastePlanes
#define halTilePastePlanes(_dst, _bx, _by, _ct) ((void)0)
#undef halTilePasteMonoPlanes
#define halTilePasteMonoPlanes(_dst, _bx, _by, _mt, _fg, _bg) ((void)0)
#undef halTileSnapPlanes
#define halTileSnapPlanes(_src, _bx, _by, _out) ((void)0)
#undef halSpriteDrawPlanes
#define halSpriteDrawPlanes(_s, _sp, _x, _y) ((void)0)
#undef halSpriteSavePlanes
#define halSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
#undef halSpriteRestorePlanes
#define halSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
#endif /* JOEYLIB_PLATFORM_IIGS */
#endif

View file

@ -10,7 +10,7 @@
#include "joey/core.h" #include "joey/core.h"
#include "codegenArenaInternal.h" #include "codegenArenaInternal.h"
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -47,35 +47,22 @@ void coreSetError(const char *message) {
// ----- Default large-allocation backing ----- // ----- Default large-allocation backing -----
// //
// jlAlloc / jlFree route through halBigAlloc / halBigFree. Most ports // jlAlloc / jlFree route through jlpBigAlloc / jlpBigFree. Most ports have a
// have a normal heap, so the default below (plain malloc / free) serves // normal heap, so the malloc/free generic default (src/generic/genericPort.c)
// them and they do NOT need to implement the hook themselves. Only ports // serves them; only ports whose C heap is too small or per-allocation-capped
// whose C heap is too small or per-allocation-capped to satisfy a large // (IIgs: bank-0 heap) define JL_HAS_BIG_ALLOC and override with their native
// request -- currently just the IIgs -- provide their own halBigAlloc / // allocator (IIgs Memory Manager NewHandle / DisposeHandle).
// halBigFree (src/port/iigs/hal.c) and compile out this default.
#ifndef JOEYLIB_PLATFORM_IIGS
void *halBigAlloc(uint32_t bytes) {
return malloc((size_t)bytes);
}
void halBigFree(void *p) {
free(p);
}
#endif
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
void *jlAlloc(uint32_t bytes) { void *jlAlloc(uint32_t bytes) {
return halBigAlloc(bytes); return jlpBigAlloc(bytes);
} }
void jlFree(void *p) { void jlFree(void *p) {
halBigFree(p); jlpBigFree(p);
} }
@ -94,18 +81,18 @@ bool jlInit(const jlConfigT *config) {
memcpy(&gConfig, config, sizeof(gConfig)); memcpy(&gConfig, config, sizeof(gConfig));
// halInit must run before stageAlloc: on IIgs the stage's pixel // jlpInit must run before stageAlloc: on IIgs the stage's pixel
// buffer comes from halStageAllocPixels, which depends on shadow / // buffer comes from jlpStageAllocPixels, which depends on shadow /
// SHR setup that halInit performs. // SHR setup that jlpInit performs.
if (!halInit(&gConfig)) { if (!jlpInit(&gConfig)) {
const char *halMsg = halLastError(); const char *halMsg = jlpLastError();
coreSetError(halMsg != NULL ? halMsg : "halInit failed"); coreSetError(halMsg != NULL ? halMsg : "jlpInit failed");
return false; return false;
} }
if (!stageAlloc()) { if (!stageAlloc()) {
coreSetError("failed to allocate stage surface"); coreSetError("failed to allocate stage surface");
halShutdown(); jlpShutdown();
return false; return false;
} }
@ -113,11 +100,11 @@ bool jlInit(const jlConfigT *config) {
: DEFAULT_CODEGEN_BYTES)) { : DEFAULT_CODEGEN_BYTES)) {
coreSetError("failed to allocate codegen arena"); coreSetError("failed to allocate codegen arena");
stageFree(); stageFree();
halShutdown(); jlpShutdown();
return false; return false;
} }
halInputInit(); jlpInputInit();
gInitialized = true; gInitialized = true;
return true; return true;
@ -138,10 +125,10 @@ void jlShutdown(void) {
if (!gInitialized) { if (!gInitialized) {
return; return;
} }
halInputShutdown(); jlpInputShutdown();
codegenArenaShutdown(); codegenArenaShutdown();
stageFree(); stageFree();
halShutdown(); jlpShutdown();
gInitialized = false; gInitialized = false;
clearError(); clearError();
} }
@ -153,20 +140,20 @@ const char *jlVersionString(void) {
void jlWaitVBL(void) { void jlWaitVBL(void) {
halWaitVBL(); jlpWaitVBL();
} }
uint16_t jlFrameCount(void) { uint16_t jlFrameCount(void) {
return halFrameCount(); return jlpFrameCount();
} }
uint16_t jlFrameHz(void) { uint16_t jlFrameHz(void) {
return halFrameHz(); return jlpFrameHz();
} }
uint32_t jlMillisElapsed(void) { uint32_t jlMillisElapsed(void) {
return halMillisElapsed(); return jlpMillisElapsed();
} }

View file

@ -12,7 +12,7 @@
#include <string.h> #include <string.h>
#include "joey/input.h" #include "joey/input.h"
#include "hal.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
@ -54,14 +54,14 @@ void jlInputPoll(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
// One asm pass for: TTL decrement + key snapshot + mouse/joy // One asm pass for: TTL decrement + key snapshot + mouse/joy
// button snapshots. Replaces 3 memcpys + the C TTL loop // button snapshots. Replaces 3 memcpys + the C TTL loop
// that used to live in halInputPoll. ~0.6 ms saved per frame. // that used to live in jlpInputPoll. ~0.6 ms saved per frame.
iigsInputSnapshot(); iigsInputSnapshot();
#else #else
memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); memcpy(gKeyPrev, gKeyState, sizeof(gKeyState));
memcpy(gMouseButtonPrev, gMouseButtonState, sizeof(gMouseButtonState)); memcpy(gMouseButtonPrev, gMouseButtonState, sizeof(gMouseButtonState));
memcpy(gJoyButtonPrev, gJoyButtonState, sizeof(gJoyButtonState)); memcpy(gJoyButtonPrev, gJoyButtonState, sizeof(gJoyButtonState));
#endif #endif
halInputPoll(); jlpInputPoll();
} }
@ -220,5 +220,5 @@ void jlJoystickReset(jlJoystickE js, uint8_t deadZone) {
return; return;
} }
gJoyDeadZone[js] = deadZone; gJoyDeadZone[js] = deadZone;
halJoystickReset(js); jlpJoystickReset(js);
} }

View file

@ -1,6 +1,6 @@
// Internal input state shared between core and per-port HAL. // Internal input state shared between core and per-port HAL.
// //
// Per-port halInputPoll() writes directly into the public state // Per-port jlpInputPoll() writes directly into the public state
// globals: gKeyState[] for keyboard, gMouse* for mouse, gJoy* for // globals: gKeyState[] for keyboard, gMouse* for mouse, gJoy* for
// joysticks. The core compares against the matching *Prev shadow // joysticks. The core compares against the matching *Prev shadow
// each jlInputPoll to derive edge events for keys/buttons. Mouse // each jlInputPoll to derive edge events for keys/buttons. Mouse
@ -37,8 +37,8 @@ extern uint8_t gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT];
// with analog paddles (IIgs); ignored on digital-stick platforms. // with analog paddles (IIgs); ignored on digital-stick platforms.
extern uint8_t gJoyDeadZone [JOYSTICK_COUNT]; extern uint8_t gJoyDeadZone [JOYSTICK_COUNT];
// Per-port hook: called from jlJoystickReset to clear any auto- // (jlpJoystickReset hook -- called from jlJoystickReset to clear any auto-
// disconnect state and arm a fresh center capture on the next poll. // disconnect state and arm a fresh center capture -- is declared with the
void halJoystickReset(jlJoystickE js); // other platform-only services in src/core/port.h.)
#endif #endif

708
src/core/port.h Normal file
View file

@ -0,0 +1,708 @@
// Internal platform-override registry (the new joeylib's joey.h equivalent).
//
// Every overridable operation is a jlp<Op>() resolved at COMPILE TIME: a
// portable-C default lives in src/generic/ and a machine supplies its own
// version when it defines JL_HAS_<OP> in include/joey/platform.h. There is no
// runtime dispatch. This header wires the two together (the dispatch tail at
// the bottom) and holds the IIgs asm-macro overrides.
//
// Two flavors of op (both use the same jlp + JL_HAS mechanism):
// (A) ops with a genuine portable generic -- draw/tile/sprite primitives,
// surface readers, allocation, millisElapsed. A machine overrides only
// where it needs native speed/behavior; otherwise it links the generic.
// (B) platform SERVICES with no portable behavior -- init/shutdown/present,
// input, vbl/frame timing, the audio engine. The "generic" is a no-op/
// zero stub so a blank/template port still links; every real port
// overrides via JL_HAS_<OP>.
//
// Included by src/core/*.c, src/<machine>/*.c, and src/generic/*.c. NOT public.
#ifndef JOEYLIB_PORT_H
#define JOEYLIB_PORT_H
#include <stdio.h>
#include "joey/audio.h"
#include "joey/core.h"
#include "joey/input.h"
#include "joey/sprite.h"
#include "joey/surface.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
// holds the IIgs's asm-macro overrides: it #define-s jlp<Op> directly to the
// iigs*Inner asm entry points, so IIgs primitives never pay a function call.
#ifdef JOEYLIB_PLATFORM_IIGS
// =====================================================================
// IIgs direct-dispatch macros.
//
// IIgs overrides the draw/tile/flood primitives by #define-ing jlp<Op>
// directly to its asm entry points (iigs*Inner), so each primitive inlines
// the asm call at the call site with no function-call plumbing. A C wrapper
// would cost ~60-80 cyc/call of pure overhead here (prologue PHB/PHD/TCD,
// redundant arg re-push for the inner JSL, then epilogue); the macro form
// eliminates it. Cross-platform code in src/core/*.c calls jlp<Op>(...)
// unchanged and the preprocessor swaps in the asm before compile.
//
// Ops the IIgs does not accelerate (or only accelerates for the stage)
// fall through to the portable-C generic jlpGeneric<Op>, like every other
// machine. Some macros use comma-expression form to evaluate to a `bool`
// (e.g. fillCircle: stage-only, returns false off-stage so the core falls
// back to its C span fill).
// =====================================================================
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 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 fillWord);
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 fillWord);
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 iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0);
extern void iigsFillRectInner (uint8_t *pixels, 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;
// 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), \
(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.
#define jlpDrawLine(_s, _x0, _y0, _x1, _y1, _c) \
iigsDrawLineInner((_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), \
(_r), (uint16_t)((_c) & 0x0F))
// fillWord = doubled byte * $0101 = (nib*$11) * $101 = nib * $1111.
// Compile-time arithmetic when caller passes a constant; at most a
// single multiply when the nibble is variable (still cheaper than
// the wrapper's three sequential ORs / shifts).
// fillCircle migrated to jlpFillCircle: stage-only asm, returns bool so the core
// falls to the C span fill for non-stage / off-surface. JL_HAS_FILL_CIRCLE-gated.
#define jlpFillCircle(_s, _cx, _cy, _r, _c) \
((_s) == jlStageGet() \
? (iigsFillCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \
(_r), (uint16_t)(((_c) & 0x0F) * 0x1111)), \
true) \
: false)
// 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) == jlStageGet() \
? iigsSurfaceClearFastInner((_s)->pixels, \
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))) \
: jlpGenericSurfaceClear((_s), (_d)))
// fillRect: macro form, same shape as the others. Stage surfaces use the asm;
// non-stage surfaces defer to the chunky generic. (Builds clean now that the
// CORESYS load-segment migration drained _ROOT; earlier the macro form retripped
// the bank-packing fragility.) Saves ~80 cyc/call.
#define jlpFillRect(_s, _x, _y, _w, _h, _c) \
((_s) == jlStageGet() \
? iigsFillRectInner((_s)->pixels, (uint16_t)(_x), (uint16_t)(_y), \
(uint16_t)(_w), (uint16_t)(_h), \
(uint16_t)((_c) & 0x0F)) \
: jlpGenericFillRect((_s), (_x), (_y), (_w), (_h), (_c)))
// Tile primitives operate on caller-computed row pointers; just
// forward the args. by/bx are tile coords -> bx*4 + by*8*160 byte
// offset within the surface. Use SURFACE_ROW_OFFSET (LUT lookup) to
// dodge a software multiply helper for the *160 multiply.
// tileFill: takes colorIndex; the asm derives the fill word ((c&0x0F) * 0x1111).
#define jlpTileFill(_s, _bx, _by, _c) \
iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \
+ ((uint16_t)(_bx) << 2)], \
(uint16_t)(((_c) & 0x0F) * 0x1111))
// tileCopy / tileCopyMasked: take surface+tile-coords; the asm macro derives
// row-0 pointers (row = by*8 -> SURFACE_ROW_OFFSET(by<<3); col byte = bx*4 -> bx<<2).
#define jlpTileCopy(_d, _dbx, _dby, _s, _sbx, _sby) \
iigsTileCopyInner(&(_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)], \
&(_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) \
iigsTilePasteInner(&(_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)])
// Flood: IIgs overrides only the all-in-one tier (JL_HAS_FLOOD_WALK_AND_SCANS).
// The dispatch tail leaves jlpFloodWalkAndScans defined here; the planar flood
// hooks (jlpFloodWalkPlanes / jlpFloodScanRowPlanes) are Amiga-only and IIgs
// never declares them (their call sites are #if'd out for non-Amiga).
//
// Tier-1 flood: multi-output. Asm sets gFloodSeedMatch / gFloodLeftX /
// gFloodRightX; 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), \
(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, \
true)
// =====================================================================
// IIgs planar-hook elision.
//
// The planar dual-write hooks (surfaceCopyPlanes, the sprite *Planes ops)
// let Amiga/ST mirror draws into their bitplanes. IIgs is chunky-native, so
// it #define-s them to ((void)0): the call site is deleted at preprocess time
// rather than paying a cross-segment JSL/RTL for a no-op. Arg lists match each
// op's arity so the preprocessor parses and discards the exact call; all args
// are plain lvalues / field reads, so dropping them changes no behavior. (DOS
// is also chunky but links the real no-op generic -- it is not the perf
// reference. The tile/fillRect planar work is folded into each jlp<Op> op, so
// there are no separate planar hooks left to elide for those.)
// =====================================================================
#define jlpSurfaceCopyPlanes(_dst, _src) ((void)0)
// sprite planes mirror: no-op on IIgs (chunky -> codegen does the work).
#define jlpSpriteDrawPlanes(_s, _sp, _x, _y) ((void)0)
#define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
#define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
#endif /* JOEYLIB_PLATFORM_IIGS */
// =====================================================================
// Migrated-op dispatch (generic default unless a machine overrides).
//
// jlpGenericSurfaceClear is the portable-C reference (src/generic/genericDraw.c),
// always available. Machine overrides are selected at compile time via
// JL_HAS_<OP> (include/joey/platform.h): a macro override (IIgs, above) already
// #define-d jlp<Op>; a function override (Amiga/ST) just declares its prototype
// here; otherwise jlp<Op> aliases the generic default. No runtime dispatch.
// =====================================================================
void jlpGenericSurfaceClear(jlSurfaceT *s, uint8_t doubled);
#if !defined(jlpSurfaceClear)
#if defined(JL_HAS_SURFACE_CLEAR)
void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled); /* function override */
#else
#define jlpSurfaceClear(_s, _d) jlpGenericSurfaceClear((_s), (_d))
#endif
#endif
void jlpGenericDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex);
#if !defined(jlpDrawPixel)
#if defined(JL_HAS_DRAW_PIXEL)
void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex); /* function override */
#else
#define jlpDrawPixel(_s, _x, _y, _c) jlpGenericDrawPixel((_s), (_x), (_y), (_c))
#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);
#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);
#endif
// fillCircle returns bool (stage/portData check is inherent); the platform
// dispatch is compile-time via JL_HAS_FILL_CIRCLE.
#if defined(JL_HAS_FILL_CIRCLE) && !defined(jlpFillCircle)
bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex);
#endif
void jlpGenericFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
#if !defined(jlpFillRect)
#if defined(JL_HAS_FILL_RECT)
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); /* function override (planar) */
#else
#define jlpFillRect(_s, _x, _y, _w, _h, _c) jlpGenericFillRect((_s), (_x), (_y), (_w), (_h), (_c))
#endif
#endif
void jlpGenericTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
#if !defined(jlpTileFill)
#if defined(JL_HAS_TILE_FILL)
void jlpTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex); /* function override (planar) */
#else
#define jlpTileFill(_s, _bx, _by, _c) jlpGenericTileFill((_s), (_bx), (_by), (_c))
#endif
#endif
void jlpGenericTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
#if !defined(jlpTileCopy)
#if defined(JL_HAS_TILE_COPY)
void jlpTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
#else
#define jlpTileCopy(_d, _dbx, _dby, _s, _sbx, _sby) jlpGenericTileCopy((_d), (_dbx), (_dby), (_s), (_sbx), (_sby))
#endif
#endif
void jlpGenericTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent);
#if !defined(jlpTileCopyMasked)
#if defined(JL_HAS_TILE_COPY_MASKED)
void jlpTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent);
#else
#define jlpTileCopyMasked(_d, _dbx, _dby, _s, _sbx, _sby, _t) jlpGenericTileCopyMasked((_d), (_dbx), (_dby), (_s), (_sbx), (_sby), (_t))
#endif
#endif
void jlpGenericTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile);
#if !defined(jlpTilePaste)
#if defined(JL_HAS_TILE_PASTE)
void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile);
#else
#define jlpTilePaste(_d, _bx, _by, _ct) jlpGenericTilePaste((_d), (_bx), (_by), (_ct))
#endif
#endif
void jlpGenericTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut);
#if !defined(jlpTileSnap)
#if defined(JL_HAS_TILE_SNAP)
void jlpTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut);
#else
#define jlpTileSnap(_s, _bx, _by, _out) jlpGenericTileSnap((_s), (_bx), (_by), (_out))
#endif
#endif
void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor);
#if !defined(jlpTilePasteMono)
#if defined(JL_HAS_TILE_PASTE_MONO)
void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor);
#else
#define jlpTilePasteMono(_d, _bx, _by, _mt, _fg, _bg) jlpGenericTilePasteMono((_d), (_bx), (_by), (_mt), (_fg), (_bg))
#endif
#endif
void jlpGenericSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y);
#if !defined(jlpSpriteDrawPlanes)
#if defined(JL_HAS_SPRITE_DRAW)
void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y);
#else
#define jlpSpriteDrawPlanes(_s, _sp, _x, _y) jlpGenericSpriteDrawPlanes((_s), (_sp), (_x), (_y))
#endif
#endif
void jlpGenericSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes);
#if !defined(jlpSpriteSavePlanes)
#if defined(JL_HAS_SPRITE_SAVE)
void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes);
#else
#define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) jlpGenericSpriteSavePlanes((_s), (_x), (_y), (_w), (_h), (_dst))
#endif
#endif
void jlpGenericSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes);
#if !defined(jlpSpriteRestorePlanes)
#if defined(JL_HAS_SPRITE_RESTORE)
void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes);
#else
#define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) jlpGenericSpriteRestorePlanes((_s), (_x), (_y), (_w), (_h), (_src))
#endif
#endif
// Flood ops (PATTERN B: no extractable generic -- the portable default is the
// inline C scanline fallback in floodFillInternal, gated per-tier by
// `#if defined(JL_HAS_FLOOD_*)` at the call site). Each real override is just a
// prototype here for the claiming machine. jlpFloodWalkAndScans is a macro on
// IIgs (defined above), so its `!defined` guard skips the proto there.
#if defined(JL_HAS_FLOOD_WALK_AND_SCANS) && !defined(jlpFloodWalkAndScans)
bool jlpFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut);
#endif
#if defined(JL_HAS_FLOOD_WALK_PLANES) && !defined(jlpFloodWalkPlanes)
bool jlpFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut);
#endif
#if defined(JL_HAS_FLOOD_SCAN_ROW_PLANES) && !defined(jlpFloodScanRowPlanes)
bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf);
#endif
// =====================================================================
// Category B: structural port services (generic default + JL_HAS override).
//
// Unlike Category A accelerators these are resolved purely at link/compile
// time (no runtime dispatch ever existed). Each op has a portable-C default in
// src/generic/ -- a real implementation where one exists, or a no-op/zero stub
// for inherently platform-specific services so a blank/template port still
// links. A port that supplies its own version defines JL_HAS_<OP> in
// include/joey/platform.h. Same jlp<Op> stanza as Category A.
// =====================================================================
// --- Generic-only surface services (no port overrides them today) ---
void jlpGenericSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src);
#if !defined(jlpSurfaceCopyChunky)
#if defined(JL_HAS_SURFACE_COPY_CHUNKY)
void jlpSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src);
#else
#define jlpSurfaceCopyChunky(_d, _s) jlpGenericSurfaceCopyChunky((_d), (_s))
#endif
#endif
void jlpGenericSurfaceFreePixels(uint8_t *pixels);
#if !defined(jlpSurfaceFreePixels)
#if defined(JL_HAS_SURFACE_FREE_PIXELS)
void jlpSurfaceFreePixels(uint8_t *pixels);
#else
#define jlpSurfaceFreePixels(_p) jlpGenericSurfaceFreePixels((_p))
#endif
#endif
const char *jlpGenericLastError(void);
#if !defined(jlpLastError)
#if defined(JL_HAS_LAST_ERROR)
const char *jlpLastError(void);
#else
#define jlpLastError() jlpGenericLastError()
#endif
#endif
// --- Planar dual-write hook (no-op generic default; planar ports override) ---
void jlpGenericSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src);
#if !defined(jlpSurfaceCopyPlanes)
#if defined(JL_HAS_SURFACE_COPY_PLANES)
void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src);
#else
#define jlpSurfaceCopyPlanes(_d, _s) jlpGenericSurfaceCopyPlanes((_d), (_s))
#endif
#endif
// --- Surface readers (chunky generic default; planar ports decode portData) ---
uint8_t jlpGenericSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y);
#if !defined(jlpSamplePixel)
#if defined(JL_HAS_SAMPLE_PIXEL)
uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y);
#else
#define jlpSamplePixel(_s, _x, _y) jlpGenericSamplePixel((_s), (_x), (_y))
#endif
#endif
uint32_t jlpGenericSurfaceHash(const jlSurfaceT *s);
#if !defined(jlpSurfaceHash)
#if defined(JL_HAS_SURFACE_HASH)
uint32_t jlpSurfaceHash(const jlSurfaceT *s);
#else
#define jlpSurfaceHash(_s) jlpGenericSurfaceHash((_s))
#endif
#endif
bool jlpGenericSurfaceLoadFile(jlSurfaceT *dst, FILE *fp);
#if !defined(jlpSurfaceLoadFile)
#if defined(JL_HAS_SURFACE_LOAD_FILE)
bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp);
#else
#define jlpSurfaceLoadFile(_d, _fp) jlpGenericSurfaceLoadFile((_d), (_fp))
#endif
#endif
bool jlpGenericSurfaceSaveFile(const jlSurfaceT *src, FILE *fp);
#if !defined(jlpSurfaceSaveFile)
#if defined(JL_HAS_SURFACE_SAVE_FILE)
bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp);
#else
#define jlpSurfaceSaveFile(_s, _fp) jlpGenericSurfaceSaveFile((_s), (_fp))
#endif
#endif
// --- Allocation ops (chunky generic defaults; planar/pinned overrides) ---
uint8_t *jlpGenericStageAllocPixels(void);
#if !defined(jlpStageAllocPixels)
#if defined(JL_HAS_STAGE_ALLOC_PIXELS)
uint8_t *jlpStageAllocPixels(void);
#else
#define jlpStageAllocPixels() jlpGenericStageAllocPixels()
#endif
#endif
void jlpGenericStageFreePixels(uint8_t *pixels);
#if !defined(jlpStageFreePixels)
#if defined(JL_HAS_STAGE_FREE_PIXELS)
void jlpStageFreePixels(uint8_t *pixels);
#else
#define jlpStageFreePixels(_p) jlpGenericStageFreePixels((_p))
#endif
#endif
uint8_t *jlpGenericSurfaceAllocPixels(void);
#if !defined(jlpSurfaceAllocPixels)
#if defined(JL_HAS_SURFACE_ALLOC_PIXELS)
uint8_t *jlpSurfaceAllocPixels(void);
#else
#define jlpSurfaceAllocPixels() jlpGenericSurfaceAllocPixels()
#endif
#endif
void *jlpGenericSurfaceAllocPortData(jlSurfaceT *s, bool isStage);
#if !defined(jlpSurfaceAllocPortData)
#if defined(JL_HAS_SURFACE_ALLOC_PORT_DATA)
void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage);
#else
#define jlpSurfaceAllocPortData(_s, _st) jlpGenericSurfaceAllocPortData((_s), (_st))
#endif
#endif
void jlpGenericSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData);
#if !defined(jlpSurfaceFreePortData)
#if defined(JL_HAS_SURFACE_FREE_PORT_DATA)
void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData);
#else
#define jlpSurfaceFreePortData(_s, _st, _pd) jlpGenericSurfaceFreePortData((_s), (_st), (_pd))
#endif
#endif
uint8_t *jlpGenericSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx);
#if !defined(jlpSurfacePlanePtr)
#if defined(JL_HAS_SURFACE_PLANE_PTR)
uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx);
#else
#define jlpSurfacePlanePtr(_s, _i) jlpGenericSurfacePlanePtr((_s), (_i))
#endif
#endif
// --- Timing services (platform-only stub generics; millisElapsed has a real default) ---
uint16_t jlpGenericFrameHz(void);
#if !defined(jlpFrameHz)
#if defined(JL_HAS_FRAME_HZ)
uint16_t jlpFrameHz(void);
#else
#define jlpFrameHz() jlpGenericFrameHz()
#endif
#endif
uint16_t jlpGenericFrameCount(void);
#if !defined(jlpFrameCount)
#if defined(JL_HAS_FRAME_COUNT)
uint16_t jlpFrameCount(void);
#else
#define jlpFrameCount() jlpGenericFrameCount()
#endif
#endif
void jlpGenericWaitVBL(void);
#if !defined(jlpWaitVBL)
#if defined(JL_HAS_WAIT_VBL)
void jlpWaitVBL(void);
#else
#define jlpWaitVBL() jlpGenericWaitVBL()
#endif
#endif
uint32_t jlpGenericMillisElapsed(void);
#if !defined(jlpMillisElapsed)
#if defined(JL_HAS_MILLIS_ELAPSED)
uint32_t jlpMillisElapsed(void);
#else
#define jlpMillisElapsed() jlpGenericMillisElapsed()
#endif
#endif
// --- Audio services (platform-only; blank-port stub generic + JL_HAS override) ---
bool jlpGenericAudioInit(void);
#if !defined(jlpAudioInit)
#if defined(JL_HAS_AUDIO_INIT)
bool jlpAudioInit(void);
#else
#define jlpAudioInit() jlpGenericAudioInit()
#endif
#endif
void jlpGenericAudioShutdown(void);
#if !defined(jlpAudioShutdown)
#if defined(JL_HAS_AUDIO_SHUTDOWN)
void jlpAudioShutdown(void);
#else
#define jlpAudioShutdown() jlpGenericAudioShutdown()
#endif
#endif
void jlpGenericAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
#if !defined(jlpAudioPlayMod)
#if defined(JL_HAS_AUDIO_PLAY_MOD)
void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
#else
#define jlpAudioPlayMod(_a, _b, _c) jlpGenericAudioPlayMod((_a), (_b), (_c))
#endif
#endif
void jlpGenericAudioStopMod(void);
#if !defined(jlpAudioStopMod)
#if defined(JL_HAS_AUDIO_STOP_MOD)
void jlpAudioStopMod(void);
#else
#define jlpAudioStopMod() jlpGenericAudioStopMod()
#endif
#endif
bool jlpGenericAudioIsPlayingMod(void);
#if !defined(jlpAudioIsPlayingMod)
#if defined(JL_HAS_AUDIO_IS_PLAYING_MOD)
bool jlpAudioIsPlayingMod(void);
#else
#define jlpAudioIsPlayingMod() jlpGenericAudioIsPlayingMod()
#endif
#endif
void jlpGenericAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
#if !defined(jlpAudioPlaySfx)
#if defined(JL_HAS_AUDIO_PLAY_SFX)
void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
#else
#define jlpAudioPlaySfx(_a, _b, _c, _d) jlpGenericAudioPlaySfx((_a), (_b), (_c), (_d))
#endif
#endif
void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
#if !defined(jlpAudioPlaySfxStream)
#if defined(JL_HAS_AUDIO_PLAY_SFX_STREAM)
void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
#else
#define jlpAudioPlaySfxStream(_a, _b, _c, _d) jlpGenericAudioPlaySfxStream((_a), (_b), (_c), (_d))
#endif
#endif
void jlpGenericAudioStopSfx(uint8_t slot);
#if !defined(jlpAudioStopSfx)
#if defined(JL_HAS_AUDIO_STOP_SFX)
void jlpAudioStopSfx(uint8_t slot);
#else
#define jlpAudioStopSfx(_a) jlpGenericAudioStopSfx((_a))
#endif
#endif
void jlpGenericAudioTone(uint16_t freqHz);
#if !defined(jlpAudioTone)
#if defined(JL_HAS_AUDIO_TONE)
void jlpAudioTone(uint16_t freqHz);
#else
#define jlpAudioTone(_a) jlpGenericAudioTone((_a))
#endif
#endif
void jlpGenericAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
#if !defined(jlpAudioVoice)
#if defined(JL_HAS_AUDIO_VOICE)
void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
#else
#define jlpAudioVoice(_a, _b, _c) jlpGenericAudioVoice((_a), (_b), (_c))
#endif
#endif
bool jlpGenericAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
#if !defined(jlpAudioTickRegister)
#if defined(JL_HAS_AUDIO_TICK_REGISTER)
bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
#else
#define jlpAudioTickRegister(_a, _b) jlpGenericAudioTickRegister((_a), (_b))
#endif
#endif
void jlpGenericAudioCriticalEnter(void);
#if !defined(jlpAudioCriticalEnter)
#if defined(JL_HAS_AUDIO_CRITICAL_ENTER)
void jlpAudioCriticalEnter(void);
#else
#define jlpAudioCriticalEnter() jlpGenericAudioCriticalEnter()
#endif
#endif
void jlpGenericAudioCriticalExit(void);
#if !defined(jlpAudioCriticalExit)
#if defined(JL_HAS_AUDIO_CRITICAL_EXIT)
void jlpAudioCriticalExit(void);
#else
#define jlpAudioCriticalExit() jlpGenericAudioCriticalExit()
#endif
#endif
void jlpGenericAudioFrameTick(void);
#if !defined(jlpAudioFrameTick)
#if defined(JL_HAS_AUDIO_FRAME_TICK)
void jlpAudioFrameTick(void);
#else
#define jlpAudioFrameTick() jlpGenericAudioFrameTick()
#endif
#endif
// --- Lifecycle / present / input (platform-only; blank stub generic + JL_HAS) ---
bool jlpGenericInit(const jlConfigT *config);
#if !defined(jlpInit)
#if defined(JL_HAS_INIT)
bool jlpInit(const jlConfigT *config);
#else
#define jlpInit(_a) jlpGenericInit((_a))
#endif
#endif
void jlpGenericShutdown(void);
#if !defined(jlpShutdown)
#if defined(JL_HAS_SHUTDOWN)
void jlpShutdown(void);
#else
#define jlpShutdown() jlpGenericShutdown()
#endif
#endif
void jlpGenericPresent(const jlSurfaceT *src);
#if !defined(jlpPresent)
#if defined(JL_HAS_PRESENT)
void jlpPresent(const jlSurfaceT *src);
#else
#define jlpPresent(_a) jlpGenericPresent((_a))
#endif
#endif
void jlpGenericInputInit(void);
#if !defined(jlpInputInit)
#if defined(JL_HAS_INPUT_INIT)
void jlpInputInit(void);
#else
#define jlpInputInit() jlpGenericInputInit()
#endif
#endif
void jlpGenericInputShutdown(void);
#if !defined(jlpInputShutdown)
#if defined(JL_HAS_INPUT_SHUTDOWN)
void jlpInputShutdown(void);
#else
#define jlpInputShutdown() jlpGenericInputShutdown()
#endif
#endif
void jlpGenericInputPoll(void);
#if !defined(jlpInputPoll)
#if defined(JL_HAS_INPUT_POLL)
void jlpInputPoll(void);
#else
#define jlpInputPoll() jlpGenericInputPoll()
#endif
#endif
void jlpGenericJoystickReset(jlJoystickE js);
#if !defined(jlpJoystickReset)
#if defined(JL_HAS_JOYSTICK_RESET)
void jlpJoystickReset(jlJoystickE js);
#else
#define jlpJoystickReset(_a) jlpGenericJoystickReset((_a))
#endif
#endif
// --- Large allocation (malloc/free generic default; IIgs Memory Manager override) ---
void *jlpGenericBigAlloc(uint32_t bytes);
void jlpGenericBigFree(void *p);
#if !defined(jlpBigAlloc)
#if defined(JL_HAS_BIG_ALLOC)
void *jlpBigAlloc(uint32_t bytes);
void jlpBigFree(void *p);
#else
#define jlpBigAlloc(_b) jlpGenericBigAlloc((_b))
#define jlpBigFree(_p) jlpGenericBigFree((_p))
#endif
#endif
#endif

View file

@ -8,7 +8,7 @@
#include "joey/debug.h" #include "joey/debug.h"
#include "joey/present.h" #include "joey/present.h"
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -21,6 +21,6 @@ void jlStagePresent(void) {
if (stage == NULL) { if (stage == NULL) {
return; return;
} }
halPresent(stage); jlpPresent(stage);
stageDirtyClearAll(); stageDirtyClearAll();
} }

View file

@ -10,7 +10,7 @@
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/tile.h" #include "joey/tile.h"
#include "codegenArenaInternal.h" #include "codegenArenaInternal.h"
#include "hal.h" #include "port.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -22,7 +22,7 @@
#define TRANSPARENT_NIBBLE 0 #define TRANSPARENT_NIBBLE 0
// On Amiga (post-Phase 9 / Phase 6 redux) the compiled sprite emitter // On Amiga (post-Phase 9 / Phase 6 redux) the compiled sprite emitter
// writes directly to the bitplanes, so the halSpritePlanes hooks are // writes directly to the bitplanes, so the jlpSprite*Planes hooks are
// pure duplicate work after a compiled call. On other ports the // pure duplicate work after a compiled call. On other ports the
// hooks are either no-op stubs (chunky-only IIgs/DOS) or the only // hooks are either no-op stubs (chunky-only IIgs/DOS) or the only
// thing writing planes (ST: chunky-shadow + planes). Slow / interpreter // thing writing planes (ST: chunky-shadow + planes). Slow / interpreter
@ -31,7 +31,7 @@
// is the only draw. // is the only draw.
/* ST also runs pure planar post-Phase-9 (s->pixels NULL); the JIT /* ST also runs pure planar post-Phase-9 (s->pixels NULL); the JIT
* routine writes plane bytes directly, so the chunky interpreter * routine writes plane bytes directly, so the chunky interpreter
* is a no-op and the halSpriteDrawPlanes hook would be a redundant * is a no-op and the jlpSpriteDrawPlanes hook would be a redundant
* second draw. Same rationale as Amiga. */ * second draw. Same rationale as Amiga. */
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) #if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST)
#define COMPILED_SPRITE_WRITES_PLANES 1 #define COMPILED_SPRITE_WRITES_PLANES 1
@ -158,7 +158,7 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
wTiles = sp->widthTiles; wTiles = sp->widthTiles;
/* Skip the chunky write loop on planar ports (s->pixels == NULL). /* Skip the chunky write loop on planar ports (s->pixels == NULL).
* halSpriteDrawPlanes is called by the jlSpriteDraw caller and does * jlpSpriteDrawPlanes is called by the jlSpriteDraw caller and does
* its own clip + plane write, so the dirty mark + planar update * its own clip + plane write, so the dirty mark + planar update
* happen there. Phase 9 dropped the chunky shadow on Amiga. */ * happen there. Phase 9 dropped the chunky shadow on Amiga. */
if (s->pixels != NULL) { if (s->pixels != NULL) {
@ -275,8 +275,8 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
} }
} else { } else {
// Planar port (Phase 9: no chunky shadow). Derive chunky 4bpp // Planar port (Phase 9: no chunky shadow). Derive chunky 4bpp
// pixels from plane bits via halSamplePixel and pack into the // pixels from plane bits via jlpSamplePixel and pack into the
// tile-major sprite layout. Slow (per-pixel halSamplePixel) but // tile-major sprite layout. Slow (per-pixel jlpSamplePixel) but
// only runs at sprite-capture time, not per frame. Without // only runs at sprite-capture time, not per frame. Without
// this branch sprites loaded from a work surface on Amiga/ST // this branch sprites loaded from a work surface on Amiga/ST
// all came back NULL -- the symptom was simply no sprites on // all came back NULL -- the symptom was simply no sprites on
@ -293,8 +293,8 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row); py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row);
for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) { for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) {
px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col); px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col);
hiNibble = (uint8_t)(halSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu); hiNibble = (uint8_t)(jlpSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu);
loNibble = (uint8_t)(halSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu); loNibble = (uint8_t)(jlpSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu);
dstTile[row * TILE_BYTES_PER_ROW + (col >> 1)] = dstTile[row * TILE_BYTES_PER_ROW + (col >> 1)] =
(uint8_t)((hiNibble << 4) | loNibble); (uint8_t)((hiNibble << 4) | loNibble);
} }
@ -346,13 +346,13 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
if (sp->slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) { if (sp->slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
spriteCompiledDraw(s, sp, x, y); spriteCompiledDraw(s, sp, x, y);
if (!COMPILED_SPRITE_WRITES_PLANES) { if (!COMPILED_SPRITE_WRITES_PLANES) {
halSpriteDrawPlanes(s, sp, x, y); jlpSpriteDrawPlanes(s, sp, x, y);
} }
surfaceMarkDirtyRect(s, x, y, (int16_t)widthPx, (int16_t)heightPx); surfaceMarkDirtyRect(s, x, y, (int16_t)widthPx, (int16_t)heightPx);
return; return;
} }
spriteDrawInterpreted(s, sp, x, y); spriteDrawInterpreted(s, sp, x, y);
halSpriteDrawPlanes(s, sp, x, y); jlpSpriteDrawPlanes(s, sp, x, y);
} }
@ -414,8 +414,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
spriteCompiledSaveUnder(s, sp, x, y, backup); spriteCompiledSaveUnder(s, sp, x, y, backup);
spriteCompiledDraw (s, sp, x, y); spriteCompiledDraw (s, sp, x, y);
if (!COMPILED_SPRITE_WRITES_PLANES) { if (!COMPILED_SPRITE_WRITES_PLANES) {
halSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes);
halSpriteDrawPlanes(s, sp, x, y); jlpSpriteDrawPlanes(s, sp, x, y);
} }
surfaceMarkDirtyRect (s, x, y, (int16_t)widthPx, (int16_t)heightPx); surfaceMarkDirtyRect (s, x, y, (int16_t)widthPx, (int16_t)heightPx);
return; return;
@ -502,7 +502,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
if (routeOffset != SPRITE_NOT_COMPILED) { if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledRestoreUnder(s, backup); spriteCompiledRestoreUnder(s, backup);
if (!COMPILED_SPRITE_WRITES_PLANES) { if (!COMPILED_SPRITE_WRITES_PLANES) {
halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes); jlpSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
} }
surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh); surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh);
return; return;
@ -512,7 +512,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
/* Slow / interpreted memcpy fallback. Skip the chunky memcpy if /* Slow / interpreted memcpy fallback. Skip the chunky memcpy if
* the port has no chunky shadow (Phase 9 Amiga: s->pixels NULL); * the port has no chunky shadow (Phase 9 Amiga: s->pixels NULL);
* halSpriteRestorePlanes below does the planar restore. */ * jlpSpriteRestorePlanes below does the planar restore. */
if (s->pixels != NULL) { if (s->pixels != NULL) {
int16_t row; int16_t row;
int16_t byteStart; int16_t byteStart;
@ -527,7 +527,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
(size_t)copyBytes); (size_t)copyBytes);
} }
} }
halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes); jlpSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh); surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh);
} }
@ -568,7 +568,7 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
if (routeOffset != SPRITE_NOT_COMPILED) { if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledSaveUnder(s, sp, x, y, backup); spriteCompiledSaveUnder(s, sp, x, y, backup);
if (!COMPILED_SPRITE_WRITES_PLANES) { if (!COMPILED_SPRITE_WRITES_PLANES) {
halSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes);
} }
return; return;
} }
@ -626,7 +626,7 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
return; return;
} }
/* Chunky save path: skip on planar ports (s->pixels NULL). /* Chunky save path: skip on planar ports (s->pixels NULL).
* halSpriteSavePlanes below covers the planar case. */ * jlpSpriteSavePlanes below covers the planar case. */
if (s->pixels != NULL) { if (s->pixels != NULL) {
for (row = 0; row < h; row++) { for (row = 0; row < h; row++) {
srcRow = &s->pixels[(dy + row) * SURFACE_BYTES_PER_ROW]; srcRow = &s->pixels[(dy + row) * SURFACE_BYTES_PER_ROW];
@ -635,6 +635,6 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
(size_t)copyBytes); (size_t)copyBytes);
} }
} }
halSpriteSavePlanes(s, clippedX, dy, (uint16_t)clippedW, (uint16_t)h, backup->bytes); jlpSpriteSavePlanes(s, clippedX, dy, (uint16_t)clippedW, (uint16_t)h, backup->bytes);
} /* end slow path */ } /* end slow path */
} }

View file

@ -7,7 +7,7 @@
#include <string.h> #include <string.h>
#include "joey/surface.h" #include "joey/surface.h"
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
@ -64,10 +64,10 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
if (dst == NULL || src == NULL || dst == src) { if (dst == NULL || src == NULL || dst == src) {
return; return;
} }
halSurfaceCopyChunky(dst, src); /* memcpy on chunky ports; no-op on planar */ jlpSurfaceCopyChunky(dst, src); /* memcpy on chunky ports; no-op on planar */
memcpy(dst->scb, src->scb, sizeof(src->scb)); memcpy(dst->scb, src->scb, sizeof(src->scb));
memcpy(dst->palette, src->palette, sizeof(src->palette)); memcpy(dst->palette, src->palette, sizeof(src->palette));
halSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */ jlpSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */
surfaceMarkDirtyAll(dst); surfaceMarkDirtyAll(dst);
// Bulk-replacing the stage's SCB/palette must re-mark them dirty so // 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; surfaceMarkDirtyAll only
@ -87,23 +87,23 @@ jlSurfaceT *jlSurfaceCreate(void) {
coreSetError("out of memory allocating surface"); coreSetError("out of memory allocating surface");
return NULL; return NULL;
} }
// halSurfaceAllocPixels returns NULL on planar ports (Amiga); the // jlpSurfaceAllocPixels returns NULL on planar ports (Amiga); the
// primary storage is the port-allocated planes via portData below. // primary storage is the port-allocated planes via portData below.
// Classify NULL by the build's storage model: chunky ports need // Classify NULL by the build's storage model: chunky ports need
// pixels, planar ports need portData. A NULL in the required slot // pixels, planar ports need portData. A NULL in the required slot
// is an out-of-memory failure (header contract: return NULL). // is an out-of-memory failure (header contract: return NULL).
s->pixels = halSurfaceAllocPixels(); s->pixels = jlpSurfaceAllocPixels();
s->portData = halSurfaceAllocPortData(s, false); s->portData = jlpSurfaceAllocPortData(s, false);
#ifdef JOEYLIB_NATIVE_CHUNKY #ifdef JOEYLIB_NATIVE_CHUNKY
if (s->pixels == NULL) { if (s->pixels == NULL) {
halSurfaceFreePortData(s, false, s->portData); jlpSurfaceFreePortData(s, false, s->portData);
free(s); free(s);
coreSetError("out of memory allocating surface pixels"); coreSetError("out of memory allocating surface pixels");
return NULL; return NULL;
} }
#else #else
if (s->portData == NULL) { if (s->portData == NULL) {
halSurfaceFreePixels(s->pixels); jlpSurfaceFreePixels(s->pixels);
free(s); free(s);
coreSetError("out of memory allocating surface planes"); coreSetError("out of memory allocating surface planes");
return NULL; return NULL;
@ -121,8 +121,8 @@ void jlSurfaceDestroy(jlSurfaceT *s) {
if (s == gStage) { if (s == gStage) {
return; return;
} }
halSurfaceFreePortData(s, false, s->portData); jlpSurfaceFreePortData(s, false, s->portData);
halSurfaceFreePixels(s->pixels); jlpSurfaceFreePixels(s->pixels);
free(s); free(s);
} }
@ -136,7 +136,7 @@ uint32_t jlSurfaceHash(const jlSurfaceT *s) {
if (s == NULL) { if (s == NULL) {
return 0u; return 0u;
} }
return halSurfaceHash(s); return jlpSurfaceHash(s);
} }
@ -164,7 +164,7 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
fclose(fp); fclose(fp);
return false; return false;
} }
if (!halSurfaceLoadFile(dst, fp)) { if (!jlpSurfaceLoadFile(dst, fp)) {
fclose(fp); fclose(fp);
return false; return false;
} }
@ -198,7 +198,7 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
if (fp == NULL) { if (fp == NULL) {
return false; return false;
} }
if (!halSurfaceSaveFile(src, fp)) { if (!jlpSurfaceSaveFile(src, fp)) {
fclose(fp); fclose(fp);
return false; return false;
} }
@ -280,26 +280,26 @@ bool stageAlloc(void) {
if (gStage == NULL) { if (gStage == NULL) {
return false; return false;
} }
// halStageAllocPixels returns NULL on planar ports (Amiga) where // jlpStageAllocPixels returns NULL on planar ports (Amiga) where
// the chunky shadow doesn't exist; the planes from portData are // the chunky shadow doesn't exist; the planes from portData are
// the source of truth. On chunky ports NULL pixels means the // the source of truth. On chunky ports NULL pixels means the
// back-buffer alloc failed (IIgs pins a fixed address and never // back-buffer alloc failed (IIgs pins a fixed address and never
// returns NULL, so this is a real OOM only on DOS). // returns NULL, so this is a real OOM only on DOS).
gStage->pixels = halStageAllocPixels(); gStage->pixels = jlpStageAllocPixels();
if (gStage->pixels != NULL) { if (gStage->pixels != NULL) {
memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE); memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
} }
gStage->portData = halSurfaceAllocPortData(gStage, true); gStage->portData = jlpSurfaceAllocPortData(gStage, true);
#ifdef JOEYLIB_NATIVE_CHUNKY #ifdef JOEYLIB_NATIVE_CHUNKY
if (gStage->pixels == NULL) { if (gStage->pixels == NULL) {
halSurfaceFreePortData(gStage, true, gStage->portData); jlpSurfaceFreePortData(gStage, true, gStage->portData);
free(gStage); free(gStage);
gStage = NULL; gStage = NULL;
return false; return false;
} }
#else #else
if (gStage->portData == NULL) { if (gStage->portData == NULL) {
halStageFreePixels(gStage->pixels); jlpStageFreePixels(gStage->pixels);
free(gStage); free(gStage);
gStage = NULL; gStage = NULL;
return false; return false;
@ -325,8 +325,8 @@ void stageFree(void) {
if (gStage == NULL) { if (gStage == NULL) {
return; return;
} }
halSurfaceFreePortData(gStage, true, gStage->portData); jlpSurfaceFreePortData(gStage, true, gStage->portData);
halStageFreePixels(gStage->pixels); jlpStageFreePixels(gStage->pixels);
free(gStage); free(gStage);
gStage = NULL; gStage = NULL;
} }

View file

@ -20,7 +20,7 @@
// (Amiga, Atari ST) it points to a port-private struct describing the // (Amiga, Atari ST) it points to a port-private struct describing the
// 4 bitplanes (Amiga: 4 separate plane buffers + stride; ST: single // 4 bitplanes (Amiga: 4 separate plane buffers + stride; ST: single
// interleaved buffer + stride). Cross-platform code never touches it // interleaved buffer + stride). Cross-platform code never touches it
// directly -- all primitive access goes through halFast* on planar // directly -- all primitive access goes through the jlp ops on planar
// ports. See project_planar_68k_plan.md for the full architecture. // ports. See project_planar_68k_plan.md for the full architecture.
struct jlSurfaceT { struct jlSurfaceT {
uint8_t *pixels; uint8_t *pixels;
@ -112,7 +112,7 @@ void stageDirtyClearAll(void);
// y -> byte offset of row y in a SURFACE_BYTES_PER_ROW-strided buffer. // y -> byte offset of row y in a SURFACE_BYTES_PER_ROW-strided buffer.
// On IIgs this expands to a single indexed long-mode read against // On IIgs this expands to a single indexed long-mode read against
// gRowOffsetLut (built once at halInit). // gRowOffsetLut (built once at jlpInit).
// //
// The explicit (y << 1) byte-pointer arithmetic dodges the multiply // The explicit (y << 1) byte-pointer arithmetic dodges the multiply
// helper that gets emitted for `uint16_t arr[N]` indexing // helper that gets emitted for `uint16_t arr[N]` indexing
@ -137,7 +137,7 @@ void coreSetError(const char *message);
// Allocate and free the library-owned stage (the back-buffer surface // Allocate and free the library-owned stage (the back-buffer surface
// that jlStagePresent flips to the display). Called from init.c during // that jlStagePresent flips to the display). Called from init.c during
// jlInit / jlShutdown. The stage's pixel storage is supplied by // jlInit / jlShutdown. The stage's pixel storage is supplied by
// the port HAL via halStageAllocPixels. // the port HAL via jlpStageAllocPixels.
bool stageAlloc(void); bool stageAlloc(void);
void stageFree(void); void stageFree(void);

View file

@ -11,7 +11,7 @@
#include <stddef.h> #include <stddef.h>
#include "joey/tile.h" #include "joey/tile.h"
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// (No <string.h> -- the 4-byte-per-row inner copies are spelled out // (No <string.h> -- the 4-byte-per-row inner copies are spelled out
@ -21,66 +21,13 @@
// ----- Prototypes ----- // ----- Prototypes -----
static void copyTileOpaque(uint8_t *dst, const uint8_t *src); // (copyTileOpaque / copyTileMasked moved to src/generic/genericTile.c as
static void copyTileMasked(uint8_t *dst, const uint8_t *src, uint8_t transparent); // jlpGenericTileCopy / jlpGenericTileCopyMasked.)
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
// 32-byte block copy with the surface row stride between rows. dst // (the chunky tile copy/masked-copy helpers now live in
// and src already point at the row-0 first byte of their respective // src/generic/genericTile.c as jlpGenericTileCopy / jlpGenericTileCopyMasked.)
// 8x8 regions.
static void copyTileOpaque(uint8_t *dst, const uint8_t *src) {
uint8_t row;
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
dst += SURFACE_BYTES_PER_ROW;
src += SURFACE_BYTES_PER_ROW;
}
}
// Same as copyTileOpaque but treats source nibbles equal to
// `transparent` as skip-through. The src bytes are inspected nibble-
// by-nibble; only non-transparent nibbles overwrite the destination.
static void copyTileMasked(uint8_t *dst, const uint8_t *src, uint8_t transparent) {
uint8_t row;
uint8_t col;
uint8_t srcByte;
uint8_t dstByte;
uint8_t transHi;
uint8_t transLo;
uint8_t srcHi;
uint8_t srcLo;
transHi = (uint8_t)((transparent & 0x0F) << 4);
transLo = (uint8_t)(transparent & 0x0F);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
for (col = 0; col < TILE_BYTES_PER_ROW; col++) {
srcByte = src[col];
srcHi = (uint8_t)(srcByte & 0xF0);
srcLo = (uint8_t)(srcByte & 0x0F);
// Both nibbles transparent: skip the byte entirely.
if (srcHi == transHi && srcLo == transLo) {
continue;
}
dstByte = dst[col];
if (srcHi != transHi) {
dstByte = (uint8_t)((dstByte & 0x0F) | srcHi);
}
if (srcLo != transLo) {
dstByte = (uint8_t)((dstByte & 0xF0) | srcLo);
}
dst[col] = dstByte;
}
dst += SURFACE_BYTES_PER_ROW;
src += SURFACE_BYTES_PER_ROW;
}
}
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
@ -119,12 +66,8 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontS
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
uint8_t *dstRow0; uint16_t dstPixelX;
const uint8_t *srcRow0; uint16_t dstPixelY;
uint16_t dstPixelX;
uint16_t dstPixelY;
uint16_t srcPixelX;
uint16_t srcPixelY;
if (dst == NULL || src == NULL) { if (dst == NULL || src == NULL) {
return; return;
@ -135,30 +78,16 @@ void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT
} }
dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE); dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE); dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
srcPixelX = (uint16_t)((uint16_t)srcBx * TILE_PIXELS_PER_SIDE); // Single op: machine asm/planar override or the generic chunky default.
srcPixelY = (uint16_t)((uint16_t)srcBy * TILE_PIXELS_PER_SIDE); jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
/* Skip the chunky path on planar ports (pixels NULL). */
if (dst->pixels != NULL && src->pixels != NULL) {
dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(dstPixelY) + (dstPixelX >> 1)];
srcRow0 = &src->pixels[SURFACE_ROW_OFFSET(srcPixelY) + (srcPixelX >> 1)];
if (!halFastTileCopy(dstRow0, srcRow0)) {
copyTileOpaque(dstRow0, srcRow0);
}
}
halTileCopyPlanes(dst, dstBx, dstBy, src, srcBx, srcBy);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY, surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
} }
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) { void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
uint8_t *dstRow0; uint16_t dstPixelX;
const uint8_t *srcRow0; uint16_t dstPixelY;
uint16_t dstPixelX;
uint16_t dstPixelY;
uint16_t srcPixelX;
uint16_t srcPixelY;
if (dst == NULL || src == NULL) { if (dst == NULL || src == NULL) {
return; return;
@ -169,25 +98,13 @@ void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSur
} }
dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE); dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE); dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
srcPixelX = (uint16_t)((uint16_t)srcBx * TILE_PIXELS_PER_SIDE); jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
srcPixelY = (uint16_t)((uint16_t)srcBy * TILE_PIXELS_PER_SIDE);
/* Skip the chunky path on planar ports (pixels NULL). */
if (dst->pixels != NULL && src->pixels != NULL) {
dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(dstPixelY) + (dstPixelX >> 1)];
srcRow0 = &src->pixels[SURFACE_ROW_OFFSET(srcPixelY) + (srcPixelX >> 1)];
if (!halFastTileCopyMasked(dstRow0, srcRow0, transparentIndex)) {
copyTileMasked(dstRow0, srcRow0, transparentIndex);
}
}
halTileCopyMaskedPlanes(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY, surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
} }
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
uint8_t doubled;
uint16_t pixelX; uint16_t pixelX;
uint16_t pixelY; uint16_t pixelY;
@ -197,23 +114,11 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return; return;
} }
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE); pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
doubled = (uint8_t)(((colorIndex & 0x0F) << 4) | (colorIndex & 0x0F)); // Single compile-time-selected op: machine asm/planar override or the
if (s->pixels != NULL // generic chunky default.
&& !halFastTileFill(s, bx, by, jlpTileFill(s, bx, by, colorIndex);
(uint16_t)((uint16_t)doubled | ((uint16_t)doubled << 8)))) {
uint8_t *row = &s->pixels[SURFACE_ROW_OFFSET(pixelY) + (pixelX >> 1)];
uint8_t i;
for (i = 0; i < TILE_PIXELS_PER_SIDE; i++) {
row[0] = doubled;
row[1] = doubled;
row[2] = doubled;
row[3] = doubled;
row += SURFACE_BYTES_PER_ROW;
}
}
halTileFillPlanes(s, bx, by, colorIndex);
surfaceMarkDirtyRect(s, (int16_t)pixelX, (int16_t)pixelY, surfaceMarkDirtyRect(s, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
} }
@ -221,55 +126,27 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *in, uint8_t fgColor, uint8_t bgColor) { const jlTileT *in, uint8_t fgColor, uint8_t bgColor) {
jlTileT colored;
uint8_t i;
uint8_t srcByte;
uint8_t hi;
uint8_t lo;
if (dst == NULL || in == NULL) { if (dst == NULL || in == NULL) {
return; return;
} }
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
fgColor &= 0x0Fu; fgColor &= 0x0Fu;
bgColor &= 0x0Fu; bgColor &= 0x0Fu;
// Single compile-time op: planar ports colorize in their own tile format
if (dst->pixels == NULL) { // (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste.
/* Planar port: in->pixels is in the port-specific layout // Replaces the old runtime pixels==NULL planar/chunky branch.
* produced by halTileSnapPlanes (plane-major on Amiga, jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor);
* row-major-with-planes-interleaved-per-row on ST). The surfaceMarkDirtyRect(dst, (int16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE),
* colorization formula -- write fgColor bits where the shape (int16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE),
* bit is 1, bgColor bits where it's 0 -- has to be applied in TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
* the port-specific format, so delegate to the port hook.
* Each port reads the shape bytes from its own layout and
* writes colorized planes back to its own destination format
* directly, without a chunky intermediate. */
halTilePasteMonoPlanes(dst, bx, by, &in->pixels[0], fgColor, bgColor);
surfaceMarkDirtyRect(dst, (int16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE),
(int16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE),
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
return;
}
// Walk the 32 bytes of 4bpp-packed source and emit a colorized
// copy: each non-zero source nibble becomes fgColor, each zero
// becomes bgColor. Then delegate to jlTilePaste so the chunky
// and planar write paths stay shared.
for (i = 0u; i < TILE_BYTES; i++) {
srcByte = in->pixels[i];
hi = (uint8_t)((srcByte >> 4) ? fgColor : bgColor);
lo = (uint8_t)((srcByte & 0x0Fu) ? fgColor : bgColor);
colored.pixels[i] = (uint8_t)((hi << 4) | lo);
}
jlTilePaste(dst, bx, by, &colored);
} }
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) { void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
uint8_t *dstRow; uint16_t pixelX;
const uint8_t *src; uint16_t pixelY;
uint16_t pixelX;
uint16_t pixelY;
uint8_t row;
if (dst == NULL || in == NULL) { if (dst == NULL || in == NULL) {
return; return;
@ -279,59 +156,21 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
} }
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE); pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
src = &in->pixels[0]; // Single op: machine asm/planar override or the generic chunky default.
/* Skip the chunky write path on planar ports (dst->pixels NULL) -- jlpTilePaste(dst, bx, by, &in->pixels[0]);
* mirrors jlTileSnap's pixels-NULL short-circuit. Saves the dstRow
* SURFACE_ROW_OFFSET multiply + halFastTilePaste jsr/rts per call
* on ST/Amiga where the planar path below does the real work. */
if (dst->pixels != NULL) {
dstRow = &dst->pixels[SURFACE_ROW_OFFSET(pixelY) + (pixelX >> 1)];
if (!halFastTilePaste(dstRow, src)) {
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
dstRow[0] = src[0];
dstRow[1] = src[1];
dstRow[2] = src[2];
dstRow[3] = src[3];
dstRow += SURFACE_BYTES_PER_ROW;
src += TILE_BYTES_PER_ROW;
}
}
}
halTilePastePlanes(dst, bx, by, &in->pixels[0]);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY, surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
} }
void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out) { void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out) {
const uint8_t *srcRow;
uint8_t *dst;
uint16_t pixelX;
uint16_t pixelY;
uint8_t row;
if (src == NULL || out == NULL) { if (src == NULL || out == NULL) {
return; return;
} }
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return; return;
} }
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE); // Read-only single op: machine asm/planar override or the generic chunky
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); // default. No dirty mark.
dst = &out->pixels[0]; jlpTileSnap(src, bx, by, &out->pixels[0]);
/* On planar ports (s->pixels NULL) the chunky read path is
* skipped; halTileSnapPlanes below derives the tile bytes from
* the bitplanes. */
if (src->pixels != NULL && !halFastTileSnap(dst, &src->pixels[SURFACE_ROW_OFFSET(pixelY) + (pixelX >> 1)])) {
srcRow = &src->pixels[SURFACE_ROW_OFFSET(pixelY) + (pixelX >> 1)];
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
dst[0] = srcRow[0];
dst[1] = srcRow[1];
dst[2] = srcRow[2];
dst[3] = srcRow[3];
srcRow += SURFACE_BYTES_PER_ROW;
dst += TILE_BYTES_PER_ROW;
}
}
halTileSnapPlanes(src, bx, by, &out->pixels[0]);
} }

View file

@ -2,16 +2,16 @@
// by 8237 auto-init DMA, fed by libxmp-lite's mixer. // by 8237 auto-init DMA, fed by libxmp-lite's mixer.
// //
// Pipeline: // Pipeline:
// * halAudioInit detects the card via DSP reset + version, allocates // * jlpAudioInit detects the card via DSP reset + version, allocates
// a 4 KB conventional-memory DMA buffer (aligned to not cross a // a 4 KB conventional-memory DMA buffer (aligned to not cross a
// 64 KB boundary), installs an IRQ handler on the SB's IRQ line, // 64 KB boundary), installs an IRQ handler on the SB's IRQ line,
// creates an xmp_context, and arms auto-init DMA at 22050 Hz mono // creates an xmp_context, and arms auto-init DMA at 22050 Hz mono
// 8-bit. The DMA buffer is split into two halves; the SB IRQ fires // 8-bit. The DMA buffer is split into two halves; the SB IRQ fires
// at the end of each half and our handler refills the *other* half // at the end of each half and our handler refills the *other* half
// by calling xmp_play_buffer with libxmp-lite's mixer output. // by calling xmp_play_buffer with libxmp-lite's mixer output.
// * halAudioPlayMod loads a .MOD blob through xmp_load_module and // * jlpAudioPlayMod loads a .MOD blob through xmp_load_module and
// starts the player; subsequent IRQs auto-pump the audio. // starts the player; subsequent IRQs auto-pump the audio.
// * halAudioPlaySfx routes through libxmp-lite's SMIX overlay so SFX // * jlpAudioPlaySfx routes through libxmp-lite's SMIX overlay so SFX
// mix on top of the music using xmp_smix_play_sample. // mix on top of the music using xmp_smix_play_sample.
// //
// Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7, // Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7,
@ -37,7 +37,7 @@
#include <libxmp-lite/xmp.h> #include <libxmp-lite/xmp.h>
#include "hal.h" #include "port.h"
#include "audioSfxMixInternal.h" #include "audioSfxMixInternal.h"
#include "joey/audio.h" #include "joey/audio.h"
@ -97,7 +97,7 @@
// MIX_RATE is the nominal target; the SB's 8-bit time-constant DAC can // MIX_RATE is the nominal target; the SB's 8-bit time-constant DAC can
// only clock at 1000000/(256-tc) Hz, which is not exactly 22050. The // only clock at 1000000/(256-tc) Hz, which is not exactly 22050. The
// SB hardware rate is the fixed one, so the effective rate derived from // SB hardware rate is the fixed one, so the effective rate derived from
// the same time constant (see gEffRate, set in halAudioInit) is the // the same time constant (see gEffRate, set in jlpAudioInit) is the
// single source of truth handed to libxmp and the SFX resampler. // single source of truth handed to libxmp and the SFX resampler.
#define MIX_RATE 22050 #define MIX_RATE 22050
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves #define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
@ -116,7 +116,7 @@ static uint8_t gDspMinor = 0;
static bool gDspPresent = false; static bool gDspPresent = false;
// Effective SB playback rate derived from the DSP time constant. Set // Effective SB playback rate derived from the DSP time constant. Set
// once in halAudioInit and used as the single rate source of truth for // once in jlpAudioInit and used as the single rate source of truth for
// libxmp's mixer and the SFX resampler, so they match what the DAC // libxmp's mixer and the SFX resampler, so they match what the DAC
// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting. // actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
static uint16_t gEffRate = MIX_RATE; static uint16_t gEffRate = MIX_RATE;
@ -145,7 +145,7 @@ static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
// ISR-side state. volatile so the main loop sees IRQ updates promptly. // ISR-side state. volatile so the main loop sees IRQ updates promptly.
// gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised // gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised
// in the IRQ handler and lowered in halAudioFrameTick after the // in the IRQ handler and lowered in jlpAudioFrameTick after the
// half has been refilled. We deliberately do NOT call libxmp-lite // half has been refilled. We deliberately do NOT call libxmp-lite
// from interrupt context -- the mixer allocates internally and is // from interrupt context -- the mixer allocates internally and is
// not reentrancy-safe -- so all work that touches the xmp context // not reentrancy-safe -- so all work that touches the xmp context
@ -426,7 +426,7 @@ static void picMaskIrq(uint8_t irq) {
// Refill one half of the DMA buffer from libxmp-lite's mixer. // Refill one half of the DMA buffer from libxmp-lite's mixer.
// Called from halAudioFrameTick on the main thread (NOT in IRQ // Called from jlpAudioFrameTick on the main thread (NOT in IRQ
// context); the IRQ only flags which halves need refilling. // context); the IRQ only flags which halves need refilling.
// xmp_play_buffer fills our bytes regardless of whether a module is // xmp_play_buffer fills our bytes regardless of whether a module is
// loaded -- it returns silence (centered 0x80) when idle. // loaded -- it returns silence (centered 0x80) when idle.
@ -448,7 +448,7 @@ static void refillHalf(uint8_t halfIdx) {
// SB IRQ. Acks the SB (read $0E), marks the half that just played // SB IRQ. Acks the SB (read $0E), marks the half that just played
// for refill, EOIs the PIC. The actual mixer work happens in // for refill, EOIs the PIC. The actual mixer work happens in
// halAudioFrameTick on the main thread. // jlpAudioFrameTick on the main thread.
static void sbIsr(void) { static void sbIsr(void) {
uint8_t fillHalf; uint8_t fillHalf;
@ -467,7 +467,7 @@ static void sbIsr(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halAudioInit(void) { bool jlpAudioInit(void) {
int major; int major;
int minor; int minor;
int vector; int vector;
@ -539,7 +539,7 @@ bool halAudioInit(void) {
} }
void halAudioShutdown(void) { void jlpAudioShutdown(void) {
int vector; int vector;
if (!gDspPresent) { if (!gDspPresent) {
@ -571,12 +571,12 @@ void halAudioShutdown(void) {
} }
bool halAudioIsPlayingMod(void) { bool jlpAudioIsPlayingMod(void) {
return gXmpStarted; return gXmpStarted;
} }
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gDspPresent || gXmpCtx == NULL) { if (!gDspPresent || gXmpCtx == NULL) {
return; return;
} }
@ -607,7 +607,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
@ -615,7 +615,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
} }
void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) { if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
@ -645,7 +645,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
#define PC_SPEAKER_PORT_GATE 0x61u #define PC_SPEAKER_PORT_GATE 0x61u
#define PC_SPEAKER_CTRL_BYTE 0xB6u #define PC_SPEAKER_CTRL_BYTE 0xB6u
void halAudioTone(uint16_t freqHz) { void jlpAudioTone(uint16_t freqHz) {
uint32_t divisor; uint32_t divisor;
uint8_t gate; uint8_t gate;
@ -802,11 +802,11 @@ static void adlibInit(void) {
// and subtract -- the math comes out to exactly 18.2065 Hz // and subtract -- the math comes out to exactly 18.2065 Hz
// regardless of our own IRQ rate. // regardless of our own IRQ rate.
// //
// 2. halMillisElapsed must report wall-clock ms even though // 2. jlpMillisElapsed must report wall-clock ms even though
// uclock()'s sub-tick PIT read is now wrong (PIT cycles // uclock()'s sub-tick PIT read is now wrong (PIT cycles
// 0..gPitDivisor, not 0..65535 as uclock assumes). We expose // 0..gPitDivisor, not 0..65535 as uclock assumes). We expose
// an own-ISR-counter-driven ms via the gAudioIsr* globals // an own-ISR-counter-driven ms via the gAudioIsr* globals
// below; src/port/dos/hal.c::halMillisElapsed prefers it // below; src/dos/hal.c::jlpMillisElapsed prefers it
// whenever gAudioIsrActive is true. // whenever gAudioIsrActive is true.
// //
// Literal "chain to old INT $08" doesn't work cleanly under DPMI // Literal "chain to old INT $08" doesn't work cleanly under DPMI
@ -822,7 +822,7 @@ static void adlibInit(void) {
#define BIOS_TICK_ADDR 0x46Cu #define BIOS_TICK_ADDR 0x46Cu
#define IRQ0_INT_VECTOR 0x08 #define IRQ0_INT_VECTOR 0x08
// Exposed (extern in hal.c) so halMillisElapsed can read wall-clock // Exposed (extern in hal.c) so jlpMillisElapsed can read wall-clock
// ms straight from the ISR counter and bypass the broken uclock. // ms straight from the ISR counter and bypass the broken uclock.
volatile uint32_t gAudioIsrFires = 0; volatile uint32_t gAudioIsrFires = 0;
uint16_t gAudioIsrHz = 0; uint16_t gAudioIsrHz = 0;
@ -863,7 +863,7 @@ static void programPit0(uint16_t divisor) {
} }
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
uint32_t divisor; uint32_t divisor;
if (fn == NULL) { if (fn == NULL) {
@ -915,7 +915,7 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
} }
void halAudioCriticalEnter(void) { void jlpAudioCriticalEnter(void) {
// "memory" clobber forces GCC to commit pending register-cached // "memory" clobber forces GCC to commit pending register-cached
// writes to memory before the cli (and reload after sti), so // writes to memory before the cli (and reload after sti), so
// the timer ISR can't observe a half-written critical section. // the timer ISR can't observe a half-written critical section.
@ -923,12 +923,12 @@ void halAudioCriticalEnter(void) {
} }
void halAudioCriticalExit(void) { void jlpAudioCriticalExit(void) {
__asm__ __volatile__("sti" ::: "memory"); __asm__ __volatile__("sti" ::: "memory");
} }
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
uint8_t car; uint8_t car;
uint8_t block; uint8_t block;
uint32_t fnum; uint32_t fnum;
@ -989,7 +989,7 @@ void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
} }
void halAudioStopMod(void) { void jlpAudioStopMod(void) {
if (!gDspPresent || gXmpCtx == NULL) { if (!gDspPresent || gXmpCtx == NULL) {
return; return;
} }
@ -1004,7 +1004,7 @@ void halAudioStopMod(void) {
} }
void halAudioStopSfx(uint8_t slot) { void jlpAudioStopSfx(uint8_t slot) {
if (slot >= JOEY_AUDIO_SFX_SLOTS) { if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
@ -1018,7 +1018,7 @@ void halAudioStopSfx(uint8_t slot) {
// 22050 Hz half-buffer of 2048 samples drains ~143000 samples/sec // 22050 Hz half-buffer of 2048 samples drains ~143000 samples/sec
// of headroom, well above the playback rate, so a missed frame or // of headroom, well above the playback rate, so a missed frame or
// two is OK without underflow. // two is OK without underflow.
void halAudioFrameTick(void) { void jlpAudioFrameTick(void) {
int i; int i;
if (!gDspPresent) { if (!gDspPresent) {

View file

@ -24,7 +24,7 @@
#include <sys/exceptn.h> #include <sys/exceptn.h>
#include <sys/nearptr.h> #include <sys/nearptr.h>
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// ----- Constants ----- // ----- Constants -----
@ -62,7 +62,7 @@ static uint8_t gPrevInVret;
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false; static bool gCacheValid = false;
// Expand LUT for halPresent. Maps a 4bpp-packed source byte to the // Expand LUT for jlpPresent. Maps a 4bpp-packed source byte to the
// 2-byte little-endian mode-13h output pair for the current scanline's // 2-byte little-endian mode-13h output pair for the current scanline's
// SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds // SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
// the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT // the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
@ -88,7 +88,7 @@ static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint
if (!gExpandLutValid || palBase != gExpandLutBase) { if (!gExpandLutValid || palBase != gExpandLutBase) {
rebuildExpandLut(palBase); rebuildExpandLut(palBase);
} }
// Dirty bands are chunky-word aligned (halPresent computes // Dirty bands are chunky-word aligned (jlpPresent computes
// pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source // pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source
// byte (= 2 nibbles) maps to 2 destination bytes. The even x keeps // byte (= 2 nibbles) maps to 2 destination bytes. The even x keeps
// &dst[x] 16-bit aligned, so each pair is one aligned 16-bit store // &dst[x] 16-bit aligned, so each pair is one aligned 16-bit store
@ -237,7 +237,7 @@ static void uploadPaletteIfNeeded(const jlSurfaceT *src) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halInit(const jlConfigT *config) { bool jlpInit(const jlConfigT *config) {
__dpmi_regs regs; __dpmi_regs regs;
(void)config; (void)config;
@ -267,12 +267,7 @@ bool halInit(const jlConfigT *config) {
} }
const char *halLastError(void) { void jlpPresent(const jlSurfaceT *src) {
return NULL;
}
void halPresent(const jlSurfaceT *src) {
int16_t y; int16_t y;
uint8_t minWord; uint8_t minWord;
uint8_t maxWord; uint8_t maxWord;
@ -313,7 +308,7 @@ void halPresent(const jlSurfaceT *src) {
// rising-edge wait (so back-to-back calls each yield one frame), // rising-edge wait (so back-to-back calls each yield one frame),
// first wait while we're already in retrace, then wait until we // first wait while we're already in retrace, then wait until we
// re-enter retrace. // re-enter retrace.
void halWaitVBL(void) { void jlpWaitVBL(void) {
while (inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT) { while (inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT) {
/* spin while still in current retrace */; /* spin while still in current retrace */;
} }
@ -323,7 +318,7 @@ void halWaitVBL(void) {
} }
uint16_t halFrameCount(void) { uint16_t jlpFrameCount(void) {
uint8_t now; uint8_t now;
now = (uint8_t)(inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT); now = (uint8_t)(inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT);
@ -335,7 +330,7 @@ uint16_t halFrameCount(void) {
} }
uint16_t halFrameHz(void) { uint16_t jlpFrameHz(void) {
/* VGA mode 13h vertical refresh on a real CRT runs at ~70 Hz /* VGA mode 13h vertical refresh on a real CRT runs at ~70 Hz
* (70.086 to be exact). Reporting 70 keeps ops/sec scaling * (70.086 to be exact). Reporting 70 keeps ops/sec scaling
* accurate within ~0.1%. */ * accurate within ~0.1%. */
@ -355,7 +350,7 @@ uint16_t halFrameHz(void) {
// because PIT 0 is at the BIOS default and DJGPP's sub-tick // because PIT 0 is at the BIOS default and DJGPP's sub-tick
// math holds. // math holds.
// //
// gAudioIsr* are owned by src/port/dos/audio.c. // gAudioIsr* are owned by src/dos/audio.c.
extern volatile uint32_t gAudioIsrFires; extern volatile uint32_t gAudioIsrFires;
extern uint16_t gAudioIsrHz; extern uint16_t gAudioIsrHz;
extern volatile bool gAudioIsrActive; extern volatile bool gAudioIsrActive;
@ -363,7 +358,7 @@ extern volatile bool gAudioIsrActive;
static uclock_t gMillisBase = 0; static uclock_t gMillisBase = 0;
static bool gMillisBaseSet = false; static bool gMillisBaseSet = false;
uint32_t halMillisElapsed(void) { uint32_t jlpMillisElapsed(void) {
uclock_t delta; uclock_t delta;
if (gAudioIsrActive && gAudioIsrHz > 0u) { if (gAudioIsrActive && gAudioIsrHz > 0u) {
@ -391,7 +386,7 @@ uint32_t halMillisElapsed(void) {
} }
void halShutdown(void) { void jlpShutdown(void) {
__dpmi_regs regs; __dpmi_regs regs;
if (gNearEnabled) { if (gNearEnabled) {
@ -411,321 +406,14 @@ void halShutdown(void) {
} }
// DOS has no asm fast paths yet; cross-platform code falls back to
// its C implementations when these return false.
bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
(void)s;
(void)doubled;
return false;
}
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)w;
(void)h;
(void)colorIndex;
return false;
}
bool halFastTileCopy(uint8_t *dstRow0, const uint8_t *srcRow0) {
(void)dstRow0;
(void)srcRow0;
return false;
}
bool halFastTileCopyMasked(uint8_t *dstRow0, const uint8_t *srcRow0, uint8_t transparent) {
(void)dstRow0;
(void)srcRow0;
(void)transparent;
return false;
}
bool halFastTilePaste(uint8_t *dstRow0, const uint8_t *srcTilePixels) {
(void)dstRow0;
(void)srcTilePixels;
return false;
}
bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
(void)dstTilePixels;
(void)srcRow0;
return false;
}
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)colorIndex;
return false;
}
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
(void)s;
(void)x0;
(void)y0;
(void)x1;
(void)y1;
(void)colorIndex;
return false;
}
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
(void)s;
(void)cx;
(void)cy;
(void)r;
(void)colorIndex;
return false;
}
bool halFastFloodWalk(uint8_t *row, int16_t startX, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)row;
(void)startX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)s; (void)startX; (void)y; (void)matchColor; (void)newColor; (void)matchEqual;
(void)seedMatched; (void)leftXOut; (void)rightXOut;
return false;
}
bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)s; (void)leftX; (void)rightX; (void)scanY; (void)matchColor; (void)newColor; (void)matchEqual;
(void)markBuf;
return false;
}
bool halFastFloodScanRow(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)markBuf;
return false;
}
bool halFastFloodScanAndPush(uint8_t *row, int16_t leftX, int16_t rightX, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t scanY, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp) {
(void)row;
(void)leftX;
(void)rightX;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)scanY;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
return false;
}
bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, int16_t *stackX, int16_t *stackY, int16_t *spInOut, int16_t maxSp, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
(void)pixels;
(void)x;
(void)y;
(void)matchColor;
(void)newColor;
(void)matchEqual;
(void)stackX;
(void)stackY;
(void)spInOut;
(void)maxSp;
(void)seedMatched;
(void)leftXOut;
(void)rightXOut;
return false;
}
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)s;
(void)bx;
(void)by;
(void)fillWord;
return false;
}
uint8_t *halStageAllocPixels(void) {
return (uint8_t *)malloc(SURFACE_PIXELS_SIZE);
}
void halStageFreePixels(uint8_t *pixels) {
free(pixels);
}
// DOS / VGA mode 13h is chunky-native (8bpp linear). portData is // DOS / VGA mode 13h is chunky-native (8bpp linear). portData is
// unused; the chunky `pixels` buffer feeds the present-time // unused; the chunky `pixels` buffer feeds the present-time
// nearest-neighbor copy to VGA RAM. // nearest-neighbor copy to VGA RAM.
void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
(void)s;
(void)isStage;
return NULL;
}
void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) { // surfaceCopyPlanes migrated: DOS defines no JL_HAS_SURFACE_COPY_PLANES, so it
(void)s; // links the no-op generic jlpGenericSurfaceCopyPlanes.
(void)isStage;
(void)portData;
}
// DOS has no bitplanes -- chunky pixels are the source of truth and
// expandAndWriteLine derives the VGA DAC indices straight from them.
// This hook is a stub here; the cross-platform jlFillRect calls it
// unconditionally.
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)w;
(void)h;
(void)colorIndex;
}
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
(void)dst;
(void)src;
}
void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
(void)s; (void)bx; (void)by; (void)colorIndex;
}
void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
(void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy;
}
void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
(void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy; (void)transparentIndex;
}
void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
(void)dst; (void)bx; (void)by; (void)chunkyTile;
}
void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
(void)dst; (void)bx; (void)by; (void)monoTile; (void)fgColor; (void)bgColor;
}
void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyTileOut) {
(void)src; (void)bx; (void)by; (void)chunkyTileOut;
}
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
(void)s; (void)sp; (void)x; (void)y;
}
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
(void)s; (void)x; (void)y; (void)w; (void)h; (void)dstPlaneBytes;
}
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
(void)s; (void)x; (void)y; (void)w; (void)h; (void)srcPlaneBytes;
}
/* Phase 9 reader hooks: chunky ports use the original s->pixels-based /* Phase 9 reader hooks: chunky ports use the original s->pixels-based
* paths. */ * paths. */
uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
if (x & 1) return (uint8_t)(byte & 0x0Fu);
return (uint8_t)((byte & 0xF0u) >> 4);
}
uint32_t halSurfaceHash(const jlSurfaceT *s) {
uint16_t lo = 0xACE1u, hi = 0x1357u, blocks, n, v;
const uint8_t *p;
const uint16_t *w;
uint8_t b;
p = s->pixels;
blocks = (uint16_t)(SURFACE_PIXELS_SIZE / 8);
do {
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
blocks--;
} while (blocks > 0u);
p = s->scb;
for (n = 0; n < (uint16_t)SURFACE_HEIGHT; n++) {
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
w = &s->palette[0][0];
for (n = 0; n < (uint16_t)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;
}
void halSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
memcpy(dst->pixels, src->pixels, SURFACE_PIXELS_SIZE);
}
bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
return fread(dst->pixels, 1, SURFACE_PIXELS_SIZE, fp) == SURFACE_PIXELS_SIZE;
}
bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
return fwrite(src->pixels, 1, SURFACE_PIXELS_SIZE, fp) == SURFACE_PIXELS_SIZE;
}
uint8_t *halSurfaceAllocPixels(void) {
return (uint8_t *)calloc(1, SURFACE_PIXELS_SIZE);
}
void halSurfaceFreePixels(uint8_t *pixels) {
free(pixels);
}
uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
(void)s; (void)planeIdx;
return NULL;
}

View file

@ -2,11 +2,11 @@
// //
// Keyboard: hooks INT 9 to capture AT set-1 scan codes from port 0x60. // 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 // The ISR reads the scan code, maps it to a jlKeyE, updates a private
// gIsrState buffer, and sends EOI to the PIC. halInputPoll snapshots // gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots
// gIsrState into gKeyState with interrupts disabled. // gIsrState into gKeyState with interrupts disabled.
// //
// The two-buffer split is required for jlKeyPressed edge detection. // The two-buffer split is required for jlKeyPressed edge detection.
// jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* halInputPoll // jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* jlpInputPoll
// runs, so whatever gKeyState holds at that moment becomes gKeyPrev. // runs, so whatever gKeyState holds at that moment becomes gKeyPrev.
// If the ISR wrote directly to gKeyState, press/release events that // If the ISR wrote directly to gKeyState, press/release events that
// happened between polls would already have landed there -- the memcpy // happened between polls would already have landed there -- the memcpy
@ -18,8 +18,8 @@
// will hang the machine. // will hang the machine.
// //
// Mouse: uses INT 33h, the standard MS DOS mouse driver interface. // Mouse: uses INT 33h, the standard MS DOS mouse driver interface.
// halInputInit calls function 0 (reset/detect), then 7/8 to clamp the // jlpInputInit calls function 0 (reset/detect), then 7/8 to clamp the
// driver's coordinate range to the surface dimensions. halInputPoll // driver's coordinate range to the surface dimensions. jlpInputPoll
// calls function 3 once per frame to read absolute X/Y and the button // 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 // bitmask. INT 33h is provided by every common DOS mouse driver and by
// DOSBox / DOSBox-X. // DOSBox / DOSBox-X.
@ -30,7 +30,7 @@
#include <pc.h> #include <pc.h>
#include <string.h> #include <string.h>
#include "hal.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -323,13 +323,13 @@ static void mousePoll(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
void halJoystickReset(jlJoystickE js) { void jlpJoystickReset(jlJoystickE js) {
// DOS sticks are digital -- no calibration to do. // DOS sticks are digital -- no calibration to do.
(void)js; (void)js;
} }
void halInputInit(void) { void jlpInputInit(void) {
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrState, 0, sizeof(gIsrState));
@ -353,7 +353,7 @@ void halInputInit(void) {
} }
void halInputPoll(void) { void jlpInputPoll(void) {
disable(); disable();
memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState));
enable(); enable();
@ -362,7 +362,7 @@ void halInputPoll(void) {
} }
void halInputShutdown(void) { void jlpInputShutdown(void) {
if (!gHooked) { if (!gHooked) {
return; return;
} }

76
src/generic/genericDraw.c Normal file
View file

@ -0,0 +1,76 @@
// Generic portable-C op defaults -- the new joeylib's "generic backend" (the
// joey.c-equivalent reference). A machine that does not override an op (no
// JL_HAS_<OP> defined for it in include/joey/platform.h) links these. An
// override may also defer edge cases here, e.g. the IIgs stage-only fast clear
// falls back to jlpGenericSurfaceClear for non-stage surfaces.
//
// These functions are always compiled into every target; the jlp<Op> dispatch
// in src/core/port.h routes to either a machine override or, by default, the
// matching generic here.
#include <string.h>
#include "surfaceInternal.h" // full jlSurfaceT (s->pixels) + SURFACE_PIXELS_SIZE
#include "port.h" // jlpGenericSurfaceClear prototype
void jlpGenericSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
if (s->pixels != NULL) {
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE);
}
}
// Chunky 4bpp on-surface rect fill. Caller has proven the rect is fully on
// surface (x,y >= 0; x+w <= WIDTH; y+h <= HEIGHT; w,h > 0). Planar ports have
// NULL s->pixels and override jlpFillRect with their plane fill, so this is the
// chunky default (IIgs non-stage / DOS).
void jlpGenericFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
uint8_t nibble = colorIndex & 0x0F;
uint8_t doubled = (uint8_t)((nibble << 4) | nibble);
int16_t row;
uint16_t pxStart;
uint16_t pxEnd;
uint16_t midBytes;
uint8_t *line;
if (s->pixels == NULL) {
return;
}
for (row = 0; row < h; row++) {
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
pxStart = (uint16_t)x;
pxEnd = (uint16_t)(x + w);
if (pxStart & 1u) {
line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0xF0) | nibble);
pxStart++;
}
midBytes = (uint16_t)((pxEnd - pxStart) >> 1);
if (midBytes > 0u) {
memset(&line[pxStart >> 1], doubled, (size_t)midBytes);
pxStart = (uint16_t)(pxStart + (midBytes << 1));
}
if (pxStart < pxEnd) {
line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0x0F) | (nibble << 4));
}
}
}
// 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;
uint8_t nibble;
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));
}
}

146
src/generic/genericPort.c Normal file
View file

@ -0,0 +1,146 @@
// Generic portable-C defaults for Category B platform SERVICES (timing, and --
// in later steps -- lifecycle/present/input/audio). Most are inherently
// platform-specific, so the "generic" is a no-op/zero stub that lets a blank/
// template port link; every real port overrides via JL_HAS_<OP>. A few have a
// genuine portable default (millisElapsed derives from the frame counter).
//
// See genericDraw.c for the jlp<Op> dispatch model. Always compiled in via the
// src/generic/*.c wildcard.
#include <stdlib.h>
#include "surfaceInternal.h"
#include "port.h"
// 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.
uint16_t jlpGenericFrameHz(void) {
return 60u;
}
// Monotonic frame/refresh counter. Each port reads its own hardware (VBL bar,
// tick counter, retrace edge); the stub returns 0 for a blank port.
uint16_t jlpGenericFrameCount(void) {
return 0u;
}
// Block until the next vertical blank. No portable implementation; a blank port
// gets a no-op (it simply won't pace to the display).
void jlpGenericWaitVBL(void) {
}
// Approximate elapsed milliseconds from the frame counter and refresh rate.
// This is the IIgs/Amiga path; ports with a real hardware timer (DOS uclock,
// ST 200 Hz tick) override via JL_HAS_MILLIS_ELAPSED.
uint32_t jlpGenericMillisElapsed(void) {
return (uint32_t)((uint32_t)jlpFrameCount() * 1000u / (uint32_t)jlpFrameHz());
}
// --- Audio services (platform-only stub generics; every real port overrides) ---
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;
(void)length;
(void)rateHz;
}
void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
(void)slot;
(void)fill;
(void)ctx;
(void)rateHz;
}
void jlpGenericAudioStopSfx(uint8_t slot) {
(void)slot;
}
void jlpGenericAudioTone(uint16_t freqHz) {
(void)freqHz;
}
void jlpGenericAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
(void)voice;
(void)freqHz;
(void)atten;
}
bool jlpGenericAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
(void)fn;
(void)hz;
return false;
}
void jlpGenericAudioCriticalEnter(void) {
}
void jlpGenericAudioCriticalExit(void) {
}
void jlpGenericAudioFrameTick(void) {
}
// --- Lifecycle / present / input services (platform-only stub generics) ---
bool jlpGenericInit(const jlConfigT *config) {
(void)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;
}
// --- Large allocation (malloc/free default; IIgs overrides via Memory Manager) ---
void *jlpGenericBigAlloc(uint32_t bytes) {
return malloc((size_t)bytes);
}
void jlpGenericBigFree(void *p) {
free(p);
}

View file

@ -0,0 +1,22 @@
// Generic sprite "planes" op defaults. The chunky software sprite blit lives in
// core/sprite.c (spriteDrawInterpreted) and the per-platform sprite codegen does
// the fast path; jlpSprite*Planes is the PLANAR mirror/interpreter, which has no
// work to do on a chunky target -- so the generic default is a no-op. Planar
// machines (Amiga/ST) override it; IIgs overrides it with a no-op macro (perf).
#include "port.h"
void jlpGenericSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
(void)s; (void)sp; (void)x; (void)y;
}
void jlpGenericSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
(void)s; (void)x; (void)y; (void)w; (void)h; (void)dstPlaneBytes;
}
void jlpGenericSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
(void)s; (void)x; (void)y; (void)w; (void)h; (void)srcPlaneBytes;
}

View file

@ -0,0 +1,145 @@
// Generic portable-C surface-service defaults -- the joey.c-equivalent reference
// for Category B structural ops (see genericDraw.c for the jlp<Op> dispatch
// model). A machine that does not override an op (no JL_HAS_<OP> in
// include/joey/platform.h) links these; the dispatch stanzas live in
// src/core/port.h. Always compiled into every target via the src/generic/*.c
// wildcard.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "surfaceInternal.h" // full jlSurfaceT (s->pixels) + SURFACE_PIXELS_SIZE
#include "port.h"
// jlSurfaceCopy's chunky payload copy. Guarded so it is a no-op on planar
// ports (pixels == NULL, plane copy is jlpSurfaceCopyPlanes' job); chunky ports
// memcpy the whole buffer.
void jlpGenericSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
if (dst->pixels != NULL && src->pixels != NULL) {
memcpy(dst->pixels, src->pixels, SURFACE_PIXELS_SIZE);
}
}
// Release a per-surface chunky pixel buffer. free(NULL) is a no-op, so this is
// safe on planar ports whose surfaces carry no chunky shadow.
void jlpGenericSurfaceFreePixels(uint8_t *pixels) {
free(pixels);
}
// jlSurfaceCopy's planar dual-write hook. No-op default: chunky ports carry no
// bitplanes (the chunky payload is jlpSurfaceCopyChunky's job), and a blank/
// template port links this until it provides its own. Planar ports (Amiga, ST)
// override via JL_HAS_SURFACE_COPY_PLANES.
void jlpGenericSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
(void)dst;
(void)src;
}
// No port currently surfaces a human-readable last-error string.
const char *jlpGenericLastError(void) {
return NULL;
}
// --- Chunky surface readers (generic default; planar ports override) ---
// Bodies are the IIgs/DOS chunky implementations verbatim; amiga/ST decode
// their portData planes in their own overrides (JL_HAS_*).
uint8_t jlpGenericSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
if (x & 1) return (uint8_t)(byte & 0x0Fu);
return (uint8_t)((byte & 0xF0u) >> 4);
}
uint32_t jlpGenericSurfaceHash(const jlSurfaceT *s) {
uint16_t lo = 0xACE1u, hi = 0x1357u, blocks, n, v;
const uint8_t *p;
const uint16_t *w;
uint8_t b;
p = s->pixels;
blocks = (uint16_t)(SURFACE_PIXELS_SIZE / 8);
do {
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
blocks--;
} while (blocks > 0u);
p = s->scb;
for (n = 0; n < (uint16_t)SURFACE_HEIGHT; n++) {
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
w = &s->palette[0][0];
for (n = 0; n < (uint16_t)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;
}
bool jlpGenericSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
return fread(dst->pixels, 1, SURFACE_PIXELS_SIZE, fp) == SURFACE_PIXELS_SIZE;
}
bool jlpGenericSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
return fwrite(src->pixels, 1, SURFACE_PIXELS_SIZE, fp) == SURFACE_PIXELS_SIZE;
}
// --- Allocation split by storage model (chunky generic defaults) ---
// Planar ports (Amiga/ST) override the per-surface allocators to manage their
// plane structs; IIgs overrides the stage allocators (pinned display memory).
// Stage back-buffer pixels: chunky default is a heap alloc.
uint8_t *jlpGenericStageAllocPixels(void) {
return (uint8_t *)malloc(SURFACE_PIXELS_SIZE);
}
void jlpGenericStageFreePixels(uint8_t *pixels) {
free(pixels);
}
// Per-surface chunky pixels: chunky ports calloc (zero-filled); planar ports
// override to return NULL (storage is the portData planes).
uint8_t *jlpGenericSurfaceAllocPixels(void) {
return (uint8_t *)calloc(1, SURFACE_PIXELS_SIZE);
}
// Per-surface portData: NULL on chunky ports (unused); planar ports override.
void *jlpGenericSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
(void)s;
(void)isStage;
return NULL;
}
void jlpGenericSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
(void)s;
(void)isStage;
(void)portData;
}
// Bitplane base pointer: NULL on chunky ports (no planes); planar ports override.
uint8_t *jlpGenericSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
(void)s;
(void)planeIdx;
return NULL;
}

150
src/generic/genericTile.c Normal file
View file

@ -0,0 +1,150 @@
// Generic portable-C tile op defaults (chunky 4bpp). The compile-time jlp<Op>
// dispatch in src/core/port.h routes to these when a machine declares no
// JL_HAS_<OP> for the tile op; machine overrides (IIgs asm macros, Amiga/ST
// planar functions) live in the machine folders.
#include "joey/tile.h"
#include "surfaceInternal.h"
#include "port.h"
// Derive the chunky row-0 pointer for tile block (bx,by) within a surface.
#define GENERIC_TILE_ROW0(_pix, _bx, _by) \
(&(_pix)[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)])
void jlpGenericTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
uint8_t *d;
const uint8_t *s;
uint8_t row;
if (dst->pixels == NULL || src->pixels == NULL) {
return;
}
d = GENERIC_TILE_ROW0(dst->pixels, dstBx, dstBy);
s = GENERIC_TILE_ROW0(src->pixels, srcBx, srcBy);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
d += SURFACE_BYTES_PER_ROW;
s += SURFACE_BYTES_PER_ROW;
}
}
void jlpGenericTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) {
uint8_t *d;
const uint8_t *s;
uint8_t row;
uint8_t col;
uint8_t srcByte;
uint8_t dstByte;
uint8_t transHi = (uint8_t)((transparent & 0x0F) << 4);
uint8_t transLo = (uint8_t)(transparent & 0x0F);
uint8_t srcHi;
uint8_t srcLo;
if (dst->pixels == NULL || src->pixels == NULL) {
return;
}
d = GENERIC_TILE_ROW0(dst->pixels, dstBx, dstBy);
s = GENERIC_TILE_ROW0(src->pixels, srcBx, srcBy);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
for (col = 0; col < TILE_BYTES_PER_ROW; col++) {
srcByte = s[col];
srcHi = (uint8_t)(srcByte & 0xF0);
srcLo = (uint8_t)(srcByte & 0x0F);
if (srcHi == transHi && srcLo == transLo) {
continue;
}
dstByte = d[col];
if (srcHi != transHi) {
dstByte = (uint8_t)((dstByte & 0x0F) | srcHi);
}
if (srcLo != transLo) {
dstByte = (uint8_t)((dstByte & 0xF0) | srcLo);
}
d[col] = dstByte;
}
d += SURFACE_BYTES_PER_ROW;
s += SURFACE_BYTES_PER_ROW;
}
}
// Colorize a 1bpp-ish mono shape (non-zero nibble -> fg, zero -> bg) into a
// chunky tile, then paste via the jlpTilePaste op (IIgs asm / DOS generic). The
// caller (jlTilePasteMono) marks the rect dirty, so this does not.
void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
jlTileT colored;
uint8_t i;
uint8_t srcByte;
uint8_t hi;
uint8_t lo;
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
for (i = 0u; i < TILE_BYTES; i++) {
srcByte = monoTile[i];
hi = (uint8_t)((srcByte >> 4) ? fgColor : bgColor);
lo = (uint8_t)((srcByte & 0x0Fu) ? fgColor : bgColor);
colored.pixels[i] = (uint8_t)((hi << 4) | lo);
}
jlpTilePaste(dst, bx, by, &colored.pixels[0]);
}
// Paste a chunky 8x8 tile (32 bytes, 4-bytes/row) into surface block (bx,by).
void jlpGenericTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
uint8_t *d;
const uint8_t *s = chunkyTile;
uint8_t row;
if (dst->pixels == NULL) {
return;
}
d = GENERIC_TILE_ROW0(dst->pixels, bx, by);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
d += SURFACE_BYTES_PER_ROW;
s += TILE_BYTES_PER_ROW;
}
}
// Read surface block (bx,by) into a chunky 8x8 tile buffer (32 bytes).
void jlpGenericTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) {
const uint8_t *s;
uint8_t *d = chunkyOut;
uint8_t row;
if (src->pixels == NULL) {
return;
}
s = GENERIC_TILE_ROW0(src->pixels, bx, by);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
s += SURFACE_BYTES_PER_ROW;
d += TILE_BYTES_PER_ROW;
}
}
void jlpGenericTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
uint8_t doubled = (uint8_t)(((colorIndex & 0x0F) << 4) | (colorIndex & 0x0F));
uint16_t pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
uint16_t pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
uint8_t *row;
uint8_t i;
if (s->pixels == NULL) {
return;
}
row = &s->pixels[SURFACE_ROW_OFFSET(pixelY) + (pixelX >> 1)];
for (i = 0; i < TILE_PIXELS_PER_SIDE; i++) {
row[0] = doubled;
row[1] = doubled;
row[2] = doubled;
row[3] = doubled;
row += SURFACE_BYTES_PER_ROW;
}
}

View file

@ -5,7 +5,7 @@
// references they actually use. // references they actually use.
// //
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time to // The NinjaTrackerPlus replayer is Merlin32-assembled at build time to
// ntpplayer.bin and LOADED FROM DISK at runtime (NTP_PATH). halAudioInit // ntpplayer.bin and LOADED FROM DISK at runtime (NTP_PATH). jlpAudioInit
// reads those bytes into a fixed-bank Memory Manager handle and JSLs into // reads those bytes into a fixed-bank Memory Manager handle and JSLs into
// them through the self-modifying call stub below; NTP is bank-internal / // them through the self-modifying call stub below; NTP is bank-internal /
// position-independent so it runs at whatever address Memory Manager picked. // position-independent so it runs at whatever address Memory Manager picked.
@ -28,7 +28,7 @@ typedef void *Pointer;
#define attrFixed 0x4000 #define attrFixed 0x4000
#define attrLocked 0x8000 #define attrLocked 0x8000
#include "hal.h" #include "port.h"
#include "joey/audio.h" #include "joey/audio.h"
// The ~34 KB NTP replayer is LOADED FROM DISK at runtime, not baked into // The ~34 KB NTP replayer is LOADED FROM DISK at runtime, not baked into
@ -95,7 +95,7 @@ static uint32_t gSfxBase = 0;
static bool gNTPReady = false; static bool gNTPReady = false;
static bool gNTPPlaying = false; static bool gNTPPlaying = false;
// Per-slot config cache. halAudioPlaySfx's biggest cost is the // Per-slot config cache. jlpAudioPlaySfx's biggest cost is the
// per-byte XOR-with-$80 loop over the entire sample (signed -> DOC's // per-byte XOR-with-$80 loop over the entire sample (signed -> DOC's
// unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms. // unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms.
// Most callers play the same SFX repeatedly into the same // Most callers play the same SFX repeatedly into the same
@ -211,7 +211,7 @@ static bool loadNTP(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool halAudioInit(void) { bool jlpAudioInit(void) {
Handle modHandle; Handle modHandle;
Handle sfxHandle; Handle sfxHandle;
@ -258,12 +258,12 @@ bool halAudioInit(void) {
} }
void halAudioShutdown(void) { void jlpAudioShutdown(void) {
if (!gNTPReady) { if (!gNTPReady) {
return; return;
} }
if (gNTPPlaying) { if (gNTPPlaying) {
halAudioStopMod(); jlpAudioStopMod();
} }
// Silence every SFX slot before disposing the handles. NTP's DOC // Silence every SFX slot before disposing the handles. NTP's DOC
// IRQ vector points into the buffer we are about to free; if any // IRQ vector points into the buffer we are about to free; if any
@ -274,7 +274,7 @@ void halAudioShutdown(void) {
{ {
uint8_t i; uint8_t i;
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) { for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
halAudioStopSfx(i); jlpAudioStopSfx(i);
gSfxSlotSample[i] = (const uint8_t *)0; gSfxSlotSample[i] = (const uint8_t *)0;
gSfxSlotLength[i] = 0; gSfxSlotLength[i] = 0;
gSfxSlotRateHz[i] = 0; gSfxSlotRateHz[i] = 0;
@ -299,12 +299,12 @@ void halAudioShutdown(void) {
} }
bool halAudioIsPlayingMod(void) { bool jlpAudioIsPlayingMod(void) {
return gNTPReady && gNTPPlaying; return gNTPReady && gNTPPlaying;
} }
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gNTPReady || gModuleHandle == NULL) { if (!gNTPReady || gModuleHandle == NULL) {
return; return;
} }
@ -313,7 +313,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
// Drop any previous song before loading a new one. // Drop any previous song before loading a new one.
if (gNTPPlaying) { if (gNTPPlaying) {
halAudioStopMod(); jlpAudioStopMod();
} }
BlockMove((Pointer)data, (Pointer)gModuleBase, length); BlockMove((Pointer)data, (Pointer)gModuleBase, length);
@ -341,7 +341,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
unsigned char *sfx; unsigned char *sfx;
uint32_t slotBase; uint32_t slotBase;
uint32_t sampleAddr; uint32_t sampleAddr;
@ -427,7 +427,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
// fully-resident samples handed to NTPstreamsound. Live refill from // fully-resident samples handed to NTPstreamsound. Live refill from
// a callback would need a DOC-IRQ ring buffer, which is a separate // a callback would need a DOC-IRQ ring buffer, which is a separate
// project. No-op until then. // project. No-op until then.
void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
(void)slot; (void)slot;
(void)fill; (void)fill;
(void)ctx; (void)ctx;
@ -440,7 +440,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
// already cooperate with the NTP MOD engine (SFX uses DOC oscillators // already cooperate with the NTP MOD engine (SFX uses DOC oscillators
// 14-21; NTP uses the lower ones). Loudness comes from the sample // 14-21; NTP uses the lower ones). Loudness comes from the sample
// amplitude, so no per-oscillator DOC volume plumbing is needed; the // amplitude, so no per-oscillator DOC volume plumbing is needed; the
// host re-asserts active voices each frame via halAudioFrameTick (the // host re-asserts active voices each frame via jlpAudioFrameTick (the
// registered tick fn), which sustains them between the one-shot triggers. // registered tick fn), which sustains them between the one-shot triggers.
#define TONE_RATE 26000U // DOC playback rate for synthesized PSG #define TONE_RATE 26000U // DOC playback rate for synthesized PSG
#define TONE_BUF_BYTES 2048 // ~2 frames of square wave @ TONE_RATE #define TONE_BUF_BYTES 2048 // ~2 frames of square wave @ TONE_RATE
@ -476,7 +476,7 @@ static uint16_t toneFill(uint16_t halfPeriod, int8_t amp) {
} }
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
uint16_t halfPeriod; uint16_t halfPeriod;
uint16_t len; uint16_t len;
int8_t amp; int8_t amp;
@ -485,7 +485,7 @@ void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
return; return;
} }
if (freqHz == 0u || atten >= AGI_ATTEN_OFF) { if (freqHz == 0u || atten >= AGI_ATTEN_OFF) {
halAudioStopSfx(voice); // freq 0 / max atten = silence jlpAudioStopSfx(voice); // freq 0 / max atten = silence
return; return;
} }
// AGI attenuation 0..14 -> amplitude 127..8 (linear; loudness is in the // AGI attenuation 0..14 -> amplitude 127..8 (linear; loudness is in the
@ -494,14 +494,14 @@ void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
halfPeriod = (uint16_t)((TONE_RATE / 2u) / freqHz); halfPeriod = (uint16_t)((TONE_RATE / 2u) / freqHz);
len = toneFill(halfPeriod, amp); len = toneFill(halfPeriod, amp);
// gToneBuf is shared across voices and its contents change every call, // gToneBuf is shared across voices and its contents change every call,
// so bust halAudioPlaySfx's (ptr,len,rate) copy-cache to force a refresh. // so bust jlpAudioPlaySfx's (ptr,len,rate) copy-cache to force a refresh.
gSfxSlotSample[voice] = (const uint8_t *)0; gSfxSlotSample[voice] = (const uint8_t *)0;
halAudioPlaySfx(voice, (const uint8_t *)gToneBuf, len, TONE_RATE); jlpAudioPlaySfx(voice, (const uint8_t *)gToneBuf, len, TONE_RATE);
} }
void halAudioTone(uint16_t freqHz) { void jlpAudioTone(uint16_t freqHz) {
halAudioVoice(0u, freqHz, 0u); // single full-volume tone on voice 0 jlpAudioVoice(0u, freqHz, 0u); // single full-volume tone on voice 0
} }
@ -509,7 +509,7 @@ void halAudioTone(uint16_t freqHz) {
// ride the host's per-frame jlAudioFrameTick pump rather than install a // ride the host's per-frame jlAudioFrameTick pump rather than install a
// raw VBL ISR (which would fight GS/OS heartbeat tasks). hz > 60 clamps to // raw VBL ISR (which would fight GS/OS heartbeat tasks). hz > 60 clamps to
// per-frame; lower rates divide down. // per-frame; lower rates divide down.
bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
if (fn == (jlAudioTickFnT)0 || hz == 0u) { if (fn == (jlAudioTickFnT)0 || hz == 0u) {
gTickFn = (jlAudioTickFnT)0; gTickFn = (jlAudioTickFnT)0;
return false; return false;
@ -521,15 +521,15 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
} }
void halAudioCriticalEnter(void) { void jlpAudioCriticalEnter(void) {
} }
void halAudioCriticalExit(void) { void jlpAudioCriticalExit(void) {
} }
void halAudioStopMod(void) { void jlpAudioStopMod(void) {
if (!gNTPReady || !gNTPPlaying) { if (!gNTPReady || !gNTPPlaying) {
return; return;
} }
@ -543,7 +543,7 @@ void halAudioStopMod(void) {
} }
void halAudioStopSfx(uint8_t slot) { void jlpAudioStopSfx(uint8_t slot) {
uint8_t firstOsc; uint8_t firstOsc;
uint8_t i; uint8_t i;
@ -566,9 +566,9 @@ void halAudioStopSfx(uint8_t slot) {
} }
void halAudioFrameTick(void) { void jlpAudioFrameTick(void) {
// NTP is DOC-IRQ driven; nothing to pump there. But a registered audio // NTP is DOC-IRQ driven; nothing to pump there. But a registered audio
// tick (halAudioTickRegister) rides this per-frame hook to re-assert PSG // tick (jlpAudioTickRegister) rides this per-frame hook to re-assert PSG
// voices and advance AGI-style sound at up to IIGS_FRAME_HZ. // voices and advance AGI-style sound at up to IIGS_FRAME_HZ.
if (gTickFn != (jlAudioTickFnT)0) { if (gTickFn != (jlAudioTickFnT)0) {
gTickCount--; gTickCount--;

View file

@ -11,9 +11,9 @@
// //
// DIRTY-WALK + PEI-SLAM PRESENT // DIRTY-WALK + PEI-SLAM PRESENT
// ----------------------------- // -----------------------------
// halPresent walks the per-row dirty bands maintained by drawing // jlpPresent walks the per-row dirty bands maintained by drawing
// primitives in src/core/*.c. Fully-dirty rows go through the PEI // primitives in src/core/*.c. Fully-dirty rows go through the PEI
// slam in src/port/iigs/peislam.s (~530 cyc/row, ~55% faster than // slam in src/iigs/peislam.s (~530 cyc/row, ~55% faster than
// memcpy/MVN); partial-dirty rows use memcpy, which lowers // memcpy/MVN); partial-dirty rows use memcpy, which lowers
// to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into // to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into
// bank $E1 when the dirty band is too narrow to amortize the slam's // bank $E1 when the dirty band is too narrow to amortize the slam's
@ -29,11 +29,11 @@
#include <string.h> #include <string.h>
#include "joey/debug.h" #include "joey/debug.h"
#include "hal.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// Memory Manager surface for halBigAlloc / halBigFree (jlAlloc backing). // Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing).
// Same idiom as src/port/iigs/audio_full.c: Handles/pointers are void*, // Same idiom as src/iigs/audio_full.c: Handles/pointers are void*,
// the owner ID comes from MMStartUp(), and the attribute bit values are // the owner ID comes from MMStartUp(), and the attribute bit values are
// Apple's Memory Manager constants. Only the bits we use are defined. // Apple's Memory Manager constants. Only the bits we use are defined.
#include <iigs/toolbox.h> #include <iigs/toolbox.h>
@ -58,17 +58,16 @@ extern uint16_t iigsGetTickWord(void);
extern uint16_t iigsReadHzParam(void); extern uint16_t iigsReadHzParam(void);
static uint16_t gFrameHz = 60u; static uint16_t gFrameHz = 60u;
// hal.c is the single TU that calls into joeyDraw.asm. Cross- // hal.c is the single TU that calls into joeyDraw.asm. The IIgs jlp<Op>
// platform draw.c / tile.c / etc. dispatch through halFast* // macros in core/port.h expand to these asm entry points; cross-platform
// functions defined here; they never reference the asm symbols // draw.c / tile.c / etc. never reference the asm symbols directly. Keeping
// directly. Keeping the asm externs in a single TU avoids the // the asm externs in a single TU avoids the link-time fragility seen when
// link-time fragility seen when each cross-platform TU brought its // each cross-platform TU brought its own asm extern.
// own asm extern.
// 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen. // 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen.
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
// Full-fill asm helper (partial leading byte + middle MVN + partial // Full-fill asm helper (partial leading byte + middle MVN + partial
// trailing byte). Called by halFastFillRect below. // trailing byte). Called by the jlpFillRect macro (core/port.h).
extern void iigsFillRectInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble); extern void iigsFillRectInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
// 16 STA abs,X stores at fixed offsets along a 160-byte stride. // 16 STA abs,X stores at fixed offsets along a 160-byte stride.
// ~120 cyc per call. // ~120 cyc per call.
@ -94,11 +93,11 @@ extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1
// Stage-to-SHR full upload: pixels (MVN $01->$E1), SCB, palette. // Stage-to-SHR full upload: pixels (MVN $01->$E1), SCB, palette.
// Asm uses post-MVN DBR=$E1 to do sta abs,Y for SCB/palette. // Asm uses post-MVN DBR=$E1 to do sta abs,Y for SCB/palette.
// The asm performs the upload directly rather than a C-side memcpy, // The asm performs the upload directly rather than a C-side memcpy,
// which is unreliable when called from halPresent (DBR-state quirk // which is unreliable when called from jlpPresent (DBR-state quirk
// after prior asm primitives). // after prior asm primitives).
extern void iigsBlitStageToShr(const uint8_t *scbPtr, const uint16_t *palettePtr, uint16_t uploadFlags); extern void iigsBlitStageToShr(const uint8_t *scbPtr, const uint16_t *palettePtr, uint16_t uploadFlags);
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner, // jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
// read back by halFastFloodWalkAndScans. // read back by the jlpFloodWalkAndScans macro (core/port.h).
extern uint16_t gFloodSeedMatch; extern uint16_t gFloodSeedMatch;
extern uint16_t gFloodLeftX; extern uint16_t gFloodLeftX;
extern uint16_t gFloodRightX; extern uint16_t gFloodRightX;
@ -107,12 +106,12 @@ extern uint16_t gFloodRightX;
// match decoders. Outputs to gFloodSeedMatch / gFloodLeftX / gFloodRightX. // 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); 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 // One-shot init for the y*160 lookup table (gRowOffsetLut, 400 bytes
// in DRAWPRIMS data). Called once from halInit. After this returns, // in DRAWPRIMS data). Called once from jlpInit. After this returns,
// every asm primitive that needs row offset can do `lda >lut,x` instead // every asm primitive that needs row offset can do `lda >lut,x` instead
// of the 7-instruction shift-add. // of the 7-instruction shift-add.
extern void iigsInitRowLut(void); extern void iigsInitRowLut(void);
// Per-row MVN blit from $01:srcOffset to $E1:srcOffset for partial- // Per-row MVN blit from $01:srcOffset to $E1:srcOffset for partial-
// screen presents (halPresentRect). srcOffset is the byte offset // screen presents (the partial-rect present). srcOffset is the byte offset
// within bank $01 of the FIRST byte to copy on the FIRST row; // within bank $01 of the FIRST byte to copy on the FIRST row;
// subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs // subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs
// C memcpy's ~30 cyc/byte. // C memcpy's ~30 cyc/byte.
@ -166,7 +165,7 @@ extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint1
// $C034 BORDER register: high nibble = beep/IRQ enables (preserve), // $C034 BORDER register: high nibble = beep/IRQ enables (preserve),
// low nibble = border color index 0..15. Color 0 is the all-zero // low nibble = border color index 0..15. Color 0 is the all-zero
// palette entry by IIgs convention; we force the low nibble to 0 // palette entry by IIgs convention; we force the low nibble to 0
// in halInit so the visible bezel matches the cleared SHR background. // in jlpInit so the visible bezel matches the cleared SHR background.
#define BORDER_COLOR_MASK 0xF0 #define BORDER_COLOR_MASK 0xF0
// ----- Module state ----- // ----- Module state -----
@ -196,7 +195,6 @@ volatile uint16_t gPeiMvnCount; // narrow-extent present: MVN byte coun
// iigsBlitStageToShr above (with dirty-row skip). // iigsBlitStageToShr above (with dirty-row skip).
// Upload SCB / palette into bank-$E1 SHR memory only when the // Upload SCB / palette into bank-$E1 SHR memory only when the
// matching dirty flag is set. Replaces a per-frame 712-byte memcmp // matching dirty flag is set. Replaces a per-frame 712-byte memcmp
// pair (~7 ms / frame) with a 2-cyc flag check. // pair (~7 ms / frame) with a 2-cyc flag check.
@ -226,7 +224,7 @@ static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) {
// pointer. The 65816 has no alignment fault, so the BIG_ALLOC_HDR-byte // pointer. The 65816 has no alignment fault, so the BIG_ALLOC_HDR-byte
// offset applied to the master pointer is safe for any access pattern. // offset applied to the master pointer is safe for any access pattern.
// The returned memory is not zero-filled (matches malloc semantics). // The returned memory is not zero-filled (matches malloc semantics).
void *halBigAlloc(uint32_t bytes) { void *jlpBigAlloc(uint32_t bytes) {
Handle h; Handle h;
uint8_t *base; uint8_t *base;
@ -241,13 +239,13 @@ void *halBigAlloc(uint32_t bytes) {
DisposeHandle(h); DisposeHandle(h);
return NULL; return NULL;
} }
// Stash the Handle in the leading slot so halBigFree can recover it. // Stash the Handle in the leading slot so jlpBigFree can recover it.
*(Handle *)base = h; *(Handle *)base = h;
return base + BIG_ALLOC_HDR; return base + BIG_ALLOC_HDR;
} }
void halBigFree(void *p) { void jlpBigFree(void *p) {
Handle h; Handle h;
if (p == NULL) { if (p == NULL) {
@ -258,7 +256,7 @@ void halBigFree(void *p) {
} }
bool halInit(const jlConfigT *config) { bool jlpInit(const jlConfigT *config) {
(void)config; (void)config;
gPreviousNewVideo = *IIGS_NEWVIDEO_REG; gPreviousNewVideo = *IIGS_NEWVIDEO_REG;
gPreviousBorder = *IIGS_BORDER_REG; gPreviousBorder = *IIGS_BORDER_REG;
@ -269,9 +267,9 @@ bool halInit(const jlConfigT *config) {
// write to $01/2000-9FFF mirrors to $E1 and the off-screen-buffer // write to $01/2000-9FFF mirrors to $E1 and the off-screen-buffer
// illusion breaks (the user would see drawing in progress). // illusion breaks (the user would see drawing in progress).
*IIGS_SHADOW_REG = (uint8_t)(gPreviousShadow | SHADOW_INHIBIT_SHR_MASK); *IIGS_SHADOW_REG = (uint8_t)(gPreviousShadow | SHADOW_INHIBIT_SHR_MASK);
// SCB and palette are uploaded by halPresent's iigsBlitStageToShr // SCB and palette are uploaded by jlpPresent's iigsBlitStageToShr
// (asm path, MVN to bank $E1). C-side memset/memcpy to bank $E1 // (asm path, MVN to bank $E1). C-side memset/memcpy to bank $E1
// is unreliable from halInit's calling context, so we don't try // is unreliable from jlpInit's calling context, so we don't try
// it here -- the first present will set up SCB to 320 mode. // it here -- the first present will set up SCB to 320 mode.
iigsInitRowLut(); iigsInitRowLut();
gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u; gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u;
@ -280,12 +278,7 @@ bool halInit(const jlConfigT *config) {
} }
const char *halLastError(void) { void jlpPresent(const jlSurfaceT *src) {
return NULL;
}
void halPresent(const jlSurfaceT *src) {
if (src == NULL) { if (src == NULL) {
return; return;
} }
@ -306,7 +299,7 @@ void halPresent(const jlSurfaceT *src) {
} }
void halShutdown(void) { void jlpShutdown(void) {
if (gModeSet) { if (gModeSet) {
*IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_NEWVIDEO_REG = gPreviousNewVideo;
*IIGS_BORDER_REG = gPreviousBorder; *IIGS_BORDER_REG = gPreviousBorder;
@ -316,28 +309,19 @@ void halShutdown(void) {
} }
// halFastSurfaceClear / halFastDrawLine / halFastDrawCircle / // The IIgs draw/tile/flood primitives (surfaceClear, drawLine, drawCircle,
// halFastFillCircle / halFastTileCopy / halFastTileCopyMasked / // fillCircle, fillRect, tileCopy / tileCopyMasked / tilePaste / tileSnap /
// halFastTilePaste / halFastTileSnap / halFastTileFill / // tileFill, floodWalkAndScans) are all dispatched via the jlp<Op> macros in
// halFastFloodWalk[AndScans] / halFastFloodScanRow / // core/port.h (#ifdef JOEYLIB_PLATFORM_IIGS block); they expand to the
// halFastFloodScanAndPush all dispatch via macros in core/hal.h on // iigs*Inner asm entry points above, so no C wrapper functions live here.
// IIgs (#ifdef JOEYLIB_PLATFORM_IIGS block). Only halFastFillRect
// remains a real function below because its partial-byte (nibble-
// edge) handling is too gnarly for a macro.
// halFastFillRect: macro-dispatched in core/hal.h, same as the other uint8_t *jlpStageAllocPixels(void) {
// halFast* primitives. The C wrapper that used to live here was kept
// as load-bearing _ROOT mass to defeat linker bank fragility;
// since the CORESYS migration drained _ROOT, the macro form is safe.
uint8_t *halStageAllocPixels(void) {
return IIGS_STAGE_PIXELS; return IIGS_STAGE_PIXELS;
} }
void halStageFreePixels(uint8_t *pixels) { void jlpStageFreePixels(uint8_t *pixels) {
(void)pixels; (void)pixels;
// Backing memory is hardware-pinned; nothing to free. // Backing memory is hardware-pinned; nothing to free.
} }
@ -346,102 +330,22 @@ void halStageFreePixels(uint8_t *pixels) {
// IIgs is chunky-native: portData is unused. The chunky `pixels` // IIgs is chunky-native: portData is unused. The chunky `pixels`
// buffer at $01:2000 is the stage's pixel storage and the source for // buffer at $01:2000 is the stage's pixel storage and the source for
// jlStagePresent's PEI-slam to $E1. // jlStagePresent's PEI-slam to $E1.
void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
(void)s;
(void)isStage;
return NULL;
}
void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
(void)s;
(void)isStage;
(void)portData;
}
// IIgs is chunky-native: the hal*Planes hooks are compiled out by // IIgs is chunky-native: the hal*Planes hooks are compiled out by
// no-op macros in core/hal.h (gated on JOEYLIB_PLATFORM_IIGS), so no // no-op macros in core/port.h (gated on JOEYLIB_PLATFORM_IIGS), so no
// stub functions are defined here. // stub functions are defined here.
/* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like /* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like
* the legacy paths did. Same logic as the DOS port. */ * the legacy paths did. Same logic as the DOS port. */
uint8_t halSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
if (x & 1) return (uint8_t)(byte & 0x0Fu);
return (uint8_t)((byte & 0xF0u) >> 4);
}
uint32_t halSurfaceHash(const jlSurfaceT *s) {
uint16_t lo = 0xACE1u, hi = 0x1357u, blocks, n, v;
const uint8_t *p;
const uint16_t *w;
uint8_t b;
p = s->pixels;
blocks = (uint16_t)(SURFACE_PIXELS_SIZE / 8);
do {
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
blocks--;
} while (blocks > 0u);
p = s->scb;
for (n = 0; n < (uint16_t)SURFACE_HEIGHT; n++) {
b = *p++; SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
w = &s->palette[0][0];
for (n = 0; n < (uint16_t)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;
}
void halSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
memcpy(dst->pixels, src->pixels, SURFACE_PIXELS_SIZE);
}
bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) {
return fread(dst->pixels, 1, SURFACE_PIXELS_SIZE, fp) == SURFACE_PIXELS_SIZE;
}
bool halSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) {
return fwrite(src->pixels, 1, SURFACE_PIXELS_SIZE, fp) == SURFACE_PIXELS_SIZE;
}
uint8_t *halSurfaceAllocPixels(void) {
return (uint8_t *)calloc(1, SURFACE_PIXELS_SIZE);
}
void halSurfaceFreePixels(uint8_t *pixels) {
free(pixels);
}
uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
(void)s; (void)planeIdx;
return NULL;
}
// $C019 RDVBLBAR: bit 7 = 0 during vertical blank, 1 during active // $C019 RDVBLBAR: bit 7 = 0 during vertical blank, 1 during active
// scan. To produce a rising-edge wait (one VBL per call), first spin // scan. To produce a rising-edge wait (one VBL per call), first spin
// while VBL is currently active (bit 7 = 0), then spin until VBL // while VBL is currently active (bit 7 = 0), then spin until VBL
// fires again (bit 7 returns to 0). The IIgs SHR refresh is 60 Hz. // fires again (bit 7 returns to 0). The IIgs SHR refresh is 60 Hz.
void halWaitVBL(void) { void jlpWaitVBL(void) {
while ((*IIGS_VBL_STATUS & VBL_BAR_BIT) == 0) { while ((*IIGS_VBL_STATUS & VBL_BAR_BIT) == 0) {
/* already in VBL: wait for active scan */; /* already in VBL: wait for active scan */;
} }
@ -460,17 +364,17 @@ void halWaitVBL(void) {
// than `gFrameCount++` because the post-increment can lower to // than `gFrameCount++` because the post-increment can lower to
// `inc |gFrameCount` (the only INC abs form on 65816 -- there is no // `inc |gFrameCount` (the only INC abs form on 65816 -- there is no
// INC long-abs). With this file in the DRAWPRIMS load segment but // INC long-abs). With this file in the DRAWPRIMS load segment but
// halFrameCount called from CORESYS via JSL, DBR isn't pointing at // jlpFrameCount called from CORESYS via JSL, DBR isn't pointing at
// DRAWPRIMS's data bank, so the abs INC silently mutates the wrong // DRAWPRIMS's data bank, so the abs INC silently mutates the wrong
// byte and the counter never advances. The explicit lda > / sta > // byte and the counter never advances. The explicit lda > / sta >
// pattern uses long-mode addressing throughout, which is // pattern uses long-mode addressing throughout, which is
// DBR-independent. // DBR-independent.
uint16_t halFrameCount(void) { uint16_t jlpFrameCount(void) {
return iigsGetTickWord(); return iigsGetTickWord();
} }
uint16_t halFrameHz(void) { uint16_t jlpFrameHz(void) {
return gFrameHz; return gFrameHz;
} }
@ -480,6 +384,3 @@ uint16_t halFrameHz(void) {
// gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so // gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so
// callers tracking deltas should use this for short intervals only // callers tracking deltas should use this for short intervals only
// (sound event scheduling is fine). // (sound event scheduling is fine).
uint32_t halMillisElapsed(void) {
return (uint32_t)((uint32_t)halFrameCount() * 1000u / (uint32_t)gFrameHz);
}

View file

@ -16,7 +16,7 @@
// //
// Release detection uses the IIe-inherited "any key currently down" // Release detection uses the IIe-inherited "any key currently down"
// live flag at $C010 bit 7 (set by the keyboard scanner independently // live flag at $C010 bit 7 (set by the keyboard scanner independently
// of the strobe). Each halInputPoll drains pending strobe events to // of the strobe). Each jlpInputPoll drains pending strobe events to
// pick up presses, then samples $C010: bit 7 == 0 means no // pick up presses, then samples $C010: bit 7 == 0 means no
// non-modifier key is physically held, and we wholesale-clear // non-modifier key is physically held, and we wholesale-clear
// gKeyState. readModifierKeys then re-asserts the modifiers from // gKeyState. readModifierKeys then re-asserts the modifiers from
@ -28,7 +28,7 @@
// returns one signed 7-bit delta; $C027 bit 1 indicates whether the // returns one signed 7-bit delta; $C027 bit 1 indicates whether the
// next read will return X (0) or Y (1). On the Y read, $C024 bit 7 // next read will return X (0) or Y (1). On the Y read, $C024 bit 7
// also encodes inverted button state (0 = pressed). We do exactly two // also encodes inverted button state (0 = pressed). We do exactly two
// $C024 reads per halInputPoll, accumulating the deltas onto an // $C024 reads per jlpInputPoll, accumulating the deltas onto an
// absolute position which is clamped to the surface rectangle. The // absolute position which is clamped to the surface rectangle. The
// IIgs ADB MCU autopolls the mouse and queues fifos behind these // IIgs ADB MCU autopolls the mouse and queues fifos behind these
// softswitches, so the per-frame two-read cadence keeps up with // softswitches, so the per-frame two-read cadence keeps up with
@ -36,7 +36,7 @@
#include <string.h> #include <string.h>
#include "hal.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -73,7 +73,7 @@
// Cap on the per-poll keyboard-FIFO drain. The IIgs ADB queue is // Cap on the per-poll keyboard-FIFO drain. The IIgs ADB queue is
// small in practice; this is purely a defensive bound so a stuck // small in practice; this is purely a defensive bound so a stuck
// strobe can't spin halInputPoll forever. // strobe can't spin jlpInputPoll forever.
#define KBD_DRAIN_GUARD 32u #define KBD_DRAIN_GUARD 32u
// $C025 layout (IIgs Hardware Reference): bit 0 = shift, bit 1 = ctrl, // $C025 layout (IIgs Hardware Reference): bit 0 = shift, bit 1 = ctrl,
@ -116,7 +116,7 @@ static int8_t thresholdPaddle(uint8_t v);
// ----- Module state ----- // ----- Module state -----
// ASCII -> jlKeyE, filled once at halInputInit. Runtime fill keeps // ASCII -> jlKeyE, filled once at jlpInputInit. Runtime fill keeps
// lookup O(1) instead of a 40-plus-case switch. // lookup O(1) instead of a 40-plus-case switch.
static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; static uint8_t gAsciiToKey[ASCII_TABLE_SIZE];
@ -244,7 +244,7 @@ static bool gJoyDisconnectLatched = false;
// captured the last time the user called jlJoystickReset. Until // captured the last time the user called jlJoystickReset. Until
// that's called, gJoyCenterValid is false and pollJoystick falls back // that's called, gJoyCenterValid is false and pollJoystick falls back
// to the digital threshold mapping. gJoyRecalibrate is set by // to the digital threshold mapping. gJoyRecalibrate is set by
// halJoystickReset and cleared on the next successful poll, which // jlpJoystickReset and cleared on the next successful poll, which
// captures the new center. // captures the new center.
// uint8_t (not bool) so the per-element stride is a known 1 byte. // uint8_t (not bool) so the per-element stride is a known 1 byte.
// Storage is still 0 or 1 either way. // Storage is still 0 or 1 either way.
@ -254,7 +254,7 @@ static uint8_t gJoyCenterValid [JOYSTICK_COUNT];
static uint8_t gJoyRecalibrate [JOYSTICK_COUNT]; static uint8_t gJoyRecalibrate [JOYSTICK_COUNT];
void halJoystickReset(jlJoystickE js) { void jlpJoystickReset(jlJoystickE js) {
if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) { if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) {
return; return;
} }
@ -410,7 +410,7 @@ static void pollMouse(void) {
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
void halInputInit(void) { void jlpInputInit(void) {
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
buildAsciiTable(); buildAsciiTable();
@ -425,7 +425,7 @@ void halInputInit(void) {
} }
void halInputPoll(void) { void jlpInputPoll(void) {
uint8_t kbd; uint8_t kbd;
uint8_t ascii; uint8_t ascii;
uint8_t key; uint8_t key;
@ -479,6 +479,6 @@ void halInputPoll(void) {
} }
void halInputShutdown(void) { void jlpInputShutdown(void) {
(void)*IIGS_KBDSTRB; (void)*IIGS_KBDSTRB;
} }

View file

@ -30,7 +30,7 @@
; void iigsInitRowLut(void) ; void iigsInitRowLut(void)
; ;
; One-shot init for the y*160 lookup table at gRowOffsetLut. C calls ; One-shot init for the y*160 lookup table at gRowOffsetLut. C calls
; this once from halInit; afterwards every primitive that needs y -> ; 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 gRowOffsetLut,x` (X = y*2) in a few cyc
; instead of a shift-add chain. 200 entries x 2 bytes = 400 bytes. ; instead of a shift-add chain. 200 entries x 2 bytes = 400 bytes.
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
@ -139,10 +139,10 @@ clrExit:
; long,X path (6 cyc / 2 bytes). SP is relocated to the top of the $01 ; long,X path (6 cyc / 2 bytes). SP is relocated to the top of the $01
; stage region ($2000..$9CFF); AUXWRITE ($C005) redirects the bank-$00 ; stage region ($2000..$9CFF); AUXWRITE ($C005) redirects the bank-$00
; stack writes to the $01 stage bank, and SHR shadow is already ; stack writes to the $01 stage bank, and SHR shadow is already
; inhibited by halInit. ~1.5x faster. ; inhibited by jlpInit. ~1.5x faster.
; ;
; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI. ; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI.
; SAFE ONLY POST-halInit. Soft switches use LONG addressing to bank $E0 ; SAFE ONLY POST-jlpInit. Soft switches use LONG addressing to bank $E0
; ($E0C005), NOT `sta $C005`: under the GS/OS Loader DBR=$08, a DBR-rel ; ($E0C005), NOT `sta $C005`: under the GS/OS Loader DBR=$08, a DBR-rel
; `sta $C0xx` hits bank-$08 RAM and the switch silently never toggles, so ; `sta $C0xx` hits bank-$08 RAM and the switch silently never toggles, so
; AUXWRITE stays off and the slam corrupts bank 0 instead of the $01 ; AUXWRITE stays off and the slam corrupts bank 0 instead of the $01

View file

@ -41,9 +41,9 @@ iigsGetTickWord:
; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz). ; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz).
; ;
; GetTick fires from the hardware VBL ISR, so its rate matches the ; GetTick fires from the hardware VBL ISR, so its rate matches the
; video field rate -- 60 Hz on NTSC, 50 Hz on PAL. halFrameHz must ; video field rate -- 60 Hz on NTSC, 50 Hz on PAL. jlpFrameHz must
; report whichever this machine actually runs so wall-clock math ; report whichever this machine actually runs so wall-clock math
; (frames * 1000 / halFrameHz) is correct on both. ; (frames * 1000 / jlpFrameHz) is correct on both.
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
.section .text.iigsReadHzParam,"ax" .section .text.iigsReadHzParam,"ax"
.globl iigsReadHzParam .globl iigsReadHzParam

View file

@ -0,0 +1,244 @@
// 68k sprite codegen for the ST word-interleaved planar layout.
//
// Emits a position-independent, cdecl-callable DRAW routine that blits
// 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 byte read-modify-writes with the plane bytes
// and masks baked in as immediates.
//
// Calling convention (m68k cdecl, the routine is invoked through a
// C function pointer by spriteCompiledDraw 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 RMWs plane bytes at d16(a0).
// Only d0/a0 (cdecl scratch) are touched, so no callee-save is needed.
//
// 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)
// matching stSpriteDrawByteAligned in src/atarist/hal.c bit-for-bit.
//
// Only shifts 0 (x mod 16 == 0) and 1 (x mod 16 == 8) are byte-aligned
// and compiled; other shifts return 0 so jlSpriteCompile records
// SPRITE_NOT_COMPILED and the dispatcher routes non-aligned x through
// the interpreted jlpSpriteDrawPlanes walker.
#include "joey/sprite.h"
#include "joey/surface.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_BYTES 32u // bytes per 8x8 4bpp tile
#define TILE_BYTES_PER_ROW 4u // 8 pixels * 4 bits / 8
#define TILE_PIXELS 8u
#define ST_PLANAR_STRIDE 160u // 320 px * 4 planes / 8 bits
#define ST_GROUP_BYTES 8u // 4 plane words per 16-px group
// 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
#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
// Emitted bytes for the trailing rts.
#define EPILOGUE_BYTES 2u
// ----- Prototypes -----
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut);
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w);
// ----- Emit helpers -----
// Convert one 8-pixel sprite tile column (4 chunky 4bpp bytes) into 4
// plane bytes plus an opaque mask, bit 7 = leftmost pixel. Identical
// pixel walk to stSpriteDrawByteAligned: 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;
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 != 0u) {
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;
}
// Append a big-endian 16-bit word at cursor; returns the new cursor.
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w) {
out[cursor] = (uint8_t)(w >> 8);
out[cursor + 1u] = (uint8_t)(w & 0xFFu);
return cursor + 2u;
}
// ----- Emit API -----
uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t wTiles;
uint16_t srcH;
uint16_t xMod16;
int16_t row;
int16_t tileCol;
if (shift > 1u) {
return 0u;
}
wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS);
xMod16 = (uint16_t)(shift * 8u); // shift 0 -> 0, shift 1 -> 8
cursor = 0u;
// Prologue: movea.l 4(sp),a0
if (cursor + 4u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_MOVEA_4SP_A0);
cursor = putWord(out, cursor, 0x0004u);
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 * TILE_BYTES
+ (uint32_t)inTileY * TILE_BYTES_PER_ROW;
if (row > 0) {
// adda.w #160,a0 -- advance to this row's planar base.
if (cursor + 4u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_ADDA_W_A0);
cursor = putWord(out, cursor, ST_PLANAR_STRIDE);
}
for (tileCol = 0; tileCol < (int16_t)wTiles; tileCol++) {
const uint8_t *trp = tileRowBase + (uint32_t)tileCol * TILE_BYTES;
uint8_t plane[4];
uint8_t opaque;
uint16_t pixOff;
uint16_t groupByteOff;
uint16_t halfByte;
uint8_t p;
c2pColumn(trp, plane, &opaque);
if (opaque == 0u) {
continue;
}
// Group + which byte half this 8-px column lands in, relative
// to groupBase. All compile-time constant given the shift.
pixOff = (uint16_t)(xMod16 + (uint16_t)tileCol * 8u);
groupByteOff = (uint16_t)((uint16_t)(pixOff >> 4) * ST_GROUP_BYTES);
halfByte = (uint16_t)((pixOff & 8u) ? 1u : 0u);
for (p = 0u; p < 4u; p++) {
uint16_t disp = (uint16_t)(groupByteOff + (uint16_t)p * 2u + halfByte);
// Exact reservation: a conservative (worst-case) reserve would
// make the emit pass -- which gets exactly the size the sizing
// pass reported -- falsely overflow on the trailing shorter
// writes. The final rts is checked separately below.
if (opaque == 0xFFu) {
// Fully opaque column: the whole byte becomes plane[p], so
// store it directly -- no read / andi / ori needed.
// move.b #plane[p],disp(a0) (6 bytes)
if (cursor + 6u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_MOVE_B_IMM_A0);
cursor = putWord(out, cursor, (uint16_t)plane[p]);
cursor = putWord(out, cursor, disp);
} else {
uint32_t planeBytes = (plane[p] != 0u) ? 16u : 12u; // +ori.b only when bits set
if (cursor + planeBytes > cap) {
return SPRITE_EMIT_OVERFLOW;
}
// move.b disp(a0),d0
cursor = putWord(out, cursor, OP_MOVE_B_A0_D0);
cursor = putWord(out, cursor, disp);
// andi.b #~opaque,d0 (clear the opaque pixel bits)
cursor = putWord(out, cursor, OP_ANDI_B_D0);
cursor = putWord(out, cursor, (uint16_t)(uint8_t)(~opaque));
// ori.b #plane[p],d0 (set this plane's bits; skip if none)
if (plane[p] != 0u) {
cursor = putWord(out, cursor, OP_ORI_B_D0);
cursor = putWord(out, cursor, (uint16_t)plane[p]);
}
// move.b d0,disp(a0)
cursor = putWord(out, cursor, OP_MOVE_B_D0_A0);
cursor = putWord(out, cursor, disp);
}
}
}
}
// rts
if (cursor + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_RTS);
return cursor;
}
// Save/restore are not compiled on the ST yet -- the interpreted
// jlpSpriteSavePlanes / jlpSpriteRestorePlanes path already meets the
// IIgs perf floor (plane byte copies), so returning 0 routes them
// there. The entry points exist only for the dispatcher's uniform API.
uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}

View file

@ -0,0 +1,235 @@
// Planar 68k sprite codegen for the Amiga (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.
//
// Calling convention (m68k cdecl, invoked through a C function pointer
// by spriteCompiledDraw 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.
//
// ONLY shift 0 (x mod 8 == 0, byte-aligned) is compiled. For shift 0 a
// sprite tile column maps to exactly one plane byte (no sub-byte spill),
// so the per-plane work mirrors the ST byte path:
// fully opaque (mask==0xFF): move.b #plane[p],off(a_i) (direct store)
// mixed: move.b off(a_i),d0; andi.b #~mask,d0; ori.b #plane[p],d0;
// move.b d0,off(a_i)
// matching jlpSpriteDrawPlanes in src/amiga/hal.c for the shift-0 case.
// Shifts 1..7 return 0 so jlSpriteCompile records SPRITE_NOT_COMPILED
// and the dispatcher routes non-aligned x through the interpreted
// jlpSpriteDrawPlanes walker (which handles the two-byte sub-byte spill).
#include "joey/sprite.h"
#include "joey/surface.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_BYTES 32u // bytes per 8x8 4bpp tile
#define TILE_BYTES_PER_ROW 4u // 8 pixels * 4 bits / 8
#define TILE_PIXELS 8u
#define AMIGA_PLANE_STRIDE (SURFACE_WIDTH / 8) // 40 bytes/plane-row
#define AMIGA_PLANES 4u
// 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
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 };
// 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)
// ----- Prototypes -----
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut);
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w);
// ----- Emit helpers -----
// Convert one 8-pixel sprite tile column (4 chunky 4bpp bytes) into 4
// plane bytes plus an opaque mask, bit 7 = leftmost pixel. Identical
// pixel walk to jlpSpriteDrawPlanes' c2p loop.
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut) {
uint8_t opaque;
uint8_t i;
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 != 0u) {
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;
}
// Append a big-endian 16-bit word at cursor; returns the new cursor.
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w) {
out[cursor] = (uint8_t)(w >> 8);
out[cursor + 1u] = (uint8_t)(w & 0xFFu);
return cursor + 2u;
}
// ----- Emit API -----
uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t wTiles;
uint16_t srcH;
uint8_t i;
int16_t row;
int16_t byteCol;
if (shift != 0u) {
return 0u;
}
wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS);
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 * TILE_BYTES
+ (uint32_t)inTileY * TILE_BYTES_PER_ROW;
uint16_t rowOff = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
const uint8_t *trp = tileRowBase + (uint32_t)byteCol * TILE_BYTES;
uint8_t plane[4];
uint8_t opaque;
uint16_t off;
c2pColumn(trp, plane, &opaque);
if (opaque == 0u) {
continue;
}
off = (uint16_t)(rowOff + (uint16_t)byteCol);
for (i = 0u; i < AMIGA_PLANES; i++) {
if (opaque == 0xFFu) {
// Fully opaque column: store the plane byte directly.
// move.b #plane[i],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)plane[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 = (plane[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 #~opaque,d0
cursor = putWord(out, cursor, OP_ANDI_B_D0);
cursor = putWord(out, cursor, (uint16_t)(uint8_t)(~opaque));
// ori.b #plane[i],d0 (skip if no bits)
if (plane[i] != 0u) {
cursor = putWord(out, cursor, OP_ORI_B_D0);
cursor = putWord(out, cursor, (uint16_t)plane[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;
}
// Save/restore are not compiled on the Amiga yet -- the interpreted
// jlpSpriteSavePlanes / jlpSpriteRestorePlanes path handles them. The
// entry points exist only for the dispatcher's uniform API.
uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}

View file

@ -26,15 +26,15 @@
| uint8_t fb2, uint8_t fb3); | uint8_t fb2, uint8_t fb3);
| |
| Fill ONE planar row across 4 planes -- the per-row body of | Fill ONE planar row across 4 planes -- the per-row body of
| halFillRectPlanes lifted into asm. Each pN points at the leading | jlpFillRect lifted into asm. Each pN points at the leading
| byte (already advanced by planeBase + y*40 + byteFirst on the C | byte (already advanced by planeBase + y*40 + byteFirst on the C
| side). leftMask and rightMask are the partial-byte masks for the | side). leftMask and rightMask are the partial-byte masks for the
| left/right edges; numMid is the count of full bytes between them. | left/right edges; numMid is the count of full bytes between them.
| fbN is 0x00 or 0xFF, the per-plane fill byte (caller pre-classifies | fbN is 0x00 or 0xFF, the per-plane fill byte (caller pre-classifies
| (colorIndex >> N) & 1 -> 0xFF or 0x00). | (colorIndex >> N) & 1 -> 0xFF or 0x00).
| |
| Used by Amiga halFastFillCircle (one call per scanline span) and | Used by Amiga jlpFillCircle (one call per scanline span) and
| Amiga halFillRectPlanes (one call per row of the rect). Replaces | Amiga jlpFillRect (one call per row of the rect). Replaces
| the C inner loop whose ~13 cyc/byte was the gating cost on | the C inner loop whose ~13 cyc/byte was the gating cost on
| jlFillCircle r=40 even after C-side inlining. | jlFillCircle r=40 even after C-side inlining.
| |