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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/pinned-behaviour.md +146 -0
  6. data/doc/snapshots.md +13 -0
  7. data/lib/badline/address_bus/pla.rb +130 -0
  8. data/lib/badline/address_bus.rb +9 -88
  9. data/lib/badline/chrout_trap.rb +4 -3
  10. data/lib/badline/computer/attachments.rb +47 -7
  11. data/lib/badline/computer/saved_state.rb +1 -0
  12. data/lib/badline/computer.rb +44 -29
  13. data/lib/badline/control_ports.rb +10 -3
  14. data/lib/badline/core.rb +3 -0
  15. data/lib/badline/datasette.rb +39 -1
  16. data/lib/badline/drive1541/serial_port.rb +5 -8
  17. data/lib/badline/drive1541/sleep.rb +5 -6
  18. data/lib/badline/drive1541.rb +3 -3
  19. data/lib/badline/frontend/app.rb +62 -64
  20. data/lib/badline/frontend/boot.rb +33 -8
  21. data/lib/badline/frontend/controls.rb +12 -6
  22. data/lib/badline/frontend/drive_led.rb +9 -10
  23. data/lib/badline/frontend/frame_rate.rb +4 -4
  24. data/lib/badline/frontend/machine_window.rb +87 -0
  25. data/lib/badline/frontend/menu_media.rb +49 -24
  26. data/lib/badline/frontend/menu_pages.rb +23 -40
  27. data/lib/badline/frontend/menu_ports.rb +69 -0
  28. data/lib/badline/frontend/pacer.rb +6 -5
  29. data/lib/badline/frontend/pause_menu.rb +30 -33
  30. data/lib/badline/frontend/player_screen.rb +1 -1
  31. data/lib/badline/frontend/player_window.rb +1 -1
  32. data/lib/badline/frontend/screen.rb +39 -37
  33. data/lib/badline/frontend/sid_view.rb +1 -1
  34. data/lib/badline/frontend/sound.rb +11 -9
  35. data/lib/badline/frontend/timeline.rb +36 -5
  36. data/lib/badline/frontend.rb +2 -0
  37. data/lib/badline/iec_bus.rb +16 -28
  38. data/lib/badline/kernal_trap/drive.rb +3 -0
  39. data/lib/badline/kernal_trap/file.rb +1 -1
  40. data/lib/badline/kernal_trap/layout.rb +112 -0
  41. data/lib/badline/kernal_trap/load.rb +16 -27
  42. data/lib/badline/kernal_trap/routine.rb +11 -27
  43. data/lib/badline/kernal_trap/save.rb +13 -23
  44. data/lib/badline/kernal_trap/serial.rb +21 -19
  45. data/lib/badline/kernal_trap.rb +1 -0
  46. data/lib/badline/keyboard.rb +17 -14
  47. data/lib/badline/machine.rb +60 -0
  48. data/lib/badline/media/vic20_basic.rb +54 -0
  49. data/lib/badline/media/vic20_cartridge.rb +74 -0
  50. data/lib/badline/media/vic20_media.rb +132 -0
  51. data/lib/badline/media.rb +32 -18
  52. data/lib/badline/options/event.rb +5 -3
  53. data/lib/badline/options/help.rb +11 -3
  54. data/lib/badline/options/table.rb +11 -1
  55. data/lib/badline/options/validation.rb +55 -4
  56. data/lib/badline/options.rb +33 -33
  57. data/lib/badline/roms/README +27 -0
  58. data/lib/badline/roms/vic20/basic.rom +0 -0
  59. data/lib/badline/roms/vic20/character.rom +0 -0
  60. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  61. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  62. data/lib/badline/snapshot/container.rb +2 -0
  63. data/lib/badline/snapshot/image.rb +15 -0
  64. data/lib/badline/snapshot/state.rb +4 -3
  65. data/lib/badline/snapshot.rb +10 -0
  66. data/lib/badline/storage/crt_file.rb +16 -5
  67. data/lib/badline/storage/disk_image.rb +3 -0
  68. data/lib/badline/storage/host_directory.rb +3 -0
  69. data/lib/badline/storage/t64.rb +3 -0
  70. data/lib/badline/storage/tap.rb +38 -4
  71. data/lib/badline/storage/unavailable.rb +2 -0
  72. data/lib/badline/timing.rb +12 -0
  73. data/lib/badline/version.rb +1 -1
  74. data/lib/badline/via/shift_register.rb +43 -16
  75. data/lib/badline/vic/bank.rb +53 -16
  76. data/lib/badline/vic/registers.rb +20 -4
  77. data/lib/badline/vic.rb +34 -15
  78. data/lib/badline/vic20/attachments.rb +103 -0
  79. data/lib/badline/vic20/bus.rb +308 -0
  80. data/lib/badline/vic20/cpu.rb +10 -0
  81. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  82. data/lib/badline/vic20/port_wiring.rb +50 -0
  83. data/lib/badline/vic20/saved_state.rb +152 -0
  84. data/lib/badline/vic20/sound/output.rb +83 -0
  85. data/lib/badline/vic20/sound.rb +233 -0
  86. data/lib/badline/vic20/user_via_ports.rb +47 -0
  87. data/lib/badline/vic20/vic/painter.rb +239 -0
  88. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  89. data/lib/badline/vic20/vic/text_window.rb +71 -0
  90. data/lib/badline/vic20/vic.rb +267 -0
  91. data/lib/badline/vic20.rb +220 -0
  92. metadata +28 -2
  93. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -6,13 +6,14 @@ module Badline
6
6
  class FormatError < StandardError; end
7
7
 
8
8
  SIGNATURE = "C64 CARTRIDGE ".b
9
+ VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
9
10
  CHIP_SIGNATURE = "CHIP".b
10
11
  CHIP_HEADER_SIZE = 0x10
11
12
  RAM_CHIP = 1
12
13
 
13
14
  Chip = Data.define(:chip_type, :bank, :address, :data)
14
15
 
15
- attr_reader :hardware_type, :subtype, :exrom, :game, :name, :chips
16
+ attr_reader :machine, :hardware_type, :subtype, :exrom, :game, :name, :chips
16
17
 
17
18
  # What .write needs of an image: the fields a parsed CRTFile reads.
18
19
  Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
@@ -33,16 +34,19 @@ module Badline
33
34
  header + packets.join
34
35
  end
35
36
 
36
- # `bytes` stands in for the file's contents.
37
- def initialize(path = nil, bytes: nil)
37
+ # `bytes` stands in for the file's contents. `machine` is the one the
38
+ # image has to be for, :c64 or :vic20, or nil for either.
39
+ def initialize(path = nil, bytes: nil, machine: :c64)
38
40
  parse(bytes || File.binread(path))
41
+ return if machine.nil? || @machine == machine
42
+
43
+ raise FormatError, "Cartridge image is for the #{@machine}, not the #{machine}"
39
44
  end
40
45
 
41
46
  private
42
47
 
43
48
  def parse(bytes)
44
- raise FormatError, "Missing CRT signature" unless bytes.start_with?(SIGNATURE)
45
-
49
+ @machine = signed_machine(bytes)
46
50
  @hardware_type = bytes[0x16, 2].unpack1("n")
47
51
  @exrom = bytes.getbyte(0x18)
48
52
  @game = bytes.getbyte(0x19)
@@ -51,6 +55,13 @@ module Badline
51
55
  @chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
52
56
  end
53
57
 
58
+ def signed_machine(bytes)
59
+ return :c64 if bytes.start_with?(SIGNATURE)
60
+ return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
61
+
62
+ raise FormatError, "Missing CRT signature"
63
+ end
64
+
54
65
  # Trailing bytes too short to hold a CHIP header are ignored.
55
66
  def parse_chips(bytes, offset)
56
67
  chips = []
@@ -36,6 +36,9 @@ module Badline
36
36
  @errors = split_error_table
37
37
  end
38
38
 
39
+ # The host file's path, as it was opened.
40
+ attr_reader :path
41
+
39
42
  # Whether the image was opened write-protected.
40
43
  def read_only? = @read_only
41
44
 
@@ -10,6 +10,9 @@ module Badline
10
10
  NO_SYNC = 21
11
11
  ID = "DIR".bytes.freeze
12
12
 
13
+ # The directory's path, as it was opened.
14
+ attr_reader :path
15
+
13
16
  def initialize(path)
14
17
  @path = path
15
18
  end
@@ -20,6 +20,9 @@ module Badline
20
20
  raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
21
21
  end
22
22
 
23
+ # The host file's path, as it was opened.
24
+ attr_reader :path
25
+
23
26
  # What Storage.reopen needs to open the archive again: its path and
24
27
  # its bytes.
25
28
  def save_setup(out)
@@ -3,9 +3,9 @@
3
3
  module Badline
4
4
  module Storage
5
5
  # Raw datasette pulse stream. The header holds the signature, the
6
- # version at $0c and the data size at $10; the data runs from $14, every
7
- # byte the time to the next falling edge on the tape read line, in units
8
- # of 8 cycles.
6
+ # version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
7
+ # data size at $10; the data runs from $14, every byte the time to the
8
+ # next falling edge on the tape read line, in units of 8 cycles.
9
9
  class TAP
10
10
  class FormatError < StandardError; end
11
11
 
@@ -14,7 +14,14 @@ module Badline
14
14
  OVERFLOW = 0x100 * 8
15
15
 
16
16
  # The file it was read from, expanded, and the file's bytes.
17
- attr_reader :version, :path, :bytes
17
+ attr_reader :version, :platform, :path, :bytes
18
+
19
+ # A blank version 1 tape for `platform` (0 C64, 1 VIC-20) at `path`,
20
+ # written to the host file.
21
+ def self.create(path, platform:)
22
+ File.binwrite(path, (SIGNATURE + [1, platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
23
+ new(path)
24
+ end
18
25
 
19
26
  # `bytes` stands in for the host file's contents, as a snapshot
20
27
  # restores the tape it holds.
@@ -26,6 +33,8 @@ module Badline
26
33
  @version = @bytes[0x0c].to_i
27
34
  raise FormatError, "Unsupported TAP version #{@version}" if @version > 1
28
35
 
36
+ @platform = @bytes[0x0d].to_i
37
+
29
38
  @end = tape_end
30
39
  rewind
31
40
  end
@@ -43,6 +52,31 @@ module Badline
43
52
 
44
53
  def end? = @pos >= @end
45
54
 
55
+ # Records a pulse `cycles` long at the position, in place of the rest
56
+ # of the tape: a byte of units of 8 cycles, or a zero and the 24-bit
57
+ # cycle count for a long one (a lone zero, an overflow, on a version 0
58
+ # tape).
59
+ def record_pulse(cycles)
60
+ units = (cycles + 4) / 8
61
+ encoded = if units.between?(1, 0xff) then [units]
62
+ elsif version.zero? then [0]
63
+ else
64
+ count = [cycles, 0xffffff].min
65
+ [0, count & 0xff, (count >> 8) & 0xff, count >> 16]
66
+ end
67
+ @bytes.slice!(@pos..)
68
+ @bytes.concat(encoded)
69
+ @pos = @end = @bytes.length
70
+ end
71
+
72
+ # Writes the tape back to its host file, with the data size the
73
+ # header states.
74
+ def save
75
+ size = @bytes.length - HEADER_SIZE
76
+ @bytes[0x10, 4] = [size & 0xff, (size >> 8) & 0xff, (size >> 16) & 0xff, size >> 24]
77
+ File.binwrite(@path, @bytes.pack("C*"))
78
+ end
79
+
46
80
  # Cycles until the next falling edge, or nil once the tape has run out.
47
81
  def next_pulse
48
82
  return if end?
@@ -6,6 +6,8 @@ module Badline
6
6
  # (Snapshot::StateReader): it finds no files and refuses every write
7
7
  # as a write-protected disk does.
8
8
  class Unavailable
9
+ def path = ""
10
+
9
11
  def read_file(_name, **) = nil
10
12
 
11
13
  def write_file(_name, _bytes, **)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # = Timing
5
+ #
6
+ # What a front end needs to run a machine's frames and show them: the CPU
7
+ # clock, the raster's cycles per line and lines per frame, and the
8
+ # [left, top, width, height] of the raster it shows, and how many of the
9
+ # window's square pixels wide each of its pixels shows. Each machine
10
+ # derives it from its region.
11
+ Timing = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :crop, :pixel_width)
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -13,14 +13,20 @@ module Badline
13
13
  # modes 3 and 7 off whatever drives CB1 from outside.
14
14
  #
15
15
  # Under its own clock the register drives CB1, idling high. Reading or
16
- # writing the data register starts a byte: CB1 falls on the first clock
17
- # tick after the access and rises on the second, and so on, so a byte is
18
- # eight pulses over sixteen ticks. The interrupt flag is set on the tick
19
- # that raises CB1 over the eighth bit, and CB1 then stays high until the
20
- # next access. A tick is a φ2 cycle in modes 2 and 6, which makes a byte
21
- # sixteen cycles, and a timer 2 low byte underflow in modes 1, 4 and 5,
22
- # which makes it sixteen underflows. Mode 4 never stops and never
23
- # interrupts: its counter is disabled, and the byte goes round and round.
16
+ # writing the data register starts a byte, but only while the register
17
+ # is idle and enabled: an access in mode 0, or one before the eighth bit
18
+ # is in, leaves the count alone. CB1 falls on the first clock tick of
19
+ # the byte and rises on the second, and so on, so a byte is eight pulses
20
+ # over sixteen ticks. The interrupt flag is set on the tick that raises
21
+ # CB1 over the eighth bit, and CB1 then stays high until the next
22
+ # access.
23
+ #
24
+ # In modes 2 and 6 a tick is a φ2 cycle, from the second cycle after
25
+ # the access on, which makes a byte sixteen cycles. In modes 1, 4 and 5
26
+ # it comes two cycles after each timer 2 low byte underflow, on the
27
+ # cycle after the low byte reloads, which makes a byte sixteen
28
+ # underflows. Mode 4 never stops and never interrupts: its counter is
29
+ # disabled, and the byte goes round and round.
24
30
  #
25
31
  # Under an external clock the register shifts on every CB1 edge, access
26
32
  # or not. The access only arms the counter: the flag is set on the eighth
@@ -43,6 +49,10 @@ module Badline
43
49
  @data = 0x00
44
50
  # Bits still to count before the flag, zero when the counter is idle.
45
51
  @bits = 0
52
+ # The clock's last two cycles, newest in bit 0: timer 2 low byte
53
+ # underflows in modes 1, 4 and 5, and whether the byte has started
54
+ # in modes 2 and 6.
55
+ @delay = 0
46
56
  @clock = true
47
57
  @cb2 = true
48
58
  # Nothing drives CB2 from outside, so it floats high.
@@ -57,10 +67,13 @@ module Badline
57
67
  @data = value & 0xff
58
68
  end
59
69
 
60
- # Every read or write of the data register (re)starts the count of
61
- # eight bits.
70
+ # A read or write of the data register, which starts a byte if the
71
+ # register is idle and enabled.
62
72
  def access!
73
+ return if @mode == DISABLED || @bits.nonzero?
74
+
63
75
  @bits = 8
76
+ @delay = 0 if @mode == IN_PHI2 || @mode == OUT_PHI2
64
77
  end
65
78
 
66
79
  # Whether the register is counting bits off its own clock, which
@@ -68,20 +81,21 @@ module Badline
68
81
  def clocking? = @bits.nonzero? && internal_clock?
69
82
 
70
83
  def save_state(out)
71
- out.int(@mode).int(@data).int(@bits).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
84
+ out.int(@mode).int(@data).int(@bits).int(@delay).boolean(@clock).boolean(@cb2).boolean(@cb2_input)
72
85
  end
73
86
 
74
87
  def load_state(input)
75
88
  @mode = input.int
76
89
  @data = input.int
77
90
  @bits = input.int
91
+ @delay = input.schema > 4 ? input.int : 0
78
92
  @clock = input.boolean?
79
93
  @cb2 = input.boolean?
80
94
  @cb2_input = input.boolean?
81
95
  end
82
96
 
83
97
  # Everything the register holds, for comparing it at two points.
84
- def state = [@mode, @data, @bits, @clock, @cb2, @cb2_input]
98
+ def state = [@mode, @data, @bits, @delay, @clock, @cb2, @cb2_input]
85
99
 
86
100
  # The CB1 level the register drives, or nil when CB1 is an input.
87
101
  def cb1_output
@@ -96,11 +110,9 @@ module Badline
96
110
  # Clocked once per φ2 cycle, with whether timer 2's low byte underflowed
97
111
  # on it. Returns true on the cycle that sets the interrupt flag.
98
112
  def cycle!(t2_low_underflow)
99
- return false if @bits.zero?
100
-
101
113
  case @mode
102
- when IN_PHI2, OUT_PHI2 then tick
103
- when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_low_underflow && tick
114
+ when IN_PHI2, OUT_PHI2 then @bits.nonzero? && phi2_tick
115
+ when IN_T2, OUT_T2, OUT_FREE_RUNNING then t2_tick(t2_low_underflow)
104
116
  else false
105
117
  end
106
118
  end
@@ -123,6 +135,21 @@ module Badline
123
135
  @mode != DISABLED && @mode != IN_EXTERNAL && @mode != OUT_EXTERNAL
124
136
  end
125
137
 
138
+ # A φ2 cycle of a byte: every one but the first ticks.
139
+ def phi2_tick
140
+ started = @delay.nonzero?
141
+ @delay = 1
142
+ started && tick
143
+ end
144
+
145
+ # Ticks on the underflow from two cycles back, while a byte is under
146
+ # way.
147
+ def t2_tick(underflow)
148
+ due = @delay.anybits?(2)
149
+ @delay = ((@delay << 1) & 2) | (underflow ? 1 : 0)
150
+ due && @bits.nonzero? && tick
151
+ end
152
+
126
153
  # One half period of the register's own clock: a falling edge puts a
127
154
  # bit out, and a rising one takes a bit in and counts it.
128
155
  def tick
@@ -5,44 +5,81 @@ module Badline
5
5
  # The 16K window the VIC sees into RAM, selected by CIA2 $DD00, in the
6
6
  # bank a +256K picks for it. Character
7
7
  # ROM shadows $1000-$1FFF in banks 0 and 2.
8
+ #
9
+ # The machine's bus pushes in what the VIC sees through #connect,
10
+ # #map_character_rom and #map whenever it changes. Until then the bank
11
+ # has a CIA 2, RAM, colour RAM and character ROM of its own.
8
12
  class Bank
9
13
  include Addressable
10
14
 
11
15
  # The CIA2 port A bank bits are inverted: %11 selects bank 0.
12
16
  BANK_STARTS = [0xc000, 0x8000, 0x4000, 0x0000].freeze
13
17
 
14
- attr_reader :address_bus
18
+ attr_reader :ram, :color_ram
15
19
 
16
20
  # The bank lines as #sample_lines last saw them.
17
21
  attr_accessor :lines
18
22
 
19
- def initialize(address_bus = nil)
23
+ def initialize(vic)
20
24
  addressable_at(0x0000, length: 2**14)
21
- @address_bus = address_bus || AddressBus.new
25
+ connect(cia2: CIA.new(start: 0xdd00), color_ram: ColorMemory.new(vic))
26
+ map_character_rom(ROM.load("character.rom", 0xd000))
27
+ @base = 0
28
+ @starts = BANK_STARTS
29
+ map(Memory.new([], length: 2**16, start: 0))
22
30
  @lines = 0b11
23
31
  @swapped = false
24
32
  end
25
33
 
34
+ # CIA 2, whose port A picks the bank, and the colour RAM, read from
35
+ # the address +color_base+ gives offset 0.
36
+ def connect(cia2:, color_ram:, color_base: 0xd800)
37
+ @cia2 = cia2
38
+ @color_ram = color_ram
39
+ @color_base = color_base
40
+ end
41
+
42
+ # The character ROM, read from the address +base+ gives offset 0. It
43
+ # shows in the banks whose low line is high, unless +shadow+ is false.
44
+ def map_character_rom(rom, base: 0xc000, shadow: true)
45
+ @character_rom = rom
46
+ @character_base = base
47
+ @shadow_mask = shadow ? 0b01 : 0b00
48
+ end
49
+
50
+ # The RAM the VIC's 64K starts at +base+ in, and the cartridge's
51
+ # Ultimax lines in each half of the cycle with the ROMH bank they
52
+ # show.
53
+ def map(ram, base: 0, phi1_ultimax: false, ultimax: false, romh: nil)
54
+ @ram = ram
55
+ unless base == @base
56
+ @base = base
57
+ @starts = BANK_STARTS.map { |start| base + start }.freeze
58
+ end
59
+ @phi1_ultimax = phi1_ultimax
60
+ @ultimax = ultimax
61
+ @romh = romh
62
+ end
63
+
26
64
  # On the 8565, a port A write that swaps the two bank lines, driving
27
65
  # one high as the other goes low, shows the VIC both lines low for the
28
66
  # cycle after it: every access in that cycle reads bank 3. A swap that
29
67
  # the DDR makes, releasing a line to float high, shows nothing.
30
68
  # Called once a cycle, ahead of the cycle's accesses.
31
69
  def sample_lines
32
- cia2 = @address_bus.cia2
33
- lines = cia2.port_a_lines & 0b11
34
- @swapped = swap?(@lines, lines) && cia2.port_registers[2].allbits?(0b11)
70
+ lines = @cia2.port_a_lines & 0b11
71
+ @swapped = swap?(@lines, lines) && @cia2.port_registers[2].allbits?(0b11)
35
72
  @lines = lines
36
73
  end
37
74
 
38
75
  def peek(offset)
39
- return ultimax_peek(offset) if @address_bus.phi1_ultimax
76
+ return ultimax_peek(offset) if @phi1_ultimax
40
77
 
41
78
  bits = bank_switch_register
42
- if bits.allbits?(0b01) && (offset & 0xf000) == 0x1000
43
- @address_bus.character_rom.peek(0xc000 + offset)
79
+ if (bits & @shadow_mask) == 0b01 && (offset & 0xf000) == 0x1000
80
+ @character_rom.peek(@character_base + offset)
44
81
  else
45
- @address_bus.video_ram.peek(BANK_STARTS[bits] + offset)
82
+ @ram.peek(@starts[bits] + offset)
46
83
  end
47
84
  end
48
85
 
@@ -50,11 +87,11 @@ module Badline
50
87
  # memory configuration the CPU does in that half. No cartridge drives
51
88
  # Ultimax mode in the first half alone, so otherwise it reads as #peek.
52
89
  def peek_phi2(offset)
53
- @address_bus.ultimax ? ultimax_peek(offset) : peek(offset)
90
+ @ultimax ? ultimax_peek(offset) : peek(offset)
54
91
  end
55
92
 
56
93
  def peek_color(offset)
57
- address_bus.color_ram.nibble(0xd800 + offset)
94
+ @color_ram.nibble(@color_base + offset)
58
95
  end
59
96
 
60
97
  def poke(_addr, _value)
@@ -63,7 +100,7 @@ module Badline
63
100
 
64
101
  # True if the VIC reads the character ROM at this offset.
65
102
  def character_rom?(offset)
66
- !@address_bus.phi1_ultimax && bank_switch_register.allbits?(0b01) && (offset & 0xf000) == 0x1000
103
+ !@phi1_ultimax && (bank_switch_register & @shadow_mask) == 0b01 && (offset & 0xf000) == 0x1000
67
104
  end
68
105
 
69
106
  def start
@@ -75,11 +112,11 @@ module Badline
75
112
  # In Ultimax mode the cartridge ROMH replaces the character ROM
76
113
  # shadow, visible at $3000-$3FFF of the window.
77
114
  def ultimax_peek(offset)
78
- romh = @address_bus.cartridge.romh
115
+ romh = @romh
79
116
  if romh && offset.allbits?(0x3000)
80
117
  romh.peek(0xe000 + (offset & 0x1fff))
81
118
  else
82
- @address_bus.video_ram.peek(BANK_STARTS[bank_switch_register] + offset)
119
+ @ram.peek(@starts[bank_switch_register] + offset)
83
120
  end
84
121
  end
85
122
 
@@ -88,7 +125,7 @@ module Badline
88
125
  def bank_switch_register
89
126
  return 0 if @swapped
90
127
 
91
- @address_bus.cia2.port_a_lines & 0b11
128
+ @cia2.port_a_lines & 0b11
92
129
  end
93
130
  end
94
131
  end
@@ -69,7 +69,13 @@ module Badline
69
69
  # $D02D: Sprite 6 color
70
70
  # $D02E: Sprite 7 color
71
71
  #
72
- # $D02F-$D03F: Not in use, always reads 0xff
72
+ # == VIC-IIe (8566 and 8564)
73
+ #
74
+ # $D02F: Extra keyboard lines K0-K2 in bits 0-2, bits 3-7 read 1
75
+ # $D030: FAST (2 MHz) in bit 0 and TEST in bit 1, bits 2-7 read 1
76
+ #
77
+ # $D02F-$D03F: Not in use on the 6569 and 8565, always reads 0xff,
78
+ # and $D031-$D03F on the VIC-IIe
73
79
  # $D040-$D3FF: Repeat $D0000 to $D03F every 64 bytes
74
80
  class Registers
75
81
  include IntegerHelper
@@ -77,10 +83,12 @@ module Badline
77
83
  # The raw register bytes, for the per-column reads on the hot path.
78
84
  attr_reader :bytes
79
85
 
80
- # Every register powers on as zero, as in VICE.
81
- def initialize
86
+ # Every register powers on as zero, as in VICE. A VIC-IIe reads
87
+ # $D02F and $D030 back as registers.
88
+ def initialize(iie: false)
82
89
  @bytes = Array.new(2**6, 0)
83
90
  @irq_line = false
91
+ @iie = iie
84
92
  end
85
93
 
86
94
  # True while any enabled latch bit is set in $D019/$D01A. Cached and
@@ -95,7 +103,9 @@ module Badline
95
103
  when 0x18 then @bytes[reg] | 0x01 # bit 0 unused, reads 1
96
104
  when 0x1a, 0x20..0x2e then @bytes[reg] | 0xf0
97
105
  when 0x1e, 0x1f then read_clear(reg) # collision registers clear on read
98
- when 0x2f..0x3f then 0xff
106
+ when 0x2f then @iie ? @bytes[reg] | 0xf8 : 0xff
107
+ when 0x30 then @iie ? @bytes[reg] | 0xfc : 0xff
108
+ when 0x31..0x3f then 0xff
99
109
  else @bytes[reg]
100
110
  end
101
111
  end
@@ -134,6 +144,12 @@ module Badline
134
144
  def border = @bytes[0x20] & 0x0f
135
145
  def background(index = 0) = @bytes[0x21 + index] & 0x0f
136
146
 
147
+ # The VIC-IIe's $D02F and $D030 bits. The 6569 and 8565 drive no
148
+ # extra keyboard lines and run at 1 MHz.
149
+ def extra_keyboard_lines = @iie ? @bytes[0x2f] & 0x07 : 0x07
150
+ def fast? = @iie && @bytes[0x30].anybits?(0x01)
151
+ def test? = @iie && @bytes[0x30].anybits?(0x02)
152
+
137
153
  def raster_target = uint16(@bytes[0x12], (@bytes[0x11] & 0x80) >> 7)
138
154
 
139
155
  def latch_irq!(bit)
data/lib/badline/vic.rb CHANGED
@@ -11,10 +11,20 @@ module Badline
11
11
  include Addressable
12
12
  include IntegerHelper
13
13
 
14
- # The 6569 of the breadbin C64, and the HMOS 8565 of the C64C.
15
- MODELS = %i[mos6569 mos8565].freeze
16
-
17
- attr_reader :address_bus, :display, :width, :height, :vic_bank, :column,
14
+ # The 6569 of the breadbin C64, the HMOS 8565 of the C64C, and the
15
+ # C128's VIC-IIe, the 8566 (PAL) and 8564 (NTSC). The VIC-IIe draws as
16
+ # the 8565 and adds the $D02F and $D030 registers.
17
+ MODELS = %i[mos6569 mos8565 mos8566 mos8564].freeze
18
+
19
+ # The sixteen colours #display's palette indices stand for, as RGB.
20
+ PALETTE = [
21
+ 0x000000, 0xffffff, 0x924a40, 0x84c5cc,
22
+ 0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
23
+ 0x675200, 0xc33d00, 0xc18178, 0x606060,
24
+ 0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
25
+ ].freeze
26
+
27
+ attr_reader :display, :width, :height, :vic_bank, :column,
18
28
  :rasterline, :dirty_lines, :model, :region
19
29
 
20
30
  # The parts a VICE snapshot reads and sets.
@@ -76,19 +86,20 @@ module Badline
76
86
  SPRITE_BA_FIRST = 54
77
87
  SPRITE_BA_LENGTH = 5
78
88
 
79
- def initialize(address_bus = nil, debug: false, model: :mos6569, region: Region::PAL)
89
+ def initialize(debug: false, model: :mos6569, region: Region::PAL)
80
90
  raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
81
91
 
82
92
  addressable_at(0xd000, length: 2**10)
83
93
  @model = model
94
+ @iie = %i[mos8566 mos8564].include?(model)
95
+ @core = model == :mos6569 ? :mos6569 : :mos8565
84
96
  @region = region
85
- @lightpen_extra = model == :mos8565 ? 1 : 2
86
- @grey_dots = model == :mos8565
87
- @delayed_fetch = model == :mos8565
88
- @dma_delay_idle = model == :mos6569
89
- @bank_swaps = model == :mos8565
90
- @address_bus = address_bus || AddressBus.new
91
- @vic_bank = VIC::Bank.new(@address_bus)
97
+ @lightpen_extra = @core == :mos8565 ? 1 : 2
98
+ @grey_dots = @core == :mos8565
99
+ @delayed_fetch = @core == :mos8565
100
+ @dma_delay_idle = @core == :mos6569
101
+ @bank_swaps = @core == :mos8565
102
+ @vic_bank = VIC::Bank.new(self)
92
103
  @debug = debug
93
104
 
94
105
  @columns_per_line = region.cycles_per_line
@@ -114,12 +125,12 @@ module Badline
114
125
  # registers, the raster position and the fetch and sprite state it
115
126
  # starts with. The display keeps its buffers, cleared to black.
116
127
  def power_on!
117
- @registers = VIC::Registers.new
128
+ @registers = VIC::Registers.new(iie: @iie)
118
129
  @register_bytes = @registers.bytes
119
130
  @display_state = VIC::DisplayState.new(@registers, @last_column)
120
- @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @model, region: @region)
131
+ @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @core, region: @region)
121
132
  @sequencer.render = @render
122
- @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @model, region: @region)
133
+ @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @core, region: @region)
123
134
  @display.fill(0)
124
135
  @lines.each { |line| line.fill(0) }
125
136
  @dirty_lines.fill(true)
@@ -222,6 +233,12 @@ module Badline
222
233
  @registers.irq_line?
223
234
  end
224
235
 
236
+ # The VIC-IIe's $D02F lines K0-K2 and $D030 bits, for the machine to
237
+ # read. The 6569 and 8565 give 7, false and false.
238
+ def extra_keyboard_lines = @registers.extra_keyboard_lines
239
+ def fast? = @registers.fast?
240
+ def test? = @registers.test?
241
+
225
242
  def peek(addr)
226
243
  i = offset_of(addr) % (2**6)
227
244
  case i
@@ -363,6 +380,8 @@ module Badline
363
380
  @dirty_lines.fill(false)
364
381
  end
365
382
 
383
+ def palette = PALETTE
384
+
366
385
  private
367
386
 
368
387
  def load_beam(input)