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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ FLOPPIES/
gost
.vscode/
.tmp/
go.work
go.work.sum
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

## Unreleased

### Changed

- Fetches and TOS-vector reads from the boot ROM now bypass the bus via a
read-only flat memory window (`m68kemu` `SetFastMemory`), cutting an EmuTOS
boot benchmark by ~8%. Wait-state accounting is unchanged, so cycle counts,
register state, and the debug-trace suite are byte-for-byte identical to the
bus path; `machine.disableFastMemory` forces every access back through the bus.
- `EnableTrace` installs CPU observation callbacks through the single
`m68kemu` `SetHooks` call instead of separate setters.
- `Machine.RequestInterrupt` and `devices.Interrupt.Vector` now take a plain
`uint8` vector; pass `cpu.AutoVector` / `devices.AutoVector` (zero) to
auto-vector, replacing the previous `*uint8`.
- The single-range peripherals (ACIA, GLUE, MFP, PSG, FDC, Blitter, STE sound,
cartridge ROM) expose `AddressRange()` and drop their `Contains` boilerplate;
the bus page-maps them instead of scanning.

### Dependencies

- `github.com/jenska/m68kemu` updated to v1.5.0 for the `WithDeferredReset`,
`SetHooks`/`Hooks`, and `SetFastMemory` APIs, a polished public surface
(plain-value `RequestInterrupt`, `*Bus` on `NewCPU`, fewer exported
internals), and optional `Device.Contains`.

## v0.4.0 - 2026-09-04

### Added
Expand Down
8 changes: 8 additions & 0 deletions configs/atari-1040st-tos100.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"preset": "st",
"model": "st",
"ram-size": 1048576,
"color-monitor": true,
"rom": "TOS/TOS100GE.IMG",
"scale": 2
}
374 changes: 374 additions & 0 deletions docs/m68kemu-api-proposal.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/ebitenui/ebitenui v0.7.3
github.com/hajimehoshi/ebiten/v2 v2.9.11
github.com/jenska/m68kdasm v1.1.0
github.com/jenska/m68kemu v1.4.0
github.com/jenska/m68kemu v1.5.0
github.com/jenska/ym2149 v1.1.0
golang.org/x/image v0.45.0
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ github.com/jenska/m68kasm v1.4.0 h1:/VDRRGtmLKynjQJxFmLsvp0nDEPtfIClSFB0+e+m7js=
github.com/jenska/m68kasm v1.4.0/go.mod h1:0791j2jGF9H6JrtHde3qYZxCt2pJbQwT3VP7x/UZqYU=
github.com/jenska/m68kdasm v1.1.0 h1:z50gR+U1fvrcz3LDv1li7x4nedvx6a/JRkyIQ++/XQU=
github.com/jenska/m68kdasm v1.1.0/go.mod h1:fCE5ioFP52IewNAcb7wV5Pnj1hxTCA/82xb2lFaU1tc=
github.com/jenska/m68kemu v1.4.0 h1:6OmmTY48hu+SVLIIqpzIiP5LJjd+pGQCxnimIeHFmgY=
github.com/jenska/m68kemu v1.4.0/go.mod h1:kTiZoKMbJ1FAAHyTPRaMYtLq2WM+ZkTymnFWrvk5UlM=
github.com/jenska/m68kemu v1.5.0 h1:TzkQ+x8PGTn28y9yz2VT3etrN4DDZNlHIsogDz14dAc=
github.com/jenska/m68kemu v1.5.0/go.mod h1:kTiZoKMbJ1FAAHyTPRaMYtLq2WM+ZkTymnFWrvk5UlM=
github.com/jenska/ym2149 v1.1.0 h1:1NDy8hOJY018LhcV2KymZ6lZE9tJB9wLlIzPwFwNIuA=
github.com/jenska/ym2149 v1.1.0/go.mod h1:qQtXDjwXPrQ/UWIVY2hbyswTlwIYxnCHsRuJ7jiNyE0=
github.com/jezek/xgb v1.3.1 h1:NQCAEfQyzN+3RjWUSHBuVIxQcy2YfG3/mNvKfs/0rEg=
Expand Down
4 changes: 2 additions & 2 deletions internal/devices/acia.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func NewACIA(aciaIRQ func(bool)) *ACIA {
}

// Contains reports whether the given address is serviced by the ACIA.
func (a *ACIA) Contains(address uint32) bool {
return address >= aciaBase && address < aciaBase+aciaSize
func (a *ACIA) AddressRange() (uint32, uint32) {
return aciaBase, aciaBase + aciaSize - 1
}

// WaitStates returns the fixed ACIA bus latency.
Expand Down
4 changes: 2 additions & 2 deletions internal/devices/acia_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func TestACIAKeyboardSignalsMFPInterruptOnReceive(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected one MFP interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x46 {
if irqs[0].Vector != 0x46 {
t.Fatalf("unexpected ACIA MFP vector: %+v", irqs[0].Vector)
}

Expand Down Expand Up @@ -198,7 +198,7 @@ func TestACIAMIDISignalsMFPInterruptOnReceive(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected one MFP interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x46 {
if irqs[0].Vector != 0x46 {
t.Fatalf("unexpected ACIA MFP vector: %+v", irqs[0].Vector)
}

Expand Down
10 changes: 7 additions & 3 deletions internal/devices/blitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func NewBlitter(ram *RAM) *Blitter {
return b
}

func (b *Blitter) Contains(address uint32) bool {
return address >= blitterBase && address < blitterBase+blitterSize
func (b *Blitter) AddressRange() (uint32, uint32) {
return blitterBase, blitterBase + blitterSize - 1
}

func (b *Blitter) Read(size cpu.Size, address uint32) (uint32, error) {
Expand Down Expand Up @@ -74,8 +74,12 @@ func (b *Blitter) Reset() {
clear(b.regs[:])
}

func (b *Blitter) contains(address uint32) bool {
return address >= blitterBase && address < blitterBase+blitterSize
}

func (b *Blitter) offsetFor(address uint32, size cpu.Size) (uint32, error) {
if !b.Contains(address) {
if !b.contains(address) {
return 0, cpu.BusError(address)
}
offset := address - blitterBase
Expand Down
4 changes: 2 additions & 2 deletions internal/devices/cartridge_rom.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ func NewCartridgeROM(image []byte) (*CartridgeROM, error) {
return &CartridgeROM{data: append([]byte(nil), image...)}, nil
}

func (c *CartridgeROM) Contains(address uint32) bool {
return address >= cartridgeROMBase && address < cartridgeROMBase+cartridgeROMSize
func (c *CartridgeROM) AddressRange() (uint32, uint32) {
return cartridgeROMBase, cartridgeROMBase + cartridgeROMSize - 1
}

func (c *CartridgeROM) Read(size cpu.Size, address uint32) (uint32, error) {
Expand Down
8 changes: 6 additions & 2 deletions internal/devices/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import (
cpu "github.com/jenska/m68kemu"
)

// AutoVector is the Interrupt.Vector value that asks the CPU to auto-vector the
// request (vector 24+Level) rather than take a device-supplied vector.
const AutoVector = cpu.AutoVector

// Interrupt models a pending CPU interrupt from a device to the CPU.
type Interrupt struct {
// Level is the interrupt priority level (1-7).
Level uint8
// Vector is a pointer to the exception vector address if applicable, nil for autovector.
Vector *uint8
// Vector is the exception vector number, or AutoVector to auto-vector.
Vector uint8
}

// Clocked represents a device that advances its internal state with CPU cycles.
Expand Down
6 changes: 3 additions & 3 deletions internal/devices/fdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ func NewFDC(ram *RAM, irq func(bool)) *FDC {
return f
}

func (f *FDC) Contains(address uint32) bool {
return address >= fdcBase && address < fdcBase+fdcSize
func (f *FDC) AddressRange() (uint32, uint32) {
return fdcBase, fdcBase + fdcSize - 1
}

func (f *FDC) WaitStates(cpu.Size, uint32) uint32 {
Expand Down Expand Up @@ -1099,7 +1099,7 @@ func (f *FDC) commandSectorCount(cmd byte) (count int, multi bool) {

func (f *FDC) queueInterrupt() {
vector := f.vector
f.pending = append(f.pending, Interrupt{Level: 5, Vector: &vector})
f.pending = append(f.pending, Interrupt{Level: 5, Vector: vector})
if f.irq != nil {
f.irq(true)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/devices/glue.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func NewGLUE(cfg ...*config.Config) *GLUE {
return g
}

func (g *GLUE) Contains(address uint32) bool {
return address >= glueBase && address < glueBase+glueSize
func (g *GLUE) AddressRange() (uint32, uint32) {
return glueBase, glueBase + glueSize - 1
}

func (g *GLUE) Read(size cpu.Size, address uint32) (uint32, error) {
Expand Down
4 changes: 2 additions & 2 deletions internal/devices/glue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestGLUEQueuesHBLAtScanlineBoundary(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected one HBL interrupt, got %d", len(irqs))
}
if irqs[0].Level != 2 || irqs[0].Vector != nil {
if irqs[0].Level != 2 || irqs[0].Vector != AutoVector {
t.Fatalf("unexpected HBL interrupt: %+v", irqs[0])
}
}
Expand All @@ -70,7 +70,7 @@ func TestGLUEQueuesVBLAtFrameBoundary(t *testing.T) {
t.Fatalf("expected GLUE frame interrupts")
}
last := irqs[len(irqs)-1]
if last.Level != 4 || last.Vector != nil {
if last.Level != 4 || last.Vector != AutoVector {
t.Fatalf("expected final frame interrupt to be VBL autovector, got %+v", last)
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/devices/mfp.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ func NewMFP(cfg *config.Config) *MFP {
return m
}

func (m *MFP) Contains(address uint32) bool {
return address >= mfpBase && address < mfpBase+mfpSize
func (m *MFP) AddressRange() (uint32, uint32) {
return mfpBase, mfpBase + mfpSize - 1
}

func (m *MFP) WaitStates(cpu.Size, uint32) uint32 {
Expand Down Expand Up @@ -213,7 +213,7 @@ func (m *MFP) DrainInterrupts() []Interrupt {
}

vector := m.vectorBase + uint8(channel)
return []Interrupt{{Level: 6, Vector: &vector}}
return []Interrupt{{Level: 6, Vector: vector}}
}

func (m *MFP) NextEventCycles() (uint64, bool) {
Expand Down
24 changes: 12 additions & 12 deletions internal/devices/mfp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestMFPTimerQueuesInterrupt(t *testing.T) {
if irqs[0].Level != 6 {
t.Fatalf("unexpected interrupt level: got %d want 6", irqs[0].Level)
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x4D {
if irqs[0].Vector != 0x4D {
t.Fatalf("unexpected vector: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -62,7 +62,7 @@ func TestMFPTimerCQueuesInterrupt(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected 1 interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x45 {
if irqs[0].Vector != 0x45 {
t.Fatalf("unexpected vector: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestMFPSoftwareEOIBlocksLowerPriorityInterrupts(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected 1 interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x4D {
if irqs[0].Vector != 0x4D {
t.Fatalf("unexpected vector for highest priority interrupt: %+v", irqs[0].Vector)
}

Expand All @@ -179,7 +179,7 @@ func TestMFPSoftwareEOIBlocksLowerPriorityInterrupts(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected 1 interrupt after software eoi, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x48 {
if irqs[0].Vector != 0x48 {
t.Fatalf("unexpected vector after software eoi: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -240,7 +240,7 @@ func TestMFPTimerAccumulatesFractionalCPUClock(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected 1 interrupt after 14 CPU cycles, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x45 {
if irqs[0].Vector != 0x45 {
t.Fatalf("unexpected vector: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -275,7 +275,7 @@ func TestMFPAutoEOITimerCRepeats(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected recurring timer c interrupt under auto-EOI, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x45 {
if irqs[0].Vector != 0x45 {
t.Fatalf("unexpected recurring timer c vector: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -346,7 +346,7 @@ func TestMFPSoftwareEOIPreventsDuplicateTimerDispatchBeforeServiceClear(t *testi
if len(irqs) != 1 {
t.Fatalf("expected pending timer c interrupt after service clear, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x45 {
if irqs[0].Vector != 0x45 {
t.Fatalf("unexpected vector after service clear: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -520,7 +520,7 @@ func TestMFPGPIPAERDefaultDetectsFallingACIAEdge(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected falling ACIA edge interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x46 {
if irqs[0].Vector != 0x46 {
t.Fatalf("unexpected falling ACIA edge vector: %+v", irqs[0].Vector)
}

Expand Down Expand Up @@ -556,7 +556,7 @@ func TestMFPGPIPAERDetectsRisingACIAEdgeWhenConfigured(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected rising ACIA edge interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x46 {
if irqs[0].Vector != 0x46 {
t.Fatalf("unexpected rising ACIA edge vector: %+v", irqs[0].Vector)
}
}
Expand Down Expand Up @@ -607,7 +607,7 @@ func TestMFPGPIPAERDetectsFallingRTCEdgeOnGPIP5(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected falling RTC edge interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x47 {
if irqs[0].Vector != 0x47 {
t.Fatalf("unexpected falling RTC edge vector: %+v", irqs[0].Vector)
}

Expand Down Expand Up @@ -719,7 +719,7 @@ func TestMFPRS232ReceiveByteSetsStatusAndInterrupt(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected receive interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x4C {
if irqs[0].Vector != 0x4C {
t.Fatalf("unexpected receive vector: %+v", irqs[0].Vector)
}

Expand Down Expand Up @@ -811,7 +811,7 @@ func TestMFPRS232TransmitCapturesOutputAndInterrupts(t *testing.T) {
if len(irqs) != 1 {
t.Fatalf("expected transmit interrupt, got %d", len(irqs))
}
if irqs[0].Vector == nil || *irqs[0].Vector != 0x4A {
if irqs[0].Vector != 0x4A {
t.Fatalf("unexpected transmit vector: %+v", irqs[0].Vector)
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/devices/psg.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func NewPSG(cpuClockHz uint64) *PSG {
}
}

func (p *PSG) Contains(address uint32) bool {
return address >= psgBase && address < psgBase+psgSize
func (p *PSG) AddressRange() (uint32, uint32) {
return psgBase, psgBase + psgSize - 1
}

func (p *PSG) WaitStates(cpu.Size, uint32) uint32 {
Expand Down
11 changes: 11 additions & 0 deletions internal/devices/rom.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ func (r *ROM) Bytes() []byte {
return append([]byte(nil), r.data...)
}

// Image returns the live ROM backing bytes. The contents are immutable, so the
// slice is safe to hand to a read-only fast-memory region.
func (r *ROM) Image() []byte {
return r.data
}

// Aliases returns the base addresses at which the ROM image is mapped.
func (r *ROM) Aliases() []uint32 {
return append([]uint32(nil), r.aliases...)
}

func (r *ROM) Slice(address uint32, size cpu.Size) (uint32, error) {
return r.Read(size, address)
}
Expand Down
10 changes: 7 additions & 3 deletions internal/devices/ste_sound.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func NewAbsentSTESound() *BusErrorRegion {
return NewBusErrorRegion(AddressRange{Start: steSoundBase, End: steSoundBase + steSoundSize})
}

func (s *STESound) Contains(address uint32) bool {
return address >= steSoundBase && address < steSoundBase+steSoundSize
func (s *STESound) AddressRange() (uint32, uint32) {
return steSoundBase, steSoundBase + steSoundSize - 1
}

func (s *STESound) WaitStates(cpu.Size, uint32) uint32 {
Expand Down Expand Up @@ -340,12 +340,16 @@ func (s *STESound) playing() bool {
return s.control&steSoundControlEnable != 0
}

func (s *STESound) contains(address uint32) bool {
return address >= steSoundBase && address < steSoundBase+steSoundSize
}

func (s *STESound) accessInRange(address uint32, count int) bool {
if count <= 0 {
return false
}
end := address + uint32(count) - 1
return s.Contains(address) && s.Contains(end)
return s.contains(address) && s.contains(end)
}

func steSoundAccessSize(size cpu.Size) (int, error) {
Expand Down
Loading
Loading