badline 0.2.1 → 0.4.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +107 -0
  5. data/CLAUDE.md +51 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +119 -51
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +293 -39
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +28 -14
  12. data/lib/badline/addressable.rb +1 -1
  13. data/lib/badline/audio/aiff.rb +8 -3
  14. data/lib/badline/audio/bare_player.rb +20 -3
  15. data/lib/badline/audio/cli.rb +17 -12
  16. data/lib/badline/audio/console.rb +12 -58
  17. data/lib/badline/audio/machine_player.rb +9 -6
  18. data/lib/badline/audio/playback.rb +3 -0
  19. data/lib/badline/audio/renderer.rb +3 -3
  20. data/lib/badline/audio/sdl_sink.rb +20 -52
  21. data/lib/badline/audio/stream.rb +117 -0
  22. data/lib/badline/audio/terminal.rb +90 -0
  23. data/lib/badline/audio.rb +3 -2
  24. data/lib/badline/cartridge/easy_flash.rb +6 -2
  25. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  26. data/lib/badline/cartridge/geo_ram.rb +63 -0
  27. data/lib/badline/cartridge/mach5.rb +1 -1
  28. data/lib/badline/cartridge/magic_desk.rb +5 -1
  29. data/lib/badline/cartridge/ocean.rb +5 -2
  30. data/lib/badline/cartridge/rex_utility.rb +1 -1
  31. data/lib/badline/cartridge/rgcd.rb +1 -1
  32. data/lib/badline/cartridge/simons_basic.rb +2 -2
  33. data/lib/badline/cartridge/westermann.rb +2 -2
  34. data/lib/badline/cartridge.rb +9 -0
  35. data/lib/badline/checkpoint.rb +98 -0
  36. data/lib/badline/cia/interrupt_register.rb +34 -10
  37. data/lib/badline/cia.rb +19 -10
  38. data/lib/badline/color_memory.rb +2 -2
  39. data/lib/badline/computer.rb +34 -14
  40. data/lib/badline/cpu.rb +2 -2
  41. data/lib/badline/gui/application.rb +70 -26
  42. data/lib/badline/gui/gamepads.rb +22 -22
  43. data/lib/badline/gui/joy_map.rb +1 -1
  44. data/lib/badline/gui/key_map.rb +6 -1
  45. data/lib/badline/gui/pane.rb +7 -6
  46. data/lib/badline/gui/screen_pane.rb +9 -13
  47. data/lib/badline/gui/window.rb +35 -20
  48. data/lib/badline/gui.rb +1 -1
  49. data/lib/badline/input/mouse1351.rb +21 -6
  50. data/lib/badline/input/paddles.rb +11 -6
  51. data/lib/badline/kernal_trap/channel.rb +102 -7
  52. data/lib/badline/kernal_trap/drive/block_commands.rb +108 -0
  53. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  54. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  55. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  56. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  57. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  58. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  59. data/lib/badline/kernal_trap/drive.rb +109 -131
  60. data/lib/badline/kernal_trap/file.rb +6 -14
  61. data/lib/badline/kernal_trap/load.rb +6 -0
  62. data/lib/badline/kernal_trap/save.rb +13 -7
  63. data/lib/badline/keyboard.rb +19 -3
  64. data/lib/badline/media.rb +44 -13
  65. data/lib/badline/memory.rb +2 -2
  66. data/lib/badline/options.rb +209 -0
  67. data/lib/badline/ram_expansion/banking.rb +30 -0
  68. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  71. data/lib/badline/ram_expansion.rb +21 -0
  72. data/lib/badline/region.rb +51 -0
  73. data/lib/badline/rom.rb +14 -5
  74. data/lib/badline/sdl/events.rb +47 -0
  75. data/lib/badline/sdl/functions.rb +73 -0
  76. data/lib/badline/sdl.rb +67 -0
  77. data/lib/badline/sid/catch_up.rb +110 -0
  78. data/lib/badline/sid/decimator.rb +6 -1
  79. data/lib/badline/sid/filter.rb +5 -5
  80. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  81. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  82. data/lib/badline/sid/waveform.rb +8 -8
  83. data/lib/badline/sid.rb +19 -93
  84. data/lib/badline/storage/d64_image.rb +10 -0
  85. data/lib/badline/storage/d71_image.rb +12 -0
  86. data/lib/badline/storage/d81_image.rb +9 -0
  87. data/lib/badline/storage/disk_image/bam.rb +111 -0
  88. data/lib/badline/storage/disk_image/directory.rb +79 -0
  89. data/lib/badline/storage/disk_image/writing.rb +110 -0
  90. data/lib/badline/storage/disk_image.rb +49 -4
  91. data/lib/badline/storage/host_directory.rb +17 -4
  92. data/lib/badline/storage/sid_file.rb +36 -2
  93. data/lib/badline/storage.rb +18 -1
  94. data/lib/badline/time_of_day.rb +11 -2
  95. data/lib/badline/version.rb +1 -1
  96. data/lib/badline/via/control_lines.rb +104 -0
  97. data/lib/badline/via/interrupt_register.rb +58 -0
  98. data/lib/badline/via/shift_register.rb +135 -0
  99. data/lib/badline/via/timer1.rb +82 -0
  100. data/lib/badline/via/timer2.rb +84 -0
  101. data/lib/badline/via.rb +262 -0
  102. data/lib/badline/vic/bank.rb +4 -3
  103. data/lib/badline/vic/color_patches.rb +6 -3
  104. data/lib/badline/vic/display_state.rb +4 -0
  105. data/lib/badline/vic/graphics_shifter.rb +49 -8
  106. data/lib/badline/vic/registers.rb +19 -27
  107. data/lib/badline/vic/sequencer.rb +18 -18
  108. data/lib/badline/vic/sequencer_output.rb +4 -4
  109. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  110. data/lib/badline/vic/sprite/shifter.rb +25 -3
  111. data/lib/badline/vic/sprite.rb +5 -4
  112. data/lib/badline/vic/sprites.rb +8 -3
  113. data/lib/badline/vic.rb +98 -46
  114. data/lib/badline.rb +5 -0
  115. data/packaging/homebrew/badline.rb +34 -0
  116. metadata +37 -20
  117. data/exe/badline +0 -75
  118. data/exe/badline-sid +0 -34
  119. data/lib/badline/audio/options.rb +0 -137
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # GEO-RAM: a RAM expansion on the expansion port, seen through a 256-byte
6
+ # window at $DE00-$DEFF. Two write-only registers pick the page the
7
+ # window shows: $DFFE selects one of the 64 pages of a 16K block, and
8
+ # $DFFF selects the block. As in VICE, the registers answer at every
9
+ # address from $DF80 up, even for $DFFE and odd for $DFFF, and the block
10
+ # number wraps at the number of blocks the RAM holds. Reads of I/O 2 are
11
+ # open bus. It leaves EXROM and GAME high, and RES clears both
12
+ # registers.
13
+ class GeoRAM < Cartridge
14
+ SIZES = [64, 128, 256, 512, 1024, 2048, 4096].freeze
15
+ BLOCK_SIZE = 0x4000
16
+ PAGE_MASK = 0x3f
17
+
18
+ attr_reader :size
19
+
20
+ # size is the RAM in kilobytes.
21
+ def initialize(size: 512)
22
+ raise ArgumentError, "Unsupported GEO-RAM size #{size}K" unless SIZES.include?(size)
23
+
24
+ @size = size
25
+ @block_mask = ((size * 1024) / BLOCK_SIZE) - 1
26
+ super(Storage::CRTFile::Image.new(hardware_type: nil, subtype: 0, exrom: 1, game: 1,
27
+ name: "GEO-RAM", chips: []))
28
+ end
29
+
30
+ def readable_io_pages
31
+ [0xde]
32
+ end
33
+
34
+ def peek(addr)
35
+ @ram_data[@window + (addr & 0xff)]
36
+ end
37
+
38
+ def poke(addr, value)
39
+ if addr <= 0xdeff
40
+ @ram_data[@window + (addr & 0xff)] = value
41
+ elsif addr >= 0xdf80
42
+ if addr.odd?
43
+ @block = value & @block_mask
44
+ else
45
+ @page = value & PAGE_MASK
46
+ end
47
+ @window = (@block * BLOCK_SIZE) + (@page * 0x100)
48
+ end
49
+ end
50
+
51
+ def reset
52
+ @block = @page = @window = 0
53
+ end
54
+
55
+ private
56
+
57
+ def install_chips(_chips)
58
+ @ram_data = Array.new(@size * 1024, 0)
59
+ reset
60
+ end
61
+ end
62
+ end
63
+ end
@@ -26,7 +26,7 @@ module Badline
26
26
  private
27
27
 
28
28
  def install_chips(chips)
29
- @roml = banks_from(chips).first.compact.first
29
+ @roml = banks_from(chips).first.compact.first || EMPTY_BANK
30
30
  reset
31
31
  end
32
32
  end
@@ -2,12 +2,15 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # Magic Desk: up to 128 8K banks. A write to I/O 1 selects the bank from
6
+ # bits 0-6, masked to the size of the ROM and at least four banks, and
7
+ # switches the ROM out with bit 7.
5
8
  class MagicDesk < Cartridge
6
9
  def poke(addr, value)
7
10
  return if addr > 0xdeff
8
11
 
9
12
  @exrom = value.anybits?(0x80) ? 1 : 0
10
- @roml = @banks[(value & 0x3f) % @banks.length]
13
+ @roml = bank(@banks, value & @bank_mask)
11
14
  changed!
12
15
  end
13
16
 
@@ -19,6 +22,7 @@ module Badline
19
22
 
20
23
  def install_chips(chips)
21
24
  @banks = banks_from(chips).first
25
+ @bank_mask = (bank_mask(@banks) | 0x03) & 0x7f
22
26
  reset
23
27
  end
24
28
  end
@@ -2,11 +2,13 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # Ocean: up to 64 8K banks. A write to I/O 1 selects the bank from bits
6
+ # 0-5, masked to the size of the ROM.
5
7
  class Ocean < Cartridge
6
8
  def poke(addr, value)
7
9
  return if addr > 0xdeff
8
10
 
9
- select_bank((value & 0x3f) % @banks.length)
11
+ select_bank(value & @bank_mask)
10
12
  changed!
11
13
  end
12
14
 
@@ -19,12 +21,13 @@ module Badline
19
21
 
20
22
  # In 16K mode the selected bank shows through ROMH as well.
21
23
  def select_bank(number)
22
- @roml = @banks[number]
24
+ @roml = bank(@banks, number)
23
25
  @romh = @roml if game.zero?
24
26
  end
25
27
 
26
28
  def install_chips(chips)
27
29
  @banks = banks_from(chips).first
30
+ @bank_mask = bank_mask(@banks) & 0x3f
28
31
  reset
29
32
  end
30
33
  end
@@ -25,7 +25,7 @@ module Badline
25
25
  private
26
26
 
27
27
  def install_chips(chips)
28
- @roml = banks_from(chips).first.compact.first
28
+ @roml = banks_from(chips).first.compact.first || EMPTY_BANK
29
29
  reset
30
30
  end
31
31
  end
@@ -40,7 +40,7 @@ module Badline
40
40
 
41
41
  def install_chips(chips)
42
42
  @banks = banks_from(chips).first
43
- @bank_mask = @hucky ? @banks.length - 1 : 0x07
43
+ @bank_mask = @hucky ? bank_mask(@banks) : 0x07
44
44
  reset
45
45
  end
46
46
  end
@@ -31,8 +31,8 @@ module Badline
31
31
 
32
32
  def install_chips(chips)
33
33
  roml, romh = banks_from(chips)
34
- @roml = roml.first
35
- @romh = romh.first
34
+ @roml = bank(roml, 0)
35
+ @romh = bank(romh, 0)
36
36
  reset
37
37
  end
38
38
  end
@@ -25,8 +25,8 @@ module Badline
25
25
 
26
26
  def install_chips(chips)
27
27
  roml, romh = banks_from(chips)
28
- @roml = roml.first
29
- @romh = romh.first
28
+ @roml = bank(roml, 0)
29
+ @romh = bank(romh, 0)
30
30
  reset
31
31
  end
32
32
  end
@@ -26,6 +26,7 @@ require "badline/cartridge/atomic_power"
26
26
  require "badline/cartridge/final_cartridge3"
27
27
  require "badline/cartridge/retro_replay"
28
28
  require "badline/cartridge/kcs_power"
29
+ require "badline/cartridge/geo_ram"
29
30
 
30
31
  module Badline
31
32
  class Cartridge
@@ -168,10 +169,18 @@ module Badline
168
169
  Bank.new(data)
169
170
  end
170
171
 
172
+ # A bank the image leaves out reads as erased ROM.
171
173
  def bank(banks, number)
172
174
  banks[number] || EMPTY_BANK
173
175
  end
174
176
 
177
+ # The bank register bits a ROM decodes. The ROM holds the highest bank
178
+ # in the image, and has address lines for the power of two that covers
179
+ # it; the register bits above those lines are ignored.
180
+ def bank_mask(banks)
181
+ (1 << (banks.length - 1).bit_length) - 1
182
+ end
183
+
175
184
  # Sorts the CHIP packets into ROML and ROMH banks by load address. A 16K
176
185
  # chip at $8000 spans both.
177
186
  def banks_from(chips)
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # A digest of the machine's state at one cycle, for comparing two runs of
5
+ # the same scenario, on two builds or two revisions. Each component is
6
+ # hashed on its own with 32-bit FNV-1a, so a mismatch names the part of the
7
+ # machine that went its own way. Nothing is read through a chip's bus
8
+ # interface, so taking a checkpoint leaves the machine as it was.
9
+ #
10
+ # Colour RAM is digested as the four bits each cell stores: the upper
11
+ # nibble a CPU read sees comes off the open bus.
12
+ class Checkpoint
13
+ COMPONENTS = %w[cpu ram color_ram display vic cia1 cia2 sid].freeze
14
+
15
+ FNV_OFFSET = 0x811c9dc5
16
+ FNV_PRIME = 0x01000193
17
+
18
+ attr_reader :cycle, :digests
19
+
20
+ class << self
21
+ def take(computer)
22
+ new(computer.cycles, [
23
+ fnv1a(cpu_state(computer.cpu)),
24
+ fnv1a(ram(computer.ram)),
25
+ fnv1a(color_ram(computer.address_bus.color_ram)),
26
+ fnv1a(computer.vic.display),
27
+ fnv1a(vic_state(computer.vic)),
28
+ fnv1a(cia_state(computer.cia1)),
29
+ fnv1a(cia_state(computer.cia2)),
30
+ fnv1a(Array.new(0x20) { |reg| computer.sid.register(reg) })
31
+ ])
32
+ end
33
+
34
+ # Reads a line written by #to_s.
35
+ def parse(line)
36
+ fields = line.split
37
+ digests = COMPONENTS.map do |name|
38
+ field = fields.find { |f| f.start_with?("#{name}=") }
39
+ raise ArgumentError, "no #{name} digest in #{line.inspect}" unless field
40
+
41
+ field.split("=").last.to_i(16)
42
+ end
43
+ new(fields.first.to_i, digests)
44
+ end
45
+
46
+ def fnv1a(values)
47
+ hash = FNV_OFFSET
48
+ values.each { |value| hash = ((hash ^ (value & 0xffffffff)) * FNV_PRIME) & 0xffffffff }
49
+ hash
50
+ end
51
+
52
+ private
53
+
54
+ def cpu_state(cpu)
55
+ [cpu.program_counter, cpu.a, cpu.x, cpu.y, cpu.stack_pointer, cpu.p, cpu.cycles]
56
+ end
57
+
58
+ def ram(memory)
59
+ Array.new(0x10000) { |addr| memory.peek(addr) }
60
+ end
61
+
62
+ def color_ram(memory)
63
+ Array.new(0x400) { |offset| memory.nibble(0xd800 + offset) }
64
+ end
65
+
66
+ def vic_state(vic)
67
+ vic.register_file + [vic.rasterline, vic.column]
68
+ end
69
+
70
+ def cia_state(cia)
71
+ start = cia.start
72
+ [cia.peek(start), cia.peek(start + 1), cia.peek(start + 2), cia.peek(start + 3),
73
+ cia.timer_a, cia.timer_a_latch, cia.timer_b, cia.timer_b_latch, cia.serial.data,
74
+ cia.control_a.value, cia.control_b.value, cia.interrupt_status.value,
75
+ cia.interrupt_control.value] + cia.time_of_day.registers
76
+ end
77
+ end
78
+
79
+ def initialize(cycle, digests)
80
+ @cycle = cycle
81
+ @digests = digests
82
+ end
83
+
84
+ # The components whose digests differ from `other`'s.
85
+ def differences(other)
86
+ COMPONENTS.each_index.reject { |i| digests[i] == other.digests[i] }.map { |i| COMPONENTS[i] }
87
+ end
88
+
89
+ def ==(other)
90
+ other.is_a?(Checkpoint) && cycle == other.cycle && digests == other.digests
91
+ end
92
+
93
+ def to_s
94
+ fields = COMPONENTS.each_index.map { |i| "#{COMPONENTS[i]}=#{digests[i].to_s(16).rjust(8, '0')}" }
95
+ "#{cycle} #{fields.join(' ')}"
96
+ end
97
+ end
98
+ end
@@ -8,7 +8,9 @@ module Badline
8
8
  class InterruptRegister
9
9
  attr_reader :status, :mask, :quiet
10
10
 
11
- def initialize
11
+ # The 6526A is the faster of the two: see the rules below.
12
+ def initialize(model = :mos6526)
13
+ @fast = model == :mos6526a
12
14
  @mask = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
13
15
  @status = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
14
16
  @pending = 0
@@ -32,37 +34,51 @@ module Badline
32
34
 
33
35
  def interrupted? = status.value >= 0x80
34
36
 
37
+ # Raises IR on this cycle.
38
+ def assert_now
39
+ status.interrupt = true
40
+ @pending = 0
41
+ end
42
+
35
43
  # Schedules IR to rise after delay cycles, keeping an earlier one.
36
44
  def assert!(delay = 1)
37
45
  @pending = @pending.positive? ? [@pending, delay].min : delay
38
46
  @quiet = false
39
47
  end
40
48
 
41
- # Latch a source, pulling the interrupt line if it is armed.
49
+ # Latch a source, pulling the interrupt line if it is armed. The 6526
50
+ # raises IR a cycle after the flag. The 6526A raises it on the flag's
51
+ # own cycle, unless the ICR was read on the cycle before.
42
52
  def flag(source)
43
53
  bit = source_bit(source)
44
54
  @status.value |= bit
45
- assert! if mask.value.anybits?(bit)
55
+ return unless mask.value.anybits?(bit)
56
+
57
+ @fast && @read_last_cycle.nil? ? assert_now : assert!
46
58
  end
47
59
 
48
60
  # The 6526 timer B bug: an underflow on the cycle after a read still
49
- # raises the flag, but the next read drops it unseen.
61
+ # raises the flag, but the next read drops it unseen. The 6526A has
62
+ # no such bug.
50
63
  def timer_b_underflow!
51
- @timer_b_bug = !@read_last_cycle.nil?
64
+ @timer_b_bug = !@fast && !@read_last_cycle.nil?
52
65
  flag(:timer_b)
53
66
  end
54
67
 
55
68
  # A read releases the interrupt line at once, and cancels an assert
56
- # due on the next cycle, but on the 6526 its IR acknowledge lands a
57
- # cycle late: the next cycle still reads IR set if it was set, or
58
- # about to be.
69
+ # due on the next cycle, but its acknowledge lands a cycle late. On
70
+ # the 6526 that holds only IR: the next cycle still reads IR set if it
71
+ # was set, or about to be. The 6526A reads an IR about to rise as set
72
+ # at once, and holds every bit it read: the next cycle reads them all
73
+ # again, along with anything flagged since.
59
74
  def read
60
75
  status.timer_b = false if @timer_b_bug
61
76
  @timer_b_bug = false
62
77
  value = status.value
78
+ value |= 0x80 if @fast && @pending == 1
63
79
  @read = @pending == 1 ? value | 0x80 : value
64
80
  @quiet = false
65
- value |= 0x80 if @read_last_cycle&.anybits?(0x80)
81
+ value |= held(@read_last_cycle) if @read_last_cycle
66
82
  status.value = 0x0
67
83
  @pending = 0
68
84
  value
@@ -76,7 +92,7 @@ module Badline
76
92
  @mask.value |= (value & 0x1f)
77
93
  end
78
94
  if mask.value.anybits?(status.value & 0x1f)
79
- assert!(2) unless interrupted?
95
+ assert!(arm_delay) unless interrupted?
80
96
  elsif @pending == 1 && @read_two_cycles_ago
81
97
  # On the 6526, masking the pending source cancels the assert due
82
98
  # on the next cycle only while a read two cycles back is
@@ -87,6 +103,14 @@ module Badline
87
103
 
88
104
  private
89
105
 
106
+ # Arming a pending source raises IR two cycles later on the 6526,
107
+ # and one cycle later on the 6526A.
108
+ def arm_delay = @fast ? 1 : 2
109
+
110
+ # What a read on the cycle before still holds: IR on the 6526, and
111
+ # every bit it read on the 6526A.
112
+ def held(last) = @fast ? last : last & 0x80
113
+
90
114
  def source_bit(source)
91
115
  case source
92
116
  when :timer_a then 0x01
data/lib/badline/cia.rb CHANGED
@@ -9,7 +9,11 @@ module Badline
9
9
  class CIA
10
10
  include Addressable
11
11
 
12
- attr_reader :start, :control_a, :control_b, :peripheral, :serial
12
+ # The original NMOS 6526 of the breadbin C64, and the HMOS 6526A of the
13
+ # C64C, which differs from it only in its interrupt control register.
14
+ MODELS = %i[mos6526 mos6526a].freeze
15
+
16
+ attr_reader :start, :control_a, :control_b, :peripheral, :serial, :model
13
17
 
14
18
  def interrupt_status = @icr.status
15
19
 
@@ -17,6 +21,8 @@ module Badline
17
21
 
18
22
  def interrupt!(delay = 1) = @icr.assert!(delay)
19
23
 
24
+ def time_of_day = @tod
25
+
20
26
  def timer_a = @ta.counter
21
27
 
22
28
  def timer_a=(value)
@@ -41,9 +47,15 @@ module Badline
41
47
  @tb.latch = value
42
48
  end
43
49
 
44
- def initialize(start: 0, peripheral: nil)
50
+ def initialize(start: 0, peripheral: nil, model: :mos6526, region: Region::PAL)
51
+ raise ArgumentError, "unknown CIA model #{model}" unless MODELS.include?(model)
52
+
45
53
  addressable_at(start, length: 2**8)
46
54
 
55
+ @model = model
56
+ @clock_hz = region.clock_hz
57
+ @mains_hz = region.mains_hz
58
+
47
59
  @peripheral = peripheral
48
60
  @port_b4_handler = nil
49
61
  @port_b4_high = true
@@ -61,8 +73,8 @@ module Badline
61
73
  @port_b4_driven_high = true
62
74
  @cnt_high = true
63
75
  @cnt_rise = false
64
- @tod = TimeOfDay.new
65
- @icr = InterruptRegister.new
76
+ @tod = TimeOfDay.new(clock_hz: @clock_hz, mains_hz: @mains_hz)
77
+ @icr = InterruptRegister.new(@model)
66
78
  @icr_status = @icr.status
67
79
  @control_a = ControlRegister.new(%i[start output out_mode run_mode load
68
80
  in_mode serial_mode clock_frequency])
@@ -115,7 +127,7 @@ module Badline
115
127
  end
116
128
 
117
129
  def peek(addr)
118
- case index(addr) & 0x0f
130
+ case offset_of(addr) & 0x0f
119
131
  when 0x00 then read_port_a
120
132
  when 0x01 then read_port_b
121
133
  when 0x02 then @data_dir_a
@@ -136,7 +148,7 @@ module Badline
136
148
  end
137
149
 
138
150
  def poke(addr, value)
139
- case index(addr) & 0x0f
151
+ case offset_of(addr) & 0x0f
140
152
  when 0x00 then @data_port_a = value
141
153
  when 0x01 then update_port_b { @data_port_b = value }
142
154
  when 0x02 then @data_dir_a = value
@@ -221,10 +233,7 @@ module Badline
221
233
  else underflowed && @cnt_high
222
234
  end
223
235
  )
224
- if underflowed
225
- interrupt_status.timer_a = true
226
- interrupt! if interrupt_control.timer_a?
227
- end
236
+ @icr.flag(:timer_a) if underflowed
228
237
  @icr.timer_b_underflow! if @tb.underflowed
229
238
  underflowed
230
239
  end
@@ -10,7 +10,7 @@ module Badline
10
10
  end
11
11
 
12
12
  def peek(addr)
13
- (@vic.phi1_data & 0xf0) | @storage[index(addr)]
13
+ (@vic.phi1_data & 0xf0) | @storage[offset_of(addr)]
14
14
  end
15
15
 
16
16
  def poke(addr, value)
@@ -19,7 +19,7 @@ module Badline
19
19
 
20
20
  # The four bits the cell stores, as the VIC's c-access sees them.
21
21
  def nibble(addr)
22
- @storage[index(addr)]
22
+ @storage[offset_of(addr)]
23
23
  end
24
24
  end
25
25
  end
@@ -7,6 +7,8 @@ module Badline
7
7
 
8
8
  attr_reader :address_bus, :cpu, :cycles
9
9
 
10
+ def region = address_bus.region
11
+
10
12
  def vic = address_bus.vic
11
13
 
12
14
  def cia1 = address_bus.cia1
@@ -29,8 +31,12 @@ module Badline
29
31
 
30
32
  def install_debug_register(&) = address_bus.install_debug_register(&)
31
33
 
32
- def initialize(debug: false, sid_model: :mos6581)
33
- @address_bus = AddressBus.new(sid_model:)
34
+ # The machine options (sid_model:, cia_model:, vic_model:, region: and
35
+ # ram_expansion:) configure the AddressBus. The region sets the clock,
36
+ # the VIC's raster and the mains frequency the CIAs' TOD clocks count.
37
+ # Only PAL runs as yet.
38
+ def initialize(debug: false, **machine)
39
+ @address_bus = AddressBus.new(**machine)
34
40
  @cpu = CPU.new(@address_bus, debug:)
35
41
  @vic = @address_bus.vic
36
42
  @vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
@@ -41,10 +47,12 @@ module Badline
41
47
  @cycles = 0
42
48
  @nmi_asserted = false
43
49
  @cartridge_nmi = false
50
+ @restore_pulse = false
44
51
  @freezing = false
45
52
  @freeze_writes = 0
46
53
  @init_handlers = []
47
54
  @pending_keys = nil
55
+ @drive = nil
48
56
  end
49
57
 
50
58
  INIT_THRESHOLD = 2_500_000
@@ -92,14 +100,16 @@ module Badline
92
100
  reset!
93
101
  end
94
102
 
95
- # The RES line reaches the CPU and its port, both CIAs, the SID and the
96
- # cartridge port. The VIC has no reset pin.
103
+ # The RES line reaches the CPU and its port, both CIAs, the SID, the
104
+ # cartridge port, a RAM expansion and, through the serial bus's RESET
105
+ # line, the drive. The VIC has no reset pin.
97
106
  def reset!
98
- address_bus.reset_port!
107
+ address_bus.reset!
99
108
  @cia1.reset!
100
109
  @cia2.reset!
101
110
  @sid.reset!
102
111
  address_bus.cartridge&.reset
112
+ @drive&.reset!
103
113
  @freezing = false
104
114
  @nmi_asserted = false
105
115
  cpu.reset!
@@ -120,14 +130,23 @@ module Badline
120
130
  address_bus.cartridge&.release_button
121
131
  end
122
132
 
133
+ # RESTORE isn't in the key matrix. It fires a one-shot that pulses the
134
+ # NMI line however long the key is held, modelled here as one cycle.
135
+ def press_restore
136
+ @restore_pulse = true
137
+ end
138
+
139
+ # Puts the storage in device 8. Mounting again swaps the disk at any
140
+ # point while the machine runs: the drive keeps its RAM, which only a
141
+ # drive reset clears, and its status.
123
142
  def mount(storage)
124
- drive = KernalTrap::Drive.new(storage)
125
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive:)
126
- cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
127
- KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
128
- return unless storage.respond_to?(:write_file)
143
+ return @drive.insert(storage) if @drive
129
144
 
130
- save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
145
+ @drive = KernalTrap::Drive.new(storage)
146
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
147
+ cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
148
+ KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
149
+ save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
131
150
  cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
132
151
  end
133
152
 
@@ -151,10 +170,11 @@ module Badline
151
170
 
152
171
  private
153
172
 
154
- # The NMI line is wired-OR between CIA 2 and the cartridge, and the CPU
155
- # takes an interrupt on its falling edge.
173
+ # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
174
+ # key, and the CPU takes an interrupt on its falling edge.
156
175
  def drive_nmi
157
- nmi = @cia2.interrupted? || @cartridge_nmi
176
+ nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
177
+ @restore_pulse = false
158
178
  @cpu.nmi = true if nmi && !@nmi_asserted
159
179
  @nmi_asserted = nmi
160
180
  end
data/lib/badline/cpu.rb CHANGED
@@ -24,10 +24,10 @@ module Badline
24
24
 
25
25
  # +ane_constant+ is ANE's magic constant, which varies from chip to
26
26
  # chip. The default is the C64 6510's.
27
- def initialize(memory = nil, debug: false, ane_constant: 0xef)
27
+ def initialize(memory, debug: false, ane_constant: 0xef)
28
28
  @debug = debug
29
29
  @ane_constant = ane_constant
30
- @memory = memory || Memory.new
30
+ @memory = memory
31
31
  @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
32
32
  reset_registers
33
33