badline 0.2.0 → 0.3.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 +67 -0
- data/CLAUDE.md +6 -0
- data/README.md +24 -6
- data/doc/pinned-behaviour.md +18 -6
- data/exe/badline +5 -2
- data/lib/badline/address_bus.rb +14 -6
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- data/lib/badline/cartridge/mach5.rb +1 -1
- data/lib/badline/cartridge/magic_desk.rb +5 -1
- data/lib/badline/cartridge/ocean.rb +5 -2
- data/lib/badline/cartridge/rex_utility.rb +1 -1
- data/lib/badline/cartridge/rgcd.rb +1 -1
- data/lib/badline/cartridge/simons_basic.rb +2 -2
- data/lib/badline/cartridge/westermann.rb +2 -2
- data/lib/badline/cartridge.rb +16 -8
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia/interrupt_register.rb +20 -6
- data/lib/badline/cia.rb +36 -19
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +41 -11
- data/lib/badline/cpu/microcode.rb +41 -43
- data/lib/badline/cpu.rb +3 -3
- data/lib/badline/gui/application.rb +41 -9
- data/lib/badline/kernal_trap/channel.rb +118 -8
- data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
- data/lib/badline/kernal_trap/drive/channels.rb +53 -0
- data/lib/badline/kernal_trap/drive/memory.rb +109 -0
- data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +148 -132
- data/lib/badline/kernal_trap/file.rb +0 -5
- data/lib/badline/kernal_trap/load.rb +93 -25
- data/lib/badline/kernal_trap/routine.rb +17 -0
- data/lib/badline/kernal_trap/save.rb +48 -15
- data/lib/badline/kernal_trap/serial.rb +34 -2
- data/lib/badline/media.rb +2 -2
- data/lib/badline/memory.rb +7 -2
- data/lib/badline/rom.rb +14 -5
- data/lib/badline/sid/catch_up.rb +110 -0
- data/lib/badline/sid/decimator.rb +6 -1
- data/lib/badline/sid/filter.rb +5 -5
- data/lib/badline/sid/waveform/fast_forward.rb +10 -10
- data/lib/badline/sid/waveform.rb +7 -7
- data/lib/badline/sid.rb +16 -92
- data/lib/badline/status.rb +255 -32
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +78 -17
- data/lib/badline/storage/host_directory.rb +26 -2
- data/lib/badline/storage/sid_file.rb +37 -3
- data/lib/badline/storage.rb +3 -1
- data/lib/badline/time_of_day.rb +7 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/graphics_shifter.rb +8 -2
- data/lib/badline/vic/registers.rb +16 -27
- data/lib/badline/vic.rb +23 -10
- data/lib/badline.rb +1 -0
- metadata +9 -1
data/lib/badline/cartridge.rb
CHANGED
|
@@ -35,12 +35,12 @@ module Badline
|
|
|
35
35
|
BANK_SIZE = 0x2000
|
|
36
36
|
|
|
37
37
|
HARDWARE_TYPES = {
|
|
38
|
-
0 =>
|
|
39
|
-
4 =>
|
|
40
|
-
9 =>
|
|
41
|
-
12 =>
|
|
42
|
-
19 =>
|
|
43
|
-
51 =>
|
|
38
|
+
0 => Standard, 1 => ActionReplay, 2 => KCSPower, 3 => FinalCartridge3,
|
|
39
|
+
4 => SimonsBasic, 5 => Ocean, 7 => FunPlay, 8 => SuperGames,
|
|
40
|
+
9 => AtomicPower, 10 => EpyxFastload, 11 => Westermann,
|
|
41
|
+
12 => RexUtility, 15 => GameSystem, 17 => Dinamic, 18 => Zaxxon,
|
|
42
|
+
19 => MagicDesk, 21 => Comal80, 32 => EasyFlash, 36 => RetroReplay,
|
|
43
|
+
51 => Mach5, 53 => Pagefox, 57 => RGCD, 60 => GMod2
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
46
46
|
# The EXROM and GAME line levels of each memory configuration. The lines
|
|
@@ -62,11 +62,11 @@ module Badline
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def from_crt(crt, **)
|
|
65
|
-
|
|
65
|
+
mapper = HARDWARE_TYPES.fetch(crt.hardware_type) do
|
|
66
66
|
raise UnsupportedTypeError,
|
|
67
67
|
"Unsupported cartridge hardware type #{crt.hardware_type}"
|
|
68
68
|
end
|
|
69
|
-
|
|
69
|
+
mapper.new(crt, **)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -168,10 +168,18 @@ module Badline
|
|
|
168
168
|
Bank.new(data)
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# A bank the image leaves out reads as erased ROM.
|
|
171
172
|
def bank(banks, number)
|
|
172
173
|
banks[number] || EMPTY_BANK
|
|
173
174
|
end
|
|
174
175
|
|
|
176
|
+
# The bank register bits a ROM decodes. The ROM holds the highest bank
|
|
177
|
+
# in the image, and has address lines for the power of two that covers
|
|
178
|
+
# it; the register bits above those lines are ignored.
|
|
179
|
+
def bank_mask(banks)
|
|
180
|
+
(1 << (banks.length - 1).bit_length) - 1
|
|
181
|
+
end
|
|
182
|
+
|
|
175
183
|
# Sorts the CHIP packets into ROML and ROMH banks by load address. A 16K
|
|
176
184
|
# chip at $8000 spans both.
|
|
177
185
|
def banks_from(chips)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# A digest of the machine's state at one cycle, for comparing two runs of
|
|
5
|
+
# the same scenario, on two builds or two revisions. Each component is
|
|
6
|
+
# hashed on its own with 32-bit FNV-1a, so a mismatch names the part of the
|
|
7
|
+
# machine that went its own way. Nothing is read through a chip's bus
|
|
8
|
+
# interface, so taking a checkpoint leaves the machine as it was.
|
|
9
|
+
#
|
|
10
|
+
# Colour RAM is digested as the four bits each cell stores: the upper
|
|
11
|
+
# nibble a CPU read sees comes off the open bus.
|
|
12
|
+
class Checkpoint
|
|
13
|
+
COMPONENTS = %w[cpu ram color_ram display vic cia1 cia2 sid].freeze
|
|
14
|
+
|
|
15
|
+
FNV_OFFSET = 0x811c9dc5
|
|
16
|
+
FNV_PRIME = 0x01000193
|
|
17
|
+
|
|
18
|
+
attr_reader :cycle, :digests
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def take(computer)
|
|
22
|
+
new(computer.cycles, [
|
|
23
|
+
fnv1a(cpu_state(computer.cpu)),
|
|
24
|
+
fnv1a(ram(computer.ram)),
|
|
25
|
+
fnv1a(color_ram(computer.address_bus.color_ram)),
|
|
26
|
+
fnv1a(computer.vic.display),
|
|
27
|
+
fnv1a(vic_state(computer.vic)),
|
|
28
|
+
fnv1a(cia_state(computer.cia1)),
|
|
29
|
+
fnv1a(cia_state(computer.cia2)),
|
|
30
|
+
fnv1a(Array.new(0x20) { |reg| computer.sid.register(reg) })
|
|
31
|
+
])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reads a line written by #to_s.
|
|
35
|
+
def parse(line)
|
|
36
|
+
fields = line.split
|
|
37
|
+
digests = COMPONENTS.map do |name|
|
|
38
|
+
field = fields.find { |f| f.start_with?("#{name}=") }
|
|
39
|
+
raise ArgumentError, "no #{name} digest in #{line.inspect}" unless field
|
|
40
|
+
|
|
41
|
+
field.split("=").last.to_i(16)
|
|
42
|
+
end
|
|
43
|
+
new(fields.first.to_i, digests)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fnv1a(values)
|
|
47
|
+
hash = FNV_OFFSET
|
|
48
|
+
values.each { |value| hash = ((hash ^ (value & 0xffffffff)) * FNV_PRIME) & 0xffffffff }
|
|
49
|
+
hash
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def cpu_state(cpu)
|
|
55
|
+
[cpu.program_counter, cpu.a, cpu.x, cpu.y, cpu.stack_pointer, cpu.p, cpu.cycles]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ram(memory)
|
|
59
|
+
Array.new(0x10000) { |addr| memory.peek(addr) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def color_ram(memory)
|
|
63
|
+
Array.new(0x400) { |offset| memory.nibble(0xd800 + offset) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def vic_state(vic)
|
|
67
|
+
vic.register_file + [vic.rasterline, vic.column]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def cia_state(cia)
|
|
71
|
+
start = cia.start
|
|
72
|
+
[cia.peek(start), cia.peek(start + 1), cia.peek(start + 2), cia.peek(start + 3),
|
|
73
|
+
cia.timer_a, cia.timer_a_latch, cia.timer_b, cia.timer_b_latch, cia.serial.data,
|
|
74
|
+
cia.control_a.value, cia.control_b.value, cia.interrupt_status.value,
|
|
75
|
+
cia.interrupt_control.value] + cia.time_of_day.registers
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initialize(cycle, digests)
|
|
80
|
+
@cycle = cycle
|
|
81
|
+
@digests = digests
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The components whose digests differ from `other`'s.
|
|
85
|
+
def differences(other)
|
|
86
|
+
COMPONENTS.each_index.reject { |i| digests[i] == other.digests[i] }.map { |i| COMPONENTS[i] }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ==(other)
|
|
90
|
+
other.is_a?(Checkpoint) && cycle == other.cycle && digests == other.digests
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_s
|
|
94
|
+
fields = COMPONENTS.each_index.map { |i| "#{COMPONENTS[i]}=#{digests[i].to_s(16).rjust(8, '0')}" }
|
|
95
|
+
"#{cycle} #{fields.join(' ')}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -9,8 +9,8 @@ module Badline
|
|
|
9
9
|
attr_reader :status, :mask, :quiet
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
|
-
@mask =
|
|
13
|
-
@status =
|
|
12
|
+
@mask = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
|
|
13
|
+
@status = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
|
|
14
14
|
@pending = 0
|
|
15
15
|
@read = @read_last_cycle = @read_two_cycles_ago = nil
|
|
16
16
|
@timer_b_bug = false
|
|
@@ -40,8 +40,9 @@ module Badline
|
|
|
40
40
|
|
|
41
41
|
# Latch a source, pulling the interrupt line if it is armed.
|
|
42
42
|
def flag(source)
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
bit = source_bit(source)
|
|
44
|
+
@status.value |= bit
|
|
45
|
+
assert! if mask.value.anybits?(bit)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# The 6526 timer B bug: an underflow on the cycle after a read still
|
|
@@ -70,9 +71,9 @@ module Badline
|
|
|
70
71
|
# Bit 7 picks between setting and clearing the mask bits given.
|
|
71
72
|
def write(value)
|
|
72
73
|
if value.nobits?(0x80)
|
|
73
|
-
mask.value &= ~(value & 0x1f)
|
|
74
|
+
@mask.value &= ~(value & 0x1f)
|
|
74
75
|
else
|
|
75
|
-
mask.value |= (value & 0x1f)
|
|
76
|
+
@mask.value |= (value & 0x1f)
|
|
76
77
|
end
|
|
77
78
|
if mask.value.anybits?(status.value & 0x1f)
|
|
78
79
|
assert!(2) unless interrupted?
|
|
@@ -83,6 +84,19 @@ module Badline
|
|
|
83
84
|
@pending = 0
|
|
84
85
|
end
|
|
85
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def source_bit(source)
|
|
91
|
+
case source
|
|
92
|
+
when :timer_a then 0x01
|
|
93
|
+
when :timer_b then 0x02
|
|
94
|
+
when :alarm then 0x04
|
|
95
|
+
when :serial then 0x08
|
|
96
|
+
when :flag then 0x10
|
|
97
|
+
else raise ArgumentError, "unknown interrupt source #{source}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
86
100
|
end
|
|
87
101
|
end
|
|
88
102
|
end
|
data/lib/badline/cia.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
3
|
require "badline/cia/interrupt_register"
|
|
5
4
|
require "badline/cia/serial"
|
|
6
5
|
require "badline/cia/timer"
|
|
@@ -9,22 +8,40 @@ module Badline
|
|
|
9
8
|
# CIA (Complex Interface Adapter) chip
|
|
10
9
|
class CIA
|
|
11
10
|
include Addressable
|
|
12
|
-
extend Forwardable
|
|
13
11
|
|
|
14
12
|
attr_reader :start, :control_a, :control_b, :peripheral, :serial
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
def_delegator :@icr, :mask, :interrupt_control
|
|
18
|
-
def_delegator :@icr, :assert!, :interrupt!
|
|
14
|
+
def interrupt_status = @icr.status
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
def interrupt_control = @icr.mask
|
|
17
|
+
|
|
18
|
+
def interrupt!(delay = 1) = @icr.assert!(delay)
|
|
19
|
+
|
|
20
|
+
def time_of_day = @tod
|
|
21
|
+
|
|
22
|
+
def timer_a = @ta.counter
|
|
23
|
+
|
|
24
|
+
def timer_a=(value)
|
|
25
|
+
@ta.counter = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def timer_a_latch = @ta.latch
|
|
29
|
+
|
|
30
|
+
def timer_a_latch=(value)
|
|
31
|
+
@ta.latch = value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def timer_b = @tb.counter
|
|
35
|
+
|
|
36
|
+
def timer_b=(value)
|
|
37
|
+
@tb.counter = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def timer_b_latch = @tb.latch
|
|
41
|
+
|
|
42
|
+
def timer_b_latch=(value)
|
|
43
|
+
@tb.latch = value
|
|
44
|
+
end
|
|
28
45
|
|
|
29
46
|
def initialize(start: 0, peripheral: nil)
|
|
30
47
|
addressable_at(start, length: 2**8)
|
|
@@ -49,10 +66,10 @@ module Badline
|
|
|
49
66
|
@tod = TimeOfDay.new
|
|
50
67
|
@icr = InterruptRegister.new
|
|
51
68
|
@icr_status = @icr.status
|
|
52
|
-
@control_a =
|
|
53
|
-
|
|
54
|
-
@control_b =
|
|
55
|
-
|
|
69
|
+
@control_a = ControlRegister.new(%i[start output out_mode run_mode load
|
|
70
|
+
in_mode serial_mode clock_frequency])
|
|
71
|
+
@control_b = ControlRegister.new(%i[start output out_mode run_mode load
|
|
72
|
+
in_cnt in_timer_a alarm])
|
|
56
73
|
@ta = Timer.new(@control_a)
|
|
57
74
|
@tb = Timer.new(@control_b)
|
|
58
75
|
@serial = Serial.new(@control_a)
|
|
@@ -100,7 +117,7 @@ module Badline
|
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
def peek(addr)
|
|
103
|
-
case
|
|
120
|
+
case offset_of(addr) & 0x0f
|
|
104
121
|
when 0x00 then read_port_a
|
|
105
122
|
when 0x01 then read_port_b
|
|
106
123
|
when 0x02 then @data_dir_a
|
|
@@ -121,7 +138,7 @@ module Badline
|
|
|
121
138
|
end
|
|
122
139
|
|
|
123
140
|
def poke(addr, value)
|
|
124
|
-
case
|
|
141
|
+
case offset_of(addr) & 0x0f
|
|
125
142
|
when 0x00 then @data_port_a = value
|
|
126
143
|
when 0x01 then update_port_b { @data_port_b = value }
|
|
127
144
|
when 0x02 then @data_dir_a = value
|
data/lib/badline/color_memory.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Badline
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def peek(addr)
|
|
13
|
-
(@vic.phi1_data & 0xf0) | @storage[
|
|
13
|
+
(@vic.phi1_data & 0xf0) | @storage[offset_of(addr)]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def poke(addr, value)
|
|
@@ -19,7 +19,7 @@ module Badline
|
|
|
19
19
|
|
|
20
20
|
# The four bits the cell stores, as the VIC's c-access sees them.
|
|
21
21
|
def nibble(addr)
|
|
22
|
-
@storage[
|
|
22
|
+
@storage[offset_of(addr)]
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
data/lib/badline/computer.rb
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Badline
|
|
6
4
|
class Computer
|
|
7
5
|
include IntegerHelper
|
|
8
6
|
include KeyboardBuffer
|
|
9
|
-
extend Forwardable
|
|
10
7
|
|
|
11
8
|
attr_reader :address_bus, :cpu, :cycles
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
def vic = address_bus.vic
|
|
11
|
+
|
|
12
|
+
def cia1 = address_bus.cia1
|
|
13
|
+
|
|
14
|
+
def cia2 = address_bus.cia2
|
|
15
|
+
|
|
16
|
+
def sid = address_bus.sid
|
|
17
|
+
|
|
18
|
+
def ram = address_bus.ram
|
|
19
|
+
|
|
20
|
+
def keyboard = address_bus.keyboard
|
|
21
|
+
|
|
22
|
+
def joystick1 = address_bus.joystick1
|
|
23
|
+
|
|
24
|
+
def joystick2 = address_bus.joystick2
|
|
25
|
+
|
|
26
|
+
def control_ports = address_bus.control_ports
|
|
27
|
+
|
|
28
|
+
def datasette = address_bus.datasette
|
|
29
|
+
|
|
30
|
+
def install_debug_register(&) = address_bus.install_debug_register(&)
|
|
15
31
|
|
|
16
32
|
def initialize(debug: false, sid_model: :mos6581)
|
|
17
33
|
@address_bus = AddressBus.new(sid_model:)
|
|
@@ -29,6 +45,7 @@ module Badline
|
|
|
29
45
|
@freeze_writes = 0
|
|
30
46
|
@init_handlers = []
|
|
31
47
|
@pending_keys = nil
|
|
48
|
+
@drive = nil
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
INIT_THRESHOLD = 2_500_000
|
|
@@ -64,17 +81,28 @@ module Badline
|
|
|
64
81
|
cartridge.clock = -> { @cycles }
|
|
65
82
|
cartridge.on_nmi_change { |level| @cartridge_nmi = level }
|
|
66
83
|
address_bus.attach_cartridge(cartridge)
|
|
84
|
+
power_cycle!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A cartridge goes in with the power off, so attaching one switches the
|
|
88
|
+
# machine off and on: the VIC and RAM start from their power-on state,
|
|
89
|
+
# and the RES line resets everything else.
|
|
90
|
+
def power_cycle!
|
|
91
|
+
vic.power_on!
|
|
92
|
+
address_bus.power_on!
|
|
67
93
|
reset!
|
|
68
94
|
end
|
|
69
95
|
|
|
70
|
-
# The RES line reaches the CPU and its port, both CIAs, the SID
|
|
71
|
-
# cartridge port
|
|
96
|
+
# The RES line reaches the CPU and its port, both CIAs, the SID, the
|
|
97
|
+
# cartridge port and, through the serial bus's RESET line, the drive.
|
|
98
|
+
# The VIC has no reset pin.
|
|
72
99
|
def reset!
|
|
73
100
|
address_bus.reset_port!
|
|
74
101
|
@cia1.reset!
|
|
75
102
|
@cia2.reset!
|
|
76
103
|
@sid.reset!
|
|
77
104
|
address_bus.cartridge&.reset
|
|
105
|
+
@drive&.reset!
|
|
78
106
|
@freezing = false
|
|
79
107
|
@nmi_asserted = false
|
|
80
108
|
cpu.reset!
|
|
@@ -95,14 +123,16 @@ module Badline
|
|
|
95
123
|
address_bus.cartridge&.release_button
|
|
96
124
|
end
|
|
97
125
|
|
|
126
|
+
# A disk swap keeps the drive's RAM, which only a drive reset clears.
|
|
98
127
|
def mount(storage)
|
|
99
|
-
|
|
128
|
+
@drive_memory ||= KernalTrap::Drive::Memory.new
|
|
129
|
+
@drive = KernalTrap::Drive.new(storage, @drive_memory)
|
|
130
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
|
|
100
131
|
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
101
|
-
|
|
102
|
-
KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
|
|
132
|
+
KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
|
|
103
133
|
return unless storage.respond_to?(:write_file)
|
|
104
134
|
|
|
105
|
-
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus,
|
|
135
|
+
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
|
|
106
136
|
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
107
137
|
end
|
|
108
138
|
|
|
@@ -107,60 +107,58 @@ module Badline
|
|
|
107
107
|
# - +optional_dummy+: true for indexed reads, which skip the extra
|
|
108
108
|
# cycle unless indexing crosses a page
|
|
109
109
|
Microcode = Struct.new(:operation, :plan, :writes, :optional_dummy) do
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
|
|
115
|
-
|
|
116
|
-
build(instruction)
|
|
117
|
-
end.freeze
|
|
118
|
-
end
|
|
110
|
+
def self.table
|
|
111
|
+
Array.new(256) do |opcode|
|
|
112
|
+
instruction = Instruction.find(opcode)
|
|
113
|
+
raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
build(instruction)
|
|
116
|
+
end.freeze
|
|
117
|
+
end
|
|
123
118
|
|
|
124
|
-
|
|
119
|
+
def self.write_mask(plan)
|
|
120
|
+
plan.map { |step| WRITE_STEPS.include?(step) }.freeze
|
|
121
|
+
end
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
def self.build(instruction)
|
|
124
|
+
plan = plan_for(instruction)
|
|
125
|
+
new(instruction.name, plan, write_mask(plan),
|
|
126
|
+
instruction.boundary_cycle?)
|
|
127
|
+
end
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
end
|
|
140
|
-
when *BRANCHES then BRANCH_PLAN
|
|
141
|
-
else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
|
|
129
|
+
def self.plan_for(instruction)
|
|
130
|
+
case instruction.name
|
|
131
|
+
when :jmp
|
|
132
|
+
if instruction.addressing_mode == :indirect
|
|
133
|
+
JMP_INDIRECT_PLAN
|
|
134
|
+
else
|
|
135
|
+
JMP_ABSOLUTE_PLAN
|
|
142
136
|
end
|
|
137
|
+
when *BRANCHES then BRANCH_PLAN
|
|
138
|
+
else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
|
|
143
139
|
end
|
|
140
|
+
end
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
end
|
|
142
|
+
def self.addressed_plan(instruction)
|
|
143
|
+
mode = instruction.addressing_mode
|
|
144
|
+
case mode
|
|
145
|
+
when :implied then IMPLIED_PLAN
|
|
146
|
+
when :accumulator then ACCUMULATOR_PLAN
|
|
147
|
+
when :immediate then IMMEDIATE_PLAN
|
|
148
|
+
else
|
|
149
|
+
[:op_fetch, *ADDRESSING_STEPS.fetch(mode),
|
|
150
|
+
*OPERATION_STEPS.fetch(operation_kind(instruction.name))].freeze
|
|
155
151
|
end
|
|
152
|
+
end
|
|
156
153
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
def self.operation_kind(name)
|
|
155
|
+
return :write if STORES.include?(name)
|
|
156
|
+
return :rmw if READ_MODIFY_WRITES.include?(name)
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
end
|
|
158
|
+
:read
|
|
163
159
|
end
|
|
160
|
+
|
|
161
|
+
private_class_method :build, :plan_for, :addressed_plan, :operation_kind
|
|
164
162
|
end
|
|
165
163
|
|
|
166
164
|
FETCH_WRITES = Microcode.write_mask(FETCH_PLAN)
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -24,11 +24,11 @@ module Badline
|
|
|
24
24
|
|
|
25
25
|
# +ane_constant+ is ANE's magic constant, which varies from chip to
|
|
26
26
|
# chip. The default is the C64 6510's.
|
|
27
|
-
def initialize(memory
|
|
27
|
+
def initialize(memory, debug: false, ane_constant: 0xef)
|
|
28
28
|
@debug = debug
|
|
29
29
|
@ane_constant = ane_constant
|
|
30
|
-
@memory = memory
|
|
31
|
-
@status =
|
|
30
|
+
@memory = memory
|
|
31
|
+
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
32
32
|
reset_registers
|
|
33
33
|
|
|
34
34
|
@nmi = @irq = false
|
|
@@ -6,6 +6,7 @@ module Badline
|
|
|
6
6
|
PAL_CLOCK_HZ = 985_248
|
|
7
7
|
TITLE = "Badline"
|
|
8
8
|
TOGGLE_SYM = SDL2::Key::TAB
|
|
9
|
+
MUTE_SYM = SDL2::Key::F10
|
|
9
10
|
REVERSE_MOD = SDL2::Key::Mod::SHIFT
|
|
10
11
|
|
|
11
12
|
SHARED_KEYS = %i[up left cursor_h cursor_v space w a s d lshift].freeze
|
|
@@ -28,24 +29,23 @@ module Badline
|
|
|
28
29
|
}.freeze
|
|
29
30
|
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
30
31
|
|
|
31
|
-
def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil,
|
|
32
|
-
@computer = Computer.new(
|
|
32
|
+
def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil, sound: false)
|
|
33
|
+
@computer = Computer.new(sid_model: sid_model || Media.sid_model(media_path))
|
|
33
34
|
puts Media.attach(@computer, media_path, autostart:, song:) if media_path
|
|
34
35
|
|
|
35
36
|
@mode = :keyboard
|
|
36
37
|
@pot_device = nil
|
|
37
38
|
@panes = [ScreenPane.new(@computer)]
|
|
39
|
+
@stream = open_stream if sound
|
|
40
|
+
@paced = ENV["NOVSYNC"].nil?
|
|
38
41
|
@window = Window.new(
|
|
39
42
|
title: TITLE,
|
|
40
43
|
width: canvas_width, height: canvas_height,
|
|
41
|
-
vsync:
|
|
44
|
+
vsync: @paced && !@stream
|
|
42
45
|
)
|
|
43
46
|
@gamepads = Gamepads.new(@computer)
|
|
44
47
|
@gamepads.names.each { |name| puts "Gamepad: #{name}" }
|
|
45
|
-
|
|
46
|
-
rate = @window.refresh_rate
|
|
47
|
-
@cycles_per_frame = PAL_CLOCK_HZ / rate
|
|
48
|
-
puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
|
|
48
|
+
fit_frame
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def run
|
|
@@ -54,9 +54,12 @@ module Badline
|
|
|
54
54
|
handle_events
|
|
55
55
|
@gamepads.poll
|
|
56
56
|
@cycles_per_frame.times { @computer.cycle! }
|
|
57
|
+
@stream&.feed
|
|
57
58
|
@window.draw(@panes)
|
|
59
|
+
@stream.pace(@frame_seconds) if @stream && @paced
|
|
58
60
|
end
|
|
59
61
|
ensure
|
|
62
|
+
@stream&.close
|
|
60
63
|
@gamepads.close
|
|
61
64
|
puts @computer.cpu.inspect
|
|
62
65
|
end
|
|
@@ -84,6 +87,7 @@ module Badline
|
|
|
84
87
|
|
|
85
88
|
def handle_key_down(event)
|
|
86
89
|
return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
|
|
90
|
+
return toggle_mute if event.sym == MUTE_SYM
|
|
87
91
|
|
|
88
92
|
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
89
93
|
if port
|
|
@@ -94,7 +98,7 @@ module Badline
|
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
def handle_key_up(event)
|
|
97
|
-
return if event.sym
|
|
101
|
+
return if [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
|
|
98
102
|
|
|
99
103
|
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
100
104
|
if port
|
|
@@ -123,7 +127,35 @@ module Badline
|
|
|
123
127
|
@mode = modes[(modes.index(@mode) + step) % modes.size]
|
|
124
128
|
release_inputs
|
|
125
129
|
attach_pot_device
|
|
126
|
-
|
|
130
|
+
update_title
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def toggle_mute
|
|
134
|
+
return unless @stream
|
|
135
|
+
|
|
136
|
+
@stream.toggle_mute
|
|
137
|
+
update_title
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def update_title
|
|
141
|
+
tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
|
|
142
|
+
@window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def open_stream
|
|
146
|
+
sink = Audio::SDLSink.new(rate: Audio::Renderer::DEFAULT_RATE)
|
|
147
|
+
puts "Sound at #{sink.rate} Hz, F10 mutes"
|
|
148
|
+
Audio::Stream.new(sink, @computer.sid,
|
|
149
|
+
on_underrun: -> { puts "Running below real time, so the sound will stutter." })
|
|
150
|
+
rescue Audio::SDLSink::Error => e
|
|
151
|
+
warn "badline: no sound, can't open the audio device: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def fit_frame
|
|
155
|
+
rate = @window.refresh_rate
|
|
156
|
+
@cycles_per_frame = PAL_CLOCK_HZ / rate
|
|
157
|
+
@frame_seconds = @cycles_per_frame.fdiv(PAL_CLOCK_HZ)
|
|
158
|
+
puts "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
|
|
127
159
|
end
|
|
128
160
|
|
|
129
161
|
def attach_pot_device
|