badline 0.3.0 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +45 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +111 -61
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +275 -33
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +22 -10
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +2 -2
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge.rb +1 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +15 -8
- data/lib/badline/computer.rb +28 -13
- data/lib/badline/gui/application.rb +33 -21
- 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 +3 -0
- data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
- 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 +51 -51
- data/lib/badline/kernal_trap/file.rb +6 -9
- data/lib/badline/kernal_trap/save.rb +9 -4
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +42 -11
- 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/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +1 -1
- data/lib/badline/sid.rb +3 -1
- 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 +27 -4
- data/lib/badline/storage/host_directory.rb +2 -1
- data/lib/badline/storage.rb +15 -0
- data/lib/badline/time_of_day.rb +4 -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 +42 -7
- data/lib/badline/vic/registers.rb +3 -0
- 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 +93 -44
- data/lib/badline.rb +4 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +30 -20
- data/exe/badline +0 -78
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -3,13 +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
|
-
MUTE_SYM =
|
|
10
|
-
REVERSE_MOD =
|
|
7
|
+
TOGGLE_SYM = SDL::KEY_TAB
|
|
8
|
+
MUTE_SYM = SDL::KEY_F10
|
|
9
|
+
REVERSE_MOD = SDL::KMOD_SHIFT
|
|
11
10
|
|
|
12
|
-
SHARED_KEYS = %i[
|
|
11
|
+
SHARED_KEYS = %i[cursor_up cursor_left cursor_h cursor_v space w a s d lshift].freeze
|
|
13
12
|
|
|
14
13
|
# Tab steps through the input modes and shift-Tab back; the title bar
|
|
15
14
|
# names the live one. The pot devices appear once per control port, since
|
|
@@ -29,9 +28,10 @@ module Badline
|
|
|
29
28
|
}.freeze
|
|
30
29
|
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
# The media options (autostart:, song:, disk:) go to Media.attach.
|
|
32
|
+
def initialize(media_path: nil, sid_model: nil, sound: false, **media)
|
|
33
33
|
@computer = Computer.new(sid_model: sid_model || Media.sid_model(media_path))
|
|
34
|
-
puts Media.attach(@computer, media_path,
|
|
34
|
+
puts Media.attach(@computer, media_path, **media) if media_path
|
|
35
35
|
|
|
36
36
|
@mode = :keyboard
|
|
37
37
|
@pot_device = nil
|
|
@@ -61,25 +61,26 @@ module Badline
|
|
|
61
61
|
ensure
|
|
62
62
|
@stream&.close
|
|
63
63
|
@gamepads.close
|
|
64
|
+
@window.close
|
|
64
65
|
puts @computer.cpu.inspect
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
70
|
def handle_events
|
|
70
|
-
while (event =
|
|
71
|
+
while (event = SDL.poll_event)
|
|
71
72
|
case event
|
|
72
|
-
when
|
|
73
|
+
when SDL::Quit
|
|
73
74
|
@running = false
|
|
74
|
-
when
|
|
75
|
+
when SDL::KeyDown
|
|
75
76
|
handle_key_down(event)
|
|
76
|
-
when
|
|
77
|
+
when SDL::KeyUp
|
|
77
78
|
handle_key_up(event)
|
|
78
|
-
when
|
|
79
|
+
when SDL::MouseMotion
|
|
79
80
|
@pot_device&.move(event.xrel, event.yrel)
|
|
80
|
-
when
|
|
81
|
+
when SDL::MouseButton
|
|
81
82
|
handle_mouse_button(event)
|
|
82
|
-
when
|
|
83
|
+
when SDL::ControllerDevice
|
|
83
84
|
@gamepads.rescan
|
|
84
85
|
end
|
|
85
86
|
end
|
|
@@ -93,10 +94,19 @@ module Badline
|
|
|
93
94
|
if port
|
|
94
95
|
joystick(port).press(dir)
|
|
95
96
|
else
|
|
96
|
-
|
|
97
|
+
press_key(KeyMap.parse(event), event.repeat)
|
|
97
98
|
end
|
|
98
99
|
end
|
|
99
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
|
+
|
|
100
110
|
def handle_key_up(event)
|
|
101
111
|
return if [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
|
|
102
112
|
|
|
@@ -112,9 +122,10 @@ module Badline
|
|
|
112
122
|
button = MOUSE_BUTTONS[event.button]
|
|
113
123
|
return unless button && @pot_device
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
if event.pressed
|
|
126
|
+
@pot_device.press(button)
|
|
127
|
+
else
|
|
128
|
+
@pot_device.release(button)
|
|
118
129
|
end
|
|
119
130
|
end
|
|
120
131
|
|
|
@@ -153,8 +164,9 @@ module Badline
|
|
|
153
164
|
|
|
154
165
|
def fit_frame
|
|
155
166
|
rate = @window.refresh_rate
|
|
156
|
-
|
|
157
|
-
@
|
|
167
|
+
clock_hz = @computer.region.clock_hz
|
|
168
|
+
@cycles_per_frame = clock_hz / rate
|
|
169
|
+
@frame_seconds = @cycles_per_frame.fdiv(clock_hz)
|
|
158
170
|
puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
|
|
159
171
|
end
|
|
160
172
|
|
|
@@ -164,7 +176,7 @@ module Badline
|
|
|
164
176
|
ports = @computer.control_ports
|
|
165
177
|
ports.device1 = port == 1 ? @pot_device : nil
|
|
166
178
|
ports.device2 = port == 2 ? @pot_device : nil
|
|
167
|
-
|
|
179
|
+
SDL::SetRelativeMouseMode.call(@pot_device ? 1 : 0)
|
|
168
180
|
end
|
|
169
181
|
|
|
170
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
|
|
@@ -19,14 +19,43 @@ module Badline
|
|
|
19
19
|
report_block_error(track, sector)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
22
|
+
# A block write puts the buffer's block on the disk. On a
|
|
23
|
+
# write-protected disk it fails at the block it names.
|
|
24
24
|
def block_write(arguments, counted: false)
|
|
25
25
|
channel, _drive, track, sector = arguments
|
|
26
26
|
return unless fetch_block(channel, track, sector)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
buffer = @channels[channel]
|
|
29
|
+
store_count(buffer) if counted
|
|
30
|
+
return report(WRITE_PROTECT_ON, track, sector) unless writable_disk?
|
|
31
|
+
|
|
32
|
+
writing { @storage.write_block(track, sector, buffer.block) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# B-A marks a block in use. One already in use fails as NO BLOCK,
|
|
36
|
+
# naming the next free one.
|
|
37
|
+
def block_allocate(arguments)
|
|
38
|
+
_drive, track, sector = arguments
|
|
39
|
+
return unless bam_entry(track, sector)
|
|
40
|
+
return report(NO_BLOCK, *@storage.next_free_block(track, sector)) unless @storage.block_free?(track, sector)
|
|
41
|
+
|
|
42
|
+
writing { @storage.allocate_block(track, sector) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# B-F marks a block free.
|
|
46
|
+
def block_free(arguments)
|
|
47
|
+
_drive, track, sector = arguments
|
|
48
|
+
writing { @storage.free_block(track, sector) } if bam_entry(track, sector)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A BAM change needs a disk that takes writes and a block the BAM
|
|
52
|
+
# has an entry for. Returns whether both hold, once it has reported
|
|
53
|
+
# why not.
|
|
54
|
+
def bam_entry(track, sector)
|
|
55
|
+
return report(WRITE_PROTECT_ON) unless writable_disk?
|
|
56
|
+
return true if sector && @storage.bam_block?(track, sector)
|
|
57
|
+
|
|
58
|
+
report(ILLEGAL_TRACK_OR_SECTOR, track, sector)
|
|
30
59
|
end
|
|
31
60
|
|
|
32
61
|
# B-W first stores the index of the last byte written, the pointer
|
|
@@ -9,6 +9,7 @@ module Badline
|
|
|
9
9
|
class Status
|
|
10
10
|
MESSAGES = [20, 21, 22, 23, 24, 27].to_h { |code| [code, "READ ERROR"] }.merge(
|
|
11
11
|
0 => " OK",
|
|
12
|
+
1 => " FILES SCRATCHED",
|
|
12
13
|
25 => "WRITE ERROR",
|
|
13
14
|
26 => "WRITE PROTECT ON",
|
|
14
15
|
28 => "WRITE ERROR",
|
|
@@ -16,8 +17,10 @@ module Badline
|
|
|
16
17
|
30 => "SYNTAX ERROR",
|
|
17
18
|
62 => "FILE NOT FOUND",
|
|
18
19
|
63 => "FILE EXISTS",
|
|
20
|
+
65 => "NO BLOCK",
|
|
19
21
|
66 => "ILLEGAL TRACK OR SECTOR",
|
|
20
22
|
70 => "NO CHANNEL",
|
|
23
|
+
72 => "DISK FULL",
|
|
21
24
|
73 => "CBM DOS V2.6 1541",
|
|
22
25
|
74 => "DRIVE NOT READY"
|
|
23
26
|
).freeze
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# A channel open for writing a file. It gathers the bytes sent to it,
|
|
7
|
+
# and the drive writes the file to the disk when the channel closes.
|
|
8
|
+
# An append starts from the file's end, a replace writes over the
|
|
9
|
+
# file of the same name.
|
|
10
|
+
class WriteFile
|
|
11
|
+
attr_accessor :buffer
|
|
12
|
+
attr_reader :name, :type, :mode
|
|
13
|
+
|
|
14
|
+
def initialize(name, type, mode: :write)
|
|
15
|
+
@name = name
|
|
16
|
+
@type = type
|
|
17
|
+
@mode = mode
|
|
18
|
+
@bytes = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(bytes) = @bytes.concat(bytes)
|
|
22
|
+
|
|
23
|
+
def writable? = true
|
|
24
|
+
|
|
25
|
+
def failed? = false
|
|
26
|
+
|
|
27
|
+
# A file open for writing has nothing to send.
|
|
28
|
+
def read = nil
|
|
29
|
+
|
|
30
|
+
# Writes the gathered bytes to the storage.
|
|
31
|
+
def store(storage)
|
|
32
|
+
if @mode == :append
|
|
33
|
+
storage.append_file(@name, @bytes, type: @type)
|
|
34
|
+
else
|
|
35
|
+
storage.write_file(@name, @bytes, type: @type, replace: @mode == :replace)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|