badline 0.1.0 → 0.2.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +96 -25
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +169 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +109 -168
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. metadata +104 -4
data/lib/badline/sid.rb CHANGED
@@ -1,25 +1,292 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/sid/waveform"
4
+ require "badline/sid/envelope"
5
+ require "badline/sid/voice"
6
+ require "badline/sid/filter"
7
+ require "badline/sid/decimator"
8
+
3
9
  module Badline
10
+ # SID (Sound Interface Device) chip, in either the 6581 or the 8580
11
+ # revision.
12
+ #
13
+ # $D400-$D418 - Voices, filter and volume - write only
14
+ # $D419-$D41A - POTX/POTY paddle inputs - read only
15
+ # $D41B-$D41C - Voice 3 oscillator/envelope - read only
16
+ # $D41D-$D41F - Unconnected
17
+ #
18
+ # The register file mirrors every 32 bytes up to $D7FF.
19
+ #
20
+ # Reading a write-only or unconnected register returns the last byte the
21
+ # SID saw on the data bus. That value fades to $00 as the capacitance
22
+ # holding it drains. Paddles and the 1351 mouse attach as a pots source
23
+ # responding to #pot_x and #pot_y; with none attached the lines read $FF.
24
+ #
25
+ # Three voices feed the filter and the master volume; #output and #sample
26
+ # read out the mixed result, and #record collects it at an audio rate for
27
+ # #drain_samples. The DSP is clocked lazily and catches up when something
28
+ # asks for its state.
29
+ #
30
+ # The 6581 drives its voices well above ground, so the mix carries a large
31
+ # DC offset that the RC network on the C64 board filters out.
4
32
  class SID
5
33
  include Addressable
6
34
 
7
- def initialize
35
+ VOICES = 3
36
+
37
+ # Cycles the data bus holds a value, measured through SID/bitfade.
38
+ BUS_TTL = { mos6581: 0x1d00, mos8580: 0xa2000 }.freeze
39
+
40
+ # Scales the filter's 20-bit mix down to a signed 16-bit sample.
41
+ SAMPLE_DIVISOR = ((0xfff * 0xff) >> 7) * 3 * 15 * 2 / (2**16)
42
+
43
+ # Writes the DSP queues between catch-ups. A full queue catches up
44
+ # there and then, so no write loses its timing.
45
+ DEFERRED_WRITES = 0x400
46
+
47
+ # Cycles the filter integrates in one step while synthesizing, after
48
+ # reSID's delta_t_flt. 1 makes the audio exact, at a per-cycle cost.
49
+ FILTER_CHUNK = 4
50
+
51
+ attr_accessor :pots
52
+ attr_reader :model
53
+
54
+ def initialize(model: :mos6581, pots: nil, filter_chunk: FILTER_CHUNK)
8
55
  addressable_at(0xd400, length: 2**10)
56
+ @model = model
57
+ @pots = pots
58
+ @bus_ttl_reset = BUS_TTL.fetch(model)
59
+ @filter = Filter.new(model:)
60
+ @synthesizing = false
61
+ @pending_cycles = 0
62
+ @deferred_writes = []
63
+ @decimator = nil
64
+ @samples = []
65
+ @filter_chunk = filter_chunk
66
+ @voices = Voice.linked(VOICES, model:)
67
+ @voice1, @voice2, @voice3 = @voices
68
+ @waveform1, @waveform2, @waveform3 = @voices.map(&:waveform)
69
+ reset!
70
+ end
71
+
72
+ # The RES line clears the registers, the data bus, the filter and the
73
+ # voices, all but their accumulators. Recording carries on across it.
74
+ def reset!
75
+ catch_up
9
76
  @registers = Memory.new(length: 2**5)
77
+ @bus_value = 0x00
78
+ @bus_ttl = 0
79
+ @voices.each(&:reset!)
80
+ @filter.reset
81
+ end
82
+
83
+ # The DSP only counts cycles as they pass. Whatever asks for its state
84
+ # catches it up, a span at a time: each span fast-forwards the voices
85
+ # and ends on one whole cycle, so everything the CPU can read stays
86
+ # exact. The filter only runs once something wants audio.
87
+ def synthesizing? = @synthesizing
88
+
89
+ def synthesize!
90
+ return if @synthesizing
91
+
92
+ @synthesizing = true
93
+ catch_up
94
+ end
95
+
96
+ # Starts collecting the output, averaged down to `rate` samples a
97
+ # second, for #drain_samples. `filter_chunk` overrides the one the SID
98
+ # was built with, for a machine that built its own.
99
+ def record(rate:, filter_chunk: @filter_chunk)
100
+ synthesize!
101
+ catch_up
102
+ @filter_chunk = filter_chunk
103
+ @decimator = Decimator.new(clock_hz: TimeOfDay::CLOCK_HZ, rate:)
104
+ @samples = []
105
+ end
106
+
107
+ # The samples recorded since the last drain.
108
+ def drain_samples
109
+ catch_up
110
+ samples = @samples
111
+ @samples = []
112
+ samples
113
+ end
114
+
115
+ def voices
116
+ catch_up
117
+ @voices
118
+ end
119
+
120
+ def filter
121
+ catch_up
122
+ @filter
123
+ end
124
+
125
+ # Voice 3's oscillator and envelope, the only synthesis state the CPU
126
+ # can see. Programs poll OSC3 with the noise waveform selected for
127
+ # random numbers, and ENV3 to time hard restarts.
128
+ def osc3
129
+ catch_up
130
+ @waveform3.osc3 >> 4
131
+ end
132
+
133
+ def env3
134
+ catch_up
135
+ @voice3.envelope.env3
136
+ end
137
+
138
+ def output
139
+ synthesize!
140
+ catch_up
141
+ @filter.output
142
+ end
143
+
144
+ def sample
145
+ output
146
+ current_sample
147
+ end
148
+
149
+ def cycle!
150
+ age_bus if @bus_ttl != 0
151
+ @pending_cycles += 1
10
152
  end
11
153
 
12
154
  def peek(addr)
13
- i = index(addr) % (2**5)
14
- case i
15
- when 0x1d..0x1f then 0xff # Unused memory
16
- else @registers.peek(i)
155
+ case index(addr) % (2**5)
156
+ when 0x19 then latch(pots ? pots.pot_x : 0xff)
157
+ when 0x1a then latch(pots ? pots.pot_y : 0xff)
158
+ when 0x1b then latch(osc3)
159
+ when 0x1c then latch(env3)
160
+ else @bus_value
17
161
  end
18
162
  end
19
163
 
20
164
  def poke(addr, value)
21
- i = index(addr) % (2**5)
22
- @registers.poke(i, value)
165
+ reg = index(addr) % (2**5)
166
+ write_register(reg, value) if reg <= 0x18
167
+ latch(value)
168
+ end
169
+
170
+ # Register contents as written, for save states and debugging.
171
+ def register(reg) = @registers.peek(reg)
172
+
173
+ private
174
+
175
+ # Runs the cycles that have passed, landing each queued write on the
176
+ # cycle the CPU wrote it on.
177
+ def catch_up
178
+ replayed = 0
179
+ @deferred_writes.each do |cycle, reg, value|
180
+ run(cycle - replayed)
181
+ replayed = cycle
182
+ apply_write(reg, value)
183
+ end
184
+ run(@pending_cycles - replayed)
185
+ @deferred_writes.clear
186
+ @pending_cycles = 0
187
+ end
188
+
189
+ def run(cycles)
190
+ while cycles.positive?
191
+ span = span(cycles)
192
+ fast_forward(span - 1) if span > 1
193
+ clock!(span)
194
+ cycles -= span
195
+ end
196
+ end
197
+
198
+ # The longest stretch that can be fast-forwarded. A combined waveform
199
+ # feeding back into its own oscillator steps cycle by cycle, and a span
200
+ # never runs past an MSB rise that hard-syncs the next voice, so both
201
+ # land on a whole cycle.
202
+ def span(cycles)
203
+ synthesizing = @synthesizing
204
+ return 1 if @waveform1.stepped?(synthesizing) || @waveform2.stepped?(synthesizing) ||
205
+ @waveform3.stepped?(synthesizing)
206
+
207
+ span = synthesizing ? synthesis_span(cycles) : cycles
208
+ span = sync_span(@waveform1, span)
209
+ span = sync_span(@waveform2, span)
210
+ sync_span(@waveform3, span)
211
+ end
212
+
213
+ # The filter steps at most @filter_chunk cycles at a time, and never
214
+ # across the end of a recorded sample.
215
+ def synthesis_span(cycles)
216
+ span = [cycles, @filter_chunk].min
217
+ return span unless @decimator
218
+
219
+ [@decimator.cycles_to_close, span].min
220
+ end
221
+
222
+ def sync_span(waveform, span)
223
+ return span unless waveform.sync_dest.sync?
224
+
225
+ rise = waveform.cycles_to_msb_rise
226
+ rise && rise < span ? rise : span
227
+ end
228
+
229
+ # Unrolled over the three voices: the per-cycle block calls cost
230
+ # measurably on the synthesis path.
231
+ def fast_forward(cycles)
232
+ @voice1.fast_forward(cycles)
233
+ @voice2.fast_forward(cycles)
234
+ @voice3.fast_forward(cycles)
235
+ end
236
+
237
+ # The last cycle of a span, run whole. The filter integrates the span
238
+ # from the voices' output at its end.
239
+ def clock!(span)
240
+ @voice1.cycle!
241
+ @voice2.cycle!
242
+ @voice3.cycle!
243
+ @waveform1.synchronize!
244
+ @waveform2.synchronize!
245
+ @waveform3.synchronize!
246
+ return unless @synthesizing
247
+
248
+ @filter.cycle!(@voices, span)
249
+ record_sample(span) if @decimator
250
+ end
251
+
252
+ def record_sample(span)
253
+ sample = @decimator.push(current_sample, span)
254
+ @samples << sample if sample
255
+ end
256
+
257
+ def current_sample = (@filter.output / SAMPLE_DIVISOR).clamp(-0x8000, 0x7fff)
258
+
259
+ def latch(value)
260
+ @bus_ttl = @bus_ttl_reset
261
+ @bus_value = value
262
+ end
263
+
264
+ def age_bus
265
+ @bus_ttl -= 1
266
+ @bus_value = 0x00 if @bus_ttl.zero?
267
+ end
268
+
269
+ def write_register(reg, value)
270
+ @registers.poke(reg, value)
271
+ catch_up if @deferred_writes.length == DEFERRED_WRITES
272
+ @deferred_writes << [@pending_cycles, reg, value]
273
+ end
274
+
275
+ def apply_write(reg, value)
276
+ reg < 0x15 ? write_voice(reg, value) : @filter.write(reg, value)
277
+ end
278
+
279
+ def write_voice(reg, value)
280
+ voice = @voices[reg / 7]
281
+ case reg % 7
282
+ when 0 then voice.waveform.frequency_low = value
283
+ when 1 then voice.waveform.frequency_high = value
284
+ when 2 then voice.waveform.pulse_width_low = value
285
+ when 3 then voice.waveform.pulse_width_high = value
286
+ when 4 then voice.control = value
287
+ when 5 then voice.envelope.attack_decay = value
288
+ when 6 then voice.envelope.sustain_release = value
289
+ end
23
290
  end
24
291
  end
25
292
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
+ # A register of flag bits. +flags+ names each bit from bit 0 up: a
5
+ # symbol for a flag, or 0 or 1 for a bit that always reads as that. The
6
+ # subclasses below carry the accessors for each register layout.
4
7
  class Status
5
8
  attr_reader :value, :flags, :bitmask, :low_mask, :high_mask
6
9
 
@@ -12,8 +15,6 @@ module Badline
12
15
  @high_mask = create_mask { |f| f.is_a?(Integer) && f == 1 }
13
16
 
14
17
  self.value = value
15
-
16
- define_accessors!
17
18
  end
18
19
 
19
20
  def value=(new_value)
@@ -27,30 +28,257 @@ module Badline
27
28
  mask + (predicate.call(flag) ? 1 << i : 0)
28
29
  end
29
30
  end
31
+ end
30
32
 
31
- def define_accessors!
32
- flags.each_with_index do |name, i|
33
- next unless name.is_a?(Symbol)
34
-
35
- mask = 1 << i
36
- define_singleton_method("#{name}=") do |enabled|
37
- update(mask, enabled)
38
- end
39
- define_singleton_method("#{name}?") do
40
- !@value.nobits?(mask)
41
- end
42
- define_singleton_method(name) do
43
- @value.nobits?(mask) ? 0 : 1
44
- end
45
- end
33
+ # The 6510 processor status register.
34
+ class CPUStatus < Status
35
+ def carry=(enabled)
36
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
37
+ end
38
+
39
+ def carry? = @value & 0x01 != 0
40
+
41
+ def carry = @value.nobits?(0x01) ? 0 : 1
42
+
43
+ def zero=(enabled)
44
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
45
+ end
46
+
47
+ def zero? = @value & 0x02 != 0
48
+
49
+ def zero = @value.nobits?(0x02) ? 0 : 1
50
+
51
+ def interrupt=(enabled)
52
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
53
+ end
54
+
55
+ def interrupt? = @value & 0x04 != 0
56
+
57
+ def interrupt = @value.nobits?(0x04) ? 0 : 1
58
+
59
+ def decimal=(enabled)
60
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
61
+ end
62
+
63
+ def decimal? = @value & 0x08 != 0
64
+
65
+ def decimal = @value.nobits?(0x08) ? 0 : 1
66
+
67
+ def break=(enabled)
68
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
69
+ end
70
+
71
+ def break? = @value & 0x10 != 0
72
+
73
+ def break = @value.nobits?(0x10) ? 0 : 1
74
+
75
+ def overflow=(enabled)
76
+ @value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
77
+ end
78
+
79
+ def overflow? = @value & 0x40 != 0
80
+
81
+ def overflow = @value.nobits?(0x40) ? 0 : 1
82
+
83
+ def negative=(enabled)
84
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
85
+ end
86
+
87
+ def negative? = @value & 0x80 != 0
88
+
89
+ def negative = @value.nobits?(0x80) ? 0 : 1
90
+ end
91
+
92
+ # The 6510 I/O port at $01.
93
+ class PortStatus < Status
94
+ def basic=(enabled)
95
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
96
+ end
97
+
98
+ def basic? = @value & 0x01 != 0
99
+
100
+ def basic = @value.nobits?(0x01) ? 0 : 1
101
+
102
+ def kernal=(enabled)
103
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
104
+ end
105
+
106
+ def kernal? = @value & 0x02 != 0
107
+
108
+ def kernal = @value.nobits?(0x02) ? 0 : 1
109
+
110
+ def io=(enabled)
111
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
112
+ end
113
+
114
+ def io? = @value & 0x04 != 0
115
+
116
+ def io = @value.nobits?(0x04) ? 0 : 1
117
+
118
+ def tape_out=(enabled)
119
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
120
+ end
121
+
122
+ def tape_out? = @value & 0x08 != 0
123
+
124
+ def tape_out = @value.nobits?(0x08) ? 0 : 1
125
+
126
+ def tape_switch=(enabled)
127
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
128
+ end
129
+
130
+ def tape_switch? = @value & 0x10 != 0
131
+
132
+ def tape_switch = @value.nobits?(0x10) ? 0 : 1
133
+
134
+ def tape_motor=(enabled)
135
+ @value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
136
+ end
137
+
138
+ def tape_motor? = @value & 0x20 != 0
139
+
140
+ def tape_motor = @value.nobits?(0x20) ? 0 : 1
141
+ end
142
+
143
+ # A CIA timer control register, CRA or CRB. Bits 5-7 differ between the two.
144
+ class ControlRegister < Status
145
+ def start=(enabled)
146
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
147
+ end
148
+
149
+ def start? = @value & 0x01 != 0
150
+
151
+ def start = @value.nobits?(0x01) ? 0 : 1
152
+
153
+ def output=(enabled)
154
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
46
155
  end
47
156
 
48
- def update(mask, enabled)
49
- self.value = if enabled && enabled != 0
50
- value | mask
51
- else
52
- value & ~mask
53
- end
157
+ def output? = @value & 0x02 != 0
158
+
159
+ def output = @value.nobits?(0x02) ? 0 : 1
160
+
161
+ def out_mode=(enabled)
162
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
163
+ end
164
+
165
+ def out_mode? = @value & 0x04 != 0
166
+
167
+ def out_mode = @value.nobits?(0x04) ? 0 : 1
168
+
169
+ def run_mode=(enabled)
170
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
171
+ end
172
+
173
+ def run_mode? = @value & 0x08 != 0
174
+
175
+ def run_mode = @value.nobits?(0x08) ? 0 : 1
176
+
177
+ def load=(enabled)
178
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
179
+ end
180
+
181
+ def load? = @value & 0x10 != 0
182
+
183
+ def load = @value.nobits?(0x10) ? 0 : 1
184
+
185
+ def in_mode=(enabled)
186
+ @value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
54
187
  end
188
+
189
+ def in_mode? = @value & 0x20 != 0
190
+
191
+ def in_mode = @value.nobits?(0x20) ? 0 : 1
192
+
193
+ def serial_mode=(enabled)
194
+ @value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
195
+ end
196
+
197
+ def serial_mode? = @value & 0x40 != 0
198
+
199
+ def serial_mode = @value.nobits?(0x40) ? 0 : 1
200
+
201
+ def clock_frequency=(enabled)
202
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
203
+ end
204
+
205
+ def clock_frequency? = @value & 0x80 != 0
206
+
207
+ def clock_frequency = @value.nobits?(0x80) ? 0 : 1
208
+
209
+ def in_cnt=(enabled)
210
+ @value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
211
+ end
212
+
213
+ def in_cnt? = @value & 0x20 != 0
214
+
215
+ def in_cnt = @value.nobits?(0x20) ? 0 : 1
216
+
217
+ def in_timer_a=(enabled)
218
+ @value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
219
+ end
220
+
221
+ def in_timer_a? = @value & 0x40 != 0
222
+
223
+ def in_timer_a = @value.nobits?(0x40) ? 0 : 1
224
+
225
+ def alarm=(enabled)
226
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
227
+ end
228
+
229
+ def alarm? = @value & 0x80 != 0
230
+
231
+ def alarm = @value.nobits?(0x80) ? 0 : 1
232
+ end
233
+
234
+ # The CIA interrupt sources, as flags or as their mask.
235
+ class InterruptFlags < Status
236
+ def timer_a=(enabled)
237
+ @value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
238
+ end
239
+
240
+ def timer_a? = @value & 0x01 != 0
241
+
242
+ def timer_a = @value.nobits?(0x01) ? 0 : 1
243
+
244
+ def timer_b=(enabled)
245
+ @value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
246
+ end
247
+
248
+ def timer_b? = @value & 0x02 != 0
249
+
250
+ def timer_b = @value.nobits?(0x02) ? 0 : 1
251
+
252
+ def alarm=(enabled)
253
+ @value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
254
+ end
255
+
256
+ def alarm? = @value & 0x04 != 0
257
+
258
+ def alarm = @value.nobits?(0x04) ? 0 : 1
259
+
260
+ def serial=(enabled)
261
+ @value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
262
+ end
263
+
264
+ def serial? = @value & 0x08 != 0
265
+
266
+ def serial = @value.nobits?(0x08) ? 0 : 1
267
+
268
+ def flag=(enabled)
269
+ @value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
270
+ end
271
+
272
+ def flag? = @value & 0x10 != 0
273
+
274
+ def flag = @value.nobits?(0x10) ? 0 : 1
275
+
276
+ def interrupt=(enabled)
277
+ @value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
278
+ end
279
+
280
+ def interrupt? = @value & 0x80 != 0
281
+
282
+ def interrupt = @value.nobits?(0x80) ? 0 : 1
55
283
  end
56
284
  end
@@ -12,7 +12,21 @@ module Badline
12
12
 
13
13
  Chip = Data.define(:chip_type, :bank, :address, :data)
14
14
 
15
- attr_reader :hardware_type, :exrom, :game, :name, :chips
15
+ attr_reader :hardware_type, :subtype, :exrom, :game, :name, :chips
16
+
17
+ # What .write needs of an image: the fields a parsed CRTFile reads.
18
+ Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
19
+
20
+ def self.write(path, image)
21
+ header = SIGNATURE +
22
+ [0x40, 0x0100, image.hardware_type, image.exrom, image.game, image.subtype].pack("NnnCCCx5") +
23
+ [image.name.to_s.b[0, 32]].pack("a32")
24
+ packets = image.chips.map do |chip|
25
+ ["CHIP", CHIP_HEADER_SIZE + chip.data.length, chip.chip_type, chip.bank, chip.address,
26
+ chip.data.length].pack("a4Nn4") + chip.data.pack("C*")
27
+ end
28
+ File.binwrite(path, header + packets.join)
29
+ end
16
30
 
17
31
  def initialize(path)
18
32
  parse(File.binread(path))
@@ -26,13 +40,15 @@ module Badline
26
40
  @hardware_type = bytes[0x16, 2].unpack1("n")
27
41
  @exrom = bytes.getbyte(0x18)
28
42
  @game = bytes.getbyte(0x19)
43
+ @subtype = bytes.getbyte(0x1a)
29
44
  @name = bytes[0x20, 32].unpack1("Z*")
30
45
  @chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
31
46
  end
32
47
 
48
+ # Trailing bytes too short to hold a CHIP header are ignored.
33
49
  def parse_chips(bytes, offset)
34
50
  chips = []
35
- while offset < bytes.length
51
+ while offset + CHIP_HEADER_SIZE <= bytes.length
36
52
  chip, offset = parse_chip(bytes, offset)
37
53
  chips << chip
38
54
  end
@@ -3,8 +3,14 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D64Image < DiskImage
6
+ # Image sizes with an error table, mapped to its length: 35 tracks
7
+ # (174848 bytes without one), 40 tracks (196608) and 42 tracks (205312)
8
+ ERROR_TABLES = { 175_531 => 683, 197_376 => 768, 206_114 => 802 }.freeze
9
+
6
10
  private
7
11
 
12
+ def error_tables = ERROR_TABLES
13
+
8
14
  def directory_track = 18
9
15
  def directory_sector = 1
10
16
 
@@ -3,8 +3,12 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D71Image < D64Image
6
+ ERROR_TABLES = { 351_062 => 1366 }.freeze
7
+
6
8
  private
7
9
 
10
+ def error_tables = ERROR_TABLES
11
+
8
12
  def sectors_in(track)
9
13
  track > 35 ? super(track - 35) : super
10
14
  end
@@ -3,8 +3,12 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D81Image < DiskImage
6
+ ERROR_TABLES = { 822_400 => 3200 }.freeze
7
+
6
8
  private
7
9
 
10
+ def error_tables = ERROR_TABLES
11
+
8
12
  def directory_track = 40
9
13
  def directory_sector = 3
10
14