badline 0.9.0 → 0.10.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 +29 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +13 -0
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus.rb +9 -88
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +47 -7
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +44 -29
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +3 -0
- data/lib/badline/datasette.rb +39 -1
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +62 -64
- data/lib/badline/frontend/boot.rb +33 -8
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_media.rb +49 -24
- data/lib/badline/frontend/menu_pages.rb +23 -40
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +30 -33
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +1 -1
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/sound.rb +11 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +2 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +16 -27
- data/lib/badline/kernal_trap/routine.rb +11 -27
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +21 -19
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +32 -18
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +11 -1
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +27 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/state.rb +4 -3
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +3 -0
- data/lib/badline/storage/tap.rb +38 -4
- data/lib/badline/storage/unavailable.rb +2 -0
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +34 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- metadata +28 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -16,7 +16,6 @@ module Badline
|
|
|
16
16
|
class PauseMenu
|
|
17
17
|
SECTIONS = ["SNAPSHOTS", "DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
|
|
18
18
|
PAGES = %i[snapshots drive datasette expansion ports sound power].freeze
|
|
19
|
-
DISKS = %w[.d64 .d71 .d81 .g64 .t64].freeze
|
|
20
19
|
|
|
21
20
|
PANEL = 0x1d2230
|
|
22
21
|
EDGE = 0x3a4256
|
|
@@ -25,11 +24,10 @@ module Badline
|
|
|
25
24
|
DIM = 0x6b7489
|
|
26
25
|
FILL = 0x343c50
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
BODY = 154
|
|
27
|
+
MARGIN = 16
|
|
28
|
+
WIDTH = 352
|
|
29
|
+
HEIGHT = 240
|
|
30
|
+
BODY = 138
|
|
33
31
|
|
|
34
32
|
ESCAPE = 41
|
|
35
33
|
RETURN = 40
|
|
@@ -48,20 +46,15 @@ module Badline
|
|
|
48
46
|
@dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options, snapshots)
|
|
49
47
|
@open = false
|
|
50
48
|
@section = 0
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# The path of the disk the command line puts in device 8, a list's
|
|
54
|
-
# first, or an empty one for other media.
|
|
55
|
-
def self.disk_path(path)
|
|
56
|
-
return "" if path.empty?
|
|
57
|
-
return path if File.directory?(path)
|
|
58
|
-
return Media::DiskList.disks(path).first.to_s if Media::DiskList.list?(path)
|
|
59
|
-
|
|
60
|
-
DISKS.include?(File.extname(path).downcase) ? path : ""
|
|
49
|
+
@left = MARGIN
|
|
50
|
+
@top = MARGIN
|
|
61
51
|
end
|
|
62
52
|
|
|
63
53
|
def open? = @open
|
|
64
54
|
|
|
55
|
+
# The index in PAGES of the page #show opens on.
|
|
56
|
+
attr_writer :section
|
|
57
|
+
|
|
65
58
|
# The machine Quick open started, which the app runs in place of the
|
|
66
59
|
# one before once #frame returns :swap.
|
|
67
60
|
def computer = @dialogs.computer
|
|
@@ -116,10 +109,10 @@ module Badline
|
|
|
116
109
|
end
|
|
117
110
|
|
|
118
111
|
# Handles the events that came in, draws the menu over the frozen
|
|
119
|
-
# picture, dimmed,
|
|
120
|
-
# any, leaving the events after one for the
|
|
121
|
-
# still reach the machine, so no key stays held
|
|
122
|
-
# still.
|
|
112
|
+
# picture, dimmed, for the app to present, and returns the action the
|
|
113
|
+
# app has to take on, if any, leaving the events after one for the
|
|
114
|
+
# machine. Key releases still reach the machine, so no key stays held
|
|
115
|
+
# down while it stands still.
|
|
123
116
|
def frame(renderer, texture, controls)
|
|
124
117
|
action = nil
|
|
125
118
|
action = event(SDL.event_type(SDL.event), controls) while action.nil? && SDL.SDL_PollEvent(SDL.event) != 0
|
|
@@ -129,26 +122,30 @@ module Badline
|
|
|
129
122
|
SDL.SDL_SetTextureColorMod(texture, 255, 255, 255)
|
|
130
123
|
draw
|
|
131
124
|
SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
|
|
132
|
-
SDL.SDL_RenderPresent(renderer)
|
|
133
|
-
SDL.SDL_Delay(10)
|
|
134
125
|
action
|
|
135
126
|
end
|
|
136
127
|
|
|
128
|
+
# Centres the menu on a screen `width` by `height`.
|
|
129
|
+
def center(width, height)
|
|
130
|
+
@left = (width - WIDTH) / 2
|
|
131
|
+
@top = (height - HEIGHT) / 2
|
|
132
|
+
end
|
|
133
|
+
|
|
137
134
|
def draw
|
|
138
135
|
painter = @painter
|
|
139
136
|
@buttons.forget
|
|
140
|
-
painter.box(
|
|
141
|
-
painter.box(
|
|
142
|
-
painter.text(
|
|
137
|
+
painter.box(@left, @top, WIDTH, HEIGHT, PANEL)
|
|
138
|
+
painter.box(@left, @top + 24, WIDTH, 1, EDGE)
|
|
139
|
+
painter.text(@left + 8, @top + 9, "PAUSED", BRIGHT)
|
|
143
140
|
resume = "RESUME"
|
|
144
|
-
@buttons.plain(
|
|
145
|
-
return @dialogs.draw(
|
|
141
|
+
@buttons.plain(@left + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), @top + 7, resume, :resume)
|
|
142
|
+
return @dialogs.draw(@left + 8, @top + 34, WIDTH - 16) if @dialogs.open?
|
|
146
143
|
|
|
147
|
-
painter.box(BODY - 8,
|
|
144
|
+
painter.box(@left + BODY - 8, @top + 32, 1, HEIGHT - 56, EDGE)
|
|
148
145
|
draw_sections
|
|
149
|
-
@buttons.plain(
|
|
150
|
-
painter.text(
|
|
151
|
-
@pages.draw(PAGES[@section], BODY,
|
|
146
|
+
@buttons.plain(@left + 6, @top + HEIGHT - 28, "QUICK OPEN...", :quick_open)
|
|
147
|
+
painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", DIM)
|
|
148
|
+
@pages.draw(PAGES[@section], @left + BODY, @top + 34)
|
|
152
149
|
end
|
|
153
150
|
|
|
154
151
|
private
|
|
@@ -253,9 +250,9 @@ module Badline
|
|
|
253
250
|
end
|
|
254
251
|
|
|
255
252
|
def draw_sections
|
|
256
|
-
top =
|
|
253
|
+
top = @top + 32
|
|
257
254
|
SECTIONS.each_with_index do |name, index|
|
|
258
|
-
@buttons.plain(
|
|
255
|
+
@buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
|
|
259
256
|
top += Buttons::HEIGHT + 2
|
|
260
257
|
end
|
|
261
258
|
end
|
|
@@ -2,60 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The visible part of the
|
|
6
|
-
#
|
|
7
|
-
# Array of Integers to C as 64-bit
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
# The visible part of the video chip's display, the crop of the
|
|
6
|
+
# machine's timing, repacked for an SDL texture in XRGB8888 in the
|
|
7
|
+
# chip's palette. Spinel hands an Array of Integers to C as 64-bit
|
|
8
|
+
# words, as badline/ffi does on CRuby, so each word carries two
|
|
9
|
+
# neighbouring pixels, the left one in the low half.
|
|
10
|
+
#
|
|
11
|
+
# The texture is as wide as the crop and at least as high as PAL's
|
|
12
|
+
# C64 crop, 272 lines, so a C64 shows in the same window whatever its
|
|
13
|
+
# region: a crop with fewer lines sits in the middle of the texture,
|
|
14
|
+
# between black bands. The VIC-20's 284 by 284 crop fills its own.
|
|
11
15
|
class Screen
|
|
12
|
-
|
|
13
|
-
HEIGHT = Region::PAL.crop[3]
|
|
14
|
-
ROW_BYTES = WIDTH * 4
|
|
15
|
-
ROW_WORDS = WIDTH / 2
|
|
16
|
+
MIN_HEIGHT = Region::PAL.crop[3]
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
0x000000, 0xffffff, 0x924a40, 0x84c5cc,
|
|
19
|
-
0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
|
|
20
|
-
0x675200, 0xc33d00, 0xc18178, 0x606060,
|
|
21
|
-
0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
|
|
22
|
-
].freeze
|
|
18
|
+
attr_reader :pixels, :width, :height
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
attr_reader :pixels
|
|
27
|
-
|
|
28
|
-
def initialize(vic)
|
|
29
|
-
@vic = vic
|
|
30
|
-
crop = vic.region.crop
|
|
20
|
+
def initialize(video, crop)
|
|
21
|
+
@video = video
|
|
31
22
|
@col_offset = crop[0]
|
|
32
23
|
@row_offset = crop[1]
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
24
|
+
@lines = crop[3]
|
|
25
|
+
@width = crop[2]
|
|
26
|
+
@stride = video.width.to_i
|
|
27
|
+
@height = [@lines, MIN_HEIGHT].max
|
|
28
|
+
@row_words = @width / 2
|
|
29
|
+
@band = (@height - @lines) / 2
|
|
30
|
+
palette = video.palette
|
|
31
|
+
@pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
|
|
32
|
+
@pixels = Array.new(@height * @row_words, 0)
|
|
36
33
|
end
|
|
37
34
|
|
|
38
|
-
#
|
|
35
|
+
# The bytes of one row of the texture.
|
|
36
|
+
def row_bytes = @width * 4
|
|
37
|
+
|
|
38
|
+
# Repacks only the lines the chip has changed since the last frame.
|
|
39
39
|
def update
|
|
40
|
-
|
|
40
|
+
video = @video
|
|
41
|
+
dirty = video.dirty_lines
|
|
42
|
+
display = video.display
|
|
41
43
|
row = 0
|
|
42
|
-
while row < @
|
|
43
|
-
pack_row(row) if dirty[row + @row_offset]
|
|
44
|
+
while row < @lines
|
|
45
|
+
pack_row(display, row) if dirty[row + @row_offset]
|
|
44
46
|
row += 1
|
|
45
47
|
end
|
|
46
|
-
|
|
48
|
+
video.clear_dirty_lines!
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
private
|
|
50
52
|
|
|
51
|
-
def pack_row(row)
|
|
52
|
-
display = @vic.display
|
|
53
|
+
def pack_row(display, row)
|
|
53
54
|
pixels = @pixels
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
pairs = @pairs
|
|
56
|
+
from = ((row + @row_offset) * @stride) + @col_offset
|
|
57
|
+
to = (row + @band) * @row_words
|
|
58
|
+
last = to + @row_words
|
|
57
59
|
while to < last
|
|
58
|
-
pixels[to] =
|
|
60
|
+
pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
|
|
59
61
|
from += 2
|
|
60
62
|
to += 1
|
|
61
63
|
end
|
|
@@ -8,7 +8,7 @@ module Badline
|
|
|
8
8
|
# a tune on more than one gets a row of buttons above the voices that
|
|
9
9
|
# pick it, which makes the view PICKER taller.
|
|
10
10
|
class SIDView
|
|
11
|
-
COLORS =
|
|
11
|
+
COLORS = VIC::PALETTE
|
|
12
12
|
TEXT = COLORS[14]
|
|
13
13
|
BRIGHT = COLORS[1]
|
|
14
14
|
DIM = COLORS[11]
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The
|
|
5
|
+
# The machine's sound source, the C64's SID or the VIC-20's VIC, on
|
|
6
|
+
# SDL's audio queue, as mono signed 16-bit samples. Both take
|
|
7
|
+
# #record(rate:, clock_hz:) and #drain_samples.
|
|
6
8
|
# Once the device plays, it is the clock: #wait holds each frame until
|
|
7
9
|
# the queue is down to `AHEAD`, so the machine runs at the device's pace.
|
|
8
10
|
#
|
|
@@ -13,7 +15,7 @@ module Badline
|
|
|
13
15
|
# only happens unpaced.
|
|
14
16
|
#
|
|
15
17
|
# Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
|
|
16
|
-
# The
|
|
18
|
+
# The chip's cycles are converted at `clock_hz`, the machine's clock, so
|
|
17
19
|
# an NTSC or Drean machine plays at its own pitch.
|
|
18
20
|
class Sound
|
|
19
21
|
RATE = 44_100
|
|
@@ -22,8 +24,8 @@ module Badline
|
|
|
22
24
|
|
|
23
25
|
attr_reader :rate, :underruns, :dropped, :queued, :low, :high
|
|
24
26
|
|
|
25
|
-
def initialize(
|
|
26
|
-
@
|
|
27
|
+
def initialize(source, clock_hz, wanted, verbose)
|
|
28
|
+
@source = source
|
|
27
29
|
@clock_hz = clock_hz
|
|
28
30
|
@verbose = verbose
|
|
29
31
|
@device = 0
|
|
@@ -40,9 +42,9 @@ module Badline
|
|
|
40
42
|
|
|
41
43
|
def on? = @device != 0
|
|
42
44
|
|
|
43
|
-
# Plays another machine's
|
|
44
|
-
def switch(
|
|
45
|
-
@
|
|
45
|
+
# Plays another machine's sound from here on, at that machine's clock.
|
|
46
|
+
def switch(source, clock_hz)
|
|
47
|
+
@source = source
|
|
46
48
|
@clock_hz = clock_hz
|
|
47
49
|
record if on?
|
|
48
50
|
end
|
|
@@ -54,7 +56,7 @@ module Badline
|
|
|
54
56
|
def feed
|
|
55
57
|
return unless on?
|
|
56
58
|
|
|
57
|
-
samples = @
|
|
59
|
+
samples = @source.drain_samples
|
|
58
60
|
return if @muted || samples.empty?
|
|
59
61
|
|
|
60
62
|
level = queued_seconds
|
|
@@ -110,7 +112,7 @@ module Badline
|
|
|
110
112
|
puts "Sound at #{@rate} Hz" if @verbose
|
|
111
113
|
end
|
|
112
114
|
|
|
113
|
-
def record = @
|
|
115
|
+
def record = @source.record(rate: @rate, clock_hz: @clock_hz)
|
|
114
116
|
|
|
115
117
|
def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
|
|
116
118
|
|
|
@@ -21,13 +21,20 @@ module Badline
|
|
|
21
21
|
@failed = false
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# What's wrong with the events, the first key
|
|
25
|
-
# or an empty string.
|
|
24
|
+
# What's wrong with the events, the first key or menu page that
|
|
25
|
+
# names nothing, or an empty string.
|
|
26
26
|
def self.error(events)
|
|
27
27
|
event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument) }
|
|
28
|
-
|
|
28
|
+
return "no such key: #{event.argument}" unless event.nil?
|
|
29
|
+
|
|
30
|
+
event = events.find { |candidate| candidate.action == "menu" && !page?(candidate.argument) }
|
|
31
|
+
event.nil? ? "" : "no such menu page: #{event.argument}"
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# Whether the pause menu has a page of the name, such as drive or
|
|
35
|
+
# sound. An empty one is its first.
|
|
36
|
+
def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
|
|
37
|
+
|
|
31
38
|
def self.key?(name) = name == "restore" || !keyboard_key(name).nil? || !joystick_direction(name).nil?
|
|
32
39
|
|
|
33
40
|
# The Keyboard's key for a name such as space, return or a.
|
|
@@ -67,6 +74,22 @@ module Badline
|
|
|
67
74
|
# events do.
|
|
68
75
|
def press_freeze(computer, frame) = freeze(computer, Options::Event.new(frame, "freeze", ""))
|
|
69
76
|
|
|
77
|
+
# Whether the pause menu opens at the frame, or turns to another page.
|
|
78
|
+
def menu?(frame) = @events.any? { |event| event.frame == frame && event.action == "menu" }
|
|
79
|
+
|
|
80
|
+
# The index in PauseMenu::PAGES of the page the frame's menu event
|
|
81
|
+
# names, 0 for the first.
|
|
82
|
+
def menu_section(frame)
|
|
83
|
+
page = ""
|
|
84
|
+
@events.each { |event| page = event.argument if event.frame == frame && event.action == "menu" }
|
|
85
|
+
section = 0
|
|
86
|
+
PauseMenu::PAGES.each_with_index { |name, index| section = index if name.to_s == page }
|
|
87
|
+
section
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether the pause menu closes at the frame.
|
|
91
|
+
def resume?(frame) = @events.any? { |event| event.frame == frame && event.action == "resume" }
|
|
92
|
+
|
|
70
93
|
# Whether the run ends at the frame: at a quit, or once an event failed.
|
|
71
94
|
def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
|
|
72
95
|
|
|
@@ -98,7 +121,7 @@ module Badline
|
|
|
98
121
|
name = event.argument
|
|
99
122
|
if name == "restore"
|
|
100
123
|
computer.press_restore
|
|
101
|
-
return
|
|
124
|
+
return hold(event)
|
|
102
125
|
end
|
|
103
126
|
|
|
104
127
|
direction = Timeline.joystick_direction(name)
|
|
@@ -110,7 +133,10 @@ module Badline
|
|
|
110
133
|
hold(event)
|
|
111
134
|
end
|
|
112
135
|
|
|
136
|
+
# The VIC-20 has no cartridge with a button yet.
|
|
113
137
|
def freeze(computer, event)
|
|
138
|
+
return if computer.family == :vic20
|
|
139
|
+
|
|
114
140
|
computer.press_cartridge_button
|
|
115
141
|
hold(event)
|
|
116
142
|
end
|
|
@@ -123,6 +149,8 @@ module Badline
|
|
|
123
149
|
return computer.release_cartridge_button if event.action == "freeze"
|
|
124
150
|
|
|
125
151
|
name = event.argument
|
|
152
|
+
return computer.release_restore if name == "restore"
|
|
153
|
+
|
|
126
154
|
direction = Timeline.joystick_direction(name)
|
|
127
155
|
if direction.nil?
|
|
128
156
|
computer.keyboard.release(Timeline.keyboard_key(name))
|
|
@@ -141,7 +169,7 @@ module Badline
|
|
|
141
169
|
end
|
|
142
170
|
|
|
143
171
|
def eject(computer, what)
|
|
144
|
-
return puts "No #{what} to eject" unless
|
|
172
|
+
return puts "No #{what} to eject" unless ejectable?(computer, what)
|
|
145
173
|
|
|
146
174
|
if what == "disk"
|
|
147
175
|
eject_disk(computer)
|
|
@@ -154,6 +182,9 @@ module Badline
|
|
|
154
182
|
puts "Ejected the #{what}"
|
|
155
183
|
end
|
|
156
184
|
|
|
185
|
+
# Whether there's one in to eject. A VIC-20 has no cartridge to take out yet.
|
|
186
|
+
def ejectable?(computer, what) = (computer.family == :c64 || what != "cartridge") && inserted?(computer, what)
|
|
187
|
+
|
|
157
188
|
def inserted?(computer, what)
|
|
158
189
|
return !computer.datasette.tape.nil? if what == "tape"
|
|
159
190
|
return !computer.address_bus.cartridge.nil? if what == "cartridge"
|
data/lib/badline/frontend.rb
CHANGED
|
@@ -15,6 +15,7 @@ require "badline/frontend/frame_report"
|
|
|
15
15
|
require "badline/frontend/controls"
|
|
16
16
|
require "badline/frontend/pad_port"
|
|
17
17
|
require "badline/frontend/gamepads"
|
|
18
|
+
require "badline/frontend/machine_window"
|
|
18
19
|
require "badline/frontend/app"
|
|
19
20
|
require "badline/frontend/audio_sink"
|
|
20
21
|
require "badline/frontend/painter"
|
|
@@ -37,4 +38,5 @@ require "badline/frontend/confirmation"
|
|
|
37
38
|
require "badline/frontend/name_field"
|
|
38
39
|
require "badline/frontend/menu_dialogs"
|
|
39
40
|
require "badline/frontend/menu_media"
|
|
41
|
+
require "badline/frontend/menu_ports"
|
|
40
42
|
require "badline/frontend/menu_pages"
|
data/lib/badline/iec_bus.rb
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
# The serial bus between the C64 and its drives: ATN, CLK and DATA, each
|
|
5
5
|
# an open-collector line with a pull-up. A line is low while any device
|
|
6
|
-
# pulls it (wired-AND), and high otherwise.
|
|
7
|
-
#
|
|
6
|
+
# pulls it (wired-AND), and high otherwise. Every read works the levels
|
|
7
|
+
# out from what each side drives now.
|
|
8
8
|
#
|
|
9
9
|
# The C64 drives the lines from CIA 2's port A through 7406 inverters, so
|
|
10
10
|
# an output bit of 1 pulls its line low: PA3 ATN, PA4 CLK and PA5 DATA.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# The machine pushes those bits into the bus (host_lines=), which holds
|
|
12
|
+
# them. PA6 and PA7 read CLK and DATA directly, 1 for a released line.
|
|
13
|
+
# The bus stands in as CIA 2's peripheral for those two inputs.
|
|
13
14
|
#
|
|
14
15
|
# A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
|
|
15
16
|
# also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
|
|
@@ -33,30 +34,21 @@ module Badline
|
|
|
33
34
|
CLK = 0x02
|
|
34
35
|
DATA = 0x04
|
|
35
36
|
|
|
36
|
-
attr_reader :
|
|
37
|
+
attr_reader :drives, :host_lines
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
def initialize(host: nil)
|
|
40
|
-
@host = host
|
|
39
|
+
def initialize
|
|
41
40
|
@drives = []
|
|
42
|
-
@
|
|
41
|
+
@host_lines = 0
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@
|
|
50
|
-
@
|
|
44
|
+
# The machine pushes the lines it pulls, as port A bits, whenever they
|
|
45
|
+
# may have moved: its output bits, with input bits floating high as the
|
|
46
|
+
# 7406 inputs see them. A bus with only drives on it keeps 0.
|
|
47
|
+
def host_lines=(lines)
|
|
48
|
+
@host_lines = lines
|
|
49
|
+
@drives.each(&:host_written!)
|
|
51
50
|
end
|
|
52
51
|
|
|
53
|
-
# Whether the host says when it may have moved a line (see plug_in!).
|
|
54
|
-
# Without, a drive asleep checks ATN every cycle.
|
|
55
|
-
def notifies? = @notifies
|
|
56
|
-
|
|
57
|
-
# CIA 2 wrote its port A, which may have moved a line.
|
|
58
|
-
def host_written = @drives.each(&:host_written!)
|
|
59
|
-
|
|
60
52
|
def attach(drive)
|
|
61
53
|
@drives << drive unless @drives.include?(drive)
|
|
62
54
|
end
|
|
@@ -68,7 +60,7 @@ module Badline
|
|
|
68
60
|
# One pass over everything on the bus, for a reader that wants more
|
|
69
61
|
# than one line. +host+ is the C64's port A lines as the reader sees
|
|
70
62
|
# them, which a drive takes a cycle late (see Drive1541::SerialPort).
|
|
71
|
-
def low_lines(host = host_lines)
|
|
63
|
+
def low_lines(host = @host_lines)
|
|
72
64
|
atn = host.anybits?(HOST_ATN_OUT)
|
|
73
65
|
low = atn ? ATN : 0
|
|
74
66
|
low |= CLK if host.anybits?(HOST_CLK_OUT)
|
|
@@ -83,7 +75,7 @@ module Badline
|
|
|
83
75
|
low
|
|
84
76
|
end
|
|
85
77
|
|
|
86
|
-
def atn_low?(host = host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
78
|
+
def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
87
79
|
|
|
88
80
|
def clk_low? = low_lines.anybits?(CLK)
|
|
89
81
|
|
|
@@ -101,9 +93,5 @@ module Badline
|
|
|
101
93
|
|
|
102
94
|
# Port B goes to the user port, not the serial bus.
|
|
103
95
|
def read_b(_port_a, _port_b) = 0xff
|
|
104
|
-
|
|
105
|
-
# What the C64 pulls, as port A bits: its output bits, with input bits
|
|
106
|
-
# floating high as the 7406 inputs see them.
|
|
107
|
-
def host_lines = @host ? @host.port_a_lines : 0
|
|
108
96
|
end
|
|
109
97
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# Where a machine's KERNAL keeps the routines the traps stand in for,
|
|
6
|
+
# and the ROM code they hand over to, with the steps that depend on the
|
|
7
|
+
# machine around it: whether the KERNAL is mapped in, the timer that
|
|
8
|
+
# times each serial byte and the release of the serial lines.
|
|
9
|
+
#
|
|
10
|
+
# +load+ and +save+ are the default ILOAD and ISAVE vector targets.
|
|
11
|
+
# LOAD hands over to the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
12
|
+
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
13
|
+
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the FILE NOT FOUND
|
|
14
|
+
# and MISSING FILE NAME error exits. SAVE hands over to the SAVING
|
|
15
|
+
# message, the two line releases that end UNLISTEN (clock, then data,
|
|
16
|
+
# leaving A as read from the port), the successful return (CLC, RTS)
|
|
17
|
+
# and the MISSING FILE NAME exit. The rest are the serial bus
|
|
18
|
+
# primitives' entry points.
|
|
19
|
+
#
|
|
20
|
+
# +banked+ says whether the 6510 port can bank the KERNAL out, and so
|
|
21
|
+
# whether RAM lies under every ROM and the I/O. Each
|
|
22
|
+
# serial byte starts a one-shot timer by writing the high byte of its
|
|
23
|
+
# count to +serial_timer+, and the control value TIMER_ONE_SHOT_START
|
|
24
|
+
# to +serial_timer_start+ where the timer has a control register to
|
|
25
|
+
# start it (a CIA's), or nil where the write to the high byte starts it
|
|
26
|
+
# (a VIA's timer 2). The ROM lets go of each serial line by reading a
|
|
27
|
+
# port, masking the line's bit off and writing it back: ATN at
|
|
28
|
+
# +atn_port+ with +atn_mask+, then the clock and the data line.
|
|
29
|
+
Layout = Data.define(:load, :save, :searching_message, :loading_message, :load_byte_loop, :load_done,
|
|
30
|
+
:file_not_found_exit, :missing_file_name_exit, :saving_message, :clock_release,
|
|
31
|
+
:data_release, :save_done, :talk, :listen, :second, :tksa, :ciout, :untalk,
|
|
32
|
+
:unlisten, :acptr, :banked, :serial_timer, :serial_timer_start, :atn_port, :atn_mask,
|
|
33
|
+
:clock_port, :clock_mask, :data_port, :data_mask) do
|
|
34
|
+
# Whether the CPU reads the KERNAL ROM at the trapped addresses: on a
|
|
35
|
+
# banked machine, the 6510 port's banking bits select it.
|
|
36
|
+
def kernal_mapped?(bus)
|
|
37
|
+
!banked || bus.io_port.kernal?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Stores a loaded file's bytes from +address+ on: on a banked machine
|
|
41
|
+
# into the RAM under whatever is banked in, and otherwise where the
|
|
42
|
+
# ROM's byte loop would store them, through the bus.
|
|
43
|
+
def store_file(bus, address, bytes)
|
|
44
|
+
return bus.ram.write(address, bytes) if banked
|
|
45
|
+
|
|
46
|
+
bytes.each_with_index { |byte, i| bus.poke(address + i, byte) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Starts the timer the ROM times a serial byte with, its count's high
|
|
50
|
+
# byte +timer_high+.
|
|
51
|
+
def time_serial_byte(bus, timer_high)
|
|
52
|
+
bus.poke(serial_timer, timer_high)
|
|
53
|
+
start = serial_timer_start
|
|
54
|
+
bus.poke(start, TIMER_ONE_SHOT_START) if start
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The ROM's release of the serial bus: ATN, then the clock, then the
|
|
58
|
+
# data line, each a read of its port, a mask and a write back, so the
|
|
59
|
+
# input bits written back follow the lines as each one is let go.
|
|
60
|
+
# Returns the last value written, which the ROM leaves in A.
|
|
61
|
+
def release_serial_lines(bus)
|
|
62
|
+
release_line(bus, atn_port, atn_mask)
|
|
63
|
+
release_line(bus, clock_port, clock_mask)
|
|
64
|
+
release_line(bus, data_port, data_mask)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def release_line(bus, port, mask)
|
|
70
|
+
value = bus.peek(port) & mask
|
|
71
|
+
bus.poke(port, value)
|
|
72
|
+
value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A CIA's control value that starts its timer: force load, one-shot,
|
|
77
|
+
# start.
|
|
78
|
+
TIMER_ONE_SHOT_START = 0x19
|
|
79
|
+
|
|
80
|
+
# The C64 KERNAL, revision 3. The SX-64's, 251104-04, differs from it
|
|
81
|
+
# only in bytes none of these addresses reach. It times serial bytes
|
|
82
|
+
# with CIA 1's timer B, and drives the lines from CIA 2's port A
|
|
83
|
+
# ($EE03: ATN $EDBE, clock $EE85, data $EE97).
|
|
84
|
+
C64_LAYOUT = Layout.new(
|
|
85
|
+
load: 0xf4a5, save: 0xf5ed,
|
|
86
|
+
searching_message: 0xf5af, loading_message: 0xf5d2, load_byte_loop: 0xf4f3, load_done: 0xf5a9,
|
|
87
|
+
file_not_found_exit: 0xf704, missing_file_name_exit: 0xf710,
|
|
88
|
+
saving_message: 0xf68f, clock_release: 0xee85, data_release: 0xee97, save_done: 0xf657,
|
|
89
|
+
talk: 0xed09, listen: 0xed0c, second: 0xedb9, tksa: 0xedc7,
|
|
90
|
+
ciout: 0xeddd, untalk: 0xedef, unlisten: 0xedfe, acptr: 0xee13,
|
|
91
|
+
banked: true, serial_timer: 0xdc07, serial_timer_start: 0xdc0f,
|
|
92
|
+
atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# The VIC-20 KERNAL, 901486-07 (PAL). The NTSC one, 901486-06, differs
|
|
96
|
+
# from it only at $E475, $EDE4-$EDE5, $FE3F, $FE44 and $FF5C-$FF70,
|
|
97
|
+
# none of them here. Nothing banks it out. It times serial bytes with
|
|
98
|
+
# VIA 2's timer 2 ($9129), drives ATN from VIA 1's PA7 ($911F) and the
|
|
99
|
+
# clock and data lines from VIA 2's CA2 and CB2 ($912C), and UNLISTEN
|
|
100
|
+
# lets them go at $EF09: ATN $EEC5, clock $EF84, data $E4A0.
|
|
101
|
+
VIC20_LAYOUT = Layout.new(
|
|
102
|
+
load: 0xf549, save: 0xf685,
|
|
103
|
+
searching_message: 0xf647, loading_message: 0xf66a, load_byte_loop: 0xf58a, load_done: 0xf641,
|
|
104
|
+
file_not_found_exit: 0xf787, missing_file_name_exit: 0xf793,
|
|
105
|
+
saving_message: 0xf728, clock_release: 0xef84, data_release: 0xe4a0, save_done: 0xf6ef,
|
|
106
|
+
talk: 0xee14, listen: 0xee17, second: 0xeec0, tksa: 0xeece,
|
|
107
|
+
ciout: 0xeee4, untalk: 0xeef6, unlisten: 0xef04, acptr: 0xef19,
|
|
108
|
+
banked: false, serial_timer: 0x9129, serial_timer_start: nil,
|
|
109
|
+
atn_port: 0x911f, atn_mask: 0x7f, clock_port: 0x912c, clock_mask: 0xfd, data_port: 0x912c, data_mask: 0xdf
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|