badline 0.9.0 → 0.10.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 +29 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +13 -0
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus.rb +9 -88
- 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 +44 -29
- 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 +17 -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/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +11 -1
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +27 -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/state.rb +4 -3
- 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 +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +34 -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 +28 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -22,11 +22,13 @@ module Badline
|
|
|
22
22
|
return @drive.insert(storage) if @drive
|
|
23
23
|
|
|
24
24
|
@drive = KernalTrap::Drive.new(storage)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
layout = KernalTrap::C64_LAYOUT
|
|
26
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
27
|
+
cpu.install_trap(layout.load) { load_trap.call }
|
|
28
|
+
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
|
|
29
|
+
device: serial_trap_device).install
|
|
30
|
+
save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
31
|
+
cpu.install_trap(layout.save) { save_trap.call }
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
@@ -35,8 +37,8 @@ module Badline
|
|
|
35
37
|
def unmount
|
|
36
38
|
return unless @drive
|
|
37
39
|
|
|
38
|
-
cpu.remove_trap(KernalTrap::
|
|
39
|
-
cpu.remove_trap(KernalTrap::
|
|
40
|
+
cpu.remove_trap(KernalTrap::C64_LAYOUT.load)
|
|
41
|
+
cpu.remove_trap(KernalTrap::C64_LAYOUT.save)
|
|
40
42
|
@serial_trap.device = nil
|
|
41
43
|
@serial_trap = nil
|
|
42
44
|
@save_trap = nil
|
|
@@ -46,6 +48,24 @@ module Badline
|
|
|
46
48
|
# Whether device 8 serves a disk or directory through the traps.
|
|
47
49
|
def mounted? = !@drive.nil?
|
|
48
50
|
|
|
51
|
+
# The serial bus, which CIA 2's port A drives through the C64's own
|
|
52
|
+
# inverters and reads back on PA6 and PA7, with or without a drive on
|
|
53
|
+
# it.
|
|
54
|
+
attr_reader :iec_bus
|
|
55
|
+
|
|
56
|
+
# Plugs in a Drive1541, which then runs alongside the C64 on its own
|
|
57
|
+
# clock and talks to it over the serial bus. The serial traps stop
|
|
58
|
+
# answering the drive's device number, so the KERNAL's TALK, LISTEN and
|
|
59
|
+
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
60
|
+
# still serve a mounted image.
|
|
61
|
+
def attach_drive1541(drive)
|
|
62
|
+
@iec_bus.detach(@drive1541) if @drive1541
|
|
63
|
+
drive.host_clock_hz = region.clock_hz
|
|
64
|
+
@drive1541 = drive
|
|
65
|
+
drive.connect(iec_bus)
|
|
66
|
+
@serial_trap&.device = serial_trap_device
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
# Unplugs the Drive1541, leaving the serial bus with nothing on it.
|
|
50
70
|
def detach_drive1541
|
|
51
71
|
return unless @drive1541
|
|
@@ -57,6 +77,26 @@ module Badline
|
|
|
57
77
|
|
|
58
78
|
private
|
|
59
79
|
|
|
80
|
+
# The device number the serial traps answer: device 8, unless a true
|
|
81
|
+
# drive is on the bus as device 8.
|
|
82
|
+
def serial_trap_device
|
|
83
|
+
@drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# CIA 2's port A drives the serial bus, which reads CLK and DATA back
|
|
87
|
+
# into it.
|
|
88
|
+
def plug_serial_bus
|
|
89
|
+
@iec_bus = IECBus.new
|
|
90
|
+
@cia2.peripheral = @iec_bus
|
|
91
|
+
@cia2.on_port_a_write { push_serial_lines }
|
|
92
|
+
push_serial_lines
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Pushes the lines CIA 2's port A pulls into the serial bus.
|
|
96
|
+
def push_serial_lines
|
|
97
|
+
@iec_bus.host_lines = @cia2.port_a_lines
|
|
98
|
+
end
|
|
99
|
+
|
|
60
100
|
def save_cartridge(out)
|
|
61
101
|
cartridge = address_bus.cartridge
|
|
62
102
|
out.boolean(!cartridge.nil?)
|
data/lib/badline/computer.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Badline
|
|
|
12
12
|
|
|
13
13
|
attr_reader :address_bus, :cpu, :cycles, :drive1541
|
|
14
14
|
|
|
15
|
+
# The path of the disk or directory device 8 serves through the traps
|
|
16
|
+
# (Attachments), or an empty one.
|
|
17
|
+
def mounted_path = @drive.nil? ? "" : @drive.path
|
|
18
|
+
|
|
19
|
+
def family = :c64
|
|
20
|
+
|
|
15
21
|
def region = address_bus.region
|
|
16
22
|
|
|
17
23
|
def vic = address_bus.vic
|
|
@@ -70,7 +76,7 @@ module Badline
|
|
|
70
76
|
@serial_trap = nil
|
|
71
77
|
@save_trap = nil
|
|
72
78
|
@drive1541 = nil
|
|
73
|
-
|
|
79
|
+
plug_serial_bus
|
|
74
80
|
@reu = reu ? plug_reu(reu) : nil
|
|
75
81
|
end
|
|
76
82
|
|
|
@@ -98,6 +104,40 @@ module Badline
|
|
|
98
104
|
@cycles += 1
|
|
99
105
|
end
|
|
100
106
|
|
|
107
|
+
# Runs `count` cycles, one #cycle! after another.
|
|
108
|
+
def run_cycles(count)
|
|
109
|
+
i = 0
|
|
110
|
+
while i < count
|
|
111
|
+
cycle!
|
|
112
|
+
i += 1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Runs cycles until the block returns true or the cycle count passes
|
|
117
|
+
# `limit`, checking before each cycle.
|
|
118
|
+
def run_until(limit)
|
|
119
|
+
cycle! until yield || @cycles > limit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The cycle at which #on_init's handlers run, once the KERNAL has booted.
|
|
123
|
+
def init_threshold
|
|
124
|
+
INIT_THRESHOLD
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The clock, the raster and the crop of the machine's region, a pixel
|
|
128
|
+
# to a window pixel.
|
|
129
|
+
def timing
|
|
130
|
+
region = address_bus.region
|
|
131
|
+
Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
|
|
132
|
+
lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The chip whose #display a front end shows.
|
|
136
|
+
def video = @vic
|
|
137
|
+
|
|
138
|
+
# The chip a front end records the machine's sound from.
|
|
139
|
+
def sound_source = @sid
|
|
140
|
+
|
|
101
141
|
def load_prg(data)
|
|
102
142
|
uint16(data[0], data[1]).tap do |load_addr|
|
|
103
143
|
ram.write(load_addr, data[2..])
|
|
@@ -159,26 +199,11 @@ module Badline
|
|
|
159
199
|
@restore_pulse = true
|
|
160
200
|
end
|
|
161
201
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
# answering the drive's device number, so the KERNAL's TALK, LISTEN and
|
|
165
|
-
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
166
|
-
# still serve a mounted image.
|
|
167
|
-
def attach_drive1541(drive)
|
|
168
|
-
@iec_bus.detach(@drive1541) if @drive1541
|
|
169
|
-
drive.host_clock_hz = region.clock_hz
|
|
170
|
-
@drive1541 = drive
|
|
171
|
-
drive.connect(iec_bus)
|
|
172
|
-
@serial_trap&.device = serial_trap_device
|
|
173
|
-
end
|
|
174
|
-
|
|
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
|
|
202
|
+
# The one-shot has pulsed already, so letting go does nothing.
|
|
203
|
+
def release_restore; end
|
|
179
204
|
|
|
180
205
|
def capture_output
|
|
181
|
-
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
|
|
206
|
+
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus, layout: KernalTrap::C64_LAYOUT).tap do |trap|
|
|
182
207
|
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
183
208
|
end
|
|
184
209
|
end
|
|
@@ -197,12 +222,6 @@ module Badline
|
|
|
197
222
|
|
|
198
223
|
private
|
|
199
224
|
|
|
200
|
-
# The device number the serial traps answer: device 8, unless a true
|
|
201
|
-
# drive is on the bus as device 8.
|
|
202
|
-
def serial_trap_device
|
|
203
|
-
@drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
|
|
204
|
-
end
|
|
205
|
-
|
|
206
225
|
# The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
|
|
207
226
|
# key, and the CPU takes an interrupt on its falling edge.
|
|
208
227
|
def drive_nmi
|
|
@@ -258,9 +277,5 @@ module Badline
|
|
|
258
277
|
def handle_init
|
|
259
278
|
@init_handlers.each(&:call)
|
|
260
279
|
end
|
|
261
|
-
|
|
262
|
-
def init_threshold
|
|
263
|
-
INIT_THRESHOLD
|
|
264
|
-
end
|
|
265
280
|
end
|
|
266
281
|
end
|
|
@@ -16,13 +16,19 @@ module Badline
|
|
|
16
16
|
# resistances; close neither and the lines float high. Reading a pot asks the
|
|
17
17
|
# CIA for the current select lines, since a program sets them and then goes
|
|
18
18
|
# straight to SID without touching the ports again.
|
|
19
|
+
#
|
|
20
|
+
# A matrix may have rows beyond port A's eight. Their select lines come from
|
|
21
|
+
# outside the CIA and are pushed into +extra_rows+, bit 0 for the ninth row
|
|
22
|
+
# upwards, active low like port A; idle, every line is high and none of
|
|
23
|
+
# those rows is selected. They settle with port A's lines as rows 8 and up,
|
|
24
|
+
# so their keys reach port B, but port A reads only its own eight.
|
|
19
25
|
class ControlPorts
|
|
20
26
|
PORT1_POTS = 0x40
|
|
21
27
|
PORT2_POTS = 0x80
|
|
22
28
|
FIRE = 0x10
|
|
23
29
|
|
|
24
30
|
attr_reader :keyboard, :joystick1, :joystick2
|
|
25
|
-
attr_accessor :device1, :device2, :port_a_source
|
|
31
|
+
attr_accessor :device1, :device2, :port_a_source, :extra_rows
|
|
26
32
|
|
|
27
33
|
def initialize(keyboard:, joystick1:, joystick2:)
|
|
28
34
|
@keyboard = keyboard
|
|
@@ -31,9 +37,10 @@ module Badline
|
|
|
31
37
|
@device1 = nil
|
|
32
38
|
@device2 = nil
|
|
33
39
|
@port_a_source = nil
|
|
40
|
+
@extra_rows = 0xff
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
def read_a(port_a, port_b) = scan(port_a, port_b).first
|
|
43
|
+
def read_a(port_a, port_b) = scan(port_a, port_b).first & 0xff
|
|
37
44
|
|
|
38
45
|
def read_b(port_a, port_b) = scan(port_a, port_b).last
|
|
39
46
|
|
|
@@ -58,7 +65,7 @@ module Badline
|
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
def scan(port_a, port_b)
|
|
61
|
-
keyboard.scan(port_a & port_bits(@joystick2, @device2),
|
|
68
|
+
keyboard.scan((port_a & port_bits(@joystick2, @device2)) | (@extra_rows << 8),
|
|
62
69
|
port_b & port_bits(@joystick1, @device1))
|
|
63
70
|
end
|
|
64
71
|
|
data/lib/badline/core.rb
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
require "badline/version"
|
|
9
9
|
require "badline/integer_helper"
|
|
10
10
|
require "badline/region"
|
|
11
|
+
require "badline/timing"
|
|
11
12
|
require "badline/model"
|
|
12
13
|
require "badline/addressable"
|
|
13
14
|
require "badline/memory"
|
|
@@ -40,9 +41,11 @@ require "badline/drive1541"
|
|
|
40
41
|
require "badline/snapshot/state"
|
|
41
42
|
require "badline/snapshot/setup"
|
|
42
43
|
require "badline/computer"
|
|
44
|
+
require "badline/vic20"
|
|
43
45
|
require "badline/checkpoint"
|
|
44
46
|
require "badline/storage"
|
|
45
47
|
require "badline/cartridge"
|
|
46
48
|
require "badline/kernal_trap"
|
|
47
49
|
require "badline/chrout_trap"
|
|
48
50
|
require "badline/media"
|
|
51
|
+
require "badline/machine"
|
data/lib/badline/datasette.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
# The 1530 datasette. Plays a pulse stream into CIA 1's FLAG pin while the
|
|
5
5
|
# motor runs with a key pressed, and reports the keys through the cassette
|
|
6
|
-
# sense line on the $01 port.
|
|
6
|
+
# sense line on the $01 port. With RECORD down too, it records the
|
|
7
|
+
# machine's write line onto the tape instead.
|
|
7
8
|
class Datasette
|
|
8
9
|
# What inserting a tape raises on a machine without a cassette port.
|
|
9
10
|
class Missing < ArgumentError; end
|
|
@@ -15,6 +16,9 @@ module Badline
|
|
|
15
16
|
@tape = tape
|
|
16
17
|
@connected = true
|
|
17
18
|
@playing = false
|
|
19
|
+
@recording = false
|
|
20
|
+
@write_high = true
|
|
21
|
+
@since = 0
|
|
18
22
|
@motor = false
|
|
19
23
|
@countdown = 0
|
|
20
24
|
@flag_handler = nil
|
|
@@ -60,6 +64,30 @@ module Badline
|
|
|
60
64
|
def play! = press(true)
|
|
61
65
|
def stop! = press(false)
|
|
62
66
|
|
|
67
|
+
# Presses RECORD and PLAY together.
|
|
68
|
+
def record!
|
|
69
|
+
return if !@connected || @tape.nil?
|
|
70
|
+
|
|
71
|
+
press(true)
|
|
72
|
+
@recording = true
|
|
73
|
+
@since = 0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def recording? = @recording
|
|
77
|
+
|
|
78
|
+
# The machine's tape write line. While the deck records with its motor
|
|
79
|
+
# running, each rising edge ends a pulse on the tape, as long as the
|
|
80
|
+
# time since the edge before, and plays back as a falling edge on the
|
|
81
|
+
# read line.
|
|
82
|
+
def write_line=(high)
|
|
83
|
+
rose = high && !@write_high
|
|
84
|
+
@write_high = high
|
|
85
|
+
return unless rose && @recording && running?
|
|
86
|
+
|
|
87
|
+
@tape.record_pulse(@since)
|
|
88
|
+
@since = 0
|
|
89
|
+
end
|
|
90
|
+
|
|
63
91
|
def playing? = @playing
|
|
64
92
|
|
|
65
93
|
# Sense reads low while a key is down on the deck.
|
|
@@ -98,6 +126,7 @@ module Badline
|
|
|
98
126
|
|
|
99
127
|
def cycle!
|
|
100
128
|
return unless running?
|
|
129
|
+
return @since += 1 if @recording
|
|
101
130
|
|
|
102
131
|
@countdown = @tape.next_pulse.to_i if @countdown.zero?
|
|
103
132
|
return if @countdown.zero?
|
|
@@ -116,8 +145,17 @@ module Badline
|
|
|
116
145
|
def press(down)
|
|
117
146
|
return if down == @playing || !@connected
|
|
118
147
|
|
|
148
|
+
finish_recording unless down
|
|
119
149
|
@playing = down
|
|
120
150
|
@sense_handler&.call
|
|
121
151
|
end
|
|
152
|
+
|
|
153
|
+
# Releasing the keys ends a recording, and writes the tape out.
|
|
154
|
+
def finish_recording
|
|
155
|
+
return unless @recording
|
|
156
|
+
|
|
157
|
+
@recording = false
|
|
158
|
+
@tape.save
|
|
159
|
+
end
|
|
122
160
|
end
|
|
123
161
|
end
|
|
@@ -27,7 +27,7 @@ module Badline
|
|
|
27
27
|
|
|
28
28
|
attr_reader :bus
|
|
29
29
|
|
|
30
|
-
def initialize(device: 8, bus:
|
|
30
|
+
def initialize(device: 8, bus: IECBus.new)
|
|
31
31
|
@port_b = (0xff & ~(SERIAL_INPUTS | JUMPERS)) | (((device - 8) & 0x03) << 5)
|
|
32
32
|
@host = 0
|
|
33
33
|
self.bus = bus
|
|
@@ -38,10 +38,10 @@ module Badline
|
|
|
38
38
|
latch_host
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Takes the C64's lines as
|
|
42
|
-
# next host cycle.
|
|
41
|
+
# Takes the C64's lines as the machine last pushed them into the bus,
|
|
42
|
+
# for the drive cycles of the next host cycle.
|
|
43
43
|
def latch_host
|
|
44
|
-
@host = @bus
|
|
44
|
+
@host = @bus.host_lines
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# The C64's lines as last latched. The jumpers and the bus are the
|
|
@@ -55,10 +55,7 @@ module Badline
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Whether the C64 has moved ATN since the last latch_host.
|
|
58
|
-
def atn_moved?
|
|
59
|
-
lines = @bus ? @bus.host_lines : 0
|
|
60
|
-
(lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
|
|
61
|
-
end
|
|
58
|
+
def atn_moved? = (@bus.host_lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
|
|
62
59
|
|
|
63
60
|
def atn_low? = @bus.atn_low?(@host)
|
|
64
61
|
|
|
@@ -8,9 +8,8 @@ module Badline
|
|
|
8
8
|
# passes go in bulk, and the rest cycle by cycle, which may find the
|
|
9
9
|
# idle loop and put the drive back to sleep.
|
|
10
10
|
#
|
|
11
|
-
# The drive looks at ATN only when the
|
|
12
|
-
#
|
|
13
|
-
# that doesn't.
|
|
11
|
+
# The drive looks at ATN only when the machine may have moved it, as
|
|
12
|
+
# it pushes its lines into the bus (IECBus#host_lines=).
|
|
14
13
|
module Sleep
|
|
15
14
|
# Whether the drive is skipping its idle loop right now.
|
|
16
15
|
def asleep? = @asleep
|
|
@@ -40,10 +39,10 @@ module Badline
|
|
|
40
39
|
@wake_at = 0
|
|
41
40
|
end
|
|
42
41
|
|
|
43
|
-
# A host cycle asleep that may wake the drive. The
|
|
44
|
-
#
|
|
42
|
+
# A host cycle asleep that may wake the drive. The next ones need no
|
|
43
|
+
# look at ATN until the machine pushes its lines again.
|
|
45
44
|
def doze
|
|
46
|
-
@host_still =
|
|
45
|
+
@host_still = true
|
|
47
46
|
if @serial_port.atn_moved?
|
|
48
47
|
settle!
|
|
49
48
|
elsif @slept == @wake_at
|
data/lib/badline/drive1541.rb
CHANGED
|
@@ -149,8 +149,8 @@ module Badline
|
|
|
149
149
|
# The host has run this cycle already, and the drive sees what it did
|
|
150
150
|
# from the next one on (see SerialPort). Asleep, the drive owes the
|
|
151
151
|
# cycles instead, until ATN moves or its counters are due (see Idle).
|
|
152
|
-
# It looks at ATN only after the
|
|
153
|
-
#
|
|
152
|
+
# It looks at ATN only after the machine pushed its lines into the bus
|
|
153
|
+
# (see host_written!).
|
|
154
154
|
def host_cycle!
|
|
155
155
|
if @asleep
|
|
156
156
|
return if (@slept += 1) != @wake_at && @host_still
|
|
@@ -169,7 +169,7 @@ module Badline
|
|
|
169
169
|
plan_wake if @asleep
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
# The
|
|
172
|
+
# The machine pushed its lines into the bus, so ATN may have moved.
|
|
173
173
|
def host_written!
|
|
174
174
|
@host_still = false
|
|
175
175
|
end
|
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)
|