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
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class REU
5
+ # The REU's state for a snapshot: the REC's registers and the values
6
+ # its counters start from, an armed or running transfer, the IRQ line,
7
+ # the transfer's own state and the RAM. The size is the machine's
8
+ # setup (Snapshot::Setup), so a restore finds an REU of the same size.
9
+ module SavedState
10
+ def size_kb = @size_kb
11
+
12
+ def save_state(out)
13
+ out.marker("REU")
14
+ [@status, @command, @c64, @c64_start, @expansion, @expansion_start, @bank, @bank_start, @length,
15
+ @length_start, @interrupt_mask, @address_control].each { |value| out.int(value) }
16
+ out.boolean(@armed).boolean(@requested).boolean(@running).boolean(@irq).boolean(@ba_was_low)
17
+ @dma.save_state(out)
18
+ @ram.save_state(out)
19
+ end
20
+
21
+ # The IRQ line comes back as it was without calling back into the
22
+ # machine, which reads it again afterwards.
23
+ def load_state(input)
24
+ input.marker("REU")
25
+ load_registers(input)
26
+ @armed = input.boolean?
27
+ @requested = input.boolean?
28
+ @running = input.boolean?
29
+ @irq = input.boolean?
30
+ @ba_was_low = input.boolean?
31
+ @dma.load_state(input)
32
+ @ram.load_state(input)
33
+ end
34
+
35
+ private
36
+
37
+ def load_registers(input)
38
+ @status = input.int
39
+ @command = input.int
40
+ @c64 = input.int
41
+ @c64_start = input.int
42
+ @expansion = input.int
43
+ @expansion_start = input.int
44
+ @bank = input.int
45
+ @bank_start = input.int
46
+ @length = input.int
47
+ @length_start = input.int
48
+ @interrupt_mask = input.int
49
+ @address_control = input.int
50
+ end
51
+ end
52
+ end
53
+ end
data/lib/badline/reu.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "badline/reu/ram"
4
4
  require "badline/reu/dma"
5
5
  require "badline/reu/trigger"
6
+ require "badline/reu/saved_state"
6
7
 
7
8
  module Badline
8
9
  # A RAM Expansion Unit: the Commodore 1700 (128K), 1764 (256K) and 1750
@@ -18,6 +19,8 @@ module Badline
18
19
  # waits for the CPU's next read cycle with BA high, and Computer then
19
20
  # clocks it through #dma_cycle! until it hands the bus back.
20
21
  class REU
22
+ include SavedState
23
+
21
24
  SIZES_KB = [128, 256, 512, 1024, 2048, 4096, 8192, 16_384].freeze
22
25
 
23
26
  # The registers, by offset.
@@ -74,6 +77,7 @@ module Badline
74
77
 
75
78
  # The 1700's REC counts 17 address bits, the others' 19.
76
79
  span = size_kb == 128 ? 0x20000 : 0x80000
80
+ @size_kb = size_kb
77
81
  size = size_kb * 1024
78
82
  @address_mask = [size, span].max - 1
79
83
  @bank_bits = size > span ? 0xff : 0x07
@@ -102,9 +106,8 @@ module Badline
102
106
  # From the moment the REC asks for the bus until it hands it back.
103
107
  def dma? = @requested || @running
104
108
 
105
- # Whether the REC held the bus on the last cycle clocked. The CPU keeps
106
- # it while the REC waits to start, and has it back from the cycle the
107
- # REC lets go.
109
+ # Whether the REC ran a transfer on the last cycle clocked, which it
110
+ # goes on doing until the cycle it lets go of the bus.
108
111
  def holds_bus? = @running
109
112
 
110
113
  def reset!
@@ -116,7 +119,7 @@ module Badline
116
119
  @length = @length_start = 0xffff
117
120
  @interrupt_mask = 0x1f
118
121
  @address_control = 0x3f
119
- @armed = @requested = @running = false
122
+ @armed = @requested = @running = @ba_was_low = false
120
123
  interrupt(false)
121
124
  end
122
125
 
@@ -154,16 +157,20 @@ module Badline
154
157
  request_bus
155
158
  end
156
159
 
157
- # Clocks the REC for a cycle, given the VIC's BA line and whether the
158
- # CPU writes on it. A requested transfer starts on the first cycle the
159
- # CPU reads with BA high.
160
- def dma_cycle!(ba_low, cpu_writing)
160
+ # Clocks the REC for a cycle, given the VIC's BA line. A requested
161
+ # transfer starts on the first cycle with BA high as the REC sees it,
162
+ # even one the CPU writes on. Pinned by REU/rmw-trigger. A fetch
163
+ # requested on the cycle before BA falls writes its first byte on the
164
+ # first BA-low cycle as well, and again when it starts. Pinned by
165
+ # REU/badoublewrite.
166
+ def dma_cycle!(ba_low)
167
+ late = ba_low && @vic.reu_ba_late?
161
168
  if @requested
162
- return if cpu_writing || ba_low
169
+ return hold_off if ba_low && !late
163
170
 
164
171
  start_transfer
165
172
  end
166
- @dma.cycle!(ba_low)
173
+ @dma.cycle!(ba_low, late, ba_low && @vic.reu_ba_handed_on?)
167
174
  end_transfer unless @dma.holds_bus?
168
175
  end
169
176
 
@@ -235,9 +242,15 @@ module Badline
235
242
 
236
243
  def request_bus
237
244
  @requested = true
245
+ @ba_was_low = @vic.ba_low?
238
246
  @on_dma&.call
239
247
  end
240
248
 
249
+ def hold_off
250
+ @dma.write_ahead(@command & TRANSFER_TYPE, @c64, @expansion | (@bank << 16)) unless @ba_was_low
251
+ @ba_was_low = true
252
+ end
253
+
241
254
  def start_transfer
242
255
  @requested = false
243
256
  @running = true
data/lib/badline/sdl.rb CHANGED
@@ -1,67 +1,139 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fiddle"
4
-
5
3
  module Badline
6
- # The libSDL2 calls the window, the gamepads and the audio sink make,
7
- # bound through Fiddle, so nothing has to compile against SDL at install
8
- # time. Only the library itself has to be present.
9
- #
10
- # Each function is a constant named after its C name without the prefix:
11
- # SDL::RenderClear.call(renderer) calls SDL_RenderClear. They're built
12
- # with need_gvl, which skips releasing Ruby's lock around the call: none
13
- # of these block for long, and releasing it costs more than most of them.
4
+ # libSDL2 through Spinel's FFI DSL, the one binding both builds use. The
5
+ # native build compiles the declarations, and finds the library's
6
+ # directory (see NativeBuild.sdl2_flags). badline-ruby loads badline/ffi
7
+ # first, which implements them over Fiddle, so only the library itself
8
+ # has to be present.
14
9
  module SDL
15
- class Error < StandardError; end
10
+ ffi_lib "SDL2"
16
11
 
17
- # Where package managers put the shared library. dlopen searches the
18
- # system paths for the bare names, but not Homebrew's.
19
- LIBRARIES = %w[
20
- libSDL2-2.0.so.0 libSDL2.so
21
- libSDL2-2.0.0.dylib /opt/homebrew/lib/libSDL2-2.0.0.dylib /usr/local/lib/libSDL2-2.0.0.dylib
22
- SDL2.dll
23
- ].freeze
12
+ ffi_func :SDL_Init, [:uint32], :int
13
+ ffi_func :SDL_Quit, [], :void
14
+ ffi_func :SDL_GetError, [], :str
15
+ ffi_func :SDL_SetHint, %i[str str], :int
16
+ ffi_func :SDL_CreateWindow, %i[str int int int int uint32], :ptr
17
+ ffi_func :SDL_SetWindowTitle, %i[ptr str], :void
18
+ ffi_func :SDL_DestroyWindow, [:ptr], :void
19
+ ffi_func :SDL_CreateRenderer, %i[ptr int uint32], :ptr
20
+ ffi_func :SDL_GetWindowDisplayMode, %i[ptr ptr], :int
21
+ ffi_func :SDL_DestroyRenderer, [:ptr], :void
22
+ ffi_func :SDL_RenderSetLogicalSize, %i[ptr int int], :int
23
+ ffi_func :SDL_CreateTexture, %i[ptr uint32 int int int], :ptr
24
+ ffi_func :SDL_DestroyTexture, [:ptr], :void
25
+ ffi_func :SDL_UpdateTexture, %i[ptr ptr int_array int], :int
26
+ ffi_func :SDL_RenderClear, [:ptr], :int
27
+ ffi_func :SDL_SetRenderDrawColor, %i[ptr int int int int], :int
28
+ ffi_func :SDL_RenderFillRect, %i[ptr ptr], :int
29
+ ffi_func :SDL_RenderCopy, %i[ptr ptr ptr ptr], :int
30
+ ffi_func :SDL_RenderPresent, [:ptr], :void
31
+ ffi_func :SDL_PollEvent, [:ptr], :int
32
+ ffi_func :SDL_SetRelativeMouseMode, [:int], :int
33
+ ffi_func :SDL_Delay, [:uint32], :void
34
+ ffi_func :SDL_GetRendererOutputSize, %i[ptr ptr ptr], :int
35
+ ffi_func :SDL_RenderReadPixels, %i[ptr ptr uint32 ptr int], :int
36
+ ffi_func :SDL_CreateRGBSurfaceWithFormatFrom, %i[ptr int int int int uint32], :ptr
37
+ ffi_func :SDL_FreeSurface, [:ptr], :void
38
+ ffi_func :SDL_RWFromFile, %i[str str], :ptr
39
+ ffi_func :SDL_SaveBMP_RW, %i[ptr ptr int], :int
40
+ ffi_func :SDL_InitSubSystem, [:uint32], :int
41
+ ffi_func :SDL_QuitSubSystem, [:uint32], :void
42
+ ffi_func :SDL_OpenAudioDevice, %i[ptr int ptr ptr int], :uint32
43
+ ffi_func :SDL_CloseAudioDevice, [:uint32], :void
44
+ ffi_func :SDL_PauseAudioDevice, %i[uint32 int], :void
45
+ ffi_func :SDL_QueueAudio, %i[uint32 buffer_in uint32], :int
46
+ ffi_func :SDL_GetQueuedAudioSize, [:uint32], :uint32
47
+ ffi_func :SDL_NumJoysticks, [], :int
48
+ ffi_func :SDL_IsGameController, [:int], :int
49
+ ffi_func :SDL_GameControllerOpen, [:int], :ptr
50
+ ffi_func :SDL_GameControllerClose, [:ptr], :void
51
+ ffi_func :SDL_GameControllerName, [:ptr], :str
52
+ ffi_func :SDL_GameControllerGetButton, %i[ptr int], :uint8
53
+ ffi_func :SDL_GameControllerGetAxis, %i[ptr int], :int16
54
+ ffi_func :SDL_ClearQueuedAudio, [:uint32], :void
55
+ ffi_func :SDL_GetCurrentDisplayMode, %i[int ptr], :int
56
+ ffi_func :SDL_CreateRGBSurfaceFrom, %i[ptr int int int int uint32 uint32 uint32 uint32], :ptr
57
+ ffi_func :SDL_CreateTextureFromSurface, %i[ptr ptr], :ptr
58
+ ffi_func :SDL_PushEvent, [:ptr], :int
59
+ ffi_func :SDL_GetKeyName, [:int], :str
24
60
 
25
- def self.open_library
26
- LIBRARIES.each do |name|
27
- return Fiddle.dlopen(name)
28
- rescue Fiddle::DLError
29
- next
30
- end
31
- raise Error, "can't find libSDL2 (tried #{LIBRARIES.join(', ')})"
32
- end
61
+ ffi_buffer :event, 56
62
+ ffi_read_u32 :event_type, 0
63
+ ffi_read_u8 :event_repeat, 13
64
+ ffi_read_i32 :event_scancode, 16
65
+ ffi_read_i32 :event_sym, 20
66
+ ffi_read_u16 :event_mod, 24
67
+ # SDL_MouseMotionEvent's xrel and yrel, and SDL_MouseButtonEvent's
68
+ # button.
69
+ ffi_read_i32 :event_xrel, 28
70
+ ffi_read_i32 :event_yrel, 32
71
+ ffi_read_u8 :event_button, 16
33
72
 
34
- LIBRARY = open_library
73
+ ffi_buffer :rect, 16
74
+ ffi_write_i32 :rect_x, 0
75
+ ffi_write_i32 :rect_y, 4
76
+ ffi_write_i32 :rect_w, 8
77
+ ffi_write_i32 :rect_h, 12
78
+ # Where the drive LED goes.
79
+ ffi_buffer :led_rect, 16
35
80
 
36
- VOID = Fiddle::TYPE_VOID
37
- INT = Fiddle::TYPE_INT
38
- UINT8 = Fiddle::TYPE_UINT8_T
39
- INT16 = Fiddle::TYPE_INT16_T
40
- UINT32 = Fiddle::TYPE_UINT32_T
41
- POINTER = Fiddle::TYPE_VOIDP
42
- STRING = Fiddle::TYPE_CONST_STRING
81
+ # SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
82
+ ffi_buffer :display_mode, 24
83
+ ffi_read_i32 :mode_refresh, 12
43
84
 
44
- def self.function(name, arguments, result)
45
- Fiddle::Function.new(LIBRARY[name], arguments, result, name:, need_gvl: true)
46
- end
85
+ ffi_buffer :output_w, 4
86
+ ffi_buffer :output_h, 4
87
+ ffi_read_i32 :read_i32, 0
47
88
 
48
- def self.error = GetError.call
89
+ # SDL_AudioSpec: int freq; Uint16 format; Uint8 channels, silence;
90
+ # Uint16 samples, padding; Uint32 size; then two pointers.
91
+ ffi_buffer :wanted, 32
92
+ ffi_buffer :obtained, 32
93
+ ffi_write_i32 :spec_freq, 0
94
+ ffi_write_u16 :spec_format, 4
95
+ ffi_write_u8 :spec_channels, 6
96
+ ffi_write_u16 :spec_samples, 8
49
97
 
50
- # A negative result from SDL is an error, and SDL_GetError says which.
51
- def self.check(result)
52
- raise Error, error if result.negative?
98
+ ffi_const :INIT_AUDIO, 0x10
99
+ ffi_const :INIT_VIDEO, 0x20
100
+ ffi_const :INIT_GAMECONTROLLER, 0x2000
101
+ ffi_const :INIT_EVENTS, 0x4000
102
+ ffi_const :WINDOWPOS_CENTERED, 0x2fff0000
103
+ ffi_const :WINDOW_RESIZABLE, 0x20
104
+ ffi_const :RENDERER_ACCELERATED, 0x02
105
+ ffi_const :RENDERER_PRESENTVSYNC, 0x04
106
+ ffi_const :PIXELFORMAT_RGB888, 0x16161804
107
+ ffi_const :TEXTUREACCESS_STREAMING, 1
108
+ ffi_const :QUIT, 0x100
109
+ ffi_const :KEYDOWN, 0x300
110
+ ffi_const :KEYUP, 0x301
111
+ ffi_const :MOUSEMOTION, 0x400
112
+ ffi_const :MOUSEBUTTONDOWN, 0x401
113
+ ffi_const :MOUSEBUTTONUP, 0x402
114
+ ffi_const :KMOD_SHIFT, 0x0003
115
+ ffi_const :CONTROLLERDEVICEADDED, 0x653
116
+ ffi_const :CONTROLLERDEVICEREMOVED, 0x654
117
+ ffi_const :CONTROLLERDEVICEREMAPPED, 0x655
118
+ ffi_const :AUDIO_S16LSB, 0x8010
119
+ ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
120
+ ffi_const :KEY_TAB, 0x09
121
+ ffi_const :KEY_F10, 0x4000_0043
122
+ ffi_const :KEY_F11, 0x4000_0044
123
+ ffi_const :KEY_F12, 0x4000_0045
124
+ end
53
125
 
54
- result
55
- end
126
+ # The few C library calls the native front end makes.
127
+ module LibC
128
+ ffi_func :malloc, [:size_t], :ptr
129
+ ffi_func :free, [:ptr], :void
130
+ ffi_func :poll, %i[ptr size_t int], :int
56
131
 
57
- # So is a null handle.
58
- def self.check_pointer(pointer)
59
- raise Error, error if pointer.null?
132
+ ffi_const :POLLIN, 0x01
60
133
 
61
- pointer
62
- end
134
+ # struct pollfd: int fd; short events, revents.
135
+ ffi_buffer :pollfd, 8
136
+ ffi_write_i32 :pollfd_fd, 0
137
+ ffi_write_i16 :pollfd_events, 4
63
138
  end
64
139
  end
65
-
66
- require "badline/sdl/functions"
67
- require "badline/sdl/events"
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class SID
5
+ class Envelope
6
+ # A snapshot of an envelope: its rates, the gate, the state and the
7
+ # counters and pipelines between them, and the same fields as reSID
8
+ # keeps them, for VICE's SIDEXTENDED module.
9
+ module State
10
+ # The fields reSID keeps for an envelope between cycles: the rate and
11
+ # exponential counters, the level, the state as an index into STATES,
12
+ # whether it holds at zero, the two periods and the pipeline.
13
+ def resid_fields
14
+ [@rate_counter, @exponential_counter, @counter, STATES.index(@state).to_i, @hold_zero ? 1 : 0,
15
+ @rate_period, @exponential_period, @envelope_pipeline]
16
+ end
17
+
18
+ # Takes the fields resid_fields gives from `fields`, from `at` on.
19
+ def restore_resid_fields(fields, at)
20
+ @rate_counter = fields[at]
21
+ @exponential_counter = fields[at + 1]
22
+ @counter = @env3 = fields[at + 2]
23
+ state = fields[at + 3]
24
+ @state = @next_state = STATES.fetch(state.clamp(0, STATES.length - 1)).to_sym
25
+ @hold_zero = fields[at + 4] != 0
26
+ @rate_period = fields[at + 5]
27
+ @exponential_period = fields[at + 6]
28
+ @envelope_pipeline = fields[at + 7]
29
+ end
30
+
31
+ def save_state(out)
32
+ [@attack, @decay, @sustain, @release].each { |rate| out.int(rate) }
33
+ out.boolean(@gate).int(STATES.index(@state)).int(STATES.index(@next_state))
34
+ out.int(@counter).int(@env3).int(@rate_counter).int(@rate_period).boolean(@reset_rate_counter)
35
+ out.int(@exponential_counter).int(@exponential_period)
36
+ out.int(@state_pipeline).int(@envelope_pipeline).int(@exponential_pipeline).boolean(@hold_zero)
37
+ end
38
+
39
+ def load_state(input)
40
+ @attack = input.int
41
+ @decay = input.int
42
+ @sustain = input.int
43
+ @release = input.int
44
+ @gate = input.boolean?
45
+ @state = STATES.fetch(input.int).to_sym
46
+ @next_state = STATES.fetch(input.int).to_sym
47
+ load_counters(input)
48
+ end
49
+
50
+ private
51
+
52
+ def load_counters(input)
53
+ @counter = input.int
54
+ @env3 = input.int
55
+ @rate_counter = input.int
56
+ @rate_period = input.int
57
+ @reset_rate_counter = input.boolean?
58
+ @exponential_counter = input.int
59
+ @exponential_period = input.int
60
+ @state_pipeline = input.int
61
+ @envelope_pipeline = input.int
62
+ @exponential_pipeline = input.int
63
+ @hold_zero = input.boolean?
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/sid/envelope/state"
4
+
3
5
  module Badline
4
6
  class SID
5
7
  # ADSR envelope generator.
@@ -25,6 +27,11 @@ module Badline
25
27
  EXPONENTIAL_PERIODS = { 0xff => 1, 0x5d => 2, 0x36 => 4, 0x1a => 8,
26
28
  0x0e => 16, 0x06 => 30, 0x00 => 1 }.freeze
27
29
 
30
+ # The states a snapshot names by index.
31
+ STATES = %i[attack decay_sustain release].freeze
32
+
33
+ include State
34
+
28
35
  attr_reader :state, :counter, :env3
29
36
 
30
37
  def initialize
@@ -90,6 +90,16 @@ module Badline
90
90
  @output = 0
91
91
  end
92
92
 
93
+ def save_state(out)
94
+ out.int(@lowpass).int(@highpass).int(@output)
95
+ end
96
+
97
+ def load_state(input)
98
+ @lowpass = input.int
99
+ @highpass = input.int
100
+ @output = input.int
101
+ end
102
+
93
103
  # Takes the input in 1/2^Filter::FRACTION units, and reads the
94
104
  # output out in whole ones.
95
105
  def cycle!(input, cycles = 1)
@@ -124,6 +134,25 @@ module Badline
124
134
  @input = @unfiltered = 0
125
135
  end
126
136
 
137
+ # The registers as the filter decoded them and its integrators. The
138
+ # W0 table is the model's.
139
+ def save_state(out)
140
+ out.int(@cutoff).int(@routing).int(@mode).int(@volume).boolean(@voice3_off).int(@w0).int(@resonance)
141
+ [@highpass, @bandpass, @lowpass, @unfiltered, @input].each { |value| out.int(value) }
142
+ @external.save_state(out)
143
+ end
144
+
145
+ def load_state(input)
146
+ @cutoff = input.int
147
+ @routing = input.int
148
+ @mode = input.int
149
+ @volume = input.int
150
+ @voice3_off = input.boolean?
151
+ @w0 = input.int
152
+ @resonance = input.int
153
+ load_integrators(input)
154
+ end
155
+
127
156
  def write(reg, value)
128
157
  case reg
129
158
  when 0x15 then self.cutoff = (@cutoff & 0x7f8) | (value & 0x07)
@@ -163,6 +192,15 @@ module Badline
163
192
 
164
193
  private
165
194
 
195
+ def load_integrators(input)
196
+ @highpass = input.int
197
+ @bandpass = input.int
198
+ @lowpass = input.int
199
+ @unfiltered = input.int
200
+ @input = input.int
201
+ @external.load_state(input)
202
+ end
203
+
166
204
  def write_resonance(value)
167
205
  @routing = value & 0x0f
168
206
  @resonance = RESONANCE[(value >> 4) & 0x0f]
@@ -38,6 +38,16 @@ module Badline
38
38
  @envelope = Envelope.new
39
39
  end
40
40
 
41
+ def save_state(out)
42
+ @waveform.save_state(out)
43
+ @envelope.save_state(out)
44
+ end
45
+
46
+ def load_state(input)
47
+ @waveform.load_state(input)
48
+ @envelope.load_state(input)
49
+ end
50
+
41
51
  def control=(value)
42
52
  @waveform.control = value
43
53
  @envelope.control = value
@@ -20,11 +20,12 @@ module Badline
20
20
  # so they lose a different number of bits. The 8580's pair is the one
21
21
  # that reproduces SID/wf12nsr's pulse+noise row (it reads $f8, and the
22
22
  # writeback leaves $fc behind), and SID/wb_testsuite's C->9 and C->E
23
- # rows need that same $fc written at release. On the 6581 the read is
24
- # the $fc of SID/wf12nsr's readme (VICE bug #1037), and the writeback
25
- # pulls nothing: wb_testsuite's 8/9/A/B->C rows run pulse+noise and
26
- # leave the register alone.
27
- PULSE_NOISE_READ = { mos6581: 0xfc0, mos8580: 0xf80 }.freeze
23
+ # rows need that same $fc written at release. The 6581 reads $00 of a
24
+ # full register (SID/wf12nsr's pulse+noise row) and pulls nothing
25
+ # while the register holds: wb_testsuite's 8->C and C->C rows run
26
+ # pulse+noise and leave the register alone. Its lines only land
27
+ # part way through a shift (#write_back_shift).
28
+ PULSE_NOISE_READ = { mos6581: 0x000, mos8580: 0xf80 }.freeze
28
29
  PULSE_NOISE_WRITE = { mos6581: 0xff0, mos8580: 0xfc0 }.freeze
29
30
 
30
31
  # What a 6581 writes back when the test bit falls with only pulse+noise
@@ -40,24 +41,45 @@ module Badline
40
41
  # (SID/wf12nsr's noise+triangle and noise+sawtooth rows).
41
42
  def write_back(selected, value)
42
43
  return value & ((value << 1) | (value >> 1)) unless selected == 0xc
43
- return value if value.zero?
44
+ return 0x000 if @pulse.zero?
44
45
 
45
46
  noise & @pulse_noise_write
46
47
  end
47
48
 
49
+ # What the lines write while the register is part way through a
50
+ # shift, where the 6581's pulse+noise writes its output as read.
51
+ def write_back_mid_shift(selected, value)
52
+ return value if pulse_noise_mid_shift?(selected)
53
+
54
+ write_back(selected, value)
55
+ end
56
+
57
+ def pulse_noise_mid_shift?(selected) = @topbit_feedback && selected == 0xc
58
+
48
59
  def write_shift_register(value)
49
60
  NOISE_TAPS.each { |bit, line| @shift_register &= ~bit if value.nobits?(line) }
50
61
  end
51
62
 
52
- # Setting the test bit starts the bleed, and writes the output of a
53
- # noise combination back first, from the accumulator it had. Only
54
- # SID/noiselfsrinit sets the bit onto such a combination from outside
55
- # it: its $f8/$80 pairs go from noise alone to all four.
63
+ # The second phase of a shift writes back by the test bit release
64
+ # rule with the waveform left alone. The 6581's pulse+noise, which
65
+ # that rule leaves out, writes its lines too (SID/wf12nsr's
66
+ # pulse+noise row).
67
+ def write_back_shift
68
+ return unless pulse_noise_mid_shift?(@selected) || release_writes_back?(@selected, @selected)
69
+
70
+ write_shift_register(write_back_mid_shift(@selected, @output))
71
+ end
72
+
73
+ # Setting the test bit starts the bleed, and stalls the register part
74
+ # way through a shift, where a noise combination writes its output
75
+ # back first, from the accumulator it had. SID/noiselfsrinit's
76
+ # $f8/$80 pairs set the bit onto all four from noise alone, and
77
+ # SID/wf12nsr's pulse+noise row sets it onto pulse+noise.
56
78
  def raise_test
57
79
  @shift_register_reset = @shift_register_reset_delay
58
80
  return unless @selected.anybits?(0x8) && combined?(@selected) && @shift_pipeline != 1
59
81
 
60
- write_shift_register(write_back(@selected, shape(@selected)))
82
+ write_shift_register(write_back_mid_shift(@selected, shape(@selected)))
61
83
  end
62
84
 
63
85
  # Releasing the test bit finishes the shift it interrupted: the old
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class SID
5
+ class Waveform
6
+ # A snapshot of an oscillator: its phase, the noise register and the
7
+ # output latch, and the same fields as reSID keeps them, for VICE's
8
+ # SIDEXTENDED module. The model's tables and the voices it syncs with
9
+ # are the SID's wiring.
10
+ module State
11
+ # The fields reSID keeps for an oscillator between cycles: the phase,
12
+ # the noise register, its shift pipeline and reset countdown, the
13
+ # floating output's countdown and the pulse level.
14
+ def resid_fields
15
+ [@accumulator, @shift_register, @shift_pipeline, @shift_register_reset, @floating_ttl, @pulse]
16
+ end
17
+
18
+ # Takes the fields resid_fields gives from `fields`, from `at` on.
19
+ def restore_resid_fields(fields, at)
20
+ @accumulator = fields[at]
21
+ @shift_register = fields[at + 1]
22
+ @shift_pipeline = fields[at + 2]
23
+ @shift_register_reset = fields[at + 3]
24
+ @floating_ttl = fields[at + 4]
25
+ @pulse = fields[at + 5]
26
+ @stale = true
27
+ end
28
+
29
+ def save_state(out)
30
+ out.int(@accumulator).int(@delayed_triangle).int(@delayed_sawtooth)
31
+ out.int(@shift_register).int(@shift_register_reset).int(@shift_pipeline)
32
+ out.int(@frequency).int(@pulse_width).int(@selected)
33
+ [@test, @ring, @sync, @msb_rising].each { |flag| out.boolean(flag) }
34
+ out.int(@floating).int(@floating_ttl).int(@pulse).int(@output).boolean(@stale)
35
+ end
36
+
37
+ def load_state(input)
38
+ @accumulator = input.int
39
+ @delayed_triangle = input.int
40
+ @delayed_sawtooth = input.int
41
+ @shift_register = input.int
42
+ @shift_register_reset = input.int
43
+ @shift_pipeline = input.int
44
+ @frequency = input.int
45
+ @pulse_width = input.int
46
+ @selected = input.int
47
+ load_latches(input)
48
+ end
49
+
50
+ private
51
+
52
+ def load_latches(input)
53
+ @test = input.boolean?
54
+ @ring = input.boolean?
55
+ @sync = input.boolean?
56
+ @msb_rising = input.boolean?
57
+ @floating = input.int
58
+ @floating_ttl = input.int
59
+ @pulse = input.int
60
+ @output = input.int
61
+ @stale = input.boolean?
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,6 +3,7 @@
3
3
  require "badline/sid/waveform/combined"
4
4
  require "badline/sid/waveform/fast_forward"
5
5
  require "badline/sid/waveform/noise_writeback"
6
+ require "badline/sid/waveform/state"
6
7
 
7
8
  module Badline
8
9
  class SID
@@ -27,6 +28,7 @@ module Badline
27
28
  class Waveform
28
29
  include FastForward
29
30
  include NoiseWriteback
31
+ include State
30
32
 
31
33
  # All bits high at power up, odd ones stored inverted (SID/oscinit).
32
34
  POWER_ON_ACCUMULATOR = 0x555555
@@ -254,8 +256,7 @@ module Badline
254
256
  # A rise of bit 19 starts a shift that takes two more cycles
255
257
  # (SID/noisewriteback's noise_writeback_test2, after libresidfp's shift
256
258
  # pipeline). The first phase latches the output, and the second writes
257
- # it over the taps, by the test bit release rule with the waveform left
258
- # alone, before the register shifts.
259
+ # it over the taps before the register shifts.
259
260
  def advance
260
261
  previous = @accumulator
261
262
  @accumulator = (previous + @frequency) & 0xffffff
@@ -263,7 +264,7 @@ module Badline
263
264
  if @accumulator & ~previous & 0x080000 != 0
264
265
  @shift_pipeline = 2
265
266
  elsif @shift_pipeline != 0 && (@shift_pipeline -= 1).zero?
266
- write_shift_register(write_back(@selected, @output)) if release_writes_back?(@selected, @selected)
267
+ write_back_shift
267
268
  shift_noise
268
269
  end
269
270
  end