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
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
21
|
+
@screen = Screen.new(computer.video, timing.crop)
|
|
23
22
|
@led = DriveLed.for(computer)
|
|
24
23
|
@controls = Controls.new(computer)
|
|
25
|
-
|
|
26
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
SDL.
|
|
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
|
|
192
|
-
|
|
193
|
-
@
|
|
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
|
|
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
|
-
|
|
205
|
+
@window.upload(@screen)
|
|
209
206
|
end
|
|
210
207
|
|
|
211
208
|
def draw
|
|
212
|
-
|
|
213
|
-
SDL.
|
|
214
|
-
|
|
215
|
-
@
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
27
|
-
SDL.rect_y(rect,
|
|
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
|
|
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
|
|
35
|
-
def self.machine(
|
|
36
|
-
cycles =
|
|
37
|
-
new(cycles, cycles.to_f /
|
|
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
|
|
8
|
-
# through: the .m3u or .vfl list it came from,
|
|
9
|
-
# the set DiskSet finds. It keeps what last went
|
|
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 :
|
|
12
|
+
attr_reader :problem
|
|
13
13
|
attr_writer :computer
|
|
14
14
|
|
|
15
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
90
|
+
path = disk_path
|
|
91
|
+
return [] if path.empty?
|
|
77
92
|
|
|
78
|
-
unless @set_path ==
|
|
79
|
-
listed = @list.empty? ? [] : Media::DiskList.disks(@list)
|
|
80
|
-
@set = listed.include?(
|
|
81
|
-
@set_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(
|
|
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
|
-
|
|
111
|
-
name =
|
|
112
|
-
|
|
113
|
-
name = File.basename(
|
|
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?
|
|
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
|