Basic rendering working.

This commit is contained in:
Scott Duensing 2026-04-24 15:30:13 -05:00
commit b3d9961e78
37 changed files with 4284 additions and 0 deletions

53
.gitignore vendored Normal file
View file

@ -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/*

73
Makefile Normal file
View file

@ -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."

62
README.md Normal file
View file

@ -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 <repo> 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/<plat>/ 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/<plat>/ per-target build outputs
```
## License
TBD.

29
examples/hello/hello.c Normal file
View file

@ -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 <stdio.h>
#include <joey/joey.h>
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;
}

147
examples/pattern/pattern.c Normal file
View file

@ -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 <stdio.h>
#include <time.h>
#include <joey/joey.h>
#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;
}

33
include/joey/core.h Normal file
View file

@ -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

28
include/joey/draw.h Normal file
View file

@ -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

17
include/joey/joey.h Normal file
View file

@ -0,0 +1,17 @@
// JoeyLib umbrella header.
//
// Game code includes this single file:
// #include <joey/joey.h>
#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

31
include/joey/palette.h Normal file
View file

@ -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

73
include/joey/platform.h Normal file
View file

@ -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_<TARGET> 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

23
include/joey/present.h Normal file
View file

@ -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

46
include/joey/surface.h Normal file
View file

@ -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

20
include/joey/types.h Normal file
View file

@ -0,0 +1,20 @@
// JoeyLib shared types and enums.
#ifndef JOEYLIB_TYPES_H
#define JOEYLIB_TYPES_H
#include <stdint.h>
#include <stdbool.h>
typedef enum {
HOST_MODE_TAKEOVER,
HOST_MODE_OS
} HostModeE;
typedef enum {
VIDEO_REGION_NTSC,
VIDEO_REGION_PAL,
VIDEO_REGION_VGA
} VideoRegionE;
#endif

67
make/amiga.mk Normal file
View file

@ -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)

63
make/atarist.mk Normal file
View file

@ -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)

18
make/common.mk Normal file
View file

@ -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

59
make/dos.mk Normal file
View file

@ -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)

60
make/iigs.mk Normal file
View file

@ -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)

110
scripts/run-amiga.sh Executable file
View file

@ -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 <<EOF
FS-UAE booting Workbench from DF0:. Once the desktop appears:
1. Double-click the JOEYLIB disk icon.
2. From the Window menu, pick "Show > 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 <<EOF
FS-UAE booting directly from the JOEYLIB hard drive. $file will
auto-run via s/startup-sequence. Drop a Workbench 3.1 ADF at
$workbench to boot the GUI instead.
EOF
fi
# Intentionally no exec: we need the bash EXIT trap to fire so the
# scratch dir gets cleaned up (and any diagnostic dumps preserved).
# exec would replace the shell and skip trap execution.
fs-uae "${fsargs[@]}"

40
scripts/run-atarist.sh Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Launch the built Atari ST example in Hatari. Defaults to PATTERN;
# pass "hello" to run HELLO instead. Hatari autostarts the .PRG via
# the --auto flag; EmuTOS (toolchains/emulators/support/emutos-512k.img)
# provides the TOS ROM since the Ubuntu hatari package does not bundle
# one.
#
# scripts/run-atarist.sh # runs PATTERN.PRG
# scripts/run-atarist.sh hello # runs HELLO.PRG
set -euo pipefail
prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/atarist/bin
case $prog in
hello) file=HELLO.PRG ;;
pattern) file=PATTERN.PRG ;;
*) echo "usage: $0 [hello|pattern]" >&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"

25
scripts/run-dos.sh Executable file
View file

@ -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"

65
scripts/run-iigs.sh Executable file
View file

@ -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 <<EOF
GSplus launching GS/OS 6.0.4.
Once Finder is up:
1. Open the JOEYLIB disk on the desktop.
2. Double-click $target to run.
EOF
# GSplus auto-creates config.kegs in its cwd on first run. cd into
# the scratch dir so that file lands there and gets cleaned up with
# the rest of the temp state, rather than polluting the directory
# the user invoked the script from.
cd "$work"
# No exec: let the bash EXIT trap fire so the scratch dir (and the
# config.kegs GSplus auto-creates) gets cleaned up.
"$gsplus" -rom "$rom" -s5d1 "$work/boot.po" -s5d2 "$work/joey.2mg"

155
src/core/draw.c Normal file
View file

@ -0,0 +1,155 @@
// Drawing primitives on chunky 4bpp packed surfaces.
//
// Byte layout: two pixels per byte, high nibble is the LEFT pixel.
// Clipping is handled at the C API boundary so ASM inner loops (future)
// can run with branch-free, pre-validated parameters.
#include <stddef.h>
#include <string.h>
#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);
}

37
src/core/hal.h Normal file
View file

@ -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/<platform>/. It is NOT part of the public API and must not
// be installed or exposed to game code.
//
// Each port must implement every function declared here.
#ifndef JOEYLIB_HAL_H
#define JOEYLIB_HAL_H
#include "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

94
src/core/init.c Normal file
View file

@ -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 <stddef.h>
#include <string.h>
#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;
}

40
src/core/palette.c Normal file
View file

@ -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 <stddef.h>
#include <string.h>
#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;
}
}

63
src/core/present.c Normal file
View file

@ -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 <stddef.h>
#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);
}

57
src/core/scb.c Normal file
View file

@ -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 <stddef.h>
#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;
}
}

68
src/core/surface.c Normal file
View file

@ -0,0 +1,68 @@
// Surface allocation, destruction, and the library-owned screen surface.
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View file

@ -0,0 +1,21 @@
// Internal surface definition shared across core and port code.
#ifndef JOEYLIB_SURFACE_INTERNAL_H
#define JOEYLIB_SURFACE_INTERNAL_H
#include <stdint.h>
#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

411
src/port/amiga/hal.c Normal file
View file

@ -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 <stddef.h>
#include <stdio.h>
#include <string.h>
#include <exec/types.h>
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <graphics/copper.h>
#include <graphics/gfxbase.h>
#include <graphics/gfxmacros.h>
#include <graphics/displayinfo.h>
#include <graphics/modeid.h>
#include <graphics/rastport.h>
#include <graphics/view.h>
#include <hardware/custom.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#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;
}
}

83
src/port/amiga/libinit.c Normal file
View file

@ -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
// "<name> failed to load" to the CLI and exits(20) before main() runs.
//
// That behavior kills the examples on AROS, which does not provide
// locale.library as a disk library. libnix unconditionally pulls the
// locale.library entry into the auto-open list because C runtime
// utilities (e.g. tzset via time()) take LocaleBase as an argument to
// ConvToLower -- even when the runtime does not actually need a real
// Locale to run.
//
// 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 <proto/exec.h>
#include <stabs.h>
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) {
}

479
src/port/atarist/hal.c Normal file
View file

@ -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 <stddef.h>
#include <stdio.h>
#include <string.h>
#include <mint/osbind.h>
#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;
}

150
src/port/dos/hal.c Normal file
View file

@ -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 <stddef.h>
#include <string.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <sys/nearptr.h>
#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(&regs, 0, sizeof(regs));
regs.x.ax = 0x0013;
__dpmi_int(0x10, &regs);
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(&regs, 0, sizeof(regs));
regs.x.ax = 0x0003;
__dpmi_int(0x10, &regs);
}

103
src/port/iigs/hal.c Normal file
View file

@ -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 <stddef.h>
#include <string.h>
#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;
}
}

83
toolchains/env.sh Normal file
View file

@ -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

1298
toolchains/install.sh Executable file

File diff suppressed because it is too large Load diff