badline 0.10.1 → 0.11.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 +15 -0
  3. data/CLAUDE.md +5 -3
  4. data/README.md +59 -4
  5. data/doc/pinned-behaviour.md +75 -0
  6. data/doc/snapshots.md +15 -0
  7. data/lib/badline/address_bus/pla.rb +3 -16
  8. data/lib/badline/address_bus.rb +14 -0
  9. data/lib/badline/c128/banks.rb +35 -0
  10. data/lib/badline/c128/bus.rb +347 -0
  11. data/lib/badline/c128/bus_state.rb +53 -0
  12. data/lib/badline/c128/color_lines.rb +39 -0
  13. data/lib/badline/c128/cpu.rb +10 -0
  14. data/lib/badline/c128/keys.rb +35 -0
  15. data/lib/badline/c128/mmu.rb +182 -0
  16. data/lib/badline/c128/mmu_pages.rb +105 -0
  17. data/lib/badline/c128/model.rb +39 -0
  18. data/lib/badline/c128/modes.rb +48 -0
  19. data/lib/badline/c128/saved_state.rb +120 -0
  20. data/lib/badline/c128/vdc/memory.rb +54 -0
  21. data/lib/badline/c128/vdc/painter.rb +143 -0
  22. data/lib/badline/c128/vdc/raster.rb +97 -0
  23. data/lib/badline/c128/vdc/window.rb +215 -0
  24. data/lib/badline/c128/vdc.rb +268 -0
  25. data/lib/badline/c128.rb +320 -0
  26. data/lib/badline/chrout_trap.rb +3 -0
  27. data/lib/badline/computer/attachments.rb +4 -13
  28. data/lib/badline/computer/kernal_traps.rb +29 -0
  29. data/lib/badline/computer.rb +5 -0
  30. data/lib/badline/frontend/app.rb +34 -21
  31. data/lib/badline/frontend/boot.rb +31 -1
  32. data/lib/badline/frontend/controls.rb +11 -0
  33. data/lib/badline/frontend/keys.rb +28 -1
  34. data/lib/badline/frontend/machine_window.rb +16 -13
  35. data/lib/badline/frontend/menu_pages.rb +6 -1
  36. data/lib/badline/frontend/menu_ports.rb +1 -1
  37. data/lib/badline/frontend/pause_menu.rb +1 -0
  38. data/lib/badline/frontend/screen.rb +33 -8
  39. data/lib/badline/frontend/screens.rb +90 -0
  40. data/lib/badline/frontend/timeline.rb +15 -4
  41. data/lib/badline/frontend.rb +1 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +58 -8
  44. data/lib/badline/kernal_trap/load.rb +1 -1
  45. data/lib/badline/kernal_trap/save.rb +1 -1
  46. data/lib/badline/kernal_trap/serial.rb +5 -0
  47. data/lib/badline/machine.rb +15 -6
  48. data/lib/badline/media.rb +8 -6
  49. data/lib/badline/options/event.rb +5 -1
  50. data/lib/badline/options/family.rb +52 -0
  51. data/lib/badline/options/help.rb +5 -1
  52. data/lib/badline/options/table.rb +7 -1
  53. data/lib/badline/options/validation.rb +14 -1
  54. data/lib/badline/options.rb +20 -19
  55. data/lib/badline/roms/README +27 -0
  56. data/lib/badline/roms/c128/basic.rom +0 -0
  57. data/lib/badline/roms/c128/character.rom +0 -0
  58. data/lib/badline/roms/c128/kernal.rom +0 -0
  59. data/lib/badline/snapshot/container.rb +2 -0
  60. data/lib/badline/snapshot/image.rb +14 -0
  61. data/lib/badline/snapshot.rb +9 -2
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/display_state.rb +12 -0
  64. data/lib/badline/vic/registers.rb +3 -0
  65. data/lib/badline/vic.rb +108 -7
  66. data/lib/badline.rb +1 -0
  67. metadata +24 -1
@@ -3,10 +3,10 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # The emulator's SDL window, its renderer and the streaming texture a
6
- # Screen's pixels go into. The renderer's logical size is the screen's,
7
- # with each of its pixels `pixel_width` square pixels wide, so the
8
- # picture keeps the machine's aspect while the menu and the drive LED
9
- # draw in square pixels over it. The window opens at SCALE times that.
6
+ # Screen's pixels go into. The renderer's logical size is the screen's
7
+ # in square pixels (Screen#window_width), so the picture keeps the
8
+ # machine's aspect while the menu and the drive LED draw in square
9
+ # pixels over it. The window opens at SCALE times that.
10
10
  class MachineWindow
11
11
  SCALE = 2
12
12
  TITLE = "Badline"
@@ -18,12 +18,12 @@ module Badline
18
18
 
19
19
  # Opens the window for `screen`, presenting in step with the display
20
20
  # when `vsync`.
21
- def initialize(screen, pixel_width, vsync:)
21
+ def initialize(screen, vsync:)
22
22
  abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
23
23
 
24
24
  SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
25
25
  SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
26
- measure(screen, pixel_width)
26
+ measure(screen)
27
27
  @window = SDL.SDL_CreateWindow(
28
28
  TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
29
29
  SDL::WINDOW_RESIZABLE
@@ -42,9 +42,9 @@ module Badline
42
42
 
43
43
  # Fits the texture, the logical size and the window to another
44
44
  # machine's screen, unless it measures the same.
45
- def fit(screen, pixel_width)
45
+ def fit(screen)
46
46
  before = [@width, @height, @texture_width]
47
- measure(screen, pixel_width)
47
+ measure(screen)
48
48
  return if before == [@width, @height, @texture_width]
49
49
 
50
50
  SDL.SDL_DestroyTexture(@texture)
@@ -56,9 +56,12 @@ module Badline
56
56
  SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
57
57
  end
58
58
 
59
- # Titles the window, TITLE followed by `tags` in brackets.
60
- def title(tags)
61
- SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |tag| "[#{tag}]" }).join(" "))
59
+ # Titles the window, TITLE followed by the controls' `tag`, unless it's
60
+ # empty, and MUTED when `muted`, each in brackets.
61
+ def title(tag, muted)
62
+ tags = tag.empty? ? [] : [tag]
63
+ tags << "MUTED" if muted
64
+ SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |name| "[#{name}]" }).join(" "))
62
65
  end
63
66
 
64
67
  def close
@@ -70,9 +73,9 @@ module Badline
70
73
 
71
74
  private
72
75
 
73
- def measure(screen, pixel_width)
76
+ def measure(screen)
74
77
  @texture_width = screen.width
75
- @width = screen.width * pixel_width
78
+ @width = screen.window_width
76
79
  @height = screen.height
77
80
  end
78
81
 
@@ -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
@@ -131,7 +134,7 @@ module Badline
131
134
  info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
132
135
  row("INSERT...", :insert_cartridge)
133
136
  row("REMOVE", :remove_cartridge)
134
- row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer?
137
+ row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer? && @computer.family == :c64
135
138
  return if @reu_kb.nil?
136
139
 
137
140
  skip
@@ -166,7 +169,9 @@ module Badline
166
169
  end
167
170
  end
168
171
 
172
+ # The C128 names its model and the mode it runs in first.
169
173
  def draw_power
174
+ info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
170
175
  row("RESET", :reset)
171
176
  row("POWER CYCLE", :power_cycle)
172
177
  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"
@@ -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:
@@ -34,14 +35,22 @@ module Badline
34
35
  # +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
35
36
  # +attach_drive1541+, +drive1541+, +install_debug_register+ and
36
37
  # +capture_output+.
38
+ #
39
+ # The C128 answers all but the cartridge button, and besides +mode+,
40
+ # :c64, as Machine.build builds it, or :c128, +vdc+, the VDC a front
41
+ # end can show in place of +video+ once +vdc_shown=+ renders it,
42
+ # +press_caps_lock+, +release_caps_lock+, +press_display_key+ and
43
+ # +release_display_key+.
37
44
  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.
45
+ # A new machine of `family`, :c64, :vic20 or :c128, the model of it
46
+ # `model` names. A C64's model is one of Model::ALL, with its SID
47
+ # `sid_model` when not nil and an REU of `reu` K when not nil. A
48
+ # VIC-20's is "pal", with the RAM expansion `ram` names
49
+ # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
50
+ # one of C128::Model::ALL, with its SID `sid_model` when not nil.
43
51
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
44
52
  return vic20(model, ram) if family == :vic20
53
+ return C128.new(model:, sid_model:) if family == :c128
45
54
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
46
55
 
47
56
  profile = Model.named(model)
data/lib/badline/media.rb CHANGED
@@ -33,11 +33,11 @@ module Badline
33
33
  # autostart then loads through it.
34
34
  #
35
35
  # An .m3u or .vfl list of disks attaches the first disk it lists
36
- # (DiskList). A machine other than the C64 takes its own media
37
- # (Vic20Media).
36
+ # (DiskList). The VIC-20 takes its own media (Vic20Media), and the
37
+ # C128 in C64 mode a C64's.
38
38
  def attach(computer, path, autostart: true, subtune: nil, **)
39
39
  path = DiskList.disk(path)
40
- return Vic20Media.attach(computer, path, autostart:, **) unless computer.is_a?(Computer)
40
+ return Vic20Media.attach(computer, path, autostart:, **) if computer.family == :vic20
41
41
 
42
42
  attach_c64(computer, path, autostart:, subtune:, **)
43
43
  end
@@ -160,13 +160,15 @@ module Badline
160
160
  def start_prg(computer, data, autostart:)
161
161
  load_addr = computer.load_prg(data)
162
162
  # Run only makes sense for programs at BASIC start, or a byte ahead
163
- # of it, where BASIC keeps the zero before its first line.
164
- return unless autostart && (load_addr == BASIC_START || load_addr == BASIC_START - 1)
163
+ # of it, where BASIC keeps the zero before its first line. BASIC 7.0
164
+ # starts at $1C01 and keeps the end of its text at $1210.
165
+ start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
166
+ return unless autostart && (load_addr == start || load_addr == start - 1)
165
167
 
166
168
  # The end of the program, where BASIC's variables start, and where
167
169
  # the KERNAL's LOAD leaves its end address.
168
170
  end_addr = load_addr + data.length - 2
169
- [0x2d, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
171
+ [text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
170
172
  computer.type_text("run\r")
171
173
  end
172
174
  end
@@ -10,7 +10,7 @@ module Badline
10
10
  # left out.
11
11
  ACTIONS = {
12
12
  "key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true, "menu" => true,
13
- "reset" => false, "freeze" => false, "resume" => false, "quit" => false
13
+ "display" => true, "reset" => false, "freeze" => false, "resume" => false, "quit" => false
14
14
  }.freeze
15
15
 
16
16
  # The frames a key or the freeze button is held for.
@@ -18,6 +18,9 @@ module Badline
18
18
 
19
19
  EJECTABLE = %w[disk tape cartridge].freeze
20
20
 
21
+ # The C128's screens display shows: the VIC-IIe's or the VDC's.
22
+ DISPLAYS = %w[vic vdc].freeze
23
+
21
24
  # What insert takes: disk images, tapes and cartridges, or an .m3u
22
25
  # or .vfl list of disks or a directory to mount as device 8.
23
26
  INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
@@ -83,6 +86,7 @@ module Badline
83
86
  return true if action == "menu"
84
87
  return argument.empty? unless ACTIONS[action]
85
88
  return EJECTABLE.include?(argument) if action == "eject"
89
+ return DISPLAYS.include?(argument) if action == "display"
86
90
  return insertable?(argument) if action == "insert"
87
91
 
88
92
  !argument.empty?
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Options
5
+ # The machine family: the subcommand that names it, or else the media,
6
+ # and the models of each.
7
+ module Family
8
+ # Where C128 BASIC starts, which a program for the C128 loads at.
9
+ C128_BASIC_START = 0x1c01
10
+
11
+ private
12
+
13
+ # Takes `sid`, `c64`, `vic20` or `c128` when given first.
14
+ def subcommand(args)
15
+ case args.first
16
+ when "sid" then @sid_command = true
17
+ when "c64" then @family = :c64
18
+ when "vic20" then @family = :vic20
19
+ when "c128" then @family = :c128
20
+ else return
21
+ end
22
+ @family_named = !@sid_command
23
+ args.shift
24
+ end
25
+
26
+ def family_models
27
+ case @family
28
+ when :vic20 then VIC20_MODELS
29
+ when :c128 then C128_MODELS
30
+ else MODELS
31
+ end
32
+ end
33
+
34
+ # Without a family named, a program that loads at C128_BASIC_START is
35
+ # the C128's.
36
+ def pick_family
37
+ return if @family_named || @sid_command || @media_path.nil?
38
+ return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
39
+
40
+ @family = :c128 if load_address(@media_path) == C128_BASIC_START
41
+ end
42
+
43
+ # The load address of a .prg, or of the program a .p00 wraps after its
44
+ # 26-byte header.
45
+ def load_address(path)
46
+ offset = File.extname(path).casecmp?(".p00") ? 26 : 0
47
+ bytes = File.binread(path).bytes
48
+ bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
49
+ end
50
+ end
51
+ end
52
+ end