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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +40 -0
- data/exe/badline +55 -0
- data/lib/badline/address_bus.rb +206 -0
- data/lib/badline/addressable.rb +61 -0
- data/lib/badline/cartridge/magic_desk.rb +23 -0
- data/lib/badline/cartridge/ocean.rb +33 -0
- data/lib/badline/cartridge/standard.rb +31 -0
- data/lib/badline/cartridge.rb +75 -0
- data/lib/badline/chrout_trap.rb +39 -0
- data/lib/badline/cia/timer.rb +122 -0
- data/lib/badline/cia.rb +189 -0
- data/lib/badline/color_memory.rb +16 -0
- data/lib/badline/computer.rb +100 -0
- data/lib/badline/control_ports.rb +24 -0
- data/lib/badline/cpu.rb +239 -0
- data/lib/badline/cycleable.rb +35 -0
- data/lib/badline/gui/application.rb +94 -0
- data/lib/badline/gui/joy_map.rb +19 -0
- data/lib/badline/gui/key_map.rb +34 -0
- data/lib/badline/gui/palette.rb +35 -0
- data/lib/badline/gui/pane.rb +35 -0
- data/lib/badline/gui/screen_pane.rb +50 -0
- data/lib/badline/gui/window.rb +46 -0
- data/lib/badline/gui.rb +11 -0
- data/lib/badline/instruction.rb +334 -0
- data/lib/badline/instruction_set/arithmetic.rb +119 -0
- data/lib/badline/instruction_set/bitwise.rb +131 -0
- data/lib/badline/instruction_set/branch.rb +78 -0
- data/lib/badline/instruction_set/flag.rb +63 -0
- data/lib/badline/instruction_set/illegal.rb +278 -0
- data/lib/badline/instruction_set/inc_dec.rb +71 -0
- data/lib/badline/instruction_set/stack.rb +104 -0
- data/lib/badline/instruction_set/transfer.rb +137 -0
- data/lib/badline/instruction_set.rb +77 -0
- data/lib/badline/integer_helper.rb +39 -0
- data/lib/badline/joystick.rb +25 -0
- data/lib/badline/kernal_trap/file.rb +54 -0
- data/lib/badline/kernal_trap/load.rb +63 -0
- data/lib/badline/kernal_trap/save.rb +42 -0
- data/lib/badline/kernal_trap.rb +5 -0
- data/lib/badline/keyboard.rb +58 -0
- data/lib/badline/keyboard_buffer.rb +33 -0
- data/lib/badline/media.rb +59 -0
- data/lib/badline/memory.rb +43 -0
- data/lib/badline/rom.rb +23 -0
- data/lib/badline/roms/README +18 -0
- data/lib/badline/roms/basic.rom +0 -0
- data/lib/badline/roms/character.rom +0 -0
- data/lib/badline/roms/kernal.rom +0 -0
- data/lib/badline/sid.rb +25 -0
- data/lib/badline/status.rb +56 -0
- data/lib/badline/storage/crt_file.rb +53 -0
- data/lib/badline/storage/d64_image.rb +21 -0
- data/lib/badline/storage/d71_image.rb +13 -0
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image.rb +71 -0
- data/lib/badline/storage/host_directory.rb +49 -0
- data/lib/badline/storage/p00.rb +24 -0
- data/lib/badline/storage.rb +28 -0
- data/lib/badline/time_of_day.rb +101 -0
- data/lib/badline/traps.rb +15 -0
- data/lib/badline/version.rb +5 -0
- data/lib/badline/vic/bank.rb +65 -0
- data/lib/badline/vic/display_state.rb +78 -0
- data/lib/badline/vic/graphics_mode.rb +139 -0
- data/lib/badline/vic/registers.rb +170 -0
- data/lib/badline/vic/sequencer.rb +237 -0
- data/lib/badline/vic/sprite.rb +121 -0
- data/lib/badline/vic/sprites.rb +112 -0
- data/lib/badline/vic.rb +192 -0
- data/lib/badline.rb +29 -0
- 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
|
+

|
|
2
|
+
[](https://codeclimate.com/github/elektronaut/badline)
|
|
3
|
+
[](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
|