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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
class Disk
|
|
6
|
+
# Saving and restoring a Disk for a snapshot: the image by its path,
|
|
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.
|
|
9
|
+
module State
|
|
10
|
+
# The image's path, expanded, and whether it was opened read-only,
|
|
11
|
+
# for a disk from Disk.open. Nil for a disk made another way.
|
|
12
|
+
attr_reader :path, :read_only
|
|
13
|
+
|
|
14
|
+
# A .g64 image by its name, and anything else a .d64.
|
|
15
|
+
def self.g64?(path) = File.extname(path).casecmp?(".g64")
|
|
16
|
+
|
|
17
|
+
# A disk for the image at the path, its tracks left for the state.
|
|
18
|
+
def self.reopen(path, read_only)
|
|
19
|
+
if g64?(path)
|
|
20
|
+
Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
|
|
21
|
+
else
|
|
22
|
+
Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The disk a state from save_state describes: +current+ when it was
|
|
27
|
+
# opened from the same image the same way, and otherwise a disk for
|
|
28
|
+
# the image at the path the state names, or one without an image,
|
|
29
|
+
# as a detached reader always gets. Either way its tracks come from
|
|
30
|
+
# the state.
|
|
31
|
+
def self.load(input, current)
|
|
32
|
+
path = input.optional_string
|
|
33
|
+
read_only = input.boolean?
|
|
34
|
+
disk = current if current && !path.nil? && current.path == path && current.read_only == read_only
|
|
35
|
+
disk ||= path && !input.detached? ? reopen(path, read_only) : Disk.new
|
|
36
|
+
disk.load_tracks(input)
|
|
37
|
+
disk
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Notes the image's path and how it was opened.
|
|
41
|
+
def opened(path, read_only)
|
|
42
|
+
@path = File.expand_path(path)
|
|
43
|
+
@read_only = read_only
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_state(out)
|
|
48
|
+
out.optional_string(@path).boolean(@read_only)
|
|
49
|
+
@tracks.each do |track|
|
|
50
|
+
out.boolean(!track.nil?)
|
|
51
|
+
save_track(track, out) if track
|
|
52
|
+
end
|
|
53
|
+
out.ints(@written.keys)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_tracks(input)
|
|
57
|
+
@tracks.each_index do |half_track|
|
|
58
|
+
@tracks[half_track] = input.boolean? ? load_track(input) : nil
|
|
59
|
+
end
|
|
60
|
+
@written.clear
|
|
61
|
+
input.ints.each { |half_track| @written[half_track] = true }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def save_track(track, out)
|
|
67
|
+
out.int(track.zone).boolean(!track.speeds.nil?)
|
|
68
|
+
out.ints(track.speeds) if track.speeds
|
|
69
|
+
out.blob(track.bytes)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_track(input)
|
|
73
|
+
zone = input.int
|
|
74
|
+
speeds = input.boolean? ? input.ints : nil
|
|
75
|
+
Track.new(input.blob, zone, speeds)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/rotation"
|
|
4
|
+
require "badline/drive1541/mechanism_state"
|
|
4
5
|
|
|
5
6
|
module Badline
|
|
6
7
|
class Drive1541
|
|
@@ -65,6 +66,7 @@ module Badline
|
|
|
65
66
|
# half track after it.
|
|
66
67
|
class Mechanism
|
|
67
68
|
include Rotation
|
|
69
|
+
include MechanismState
|
|
68
70
|
|
|
69
71
|
MOTOR = 0x04
|
|
70
72
|
LED = 0x08
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Saving and restoring the Mechanism for a snapshot.
|
|
6
|
+
module MechanismState
|
|
7
|
+
# The disk (Disk#save_state) and everything the head, the motor, the
|
|
8
|
+
# stepper and the read and write electronics hold, down to the cell
|
|
9
|
+
# under the head and the read clock's phase.
|
|
10
|
+
def save_state(out)
|
|
11
|
+
out.boolean(!@disk.nil?)
|
|
12
|
+
@disk&.save_state(out)
|
|
13
|
+
[@half_track, @slip, @zone, @clock, @time, @clock_at, @clocks, @shift, @ones, @bits, @write_shift,
|
|
14
|
+
@index, @mask, @cell_end, @width, @next].each { |value| out.int(value) }
|
|
15
|
+
[@motor, @led, @sync, @byte_ready, @writing, @write_gate, @protected, @track_written].each do |flag|
|
|
16
|
+
out.boolean(flag)
|
|
17
|
+
end
|
|
18
|
+
out.optional_int(@write_index)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Puts the state back, reusing the disk in when it's the one the state
|
|
22
|
+
# names (Disk::State.load). A disk taken out is flushed, as insert
|
|
23
|
+
# does.
|
|
24
|
+
def load_state(input)
|
|
25
|
+
disk = input.boolean? ? Disk::State.load(input, @disk) : nil
|
|
26
|
+
flush unless disk.equal?(@disk)
|
|
27
|
+
@disk = disk
|
|
28
|
+
load_head(input)
|
|
29
|
+
load_flags(input)
|
|
30
|
+
@write_index = input.optional_int
|
|
31
|
+
head_on_track
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_head(input)
|
|
37
|
+
@half_track = input.int
|
|
38
|
+
@slip = input.int
|
|
39
|
+
@zone = input.int
|
|
40
|
+
@clock = input.int
|
|
41
|
+
@time = input.int
|
|
42
|
+
@clock_at = input.int
|
|
43
|
+
@clocks = input.int
|
|
44
|
+
@shift = input.int
|
|
45
|
+
@ones = input.int
|
|
46
|
+
@bits = input.int
|
|
47
|
+
@write_shift = input.int
|
|
48
|
+
@index = input.int
|
|
49
|
+
@mask = input.int
|
|
50
|
+
@cell_end = input.int
|
|
51
|
+
@width = input.int
|
|
52
|
+
@next = input.int
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_flags(input)
|
|
56
|
+
@motor = input.boolean?
|
|
57
|
+
@led = input.boolean?
|
|
58
|
+
@sync = input.boolean?
|
|
59
|
+
@byte_ready = input.boolean?
|
|
60
|
+
@writing = input.boolean?
|
|
61
|
+
@write_gate = input.boolean?
|
|
62
|
+
@protected = input.boolean?
|
|
63
|
+
@track_written = input.boolean?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -92,6 +92,16 @@ module Badline
|
|
|
92
92
|
@writing ? write_bit(at) : read_bit(one)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# The track under the head and its layout, leaving the head's place
|
|
96
|
+
# on it as it stands.
|
|
97
|
+
def head_on_track
|
|
98
|
+
@track = @disk&.track(@half_track) || BLANK
|
|
99
|
+
@bytes = @track.bytes
|
|
100
|
+
@widths = @track.widths
|
|
101
|
+
@length = @track.length
|
|
102
|
+
@last = @length - 1
|
|
103
|
+
end
|
|
104
|
+
|
|
95
105
|
# The track under the head, from the cell that's under it +time+
|
|
96
106
|
# into the turn.
|
|
97
107
|
def load_track(time = @time)
|
|
@@ -44,6 +44,16 @@ module Badline
|
|
|
44
44
|
@host = @bus ? @bus.host_lines : 0
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The C64's lines as last latched. The jumpers and the bus are the
|
|
48
|
+
# drive's wiring.
|
|
49
|
+
def save_state(out)
|
|
50
|
+
out.int(@host)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load_state(input)
|
|
54
|
+
@host = input.int
|
|
55
|
+
end
|
|
56
|
+
|
|
47
57
|
# Whether the C64 has moved ATN since the last latch_host.
|
|
48
58
|
def atn_moved?
|
|
49
59
|
lines = @bus ? @bus.host_lines : 0
|
data/lib/badline/drive1541.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/bus"
|
|
4
|
+
require "badline/drive1541/cpu"
|
|
4
5
|
require "badline/drive1541/serial_port"
|
|
5
6
|
require "badline/drive1541/gcr"
|
|
6
7
|
require "badline/drive1541/track"
|
|
@@ -180,6 +181,40 @@ module Badline
|
|
|
180
181
|
step
|
|
181
182
|
end
|
|
182
183
|
|
|
184
|
+
# The drive's whole state: its clock phase, the CPU, RAM, both VIAs,
|
|
185
|
+
# the serial port's latch of the C64's lines and the mechanism with its
|
|
186
|
+
# disk. The drive settles first, so it is awake and not recording a
|
|
187
|
+
# pass, and a restored drive starts the same way, its orbits forgotten
|
|
188
|
+
# from the state it takes: Idle's and Orbit's bookkeeping
|
|
189
|
+
# isn't stored. The device number, the host's clock rate and whether
|
|
190
|
+
# the drive may sleep are the machine's wiring.
|
|
191
|
+
def save_state(out)
|
|
192
|
+
settle!
|
|
193
|
+
out.marker("DRIVE1541")
|
|
194
|
+
out.int(@phase).int(@cycles).boolean(@so_pending)
|
|
195
|
+
@serial_port.save_state(out)
|
|
196
|
+
@via1.save_state(out)
|
|
197
|
+
@via2.save_state(out)
|
|
198
|
+
@bus.save_state(out)
|
|
199
|
+
@cpu.save_state(out)
|
|
200
|
+
@mechanism.save_state(out)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def load_state(input)
|
|
204
|
+
settle!
|
|
205
|
+
input.marker("DRIVE1541")
|
|
206
|
+
@phase = input.int
|
|
207
|
+
@cycles = input.int
|
|
208
|
+
@so_pending = input.boolean?
|
|
209
|
+
@serial_port.load_state(input)
|
|
210
|
+
@via1.load_state(input)
|
|
211
|
+
@via2.load_state(input)
|
|
212
|
+
@bus.load_state(input)
|
|
213
|
+
@cpu.load_state(input)
|
|
214
|
+
@mechanism.load_state(input)
|
|
215
|
+
forget_orbits
|
|
216
|
+
end
|
|
217
|
+
|
|
183
218
|
def inspect
|
|
184
219
|
"#<#{self.class.name} cycles=#{cycles} cpu=(#{cpu.inspect})>"
|
|
185
220
|
end
|
data/lib/badline/ffi.rb
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
# The part of Spinel's FFI DSL the SDL binding uses, for CRuby, over
|
|
7
|
+
# Fiddle, so the one binding in badline/sdl serves both builds. Spinel
|
|
8
|
+
# compiles the declarations itself and never loads this file: require it
|
|
9
|
+
# before a file that declares a binding.
|
|
10
|
+
#
|
|
11
|
+
# The declarations behave as they do in Spinel. ffi_func defines
|
|
12
|
+
# Module.name(...); a pointer argument takes nil for NULL, a Fiddle
|
|
13
|
+
# pointer, a String (its bytes) or an IO::Buffer, whose contents C sees
|
|
14
|
+
# for the call's duration; a NULL pointer comes back as nil. ffi_const
|
|
15
|
+
# sets Module::NAME, ffi_buffer defines Module.name returning a zeroed
|
|
16
|
+
# static buffer, and ffi_read_<width> and ffi_write_<width> define field
|
|
17
|
+
# accessors at a fixed offset into a pointer.
|
|
18
|
+
module FFI
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
TYPES = {
|
|
22
|
+
void: Fiddle::TYPE_VOID,
|
|
23
|
+
int: Fiddle::TYPE_INT, bool: Fiddle::TYPE_INT, long: Fiddle::TYPE_LONG, size_t: Fiddle::TYPE_SIZE_T,
|
|
24
|
+
int8: Fiddle::TYPE_INT8_T, uint8: Fiddle::TYPE_UINT8_T, int16: Fiddle::TYPE_INT16_T,
|
|
25
|
+
uint16: Fiddle::TYPE_UINT16_T, int32: Fiddle::TYPE_INT32_T, uint32: Fiddle::TYPE_UINT32_T,
|
|
26
|
+
float: Fiddle::TYPE_FLOAT, double: Fiddle::TYPE_DOUBLE,
|
|
27
|
+
str: Fiddle::TYPE_CONST_STRING,
|
|
28
|
+
ptr: Fiddle::TYPE_VOIDP, buffer_in: Fiddle::TYPE_VOIDP, buffer_out: Fiddle::TYPE_VOIDP,
|
|
29
|
+
buffer_inout: Fiddle::TYPE_VOIDP, int_array: Fiddle::TYPE_VOIDP, float_array: Fiddle::TYPE_VOIDP
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# The argument specs C may write through, so an IO::Buffer passed in one
|
|
33
|
+
# gets back what C left in it.
|
|
34
|
+
WRITABLE = %i[ptr buffer_out buffer_inout].freeze
|
|
35
|
+
|
|
36
|
+
# pack directives for the accessor widths, in the host's byte order.
|
|
37
|
+
WIDTHS = {
|
|
38
|
+
u8: "C", i8: "c", u16: "S", i16: "s", u32: "L", i32: "l", u64: "Q", i64: "q", ptr: "J"
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Where dlopen looks for a library besides its search path, which leaves
|
|
42
|
+
# out Homebrew's, and the versioned names a runtime package installs
|
|
43
|
+
# without the development package's unversioned link.
|
|
44
|
+
DIRECTORIES = [nil, "/opt/homebrew/lib", "/usr/local/lib"].freeze
|
|
45
|
+
VERSIONED = { "SDL2" => %w[libSDL2-2.0.so.0 libSDL2-2.0.0.dylib] }.freeze
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
def open_library(name)
|
|
50
|
+
candidates = library_candidates(name)
|
|
51
|
+
candidates.each do |candidate|
|
|
52
|
+
return Fiddle.dlopen(candidate)
|
|
53
|
+
rescue Fiddle::DLError
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
raise Error, "can't find lib#{name} (tried #{candidates.join(', ')})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def library_candidates(name)
|
|
60
|
+
names = ["lib#{name}.dylib", "lib#{name}.so", *VERSIONED.fetch(name, []), "#{name}.dll"]
|
|
61
|
+
DIRECTORIES.flat_map { |dir| names.map { |file| dir ? File.join(dir, file) : file } }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The value Fiddle passes for one argument, and a block that copies what
|
|
65
|
+
# C wrote back into an IO::Buffer once the call returns.
|
|
66
|
+
def argument(spec, value)
|
|
67
|
+
case value
|
|
68
|
+
when IO::Buffer then buffer_argument(spec, value)
|
|
69
|
+
when Array then [value.pack(spec == :float_array ? "d*" : "q*")]
|
|
70
|
+
when true, false then [value ? 1 : 0]
|
|
71
|
+
else [value]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def buffer_argument(spec, buffer)
|
|
76
|
+
return [nil] if buffer.null? || buffer.empty?
|
|
77
|
+
|
|
78
|
+
bytes = buffer.get_string
|
|
79
|
+
return [bytes] unless WRITABLE.include?(spec) && !buffer.readonly?
|
|
80
|
+
|
|
81
|
+
[bytes, -> { buffer.set_string(bytes) }]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def result(spec, value)
|
|
85
|
+
case spec
|
|
86
|
+
when :ptr then value.null? ? nil : value
|
|
87
|
+
when :bool then value != 0
|
|
88
|
+
else value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read(pointer, offset, width)
|
|
93
|
+
value = Fiddle::Pointer[pointer][offset, width_size(width)].unpack1(WIDTHS.fetch(width))
|
|
94
|
+
width == :ptr ? result(:ptr, Fiddle::Pointer.new(value)) : value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write(pointer, offset, width, value)
|
|
98
|
+
value = value.to_i if width == :ptr
|
|
99
|
+
Fiddle::Pointer[pointer][offset, width_size(width)] = [value || 0].pack(WIDTHS.fetch(width))
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def width_size(width) = [0].pack(WIDTHS.fetch(width)).bytesize
|
|
104
|
+
|
|
105
|
+
# The declarations, private methods of every module once this file is
|
|
106
|
+
# loaded, as they are keywords of a module body in Spinel.
|
|
107
|
+
module DSL
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def ffi_lib(name)
|
|
111
|
+
(@ffi_libraries ||= []) << FFI.open_library(name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def ffi_func(name, arguments, returns, blocking: false)
|
|
115
|
+
function = Fiddle::Function.new(ffi_symbol(name), arguments.map { |spec| TYPES.fetch(spec) },
|
|
116
|
+
TYPES.fetch(returns), name: name.to_s, need_gvl: !blocking)
|
|
117
|
+
define_singleton_method(name) do |*values|
|
|
118
|
+
converted = arguments.zip(values).map { |spec, value| FFI.argument(spec, value) }
|
|
119
|
+
returned = function.call(*converted.map(&:first))
|
|
120
|
+
converted.each { |argument| argument[1]&.call }
|
|
121
|
+
FFI.result(returns, returned)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def ffi_const(name, value)
|
|
126
|
+
const_set(name, value)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ffi_buffer(name, size)
|
|
130
|
+
buffer = Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE)
|
|
131
|
+
buffer[0, size] = "\0" * size
|
|
132
|
+
define_singleton_method(name) { buffer }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
WIDTHS.each_key do |width|
|
|
136
|
+
define_method(:"ffi_read_#{width}") do |name, offset|
|
|
137
|
+
define_singleton_method(name) { |pointer| FFI.read(pointer, offset, width) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
define_method(:"ffi_write_#{width}") do |name, offset|
|
|
141
|
+
define_singleton_method(name) { |pointer, value| FFI.write(pointer, offset, width, value) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ffi_symbol(name)
|
|
146
|
+
(@ffi_libraries || []).each do |library|
|
|
147
|
+
return library[name.to_s]
|
|
148
|
+
rescue Fiddle::DLError
|
|
149
|
+
next
|
|
150
|
+
end
|
|
151
|
+
Fiddle::Handle::DEFAULT[name.to_s]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Module.include(Badline::FFI::DSL)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# Opens the window, then runs the machine a frame at a time: poll
|
|
6
|
+
# events, clock the frame's cycles, queue the SID's samples, upload the
|
|
7
|
+
# changed lines, present and wait. Pacer decides the cycles and the wait.
|
|
8
|
+
class App
|
|
9
|
+
SCALE = 2
|
|
10
|
+
TITLE = "Badline"
|
|
11
|
+
STAGES = %w[events emulate audio blit present wait].freeze
|
|
12
|
+
|
|
13
|
+
# Takes the frame limit, the pacing, the snapshot path, the sound and
|
|
14
|
+
# the verbosity from Options, and runs the timeline's events.
|
|
15
|
+
def initialize(computer, options, timeline)
|
|
16
|
+
@computer = computer
|
|
17
|
+
@frame_limit = options.frames
|
|
18
|
+
@verbose = options.verbose?
|
|
19
|
+
@pacer = Pacer.new(paced: options.paced?, vsync: options.vsync?, verbose: @verbose, region: computer.region)
|
|
20
|
+
@timeline = timeline
|
|
21
|
+
@snapshots = Snapshots.new(computer, options)
|
|
22
|
+
@screen = Screen.new(computer.vic)
|
|
23
|
+
@led = DriveLed.for(computer)
|
|
24
|
+
@controls = Controls.new(computer)
|
|
25
|
+
@spent = Array.new(STAGES.size, 0.0)
|
|
26
|
+
@slowest = 0.0
|
|
27
|
+
open_window
|
|
28
|
+
@sound = Sound.new(computer.sid, options.sound?, @verbose)
|
|
29
|
+
@gamepads = Gamepads.new(computer, @verbose)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
@frames = 0
|
|
34
|
+
@running = true
|
|
35
|
+
@started = @reported = now
|
|
36
|
+
@pacer.start(@started)
|
|
37
|
+
@reported_samples = 0
|
|
38
|
+
frame while @running
|
|
39
|
+
@snapshots.finish
|
|
40
|
+
@computer.drive1541&.flush
|
|
41
|
+
@gamepads.close
|
|
42
|
+
@sound.close
|
|
43
|
+
close_window
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def frame
|
|
49
|
+
stamps = [now]
|
|
50
|
+
handle_events
|
|
51
|
+
@gamepads.poll
|
|
52
|
+
stamps << now
|
|
53
|
+
emulate
|
|
54
|
+
stamps << now
|
|
55
|
+
@sound.feed
|
|
56
|
+
stamps << now
|
|
57
|
+
upload
|
|
58
|
+
stamps << now
|
|
59
|
+
draw
|
|
60
|
+
stamps << now
|
|
61
|
+
@pacer.wait(@sound)
|
|
62
|
+
stamps << now
|
|
63
|
+
finish_frame(stamps)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def finish_frame(stamps)
|
|
67
|
+
STAGES.size.times { |stage| @spent[stage] += stamps[stage + 1] - stamps[stage] }
|
|
68
|
+
took = stamps[-2] - stamps.first
|
|
69
|
+
@slowest = took if took > @slowest
|
|
70
|
+
@frames += 1
|
|
71
|
+
@timeline.run(@computer, @frames)
|
|
72
|
+
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
73
|
+
@pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
|
|
74
|
+
report(stamps.last) if (@frames % 50).zero?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def open_window
|
|
78
|
+
abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
|
|
79
|
+
|
|
80
|
+
SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
|
|
81
|
+
SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
|
|
82
|
+
@window = SDL.SDL_CreateWindow(
|
|
83
|
+
TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED,
|
|
84
|
+
Screen::WIDTH * SCALE, Screen::HEIGHT * SCALE, SDL::WINDOW_RESIZABLE
|
|
85
|
+
)
|
|
86
|
+
flags = SDL::RENDERER_ACCELERATED
|
|
87
|
+
flags |= SDL::RENDERER_PRESENTVSYNC if @pacer.vsync?
|
|
88
|
+
@renderer = SDL.SDL_CreateRenderer(@window, -1, flags)
|
|
89
|
+
fit_display if @pacer.vsync?
|
|
90
|
+
SDL.SDL_RenderSetLogicalSize(@renderer, Screen::WIDTH, Screen::HEIGHT)
|
|
91
|
+
create_texture
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def create_texture
|
|
95
|
+
@texture = SDL.SDL_CreateTexture(
|
|
96
|
+
@renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, Screen::WIDTH, Screen::HEIGHT
|
|
97
|
+
)
|
|
98
|
+
SDL.rect_w(SDL.rect, Screen::WIDTH)
|
|
99
|
+
SDL.rect_h(SDL.rect, Screen::HEIGHT)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def close_window
|
|
103
|
+
SDL.SDL_DestroyTexture(@texture)
|
|
104
|
+
SDL.SDL_DestroyRenderer(@renderer)
|
|
105
|
+
SDL.SDL_DestroyWindow(@window)
|
|
106
|
+
SDL.SDL_Quit
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def handle_events
|
|
110
|
+
handle_event(SDL.event_type(SDL.event)) while SDL.SDL_PollEvent(SDL.event) != 0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle_event(type)
|
|
114
|
+
case type
|
|
115
|
+
when SDL::QUIT
|
|
116
|
+
@running = false
|
|
117
|
+
when SDL::KEYDOWN, SDL::KEYUP
|
|
118
|
+
handle_key(SDL.event_scancode(SDL.event), type == SDL::KEYDOWN) if SDL.event_repeat(SDL.event).zero?
|
|
119
|
+
when SDL::MOUSEMOTION
|
|
120
|
+
@controls.mouse_motion(SDL.event_xrel(SDL.event), SDL.event_yrel(SDL.event))
|
|
121
|
+
when SDL::MOUSEBUTTONDOWN, SDL::MOUSEBUTTONUP
|
|
122
|
+
@controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
|
|
123
|
+
when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
|
|
124
|
+
@gamepads.rescan
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handle_key(scancode, down)
|
|
129
|
+
if [Keys::TAB, Keys::F9, Keys::F10].include?(scancode)
|
|
130
|
+
handle_toggle(scancode) if down
|
|
131
|
+
elsif down && @snapshots.key(scancode)
|
|
132
|
+
swap(@snapshots.computer)
|
|
133
|
+
elsif !Snapshots::KEYS.include?(scancode)
|
|
134
|
+
@controls.key(scancode, down)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Runs the restored machine in place of the one before: the screen,
|
|
139
|
+
# the drive LED, the controls, the gamepads and the sound go over to
|
|
140
|
+
# it.
|
|
141
|
+
def swap(computer)
|
|
142
|
+
@computer = computer
|
|
143
|
+
@screen = Screen.new(computer.vic)
|
|
144
|
+
@led = DriveLed.for(computer)
|
|
145
|
+
@controls.computer = computer
|
|
146
|
+
@gamepads.computer = computer
|
|
147
|
+
@sound.sid = computer.sid
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def handle_toggle(scancode)
|
|
151
|
+
if scancode == Keys::TAB
|
|
152
|
+
@controls.cycle_mode(SDL.event_mod(SDL.event).anybits?(SDL::KMOD_SHIFT) ? -1 : 1)
|
|
153
|
+
SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
|
|
154
|
+
elsif scancode == Keys::F9
|
|
155
|
+
@controls.swap_ports
|
|
156
|
+
else
|
|
157
|
+
@sound.toggle_mute
|
|
158
|
+
end
|
|
159
|
+
update_title
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def update_title
|
|
163
|
+
title = TITLE
|
|
164
|
+
title += " [#{@controls.tag}]" unless @controls.tag.empty?
|
|
165
|
+
title += " [MUTED]" if @sound.muted?
|
|
166
|
+
SDL.SDL_SetWindowTitle(@window, title)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def fit_display
|
|
170
|
+
SDL.SDL_GetWindowDisplayMode(@window, SDL.display_mode)
|
|
171
|
+
@pacer.fit(SDL.mode_refresh(SDL.display_mode))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def emulate
|
|
175
|
+
computer = @computer
|
|
176
|
+
cycles = @pacer.cycles(@sound)
|
|
177
|
+
i = 0
|
|
178
|
+
while i < cycles
|
|
179
|
+
computer.cycle!
|
|
180
|
+
i += 1
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def upload
|
|
185
|
+
@screen.update
|
|
186
|
+
SDL.SDL_UpdateTexture(@texture, SDL.rect, @screen.pixels, Screen::ROW_BYTES)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def draw
|
|
190
|
+
SDL.SDL_RenderClear(@renderer)
|
|
191
|
+
SDL.SDL_RenderCopy(@renderer, @texture, SDL.rect, SDL.rect)
|
|
192
|
+
@led&.draw(@renderer)
|
|
193
|
+
@timeline.screenshots(@frames + 1).each { |path| Screenshot.write(@renderer, path) }
|
|
194
|
+
SDL.SDL_RenderPresent(@renderer)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def report(at)
|
|
198
|
+
@pacer.check(50, at - @reported, at)
|
|
199
|
+
@pacer.measure(50, at - @reported)
|
|
200
|
+
report_frames(at) if @verbose
|
|
201
|
+
@spent = Array.new(STAGES.size, 0.0)
|
|
202
|
+
@slowest = 0.0
|
|
203
|
+
@reported = at
|
|
204
|
+
end
|
|
205
|
+
|
|
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
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The host's audio device for --headless, fed through SDL's queue: mono
|
|
6
|
+
# signed 16-bit samples go in at the device's rate and SDL plays them
|
|
7
|
+
# out behind us. It answers what Audio::Playback asks of a sink.
|
|
8
|
+
class AudioSink
|
|
9
|
+
class Error < Audio::Playback::DeviceError; end
|
|
10
|
+
|
|
11
|
+
attr_reader :rate
|
|
12
|
+
|
|
13
|
+
# With `exact_rate` false SDL may pick the device's own rate instead,
|
|
14
|
+
# which #rate then reports.
|
|
15
|
+
def initialize(rate:, exact_rate: false, buffer: 1024)
|
|
16
|
+
@device = 0
|
|
17
|
+
@rate = rate
|
|
18
|
+
@buffer = IO::Buffer.new(4096)
|
|
19
|
+
check(SDL.SDL_InitSubSystem(SDL::INIT_AUDIO))
|
|
20
|
+
open_device(rate, exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE, buffer)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def queue(samples)
|
|
24
|
+
count = samples.size
|
|
25
|
+
bytes = count * 2
|
|
26
|
+
@buffer.resize(bytes) if bytes > @buffer.size
|
|
27
|
+
buffer = @buffer
|
|
28
|
+
i = 0
|
|
29
|
+
while i < count
|
|
30
|
+
buffer.set_value(:s16, i * 2, samples[i])
|
|
31
|
+
i += 1
|
|
32
|
+
end
|
|
33
|
+
check(SDL.SDL_QueueAudio(@device, buffer, bytes))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
|
|
37
|
+
|
|
38
|
+
def start = SDL.SDL_PauseAudioDevice(@device, 0)
|
|
39
|
+
|
|
40
|
+
def pause = SDL.SDL_PauseAudioDevice(@device, 1)
|
|
41
|
+
|
|
42
|
+
def clear = SDL.SDL_ClearQueuedAudio(@device)
|
|
43
|
+
|
|
44
|
+
def close
|
|
45
|
+
return if @device.zero?
|
|
46
|
+
|
|
47
|
+
SDL.SDL_CloseAudioDevice(@device)
|
|
48
|
+
SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
|
|
49
|
+
@device = 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def open_device(rate, allowed_changes, buffer)
|
|
55
|
+
SDL.spec_freq(SDL.wanted, rate)
|
|
56
|
+
SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
|
|
57
|
+
SDL.spec_channels(SDL.wanted, 1)
|
|
58
|
+
SDL.spec_samples(SDL.wanted, buffer)
|
|
59
|
+
@device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
|
|
60
|
+
raise Error, SDL.SDL_GetError if @device.zero?
|
|
61
|
+
|
|
62
|
+
@rate = SDL.read_i32(SDL.obtained)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check(result)
|
|
66
|
+
raise Error, SDL.SDL_GetError if result.negative?
|
|
67
|
+
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|