diff --git a/Makefile b/Makefile index 37fc655..53a0643 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ ifeq ($(HAVE_DOS),1) ALL_TARGETS += dos 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 ifeq ($(ALL_TARGETS),) @@ -52,6 +52,9 @@ iigs: iigs-disk: @$(MAKE) -f $(REPO_DIR)/make/iigs.mk iigs-disk +iigs-verify: + @$(MAKE) -f $(REPO_DIR)/make/iigs.mk iigs-verify + amiga: @$(MAKE) -f $(REPO_DIR)/make/amiga.mk @@ -61,6 +64,11 @@ atarist: dos: @$(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: rm -rf $(REPO_DIR)/build @@ -73,4 +81,6 @@ 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)." diff --git a/include/joey/core.h b/include/joey/core.h index caa89bb..4ecd2ed 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -49,7 +49,7 @@ void jlWaitVBL(void); // Monotonic 16-bit frame counter. Caller must poll faster than // 2 * jlFrameHz() so no edge is missed. Used by benchmarks and // 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); // 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 // render report the true elapsed time, not the number of VBLs that // 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); // Deterministic, seedable pseudo-random generator. Bit-identical diff --git a/include/joey/input.h b/include/joey/input.h index acf4cad..f58e8e0 100644 --- a/include/joey/input.h +++ b/include/joey/input.h @@ -90,7 +90,7 @@ typedef enum { void jlInputPoll(void); // 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 // wait loop runs. void jlWaitForAnyKey(void); diff --git a/include/joey/platform.h b/include/joey/platform.h index 6278dab..327c996 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -12,7 +12,8 @@ #if !defined(JOEYLIB_PLATFORM_IIGS) && \ !defined(JOEYLIB_PLATFORM_AMIGA) && \ !defined(JOEYLIB_PLATFORM_ATARIST) && \ - !defined(JOEYLIB_PLATFORM_DOS) + !defined(JOEYLIB_PLATFORM_DOS) && \ + !defined(JOEYLIB_PLATFORM_BLANK) #if defined(__DJGPP__) || defined(__MSDOS__) #define JOEYLIB_PLATFORM_DOS @@ -28,12 +29,17 @@ #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 ----- #if (defined(JOEYLIB_PLATFORM_IIGS) + \ defined(JOEYLIB_PLATFORM_AMIGA) + \ 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" #endif @@ -61,6 +67,227 @@ #define JOEYLIB_ENDIAN_LITTLE #define JOEYLIB_NATIVE_CHUNKY #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_) ----- +// +// A machine #define-s JL_HAS_ for each overridable platform op it replaces +// with its own (asm / native) implementation in src//. 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 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_ 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 // ----- Library version ----- diff --git a/make/amiga.mk b/make/amiga.mk index 4afa494..39fc66d 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -10,10 +10,10 @@ BINDIR := $(BUILD)/bin # PTPlayer is staged by toolchains/install.sh into PTPLAYER_DIR; we # reference its ptplayer.asm + header from there rather than copying # them into the source tree, so install.sh can refresh / version it -# independently. -I on $(SRC_PORT)/amiga lets ptplayer.h resolve -# from the port-local shim alongside our HAL code. +# independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve +# from the machine-local shim alongside our code. 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 # CIA-B + audio.device interrupts via the OS rather than taking over # 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). PTPLAYER_ASFLAGS := -Fhunk -m68000 -quiet -DOSCOMPAT=1 # --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 # LocaleBase; our no-op skips the deref. LDFLAGS := -noixemul -Wl,--allow-multiple-definition -PORT_C_SRCS := $(wildcard $(SRC_PORT)/amiga/*.c) -PORT_S_SRCS := $(wildcard $(SRC_PORT)/amiga/*.s) +PORT_C_SRCS := $(wildcard $(SRC_DIR)/amiga/*.c) +PORT_S_SRCS := $(wildcard $(SRC_DIR)/amiga/*.s) SHARED_S := $(wildcard $(SRC_68K)/*.s) # 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. 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 LIB_OBJS := \ $(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_PORT)/amiga/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ + $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_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)) \ $(BUILD)/obj/port/ptplayer.o \ - $(BUILD)/obj/codegen/spriteEmit68k.o \ - $(BUILD)/obj/codegen/spriteEmitPlanar68k.o \ + $(BUILD)/obj/68k/spriteEmit68k.o \ + $(BUILD)/obj/68k/spriteEmitPlanar68k.o \ $(BUILD)/obj/codegen/spriteCompile.o LIB := $(LIBDIR)/libjoey.a @@ -129,13 +132,17 @@ $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(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 $@) $(AMIGA_CC) $(CFLAGS) -c $< -o $@ # Hand-written 68k assembly: GAS syntax, fed through the gcc driver # so the bundled m68k-amigaos-as (binutils) does the work. -$(BUILD)/obj/port/%.o: $(SRC_PORT)/amiga/%.s +$(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.s @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) -c $< -o $@ @@ -153,6 +160,12 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s @mkdir -p $(dir $@) $(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) @mkdir -p $(dir $@) $(AMIGA_AR) rcs $@ $^ diff --git a/make/atarist.mk b/make/atarist.mk index ad60c26..6d55bac 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -24,20 +24,23 @@ LIBXMP_AR := $(LIBDIR)/libxmplite.a # 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 -PORT_C_SRCS := $(wildcard $(SRC_PORT)/atarist/*.c) -PORT_S_SRCS := $(wildcard $(SRC_PORT)/atarist/*.s) +PORT_C_SRCS := $(wildcard $(SRC_DIR)/atarist/*.c) +PORT_S_SRCS := $(wildcard $(SRC_DIR)/atarist/*.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 LIB_OBJS := \ $(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_PORT)/atarist/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ + $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_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)) \ - $(BUILD)/obj/codegen/spriteEmit68k.o \ - $(BUILD)/obj/codegen/spriteEmitInterleaved68k.o \ + $(BUILD)/obj/68k/spriteEmit68k.o \ + $(BUILD)/obj/68k/spriteEmitInterleaved68k.o \ $(BUILD)/obj/codegen/spriteCompile.o LIB := $(LIBDIR)/libjoey.a @@ -105,14 +108,18 @@ $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(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 $@) $(ST_CC) $(CFLAGS) -c $< -o $@ # Hand-written 68k assembly: GAS syntax, fed through the gcc driver # so the bundled m68k-atari-mint-as (binutils) does the work. No vasm # dependency on the ST path. -$(BUILD)/obj/port/%.o: $(SRC_PORT)/atarist/%.s +$(BUILD)/obj/port/%.o: $(SRC_DIR)/atarist/%.s @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) -c $< -o $@ @@ -120,6 +127,12 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s @mkdir -p $(dir $@) $(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 @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ diff --git a/make/common.mk b/make/common.mk index 30cabff..ecbe9ae 100644 --- a/make/common.mk +++ b/make/common.mk @@ -4,15 +4,23 @@ REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) INCLUDE_DIR := $(REPO_DIR)/include +# Machine-specific code lives in src// (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_PORT := $(REPO_DIR)/src/port SRC_CG := $(REPO_DIR)/src/codegen -SRC_68K := $(REPO_DIR)/src/shared68k +SRC_68K := $(REPO_DIR)/src/m68k EXAMPLES := $(REPO_DIR)/examples # Portable C sources for libjoey -- present on every target. 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. COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 diff --git a/make/dos.mk b/make/dos.mk index 113949d..51ead0a 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -22,15 +22,17 @@ LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP 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 -PORT_C_SRCS := $(wildcard $(SRC_PORT)/dos/*.c) -PORT_S_SRCS := $(wildcard $(SRC_PORT)/dos/*.asm) +# The x86 sprite emitter (spriteEmitX86.c) now lives in src/dos/ and is built +# 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 LIB_OBJS := \ $(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_PORT)/dos/%.asm,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ - $(BUILD)/obj/codegen/spriteEmitX86.o \ + $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ + $(patsubst $(SRC_DIR)/dos/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ + $(patsubst $(SRC_DIR)/dos/%.asm,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(BUILD)/obj/codegen/spriteCompile.o LIB := $(LIBDIR)/libjoey.a @@ -119,11 +121,15 @@ $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -c $< -o $@ -$(BUILD)/obj/port/%.o: $(SRC_PORT)/dos/%.c +$(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c @mkdir -p $(dir $@) $(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 $@) $(DOS_AS) $(ASFLAGS) $< -o $@ diff --git a/make/iigs.mk b/make/iigs.mk index 5461f1a..441b8b7 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -2,7 +2,7 @@ # # toolchains/iigs/clang-build.sh drives compile (clang) / assemble # (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 # dead-stripping. # @@ -26,19 +26,20 @@ RT_INC := $(LLVM816_ROOT)/runtime/include 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 -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. -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 # DOS and ST share, so src/core/audioSfxMix.c is unused here. CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS)) -# Sprite codegen: 65816 emitter + cross-platform compile dispatch. -CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \ - $(REPO_DIR)/src/codegen/spriteCompile.c +# Sprite codegen: the 65816 emitter (spriteEmitIigs.c) now lives in src/iigs/ +# and is picked up by PORT_C_SRCS; only the CPU-independent compile dispatch +# 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) # Example sources (single-TU unless noted). @@ -64,7 +65,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_ASM := $(BUILD)/audio/ntpdata.s 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. 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)/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) @mkdir -p $(BUILD)/audio @cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s diff --git a/scripts/bench-amiga.sh b/scripts/bench-amiga.sh new file mode 100755 index 0000000..3c89e02 --- /dev/null +++ b/scripts/bench-amiga.sh @@ -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 diff --git a/scripts/bench-atarist.sh b/scripts/bench-atarist.sh new file mode 100755 index 0000000..98969dc --- /dev/null +++ b/scripts/bench-atarist.sh @@ -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: : 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 diff --git a/scripts/bench-dos.sh b/scripts/bench-dos.sh new file mode 100755 index 0000000..779602b --- /dev/null +++ b/scripts/bench-dos.sh @@ -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 diff --git a/scripts/check-blank.sh b/scripts/check-blank.sh new file mode 100755 index 0000000..77c6a5c --- /dev/null +++ b/scripts/check-blank.sh @@ -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)" diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh new file mode 100755 index 0000000..03bb5af --- /dev/null +++ b/scripts/make-iigs-disk.sh @@ -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 diff --git a/scripts/run-iigs-mame.sh b/scripts/run-iigs-mame.sh index 956160d..78e414b 100755 --- a/scripts/run-iigs-mame.sh +++ b/scripts/run-iigs-mame.sh @@ -251,17 +251,32 @@ cleanup() { trap cleanup EXIT # 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" 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 +# 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. rm -f "$out/.done" cd "$work" mame apple2gs \ + -rompath "$rompath" \ -flop3 "$work/boot.po" \ -flop4 "$work/joey.2mg" \ $video_arg -sound none \ diff --git a/scripts/verify-iigs.sh b/scripts/verify-iigs.sh new file mode 100755 index 0000000..7a7efb4 --- /dev/null +++ b/scripts/verify-iigs.sh @@ -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" <> 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" &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 diff --git a/src/port/amiga/SDI_compiler.h b/src/amiga/SDI_compiler.h similarity index 100% rename from src/port/amiga/SDI_compiler.h rename to src/amiga/SDI_compiler.h diff --git a/src/port/amiga/audio.c b/src/amiga/audio.c similarity index 91% rename from src/port/amiga/audio.c rename to src/amiga/audio.c index 37877a7..65f07bd 100644 --- a/src/port/amiga/audio.c +++ b/src/amiga/audio.c @@ -22,7 +22,7 @@ #include #include "ptplayer.h" -#include "hal.h" +#include "port.h" #include "joey/audio.h" extern struct Custom custom; @@ -48,7 +48,7 @@ extern UBYTE 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. // 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 // ----- Module state ----- @@ -86,7 +86,7 @@ static UWORD periodForRate(uint16_t rateHz) { // ----- HAL API (alphabetical) ----- -bool halAudioInit(void) { +bool jlpAudioInit(void) { int i; if (gInstalled) { @@ -109,7 +109,7 @@ bool halAudioInit(void) { } -void halAudioShutdown(void) { +void jlpAudioShutdown(void) { int i; if (!gInstalled) { @@ -136,12 +136,12 @@ void halAudioShutdown(void) { } -bool halAudioIsPlayingMod(void) { +bool jlpAudioIsPlayingMod(void) { 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; 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 // 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. mt_init((void *)&custom, chip, NULL, 0); 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; SfxSlotT *s; 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 // audio IRQ. No-op until then; callers that depend on streaming // 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)fill; (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 // Paula's period register per note. Not wired yet -- AGI-on-Amiga // will be silent until this is filled in. -void halAudioTone(uint16_t freqHz) { +void jlpAudioTone(uint16_t freqHz) { (void)freqHz; } @@ -246,14 +246,14 @@ void halAudioTone(uint16_t freqHz) { // AGI 0..15 attenuation. Stubbed for now -- AGI-on-Amiga is silent // until the chip buffer + AllocAudio + direct Paula programming // 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)freqHz; (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 // Paula voice path lands. (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) { return; } @@ -281,7 +281,7 @@ void halAudioStopMod(void) { } -void halAudioStopSfx(uint8_t slot) { +void jlpAudioStopSfx(uint8_t slot) { UBYTE i; 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 // play-once watchdog. When the song author placed an `E8FF` at // song end, mt_E8Trigger latches to 0xFF -- if the caller passed diff --git a/src/port/amiga/circle.s b/src/amiga/circle.s similarity index 100% rename from src/port/amiga/circle.s rename to src/amiga/circle.s diff --git a/src/port/amiga/hal.c b/src/amiga/hal.c similarity index 90% rename from src/port/amiga/hal.c rename to src/amiga/hal.c index c0aaa38..da68d60 100644 --- a/src/port/amiga/hal.c +++ b/src/amiga/hal.c @@ -46,7 +46,7 @@ #include #include -#include "hal.h" +#include "port.h" #include "spriteInternal.h" #include "surfaceInternal.h" @@ -54,7 +54,7 @@ extern struct Custom custom; // 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. static void installVblServer(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 // under the original name to keep diffs minimal); gPlanesB is set B. // 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. static UBYTE *gPlanes [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 // after every installCopperList; patchBplCopperPokes writes new plane // 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] // 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]; // "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 // 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 @@ -136,7 +136,7 @@ static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT]; // presents still run. 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 // RGB values differ from what is already on screen; pure pixel updates // (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 // 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. scanBplCopperPokes(); } @@ -402,7 +402,7 @@ static void scanBplCopperPokes(void) { // of CHIP plane buffers. Takes effect at the next vblank when the // copper next runs through these pokes. Disable() guards against an // 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 // scan region by here), so we can't actually tear a poke in flight. static void patchBplCopperPokes(UBYTE *planes[AMIGA_BITPLANES]) { @@ -518,7 +518,7 @@ static void uploadFirstBandPalette(const jlSurfaceT *src) { // ----- HAL API (alphabetical) ----- -bool halInit(const jlConfigT *config) { +bool jlpInit(const jlConfigT *config) { uint16_t i; uint16_t j; @@ -545,7 +545,7 @@ bool halInit(const jlConfigT *config) { return false; } 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 // swaps which one BPL DMA fetches from. Allocate both up front; // bail with full rollback if either runs out of chip RAM. @@ -574,7 +574,7 @@ bool halInit(const jlConfigT *config) { return false; } // 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]; } gFrontIdx = 0; @@ -590,7 +590,7 @@ bool halInit(const jlConfigT *config) { // SA_DisplayID pins us to OCS PAL low-res so Intuition opens a // real planar screen rather than an RTG substitute. SA_BitMap // makes Intuition use OUR pre-allocated planes; CloseScreen will - // not free them -- our halShutdown does. + // not free them -- our jlpShutdown does. gScreen = OpenScreenTags(NULL, (ULONG)SA_Width, (ULONG)SURFACE_WIDTH, (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 // from the off-screen shadow planes (where every drawing primitive // dual-writes today) into gPlanes[] (the displayed BitMap). c2p is // gone; the chunky `s->pixels` shadow is still maintained by the -// halFast* primitives but no longer drives display. Phase 10 will +// jlp primitives but no longer drives display. Phase 10 will // either (a) BPLPTR-swap shadow <-> display planes (zero-copy) or // (b) stop writing chunky in the fast paths to recover the dual- // 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; int16_t y; int16_t firstRow; @@ -836,7 +831,7 @@ void halPresent(const jlSurfaceT *src) { } // 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 // 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 @@ -859,7 +854,7 @@ void halPresent(const jlSurfaceT *src) { // WaitTOF() blocks the calling task until the next "top of frame" // VBlank interrupt -- 50 Hz on PAL, 60 Hz on NTSC. graphics.library // is auto-opened by libnix so no extra plumbing is needed. -void halWaitVBL(void) { +void jlpWaitVBL(void) { WaitTOF(); } @@ -871,7 +866,7 @@ void halWaitVBL(void) { // when the caller's loop body is long. The interrupt server fires // 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. static volatile uint16_t gFrameCount = 0; @@ -915,12 +910,12 @@ static void removeVblServer(void) { } -uint16_t halFrameCount(void) { +uint16_t jlpFrameCount(void) { return gFrameCount; } -uint16_t halFrameHz(void) { +uint16_t jlpFrameHz(void) { /* PAL by default. The toolchain doesn't currently switch modes * at runtime; if we ever expose NTSC this returns 60. */ return 50u; @@ -931,17 +926,14 @@ uint16_t halFrameHz(void) { // IIgs. A timer.device-based ReadEClock impl would give microsecond // resolution and decouple from VBL load, but adds a 7-line library // 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. -uint32_t halMillisElapsed(void) { - return (uint32_t)((uint32_t)halFrameCount() * 1000u / 50u); -} -void halShutdown(void) { +void jlpShutdown(void) { uint16_t i; - amigaDumpPresentCounters("halShutdown"); + amigaDumpPresentCounters("jlpShutdown"); // Tear down the VBL server before closing the screen so the // interrupt chain is clean if anything else is watching. removeVblServer(); @@ -956,7 +948,7 @@ void halShutdown(void) { CloseScreen(gScreen); 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. // CloseScreen with an SA_BitMap'd screen does NOT free our // 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); -// Phase 3 planar dual-write: write the bitplanes alongside the -// Phase 9: Amiga is pure planar. Every halFast* below returns true -// for any surface that has planar storage (s->portData != NULL) to -// suppress the cross-platform chunky fallback path -- there is no -// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The gate -// is portData presence, NOT stage identity, so non-stage surfaces -// 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) { +// Amiga is pure planar (s->pixels is NULL). Each jlp below does the planar +// work directly on the bitplanes in s->portData (AmigaPlanarT); the degenerate +// portData == NULL case defers to the chunky-C generic (a no-op there since +// s->pixels is NULL). The gate is portData presence, not stage identity, so +// non-stage surfaces created by jlSurfaceCreate use their own planes. +void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) { AmigaPlanarT *pd; uint8_t colorIndex; uint16_t i; bool setBits; if (s->portData == NULL) { - return false; + jlpGenericSurfaceClear(s, doubled); + return; } colorIndex = (uint8_t)(doubled & 0x0Fu); 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) // or $FF (set). Each plane is 200 rows x 20 words, contiguous, so // 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 // WaitBlit before DisownBlitter guarantees the planes are fully // written before any CPU read (a subsequent draw or the present @@ -1034,27 +1019,13 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) { } WaitBlit(); 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 // shadow plane buffers alongside the chunky shadow. Caller (cross- // 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. // // 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 // constant: (colorIndex >> plane) & 1. Set bit -> OR 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; uint16_t byteFirst; 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; if (pd == NULL) { + jlpGenericFillRect(s, x, y, w, h, colorIndex); return; } /* 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 - * to a single byte load. jlFillCircle r=40 calls halFillRectPlanes - * 160 times, so this saves ~160*60=9600 cyc per jlFillCircle. */ + * to a single byte load. jlFillCircle r=40 fills 160 spans, so + * this saves ~160*60=9600 cyc per jlFillCircle. */ static const uint8_t kLeftMaskLut[8] = { 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 // 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; uint16_t plane; uint8_t fillByte; @@ -1348,6 +1320,7 @@ void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex pd = (AmigaPlanarT *)s->portData; if (pd == NULL) { + jlpGenericTileFill(s, bx, by, colorIndex); return; } 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 *srcPd; uint16_t plane; @@ -1372,6 +1345,7 @@ void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu dstPd = (AmigaPlanarT *)dst->portData; srcPd = (AmigaPlanarT *)src->portData; if (dstPd == NULL || srcPd == NULL) { + jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); return; } 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, // dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set // 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 *srcPd; - uint8_t transparent; uint8_t transBitByte[AMIGA_BITPLANES]; uint16_t i; uint8_t row; @@ -1408,9 +1381,10 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons dstPd = (AmigaPlanarT *)dst->portData; srcPd = (AmigaPlanarT *)src->portData; if (dstPd == NULL || srcPd == NULL) { + jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); 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 * else all 0" -- so XOR gives bit set where pixel differs from * 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 // 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; uint16_t pairs; 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) { 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); for (row = 0; row < (uint16_t)h; 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; uint16_t pairs; 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) { 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); for (row = 0; row < (uint16_t)h; 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 * codegen arena no longer needs the ~17-23 KB of unrolled per-shift * 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; int16_t dy; 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 // 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper. #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; uint16_t row; 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; if (pd == NULL) { + jlpGenericTileSnap(src, bx, by, chunkyOut); return; } rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; for (plane = 0; plane < AMIGA_BITPLANES; plane++) { 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++) { 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; uint8_t row; uint8_t plane; @@ -1732,14 +1788,15 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t * pd = (AmigaPlanarT *)dst->portData; if (pd == NULL) { + jlpGenericTilePaste(dst, bx, by, chunkyTile); return; } /* 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. */ rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; 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; for (row = 0; row < 8u; 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 - * 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. * Each pixel's output color: fgColor where shape bit is 1, bgColor * where 0. Output is written directly to the dst planar buffers via * the per-plane formula * out_plane_k = (shape & mask_fg_k) | (~shape & mask_bg_k) * where mask_X_k = $FF if (X & (1<portData; if (pd == NULL) { + jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor); return; } 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 // are off-screen shadow buffers; the displayed gPlanes[] is updated // only at jlStagePresent. -void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { +void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { AmigaPlanarT *dstPd; 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 // 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 // before DisownBlitter so the dest planes are complete before any // 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 // the four shadow planes. Caller (the per-primitive walker below) has // 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; uint8_t nibLo; if (s->portData == NULL) { - return false; + jlpGenericDrawPixel(s, x, y, colorIndex); + return; } nibLo = (uint8_t)(colorIndex & 0x0Fu); pd = (AmigaPlanarT *)s->portData; 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; if (s->portData == NULL) { - return false; + return; } pd = (AmigaPlanarT *)s->portData; 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; if (s->portData == NULL) { - return false; + return; } pd = (AmigaPlanarT *)s->portData; surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1], pd->planes[2], pd->planes[3], (uint16_t)cx, (uint16_t)cy, r, (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; int16_t x; int16_t y; @@ -2050,7 +2069,7 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_ return false; } 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). */ x = (int16_t)r; 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 * byte per plane and assembles the 4-bit color from a single bit * 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; uint8_t *rowPlanes[AMIGA_BITPLANES]; 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; uint8_t *rowPlanes[AMIGA_BITPLANES]; 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 ===== * * 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 * 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; uint16_t byteOff; 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 - * (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- * port hash comparisons stay valid. * 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; uint16_t lo = 0xACE1u, hi = 0x1357u; 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 // 0..3 written sequentially, AMIGA_PLANE_SIZE bytes each. -bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { +bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { AmigaPlanarT *pd; 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; 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 - * stays NULL; cross-platform code reads pixels via halSamplePixel - * (or other halXxxChunky hooks) which read from planes. NULL is + * stays NULL; cross-platform code reads pixels via jlpSamplePixel + * (or the other chunky reader ops) which read from planes. NULL is * a valid return -- cross-platform stageAlloc treats NULL as * "port has no chunky storage" and skips the chunky memset. */ return NULL; } -void halStageFreePixels(uint8_t *pixels) { - /* halStageAllocPixels returned NULL on Amiga, so this is always - * 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. */ +uint8_t *jlpSurfaceAllocPixels(void) { + /* Same rationale as jlpStageAllocPixels: no chunky on Amiga. */ return NULL; } -void halSurfaceFreePixels(uint8_t *pixels) { - free(pixels); -} - - -uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) { +uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) { AmigaPlanarT *pd; if (planeIdx >= AMIGA_BITPLANES) { 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 // result in s->portData; primitives access via (AmigaPlanarT *) // s->portData. -void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { +void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { AmigaPlanarT *pd; uint16_t i; @@ -2512,7 +2440,7 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { /* Both stage and non-stage: AllocMem fresh planes, MEMF_CLEAR * for the JoeyLib contract that color 0 = black at surface * 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. * * 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; uint16_t i; diff --git a/src/port/amiga/input.c b/src/amiga/input.c similarity index 98% rename from src/port/amiga/input.c rename to src/amiga/input.c index eff7f32..1a671d2 100644 --- a/src/port/amiga/input.c +++ b/src/amiga/input.c @@ -30,7 +30,7 @@ #include #include -#include "hal.h" +#include "port.h" #include "inputInternal.h" // ----- Constants ----- @@ -242,13 +242,13 @@ static void pollJoysticks(void) { // ----- HAL API (alphabetical) ----- -void halJoystickReset(jlJoystickE js) { +void jlpJoystickReset(jlJoystickE js) { // Amiga sticks are digital -- no calibration to do. (void)js; } -void halInputInit(void) { +void jlpInputInit(void) { memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); @@ -291,13 +291,13 @@ void halInputInit(void) { } -void halInputPoll(void) { +void jlpInputPoll(void) { drainMessages(); pollJoysticks(); } -void halInputShutdown(void) { +void jlpInputShutdown(void) { if (gLowLevelBase != NULL) { CloseLibrary(gLowLevelBase); gLowLevelBase = NULL; diff --git a/src/port/amiga/libinit.c b/src/amiga/libinit.c similarity index 100% rename from src/port/amiga/libinit.c rename to src/amiga/libinit.c diff --git a/src/port/atarist/audio.c b/src/atarist/audio.c similarity index 93% rename from src/port/atarist/audio.c rename to src/atarist/audio.c index 64d6c35..ea5a8b5 100644 --- a/src/port/atarist/audio.c +++ b/src/atarist/audio.c @@ -2,16 +2,16 @@ // running PWM through YM2149 channel-A volume register. // // 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), // 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 // output that the ISR consumes one byte at a time. // * 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 // 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. // // Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by @@ -25,7 +25,7 @@ #include -#include "hal.h" +#include "port.h" #include "audioSfxMixInternal.h" #include "joey/audio.h" @@ -84,7 +84,7 @@ static int gXmpLoopCount = 0; static void (*gOldTimerAVec)(void) = NULL; // 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]; // 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) ----- -bool halAudioInit(void) { +bool jlpAudioInit(void) { if (gReady) { return true; } @@ -194,7 +194,7 @@ bool halAudioInit(void) { } -void halAudioShutdown(void) { +void jlpAudioShutdown(void) { if (!gReady) { return; } @@ -217,12 +217,12 @@ void halAudioShutdown(void) { } -bool halAudioIsPlayingMod(void) { +bool jlpAudioIsPlayingMod(void) { 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) { 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) { 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) { 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 // program voice A here. Not wired yet -- AGI-on-ST will be silent // until this is filled in. -void halAudioTone(uint16_t freqHz) { +void jlpAudioTone(uint16_t freqHz) { (void)freqHz; } @@ -342,30 +342,30 @@ static long applyYmVoice(void) { // driving 12 kHz PWM for MOD output (see installTimerA). A future // AGI-on-ST sound path could lift the YM2149 tone driver onto // 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)hz; return false; } -void halAudioCriticalEnter(void) { - // No tick-callback ISR is registered (halAudioTickRegister returns +void jlpAudioCriticalEnter(void) { + // No tick-callback ISR is registered (jlpAudioTickRegister returns // false). The Timer A MOD ISR synchronizes with the producer via // the gNeedRefill flags, not this critical section, so masking // 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) { 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 // running, it will fight the per-voice writes here. AGI does not // 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) { return; } @@ -394,7 +394,7 @@ void halAudioStopMod(void) { } -void halAudioStopSfx(uint8_t slot) { +void jlpAudioStopSfx(uint8_t slot) { if (slot >= JOEY_AUDIO_SFX_SLOTS) { return; } @@ -412,7 +412,7 @@ void halAudioStopSfx(uint8_t slot) { // being rewritten (audible tearing). A full MOD-frame mix plus // audioSfxOverlayMix can approach that budget under load, so callers // should run the game loop comfortably faster than ~12 Hz. -void halAudioFrameTick(void) { +void jlpAudioFrameTick(void) { if (!gReady) { return; } diff --git a/src/port/atarist/audio_isr.s b/src/atarist/audio_isr.s similarity index 100% rename from src/port/atarist/audio_isr.s rename to src/atarist/audio_isr.s diff --git a/src/port/atarist/circle.s b/src/atarist/circle.s similarity index 99% rename from src/port/atarist/circle.s rename to src/atarist/circle.s index 8c219cc..bb3161d 100644 --- a/src/port/atarist/circle.s +++ b/src/atarist/circle.s @@ -1,6 +1,6 @@ | 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: | * Per scanline: 20 groups of 8 bytes; each group is 4 plane | words back-to-back (p0_word, p1_word, p2_word, p3_word). diff --git a/src/port/atarist/fillCircle.s b/src/atarist/fillCircle.s similarity index 100% rename from src/port/atarist/fillCircle.s rename to src/atarist/fillCircle.s diff --git a/src/port/atarist/hal.c b/src/atarist/hal.c similarity index 90% rename from src/port/atarist/hal.c rename to src/atarist/hal.c index d3e3bd2..51a9942 100644 --- a/src/port/atarist/hal.c +++ b/src/atarist/hal.c @@ -5,7 +5,7 @@ // * Word-interleaved ST planar buffer copied to the screen at present. // // 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) // 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). @@ -20,7 +20,7 @@ // ~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. // * 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. // // Deferred: @@ -34,7 +34,7 @@ #include -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" #include "spriteInternal.h" #include "joey/tile.h" @@ -105,7 +105,7 @@ typedef struct { // Phase 10: planar primitive helpers must be visible everywhere they // 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 / fillSpan / circle walker // can fold the 4-plane RMW directly into its body. always_inline // 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 // padding for runtime 256-byte alignment. TOS .PRG format only supports // 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 // the first, so it stays 256-byte aligned as the shifter requires. 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 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 // 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 @@ -196,7 +205,7 @@ static uint8_t gPrevStageMinWord[SURFACE_HEIGHT]; static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT]; // "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 // the current frame touched any row, union(prev,current) is empty, the // 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 // 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 static uint16_t gBandStart [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 // 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) { uint16_t line; 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 // two byte writes fully specify the address. The shifter latches the // 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 // involvement, which matters because our VBL ISR replaced TOS's and // does not run TOS's Setscreen-latch path. @@ -532,12 +541,26 @@ static void writeDiagnostics(void) { // ----- HAL API (alphabetical) ----- -bool halInit(const jlConfigT *config) { +bool jlpInit(const jlConfigT *config) { uintptr_t addr; uint16_t i; (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 // follows exactly one 32000-byte (256-aligned) page later, so it // 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; - // 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. Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0); gModeSet = true; @@ -596,12 +619,7 @@ bool halInit(const jlConfigT *config) { } -const char *halLastError(void) { - return NULL; -} - - -void halPresent(const jlSurfaceT *src) { +void jlpPresent(const jlSurfaceT *src) { StPlanarT *pd; uint8_t *backPage; uint8_t backIdx; @@ -778,7 +796,7 @@ void halPresent(const jlSurfaceT *src) { // Snapshot this frame's dirty bands as "previous" for the next // 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 // page is stale on. Record whether any row was dirty so the next // 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 // until the next 50 Hz (PAL) or 60 Hz (NTSC) vertical blank. -void halWaitVBL(void) { +void jlpWaitVBL(void) { int16_t before; // 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 // uint16_t for the cross-port HAL contract. -uint16_t halFrameCount(void) { +uint16_t jlpFrameCount(void) { 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 * report 50 as the baseline -- close enough for ops/sec scaling, * 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); } -uint32_t halMillisElapsed(void) { +uint32_t jlpMillisElapsed(void) { uint32_t ticks; ticks = (uint32_t)Supexec(readHz200); // 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) { return; } // 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 // forgets jlAudioShutdown(), Timer A keeps firing after our // 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. - halAudioShutdown(); + jlpAudioShutdown(); // Disable MFP Timer B and restore the exception vectors before // changing the screen -- a late ISR firing mid-Setscreen would @@ -884,6 +902,13 @@ void halShutdown(void) { Supexec(writePrevPaletteRegs); writeDiagnostics(); 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 // 8-byte group template (4 plane words: 0xFFFF or 0x0000 each by // 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; uint8_t color; uint32_t loLong; uint32_t hiLong; if (s->portData == NULL) { - return false; + jlpGenericSurfaceClear(s, doubled); + return; } pd = (StPlanarT *)s->portData; color = (uint8_t)(doubled & 0x0Fu); @@ -927,62 +953,20 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) { surface68kStLongFill(pd->base, (uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP), 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 // 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; if (s->portData == NULL) { - return false; + jlpGenericDrawPixel(s, x, y, colorIndex); + return; } pd = (StPlanarT *)s->portData; 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 / -// 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 // r=40 ~1 ops/sec. 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; if (s->portData == NULL) { - return false; + return; } pd = (StPlanarT *)s->portData; // 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 { 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; if (s->portData == NULL) { - return false; + return; } pd = (StPlanarT *)s->portData; /* 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, (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; if (s->portData == NULL) { 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 // surface. Both stage and non-stage get their own buffer; the two // 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 // after the red startup paint as the first long write hits an // odd-by-2 base. -void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { +void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { StPlanarT *pd; uint8_t *raw; 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; (void)s; @@ -1312,12 +1209,16 @@ void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) { // (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; uint16_t groupFirst; uint16_t groupLast; uint8_t *rowBase; + if (s->portData == NULL) { + jlpGenericFillRect(s, x, y, w, h, colorIndex); + return; + } if (s == NULL || w == 0u || h == 0u) { 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 *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 // 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. -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; uint16_t group; uint16_t halfMask; uint8_t *gp; + if (s->portData == NULL) { + jlpGenericTileFill(s, bx, by, colorIndex); + return; + } if (s == NULL) { 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 // write per plane. The asm walker handles 8 rows just as well // 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; uint16_t group; uint8_t *dstAddr; int16_t row; - if (dst == NULL || tileBytes == NULL) { + if (dst->portData == NULL) { + jlpGenericTilePaste(dst, bx, by, chunkyTile); + return; + } + if (dst == NULL || chunkyTile == NULL) { return; } 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 * per row * 8 rows = 32 bytes). Direct byte copy to the planar * 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. */ group = (uint16_t)((uint16_t)bx >> 1); dstAddr = pd->base @@ -1452,12 +1361,12 @@ void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t * (void)row; #define ST_TILE_PASTE_ROW \ do { \ - dstAddr[0] = tileBytes[0]; \ - dstAddr[2] = tileBytes[1]; \ - dstAddr[4] = tileBytes[2]; \ - dstAddr[6] = tileBytes[3]; \ - dstAddr += ST_BYTES_PER_ROW; \ - tileBytes += TILE_BYTES_PER_ROW; \ + dstAddr[0] = chunkyTile[0]; \ + dstAddr[2] = chunkyTile[1]; \ + dstAddr[4] = chunkyTile[2]; \ + dstAddr[6] = chunkyTile[3]; \ + dstAddr += ST_BYTES_PER_ROW; \ + chunkyTile += TILE_BYTES_PER_ROW; \ } while (0) 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 - * 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 1 at byte 1 * 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 * row's base. bx odd selects the second byte of the plane word * (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; uint16_t group; 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_bg[4]; + if (dst->portData == NULL) { + jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor); + return; + } if (dst == NULL || monoTile == NULL) { 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; uint16_t group; const uint8_t *srcAddr; int16_t row; - if (src == NULL || tileOut == NULL) { + if (src->portData == NULL) { + jlpGenericTileSnap(src, bx, by, chunkyOut); + return; + } + if (src == NULL || chunkyOut == NULL) { return; } 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; #define ST_TILE_SNAP_ROW \ do { \ - tileOut[0] = srcAddr[0]; \ - tileOut[1] = srcAddr[2]; \ - tileOut[2] = srcAddr[4]; \ - tileOut[3] = srcAddr[6]; \ + chunkyOut[0] = srcAddr[0]; \ + chunkyOut[1] = srcAddr[2]; \ + chunkyOut[2] = srcAddr[4]; \ + chunkyOut[3] = srcAddr[6]; \ srcAddr += ST_BYTES_PER_ROW; \ - tileOut += TILE_BYTES_PER_ROW; \ + chunkyOut += TILE_BYTES_PER_ROW; \ } while (0) 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 // bit transpose, no LUT. ~640 cyc per tile vs ~5000 cyc for the // 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; const StPlanarT *srcPd; uint8_t *dstAddr; @@ -1587,6 +1504,10 @@ void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu uint16_t dstGroup; int16_t row; + if (dst->portData == NULL || src->portData == NULL) { + jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); + return; + } if (dst == NULL || src == NULL) { 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; uint8_t scratch[TILE_BYTES]; int16_t row; @@ -1650,6 +1571,10 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons uint8_t mask; uint8_t notMask; + if (dst->portData == NULL) { + jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); + return; + } if (dst == NULL || src == NULL) { 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. * 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); dstY0 = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE); 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 + group * ST_BYTES_PER_GROUP + halfOff; - xK0 = (transparentIndex & 1u) ? 0xFFu : 0u; - xK1 = (transparentIndex & 2u) ? 0xFFu : 0u; - xK2 = (transparentIndex & 4u) ? 0xFFu : 0u; - xK3 = (transparentIndex & 8u) ? 0xFFu : 0u; + xK0 = (transparent & 1u) ? 0xFFu : 0u; + xK1 = (transparent & 2u) ? 0xFFu : 0u; + xK2 = (transparent & 4u) ? 0xFFu : 0u; + xK3 = (transparent & 8u) ? 0xFFu : 0u; for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) { 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 // re-derivation. Major rewrite of the dispatcher path that drove // 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; int16_t spritePxStart; 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 // (group, bitMask, 4 plane RMW). 2x speedup over the per-pixel // 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; int16_t row; 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; int16_t row; 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. // Once Phase 9 sets s->pixels = NULL the planar shadow becomes // 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) { uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)]; 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 // buffer (20 groups x 4 plane words). Same shape as the Amiga's // 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. static void stPlanarToChunkyRow(const StPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) { 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; uint16_t lo = 0xACE1u; 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 // fread fills it directly, no chunky scratch or c2p step. -bool halSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { +bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { StPlanarT *pd; 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; 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 // NULL as "port has no chunky shadow" (same contract Amiga uses). -uint8_t *halSurfaceAllocPixels(void) { +uint8_t *jlpSurfaceAllocPixels(void) { 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 // 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 @@ -2230,7 +2140,7 @@ void halSurfaceFreePixels(uint8_t *pixels) { // routine, which computes plane offsets internally via d16(a0) // chains. planeIdx >= 1 returns NULL since they don't make sense // in interleaved layout. -uint8_t *halSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) { +uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) { StPlanarT *pd; 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 // stores NULL in s->pixels and skips the chunky memset. -uint8_t *halStageAllocPixels(void) { +uint8_t *jlpStageAllocPixels(void) { return NULL; } - -void halStageFreePixels(uint8_t *pixels) { - free(pixels); -} diff --git a/src/port/atarist/input.c b/src/atarist/input.c similarity index 96% rename from src/port/atarist/input.c rename to src/atarist/input.c index 4a073ea..38495ba 100644 --- a/src/port/atarist/input.c +++ b/src/atarist/input.c @@ -14,17 +14,17 @@ // consume the documented byte count to stay in sync. // // 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 -// advance during halInputPoll, never at interrupt time -- jlInputPoll -// snapshots gKeyState into gKeyPrev before halInputPoll runs. +// advance during jlpInputPoll, never at interrupt time -- jlInputPoll +// snapshots gKeyState into gKeyPrev before jlpInputPoll runs. #include #include #include -#include "hal.h" +#include "port.h" #include "inputInternal.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 // 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]; // ----- Internal helpers ----- @@ -283,13 +283,13 @@ static long restoreIkbdVector(void) { // ----- HAL API (alphabetical) ----- -void halJoystickReset(jlJoystickE js) { +void jlpJoystickReset(jlJoystickE js) { // Atari ST sticks are digital -- no calibration to do. (void)js; } -void halInputInit(void) { +void jlpInputInit(void) { memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); @@ -320,7 +320,7 @@ void halInputInit(void) { // gIsrJoyByte[] at ~100 Hz max; the ~60-byte memcpy is essentially // never racing a write. Worst case is a single key or one packet // lagging one frame -- well under perceptible. -void halInputPoll(void) { +void jlpInputPoll(void) { int32_t dx; int32_t dy; int32_t newX; @@ -370,7 +370,7 @@ void halInputPoll(void) { } -void halInputShutdown(void) { +void jlpInputShutdown(void) { if (!gHooked) { return; } diff --git a/src/port/atarist/lineSpan.s b/src/atarist/lineSpan.s similarity index 99% rename from src/port/atarist/lineSpan.s rename to src/atarist/lineSpan.s index 4ac05e9..0c87ecc 100644 --- a/src/port/atarist/lineSpan.s +++ b/src/atarist/lineSpan.s @@ -537,7 +537,7 @@ _surface68kStFillRectSingleGroup: | | Specialized 8x8 single-group fill: 16-way color dispatch + 8 rows | fully unrolled. Drops the per-row subq+bne overhead that the -| generic FRG_LOOP pays. Used by halTileFillPlanes. +| generic FRG_LOOP pays. Used by jlpTileFill. | | void surface68kStTileFill8x8(uint8_t *firstGroupPtr, | uint16_t mask, diff --git a/src/port/atarist/spriteAsm.s b/src/atarist/spriteAsm.s similarity index 100% rename from src/port/atarist/spriteAsm.s rename to src/atarist/spriteAsm.s diff --git a/src/blank/blank.c b/src/blank/blank.c new file mode 100644 index 0000000..b41b8ef --- /dev/null +++ b/src/blank/blank.c @@ -0,0 +1,135 @@ +// JoeyLib new-port template ("jBlank"). +// +// Copy this directory to src//, set JOEYLIB_PLATFORM_ in +// include/joey/platform.h (mirror the JOEYLIB_PLATFORM_BLANK blocks there), and +// fill in the TODOs below. Build with -DJOEYLIB_PLATFORM_. +// +// 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() +// 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_ 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_ to your platform.h +// block and implement the matching jlpAudio() here (see src//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). diff --git a/src/codegen/spriteCompile.c b/src/codegen/spriteCompile.c index 0e5f6cf..99502a4 100644 --- a/src/codegen/spriteCompile.c +++ b/src/codegen/spriteCompile.c @@ -14,7 +14,7 @@ #include "joey/sprite.h" #include "joey/surface.h" #include "codegenArenaInternal.h" -#include "hal.h" +#include "port.h" #include "spriteEmitter.h" #include "spriteInternal.h" #include "surfaceInternal.h" @@ -30,16 +30,16 @@ // `cap` argument and returns SPRITE_EMIT_OVERFLOW when a routine would // not fit. jlSpriteCompile then bails cleanly (no arena alloc, // 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 // 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 // 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 // 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). #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 * 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) { return false; } @@ -186,7 +186,7 @@ bool jlSpriteCompile(jlSpriteT *sp) { /* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single * routine that did not fit the scratch is rejected here as well. */ if (totalSize > 0xFFFFu) { - halBigFree(scratch); + jlpBigFree(scratch); return false; } if (totalSize == 0) { @@ -194,14 +194,14 @@ bool jlSpriteCompile(jlSpriteT *sp) { * no compiled bytes -- spriteCompiledDraw / SaveUnder / * RestoreUnder would dereference a degenerate slot or chunky * shadow. Bail so sp->slot stays NULL and the dispatcher - * routes through the interpreted halSpriteXxxPlanes path. */ - halBigFree(scratch); + * routes through the interpreted the jlpSprite*Planes ops path. */ + jlpBigFree(scratch); return false; } slot = codegenArenaAlloc(totalSize); if (slot == NULL) { - halBigFree(scratch); + jlpBigFree(scratch); return false; } @@ -222,7 +222,7 @@ bool jlSpriteCompile(jlSpriteT *sp) { /* The second pass disagreed with the sizing pass (must * not happen, but never write past the slot). Roll back. */ codegenArenaFree(slot); - halBigFree(scratch); + jlpBigFree(scratch); return false; } /* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED) @@ -249,7 +249,7 @@ bool jlSpriteCompile(jlSpriteT *sp) { } #endif sp->slot = slot; - halBigFree(scratch); + jlpBigFree(scratch); 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 * src/core/sprite.c (jlSpriteDraw / jlSpriteSaveUnder / jlSpriteRestoreUnder) * sees SPRITE_NOT_COMPILED for the shift and falls back to the - * interpreter, which handles arbitrary x via halSpriteDrawPlanes / - * halSpriteSavePlanes / halSpriteRestorePlanes. */ + * interpreter, which handles arbitrary x via jlpSpriteDrawPlanes / + * jlpSpriteSavePlanes / jlpSpriteRestorePlanes. */ // Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40. // 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; 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)); - p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return; - p1 = halSurfacePlanePtr(dst, 1); - p2 = halSurfacePlanePtr(dst, 2); - p3 = halSurfacePlanePtr(dst, 3); + p0 = jlpSurfacePlanePtr(dst, 0); if (p0 == NULL) return; + p1 = jlpSurfacePlanePtr(dst, 1); + p2 = jlpSurfacePlanePtr(dst, 2); + p3 = jlpSurfacePlanePtr(dst, 3); fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_DRAW]); 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. */ backup->sizeBytes = (uint16_t)((uint16_t)heightPx * (widthPx >> 1)); - p0 = halSurfacePlanePtr(src, 0); if (p0 == NULL) return; - p1 = halSurfacePlanePtr(src, 1); - p2 = halSurfacePlanePtr(src, 2); - p3 = halSurfacePlanePtr(src, 3); + p0 = jlpSurfacePlanePtr(src, 0); if (p0 == NULL) return; + p1 = jlpSurfacePlanePtr(src, 1); + p2 = jlpSurfacePlanePtr(src, 2); + p3 = jlpSurfacePlanePtr(src, 3); fn = (SaveFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_SAVE]); 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); 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; - p1 = halSurfacePlanePtr(dst, 1); - p2 = halSurfacePlanePtr(dst, 2); - p3 = halSurfacePlanePtr(dst, 3); + p0 = jlpSurfacePlanePtr(dst, 0); if (p0 == NULL) return; + p1 = jlpSurfacePlanePtr(dst, 1); + p2 = jlpSurfacePlanePtr(dst, 2); + p3 = jlpSurfacePlanePtr(dst, 3); fn = (RestoreFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_RESTORE]); 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) * 2+ : non-byte-aligned x, never compiled (emitter returns 0); the * 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 // 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]; if (routeOffset == SPRITE_NOT_COMPILED) { /* 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, * but COMPILED_SPRITE_WRITES_PLANES is 1 on ST so it normally * suppresses the planes hook). For non-aligned shifts we * deliberately want the interpreted planes hook to run, so - * delegate via halSpriteDrawPlanes here. */ - halSpriteDrawPlanes(dst, sp, x, y); + * delegate via jlpSpriteDrawPlanes here. */ + jlpSpriteDrawPlanes(dst, sp, x, y); return; } - base = halSurfacePlanePtr(dst, 0); + base = jlpSurfacePlanePtr(dst, 0); if (base == NULL) { 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 * dispatcher's check on sp->routineOffsets[shift][SPRITE_OP_SAVE/_RESTORE] * == SPRITE_NOT_COMPILED already routes those through the - * interpreted halSpriteSavePlanes / halSpriteRestorePlanes. These + * interpreted jlpSpriteSavePlanes / jlpSpriteRestorePlanes. These * stubs exist only to satisfy the linker. */ 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; diff --git a/src/codegen/spriteEmitInterleaved68k.c b/src/codegen/spriteEmitInterleaved68k.c deleted file mode 100644 index ef969dd..0000000 --- a/src/codegen/spriteEmitInterleaved68k.c +++ /dev/null @@ -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; -} diff --git a/src/codegen/spriteEmitPlanar68k.c b/src/codegen/spriteEmitPlanar68k.c deleted file mode 100644 index d50f109..0000000 --- a/src/codegen/spriteEmitPlanar68k.c +++ /dev/null @@ -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; -} diff --git a/src/codegen/spriteEmitter.h b/src/codegen/spriteEmitter.h index d119165..34b4037 100644 --- a/src/codegen/spriteEmitter.h +++ b/src/codegen/spriteEmitter.h @@ -4,7 +4,7 @@ // function. jlSpriteCompile.c picks the right one at compile time // (via #ifdef on JOEYLIB_PLATFORM_*) for the runtime build. 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 // 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 // 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 -// to halSpriteDrawPlanes. +// to jlpSpriteDrawPlanes. uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift); diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index 693bd85..d216484 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -58,6 +58,8 @@ #define EXPECTED_TILE_TARGET 3u #elif defined(JOEYLIB_PLATFORM_IIGS) #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 #error "Unknown platform for asset loader" #endif diff --git a/src/core/audio.c b/src/core/audio.c index 24ee913..34c68ab 100644 --- a/src/core/audio.c +++ b/src/core/audio.c @@ -6,7 +6,7 @@ #include #include "joey/audio.h" -#include "hal.h" +#include "port.h" static bool gAudioReady = false; @@ -16,7 +16,7 @@ bool jlAudioInit(void) { if (gAudioReady) { return true; } - gAudioReady = halAudioInit(); + gAudioReady = jlpAudioInit(); return gAudioReady; } @@ -25,7 +25,7 @@ void jlAudioShutdown(void) { if (!gAudioReady) { return; } - halAudioShutdown(); + jlpAudioShutdown(); gAudioReady = false; } @@ -34,7 +34,7 @@ void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { if (!gAudioReady || data == NULL || length == 0) { return; } - halAudioPlayMod(data, length, loop); + jlpAudioPlayMod(data, length, loop); } @@ -42,7 +42,7 @@ void jlAudioStopMod(void) { if (!gAudioReady) { return; } - halAudioStopMod(); + jlpAudioStopMod(); } @@ -50,7 +50,7 @@ bool jlAudioIsPlayingMod(void) { if (!gAudioReady) { 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) { 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) { 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 // (and other simple hardware tone generators) is direct port // 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) { 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) { return false; } - return halAudioTickRegister(fn, hz); + return jlpAudioTickRegister(fn, hz); } void jlAudioCriticalEnter(void) { - halAudioCriticalEnter(); + jlpAudioCriticalEnter(); } void jlAudioCriticalExit(void) { - halAudioCriticalExit(); + jlpAudioCriticalExit(); } @@ -118,7 +118,7 @@ void jlAudioStopSfx(uint8_t slot) { if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) { return; } - halAudioStopSfx(slot); + jlpAudioStopSfx(slot); } @@ -127,9 +127,9 @@ void jlAudioFrameTick(void) { return; } #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 // so per-frame audio cost stays at the gAudioReady branch above. - halAudioFrameTick(); + jlpAudioFrameTick(); #endif } diff --git a/src/core/draw.c b/src/core/draw.c index 399cd21..546636a 100644 --- a/src/core/draw.c +++ b/src/core/draw.c @@ -9,7 +9,7 @@ #include "joey/draw.h" #include "joey/debug.h" -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" @@ -30,54 +30,14 @@ // ----- 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 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); // ----- Internal helpers (alphabetical) ----- -static void fillRectClipped(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; - - // 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)); - } - } -} - +// (fillRectClipped moved to src/generic/genericDraw.c as jlpGenericFillRect, +// the chunky default for the migrated jlpFillRect op.) // 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 <= @@ -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 // 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) { - if (!halFastFillRect(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex)) { - fillRectClipped(s, x, y, w, h, colorIndex); - } - halFillRectPlanes(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex); + // Compile-time-selected op: machine fill (IIgs asm stage / Amiga+ST planar) + // or the generic chunky fill. + jlpFillRect(s, x, y, w, h, colorIndex); 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 leftX; int16_t rightX; - uint8_t *row; uint8_t pix; bool pixMatch; uint8_t newNibble; @@ -150,19 +108,17 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin continue; } - /* Phase 9: planar ports have NULL s->pixels and the asm fast - * paths take a chunky-row pointer. Skip them on planar; the C - * fallback below uses halSamplePixel which works on both - * storage layouts. */ +#if defined(JL_HAS_FLOOD_WALK_AND_SCANS) + // Highest-tier asm fast path (IIgs): 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. On this platform it + // always handles the pop, so the C fallback below is never reached. 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; - if (halFastFloodWalkAndScans(s->pixels, x, y, + if (jlpFloodWalkAndScans(s->pixels, x, y, matchColor, newNibble, matchEqual, stackX, stackY, &sp, FLOOD_STACK_SIZE, @@ -176,28 +132,23 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin continue; } } +#endif - /* Fallback path: compute row only if chunky; halFastFloodWalk - * needs it but isn't implemented on Amiga. */ - row = (s->pixels != NULL) ? &s->pixels[SURFACE_ROW_OFFSET(y)] : NULL; - - // 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. + // Walk-out: find the matching run containing the seed. Planar ports + // (Amiga) override with a plane-aware walk; everyone else uses the + // portable jlpSamplePixel walk, which works on chunky and planar. { +#if defined(JL_HAS_FLOOD_WALK_PLANES) bool seedMatched; - if (row != NULL && halFastFloodWalk(row, x, matchColor, newNibble, matchEqual, - &seedMatched, &leftX, &rightX)) { - if (!seedMatched) { - continue; - } - } else if (halFloodWalkPlanes(s, x, y, matchColor, newNibble, matchEqual, + if (jlpFloodWalkPlanes(s, x, y, matchColor, newNibble, matchEqual, &seedMatched, &leftX, &rightX)) { if (!seedMatched) { continue; } - } else { - pix = halSamplePixel(s, x, y); + } else +#endif + { + pix = jlpSamplePixel(s, x, y); pixMatch = (pix == matchColor); if (matchEqual) { 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. leftX = x; while (leftX > 0) { - pix = halSamplePixel(s, (int16_t)(leftX - 1), y); + pix = jlpSamplePixel(s, (int16_t)(leftX - 1), y); pixMatch = (pix == matchColor); if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) { break; @@ -223,7 +174,7 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin // Walk right to find the end. rightX = x; while (rightX < SURFACE_WIDTH - 1) { - pix = halSamplePixel(s, (int16_t)(rightX + 1), y); + pix = jlpSamplePixel(s, (int16_t)(rightX + 1), y); pixMatch = (pix == matchColor); if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) { break; @@ -246,15 +197,14 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin fillRectOnSurface(s, leftX, y, spanW, 1, newNibble); } - // Scan rows above and below for run boundaries. The hot - // per-pixel match check goes through halFastFloodScanRow on - // ports that have it (IIgs); fills markBuf[] with 1/0 per - // pixel so the run-edge walk below is array-only -- no - // function call, no nibble extract. + // Scan rows above and below for run boundaries: fill markBuf[] with + // 1/0 per pixel, then walk it for run-edge transitions and push a + // seed at each run's right edge. Planar ports (Amiga) override the + // markBuf fill with a plane-aware scan; everyone else fills it via + // the portable jlpSamplePixel walk. { int16_t i; int16_t spanLen; - uint8_t *scanRow; int16_t scanY; int16_t side; bool curHit; @@ -273,52 +223,43 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin } scanY = (int16_t)(y + 1); } - scanRow = (s->pixels != NULL) ? &s->pixels[SURFACE_ROW_OFFSET(scanY)] : NULL; - // Prefer the combined scan+push asm path (one call per - // scan, no markBuf and no per-pixel C edge walk). Skip - // the asm tiers if we don't have a chunky row pointer - // (Phase 9 planar ports). - if (scanRow == NULL || - !halFastFloodScanAndPush(scanRow, leftX, rightX, - matchColor, newNibble, matchEqual, - scanY, stackX, stackY, - &sp, FLOOD_STACK_SIZE)) { - 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; + if ( +#if defined(JL_HAS_FLOOD_SCAN_ROW_PLANES) + !jlpFloodScanRowPlanes(s, leftX, rightX, scanY, + matchColor, newNibble, matchEqual, + floodMarkBuf) +#else + true +#endif + ) { + // C fallback: fill markBuf the slow way. for (i = 0; i < spanLen; i++) { - curHit = floodMarkBuf[i] != 0; - if (!curHit && prevHit) { - if (sp < FLOOD_STACK_SIZE) { - stackX[sp] = (int16_t)(leftX + i - 1); - stackY[sp] = scanY; - sp++; - } - } - prevHit = curHit; + pix = jlpSamplePixel(s, (int16_t)(leftX + i), scanY); + pixMatch = (pix == matchColor); + floodMarkBuf[i] = (uint8_t)(matchEqual + ? (pixMatch ? 1 : 0) + : ((!pixMatch && pix != newNibble) ? 1 : 0)); } - 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) { - stackX[sp] = rightX; + stackX[sp] = (int16_t)(leftX + i - 1); stackY[sp] = scanY; 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 // Bresenham line/circle fallbacks (which accumulate one bounding box and // 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) { - uint8_t *byte; - uint8_t nibble; - if (s == NULL) { return; } if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { return; } - - if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex) && s->pixels != NULL) { - /* Cast to uint16_t before shift -- already validated x >= 0, - * 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)); - } - } + // Compile-time-selected op: machine plot (asm/planar) or the generic chunky + // nibble RMW (src/generic/genericDraw.c). No runtime dispatch. + jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex); } @@ -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 y; int16_t err; - int16_t ir; int16_t minX; int16_t minY; 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 // such an r to int16_t can wrap negative (r > 32767), so reject it // here before the cast feeds the fast path / dirty-rect extents. +#if defined(JL_HAS_DRAW_CIRCLE) 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 && - cy - ir >= 0 && cy + ir < SURFACE_HEIGHT && - halFastDrawCircle(s, cx, cy, r, colorIndex)) { + cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) { + jlpDrawCircle(s, cx, cy, r, colorIndex); surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); return; } } +#endif // Bresenham midpoint: maintain (x, y) on the perimeter, eight- // octant symmetry plots all 8 reflections each iteration. Plots @@ -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 // fast path; bounding-box dirty marking happens here in C either // way. +#if defined(JL_HAS_DRAW_LINE) if (x0 >= 0 && x0 < SURFACE_WIDTH && x1 >= 0 && x1 < SURFACE_WIDTH && - y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT && - halFastDrawLine(s, x0, y0, x1, y1, colorIndex)) { + y0 >= 0 && y0 < SURFACE_HEIGHT && y1 >= 0 && y1 < SURFACE_HEIGHT) { int16_t bbx = (x0 < x1) ? x0 : x1; int16_t bby = (y0 < y1) ? y0 : y1; int16_t bbw = (int16_t)(((x0 > x1) ? x0 : x1) - bbx + 1); int16_t bbh = (int16_t)(((y0 > y1) ? y0 : y1) - bby + 1); + // On-surface fast path: machine asm / planar, compiled out where unsupported. + jlpDrawLine(s, x0, y0, x1, y1, colorIndex); surfaceMarkDirtyRect(s, bbx, bby, bbw, bbh); return; } +#endif // Diagonal fallback: plot through plotPixelNoMark so each pixel still // clips off-surface individually (matching jlDrawPixel), but skip the @@ -584,7 +519,7 @@ void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { return; } #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 // call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table, // 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) { return; } - (void)halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex); + jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex); if (s == jlStageGet()) { 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 // re-clipping each one through jlFillRect. 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), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1)); 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) { 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. */ - seedColor = halSamplePixel(s, x, y); + seedColor = jlpSamplePixel(s, x, y); if ((seedColor & 0x0F) == (newColor & 0x0F)) { 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) { return; } - pix = halSamplePixel(s, x, y); + pix = jlpSamplePixel(s, x, y); // Starting on a boundary pixel or already-filled pixel: nothing // to do. 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) { 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 * 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; doubled = (uint8_t)((nibble << 4) | nibble); - if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) { - memset(s->pixels, doubled, SURFACE_PIXELS_SIZE); - } + // Compile-time-selected op: a machine override (asm / planar) or the + // portable-C generic default -- no runtime dispatch. + jlpSurfaceClear(s, doubled); surfaceMarkDirtyAll(s); } diff --git a/src/core/hal.h b/src/core/hal.h deleted file mode 100644 index 255164f..0000000 --- a/src/core/hal.h +++ /dev/null @@ -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//. 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 - -#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 diff --git a/src/core/init.c b/src/core/init.c index de45b06..b3a6f8d 100644 --- a/src/core/init.c +++ b/src/core/init.c @@ -10,7 +10,7 @@ #include "joey/core.h" #include "codegenArenaInternal.h" -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" @@ -47,35 +47,22 @@ void coreSetError(const char *message) { // ----- Default large-allocation backing ----- // -// jlAlloc / jlFree route through halBigAlloc / halBigFree. Most ports -// have a normal heap, so the default below (plain malloc / free) serves -// them and they do NOT need to implement the hook themselves. Only ports -// whose C heap is too small or per-allocation-capped to satisfy a large -// request -- currently just the IIgs -- provide their own halBigAlloc / -// 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 +// jlAlloc / jlFree route through jlpBigAlloc / jlpBigFree. Most ports have a +// normal heap, so the malloc/free generic default (src/generic/genericPort.c) +// serves them; only ports whose C heap is too small or per-allocation-capped +// (IIgs: bank-0 heap) define JL_HAS_BIG_ALLOC and override with their native +// allocator (IIgs Memory Manager NewHandle / DisposeHandle). // ----- Public API (alphabetical) ----- void *jlAlloc(uint32_t bytes) { - return halBigAlloc(bytes); + return jlpBigAlloc(bytes); } void jlFree(void *p) { - halBigFree(p); + jlpBigFree(p); } @@ -94,18 +81,18 @@ bool jlInit(const jlConfigT *config) { memcpy(&gConfig, config, sizeof(gConfig)); - // halInit must run before stageAlloc: on IIgs the stage's pixel - // buffer comes from halStageAllocPixels, which depends on shadow / - // SHR setup that halInit performs. - if (!halInit(&gConfig)) { - const char *halMsg = halLastError(); - coreSetError(halMsg != NULL ? halMsg : "halInit failed"); + // jlpInit must run before stageAlloc: on IIgs the stage's pixel + // buffer comes from jlpStageAllocPixels, which depends on shadow / + // SHR setup that jlpInit performs. + if (!jlpInit(&gConfig)) { + const char *halMsg = jlpLastError(); + coreSetError(halMsg != NULL ? halMsg : "jlpInit failed"); return false; } if (!stageAlloc()) { coreSetError("failed to allocate stage surface"); - halShutdown(); + jlpShutdown(); return false; } @@ -113,11 +100,11 @@ bool jlInit(const jlConfigT *config) { : DEFAULT_CODEGEN_BYTES)) { coreSetError("failed to allocate codegen arena"); stageFree(); - halShutdown(); + jlpShutdown(); return false; } - halInputInit(); + jlpInputInit(); gInitialized = true; return true; @@ -138,10 +125,10 @@ void jlShutdown(void) { if (!gInitialized) { return; } - halInputShutdown(); + jlpInputShutdown(); codegenArenaShutdown(); stageFree(); - halShutdown(); + jlpShutdown(); gInitialized = false; clearError(); } @@ -153,20 +140,20 @@ const char *jlVersionString(void) { void jlWaitVBL(void) { - halWaitVBL(); + jlpWaitVBL(); } uint16_t jlFrameCount(void) { - return halFrameCount(); + return jlpFrameCount(); } uint16_t jlFrameHz(void) { - return halFrameHz(); + return jlpFrameHz(); } uint32_t jlMillisElapsed(void) { - return halMillisElapsed(); + return jlpMillisElapsed(); } diff --git a/src/core/input.c b/src/core/input.c index 13c8296..70f395f 100644 --- a/src/core/input.c +++ b/src/core/input.c @@ -12,7 +12,7 @@ #include #include "joey/input.h" -#include "hal.h" +#include "port.h" #include "inputInternal.h" @@ -54,14 +54,14 @@ void jlInputPoll(void) { #ifdef JOEYLIB_PLATFORM_IIGS // One asm pass for: TTL decrement + key snapshot + mouse/joy // button snapshots. Replaces 3 memcpys + the C TTL loop - // that used to live in halInputPoll. ~0.6 ms saved per frame. + // that used to live in jlpInputPoll. ~0.6 ms saved per frame. iigsInputSnapshot(); #else memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); memcpy(gMouseButtonPrev, gMouseButtonState, sizeof(gMouseButtonState)); memcpy(gJoyButtonPrev, gJoyButtonState, sizeof(gJoyButtonState)); #endif - halInputPoll(); + jlpInputPoll(); } @@ -220,5 +220,5 @@ void jlJoystickReset(jlJoystickE js, uint8_t deadZone) { return; } gJoyDeadZone[js] = deadZone; - halJoystickReset(js); + jlpJoystickReset(js); } diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h index 1579e2a..eb1d40e 100644 --- a/src/core/inputInternal.h +++ b/src/core/inputInternal.h @@ -1,6 +1,6 @@ // 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 // joysticks. The core compares against the matching *Prev shadow // 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. extern uint8_t gJoyDeadZone [JOYSTICK_COUNT]; -// Per-port hook: called from jlJoystickReset to clear any auto- -// disconnect state and arm a fresh center capture on the next poll. -void halJoystickReset(jlJoystickE js); +// (jlpJoystickReset hook -- called from jlJoystickReset to clear any auto- +// disconnect state and arm a fresh center capture -- is declared with the +// other platform-only services in src/core/port.h.) #endif diff --git a/src/core/port.h b/src/core/port.h new file mode 100644 index 0000000..f8f07f5 --- /dev/null +++ b/src/core/port.h @@ -0,0 +1,708 @@ +// Internal platform-override registry (the new joeylib's joey.h equivalent). +// +// Every overridable operation is a jlp() resolved at COMPILE TIME: a +// portable-C default lives in src/generic/ and a machine supplies its own +// version when it defines JL_HAS_ 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_. +// +// Included by src/core/*.c, src//*.c, and src/generic/*.c. NOT public. + +#ifndef JOEYLIB_PORT_H +#define JOEYLIB_PORT_H + +#include + +#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 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 +// 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(...) +// 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, 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: 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 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, 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_ (include/joey/platform.h): a macro override (IIgs, above) already +// #define-d jlp; a function override (Amiga/ST) just declares its prototype +// here; otherwise jlp 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_ in +// include/joey/platform.h. Same jlp 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 diff --git a/src/core/present.c b/src/core/present.c index 420820a..92ab005 100644 --- a/src/core/present.c +++ b/src/core/present.c @@ -8,7 +8,7 @@ #include "joey/debug.h" #include "joey/present.h" -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" @@ -21,6 +21,6 @@ void jlStagePresent(void) { if (stage == NULL) { return; } - halPresent(stage); + jlpPresent(stage); stageDirtyClearAll(); } diff --git a/src/core/sprite.c b/src/core/sprite.c index 3cbe3e7..30d1324 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -10,7 +10,7 @@ #include "joey/sprite.h" #include "joey/tile.h" #include "codegenArenaInternal.h" -#include "hal.h" +#include "port.h" #include "spriteInternal.h" #include "surfaceInternal.h" @@ -22,7 +22,7 @@ #define TRANSPARENT_NIBBLE 0 // 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 // hooks are either no-op stubs (chunky-only IIgs/DOS) or the only // thing writing planes (ST: chunky-shadow + planes). Slow / interpreter @@ -31,7 +31,7 @@ // is the only draw. /* ST also runs pure planar post-Phase-9 (s->pixels NULL); the JIT * 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. */ #if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) #define COMPILED_SPRITE_WRITES_PLANES 1 @@ -158,7 +158,7 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16 wTiles = sp->widthTiles; /* 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 * happen there. Phase 9 dropped the chunky shadow on Amiga. */ if (s->pixels != NULL) { @@ -275,8 +275,8 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y } } else { // Planar port (Phase 9: no chunky shadow). Derive chunky 4bpp - // pixels from plane bits via halSamplePixel and pack into the - // tile-major sprite layout. Slow (per-pixel halSamplePixel) but + // pixels from plane bits via jlpSamplePixel and pack into the + // tile-major sprite layout. Slow (per-pixel jlpSamplePixel) but // only runs at sprite-capture time, not per frame. Without // this branch sprites loaded from a work surface on Amiga/ST // 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); for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) { px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col); - hiNibble = (uint8_t)(halSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu); - loNibble = (uint8_t)(halSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu); + hiNibble = (uint8_t)(jlpSamplePixel(src, (int16_t)px, (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)] = (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)) { spriteCompiledDraw(s, sp, x, y); 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); return; } 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); spriteCompiledDraw (s, sp, x, y); if (!COMPILED_SPRITE_WRITES_PLANES) { - halSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); - halSpriteDrawPlanes(s, sp, x, y); + jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); + jlpSpriteDrawPlanes(s, sp, x, y); } surfaceMarkDirtyRect (s, x, y, (int16_t)widthPx, (int16_t)heightPx); return; @@ -502,7 +502,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { if (routeOffset != SPRITE_NOT_COMPILED) { spriteCompiledRestoreUnder(s, backup); 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); return; @@ -512,7 +512,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { /* Slow / interpreted memcpy fallback. Skip the chunky memcpy if * 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) { int16_t row; int16_t byteStart; @@ -527,7 +527,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { (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); } @@ -568,7 +568,7 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, if (routeOffset != SPRITE_NOT_COMPILED) { spriteCompiledSaveUnder(s, sp, x, y, backup); 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; } @@ -626,7 +626,7 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, return; } /* 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) { for (row = 0; row < h; 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); } } - 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 */ } diff --git a/src/core/surface.c b/src/core/surface.c index 4d5b4a8..d16c23f 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -7,7 +7,7 @@ #include #include "joey/surface.h" -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" #ifdef JOEYLIB_PLATFORM_IIGS @@ -64,10 +64,10 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) { if (dst == NULL || src == NULL || dst == src) { 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->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); // Bulk-replacing the stage's SCB/palette must re-mark them dirty so // the per-port present re-uploads them; surfaceMarkDirtyAll only @@ -87,23 +87,23 @@ jlSurfaceT *jlSurfaceCreate(void) { coreSetError("out of memory allocating surface"); 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. // Classify NULL by the build's storage model: chunky ports need // pixels, planar ports need portData. A NULL in the required slot // is an out-of-memory failure (header contract: return NULL). - s->pixels = halSurfaceAllocPixels(); - s->portData = halSurfaceAllocPortData(s, false); + s->pixels = jlpSurfaceAllocPixels(); + s->portData = jlpSurfaceAllocPortData(s, false); #ifdef JOEYLIB_NATIVE_CHUNKY if (s->pixels == NULL) { - halSurfaceFreePortData(s, false, s->portData); + jlpSurfaceFreePortData(s, false, s->portData); free(s); coreSetError("out of memory allocating surface pixels"); return NULL; } #else if (s->portData == NULL) { - halSurfaceFreePixels(s->pixels); + jlpSurfaceFreePixels(s->pixels); free(s); coreSetError("out of memory allocating surface planes"); return NULL; @@ -121,8 +121,8 @@ void jlSurfaceDestroy(jlSurfaceT *s) { if (s == gStage) { return; } - halSurfaceFreePortData(s, false, s->portData); - halSurfaceFreePixels(s->pixels); + jlpSurfaceFreePortData(s, false, s->portData); + jlpSurfaceFreePixels(s->pixels); free(s); } @@ -136,7 +136,7 @@ uint32_t jlSurfaceHash(const jlSurfaceT *s) { if (s == NULL) { return 0u; } - return halSurfaceHash(s); + return jlpSurfaceHash(s); } @@ -164,7 +164,7 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) { fclose(fp); return false; } - if (!halSurfaceLoadFile(dst, fp)) { + if (!jlpSurfaceLoadFile(dst, fp)) { fclose(fp); return false; } @@ -198,7 +198,7 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) { if (fp == NULL) { return false; } - if (!halSurfaceSaveFile(src, fp)) { + if (!jlpSurfaceSaveFile(src, fp)) { fclose(fp); return false; } @@ -280,26 +280,26 @@ bool stageAlloc(void) { if (gStage == NULL) { 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 source of truth. On chunky ports NULL pixels means the // back-buffer alloc failed (IIgs pins a fixed address and never // returns NULL, so this is a real OOM only on DOS). - gStage->pixels = halStageAllocPixels(); + gStage->pixels = jlpStageAllocPixels(); if (gStage->pixels != NULL) { memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE); } - gStage->portData = halSurfaceAllocPortData(gStage, true); + gStage->portData = jlpSurfaceAllocPortData(gStage, true); #ifdef JOEYLIB_NATIVE_CHUNKY if (gStage->pixels == NULL) { - halSurfaceFreePortData(gStage, true, gStage->portData); + jlpSurfaceFreePortData(gStage, true, gStage->portData); free(gStage); gStage = NULL; return false; } #else if (gStage->portData == NULL) { - halStageFreePixels(gStage->pixels); + jlpStageFreePixels(gStage->pixels); free(gStage); gStage = NULL; return false; @@ -325,8 +325,8 @@ void stageFree(void) { if (gStage == NULL) { return; } - halSurfaceFreePortData(gStage, true, gStage->portData); - halStageFreePixels(gStage->pixels); + jlpSurfaceFreePortData(gStage, true, gStage->portData); + jlpStageFreePixels(gStage->pixels); free(gStage); gStage = NULL; } diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h index 2e2c105..01d262b 100644 --- a/src/core/surfaceInternal.h +++ b/src/core/surfaceInternal.h @@ -20,7 +20,7 @@ // (Amiga, Atari ST) it points to a port-private struct describing the // 4 bitplanes (Amiga: 4 separate plane buffers + stride; ST: single // 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. struct jlSurfaceT { uint8_t *pixels; @@ -112,7 +112,7 @@ void stageDirtyClearAll(void); // 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 -// gRowOffsetLut (built once at halInit). +// gRowOffsetLut (built once at jlpInit). // // The explicit (y << 1) byte-pointer arithmetic dodges the multiply // 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 // that jlStagePresent flips to the display). Called from init.c during // jlInit / jlShutdown. The stage's pixel storage is supplied by -// the port HAL via halStageAllocPixels. +// the port HAL via jlpStageAllocPixels. bool stageAlloc(void); void stageFree(void); diff --git a/src/core/tile.c b/src/core/tile.c index 5eb3783..a17fc15 100644 --- a/src/core/tile.c +++ b/src/core/tile.c @@ -11,7 +11,7 @@ #include #include "joey/tile.h" -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" // (No -- the 4-byte-per-row inner copies are spelled out @@ -21,66 +21,13 @@ // ----- Prototypes ----- -static void copyTileOpaque(uint8_t *dst, const uint8_t *src); -static void copyTileMasked(uint8_t *dst, const uint8_t *src, uint8_t transparent); +// (copyTileOpaque / copyTileMasked moved to src/generic/genericTile.c as +// jlpGenericTileCopy / jlpGenericTileCopyMasked.) // ----- Internal helpers (alphabetical) ----- -// 32-byte block copy with the surface row stride between rows. dst -// and src already point at the row-0 first byte of their respective -// 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; - } -} +// (the chunky tile copy/masked-copy helpers now live in +// src/generic/genericTile.c as jlpGenericTileCopy / jlpGenericTileCopyMasked.) // ----- 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) { - uint8_t *dstRow0; - const uint8_t *srcRow0; - uint16_t dstPixelX; - uint16_t dstPixelY; - uint16_t srcPixelX; - uint16_t srcPixelY; + uint16_t dstPixelX; + uint16_t dstPixelY; if (dst == NULL || src == NULL) { 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); dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE); - srcPixelX = (uint16_t)((uint16_t)srcBx * TILE_PIXELS_PER_SIDE); - 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 (!halFastTileCopy(dstRow0, srcRow0)) { - copyTileOpaque(dstRow0, srcRow0); - } - } - halTileCopyPlanes(dst, dstBx, dstBy, src, srcBx, srcBy); + // Single op: machine asm/planar override or the generic chunky default. + jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY, 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) { - uint8_t *dstRow0; - const uint8_t *srcRow0; - uint16_t dstPixelX; - uint16_t dstPixelY; - uint16_t srcPixelX; - uint16_t srcPixelY; + uint16_t dstPixelX; + uint16_t dstPixelY; if (dst == NULL || src == NULL) { 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); dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE); - srcPixelX = (uint16_t)((uint16_t)srcBx * TILE_PIXELS_PER_SIDE); - 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); + jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex); surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY, TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); } void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { - uint8_t doubled; uint16_t pixelX; 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) { return; } - pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE); - pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); - doubled = (uint8_t)(((colorIndex & 0x0F) << 4) | (colorIndex & 0x0F)); - if (s->pixels != NULL - && !halFastTileFill(s, bx, by, - (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); + pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE); + pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); + // Single compile-time-selected op: machine asm/planar override or the + // generic chunky default. + jlpTileFill(s, bx, by, colorIndex); surfaceMarkDirtyRect(s, (int16_t)pixelX, (int16_t)pixelY, 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, 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) { return; } + if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { + return; + } fgColor &= 0x0Fu; bgColor &= 0x0Fu; - - if (dst->pixels == NULL) { - /* Planar port: in->pixels is in the port-specific layout - * produced by halTileSnapPlanes (plane-major on Amiga, - * row-major-with-planes-interleaved-per-row on ST). The - * colorization formula -- write fgColor bits where the shape - * bit is 1, bgColor bits where it's 0 -- has to be applied in - * 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); + // Single compile-time op: planar ports colorize in their own tile format + // (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste. + // Replaces the old runtime pixels==NULL planar/chunky branch. + jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor); + 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); } void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) { - uint8_t *dstRow; - const uint8_t *src; - uint16_t pixelX; - uint16_t pixelY; - uint8_t row; + uint16_t pixelX; + uint16_t pixelY; if (dst == NULL || in == NULL) { 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); pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); - src = &in->pixels[0]; - /* Skip the chunky write path on planar ports (dst->pixels NULL) -- - * 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]); + // Single op: machine asm/planar override or the generic chunky default. + jlpTilePaste(dst, bx, by, &in->pixels[0]); surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY, TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); } 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) { return; } if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { return; } - pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE); - pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE); - dst = &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]); + // Read-only single op: machine asm/planar override or the generic chunky + // default. No dirty mark. + jlpTileSnap(src, bx, by, &out->pixels[0]); } diff --git a/src/port/dos/audio.c b/src/dos/audio.c similarity index 96% rename from src/port/dos/audio.c rename to src/dos/audio.c index f9a7590..e5f1d8f 100644 --- a/src/port/dos/audio.c +++ b/src/dos/audio.c @@ -2,16 +2,16 @@ // by 8237 auto-init DMA, fed by libxmp-lite's mixer. // // 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 // 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 // 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 // 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. -// * 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. // // Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7, @@ -37,7 +37,7 @@ #include -#include "hal.h" +#include "port.h" #include "audioSfxMixInternal.h" #include "joey/audio.h" @@ -97,7 +97,7 @@ // 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 // 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. #define MIX_RATE 22050 #define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves @@ -116,7 +116,7 @@ static uint8_t gDspMinor = 0; static bool gDspPresent = false; // 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 // actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting. 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. // 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 // from interrupt context -- the mixer allocates internally and is // 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. -// 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. // xmp_play_buffer fills our bytes regardless of whether a module is // 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 // 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) { uint8_t fillHalf; @@ -467,7 +467,7 @@ static void sbIsr(void) { // ----- HAL API (alphabetical) ----- -bool halAudioInit(void) { +bool jlpAudioInit(void) { int major; int minor; int vector; @@ -539,7 +539,7 @@ bool halAudioInit(void) { } -void halAudioShutdown(void) { +void jlpAudioShutdown(void) { int vector; if (!gDspPresent) { @@ -571,12 +571,12 @@ void halAudioShutdown(void) { } -bool halAudioIsPlayingMod(void) { +bool jlpAudioIsPlayingMod(void) { 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) { 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) { 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) { return; } @@ -645,7 +645,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin #define PC_SPEAKER_PORT_GATE 0x61u #define PC_SPEAKER_CTRL_BYTE 0xB6u -void halAudioTone(uint16_t freqHz) { +void jlpAudioTone(uint16_t freqHz) { uint32_t divisor; uint8_t gate; @@ -802,11 +802,11 @@ static void adlibInit(void) { // and subtract -- the math comes out to exactly 18.2065 Hz // 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 // 0..gPitDivisor, not 0..65535 as uclock assumes). We expose // 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. // // 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 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. volatile uint32_t gAudioIsrFires = 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; 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 // writes to memory before the cli (and reload after sti), so // 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"); } -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 block; 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) { return; } @@ -1004,7 +1004,7 @@ void halAudioStopMod(void) { } -void halAudioStopSfx(uint8_t slot) { +void jlpAudioStopSfx(uint8_t slot) { if (slot >= JOEY_AUDIO_SFX_SLOTS) { return; } @@ -1018,7 +1018,7 @@ void halAudioStopSfx(uint8_t slot) { // 22050 Hz half-buffer of 2048 samples drains ~143000 samples/sec // of headroom, well above the playback rate, so a missed frame or // two is OK without underflow. -void halAudioFrameTick(void) { +void jlpAudioFrameTick(void) { int i; if (!gDspPresent) { diff --git a/src/port/dos/hal.c b/src/dos/hal.c similarity index 59% rename from src/port/dos/hal.c rename to src/dos/hal.c index 7e63c2f..aad22ef 100644 --- a/src/port/dos/hal.c +++ b/src/dos/hal.c @@ -24,7 +24,7 @@ #include #include -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" // ----- Constants ----- @@ -62,7 +62,7 @@ static uint8_t gPrevInVret; static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; 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 // 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 @@ -88,7 +88,7 @@ static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint if (!gExpandLutValid || palBase != gExpandLutBase) { 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 // 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 @@ -237,7 +237,7 @@ static void uploadPaletteIfNeeded(const jlSurfaceT *src) { // ----- HAL API (alphabetical) ----- -bool halInit(const jlConfigT *config) { +bool jlpInit(const jlConfigT *config) { __dpmi_regs regs; (void)config; @@ -267,12 +267,7 @@ bool halInit(const jlConfigT *config) { } -const char *halLastError(void) { - return NULL; -} - - -void halPresent(const jlSurfaceT *src) { +void jlpPresent(const jlSurfaceT *src) { int16_t y; uint8_t minWord; uint8_t maxWord; @@ -313,7 +308,7 @@ void halPresent(const jlSurfaceT *src) { // rising-edge wait (so back-to-back calls each yield one frame), // first wait while we're already in retrace, then wait until we // re-enter retrace. -void halWaitVBL(void) { +void jlpWaitVBL(void) { while (inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT) { /* spin while still in current retrace */; } @@ -323,7 +318,7 @@ void halWaitVBL(void) { } -uint16_t halFrameCount(void) { +uint16_t jlpFrameCount(void) { uint8_t now; 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 * (70.086 to be exact). Reporting 70 keeps ops/sec scaling * 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 // 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 uint16_t gAudioIsrHz; extern volatile bool gAudioIsrActive; @@ -363,7 +358,7 @@ extern volatile bool gAudioIsrActive; static uclock_t gMillisBase = 0; static bool gMillisBaseSet = false; -uint32_t halMillisElapsed(void) { +uint32_t jlpMillisElapsed(void) { uclock_t delta; if (gAudioIsrActive && gAudioIsrHz > 0u) { @@ -391,7 +386,7 @@ uint32_t halMillisElapsed(void) { } -void halShutdown(void) { +void jlpShutdown(void) { __dpmi_regs regs; 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 // unused; the chunky `pixels` buffer feeds the present-time // 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) { - (void)s; - (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; -} - +// surfaceCopyPlanes migrated: DOS defines no JL_HAS_SURFACE_COPY_PLANES, so it +// links the no-op generic jlpGenericSurfaceCopyPlanes. /* Phase 9 reader hooks: chunky ports use the original s->pixels-based * 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; -} diff --git a/src/port/dos/input.c b/src/dos/input.c similarity index 96% rename from src/port/dos/input.c rename to src/dos/input.c index c8f18f7..bec31b8 100644 --- a/src/port/dos/input.c +++ b/src/dos/input.c @@ -2,11 +2,11 @@ // // Keyboard: hooks INT 9 to capture AT set-1 scan codes from port 0x60. // The ISR reads the scan code, maps it to a jlKeyE, updates a private -// gIsrState buffer, and sends EOI to the PIC. halInputPoll snapshots +// gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots // gIsrState into gKeyState with interrupts disabled. // // 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. // If the ISR wrote directly to gKeyState, press/release events that // happened between polls would already have landed there -- the memcpy @@ -18,8 +18,8 @@ // will hang the machine. // // Mouse: uses INT 33h, the standard MS DOS mouse driver interface. -// halInputInit calls function 0 (reset/detect), then 7/8 to clamp the -// driver's coordinate range to the surface dimensions. halInputPoll +// jlpInputInit calls function 0 (reset/detect), then 7/8 to clamp the +// driver's coordinate range to the surface dimensions. jlpInputPoll // calls function 3 once per frame to read absolute X/Y and the button // bitmask. INT 33h is provided by every common DOS mouse driver and by // DOSBox / DOSBox-X. @@ -30,7 +30,7 @@ #include #include -#include "hal.h" +#include "port.h" #include "inputInternal.h" #include "joey/surface.h" @@ -323,13 +323,13 @@ static void mousePoll(void) { // ----- HAL API (alphabetical) ----- -void halJoystickReset(jlJoystickE js) { +void jlpJoystickReset(jlJoystickE js) { // DOS sticks are digital -- no calibration to do. (void)js; } -void halInputInit(void) { +void jlpInputInit(void) { memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); @@ -353,7 +353,7 @@ void halInputInit(void) { } -void halInputPoll(void) { +void jlpInputPoll(void) { disable(); memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); enable(); @@ -362,7 +362,7 @@ void halInputPoll(void) { } -void halInputShutdown(void) { +void jlpInputShutdown(void) { if (!gHooked) { return; } diff --git a/src/codegen/spriteEmitX86.c b/src/dos/spriteEmitX86.c similarity index 100% rename from src/codegen/spriteEmitX86.c rename to src/dos/spriteEmitX86.c diff --git a/src/generic/genericDraw.c b/src/generic/genericDraw.c new file mode 100644 index 0000000..a661e30 --- /dev/null +++ b/src/generic/genericDraw.c @@ -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_ 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 dispatch +// in src/core/port.h routes to either a machine override or, by default, the +// matching generic here. + +#include + +#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)); + } +} diff --git a/src/generic/genericPort.c b/src/generic/genericPort.c new file mode 100644 index 0000000..023ca45 --- /dev/null +++ b/src/generic/genericPort.c @@ -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_. A few have a +// genuine portable default (millisElapsed derives from the frame counter). +// +// See genericDraw.c for the jlp dispatch model. Always compiled in via the +// src/generic/*.c wildcard. + +#include + +#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); +} diff --git a/src/generic/genericSprite.c b/src/generic/genericSprite.c new file mode 100644 index 0000000..1bb8994 --- /dev/null +++ b/src/generic/genericSprite.c @@ -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; +} diff --git a/src/generic/genericSurface.c b/src/generic/genericSurface.c new file mode 100644 index 0000000..a0d8b97 --- /dev/null +++ b/src/generic/genericSurface.c @@ -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 dispatch +// model). A machine that does not override an op (no JL_HAS_ 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 +#include +#include + +#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; +} diff --git a/src/generic/genericTile.c b/src/generic/genericTile.c new file mode 100644 index 0000000..1fe040c --- /dev/null +++ b/src/generic/genericTile.c @@ -0,0 +1,150 @@ +// Generic portable-C tile op defaults (chunky 4bpp). The compile-time jlp +// dispatch in src/core/port.h routes to these when a machine declares no +// JL_HAS_ 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; + } +} diff --git a/src/codegen/spriteEmitStub.c b/src/generic/spriteEmitStub.c similarity index 100% rename from src/codegen/spriteEmitStub.c rename to src/generic/spriteEmitStub.c diff --git a/src/port/iigs/audio_full.c b/src/iigs/audio_full.c similarity index 94% rename from src/port/iigs/audio_full.c rename to src/iigs/audio_full.c index 2ec8a21..65487a6 100644 --- a/src/port/iigs/audio_full.c +++ b/src/iigs/audio_full.c @@ -5,7 +5,7 @@ // references they actually use. // // 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 // them through the self-modifying call stub below; NTP is bank-internal / // position-independent so it runs at whatever address Memory Manager picked. @@ -28,7 +28,7 @@ typedef void *Pointer; #define attrFixed 0x4000 #define attrLocked 0x8000 -#include "hal.h" +#include "port.h" #include "joey/audio.h" // 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 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 // unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms. // Most callers play the same SFX repeatedly into the same @@ -211,7 +211,7 @@ static bool loadNTP(void) { // ----- HAL API (alphabetical) ----- -bool halAudioInit(void) { +bool jlpAudioInit(void) { Handle modHandle; Handle sfxHandle; @@ -258,12 +258,12 @@ bool halAudioInit(void) { } -void halAudioShutdown(void) { +void jlpAudioShutdown(void) { if (!gNTPReady) { return; } if (gNTPPlaying) { - halAudioStopMod(); + jlpAudioStopMod(); } // Silence every SFX slot before disposing the handles. NTP's DOC // IRQ vector points into the buffer we are about to free; if any @@ -274,7 +274,7 @@ void halAudioShutdown(void) { { uint8_t i; for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) { - halAudioStopSfx(i); + jlpAudioStopSfx(i); gSfxSlotSample[i] = (const uint8_t *)0; gSfxSlotLength[i] = 0; gSfxSlotRateHz[i] = 0; @@ -299,12 +299,12 @@ void halAudioShutdown(void) { } -bool halAudioIsPlayingMod(void) { +bool jlpAudioIsPlayingMod(void) { 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) { 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. if (gNTPPlaying) { - halAudioStopMod(); + jlpAudioStopMod(); } 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; uint32_t slotBase; 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 // a callback would need a DOC-IRQ ring buffer, which is a separate // 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)fill; (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 // 14-21; NTP uses the lower ones). Loudness comes from the sample // 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. #define TONE_RATE 26000U // DOC playback rate for synthesized PSG #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 len; int8_t amp; @@ -485,7 +485,7 @@ void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { return; } if (freqHz == 0u || atten >= AGI_ATTEN_OFF) { - halAudioStopSfx(voice); // freq 0 / max atten = silence + jlpAudioStopSfx(voice); // freq 0 / max atten = silence return; } // 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); len = toneFill(halfPeriod, amp); // 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; - halAudioPlaySfx(voice, (const uint8_t *)gToneBuf, len, TONE_RATE); + jlpAudioPlaySfx(voice, (const uint8_t *)gToneBuf, len, TONE_RATE); } -void halAudioTone(uint16_t freqHz) { - halAudioVoice(0u, freqHz, 0u); // single full-volume tone on voice 0 +void jlpAudioTone(uint16_t freqHz) { + 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 // raw VBL ISR (which would fight GS/OS heartbeat tasks). hz > 60 clamps to // 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) { gTickFn = (jlAudioTickFnT)0; 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) { return; } @@ -543,7 +543,7 @@ void halAudioStopMod(void) { } -void halAudioStopSfx(uint8_t slot) { +void jlpAudioStopSfx(uint8_t slot) { uint8_t firstOsc; 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 - // 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. if (gTickFn != (jlAudioTickFnT)0) { gTickCount--; diff --git a/src/port/iigs/hal.c b/src/iigs/hal.c similarity index 75% rename from src/port/iigs/hal.c rename to src/iigs/hal.c index 0388e59..fc6c2d5 100644 --- a/src/port/iigs/hal.c +++ b/src/iigs/hal.c @@ -11,9 +11,9 @@ // // 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 -// 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 // 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 @@ -29,11 +29,11 @@ #include #include "joey/debug.h" -#include "hal.h" +#include "port.h" #include "surfaceInternal.h" -// Memory Manager surface for halBigAlloc / halBigFree (jlAlloc backing). -// Same idiom as src/port/iigs/audio_full.c: Handles/pointers are void*, +// Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing). +// Same idiom as src/iigs/audio_full.c: Handles/pointers are void*, // the owner ID comes from MMStartUp(), and the attribute bit values are // Apple's Memory Manager constants. Only the bits we use are defined. #include @@ -58,17 +58,16 @@ extern uint16_t iigsGetTickWord(void); extern uint16_t iigsReadHzParam(void); static uint16_t gFrameHz = 60u; -// hal.c is the single TU that calls into joeyDraw.asm. Cross- -// platform draw.c / tile.c / etc. dispatch through halFast* -// functions defined here; they never reference the asm symbols -// directly. Keeping the asm externs in a single TU avoids the -// link-time fragility seen when each cross-platform TU brought its -// own asm extern. +// hal.c is the single TU that calls into joeyDraw.asm. The IIgs jlp +// macros in core/port.h expand to these asm entry points; cross-platform +// draw.c / tile.c / etc. never reference the asm symbols directly. Keeping +// the asm externs in a single TU avoids the link-time fragility seen when +// each cross-platform TU brought its own asm extern. // 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen. extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); // 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); // 16 STA abs,X stores at fixed offsets along a 160-byte stride. // ~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. // 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, -// which is unreliable when called from halPresent (DBR-state quirk +// which is unreliable when called from jlpPresent (DBR-state quirk // after prior asm primitives). extern void iigsBlitStageToShr(const uint8_t *scbPtr, const uint16_t *palettePtr, uint16_t uploadFlags); // 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 gFloodLeftX; extern uint16_t gFloodRightX; @@ -107,12 +106,12 @@ extern uint16_t 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); // 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 // of the 7-instruction shift-add. extern void iigsInitRowLut(void); // 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; // subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs // 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), // 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 -// 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 // ----- Module state ----- @@ -196,7 +195,6 @@ volatile uint16_t gPeiMvnCount; // narrow-extent present: MVN byte coun // iigsBlitStageToShr above (with dirty-row skip). - // Upload SCB / palette into bank-$E1 SHR memory only when the // matching dirty flag is set. Replaces a per-frame 712-byte memcmp // pair (~7 ms / frame) with a 2-cyc flag check. @@ -226,7 +224,7 @@ static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { // 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. // The returned memory is not zero-filled (matches malloc semantics). -void *halBigAlloc(uint32_t bytes) { +void *jlpBigAlloc(uint32_t bytes) { Handle h; uint8_t *base; @@ -241,13 +239,13 @@ void *halBigAlloc(uint32_t bytes) { DisposeHandle(h); 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; return base + BIG_ALLOC_HDR; } -void halBigFree(void *p) { +void jlpBigFree(void *p) { Handle h; if (p == NULL) { @@ -258,7 +256,7 @@ void halBigFree(void *p) { } -bool halInit(const jlConfigT *config) { +bool jlpInit(const jlConfigT *config) { (void)config; gPreviousNewVideo = *IIGS_NEWVIDEO_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 // illusion breaks (the user would see drawing in progress). *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 - // 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. iigsInitRowLut(); gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u; @@ -280,12 +278,7 @@ bool halInit(const jlConfigT *config) { } -const char *halLastError(void) { - return NULL; -} - - -void halPresent(const jlSurfaceT *src) { +void jlpPresent(const jlSurfaceT *src) { if (src == NULL) { return; } @@ -306,7 +299,7 @@ void halPresent(const jlSurfaceT *src) { } -void halShutdown(void) { +void jlpShutdown(void) { if (gModeSet) { *IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_BORDER_REG = gPreviousBorder; @@ -316,28 +309,19 @@ void halShutdown(void) { } -// halFastSurfaceClear / halFastDrawLine / halFastDrawCircle / -// halFastFillCircle / halFastTileCopy / halFastTileCopyMasked / -// halFastTilePaste / halFastTileSnap / halFastTileFill / -// halFastFloodWalk[AndScans] / halFastFloodScanRow / -// halFastFloodScanAndPush all dispatch via macros in core/hal.h on -// 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. +// The IIgs draw/tile/flood primitives (surfaceClear, drawLine, drawCircle, +// fillCircle, fillRect, tileCopy / tileCopyMasked / tilePaste / tileSnap / +// tileFill, floodWalkAndScans) are all dispatched via the jlp macros in +// core/port.h (#ifdef JOEYLIB_PLATFORM_IIGS block); they expand to the +// iigs*Inner asm entry points above, so no C wrapper functions live here. -// halFastFillRect: macro-dispatched in core/hal.h, same as the other -// 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) { +uint8_t *jlpStageAllocPixels(void) { return IIGS_STAGE_PIXELS; } -void halStageFreePixels(uint8_t *pixels) { +void jlpStageFreePixels(uint8_t *pixels) { (void)pixels; // 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` // buffer at $01:2000 is the stage's pixel storage and the source for // 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 -// 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. /* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like * 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 // 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 // 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) { /* already in VBL: wait for active scan */; } @@ -460,17 +364,17 @@ void halWaitVBL(void) { // than `gFrameCount++` because the post-increment can lower to // `inc |gFrameCount` (the only INC abs form on 65816 -- there is no // 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 // byte and the counter never advances. The explicit lda > / sta > // pattern uses long-mode addressing throughout, which is // DBR-independent. -uint16_t halFrameCount(void) { +uint16_t jlpFrameCount(void) { return iigsGetTickWord(); } -uint16_t halFrameHz(void) { +uint16_t jlpFrameHz(void) { return gFrameHz; } @@ -480,6 +384,3 @@ uint16_t halFrameHz(void) { // gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so // callers tracking deltas should use this for short intervals only // (sound event scheduling is fine). -uint32_t halMillisElapsed(void) { - return (uint32_t)((uint32_t)halFrameCount() * 1000u / (uint32_t)gFrameHz); -} diff --git a/src/port/iigs/input.c b/src/iigs/input.c similarity index 97% rename from src/port/iigs/input.c rename to src/iigs/input.c index 57006f5..9504e4a 100644 --- a/src/port/iigs/input.c +++ b/src/iigs/input.c @@ -16,7 +16,7 @@ // // Release detection uses the IIe-inherited "any key currently down" // 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 // non-modifier key is physically held, and we wholesale-clear // gKeyState. readModifierKeys then re-asserts the modifiers from @@ -28,7 +28,7 @@ // 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 // 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 // IIgs ADB MCU autopolls the mouse and queues fifos behind these // softswitches, so the per-frame two-read cadence keeps up with @@ -36,7 +36,7 @@ #include -#include "hal.h" +#include "port.h" #include "inputInternal.h" #include "joey/surface.h" @@ -73,7 +73,7 @@ // 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 -// strobe can't spin halInputPoll forever. +// strobe can't spin jlpInputPoll forever. #define KBD_DRAIN_GUARD 32u // $C025 layout (IIgs Hardware Reference): bit 0 = shift, bit 1 = ctrl, @@ -116,7 +116,7 @@ static int8_t thresholdPaddle(uint8_t v); // ----- 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. static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; @@ -244,7 +244,7 @@ static bool gJoyDisconnectLatched = false; // captured the last time the user called jlJoystickReset. Until // that's called, gJoyCenterValid is false and pollJoystick falls back // 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. // uint8_t (not bool) so the per-element stride is a known 1 byte. // Storage is still 0 or 1 either way. @@ -254,7 +254,7 @@ static uint8_t gJoyCenterValid [JOYSTICK_COUNT]; static uint8_t gJoyRecalibrate [JOYSTICK_COUNT]; -void halJoystickReset(jlJoystickE js) { +void jlpJoystickReset(jlJoystickE js) { if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) { return; } @@ -410,7 +410,7 @@ static void pollMouse(void) { // ----- HAL API (alphabetical) ----- -void halInputInit(void) { +void jlpInputInit(void) { memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); buildAsciiTable(); @@ -425,7 +425,7 @@ void halInputInit(void) { } -void halInputPoll(void) { +void jlpInputPoll(void) { uint8_t kbd; uint8_t ascii; uint8_t key; @@ -479,6 +479,6 @@ void halInputPoll(void) { } -void halInputShutdown(void) { +void jlpInputShutdown(void) { (void)*IIGS_KBDSTRB; } diff --git a/src/port/iigs/joeyDraw.s b/src/iigs/joeyDraw.s similarity index 99% rename from src/port/iigs/joeyDraw.s rename to src/iigs/joeyDraw.s index a1e3fa8..adf2b86 100644 --- a/src/port/iigs/joeyDraw.s +++ b/src/iigs/joeyDraw.s @@ -30,7 +30,7 @@ ; void iigsInitRowLut(void) ; ; 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 ; 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 ; stage region ($2000..$9CFF); AUXWRITE ($C005) redirects the bank-$00 ; 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. -; 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 ; `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 diff --git a/src/port/iigs/peislam.s b/src/iigs/peislam.s similarity index 94% rename from src/port/iigs/peislam.s rename to src/iigs/peislam.s index 825b32b..2dcfbf7 100644 --- a/src/port/iigs/peislam.s +++ b/src/iigs/peislam.s @@ -41,9 +41,9 @@ iigsGetTickWord: ; 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 -; 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 -; (frames * 1000 / halFrameHz) is correct on both. +; (frames * 1000 / jlpFrameHz) is correct on both. ; ---------------------------------------------------------------- .section .text.iigsReadHzParam,"ax" .globl iigsReadHzParam diff --git a/src/codegen/spriteEmitIigs.c b/src/iigs/spriteEmitIigs.c similarity index 100% rename from src/codegen/spriteEmitIigs.c rename to src/iigs/spriteEmitIigs.c diff --git a/src/codegen/spriteEmit68k.c b/src/m68k/spriteEmit68k.c similarity index 100% rename from src/codegen/spriteEmit68k.c rename to src/m68k/spriteEmit68k.c diff --git a/src/m68k/spriteEmitInterleaved68k.c b/src/m68k/spriteEmitInterleaved68k.c new file mode 100644 index 0000000..23f0b74 --- /dev/null +++ b/src/m68k/spriteEmitInterleaved68k.c @@ -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; +} diff --git a/src/m68k/spriteEmitPlanar68k.c b/src/m68k/spriteEmitPlanar68k.c new file mode 100644 index 0000000..60bed94 --- /dev/null +++ b/src/m68k/spriteEmitPlanar68k.c @@ -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; +} diff --git a/src/shared68k/surface68k.s b/src/m68k/surface68k.s similarity index 98% rename from src/shared68k/surface68k.s rename to src/m68k/surface68k.s index 3de9840..e566170 100644 --- a/src/shared68k/surface68k.s +++ b/src/m68k/surface68k.s @@ -26,15 +26,15 @@ | uint8_t fb2, uint8_t fb3); | | 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 | side). leftMask and rightMask are the partial-byte masks for the | 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 | (colorIndex >> N) & 1 -> 0xFF or 0x00). | -| Used by Amiga halFastFillCircle (one call per scanline span) and -| Amiga halFillRectPlanes (one call per row of the rect). Replaces +| Used by Amiga jlpFillCircle (one call per scanline span) and +| Amiga jlpFillRect (one call per row of the rect). Replaces | the C inner loop whose ~13 cyc/byte was the gating cost on | jlFillCircle r=40 even after C-side inlining. |