commit b3d9961e78a39c56de25af6c237daddb3ffddaf9 Author: Scott Duensing Date: Fri Apr 24 15:30:13 2026 -0500 Basic rendering working. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f723ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Build outputs +build/ +*.o +*.obj +*.a +*.lib +*.exe +*.prg +*.tos +*.sys16 +*.gs +*.bin + +# Toolchain installations (everything fetched/built by install.sh) +toolchains/iigs/* +toolchains/amiga/* +toolchains/atarist/* +toolchains/dos/* +toolchains/emulators/* +toolchains/.install_state/* +toolchains/cache/* + +# Disk images +*.adf +*.hdf +*.st +*.2mg +*.po +*.img +*.dsk + +# Editor / OS cruft +*.swp +*.swo +*~ +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Generated asset blobs (if running tools locally) +*.SUR +*.TIL +*.SPR +*.PAL +*.MAP +*.FNT +*.SND +*.ANI + +# Crap I added +stuff/* +docs/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d32132f --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +# JoeyLib top-level Makefile. +# +# Usage: +# source toolchains/env.sh (first, every shell session) +# make builds every target whose toolchain is present +# make iigs builds Apple IIgs only +# make amiga builds Commodore Amiga only +# make atarist builds Atari ST only +# make dos builds MS-DOS only +# make clean removes all build outputs +# +# The build references cross tools exclusively from toolchains/, set up +# by sourcing toolchains/env.sh. Do not invoke make without sourcing +# env.sh first or builds will fail with missing tool errors. + +REPO_DIR := $(abspath $(dir $(firstword $(MAKEFILE_LIST)))) + +# Detect which toolchains are actually available. +HAVE_IIGS := $(shell [ -x "$(IIGS_CC)" ] && echo 1) +HAVE_AMIGA := $(shell [ -x "$(AMIGA_CC)" ] && echo 1) +HAVE_ATARIST := $(shell [ -x "$(ST_CC)" ] && echo 1) +HAVE_DOS := $(shell [ -x "$(DOS_CC)" ] && echo 1) + +ALL_TARGETS := +ifeq ($(HAVE_IIGS),1) + ALL_TARGETS += iigs +endif +ifeq ($(HAVE_AMIGA),1) + ALL_TARGETS += amiga +endif +ifeq ($(HAVE_ATARIST),1) + ALL_TARGETS += atarist +endif +ifeq ($(HAVE_DOS),1) + ALL_TARGETS += dos +endif + +.PHONY: all iigs iigs-disk amiga atarist dos clean help status + +all: $(ALL_TARGETS) +ifeq ($(ALL_TARGETS),) + @echo "No toolchains detected. Run ./toolchains/install.sh and source toolchains/env.sh." + @exit 1 +endif + +iigs: + @$(MAKE) -f $(REPO_DIR)/make/iigs.mk + +iigs-disk: + @$(MAKE) -f $(REPO_DIR)/make/iigs.mk iigs-disk + +amiga: + @$(MAKE) -f $(REPO_DIR)/make/amiga.mk + +atarist: + @$(MAKE) -f $(REPO_DIR)/make/atarist.mk + +dos: + @$(MAKE) -f $(REPO_DIR)/make/dos.mk + +clean: + rm -rf $(REPO_DIR)/build + +status: + @echo "Toolchain availability:" + @echo " iigs: $(if $(HAVE_IIGS),yes,NO)" + @echo " amiga: $(if $(HAVE_AMIGA),yes,NO)" + @echo " atarist: $(if $(HAVE_ATARIST),yes,NO)" + @echo " dos: $(if $(HAVE_DOS),yes,NO)" + +help: + @echo "Targets: iigs amiga atarist dos all clean status help" + @echo "Source toolchains/env.sh first." diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f54bd4 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# JoeyLib + +A unified C game-development library targeting four early 16-bit +platforms from a single codebase: + +- Apple IIgs (reference platform) +- Commodore Amiga (A500 / 68000 baseline) +- Atari ST (STF / 68000 baseline) +- MS-DOS (386 / VGA, DJGPP) + +The Apple IIgs defines the capability ceiling. Stronger platforms coast. +Hot paths are hand-written assembly per port; the public API is C. + +See `docs/DESIGN.md` for the full 1.0 design. + + +## Quick start + +``` +git clone joeylib +cd joeylib +./toolchains/install.sh +# follow any non-free-tool placement instructions the script prints +source toolchains/env.sh +make +``` + +This builds `libjoey.a` for every target whose toolchain is installed, +plus the `examples/hello` program for each. + + +## Building for a single target + +``` +source toolchains/env.sh +make iigs +make amiga +make atarist +make dos +``` + + +## Repository layout + +``` +docs/ design and reference documentation +include/joey/ public headers +src/core/ portable library code +src/codegen/ runtime sprite codegen (per-CPU emitters) +src/port// per-platform HAL implementations +src/shared68k/ assembly shared by Amiga and Atari ST +tools/joeytool/ asset pipeline and packaging +examples/ example programs +toolchains/ self-contained cross-build tools +make/ per-target Makefile fragments +build// per-target build outputs +``` + + +## License + +TBD. diff --git a/examples/hello/hello.c b/examples/hello/hello.c new file mode 100644 index 0000000..9d847d6 --- /dev/null +++ b/examples/hello/hello.c @@ -0,0 +1,29 @@ +// JoeyLib hello-world example. +// +// Validates that the C API headers, library, and per-platform link +// path all work end-to-end. Runs in HOST_MODE_OS so it can use stdio. + +#include +#include + +int main(void) { + JoeyConfigT config; + + config.hostMode = HOST_MODE_OS; + config.codegenBytes = 32 * 1024; + config.maxSurfaces = 4; + config.audioBytes = 64 * 1024; + config.assetBytes = 128 * 1024; + + if (!joeyInit(&config)) { + fprintf(stderr, "joeyInit failed: %s\n", joeyLastError()); + return 1; + } + + printf("JoeyLib %s\n", joeyVersionString()); + printf("Platform: %s\n", joeyPlatformName()); + printf("Hello from JoeyLib.\n"); + + joeyShutdown(); + return 0; +} diff --git a/examples/pattern/pattern.c b/examples/pattern/pattern.c new file mode 100644 index 0000000..8bdb3fb --- /dev/null +++ b/examples/pattern/pattern.c @@ -0,0 +1,147 @@ +// M1 deliverable: visible test pattern exercising surfaces, palettes, +// SCBs, fillRect, and present. +// +// Screen is divided into 8 horizontal bands, each assigned its own +// palette. The pattern draws 16 vertical color-index stripes across +// the entire height; as a result each band displays its palette's +// gradient. Color index 0 in every palette is forced to black by the +// library contract, so the leftmost stripe is black in every band. + +#include +#include + +#include + +#define BAND_COUNT 8 +#define BAND_HEIGHT (SURFACE_HEIGHT / BAND_COUNT) +#define STRIPE_COUNT 16 +#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT) +#define DISPLAY_SECONDS 5 + +static void buildPalettes(SurfaceT *screen); +static void buildScbs(SurfaceT *screen); +static void drawStripes(SurfaceT *screen); +static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn); +static void waitSeconds(int seconds); + + +static void buildPalettes(SurfaceT *screen) { + uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + + // Palette 0: grayscale + makeGradient(colors, 1, 1, 1); + paletteSet(screen, 0, colors); + + // Palette 1: red + makeGradient(colors, 1, 0, 0); + paletteSet(screen, 1, colors); + + // Palette 2: yellow + makeGradient(colors, 1, 1, 0); + paletteSet(screen, 2, colors); + + // Palette 3: green + makeGradient(colors, 0, 1, 0); + paletteSet(screen, 3, colors); + + // Palette 4: cyan + makeGradient(colors, 0, 1, 1); + paletteSet(screen, 4, colors); + + // Palette 5: blue + makeGradient(colors, 0, 0, 1); + paletteSet(screen, 5, colors); + + // Palette 6: magenta + makeGradient(colors, 1, 0, 1); + paletteSet(screen, 6, colors); + + // Palette 7: white-only (same as grayscale but serves as sanity check) + makeGradient(colors, 1, 1, 1); + paletteSet(screen, 7, colors); +} + + +static void buildScbs(SurfaceT *screen) { + uint16_t band; + uint16_t first; + uint16_t last; + + for (band = 0; band < BAND_COUNT; band++) { + first = (uint16_t)(band * BAND_HEIGHT); + last = (uint16_t)(first + BAND_HEIGHT - 1); + scbSetRange(screen, first, last, (uint8_t)band); + } +} + + +static void drawStripes(SurfaceT *screen) { + uint8_t colorIndex; + int16_t x; + + surfaceClear(screen, 0); + for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) { + x = (int16_t)(colorIndex * STRIPE_WIDTH); + fillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex); + } +} + + +static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) { + uint8_t i; + uint8_t r; + uint8_t g; + uint8_t b; + + for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + r = (uint8_t)(redOn ? i : 0); + g = (uint8_t)(greenOn ? i : 0); + b = (uint8_t)(blueOn ? i : 0); + out16[i] = (uint16_t)((r << 8) | (g << 4) | b); + } +} + + +static void waitSeconds(int seconds) { + time_t start; + time_t now; + + start = time(NULL); + do { + now = time(NULL); + } while ((long)(now - start) < (long)seconds); +} + + +int main(void) { + JoeyConfigT config; + SurfaceT *screen; + + config.hostMode = HOST_MODE_TAKEOVER; + config.codegenBytes = 32 * 1024; + config.maxSurfaces = 4; + config.audioBytes = 64 * 1024; + config.assetBytes = 128 * 1024; + + if (!joeyInit(&config)) { + fprintf(stderr, "joeyInit failed: %s\n", joeyLastError()); + return 1; + } + + screen = surfaceGetScreen(); + if (screen == NULL) { + fprintf(stderr, "surfaceGetScreen returned NULL\n"); + joeyShutdown(); + return 1; + } + + buildPalettes(screen); + buildScbs(screen); + drawStripes(screen); + surfacePresent(screen); + + waitSeconds(DISPLAY_SECONDS); + + joeyShutdown(); + return 0; +} diff --git a/include/joey/core.h b/include/joey/core.h new file mode 100644 index 0000000..ec7a009 --- /dev/null +++ b/include/joey/core.h @@ -0,0 +1,33 @@ +// JoeyLib lifecycle: configuration, initialization, shutdown. + +#ifndef JOEYLIB_CORE_H +#define JOEYLIB_CORE_H + +#include "platform.h" +#include "types.h" + +typedef struct { + HostModeE hostMode; // takeover or cooperate with host OS + uint32_t codegenBytes; // runtime compiled-sprite cache size + uint16_t maxSurfaces; // maximum concurrent surfaces + uint32_t audioBytes; // audio sample and module RAM pool + uint32_t assetBytes; // tileset / sprite / map RAM pool +} JoeyConfigT; + +// Initialize the library. Returns true on success. +// On failure, joeyLastError() returns a human-readable description. +bool joeyInit(const JoeyConfigT *config); + +// Shut down the library, releasing all resources. +void joeyShutdown(void); + +// Returns the most recent error message, or NULL if none. +const char *joeyLastError(void); + +// Returns the platform identifier string (e.g., "Apple IIgs", "MS-DOS"). +const char *joeyPlatformName(void); + +// Returns the library version string (e.g., "1.0.0"). +const char *joeyVersionString(void); + +#endif diff --git a/include/joey/draw.h b/include/joey/draw.h new file mode 100644 index 0000000..d0d13df --- /dev/null +++ b/include/joey/draw.h @@ -0,0 +1,28 @@ +// Drawing primitives. +// +// All primitives clip to the surface rectangle. Out-of-bounds draws are +// silent no-ops, not errors. samplePixel returns 0 for off-surface reads. +// Primitives operate on color indices; per-scanline palette resolution +// happens only at display time. + +#ifndef JOEYLIB_DRAW_H +#define JOEYLIB_DRAW_H + +#include "platform.h" +#include "surface.h" +#include "types.h" + +// Fill the entire surface with a single color index. Writes color 0 is +// a legitimate clear-to-black and does not skip. +void surfaceClear(SurfaceT *s, uint8_t colorIndex); + +// Plot a single pixel. Off-surface coordinates are no-ops. +void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex); + +// Read a pixel value. Off-surface coordinates return 0. +uint8_t samplePixel(const SurfaceT *s, int16_t x, int16_t y); + +// Fill a solid rectangle. Negative or zero dimensions are no-ops. +void fillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex); + +#endif diff --git a/include/joey/joey.h b/include/joey/joey.h new file mode 100644 index 0000000..e3f18fa --- /dev/null +++ b/include/joey/joey.h @@ -0,0 +1,17 @@ +// JoeyLib umbrella header. +// +// Game code includes this single file: +// #include + +#ifndef JOEYLIB_H +#define JOEYLIB_H + +#include "platform.h" +#include "types.h" +#include "core.h" +#include "surface.h" +#include "palette.h" +#include "draw.h" +#include "present.h" + +#endif diff --git a/include/joey/palette.h b/include/joey/palette.h new file mode 100644 index 0000000..ef7a4ef --- /dev/null +++ b/include/joey/palette.h @@ -0,0 +1,31 @@ +// Palette and SCB (scanline control byte) accessors. +// +// Colors are 12-bit $0RGB (bits 11..8 red, 7..4 green, 3..0 blue). +// The library forces color 0 of every palette to black; paletteSet +// silently masks that entry to $000 regardless of what the caller +// passed. See docs/DESIGN.md sections 5 and 10. + +#ifndef JOEYLIB_PALETTE_H +#define JOEYLIB_PALETTE_H + +#include "platform.h" +#include "surface.h" +#include "types.h" + +// Load all 16 colors of a palette. colors16 must point to exactly 16 +// uint16_t values in $0RGB format. +void paletteSet(SurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16); + +// Read all 16 colors of a palette into out16. +void paletteGet(const SurfaceT *s, uint8_t paletteIndex, uint16_t *out16); + +// Assign a single palette index (0..15) to a single scanline (0..199). +void scbSet(SurfaceT *s, uint16_t line, uint8_t paletteIndex); + +// Assign a palette index to an inclusive range of scanlines. +void scbSetRange(SurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t paletteIndex); + +// Read the palette index assigned to a scanline. +uint8_t scbGet(const SurfaceT *s, uint16_t line); + +#endif diff --git a/include/joey/platform.h b/include/joey/platform.h new file mode 100644 index 0000000..d33146f --- /dev/null +++ b/include/joey/platform.h @@ -0,0 +1,73 @@ +// JoeyLib platform, CPU, endian, and capability defines. +// +// Exactly one JOEYLIB_PLATFORM_* must be defined per build. The build +// system normally sets this via -D; if absent, the header attempts to +// auto-detect from compiler-predefined macros. + +#ifndef JOEYLIB_PLATFORM_H +#define JOEYLIB_PLATFORM_H + +// ----- Auto-detect platform from compiler macros if not preset ----- + +#if !defined(JOEYLIB_PLATFORM_IIGS) && \ + !defined(JOEYLIB_PLATFORM_AMIGA) && \ + !defined(JOEYLIB_PLATFORM_ATARIST) && \ + !defined(JOEYLIB_PLATFORM_DOS) + + #if defined(__DJGPP__) || defined(__MSDOS__) + #define JOEYLIB_PLATFORM_DOS + #elif defined(__amigaos__) || defined(AMIGA) || defined(__amigaos4__) + #define JOEYLIB_PLATFORM_AMIGA + #elif defined(__atarist__) || defined(__MINT__) || defined(__TOS__) + #define JOEYLIB_PLATFORM_ATARIST + #elif defined(__ORCAC__) || defined(__APPLE2GS__) || defined(__GNO__) + #define JOEYLIB_PLATFORM_IIGS + #else + #error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_ explicitly via -D" + #endif + +#endif + +// ----- Validate exactly one platform is defined ----- + +#if (defined(JOEYLIB_PLATFORM_IIGS) + \ + defined(JOEYLIB_PLATFORM_AMIGA) + \ + defined(JOEYLIB_PLATFORM_ATARIST) + \ + defined(JOEYLIB_PLATFORM_DOS)) != 1 + #error "JoeyLib: exactly one JOEYLIB_PLATFORM_* must be defined" +#endif + +// ----- Derived CPU markers ----- + +#if defined(JOEYLIB_PLATFORM_IIGS) + #define JOEYLIB_CPU_65816 + #define JOEYLIB_ENDIAN_LITTLE + #define JOEYLIB_NATIVE_CHUNKY + #define JOEYLIB_PLATFORM_NAME "Apple IIgs" +#elif defined(JOEYLIB_PLATFORM_AMIGA) + #define JOEYLIB_CPU_68000 + #define JOEYLIB_ENDIAN_BIG + #define JOEYLIB_NATIVE_PLANAR + #define JOEYLIB_HAS_BLITTER + #define JOEYLIB_HAS_COPPER + #define JOEYLIB_PLATFORM_NAME "Commodore Amiga" +#elif defined(JOEYLIB_PLATFORM_ATARIST) + #define JOEYLIB_CPU_68000 + #define JOEYLIB_ENDIAN_BIG + #define JOEYLIB_NATIVE_PLANAR + #define JOEYLIB_PLATFORM_NAME "Atari ST" +#elif defined(JOEYLIB_PLATFORM_DOS) + #define JOEYLIB_CPU_X86 + #define JOEYLIB_ENDIAN_LITTLE + #define JOEYLIB_NATIVE_CHUNKY + #define JOEYLIB_PLATFORM_NAME "MS-DOS" +#endif + +// ----- Library version ----- + +#define JOEYLIB_VERSION_MAJOR 1 +#define JOEYLIB_VERSION_MINOR 0 +#define JOEYLIB_VERSION_PATCH 0 +#define JOEYLIB_VERSION_STRING "1.0.0" + +#endif diff --git a/include/joey/present.h b/include/joey/present.h new file mode 100644 index 0000000..7e64687 --- /dev/null +++ b/include/joey/present.h @@ -0,0 +1,23 @@ +// Present / slam. +// +// surfacePresent copies pixels, SCBs, and palettes from a source +// surface to the visible display. On chunky platforms (IIgs, DOS) this +// is a direct copy; on planar platforms (Amiga, Atari ST) this is a +// chunky-to-planar conversion. See docs/DESIGN.md section 7. + +#ifndef JOEYLIB_PRESENT_H +#define JOEYLIB_PRESENT_H + +#include "platform.h" +#include "surface.h" +#include "types.h" + +// Present the entire source surface to the display. +void surfacePresent(const SurfaceT *src); + +// Present a rectangular region of the source surface to the display. +// The rect is clipped to the surface. Negative or zero dimensions are +// no-ops. +void surfacePresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h); + +#endif diff --git a/include/joey/surface.h b/include/joey/surface.h new file mode 100644 index 0000000..2b1db07 --- /dev/null +++ b/include/joey/surface.h @@ -0,0 +1,46 @@ +// Surface type and allocation. +// +// All surfaces are 320x200 pixels, 4 bits per pixel packed (two pixels +// per byte, high nibble is the left pixel). Each surface carries its +// own 200-entry SCB (scanline control byte) table and a 16-by-16 $0RGB +// palette set. See docs/DESIGN.md section 6. + +#ifndef JOEYLIB_SURFACE_H +#define JOEYLIB_SURFACE_H + +#include "platform.h" +#include "types.h" + +// ----- Fixed surface dimensions (library contract) ----- + +#define SURFACE_WIDTH 320 +#define SURFACE_HEIGHT 200 +#define SURFACE_BYTES_PER_ROW 160 +#define SURFACE_PIXELS_SIZE (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT) + +#define SURFACE_PALETTE_COUNT 16 +#define SURFACE_COLORS_PER_PALETTE 16 +#define SURFACE_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE) + +// ----- Opaque public type ----- + +typedef struct SurfaceT SurfaceT; + +// Allocate a new offscreen surface. Returns NULL on failure (joeyLastError +// describes the reason). +SurfaceT *surfaceCreate(void); + +// Release an offscreen surface previously returned by surfaceCreate. +// Passing NULL is a no-op. Passing the screen surface is a no-op. +void surfaceDestroy(SurfaceT *s); + +// The library's pre-allocated screen surface. This is the surface the +// library presents to the display. Always valid between joeyInit and +// joeyShutdown. +SurfaceT *surfaceGetScreen(void); + +// Copy pixels, SCBs, and palettes from src into dst. Both must be valid +// surfaces. +void surfaceCopy(SurfaceT *dst, const SurfaceT *src); + +#endif diff --git a/include/joey/types.h b/include/joey/types.h new file mode 100644 index 0000000..a1a40bb --- /dev/null +++ b/include/joey/types.h @@ -0,0 +1,20 @@ +// JoeyLib shared types and enums. + +#ifndef JOEYLIB_TYPES_H +#define JOEYLIB_TYPES_H + +#include +#include + +typedef enum { + HOST_MODE_TAKEOVER, + HOST_MODE_OS +} HostModeE; + +typedef enum { + VIDEO_REGION_NTSC, + VIDEO_REGION_PAL, + VIDEO_REGION_VGA +} VideoRegionE; + +#endif diff --git a/make/amiga.mk b/make/amiga.mk new file mode 100644 index 0000000..74c4b15 --- /dev/null +++ b/make/amiga.mk @@ -0,0 +1,67 @@ +# Amiga (Bebbo m68k-amigaos-gcc) build rules. + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +PLATFORM := amiga +BUILD := $(REPO_DIR)/build/$(PLATFORM) +LIBDIR := $(BUILD)/lib +BINDIR := $(BUILD)/bin + +CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA -m68000 -fomit-frame-pointer -noixemul +ASFLAGS := -Felf -m68000 -quiet +# --allow-multiple-definition lets our user-space tzset stub +# (src/port/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) +SHARED_S := $(wildcard $(SRC_68K)/*.s) + +LIB_OBJS := \ + $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ + $(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_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) + +LIB := $(LIBDIR)/libjoey.a + +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/Hello +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/Pattern + +.PHONY: all amiga clean-amiga +all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) + +$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_PORT)/amiga/%.c + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_PORT)/amiga/%.s + @mkdir -p $(dir $@) + $(AMIGA_AS) $(ASFLAGS) $< -o $@ + +$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s + @mkdir -p $(dir $@) + $(AMIGA_AS) $(ASFLAGS) $< -o $@ + +$(LIB): $(LIB_OBJS) + @mkdir -p $(dir $@) + $(AMIGA_AR) rcs $@ $^ + +$(HELLO_BIN): $(HELLO_SRC) $(LIB) + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + +$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + +clean-amiga: + rm -rf $(BUILD) diff --git a/make/atarist.mk b/make/atarist.mk new file mode 100644 index 0000000..b0f7c9f --- /dev/null +++ b/make/atarist.mk @@ -0,0 +1,63 @@ +# Atari ST (m68k-atari-mint-gcc) build rules. + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +PLATFORM := atarist +BUILD := $(REPO_DIR)/build/$(PLATFORM) +LIBDIR := $(BUILD)/lib +BINDIR := $(BUILD)/bin + +CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST -m68000 -fomit-frame-pointer +ASFLAGS := -Felf -m68000 -quiet +LDFLAGS := + +PORT_C_SRCS := $(wildcard $(SRC_PORT)/atarist/*.c) +PORT_S_SRCS := $(wildcard $(SRC_PORT)/atarist/*.s) +SHARED_S := $(wildcard $(SRC_68K)/*.s) + +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_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) + +LIB := $(LIBDIR)/libjoey.a + +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/HELLO.PRG +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/PATTERN.PRG + +.PHONY: all atarist clean-atarist +all atarist: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) + +$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_PORT)/atarist/%.c + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_PORT)/atarist/%.s + @mkdir -p $(dir $@) + $(ST_AS) $(ASFLAGS) $< -o $@ + +$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s + @mkdir -p $(dir $@) + $(ST_AS) $(ASFLAGS) $< -o $@ + +$(LIB): $(LIB_OBJS) + @mkdir -p $(dir $@) + $(ST_AR) rcs $@ $^ + +$(HELLO_BIN): $(HELLO_SRC) $(LIB) + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + +$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + +clean-atarist: + rm -rf $(BUILD) diff --git a/make/common.mk b/make/common.mk new file mode 100644 index 0000000..5c76c7f --- /dev/null +++ b/make/common.mk @@ -0,0 +1,18 @@ +# Shared variables used by per-target Makefile fragments. +# +# Source this only via the per-target make file (make/iigs.mk etc). + +REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..) +INCLUDE_DIR := $(REPO_DIR)/include +SRC_CORE := $(REPO_DIR)/src/core +SRC_PORT := $(REPO_DIR)/src/port +SRC_CG := $(REPO_DIR)/src/codegen +SRC_68K := $(REPO_DIR)/src/shared68k +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 +# directly because SRC_CORE is in the include path. +COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -O2 diff --git a/make/dos.mk b/make/dos.mk new file mode 100644 index 0000000..3e85447 --- /dev/null +++ b/make/dos.mk @@ -0,0 +1,59 @@ +# DOS (DJGPP) build rules. + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +PLATFORM := dos +BUILD := $(REPO_DIR)/build/$(PLATFORM) +LIBDIR := $(BUILD)/lib +BINDIR := $(BUILD)/bin + +CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_DOS -march=i386 -m32 +ASFLAGS := -f coff +LDFLAGS := + +PORT_C_SRCS := $(wildcard $(SRC_PORT)/dos/*.c) +PORT_S_SRCS := $(wildcard $(SRC_PORT)/dos/*.asm) + +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)) + +LIB := $(LIBDIR)/libjoey.a + +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/HELLO.EXE +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/PATTERN.EXE + +.PHONY: all dos clean-dos +all dos: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) + +$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_PORT)/dos/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_PORT)/dos/%.asm + @mkdir -p $(dir $@) + $(DOS_AS) $(ASFLAGS) $< -o $@ + +$(LIB): $(LIB_OBJS) + @mkdir -p $(dir $@) + $(DOS_AR) rcs $@ $^ + +$(HELLO_BIN): $(HELLO_SRC) $(LIB) + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ + $(DOS_EMBED_DPMI) $@ + +$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ + $(DOS_EMBED_DPMI) $@ + +clean-dos: + rm -rf $(BUILD) diff --git a/make/iigs.mk b/make/iigs.mk new file mode 100644 index 0000000..cf6f416 --- /dev/null +++ b/make/iigs.mk @@ -0,0 +1,60 @@ +# Apple IIgs build rules. +# +# Uses GoldenGate's iix to drive ORCA/C 2.1.0 + ORCA Linker. The +# toolchains/iigs/iix-build.sh wrapper handles ORCA's case-sensitivity +# quirks (lowercase .a/.root/.sym vs linker demands for uppercase), +# multi-source compile/link, and pragma injection for include paths +# and the stdint/stdbool shim headers. + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +PLATFORM := iigs +BUILD := $(REPO_DIR)/build/$(PLATFORM) +BINDIR := $(BUILD)/bin + +PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c) + +LIB_SRCS := $(CORE_C_SRCS) $(PORT_C_SRCS) + +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/HELLO +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/PATTERN +DISK_IMG := $(BINDIR)/joey.2mg + +IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh + +IIX_INCLUDES := \ + -I $(IIGS_INCLUDE_SHIM) \ + -I $(INCLUDE_DIR) \ + -I $(INCLUDE_DIR)/joey \ + -I $(SRC_CORE) + +.PHONY: all iigs iigs-disk clean-iigs +all iigs: $(HELLO_BIN) $(PATTERN_BIN) + +# iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with +# #pragma noroot). The example source supplies main(); libjoey sources +# are the extras. The chtyp post-step tags the output as GS/OS S16 +# ($B3) so GS/OS recognizes it as launchable; the file-type lives in +# a user.com.apple.FinderInfo xattr that iix and profuse preserve. +$(HELLO_BIN): $(HELLO_SRC) $(LIB_SRCS) $(IIGS_BUILD) + @mkdir -p $(dir $@) + $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(HELLO_SRC) $(LIB_SRCS) + $(IIGS_IIX) chtyp -t S16 $@ + +$(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_BUILD) + @mkdir -p $(dir $@) + $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS) + $(IIGS_IIX) chtyp -t S16 $@ + +# Assemble an 800KB ProDOS 2img containing both examples, ready to +# mount in GSplus alongside a GS/OS boot volume. +iigs-disk: $(DISK_IMG) + +$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(IIGS_PACKAGE) + @mkdir -p $(dir $@) + $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) + +clean-iigs: + rm -rf $(BUILD) diff --git a/scripts/run-amiga.sh b/scripts/run-amiga.sh new file mode 100755 index 0000000..3dfc63c --- /dev/null +++ b/scripts/run-amiga.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Launch the built Amiga example in FS-UAE. Defaults to PATTERN; pass +# "hello" to run HELLO instead. +# +# Kickstart and Workbench: +# - If toolchains/emulators/support/kickstart.rom is present, it is +# used as the Kickstart ROM. Otherwise FS-UAE's built-in AROS ROM +# is used (less compatible with OCS Intuition but requires no +# licensed files). +# - If toolchains/emulators/support/workbench.adf is present, it is +# inserted into DF0: and the Amiga boots Workbench. The user +# launches the example from the JOEYLIB drawer on the desktop. +# - If Workbench is not present, AmigaDOS boots directly from DH0:, +# executes s/startup-sequence, and auto-runs the requested example. +# +# Always on A500 hardware (OCS chipset). Our HAL only uses OCS +# features so this matches real-hardware behavior. +# +# scripts/run-amiga.sh # runs Pattern +# scripts/run-amiga.sh hello # runs Hello + +set -euo pipefail + +prog=${1:-pattern} +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +bin_dir=$repo/build/amiga/bin +support=$repo/toolchains/emulators/support + +case $prog in + hello) file=Hello ;; + pattern) file=Pattern ;; + *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; +esac + +if [[ ! -f "$bin_dir/$file" ]]; then + echo "$bin_dir/$file not built. Run 'make amiga' first." >&2 + exit 1 +fi + +kickstart=$support/kickstart.rom +workbench=$support/workbench.adf + +# Stage a fresh hard-drive directory containing both example binaries +# plus an s/startup-sequence that auto-invokes the chosen one. The +# startup-sequence only runs when booting directly from the hard +# drive (i.e. no Workbench floppy); Workbench takes over boot if +# present and the user launches manually. +work=$(mktemp -d -t joeylib-amiga.XXXXXX) +# Preserve any diagnostic dumps Pattern writes to the virtual HD +# (copper.txt, etc.) before the temp dir is removed on script exit. +dump_keep=/tmp/joeylib-amiga-dump +trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; rm -rf "$work"' EXIT + +mkdir -p "$work/s" +cp "$bin_dir/Hello" "$work/" 2>/dev/null || true +cp "$bin_dir/Pattern" "$work/" 2>/dev/null || true +# ':' prefix anchors to the root of the current volume; otherwise +# AmigaDOS looks in C: and the command is not found. +echo ":$file" > "$work/s/startup-sequence" + +fsargs=( + --amiga_model=A500 + --fast_memory=2048 + --hard_drive_0="$work" + --hard_drive_0_label=JOEYLIB + # Mouse-handling overrides for VM hosts where FS-UAE's default + # relative-mode grab fights with the hypervisor's own pointer + # integration. automatic_input_grab=0 stops FS-UAE from grabbing + # the pointer on focus; middle_click_ungrab=1 is a fallback so a + # middle-click releases any grab that does occur; mouse_integration=1 + # asks FS-UAE to track host pointer position directly instead of + # sampling relative deltas. + --automatic_input_grab=0 + --middle_click_ungrab=1 + --mouse_integration=1 + # Speed overrides: floppy_drive_speed=800 runs the floppy at 8x so + # the Workbench boot finishes in a few seconds instead of a minute. + # accuracy=0 and fast_copper=1 are deliberately NOT set -- they + # compromise copper timing, which our per-scanline palette swaps + # depend on. + --floppy_drive_speed=800 +) + +if [[ -f $kickstart ]]; then + fsargs+=(--kickstart_file="$kickstart") +fi + +if [[ -f $workbench ]]; then + fsargs+=(--floppy_drive_0="$workbench") + cat < All Files" (Workbench hides + files that have no .info icon by default). + 3. Double-click $file to run it. +Alternatively open a Shell (Workbench > Tools > Shell), type 'cd +JOEYLIB:' then '$file'. +EOF +else + cat <&2; exit 2 ;; +esac + +tos=$repo/toolchains/emulators/support/emutos-512k.img + +if [[ ! -f "$bin_dir/$file" ]]; then + echo "$bin_dir/$file not built. Run 'make atarist' first." >&2 + exit 1 +fi +if [[ ! -f $tos ]]; then + echo "TOS ROM missing: $tos" >&2 + echo "run ./toolchains/install.sh (EmuTOS should have been staged)." >&2 + exit 1 +fi + +# Hatari's --auto needs the target on a mounted GEMDOS drive. +exec hatari \ + --tos "$tos" \ + --harddrive "$bin_dir" \ + --gemdos-drive C \ + --auto "C:\\$file" diff --git a/scripts/run-dos.sh b/scripts/run-dos.sh new file mode 100755 index 0000000..377ee2f --- /dev/null +++ b/scripts/run-dos.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Launch the built DOS example in DOSBox. Defaults to PATTERN; pass +# "hello" to run HELLO instead. +# +# scripts/run-dos.sh # runs PATTERN +# scripts/run-dos.sh hello # runs HELLO + +set -euo pipefail + +prog=${1:-pattern} +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +bin_dir=$repo/build/dos/bin + +case $prog in + hello) file=HELLO.EXE ;; + pattern) file=PATTERN.EXE ;; + *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; +esac + +if [[ ! -f "$bin_dir/$file" ]]; then + echo "$bin_dir/$file not built. Run 'make dos' first." >&2 + exit 1 +fi + +exec dosbox -c "C:" -c "$file" -c "pause" --exit "$bin_dir" diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh new file mode 100755 index 0000000..009e262 --- /dev/null +++ b/scripts/run-iigs.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Launch the built Apple IIgs examples in GSplus. GSplus is booted from +# a GS/OS 6.0.4 System disk (toolchains/emulators/support/gsos-system.po) +# with joey.2mg mounted as the data disk on slot 5 drive 2. The user +# navigates to the JOEYLIB volume in Finder and double-clicks HELLO or +# PATTERN to run it. +# +# Unlike the other emulators, GS/OS does not auto-run on boot -- it +# drops to Finder. Pass "hello" or "pattern" to print a reminder of +# which example to launch. +# +# scripts/run-iigs.sh # boots and waits for user (Pattern hint) +# scripts/run-iigs.sh hello # same, but hints to click HELLO + +set -euo pipefail + +prog=${1:-pattern} +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + +gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus +rom=$repo/toolchains/emulators/support/apple-iigs.rom +sys_disk=$repo/toolchains/emulators/support/gsos-system.po +data_disk=$repo/build/iigs/bin/joey.2mg + +case $prog in + hello|pattern) ;; + *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; +esac + +for f in "$gsplus" "$rom" "$sys_disk" "$data_disk"; do + if [[ ! -f $f ]]; then + echo "missing: $f" >&2 + if [[ $f == "$data_disk" ]]; then + echo "run 'make iigs-disk' to build it." >&2 + else + echo "run ./toolchains/install.sh (ROM/system disk should have been staged)." >&2 + fi + exit 1 + fi +done + +# GSplus writes back to disk images during the session; stage writable +# copies so repeated runs do not mutate the originals. +work=$(mktemp -d -t joeylib-iigs.XXXXXX) +trap 'rm -rf "$work"' EXIT + +cp "$sys_disk" "$work/boot.po" +cp "$data_disk" "$work/joey.2mg" + +target=$(echo "$prog" | tr '[:lower:]' '[:upper:]') +cat < +#include + +#include "joey/draw.h" +#include "surfaceInternal.h" + +// ----- Prototypes ----- + +static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible); +static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); + +// ----- Internal helpers (alphabetical) ----- + +// Clip a rectangle to the surface bounds. Outputs *outVisible = false +// if the rect is entirely off-surface. +static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible) { + if (*w <= 0 || *h <= 0) { + *outVisible = false; + return; + } + if (*x < 0) { + *w += *x; + *x = 0; + } + if (*y < 0) { + *h += *y; + *y = 0; + } + if (*x >= SURFACE_WIDTH || *y >= SURFACE_HEIGHT) { + *outVisible = false; + return; + } + if (*x + *w > SURFACE_WIDTH) { + *w = SURFACE_WIDTH - *x; + } + if (*y + *h > SURFACE_HEIGHT) { + *h = SURFACE_HEIGHT - *y; + } + *outVisible = (*w > 0 && *h > 0); +} + + +static void fillRectClipped(SurfaceT *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; + int16_t pxStart; + int16_t pxEnd; + int16_t midBytes; + uint8_t *line; + + for (row = 0; row < h; row++) { + line = &s->pixels[(y + row) * SURFACE_BYTES_PER_ROW]; + pxStart = x; + pxEnd = x + w; + + if (pxStart & 1) { + line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0xF0) | nibble); + pxStart++; + } + + midBytes = (pxEnd - pxStart) >> 1; + if (midBytes > 0) { + memset(&line[pxStart >> 1], doubled, (size_t)midBytes); + pxStart += midBytes << 1; + } + + if (pxStart < pxEnd) { + line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0x0F) | (nibble << 4)); + } + } +} + + +// ----- Public API (alphabetical) ----- + +void drawPixel(SurfaceT *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; + } + + byte = &s->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)]; + nibble = colorIndex & 0x0F; + if (x & 1) { + *byte = (uint8_t)((*byte & 0xF0) | nibble); + } else { + *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4)); + } +} + + +void fillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { + int16_t sx; + int16_t sy; + int16_t sw; + int16_t sh; + bool visible; + + if (s == NULL) { + return; + } + + sx = x; + sy = y; + sw = (int16_t)w; + sh = (int16_t)h; + clipRect(&sx, &sy, &sw, &sh, &visible); + if (!visible) { + return; + } + fillRectClipped(s, sx, sy, sw, sh, colorIndex); +} + + +uint8_t samplePixel(const SurfaceT *s, int16_t x, int16_t y) { + uint8_t byte; + + if (s == NULL) { + return 0; + } + if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) { + return 0; + } + + byte = s->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)]; + if (x & 1) { + return (uint8_t)(byte & 0x0F); + } + return (uint8_t)(byte >> 4); +} + + +void surfaceClear(SurfaceT *s, uint8_t colorIndex) { + uint8_t nibble; + uint8_t doubled; + + if (s == NULL) { + return; + } + nibble = colorIndex & 0x0F; + doubled = (uint8_t)((nibble << 4) | nibble); + memset(s->pixels, doubled, SURFACE_PIXELS_SIZE); +} diff --git a/src/core/hal.h b/src/core/hal.h new file mode 100644 index 0000000..75e99d3 --- /dev/null +++ b/src/core/hal.h @@ -0,0 +1,37 @@ +// 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 "joey/core.h" +#include "joey/surface.h" + +// Per-port one-shot initialization. Called from joeyInit 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 JoeyConfigT *config); + +// Per-port teardown. Restores display mode, frees HW-adjacent buffers. +void halShutdown(void); + +// Present the entire source surface to the display. +void halPresent(const SurfaceT *src); + +// Present a rectangular region of the source surface. The caller has +// already validated and clipped the rect to be fully inside the +// surface bounds and to have positive extents. +void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h); + +// 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); + +#endif diff --git a/src/core/init.c b/src/core/init.c new file mode 100644 index 0000000..e689f2a --- /dev/null +++ b/src/core/init.c @@ -0,0 +1,94 @@ +// JoeyLib lifecycle: init, shutdown, error reporting. +// +// joeyInit stores configuration, allocates the library-owned screen +// surface, and asks the port HAL to set up the display mode. +// joeyShutdown tears those down in reverse order. + +#include +#include + +#include "joey/core.h" +#include "hal.h" +#include "surfaceInternal.h" + +// ----- Prototypes ----- + +static void clearError(void); +static void setError(const char *message); + +// ----- Module state ----- + +static JoeyConfigT gConfig; +static bool gInitialized = false; +static const char *gLastError = NULL; + +// ----- Internal helpers (alphabetical) ----- + +static void clearError(void) { + gLastError = NULL; +} + + +static void setError(const char *message) { + gLastError = message; +} + + +// ----- Public API (alphabetical) ----- + +bool joeyInit(const JoeyConfigT *config) { + clearError(); + + if (gInitialized) { + setError("joeyInit called while already initialized"); + return false; + } + + if (config == NULL) { + setError("joeyInit called with NULL config"); + return false; + } + + memcpy(&gConfig, config, sizeof(gConfig)); + + if (!surfaceAllocScreen()) { + setError("failed to allocate screen surface"); + return false; + } + + if (!halInit(&gConfig)) { + const char *halMsg = halLastError(); + setError(halMsg != NULL ? halMsg : "halInit failed"); + surfaceFreeScreen(); + return false; + } + + gInitialized = true; + return true; +} + + +const char *joeyLastError(void) { + return gLastError; +} + + +const char *joeyPlatformName(void) { + return JOEYLIB_PLATFORM_NAME; +} + + +void joeyShutdown(void) { + if (!gInitialized) { + return; + } + halShutdown(); + surfaceFreeScreen(); + gInitialized = false; + clearError(); +} + + +const char *joeyVersionString(void) { + return JOEYLIB_VERSION_STRING; +} diff --git a/src/core/palette.c b/src/core/palette.c new file mode 100644 index 0000000..94f2e2f --- /dev/null +++ b/src/core/palette.c @@ -0,0 +1,40 @@ +// Palette accessors. +// +// Library contract: color 0 of every palette is forced to black ($000). +// paletteSet silently masks that entry regardless of what the caller +// provides. + +#include +#include + +#include "joey/palette.h" +#include "surfaceInternal.h" + +// ----- Public API (alphabetical) ----- + +void paletteGet(const SurfaceT *s, uint8_t paletteIndex, uint16_t *out16) { + if (s == NULL || out16 == NULL) { + return; + } + if (paletteIndex >= SURFACE_PALETTE_COUNT) { + return; + } + memcpy(out16, s->palette[paletteIndex], SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t)); +} + + +void paletteSet(SurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16) { + uint8_t i; + + if (s == NULL || colors16 == NULL) { + return; + } + if (paletteIndex >= SURFACE_PALETTE_COUNT) { + return; + } + + s->palette[paletteIndex][0] = 0x0000; + for (i = 1; i < SURFACE_COLORS_PER_PALETTE; i++) { + s->palette[paletteIndex][i] = colors16[i] & 0x0FFF; + } +} diff --git a/src/core/present.c b/src/core/present.c new file mode 100644 index 0000000..8cc35de --- /dev/null +++ b/src/core/present.c @@ -0,0 +1,63 @@ +// Present / slam dispatcher. +// +// Validates and clips the source rectangle, then routes to the port's +// HAL implementation for the actual pixel format conversion and +// display-memory write. + +#include + +#include "joey/present.h" +#include "hal.h" +#include "surfaceInternal.h" + +// ----- Public API (alphabetical) ----- + +void surfacePresent(const SurfaceT *src) { + if (src == NULL) { + return; + } + halPresent(src); +} + + +void surfacePresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) { + int16_t sx; + int16_t sy; + int16_t sw; + int16_t sh; + + if (src == NULL) { + return; + } + + sx = x; + sy = y; + sw = (int16_t)w; + sh = (int16_t)h; + + if (sw <= 0 || sh <= 0) { + return; + } + if (sx < 0) { + sw += sx; + sx = 0; + } + if (sy < 0) { + sh += sy; + sy = 0; + } + if (sx >= SURFACE_WIDTH || sy >= SURFACE_HEIGHT) { + return; + } + if (sx + sw > SURFACE_WIDTH) { + sw = SURFACE_WIDTH - sx; + } + if (sy + sh > SURFACE_HEIGHT) { + sh = SURFACE_HEIGHT - sy; + } + if (sw <= 0 || sh <= 0) { + return; + } + + halPresentRect(src, sx, sy, (uint16_t)sw, (uint16_t)sh); +} diff --git a/src/core/scb.c b/src/core/scb.c new file mode 100644 index 0000000..cba68d9 --- /dev/null +++ b/src/core/scb.c @@ -0,0 +1,57 @@ +// Scanline control byte (SCB) accessors. +// +// Each scanline holds one uint8_t SCB value in range 0..15 selecting +// which of the 16 palettes that scanline uses at display time. + +#include + +#include "joey/palette.h" +#include "surfaceInternal.h" + +// ----- Public API (alphabetical) ----- + +uint8_t scbGet(const SurfaceT *s, uint16_t line) { + if (s == NULL || line >= SURFACE_HEIGHT) { + return 0; + } + return s->scb[line]; +} + + +void scbSet(SurfaceT *s, uint16_t line, uint8_t paletteIndex) { + if (s == NULL || line >= SURFACE_HEIGHT) { + return; + } + if (paletteIndex >= SURFACE_PALETTE_COUNT) { + return; + } + s->scb[line] = paletteIndex; +} + + +void scbSetRange(SurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t paletteIndex) { + uint16_t line; + uint16_t last; + + if (s == NULL) { + return; + } + if (paletteIndex >= SURFACE_PALETTE_COUNT) { + return; + } + if (firstLine >= SURFACE_HEIGHT) { + return; + } + + last = lastLine; + if (last >= SURFACE_HEIGHT) { + last = SURFACE_HEIGHT - 1; + } + if (last < firstLine) { + return; + } + + for (line = firstLine; line <= last; line++) { + s->scb[line] = paletteIndex; + } +} diff --git a/src/core/surface.c b/src/core/surface.c new file mode 100644 index 0000000..5171a53 --- /dev/null +++ b/src/core/surface.c @@ -0,0 +1,68 @@ +// Surface allocation, destruction, and the library-owned screen surface. + +#include +#include +#include + +#include "joey/surface.h" +#include "surfaceInternal.h" + +// ----- Prototypes ----- + +// (public API declared in joey/surface.h) +// (internal prototypes declared in surfaceInternal.h) + +// ----- Module state ----- + +static SurfaceT *gScreen = NULL; + +// ----- Public API (alphabetical) ----- + +void surfaceCopy(SurfaceT *dst, const SurfaceT *src) { + if (dst == NULL || src == NULL || dst == src) { + return; + } + memcpy(dst, src, sizeof(SurfaceT)); +} + + +SurfaceT *surfaceCreate(void) { + SurfaceT *s = (SurfaceT *)calloc(1, sizeof(SurfaceT)); + return s; +} + + +void surfaceDestroy(SurfaceT *s) { + if (s == NULL) { + return; + } + if (s == gScreen) { + return; + } + free(s); +} + + +SurfaceT *surfaceGetScreen(void) { + return gScreen; +} + + +// ----- Internal (alphabetical) ----- + +bool surfaceAllocScreen(void) { + if (gScreen != NULL) { + return true; + } + gScreen = (SurfaceT *)calloc(1, sizeof(SurfaceT)); + return gScreen != NULL; +} + + +void surfaceFreeScreen(void) { + if (gScreen == NULL) { + return; + } + free(gScreen); + gScreen = NULL; +} diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h new file mode 100644 index 0000000..5da4e74 --- /dev/null +++ b/src/core/surfaceInternal.h @@ -0,0 +1,21 @@ +// Internal surface definition shared across core and port code. + +#ifndef JOEYLIB_SURFACE_INTERNAL_H +#define JOEYLIB_SURFACE_INTERNAL_H + +#include + +#include "joey/surface.h" + +struct SurfaceT { + uint8_t pixels[SURFACE_PIXELS_SIZE]; + uint8_t scb[SURFACE_HEIGHT]; + uint16_t palette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; +}; + +// Allocate and free the library's pre-allocated screen surface. Called +// from init.c during joeyInit / joeyShutdown. +bool surfaceAllocScreen(void); +void surfaceFreeScreen(void); + +#endif diff --git a/src/port/amiga/hal.c b/src/port/amiga/hal.c new file mode 100644 index 0000000..4ef5426 --- /dev/null +++ b/src/port/amiga/hal.c @@ -0,0 +1,411 @@ +// Commodore Amiga HAL for M2 + M2.5. +// +// M2 scope: +// * OpenScreen (Intuition) for a CUSTOMSCREEN at 320x200x4 bitplanes. +// * Chunky 4bpp to 4 separate bitplanes c2p at present time. +// * Partial-rect present covers only the dirty scanlines. +// +// M2.5 scope (per-scanline palette / SCB emulation): +// * Build a user copper list that WAITs for each display scanline +// and MOVEs the 16 color registers with that line's palette. +// * Install it via ViewPort.UCopIns + MakeScreen + RethinkDisplay. +// * Rebuild on every halPresent since SCB and palettes may have +// changed anywhere. +// +// Deferred: +// * Blitter-assisted c2p for speed on A500. +// * Takeover mode (LoadView(NULL) + OwnBlitter + direct hardware). + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "hal.h" +#include "surfaceInternal.h" + +extern struct Custom custom; + +// ----- Constants ----- + +#define AMIGA_BITPLANES 4 +#define AMIGA_BYTES_PER_ROW 40 + +// ----- Prototypes ----- + +static void buildCopperList(const SurfaceT *src); +static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1); +static void dumpCopperList(void); +static void installCopperList(void); +static void uploadFirstBandPalette(const SurfaceT *src); + +// ----- Module state ----- + +static struct Screen *gScreen = NULL; +static struct BitMap *gBitMap = NULL; +static UBYTE *gPlanes[AMIGA_BITPLANES]; +static struct UCopList *gNewUCL = NULL; // built but not yet installed + +// ----- Internal helpers (alphabetical) ----- + +// Convert a range of chunky scanlines [y0, y1) to Amiga planar. +// Each plane scanline is 40 bytes (1 bit per pixel x 320 pixels). +// For each destination byte, 8 pixels' worth of 4bpp chunky source is +// read and split into one bit per plane. +static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) { + const uint8_t *srcLine; + UBYTE *p0; + UBYTE *p1; + UBYTE *p2; + UBYTE *p3; + int16_t y; + uint16_t planarByte; + uint16_t px; + uint16_t pixel; + uint8_t srcByte; + uint8_t nibble; + uint8_t bit; + uint8_t b0; + uint8_t b1; + uint8_t b2; + uint8_t b3; + + for (y = y0; y < y1; y++) { + srcLine = &src->pixels[y * SURFACE_BYTES_PER_ROW]; + p0 = &gPlanes[0][y * AMIGA_BYTES_PER_ROW]; + p1 = &gPlanes[1][y * AMIGA_BYTES_PER_ROW]; + p2 = &gPlanes[2][y * AMIGA_BYTES_PER_ROW]; + p3 = &gPlanes[3][y * AMIGA_BYTES_PER_ROW]; + + for (planarByte = 0; planarByte < AMIGA_BYTES_PER_ROW; planarByte++) { + b0 = 0; + b1 = 0; + b2 = 0; + b3 = 0; + for (px = 0; px < 8; px++) { + pixel = (uint16_t)(planarByte * 8 + px); + srcByte = srcLine[pixel >> 1]; + nibble = (uint8_t)((pixel & 1) ? (srcByte & 0x0F) : (srcByte >> 4)); + bit = (uint8_t)(7 - px); + b0 = (uint8_t)(b0 | (((nibble >> 0) & 1) << bit)); + b1 = (uint8_t)(b1 | (((nibble >> 1) & 1) << bit)); + b2 = (uint8_t)(b2 | (((nibble >> 2) & 1) << bit)); + b3 = (uint8_t)(b3 | (((nibble >> 3) & 1) << bit)); + } + p0[planarByte] = b0; + p1[planarByte] = b1; + p2[planarByte] = b2; + p3[planarByte] = b3; + } + } +} + + +// Build a user copper list for per-scanline palette (SCB emulation). +// One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is +// stored in gNewUCL until installCopperList swaps it onto the screen. +// DyOffset tells us where display line 0 sits in hardware coordinates +// so the WAITs line up with the real visible region regardless of +// PAL/NTSC or any overscan the user may have configured. +static void buildCopperList(const SurfaceT *src) { + struct UCopList *ucl; + UWORD line; + UWORD col; + UBYTE palIdx; + UWORD prevPalIdx; + UWORD vpos; + UWORD topBorder; + UWORD bandCount; + + ucl = (struct UCopList *)AllocMem(sizeof(struct UCopList), + MEMF_PUBLIC | MEMF_CLEAR); + if (ucl == NULL) { + gNewUCL = NULL; + return; + } + + // Worst-case reservation is one band-change per scanline (16 MOVEs + // + 1 WAIT per change), plus the terminal wait. For realistic SCB + // tables the actual count is far smaller, but CINIT only takes a + // single number so we size for the cap. + CINIT(ucl, (SURFACE_HEIGHT * 17) + 1); + + // Hardware scanline where display line 0 lives. 0x2C is the + // standard top border for a PAL screen at TopEdge=0; we hardcode + // rather than reading ViewPort.DyOffset because DyOffset is a + // signed +/- adjustment around the standard value, not the + // absolute hardware line. + // User-copper vpos values are DISPLAY-RELATIVE -- graphics.lib + // MrgCop adds the active View's DyOffset to each WAIT before + // emitting, so a vp=0 user WAIT lands at beam line DyOffset, + // which is where Intuition places display line 0. Emitting at + // vpos=line keeps merged vpos under 256 even for the last band + // (175 + 44 = 219 < 256), avoiding MrgCop's destructive wrap- + // handling path that would otherwise disable bitplane DMA at + // the viewport end. + topBorder = 0; + prevPalIdx = 0xFFFF; + bandCount = 0; + + for (line = 0; line < SURFACE_HEIGHT; line++) { + palIdx = src->scb[line]; + if (palIdx >= SURFACE_PALETTE_COUNT) { + palIdx = 0; + } + if ((UWORD)palIdx == prevPalIdx) { + continue; + } + + vpos = (UWORD)(line + topBorder); + CWAIT(ucl, vpos, 0); + for (col = 0; col < SURFACE_COLORS_PER_PALETTE; col++) { + CMOVE(ucl, custom.color[col], src->palette[palIdx][col]); + } + prevPalIdx = (UWORD)palIdx; + bandCount++; + } + (void)bandCount; + CEND(ucl); + + gNewUCL = ucl; +} + + +// Swap the freshly built user copper list onto the screen's ViewPort +// and force a full graphics-library recomputation of the hardware +// copper list. MakeScreen regenerates the viewport copper to include +// our UCopIns; MrgCop merges every viewport's copper into one hardware +// list; LoadView swaps the live copper pointers. Calling the graphics +// primitives directly (rather than only Intuition's RethinkDisplay / +// RemakeDisplay) was observed here to be the step that actually makes +// the user copper list visible -- Intuition's wrappers sometimes +// skipped the merge. +static void installCopperList(void) { + struct View *view; + + if (gNewUCL == NULL || gScreen == NULL) { + return; + } + Forbid(); + if (gScreen->ViewPort.UCopIns != NULL) { + FreeVPortCopLists(&gScreen->ViewPort); + } + gScreen->ViewPort.UCopIns = gNewUCL; + gNewUCL = NULL; + Permit(); + + MakeScreen(gScreen); + + view = ViewAddress(); + Forbid(); + MrgCop(view); + LoadView(view); + Permit(); + WaitTOF(); +} + + +// Diagnostic: dump the merged hardware copper list (LOFCprList) to a +// text file on the current volume. Written once per halPresent after +// MrgCop, so the host can inspect exactly what the copper is being +// asked to execute. Each line is either MOVE (destination offset + +// data) or WAIT (vp, hp, mask). The dump stops at the first "end of +// copper" marker (0xFFFF + any mask with bit15 clear would be a wait +// past frame end). +static void dumpCopperList(void) { + FILE *fp; + struct View *view; + struct cprlist *cl; + UWORD *p; + WORD i; + WORD count; + UWORD w1; + UWORD w2; + + fp = fopen("copper.txt", "w"); + if (fp == NULL) { + return; + } + + view = ViewAddress(); + if (view == NULL) { + fprintf(fp, "view is NULL\n"); + fclose(fp); + return; + } + + cl = view->LOFCprList; + if (cl == NULL) { + fprintf(fp, "LOFCprList is NULL\n"); + fclose(fp); + return; + } + + p = cl->start; + count = cl->MaxCount; + fprintf(fp, "LOFCprList.start=0x%08lx MaxCount=%d\n", + (unsigned long)p, (int)count); + fprintf(fp, "vp.DyOffset=%d vp.DxOffset=%d\n", + (int)gScreen->ViewPort.DyOffset, + (int)gScreen->ViewPort.DxOffset); + fprintf(fp, "view.DyOffset=%d view.DxOffset=%d\n", + (int)view->DyOffset, (int)view->DxOffset); + fprintf(fp, "--\n"); + + if (p == NULL) { + fclose(fp); + return; + } + + for (i = 0; i < count; i++) { + w1 = p[i * 2]; + w2 = p[i * 2 + 1]; + if (w1 == 0xFFFF && w2 == 0xFFFE) { + fprintf(fp, "%4d: END %04x %04x\n", (int)i, w1, w2); + break; + } + if (w1 & 1) { + fprintf(fp, "%4d: %s vp=%3d hp=%3d mask=%04x\n", + (int)i, + (w2 & 0x8000) ? "SKIP" : "WAIT", + (int)(w1 >> 8), + (int)(w1 & 0xFE), + (unsigned)w2); + } else { + fprintf(fp, "%4d: MOVE dst=%03x data=%04x\n", + (int)i, + (unsigned)(w1 & 0x1FE), + (unsigned)w2); + } + } + + fclose(fp); +} + + +// Load the first band's palette into the screen's ColorMap so the +// Intuition-generated frame-start copper writes those values on each +// frame. This acts as a safety net: even if our user copper list does +// not fire (or fires late) for the very first band, the top of the +// display still shows the correct colors because Intuition's own +// COLORxx loads happen before any user copper instruction. +static void uploadFirstBandPalette(const SurfaceT *src) { + UWORD aPalette[SURFACE_COLORS_PER_PALETTE]; + UWORD i; + UBYTE palIdx; + + palIdx = src->scb[0]; + if (palIdx >= SURFACE_PALETTE_COUNT) { + palIdx = 0; + } + for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + aPalette[i] = (UWORD)src->palette[palIdx][i]; + } + LoadRGB4(&gScreen->ViewPort, aPalette, SURFACE_COLORS_PER_PALETTE); +} + + +// ----- HAL API (alphabetical) ----- + +bool halInit(const JoeyConfigT *config) { + uint16_t i; + + (void)config; + + // SA_DisplayID pins us to OCS PAL low-res so Intuition opens a + // real planar screen rather than an RTG substitute. + gScreen = OpenScreenTags(NULL, + (ULONG)SA_Width, (ULONG)SURFACE_WIDTH, + (ULONG)SA_Height, (ULONG)SURFACE_HEIGHT, + (ULONG)SA_Depth, (ULONG)AMIGA_BITPLANES, + (ULONG)SA_DisplayID, (ULONG)(PAL_MONITOR_ID | LORES_KEY), + (ULONG)SA_DetailPen, (ULONG)0, + (ULONG)SA_BlockPen, (ULONG)1, + (ULONG)SA_Title, (ULONG)"JoeyLib", + (ULONG)SA_Type, (ULONG)CUSTOMSCREEN, + (ULONG)SA_Quiet, (ULONG)TRUE, + TAG_DONE); + + if (gScreen == NULL) { + return false; + } + gBitMap = gScreen->RastPort.BitMap; + for (i = 0; i < AMIGA_BITPLANES; i++) { + gPlanes[i] = gBitMap->Planes[i]; + if (gPlanes[i] == NULL) { + CloseScreen(gScreen); + gScreen = NULL; + return false; + } + } + return true; +} + + +const char *halLastError(void) { + return NULL; +} + + +void halPresent(const SurfaceT *src) { + if (src == NULL || gScreen == NULL) { + return; + } + uploadFirstBandPalette(src); + buildCopperList(src); + installCopperList(); + dumpCopperList(); + c2pRange(src, 0, SURFACE_HEIGHT); +} + + +void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) { + (void)x; + (void)w; + + if (src == NULL || gScreen == NULL) { + return; + } + // Whole-scanline c2p for M2; rebuild the entire copper list since + // SCB entries outside the rect may have changed too. + uploadFirstBandPalette(src); + buildCopperList(src); + installCopperList(); + c2pRange(src, y, y + (int16_t)h); +} + + +void halShutdown(void) { + if (gScreen != NULL) { + // CloseScreen should free attached UCopList, but be explicit + // to catch any case where the screen close path skips it. + Forbid(); + if (gScreen->ViewPort.UCopIns != NULL) { + FreeVPortCopLists(&gScreen->ViewPort); + } + Permit(); + CloseScreen(gScreen); + gScreen = NULL; + gBitMap = NULL; + } + if (gNewUCL != NULL) { + FreeMem(gNewUCL, sizeof(struct UCopList)); + gNewUCL = NULL; + } +} diff --git a/src/port/amiga/libinit.c b/src/port/amiga/libinit.c new file mode 100644 index 0000000..20ad16a --- /dev/null +++ b/src/port/amiga/libinit.c @@ -0,0 +1,83 @@ +// Tolerant replacement for libnix's __initlibraries / __exitlibraries. +// +// libnix's version treats any OpenLibrary failure as fatal: it walks the +// auto-open list, and if any entry cannot be opened, it writes +// " failed to load" to the CLI and exits(20) before main() runs. +// +// That behavior kills the examples on AROS, which does not provide +// locale.library as a disk library. libnix unconditionally pulls the +// locale.library entry into the auto-open list because C runtime +// utilities (e.g. tzset via time()) take LocaleBase as an argument to +// ConvToLower -- even when the runtime does not actually need a real +// Locale to run. +// +// Our replacement walks the same list but records NULL for any +// library that cannot be opened, then continues. Code that actually +// needs the library will crash when it dereferences the NULL base, +// but none of the joeylib examples do. Since these symbols are +// defined here in a user object, the linker satisfies references +// from this file instead of pulling in libnix's version. + +#include +#include + +extern long __LIB_LIST__; + + +static int endsWithResource(const char *name) { + const char *p = name; + while (*p) { + p++; + } + p -= 9; + if (p < name) { + return 0; + } + return p[0] == '.' && p[1] == 'r' && p[2] == 'e' && p[3] == 's' && + p[4] == 'o' && p[5] == 'u' && p[6] == 'r' && p[7] == 'c' && + p[8] == 'e'; +} + + +void __initlibraries(void) { + long *entry = &__LIB_LIST__ + 1; + while (*entry) { + long *base = entry++; + const char *name = *(const char **)entry++; + long lib; + + if (endsWithResource(name)) { + lib = (long)OpenResource((STRPTR)name); + } else { + lib = (long)OldOpenLibrary((STRPTR)name); + } + *base = lib; + } +} + + +void __exitlibraries(void) { + long *entry = &__LIB_LIST__ + 1; + while (*entry) { + long *base = entry++; + const char *name = *(const char **)entry++; + if (*base != 0 && *base != -1 && !endsWithResource(name)) { + CloseLibrary((struct Library *)*base); + } + } +} + + +ADD2INIT(__initlibraries, -79); +ADD2EXIT(__exitlibraries, -79); + + +// libnix's tzset dereferences LocaleBase to call locale.library's +// ConvToLower. On AROS (where locale.library is unavailable), our +// tolerant __initlibraries leaves LocaleBase NULL, which would crash +// the first time time() is called (via tzset). Our pattern example +// uses time() for the 5-second display; override tzset with a no-op +// so time() can run. Difference-based timing is unaffected because +// any DST correction applied consistently cancels out. +void tzset(void) { +} diff --git a/src/port/atarist/hal.c b/src/port/atarist/hal.c new file mode 100644 index 0000000..1f0eccf --- /dev/null +++ b/src/port/atarist/hal.c @@ -0,0 +1,479 @@ +// Atari ST HAL for M2 + M2.5. +// +// M2 scope: +// * XBIOS Setscreen to ST low-res (320x200x16, mode 0). +// * Chunky 4bpp to word-interleaved ST planar c2p at present time. +// +// M2.5 scope (per-band palette / SCB emulation): +// * halPresent scans the SurfaceT'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). +// * VBL ISR pre-loads the first band's palette, then programs +// MFP Timer B (event-count mode, TBDR = HBL delta to first +// transition) to fire at the END of the last scanline before +// the next band starts. +// * Timer B ISR writes the current band's palette, advances the +// transition index, and (stop/reload TBDR/restart) reprograms +// Timer B to fire at the next transition. With 8 transitions per +// frame the ISR runs 8 times instead of 313 -- well under the +// ~147-HBL-fires-per-frame cap Hatari's MFP emulation imposes on +// event-count mode, and ~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 +// its first row per band as the source of 16 shifter writes. +// +// Deferred: +// * Takeover mode (direct shifter programming without TOS). +// * STE's extended palette bits (we drop to STF 9-bit for now). + +#include +#include +#include + +#include + +#include "hal.h" +#include "surfaceInternal.h" + +// ----- Constants ----- + +// Word-interleaved ST planar uses the same 160 bytes/scanline as our +// chunky source, but organized as 20 groups of 4 words per scanline, +// with each word holding the 16 one-bit samples for one bitplane. +#define ST_BYTES_PER_ROW 160 +#define ST_GROUPS_PER_ROW 20 +#define ST_SCREEN_ALIGN 256 + +// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F. +#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L) + +// MFP hardware addresses. +#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control +#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data +#define ST_MFP_ISRA ((volatile uint8_t *)0xFFFFFA0FL) // In-service A +#define MFP_TBCR_STOP 0x00 +#define MFP_TBCR_EVENT 0x08 +#define MFP_TB_CLEAR 0xFE // clear bit 0 of ISRA (Timer B) + +// Exception-vector numbers passed to Setexc (= vector offset / 4). +#define VEC_VBL (0x70 / 4) // 68k autovector IRQ 4 +#define VEC_MFP_TB (0x120 / 4) // MFP Timer B +#define INT_TIMER_B 8 + +// ----- Prototypes ----- + +static uint16_t quantizeColorToSt(uint16_t orgb); +static void c2pRow(const uint8_t *src, uint16_t *dst); +static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1); +static void flattenScbPalettes(const SurfaceT *src); +static void writeDiagnostics(void); +static long writePrevPaletteRegs(void); + +static __attribute__((interrupt_handler)) void timerBIsr(void); +static __attribute__((interrupt_handler)) void vblIsr(void); +static void buildTransitions(const SurfaceT *src); + +// ----- Module state ----- + +// Screen buffer: enough for 320x200x4bpp planar plus padding for +// runtime 256-byte alignment. TOS .PRG format only supports 2-byte +// object-file alignment, so we overallocate and align the pointer +// manually in halInit. +static uint8_t gScreenBuffer[SURFACE_PIXELS_SIZE + ST_SCREEN_ALIGN]; + +static uint8_t *gScreenBase = NULL; +static void *gPrevPhysbase = NULL; +static void *gPrevLogbase = NULL; +static int16_t gPrevRez = 0; +static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE]; +static bool gModeSet = false; + +// Per-scanline pre-quantized palette table. Indexed by display line; +// each row is a 16-word palette ready to be copied straight into the +// shifter registers. Written at present() time, read by the Timer B +// ISR with no CPU-side math beyond a counter subtract. +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 SurfaceT's SCB array. +#define MAX_BANDS SURFACE_HEIGHT +static uint16_t gBandStart [MAX_BANDS]; +static uint8_t gBandPalIdx[MAX_BANDS]; +static uint16_t gBandCount = 0; + +// Index of the band the Timer B ISR is currently scheduling TO. At +// VBL this is 0 (band 0 palette pre-loaded, Timer B scheduled to +// fire when it's time to transition to band 1). Each ISR fire writes +// the palette for gCurrentBand and advances to the next. +static volatile uint16_t gCurrentBand = 0; + +// Diagnostic captures. +static volatile int16_t gFrameCount = 0; +static volatile uint16_t gLastBandCount = 0; + +// Saved exception vectors for restore on shutdown. +static void (*gOldVblVec)(void) = NULL; +static void (*gOldTimerBVec)(void) = NULL; + +// ----- Internal helpers (alphabetical) ----- + +// Convert 16 chunky pixels (8 bytes 4bpp packed) to 4 ST planar words. +// The output word order is plane 0, 1, 2, 3 (low bit = plane 0). +static void c2pRow(const uint8_t *src, uint16_t *dst) { + uint16_t group; + uint16_t px; + uint16_t plane0; + uint16_t plane1; + uint16_t plane2; + uint16_t plane3; + uint8_t byte; + uint8_t nibble; + uint16_t bit; + + for (group = 0; group < ST_GROUPS_PER_ROW; group++) { + plane0 = 0; + plane1 = 0; + plane2 = 0; + plane3 = 0; + + for (px = 0; px < 16; px++) { + byte = src[(group * 8) + (px >> 1)]; + nibble = (uint8_t)((px & 1) ? (byte & 0x0F) : (byte >> 4)); + bit = (uint16_t)(15 - px); + plane0 = (uint16_t)(plane0 | (((nibble >> 0) & 1) << bit)); + plane1 = (uint16_t)(plane1 | (((nibble >> 1) & 1) << bit)); + plane2 = (uint16_t)(plane2 | (((nibble >> 2) & 1) << bit)); + plane3 = (uint16_t)(plane3 | (((nibble >> 3) & 1) << bit)); + } + + dst[(group * 4) + 0] = plane0; + dst[(group * 4) + 1] = plane1; + dst[(group * 4) + 2] = plane2; + dst[(group * 4) + 3] = plane3; + } +} + + +static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) { + int16_t y; + const uint8_t *srcLine; + uint16_t *dstLine; + + for (y = y0; y < y1; y++) { + srcLine = &src->pixels[y * SURFACE_BYTES_PER_ROW]; + dstLine = (uint16_t *)&gScreenBase[y * ST_BYTES_PER_ROW]; + c2pRow(srcLine, dstLine); + } +} + + +// Scan the surface's SCB and record one transition entry for each +// run of the same palette index. gBandCount is the number of +// distinct bands; gBandStart[i] is the display line where band i +// begins; gBandPalIdx[i] is the palette index that band uses. +static void buildTransitions(const SurfaceT *src) { + uint16_t line; + uint8_t idx; + uint8_t prev; + + gBandCount = 0; + prev = 0xFF; + + for (line = 0; line < SURFACE_HEIGHT; line++) { + idx = src->scb[line]; + if (idx >= SURFACE_PALETTE_COUNT) { + idx = 0; + } + if (idx != prev) { + if (gBandCount < MAX_BANDS) { + gBandStart [gBandCount] = line; + gBandPalIdx[gBandCount] = idx; + gBandCount++; + } + prev = idx; + } + } + gLastBandCount = gBandCount; +} + + +// 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. +static void flattenScbPalettes(const SurfaceT *src) { + uint16_t line; + uint16_t col; + uint8_t idx; + + for (line = 0; line < SURFACE_HEIGHT; line++) { + idx = src->scb[line]; + if (idx >= SURFACE_PALETTE_COUNT) { + idx = 0; + } + for (col = 0; col < SURFACE_COLORS_PER_PALETTE; col++) { + gLinePalettes[line][col] = quantizeColorToSt(src->palette[idx][col]); + } + } +} + + +// 12-bit $0RGB to STF 9-bit palette register (drops the low bit of +// each 4-bit channel). +static uint16_t quantizeColorToSt(uint16_t orgb) { + uint16_t r; + uint16_t g; + uint16_t b; + + r = (orgb >> 8) & 0x0F; + g = (orgb >> 4) & 0x0F; + b = orgb & 0x0F; + r = r >> 1; + g = g >> 1; + b = b >> 1; + return (uint16_t)((r << 8) | (g << 4) | b); +} + + +// Timer B interrupt handler. Fires once at each band transition; +// writes the band's palette to the shifter and lets Timer B's +// auto-reload keep counting for the next fire. We deliberately do +// NOT stop/reload/restart the timer here: that sequence would cost +// 1-2 HBL edges each fire, and those losses compound across 7+ +// transitions into a visible "last band short" drift. Updating +// TBDR in place is enough for variable-length bands -- the new +// value takes effect on the fire AFTER next, which is acceptable +// when adjacent bands have similar lengths; uniform bands (like +// pattern.c) don't need TBDR updates at all so stay perfectly +// aligned with no drift. +static void timerBIsr(void) { + uint16_t band; + uint8_t palIdx; + const uint16_t *src; + volatile uint16_t *dst; + uint16_t nextDelta; + + band = gCurrentBand + 1; + gCurrentBand = band; + + if (band < gBandCount) { + palIdx = gBandPalIdx[band]; + if (palIdx >= SURFACE_PALETTE_COUNT) { + palIdx = 0; + } + src = &gLinePalettes[gBandStart[band]][0]; + dst = ST_PALETTE_REGS; + dst[ 0] = src[ 0]; + dst[ 1] = src[ 1]; + dst[ 2] = src[ 2]; + dst[ 3] = src[ 3]; + dst[ 4] = src[ 4]; + dst[ 5] = src[ 5]; + dst[ 6] = src[ 6]; + dst[ 7] = src[ 7]; + dst[ 8] = src[ 8]; + dst[ 9] = src[ 9]; + dst[10] = src[10]; + dst[11] = src[11]; + dst[12] = src[12]; + dst[13] = src[13]; + dst[14] = src[14]; + dst[15] = src[15]; + + if (band + 1 < gBandCount) { + // Update TBDR for the fire-after-next (auto-reload at + // the NEXT fire still uses the old value). Don't stop + // the timer. + nextDelta = gBandStart[band + 1] - gBandStart[band]; + if (nextDelta == 0 || nextDelta > 255) { + nextDelta = 1; + } + *ST_MFP_TBDR = (uint8_t)nextDelta; + *ST_MFP_ISRA = MFP_TB_CLEAR; + return; + } + } + + // No further transitions this frame; stopping Timer B here only + // affects the (never-used) next fire, not timing of any band + // we've already scheduled. + *ST_MFP_TBCR = MFP_TBCR_STOP; + *ST_MFP_ISRA = MFP_TB_CLEAR; +} + + +// Vertical blank handler. Pre-loads band 0's palette so the first +// visible scanline is correct, then programs Timer B to fire at +// the HBL delta to the next band transition (if any). +static void vblIsr(void) { + uint16_t delta; + const uint16_t *src; + volatile uint16_t *dst; + + gFrameCount = gFrameCount + 1; + gCurrentBand = 0; + + if (gBandCount == 0) { + return; + } + + // Stage band 0's palette into the shifter registers. + src = &gLinePalettes[gBandStart[0]][0]; + dst = ST_PALETTE_REGS; + dst[ 0] = src[ 0]; + dst[ 1] = src[ 1]; + dst[ 2] = src[ 2]; + dst[ 3] = src[ 3]; + dst[ 4] = src[ 4]; + dst[ 5] = src[ 5]; + dst[ 6] = src[ 6]; + dst[ 7] = src[ 7]; + dst[ 8] = src[ 8]; + dst[ 9] = src[ 9]; + dst[10] = src[10]; + dst[11] = src[11]; + dst[12] = src[12]; + dst[13] = src[13]; + dst[14] = src[14]; + dst[15] = src[15]; + + // Program Timer B for the next band transition. + if (gBandCount > 1) { + delta = gBandStart[1] - gBandStart[0]; + if (delta == 0 || delta > 255) { + delta = 1; + } + *ST_MFP_TBCR = MFP_TBCR_STOP; + *ST_MFP_TBDR = (uint8_t)delta; + *ST_MFP_ISRA = MFP_TB_CLEAR; + *ST_MFP_TBCR = MFP_TBCR_EVENT; + } else { + *ST_MFP_TBCR = MFP_TBCR_STOP; + *ST_MFP_ISRA = MFP_TB_CLEAR; + } +} + + +static long writePrevPaletteRegs(void) { + uint16_t i; + for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + ST_PALETTE_REGS[i] = gPrevPalette[i]; + } + return 0; +} + + +static void writeDiagnostics(void) { + FILE *fp; + uint16_t i; + + fp = fopen("diag.txt", "w"); + if (fp == NULL) { + return; + } + fprintf(fp, "frames observed: %d\n", (int)gFrameCount); + fprintf(fp, "band count: %d\n", (int)gLastBandCount); + for (i = 0; i < gLastBandCount && i < 16; i++) { + fprintf(fp, " band %2d: start line %3d, palIdx %d\n", + (int)i, (int)gBandStart[i], (int)gBandPalIdx[i]); + } + fclose(fp); +} + + +// ----- HAL API (alphabetical) ----- + +bool halInit(const JoeyConfigT *config) { + uintptr_t addr; + + (void)config; + + // Align screen buffer to 256 bytes inside the static storage. + addr = (uintptr_t)gScreenBuffer; + addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1); + gScreenBase = (uint8_t *)addr; + + memset(gScreenBase, 0, SURFACE_PIXELS_SIZE); + + gPrevPhysbase = Physbase(); + gPrevLogbase = Logbase(); + gPrevRez = Getrez(); + + // Capture current palette so we can restore exactly on shutdown. + { + uint16_t i; + for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1); + } + } + + // Switch to ST low-res: 320x200x16, mode 0. + Setscreen((long)gScreenBase, (long)gScreenBase, 0); + gModeSet = true; + + // Save previous VBL + Timer B vectors, install ours. Timer B + // is at MFP vector $120; vector installed by Xbtimer below. + gOldVblVec = (void (*)(void))Setexc(VEC_VBL, -1L); + gOldTimerBVec = (void (*)(void))Setexc(VEC_MFP_TB, -1L); + (void)Setexc(VEC_VBL, (long)vblIsr); + + // Program MFP Timer B: event-count (HBL) mode, initial TBDR=1 + // (a placeholder -- VBL ISR reprograms it for the first real + // transition per frame), vector=timerBIsr, then enable in IMRA. + Xbtimer(1, MFP_TBCR_EVENT, 1, timerBIsr); + Jenabint(INT_TIMER_B); + + return true; +} + + +const char *halLastError(void) { + return NULL; +} + + +void halPresent(const SurfaceT *src) { + if (src == NULL || !gModeSet) { + return; + } + flattenScbPalettes(src); + buildTransitions(src); + c2pRange(src, 0, SURFACE_HEIGHT); +} + + +void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) { + (void)x; + (void)w; + + if (src == NULL || !gModeSet) { + return; + } + flattenScbPalettes(src); + buildTransitions(src); + c2pRange(src, y, y + (int16_t)h); +} + + +void halShutdown(void) { + if (!gModeSet) { + return; + } + + // Disable MFP Timer B and restore the exception vectors before + // changing the screen -- a late ISR firing mid-Setscreen would + // write palette into whatever buffer TOS remapped. + Jdisint(INT_TIMER_B); + if (gOldTimerBVec != NULL) { + (void)Setexc(VEC_MFP_TB, (long)gOldTimerBVec); + } + if (gOldVblVec != NULL) { + (void)Setexc(VEC_VBL, (long)gOldVblVec); + } + + Setscreen((long)gPrevLogbase, (long)gPrevPhysbase, gPrevRez); + Supexec(writePrevPaletteRegs); + writeDiagnostics(); + gModeSet = false; +} diff --git a/src/port/dos/hal.c b/src/port/dos/hal.c new file mode 100644 index 0000000..5352ab4 --- /dev/null +++ b/src/port/dos/hal.c @@ -0,0 +1,150 @@ +// DOS HAL: VGA mode 13h, 8-bit DAC, nibble-expand present. +// +// JoeyLib surfaces are 4bpp chunky with a 200-entry SCB table selecting +// one of 16 palettes per scanline. Mode 13h is 8bpp; the three pairs of +// operations map cleanly: +// * All 16 palettes x 16 colors = 256 entries fill the VGA DAC once. +// * Pixel byte = (scb[y] << 4) | (source nibble). +// * No per-scanline palette programming needed at display time. +// +// Access to VGA memory uses DJGPP's nearptr mechanism for speed. + +#include +#include + +#include +#include +#include +#include + +#include "hal.h" +#include "surfaceInternal.h" + +// ----- Constants ----- + +#define VGA_LINEAR_ADDR 0xA0000u +#define VGA_STRIDE 320u +#define DAC_INDEX_PORT 0x3C8 +#define DAC_DATA_PORT 0x3C9 + +// ----- Prototypes ----- + +static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst); +static void uploadPalette(const SurfaceT *src); + +// ----- Module state ----- + +static uint8_t *gVgaMem = NULL; +static bool gNearEnabled = false; + +// ----- Internal helpers (alphabetical) ----- + +static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) { + uint8_t palBase; + const uint8_t *srcBytes; + int16_t i; + int16_t xEnd; + uint8_t byte; + uint8_t nibble; + + palBase = (uint8_t)(src->scb[y] << 4); + srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW]; + xEnd = x + (int16_t)w; + + for (i = x; i < xEnd; i++) { + byte = srcBytes[i >> 1]; + nibble = (uint8_t)((i & 1) ? (byte & 0x0F) : (byte >> 4)); + dst[i] = (uint8_t)(palBase | nibble); + } +} + + +static void uploadPalette(const SurfaceT *src) { + uint16_t p; + uint16_t c; + uint16_t rgb; + uint8_t r; + uint8_t g; + uint8_t b; + + outportb(DAC_INDEX_PORT, 0); + for (p = 0; p < SURFACE_PALETTE_COUNT; p++) { + for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) { + rgb = src->palette[p][c]; + r = (uint8_t)((rgb >> 8) & 0x0F); + g = (uint8_t)((rgb >> 4) & 0x0F); + b = (uint8_t)(rgb & 0x0F); + outportb(DAC_DATA_PORT, (uint8_t)((r << 2) | (r >> 2))); + outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2))); + outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2))); + } + } +} + + +// ----- HAL API (alphabetical) ----- + +bool halInit(const JoeyConfigT *config) { + __dpmi_regs regs; + + (void)config; + + memset(®s, 0, sizeof(regs)); + regs.x.ax = 0x0013; + __dpmi_int(0x10, ®s); + + if (!__djgpp_nearptr_enable()) { + return false; + } + gNearEnabled = true; + gVgaMem = (uint8_t *)(__djgpp_conventional_base + VGA_LINEAR_ADDR); + return true; +} + + +const char *halLastError(void) { + return NULL; +} + + +void halPresent(const SurfaceT *src) { + int16_t y; + + if (src == NULL || gVgaMem == NULL) { + return; + } + uploadPalette(src); + for (y = 0; y < SURFACE_HEIGHT; y++) { + expandAndWriteLine(src, y, 0, SURFACE_WIDTH, &gVgaMem[y * VGA_STRIDE]); + } +} + + +void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) { + int16_t py; + int16_t yEnd; + + if (src == NULL || gVgaMem == NULL) { + return; + } + uploadPalette(src); + yEnd = y + (int16_t)h; + for (py = y; py < yEnd; py++) { + expandAndWriteLine(src, py, x, w, &gVgaMem[py * VGA_STRIDE]); + } +} + + +void halShutdown(void) { + __dpmi_regs regs; + + if (gNearEnabled) { + __djgpp_nearptr_disable(); + gNearEnabled = false; + } + gVgaMem = NULL; + + memset(®s, 0, sizeof(regs)); + regs.x.ax = 0x0003; + __dpmi_int(0x10, ®s); +} diff --git a/src/port/iigs/hal.c b/src/port/iigs/hal.c new file mode 100644 index 0000000..2cf693a --- /dev/null +++ b/src/port/iigs/hal.c @@ -0,0 +1,103 @@ +// Apple IIgs HAL: enable SHR, write pixels / SCBs / palettes into the +// $E1 bank at the stock addresses the shifter reads from. +// +// Memory map in bank $E1: +// $2000 - $9CFF pixel data (32000 bytes, 160 bytes per scanline) +// $9D00 - $9DC7 SCB bytes (200 used) +// $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB +// +// NEWVIDEO register at $00C029 controls SHR enable. Bit 7 turns SHR on. +// ORCA/C must be built with 32-bit pointer mode (-w or equivalent) so +// that the long addresses resolve to bank $E1. +// +// For M1 this is a simple direct-copy present. PEI-slam (in assembly) +// arrives as an optimization in a later milestone; the structure here +// is unchanged -- only halPresent / halPresentRect get faster inner +// loops. + +#include +#include + +#include "hal.h" +#include "surfaceInternal.h" + +// ----- Hardware addresses (24-bit / long pointers) ----- + +#define IIGS_NEWVIDEO_REG ((volatile uint8_t *)0x00C029L) +#define IIGS_SHR_PIXELS ((uint8_t *)0xE12000L) +#define IIGS_SHR_SCB ((uint8_t *)0xE19D00L) +#define IIGS_SHR_PALETTE ((uint16_t *)0xE19E00L) + +// NEWVIDEO bit masks +#define NEWVIDEO_SHR_ON 0x80 +#define NEWVIDEO_LINEARIZE 0x40 + +// ----- Module state ----- + +static uint8_t gPreviousNewVideo = 0; +static bool gModeSet = false; + +// ----- HAL API (alphabetical) ----- + +bool halInit(const JoeyConfigT *config) { + (void)config; + gPreviousNewVideo = *IIGS_NEWVIDEO_REG; + *IIGS_NEWVIDEO_REG = (uint8_t)(NEWVIDEO_SHR_ON | NEWVIDEO_LINEARIZE); + gModeSet = true; + return true; +} + + +const char *halLastError(void) { + return NULL; +} + + +void halPresent(const SurfaceT *src) { + if (src == NULL) { + return; + } + memcpy(IIGS_SHR_PIXELS, src->pixels, SURFACE_PIXELS_SIZE); + memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT); + memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette)); +} + + +void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) { + int16_t py; + int16_t yEnd; + uint16_t copyBytes; + int16_t byteStart; + + if (src == NULL) { + return; + } + + // SCBs and palettes track the whole surface, not just the rect -- + // cheap enough on IIgs (200 bytes + 512 bytes) and avoids tracking + // which palette/SCB regions changed. A future optimization can + // limit these to the affected scanlines. + memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT); + memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette)); + + // Pixel copy: byte-aligned runs per scanline. x is always even + // after API-level clipping for 4bpp packed if caller aligned it; + // otherwise we include the byte containing the leftmost pixel. + byteStart = x >> 1; + copyBytes = (uint16_t)(((x + (int16_t)w + 1) >> 1) - byteStart); + yEnd = y + (int16_t)h; + + for (py = y; py < yEnd; py++) { + memcpy(&IIGS_SHR_PIXELS[py * SURFACE_BYTES_PER_ROW + byteStart], + &src->pixels[py * SURFACE_BYTES_PER_ROW + byteStart], + copyBytes); + } +} + + +void halShutdown(void) { + if (gModeSet) { + *IIGS_NEWVIDEO_REG = gPreviousNewVideo; + gModeSet = false; + } +} diff --git a/toolchains/env.sh b/toolchains/env.sh new file mode 100644 index 0000000..e0b886c --- /dev/null +++ b/toolchains/env.sh @@ -0,0 +1,83 @@ +# JoeyLib toolchain environment. +# +# Source this file before invoking make: +# source toolchains/env.sh +# +# All build commands resolve cross-compilers and assemblers exclusively +# from toolchains/ regardless of what is installed on the host. The +# Makefiles reference these variables by name; never hardcode paths. + +JOEY_TOOLCHAINS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export JOEY_TOOLCHAINS + +# ------------------------------------------------------------------------ +# IIgs +# +# Two separate trees: +# - toolchains/iigs/gg-tools/bin/ Unix host binaries (iix, dumpobj, ...) +# built from the GoldenGate source repo. +# - toolchains/iigs/goldengate/ The IIgs filesystem iix emulates. +# $GOLDEN_GATE points here. +# +# iix is the host-side command shell that runs ORCA tools (cc, Linker, +# Asm65816, ...) via 65816 emulation against the $GOLDEN_GATE tree. +# ------------------------------------------------------------------------ + +export GOLDEN_GATE="${JOEY_TOOLCHAINS}/iigs/goldengate" +export ORCA_ROOT="${GOLDEN_GATE}" + +export IIGS_AS="${JOEY_TOOLCHAINS}/iigs/merlin32/bin/merlin32" +export IIGS_IIX="${JOEY_TOOLCHAINS}/iigs/gg-tools/bin/iix" +export IIGS_BUILD="${JOEY_TOOLCHAINS}/iigs/iix-build.sh" +export IIGS_INCLUDE_SHIM="${JOEY_TOOLCHAINS}/iigs/include-shim" +# Kept as an alias for IIGS_IIX so the top-level Makefile's HAVE_IIGS +# availability probe (`[ -x $IIGS_CC ]`) works with the common pattern +# used for the other platforms. +export IIGS_CC="${IIGS_IIX}" + +# ------------------------------------------------------------------------ +# Amiga +# ------------------------------------------------------------------------ + +export AMIGA_PREFIX="${JOEY_TOOLCHAINS}/amiga/gcc" +export AMIGA_CC="${AMIGA_PREFIX}/bin/m68k-amigaos-gcc" +export AMIGA_AR="${AMIGA_PREFIX}/bin/m68k-amigaos-ar" +export AMIGA_LD="${AMIGA_PREFIX}/bin/m68k-amigaos-ld" +export AMIGA_AS="${JOEY_TOOLCHAINS}/amiga/vasm/bin/vasmm68k_mot" +export AMIGA_NDK="${AMIGA_PREFIX}/m68k-amigaos/sys-include" +export AMIGA_PTPLAYER="${JOEY_TOOLCHAINS}/amiga/ptplayer" + +# ------------------------------------------------------------------------ +# Atari ST +# ------------------------------------------------------------------------ + +export ST_PREFIX="${JOEY_TOOLCHAINS}/atarist/gcc" +export ST_CC="${ST_PREFIX}/bin/m68k-atari-mint-gcc" +export ST_AR="${ST_PREFIX}/bin/m68k-atari-mint-ar" +export ST_LD="${ST_PREFIX}/bin/m68k-atari-mint-ld" +export ST_AS="${JOEY_TOOLCHAINS}/atarist/vasm/bin/vasmm68k_mot" + +# ------------------------------------------------------------------------ +# DOS +# ------------------------------------------------------------------------ + +export DOS_PREFIX="${JOEY_TOOLCHAINS}/dos/djgpp" +export DOS_CC="${DOS_PREFIX}/bin/i586-pc-msdosdjgpp-gcc" +export DOS_AR="${DOS_PREFIX}/bin/i586-pc-msdosdjgpp-ar" +export DOS_LD="${DOS_PREFIX}/bin/i586-pc-msdosdjgpp-ld" +# DJGPP's DOS-target utilities live under the i586-...-msdosdjgpp +# sysroot bin, not the host bin that holds the cross-gcc. +export DOS_STUBEDIT="${DOS_PREFIX}/i586-pc-msdosdjgpp/bin/stubedit" +export DOS_STUBIFY="${DOS_PREFIX}/i586-pc-msdosdjgpp/bin/stubify" +export DOS_EXE2COFF="${DOS_PREFIX}/i586-pc-msdosdjgpp/bin/exe2coff" +export DOS_AS="${JOEY_TOOLCHAINS}/dos/nasm/bin/nasm" +export DOS_CWSDPMI="${JOEY_TOOLCHAINS}/dos/cwsdpmi/bin/bin" +export DOS_CWSDSTUB="${DOS_CWSDPMI}/CWSDSTUB.EXE" +export DOS_EMBED_DPMI="${JOEY_TOOLCHAINS}/dos/embed-dpmi.sh" + +# ------------------------------------------------------------------------ +# PATH augmentation (in case sub-tools shell out unprefixed names) +# ------------------------------------------------------------------------ + +PATH="${AMIGA_PREFIX}/bin:${ST_PREFIX}/bin:${DOS_PREFIX}/bin:${JOEY_TOOLCHAINS}/iigs/merlin32/bin:${JOEY_TOOLCHAINS}/amiga/vasm/bin:${JOEY_TOOLCHAINS}/dos/nasm/bin:${PATH}" +export PATH diff --git a/toolchains/install.sh b/toolchains/install.sh new file mode 100755 index 0000000..11f3762 --- /dev/null +++ b/toolchains/install.sh @@ -0,0 +1,1298 @@ +#!/usr/bin/env bash +# +# JoeyLib toolchain installer. +# +# Installs all cross-compilers, assemblers, and libraries needed to build +# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local +# toolchains/ folder. Nothing is installed system-wide. Free tools are +# fetched and built; non-free tools (ORCA/C, GoldenGate) print clear +# placement instructions and the script verifies they were placed +# correctly on a subsequent run. +# +# Usage: +# ./toolchains/install.sh install everything we can get +# ./toolchains/install.sh --only iigs install only one platform +# ./toolchains/install.sh --force wipe and reinstall +# ./toolchains/install.sh --skip-emulators skip emulators (cross tools only) +# ./toolchains/install.sh --help show this message +# +# Emulators: dosbox, fs-uae (Amiga), hatari (Atari ST), GSplus (Apple IIgs). +# On Linux with apt, the first three are installed via 'sudo apt install'. +# On macOS with Homebrew, they are installed via 'brew install'. GSplus is +# always built from source into toolchains/emulators/gsplus/. +# + +set -euo pipefail + +# Never prompt for git credentials. Missing / private GitHub repos +# otherwise ask for a username/password on HTTPS clones; we want those +# to fail fast so the installer reports clear instructions instead. +export GIT_TERMINAL_PROMPT=0 +export GIT_ASKPASS=/bin/true + +# ------------------------------------------------------------------------ +# Paths and globals +# ------------------------------------------------------------------------ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +CACHE_DIR="${SCRIPT_DIR}/cache" +STATE_DIR="${SCRIPT_DIR}/.install_state" + +ONLY_PLATFORM="" +FORCE_INSTALL=0 +SKIP_EMULATORS=0 + +# Status tracking. Keys are component identifiers; values are "ok", +# "missing", or "failed". +declare -A STATUS +declare -A INSTRUCTIONS + +# ------------------------------------------------------------------------ +# Output helpers (ANSI color when stdout is a TTY) +# ------------------------------------------------------------------------ + +if [ -t 1 ]; then + C_RESET=$'\033[0m' + C_BOLD=$'\033[1m' + C_RED=$'\033[31m' + C_GREEN=$'\033[32m' + C_YELLOW=$'\033[33m' + C_CYAN=$'\033[36m' +else + C_RESET="" + C_BOLD="" + C_RED="" + C_GREEN="" + C_YELLOW="" + C_CYAN="" +fi + +info() { printf '%s[*]%s %s\n' "${C_CYAN}" "${C_RESET}" "$*"; } +ok() { printf '%s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$*"; } +warn() { printf '%s[!!]%s %s\n' "${C_YELLOW}" "${C_RESET}" "$*"; } +err() { printf '%s[xx]%s %s\n' "${C_RED}" "${C_RESET}" "$*" 1>&2; } +header() { printf '\n%s== %s ==%s\n' "${C_BOLD}" "$*" "${C_RESET}"; } + +# ------------------------------------------------------------------------ +# Utility functions +# ------------------------------------------------------------------------ + +usage() { + sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + exit 0 +} + +require_tool() { + local tool="$1" + if ! command -v "${tool}" >/dev/null 2>&1; then + err "Required host tool not found: ${tool}" + err "Install it via your system package manager and re-run." + exit 1 + fi +} + +detect_host_os() { + case "$(uname -s)" in + Linux*) HOST_OS="linux" ;; + Darwin*) HOST_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) + HOST_OS="windows" ;; + *) + err "Unsupported host OS: $(uname -s)" + exit 1 + ;; + esac + info "Host OS: ${HOST_OS}" +} + +ensure_dirs() { + mkdir -p "${CACHE_DIR}" "${STATE_DIR}" +} + +mark_done() { + touch "${STATE_DIR}/$1.done" +} + +is_done() { + [ -f "${STATE_DIR}/$1.done" ] +} + +clear_done() { + rm -f "${STATE_DIR}/$1.done" +} + +download() { + local url="$1" + local dest="$2" + if [ -f "${dest}" ]; then + info "Cached: $(basename "${dest}")" + return 0 + fi + info "Downloading $(basename "${dest}")" + if command -v curl >/dev/null 2>&1; then + curl -fL --retry 3 -o "${dest}" "${url}" + elif command -v wget >/dev/null 2>&1; then + wget -O "${dest}" "${url}" + else + err "Neither curl nor wget is installed" + return 1 + fi +} + +# ------------------------------------------------------------------------ +# Argument parsing +# ------------------------------------------------------------------------ + +while [ $# -gt 0 ]; do + case "$1" in + --only) + ONLY_PLATFORM="$2" + shift 2 + ;; + --force) + FORCE_INSTALL=1 + shift + ;; + --skip-emulators) + SKIP_EMULATORS=1 + shift + ;; + --help|-h) + usage + ;; + *) + err "Unknown argument: $1" + usage + ;; + esac +done + +should_install() { + local platform="$1" + if [ -z "${ONLY_PLATFORM}" ]; then + return 0 + fi + [ "${ONLY_PLATFORM}" = "${platform}" ] +} + +# ------------------------------------------------------------------------ +# IIgs: Merlin32 (free), ORCA/C (manual), GoldenGate (manual) +# ------------------------------------------------------------------------ + +install_iigs() { + header "IIgs toolchain" + local base="${SCRIPT_DIR}/iigs" + mkdir -p "${base}" + + # Merlin32 -- 65816 cross-assembler from Brutal Deluxe. + # The canonical distribution is a zip on brutaldeluxe.fr that bundles + # prebuilt binaries for Linux / macOS / Windows plus the C source. + # We prefer the host's prebuilt binary; if not present, we build + # from source with plain gcc (the source has no Makefile). + if [ "${FORCE_INSTALL}" -eq 1 ]; then + rm -rf "${base}/merlin32" + clear_done "iigs_merlin32" + fi + + if is_done "iigs_merlin32" && [ -x "${base}/merlin32/bin/merlin32" ]; then + ok "Merlin32 already installed" + STATUS[iigs_merlin32]="ok" + else + local merlin_installed=0 + local merlin_urls=( + "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32_v1.1.zip" + "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32.zip" + ) + local merlin_zip="${CACHE_DIR}/merlin32.zip" + local merlin_extract="${CACHE_DIR}/merlin32-extract" + local prebuilt_subdir="" + + case "${HOST_OS}" in + linux) prebuilt_subdir="Linux" ;; + macos) prebuilt_subdir="MacOs" ;; + *) prebuilt_subdir="" ;; + esac + + for url in "${merlin_urls[@]}"; do + if download "${url}" "${merlin_zip}" 2>/dev/null; then + info "Extracting Merlin32" + rm -rf "${merlin_extract}" + mkdir -p "${merlin_extract}" + if ! unzip -o "${merlin_zip}" -d "${merlin_extract}" >/dev/null 2>&1; then + continue + fi + mkdir -p "${base}/merlin32/bin" + + # 1. Try prebuilt binary for the host OS. Name may be + # capitalized ("Merlin32") or lowercase. No executable + # bit in the zip, so we chmod after copying. + local mbin="" + if [ -n "${prebuilt_subdir}" ]; then + mbin=$(find "${merlin_extract}" -type d -name "${prebuilt_subdir}" -print 2>/dev/null | head -n1) + if [ -n "${mbin}" ]; then + mbin=$(find "${mbin}" -maxdepth 1 -type f -iname "merlin32" 2>/dev/null | head -n1) + fi + fi + + if [ -n "${mbin}" ] && [ -f "${mbin}" ]; then + cp "${mbin}" "${base}/merlin32/bin/merlin32" + chmod +x "${base}/merlin32/bin/merlin32" + mark_done "iigs_merlin32" + ok "Merlin32 installed (prebuilt ${prebuilt_subdir} binary)" + STATUS[iigs_merlin32]="ok" + merlin_installed=1 + break + fi + + # 2. No prebuilt for this host; build from Source/. The + # source has no Makefile; 'gcc -O2 *.c' produces the + # binary directly. + local msrc + msrc=$(find "${merlin_extract}" -type d -name "Source" 2>/dev/null | head -n1) + if [ -n "${msrc}" ] && [ -f "${msrc}/Main.c" ]; then + info "Building Merlin32 from source" + ( + cd "${msrc}" + gcc -O2 -o merlin32 *.c + ) && { + if [ -x "${msrc}/merlin32" ]; then + cp "${msrc}/merlin32" "${base}/merlin32/bin/merlin32" + mark_done "iigs_merlin32" + ok "Merlin32 built from source and installed" + STATUS[iigs_merlin32]="ok" + merlin_installed=1 + break + fi + } + fi + fi + done + + if [ "${merlin_installed}" -eq 0 ]; then + STATUS[iigs_merlin32]="missing" + INSTRUCTIONS[iigs_merlin32]=$(cat </dev/null || true + fi + + # Clean up any dangling symlinks that reference a now-moved iix. + if [ -d "${gg_root}/bin" ]; then + find "${gg_root}/bin" -maxdepth 1 -type l -xtype l -delete 2>/dev/null || true + fi + + # Recreate gno/orca symlinks next to iix in gg-tools if they're + # missing (iix behaves differently when invoked as 'gno' or 'orca'). + if [ -x "${gg_tools_dir}/bin/iix" ]; then + for alias in gno orca; do + if [ ! -e "${gg_tools_dir}/bin/${alias}" ]; then + ln -s iix "${gg_tools_dir}/bin/${alias}" + fi + done + fi + + # Verify Unix tools + if [ -x "${gg_tools_dir}/bin/iix" ]; then + ok "GoldenGate host tools present at ${gg_tools_dir}/bin/" + STATUS[iigs_gg_tools]="ok" + else + STATUS[iigs_gg_tools]="missing" + INSTRUCTIONS[iigs_gg_tools]=$(cat </dev/null || { + STATUS[iigs_goldengate]="failed" + INSTRUCTIONS[iigs_goldengate]="Extract of ${gg_linux_tgz} failed" + } + + # Extract fix-filetypes.sh separately (it sits at gg-linux/ root). + if [ ! -f "${CACHE_DIR}/fix-filetypes.sh" ]; then + tar -xzf "${gg_linux_tgz}" -C "${CACHE_DIR}" --strip-components=1 \ + gg-linux/fix-filetypes.sh 2>/dev/null || true + fi + + # FST tarball: merge fst/orca/ subtree into goldengate/. + if [ -f "${fst_tgz}" ]; then + info "Merging fst.tgz into goldengate/System/" + tar -xzf "${fst_tgz}" -C "${gg_root}" --strip-components=2 \ + fst/orca/ 2>/dev/null || warn "fst.tgz extract reported issues" + else + warn "${fst_tgz} not found; FSTs will be missing" + fi + + if [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then + mark_done "iigs_goldengate" + ok "GoldenGate IIgs filesystem populated" + STATUS[iigs_goldengate]="ok" + else + STATUS[iigs_goldengate]="failed" + INSTRUCTIONS[iigs_goldengate]="goldengate/ did not end up with System/rom and bin/; check ${gg_linux_tgz}" + fi + fi + fi + + # Merge ORCA (from Opus II) directories into goldengate/. + if is_done "iigs_orca" && [ -f "${gg_root}/Languages/cc" ]; then + ok "ORCA/C merged into goldengate/" + STATUS[iigs_orca]="ok" + else + if [ "${STATUS[iigs_goldengate]}" != "ok" ]; then + STATUS[iigs_orca]="missing" + INSTRUCTIONS[iigs_orca]="Install GoldenGate IIgs filesystem first." + elif [ ! -d "${opus_dir}" ]; then + STATUS[iigs_orca]="missing" + INSTRUCTIONS[iigs_orca]=$(cat </dev/null || orca_ok=0 + else + warn "${opus_dir}/${sub} missing" + orca_ok=0 + fi + done + + # Run fix-filetypes.sh which sets IIgs filetype xattrs via chtyp. + # chtyp is an IIgs binary inside goldengate/bin and is invoked + # by fix-filetypes through iix, so we need iix on PATH and + # GOLDEN_GATE exported first. + if [ "${orca_ok}" -eq 1 ] && [ -f "${CACHE_DIR}/fix-filetypes.sh" ] \ + && [ -x "${gg_tools_dir}/bin/iix" ]; then + info "Running fix-filetypes.sh" + ( + cd "${gg_root}" + export GOLDEN_GATE="${gg_root}" + export PATH="${gg_tools_dir}/bin:${PATH}" + bash "${CACHE_DIR}/fix-filetypes.sh" >/dev/null 2>&1 + ) || warn "fix-filetypes.sh reported issues; proceeding" + fi + + if [ "${orca_ok}" -eq 1 ] && [ -f "${gg_root}/Languages/cc" ]; then + mark_done "iigs_orca" + ok "ORCA/C merged and filetypes fixed" + STATUS[iigs_orca]="ok" + else + STATUS[iigs_orca]="failed" + INSTRUCTIONS[iigs_orca]="ORCA merge did not produce ${gg_root}/Languages/cc; check ${opus_dir}/" + fi + fi + fi +} + +# ------------------------------------------------------------------------ +# Amiga: Bebbo m68k-amigaos-gcc, vasm, NDK, PTPlayer +# ------------------------------------------------------------------------ + +install_amiga() { + header "Amiga toolchain" + local base="${SCRIPT_DIR}/amiga" + mkdir -p "${base}" + + # Bebbo amiga-gcc -- build from source; long but reliable + if [ "${FORCE_INSTALL}" -eq 1 ]; then + rm -rf "${base}/gcc" + clear_done "amiga_gcc" + fi + + if is_done "amiga_gcc" && [ -x "${base}/gcc/bin/m68k-amigaos-gcc" ]; then + ok "m68k-amigaos-gcc already installed" + STATUS[amiga_gcc]="ok" + else + ensure_build_deps "m68k-amigaos-gcc" \ + "build-essential bison flex m4 texinfo libgmp-dev libmpfr-dev libmpc-dev autoconf automake lhasa" \ + "bison flex m4 texinfo gmp mpfr libmpc autoconf automake lhasa" || true + info "Cloning AmigaPorts/m68k-amigaos-gcc (this build takes a while)" + local src="${CACHE_DIR}/amiga-gcc-src" + if [ ! -d "${src}" ]; then + if ! git clone https://github.com/AmigaPorts/m68k-amigaos-gcc.git "${src}"; then + STATUS[amiga_gcc]="failed" + INSTRUCTIONS[amiga_gcc]=$(cat </dev/null || true + ) && { + mark_done "vasm" + ok "vasm installed" + STATUS[vasm]="ok" + } || { + STATUS[vasm]="failed" + INSTRUCTIONS[vasm]="vasm build failed; see ${vasm_src}" + } + fi + + # NDK -- AmigaOS includes (Bebbo's repo bundles these via 'make all') + if [ -d "${base}/gcc/m68k-amigaos/sys-include" ] || [ -d "${base}/gcc/m68k-amigaos/include" ]; then + ok "Amiga NDK headers present (bundled with amiga-gcc)" + STATUS[amiga_ndk]="ok" + else + STATUS[amiga_ndk]="missing" + INSTRUCTIONS[amiga_ndk]="Amiga NDK headers should be installed by Bebbo's amiga-gcc 'make all'. Re-run installer after fixing amiga-gcc." + fi + + # PTPlayer -- MOD replayer by Frank Wille, distributed as an LHA + # archive on Aminet. Extract with lhasa (installed as part of the + # Amiga GCC build deps above). + if [ "${FORCE_INSTALL}" -eq 1 ]; then + rm -rf "${base}/ptplayer" + clear_done "amiga_ptplayer" + fi + + if is_done "amiga_ptplayer" && [ -f "${base}/ptplayer/ptplayer.asm" ]; then + ok "PTPlayer already installed" + STATUS[amiga_ptplayer]="ok" + else + info "Downloading PTPlayer from Aminet" + local ptp_lha="${CACHE_DIR}/ptplayer.lha" + if ! download "http://aminet.net/mus/play/ptplayer.lha" "${ptp_lha}"; then + STATUS[amiga_ptplayer]="failed" + INSTRUCTIONS[amiga_ptplayer]="Could not download http://aminet.net/mus/play/ptplayer.lha" + else + local extractor="" + if command -v lhasa >/dev/null 2>&1; then + extractor="lhasa" + elif command -v lha >/dev/null 2>&1; then + extractor="lha" + fi + + if [ -z "${extractor}" ]; then + STATUS[amiga_ptplayer]="missing" + INSTRUCTIONS[amiga_ptplayer]=$(cat < amiga/vasm" + STATUS[atarist_vasm]="ok" + else + STATUS[atarist_vasm]="missing" + INSTRUCTIONS[atarist_vasm]="vasm not yet installed for Amiga; install Amiga toolchain first." + fi + fi +} + +# ------------------------------------------------------------------------ +# DOS: DJGPP, NASM, CWSDPMI +# ------------------------------------------------------------------------ + +install_dos() { + header "DOS toolchain" + local base="${SCRIPT_DIR}/dos" + mkdir -p "${base}" + + # DJGPP -- the canonical zip set + if [ "${FORCE_INSTALL}" -eq 1 ]; then + rm -rf "${base}/djgpp" + clear_done "dos_djgpp" + fi + + if is_done "dos_djgpp" && [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then + ok "DJGPP already installed" + STATUS[dos_djgpp]="ok" + else + # Use prebuilt DJGPP binaries from andrewwutw/build-djgpp + # releases. Much faster than source build and avoids a long list + # of build-dep prerequisites. + info "Downloading DJGPP prebuilt binaries" + local djgpp_arch="" + local djgpp_url="" + case "${HOST_OS}" in + linux) + if [ "$(uname -m)" = "x86_64" ]; then + djgpp_arch="linux64" + else + djgpp_arch="linux32" + fi + ;; + macos) + djgpp_arch="osx" + ;; + esac + + if [ -n "${djgpp_arch}" ]; then + djgpp_url="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-${djgpp_arch}-gcc1220.tar.bz2" + fi + + local djgpp_tar="${CACHE_DIR}/djgpp.tar.bz2" + if [ -n "${djgpp_url}" ] && download "${djgpp_url}" "${djgpp_tar}" 2>/dev/null; then + mkdir -p "${base}/djgpp" + # The tarball extracts to a 'djgpp' directory at the top level. + # Strip one component so contents land directly in ${base}/djgpp. + tar -xjf "${djgpp_tar}" -C "${base}/djgpp" --strip-components=1 && { + if [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then + mark_done "dos_djgpp" + ok "DJGPP installed (prebuilt ${djgpp_arch})" + STATUS[dos_djgpp]="ok" + else + STATUS[dos_djgpp]="failed" + INSTRUCTIONS[dos_djgpp]="DJGPP tarball extracted but i586-pc-msdosdjgpp-gcc not found under ${base}/djgpp/bin/" + fi + } || { + STATUS[dos_djgpp]="failed" + INSTRUCTIONS[dos_djgpp]="Failed to extract DJGPP tarball ${djgpp_tar}" + } + else + STATUS[dos_djgpp]="failed" + INSTRUCTIONS[dos_djgpp]=$(cat </dev/null) && { + ok "CWSDPMI installed" + STATUS[dos_cwsdpmi]="ok" + } || { + STATUS[dos_cwsdpmi]="failed" + } + fi +} + +# ------------------------------------------------------------------------ +# Emulators: DOSBox, FS-UAE, Hatari, GSplus +# +# Package-manager-installable emulators (DOSBox, FS-UAE, Hatari) are +# installed via apt on Linux / brew on macOS and exist on the host PATH +# after install. GSplus has no package and is always built from source +# into toolchains/emulators/gsplus/. +# ------------------------------------------------------------------------ + +pkg_install() { + # Install a list of package names on the current host. Returns 0 on + # success, non-zero on any failure. Uses apt on Linux, brew on macOS. + case "${HOST_OS}" in + linux) + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get install -y "$@" + return $? + fi + ;; + macos) + if command -v brew >/dev/null 2>&1; then + brew install "$@" + return $? + fi + ;; + esac + return 1 +} + +# Install build dependencies. Args: label, apt-list, brew-list. +# The label identifies what we're about to build; the lists are the +# space-separated package names for each package manager. On failure, +# prints instructions but does not abort the installer (the build step +# will then fail with a clearer message). +ensure_build_deps() { + local label="$1" + local apt_pkgs="$2" + local brew_pkgs="$3" + local pkgs="" + + case "${HOST_OS}" in + linux) pkgs="${apt_pkgs}" ;; + macos) pkgs="${brew_pkgs}" ;; + esac + + if [ -z "${pkgs}" ]; then + return 0 + fi + + info "Installing ${label} build dependencies" + if ! pkg_install ${pkgs}; then + warn "Failed to install ${label} dependencies via package manager." + warn "You may need to install manually: ${pkgs}" + return 1 + fi + return 0 +} + +install_apt_emulator() { + local key="$1" + local cmd="$2" + local aptpkg="$3" + local brewpkg="$4" + + if command -v "${cmd}" >/dev/null 2>&1; then + ok "${cmd} already on PATH" + STATUS[${key}]="ok" + return + fi + + info "Installing ${cmd} via host package manager" + local pkgs + case "${HOST_OS}" in + linux) pkgs="${aptpkg}" ;; + macos) pkgs="${brewpkg}" ;; + *) pkgs="" ;; + esac + + if [ -n "${pkgs}" ] && pkg_install ${pkgs}; then + ok "${cmd} installed" + STATUS[${key}]="ok" + else + STATUS[${key}]="missing" + INSTRUCTIONS[${key}]=$(cat </dev/null 2>&1; then + ok "gsplus already on PATH" + STATUS[emu_gsplus]="ok" + return + fi + + if is_done "emu_gsplus" && [ -x "${bin}" ]; then + ok "GSplus already built at ${bin}" + STATUS[emu_gsplus]="ok" + return + fi + + ensure_build_deps "GSplus" \ + "build-essential libx11-dev libxext-dev libpulse-dev libpcap-dev libfreetype6-dev" \ + "libpcap freetype pulseaudio" || true + + info "Building GSplus from source (Apple IIgs emulator)" + local src="${CACHE_DIR}/gsplus-src" + if [ ! -d "${src}" ]; then + if ! git clone --depth 1 https://github.com/digarok/gsplus.git "${src}"; then + STATUS[emu_gsplus]="failed" + INSTRUCTIONS[emu_gsplus]="git clone https://github.com/digarok/gsplus.git failed" + return + fi + fi + + # GSplus build layout: source lives at gsplus/src/ and expects one + # of the vars_* config files to be copied into 'vars' before make. + # On Linux we use the X11/PulseAudio build; on macOS we use the + # Mac X11 build for command-line use (the Swift/clang .app build + # needs Xcode and is skipped here). + local gsplus_src_dir="${src}/gsplus/src" + local gsplus_target="" + local vars_file="" + case "${HOST_OS}" in + linux) + vars_file="vars_x86linux" + gsplus_target="gsplus-x" + ;; + macos) + vars_file="vars_mac_x" + gsplus_target="gsplus-x" + ;; + esac + + if [ -z "${vars_file}" ] || [ ! -f "${gsplus_src_dir}/${vars_file}" ]; then + STATUS[emu_gsplus]="failed" + INSTRUCTIONS[emu_gsplus]=$(cat </dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ') +Copy the appropriate vars_* file to 'vars' and run make manually. +EOF +) + return + fi + + ( + cd "${gsplus_src_dir}" + cp "${vars_file}" vars + make clean >/dev/null 2>&1 || true + make "${gsplus_target}" + ) && { + # Built binary lands at gsplus/src/../gsplus per the Makefile + # (it's moved up one level after linking). + local gsbin="${src}/gsplus/gsplus" + if [ ! -f "${gsbin}" ]; then + # Fallback: find anywhere under the tree + gsbin=$(find "${src}" -maxdepth 4 -name gsplus -type f 2>/dev/null | head -n1) + fi + if [ -n "${gsbin}" ] && [ -f "${gsbin}" ]; then + mkdir -p "${base}/bin" + cp "${gsbin}" "${base}/bin/gsplus" + chmod +x "${base}/bin/gsplus" + # Also copy the support files GSplus reads at runtime + if [ -d "${src}/gsplus/lib" ]; then + mkdir -p "${base}/lib" + cp -r "${src}/gsplus/lib/." "${base}/lib/" + fi + mark_done "emu_gsplus" + ok "GSplus built and installed" + STATUS[emu_gsplus]="ok" + else + STATUS[emu_gsplus]="failed" + INSTRUCTIONS[emu_gsplus]="GSplus build succeeded but binary not found; check ${src}" + fi + } || { + STATUS[emu_gsplus]="failed" + INSTRUCTIONS[emu_gsplus]=$(cat <