badline 0.9.0 → 0.10.1

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/library.md +15 -1
  6. data/doc/pinned-behaviour.md +146 -0
  7. data/doc/snapshots.md +17 -3
  8. data/lib/badline/address_bus/fittings.rb +38 -0
  9. data/lib/badline/address_bus/pla.rb +160 -0
  10. data/lib/badline/address_bus/roms.rb +12 -5
  11. data/lib/badline/address_bus.rb +15 -90
  12. data/lib/badline/chrout_trap.rb +4 -3
  13. data/lib/badline/computer/attachments.rb +47 -7
  14. data/lib/badline/computer/saved_state.rb +1 -0
  15. data/lib/badline/computer.rb +52 -37
  16. data/lib/badline/control_ports.rb +10 -3
  17. data/lib/badline/core.rb +3 -0
  18. data/lib/badline/datasette.rb +39 -1
  19. data/lib/badline/drive1541/serial_port.rb +5 -8
  20. data/lib/badline/drive1541/sleep.rb +5 -6
  21. data/lib/badline/drive1541.rb +3 -3
  22. data/lib/badline/frontend/app.rb +62 -64
  23. data/lib/badline/frontend/boot.rb +33 -8
  24. data/lib/badline/frontend/controls.rb +12 -6
  25. data/lib/badline/frontend/drive_led.rb +9 -10
  26. data/lib/badline/frontend/frame_rate.rb +4 -4
  27. data/lib/badline/frontend/machine_window.rb +87 -0
  28. data/lib/badline/frontend/menu_media.rb +49 -24
  29. data/lib/badline/frontend/menu_pages.rb +23 -40
  30. data/lib/badline/frontend/menu_ports.rb +69 -0
  31. data/lib/badline/frontend/pacer.rb +6 -5
  32. data/lib/badline/frontend/pause_menu.rb +30 -33
  33. data/lib/badline/frontend/player_screen.rb +1 -1
  34. data/lib/badline/frontend/player_window.rb +1 -1
  35. data/lib/badline/frontend/screen.rb +39 -37
  36. data/lib/badline/frontend/sid_view.rb +1 -1
  37. data/lib/badline/frontend/sound.rb +11 -9
  38. data/lib/badline/frontend/timeline.rb +36 -5
  39. data/lib/badline/frontend.rb +2 -0
  40. data/lib/badline/iec_bus.rb +16 -28
  41. data/lib/badline/kernal_trap/drive.rb +3 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +112 -0
  44. data/lib/badline/kernal_trap/load.rb +16 -27
  45. data/lib/badline/kernal_trap/routine.rb +11 -27
  46. data/lib/badline/kernal_trap/save.rb +13 -23
  47. data/lib/badline/kernal_trap/serial.rb +21 -19
  48. data/lib/badline/kernal_trap.rb +1 -0
  49. data/lib/badline/keyboard.rb +28 -14
  50. data/lib/badline/machine.rb +60 -0
  51. data/lib/badline/media/vic20_basic.rb +54 -0
  52. data/lib/badline/media/vic20_cartridge.rb +74 -0
  53. data/lib/badline/media/vic20_media.rb +132 -0
  54. data/lib/badline/media.rb +32 -18
  55. data/lib/badline/model.rb +29 -12
  56. data/lib/badline/options/event.rb +5 -3
  57. data/lib/badline/options/help.rb +11 -3
  58. data/lib/badline/options/table.rb +12 -2
  59. data/lib/badline/options/validation.rb +55 -4
  60. data/lib/badline/options.rb +33 -33
  61. data/lib/badline/roms/README +51 -0
  62. data/lib/badline/roms/c64gs.rom +0 -0
  63. data/lib/badline/roms/kernal-pet64.rom +0 -0
  64. data/lib/badline/roms/vic20/basic.rom +0 -0
  65. data/lib/badline/roms/vic20/character.rom +0 -0
  66. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  67. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  68. data/lib/badline/snapshot/container.rb +2 -0
  69. data/lib/badline/snapshot/image.rb +15 -0
  70. data/lib/badline/snapshot/setup.rb +15 -10
  71. data/lib/badline/snapshot/state.rb +5 -3
  72. data/lib/badline/snapshot/vice.rb +2 -1
  73. data/lib/badline/snapshot.rb +10 -0
  74. data/lib/badline/storage/crt_file.rb +16 -5
  75. data/lib/badline/storage/disk_image.rb +3 -0
  76. data/lib/badline/storage/host_directory.rb +3 -0
  77. data/lib/badline/storage/t64.rb +3 -0
  78. data/lib/badline/storage/tap.rb +38 -4
  79. data/lib/badline/storage/unavailable.rb +2 -0
  80. data/lib/badline/timing.rb +12 -0
  81. data/lib/badline/version.rb +1 -1
  82. data/lib/badline/via/shift_register.rb +43 -16
  83. data/lib/badline/vic/bank.rb +55 -17
  84. data/lib/badline/vic/registers.rb +20 -4
  85. data/lib/badline/vic.rb +48 -15
  86. data/lib/badline/vic20/attachments.rb +103 -0
  87. data/lib/badline/vic20/bus.rb +308 -0
  88. data/lib/badline/vic20/cpu.rb +10 -0
  89. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  90. data/lib/badline/vic20/port_wiring.rb +50 -0
  91. data/lib/badline/vic20/saved_state.rb +152 -0
  92. data/lib/badline/vic20/sound/output.rb +83 -0
  93. data/lib/badline/vic20/sound.rb +233 -0
  94. data/lib/badline/vic20/user_via_ports.rb +47 -0
  95. data/lib/badline/vic20/vic/painter.rb +239 -0
  96. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  97. data/lib/badline/vic20/vic/text_window.rb +71 -0
  98. data/lib/badline/vic20/vic.rb +267 -0
  99. data/lib/badline/vic20.rb +220 -0
  100. metadata +31 -2
  101. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -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
@@ -73,13 +110,14 @@ module Badline
73
110
  private
74
111
 
75
112
  # In Ultimax mode the cartridge ROMH replaces the character ROM
76
- # shadow, visible at $3000-$3FFF of the window.
113
+ # shadow, visible at $3000-$3FFF of the window. The MAX is in
114
+ # Ultimax mode with no cartridge too.
77
115
  def ultimax_peek(offset)
78
- romh = @address_bus.cartridge.romh
116
+ romh = @romh
79
117
  if romh && offset.allbits?(0x3000)
80
118
  romh.peek(0xe000 + (offset & 0x1fff))
81
119
  else
82
- @address_bus.video_ram.peek(BANK_STARTS[bank_switch_register] + offset)
120
+ @ram.peek(@starts[bank_switch_register] + offset)
83
121
  end
84
122
  end
85
123
 
@@ -88,7 +126,7 @@ module Badline
88
126
  def bank_switch_register
89
127
  return 0 if @swapped
90
128
 
91
- @address_bus.cia2.port_a_lines & 0b11
129
+ @cia2.port_a_lines & 0b11
92
130
  end
93
131
  end
94
132
  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,12 +11,37 @@ 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
+ # The sixteen colours' luminances, out of 32, as Philip Timmermann
28
+ # (Pepto) measured them on the 6569.
29
+ LUMINANCES = [0, 32, 10, 20, 12, 16, 8, 24, 12, 8, 16, 10, 15, 24, 15, 20].freeze
30
+
31
+ # The sixteen colours on a green monochrome monitor, which shows only
32
+ # their luminance, as the PET 64's does.
33
+ GREEN_PALETTE = LUMINANCES.map do |luminance|
34
+ green = luminance * 255 / 32
35
+ ((green / 4) << 16) | (green << 8) | (green / 4)
36
+ end.freeze
37
+
38
+ attr_reader :display, :width, :height, :vic_bank, :column,
18
39
  :rasterline, :dirty_lines, :model, :region
19
40
 
41
+ # The RGB colours #display's palette indices stand for: PALETTE, or
42
+ # GREEN_PALETTE on a monochrome monitor.
43
+ attr_accessor :palette
44
+
20
45
  # The parts a VICE snapshot reads and sets.
21
46
  attr_reader :registers, :display_state, :sequencer, :sprites, :character_buffer, :color_buffer,
22
47
  :fetch_d011
@@ -76,19 +101,21 @@ module Badline
76
101
  SPRITE_BA_FIRST = 54
77
102
  SPRITE_BA_LENGTH = 5
78
103
 
79
- def initialize(address_bus = nil, debug: false, model: :mos6569, region: Region::PAL)
104
+ def initialize(debug: false, model: :mos6569, region: Region::PAL)
80
105
  raise ArgumentError, "unknown VIC-II model #{model}" unless MODELS.include?(model)
81
106
 
82
107
  addressable_at(0xd000, length: 2**10)
83
108
  @model = model
109
+ @iie = %i[mos8566 mos8564].include?(model)
110
+ @core = model == :mos6569 ? :mos6569 : :mos8565
84
111
  @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)
112
+ @palette = PALETTE
113
+ @lightpen_extra = @core == :mos8565 ? 1 : 2
114
+ @grey_dots = @core == :mos8565
115
+ @delayed_fetch = @core == :mos8565
116
+ @dma_delay_idle = @core == :mos6569
117
+ @bank_swaps = @core == :mos8565
118
+ @vic_bank = VIC::Bank.new(self)
92
119
  @debug = debug
93
120
 
94
121
  @columns_per_line = region.cycles_per_line
@@ -114,12 +141,12 @@ module Badline
114
141
  # registers, the raster position and the fetch and sprite state it
115
142
  # starts with. The display keeps its buffers, cleared to black.
116
143
  def power_on!
117
- @registers = VIC::Registers.new
144
+ @registers = VIC::Registers.new(iie: @iie)
118
145
  @register_bytes = @registers.bytes
119
146
  @display_state = VIC::DisplayState.new(@registers, @last_column)
120
- @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @model, region: @region)
147
+ @sequencer = VIC::Sequencer.new(@width, @registers, @vic_bank, model: @core, region: @region)
121
148
  @sequencer.render = @render
122
- @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @model, region: @region)
149
+ @sprites = VIC::Sprites.new(@registers, @vic_bank, @width, model: @core, region: @region)
123
150
  @display.fill(0)
124
151
  @lines.each { |line| line.fill(0) }
125
152
  @dirty_lines.fill(true)
@@ -222,6 +249,12 @@ module Badline
222
249
  @registers.irq_line?
223
250
  end
224
251
 
252
+ # The VIC-IIe's $D02F lines K0-K2 and $D030 bits, for the machine to
253
+ # read. The 6569 and 8565 give 7, false and false.
254
+ def extra_keyboard_lines = @registers.extra_keyboard_lines
255
+ def fast? = @registers.fast?
256
+ def test? = @registers.test?
257
+
225
258
  def peek(addr)
226
259
  i = offset_of(addr) % (2**6)
227
260
  case i
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # What plugs into the VIC-20 besides its chips: cartridge ROM in the
6
+ # expansion blocks, the disk device 8 serves through the KERNAL traps,
7
+ # and a true 1541 on the serial bus.
8
+ module Attachments
9
+ # The cycle by which a 1541 switched on with the machine has run its
10
+ # DOS's reset, the RAM test and the ROM checksum, and waits on the
11
+ # bus, with a margin: about 1.1 million of the VIC-20's cycles.
12
+ DRIVE_BOOT_CYCLES = 1_300_000
13
+
14
+ # Where BASIC's program starts, from TXTTAB, which the KERNAL sets at
15
+ # boot by the RAM it finds: $1001 unexpanded, $0401 with the 3K
16
+ # expansion and $1201 with 8K or more in BLK1.
17
+ def basic_start = ram.peek16(0x2b)
18
+
19
+ # Puts a cartridge's ROM chips, each a CRTFile::Chip with its address
20
+ # and bytes, into their blocks, and switches the machine off and on,
21
+ # as a cartridge goes in with the power off. The KERNAL then starts
22
+ # the cartridge in BLK5 that signs itself with A0CBM.
23
+ def attach_cartridge(chips)
24
+ chips.each { |chip| @bus.map_rom(chip.address, chip.data) }
25
+ power_cycle!
26
+ end
27
+
28
+ # Puts the storage in device 8. Mounting again swaps the disk at any
29
+ # point while the machine runs: the drive keeps its RAM and its
30
+ # status.
31
+ def mount(storage)
32
+ return @drive.insert(storage) if @drive
33
+
34
+ @drive = KernalTrap::Drive.new(storage)
35
+ layout = KernalTrap::VIC20_LAYOUT
36
+ load_trap = KernalTrap::Load.new(cpu:, bus:, layout:, drive: @drive)
37
+ cpu.install_trap(layout.load) { load_trap.call }
38
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus:, layout:, drive: @drive, device: serial_trap_device).install
39
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus:, layout:, drive: @drive)
40
+ cpu.install_trap(layout.save) { save_trap.call }
41
+ end
42
+
43
+ # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
44
+ # serial traps, so the KERNAL's routines go out over the serial bus.
45
+ # Mounting again starts a new drive, with its RAM cleared.
46
+ def unmount
47
+ return unless @drive
48
+
49
+ cpu.remove_trap(KernalTrap::VIC20_LAYOUT.load)
50
+ cpu.remove_trap(KernalTrap::VIC20_LAYOUT.save)
51
+ @serial_trap.device = nil
52
+ @serial_trap = nil
53
+ @save_trap = nil
54
+ @drive = nil
55
+ end
56
+
57
+ # Whether device 8 serves a disk or directory through the traps.
58
+ def mounted? = !@drive.nil?
59
+
60
+ # Plugs in a Drive1541, which then runs alongside the VIC-20 on its own
61
+ # clock and talks to it over the serial bus. The serial traps stop
62
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN
63
+ # and byte transfers reach the drive. The LOAD and SAVE traps stay,
64
+ # and still serve a mounted image.
65
+ #
66
+ # A drive that powers on with the machine takes longer to boot than
67
+ # the KERNAL, and doesn't answer the bus until it has, so the
68
+ # machine's on_init handlers wait for it too.
69
+ def attach_drive1541(drive)
70
+ @init_threshold = [@init_threshold, DRIVE_BOOT_CYCLES].max if @cycles < @init_threshold
71
+ @iec_bus.detach(@drive1541) if @drive1541
72
+ drive.host_clock_hz = timing.clock_hz
73
+ @drive1541 = drive
74
+ drive.connect(@iec_bus)
75
+ @serial_trap&.device = serial_trap_device
76
+ end
77
+
78
+ # Unplugs the Drive1541, leaving the serial bus with nothing on it.
79
+ def detach_drive1541
80
+ return unless @drive1541
81
+
82
+ @iec_bus.detach(@drive1541)
83
+ @drive1541 = nil
84
+ @serial_trap&.device = serial_trap_device
85
+ end
86
+
87
+ # Records what the KERNAL prints through CHROUT (ChroutTrap).
88
+ def capture_output
89
+ @capture_output ||= ChroutTrap.new(cpu:, bus:, layout: KernalTrap::VIC20_LAYOUT).tap do |trap|
90
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # The device number the serial traps answer: device 8, unless a true
97
+ # drive is on the bus as device 8.
98
+ def serial_trap_device
99
+ @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
100
+ end
101
+ end
102
+ end
103
+ end