badline 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/CLAUDE.md +6 -0
  4. data/README.md +24 -6
  5. data/doc/pinned-behaviour.md +18 -6
  6. data/exe/badline +5 -2
  7. data/lib/badline/address_bus.rb +14 -6
  8. data/lib/badline/addressable.rb +1 -1
  9. data/lib/badline/audio/bare_player.rb +20 -3
  10. data/lib/badline/audio/machine_player.rb +9 -6
  11. data/lib/badline/audio/renderer.rb +3 -3
  12. data/lib/badline/audio/stream.rb +117 -0
  13. data/lib/badline/audio.rb +1 -0
  14. data/lib/badline/cartridge/easy_flash.rb +6 -2
  15. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  16. data/lib/badline/cartridge/mach5.rb +1 -1
  17. data/lib/badline/cartridge/magic_desk.rb +5 -1
  18. data/lib/badline/cartridge/ocean.rb +5 -2
  19. data/lib/badline/cartridge/rex_utility.rb +1 -1
  20. data/lib/badline/cartridge/rgcd.rb +1 -1
  21. data/lib/badline/cartridge/simons_basic.rb +2 -2
  22. data/lib/badline/cartridge/westermann.rb +2 -2
  23. data/lib/badline/cartridge.rb +16 -8
  24. data/lib/badline/checkpoint.rb +98 -0
  25. data/lib/badline/cia/interrupt_register.rb +20 -6
  26. data/lib/badline/cia.rb +36 -19
  27. data/lib/badline/color_memory.rb +2 -2
  28. data/lib/badline/computer.rb +41 -11
  29. data/lib/badline/cpu/microcode.rb +41 -43
  30. data/lib/badline/cpu.rb +3 -3
  31. data/lib/badline/gui/application.rb +41 -9
  32. data/lib/badline/kernal_trap/channel.rb +118 -8
  33. data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
  34. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  35. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  36. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  37. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  38. data/lib/badline/kernal_trap/drive.rb +148 -132
  39. data/lib/badline/kernal_trap/file.rb +0 -5
  40. data/lib/badline/kernal_trap/load.rb +93 -25
  41. data/lib/badline/kernal_trap/routine.rb +17 -0
  42. data/lib/badline/kernal_trap/save.rb +48 -15
  43. data/lib/badline/kernal_trap/serial.rb +34 -2
  44. data/lib/badline/media.rb +2 -2
  45. data/lib/badline/memory.rb +7 -2
  46. data/lib/badline/rom.rb +14 -5
  47. data/lib/badline/sid/catch_up.rb +110 -0
  48. data/lib/badline/sid/decimator.rb +6 -1
  49. data/lib/badline/sid/filter.rb +5 -5
  50. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  51. data/lib/badline/sid/waveform.rb +7 -7
  52. data/lib/badline/sid.rb +16 -92
  53. data/lib/badline/status.rb +255 -32
  54. data/lib/badline/storage/d64_image.rb +6 -0
  55. data/lib/badline/storage/d71_image.rb +4 -0
  56. data/lib/badline/storage/d81_image.rb +4 -0
  57. data/lib/badline/storage/disk_image.rb +78 -17
  58. data/lib/badline/storage/host_directory.rb +26 -2
  59. data/lib/badline/storage/sid_file.rb +37 -3
  60. data/lib/badline/storage.rb +3 -1
  61. data/lib/badline/time_of_day.rb +7 -0
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/graphics_shifter.rb +8 -2
  64. data/lib/badline/vic/registers.rb +16 -27
  65. data/lib/badline/vic.rb +23 -10
  66. data/lib/badline.rb +1 -0
  67. metadata +9 -1
@@ -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
@@ -35,12 +35,12 @@ module Badline
35
35
  BANK_SIZE = 0x2000
36
36
 
37
37
  HARDWARE_TYPES = {
38
- 0 => :Standard, 1 => :ActionReplay, 2 => :KCSPower, 3 => :FinalCartridge3,
39
- 4 => :SimonsBasic, 5 => :Ocean, 7 => :FunPlay, 8 => :SuperGames,
40
- 9 => :AtomicPower, 10 => :EpyxFastload, 11 => :Westermann,
41
- 12 => :RexUtility, 15 => :GameSystem, 17 => :Dinamic, 18 => :Zaxxon,
42
- 19 => :MagicDesk, 21 => :Comal80, 32 => :EasyFlash, 36 => :RetroReplay,
43
- 51 => :Mach5, 53 => :Pagefox, 57 => :RGCD, 60 => :GMod2
38
+ 0 => Standard, 1 => ActionReplay, 2 => KCSPower, 3 => FinalCartridge3,
39
+ 4 => SimonsBasic, 5 => Ocean, 7 => FunPlay, 8 => SuperGames,
40
+ 9 => AtomicPower, 10 => EpyxFastload, 11 => Westermann,
41
+ 12 => RexUtility, 15 => GameSystem, 17 => Dinamic, 18 => Zaxxon,
42
+ 19 => MagicDesk, 21 => Comal80, 32 => EasyFlash, 36 => RetroReplay,
43
+ 51 => Mach5, 53 => Pagefox, 57 => RGCD, 60 => GMod2
44
44
  }.freeze
45
45
 
46
46
  # The EXROM and GAME line levels of each memory configuration. The lines
@@ -62,11 +62,11 @@ module Badline
62
62
  end
63
63
 
64
64
  def from_crt(crt, **)
65
- type = HARDWARE_TYPES.fetch(crt.hardware_type) do
65
+ mapper = HARDWARE_TYPES.fetch(crt.hardware_type) do
66
66
  raise UnsupportedTypeError,
67
67
  "Unsupported cartridge hardware type #{crt.hardware_type}"
68
68
  end
69
- const_get(type).new(crt, **)
69
+ mapper.new(crt, **)
70
70
  end
71
71
  end
72
72
 
@@ -168,10 +168,18 @@ module Badline
168
168
  Bank.new(data)
169
169
  end
170
170
 
171
+ # A bank the image leaves out reads as erased ROM.
171
172
  def bank(banks, number)
172
173
  banks[number] || EMPTY_BANK
173
174
  end
174
175
 
176
+ # The bank register bits a ROM decodes. The ROM holds the highest bank
177
+ # in the image, and has address lines for the power of two that covers
178
+ # it; the register bits above those lines are ignored.
179
+ def bank_mask(banks)
180
+ (1 << (banks.length - 1).bit_length) - 1
181
+ end
182
+
175
183
  # Sorts the CHIP packets into ROML and ROMH banks by load address. A 16K
176
184
  # chip at $8000 spans both.
177
185
  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
@@ -9,8 +9,8 @@ module Badline
9
9
  attr_reader :status, :mask, :quiet
10
10
 
11
11
  def initialize
12
- @mask = Status.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
13
- @status = Status.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
12
+ @mask = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
13
+ @status = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
14
14
  @pending = 0
15
15
  @read = @read_last_cycle = @read_two_cycles_ago = nil
16
16
  @timer_b_bug = false
@@ -40,8 +40,9 @@ module Badline
40
40
 
41
41
  # Latch a source, pulling the interrupt line if it is armed.
42
42
  def flag(source)
43
- status.public_send(:"#{source}=", true)
44
- assert! if mask.public_send(:"#{source}?")
43
+ bit = source_bit(source)
44
+ @status.value |= bit
45
+ assert! if mask.value.anybits?(bit)
45
46
  end
46
47
 
47
48
  # The 6526 timer B bug: an underflow on the cycle after a read still
@@ -70,9 +71,9 @@ module Badline
70
71
  # Bit 7 picks between setting and clearing the mask bits given.
71
72
  def write(value)
72
73
  if value.nobits?(0x80)
73
- mask.value &= ~(value & 0x1f)
74
+ @mask.value &= ~(value & 0x1f)
74
75
  else
75
- mask.value |= (value & 0x1f)
76
+ @mask.value |= (value & 0x1f)
76
77
  end
77
78
  if mask.value.anybits?(status.value & 0x1f)
78
79
  assert!(2) unless interrupted?
@@ -83,6 +84,19 @@ module Badline
83
84
  @pending = 0
84
85
  end
85
86
  end
87
+
88
+ private
89
+
90
+ def source_bit(source)
91
+ case source
92
+ when :timer_a then 0x01
93
+ when :timer_b then 0x02
94
+ when :alarm then 0x04
95
+ when :serial then 0x08
96
+ when :flag then 0x10
97
+ else raise ArgumentError, "unknown interrupt source #{source}"
98
+ end
99
+ end
86
100
  end
87
101
  end
88
102
  end
data/lib/badline/cia.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "badline/cia/interrupt_register"
5
4
  require "badline/cia/serial"
6
5
  require "badline/cia/timer"
@@ -9,22 +8,40 @@ module Badline
9
8
  # CIA (Complex Interface Adapter) chip
10
9
  class CIA
11
10
  include Addressable
12
- extend Forwardable
13
11
 
14
12
  attr_reader :start, :control_a, :control_b, :peripheral, :serial
15
13
 
16
- def_delegator :@icr, :status, :interrupt_status
17
- def_delegator :@icr, :mask, :interrupt_control
18
- def_delegator :@icr, :assert!, :interrupt!
14
+ def interrupt_status = @icr.status
19
15
 
20
- def_delegator :@ta, :counter, :timer_a
21
- def_delegator :@ta, :counter=, :timer_a=
22
- def_delegator :@ta, :latch, :timer_a_latch
23
- def_delegator :@ta, :latch=, :timer_a_latch=
24
- def_delegator :@tb, :counter, :timer_b
25
- def_delegator :@tb, :counter=, :timer_b=
26
- def_delegator :@tb, :latch, :timer_b_latch
27
- def_delegator :@tb, :latch=, :timer_b_latch=
16
+ def interrupt_control = @icr.mask
17
+
18
+ def interrupt!(delay = 1) = @icr.assert!(delay)
19
+
20
+ def time_of_day = @tod
21
+
22
+ def timer_a = @ta.counter
23
+
24
+ def timer_a=(value)
25
+ @ta.counter = value
26
+ end
27
+
28
+ def timer_a_latch = @ta.latch
29
+
30
+ def timer_a_latch=(value)
31
+ @ta.latch = value
32
+ end
33
+
34
+ def timer_b = @tb.counter
35
+
36
+ def timer_b=(value)
37
+ @tb.counter = value
38
+ end
39
+
40
+ def timer_b_latch = @tb.latch
41
+
42
+ def timer_b_latch=(value)
43
+ @tb.latch = value
44
+ end
28
45
 
29
46
  def initialize(start: 0, peripheral: nil)
30
47
  addressable_at(start, length: 2**8)
@@ -49,10 +66,10 @@ module Badline
49
66
  @tod = TimeOfDay.new
50
67
  @icr = InterruptRegister.new
51
68
  @icr_status = @icr.status
52
- @control_a = Status.new(%i[start output out_mode run_mode load
53
- in_mode serial_mode clock_frequency])
54
- @control_b = Status.new(%i[start output out_mode run_mode load
55
- in_cnt in_timer_a alarm])
69
+ @control_a = ControlRegister.new(%i[start output out_mode run_mode load
70
+ in_mode serial_mode clock_frequency])
71
+ @control_b = ControlRegister.new(%i[start output out_mode run_mode load
72
+ in_cnt in_timer_a alarm])
56
73
  @ta = Timer.new(@control_a)
57
74
  @tb = Timer.new(@control_b)
58
75
  @serial = Serial.new(@control_a)
@@ -100,7 +117,7 @@ module Badline
100
117
  end
101
118
 
102
119
  def peek(addr)
103
- case index(addr) & 0x0f
120
+ case offset_of(addr) & 0x0f
104
121
  when 0x00 then read_port_a
105
122
  when 0x01 then read_port_b
106
123
  when 0x02 then @data_dir_a
@@ -121,7 +138,7 @@ module Badline
121
138
  end
122
139
 
123
140
  def poke(addr, value)
124
- case index(addr) & 0x0f
141
+ case offset_of(addr) & 0x0f
125
142
  when 0x00 then @data_port_a = value
126
143
  when 0x01 then update_port_b { @data_port_b = value }
127
144
  when 0x02 then @data_dir_a = value
@@ -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
@@ -1,17 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
-
5
3
  module Badline
6
4
  class Computer
7
5
  include IntegerHelper
8
6
  include KeyboardBuffer
9
- extend Forwardable
10
7
 
11
8
  attr_reader :address_bus, :cpu, :cycles
12
9
 
13
- def_delegators :address_bus, :vic, :cia1, :cia2, :sid, :ram, :keyboard, :joystick1, :joystick2,
14
- :control_ports, :datasette, :install_debug_register
10
+ def vic = address_bus.vic
11
+
12
+ def cia1 = address_bus.cia1
13
+
14
+ def cia2 = address_bus.cia2
15
+
16
+ def sid = address_bus.sid
17
+
18
+ def ram = address_bus.ram
19
+
20
+ def keyboard = address_bus.keyboard
21
+
22
+ def joystick1 = address_bus.joystick1
23
+
24
+ def joystick2 = address_bus.joystick2
25
+
26
+ def control_ports = address_bus.control_ports
27
+
28
+ def datasette = address_bus.datasette
29
+
30
+ def install_debug_register(&) = address_bus.install_debug_register(&)
15
31
 
16
32
  def initialize(debug: false, sid_model: :mos6581)
17
33
  @address_bus = AddressBus.new(sid_model:)
@@ -29,6 +45,7 @@ module Badline
29
45
  @freeze_writes = 0
30
46
  @init_handlers = []
31
47
  @pending_keys = nil
48
+ @drive = nil
32
49
  end
33
50
 
34
51
  INIT_THRESHOLD = 2_500_000
@@ -64,17 +81,28 @@ module Badline
64
81
  cartridge.clock = -> { @cycles }
65
82
  cartridge.on_nmi_change { |level| @cartridge_nmi = level }
66
83
  address_bus.attach_cartridge(cartridge)
84
+ power_cycle!
85
+ end
86
+
87
+ # A cartridge goes in with the power off, so attaching one switches the
88
+ # machine off and on: the VIC and RAM start from their power-on state,
89
+ # and the RES line resets everything else.
90
+ def power_cycle!
91
+ vic.power_on!
92
+ address_bus.power_on!
67
93
  reset!
68
94
  end
69
95
 
70
- # The RES line reaches the CPU and its port, both CIAs, the SID and the
71
- # cartridge port. The VIC has no reset pin.
96
+ # The RES line reaches the CPU and its port, both CIAs, the SID, the
97
+ # cartridge port and, through the serial bus's RESET line, the drive.
98
+ # The VIC has no reset pin.
72
99
  def reset!
73
100
  address_bus.reset_port!
74
101
  @cia1.reset!
75
102
  @cia2.reset!
76
103
  @sid.reset!
77
104
  address_bus.cartridge&.reset
105
+ @drive&.reset!
78
106
  @freezing = false
79
107
  @nmi_asserted = false
80
108
  cpu.reset!
@@ -95,14 +123,16 @@ module Badline
95
123
  address_bus.cartridge&.release_button
96
124
  end
97
125
 
126
+ # A disk swap keeps the drive's RAM, which only a drive reset clears.
98
127
  def mount(storage)
99
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, storage:)
128
+ @drive_memory ||= KernalTrap::Drive::Memory.new
129
+ @drive = KernalTrap::Drive.new(storage, @drive_memory)
130
+ load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
100
131
  cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
101
- drive = KernalTrap::Drive.new(storage)
102
- KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
132
+ KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
103
133
  return unless storage.respond_to?(:write_file)
104
134
 
105
- save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
135
+ save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
106
136
  cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
107
137
  end
108
138
 
@@ -107,60 +107,58 @@ module Badline
107
107
  # - +optional_dummy+: true for indexed reads, which skip the extra
108
108
  # cycle unless indexing crosses a page
109
109
  Microcode = Struct.new(:operation, :plan, :writes, :optional_dummy) do
110
- class << self
111
- def table
112
- Array.new(256) do |opcode|
113
- instruction = Instruction.find(opcode)
114
- raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
115
-
116
- build(instruction)
117
- end.freeze
118
- end
110
+ def self.table
111
+ Array.new(256) do |opcode|
112
+ instruction = Instruction.find(opcode)
113
+ raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
119
114
 
120
- def write_mask(plan)
121
- plan.map { |step| WRITE_STEPS.include?(step) }.freeze
122
- end
115
+ build(instruction)
116
+ end.freeze
117
+ end
123
118
 
124
- private
119
+ def self.write_mask(plan)
120
+ plan.map { |step| WRITE_STEPS.include?(step) }.freeze
121
+ end
125
122
 
126
- def build(instruction)
127
- plan = plan_for(instruction)
128
- new(instruction.name, plan, write_mask(plan),
129
- instruction.boundary_cycle?)
130
- end
123
+ def self.build(instruction)
124
+ plan = plan_for(instruction)
125
+ new(instruction.name, plan, write_mask(plan),
126
+ instruction.boundary_cycle?)
127
+ end
131
128
 
132
- def plan_for(instruction)
133
- case instruction.name
134
- when :jmp
135
- if instruction.addressing_mode == :indirect
136
- JMP_INDIRECT_PLAN
137
- else
138
- JMP_ABSOLUTE_PLAN
139
- end
140
- when *BRANCHES then BRANCH_PLAN
141
- else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
129
+ def self.plan_for(instruction)
130
+ case instruction.name
131
+ when :jmp
132
+ if instruction.addressing_mode == :indirect
133
+ JMP_INDIRECT_PLAN
134
+ else
135
+ JMP_ABSOLUTE_PLAN
142
136
  end
137
+ when *BRANCHES then BRANCH_PLAN
138
+ else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
143
139
  end
140
+ end
144
141
 
145
- def addressed_plan(instruction)
146
- mode = instruction.addressing_mode
147
- case mode
148
- when :implied then IMPLIED_PLAN
149
- when :accumulator then ACCUMULATOR_PLAN
150
- when :immediate then IMMEDIATE_PLAN
151
- else
152
- [:op_fetch, *ADDRESSING_STEPS.fetch(mode),
153
- *OPERATION_STEPS.fetch(operation_kind(instruction.name))].freeze
154
- end
142
+ def self.addressed_plan(instruction)
143
+ mode = instruction.addressing_mode
144
+ case mode
145
+ when :implied then IMPLIED_PLAN
146
+ when :accumulator then ACCUMULATOR_PLAN
147
+ when :immediate then IMMEDIATE_PLAN
148
+ else
149
+ [:op_fetch, *ADDRESSING_STEPS.fetch(mode),
150
+ *OPERATION_STEPS.fetch(operation_kind(instruction.name))].freeze
155
151
  end
152
+ end
156
153
 
157
- def operation_kind(name)
158
- return :write if STORES.include?(name)
159
- return :rmw if READ_MODIFY_WRITES.include?(name)
154
+ def self.operation_kind(name)
155
+ return :write if STORES.include?(name)
156
+ return :rmw if READ_MODIFY_WRITES.include?(name)
160
157
 
161
- :read
162
- end
158
+ :read
163
159
  end
160
+
161
+ private_class_method :build, :plan_for, :addressed_plan, :operation_kind
164
162
  end
165
163
 
166
164
  FETCH_WRITES = Microcode.write_mask(FETCH_PLAN)
data/lib/badline/cpu.rb CHANGED
@@ -24,11 +24,11 @@ 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
31
- @status = Status.new(STATUS_FLAGS, value: 0b00100000)
30
+ @memory = memory
31
+ @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
32
32
  reset_registers
33
33
 
34
34
  @nmi = @irq = false
@@ -6,6 +6,7 @@ module Badline
6
6
  PAL_CLOCK_HZ = 985_248
7
7
  TITLE = "Badline"
8
8
  TOGGLE_SYM = SDL2::Key::TAB
9
+ MUTE_SYM = SDL2::Key::F10
9
10
  REVERSE_MOD = SDL2::Key::Mod::SHIFT
10
11
 
11
12
  SHARED_KEYS = %i[up left cursor_h cursor_v space w a s d lshift].freeze
@@ -28,24 +29,23 @@ module Badline
28
29
  }.freeze
29
30
  MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
30
31
 
31
- def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil, debug: false)
32
- @computer = Computer.new(debug:, sid_model: sid_model || Media.sid_model(media_path))
32
+ def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil, sound: false)
33
+ @computer = Computer.new(sid_model: sid_model || Media.sid_model(media_path))
33
34
  puts Media.attach(@computer, media_path, autostart:, song:) if media_path
34
35
 
35
36
  @mode = :keyboard
36
37
  @pot_device = nil
37
38
  @panes = [ScreenPane.new(@computer)]
39
+ @stream = open_stream if sound
40
+ @paced = ENV["NOVSYNC"].nil?
38
41
  @window = Window.new(
39
42
  title: TITLE,
40
43
  width: canvas_width, height: canvas_height,
41
- vsync: ENV["NOVSYNC"].nil?
44
+ vsync: @paced && !@stream
42
45
  )
43
46
  @gamepads = Gamepads.new(@computer)
44
47
  @gamepads.names.each { |name| puts "Gamepad: #{name}" }
45
-
46
- rate = @window.refresh_rate
47
- @cycles_per_frame = PAL_CLOCK_HZ / rate
48
- puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
48
+ fit_frame
49
49
  end
50
50
 
51
51
  def run
@@ -54,9 +54,12 @@ module Badline
54
54
  handle_events
55
55
  @gamepads.poll
56
56
  @cycles_per_frame.times { @computer.cycle! }
57
+ @stream&.feed
57
58
  @window.draw(@panes)
59
+ @stream.pace(@frame_seconds) if @stream && @paced
58
60
  end
59
61
  ensure
62
+ @stream&.close
60
63
  @gamepads.close
61
64
  puts @computer.cpu.inspect
62
65
  end
@@ -84,6 +87,7 @@ module Badline
84
87
 
85
88
  def handle_key_down(event)
86
89
  return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
90
+ return toggle_mute if event.sym == MUTE_SYM
87
91
 
88
92
  port, dir = JoyMap.parse(event) if @mode == :joystick
89
93
  if port
@@ -94,7 +98,7 @@ module Badline
94
98
  end
95
99
 
96
100
  def handle_key_up(event)
97
- return if event.sym == TOGGLE_SYM
101
+ return if [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
98
102
 
99
103
  port, dir = JoyMap.parse(event) if @mode == :joystick
100
104
  if port
@@ -123,7 +127,35 @@ module Badline
123
127
  @mode = modes[(modes.index(@mode) + step) % modes.size]
124
128
  release_inputs
125
129
  attach_pot_device
126
- @window.title = [TITLE, MODES[@mode] && "[#{MODES[@mode]}]"].compact.join(" ")
130
+ update_title
131
+ end
132
+
133
+ def toggle_mute
134
+ return unless @stream
135
+
136
+ @stream.toggle_mute
137
+ update_title
138
+ end
139
+
140
+ def update_title
141
+ tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
142
+ @window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
143
+ end
144
+
145
+ def open_stream
146
+ sink = Audio::SDLSink.new(rate: Audio::Renderer::DEFAULT_RATE)
147
+ puts "Sound at #{sink.rate} Hz, F10 mutes"
148
+ Audio::Stream.new(sink, @computer.sid,
149
+ on_underrun: -> { puts "Running below real time, so the sound will stutter." })
150
+ rescue Audio::SDLSink::Error => e
151
+ warn "badline: no sound, can't open the audio device: #{e.message}"
152
+ end
153
+
154
+ def fit_frame
155
+ rate = @window.refresh_rate
156
+ @cycles_per_frame = PAL_CLOCK_HZ / rate
157
+ @frame_seconds = @cycles_per_frame.fdiv(PAL_CLOCK_HZ)
158
+ puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
127
159
  end
128
160
 
129
161
  def attach_pot_device