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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/CLAUDE.md +5 -3
  4. data/README.md +59 -4
  5. data/doc/pinned-behaviour.md +75 -0
  6. data/doc/snapshots.md +15 -0
  7. data/lib/badline/address_bus/pla.rb +3 -16
  8. data/lib/badline/address_bus.rb +14 -0
  9. data/lib/badline/c128/banks.rb +35 -0
  10. data/lib/badline/c128/bus.rb +347 -0
  11. data/lib/badline/c128/bus_state.rb +53 -0
  12. data/lib/badline/c128/color_lines.rb +39 -0
  13. data/lib/badline/c128/cpu.rb +10 -0
  14. data/lib/badline/c128/keys.rb +35 -0
  15. data/lib/badline/c128/mmu.rb +182 -0
  16. data/lib/badline/c128/mmu_pages.rb +105 -0
  17. data/lib/badline/c128/model.rb +39 -0
  18. data/lib/badline/c128/modes.rb +48 -0
  19. data/lib/badline/c128/saved_state.rb +120 -0
  20. data/lib/badline/c128/vdc/memory.rb +54 -0
  21. data/lib/badline/c128/vdc/painter.rb +143 -0
  22. data/lib/badline/c128/vdc/raster.rb +97 -0
  23. data/lib/badline/c128/vdc/window.rb +215 -0
  24. data/lib/badline/c128/vdc.rb +268 -0
  25. data/lib/badline/c128.rb +320 -0
  26. data/lib/badline/chrout_trap.rb +3 -0
  27. data/lib/badline/computer/attachments.rb +4 -13
  28. data/lib/badline/computer/kernal_traps.rb +29 -0
  29. data/lib/badline/computer.rb +5 -0
  30. data/lib/badline/frontend/app.rb +34 -21
  31. data/lib/badline/frontend/boot.rb +31 -1
  32. data/lib/badline/frontend/controls.rb +11 -0
  33. data/lib/badline/frontend/keys.rb +28 -1
  34. data/lib/badline/frontend/machine_window.rb +16 -13
  35. data/lib/badline/frontend/menu_pages.rb +6 -1
  36. data/lib/badline/frontend/menu_ports.rb +1 -1
  37. data/lib/badline/frontend/pause_menu.rb +1 -0
  38. data/lib/badline/frontend/screen.rb +33 -8
  39. data/lib/badline/frontend/screens.rb +90 -0
  40. data/lib/badline/frontend/timeline.rb +15 -4
  41. data/lib/badline/frontend.rb +1 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +58 -8
  44. data/lib/badline/kernal_trap/load.rb +1 -1
  45. data/lib/badline/kernal_trap/save.rb +1 -1
  46. data/lib/badline/kernal_trap/serial.rb +5 -0
  47. data/lib/badline/machine.rb +15 -6
  48. data/lib/badline/media.rb +8 -6
  49. data/lib/badline/options/event.rb +5 -1
  50. data/lib/badline/options/family.rb +52 -0
  51. data/lib/badline/options/help.rb +5 -1
  52. data/lib/badline/options/table.rb +7 -1
  53. data/lib/badline/options/validation.rb +14 -1
  54. data/lib/badline/options.rb +20 -19
  55. data/lib/badline/roms/README +27 -0
  56. data/lib/badline/roms/c128/basic.rom +0 -0
  57. data/lib/badline/roms/c128/character.rom +0 -0
  58. data/lib/badline/roms/c128/kernal.rom +0 -0
  59. data/lib/badline/snapshot/container.rb +2 -0
  60. data/lib/badline/snapshot/image.rb +14 -0
  61. data/lib/badline/snapshot.rb +9 -2
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/display_state.rb +12 -0
  64. data/lib/badline/vic/registers.rb +3 -0
  65. data/lib/badline/vic.rb +108 -7
  66. data/lib/badline.rb +1 -0
  67. 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