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
@@ -2,7 +2,9 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The SID's output on SDL's audio queue, as mono signed 16-bit samples.
5
+ # The machine's sound source, the C64's SID or the VIC-20's VIC, on
6
+ # SDL's audio queue, as mono signed 16-bit samples. Both take
7
+ # #record(rate:, clock_hz:) and #drain_samples.
6
8
  # Once the device plays, it is the clock: #wait holds each frame until
7
9
  # the queue is down to `AHEAD`, so the machine runs at the device's pace.
8
10
  #
@@ -13,6 +15,8 @@ module Badline
13
15
  # only happens unpaced.
14
16
  #
15
17
  # Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
18
+ # The chip's cycles are converted at `clock_hz`, the machine's clock, so
19
+ # an NTSC or Drean machine plays at its own pitch.
16
20
  class Sound
17
21
  RATE = 44_100
18
22
  AHEAD = 0.08
@@ -20,8 +24,9 @@ module Badline
20
24
 
21
25
  attr_reader :rate, :underruns, :dropped, :queued, :low, :high
22
26
 
23
- def initialize(sid, wanted, verbose)
24
- @sid = sid
27
+ def initialize(source, clock_hz, wanted, verbose)
28
+ @source = source
29
+ @clock_hz = clock_hz
25
30
  @verbose = verbose
26
31
  @device = 0
27
32
  @rate = RATE
@@ -37,10 +42,11 @@ module Badline
37
42
 
38
43
  def on? = @device != 0
39
44
 
40
- # Plays another machine's SID from here on.
41
- def sid=(sid)
42
- @sid = sid
43
- sid.record(rate: @rate) if on?
45
+ # Plays another machine's sound from here on, at that machine's clock.
46
+ def switch(source, clock_hz)
47
+ @source = source
48
+ @clock_hz = clock_hz
49
+ record if on?
44
50
  end
45
51
 
46
52
  def muted? = @muted
@@ -50,7 +56,7 @@ module Badline
50
56
  def feed
51
57
  return unless on?
52
58
 
53
- samples = @sid.drain_samples
59
+ samples = @source.drain_samples
54
60
  return if @muted || samples.empty?
55
61
 
56
62
  level = queued_seconds
@@ -102,10 +108,12 @@ module Badline
102
108
  return puts "No sound: #{SDL.SDL_GetError}" unless on?
103
109
 
104
110
  @rate = SDL.read_i32(SDL.obtained)
105
- @sid.record(rate: @rate)
111
+ record
106
112
  puts "Sound at #{@rate} Hz" if @verbose
107
113
  end
108
114
 
115
+ def record = @source.record(rate: @rate, clock_hz: @clock_hz)
116
+
109
117
  def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
110
118
 
111
119
  def play(samples, level)
@@ -21,13 +21,20 @@ module Badline
21
21
  @failed = false
22
22
  end
23
23
 
24
- # What's wrong with the events, the first key that names nothing,
25
- # or an empty string.
24
+ # What's wrong with the events, the first key or menu page that
25
+ # names nothing, or an empty string.
26
26
  def self.error(events)
27
27
  event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument) }
28
- event.nil? ? "" : "no such key: #{event.argument}"
28
+ return "no such key: #{event.argument}" unless event.nil?
29
+
30
+ event = events.find { |candidate| candidate.action == "menu" && !page?(candidate.argument) }
31
+ event.nil? ? "" : "no such menu page: #{event.argument}"
29
32
  end
30
33
 
34
+ # Whether the pause menu has a page of the name, such as drive or
35
+ # sound. An empty one is its first.
36
+ def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
37
+
31
38
  def self.key?(name) = name == "restore" || !keyboard_key(name).nil? || !joystick_direction(name).nil?
32
39
 
33
40
  # The Keyboard's key for a name such as space, return or a.
@@ -67,6 +74,22 @@ module Badline
67
74
  # events do.
68
75
  def press_freeze(computer, frame) = freeze(computer, Options::Event.new(frame, "freeze", ""))
69
76
 
77
+ # Whether the pause menu opens at the frame, or turns to another page.
78
+ def menu?(frame) = @events.any? { |event| event.frame == frame && event.action == "menu" }
79
+
80
+ # The index in PauseMenu::PAGES of the page the frame's menu event
81
+ # names, 0 for the first.
82
+ def menu_section(frame)
83
+ page = ""
84
+ @events.each { |event| page = event.argument if event.frame == frame && event.action == "menu" }
85
+ section = 0
86
+ PauseMenu::PAGES.each_with_index { |name, index| section = index if name.to_s == page }
87
+ section
88
+ end
89
+
90
+ # Whether the pause menu closes at the frame.
91
+ def resume?(frame) = @events.any? { |event| event.frame == frame && event.action == "resume" }
92
+
70
93
  # Whether the run ends at the frame: at a quit, or once an event failed.
71
94
  def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
72
95
 
@@ -98,7 +121,7 @@ module Badline
98
121
  name = event.argument
99
122
  if name == "restore"
100
123
  computer.press_restore
101
- return
124
+ return hold(event)
102
125
  end
103
126
 
104
127
  direction = Timeline.joystick_direction(name)
@@ -110,7 +133,10 @@ module Badline
110
133
  hold(event)
111
134
  end
112
135
 
136
+ # The VIC-20 has no cartridge with a button yet.
113
137
  def freeze(computer, event)
138
+ return if computer.family == :vic20
139
+
114
140
  computer.press_cartridge_button
115
141
  hold(event)
116
142
  end
@@ -123,6 +149,8 @@ module Badline
123
149
  return computer.release_cartridge_button if event.action == "freeze"
124
150
 
125
151
  name = event.argument
152
+ return computer.release_restore if name == "restore"
153
+
126
154
  direction = Timeline.joystick_direction(name)
127
155
  if direction.nil?
128
156
  computer.keyboard.release(Timeline.keyboard_key(name))
@@ -141,7 +169,7 @@ module Badline
141
169
  end
142
170
 
143
171
  def eject(computer, what)
144
- return puts "No #{what} to eject" unless inserted?(computer, what)
172
+ return puts "No #{what} to eject" unless ejectable?(computer, what)
145
173
 
146
174
  if what == "disk"
147
175
  eject_disk(computer)
@@ -154,6 +182,9 @@ module Badline
154
182
  puts "Ejected the #{what}"
155
183
  end
156
184
 
185
+ # Whether there's one in to eject. A VIC-20 has no cartridge to take out yet.
186
+ def ejectable?(computer, what) = (computer.family == :c64 || what != "cartridge") && inserted?(computer, what)
187
+
157
188
  def inserted?(computer, what)
158
189
  return !computer.datasette.tape.nil? if what == "tape"
159
190
  return !computer.address_bus.cartridge.nil? if what == "cartridge"
@@ -15,6 +15,7 @@ require "badline/frontend/frame_report"
15
15
  require "badline/frontend/controls"
16
16
  require "badline/frontend/pad_port"
17
17
  require "badline/frontend/gamepads"
18
+ require "badline/frontend/machine_window"
18
19
  require "badline/frontend/app"
19
20
  require "badline/frontend/audio_sink"
20
21
  require "badline/frontend/painter"
@@ -34,6 +35,8 @@ require "badline/frontend/player_window"
34
35
  require "badline/frontend/pause_menu"
35
36
  require "badline/frontend/file_browser"
36
37
  require "badline/frontend/confirmation"
38
+ require "badline/frontend/name_field"
37
39
  require "badline/frontend/menu_dialogs"
38
40
  require "badline/frontend/menu_media"
41
+ require "badline/frontend/menu_ports"
39
42
  require "badline/frontend/menu_pages"
@@ -3,13 +3,14 @@
3
3
  module Badline
4
4
  # The serial bus between the C64 and its drives: ATN, CLK and DATA, each
5
5
  # an open-collector line with a pull-up. A line is low while any device
6
- # pulls it (wired-AND), and high otherwise. The bus holds no state of its
7
- # own: every read works the levels out from what each side drives now.
6
+ # pulls it (wired-AND), and high otherwise. Every read works the levels
7
+ # out from what each side drives now.
8
8
  #
9
9
  # The C64 drives the lines from CIA 2's port A through 7406 inverters, so
10
10
  # an output bit of 1 pulls its line low: PA3 ATN, PA4 CLK and PA5 DATA.
11
- # PA6 and PA7 read CLK and DATA directly, 1 for a released line. The bus
12
- # stands in as CIA 2's peripheral for those two inputs.
11
+ # The machine pushes those bits into the bus (host_lines=), which holds
12
+ # them. PA6 and PA7 read CLK and DATA directly, 1 for a released line.
13
+ # The bus stands in as CIA 2's peripheral for those two inputs.
13
14
  #
14
15
  # A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
15
16
  # also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
@@ -33,30 +34,21 @@ module Badline
33
34
  CLK = 0x02
34
35
  DATA = 0x04
35
36
 
36
- attr_reader :host, :drives
37
+ attr_reader :drives, :host_lines
37
38
 
38
- # +host+ is CIA 2, or nil for a bus with only drives on it.
39
- def initialize(host: nil)
40
- @host = host
39
+ def initialize
41
40
  @drives = []
42
- @notifies = false
41
+ @host_lines = 0
43
42
  end
44
43
 
45
- # Stands in as the host's peripheral, CIA 2's, and has it say when it
46
- # writes port A (see notifies?).
47
- def plug_in!
48
- @host.peripheral = self
49
- @host.on_port_a_write { host_written }
50
- @notifies = true
44
+ # The machine pushes the lines it pulls, as port A bits, whenever they
45
+ # may have moved: its output bits, with input bits floating high as the
46
+ # 7406 inputs see them. A bus with only drives on it keeps 0.
47
+ def host_lines=(lines)
48
+ @host_lines = lines
49
+ @drives.each(&:host_written!)
51
50
  end
52
51
 
53
- # Whether the host says when it may have moved a line (see plug_in!).
54
- # Without, a drive asleep checks ATN every cycle.
55
- def notifies? = @notifies
56
-
57
- # CIA 2 wrote its port A, which may have moved a line.
58
- def host_written = @drives.each(&:host_written!)
59
-
60
52
  def attach(drive)
61
53
  @drives << drive unless @drives.include?(drive)
62
54
  end
@@ -68,7 +60,7 @@ module Badline
68
60
  # One pass over everything on the bus, for a reader that wants more
69
61
  # than one line. +host+ is the C64's port A lines as the reader sees
70
62
  # them, which a drive takes a cycle late (see Drive1541::SerialPort).
71
- def low_lines(host = host_lines)
63
+ def low_lines(host = @host_lines)
72
64
  atn = host.anybits?(HOST_ATN_OUT)
73
65
  low = atn ? ATN : 0
74
66
  low |= CLK if host.anybits?(HOST_CLK_OUT)
@@ -83,7 +75,7 @@ module Badline
83
75
  low
84
76
  end
85
77
 
86
- def atn_low?(host = host_lines) = host.anybits?(HOST_ATN_OUT)
78
+ def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
87
79
 
88
80
  def clk_low? = low_lines.anybits?(CLK)
89
81
 
@@ -101,9 +93,5 @@ module Badline
101
93
 
102
94
  # Port B goes to the user port, not the serial bus.
103
95
  def read_b(_port_a, _port_b) = 0xff
104
-
105
- # What the C64 pulls, as port A bits: its output bits, with input bits
106
- # floating high as the 7406 inputs see them.
107
- def host_lines = @host ? @host.port_a_lines : 0
108
96
  end
109
97
  end
@@ -84,6 +84,9 @@ module Badline
84
84
  @memory.read_bam
85
85
  end
86
86
 
87
+ # The path of the disk or directory in the drive.
88
+ def path = @storage.path
89
+
87
90
  # Whether the disk takes a SAVE handed over whole.
88
91
  def saves? = @storage.respond_to?(:write_file)
89
92
 
@@ -6,7 +6,7 @@ module Badline
6
6
  private
7
7
 
8
8
  def active?
9
- kernal? && @bus.peek(0xba) == DEVICE
9
+ kernal_mapped? && @bus.peek(0xba) == DEVICE
10
10
  end
11
11
 
12
12
  def filename
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # Where a machine's KERNAL keeps the routines the traps stand in for,
6
+ # and the ROM code they hand over to, with the steps that depend on the
7
+ # machine around it: whether the KERNAL is mapped in, the timer that
8
+ # times each serial byte and the release of the serial lines.
9
+ #
10
+ # +load+ and +save+ are the default ILOAD and ISAVE vector targets.
11
+ # LOAD hands over to the SEARCHING FOR and LOADING/VERIFYING messages,
12
+ # the byte loop that retries a timed-out byte until RUN/STOP, the
13
+ # successful return (CLC, LDX $AE, LDY $AF, RTS) and the FILE NOT FOUND
14
+ # and MISSING FILE NAME error exits. SAVE hands over to the SAVING
15
+ # message, the two line releases that end UNLISTEN (clock, then data,
16
+ # leaving A as read from the port), the successful return (CLC, RTS)
17
+ # and the MISSING FILE NAME exit. The rest are the serial bus
18
+ # primitives' entry points.
19
+ #
20
+ # +banked+ says whether the 6510 port can bank the KERNAL out, and so
21
+ # whether RAM lies under every ROM and the I/O. Each
22
+ # serial byte starts a one-shot timer by writing the high byte of its
23
+ # count to +serial_timer+, and the control value TIMER_ONE_SHOT_START
24
+ # to +serial_timer_start+ where the timer has a control register to
25
+ # start it (a CIA's), or nil where the write to the high byte starts it
26
+ # (a VIA's timer 2). The ROM lets go of each serial line by reading a
27
+ # port, masking the line's bit off and writing it back: ATN at
28
+ # +atn_port+ with +atn_mask+, then the clock and the data line.
29
+ Layout = Data.define(:load, :save, :searching_message, :loading_message, :load_byte_loop, :load_done,
30
+ :file_not_found_exit, :missing_file_name_exit, :saving_message, :clock_release,
31
+ :data_release, :save_done, :talk, :listen, :second, :tksa, :ciout, :untalk,
32
+ :unlisten, :acptr, :banked, :serial_timer, :serial_timer_start, :atn_port, :atn_mask,
33
+ :clock_port, :clock_mask, :data_port, :data_mask) do
34
+ # Whether the CPU reads the KERNAL ROM at the trapped addresses: on a
35
+ # banked machine, the 6510 port's banking bits select it.
36
+ def kernal_mapped?(bus)
37
+ !banked || bus.io_port.kernal?
38
+ end
39
+
40
+ # Stores a loaded file's bytes from +address+ on: on a banked machine
41
+ # into the RAM under whatever is banked in, and otherwise where the
42
+ # ROM's byte loop would store them, through the bus.
43
+ def store_file(bus, address, bytes)
44
+ return bus.ram.write(address, bytes) if banked
45
+
46
+ bytes.each_with_index { |byte, i| bus.poke(address + i, byte) }
47
+ end
48
+
49
+ # Starts the timer the ROM times a serial byte with, its count's high
50
+ # byte +timer_high+.
51
+ def time_serial_byte(bus, timer_high)
52
+ bus.poke(serial_timer, timer_high)
53
+ start = serial_timer_start
54
+ bus.poke(start, TIMER_ONE_SHOT_START) if start
55
+ end
56
+
57
+ # The ROM's release of the serial bus: ATN, then the clock, then the
58
+ # data line, each a read of its port, a mask and a write back, so the
59
+ # input bits written back follow the lines as each one is let go.
60
+ # Returns the last value written, which the ROM leaves in A.
61
+ def release_serial_lines(bus)
62
+ release_line(bus, atn_port, atn_mask)
63
+ release_line(bus, clock_port, clock_mask)
64
+ release_line(bus, data_port, data_mask)
65
+ end
66
+
67
+ private
68
+
69
+ def release_line(bus, port, mask)
70
+ value = bus.peek(port) & mask
71
+ bus.poke(port, value)
72
+ value
73
+ end
74
+ end
75
+
76
+ # A CIA's control value that starts its timer: force load, one-shot,
77
+ # start.
78
+ TIMER_ONE_SHOT_START = 0x19
79
+
80
+ # The C64 KERNAL, revision 3. The SX-64's, 251104-04, differs from it
81
+ # only in bytes none of these addresses reach. It times serial bytes
82
+ # with CIA 1's timer B, and drives the lines from CIA 2's port A
83
+ # ($EE03: ATN $EDBE, clock $EE85, data $EE97).
84
+ C64_LAYOUT = Layout.new(
85
+ load: 0xf4a5, save: 0xf5ed,
86
+ searching_message: 0xf5af, loading_message: 0xf5d2, load_byte_loop: 0xf4f3, load_done: 0xf5a9,
87
+ file_not_found_exit: 0xf704, missing_file_name_exit: 0xf710,
88
+ saving_message: 0xf68f, clock_release: 0xee85, data_release: 0xee97, save_done: 0xf657,
89
+ talk: 0xed09, listen: 0xed0c, second: 0xedb9, tksa: 0xedc7,
90
+ ciout: 0xeddd, untalk: 0xedef, unlisten: 0xedfe, acptr: 0xee13,
91
+ banked: true, serial_timer: 0xdc07, serial_timer_start: 0xdc0f,
92
+ atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf
93
+ )
94
+
95
+ # The VIC-20 KERNAL, 901486-07 (PAL). The NTSC one, 901486-06, differs
96
+ # from it only at $E475, $EDE4-$EDE5, $FE3F, $FE44 and $FF5C-$FF70,
97
+ # none of them here. Nothing banks it out. It times serial bytes with
98
+ # VIA 2's timer 2 ($9129), drives ATN from VIA 1's PA7 ($911F) and the
99
+ # clock and data lines from VIA 2's CA2 and CB2 ($912C), and UNLISTEN
100
+ # lets them go at $EF09: ATN $EEC5, clock $EF84, data $E4A0.
101
+ VIC20_LAYOUT = Layout.new(
102
+ load: 0xf549, save: 0xf685,
103
+ searching_message: 0xf647, loading_message: 0xf66a, load_byte_loop: 0xf58a, load_done: 0xf641,
104
+ file_not_found_exit: 0xf787, missing_file_name_exit: 0xf793,
105
+ saving_message: 0xf728, clock_release: 0xef84, data_release: 0xe4a0, save_done: 0xf6ef,
106
+ talk: 0xee14, listen: 0xee17, second: 0xeec0, tksa: 0xeece,
107
+ ciout: 0xeee4, untalk: 0xeef6, unlisten: 0xef04, acptr: 0xef19,
108
+ banked: false, serial_timer: 0x9129, serial_timer_start: nil,
109
+ atn_port: 0x911f, atn_mask: 0x7f, clock_port: 0x912c, clock_mask: 0xfd, data_port: 0x912c, data_mask: 0xdf
110
+ )
111
+ end
112
+ end
@@ -2,29 +2,17 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- # PC trap on the KERNAL serial LOAD routine ($F4A5, the default ILOAD
6
- # vector target). Reads device 8 requests from the virtual drive's
7
- # channel 0, then hands over to the ROM's own tail so it prints
8
- # SEARCHING FOR and LOADING (or VERIFYING) in direct mode, reports
9
- # errors and returns with the routine's register/zeropage contract;
10
- # other devices fall through to the ROM. So does a load that reaches
5
+ # PC trap on the KERNAL serial LOAD routine (the default ILOAD vector
6
+ # target, $F4A5 on the C64, $F549 on the VIC-20). Reads device 8
7
+ # requests from the virtual drive's channel 0, then hands over to the
8
+ # ROM's own tail so it prints SEARCHING FOR and LOADING (or VERIFYING)
9
+ # in direct mode, reports errors and returns with the routine's
10
+ # register/zeropage contract; other devices fall through to the ROM.
11
+ # So does a load that reaches
11
12
  # below $0334, into the zero page, the stack or the KERNAL vectors: the
12
13
  # ROM's byte loop then loads it through the serial traps, and a loader
13
14
  # that overwrites ISTOP takes over mid-load as it does on a real drive.
14
15
  class Load < File
15
- ADDRESS = 0xf4a5
16
-
17
- # ROM entry points: the SEARCHING FOR and LOADING/VERIFYING messages,
18
- # the byte loop that retries a timed-out byte until RUN/STOP, the
19
- # successful return (CLC, LDX $AE, LDY $AF, RTS) and the
20
- # FILE NOT FOUND and MISSING FILE NAME error exits
21
- SEARCHING_MESSAGE = 0xf5af
22
- LOADING_MESSAGE = 0xf5d2
23
- BYTE_LOOP = 0xf4f3
24
- LOAD_DONE = 0xf5a9
25
- FILE_NOT_FOUND_EXIT = 0xf704
26
- MISSING_FILE_NAME_EXIT = 0xf710
27
-
28
16
  # ST bits at $90
29
17
  EOI = 0x40
30
18
  VERIFY_MISMATCH = 0x10
@@ -35,8 +23,8 @@ module Badline
35
23
  # address even when the first one relocated.
36
24
  LOAD_SECONDARY = 0x60
37
25
 
38
- def initialize(cpu:, bus:, drive:)
39
- super(cpu:, bus:)
26
+ def initialize(cpu:, bus:, layout:, drive:)
27
+ super(cpu:, bus:, layout:)
40
28
  @drive = drive
41
29
  end
42
30
 
@@ -46,7 +34,7 @@ module Badline
46
34
  @bus.poke(0x93, @cpu.a)
47
35
  @bus.poke(0x90, 0x00)
48
36
  name = filename
49
- return @cpu.program_counter = MISSING_FILE_NAME_EXIT if Storage.parse_name(name).first.empty?
37
+ return @cpu.program_counter = @layout.missing_file_name_exit if Storage.parse_name(name).first.empty?
50
38
 
51
39
  data, complete = receive(name)
52
40
  if low_memory?(data)
@@ -79,15 +67,26 @@ module Badline
79
67
  def finish(data, complete)
80
68
  if data.empty?
81
69
  @bus.poke(0x90, EOI | READ_TIMEOUT)
82
- continue_with(SEARCHING_MESSAGE, FILE_NOT_FOUND_EXIT)
70
+ leave_bus(ACPTR_TIMEOUT)
71
+ continue_with(@layout.searching_message, @layout.file_not_found_exit)
83
72
  else
84
73
  deliver(data)
85
74
  @bus.poke(0x90, @bus.peek(0x90) & ~EOI) unless complete
86
- continue_with(SEARCHING_MESSAGE, LOADING_MESSAGE, complete ? LOAD_DONE : BYTE_LOOP)
75
+ leave_bus(ISOUR_TIMEOUT)
76
+ continue_with(@layout.searching_message, @layout.loading_message,
77
+ complete ? @layout.load_done : @layout.load_byte_loop)
87
78
  end
88
79
  @bus.poke(0xb9, LOAD_SECONDARY)
89
80
  end
90
81
 
82
+ # A load ends with the UNLISTEN that closes the file, a byte sent,
83
+ # and a missing file with the ACPTR that timed out. Either way it
84
+ # releases ATN, the clock and the data line.
85
+ def leave_bus(timer_high)
86
+ time_serial_byte(timer_high)
87
+ @layout.release_serial_lines(@bus)
88
+ end
89
+
91
90
  def low_memory?(data)
92
91
  load? && data.length > 2 && load_address(data) < 0x0334
93
92
  end
@@ -102,7 +101,7 @@ module Badline
102
101
  payload = data[2..] || []
103
102
  payload = payload[0, 0x10000 - addr] if addr + payload.length > 0x10000
104
103
  if load?
105
- @bus.ram.write(addr, payload)
104
+ @layout.store_file(@bus, addr, payload)
106
105
  @bus.poke(0x90, EOI)
107
106
  else
108
107
  verify(addr, payload)
@@ -3,22 +3,34 @@
3
3
  module Badline
4
4
  module KernalTrap
5
5
  # Base class for the PC traps on KERNAL routines. Holds the device
6
- # number they answer for and the return that stands in for the trapped
7
- # routine's RTS.
6
+ # number they answer for, the layout of the KERNAL they trap, and the
7
+ # return that stands in for the trapped routine's RTS.
8
8
  class Routine
9
9
  include IntegerHelper
10
10
 
11
11
  DEVICE = 8
12
12
 
13
- def initialize(cpu:, bus:)
13
+ # The serial timer's high byte as ISOUR and ACPTR set it.
14
+ ISOUR_TIMEOUT = 0x04
15
+ ACPTR_TIMEOUT = 0x01
16
+
17
+ def initialize(cpu:, bus:, layout:)
14
18
  @cpu = cpu
15
19
  @bus = bus
20
+ @layout = layout
16
21
  end
17
22
 
18
23
  private
19
24
 
20
- def kernal?
21
- @bus.io_port.kernal?
25
+ def kernal_mapped?
26
+ @layout.kernal_mapped?(@bus)
27
+ end
28
+
29
+ # The ROM times each byte on the serial bus with a one-shot timer
30
+ # (Layout#time_serial_byte): ISOUR_TIMEOUT for a byte sent,
31
+ # ACPTR_TIMEOUT for one received.
32
+ def time_serial_byte(timer_high)
33
+ @layout.time_serial_byte(@bus, timer_high)
22
34
  end
23
35
 
24
36
  def return_to_caller
@@ -2,37 +2,27 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- # PC trap on the KERNAL serial SAVE routine ($F5ED, the default ISAVE
6
- # vector target). Hands device 8 saves to the virtual drive as a PRG
7
- # (load address followed by the memory range); other devices fall
8
- # through to the ROM, and so do saves to a disk that doesn't take them
9
- # whole. The ROM prints SAVING in direct mode and returns
10
- # into the trap, which then writes the file and leaves through the
11
- # ROM's own tail with the registers its UNLISTEN and return leave. A
5
+ # PC trap on the KERNAL serial SAVE routine (the default ISAVE vector
6
+ # target, $F5ED on the C64, $F685 on the VIC-20). Hands device 8 saves
7
+ # to the virtual drive as a PRG (load address followed by the memory
8
+ # range); other devices fall through to the ROM, and so do saves to a
9
+ # disk that doesn't take them whole. The ROM prints SAVING in direct
10
+ # mode and returns into the trap, which then writes the file and leaves
11
+ # through the ROM's own tail with the registers its UNLISTEN and return
12
+ # leave. A
12
13
  # write that fails, to a full disk, a name already on it or a host file
13
14
  # that can't be written, ends the way a 1541 ends a SAVE it can't
14
15
  # write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
15
16
  # and the ROM returns without an error. A "@" before the drive prefix
16
17
  # writes over a file of the same name.
17
18
  class Save < File
18
- ADDRESS = 0xf5ed
19
-
20
- # ROM entry points: the SAVING message, the two line releases that
21
- # end UNLISTEN (clock, then data, leaving A as read from $DD00), the
22
- # successful return (CLC, RTS) and the MISSING FILE NAME error exit
23
- SAVING_MESSAGE = 0xf68f
24
- CLOCK_RELEASE = 0xee85
25
- DATA_RELEASE = 0xee97
26
- SAVE_DONE = 0xf657
27
- MISSING_FILE_NAME_EXIT = 0xf710
28
-
29
19
  SECONDARY = 0x61
30
20
 
31
21
  # ST bit at $90
32
22
  DEVICE_NOT_PRESENT = 0x80
33
23
 
34
- def initialize(cpu:, bus:, drive:)
35
- super(cpu:, bus:)
24
+ def initialize(cpu:, bus:, layout:, drive:)
25
+ super(cpu:, bus:, layout:)
36
26
  @drive = drive
37
27
  @saving = false
38
28
  end
@@ -51,11 +41,11 @@ module Badline
51
41
  return unless @drive.saves?
52
42
 
53
43
  @bus.poke(0xb9, SECONDARY)
54
- return @cpu.program_counter = MISSING_FILE_NAME_EXIT if name.empty?
44
+ return @cpu.program_counter = @layout.missing_file_name_exit if name.empty?
55
45
 
56
46
  @bus.poke(0x90, 0x00)
57
47
  @saving = true
58
- continue_with(SAVING_MESSAGE, ADDRESS)
48
+ continue_with(@layout.saving_message, @layout.save)
59
49
  end
60
50
 
61
51
  private
@@ -72,7 +62,7 @@ module Badline
72
62
  @bus.poke(0xad, @bus.peek(0xaf))
73
63
  @cpu.y = 0
74
64
  @cpu.status.overflow = true
75
- continue_with(CLOCK_RELEASE, DATA_RELEASE, SAVE_DONE)
65
+ continue_with(@layout.clock_release, @layout.data_release, @layout.save_done)
76
66
  end
77
67
 
78
68
  # Start address at $C1/$C2 (STAL), end address (exclusive) at