badline 0.15.1 → 0.16.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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/pet/rom_sets"
3
4
  require "badline/options/option"
4
5
  require "badline/options/event"
5
6
  require "badline/options/table"
@@ -12,10 +13,12 @@ module Badline
12
13
  # Either opens the window for any media, or with --headless or
13
14
  # --audio-out plays or renders a .sid tune without one. `sid`, given
14
15
  # first, plays .sid tunes and directories of them in the terminal.
15
- # `c64`, `vic20`, `c128` or `plus4`, given first, names the machine
16
- # family, and without one the family is the C64, or the C128 for a program
17
- # that loads where C128 BASIC starts. The native build adds --no-sound and
18
- # --version, and badline-ruby adds --disable-jit.
16
+ # `c64`, `vic20`, `c128`, `plus4` or `pet`, given first, names the
17
+ # machine family, and without one the family is the C64, the C128 for a
18
+ # program that loads where C128 BASIC starts, or the PET for an 8050's
19
+ # or 8250's disk or a PET tape.
20
+ # The native build adds --no-sound and --version, and badline-ruby adds
21
+ # --disable-jit.
19
22
  #
20
23
  # TABLE lists the options, and both the parser and the help read it. It
21
24
  # parses by hand, inside the subset of Ruby Spinel compiles. Values come
@@ -51,6 +54,9 @@ module Badline
51
54
  # The Plus/4's RAM sizes as --ram names them, in K.
52
55
  PLUS4_RAM = { "16k" => 16, "32k" => 32, "64k" => 64 }.freeze
53
56
 
57
+ # The PET's, below $8000.
58
+ PET_RAM = { "4k" => 4, "8k" => 8, "16k" => 16, "32k" => 32 }.freeze
59
+
54
60
  # Where --sid-card-address puts the Plus/4's SID card.
55
61
  SID_CARD_ADDRESSES = { "fd40" => 0xfd40, "fe80" => 0xfe80 }.freeze
56
62
 
@@ -70,16 +76,24 @@ module Badline
70
76
  attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
71
77
  :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
72
78
 
73
- # The machine family to build (Badline::Machine), :c64, :vic20, :c128
74
- # or :plus4.
79
+ # The machine family to build (Badline::Machine), :c64, :vic20, :c128,
80
+ # :plus4 or :pet.
75
81
  attr_reader :family
76
82
 
83
+ # The ROMs --basic, --editor and --characters fit a PET with, keys of
84
+ # PET::ROMSets::BASICS, PET::ROMSets::EDITORS and
85
+ # PET::ROMSets::CHARACTERS, or nil for its own.
86
+ attr_reader :basic, :editor, :characters
87
+
88
+ # The 8296's board jumpers --jumpers names (PET::Bus::Expansion::JUMPERS).
89
+ attr_reader :jumpers
90
+
77
91
  # The VIC-20's RAM expansion --ram names, a key of
78
92
  # Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
79
93
  attr_reader :ram
80
94
 
81
- # The Plus/4's RAM in K --ram names, one of PLUS4_RAM's, or nil without
82
- # --ram.
95
+ # The Plus/4's or the PET's RAM in K --ram names, one of PLUS4_RAM's or
96
+ # PET_RAM's, or nil without --ram.
83
97
  attr_reader :ram_kb
84
98
 
85
99
  # The Plus/4's cartridge ROM images --c1lo, --c1hi, --c2lo and --c2hi
@@ -121,6 +135,8 @@ module Badline
121
135
  @reu = nil
122
136
  @ram = nil
123
137
  @ram_kb = nil
138
+ @ram_name = nil
139
+ initialize_pet
124
140
  @cartridges = {}
125
141
  @autostart = true
126
142
  @writable = false
@@ -229,6 +245,10 @@ module Badline
229
245
  # power-on.
230
246
  def c64_mode? = @c64_mode
231
247
 
248
+ # The frame rate of a CRTC PET's editor: 60 with --60hz or a 60 Hz
249
+ # --editor, else 50.
250
+ def refresh_hz = @refresh_hz || 50
251
+
232
252
  def help? = @help
233
253
 
234
254
  def version? = @version
@@ -244,7 +264,7 @@ module Badline
244
264
  # A .sid on its own plays in the SID player, as `sid` would, unless an
245
265
  # option of the emulator's window asks for the machine.
246
266
  def play_lone_tune
247
- return if @sid_command || %i[vic20 plus4].include?(@family) || headless? || !@window_only.empty?
267
+ return if @sid_command || %i[vic20 plus4 pet].include?(@family) || headless? || !@window_only.empty?
248
268
  return if @media_path.nil?
249
269
 
250
270
  @sid_command = File.extname(@media_path).casecmp?(".sid")
@@ -303,7 +323,8 @@ module Badline
303
323
  when "--ram-expansion" then @ram_expansion = plus4_ram_expansion(value)
304
324
  when "--model" then @models << value
305
325
  when "--reu" then @reu = reu_size(value)
306
- when "--ram" then ram_option(value)
326
+ when "--ram" then @ram_name = value
327
+ when "--basic", "--editor", "--characters", "--jumpers" then pet_option(name, value)
307
328
  when "--drive-type" then @drive_type = drive_type_for(value)
308
329
  when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
309
330
  when "--frames" then @frames = number(flag, value)
@@ -318,12 +339,21 @@ module Badline
318
339
  end
319
340
  end
320
341
 
321
- # --ram names the VIC-20's RAM expansion, or the Plus/4's RAM.
322
- def ram_option(value)
323
- if @family == :plus4
324
- @ram_kb = plus4_ram(value)
325
- else
326
- @ram = ram_configuration(value)
342
+ # The PET's frame rate, ROMs and board jumpers, its own until named.
343
+ def initialize_pet
344
+ @refresh_hz = nil
345
+ @basic = nil
346
+ @editor = nil
347
+ @characters = nil
348
+ @jumpers = []
349
+ end
350
+
351
+ def pet_option(name, value)
352
+ case name
353
+ when "--basic" then @basic = value
354
+ when "--editor" then @editor = value
355
+ when "--characters" then @characters = value
356
+ else @jumpers = value.split(",")
327
357
  end
328
358
  end
329
359
 
@@ -339,6 +369,7 @@ module Badline
339
369
  when "--vice" then @vice = true
340
370
  when "--ntsc" then @models << "ntsc"
341
371
  when "--c64" then @c64_mode = true
372
+ when "--60hz" then @refresh_hz = 60
342
373
  when "--unpaced" then @paced = false
343
374
  when "--disable-jit" then @jit = false
344
375
  when "--help" then @help = true
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class Bus
6
+ # The 64K board of the 8096 and the 8296: four 16K banks, which a
7
+ # write-only control register at $FFF0 lays over $8000-$FFFF, two at
8
+ # a time:
9
+ #
10
+ # bit 7 the board on; when clear, the rest ignored
11
+ # bit 6 I/O shows through at $E800-$EFFF
12
+ # bit 5 the screen shows through at $8000-$8FFF
13
+ # bit 3 bank 3 at $C000-$FFFF, else bank 1
14
+ # bit 2 bank 2 at $8000-$BFFF, else bank 0
15
+ # bit 1 writes to $C000-$FFFF go nowhere
16
+ # bit 0 writes to $8000-$BFFF go nowhere
17
+ #
18
+ # A write to $FFF0 lands where $FFF0 was before it, too. The RES line
19
+ # clears the register, so the 8096 starts as an 8032.
20
+ #
21
+ # The 8296 decodes its memory in a PLA, after the logic equations
22
+ # André Fachat read out of it (8296desc.txt on zimmers.net), with 32K
23
+ # of RAM from $8000 (Bus#screen) besides the board:
24
+ #
25
+ # - With bit 7 clear, every write from $8000 up but to $E8xx goes to
26
+ # that RAM, under the ROMs too. Reads find the screen at $8000,
27
+ # the empty sockets at $9000 and $A000, BASIC, the editor at
28
+ # $E000-$E7FF and again at $E900-$EFFF, and the KERNAL, unless the
29
+ # lines /RAMSEL9, /RAMSELA and /RAMON put RAM there (#ram_lines).
30
+ # RAMSEL9 puts it at $9000, RAMSELA at $A000, RAMON at
31
+ # $8000-$DFFF, RAMON with either of the others at $E000-$EFFF but
32
+ # $E8xx, and RAMON with RAMSELA at $F000 as well.
33
+ # - With bit 7 set, I/O showing through brings back the editor at
34
+ # $E900-$EFFF, which an 8096 leaves empty.
35
+ #
36
+ # Jumpers tie the lines: JU2 RAMSEL9 low and JU1 RAMSELA low
37
+ # (:ram9, :rama), or JU4, JU3 and JU5 RAMSEL9, RAMSELA and RAMON to
38
+ # the user port's PA1, PA0 and PA2 (:userport), each active low.
39
+ # With none fitted, as on xpet's 8296, the lines stay high.
40
+ class Expansion
41
+ # A 16K bank of the board, seen from +start+.
42
+ class Window
43
+ attr_accessor :read_only
44
+
45
+ def initialize(cells, offset, start)
46
+ @cells = cells
47
+ @offset = offset - start
48
+ @read_only = false
49
+ end
50
+
51
+ def peek(addr) = @cells[addr + @offset]
52
+
53
+ def poke(addr, value)
54
+ @cells[addr + @offset] = value unless @read_only
55
+ end
56
+ end
57
+
58
+ # Page $FF's writes, which pass to what the page holds and set the
59
+ # control register at $FFF0.
60
+ class ControlPort
61
+ attr_accessor :under
62
+
63
+ def initialize(expansion, under)
64
+ @expansion = expansion
65
+ @under = under
66
+ end
67
+
68
+ def poke(addr, value)
69
+ @under.poke(addr, value)
70
+ @expansion.control = value if addr == 0xfff0
71
+ end
72
+ end
73
+
74
+ # The user port lines that RAMSELA, RAMSEL9 and RAMON follow.
75
+ USER_PORT_LINES = 0x07
76
+
77
+ attr_reader :cells, :control
78
+
79
+ # The user port's PA0-PA2 as the board last saw them.
80
+ attr_reader :lines
81
+
82
+ # +jumpers+ are the 8296's (ROMSets::JUMPERS) when +board8296+ is.
83
+ def initialize(bus, board8296: false, jumpers: [])
84
+ @bus = bus
85
+ @cells = Array.new(0x10000, 0)
86
+ @banks = [0x8000, 0xc000, 0x8000, 0xc000].each_with_index.map do |start, bank|
87
+ Window.new(@cells, bank * 0x4000, start)
88
+ end
89
+ @port = ControlPort.new(self, @banks[1])
90
+ @control = 0
91
+ @board8296 = board8296
92
+ @ram9 = jumpers.include?("ram9")
93
+ @rama = jumpers.include?("rama")
94
+ @user_port = jumpers.include?("userport")
95
+ @lines = USER_PORT_LINES
96
+ end
97
+
98
+ def board8296? = @board8296
99
+
100
+ # Whether the board follows the user port's lines.
101
+ def user_port? = @user_port
102
+
103
+ def control=(value)
104
+ @control = value
105
+ @bus.map_pages
106
+ end
107
+
108
+ # Follows the user port's lines, +port_a+ as the VIA drives them.
109
+ def lines=(port_a)
110
+ lines = port_a & USER_PORT_LINES
111
+ return if lines == @lines
112
+
113
+ @lines = lines
114
+ @bus.map_pages
115
+ end
116
+
117
+ def reset!
118
+ self.control = 0
119
+ end
120
+
121
+ def power_on!
122
+ @cells.fill(0)
123
+ end
124
+
125
+ # Whether the CPU reads the KERNAL at $F000-$FFFF.
126
+ def kernal_mapped?
127
+ return false if @control.anybits?(0x80)
128
+
129
+ !(@board8296 && ram_on? && ram_a?)
130
+ end
131
+
132
+ # Lays the banks the register picks over the pages, and the
133
+ # control register over page $FF's writes.
134
+ def map(reads, writes)
135
+ if @control.anybits?(0x80)
136
+ map_banks(reads, writes)
137
+ elsif @board8296
138
+ map_ram8296(reads, writes)
139
+ end
140
+ @port.under = writes[0xff]
141
+ writes[0xff] = @port
142
+ end
143
+
144
+ private
145
+
146
+ def ram_on? = @user_port && @lines.nobits?(0x04)
147
+
148
+ def ram9? = @ram9 || (@user_port && @lines.nobits?(0x02))
149
+
150
+ def ram_a? = @rama || (@user_port && @lines.nobits?(0x01))
151
+
152
+ def map_banks(reads, writes)
153
+ low = @banks[@control.anybits?(0x04) ? 2 : 0]
154
+ high = @banks[@control.anybits?(0x08) ? 3 : 1]
155
+ low.read_only = @control.anybits?(0x01)
156
+ high.read_only = @control.anybits?(0x02)
157
+ first = @control.anybits?(0x20) ? 0x90 : 0x80
158
+ reads.fill(low, first, 0xc0 - first)
159
+ writes.fill(low, first, 0xc0 - first)
160
+ [[0xc0, 0x28], [0xe8, 0x08], [0xf0, 0x10]].each do |page, count|
161
+ next if page == 0xe8 && @control.anybits?(0x40)
162
+
163
+ reads.fill(high, page, count)
164
+ writes.fill(high, page, count)
165
+ end
166
+ map_editor_through(reads, writes) if @board8296
167
+ end
168
+
169
+ # I/O showing through on the 8296 leaves $E900-$EFFF to the editor,
170
+ # and its writes to nothing.
171
+ def map_editor_through(reads, writes)
172
+ return unless @control.anybits?(0x40)
173
+
174
+ reads.fill(@bus.editor_rom, 0xe9, 0x07)
175
+ writes.fill(@bus.open_bus, 0xe9, 0x07)
176
+ end
177
+
178
+ def map_ram8296(reads, writes)
179
+ ram = @bus.screen
180
+ writes.fill(ram, 0x90, 0x58)
181
+ writes.fill(ram, 0xe9, 0x17)
182
+ reads.fill(@bus.editor_rom, 0xe9, 0x07)
183
+ ram_on = ram_on?
184
+ ram9 = ram9?
185
+ ram_a = ram_a?
186
+ reads.fill(ram, 0x90, 0x10) if ram9 || ram_on
187
+ reads.fill(ram, 0xa0, 0x10) if ram_a || ram_on
188
+ return unless ram_on
189
+
190
+ reads.fill(ram, 0xb0, 0x30)
191
+ return unless ram9 || ram_a
192
+
193
+ reads.fill(ram, 0xe0, 0x08)
194
+ reads.fill(ram, 0xe9, 0x07)
195
+ reads.fill(ram, 0xf0, 0x10) if ram_a
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/pet/bus/expansion"
4
+
5
+ module Badline
6
+ class PET
7
+ # The CPU's bus:
8
+ #
9
+ # $0000-$7FFF - RAM, 4K, 8K, 16K or 32K of it from $0000, then nothing
10
+ # $8000-$8FFF - screen RAM, repeated (Model), then nothing
11
+ # $9000-$AFFF - two empty ROM sockets
12
+ # $B000-$DFFF - BASIC 4, or BASIC 1 or 2 from $C000
13
+ # $E000-$E7FF - the screen editor ROM
14
+ # $E800-$E8FF - I/O (IO)
15
+ # $E900-$EFFF - nothing
16
+ # $F000-$FFFF - the KERNAL ROM
17
+ #
18
+ # The data lines hold the last byte they carried, so a read from an
19
+ # empty spot finds it, mostly the high byte of the address just
20
+ # fetched, as PET/unconnected shows on a real 3016. Writes to ROM and
21
+ # to empty spots go nowhere. An 8096 banks its 64K board over
22
+ # $8000-$FFFF (Expansion), and an 8296 its board and the RAM under its
23
+ # ROMs.
24
+ class Bus
25
+ include Addressable
26
+
27
+ # The last byte the CPU moved, which an empty spot reads back.
28
+ class OpenBus
29
+ def initialize(bus)
30
+ @bus = bus
31
+ end
32
+
33
+ def peek(_addr) = @bus.data
34
+ def poke(_addr, _value); end
35
+ end
36
+
37
+ # Screen RAM, repeated over the pages it answers in. On the 8296 it is
38
+ # all 32K of RAM from $8000, which the board lays under the ROMs.
39
+ class ScreenRAM
40
+ attr_reader :cells
41
+
42
+ def initialize(size)
43
+ @cells = Array.new(size, 0)
44
+ @mask = size - 1
45
+ end
46
+
47
+ def peek(addr) = @cells[addr & @mask]
48
+
49
+ def poke(addr, value)
50
+ @cells[addr & @mask] = value
51
+ end
52
+ end
53
+
54
+ # $E800-$E8FF. Each chip answers on one address line, with its
55
+ # registers repeated: A4 selects PIA 1, A5 PIA 2, A6 the VIA and A7
56
+ # the CRTC, where there is one. Where more than one chip answers a
57
+ # read, each pulls the lines it drives low, so the CPU reads the AND
58
+ # of them, and a write reaches them all. Where none answers, as in
59
+ # $E800-$E80F, the CPU reads the last byte on the bus.
60
+ class IO
61
+ def initialize(bus, pia1:, pia2:, via:, crtc:, crtc_fitted:)
62
+ @bus = bus
63
+ @pia1 = pia1
64
+ @pia2 = pia2
65
+ @via = via
66
+ @crtc = crtc
67
+ @lines = crtc_fitted ? 0xf0 : 0x70
68
+ end
69
+
70
+ def peek(addr)
71
+ return @bus.data if addr.nobits?(@lines)
72
+
73
+ value = 0xff
74
+ value &= @pia1.peek(addr) if addr.anybits?(0x10)
75
+ value &= @pia2.peek(addr) if addr.anybits?(0x20)
76
+ value &= @via.peek(addr) if addr.anybits?(0x40)
77
+ value &= @crtc.peek(addr) if addr.anybits?(@lines & 0x80)
78
+ value
79
+ end
80
+
81
+ def poke(addr, value)
82
+ @pia1.poke(addr, value) if addr.anybits?(0x10)
83
+ @pia2.poke(addr, value) if addr.anybits?(0x20)
84
+ @via.poke(addr, value) if addr.anybits?(0x40)
85
+ @crtc.poke(addr, value) if addr.anybits?(@lines & 0x80)
86
+ @bus.io_written
87
+ end
88
+ end
89
+
90
+ # VICE's debug cartridge register at $8BFF, which a write calls the
91
+ # handler with, the exit code the VICE testprogs report. It answers
92
+ # on every model, ahead of the 2001's screen RAM there and the
93
+ # 8096's banks, and every other write to its page passes to what
94
+ # the page holds.
95
+ class DebugRegister
96
+ ADDRESS = 0x8bff
97
+
98
+ attr_accessor :under
99
+
100
+ def initialize(&handler)
101
+ @handler = handler
102
+ @under = nil
103
+ end
104
+
105
+ def poke(addr, value)
106
+ return @handler.call(value) if addr == ADDRESS
107
+
108
+ @under.poke(addr, value)
109
+ end
110
+ end
111
+
112
+ attr_reader :ram, :screen, :expansion, :editor_rom, :open_bus
113
+
114
+ # The last byte the CPU read or wrote.
115
+ attr_reader :data
116
+
117
+ # +model+ is a Model::Profile, +ram_kb+ the RAM below $8000 and
118
+ # +editor+ the editor ROM's file. The CRTC answers only on a board
119
+ # the model fits one on. +jumpers+ are the 8296's (ROMSets::JUMPERS).
120
+ def initialize(model:, ram_kb:, editor:, pia1:, pia2:, via:, crtc:, jumpers: [])
121
+ addressable_at(0, length: 2**16)
122
+ @ram = Memory.new([], length: ram_kb * 1024, start: 0)
123
+ @screen = ScreenRAM.new(model.screen_bytes)
124
+ @screen_pages = model.screen_pages
125
+ basic = ROM.read(model.basic)
126
+ @basic_rom = ROM.new(basic, length: basic.length, start: 0xe000 - basic.length)
127
+ @editor_rom = model.cbm8296? ? editor_socket(editor) : ROM.load(editor, 0xe000)
128
+ @kernal_rom = ROM.load(model.kernal, 0xf000)
129
+ @io = IO.new(self, pia1:, pia2:, via:, crtc:, crtc_fitted: model.crtc?)
130
+ @open_bus = OpenBus.new(self)
131
+ @data = 0
132
+ @read_pages = Array.new(256, @open_bus)
133
+ @write_pages = Array.new(256, @open_bus)
134
+ @expansion = model.expansion ? Expansion.new(self, board8296: model.cbm8296?, jumpers:) : nil
135
+ @via = via
136
+ @user_port = !@expansion.nil? && @expansion.user_port?
137
+ @debug_register = nil
138
+ @io_write_handler = nil
139
+ map_pages
140
+ end
141
+
142
+ # Calls the block after each write to the I/O chips, which may have
143
+ # moved the lines their ports drive.
144
+ def on_io_write(&handler)
145
+ @io_write_handler = handler
146
+ end
147
+
148
+ def io_written
149
+ @io_write_handler&.call
150
+ @expansion.lines = @via.port_a_output if @user_port
151
+ end
152
+
153
+ # The RES line reaches the board's control register, and the user
154
+ # port's lines float high with the VIA's.
155
+ def reset!
156
+ return if @expansion.nil?
157
+
158
+ @expansion.reset!
159
+ @expansion.lines = @via.port_a_output if @user_port
160
+ end
161
+
162
+ def power_on!
163
+ @ram.clear!
164
+ @screen.cells.fill(0)
165
+ @expansion&.power_on!
166
+ end
167
+
168
+ def peek(addr)
169
+ @data = @read_pages[addr >> 8].peek(addr)
170
+ end
171
+
172
+ def poke(addr, value)
173
+ @data = value
174
+ @write_pages[addr >> 8].poke(addr, value)
175
+ end
176
+
177
+ # Whether the CPU reads the KERNAL ROM at $F000-$FFFF, which an 8096's
178
+ # board covers while its control register turns it on, and an 8296's
179
+ # RAM too.
180
+ def kernal_mapped? = @expansion.nil? || @expansion.kernal_mapped?
181
+
182
+ # Calls the block with each byte written to $8BFF (DebugRegister).
183
+ def install_debug_register(&)
184
+ @debug_register = DebugRegister.new(&)
185
+ map_pages
186
+ end
187
+
188
+ # Lays the pages out again, for the board's control register.
189
+ def map_pages
190
+ map(@open_bus, 0x00, 0x100)
191
+ map(@ram, 0x00, @ram.length >> 8)
192
+ map(@screen, 0x80, @screen_pages)
193
+ @read_pages.fill(@basic_rom, @basic_rom.start >> 8, @basic_rom.length >> 8)
194
+ @read_pages.fill(@editor_rom, 0xe0, 0x08)
195
+ map(@io, 0xe8, 0x01)
196
+ @read_pages.fill(@kernal_rom, 0xf0, 0x10)
197
+ @expansion&.map(@read_pages, @write_pages)
198
+ map_debug_register if @debug_register
199
+ end
200
+
201
+ private
202
+
203
+ # The 8296's editor socket, which answers at $E000-$E7FF and
204
+ # $E900-$EFFF: a 4K ROM's halves there, or a 2K one twice over.
205
+ def editor_socket(file)
206
+ bytes = ROM.read(file)
207
+ bytes += bytes if bytes.length == 0x800
208
+ ROM.new(bytes, length: 0x1000, start: 0xe000)
209
+ end
210
+
211
+ def map_debug_register
212
+ @debug_register.under = @write_pages[0x8b]
213
+ @write_pages[0x8b] = @debug_register
214
+ end
215
+
216
+ def map(chip, first, count)
217
+ @read_pages.fill(chip, first, count)
218
+ @write_pages.fill(chip, first, count)
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ # The picture a front end shows, a palette index a dot: 0 for black and
6
+ # 1 for the lit phosphor. The video paints it eight dots at a time, one
7
+ # character ROM row, and leaves alone the dots that would not change:
8
+ # each four dots remember the row last painted from them, and a
9
+ # painting that overlaps theirs from elsewhere makes them forget it.
10
+ #
11
+ # Each line keeps the span its last painting covered. When a line's
12
+ # span changes, the dots it covered before and doesn't now go black.
13
+ class Canvas
14
+ # Black and the green of a P1 phosphor, as the VICE testbench's
15
+ # references draw them.
16
+ PALETTE = [0x000000, 0x41ff00].freeze
17
+
18
+ # The eight dots of each ROM row, the leftmost in bit 7.
19
+ DOTS = Array.new(256) { |bits| Array.new(8) { |dot| (bits >> (7 - dot)) & 1 } }.freeze
20
+
21
+ attr_reader :display, :width, :height, :dirty_lines
22
+
23
+ # Nothing paints within +margin+ dots of either side.
24
+ def initialize(width, height, margin: 0)
25
+ @first = margin
26
+ @last = width - 8 - margin
27
+ @width = width
28
+ @height = height
29
+ @display = Array.new(width * height, 0)
30
+ @dirty_lines = Array.new(height, true)
31
+ @slot_width = width >> 2
32
+ @painted = Array.new(@slot_width * height, 0)
33
+ @span_from = Array.new(height, 0)
34
+ @span_to = Array.new(height, 0)
35
+ end
36
+
37
+ def palette = PALETTE
38
+
39
+ def clear_dirty_lines!
40
+ @dirty_lines.fill(false)
41
+ end
42
+
43
+ # Paints `bits` on `line` from dot `column` on, if they fit.
44
+ def paint(column, line, bits)
45
+ return if column < @first || column > @last
46
+
47
+ slot = (line * @slot_width) + (column >> 2)
48
+ return if @painted[slot] == bits
49
+
50
+ @painted[slot] = bits
51
+ @painted[slot - 1] = -1 if slot.positive?
52
+ @painted[slot + 1] = -1 if slot + 1 < @painted.length
53
+ @display[(line * @width) + column, 8] = DOTS[bits]
54
+ @dirty_lines[line] = true
55
+ end
56
+
57
+ # Sets `line`'s span to the dots from `from` up to `to`, none if `to`
58
+ # isn't past `from`, and blacks out what its last one covered beyond
59
+ # it.
60
+ def span(line, from, to)
61
+ width = @width
62
+ from = 0 if from.negative?
63
+ to = width if to > width
64
+ from = to = 0 if to <= from
65
+ old_from = @span_from[line]
66
+ old_to = @span_to[line]
67
+ return if from == old_from && to == old_to
68
+
69
+ blank(line, old_from, [from, old_to].min) if old_from < from
70
+ blank(line, [to, old_from].max, old_to) if old_to > to
71
+ @span_from[line] = from
72
+ @span_to[line] = to
73
+ end
74
+
75
+ # Blacks out every dot, as at power on.
76
+ def clear!
77
+ @display.fill(0)
78
+ @painted.fill(0)
79
+ @span_from.fill(0)
80
+ @span_to.fill(0)
81
+ @dirty_lines.fill(true)
82
+ end
83
+
84
+ private
85
+
86
+ def blank(line, from, to)
87
+ return if to <= from
88
+
89
+ @display.fill(0, (line * @width) + from, to - from)
90
+ first = (line * @slot_width) + (from >> 2)
91
+ @painted.fill(0, first, ((to + 3) >> 2) - (from >> 2))
92
+ @painted[first - 1] = -1 if from >= 4
93
+ @dirty_lines[line] = true
94
+ end
95
+ end
96
+ end
97
+ end