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.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +82 -10
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +20 -23
  7. data/lib/badline/address_bus/saved_state.rb +72 -0
  8. data/lib/badline/address_bus.rb +11 -1
  9. data/lib/badline/audio/cli.rb +18 -5
  10. data/lib/badline/audio/console.rb +2 -1
  11. data/lib/badline/audio.rb +0 -3
  12. data/lib/badline/cartridge/action_replay.rb +15 -0
  13. data/lib/badline/cartridge/comal80.rb +8 -0
  14. data/lib/badline/cartridge/dinamic.rb +8 -0
  15. data/lib/badline/cartridge/easy_flash.rb +20 -1
  16. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  17. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  18. data/lib/badline/cartridge/flash.rb +41 -0
  19. data/lib/badline/cartridge/fun_play.rb +8 -0
  20. data/lib/badline/cartridge/g_mod2.rb +12 -0
  21. data/lib/badline/cartridge/game_system.rb +8 -0
  22. data/lib/badline/cartridge/geo_ram.rb +16 -0
  23. data/lib/badline/cartridge/magic_desk.rb +8 -0
  24. data/lib/badline/cartridge/ocean.rb +8 -0
  25. data/lib/badline/cartridge/pagefox.rb +12 -0
  26. data/lib/badline/cartridge/retro_replay.rb +44 -1
  27. data/lib/badline/cartridge/rgcd.rb +10 -0
  28. data/lib/badline/cartridge/super_games.rb +10 -0
  29. data/lib/badline/cartridge/zaxxon.rb +16 -0
  30. data/lib/badline/cartridge.rb +72 -0
  31. data/lib/badline/cia/interrupt_register.rb +17 -0
  32. data/lib/badline/cia/serial.rb +44 -0
  33. data/lib/badline/cia/state.rb +47 -0
  34. data/lib/badline/cia/timer.rb +33 -0
  35. data/lib/badline/cia.rb +8 -0
  36. data/lib/badline/computer/attachments.rb +132 -0
  37. data/lib/badline/computer/saved_state.rb +116 -0
  38. data/lib/badline/computer.rb +13 -33
  39. data/lib/badline/cpu/boundary.rb +54 -0
  40. data/lib/badline/cpu/core.rb +165 -0
  41. data/lib/badline/cpu/microcode.rb +8 -0
  42. data/lib/badline/cpu/saved_state.rb +115 -0
  43. data/lib/badline/cpu.rb +3 -212
  44. data/lib/badline/datasette.rb +27 -0
  45. data/lib/badline/drive1541/bus.rb +13 -0
  46. data/lib/badline/drive1541/cpu.rb +29 -0
  47. data/lib/badline/drive1541/disk.rb +7 -2
  48. data/lib/badline/drive1541/disk_state.rb +80 -0
  49. data/lib/badline/drive1541/mechanism.rb +2 -0
  50. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  51. data/lib/badline/drive1541/rotation.rb +10 -0
  52. data/lib/badline/drive1541/serial_port.rb +10 -0
  53. data/lib/badline/drive1541.rb +35 -0
  54. data/lib/badline/ffi.rb +157 -0
  55. data/lib/badline/frontend/app.rb +225 -0
  56. data/lib/badline/frontend/audio_sink.rb +72 -0
  57. data/lib/badline/frontend/boot.rb +52 -0
  58. data/lib/badline/frontend/controls.rb +163 -0
  59. data/lib/badline/frontend/drive_led.rb +51 -0
  60. data/lib/badline/frontend/frame_rate.rb +83 -0
  61. data/lib/badline/frontend/gamepads.rb +76 -0
  62. data/lib/badline/frontend/keys.rb +48 -0
  63. data/lib/badline/frontend/pacer.rb +100 -0
  64. data/lib/badline/frontend/pad_port.rb +61 -0
  65. data/lib/badline/frontend/screen.rb +65 -0
  66. data/lib/badline/frontend/screenshot.rb +22 -0
  67. data/lib/badline/frontend/snapshots.rb +60 -0
  68. data/lib/badline/frontend/sound.rb +153 -0
  69. data/lib/badline/frontend/timeline.rb +171 -0
  70. data/lib/badline/frontend.rb +18 -0
  71. data/lib/badline/kernal_trap/channel.rb +27 -0
  72. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  73. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  74. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  75. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  76. data/lib/badline/kernal_trap/drive.rb +19 -0
  77. data/lib/badline/kernal_trap/save.rb +8 -0
  78. data/lib/badline/kernal_trap/serial.rb +16 -0
  79. data/lib/badline/memory.rb +8 -0
  80. data/lib/badline/options/event.rb +94 -0
  81. data/lib/badline/options/help.rb +78 -0
  82. data/lib/badline/options/option.rb +29 -0
  83. data/lib/badline/options/table.rb +62 -0
  84. data/lib/badline/options.rb +181 -144
  85. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  86. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  87. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  88. data/lib/badline/reu/dma/saved_state.rb +50 -0
  89. data/lib/badline/reu/dma.rb +56 -11
  90. data/lib/badline/reu/ram.rb +15 -0
  91. data/lib/badline/reu/saved_state.rb +53 -0
  92. data/lib/badline/reu.rb +23 -10
  93. data/lib/badline/sdl.rb +123 -51
  94. data/lib/badline/sid/envelope/state.rb +68 -0
  95. data/lib/badline/sid/envelope.rb +7 -0
  96. data/lib/badline/sid/filter.rb +38 -0
  97. data/lib/badline/sid/voice.rb +10 -0
  98. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  99. data/lib/badline/sid/waveform/state.rb +66 -0
  100. data/lib/badline/sid/waveform.rb +4 -3
  101. data/lib/badline/sid.rb +34 -0
  102. data/lib/badline/snapshot/container.rb +117 -0
  103. data/lib/badline/snapshot/fields.rb +54 -0
  104. data/lib/badline/snapshot/image.rb +70 -0
  105. data/lib/badline/snapshot/machine_state.rb +77 -0
  106. data/lib/badline/snapshot/setup.rb +58 -0
  107. data/lib/badline/snapshot/state.rb +178 -0
  108. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  109. data/lib/badline/snapshot/vice/cias.rb +89 -0
  110. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  111. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  112. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  113. data/lib/badline/snapshot/vice/restore.rb +125 -0
  114. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  115. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  116. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  117. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  118. data/lib/badline/snapshot/vice.rb +74 -0
  119. data/lib/badline/snapshot.rb +52 -0
  120. data/lib/badline/storage/crt_file.rb +9 -3
  121. data/lib/badline/storage/d64_image.rb +2 -0
  122. data/lib/badline/storage/d71_image.rb +2 -0
  123. data/lib/badline/storage/d81_image.rb +2 -0
  124. data/lib/badline/storage/disk_image.rb +11 -2
  125. data/lib/badline/storage/host_directory.rb +5 -0
  126. data/lib/badline/storage/t64.rb +6 -0
  127. data/lib/badline/storage/tap.rb +10 -1
  128. data/lib/badline/storage/unavailable.rb +20 -0
  129. data/lib/badline/storage.rb +34 -0
  130. data/lib/badline/time_of_day.rb +56 -0
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/via/control_lines.rb +15 -0
  133. data/lib/badline/via/interrupt_register.rb +9 -0
  134. data/lib/badline/via/shift_register.rb +13 -0
  135. data/lib/badline/via/state.rb +42 -0
  136. data/lib/badline/via/timer1.rb +13 -0
  137. data/lib/badline/via/timer2.rb +18 -0
  138. data/lib/badline/via.rb +2 -0
  139. data/lib/badline/vic/bank.rb +21 -0
  140. data/lib/badline/vic/border_mask.rb +10 -0
  141. data/lib/badline/vic/collisions.rb +11 -0
  142. data/lib/badline/vic/color_patches.rb +11 -0
  143. data/lib/badline/vic/display_state.rb +29 -1
  144. data/lib/badline/vic/graphics_shifter.rb +26 -0
  145. data/lib/badline/vic/register_log.rb +18 -0
  146. data/lib/badline/vic/registers.rb +13 -2
  147. data/lib/badline/vic/sequencer.rb +12 -0
  148. data/lib/badline/vic/sequencer_state.rb +89 -0
  149. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  150. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  151. data/lib/badline/vic/sprite.rb +2 -0
  152. data/lib/badline/vic/sprites.rb +3 -0
  153. data/lib/badline/vic/sprites_state.rb +57 -0
  154. data/lib/badline/vic.rb +109 -16
  155. data/lib/badline.rb +1 -0
  156. metadata +59 -18
  157. data/.claude/hooks/session-start.sh +0 -57
  158. data/.claude/settings.json +0 -14
  159. data/lib/badline/audio/sdl_sink.rb +0 -70
  160. data/lib/badline/audio/stream.rb +0 -117
  161. data/lib/badline/gui/application.rb +0 -216
  162. data/lib/badline/gui/drive_led_pane.rb +0 -29
  163. data/lib/badline/gui/gamepads.rb +0 -97
  164. data/lib/badline/gui/joy_map.rb +0 -27
  165. data/lib/badline/gui/key_map.rb +0 -39
  166. data/lib/badline/gui/palette.rb +0 -35
  167. data/lib/badline/gui/pane.rb +0 -36
  168. data/lib/badline/gui/screen_pane.rb +0 -46
  169. data/lib/badline/gui/window.rb +0 -61
  170. data/lib/badline/gui.rb +0 -13
  171. data/lib/badline/sdl/events.rb +0 -47
  172. data/lib/badline/sdl/functions.rb +0 -74
  173. data/vendor/.gitkeep +0 -0
@@ -1,216 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- class Application
6
- TITLE = "Badline"
7
- TOGGLE_SYM = SDL::KEY_TAB
8
- MUTE_SYM = SDL::KEY_F10
9
- REVERSE_MOD = SDL::KMOD_SHIFT
10
-
11
- SHARED_KEYS = %i[cursor_up cursor_left cursor_h cursor_v space w a s d lshift].freeze
12
-
13
- # Tab steps through the input modes and shift-Tab back; the title bar
14
- # names the live one. The pot devices appear once per control port, since
15
- # games disagree on which one they read.
16
- MODES = {
17
- keyboard: nil, joystick: "JOY",
18
- mouse1: "MOUSE 1", mouse2: "MOUSE 2",
19
- paddles1: "PADDLE 1", paddles2: "PADDLE 2"
20
- }.freeze
21
-
22
- # Both pot devices take their input from the host mouse. Motion turns the
23
- # paddle knobs or steps the 1351's counters, and the host buttons go to
24
- # whichever lines the device puts them on.
25
- POT_DEVICES = {
26
- mouse1: [Input::Mouse1351, 1], mouse2: [Input::Mouse1351, 2],
27
- paddles1: [Input::Paddles, 1], paddles2: [Input::Paddles, 2]
28
- }.freeze
29
- MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
30
-
31
- # The machine options (sid_model:, reu:, true_drive:, ntsc:) build
32
- # the machine: reu plugs in an REU of that many K, true_drive puts a
33
- # true 1541 on device 8, and ntsc makes it an NTSC C64. The media
34
- # options (autostart:, song:, disk:) go to Media.attach.
35
- def initialize(media_path: nil, machine: {}, sound: false, verbose: false, **media)
36
- @verbose = verbose
37
- @computer = Computer.new(**machine_options(machine, media_path))
38
- Media::TrueDrive.plug(@computer) if machine[:true_drive]
39
- puts Media.attach(@computer, media_path, **media) if media_path
40
-
41
- @mode = :keyboard
42
- @pot_device = nil
43
- @panes = [ScreenPane.new(@computer)]
44
- @panes << DriveLedPane.new(@computer.drive1541, @panes.first) if @computer.drive1541
45
- @stream = open_stream if sound
46
- @paced = ENV["NOVSYNC"].nil?
47
- @window = Window.new(
48
- title: TITLE,
49
- width: canvas_width, height: canvas_height,
50
- vsync: @paced && !@stream
51
- )
52
- @gamepads = Gamepads.new(@computer)
53
- @gamepads.names.each { |name| report "Gamepad: #{name}" }
54
- fit_frame
55
- end
56
-
57
- def run
58
- @running = true
59
- while @running
60
- handle_events
61
- @gamepads.poll
62
- @cycles_per_frame.times { @computer.cycle! }
63
- @stream&.feed
64
- @window.draw(@panes)
65
- @stream.pace(@frame_seconds) if @stream && @paced
66
- end
67
- ensure
68
- @computer.drive1541&.flush
69
- @stream&.close
70
- @gamepads.close
71
- @window.close
72
- report @computer.cpu.inspect
73
- end
74
-
75
- private
76
-
77
- def handle_events
78
- while (event = SDL.poll_event)
79
- case event
80
- when SDL::Quit
81
- @running = false
82
- when SDL::KeyDown
83
- handle_key_down(event)
84
- when SDL::KeyUp
85
- handle_key_up(event)
86
- when SDL::MouseMotion
87
- @pot_device&.move(event.xrel, event.yrel)
88
- when SDL::MouseButton
89
- handle_mouse_button(event)
90
- when SDL::ControllerDevice
91
- @gamepads.rescan
92
- end
93
- end
94
- end
95
-
96
- def handle_key_down(event)
97
- return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
98
- return toggle_mute if event.sym == MUTE_SYM
99
-
100
- port, dir = JoyMap.parse(event) if @mode == :joystick
101
- if port
102
- joystick(port).press(dir)
103
- else
104
- press_key(KeyMap.parse(event), event.repeat)
105
- end
106
- end
107
-
108
- # RESTORE isn't in the key matrix, so it goes to the machine instead,
109
- # once per press as the real key's one-shot fires.
110
- def press_key(key, repeat)
111
- return @computer.press_restore if key == :restore && !repeat
112
- return if key == :restore
113
-
114
- @computer.keyboard.press(key)
115
- end
116
-
117
- def handle_key_up(event)
118
- return if [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
119
-
120
- port, dir = JoyMap.parse(event) if @mode == :joystick
121
- if port
122
- joystick(port).release(dir)
123
- else
124
- @computer.keyboard.release(KeyMap.parse(event))
125
- end
126
- end
127
-
128
- def handle_mouse_button(event)
129
- button = MOUSE_BUTTONS[event.button]
130
- return unless button && @pot_device
131
-
132
- if event.pressed
133
- @pot_device.press(button)
134
- else
135
- @pot_device.release(button)
136
- end
137
- end
138
-
139
- def joystick(port)
140
- port == 1 ? @computer.joystick1 : @computer.joystick2
141
- end
142
-
143
- def cycle_mode(step)
144
- modes = MODES.keys
145
- @mode = modes[(modes.index(@mode) + step) % modes.size]
146
- release_inputs
147
- attach_pot_device
148
- update_title
149
- end
150
-
151
- def toggle_mute
152
- return unless @stream
153
-
154
- @stream.toggle_mute
155
- update_title
156
- end
157
-
158
- def update_title
159
- tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
160
- @window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
161
- end
162
-
163
- def open_stream
164
- sink = Audio::SDLSink.new(rate: Audio::Renderer::DEFAULT_RATE)
165
- report "Sound at #{sink.rate} Hz, F10 mutes"
166
- Audio::Stream.new(sink, @computer.sid,
167
- on_underrun: -> { puts "Running below real time, so the sound will stutter." })
168
- rescue Audio::SDLSink::Error => e
169
- warn "badline: no sound, can't open the audio device: #{e.message}"
170
- end
171
-
172
- def machine_options(machine, media_path)
173
- options = { sid_model: machine[:sid_model] || Media.sid_model(media_path), **machine.slice(:reu) }
174
- options[:region] = Region::NTSC if machine[:ntsc]
175
- options
176
- end
177
-
178
- def fit_frame
179
- rate = @window.refresh_rate
180
- clock_hz = @computer.region.clock_hz
181
- @cycles_per_frame = clock_hz / rate
182
- @frame_seconds = @cycles_per_frame.fdiv(clock_hz)
183
- report "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
184
- end
185
-
186
- def report(line)
187
- puts line if @verbose
188
- end
189
-
190
- def attach_pot_device
191
- device_class, port = POT_DEVICES[@mode]
192
- @pot_device = device_class&.new
193
- ports = @computer.control_ports
194
- ports.device1 = port == 1 ? @pot_device : nil
195
- ports.device2 = port == 2 ? @pot_device : nil
196
- SDL::SetRelativeMouseMode.call(@pot_device ? 1 : 0)
197
- end
198
-
199
- def release_inputs
200
- SHARED_KEYS.each { |key| @computer.keyboard.release(key) }
201
- Joystick::DIRECTIONS.each_key do |dir|
202
- @computer.joystick1.release(dir)
203
- @computer.joystick2.release(dir)
204
- end
205
- end
206
-
207
- def canvas_width
208
- @panes.map { |pane| pane.left + pane.width }.max
209
- end
210
-
211
- def canvas_height
212
- @panes.map { |pane| pane.top + pane.height }.max
213
- end
214
- end
215
- end
216
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # The true drive's red LED, in the bottom right corner of the screen
6
- # pane's border: bright while the DOS lights it, as it does while a file
7
- # is open or after an error, and dim otherwise.
8
- class DriveLedPane < Pane
9
- WIDTH = 12
10
- HEIGHT = 4
11
-
12
- LIT = [0xff, 0x20, 0x20].freeze
13
- DARK = [0x40, 0x00, 0x00].freeze
14
-
15
- def initialize(drive, screen)
16
- super(width: WIDTH, height: HEIGHT,
17
- left: screen.left + screen.width - WIDTH - 8, top: screen.top + screen.height - HEIGHT - 6)
18
- @drive = drive
19
- end
20
-
21
- def color = @drive.led_on? ? LIT : DARK
22
-
23
- def render(renderer)
24
- SDL::SetRenderDrawColor.call(renderer, *color, 0xff)
25
- SDL::RenderFillRect.call(renderer, @rect)
26
- end
27
- end
28
- end
29
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # Host game controllers wired onto the control ports.
6
- #
7
- # The first controller found drives joystick 2, the port most games read;
8
- # a second one drives joystick 1. D-pad and left stick both steer, every
9
- # face and shoulder button fires.
10
- #
11
- # SDL announces arrivals by device index but reports button presses by
12
- # instance id, so rather than line the two up, this class polls the
13
- # controllers every frame. It only releases directions it
14
- # pressed itself, so the keyboard mapping keeps working with a pad
15
- # connected.
16
- class Gamepads
17
- DEADZONE = 8_000
18
- PORTS = [2, 1].freeze
19
-
20
- BUTTONS = {
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]
28
- }.freeze
29
-
30
- AXES = {
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]
35
- }.freeze
36
-
37
- def initialize(computer)
38
- SDL.check(SDL::InitSubSystem.call(SDL::INIT_GAMECONTROLLER))
39
- @computer = computer
40
- @controllers = []
41
- @pressed = Array.new(PORTS.size) { [] }
42
- rescan
43
- end
44
-
45
- def names = @controllers.map { |controller| SDL::GameControllerName.call(controller) }
46
-
47
- def rescan
48
- close
49
- @controllers = (0...SDL::NumJoysticks.call)
50
- .select { |index| SDL::IsGameController.call(index) == 1 }
51
- .first(PORTS.size)
52
- .map { |index| SDL::GameControllerOpen.call(index) }
53
- .reject(&:null?)
54
- end
55
-
56
- def poll
57
- @controllers.each_with_index { |controller, index| apply(controller, index) }
58
- end
59
-
60
- def close
61
- @controllers.each { |controller| SDL::GameControllerClose.call(controller) }
62
- @controllers = []
63
- @pressed.size.times { |index| release(index) }
64
- end
65
-
66
- private
67
-
68
- def apply(controller, index)
69
- active = Joystick::DIRECTIONS.keys.select { |dir| active?(controller, dir) }
70
- (@pressed[index] - active).each { |dir| joystick(index).release(dir) }
71
- active.each { |dir| joystick(index).press(dir) }
72
- @pressed[index] = active
73
- end
74
-
75
- def release(index)
76
- @pressed[index].each { |dir| joystick(index).release(dir) }
77
- @pressed[index] = []
78
- end
79
-
80
- def active?(controller, direction)
81
- BUTTONS.fetch(direction).any? { |button| SDL::GameControllerGetButton.call(controller, button) == 1 } ||
82
- tilted?(controller, direction)
83
- end
84
-
85
- def tilted?(controller, direction)
86
- axis, sign = AXES[direction]
87
- return false unless axis
88
-
89
- SDL::GameControllerGetAxis.call(controller, axis) * sign > DEADZONE
90
- end
91
-
92
- def joystick(index)
93
- PORTS[index] == 1 ? @computer.joystick1 : @computer.joystick2
94
- end
95
- end
96
- end
97
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # Maps host keys onto the two control ports while joystick mode is on.
6
- #
7
- # The arrow cluster plus space drives joystick 2, WASD plus left shift
8
- # joystick 1, so both ports can be played at once.
9
- class JoyMap
10
- PORTS = {
11
- 2 => { "Up" => :up, "Down" => :down, "Left" => :left, "Right" => :right, "Space" => :fire }.freeze,
12
- 1 => { "W" => :up, "S" => :down, "A" => :left, "D" => :right, "Left Shift" => :fire }.freeze
13
- }.freeze
14
-
15
- def self.parse(event)
16
- name = SDL.key_name(event.sym)
17
-
18
- PORTS.each do |port, map|
19
- direction = map[name]
20
- return [port, direction] if direction
21
- end
22
-
23
- nil
24
- end
25
- end
26
- end
27
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # Translates SDL key events into the emulator's key symbols.
6
- #
7
- # MAP is just the overrides, anything not covered simply falls though.
8
- class KeyMap
9
- MAP = {
10
- "Backspace" => :delete,
11
- "Return" => :return,
12
- "Right" => :cursor_h,
13
- "Down" => :cursor_v,
14
- "Left" => :cursor_left,
15
- "Up" => :cursor_up,
16
- "`" => :left,
17
- "]" => :up,
18
- "PageUp" => :restore,
19
- "Space" => :space,
20
- "Left Ctrl" => :control,
21
- "Left Alt" => :cbm,
22
- "Left Shift" => :lshift,
23
- "Right Shift" => :rshift,
24
- "Home" => :clr_home,
25
- "Escape" => :run_stop,
26
- "Backslash" => :"@",
27
- "'" => :":",
28
- "End" => :£,
29
- "Keypad +" => :+,
30
- "Keypad *" => :*
31
- }.freeze
32
-
33
- def self.parse(event)
34
- name = SDL.key_name(event.sym)
35
- MAP[name] || name.downcase.to_sym
36
- end
37
- end
38
- end
39
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- class Palette
6
- COLORS = [
7
- "#000000", "#FFFFFF", "#924A40", "#84C5CC",
8
- "#9351B6", "#72B14B", "#483AAA", "#D5DF7C",
9
- "#675200", "#C33D00", "#C18178", "#606060",
10
- "#8A8A8A", "#B3EC91", "#867ADE", "#B3B3B3"
11
- ].freeze
12
-
13
- attr_reader :dwords
14
-
15
- def initialize
16
- @dwords = COLORS.map { |hex| dword(hex) }.freeze
17
- @entries = @dwords.map { |rgba| [rgba].pack("V") }.freeze
18
- end
19
-
20
- def [](color)
21
- @entries[color]
22
- end
23
-
24
- private
25
-
26
- # Color as a little-endian RGBA dword.
27
- def dword(hex)
28
- r = hex[1, 2].to_i(16)
29
- g = hex[3, 2].to_i(16)
30
- b = hex[5, 2].to_i(16)
31
- (255 << 24) | (b << 16) | (g << 8) | r
32
- end
33
- end
34
- end
35
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- class Pane
6
- attr_reader :left, :top, :width, :height
7
-
8
- def initialize(width:, height:, left: 0, top: 0)
9
- @width = width
10
- @height = height
11
- @left = left
12
- @top = top
13
- @rect = [left, top, width, height].pack("l4")
14
- end
15
-
16
- def render(_renderer)
17
- raise NotImplementedError, "#{self.class} must implement #render"
18
- end
19
-
20
- private
21
-
22
- # The pixels are little-endian RGBA dwords, red in the low byte.
23
- def blit(renderer, pixels)
24
- surface = SDL::CreateRGBSurfaceFrom.call(
25
- pixels, width, height, 32, width * 4,
26
- 0x0000_00ff, 0x0000_ff00, 0x00ff_0000, 0xff00_0000
27
- )
28
- texture = SDL::CreateTextureFromSurface.call(renderer, surface)
29
- SDL::RenderCopy.call(renderer, texture, nil, @rect)
30
- ensure
31
- SDL::DestroyTexture.call(texture) if texture
32
- SDL::FreeSurface.call(surface) if surface
33
- end
34
- end
35
- end
36
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # The part of the VIC's raster the machine's region crops out.
6
- class ScreenPane < Pane
7
- def initialize(computer, left: 0, top: 0, palette: Palette.new)
8
- @col_offset, @row_offset, crop_width, crop_height = computer.region.crop
9
- super(width: crop_width, height: crop_height, left:, top:)
10
- @computer = computer
11
- @row_bytes = width * 4
12
- @palette = palette.dwords
13
- @buffer = ("\x00" * (height * @row_bytes)).b
14
- end
15
-
16
- def render(renderer)
17
- blit(renderer, framebuffer)
18
- end
19
-
20
- private
21
-
22
- # Repack only the scanlines the VIC has touched since the last frame.
23
- def framebuffer
24
- vic = @computer.vic
25
- display = vic.display
26
- vic_width = vic.width
27
- dirty = vic.dirty_lines
28
-
29
- height.times do |row|
30
- next unless dirty[row + @row_offset]
31
-
32
- @buffer[row * @row_bytes, @row_bytes] =
33
- pack_row(display, vic_width, row)
34
- end
35
- vic.clear_dirty_lines!
36
- @buffer
37
- end
38
-
39
- def pack_row(display, vic_width, row)
40
- line = display[((row + @row_offset) * vic_width) + @col_offset, width]
41
- line.map! { |c| @palette[c] }
42
- line.pack("V*")
43
- end
44
- end
45
- end
46
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- class Window
6
- DEFAULT_REFRESH_RATE = 60
7
-
8
- # SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
9
- DISPLAY_MODE_SIZE = 24
10
-
11
- attr_reader :renderer
12
-
13
- def initialize(title:, width:, height:, scale: 2, vsync: true)
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))
29
- end
30
-
31
- def title=(title)
32
- SDL::SetWindowTitle.call(@window, title)
33
- end
34
-
35
- def refresh_rate
36
- mode = "\0".b * DISPLAY_MODE_SIZE
37
- SDL.check(SDL::GetCurrentDisplayMode.call(0, mode))
38
- rate = mode.unpack1("l", offset: 12)
39
- rate.positive? ? rate : DEFAULT_REFRESH_RATE
40
- rescue SDL::Error
41
- DEFAULT_REFRESH_RATE
42
- end
43
-
44
- def draw(panes)
45
- SDL::SetRenderDrawColor.call(@renderer, 0, 0, 0, 255)
46
- SDL::RenderClear.call(@renderer)
47
- panes.each { |pane| pane.render(@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
58
- end
59
- end
60
- end
61
- end
data/lib/badline/gui.rb DELETED
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "badline/sdl"
4
-
5
- require "badline/gui/palette"
6
- require "badline/gui/key_map"
7
- require "badline/gui/joy_map"
8
- require "badline/gui/gamepads"
9
- require "badline/gui/pane"
10
- require "badline/gui/screen_pane"
11
- require "badline/gui/drive_led_pane"
12
- require "badline/gui/window"
13
- require "badline/gui/application"
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module SDL
5
- KEY_TAB = 0x09
6
- KEY_F10 = 0x4000_0043
7
- KMOD_SHIFT = 0x0003
8
-
9
- # The events the front end handles. SDL_Event is a 56-byte union, read
10
- # here at the offsets its keyboard, mouse and controller members use.
11
- Quit = Data.define
12
- # A held key sends repeats, flagged at byte 13 of the event.
13
- KeyDown = Data.define(:sym, :mod, :repeat) do
14
- def initialize(sym:, mod:, repeat: false) = super
15
- end
16
- KeyUp = Data.define(:sym, :mod)
17
- MouseMotion = Data.define(:xrel, :yrel)
18
- MouseButton = Data.define(:button, :pressed)
19
- ControllerDevice = Data.define
20
-
21
- EVENT_SIZE = 56
22
-
23
- # The next event the front end handles, skipping the others, or nil once
24
- # the queue is empty.
25
- def self.poll_event
26
- buffer = (@event ||= Fiddle::Pointer.malloc(EVENT_SIZE, Fiddle::RUBY_FREE))
27
- while PollEvent.call(buffer) == 1
28
- event = decode(buffer.to_str(EVENT_SIZE))
29
- return event if event
30
- end
31
- nil
32
- end
33
-
34
- def self.decode(bytes)
35
- case bytes.unpack1("L")
36
- when 0x100 then Quit.new
37
- when 0x300 then KeyDown.new(*bytes.unpack("lS", offset: 20), bytes.getbyte(13) != 0)
38
- when 0x301 then KeyUp.new(*bytes.unpack("lS", offset: 20))
39
- when 0x400 then MouseMotion.new(*bytes.unpack("l2", offset: 28))
40
- when 0x401, 0x402 then MouseButton.new(bytes.getbyte(16), bytes.getbyte(17) == 1)
41
- when 0x653..0x655 then ControllerDevice.new
42
- end
43
- end
44
-
45
- def self.key_name(sym) = GetKeyName.call(sym)
46
- end
47
- end