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,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/c128/vdc/memory"
4
+ require "badline/c128/vdc/window"
5
+ require "badline/c128/vdc/painter"
6
+ require "badline/c128/vdc/raster"
7
+
8
+ module Badline
9
+ class C128
10
+ # The 8563 or 8568 VDC: the address register at $D600 and the data
11
+ # register at $D601, mirrored through $D6FF, in front of 37 internal
12
+ # registers (38 on the 8568), the VDC's own RAM (Memory) and its 80
13
+ # column RGBI display (Raster, Painter).
14
+ #
15
+ # A write to $D600 selects a register, and a read there gives the
16
+ # status: bit 7 ready for the next access, bit 5 in the vertical blank
17
+ # (outside the displayed rows), and bits 0-2 the version. $D601 reads
18
+ # and writes the selected register. A register past the last reads $FF.
19
+ # The light pen is left out.
20
+ #
21
+ # R18 and R19 hold the update address, high byte first, and a write to
22
+ # either reads the RAM there into R31. A read of R31 gives that byte,
23
+ # moves the address on and reads the next. A write of R31 stores the
24
+ # byte, moves the address on and reads the next. A write of R30 then
25
+ # repeats the last byte written over R30 more bytes (256 for 0), or with
26
+ # R24 bit 7 copies that many from R32/R33 on, and leaves the update
27
+ # address, and R32/R33 for a copy, after the last byte.
28
+ #
29
+ # The VDC runs on its own 16 MHz dot clock, a character clock every
30
+ # character width of dots (R22, doubled by R25 bit 4). The status reads
31
+ # not ready for ACCESS_CLOCKS character clocks after an access through
32
+ # R31 or a write of R18 or R19, and for one clock per byte a block write
33
+ # fills or two per byte it copies.
34
+ class VDC
35
+ include Addressable
36
+
37
+ STATUS_READY = 0x80
38
+ STATUS_VBLANK = 0x20
39
+ UPDATE_HIGH = 18
40
+ UPDATE_LOW = 19
41
+ WORD_COUNT = 30
42
+ DATA = 31
43
+ SOURCE_HIGH = 32
44
+ SOURCE_LOW = 33
45
+ DOT_CLOCK_HZ = 16_000_000
46
+
47
+ # Character clocks the status reads busy after an access through R31
48
+ # or a write of R18 or R19, fitted to the timings a flat C128 gave
49
+ # VDC/vdctiming (TimingResultsFlatC128.jpg).
50
+ ACCESS_CLOCKS = 30
51
+
52
+ # The version in the status bits: 1 for the 8563 R8 and R9, and 2 for
53
+ # the 8568.
54
+ VERSIONS = { mos8563: 1, mos8568: 2 }.freeze
55
+
56
+ # The 16 RGBI colours, red, green, blue and intensity from bit 3 down,
57
+ # as the Programmer's Reference Guide names them: 1100 is dark yellow.
58
+ PALETTE = Array.new(16) do |rgbi|
59
+ intensity = rgbi.anybits?(1) ? 0x55 : 0
60
+ red = (rgbi.anybits?(8) ? 0xaa : 0) + intensity
61
+ green = (rgbi.anybits?(4) ? 0xaa : 0) + intensity
62
+ blue = (rgbi.anybits?(2) ? 0xaa : 0) + intensity
63
+ (red << 16) | (green << 8) | blue
64
+ end.freeze
65
+
66
+ attr_reader :model, :registers
67
+
68
+ # Whether lines paint into the display as the raster passes them.
69
+ attr_reader :render
70
+
71
+ # +ram_kb+ is 16 or 64, and +clock_hz+ the CPU's clock the VDC is
72
+ # clocked against.
73
+ def initialize(model: :mos8563, ram_kb: 16, clock_hz: Region::PAL.clock_hz)
74
+ addressable_at(0xd600, length: 0x100)
75
+ @model = model
76
+ @version = VERSIONS.fetch(model)
77
+ @clock_hz = clock_hz
78
+ @registers = Array.new(model == :mos8568 ? 38 : 37, 0)
79
+ @memory = Memory.new(ram_kb)
80
+ @painter = Painter.new(@registers, @memory)
81
+ @raster = Raster.new(@registers, @painter)
82
+ @render = false
83
+ power_on!
84
+ end
85
+
86
+ # The dots a character takes, R22 bits 4-7 plus one, or R22 bits 4-7
87
+ # doubled in double-width mode.
88
+ def self.char_dots(registers)
89
+ width = registers[22] >> 4
90
+ registers[25].anybits?(0x10) ? [width, 1].max * 2 : width + 1
91
+ end
92
+
93
+ # The registers clear, the RAM clears and the raster starts a frame.
94
+ def power_on!
95
+ @registers.fill(0)
96
+ @memory.clear!
97
+ @memory.mode64 = false
98
+ @selected = 0
99
+ @read_latch = 0
100
+ @write_data = 0
101
+ @busy_until = 0
102
+ @phase = 0
103
+ @raster.reset!
104
+ @line_units = @raster.line_dots * @clock_hz
105
+ @painter.clear!
106
+ end
107
+
108
+ def ram = @memory.ram
109
+
110
+ def render=(on)
111
+ @render = on
112
+ @painter.clear!
113
+ end
114
+
115
+ # Everything the VDC holds but its display, which it paints again
116
+ # while it renders: the registers, the RAM, the data port and the
117
+ # raster. Whether it renders is the host's.
118
+ def save_state(out)
119
+ out.marker("VDC")
120
+ out.ints(@registers).blob(@memory.ram)
121
+ out.int(@selected).int(@read_latch).int(@write_data).int(@busy_until).int(@phase).int(@line_units)
122
+ @raster.save_state(out)
123
+ @painter.save_state(out)
124
+ end
125
+
126
+ def load_state(input)
127
+ input.marker("VDC")
128
+ input.ints_into(@registers)
129
+ input.blob_into(@memory.ram)
130
+ @memory.mode64 = @registers[28].anybits?(0x10)
131
+ @selected = input.int
132
+ @read_latch = input.int
133
+ @write_data = input.int
134
+ @busy_until = input.int
135
+ @phase = input.int
136
+ @line_units = input.int
137
+ @raster.load_state(input)
138
+ @painter.load_state(input)
139
+ end
140
+
141
+ # The frames the raster has finished, which the blinking counts.
142
+ def frame = @raster.frame
143
+
144
+ def display = @painter.display
145
+
146
+ def width = @painter.width
147
+
148
+ def height = @painter.height
149
+
150
+ def dirty_lines = @painter.dirty_lines
151
+
152
+ def clear_dirty_lines! = @painter.clear_dirty_lines!
153
+
154
+ def palette = PALETTE
155
+
156
+ # The [left, top, width, height] of the display a monitor shows.
157
+ def crop = @painter.crop(@raster.line_dots, @raster.frame_lines, @raster.sync_lines)
158
+
159
+ # The update address R18 and R19 hold.
160
+ def update_address = (@registers[UPDATE_HIGH] << 8) | @registers[UPDATE_LOW]
161
+
162
+ # Whether an access or a block operation is still under way.
163
+ def busy? = @phase < @busy_until
164
+
165
+ # One CPU cycle: the dot clock runs on, and a line ends once it has
166
+ # run a line's dots. The line's phase counts dots times the CPU's
167
+ # clock, DOT_CLOCK_HZ a cycle.
168
+ def cycle!
169
+ @phase += DOT_CLOCK_HZ
170
+ end_line if @phase >= @line_units
171
+ end
172
+
173
+ def peek(addr)
174
+ return status if offset_of(addr).even?
175
+ return read_ram if @selected == DATA
176
+
177
+ @selected < @registers.length ? @registers[@selected] : 0xff
178
+ end
179
+
180
+ def poke(addr, value)
181
+ return @selected = value & 0x3f if offset_of(addr).even?
182
+ return if @selected >= @registers.length
183
+
184
+ write_register(@selected, value)
185
+ end
186
+
187
+ private
188
+
189
+ def status
190
+ value = @version
191
+ value |= STATUS_READY unless busy?
192
+ value |= STATUS_VBLANK if @raster.vertical_blank?
193
+ value
194
+ end
195
+
196
+ def end_line
197
+ @phase -= @line_units
198
+ @busy_until -= @line_units
199
+ @raster.end_line(@render)
200
+ @line_units = @raster.line_dots * @clock_hz
201
+ end
202
+
203
+ def write_register(number, value)
204
+ @registers[number] = value
205
+ case number
206
+ when DATA then write_ram(value)
207
+ when WORD_COUNT then block(value.zero? ? 256 : value)
208
+ when UPDATE_HIGH, UPDATE_LOW then read_ahead
209
+ when 28 then @memory.mode64 = value.anybits?(0x10)
210
+ end
211
+ end
212
+
213
+ def read_ram
214
+ value = @read_latch
215
+ advance(1)
216
+ read_ahead
217
+ value
218
+ end
219
+
220
+ def write_ram(value)
221
+ @memory.store(update_address, value)
222
+ @write_data = value
223
+ advance(1)
224
+ read_ahead
225
+ end
226
+
227
+ def read_ahead
228
+ @read_latch = @memory.fetch(update_address)
229
+ busy(ACCESS_CLOCKS)
230
+ end
231
+
232
+ def block(count)
233
+ copy = @registers[24].anybits?(0x80)
234
+ copy ? copy_block(count) : fill_block(count)
235
+ @read_latch = @memory.fetch(update_address)
236
+ busy(copy ? count * 2 : count)
237
+ end
238
+
239
+ def fill_block(count)
240
+ address = update_address
241
+ count.times { |i| @memory.store(address + i, @write_data) }
242
+ advance(count)
243
+ end
244
+
245
+ def copy_block(count)
246
+ destination = update_address
247
+ source = (@registers[SOURCE_HIGH] << 8) | @registers[SOURCE_LOW]
248
+ count.times { |i| @memory.store(destination + i, @memory.fetch(source + i)) }
249
+ advance(count)
250
+ source = (source + count) & 0xffff
251
+ @registers[SOURCE_HIGH] = source >> 8
252
+ @registers[SOURCE_LOW] = source & 0xff
253
+ end
254
+
255
+ def advance(count)
256
+ address = (update_address + count) & 0xffff
257
+ @registers[UPDATE_HIGH] = address >> 8
258
+ @registers[UPDATE_LOW] = address & 0xff
259
+ end
260
+
261
+ # Holds the status busy for `clocks` character clocks after whatever
262
+ # is already under way.
263
+ def busy(clocks)
264
+ @busy_until = [@busy_until, @phase].max + (clocks * VDC.char_dots(@registers) * @clock_hz)
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,322 @@
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
+ require "badline/c128/drives"
16
+ require "badline/c128/fast_serial"
17
+ require "badline/drive1571"
18
+
19
+ module Badline
20
+ # The Commodore 128: the 8502 on the VIC-IIe's clock, two CIAs, the SID,
21
+ # the VDC and its 80 column display, and 128K of RAM, which the 8722 MMU
22
+ # maps (C128::Bus).
23
+ #
24
+ # Built for C128 mode, it runs BASIC 7.0 and the C128 KERNAL from reset.
25
+ # The C128's Z80 runs first and hands the bus to the 8502, but here the
26
+ # 8502 starts at its reset vector, with the MMU as the Z80 leaves it.
27
+ # The KERNAL goes to C64 mode as on the C128, with C= held at reset, a
28
+ # C64 cartridge or GO64.
29
+ #
30
+ # Built for C64 mode, the default, it powers on in C64 mode, the state
31
+ # the C128 KERNAL reaches when C= is held at power-on, and a reset comes
32
+ # back there. It runs the C64's BASIC and KERNAL, so the C64's KERNAL
33
+ # traps, the keyboard buffer and CHROUT capture work unchanged.
34
+ #
35
+ # Where a C64 program sees it differ from a C64C: the 8502's P6 senses CAPS
36
+ # LOCK, $D02F drives the extra keyboard rows, $D030's FAST bit runs the
37
+ # CPU at 2 MHz and its TEST bit races the raster counter, the SID has no
38
+ # mirrors, and $D500-$D7FF holds the hidden MMU, the VDC and open bus.
39
+ class C128
40
+ include IntegerHelper
41
+ include KeyboardBuffer
42
+ include Computer::Attachments
43
+ include Computer::KernalTraps
44
+ include Keys
45
+ include Modes
46
+ include Drives
47
+ include FastSerial
48
+
49
+ # The C64's 8x8 matrix and the three rows K0-K2 select, in port B
50
+ # column order.
51
+ KEYBOARD_MATRIX = (Keyboard::C64_MATRIX + [
52
+ %i[help keypad8 keypad5 tab keypad2 keypad4 keypad7 keypad1],
53
+ %i[esc keypad_plus keypad_minus line_feed keypad_enter keypad6 keypad9 keypad3],
54
+ %i[alt keypad0 keypad_period crsr_up crsr_down crsr_left crsr_right no_scroll]
55
+ ]).freeze
56
+
57
+ # The cycle at which #on_init's handlers run, once the KERNAL of the
58
+ # mode the machine is built for has booted: the C64's as on a C64, or
59
+ # BASIC 7.0's, which is READY by then.
60
+ C128_INIT_THRESHOLD = 2_000_000
61
+
62
+ # The C128 KERNAL's keyboard buffer and its count.
63
+ C128_KEYBOARD_BUFFER = 0x034a
64
+ C128_KEYBOARD_COUNT = 0xd0
65
+
66
+ attr_reader :cpu, :cycles, :drive1541, :model, :init_threshold
67
+
68
+ # The path of the disk or directory device 8 serves through the traps
69
+ # (Computer::Attachments), or an empty one.
70
+ def mounted_path = @drive.nil? ? "" : @drive.path
71
+
72
+ def family = :c128
73
+
74
+ def address_bus = @bus
75
+
76
+ def region = @bus.region
77
+
78
+ def vic = @bus.vic
79
+
80
+ def cia1 = @bus.cia1
81
+
82
+ def cia2 = @bus.cia2
83
+
84
+ def sid = @bus.sid
85
+
86
+ def vdc = @bus.vdc
87
+
88
+ def mmu = @bus.mmu
89
+
90
+ def ram = @bus.ram
91
+
92
+ def keyboard = @bus.keyboard
93
+
94
+ def joystick1 = @bus.joystick1
95
+
96
+ def joystick2 = @bus.joystick2
97
+
98
+ def control_ports = @bus.control_ports
99
+
100
+ def datasette = @bus.datasette
101
+
102
+ # :c64 or :c128, the mode the MMU is in.
103
+ def mode = @bus.mmu.mode
104
+
105
+ def install_debug_register(&) = @bus.install_debug_register(&)
106
+
107
+ # +model+ names one of Model::ALL, and +sid_model+, when not nil, the
108
+ # SID in place of the model's. +mode+, :c64 or :c128, is the mode it
109
+ # powers on and resets into.
110
+ def initialize(model: "c128", sid_model: nil, mode: :c64, debug: false)
111
+ raise ArgumentError, "no C128 mode named #{mode}" unless %i[c64 c128].include?(mode)
112
+
113
+ @model = Model.named(model)
114
+ @c64_built = mode == :c64
115
+ @init_threshold = @c64_built ? Computer::INIT_THRESHOLD : C128_INIT_THRESHOLD
116
+ @holding_commodore = false
117
+ @bus = Bus.new(@model, sid_model: sid_model || @model.sid_model, mode:)
118
+ @cpu = CPU.new(@bus, debug:)
119
+ @vic = @bus.vic
120
+ @vic.open_bus = -> { @bus.ram.peek(@cpu.program_counter) }
121
+ @cia1 = @bus.cia1
122
+ @cia2 = @bus.cia2
123
+ @sid = @bus.sid
124
+ @vdc = @bus.vdc
125
+ @datasette = @bus.datasette
126
+ @cycles = 0
127
+ @clock_bits = 0
128
+ @nmi_asserted = false
129
+ @cartridge_nmi = false
130
+ @restore_pulse = false
131
+ @init_handlers = []
132
+ @pending_keys = nil
133
+ @capture_output = nil
134
+ init_drives
135
+ enter_c64_mode if @c64_built
136
+ @bus.on_mode_change { mode_changed }
137
+ end
138
+
139
+ # The chips clock ahead of the CPU, as on the C64. $D030's FAST and
140
+ # TEST bits take hold a cycle after the write that sets them: in FAST
141
+ # mode the CPU runs in both halves of the cycle (#clock_fast), and the
142
+ # TEST bit steps the raster counter in every cycle.
143
+ def cycle!
144
+ handle_init if @cycles == @init_threshold
145
+ feed_keyboard if @pending_keys
146
+
147
+ clock_bits = @clock_bits
148
+ @clock_bits = @vic.clock_bits
149
+ @vic.test_step! if clock_bits >= 0x02
150
+
151
+ @vic.cycle!
152
+ @cia1.cycle!
153
+ @cia2.cycle!
154
+ @sid.cycle!
155
+ @datasette.cycle!
156
+ @vdc.cycle!
157
+
158
+ @cpu.irq = @cia1.interrupted? || @vic.interrupted?
159
+
160
+ drive_nmi
161
+ clock_bits.odd? ? clock_fast : clock_cpu
162
+ clock_serial_bus
163
+
164
+ @cycles += 1
165
+ end
166
+
167
+ # Runs `count` cycles, one #cycle! after another.
168
+ def run_cycles(count)
169
+ i = 0
170
+ while i < count
171
+ cycle!
172
+ i += 1
173
+ end
174
+ end
175
+
176
+ # Runs cycles until the block returns true or the cycle count passes
177
+ # `limit`, checking before each cycle.
178
+ def run_until(limit)
179
+ cycle! until yield || @cycles > limit
180
+ end
181
+
182
+ def on_init(&block)
183
+ if @cycles < init_threshold
184
+ @init_handlers << block
185
+ else
186
+ block.call
187
+ end
188
+ end
189
+
190
+ # The clock, the raster and the crop of the VIC-IIe's region.
191
+ def timing
192
+ region = @bus.region
193
+ Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
194
+ lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
195
+ end
196
+
197
+ # The chip whose #display a front end shows: the VIC-IIe, or with
198
+ # :vdc the VDC, whose display paints only while its #render is on.
199
+ def video(chip = :vic) = chip == :vdc ? @vdc : @vic
200
+
201
+ # Renders the VDC's display, and not the VIC-IIe's, while a front end
202
+ # shows it, or with false the other way round.
203
+ def vdc_shown=(shown)
204
+ @vdc.render = shown
205
+ @vic.render = !shown
206
+ end
207
+
208
+ # The chip a front end records the machine's sound from.
209
+ def sound_source = @sid
210
+
211
+ def load_prg(data)
212
+ uint16(data[0], data[1]).tap do |load_addr|
213
+ ram.write(load_addr, data[2..])
214
+ end
215
+ end
216
+
217
+ # A cartridge goes in with the power off.
218
+ def attach_cartridge(cartridge)
219
+ connect_cartridge(cartridge)
220
+ power_cycle!
221
+ end
222
+
223
+ # RAM, the VIC and the VDC start from their power-on state, and the RES
224
+ # line resets everything else.
225
+ def power_cycle!
226
+ @vic.power_on!
227
+ @vdc.power_on!
228
+ @bus.power_on!
229
+ reset!
230
+ enter_c64_mode if @c64_built
231
+ end
232
+
233
+ # The RES line reaches the CPU and its port, the MMU, both CIAs, the
234
+ # SID, the cartridge port and, through the serial bus, the drives.
235
+ def reset!
236
+ @bus.reset!
237
+ @cia1.reset!
238
+ @cia2.reset!
239
+ @sid.reset!
240
+ @bus.cartridge&.reset
241
+ @drive&.reset!
242
+ @drive1541&.reset!
243
+ @drive1571&.reset!
244
+ @nmi_asserted = false
245
+ @cpu.reset!
246
+ end
247
+
248
+ # CHROUT is at $FFD2 in both KERNALs, and the trap follows the mode.
249
+ def capture_output
250
+ @capture_output ||= ChroutTrap.new(cpu:, bus: @bus, layout: trap_layout).tap do |trap|
251
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
252
+ end
253
+ end
254
+
255
+ def inspect
256
+ "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
257
+ end
258
+
259
+ private
260
+
261
+ # The C128 KERNAL leaves $D02F at $FF, every extra keyboard row
262
+ # deselected, before it jumps to the C64's reset.
263
+ def enter_c64_mode
264
+ @vic.poke(0xd02f, 0xff)
265
+ @bus.control_ports.extra_rows = 0xff
266
+ end
267
+
268
+ # The NMI line is wired-OR between CIA 2, the cartridge and RESTORE,
269
+ # and the CPU takes an interrupt on its falling edge.
270
+ def drive_nmi
271
+ nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
272
+ @restore_pulse = false
273
+ @cpu.nmi = true if nmi && !@nmi_asserted
274
+ @nmi_asserted = nmi
275
+ end
276
+
277
+ # BA halts the CPU on a read cycle.
278
+ def clock_cpu
279
+ @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
280
+ end
281
+
282
+ # The 8502 at 2 MHz takes both halves of the cycle, and BA no longer
283
+ # halts it. It leaves phi1 to the VIC in the refresh cycles, and an
284
+ # access to I/O, whose chips run at 1 MHz, waits for phi2. The VIC's
285
+ # fetches in the cycle see the bytes on the bus. In a phi1 the CPU
286
+ # spends waiting that is the byte it writes, or the last byte the bus
287
+ # held.
288
+ def clock_fast
289
+ phi1 = 0xff
290
+ unless @vic.refresh_cycle?
291
+ held = @bus.data
292
+ write = @cpu.pending_write?
293
+ @cpu.cycle!
294
+ if @bus.io_access?
295
+ data = @bus.data
296
+ return @vic.take_cpu_bus(write ? data : held, data, @bus.vic_access?)
297
+ end
298
+
299
+ phi1 = @bus.data
300
+ end
301
+ @cpu.cycle!
302
+ @vic.take_cpu_bus(phi1, @bus.data, @bus.vic_access?)
303
+ end
304
+
305
+ def handle_init
306
+ @init_handlers.each(&:call)
307
+ end
308
+
309
+ # The keyboard buffer of the KERNAL the machine runs: the C64's, or in
310
+ # C128 mode BASIC 7.0's.
311
+ def feed_keyboard
312
+ c64 = mode == :c64
313
+ count = c64 ? KeyboardBuffer::COUNT : C128_KEYBOARD_COUNT
314
+ return unless ram.peek(count).zero?
315
+
316
+ chunk = @pending_keys.shift(KeyboardBuffer::CAPACITY)
317
+ ram.write(c64 ? KeyboardBuffer::ADDRESS : C128_KEYBOARD_BUFFER, chunk)
318
+ ram.poke(count, chunk.length)
319
+ @pending_keys = nil if @pending_keys.empty?
320
+ end
321
+ end
322
+ 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
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CIA
5
+ # Running a CIA a stretch of cycles at once, for Drive::Idle.
6
+ module FastForward
7
+ # Whether fast_forward can run the CIA: timer B stopped with nothing
8
+ # in its pipeline, no interrupt or acknowledge on its way, CNT where it
9
+ # was, the TOD clock stopped, and timer A either stopped, or free
10
+ # running (Timer#free_running?) with its interrupt masked and the
11
+ # serial port an idle input. Drive::Idle sleeps the 1571's CIA through
12
+ # such cycles, which its DOS leaves timer A running in.
13
+ def quiet?
14
+ return false unless @tb.idle? && @icr.quiet && @tod.quiet? && cnt_steady?
15
+
16
+ @ta.idle? ? @serial.idle : timer_a_free_running?
17
+ end
18
+
19
+ # Runs +cycles+ cycles at once while quiet?, as cycle! would: timer A's
20
+ # underflows flag in the ICR, and reach the serial port's underflow
21
+ # line.
22
+ def fast_forward(cycles)
23
+ @tod.fast_forward(cycles)
24
+ return if @ta.idle?
25
+
26
+ @icr.flag(:timer_a) if @ta.fast_forward(cycles).positive?
27
+ @serial.follow_underflow(@ta.underflowed)
28
+ end
29
+
30
+ private
31
+
32
+ # Timer A free running with its interrupt masked, and the serial port
33
+ # an idle input it only clocks.
34
+ def timer_a_free_running?
35
+ @ta.free_running? && !@control_a.serial_mode? && @icr.mask.value.nobits?(0x01) && @serial.drained?
36
+ end
37
+
38
+ # Whether CNT holds the level the last cycle sampled.
39
+ def cnt_steady? = @serial.cnt == @cnt_high && !@cnt_rise
40
+ end
41
+ end
42
+ end
@@ -79,6 +79,18 @@ module Badline
79
79
  @control.serial_mode?
80
80
  end
81
81
 
82
+ # Whether nothing is counting down or in the delay lines, so only
83
+ # timer A's underflows reach the register.
84
+ def drained? = idle?
85
+
86
+ # Takes timer A's underflow line as cycle! would have left it, with
87
+ # +underflowed+ its level on the last cycle, for a CIA run without
88
+ # the register in use (CIA#fast_forward).
89
+ def follow_underflow(underflowed)
90
+ @underflow_high = underflowed
91
+ @idle = !underflowed
92
+ end
93
+
82
94
  def cnt_in=(level)
83
95
  @cnt_in = level
84
96
  @cnt = level unless output?