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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class Bus
|
|
6
|
+
# What the MMU maps. For the CPU: the RAM bank, common RAM and the
|
|
7
|
+
# pages page 0 and page 1 move to in both modes, and CR's ROMs and I/O
|
|
8
|
+
# in C128 mode. C64 mode lays the PLA's ROMs and I/O over the same RAM
|
|
9
|
+
# (AddressBus::PLA).
|
|
10
|
+
module MMUPages
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# CR's ROMs and I/O over the RAM, with the configuration registers on
|
|
14
|
+
# top at $FF00.
|
|
15
|
+
def map_c128_pages
|
|
16
|
+
cr = @mmu.cr
|
|
17
|
+
map_ram_pages
|
|
18
|
+
@read_pages.fill(@basic_low_rom, 0x40, 0x40) if cr.nobits?(0x02)
|
|
19
|
+
map_rom_block((cr >> 2) & 0x03, 0x80)
|
|
20
|
+
map_rom_block((cr >> 4) & 0x03, 0xc0)
|
|
21
|
+
map_io_pages if cr.nobits?(0x01)
|
|
22
|
+
@configuration_reads.below = @read_pages[0xff]
|
|
23
|
+
@configuration_writes.below = @write_pages[0xff]
|
|
24
|
+
@read_pages[0xff] = @configuration_reads
|
|
25
|
+
@write_pages[0xff] = @configuration_writes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A 16K block's two CR bits pick the system ROM, the internal or the
|
|
29
|
+
# external function ROM, neither of which is fitted, or RAM.
|
|
30
|
+
def map_rom_block(select, first_page)
|
|
31
|
+
case select
|
|
32
|
+
when 0 then map_system_rom(first_page)
|
|
33
|
+
when 1, 2 then @read_pages.fill(@open_bus, first_page, 0x40)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# BASIC's high half at $8000, or the screen editor, the character ROM
|
|
38
|
+
# and the KERNAL at $C000.
|
|
39
|
+
def map_system_rom(first_page)
|
|
40
|
+
if first_page == 0x80
|
|
41
|
+
@read_pages.fill(@basic_high_rom, 0x80, 0x40)
|
|
42
|
+
else
|
|
43
|
+
@read_pages.fill(@editor_rom, 0xc0, 0x10)
|
|
44
|
+
@read_pages.fill(@c128_character_rom, 0xd0, 0x10)
|
|
45
|
+
@read_pages.fill(@c128_kernal_rom, 0xe0, 0x20)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The bank CR picks, with common RAM in bank 0, then the pages page 0
|
|
50
|
+
# and page 1 move to.
|
|
51
|
+
def map_ram_pages
|
|
52
|
+
@io_mapped = false
|
|
53
|
+
bank_ram = @mmu.cpu_bank.zero? ? @ram : @bank1
|
|
54
|
+
@read_pages.fill(bank_ram)
|
|
55
|
+
@write_pages.fill(bank_ram)
|
|
56
|
+
map_common_ram
|
|
57
|
+
relocate_pages
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def map_common_ram
|
|
61
|
+
low = @mmu.common_low_pages
|
|
62
|
+
high = @mmu.common_high_start
|
|
63
|
+
@read_pages.fill(@ram, 0, low)
|
|
64
|
+
@write_pages.fill(@ram, 0, low)
|
|
65
|
+
@read_pages.fill(@ram, high, 256 - high)
|
|
66
|
+
@write_pages.fill(@ram, high, 256 - high)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# In C128 mode page 0 and page 1 go to the pages P0 and P1 name, in
|
|
70
|
+
# bank 0 if common RAM holds them. In both modes, the page each moved
|
|
71
|
+
# to reaches back to page 0 or page 1, in the bank it named, if the
|
|
72
|
+
# CPU sees that bank there. Page 1's takes the page they both name.
|
|
73
|
+
# C64 mode moves neither page 0 nor page 1 forward, so either can be
|
|
74
|
+
# the page that reaches back.
|
|
75
|
+
def relocate_pages
|
|
76
|
+
mmu = @mmu
|
|
77
|
+
forward = !mmu.c64_mode?
|
|
78
|
+
if forward
|
|
79
|
+
map_relocated(0, @relocated[0], bank_of(0, mmu.p0_bank), mmu.p0_page)
|
|
80
|
+
map_relocated(1, @relocated[1], bank_of(1, mmu.p1_bank), mmu.p1_page)
|
|
81
|
+
end
|
|
82
|
+
reach_back(@relocated[2], mmu.p0_page, mmu.p0_bank, 0, forward)
|
|
83
|
+
reach_back(@relocated[3], mmu.p1_page, mmu.p1_bank, 1, forward)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reach_back(window, page, bank, home, forward)
|
|
87
|
+
return if forward && page < 2
|
|
88
|
+
return unless bank_of(page, @mmu.cpu_bank) == bank
|
|
89
|
+
|
|
90
|
+
map_relocated(page, window, bank, home)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def map_relocated(page, window, bank, target)
|
|
94
|
+
window.base = (bank << 16) | (target << 8)
|
|
95
|
+
@read_pages[page] = @write_pages[page] = window
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The bank +page+ reaches when the CPU or a pointer asks for +bank+.
|
|
99
|
+
def bank_of(page, bank)
|
|
100
|
+
page < @mmu.common_low_pages || page >= @mmu.common_high_start ? 0 : bank
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The C128s badline builds by name: the chips each board has, and its
|
|
6
|
+
# video standard. The plastic C128D is a "c128" board with a 1571 built
|
|
7
|
+
# in. A VDC on either board can have 16K or 64K of RAM.
|
|
8
|
+
module Model
|
|
9
|
+
Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :vdc_model, :vdc_ram_kb)
|
|
10
|
+
|
|
11
|
+
# The C128 and the plastic C128D, PAL: the 8566 VIC-IIe, 6526 CIAs,
|
|
12
|
+
# the 6581 SID and the 8563 VDC with 16K.
|
|
13
|
+
C128 = Profile.new(name: "c128", vic_model: :mos8566, cia_model: :mos6526, sid_model: :mos6581,
|
|
14
|
+
region: Region::PAL, vdc_model: :mos8563, vdc_ram_kb: 16)
|
|
15
|
+
|
|
16
|
+
# The NTSC C128, with the 8564.
|
|
17
|
+
C128_NTSC = Profile.new(name: "c128ntsc", vic_model: :mos8564, cia_model: :mos6526, sid_model: :mos6581,
|
|
18
|
+
region: Region::NTSC, vdc_model: :mos8563, vdc_ram_kb: 16)
|
|
19
|
+
|
|
20
|
+
# The metal C128DCR, PAL: 6526A CIAs, the 8580 SID and the 8568 VDC
|
|
21
|
+
# with 64K.
|
|
22
|
+
DCR = Profile.new(name: "c128dcr", vic_model: :mos8566, cia_model: :mos6526a, sid_model: :mos8580,
|
|
23
|
+
region: Region::PAL, vdc_model: :mos8568, vdc_ram_kb: 64)
|
|
24
|
+
|
|
25
|
+
# The NTSC C128DCR, with the 8564.
|
|
26
|
+
DCR_NTSC = Profile.new(name: "c128dcrntsc", vic_model: :mos8564, cia_model: :mos6526a, sid_model: :mos8580,
|
|
27
|
+
region: Region::NTSC, vdc_model: :mos8568, vdc_ram_kb: 64)
|
|
28
|
+
|
|
29
|
+
ALL = [C128, C128_NTSC, DCR, DCR_NTSC].freeze
|
|
30
|
+
|
|
31
|
+
def self.named(name)
|
|
32
|
+
model = ALL.find { |candidate| candidate.name == name }
|
|
33
|
+
raise ArgumentError, "no C128 model named #{name}" if model.nil?
|
|
34
|
+
|
|
35
|
+
model
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# Following the MMU between C128 and C64 mode: the KERNAL traps go to
|
|
6
|
+
# the KERNAL the machine runs, C= held from power-on starts it in C64
|
|
7
|
+
# mode through the C128 KERNAL, and #active_screen tells a front end
|
|
8
|
+
# which screen the mode's editor prints to.
|
|
9
|
+
module Modes
|
|
10
|
+
# Holds C= down from power-on until the C128 KERNAL has taken the
|
|
11
|
+
# machine to C64 mode, as a user does to start a C128 in C64 mode.
|
|
12
|
+
# #on_init's handlers then wait for the C64 KERNAL's boot.
|
|
13
|
+
def hold_commodore_key
|
|
14
|
+
keyboard.press(:cbm)
|
|
15
|
+
@holding_commodore = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The screen the machine prints to: in C128 mode the screen editor's,
|
|
19
|
+
# :vdc while bit 7 of its 40/80 flag at $D7 is set, as GRAPHIC 5,
|
|
20
|
+
# ESC X and a reset with 40/80 DISPLAY down leave it, or :vic. C64
|
|
21
|
+
# mode has the VIC-IIe's alone.
|
|
22
|
+
def active_screen = mode == :c128 && ram.peek(0xd7).anybits?(0x80) ? :vdc : :vic
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# The KERNAL the traps stand in for: the C128's in C128 mode, the C64's
|
|
27
|
+
# in C64 mode.
|
|
28
|
+
def trap_layout = mode == :c128 ? KernalTrap::C128_LAYOUT : KernalTrap::C64_LAYOUT
|
|
29
|
+
|
|
30
|
+
# The machine went to C64 mode or came back: the traps move to the
|
|
31
|
+
# KERNAL it now runs. C= held from power-on lets go, and #on_init's
|
|
32
|
+
# handlers wait for the C64 KERNAL to boot.
|
|
33
|
+
def mode_changed
|
|
34
|
+
layout = trap_layout
|
|
35
|
+
@capture_output&.layout = layout
|
|
36
|
+
if @drive
|
|
37
|
+
remove_kernal_traps
|
|
38
|
+
install_kernal_traps(layout)
|
|
39
|
+
end
|
|
40
|
+
return unless @holding_commodore && mode == :c64
|
|
41
|
+
|
|
42
|
+
keyboard.release(:cbm)
|
|
43
|
+
@holding_commodore = false
|
|
44
|
+
@init_threshold = @cycles + Computer::INIT_THRESHOLD if @cycles < @init_threshold
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
MARKER = "C128"
|
|
6
|
+
|
|
7
|
+
# The model, the SID and the mode it resets into of the machine a State
|
|
8
|
+
# from #snapshot was taken of, the arguments C128.new takes.
|
|
9
|
+
def self.setup(state)
|
|
10
|
+
input = Snapshot::StateReader.new(state)
|
|
11
|
+
input.marker(MARKER)
|
|
12
|
+
input.check_stamp
|
|
13
|
+
SavedState.read_setup(input)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A new machine, built as the one a State from #snapshot was taken of,
|
|
17
|
+
# at that state.
|
|
18
|
+
def self.restored(state)
|
|
19
|
+
setup = setup(state)
|
|
20
|
+
new(model: setup[0], sid_model: setup[1], mode: setup[2]).apply_state(state)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The whole machine's state for a snapshot, as Computer's: the
|
|
24
|
+
# machine's own latches, FAST and TEST as they last took hold, the bus with its RAM, the MMU, the chips and
|
|
25
|
+
# the VDC, the CPU, the cartridge, the disk device 8 serves through the
|
|
26
|
+
# traps and a true 1541. The serial lines go back into the bus from the
|
|
27
|
+
# restored CIA 2. What the host holds stays out: the keyboard, the
|
|
28
|
+
# joysticks, CAPS LOCK and 40/80 DISPLAY, the traps and callbacks, and
|
|
29
|
+
# which chip renders.
|
|
30
|
+
module SavedState
|
|
31
|
+
SID_MODELS = %i[mos6581 mos8580].freeze
|
|
32
|
+
MODES = %i[c64 c128].freeze
|
|
33
|
+
|
|
34
|
+
# The model's name, the SID's model and the mode.
|
|
35
|
+
def self.read_setup(input)
|
|
36
|
+
name = input.string
|
|
37
|
+
known = Model::ALL.any? { |model| model.name == name }
|
|
38
|
+
raise Snapshot::FormatError, "the state names a C128 model badline doesn't know" unless known
|
|
39
|
+
|
|
40
|
+
sid_model = SID_MODELS.fetch(input.int)
|
|
41
|
+
[name, sid_model, MODES.fetch(input.int)]
|
|
42
|
+
rescue IndexError
|
|
43
|
+
raise Snapshot::FormatError, "the state names a SID or a mode badline doesn't know"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# How many more on_init handlers the machine a State was taken of had
|
|
47
|
+
# yet to run than this one has. Nil until the machine is restored.
|
|
48
|
+
attr_reader :init_handlers_lost
|
|
49
|
+
|
|
50
|
+
def snapshot
|
|
51
|
+
out = Snapshot::StateWriter.new
|
|
52
|
+
save_state(out)
|
|
53
|
+
out.state
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Takes the machine back to a State from #snapshot, trying it on a new
|
|
57
|
+
# machine first, so one that fails leaves this machine as it was.
|
|
58
|
+
# Raises Snapshot::FormatError for a state of another model or SID.
|
|
59
|
+
def restore(state)
|
|
60
|
+
check_setup(C128.setup(state))
|
|
61
|
+
C128.restored(state)
|
|
62
|
+
apply_state(state)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Puts a State into the machine without trying it on a new one first.
|
|
66
|
+
def apply_state(state)
|
|
67
|
+
input = Snapshot::StateReader.new(state)
|
|
68
|
+
load_state(input)
|
|
69
|
+
raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
|
|
70
|
+
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save_state(out)
|
|
75
|
+
out.marker(MARKER).stamp
|
|
76
|
+
out.string(@model.name).int(SID_MODELS.index(@sid.model)).int(@c64_built ? 0 : 1)
|
|
77
|
+
out.int(@cycles).int(@clock_bits).boolean(@nmi_asserted).boolean(@cartridge_nmi).boolean(@restore_pulse)
|
|
78
|
+
out.boolean(!@pending_keys.nil?)
|
|
79
|
+
out.ints(@pending_keys) if @pending_keys
|
|
80
|
+
out.int(@cycles <= init_threshold ? @init_handlers.length : 0)
|
|
81
|
+
save_cartridge(out)
|
|
82
|
+
@bus.save_state(out)
|
|
83
|
+
@cpu.save_state(out)
|
|
84
|
+
save_trap_drive(out)
|
|
85
|
+
save_serial_bus(out)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_state(input)
|
|
89
|
+
input.marker(MARKER)
|
|
90
|
+
input.check_stamp
|
|
91
|
+
check_setup(SavedState.read_setup(input))
|
|
92
|
+
@cycles = input.int
|
|
93
|
+
@clock_bits = input.int
|
|
94
|
+
@nmi_asserted = input.boolean?
|
|
95
|
+
@cartridge_nmi = input.boolean?
|
|
96
|
+
@restore_pulse = input.boolean?
|
|
97
|
+
@pending_keys = nil
|
|
98
|
+
@pending_keys = input.ints if input.boolean?
|
|
99
|
+
@init_handlers_lost = [input.int - @init_handlers.length, 0].max
|
|
100
|
+
load_cartridge(input)
|
|
101
|
+
@bus.load_state(input)
|
|
102
|
+
push_serial_lines
|
|
103
|
+
@cpu.load_state(input)
|
|
104
|
+
load_trap_drive(input)
|
|
105
|
+
load_serial_bus(input)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def check_setup(setup)
|
|
111
|
+
ours = [@model.name, @sid.model, @c64_built ? :c64 : :c128]
|
|
112
|
+
return if setup == ours
|
|
113
|
+
|
|
114
|
+
raise Snapshot::FormatError, "the state is of a #{setup.join(' with a ')}, not a #{ours.join(' with a ')}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
include SavedState
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class VDC
|
|
6
|
+
# The VDC's own DRAM: 16K of two 4416s (16K by 4) or 64K of 4164s or
|
|
7
|
+
# 4464s, behind the 16-bit addresses the VDC computes.
|
|
8
|
+
#
|
|
9
|
+
# The VDC drives an address onto the DRAM's pins as a row and a
|
|
10
|
+
# column, and R28 bit 4 picks the order (Programmer's Reference Guide,
|
|
11
|
+
# R28): set, for 64K parts, the row is A0-A7 and the column A8-A15;
|
|
12
|
+
# clear, for 4416s, the column pins carry A8, A8, A9-A13 and A15, and
|
|
13
|
+
# A14 goes nowhere. A 4416 has only six column lines and ignores the
|
|
14
|
+
# first and the last column pin. That gives the four layouts
|
|
15
|
+
# VDC/vdcdump's patterns.txt records from real machines: a 16K VDC in
|
|
16
|
+
# 64K mode sees A9-A14, so pages p, p ^ 1 and p ^ $80 are one page,
|
|
17
|
+
# and a 64K VDC in 16K mode sees page p at page
|
|
18
|
+
# (p & $80) | (p & $3E) << 1 | (p & 1) * 3.
|
|
19
|
+
class Memory
|
|
20
|
+
attr_reader :ram
|
|
21
|
+
|
|
22
|
+
def initialize(ram_kb)
|
|
23
|
+
@ram = Array.new(ram_kb * 1024, 0)
|
|
24
|
+
@layouts = [false, true].map { |mode64| Array.new(256) { |page| page_base(page, mode64) }.freeze }
|
|
25
|
+
@pages = @layouts[0]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Selects 64K mode, R28 bit 4, or 16K mode.
|
|
29
|
+
def mode64=(on)
|
|
30
|
+
@pages = @layouts[on ? 1 : 0]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(addr) = @ram[@pages[(addr >> 8) & 0xff] | (addr & 0xff)]
|
|
34
|
+
|
|
35
|
+
def store(addr, value)
|
|
36
|
+
@ram[@pages[(addr >> 8) & 0xff] | (addr & 0xff)] = value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear!
|
|
40
|
+
@ram.fill(0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def page_base(page, mode64)
|
|
46
|
+
pins = mode64 ? page : (page & 0x80) | ((page & 0x3e) << 1) | ((page & 1) * 3)
|
|
47
|
+
return pins << 8 if @ram.length == 0x10000
|
|
48
|
+
|
|
49
|
+
((pins >> 1) & 0x3f) << 8
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class VDC
|
|
6
|
+
# Paints the VDC's scan lines into its display, one line at a time,
|
|
7
|
+
# from the registers as they stand when the line ends. Each pixel of
|
|
8
|
+
# the display is one period of the 16 MHz dot clock, so a pixel of
|
|
9
|
+
# the double-width mode, R25 bit 4, covers two. Window paints the
|
|
10
|
+
# characters of the lines in the displayed rows.
|
|
11
|
+
#
|
|
12
|
+
# The horizontal counter that R2, R34 and R35 compare runs
|
|
13
|
+
# COUNTER_LEAD characters ahead of the character on screen: the soci
|
|
14
|
+
# testprogs set R34 = 6 and R35 = R1 + 8 for one character of border
|
|
15
|
+
# either side of the display, as test01.jpg shows on a real machine.
|
|
16
|
+
# The display holds each line from the end of the horizontal sync, R2
|
|
17
|
+
# plus R3 bits 0-3 on that counter, as a monitor shows it, and its
|
|
18
|
+
# rows from the end of the vertical sync, which starts R7 - 1 rows
|
|
19
|
+
# into the frame (the Programmer's Reference Guide counts R7 "plus 1")
|
|
20
|
+
# and lasts R3 bits 4-7 lines, or 16 for 0. The vertical sync and the
|
|
21
|
+
# characters outside R34 up to R35 are black, and the rest of the
|
|
22
|
+
# border is the background colour, R26 bits 0-3.
|
|
23
|
+
#
|
|
24
|
+
# The display is as wide as a line and as high as a frame, up to
|
|
25
|
+
# MAX_WIDTH by MAX_HEIGHT; lines and pixels past those are left out.
|
|
26
|
+
class Painter
|
|
27
|
+
COUNTER_LEAD = 7
|
|
28
|
+
MIN_LINE_DOTS = 128
|
|
29
|
+
MAX_WIDTH = 1024
|
|
30
|
+
MAX_HEIGHT = 320
|
|
31
|
+
|
|
32
|
+
attr_reader :display, :width, :height, :dirty_lines
|
|
33
|
+
|
|
34
|
+
def initialize(registers, memory)
|
|
35
|
+
@registers = registers
|
|
36
|
+
@window = Window.new(registers, memory)
|
|
37
|
+
@line = Array.new(MAX_WIDTH, 0)
|
|
38
|
+
resize(MIN_LINE_DOTS, 1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Latches the start addresses, and starts the display again when
|
|
42
|
+
# the frame's size has changed.
|
|
43
|
+
def begin_frame(line_dots, frame_lines)
|
|
44
|
+
@window.latch_starts
|
|
45
|
+
width = line_dots.clamp(MIN_LINE_DOTS, MAX_WIDTH)
|
|
46
|
+
height = frame_lines.clamp(1, MAX_HEIGHT)
|
|
47
|
+
resize(width, height) if width != @width || height != @height
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The [left, top, width, height] of the display a monitor shows:
|
|
51
|
+
# all of a line but the horizontal sync, all of a frame but the
|
|
52
|
+
# vertical sync.
|
|
53
|
+
def crop(line_dots, frame_lines, sync_lines)
|
|
54
|
+
regs = @registers
|
|
55
|
+
shown = line_dots - ((regs[3] & 0x0f) * VDC.char_dots(regs))
|
|
56
|
+
[0, 0, shown.clamp(1, @width), (frame_lines - sync_lines).clamp(1, @height)]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The display's size and the window's start addresses. The display
|
|
60
|
+
# starts again cleared, to be painted while the VDC renders.
|
|
61
|
+
def save_state(out)
|
|
62
|
+
out.int(@width).int(@height)
|
|
63
|
+
@window.save_state(out)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_state(input)
|
|
67
|
+
resize(input.int, input.int)
|
|
68
|
+
@window.load_state(input)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def clear_dirty_lines!
|
|
72
|
+
@dirty_lines.fill(false)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def clear!
|
|
76
|
+
@display.fill(0)
|
|
77
|
+
@dirty_lines.fill(true)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Paints the line `line_y` lines after the vertical sync ends.
|
|
81
|
+
# `row` and `row_line` place it in the frame's character rows, or
|
|
82
|
+
# `row` is -1 in the adjust lines after the last row, and `sync` is
|
|
83
|
+
# set in the vertical sync. `frame` counts frames for the blinking.
|
|
84
|
+
def paint(line_y, row, row_line, sync, frame)
|
|
85
|
+
return if line_y >= @height
|
|
86
|
+
|
|
87
|
+
regs = @registers
|
|
88
|
+
char_dots = VDC.char_dots(regs)
|
|
89
|
+
line_dots = (regs[0] + 1) * char_dots
|
|
90
|
+
@line = Array.new(line_dots, 0) if @line.length < line_dots
|
|
91
|
+
line = @line
|
|
92
|
+
line.fill(sync ? 0 : regs[26] & 0x0f, 0, line_dots)
|
|
93
|
+
unless sync
|
|
94
|
+
@window.paint(line, row, row_line, frame) if row >= 0 && row < regs[6]
|
|
95
|
+
blank(line, line_dots, char_dots)
|
|
96
|
+
end
|
|
97
|
+
copy_out(line, line_y, line_dots, char_dots)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def resize(width, height)
|
|
103
|
+
@width = width
|
|
104
|
+
@height = height
|
|
105
|
+
@display = Array.new(width * height, 0)
|
|
106
|
+
@dirty_lines = Array.new(height, true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def sync_end_chars(regs) = regs[2] + (regs[3] & 0x0f) - COUNTER_LEAD
|
|
110
|
+
|
|
111
|
+
def blank(line, line_dots, char_dots)
|
|
112
|
+
first = @registers[34]
|
|
113
|
+
last = @registers[35]
|
|
114
|
+
return if first == last
|
|
115
|
+
|
|
116
|
+
total = line_dots / char_dots
|
|
117
|
+
char = 0
|
|
118
|
+
while char < total
|
|
119
|
+
counter = (char + COUNTER_LEAD) % total
|
|
120
|
+
line.fill(0, char * char_dots, char_dots) unless enabled?(counter, first, last)
|
|
121
|
+
char += 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def enabled?(counter, first, last)
|
|
126
|
+
return counter >= first && counter < last if first < last
|
|
127
|
+
|
|
128
|
+
counter >= first || counter < last
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def copy_out(line, line_y, line_dots, char_dots)
|
|
132
|
+
origin = (sync_end_chars(@registers) * char_dots) % line_dots
|
|
133
|
+
base = line_y * @width
|
|
134
|
+
head = [line_dots - origin, @width].min
|
|
135
|
+
@display[base, head] = line[origin, head]
|
|
136
|
+
tail = [@width - head, origin].min
|
|
137
|
+
@display[base + head, tail] = line[0, tail] if tail.positive?
|
|
138
|
+
@dirty_lines[line_y] = true
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class VDC
|
|
6
|
+
# The VDC's raster: R0 + 1 characters a line, R9 + 1 lines a row,
|
|
7
|
+
# R4 + 1 rows a frame and then R5 more lines, with the frame's size
|
|
8
|
+
# and sync taken as it begins. Each line ends into the Painter while
|
|
9
|
+
# the VDC renders. Interlace, R8, is left out.
|
|
10
|
+
class Raster
|
|
11
|
+
attr_reader :frame, :frame_lines, :sync_lines
|
|
12
|
+
|
|
13
|
+
def initialize(registers, painter)
|
|
14
|
+
@registers = registers
|
|
15
|
+
@painter = painter
|
|
16
|
+
reset!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset!
|
|
20
|
+
@frame = 0
|
|
21
|
+
begin_frame
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The dots of a line, at least Painter::MIN_LINE_DOTS.
|
|
25
|
+
def line_dots
|
|
26
|
+
dots = (@registers[0] + 1) * VDC.char_dots(@registers)
|
|
27
|
+
[dots, Painter::MIN_LINE_DOTS].max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Outside the displayed rows, as status bit 5 reads.
|
|
31
|
+
def vertical_blank? = @adjusting || @row >= @registers[6]
|
|
32
|
+
|
|
33
|
+
def save_state(out)
|
|
34
|
+
out.int(@frame).int(@row).int(@row_line).boolean(@adjusting).int(@frame_line).int(@frame_lines)
|
|
35
|
+
out.int(@sync_start).int(@sync_lines)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def load_state(input)
|
|
39
|
+
@frame = input.int
|
|
40
|
+
@row = input.int
|
|
41
|
+
@row_line = input.int
|
|
42
|
+
@adjusting = input.boolean?
|
|
43
|
+
@frame_line = input.int
|
|
44
|
+
@frame_lines = input.int
|
|
45
|
+
@sync_start = input.int
|
|
46
|
+
@sync_lines = input.int
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def end_line(render)
|
|
50
|
+
paint_line if render
|
|
51
|
+
@frame_line += 1
|
|
52
|
+
@row_line += 1
|
|
53
|
+
if @adjusting
|
|
54
|
+
begin_frame(next_frame: true) if @row_line >= (@registers[5] & 0x1f)
|
|
55
|
+
elsif @row_line > (@registers[9] & 0x1f)
|
|
56
|
+
@row_line = 0
|
|
57
|
+
@row += 1
|
|
58
|
+
end_rows if @row > @registers[4]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def paint_line
|
|
65
|
+
sync = @frame_line >= @sync_start && @frame_line < @sync_start + @sync_lines
|
|
66
|
+
line_y = (@frame_line - @sync_start - @sync_lines) % @frame_lines
|
|
67
|
+
@painter.paint(line_y, @adjusting ? -1 : @row, @row_line, sync, @frame)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def end_rows
|
|
71
|
+
return begin_frame(next_frame: true) if @registers[5].nobits?(0x1f)
|
|
72
|
+
|
|
73
|
+
@adjusting = true
|
|
74
|
+
@row_line = 0
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The vertical sync starts R7 - 1 rows in, as the Programmer's
|
|
78
|
+
# Reference Guide counts R7 "plus 1", and lasts R3 bits 4-7 lines,
|
|
79
|
+
# or 16 for 0.
|
|
80
|
+
def begin_frame(next_frame: false)
|
|
81
|
+
regs = @registers
|
|
82
|
+
@frame += 1 if next_frame
|
|
83
|
+
@row = 0
|
|
84
|
+
@row_line = 0
|
|
85
|
+
@adjusting = false
|
|
86
|
+
@frame_line = 0
|
|
87
|
+
row_lines = (regs[9] & 0x1f) + 1
|
|
88
|
+
@frame_lines = ((regs[4] + 1) * row_lines) + (regs[5] & 0x1f)
|
|
89
|
+
@sync_start = ((regs[7] - 1) % (regs[4] + 1)) * row_lines
|
|
90
|
+
@sync_lines = regs[3] >> 4
|
|
91
|
+
@sync_lines = 16 if @sync_lines.zero?
|
|
92
|
+
@painter.begin_frame(line_dots, @frame_lines)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|