badline 0.8.0 → 0.10.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +60 -19
  5. data/doc/library.md +10 -3
  6. data/doc/media.md +11 -2
  7. data/doc/pinned-behaviour.md +199 -9
  8. data/doc/sid-player.md +8 -4
  9. data/doc/snapshots.md +79 -33
  10. data/lib/badline/address_bus/pla.rb +130 -0
  11. data/lib/badline/address_bus/roms.rb +32 -0
  12. data/lib/badline/address_bus.rb +13 -92
  13. data/lib/badline/audio/bare_player.rb +24 -0
  14. data/lib/badline/audio/checkpoints.rb +42 -0
  15. data/lib/badline/audio/jukebox.rb +11 -4
  16. data/lib/badline/audio/machine_player.rb +16 -0
  17. data/lib/badline/audio/renderer.rb +80 -9
  18. data/lib/badline/audio/stereo.rb +3 -5
  19. data/lib/badline/audio.rb +1 -0
  20. data/lib/badline/chrout_trap.rb +4 -3
  21. data/lib/badline/computer/attachments.rb +52 -12
  22. data/lib/badline/computer/saved_state.rb +1 -0
  23. data/lib/badline/computer.rb +50 -33
  24. data/lib/badline/control_ports.rb +10 -3
  25. data/lib/badline/core.rb +51 -0
  26. data/lib/badline/data_path.rb +37 -0
  27. data/lib/badline/datasette.rb +63 -10
  28. data/lib/badline/drive1541/disk.rb +3 -3
  29. data/lib/badline/drive1541/disk_state.rb +14 -5
  30. data/lib/badline/drive1541/serial_port.rb +5 -8
  31. data/lib/badline/drive1541/sleep.rb +5 -6
  32. data/lib/badline/drive1541.rb +3 -3
  33. data/lib/badline/frontend/app.rb +63 -64
  34. data/lib/badline/frontend/boot.rb +34 -4
  35. data/lib/badline/frontend/confirmation.rb +17 -11
  36. data/lib/badline/frontend/controls.rb +12 -6
  37. data/lib/badline/frontend/drive_led.rb +9 -10
  38. data/lib/badline/frontend/frame_rate.rb +4 -4
  39. data/lib/badline/frontend/machine_window.rb +87 -0
  40. data/lib/badline/frontend/menu_dialogs.rb +52 -14
  41. data/lib/badline/frontend/menu_media.rb +70 -16
  42. data/lib/badline/frontend/menu_pages.rb +47 -42
  43. data/lib/badline/frontend/menu_ports.rb +69 -0
  44. data/lib/badline/frontend/name_field.rb +88 -0
  45. data/lib/badline/frontend/pacer.rb +6 -5
  46. data/lib/badline/frontend/pause_menu.rb +52 -37
  47. data/lib/badline/frontend/player_screen.rb +1 -1
  48. data/lib/badline/frontend/player_window.rb +4 -4
  49. data/lib/badline/frontend/screen.rb +39 -37
  50. data/lib/badline/frontend/sid_view.rb +1 -1
  51. data/lib/badline/frontend/snapshots.rb +145 -17
  52. data/lib/badline/frontend/sound.rb +17 -9
  53. data/lib/badline/frontend/timeline.rb +36 -5
  54. data/lib/badline/frontend.rb +3 -0
  55. data/lib/badline/iec_bus.rb +16 -28
  56. data/lib/badline/kernal_trap/drive.rb +3 -0
  57. data/lib/badline/kernal_trap/file.rb +1 -1
  58. data/lib/badline/kernal_trap/layout.rb +112 -0
  59. data/lib/badline/kernal_trap/load.rb +24 -25
  60. data/lib/badline/kernal_trap/routine.rb +17 -5
  61. data/lib/badline/kernal_trap/save.rb +13 -23
  62. data/lib/badline/kernal_trap/serial.rb +38 -24
  63. data/lib/badline/kernal_trap.rb +1 -0
  64. data/lib/badline/keyboard.rb +17 -14
  65. data/lib/badline/machine.rb +60 -0
  66. data/lib/badline/media/disk_list.rb +75 -0
  67. data/lib/badline/media/disk_set.rb +13 -3
  68. data/lib/badline/media/vic20_basic.rb +54 -0
  69. data/lib/badline/media/vic20_cartridge.rb +74 -0
  70. data/lib/badline/media/vic20_media.rb +132 -0
  71. data/lib/badline/media.rb +39 -20
  72. data/lib/badline/model.rb +62 -0
  73. data/lib/badline/options/event.rb +10 -6
  74. data/lib/badline/options/help.rb +25 -13
  75. data/lib/badline/options/table.rb +15 -1
  76. data/lib/badline/options/validation.rb +63 -3
  77. data/lib/badline/options.rb +37 -34
  78. data/lib/badline/region.rb +16 -4
  79. data/lib/badline/reu/ram.rb +27 -0
  80. data/lib/badline/reu/saved_state.rb +27 -0
  81. data/lib/badline/roms/README +37 -0
  82. data/lib/badline/roms/kernal-sx64.rom +0 -0
  83. data/lib/badline/roms/vic20/basic.rom +0 -0
  84. data/lib/badline/roms/vic20/character.rom +0 -0
  85. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  86. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  87. data/lib/badline/sid/decimator.rb +13 -0
  88. data/lib/badline/sid/filter.rb +6 -3
  89. data/lib/badline/sid.rb +19 -0
  90. data/lib/badline/snapshot/container.rb +2 -0
  91. data/lib/badline/snapshot/fields.rb +1 -0
  92. data/lib/badline/snapshot/image.rb +15 -0
  93. data/lib/badline/snapshot/setup.rb +20 -8
  94. data/lib/badline/snapshot/state.rb +15 -7
  95. data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
  96. data/lib/badline/snapshot/vice/peripherals.rb +12 -2
  97. data/lib/badline/snapshot/vice/restore.rb +3 -1
  98. data/lib/badline/snapshot/vice/reu1764.rb +55 -0
  99. data/lib/badline/snapshot/vice/vicii.rb +59 -13
  100. data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
  101. data/lib/badline/snapshot/vice.rb +25 -23
  102. data/lib/badline/snapshot.rb +10 -0
  103. data/lib/badline/storage/crt_file.rb +16 -5
  104. data/lib/badline/storage/disk_image.rb +3 -0
  105. data/lib/badline/storage/host_directory.rb +3 -0
  106. data/lib/badline/storage/t64.rb +10 -4
  107. data/lib/badline/storage/tap.rb +44 -8
  108. data/lib/badline/storage/unavailable.rb +3 -1
  109. data/lib/badline/storage.rb +5 -5
  110. data/lib/badline/timing.rb +12 -0
  111. data/lib/badline/version.rb +1 -1
  112. data/lib/badline/via/shift_register.rb +43 -16
  113. data/lib/badline/vic/bank.rb +53 -16
  114. data/lib/badline/vic/registers.rb +20 -4
  115. data/lib/badline/vic/sprite/timing.rb +5 -9
  116. data/lib/badline/vic/sprite.rb +4 -3
  117. data/lib/badline/vic.rb +34 -15
  118. data/lib/badline/vic20/attachments.rb +103 -0
  119. data/lib/badline/vic20/bus.rb +308 -0
  120. data/lib/badline/vic20/cpu.rb +10 -0
  121. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  122. data/lib/badline/vic20/port_wiring.rb +50 -0
  123. data/lib/badline/vic20/saved_state.rb +152 -0
  124. data/lib/badline/vic20/sound/output.rb +83 -0
  125. data/lib/badline/vic20/sound.rb +233 -0
  126. data/lib/badline/vic20/user_via_ports.rb +47 -0
  127. data/lib/badline/vic20/vic/painter.rb +239 -0
  128. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  129. data/lib/badline/vic20/vic/text_window.rb +71 -0
  130. data/lib/badline/vic20/vic.rb +267 -0
  131. data/lib/badline/vic20.rb +220 -0
  132. data/lib/badline.rb +2 -38
  133. metadata +37 -2
  134. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -27,8 +27,12 @@ module Badline
27
27
  path
28
28
  end
29
29
 
30
+ # A VIC-20's snapshot holds the BADLINE module alone, in a container
31
+ # named as xvic names its machine.
30
32
  def dump(computer)
31
33
  state = computer.snapshot
34
+ return Container.new([MachineState.section(state)], machine: Container::VIC20).to_s if computer.family == :vic20
35
+
32
36
  Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
33
37
  end
34
38
 
@@ -49,4 +53,10 @@ module Badline
49
53
 
50
54
  def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
51
55
  end
56
+
57
+ class Vic20
58
+ def save_snapshot(path) = Snapshot.save(self, path)
59
+
60
+ def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
61
+ end
52
62
  end
@@ -6,13 +6,14 @@ module Badline
6
6
  class FormatError < StandardError; end
7
7
 
8
8
  SIGNATURE = "C64 CARTRIDGE ".b
9
+ VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
9
10
  CHIP_SIGNATURE = "CHIP".b
10
11
  CHIP_HEADER_SIZE = 0x10
11
12
  RAM_CHIP = 1
12
13
 
13
14
  Chip = Data.define(:chip_type, :bank, :address, :data)
14
15
 
15
- attr_reader :hardware_type, :subtype, :exrom, :game, :name, :chips
16
+ attr_reader :machine, :hardware_type, :subtype, :exrom, :game, :name, :chips
16
17
 
17
18
  # What .write needs of an image: the fields a parsed CRTFile reads.
18
19
  Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
@@ -33,16 +34,19 @@ module Badline
33
34
  header + packets.join
34
35
  end
35
36
 
36
- # `bytes` stands in for the file's contents.
37
- def initialize(path = nil, bytes: nil)
37
+ # `bytes` stands in for the file's contents. `machine` is the one the
38
+ # image has to be for, :c64 or :vic20, or nil for either.
39
+ def initialize(path = nil, bytes: nil, machine: :c64)
38
40
  parse(bytes || File.binread(path))
41
+ return if machine.nil? || @machine == machine
42
+
43
+ raise FormatError, "Cartridge image is for the #{@machine}, not the #{machine}"
39
44
  end
40
45
 
41
46
  private
42
47
 
43
48
  def parse(bytes)
44
- raise FormatError, "Missing CRT signature" unless bytes.start_with?(SIGNATURE)
45
-
49
+ @machine = signed_machine(bytes)
46
50
  @hardware_type = bytes[0x16, 2].unpack1("n")
47
51
  @exrom = bytes.getbyte(0x18)
48
52
  @game = bytes.getbyte(0x19)
@@ -51,6 +55,13 @@ module Badline
51
55
  @chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
52
56
  end
53
57
 
58
+ def signed_machine(bytes)
59
+ return :c64 if bytes.start_with?(SIGNATURE)
60
+ return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
61
+
62
+ raise FormatError, "Missing CRT signature"
63
+ end
64
+
54
65
  # Trailing bytes too short to hold a CHIP header are ignored.
55
66
  def parse_chips(bytes, offset)
56
67
  chips = []
@@ -36,6 +36,9 @@ module Badline
36
36
  @errors = split_error_table
37
37
  end
38
38
 
39
+ # The host file's path, as it was opened.
40
+ attr_reader :path
41
+
39
42
  # Whether the image was opened write-protected.
40
43
  def read_only? = @read_only
41
44
 
@@ -10,6 +10,9 @@ module Badline
10
10
  NO_SYNC = 21
11
11
  ID = "DIR".bytes.freeze
12
12
 
13
+ # The directory's path, as it was opened.
14
+ attr_reader :path
15
+
13
16
  def initialize(path)
14
17
  @path = path
15
18
  end
@@ -12,15 +12,21 @@ module Badline
12
12
  TAPE_NAME = 0x28
13
13
  ID = "T64".bytes.freeze
14
14
 
15
- def initialize(path)
15
+ # `bytes` stands in for the host file's contents, as a snapshot
16
+ # restores the archive it holds.
17
+ def initialize(path, bytes: nil)
16
18
  @path = path
17
- @bytes = File.binread(path).bytes
19
+ @bytes = bytes || File.binread(path).bytes
18
20
  raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
19
21
  end
20
22
 
21
- # What Storage.reopen needs to open the archive again.
23
+ # The host file's path, as it was opened.
24
+ attr_reader :path
25
+
26
+ # What Storage.reopen needs to open the archive again: its path and
27
+ # its bytes.
22
28
  def save_setup(out)
23
- out.int(T64_ARCHIVE).string(::File.expand_path(@path))
29
+ out.int(T64_ARCHIVE).string(::File.expand_path(@path)).blob(@bytes)
24
30
  end
25
31
 
26
32
  def read_file(name, **)
@@ -3,9 +3,9 @@
3
3
  module Badline
4
4
  module Storage
5
5
  # Raw datasette pulse stream. The header holds the signature, the
6
- # version at $0c and the data size at $10; the data runs from $14, every
7
- # byte the time to the next falling edge on the tape read line, in units
8
- # of 8 cycles.
6
+ # version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
7
+ # data size at $10; the data runs from $14, every byte the time to the
8
+ # next falling edge on the tape read line, in units of 8 cycles.
9
9
  class TAP
10
10
  class FormatError < StandardError; end
11
11
 
@@ -13,18 +13,28 @@ module Badline
13
13
  HEADER_SIZE = 0x14
14
14
  OVERFLOW = 0x100 * 8
15
15
 
16
- # The file it was read from, expanded, and the offset of the next
17
- # pulse byte.
18
- attr_reader :version, :path
16
+ # The file it was read from, expanded, and the file's bytes.
17
+ attr_reader :version, :platform, :path, :bytes
19
18
 
20
- def initialize(path)
19
+ # A blank version 1 tape for `platform` (0 C64, 1 VIC-20) at `path`,
20
+ # written to the host file.
21
+ def self.create(path, platform:)
22
+ File.binwrite(path, (SIGNATURE + [1, platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
23
+ new(path)
24
+ end
25
+
26
+ # `bytes` stands in for the host file's contents, as a snapshot
27
+ # restores the tape it holds.
28
+ def initialize(path, bytes: nil)
21
29
  @path = File.expand_path(path)
22
- @bytes = File.binread(path).bytes
30
+ @bytes = bytes || File.binread(path).bytes
23
31
  raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
24
32
 
25
33
  @version = @bytes[0x0c].to_i
26
34
  raise FormatError, "Unsupported TAP version #{@version}" if @version > 1
27
35
 
36
+ @platform = @bytes[0x0d].to_i
37
+
28
38
  @end = tape_end
29
39
  rewind
30
40
  end
@@ -33,6 +43,7 @@ module Badline
33
43
  @pos = HEADER_SIZE
34
44
  end
35
45
 
46
+ # The offset of the next pulse byte.
36
47
  def position = @pos
37
48
 
38
49
  def position=(offset)
@@ -41,6 +52,31 @@ module Badline
41
52
 
42
53
  def end? = @pos >= @end
43
54
 
55
+ # Records a pulse `cycles` long at the position, in place of the rest
56
+ # of the tape: a byte of units of 8 cycles, or a zero and the 24-bit
57
+ # cycle count for a long one (a lone zero, an overflow, on a version 0
58
+ # tape).
59
+ def record_pulse(cycles)
60
+ units = (cycles + 4) / 8
61
+ encoded = if units.between?(1, 0xff) then [units]
62
+ elsif version.zero? then [0]
63
+ else
64
+ count = [cycles, 0xffffff].min
65
+ [0, count & 0xff, (count >> 8) & 0xff, count >> 16]
66
+ end
67
+ @bytes.slice!(@pos..)
68
+ @bytes.concat(encoded)
69
+ @pos = @end = @bytes.length
70
+ end
71
+
72
+ # Writes the tape back to its host file, with the data size the
73
+ # header states.
74
+ def save
75
+ size = @bytes.length - HEADER_SIZE
76
+ @bytes[0x10, 4] = [size & 0xff, (size >> 8) & 0xff, (size >> 16) & 0xff, size >> 24]
77
+ File.binwrite(@path, @bytes.pack("C*"))
78
+ end
79
+
44
80
  # Cycles until the next falling edge, or nil once the tape has run out.
45
81
  def next_pulse
46
82
  return if end?
@@ -2,10 +2,12 @@
2
2
 
3
3
  module Badline
4
4
  module Storage
5
- # Stands in for a directory or archive a detached restore leaves out
5
+ # Stands in for a directory a detached restore leaves out
6
6
  # (Snapshot::StateReader): it finds no files and refuses every write
7
7
  # as a write-protected disk does.
8
8
  class Unavailable
9
+ def path = ""
10
+
9
11
  def read_file(_name, **) = nil
10
12
 
11
13
  def write_file(_name, _bytes, **)
@@ -45,16 +45,16 @@ module Badline
45
45
  T64_ARCHIVE = 4
46
46
 
47
47
  class << self
48
- # Opens the storage a save_setup wrote: the same directory or archive
49
- # by its path, or a disk image with the bytes it held, writes and
50
- # all. A detached reader gets the image write-protected, and
51
- # Unavailable for a directory or archive.
48
+ # Opens the storage a save_setup wrote: the same directory by its
49
+ # path, an archive with the bytes it held, or a disk image with the
50
+ # bytes it held, writes and all. A detached reader gets the image
51
+ # write-protected, and Unavailable for a directory.
52
52
  def reopen(input)
53
53
  kind = input.int
54
54
  path = input.string
55
55
  case kind
56
56
  when HOST_DIRECTORY then input.detached? ? Unavailable.new : HostDirectory.new(path)
57
- when T64_ARCHIVE then input.detached? ? Unavailable.new : T64.new(path)
57
+ when T64_ARCHIVE then T64.new(path, bytes: input.blob)
58
58
  else reopen_image(kind, path, input)
59
59
  end
60
60
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # = Timing
5
+ #
6
+ # What a front end needs to run a machine's frames and show them: the CPU
7
+ # clock, the raster's cycles per line and lines per frame, and the
8
+ # [left, top, width, height] of the raster it shows, and how many of the
9
+ # window's square pixels wide each of its pixels shows. Each machine
10
+ # derives it from its region.
11
+ Timing = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :crop, :pixel_width)
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.8.0"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -13,14 +13,20 @@ module Badline
13
13
  # modes 3 and 7 off whatever drives CB1 from outside.
14
14
  #
15
15
  # Under its own clock the register drives CB1, idling high. Reading or
16
- # writing the data register starts a byte: CB1 falls on the first clock
17
- # tick after the access and rises on the second, and so on, so a byte is
18
- # eight pulses over sixteen ticks. The interrupt flag is set on the tick
19
- # that raises CB1 over the eighth bit, and CB1 then stays high until the
20
- # next access. A tick is a φ2 cycle in modes 2 and 6, which makes a byte
21
- # sixteen cycles, and a timer 2 low byte underflow in modes 1, 4 and 5,
22
- # which makes it sixteen underflows. Mode 4 never stops and never
23
- # interrupts: its counter is disabled, and the byte goes round and round.
16
+ # writing the data register starts a byte, but only while the register
17
+ # is idle and enabled: an access in mode 0, or one before the eighth bit
18
+ # is in, leaves the count alone. CB1 falls on the first clock tick of
19
+ # the byte and rises on the second, and so on, so a byte is eight pulses
20
+ # over sixteen ticks. The interrupt flag is set on the tick that raises
21
+ # CB1 over the eighth bit, and CB1 then stays high until the next
22
+ # access.
23
+ #
24
+ # In modes 2 and 6 a tick is a φ2 cycle, from the second cycle after
25
+ # the access on, which makes a byte sixteen cycles. In modes 1, 4 and 5
26
+ # it comes two cycles after each timer 2 low byte underflow, on the
27
+ # cycle after the low byte reloads, which makes a byte sixteen
28
+ # underflows. Mode 4 never stops and never interrupts: its counter is
29
+ # disabled, and the byte goes round and round.
24
30
  #
25
31
  # Under an external clock the register shifts on every CB1 edge, access
26
32
  # or not. The access only arms the counter: the flag is set on the eighth
@@ -43,6 +49,10 @@ module Badline
43
49
  @data = 0x00
44
50
  # Bits still to count before the flag, zero when the counter is idle.
45
51
  @bits = 0
52
+ # The clock's last two cycles, newest in bit 0: timer 2 low byte
53
+ # underflows in modes 1, 4 and 5, and whether the byte has started
54
+ # in modes 2 and 6.
55
+ @delay = 0
46
56
  @clock = true
47
57
  @cb2 = true
48
58
  # Nothing drives CB2 from outside, so it floats high.
@@ -57,10 +67,13 @@ module Badline
57
67
  @data = value & 0xff
58
68
  end
59
69
 
60
- # Every read or write of the data register (re)starts the count of
61
- # eight bits.
70
+ # A read or write of the data register, which starts a byte if the
71
+ # register is idle and enabled.
62
72
  def access!
73
+ return if @mode == DISABLED || @bits.nonzero?
74
+
63
75
  @bits = 8
76
+ @delay = 0 if @mode == IN_PHI2 || @mode == OUT_PHI2
64
77
  end
65
78
 
66
79
  # Whether the register is counting bits off its own clock, which
@@ -68,20 +81,21 @@ module Badline
68
81
  def clocking? = @bits.nonzero? && internal_clock?
69
82
 
70
83
  def save_state(out)
71
- out.int(@mode).int(@data).int(@bits).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
84
+ out.int(@mode).int(@data).int(@bits).int(@delay).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
72
85
  end
73
86
 
74
87
  def load_state(input)
75
88
  @mode = input.int
76
89
  @data = input.int
77
90
  @bits = input.int
91
+ @delay = input.schema > 4 ? input.int : 0
78
92
  @clock = input.boolean?
79
93
  @cb2 = input.boolean?
80
94
  @cb2_input = input.boolean?
81
95
  end
82
96
 
83
97
  # Everything the register holds, for comparing it at two points.
84
- def state = [@mode, @data, @bits, @clock, @cb2, @cb2_input]
98
+ def state = [@mode, @data, @bits, @delay, @clock, @cb2, @cb2_input]
85
99
 
86
100
  # The CB1 level the register drives, or nil when CB1 is an input.
87
101
  def cb1_output
@@ -96,11 +110,9 @@ module Badline
96
110
  # Clocked once per φ2 cycle, with whether timer 2's low byte underflowed
97
111
  # on it. Returns true on the cycle that sets the interrupt flag.
98
112
  def cycle!(t2_low_underflow)
99
- return false if @bits.zero?
100
-
101
113
  case @mode
102
- when IN_PHI2, OUT_PHI2 then tick
103
- when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_low_underflow && tick
114
+ when IN_PHI2, OUT_PHI2 then @bits.nonzero? && phi2_tick
115
+ when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_tick(t2_low_underflow)
104
116
  else false
105
117
  end
106
118
  end
@@ -123,6 +135,21 @@ module Badline
123
135
  @mode != DISABLED && @mode != IN_EXTERNAL && @mode != OUT_EXTERNAL
124
136
  end
125
137
 
138
+ # A φ2 cycle of a byte: every one but the first ticks.
139
+ def phi2_tick
140
+ started = @delay.nonzero?
141
+ @delay = 1
142
+ started && tick
143
+ end
144
+
145
+ # Ticks on the underflow from two cycles back, while a byte is under
146
+ # way.
147
+ def t2_tick(underflow)
148
+ due = @delay.anybits?(2)
149
+ @delay = ((@delay << 1) & 2) | (underflow ? 1 : 0)
150
+ due && @bits.nonzero? && tick
151
+ end
152
+
126
153
  # One half period of the register's own clock: a falling edge puts a
127
154
  # bit out, and a rising one takes a bit in and counts it.
128
155
  def tick
@@ -5,44 +5,81 @@ module Badline
5
5
  # The 16K window the VIC sees into RAM, selected by CIA2 $DD00, in the
6
6
  # bank a +256K picks for it. Character
7
7
  # ROM shadows $1000-$1FFF in banks 0 and 2.
8
+ #
9
+ # The machine's bus pushes in what the VIC sees through #connect,
10
+ # #map_character_rom and #map whenever it changes. Until then the bank
11
+ # has a CIA 2, RAM, colour RAM and character ROM of its own.
8
12
  class Bank
9
13
  include Addressable
10
14
 
11
15
  # The CIA2 port A bank bits are inverted: %11 selects bank 0.
12
16
  BANK_STARTS = [0xc000, 0x8000, 0x4000, 0x0000].freeze
13
17
 
14
- attr_reader :address_bus
18
+ attr_reader :ram, :color_ram
15
19
 
16
20
  # The bank lines as #sample_lines last saw them.
17
21
  attr_accessor :lines
18
22
 
19
- def initialize(address_bus = nil)
23
+ def initialize(vic)
20
24
  addressable_at(0x0000, length: 2**14)
21
- @address_bus = address_bus || AddressBus.new
25
+ connect(cia2: CIA.new(start: 0xdd00), color_ram: ColorMemory.new(vic))
26
+ map_character_rom(ROM.load("character.rom", 0xd000))
27
+ @base = 0
28
+ @starts = BANK_STARTS
29
+ map(Memory.new([], length: 2**16, start: 0))
22
30
  @lines = 0b11
23
31
  @swapped = false
24
32
  end
25
33
 
34
+ # CIA 2, whose port A picks the bank, and the colour RAM, read from
35
+ # the address +color_base+ gives offset 0.
36
+ def connect(cia2:, color_ram:, color_base: 0xd800)
37
+ @cia2 = cia2
38
+ @color_ram = color_ram
39
+ @color_base = color_base
40
+ end
41
+
42
+ # The character ROM, read from the address +base+ gives offset 0. It
43
+ # shows in the banks whose low line is high, unless +shadow+ is false.
44
+ def map_character_rom(rom, base: 0xc000, shadow: true)
45
+ @character_rom = rom
46
+ @character_base = base
47
+ @shadow_mask = shadow ? 0b01 : 0b00
48
+ end
49
+
50
+ # The RAM the VIC's 64K starts at +base+ in, and the cartridge's
51
+ # Ultimax lines in each half of the cycle with the ROMH bank they
52
+ # show.
53
+ def map(ram, base: 0, phi1_ultimax: false, ultimax: false, romh: nil)
54
+ @ram = ram
55
+ unless base == @base
56
+ @base = base
57
+ @starts = BANK_STARTS.map { |start| base + start }.freeze
58
+ end
59
+ @phi1_ultimax = phi1_ultimax
60
+ @ultimax = ultimax
61
+ @romh = romh
62
+ end
63
+
26
64
  # On the 8565, a port A write that swaps the two bank lines, driving
27
65
  # one high as the other goes low, shows the VIC both lines low for the
28
66
  # cycle after it: every access in that cycle reads bank 3. A swap that
29
67
  # the DDR makes, releasing a line to float high, shows nothing.
30
68
  # Called once a cycle, ahead of the cycle's accesses.
31
69
  def sample_lines
32
- cia2 = @address_bus.cia2
33
- lines = cia2.port_a_lines & 0b11
34
- @swapped = swap?(@lines, lines) && cia2.port_registers[2].allbits?(0b11)
70
+ lines = @cia2.port_a_lines & 0b11
71
+ @swapped = swap?(@lines, lines) && @cia2.port_registers[2].allbits?(0b11)
35
72
  @lines = lines
36
73
  end
37
74
 
38
75
  def peek(offset)
39
- return ultimax_peek(offset) if @address_bus.phi1_ultimax
76
+ return ultimax_peek(offset) if @phi1_ultimax
40
77
 
41
78
  bits = bank_switch_register
42
- if bits.allbits?(0b01) && (offset & 0xf000) == 0x1000
43
- @address_bus.character_rom.peek(0xc000 + offset)
79
+ if (bits & @shadow_mask) == 0b01 && (offset & 0xf000) == 0x1000
80
+ @character_rom.peek(@character_base + offset)
44
81
  else
45
- @address_bus.video_ram.peek(BANK_STARTS[bits] + offset)
82
+ @ram.peek(@starts[bits] + offset)
46
83
  end
47
84
  end
48
85
 
@@ -50,11 +87,11 @@ module Badline
50
87
  # memory configuration the CPU does in that half. No cartridge drives
51
88
  # Ultimax mode in the first half alone, so otherwise it reads as #peek.
52
89
  def peek_phi2(offset)
53
- @address_bus.ultimax ? ultimax_peek(offset) : peek(offset)
90
+ @ultimax ? ultimax_peek(offset) : peek(offset)
54
91
  end
55
92
 
56
93
  def peek_color(offset)
57
- address_bus.color_ram.nibble(0xd800 + offset)
94
+ @color_ram.nibble(@color_base + offset)
58
95
  end
59
96
 
60
97
  def poke(_addr, _value)
@@ -63,7 +100,7 @@ module Badline
63
100
 
64
101
  # True if the VIC reads the character ROM at this offset.
65
102
  def character_rom?(offset)
66
- !@address_bus.phi1_ultimax && bank_switch_register.allbits?(0b01) && (offset & 0xf000) == 0x1000
103
+ !@phi1_ultimax && (bank_switch_register & @shadow_mask) == 0b01 && (offset & 0xf000) == 0x1000
67
104
  end
68
105
 
69
106
  def start
@@ -75,11 +112,11 @@ module Badline
75
112
  # In Ultimax mode the cartridge ROMH replaces the character ROM
76
113
  # shadow, visible at $3000-$3FFF of the window.
77
114
  def ultimax_peek(offset)
78
- romh = @address_bus.cartridge.romh
115
+ romh = @romh
79
116
  if romh && offset.allbits?(0x3000)
80
117
  romh.peek(0xe000 + (offset & 0x1fff))
81
118
  else
82
- @address_bus.video_ram.peek(BANK_STARTS[bank_switch_register] + offset)
119
+ @ram.peek(@starts[bank_switch_register] + offset)
83
120
  end
84
121
  end
85
122
 
@@ -88,7 +125,7 @@ module Badline
88
125
  def bank_switch_register
89
126
  return 0 if @swapped
90
127
 
91
- @address_bus.cia2.port_a_lines & 0b11
128
+ @cia2.port_a_lines & 0b11
92
129
  end
93
130
  end
94
131
  end
@@ -69,7 +69,13 @@ module Badline
69
69
  # $D02D: Sprite 6 color
70
70
  # $D02E: Sprite 7 color
71
71
  #
72
- # $D02F-$D03F: Not in use, always reads 0xff
72
+ # == VIC-IIe (8566 and 8564)
73
+ #
74
+ # $D02F: Extra keyboard lines K0-K2 in bits 0-2, bits 3-7 read 1
75
+ # $D030: FAST (2 MHz) in bit 0 and TEST in bit 1, bits 2-7 read 1
76
+ #
77
+ # $D02F-$D03F: Not in use on the 6569 and 8565, always reads 0xff,
78
+ # and $D031-$D03F on the VIC-IIe
73
79
  # $D040-$D3FF: Repeat $D0000 to $D03F every 64 bytes
74
80
  class Registers
75
81
  include IntegerHelper
@@ -77,10 +83,12 @@ module Badline
77
83
  # The raw register bytes, for the per-column reads on the hot path.
78
84
  attr_reader :bytes
79
85
 
80
- # Every register powers on as zero, as in VICE.
81
- def initialize
86
+ # Every register powers on as zero, as in VICE. A VIC-IIe reads
87
+ # $D02F and $D030 back as registers.
88
+ def initialize(iie: false)
82
89
  @bytes = Array.new(2**6, 0)
83
90
  @irq_line = false
91
+ @iie = iie
84
92
  end
85
93
 
86
94
  # True while any enabled latch bit is set in $D019/$D01A. Cached and
@@ -95,7 +103,9 @@ module Badline
95
103
  when 0x18 then @bytes[reg] | 0x01 # bit 0 unused, reads 1
96
104
  when 0x1a, 0x20..0x2e then @bytes[reg] | 0xf0
97
105
  when 0x1e, 0x1f then read_clear(reg) # collision registers clear on read
98
- when 0x2f..0x3f then 0xff
106
+ when 0x2f then @iie ? @bytes[reg] | 0xf8 : 0xff
107
+ when 0x30 then @iie ? @bytes[reg] | 0xfc : 0xff
108
+ when 0x31..0x3f then 0xff
99
109
  else @bytes[reg]
100
110
  end
101
111
  end
@@ -134,6 +144,12 @@ module Badline
134
144
  def border = @bytes[0x20] & 0x0f
135
145
  def background(index = 0) = @bytes[0x21 + index] & 0x0f
136
146
 
147
+ # The VIC-IIe's $D02F and $D030 bits. The 6569 and 8565 drive no
148
+ # extra keyboard lines and run at 1 MHz.
149
+ def extra_keyboard_lines = @iie ? @bytes[0x2f] & 0x07 : 0x07
150
+ def fast? = @iie && @bytes[0x30].anybits?(0x01)
151
+ def test? = @iie && @bytes[0x30].anybits?(0x02)
152
+
137
153
  def raster_target = uint16(@bytes[0x12], (@bytes[0x11] & 0x80) >> 7)
138
154
 
139
155
  def latch_irq!(bit)
@@ -11,10 +11,9 @@ module Badline
11
11
  # fetches or the compare run a cycle later than on the 6569, what
12
12
  # hangs off them moves 8 pixels with them.
13
13
  class Timing
14
- # The X counter's value where it holds on a raster of more than 512
15
- # pixels, and the four values it repeats from there.
14
+ # The X counter's value where it steps back on a raster of more than
15
+ # 512 pixels, to run over the eight values below it again.
16
16
  HOLD_AT = 0x188
17
- HOLD_BASE = 0x184
18
17
 
19
18
  attr_reader :columns, :sprite_cycle, :display_off_x, :x_pixels
20
19
 
@@ -36,14 +35,11 @@ module Badline
36
35
  def reload_x(index) = RELOAD_X + (8 * @shift) + (RELOAD_STEP * index)
37
36
 
38
37
  # The X counter at a raster pixel: X_OFFSET behind it, wrapping at
39
- # the end of the line, less the pixels it held for past HOLD_AT,
40
- # where it ran over $184-$187 again instead.
38
+ # the end of the line, less the pixels it held for from HOLD_AT on,
39
+ # where it ran over $180-$187 a second time.
41
40
  def self.xpos(pixel, width, hold)
42
41
  count = (pixel - X_OFFSET) % width
43
- return count if count < HOLD_AT
44
- return count - hold if count >= HOLD_AT + hold
45
-
46
- HOLD_BASE + ((count - HOLD_AT) % 4)
42
+ count < HOLD_AT ? count : count - hold
47
43
  end
48
44
 
49
45
  # The raster pixels at which the X counter reads each coordinate, 512
@@ -37,8 +37,9 @@ module Badline
37
37
  # The s-accesses reload the shift register partway through the line,
38
38
  # at raster pixel 459 for sprite 0 and 16 later for each sprite after
39
39
  # it. A sprite still shifting there loses the rest of its row, and the
40
- # comparator is ignored for the twelve pixels from it (the spritescan
41
- # dump). Its output holds the last pixel for seven, through K+6
40
+ # comparator is ignored for the twelve pixels from it, running on into
41
+ # the next line from a reload near the end of this one (the spritescan
42
+ # dumps). Its output holds the last pixel for seven, through K+6
42
43
  # (spritefetchbug).
43
44
  RELOAD_X = 459
44
45
  RELOAD_STEP = 16
@@ -275,7 +276,7 @@ module Badline
275
276
  end
276
277
 
277
278
  def sequence_hit(log, start, reload_bits)
278
- return if (@stop_x && start >= @stop_x) || start < @show_from
279
+ return if (@stop_x && start >= @stop_x) || start < @show_from || start < @reload_x + RELOAD_DEAD - @width
279
280
 
280
281
  if start < @reload_x
281
282
  sequence_early(log, start)