badline 0.3.0 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +45 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +111 -61
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +275 -33
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +22 -10
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +2 -2
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge.rb +1 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +15 -8
- data/lib/badline/computer.rb +28 -13
- data/lib/badline/gui/application.rb +33 -21
- 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 +3 -0
- data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
- 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 +51 -51
- data/lib/badline/kernal_trap/file.rb +6 -9
- data/lib/badline/kernal_trap/save.rb +9 -4
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +42 -11
- 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/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +1 -1
- data/lib/badline/sid.rb +3 -1
- 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 +27 -4
- data/lib/badline/storage/host_directory.rb +2 -1
- data/lib/badline/storage.rb +15 -0
- data/lib/badline/time_of_day.rb +4 -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 +42 -7
- data/lib/badline/vic/registers.rb +3 -0
- 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 +93 -44
- data/lib/badline.rb +4 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +30 -20
- data/exe/badline +0 -78
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -5,70 +5,24 @@ require "io/wait"
|
|
|
5
5
|
|
|
6
6
|
module Badline
|
|
7
7
|
module Audio
|
|
8
|
-
# The terminal
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
class Console
|
|
12
|
-
KEYS = {
|
|
13
|
-
"n" => :next, "\e[C" => :next,
|
|
14
|
-
"p" => :previous, "\e[D" => :previous,
|
|
15
|
-
" " => :pause,
|
|
16
|
-
"q" => :quit
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
def initialize(input:, output:)
|
|
20
|
-
@input = input
|
|
21
|
-
@output = output
|
|
22
|
-
@closed = false
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Raw input and a hidden cursor for the length of the block.
|
|
26
|
-
def session(&)
|
|
27
|
-
@output.print "\e[?25l"
|
|
28
|
-
@input.tty? ? @input.raw(intr: true, &) : yield
|
|
29
|
-
ensure
|
|
30
|
-
@output.print "\e[?25h\r\n"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Waits up to `seconds` for keys and returns what they ask for.
|
|
34
|
-
def wait(seconds)
|
|
35
|
-
return idle(seconds) if @closed
|
|
36
|
-
return [] unless @input.wait_readable(seconds)
|
|
37
|
-
|
|
38
|
-
case (keys = @input.read_nonblock(64, exception: false))
|
|
39
|
-
when :wait_readable then []
|
|
40
|
-
when nil then close_input(seconds)
|
|
41
|
-
else keys.scan(/\e\[[A-D]|./m).filter_map { |key| KEYS[key] }
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def header(lines)
|
|
46
|
-
lines.each { |line| @output.print "#{line}\r\n" }
|
|
47
|
-
@output.print "n/→ next p/← previous space pause q quit\r\n"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def status(song:, songs:, elapsed:, length:, notes: [])
|
|
51
|
-
line = format("song %<song>d/%<songs>d %<elapsed>s / %<length>s",
|
|
52
|
-
song:, songs:, elapsed: clock(elapsed), length: clock(length))
|
|
53
|
-
@output.print "\r\e[K#{([line] + notes).join(' ')}"
|
|
54
|
-
@output.flush
|
|
55
|
-
end
|
|
56
|
-
|
|
8
|
+
# The terminal of `badline-ruby --headless`, put in raw mode with
|
|
9
|
+
# io/console.
|
|
10
|
+
class Console < Terminal
|
|
57
11
|
private
|
|
58
12
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
13
|
+
# Ctrl-C still raises Interrupt.
|
|
14
|
+
def raw!
|
|
15
|
+
mode = @input.console_mode
|
|
16
|
+
@input.raw!(intr: true)
|
|
17
|
+
mode
|
|
62
18
|
end
|
|
63
19
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
[]
|
|
20
|
+
def restore(mode)
|
|
21
|
+
@input.console_mode = mode
|
|
67
22
|
end
|
|
68
23
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
format("%<minutes>d:%<seconds>02d", minutes: whole / 60, seconds: whole % 60)
|
|
24
|
+
def readable?(seconds)
|
|
25
|
+
!@input.wait_readable(seconds).nil?
|
|
72
26
|
end
|
|
73
27
|
end
|
|
74
28
|
end
|
|
@@ -12,6 +12,9 @@ module Badline
|
|
|
12
12
|
# real time lets the queue run dry and the audio stutters; `on_underrun`
|
|
13
13
|
# hears about the first time.
|
|
14
14
|
class Playback
|
|
15
|
+
# What a sink raises when the audio device won't open.
|
|
16
|
+
class DeviceError < StandardError; end
|
|
17
|
+
|
|
15
18
|
AHEAD = 0.15
|
|
16
19
|
|
|
17
20
|
def initialize(sink, ahead: AHEAD, sleeper: ->(seconds) { sleep(seconds) }, on_underrun: nil)
|
|
@@ -4,98 +4,66 @@ module Badline
|
|
|
4
4
|
module Audio
|
|
5
5
|
# The host's audio device, fed through SDL's queue: mono signed 16-bit
|
|
6
6
|
# samples go in at the device's rate and SDL plays them out behind us.
|
|
7
|
-
#
|
|
8
|
-
# ruby-sdl2 only wraps SDL_mixer, which can't stream raw samples, so the
|
|
9
|
-
# queue API is called through Fiddle on the libSDL2 the extension has
|
|
10
|
-
# already loaded.
|
|
11
7
|
class SDLSink
|
|
12
|
-
class Error <
|
|
13
|
-
|
|
14
|
-
INIT_AUDIO = 0x10
|
|
15
|
-
|
|
16
|
-
AUDIO_S16SYS = [1].pack("s").getbyte(0) == 1 ? 0x8010 : 0x9010
|
|
17
|
-
|
|
18
|
-
ALLOW_FREQUENCY_CHANGE = 0x01
|
|
8
|
+
class Error < Playback::DeviceError; end
|
|
19
9
|
|
|
20
10
|
# int freq; Uint16 format; Uint8 channels, silence; Uint16 samples,
|
|
21
11
|
# padding; Uint32 size; then the callback and userdata pointers.
|
|
22
12
|
SPEC_LAYOUT = "lSCCSSLQQ"
|
|
23
13
|
|
|
24
|
-
FUNCTIONS = {
|
|
25
|
-
SDL_InitSubSystem: [%i[uint32], :int],
|
|
26
|
-
SDL_QuitSubSystem: [%i[uint32], :void],
|
|
27
|
-
SDL_GetError: [[], :string],
|
|
28
|
-
SDL_OpenAudioDevice: [%i[pointer int pointer pointer int], :uint32],
|
|
29
|
-
SDL_CloseAudioDevice: [%i[uint32], :void],
|
|
30
|
-
SDL_PauseAudioDevice: [%i[uint32 int], :void],
|
|
31
|
-
SDL_QueueAudio: [%i[uint32 pointer uint32], :int],
|
|
32
|
-
SDL_GetQueuedAudioSize: [%i[uint32], :uint32],
|
|
33
|
-
SDL_ClearQueuedAudio: [%i[uint32], :void]
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
14
|
attr_reader :rate
|
|
37
15
|
|
|
38
16
|
# With `exact_rate` false SDL may pick the device's own rate instead,
|
|
39
17
|
# which #rate then reports.
|
|
40
18
|
def initialize(rate:, exact_rate: false, buffer: 1024)
|
|
41
19
|
load_sdl
|
|
42
|
-
check(
|
|
43
|
-
@device = open_device(rate, exact_rate ? 0 :
|
|
20
|
+
check(SDL::InitSubSystem.call(SDL::INIT_AUDIO))
|
|
21
|
+
@device = open_device(rate, exact_rate ? 0 : SDL::AUDIO_ALLOW_FREQUENCY_CHANGE, buffer)
|
|
44
22
|
end
|
|
45
23
|
|
|
46
24
|
def queue(samples)
|
|
47
25
|
data = samples.pack("s*")
|
|
48
|
-
check(
|
|
26
|
+
check(SDL::QueueAudio.call(@device, data, data.bytesize))
|
|
49
27
|
end
|
|
50
28
|
|
|
51
|
-
def queued_seconds =
|
|
29
|
+
def queued_seconds = SDL::GetQueuedAudioSize.call(@device).fdiv(2 * rate)
|
|
52
30
|
|
|
53
|
-
def start =
|
|
31
|
+
def start = SDL::PauseAudioDevice.call(@device, 0)
|
|
54
32
|
|
|
55
|
-
def pause =
|
|
33
|
+
def pause = SDL::PauseAudioDevice.call(@device, 1)
|
|
56
34
|
|
|
57
|
-
def clear =
|
|
35
|
+
def clear = SDL::ClearQueuedAudio.call(@device)
|
|
58
36
|
|
|
59
37
|
def close
|
|
60
38
|
return unless @device
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
SDL::CloseAudioDevice.call(@device)
|
|
41
|
+
SDL::QuitSubSystem.call(SDL::INIT_AUDIO)
|
|
64
42
|
@device = nil
|
|
65
43
|
end
|
|
66
44
|
|
|
67
45
|
private
|
|
68
46
|
|
|
69
47
|
def open_device(rate, allowed_changes, buffer)
|
|
70
|
-
wanted = [rate, AUDIO_S16SYS, 1, 0, buffer, 0, 0, 0, 0].pack(SPEC_LAYOUT)
|
|
48
|
+
wanted = [rate, SDL::AUDIO_S16SYS, 1, 0, buffer, 0, 0, 0, 0].pack(SPEC_LAYOUT)
|
|
71
49
|
obtained = "\0".b * 32
|
|
72
|
-
device =
|
|
73
|
-
raise Error,
|
|
50
|
+
device = SDL::OpenAudioDevice.call(nil, 0, wanted, obtained, allowed_changes)
|
|
51
|
+
raise Error, SDL.error if device.zero?
|
|
74
52
|
|
|
75
53
|
@rate = obtained.unpack1("l")
|
|
76
54
|
device
|
|
77
55
|
end
|
|
78
56
|
|
|
79
|
-
def check(result)
|
|
80
|
-
raise Error, sdl(:SDL_GetError) if result.negative?
|
|
81
|
-
|
|
82
|
-
result
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def sdl(name, *) = @functions.fetch(name).call(*)
|
|
86
|
-
|
|
87
57
|
def load_sdl
|
|
88
|
-
require "
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[name, Fiddle::Function.new(Fiddle::Handle::DEFAULT[name.to_s],
|
|
92
|
-
arguments.map { |type| fiddle_type(type) }, fiddle_type(result))]
|
|
93
|
-
end
|
|
58
|
+
require "badline/sdl"
|
|
59
|
+
rescue SDL::Error => e
|
|
60
|
+
raise Error, e.message
|
|
94
61
|
end
|
|
95
62
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
63
|
+
def check(result)
|
|
64
|
+
raise Error, SDL.error if result.negative?
|
|
65
|
+
|
|
66
|
+
result
|
|
99
67
|
end
|
|
100
68
|
end
|
|
101
69
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Audio
|
|
5
|
+
# The terminal face of `--headless`: the tune's header, a status line
|
|
6
|
+
# redrawn in place, and single keypresses read without waiting for
|
|
7
|
+
# Return. Ctrl-C still interrupts. A subclass puts the terminal in raw
|
|
8
|
+
# mode with #raw! and #restore and waits for keys with #readable?:
|
|
9
|
+
# Console with io/console for badline-ruby, and Native::Console with
|
|
10
|
+
# stty and poll(2) for the native badline.
|
|
11
|
+
class Terminal
|
|
12
|
+
KEYS = {
|
|
13
|
+
"n" => :next, "\e[C" => :next,
|
|
14
|
+
"p" => :previous, "\e[D" => :previous,
|
|
15
|
+
" " => :pause,
|
|
16
|
+
"q" => :quit
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(input:, output:)
|
|
20
|
+
@input = input
|
|
21
|
+
@output = output
|
|
22
|
+
@closed = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Raw input and a hidden cursor for the length of the block.
|
|
26
|
+
def session
|
|
27
|
+
@output.print "\e[?25l"
|
|
28
|
+
mode = raw! if @input.tty?
|
|
29
|
+
yield
|
|
30
|
+
ensure
|
|
31
|
+
restore(mode) if mode
|
|
32
|
+
@output.print "\e[?25h\r\n"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Waits up to `seconds` for keys and returns what they ask for.
|
|
36
|
+
def wait(seconds)
|
|
37
|
+
return idle(seconds) if @closed
|
|
38
|
+
return [] unless readable?(seconds)
|
|
39
|
+
|
|
40
|
+
case (keys = @input.read_nonblock(64, exception: false))
|
|
41
|
+
when :wait_readable then []
|
|
42
|
+
when nil then close_input(seconds)
|
|
43
|
+
else keys.scan(/\e\[[A-D]|./m).filter_map { |key| KEYS[key] }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def header(lines)
|
|
48
|
+
lines.each { |line| @output.print "#{line}\r\n" }
|
|
49
|
+
@output.print "n/→ next p/← previous space pause q quit\r\n"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def status(song:, songs:, elapsed:, length:, notes: [])
|
|
53
|
+
line = format("song %<song>d/%<songs>d %<elapsed>s / %<length>s",
|
|
54
|
+
song:, songs:, elapsed: clock(elapsed), length: clock(length))
|
|
55
|
+
@output.print "\r\e[K#{([line] + notes).join(' ')}"
|
|
56
|
+
@output.flush
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# A subclass puts the terminal in raw mode and returns the mode to
|
|
62
|
+
# restore afterwards.
|
|
63
|
+
def raw! = nil
|
|
64
|
+
|
|
65
|
+
def restore(mode) = mode
|
|
66
|
+
|
|
67
|
+
# A subclass waits up to `seconds` for input and says whether any
|
|
68
|
+
# came.
|
|
69
|
+
def readable?(seconds)
|
|
70
|
+
sleep(seconds)
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_input(seconds)
|
|
75
|
+
@closed = true
|
|
76
|
+
idle(seconds)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def idle(seconds)
|
|
80
|
+
sleep(seconds)
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def clock(seconds)
|
|
85
|
+
whole = seconds.floor
|
|
86
|
+
format("%<minutes>d:%<seconds>02d", minutes: whole / 60, seconds: whole % 60)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/badline/audio.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "badline/audio/options"
|
|
4
3
|
require "badline/audio/pcm_writer"
|
|
5
4
|
require "badline/audio/wav"
|
|
6
5
|
require "badline/audio/aiff"
|
|
7
6
|
require "badline/audio/bare_player"
|
|
8
7
|
require "badline/audio/machine_player"
|
|
9
8
|
require "badline/audio/renderer"
|
|
10
|
-
require "badline/audio/sdl_sink"
|
|
11
9
|
require "badline/audio/playback"
|
|
10
|
+
require "badline/audio/sdl_sink"
|
|
12
11
|
require "badline/audio/stream"
|
|
12
|
+
require "badline/audio/terminal"
|
|
13
13
|
require "badline/audio/console"
|
|
14
14
|
require "badline/audio/jukebox"
|
|
15
15
|
require "badline/audio/cli"
|
|
@@ -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
|
data/lib/badline/cartridge.rb
CHANGED
|
@@ -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
|
|
|
@@ -43,9 +47,15 @@ module Badline
|
|
|
43
47
|
@tb.latch = value
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
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
|
+
|
|
47
53
|
addressable_at(start, length: 2**8)
|
|
48
54
|
|
|
55
|
+
@model = model
|
|
56
|
+
@clock_hz = region.clock_hz
|
|
57
|
+
@mains_hz = region.mains_hz
|
|
58
|
+
|
|
49
59
|
@peripheral = peripheral
|
|
50
60
|
@port_b4_handler = nil
|
|
51
61
|
@port_b4_high = true
|
|
@@ -63,8 +73,8 @@ module Badline
|
|
|
63
73
|
@port_b4_driven_high = true
|
|
64
74
|
@cnt_high = true
|
|
65
75
|
@cnt_rise = false
|
|
66
|
-
@tod = TimeOfDay.new
|
|
67
|
-
@icr = InterruptRegister.new
|
|
76
|
+
@tod = TimeOfDay.new(clock_hz: @clock_hz, mains_hz: @mains_hz)
|
|
77
|
+
@icr = InterruptRegister.new(@model)
|
|
68
78
|
@icr_status = @icr.status
|
|
69
79
|
@control_a = ControlRegister.new(%i[start output out_mode run_mode load
|
|
70
80
|
in_mode serial_mode clock_frequency])
|
|
@@ -223,10 +233,7 @@ module Badline
|
|
|
223
233
|
else underflowed && @cnt_high
|
|
224
234
|
end
|
|
225
235
|
)
|
|
226
|
-
if underflowed
|
|
227
|
-
interrupt_status.timer_a = true
|
|
228
|
-
interrupt! if interrupt_control.timer_a?
|
|
229
|
-
end
|
|
236
|
+
@icr.flag(:timer_a) if underflowed
|
|
230
237
|
@icr.timer_b_underflow! if @tb.underflowed
|
|
231
238
|
underflowed
|
|
232
239
|
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,6 +47,7 @@ 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 = []
|
|
@@ -94,10 +101,10 @@ module Badline
|
|
|
94
101
|
end
|
|
95
102
|
|
|
96
103
|
# The RES line reaches the CPU and its port, both CIAs, the SID, the
|
|
97
|
-
# cartridge port and, through the serial bus's RESET
|
|
98
|
-
# The VIC has no reset pin.
|
|
104
|
+
# cartridge port, a RAM expansion and, through the serial bus's RESET
|
|
105
|
+
# line, the drive. The VIC has no reset pin.
|
|
99
106
|
def reset!
|
|
100
|
-
address_bus.
|
|
107
|
+
address_bus.reset!
|
|
101
108
|
@cia1.reset!
|
|
102
109
|
@cia2.reset!
|
|
103
110
|
@sid.reset!
|
|
@@ -123,15 +130,22 @@ module Badline
|
|
|
123
130
|
address_bus.cartridge&.release_button
|
|
124
131
|
end
|
|
125
132
|
|
|
126
|
-
#
|
|
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.
|
|
127
142
|
def mount(storage)
|
|
128
|
-
@
|
|
129
|
-
|
|
143
|
+
return @drive.insert(storage) if @drive
|
|
144
|
+
|
|
145
|
+
@drive = KernalTrap::Drive.new(storage)
|
|
130
146
|
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
|
|
131
147
|
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
132
148
|
KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
|
|
133
|
-
return unless storage.respond_to?(:write_file)
|
|
134
|
-
|
|
135
149
|
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
|
|
136
150
|
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
137
151
|
end
|
|
@@ -156,10 +170,11 @@ module Badline
|
|
|
156
170
|
|
|
157
171
|
private
|
|
158
172
|
|
|
159
|
-
# The NMI line is wired-OR between CIA 2
|
|
160
|
-
# 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.
|
|
161
175
|
def drive_nmi
|
|
162
|
-
nmi = @cia2.interrupted? || @cartridge_nmi
|
|
176
|
+
nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
|
|
177
|
+
@restore_pulse = false
|
|
163
178
|
@cpu.nmi = true if nmi && !@nmi_asserted
|
|
164
179
|
@nmi_asserted = nmi
|
|
165
180
|
end
|