badline 0.18.0 → 0.19.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/CLAUDE.md +8 -0
  4. data/CONTRIBUTING.md +4 -0
  5. data/README.md +10 -7
  6. data/doc/library.md +5 -1
  7. data/doc/media.md +3 -2
  8. data/doc/pinned-behaviour.md +41 -1
  9. data/doc/snapshot-format.md +56 -24
  10. data/doc/snapshots.md +45 -8
  11. data/doc/vice.md +344 -0
  12. data/lib/badline/audio/renderer.rb +4 -8
  13. data/lib/badline/c128.rb +4 -0
  14. data/lib/badline/c64/bus/fittings.rb +6 -0
  15. data/lib/badline/c64.rb +6 -0
  16. data/lib/badline/cbm2/p500.rb +4 -0
  17. data/lib/badline/cbm2.rb +4 -0
  18. data/lib/badline/chips/cpu/core.rb +29 -4
  19. data/lib/badline/chips/cpu/interrupts.rb +9 -0
  20. data/lib/badline/chips/cpu/keyed_state.rb +15 -9
  21. data/lib/badline/chips/cpu/microcode.rb +10 -0
  22. data/lib/badline/chips/rriot.rb +4 -1
  23. data/lib/badline/chips/sid/decimator.rb +1 -1
  24. data/lib/badline/chips/sid/keyed_parts.rb +51 -18
  25. data/lib/badline/chips/sid.rb +6 -2
  26. data/lib/badline/chips/vic_i/keyed_parts.rb +6 -1
  27. data/lib/badline/chips/vic_i/light_pen.rb +89 -0
  28. data/lib/badline/chips/vic_i.rb +12 -7
  29. data/lib/badline/drives/model1541/sector_reader.rb +20 -3
  30. data/lib/badline/drives/model1541.rb +1 -0
  31. data/lib/badline/drives/model1551.rb +1 -0
  32. data/lib/badline/drives/model1571.rb +1 -0
  33. data/lib/badline/drives/model1581.rb +1 -0
  34. data/lib/badline/drives/model2031.rb +1 -0
  35. data/lib/badline/drives/model4040/mechanism.rb +6 -3
  36. data/lib/badline/drives/model4040.rb +17 -1
  37. data/lib/badline/drives/model8050/board.rb +2 -0
  38. data/lib/badline/drives/model8050/dos_board.rb +49 -6
  39. data/lib/badline/drives/model8050/fdc_bus.rb +7 -0
  40. data/lib/badline/drives/model8050/idle.rb +137 -24
  41. data/lib/badline/drives/model8050.rb +14 -0
  42. data/lib/badline/drives/model9060/idle.rb +30 -10
  43. data/lib/badline/drives/model9060.rb +2 -0
  44. data/lib/badline/input/joystick.rb +15 -2
  45. data/lib/badline/kim1/tape.rb +3 -1
  46. data/lib/badline/kim1.rb +4 -0
  47. data/lib/badline/machine.rb +3 -0
  48. data/lib/badline/media/true_drive.rb +10 -0
  49. data/lib/badline/media/vic20_media.rb +4 -2
  50. data/lib/badline/media.rb +5 -1
  51. data/lib/badline/memory.rb +2 -4
  52. data/lib/badline/options/table.rb +1 -1
  53. data/lib/badline/pet/superpet/dongle.rb +13 -2
  54. data/lib/badline/pet/superpet.rb +5 -0
  55. data/lib/badline/pet.rb +4 -0
  56. data/lib/badline/plus4.rb +4 -0
  57. data/lib/badline/roms/README +11 -1
  58. data/lib/badline/snapshot/container.rb +6 -4
  59. data/lib/badline/snapshot/field_doc.rb +8 -4
  60. data/lib/badline/snapshot/image.rb +15 -6
  61. data/lib/badline/snapshot/vice/pet.rb +1 -0
  62. data/lib/badline/snapshot/vice/pet_dongle.rb +72 -0
  63. data/lib/badline/snapshot/vice/pet_mem.rb +11 -5
  64. data/lib/badline/snapshot/vice/vic20.rb +70 -0
  65. data/lib/badline/snapshot/vice/vic20_cpu.rb +61 -0
  66. data/lib/badline/snapshot/vice/vic20_mem.rb +85 -0
  67. data/lib/badline/snapshot/vice/vic20_restore.rb +124 -0
  68. data/lib/badline/snapshot/vice/vic20_vici.rb +200 -0
  69. data/lib/badline/snapshot/vice.rb +1 -0
  70. data/lib/badline/snapshot.rb +4 -4
  71. data/lib/badline/storage/disk_image/writing.rb +2 -2
  72. data/lib/badline/version.rb +1 -1
  73. data/lib/badline/vic20/port_wiring.rb +11 -0
  74. data/lib/badline/vic20.rb +4 -0
  75. metadata +9 -1
@@ -95,6 +95,14 @@ module Badline
95
95
  op_int_vector_low op_int_vector_high
96
96
  ].freeze
97
97
 
98
+ # RES runs BRK's sequence with its three stack writes turned into
99
+ # reads, which still count S down, and reads the vector at $FFFC. Its
100
+ # first two cycles read the program counter and leave it where it was.
101
+ RESET_PLAN = %i[
102
+ op_reset_dummy op_reset_dummy op_reset_stack op_reset_stack op_reset_stack
103
+ op_int_vector_low op_int_vector_high
104
+ ].freeze
105
+
98
106
  SPECIAL_PLANS = {
99
107
  brk: BRK_PLAN, jam: JAM_PLAN, jsr: JSR_PLAN, rts: RTS_PLAN,
100
108
  rti: RTI_PLAN, pha: PUSH_PLAN, php: PUSH_PLAN,
@@ -181,6 +189,7 @@ module Badline
181
189
  FETCH_WRITES = Microcode.write_mask(FETCH_PLAN)
182
190
  INTERRUPT_WRITES = Microcode.write_mask(INTERRUPT_PLAN)
183
191
  JAMMED_WRITES = Microcode.write_mask(JAMMED_PLAN)
192
+ RESET_WRITES = Microcode.write_mask(RESET_PLAN)
184
193
  MICROCODE = Microcode.table
185
194
 
186
195
  # The instruction methods, in a fixed order a snapshot names them by.
@@ -190,6 +199,7 @@ module Badline
190
199
  FETCH = -1
191
200
  INTERRUPT = -2
192
201
  JAMMED = -3
202
+ RESET = -4
193
203
  end
194
204
  end
195
205
  end
@@ -74,7 +74,10 @@ module Badline
74
74
 
75
75
  # Everything the RRIOT holds but its timer's counter, for comparing
76
76
  # it at two points.
77
- def idle_state = [*@ram, @ora, @orb, @ddra, @ddrb, *@timer.state, @timer.flag]
77
+ def idle_state = [*@ram, *port_state]
78
+
79
+ # The ports and the timer but for its counter.
80
+ def port_state = [@ora, @orb, @ddra, @ddrb, *@timer.state, @timer.flag]
78
81
 
79
82
  def read_rom(addr) = @rom[addr & 0x3ff]
80
83
 
@@ -21,7 +21,7 @@ module Badline
21
21
  end
22
22
 
23
23
  # Cycles until the current window closes, counting the next one as 1.
24
- attr_reader :cycles_to_close
24
+ attr_reader :cycles_to_close, :rate, :clock_hz
25
25
 
26
26
  # How far the current window has got, in a keyed snapshot module
27
27
  # (Snapshot::Keyed) under `prefix`. The clock and the rate are the
@@ -221,7 +221,11 @@ module Badline
221
221
  # voice's under voice1. to voice3., and the filter's under filter.
222
222
  # and ext. Saving catches the DSP up first, so no span or deferred
223
223
  # write is left to save. What the registers decode to is worked out
224
- # from them on load.
224
+ # from them on load. A recording SID also saves how far its recording
225
+ # has got under rec., with the rate, the clock and the filter chunk it
226
+ # records at: the samples' windows bound the filter's steps, so a SID
227
+ # loaded from it that records at the same rate and clock carries on
228
+ # from that window and integrates the steps the saved one does.
225
229
  module KeyedState
226
230
  def save_fields(out)
227
231
  catch_up
@@ -230,6 +234,8 @@ module Badline
230
234
  @voice2.save_fields(out, "voice2.")
231
235
  @voice3.save_fields(out, "voice3.")
232
236
  @filter.save_fields(out, "filter.")
237
+ recording = @decimator || @resumed_recording
238
+ save_recording(out, recording) if recording
233
239
  out
234
240
  end
235
241
 
@@ -243,23 +249,7 @@ module Badline
243
249
  @voice2.load_fields(input, "voice2.")
244
250
  @voice3.load_fields(input, "voice3.")
245
251
  @filter.load_fields(input, "filter.")
246
- end
247
-
248
- # How far the recording has got into the sample it is averaging,
249
- # under `prefix`, for a host that carries on recording from a
250
- # snapshot: it calls #record, then #load_recording_fields.
251
- def save_recording_fields(out, prefix)
252
- decimator = @decimator
253
- raise ArgumentError, "the SID isn't recording" if decimator.nil?
254
-
255
- decimator.save_fields(out, prefix)
256
- end
257
-
258
- def load_recording_fields(input, prefix)
259
- decimator = @decimator
260
- raise ArgumentError, "the SID isn't recording" if decimator.nil?
261
-
262
- decimator.load_fields(input, prefix)
252
+ load_recording(input)
263
253
  end
264
254
 
265
255
  def restored!
@@ -272,6 +262,49 @@ module Badline
272
262
 
273
263
  private
274
264
 
265
+ def save_recording(out, decimator)
266
+ out.int("rec.rate", decimator.rate).int("rec.clock_hz", decimator.clock_hz)
267
+ out.int("rec.filter_chunk", @filter_chunk)
268
+ decimator.save_fields(out, "rec.")
269
+ end
270
+
271
+ # A SID recording already carries on from the loaded window when it
272
+ # records at its rate, clock and filter chunk, and starts a new
273
+ # window otherwise.
274
+ def load_recording(input)
275
+ @resumed_recording = nil
276
+ live = @decimator
277
+ loaded = loaded_recording(input)
278
+ chunk = loaded ? input.int("rec.filter_chunk", @filter_chunk) : @filter_chunk
279
+ if live.nil?
280
+ @filter_chunk = chunk
281
+ @resumed_recording = loaded
282
+ elsif loaded && chunk == @filter_chunk && loaded.rate == live.rate && loaded.clock_hz == live.clock_hz
283
+ @decimator = loaded
284
+ else
285
+ @decimator = Decimator.new(clock_hz: live.clock_hz, rate: live.rate)
286
+ end
287
+ end
288
+
289
+ def loaded_recording(input)
290
+ rate = input.int("rec.rate", 0)
291
+ return unless rate.positive?
292
+
293
+ decimator = Decimator.new(clock_hz: input.int("rec.clock_hz", Region::PAL.clock_hz), rate:)
294
+ decimator.load_fields(input, "rec.")
295
+ decimator
296
+ end
297
+
298
+ # The window a loaded snapshot's recording had got to, when this
299
+ # recording is at its rate, clock and filter chunk.
300
+ def resumed_recording(rate, filter_chunk, clock_hz)
301
+ resumed = @resumed_recording
302
+ @resumed_recording = nil
303
+ return if resumed.nil? || filter_chunk != @filter_chunk
304
+
305
+ resumed if resumed.rate == rate && resumed.clock_hz == clock_hz
306
+ end
307
+
275
308
  def restore_voice(voice, base)
276
309
  control = register(base + 4)
277
310
  voice.waveform.restore_registers(register(base) | (register(base + 1) << 8),
@@ -70,6 +70,7 @@ module Badline
70
70
  @pending_cycles = 0
71
71
  @deferred_writes = []
72
72
  @decimator = nil
73
+ @resumed_recording = nil
73
74
  @samples = []
74
75
  @voice_samples = nil
75
76
  @filter_chunk = filter_chunk
@@ -120,12 +121,15 @@ module Badline
120
121
  # Starts collecting the output, averaged down to `rate` samples a
121
122
  # second, for #drain_samples. `filter_chunk` overrides the one the SID
122
123
  # was built with, for a machine that built its own. `clock_hz` is the
123
- # rate the SID's cycles are taken to run at.
124
+ # rate the SID's cycles are taken to run at. A SID loaded from a
125
+ # recording one carries on from the window that one had got to, at the
126
+ # same rate, clock and filter chunk.
124
127
  def record(rate:, filter_chunk: @filter_chunk, clock_hz: Region::PAL.clock_hz)
125
128
  synthesize!
126
129
  catch_up
130
+ resumed = resumed_recording(rate, filter_chunk, clock_hz)
127
131
  @filter_chunk = filter_chunk
128
- @decimator = Decimator.new(clock_hz:, rate:)
132
+ @decimator = resumed || Decimator.new(clock_hz:, rate:)
129
133
  @samples = []
130
134
  end
131
135
 
@@ -88,7 +88,8 @@ module Badline
88
88
  # The VIC-I's fields in a keyed snapshot module (Snapshot::Keyed): the
89
89
  # registers, the beam, the frame's length in lines and whether the
90
90
  # next interlaced field is the long one, the text window's counters
91
- # and the fetch in flight, the painter's lines under line.*, the picture, and the sound
91
+ # and the fetch in flight, the light pen's latch and line under pen.*,
92
+ # the painter's lines under line.*, the picture, and the sound
92
93
  # under sound.* What the registers decode to, where the line's fetches
93
94
  # end and the next event are worked out on load.
94
95
  module KeyedState
@@ -98,6 +99,7 @@ module Badline
98
99
  out.bool("window_open", @window_open).bool("window_done", @window_done).int("row_start", @row_start)
99
100
  out.int("matrix", @matrix).int("char_line", @char_line).int("text_rows", @text_rows)
100
101
  out.int("fetch_from", @fetch_from).int("fetched", @fetched).int("code", @code)
102
+ out.int("pen.x", @pen_x).int("pen.y", @pen_y).bool("pen.low", @pen_low)
101
103
  out.int("line.layout", LINE_LAYOUT)
102
104
  @painter.save_fields(out)
103
105
  @sound&.save_fields(out, "sound.")
@@ -119,6 +121,9 @@ module Badline
119
121
  @fetch_from = input.int("fetch_from", @fetch_from)
120
122
  @fetched = input.int("fetched", @fetched)
121
123
  @code = input.byte("code", @code)
124
+ @pen_x = input.byte("pen.x", @pen_x)
125
+ @pen_y = input.byte("pen.y", @pen_y)
126
+ @pen_low = input.bool("pen.low", @pen_low)
122
127
  line = input.int("line.layout", 0) == LINE_LAYOUT
123
128
  input.adjust("line in progress from another renderer, restarted") unless line || input.missing?
124
129
  @painter.load_fields(input, line)
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class VICI
6
+ # The light pen input, the line the VIC-20 shares with the joystick's
7
+ # fire button, and the position a falling edge on it latches into
8
+ # $9006 and $9007.
9
+ #
10
+ # $9006 reads 2h + 1, where h is the edge's column plus two, wrapping
11
+ # at the line's end, and $9007 bits 8-1 of the raster count in the
12
+ # cycle after the edge. Around the cycle where the count turns to 0
13
+ # the latch reads otherwise:
14
+ #
15
+ # - An edge from 8 cycles before that cycle to 1 after it latches as
16
+ # an edge in the cycle after it would, with $9007 at 0, but for an
17
+ # edge 2 cycles before it, which latches an even $9006, one less.
18
+ # - Before that, an edge latches nothing while the count read its
19
+ # last value of the frame in the cycle before the edge: line 311 on
20
+ # the 6561, and 261, past the last line, on the 6560.
21
+ module LightPen
22
+ # Where the window around the count's turn to 0 starts and ends,
23
+ # in cycles from that turn, and the edge in it that latches an even
24
+ # $9006.
25
+ WRAP_WINDOW = (-8..1)
26
+ WRAP_EVEN = -2
27
+
28
+ # The pen's line, low while the pen sees the beam or fire is held.
29
+ def lightpen_level(high)
30
+ low = !high
31
+ return if low == @pen_low
32
+
33
+ @pen_low = low
34
+ pen_fell if low
35
+ end
36
+
37
+ private
38
+
39
+ def power_on_pen!
40
+ @pen_x = @pen_y = 0
41
+ @pen_low = false
42
+ end
43
+
44
+ def pen_fell
45
+ line = @rasterline
46
+ column = @column
47
+ from_wrap = cycles_from_wrap(line, column)
48
+ if WRAP_WINDOW.cover?(from_wrap)
49
+ latch_pen(@frame_wrap + 1, 0, from_wrap == WRAP_EVEN)
50
+ elsif !pen_blocked?(line, column)
51
+ next_column = column + 1
52
+ if next_column == @cycles_per_line
53
+ next_column = 0
54
+ line = line == @frame_lines - 1 ? 0 : line + 1
55
+ end
56
+ latch_pen(column, count_at(line, next_column) >> 1, false)
57
+ end
58
+ end
59
+
60
+ # How many cycles the beam is past the cycle where the count turns
61
+ # to 0, negative before it. On the 6561 the count turns as the
62
+ # frame starts, so the frame's first cycles are past it.
63
+ def cycles_from_wrap(line, column)
64
+ position = (line * @cycles_per_line) + column
65
+ return position if position < 2 && @frame_wrap == @cycles_per_line
66
+
67
+ position - ((@frame_lines - 1) * @cycles_per_line) - @frame_wrap
68
+ end
69
+
70
+ # Whether the count read its last value of the frame in the cycle
71
+ # before this one.
72
+ def pen_blocked?(line, column)
73
+ column -= 1
74
+ if column.negative?
75
+ column = @cycles_per_line - 1
76
+ line = line.zero? ? @frame_lines - 1 : line - 1
77
+ end
78
+ last = @frame_wrap > @count_column ? @frame_lines : @frame_lines - 1
79
+ count_at(line, column) == last
80
+ end
81
+
82
+ def latch_pen(column, count, even)
83
+ @pen_x = (2 * ((column + 2) % @cycles_per_line)) + (even ? 0 : 1)
84
+ @pen_y = count
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "badline/chips/vic_i/painter"
4
4
  require "badline/chips/vic_i/text_window"
5
+ require "badline/chips/vic_i/light_pen"
5
6
  require "badline/chips/vic_i/sound"
6
7
  require "badline/chips/vic_i/keyed_parts"
7
8
 
@@ -16,7 +17,7 @@ module Badline
16
17
  # The registers repeat through $9000-$90FF. $9003 bit 7 and $9004 read
17
18
  # the raster count, its bit 0 and bits 8-1, which on the 6561 is the
18
19
  # raster line (see Profile for the 6560's). $9006 and $9007 read the
19
- # light pen, which nothing triggers yet, and $9008 and $9009 the two
20
+ # light pen's latch (LightPen), and $9008 and $9009 the two
20
21
  # pots, which read $FF with nothing plugged in. The other registers
21
22
  # read back what was written to them.
22
23
  #
@@ -45,6 +46,7 @@ module Badline
45
46
  # blanked.
46
47
  class VICI
47
48
  include TextWindow
49
+ include LightPen
48
50
 
49
51
  # What a VIC-I sets for the machine: the clock, the raster's cycles
50
52
  # per line and lines per frame, how many lines at the top of the frame
@@ -149,6 +151,7 @@ module Badline
149
151
  @fetched = @slot = 0
150
152
  @code = 0
151
153
  @painter.power_on!(@frame_lines - 1)
154
+ power_on_pen!
152
155
  schedule
153
156
  end
154
157
 
@@ -195,7 +198,8 @@ module Badline
195
198
  case register
196
199
  when 0x03 then ((raster_count & 0x01) << 7) | (@registers[3] & 0x7f)
197
200
  when 0x04 then raster_count >> 1
198
- when 0x06, 0x07 then 0
201
+ when 0x06 then @pen_x
202
+ when 0x07 then @pen_y
199
203
  when 0x08, 0x09 then 0xff
200
204
  else @registers[register]
201
205
  end
@@ -218,17 +222,18 @@ module Badline
218
222
  end
219
223
 
220
224
  # The raster count $9003 and $9004 read (see Profile).
221
- def raster_count
222
- line = @rasterline
223
- column = @column
225
+ def raster_count = count_at(@rasterline, @column)
226
+
227
+ private
228
+
229
+ # What the count reads at +column+ of +line+ in this frame.
230
+ def count_at(line, column)
224
231
  return line if column < @count_column
225
232
  return 0 if line == @frame_lines - 1 && column >= @frame_wrap
226
233
 
227
234
  line + 1
228
235
  end
229
236
 
230
- private
231
-
232
237
  def event(column)
233
238
  if column == @cycles_per_line
234
239
  column = @column = 0
@@ -13,12 +13,22 @@ module Badline
13
13
  # that sector's data block, whatever it holds.
14
14
  module SectorReader
15
15
  HEADER_BITS = 10 * 8
16
- DATA_BITS = 325 * 8
17
16
  SYNC = /1{10,}/
18
17
 
18
+ # A data block as far as its checksum: its ID, 256 bytes and the
19
+ # checksum. The DOS writes two more after them, and switches back to
20
+ # read mode a few bits into the last, so the gap that was there
21
+ # before finishes its last code.
22
+ CHECKED_BYTES = 258
23
+ CHECKED_BITS = CHECKED_BYTES * 10
24
+
25
+ # The codes of two $0 nibbles, which finish the last group of
26
+ # CHECKED_BITS.
27
+ PADDING = "01010" * 4
28
+
19
29
  # A sector as read: its decoded header, [ID, checksum, sector, track,
20
30
  # the second ID byte, the first, ...], and its decoded data block,
21
- # [ID, 256 data bytes, checksum, ...], nil when there isn't one that
31
+ # [ID, 256 data bytes, checksum], nil when there isn't one that
22
32
  # decodes.
23
33
  Sector = Struct.new(:header, :data)
24
34
 
@@ -62,10 +72,17 @@ module Badline
62
72
  next unless header && header[0] == Disk::HEADER_ID
63
73
 
64
74
  data_sync = SYNC.match(bits, pos + HEADER_BITS)
65
- yield header, data_sync && decode(bits, data_sync.end(0), DATA_BITS)
75
+ yield header, data_sync && decode_data(bits, data_sync.end(0))
66
76
  end
67
77
  end
68
78
 
79
+ def decode_data(bits, pos)
80
+ slice = bits[pos, CHECKED_BITS]
81
+ return unless slice.length == CHECKED_BITS
82
+
83
+ GCR.decode([slice + PADDING].pack("B*").bytes)&.first(CHECKED_BYTES)
84
+ end
85
+
69
86
  def decode(bits, pos, count)
70
87
  slice = bits[pos, count]
71
88
  return unless slice.length == count
@@ -39,6 +39,7 @@ module Badline
39
39
  @via2 = DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
40
40
  @bus = Bus.new(rom: rom || ROM.load("dos1541.rom", 0xc000), via1: @via1, via2: @via2)
41
41
  @cpu = CPU.new(@bus, debug:)
42
+ @cpu.power_on!
42
43
  @clock_hz = CLOCK_HZ
43
44
  @idle_loop = IDLE_LOOP
44
45
  @host_clock_hz = host_clock_hz
@@ -97,6 +97,7 @@ module Badline
97
97
  @host_tia = Chips::TIA.new
98
98
  @bus = Bus.new(rom: rom || ROM.load("dos1551.rom", 0xc000), port: @port, tia: @tia)
99
99
  @cpu = CPU.new(@bus, debug:)
100
+ @cpu.power_on!
100
101
  @idle_loop = IDLE_LOOP
101
102
  @host_clock_hz = host_clock_hz
102
103
  @phase = 0
@@ -68,6 +68,7 @@ module Badline
68
68
  @fdc = WD1770.new
69
69
  @bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
70
70
  @cpu = CPU.new(@bus, debug:)
71
+ @cpu.power_on!
71
72
  @fdc.cpu = @cpu
72
73
  @idle_loop = IDLE_LOOP
73
74
  @host_clock_hz = host_clock_hz
@@ -87,6 +87,7 @@ module Badline
87
87
  @cia = Chips::CIA.new(start: 0x4000, peripheral: @serial_port, tod_hz: 0)
88
88
  @bus = Bus.new(rom: rom || ROM.load("dos1581.rom", 0x8000), cia: @cia, fdc: @fdc, drive: self)
89
89
  @cpu = CPU.new(@bus, debug:)
90
+ @cpu.power_on!
90
91
  @idle_loop = IDLE_LOOP
91
92
  @host_clock_hz = host_clock_hz
92
93
  @phase = 0
@@ -52,6 +52,7 @@ module Badline
52
52
  @via2 = Model1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
53
53
  @bus = Bus.new(rom: rom || ROM.load("dos2031.rom", 0xc000), via1: @via1, via2: @via2)
54
54
  @cpu = CPU.new(@bus, debug:)
55
+ @cpu.power_on!
55
56
  @clock_hz = CLOCK_HZ
56
57
  @idle_loop = IDLE_LOOP
57
58
  @host_clock_hz = host_clock_hz
@@ -79,9 +79,10 @@ module Badline
79
79
  turn(1, drives.nobits?(0x10), drives & 0x03)
80
80
  end
81
81
 
82
- # Whether a cycle would change nothing: both motors off and no byte
83
- # ready under way.
84
- def quiet? = !@motors[0] && !@motors[1] && !@ca1_low
82
+ # Whether a cycle would change nothing: no byte ready under way, and
83
+ # each drive's motor off or turning with no disk in it, as both turn
84
+ # from the bump at boot until the run-down.
85
+ def quiet? = !@ca1_low && still?(0) && still?(1)
85
86
 
86
87
  # What the drives hold, for comparing them at two points.
87
88
  def idle_state = [*@half_tracks, *@phases, *@motors, *@syncs, @data_in]
@@ -98,6 +99,8 @@ module Badline
98
99
 
99
100
  def selected_drive = @rriot.port_b_output & 0x01
100
101
 
102
+ def still?(drive) = !@motors[drive] || @disks[drive].nil?
103
+
101
104
  def turn(drive, motor, phase)
102
105
  step(drive, phase) if phase != @phases[drive]
103
106
  motor_off(drive) if @motors[drive] && !motor
@@ -26,7 +26,11 @@ module Badline
26
26
  # At reset the controller waits for the common RAM's $0400 to read 0,
27
27
  # starts, and writes its timer's period there. The interface processor
28
28
  # tests its RAM and ROMs, has the controller reset its drives, and
29
- # bumps both heads to track 1.
29
+ # bumps both heads to track 1. The motors run from then until the
30
+ # run-down after the first job, about 9.5 million host cycles in for a
31
+ # directory LOAD, or 8.8 million with no job. A motor turning with no
32
+ # disk in its drive changes nothing, so a unit with no disks sleeps
33
+ # from the bump on, and one with a disk only after the run-down.
30
34
  class Model4040
31
35
  include Model8050::Board
32
36
  include Model8050::Idle
@@ -61,7 +65,9 @@ module Badline
61
65
  build_ip(dos_rom || ROM.load(dos_file, dos_start))
62
66
  build_fdc(fdc_rom || ROM.read(fdc_file))
63
67
  @ip_cpu = IPCPU.new(@ip_bus)
68
+ @ip_cpu.power_on!
64
69
  @fdc_cpu = FDCCPU.new(@fdc_bus)
70
+ @fdc_cpu.power_on!
65
71
  init_idle(ip_loop, fdc_loop)
66
72
  @ieee_bus.attach(self)
67
73
  end
@@ -128,6 +134,16 @@ module Badline
128
134
  @fdc_cpu.cycle!
129
135
  @cycles += 1
130
136
  end
137
+
138
+ # One cycle of the FDC's side alone, as step runs it (Model8050::Idle).
139
+ def fdc_step
140
+ @mechanism.cycle!
141
+ @via.cycle!
142
+ @rriot.cycle!
143
+ @fdc_cpu.irq = @rriot.irq?
144
+ @fdc_cpu.cycle!
145
+ @cycles += 1
146
+ end
131
147
  end
132
148
 
133
149
  # The 3040: the 4040's board with the first DOS 2.1, as xpet's 3040
@@ -68,6 +68,8 @@ module Badline
68
68
  private
69
69
 
70
70
  def run_cycle
71
+ return fdc_alone_cycle if @fdc_alone
72
+
71
73
  step
72
74
  watch_loops if @idle_skip
73
75
  end
@@ -15,7 +15,9 @@ module Badline
15
15
  # Drives::Engine::Core does: each host cycle adds 1 MHz of phase, and
16
16
  # every whole host period in it runs a cycle of both processors. While
17
17
  # both processors idle, it sleeps instead, and the readers of its
18
- # parts catch it up first.
18
+ # parts catch it up first. Asleep, a host cycle only counts towards
19
+ # the one planned to wake the unit, which its own doze wakes, and a
20
+ # line the controller moves wakes it at once (host_written!).
19
21
  module DOSBoard
20
22
  attr_reader :device, :ieee_bus
21
23
 
@@ -51,9 +53,6 @@ module Badline
51
53
  # (IEEEPort).
52
54
  def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
53
55
 
54
- # The controller pushed its lines into the bus.
55
- def host_written! = nil
56
-
57
56
  # Which LEDs are lit, as the unit's port drives them. They hold
58
57
  # still while the unit sleeps, so reading them leaves it asleep.
59
58
  def leds = @ieee_port.leds
@@ -62,16 +61,20 @@ module Badline
62
61
 
63
62
  # Runs the drive cycles that fall in one host cycle, then takes the
64
63
  # controller's lines for the next. Asleep, the unit owes the cycles
65
- # instead, and wakes once the controller has moved a line.
64
+ # instead, and wakes once the controller has moved a line, as it
65
+ # does with a processor owing cycles (dozing?).
66
66
  def host_cycle!
67
+ return if slept_through?
68
+
67
69
  phase = @phase + CLOCK_HZ
68
70
  while phase >= @host_clock_hz
69
71
  @asleep ? owe_cycle : run_cycle
70
72
  phase -= @host_clock_hz
71
73
  end
72
74
  @phase = phase
73
- settle! if @asleep && @ieee_port.lines_moved?
75
+ settle! if dozing? && @ieee_port.lines_moved?
74
76
  @ieee_port.latch_host
77
+ plan_sleep if @asleep
75
78
  end
76
79
 
77
80
  def cycle!
@@ -79,12 +82,52 @@ module Badline
79
82
  run_cycle
80
83
  end
81
84
 
85
+ # The controller pushed its lines into the bus. A line it moved
86
+ # wakes a unit that owes cycles, which saw the lines as they stood.
87
+ def host_written!
88
+ settle! if dozing? && @ieee_port.lines_moved?
89
+ end
90
+
82
91
  private
83
92
 
84
93
  def settled(part)
85
94
  settle!
86
95
  part
87
96
  end
97
+
98
+ # Asleep, a host cycle only counts towards the one planned to wake
99
+ # the unit (plan_sleep). Returns whether it did.
100
+ def slept_through?
101
+ return false unless @planned
102
+
103
+ @slept += 1
104
+ doze if @slept == @wake_at
105
+ true
106
+ end
107
+
108
+ # Plans the host cycle that brings the cycles owed up to the budget,
109
+ # once the unit has fallen asleep and the host cycle it fell asleep
110
+ # in is over.
111
+ def plan_sleep
112
+ return if @planned
113
+
114
+ @planned = true
115
+ @slept = 0
116
+ needed = ((@budget - @owed) * @host_clock_hz) - @phase
117
+ @wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
118
+ end
119
+
120
+ # Turns the host cycles slept into cycles owed, through the phase
121
+ # accumulator.
122
+ def owe_slept
123
+ return unless @planned
124
+
125
+ @planned = false
126
+ phase = @phase + (@slept * CLOCK_HZ)
127
+ @owed += phase / @host_clock_hz
128
+ @phase = phase % @host_clock_hz
129
+ @slept = 0
130
+ end
88
131
  end
89
132
  end
90
133
  end
@@ -31,6 +31,10 @@ module Badline
31
31
  # last cleared (Model8050::Idle).
32
32
  attr_accessor :volatile
33
33
 
34
+ # The bytes of the 6530's RAM read or written since it was last
35
+ # cleared, a bit for each pair of them (Model8050::Idle).
36
+ attr_accessor :ram_touched
37
+
34
38
  def initialize(common:, via:, rriot:)
35
39
  addressable_at(0, length: 2**16)
36
40
  @common = common
@@ -38,6 +42,7 @@ module Badline
38
42
  @rriot = rriot
39
43
  @data = 0
40
44
  @volatile = false
45
+ @ram_touched = 0
41
46
  @common_changes = 0
42
47
  end
43
48
 
@@ -78,6 +83,7 @@ module Badline
78
83
  end
79
84
  return peek_via(addr & 0x0f) if addr.anybits?(0x40)
80
85
 
86
+ @ram_touched |= 1 << ((addr & 0x3f) >> 1)
81
87
  @rriot.read_ram(addr)
82
88
  end
83
89
 
@@ -93,6 +99,7 @@ module Badline
93
99
  elsif addr.anybits?(0x40)
94
100
  @via.poke(0x40 | (addr & 0x0f), value)
95
101
  else
102
+ @ram_touched |= 1 << ((addr & 0x3f) >> 1)
96
103
  @rriot.write_ram(addr, value)
97
104
  end
98
105
  end