badline 0.2.1 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +107 -0
- data/CLAUDE.md +51 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +119 -51
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +293 -39
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +28 -14
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +3 -2
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge/mach5.rb +1 -1
- data/lib/badline/cartridge/magic_desk.rb +5 -1
- data/lib/badline/cartridge/ocean.rb +5 -2
- data/lib/badline/cartridge/rex_utility.rb +1 -1
- data/lib/badline/cartridge/rgcd.rb +1 -1
- data/lib/badline/cartridge/simons_basic.rb +2 -2
- data/lib/badline/cartridge/westermann.rb +2 -2
- data/lib/badline/cartridge.rb +9 -0
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +19 -10
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +34 -14
- data/lib/badline/cpu.rb +2 -2
- data/lib/badline/gui/application.rb +70 -26
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +102 -7
- data/lib/badline/kernal_trap/drive/block_commands.rb +108 -0
- data/lib/badline/kernal_trap/drive/channels.rb +53 -0
- data/lib/badline/kernal_trap/drive/memory.rb +109 -0
- data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
- data/lib/badline/kernal_trap/drive/status.rb +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +109 -131
- data/lib/badline/kernal_trap/file.rb +6 -14
- data/lib/badline/kernal_trap/load.rb +6 -0
- data/lib/badline/kernal_trap/save.rb +13 -7
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +44 -13
- data/lib/badline/memory.rb +2 -2
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/rom.rb +14 -5
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/catch_up.rb +110 -0
- data/lib/badline/sid/decimator.rb +6 -1
- data/lib/badline/sid/filter.rb +5 -5
- data/lib/badline/sid/waveform/fast_forward.rb +10 -10
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +8 -8
- data/lib/badline/sid.rb +19 -93
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +49 -4
- data/lib/badline/storage/host_directory.rb +17 -4
- data/lib/badline/storage/sid_file.rb +36 -2
- data/lib/badline/storage.rb +18 -1
- data/lib/badline/time_of_day.rb +11 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +49 -8
- data/lib/badline/vic/registers.rb +19 -27
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +98 -46
- data/lib/badline.rb +5 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +37 -20
- data/exe/badline +0 -75
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module GUI
|
|
5
5
|
class Application
|
|
6
|
-
PAL_CLOCK_HZ = 985_248
|
|
7
6
|
TITLE = "Badline"
|
|
8
|
-
TOGGLE_SYM =
|
|
9
|
-
|
|
7
|
+
TOGGLE_SYM = SDL::KEY_TAB
|
|
8
|
+
MUTE_SYM = SDL::KEY_F10
|
|
9
|
+
REVERSE_MOD = SDL::KMOD_SHIFT
|
|
10
10
|
|
|
11
|
-
SHARED_KEYS = %i[
|
|
11
|
+
SHARED_KEYS = %i[cursor_up cursor_left cursor_h cursor_v space w a s d lshift].freeze
|
|
12
12
|
|
|
13
13
|
# Tab steps through the input modes and shift-Tab back; the title bar
|
|
14
14
|
# names the live one. The pot devices appear once per control port, since
|
|
@@ -28,24 +28,24 @@ module Badline
|
|
|
28
28
|
}.freeze
|
|
29
29
|
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# The media options (autostart:, song:, disk:) go to Media.attach.
|
|
32
|
+
def initialize(media_path: nil, sid_model: nil, sound: false, **media)
|
|
33
|
+
@computer = Computer.new(sid_model: sid_model || Media.sid_model(media_path))
|
|
34
|
+
puts Media.attach(@computer, media_path, **media) if media_path
|
|
34
35
|
|
|
35
36
|
@mode = :keyboard
|
|
36
37
|
@pot_device = nil
|
|
37
38
|
@panes = [ScreenPane.new(@computer)]
|
|
39
|
+
@stream = open_stream if sound
|
|
40
|
+
@paced = ENV["NOVSYNC"].nil?
|
|
38
41
|
@window = Window.new(
|
|
39
42
|
title: TITLE,
|
|
40
43
|
width: canvas_width, height: canvas_height,
|
|
41
|
-
vsync:
|
|
44
|
+
vsync: @paced && !@stream
|
|
42
45
|
)
|
|
43
46
|
@gamepads = Gamepads.new(@computer)
|
|
44
47
|
@gamepads.names.each { |name| puts "Gamepad: #{name}" }
|
|
45
|
-
|
|
46
|
-
rate = @window.refresh_rate
|
|
47
|
-
@cycles_per_frame = PAL_CLOCK_HZ / rate
|
|
48
|
-
puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
|
|
48
|
+
fit_frame
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def run
|
|
@@ -54,29 +54,33 @@ module Badline
|
|
|
54
54
|
handle_events
|
|
55
55
|
@gamepads.poll
|
|
56
56
|
@cycles_per_frame.times { @computer.cycle! }
|
|
57
|
+
@stream&.feed
|
|
57
58
|
@window.draw(@panes)
|
|
59
|
+
@stream.pace(@frame_seconds) if @stream && @paced
|
|
58
60
|
end
|
|
59
61
|
ensure
|
|
62
|
+
@stream&.close
|
|
60
63
|
@gamepads.close
|
|
64
|
+
@window.close
|
|
61
65
|
puts @computer.cpu.inspect
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
private
|
|
65
69
|
|
|
66
70
|
def handle_events
|
|
67
|
-
while (event =
|
|
71
|
+
while (event = SDL.poll_event)
|
|
68
72
|
case event
|
|
69
|
-
when
|
|
73
|
+
when SDL::Quit
|
|
70
74
|
@running = false
|
|
71
|
-
when
|
|
75
|
+
when SDL::KeyDown
|
|
72
76
|
handle_key_down(event)
|
|
73
|
-
when
|
|
77
|
+
when SDL::KeyUp
|
|
74
78
|
handle_key_up(event)
|
|
75
|
-
when
|
|
79
|
+
when SDL::MouseMotion
|
|
76
80
|
@pot_device&.move(event.xrel, event.yrel)
|
|
77
|
-
when
|
|
81
|
+
when SDL::MouseButton
|
|
78
82
|
handle_mouse_button(event)
|
|
79
|
-
when
|
|
83
|
+
when SDL::ControllerDevice
|
|
80
84
|
@gamepads.rescan
|
|
81
85
|
end
|
|
82
86
|
end
|
|
@@ -84,17 +88,27 @@ module Badline
|
|
|
84
88
|
|
|
85
89
|
def handle_key_down(event)
|
|
86
90
|
return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
|
|
91
|
+
return toggle_mute if event.sym == MUTE_SYM
|
|
87
92
|
|
|
88
93
|
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
89
94
|
if port
|
|
90
95
|
joystick(port).press(dir)
|
|
91
96
|
else
|
|
92
|
-
|
|
97
|
+
press_key(KeyMap.parse(event), event.repeat)
|
|
93
98
|
end
|
|
94
99
|
end
|
|
95
100
|
|
|
101
|
+
# RESTORE isn't in the key matrix, so it goes to the machine instead,
|
|
102
|
+
# once per press as the real key's one-shot fires.
|
|
103
|
+
def press_key(key, repeat)
|
|
104
|
+
return @computer.press_restore if key == :restore && !repeat
|
|
105
|
+
return if key == :restore
|
|
106
|
+
|
|
107
|
+
@computer.keyboard.press(key)
|
|
108
|
+
end
|
|
109
|
+
|
|
96
110
|
def handle_key_up(event)
|
|
97
|
-
return if event.sym
|
|
111
|
+
return if [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
|
|
98
112
|
|
|
99
113
|
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
100
114
|
if port
|
|
@@ -108,9 +122,10 @@ module Badline
|
|
|
108
122
|
button = MOUSE_BUTTONS[event.button]
|
|
109
123
|
return unless button && @pot_device
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
if event.pressed
|
|
126
|
+
@pot_device.press(button)
|
|
127
|
+
else
|
|
128
|
+
@pot_device.release(button)
|
|
114
129
|
end
|
|
115
130
|
end
|
|
116
131
|
|
|
@@ -123,7 +138,36 @@ module Badline
|
|
|
123
138
|
@mode = modes[(modes.index(@mode) + step) % modes.size]
|
|
124
139
|
release_inputs
|
|
125
140
|
attach_pot_device
|
|
126
|
-
|
|
141
|
+
update_title
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def toggle_mute
|
|
145
|
+
return unless @stream
|
|
146
|
+
|
|
147
|
+
@stream.toggle_mute
|
|
148
|
+
update_title
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def update_title
|
|
152
|
+
tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
|
|
153
|
+
@window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def open_stream
|
|
157
|
+
sink = Audio::SDLSink.new(rate: Audio::Renderer::DEFAULT_RATE)
|
|
158
|
+
puts "Sound at #{sink.rate} Hz, F10 mutes"
|
|
159
|
+
Audio::Stream.new(sink, @computer.sid,
|
|
160
|
+
on_underrun: -> { puts "Running below real time, so the sound will stutter." })
|
|
161
|
+
rescue Audio::SDLSink::Error => e
|
|
162
|
+
warn "badline: no sound, can't open the audio device: #{e.message}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def fit_frame
|
|
166
|
+
rate = @window.refresh_rate
|
|
167
|
+
clock_hz = @computer.region.clock_hz
|
|
168
|
+
@cycles_per_frame = clock_hz / rate
|
|
169
|
+
@frame_seconds = @cycles_per_frame.fdiv(clock_hz)
|
|
170
|
+
puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
|
|
127
171
|
end
|
|
128
172
|
|
|
129
173
|
def attach_pot_device
|
|
@@ -132,7 +176,7 @@ module Badline
|
|
|
132
176
|
ports = @computer.control_ports
|
|
133
177
|
ports.device1 = port == 1 ? @pot_device : nil
|
|
134
178
|
ports.device2 = port == 2 ? @pot_device : nil
|
|
135
|
-
|
|
179
|
+
SDL::SetRelativeMouseMode.call(@pot_device ? 1 : 0)
|
|
136
180
|
end
|
|
137
181
|
|
|
138
182
|
def release_inputs
|
data/lib/badline/gui/gamepads.rb
CHANGED
|
@@ -9,8 +9,8 @@ module Badline
|
|
|
9
9
|
# face and shoulder button fires.
|
|
10
10
|
#
|
|
11
11
|
# SDL announces arrivals by device index but reports button presses by
|
|
12
|
-
# instance id,
|
|
13
|
-
#
|
|
12
|
+
# instance id, so rather than line the two up, this class polls the
|
|
13
|
+
# controllers every frame. It only releases directions it
|
|
14
14
|
# pressed itself, so the keyboard mapping keeps working with a pad
|
|
15
15
|
# connected.
|
|
16
16
|
class Gamepads
|
|
@@ -18,39 +18,39 @@ module Badline
|
|
|
18
18
|
PORTS = [2, 1].freeze
|
|
19
19
|
|
|
20
20
|
BUTTONS = {
|
|
21
|
-
up: [
|
|
22
|
-
down: [
|
|
23
|
-
left: [
|
|
24
|
-
right: [
|
|
25
|
-
fire: [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
SDL2::GameController::Button::RIGHTSHOULDER]
|
|
21
|
+
up: [SDL::CONTROLLER_BUTTON_DPAD_UP],
|
|
22
|
+
down: [SDL::CONTROLLER_BUTTON_DPAD_DOWN],
|
|
23
|
+
left: [SDL::CONTROLLER_BUTTON_DPAD_LEFT],
|
|
24
|
+
right: [SDL::CONTROLLER_BUTTON_DPAD_RIGHT],
|
|
25
|
+
fire: [SDL::CONTROLLER_BUTTON_A, SDL::CONTROLLER_BUTTON_B,
|
|
26
|
+
SDL::CONTROLLER_BUTTON_X, SDL::CONTROLLER_BUTTON_Y,
|
|
27
|
+
SDL::CONTROLLER_BUTTON_LEFTSHOULDER, SDL::CONTROLLER_BUTTON_RIGHTSHOULDER]
|
|
29
28
|
}.freeze
|
|
30
29
|
|
|
31
30
|
AXES = {
|
|
32
|
-
up: [
|
|
33
|
-
down: [
|
|
34
|
-
left: [
|
|
35
|
-
right: [
|
|
31
|
+
up: [SDL::CONTROLLER_AXIS_LEFTY, -1],
|
|
32
|
+
down: [SDL::CONTROLLER_AXIS_LEFTY, 1],
|
|
33
|
+
left: [SDL::CONTROLLER_AXIS_LEFTX, -1],
|
|
34
|
+
right: [SDL::CONTROLLER_AXIS_LEFTX, 1]
|
|
36
35
|
}.freeze
|
|
37
36
|
|
|
38
37
|
def initialize(computer)
|
|
39
|
-
|
|
38
|
+
SDL.check(SDL::InitSubSystem.call(SDL::INIT_GAMECONTROLLER))
|
|
40
39
|
@computer = computer
|
|
41
40
|
@controllers = []
|
|
42
41
|
@pressed = Array.new(PORTS.size) { [] }
|
|
43
42
|
rescan
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
def names = @controllers.map(
|
|
45
|
+
def names = @controllers.map { |controller| SDL::GameControllerName.call(controller) }
|
|
47
46
|
|
|
48
47
|
def rescan
|
|
49
48
|
close
|
|
50
|
-
@controllers = (0...
|
|
51
|
-
.select { |index|
|
|
49
|
+
@controllers = (0...SDL::NumJoysticks.call)
|
|
50
|
+
.select { |index| SDL::IsGameController.call(index) == 1 }
|
|
52
51
|
.first(PORTS.size)
|
|
53
|
-
.map { |index|
|
|
52
|
+
.map { |index| SDL::GameControllerOpen.call(index) }
|
|
53
|
+
.reject(&:null?)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def poll
|
|
@@ -58,7 +58,7 @@ module Badline
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def close
|
|
61
|
-
@controllers.each { |controller|
|
|
61
|
+
@controllers.each { |controller| SDL::GameControllerClose.call(controller) }
|
|
62
62
|
@controllers = []
|
|
63
63
|
@pressed.size.times { |index| release(index) }
|
|
64
64
|
end
|
|
@@ -78,7 +78,7 @@ module Badline
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def active?(controller, direction)
|
|
81
|
-
BUTTONS.fetch(direction).any? { |button|
|
|
81
|
+
BUTTONS.fetch(direction).any? { |button| SDL::GameControllerGetButton.call(controller, button) == 1 } ||
|
|
82
82
|
tilted?(controller, direction)
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -86,7 +86,7 @@ module Badline
|
|
|
86
86
|
axis, sign = AXES[direction]
|
|
87
87
|
return false unless axis
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
SDL::GameControllerGetAxis.call(controller, axis) * sign > DEADZONE
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def joystick(index)
|
data/lib/badline/gui/joy_map.rb
CHANGED
data/lib/badline/gui/key_map.rb
CHANGED
|
@@ -11,6 +11,11 @@ module Badline
|
|
|
11
11
|
"Return" => :return,
|
|
12
12
|
"Right" => :cursor_h,
|
|
13
13
|
"Down" => :cursor_v,
|
|
14
|
+
"Left" => :cursor_left,
|
|
15
|
+
"Up" => :cursor_up,
|
|
16
|
+
"`" => :left,
|
|
17
|
+
"]" => :up,
|
|
18
|
+
"PageUp" => :restore,
|
|
14
19
|
"Space" => :space,
|
|
15
20
|
"Left Ctrl" => :control,
|
|
16
21
|
"Left Alt" => :cbm,
|
|
@@ -26,7 +31,7 @@ module Badline
|
|
|
26
31
|
}.freeze
|
|
27
32
|
|
|
28
33
|
def self.parse(event)
|
|
29
|
-
name =
|
|
34
|
+
name = SDL.key_name(event.sym)
|
|
30
35
|
MAP[name] || name.downcase.to_sym
|
|
31
36
|
end
|
|
32
37
|
end
|
data/lib/badline/gui/pane.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Badline
|
|
|
10
10
|
@height = height
|
|
11
11
|
@left = left
|
|
12
12
|
@top = top
|
|
13
|
-
@rect =
|
|
13
|
+
@rect = [left, top, width, height].pack("l4")
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def render(_renderer)
|
|
@@ -19,16 +19,17 @@ module Badline
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
+
# The pixels are little-endian RGBA dwords, red in the low byte.
|
|
22
23
|
def blit(renderer, pixels)
|
|
23
|
-
surface =
|
|
24
|
+
surface = SDL::CreateRGBSurfaceFrom.call(
|
|
24
25
|
pixels, width, height, 32, width * 4,
|
|
25
26
|
0x0000_00ff, 0x0000_ff00, 0x00ff_0000, 0xff00_0000
|
|
26
27
|
)
|
|
27
|
-
texture =
|
|
28
|
-
|
|
28
|
+
texture = SDL::CreateTextureFromSurface.call(renderer, surface)
|
|
29
|
+
SDL::RenderCopy.call(renderer, texture, nil, @rect)
|
|
29
30
|
ensure
|
|
30
|
-
texture
|
|
31
|
-
surface
|
|
31
|
+
SDL::DestroyTexture.call(texture) if texture
|
|
32
|
+
SDL::FreeSurface.call(surface) if surface
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
end
|
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module GUI
|
|
5
|
+
# The part of the VIC's raster the machine's region crops out.
|
|
5
6
|
class ScreenPane < Pane
|
|
6
|
-
WIDTH = 384
|
|
7
|
-
HEIGHT = 272
|
|
8
|
-
COL_OFFSET = 96
|
|
9
|
-
ROW_OFFSET = 20
|
|
10
|
-
|
|
11
|
-
ROW_BYTES = WIDTH * 4
|
|
12
|
-
|
|
13
7
|
def initialize(computer, left: 0, top: 0, palette: Palette.new)
|
|
14
|
-
|
|
8
|
+
@col_offset, @row_offset, crop_width, crop_height = computer.region.crop
|
|
9
|
+
super(width: crop_width, height: crop_height, left:, top:)
|
|
15
10
|
@computer = computer
|
|
11
|
+
@row_bytes = width * 4
|
|
16
12
|
@palette = palette.dwords
|
|
17
|
-
@buffer = ("\x00" * (
|
|
13
|
+
@buffer = ("\x00" * (height * @row_bytes)).b
|
|
18
14
|
end
|
|
19
15
|
|
|
20
16
|
def render(renderer)
|
|
@@ -30,10 +26,10 @@ module Badline
|
|
|
30
26
|
vic_width = vic.width
|
|
31
27
|
dirty = vic.dirty_lines
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
next unless dirty[row +
|
|
29
|
+
height.times do |row|
|
|
30
|
+
next unless dirty[row + @row_offset]
|
|
35
31
|
|
|
36
|
-
@buffer[row *
|
|
32
|
+
@buffer[row * @row_bytes, @row_bytes] =
|
|
37
33
|
pack_row(display, vic_width, row)
|
|
38
34
|
end
|
|
39
35
|
vic.clear_dirty_lines!
|
|
@@ -41,7 +37,7 @@ module Badline
|
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
def pack_row(display, vic_width, row)
|
|
44
|
-
line = display[((row +
|
|
40
|
+
line = display[((row + @row_offset) * vic_width) + @col_offset, width]
|
|
45
41
|
line.map! { |c| @palette[c] }
|
|
46
42
|
line.pack("V*")
|
|
47
43
|
end
|
data/lib/badline/gui/window.rb
CHANGED
|
@@ -5,41 +5,56 @@ module Badline
|
|
|
5
5
|
class Window
|
|
6
6
|
DEFAULT_REFRESH_RATE = 60
|
|
7
7
|
|
|
8
|
+
# SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
|
|
9
|
+
DISPLAY_MODE_SIZE = 24
|
|
10
|
+
|
|
8
11
|
attr_reader :renderer
|
|
9
12
|
|
|
10
13
|
def initialize(title:, width:, height:, scale: 2, vsync: true)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
flags
|
|
23
|
-
|
|
24
|
-
@renderer
|
|
14
|
+
SDL.check(SDL::InitSubSystem.call(SDL::INIT_VIDEO | SDL::INIT_EVENTS))
|
|
15
|
+
SDL::SetHint.call("SDL_RENDER_SCALE_QUALITY", "0") # nearest-neighbour
|
|
16
|
+
SDL::SetHint.call("SDL_MOUSE_RELATIVE_SCALING", "0") # host pixels, whatever the window size
|
|
17
|
+
|
|
18
|
+
@window = SDL.check_pointer(SDL::CreateWindow.call(
|
|
19
|
+
title,
|
|
20
|
+
SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED,
|
|
21
|
+
width * scale, height * scale,
|
|
22
|
+
SDL::WINDOW_RESIZABLE
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
flags = SDL::RENDERER_ACCELERATED
|
|
26
|
+
flags |= SDL::RENDERER_PRESENTVSYNC if vsync
|
|
27
|
+
@renderer = SDL.check_pointer(SDL::CreateRenderer.call(@window, -1, flags))
|
|
28
|
+
SDL.check(SDL::RenderSetLogicalSize.call(@renderer, width, height))
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
def title=(title)
|
|
28
|
-
@window
|
|
32
|
+
SDL::SetWindowTitle.call(@window, title)
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def refresh_rate
|
|
32
|
-
|
|
36
|
+
mode = "\0".b * DISPLAY_MODE_SIZE
|
|
37
|
+
SDL.check(SDL::GetCurrentDisplayMode.call(0, mode))
|
|
38
|
+
rate = mode.unpack1("l", offset: 12)
|
|
33
39
|
rate.positive? ? rate : DEFAULT_REFRESH_RATE
|
|
34
|
-
rescue
|
|
40
|
+
rescue SDL::Error
|
|
35
41
|
DEFAULT_REFRESH_RATE
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def draw(panes)
|
|
39
|
-
@renderer
|
|
40
|
-
@renderer
|
|
45
|
+
SDL::SetRenderDrawColor.call(@renderer, 0, 0, 0, 255)
|
|
46
|
+
SDL::RenderClear.call(@renderer)
|
|
41
47
|
panes.each { |pane| pane.render(@renderer) }
|
|
42
|
-
@renderer
|
|
48
|
+
SDL::RenderPresent.call(@renderer)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close
|
|
52
|
+
return unless @window
|
|
53
|
+
|
|
54
|
+
SDL::DestroyRenderer.call(@renderer)
|
|
55
|
+
SDL::DestroyWindow.call(@window)
|
|
56
|
+
SDL::QuitSubSystem.call(SDL::INIT_VIDEO | SDL::INIT_EVENTS)
|
|
57
|
+
@window = nil
|
|
43
58
|
end
|
|
44
59
|
end
|
|
45
60
|
end
|
data/lib/badline/gui.rb
CHANGED
|
@@ -10,16 +10,23 @@ module Badline
|
|
|
10
10
|
# wrap, so a single reading says nothing on its own, and the driver has to
|
|
11
11
|
# sample before the mouse travels 32 counts.
|
|
12
12
|
#
|
|
13
|
-
# #move takes host deltas with y counting down the screen; the counter
|
|
14
|
-
# counts the other way.
|
|
13
|
+
# #move takes host pixel deltas with y counting down the screen; the counter
|
|
14
|
+
# counts the other way. Two host pixels make one count, as in VICE, and the
|
|
15
|
+
# counters keep the odd half count so slow movement adds up. Motion waits
|
|
16
|
+
# until the pot register is read, and at most 31 counts of it reach the
|
|
17
|
+
# counter per read, so a fast flick can't wrap the counter into the
|
|
18
|
+
# opposite direction. The left button sits on the joystick fire line, the
|
|
15
19
|
# right button on the up line.
|
|
16
20
|
class Mouse1351
|
|
17
21
|
BUTTONS = { left: :fire, right: :up }.freeze
|
|
22
|
+
MAX_TRAVEL = 62
|
|
18
23
|
|
|
19
24
|
def initialize
|
|
20
25
|
@lines = Joystick.new
|
|
21
26
|
@x = 0
|
|
22
27
|
@y = 0
|
|
28
|
+
@pending_x = 0
|
|
29
|
+
@pending_y = 0
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
def press(button) = @lines.press(BUTTONS[button])
|
|
@@ -29,13 +36,21 @@ module Badline
|
|
|
29
36
|
def port_bits = @lines.port_bits
|
|
30
37
|
|
|
31
38
|
def move(delta_x, delta_y)
|
|
32
|
-
@
|
|
33
|
-
@
|
|
39
|
+
@pending_x = (@pending_x + delta_x).clamp(-MAX_TRAVEL, MAX_TRAVEL)
|
|
40
|
+
@pending_y = (@pending_y - delta_y).clamp(-MAX_TRAVEL, MAX_TRAVEL)
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
def pot_x
|
|
43
|
+
def pot_x
|
|
44
|
+
@x = (@x + @pending_x) & 0x7f
|
|
45
|
+
@pending_x = 0
|
|
46
|
+
@x & 0x7e
|
|
47
|
+
end
|
|
37
48
|
|
|
38
|
-
def pot_y
|
|
49
|
+
def pot_y
|
|
50
|
+
@y = (@y + @pending_y) & 0x7f
|
|
51
|
+
@pending_y = 0
|
|
52
|
+
@y & 0x7e
|
|
53
|
+
end
|
|
39
54
|
end
|
|
40
55
|
end
|
|
41
56
|
end
|
|
@@ -8,17 +8,18 @@ module Badline
|
|
|
8
8
|
# on POTY with its button on the right line. Positions are the 0-255 counts
|
|
9
9
|
# SID reads back, and the knobs stop at either end.
|
|
10
10
|
#
|
|
11
|
+
# #move takes host pixel deltas, two to a count, and the knobs keep the odd
|
|
12
|
+
# half count so slow turns add up.
|
|
13
|
+
#
|
|
11
14
|
# Buttons are named after the host mouse buttons, as on the 1351: the left
|
|
12
15
|
# one fires paddle A, the right one paddle B.
|
|
13
16
|
class Paddles
|
|
14
17
|
BUTTONS = { left: :left, right: :right }.freeze
|
|
15
18
|
|
|
16
|
-
attr_reader :pot_x, :pot_y
|
|
17
|
-
|
|
18
19
|
def initialize
|
|
19
20
|
@lines = Joystick.new
|
|
20
|
-
@
|
|
21
|
-
@
|
|
21
|
+
@half_x = 0x100
|
|
22
|
+
@half_y = 0x100
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def press(button) = @lines.press(BUTTONS[button])
|
|
@@ -28,9 +29,13 @@ module Badline
|
|
|
28
29
|
def port_bits = @lines.port_bits
|
|
29
30
|
|
|
30
31
|
def move(turn_a, turn_b)
|
|
31
|
-
@
|
|
32
|
-
@
|
|
32
|
+
@half_x = (@half_x + turn_a).clamp(0, 0x1ff)
|
|
33
|
+
@half_y = (@half_y + turn_b).clamp(0, 0x1ff)
|
|
33
34
|
end
|
|
35
|
+
|
|
36
|
+
def pot_x = @half_x >> 1
|
|
37
|
+
|
|
38
|
+
def pot_y = @half_y >> 1
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
41
|
end
|