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,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # An AMD 29F0x0 flash chip, driven through the JEDEC command set in the
6
+ # AMD datasheets. Each command opens with two unlock cycles, $AA to the
7
+ # first unlock address and $55 to the second, and a third write picks
8
+ # the command:
9
+ #
10
+ # $90 autoselect: offset 0 reads the manufacturer, 1 the device and 2
11
+ # the sector protection, until a reset
12
+ # $F0 reset to reading the array, which a lone $F0 also does
13
+ # $A0 byte program: the next write clears bits of the array, which
14
+ # only an erase sets again
15
+ # $80 erase, after two more unlock cycles: $10 to the first unlock
16
+ # address erases the chip, and $30 to a sector erases the sector.
17
+ # Further sectors can follow within the 50 us window.
18
+ #
19
+ # While a program or erase runs, a read returns status: DQ7 the
20
+ # complement of the programmed bit (0 while erasing), DQ6 toggling on
21
+ # every read, DQ5 set when a program failed and DQ3 set once an erase
22
+ # has left its window. $B0 suspends a sector erase and $30 resumes it.
23
+ #
24
+ # The cartridge reads the array through windows, and the chip swaps
25
+ # them for status windows while it is busy or in autoselect.
26
+ class Flash
27
+ Model = Data.define(:size, :sector_size, :unlock1, :unlock2, :unlock_mask, :manufacturer, :device)
28
+
29
+ AM29F040 = Model.new(size: 0x80000, sector_size: 0x10000, unlock1: 0x5555, unlock2: 0x2aaa,
30
+ unlock_mask: 0x7fff, manufacturer: 0x01, device: 0xa4)
31
+ AM29F040B = Model.new(size: 0x80000, sector_size: 0x10000, unlock1: 0x555, unlock2: 0x2aa,
32
+ unlock_mask: 0x7ff, manufacturer: 0x01, device: 0xa4)
33
+ AM29F010 = Model.new(size: 0x20000, sector_size: 0x4000, unlock1: 0x5555, unlock2: 0x2aaa,
34
+ unlock_mask: 0x7fff, manufacturer: 0x01, device: 0x20)
35
+
36
+ # The datasheet's typical times, in cycles at 1 MHz.
37
+ PROGRAM_CYCLES = 7
38
+ ERASE_WINDOW_CYCLES = 50
39
+ SECTOR_ERASE_CYCLES = 1_000_000
40
+ CHIP_ERASE_CYCLES = 8_000_000
41
+
42
+ # The array, read directly while the chip reads it.
43
+ class Window
44
+ def initialize(flash, base)
45
+ @flash = flash
46
+ @data = flash.data
47
+ @base = base
48
+ end
49
+
50
+ def peek(addr)
51
+ @data[@base | (addr & 0x1fff)]
52
+ end
53
+ alias [] peek
54
+
55
+ def poke(addr, value)
56
+ @flash.write(@base | (addr & 0x1fff), value)
57
+ end
58
+ alias []= poke
59
+ end
60
+
61
+ # Reads through the chip, for its status and autoselect codes.
62
+ class StatusWindow < Window
63
+ def peek(addr)
64
+ @flash.read(@base | (addr & 0x1fff))
65
+ end
66
+ alias [] peek
67
+ end
68
+
69
+ attr_reader :data, :state
70
+ attr_writer :clock
71
+
72
+ def initialize(data, model: AM29F040B, clock: -> { 0 })
73
+ @model = model
74
+ @data = data + ([0xff] * (model.size - data.length))
75
+ @clock = clock
76
+ @windows = {}
77
+ @status_windows = {}
78
+ @on_change = nil
79
+ @state = :read
80
+ @base_state = :read
81
+ @cycle = 0
82
+ @toggle = 0
83
+ end
84
+
85
+ # Called when the chip starts or stops answering reads from the array.
86
+ def on_change(&block)
87
+ @on_change = block
88
+ end
89
+
90
+ def window(base)
91
+ if array_mode?
92
+ @windows[base] ||= Window.new(self, base)
93
+ else
94
+ @status_windows[base] ||= StatusWindow.new(self, base)
95
+ end
96
+ end
97
+
98
+ def array_mode?
99
+ @state == :read || @state == :program_setup
100
+ end
101
+
102
+ def read(offset)
103
+ settle
104
+ case @state
105
+ when :read then @data[offset]
106
+ when :autoselect then autoselect_code(offset)
107
+ when :suspended then suspended_read(offset)
108
+ else status
109
+ end
110
+ end
111
+
112
+ def write(offset, value)
113
+ settle
114
+ case @state
115
+ when :program_setup then program(offset, value)
116
+ when :erase_window then extend_erase(offset, value)
117
+ when :erasing then suspend if value == 0xb0
118
+ when :suspended then resume if value == 0x30
119
+ when :program_error then enter(:read, base: :read) if value == 0xf0
120
+ when :programming, :chip_erase then nil
121
+ else command(offset, value)
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def command(offset, value)
128
+ if value == 0xf0
129
+ @cycle = 0
130
+ return enter(:read, base: :read)
131
+ end
132
+
133
+ address = offset & @model.unlock_mask
134
+ @cycle = case @cycle
135
+ when 0, 3 then address == @model.unlock1 && value == 0xaa ? @cycle + 1 : 0
136
+ when 1, 4 then address == @model.unlock2 && value == 0x55 ? @cycle + 1 : 0
137
+ when 2 then command_cycle(address, value)
138
+ when 5 then erase_command(offset, address, value)
139
+ end
140
+ end
141
+
142
+ def command_cycle(address, value)
143
+ return 0 unless address == @model.unlock1
144
+
145
+ case value
146
+ when 0x90 then enter(:autoselect, base: :autoselect)
147
+ when 0xa0 then @state = :program_setup
148
+ when 0x80 then return 3
149
+ end
150
+ 0
151
+ end
152
+
153
+ def erase_command(offset, address, value)
154
+ if value == 0x10 && address == @model.unlock1
155
+ start_busy(:chip_erase, CHIP_ERASE_CYCLES)
156
+ elsif value == 0x30
157
+ @erasing_sectors = [sector(offset)]
158
+ start_busy(:erase_window, ERASE_WINDOW_CYCLES)
159
+ end
160
+ 0
161
+ end
162
+
163
+ def program(offset, value)
164
+ @programmed = value
165
+ @data[offset] &= value
166
+ if @data[offset] == value
167
+ start_busy(:programming, PROGRAM_CYCLES)
168
+ else
169
+ enter(:program_error)
170
+ end
171
+ end
172
+
173
+ def extend_erase(offset, value)
174
+ case value
175
+ when 0x30
176
+ @erasing_sectors |= [sector(offset)]
177
+ @done_at = now + ERASE_WINDOW_CYCLES
178
+ when 0xb0
179
+ @remaining = @erasing_sectors.length * SECTOR_ERASE_CYCLES
180
+ enter(:suspended)
181
+ else
182
+ @erasing_sectors = nil
183
+ enter(@base_state)
184
+ end
185
+ end
186
+
187
+ def suspend
188
+ @remaining = @done_at - now
189
+ enter(:suspended)
190
+ end
191
+
192
+ def resume
193
+ start_busy(:erasing, @remaining)
194
+ end
195
+
196
+ # Moves a timed operation on once the clock has passed its end.
197
+ def settle
198
+ return unless @done_at && now >= @done_at
199
+
200
+ case @state
201
+ when :erase_window
202
+ start_busy(:erasing, @done_at - now + (@erasing_sectors.length * SECTOR_ERASE_CYCLES))
203
+ settle
204
+ when :erasing
205
+ @erasing_sectors.each { |s| @data.fill(0xff, s * @model.sector_size, @model.sector_size) }
206
+ @erasing_sectors = nil
207
+ finish
208
+ when :chip_erase
209
+ @data.fill(0xff)
210
+ finish
211
+ else
212
+ finish
213
+ end
214
+ end
215
+
216
+ def finish
217
+ @done_at = nil
218
+ enter(@base_state)
219
+ end
220
+
221
+ def start_busy(state, cycles)
222
+ @done_at = now + cycles
223
+ @programmed = 0xff unless state == :programming
224
+ enter(state)
225
+ end
226
+
227
+ def enter(state, base: @base_state)
228
+ was_array = array_mode?
229
+ @state = state
230
+ @base_state = base
231
+ @done_at = nil unless %i[programming erase_window erasing chip_erase].include?(state)
232
+ @on_change&.call if was_array != array_mode?
233
+ end
234
+
235
+ def status
236
+ @toggle ^= 0x40
237
+ value = (~@programmed & 0x80) | @toggle
238
+ value |= 0x20 if @state == :program_error
239
+ value |= 0x08 if @state == :erasing || @state == :chip_erase
240
+ value
241
+ end
242
+
243
+ def suspended_read(offset)
244
+ @erasing_sectors.include?(sector(offset)) ? 0x80 : @data[offset]
245
+ end
246
+
247
+ def autoselect_code(offset)
248
+ case offset & 0x43
249
+ when 0 then @model.manufacturer
250
+ when 1 then @model.device
251
+ when 2 then 0x00
252
+ else @data[offset]
253
+ end
254
+ end
255
+
256
+ def sector(offset)
257
+ offset / @model.sector_size
258
+ end
259
+
260
+ def now
261
+ @clock.call
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # The freeze button of a freezer cartridge. A press pulls NMI, and the
6
+ # computer calls #freeze! once the CPU has pushed the return address and
7
+ # status of the interrupt, so the vector fetch that follows reads the
8
+ # cartridge ROM in Ultimax mode. The cartridge lets go of NMI when its
9
+ # software acknowledges the freeze.
10
+ module Freezer
11
+ def press_button
12
+ @button = true
13
+ self.nmi = true if freeze_allowed?
14
+ end
15
+
16
+ def release_button
17
+ @button = false
18
+ end
19
+
20
+ private
21
+
22
+ def freeze_allowed?
23
+ true
24
+ end
25
+
26
+ def button_pressed?
27
+ @button == true
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Fun Play / Power Play: up to 16 8K banks. A write to I/O 1 selects the
6
+ # bank from bits 3-5 and 0, and either 8K mode ($00 in bits 7, 6, 2 and 1)
7
+ # or no ROM ($86 in those bits).
8
+ class FunPlay < Cartridge
9
+ def poke(addr, value)
10
+ return if addr > 0xdeff
11
+
12
+ @roml = bank(@banks, self.class.bank_number(value))
13
+ case value & 0xc6
14
+ when 0x00 then self.mode = :rom8k
15
+ when 0x86 then self.mode = :off
16
+ end
17
+ changed!
18
+ end
19
+
20
+ # The CRT file numbers each bank by the register value that selects it.
21
+ def reset
22
+ poke(0xde00, 0)
23
+ end
24
+
25
+ def self.bank_number(value)
26
+ ((value >> 3) & 0x07) | ((value & 0x01) << 3)
27
+ end
28
+
29
+ private
30
+
31
+ def install_chips(chips)
32
+ @banks = []
33
+ chips.each { |chip| @banks[self.class.bank_number(chip.bank)] = rom_bank(chip.data) }
34
+ reset
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # GMod2: an Am29F040 flash chip of 64 8K banks at ROML. A write to I/O 1
6
+ # selects the bank from bits 0-5. Bit 6 selects the EEPROM, which
7
+ # switches the ROM out, and bits 6 and 7 together enable flash writes:
8
+ # the cartridge then asserts Ultimax on write cycles only, so reads see
9
+ # the C64's memory and writes to $E000-$FFFF reach the flash. The EEPROM
10
+ # isn't emulated.
11
+ class GMod2 < Cartridge
12
+ BANKS = 64
13
+
14
+ attr_reader :flash
15
+
16
+ def clock=(clock)
17
+ super
18
+ @flash.clock = clock
19
+ end
20
+
21
+ def poke(addr, value)
22
+ return if addr > 0xdeff
23
+
24
+ @bank = value & 0x3f
25
+ @flash_writes = value.allbits?(0xc0)
26
+ self.mode = value.anybits?(0x40) ? :off : :rom8k
27
+ select_bank
28
+ end
29
+
30
+ def romh_writes
31
+ @roml if @flash_writes
32
+ end
33
+
34
+ # Reset selects the first bank in 8K mode, and leaves the flash as it
35
+ # is.
36
+ def reset
37
+ @bank = 0
38
+ @flash_writes = false
39
+ self.mode = :rom8k
40
+ select_bank
41
+ end
42
+
43
+ private
44
+
45
+ def select_bank
46
+ @roml = @flash.window(@bank * BANK_SIZE)
47
+ changed!
48
+ end
49
+
50
+ def install_chips(chips)
51
+ data = Array.new(BANKS * BANK_SIZE, 0xff)
52
+ chips.each do |chip|
53
+ bytes = chip.data.first(BANK_SIZE)
54
+ data[(chip.bank & 0x3f) * BANK_SIZE, bytes.length] = bytes
55
+ end
56
+ @flash = Flash.new(data, model: Flash::AM29F040)
57
+ @flash.on_change { select_bank }
58
+ reset
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # C64 Game System / System 3: up to 64 8K banks. Any access to
6
+ # $DE00-$DEFF selects the bank from the low six address bits; the data
7
+ # is ignored.
8
+ class GameSystem < Cartridge
9
+ def readable_io_pages
10
+ [0xde]
11
+ end
12
+
13
+ def peek(addr)
14
+ select(addr) if addr < 0xdf00
15
+ open_bus(addr)
16
+ end
17
+
18
+ def poke(addr, _value)
19
+ select(addr) if addr < 0xdf00
20
+ end
21
+
22
+ def reset
23
+ select(0)
24
+ end
25
+
26
+ private
27
+
28
+ def select(addr)
29
+ @roml = bank(@banks, addr & 0x3f)
30
+ changed!
31
+ end
32
+
33
+ def install_chips(chips)
34
+ @banks = banks_from(chips).first
35
+ self.mode = :rom8k
36
+ reset
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # The KCS Power Cartridge: 16K of ROM, 128 bytes of RAM and a freeze
6
+ # button. It boots in 16K mode, and any access to I/O 1 sets the lines
7
+ # from address bit 1 and the direction:
8
+ #
9
+ # read, bit 1 clear 8K
10
+ # read, bit 1 set off
11
+ # write, bit 1 clear 16K
12
+ # write, bit 1 set Ultimax
13
+ #
14
+ # A read of I/O 1 returns the second last page of ROML. I/O 2 holds the
15
+ # RAM in its lower half, and the upper half reads the EXROM line in bit 7
16
+ # and GAME in bit 6, which the freezer checks to find the mode it
17
+ # interrupted. A freeze switches to Ultimax and lets go of NMI.
18
+ class KCSPower < Cartridge
19
+ include Freezer
20
+
21
+ IO1_ROM_PAGE = 0x1e00
22
+
23
+ def readable_io_pages
24
+ [0xde, 0xdf]
25
+ end
26
+
27
+ def peek(addr)
28
+ if addr < 0xdf00
29
+ switch(addr.anybits?(0x02) ? :off : :rom8k)
30
+ @roml.peek(IO1_ROM_PAGE | (addr & 0xff))
31
+ elsif addr.anybits?(0x80)
32
+ (@exrom << 7) | (@game << 6) | (open_bus(addr) & 0x3f)
33
+ else
34
+ @ram[addr & 0x7f]
35
+ end
36
+ end
37
+
38
+ def poke(addr, value)
39
+ if addr < 0xdf00
40
+ switch(addr.anybits?(0x02) ? :ultimax : :rom16k)
41
+ elsif addr.nobits?(0x80)
42
+ @ram[addr & 0x7f] = value
43
+ end
44
+ end
45
+
46
+ def reset
47
+ self.nmi = false
48
+ switch(:rom16k)
49
+ end
50
+
51
+ def freeze!
52
+ self.nmi = false
53
+ switch(:ultimax)
54
+ end
55
+
56
+ private
57
+
58
+ def switch(mode)
59
+ self.mode = mode
60
+ changed!
61
+ end
62
+
63
+ def install_chips(chips)
64
+ roml, romh = banks_from(chips)
65
+ @roml = roml.first || EMPTY_BANK
66
+ @romh = romh.first || EMPTY_BANK
67
+ @ram = Array.new(0x80, 0xff)
68
+ reset
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # 8K. Writing I/O 1 switches the ROM in, writing I/O 2 switches it out.
6
+ # Both I/O pages read the last 512 bytes of the ROM.
7
+ class Mach5 < Cartridge
8
+ def readable_io_pages
9
+ [0xde, 0xdf]
10
+ end
11
+
12
+ def peek(addr)
13
+ @roml.peek(addr)
14
+ end
15
+
16
+ def poke(addr, _value)
17
+ self.mode = addr < 0xdf00 ? :rom8k : :off
18
+ changed!
19
+ end
20
+
21
+ def reset
22
+ self.mode = :rom8k
23
+ changed!
24
+ end
25
+
26
+ private
27
+
28
+ def install_chips(chips)
29
+ @roml = banks_from(chips).first.compact.first
30
+ reset
31
+ end
32
+ end
33
+ end
34
+ end
@@ -11,12 +11,15 @@ module Badline
11
11
  changed!
12
12
  end
13
13
 
14
+ def reset
15
+ poke(0xde00, 0)
16
+ end
17
+
14
18
  private
15
19
 
16
20
  def install_chips(chips)
17
- @banks = []
18
- chips.each { |chip| @banks[chip.bank] = rom_bank(chip.data, ROML_START) }
19
- @roml = @banks.first
21
+ @banks = banks_from(chips).first
22
+ reset
20
23
  end
21
24
  end
22
25
  end
@@ -6,27 +6,26 @@ module Badline
6
6
  def poke(addr, value)
7
7
  return if addr > 0xdeff
8
8
 
9
- select_bank((value & 0x3f) % @roml_banks.length)
9
+ select_bank((value & 0x3f) % @banks.length)
10
+ changed!
11
+ end
12
+
13
+ def reset
14
+ select_bank(0)
10
15
  changed!
11
16
  end
12
17
 
13
18
  private
14
19
 
20
+ # In 16K mode the selected bank shows through ROMH as well.
15
21
  def select_bank(number)
16
- @roml = @roml_banks[number]
17
- @romh = @romh_banks[number] if @romh_banks
22
+ @roml = @banks[number]
23
+ @romh = @roml if game.zero?
18
24
  end
19
25
 
20
26
  def install_chips(chips)
21
- @roml_banks = []
22
- @romh_banks = game.zero? ? [] : nil
23
- chips.each { |chip| install_chip(chip) }
24
- select_bank(0)
25
- end
26
-
27
- def install_chip(chip)
28
- @roml_banks[chip.bank] = rom_bank(chip.data, ROML_START)
29
- @romh_banks[chip.bank] = rom_bank(chip.data, ROMH_START) if @romh_banks
27
+ @banks = banks_from(chips).first
28
+ reset
30
29
  end
31
30
  end
32
31
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ # Pagefox: two 32K EPROMs and 32K of RAM, seen 16K at a time. A write to
6
+ # $DE80-$DEFF selects the half with bit 1 and the chip with bits 2-3
7
+ # (EPROM, EPROM, RAM, nothing), and switches the cartridge out with
8
+ # bit 4. Writes to the RAM land in the C64's RAM as well.
9
+ class Pagefox < Cartridge
10
+ RAM_CHIP = 2
11
+ EMPTY_CHIP = 3
12
+
13
+ def connect(ram:, open_bus:)
14
+ super
15
+ @ram_banks.each { |b| b.backing = ram }
16
+ end
17
+
18
+ def poke(addr, value)
19
+ return if addr < 0xde80 || addr > 0xdeff
20
+
21
+ select(value)
22
+ changed!
23
+ end
24
+
25
+ def reset
26
+ select(0)
27
+ changed!
28
+ end
29
+
30
+ private
31
+
32
+ def select(value)
33
+ half = (value >> 1) & 0x01
34
+ chip = (value >> 2) & 0x03
35
+ @roml, @romh = window(chip, half)
36
+ self.mode = value.anybits?(0x10) ? :off : :rom16k
37
+ end
38
+
39
+ def window(chip, half)
40
+ case chip
41
+ when RAM_CHIP then @ram_banks[half * 2, 2]
42
+ when EMPTY_CHIP then [@open_bus || EMPTY_BANK] * 2
43
+ else
44
+ number = (chip << 1) | half
45
+ [bank(@roml_banks, number), bank(@romh_banks, number)]
46
+ end
47
+ end
48
+
49
+ def install_chips(chips)
50
+ @roml_banks, @romh_banks = banks_from(chips)
51
+ @ram_banks = Array.new(4) { RAMBank.new }
52
+ reset
53
+ end
54
+ end
55
+ end
56
+ end