badline 0.10.1 → 0.11.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 +23 -0
- data/CLAUDE.md +25 -6
- data/CONTRIBUTING.md +8 -3
- data/README.md +63 -4
- data/doc/pinned-behaviour.md +75 -0
- data/doc/snapshots.md +15 -0
- data/lib/badline/address_bus/pla.rb +3 -16
- data/lib/badline/address_bus.rb +14 -0
- 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/drives.rb +95 -0
- data/lib/badline/c128/fast_serial.rb +62 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +197 -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 +121 -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 +322 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/cia/fast_forward.rb +42 -0
- data/lib/badline/cia/serial.rb +12 -0
- data/lib/badline/cia/timer.rb +39 -0
- data/lib/badline/cia.rb +2 -0
- data/lib/badline/computer/attachments.rb +10 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +5 -0
- data/lib/badline/drive/core.rb +172 -0
- data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
- data/lib/badline/drive/idle_cpu.rb +28 -0
- data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
- data/lib/badline/drive/serial_lines.rb +73 -0
- data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
- data/lib/badline/drive/watch.rb +150 -0
- data/lib/badline/drive.rb +9 -0
- data/lib/badline/drive1541/bus.rb +5 -129
- data/lib/badline/drive1541/byte_ready.rb +48 -0
- data/lib/badline/drive1541/cpu.rb +1 -19
- data/lib/badline/drive1541/disk.rb +77 -20
- data/lib/badline/drive1541/disk_state.rb +7 -2
- data/lib/badline/drive1541/disk_via.rb +3 -0
- data/lib/badline/drive1541/mechanism.rb +48 -14
- data/lib/badline/drive1541/mechanism_state.rb +5 -4
- data/lib/badline/drive1541/rotation.rb +5 -5
- data/lib/badline/drive1541/serial_port.rb +4 -60
- data/lib/badline/drive1541.rb +13 -172
- data/lib/badline/drive1571/bus.rb +111 -0
- data/lib/badline/drive1571/cpu.rb +11 -0
- data/lib/badline/drive1571/serial_port.rb +37 -0
- data/lib/badline/drive1571/serial_via.rb +25 -0
- data/lib/badline/drive1571/wd1770.rb +34 -0
- data/lib/badline/drive1571.rb +225 -0
- data/lib/badline/frontend/app.rb +35 -22
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/drive_led.rb +1 -1
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_media.rb +3 -3
- data/lib/badline/frontend/menu_pages.rb +8 -2
- 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/iec_bus.rb +54 -4
- 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/machine.rb +20 -9
- data/lib/badline/media/true_drive.rb +26 -23
- data/lib/badline/media.rb +8 -6
- data/lib/badline/options/event.rb +5 -1
- data/lib/badline/options/family.rb +52 -0
- data/lib/badline/options/help.rb +7 -2
- 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 +36 -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/dos1571.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +14 -0
- data/lib/badline/snapshot.rb +9 -2
- data/lib/badline/storage/d64_image.rb +3 -0
- data/lib/badline/storage/d71_image.rb +9 -4
- data/lib/badline/storage/g64_image.rb +7 -2
- data/lib/badline/time_of_day.rb +9 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/fast_forward.rb +3 -3
- data/lib/badline/vic/display_state.rb +12 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic.rb +108 -7
- data/lib/badline/vic20/attachments.rb +6 -0
- data/lib/badline/z80/arithmetic.rb +117 -0
- data/lib/badline/z80/bit_page.rb +55 -0
- data/lib/badline/z80/block_io.rb +58 -0
- data/lib/badline/z80/block_transfers.rb +59 -0
- data/lib/badline/z80/bus_cycles.rb +111 -0
- data/lib/badline/z80/core.rb +111 -0
- data/lib/badline/z80/exchanges.rb +50 -0
- data/lib/badline/z80/extended_page.rb +89 -0
- data/lib/badline/z80/flags.rb +37 -0
- data/lib/badline/z80/interrupt_acceptance.rb +46 -0
- data/lib/badline/z80/jumps.rb +82 -0
- data/lib/badline/z80/loads.rb +98 -0
- data/lib/badline/z80/main_page.rb +134 -0
- data/lib/badline/z80/registers.rb +120 -0
- data/lib/badline/z80/shifts.rb +64 -0
- data/lib/badline/z80.rb +11 -0
- data/lib/badline.rb +1 -0
- metadata +59 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1571
|
|
5
|
+
# What VIA 1's ports read from outside the chip. Port B carries the
|
|
6
|
+
# serial bus inputs and the device number jumpers, as on the 1541
|
|
7
|
+
# (Drive::SerialLines). Port A reads the mechanism on two inputs:
|
|
8
|
+
#
|
|
9
|
+
# PA0 track 0 sensor, low while the head sits on track 1
|
|
10
|
+
# PA7 BYTE READY, latched low from a whole byte until the CPU next
|
|
11
|
+
# reads or writes VIA 2
|
|
12
|
+
#
|
|
13
|
+
# The DOS steps the head out until PA0 reads low with the stepper on
|
|
14
|
+
# phase 0, and polls PA7 for each byte at 2 MHz, taking the byte from
|
|
15
|
+
# VIA 2's port A or putting the next one there after each. PA1 (fast serial
|
|
16
|
+
# direction), PA2 (side) and PA5 (1 or 2 MHz) are outputs, which
|
|
17
|
+
# Drive1571 follows (SerialVIA).
|
|
18
|
+
class SerialPort
|
|
19
|
+
include Drive::SerialLines
|
|
20
|
+
|
|
21
|
+
TRACK0 = 0x01
|
|
22
|
+
BYTE_READY = 0x80
|
|
23
|
+
|
|
24
|
+
def initialize(mechanism, device: 8, bus: IECBus.new)
|
|
25
|
+
@mechanism = mechanism
|
|
26
|
+
init_serial_lines(device, bus)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read_a(_lines)
|
|
30
|
+
value = 0xff
|
|
31
|
+
value &= ~TRACK0 if @mechanism.half_track == Drive1541::Mechanism::MIN_HALF_TRACK
|
|
32
|
+
value &= ~BYTE_READY if @mechanism.byte_latched?
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1571
|
|
5
|
+
# VIA 1, the one facing the serial bus. Its port A also selects the
|
|
6
|
+
# side, the clock rate and the fast serial direction, so the drive
|
|
7
|
+
# hears port A after every write and reset (Drive1571#port_a_written).
|
|
8
|
+
class SerialVIA < VIA
|
|
9
|
+
def initialize(start:, peripheral:, drive:)
|
|
10
|
+
@drive = drive
|
|
11
|
+
super(start:, peripheral:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset!
|
|
15
|
+
super
|
|
16
|
+
@drive.port_a_written(port_a_output)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def poke(addr, value)
|
|
20
|
+
super
|
|
21
|
+
@drive.port_a_written(port_a_output)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1571
|
|
5
|
+
# The WD1770 floppy controller at $2000, which the DOS needs only for
|
|
6
|
+
# MFM disks. Its four registers hold what's written to them and it
|
|
7
|
+
# runs no commands: the status reads idle, with the motor off.
|
|
8
|
+
#
|
|
9
|
+
# $2000 status (read), command (write)
|
|
10
|
+
# $2001 track
|
|
11
|
+
# $2002 sector
|
|
12
|
+
# $2003 data
|
|
13
|
+
class WD1770
|
|
14
|
+
def initialize
|
|
15
|
+
@registers = [0, 0, 0, 0]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Register +offset+, 0 to 3.
|
|
19
|
+
def peek(offset) = offset.zero? ? 0 : @registers[offset]
|
|
20
|
+
|
|
21
|
+
def poke(offset, value)
|
|
22
|
+
@registers[offset] = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save_state(out)
|
|
26
|
+
out.ints(@registers)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def load_state(input)
|
|
30
|
+
@registers = input.ints
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive1571/cpu"
|
|
4
|
+
require "badline/drive1571/wd1770"
|
|
5
|
+
require "badline/drive1571/serial_port"
|
|
6
|
+
require "badline/drive1571/serial_via"
|
|
7
|
+
require "badline/drive1571/bus"
|
|
8
|
+
|
|
9
|
+
module Badline
|
|
10
|
+
# The 1571 disk drive, the C128D's built-in one: the 1541's 6502, two
|
|
11
|
+
# VIAs and GCR mechanism, with a second head, a 6526 CIA for fast
|
|
12
|
+
# serial, a WD1770 for MFM disks and DOS 3.0 in 32 KB of ROM. Drive::Core
|
|
13
|
+
# clocks it against the host and sleeps through the DOS's idle loop.
|
|
14
|
+
#
|
|
15
|
+
# VIA 1's port A picks the clock, 1 or 2 MHz on PA5, and the head on
|
|
16
|
+
# PA2, and reads the track 0 sensor and BYTE READY (SerialPort). The
|
|
17
|
+
# DOS resets at 2 MHz and drops to 1 MHz as a 1541 would run, which is
|
|
18
|
+
# how it stays until a C128 asks for fast serial. The disk turns at the
|
|
19
|
+
# same speed either way (Drive1541::Mechanism#clock_hz=).
|
|
20
|
+
#
|
|
21
|
+
# The CIA sits at $4000, on the IRQ line with both VIAs. Only its serial
|
|
22
|
+
# port is wired: through buffers PA1 turns round, its CNT drives SRQ and
|
|
23
|
+
# its SP DATA, or it hears them. The drive pushes its pins into the bus
|
|
24
|
+
# as they move, and hears the host's as the host pushes them, pin by
|
|
25
|
+
# pin, so each byte goes through both shift registers bit by bit. The
|
|
26
|
+
# CIA takes no part in a pass of the idle loop: the drive sleeps only
|
|
27
|
+
# while the CIA is quiet (CIA#quiet?), and any access to it, or the
|
|
28
|
+
# host moving its fast serial pins, wakes the drive.
|
|
29
|
+
class Drive1571
|
|
30
|
+
include Drive::Core
|
|
31
|
+
|
|
32
|
+
CLOCK_HZ = 1_000_000
|
|
33
|
+
FAST_CLOCK_HZ = 2_000_000
|
|
34
|
+
|
|
35
|
+
# Where the DOS 3.0 idle loop starts over, as DOS 2.6's does.
|
|
36
|
+
IDLE_LOOP = 0xebff
|
|
37
|
+
|
|
38
|
+
# VIA 1's port A outputs.
|
|
39
|
+
FAST_SERIAL_OUT = 0x02
|
|
40
|
+
SIDE = 0x04
|
|
41
|
+
FAST = 0x20
|
|
42
|
+
|
|
43
|
+
# Whether PA1 turns the fast serial buffers outwards, the drive's CIA
|
|
44
|
+
# driving DATA and SRQ.
|
|
45
|
+
attr_reader :fast_serial_out
|
|
46
|
+
|
|
47
|
+
def cia
|
|
48
|
+
settle!
|
|
49
|
+
@cia
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fdc
|
|
53
|
+
settle!
|
|
54
|
+
@fdc
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def model_name = "1571"
|
|
58
|
+
|
|
59
|
+
# +rom+ covers $8000-$FFFF, and defaults to the DOS image in the ROM
|
|
60
|
+
# path. +device+ is the number the jumpers on VIA 1's PB5 and PB6 set,
|
|
61
|
+
# 8 to 11.
|
|
62
|
+
def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
|
|
63
|
+
@device = device
|
|
64
|
+
@clock_hz = CLOCK_HZ
|
|
65
|
+
@fast_serial_out = false
|
|
66
|
+
@fast_output = 0
|
|
67
|
+
@mechanism = Drive1541::Mechanism.new(slip: 2)
|
|
68
|
+
@serial_port = SerialPort.new(@mechanism, device:)
|
|
69
|
+
@via1 = SerialVIA.new(start: 0x1800, peripheral: @serial_port, drive: self)
|
|
70
|
+
@via2 = Drive1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
|
|
71
|
+
@cia = CIA.new(start: 0x4000)
|
|
72
|
+
@fdc = WD1770.new
|
|
73
|
+
@bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
|
|
74
|
+
@cpu = CPU.new(@bus, debug:)
|
|
75
|
+
@idle_loop = IDLE_LOOP
|
|
76
|
+
@host_clock_hz = host_clock_hz
|
|
77
|
+
@phase = 0
|
|
78
|
+
@cycles = 0
|
|
79
|
+
@serial_bus = nil
|
|
80
|
+
init_idle(debug)
|
|
81
|
+
# CA1 powers up at the level of a released ATN, without an edge.
|
|
82
|
+
@via1.ca1 = false
|
|
83
|
+
@via1.reset!
|
|
84
|
+
connect(IECBus.new)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The serial bus's RESET line reaches the CPU, both VIAs and the CIA.
|
|
88
|
+
# RAM keeps its contents.
|
|
89
|
+
def reset!
|
|
90
|
+
settle!
|
|
91
|
+
@via1.reset!
|
|
92
|
+
@via2.reset!
|
|
93
|
+
@cia.reset!
|
|
94
|
+
@cpu.reset!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# VIA 1 drives port A to +lines+: the side, the clock rate and the fast
|
|
98
|
+
# serial direction follow it.
|
|
99
|
+
def port_a_written(lines)
|
|
100
|
+
@mechanism.side = lines.anybits?(SIDE) ? 1 : 0
|
|
101
|
+
fast_serial_out = lines.anybits?(FAST_SERIAL_OUT)
|
|
102
|
+
turn_fast_serial(fast_serial_out) if fast_serial_out != @fast_serial_out
|
|
103
|
+
clock_hz = lines.anybits?(FAST) ? FAST_CLOCK_HZ : CLOCK_HZ
|
|
104
|
+
return if clock_hz == @clock_hz
|
|
105
|
+
|
|
106
|
+
@clock_hz = clock_hz
|
|
107
|
+
@mechanism.clock_hz = clock_hz
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Whether the drive runs at 2 MHz.
|
|
111
|
+
def fast? = @clock_hz == FAST_CLOCK_HZ
|
|
112
|
+
|
|
113
|
+
# VIA 1's port B, with the fast serial pins pulling DATA and SRQ
|
|
114
|
+
# (IECBus::DRIVE_FAST_DATA and DRIVE_FAST_SRQ) above it.
|
|
115
|
+
def serial_output = @via1.port_b_output | @fast_output
|
|
116
|
+
|
|
117
|
+
# The host moved its fast serial pins: the CIA hears SRQ on CNT and
|
|
118
|
+
# DATA on SP while PA1 turns the buffers inwards.
|
|
119
|
+
def fast_lines_moved
|
|
120
|
+
settle!
|
|
121
|
+
hear_fast_serial unless @fast_serial_out
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The drive's whole state, as the 1541's (Drive1541#save_state), with
|
|
125
|
+
# the CIA and the WD1770's registers. The clock rate, the side and the
|
|
126
|
+
# fast serial direction follow from VIA 1's port A.
|
|
127
|
+
def save_state(out)
|
|
128
|
+
settle!
|
|
129
|
+
out.marker("DRIVE1571")
|
|
130
|
+
out.int(@phase).int(@cycles).boolean(@mechanism.so_pending)
|
|
131
|
+
@serial_port.save_state(out)
|
|
132
|
+
@via1.save_state(out)
|
|
133
|
+
@via2.save_state(out)
|
|
134
|
+
@cia.save_state(out)
|
|
135
|
+
@fdc.save_state(out)
|
|
136
|
+
@bus.save_state(out)
|
|
137
|
+
@cpu.save_state(out)
|
|
138
|
+
@mechanism.save_state(out)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def load_state(input)
|
|
142
|
+
settle!
|
|
143
|
+
input.marker("DRIVE1571")
|
|
144
|
+
@phase = input.int
|
|
145
|
+
@cycles = input.int
|
|
146
|
+
@mechanism.so_pending = input.boolean?
|
|
147
|
+
@serial_port.load_state(input)
|
|
148
|
+
@via1.load_state(input)
|
|
149
|
+
@via2.load_state(input)
|
|
150
|
+
@cia.load_state(input)
|
|
151
|
+
@fdc.load_state(input)
|
|
152
|
+
@bus.load_state(input)
|
|
153
|
+
@cpu.load_state(input)
|
|
154
|
+
@mechanism.load_state(input)
|
|
155
|
+
@fast_serial_out = @via1.port_a_output.anybits?(FAST_SERIAL_OUT)
|
|
156
|
+
port_a_written(@via1.port_a_output)
|
|
157
|
+
push_fast_output(@fast_serial_out ? fast_pins : 0)
|
|
158
|
+
forget_orbits
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# One drive cycle, as the 1541's (Drive1541#step), with the CIA
|
|
164
|
+
# clocked beside the VIAs and pulling IRQ with them.
|
|
165
|
+
def step
|
|
166
|
+
so = @mechanism.take_so
|
|
167
|
+
@mechanism.cycle!
|
|
168
|
+
@via1.ca1 = @serial_port.atn_low?
|
|
169
|
+
@via1.cycle!
|
|
170
|
+
@via2.cycle!
|
|
171
|
+
@cia.cycle!
|
|
172
|
+
drive_fast_serial if @fast_serial_out
|
|
173
|
+
@cpu.irq = @via1.irq? || @via2.irq? || @cia.interrupted?
|
|
174
|
+
@cpu.so! if so
|
|
175
|
+
@cpu.cycle!
|
|
176
|
+
@cycles += 1
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The VIAs' quiet cycles while the CIA is quiet, and none otherwise.
|
|
180
|
+
def quiet_cycles
|
|
181
|
+
@cia.quiet? ? [@via1.quiet_cycles, @via2.quiet_cycles].min : 0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def fast_forward_chips(cycles)
|
|
185
|
+
@cia.fast_forward(cycles)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# PA1 turned the buffers round: outwards the CIA's pins drive the
|
|
189
|
+
# lines, and inwards they let go and hear them.
|
|
190
|
+
def turn_fast_serial(out)
|
|
191
|
+
@fast_serial_out = out
|
|
192
|
+
return unless @serial_bus
|
|
193
|
+
|
|
194
|
+
push_fast_output(out ? fast_pins : 0)
|
|
195
|
+
hear_fast_serial unless out
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def drive_fast_serial
|
|
199
|
+
low = fast_pins
|
|
200
|
+
push_fast_output(low) if low != @fast_output
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The lines the CIA's CNT and SP pull while its serial port drives
|
|
204
|
+
# them.
|
|
205
|
+
def fast_pins
|
|
206
|
+
serial = @cia.serial
|
|
207
|
+
return 0 unless serial.output?
|
|
208
|
+
|
|
209
|
+
low = serial.cnt ? 0 : IECBus::DRIVE_FAST_SRQ
|
|
210
|
+
serial.sp_out ? low : low | IECBus::DRIVE_FAST_DATA
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def push_fast_output(low)
|
|
214
|
+
@fast_output = low
|
|
215
|
+
@serial_bus&.drives_fast_moved!
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def hear_fast_serial
|
|
219
|
+
low = @serial_bus.low_lines
|
|
220
|
+
serial = @cia.serial
|
|
221
|
+
serial.cnt_in = low.nobits?(IECBus::SRQ)
|
|
222
|
+
serial.sp_in = low.nobits?(IECBus::DATA)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
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)
|
|
@@ -38,7 +42,7 @@ module Badline
|
|
|
38
42
|
@pacer.start(@started)
|
|
39
43
|
@menu.open? ? paused_frame : frame while @running
|
|
40
44
|
@snapshots.finish
|
|
41
|
-
@computer.
|
|
45
|
+
@computer.true_drive&.flush
|
|
42
46
|
@gamepads.close
|
|
43
47
|
@sound.close
|
|
44
48
|
@window.close
|
|
@@ -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
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# The emulator's SDL window, its renderer and the streaming texture a
|
|
6
|
-
# Screen's pixels go into. The renderer's logical size is the screen's
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Screen's pixels go into. The renderer's logical size is the screen's
|
|
7
|
+
# in square pixels (Screen#window_width), so the picture keeps the
|
|
8
|
+
# machine's aspect while the menu and the drive LED draw in square
|
|
9
|
+
# pixels over it. The window opens at SCALE times that.
|
|
10
10
|
class MachineWindow
|
|
11
11
|
SCALE = 2
|
|
12
12
|
TITLE = "Badline"
|
|
@@ -18,12 +18,12 @@ module Badline
|
|
|
18
18
|
|
|
19
19
|
# Opens the window for `screen`, presenting in step with the display
|
|
20
20
|
# when `vsync`.
|
|
21
|
-
def initialize(screen,
|
|
21
|
+
def initialize(screen, vsync:)
|
|
22
22
|
abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
|
|
23
23
|
|
|
24
24
|
SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
|
|
25
25
|
SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
|
|
26
|
-
measure(screen
|
|
26
|
+
measure(screen)
|
|
27
27
|
@window = SDL.SDL_CreateWindow(
|
|
28
28
|
TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
|
|
29
29
|
SDL::WINDOW_RESIZABLE
|
|
@@ -42,9 +42,9 @@ module Badline
|
|
|
42
42
|
|
|
43
43
|
# Fits the texture, the logical size and the window to another
|
|
44
44
|
# machine's screen, unless it measures the same.
|
|
45
|
-
def fit(screen
|
|
45
|
+
def fit(screen)
|
|
46
46
|
before = [@width, @height, @texture_width]
|
|
47
|
-
measure(screen
|
|
47
|
+
measure(screen)
|
|
48
48
|
return if before == [@width, @height, @texture_width]
|
|
49
49
|
|
|
50
50
|
SDL.SDL_DestroyTexture(@texture)
|
|
@@ -56,9 +56,12 @@ module Badline
|
|
|
56
56
|
SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Titles the window, TITLE followed by `
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# Titles the window, TITLE followed by the controls' `tag`, unless it's
|
|
60
|
+
# empty, and MUTED when `muted`, each in brackets.
|
|
61
|
+
def title(tag, muted)
|
|
62
|
+
tags = tag.empty? ? [] : [tag]
|
|
63
|
+
tags << "MUTED" if muted
|
|
64
|
+
SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |name| "[#{name}]" }).join(" "))
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def close
|
|
@@ -70,9 +73,9 @@ module Badline
|
|
|
70
73
|
|
|
71
74
|
private
|
|
72
75
|
|
|
73
|
-
def measure(screen
|
|
76
|
+
def measure(screen)
|
|
74
77
|
@texture_width = screen.width
|
|
75
|
-
@width = screen.
|
|
78
|
+
@width = screen.window_width
|
|
76
79
|
@height = screen.height
|
|
77
80
|
end
|
|
78
81
|
|
|
@@ -28,7 +28,7 @@ module Badline
|
|
|
28
28
|
# The expanded path of the disk in device 8, however it went in, or
|
|
29
29
|
# an empty one.
|
|
30
30
|
def disk_path
|
|
31
|
-
drive = @computer.
|
|
31
|
+
drive = @computer.true_drive
|
|
32
32
|
unless drive.nil?
|
|
33
33
|
disk = drive.disk
|
|
34
34
|
return disk.nil? ? "" : disk.path.to_s
|
|
@@ -39,7 +39,7 @@ module Badline
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def inserted?
|
|
42
|
-
drive = @computer.
|
|
42
|
+
drive = @computer.true_drive
|
|
43
43
|
drive.nil? ? @computer.mounted? : !drive.disk.nil?
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -73,7 +73,7 @@ module Badline
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def eject_disk
|
|
76
|
-
drive = @computer.
|
|
76
|
+
drive = @computer.true_drive
|
|
77
77
|
drive.nil? ? @computer.unmount : drive.insert(nil)
|
|
78
78
|
end
|
|
79
79
|
|