badline 0.10.1 → 0.11.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +25 -6
  4. data/CONTRIBUTING.md +8 -3
  5. data/README.md +63 -4
  6. data/doc/pinned-behaviour.md +75 -0
  7. data/doc/snapshots.md +15 -0
  8. data/lib/badline/address_bus/pla.rb +3 -16
  9. data/lib/badline/address_bus.rb +14 -0
  10. data/lib/badline/c128/banks.rb +35 -0
  11. data/lib/badline/c128/bus.rb +347 -0
  12. data/lib/badline/c128/bus_state.rb +53 -0
  13. data/lib/badline/c128/color_lines.rb +39 -0
  14. data/lib/badline/c128/cpu.rb +10 -0
  15. data/lib/badline/c128/drives.rb +95 -0
  16. data/lib/badline/c128/fast_serial.rb +62 -0
  17. data/lib/badline/c128/keys.rb +35 -0
  18. data/lib/badline/c128/mmu.rb +197 -0
  19. data/lib/badline/c128/mmu_pages.rb +105 -0
  20. data/lib/badline/c128/model.rb +39 -0
  21. data/lib/badline/c128/modes.rb +48 -0
  22. data/lib/badline/c128/saved_state.rb +121 -0
  23. data/lib/badline/c128/vdc/memory.rb +54 -0
  24. data/lib/badline/c128/vdc/painter.rb +143 -0
  25. data/lib/badline/c128/vdc/raster.rb +97 -0
  26. data/lib/badline/c128/vdc/window.rb +215 -0
  27. data/lib/badline/c128/vdc.rb +268 -0
  28. data/lib/badline/c128.rb +322 -0
  29. data/lib/badline/chrout_trap.rb +3 -0
  30. data/lib/badline/cia/fast_forward.rb +42 -0
  31. data/lib/badline/cia/serial.rb +12 -0
  32. data/lib/badline/cia/timer.rb +39 -0
  33. data/lib/badline/cia.rb +2 -0
  34. data/lib/badline/computer/attachments.rb +10 -13
  35. data/lib/badline/computer/kernal_traps.rb +29 -0
  36. data/lib/badline/computer.rb +5 -0
  37. data/lib/badline/drive/core.rb +172 -0
  38. data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
  39. data/lib/badline/drive/idle_cpu.rb +28 -0
  40. data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
  41. data/lib/badline/drive/serial_lines.rb +73 -0
  42. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
  43. data/lib/badline/drive/watch.rb +150 -0
  44. data/lib/badline/drive.rb +9 -0
  45. data/lib/badline/drive1541/bus.rb +5 -129
  46. data/lib/badline/drive1541/byte_ready.rb +48 -0
  47. data/lib/badline/drive1541/cpu.rb +1 -19
  48. data/lib/badline/drive1541/disk.rb +77 -20
  49. data/lib/badline/drive1541/disk_state.rb +7 -2
  50. data/lib/badline/drive1541/disk_via.rb +3 -0
  51. data/lib/badline/drive1541/mechanism.rb +48 -14
  52. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  53. data/lib/badline/drive1541/rotation.rb +5 -5
  54. data/lib/badline/drive1541/serial_port.rb +4 -60
  55. data/lib/badline/drive1541.rb +13 -172
  56. data/lib/badline/drive1571/bus.rb +111 -0
  57. data/lib/badline/drive1571/cpu.rb +11 -0
  58. data/lib/badline/drive1571/serial_port.rb +37 -0
  59. data/lib/badline/drive1571/serial_via.rb +25 -0
  60. data/lib/badline/drive1571/wd1770.rb +34 -0
  61. data/lib/badline/drive1571.rb +225 -0
  62. data/lib/badline/frontend/app.rb +35 -22
  63. data/lib/badline/frontend/boot.rb +31 -1
  64. data/lib/badline/frontend/controls.rb +11 -0
  65. data/lib/badline/frontend/drive_led.rb +1 -1
  66. data/lib/badline/frontend/keys.rb +28 -1
  67. data/lib/badline/frontend/machine_window.rb +16 -13
  68. data/lib/badline/frontend/menu_media.rb +3 -3
  69. data/lib/badline/frontend/menu_pages.rb +8 -2
  70. data/lib/badline/frontend/menu_ports.rb +1 -1
  71. data/lib/badline/frontend/pause_menu.rb +1 -0
  72. data/lib/badline/frontend/screen.rb +33 -8
  73. data/lib/badline/frontend/screens.rb +90 -0
  74. data/lib/badline/frontend/timeline.rb +15 -4
  75. data/lib/badline/frontend.rb +1 -0
  76. data/lib/badline/iec_bus.rb +54 -4
  77. data/lib/badline/kernal_trap/file.rb +1 -1
  78. data/lib/badline/kernal_trap/layout.rb +58 -8
  79. data/lib/badline/kernal_trap/load.rb +1 -1
  80. data/lib/badline/kernal_trap/save.rb +1 -1
  81. data/lib/badline/kernal_trap/serial.rb +5 -0
  82. data/lib/badline/machine.rb +20 -9
  83. data/lib/badline/media/true_drive.rb +26 -23
  84. data/lib/badline/media.rb +8 -6
  85. data/lib/badline/options/event.rb +5 -1
  86. data/lib/badline/options/family.rb +52 -0
  87. data/lib/badline/options/help.rb +7 -2
  88. data/lib/badline/options/table.rb +8 -2
  89. data/lib/badline/options/validation.rb +14 -1
  90. data/lib/badline/options.rb +20 -19
  91. data/lib/badline/roms/README +36 -0
  92. data/lib/badline/roms/c128/basic.rom +0 -0
  93. data/lib/badline/roms/c128/character.rom +0 -0
  94. data/lib/badline/roms/c128/kernal.rom +0 -0
  95. data/lib/badline/roms/dos1571.rom +0 -0
  96. data/lib/badline/snapshot/container.rb +2 -0
  97. data/lib/badline/snapshot/image.rb +14 -0
  98. data/lib/badline/snapshot.rb +9 -2
  99. data/lib/badline/storage/d64_image.rb +3 -0
  100. data/lib/badline/storage/d71_image.rb +9 -4
  101. data/lib/badline/storage/g64_image.rb +7 -2
  102. data/lib/badline/time_of_day.rb +9 -0
  103. data/lib/badline/version.rb +1 -1
  104. data/lib/badline/via/fast_forward.rb +3 -3
  105. data/lib/badline/vic/display_state.rb +12 -0
  106. data/lib/badline/vic/registers.rb +3 -0
  107. data/lib/badline/vic.rb +108 -7
  108. data/lib/badline/vic20/attachments.rb +6 -0
  109. data/lib/badline/z80/arithmetic.rb +117 -0
  110. data/lib/badline/z80/bit_page.rb +55 -0
  111. data/lib/badline/z80/block_io.rb +58 -0
  112. data/lib/badline/z80/block_transfers.rb +59 -0
  113. data/lib/badline/z80/bus_cycles.rb +111 -0
  114. data/lib/badline/z80/core.rb +111 -0
  115. data/lib/badline/z80/exchanges.rb +50 -0
  116. data/lib/badline/z80/extended_page.rb +89 -0
  117. data/lib/badline/z80/flags.rb +37 -0
  118. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  119. data/lib/badline/z80/jumps.rb +82 -0
  120. data/lib/badline/z80/loads.rb +98 -0
  121. data/lib/badline/z80/main_page.rb +134 -0
  122. data/lib/badline/z80/registers.rb +120 -0
  123. data/lib/badline/z80/shifts.rb +64 -0
  124. data/lib/badline/z80.rb +11 -0
  125. data/lib/badline.rb +1 -0
  126. metadata +59 -4
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # What VIA 1's ports read from outside the chip. Port B carries the
6
+ # serial bus inputs and the device number jumpers, as on the 1541
7
+ # (Drive::SerialLines). Port A reads the mechanism on two inputs:
8
+ #
9
+ # PA0 track 0 sensor, low while the head sits on track 1
10
+ # PA7 BYTE READY, latched low from a whole byte until the CPU next
11
+ # reads or writes VIA 2
12
+ #
13
+ # The DOS steps the head out until PA0 reads low with the stepper on
14
+ # phase 0, and polls PA7 for each byte at 2 MHz, taking the byte from
15
+ # VIA 2's port A or putting the next one there after each. PA1 (fast serial
16
+ # direction), PA2 (side) and PA5 (1 or 2 MHz) are outputs, which
17
+ # Drive1571 follows (SerialVIA).
18
+ class SerialPort
19
+ include Drive::SerialLines
20
+
21
+ TRACK0 = 0x01
22
+ BYTE_READY = 0x80
23
+
24
+ def initialize(mechanism, device: 8, bus: IECBus.new)
25
+ @mechanism = mechanism
26
+ init_serial_lines(device, bus)
27
+ end
28
+
29
+ def read_a(_lines)
30
+ value = 0xff
31
+ value &= ~TRACK0 if @mechanism.half_track == Drive1541::Mechanism::MIN_HALF_TRACK
32
+ value &= ~BYTE_READY if @mechanism.byte_latched?
33
+ value
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # VIA 1, the one facing the serial bus. Its port A also selects the
6
+ # side, the clock rate and the fast serial direction, so the drive
7
+ # hears port A after every write and reset (Drive1571#port_a_written).
8
+ class SerialVIA < VIA
9
+ def initialize(start:, peripheral:, drive:)
10
+ @drive = drive
11
+ super(start:, peripheral:)
12
+ end
13
+
14
+ def reset!
15
+ super
16
+ @drive.port_a_written(port_a_output)
17
+ end
18
+
19
+ def poke(addr, value)
20
+ super
21
+ @drive.port_a_written(port_a_output)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # The WD1770 floppy controller at $2000, which the DOS needs only for
6
+ # MFM disks. Its four registers hold what's written to them and it
7
+ # runs no commands: the status reads idle, with the motor off.
8
+ #
9
+ # $2000 status (read), command (write)
10
+ # $2001 track
11
+ # $2002 sector
12
+ # $2003 data
13
+ class WD1770
14
+ def initialize
15
+ @registers = [0, 0, 0, 0]
16
+ end
17
+
18
+ # Register +offset+, 0 to 3.
19
+ def peek(offset) = offset.zero? ? 0 : @registers[offset]
20
+
21
+ def poke(offset, value)
22
+ @registers[offset] = value
23
+ end
24
+
25
+ def save_state(out)
26
+ out.ints(@registers)
27
+ end
28
+
29
+ def load_state(input)
30
+ @registers = input.ints
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drive1571/cpu"
4
+ require "badline/drive1571/wd1770"
5
+ require "badline/drive1571/serial_port"
6
+ require "badline/drive1571/serial_via"
7
+ require "badline/drive1571/bus"
8
+
9
+ module Badline
10
+ # The 1571 disk drive, the C128D's built-in one: the 1541's 6502, two
11
+ # VIAs and GCR mechanism, with a second head, a 6526 CIA for fast
12
+ # serial, a WD1770 for MFM disks and DOS 3.0 in 32 KB of ROM. Drive::Core
13
+ # clocks it against the host and sleeps through the DOS's idle loop.
14
+ #
15
+ # VIA 1's port A picks the clock, 1 or 2 MHz on PA5, and the head on
16
+ # PA2, and reads the track 0 sensor and BYTE READY (SerialPort). The
17
+ # DOS resets at 2 MHz and drops to 1 MHz as a 1541 would run, which is
18
+ # how it stays until a C128 asks for fast serial. The disk turns at the
19
+ # same speed either way (Drive1541::Mechanism#clock_hz=).
20
+ #
21
+ # The CIA sits at $4000, on the IRQ line with both VIAs. Only its serial
22
+ # port is wired: through buffers PA1 turns round, its CNT drives SRQ and
23
+ # its SP DATA, or it hears them. The drive pushes its pins into the bus
24
+ # as they move, and hears the host's as the host pushes them, pin by
25
+ # pin, so each byte goes through both shift registers bit by bit. The
26
+ # CIA takes no part in a pass of the idle loop: the drive sleeps only
27
+ # while the CIA is quiet (CIA#quiet?), and any access to it, or the
28
+ # host moving its fast serial pins, wakes the drive.
29
+ class Drive1571
30
+ include Drive::Core
31
+
32
+ CLOCK_HZ = 1_000_000
33
+ FAST_CLOCK_HZ = 2_000_000
34
+
35
+ # Where the DOS 3.0 idle loop starts over, as DOS 2.6's does.
36
+ IDLE_LOOP = 0xebff
37
+
38
+ # VIA 1's port A outputs.
39
+ FAST_SERIAL_OUT = 0x02
40
+ SIDE = 0x04
41
+ FAST = 0x20
42
+
43
+ # Whether PA1 turns the fast serial buffers outwards, the drive's CIA
44
+ # driving DATA and SRQ.
45
+ attr_reader :fast_serial_out
46
+
47
+ def cia
48
+ settle!
49
+ @cia
50
+ end
51
+
52
+ def fdc
53
+ settle!
54
+ @fdc
55
+ end
56
+
57
+ def model_name = "1571"
58
+
59
+ # +rom+ covers $8000-$FFFF, and defaults to the DOS image in the ROM
60
+ # path. +device+ is the number the jumpers on VIA 1's PB5 and PB6 set,
61
+ # 8 to 11.
62
+ def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
63
+ @device = device
64
+ @clock_hz = CLOCK_HZ
65
+ @fast_serial_out = false
66
+ @fast_output = 0
67
+ @mechanism = Drive1541::Mechanism.new(slip: 2)
68
+ @serial_port = SerialPort.new(@mechanism, device:)
69
+ @via1 = SerialVIA.new(start: 0x1800, peripheral: @serial_port, drive: self)
70
+ @via2 = Drive1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
71
+ @cia = CIA.new(start: 0x4000)
72
+ @fdc = WD1770.new
73
+ @bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
74
+ @cpu = CPU.new(@bus, debug:)
75
+ @idle_loop = IDLE_LOOP
76
+ @host_clock_hz = host_clock_hz
77
+ @phase = 0
78
+ @cycles = 0
79
+ @serial_bus = nil
80
+ init_idle(debug)
81
+ # CA1 powers up at the level of a released ATN, without an edge.
82
+ @via1.ca1 = false
83
+ @via1.reset!
84
+ connect(IECBus.new)
85
+ end
86
+
87
+ # The serial bus's RESET line reaches the CPU, both VIAs and the CIA.
88
+ # RAM keeps its contents.
89
+ def reset!
90
+ settle!
91
+ @via1.reset!
92
+ @via2.reset!
93
+ @cia.reset!
94
+ @cpu.reset!
95
+ end
96
+
97
+ # VIA 1 drives port A to +lines+: the side, the clock rate and the fast
98
+ # serial direction follow it.
99
+ def port_a_written(lines)
100
+ @mechanism.side = lines.anybits?(SIDE) ? 1 : 0
101
+ fast_serial_out = lines.anybits?(FAST_SERIAL_OUT)
102
+ turn_fast_serial(fast_serial_out) if fast_serial_out != @fast_serial_out
103
+ clock_hz = lines.anybits?(FAST) ? FAST_CLOCK_HZ : CLOCK_HZ
104
+ return if clock_hz == @clock_hz
105
+
106
+ @clock_hz = clock_hz
107
+ @mechanism.clock_hz = clock_hz
108
+ end
109
+
110
+ # Whether the drive runs at 2 MHz.
111
+ def fast? = @clock_hz == FAST_CLOCK_HZ
112
+
113
+ # VIA 1's port B, with the fast serial pins pulling DATA and SRQ
114
+ # (IECBus::DRIVE_FAST_DATA and DRIVE_FAST_SRQ) above it.
115
+ def serial_output = @via1.port_b_output | @fast_output
116
+
117
+ # The host moved its fast serial pins: the CIA hears SRQ on CNT and
118
+ # DATA on SP while PA1 turns the buffers inwards.
119
+ def fast_lines_moved
120
+ settle!
121
+ hear_fast_serial unless @fast_serial_out
122
+ end
123
+
124
+ # The drive's whole state, as the 1541's (Drive1541#save_state), with
125
+ # the CIA and the WD1770's registers. The clock rate, the side and the
126
+ # fast serial direction follow from VIA 1's port A.
127
+ def save_state(out)
128
+ settle!
129
+ out.marker("DRIVE1571")
130
+ out.int(@phase).int(@cycles).boolean(@mechanism.so_pending)
131
+ @serial_port.save_state(out)
132
+ @via1.save_state(out)
133
+ @via2.save_state(out)
134
+ @cia.save_state(out)
135
+ @fdc.save_state(out)
136
+ @bus.save_state(out)
137
+ @cpu.save_state(out)
138
+ @mechanism.save_state(out)
139
+ end
140
+
141
+ def load_state(input)
142
+ settle!
143
+ input.marker("DRIVE1571")
144
+ @phase = input.int
145
+ @cycles = input.int
146
+ @mechanism.so_pending = input.boolean?
147
+ @serial_port.load_state(input)
148
+ @via1.load_state(input)
149
+ @via2.load_state(input)
150
+ @cia.load_state(input)
151
+ @fdc.load_state(input)
152
+ @bus.load_state(input)
153
+ @cpu.load_state(input)
154
+ @mechanism.load_state(input)
155
+ @fast_serial_out = @via1.port_a_output.anybits?(FAST_SERIAL_OUT)
156
+ port_a_written(@via1.port_a_output)
157
+ push_fast_output(@fast_serial_out ? fast_pins : 0)
158
+ forget_orbits
159
+ end
160
+
161
+ private
162
+
163
+ # One drive cycle, as the 1541's (Drive1541#step), with the CIA
164
+ # clocked beside the VIAs and pulling IRQ with them.
165
+ def step
166
+ so = @mechanism.take_so
167
+ @mechanism.cycle!
168
+ @via1.ca1 = @serial_port.atn_low?
169
+ @via1.cycle!
170
+ @via2.cycle!
171
+ @cia.cycle!
172
+ drive_fast_serial if @fast_serial_out
173
+ @cpu.irq = @via1.irq? || @via2.irq? || @cia.interrupted?
174
+ @cpu.so! if so
175
+ @cpu.cycle!
176
+ @cycles += 1
177
+ end
178
+
179
+ # The VIAs' quiet cycles while the CIA is quiet, and none otherwise.
180
+ def quiet_cycles
181
+ @cia.quiet? ? [@via1.quiet_cycles, @via2.quiet_cycles].min : 0
182
+ end
183
+
184
+ def fast_forward_chips(cycles)
185
+ @cia.fast_forward(cycles)
186
+ end
187
+
188
+ # PA1 turned the buffers round: outwards the CIA's pins drive the
189
+ # lines, and inwards they let go and hear them.
190
+ def turn_fast_serial(out)
191
+ @fast_serial_out = out
192
+ return unless @serial_bus
193
+
194
+ push_fast_output(out ? fast_pins : 0)
195
+ hear_fast_serial unless out
196
+ end
197
+
198
+ def drive_fast_serial
199
+ low = fast_pins
200
+ push_fast_output(low) if low != @fast_output
201
+ end
202
+
203
+ # The lines the CIA's CNT and SP pull while its serial port drives
204
+ # them.
205
+ def fast_pins
206
+ serial = @cia.serial
207
+ return 0 unless serial.output?
208
+
209
+ low = serial.cnt ? 0 : IECBus::DRIVE_FAST_SRQ
210
+ serial.sp_out ? low : low | IECBus::DRIVE_FAST_DATA
211
+ end
212
+
213
+ def push_fast_output(low)
214
+ @fast_output = low
215
+ @serial_bus&.drives_fast_moved!
216
+ end
217
+
218
+ def hear_fast_serial
219
+ low = @serial_bus.low_lines
220
+ serial = @cia.serial
221
+ serial.cnt_in = low.nobits?(IECBus::SRQ)
222
+ serial.sp_in = low.nobits?(IECBus::DATA)
223
+ end
224
+ end
225
+ end
@@ -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)
@@ -38,7 +42,7 @@ module Badline
38
42
  @pacer.start(@started)
39
43
  @menu.open? ? paused_frame : frame while @running
40
44
  @snapshots.finish
41
- @computer.drive1541&.flush
45
+ @computer.true_drive&.flush
42
46
  @gamepads.close
43
47
  @sound.close
44
48
  @window.close
@@ -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
@@ -14,7 +14,7 @@ module Badline
14
14
 
15
15
  # The LED of the machine's true drive, or nil without one.
16
16
  def self.for(computer)
17
- drive = computer.drive1541
17
+ drive = computer.true_drive
18
18
  drive ? new(drive) : nil
19
19
  end
20
20
 
@@ -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
@@ -3,10 +3,10 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # The emulator's SDL window, its renderer and the streaming texture a
6
- # Screen's pixels go into. The renderer's logical size is the screen's,
7
- # with each of its pixels `pixel_width` square pixels wide, so the
8
- # picture keeps the machine's aspect while the menu and the drive LED
9
- # draw in square pixels over it. The window opens at SCALE times that.
6
+ # Screen's pixels go into. The renderer's logical size is the screen's
7
+ # in square pixels (Screen#window_width), so the picture keeps the
8
+ # machine's aspect while the menu and the drive LED draw in square
9
+ # pixels over it. The window opens at SCALE times that.
10
10
  class MachineWindow
11
11
  SCALE = 2
12
12
  TITLE = "Badline"
@@ -18,12 +18,12 @@ module Badline
18
18
 
19
19
  # Opens the window for `screen`, presenting in step with the display
20
20
  # when `vsync`.
21
- def initialize(screen, pixel_width, vsync:)
21
+ def initialize(screen, vsync:)
22
22
  abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
23
23
 
24
24
  SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
25
25
  SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
26
- measure(screen, pixel_width)
26
+ measure(screen)
27
27
  @window = SDL.SDL_CreateWindow(
28
28
  TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
29
29
  SDL::WINDOW_RESIZABLE
@@ -42,9 +42,9 @@ module Badline
42
42
 
43
43
  # Fits the texture, the logical size and the window to another
44
44
  # machine's screen, unless it measures the same.
45
- def fit(screen, pixel_width)
45
+ def fit(screen)
46
46
  before = [@width, @height, @texture_width]
47
- measure(screen, pixel_width)
47
+ measure(screen)
48
48
  return if before == [@width, @height, @texture_width]
49
49
 
50
50
  SDL.SDL_DestroyTexture(@texture)
@@ -56,9 +56,12 @@ module Badline
56
56
  SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
57
57
  end
58
58
 
59
- # Titles the window, TITLE followed by `tags` in brackets.
60
- def title(tags)
61
- SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |tag| "[#{tag}]" }).join(" "))
59
+ # Titles the window, TITLE followed by the controls' `tag`, unless it's
60
+ # empty, and MUTED when `muted`, each in brackets.
61
+ def title(tag, muted)
62
+ tags = tag.empty? ? [] : [tag]
63
+ tags << "MUTED" if muted
64
+ SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |name| "[#{name}]" }).join(" "))
62
65
  end
63
66
 
64
67
  def close
@@ -70,9 +73,9 @@ module Badline
70
73
 
71
74
  private
72
75
 
73
- def measure(screen, pixel_width)
76
+ def measure(screen)
74
77
  @texture_width = screen.width
75
- @width = screen.width * pixel_width
78
+ @width = screen.window_width
76
79
  @height = screen.height
77
80
  end
78
81
 
@@ -28,7 +28,7 @@ module Badline
28
28
  # The expanded path of the disk in device 8, however it went in, or
29
29
  # an empty one.
30
30
  def disk_path
31
- drive = @computer.drive1541
31
+ drive = @computer.true_drive
32
32
  unless drive.nil?
33
33
  disk = drive.disk
34
34
  return disk.nil? ? "" : disk.path.to_s
@@ -39,7 +39,7 @@ module Badline
39
39
  end
40
40
 
41
41
  def inserted?
42
- drive = @computer.drive1541
42
+ drive = @computer.true_drive
43
43
  drive.nil? ? @computer.mounted? : !drive.disk.nil?
44
44
  end
45
45
 
@@ -73,7 +73,7 @@ module Badline
73
73
  end
74
74
 
75
75
  def eject_disk
76
- drive = @computer.drive1541
76
+ drive = @computer.true_drive
77
77
  drive.nil? ? @computer.unmount : drive.insert(nil)
78
78
  end
79
79