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
@@ -3,12 +3,10 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # Opens the window, then runs the machine a frame at a time: poll
6
- # events, clock the frame's cycles, queue the SID's samples, upload the
6
+ # events, clock the frame's cycles, queue the sound's samples, upload the
7
7
  # changed lines, present and wait. Pacer decides the cycles and the wait.
8
8
  class App
9
- SCALE = 2
10
9
  DROPFILE = 0x1000
11
- TITLE = "Badline"
12
10
 
13
11
  # Takes the frame limit, the pacing, the snapshot path, the sound and
14
12
  # the verbosity from Options, and runs the timeline's events.
@@ -16,17 +14,21 @@ module Badline
16
14
  @computer = computer
17
15
  @frame_limit = options.frames
18
16
  @verbose = options.verbose?
19
- @pacer = Pacer.new(paced: options.paced?, vsync: options.vsync?, verbose: @verbose, region: computer.region)
17
+ timing = computer.timing
18
+ @pacer = Pacer.new(paced: options.paced?, vsync: options.vsync?, verbose: @verbose, timing:)
20
19
  @timeline = timeline
21
20
  @snapshots = Snapshots.new(computer, options)
22
- @screen = Screen.new(computer.vic)
21
+ @screen = Screen.new(computer.video, timing.crop)
23
22
  @led = DriveLed.for(computer)
24
23
  @controls = Controls.new(computer)
25
- open_window
26
- @sound = Sound.new(computer.sid, computer.region.clock_hz, options.sound?, @verbose)
24
+ @window = MachineWindow.new(@screen, timing.pixel_width, vsync: @pacer.vsync?)
25
+ @pacer.fit(@window.refresh_rate) if @pacer.vsync?
26
+ @sound = Sound.new(computer.sound_source, timing.clock_hz, options.sound?, @verbose)
27
27
  @gamepads = Gamepads.new(computer, @verbose)
28
28
  @frame_report = FrameReport.new(@sound)
29
- @menu = PauseMenu.new(Painter.new(@renderer), options, @snapshots)
29
+ @menu = PauseMenu.new(Painter.new(@window.renderer), options, @snapshots)
30
+ @menu.center(@window.width, @window.height)
31
+ @scripted = false
30
32
  end
31
33
 
32
34
  def run
@@ -39,7 +41,7 @@ module Badline
39
41
  @computer.drive1541&.flush
40
42
  @gamepads.close
41
43
  @sound.close
42
- close_window
44
+ @window.close
43
45
  end
44
46
 
45
47
  private
@@ -70,36 +72,20 @@ module Badline
70
72
  @running = false if @frames == @frame_limit || @timeline.quit?(@frames)
71
73
  @pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
72
74
  report(stamps.last) if (@frames % 50).zero?
73
- end
74
-
75
- def open_window
76
- abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
77
-
78
- SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
79
- SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
80
- @window = SDL.SDL_CreateWindow(
81
- TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED,
82
- Screen::WIDTH * SCALE, Screen::HEIGHT * SCALE, SDL::WINDOW_RESIZABLE
83
- )
84
- flags = SDL::RENDERER_ACCELERATED
85
- flags |= SDL::RENDERER_PRESENTVSYNC if @pacer.vsync?
86
- @renderer = SDL.SDL_CreateRenderer(@window, -1, flags)
87
- fit_display if @pacer.vsync?
88
- SDL.SDL_RenderSetLogicalSize(@renderer, Screen::WIDTH, Screen::HEIGHT)
89
- create_texture
90
- end
91
-
92
- def create_texture
93
- @texture = SDL.SDL_CreateTexture(
94
- @renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, Screen::WIDTH, Screen::HEIGHT
95
- )
96
- end
97
-
98
- def close_window
99
- SDL.SDL_DestroyTexture(@texture)
100
- SDL.SDL_DestroyRenderer(@renderer)
101
- SDL.SDL_DestroyWindow(@window)
102
- SDL.SDL_Quit
75
+ menu_events
76
+ end
77
+
78
+ # Opens the menu at a menu event, on the page it names, and closes it
79
+ # at a resume. While the timeline holds it open, its frames count
80
+ # towards the events after it and --frames.
81
+ def menu_events
82
+ if @timeline.menu?(@frames)
83
+ @menu.section = @timeline.menu_section(@frames)
84
+ open_menu
85
+ @scripted = true
86
+ elsif @menu.open? && @timeline.resume?(@frames)
87
+ resume(false)
88
+ end
103
89
  end
104
90
 
105
91
  def handle_events
@@ -137,11 +123,14 @@ module Badline
137
123
  # it.
138
124
  def swap(computer)
139
125
  @computer = computer
140
- @screen = Screen.new(computer.vic)
126
+ timing = computer.timing
127
+ @screen = Screen.new(computer.video, timing.crop)
128
+ @window.fit(@screen, timing.pixel_width)
129
+ @menu.center(@window.width, @window.height)
141
130
  @led = DriveLed.for(computer)
142
131
  @controls.computer = computer
143
132
  @gamepads.computer = computer
144
- @sound.switch(computer.sid, computer.region.clock_hz)
133
+ @sound.switch(computer.sound_source, timing.clock_hz)
145
134
  @snapshots.computer = computer
146
135
  end
147
136
 
@@ -162,6 +151,7 @@ module Badline
162
151
  # pressing the cartridge's freeze button if asked.
163
152
  def resume(freeze)
164
153
  @menu.close
154
+ @scripted = false
165
155
  SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
166
156
  @timeline.press_freeze(@computer, @frames) if freeze
167
157
  @reported = now
@@ -170,50 +160,58 @@ module Badline
170
160
  end
171
161
 
172
162
  # Runs a frame of the menu, which has the keys and the mouse while the
173
- # machine stands still.
163
+ # machine stands still. A menu the timeline opened counts its frames
164
+ # and takes their screenshots.
174
165
  def paused_frame
175
- action = @menu.frame(@renderer, @texture, @controls)
166
+ scripted = @scripted
167
+ action = @menu.frame(@window.renderer, @window.texture, @controls)
168
+ present_menu(scripted)
176
169
  if action == :quit
177
170
  @running = false
178
171
  elsif !action.nil?
179
172
  swap(@menu.computer) if action == :swap
180
173
  resume(action == :freeze)
181
174
  end
175
+ finish_paused_frame if scripted && @running
182
176
  end
183
177
 
184
- def update_title
185
- title = TITLE
186
- title += " [#{@controls.tag}]" unless @controls.tag.empty?
187
- title += " [MUTED]" if @sound.muted?
188
- SDL.SDL_SetWindowTitle(@window, title)
178
+ def present_menu(scripted)
179
+ renderer = @window.renderer
180
+ @timeline.screenshots(@frames + 1).each { |path| Screenshot.write(renderer, path) } if scripted
181
+ SDL.SDL_RenderPresent(renderer)
182
+ SDL.SDL_Delay(10)
183
+ end
184
+
185
+ def finish_paused_frame
186
+ @frames += 1
187
+ @timeline.run(@computer, @frames)
188
+ @running = false if @frames == @frame_limit || @timeline.quit?(@frames)
189
+ menu_events
189
190
  end
190
191
 
191
- def fit_display
192
- SDL.SDL_GetWindowDisplayMode(@window, SDL.display_mode)
193
- @pacer.fit(SDL.mode_refresh(SDL.display_mode))
192
+ def update_title
193
+ tags = []
194
+ tags << @controls.tag unless @controls.tag.empty?
195
+ tags << "MUTED" if @sound.muted?
196
+ @window.title(tags)
194
197
  end
195
198
 
196
199
  def emulate
197
- computer = @computer
198
- cycles = @pacer.cycles(@sound)
199
- i = 0
200
- while i < cycles
201
- computer.cycle!
202
- i += 1
203
- end
200
+ @computer.run_cycles(@pacer.cycles(@sound))
204
201
  end
205
202
 
206
203
  def upload
207
204
  @screen.update
208
- SDL.SDL_UpdateTexture(@texture, nil, @screen.pixels, Screen::ROW_BYTES)
205
+ @window.upload(@screen)
209
206
  end
210
207
 
211
208
  def draw
212
- SDL.SDL_RenderClear(@renderer)
213
- SDL.SDL_RenderCopy(@renderer, @texture, nil, nil)
214
- @led&.draw(@renderer)
215
- @timeline.screenshots(@frames + 1).each { |path| Screenshot.write(@renderer, path) }
216
- SDL.SDL_RenderPresent(@renderer)
209
+ renderer = @window.renderer
210
+ SDL.SDL_RenderClear(renderer)
211
+ SDL.SDL_RenderCopy(renderer, @window.texture, nil, nil)
212
+ @led&.draw(renderer, @window.width, @window.height)
213
+ @timeline.screenshots(@frames + 1).each { |path| Screenshot.write(renderer, path) }
214
+ SDL.SDL_RenderPresent(renderer)
217
215
  end
218
216
 
219
217
  def report(at)
@@ -21,7 +21,7 @@ module Badline
21
21
  rescue Media::TrueDrive::Error, Storage::SIDFile::FormatError, Storage::T64::FormatError,
22
22
  Storage::TAP::FormatError, Storage::CRTFile::FormatError, Storage::G64Image::FormatError,
23
23
  Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
24
- Datasette::Missing => e
24
+ Datasette::Missing, Unsupported => e
25
25
  warn "#{options.program}: #{options.media_path}: #{e.message}"
26
26
  return 1
27
27
  end
@@ -45,14 +45,11 @@ module Badline
45
45
 
46
46
  # A new machine built from the options, with `media`, if not nil,
47
47
  # attached and started as the command line does, its disk writable if
48
- # `writable`. The model gives the chips, and a .sid tune or --sid the
49
- # SID.
48
+ # `writable`. The family and model give the machine, and a .sid tune
49
+ # or --sid the SID. The VIC-20 raises Unsupported for a .sid
50
+ # tune.
50
51
  def self.start(options, media, writable)
51
- model = Model.named(options.model)
52
- computer = Computer.new(vic_model: model.vic_model, cia_model: model.cia_model,
53
- sid_model: options.sid_model || Media.sid_model(media, otherwise: model.sid_model),
54
- region: model.region, reu: options.reu, kernal: model.kernal,
55
- datasette: model.datasette)
52
+ computer = options.family == :vic20 ? vic20(options, media) : c64(options, media)
56
53
  Media::TrueDrive.plug(computer) if options.true_drive?
57
54
  unless media.nil?
58
55
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
@@ -60,5 +57,33 @@ module Badline
60
57
  end
61
58
  computer
62
59
  end
60
+
61
+ def self.c64(options, media)
62
+ sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
63
+ Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
64
+ end
65
+
66
+ # A VIC-20 with the RAM --ram names, or else the RAM `media` needs
67
+ # (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
68
+ # media a VIC-20 doesn't take.
69
+ def self.vic20(options, media)
70
+ ram = options.ram
71
+ unless media.nil?
72
+ unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
73
+ raise Unsupported, "doesn't go in a VIC-20" if unsupported
74
+
75
+ ram ||= vic20_ram(media)
76
+ end
77
+ Machine.build(:vic20, model: options.model, ram:)
78
+ end
79
+
80
+ def self.vic20_ram(media)
81
+ ram = Media::Vic20Media.ram_for(media)
82
+ puts "RAM expansion: #{ram}"
83
+ ram
84
+ end
85
+
86
+ # Media the machine the options name doesn't take.
87
+ class Unsupported < ArgumentError; end
63
88
  end
64
89
  end
@@ -4,11 +4,11 @@ module Badline
4
4
  module Frontend
5
5
  # Routes host input to the machine. Two settings decide where it goes:
6
6
  #
7
- # - The keys: every host key goes to the C64 keyboard, or, as Tab
7
+ # - The keys: every host key goes to the machine's keyboard, or, as Tab
8
8
  # switches, the arrow cluster and space drive one joystick and WASD
9
9
  # and left shift the other. The arrows start on joystick 2, and
10
- # #swap_ports swaps them.
11
- # - A pot device the pause menu plugs into a control port: a 1351
10
+ # #swap_ports swaps them. The VIC-20's one joystick takes both.
11
+ # - A pot device the pause menu plugs into a C64 control port: a 1351
12
12
  # mouse or a pair of paddles, which the host mouse drives. Motion
13
13
  # turns the paddles or moves the mouse, and the host's left and right
14
14
  # buttons go to whichever lines the device puts them on.
@@ -65,7 +65,7 @@ module Badline
65
65
  # The title bar's tag for the settings, empty for the keyboard alone.
66
66
  def tag
67
67
  tags = []
68
- tags << "JOY #{@arrows_port}" if @joystick
68
+ tags << (@computer.family == :vic20 ? "JOY" : "JOY #{@arrows_port}") if @joystick
69
69
  tags << TAGS[POTS.index(@pot)] if pot_device?
70
70
  tags.join(", ")
71
71
  end
@@ -105,7 +105,7 @@ module Badline
105
105
  # RESTORE isn't in the key matrix, so it goes to the machine instead.
106
106
  def c64_key(key, down)
107
107
  if key == :restore
108
- @computer.press_restore if down
108
+ down ? @computer.press_restore : @computer.release_restore
109
109
  else
110
110
  down ? @computer.keyboard.press(key) : @computer.keyboard.release(key)
111
111
  end
@@ -113,9 +113,15 @@ module Badline
113
113
 
114
114
  def paddles? = POTS.index(@pot) >= 3
115
115
 
116
- # A fresh pot device on its port, and none on the other.
116
+ # A fresh pot device on its port, and none on the other. A machine
117
+ # without the ports takes none.
117
118
  def attach_pot_device
118
119
  ports = @computer.control_ports
120
+ if ports.nil?
121
+ @pot = :none
122
+ return
123
+ end
124
+
119
125
  ports.device1 = nil
120
126
  ports.device2 = nil
121
127
  return unless pot_device?
@@ -8,8 +8,6 @@ module Badline
8
8
  class DriveLed
9
9
  WIDTH = 12
10
10
  HEIGHT = 4
11
- LEFT = Screen::WIDTH - WIDTH - 8
12
- TOP = Screen::HEIGHT - HEIGHT - 6
13
11
 
14
12
  LIT = [0xff, 0x20, 0x20].freeze
15
13
  DARK = [0x40, 0x00, 0x00].freeze
@@ -20,11 +18,12 @@ module Badline
20
18
  drive ? new(drive) : nil
21
19
  end
22
20
 
23
- # Sets SDL.led_rect to where the LED goes.
24
- def self.place
21
+ # Sets SDL.led_rect to where the LED goes on a screen `width` by
22
+ # `height`.
23
+ def self.place(width, height)
25
24
  rect = SDL.led_rect
26
- SDL.rect_x(rect, LEFT)
27
- SDL.rect_y(rect, TOP)
25
+ SDL.rect_x(rect, width - WIDTH - 8)
26
+ SDL.rect_y(rect, height - HEIGHT - 6)
28
27
  SDL.rect_w(rect, WIDTH)
29
28
  SDL.rect_h(rect, HEIGHT)
30
29
  end
@@ -37,10 +36,10 @@ module Badline
37
36
 
38
37
  def color = lit? ? LIT : DARK
39
38
 
40
- # Fills the LED's rectangle in its colour, then puts back the black
41
- # that RenderClear clears with.
42
- def draw(renderer)
43
- DriveLed.place
39
+ # Fills the LED's rectangle on a screen `width` by `height` in its
40
+ # colour, then puts back the black that RenderClear clears with.
41
+ def draw(renderer, width, height)
42
+ DriveLed.place(width, height)
44
43
  rgb = color
45
44
  SDL.SDL_SetRenderDrawColor(renderer, rgb[0], rgb[1], rgb[2], 255)
46
45
  SDL.SDL_RenderFillRect(renderer, SDL.led_rect)
@@ -31,10 +31,10 @@ module Badline
31
31
 
32
32
  attr_reader :base_cycles, :seconds, :refresh, :trim
33
33
 
34
- # One frame of the region's raster.
35
- def self.machine(region)
36
- cycles = region.cycles_per_line * region.lines_per_frame
37
- new(cycles, cycles.to_f / region.clock_hz, 0, region.clock_hz)
34
+ # One frame of the machine's raster, from its Timing.
35
+ def self.machine(timing)
36
+ cycles = timing.cycles_per_line * timing.lines_per_frame
37
+ new(cycles, cycles.to_f / timing.clock_hz, 0, timing.clock_hz)
38
38
  end
39
39
 
40
40
  def self.display(refresh, clock_hz)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
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.
10
+ class MachineWindow
11
+ SCALE = 2
12
+ TITLE = "Badline"
13
+
14
+ # The logical size, in square pixels.
15
+ attr_reader :width, :height
16
+
17
+ attr_reader :renderer, :texture
18
+
19
+ # Opens the window for `screen`, presenting in step with the display
20
+ # when `vsync`.
21
+ def initialize(screen, pixel_width, vsync:)
22
+ abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
23
+
24
+ SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
25
+ SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
26
+ measure(screen, pixel_width)
27
+ @window = SDL.SDL_CreateWindow(
28
+ TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
29
+ SDL::WINDOW_RESIZABLE
30
+ )
31
+ flags = SDL::RENDERER_ACCELERATED
32
+ flags |= SDL::RENDERER_PRESENTVSYNC if vsync
33
+ @renderer = SDL.SDL_CreateRenderer(@window, -1, flags)
34
+ create_texture
35
+ end
36
+
37
+ # The refresh rate of the display the window is on, in Hz.
38
+ def refresh_rate
39
+ SDL.SDL_GetWindowDisplayMode(@window, SDL.display_mode)
40
+ SDL.mode_refresh(SDL.display_mode)
41
+ end
42
+
43
+ # Fits the texture, the logical size and the window to another
44
+ # machine's screen, unless it measures the same.
45
+ def fit(screen, pixel_width)
46
+ before = [@width, @height, @texture_width]
47
+ measure(screen, pixel_width)
48
+ return if before == [@width, @height, @texture_width]
49
+
50
+ SDL.SDL_DestroyTexture(@texture)
51
+ create_texture
52
+ SDL.SDL_SetWindowSize(@window, @width * SCALE, @height * SCALE)
53
+ end
54
+
55
+ def upload(screen)
56
+ SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
57
+ end
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(" "))
62
+ end
63
+
64
+ def close
65
+ SDL.SDL_DestroyTexture(@texture)
66
+ SDL.SDL_DestroyRenderer(@renderer)
67
+ SDL.SDL_DestroyWindow(@window)
68
+ SDL.SDL_Quit
69
+ end
70
+
71
+ private
72
+
73
+ def measure(screen, pixel_width)
74
+ @texture_width = screen.width
75
+ @width = screen.width * pixel_width
76
+ @height = screen.height
77
+ end
78
+
79
+ def create_texture
80
+ SDL.SDL_RenderSetLogicalSize(@renderer, @width, @height)
81
+ @texture = SDL.SDL_CreateTexture(
82
+ @renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, @texture_width, @height
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
@@ -4,17 +4,17 @@ module Badline
4
4
  module Frontend
5
5
  # What the pause menu puts in the machine and takes out: disks, tapes
6
6
  # and cartridges, whether disks are writable, which carries over from
7
- # one disk to the next, and the disk's set, which PREVIOUS and NEXT step
8
- # through: the .m3u or .vfl list it came from, while that lists it, or
9
- # the set DiskSet finds. It keeps what last went wrong for the page to
10
- # show.
7
+ # one disk to the next, and the set of the disk in device 8, which
8
+ # PREVIOUS and NEXT step through: the .m3u or .vfl list it came from,
9
+ # while that lists it, or the set DiskSet finds. It keeps what last went
10
+ # wrong for the page to show.
11
11
  class MenuMedia
12
- attr_reader :disk_path, :problem
12
+ attr_reader :problem
13
13
  attr_writer :computer
14
14
 
15
- # Starts on the disk the command line's `media_path` put in.
15
+ # Takes the disk set from the command line's `media_path` when it's a
16
+ # list.
16
17
  def initialize(media_path, writable)
17
- @disk_path = PauseMenu.disk_path(media_path)
18
18
  @list = Media::DiskList.list?(media_path) ? media_path : ""
19
19
  @writable = writable
20
20
  @set_path = ""
@@ -25,26 +25,40 @@ module Badline
25
25
 
26
26
  def writable? = @writable
27
27
 
28
+ # The expanded path of the disk in device 8, however it went in, or
29
+ # an empty one.
30
+ def disk_path
31
+ drive = @computer.drive1541
32
+ unless drive.nil?
33
+ disk = drive.disk
34
+ return disk.nil? ? "" : disk.path.to_s
35
+ end
36
+
37
+ path = @computer.mounted_path
38
+ path.empty? ? path : File.expand_path(path)
39
+ end
40
+
28
41
  def inserted?
29
42
  drive = @computer.drive1541
30
43
  drive.nil? ? @computer.mounted? : !drive.disk.nil?
31
44
  end
32
45
 
33
46
  # Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
34
- # :tape. A list puts its first disk in.
47
+ # :tape, and says whether it went in. A list puts its first disk in.
35
48
  def insert(kind, path)
36
49
  @problem = ""
37
50
  if kind == :disk
38
51
  disk = Media::DiskList.disk(path)
39
52
  Media.insert_disk(@computer, disk, read_only: !@writable)
40
53
  list(path)
41
- @disk_path = disk
42
54
  else
43
55
  @computer.datasette.insert(Storage::TAP.new(path))
44
56
  end
57
+ true
45
58
  rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
46
59
  Storage::G64Image::FormatError => e
47
60
  @problem = e.message
61
+ false
48
62
  end
49
63
 
50
64
  # Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
@@ -61,24 +75,25 @@ module Badline
61
75
  def eject_disk
62
76
  drive = @computer.drive1541
63
77
  drive.nil? ? @computer.unmount : drive.insert(nil)
64
- @disk_path = ""
65
78
  end
66
79
 
67
80
  # Sets whether disks are writable, for the disk in the drive too,
68
81
  # which goes in again as the notch now says.
69
82
  def writable(writable)
70
83
  @writable = writable
71
- insert(:disk, @disk_path) if inserted? && !@disk_path.empty?
84
+ path = disk_path
85
+ insert(:disk, path) if inserted? && !path.empty?
72
86
  end
73
87
 
74
88
  # The disks of the set the disk in the drive belongs to.
75
89
  def disk_set
76
- return [] if @disk_path.empty?
90
+ path = disk_path
91
+ return [] if path.empty?
77
92
 
78
- unless @set_path == @disk_path
79
- listed = @list.empty? ? [] : Media::DiskList.disks(@list)
80
- @set = listed.include?(@disk_path) ? listed : Media::DiskSet.around(@disk_path)
81
- @set_path = @disk_path
93
+ unless @set_path == path
94
+ listed = @list.empty? ? [] : expanded(Media::DiskList.disks(@list))
95
+ @set = listed.include?(path) ? listed : expanded(Media::DiskSet.around(path))
96
+ @set_path = path
82
97
  end
83
98
  @set
84
99
  end
@@ -87,7 +102,7 @@ module Badline
87
102
  # drive, if there is one.
88
103
  def step_disk(step)
89
104
  set = disk_set
90
- index = set.index(@disk_path)
105
+ index = set.index(disk_path)
91
106
  return if index.nil? || !(index + step).between?(0, set.size - 1)
92
107
 
93
108
  insert(:disk, set[index + step])
@@ -105,14 +120,23 @@ module Badline
105
120
  end
106
121
 
107
122
  # What a save of the machine is named after: the cartridge, the disk,
108
- # the tape, or BASIC, without the bracketed parts of a file's name.
123
+ # the tape, or BASIC, without the bracketed parts of a file's name. A
124
+ # VIC-20's are named after its disk, or else the machine.
109
125
  def game_name
110
- name = cartridge_name
111
- name = File.basename(@disk_path, File.extname(@disk_path)) if name.empty? && !@disk_path.empty?
112
- tape = @computer.datasette.tape
113
- name = File.basename(tape.path, File.extname(tape.path)) if name.empty? && !tape.nil?
126
+ vic20 = @computer.family == :vic20
127
+ name = vic20 ? "" : cartridge_name
128
+ disk = disk_path
129
+ name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
130
+ name = tape_name if name.empty? && !vic20
114
131
  name = name.sub(/\s*[(\[].*\z/, "").strip
115
- name.empty? ? "BASIC" : name
132
+ return name unless name.empty?
133
+
134
+ vic20 ? "VIC-20" : "BASIC"
135
+ end
136
+
137
+ def tape_name
138
+ tape = @computer.datasette.tape
139
+ tape.nil? ? "" : File.basename(tape.path, File.extname(tape.path))
116
140
  end
117
141
 
118
142
  # The name the cartridge in the expansion port gives itself, or an
@@ -133,7 +157,6 @@ module Badline
133
157
  def start(options, path)
134
158
  @problem = ""
135
159
  computer = Frontend.start(options, path, @writable)
136
- @disk_path = PauseMenu.disk_path(path)
137
160
  list(path)
138
161
  computer
139
162
  rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
@@ -152,6 +175,8 @@ module Badline
152
175
  @list = path
153
176
  @set_path = ""
154
177
  end
178
+
179
+ def expanded(paths) = paths.map { |path| File.expand_path(path) }
155
180
  end
156
181
  end
157
182
  end