badline 0.10.1 → 0.11.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 +15 -0
- data/CLAUDE.md +5 -3
- data/README.md +59 -4
- data/doc/pinned-behaviour.md +75 -0
- data/doc/snapshots.md +15 -0
- data/lib/badline/address_bus/pla.rb +3 -16
- data/lib/badline/address_bus.rb +14 -0
- data/lib/badline/c128/banks.rb +35 -0
- data/lib/badline/c128/bus.rb +347 -0
- data/lib/badline/c128/bus_state.rb +53 -0
- data/lib/badline/c128/color_lines.rb +39 -0
- data/lib/badline/c128/cpu.rb +10 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +182 -0
- data/lib/badline/c128/mmu_pages.rb +105 -0
- data/lib/badline/c128/model.rb +39 -0
- data/lib/badline/c128/modes.rb +48 -0
- data/lib/badline/c128/saved_state.rb +120 -0
- data/lib/badline/c128/vdc/memory.rb +54 -0
- data/lib/badline/c128/vdc/painter.rb +143 -0
- data/lib/badline/c128/vdc/raster.rb +97 -0
- data/lib/badline/c128/vdc/window.rb +215 -0
- data/lib/badline/c128/vdc.rb +268 -0
- data/lib/badline/c128.rb +320 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/computer/attachments.rb +4 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +5 -0
- data/lib/badline/frontend/app.rb +34 -21
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_pages.rb +6 -1
- data/lib/badline/frontend/menu_ports.rb +1 -1
- data/lib/badline/frontend/pause_menu.rb +1 -0
- data/lib/badline/frontend/screen.rb +33 -8
- data/lib/badline/frontend/screens.rb +90 -0
- data/lib/badline/frontend/timeline.rb +15 -4
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +58 -8
- data/lib/badline/kernal_trap/load.rb +1 -1
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +5 -0
- data/lib/badline/machine.rb +15 -6
- data/lib/badline/media.rb +8 -6
- data/lib/badline/options/event.rb +5 -1
- data/lib/badline/options/family.rb +52 -0
- data/lib/badline/options/help.rb +5 -1
- data/lib/badline/options/table.rb +7 -1
- data/lib/badline/options/validation.rb +14 -1
- data/lib/badline/options.rb +20 -19
- data/lib/badline/roms/README +27 -0
- data/lib/badline/roms/c128/basic.rom +0 -0
- data/lib/badline/roms/c128/character.rom +0 -0
- data/lib/badline/roms/c128/kernal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +14 -0
- data/lib/badline/snapshot.rb +9 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/display_state.rb +12 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic.rb +108 -7
- data/lib/badline.rb +1 -0
- metadata +24 -1
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# The emulator's SDL window, its renderer and the streaming texture a
|
|
6
|
-
# Screen's pixels go into. The renderer's logical size is the screen's
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Screen's pixels go into. The renderer's logical size is the screen's
|
|
7
|
+
# in square pixels (Screen#window_width), so the picture keeps the
|
|
8
|
+
# machine's aspect while the menu and the drive LED draw in square
|
|
9
|
+
# pixels over it. The window opens at SCALE times that.
|
|
10
10
|
class MachineWindow
|
|
11
11
|
SCALE = 2
|
|
12
12
|
TITLE = "Badline"
|
|
@@ -18,12 +18,12 @@ module Badline
|
|
|
18
18
|
|
|
19
19
|
# Opens the window for `screen`, presenting in step with the display
|
|
20
20
|
# when `vsync`.
|
|
21
|
-
def initialize(screen,
|
|
21
|
+
def initialize(screen, vsync:)
|
|
22
22
|
abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
|
|
23
23
|
|
|
24
24
|
SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
|
|
25
25
|
SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
|
|
26
|
-
measure(screen
|
|
26
|
+
measure(screen)
|
|
27
27
|
@window = SDL.SDL_CreateWindow(
|
|
28
28
|
TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
|
|
29
29
|
SDL::WINDOW_RESIZABLE
|
|
@@ -42,9 +42,9 @@ module Badline
|
|
|
42
42
|
|
|
43
43
|
# Fits the texture, the logical size and the window to another
|
|
44
44
|
# machine's screen, unless it measures the same.
|
|
45
|
-
def fit(screen
|
|
45
|
+
def fit(screen)
|
|
46
46
|
before = [@width, @height, @texture_width]
|
|
47
|
-
measure(screen
|
|
47
|
+
measure(screen)
|
|
48
48
|
return if before == [@width, @height, @texture_width]
|
|
49
49
|
|
|
50
50
|
SDL.SDL_DestroyTexture(@texture)
|
|
@@ -56,9 +56,12 @@ module Badline
|
|
|
56
56
|
SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Titles the window, TITLE followed by `
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# Titles the window, TITLE followed by the controls' `tag`, unless it's
|
|
60
|
+
# empty, and MUTED when `muted`, each in brackets.
|
|
61
|
+
def title(tag, muted)
|
|
62
|
+
tags = tag.empty? ? [] : [tag]
|
|
63
|
+
tags << "MUTED" if muted
|
|
64
|
+
SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |name| "[#{name}]" }).join(" "))
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def close
|
|
@@ -70,9 +73,9 @@ module Badline
|
|
|
70
73
|
|
|
71
74
|
private
|
|
72
75
|
|
|
73
|
-
def measure(screen
|
|
76
|
+
def measure(screen)
|
|
74
77
|
@texture_width = screen.width
|
|
75
|
-
@width = screen.
|
|
78
|
+
@width = screen.window_width
|
|
76
79
|
@height = screen.height
|
|
77
80
|
end
|
|
78
81
|
|
|
@@ -85,6 +85,9 @@ module Badline
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Whether the machine is a C128, whose F8 switches its screens.
|
|
89
|
+
def c128? = @computer.family == :c128
|
|
90
|
+
|
|
88
91
|
private
|
|
89
92
|
|
|
90
93
|
def vic20? = @computer.family == :vic20
|
|
@@ -131,7 +134,7 @@ module Badline
|
|
|
131
134
|
info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
|
|
132
135
|
row("INSERT...", :insert_cartridge)
|
|
133
136
|
row("REMOVE", :remove_cartridge)
|
|
134
|
-
row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer?
|
|
137
|
+
row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer? && @computer.family == :c64
|
|
135
138
|
return if @reu_kb.nil?
|
|
136
139
|
|
|
137
140
|
skip
|
|
@@ -166,7 +169,9 @@ module Badline
|
|
|
166
169
|
end
|
|
167
170
|
end
|
|
168
171
|
|
|
172
|
+
# The C128 names its model and the mode it runs in first.
|
|
169
173
|
def draw_power
|
|
174
|
+
info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
|
|
170
175
|
row("RESET", :reset)
|
|
171
176
|
row("POWER CYCLE", :power_cycle)
|
|
172
177
|
skip
|
|
@@ -19,7 +19,7 @@ module Badline
|
|
|
19
19
|
|
|
20
20
|
toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
|
|
21
21
|
toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
|
|
22
|
-
return unless draw_keys("C64")
|
|
22
|
+
return unless draw_keys(@computer.family == :c128 ? "C128" : "C64")
|
|
23
23
|
|
|
24
24
|
row("SWAP JOYSTICKS", :swap)
|
|
25
25
|
skip
|
|
@@ -145,6 +145,7 @@ module Badline
|
|
|
145
145
|
draw_sections
|
|
146
146
|
@buttons.plain(@left + 6, @top + HEIGHT - 28, "QUICK OPEN...", :quick_open)
|
|
147
147
|
painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", DIM)
|
|
148
|
+
painter.text(@left + BODY, @top + HEIGHT - 12, "F6: 40/80 KEY F8: SCREEN", DIM) if @pages.c128?
|
|
148
149
|
@pages.draw(PAGES[@section], @left + BODY, @top + 34)
|
|
149
150
|
end
|
|
150
151
|
|
|
@@ -8,30 +8,44 @@ module Badline
|
|
|
8
8
|
# words, as badline/ffi does on CRuby, so each word carries two
|
|
9
9
|
# neighbouring pixels, the left one in the low half.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# Each pixel shows `pixel_width` of the window's square pixels wide,
|
|
12
|
+
# or with `dots`, that many pixels share one, as the VDC's dots do. The
|
|
13
|
+
# texture is as wide as the crop, an even number of pixels, and at
|
|
14
|
+
# least MIN_WIDTH window pixels wide, and at least as high as PAL's C64
|
|
15
|
+
# crop, 272 lines, so a C64 shows in the same window whatever its
|
|
16
|
+
# region. A crop with fewer lines or pixels sits in the middle of the
|
|
17
|
+
# texture, between black bands. The VIC-20's 284 by 284 crop fills its
|
|
18
|
+
# own.
|
|
15
19
|
class Screen
|
|
20
|
+
MIN_WIDTH = Region::PAL.crop[2]
|
|
16
21
|
MIN_HEIGHT = Region::PAL.crop[3]
|
|
17
22
|
|
|
18
23
|
attr_reader :pixels, :width, :height
|
|
19
24
|
|
|
20
|
-
def initialize(video, crop)
|
|
25
|
+
def initialize(video, crop, pixel_width: 1, dots: 1)
|
|
21
26
|
@video = video
|
|
22
27
|
@col_offset = crop[0]
|
|
23
28
|
@row_offset = crop[1]
|
|
24
29
|
@lines = crop[3]
|
|
25
|
-
@
|
|
30
|
+
@columns = crop[2] & ~1
|
|
31
|
+
@pixel_width = pixel_width
|
|
32
|
+
@dots = dots
|
|
26
33
|
@stride = video.width.to_i
|
|
34
|
+
@width = [@columns, ((MIN_WIDTH * dots / pixel_width) + 1) & ~1].max
|
|
27
35
|
@height = [@lines, MIN_HEIGHT].max
|
|
28
36
|
@row_words = @width / 2
|
|
37
|
+
@pair_count = @columns / 2
|
|
38
|
+
@margin = (@row_words - @pair_count) / 2
|
|
29
39
|
@band = (@height - @lines) / 2
|
|
30
40
|
palette = video.palette
|
|
31
41
|
@pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
|
|
32
42
|
@pixels = Array.new(@height * @row_words, 0)
|
|
43
|
+
pack_all
|
|
33
44
|
end
|
|
34
45
|
|
|
46
|
+
# The width in the window's square pixels.
|
|
47
|
+
def window_width = @width * @pixel_width / @dots
|
|
48
|
+
|
|
35
49
|
# The bytes of one row of the texture.
|
|
36
50
|
def row_bytes = @width * 4
|
|
37
51
|
|
|
@@ -50,12 +64,23 @@ module Badline
|
|
|
50
64
|
|
|
51
65
|
private
|
|
52
66
|
|
|
67
|
+
# Repacks every line, as a screen built for a display the chip has
|
|
68
|
+
# already drawn starts with all of it.
|
|
69
|
+
def pack_all
|
|
70
|
+
display = @video.display
|
|
71
|
+
row = 0
|
|
72
|
+
while row < @lines
|
|
73
|
+
pack_row(display, row)
|
|
74
|
+
row += 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
53
78
|
def pack_row(display, row)
|
|
54
79
|
pixels = @pixels
|
|
55
80
|
pairs = @pairs
|
|
56
81
|
from = ((row + @row_offset) * @stride) + @col_offset
|
|
57
|
-
to = (row + @band) * @row_words
|
|
58
|
-
last = to + @
|
|
82
|
+
to = ((row + @band) * @row_words) + @margin
|
|
83
|
+
last = to + @pair_count
|
|
59
84
|
while to < last
|
|
60
85
|
pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
|
|
61
86
|
from += 2
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The Screen the window shows: the machine's video chip's, or on the
|
|
6
|
+
# C128 the VDC's, two of its dots to a window pixel, once #show_vdc
|
|
7
|
+
# switches to it. Only the chip shown renders. The VDC's display changes
|
|
8
|
+
# size with its registers, and the screen follows it.
|
|
9
|
+
#
|
|
10
|
+
# On the C128, #follow switches to the screen the machine prints to
|
|
11
|
+
# (C128#active_screen) each time it moves there, and #show_vdc's choice
|
|
12
|
+
# holds until the next move.
|
|
13
|
+
class Screens
|
|
14
|
+
# The frames a move holds before #follow takes it. The C128 KERNAL's
|
|
15
|
+
# reset clears the 80 column screen with the editor on it for over a
|
|
16
|
+
# frame.
|
|
17
|
+
FOLLOW_FRAMES = 3
|
|
18
|
+
|
|
19
|
+
attr_reader :screen
|
|
20
|
+
|
|
21
|
+
def initialize(computer)
|
|
22
|
+
@computer = computer
|
|
23
|
+
@vdc_shown = false
|
|
24
|
+
@vdc_shape = []
|
|
25
|
+
forget_screen
|
|
26
|
+
build
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def vdc_shown? = @vdc_shown
|
|
30
|
+
|
|
31
|
+
# Shows another machine's video chip, once #build builds its screen.
|
|
32
|
+
def computer=(computer)
|
|
33
|
+
@computer = computer
|
|
34
|
+
@vdc_shown = false
|
|
35
|
+
forget_screen
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Shows the screen a C128 has moved its output to once the move has
|
|
39
|
+
# held for FOLLOW_FRAMES calls, one a frame. #build then builds it.
|
|
40
|
+
def follow
|
|
41
|
+
return unless @computer.family == :c128
|
|
42
|
+
|
|
43
|
+
vdc = @computer.active_screen == :vdc
|
|
44
|
+
@held = vdc == @seen ? @held + 1 : 1
|
|
45
|
+
@seen = vdc
|
|
46
|
+
return if @held < FOLLOW_FRAMES || vdc == @followed
|
|
47
|
+
|
|
48
|
+
@followed = vdc
|
|
49
|
+
show_vdc(vdc)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Shows the C128's VDC, or with false its VIC-IIe, once #build builds
|
|
53
|
+
# its screen. Other machines have the one screen.
|
|
54
|
+
def show_vdc(shown)
|
|
55
|
+
return unless @computer.family == :c128
|
|
56
|
+
|
|
57
|
+
@vdc_shown = shown
|
|
58
|
+
@computer.vdc_shown = shown
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether the screen was built for the other chip, or the VDC's
|
|
62
|
+
# display has changed size or crop since the screen was built for it.
|
|
63
|
+
def stale? = @built_vdc != @vdc_shown || (@vdc_shown && vdc_shape(@computer.vdc) != @vdc_shape)
|
|
64
|
+
|
|
65
|
+
def build
|
|
66
|
+
@built_vdc = @vdc_shown
|
|
67
|
+
if @vdc_shown
|
|
68
|
+
vdc = @computer.vdc
|
|
69
|
+
@vdc_shape = vdc_shape(vdc)
|
|
70
|
+
@screen = Screen.new(vdc, vdc.crop, dots: 2)
|
|
71
|
+
else
|
|
72
|
+
timing = @computer.timing
|
|
73
|
+
@screen = Screen.new(@computer.video, timing.crop, pixel_width: timing.pixel_width)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# The machine's output counts as on the VIC-IIe's screen, the one
|
|
80
|
+
# shown.
|
|
81
|
+
def forget_screen
|
|
82
|
+
@followed = false
|
|
83
|
+
@seen = false
|
|
84
|
+
@held = 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def vdc_shape(vdc) = [vdc.width, vdc.height] + vdc.crop
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -87,6 +87,17 @@ module Badline
|
|
|
87
87
|
section
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# Whether the frame has a display event.
|
|
91
|
+
def display?(frame) = @events.any? { |event| event.frame == frame && event.action == "display" }
|
|
92
|
+
|
|
93
|
+
# The C128 screen the frame's display event shows, vic or vdc, or an
|
|
94
|
+
# empty string without one.
|
|
95
|
+
def display_at(frame)
|
|
96
|
+
chip = ""
|
|
97
|
+
@events.each { |event| chip = event.argument if event.frame == frame && event.action == "display" }
|
|
98
|
+
chip
|
|
99
|
+
end
|
|
100
|
+
|
|
90
101
|
# Whether the pause menu closes at the frame.
|
|
91
102
|
def resume?(frame) = @events.any? { |event| event.frame == frame && event.action == "resume" }
|
|
92
103
|
|
|
@@ -133,9 +144,9 @@ module Badline
|
|
|
133
144
|
hold(event)
|
|
134
145
|
end
|
|
135
146
|
|
|
136
|
-
# The VIC-20
|
|
147
|
+
# The VIC-20 and the C128 have no cartridge with a button yet.
|
|
137
148
|
def freeze(computer, event)
|
|
138
|
-
return
|
|
149
|
+
return unless computer.family == :c64
|
|
139
150
|
|
|
140
151
|
computer.press_cartridge_button
|
|
141
152
|
hold(event)
|
|
@@ -146,7 +157,7 @@ module Badline
|
|
|
146
157
|
end
|
|
147
158
|
|
|
148
159
|
def release(computer, event)
|
|
149
|
-
return computer.release_cartridge_button if event.action == "freeze"
|
|
160
|
+
return computer.release_cartridge_button if event.action == "freeze" && computer.family == :c64
|
|
150
161
|
|
|
151
162
|
name = event.argument
|
|
152
163
|
return computer.release_restore if name == "restore"
|
|
@@ -183,7 +194,7 @@ module Badline
|
|
|
183
194
|
end
|
|
184
195
|
|
|
185
196
|
# Whether there's one in to eject. A VIC-20 has no cartridge to take out yet.
|
|
186
|
-
def ejectable?(computer, what) = (computer.family
|
|
197
|
+
def ejectable?(computer, what) = (computer.family != :vic20 || what != "cartridge") && inserted?(computer, what)
|
|
187
198
|
|
|
188
199
|
def inserted?(computer, what)
|
|
189
200
|
return !computer.datasette.tape.nil? if what == "tape"
|
data/lib/badline/frontend.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "badline/sdl"
|
|
|
4
4
|
require "badline/frontend/boot"
|
|
5
5
|
require "badline/frontend/frame_rate"
|
|
6
6
|
require "badline/frontend/screen"
|
|
7
|
+
require "badline/frontend/screens"
|
|
7
8
|
require "badline/frontend/drive_led"
|
|
8
9
|
require "badline/frontend/keys"
|
|
9
10
|
require "badline/frontend/snapshots"
|
|
@@ -17,7 +17,7 @@ module Badline
|
|
|
17
17
|
def full_filename
|
|
18
18
|
pointer = uint16(@bus.peek(0xbb), @bus.peek(0xbc))
|
|
19
19
|
bytes = Array.new(@bus.peek(0xb7)) do |i|
|
|
20
|
-
@
|
|
20
|
+
@layout.filename_byte(@bus, (pointer + i) & 0xffff)
|
|
21
21
|
end
|
|
22
22
|
Storage.ascii(bytes)
|
|
23
23
|
end
|
|
@@ -18,37 +18,59 @@ module Badline
|
|
|
18
18
|
# primitives' entry points.
|
|
19
19
|
#
|
|
20
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.
|
|
21
|
+
# whether RAM lies under every ROM and the I/O. +mmu+ says the C128's
|
|
22
|
+
# MMU maps the KERNAL instead, and that the file name and the file's
|
|
23
|
+
# bytes lie in the banks the KERNAL keeps at FNBANK and BA. Each
|
|
22
24
|
# serial byte starts a one-shot timer by writing the high byte of its
|
|
23
25
|
# count to +serial_timer+, and the control value TIMER_ONE_SHOT_START
|
|
24
26
|
# to +serial_timer_start+ where the timer has a control register to
|
|
25
27
|
# start it (a CIA's), or nil where the write to the high byte starts it
|
|
26
|
-
# (a VIA's timer 2).
|
|
28
|
+
# (a VIA's timer 2). A KERNAL that times its bytes with a loop has no
|
|
29
|
+
# +serial_timer+. The ROM lets go of each serial line by reading a
|
|
27
30
|
# port, masking the line's bit off and writing it back: ATN at
|
|
28
31
|
# +atn_port+ with +atn_mask+, then the clock and the data line.
|
|
29
32
|
Layout = Data.define(:load, :save, :searching_message, :loading_message, :load_byte_loop, :load_done,
|
|
30
33
|
:file_not_found_exit, :missing_file_name_exit, :saving_message, :clock_release,
|
|
31
34
|
:data_release, :save_done, :talk, :listen, :second, :tksa, :ciout, :untalk,
|
|
32
35
|
:unlisten, :acptr, :banked, :serial_timer, :serial_timer_start, :atn_port, :atn_mask,
|
|
33
|
-
:clock_port, :clock_mask, :data_port, :data_mask) do
|
|
36
|
+
:clock_port, :clock_mask, :data_port, :data_mask, :mmu) do
|
|
34
37
|
# Whether the CPU reads the KERNAL ROM at the trapped addresses: on a
|
|
35
|
-
# banked machine, the 6510 port's banking bits select it
|
|
38
|
+
# banked machine, the 6510 port's banking bits select it, and on the
|
|
39
|
+
# C128 the MMU.
|
|
36
40
|
def kernal_mapped?(bus)
|
|
41
|
+
return bus.system_rom_mapped? if mmu
|
|
42
|
+
|
|
37
43
|
!banked || bus.io_port.kernal?
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# Stores a loaded file's bytes from +address+ on: on a banked machine
|
|
41
|
-
# into the RAM under whatever is banked in,
|
|
42
|
-
# ROM's byte loop would
|
|
47
|
+
# into the RAM under whatever is banked in, on the C128 into the RAM
|
|
48
|
+
# of the bank at BA, and otherwise where the ROM's byte loop would
|
|
49
|
+
# store them, through the bus.
|
|
43
50
|
def store_file(bus, address, bytes)
|
|
51
|
+
return bus.write_bank(bus.peek(BANK), address, bytes) if mmu
|
|
44
52
|
return bus.ram.write(address, bytes) if banked
|
|
45
53
|
|
|
46
54
|
bytes.each_with_index { |byte, i| bus.poke(address + i, byte) }
|
|
47
55
|
end
|
|
48
56
|
|
|
57
|
+
# The file name's byte at +address+, in the bank at FNBANK on the
|
|
58
|
+
# C128.
|
|
59
|
+
def filename_byte(bus, address)
|
|
60
|
+
mmu ? bus.peek_bank(bus.peek(FILENAME_BANK), address) : bus.peek(address)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The byte at +address+ a SAVE writes or a VERIFY compares, in the bank
|
|
64
|
+
# at BA on the C128.
|
|
65
|
+
def file_byte(bus, address)
|
|
66
|
+
mmu ? bus.peek_bank(bus.peek(BANK), address) : bus.peek(address)
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
# Starts the timer the ROM times a serial byte with, its count's high
|
|
50
70
|
# byte +timer_high+.
|
|
51
71
|
def time_serial_byte(bus, timer_high)
|
|
72
|
+
return unless serial_timer
|
|
73
|
+
|
|
52
74
|
bus.poke(serial_timer, timer_high)
|
|
53
75
|
start = serial_timer_start
|
|
54
76
|
bus.poke(start, TIMER_ONE_SHOT_START) if start
|
|
@@ -77,6 +99,11 @@ module Badline
|
|
|
77
99
|
# start.
|
|
78
100
|
TIMER_ONE_SHOT_START = 0x19
|
|
79
101
|
|
|
102
|
+
# Where the C128 KERNAL keeps the bank a LOAD or SAVE goes to (BA) and
|
|
103
|
+
# the bank of the file name (FNBANK).
|
|
104
|
+
BANK = 0xc6
|
|
105
|
+
FILENAME_BANK = 0xc7
|
|
106
|
+
|
|
80
107
|
# The C64 KERNAL, revision 3. The SX-64's, 251104-04, differs from it
|
|
81
108
|
# only in bytes none of these addresses reach. It times serial bytes
|
|
82
109
|
# with CIA 1's timer B, and drives the lines from CIA 2's port A
|
|
@@ -89,7 +116,8 @@ module Badline
|
|
|
89
116
|
talk: 0xed09, listen: 0xed0c, second: 0xedb9, tksa: 0xedc7,
|
|
90
117
|
ciout: 0xeddd, untalk: 0xedef, unlisten: 0xedfe, acptr: 0xee13,
|
|
91
118
|
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
|
|
119
|
+
atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf,
|
|
120
|
+
mmu: false
|
|
93
121
|
)
|
|
94
122
|
|
|
95
123
|
# The VIC-20 KERNAL, 901486-07 (PAL). The NTSC one, 901486-06, differs
|
|
@@ -106,7 +134,29 @@ module Badline
|
|
|
106
134
|
talk: 0xee14, listen: 0xee17, second: 0xeec0, tksa: 0xeece,
|
|
107
135
|
ciout: 0xeee4, untalk: 0xeef6, unlisten: 0xef04, acptr: 0xef19,
|
|
108
136
|
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
|
|
137
|
+
atn_port: 0x911f, atn_mask: 0x7f, clock_port: 0x912c, clock_mask: 0xfd, data_port: 0x912c, data_mask: 0xdf,
|
|
138
|
+
mmu: false
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# The C128 KERNAL, 318020-05, in C128 mode: the jump table's targets
|
|
142
|
+
# for the serial primitives, ILOAD's and ISAVE's default targets, and
|
|
143
|
+
# the ROM's own tails. Its LOAD prints SEARCHING FOR at $F50F and
|
|
144
|
+
# LOADING or VERIFYING at $F533, retries a byte at $F2CF and returns at
|
|
145
|
+
# $F39B. FILE NOT FOUND leaves through $F685 and MISSING FILE NAME
|
|
146
|
+
# through $F691. SAVE prints SAVING at $F5BC and returns at $F5B3, after
|
|
147
|
+
# the UNLISTEN whose clock and data releases are $E545 and $E557. It
|
|
148
|
+
# drives the lines from CIA 2's port A, and times serial bytes with
|
|
149
|
+
# loops, not a timer.
|
|
150
|
+
C128_LAYOUT = Layout.new(
|
|
151
|
+
load: 0xf26c, save: 0xf54e,
|
|
152
|
+
searching_message: 0xf50f, loading_message: 0xf533, load_byte_loop: 0xf2cf, load_done: 0xf39b,
|
|
153
|
+
file_not_found_exit: 0xf685, missing_file_name_exit: 0xf691,
|
|
154
|
+
saving_message: 0xf5bc, clock_release: 0xe545, data_release: 0xe557, save_done: 0xf5b3,
|
|
155
|
+
talk: 0xe33b, listen: 0xe33e, second: 0xe4d2, tksa: 0xe4e0,
|
|
156
|
+
ciout: 0xe503, untalk: 0xe515, unlisten: 0xe526, acptr: 0xe43e,
|
|
157
|
+
banked: true, serial_timer: nil, serial_timer_start: nil,
|
|
158
|
+
atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf,
|
|
159
|
+
mmu: true
|
|
110
160
|
)
|
|
111
161
|
end
|
|
112
162
|
end
|
|
@@ -112,7 +112,7 @@ module Badline
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def verify(addr, payload)
|
|
115
|
-
match = payload.each_with_index.all? { |byte, i| @
|
|
115
|
+
match = payload.each_with_index.all? { |byte, i| @layout.file_byte(@bus, addr + i) == byte }
|
|
116
116
|
@bus.poke(0x90, match ? EOI : EOI | VERIFY_MISMATCH)
|
|
117
117
|
end
|
|
118
118
|
|
|
@@ -71,7 +71,7 @@ module Badline
|
|
|
71
71
|
start = uint16(@bus.peek(0xc1), @bus.peek(0xc2))
|
|
72
72
|
length = (uint16(@bus.peek(0xae), @bus.peek(0xaf)) - start) & 0xffff
|
|
73
73
|
[low_byte(start), high_byte(start)] +
|
|
74
|
-
Array.new(length) { |i| @
|
|
74
|
+
Array.new(length) { |i| @layout.file_byte(@bus, (start + i) & 0xffff) }
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
end
|
data/lib/badline/machine.rb
CHANGED
|
@@ -4,7 +4,8 @@ module Badline
|
|
|
4
4
|
# = Machine
|
|
5
5
|
#
|
|
6
6
|
# The machines badline builds by family, as the command line's
|
|
7
|
-
# subcommand names them: the C64, Computer,
|
|
7
|
+
# subcommand names them: the C64, Computer, the VIC-20, Vic20, and the
|
|
8
|
+
# C128 in C64 mode, C128.
|
|
8
9
|
#
|
|
9
10
|
# A machine answers what the front end, Media, the runners and the
|
|
10
11
|
# snapshots call on it:
|
|
@@ -34,14 +35,22 @@ module Badline
|
|
|
34
35
|
# +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
|
|
35
36
|
# +attach_drive1541+, +drive1541+, +install_debug_register+ and
|
|
36
37
|
# +capture_output+.
|
|
38
|
+
#
|
|
39
|
+
# The C128 answers all but the cartridge button, and besides +mode+,
|
|
40
|
+
# :c64, as Machine.build builds it, or :c128, +vdc+, the VDC a front
|
|
41
|
+
# end can show in place of +video+ once +vdc_shown=+ renders it,
|
|
42
|
+
# +press_caps_lock+, +release_caps_lock+, +press_display_key+ and
|
|
43
|
+
# +release_display_key+.
|
|
37
44
|
module Machine
|
|
38
|
-
# A new machine of `family`, :c64 or :
|
|
39
|
-
# names. A C64's model is one of Model::ALL, with its SID
|
|
40
|
-
# when not nil and an REU of `reu` K when not nil. A
|
|
41
|
-
# with the RAM expansion `ram` names
|
|
42
|
-
# unexpanded when nil.
|
|
45
|
+
# A new machine of `family`, :c64, :vic20 or :c128, the model of it
|
|
46
|
+
# `model` names. A C64's model is one of Model::ALL, with its SID
|
|
47
|
+
# `sid_model` when not nil and an REU of `reu` K when not nil. A
|
|
48
|
+
# VIC-20's is "pal", with the RAM expansion `ram` names
|
|
49
|
+
# (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
|
|
50
|
+
# one of C128::Model::ALL, with its SID `sid_model` when not nil.
|
|
43
51
|
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
44
52
|
return vic20(model, ram) if family == :vic20
|
|
53
|
+
return C128.new(model:, sid_model:) if family == :c128
|
|
45
54
|
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
46
55
|
|
|
47
56
|
profile = Model.named(model)
|
data/lib/badline/media.rb
CHANGED
|
@@ -33,11 +33,11 @@ module Badline
|
|
|
33
33
|
# autostart then loads through it.
|
|
34
34
|
#
|
|
35
35
|
# An .m3u or .vfl list of disks attaches the first disk it lists
|
|
36
|
-
# (DiskList).
|
|
37
|
-
#
|
|
36
|
+
# (DiskList). The VIC-20 takes its own media (Vic20Media), and the
|
|
37
|
+
# C128 in C64 mode a C64's.
|
|
38
38
|
def attach(computer, path, autostart: true, subtune: nil, **)
|
|
39
39
|
path = DiskList.disk(path)
|
|
40
|
-
return Vic20Media.attach(computer, path, autostart:, **)
|
|
40
|
+
return Vic20Media.attach(computer, path, autostart:, **) if computer.family == :vic20
|
|
41
41
|
|
|
42
42
|
attach_c64(computer, path, autostart:, subtune:, **)
|
|
43
43
|
end
|
|
@@ -160,13 +160,15 @@ module Badline
|
|
|
160
160
|
def start_prg(computer, data, autostart:)
|
|
161
161
|
load_addr = computer.load_prg(data)
|
|
162
162
|
# Run only makes sense for programs at BASIC start, or a byte ahead
|
|
163
|
-
# of it, where BASIC keeps the zero before its first line.
|
|
164
|
-
|
|
163
|
+
# of it, where BASIC keeps the zero before its first line. BASIC 7.0
|
|
164
|
+
# starts at $1C01 and keeps the end of its text at $1210.
|
|
165
|
+
start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
|
|
166
|
+
return unless autostart && (load_addr == start || load_addr == start - 1)
|
|
165
167
|
|
|
166
168
|
# The end of the program, where BASIC's variables start, and where
|
|
167
169
|
# the KERNAL's LOAD leaves its end address.
|
|
168
170
|
end_addr = load_addr + data.length - 2
|
|
169
|
-
[
|
|
171
|
+
[text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
|
|
170
172
|
computer.type_text("run\r")
|
|
171
173
|
end
|
|
172
174
|
end
|
|
@@ -10,7 +10,7 @@ module Badline
|
|
|
10
10
|
# left out.
|
|
11
11
|
ACTIONS = {
|
|
12
12
|
"key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true, "menu" => true,
|
|
13
|
-
"reset" => false, "freeze" => false, "resume" => false, "quit" => false
|
|
13
|
+
"display" => true, "reset" => false, "freeze" => false, "resume" => false, "quit" => false
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
# The frames a key or the freeze button is held for.
|
|
@@ -18,6 +18,9 @@ module Badline
|
|
|
18
18
|
|
|
19
19
|
EJECTABLE = %w[disk tape cartridge].freeze
|
|
20
20
|
|
|
21
|
+
# The C128's screens display shows: the VIC-IIe's or the VDC's.
|
|
22
|
+
DISPLAYS = %w[vic vdc].freeze
|
|
23
|
+
|
|
21
24
|
# What insert takes: disk images, tapes and cartridges, or an .m3u
|
|
22
25
|
# or .vfl list of disks or a directory to mount as device 8.
|
|
23
26
|
INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
|
|
@@ -83,6 +86,7 @@ module Badline
|
|
|
83
86
|
return true if action == "menu"
|
|
84
87
|
return argument.empty? unless ACTIONS[action]
|
|
85
88
|
return EJECTABLE.include?(argument) if action == "eject"
|
|
89
|
+
return DISPLAYS.include?(argument) if action == "display"
|
|
86
90
|
return insertable?(argument) if action == "insert"
|
|
87
91
|
|
|
88
92
|
!argument.empty?
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Options
|
|
5
|
+
# The machine family: the subcommand that names it, or else the media,
|
|
6
|
+
# and the models of each.
|
|
7
|
+
module Family
|
|
8
|
+
# Where C128 BASIC starts, which a program for the C128 loads at.
|
|
9
|
+
C128_BASIC_START = 0x1c01
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Takes `sid`, `c64`, `vic20` or `c128` when given first.
|
|
14
|
+
def subcommand(args)
|
|
15
|
+
case args.first
|
|
16
|
+
when "sid" then @sid_command = true
|
|
17
|
+
when "c64" then @family = :c64
|
|
18
|
+
when "vic20" then @family = :vic20
|
|
19
|
+
when "c128" then @family = :c128
|
|
20
|
+
else return
|
|
21
|
+
end
|
|
22
|
+
@family_named = !@sid_command
|
|
23
|
+
args.shift
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def family_models
|
|
27
|
+
case @family
|
|
28
|
+
when :vic20 then VIC20_MODELS
|
|
29
|
+
when :c128 then C128_MODELS
|
|
30
|
+
else MODELS
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Without a family named, a program that loads at C128_BASIC_START is
|
|
35
|
+
# the C128's.
|
|
36
|
+
def pick_family
|
|
37
|
+
return if @family_named || @sid_command || @media_path.nil?
|
|
38
|
+
return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
|
|
39
|
+
|
|
40
|
+
@family = :c128 if load_address(@media_path) == C128_BASIC_START
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The load address of a .prg, or of the program a .p00 wraps after its
|
|
44
|
+
# 26-byte header.
|
|
45
|
+
def load_address(path)
|
|
46
|
+
offset = File.extname(path).casecmp?(".p00") ? 26 : 0
|
|
47
|
+
bytes = File.binread(path).bytes
|
|
48
|
+
bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|