badline 0.10.0 → 0.11.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 +34 -0
- data/CLAUDE.md +5 -3
- data/README.md +60 -5
- data/doc/library.md +15 -1
- data/doc/pinned-behaviour.md +75 -0
- data/doc/snapshots.md +19 -3
- data/lib/badline/address_bus/fittings.rb +38 -0
- data/lib/badline/address_bus/pla.rb +38 -21
- data/lib/badline/address_bus/roms.rb +12 -5
- data/lib/badline/address_bus.rb +20 -2
- data/lib/badline/c128/banks.rb +35 -0
- data/lib/badline/c128/bus.rb +347 -0
- data/lib/badline/c128/bus_state.rb +53 -0
- data/lib/badline/c128/color_lines.rb +39 -0
- data/lib/badline/c128/cpu.rb +10 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +182 -0
- data/lib/badline/c128/mmu_pages.rb +105 -0
- data/lib/badline/c128/model.rb +39 -0
- data/lib/badline/c128/modes.rb +48 -0
- data/lib/badline/c128/saved_state.rb +120 -0
- data/lib/badline/c128/vdc/memory.rb +54 -0
- data/lib/badline/c128/vdc/painter.rb +143 -0
- data/lib/badline/c128/vdc/raster.rb +97 -0
- data/lib/badline/c128/vdc/window.rb +215 -0
- data/lib/badline/c128/vdc.rb +268 -0
- data/lib/badline/c128.rb +320 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/computer/attachments.rb +4 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +13 -8
- data/lib/badline/frontend/app.rb +34 -21
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_pages.rb +6 -1
- data/lib/badline/frontend/menu_ports.rb +1 -1
- data/lib/badline/frontend/pause_menu.rb +1 -0
- data/lib/badline/frontend/screen.rb +33 -8
- data/lib/badline/frontend/screens.rb +90 -0
- data/lib/badline/frontend/timeline.rb +15 -4
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +58 -8
- data/lib/badline/kernal_trap/load.rb +1 -1
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +5 -0
- data/lib/badline/keyboard.rb +12 -1
- data/lib/badline/machine.rb +16 -7
- data/lib/badline/media.rb +8 -6
- data/lib/badline/model.rb +29 -12
- data/lib/badline/options/event.rb +5 -1
- data/lib/badline/options/family.rb +52 -0
- data/lib/badline/options/help.rb +5 -1
- data/lib/badline/options/table.rb +8 -2
- data/lib/badline/options/validation.rb +14 -1
- data/lib/badline/options.rb +20 -19
- data/lib/badline/roms/README +51 -0
- data/lib/badline/roms/c128/basic.rom +0 -0
- data/lib/badline/roms/c128/character.rom +0 -0
- data/lib/badline/roms/c128/kernal.rom +0 -0
- data/lib/badline/roms/c64gs.rom +0 -0
- data/lib/badline/roms/kernal-pet64.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +14 -0
- data/lib/badline/snapshot/setup.rb +15 -10
- data/lib/badline/snapshot/state.rb +5 -4
- data/lib/badline/snapshot/vice.rb +2 -1
- data/lib/badline/snapshot.rb +9 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +2 -1
- data/lib/badline/vic/display_state.rb +12 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic.rb +124 -9
- data/lib/badline.rb +1 -0
- metadata +27 -1
data/lib/badline/c128.rb
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/c128/model"
|
|
4
|
+
require "badline/c128/mmu"
|
|
5
|
+
require "badline/c128/vdc"
|
|
6
|
+
require "badline/c128/mmu_pages"
|
|
7
|
+
require "badline/c128/color_lines"
|
|
8
|
+
require "badline/c128/banks"
|
|
9
|
+
require "badline/c128/bus_state"
|
|
10
|
+
require "badline/c128/bus"
|
|
11
|
+
require "badline/c128/cpu"
|
|
12
|
+
require "badline/c128/saved_state"
|
|
13
|
+
require "badline/c128/keys"
|
|
14
|
+
require "badline/c128/modes"
|
|
15
|
+
|
|
16
|
+
module Badline
|
|
17
|
+
# The Commodore 128: the 8502 on the VIC-IIe's clock, two CIAs, the SID,
|
|
18
|
+
# the VDC and its 80 column display, and 128K of RAM, which the 8722 MMU
|
|
19
|
+
# maps (C128::Bus).
|
|
20
|
+
#
|
|
21
|
+
# Built for C128 mode, it runs BASIC 7.0 and the C128 KERNAL from reset.
|
|
22
|
+
# The C128's Z80 runs first and hands the bus to the 8502, but here the
|
|
23
|
+
# 8502 starts at its reset vector, with the MMU as the Z80 leaves it.
|
|
24
|
+
# The KERNAL goes to C64 mode as on the C128, with C= held at reset, a
|
|
25
|
+
# C64 cartridge or GO64.
|
|
26
|
+
#
|
|
27
|
+
# Built for C64 mode, the default, it powers on in C64 mode, the state
|
|
28
|
+
# the C128 KERNAL reaches when C= is held at power-on, and a reset comes
|
|
29
|
+
# back there. It runs the C64's BASIC and KERNAL, so the C64's KERNAL
|
|
30
|
+
# traps, the keyboard buffer and CHROUT capture work unchanged.
|
|
31
|
+
#
|
|
32
|
+
# Where a C64 program sees it differ from a C64C: the 8502's P6 senses CAPS
|
|
33
|
+
# LOCK, $D02F drives the extra keyboard rows, $D030's FAST bit runs the
|
|
34
|
+
# CPU at 2 MHz and its TEST bit races the raster counter, the SID has no
|
|
35
|
+
# mirrors, and $D500-$D7FF holds the hidden MMU, the VDC and open bus.
|
|
36
|
+
class C128
|
|
37
|
+
include IntegerHelper
|
|
38
|
+
include KeyboardBuffer
|
|
39
|
+
include Computer::Attachments
|
|
40
|
+
include Computer::KernalTraps
|
|
41
|
+
include Keys
|
|
42
|
+
include Modes
|
|
43
|
+
|
|
44
|
+
# The C64's 8x8 matrix and the three rows K0-K2 select, in port B
|
|
45
|
+
# column order.
|
|
46
|
+
KEYBOARD_MATRIX = (Keyboard::C64_MATRIX + [
|
|
47
|
+
%i[help keypad8 keypad5 tab keypad2 keypad4 keypad7 keypad1],
|
|
48
|
+
%i[esc keypad_plus keypad_minus line_feed keypad_enter keypad6 keypad9 keypad3],
|
|
49
|
+
%i[alt keypad0 keypad_period crsr_up crsr_down crsr_left crsr_right no_scroll]
|
|
50
|
+
]).freeze
|
|
51
|
+
|
|
52
|
+
# The cycle at which #on_init's handlers run, once the KERNAL of the
|
|
53
|
+
# mode the machine is built for has booted: the C64's as on a C64, or
|
|
54
|
+
# BASIC 7.0's, which is READY by then.
|
|
55
|
+
C128_INIT_THRESHOLD = 2_000_000
|
|
56
|
+
|
|
57
|
+
# The C128 KERNAL's keyboard buffer and its count.
|
|
58
|
+
C128_KEYBOARD_BUFFER = 0x034a
|
|
59
|
+
C128_KEYBOARD_COUNT = 0xd0
|
|
60
|
+
|
|
61
|
+
attr_reader :cpu, :cycles, :drive1541, :model, :init_threshold
|
|
62
|
+
|
|
63
|
+
# The path of the disk or directory device 8 serves through the traps
|
|
64
|
+
# (Computer::Attachments), or an empty one.
|
|
65
|
+
def mounted_path = @drive.nil? ? "" : @drive.path
|
|
66
|
+
|
|
67
|
+
def family = :c128
|
|
68
|
+
|
|
69
|
+
def address_bus = @bus
|
|
70
|
+
|
|
71
|
+
def region = @bus.region
|
|
72
|
+
|
|
73
|
+
def vic = @bus.vic
|
|
74
|
+
|
|
75
|
+
def cia1 = @bus.cia1
|
|
76
|
+
|
|
77
|
+
def cia2 = @bus.cia2
|
|
78
|
+
|
|
79
|
+
def sid = @bus.sid
|
|
80
|
+
|
|
81
|
+
def vdc = @bus.vdc
|
|
82
|
+
|
|
83
|
+
def mmu = @bus.mmu
|
|
84
|
+
|
|
85
|
+
def ram = @bus.ram
|
|
86
|
+
|
|
87
|
+
def keyboard = @bus.keyboard
|
|
88
|
+
|
|
89
|
+
def joystick1 = @bus.joystick1
|
|
90
|
+
|
|
91
|
+
def joystick2 = @bus.joystick2
|
|
92
|
+
|
|
93
|
+
def control_ports = @bus.control_ports
|
|
94
|
+
|
|
95
|
+
def datasette = @bus.datasette
|
|
96
|
+
|
|
97
|
+
# :c64 or :c128, the mode the MMU is in.
|
|
98
|
+
def mode = @bus.mmu.mode
|
|
99
|
+
|
|
100
|
+
def install_debug_register(&) = @bus.install_debug_register(&)
|
|
101
|
+
|
|
102
|
+
# +model+ names one of Model::ALL, and +sid_model+, when not nil, the
|
|
103
|
+
# SID in place of the model's. +mode+, :c64 or :c128, is the mode it
|
|
104
|
+
# powers on and resets into.
|
|
105
|
+
def initialize(model: "c128", sid_model: nil, mode: :c64, debug: false)
|
|
106
|
+
raise ArgumentError, "no C128 mode named #{mode}" unless %i[c64 c128].include?(mode)
|
|
107
|
+
|
|
108
|
+
@model = Model.named(model)
|
|
109
|
+
@c64_built = mode == :c64
|
|
110
|
+
@init_threshold = @c64_built ? Computer::INIT_THRESHOLD : C128_INIT_THRESHOLD
|
|
111
|
+
@holding_commodore = false
|
|
112
|
+
@bus = Bus.new(@model, sid_model: sid_model || @model.sid_model, mode:)
|
|
113
|
+
@cpu = CPU.new(@bus, debug:)
|
|
114
|
+
@vic = @bus.vic
|
|
115
|
+
@vic.open_bus = -> { @bus.ram.peek(@cpu.program_counter) }
|
|
116
|
+
@cia1 = @bus.cia1
|
|
117
|
+
@cia2 = @bus.cia2
|
|
118
|
+
@sid = @bus.sid
|
|
119
|
+
@vdc = @bus.vdc
|
|
120
|
+
@datasette = @bus.datasette
|
|
121
|
+
@cycles = 0
|
|
122
|
+
@clock_bits = 0
|
|
123
|
+
@nmi_asserted = false
|
|
124
|
+
@cartridge_nmi = false
|
|
125
|
+
@restore_pulse = false
|
|
126
|
+
@init_handlers = []
|
|
127
|
+
@pending_keys = nil
|
|
128
|
+
@drive = nil
|
|
129
|
+
@serial_trap = nil
|
|
130
|
+
@save_trap = nil
|
|
131
|
+
@drive1541 = nil
|
|
132
|
+
@capture_output = nil
|
|
133
|
+
plug_serial_bus
|
|
134
|
+
enter_c64_mode if @c64_built
|
|
135
|
+
@bus.on_mode_change { mode_changed }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The chips clock ahead of the CPU, as on the C64. $D030's FAST and
|
|
139
|
+
# TEST bits take hold a cycle after the write that sets them: in FAST
|
|
140
|
+
# mode the CPU runs in both halves of the cycle (#clock_fast), and the
|
|
141
|
+
# TEST bit steps the raster counter in every cycle.
|
|
142
|
+
def cycle!
|
|
143
|
+
handle_init if @cycles == @init_threshold
|
|
144
|
+
feed_keyboard if @pending_keys
|
|
145
|
+
|
|
146
|
+
clock_bits = @clock_bits
|
|
147
|
+
@clock_bits = @vic.clock_bits
|
|
148
|
+
@vic.test_step! if clock_bits >= 0x02
|
|
149
|
+
|
|
150
|
+
@vic.cycle!
|
|
151
|
+
@cia1.cycle!
|
|
152
|
+
@cia2.cycle!
|
|
153
|
+
@sid.cycle!
|
|
154
|
+
@datasette.cycle!
|
|
155
|
+
@vdc.cycle!
|
|
156
|
+
|
|
157
|
+
@cpu.irq = @cia1.interrupted? || @vic.interrupted?
|
|
158
|
+
|
|
159
|
+
drive_nmi
|
|
160
|
+
clock_bits.odd? ? clock_fast : clock_cpu
|
|
161
|
+
@drive1541&.host_cycle!
|
|
162
|
+
|
|
163
|
+
@cycles += 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Runs `count` cycles, one #cycle! after another.
|
|
167
|
+
def run_cycles(count)
|
|
168
|
+
i = 0
|
|
169
|
+
while i < count
|
|
170
|
+
cycle!
|
|
171
|
+
i += 1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Runs cycles until the block returns true or the cycle count passes
|
|
176
|
+
# `limit`, checking before each cycle.
|
|
177
|
+
def run_until(limit)
|
|
178
|
+
cycle! until yield || @cycles > limit
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def on_init(&block)
|
|
182
|
+
if @cycles < init_threshold
|
|
183
|
+
@init_handlers << block
|
|
184
|
+
else
|
|
185
|
+
block.call
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The clock, the raster and the crop of the VIC-IIe's region.
|
|
190
|
+
def timing
|
|
191
|
+
region = @bus.region
|
|
192
|
+
Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
|
|
193
|
+
lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The chip whose #display a front end shows: the VIC-IIe, or with
|
|
197
|
+
# :vdc the VDC, whose display paints only while its #render is on.
|
|
198
|
+
def video(chip = :vic) = chip == :vdc ? @vdc : @vic
|
|
199
|
+
|
|
200
|
+
# Renders the VDC's display, and not the VIC-IIe's, while a front end
|
|
201
|
+
# shows it, or with false the other way round.
|
|
202
|
+
def vdc_shown=(shown)
|
|
203
|
+
@vdc.render = shown
|
|
204
|
+
@vic.render = !shown
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The chip a front end records the machine's sound from.
|
|
208
|
+
def sound_source = @sid
|
|
209
|
+
|
|
210
|
+
def load_prg(data)
|
|
211
|
+
uint16(data[0], data[1]).tap do |load_addr|
|
|
212
|
+
ram.write(load_addr, data[2..])
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# A cartridge goes in with the power off.
|
|
217
|
+
def attach_cartridge(cartridge)
|
|
218
|
+
connect_cartridge(cartridge)
|
|
219
|
+
power_cycle!
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# RAM, the VIC and the VDC start from their power-on state, and the RES
|
|
223
|
+
# line resets everything else.
|
|
224
|
+
def power_cycle!
|
|
225
|
+
@vic.power_on!
|
|
226
|
+
@vdc.power_on!
|
|
227
|
+
@bus.power_on!
|
|
228
|
+
reset!
|
|
229
|
+
enter_c64_mode if @c64_built
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The RES line reaches the CPU and its port, the MMU, both CIAs, the
|
|
233
|
+
# SID, the cartridge port and, through the serial bus, the drive.
|
|
234
|
+
def reset!
|
|
235
|
+
@bus.reset!
|
|
236
|
+
@cia1.reset!
|
|
237
|
+
@cia2.reset!
|
|
238
|
+
@sid.reset!
|
|
239
|
+
@bus.cartridge&.reset
|
|
240
|
+
@drive&.reset!
|
|
241
|
+
@drive1541&.reset!
|
|
242
|
+
@nmi_asserted = false
|
|
243
|
+
@cpu.reset!
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# CHROUT is at $FFD2 in both KERNALs, and the trap follows the mode.
|
|
247
|
+
def capture_output
|
|
248
|
+
@capture_output ||= ChroutTrap.new(cpu:, bus: @bus, layout: trap_layout).tap do |trap|
|
|
249
|
+
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def inspect
|
|
254
|
+
"#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
# The C128 KERNAL leaves $D02F at $FF, every extra keyboard row
|
|
260
|
+
# deselected, before it jumps to the C64's reset.
|
|
261
|
+
def enter_c64_mode
|
|
262
|
+
@vic.poke(0xd02f, 0xff)
|
|
263
|
+
@bus.control_ports.extra_rows = 0xff
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# The NMI line is wired-OR between CIA 2, the cartridge and RESTORE,
|
|
267
|
+
# and the CPU takes an interrupt on its falling edge.
|
|
268
|
+
def drive_nmi
|
|
269
|
+
nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
|
|
270
|
+
@restore_pulse = false
|
|
271
|
+
@cpu.nmi = true if nmi && !@nmi_asserted
|
|
272
|
+
@nmi_asserted = nmi
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# BA halts the CPU on a read cycle.
|
|
276
|
+
def clock_cpu
|
|
277
|
+
@cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# The 8502 at 2 MHz takes both halves of the cycle, and BA no longer
|
|
281
|
+
# halts it. It leaves phi1 to the VIC in the refresh cycles, and an
|
|
282
|
+
# access to I/O, whose chips run at 1 MHz, waits for phi2. The VIC's
|
|
283
|
+
# fetches in the cycle see the bytes on the bus. In a phi1 the CPU
|
|
284
|
+
# spends waiting that is the byte it writes, or the last byte the bus
|
|
285
|
+
# held.
|
|
286
|
+
def clock_fast
|
|
287
|
+
phi1 = 0xff
|
|
288
|
+
unless @vic.refresh_cycle?
|
|
289
|
+
held = @bus.data
|
|
290
|
+
write = @cpu.pending_write?
|
|
291
|
+
@cpu.cycle!
|
|
292
|
+
if @bus.io_access?
|
|
293
|
+
data = @bus.data
|
|
294
|
+
return @vic.take_cpu_bus(write ? data : held, data, @bus.vic_access?)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
phi1 = @bus.data
|
|
298
|
+
end
|
|
299
|
+
@cpu.cycle!
|
|
300
|
+
@vic.take_cpu_bus(phi1, @bus.data, @bus.vic_access?)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def handle_init
|
|
304
|
+
@init_handlers.each(&:call)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# The keyboard buffer of the KERNAL the machine runs: the C64's, or in
|
|
308
|
+
# C128 mode BASIC 7.0's.
|
|
309
|
+
def feed_keyboard
|
|
310
|
+
c64 = mode == :c64
|
|
311
|
+
count = c64 ? KeyboardBuffer::COUNT : C128_KEYBOARD_COUNT
|
|
312
|
+
return unless ram.peek(count).zero?
|
|
313
|
+
|
|
314
|
+
chunk = @pending_keys.shift(KeyboardBuffer::CAPACITY)
|
|
315
|
+
ram.write(c64 ? KeyboardBuffer::ADDRESS : C128_KEYBOARD_BUFFER, chunk)
|
|
316
|
+
ram.poke(count, chunk.length)
|
|
317
|
+
@pending_keys = nil if @pending_keys.empty?
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
data/lib/badline/chrout_trap.rb
CHANGED
|
@@ -17,18 +17,13 @@ module Badline
|
|
|
17
17
|
|
|
18
18
|
# Puts the storage in device 8. Mounting again swaps the disk at any
|
|
19
19
|
# point while the machine runs: the drive keeps its RAM, which only a
|
|
20
|
-
# drive reset clears, and its status.
|
|
20
|
+
# drive reset clears, and its status. The traps go on the KERNAL the
|
|
21
|
+
# machine runs (#trap_layout).
|
|
21
22
|
def mount(storage)
|
|
22
23
|
return @drive.insert(storage) if @drive
|
|
23
24
|
|
|
24
25
|
@drive = KernalTrap::Drive.new(storage)
|
|
25
|
-
|
|
26
|
-
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
27
|
-
cpu.install_trap(layout.load) { load_trap.call }
|
|
28
|
-
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
|
|
29
|
-
device: serial_trap_device).install
|
|
30
|
-
save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
31
|
-
cpu.install_trap(layout.save) { save_trap.call }
|
|
26
|
+
install_kernal_traps(trap_layout)
|
|
32
27
|
end
|
|
33
28
|
|
|
34
29
|
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
@@ -37,11 +32,7 @@ module Badline
|
|
|
37
32
|
def unmount
|
|
38
33
|
return unless @drive
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
cpu.remove_trap(KernalTrap::C64_LAYOUT.save)
|
|
42
|
-
@serial_trap.device = nil
|
|
43
|
-
@serial_trap = nil
|
|
44
|
-
@save_trap = nil
|
|
35
|
+
remove_kernal_traps
|
|
45
36
|
@drive = nil
|
|
46
37
|
end
|
|
47
38
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Computer
|
|
5
|
+
# The LOAD, SAVE and serial traps on the KERNAL a KernalTrap::Layout
|
|
6
|
+
# describes, which serve the disk in device 8 (Attachments#mount).
|
|
7
|
+
module KernalTraps
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def install_kernal_traps(layout)
|
|
11
|
+
@trap_layout = layout
|
|
12
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
13
|
+
cpu.install_trap(layout.load) { load_trap.call }
|
|
14
|
+
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
|
|
15
|
+
device: serial_trap_device).install
|
|
16
|
+
save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
|
|
17
|
+
cpu.install_trap(layout.save) { save_trap.call }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove_kernal_traps
|
|
21
|
+
cpu.remove_trap(@trap_layout.load)
|
|
22
|
+
cpu.remove_trap(@trap_layout.save)
|
|
23
|
+
@serial_trap.uninstall
|
|
24
|
+
@serial_trap = nil
|
|
25
|
+
@save_trap = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/badline/computer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/computer/attachments"
|
|
4
|
+
require "badline/computer/kernal_traps"
|
|
4
5
|
require "badline/computer/saved_state"
|
|
5
6
|
|
|
6
7
|
module Badline
|
|
@@ -8,6 +9,7 @@ module Badline
|
|
|
8
9
|
include IntegerHelper
|
|
9
10
|
include KeyboardBuffer
|
|
10
11
|
include Attachments
|
|
12
|
+
include KernalTraps
|
|
11
13
|
include SavedState
|
|
12
14
|
|
|
13
15
|
attr_reader :address_bus, :cpu, :cycles, :drive1541
|
|
@@ -48,13 +50,12 @@ module Badline
|
|
|
48
50
|
# TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
|
|
49
51
|
# every region boots the same ROMs. reu plugs an REU of that many K
|
|
50
52
|
# into the expansion port, where it drives the IRQ line and takes the
|
|
51
|
-
# bus for its transfers. kernal:
|
|
52
|
-
# (AddressBus::
|
|
53
|
-
# empty
|
|
54
|
-
def initialize(debug: false, reu: nil,
|
|
55
|
-
@address_bus = AddressBus.new(**machine)
|
|
56
|
-
@address_bus.
|
|
57
|
-
@address_bus.datasette.disconnect! unless datasette
|
|
53
|
+
# bus for its transfers. kernal:, datasette: and board: fit the bus
|
|
54
|
+
# afterwards (AddressBus::Fittings): the KERNAL ROM, the cassette port,
|
|
55
|
+
# which is empty on the SX-64, and the board and case.
|
|
56
|
+
def initialize(debug: false, reu: nil, **machine)
|
|
57
|
+
@address_bus = AddressBus.new(**machine.except(*AddressBus::Fittings::NAMES))
|
|
58
|
+
@address_bus.fit(**machine.slice(*AddressBus::Fittings::NAMES))
|
|
58
59
|
@cpu = CPU.new(@address_bus, debug:)
|
|
59
60
|
@vic = @address_bus.vic
|
|
60
61
|
@vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
|
|
@@ -195,8 +196,9 @@ module Badline
|
|
|
195
196
|
|
|
196
197
|
# RESTORE isn't in the key matrix. It fires a one-shot that pulses the
|
|
197
198
|
# NMI line however long the key is held, modelled here as one cycle.
|
|
199
|
+
# A machine without a keyboard has no RESTORE key either.
|
|
198
200
|
def press_restore
|
|
199
|
-
@restore_pulse = true
|
|
201
|
+
@restore_pulse = true if keyboard.connected?
|
|
200
202
|
end
|
|
201
203
|
|
|
202
204
|
# The one-shot has pulsed already, so letting go does nothing.
|
|
@@ -222,6 +224,9 @@ module Badline
|
|
|
222
224
|
|
|
223
225
|
private
|
|
224
226
|
|
|
227
|
+
# The KERNAL the traps stand in for (Attachments#mount).
|
|
228
|
+
def trap_layout = KernalTrap::C64_LAYOUT
|
|
229
|
+
|
|
225
230
|
# The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
|
|
226
231
|
# key, and the CPU takes an interrupt on its falling edge.
|
|
227
232
|
def drive_nmi
|
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -5,6 +5,10 @@ module Badline
|
|
|
5
5
|
# Opens the window, then runs the machine a frame at a time: poll
|
|
6
6
|
# events, clock the frame's cycles, queue the sound's samples, upload the
|
|
7
7
|
# changed lines, present and wait. Pacer decides the cycles and the wait.
|
|
8
|
+
#
|
|
9
|
+
# The window shows the machine's video chip, sized to the screen shown.
|
|
10
|
+
# On the C128 it follows the machine between the VIC-IIe's 40 columns
|
|
11
|
+
# and the VDC's 80, and F8 switches them until the machine next does.
|
|
8
12
|
class App
|
|
9
13
|
DROPFILE = 0x1000
|
|
10
14
|
|
|
@@ -18,10 +22,10 @@ module Badline
|
|
|
18
22
|
@pacer = Pacer.new(paced: options.paced?, vsync: options.vsync?, verbose: @verbose, timing:)
|
|
19
23
|
@timeline = timeline
|
|
20
24
|
@snapshots = Snapshots.new(computer, options)
|
|
21
|
-
@
|
|
25
|
+
@screens = Screens.new(computer)
|
|
22
26
|
@led = DriveLed.for(computer)
|
|
23
27
|
@controls = Controls.new(computer)
|
|
24
|
-
@window = MachineWindow.new(@screen,
|
|
28
|
+
@window = MachineWindow.new(@screens.screen, vsync: @pacer.vsync?)
|
|
25
29
|
@pacer.fit(@window.refresh_rate) if @pacer.vsync?
|
|
26
30
|
@sound = Sound.new(computer.sound_source, timing.clock_hz, options.sound?, @verbose)
|
|
27
31
|
@gamepads = Gamepads.new(computer, @verbose)
|
|
@@ -68,6 +72,7 @@ module Badline
|
|
|
68
72
|
@frame_report.add(stamps) if @verbose
|
|
69
73
|
@frames += 1
|
|
70
74
|
@timeline.run(@computer, @frames)
|
|
75
|
+
show_vdc(@timeline.display_at(@frames) == "vdc") if @timeline.display?(@frames)
|
|
71
76
|
@snapshots.tick(@frames)
|
|
72
77
|
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
73
78
|
@pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
|
|
@@ -109,7 +114,7 @@ module Badline
|
|
|
109
114
|
end
|
|
110
115
|
|
|
111
116
|
def handle_key(scancode, down)
|
|
112
|
-
if [Keys::TAB, Keys::F9, Keys::F10].include?(scancode)
|
|
117
|
+
if [Keys::TAB, Keys::F8, Keys::F9, Keys::F10].include?(scancode)
|
|
113
118
|
handle_toggle(scancode) if down
|
|
114
119
|
elsif down && @snapshots.key(scancode)
|
|
115
120
|
swap(@snapshots.computer)
|
|
@@ -123,24 +128,21 @@ module Badline
|
|
|
123
128
|
# it.
|
|
124
129
|
def swap(computer)
|
|
125
130
|
@computer = computer
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@window.fit(@screen, timing.pixel_width)
|
|
129
|
-
@menu.center(@window.width, @window.height)
|
|
131
|
+
@screens.computer = computer
|
|
132
|
+
fit_window
|
|
130
133
|
@led = DriveLed.for(computer)
|
|
131
134
|
@controls.computer = computer
|
|
132
135
|
@gamepads.computer = computer
|
|
133
|
-
@sound.switch(computer.sound_source, timing.clock_hz)
|
|
136
|
+
@sound.switch(computer.sound_source, computer.timing.clock_hz)
|
|
134
137
|
@snapshots.computer = computer
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
def handle_toggle(scancode)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
else
|
|
143
|
-
@sound.toggle_mute
|
|
141
|
+
case scancode
|
|
142
|
+
when Keys::TAB then @controls.toggle_keys
|
|
143
|
+
when Keys::F8 then show_vdc(!@screens.vdc_shown?)
|
|
144
|
+
when Keys::F9 then return open_menu
|
|
145
|
+
else @sound.toggle_mute
|
|
144
146
|
end
|
|
145
147
|
update_title
|
|
146
148
|
end
|
|
@@ -189,11 +191,19 @@ module Badline
|
|
|
189
191
|
menu_events
|
|
190
192
|
end
|
|
191
193
|
|
|
192
|
-
def update_title
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
@
|
|
194
|
+
def update_title = @window.title(@controls.tag, @sound.muted?)
|
|
195
|
+
|
|
196
|
+
# Shows the C128's VDC in the window, or with false its VIC-IIe.
|
|
197
|
+
def show_vdc(shown)
|
|
198
|
+
@screens.show_vdc(shown)
|
|
199
|
+
fit_window
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Builds the screen for the chip shown and fits the window to it.
|
|
203
|
+
def fit_window
|
|
204
|
+
@screens.build
|
|
205
|
+
@window.fit(@screens.screen)
|
|
206
|
+
@menu.center(@window.width, @window.height)
|
|
197
207
|
end
|
|
198
208
|
|
|
199
209
|
def emulate
|
|
@@ -201,8 +211,11 @@ module Badline
|
|
|
201
211
|
end
|
|
202
212
|
|
|
203
213
|
def upload
|
|
204
|
-
@
|
|
205
|
-
@
|
|
214
|
+
@screens.follow
|
|
215
|
+
fit_window if @screens.stale?
|
|
216
|
+
screen = @screens.screen
|
|
217
|
+
screen.update
|
|
218
|
+
@window.upload(screen)
|
|
206
219
|
end
|
|
207
220
|
|
|
208
221
|
def draw
|
|
@@ -49,7 +49,7 @@ module Badline
|
|
|
49
49
|
# or --sid the SID. The VIC-20 raises Unsupported for a .sid
|
|
50
50
|
# tune.
|
|
51
51
|
def self.start(options, media, writable)
|
|
52
|
-
computer =
|
|
52
|
+
computer = build(options, media)
|
|
53
53
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
54
54
|
unless media.nil?
|
|
55
55
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
@@ -58,11 +58,41 @@ module Badline
|
|
|
58
58
|
computer
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def self.build(options, media)
|
|
62
|
+
case options.family
|
|
63
|
+
when :vic20 then vic20(options, media)
|
|
64
|
+
when :c128 then c128(options, media)
|
|
65
|
+
else c64(options, media)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
61
69
|
def self.c64(options, media)
|
|
62
70
|
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
63
71
|
Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
|
|
64
72
|
end
|
|
65
73
|
|
|
74
|
+
# A C128 in C128 mode. With --c64, or media only C64 mode runs, a .sid
|
|
75
|
+
# tune or a program that loads at the C64's BASIC start, C= is held
|
|
76
|
+
# through the reset, so the C128 KERNAL starts C64 mode.
|
|
77
|
+
def self.c128(options, media)
|
|
78
|
+
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
79
|
+
machine = C128.new(model: options.model, sid_model:, mode: :c128)
|
|
80
|
+
machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
|
|
81
|
+
machine
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.c64_media?(media)
|
|
85
|
+
return false if media.nil? || !File.file?(media)
|
|
86
|
+
|
|
87
|
+
extension = File.extname(media).downcase
|
|
88
|
+
return true if extension == ".sid"
|
|
89
|
+
return false unless %w[.prg .p00].include?(extension)
|
|
90
|
+
|
|
91
|
+
bytes = File.binread(media).bytes
|
|
92
|
+
bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
|
|
93
|
+
bytes.length >= 2 && [Media::BASIC_START, Media::BASIC_START - 1].include?(bytes[0] | (bytes[1] << 8))
|
|
94
|
+
end
|
|
95
|
+
|
|
66
96
|
# A VIC-20 with the RAM --ram names, or else the RAM `media` needs
|
|
67
97
|
# (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
|
|
68
98
|
# media a VIC-20 doesn't take.
|
|
@@ -73,6 +73,8 @@ module Badline
|
|
|
73
73
|
def key(scancode, down)
|
|
74
74
|
if joystick_mode? && Keys.joystick?(scancode)
|
|
75
75
|
joystick_key(scancode, down)
|
|
76
|
+
elsif @computer.family == :c128
|
|
77
|
+
c128_key(Keys.c128_key(scancode, @computer.mode), down)
|
|
76
78
|
else
|
|
77
79
|
c64_key(Keys.c64_key(scancode), down)
|
|
78
80
|
end
|
|
@@ -111,6 +113,15 @@ module Badline
|
|
|
111
113
|
end
|
|
112
114
|
end
|
|
113
115
|
|
|
116
|
+
# CAPS LOCK and 40/80 DISPLAY aren't in the C128's matrix either.
|
|
117
|
+
def c128_key(key, down)
|
|
118
|
+
case key
|
|
119
|
+
when :caps_lock then down ? @computer.press_caps_lock : @computer.release_caps_lock
|
|
120
|
+
when :forty_eighty then down ? @computer.press_display_key : @computer.release_display_key
|
|
121
|
+
else c64_key(key, down)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
114
125
|
def paddles? = POTS.index(@pot) >= 3
|
|
115
126
|
|
|
116
127
|
# A fresh pot device on its port, and none on the other. A machine
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# SDL scancodes follow key positions on a US layout. They map onto the C64
|
|
6
|
-
# keys and, in joystick mode, the joystick
|
|
6
|
+
# keys, the C128's own keys and, in joystick mode, the joystick
|
|
7
|
+
# directions.
|
|
7
8
|
module Keys
|
|
8
9
|
TAB = 43
|
|
9
10
|
|
|
@@ -20,6 +21,24 @@ module Badline
|
|
|
20
21
|
224 => :control, 225 => :lshift, 226 => :cbm, 229 => :rshift
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
24
|
+
# The C128's keys that have a host key of their own: the keypad, HELP
|
|
25
|
+
# on Insert or Help, ALT on the right Alt, NO SCROLL on Scroll Lock,
|
|
26
|
+
# and CAPS LOCK and 40/80 DISPLAY, which sit outside the matrix, on
|
|
27
|
+
# Caps Lock and F6 or Pause. In C128 mode Esc is ESC, and RUN/STOP
|
|
28
|
+
# moves to Page Down. TAB and the separate cursor keys stay with the
|
|
29
|
+
# C64's keys (the joystick toggle and the cursor combinations), and in
|
|
30
|
+
# C64 mode Esc stays RUN/STOP.
|
|
31
|
+
C128_OTHERS = {
|
|
32
|
+
89 => :keypad1, 90 => :keypad2, 91 => :keypad3, 92 => :keypad4, 93 => :keypad5, 94 => :keypad6,
|
|
33
|
+
95 => :keypad7, 96 => :keypad8, 97 => :keypad9, 98 => :keypad0, 99 => :keypad_period,
|
|
34
|
+
86 => :keypad_minus, 87 => :keypad_plus, 88 => :keypad_enter,
|
|
35
|
+
73 => :help, 117 => :help, 230 => :alt, 71 => :no_scroll, 57 => :caps_lock,
|
|
36
|
+
63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
ESCAPE = 41
|
|
40
|
+
|
|
41
|
+
F8 = 65
|
|
23
42
|
F9 = 66
|
|
24
43
|
F10 = 67
|
|
25
44
|
F11 = 68
|
|
@@ -38,6 +57,14 @@ module Badline
|
|
|
38
57
|
end
|
|
39
58
|
end
|
|
40
59
|
|
|
60
|
+
# The C128's key for the scancode in the C128's +mode+, :c128 or
|
|
61
|
+
# :c64: one of C128_OTHERS, ESC, or the C64's.
|
|
62
|
+
def self.c128_key(scancode, mode)
|
|
63
|
+
return :esc if scancode == ESCAPE && mode == :c128
|
|
64
|
+
|
|
65
|
+
C128_OTHERS[scancode] || c64_key(scancode)
|
|
66
|
+
end
|
|
67
|
+
|
|
41
68
|
def self.arrows(scancode) = ARROWS[scancode] || :none
|
|
42
69
|
|
|
43
70
|
def self.wasd(scancode) = WASD[scancode] || :none
|