badline 0.8.0 → 0.10.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.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +60 -19
  5. data/doc/library.md +10 -3
  6. data/doc/media.md +11 -2
  7. data/doc/pinned-behaviour.md +199 -9
  8. data/doc/sid-player.md +8 -4
  9. data/doc/snapshots.md +79 -33
  10. data/lib/badline/address_bus/pla.rb +130 -0
  11. data/lib/badline/address_bus/roms.rb +32 -0
  12. data/lib/badline/address_bus.rb +13 -92
  13. data/lib/badline/audio/bare_player.rb +24 -0
  14. data/lib/badline/audio/checkpoints.rb +42 -0
  15. data/lib/badline/audio/jukebox.rb +11 -4
  16. data/lib/badline/audio/machine_player.rb +16 -0
  17. data/lib/badline/audio/renderer.rb +80 -9
  18. data/lib/badline/audio/stereo.rb +3 -5
  19. data/lib/badline/audio.rb +1 -0
  20. data/lib/badline/chrout_trap.rb +4 -3
  21. data/lib/badline/computer/attachments.rb +52 -12
  22. data/lib/badline/computer/saved_state.rb +1 -0
  23. data/lib/badline/computer.rb +50 -33
  24. data/lib/badline/control_ports.rb +10 -3
  25. data/lib/badline/core.rb +51 -0
  26. data/lib/badline/data_path.rb +37 -0
  27. data/lib/badline/datasette.rb +63 -10
  28. data/lib/badline/drive1541/disk.rb +3 -3
  29. data/lib/badline/drive1541/disk_state.rb +14 -5
  30. data/lib/badline/drive1541/serial_port.rb +5 -8
  31. data/lib/badline/drive1541/sleep.rb +5 -6
  32. data/lib/badline/drive1541.rb +3 -3
  33. data/lib/badline/frontend/app.rb +63 -64
  34. data/lib/badline/frontend/boot.rb +34 -4
  35. data/lib/badline/frontend/confirmation.rb +17 -11
  36. data/lib/badline/frontend/controls.rb +12 -6
  37. data/lib/badline/frontend/drive_led.rb +9 -10
  38. data/lib/badline/frontend/frame_rate.rb +4 -4
  39. data/lib/badline/frontend/machine_window.rb +87 -0
  40. data/lib/badline/frontend/menu_dialogs.rb +52 -14
  41. data/lib/badline/frontend/menu_media.rb +70 -16
  42. data/lib/badline/frontend/menu_pages.rb +47 -42
  43. data/lib/badline/frontend/menu_ports.rb +69 -0
  44. data/lib/badline/frontend/name_field.rb +88 -0
  45. data/lib/badline/frontend/pacer.rb +6 -5
  46. data/lib/badline/frontend/pause_menu.rb +52 -37
  47. data/lib/badline/frontend/player_screen.rb +1 -1
  48. data/lib/badline/frontend/player_window.rb +4 -4
  49. data/lib/badline/frontend/screen.rb +39 -37
  50. data/lib/badline/frontend/sid_view.rb +1 -1
  51. data/lib/badline/frontend/snapshots.rb +145 -17
  52. data/lib/badline/frontend/sound.rb +17 -9
  53. data/lib/badline/frontend/timeline.rb +36 -5
  54. data/lib/badline/frontend.rb +3 -0
  55. data/lib/badline/iec_bus.rb +16 -28
  56. data/lib/badline/kernal_trap/drive.rb +3 -0
  57. data/lib/badline/kernal_trap/file.rb +1 -1
  58. data/lib/badline/kernal_trap/layout.rb +112 -0
  59. data/lib/badline/kernal_trap/load.rb +24 -25
  60. data/lib/badline/kernal_trap/routine.rb +17 -5
  61. data/lib/badline/kernal_trap/save.rb +13 -23
  62. data/lib/badline/kernal_trap/serial.rb +38 -24
  63. data/lib/badline/kernal_trap.rb +1 -0
  64. data/lib/badline/keyboard.rb +17 -14
  65. data/lib/badline/machine.rb +60 -0
  66. data/lib/badline/media/disk_list.rb +75 -0
  67. data/lib/badline/media/disk_set.rb +13 -3
  68. data/lib/badline/media/vic20_basic.rb +54 -0
  69. data/lib/badline/media/vic20_cartridge.rb +74 -0
  70. data/lib/badline/media/vic20_media.rb +132 -0
  71. data/lib/badline/media.rb +39 -20
  72. data/lib/badline/model.rb +62 -0
  73. data/lib/badline/options/event.rb +10 -6
  74. data/lib/badline/options/help.rb +25 -13
  75. data/lib/badline/options/table.rb +15 -1
  76. data/lib/badline/options/validation.rb +63 -3
  77. data/lib/badline/options.rb +37 -34
  78. data/lib/badline/region.rb +16 -4
  79. data/lib/badline/reu/ram.rb +27 -0
  80. data/lib/badline/reu/saved_state.rb +27 -0
  81. data/lib/badline/roms/README +37 -0
  82. data/lib/badline/roms/kernal-sx64.rom +0 -0
  83. data/lib/badline/roms/vic20/basic.rom +0 -0
  84. data/lib/badline/roms/vic20/character.rom +0 -0
  85. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  86. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  87. data/lib/badline/sid/decimator.rb +13 -0
  88. data/lib/badline/sid/filter.rb +6 -3
  89. data/lib/badline/sid.rb +19 -0
  90. data/lib/badline/snapshot/container.rb +2 -0
  91. data/lib/badline/snapshot/fields.rb +1 -0
  92. data/lib/badline/snapshot/image.rb +15 -0
  93. data/lib/badline/snapshot/setup.rb +20 -8
  94. data/lib/badline/snapshot/state.rb +15 -7
  95. data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
  96. data/lib/badline/snapshot/vice/peripherals.rb +12 -2
  97. data/lib/badline/snapshot/vice/restore.rb +3 -1
  98. data/lib/badline/snapshot/vice/reu1764.rb +55 -0
  99. data/lib/badline/snapshot/vice/vicii.rb +59 -13
  100. data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
  101. data/lib/badline/snapshot/vice.rb +25 -23
  102. data/lib/badline/snapshot.rb +10 -0
  103. data/lib/badline/storage/crt_file.rb +16 -5
  104. data/lib/badline/storage/disk_image.rb +3 -0
  105. data/lib/badline/storage/host_directory.rb +3 -0
  106. data/lib/badline/storage/t64.rb +10 -4
  107. data/lib/badline/storage/tap.rb +44 -8
  108. data/lib/badline/storage/unavailable.rb +3 -1
  109. data/lib/badline/storage.rb +5 -5
  110. data/lib/badline/timing.rb +12 -0
  111. data/lib/badline/version.rb +1 -1
  112. data/lib/badline/via/shift_register.rb +43 -16
  113. data/lib/badline/vic/bank.rb +53 -16
  114. data/lib/badline/vic/registers.rb +20 -4
  115. data/lib/badline/vic/sprite/timing.rb +5 -9
  116. data/lib/badline/vic/sprite.rb +4 -3
  117. data/lib/badline/vic.rb +34 -15
  118. data/lib/badline/vic20/attachments.rb +103 -0
  119. data/lib/badline/vic20/bus.rb +308 -0
  120. data/lib/badline/vic20/cpu.rb +10 -0
  121. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  122. data/lib/badline/vic20/port_wiring.rb +50 -0
  123. data/lib/badline/vic20/saved_state.rb +152 -0
  124. data/lib/badline/vic20/sound/output.rb +83 -0
  125. data/lib/badline/vic20/sound.rb +233 -0
  126. data/lib/badline/vic20/user_via_ports.rb +47 -0
  127. data/lib/badline/vic20/vic/painter.rb +239 -0
  128. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  129. data/lib/badline/vic20/vic/text_window.rb +71 -0
  130. data/lib/badline/vic20/vic.rb +267 -0
  131. data/lib/badline/vic20.rb +220 -0
  132. data/lib/badline.rb +2 -38
  133. metadata +37 -2
  134. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -35,10 +35,15 @@ module Badline
35
35
 
36
36
  # Where #stream starts: the frames before it are rendered as fast as
37
37
  # they go and yielded without their samples, so the seconds rendered
38
- # still count up to it. Set while streaming, it skips ahead from the
39
- # frame rendered last.
38
+ # still count up to it. Rendering picks up from the latest of the
39
+ # checkpoints at or before it, without the frames before that. Set
40
+ # while streaming, it skips ahead from the frame rendered last.
40
41
  attr_writer :from
41
42
 
43
+ # The Checkpoints this subtune's renderers keep as they go and start
44
+ # from, for a player that seeks. Without them nothing is kept.
45
+ attr_writer :checkpoints
46
+
42
47
  def initialize(tune, seconds:, subtune: nil, rate: DEFAULT_RATE, sid_models: tune.sid_models)
43
48
  @tune = tune
44
49
  @seconds = seconds
@@ -52,6 +57,8 @@ module Badline
52
57
  @rendered = 0.0
53
58
  @observer = ->(_samples, _rendered) {}
54
59
  @from = 0.0
60
+ @played = 0
61
+ @checkpoints = nil
55
62
  end
56
63
 
57
64
  def silence=(seconds)
@@ -65,7 +72,6 @@ module Badline
65
72
  # Yields the seconds rendered so far after every frame.
66
73
  def render(path)
67
74
  container = container_for(path)
68
- player.start
69
75
  container.open(path, rate: @rate, channels: 2) do |writer|
70
76
  each_frame do |samples, seconds|
71
77
  samples.each { |sample| writer << sample }
@@ -76,9 +82,16 @@ module Badline
76
82
 
77
83
  # Starts the tune, then yields each frame's samples along with the
78
84
  # seconds rendered so far.
79
- def stream(&)
80
- player.start
81
- each_frame(&)
85
+ def stream(&) = each_frame(&)
86
+
87
+ # Whether the latest checkpoint at or before `seconds` lies past the
88
+ # frame rendered last, so that a seek there gets there sooner from it
89
+ # than on from here.
90
+ def checkpoint_ahead?(seconds)
91
+ checkpoints = @checkpoints
92
+ return false if checkpoints.nil?
93
+
94
+ checkpoints.reach((seconds * player.clock_hz).round) > @played
82
95
  end
83
96
 
84
97
  private
@@ -101,19 +114,77 @@ module Badline
101
114
  end
102
115
 
103
116
  def each_frame
104
- player.stereo.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
117
+ clock_hz = player.clock_hz
105
118
  total = total_cycles
106
- remaining = total
119
+ @played = start((@from * clock_hz).round)
120
+ remaining = total - @played
107
121
  while remaining.positive?
108
122
  samples = []
109
123
  remaining -= player.frame(remaining) { |sample| samples << sample }
110
- @rendered = (total - remaining).fdiv(player.clock_hz)
124
+ played = total - remaining
125
+ @played = played
126
+ @rendered = played.fdiv(clock_hz)
111
127
  @observer.call(samples, @rendered)
112
128
  yield(@rendered < @from ? [] : samples, @rendered)
113
129
  break if fallen_silent?(samples)
130
+
131
+ save_checkpoint(played, clock_hz)
114
132
  end
115
133
  end
116
134
 
135
+ # Starts the player and its recording, from the latest checkpoint at
136
+ # or before `cycles` if there is one, and returns the cycles into the
137
+ # subtune it starts at.
138
+ def start(cycles)
139
+ state = @checkpoints&.latest(cycles)
140
+ return start_afresh if state.nil?
141
+
142
+ input = Snapshot::StateReader.new(state)
143
+ played = input.int
144
+ resume(input)
145
+ record
146
+ player.stereo.sids.each { |sid| sid.load_recording(input) }
147
+ @level = input.int
148
+ @still = input.int
149
+ played
150
+ end
151
+
152
+ def start_afresh
153
+ player.start
154
+ record
155
+ 0
156
+ end
157
+
158
+ def record = player.stereo.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
159
+
160
+ # Restores the player as it was, then puts back the SID models chosen
161
+ # for this renderer.
162
+ def resume(input)
163
+ sids = player.stereo.sids
164
+ chosen = sids.map(&:model)
165
+ sids.each { |sid| refit(sid, Snapshot::Setup::SID_MODELS.fetch(input.int)) }
166
+ player.load_state(input)
167
+ sids.each_with_index { |sid, index| refit(sid, chosen[index]) }
168
+ end
169
+
170
+ def refit(sid, model)
171
+ sid.model = model unless sid.model == model
172
+ end
173
+
174
+ def save_checkpoint(played, clock_hz)
175
+ checkpoints = @checkpoints
176
+ return if checkpoints.nil? || !checkpoints.due?(played, clock_hz)
177
+
178
+ sids = player.stereo.sids
179
+ out = Snapshot::StateWriter.new
180
+ out.int(played)
181
+ sids.each { |sid| out.int(Snapshot::Setup::SID_MODELS.index(sid.model) || 0) }
182
+ player.save_state(out)
183
+ sids.each { |sid| sid.save_recording(out) }
184
+ out.int(@level).int(@still)
185
+ checkpoints.keep(played, out.state)
186
+ end
187
+
117
188
  # Counts the samples since the output last strayed more than QUIET
118
189
  # from where it settled.
119
190
  def fallen_silent?(samples)
@@ -65,11 +65,9 @@ module Badline
65
65
  private
66
66
 
67
67
  def both(samples)
68
- i = 0
69
- while i < samples.size
70
- yield samples[i]
71
- yield samples[i]
72
- i += 1
68
+ samples.each do |sample|
69
+ yield sample
70
+ yield sample
73
71
  end
74
72
  end
75
73
 
data/lib/badline/audio.rb CHANGED
@@ -6,6 +6,7 @@ require "badline/audio/aiff"
6
6
  require "badline/audio/stereo"
7
7
  require "badline/audio/bare_player"
8
8
  require "badline/audio/machine_player"
9
+ require "badline/audio/checkpoints"
9
10
  require "badline/audio/renderer"
10
11
  require "badline/audio/playback"
11
12
  require "badline/audio/terminal"
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Badline
4
4
  # Observe-only PC trap on the KERNAL CHROUT routine ($FFD2). Records each
5
- # character written while the KERNAL is banked in, then lets execution fall
5
+ # character written while the layout finds the KERNAL mapped in, then lets execution fall
6
6
  # through to the ROM, so the screen output is unaffected. Used for headless
7
7
  # capture of program output.
8
8
  class ChroutTrap
@@ -10,14 +10,15 @@ module Badline
10
10
 
11
11
  attr_reader :output
12
12
 
13
- def initialize(cpu:, bus:)
13
+ def initialize(cpu:, bus:, layout:)
14
14
  @cpu = cpu
15
15
  @bus = bus
16
+ @layout = layout
16
17
  @output = +""
17
18
  end
18
19
 
19
20
  def call
20
- @output << ascii(@cpu.a) if @bus.io_port.kernal?
21
+ @output << ascii(@cpu.a) if @layout.kernal_mapped?(@bus)
21
22
  end
22
23
 
23
24
  def inspect
@@ -22,11 +22,13 @@ module Badline
22
22
  return @drive.insert(storage) if @drive
23
23
 
24
24
  @drive = KernalTrap::Drive.new(storage)
25
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
26
- cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
27
- @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
28
- save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
29
- cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
25
+ layout = KernalTrap::C64_LAYOUT
26
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
27
+ cpu.install_trap(layout.load) { load_trap.call }
28
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
29
+ device: serial_trap_device).install
30
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
31
+ cpu.install_trap(layout.save) { save_trap.call }
30
32
  end
31
33
 
32
34
  # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
@@ -35,8 +37,8 @@ module Badline
35
37
  def unmount
36
38
  return unless @drive
37
39
 
38
- cpu.remove_trap(KernalTrap::Load::ADDRESS)
39
- cpu.remove_trap(KernalTrap::Save::ADDRESS)
40
+ cpu.remove_trap(KernalTrap::C64_LAYOUT.load)
41
+ cpu.remove_trap(KernalTrap::C64_LAYOUT.save)
40
42
  @serial_trap.device = nil
41
43
  @serial_trap = nil
42
44
  @save_trap = nil
@@ -46,17 +48,55 @@ module Badline
46
48
  # Whether device 8 serves a disk or directory through the traps.
47
49
  def mounted? = !@drive.nil?
48
50
 
51
+ # The serial bus, which CIA 2's port A drives through the C64's own
52
+ # inverters and reads back on PA6 and PA7, with or without a drive on
53
+ # it.
54
+ attr_reader :iec_bus
55
+
56
+ # Plugs in a Drive1541, which then runs alongside the C64 on its own
57
+ # clock and talks to it over the serial bus. The serial traps stop
58
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN and
59
+ # byte transfers reach the drive. The LOAD and SAVE traps stay, and
60
+ # still serve a mounted image.
61
+ def attach_drive1541(drive)
62
+ @iec_bus.detach(@drive1541) if @drive1541
63
+ drive.host_clock_hz = region.clock_hz
64
+ @drive1541 = drive
65
+ drive.connect(iec_bus)
66
+ @serial_trap&.device = serial_trap_device
67
+ end
68
+
49
69
  # Unplugs the Drive1541, leaving the serial bus with nothing on it.
50
70
  def detach_drive1541
51
71
  return unless @drive1541
52
72
 
53
- @iec_bus&.detach(@drive1541)
73
+ @iec_bus.detach(@drive1541)
54
74
  @drive1541 = nil
55
75
  @serial_trap&.device = serial_trap_device
56
76
  end
57
77
 
58
78
  private
59
79
 
80
+ # The device number the serial traps answer: device 8, unless a true
81
+ # drive is on the bus as device 8.
82
+ def serial_trap_device
83
+ @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
84
+ end
85
+
86
+ # CIA 2's port A drives the serial bus, which reads CLK and DATA back
87
+ # into it.
88
+ def plug_serial_bus
89
+ @iec_bus = IECBus.new
90
+ @cia2.peripheral = @iec_bus
91
+ @cia2.on_port_a_write { push_serial_lines }
92
+ push_serial_lines
93
+ end
94
+
95
+ # Pushes the lines CIA 2's port A pulls into the serial bus.
96
+ def push_serial_lines
97
+ @iec_bus.host_lines = @cia2.port_a_lines
98
+ end
99
+
60
100
  def save_cartridge(out)
61
101
  cartridge = address_bus.cartridge
62
102
  out.boolean(!cartridge.nil?)
@@ -95,10 +135,10 @@ module Badline
95
135
  @save_trap.load_state(input)
96
136
  end
97
137
 
98
- # The serial bus, whose presence decides what CIA 2's port A reads,
99
- # and a true 1541 on it.
138
+ # A true 1541 on the serial bus. The leading flag is the bus itself,
139
+ # which every machine now has, kept so older snapshots still load.
100
140
  def save_serial_bus(out)
101
- out.boolean(!@iec_bus.nil?).boolean(!@drive1541.nil?)
141
+ out.boolean(true).boolean(!@drive1541.nil?)
102
142
  return unless @drive1541
103
143
 
104
144
  out.int(@drive1541.device)
@@ -106,7 +146,7 @@ module Badline
106
146
  end
107
147
 
108
148
  def load_serial_bus(input)
109
- iec_bus if input.boolean?
149
+ input.boolean?
110
150
  return detach_drive1541 unless input.boolean?
111
151
 
112
152
  device = input.int
@@ -97,6 +97,7 @@ module Badline
97
97
  @init_handlers_lost = [input.int - @init_handlers.length, 0].max
98
98
  load_cartridge(input)
99
99
  address_bus.load_state(input)
100
+ push_serial_lines
100
101
  @cpu.load_state(input)
101
102
  load_trap_drive(input)
102
103
  load_serial_bus(input)
@@ -12,6 +12,12 @@ module Badline
12
12
 
13
13
  attr_reader :address_bus, :cpu, :cycles, :drive1541
14
14
 
15
+ # The path of the disk or directory device 8 serves through the traps
16
+ # (Attachments), or an empty one.
17
+ def mounted_path = @drive.nil? ? "" : @drive.path
18
+
19
+ def family = :c64
20
+
15
21
  def region = address_bus.region
16
22
 
17
23
  def vic = address_bus.vic
@@ -42,9 +48,13 @@ module Badline
42
48
  # TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
43
49
  # every region boots the same ROMs. reu plugs an REU of that many K
44
50
  # into the expansion port, where it drives the IRQ line and takes the
45
- # bus for its transfers.
46
- def initialize(debug: false, reu: nil, **machine)
51
+ # bus for its transfers. kernal: names the KERNAL ROM
52
+ # (AddressBus::ROMs::KERNALS), and datasette: false leaves the cassette port
53
+ # empty, as on the SX-64.
54
+ def initialize(debug: false, reu: nil, kernal: :c64, datasette: true, **machine)
47
55
  @address_bus = AddressBus.new(**machine)
56
+ @address_bus.kernal = kernal unless kernal == :c64
57
+ @address_bus.datasette.disconnect! unless datasette
48
58
  @cpu = CPU.new(@address_bus, debug:)
49
59
  @vic = @address_bus.vic
50
60
  @vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
@@ -66,7 +76,7 @@ module Badline
66
76
  @serial_trap = nil
67
77
  @save_trap = nil
68
78
  @drive1541 = nil
69
- @iec_bus = nil
79
+ plug_serial_bus
70
80
  @reu = reu ? plug_reu(reu) : nil
71
81
  end
72
82
 
@@ -94,6 +104,40 @@ module Badline
94
104
  @cycles += 1
95
105
  end
96
106
 
107
+ # Runs `count` cycles, one #cycle! after another.
108
+ def run_cycles(count)
109
+ i = 0
110
+ while i < count
111
+ cycle!
112
+ i += 1
113
+ end
114
+ end
115
+
116
+ # Runs cycles until the block returns true or the cycle count passes
117
+ # `limit`, checking before each cycle.
118
+ def run_until(limit)
119
+ cycle! until yield || @cycles > limit
120
+ end
121
+
122
+ # The cycle at which #on_init's handlers run, once the KERNAL has booted.
123
+ def init_threshold
124
+ INIT_THRESHOLD
125
+ end
126
+
127
+ # The clock, the raster and the crop of the machine's region, a pixel
128
+ # to a window pixel.
129
+ def timing
130
+ region = address_bus.region
131
+ Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
132
+ lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
133
+ end
134
+
135
+ # The chip whose #display a front end shows.
136
+ def video = @vic
137
+
138
+ # The chip a front end records the machine's sound from.
139
+ def sound_source = @sid
140
+
97
141
  def load_prg(data)
98
142
  uint16(data[0], data[1]).tap do |load_addr|
99
143
  ram.write(load_addr, data[2..])
@@ -155,28 +199,11 @@ module Badline
155
199
  @restore_pulse = true
156
200
  end
157
201
 
158
- # Plugs in a Drive1541, which then runs alongside the C64 on its own
159
- # clock and talks to it over the serial bus. The serial traps stop
160
- # answering the drive's device number, so the KERNAL's TALK, LISTEN and
161
- # byte transfers reach the drive. The LOAD and SAVE traps stay, and
162
- # still serve a mounted image.
163
- def attach_drive1541(drive)
164
- @iec_bus.detach(@drive1541) if @iec_bus && @drive1541
165
- drive.host_clock_hz = region.clock_hz
166
- @drive1541 = drive
167
- drive.connect(iec_bus)
168
- @serial_trap&.device = serial_trap_device
169
- end
170
-
171
- # The serial bus. CIA 2's port A joins it the first time it's asked
172
- # for, which plugging in a drive does. Until then port A's serial
173
- # inputs float high.
174
- def iec_bus
175
- @iec_bus ||= IECBus.new(host: @cia2).tap(&:plug_in!)
176
- end
202
+ # The one-shot has pulsed already, so letting go does nothing.
203
+ def release_restore; end
177
204
 
178
205
  def capture_output
179
- @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
206
+ @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus, layout: KernalTrap::C64_LAYOUT).tap do |trap|
180
207
  cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
181
208
  end
182
209
  end
@@ -195,12 +222,6 @@ module Badline
195
222
 
196
223
  private
197
224
 
198
- # The device number the serial traps answer: device 8, unless a true
199
- # drive is on the bus as device 8.
200
- def serial_trap_device
201
- @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
202
- end
203
-
204
225
  # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
205
226
  # key, and the CPU takes an interrupt on its falling edge.
206
227
  def drive_nmi
@@ -256,9 +277,5 @@ module Badline
256
277
  def handle_init
257
278
  @init_handlers.each(&:call)
258
279
  end
259
-
260
- def init_threshold
261
- INIT_THRESHOLD
262
- end
263
280
  end
264
281
  end
@@ -16,13 +16,19 @@ module Badline
16
16
  # resistances; close neither and the lines float high. Reading a pot asks the
17
17
  # CIA for the current select lines, since a program sets them and then goes
18
18
  # straight to SID without touching the ports again.
19
+ #
20
+ # A matrix may have rows beyond port A's eight. Their select lines come from
21
+ # outside the CIA and are pushed into +extra_rows+, bit 0 for the ninth row
22
+ # upwards, active low like port A; idle, every line is high and none of
23
+ # those rows is selected. They settle with port A's lines as rows 8 and up,
24
+ # so their keys reach port B, but port A reads only its own eight.
19
25
  class ControlPorts
20
26
  PORT1_POTS = 0x40
21
27
  PORT2_POTS = 0x80
22
28
  FIRE = 0x10
23
29
 
24
30
  attr_reader :keyboard, :joystick1, :joystick2
25
- attr_accessor :device1, :device2, :port_a_source
31
+ attr_accessor :device1, :device2, :port_a_source, :extra_rows
26
32
 
27
33
  def initialize(keyboard:, joystick1:, joystick2:)
28
34
  @keyboard = keyboard
@@ -31,9 +37,10 @@ module Badline
31
37
  @device1 = nil
32
38
  @device2 = nil
33
39
  @port_a_source = nil
40
+ @extra_rows = 0xff
34
41
  end
35
42
 
36
- def read_a(port_a, port_b) = scan(port_a, port_b).first
43
+ def read_a(port_a, port_b) = scan(port_a, port_b).first & 0xff
37
44
 
38
45
  def read_b(port_a, port_b) = scan(port_a, port_b).last
39
46
 
@@ -58,7 +65,7 @@ module Badline
58
65
  end
59
66
 
60
67
  def scan(port_a, port_b)
61
- keyboard.scan(port_a & port_bits(@joystick2, @device2),
68
+ keyboard.scan((port_a & port_bits(@joystick2, @device2)) | (@extra_rows << 8),
62
69
  port_b & port_bits(@joystick1, @device1))
63
70
  end
64
71
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The emulator core: the machine, its chips, media and traps, and the
4
+ # snapshot state Computer saves and restores with, without the options,
5
+ # the rest of the snapshot code, audio or front end. lib/badline.rb and
6
+ # the Spinel harnesses in spinel/ load it, so a file the core needs is
7
+ # added here.
8
+ require "badline/version"
9
+ require "badline/integer_helper"
10
+ require "badline/region"
11
+ require "badline/timing"
12
+ require "badline/model"
13
+ require "badline/addressable"
14
+ require "badline/memory"
15
+ require "badline/color_memory"
16
+ require "badline/rom"
17
+ require "badline/ram_expansion"
18
+ require "badline/address_bus"
19
+ require "badline/instruction"
20
+ require "badline/instruction_set"
21
+ require "badline/status"
22
+ require "badline/keyboard"
23
+ require "badline/joystick"
24
+ require "badline/control_ports"
25
+ require "badline/input"
26
+ require "badline/cycleable"
27
+ require "badline/datasette"
28
+ require "badline/time_of_day"
29
+ require "badline/cia"
30
+ require "badline/via"
31
+ require "badline/sid"
32
+ require "badline/debug_register"
33
+ require "badline/reu"
34
+ require "badline/interrupts"
35
+ require "badline/traps"
36
+ require "badline/cpu"
37
+ require "badline/vic"
38
+ require "badline/keyboard_buffer"
39
+ require "badline/iec_bus"
40
+ require "badline/drive1541"
41
+ require "badline/snapshot/state"
42
+ require "badline/snapshot/setup"
43
+ require "badline/computer"
44
+ require "badline/vic20"
45
+ require "badline/checkpoint"
46
+ require "badline/storage"
47
+ require "badline/cartridge"
48
+ require "badline/kernal_trap"
49
+ require "badline/chrout_trap"
50
+ require "badline/media"
51
+ require "badline/machine"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Badline
6
+ class << self
7
+ attr_writer :data_path
8
+
9
+ # The per-user folder badline keeps its own files in. Defaults to
10
+ # $BADLINE_DATA_PATH, then to the platform's folder for application
11
+ # data. Assigning nil restores the default. Nothing is created until
12
+ # data_folder is asked for a subfolder.
13
+ def data_path
14
+ @data_path ||= ENV.fetch("BADLINE_DATA_PATH", nil) || default_data_path(RUBY_PLATFORM)
15
+ end
16
+
17
+ # ~/Library/Application Support/badline on macOS, and
18
+ # $XDG_DATA_HOME/badline or ~/.local/share/badline elsewhere.
19
+ def default_data_path(platform)
20
+ return File.join(Dir.home, "Library", "Application Support", "badline") if platform.include?("darwin")
21
+
22
+ xdg = ENV.fetch("XDG_DATA_HOME", nil)
23
+ xdg = File.join(Dir.home, ".local", "share") if xdg.nil? || !xdg.start_with?("/")
24
+ File.join(xdg, "badline")
25
+ end
26
+
27
+ # The quicksaves, autosaves or saves subfolder of data_path, created
28
+ # along with data_path if it doesn't exist yet.
29
+ def data_folder(name)
30
+ raise ArgumentError, "unknown data folder: #{name}" unless %w[quicksaves autosaves saves].include?(name)
31
+
32
+ path = File.join(data_path, name)
33
+ FileUtils.mkdir_p(path)
34
+ path
35
+ end
36
+ end
37
+ end