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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +185 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +96 -25
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +132 -16
- data/lib/badline/cia/interrupt_register.rb +102 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +138 -88
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +97 -12
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +169 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +109 -168
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +51 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +277 -0
- data/lib/badline/kernal_trap/file.rb +5 -25
- data/lib/badline/kernal_trap/load.rb +86 -24
- data/lib/badline/kernal_trap/routine.rb +51 -0
- data/lib/badline/kernal_trap/save.rb +46 -14
- data/lib/badline/kernal_trap/serial.rb +171 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/memory.rb +5 -0
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +251 -23
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +99 -13
- data/lib/badline/storage/host_directory.rb +30 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +389 -55
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- 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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
when
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
data/lib/badline/status.rb
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|