badline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +40 -0
  4. data/exe/badline +55 -0
  5. data/lib/badline/address_bus.rb +206 -0
  6. data/lib/badline/addressable.rb +61 -0
  7. data/lib/badline/cartridge/magic_desk.rb +23 -0
  8. data/lib/badline/cartridge/ocean.rb +33 -0
  9. data/lib/badline/cartridge/standard.rb +31 -0
  10. data/lib/badline/cartridge.rb +75 -0
  11. data/lib/badline/chrout_trap.rb +39 -0
  12. data/lib/badline/cia/timer.rb +122 -0
  13. data/lib/badline/cia.rb +189 -0
  14. data/lib/badline/color_memory.rb +16 -0
  15. data/lib/badline/computer.rb +100 -0
  16. data/lib/badline/control_ports.rb +24 -0
  17. data/lib/badline/cpu.rb +239 -0
  18. data/lib/badline/cycleable.rb +35 -0
  19. data/lib/badline/gui/application.rb +94 -0
  20. data/lib/badline/gui/joy_map.rb +19 -0
  21. data/lib/badline/gui/key_map.rb +34 -0
  22. data/lib/badline/gui/palette.rb +35 -0
  23. data/lib/badline/gui/pane.rb +35 -0
  24. data/lib/badline/gui/screen_pane.rb +50 -0
  25. data/lib/badline/gui/window.rb +46 -0
  26. data/lib/badline/gui.rb +11 -0
  27. data/lib/badline/instruction.rb +334 -0
  28. data/lib/badline/instruction_set/arithmetic.rb +119 -0
  29. data/lib/badline/instruction_set/bitwise.rb +131 -0
  30. data/lib/badline/instruction_set/branch.rb +78 -0
  31. data/lib/badline/instruction_set/flag.rb +63 -0
  32. data/lib/badline/instruction_set/illegal.rb +278 -0
  33. data/lib/badline/instruction_set/inc_dec.rb +71 -0
  34. data/lib/badline/instruction_set/stack.rb +104 -0
  35. data/lib/badline/instruction_set/transfer.rb +137 -0
  36. data/lib/badline/instruction_set.rb +77 -0
  37. data/lib/badline/integer_helper.rb +39 -0
  38. data/lib/badline/joystick.rb +25 -0
  39. data/lib/badline/kernal_trap/file.rb +54 -0
  40. data/lib/badline/kernal_trap/load.rb +63 -0
  41. data/lib/badline/kernal_trap/save.rb +42 -0
  42. data/lib/badline/kernal_trap.rb +5 -0
  43. data/lib/badline/keyboard.rb +58 -0
  44. data/lib/badline/keyboard_buffer.rb +33 -0
  45. data/lib/badline/media.rb +59 -0
  46. data/lib/badline/memory.rb +43 -0
  47. data/lib/badline/rom.rb +23 -0
  48. data/lib/badline/roms/README +18 -0
  49. data/lib/badline/roms/basic.rom +0 -0
  50. data/lib/badline/roms/character.rom +0 -0
  51. data/lib/badline/roms/kernal.rom +0 -0
  52. data/lib/badline/sid.rb +25 -0
  53. data/lib/badline/status.rb +56 -0
  54. data/lib/badline/storage/crt_file.rb +53 -0
  55. data/lib/badline/storage/d64_image.rb +21 -0
  56. data/lib/badline/storage/d71_image.rb +13 -0
  57. data/lib/badline/storage/d81_image.rb +14 -0
  58. data/lib/badline/storage/disk_image.rb +71 -0
  59. data/lib/badline/storage/host_directory.rb +49 -0
  60. data/lib/badline/storage/p00.rb +24 -0
  61. data/lib/badline/storage.rb +28 -0
  62. data/lib/badline/time_of_day.rb +101 -0
  63. data/lib/badline/traps.rb +15 -0
  64. data/lib/badline/version.rb +5 -0
  65. data/lib/badline/vic/bank.rb +65 -0
  66. data/lib/badline/vic/display_state.rb +78 -0
  67. data/lib/badline/vic/graphics_mode.rb +139 -0
  68. data/lib/badline/vic/registers.rb +170 -0
  69. data/lib/badline/vic/sequencer.rb +237 -0
  70. data/lib/badline/vic/sprite.rb +121 -0
  71. data/lib/badline/vic/sprites.rb +112 -0
  72. data/lib/badline/vic.rb +192 -0
  73. data/lib/badline.rb +29 -0
  74. metadata +131 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ee3a30a07439b0a0dbffe352b5667945721a7bc84fc594b62e79e007d255b801
4
+ data.tar.gz: 2737e2a0dc04a81d264506aa406dc9cb3a661f37fdc3ec5135976ce5a654d558
5
+ SHA512:
6
+ metadata.gz: d450522ea11e666044fceff4d1c0ab24f3bfb384c04d71914c3eb8a4bd62319911da97873101a6e1a759b053cdac2a7c421aaa4d7d16a55dc30eb05e5ce01a8d
7
+ data.tar.gz: 4cc7bdaa7cfdc414b0e1e8e5104115a8d5ff465428492165ca9880222cbbd56d06e45db9625f51b18f57956c5a9b86e51e442d42504b70c63ba2058c6bc1a091
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Inge Jørgensen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ ![Build](https://github.com/elektronaut/badline/workflows/Build/badge.svg)
2
+ [![Code Climate](https://codeclimate.com/github/elektronaut/badline/badges/gpa.svg)](https://codeclimate.com/github/elektronaut/badline)
3
+ [![Code Climate](https://codeclimate.com/github/elektronaut/badline/badges/coverage.svg)](https://codeclimate.com/github/elektronaut/badline)
4
+
5
+ # Badline
6
+
7
+ Badline is a Commodore 64 emulator in written in Ruby. It is cycle accurate,
8
+ utilizing Fibers to emulate cycles.
9
+
10
+ Currently the memory map and 6510 CPU is working.
11
+
12
+ ## TODO
13
+
14
+ - VIC-II emulation
15
+ - CIA 1/2
16
+ - C1541 emulation
17
+ - SID emulation?
18
+
19
+ ## License
20
+
21
+ Copyright 2016 Inge Jørgensen
22
+
23
+ Permission is hereby granted, free of charge, to any person obtaining
24
+ a copy of this software and associated documentation files (the
25
+ "Software"), to deal in the Software without restriction, including
26
+ without limitation the rights to use, copy, modify, merge, publish,
27
+ distribute, sublicense, and/or sell copies of the Software, and to
28
+ permit persons to whom the Software is furnished to do so, subject to
29
+ the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be
32
+ included in all copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
35
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
36
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
37
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
38
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
39
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/exe/badline ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require "optparse"
8
+
9
+ options = { autostart: true, jit: true }
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = <<~BANNER
13
+ Usage: badline [options] [media]
14
+
15
+ Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image,
16
+ a .crt cartridge, or a directory to mount as device 8.
17
+
18
+ Options:
19
+ BANNER
20
+
21
+ opts.on("--no-autostart", "Boot to READY. instead of running the program") do
22
+ options[:autostart] = false
23
+ end
24
+
25
+ opts.on("--disable-jit", "Run without enabling YJIT") do
26
+ options[:jit] = false
27
+ end
28
+
29
+ opts.on("-h", "--help", "Show this help") do
30
+ puts opts
31
+ exit
32
+ end
33
+ end
34
+
35
+ begin
36
+ parser.parse!
37
+ rescue OptionParser::ParseError => e
38
+ warn "badline: #{e.message}"
39
+ warn "Try 'badline --help' for more information."
40
+ exit 1
41
+ end
42
+
43
+ media_path = ARGV[0]
44
+ if media_path && !File.exist?(media_path)
45
+ warn "badline: no such file or directory: #{media_path}"
46
+ exit 1
47
+ end
48
+
49
+ jit_available = defined?(RubyVM::YJIT) && !RubyVM::YJIT.enabled?
50
+ RubyVM::YJIT.enable if options[:jit] && jit_available
51
+
52
+ require "badline"
53
+ require "badline/gui"
54
+
55
+ Badline::GUI::Application.new(media_path:, autostart: options[:autostart]).run
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # Memory layout:
5
+ #
6
+ # 0x0000-0x00FF - Page 0 - Zeropage
7
+ # 0x0100-0x01FF - Page 1 - Stack
8
+ # 0x0200-0x02FF - Page 2 - OS/BASIC pointers
9
+ # 0x0300-0x03FF - Page 3 - OS/BASIC pointers
10
+ # 0x0400-0x07FF - Page 4-7 - Screen memory
11
+ # 0x0800-0x9FFF - Page 8-159 - BASIC program storage area
12
+ # 0xA000-0xBFFF - Page 160-191 - Machine code program storage (ROM overlay)
13
+ # 0xC000-0xCFFF - Page 192-207 - Machine code program storage
14
+ # 0xD000-0xD3FF - Page 208-211 - VIC II registers
15
+ # 0xD400-0xD7FF - Page 212-215 - SID registers
16
+ # 0xD800-0xDBFF - Page 216-219 - Color memory
17
+ # 0xDC00-0xDCFF - Page 220 - CIA 1
18
+ # 0xDD00-0xDDFF - Page 221 - CIA 2
19
+ # 0xDE00-0xDEFF - Page 222 - I/O 1
20
+ # 0xDF00-0xDFFF - Page 223 - I/O 2
21
+ # 0xE000-0xFFFF - Page 224-255 - Machine code program storage (ROM overlay)
22
+
23
+ # Overlays:
24
+ #
25
+ # 0x8000-0x9FFF - Cartridge ROM (low) - 8kb
26
+ # 0xA000-0xBFFF - BASIC ROM / Cartridge ROM (high) - 8kb
27
+ # 0xD000-0xDFFF - Character ROM / I/O - 4kb
28
+ # 0xE000-0xFFFF - KERNAL ROM / Cartridge ROM (high) - 8kb
29
+ class AddressBus
30
+ include Addressable
31
+
32
+ # Unmapped address space for Ultimax cartridges.
33
+ module OpenSpace
34
+ module_function
35
+
36
+ def peek(_addr) = 0xff
37
+ def poke(_addr, _value); end
38
+ end
39
+
40
+ PORT_PULLUPS = 0b0001_0111
41
+ PORT_FLOATING = 0b1100_1000
42
+
43
+ attr_reader :io_port, :ram, :basic_rom, :character_rom, :kernal_rom,
44
+ :vic, :sid, :color_ram, :cia1, :cia2, :keyboard, :joystick2,
45
+ :cartridge, :ultimax
46
+
47
+ def initialize
48
+ @ram = Memory.new([0xff, 0x07], length: 2**16, start: 0)
49
+ @cartridge = nil
50
+
51
+ @basic_rom = ROM.load("basic.rom", 0xa000)
52
+ @character_rom = ROM.load("character.rom", 0xd000)
53
+ @kernal_rom = ROM.load("kernal.rom", 0xe000)
54
+
55
+ @keyboard = Keyboard.new
56
+ @joystick2 = Joystick.new
57
+ @vic = VIC.new(self)
58
+ @cia1 = CIA.new(
59
+ start: 0xdc00,
60
+ peripheral: ControlPorts.new(keyboard: @keyboard, joystick2: @joystick2)
61
+ )
62
+ @cia2 = CIA.new(start: 0xdd00)
63
+ @sid = SID.new
64
+
65
+ @color_ram = ColorMemory.new(start: 0xd800, length: 2**10)
66
+
67
+ @port_ddr = 0x2f
68
+ @port_out = 0x37
69
+ @port_floating = 0x00
70
+ @io_port = Status.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
71
+
72
+ @read_pages = Array.new(256)
73
+ @write_pages = Array.new(256)
74
+ update_overlays!
75
+ end
76
+
77
+ def attach_cartridge(cartridge)
78
+ @cartridge = cartridge
79
+ cartridge.on_change { update_overlays! }
80
+ update_overlays!
81
+ end
82
+
83
+ def disable_overlays!
84
+ poke(1, 0)
85
+ end
86
+
87
+ def peek(addr)
88
+ return @port_ddr if addr.zero?
89
+ return @io_port.value if addr == 0x01
90
+
91
+ @read_pages[addr >> 8].peek(addr)
92
+ end
93
+
94
+ def poke(addr, value)
95
+ if addr < 0x02
96
+ addr.zero? ? @port_ddr = value : @port_out = value
97
+ update_port!
98
+ else
99
+ @write_pages[addr >> 8].poke(addr, value)
100
+ end
101
+ end
102
+
103
+ def inspect
104
+ "#<#{self.class.name} port=#{format('0x%02x', @io_port.value)} " \
105
+ "cartridge=#{@cartridge ? @cartridge.class.name : 'none'} " \
106
+ "ultimax=#{@ultimax}>"
107
+ end
108
+
109
+ private
110
+
111
+ def update_port!
112
+ driven = @port_ddr & PORT_FLOATING
113
+ @port_floating = (@port_floating & ~driven) | (@port_out & driven)
114
+ @io_port.value = port_value
115
+ update_overlays!
116
+ end
117
+
118
+ def port_value
119
+ input = PORT_PULLUPS | (@port_floating & PORT_FLOATING)
120
+ (@port_out & @port_ddr) | (input & ~@port_ddr & 0xff)
121
+ end
122
+
123
+ # Banking changes only on $01 writes and cartridge line/bank changes,
124
+ # so reads and writes dispatch through per-page handler tables instead
125
+ # of range checks.
126
+ def update_overlays!
127
+ @ultimax = @cartridge ? @cartridge.ultimax? : false
128
+ @read_pages.fill(@ram)
129
+ @write_pages.fill(@ram)
130
+
131
+ @ultimax ? map_ultimax_pages : map_banked_pages
132
+ end
133
+
134
+ def map_banked_pages
135
+ map_rom_overlays
136
+
137
+ if io?
138
+ map_io_pages
139
+ elsif character?
140
+ @read_pages.fill(character_rom, 0xd0, 0x10)
141
+ end
142
+ end
143
+
144
+ def map_rom_overlays
145
+ @read_pages.fill(@cartridge.roml, 0x80, 0x20) if roml?
146
+ if romh?
147
+ @read_pages.fill(@cartridge.romh, 0xa0, 0x20)
148
+ elsif basic?
149
+ @read_pages.fill(basic_rom, 0xa0, 0x20)
150
+ end
151
+ @read_pages.fill(kernal_rom, 0xe0, 0x20) if kernal?
152
+ end
153
+
154
+ # Ultimax cartridges ignore the $01 lines: 4K of RAM, ROML/ROMH windows,
155
+ # I/O always visible and open address space everywhere else.
156
+ def map_ultimax_pages
157
+ @read_pages.fill(OpenSpace, 0x10, 0xf0)
158
+ @write_pages.fill(OpenSpace, 0x10, 0xf0)
159
+ @read_pages.fill(@cartridge.roml, 0x80, 0x20) if @cartridge.roml
160
+ @read_pages.fill(@cartridge.romh, 0xe0, 0x20) if @cartridge.romh
161
+ map_io_pages
162
+ end
163
+
164
+ def map_io_pages
165
+ {
166
+ vic => 0xd0..0xd3, sid => 0xd4..0xd7, color_ram => 0xd8..0xdb,
167
+ cia1 => 0xdc..0xdc, cia2 => 0xdd..0xdd
168
+ # 0xde/0xdf are open I/O unless a cartridge claims them
169
+ }.each do |chip, pages|
170
+ pages.each { |p| @read_pages[p] = @write_pages[p] = chip }
171
+ end
172
+ return unless @cartridge
173
+
174
+ @read_pages.fill(@cartridge, 0xde, 2)
175
+ @write_pages.fill(@cartridge, 0xde, 2)
176
+ end
177
+
178
+ def basic?
179
+ io_port.kernal? && io_port.basic? && game_high?
180
+ end
181
+
182
+ def game_high?
183
+ @cartridge.nil? || @cartridge.game == 1
184
+ end
185
+
186
+ def roml?
187
+ @cartridge&.roml && @cartridge.exrom.zero? && io_port.kernal? && io_port.basic?
188
+ end
189
+
190
+ def romh?
191
+ @cartridge&.romh && @cartridge.exrom.zero? && @cartridge.game.zero? && io_port.kernal?
192
+ end
193
+
194
+ def character?
195
+ (io_port.basic? || io_port.kernal?) && !io_port.io?
196
+ end
197
+
198
+ def io?
199
+ (io_port.basic? || io_port.kernal?) && io_port.io?
200
+ end
201
+
202
+ def kernal?
203
+ io_port.kernal?
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Addressable
5
+ include IntegerHelper
6
+
7
+ class ReadOnlyMemoryError < StandardError; end
8
+ class OutOfBoundsError < StandardError; end
9
+
10
+ attr_reader :start, :length, :end
11
+
12
+ def addressable_at(start = 0, length: 2**16)
13
+ @start = start
14
+ @length = length
15
+ @end = start + length
16
+ end
17
+
18
+ def range
19
+ start..(start + (length - 1))
20
+ end
21
+
22
+ def in_range?(addr)
23
+ addr >= @start && addr < @end
24
+ end
25
+
26
+ def peek(_addr)
27
+ raise NoMethodError
28
+ end
29
+
30
+ def peek16(addr)
31
+ uint16(peek(addr), peek(addr + 1))
32
+ end
33
+
34
+ def poke(_addr, _value)
35
+ raise NoMethodError
36
+ end
37
+
38
+ def poke16(addr, value)
39
+ poke(addr, low_byte(value))
40
+ poke(addr + 1, high_byte(value))
41
+ value
42
+ end
43
+
44
+ def [](addr)
45
+ peek(addr)
46
+ end
47
+
48
+ def []=(addr, value)
49
+ poke(addr, value)
50
+ end
51
+
52
+ private
53
+
54
+ def index(addr)
55
+ i = addr - @start
56
+ raise OutOfBoundsError, "#{addr.inspect} (#{range})" unless i >= 0 && i < @length
57
+
58
+ i
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ class MagicDesk < Cartridge
6
+ def poke(addr, value)
7
+ return if addr > 0xdeff
8
+
9
+ @exrom = value.anybits?(0x80) ? 1 : 0
10
+ @roml = @banks[(value & 0x3f) % @banks.length]
11
+ changed!
12
+ end
13
+
14
+ private
15
+
16
+ def install_chips(chips)
17
+ @banks = []
18
+ chips.each { |chip| @banks[chip.bank] = rom_bank(chip.data, ROML_START) }
19
+ @roml = @banks.first
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ class Ocean < Cartridge
6
+ def poke(addr, value)
7
+ return if addr > 0xdeff
8
+
9
+ select_bank((value & 0x3f) % @roml_banks.length)
10
+ changed!
11
+ end
12
+
13
+ private
14
+
15
+ def select_bank(number)
16
+ @roml = @roml_banks[number]
17
+ @romh = @romh_banks[number] if @romh_banks
18
+ end
19
+
20
+ 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
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Cartridge
5
+ class Standard < Cartridge
6
+ private
7
+
8
+ def install_chips(chips)
9
+ chips.each { |chip| install_chip(chip) }
10
+ end
11
+
12
+ def install_chip(chip)
13
+ if chip.address >= ULTIMAX_ROMH_START
14
+ @romh = rom_bank(chip.data, ULTIMAX_ROMH_START)
15
+ elsif chip.address >= ROMH_START
16
+ @romh = rom_bank(chip.data, ROMH_START)
17
+ elsif chip.data.length > BANK_SIZE
18
+ install_split_chip(chip)
19
+ else
20
+ @roml = rom_bank(chip.data, ROML_START)
21
+ end
22
+ end
23
+
24
+ def install_split_chip(chip)
25
+ # A single 16K chip at $8000 spans both ROML and ROMH.
26
+ @roml = rom_bank(chip.data[0, BANK_SIZE], ROML_START)
27
+ @romh = rom_bank(chip.data[BANK_SIZE, BANK_SIZE], ROMH_START)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/cartridge/standard"
4
+ require "badline/cartridge/ocean"
5
+ require "badline/cartridge/magic_desk"
6
+
7
+ module Badline
8
+ class Cartridge
9
+ class UnsupportedTypeError < StandardError; end
10
+
11
+ ROML_START = 0x8000
12
+ ROMH_START = 0xa000
13
+ ULTIMAX_ROMH_START = 0xe000
14
+ BANK_SIZE = 0x2000
15
+
16
+ HARDWARE_TYPES = { 0 => :Standard, 5 => :Ocean, 19 => :MagicDesk }.freeze
17
+
18
+ attr_reader :name, :exrom, :game, :roml, :romh
19
+
20
+ class << self
21
+ def from_file(path)
22
+ from_crt(Storage::CRTFile.new(path))
23
+ end
24
+
25
+ def from_crt(crt)
26
+ type = HARDWARE_TYPES.fetch(crt.hardware_type) do
27
+ raise UnsupportedTypeError,
28
+ "Unsupported cartridge hardware type #{crt.hardware_type}"
29
+ end
30
+ const_get(type).new(crt)
31
+ end
32
+ end
33
+
34
+ def initialize(crt)
35
+ @name = crt.name
36
+ @exrom = crt.exrom
37
+ @game = crt.game
38
+ @roml = @romh = nil
39
+ @on_change = nil
40
+ install_chips(crt.chips)
41
+ end
42
+
43
+ def on_change(&block)
44
+ @on_change = block
45
+ end
46
+
47
+ def ultimax?
48
+ @game.zero? && @exrom == 1
49
+ end
50
+
51
+ # IO1/IO2 reads are open bus unless a mapper says otherwise.
52
+ def peek(_addr)
53
+ 0xff
54
+ end
55
+
56
+ def poke(_addr, _value); end
57
+
58
+ private
59
+
60
+ def changed!
61
+ @on_change&.call
62
+ end
63
+
64
+ def install_chips(_chips)
65
+ raise NotImplementedError
66
+ end
67
+
68
+ # Chips smaller than 8K (e.g. 4K Ultimax ROMs) have unconnected upper
69
+ # address lines and mirror across the full bank.
70
+ def rom_bank(data, start)
71
+ data *= BANK_SIZE / data.length if data.length < BANK_SIZE
72
+ ROM.new(data, length: data.length, start:)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # Observe-only PC trap on the KERNAL CHROUT routine ($FFD2). Records each
5
+ # character written while the KERNAL is banked in, then lets execution fall
6
+ # through to the ROM, so the screen output is unaffected. Used for headless
7
+ # capture of program output.
8
+ class ChroutTrap
9
+ ADDRESS = 0xffd2
10
+
11
+ attr_reader :output
12
+
13
+ def initialize(cpu:, bus:)
14
+ @cpu = cpu
15
+ @bus = bus
16
+ @output = +""
17
+ end
18
+
19
+ def call
20
+ @output << ascii(@cpu.a) if @bus.io_port.kernal?
21
+ end
22
+
23
+ def inspect
24
+ "#<#{self.class.name} output=#{@output.inspect}>"
25
+ end
26
+
27
+ private
28
+
29
+ def ascii(byte)
30
+ case byte
31
+ when 0x0d then "\n"
32
+ when 0x41..0x5a then (byte + 0x20).chr
33
+ when 0xc1..0xda then (byte - 0x80).chr
34
+ when 0x20..0x7e then byte.chr
35
+ else ""
36
+ end
37
+ end
38
+ end
39
+ end