badline 0.1.0 → 0.2.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +96 -25
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +169 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +109 -168
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. metadata +104 -4
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Action Replay v4.2 to v6: four 8K ROM banks and 8K of RAM. A write to
6
+ # I/O 1 sets the control register:
7
+ #
8
+ # bit 0 pulls GAME low
9
+ # bit 1 releases EXROM
10
+ # bit 2 switches the register off until the next freeze or reset
11
+ # bits 3-4 ROM bank
12
+ # bit 5 RAM instead of ROM at ROML and I/O 2
13
+ # bit 6 acknowledges a freeze and lets go of NMI
14
+ #
15
+ # I/O 2 shows the last page of the RAM or of the ROM bank. The register
16
+ # doesn't decode R/W, so a read of I/O 1 writes whatever is on the bus
17
+ # into it. RAM with only EXROM released ($22) selects the cartridge RAM
18
+ # and the C64's together at ROML. A freeze switches the cartridge back
19
+ # on, in Ultimax mode with the RAM at ROML and the first bank at ROMH.
20
+ class ActionReplay < Cartridge
21
+ include Freezer
22
+
23
+ CONTROL_MODES = %i[rom8k rom16k off ultimax].freeze
24
+ IO2_PAGE = 0x1f00
25
+
26
+ def connect(ram:, open_bus:)
27
+ super
28
+ @through.backing = ram
29
+ @contended.backing = ram
30
+ end
31
+
32
+ def readable_io_pages
33
+ [0xde, 0xdf]
34
+ end
35
+
36
+ def peek(addr)
37
+ return open_bus(addr) unless @active
38
+ return io2_peek(addr) if addr >= 0xdf00
39
+
40
+ open_bus(addr).tap { |value| control(value) }
41
+ end
42
+
43
+ def poke(addr, value)
44
+ return unless @active
45
+
46
+ if addr < 0xdf00
47
+ control(value)
48
+ elsif @io2_ram
49
+ @ram_data[IO2_PAGE | (addr & 0xff)] = value
50
+ end
51
+ end
52
+
53
+ def reset
54
+ @active = true
55
+ self.nmi = false
56
+ control(0)
57
+ end
58
+
59
+ def freeze!
60
+ @active = true
61
+ @rom = bank(@rom_banks, 0)
62
+ select(:ultimax, @isolated, io2_ram: true)
63
+ end
64
+
65
+ private
66
+
67
+ def io2_peek(addr)
68
+ @io2_ram ? @ram_data[IO2_PAGE | (addr & 0xff)] : @rom.peek(addr)
69
+ end
70
+
71
+ def control(value)
72
+ self.nmi = false if value.anybits?(0x40)
73
+ @rom = bank(@rom_banks, (value >> 3) & 0x03)
74
+ configure(value)
75
+ @active = false if value.anybits?(0x04)
76
+ end
77
+
78
+ def configure(value)
79
+ return select(:rom8k, @contended, io2_ram: true) if (value & 0x23) == 0x22
80
+
81
+ configure_standard(value)
82
+ end
83
+
84
+ def configure_standard(value)
85
+ mode = CONTROL_MODES[value & 0x03]
86
+ if value.anybits?(0x20)
87
+ select(mode, mode == :ultimax ? @isolated : @through, io2_ram: true)
88
+ else
89
+ select(mode, @rom, io2_ram: false)
90
+ end
91
+ end
92
+
93
+ def select(mode, roml, io2_ram:, romh: @rom)
94
+ self.mode = mode
95
+ @roml = roml
96
+ @romh = romh
97
+ @io2_ram = io2_ram
98
+ changed!
99
+ end
100
+
101
+ def install_chips(chips)
102
+ @rom_banks = banks_from(chips).first
103
+ @ram_data = Array.new(BANK_SIZE, 0)
104
+ @through = RAMBank.new(@ram_data)
105
+ @isolated = RAMBank.new(@ram_data)
106
+ @contended = ContendedRAM.new(@ram_data)
107
+ reset
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Atomic Power, sold as Nordic Power: an Action Replay 5 with one more
6
+ # configuration. RAM with only EXROM released and bits 2, 6 and 7 clear
7
+ # ($22) selects 16K mode with the ROM bank at ROML and the RAM at ROMH
8
+ # and I/O 2. Writes to that RAM don't reach the C64's. The VIC never
9
+ # sees Ultimax mode.
10
+ class AtomicPower < ActionReplay
11
+ def phi1_ultimax?
12
+ false
13
+ end
14
+
15
+ private
16
+
17
+ def configure(value)
18
+ return select(:rom16k, @rom, romh: @isolated, io2_ram: true) if (value & 0xe7) == 0x22
19
+
20
+ configure_standard(value)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # One 8K bank of cartridge ROM. The chip decodes only the low address
6
+ # lines, so the same bank answers at $8000, $A000 or $E000, and a smaller
7
+ # chip mirrors across the window.
8
+ class Bank
9
+ def initialize(data)
10
+ size = [1 << (data.length - 1).bit_length, 1].max
11
+ @mask = size - 1
12
+ @data = (data + ([0xff] * (size - data.length))).freeze
13
+ end
14
+
15
+ def peek(addr)
16
+ @data[addr & @mask]
17
+ end
18
+ alias [] peek
19
+ end
20
+
21
+ # Cartridge RAM mapped into the ROML or ROMH window. Writes land in the
22
+ # C64's RAM underneath as well, when there is a backing. Banks built on
23
+ # the same data are views of the same RAM.
24
+ class RAMBank
25
+ attr_reader :data
26
+ attr_writer :backing
27
+
28
+ def initialize(data = Array.new(BANK_SIZE, 0))
29
+ @data = data
30
+ @backing = nil
31
+ end
32
+
33
+ def peek(addr)
34
+ @data[addr & 0x1fff]
35
+ end
36
+ alias [] peek
37
+
38
+ def poke(addr, value)
39
+ @data[addr & 0x1fff] = value
40
+ @backing&.poke(addr, value)
41
+ end
42
+ alias []= poke
43
+ end
44
+
45
+ # Cartridge RAM the window reads, where the cartridge ignores writes.
46
+ class RAMReader
47
+ def initialize(data)
48
+ @data = data
49
+ end
50
+
51
+ def peek(addr)
52
+ @data[addr & 0x1fff]
53
+ end
54
+ alias [] peek
55
+ end
56
+
57
+ # Cartridge RAM that takes writes while the window reads something else.
58
+ class WriteOnlyRAM < RAMBank
59
+ def initialize(data, reader)
60
+ super(data)
61
+ @reader = reader
62
+ end
63
+
64
+ def peek(addr)
65
+ @reader.peek(addr)
66
+ end
67
+ alias [] peek
68
+ end
69
+
70
+ # Cartridge RAM selected together with the C64's RAM, both driving the
71
+ # bus on a read. The result is ORed, as VICE has it.
72
+ class ContendedRAM < RAMBank
73
+ def peek(addr)
74
+ @data[addr & 0x1fff] | @backing.peek(addr)
75
+ end
76
+ alias [] peek
77
+ end
78
+
79
+ EMPTY_BANK = Bank.new([0xff])
80
+ end
81
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Comal-80: four 16K banks, or eight with the extra ROMs. A write to
6
+ # I/O 1 selects the bank and the memory configuration. The black board
7
+ # (subtype 0) takes the bank from bits 0-2 and switches the ROM out with
8
+ # bit 6; the grey one (subtype 1) takes the bank from bits 0-1 and the
9
+ # configuration from bits 5-6.
10
+ class Comal80 < Cartridge
11
+ GREY_MODES = %i[rom16k ultimax rom8k off].freeze
12
+
13
+ def initialize(crt)
14
+ @grey = crt.subtype == 1
15
+ super
16
+ end
17
+
18
+ def poke(addr, value)
19
+ return if addr > 0xdeff
20
+
21
+ if @grey
22
+ select(value & 0x03, GREY_MODES[(value >> 5) & 0x03])
23
+ else
24
+ select(value & 0x07, value.anybits?(0x40) ? :off : :rom16k)
25
+ end
26
+ changed!
27
+ end
28
+
29
+ def reset
30
+ select(0, :rom16k)
31
+ changed!
32
+ end
33
+
34
+ private
35
+
36
+ def select(number, mode)
37
+ @roml = bank(@roml_banks, number)
38
+ @romh = bank(@romh_banks, number)
39
+ self.mode = mode
40
+ end
41
+
42
+ def install_chips(chips)
43
+ @roml_banks, @romh_banks = banks_from(chips)
44
+ reset
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Dinamic: up to 16 8K banks. Reading $DE00-$DE0F selects the bank from
6
+ # the low address bits.
7
+ class Dinamic < Cartridge
8
+ def readable_io_pages
9
+ [0xde]
10
+ end
11
+
12
+ def peek(addr)
13
+ if addr < 0xde10
14
+ @roml = bank(@banks, addr & 0x0f)
15
+ changed!
16
+ end
17
+ open_bus(addr)
18
+ end
19
+
20
+ def reset
21
+ @roml = bank(@banks, 0)
22
+ changed!
23
+ end
24
+
25
+ private
26
+
27
+ def install_chips(chips)
28
+ @banks = banks_from(chips).first
29
+ self.mode = :rom8k
30
+ reset
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # EasyFlash: two Am29F040B flash chips, one for ROML and one for ROMH,
6
+ # each 64 banks of 8K, and 256 bytes of RAM at $DF00. $DE00 selects the
7
+ # bank. $DE02 sets the memory configuration: bit 2 hands the GAME line
8
+ # to bit 0 (otherwise the boot jumper holds it low), bit 1 pulls EXROM
9
+ # low and bit 7 drives the LED.
10
+ #
11
+ # The flash takes writes in Ultimax mode, at $8000 for ROML and $E000
12
+ # for ROMH, which is how EAPI programs and erases it. The writes stay in
13
+ # memory; #save_crt writes the flash out as a new image.
14
+ #
15
+ # An image built for EasyFlash carries an EAPI, the flash driver, at $B800
16
+ # in bank 0 of ROMH. On attach, as in VICE, the bundled Am29F040 EAPI
17
+ # (roms/eapi) takes its place, so the image drives the flash emulated here.
18
+ class EasyFlash < Cartridge
19
+ BANKS = 64
20
+ EAPI_OFFSET = 0x1800
21
+ EAPI = File.binread(File.expand_path("../roms/eapi/eapi-am29f040-14", __dir__)).bytes.drop(2).freeze
22
+
23
+ attr_reader :low_flash, :high_flash
24
+
25
+ def clock=(clock)
26
+ super
27
+ [@low_flash, @high_flash].each { |flash| flash.clock = clock }
28
+ end
29
+
30
+ def readable_io_pages
31
+ [0xdf]
32
+ end
33
+
34
+ def peek(addr)
35
+ @io_ram[addr & 0xff]
36
+ end
37
+
38
+ def poke(addr, value)
39
+ if addr >= 0xdf00
40
+ @io_ram[addr & 0xff] = value
41
+ elsif addr.nobits?(0x02)
42
+ @bank = value & 0x3f
43
+ select_bank
44
+ else
45
+ @control = value & 0x87
46
+ apply_control
47
+ changed!
48
+ end
49
+ end
50
+
51
+ def led?
52
+ @control.anybits?(0x80)
53
+ end
54
+
55
+ # Reset clears both registers, and leaves the flash and the RAM.
56
+ def reset
57
+ @bank = 0
58
+ @control = 0
59
+ apply_control
60
+ select_bank
61
+ end
62
+
63
+ # Writes every bank that isn't blank to a CRT image at path.
64
+ def save_crt(path)
65
+ chips = (0...BANKS).flat_map do |number|
66
+ [[@low_flash, 0x8000], [@high_flash, 0xa000]].filter_map do |flash, address|
67
+ data = flash.data[number * BANK_SIZE, BANK_SIZE]
68
+ Storage::CRTFile::Chip.new(chip_type: 2, bank: number, address:, data:) if data.any?(0..0xfe)
69
+ end
70
+ end
71
+ image = Storage::CRTFile::Image.new(hardware_type: 32, subtype: 0, exrom: 1, game: 0, name:, chips:)
72
+ Storage::CRTFile.write(path, image)
73
+ end
74
+
75
+ private
76
+
77
+ def select_bank
78
+ @roml = @low_flash.window(@bank * BANK_SIZE)
79
+ @romh = @high_flash.window(@bank * BANK_SIZE)
80
+ changed!
81
+ end
82
+
83
+ def apply_control
84
+ game = @control.nobits?(0x04) || @control.anybits?(0x01)
85
+ exrom = @control.anybits?(0x02)
86
+ @game = game ? 0 : 1
87
+ @exrom = exrom ? 0 : 1
88
+ end
89
+
90
+ def install_chips(chips)
91
+ low = Array.new(BANKS * BANK_SIZE, 0xff)
92
+ high = Array.new(BANKS * BANK_SIZE, 0xff)
93
+ chips.each do |chip|
94
+ offset = (chip.bank & 0x3f) * BANK_SIZE
95
+ if chip.address >= ROMH_START
96
+ place(high, offset, chip.data)
97
+ else
98
+ place(low, offset, chip.data)
99
+ place(high, offset, chip.data[BANK_SIZE..]) if chip.data.length > BANK_SIZE
100
+ end
101
+ end
102
+ install_eapi(high)
103
+ @low_flash = Flash.new(low)
104
+ @high_flash = Flash.new(high)
105
+ [@low_flash, @high_flash].each { |flash| flash.on_change { select_bank } }
106
+ @io_ram = Array.new(0x100, 0xff)
107
+ reset
108
+ end
109
+
110
+ def install_eapi(high)
111
+ return unless high[EAPI_OFFSET, 4] == "eapi".bytes
112
+
113
+ high[EAPI_OFFSET, EAPI.length] = EAPI
114
+ end
115
+
116
+ def place(flash_data, offset, bytes)
117
+ bytes = bytes.first(BANK_SIZE)
118
+ flash_data[offset, bytes.length] = bytes
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Epyx FastLoad: 8K. A capacitor holds the ROM in for 512 cycles after
6
+ # it was last discharged, by reset, a read through ROML or a read of
7
+ # I/O 1; once it charges, $8000-$9FFF reads RAM. I/O 2 reads the last
8
+ # 256 bytes of the ROM.
9
+ class EpyxFastload < Cartridge
10
+ DISCHARGED_CYCLES = 512
11
+
12
+ class Window
13
+ def initialize(cartridge, rom)
14
+ @cartridge = cartridge
15
+ @rom = rom
16
+ end
17
+
18
+ def peek(addr)
19
+ @cartridge.rom_visible? ? @rom.peek(addr) : @cartridge.ram_peek(addr)
20
+ end
21
+ end
22
+
23
+ def clock=(clock)
24
+ super
25
+ discharge!
26
+ end
27
+
28
+ def readable_io_pages
29
+ [0xde, 0xdf]
30
+ end
31
+
32
+ def peek(addr)
33
+ return @rom.peek(addr) if addr >= 0xdf00
34
+
35
+ discharge!
36
+ open_bus(addr)
37
+ end
38
+
39
+ # True while the capacitor holds the ROM in, discharging it again.
40
+ def rom_visible?
41
+ return true unless @clock
42
+ return false if @clock.call - @discharged_at >= DISCHARGED_CYCLES
43
+
44
+ discharge!
45
+ true
46
+ end
47
+
48
+ def ram_peek(addr)
49
+ @ram.peek(addr)
50
+ end
51
+
52
+ # Reset discharges the capacitor, so the ROM shows.
53
+ def reset
54
+ discharge!
55
+ end
56
+
57
+ private
58
+
59
+ def discharge!
60
+ @discharged_at = @clock ? @clock.call : 0
61
+ end
62
+
63
+ def install_chips(chips)
64
+ @rom = banks_from(chips).first.compact.first
65
+ @roml = Window.new(self, @rom)
66
+ @discharged_at = 0
67
+ self.mode = :rom8k
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # The Final Cartridge III: four 16K banks, or sixteen on the III+. I/O 1
6
+ # and I/O 2 show the last two pages of the selected ROML bank. A write
7
+ # to $DFFF sets the register:
8
+ #
9
+ # bits 0-1 bank (0-3 on the III+)
10
+ # bit 4 EXROM level
11
+ # bit 5 GAME level
12
+ # bit 6 NMI line, active low
13
+ # bit 7 hides the register until the next freeze
14
+ #
15
+ # Clearing bit 6 pulls NMI without freezing, and setting it lets go of
16
+ # NMI after a freeze. A freeze keeps the bank and switches to Ultimax,
17
+ # which the VIC doesn't see until the register sets it again.
18
+ class FinalCartridge3 < Cartridge
19
+ include Freezer
20
+
21
+ REGISTER = 0xdfff
22
+
23
+ def readable_io_pages
24
+ [0xde, 0xdf]
25
+ end
26
+
27
+ def peek(addr)
28
+ @io_bank.peek(addr)
29
+ end
30
+
31
+ def poke(addr, value)
32
+ return unless addr == REGISTER && @register_visible
33
+
34
+ @register_visible = value.nobits?(0x80)
35
+ self.nmi = value.nobits?(0x40)
36
+ select(value & @bank_mask)
37
+ @exrom = (value >> 4) & 0x01
38
+ @game = (value >> 5) & 0x01
39
+ @phi1_ultimax = ultimax?
40
+ changed!
41
+ end
42
+
43
+ def phi1_ultimax?
44
+ @phi1_ultimax
45
+ end
46
+
47
+ def reset
48
+ @register_visible = true
49
+ @phi1_ultimax = false
50
+ self.nmi = false
51
+ select(0)
52
+ self.mode = :rom16k
53
+ changed!
54
+ end
55
+
56
+ def freeze!
57
+ @register_visible = true
58
+ @phi1_ultimax = false
59
+ self.mode = :ultimax
60
+ changed!
61
+ end
62
+
63
+ private
64
+
65
+ def select(number)
66
+ @io_bank = @roml = bank(@roml_banks, number)
67
+ @romh = bank(@romh_banks, number)
68
+ end
69
+
70
+ def install_chips(chips)
71
+ @roml_banks, @romh_banks = banks_from(chips)
72
+ @bank_mask = @roml_banks.length > 4 ? 0x0f : 0x03
73
+ reset
74
+ end
75
+ end
76
+ end
77
+ end