badline 0.9.0 → 0.10.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/library.md +15 -1
  6. data/doc/pinned-behaviour.md +146 -0
  7. data/doc/snapshots.md +17 -3
  8. data/lib/badline/address_bus/fittings.rb +38 -0
  9. data/lib/badline/address_bus/pla.rb +160 -0
  10. data/lib/badline/address_bus/roms.rb +12 -5
  11. data/lib/badline/address_bus.rb +15 -90
  12. data/lib/badline/chrout_trap.rb +4 -3
  13. data/lib/badline/computer/attachments.rb +47 -7
  14. data/lib/badline/computer/saved_state.rb +1 -0
  15. data/lib/badline/computer.rb +52 -37
  16. data/lib/badline/control_ports.rb +10 -3
  17. data/lib/badline/core.rb +3 -0
  18. data/lib/badline/datasette.rb +39 -1
  19. data/lib/badline/drive1541/serial_port.rb +5 -8
  20. data/lib/badline/drive1541/sleep.rb +5 -6
  21. data/lib/badline/drive1541.rb +3 -3
  22. data/lib/badline/frontend/app.rb +62 -64
  23. data/lib/badline/frontend/boot.rb +33 -8
  24. data/lib/badline/frontend/controls.rb +12 -6
  25. data/lib/badline/frontend/drive_led.rb +9 -10
  26. data/lib/badline/frontend/frame_rate.rb +4 -4
  27. data/lib/badline/frontend/machine_window.rb +87 -0
  28. data/lib/badline/frontend/menu_media.rb +49 -24
  29. data/lib/badline/frontend/menu_pages.rb +23 -40
  30. data/lib/badline/frontend/menu_ports.rb +69 -0
  31. data/lib/badline/frontend/pacer.rb +6 -5
  32. data/lib/badline/frontend/pause_menu.rb +30 -33
  33. data/lib/badline/frontend/player_screen.rb +1 -1
  34. data/lib/badline/frontend/player_window.rb +1 -1
  35. data/lib/badline/frontend/screen.rb +39 -37
  36. data/lib/badline/frontend/sid_view.rb +1 -1
  37. data/lib/badline/frontend/sound.rb +11 -9
  38. data/lib/badline/frontend/timeline.rb +36 -5
  39. data/lib/badline/frontend.rb +2 -0
  40. data/lib/badline/iec_bus.rb +16 -28
  41. data/lib/badline/kernal_trap/drive.rb +3 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +112 -0
  44. data/lib/badline/kernal_trap/load.rb +16 -27
  45. data/lib/badline/kernal_trap/routine.rb +11 -27
  46. data/lib/badline/kernal_trap/save.rb +13 -23
  47. data/lib/badline/kernal_trap/serial.rb +21 -19
  48. data/lib/badline/kernal_trap.rb +1 -0
  49. data/lib/badline/keyboard.rb +28 -14
  50. data/lib/badline/machine.rb +60 -0
  51. data/lib/badline/media/vic20_basic.rb +54 -0
  52. data/lib/badline/media/vic20_cartridge.rb +74 -0
  53. data/lib/badline/media/vic20_media.rb +132 -0
  54. data/lib/badline/media.rb +32 -18
  55. data/lib/badline/model.rb +29 -12
  56. data/lib/badline/options/event.rb +5 -3
  57. data/lib/badline/options/help.rb +11 -3
  58. data/lib/badline/options/table.rb +12 -2
  59. data/lib/badline/options/validation.rb +55 -4
  60. data/lib/badline/options.rb +33 -33
  61. data/lib/badline/roms/README +51 -0
  62. data/lib/badline/roms/c64gs.rom +0 -0
  63. data/lib/badline/roms/kernal-pet64.rom +0 -0
  64. data/lib/badline/roms/vic20/basic.rom +0 -0
  65. data/lib/badline/roms/vic20/character.rom +0 -0
  66. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  67. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  68. data/lib/badline/snapshot/container.rb +2 -0
  69. data/lib/badline/snapshot/image.rb +15 -0
  70. data/lib/badline/snapshot/setup.rb +15 -10
  71. data/lib/badline/snapshot/state.rb +5 -3
  72. data/lib/badline/snapshot/vice.rb +2 -1
  73. data/lib/badline/snapshot.rb +10 -0
  74. data/lib/badline/storage/crt_file.rb +16 -5
  75. data/lib/badline/storage/disk_image.rb +3 -0
  76. data/lib/badline/storage/host_directory.rb +3 -0
  77. data/lib/badline/storage/t64.rb +3 -0
  78. data/lib/badline/storage/tap.rb +38 -4
  79. data/lib/badline/storage/unavailable.rb +2 -0
  80. data/lib/badline/timing.rb +12 -0
  81. data/lib/badline/version.rb +1 -1
  82. data/lib/badline/via/shift_register.rb +43 -16
  83. data/lib/badline/vic/bank.rb +55 -17
  84. data/lib/badline/vic/registers.rb +20 -4
  85. data/lib/badline/vic.rb +48 -15
  86. data/lib/badline/vic20/attachments.rb +103 -0
  87. data/lib/badline/vic20/bus.rb +308 -0
  88. data/lib/badline/vic20/cpu.rb +10 -0
  89. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  90. data/lib/badline/vic20/port_wiring.rb +50 -0
  91. data/lib/badline/vic20/saved_state.rb +152 -0
  92. data/lib/badline/vic20/sound/output.rb +83 -0
  93. data/lib/badline/vic20/sound.rb +233 -0
  94. data/lib/badline/vic20/user_via_ports.rb +47 -0
  95. data/lib/badline/vic20/vic/painter.rb +239 -0
  96. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  97. data/lib/badline/vic20/vic/text_window.rb +71 -0
  98. data/lib/badline/vic20/vic.rb +267 -0
  99. data/lib/badline/vic20.rb +220 -0
  100. metadata +31 -2
  101. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -7,6 +7,8 @@ module Badline
7
7
  # row for each thing to do, a setting showing its value on the right,
8
8
  # which a press steps on.
9
9
  class MenuPages
10
+ include MenuPorts
11
+
10
12
  TEXT = PauseMenu::TEXT
11
13
  DIM = PauseMenu::DIM
12
14
  LINE = 12
@@ -16,10 +18,11 @@ module Badline
16
18
  WARNING = 0xff7a6b
17
19
  RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
18
20
 
19
- PORT_DEVICES = %i[joystick mouse paddles].freeze
20
- PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
21
- PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
22
- POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
21
+ SID_MODELS = %i[mos6581 mos8580].freeze
22
+ RAM_NAMES = {
23
+ unexpanded: "", "3k": "3K", "8k": "8K", "16k": "16K", "24k": "24K", "32k": "32K",
24
+ all: "35K, ALL BLOCKS"
25
+ }.freeze
23
26
 
24
27
  # The disks, tapes and cartridges the pages put in and take out.
25
28
  attr_reader :media
@@ -75,7 +78,7 @@ module Badline
75
78
  when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
76
79
  when :swap then @controls.swap_ports
77
80
  when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
78
- when :sid6581, :sid8580 then @computer.sid.model = action == :sid8580 ? :mos8580 : :mos6581
81
+ when :sid6581, :sid8580 then @computer.sound_source.model = action == :sid8580 ? :mos8580 : :mos6581
79
82
  when :reset then @computer.reset!
80
83
  when :power_cycle then @computer.power_cycle!
81
84
  else pick_port(action)
@@ -84,6 +87,14 @@ module Badline
84
87
 
85
88
  private
86
89
 
90
+ def vic20? = @computer.family == :vic20
91
+
92
+ # Says in the dim colour that the VIC-20 hasn't got what the page
93
+ # shows.
94
+ def note
95
+ @painter.text(@left, @top, "NOT ON THE VIC-20 YET", DIM)
96
+ end
97
+
87
98
  def draw_drive
88
99
  media = @media
89
100
  inserted = media.inserted?
@@ -114,6 +125,8 @@ module Badline
114
125
  end
115
126
 
116
127
  def draw_expansion
128
+ return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
129
+
117
130
  cartridge = @computer.address_bus.cartridge
118
131
  info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
119
132
  row("INSERT...", :insert_cartridge)
@@ -125,27 +138,17 @@ module Badline
125
138
  info("RAM EXPANSION", "REU, #{@reu_kb} KB")
126
139
  end
127
140
 
128
- def draw_ports
129
- toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
130
- toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
131
- joystick = @controls.joystick_mode?
132
- toggle("KEYS", :keys, [%w[C64 JOYSTICK], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
133
- return unless joystick
134
-
135
- row("SWAP JOYSTICKS", :swap)
136
- skip
137
- arrows = @controls.arrows_port
138
- info("ARROWS, SPACE", "PORT #{arrows}")
139
- info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
140
- end
141
-
141
+ # The SID's model is a setting, and other sound chips have none.
142
142
  def draw_sound
143
143
  if @sound.on?
144
144
  toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
145
145
  else
146
146
  info("OUTPUT", "OFF")
147
147
  end
148
- toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], @computer.sid.model == :mos8580 ? 1 : 0])
148
+ model = @computer.sound_source.model
149
+ return unless SID_MODELS.include?(model)
150
+
151
+ toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
149
152
  end
150
153
 
151
154
  # Saving now and by name, loading a named save, and the quicksaves
@@ -195,26 +198,6 @@ module Badline
195
198
  def skip
196
199
  @top += LINE
197
200
  end
198
-
199
- def keys(mode) = @controls.joystick_mode = mode == :joystick
200
-
201
- def port_device(port)
202
- index = POTS[port - 1].index(@controls.pot)
203
- index.nil? ? :joystick : PORT_DEVICES[index]
204
- end
205
-
206
- # Plugs the device a choice such as :port1_mouse names into its port.
207
- def pick_port(action)
208
- index = PORT_ACTIONS[0].include?(action) ? 0 : 1
209
- choice = PORT_ACTIONS[index].index(action)
210
- plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
211
- end
212
-
213
- # Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
214
- # the pot device.
215
- def plug_device(port, choice)
216
- @controls.plug(POTS[port - 1][choice])
217
- end
218
201
  end
219
202
  end
220
203
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The pause menu's PORTS page, which MenuPages draws: what each C64
6
+ # control port has plugged in, a joystick or a pot device, and whether
7
+ # the keys type or drive the joysticks. The VIC-20 has one port, a
8
+ # joystick's.
9
+ module MenuPorts
10
+ PORT_DEVICES = %i[joystick mouse paddles].freeze
11
+ PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
12
+ PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
13
+ POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
14
+
15
+ private
16
+
17
+ def draw_ports
18
+ return draw_vic20_port if @computer.family == :vic20
19
+
20
+ toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
21
+ toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
22
+ return unless draw_keys("C64")
23
+
24
+ row("SWAP JOYSTICKS", :swap)
25
+ skip
26
+ arrows = @controls.arrows_port
27
+ info("ARROWS, SPACE", "PORT #{arrows}")
28
+ info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
29
+ end
30
+
31
+ # The VIC-20's one joystick, which both sets of keys drive.
32
+ def draw_vic20_port
33
+ return unless draw_keys("VIC-20")
34
+
35
+ skip
36
+ info("ARROWS, SPACE", "JOYSTICK")
37
+ info("WASD, LEFT SHIFT", "JOYSTICK")
38
+ end
39
+
40
+ # The toggle between the keyboard named and the joysticks, and
41
+ # whether the keys drive the joysticks.
42
+ def draw_keys(keyboard)
43
+ joystick = @controls.joystick_mode?
44
+ toggle("KEYS", :keys, [[keyboard, "JOYSTICK"], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
45
+ joystick
46
+ end
47
+
48
+ def keys(mode) = @controls.joystick_mode = mode == :joystick
49
+
50
+ def port_device(port)
51
+ index = POTS[port - 1].index(@controls.pot)
52
+ index.nil? ? :joystick : PORT_DEVICES[index]
53
+ end
54
+
55
+ # Plugs the device a choice such as :port1_mouse names into its port.
56
+ def pick_port(action)
57
+ index = PORT_ACTIONS[0].include?(action) ? 0 : 1
58
+ choice = PORT_ACTIONS[index].index(action)
59
+ plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
60
+ end
61
+
62
+ # Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
63
+ # the pot device.
64
+ def plug_device(port, choice)
65
+ @controls.plug(POTS[port - 1][choice])
66
+ end
67
+ end
68
+ end
69
+ end
@@ -13,7 +13,8 @@ module Badline
13
13
  # SAFETY, so no samples are dropped. #measure fits the frames to the
14
14
  # rate the display presents at, measured over the whole run.
15
15
  #
16
- # Without vsync, a frame is one of the machine's frames, 20 ms on PAL.
16
+ # Without vsync, a frame is one of the machine's frames, as its Timing
17
+ # gives them, 20 ms on PAL.
17
18
  # With the sound playing, #wait lasts until the audio queue is down to
18
19
  # Sound::AHEAD; otherwise it waits out the rest of the frame.
19
20
  class Pacer
@@ -23,12 +24,12 @@ module Badline
23
24
 
24
25
  attr_reader :rate
25
26
 
26
- def initialize(paced:, vsync:, verbose:, region: Region::PAL)
27
+ def initialize(paced:, vsync:, verbose:, timing:)
27
28
  @paced = paced
28
29
  @verbose = verbose
29
30
  @vsync = paced && vsync
30
- @region = region
31
- @rate = FrameRate.machine(region)
31
+ @timing = timing
32
+ @rate = FrameRate.machine(timing)
32
33
  @deadline = 0.0
33
34
  @measured_frames = 0
34
35
  @measured_seconds = 0.0
@@ -39,7 +40,7 @@ module Badline
39
40
  # Fits the frames to the display's refresh rate, once the window has
40
41
  # been opened with vsync.
41
42
  def fit(refresh)
42
- @rate = FrameRate.display(refresh, @region.clock_hz)
43
+ @rate = FrameRate.display(refresh, @timing.clock_hz)
43
44
  puts "Display #{@rate.refresh} Hz, vsync: #{@rate.base_cycles} cycles a frame" if @verbose
44
45
  end
45
46
 
@@ -16,7 +16,6 @@ module Badline
16
16
  class PauseMenu
17
17
  SECTIONS = ["SNAPSHOTS", "DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
18
18
  PAGES = %i[snapshots drive datasette expansion ports sound power].freeze
19
- DISKS = %w[.d64 .d71 .d81 .g64 .t64].freeze
20
19
 
21
20
  PANEL = 0x1d2230
22
21
  EDGE = 0x3a4256
@@ -25,11 +24,10 @@ module Badline
25
24
  DIM = 0x6b7489
26
25
  FILL = 0x343c50
27
26
 
28
- LEFT = 16
29
- TOP = 16
30
- WIDTH = Screen::WIDTH - 32
31
- HEIGHT = Screen::HEIGHT - 32
32
- BODY = 154
27
+ MARGIN = 16
28
+ WIDTH = 352
29
+ HEIGHT = 240
30
+ BODY = 138
33
31
 
34
32
  ESCAPE = 41
35
33
  RETURN = 40
@@ -48,20 +46,15 @@ module Badline
48
46
  @dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options, snapshots)
49
47
  @open = false
50
48
  @section = 0
51
- end
52
-
53
- # The path of the disk the command line puts in device 8, a list's
54
- # first, or an empty one for other media.
55
- def self.disk_path(path)
56
- return "" if path.empty?
57
- return path if File.directory?(path)
58
- return Media::DiskList.disks(path).first.to_s if Media::DiskList.list?(path)
59
-
60
- DISKS.include?(File.extname(path).downcase) ? path : ""
49
+ @left = MARGIN
50
+ @top = MARGIN
61
51
  end
62
52
 
63
53
  def open? = @open
64
54
 
55
+ # The index in PAGES of the page #show opens on.
56
+ attr_writer :section
57
+
65
58
  # The machine Quick open started, which the app runs in place of the
66
59
  # one before once #frame returns :swap.
67
60
  def computer = @dialogs.computer
@@ -116,10 +109,10 @@ module Badline
116
109
  end
117
110
 
118
111
  # Handles the events that came in, draws the menu over the frozen
119
- # picture, dimmed, and returns the action the app has to take on, if
120
- # any, leaving the events after one for the machine. Key releases
121
- # still reach the machine, so no key stays held down while it stands
122
- # still.
112
+ # picture, dimmed, for the app to present, and returns the action the
113
+ # app has to take on, if any, leaving the events after one for the
114
+ # machine. Key releases still reach the machine, so no key stays held
115
+ # down while it stands still.
123
116
  def frame(renderer, texture, controls)
124
117
  action = nil
125
118
  action = event(SDL.event_type(SDL.event), controls) while action.nil? && SDL.SDL_PollEvent(SDL.event) != 0
@@ -129,26 +122,30 @@ module Badline
129
122
  SDL.SDL_SetTextureColorMod(texture, 255, 255, 255)
130
123
  draw
131
124
  SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
132
- SDL.SDL_RenderPresent(renderer)
133
- SDL.SDL_Delay(10)
134
125
  action
135
126
  end
136
127
 
128
+ # Centres the menu on a screen `width` by `height`.
129
+ def center(width, height)
130
+ @left = (width - WIDTH) / 2
131
+ @top = (height - HEIGHT) / 2
132
+ end
133
+
137
134
  def draw
138
135
  painter = @painter
139
136
  @buttons.forget
140
- painter.box(LEFT, TOP, WIDTH, HEIGHT, PANEL)
141
- painter.box(LEFT, TOP + 24, WIDTH, 1, EDGE)
142
- painter.text(LEFT + 8, TOP + 9, "PAUSED", BRIGHT)
137
+ painter.box(@left, @top, WIDTH, HEIGHT, PANEL)
138
+ painter.box(@left, @top + 24, WIDTH, 1, EDGE)
139
+ painter.text(@left + 8, @top + 9, "PAUSED", BRIGHT)
143
140
  resume = "RESUME"
144
- @buttons.plain(LEFT + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), TOP + 7, resume, :resume)
145
- return @dialogs.draw(LEFT + 8, TOP + 34, WIDTH - 16) if @dialogs.open?
141
+ @buttons.plain(@left + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), @top + 7, resume, :resume)
142
+ return @dialogs.draw(@left + 8, @top + 34, WIDTH - 16) if @dialogs.open?
146
143
 
147
- painter.box(BODY - 8, TOP + 32, 1, HEIGHT - 56, EDGE)
144
+ painter.box(@left + BODY - 8, @top + 32, 1, HEIGHT - 56, EDGE)
148
145
  draw_sections
149
- @buttons.plain(LEFT + 6, TOP + HEIGHT - 28, "QUICK OPEN...", :quick_open)
150
- painter.text(LEFT + 8, TOP + HEIGHT - 12, "F9/ESC: RESUME", DIM)
151
- @pages.draw(PAGES[@section], BODY, TOP + 34)
146
+ @buttons.plain(@left + 6, @top + HEIGHT - 28, "QUICK OPEN...", :quick_open)
147
+ painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", DIM)
148
+ @pages.draw(PAGES[@section], @left + BODY, @top + 34)
152
149
  end
153
150
 
154
151
  private
@@ -253,9 +250,9 @@ module Badline
253
250
  end
254
251
 
255
252
  def draw_sections
256
- top = TOP + 32
253
+ top = @top + 32
257
254
  SECTIONS.each_with_index do |name, index|
258
- @buttons.plain(LEFT + 6, top, name.ljust(14), PAGES[index], on: index == @section)
255
+ @buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
259
256
  top += Buttons::HEIGHT + 2
260
257
  end
261
258
  end
@@ -10,7 +10,7 @@ module Badline
10
10
  HEADER = 64
11
11
  SCALE = 2
12
12
  TITLE = "Badline"
13
- BACKGROUND = Screen::COLORS[6]
13
+ BACKGROUND = VIC::PALETTE[6]
14
14
 
15
15
  attr_reader :buttons, :info
16
16
 
@@ -23,7 +23,7 @@ module Badline
23
23
  UP = 82
24
24
  DOWN = 81
25
25
 
26
- WARNING = Screen::COLORS[10]
26
+ WARNING = VIC::PALETTE[10]
27
27
 
28
28
  CHIPS = PlayerHeader::CHIPS
29
29
 
@@ -2,60 +2,62 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The visible part of the VIC's display, the crop of the machine's
6
- # region, repacked for an SDL texture in XRGB8888. Spinel hands an
7
- # Array of Integers to C as 64-bit words, as badline/ffi does on CRuby,
8
- # so each word carries two neighbouring pixels, the left one in the low
9
- # half. The texture is PAL's crop, 384x272, and a region with fewer
10
- # lines to show sits in the middle of it, between black bands.
5
+ # The visible part of the video chip's display, the crop of the
6
+ # machine's timing, repacked for an SDL texture in XRGB8888 in the
7
+ # chip's palette. Spinel hands an Array of Integers to C as 64-bit
8
+ # words, as badline/ffi does on CRuby, so each word carries two
9
+ # neighbouring pixels, the left one in the low half.
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
15
  class Screen
12
- WIDTH = Region::PAL.crop[2]
13
- HEIGHT = Region::PAL.crop[3]
14
- ROW_BYTES = WIDTH * 4
15
- ROW_WORDS = WIDTH / 2
16
+ MIN_HEIGHT = Region::PAL.crop[3]
16
17
 
17
- COLORS = [
18
- 0x000000, 0xffffff, 0x924a40, 0x84c5cc,
19
- 0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
20
- 0x675200, 0xc33d00, 0xc18178, 0x606060,
21
- 0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
22
- ].freeze
18
+ attr_reader :pixels, :width, :height
23
19
 
24
- PAIRS = Array.new(256) { |pair| COLORS[pair & 0x0f] | (COLORS[pair >> 4] << 32) }.freeze
25
-
26
- attr_reader :pixels
27
-
28
- def initialize(vic)
29
- @vic = vic
30
- crop = vic.region.crop
20
+ def initialize(video, crop)
21
+ @video = video
31
22
  @col_offset = crop[0]
32
23
  @row_offset = crop[1]
33
- @height = crop[3]
34
- @band = (HEIGHT - @height) / 2
35
- @pixels = Array.new(HEIGHT * ROW_WORDS, 0)
24
+ @lines = crop[3]
25
+ @width = crop[2]
26
+ @stride = video.width.to_i
27
+ @height = [@lines, MIN_HEIGHT].max
28
+ @row_words = @width / 2
29
+ @band = (@height - @lines) / 2
30
+ palette = video.palette
31
+ @pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
32
+ @pixels = Array.new(@height * @row_words, 0)
36
33
  end
37
34
 
38
- # Repacks only the lines the VIC has changed since the last frame.
35
+ # The bytes of one row of the texture.
36
+ def row_bytes = @width * 4
37
+
38
+ # Repacks only the lines the chip has changed since the last frame.
39
39
  def update
40
- dirty = @vic.dirty_lines
40
+ video = @video
41
+ dirty = video.dirty_lines
42
+ display = video.display
41
43
  row = 0
42
- while row < @height
43
- pack_row(row) if dirty[row + @row_offset]
44
+ while row < @lines
45
+ pack_row(display, row) if dirty[row + @row_offset]
44
46
  row += 1
45
47
  end
46
- @vic.clear_dirty_lines!
48
+ video.clear_dirty_lines!
47
49
  end
48
50
 
49
51
  private
50
52
 
51
- def pack_row(row)
52
- display = @vic.display
53
+ def pack_row(display, row)
53
54
  pixels = @pixels
54
- from = ((row + @row_offset) * @vic.width) + @col_offset
55
- to = (row + @band) * ROW_WORDS
56
- last = to + ROW_WORDS
55
+ pairs = @pairs
56
+ from = ((row + @row_offset) * @stride) + @col_offset
57
+ to = (row + @band) * @row_words
58
+ last = to + @row_words
57
59
  while to < last
58
- pixels[to] = PAIRS[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
60
+ pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
59
61
  from += 2
60
62
  to += 1
61
63
  end
@@ -8,7 +8,7 @@ module Badline
8
8
  # a tune on more than one gets a row of buttons above the voices that
9
9
  # pick it, which makes the view PICKER taller.
10
10
  class SIDView
11
- COLORS = Screen::COLORS
11
+ COLORS = VIC::PALETTE
12
12
  TEXT = COLORS[14]
13
13
  BRIGHT = COLORS[1]
14
14
  DIM = COLORS[11]
@@ -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,7 +15,7 @@ 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.
16
- # The SID's cycles are converted at `clock_hz`, the machine's clock, so
18
+ # The chip's cycles are converted at `clock_hz`, the machine's clock, so
17
19
  # an NTSC or Drean machine plays at its own pitch.
18
20
  class Sound
19
21
  RATE = 44_100
@@ -22,8 +24,8 @@ module Badline
22
24
 
23
25
  attr_reader :rate, :underruns, :dropped, :queued, :low, :high
24
26
 
25
- def initialize(sid, clock_hz, wanted, verbose)
26
- @sid = sid
27
+ def initialize(source, clock_hz, wanted, verbose)
28
+ @source = source
27
29
  @clock_hz = clock_hz
28
30
  @verbose = verbose
29
31
  @device = 0
@@ -40,9 +42,9 @@ module Badline
40
42
 
41
43
  def on? = @device != 0
42
44
 
43
- # Plays another machine's SID from here on, at that machine's clock.
44
- def switch(sid, clock_hz)
45
- @sid = sid
45
+ # Plays another machine's sound from here on, at that machine's clock.
46
+ def switch(source, clock_hz)
47
+ @source = source
46
48
  @clock_hz = clock_hz
47
49
  record if on?
48
50
  end
@@ -54,7 +56,7 @@ module Badline
54
56
  def feed
55
57
  return unless on?
56
58
 
57
- samples = @sid.drain_samples
59
+ samples = @source.drain_samples
58
60
  return if @muted || samples.empty?
59
61
 
60
62
  level = queued_seconds
@@ -110,7 +112,7 @@ module Badline
110
112
  puts "Sound at #{@rate} Hz" if @verbose
111
113
  end
112
114
 
113
- def record = @sid.record(rate: @rate, clock_hz: @clock_hz)
115
+ def record = @source.record(rate: @rate, clock_hz: @clock_hz)
114
116
 
115
117
  def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
116
118
 
@@ -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"
@@ -37,4 +38,5 @@ require "badline/frontend/confirmation"
37
38
  require "badline/frontend/name_field"
38
39
  require "badline/frontend/menu_dialogs"
39
40
  require "badline/frontend/menu_media"
41
+ require "badline/frontend/menu_ports"
40
42
  require "badline/frontend/menu_pages"