badline 0.11.1 → 0.12.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/c128/bus.rb +4 -17
  10. data/lib/badline/c128/drives.rb +35 -22
  11. data/lib/badline/c128/fast_serial.rb +1 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +9 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +22 -0
  16. data/lib/badline/c128/z80.rb +49 -0
  17. data/lib/badline/c128/z80_pages.rb +118 -0
  18. data/lib/badline/c128/z80_turns.rb +106 -0
  19. data/lib/badline/c128.rb +16 -19
  20. data/lib/badline/cartridge/action_replay.rb +2 -2
  21. data/lib/badline/cartridge/comal80.rb +1 -7
  22. data/lib/badline/cartridge/dinamic.rb +0 -8
  23. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  24. data/lib/badline/cartridge/fun_play.rb +0 -8
  25. data/lib/badline/cartridge/game_system.rb +0 -8
  26. data/lib/badline/cartridge/kcs_power.rb +10 -7
  27. data/lib/badline/cartridge/mach5.rb +2 -4
  28. data/lib/badline/cartridge/magic_desk.rb +0 -8
  29. data/lib/badline/cartridge/ocean.rb +0 -8
  30. data/lib/badline/cartridge/pagefox.rb +2 -2
  31. data/lib/badline/cartridge/retro_replay.rb +7 -6
  32. data/lib/badline/cartridge/rex_utility.rb +2 -6
  33. data/lib/badline/cartridge/rgcd.rb +2 -2
  34. data/lib/badline/cartridge/simons_basic.rb +0 -5
  35. data/lib/badline/cartridge/super_games.rb +4 -2
  36. data/lib/badline/cartridge/westermann.rb +2 -6
  37. data/lib/badline/cartridge.rb +37 -9
  38. data/lib/badline/cia/fast_forward.rb +21 -3
  39. data/lib/badline/cia.rb +8 -5
  40. data/lib/badline/computer/attachments.rb +4 -56
  41. data/lib/badline/computer/true_drives.rb +80 -0
  42. data/lib/badline/computer.rb +4 -0
  43. data/lib/badline/core.rb +2 -0
  44. data/lib/badline/drive/core.rb +8 -33
  45. data/lib/badline/drive/fast_serial.rb +73 -0
  46. data/lib/badline/drive/idle.rb +1 -11
  47. data/lib/badline/drive/orbit.rb +2 -5
  48. data/lib/badline/drive/sleep.rb +0 -2
  49. data/lib/badline/drive/vias.rb +84 -0
  50. data/lib/badline/drive.rb +2 -0
  51. data/lib/badline/drive1541.rb +1 -0
  52. data/lib/badline/drive1571/wd1770.rb +94 -8
  53. data/lib/badline/drive1571.rb +8 -58
  54. data/lib/badline/drive1581/bus.rb +137 -0
  55. data/lib/badline/drive1581/cpu.rb +11 -0
  56. data/lib/badline/drive1581/disk.rb +135 -0
  57. data/lib/badline/drive1581/disk_state.rb +87 -0
  58. data/lib/badline/drive1581/mechanism.rb +117 -0
  59. data/lib/badline/drive1581/serial_port.rb +52 -0
  60. data/lib/badline/drive1581/slot.rb +78 -0
  61. data/lib/badline/drive1581/track.rb +100 -0
  62. data/lib/badline/drive1581/track_bytes.rb +75 -0
  63. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  64. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  65. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  66. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  67. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  68. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  69. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  70. data/lib/badline/drive1581/wd1772.rb +216 -0
  71. data/lib/badline/drive1581.rb +233 -0
  72. data/lib/badline/frontend/app.rb +1 -3
  73. data/lib/badline/frontend/audio_device.rb +67 -0
  74. data/lib/badline/frontend/audio_sink.rb +16 -50
  75. data/lib/badline/frontend/boot.rb +18 -10
  76. data/lib/badline/frontend/buttons.rb +7 -7
  77. data/lib/badline/frontend/keys.rb +33 -8
  78. data/lib/badline/frontend/media_slots.rb +42 -0
  79. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  80. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  81. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  82. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  83. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  84. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  85. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  86. data/lib/badline/frontend/menu/theme.rb +18 -0
  87. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  88. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  89. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  90. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  91. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  92. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  93. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  94. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  95. data/lib/badline/frontend/player/theme.rb +18 -0
  96. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  97. data/lib/badline/frontend/sound.rb +19 -38
  98. data/lib/badline/frontend/timeline.rb +3 -14
  99. data/lib/badline/frontend.rb +24 -20
  100. data/lib/badline/iec_bus.rb +14 -2
  101. data/lib/badline/machine.rb +5 -4
  102. data/lib/badline/media/boot_disk.rb +50 -0
  103. data/lib/badline/media/not_disk.rb +8 -0
  104. data/lib/badline/media/true_drive.rb +35 -15
  105. data/lib/badline/media.rb +9 -3
  106. data/lib/badline/options/event.rb +2 -2
  107. data/lib/badline/options/family.rb +3 -9
  108. data/lib/badline/options/help.rb +2 -1
  109. data/lib/badline/roms/README +9 -0
  110. data/lib/badline/roms/dos1581.rom +0 -0
  111. data/lib/badline/sdl.rb +37 -20
  112. data/lib/badline/snapshot/state.rb +5 -4
  113. data/lib/badline/storage/p00.rb +8 -0
  114. data/lib/badline/time_of_day.rb +3 -3
  115. data/lib/badline/version.rb +1 -1
  116. data/lib/badline/vic20/attachments.rb +21 -7
  117. data/lib/badline/vic20/saved_state.rb +2 -0
  118. data/lib/badline/vic20.rb +3 -0
  119. data/lib/badline/z80/core.rb +2 -0
  120. data/lib/badline/z80/saved_state.rb +69 -0
  121. metadata +54 -21
  122. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  123. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  124. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  125. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drive1581/wd1772/registers"
4
+ require "badline/drive1581/wd1772/turn"
5
+ require "badline/drive1581/wd1772/stepping"
6
+ require "badline/drive1581/wd1772/sectors"
7
+ require "badline/drive1581/wd1772/sector_writes"
8
+ require "badline/drive1581/wd1772/tracks"
9
+ require "badline/drive1581/wd1772/state"
10
+
11
+ module Badline
12
+ class Drive1581
13
+ # The WD1772 floppy disk controller at $6000, as the WD177x data sheet
14
+ # describes it, working on the Mechanism's Track at sector level: it
15
+ # finds ID and data fields where they lie round the turn and moves
16
+ # their bytes at the MFM double-density rate, a byte every 32 µs, but
17
+ # keeps no flux or bitstream.
18
+ #
19
+ # $6000 status (read), command (write)
20
+ # $6001 track
21
+ # $6002 sector
22
+ # $6003 data
23
+ #
24
+ # It runs on the drive's clock, 2 MHz here, so the data sheet's times,
25
+ # given for 8 MHz on the chip's clock pin, come out as BYTE cycles a
26
+ # byte, STEP_RATES between steps and SETTLE for the head to settle.
27
+ #
28
+ # A command runs as a chain of phases, each due at a cycle (@due): a
29
+ # step, an index pulse, the end of a byte under the head. Between them
30
+ # nothing happens, so cycle! only counts. BUSY sets as the command is
31
+ # written, and the chip takes it up START cycles later, 16 µs, so a
32
+ # command with nothing to do, such as a seek to the track the head is
33
+ # on, still shows BUSY to the DOS, which polls for it before waiting
34
+ # for it to clear.
35
+ #
36
+ # The 1581 leaves INTRQ and DRQ unconnected and polls the status
37
+ # register instead (schematic sheet 2), but both are here (intrq?,
38
+ # drq?). Its DOS sets h in every command, so the motor spin-up wait
39
+ # never runs; the spindle follows the 8520's /MOTOR line instead.
40
+ class WD1772
41
+ include Registers
42
+ include Turn
43
+ include Stepping
44
+ include Sectors
45
+ include SectorWrites
46
+ include Tracks
47
+ include State
48
+
49
+ BYTE = 64
50
+ START = 32
51
+ STEP_RATES = [12_000, 24_000, 4_000, 6_000].freeze
52
+ SETTLE = 30_000
53
+
54
+ NEVER = 1 << 62
55
+
56
+ # Status bits. Bits 1 and 2 report the index pulse and track 0 after
57
+ # a type I command, and DRQ and lost data after the others; bit 5
58
+ # reports spin-up after a type I command and a deleted data mark
59
+ # after a read.
60
+ BUSY = 0x01
61
+ INDEX = 0x02
62
+ DATA_REQUEST = 0x02
63
+ TRACK0 = 0x04
64
+ LOST_DATA = 0x04
65
+ CRC_ERROR = 0x08
66
+ NOT_FOUND = 0x10
67
+ SPUN_UP = 0x20
68
+ DELETED = 0x20
69
+ PROTECTED = 0x40
70
+ MOTOR_ON = 0x80
71
+
72
+ # Command flags.
73
+ NO_SPIN_UP = 0x08
74
+ VERIFY = 0x04
75
+ SETTLE_DELAY = 0x04
76
+ MULTIPLE = 0x10
77
+ UPDATE = 0x10
78
+ DELETED_MARK = 0x01
79
+
80
+ # Phases.
81
+ IDLE = 0
82
+ SPIN_UP = 1
83
+ STEPPED = 2
84
+ SETTLED = 3
85
+ SEARCH = 4
86
+ READ_DATA = 5
87
+ WRITE_GATE = 6
88
+ WRITE_DATA = 7
89
+ READ_TRACK = 8
90
+ TRACK_INDEX = 9
91
+ WRITE_TRACK = 10
92
+ MOTOR_IDLE = 11
93
+ INDEX_INTERRUPT = 12
94
+ SETTLE_SEARCH = 13
95
+ READ_ADDRESS = 14
96
+ WRITE_TRACK_START = 15
97
+ STARTING = 16
98
+
99
+ # What a search for an ID field is for.
100
+ READ = 0
101
+ WRITE = 1
102
+ ADDRESS = 2
103
+ VERIFY_TRACK = 3
104
+
105
+ attr_reader :now, :track, :sector, :data, :command
106
+
107
+ def initialize(mechanism)
108
+ @mechanism = mechanism
109
+ @now = 0
110
+ @due = NEVER
111
+ @phase = IDLE
112
+ reset!
113
+ end
114
+
115
+ # The MR line: the registers as the data sheet leaves them, $03 in
116
+ # the command register and 1 in the sector register, and no command
117
+ # running.
118
+ def reset!
119
+ @command = 0x03
120
+ @track = 0
121
+ @sector = 1
122
+ @data = 0
123
+ @status = 0
124
+ @type1 = true
125
+ @drq = false
126
+ @intrq = false
127
+ @motor_on = false
128
+ @inward = true
129
+ clear_command
130
+ idle
131
+ end
132
+
133
+ def cycle!
134
+ @now += 1
135
+ advance if @now == @due
136
+ end
137
+
138
+ # How many cycles can go by before the next phase is due.
139
+ def quiet_cycles = @due - @now - 1
140
+
141
+ # Runs +cycles+ cycles at once, as many as quiet_cycles allows.
142
+ def fast_forward(cycles)
143
+ @now += cycles
144
+ end
145
+
146
+ def busy? = @phase != IDLE && @phase != MOTOR_IDLE && @phase != INDEX_INTERRUPT
147
+
148
+ def drq? = @drq
149
+
150
+ def intrq? = @intrq
151
+
152
+ def motor_on? = @motor_on
153
+
154
+ private
155
+
156
+ # A command ends: INTRQ, and the motor turns off after nine more
157
+ # index pulses unless a command comes first.
158
+ def finish
159
+ @intrq = true
160
+ @phase = MOTOR_IDLE
161
+ @count = 9
162
+ count_index
163
+ end
164
+
165
+ # What a command keeps between its phases.
166
+ def clear_command
167
+ @steps = 0
168
+ @count = 0
169
+ @after_spin_up = IDLE
170
+ @mode = 0
171
+ @deadline = NEVER
172
+ @id_at = NEVER
173
+ @found = -1
174
+ @bytes = []
175
+ @syncs = []
176
+ @index = 0
177
+ @crc = 0xffff
178
+ @crc_good = true
179
+ @write_id = 0
180
+ @size = 0
181
+ @gate_open = false
182
+ @late = false
183
+ @settling = false
184
+ end
185
+
186
+ def idle
187
+ @phase = IDLE
188
+ @due = NEVER
189
+ end
190
+
191
+ def advance
192
+ case @phase
193
+ when STARTING then start_command
194
+ when SPIN_UP, MOTOR_IDLE, INDEX_INTERRUPT then index_counted
195
+ when STEPPED then step_done
196
+ when SETTLED then verify_track
197
+ when SETTLE_SEARCH then begin_search
198
+ when SEARCH then id_passed
199
+ when READ_DATA, READ_ADDRESS then byte_read
200
+ else advance_write
201
+ end
202
+ end
203
+
204
+ def advance_write
205
+ case @phase
206
+ when WRITE_GATE then write_gate
207
+ when WRITE_DATA then byte_written
208
+ when READ_TRACK then track_byte_read
209
+ when TRACK_INDEX then track_index
210
+ when WRITE_TRACK_START then write_track_start
211
+ when WRITE_TRACK then track_byte_written
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drive"
4
+ require "badline/drive1581/track"
5
+ require "badline/drive1581/track_bytes"
6
+ require "badline/drive1581/disk"
7
+ require "badline/drive1581/mechanism"
8
+ require "badline/drive1581/wd1772"
9
+ require "badline/drive1581/cpu"
10
+ require "badline/drive1581/serial_port"
11
+ require "badline/drive1581/bus"
12
+
13
+ module Badline
14
+ # The 1581 3.5" disk drive: a 6502 at 2 MHz with 8 KB of RAM, an 8520
15
+ # for the serial bus and the mechanism's lines, a WD1772 for the MFM
16
+ # disk, and the DOS in 32 KB of ROM (1581 Service Manual). Drive::Core
17
+ # clocks it against the host.
18
+ #
19
+ # The 8520's port B faces the serial bus as VIA 1's does on the 1541,
20
+ # with the same bits, and ATN reaches its FLAG pin, so ATN going low
21
+ # interrupts the DOS. Its SP and CNT carry fast serial on DATA and SRQ,
22
+ # through buffers PB5 turns round (Drive::FastSerial), as the 1571's
23
+ # 6526 does. The ATN acknowledge differs: a NAND of ATN IN and
24
+ # ATN ACK pulls DATA, so DATA follows ATN only while ATN is asserted
25
+ # and PB4 is high, where the 1541's XOR pulls it whenever the two
26
+ # differ (IECBus::DRIVE_ATN_GATED). Port A drives the motor, the side
27
+ # and the two LEDs, and reads the switches and the mechanism's /RDY and
28
+ # /DISK CHNG (SerialPort). Timer B interrupts every 10 ms for the
29
+ # controller code that runs the WD1772.
30
+ #
31
+ # The 8520 differs from the 6526 in its TOD, a 24-bit binary event
32
+ # counter where the 6526 keeps a BCD clock. The 1581 holds the TOD pin
33
+ # high when it carries a WD1772 (jumper J1 out), so the counter never
34
+ # counts, and the DOS's one use of it, writing $00 to the low byte and
35
+ # reading it back 20 µs later to tell a WD1770 from a WD1772, reads $00
36
+ # on either chip: the drive's CIA gets no TOD pulses (CIA's tod_hz).
37
+ class Drive1581
38
+ include Drive::Core
39
+ include Drive::FastSerial
40
+
41
+ CLOCK_HZ = 2_000_000
42
+
43
+ # Where the DOS's idle loop starts over (see Drive::Idle).
44
+ IDLE_LOOP = 0xb105
45
+
46
+ # The host cycles, at about 1 MHz, by which a 1581 switched on with the
47
+ # machine has run its DOS's reset, the RAM test, the ROM checksum and
48
+ # the controller's half-second wait, and waits on the bus: 2.32 s,
49
+ # with a margin.
50
+ BOOT_CYCLES = 2_700_000
51
+
52
+ # The 8520's port A outputs.
53
+ SIDE = 0x01
54
+ MOTOR = 0x04
55
+ POWER_LED = 0x20
56
+ ACTIVITY_LED = 0x40
57
+
58
+ # Port B's ATN acknowledge, and the fast serial direction.
59
+ ATN_ACK = 0x10
60
+ FAST_SERIAL_OUT = 0x20
61
+
62
+ def cia
63
+ settle!
64
+ @cia
65
+ end
66
+
67
+ def fdc
68
+ settle!
69
+ @fdc
70
+ end
71
+
72
+ def model_name = "1581"
73
+
74
+ # +rom+ covers $8000-$FFFF, and defaults to the DOS image in the ROM
75
+ # path. +device+ is the number the switches on port A's PA3 and PA4
76
+ # set, 8 to 11.
77
+ def initialize(rom: nil, host_clock_hz: Drive1541::CLOCK_HZ, device: 8, debug: false)
78
+ @device = device
79
+ @clock_hz = CLOCK_HZ
80
+ @mechanism = Mechanism.new
81
+ @fdc = WD1772.new(@mechanism)
82
+ @serial_port = SerialPort.new(@mechanism, device:)
83
+ @cia = CIA.new(start: 0x4000, peripheral: @serial_port, tod_hz: 0)
84
+ @bus = Bus.new(rom: rom || ROM.load("dos1581.rom", 0x8000), cia: @cia, fdc: @fdc, drive: self)
85
+ @cpu = CPU.new(@bus, debug:)
86
+ @idle_loop = IDLE_LOOP
87
+ @host_clock_hz = host_clock_hz
88
+ @phase = 0
89
+ @cycles = 0
90
+ @serial_bus = nil
91
+ @atn = false
92
+ @activity = false
93
+ @power = false
94
+ init_fast_serial
95
+ init_idle(debug)
96
+ ports_written
97
+ connect(IECBus.new)
98
+ end
99
+
100
+ # The serial bus's RESET line reaches the CPU, the 8520 and the
101
+ # WD1772. RAM keeps its contents.
102
+ def reset!
103
+ settle!
104
+ @cia.reset!
105
+ @fdc.reset!
106
+ ports_written
107
+ @cpu.reset!
108
+ end
109
+
110
+ def insert(disk)
111
+ settle!
112
+ @mechanism.insert(disk, @fdc.now)
113
+ @fdc.spin_changed
114
+ end
115
+
116
+ # Puts in a Disk from the .d81 image at +path+, write-protected with
117
+ # `read_only`.
118
+ def insert_image(path, read_only: false)
119
+ insert(Disk.open(path, read_only:))
120
+ end
121
+
122
+ # Whether the activity LED is lit. The power LED stays lit, and
123
+ # flashes on an error.
124
+ def led_on? = @activity
125
+
126
+ def power_led_on? = @power
127
+
128
+ # The 8520's port B as it drives the serial bus, in VIA 1's bits, the
129
+ # ATN acknowledge turned round for the NAND that gates it, with the
130
+ # fast serial pins above it. It holds still while the drive sleeps, so
131
+ # reading it leaves the drive asleep.
132
+ def serial_output
133
+ lines = @cia.port_b_lines
134
+ output = (lines & (IECBus::DRIVE_DATA_OUT | IECBus::DRIVE_CLK_OUT)) | IECBus::DRIVE_ATN_GATED | @fast_output
135
+ lines.anybits?(ATN_ACK) ? output : output | IECBus::DRIVE_ATNA
136
+ end
137
+
138
+ # The 8520's ports drive the motor, the side, the LEDs and the fast
139
+ # serial buffers: the drive follows them after every write to a port
140
+ # or direction register.
141
+ def ports_written
142
+ fast_serial_out = @cia.port_b_lines.anybits?(FAST_SERIAL_OUT)
143
+ turn_fast_serial(fast_serial_out) if fast_serial_out != @fast_serial_out
144
+ lines = @cia.port_a_lines
145
+ @mechanism.side = lines.anybits?(SIDE) ? 1 : 0
146
+ @activity = lines.anybits?(ACTIVITY_LED)
147
+ @power = lines.anybits?(POWER_LED)
148
+ motor = lines.nobits?(MOTOR)
149
+ return if motor == @mechanism.motor_on?
150
+
151
+ @mechanism.motor(motor, @fdc.now)
152
+ @fdc.spin_changed
153
+ end
154
+
155
+ # The drive's whole state, as the 1541's (Drive1541#save_state), with
156
+ # the 8520 and the WD1772 in place of the VIAs.
157
+ def save_state(out)
158
+ settle!
159
+ out.marker("DRIVE1581")
160
+ out.int(@phase).int(@cycles).boolean(@atn)
161
+ @serial_port.save_state(out)
162
+ @cia.save_state(out)
163
+ @fdc.save_state(out)
164
+ @bus.save_state(out)
165
+ @cpu.save_state(out)
166
+ @mechanism.save_state(out)
167
+ end
168
+
169
+ def load_state(input)
170
+ settle!
171
+ input.marker("DRIVE1581")
172
+ @phase = input.int
173
+ @cycles = input.int
174
+ @atn = input.boolean?
175
+ @serial_port.load_state(input)
176
+ @cia.load_state(input)
177
+ @fdc.load_state(input)
178
+ @bus.load_state(input)
179
+ @cpu.load_state(input)
180
+ @mechanism.load_state(input)
181
+ @fast_serial_out = @cia.port_b_lines.anybits?(FAST_SERIAL_OUT)
182
+ ports_written
183
+ push_fast_output(@fast_serial_out ? fast_pins : 0)
184
+ forget_orbits
185
+ end
186
+
187
+ private
188
+
189
+ # One drive cycle: ATN going low pulls FLAG, the WD1772 and the 8520
190
+ # clock ahead of the CPU, and the 8520 alone pulls IRQ.
191
+ def step
192
+ atn = @serial_port.atn_low?
193
+ if atn != @atn
194
+ @atn = atn
195
+ @cia.flag! if atn
196
+ end
197
+ @fdc.cycle!
198
+ @cia.cycle!
199
+ drive_fast_serial if @fast_serial_out
200
+ @cpu.irq = @cia.interrupted?
201
+ @cpu.cycle!
202
+ @cycles += 1
203
+ end
204
+
205
+ def hear_atn
206
+ @atn = @serial_port.atn_low?
207
+ end
208
+
209
+ # A pass of the idle loop reads and writes RAM alone, so it repeats
210
+ # while the 8520 pulls no IRQ, for as long as the 8520's counters and
211
+ # the WD1772's next phase leave room: until timer B's next interrupt
212
+ # for the controller code. Neither chip changes but by the clock, so
213
+ # only the CPU is compared.
214
+ def busy_for_pass? = @cia.interrupted?
215
+
216
+ def quiet_cycles = [@cia.quiet_cycles, @fdc.quiet_cycles].min
217
+
218
+ def idle_state = [*@cpu.idle_state, @bus.data, @atn]
219
+
220
+ def fast_forward_chips(cycles)
221
+ @cia.fast_forward(cycles)
222
+ @fdc.fast_forward(cycles)
223
+ end
224
+
225
+ # Every timer B interrupt reads the 8520, so no orbit (Drive::Orbit)
226
+ # comes round, and the drive keeps no anchors.
227
+ def anchor = nil
228
+
229
+ def counter_state(_touched) = []
230
+
231
+ def skip_chip_orbits(_cycles, _touched) = nil
232
+ end
233
+ end
@@ -10,8 +10,6 @@ module Badline
10
10
  # On the C128 it follows the machine between the VIC-IIe's 40 columns
11
11
  # and the VDC's 80, and F8 switches them until the machine next does.
12
12
  class App
13
- DROPFILE = 0x1000
14
-
15
13
  # Takes the frame limit, the pacing, the snapshot path, the sound and
16
14
  # the verbosity from Options, and runs the timeline's events.
17
15
  def initialize(computer, options, timeline)
@@ -109,7 +107,7 @@ module Badline
109
107
  @controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
110
108
  when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
111
109
  @gamepads.rescan
112
- when DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
110
+ when SDL::DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
113
111
  end
114
112
  end
115
113
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # An SDL audio device on SDL's queue: signed 16-bit samples, `channels`
6
+ # of them interleaved per frame, go in at the device's rate and SDL
7
+ # plays them out behind us. When #open fails the device stays closed,
8
+ # with the reason in SDL_GetError for the caller to report.
9
+ class AudioDevice
10
+ attr_reader :rate
11
+
12
+ def initialize(channels:, rate:, samples:)
13
+ @channels = channels
14
+ @rate = rate
15
+ @samples = samples
16
+ @device = 0
17
+ @buffer = IO::Buffer.new(4096)
18
+ end
19
+
20
+ def open? = @device != 0
21
+
22
+ # With `exact_rate` false SDL may pick the device's own rate instead,
23
+ # which #rate then reports.
24
+ def open(exact_rate)
25
+ return unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
26
+
27
+ SDL.spec_freq(SDL.wanted, @rate)
28
+ SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
29
+ SDL.spec_channels(SDL.wanted, @channels)
30
+ SDL.spec_samples(SDL.wanted, @samples)
31
+ allowed_changes = exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE
32
+ @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
33
+ @rate = SDL.read_i32(SDL.obtained) if open?
34
+ end
35
+
36
+ # Returns SDL_QueueAudio's result, negative on failure.
37
+ def queue(samples)
38
+ count = samples.size
39
+ bytes = count * 2
40
+ @buffer.resize(bytes) if bytes > @buffer.size
41
+ buffer = @buffer
42
+ i = 0
43
+ while i < count
44
+ buffer.set_value(:s16, i * 2, samples[i])
45
+ i += 1
46
+ end
47
+ SDL.SDL_QueueAudio(@device, buffer, bytes)
48
+ end
49
+
50
+ def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @channels * @rate)
51
+
52
+ def start = SDL.SDL_PauseAudioDevice(@device, 0)
53
+
54
+ def pause = SDL.SDL_PauseAudioDevice(@device, 1)
55
+
56
+ def clear = SDL.SDL_ClearQueuedAudio(@device)
57
+
58
+ def close
59
+ return unless open?
60
+
61
+ SDL.SDL_CloseAudioDevice(@device)
62
+ SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
63
+ @device = 0
64
+ end
65
+ end
66
+ end
67
+ end
@@ -2,72 +2,38 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The host's audio device for --headless and the SID player, fed through
6
- # SDL's queue: stereo signed 16-bit samples, the left and right
7
- # interleaved, go in at the device's rate and SDL plays them out behind
8
- # us. It answers what Audio::Playback asks of a sink.
5
+ # The host's audio device for --headless and the SID player: a stereo
6
+ # AudioDevice, the left and right samples interleaved, that raises on
7
+ # SDL's failures. It answers what Audio::Playback asks of a sink.
9
8
  class AudioSink
10
9
  class Error < Audio::Playback::DeviceError; end
11
10
 
12
- attr_reader :rate
13
-
14
11
  # With `exact_rate` false SDL may pick the device's own rate instead,
15
12
  # which #rate then reports.
16
13
  def initialize(rate:, exact_rate: false, buffer: 1024)
17
- @device = 0
18
- @rate = rate
19
- @buffer = IO::Buffer.new(4096)
20
- check(SDL.SDL_InitSubSystem(SDL::INIT_AUDIO))
21
- open_device(rate, exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE, buffer)
22
- end
23
-
24
- def queue(samples)
25
- count = samples.size
26
- bytes = count * 2
27
- @buffer.resize(bytes) if bytes > @buffer.size
28
- buffer = @buffer
29
- i = 0
30
- while i < count
31
- buffer.set_value(:s16, i * 2, samples[i])
32
- i += 1
33
- end
34
- check(SDL.SDL_QueueAudio(@device, buffer, bytes))
14
+ @device = AudioDevice.new(channels: 2, rate:, samples: buffer)
15
+ @device.open(exact_rate)
16
+ raise Error, SDL.SDL_GetError unless @device.open?
35
17
  end
36
18
 
37
- def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (4.0 * @rate)
38
-
39
- def start = SDL.SDL_PauseAudioDevice(@device, 0)
40
-
41
- def pause = SDL.SDL_PauseAudioDevice(@device, 1)
42
-
43
- def clear = SDL.SDL_ClearQueuedAudio(@device)
19
+ def rate = @device.rate
44
20
 
45
- def close
46
- return if @device.zero?
21
+ def queue(samples)
22
+ result = @device.queue(samples)
23
+ raise Error, SDL.SDL_GetError if result.negative?
47
24
 
48
- SDL.SDL_CloseAudioDevice(@device)
49
- SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
50
- @device = 0
25
+ result
51
26
  end
52
27
 
53
- private
28
+ def queued_seconds = @device.queued_seconds
54
29
 
55
- def open_device(rate, allowed_changes, buffer)
56
- SDL.spec_freq(SDL.wanted, rate)
57
- SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
58
- SDL.spec_channels(SDL.wanted, 2)
59
- SDL.spec_samples(SDL.wanted, buffer)
60
- @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
61
- raise Error, SDL.SDL_GetError if @device.zero?
30
+ def start = @device.start
62
31
 
63
- @rate = SDL.read_i32(SDL.obtained)
64
- end
32
+ def pause = @device.pause
65
33
 
66
- def check(result)
67
- raise Error, SDL.SDL_GetError if result.negative?
34
+ def clear = @device.clear
68
35
 
69
- result
70
- end
36
+ def close = @device.close
71
37
  end
72
38
  end
73
39
  end
@@ -16,13 +16,10 @@ module Badline
16
16
  return 1
17
17
  end
18
18
 
19
- begin
20
- computer = boot(options)
21
- rescue Media::TrueDrive::Error, Storage::SIDFile::FormatError, Storage::T64::FormatError,
22
- Storage::TAP::FormatError, Storage::CRTFile::FormatError, Storage::G64Image::FormatError,
23
- Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
24
- Datasette::Missing, Unsupported => e
25
- warn "#{options.program}: #{options.media_path}: #{e.message}"
19
+ computer = nil
20
+ problem = media_problem { computer = boot(options) }
21
+ unless problem.empty?
22
+ warn "#{options.program}: #{options.media_path}: #{problem}"
26
23
  return 1
27
24
  end
28
25
  timeline = Timeline.new(options)
@@ -30,6 +27,19 @@ module Badline
30
27
  timeline.failed? ? 1 : 0
31
28
  end
32
29
 
30
+ # Runs the block and returns the message of the error it raised over
31
+ # media that won't go in or start, or an empty string. Other errors,
32
+ # a bare ArgumentError among them, go through.
33
+ def self.media_problem
34
+ yield
35
+ ""
36
+ rescue Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Datasette::Missing,
37
+ SystemCallError, Storage::SIDFile::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
38
+ Storage::CRTFile::FormatError, Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError,
39
+ Snapshot::FormatError => e
40
+ e.message
41
+ end
42
+
33
43
  # The machine a .vsf snapshot holds, built as the saved one was, or a
34
44
  # new one built from the options with the media attached.
35
45
  def self.boot(options)
@@ -88,9 +98,7 @@ module Badline
88
98
  return true if extension == ".sid"
89
99
  return false unless %w[.prg .p00].include?(extension)
90
100
 
91
- bytes = File.binread(media).bytes
92
- bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
93
- bytes.length >= 2 && [Media::BASIC_START, Media::BASIC_START - 1].include?(bytes[0] | (bytes[1] << 8))
101
+ [Media::BASIC_START, Media::BASIC_START - 1].include?(Storage::P00.load_address(media))
94
102
  end
95
103
 
96
104
  # A VIC-20 with the RAM --ram names, or else the RAM `media` needs
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The SID player's buttons: each is drawn where it goes on every frame,
6
- # and remembers its place, so a click finds the action under it. The
7
- # one under the pointer lights up, and one that's on is drawn reversed.
8
- # They take the player's colours unless given others: the text, the text
9
- # under the pointer, the background and, if any, a fill behind each
10
- # button.
5
+ # The buttons of the SID player and the pause menu: each is drawn where
6
+ # it goes on every frame, and remembers its place, so a click finds the
7
+ # action under it. The one under the pointer lights up, and one that's
8
+ # on is drawn reversed. They take their colours from the caller: the
9
+ # text, the text under the pointer, the background and, if any, a fill
10
+ # behind each button.
11
11
  class Buttons
12
12
  PAD = 2
13
13
  HEIGHT = Painter::GLYPH + (PAD * 2)
14
14
 
15
- def initialize(painter, colors = [SIDView::TEXT, SIDView::BRIGHT, PlayerScreen::BACKGROUND])
15
+ def initialize(painter, colors)
16
16
  @painter = painter
17
17
  @text = colors[0]
18
18
  @bright = colors[1]