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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/kernal_trap/drive/status"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
module KernalTrap
|
|
7
|
+
# The CBM DOS side of the serial bus. Keeps the open channels, the
|
|
8
|
+
# command channel and its status message. `U1` reads a raw block into a
|
|
9
|
+
# buffer channel, which is how block-access loaders bypass the KERNAL's
|
|
10
|
+
# LOAD.
|
|
11
|
+
class Drive
|
|
12
|
+
COMMAND_CHANNEL = 15
|
|
13
|
+
BLOCK_SIZE = 256
|
|
14
|
+
|
|
15
|
+
OK = 0
|
|
16
|
+
WRITE_PROTECT_ON = 26
|
|
17
|
+
SYNTAX_ERROR = 30
|
|
18
|
+
FILE_NOT_FOUND = 62
|
|
19
|
+
FILE_EXISTS = 63
|
|
20
|
+
ILLEGAL_TRACK_OR_SECTOR = 66
|
|
21
|
+
NO_CHANNEL = 70
|
|
22
|
+
DOS_VERSION = 73
|
|
23
|
+
DRIVE_NOT_READY = 74
|
|
24
|
+
|
|
25
|
+
MEMORY_COMMANDS = { "M-W" => :memory_write, "M-R" => :memory_read }.freeze
|
|
26
|
+
|
|
27
|
+
RAM_SIZE = 0x800
|
|
28
|
+
|
|
29
|
+
# The job queue: a job code for each of five buffers at $00, their
|
|
30
|
+
# track and sector pairs from $06, and the buffers from $0300. A job
|
|
31
|
+
# code is replaced by its result, 1 for success or an error table
|
|
32
|
+
# code: the DOS error less 18, or 15 for DRIVE NOT READY.
|
|
33
|
+
JOBS = 5
|
|
34
|
+
READ_JOB = 0x80
|
|
35
|
+
JOB_OK = 1
|
|
36
|
+
HEADER_NOT_FOUND = 20
|
|
37
|
+
|
|
38
|
+
# A U command jumps through the user table at $FFEA, indexed by the
|
|
39
|
+
# low nibble of its second character less one, so UA and UQ are U1.
|
|
40
|
+
# U1 and U2 read and write a block. U3 to U8 run code in the buffer
|
|
41
|
+
# at $0500, and the five entries past the table's end take vectors
|
|
42
|
+
# from the job queue. Neither runs here, so they report OK as a
|
|
43
|
+
# routine that returns does. U9 (UI) takes the NMI vector and U; the
|
|
44
|
+
# IRQ one, which restart the DOS with its RAM intact, except that a
|
|
45
|
+
# + or - after U9 only switches the bus speed. U: takes the reset
|
|
46
|
+
# vector, which runs the RAM test. U0 restores the table.
|
|
47
|
+
USER_TABLE = [:block_read, :write_protected, *[:initialized] * 6,
|
|
48
|
+
:warm_reset, :cold_reset, :warm_reset, *[:initialized] * 5].freeze
|
|
49
|
+
|
|
50
|
+
COMMANDS = {
|
|
51
|
+
/\AU[9IY)][+-]/i => :initialized,
|
|
52
|
+
/\AU.\s*:?\s*(.*)/i => :user,
|
|
53
|
+
/\AB-R\s*:?\s*(.*)/i => :counted_block_read,
|
|
54
|
+
/\AB-P\s*:?\s*(.*)/i => :buffer_pointer,
|
|
55
|
+
/\A[IV]/i => :initialized,
|
|
56
|
+
/\AB-[WAF]/i => :write_protected
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# A drive powers on reporting its DOS version, as a reset does.
|
|
60
|
+
def initialize(storage)
|
|
61
|
+
@storage = storage
|
|
62
|
+
@channels = {}
|
|
63
|
+
@status = Status.new
|
|
64
|
+
@ram = Array.new(RAM_SIZE, 0)
|
|
65
|
+
report(DOS_VERSION)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A name on the command channel is a command, "#" opens a block
|
|
69
|
+
# buffer, anything else opens a file for reading. The name comes as
|
|
70
|
+
# the bytes sent, because a memory command's arguments are binary.
|
|
71
|
+
def open(secondary, name)
|
|
72
|
+
if secondary == COMMAND_CHANNEL
|
|
73
|
+
command(name.bytes) unless name.empty?
|
|
74
|
+
elsif name.start_with?("#")
|
|
75
|
+
@channels[secondary] = Channel.new(Array.new(BLOCK_SIZE, 0))
|
|
76
|
+
report(OK)
|
|
77
|
+
else
|
|
78
|
+
open_file(secondary, Storage.ascii(name.bytes))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Closing the command channel closes every other channel with it.
|
|
83
|
+
def close(secondary)
|
|
84
|
+
secondary == COMMAND_CHANNEL ? @channels.clear : @channels.delete(secondary)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write(secondary, bytes)
|
|
88
|
+
return command(bytes) if secondary == COMMAND_CHANNEL
|
|
89
|
+
|
|
90
|
+
report(WRITE_PROTECT_ON) if listening?(secondary)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether data sent on the channel reaches the drive. The command
|
|
94
|
+
# channel always listens, other channels once they are open.
|
|
95
|
+
def listening?(secondary)
|
|
96
|
+
secondary == COMMAND_CHANNEL || @channels.key?(secondary)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns the next byte and whether it is the channel's last, or nil
|
|
100
|
+
# when there is nothing left to send.
|
|
101
|
+
def read(secondary)
|
|
102
|
+
return @status.read if secondary == COMMAND_CHANNEL
|
|
103
|
+
|
|
104
|
+
channel = @channels[secondary]
|
|
105
|
+
result = channel&.read
|
|
106
|
+
report(*channel.error) if result && channel.error && channel.exhausted?
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Secondary addresses 0 and 1 are LOAD and SAVE, which look for a PRG
|
|
113
|
+
# file unless the name asks for another type. Other channels take any
|
|
114
|
+
# type the name doesn't pin down.
|
|
115
|
+
def open_file(secondary, name)
|
|
116
|
+
file, type = Storage.parse_name(name)
|
|
117
|
+
return refuse_write(secondary, name, file) if writing?(secondary, name)
|
|
118
|
+
|
|
119
|
+
type ||= :prg if secondary < 2
|
|
120
|
+
channel = @channels[secondary] = Channel.for_file(@storage, file, type)
|
|
121
|
+
channel.exhausted? && channel.error ? report(*channel.error) : report(OK)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# SAVE's secondary address 1 and a W mode open a file for writing.
|
|
125
|
+
def writing?(secondary, name)
|
|
126
|
+
secondary == 1 || name.split(",").drop(1).any? { |field| field.strip.match?(/\AW/i) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The disk is write-protected, so an open for writing fails and leaves
|
|
130
|
+
# the channel closed. A name already on the disk fails as FILE EXISTS
|
|
131
|
+
# unless @ asks to replace it, anything else as WRITE PROTECT ON.
|
|
132
|
+
def refuse_write(secondary, name, file)
|
|
133
|
+
@channels.delete(secondary)
|
|
134
|
+
return report(FILE_EXISTS) if !name.start_with?("@") && @storage.read_file(file, type: nil)
|
|
135
|
+
|
|
136
|
+
report(WRITE_PROTECT_ON, *protected_block(secondary))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# SAVE's channel fails at the directory block its entry would go to,
|
|
140
|
+
# a W mode open at the disk's header block.
|
|
141
|
+
def protected_block(secondary)
|
|
142
|
+
return [] unless @storage.respond_to?(:header_block)
|
|
143
|
+
|
|
144
|
+
secondary == 1 ? @storage.new_entry_block : @storage.header_block
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def execute(text)
|
|
148
|
+
command = text.strip
|
|
149
|
+
pattern, action = COMMANDS.find { |candidate, _| candidate.match?(command) }
|
|
150
|
+
return report(SYNTAX_ERROR) unless action
|
|
151
|
+
|
|
152
|
+
action = USER_TABLE[(command.getbyte(1) - 1) & 0x0f] if action == :user
|
|
153
|
+
run_command(action, arguments(pattern.match(command)[1]))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def arguments(text)
|
|
157
|
+
text.to_s.scan(/\d+/).map(&:to_i)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def command(bytes)
|
|
161
|
+
action = MEMORY_COMMANDS[bytes[0, 3].pack("C*")]
|
|
162
|
+
action ? run_command(action, bytes[3..]) : execute(Storage.ascii(bytes))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def run_command(action, arguments)
|
|
166
|
+
case action
|
|
167
|
+
when :block_read then block_read(arguments)
|
|
168
|
+
when :counted_block_read then counted_block_read(arguments)
|
|
169
|
+
when :buffer_pointer then buffer_pointer(arguments)
|
|
170
|
+
when :initialized then report(OK)
|
|
171
|
+
when :write_protected then write_protected(arguments)
|
|
172
|
+
when :cold_reset then reset(cold: true)
|
|
173
|
+
when :warm_reset then reset(cold: false)
|
|
174
|
+
when :memory_write then memory_write(arguments)
|
|
175
|
+
when :memory_read then memory_read(arguments)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def block_read(arguments, counted: false)
|
|
180
|
+
channel, _drive, track, sector = arguments
|
|
181
|
+
buffer = @channels[channel]
|
|
182
|
+
return report(NO_CHANNEL) unless buffer
|
|
183
|
+
return report(DRIVE_NOT_READY) unless @storage.respond_to?(:read_block)
|
|
184
|
+
|
|
185
|
+
data = sector && @storage.read_block(track, sector)
|
|
186
|
+
return report(ILLEGAL_TRACK_OR_SECTOR, track, sector) unless data
|
|
187
|
+
|
|
188
|
+
buffer.replace(counted ? data[0, data[0] + 1] : data)
|
|
189
|
+
buffer.pointer = 1 if counted
|
|
190
|
+
report_block_error(track, sector)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A block the image's error table marks bad still fills the buffer,
|
|
194
|
+
# but the read reports its error, which copy protection checks for.
|
|
195
|
+
def report_block_error(track, sector)
|
|
196
|
+
error = @storage.block_error(track, sector)
|
|
197
|
+
error ? report(error, track, sector) : report(OK)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# B-R takes the block's first byte as the index of its last one, and
|
|
201
|
+
# hands out the bytes from the second up to there.
|
|
202
|
+
def counted_block_read(arguments)
|
|
203
|
+
block_read(arguments, counted: true)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def buffer_pointer(arguments)
|
|
207
|
+
channel, position = arguments
|
|
208
|
+
buffer = @channels[channel]
|
|
209
|
+
return report(NO_CHANNEL) unless buffer
|
|
210
|
+
|
|
211
|
+
buffer.pointer = position.to_i
|
|
212
|
+
report(OK)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# M-W takes an address, a count and that many bytes. Writes past the
|
|
216
|
+
# RAM are dropped, since the rest of the address space is I/O and ROM.
|
|
217
|
+
def memory_write(arguments)
|
|
218
|
+
address = word(arguments)
|
|
219
|
+
arguments[3, arguments[2].to_i].to_a.each.with_index(address) do |byte, target|
|
|
220
|
+
@ram[target] = byte if target < RAM_SIZE
|
|
221
|
+
end
|
|
222
|
+
run_jobs
|
|
223
|
+
report(OK)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# M-R takes an address and a count, and answers on the command
|
|
227
|
+
# channel. Without a count, or with only the carriage return PRINT#
|
|
228
|
+
# ends a line with, it reads one byte. Only the RAM reads back.
|
|
229
|
+
def memory_read(arguments)
|
|
230
|
+
address = word(arguments)
|
|
231
|
+
count = arguments[2..] == ["\r".ord] ? 1 : arguments.fetch(2, 1)
|
|
232
|
+
count = BLOCK_SIZE if count.zero?
|
|
233
|
+
@status.replace(Array.new(count) { |i| @ram.fetch(address + i, 0) })
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def word(arguments)
|
|
238
|
+
arguments[0].to_i | (arguments[1].to_i << 8)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def run_jobs
|
|
242
|
+
JOBS.times do |job|
|
|
243
|
+
next unless @ram[job] == READ_JOB
|
|
244
|
+
|
|
245
|
+
track, sector = @ram[6 + (job * 2), 2]
|
|
246
|
+
@ram[job] = read_job(0x300 + (job * BLOCK_SIZE), track, sector)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def read_job(buffer, track, sector)
|
|
251
|
+
data = @storage.respond_to?(:read_block) && @storage.read_block(track, sector)
|
|
252
|
+
return job_result(HEADER_NOT_FOUND) unless data
|
|
253
|
+
|
|
254
|
+
@ram[buffer, BLOCK_SIZE] = data
|
|
255
|
+
job_result(@storage.block_error(track, sector))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def job_result(error)
|
|
259
|
+
return JOB_OK unless error
|
|
260
|
+
|
|
261
|
+
error == DRIVE_NOT_READY ? 15 : error - 18
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def write_protected(_arguments)
|
|
265
|
+
report(WRITE_PROTECT_ON)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def reset(cold:)
|
|
269
|
+
@ram.fill(0) if cold
|
|
270
|
+
@channels.clear
|
|
271
|
+
report(DOS_VERSION)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def report(...) = @status.report(...)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -2,23 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
|
-
class File
|
|
6
|
-
include IntegerHelper
|
|
7
|
-
|
|
8
|
-
DEVICE = 8
|
|
9
|
-
|
|
5
|
+
class File < Routine
|
|
10
6
|
MISSING_FILENAME = 0x08
|
|
11
7
|
|
|
12
|
-
def initialize(cpu:, bus:, storage:)
|
|
13
|
-
|
|
14
|
-
@bus = bus
|
|
8
|
+
def initialize(cpu:, bus:, storage: nil)
|
|
9
|
+
super(cpu:, bus:)
|
|
15
10
|
@storage = storage
|
|
16
11
|
end
|
|
17
12
|
|
|
18
13
|
private
|
|
19
14
|
|
|
20
15
|
def active?
|
|
21
|
-
|
|
16
|
+
kernal? && @bus.peek(0xba) == DEVICE
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
# Filename pointer at $BB/$BC, length at $B7
|
|
@@ -27,28 +22,13 @@ module Badline
|
|
|
27
22
|
bytes = Array.new(@bus.peek(0xb7)) do |i|
|
|
28
23
|
@bus.peek((pointer + i) & 0xffff)
|
|
29
24
|
end
|
|
30
|
-
strip_drive_prefix(Storage.ascii(bytes))
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# CBM DOS drive prefix — "0:NAME" selects a drive, "@0:NAME" is
|
|
34
|
-
# save-with-replace
|
|
35
|
-
def strip_drive_prefix(name)
|
|
36
|
-
name.sub(/\A@?\d*:/, "")
|
|
25
|
+
Storage.strip_drive_prefix(Storage.ascii(bytes))
|
|
37
26
|
end
|
|
38
27
|
|
|
39
28
|
def error(code)
|
|
40
29
|
@cpu.a = code
|
|
41
30
|
@cpu.status.carry = true
|
|
42
31
|
end
|
|
43
|
-
|
|
44
|
-
def return_to_caller
|
|
45
|
-
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def pull_byte
|
|
49
|
-
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
50
|
-
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
51
|
-
end
|
|
52
32
|
end
|
|
53
33
|
end
|
|
54
34
|
end
|
|
@@ -3,41 +3,112 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
5
|
# PC trap on the KERNAL serial LOAD routine ($F4A5, the default ILOAD
|
|
6
|
-
# vector target).
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# vector target). Reads device 8 requests from the virtual drive's
|
|
7
|
+
# channel 0, then hands over to the ROM's own tail so it prints
|
|
8
|
+
# SEARCHING FOR and LOADING (or VERIFYING) in direct mode, reports
|
|
9
|
+
# errors and returns with the routine's register/zeropage contract;
|
|
10
|
+
# other devices fall through to the ROM. So does a load that reaches
|
|
11
|
+
# below $0334, into the zero page, the stack or the KERNAL vectors: the
|
|
12
|
+
# ROM's byte loop then loads it through the serial traps, and a loader
|
|
13
|
+
# that overwrites ISTOP takes over mid-load as it does on a real drive.
|
|
9
14
|
class Load < File
|
|
10
15
|
ADDRESS = 0xf4a5
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
# ROM entry points: the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
18
|
+
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
19
|
+
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the
|
|
20
|
+
# FILE NOT FOUND and MISSING FILE NAME error exits
|
|
21
|
+
SEARCHING_MESSAGE = 0xf5af
|
|
22
|
+
LOADING_MESSAGE = 0xf5d2
|
|
23
|
+
BYTE_LOOP = 0xf4f3
|
|
24
|
+
LOAD_DONE = 0xf5a9
|
|
25
|
+
FILE_NOT_FOUND_EXIT = 0xf704
|
|
26
|
+
MISSING_FILE_NAME_EXIT = 0xf710
|
|
27
|
+
|
|
28
|
+
# ST bits at $90
|
|
29
|
+
EOI = 0x40
|
|
30
|
+
VERIFY_MISMATCH = 0x10
|
|
31
|
+
READ_TIMEOUT = 0x02
|
|
32
|
+
|
|
33
|
+
def initialize(cpu:, bus:, drive:)
|
|
34
|
+
super(cpu:, bus:)
|
|
35
|
+
@drive = drive
|
|
36
|
+
end
|
|
13
37
|
|
|
14
38
|
def call
|
|
15
39
|
return unless active?
|
|
16
40
|
|
|
41
|
+
@bus.poke(0x93, @cpu.a)
|
|
42
|
+
@bus.poke(0x90, 0x00)
|
|
17
43
|
name = filename
|
|
18
|
-
if name.empty?
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
return @cpu.program_counter = MISSING_FILE_NAME_EXIT if Storage.parse_name(name).first.empty?
|
|
45
|
+
|
|
46
|
+
data, complete = receive(name)
|
|
47
|
+
if low_memory?(data)
|
|
48
|
+
@drive.close(0)
|
|
22
49
|
else
|
|
23
|
-
|
|
50
|
+
finish(data, complete)
|
|
24
51
|
end
|
|
25
|
-
return_to_caller
|
|
26
52
|
end
|
|
27
53
|
|
|
28
54
|
private
|
|
29
55
|
|
|
30
|
-
#
|
|
56
|
+
# Reads the file the way the ROM's byte loop would, up to the byte
|
|
57
|
+
# flagged EOI. A read error in the file's chain stops the bytes early,
|
|
58
|
+
# with no EOI.
|
|
59
|
+
def receive(name)
|
|
60
|
+
@drive.open(0, name)
|
|
61
|
+
data = []
|
|
62
|
+
loop do
|
|
63
|
+
byte, eoi = @drive.read(0)
|
|
64
|
+
return [data, false] unless byte
|
|
65
|
+
|
|
66
|
+
data << byte
|
|
67
|
+
return [data, true] if eoi
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# No first byte means the file isn't there, or its first block didn't
|
|
72
|
+
# read. A later read error leaves the ROM retrying the next byte
|
|
73
|
+
# until RUN/STOP breaks the load, as on a real drive.
|
|
74
|
+
def finish(data, complete)
|
|
75
|
+
if data.empty?
|
|
76
|
+
@bus.poke(0x90, EOI | READ_TIMEOUT)
|
|
77
|
+
continue_with(SEARCHING_MESSAGE, FILE_NOT_FOUND_EXIT)
|
|
78
|
+
else
|
|
79
|
+
deliver(data)
|
|
80
|
+
@bus.poke(0x90, @bus.peek(0x90) & ~EOI) unless complete
|
|
81
|
+
continue_with(SEARCHING_MESSAGE, LOADING_MESSAGE, complete ? LOAD_DONE : BYTE_LOOP)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def low_memory?(data)
|
|
86
|
+
load? && data.length > 2 && load_address(data) < 0x0334
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A=0 is LOAD, A=1 is VERIFY, kept at $93 (VERCK)
|
|
31
90
|
def load?
|
|
32
|
-
@
|
|
91
|
+
@bus.peek(0x93).zero?
|
|
33
92
|
end
|
|
34
93
|
|
|
35
94
|
def deliver(data)
|
|
36
95
|
addr = load_address(data)
|
|
37
96
|
payload = data[2..] || []
|
|
38
97
|
payload = payload[0, 0x10000 - addr] if addr + payload.length > 0x10000
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
if load?
|
|
99
|
+
@bus.ram.write(addr, payload)
|
|
100
|
+
@bus.poke(0x90, EOI)
|
|
101
|
+
else
|
|
102
|
+
verify(addr, payload)
|
|
103
|
+
end
|
|
104
|
+
end_addr = (addr + payload.length) & 0xffff
|
|
105
|
+
@bus.poke(0xae, low_byte(end_addr))
|
|
106
|
+
@bus.poke(0xaf, high_byte(end_addr))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def verify(addr, payload)
|
|
110
|
+
match = payload.each_with_index.all? { |byte, i| @bus.peek(addr + i) == byte }
|
|
111
|
+
@bus.poke(0x90, match ? EOI : EOI | VERIFY_MISMATCH)
|
|
41
112
|
end
|
|
42
113
|
|
|
43
114
|
# Secondary address $B9 zero relocates to the caller-supplied address
|
|
@@ -46,18 +117,9 @@ module Badline
|
|
|
46
117
|
if @bus.peek(0xb9).zero?
|
|
47
118
|
uint16(@bus.peek(0xc3), @bus.peek(0xc4))
|
|
48
119
|
else
|
|
49
|
-
uint16(data[0], data[1])
|
|
120
|
+
uint16(data[0], data[1].to_i)
|
|
50
121
|
end
|
|
51
122
|
end
|
|
52
|
-
|
|
53
|
-
def finish(end_addr)
|
|
54
|
-
@bus.poke(0x90, 0x40)
|
|
55
|
-
@bus.poke(0xae, low_byte(end_addr))
|
|
56
|
-
@bus.poke(0xaf, high_byte(end_addr))
|
|
57
|
-
@cpu.x = low_byte(end_addr)
|
|
58
|
-
@cpu.y = high_byte(end_addr)
|
|
59
|
-
@cpu.status.carry = false
|
|
60
|
-
end
|
|
61
123
|
end
|
|
62
124
|
end
|
|
63
125
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# Base class for the PC traps on KERNAL routines. Holds the device
|
|
6
|
+
# number they answer for and the return that stands in for the trapped
|
|
7
|
+
# routine's RTS.
|
|
8
|
+
class Routine
|
|
9
|
+
include IntegerHelper
|
|
10
|
+
|
|
11
|
+
DEVICE = 8
|
|
12
|
+
|
|
13
|
+
def initialize(cpu:, bus:)
|
|
14
|
+
@cpu = cpu
|
|
15
|
+
@bus = bus
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def kernal?
|
|
21
|
+
@bus.io_port.kernal?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def return_to_caller
|
|
25
|
+
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Runs the ROM routines in order, each returning into the next, and
|
|
29
|
+
# the last one to the trapped routine's caller
|
|
30
|
+
def continue_with(first, *rest)
|
|
31
|
+
rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
|
|
32
|
+
@cpu.program_counter = first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def push_address(address)
|
|
36
|
+
push_byte(high_byte(address))
|
|
37
|
+
push_byte(low_byte(address))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def push_byte(value)
|
|
41
|
+
@bus.poke(0x0100 + @cpu.stack_pointer, value)
|
|
42
|
+
@cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def pull_byte
|
|
46
|
+
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
47
|
+
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -5,25 +5,62 @@ module Badline
|
|
|
5
5
|
# PC trap on the KERNAL serial SAVE routine ($F5ED, the default ISAVE
|
|
6
6
|
# vector target). Writes device 8 saves to a storage backend as a PRG
|
|
7
7
|
# (load address followed by the memory range); other devices fall
|
|
8
|
-
# through to the ROM.
|
|
8
|
+
# through to the ROM. The ROM prints SAVING in direct mode and returns
|
|
9
|
+
# into the trap, which then writes the file and leaves through the
|
|
10
|
+
# ROM's own tail with the registers its UNLISTEN and return leave. A
|
|
11
|
+
# host write that fails ends the way a 1541 ends a SAVE it can't
|
|
12
|
+
# write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
|
|
13
|
+
# and the ROM returns without an error.
|
|
9
14
|
class Save < File
|
|
10
15
|
ADDRESS = 0xf5ed
|
|
11
16
|
|
|
17
|
+
# ROM entry points: the SAVING message, the two line releases that
|
|
18
|
+
# end UNLISTEN (clock, then data, leaving A as read from $DD00), the
|
|
19
|
+
# successful return (CLC, RTS) and the MISSING FILE NAME error exit
|
|
20
|
+
SAVING_MESSAGE = 0xf68f
|
|
21
|
+
CLOCK_RELEASE = 0xee85
|
|
22
|
+
DATA_RELEASE = 0xee97
|
|
23
|
+
SAVE_DONE = 0xf657
|
|
24
|
+
MISSING_FILE_NAME_EXIT = 0xf710
|
|
25
|
+
|
|
26
|
+
SECONDARY = 0x61
|
|
27
|
+
|
|
28
|
+
# ST bit at $90
|
|
29
|
+
DEVICE_NOT_PRESENT = 0x80
|
|
30
|
+
|
|
31
|
+
def initialize(cpu:, bus:, storage:)
|
|
32
|
+
super
|
|
33
|
+
@saving = false
|
|
34
|
+
end
|
|
35
|
+
|
|
12
36
|
def call
|
|
13
37
|
return unless active?
|
|
38
|
+
return finish if @saving
|
|
39
|
+
|
|
40
|
+
@bus.poke(0xb9, SECONDARY)
|
|
41
|
+
return @cpu.program_counter = MISSING_FILE_NAME_EXIT if name.empty?
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
else
|
|
19
|
-
@storage.write_file(name, payload)
|
|
20
|
-
finish
|
|
21
|
-
end
|
|
22
|
-
return_to_caller
|
|
43
|
+
@bus.poke(0x90, 0x00)
|
|
44
|
+
@saving = true
|
|
45
|
+
continue_with(SAVING_MESSAGE, ADDRESS)
|
|
23
46
|
end
|
|
24
47
|
|
|
25
48
|
private
|
|
26
49
|
|
|
50
|
+
def name
|
|
51
|
+
Storage.parse_name(filename).first
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def finish
|
|
55
|
+
@saving = false
|
|
56
|
+
@bus.poke(0x90, DEVICE_NOT_PRESENT) unless @storage.write_file(name, payload)
|
|
57
|
+
@bus.poke(0xac, @bus.peek(0xae))
|
|
58
|
+
@bus.poke(0xad, @bus.peek(0xaf))
|
|
59
|
+
@cpu.y = 0
|
|
60
|
+
@cpu.status.overflow = true
|
|
61
|
+
continue_with(CLOCK_RELEASE, DATA_RELEASE, SAVE_DONE)
|
|
62
|
+
end
|
|
63
|
+
|
|
27
64
|
# Start address at $C1/$C2 (STAL), end address (exclusive) at
|
|
28
65
|
# $AE/$AF (EAL)
|
|
29
66
|
def payload
|
|
@@ -32,11 +69,6 @@ module Badline
|
|
|
32
69
|
[low_byte(start), high_byte(start)] +
|
|
33
70
|
Array.new(length) { |i| @bus.peek((start + i) & 0xffff) }
|
|
34
71
|
end
|
|
35
|
-
|
|
36
|
-
def finish
|
|
37
|
-
@bus.poke(0x90, 0x00)
|
|
38
|
-
@cpu.status.carry = false
|
|
39
|
-
end
|
|
40
72
|
end
|
|
41
73
|
end
|
|
42
74
|
end
|