badline 0.4.2 → 0.5.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 +37 -0
- data/CLAUDE.md +32 -27
- data/CONTRIBUTING.md +5 -0
- data/README.md +55 -24
- data/doc/pinned-behaviour.md +223 -0
- data/exe/badline-ruby +7 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/jukebox.rb +6 -4
- data/lib/badline/audio/machine_player.rb +10 -6
- data/lib/badline/cartridge/kcs_power.rb +3 -3
- data/lib/badline/cia.rb +21 -3
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +197 -0
- data/lib/badline/drive1541/disk.rb +256 -0
- data/lib/badline/drive1541/disk_via.rb +33 -0
- data/lib/badline/drive1541/gcr.rb +57 -0
- data/lib/badline/drive1541/idle.rb +136 -0
- data/lib/badline/drive1541/mechanism.rb +277 -0
- data/lib/badline/drive1541/orbit.rb +81 -0
- data/lib/badline/drive1541/rotation.rb +111 -0
- data/lib/badline/drive1541/sector_reader.rb +76 -0
- data/lib/badline/drive1541/serial_port.rb +67 -0
- data/lib/badline/drive1541/sleep.rb +114 -0
- data/lib/badline/drive1541/track.rb +98 -0
- data/lib/badline/drive1541.rb +234 -0
- data/lib/badline/gui/application.rb +15 -3
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +109 -0
- data/lib/badline/kernal_trap/channel.rb +9 -1
- data/lib/badline/kernal_trap/serial.rb +9 -3
- data/lib/badline/media/true_drive.rb +54 -0
- data/lib/badline/media.rb +22 -1
- data/lib/badline/memory.rb +3 -0
- data/lib/badline/options.rb +24 -4
- data/lib/badline/region.rb +35 -10
- data/lib/badline/reu/dma.rb +228 -0
- data/lib/badline/reu/ram.rb +65 -0
- data/lib/badline/reu/trigger.rb +32 -0
- data/lib/badline/reu.rb +276 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/dos1541.rom +0 -0
- data/lib/badline/sdl/functions.rb +1 -0
- data/lib/badline/storage/d64_image.rb +18 -9
- data/lib/badline/storage/d81_image.rb +1 -0
- data/lib/badline/storage/disk_image/catalog.rb +52 -0
- data/lib/badline/storage/disk_image/writing.rb +16 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/g64_image.rb +222 -0
- data/lib/badline/storage/host_directory.rb +21 -1
- data/lib/badline/storage/listing.rb +118 -0
- data/lib/badline/storage/t64.rb +14 -0
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/traps.rb +4 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +3 -0
- data/lib/badline/via/fast_forward.rb +53 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +36 -0
- data/lib/badline/via/timer2.rb +31 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/display_state.rb +7 -7
- data/lib/badline/vic/sprite/internal_bus.rb +11 -7
- data/lib/badline/vic/sprite/shifter.rb +10 -15
- data/lib/badline/vic/sprite/timing.rb +60 -0
- data/lib/badline/vic/sprite.rb +14 -12
- data/lib/badline/vic/sprites.rb +2 -2
- data/lib/badline/vic.rb +86 -39
- data/lib/badline.rb +3 -0
- metadata +28 -1
data/lib/badline/reu.rb
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/reu/ram"
|
|
4
|
+
require "badline/reu/dma"
|
|
5
|
+
require "badline/reu/trigger"
|
|
6
|
+
|
|
7
|
+
module Badline
|
|
8
|
+
# A RAM Expansion Unit: the Commodore 1700 (128K), 1764 (256K) and 1750
|
|
9
|
+
# (512K), and the bigger units built around the same 8726 RAM Expansion
|
|
10
|
+
# Controller (REC), such as the CMD 1750XL, up to 16M.
|
|
11
|
+
#
|
|
12
|
+
# The REC's eleven registers sit in I/O 2 from $DF00, repeated every 32
|
|
13
|
+
# bytes up to $DFFF, as the 1764 and 1750 manuals map them. It moves
|
|
14
|
+
# bytes between the C64 and its RAM by DMA, one bus cycle each, with the
|
|
15
|
+
# CPU held off the bus. A command with the execute bit set starts a
|
|
16
|
+
# transfer, or with bit 4 clear arms the REC to start one on the next
|
|
17
|
+
# write to $FF00, so a program can bank the ROMs out first. A transfer
|
|
18
|
+
# waits for the CPU's next read cycle with BA high, and Computer then
|
|
19
|
+
# clocks it through #dma_cycle! until it hands the bus back.
|
|
20
|
+
class REU
|
|
21
|
+
SIZES_KB = [128, 256, 512, 1024, 2048, 4096, 8192, 16_384].freeze
|
|
22
|
+
|
|
23
|
+
# The registers, by offset.
|
|
24
|
+
STATUS = 0x00
|
|
25
|
+
COMMAND = 0x01
|
|
26
|
+
C64_LOW = 0x02
|
|
27
|
+
C64_HIGH = 0x03
|
|
28
|
+
EXPANSION_LOW = 0x04
|
|
29
|
+
EXPANSION_HIGH = 0x05
|
|
30
|
+
EXPANSION_BANK = 0x06
|
|
31
|
+
LENGTH_LOW = 0x07
|
|
32
|
+
LENGTH_HIGH = 0x08
|
|
33
|
+
INTERRUPT_MASK = 0x09
|
|
34
|
+
ADDRESS_CONTROL = 0x0a
|
|
35
|
+
|
|
36
|
+
# Status: an interrupt is pending, the block ended, a verify found a
|
|
37
|
+
# difference (the manual's fault bit), and the unit is built from 256K
|
|
38
|
+
# chips, which only the 1700 isn't. Reading the status clears the top
|
|
39
|
+
# three.
|
|
40
|
+
INTERRUPT_PENDING = 0x80
|
|
41
|
+
END_OF_BLOCK = 0x40
|
|
42
|
+
FAULT = 0x20
|
|
43
|
+
CHIPS_256K = 0x10
|
|
44
|
+
|
|
45
|
+
# Command: execute, reload the registers after the transfer, don't wait
|
|
46
|
+
# for $FF00, and the transfer type in the bottom two bits.
|
|
47
|
+
EXECUTE = 0x80
|
|
48
|
+
AUTOLOAD = 0x20
|
|
49
|
+
NO_FF00_TRIGGER = 0x10
|
|
50
|
+
TRANSFER_TYPE = 0x03
|
|
51
|
+
|
|
52
|
+
# Interrupt mask: interrupts on at all, then on the end of a block and
|
|
53
|
+
# on a fault. The low five bits aren't there and read as 1.
|
|
54
|
+
INTERRUPTS_ON = 0x80
|
|
55
|
+
INTERRUPT_ON_END = 0x40
|
|
56
|
+
INTERRUPT_ON_FAULT = 0x20
|
|
57
|
+
|
|
58
|
+
# Address control: hold the C64 address or the expansion address where
|
|
59
|
+
# it is. The low six bits aren't there and read as 1.
|
|
60
|
+
FIX_C64 = 0x80
|
|
61
|
+
FIX_EXPANSION = 0x40
|
|
62
|
+
|
|
63
|
+
# The REC's bank register has three bits, and the other five read as
|
|
64
|
+
# 1. A unit past 512K latches all eight onto its own address lines.
|
|
65
|
+
BANK_STUCK = 0xf8
|
|
66
|
+
|
|
67
|
+
attr_reader :trigger
|
|
68
|
+
|
|
69
|
+
# bus is the bus the REC reaches C64 memory through, and vic the VIC,
|
|
70
|
+
# whose phi1 fetch is what a register read finds on the bus during a
|
|
71
|
+
# transfer.
|
|
72
|
+
def initialize(size_kb, bus:, vic:)
|
|
73
|
+
raise ArgumentError, "No #{size_kb}K REU. Pick one of #{SIZES_KB.join(', ')}." unless SIZES_KB.include?(size_kb)
|
|
74
|
+
|
|
75
|
+
# The 1700's REC counts 17 address bits, the others' 19.
|
|
76
|
+
span = size_kb == 128 ? 0x20000 : 0x80000
|
|
77
|
+
size = size_kb * 1024
|
|
78
|
+
@address_mask = [size, span].max - 1
|
|
79
|
+
@bank_bits = size > span ? 0xff : 0x07
|
|
80
|
+
@chips = size_kb == 128 ? 0 : CHIPS_256K
|
|
81
|
+
@ram = RAM.new(size, span)
|
|
82
|
+
@dma = DMA.new(@ram, span, bus)
|
|
83
|
+
@trigger = Trigger.new(self)
|
|
84
|
+
@vic = vic
|
|
85
|
+
@on_irq_change = nil
|
|
86
|
+
@on_dma = nil
|
|
87
|
+
@irq = false
|
|
88
|
+
reset!
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def on_irq_change(&block)
|
|
92
|
+
@on_irq_change = block
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Called when the REC asks for the bus.
|
|
96
|
+
def on_dma(&block)
|
|
97
|
+
@on_dma = block
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def irq? = @irq
|
|
101
|
+
|
|
102
|
+
# From the moment the REC asks for the bus until it hands it back.
|
|
103
|
+
def dma? = @requested || @running
|
|
104
|
+
|
|
105
|
+
# Whether the REC held the bus on the last cycle clocked. The CPU keeps
|
|
106
|
+
# it while the REC waits to start, and has it back from the cycle the
|
|
107
|
+
# REC lets go.
|
|
108
|
+
def holds_bus? = @running
|
|
109
|
+
|
|
110
|
+
def reset!
|
|
111
|
+
@status = @chips
|
|
112
|
+
@command = NO_FF00_TRIGGER
|
|
113
|
+
@c64 = @c64_start = 0
|
|
114
|
+
@expansion = @expansion_start = 0
|
|
115
|
+
@bank = @bank_start = 0
|
|
116
|
+
@length = @length_start = 0xffff
|
|
117
|
+
@interrupt_mask = 0x1f
|
|
118
|
+
@address_control = 0x3f
|
|
119
|
+
@armed = @requested = @running = false
|
|
120
|
+
interrupt(false)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def peek(addr)
|
|
124
|
+
return @vic.phi1_data if @running
|
|
125
|
+
|
|
126
|
+
register = addr & 0x1f
|
|
127
|
+
value = register_value(register)
|
|
128
|
+
read_status if register == STATUS
|
|
129
|
+
value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def poke(addr, value)
|
|
133
|
+
return if @running
|
|
134
|
+
|
|
135
|
+
register = addr & 0x1f
|
|
136
|
+
case register
|
|
137
|
+
when COMMAND then write_command(value)
|
|
138
|
+
when C64_LOW, C64_HIGH then @c64 = @c64_start = with_byte(@c64_start, value, register == C64_HIGH)
|
|
139
|
+
when EXPANSION_LOW, EXPANSION_HIGH
|
|
140
|
+
@expansion = @expansion_start = with_byte(@expansion_start, value, register == EXPANSION_HIGH)
|
|
141
|
+
when EXPANSION_BANK then @bank = @bank_start = value & @bank_bits
|
|
142
|
+
when LENGTH_LOW, LENGTH_HIGH
|
|
143
|
+
@length = @length_start = with_byte(@length_start, value, register == LENGTH_HIGH)
|
|
144
|
+
when INTERRUPT_MASK then write_interrupt_mask(value)
|
|
145
|
+
when ADDRESS_CONTROL then @address_control = value | 0x3f
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A write to $FF00 starts the transfer the command register armed.
|
|
150
|
+
def ff00_written
|
|
151
|
+
return unless @armed
|
|
152
|
+
|
|
153
|
+
@armed = false
|
|
154
|
+
request_bus
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Clocks the REC for a cycle, given the VIC's BA line and whether the
|
|
158
|
+
# CPU writes on it. A requested transfer starts on the first cycle the
|
|
159
|
+
# CPU reads with BA high.
|
|
160
|
+
def dma_cycle!(ba_low, cpu_writing)
|
|
161
|
+
if @requested
|
|
162
|
+
return if cpu_writing || ba_low
|
|
163
|
+
|
|
164
|
+
start_transfer
|
|
165
|
+
end
|
|
166
|
+
@dma.cycle!(ba_low)
|
|
167
|
+
end_transfer unless @dma.holds_bus?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The REU's RAM, at an address as the REC's registers give it.
|
|
171
|
+
def ram_peek(addr) = @ram.peek(addr)
|
|
172
|
+
|
|
173
|
+
def ram_poke(addr, value) = @ram.poke(addr, value)
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def register_value(register)
|
|
178
|
+
case register
|
|
179
|
+
when STATUS then @status
|
|
180
|
+
when COMMAND then @command
|
|
181
|
+
when C64_LOW then @c64 & 0xff
|
|
182
|
+
when C64_HIGH then @c64 >> 8
|
|
183
|
+
when EXPANSION_LOW then @expansion & 0xff
|
|
184
|
+
when EXPANSION_HIGH then @expansion >> 8
|
|
185
|
+
when EXPANSION_BANK then @bank | BANK_STUCK
|
|
186
|
+
when LENGTH_LOW then @length & 0xff
|
|
187
|
+
when LENGTH_HIGH then @length >> 8
|
|
188
|
+
when INTERRUPT_MASK then @interrupt_mask
|
|
189
|
+
when ADDRESS_CONTROL then @address_control
|
|
190
|
+
else 0xff
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def read_status
|
|
195
|
+
@status &= CHIPS_256K
|
|
196
|
+
interrupt(false)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Writing either byte of an address or the length sets the value the
|
|
200
|
+
# transfer starts from, and puts the counter back to it.
|
|
201
|
+
def with_byte(word, value, high)
|
|
202
|
+
high ? (word & 0x00ff) | (value << 8) : (word & 0xff00) | value
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def write_command(value)
|
|
206
|
+
@command = value
|
|
207
|
+
return if value.nobits?(EXECUTE)
|
|
208
|
+
|
|
209
|
+
@armed = value.nobits?(NO_FF00_TRIGGER)
|
|
210
|
+
request_bus unless @armed
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# An interrupt enabled over a block that already ended, or a fault
|
|
214
|
+
# already found, fires at once.
|
|
215
|
+
def write_interrupt_mask(value)
|
|
216
|
+
@interrupt_mask = value | 0x1f
|
|
217
|
+
interrupt_on(@status)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def interrupt_on(events)
|
|
221
|
+
return unless @interrupt_mask.anybits?(INTERRUPTS_ON)
|
|
222
|
+
return unless (events.anybits?(END_OF_BLOCK) && @interrupt_mask.anybits?(INTERRUPT_ON_END)) ||
|
|
223
|
+
(events.anybits?(FAULT) && @interrupt_mask.anybits?(INTERRUPT_ON_FAULT))
|
|
224
|
+
|
|
225
|
+
@status |= INTERRUPT_PENDING
|
|
226
|
+
interrupt(true)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def interrupt(level)
|
|
230
|
+
return if @irq == level
|
|
231
|
+
|
|
232
|
+
@irq = level
|
|
233
|
+
@on_irq_change&.call(level)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def request_bus
|
|
237
|
+
@requested = true
|
|
238
|
+
@on_dma&.call
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def start_transfer
|
|
242
|
+
@requested = false
|
|
243
|
+
@running = true
|
|
244
|
+
@dma.start(@command & TRANSFER_TYPE, @c64, @expansion | (@bank << 16), @length, @address_control)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The execute bit drops and the $FF00 trigger turns off. With autoload
|
|
248
|
+
# the addresses and the length go back to where the transfer started,
|
|
249
|
+
# and otherwise they stay where it stopped.
|
|
250
|
+
def end_transfer
|
|
251
|
+
@running = false
|
|
252
|
+
@command = (@command & ~EXECUTE & 0xff) | NO_FF00_TRIGGER
|
|
253
|
+
events = @dma.events
|
|
254
|
+
@status |= events
|
|
255
|
+
@command.anybits?(AUTOLOAD) ? reload : keep_counters
|
|
256
|
+
interrupt_on(events)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def reload
|
|
260
|
+
@c64 = @c64_start
|
|
261
|
+
@expansion = @expansion_start
|
|
262
|
+
@bank = @bank_start
|
|
263
|
+
@length = @length_start
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def keep_counters
|
|
267
|
+
@c64 = @dma.c64 if @address_control.nobits?(FIX_C64)
|
|
268
|
+
@length = @dma.length & 0xffff
|
|
269
|
+
return if @address_control.anybits?(FIX_EXPANSION)
|
|
270
|
+
|
|
271
|
+
address = @dma.expansion & @address_mask
|
|
272
|
+
@expansion = address & 0xffff
|
|
273
|
+
@bank = (address >> 16) & 0xff
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
data/lib/badline/roms/README
CHANGED
|
@@ -16,3 +16,13 @@ the C128's C64 mode.
|
|
|
16
16
|
character.rom
|
|
17
17
|
-------------
|
|
18
18
|
The character generator ROM.
|
|
19
|
+
|
|
20
|
+
dos1541.rom
|
|
21
|
+
-----------
|
|
22
|
+
The 1541 disk drive's DOS 2.6, 16 KB for $C000-$FFFF: 325302-01
|
|
23
|
+
($C000-$DFFF) followed by 901229-05 ($E000-$FFFF). Drive1541 loads it.
|
|
24
|
+
|
|
25
|
+
Both halves downloaded from
|
|
26
|
+
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1541/
|
|
27
|
+
(1541-c000.325302-01.bin and 1541-e000.901229-05.bin) and concatenated.
|
|
28
|
+
SHA-1 ab16f56989b27d89babe5f89c5a8cb3da71a82f0.
|
|
Binary file
|
|
@@ -42,6 +42,7 @@ module Badline
|
|
|
42
42
|
RenderSetLogicalSize = function("SDL_RenderSetLogicalSize", [POINTER, INT, INT], INT)
|
|
43
43
|
SetRenderDrawColor = function("SDL_SetRenderDrawColor", [POINTER, UINT8, UINT8, UINT8, UINT8], INT)
|
|
44
44
|
RenderClear = function("SDL_RenderClear", [POINTER], INT)
|
|
45
|
+
RenderFillRect = function("SDL_RenderFillRect", [POINTER, POINTER], INT)
|
|
45
46
|
RenderCopy = function("SDL_RenderCopy", [POINTER, POINTER, POINTER, POINTER], INT)
|
|
46
47
|
RenderPresent = function("SDL_RenderPresent", [POINTER], VOID)
|
|
47
48
|
CreateRGBSurfaceFrom = function("SDL_CreateRGBSurfaceFrom",
|
|
@@ -7,10 +7,28 @@ module Badline
|
|
|
7
7
|
# (174848 bytes without one), 40 tracks (196608) and 42 tracks (205312)
|
|
8
8
|
ERROR_TABLES = { 175_531 => 683, 197_376 => 768, 206_114 => 802 }.freeze
|
|
9
9
|
|
|
10
|
+
# The tracks the image holds: 35, 40 or 42.
|
|
11
|
+
def track_count
|
|
12
|
+
blocks = @bytes.length / SECTOR_SIZE
|
|
13
|
+
count = 0
|
|
14
|
+
count += 1 while count < 42 && track_offset(count + 2) <= blocks
|
|
15
|
+
count
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sectors_in(track)
|
|
19
|
+
case track
|
|
20
|
+
when 1..17 then 21
|
|
21
|
+
when 18..24 then 19
|
|
22
|
+
when 25..30 then 18
|
|
23
|
+
else 17
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
10
27
|
private
|
|
11
28
|
|
|
12
29
|
def error_tables = ERROR_TABLES
|
|
13
30
|
|
|
31
|
+
def header_name = 0x90
|
|
14
32
|
def directory_track = 18
|
|
15
33
|
def directory_sector = 1
|
|
16
34
|
def directory_interleave = 3
|
|
@@ -23,15 +41,6 @@ module Badline
|
|
|
23
41
|
def bam_tracks = 1..35
|
|
24
42
|
def bam_count(track) = [18, 0, 4 * track]
|
|
25
43
|
def bam_bitmap(track) = [18, 0, (4 * track) + 1]
|
|
26
|
-
|
|
27
|
-
def sectors_in(track)
|
|
28
|
-
case track
|
|
29
|
-
when 1..17 then 21
|
|
30
|
-
when 18..24 then 19
|
|
31
|
-
when 25..30 then 18
|
|
32
|
-
else 17
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
44
|
end
|
|
36
45
|
end
|
|
37
46
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
# The directory as LOAD"$" lists it: the disk name and ID from the
|
|
7
|
+
# header block, every entry with a type, and the free blocks the BAM
|
|
8
|
+
# counts outside the directory track.
|
|
9
|
+
module Catalog
|
|
10
|
+
HEADER_ID = 18
|
|
11
|
+
ID_LENGTH = 5
|
|
12
|
+
TYPE_MASK = 0x07
|
|
13
|
+
|
|
14
|
+
def directory
|
|
15
|
+
header = sector_at(*header_block)
|
|
16
|
+
Listing.new(name: header[header_name, DiskImage::Directory::NAME_LENGTH],
|
|
17
|
+
id: header[header_name + HEADER_ID, ID_LENGTH],
|
|
18
|
+
entries: catalog_entries,
|
|
19
|
+
blocks_free:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# A slot whose type byte is zero is empty or scratched.
|
|
25
|
+
def catalog_entries
|
|
26
|
+
list = []
|
|
27
|
+
each_sector(directory_track, directory_sector) do |data|
|
|
28
|
+
(0...ENTRIES_PER_SECTOR).each do |i|
|
|
29
|
+
entry = data[i * ENTRY_SIZE, ENTRY_SIZE]
|
|
30
|
+
list << catalog_entry(entry) unless entry[2].zero?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
list
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def catalog_entry(entry)
|
|
37
|
+
Listing::Entry.new(name: entry[5, DiskImage::Directory::NAME_LENGTH],
|
|
38
|
+
type: entry[2] & TYPE_MASK,
|
|
39
|
+
blocks: entry[30] | (entry[31] << 8),
|
|
40
|
+
closed: entry[2].anybits?(DiskImage::Directory::CLOSED),
|
|
41
|
+
locked: entry[2].anybits?(0x40))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def blocks_free
|
|
45
|
+
(bam_tracks.to_a - reserved_tracks).sum do |track|
|
|
46
|
+
block?(*bam_count(track).first(2)) ? @bytes[count_offset(track)] : 0
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -57,6 +57,22 @@ module Badline
|
|
|
57
57
|
update_image { put_sector(track, sector, data) } if block?(track, sector)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# Stores blocks as a true drive read them back off its disk, in one
|
|
61
|
+
# write to the host file. Each is [track, sector, data, error]: nil
|
|
62
|
+
# data leaves the block's bytes as they were, and the error, the
|
|
63
|
+
# DOS error a read of it raises or nil, goes into the error table
|
|
64
|
+
# when the image has one.
|
|
65
|
+
def store_blocks(blocks)
|
|
66
|
+
update_image do
|
|
67
|
+
blocks.each do |track, sector, data, error|
|
|
68
|
+
next unless block?(track, sector)
|
|
69
|
+
|
|
70
|
+
put_sector(track, sector, data) if data
|
|
71
|
+
@errors[track_offset(track) + sector] = error ? DOS_ERRORS.key(error) : 1 if @errors
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
# Marks a block in use, as B-A does.
|
|
61
77
|
def allocate_block(track, sector) = update_image { mark(track, sector, false) }
|
|
62
78
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/storage/disk_image/bam"
|
|
4
|
+
require "badline/storage/disk_image/catalog"
|
|
4
5
|
require "badline/storage/disk_image/directory"
|
|
5
6
|
require "badline/storage/disk_image/writing"
|
|
6
7
|
|
|
@@ -12,6 +13,7 @@ module Badline
|
|
|
12
13
|
# written.
|
|
13
14
|
class DiskImage
|
|
14
15
|
include Bam
|
|
16
|
+
include Catalog
|
|
15
17
|
include Directory
|
|
16
18
|
include Writing
|
|
17
19
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# A .g64 image: a 1541 disk's flux as raw GCR, track by track, for the
|
|
6
|
+
# true drive to read the way it reads a real disk, copy protection and
|
|
7
|
+
# all. It has no file system of its own, so only the true drive
|
|
8
|
+
# (Drive1541::Disk.from_g64) can use it.
|
|
9
|
+
#
|
|
10
|
+
# The header is the signature "GCR-1541", a version byte ($00), the
|
|
11
|
+
# number of half tracks the tables cover (84, tracks 1 to 42.5) and
|
|
12
|
+
# the longest track a block may hold, a little-endian word. Two tables
|
|
13
|
+
# of little-endian longs follow, one entry per half track from track
|
|
14
|
+
# 1: the file offset of each half track's block, 0 for one without
|
|
15
|
+
# data, and its speed zone. A block is the track's length in bytes, a
|
|
16
|
+
# word, and then its bytes.
|
|
17
|
+
#
|
|
18
|
+
# A speed entry of 0 to 3 is the zone the whole track was written in.
|
|
19
|
+
# A larger one is the offset of a speed map, two bits a byte, four
|
|
20
|
+
# track bytes to a map byte, the first byte's in the top two bits, for
|
|
21
|
+
# a track whose zone changes along it. The drive passes each byte
|
|
22
|
+
# under the head at its own zone's rate (Drive1541::Track).
|
|
23
|
+
#
|
|
24
|
+
# The drive writes a track at one zone, so a track written back takes
|
|
25
|
+
# that zone as its speed entry. Writing it puts it in its own block,
|
|
26
|
+
# where it fits, and otherwise appends a block to the file for it, as
|
|
27
|
+
# for a half track that had no data. A track longer than the header's longest raises
|
|
28
|
+
# it, so the header stays true of every block. A half track beyond
|
|
29
|
+
# the tables stays on the disk alone. One opened with `read_only` is a
|
|
30
|
+
# write-protected disk, and the host file is never written.
|
|
31
|
+
#
|
|
32
|
+
# Opening an image checks that its header and tables are whole, and
|
|
33
|
+
# that every block and speed map lies past the tables and inside the
|
|
34
|
+
# file, and raises FormatError where one doesn't.
|
|
35
|
+
class G64Image
|
|
36
|
+
class FormatError < StandardError; end
|
|
37
|
+
|
|
38
|
+
SIGNATURE = "GCR-1541"
|
|
39
|
+
HEADER_SIZE = 12
|
|
40
|
+
HALF_TRACKS = 84
|
|
41
|
+
MAX_TRACK_SIZE = 7928
|
|
42
|
+
|
|
43
|
+
# A new image of +tracks+, {entry => [bytes, zone]} with entry 0 for
|
|
44
|
+
# track 1 and 1 for track 1.5, laid out as VICE lays one out: every
|
|
45
|
+
# block as long as the longest track.
|
|
46
|
+
def self.create(path, tracks, half_tracks: HALF_TRACKS)
|
|
47
|
+
max = [MAX_TRACK_SIZE, *tracks.values.map { |bytes, _| bytes.length }].max
|
|
48
|
+
offsets = Array.new(half_tracks, 0)
|
|
49
|
+
zones = Array.new(half_tracks, 0)
|
|
50
|
+
blocks = +"".b
|
|
51
|
+
position = HEADER_SIZE + (half_tracks * 8)
|
|
52
|
+
tracks.keys.sort.each do |entry|
|
|
53
|
+
bytes, zones[entry] = tracks[entry]
|
|
54
|
+
offsets[entry] = position + blocks.bytesize
|
|
55
|
+
blocks << block(bytes, max)
|
|
56
|
+
end
|
|
57
|
+
header = SIGNATURE.b + [0, half_tracks, max].pack("CCv")
|
|
58
|
+
File.binwrite(path, header + offsets.pack("V*") + zones.pack("V*") + blocks)
|
|
59
|
+
new(path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.block(bytes, size)
|
|
63
|
+
[bytes.length].pack("v") + bytes.pack("C*").ljust(size, "\x00".b)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
attr_reader :half_tracks, :max_track_size
|
|
67
|
+
|
|
68
|
+
def initialize(path, read_only: false)
|
|
69
|
+
@path = path
|
|
70
|
+
@read_only = read_only
|
|
71
|
+
@data = File.binread(path)
|
|
72
|
+
parse
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Whether the image was opened write-protected.
|
|
76
|
+
def read_only? = @read_only
|
|
77
|
+
|
|
78
|
+
# Whether the host file takes writes.
|
|
79
|
+
def writable? = !read_only? && File.writable?(@path)
|
|
80
|
+
|
|
81
|
+
# The track at table entry +entry+ (0 for track 1) as [bytes, zone],
|
|
82
|
+
# or nil where it has no data. A speed-mapped track's zone is the one
|
|
83
|
+
# most of its bytes were written in.
|
|
84
|
+
def track(entry)
|
|
85
|
+
offset = @offsets[entry].to_i
|
|
86
|
+
return if offset.zero?
|
|
87
|
+
|
|
88
|
+
length = block_length(offset)
|
|
89
|
+
return if length.zero?
|
|
90
|
+
|
|
91
|
+
speeds = mapped_speeds(entry, length)
|
|
92
|
+
[@data.byteslice(offset + 2, length).bytes, speeds ? majority(speeds) : @speeds[entry]]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Each byte's zone of the track at table entry +entry+, where its
|
|
96
|
+
# speed map changes along it, and nil otherwise.
|
|
97
|
+
def speeds(entry)
|
|
98
|
+
offset = @offsets[entry].to_i
|
|
99
|
+
return if offset.zero?
|
|
100
|
+
|
|
101
|
+
speeds = mapped_speeds(entry, block_length(offset))
|
|
102
|
+
speeds unless speeds.nil? || speeds.uniq.one?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Stores tracks the drive wrote, {entry => [bytes, zone]}, in one
|
|
106
|
+
# write to the host file. Each takes its zone as its speed entry.
|
|
107
|
+
def store_tracks(tracks)
|
|
108
|
+
raise WriteError, WriteError::WRITE_PROTECT_ON unless writable?
|
|
109
|
+
|
|
110
|
+
data = @data.dup
|
|
111
|
+
tracks.each do |entry, (bytes, zone)|
|
|
112
|
+
next unless entry < @half_tracks
|
|
113
|
+
|
|
114
|
+
store_track(data, entry, bytes, zone)
|
|
115
|
+
end
|
|
116
|
+
File.binwrite(@path, data)
|
|
117
|
+
@data = data
|
|
118
|
+
parse
|
|
119
|
+
rescue SystemCallError
|
|
120
|
+
parse
|
|
121
|
+
raise WriteError, WriteError::WRITE_PROTECT_ON
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def parse
|
|
127
|
+
raise FormatError, "Missing G64 signature" unless @data.start_with?(SIGNATURE)
|
|
128
|
+
raise FormatError, "G64 header is cut short" if @data.bytesize < HEADER_SIZE
|
|
129
|
+
|
|
130
|
+
version, @half_tracks, @max_track_size = @data.byteslice(8, 4).unpack("CCv")
|
|
131
|
+
raise FormatError, "Unsupported G64 version #{version}" unless version.zero?
|
|
132
|
+
|
|
133
|
+
tables = @data.byteslice(HEADER_SIZE, @half_tracks * 8).to_s
|
|
134
|
+
raise FormatError, "G64 track tables are cut short" if tables.bytesize < @half_tracks * 8
|
|
135
|
+
|
|
136
|
+
@offsets = tables.byteslice(0, @half_tracks * 4).unpack("V*")
|
|
137
|
+
@speeds = tables.byteslice(@half_tracks * 4, @half_tracks * 4).unpack("V*")
|
|
138
|
+
@half_tracks.times { |entry| check_block(entry) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def tables_end = HEADER_SIZE + (@half_tracks * 8)
|
|
142
|
+
|
|
143
|
+
def block_length(offset) = @data.byteslice(offset, 2).unpack1("v")
|
|
144
|
+
|
|
145
|
+
def check_block(entry)
|
|
146
|
+
offset = @offsets[entry]
|
|
147
|
+
return if offset.zero?
|
|
148
|
+
|
|
149
|
+
problem = misplaced(offset, 2) || misplaced(offset, 2 + block_length(offset))
|
|
150
|
+
raise FormatError, "G64 track entry #{entry} #{problem}" if problem
|
|
151
|
+
|
|
152
|
+
check_speed_map(entry, block_length(offset))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def check_speed_map(entry, length)
|
|
156
|
+
speed = @speeds[entry]
|
|
157
|
+
return if speed <= 3
|
|
158
|
+
|
|
159
|
+
problem = misplaced(speed, (length + 3) / 4)
|
|
160
|
+
raise FormatError, "G64 speed map of track entry #{entry} #{problem}" if problem
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# What's wrong with +size+ bytes at +offset+, or nil where they lie
|
|
164
|
+
# past the tables and inside the file.
|
|
165
|
+
def misplaced(offset, size)
|
|
166
|
+
if offset < tables_end then "points into the header or its tables"
|
|
167
|
+
elsif offset + size > @data.bytesize then "runs past the end of the image"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Each byte's zone from the track's speed map, or nil where its speed
|
|
172
|
+
# entry is a zone.
|
|
173
|
+
def mapped_speeds(entry, length)
|
|
174
|
+
speed = @speeds[entry]
|
|
175
|
+
return if speed <= 3
|
|
176
|
+
|
|
177
|
+
map = @data.byteslice(speed, (length + 3) / 4).to_s.bytes
|
|
178
|
+
Array.new(length) { |index| (map[index >> 2] >> (6 - ((index & 3) * 2))) & 3 }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The zone most of the bytes were written in.
|
|
182
|
+
def majority(speeds)
|
|
183
|
+
counts = [0, 0, 0, 0]
|
|
184
|
+
speeds.each { |zone| counts[zone] += 1 }
|
|
185
|
+
counts.index(counts.max)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def store_track(data, entry, bytes, zone)
|
|
189
|
+
offset = @offsets[entry]
|
|
190
|
+
if offset.zero? || bytes.length > capacity(offset)
|
|
191
|
+
offset = data.bytesize
|
|
192
|
+
grow(data, bytes.length)
|
|
193
|
+
data << self.class.block(bytes, @max_track_size)
|
|
194
|
+
else
|
|
195
|
+
data[offset, bytes.length + 2] = [bytes.length].pack("v") + bytes.pack("C*")
|
|
196
|
+
end
|
|
197
|
+
set_entry(data, entry, offset, zone)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The most bytes the block at +offset+ holds without running into
|
|
201
|
+
# the next block or speed map, or off the end of the file.
|
|
202
|
+
def capacity(offset)
|
|
203
|
+
ends = (@offsets + @speeds.select { |speed| speed > 3 }).select { |start| start > offset }
|
|
204
|
+
ends.push(@data.bytesize).min - offset - 2
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def grow(data, length)
|
|
208
|
+
return if length <= @max_track_size
|
|
209
|
+
|
|
210
|
+
@max_track_size = length
|
|
211
|
+
data[10, 2] = [length].pack("v")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_entry(data, entry, offset, zone)
|
|
215
|
+
@offsets[entry] = offset
|
|
216
|
+
@speeds[entry] = zone
|
|
217
|
+
data[HEADER_SIZE + (entry * 4), 4] = [offset].pack("V")
|
|
218
|
+
data[HEADER_SIZE + ((@half_tracks + entry) * 4), 4] = [zone].pack("V")
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|