badline 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +10 -5
- data/README.md +59 -24
- data/doc/pinned-behaviour.md +221 -0
- data/exe/badline-ruby +8 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/cli.rb +2 -1
- 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 +5 -1
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +134 -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 +184 -0
- data/lib/badline/drive1541/mechanism.rb +277 -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/track.rb +98 -0
- data/lib/badline/drive1541.rb +218 -0
- data/lib/badline/gui/application.rb +24 -7
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +93 -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/options.rb +30 -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 +38 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +30 -0
- data/lib/badline/via/timer2.rb +25 -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 +26 -1
data/lib/badline/region.rb
CHANGED
|
@@ -6,8 +6,9 @@ module Badline
|
|
|
6
6
|
# The timing and geometry a video standard gives the machine.
|
|
7
7
|
module Region
|
|
8
8
|
# The CPU clock, the VIC's raster, where the beam is blanked, the display
|
|
9
|
-
# window's edges, the part of the raster a front end shows,
|
|
10
|
-
#
|
|
9
|
+
# window's edges, the part of the raster a front end shows, the mains
|
|
10
|
+
# frequency that feeds the CIAs' TOD pins, and where the VIC's sprite
|
|
11
|
+
# fetches and X counter sit in the line.
|
|
11
12
|
#
|
|
12
13
|
# Columns are the VIC's cycles within a line, counted from 0, and a
|
|
13
14
|
# column draws 8 pixels, so the raster is `cycles_per_line * 8` pixels
|
|
@@ -16,8 +17,15 @@ module Badline
|
|
|
16
17
|
# is the larger. `display_x_bounds` holds the display window's
|
|
17
18
|
# [left, right] pixels per CSEL state, 38 columns then 40, and `crop`
|
|
18
19
|
# the [left, top, width, height] a front end shows.
|
|
20
|
+
#
|
|
21
|
+
# `sprite_cycle` is Bauer's cycle for sprite 0's p-access, and each
|
|
22
|
+
# sprite after it takes two more. `sprite_display_cycle` is the cycle
|
|
23
|
+
# that turns a sprite's display on or off. `x_hold` is how many pixels
|
|
24
|
+
# the X counter spends past its run, repeating $184-$187, and a raster
|
|
25
|
+
# of more than 512 pixels needs them.
|
|
19
26
|
Profile = Data.define(:name, :clock_hz, :cycles_per_line, :lines_per_frame, :hblank, :vblank,
|
|
20
|
-
:display_x_bounds, :crop, :mains_hz
|
|
27
|
+
:display_x_bounds, :crop, :mains_hz, :sprite_cycle, :sprite_display_cycle,
|
|
28
|
+
:x_hold)
|
|
21
29
|
|
|
22
30
|
# The 6569 of a PAL C64: 63 cycles by 312 lines at 985,248 Hz, with
|
|
23
31
|
# 50 Hz mains.
|
|
@@ -25,19 +33,36 @@ module Badline
|
|
|
25
33
|
name: :pal, clock_hz: 985_248, cycles_per_line: 63, lines_per_frame: 312,
|
|
26
34
|
hblank: [61, 9].freeze, vblank: [300, 15].freeze,
|
|
27
35
|
display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
|
|
28
|
-
crop: [96, 20, 384, 272].freeze, mains_hz: 50
|
|
36
|
+
crop: [96, 20, 384, 272].freeze, mains_hz: 50,
|
|
37
|
+
sprite_cycle: 58, sprite_display_cycle: 58, x_hold: 0
|
|
29
38
|
)
|
|
30
39
|
|
|
31
40
|
# The 6567R8 of an NTSC C64: 65 cycles by 263 lines at 1,022,727 Hz,
|
|
32
|
-
# with 60 Hz mains
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
41
|
+
# with 60 Hz mains. The sprite fetches and the compares ahead of them
|
|
42
|
+
# run a cycle later than on the 6569 (the spritesteal, spritex and
|
|
43
|
+
# phi1timing testprogs), and the X counter pauses near $184, as Bauer's
|
|
44
|
+
# 6567R8 diagram shows, reading $184-$187 for 8 pixels more. The
|
|
45
|
+
# blanked lines are the ones VICE's NTSC view leaves out, 12-27. It shows lines 0-11 below
|
|
46
|
+
# line 262, and the crop leaves those out.
|
|
36
47
|
NTSC = Profile.new(
|
|
37
48
|
name: :ntsc, clock_hz: 1_022_727, cycles_per_line: 65, lines_per_frame: 263,
|
|
38
|
-
hblank: [
|
|
49
|
+
hblank: [63, 9].freeze, vblank: [12, 27].freeze,
|
|
50
|
+
display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
|
|
51
|
+
crop: [96, 28, 384, 235].freeze, mains_hz: 60,
|
|
52
|
+
sprite_cycle: 59, sprite_display_cycle: 59, x_hold: 8
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# The 6567R56A of the first NTSC C64s: 64 cycles by 262 lines at
|
|
56
|
+
# 1,022,727 Hz. Its sprite fetches run a cycle later than the 6569's,
|
|
57
|
+
# as on the 6567R8, but it turns the display on in the same cycle as
|
|
58
|
+
# the 6569, and its X counter runs straight through 512 pixels, one
|
|
59
|
+
# coordinate for each.
|
|
60
|
+
NTSC_OLD = Profile.new(
|
|
61
|
+
name: :ntscold, clock_hz: 1_022_727, cycles_per_line: 64, lines_per_frame: 262,
|
|
62
|
+
hblank: [62, 9].freeze, vblank: [13, 27].freeze,
|
|
39
63
|
display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
|
|
40
|
-
crop: [96,
|
|
64
|
+
crop: [96, 28, 384, 234].freeze, mains_hz: 60,
|
|
65
|
+
sprite_cycle: 59, sprite_display_cycle: 58, x_hold: 0
|
|
41
66
|
)
|
|
42
67
|
|
|
43
68
|
# Whether a column or line lies in a [first, last] blanking span.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class REU
|
|
5
|
+
# One transfer between the C64 and the REU's RAM, clocked a cycle at a
|
|
6
|
+
# time while the REC holds the bus. The REC only reads the C64's bus
|
|
7
|
+
# while BA is high, so a read followed by BA low waits for BA to rise.
|
|
8
|
+
# After a write it goes on through the first BA-low cycle and stops at
|
|
9
|
+
# the second. See doc/pinned-behaviour.md, REU DMA.
|
|
10
|
+
class DMA
|
|
11
|
+
# Transfer types, as the command register's bottom two bits give
|
|
12
|
+
# them.
|
|
13
|
+
STASH = 0
|
|
14
|
+
FETCH = 1
|
|
15
|
+
SWAP = 2
|
|
16
|
+
VERIFY = 3
|
|
17
|
+
|
|
18
|
+
# What the REC did on the cycle before.
|
|
19
|
+
IDLE = 0
|
|
20
|
+
READ = 1
|
|
21
|
+
WRITE = 2
|
|
22
|
+
|
|
23
|
+
# Where the transfer stopped, what it left for the length register,
|
|
24
|
+
# and the status bits it ended with.
|
|
25
|
+
attr_reader :c64, :expansion, :length, :events
|
|
26
|
+
|
|
27
|
+
def initialize(ram, span, bus)
|
|
28
|
+
@ram = ram
|
|
29
|
+
@span = span
|
|
30
|
+
@bus = bus
|
|
31
|
+
@c64 = @expansion = @length = @events = 0
|
|
32
|
+
@holding = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whether the REC held the bus on the last cycle clocked, which it
|
|
36
|
+
# does while it waits for BA too, since the CPU stays halted.
|
|
37
|
+
def holds_bus? = @holding
|
|
38
|
+
|
|
39
|
+
# A length of 0 moves 64K. control is the address control register,
|
|
40
|
+
# which can hold either address where it is.
|
|
41
|
+
def start(type, c64, expansion, length, control)
|
|
42
|
+
@type = type
|
|
43
|
+
@c64 = c64
|
|
44
|
+
@expansion = expansion
|
|
45
|
+
@length = length.zero? ? 0x10000 : length
|
|
46
|
+
@c64_step = control.anybits?(FIX_C64) ? 0 : 1
|
|
47
|
+
@expansion_step = control.anybits?(FIX_EXPANSION) ? 0 : 1
|
|
48
|
+
@events = 0
|
|
49
|
+
@holding = true
|
|
50
|
+
@last = IDLE
|
|
51
|
+
@waiting = false
|
|
52
|
+
@ba_low_after_writes = 0
|
|
53
|
+
@stopped_after_write = false
|
|
54
|
+
@swap_write_due = false
|
|
55
|
+
@swap_byte = 0
|
|
56
|
+
@held_read = 0
|
|
57
|
+
@moved_all = false
|
|
58
|
+
@extra_cycle = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# One cycle of the transfer. The cycle the REC lets go of the bus on
|
|
62
|
+
# is the CPU's.
|
|
63
|
+
def cycle!(ba_low)
|
|
64
|
+
note_ba(ba_low)
|
|
65
|
+
settle_held_read(ba_low) if @held_read.positive?
|
|
66
|
+
return if @waiting && ba_low
|
|
67
|
+
|
|
68
|
+
@waiting = false
|
|
69
|
+
@moved_all ? wind_down : move(ba_low)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# The second BA-low cycle in a row after writes stops the REC, and so
|
|
75
|
+
# does any BA-low cycle after a read. It then waits for BA to rise.
|
|
76
|
+
def note_ba(ba_low)
|
|
77
|
+
if @last == READ
|
|
78
|
+
@waiting = ba_low
|
|
79
|
+
elsif @last == WRITE
|
|
80
|
+
@ba_low_after_writes = ba_low ? @ba_low_after_writes + 1 : 0
|
|
81
|
+
@stopped_after_write = @ba_low_after_writes == 2
|
|
82
|
+
if @stopped_after_write
|
|
83
|
+
@waiting = true
|
|
84
|
+
@ba_low_after_writes = 0
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
@last = IDLE
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A fetch or a swap whose last write ran into BA, or a verify that
|
|
91
|
+
# found a difference before its last byte, takes one more cycle before
|
|
92
|
+
# the REC lets go of the bus.
|
|
93
|
+
def wind_down
|
|
94
|
+
if @extra_cycle || (@stopped_after_write && (@type == FETCH || @type == SWAP))
|
|
95
|
+
@extra_cycle = @stopped_after_write = false
|
|
96
|
+
@last = READ
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@holding = false
|
|
101
|
+
settle
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def move(ba_low)
|
|
105
|
+
case @type
|
|
106
|
+
when STASH then stash
|
|
107
|
+
when FETCH then fetch
|
|
108
|
+
when SWAP then swap(ba_low)
|
|
109
|
+
else verify
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The REU's data bus latch keeps the last byte the REC stored.
|
|
114
|
+
def stash
|
|
115
|
+
byte = read_c64
|
|
116
|
+
@ram.poke(@expansion, byte)
|
|
117
|
+
@ram.latch = byte
|
|
118
|
+
step
|
|
119
|
+
@last = READ
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def fetch
|
|
123
|
+
byte = @ram.peek(@expansion)
|
|
124
|
+
@ram.latch = byte
|
|
125
|
+
write_c64(byte)
|
|
126
|
+
step
|
|
127
|
+
@last = WRITE
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Two cycles a byte: the C64's byte is read and stored while the
|
|
131
|
+
# REU's is fetched, and the REU's is written to the C64 on the next.
|
|
132
|
+
def swap(ba_low)
|
|
133
|
+
if @swap_write_due
|
|
134
|
+
@swap_write_due = false
|
|
135
|
+
write_c64(@swap_byte)
|
|
136
|
+
step
|
|
137
|
+
@last = WRITE
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
@swap_byte = @ram.peek(@expansion)
|
|
142
|
+
@swap_write_due = true
|
|
143
|
+
@last = READ
|
|
144
|
+
return swap_read_on_ba if ba_low
|
|
145
|
+
|
|
146
|
+
@ram.poke(@expansion, read_c64)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A swap's read on the first BA-low cycle after a write, while AEC is
|
|
150
|
+
# still high. On the last byte the read is made there, the write
|
|
151
|
+
# follows on the next cycle whatever BA does, and the REC lets go.
|
|
152
|
+
# Otherwise the read stays open and takes what is on the bus two
|
|
153
|
+
# cycles later, or on the cycle BA rises if that comes first. Pinned
|
|
154
|
+
# by REU/reutiming2/e5-m2, f3-m2 and f4-m2.
|
|
155
|
+
def swap_read_on_ba
|
|
156
|
+
return @held_read = 2 unless @length == 1
|
|
157
|
+
|
|
158
|
+
@ram.poke(@expansion, read_c64)
|
|
159
|
+
@last = IDLE
|
|
160
|
+
@ba_low_after_writes = 0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def settle_held_read(ba_low)
|
|
164
|
+
@held_read -= 1
|
|
165
|
+
return if @held_read.positive? && ba_low
|
|
166
|
+
|
|
167
|
+
@held_read = 0
|
|
168
|
+
@ram.poke(@expansion, read_c64)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A difference ends the transfer, with one cycle more if bytes were
|
|
172
|
+
# left to compare.
|
|
173
|
+
def verify
|
|
174
|
+
same = @ram.peek(@expansion) == read_c64
|
|
175
|
+
step
|
|
176
|
+
@last = READ
|
|
177
|
+
return if same
|
|
178
|
+
|
|
179
|
+
@events |= FAULT
|
|
180
|
+
@extra_cycle = @length.positive?
|
|
181
|
+
@moved_all = true
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The REC reaches C64 memory as the CPU sees it, but at $00 and $01
|
|
185
|
+
# it reaches the RAM under the CPU's port.
|
|
186
|
+
def read_c64
|
|
187
|
+
@c64 < 0x02 ? @bus.ram.peek(@c64) : @bus.peek(@c64)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def write_c64(byte)
|
|
191
|
+
@c64 < 0x02 ? @bus.ram.poke(@c64, byte) : @bus.poke(@c64, byte)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The REC counts the expansion address in its 19 low bits, from the
|
|
195
|
+
# top of its span back to 0, and leaves the bank bits above them as
|
|
196
|
+
# they were.
|
|
197
|
+
def step
|
|
198
|
+
@c64 = (@c64 + @c64_step) & 0xffff
|
|
199
|
+
low = (@expansion & 0x7ffff) + @expansion_step
|
|
200
|
+
@expansion = (@expansion & 0xf80000) | (low == @span ? 0 : low)
|
|
201
|
+
@length -= 1
|
|
202
|
+
@moved_all = true if @length.zero?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The length register is left at 1 after a whole block. A fetch has
|
|
206
|
+
# already fetched the byte after its last into the latch.
|
|
207
|
+
def settle
|
|
208
|
+
return settle_verify if @type == VERIFY
|
|
209
|
+
|
|
210
|
+
@length += 1
|
|
211
|
+
@events |= END_OF_BLOCK
|
|
212
|
+
@ram.latch = @ram.peek(@expansion) if @type == FETCH
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# A verify that compared every byte ends the block. One that found a
|
|
216
|
+
# difference on the byte before the last compares the last too, and
|
|
217
|
+
# ends the block if that one is the same.
|
|
218
|
+
def settle_verify
|
|
219
|
+
if @length.zero?
|
|
220
|
+
@length = 1
|
|
221
|
+
@events |= END_OF_BLOCK
|
|
222
|
+
elsif @length == 1 && @ram.peek(@expansion) == read_c64
|
|
223
|
+
@events |= END_OF_BLOCK
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class REU
|
|
5
|
+
# The REU's DRAM, as the REC addresses it. The REC counts 19 address
|
|
6
|
+
# bits, 17 on the 1700, and the 1764 fills only the first 256K of
|
|
7
|
+
# them: past that a read finds the latch on the REU's data bus, which
|
|
8
|
+
# holds the last byte the REC moved. A unit past 512K takes its upper
|
|
9
|
+
# address lines from the bank register.
|
|
10
|
+
class RAM
|
|
11
|
+
BANK_SIZE = 0x10000
|
|
12
|
+
|
|
13
|
+
# Where a bank of a 1764 that has just been switched on reads
|
|
14
|
+
# inverted, over the first 128K of each 256K. Over the second 128K
|
|
15
|
+
# it is the rest of the bank that reads inverted. Measured from
|
|
16
|
+
# REU/raminitpattern/dumpfile-256k-x1541.bin, a real 1764's dump.
|
|
17
|
+
INVERTED = [0x2a00...0x5400, 0x8000...0xac00, 0xd600...0x10000].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :size
|
|
20
|
+
attr_accessor :latch
|
|
21
|
+
|
|
22
|
+
def initialize(size, span)
|
|
23
|
+
@size = size
|
|
24
|
+
@mask = (size > 0x80000 ? size : span) - 1
|
|
25
|
+
@banks = Array.new(size / BANK_SIZE) { [] }
|
|
26
|
+
@power_on = [[], []]
|
|
27
|
+
@latch = 0xff
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def peek(addr)
|
|
31
|
+
addr &= @mask
|
|
32
|
+
addr < @size ? bank(addr >> 16)[addr & 0xffff] : @latch
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def poke(addr, value)
|
|
36
|
+
addr &= @mask
|
|
37
|
+
bank(addr >> 16)[addr & 0xffff] = value if addr < @size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# A bank takes its power-on contents the first time it is touched,
|
|
43
|
+
# so an REU costs only the memory a program uses.
|
|
44
|
+
def bank(number)
|
|
45
|
+
bank = @banks[number]
|
|
46
|
+
return bank unless bank.empty?
|
|
47
|
+
|
|
48
|
+
half = (number >> 1) & 1
|
|
49
|
+
@power_on[half] = power_on_bank(half == 1) if @power_on[half].empty?
|
|
50
|
+
@banks[number] = @power_on[half].dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Pairs of $FF and $00 bytes, starting with a single $FF, and each
|
|
54
|
+
# odd page the inverse of the even one before it.
|
|
55
|
+
def power_on_bank(upper)
|
|
56
|
+
Array.new(BANK_SIZE) do |offset|
|
|
57
|
+
byte = (offset + 1).anybits?(2) ? 0x00 : 0xff
|
|
58
|
+
byte ^= 0xff if offset.anybits?(0x100)
|
|
59
|
+
inverted = INVERTED.any? { |range| range.cover?(offset) }
|
|
60
|
+
inverted == upper ? byte : byte ^ 0xff
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class REU
|
|
5
|
+
# Stands in for the page at $FF00 in the write table, passing each
|
|
6
|
+
# write on to what is mapped there and telling the REC when $FF00 is
|
|
7
|
+
# written, which starts an armed transfer.
|
|
8
|
+
class Trigger
|
|
9
|
+
ADDRESS = 0xff00
|
|
10
|
+
|
|
11
|
+
def initialize(reu)
|
|
12
|
+
@reu = reu
|
|
13
|
+
@target = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Puts the trigger in front of target and returns it.
|
|
17
|
+
def wrap(target)
|
|
18
|
+
@target = target
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def peek(addr)
|
|
23
|
+
@target.peek(addr)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def poke(addr, value)
|
|
27
|
+
@target.poke(addr, value)
|
|
28
|
+
@reu.ff00_written if addr == ADDRESS
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
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",
|