badline 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +10 -9
- data/README.md +82 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -23
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +11 -1
- data/lib/badline/audio/cli.rb +18 -5
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio.rb +0 -3
- data/lib/badline/cartridge/action_replay.rb +15 -0
- data/lib/badline/cartridge/comal80.rb +8 -0
- data/lib/badline/cartridge/dinamic.rb +8 -0
- data/lib/badline/cartridge/easy_flash.rb +20 -1
- data/lib/badline/cartridge/epyx_fastload.rb +8 -0
- data/lib/badline/cartridge/final_cartridge3.rb +12 -0
- data/lib/badline/cartridge/flash.rb +41 -0
- data/lib/badline/cartridge/fun_play.rb +8 -0
- data/lib/badline/cartridge/g_mod2.rb +12 -0
- data/lib/badline/cartridge/game_system.rb +8 -0
- data/lib/badline/cartridge/geo_ram.rb +16 -0
- data/lib/badline/cartridge/magic_desk.rb +8 -0
- data/lib/badline/cartridge/ocean.rb +8 -0
- data/lib/badline/cartridge/pagefox.rb +12 -0
- data/lib/badline/cartridge/retro_replay.rb +44 -1
- data/lib/badline/cartridge/rgcd.rb +10 -0
- data/lib/badline/cartridge/super_games.rb +10 -0
- data/lib/badline/cartridge/zaxxon.rb +16 -0
- data/lib/badline/cartridge.rb +72 -0
- data/lib/badline/cia/interrupt_register.rb +17 -0
- data/lib/badline/cia/serial.rb +44 -0
- data/lib/badline/cia/state.rb +47 -0
- data/lib/badline/cia/timer.rb +33 -0
- data/lib/badline/cia.rb +8 -0
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +13 -33
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +3 -212
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +13 -0
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541.rb +35 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +72 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend.rb +18 -0
- data/lib/badline/kernal_trap/channel.rb +27 -0
- data/lib/badline/kernal_trap/drive/channels.rb +25 -0
- data/lib/badline/kernal_trap/drive/memory.rb +9 -0
- data/lib/badline/kernal_trap/drive/status.rb +8 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
- data/lib/badline/kernal_trap/drive.rb +19 -0
- data/lib/badline/kernal_trap/save.rb +8 -0
- data/lib/badline/kernal_trap/serial.rb +16 -0
- data/lib/badline/memory.rb +8 -0
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +78 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +62 -0
- data/lib/badline/options.rb +181 -144
- data/lib/badline/ram_expansion/plus256k.rb +16 -0
- data/lib/badline/ram_expansion/plus60k.rb +12 -0
- data/lib/badline/ram_expansion/unexpanded.rb +6 -0
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -11
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +53 -0
- data/lib/badline/reu.rb +23 -10
- data/lib/badline/sdl.rb +123 -51
- data/lib/badline/sid/envelope/state.rb +68 -0
- data/lib/badline/sid/envelope.rb +7 -0
- data/lib/badline/sid/filter.rb +38 -0
- data/lib/badline/sid/voice.rb +10 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +4 -3
- data/lib/badline/sid.rb +34 -0
- data/lib/badline/snapshot/container.rb +117 -0
- data/lib/badline/snapshot/fields.rb +54 -0
- data/lib/badline/snapshot/image.rb +70 -0
- data/lib/badline/snapshot/machine_state.rb +77 -0
- data/lib/badline/snapshot/setup.rb +58 -0
- data/lib/badline/snapshot/state.rb +178 -0
- data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
- data/lib/badline/snapshot/vice/cias.rb +89 -0
- data/lib/badline/snapshot/vice/cias_import.rb +70 -0
- data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
- data/lib/badline/snapshot/vice/peripherals.rb +47 -0
- data/lib/badline/snapshot/vice/restore.rb +125 -0
- data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
- data/lib/badline/snapshot/vice/vicii.rb +119 -0
- data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
- data/lib/badline/snapshot/vice.rb +74 -0
- data/lib/badline/snapshot.rb +52 -0
- data/lib/badline/storage/crt_file.rb +9 -3
- data/lib/badline/storage/d64_image.rb +2 -0
- data/lib/badline/storage/d71_image.rb +2 -0
- data/lib/badline/storage/d81_image.rb +2 -0
- data/lib/badline/storage/disk_image.rb +11 -2
- data/lib/badline/storage/host_directory.rb +5 -0
- data/lib/badline/storage/t64.rb +6 -0
- data/lib/badline/storage/tap.rb +10 -1
- data/lib/badline/storage/unavailable.rb +20 -0
- data/lib/badline/storage.rb +34 -0
- data/lib/badline/time_of_day.rb +56 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +15 -0
- data/lib/badline/via/interrupt_register.rb +9 -0
- data/lib/badline/via/shift_register.rb +13 -0
- data/lib/badline/via/state.rb +42 -0
- data/lib/badline/via/timer1.rb +13 -0
- data/lib/badline/via/timer2.rb +18 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic/border_mask.rb +10 -0
- data/lib/badline/vic/collisions.rb +11 -0
- data/lib/badline/vic/color_patches.rb +11 -0
- data/lib/badline/vic/display_state.rb +29 -1
- data/lib/badline/vic/graphics_shifter.rb +26 -0
- data/lib/badline/vic/register_log.rb +18 -0
- data/lib/badline/vic/registers.rb +13 -2
- data/lib/badline/vic/sequencer.rb +12 -0
- data/lib/badline/vic/sequencer_state.rb +89 -0
- data/lib/badline/vic/sprite/internal_bus.rb +11 -0
- data/lib/badline/vic/sprite/saved_state.rb +64 -0
- data/lib/badline/vic/sprite.rb +2 -0
- data/lib/badline/vic/sprites.rb +3 -0
- data/lib/badline/vic/sprites_state.rb +57 -0
- data/lib/badline/vic.rb +109 -16
- data/lib/badline.rb +1 -0
- metadata +59 -18
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -117
- data/lib/badline/gui/application.rb +0 -216
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -97
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -13
- data/lib/badline/sdl/events.rb +0 -47
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module VICII
|
|
7
|
+
# Reads a VIC-II module and puts it into badline's VIC.
|
|
8
|
+
class Import
|
|
9
|
+
def initialize(fields)
|
|
10
|
+
@fields = fields
|
|
11
|
+
read_beam
|
|
12
|
+
read_counters
|
|
13
|
+
read_sprites
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def apply(computer)
|
|
17
|
+
vic = computer.vic
|
|
18
|
+
computer.address_bus.color_ram.tap do |ram|
|
|
19
|
+
@color_ram.each_with_index { |value, offset| ram.poke(0xd800 + offset, value) }
|
|
20
|
+
end
|
|
21
|
+
load_registers(vic)
|
|
22
|
+
position(vic)
|
|
23
|
+
load_counters(vic)
|
|
24
|
+
load_sprites(vic)
|
|
25
|
+
vic.rebuild_sprite_ba
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def read_beam
|
|
31
|
+
f = @fields
|
|
32
|
+
f.skip(1)
|
|
33
|
+
@registers = f.bytes(0x40)
|
|
34
|
+
@cycle = f.dword
|
|
35
|
+
f.skip(4)
|
|
36
|
+
@line = f.dword
|
|
37
|
+
f.skip(1)
|
|
38
|
+
@irq_status = f.byte
|
|
39
|
+
f.skip(4)
|
|
40
|
+
@raster_match = f.flag?
|
|
41
|
+
@character_buffer = f.bytes(40)
|
|
42
|
+
@color_buffer = f.bytes(40)
|
|
43
|
+
f.skip(1 + 4 + (65 * 8) + 4)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_counters
|
|
47
|
+
f = @fields
|
|
48
|
+
@bad_lines_enabled = f.flag?
|
|
49
|
+
@collisions = [f.byte, f.byte]
|
|
50
|
+
f.skip(1)
|
|
51
|
+
@display = f.dword.zero?
|
|
52
|
+
@vc_base, @vc, @rc, @vmli = Array.new(4) { f.dword }
|
|
53
|
+
f.skip(4)
|
|
54
|
+
@lp_low = f.flag?
|
|
55
|
+
@lp_triggered = f.flag?
|
|
56
|
+
f.skip(4 + 4 + 4 + 8)
|
|
57
|
+
@fetch_d011 = f.byte
|
|
58
|
+
f.skip(4)
|
|
59
|
+
@display_bits = f.dword
|
|
60
|
+
@dma_bits = f.byte
|
|
61
|
+
f.skip(4)
|
|
62
|
+
@vertical_border = f.flag?
|
|
63
|
+
@vertical_armed = f.flag?
|
|
64
|
+
@main_border = f.flag?
|
|
65
|
+
f.skip(1)
|
|
66
|
+
@color_ram = f.bytes(0x400)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Each sprite's MC and MCBASE, and its expansion flip-flop as a bit.
|
|
70
|
+
def read_sprites
|
|
71
|
+
f = @fields
|
|
72
|
+
@sprite_mc = Array.new(8, 0)
|
|
73
|
+
@sprite_mcbase = Array.new(8, 0)
|
|
74
|
+
@sprite_exp_ff = 0
|
|
75
|
+
8.times do |n|
|
|
76
|
+
f.skip(4)
|
|
77
|
+
@sprite_mc[n] = f.byte
|
|
78
|
+
@sprite_mcbase[n] = f.byte
|
|
79
|
+
f.skip(1)
|
|
80
|
+
@sprite_exp_ff |= 1 << n if f.flag?
|
|
81
|
+
f.skip(4)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_registers(vic)
|
|
86
|
+
registers = vic.registers
|
|
87
|
+
bytes = registers.bytes
|
|
88
|
+
0x2f.times { |reg| bytes[reg] = @registers[reg] }
|
|
89
|
+
bytes[0x19] = @irq_status & 0x0f
|
|
90
|
+
bytes[0x1a] &= 0x0f
|
|
91
|
+
bytes[0x1e], bytes[0x1f] = @collisions
|
|
92
|
+
registers.update_irq_line
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clocks the VIC alone from the start of the line to the cycle,
|
|
96
|
+
# so the line's fetches and sprite checks have run.
|
|
97
|
+
def position(vic)
|
|
98
|
+
vic.restore_line(@line)
|
|
99
|
+
load_display_state(vic)
|
|
100
|
+
@cycle.times { vic.cycle! }
|
|
101
|
+
registers = vic.registers
|
|
102
|
+
registers.bytes[0x19] = @irq_status & 0x0f
|
|
103
|
+
registers.bytes[0x1e], registers.bytes[0x1f] = @collisions
|
|
104
|
+
registers.update_irq_line
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def load_display_state(vic)
|
|
108
|
+
vic.display_state.restore_counters([@vc_base, @vc, @rc, @vmli], @display, @bad_lines_enabled)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def load_counters(vic)
|
|
112
|
+
load_display_state(vic)
|
|
113
|
+
vic.character_buffer.replace(@character_buffer)
|
|
114
|
+
vic.color_buffer.replace(@color_buffer)
|
|
115
|
+
vic.restore_latches((@raster_match ? 1 : 0) | (@lp_low ? 2 : 0) | (@lp_triggered ? 4 : 0), @fetch_d011)
|
|
116
|
+
vic.sequencer.restore_borders(@vertical_border, @vertical_armed, @main_border)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def load_sprites(vic)
|
|
120
|
+
8.times do |n|
|
|
121
|
+
bit = 1 << n
|
|
122
|
+
vic.sprites[n].restore_counters(@sprite_mc[n], @sprite_mcbase[n], @sprite_exp_ff.anybits?(bit),
|
|
123
|
+
@dma_bits.anybits?(bit), @display_bits.anybits?(bit))
|
|
124
|
+
end
|
|
125
|
+
vic.sprites.update_any_dma
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/snapshot/vice/main_cpu"
|
|
4
|
+
require "badline/snapshot/vice/c64_mem"
|
|
5
|
+
require "badline/snapshot/vice/cias"
|
|
6
|
+
require "badline/snapshot/vice/cias_import"
|
|
7
|
+
require "badline/snapshot/vice/sid_registers"
|
|
8
|
+
require "badline/snapshot/vice/sid_extended"
|
|
9
|
+
require "badline/snapshot/vice/peripherals"
|
|
10
|
+
require "badline/snapshot/vice/vicii"
|
|
11
|
+
require "badline/snapshot/vice/vicii_import"
|
|
12
|
+
require "badline/snapshot/vice/restore"
|
|
13
|
+
|
|
14
|
+
module Badline
|
|
15
|
+
module Snapshot
|
|
16
|
+
# VICE's own modules for the chips both emulators model, written from
|
|
17
|
+
# and read into badline's machine. Each module's file says what maps
|
|
18
|
+
# and what is approximated. Reading a snapshot from VICE starts from a
|
|
19
|
+
# machine at power-on with nothing attached, and applies the modules it
|
|
20
|
+
# has, in the order x64sc writes them.
|
|
21
|
+
module Vice
|
|
22
|
+
# Cycles a copy of the machine may run to reach an instruction
|
|
23
|
+
# boundary: the longest instruction, stretched by the VIC's longest
|
|
24
|
+
# hold on the bus.
|
|
25
|
+
BOUNDARY_CYCLES = 100
|
|
26
|
+
|
|
27
|
+
# The modules a badline snapshot carries for VICE, in x64sc's order.
|
|
28
|
+
# They describe a copy of the machine restored from `state`, so saving
|
|
29
|
+
# leaves the machine itself alone. VICE saves between instructions,
|
|
30
|
+
# and resumes by fetching an opcode at the program counter, so when
|
|
31
|
+
# badline's CPU is inside an instruction the copy runs on to its end.
|
|
32
|
+
# The copy's SID catches up, so its registers and voices stand as of
|
|
33
|
+
# that cycle. badline writes no VICE module for an REU or an NTSC
|
|
34
|
+
# VIC-II yet, and fails for a machine with either.
|
|
35
|
+
def self.export(state)
|
|
36
|
+
check_exportable(Computer.setup(state))
|
|
37
|
+
machine = settled(state)
|
|
38
|
+
[MainCPU.export(machine), C64Mem.export(machine), Peripherals.cartridge_port, *CIAs.export(machine),
|
|
39
|
+
SIDRegisters.export(machine), SIDExtended.export(machine), Peripherals.drives, VICII.export(machine),
|
|
40
|
+
Peripherals.glue(machine), *Peripherals.rest]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.check_exportable(setup)
|
|
44
|
+
raise FormatError, "badline can't save an REU in VICE's terms yet, so can't save this machine" if setup.reu
|
|
45
|
+
return if setup.region == Region::PAL
|
|
46
|
+
|
|
47
|
+
raise FormatError, "badline can't save an NTSC VIC-II in VICE's terms yet, so can't save this machine"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The copy is detached from the host's files (Snapshot::StateReader),
|
|
51
|
+
# so it reads none and its traps write none.
|
|
52
|
+
def self.settled(state)
|
|
53
|
+
copy = Computer.restored(state, detached: true)
|
|
54
|
+
BOUNDARY_CYCLES.times do
|
|
55
|
+
break if copy.cpu.boundary?
|
|
56
|
+
|
|
57
|
+
copy.cycle!
|
|
58
|
+
end
|
|
59
|
+
copy.sid.catch_up!
|
|
60
|
+
copy
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# How the machine the snapshot was saved from was built. VICE doesn't
|
|
64
|
+
# save the CIA model; the C64C's 8565 VIC-II goes with the 6526A. An
|
|
65
|
+
# NTSC or PAL-N VIC-II fails, as badline runs PAL only.
|
|
66
|
+
def self.setup(container)
|
|
67
|
+
vic = VICII.find(container) ? VICII.model(VICII.find(container)) : :mos6569
|
|
68
|
+
sid = container[SIDRegisters::NAME] ? SIDRegisters.model(container[SIDRegisters::NAME]) : :mos6581
|
|
69
|
+
Setup.new(vic_model: vic, cia_model: vic == :mos8565 ? :mos6526a : :mos6526, sid_model: sid,
|
|
70
|
+
region: Region::PAL, ram_expansion: nil, reu: nil)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "badline/snapshot/state"
|
|
5
|
+
require "badline/snapshot/setup"
|
|
6
|
+
require "badline/snapshot/container"
|
|
7
|
+
require "badline/snapshot/fields"
|
|
8
|
+
require "badline/snapshot/machine_state"
|
|
9
|
+
require "badline/snapshot/vice"
|
|
10
|
+
require "badline/snapshot/image"
|
|
11
|
+
|
|
12
|
+
module Badline
|
|
13
|
+
# Saves and restores the whole machine in VICE's .vsf snapshot format.
|
|
14
|
+
#
|
|
15
|
+
# A snapshot badline writes carries VICE's modules for what both
|
|
16
|
+
# emulators model, and a BADLINE module holding the State
|
|
17
|
+
# Computer#snapshot takes, with everything badline needs to carry on
|
|
18
|
+
# exactly where it stopped. VICE skips modules it doesn't know. Restoring
|
|
19
|
+
# a badline snapshot uses the BADLINE module alone; a snapshot from VICE
|
|
20
|
+
# restores through its modules, as far as they map, and reports the
|
|
21
|
+
# modules badline doesn't take.
|
|
22
|
+
module Snapshot
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def save(computer, path)
|
|
26
|
+
File.binwrite(path, dump(computer))
|
|
27
|
+
path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dump(computer)
|
|
31
|
+
state = computer.snapshot
|
|
32
|
+
Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read(path) = Image.new(Container.parse(File.binread(path)))
|
|
36
|
+
|
|
37
|
+
# A new machine, built as the snapshot's was and restored from it.
|
|
38
|
+
# Yields a line for each thing it leaves out.
|
|
39
|
+
def load(path, &) = read(path).load(&)
|
|
40
|
+
|
|
41
|
+
# Restores `computer` from the snapshot at `path`, and returns what it
|
|
42
|
+
# applied and left out, yielding a line for each thing left out. A
|
|
43
|
+
# snapshot that fails leaves the machine as it was.
|
|
44
|
+
def restore(computer, path, &) = read(path).restore(computer, &)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Computer
|
|
48
|
+
def save_snapshot(path) = Snapshot.save(self, path)
|
|
49
|
+
|
|
50
|
+
def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -18,6 +18,11 @@ module Badline
|
|
|
18
18
|
Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
|
|
19
19
|
|
|
20
20
|
def self.write(path, image)
|
|
21
|
+
File.binwrite(path, encode(image))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The CRT file for an image: a parsed CRTFile or an Image.
|
|
25
|
+
def self.encode(image)
|
|
21
26
|
header = SIGNATURE +
|
|
22
27
|
[0x40, 0x0100, image.hardware_type, image.exrom, image.game, image.subtype].pack("NnnCCCx5") +
|
|
23
28
|
[image.name.to_s.b[0, 32]].pack("a32")
|
|
@@ -25,11 +30,12 @@ module Badline
|
|
|
25
30
|
["CHIP", CHIP_HEADER_SIZE + chip.data.length, chip.chip_type, chip.bank, chip.address,
|
|
26
31
|
chip.data.length].pack("a4Nn4") + chip.data.pack("C*")
|
|
27
32
|
end
|
|
28
|
-
|
|
33
|
+
header + packets.join
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
# `bytes` stands in for the file's contents.
|
|
37
|
+
def initialize(path = nil, bytes: nil)
|
|
38
|
+
parse(bytes || File.binread(path))
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
private
|
|
@@ -27,16 +27,25 @@ module Badline
|
|
|
27
27
|
# the read, write and ID errors, 74 is DRIVE NOT READY.
|
|
28
28
|
DOS_ERRORS = (2..11).to_h { |code| [code, code + 18] }.merge(15 => 74).freeze
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# `bytes` stands in for the host file's contents, as a snapshot
|
|
31
|
+
# restores the image as it was.
|
|
32
|
+
def initialize(path, read_only: false, bytes: nil)
|
|
31
33
|
@path = path
|
|
32
34
|
@read_only = read_only
|
|
33
|
-
@bytes = File.binread(path).bytes
|
|
35
|
+
@bytes = bytes || File.binread(path).bytes
|
|
34
36
|
@errors = split_error_table
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# Whether the image was opened write-protected.
|
|
38
40
|
def read_only? = @read_only
|
|
39
41
|
|
|
42
|
+
# What Storage.reopen needs to open the image again as it stands: its
|
|
43
|
+
# path, whether it is write-protected, and its bytes with the error
|
|
44
|
+
# table after them.
|
|
45
|
+
def save_setup(out)
|
|
46
|
+
out.int(storage_kind).string(::File.expand_path(@path)).boolean(@read_only).blob(@bytes + @errors.to_a)
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
# A LOAD reads only PRG files. An OPEN names the type it wants, or
|
|
41
50
|
# takes the first file of any type with a nil `type`.
|
|
42
51
|
def read_file(name, type: :prg)
|
|
@@ -14,6 +14,11 @@ module Badline
|
|
|
14
14
|
@path = path
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# What Storage.reopen needs to open the directory again.
|
|
18
|
+
def save_setup(out)
|
|
19
|
+
out.int(HOST_DIRECTORY).string(::File.expand_path(@path))
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
def read_file(name, **)
|
|
18
23
|
entry = find(name)
|
|
19
24
|
return unless entry
|
data/lib/badline/storage/t64.rb
CHANGED
|
@@ -13,10 +13,16 @@ module Badline
|
|
|
13
13
|
ID = "T64".bytes.freeze
|
|
14
14
|
|
|
15
15
|
def initialize(path)
|
|
16
|
+
@path = path
|
|
16
17
|
@bytes = File.binread(path).bytes
|
|
17
18
|
raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# What Storage.reopen needs to open the archive again.
|
|
22
|
+
def save_setup(out)
|
|
23
|
+
out.int(T64_ARCHIVE).string(::File.expand_path(@path))
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def read_file(name, **)
|
|
21
27
|
pattern = Storage.matcher(name)
|
|
22
28
|
entry = entries.find { |e| pattern.match?(e[:name]) }
|
data/lib/badline/storage/tap.rb
CHANGED
|
@@ -13,9 +13,12 @@ module Badline
|
|
|
13
13
|
HEADER_SIZE = 0x14
|
|
14
14
|
OVERFLOW = 0x100 * 8
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# The file it was read from, expanded, and the offset of the next
|
|
17
|
+
# pulse byte.
|
|
18
|
+
attr_reader :version, :path
|
|
17
19
|
|
|
18
20
|
def initialize(path)
|
|
21
|
+
@path = File.expand_path(path)
|
|
19
22
|
@bytes = File.binread(path).bytes
|
|
20
23
|
raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
|
|
21
24
|
|
|
@@ -30,6 +33,12 @@ module Badline
|
|
|
30
33
|
@pos = HEADER_SIZE
|
|
31
34
|
end
|
|
32
35
|
|
|
36
|
+
def position = @pos
|
|
37
|
+
|
|
38
|
+
def position=(offset)
|
|
39
|
+
@pos = offset
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
def end? = @pos >= @end
|
|
34
43
|
|
|
35
44
|
# Cycles until the next falling edge, or nil once the tape has run out.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# Stands in for a directory or archive a detached restore leaves out
|
|
6
|
+
# (Snapshot::StateReader): it finds no files and refuses every write
|
|
7
|
+
# as a write-protected disk does.
|
|
8
|
+
class Unavailable
|
|
9
|
+
def read_file(_name, **) = nil
|
|
10
|
+
|
|
11
|
+
def write_file(_name, _bytes, **)
|
|
12
|
+
raise WriteError, WriteError::WRITE_PROTECT_ON
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def append_file(_name, _bytes, **)
|
|
16
|
+
raise WriteError, WriteError::WRITE_PROTECT_ON
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/badline/storage.rb
CHANGED
|
@@ -13,6 +13,7 @@ require "badline/storage/tap"
|
|
|
13
13
|
require "badline/storage/crt_file"
|
|
14
14
|
require "badline/storage/sid_file"
|
|
15
15
|
require "badline/storage/song_lengths"
|
|
16
|
+
require "badline/storage/unavailable"
|
|
16
17
|
|
|
17
18
|
module Badline
|
|
18
19
|
module Storage
|
|
@@ -33,7 +34,29 @@ module Badline
|
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# The storage a snapshot's device 8 serves, by the number save_setup
|
|
38
|
+
# writes first.
|
|
39
|
+
HOST_DIRECTORY = 0
|
|
40
|
+
D64 = 1
|
|
41
|
+
D71 = 2
|
|
42
|
+
D81 = 3
|
|
43
|
+
T64_ARCHIVE = 4
|
|
44
|
+
|
|
36
45
|
class << self
|
|
46
|
+
# Opens the storage a save_setup wrote: the same directory or archive
|
|
47
|
+
# by its path, or a disk image with the bytes it held, writes and
|
|
48
|
+
# all. A detached reader gets the image write-protected, and
|
|
49
|
+
# Unavailable for a directory or archive.
|
|
50
|
+
def reopen(input)
|
|
51
|
+
kind = input.int
|
|
52
|
+
path = input.string
|
|
53
|
+
case kind
|
|
54
|
+
when HOST_DIRECTORY then input.detached? ? Unavailable.new : HostDirectory.new(path)
|
|
55
|
+
when T64_ARCHIVE then input.detached? ? Unavailable.new : T64.new(path)
|
|
56
|
+
else reopen_image(kind, path, input)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
37
60
|
# Folds shifted PETSCII letters to their ASCII equivalents.
|
|
38
61
|
def ascii(bytes)
|
|
39
62
|
bytes.map { |b| b.between?(0xc1, 0xda) ? b - 0x80 : b }.pack("C*")
|
|
@@ -52,6 +75,17 @@ module Badline
|
|
|
52
75
|
[base.to_s, FILE_TYPES[type.to_s.strip[0]&.upcase]]
|
|
53
76
|
end
|
|
54
77
|
|
|
78
|
+
def reopen_image(kind, path, input)
|
|
79
|
+
read_only = input.boolean? || input.detached?
|
|
80
|
+
bytes = input.blob
|
|
81
|
+
case kind
|
|
82
|
+
when D64 then D64Image.new(path, read_only:, bytes:)
|
|
83
|
+
when D71 then D71Image.new(path, read_only:, bytes:)
|
|
84
|
+
when D81 then D81Image.new(path, read_only:, bytes:)
|
|
85
|
+
else raise Snapshot::FormatError, "unknown storage #{kind} in the state"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
55
89
|
# CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
|
|
56
90
|
# The DOS stops comparing at the first "*", so whatever follows it
|
|
57
91
|
# is ignored.
|
data/lib/badline/time_of_day.rb
CHANGED
|
@@ -7,6 +7,8 @@ module Badline
|
|
|
7
7
|
CLOCK_HZ = Region::PAL.clock_hz
|
|
8
8
|
MAINS_HZ = Region::PAL.mains_hz
|
|
9
9
|
|
|
10
|
+
FIELDS = %i[tenths seconds minutes hours].freeze
|
|
11
|
+
|
|
10
12
|
def initialize(clock_hz: CLOCK_HZ, mains_hz: MAINS_HZ)
|
|
11
13
|
# The accumulator advances mains_hz per cycle, so a TOD pin pulse has
|
|
12
14
|
# arrived when it reaches clock_hz. Integer math keeps it exact.
|
|
@@ -28,6 +30,28 @@ module Badline
|
|
|
28
30
|
@match = enabled ? 4 : 5
|
|
29
31
|
end
|
|
30
32
|
|
|
33
|
+
# The clock and the alarm, field by field in FIELDS order, the latch,
|
|
34
|
+
# and the divider's phase.
|
|
35
|
+
def save_state(out)
|
|
36
|
+
out.int(@accumulator).int(@pulses).int(@match).boolean(@stopped).boolean(@alarm_pending)
|
|
37
|
+
FIELDS.each { |field| out.int(@clock[field]).int(@alarm[field]) }
|
|
38
|
+
out.boolean(!@latch.nil?)
|
|
39
|
+
FIELDS.each { |field| out.int(@latch[field]) } if @latch
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_state(input)
|
|
43
|
+
@accumulator = input.int
|
|
44
|
+
@pulses = input.int
|
|
45
|
+
@match = input.int
|
|
46
|
+
@stopped = input.boolean?
|
|
47
|
+
@alarm_pending = input.boolean?
|
|
48
|
+
FIELDS.each do |field|
|
|
49
|
+
@clock[field] = input.int
|
|
50
|
+
@alarm[field] = input.int
|
|
51
|
+
end
|
|
52
|
+
@latch = input.boolean? ? FIELDS.to_h { |field| [field, input.int] } : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
31
55
|
def cycle!
|
|
32
56
|
@accumulator += @mains_hz
|
|
33
57
|
pulse! if @accumulator >= @cycles_per_pulse
|
|
@@ -51,6 +75,38 @@ module Badline
|
|
|
51
75
|
@latch[:hours]
|
|
52
76
|
end
|
|
53
77
|
|
|
78
|
+
attr_reader :pulses
|
|
79
|
+
|
|
80
|
+
# The clock as a read of the tenths would see it: the latch while the
|
|
81
|
+
# clock is latched. In FIELDS order.
|
|
82
|
+
def latched_fields = FIELDS.map { |field| (@latch || @clock)[field] }
|
|
83
|
+
|
|
84
|
+
# The cycles until the clock next steps a tenth, as VICE counts them.
|
|
85
|
+
def cycles_to_tenth
|
|
86
|
+
to_pulse = (@cycles_per_pulse - @accumulator + @mains_hz - 1) / @mains_hz
|
|
87
|
+
to_pulse + ([@match - @pulses, 0].max * @cycles_per_pulse / @mains_hz)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Sets the clock, the alarm and the latch from VICE's fields, in FIELDS
|
|
91
|
+
# order, the latch nil while unlatched.
|
|
92
|
+
def restore_fields(clock, alarm, latch)
|
|
93
|
+
FIELDS.each_with_index do |field, i|
|
|
94
|
+
@clock[field] = clock[i]
|
|
95
|
+
@alarm[field] = alarm[i]
|
|
96
|
+
end
|
|
97
|
+
@latch = latch && FIELDS.each_with_index.to_h { |field, i| [field, latch[i]] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sets whether the clock is stopped, the pulses counted towards the
|
|
101
|
+
# next tenth and the cycles until it, placing the divider so the tenth
|
|
102
|
+
# lands then.
|
|
103
|
+
def restore_divider(stopped, pulses, cycles_to_tenth)
|
|
104
|
+
@stopped = stopped
|
|
105
|
+
@pulses = pulses
|
|
106
|
+
to_pulse = cycles_to_tenth - ([@match - pulses, 0].max * @cycles_per_pulse / @mains_hz)
|
|
107
|
+
@accumulator = (@cycles_per_pulse - (to_pulse * @mains_hz)).clamp(0, @cycles_per_pulse - 1)
|
|
108
|
+
end
|
|
109
|
+
|
|
54
110
|
# The clock and alarm as stored, read without latching the clock.
|
|
55
111
|
def registers
|
|
56
112
|
[@clock[:tenths], @clock[:seconds], @clock[:minutes], @clock[:hours],
|
data/lib/badline/version.rb
CHANGED
|
@@ -55,6 +55,21 @@ module Badline
|
|
|
55
55
|
|
|
56
56
|
def active_c1_edge?(high) = high == @control.anybits?(0x01)
|
|
57
57
|
|
|
58
|
+
# The lines' levels and mode. Which flags they set, and whether reads
|
|
59
|
+
# handshake, are the port's wiring.
|
|
60
|
+
def save_state(out)
|
|
61
|
+
out.int(@control).boolean(@c1_high).boolean(@c2_high).boolean(@handshake_low).int(@pulse).boolean(@c2_output)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def load_state(input)
|
|
65
|
+
@control = input.int
|
|
66
|
+
@c1_high = input.boolean?
|
|
67
|
+
@c2_high = input.boolean?
|
|
68
|
+
@handshake_low = input.boolean?
|
|
69
|
+
@pulse = input.int
|
|
70
|
+
@c2_output = input.boolean?
|
|
71
|
+
end
|
|
72
|
+
|
|
58
73
|
# Everything the lines hold, for comparing them at two points.
|
|
59
74
|
def state = [@control, @c1_high, @c2_high, @handshake_low, @pulse, @c2_output]
|
|
60
75
|
|
|
@@ -67,6 +67,19 @@ module Badline
|
|
|
67
67
|
# changes it from cycle to cycle.
|
|
68
68
|
def clocking? = @bits.nonzero? && internal_clock?
|
|
69
69
|
|
|
70
|
+
def save_state(out)
|
|
71
|
+
out.int(@mode).int(@data).int(@bits).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def load_state(input)
|
|
75
|
+
@mode = input.int
|
|
76
|
+
@data = input.int
|
|
77
|
+
@bits = input.int
|
|
78
|
+
@clock = input.boolean?
|
|
79
|
+
@cb2 = input.boolean?
|
|
80
|
+
@cb2_input = input.boolean?
|
|
81
|
+
end
|
|
82
|
+
|
|
70
83
|
# Everything the register holds, for comparing it at two points.
|
|
71
84
|
def state = [@mode, @data, @bits, @clock, @cb2, @cb2_input]
|
|
72
85
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class VIA
|
|
5
|
+
# Saving and restoring a VIA for a snapshot.
|
|
6
|
+
module State
|
|
7
|
+
# The registers, the timers, the shift register, the interrupt flags
|
|
8
|
+
# and the control lines. The peripheral is the drive's wiring.
|
|
9
|
+
def save_state(out)
|
|
10
|
+
out.int(@ora).int(@orb).int(@ddra).int(@ddrb).int(@latch_a).int(@latch_b).int(@acr).int(@pcr)
|
|
11
|
+
out.boolean(@pb6_high).boolean(@sr_uses_t2)
|
|
12
|
+
@t1.save_state(out)
|
|
13
|
+
@t2.save_state(out)
|
|
14
|
+
@shift_register.save_state(out)
|
|
15
|
+
@ifr.save_state(out)
|
|
16
|
+
@ca.save_state(out)
|
|
17
|
+
@cb.save_state(out)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Puts the state back without telling the peripheral, which restores
|
|
21
|
+
# its own.
|
|
22
|
+
def load_state(input)
|
|
23
|
+
@ora = input.int
|
|
24
|
+
@orb = input.int
|
|
25
|
+
@ddra = input.int
|
|
26
|
+
@ddrb = input.int
|
|
27
|
+
@latch_a = input.int
|
|
28
|
+
@latch_b = input.int
|
|
29
|
+
@acr = input.int
|
|
30
|
+
@pcr = input.int
|
|
31
|
+
@pb6_high = input.boolean?
|
|
32
|
+
@sr_uses_t2 = input.boolean?
|
|
33
|
+
@t1.load_state(input)
|
|
34
|
+
@t2.load_state(input)
|
|
35
|
+
@shift_register.load_state(input)
|
|
36
|
+
@ifr.load_state(input)
|
|
37
|
+
@ca.load_state(input)
|
|
38
|
+
@cb.load_state(input)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|