badline 0.10.1 → 0.11.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +25 -6
  4. data/CONTRIBUTING.md +8 -3
  5. data/README.md +63 -4
  6. data/doc/pinned-behaviour.md +75 -0
  7. data/doc/snapshots.md +15 -0
  8. data/lib/badline/address_bus/pla.rb +3 -16
  9. data/lib/badline/address_bus.rb +14 -0
  10. data/lib/badline/c128/banks.rb +35 -0
  11. data/lib/badline/c128/bus.rb +347 -0
  12. data/lib/badline/c128/bus_state.rb +53 -0
  13. data/lib/badline/c128/color_lines.rb +39 -0
  14. data/lib/badline/c128/cpu.rb +10 -0
  15. data/lib/badline/c128/drives.rb +95 -0
  16. data/lib/badline/c128/fast_serial.rb +62 -0
  17. data/lib/badline/c128/keys.rb +35 -0
  18. data/lib/badline/c128/mmu.rb +197 -0
  19. data/lib/badline/c128/mmu_pages.rb +105 -0
  20. data/lib/badline/c128/model.rb +39 -0
  21. data/lib/badline/c128/modes.rb +48 -0
  22. data/lib/badline/c128/saved_state.rb +121 -0
  23. data/lib/badline/c128/vdc/memory.rb +54 -0
  24. data/lib/badline/c128/vdc/painter.rb +143 -0
  25. data/lib/badline/c128/vdc/raster.rb +97 -0
  26. data/lib/badline/c128/vdc/window.rb +215 -0
  27. data/lib/badline/c128/vdc.rb +268 -0
  28. data/lib/badline/c128.rb +322 -0
  29. data/lib/badline/chrout_trap.rb +3 -0
  30. data/lib/badline/cia/fast_forward.rb +42 -0
  31. data/lib/badline/cia/serial.rb +12 -0
  32. data/lib/badline/cia/timer.rb +39 -0
  33. data/lib/badline/cia.rb +2 -0
  34. data/lib/badline/computer/attachments.rb +10 -13
  35. data/lib/badline/computer/kernal_traps.rb +29 -0
  36. data/lib/badline/computer.rb +5 -0
  37. data/lib/badline/drive/core.rb +172 -0
  38. data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
  39. data/lib/badline/drive/idle_cpu.rb +28 -0
  40. data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
  41. data/lib/badline/drive/serial_lines.rb +73 -0
  42. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
  43. data/lib/badline/drive/watch.rb +150 -0
  44. data/lib/badline/drive.rb +9 -0
  45. data/lib/badline/drive1541/bus.rb +5 -129
  46. data/lib/badline/drive1541/byte_ready.rb +48 -0
  47. data/lib/badline/drive1541/cpu.rb +1 -19
  48. data/lib/badline/drive1541/disk.rb +77 -20
  49. data/lib/badline/drive1541/disk_state.rb +7 -2
  50. data/lib/badline/drive1541/disk_via.rb +3 -0
  51. data/lib/badline/drive1541/mechanism.rb +48 -14
  52. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  53. data/lib/badline/drive1541/rotation.rb +5 -5
  54. data/lib/badline/drive1541/serial_port.rb +4 -60
  55. data/lib/badline/drive1541.rb +13 -172
  56. data/lib/badline/drive1571/bus.rb +111 -0
  57. data/lib/badline/drive1571/cpu.rb +11 -0
  58. data/lib/badline/drive1571/serial_port.rb +37 -0
  59. data/lib/badline/drive1571/serial_via.rb +25 -0
  60. data/lib/badline/drive1571/wd1770.rb +34 -0
  61. data/lib/badline/drive1571.rb +225 -0
  62. data/lib/badline/frontend/app.rb +35 -22
  63. data/lib/badline/frontend/boot.rb +31 -1
  64. data/lib/badline/frontend/controls.rb +11 -0
  65. data/lib/badline/frontend/drive_led.rb +1 -1
  66. data/lib/badline/frontend/keys.rb +28 -1
  67. data/lib/badline/frontend/machine_window.rb +16 -13
  68. data/lib/badline/frontend/menu_media.rb +3 -3
  69. data/lib/badline/frontend/menu_pages.rb +8 -2
  70. data/lib/badline/frontend/menu_ports.rb +1 -1
  71. data/lib/badline/frontend/pause_menu.rb +1 -0
  72. data/lib/badline/frontend/screen.rb +33 -8
  73. data/lib/badline/frontend/screens.rb +90 -0
  74. data/lib/badline/frontend/timeline.rb +15 -4
  75. data/lib/badline/frontend.rb +1 -0
  76. data/lib/badline/iec_bus.rb +54 -4
  77. data/lib/badline/kernal_trap/file.rb +1 -1
  78. data/lib/badline/kernal_trap/layout.rb +58 -8
  79. data/lib/badline/kernal_trap/load.rb +1 -1
  80. data/lib/badline/kernal_trap/save.rb +1 -1
  81. data/lib/badline/kernal_trap/serial.rb +5 -0
  82. data/lib/badline/machine.rb +20 -9
  83. data/lib/badline/media/true_drive.rb +26 -23
  84. data/lib/badline/media.rb +8 -6
  85. data/lib/badline/options/event.rb +5 -1
  86. data/lib/badline/options/family.rb +52 -0
  87. data/lib/badline/options/help.rb +7 -2
  88. data/lib/badline/options/table.rb +8 -2
  89. data/lib/badline/options/validation.rb +14 -1
  90. data/lib/badline/options.rb +20 -19
  91. data/lib/badline/roms/README +36 -0
  92. data/lib/badline/roms/c128/basic.rom +0 -0
  93. data/lib/badline/roms/c128/character.rom +0 -0
  94. data/lib/badline/roms/c128/kernal.rom +0 -0
  95. data/lib/badline/roms/dos1571.rom +0 -0
  96. data/lib/badline/snapshot/container.rb +2 -0
  97. data/lib/badline/snapshot/image.rb +14 -0
  98. data/lib/badline/snapshot.rb +9 -2
  99. data/lib/badline/storage/d64_image.rb +3 -0
  100. data/lib/badline/storage/d71_image.rb +9 -4
  101. data/lib/badline/storage/g64_image.rb +7 -2
  102. data/lib/badline/time_of_day.rb +9 -0
  103. data/lib/badline/version.rb +1 -1
  104. data/lib/badline/via/fast_forward.rb +3 -3
  105. data/lib/badline/vic/display_state.rb +12 -0
  106. data/lib/badline/vic/registers.rb +3 -0
  107. data/lib/badline/vic.rb +108 -7
  108. data/lib/badline/vic20/attachments.rb +6 -0
  109. data/lib/badline/z80/arithmetic.rb +117 -0
  110. data/lib/badline/z80/bit_page.rb +55 -0
  111. data/lib/badline/z80/block_io.rb +58 -0
  112. data/lib/badline/z80/block_transfers.rb +59 -0
  113. data/lib/badline/z80/bus_cycles.rb +111 -0
  114. data/lib/badline/z80/core.rb +111 -0
  115. data/lib/badline/z80/exchanges.rb +50 -0
  116. data/lib/badline/z80/extended_page.rb +89 -0
  117. data/lib/badline/z80/flags.rb +37 -0
  118. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  119. data/lib/badline/z80/jumps.rb +82 -0
  120. data/lib/badline/z80/loads.rb +98 -0
  121. data/lib/badline/z80/main_page.rb +134 -0
  122. data/lib/badline/z80/registers.rb +120 -0
  123. data/lib/badline/z80/shifts.rb +64 -0
  124. data/lib/badline/z80.rb +11 -0
  125. data/lib/badline.rb +1 -0
  126. metadata +59 -4
@@ -85,6 +85,9 @@ module Badline
85
85
  end
86
86
  end
87
87
 
88
+ # Whether the machine is a C128, whose F8 switches its screens.
89
+ def c128? = @computer.family == :c128
90
+
88
91
  private
89
92
 
90
93
  def vic20? = @computer.family == :vic20
@@ -109,7 +112,8 @@ module Badline
109
112
  end
110
113
  toggle("WRITABLE", :writable, [%w[OFF ON], %i[protect unprotect], media.writable? ? 1 : 0])
111
114
  skip
112
- info("DEVICE", @computer.drive1541.nil? ? "FAST LOADING" : "1541, TRUE DRIVE")
115
+ drive = @computer.true_drive
116
+ info("DEVICE", drive.nil? ? "FAST LOADING" : "#{drive.model_name}, TRUE DRIVE")
113
117
  end
114
118
 
115
119
  def draw_datasette
@@ -131,7 +135,7 @@ module Badline
131
135
  info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
132
136
  row("INSERT...", :insert_cartridge)
133
137
  row("REMOVE", :remove_cartridge)
134
- row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer?
138
+ row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer? && @computer.family == :c64
135
139
  return if @reu_kb.nil?
136
140
 
137
141
  skip
@@ -166,7 +170,9 @@ module Badline
166
170
  end
167
171
  end
168
172
 
173
+ # The C128 names its model and the mode it runs in first.
169
174
  def draw_power
175
+ info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
170
176
  row("RESET", :reset)
171
177
  row("POWER CYCLE", :power_cycle)
172
178
  skip
@@ -19,7 +19,7 @@ module Badline
19
19
 
20
20
  toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
21
21
  toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
22
- return unless draw_keys("C64")
22
+ return unless draw_keys(@computer.family == :c128 ? "C128" : "C64")
23
23
 
24
24
  row("SWAP JOYSTICKS", :swap)
25
25
  skip
@@ -145,6 +145,7 @@ module Badline
145
145
  draw_sections
146
146
  @buttons.plain(@left + 6, @top + HEIGHT - 28, "QUICK OPEN...", :quick_open)
147
147
  painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", DIM)
148
+ painter.text(@left + BODY, @top + HEIGHT - 12, "F6: 40/80 KEY F8: SCREEN", DIM) if @pages.c128?
148
149
  @pages.draw(PAGES[@section], @left + BODY, @top + 34)
149
150
  end
150
151
 
@@ -8,30 +8,44 @@ module Badline
8
8
  # words, as badline/ffi does on CRuby, so each word carries two
9
9
  # neighbouring pixels, the left one in the low half.
10
10
  #
11
- # The texture is as wide as the crop and at least as high as PAL's
12
- # C64 crop, 272 lines, so a C64 shows in the same window whatever its
13
- # region: a crop with fewer lines sits in the middle of the texture,
14
- # between black bands. The VIC-20's 284 by 284 crop fills its own.
11
+ # Each pixel shows `pixel_width` of the window's square pixels wide,
12
+ # or with `dots`, that many pixels share one, as the VDC's dots do. The
13
+ # texture is as wide as the crop, an even number of pixels, and at
14
+ # least MIN_WIDTH window pixels wide, and at least as high as PAL's C64
15
+ # crop, 272 lines, so a C64 shows in the same window whatever its
16
+ # region. A crop with fewer lines or pixels sits in the middle of the
17
+ # texture, between black bands. The VIC-20's 284 by 284 crop fills its
18
+ # own.
15
19
  class Screen
20
+ MIN_WIDTH = Region::PAL.crop[2]
16
21
  MIN_HEIGHT = Region::PAL.crop[3]
17
22
 
18
23
  attr_reader :pixels, :width, :height
19
24
 
20
- def initialize(video, crop)
25
+ def initialize(video, crop, pixel_width: 1, dots: 1)
21
26
  @video = video
22
27
  @col_offset = crop[0]
23
28
  @row_offset = crop[1]
24
29
  @lines = crop[3]
25
- @width = crop[2]
30
+ @columns = crop[2] & ~1
31
+ @pixel_width = pixel_width
32
+ @dots = dots
26
33
  @stride = video.width.to_i
34
+ @width = [@columns, ((MIN_WIDTH * dots / pixel_width) + 1) & ~1].max
27
35
  @height = [@lines, MIN_HEIGHT].max
28
36
  @row_words = @width / 2
37
+ @pair_count = @columns / 2
38
+ @margin = (@row_words - @pair_count) / 2
29
39
  @band = (@height - @lines) / 2
30
40
  palette = video.palette
31
41
  @pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
32
42
  @pixels = Array.new(@height * @row_words, 0)
43
+ pack_all
33
44
  end
34
45
 
46
+ # The width in the window's square pixels.
47
+ def window_width = @width * @pixel_width / @dots
48
+
35
49
  # The bytes of one row of the texture.
36
50
  def row_bytes = @width * 4
37
51
 
@@ -50,12 +64,23 @@ module Badline
50
64
 
51
65
  private
52
66
 
67
+ # Repacks every line, as a screen built for a display the chip has
68
+ # already drawn starts with all of it.
69
+ def pack_all
70
+ display = @video.display
71
+ row = 0
72
+ while row < @lines
73
+ pack_row(display, row)
74
+ row += 1
75
+ end
76
+ end
77
+
53
78
  def pack_row(display, row)
54
79
  pixels = @pixels
55
80
  pairs = @pairs
56
81
  from = ((row + @row_offset) * @stride) + @col_offset
57
- to = (row + @band) * @row_words
58
- last = to + @row_words
82
+ to = ((row + @band) * @row_words) + @margin
83
+ last = to + @pair_count
59
84
  while to < last
60
85
  pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
61
86
  from += 2
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The Screen the window shows: the machine's video chip's, or on the
6
+ # C128 the VDC's, two of its dots to a window pixel, once #show_vdc
7
+ # switches to it. Only the chip shown renders. The VDC's display changes
8
+ # size with its registers, and the screen follows it.
9
+ #
10
+ # On the C128, #follow switches to the screen the machine prints to
11
+ # (C128#active_screen) each time it moves there, and #show_vdc's choice
12
+ # holds until the next move.
13
+ class Screens
14
+ # The frames a move holds before #follow takes it. The C128 KERNAL's
15
+ # reset clears the 80 column screen with the editor on it for over a
16
+ # frame.
17
+ FOLLOW_FRAMES = 3
18
+
19
+ attr_reader :screen
20
+
21
+ def initialize(computer)
22
+ @computer = computer
23
+ @vdc_shown = false
24
+ @vdc_shape = []
25
+ forget_screen
26
+ build
27
+ end
28
+
29
+ def vdc_shown? = @vdc_shown
30
+
31
+ # Shows another machine's video chip, once #build builds its screen.
32
+ def computer=(computer)
33
+ @computer = computer
34
+ @vdc_shown = false
35
+ forget_screen
36
+ end
37
+
38
+ # Shows the screen a C128 has moved its output to once the move has
39
+ # held for FOLLOW_FRAMES calls, one a frame. #build then builds it.
40
+ def follow
41
+ return unless @computer.family == :c128
42
+
43
+ vdc = @computer.active_screen == :vdc
44
+ @held = vdc == @seen ? @held + 1 : 1
45
+ @seen = vdc
46
+ return if @held < FOLLOW_FRAMES || vdc == @followed
47
+
48
+ @followed = vdc
49
+ show_vdc(vdc)
50
+ end
51
+
52
+ # Shows the C128's VDC, or with false its VIC-IIe, once #build builds
53
+ # its screen. Other machines have the one screen.
54
+ def show_vdc(shown)
55
+ return unless @computer.family == :c128
56
+
57
+ @vdc_shown = shown
58
+ @computer.vdc_shown = shown
59
+ end
60
+
61
+ # Whether the screen was built for the other chip, or the VDC's
62
+ # display has changed size or crop since the screen was built for it.
63
+ def stale? = @built_vdc != @vdc_shown || (@vdc_shown && vdc_shape(@computer.vdc) != @vdc_shape)
64
+
65
+ def build
66
+ @built_vdc = @vdc_shown
67
+ if @vdc_shown
68
+ vdc = @computer.vdc
69
+ @vdc_shape = vdc_shape(vdc)
70
+ @screen = Screen.new(vdc, vdc.crop, dots: 2)
71
+ else
72
+ timing = @computer.timing
73
+ @screen = Screen.new(@computer.video, timing.crop, pixel_width: timing.pixel_width)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # The machine's output counts as on the VIC-IIe's screen, the one
80
+ # shown.
81
+ def forget_screen
82
+ @followed = false
83
+ @seen = false
84
+ @held = 0
85
+ end
86
+
87
+ def vdc_shape(vdc) = [vdc.width, vdc.height] + vdc.crop
88
+ end
89
+ end
90
+ end
@@ -87,6 +87,17 @@ module Badline
87
87
  section
88
88
  end
89
89
 
90
+ # Whether the frame has a display event.
91
+ def display?(frame) = @events.any? { |event| event.frame == frame && event.action == "display" }
92
+
93
+ # The C128 screen the frame's display event shows, vic or vdc, or an
94
+ # empty string without one.
95
+ def display_at(frame)
96
+ chip = ""
97
+ @events.each { |event| chip = event.argument if event.frame == frame && event.action == "display" }
98
+ chip
99
+ end
100
+
90
101
  # Whether the pause menu closes at the frame.
91
102
  def resume?(frame) = @events.any? { |event| event.frame == frame && event.action == "resume" }
92
103
 
@@ -133,9 +144,9 @@ module Badline
133
144
  hold(event)
134
145
  end
135
146
 
136
- # The VIC-20 has no cartridge with a button yet.
147
+ # The VIC-20 and the C128 have no cartridge with a button yet.
137
148
  def freeze(computer, event)
138
- return if computer.family == :vic20
149
+ return unless computer.family == :c64
139
150
 
140
151
  computer.press_cartridge_button
141
152
  hold(event)
@@ -146,7 +157,7 @@ module Badline
146
157
  end
147
158
 
148
159
  def release(computer, event)
149
- return computer.release_cartridge_button if event.action == "freeze"
160
+ return computer.release_cartridge_button if event.action == "freeze" && computer.family == :c64
150
161
 
151
162
  name = event.argument
152
163
  return computer.release_restore if name == "restore"
@@ -183,7 +194,7 @@ module Badline
183
194
  end
184
195
 
185
196
  # 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)
197
+ def ejectable?(computer, what) = (computer.family != :vic20 || what != "cartridge") && inserted?(computer, what)
187
198
 
188
199
  def inserted?(computer, what)
189
200
  return !computer.datasette.tape.nil? if what == "tape"
@@ -4,6 +4,7 @@ require "badline/sdl"
4
4
  require "badline/frontend/boot"
5
5
  require "badline/frontend/frame_rate"
6
6
  require "badline/frontend/screen"
7
+ require "badline/frontend/screens"
7
8
  require "badline/frontend/drive_led"
8
9
  require "badline/frontend/keys"
9
10
  require "badline/frontend/snapshots"
@@ -16,6 +16,14 @@ module Badline
16
16
  # also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
17
17
  # (an XOR gate), which answers ATN in hardware before the DOS gets to it.
18
18
  # SerialPort reads the lines back into VIA 1.
19
+ #
20
+ # Fast serial adds SRQ, which the C64 leaves alone. A C128 and a 1571
21
+ # clock bytes over it from a CIA's CNT, with the bits on DATA from its
22
+ # SP, through buffers each side turns towards the bus to send. A pin low
23
+ # pulls its line low. Each side pushes the lines it pulls when they
24
+ # move: the host through host_fast_lines=, which tells the drives
25
+ # (fast_lines_moved), and a drive through drives_fast_moved!, which
26
+ # tells the host (on_fast_change).
19
27
  class IECBus
20
28
  # CIA 2 port A
21
29
  HOST_ATN_OUT = 0x08
@@ -29,16 +37,56 @@ module Badline
29
37
  DRIVE_CLK_OUT = 0x08
30
38
  DRIVE_ATNA = 0x10
31
39
 
32
- # The lines pulled low, as ATN, CLK and DATA bits
40
+ # The lines pulled low, as ATN, CLK, DATA and SRQ bits
33
41
  ATN = 0x01
34
42
  CLK = 0x02
35
43
  DATA = 0x04
44
+ SRQ = 0x08
45
+
46
+ # A drive's serial_output bits above VIA 1's port B: its fast serial
47
+ # pins pulling DATA and SRQ.
48
+ DRIVE_FAST_DATA = 0x100
49
+ DRIVE_FAST_SRQ = 0x200
36
50
 
37
51
  attr_reader :drives, :host_lines
38
52
 
53
+ # The lines the host's fast serial pins pull, as DATA and SRQ bits.
54
+ attr_reader :host_fast_lines
55
+
39
56
  def initialize
40
57
  @drives = []
41
58
  @host_lines = 0
59
+ @host_fast_lines = 0
60
+ @drive_fast_lines = 0
61
+ @on_fast_change = nil
62
+ end
63
+
64
+ # The host's fast serial pins moved: +low+ is the lines they pull, as
65
+ # DATA and SRQ bits.
66
+ def host_fast_lines=(low)
67
+ return if low == @host_fast_lines
68
+
69
+ @host_fast_lines = low
70
+ @drives.each(&:fast_lines_moved)
71
+ end
72
+
73
+ # A drive's fast serial pins moved (serial_output's fast bits).
74
+ def drives_fast_moved!
75
+ low = 0
76
+ @drives.each do |drive|
77
+ output = drive.serial_output
78
+ low |= DATA if output.anybits?(DRIVE_FAST_DATA)
79
+ low |= SRQ if output.anybits?(DRIVE_FAST_SRQ)
80
+ end
81
+ return if low == @drive_fast_lines
82
+
83
+ @drive_fast_lines = low
84
+ @on_fast_change&.call
85
+ end
86
+
87
+ # Calls the block whenever the drives move their fast serial pins.
88
+ def on_fast_change(&block)
89
+ @on_fast_change = block
42
90
  end
43
91
 
44
92
  # The machine pushes the lines it pulls, as port A bits, whenever they
@@ -58,11 +106,13 @@ module Badline
58
106
  end
59
107
 
60
108
  # One pass over everything on the bus, for a reader that wants more
61
- # than one line. +host+ is the C64's port A lines as the reader sees
62
- # them, which a drive takes a cycle late (see Drive1541::SerialPort).
109
+ # than one line, SRQ included. +host+ is the C64's port A lines as the
110
+ # reader sees them, which a drive takes a cycle late (see
111
+ # Drive::SerialLines).
63
112
  def low_lines(host = @host_lines)
64
113
  atn = host.anybits?(HOST_ATN_OUT)
65
- low = atn ? ATN : 0
114
+ low = @host_fast_lines | @drive_fast_lines
115
+ low |= ATN if atn
66
116
  low |= CLK if host.anybits?(HOST_CLK_OUT)
67
117
  low |= DATA if host.anybits?(HOST_DATA_OUT)
68
118
  i = 0
@@ -17,7 +17,7 @@ module Badline
17
17
  def full_filename
18
18
  pointer = uint16(@bus.peek(0xbb), @bus.peek(0xbc))
19
19
  bytes = Array.new(@bus.peek(0xb7)) do |i|
20
- @bus.peek((pointer + i) & 0xffff)
20
+ @layout.filename_byte(@bus, (pointer + i) & 0xffff)
21
21
  end
22
22
  Storage.ascii(bytes)
23
23
  end
@@ -18,37 +18,59 @@ module Badline
18
18
  # primitives' entry points.
19
19
  #
20
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
21
+ # whether RAM lies under every ROM and the I/O. +mmu+ says the C128's
22
+ # MMU maps the KERNAL instead, and that the file name and the file's
23
+ # bytes lie in the banks the KERNAL keeps at FNBANK and BA. Each
22
24
  # serial byte starts a one-shot timer by writing the high byte of its
23
25
  # count to +serial_timer+, and the control value TIMER_ONE_SHOT_START
24
26
  # to +serial_timer_start+ where the timer has a control register to
25
27
  # 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
28
+ # (a VIA's timer 2). A KERNAL that times its bytes with a loop has no
29
+ # +serial_timer+. The ROM lets go of each serial line by reading a
27
30
  # port, masking the line's bit off and writing it back: ATN at
28
31
  # +atn_port+ with +atn_mask+, then the clock and the data line.
29
32
  Layout = Data.define(:load, :save, :searching_message, :loading_message, :load_byte_loop, :load_done,
30
33
  :file_not_found_exit, :missing_file_name_exit, :saving_message, :clock_release,
31
34
  :data_release, :save_done, :talk, :listen, :second, :tksa, :ciout, :untalk,
32
35
  :unlisten, :acptr, :banked, :serial_timer, :serial_timer_start, :atn_port, :atn_mask,
33
- :clock_port, :clock_mask, :data_port, :data_mask) do
36
+ :clock_port, :clock_mask, :data_port, :data_mask, :mmu) do
34
37
  # Whether the CPU reads the KERNAL ROM at the trapped addresses: on a
35
- # banked machine, the 6510 port's banking bits select it.
38
+ # banked machine, the 6510 port's banking bits select it, and on the
39
+ # C128 the MMU.
36
40
  def kernal_mapped?(bus)
41
+ return bus.system_rom_mapped? if mmu
42
+
37
43
  !banked || bus.io_port.kernal?
38
44
  end
39
45
 
40
46
  # 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.
47
+ # into the RAM under whatever is banked in, on the C128 into the RAM
48
+ # of the bank at BA, and otherwise where the ROM's byte loop would
49
+ # store them, through the bus.
43
50
  def store_file(bus, address, bytes)
51
+ return bus.write_bank(bus.peek(BANK), address, bytes) if mmu
44
52
  return bus.ram.write(address, bytes) if banked
45
53
 
46
54
  bytes.each_with_index { |byte, i| bus.poke(address + i, byte) }
47
55
  end
48
56
 
57
+ # The file name's byte at +address+, in the bank at FNBANK on the
58
+ # C128.
59
+ def filename_byte(bus, address)
60
+ mmu ? bus.peek_bank(bus.peek(FILENAME_BANK), address) : bus.peek(address)
61
+ end
62
+
63
+ # The byte at +address+ a SAVE writes or a VERIFY compares, in the bank
64
+ # at BA on the C128.
65
+ def file_byte(bus, address)
66
+ mmu ? bus.peek_bank(bus.peek(BANK), address) : bus.peek(address)
67
+ end
68
+
49
69
  # Starts the timer the ROM times a serial byte with, its count's high
50
70
  # byte +timer_high+.
51
71
  def time_serial_byte(bus, timer_high)
72
+ return unless serial_timer
73
+
52
74
  bus.poke(serial_timer, timer_high)
53
75
  start = serial_timer_start
54
76
  bus.poke(start, TIMER_ONE_SHOT_START) if start
@@ -77,6 +99,11 @@ module Badline
77
99
  # start.
78
100
  TIMER_ONE_SHOT_START = 0x19
79
101
 
102
+ # Where the C128 KERNAL keeps the bank a LOAD or SAVE goes to (BA) and
103
+ # the bank of the file name (FNBANK).
104
+ BANK = 0xc6
105
+ FILENAME_BANK = 0xc7
106
+
80
107
  # The C64 KERNAL, revision 3. The SX-64's, 251104-04, differs from it
81
108
  # only in bytes none of these addresses reach. It times serial bytes
82
109
  # with CIA 1's timer B, and drives the lines from CIA 2's port A
@@ -89,7 +116,8 @@ module Badline
89
116
  talk: 0xed09, listen: 0xed0c, second: 0xedb9, tksa: 0xedc7,
90
117
  ciout: 0xeddd, untalk: 0xedef, unlisten: 0xedfe, acptr: 0xee13,
91
118
  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
119
+ atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf,
120
+ mmu: false
93
121
  )
94
122
 
95
123
  # The VIC-20 KERNAL, 901486-07 (PAL). The NTSC one, 901486-06, differs
@@ -106,7 +134,29 @@ module Badline
106
134
  talk: 0xee14, listen: 0xee17, second: 0xeec0, tksa: 0xeece,
107
135
  ciout: 0xeee4, untalk: 0xeef6, unlisten: 0xef04, acptr: 0xef19,
108
136
  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
137
+ atn_port: 0x911f, atn_mask: 0x7f, clock_port: 0x912c, clock_mask: 0xfd, data_port: 0x912c, data_mask: 0xdf,
138
+ mmu: false
139
+ )
140
+
141
+ # The C128 KERNAL, 318020-05, in C128 mode: the jump table's targets
142
+ # for the serial primitives, ILOAD's and ISAVE's default targets, and
143
+ # the ROM's own tails. Its LOAD prints SEARCHING FOR at $F50F and
144
+ # LOADING or VERIFYING at $F533, retries a byte at $F2CF and returns at
145
+ # $F39B. FILE NOT FOUND leaves through $F685 and MISSING FILE NAME
146
+ # through $F691. SAVE prints SAVING at $F5BC and returns at $F5B3, after
147
+ # the UNLISTEN whose clock and data releases are $E545 and $E557. It
148
+ # drives the lines from CIA 2's port A, and times serial bytes with
149
+ # loops, not a timer.
150
+ C128_LAYOUT = Layout.new(
151
+ load: 0xf26c, save: 0xf54e,
152
+ searching_message: 0xf50f, loading_message: 0xf533, load_byte_loop: 0xf2cf, load_done: 0xf39b,
153
+ file_not_found_exit: 0xf685, missing_file_name_exit: 0xf691,
154
+ saving_message: 0xf5bc, clock_release: 0xe545, data_release: 0xe557, save_done: 0xf5b3,
155
+ talk: 0xe33b, listen: 0xe33e, second: 0xe4d2, tksa: 0xe4e0,
156
+ ciout: 0xe503, untalk: 0xe515, unlisten: 0xe526, acptr: 0xe43e,
157
+ banked: true, serial_timer: nil, serial_timer_start: nil,
158
+ atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf,
159
+ mmu: true
110
160
  )
111
161
  end
112
162
  end
@@ -112,7 +112,7 @@ module Badline
112
112
  end
113
113
 
114
114
  def verify(addr, payload)
115
- match = payload.each_with_index.all? { |byte, i| @bus.peek(addr + i) == byte }
115
+ match = payload.each_with_index.all? { |byte, i| @layout.file_byte(@bus, addr + i) == byte }
116
116
  @bus.poke(0x90, match ? EOI : EOI | VERIFY_MISMATCH)
117
117
  end
118
118
 
@@ -71,7 +71,7 @@ module Badline
71
71
  start = uint16(@bus.peek(0xc1), @bus.peek(0xc2))
72
72
  length = (uint16(@bus.peek(0xae), @bus.peek(0xaf)) - start) & 0xffff
73
73
  [low_byte(start), high_byte(start)] +
74
- Array.new(length) { |i| @bus.peek((start + i) & 0xffff) }
74
+ Array.new(length) { |i| @layout.file_byte(@bus, (start + i) & 0xffff) }
75
75
  end
76
76
  end
77
77
  end
@@ -58,6 +58,11 @@ module Badline
58
58
  self
59
59
  end
60
60
 
61
+ # Takes the traps out again, so the KERNAL's own routines run.
62
+ def uninstall
63
+ routines.each_key { |address| @cpu.remove_trap(address) }
64
+ end
65
+
61
66
  private
62
67
 
63
68
  def routines
@@ -4,7 +4,8 @@ module Badline
4
4
  # = Machine
5
5
  #
6
6
  # The machines badline builds by family, as the command line's
7
- # subcommand names them: the C64, Computer, and the VIC-20, Vic20.
7
+ # subcommand names them: the C64, Computer, the VIC-20, Vic20, and the
8
+ # C128 in C64 mode, C128.
8
9
  #
9
10
  # A machine answers what the front end, Media, the runners and the
10
11
  # snapshots call on it:
@@ -23,7 +24,8 @@ module Badline
23
24
  # - Media: +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
24
25
  # +mounted?+, +datasette+, +attach_cartridge+,
25
26
  # +press_cartridge_button+, +release_cartridge_button+,
26
- # +attach_drive1541+, +drive1541+, and +address_bus+ for its
27
+ # +attach_drive1541+, +drive1541+, +true_drive+ and +plug_true_drive+
28
+ # for the drive Media::TrueDrive plugs in, and +address_bus+ for its
27
29
  # +cartridge+ and +detach_cartridge+.
28
30
  # - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
29
31
  # +install_debug_register+ and +capture_output+.
@@ -32,16 +34,25 @@ module Badline
32
34
  # input parts, with its one joystick as both +joystick1+ and +joystick2+
33
35
  # and nil for +control_ports+, and of the rest +ram+, +load_prg+,
34
36
  # +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
35
- # +attach_drive1541+, +drive1541+, +install_debug_register+ and
36
- # +capture_output+.
37
+ # +attach_drive1541+, +drive1541+, +true_drive+, +plug_true_drive+,
38
+ # +install_debug_register+ and +capture_output+.
39
+ #
40
+ # The C128 answers all but the cartridge button, and besides +mode+,
41
+ # :c64, as Machine.build builds it, or :c128, +vdc+, the VDC a front
42
+ # end can show in place of +video+ once +vdc_shown=+ renders it,
43
+ # +press_caps_lock+, +release_caps_lock+, +press_display_key+,
44
+ # +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
45
+ # C128D's 1571, its true drive.
37
46
  module Machine
38
- # A new machine of `family`, :c64 or :vic20, the model of it `model`
39
- # names. A C64's model is one of Model::ALL, with its SID `sid_model`
40
- # when not nil and an REU of `reu` K when not nil. A VIC-20's is "pal",
41
- # with the RAM expansion `ram` names (Vic20::Bus::RAM_CONFIGURATIONS),
42
- # unexpanded when nil.
47
+ # A new machine of `family`, :c64, :vic20 or :c128, the model of it
48
+ # `model` names. A C64's model is one of Model::ALL, with its SID
49
+ # `sid_model` when not nil and an REU of `reu` K when not nil. A
50
+ # VIC-20's is "pal", with the RAM expansion `ram` names
51
+ # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
52
+ # one of C128::Model::ALL, with its SID `sid_model` when not nil.
43
53
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
44
54
  return vic20(model, ram) if family == :vic20
55
+ return C128.new(model:, sid_model:) if family == :c128
45
56
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
46
57
 
47
58
  profile = Model.named(model)