badline 0.5.1 → 0.6.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 +52 -0
- data/CLAUDE.md +10 -9
- data/README.md +82 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -23
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +11 -1
- data/lib/badline/audio/cli.rb +18 -5
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio.rb +0 -3
- data/lib/badline/cartridge/action_replay.rb +15 -0
- data/lib/badline/cartridge/comal80.rb +8 -0
- data/lib/badline/cartridge/dinamic.rb +8 -0
- data/lib/badline/cartridge/easy_flash.rb +20 -1
- data/lib/badline/cartridge/epyx_fastload.rb +8 -0
- data/lib/badline/cartridge/final_cartridge3.rb +12 -0
- data/lib/badline/cartridge/flash.rb +41 -0
- data/lib/badline/cartridge/fun_play.rb +8 -0
- data/lib/badline/cartridge/g_mod2.rb +12 -0
- data/lib/badline/cartridge/game_system.rb +8 -0
- data/lib/badline/cartridge/geo_ram.rb +16 -0
- data/lib/badline/cartridge/magic_desk.rb +8 -0
- data/lib/badline/cartridge/ocean.rb +8 -0
- data/lib/badline/cartridge/pagefox.rb +12 -0
- data/lib/badline/cartridge/retro_replay.rb +44 -1
- data/lib/badline/cartridge/rgcd.rb +10 -0
- data/lib/badline/cartridge/super_games.rb +10 -0
- data/lib/badline/cartridge/zaxxon.rb +16 -0
- data/lib/badline/cartridge.rb +72 -0
- data/lib/badline/cia/interrupt_register.rb +17 -0
- data/lib/badline/cia/serial.rb +44 -0
- data/lib/badline/cia/state.rb +47 -0
- data/lib/badline/cia/timer.rb +33 -0
- data/lib/badline/cia.rb +8 -0
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +13 -33
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +3 -212
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +13 -0
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541.rb +35 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +72 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend.rb +18 -0
- data/lib/badline/kernal_trap/channel.rb +27 -0
- data/lib/badline/kernal_trap/drive/channels.rb +25 -0
- data/lib/badline/kernal_trap/drive/memory.rb +9 -0
- data/lib/badline/kernal_trap/drive/status.rb +8 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
- data/lib/badline/kernal_trap/drive.rb +19 -0
- data/lib/badline/kernal_trap/save.rb +8 -0
- data/lib/badline/kernal_trap/serial.rb +16 -0
- data/lib/badline/memory.rb +8 -0
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +78 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +62 -0
- data/lib/badline/options.rb +181 -144
- data/lib/badline/ram_expansion/plus256k.rb +16 -0
- data/lib/badline/ram_expansion/plus60k.rb +12 -0
- data/lib/badline/ram_expansion/unexpanded.rb +6 -0
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -11
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +53 -0
- data/lib/badline/reu.rb +23 -10
- data/lib/badline/sdl.rb +123 -51
- data/lib/badline/sid/envelope/state.rb +68 -0
- data/lib/badline/sid/envelope.rb +7 -0
- data/lib/badline/sid/filter.rb +38 -0
- data/lib/badline/sid/voice.rb +10 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +4 -3
- data/lib/badline/sid.rb +34 -0
- data/lib/badline/snapshot/container.rb +117 -0
- data/lib/badline/snapshot/fields.rb +54 -0
- data/lib/badline/snapshot/image.rb +70 -0
- data/lib/badline/snapshot/machine_state.rb +77 -0
- data/lib/badline/snapshot/setup.rb +58 -0
- data/lib/badline/snapshot/state.rb +178 -0
- data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
- data/lib/badline/snapshot/vice/cias.rb +89 -0
- data/lib/badline/snapshot/vice/cias_import.rb +70 -0
- data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
- data/lib/badline/snapshot/vice/peripherals.rb +47 -0
- data/lib/badline/snapshot/vice/restore.rb +125 -0
- data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
- data/lib/badline/snapshot/vice/vicii.rb +119 -0
- data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
- data/lib/badline/snapshot/vice.rb +74 -0
- data/lib/badline/snapshot.rb +52 -0
- data/lib/badline/storage/crt_file.rb +9 -3
- data/lib/badline/storage/d64_image.rb +2 -0
- data/lib/badline/storage/d71_image.rb +2 -0
- data/lib/badline/storage/d81_image.rb +2 -0
- data/lib/badline/storage/disk_image.rb +11 -2
- data/lib/badline/storage/host_directory.rb +5 -0
- data/lib/badline/storage/t64.rb +6 -0
- data/lib/badline/storage/tap.rb +10 -1
- data/lib/badline/storage/unavailable.rb +20 -0
- data/lib/badline/storage.rb +34 -0
- data/lib/badline/time_of_day.rb +56 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +15 -0
- data/lib/badline/via/interrupt_register.rb +9 -0
- data/lib/badline/via/shift_register.rb +13 -0
- data/lib/badline/via/state.rb +42 -0
- data/lib/badline/via/timer1.rb +13 -0
- data/lib/badline/via/timer2.rb +18 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic/border_mask.rb +10 -0
- data/lib/badline/vic/collisions.rb +11 -0
- data/lib/badline/vic/color_patches.rb +11 -0
- data/lib/badline/vic/display_state.rb +29 -1
- data/lib/badline/vic/graphics_shifter.rb +26 -0
- data/lib/badline/vic/register_log.rb +18 -0
- data/lib/badline/vic/registers.rb +13 -2
- data/lib/badline/vic/sequencer.rb +12 -0
- data/lib/badline/vic/sequencer_state.rb +89 -0
- data/lib/badline/vic/sprite/internal_bus.rb +11 -0
- data/lib/badline/vic/sprite/saved_state.rb +64 -0
- data/lib/badline/vic/sprite.rb +2 -0
- data/lib/badline/vic/sprites.rb +3 -0
- data/lib/badline/vic/sprites_state.rb +57 -0
- data/lib/badline/vic.rb +109 -16
- data/lib/badline.rb +1 -0
- metadata +59 -18
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -117
- data/lib/badline/gui/application.rb +0 -216
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -97
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -13
- data/lib/badline/sdl/events.rb +0 -47
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The visible part of the VIC's display, the crop of the machine's
|
|
6
|
+
# region, repacked for an SDL texture in XRGB8888. Spinel hands an
|
|
7
|
+
# Array of Integers to C as 64-bit words, as badline/ffi does on CRuby,
|
|
8
|
+
# so each word carries two neighbouring pixels, the left one in the low
|
|
9
|
+
# half. The texture is PAL's crop, 384x272, and a region with fewer
|
|
10
|
+
# lines to show sits in the middle of it, between black bands.
|
|
11
|
+
class Screen
|
|
12
|
+
WIDTH = Region::PAL.crop[2]
|
|
13
|
+
HEIGHT = Region::PAL.crop[3]
|
|
14
|
+
ROW_BYTES = WIDTH * 4
|
|
15
|
+
ROW_WORDS = WIDTH / 2
|
|
16
|
+
|
|
17
|
+
COLORS = [
|
|
18
|
+
0x000000, 0xffffff, 0x924a40, 0x84c5cc,
|
|
19
|
+
0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
|
|
20
|
+
0x675200, 0xc33d00, 0xc18178, 0x606060,
|
|
21
|
+
0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
PAIRS = Array.new(256) { |pair| COLORS[pair & 0x0f] | (COLORS[pair >> 4] << 32) }.freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :pixels
|
|
27
|
+
|
|
28
|
+
def initialize(vic)
|
|
29
|
+
@vic = vic
|
|
30
|
+
crop = vic.region.crop
|
|
31
|
+
@col_offset = crop[0]
|
|
32
|
+
@row_offset = crop[1]
|
|
33
|
+
@height = crop[3]
|
|
34
|
+
@band = (HEIGHT - @height) / 2
|
|
35
|
+
@pixels = Array.new(HEIGHT * ROW_WORDS, 0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Repacks only the lines the VIC has changed since the last frame.
|
|
39
|
+
def update
|
|
40
|
+
dirty = @vic.dirty_lines
|
|
41
|
+
row = 0
|
|
42
|
+
while row < @height
|
|
43
|
+
pack_row(row) if dirty[row + @row_offset]
|
|
44
|
+
row += 1
|
|
45
|
+
end
|
|
46
|
+
@vic.clear_dirty_lines!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def pack_row(row)
|
|
52
|
+
display = @vic.display
|
|
53
|
+
pixels = @pixels
|
|
54
|
+
from = ((row + @row_offset) * @vic.width) + @col_offset
|
|
55
|
+
to = (row + @band) * ROW_WORDS
|
|
56
|
+
last = to + ROW_WORDS
|
|
57
|
+
while to < last
|
|
58
|
+
pixels[to] = PAIRS[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
|
|
59
|
+
from += 2
|
|
60
|
+
to += 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# --screenshot's frame: what the renderer drew, read back before it is
|
|
6
|
+
# presented and saved as a BMP.
|
|
7
|
+
module Screenshot
|
|
8
|
+
def self.write(renderer, path)
|
|
9
|
+
SDL.SDL_GetRendererOutputSize(renderer, SDL.output_w, SDL.output_h)
|
|
10
|
+
width = SDL.read_i32(SDL.output_w)
|
|
11
|
+
height = SDL.read_i32(SDL.output_h)
|
|
12
|
+
data = LibC.malloc(width * height * 4)
|
|
13
|
+
SDL.SDL_RenderReadPixels(renderer, nil, SDL::PIXELFORMAT_RGB888, data, width * 4)
|
|
14
|
+
surface = SDL.SDL_CreateRGBSurfaceWithFormatFrom(data, width, height, 32, width * 4, SDL::PIXELFORMAT_RGB888)
|
|
15
|
+
SDL.SDL_SaveBMP_RW(surface, SDL.SDL_RWFromFile(path, "wb"), 1)
|
|
16
|
+
SDL.SDL_FreeSurface(surface)
|
|
17
|
+
LibC.free(data)
|
|
18
|
+
puts "wrote #{path}, #{width}x#{height}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The window's snapshots: F11 saves the machine to a new
|
|
6
|
+
# badline-<date>-<time>.vsf in the working directory, and F12 goes back
|
|
7
|
+
# to the snapshot last saved or opened, in a new machine. --save-snapshot
|
|
8
|
+
# saves one after the last frame.
|
|
9
|
+
class Snapshots
|
|
10
|
+
KEYS = [Keys::F11, Keys::F12].freeze
|
|
11
|
+
|
|
12
|
+
# The machine running, which F12 replaces with the restored one.
|
|
13
|
+
attr_reader :computer
|
|
14
|
+
|
|
15
|
+
def initialize(computer, options)
|
|
16
|
+
@computer = computer
|
|
17
|
+
@last = options.snapshot? ? options.media_path.to_s : ""
|
|
18
|
+
@at_finish = options.save_snapshot
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Saves or restores for F11 or F12, and says whether a restored
|
|
22
|
+
# machine now runs in place of the one before.
|
|
23
|
+
def key(scancode)
|
|
24
|
+
return restore if scancode == Keys::F12
|
|
25
|
+
|
|
26
|
+
save if scancode == Keys::F11
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Saves the machine, and warns when it can't.
|
|
31
|
+
def save(path = "badline-#{Time.now.strftime('%Y%m%d-%H%M%S')}.vsf")
|
|
32
|
+
@computer.save_snapshot(path)
|
|
33
|
+
@last = path
|
|
34
|
+
puts "Saved #{path}"
|
|
35
|
+
rescue Snapshot::FormatError, SystemCallError => e
|
|
36
|
+
warn "badline: #{path}: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Restores the snapshot last saved or opened into a new machine, and
|
|
40
|
+
# warns when it fails, leaving the machine running as it was.
|
|
41
|
+
def restore
|
|
42
|
+
if @last.empty?
|
|
43
|
+
puts "No snapshot to restore, F11 saves one"
|
|
44
|
+
return false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@computer = Snapshot.load(@last) { |line| puts line }
|
|
48
|
+
puts "Restored #{@last}"
|
|
49
|
+
true
|
|
50
|
+
rescue Snapshot::FormatError, SystemCallError => e
|
|
51
|
+
warn "badline: #{@last}: #{e.message}"
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def finish
|
|
56
|
+
save(@at_finish) unless @at_finish.empty?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The SID's output on SDL's audio queue, as mono signed 16-bit samples.
|
|
6
|
+
# Once the device plays, it is the clock: #wait holds each frame until
|
|
7
|
+
# the queue is down to `AHEAD`, so the machine runs at the device's pace.
|
|
8
|
+
#
|
|
9
|
+
# The device starts once the queue holds AHEAD. Below real time the
|
|
10
|
+
# queue runs dry, and the device then stops until it holds AHEAD again,
|
|
11
|
+
# so the sound stutters with silent gaps rather than slowing down. A
|
|
12
|
+
# frame that would take the queue past LIMIT is dropped whole, which
|
|
13
|
+
# only happens unpaced.
|
|
14
|
+
#
|
|
15
|
+
# Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
|
|
16
|
+
class Sound
|
|
17
|
+
RATE = 44_100
|
|
18
|
+
AHEAD = 0.08
|
|
19
|
+
LIMIT = 0.25
|
|
20
|
+
|
|
21
|
+
attr_reader :rate, :underruns, :dropped, :queued, :low, :high
|
|
22
|
+
|
|
23
|
+
def initialize(sid, wanted, verbose)
|
|
24
|
+
@sid = sid
|
|
25
|
+
@verbose = verbose
|
|
26
|
+
@device = 0
|
|
27
|
+
@rate = RATE
|
|
28
|
+
@started = false
|
|
29
|
+
@muted = false
|
|
30
|
+
@underruns = 0
|
|
31
|
+
@dropped = 0
|
|
32
|
+
@queued = 0
|
|
33
|
+
@buffer = IO::Buffer.new(4096)
|
|
34
|
+
reset_levels
|
|
35
|
+
open_device if wanted
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on? = @device != 0
|
|
39
|
+
|
|
40
|
+
# Plays another machine's SID from here on.
|
|
41
|
+
def sid=(sid)
|
|
42
|
+
@sid = sid
|
|
43
|
+
sid.record(rate: @rate) if on?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def muted? = @muted
|
|
47
|
+
|
|
48
|
+
def playing? = @started
|
|
49
|
+
|
|
50
|
+
def feed
|
|
51
|
+
return unless on?
|
|
52
|
+
|
|
53
|
+
samples = @sid.drain_samples
|
|
54
|
+
return if @muted || samples.empty?
|
|
55
|
+
|
|
56
|
+
level = queued_seconds
|
|
57
|
+
if level + (samples.size.to_f / @rate) > LIMIT
|
|
58
|
+
@dropped += samples.size
|
|
59
|
+
else
|
|
60
|
+
play(samples, level)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# How much the audio queue holds, in seconds.
|
|
65
|
+
def level = queued_seconds
|
|
66
|
+
|
|
67
|
+
# Waits until the queue is down to `level` seconds.
|
|
68
|
+
def wait(level = AHEAD)
|
|
69
|
+
SDL.SDL_Delay(1) while queued_seconds > level
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def toggle_mute
|
|
73
|
+
@muted = !@muted
|
|
74
|
+
return unless @muted
|
|
75
|
+
|
|
76
|
+
halt
|
|
77
|
+
SDL.SDL_ClearQueuedAudio(@device)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reset_levels
|
|
81
|
+
@low = LIMIT
|
|
82
|
+
@high = 0.0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def close
|
|
86
|
+
SDL.SDL_CloseAudioDevice(@device) if on?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def open_device
|
|
92
|
+
unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
|
|
93
|
+
puts "No sound: #{SDL.SDL_GetError}"
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
SDL.spec_freq(SDL.wanted, RATE)
|
|
98
|
+
SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
|
|
99
|
+
SDL.spec_channels(SDL.wanted, 1)
|
|
100
|
+
SDL.spec_samples(SDL.wanted, 512)
|
|
101
|
+
@device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, SDL::ALLOW_FREQUENCY_CHANGE)
|
|
102
|
+
return puts "No sound: #{SDL.SDL_GetError}" unless on?
|
|
103
|
+
|
|
104
|
+
@rate = SDL.read_i32(SDL.obtained)
|
|
105
|
+
@sid.record(rate: @rate)
|
|
106
|
+
puts "Sound at #{@rate} Hz" if @verbose
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
|
|
110
|
+
|
|
111
|
+
def play(samples, level)
|
|
112
|
+
@low = level if level < @low
|
|
113
|
+
underrun! if @started && level.zero?
|
|
114
|
+
queue(samples)
|
|
115
|
+
level = queued_seconds
|
|
116
|
+
@high = level if level > @high
|
|
117
|
+
start if level >= AHEAD
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def queue(samples)
|
|
121
|
+
count = samples.size
|
|
122
|
+
bytes = count * 2
|
|
123
|
+
@buffer.resize(bytes) if bytes > @buffer.size
|
|
124
|
+
buffer = @buffer
|
|
125
|
+
i = 0
|
|
126
|
+
while i < count
|
|
127
|
+
buffer.set_value(:s16, i * 2, samples[i])
|
|
128
|
+
i += 1
|
|
129
|
+
end
|
|
130
|
+
SDL.SDL_QueueAudio(@device, buffer, bytes)
|
|
131
|
+
@queued += count
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def start
|
|
135
|
+
return if @started
|
|
136
|
+
|
|
137
|
+
@started = true
|
|
138
|
+
SDL.SDL_PauseAudioDevice(@device, 0)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def halt
|
|
142
|
+
@started = false
|
|
143
|
+
SDL.SDL_PauseAudioDevice(@device, 1)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def underrun!
|
|
147
|
+
@underruns += 1
|
|
148
|
+
puts "Running below real time, so the sound will stutter." if @underruns == 1
|
|
149
|
+
halt
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The events of --at and --script. Each runs once its number of frames
|
|
6
|
+
# has run: a screenshot saves that frame, and the rest act on the
|
|
7
|
+
# machine before the next frame is clocked. A key or the freeze button
|
|
8
|
+
# is let go Options::Event::HOLD frames after it is pressed. An event
|
|
9
|
+
# that fails ends the run.
|
|
10
|
+
class Timeline
|
|
11
|
+
# Takes the events, --screenshot's at the last frame and whether to
|
|
12
|
+
# insert disks write-protected from Options.
|
|
13
|
+
def initialize(options)
|
|
14
|
+
@events = options.timeline
|
|
15
|
+
unless options.screenshot.empty? || options.frames.zero?
|
|
16
|
+
@events += [Options::Event.new(options.frames, "screenshot", options.screenshot)]
|
|
17
|
+
end
|
|
18
|
+
@read_only = options.read_only?
|
|
19
|
+
@program = options.program
|
|
20
|
+
@releases = []
|
|
21
|
+
@failed = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# What's wrong with the events, the first key that names nothing,
|
|
25
|
+
# or an empty string.
|
|
26
|
+
def self.error(events)
|
|
27
|
+
event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument) }
|
|
28
|
+
event.nil? ? "" : "no such key: #{event.argument}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.key?(name) = name == "restore" || !keyboard_key(name).nil? || !joystick_direction(name).nil?
|
|
32
|
+
|
|
33
|
+
# The Keyboard's key for a name such as space, return or a.
|
|
34
|
+
def self.keyboard_key(name)
|
|
35
|
+
key = Keyboard.new.matrix.flatten.find { |candidate| candidate.to_s == name }
|
|
36
|
+
key.nil? ? Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name } : key
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The direction of joy1-NAME or joy2-NAME, such as up or fire.
|
|
40
|
+
def self.joystick_direction(name)
|
|
41
|
+
return nil unless name.start_with?("joy1-") || name.start_with?("joy2-")
|
|
42
|
+
|
|
43
|
+
Joystick::DIRECTIONS.keys.find { |direction| direction.to_s == name[5, name.size - 5] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The screenshot's file name, with the frame's number in place of %d,
|
|
47
|
+
# or of %Nd or %0Nd padded to N digits.
|
|
48
|
+
def self.path(pattern, frame)
|
|
49
|
+
start = pattern.index("%")
|
|
50
|
+
finish = start.nil? ? nil : pattern.index("d", start)
|
|
51
|
+
return pattern if finish.nil?
|
|
52
|
+
|
|
53
|
+
width = pattern[start + 1, finish - start - 1]
|
|
54
|
+
return pattern unless width.match?(/\A\d*\z/)
|
|
55
|
+
|
|
56
|
+
number = frame.to_s.rjust(width.to_i, width.start_with?("0") ? "0" : " ")
|
|
57
|
+
pattern[0, start] + number + pattern[finish + 1, pattern.size - finish - 1]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The files the frame's screenshots go to.
|
|
61
|
+
def screenshots(frame)
|
|
62
|
+
@events.select { |event| event.frame == frame && event.action == "screenshot" }
|
|
63
|
+
.map { |event| Timeline.path(event.argument, frame) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether the run ends at the frame: at a quit, or once an event failed.
|
|
67
|
+
def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
|
|
68
|
+
|
|
69
|
+
# Whether an event failed, which ends the run.
|
|
70
|
+
def failed? = @failed
|
|
71
|
+
|
|
72
|
+
# Lets go of what was held until this frame, then runs its events, up
|
|
73
|
+
# to one that fails.
|
|
74
|
+
def run(computer, frame)
|
|
75
|
+
@releases.each { |event| release(computer, event) if event.frame == frame }
|
|
76
|
+
@releases.reject! { |event| event.frame == frame }
|
|
77
|
+
@events.each { |event| perform(computer, event) if event.frame == frame && !@failed }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def perform(computer, event)
|
|
83
|
+
case event.action
|
|
84
|
+
when "key" then press(computer, event)
|
|
85
|
+
when "type" then computer.type_text(event.argument.gsub("\\n", "\r").gsub("\\r", "\r"))
|
|
86
|
+
when "insert" then insert(computer, event.argument)
|
|
87
|
+
when "eject" then eject(computer, event.argument)
|
|
88
|
+
when "reset" then computer.reset!
|
|
89
|
+
when "freeze" then freeze(computer, event)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def press(computer, event)
|
|
94
|
+
name = event.argument
|
|
95
|
+
if name == "restore"
|
|
96
|
+
computer.press_restore
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
direction = Timeline.joystick_direction(name)
|
|
101
|
+
if direction.nil?
|
|
102
|
+
computer.keyboard.press(Timeline.keyboard_key(name))
|
|
103
|
+
else
|
|
104
|
+
joystick(computer, name).press(direction)
|
|
105
|
+
end
|
|
106
|
+
hold(event)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def freeze(computer, event)
|
|
110
|
+
computer.press_cartridge_button
|
|
111
|
+
hold(event)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def hold(event)
|
|
115
|
+
@releases << Options::Event.new(event.frame + Options::Event::HOLD, event.action, event.argument)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def release(computer, event)
|
|
119
|
+
return computer.release_cartridge_button if event.action == "freeze"
|
|
120
|
+
|
|
121
|
+
name = event.argument
|
|
122
|
+
direction = Timeline.joystick_direction(name)
|
|
123
|
+
if direction.nil?
|
|
124
|
+
computer.keyboard.release(Timeline.keyboard_key(name))
|
|
125
|
+
else
|
|
126
|
+
joystick(computer, name).release(direction)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def joystick(computer, name) = name.start_with?("joy1-") ? computer.joystick1 : computer.joystick2
|
|
131
|
+
|
|
132
|
+
def insert(computer, path)
|
|
133
|
+
puts Media.attach(computer, path, autostart: false, disk: { read_only: @read_only })
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
warn "#{@program}: #{path}: #{e.message}"
|
|
136
|
+
@failed = true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def eject(computer, what)
|
|
140
|
+
return puts "No #{what} to eject" unless inserted?(computer, what)
|
|
141
|
+
|
|
142
|
+
if what == "disk"
|
|
143
|
+
eject_disk(computer)
|
|
144
|
+
elsif what == "tape"
|
|
145
|
+
computer.datasette.eject
|
|
146
|
+
else
|
|
147
|
+
computer.address_bus.detach_cartridge
|
|
148
|
+
computer.power_cycle!
|
|
149
|
+
end
|
|
150
|
+
puts "Ejected the #{what}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def inserted?(computer, what)
|
|
154
|
+
return !computer.datasette.tape.nil? if what == "tape"
|
|
155
|
+
return !computer.address_bus.cartridge.nil? if what == "cartridge"
|
|
156
|
+
|
|
157
|
+
drive = Media::TrueDrive.drive(computer)
|
|
158
|
+
drive.nil? ? computer.mounted? : !drive.disk.nil?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def eject_disk(computer)
|
|
162
|
+
drive = Media::TrueDrive.drive(computer)
|
|
163
|
+
if drive.nil?
|
|
164
|
+
computer.unmount
|
|
165
|
+
else
|
|
166
|
+
drive.insert(nil)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/sdl"
|
|
4
|
+
require "badline/frontend/boot"
|
|
5
|
+
require "badline/frontend/frame_rate"
|
|
6
|
+
require "badline/frontend/screen"
|
|
7
|
+
require "badline/frontend/drive_led"
|
|
8
|
+
require "badline/frontend/keys"
|
|
9
|
+
require "badline/frontend/snapshots"
|
|
10
|
+
require "badline/frontend/screenshot"
|
|
11
|
+
require "badline/frontend/timeline"
|
|
12
|
+
require "badline/frontend/sound"
|
|
13
|
+
require "badline/frontend/pacer"
|
|
14
|
+
require "badline/frontend/controls"
|
|
15
|
+
require "badline/frontend/pad_port"
|
|
16
|
+
require "badline/frontend/gamepads"
|
|
17
|
+
require "badline/frontend/app"
|
|
18
|
+
require "badline/frontend/audio_sink"
|
|
@@ -80,6 +80,33 @@ module Badline
|
|
|
80
80
|
data ? new(data) : new([], error: [Drive::ILLEGAL_TRACK_OR_SECTOR, track, sector])
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Reads what save_state wrote after the flag that marks a file open
|
|
84
|
+
# for writing (Drive::Channels.channel_from_state). A buffer channel
|
|
85
|
+
# takes its block in `ram`, the drive's RAM, again.
|
|
86
|
+
def self.from_state(input, ram)
|
|
87
|
+
new.tap { |channel| channel.load_state(input, ram) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A buffer channel's bytes are the drive's RAM, which the drive
|
|
91
|
+
# writes itself.
|
|
92
|
+
def save_state(out)
|
|
93
|
+
out.boolean(false).boolean(@writable).int(@base).int(@length).int(@pointer)
|
|
94
|
+
out.optional_int(@lead).optional_int(@buffer).boolean(!@error.nil?)
|
|
95
|
+
out.ints(@error) if @error
|
|
96
|
+
out.blob(@bytes) unless @writable
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def load_state(input, ram)
|
|
100
|
+
@writable = input.boolean?
|
|
101
|
+
@base = input.int
|
|
102
|
+
@length = input.int
|
|
103
|
+
@pointer = input.int
|
|
104
|
+
@lead = input.optional_int
|
|
105
|
+
@buffer = input.optional_int
|
|
106
|
+
@error = input.boolean? ? input.ints : nil
|
|
107
|
+
@bytes = @writable ? ram : input.blob
|
|
108
|
+
end
|
|
109
|
+
|
|
83
110
|
def replace(bytes, length = bytes.length)
|
|
84
111
|
@bytes = bytes
|
|
85
112
|
rewind(length)
|
|
@@ -22,6 +22,31 @@ module Badline
|
|
|
22
22
|
|
|
23
23
|
def clear = @channels.clear
|
|
24
24
|
|
|
25
|
+
# Each open channel with its secondary address, in the order they
|
|
26
|
+
# opened.
|
|
27
|
+
def save_state(out)
|
|
28
|
+
out.int(@channels.length)
|
|
29
|
+
@channels.each do |secondary, channel|
|
|
30
|
+
out.int(secondary)
|
|
31
|
+
channel.save_state(out)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Buffer channels take their block in the drive's RAM again.
|
|
36
|
+
def load_state(input, memory)
|
|
37
|
+
@channels.clear
|
|
38
|
+
input.int.times do
|
|
39
|
+
secondary = input.int
|
|
40
|
+
@channels[secondary] = Channels.channel_from_state(input, memory.ram)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A channel save_state wrote: a file open for writing, or a Channel
|
|
45
|
+
# whose buffer, if it has one, is in `ram`.
|
|
46
|
+
def self.channel_from_state(input, ram)
|
|
47
|
+
input.boolean? ? WriteFile.from_state(input) : Channel.from_state(input, ram)
|
|
48
|
+
end
|
|
49
|
+
|
|
25
50
|
# Opens a buffer channel on the buffer a "#n" name asks for, or on
|
|
26
51
|
# the highest free one. Returns nil when that buffer isn't free,
|
|
27
52
|
# which leaves a channel asking again for its own buffer open.
|
|
@@ -62,6 +62,15 @@ module Badline
|
|
|
62
62
|
load_buffer(Channels::BAM_BUFFER, block) if block
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
def save_state(out)
|
|
66
|
+
out.blob(@ram).boolean(@bam)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_state(input)
|
|
70
|
+
input.blob_into(@ram)
|
|
71
|
+
@bam = input.boolean?
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
# Whether the BAM has been read since the last reset.
|
|
66
75
|
def bam? = @bam
|
|
67
76
|
|
|
@@ -11,6 +11,17 @@ module Badline
|
|
|
11
11
|
attr_accessor :buffer
|
|
12
12
|
attr_reader :name, :type, :mode
|
|
13
13
|
|
|
14
|
+
TYPES = %i[del seq prg usr rel].freeze
|
|
15
|
+
MODES = %i[write replace append].freeze
|
|
16
|
+
|
|
17
|
+
# Reads what save_state wrote after the flag that marks a file
|
|
18
|
+
# open for writing.
|
|
19
|
+
def self.from_state(input)
|
|
20
|
+
name = input.string
|
|
21
|
+
type = TYPES.fetch(input.int).to_sym
|
|
22
|
+
new(name, type, mode: MODES.fetch(input.int).to_sym).tap { |file| file.load_state(input) }
|
|
23
|
+
end
|
|
24
|
+
|
|
14
25
|
def initialize(name, type, mode: :write)
|
|
15
26
|
@name = name
|
|
16
27
|
@type = type
|
|
@@ -18,6 +29,16 @@ module Badline
|
|
|
18
29
|
@bytes = []
|
|
19
30
|
end
|
|
20
31
|
|
|
32
|
+
def save_state(out)
|
|
33
|
+
out.boolean(true).string(@name).int(TYPES.index(@type)).int(MODES.index(@mode))
|
|
34
|
+
out.blob(@bytes).optional_int(@buffer)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_state(input)
|
|
38
|
+
@bytes = input.blob
|
|
39
|
+
@buffer = input.optional_int
|
|
40
|
+
end
|
|
41
|
+
|
|
21
42
|
def write(bytes) = @bytes.concat(bytes)
|
|
22
43
|
|
|
23
44
|
def writable? = true
|
|
@@ -143,6 +143,25 @@ module Badline
|
|
|
143
143
|
# vector, as U: does.
|
|
144
144
|
def reset! = reset(cold: true)
|
|
145
145
|
|
|
146
|
+
# The mounted storage's setup, for Storage.reopen, then the drive's
|
|
147
|
+
# RAM, its status and its open channels.
|
|
148
|
+
def save_state(out)
|
|
149
|
+
@storage.save_setup(out)
|
|
150
|
+
out.marker("TRAP DRIVE")
|
|
151
|
+
@memory.save_state(out)
|
|
152
|
+
@status.save_state(out)
|
|
153
|
+
@channels.save_state(out)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Takes the state after the storage's setup, into a drive mounted on
|
|
157
|
+
# the reopened storage.
|
|
158
|
+
def load_state(input)
|
|
159
|
+
input.marker("TRAP DRIVE")
|
|
160
|
+
@memory.load_state(input)
|
|
161
|
+
@status.load_state(input)
|
|
162
|
+
@channels.load_state(input, @memory)
|
|
163
|
+
end
|
|
164
|
+
|
|
146
165
|
# Returns the next byte and whether it is the channel's last, or nil
|
|
147
166
|
# when there is nothing left to send.
|
|
148
167
|
def read(secondary)
|
|
@@ -46,6 +46,22 @@ module Badline
|
|
|
46
46
|
@buffer = []
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# The frame under way, with the channels and the bytes it gathered.
|
|
50
|
+
# The device it answers is the machine's to set.
|
|
51
|
+
def save_state(out)
|
|
52
|
+
out.boolean(@listening).boolean(@talking).optional_int(@listen_channel).optional_int(@talk_channel)
|
|
53
|
+
out.int(@frame).blob(@buffer)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_state(input)
|
|
57
|
+
@listening = input.boolean?
|
|
58
|
+
@talking = input.boolean?
|
|
59
|
+
@listen_channel = input.optional_int
|
|
60
|
+
@talk_channel = input.optional_int
|
|
61
|
+
@frame = input.int
|
|
62
|
+
@buffer = input.blob
|
|
63
|
+
end
|
|
64
|
+
|
|
49
65
|
def install
|
|
50
66
|
ROUTINES.each do |address, routine|
|
|
51
67
|
@cpu.install_trap(address) { call_routine(routine) if kernal? }
|