badline 0.10.1 → 0.11.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/CLAUDE.md +5 -3
  4. data/README.md +59 -4
  5. data/doc/pinned-behaviour.md +75 -0
  6. data/doc/snapshots.md +15 -0
  7. data/lib/badline/address_bus/pla.rb +3 -16
  8. data/lib/badline/address_bus.rb +14 -0
  9. data/lib/badline/c128/banks.rb +35 -0
  10. data/lib/badline/c128/bus.rb +347 -0
  11. data/lib/badline/c128/bus_state.rb +53 -0
  12. data/lib/badline/c128/color_lines.rb +39 -0
  13. data/lib/badline/c128/cpu.rb +10 -0
  14. data/lib/badline/c128/keys.rb +35 -0
  15. data/lib/badline/c128/mmu.rb +182 -0
  16. data/lib/badline/c128/mmu_pages.rb +105 -0
  17. data/lib/badline/c128/model.rb +39 -0
  18. data/lib/badline/c128/modes.rb +48 -0
  19. data/lib/badline/c128/saved_state.rb +120 -0
  20. data/lib/badline/c128/vdc/memory.rb +54 -0
  21. data/lib/badline/c128/vdc/painter.rb +143 -0
  22. data/lib/badline/c128/vdc/raster.rb +97 -0
  23. data/lib/badline/c128/vdc/window.rb +215 -0
  24. data/lib/badline/c128/vdc.rb +268 -0
  25. data/lib/badline/c128.rb +320 -0
  26. data/lib/badline/chrout_trap.rb +3 -0
  27. data/lib/badline/computer/attachments.rb +4 -13
  28. data/lib/badline/computer/kernal_traps.rb +29 -0
  29. data/lib/badline/computer.rb +5 -0
  30. data/lib/badline/frontend/app.rb +34 -21
  31. data/lib/badline/frontend/boot.rb +31 -1
  32. data/lib/badline/frontend/controls.rb +11 -0
  33. data/lib/badline/frontend/keys.rb +28 -1
  34. data/lib/badline/frontend/machine_window.rb +16 -13
  35. data/lib/badline/frontend/menu_pages.rb +6 -1
  36. data/lib/badline/frontend/menu_ports.rb +1 -1
  37. data/lib/badline/frontend/pause_menu.rb +1 -0
  38. data/lib/badline/frontend/screen.rb +33 -8
  39. data/lib/badline/frontend/screens.rb +90 -0
  40. data/lib/badline/frontend/timeline.rb +15 -4
  41. data/lib/badline/frontend.rb +1 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +58 -8
  44. data/lib/badline/kernal_trap/load.rb +1 -1
  45. data/lib/badline/kernal_trap/save.rb +1 -1
  46. data/lib/badline/kernal_trap/serial.rb +5 -0
  47. data/lib/badline/machine.rb +15 -6
  48. data/lib/badline/media.rb +8 -6
  49. data/lib/badline/options/event.rb +5 -1
  50. data/lib/badline/options/family.rb +52 -0
  51. data/lib/badline/options/help.rb +5 -1
  52. data/lib/badline/options/table.rb +7 -1
  53. data/lib/badline/options/validation.rb +14 -1
  54. data/lib/badline/options.rb +20 -19
  55. data/lib/badline/roms/README +27 -0
  56. data/lib/badline/roms/c128/basic.rom +0 -0
  57. data/lib/badline/roms/c128/character.rom +0 -0
  58. data/lib/badline/roms/c128/kernal.rom +0 -0
  59. data/lib/badline/snapshot/container.rb +2 -0
  60. data/lib/badline/snapshot/image.rb +14 -0
  61. data/lib/badline/snapshot.rb +9 -2
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/display_state.rb +12 -0
  64. data/lib/badline/vic/registers.rb +3 -0
  65. data/lib/badline/vic.rb +108 -7
  66. data/lib/badline.rb +1 -0
  67. metadata +24 -1
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/c128/model"
4
+ require "badline/c128/mmu"
5
+ require "badline/c128/vdc"
6
+ require "badline/c128/mmu_pages"
7
+ require "badline/c128/color_lines"
8
+ require "badline/c128/banks"
9
+ require "badline/c128/bus_state"
10
+ require "badline/c128/bus"
11
+ require "badline/c128/cpu"
12
+ require "badline/c128/saved_state"
13
+ require "badline/c128/keys"
14
+ require "badline/c128/modes"
15
+
16
+ module Badline
17
+ # The Commodore 128: the 8502 on the VIC-IIe's clock, two CIAs, the SID,
18
+ # the VDC and its 80 column display, and 128K of RAM, which the 8722 MMU
19
+ # maps (C128::Bus).
20
+ #
21
+ # Built for C128 mode, it runs BASIC 7.0 and the C128 KERNAL from reset.
22
+ # The C128's Z80 runs first and hands the bus to the 8502, but here the
23
+ # 8502 starts at its reset vector, with the MMU as the Z80 leaves it.
24
+ # The KERNAL goes to C64 mode as on the C128, with C= held at reset, a
25
+ # C64 cartridge or GO64.
26
+ #
27
+ # Built for C64 mode, the default, it powers on in C64 mode, the state
28
+ # the C128 KERNAL reaches when C= is held at power-on, and a reset comes
29
+ # back there. It runs the C64's BASIC and KERNAL, so the C64's KERNAL
30
+ # traps, the keyboard buffer and CHROUT capture work unchanged.
31
+ #
32
+ # Where a C64 program sees it differ from a C64C: the 8502's P6 senses CAPS
33
+ # LOCK, $D02F drives the extra keyboard rows, $D030's FAST bit runs the
34
+ # CPU at 2 MHz and its TEST bit races the raster counter, the SID has no
35
+ # mirrors, and $D500-$D7FF holds the hidden MMU, the VDC and open bus.
36
+ class C128
37
+ include IntegerHelper
38
+ include KeyboardBuffer
39
+ include Computer::Attachments
40
+ include Computer::KernalTraps
41
+ include Keys
42
+ include Modes
43
+
44
+ # The C64's 8x8 matrix and the three rows K0-K2 select, in port B
45
+ # column order.
46
+ KEYBOARD_MATRIX = (Keyboard::C64_MATRIX + [
47
+ %i[help keypad8 keypad5 tab keypad2 keypad4 keypad7 keypad1],
48
+ %i[esc keypad_plus keypad_minus line_feed keypad_enter keypad6 keypad9 keypad3],
49
+ %i[alt keypad0 keypad_period crsr_up crsr_down crsr_left crsr_right no_scroll]
50
+ ]).freeze
51
+
52
+ # The cycle at which #on_init's handlers run, once the KERNAL of the
53
+ # mode the machine is built for has booted: the C64's as on a C64, or
54
+ # BASIC 7.0's, which is READY by then.
55
+ C128_INIT_THRESHOLD = 2_000_000
56
+
57
+ # The C128 KERNAL's keyboard buffer and its count.
58
+ C128_KEYBOARD_BUFFER = 0x034a
59
+ C128_KEYBOARD_COUNT = 0xd0
60
+
61
+ attr_reader :cpu, :cycles, :drive1541, :model, :init_threshold
62
+
63
+ # The path of the disk or directory device 8 serves through the traps
64
+ # (Computer::Attachments), or an empty one.
65
+ def mounted_path = @drive.nil? ? "" : @drive.path
66
+
67
+ def family = :c128
68
+
69
+ def address_bus = @bus
70
+
71
+ def region = @bus.region
72
+
73
+ def vic = @bus.vic
74
+
75
+ def cia1 = @bus.cia1
76
+
77
+ def cia2 = @bus.cia2
78
+
79
+ def sid = @bus.sid
80
+
81
+ def vdc = @bus.vdc
82
+
83
+ def mmu = @bus.mmu
84
+
85
+ def ram = @bus.ram
86
+
87
+ def keyboard = @bus.keyboard
88
+
89
+ def joystick1 = @bus.joystick1
90
+
91
+ def joystick2 = @bus.joystick2
92
+
93
+ def control_ports = @bus.control_ports
94
+
95
+ def datasette = @bus.datasette
96
+
97
+ # :c64 or :c128, the mode the MMU is in.
98
+ def mode = @bus.mmu.mode
99
+
100
+ def install_debug_register(&) = @bus.install_debug_register(&)
101
+
102
+ # +model+ names one of Model::ALL, and +sid_model+, when not nil, the
103
+ # SID in place of the model's. +mode+, :c64 or :c128, is the mode it
104
+ # powers on and resets into.
105
+ def initialize(model: "c128", sid_model: nil, mode: :c64, debug: false)
106
+ raise ArgumentError, "no C128 mode named #{mode}" unless %i[c64 c128].include?(mode)
107
+
108
+ @model = Model.named(model)
109
+ @c64_built = mode == :c64
110
+ @init_threshold = @c64_built ? Computer::INIT_THRESHOLD : C128_INIT_THRESHOLD
111
+ @holding_commodore = false
112
+ @bus = Bus.new(@model, sid_model: sid_model || @model.sid_model, mode:)
113
+ @cpu = CPU.new(@bus, debug:)
114
+ @vic = @bus.vic
115
+ @vic.open_bus = -> { @bus.ram.peek(@cpu.program_counter) }
116
+ @cia1 = @bus.cia1
117
+ @cia2 = @bus.cia2
118
+ @sid = @bus.sid
119
+ @vdc = @bus.vdc
120
+ @datasette = @bus.datasette
121
+ @cycles = 0
122
+ @clock_bits = 0
123
+ @nmi_asserted = false
124
+ @cartridge_nmi = false
125
+ @restore_pulse = false
126
+ @init_handlers = []
127
+ @pending_keys = nil
128
+ @drive = nil
129
+ @serial_trap = nil
130
+ @save_trap = nil
131
+ @drive1541 = nil
132
+ @capture_output = nil
133
+ plug_serial_bus
134
+ enter_c64_mode if @c64_built
135
+ @bus.on_mode_change { mode_changed }
136
+ end
137
+
138
+ # The chips clock ahead of the CPU, as on the C64. $D030's FAST and
139
+ # TEST bits take hold a cycle after the write that sets them: in FAST
140
+ # mode the CPU runs in both halves of the cycle (#clock_fast), and the
141
+ # TEST bit steps the raster counter in every cycle.
142
+ def cycle!
143
+ handle_init if @cycles == @init_threshold
144
+ feed_keyboard if @pending_keys
145
+
146
+ clock_bits = @clock_bits
147
+ @clock_bits = @vic.clock_bits
148
+ @vic.test_step! if clock_bits >= 0x02
149
+
150
+ @vic.cycle!
151
+ @cia1.cycle!
152
+ @cia2.cycle!
153
+ @sid.cycle!
154
+ @datasette.cycle!
155
+ @vdc.cycle!
156
+
157
+ @cpu.irq = @cia1.interrupted? || @vic.interrupted?
158
+
159
+ drive_nmi
160
+ clock_bits.odd? ? clock_fast : clock_cpu
161
+ @drive1541&.host_cycle!
162
+
163
+ @cycles += 1
164
+ end
165
+
166
+ # Runs `count` cycles, one #cycle! after another.
167
+ def run_cycles(count)
168
+ i = 0
169
+ while i < count
170
+ cycle!
171
+ i += 1
172
+ end
173
+ end
174
+
175
+ # Runs cycles until the block returns true or the cycle count passes
176
+ # `limit`, checking before each cycle.
177
+ def run_until(limit)
178
+ cycle! until yield || @cycles > limit
179
+ end
180
+
181
+ def on_init(&block)
182
+ if @cycles < init_threshold
183
+ @init_handlers << block
184
+ else
185
+ block.call
186
+ end
187
+ end
188
+
189
+ # The clock, the raster and the crop of the VIC-IIe's region.
190
+ def timing
191
+ region = @bus.region
192
+ Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
193
+ lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
194
+ end
195
+
196
+ # The chip whose #display a front end shows: the VIC-IIe, or with
197
+ # :vdc the VDC, whose display paints only while its #render is on.
198
+ def video(chip = :vic) = chip == :vdc ? @vdc : @vic
199
+
200
+ # Renders the VDC's display, and not the VIC-IIe's, while a front end
201
+ # shows it, or with false the other way round.
202
+ def vdc_shown=(shown)
203
+ @vdc.render = shown
204
+ @vic.render = !shown
205
+ end
206
+
207
+ # The chip a front end records the machine's sound from.
208
+ def sound_source = @sid
209
+
210
+ def load_prg(data)
211
+ uint16(data[0], data[1]).tap do |load_addr|
212
+ ram.write(load_addr, data[2..])
213
+ end
214
+ end
215
+
216
+ # A cartridge goes in with the power off.
217
+ def attach_cartridge(cartridge)
218
+ connect_cartridge(cartridge)
219
+ power_cycle!
220
+ end
221
+
222
+ # RAM, the VIC and the VDC start from their power-on state, and the RES
223
+ # line resets everything else.
224
+ def power_cycle!
225
+ @vic.power_on!
226
+ @vdc.power_on!
227
+ @bus.power_on!
228
+ reset!
229
+ enter_c64_mode if @c64_built
230
+ end
231
+
232
+ # The RES line reaches the CPU and its port, the MMU, both CIAs, the
233
+ # SID, the cartridge port and, through the serial bus, the drive.
234
+ def reset!
235
+ @bus.reset!
236
+ @cia1.reset!
237
+ @cia2.reset!
238
+ @sid.reset!
239
+ @bus.cartridge&.reset
240
+ @drive&.reset!
241
+ @drive1541&.reset!
242
+ @nmi_asserted = false
243
+ @cpu.reset!
244
+ end
245
+
246
+ # CHROUT is at $FFD2 in both KERNALs, and the trap follows the mode.
247
+ def capture_output
248
+ @capture_output ||= ChroutTrap.new(cpu:, bus: @bus, layout: trap_layout).tap do |trap|
249
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
250
+ end
251
+ end
252
+
253
+ def inspect
254
+ "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
255
+ end
256
+
257
+ private
258
+
259
+ # The C128 KERNAL leaves $D02F at $FF, every extra keyboard row
260
+ # deselected, before it jumps to the C64's reset.
261
+ def enter_c64_mode
262
+ @vic.poke(0xd02f, 0xff)
263
+ @bus.control_ports.extra_rows = 0xff
264
+ end
265
+
266
+ # The NMI line is wired-OR between CIA 2, the cartridge and RESTORE,
267
+ # and the CPU takes an interrupt on its falling edge.
268
+ def drive_nmi
269
+ nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
270
+ @restore_pulse = false
271
+ @cpu.nmi = true if nmi && !@nmi_asserted
272
+ @nmi_asserted = nmi
273
+ end
274
+
275
+ # BA halts the CPU on a read cycle.
276
+ def clock_cpu
277
+ @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
278
+ end
279
+
280
+ # The 8502 at 2 MHz takes both halves of the cycle, and BA no longer
281
+ # halts it. It leaves phi1 to the VIC in the refresh cycles, and an
282
+ # access to I/O, whose chips run at 1 MHz, waits for phi2. The VIC's
283
+ # fetches in the cycle see the bytes on the bus. In a phi1 the CPU
284
+ # spends waiting that is the byte it writes, or the last byte the bus
285
+ # held.
286
+ def clock_fast
287
+ phi1 = 0xff
288
+ unless @vic.refresh_cycle?
289
+ held = @bus.data
290
+ write = @cpu.pending_write?
291
+ @cpu.cycle!
292
+ if @bus.io_access?
293
+ data = @bus.data
294
+ return @vic.take_cpu_bus(write ? data : held, data, @bus.vic_access?)
295
+ end
296
+
297
+ phi1 = @bus.data
298
+ end
299
+ @cpu.cycle!
300
+ @vic.take_cpu_bus(phi1, @bus.data, @bus.vic_access?)
301
+ end
302
+
303
+ def handle_init
304
+ @init_handlers.each(&:call)
305
+ end
306
+
307
+ # The keyboard buffer of the KERNAL the machine runs: the C64's, or in
308
+ # C128 mode BASIC 7.0's.
309
+ def feed_keyboard
310
+ c64 = mode == :c64
311
+ count = c64 ? KeyboardBuffer::COUNT : C128_KEYBOARD_COUNT
312
+ return unless ram.peek(count).zero?
313
+
314
+ chunk = @pending_keys.shift(KeyboardBuffer::CAPACITY)
315
+ ram.write(c64 ? KeyboardBuffer::ADDRESS : C128_KEYBOARD_BUFFER, chunk)
316
+ ram.poke(count, chunk.length)
317
+ @pending_keys = nil if @pending_keys.empty?
318
+ end
319
+ end
320
+ end
@@ -10,6 +10,9 @@ module Badline
10
10
 
11
11
  attr_reader :output
12
12
 
13
+ # The KERNAL layout whose mapping the trap checks.
14
+ attr_writer :layout
15
+
13
16
  def initialize(cpu:, bus:, layout:)
14
17
  @cpu = cpu
15
18
  @bus = bus
@@ -17,18 +17,13 @@ module Badline
17
17
 
18
18
  # Puts the storage in device 8. Mounting again swaps the disk at any
19
19
  # point while the machine runs: the drive keeps its RAM, which only a
20
- # drive reset clears, and its status.
20
+ # drive reset clears, and its status. The traps go on the KERNAL the
21
+ # machine runs (#trap_layout).
21
22
  def mount(storage)
22
23
  return @drive.insert(storage) if @drive
23
24
 
24
25
  @drive = KernalTrap::Drive.new(storage)
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 }
26
+ install_kernal_traps(trap_layout)
32
27
  end
33
28
 
34
29
  # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
@@ -37,11 +32,7 @@ module Badline
37
32
  def unmount
38
33
  return unless @drive
39
34
 
40
- cpu.remove_trap(KernalTrap::C64_LAYOUT.load)
41
- cpu.remove_trap(KernalTrap::C64_LAYOUT.save)
42
- @serial_trap.device = nil
43
- @serial_trap = nil
44
- @save_trap = nil
35
+ remove_kernal_traps
45
36
  @drive = nil
46
37
  end
47
38
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # The LOAD, SAVE and serial traps on the KERNAL a KernalTrap::Layout
6
+ # describes, which serve the disk in device 8 (Attachments#mount).
7
+ module KernalTraps
8
+ private
9
+
10
+ def install_kernal_traps(layout)
11
+ @trap_layout = layout
12
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
13
+ cpu.install_trap(layout.load) { load_trap.call }
14
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
15
+ device: serial_trap_device).install
16
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
17
+ cpu.install_trap(layout.save) { save_trap.call }
18
+ end
19
+
20
+ def remove_kernal_traps
21
+ cpu.remove_trap(@trap_layout.load)
22
+ cpu.remove_trap(@trap_layout.save)
23
+ @serial_trap.uninstall
24
+ @serial_trap = nil
25
+ @save_trap = nil
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/computer/attachments"
4
+ require "badline/computer/kernal_traps"
4
5
  require "badline/computer/saved_state"
5
6
 
6
7
  module Badline
@@ -8,6 +9,7 @@ module Badline
8
9
  include IntegerHelper
9
10
  include KeyboardBuffer
10
11
  include Attachments
12
+ include KernalTraps
11
13
  include SavedState
12
14
 
13
15
  attr_reader :address_bus, :cpu, :cycles, :drive1541
@@ -222,6 +224,9 @@ module Badline
222
224
 
223
225
  private
224
226
 
227
+ # The KERNAL the traps stand in for (Attachments#mount).
228
+ def trap_layout = KernalTrap::C64_LAYOUT
229
+
225
230
  # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
226
231
  # key, and the CPU takes an interrupt on its falling edge.
227
232
  def drive_nmi
@@ -5,6 +5,10 @@ module Badline
5
5
  # Opens the window, then runs the machine a frame at a time: poll
6
6
  # events, clock the frame's cycles, queue the sound's samples, upload the
7
7
  # changed lines, present and wait. Pacer decides the cycles and the wait.
8
+ #
9
+ # The window shows the machine's video chip, sized to the screen shown.
10
+ # On the C128 it follows the machine between the VIC-IIe's 40 columns
11
+ # and the VDC's 80, and F8 switches them until the machine next does.
8
12
  class App
9
13
  DROPFILE = 0x1000
10
14
 
@@ -18,10 +22,10 @@ module Badline
18
22
  @pacer = Pacer.new(paced: options.paced?, vsync: options.vsync?, verbose: @verbose, timing:)
19
23
  @timeline = timeline
20
24
  @snapshots = Snapshots.new(computer, options)
21
- @screen = Screen.new(computer.video, timing.crop)
25
+ @screens = Screens.new(computer)
22
26
  @led = DriveLed.for(computer)
23
27
  @controls = Controls.new(computer)
24
- @window = MachineWindow.new(@screen, timing.pixel_width, vsync: @pacer.vsync?)
28
+ @window = MachineWindow.new(@screens.screen, vsync: @pacer.vsync?)
25
29
  @pacer.fit(@window.refresh_rate) if @pacer.vsync?
26
30
  @sound = Sound.new(computer.sound_source, timing.clock_hz, options.sound?, @verbose)
27
31
  @gamepads = Gamepads.new(computer, @verbose)
@@ -68,6 +72,7 @@ module Badline
68
72
  @frame_report.add(stamps) if @verbose
69
73
  @frames += 1
70
74
  @timeline.run(@computer, @frames)
75
+ show_vdc(@timeline.display_at(@frames) == "vdc") if @timeline.display?(@frames)
71
76
  @snapshots.tick(@frames)
72
77
  @running = false if @frames == @frame_limit || @timeline.quit?(@frames)
73
78
  @pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
@@ -109,7 +114,7 @@ module Badline
109
114
  end
110
115
 
111
116
  def handle_key(scancode, down)
112
- if [Keys::TAB, Keys::F9, Keys::F10].include?(scancode)
117
+ if [Keys::TAB, Keys::F8, Keys::F9, Keys::F10].include?(scancode)
113
118
  handle_toggle(scancode) if down
114
119
  elsif down && @snapshots.key(scancode)
115
120
  swap(@snapshots.computer)
@@ -123,24 +128,21 @@ module Badline
123
128
  # it.
124
129
  def swap(computer)
125
130
  @computer = computer
126
- timing = computer.timing
127
- @screen = Screen.new(computer.video, timing.crop)
128
- @window.fit(@screen, timing.pixel_width)
129
- @menu.center(@window.width, @window.height)
131
+ @screens.computer = computer
132
+ fit_window
130
133
  @led = DriveLed.for(computer)
131
134
  @controls.computer = computer
132
135
  @gamepads.computer = computer
133
- @sound.switch(computer.sound_source, timing.clock_hz)
136
+ @sound.switch(computer.sound_source, computer.timing.clock_hz)
134
137
  @snapshots.computer = computer
135
138
  end
136
139
 
137
140
  def handle_toggle(scancode)
138
- if scancode == Keys::TAB
139
- @controls.toggle_keys
140
- elsif scancode == Keys::F9
141
- return open_menu
142
- else
143
- @sound.toggle_mute
141
+ case scancode
142
+ when Keys::TAB then @controls.toggle_keys
143
+ when Keys::F8 then show_vdc(!@screens.vdc_shown?)
144
+ when Keys::F9 then return open_menu
145
+ else @sound.toggle_mute
144
146
  end
145
147
  update_title
146
148
  end
@@ -189,11 +191,19 @@ module Badline
189
191
  menu_events
190
192
  end
191
193
 
192
- def update_title
193
- tags = []
194
- tags << @controls.tag unless @controls.tag.empty?
195
- tags << "MUTED" if @sound.muted?
196
- @window.title(tags)
194
+ def update_title = @window.title(@controls.tag, @sound.muted?)
195
+
196
+ # Shows the C128's VDC in the window, or with false its VIC-IIe.
197
+ def show_vdc(shown)
198
+ @screens.show_vdc(shown)
199
+ fit_window
200
+ end
201
+
202
+ # Builds the screen for the chip shown and fits the window to it.
203
+ def fit_window
204
+ @screens.build
205
+ @window.fit(@screens.screen)
206
+ @menu.center(@window.width, @window.height)
197
207
  end
198
208
 
199
209
  def emulate
@@ -201,8 +211,11 @@ module Badline
201
211
  end
202
212
 
203
213
  def upload
204
- @screen.update
205
- @window.upload(@screen)
214
+ @screens.follow
215
+ fit_window if @screens.stale?
216
+ screen = @screens.screen
217
+ screen.update
218
+ @window.upload(screen)
206
219
  end
207
220
 
208
221
  def draw
@@ -49,7 +49,7 @@ module Badline
49
49
  # or --sid the SID. The VIC-20 raises Unsupported for a .sid
50
50
  # tune.
51
51
  def self.start(options, media, writable)
52
- computer = options.family == :vic20 ? vic20(options, media) : c64(options, media)
52
+ computer = build(options, media)
53
53
  Media::TrueDrive.plug(computer) if options.true_drive?
54
54
  unless media.nil?
55
55
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
@@ -58,11 +58,41 @@ module Badline
58
58
  computer
59
59
  end
60
60
 
61
+ def self.build(options, media)
62
+ case options.family
63
+ when :vic20 then vic20(options, media)
64
+ when :c128 then c128(options, media)
65
+ else c64(options, media)
66
+ end
67
+ end
68
+
61
69
  def self.c64(options, media)
62
70
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
63
71
  Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
64
72
  end
65
73
 
74
+ # A C128 in C128 mode. With --c64, or media only C64 mode runs, a .sid
75
+ # tune or a program that loads at the C64's BASIC start, C= is held
76
+ # through the reset, so the C128 KERNAL starts C64 mode.
77
+ def self.c128(options, media)
78
+ sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
79
+ machine = C128.new(model: options.model, sid_model:, mode: :c128)
80
+ machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
81
+ machine
82
+ end
83
+
84
+ def self.c64_media?(media)
85
+ return false if media.nil? || !File.file?(media)
86
+
87
+ extension = File.extname(media).downcase
88
+ return true if extension == ".sid"
89
+ return false unless %w[.prg .p00].include?(extension)
90
+
91
+ bytes = File.binread(media).bytes
92
+ bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
93
+ bytes.length >= 2 && [Media::BASIC_START, Media::BASIC_START - 1].include?(bytes[0] | (bytes[1] << 8))
94
+ end
95
+
66
96
  # A VIC-20 with the RAM --ram names, or else the RAM `media` needs
67
97
  # (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
68
98
  # media a VIC-20 doesn't take.
@@ -73,6 +73,8 @@ module Badline
73
73
  def key(scancode, down)
74
74
  if joystick_mode? && Keys.joystick?(scancode)
75
75
  joystick_key(scancode, down)
76
+ elsif @computer.family == :c128
77
+ c128_key(Keys.c128_key(scancode, @computer.mode), down)
76
78
  else
77
79
  c64_key(Keys.c64_key(scancode), down)
78
80
  end
@@ -111,6 +113,15 @@ module Badline
111
113
  end
112
114
  end
113
115
 
116
+ # CAPS LOCK and 40/80 DISPLAY aren't in the C128's matrix either.
117
+ def c128_key(key, down)
118
+ case key
119
+ when :caps_lock then down ? @computer.press_caps_lock : @computer.release_caps_lock
120
+ when :forty_eighty then down ? @computer.press_display_key : @computer.release_display_key
121
+ else c64_key(key, down)
122
+ end
123
+ end
124
+
114
125
  def paddles? = POTS.index(@pot) >= 3
115
126
 
116
127
  # A fresh pot device on its port, and none on the other. A machine
@@ -3,7 +3,8 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # SDL scancodes follow key positions on a US layout. They map onto the C64
6
- # keys and, in joystick mode, the joystick directions.
6
+ # keys, the C128's own keys and, in joystick mode, the joystick
7
+ # directions.
7
8
  module Keys
8
9
  TAB = 43
9
10
 
@@ -20,6 +21,24 @@ module Badline
20
21
  224 => :control, 225 => :lshift, 226 => :cbm, 229 => :rshift
21
22
  }.freeze
22
23
 
24
+ # The C128's keys that have a host key of their own: the keypad, HELP
25
+ # on Insert or Help, ALT on the right Alt, NO SCROLL on Scroll Lock,
26
+ # and CAPS LOCK and 40/80 DISPLAY, which sit outside the matrix, on
27
+ # Caps Lock and F6 or Pause. In C128 mode Esc is ESC, and RUN/STOP
28
+ # moves to Page Down. TAB and the separate cursor keys stay with the
29
+ # C64's keys (the joystick toggle and the cursor combinations), and in
30
+ # C64 mode Esc stays RUN/STOP.
31
+ C128_OTHERS = {
32
+ 89 => :keypad1, 90 => :keypad2, 91 => :keypad3, 92 => :keypad4, 93 => :keypad5, 94 => :keypad6,
33
+ 95 => :keypad7, 96 => :keypad8, 97 => :keypad9, 98 => :keypad0, 99 => :keypad_period,
34
+ 86 => :keypad_minus, 87 => :keypad_plus, 88 => :keypad_enter,
35
+ 73 => :help, 117 => :help, 230 => :alt, 71 => :no_scroll, 57 => :caps_lock,
36
+ 63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
37
+ }.freeze
38
+
39
+ ESCAPE = 41
40
+
41
+ F8 = 65
23
42
  F9 = 66
24
43
  F10 = 67
25
44
  F11 = 68
@@ -38,6 +57,14 @@ module Badline
38
57
  end
39
58
  end
40
59
 
60
+ # The C128's key for the scancode in the C128's +mode+, :c128 or
61
+ # :c64: one of C128_OTHERS, ESC, or the C64's.
62
+ def self.c128_key(scancode, mode)
63
+ return :esc if scancode == ESCAPE && mode == :c128
64
+
65
+ C128_OTHERS[scancode] || c64_key(scancode)
66
+ end
67
+
41
68
  def self.arrows(scancode) = ARROWS[scancode] || :none
42
69
 
43
70
  def self.wasd(scancode) = WASD[scancode] || :none