badline 0.2.1 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +107 -0
- data/CLAUDE.md +51 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +119 -51
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +293 -39
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +28 -14
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +3 -2
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- 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 +9 -0
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +19 -10
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +34 -14
- data/lib/badline/cpu.rb +2 -2
- data/lib/badline/gui/application.rb +70 -26
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +102 -7
- data/lib/badline/kernal_trap/drive/block_commands.rb +108 -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 +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +109 -131
- data/lib/badline/kernal_trap/file.rb +6 -14
- data/lib/badline/kernal_trap/load.rb +6 -0
- data/lib/badline/kernal_trap/save.rb +13 -7
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +44 -13
- data/lib/badline/memory.rb +2 -2
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/rom.rb +14 -5
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- 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/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +8 -8
- data/lib/badline/sid.rb +19 -93
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +49 -4
- data/lib/badline/storage/host_directory.rb +17 -4
- data/lib/badline/storage/sid_file.rb +36 -2
- data/lib/badline/storage.rb +18 -1
- data/lib/badline/time_of_day.rb +11 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +49 -8
- data/lib/badline/vic/registers.rb +19 -27
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +98 -46
- data/lib/badline.rb +5 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +37 -20
- data/exe/badline +0 -75
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# GEO-RAM: a RAM expansion on the expansion port, seen through a 256-byte
|
|
6
|
+
# window at $DE00-$DEFF. Two write-only registers pick the page the
|
|
7
|
+
# window shows: $DFFE selects one of the 64 pages of a 16K block, and
|
|
8
|
+
# $DFFF selects the block. As in VICE, the registers answer at every
|
|
9
|
+
# address from $DF80 up, even for $DFFE and odd for $DFFF, and the block
|
|
10
|
+
# number wraps at the number of blocks the RAM holds. Reads of I/O 2 are
|
|
11
|
+
# open bus. It leaves EXROM and GAME high, and RES clears both
|
|
12
|
+
# registers.
|
|
13
|
+
class GeoRAM < Cartridge
|
|
14
|
+
SIZES = [64, 128, 256, 512, 1024, 2048, 4096].freeze
|
|
15
|
+
BLOCK_SIZE = 0x4000
|
|
16
|
+
PAGE_MASK = 0x3f
|
|
17
|
+
|
|
18
|
+
attr_reader :size
|
|
19
|
+
|
|
20
|
+
# size is the RAM in kilobytes.
|
|
21
|
+
def initialize(size: 512)
|
|
22
|
+
raise ArgumentError, "Unsupported GEO-RAM size #{size}K" unless SIZES.include?(size)
|
|
23
|
+
|
|
24
|
+
@size = size
|
|
25
|
+
@block_mask = ((size * 1024) / BLOCK_SIZE) - 1
|
|
26
|
+
super(Storage::CRTFile::Image.new(hardware_type: nil, subtype: 0, exrom: 1, game: 1,
|
|
27
|
+
name: "GEO-RAM", chips: []))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def readable_io_pages
|
|
31
|
+
[0xde]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def peek(addr)
|
|
35
|
+
@ram_data[@window + (addr & 0xff)]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def poke(addr, value)
|
|
39
|
+
if addr <= 0xdeff
|
|
40
|
+
@ram_data[@window + (addr & 0xff)] = value
|
|
41
|
+
elsif addr >= 0xdf80
|
|
42
|
+
if addr.odd?
|
|
43
|
+
@block = value & @block_mask
|
|
44
|
+
else
|
|
45
|
+
@page = value & PAGE_MASK
|
|
46
|
+
end
|
|
47
|
+
@window = (@block * BLOCK_SIZE) + (@page * 0x100)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reset
|
|
52
|
+
@block = @page = @window = 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def install_chips(_chips)
|
|
58
|
+
@ram_data = Array.new(@size * 1024, 0)
|
|
59
|
+
reset
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Cartridge
|
|
5
|
+
# Magic Desk: up to 128 8K banks. A write to I/O 1 selects the bank from
|
|
6
|
+
# bits 0-6, masked to the size of the ROM and at least four banks, and
|
|
7
|
+
# switches the ROM out with bit 7.
|
|
5
8
|
class MagicDesk < Cartridge
|
|
6
9
|
def poke(addr, value)
|
|
7
10
|
return if addr > 0xdeff
|
|
8
11
|
|
|
9
12
|
@exrom = value.anybits?(0x80) ? 1 : 0
|
|
10
|
-
@roml = @banks
|
|
13
|
+
@roml = bank(@banks, value & @bank_mask)
|
|
11
14
|
changed!
|
|
12
15
|
end
|
|
13
16
|
|
|
@@ -19,6 +22,7 @@ module Badline
|
|
|
19
22
|
|
|
20
23
|
def install_chips(chips)
|
|
21
24
|
@banks = banks_from(chips).first
|
|
25
|
+
@bank_mask = (bank_mask(@banks) | 0x03) & 0x7f
|
|
22
26
|
reset
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Cartridge
|
|
5
|
+
# Ocean: up to 64 8K banks. A write to I/O 1 selects the bank from bits
|
|
6
|
+
# 0-5, masked to the size of the ROM.
|
|
5
7
|
class Ocean < Cartridge
|
|
6
8
|
def poke(addr, value)
|
|
7
9
|
return if addr > 0xdeff
|
|
8
10
|
|
|
9
|
-
select_bank(
|
|
11
|
+
select_bank(value & @bank_mask)
|
|
10
12
|
changed!
|
|
11
13
|
end
|
|
12
14
|
|
|
@@ -19,12 +21,13 @@ module Badline
|
|
|
19
21
|
|
|
20
22
|
# In 16K mode the selected bank shows through ROMH as well.
|
|
21
23
|
def select_bank(number)
|
|
22
|
-
@roml = @banks
|
|
24
|
+
@roml = bank(@banks, number)
|
|
23
25
|
@romh = @roml if game.zero?
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def install_chips(chips)
|
|
27
29
|
@banks = banks_from(chips).first
|
|
30
|
+
@bank_mask = bank_mask(@banks) & 0x3f
|
|
28
31
|
reset
|
|
29
32
|
end
|
|
30
33
|
end
|
data/lib/badline/cartridge.rb
CHANGED
|
@@ -26,6 +26,7 @@ require "badline/cartridge/atomic_power"
|
|
|
26
26
|
require "badline/cartridge/final_cartridge3"
|
|
27
27
|
require "badline/cartridge/retro_replay"
|
|
28
28
|
require "badline/cartridge/kcs_power"
|
|
29
|
+
require "badline/cartridge/geo_ram"
|
|
29
30
|
|
|
30
31
|
module Badline
|
|
31
32
|
class Cartridge
|
|
@@ -168,10 +169,18 @@ module Badline
|
|
|
168
169
|
Bank.new(data)
|
|
169
170
|
end
|
|
170
171
|
|
|
172
|
+
# A bank the image leaves out reads as erased ROM.
|
|
171
173
|
def bank(banks, number)
|
|
172
174
|
banks[number] || EMPTY_BANK
|
|
173
175
|
end
|
|
174
176
|
|
|
177
|
+
# The bank register bits a ROM decodes. The ROM holds the highest bank
|
|
178
|
+
# in the image, and has address lines for the power of two that covers
|
|
179
|
+
# it; the register bits above those lines are ignored.
|
|
180
|
+
def bank_mask(banks)
|
|
181
|
+
(1 << (banks.length - 1).bit_length) - 1
|
|
182
|
+
end
|
|
183
|
+
|
|
175
184
|
# Sorts the CHIP packets into ROML and ROMH banks by load address. A 16K
|
|
176
185
|
# chip at $8000 spans both.
|
|
177
186
|
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
|
|
@@ -8,7 +8,9 @@ module Badline
|
|
|
8
8
|
class InterruptRegister
|
|
9
9
|
attr_reader :status, :mask, :quiet
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# The 6526A is the faster of the two: see the rules below.
|
|
12
|
+
def initialize(model = :mos6526)
|
|
13
|
+
@fast = model == :mos6526a
|
|
12
14
|
@mask = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
|
|
13
15
|
@status = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
|
|
14
16
|
@pending = 0
|
|
@@ -32,37 +34,51 @@ module Badline
|
|
|
32
34
|
|
|
33
35
|
def interrupted? = status.value >= 0x80
|
|
34
36
|
|
|
37
|
+
# Raises IR on this cycle.
|
|
38
|
+
def assert_now
|
|
39
|
+
status.interrupt = true
|
|
40
|
+
@pending = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
35
43
|
# Schedules IR to rise after delay cycles, keeping an earlier one.
|
|
36
44
|
def assert!(delay = 1)
|
|
37
45
|
@pending = @pending.positive? ? [@pending, delay].min : delay
|
|
38
46
|
@quiet = false
|
|
39
47
|
end
|
|
40
48
|
|
|
41
|
-
# Latch a source, pulling the interrupt line if it is armed.
|
|
49
|
+
# Latch a source, pulling the interrupt line if it is armed. The 6526
|
|
50
|
+
# raises IR a cycle after the flag. The 6526A raises it on the flag's
|
|
51
|
+
# own cycle, unless the ICR was read on the cycle before.
|
|
42
52
|
def flag(source)
|
|
43
53
|
bit = source_bit(source)
|
|
44
54
|
@status.value |= bit
|
|
45
|
-
|
|
55
|
+
return unless mask.value.anybits?(bit)
|
|
56
|
+
|
|
57
|
+
@fast && @read_last_cycle.nil? ? assert_now : assert!
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
# The 6526 timer B bug: an underflow on the cycle after a read still
|
|
49
|
-
# raises the flag, but the next read drops it unseen.
|
|
61
|
+
# raises the flag, but the next read drops it unseen. The 6526A has
|
|
62
|
+
# no such bug.
|
|
50
63
|
def timer_b_underflow!
|
|
51
|
-
@timer_b_bug = !@read_last_cycle.nil?
|
|
64
|
+
@timer_b_bug = !@fast && !@read_last_cycle.nil?
|
|
52
65
|
flag(:timer_b)
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
# A read releases the interrupt line at once, and cancels an assert
|
|
56
|
-
# due on the next cycle, but
|
|
57
|
-
#
|
|
58
|
-
# about to be.
|
|
69
|
+
# due on the next cycle, but its acknowledge lands a cycle late. On
|
|
70
|
+
# the 6526 that holds only IR: the next cycle still reads IR set if it
|
|
71
|
+
# was set, or about to be. The 6526A reads an IR about to rise as set
|
|
72
|
+
# at once, and holds every bit it read: the next cycle reads them all
|
|
73
|
+
# again, along with anything flagged since.
|
|
59
74
|
def read
|
|
60
75
|
status.timer_b = false if @timer_b_bug
|
|
61
76
|
@timer_b_bug = false
|
|
62
77
|
value = status.value
|
|
78
|
+
value |= 0x80 if @fast && @pending == 1
|
|
63
79
|
@read = @pending == 1 ? value | 0x80 : value
|
|
64
80
|
@quiet = false
|
|
65
|
-
value |=
|
|
81
|
+
value |= held(@read_last_cycle) if @read_last_cycle
|
|
66
82
|
status.value = 0x0
|
|
67
83
|
@pending = 0
|
|
68
84
|
value
|
|
@@ -76,7 +92,7 @@ module Badline
|
|
|
76
92
|
@mask.value |= (value & 0x1f)
|
|
77
93
|
end
|
|
78
94
|
if mask.value.anybits?(status.value & 0x1f)
|
|
79
|
-
assert!(
|
|
95
|
+
assert!(arm_delay) unless interrupted?
|
|
80
96
|
elsif @pending == 1 && @read_two_cycles_ago
|
|
81
97
|
# On the 6526, masking the pending source cancels the assert due
|
|
82
98
|
# on the next cycle only while a read two cycles back is
|
|
@@ -87,6 +103,14 @@ module Badline
|
|
|
87
103
|
|
|
88
104
|
private
|
|
89
105
|
|
|
106
|
+
# Arming a pending source raises IR two cycles later on the 6526,
|
|
107
|
+
# and one cycle later on the 6526A.
|
|
108
|
+
def arm_delay = @fast ? 1 : 2
|
|
109
|
+
|
|
110
|
+
# What a read on the cycle before still holds: IR on the 6526, and
|
|
111
|
+
# every bit it read on the 6526A.
|
|
112
|
+
def held(last) = @fast ? last : last & 0x80
|
|
113
|
+
|
|
90
114
|
def source_bit(source)
|
|
91
115
|
case source
|
|
92
116
|
when :timer_a then 0x01
|
data/lib/badline/cia.rb
CHANGED
|
@@ -9,7 +9,11 @@ module Badline
|
|
|
9
9
|
class CIA
|
|
10
10
|
include Addressable
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# The original NMOS 6526 of the breadbin C64, and the HMOS 6526A of the
|
|
13
|
+
# C64C, which differs from it only in its interrupt control register.
|
|
14
|
+
MODELS = %i[mos6526 mos6526a].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :start, :control_a, :control_b, :peripheral, :serial, :model
|
|
13
17
|
|
|
14
18
|
def interrupt_status = @icr.status
|
|
15
19
|
|
|
@@ -17,6 +21,8 @@ module Badline
|
|
|
17
21
|
|
|
18
22
|
def interrupt!(delay = 1) = @icr.assert!(delay)
|
|
19
23
|
|
|
24
|
+
def time_of_day = @tod
|
|
25
|
+
|
|
20
26
|
def timer_a = @ta.counter
|
|
21
27
|
|
|
22
28
|
def timer_a=(value)
|
|
@@ -41,9 +47,15 @@ module Badline
|
|
|
41
47
|
@tb.latch = value
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
def initialize(start: 0, peripheral: nil)
|
|
50
|
+
def initialize(start: 0, peripheral: nil, model: :mos6526, region: Region::PAL)
|
|
51
|
+
raise ArgumentError, "unknown CIA model #{model}" unless MODELS.include?(model)
|
|
52
|
+
|
|
45
53
|
addressable_at(start, length: 2**8)
|
|
46
54
|
|
|
55
|
+
@model = model
|
|
56
|
+
@clock_hz = region.clock_hz
|
|
57
|
+
@mains_hz = region.mains_hz
|
|
58
|
+
|
|
47
59
|
@peripheral = peripheral
|
|
48
60
|
@port_b4_handler = nil
|
|
49
61
|
@port_b4_high = true
|
|
@@ -61,8 +73,8 @@ module Badline
|
|
|
61
73
|
@port_b4_driven_high = true
|
|
62
74
|
@cnt_high = true
|
|
63
75
|
@cnt_rise = false
|
|
64
|
-
@tod = TimeOfDay.new
|
|
65
|
-
@icr = InterruptRegister.new
|
|
76
|
+
@tod = TimeOfDay.new(clock_hz: @clock_hz, mains_hz: @mains_hz)
|
|
77
|
+
@icr = InterruptRegister.new(@model)
|
|
66
78
|
@icr_status = @icr.status
|
|
67
79
|
@control_a = ControlRegister.new(%i[start output out_mode run_mode load
|
|
68
80
|
in_mode serial_mode clock_frequency])
|
|
@@ -115,7 +127,7 @@ module Badline
|
|
|
115
127
|
end
|
|
116
128
|
|
|
117
129
|
def peek(addr)
|
|
118
|
-
case
|
|
130
|
+
case offset_of(addr) & 0x0f
|
|
119
131
|
when 0x00 then read_port_a
|
|
120
132
|
when 0x01 then read_port_b
|
|
121
133
|
when 0x02 then @data_dir_a
|
|
@@ -136,7 +148,7 @@ module Badline
|
|
|
136
148
|
end
|
|
137
149
|
|
|
138
150
|
def poke(addr, value)
|
|
139
|
-
case
|
|
151
|
+
case offset_of(addr) & 0x0f
|
|
140
152
|
when 0x00 then @data_port_a = value
|
|
141
153
|
when 0x01 then update_port_b { @data_port_b = value }
|
|
142
154
|
when 0x02 then @data_dir_a = value
|
|
@@ -221,10 +233,7 @@ module Badline
|
|
|
221
233
|
else underflowed && @cnt_high
|
|
222
234
|
end
|
|
223
235
|
)
|
|
224
|
-
if underflowed
|
|
225
|
-
interrupt_status.timer_a = true
|
|
226
|
-
interrupt! if interrupt_control.timer_a?
|
|
227
|
-
end
|
|
236
|
+
@icr.flag(:timer_a) if underflowed
|
|
228
237
|
@icr.timer_b_underflow! if @tb.underflowed
|
|
229
238
|
underflowed
|
|
230
239
|
end
|
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
|
@@ -7,6 +7,8 @@ module Badline
|
|
|
7
7
|
|
|
8
8
|
attr_reader :address_bus, :cpu, :cycles
|
|
9
9
|
|
|
10
|
+
def region = address_bus.region
|
|
11
|
+
|
|
10
12
|
def vic = address_bus.vic
|
|
11
13
|
|
|
12
14
|
def cia1 = address_bus.cia1
|
|
@@ -29,8 +31,12 @@ module Badline
|
|
|
29
31
|
|
|
30
32
|
def install_debug_register(&) = address_bus.install_debug_register(&)
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
# The machine options (sid_model:, cia_model:, vic_model:, region: and
|
|
35
|
+
# ram_expansion:) configure the AddressBus. The region sets the clock,
|
|
36
|
+
# the VIC's raster and the mains frequency the CIAs' TOD clocks count.
|
|
37
|
+
# Only PAL runs as yet.
|
|
38
|
+
def initialize(debug: false, **machine)
|
|
39
|
+
@address_bus = AddressBus.new(**machine)
|
|
34
40
|
@cpu = CPU.new(@address_bus, debug:)
|
|
35
41
|
@vic = @address_bus.vic
|
|
36
42
|
@vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
|
|
@@ -41,10 +47,12 @@ module Badline
|
|
|
41
47
|
@cycles = 0
|
|
42
48
|
@nmi_asserted = false
|
|
43
49
|
@cartridge_nmi = false
|
|
50
|
+
@restore_pulse = false
|
|
44
51
|
@freezing = false
|
|
45
52
|
@freeze_writes = 0
|
|
46
53
|
@init_handlers = []
|
|
47
54
|
@pending_keys = nil
|
|
55
|
+
@drive = nil
|
|
48
56
|
end
|
|
49
57
|
|
|
50
58
|
INIT_THRESHOLD = 2_500_000
|
|
@@ -92,14 +100,16 @@ module Badline
|
|
|
92
100
|
reset!
|
|
93
101
|
end
|
|
94
102
|
|
|
95
|
-
# The RES line reaches the CPU and its port, both CIAs, the SID
|
|
96
|
-
# cartridge port
|
|
103
|
+
# The RES line reaches the CPU and its port, both CIAs, the SID, the
|
|
104
|
+
# cartridge port, a RAM expansion and, through the serial bus's RESET
|
|
105
|
+
# line, the drive. The VIC has no reset pin.
|
|
97
106
|
def reset!
|
|
98
|
-
address_bus.
|
|
107
|
+
address_bus.reset!
|
|
99
108
|
@cia1.reset!
|
|
100
109
|
@cia2.reset!
|
|
101
110
|
@sid.reset!
|
|
102
111
|
address_bus.cartridge&.reset
|
|
112
|
+
@drive&.reset!
|
|
103
113
|
@freezing = false
|
|
104
114
|
@nmi_asserted = false
|
|
105
115
|
cpu.reset!
|
|
@@ -120,14 +130,23 @@ module Badline
|
|
|
120
130
|
address_bus.cartridge&.release_button
|
|
121
131
|
end
|
|
122
132
|
|
|
133
|
+
# RESTORE isn't in the key matrix. It fires a one-shot that pulses the
|
|
134
|
+
# NMI line however long the key is held, modelled here as one cycle.
|
|
135
|
+
def press_restore
|
|
136
|
+
@restore_pulse = true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Puts the storage in device 8. Mounting again swaps the disk at any
|
|
140
|
+
# point while the machine runs: the drive keeps its RAM, which only a
|
|
141
|
+
# drive reset clears, and its status.
|
|
123
142
|
def mount(storage)
|
|
124
|
-
drive
|
|
125
|
-
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive:)
|
|
126
|
-
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
127
|
-
KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
|
|
128
|
-
return unless storage.respond_to?(:write_file)
|
|
143
|
+
return @drive.insert(storage) if @drive
|
|
129
144
|
|
|
130
|
-
|
|
145
|
+
@drive = KernalTrap::Drive.new(storage)
|
|
146
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
|
|
147
|
+
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
148
|
+
KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
|
|
149
|
+
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
|
|
131
150
|
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
132
151
|
end
|
|
133
152
|
|
|
@@ -151,10 +170,11 @@ module Badline
|
|
|
151
170
|
|
|
152
171
|
private
|
|
153
172
|
|
|
154
|
-
# The NMI line is wired-OR between CIA 2
|
|
155
|
-
# takes an interrupt on its falling edge.
|
|
173
|
+
# The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
|
|
174
|
+
# key, and the CPU takes an interrupt on its falling edge.
|
|
156
175
|
def drive_nmi
|
|
157
|
-
nmi = @cia2.interrupted? || @cartridge_nmi
|
|
176
|
+
nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
|
|
177
|
+
@restore_pulse = false
|
|
158
178
|
@cpu.nmi = true if nmi && !@nmi_asserted
|
|
159
179
|
@nmi_asserted = nmi
|
|
160
180
|
end
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -24,10 +24,10 @@ 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
|
|
30
|
+
@memory = memory
|
|
31
31
|
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
32
32
|
reset_registers
|
|
33
33
|
|