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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +107 -0
  5. data/CLAUDE.md +51 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +119 -51
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +293 -39
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +28 -14
  12. data/lib/badline/addressable.rb +1 -1
  13. data/lib/badline/audio/aiff.rb +8 -3
  14. data/lib/badline/audio/bare_player.rb +20 -3
  15. data/lib/badline/audio/cli.rb +17 -12
  16. data/lib/badline/audio/console.rb +12 -58
  17. data/lib/badline/audio/machine_player.rb +9 -6
  18. data/lib/badline/audio/playback.rb +3 -0
  19. data/lib/badline/audio/renderer.rb +3 -3
  20. data/lib/badline/audio/sdl_sink.rb +20 -52
  21. data/lib/badline/audio/stream.rb +117 -0
  22. data/lib/badline/audio/terminal.rb +90 -0
  23. data/lib/badline/audio.rb +3 -2
  24. data/lib/badline/cartridge/easy_flash.rb +6 -2
  25. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  26. data/lib/badline/cartridge/geo_ram.rb +63 -0
  27. data/lib/badline/cartridge/mach5.rb +1 -1
  28. data/lib/badline/cartridge/magic_desk.rb +5 -1
  29. data/lib/badline/cartridge/ocean.rb +5 -2
  30. data/lib/badline/cartridge/rex_utility.rb +1 -1
  31. data/lib/badline/cartridge/rgcd.rb +1 -1
  32. data/lib/badline/cartridge/simons_basic.rb +2 -2
  33. data/lib/badline/cartridge/westermann.rb +2 -2
  34. data/lib/badline/cartridge.rb +9 -0
  35. data/lib/badline/checkpoint.rb +98 -0
  36. data/lib/badline/cia/interrupt_register.rb +34 -10
  37. data/lib/badline/cia.rb +19 -10
  38. data/lib/badline/color_memory.rb +2 -2
  39. data/lib/badline/computer.rb +34 -14
  40. data/lib/badline/cpu.rb +2 -2
  41. data/lib/badline/gui/application.rb +70 -26
  42. data/lib/badline/gui/gamepads.rb +22 -22
  43. data/lib/badline/gui/joy_map.rb +1 -1
  44. data/lib/badline/gui/key_map.rb +6 -1
  45. data/lib/badline/gui/pane.rb +7 -6
  46. data/lib/badline/gui/screen_pane.rb +9 -13
  47. data/lib/badline/gui/window.rb +35 -20
  48. data/lib/badline/gui.rb +1 -1
  49. data/lib/badline/input/mouse1351.rb +21 -6
  50. data/lib/badline/input/paddles.rb +11 -6
  51. data/lib/badline/kernal_trap/channel.rb +102 -7
  52. data/lib/badline/kernal_trap/drive/block_commands.rb +108 -0
  53. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  54. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  55. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  56. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  57. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  58. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  59. data/lib/badline/kernal_trap/drive.rb +109 -131
  60. data/lib/badline/kernal_trap/file.rb +6 -14
  61. data/lib/badline/kernal_trap/load.rb +6 -0
  62. data/lib/badline/kernal_trap/save.rb +13 -7
  63. data/lib/badline/keyboard.rb +19 -3
  64. data/lib/badline/media.rb +44 -13
  65. data/lib/badline/memory.rb +2 -2
  66. data/lib/badline/options.rb +209 -0
  67. data/lib/badline/ram_expansion/banking.rb +30 -0
  68. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  71. data/lib/badline/ram_expansion.rb +21 -0
  72. data/lib/badline/region.rb +51 -0
  73. data/lib/badline/rom.rb +14 -5
  74. data/lib/badline/sdl/events.rb +47 -0
  75. data/lib/badline/sdl/functions.rb +73 -0
  76. data/lib/badline/sdl.rb +67 -0
  77. data/lib/badline/sid/catch_up.rb +110 -0
  78. data/lib/badline/sid/decimator.rb +6 -1
  79. data/lib/badline/sid/filter.rb +5 -5
  80. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  81. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  82. data/lib/badline/sid/waveform.rb +8 -8
  83. data/lib/badline/sid.rb +19 -93
  84. data/lib/badline/storage/d64_image.rb +10 -0
  85. data/lib/badline/storage/d71_image.rb +12 -0
  86. data/lib/badline/storage/d81_image.rb +9 -0
  87. data/lib/badline/storage/disk_image/bam.rb +111 -0
  88. data/lib/badline/storage/disk_image/directory.rb +79 -0
  89. data/lib/badline/storage/disk_image/writing.rb +110 -0
  90. data/lib/badline/storage/disk_image.rb +49 -4
  91. data/lib/badline/storage/host_directory.rb +17 -4
  92. data/lib/badline/storage/sid_file.rb +36 -2
  93. data/lib/badline/storage.rb +18 -1
  94. data/lib/badline/time_of_day.rb +11 -2
  95. data/lib/badline/version.rb +1 -1
  96. data/lib/badline/via/control_lines.rb +104 -0
  97. data/lib/badline/via/interrupt_register.rb +58 -0
  98. data/lib/badline/via/shift_register.rb +135 -0
  99. data/lib/badline/via/timer1.rb +82 -0
  100. data/lib/badline/via/timer2.rb +84 -0
  101. data/lib/badline/via.rb +262 -0
  102. data/lib/badline/vic/bank.rb +4 -3
  103. data/lib/badline/vic/color_patches.rb +6 -3
  104. data/lib/badline/vic/display_state.rb +4 -0
  105. data/lib/badline/vic/graphics_shifter.rb +49 -8
  106. data/lib/badline/vic/registers.rb +19 -27
  107. data/lib/badline/vic/sequencer.rb +18 -18
  108. data/lib/badline/vic/sequencer_output.rb +4 -4
  109. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  110. data/lib/badline/vic/sprite/shifter.rb +25 -3
  111. data/lib/badline/vic/sprite.rb +5 -4
  112. data/lib/badline/vic/sprites.rb +8 -3
  113. data/lib/badline/vic.rb +98 -46
  114. data/lib/badline.rb +5 -0
  115. data/packaging/homebrew/badline.rb +34 -0
  116. metadata +37 -20
  117. data/exe/badline +0 -75
  118. data/exe/badline-sid +0 -34
  119. 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 = SDL2::Key::TAB
9
- REVERSE_MOD = SDL2::Key::Mod::SHIFT
7
+ TOGGLE_SYM = SDL::KEY_TAB
8
+ MUTE_SYM = SDL::KEY_F10
9
+ REVERSE_MOD = SDL::KMOD_SHIFT
10
10
 
11
- SHARED_KEYS = %i[up left cursor_h cursor_v space w a s d lshift].freeze
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
- def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil, debug: false)
32
- @computer = Computer.new(debug:, sid_model: sid_model || Media.sid_model(media_path))
33
- puts Media.attach(@computer, media_path, autostart:, song:) if media_path
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: ENV["NOVSYNC"].nil?
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 = SDL2::Event.poll)
71
+ while (event = SDL.poll_event)
68
72
  case event
69
- when SDL2::Event::Quit
73
+ when SDL::Quit
70
74
  @running = false
71
- when SDL2::Event::KeyDown
75
+ when SDL::KeyDown
72
76
  handle_key_down(event)
73
- when SDL2::Event::KeyUp
77
+ when SDL::KeyUp
74
78
  handle_key_up(event)
75
- when SDL2::Event::MouseMotion
79
+ when SDL::MouseMotion
76
80
  @pot_device&.move(event.xrel, event.yrel)
77
- when SDL2::Event::MouseButton
81
+ when SDL::MouseButton
78
82
  handle_mouse_button(event)
79
- when SDL2::Event::ControllerDevice
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
- @computer.keyboard.press(KeyMap.parse(event))
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 == TOGGLE_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
- case event
112
- when SDL2::Event::MouseButtonDown then @pot_device.press(button)
113
- when SDL2::Event::MouseButtonUp then @pot_device.release(button)
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
- @window.title = [TITLE, MODES[@mode] && "[#{MODES[@mode]}]"].compact.join(" ")
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
- SDL2::Mouse.relative_mode = !@pot_device.nil?
179
+ SDL::SetRelativeMouseMode.call(@pot_device ? 1 : 0)
136
180
  end
137
181
 
138
182
  def release_inputs
@@ -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, and the binding gives no way to line the two up, so this
13
- # class polls the controllers every frame. It only releases directions it
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: [SDL2::GameController::Button::DPAD_UP],
22
- down: [SDL2::GameController::Button::DPAD_DOWN],
23
- left: [SDL2::GameController::Button::DPAD_LEFT],
24
- right: [SDL2::GameController::Button::DPAD_RIGHT],
25
- fire: [SDL2::GameController::Button::A, SDL2::GameController::Button::B,
26
- SDL2::GameController::Button::X, SDL2::GameController::Button::Y,
27
- SDL2::GameController::Button::LEFTSHOULDER,
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: [SDL2::GameController::Axis::LEFTY, -1],
33
- down: [SDL2::GameController::Axis::LEFTY, 1],
34
- left: [SDL2::GameController::Axis::LEFTX, -1],
35
- right: [SDL2::GameController::Axis::LEFTX, 1]
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
- SDL2.init(SDL2::INIT_GAMECONTROLLER)
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(&:name)
45
+ def names = @controllers.map { |controller| SDL::GameControllerName.call(controller) }
47
46
 
48
47
  def rescan
49
48
  close
50
- @controllers = (0...SDL2::Joystick.num_connected_joysticks)
51
- .select { |index| SDL2::Joystick.game_controller?(index) }
49
+ @controllers = (0...SDL::NumJoysticks.call)
50
+ .select { |index| SDL::IsGameController.call(index) == 1 }
52
51
  .first(PORTS.size)
53
- .map { |index| SDL2::GameController.open(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| controller.destroy unless controller.destroy? }
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| controller.button_pressed?(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
- controller.axis(axis) * sign > DEADZONE
89
+ SDL::GameControllerGetAxis.call(controller, axis) * sign > DEADZONE
90
90
  end
91
91
 
92
92
  def joystick(index)
@@ -13,7 +13,7 @@ module Badline
13
13
  }.freeze
14
14
 
15
15
  def self.parse(event)
16
- name = SDL2::Key.name_of(event.sym)
16
+ name = SDL.key_name(event.sym)
17
17
 
18
18
  PORTS.each do |port, map|
19
19
  direction = map[name]
@@ -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 = SDL2::Key.name_of(event.sym)
34
+ name = SDL.key_name(event.sym)
30
35
  MAP[name] || name.downcase.to_sym
31
36
  end
32
37
  end
@@ -10,7 +10,7 @@ module Badline
10
10
  @height = height
11
11
  @left = left
12
12
  @top = top
13
- @rect = SDL2::Rect.new(left, top, width, height)
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 = SDL2::Surface.from_string(
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 = renderer.create_texture_from(surface)
28
- renderer.copy(texture, nil, @rect)
28
+ texture = SDL::CreateTextureFromSurface.call(renderer, surface)
29
+ SDL::RenderCopy.call(renderer, texture, nil, @rect)
29
30
  ensure
30
- texture&.destroy
31
- surface&.destroy
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
- super(width: WIDTH, height: HEIGHT, left:, top:)
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" * (HEIGHT * ROW_BYTES)).b
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
- HEIGHT.times do |row|
34
- next unless dirty[row + ROW_OFFSET]
29
+ height.times do |row|
30
+ next unless dirty[row + @row_offset]
35
31
 
36
- @buffer[row * ROW_BYTES, ROW_BYTES] =
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 + ROW_OFFSET) * vic_width) + COL_OFFSET, WIDTH]
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
@@ -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
- SDL2.init(SDL2::INIT_VIDEO | SDL2::INIT_EVENTS)
12
- SDL2::Hints["SDL_RENDER_SCALE_QUALITY"] = "0" # nearest-neighbour
13
-
14
- @window = SDL2::Window.create(
15
- title,
16
- SDL2::Window::POS_CENTERED, SDL2::Window::POS_CENTERED,
17
- width * scale, height * scale,
18
- SDL2::Window::Flags::RESIZABLE
19
- )
20
-
21
- flags = SDL2::Renderer::Flags::ACCELERATED
22
- flags |= SDL2::Renderer::Flags::PRESENTVSYNC if vsync
23
- @renderer = @window.create_renderer(-1, flags)
24
- @renderer.logical_size = [width, height]
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.title = title
32
+ SDL::SetWindowTitle.call(@window, title)
29
33
  end
30
34
 
31
35
  def refresh_rate
32
- rate = SDL2::Display.displays.first.current_mode.refresh_rate
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 StandardError
40
+ rescue SDL::Error
35
41
  DEFAULT_REFRESH_RATE
36
42
  end
37
43
 
38
44
  def draw(panes)
39
- @renderer.draw_color = [0, 0, 0]
40
- @renderer.clear
45
+ SDL::SetRenderDrawColor.call(@renderer, 0, 0, 0, 255)
46
+ SDL::RenderClear.call(@renderer)
41
47
  panes.each { |pane| pane.render(@renderer) }
42
- @renderer.present
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sdl2"
3
+ require "badline/sdl"
4
4
 
5
5
  require "badline/gui/palette"
6
6
  require "badline/gui/key_map"
@@ -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. The left button sits on the joystick fire line, the
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
- @x = (@x + delta_x) & 0x3f
33
- @y = (@y - delta_y) & 0x3f
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 = @x << 1
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 = @y << 1
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
- @pot_x = 0x80
21
- @pot_y = 0x80
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
- @pot_x = (@pot_x + turn_a).clamp(0, 0xff)
32
- @pot_y = (@pot_y + turn_b).clamp(0, 0xff)
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