badline 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) 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 +65 -0
  5. data/CLAUDE.md +45 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +111 -61
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +275 -33
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +22 -10
  12. data/lib/badline/audio/aiff.rb +8 -3
  13. data/lib/badline/audio/cli.rb +17 -12
  14. data/lib/badline/audio/console.rb +12 -58
  15. data/lib/badline/audio/playback.rb +3 -0
  16. data/lib/badline/audio/sdl_sink.rb +20 -52
  17. data/lib/badline/audio/terminal.rb +90 -0
  18. data/lib/badline/audio.rb +2 -2
  19. data/lib/badline/cartridge/geo_ram.rb +63 -0
  20. data/lib/badline/cartridge.rb +1 -0
  21. data/lib/badline/cia/interrupt_register.rb +34 -10
  22. data/lib/badline/cia.rb +15 -8
  23. data/lib/badline/computer.rb +28 -13
  24. data/lib/badline/gui/application.rb +33 -21
  25. data/lib/badline/gui/gamepads.rb +22 -22
  26. data/lib/badline/gui/joy_map.rb +1 -1
  27. data/lib/badline/gui/key_map.rb +6 -1
  28. data/lib/badline/gui/pane.rb +7 -6
  29. data/lib/badline/gui/screen_pane.rb +9 -13
  30. data/lib/badline/gui/window.rb +35 -20
  31. data/lib/badline/gui.rb +1 -1
  32. data/lib/badline/input/mouse1351.rb +21 -6
  33. data/lib/badline/input/paddles.rb +11 -6
  34. data/lib/badline/kernal_trap/channel.rb +3 -0
  35. data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
  36. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  37. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  38. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  39. data/lib/badline/kernal_trap/drive.rb +51 -51
  40. data/lib/badline/kernal_trap/file.rb +6 -9
  41. data/lib/badline/kernal_trap/save.rb +9 -4
  42. data/lib/badline/keyboard.rb +19 -3
  43. data/lib/badline/media.rb +42 -11
  44. data/lib/badline/options.rb +209 -0
  45. data/lib/badline/ram_expansion/banking.rb +30 -0
  46. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  47. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  48. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  49. data/lib/badline/ram_expansion.rb +21 -0
  50. data/lib/badline/region.rb +51 -0
  51. data/lib/badline/sdl/events.rb +47 -0
  52. data/lib/badline/sdl/functions.rb +73 -0
  53. data/lib/badline/sdl.rb +67 -0
  54. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  55. data/lib/badline/sid/waveform.rb +1 -1
  56. data/lib/badline/sid.rb +3 -1
  57. data/lib/badline/storage/d64_image.rb +10 -0
  58. data/lib/badline/storage/d71_image.rb +12 -0
  59. data/lib/badline/storage/d81_image.rb +9 -0
  60. data/lib/badline/storage/disk_image/bam.rb +111 -0
  61. data/lib/badline/storage/disk_image/directory.rb +79 -0
  62. data/lib/badline/storage/disk_image/writing.rb +110 -0
  63. data/lib/badline/storage/disk_image.rb +27 -4
  64. data/lib/badline/storage/host_directory.rb +2 -1
  65. data/lib/badline/storage.rb +15 -0
  66. data/lib/badline/time_of_day.rb +4 -2
  67. data/lib/badline/version.rb +1 -1
  68. data/lib/badline/via/control_lines.rb +104 -0
  69. data/lib/badline/via/interrupt_register.rb +58 -0
  70. data/lib/badline/via/shift_register.rb +135 -0
  71. data/lib/badline/via/timer1.rb +82 -0
  72. data/lib/badline/via/timer2.rb +84 -0
  73. data/lib/badline/via.rb +262 -0
  74. data/lib/badline/vic/bank.rb +4 -3
  75. data/lib/badline/vic/color_patches.rb +6 -3
  76. data/lib/badline/vic/display_state.rb +4 -0
  77. data/lib/badline/vic/graphics_shifter.rb +42 -7
  78. data/lib/badline/vic/registers.rb +3 -0
  79. data/lib/badline/vic/sequencer.rb +18 -18
  80. data/lib/badline/vic/sequencer_output.rb +4 -4
  81. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  82. data/lib/badline/vic/sprite/shifter.rb +25 -3
  83. data/lib/badline/vic/sprite.rb +5 -4
  84. data/lib/badline/vic/sprites.rb +8 -3
  85. data/lib/badline/vic.rb +93 -44
  86. data/lib/badline.rb +4 -0
  87. data/packaging/homebrew/badline.rb +34 -0
  88. metadata +30 -20
  89. data/exe/badline +0 -78
  90. data/exe/badline-sid +0 -34
  91. data/lib/badline/audio/options.rb +0 -137
@@ -5,70 +5,24 @@ require "io/wait"
5
5
 
6
6
  module Badline
7
7
  module Audio
8
- # The terminal face of `badline-sid`: the tune's header, a status line
9
- # redrawn in place, and single keypresses read without waiting for
10
- # Return. Ctrl-C still interrupts.
11
- class Console
12
- KEYS = {
13
- "n" => :next, "\e[C" => :next,
14
- "p" => :previous, "\e[D" => :previous,
15
- " " => :pause,
16
- "q" => :quit
17
- }.freeze
18
-
19
- def initialize(input:, output:)
20
- @input = input
21
- @output = output
22
- @closed = false
23
- end
24
-
25
- # Raw input and a hidden cursor for the length of the block.
26
- def session(&)
27
- @output.print "\e[?25l"
28
- @input.tty? ? @input.raw(intr: true, &) : yield
29
- ensure
30
- @output.print "\e[?25h\r\n"
31
- end
32
-
33
- # Waits up to `seconds` for keys and returns what they ask for.
34
- def wait(seconds)
35
- return idle(seconds) if @closed
36
- return [] unless @input.wait_readable(seconds)
37
-
38
- case (keys = @input.read_nonblock(64, exception: false))
39
- when :wait_readable then []
40
- when nil then close_input(seconds)
41
- else keys.scan(/\e\[[A-D]|./m).filter_map { |key| KEYS[key] }
42
- end
43
- end
44
-
45
- def header(lines)
46
- lines.each { |line| @output.print "#{line}\r\n" }
47
- @output.print "n/→ next p/← previous space pause q quit\r\n"
48
- end
49
-
50
- def status(song:, songs:, elapsed:, length:, notes: [])
51
- line = format("song %<song>d/%<songs>d %<elapsed>s / %<length>s",
52
- song:, songs:, elapsed: clock(elapsed), length: clock(length))
53
- @output.print "\r\e[K#{([line] + notes).join(' ')}"
54
- @output.flush
55
- end
56
-
8
+ # The terminal of `badline-ruby --headless`, put in raw mode with
9
+ # io/console.
10
+ class Console < Terminal
57
11
  private
58
12
 
59
- def close_input(seconds)
60
- @closed = true
61
- idle(seconds)
13
+ # Ctrl-C still raises Interrupt.
14
+ def raw!
15
+ mode = @input.console_mode
16
+ @input.raw!(intr: true)
17
+ mode
62
18
  end
63
19
 
64
- def idle(seconds)
65
- sleep(seconds)
66
- []
20
+ def restore(mode)
21
+ @input.console_mode = mode
67
22
  end
68
23
 
69
- def clock(seconds)
70
- whole = seconds.floor
71
- format("%<minutes>d:%<seconds>02d", minutes: whole / 60, seconds: whole % 60)
24
+ def readable?(seconds)
25
+ !@input.wait_readable(seconds).nil?
72
26
  end
73
27
  end
74
28
  end
@@ -12,6 +12,9 @@ module Badline
12
12
  # real time lets the queue run dry and the audio stutters; `on_underrun`
13
13
  # hears about the first time.
14
14
  class Playback
15
+ # What a sink raises when the audio device won't open.
16
+ class DeviceError < StandardError; end
17
+
15
18
  AHEAD = 0.15
16
19
 
17
20
  def initialize(sink, ahead: AHEAD, sleeper: ->(seconds) { sleep(seconds) }, on_underrun: nil)
@@ -4,98 +4,66 @@ module Badline
4
4
  module Audio
5
5
  # The host's audio device, fed through SDL's queue: mono signed 16-bit
6
6
  # samples go in at the device's rate and SDL plays them out behind us.
7
- #
8
- # ruby-sdl2 only wraps SDL_mixer, which can't stream raw samples, so the
9
- # queue API is called through Fiddle on the libSDL2 the extension has
10
- # already loaded.
11
7
  class SDLSink
12
- class Error < StandardError; end
13
-
14
- INIT_AUDIO = 0x10
15
-
16
- AUDIO_S16SYS = [1].pack("s").getbyte(0) == 1 ? 0x8010 : 0x9010
17
-
18
- ALLOW_FREQUENCY_CHANGE = 0x01
8
+ class Error < Playback::DeviceError; end
19
9
 
20
10
  # int freq; Uint16 format; Uint8 channels, silence; Uint16 samples,
21
11
  # padding; Uint32 size; then the callback and userdata pointers.
22
12
  SPEC_LAYOUT = "lSCCSSLQQ"
23
13
 
24
- FUNCTIONS = {
25
- SDL_InitSubSystem: [%i[uint32], :int],
26
- SDL_QuitSubSystem: [%i[uint32], :void],
27
- SDL_GetError: [[], :string],
28
- SDL_OpenAudioDevice: [%i[pointer int pointer pointer int], :uint32],
29
- SDL_CloseAudioDevice: [%i[uint32], :void],
30
- SDL_PauseAudioDevice: [%i[uint32 int], :void],
31
- SDL_QueueAudio: [%i[uint32 pointer uint32], :int],
32
- SDL_GetQueuedAudioSize: [%i[uint32], :uint32],
33
- SDL_ClearQueuedAudio: [%i[uint32], :void]
34
- }.freeze
35
-
36
14
  attr_reader :rate
37
15
 
38
16
  # With `exact_rate` false SDL may pick the device's own rate instead,
39
17
  # which #rate then reports.
40
18
  def initialize(rate:, exact_rate: false, buffer: 1024)
41
19
  load_sdl
42
- check(sdl(:SDL_InitSubSystem, INIT_AUDIO))
43
- @device = open_device(rate, exact_rate ? 0 : ALLOW_FREQUENCY_CHANGE, buffer)
20
+ check(SDL::InitSubSystem.call(SDL::INIT_AUDIO))
21
+ @device = open_device(rate, exact_rate ? 0 : SDL::AUDIO_ALLOW_FREQUENCY_CHANGE, buffer)
44
22
  end
45
23
 
46
24
  def queue(samples)
47
25
  data = samples.pack("s*")
48
- check(sdl(:SDL_QueueAudio, @device, data, data.bytesize))
26
+ check(SDL::QueueAudio.call(@device, data, data.bytesize))
49
27
  end
50
28
 
51
- def queued_seconds = sdl(:SDL_GetQueuedAudioSize, @device).fdiv(2 * rate)
29
+ def queued_seconds = SDL::GetQueuedAudioSize.call(@device).fdiv(2 * rate)
52
30
 
53
- def start = sdl(:SDL_PauseAudioDevice, @device, 0)
31
+ def start = SDL::PauseAudioDevice.call(@device, 0)
54
32
 
55
- def pause = sdl(:SDL_PauseAudioDevice, @device, 1)
33
+ def pause = SDL::PauseAudioDevice.call(@device, 1)
56
34
 
57
- def clear = sdl(:SDL_ClearQueuedAudio, @device)
35
+ def clear = SDL::ClearQueuedAudio.call(@device)
58
36
 
59
37
  def close
60
38
  return unless @device
61
39
 
62
- sdl(:SDL_CloseAudioDevice, @device)
63
- sdl(:SDL_QuitSubSystem, INIT_AUDIO)
40
+ SDL::CloseAudioDevice.call(@device)
41
+ SDL::QuitSubSystem.call(SDL::INIT_AUDIO)
64
42
  @device = nil
65
43
  end
66
44
 
67
45
  private
68
46
 
69
47
  def open_device(rate, allowed_changes, buffer)
70
- wanted = [rate, AUDIO_S16SYS, 1, 0, buffer, 0, 0, 0, 0].pack(SPEC_LAYOUT)
48
+ wanted = [rate, SDL::AUDIO_S16SYS, 1, 0, buffer, 0, 0, 0, 0].pack(SPEC_LAYOUT)
71
49
  obtained = "\0".b * 32
72
- device = sdl(:SDL_OpenAudioDevice, nil, 0, wanted, obtained, allowed_changes)
73
- raise Error, sdl(:SDL_GetError) if device.zero?
50
+ device = SDL::OpenAudioDevice.call(nil, 0, wanted, obtained, allowed_changes)
51
+ raise Error, SDL.error if device.zero?
74
52
 
75
53
  @rate = obtained.unpack1("l")
76
54
  device
77
55
  end
78
56
 
79
- def check(result)
80
- raise Error, sdl(:SDL_GetError) if result.negative?
81
-
82
- result
83
- end
84
-
85
- def sdl(name, *) = @functions.fetch(name).call(*)
86
-
87
57
  def load_sdl
88
- require "fiddle"
89
- require "sdl2"
90
- @functions = FUNCTIONS.to_h do |name, (arguments, result)|
91
- [name, Fiddle::Function.new(Fiddle::Handle::DEFAULT[name.to_s],
92
- arguments.map { |type| fiddle_type(type) }, fiddle_type(result))]
93
- end
58
+ require "badline/sdl"
59
+ rescue SDL::Error => e
60
+ raise Error, e.message
94
61
  end
95
62
 
96
- def fiddle_type(type)
97
- { uint32: Fiddle::TYPE_UINT32_T, int: Fiddle::TYPE_INT, void: Fiddle::TYPE_VOID,
98
- pointer: Fiddle::TYPE_VOIDP, string: Fiddle::TYPE_CONST_STRING }.fetch(type)
63
+ def check(result)
64
+ raise Error, SDL.error if result.negative?
65
+
66
+ result
99
67
  end
100
68
  end
101
69
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Audio
5
+ # The terminal face of `--headless`: the tune's header, a status line
6
+ # redrawn in place, and single keypresses read without waiting for
7
+ # Return. Ctrl-C still interrupts. A subclass puts the terminal in raw
8
+ # mode with #raw! and #restore and waits for keys with #readable?:
9
+ # Console with io/console for badline-ruby, and Native::Console with
10
+ # stty and poll(2) for the native badline.
11
+ class Terminal
12
+ KEYS = {
13
+ "n" => :next, "\e[C" => :next,
14
+ "p" => :previous, "\e[D" => :previous,
15
+ " " => :pause,
16
+ "q" => :quit
17
+ }.freeze
18
+
19
+ def initialize(input:, output:)
20
+ @input = input
21
+ @output = output
22
+ @closed = false
23
+ end
24
+
25
+ # Raw input and a hidden cursor for the length of the block.
26
+ def session
27
+ @output.print "\e[?25l"
28
+ mode = raw! if @input.tty?
29
+ yield
30
+ ensure
31
+ restore(mode) if mode
32
+ @output.print "\e[?25h\r\n"
33
+ end
34
+
35
+ # Waits up to `seconds` for keys and returns what they ask for.
36
+ def wait(seconds)
37
+ return idle(seconds) if @closed
38
+ return [] unless readable?(seconds)
39
+
40
+ case (keys = @input.read_nonblock(64, exception: false))
41
+ when :wait_readable then []
42
+ when nil then close_input(seconds)
43
+ else keys.scan(/\e\[[A-D]|./m).filter_map { |key| KEYS[key] }
44
+ end
45
+ end
46
+
47
+ def header(lines)
48
+ lines.each { |line| @output.print "#{line}\r\n" }
49
+ @output.print "n/→ next p/← previous space pause q quit\r\n"
50
+ end
51
+
52
+ def status(song:, songs:, elapsed:, length:, notes: [])
53
+ line = format("song %<song>d/%<songs>d %<elapsed>s / %<length>s",
54
+ song:, songs:, elapsed: clock(elapsed), length: clock(length))
55
+ @output.print "\r\e[K#{([line] + notes).join(' ')}"
56
+ @output.flush
57
+ end
58
+
59
+ private
60
+
61
+ # A subclass puts the terminal in raw mode and returns the mode to
62
+ # restore afterwards.
63
+ def raw! = nil
64
+
65
+ def restore(mode) = mode
66
+
67
+ # A subclass waits up to `seconds` for input and says whether any
68
+ # came.
69
+ def readable?(seconds)
70
+ sleep(seconds)
71
+ false
72
+ end
73
+
74
+ def close_input(seconds)
75
+ @closed = true
76
+ idle(seconds)
77
+ end
78
+
79
+ def idle(seconds)
80
+ sleep(seconds)
81
+ []
82
+ end
83
+
84
+ def clock(seconds)
85
+ whole = seconds.floor
86
+ format("%<minutes>d:%<seconds>02d", minutes: whole / 60, seconds: whole % 60)
87
+ end
88
+ end
89
+ end
90
+ end
data/lib/badline/audio.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "badline/audio/options"
4
3
  require "badline/audio/pcm_writer"
5
4
  require "badline/audio/wav"
6
5
  require "badline/audio/aiff"
7
6
  require "badline/audio/bare_player"
8
7
  require "badline/audio/machine_player"
9
8
  require "badline/audio/renderer"
10
- require "badline/audio/sdl_sink"
11
9
  require "badline/audio/playback"
10
+ require "badline/audio/sdl_sink"
12
11
  require "badline/audio/stream"
12
+ require "badline/audio/terminal"
13
13
  require "badline/audio/console"
14
14
  require "badline/audio/jukebox"
15
15
  require "badline/audio/cli"
@@ -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,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
@@ -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
 
@@ -43,9 +47,15 @@ module Badline
43
47
  @tb.latch = value
44
48
  end
45
49
 
46
- 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
+
47
53
  addressable_at(start, length: 2**8)
48
54
 
55
+ @model = model
56
+ @clock_hz = region.clock_hz
57
+ @mains_hz = region.mains_hz
58
+
49
59
  @peripheral = peripheral
50
60
  @port_b4_handler = nil
51
61
  @port_b4_high = true
@@ -63,8 +73,8 @@ module Badline
63
73
  @port_b4_driven_high = true
64
74
  @cnt_high = true
65
75
  @cnt_rise = false
66
- @tod = TimeOfDay.new
67
- @icr = InterruptRegister.new
76
+ @tod = TimeOfDay.new(clock_hz: @clock_hz, mains_hz: @mains_hz)
77
+ @icr = InterruptRegister.new(@model)
68
78
  @icr_status = @icr.status
69
79
  @control_a = ControlRegister.new(%i[start output out_mode run_mode load
70
80
  in_mode serial_mode clock_frequency])
@@ -223,10 +233,7 @@ module Badline
223
233
  else underflowed && @cnt_high
224
234
  end
225
235
  )
226
- if underflowed
227
- interrupt_status.timer_a = true
228
- interrupt! if interrupt_control.timer_a?
229
- end
236
+ @icr.flag(:timer_a) if underflowed
230
237
  @icr.timer_b_underflow! if @tb.underflowed
231
238
  underflowed
232
239
  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,6 +47,7 @@ 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 = []
@@ -94,10 +101,10 @@ module Badline
94
101
  end
95
102
 
96
103
  # 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.
104
+ # cartridge port, a RAM expansion and, through the serial bus's RESET
105
+ # line, the drive. The VIC has no reset pin.
99
106
  def reset!
100
- address_bus.reset_port!
107
+ address_bus.reset!
101
108
  @cia1.reset!
102
109
  @cia2.reset!
103
110
  @sid.reset!
@@ -123,15 +130,22 @@ module Badline
123
130
  address_bus.cartridge&.release_button
124
131
  end
125
132
 
126
- # A disk swap keeps the drive's RAM, which only a drive reset clears.
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.
127
142
  def mount(storage)
128
- @drive_memory ||= KernalTrap::Drive::Memory.new
129
- @drive = KernalTrap::Drive.new(storage, @drive_memory)
143
+ return @drive.insert(storage) if @drive
144
+
145
+ @drive = KernalTrap::Drive.new(storage)
130
146
  load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
131
147
  cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
132
148
  KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
133
- return unless storage.respond_to?(:write_file)
134
-
135
149
  save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
136
150
  cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
137
151
  end
@@ -156,10 +170,11 @@ module Badline
156
170
 
157
171
  private
158
172
 
159
- # The NMI line is wired-OR between CIA 2 and the cartridge, and the CPU
160
- # 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.
161
175
  def drive_nmi
162
- nmi = @cia2.interrupted? || @cartridge_nmi
176
+ nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
177
+ @restore_pulse = false
163
178
  @cpu.nmi = true if nmi && !@nmi_asserted
164
179
  @nmi_asserted = nmi
165
180
  end