Skip to content

Repository files navigation

Rigel

Rigel is a C library for classic Amiga chipset and peripheral emulation.

The library provides a deterministic, single-threaded hardware-facing core. The host owns the CPU, memory map, ROM, and presentation layer; Rigel owns the chipset behavior.

Build

cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
./build/test_blitter       # run a single test

Optional: the Musashi integration harness — a real 68k machine around Rigel, used for timing verification and for booting Kickstart against the chipset.

./run.sh

That fetches and patches Musashi, builds the harness and the launcher, and opens a TUI to pick the ROM, the disk and the machine options. Drop images into media/roms and media/disks (created on first run), or point MEDIA_DIR somewhere else.

./run.sh --headless --frames 600 --screenshot boot.ppm   # options pass through
./run.sh test                                            # every suite
KICKSTART=kick13.rom ADF=wb13.adf ./run.sh               # skip the TUI

See harness/README.md for the memory map, the full option list, and the manual CMake path.

Bare-metal or freestanding-style hosts can disable the default stderr log sink:

cmake -S . -B build-bare -DRIGEL_ENABLE_STDIO_LOG=OFF -DRIGEL_BUILD_TESTS=OFF
cmake --build build-bare

SIMD pixel/buffer helpers are enabled by default where Rigel has a backend (SSE2 on x86_64, NEON on AArch64). Disable them with:

cmake -S . -B build-nosimd -DRIGEL_ENABLE_SIMD=OFF
cmake --build build-nosimd

Public API

All headers are included via <rigel/rigel.h>.

Lifecycle:

RigelContext *rigel_create(const rigel_config_t *config);
void          rigel_destroy(RigelContext *ctx);
void          rigel_reset(RigelContext *ctx);

Temporal API — scheduling and synchronization:

rigel_cycle_t       rigel_get_time(const RigelContext *ctx);
rigel_cycle_t       rigel_get_next_deadline(const RigelContext *ctx);
rigel_step_result_t rigel_step(RigelContext *ctx, rigel_cycle_t cycles);
rigel_step_result_t rigel_step_until(RigelContext *ctx, rigel_cycle_t target_time);

rigel_step_result_t carries the current time and a bitmask of what changed (RIGEL_EVENT_IRQ_CHANGED, RIGEL_EVENT_FRAME_READY, RIGEL_EVENT_BLIT_DONE, etc.).

Bus observation — contention and wait states:

rigel_bus_state_t rigel_get_bus_state(const RigelContext *ctx);
rigel_cycle_t     rigel_get_next_bus_change(const RigelContext *ctx);
bool              rigel_cpu_can_access_chip_ram(const RigelContext *ctx);
rigel_cycle_t     rigel_get_cpu_resume_time(const RigelContext *ctx);

Custom register MMIO:

rigel_u16 rigel_custom_read16(const RigelContext *ctx, rigel_u32 offset);
void      rigel_custom_write16(RigelContext *ctx, rigel_u32 offset, rigel_u16 value);

IRQ state:

rigel_u16 rigel_get_intreq(const RigelContext *ctx);
rigel_u16 rigel_get_intena(const RigelContext *ctx);
rigel_u8  rigel_get_ipl(const RigelContext *ctx);

Video output: rigel_get_frame(), rigel_get_scanline(), and rigel_denise_get_video_desc() expose the completed frame, visible window, and debug mode flags. Current mode flags cover lores/hires distinction, interlace intent, dual-playfield, HAM6, and EHB. ECS identity, DIWHIGH, and PAL/NTSC BEAMCON0 switching are present; ECS SuperHires/Productivity/Euro/Dbl modes are tracked as planned work and are not advertised as supported modes yet.

Peripherals: rigel_floppy_* supports DF0-DF3 media/status and selected-drive DMA routing; rigel_input_*, rigel_rtc_*, rigel_serial_*, and rigel_audio_* expose the other integrated devices.

Host logging: set rigel_config_t.log_fn to receive internal log messages. With RIGEL_ENABLE_STDIO_LOG=OFF, a NULL log callback becomes a no-op so Rigel does not require stderr.

SIMD: the public API remains scalar and deterministic. Optional internal SIMD helpers accelerate video buffer fills/copies when available, with scalar fallback for unsupported targets or RIGEL_ENABLE_SIMD=OFF.

Host loop

while (running) {
    rigel_cycle_t until = rigel_get_next_deadline(rigel);

    cpu_run_until(cpu, until);

    rigel_step_result_t r = rigel_step_until(rigel, cpu_get_time(cpu));

    if (r.events & RIGEL_EVENT_IRQ_CHANGED)
        cpu_set_ipl(cpu, rigel_get_ipl(rigel));

    if (r.events & RIGEL_EVENT_FRAME_READY)
        host_present_frame(rigel_get_frame(rigel));
}

For fine-grained bus integration (PiStorm/Emu68 style), also consult rigel_get_bus_state() and rigel_get_next_bus_change().

Minimal example

#include <rigel/rigel.h>

static rigel_u16 chip_ram_read16(void *opaque, rigel_u32 addr)
{
    rigel_u16 *ram = opaque;
    return ram[addr >> 1];
}

static void chip_ram_write16(void *opaque, rigel_u32 addr, rigel_u16 value)
{
    rigel_u16 *ram = opaque;
    ram[addr >> 1] = value;
}

int main(void)
{
    rigel_u16 chip_ram[256 * 1024] = {0};
    rigel_config_t config = {
        .clock_hz      = 7093790,
        .chip_ram_size = sizeof(chip_ram),
        .chip_ram      = { .opaque = chip_ram,
                           .read16 = chip_ram_read16,
                           .write16 = chip_ram_write16 },
    };

    RigelContext *ctx = rigel_create(&config);

    rigel_custom_write16(ctx, 0x096, 0x8200); /* DMACON */
    rigel_custom_write16(ctx, 0x09a, 0x8020); /* INTENA */

    rigel_step_result_t r = rigel_step(ctx, 227); /* one scanline */

    if (r.events & RIGEL_EVENT_IRQ_CHANGED)
        handle_ipl(rigel_get_ipl(ctx));

    rigel_destroy(ctx);
    return 0;
}

Architecture

See docs/ for detailed documentation:

  • architecture.md — layers, domains, chipset composition
  • integration.md — host loop, memory-map forwarding, bus observation
  • timing_model.md — DMA slot sequence, Temporal API, frame pacing
  • video_output.md — video pipeline, frame struct, pixel formats, dirty tracking
  • irq_model.md — interrupt sources, INTREQ/INTENA, host delivery
  • memory_map.md — custom register offsets

About

A portable classic amiga chipset emulation

Resources

Stars

4 stars

Watchers

0 watching

Forks

Contributors

Languages