Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
# and ships with the build, so it is committed despite the *.bin rule.
!/assets/aros/aros-amiga-m68k-rom.bin
!/assets/aros/aros-amiga-m68k-ext.bin
!/assets/services/services_rom.bin

# Local run artifacts (screenshots, recordings, save states, input scripts).
copperline-screenshot-*.png
Expand All @@ -55,3 +56,9 @@ copperline-video-*.avi
# macOS disk image build artifact (packaging/macos/build-dmg.sh); the .app
# bundle stages under /target, already ignored above.
/*.dmg

# Guest-code build intermediates (the committed artifact is the .bin blob).
/guest/services/handler.*

# Local clangd configs (machine-specific include paths).
.clangd
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ wasmtime = { version = "=27.0.0", default-features = false, features = ["craneli
# time-zone filename stamps) and, on macOS, the pthread QoS class used for the
# optional realtime-priority feature (see src/priority.rs).
libc = "0.2"
zerocopy = { version = "0.8", features = ["derive"] }

# macOS dock icon: winit's with_window_icon is a no-op on macOS, so the dock
# icon must be set at runtime via NSApplication. These objc2 versions match the
Expand Down
Binary file added assets/services/services_rom.bin
Binary file not shown.
15 changes: 15 additions & 0 deletions copperline.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@ video = "PAL"
# slave = "data.hdf"


# Host directories mounted directly as AmigaDOS volumes (HOSTFS0:,
# HOSTFS1:, ...), served live with no disk image in between. Read-only for
# now: write operations fail with "disk is write-protected". Up to 16
# mounts. volume defaults to the directory name. bootpri (-128..127,
# default -128 = never boot) enters the boot-device vote: hard-disk boot
# partitions typically sit at 0 and DF0: at 5.
# [[filesys]]
# path = "/data/amiga/Workbench"
# volume = "Workbench"
# bootpri = 6
#
# [[filesys]]
# path = "/data/amiga/downloads"


# Presentation options. overscan: "tv" (default; mask deep horizontal
# overscan like a CRT bezel) or "full". pixel_aspect: "tv" (default; the
# non-square pixel aspect of a 4:3 CRT) or "square" (one host row per
Expand Down
31 changes: 31 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,37 @@ in-memory FFS volumes -- including the `{ path = "...", name = "..." }`
table form that overrides a directory mount's volume name. The HDD
activity LED covers SCSI traffic too.

## `[[filesys]]` -- host directories as live volumes

```toml
[[filesys]]
path = "/data/amiga/Workbench"
volume = "Workbench" # optional, defaults to the directory name
bootpri = 6 # optional boot priority; default -128 = never boot

[[filesys]]
path = "/data/amiga/downloads"
```

Each `[[filesys]]` entry exports a host directory to the guest as an
AmigaDOS volume on its own `HOSTFS<n>:` device, served live by the
emulator: no disk image is built, and guest reads always see the current
host contents. This differs from giving `[ide]`/`[scsi]` a directory
path, which snapshots the tree into an in-memory FFS volume at startup.
Up to 16 mounts.

The volumes are read-only for now: write operations fail with the same
"disk is write-protected" error a physical write-protected disk gives.
Protection bits, comments, and exact datestamps are taken from UAE-style
`.uaem` sidecar files when present (the sidecars stay hidden from
listings).

`volume` sets the AmigaDOS volume name (up to 30 characters, no `:` or
`/`). `bootpri` enters the volume in the boot-device vote (-128..127;
the default -128 means mounted but never booted from): hard-disk boot
partitions typically sit at priority 0 and DF0: at 5, so a bootable
Workbench directory with `bootpri = 6` boots ahead of both.

## `[cd]` -- CDTV and CD32

```toml
Expand Down
41 changes: 41 additions & 0 deletions guest/services/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Builds the guest-side handler ROM for Copperline's services board with the
# dockerized m68k-amigaos cross-GCC, and installs it as the committed artifact
# assets/services/services_rom.bin (rebuilding needs docker; a plain cargo
# build just embeds the committed ROM).
#
# The ROM must be position-independent and self-contained: -mpcrel makes all
# code/rodata references PC-relative, and the checks below fail the build if
# the linked executable still contains relocations or data/bss hunks
# (objcopy would silently emit a ROM that breaks at any other base).

ROM = ../../assets/services/services_rom.bin

IMAGE = stefanreinauer/amiga-gcc:gcc-v16.1
DOCKER = docker run --rm --user $(shell id -u):$(shell id -g) \
-v "$(CURDIR):/src" -w /src $(IMAGE)
CC = $(DOCKER) m68k-amigaos-gcc
OBJDUMP = $(DOCKER) m68k-amigaos-objdump
OBJCOPY = $(DOCKER) m68k-amigaos-objcopy
CFLAGS = -Os -m68000 -mpcrel -fomit-frame-pointer -fno-jump-tables \
-Wall -Wextra -nostdlib -nostartfiles

$(ROM): handler.bin
cp handler.bin $(ROM)

# The docker mount only covers this directory, so objcopy writes a local
# intermediate and the install step above copies it into assets/.
handler.bin: handler.exe
@! $(OBJDUMP) -r handler.exe | grep -q RELOC \
|| { echo "handler.exe: has relocations; code must be PC-relative"; exit 1; }
@! $(OBJDUMP) -h handler.exe | grep -Eq '[.](data|bss)' \
|| { echo "handler.exe: has data/bss; keep state on the stack"; exit 1; }
$(OBJCOPY) -O binary handler.exe $@

# entry.s first: the entry table must sit at the start of the code hunk.
handler.exe: entry.s handler.c copperline_board.h
$(CC) $(CFLAGS) entry.s handler.c -o $@

clean:
rm -f handler.exe handler.bin

.PHONY: clean
37 changes: 37 additions & 0 deletions guest/services/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Guest-side filesys handler

`services_rom.bin` is the m68k code Copperline maps into its services board
(Zorro II, manufacturer 0x1448 "dec0de Consulting", product 5) to provide
host-directory mounts (`HOSTFS0:`, `HOSTFS1:`, ...). It is deliberately tiny:
all filesystem semantics live in the emulator (`src/filesys.rs`); the handler
only mounts the DOS devices and pumps DosPackets to the host through one
reserved A-line trap per packet.

Two entry points (see `entry.s` and `copperline_board.h`):

- `mount_boards()` -- called at expansion init from the board's DiagArea
with the documented DiagPoint context. Builds one DeviceNode per entry in
the mount table the emulator wrote into the board window and adds it with
`AddBootNode` (priority -128: mounted at DOS init, never a boot candidate).
- `handler_main()` -- the DOS handler process, started by DOS on first
reference to a mount. `WaitPort`/`GetMsg`, trap to the emulator (which
fills `dp_Res1`/`dp_Res2`), reply the packet.

## Rebuilding

The ROM is a committed artifact, rebuilt by hand when the handler changes:

```sh
make # dockerized m68k-amigaos-gcc -> handler.exe -> services_rom.bin
```

The Makefile uses the `stefanreinauer/amiga-gcc` container (GCC 16 with the
AmigaOS hunk patches; https://github.com/reinauer/container-amiga-gcc), so no
local cross-toolchain is needed. The ROM must be position-independent: it
runs at whatever base autoconfig assigns, so everything is compiled with
`-mpcrel`, and the Makefile fails the build if the linked executable contains
relocations or data/bss hunks (checked with objdump before objcopy extracts
the flat code hunk).

`copperline_board.h` pins the board-window layout and trap opcodes shared with
`src/filesys.rs`; keep the two in sync (Rust unit tests lock the layout).
59 changes: 59 additions & 0 deletions guest/services/copperline_board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Board-window layout and trap opcodes shared between the guest handler and
// the emulator. Keep in sync with the constants in src/filesys.rs; the Rust
// unit tests lock the layout.
//
// 64K window layout:
// 0x0000 u32: fake seglist length (longwords), for tools that look at it
// 0x0004 u32: 0 (seglist next pointer); dn_SegList = MKBADDR(board + 4)
// 0x0008 handler code (services_rom.bin). Entry table:
// +0 process entry (DOS RunHandler starts the handler here)
// +4 expansion-init entry (jsr-ed by the DiagArea stub
// with the DiagPoint registers; mounts the volumes)
// +0x40 struct DiagArea (er_InitDiagVec points here; the
// DiagPoint stub reaches the ROM via jsr 12(a0))
// 0x3800 mount table, written by the emulator:
// u16 count, then count fixed-size entries of the DOS device name
// as a NUL-terminated string ("HOSTFS0", ...)
// 0x7000 per-unit volume DosList nodes, built by the emulator at startup
// and AddDosEntry'd by the handler (TRAP_RES_ADDVOLUME)
// 0x8000 emulator-managed guest object pool (FileLocks etc.); the handler
// never touches it

#ifndef COPPERLINE_BOARD_H
#define COPPERLINE_BOARD_H

#define BOARD_MANUFACTURER 0x1448 // dec0de Consulting
#define BOARD_PRODUCT 0x05 // Copperline services board

#define ROM_OFFSET 0x0008
#define MOUNTS_OFFSET 0x3800
#define MOUNT_ENTRY_SIZE 32
#define MOUNT_MAX_COUNT 16
#define VOLUMES_OFFSET 0x7000
#define VOLUME_SLOT_SIZE 128
// Per-unit FileSysStartupMsg, written by the emulator at expansion init;
// dn_Startup points here so the Early Startup boot menu can display the
// device name, unit, and dostype instead of dereferencing garbage. Each
// FSSM references a per-unit DosEnvec whose de_BootPri carries the
// configured AddBootNode priority.
#define FSSM_OFFSET 0x7800
#define FSSM_SLOT_SIZE 16

// A-line opcodes reserved for host traps (see FilesysHle in src/filesys.rs).
#define TRAP_DIAG_ENTRY 0xA400 // DiagPoint entered (logged by the host)
#define TRAP_PACKET 0xA402 // D1 = struct DosPacket *, A1 = handler port

// trap_packet return values (D0).
#define TRAP_RES_REPLY 0 // packet complete: reply it to the sender
#define TRAP_RES_NOREPLY 1 // host keeps the packet (reserved, not yet used)
#define TRAP_RES_ADDVOLUME 2 // reply, then AddDosEntry the volume DosList
// node the host built (returned in A0): only
// guest code may take the DosList semaphore
#define TRAP_RES_DIE 3 // ACTION_DIE accepted: reply, RemDosEntry the
// volume node (in A0), and exit the process
// (dn_Task is already cleared, so the next
// reference restarts the handler)

#endif // COPPERLINE_BOARD_H
74 changes: 74 additions & 0 deletions guest/services/entry.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
| SPDX-License-Identifier: GPL-3.0-or-later
|
| Entry table and DiagArea of the handler ROM. Linked first, so this sits at
| ROM_OFFSET in the board window (see copperline_board.h). Everything must
| stay PC-relative: the ROM runs at whatever base autoconfig assigns.

.text
.globl _entry_table
.globl _handler_main
.globl _mount_boards

_entry_table:
| +0: handler process entry (DOS RunHandler jumps here via dn_SegList)
bra.w _handler_main

| +4: expansion-init entry. The DiagArea's DiagPoint jsr's here from
| the diag copy with the documented DiagPoint registers still live:
| A0 = board base, A3 = ConfigDev, A5 = ExpansionBase. Trap to the
| host (which captures the board base), mount the configured volumes,
| and return D0 != 0 so Kickstart keeps the diag copy: strap calls
| da_BootPoint from it if one of our mounts wins the boot vote.
_diag_entry:
.short 0xA400 | TRAP_DIAG_ENTRY
move.l a3,-(sp) | ConfigDev
move.l a5,-(sp) | ExpansionBase
move.l a0,-(sp) | board base
bsr.w _mount_boards | mount_boards(board, ExpansionBase, ConfigDev)
lea 12(sp),sp
moveq #1,d0
rts

| struct DiagArea (libraries/configregs.h), at the fixed ROM offset
| DIAG_AREA_IN_ROM: er_InitDiagVec points here and Kickstart copies
| da_Size bytes to RAM before calling da_DiagPoint. All code offsets
| are relative to the copy, so the DiagPoint stub reaches the ROM
| through A0 (the board base) -- a bsr would aim into the copy.
| Hard-won Kickstart 3.x gotchas: da_Config needs a DAC_BOOTTIME bit
| or the area is abandoned after one read, and DAC_CONFIGTIME
| requires a non-zero da_BootPoint.
.org 0x40 | errors out if the code above grows past this
_diag_area:
.byte 0x90, 0x00 | da_Config = DAC_WORDWIDE
| | DAC_CONFIGTIME; da_Flags
.short _diag_area_end-_diag_area | da_Size
.short _diag_point-_diag_area | da_DiagPoint
.short _boot_point-_diag_area | da_BootPoint
.short _diag_name-_diag_area | da_Name
.short 0, 0 | da_Reserved01/02
_diag_point:
jsr (_diag_entry-_entry_table+8)(a0) | +8 = ROM_OFFSET
rts
_boot_point:
| Called by strap (with A6 = ExecBase) when one of our BootNodes has
| the highest boot priority. The standard autoboot boot code, same as
| real autoboot ROMs: fire up dos.library, whose init then mounts the
| highest-priority BootNode -- ours -- as SYS:. Returns (boot failed,
| strap tries the next candidate) only if dos.library is missing.
lea _dos_name(pc),a1
jsr -96(a6) | FindResident("dos.library")
tst.l d0
beq.s 1f
move.l d0,a0
move.l 22(a0),d0 | rt_Init
beq.s 1f
move.l d0,a0
jsr (a0) | boots DOS; does not return on success
1: moveq #0,d0
rts
_dos_name:
.asciz "dos.library"
_diag_name:
.asciz "Copperline"
.balign 2
_diag_area_end:
Loading
Loading