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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/library.md +15 -1
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +17 -3
- data/lib/badline/address_bus/fittings.rb +38 -0
- data/lib/badline/address_bus/pla.rb +160 -0
- data/lib/badline/address_bus/roms.rb +12 -5
- data/lib/badline/address_bus.rb +15 -90
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +47 -7
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +52 -37
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +3 -0
- data/lib/badline/datasette.rb +39 -1
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +62 -64
- data/lib/badline/frontend/boot.rb +33 -8
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_media.rb +49 -24
- data/lib/badline/frontend/menu_pages.rb +23 -40
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +30 -33
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +1 -1
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/sound.rb +11 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +2 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +16 -27
- data/lib/badline/kernal_trap/routine.rb +11 -27
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +21 -19
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +28 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +32 -18
- data/lib/badline/model.rb +29 -12
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +12 -2
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +51 -0
- data/lib/badline/roms/c64gs.rom +0 -0
- data/lib/badline/roms/kernal-pet64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +15 -10
- data/lib/badline/snapshot/state.rb +5 -3
- data/lib/badline/snapshot/vice.rb +2 -1
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +3 -0
- data/lib/badline/storage/tap.rb +38 -4
- data/lib/badline/storage/unavailable.rb +2 -0
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +55 -17
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +48 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- metadata +31 -2
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:,
|
|
27
|
+
def initialize(paced:, vsync:, verbose:, timing:)
|
|
27
28
|
@paced = paced
|
|
28
29
|
@verbose = verbose
|
|
29
30
|
@vsync = paced && vsync
|
|
30
|
-
@
|
|
31
|
-
@rate = FrameRate.machine(
|
|
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, @
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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,
|
|
120
|
-
# any, leaving the events after one for the
|
|
121
|
-
# still reach the machine, so no key stays held
|
|
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(
|
|
141
|
-
painter.box(
|
|
142
|
-
painter.text(
|
|
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(
|
|
145
|
-
return @dialogs.draw(
|
|
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,
|
|
144
|
+
painter.box(@left + BODY - 8, @top + 32, 1, HEIGHT - 56, EDGE)
|
|
148
145
|
draw_sections
|
|
149
|
-
@buttons.plain(
|
|
150
|
-
painter.text(
|
|
151
|
-
@pages.draw(PAGES[@section], BODY,
|
|
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 =
|
|
253
|
+
top = @top + 32
|
|
257
254
|
SECTIONS.each_with_index do |name, index|
|
|
258
|
-
@buttons.plain(
|
|
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
|
|
@@ -2,60 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The visible part of the
|
|
6
|
-
#
|
|
7
|
-
# Array of Integers to C as 64-bit
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
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
|
-
#
|
|
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
|
-
|
|
40
|
+
video = @video
|
|
41
|
+
dirty = video.dirty_lines
|
|
42
|
+
display = video.display
|
|
41
43
|
row = 0
|
|
42
|
-
while row < @
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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] =
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
26
|
-
@
|
|
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
|
|
44
|
-
def switch(
|
|
45
|
-
@
|
|
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 = @
|
|
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 = @
|
|
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
|
|
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
|
-
|
|
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
|
|
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"
|
data/lib/badline/frontend.rb
CHANGED
|
@@ -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"
|