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,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class C128
5
+ # The 8502's bus over the 128K of RAM, as the MMU maps it.
6
+ #
7
+ # In C128 mode CR picks what the CPU sees: BASIC at $4000-$BFFF, the
8
+ # screen editor, the character ROM and the KERNAL at $C000-$FFFF, an
9
+ # empty function ROM socket, or RAM, and I/O at $D000-$DFFF. The MMU's
10
+ # registers sit at $D500 and its configuration registers at
11
+ # $FF00-$FF04.
12
+ #
13
+ # In C64 mode the 8721 PLA decodes $01's LORAM, HIRAM and CHAREN and the
14
+ # cartridge's EXROM and GAME as a C64's does (AddressBus::PLA), and the
15
+ # MMU answers nowhere.
16
+ #
17
+ # In both modes the RAM is the bank CR picks, with common RAM in bank 0,
18
+ # and a page that page 0 or page 1 moves to reaches page 0 or page 1 of
19
+ # that bank. C128 mode also moves page 0 and page 1 there. The I/O area:
20
+ #
21
+ # $D000-$D3FF - the VIC-IIe
22
+ # $D400-$D4FF - the SID, with no mirrors above it
23
+ # $D500-$D5FF - the MMU in C128 mode, nothing in C64 mode
24
+ # $D600-$D6FF - the VDC
25
+ # $D700-$D7FF - nothing, but for VICE's debug register at $D7FF
26
+ # $D800-$DBFF - colour RAM
27
+ # $DC00-$DCFF - CIA 1
28
+ # $DD00-$DDFF - CIA 2
29
+ # $DE00-$DFFF - I/O 1 and 2, for cartridges
30
+ #
31
+ # An empty page reads the byte the VIC fetched in the preceding phi1
32
+ # half-cycle, as I/O 1 and 2 do on the C64.
33
+ #
34
+ # Colour RAM has two 1K banks (ColorLines).
35
+ class Bus
36
+ include Addressable
37
+ include AddressBus::PLA
38
+ include AddressBus::ROMs
39
+ include MMUPages
40
+ include SavedState
41
+
42
+ # The 8502's port has seven pins. P0-P5 are the 6510's, and P6 senses
43
+ # the CAPS LOCK key, high while it is up. Bit 7 has no pin, and holds
44
+ # the charge last driven onto it, as the 6510's floating bits do.
45
+ PORT_PULLUPS = 0b0001_0111
46
+ PORT_FLOATING = 0b1000_1000
47
+ CAPS_LOCK = 0b0100_0000
48
+ TAPE_SENSE = 0b0001_0000
49
+
50
+ # The C64's power-on pattern (AddressBus::RAM_POWER_ON) in both 64K
51
+ # banks. The C128's own DRAMs' pattern is unmeasured.
52
+ RAM_POWER_ON = Array.new(2**17) { |addr| AddressBus::RAM_POWER_ON[addr & 0xffff] }.freeze
53
+
54
+ # The VIC's write side: a write to $D02F also drives the extra
55
+ # keyboard lines K0-K2 onto the keyboard's rows 8-10.
56
+ class VICWrites
57
+ def initialize(vic, control_ports)
58
+ @vic = vic
59
+ @control_ports = control_ports
60
+ end
61
+
62
+ def peek(addr) = @vic.peek(addr)
63
+
64
+ def poke(addr, value)
65
+ @vic.poke(addr, value)
66
+ @control_ports.extra_rows = 0xf8 | @vic.extra_keyboard_lines if (addr & 0x3f) == 0x2f
67
+ end
68
+ end
69
+
70
+ # The $D7xx page with VICE's debug register at $D7FF: a write there
71
+ # hands the handler its byte, and the rest of the page is empty.
72
+ class DebugPage
73
+ ADDRESS = 0xd7ff
74
+
75
+ def initialize(open_bus, &handler)
76
+ @open_bus = open_bus
77
+ @handler = handler
78
+ end
79
+
80
+ def peek(addr) = @open_bus.peek(addr)
81
+
82
+ def poke(addr, value)
83
+ @handler.call(value) if addr == ADDRESS
84
+ end
85
+ end
86
+
87
+ # RAM seen somewhere else: the address's bits in +mask+ from +base+
88
+ # on. Bank 1 is the whole 64K from $10000, and a relocated page the
89
+ # low byte from the page it moved to.
90
+ class RAMWindow
91
+ attr_accessor :base
92
+
93
+ def initialize(ram, base, mask)
94
+ @ram = ram
95
+ @base = base
96
+ @mask = mask
97
+ end
98
+
99
+ def peek(addr) = @ram.peek(@base | (addr & @mask))
100
+ def poke(addr, value) = @ram.poke(@base | (addr & @mask), value)
101
+ end
102
+
103
+ # The page at $FF00 in C128 mode: CR and the LCRs at $FF00-$FF04, and
104
+ # whatever CR maps there above them.
105
+ class ConfigurationPage
106
+ attr_accessor :below
107
+
108
+ def initialize(mmu, below)
109
+ @mmu = mmu
110
+ @below = below
111
+ end
112
+
113
+ def peek(addr) = addr < 0xff05 ? @mmu.peek_configuration(addr) : @below.peek(addr)
114
+
115
+ def poke(addr, value)
116
+ addr < 0xff05 ? @mmu.poke_configuration(addr, value) : @below.poke(addr, value)
117
+ end
118
+ end
119
+
120
+ attr_reader :io_port, :ram, :mmu, :vic, :sid, :vdc, :color_ram, :cia1, :cia2, :keyboard, :joystick1,
121
+ :joystick2, :control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette, :region
122
+
123
+ # The C128 mode ROMs: BASIC's two halves, the screen editor, the
124
+ # KERNAL and the character ROM's C128 set.
125
+ attr_reader :basic_low_rom, :basic_high_rom, :editor_rom, :c128_kernal_rom, :c128_character_rom
126
+
127
+ # Whether CAPS LOCK is down, holding P6 low.
128
+ attr_reader :caps_lock
129
+
130
+ # The chips +model+, a Model::Profile, names, with the SID
131
+ # +sid_model+. +mode+ is the mode the MMU resets into.
132
+ def initialize(model, sid_model: model.sid_model, mode: :c64)
133
+ @region = model.region
134
+ @ram = Memory.new(RAM_POWER_ON, length: 2**17, start: 0)
135
+ @bank1 = RAMWindow.new(@ram, 0x10000, 0xffff)
136
+ @relocated = Array.new(4) { RAMWindow.new(@ram, 0, 0xff) }
137
+ @mmu = MMU.new(mode)
138
+ @cartridge = nil
139
+ @debug_page = nil
140
+ @caps_lock = false
141
+
142
+ load_roms
143
+ load_c128_roms
144
+ @banks = Banks.new(@ram, @mmu, @c128_kernal_rom)
145
+ @c64_mode = @mmu.c64_mode?
146
+ @on_mode_change = nil
147
+ plug_chips(model, sid_model)
148
+
149
+ @datasette = Datasette.new
150
+ @datasette.on_flag { @cia1.flag! }
151
+ @datasette.on_sense_change { @io_port.value = port_value }
152
+
153
+ @color_lines = ColorLines.new(@vic, @cia2, character_rom, @c128_character_rom)
154
+ @color_ram = @color_lines.color_ram(1)
155
+ @vic_writes = VICWrites.new(@vic, @control_ports)
156
+ @open_bus = AddressBus::OpenBus.new(@vic)
157
+ @configuration_reads = ConfigurationPage.new(@mmu, @open_bus)
158
+ @configuration_writes = ConfigurationPage.new(@mmu, @open_bus)
159
+
160
+ @port_ddr = 0x00
161
+ @port_out = 0x00
162
+ @port_floating = 0x00
163
+ @io_port = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
164
+
165
+ @read_pages = Array.new(256)
166
+ @write_pages = Array.new(256)
167
+ @address = 0
168
+ @data = 0
169
+ @io_mapped = false
170
+ @mmu.on_change { update_overlays! }
171
+ update_overlays!
172
+ end
173
+
174
+ def attach_cartridge(cartridge)
175
+ @cartridge = cartridge
176
+ cartridge.connect(ram: @ram, open_bus: @open_bus)
177
+ cartridge.on_change { update_overlays! }
178
+ update_overlays!
179
+ end
180
+
181
+ # Takes the cartridge out of the expansion port.
182
+ def detach_cartridge
183
+ @cartridge = nil
184
+ update_overlays!
185
+ end
186
+
187
+ def power_on!
188
+ @ram.clear!(RAM_POWER_ON)
189
+ end
190
+
191
+ # The RES line clears the port's direction and output registers and
192
+ # resets the MMU. The port's floating bit keeps its charge.
193
+ def reset!
194
+ @port_ddr = 0x00
195
+ @port_out = 0x00
196
+ @mmu.reset!
197
+ update_port!
198
+ end
199
+
200
+ # Holds CAPS LOCK down, or lets it up.
201
+ def caps_lock=(down)
202
+ @caps_lock = down
203
+ @io_port.value = port_value
204
+ end
205
+
206
+ # Calls the block whenever MCR bit 6 takes the machine into C64 mode or
207
+ # a reset brings it back to C128 mode.
208
+ def on_mode_change(&block)
209
+ @on_mode_change = block
210
+ end
211
+
212
+ # Whether the CPU sees the C128 KERNAL, in C128 mode with CR's system
213
+ # ROM at $C000-$FFFF.
214
+ def system_rom_mapped? = !@mmu.c64_mode? && @mmu.cr.nobits?(0x30)
215
+
216
+ # The RAM the C128 KERNAL's bank number +bank+ reaches (Banks).
217
+ def peek_bank(bank, addr) = @banks.peek(bank, addr)
218
+
219
+ def write_bank(bank, addr, bytes) = @banks.write(bank, addr, bytes)
220
+
221
+ def install_debug_register(&)
222
+ @debug_page = DebugPage.new(@open_bus, &)
223
+ update_overlays!
224
+ end
225
+
226
+ # The address and the byte of the CPU's last access, which a VIC-IIe
227
+ # in FAST mode fetches in place of its own, and which tells the
228
+ # machine when an access reached I/O.
229
+ attr_reader :address, :data
230
+
231
+ def peek(addr)
232
+ @address = addr
233
+ @data = if addr > 0x01
234
+ @read_pages[addr >> 8].peek(addr)
235
+ else
236
+ addr.zero? ? @port_ddr : @io_port.value
237
+ end
238
+ end
239
+
240
+ # Whether the last access went to a chip at $D000-$DFFF, which runs
241
+ # at 1 MHz.
242
+ def io_access? = @io_mapped && (@address & 0xf000) == 0xd000
243
+
244
+ # Whether the last access went to the VIC's registers.
245
+ def vic_access? = @io_mapped && (@address & 0xfc00) == 0xd000
246
+
247
+ # A write to $00 or $01 goes to the port, and the RAM below takes the
248
+ # byte the VIC fetched in the phi1 half of the cycle.
249
+ def poke(addr, value)
250
+ @address = addr
251
+ @data = value
252
+ if addr < 0x02
253
+ @write_pages[0].poke(addr, @vic.phi1_data)
254
+ addr.zero? ? @port_ddr = value : @port_out = value
255
+ update_port!
256
+ else
257
+ @write_pages[addr >> 8].poke(addr, value)
258
+ end
259
+ end
260
+
261
+ def inspect
262
+ "#<#{self.class.name} mode=#{@mmu.mode} port=#{format('0x%02x', @io_port.value)} " \
263
+ "cartridge=#{@cartridge ? @cartridge.class.name : 'none'} " \
264
+ "ultimax=#{@ultimax}>"
265
+ end
266
+
267
+ private
268
+
269
+ def load_c128_roms
270
+ basic = ROM.read("c128/basic.rom")
271
+ kernal = ROM.read("c128/kernal.rom")
272
+ @basic_low_rom = ROM.new(basic[0, 0x4000], length: 0x4000, start: 0x4000)
273
+ @basic_high_rom = ROM.new(basic[0x4000, 0x4000], length: 0x4000, start: 0x8000)
274
+ @editor_rom = ROM.new(kernal[0, 0x1000], length: 0x1000, start: 0xc000)
275
+ @c128_kernal_rom = ROM.new(kernal[0x2000, 0x2000], length: 0x2000, start: 0xe000)
276
+ characters = ROM.read("c128/character.rom")
277
+ @c128_character_rom = ROM.new(characters[0x1000, 0x1000], length: 0x1000, start: 0xd000)
278
+ end
279
+
280
+ def plug_chips(model, sid_model)
281
+ @keyboard = Keyboard.new(matrix: KEYBOARD_MATRIX)
282
+ @joystick1 = Joystick.new
283
+ @joystick2 = Joystick.new
284
+ @control_ports = ControlPorts.new(keyboard: @keyboard, joystick1: @joystick1, joystick2: @joystick2)
285
+ @vic = VIC.new(model: model.vic_model, region: @region)
286
+ @cia1 = CIA.new(start: 0xdc00, peripheral: @control_ports, model: model.cia_model, region: @region)
287
+ @cia2 = CIA.new(start: 0xdd00, model: model.cia_model, region: @region)
288
+ @control_ports.port_a_source = @cia1
289
+ @cia1.on_port_b4_change { |high| @vic.lightpen_level(high) }
290
+ @sid = SID.new(model: sid_model, pots: @control_ports)
291
+ @vdc = VDC.new(model: model.vdc_model, ram_kb: model.vdc_ram_kb, clock_hz: @region.clock_hz)
292
+ end
293
+
294
+ def update_port!
295
+ driven = @port_ddr & PORT_FLOATING
296
+ @port_floating = (@port_floating & ~driven) | (@port_out & driven)
297
+ @io_port.value = port_value
298
+ @datasette.motor = !@io_port.tape_motor?
299
+ update_overlays!
300
+ end
301
+
302
+ def port_value
303
+ input = PORT_PULLUPS | (@port_floating & PORT_FLOATING)
304
+ input |= CAPS_LOCK unless @caps_lock
305
+ input &= ~TAPE_SENSE if @datasette.sense_low?
306
+ (@port_out & @port_ddr) | (input & ~@port_ddr & 0xff)
307
+ end
308
+
309
+ def update_overlays!
310
+ @ultimax = @cartridge ? @cartridge.ultimax? : false
311
+ @phi1_ultimax = @cartridge ? @cartridge.phi1_ultimax? : false
312
+ @mmu.game = @cartridge ? @cartridge.game : 1
313
+ @mmu.exrom = @cartridge ? @cartridge.exrom : 1
314
+ c64_mode = @mmu.c64_mode?
315
+ port = @io_port.value
316
+ @color_lines.push(c64_mode, port)
317
+ @vic.vic_bank.map(@ram, base: @mmu.vic_bank << 16, phi1_ultimax: @phi1_ultimax, ultimax: @ultimax,
318
+ romh: @cartridge&.romh)
319
+ @color_ram = @color_lines.cpu_color_ram(c64_mode, port)
320
+ c64_mode ? map_pla_pages : map_c128_pages
321
+ mode_changed if c64_mode != @c64_mode
322
+ end
323
+
324
+ def mode_changed
325
+ @c64_mode = !@c64_mode
326
+ @on_mode_change&.call
327
+ end
328
+
329
+ def map_io_pages
330
+ @io_mapped = true
331
+ @read_pages.fill(@vic, 0xd0, 4)
332
+ @write_pages.fill(@vic_writes, 0xd0, 4)
333
+ @read_pages[0xd4] = @write_pages[0xd4] = @sid
334
+ @read_pages[0xd5] = @write_pages[0xd5] = @mmu.c64_mode? ? @open_bus : @mmu
335
+ @read_pages[0xd6] = @write_pages[0xd6] = @vdc
336
+ @read_pages[0xd7] = @write_pages[0xd7] = @debug_page || @open_bus
337
+ @read_pages.fill(@color_ram, 0xd8, 4)
338
+ @write_pages.fill(@color_ram, 0xd8, 4)
339
+ @read_pages[0xdc] = @write_pages[0xdc] = @cia1
340
+ @read_pages[0xdd] = @write_pages[0xdd] = @cia2
341
+ @read_pages.fill(@open_bus, 0xde, 2)
342
+ @write_pages.fill(@open_bus, 0xde, 2)
343
+ map_cartridge_io if @cartridge
344
+ end
345
+ end
346
+ end
347
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class C128
5
+ class Bus
6
+ # The bus's state for a snapshot, as AddressBus::SavedState is the
7
+ # C64's.
8
+ module SavedState
9
+ # The machine's memory, the last access and everything on the bus but
10
+ # the keyboard, the joysticks, the pot devices and CAPS LOCK, which the
11
+ # host holds. A cartridge is already in the port, built from its setup.
12
+ def save_state(out)
13
+ out.marker("C128 BUS")
14
+ out.int(@port_ddr).int(@port_out).int(@port_floating).int(@address).int(@data)
15
+ @mmu.save_state(out)
16
+ @ram.save_state(out)
17
+ @color_lines.color_ram(0).save_state(out)
18
+ @color_lines.color_ram(1).save_state(out)
19
+ @cartridge&.save_state(out)
20
+ @datasette.save_state(out)
21
+ @vic.save_state(out)
22
+ @cia1.save_state(out)
23
+ @cia2.save_state(out)
24
+ @sid.save_state(out)
25
+ @vdc.save_state(out)
26
+ end
27
+
28
+ def load_state(input)
29
+ input.marker("C128 BUS")
30
+ @port_ddr = input.int
31
+ @port_out = input.int
32
+ @port_floating = input.int
33
+ @address = input.int
34
+ @data = input.int
35
+ @mmu.load_state(input)
36
+ @ram.load_state(input)
37
+ @color_lines.color_ram(0).load_state(input)
38
+ @color_lines.color_ram(1).load_state(input)
39
+ @cartridge&.load_state(input)
40
+ @datasette.load_state(input)
41
+ @io_port.value = port_value
42
+ update_overlays!
43
+ @vic.load_state(input)
44
+ @control_ports.extra_rows = 0xf8 | @vic.extra_keyboard_lines
45
+ @cia1.load_state(input)
46
+ @cia2.load_state(input)
47
+ @sid.load_state(input)
48
+ @vdc.load_state(input)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class C128
5
+ # The C128's two 1K colour RAM banks, and the colour RAM and character
6
+ # ROM it pushes into the VIC's bank. In C128 mode the 8502's port picks
7
+ # them: P0 the colour RAM bank the CPU sees, P1 the one the VIC sees,
8
+ # and P2 low shows the VIC the character ROM's C128 set. In C64 mode
9
+ # both see colour RAM bank 1, and the VIC the C64 set.
10
+ class ColorLines
11
+ def initialize(vic, cia2, c64_characters, c128_characters)
12
+ @vic_bank = vic.vic_bank
13
+ @cia2 = cia2
14
+ @c64_characters = c64_characters
15
+ @c128_characters = c128_characters
16
+ @bank0 = ColorMemory.new(vic)
17
+ @bank1 = ColorMemory.new(vic)
18
+ end
19
+
20
+ # Colour RAM bank +number+, 0 or 1.
21
+ def color_ram(number) = number.zero? ? @bank0 : @bank1
22
+
23
+ # The colour RAM bank the CPU sees.
24
+ def cpu_color_ram(c64_mode, port) = c64_mode ? @bank1 : color_ram(port & 0x01)
25
+
26
+ # Pushes the colour RAM and the character ROM the VIC sees into its
27
+ # bank.
28
+ def push(c64_mode, port)
29
+ if c64_mode
30
+ @vic_bank.connect(cia2: @cia2, color_ram: @bank1)
31
+ @vic_bank.map_character_rom(@c64_characters)
32
+ else
33
+ @vic_bank.connect(cia2: @cia2, color_ram: color_ram((port >> 1) & 0x01))
34
+ @vic_bank.map_character_rom(@c128_characters, shadow: port.nobits?(0x04))
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class C128
5
+ # The C128's 8502, on the C128::Bus, which holds its I/O port.
6
+ class CPU
7
+ include Badline::CPU::Core
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class C128
5
+ # The three keys outside the keyboard matrix: RESTORE, CAPS LOCK and
6
+ # 40/80 DISPLAY.
7
+ module Keys
8
+ # RESTORE pulses NMI for a cycle, as on the C64.
9
+ def press_restore
10
+ @restore_pulse = true
11
+ end
12
+
13
+ def release_restore; end
14
+
15
+ # CAPS LOCK locks down and up, and holds the 8502's P6 low while down.
16
+ def press_caps_lock
17
+ @bus.caps_lock = true
18
+ end
19
+
20
+ def release_caps_lock
21
+ @bus.caps_lock = false
22
+ end
23
+
24
+ # The 40/80 DISPLAY key locks down and up. The KERNAL reads it at reset
25
+ # and starts the screen editor on the VDC's 80 columns while it is down.
26
+ def press_display_key
27
+ @bus.mmu.display_key = true
28
+ end
29
+
30
+ def release_display_key
31
+ @bus.mmu.display_key = false
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class C128
5
+ # The 8722 MMU's registers, $D500-$D50B in C128 mode:
6
+ #
7
+ # $D500 CR: I/O, the ROMs and the RAM bank the CPU sees
8
+ # $D501-$D504 PCRA-PCRD: preset configurations
9
+ # $D505 MCR: the CPU (bit 0, 1 for the 8502), FSDIR, GAME and EXROM
10
+ # in, C64 mode (bit 6) and the 40/80 key
11
+ # $D506 RCR: common RAM and the VIC's RAM bank
12
+ # $D507-$D50A P0L, P0H, P1L, P1H: page 0 and page 1 relocation
13
+ # $D50B VR: the version, 2 banks of version 0
14
+ #
15
+ # The rest of the page reads $FF. $FF00 is CR again, and a write to
16
+ # $FF01-$FF04, the LCRs, copies PCRA-PCRD into CR. Those five answer in
17
+ # every configuration, whatever CR maps there.
18
+ #
19
+ # A write to P0H or P1H waits until the matching P0L or P1L write
20
+ # takes it.
21
+ #
22
+ # On the C128 the Z80 runs first and hands the bus to the 8502. Here the
23
+ # 8502 runs from reset, with MCR as the Z80 leaves it. A machine built
24
+ # for C64 mode resets into C64 mode, the state the C128 KERNAL leaves
25
+ # when C= is held at power-on. In C64 mode the MMU answers nowhere, but
26
+ # the CPU's RAM bank, common RAM and the VIC's bank it holds stay in
27
+ # force.
28
+ class MMU
29
+ CR = 0
30
+ MCR = 5
31
+ RCR = 6
32
+ P0L = 7
33
+ P0H = 8
34
+ P1L = 9
35
+ P1H = 10
36
+ VR = 11
37
+
38
+ MCR_8502 = 0x01
39
+ MCR_C64_MODE = 0x40
40
+ VERSION = 0x20
41
+
42
+ # MCR's bits that read back what was written: the CPU, FSDIR and C64
43
+ # mode. Bits 1 and 2 have no pins and read 1.
44
+ MCR_WRITABLE = 0x49
45
+ MCR_UNUSED = 0x06
46
+ MCR_GAME = 0x10
47
+ MCR_EXROM = 0x20
48
+ MCR_DISPLAY_KEY = 0x80
49
+
50
+ # The common RAM sizes RCR bits 0-1 pick, in pages.
51
+ COMMON_PAGES = [4, 16, 32, 64].freeze
52
+
53
+ # The registers, CR first.
54
+ attr_reader :registers
55
+
56
+ # Whether the 40/80 DISPLAY key is locked down, which MCR bit 7 reads
57
+ # as 0 in C128 mode.
58
+ attr_accessor :display_key
59
+
60
+ # The cartridge's GAME and EXROM lines, which MCR bits 4 and 5 read.
61
+ attr_writer :game, :exrom
62
+
63
+ # +mode+ is the mode a reset leaves, :c128 or :c64.
64
+ def initialize(mode = :c64)
65
+ @reset_mode = mode
66
+ @registers = Array.new(12, 0)
67
+ @display_key = false
68
+ @game = 1
69
+ @exrom = 1
70
+ @on_change = nil
71
+ reset!
72
+ end
73
+
74
+ # Calls the block whenever the mapping the registers ask for changes.
75
+ def on_change(&block)
76
+ @on_change = block
77
+ end
78
+
79
+ def reset!
80
+ @registers.fill(0)
81
+ @registers[MCR] = @reset_mode == :c64 ? MCR_C64_MODE | MCR_8502 : MCR_8502
82
+ @registers[P1L] = 0x01
83
+ @registers[VR] = VERSION
84
+ @p0h_latch = 0
85
+ @p1h_latch = 0
86
+ @on_change&.call
87
+ end
88
+
89
+ def c64_mode? = @registers[MCR].anybits?(MCR_C64_MODE)
90
+
91
+ # :c64 or :c128, the mode MCR bit 6 selects.
92
+ def mode = c64_mode? ? :c64 : :c128
93
+
94
+ # The 64K bank the VIC sees, RCR bit 6. Bit 7 picks banks a 256K
95
+ # machine has.
96
+ def vic_bank = (@registers[RCR] >> 6) & 0x01
97
+
98
+ # The RAM bank the CPU sees, CR bit 6.
99
+ def cpu_bank = (@registers[CR] >> 6) & 0x01
100
+
101
+ def cr = @registers[CR]
102
+
103
+ # The pages from $0000 up that common RAM keeps in bank 0, or 0.
104
+ def common_low_pages = @registers[RCR].anybits?(0x04) ? common_pages : 0
105
+
106
+ # The first page of the common RAM below $FFFF, or 256 for none.
107
+ def common_high_start = @registers[RCR].anybits?(0x08) ? 256 - common_pages : 256
108
+
109
+ # The page and the bank page 0 moves to.
110
+ def p0_page = @registers[P0L]
111
+ def p0_bank = @registers[P0H] & 0x01
112
+
113
+ # The page and the bank page 1 moves to.
114
+ def p1_page = @registers[P1L]
115
+ def p1_bank = @registers[P1H] & 0x01
116
+
117
+ # The registers at $D500-$D5FF.
118
+ def peek(addr)
119
+ offset = addr & 0xff
120
+ case offset
121
+ when MCR then mcr
122
+ when P0H, P1H then 0xf0 | @registers[offset]
123
+ when 0..VR then @registers[offset]
124
+ else 0xff
125
+ end
126
+ end
127
+
128
+ def poke(addr, value)
129
+ offset = addr & 0xff
130
+ case offset
131
+ when P0H then @p0h_latch = value & 0x0f
132
+ when P1H then @p1h_latch = value & 0x0f
133
+ when P0L then relocate(P0L, P0H, @p0h_latch, value)
134
+ when P1L then relocate(P1L, P1H, @p1h_latch, value)
135
+ when CR..RCR then write(offset, value)
136
+ end
137
+ end
138
+
139
+ # The registers and the P0H and P1H writes waiting for P0L and P1L.
140
+ # The bus maps the restored registers once its own state is back.
141
+ def save_state(out)
142
+ out.ints(@registers).int(@p0h_latch).int(@p1h_latch)
143
+ end
144
+
145
+ def load_state(input)
146
+ input.ints_into(@registers)
147
+ @p0h_latch = input.int
148
+ @p1h_latch = input.int
149
+ end
150
+
151
+ # CR and the LCRs at $FF00-$FF04: an LCR reads its PCR.
152
+ def peek_configuration(addr) = @registers[addr & 0x07]
153
+
154
+ def poke_configuration(addr, value)
155
+ offset = addr & 0x07
156
+ write(CR, offset.zero? ? value : @registers[offset])
157
+ end
158
+
159
+ private
160
+
161
+ def common_pages = COMMON_PAGES[@registers[RCR] & 0x03]
162
+
163
+ def mcr
164
+ value = (@registers[MCR] & MCR_WRITABLE) | MCR_UNUSED
165
+ value |= MCR_GAME if @game == 1
166
+ value |= MCR_EXROM if @exrom == 1
167
+ value |= MCR_DISPLAY_KEY unless @display_key
168
+ value
169
+ end
170
+
171
+ def relocate(low, high, latch, value)
172
+ @registers[high] = latch
173
+ write(low, value)
174
+ end
175
+
176
+ def write(offset, value)
177
+ @registers[offset] = value
178
+ @on_change&.call
179
+ end
180
+ end
181
+ end
182
+ end