badline 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +3 -1
- data/README.md +104 -292
- data/doc/library.md +69 -0
- data/doc/media.md +78 -0
- data/doc/pinned-behaviour.md +53 -9
- data/doc/sid-player.md +136 -0
- data/doc/snapshots.md +102 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +4 -4
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/computer/attachments.rb +5 -5
- data/lib/badline/computer.rb +12 -10
- data/lib/badline/core.rb +48 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +24 -9
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/frontend/app.rb +41 -37
- data/lib/badline/frontend/boot.rb +16 -4
- data/lib/badline/frontend/buttons.rb +101 -7
- data/lib/badline/frontend/confirmation.rb +56 -0
- data/lib/badline/frontend/controls.rb +44 -32
- data/lib/badline/frontend/file_browser.rb +225 -0
- data/lib/badline/frontend/frame_report.rb +52 -0
- data/lib/badline/frontend/menu_dialogs.rb +160 -0
- data/lib/badline/frontend/menu_media.rb +157 -0
- data/lib/badline/frontend/menu_pages.rb +220 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/painter.rb +10 -0
- data/lib/badline/frontend/pause_menu.rb +264 -0
- data/lib/badline/frontend/player_window.rb +3 -3
- data/lib/badline/frontend/snapshots.rb +146 -18
- data/lib/badline/frontend/sound.rb +11 -6
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend.rb +8 -0
- data/lib/badline/kernal_trap/load.rb +10 -0
- data/lib/badline/kernal_trap/routine.rb +28 -0
- data/lib/badline/kernal_trap/serial.rb +19 -7
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +118 -0
- data/lib/badline/media.rb +9 -3
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +16 -12
- data/lib/badline/options/table.rb +6 -2
- data/lib/badline/options/validation.rb +9 -0
- data/lib/badline/options.rb +10 -7
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +14 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- data/lib/badline/storage/t64.rb +7 -4
- data/lib/badline/storage/tap.rb +7 -5
- data/lib/badline/storage/unavailable.rb +1 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline.rb +2 -38
- metadata +22 -1
|
@@ -35,10 +35,15 @@ module Badline
|
|
|
35
35
|
|
|
36
36
|
# Where #stream starts: the frames before it are rendered as fast as
|
|
37
37
|
# they go and yielded without their samples, so the seconds rendered
|
|
38
|
-
# still count up to it.
|
|
39
|
-
#
|
|
38
|
+
# still count up to it. Rendering picks up from the latest of the
|
|
39
|
+
# checkpoints at or before it, without the frames before that. Set
|
|
40
|
+
# while streaming, it skips ahead from the frame rendered last.
|
|
40
41
|
attr_writer :from
|
|
41
42
|
|
|
43
|
+
# The Checkpoints this subtune's renderers keep as they go and start
|
|
44
|
+
# from, for a player that seeks. Without them nothing is kept.
|
|
45
|
+
attr_writer :checkpoints
|
|
46
|
+
|
|
42
47
|
def initialize(tune, seconds:, subtune: nil, rate: DEFAULT_RATE, sid_models: tune.sid_models)
|
|
43
48
|
@tune = tune
|
|
44
49
|
@seconds = seconds
|
|
@@ -52,6 +57,8 @@ module Badline
|
|
|
52
57
|
@rendered = 0.0
|
|
53
58
|
@observer = ->(_samples, _rendered) {}
|
|
54
59
|
@from = 0.0
|
|
60
|
+
@played = 0
|
|
61
|
+
@checkpoints = nil
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
def silence=(seconds)
|
|
@@ -65,7 +72,6 @@ module Badline
|
|
|
65
72
|
# Yields the seconds rendered so far after every frame.
|
|
66
73
|
def render(path)
|
|
67
74
|
container = container_for(path)
|
|
68
|
-
player.start
|
|
69
75
|
container.open(path, rate: @rate, channels: 2) do |writer|
|
|
70
76
|
each_frame do |samples, seconds|
|
|
71
77
|
samples.each { |sample| writer << sample }
|
|
@@ -76,9 +82,16 @@ module Badline
|
|
|
76
82
|
|
|
77
83
|
# Starts the tune, then yields each frame's samples along with the
|
|
78
84
|
# seconds rendered so far.
|
|
79
|
-
def stream(&)
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
def stream(&) = each_frame(&)
|
|
86
|
+
|
|
87
|
+
# Whether the latest checkpoint at or before `seconds` lies past the
|
|
88
|
+
# frame rendered last, so that a seek there gets there sooner from it
|
|
89
|
+
# than on from here.
|
|
90
|
+
def checkpoint_ahead?(seconds)
|
|
91
|
+
checkpoints = @checkpoints
|
|
92
|
+
return false if checkpoints.nil?
|
|
93
|
+
|
|
94
|
+
checkpoints.reach((seconds * player.clock_hz).round) > @played
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
private
|
|
@@ -101,19 +114,77 @@ module Badline
|
|
|
101
114
|
end
|
|
102
115
|
|
|
103
116
|
def each_frame
|
|
104
|
-
|
|
117
|
+
clock_hz = player.clock_hz
|
|
105
118
|
total = total_cycles
|
|
106
|
-
|
|
119
|
+
@played = start((@from * clock_hz).round)
|
|
120
|
+
remaining = total - @played
|
|
107
121
|
while remaining.positive?
|
|
108
122
|
samples = []
|
|
109
123
|
remaining -= player.frame(remaining) { |sample| samples << sample }
|
|
110
|
-
|
|
124
|
+
played = total - remaining
|
|
125
|
+
@played = played
|
|
126
|
+
@rendered = played.fdiv(clock_hz)
|
|
111
127
|
@observer.call(samples, @rendered)
|
|
112
128
|
yield(@rendered < @from ? [] : samples, @rendered)
|
|
113
129
|
break if fallen_silent?(samples)
|
|
130
|
+
|
|
131
|
+
save_checkpoint(played, clock_hz)
|
|
114
132
|
end
|
|
115
133
|
end
|
|
116
134
|
|
|
135
|
+
# Starts the player and its recording, from the latest checkpoint at
|
|
136
|
+
# or before `cycles` if there is one, and returns the cycles into the
|
|
137
|
+
# subtune it starts at.
|
|
138
|
+
def start(cycles)
|
|
139
|
+
state = @checkpoints&.latest(cycles)
|
|
140
|
+
return start_afresh if state.nil?
|
|
141
|
+
|
|
142
|
+
input = Snapshot::StateReader.new(state)
|
|
143
|
+
played = input.int
|
|
144
|
+
resume(input)
|
|
145
|
+
record
|
|
146
|
+
player.stereo.sids.each { |sid| sid.load_recording(input) }
|
|
147
|
+
@level = input.int
|
|
148
|
+
@still = input.int
|
|
149
|
+
played
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def start_afresh
|
|
153
|
+
player.start
|
|
154
|
+
record
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def record = player.stereo.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
|
|
159
|
+
|
|
160
|
+
# Restores the player as it was, then puts back the SID models chosen
|
|
161
|
+
# for this renderer.
|
|
162
|
+
def resume(input)
|
|
163
|
+
sids = player.stereo.sids
|
|
164
|
+
chosen = sids.map(&:model)
|
|
165
|
+
sids.each { |sid| refit(sid, Snapshot::Setup::SID_MODELS.fetch(input.int)) }
|
|
166
|
+
player.load_state(input)
|
|
167
|
+
sids.each_with_index { |sid, index| refit(sid, chosen[index]) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def refit(sid, model)
|
|
171
|
+
sid.model = model unless sid.model == model
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def save_checkpoint(played, clock_hz)
|
|
175
|
+
checkpoints = @checkpoints
|
|
176
|
+
return if checkpoints.nil? || !checkpoints.due?(played, clock_hz)
|
|
177
|
+
|
|
178
|
+
sids = player.stereo.sids
|
|
179
|
+
out = Snapshot::StateWriter.new
|
|
180
|
+
out.int(played)
|
|
181
|
+
sids.each { |sid| out.int(Snapshot::Setup::SID_MODELS.index(sid.model) || 0) }
|
|
182
|
+
player.save_state(out)
|
|
183
|
+
sids.each { |sid| sid.save_recording(out) }
|
|
184
|
+
out.int(@level).int(@still)
|
|
185
|
+
checkpoints.keep(played, out.state)
|
|
186
|
+
end
|
|
187
|
+
|
|
117
188
|
# Counts the samples since the output last strayed more than QUIET
|
|
118
189
|
# from where it settled.
|
|
119
190
|
def fallen_silent?(samples)
|
data/lib/badline/audio/stereo.rb
CHANGED
data/lib/badline/audio.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "badline/audio/aiff"
|
|
|
6
6
|
require "badline/audio/stereo"
|
|
7
7
|
require "badline/audio/bare_player"
|
|
8
8
|
require "badline/audio/machine_player"
|
|
9
|
+
require "badline/audio/checkpoints"
|
|
9
10
|
require "badline/audio/renderer"
|
|
10
11
|
require "badline/audio/playback"
|
|
11
12
|
require "badline/audio/terminal"
|
data/lib/badline/cartridge.rb
CHANGED
|
@@ -50,7 +50,7 @@ module Badline
|
|
|
50
50
|
def detach_drive1541
|
|
51
51
|
return unless @drive1541
|
|
52
52
|
|
|
53
|
-
@iec_bus
|
|
53
|
+
@iec_bus.detach(@drive1541)
|
|
54
54
|
@drive1541 = nil
|
|
55
55
|
@serial_trap&.device = serial_trap_device
|
|
56
56
|
end
|
|
@@ -95,10 +95,10 @@ module Badline
|
|
|
95
95
|
@save_trap.load_state(input)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
98
|
+
# A true 1541 on the serial bus. The leading flag is the bus itself,
|
|
99
|
+
# which every machine now has, kept so older snapshots still load.
|
|
100
100
|
def save_serial_bus(out)
|
|
101
|
-
out.boolean(
|
|
101
|
+
out.boolean(true).boolean(!@drive1541.nil?)
|
|
102
102
|
return unless @drive1541
|
|
103
103
|
|
|
104
104
|
out.int(@drive1541.device)
|
|
@@ -106,7 +106,7 @@ module Badline
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def load_serial_bus(input)
|
|
109
|
-
|
|
109
|
+
input.boolean?
|
|
110
110
|
return detach_drive1541 unless input.boolean?
|
|
111
111
|
|
|
112
112
|
device = input.int
|
data/lib/badline/computer.rb
CHANGED
|
@@ -42,9 +42,13 @@ module Badline
|
|
|
42
42
|
# TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
|
|
43
43
|
# every region boots the same ROMs. reu plugs an REU of that many K
|
|
44
44
|
# into the expansion port, where it drives the IRQ line and takes the
|
|
45
|
-
# bus for its transfers.
|
|
46
|
-
|
|
45
|
+
# bus for its transfers. kernal: names the KERNAL ROM
|
|
46
|
+
# (AddressBus::ROMs::KERNALS), and datasette: false leaves the cassette port
|
|
47
|
+
# empty, as on the SX-64.
|
|
48
|
+
def initialize(debug: false, reu: nil, kernal: :c64, datasette: true, **machine)
|
|
47
49
|
@address_bus = AddressBus.new(**machine)
|
|
50
|
+
@address_bus.kernal = kernal unless kernal == :c64
|
|
51
|
+
@address_bus.datasette.disconnect! unless datasette
|
|
48
52
|
@cpu = CPU.new(@address_bus, debug:)
|
|
49
53
|
@vic = @address_bus.vic
|
|
50
54
|
@vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
|
|
@@ -66,7 +70,7 @@ module Badline
|
|
|
66
70
|
@serial_trap = nil
|
|
67
71
|
@save_trap = nil
|
|
68
72
|
@drive1541 = nil
|
|
69
|
-
@iec_bus =
|
|
73
|
+
@iec_bus = IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
70
74
|
@reu = reu ? plug_reu(reu) : nil
|
|
71
75
|
end
|
|
72
76
|
|
|
@@ -161,19 +165,17 @@ module Badline
|
|
|
161
165
|
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
162
166
|
# still serve a mounted image.
|
|
163
167
|
def attach_drive1541(drive)
|
|
164
|
-
@iec_bus.detach(@drive1541) if @
|
|
168
|
+
@iec_bus.detach(@drive1541) if @drive1541
|
|
165
169
|
drive.host_clock_hz = region.clock_hz
|
|
166
170
|
@drive1541 = drive
|
|
167
171
|
drive.connect(iec_bus)
|
|
168
172
|
@serial_trap&.device = serial_trap_device
|
|
169
173
|
end
|
|
170
174
|
|
|
171
|
-
# The serial bus
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
@iec_bus ||= IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
176
|
-
end
|
|
175
|
+
# The serial bus, which CIA 2's port A drives through the C64's own
|
|
176
|
+
# inverters and reads back on PA6 and PA7, with or without a drive on
|
|
177
|
+
# it.
|
|
178
|
+
attr_reader :iec_bus
|
|
177
179
|
|
|
178
180
|
def capture_output
|
|
179
181
|
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
|
data/lib/badline/core.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The emulator core: the machine, its chips, media and traps, and the
|
|
4
|
+
# snapshot state Computer saves and restores with, without the options,
|
|
5
|
+
# the rest of the snapshot code, audio or front end. lib/badline.rb and
|
|
6
|
+
# the Spinel harnesses in spinel/ load it, so a file the core needs is
|
|
7
|
+
# added here.
|
|
8
|
+
require "badline/version"
|
|
9
|
+
require "badline/integer_helper"
|
|
10
|
+
require "badline/region"
|
|
11
|
+
require "badline/model"
|
|
12
|
+
require "badline/addressable"
|
|
13
|
+
require "badline/memory"
|
|
14
|
+
require "badline/color_memory"
|
|
15
|
+
require "badline/rom"
|
|
16
|
+
require "badline/ram_expansion"
|
|
17
|
+
require "badline/address_bus"
|
|
18
|
+
require "badline/instruction"
|
|
19
|
+
require "badline/instruction_set"
|
|
20
|
+
require "badline/status"
|
|
21
|
+
require "badline/keyboard"
|
|
22
|
+
require "badline/joystick"
|
|
23
|
+
require "badline/control_ports"
|
|
24
|
+
require "badline/input"
|
|
25
|
+
require "badline/cycleable"
|
|
26
|
+
require "badline/datasette"
|
|
27
|
+
require "badline/time_of_day"
|
|
28
|
+
require "badline/cia"
|
|
29
|
+
require "badline/via"
|
|
30
|
+
require "badline/sid"
|
|
31
|
+
require "badline/debug_register"
|
|
32
|
+
require "badline/reu"
|
|
33
|
+
require "badline/interrupts"
|
|
34
|
+
require "badline/traps"
|
|
35
|
+
require "badline/cpu"
|
|
36
|
+
require "badline/vic"
|
|
37
|
+
require "badline/keyboard_buffer"
|
|
38
|
+
require "badline/iec_bus"
|
|
39
|
+
require "badline/drive1541"
|
|
40
|
+
require "badline/snapshot/state"
|
|
41
|
+
require "badline/snapshot/setup"
|
|
42
|
+
require "badline/computer"
|
|
43
|
+
require "badline/checkpoint"
|
|
44
|
+
require "badline/storage"
|
|
45
|
+
require "badline/cartridge"
|
|
46
|
+
require "badline/kernal_trap"
|
|
47
|
+
require "badline/chrout_trap"
|
|
48
|
+
require "badline/media"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :data_path
|
|
8
|
+
|
|
9
|
+
# The per-user folder badline keeps its own files in. Defaults to
|
|
10
|
+
# $BADLINE_DATA_PATH, then to the platform's folder for application
|
|
11
|
+
# data. Assigning nil restores the default. Nothing is created until
|
|
12
|
+
# data_folder is asked for a subfolder.
|
|
13
|
+
def data_path
|
|
14
|
+
@data_path ||= ENV.fetch("BADLINE_DATA_PATH", nil) || default_data_path(RUBY_PLATFORM)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ~/Library/Application Support/badline on macOS, and
|
|
18
|
+
# $XDG_DATA_HOME/badline or ~/.local/share/badline elsewhere.
|
|
19
|
+
def default_data_path(platform)
|
|
20
|
+
return File.join(Dir.home, "Library", "Application Support", "badline") if platform.include?("darwin")
|
|
21
|
+
|
|
22
|
+
xdg = ENV.fetch("XDG_DATA_HOME", nil)
|
|
23
|
+
xdg = File.join(Dir.home, ".local", "share") if xdg.nil? || !xdg.start_with?("/")
|
|
24
|
+
File.join(xdg, "badline")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The quicksaves, autosaves or saves subfolder of data_path, created
|
|
28
|
+
# along with data_path if it doesn't exist yet.
|
|
29
|
+
def data_folder(name)
|
|
30
|
+
raise ArgumentError, "unknown data folder: #{name}" unless %w[quicksaves autosaves saves].include?(name)
|
|
31
|
+
|
|
32
|
+
path = File.join(data_path, name)
|
|
33
|
+
FileUtils.mkdir_p(path)
|
|
34
|
+
path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/badline/datasette.rb
CHANGED
|
@@ -5,11 +5,15 @@ module Badline
|
|
|
5
5
|
# motor runs with a key pressed, and reports the keys through the cassette
|
|
6
6
|
# sense line on the $01 port.
|
|
7
7
|
class Datasette
|
|
8
|
+
# What inserting a tape raises on a machine without a cassette port.
|
|
9
|
+
class Missing < ArgumentError; end
|
|
10
|
+
|
|
8
11
|
attr_reader :tape
|
|
9
12
|
attr_writer :motor
|
|
10
13
|
|
|
11
14
|
def initialize(tape = nil)
|
|
12
15
|
@tape = tape
|
|
16
|
+
@connected = true
|
|
13
17
|
@playing = false
|
|
14
18
|
@motor = false
|
|
15
19
|
@countdown = 0
|
|
@@ -27,7 +31,18 @@ module Badline
|
|
|
27
31
|
@sense_handler = handler
|
|
28
32
|
end
|
|
29
33
|
|
|
34
|
+
# Whether the machine has a datasette at its cassette port. The SX-64
|
|
35
|
+
# has none: its keys stay up and it takes no tape.
|
|
36
|
+
def connected? = @connected
|
|
37
|
+
|
|
38
|
+
def disconnect!
|
|
39
|
+
eject
|
|
40
|
+
@connected = false
|
|
41
|
+
end
|
|
42
|
+
|
|
30
43
|
def insert(tape)
|
|
44
|
+
raise Missing, "this machine has no datasette" unless @connected
|
|
45
|
+
|
|
31
46
|
@tape = tape
|
|
32
47
|
rewind
|
|
33
48
|
end
|
|
@@ -54,18 +69,19 @@ module Badline
|
|
|
54
69
|
|
|
55
70
|
def running? = @playing && @motor && !@tape.nil?
|
|
56
71
|
|
|
57
|
-
# The keys, the motor, the countdown to the next pulse and the tape:
|
|
58
|
-
# path and how far it has played.
|
|
72
|
+
# The keys, the motor, the countdown to the next pulse and the tape:
|
|
73
|
+
# its path, its bytes and how far it has played.
|
|
59
74
|
def save_state(out)
|
|
60
75
|
out.marker("DATASETTE")
|
|
61
76
|
out.boolean(@playing).boolean(@motor).int(@countdown).boolean(!@tape.nil?)
|
|
62
77
|
return unless @tape
|
|
63
78
|
|
|
64
|
-
out.string(@tape.path).int(@tape.position)
|
|
79
|
+
out.string(@tape.path).blob(@tape.bytes).int(@tape.position)
|
|
65
80
|
end
|
|
66
81
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
82
|
+
# The tape goes back in from the bytes the state holds, without its
|
|
83
|
+
# host file, unless the tape in is the same one. The sense and flag
|
|
84
|
+
# handlers don't fire.
|
|
69
85
|
def load_state(input)
|
|
70
86
|
input.marker("DATASETTE")
|
|
71
87
|
@playing = input.boolean?
|
|
@@ -74,10 +90,9 @@ module Badline
|
|
|
74
90
|
return @tape = nil unless input.boolean?
|
|
75
91
|
|
|
76
92
|
path = input.string
|
|
93
|
+
bytes = input.blob
|
|
77
94
|
position = input.int
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@tape = Storage::TAP.new(path) unless @tape&.path&.b == path
|
|
95
|
+
@tape = Storage::TAP.new(path, bytes:) unless @tape&.path&.b == path && @tape.bytes == bytes
|
|
81
96
|
@tape.position = position
|
|
82
97
|
end
|
|
83
98
|
|
|
@@ -99,7 +114,7 @@ module Badline
|
|
|
99
114
|
private
|
|
100
115
|
|
|
101
116
|
def press(down)
|
|
102
|
-
return if down == @playing
|
|
117
|
+
return if down == @playing || !@connected
|
|
103
118
|
|
|
104
119
|
@playing = down
|
|
105
120
|
@sense_handler&.call
|
|
@@ -160,9 +160,9 @@ module Badline
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Whether the write-protect notch is covered: the disk's image won't
|
|
163
|
-
# take writes
|
|
164
|
-
# only in its tracks.
|
|
165
|
-
def write_protected? =
|
|
163
|
+
# take writes, or a disk without an image was opened read-only. A
|
|
164
|
+
# disk without an image keeps what it takes only in its tracks.
|
|
165
|
+
def write_protected? = @image.nil? ? @read_only : !@image.writable?
|
|
166
166
|
|
|
167
167
|
# A half track the head writes to, blank to begin with: no flux, and
|
|
168
168
|
# as many bytes as a turn holds at the bit rate of the +zone+ it's
|
|
@@ -5,7 +5,8 @@ module Badline
|
|
|
5
5
|
class Disk
|
|
6
6
|
# Saving and restoring a Disk for a snapshot: the image by its path,
|
|
7
7
|
# and every half track as the head sees it, with those written since
|
|
8
|
-
# the last flush. The image itself is read again from its host file
|
|
8
|
+
# the last flush. The image itself is read again from its host file,
|
|
9
|
+
# where there still is one.
|
|
9
10
|
module State
|
|
10
11
|
# The image's path, expanded, and whether it was opened read-only,
|
|
11
12
|
# for a disk from Disk.open. Nil for a disk made another way.
|
|
@@ -25,18 +26,26 @@ module Badline
|
|
|
25
26
|
|
|
26
27
|
# The disk a state from save_state describes: +current+ when it was
|
|
27
28
|
# opened from the same image the same way, and otherwise a disk for
|
|
28
|
-
# the image at the path the state names
|
|
29
|
-
#
|
|
30
|
-
#
|
|
29
|
+
# the image at the path the state names. Where no file is at that
|
|
30
|
+
# path, the disk has no image and is write-protected, and a
|
|
31
|
+
# detached reader always gets one without an image. Either way its
|
|
32
|
+
# tracks come from the state.
|
|
31
33
|
def self.load(input, current)
|
|
32
34
|
path = input.optional_string
|
|
33
35
|
read_only = input.boolean?
|
|
34
36
|
disk = current if current && !path.nil? && current.path == path && current.read_only == read_only
|
|
35
|
-
disk ||= path
|
|
37
|
+
disk ||= fresh(path, read_only, input.detached?)
|
|
36
38
|
disk.load_tracks(input)
|
|
37
39
|
disk
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def self.fresh(path, read_only, detached)
|
|
43
|
+
return Disk.new if path.nil? || detached
|
|
44
|
+
return Disk.new.opened(path, true) unless File.file?(path)
|
|
45
|
+
|
|
46
|
+
reopen(path, read_only)
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
# Notes the image's path and how it was opened.
|
|
41
50
|
def opened(path, read_only)
|
|
42
51
|
@path = File.expand_path(path)
|
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -7,8 +7,8 @@ module Badline
|
|
|
7
7
|
# changed lines, present and wait. Pacer decides the cycles and the wait.
|
|
8
8
|
class App
|
|
9
9
|
SCALE = 2
|
|
10
|
+
DROPFILE = 0x1000
|
|
10
11
|
TITLE = "Badline"
|
|
11
|
-
STAGES = %w[events emulate audio blit present wait].freeze
|
|
12
12
|
|
|
13
13
|
# Takes the frame limit, the pacing, the snapshot path, the sound and
|
|
14
14
|
# the verbosity from Options, and runs the timeline's events.
|
|
@@ -22,11 +22,11 @@ module Badline
|
|
|
22
22
|
@screen = Screen.new(computer.vic)
|
|
23
23
|
@led = DriveLed.for(computer)
|
|
24
24
|
@controls = Controls.new(computer)
|
|
25
|
-
@spent = Array.new(STAGES.size, 0.0)
|
|
26
|
-
@slowest = 0.0
|
|
27
25
|
open_window
|
|
28
|
-
@sound = Sound.new(computer.sid, options.sound?, @verbose)
|
|
26
|
+
@sound = Sound.new(computer.sid, computer.region.clock_hz, options.sound?, @verbose)
|
|
29
27
|
@gamepads = Gamepads.new(computer, @verbose)
|
|
28
|
+
@frame_report = FrameReport.new(@sound)
|
|
29
|
+
@menu = PauseMenu.new(Painter.new(@renderer), options, @snapshots)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def run
|
|
@@ -34,8 +34,7 @@ module Badline
|
|
|
34
34
|
@running = true
|
|
35
35
|
@started = @reported = now
|
|
36
36
|
@pacer.start(@started)
|
|
37
|
-
@
|
|
38
|
-
frame while @running
|
|
37
|
+
@menu.open? ? paused_frame : frame while @running
|
|
39
38
|
@snapshots.finish
|
|
40
39
|
@computer.drive1541&.flush
|
|
41
40
|
@gamepads.close
|
|
@@ -64,11 +63,10 @@ module Badline
|
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
def finish_frame(stamps)
|
|
67
|
-
|
|
68
|
-
took = stamps[-2] - stamps.first
|
|
69
|
-
@slowest = took if took > @slowest
|
|
66
|
+
@frame_report.add(stamps) if @verbose
|
|
70
67
|
@frames += 1
|
|
71
68
|
@timeline.run(@computer, @frames)
|
|
69
|
+
@snapshots.tick(@frames)
|
|
72
70
|
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
73
71
|
@pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
|
|
74
72
|
report(stamps.last) if (@frames % 50).zero?
|
|
@@ -95,8 +93,6 @@ module Badline
|
|
|
95
93
|
@texture = SDL.SDL_CreateTexture(
|
|
96
94
|
@renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, Screen::WIDTH, Screen::HEIGHT
|
|
97
95
|
)
|
|
98
|
-
SDL.rect_w(SDL.rect, Screen::WIDTH)
|
|
99
|
-
SDL.rect_h(SDL.rect, Screen::HEIGHT)
|
|
100
96
|
end
|
|
101
97
|
|
|
102
98
|
def close_window
|
|
@@ -107,7 +103,7 @@ module Badline
|
|
|
107
103
|
end
|
|
108
104
|
|
|
109
105
|
def handle_events
|
|
110
|
-
handle_event(SDL.event_type(SDL.event)) while SDL.SDL_PollEvent(SDL.event) != 0
|
|
106
|
+
handle_event(SDL.event_type(SDL.event)) while !@menu.open? && SDL.SDL_PollEvent(SDL.event) != 0
|
|
111
107
|
end
|
|
112
108
|
|
|
113
109
|
def handle_event(type)
|
|
@@ -122,6 +118,7 @@ module Badline
|
|
|
122
118
|
@controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
|
|
123
119
|
when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
|
|
124
120
|
@gamepads.rescan
|
|
121
|
+
when DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
|
|
125
122
|
end
|
|
126
123
|
end
|
|
127
124
|
|
|
@@ -144,21 +141,46 @@ module Badline
|
|
|
144
141
|
@led = DriveLed.for(computer)
|
|
145
142
|
@controls.computer = computer
|
|
146
143
|
@gamepads.computer = computer
|
|
147
|
-
@sound.sid
|
|
144
|
+
@sound.switch(computer.sid, computer.region.clock_hz)
|
|
145
|
+
@snapshots.computer = computer
|
|
148
146
|
end
|
|
149
147
|
|
|
150
148
|
def handle_toggle(scancode)
|
|
151
149
|
if scancode == Keys::TAB
|
|
152
|
-
@controls.
|
|
153
|
-
SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
|
|
150
|
+
@controls.toggle_keys
|
|
154
151
|
elsif scancode == Keys::F9
|
|
155
|
-
|
|
152
|
+
return open_menu
|
|
156
153
|
else
|
|
157
154
|
@sound.toggle_mute
|
|
158
155
|
end
|
|
159
156
|
update_title
|
|
160
157
|
end
|
|
161
158
|
|
|
159
|
+
def open_menu = @menu.show(@computer, @controls, @sound)
|
|
160
|
+
|
|
161
|
+
# Closes the menu and runs the machine on from where it stood,
|
|
162
|
+
# pressing the cartridge's freeze button if asked.
|
|
163
|
+
def resume(freeze)
|
|
164
|
+
@menu.close
|
|
165
|
+
SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
|
|
166
|
+
@timeline.press_freeze(@computer, @frames) if freeze
|
|
167
|
+
@reported = now
|
|
168
|
+
@pacer.start(@reported)
|
|
169
|
+
update_title
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Runs a frame of the menu, which has the keys and the mouse while the
|
|
173
|
+
# machine stands still.
|
|
174
|
+
def paused_frame
|
|
175
|
+
action = @menu.frame(@renderer, @texture, @controls)
|
|
176
|
+
if action == :quit
|
|
177
|
+
@running = false
|
|
178
|
+
elsif !action.nil?
|
|
179
|
+
swap(@menu.computer) if action == :swap
|
|
180
|
+
resume(action == :freeze)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
162
184
|
def update_title
|
|
163
185
|
title = TITLE
|
|
164
186
|
title += " [#{@controls.tag}]" unless @controls.tag.empty?
|
|
@@ -183,12 +205,12 @@ module Badline
|
|
|
183
205
|
|
|
184
206
|
def upload
|
|
185
207
|
@screen.update
|
|
186
|
-
SDL.SDL_UpdateTexture(@texture,
|
|
208
|
+
SDL.SDL_UpdateTexture(@texture, nil, @screen.pixels, Screen::ROW_BYTES)
|
|
187
209
|
end
|
|
188
210
|
|
|
189
211
|
def draw
|
|
190
212
|
SDL.SDL_RenderClear(@renderer)
|
|
191
|
-
SDL.SDL_RenderCopy(@renderer, @texture,
|
|
213
|
+
SDL.SDL_RenderCopy(@renderer, @texture, nil, nil)
|
|
192
214
|
@led&.draw(@renderer)
|
|
193
215
|
@timeline.screenshots(@frames + 1).each { |path| Screenshot.write(@renderer, path) }
|
|
194
216
|
SDL.SDL_RenderPresent(@renderer)
|
|
@@ -197,28 +219,10 @@ module Badline
|
|
|
197
219
|
def report(at)
|
|
198
220
|
@pacer.check(50, at - @reported, at)
|
|
199
221
|
@pacer.measure(50, at - @reported)
|
|
200
|
-
|
|
201
|
-
@spent = Array.new(STAGES.size, 0.0)
|
|
202
|
-
@slowest = 0.0
|
|
222
|
+
@frame_report.show(at - @reported) if @verbose
|
|
203
223
|
@reported = at
|
|
204
224
|
end
|
|
205
225
|
|
|
206
|
-
def report_frames(at)
|
|
207
|
-
fps = 50 / (at - @reported)
|
|
208
|
-
stages = STAGES.each_with_index.map { |name, stage| "#{name} #{(@spent[stage] * 20).round(2)}" }
|
|
209
|
-
puts "#{fps.round(1)} fps, per frame ms: #{stages.join(' ')}, slowest work #{(@slowest * 1000).round(2)}"
|
|
210
|
-
report_sound(at) if @sound.on?
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def report_sound(at)
|
|
214
|
-
sound = @sound
|
|
215
|
-
rate = (sound.queued - @reported_samples) / (at - @reported)
|
|
216
|
-
queue = sound.high.zero? ? "empty" : "#{(sound.low * 1000).round(1)}-#{(sound.high * 1000).round(1)} ms"
|
|
217
|
-
puts " sound #{rate.round} samples/s, queue #{queue}, #{sound.underruns} underruns, #{sound.dropped} dropped"
|
|
218
|
-
sound.reset_levels
|
|
219
|
-
@reported_samples = sound.queued
|
|
220
|
-
end
|
|
221
|
-
|
|
222
226
|
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
223
227
|
end
|
|
224
228
|
end
|