badline 0.5.0 → 0.6.0
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 +24 -0
- data/CLAUDE.md +23 -23
- data/CONTRIBUTING.md +5 -0
- data/README.md +59 -1
- data/doc/pinned-behaviour.md +4 -2
- data/exe/badline-ruby +2 -1
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +1 -0
- data/lib/badline/audio/stream.rb +6 -0
- 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 +24 -2
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +8 -31
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +2 -0
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +89 -13
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/idle.rb +9 -57
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/orbit.rb +81 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541/sleep.rb +114 -0
- data/lib/badline/drive1541.rb +52 -2
- data/lib/badline/gui/application.rb +41 -14
- data/lib/badline/gui/gamepads.rb +7 -0
- data/lib/badline/gui/snapshots.rb +67 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +16 -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 +11 -0
- data/lib/badline/options.rb +3 -2
- 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.rb +36 -0
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +52 -0
- data/lib/badline/reu.rb +4 -0
- data/lib/badline/sdl/events.rb +2 -0
- 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/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +2 -0
- 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/fast_forward.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 +19 -0
- data/lib/badline/via/timer2.rb +24 -0
- data/lib/badline/via.rb +2 -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 +94 -10
- data/lib/badline.rb +1 -0
- metadata +37 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
# VIC-II 1.3 as x64sc writes it, and VIC-IISC 1.4, the same fields
|
|
7
|
+
# under a new name, as VICE's development versions write it: the model, the registers, the beam's
|
|
8
|
+
# line and cycle, the IRQ latch, the video counters and buffers, the
|
|
9
|
+
# light pen, the borders, colour RAM and each sprite's counters, then
|
|
10
|
+
# x64sc's pixel pipeline and its frame buffer.
|
|
11
|
+
#
|
|
12
|
+
# Written: what badline's VIC holds in VICE's terms. The pixel
|
|
13
|
+
# pipeline and frame buffer are left blank, as VICE repaints on
|
|
14
|
+
# loading.
|
|
15
|
+
#
|
|
16
|
+
# Read: the registers, colour RAM, the IRQ latch and collisions, the
|
|
17
|
+
# video counters and buffers and the sprite counters. The VIC is put
|
|
18
|
+
# at the start of the snapshot's line and clocked on its own to the
|
|
19
|
+
# snapshot's cycle, so its fetch and sprite state match the line, then
|
|
20
|
+
# takes the snapshot's counters. Its pixel pipeline starts empty, so
|
|
21
|
+
# the first pixels drawn may differ.
|
|
22
|
+
module VICII
|
|
23
|
+
NAME = "VIC-II"
|
|
24
|
+
TRUNK_NAME = "VIC-IISC"
|
|
25
|
+
MAJOR = 1
|
|
26
|
+
MINOR = 3
|
|
27
|
+
VERSIONS = ["VIC-II 1.3", "VIC-IISC 1.4"].freeze
|
|
28
|
+
MODELS = %i[mos6569 mos8565].freeze
|
|
29
|
+
FRAME_WIDTH = 384
|
|
30
|
+
FRAME_HEIGHT = 312
|
|
31
|
+
# The pixel pipeline x64sc keeps between cycles, in bytes.
|
|
32
|
+
PIPELINE = 16 + 32 + 6 + 32 + 8 + 3 + 24 + 0x2f + 2 + 4
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def export(computer)
|
|
37
|
+
vic = computer.vic
|
|
38
|
+
fields = FieldWriter.new.byte(MODELS.index(vic.model)).bytes(vic.register_file)
|
|
39
|
+
beam(vic, fields)
|
|
40
|
+
counters(vic, fields)
|
|
41
|
+
borders(vic, fields)
|
|
42
|
+
fields.bytes(Array.new(0x400) { |offset| computer.address_bus.color_ram.nibble(0xd800 + offset) })
|
|
43
|
+
8.times { |n| sprite(vic, n, fields) }
|
|
44
|
+
pipeline(vic, fields)
|
|
45
|
+
fields.dword(vic.rasterline).dword(FRAME_WIDTH).dword(FRAME_HEIGHT).dword(FRAME_WIDTH)
|
|
46
|
+
fields.zeros(FRAME_WIDTH * (FRAME_HEIGHT + 4))
|
|
47
|
+
fields.section(NAME, MAJOR, MINOR)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def beam(vic, fields)
|
|
51
|
+
registers = vic.registers
|
|
52
|
+
fields.dword(vic.column).dword(0).dword(vic.rasterline).byte(0)
|
|
53
|
+
fields.byte((registers[0x19] & 0x0f) | (vic.interrupted? ? 0x80 : 0))
|
|
54
|
+
fields.dword(registers.raster_target).flag(vic.latch_bits.anybits?(1))
|
|
55
|
+
[vic.character_buffer, vic.color_buffer].each { |buffer| fields.bytes(buffer.map { |value| value || 0 }) }
|
|
56
|
+
fields.byte(0).dword(0).zeros(65 * 8) # x64sc's graphics buffer and draw buffer
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def counters(vic, fields)
|
|
60
|
+
registers = vic.registers
|
|
61
|
+
state = vic.display_state
|
|
62
|
+
fields.dword(registers.yscroll).flag(state.bad_lines_enabled)
|
|
63
|
+
fields.byte(registers[0x1e]).byte(registers[0x1f]).byte(0)
|
|
64
|
+
fields.dword(state.display? ? 0 : 1)
|
|
65
|
+
[state.vc_base, state.vc, state.rc, state.vmli].each { |counter| fields.dword(counter) }
|
|
66
|
+
fields.dword(state.bad_line_condition? ? 1 : 0)
|
|
67
|
+
fields.flag(vic.latch_bits.anybits?(2)).flag(vic.latch_bits.anybits?(4))
|
|
68
|
+
fields.dword(registers[0x13]).dword(registers[0x14]).dword(0).qword(0)
|
|
69
|
+
fields.byte(vic.fetch_d011).dword(0)
|
|
70
|
+
fields.dword(sprite_bits(vic, &:display_on)).byte(sprite_bits(vic, &:displaying?))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def borders(vic, fields)
|
|
74
|
+
fields.byte(0xff).byte(0).byte(0).byte(0) # last colour register and value, last bus bytes
|
|
75
|
+
vic.sequencer.borders.each { |flop| fields.flag(flop) }
|
|
76
|
+
fields.byte(0xff - (5 * vic.rasterline))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sprite(vic, index, fields)
|
|
80
|
+
sprite = vic.sprites[index]
|
|
81
|
+
fields.dword(sprite.sr).byte(sprite.mc).byte(sprite.mcbase).byte(0).flag(sprite.exp_ff).dword(sprite.x)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Blank but for the colour registers and the sprite priority,
|
|
85
|
+
# multicolour and expansion bits, which x64sc keeps copies of.
|
|
86
|
+
def pipeline(vic, fields)
|
|
87
|
+
file = vic.register_file
|
|
88
|
+
fields.zeros(16 + 32).bytes([file[0x1b], file[0x1c], file[0x1d], 0, 0, 0])
|
|
89
|
+
fields.zeros(32 + 8 + 3 + 24).bytes(file.first(0x2f)).byte(0xff).byte(0).dword(0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def sprite_bits(vic)
|
|
93
|
+
8.times.sum { |n| yield(vic.sprites[n]) ? 1 << n : 0 }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The module a snapshot has, under either name.
|
|
97
|
+
def find(container) = container[NAME] || container[TRUNK_NAME]
|
|
98
|
+
|
|
99
|
+
def reads?(section) = VERSIONS.include?(section.to_s)
|
|
100
|
+
|
|
101
|
+
# VICE's model numbers: the 6569, the 8565 and the 6569R1 run PAL;
|
|
102
|
+
# the 6567, 8562 and 6567R56A NTSC, and the 6572 PAL-N, which badline
|
|
103
|
+
# doesn't run yet.
|
|
104
|
+
def model(section)
|
|
105
|
+
number = FieldReader.new(section).byte
|
|
106
|
+
return :mos8565 if number == 1
|
|
107
|
+
return :mos6569 if [0, 2].include?(number)
|
|
108
|
+
|
|
109
|
+
raise FormatError, "#{section}: an NTSC or PAL-N VIC-II (VICE model #{number}), which badline doesn't run"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def import(section, computer)
|
|
113
|
+
snapshot = Import.new(FieldReader.new(section))
|
|
114
|
+
snapshot.apply(computer)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -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