amaterasu 0.6.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/.gitignore +14 -0
- data/.rspec +1 -0
- data/.rubocop.yml +54 -0
- data/Gemfile +32 -0
- data/Gemfile.lock +267 -0
- data/LICENSE +21 -0
- data/README.md +115 -0
- data/Steepfile +7 -0
- data/exe/amaterasu +23 -0
- data/lib/amaterasu/cartridge/mbc1.rb +56 -0
- data/lib/amaterasu/cartridge/rom.rb +118 -0
- data/lib/amaterasu/cartridge.rb +68 -0
- data/lib/amaterasu/cli.rb +60 -0
- data/lib/amaterasu/emulator.rb +121 -0
- data/lib/amaterasu/game_boy/apu.rb +12 -0
- data/lib/amaterasu/game_boy/bus.rb +161 -0
- data/lib/amaterasu/game_boy/cpu/instructions/adc.rb +64 -0
- data/lib/amaterasu/game_boy/cpu/instructions/add16.rb +73 -0
- data/lib/amaterasu/game_boy/cpu/instructions/add8.rb +63 -0
- data/lib/amaterasu/game_boy/cpu/instructions/and.rb +62 -0
- data/lib/amaterasu/game_boy/cpu/instructions/base.rb +38 -0
- data/lib/amaterasu/game_boy/cpu/instructions/call.rb +48 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_bit.rb +52 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_res.rb +49 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_rl.rb +70 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_rlc.rb +68 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_rr.rb +70 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_rrc.rb +68 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_set.rb +51 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_sla.rb +69 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_sra.rb +71 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_srl.rb +69 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cb_swap.rb +67 -0
- data/lib/amaterasu/game_boy/cpu/instructions/cp.rb +61 -0
- data/lib/amaterasu/game_boy/cpu/instructions/daa.rb +59 -0
- data/lib/amaterasu/game_boy/cpu/instructions/dec.rb +64 -0
- data/lib/amaterasu/game_boy/cpu/instructions/di.rb +21 -0
- data/lib/amaterasu/game_boy/cpu/instructions/ei.rb +19 -0
- data/lib/amaterasu/game_boy/cpu/instructions/halt.rb +19 -0
- data/lib/amaterasu/game_boy/cpu/instructions/inc.rb +64 -0
- data/lib/amaterasu/game_boy/cpu/instructions/jp.rb +54 -0
- data/lib/amaterasu/game_boy/cpu/instructions/jr.rb +45 -0
- data/lib/amaterasu/game_boy/cpu/instructions/ld16.rb +79 -0
- data/lib/amaterasu/game_boy/cpu/instructions/ld8.rb +210 -0
- data/lib/amaterasu/game_boy/cpu/instructions/ldh.rb +61 -0
- data/lib/amaterasu/game_boy/cpu/instructions/misc.rb +53 -0
- data/lib/amaterasu/game_boy/cpu/instructions/nop.rb +19 -0
- data/lib/amaterasu/game_boy/cpu/instructions/or.rb +56 -0
- data/lib/amaterasu/game_boy/cpu/instructions/pop.rb +39 -0
- data/lib/amaterasu/game_boy/cpu/instructions/push.rb +43 -0
- data/lib/amaterasu/game_boy/cpu/instructions/ret.rb +70 -0
- data/lib/amaterasu/game_boy/cpu/instructions/rotate.rb +120 -0
- data/lib/amaterasu/game_boy/cpu/instructions/rst.rb +33 -0
- data/lib/amaterasu/game_boy/cpu/instructions/sbc.rb +64 -0
- data/lib/amaterasu/game_boy/cpu/instructions/stop.rb +19 -0
- data/lib/amaterasu/game_boy/cpu/instructions/sub.rb +63 -0
- data/lib/amaterasu/game_boy/cpu/instructions/xor.rb +60 -0
- data/lib/amaterasu/game_boy/cpu/instructions.rb +600 -0
- data/lib/amaterasu/game_boy/cpu/registers.rb +264 -0
- data/lib/amaterasu/game_boy/cpu.rb +232 -0
- data/lib/amaterasu/game_boy/dma.rb +114 -0
- data/lib/amaterasu/game_boy/interrupts.rb +108 -0
- data/lib/amaterasu/game_boy/joypad.rb +127 -0
- data/lib/amaterasu/game_boy/oam/sprite.rb +106 -0
- data/lib/amaterasu/game_boy/oam.rb +29 -0
- data/lib/amaterasu/game_boy/ppu/modes/disabled.rb +29 -0
- data/lib/amaterasu/game_boy/ppu/modes/h_blank.rb +45 -0
- data/lib/amaterasu/game_boy/ppu/modes/oam_scan.rb +93 -0
- data/lib/amaterasu/game_boy/ppu/modes/rendering/bg_win_fetcher.rb +204 -0
- data/lib/amaterasu/game_boy/ppu/modes/rendering/pixel_emitter.rb +83 -0
- data/lib/amaterasu/game_boy/ppu/modes/rendering/pixel_fifo.rb +70 -0
- data/lib/amaterasu/game_boy/ppu/modes/rendering/sprite_fetcher.rb +140 -0
- data/lib/amaterasu/game_boy/ppu/modes/rendering.rb +108 -0
- data/lib/amaterasu/game_boy/ppu/modes/v_blank.rb +43 -0
- data/lib/amaterasu/game_boy/ppu/modes.rb +22 -0
- data/lib/amaterasu/game_boy/ppu/registers/lcd_control.rb +57 -0
- data/lib/amaterasu/game_boy/ppu/registers/lcd_status.rb +88 -0
- data/lib/amaterasu/game_boy/ppu/registers.rb +131 -0
- data/lib/amaterasu/game_boy/ppu.rb +207 -0
- data/lib/amaterasu/game_boy/ram.rb +70 -0
- data/lib/amaterasu/game_boy/serial.rb +91 -0
- data/lib/amaterasu/game_boy/timer.rb +230 -0
- data/lib/amaterasu/game_boy/vram/tile.rb +68 -0
- data/lib/amaterasu/game_boy/vram/tile_data.rb +52 -0
- data/lib/amaterasu/game_boy/vram/tile_map.rb +71 -0
- data/lib/amaterasu/game_boy/vram.rb +51 -0
- data/lib/amaterasu/hal/console.rb +23 -0
- data/lib/amaterasu/hal/sdl2/bindings.rb +59 -0
- data/lib/amaterasu/hal/sdl2.rb +127 -0
- data/lib/amaterasu/utils/bit_ops.rb +22 -0
- data/lib/amaterasu.rb +13 -0
- data/sig/akane/cartridge/rom.rbs +29 -0
- data/sig/akane/cartridge.rbs +12 -0
- data/sig/akane/cli.rbs +16 -0
- data/sig/akane/emulator.rbs +19 -0
- data/sig/akane/game_boy/apu.rbs +7 -0
- data/sig/akane/game_boy/bus.rbs +25 -0
- data/sig/akane/game_boy/cpu/instructions/adc.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/add16.rbs +19 -0
- data/sig/akane/game_boy/cpu/instructions/add8.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/and.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/base.rbs +20 -0
- data/sig/akane/game_boy/cpu/instructions/call.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/cb_bit.rbs +20 -0
- data/sig/akane/game_boy/cpu/instructions/cb_res.rbs +19 -0
- data/sig/akane/game_boy/cpu/instructions/cb_rl.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_rlc.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_rr.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_rrc.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_set.rbs +20 -0
- data/sig/akane/game_boy/cpu/instructions/cb_sla.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_sra.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_srl.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cb_swap.rbs +21 -0
- data/sig/akane/game_boy/cpu/instructions/cp.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/daa.rbs +17 -0
- data/sig/akane/game_boy/cpu/instructions/dec.rbs +19 -0
- data/sig/akane/game_boy/cpu/instructions/di.rbs +13 -0
- data/sig/akane/game_boy/cpu/instructions/ei.rbs +13 -0
- data/sig/akane/game_boy/cpu/instructions/halt.rbs +13 -0
- data/sig/akane/game_boy/cpu/instructions/inc.rbs +19 -0
- data/sig/akane/game_boy/cpu/instructions/jp.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/jr.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/ld16.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/ld8.rbs +31 -0
- data/sig/akane/game_boy/cpu/instructions/ldh.rbs +23 -0
- data/sig/akane/game_boy/cpu/instructions/misc.rbs +20 -0
- data/sig/akane/game_boy/cpu/instructions/nop.rbs +13 -0
- data/sig/akane/game_boy/cpu/instructions/or.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/pop.rbs +17 -0
- data/sig/akane/game_boy/cpu/instructions/push.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/ret.rbs +20 -0
- data/sig/akane/game_boy/cpu/instructions/rotate.rbs +23 -0
- data/sig/akane/game_boy/cpu/instructions/rst.rbs +17 -0
- data/sig/akane/game_boy/cpu/instructions/sbc.rbs +19 -0
- data/sig/akane/game_boy/cpu/instructions/stop.rbs +13 -0
- data/sig/akane/game_boy/cpu/instructions/sub.rbs +18 -0
- data/sig/akane/game_boy/cpu/instructions/xor.rbs +19 -0
- data/sig/akane/game_boy/cpu/instructions.rbs +12 -0
- data/sig/akane/game_boy/cpu/registers.rbs +56 -0
- data/sig/akane/game_boy/cpu.rbs +39 -0
- data/sig/akane/game_boy/interrupts.rbs +28 -0
- data/sig/akane/game_boy/joypad.rbs +25 -0
- data/sig/akane/game_boy/oam/sprite.rbs +30 -0
- data/sig/akane/game_boy/ppu/modes/disabled.rbs +17 -0
- data/sig/akane/game_boy/ppu/modes/h_blank.rbs +20 -0
- data/sig/akane/game_boy/ppu/modes/oam_scan.rbs +28 -0
- data/sig/akane/game_boy/ppu/modes/rendering.rbs +26 -0
- data/sig/akane/game_boy/ppu/modes/v_blank.rbs +20 -0
- data/sig/akane/game_boy/ppu/modes.rbs +13 -0
- data/sig/akane/game_boy/ppu.rbs +59 -0
- data/sig/akane/game_boy/ram.rbs +16 -0
- data/sig/akane/game_boy/serial.rbs +21 -0
- data/sig/akane/game_boy/timer.rbs +30 -0
- data/sig/akane/utils/bit_ops.rbs +11 -0
- data/sig/akane.rbs +3 -0
- metadata +226 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amaterasu
|
|
4
|
+
class Cartridge
|
|
5
|
+
# models the ROM inside the Game Boy cartridge.
|
|
6
|
+
class Rom
|
|
7
|
+
CARTRIDGE_TYPES = {
|
|
8
|
+
0x00 => 'ROM ONLY',
|
|
9
|
+
0x01 => 'MBC1',
|
|
10
|
+
0x02 => 'MBC1+RAM',
|
|
11
|
+
0x03 => 'MBC1+RAM+BATTERY',
|
|
12
|
+
0x05 => 'MBC2'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
ROM_SIZES = {
|
|
16
|
+
0x00 => 32 * 1024,
|
|
17
|
+
0x01 => 64 * 1024,
|
|
18
|
+
0x02 => 128 * 1024,
|
|
19
|
+
0x03 => 256 * 1024,
|
|
20
|
+
0x04 => 512 * 1024,
|
|
21
|
+
0x05 => 1024 * 1024,
|
|
22
|
+
0x06 => 2 * 1024 * 1024,
|
|
23
|
+
0x07 => 4 * 1024 * 1024,
|
|
24
|
+
0x08 => 8 * 1024 * 1024
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
RAM_SIZES = {
|
|
28
|
+
0x00 => 0,
|
|
29
|
+
0x02 => 8 * 1024,
|
|
30
|
+
0x03 => 32 * 1024,
|
|
31
|
+
0x04 => 128 * 1024,
|
|
32
|
+
0x05 => 64 * 1024
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def self.from_file(file_path)
|
|
36
|
+
new(File.binread(file_path).bytes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Creates a ROM object given the bytes array and freezes it (read-only).
|
|
40
|
+
def initialize(data)
|
|
41
|
+
@data = data
|
|
42
|
+
@data.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns a 8-bit value stored in the given address/offset.
|
|
46
|
+
def read_byte(offset)
|
|
47
|
+
@data[offset]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def header
|
|
51
|
+
@data[0x0100..0x014F]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns a string containing the ROM title.
|
|
55
|
+
def title
|
|
56
|
+
@data[0x0134..0x0143].pack('C*').strip
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def manufacturer_code
|
|
60
|
+
@data[0x013F..0x0142]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cgb_flag
|
|
64
|
+
@data[0x0143]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def new_licensee_code
|
|
68
|
+
@data[0x0144..0x0145]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def sgb_flag
|
|
72
|
+
@data[0x0146]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns a symbol for the cartridge type (:rom_only, :mbc1, ...).
|
|
76
|
+
def cartridge_type
|
|
77
|
+
CARTRIDGE_TYPES[@data[0x0147]]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns an Integer indicating the ROM size.
|
|
81
|
+
def rom_size
|
|
82
|
+
ROM_SIZES[@data[0x0148]]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns an Integer indicating the RAM size.
|
|
86
|
+
def ram_size
|
|
87
|
+
RAM_SIZES[@data[0x0149]]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def destination_code
|
|
91
|
+
@data[0x014A]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def old_licensee_code
|
|
95
|
+
@data[0x014B]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def mask_rom_version
|
|
99
|
+
@data[0x014C]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns a 8-bit value for the header checksum.
|
|
103
|
+
def header_checksum
|
|
104
|
+
@data[0x014D]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Calculates the checksum and returns true if it matches.
|
|
108
|
+
def valid_checksum?
|
|
109
|
+
checksum = 0
|
|
110
|
+
(0x0134..0x014C).each do |address|
|
|
111
|
+
checksum = checksum - @data[address] - 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
(checksum & 0xFF) == header_checksum
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amaterasu
|
|
4
|
+
# Models a Game Boy cartridge.
|
|
5
|
+
class Cartridge
|
|
6
|
+
ROM_BANK_SIZE = 16 * 1024
|
|
7
|
+
RAM_BANK_SIZE = 8 * 1024
|
|
8
|
+
|
|
9
|
+
# Loads a Rom from a file path.
|
|
10
|
+
def self.load_rom(file_path, trace_rom:)
|
|
11
|
+
rom = Rom.from_file(file_path)
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, 'Invalid ROM' unless rom.valid_checksum?
|
|
14
|
+
|
|
15
|
+
if trace_rom
|
|
16
|
+
puts rom.title
|
|
17
|
+
puts rom.cartridge_type
|
|
18
|
+
puts rom.rom_size
|
|
19
|
+
puts rom.ram_size
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
new(rom: rom)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Creates a cartridge object based on the cartridge type.
|
|
26
|
+
def initialize(rom:, mbc: nil, ram: nil)
|
|
27
|
+
@rom = rom
|
|
28
|
+
@ram = ram
|
|
29
|
+
@mbc = mbc
|
|
30
|
+
|
|
31
|
+
add_ram if rom.cartridge_type.include?('RAM')
|
|
32
|
+
add_mbc(Mbc1.new(@rom, @ram)) if rom.cartridge_type.include?('MBC1')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_mbc(mbc)
|
|
36
|
+
@mbc = mbc
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_ram
|
|
40
|
+
@ram = GameBoy::Ram.new(size: @rom.ram_size, offset: 0xA000)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Delegates the read byte to either the ROM or the MBC (Not implemented yet).
|
|
44
|
+
def read_rom(address)
|
|
45
|
+
@mbc.nil? ? @rom.read_byte(address) : @mbc.read_byte(address)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Delegates the read byte to the MBC (Not implemented yet).
|
|
49
|
+
def write_rom(address, value)
|
|
50
|
+
return unless @mbc
|
|
51
|
+
|
|
52
|
+
@mbc.write_byte(address, value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_ram(address)
|
|
56
|
+
return 0xFF if @ram.nil?
|
|
57
|
+
|
|
58
|
+
@mbc.nil? ? @ram.read_byte(address) : @mbc.read_byte(address)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Delegates the write byte to the MBC (Not implemented yet).
|
|
62
|
+
def write_ram(address, value)
|
|
63
|
+
return unless @mbc
|
|
64
|
+
|
|
65
|
+
@mbc.write_byte(address, value)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Amaterasu
|
|
6
|
+
# Handles arguments parsing when launching the emulator.
|
|
7
|
+
class CLI
|
|
8
|
+
def self.parse(arguments)
|
|
9
|
+
# @type var options: emulator_options
|
|
10
|
+
options = {
|
|
11
|
+
audio: nil,
|
|
12
|
+
cycles: nil,
|
|
13
|
+
profiling: nil,
|
|
14
|
+
steps: nil,
|
|
15
|
+
trace: nil,
|
|
16
|
+
video: nil,
|
|
17
|
+
rom_path: nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
opt_parser = OptionParser.new do |parser|
|
|
21
|
+
parser.banner = 'Usage: amaterasu [options] ROM_PATH'
|
|
22
|
+
|
|
23
|
+
parser.on('-a', '--audio=AUDIO', 'Define the audio backend') do |audio|
|
|
24
|
+
options[:audio] = audio
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
parser.on('-c', '--cycles=n', Integer, 'Amount of dots to tick') do |n|
|
|
28
|
+
options[:cycles] = n
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
parser.on(
|
|
32
|
+
'-p',
|
|
33
|
+
'--profiling=MODE',
|
|
34
|
+
%w[cpu object wall],
|
|
35
|
+
'Enable Stackprof profiling (cpu, object, wall)'
|
|
36
|
+
) do |mode|
|
|
37
|
+
options[:profiling] = mode.to_sym
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
parser.on('-s', '--steps=n', Integer, 'Amount of CPU steps to run') do |n|
|
|
41
|
+
options[:steps] = n
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
parser.on('-t', '--trace=COMPONENT', 'Enable logging for specific component') do |component|
|
|
45
|
+
options[:trace] = component
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
parser.on('-v', '--video=VIDEO', 'Define the video backend') do |video|
|
|
49
|
+
options[:video] = video
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opt_parser.parse!(arguments)
|
|
54
|
+
options[:rom_path] = arguments.first
|
|
55
|
+
raise ArgumentError, 'ROM_PATH must be provided' unless options[:rom_path]
|
|
56
|
+
|
|
57
|
+
options
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amaterasu
|
|
4
|
+
# Handles the core emulation loop.
|
|
5
|
+
class Emulator
|
|
6
|
+
CYCLES_PER_FRAME = 17_556
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
audio:,
|
|
10
|
+
cycles:,
|
|
11
|
+
profiling:,
|
|
12
|
+
steps:,
|
|
13
|
+
trace:,
|
|
14
|
+
video:,
|
|
15
|
+
rom_path:
|
|
16
|
+
)
|
|
17
|
+
@audio = audio
|
|
18
|
+
@video = video
|
|
19
|
+
@stop_cycles = cycles
|
|
20
|
+
@stop_steps = steps
|
|
21
|
+
@profiling_mode = profiling
|
|
22
|
+
@trace = trace
|
|
23
|
+
@rom_path = rom_path
|
|
24
|
+
|
|
25
|
+
@cycles = 0
|
|
26
|
+
@steps = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def start
|
|
30
|
+
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
|
+
|
|
32
|
+
load_cartridge
|
|
33
|
+
load_memory
|
|
34
|
+
load_components
|
|
35
|
+
|
|
36
|
+
Kernel.loop do
|
|
37
|
+
stop if @stop_steps && @steps == @stop_steps
|
|
38
|
+
|
|
39
|
+
@cpu.step
|
|
40
|
+
@steps += 1
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# This method is called every time the CPU spends
|
|
45
|
+
# exactly 1 M-cycle to advance other components.
|
|
46
|
+
def advance_cycle
|
|
47
|
+
stop if @stop_cycles && @cycles == @stop_cycles
|
|
48
|
+
|
|
49
|
+
@timer.tick
|
|
50
|
+
@apu.tick
|
|
51
|
+
@dma.tick
|
|
52
|
+
|
|
53
|
+
i = 0
|
|
54
|
+
|
|
55
|
+
while i < 4
|
|
56
|
+
@ppu.tick
|
|
57
|
+
i += 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@cycles += 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def load_cartridge
|
|
66
|
+
@cartridge = Cartridge.load_rom(@rom_path, trace_rom: @trace == 'rom')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_memory
|
|
70
|
+
@wram = GameBoy::Ram.new(size: 8192, offset: 0xC000)
|
|
71
|
+
@hram = GameBoy::Ram.new(size: 127, offset: 0xFF80)
|
|
72
|
+
@vram = GameBoy::Vram.new
|
|
73
|
+
@oam = GameBoy::Oam.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def load_components
|
|
77
|
+
@bus = GameBoy::Bus.new
|
|
78
|
+
@sdl2 = HAL::SDL2.new unless @video == 'null'
|
|
79
|
+
@apu = GameBoy::Apu.new
|
|
80
|
+
@dma = GameBoy::Dma.new(@bus, trace_dma: @trace == 'dma')
|
|
81
|
+
@interrupts = GameBoy::Interrupts.new
|
|
82
|
+
@ppu = GameBoy::Ppu.new(@vram, @oam, @sdl2, @interrupts, trace_ppu: @trace == 'ppu')
|
|
83
|
+
@timer = GameBoy::Timer.new(@interrupts, trace_timer: @trace == 'timer')
|
|
84
|
+
@serial = GameBoy::Serial.new(@interrupts, trace_serial: @trace == 'serial')
|
|
85
|
+
@joypad = GameBoy::Joypad.new(@interrupts)
|
|
86
|
+
@sdl2.joypad = @joypad
|
|
87
|
+
@bus.wire_components(
|
|
88
|
+
cartridge: @cartridge,
|
|
89
|
+
ppu: @ppu,
|
|
90
|
+
wram: @wram,
|
|
91
|
+
hram: @hram,
|
|
92
|
+
interrupts: @interrupts,
|
|
93
|
+
apu: @apu,
|
|
94
|
+
timer: @timer,
|
|
95
|
+
serial: @serial,
|
|
96
|
+
joypad: @joypad,
|
|
97
|
+
dma: @dma
|
|
98
|
+
)
|
|
99
|
+
@cpu = GameBoy::Cpu.new(
|
|
100
|
+
@bus,
|
|
101
|
+
@hram,
|
|
102
|
+
@interrupts,
|
|
103
|
+
-> { advance_cycle },
|
|
104
|
+
trace_cpu: @trace == 'cpu'
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stop
|
|
109
|
+
@elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
|
110
|
+
|
|
111
|
+
frames = @cycles.to_f / 17_556
|
|
112
|
+
fps = frames / @elapsed_time
|
|
113
|
+
|
|
114
|
+
puts "#{@steps} steps / #{@cycles} cycles in #{@elapsed_time.round(2)}s"
|
|
115
|
+
puts "#{fps.round(2)} FPS (Target: 59.73)"
|
|
116
|
+
puts "#{(fps / 59.73).round(2)}x real-time Game Boy"
|
|
117
|
+
|
|
118
|
+
exit
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amaterasu
|
|
4
|
+
module GameBoy
|
|
5
|
+
# | 0x0000-0x7FFF | 32 KiB | Cartridge ROM | Bank 0 fixed, Bank N switchable |
|
|
6
|
+
# | 0x8000-0x9FFF | 8 KiB | Video RAM (VRAM) | Tile data + tile maps |
|
|
7
|
+
# | 0xA000-0xBFFF | 8 KiB | External RAM | Cartridge RAM, battery-backed |
|
|
8
|
+
# | 0xC000-0xDFFF | 8 KiB | Work RAM (WRAM) | General purpose |
|
|
9
|
+
# | 0xE000-0xFDFF | ~8 KiB | Echo RAM | Mirror of WRAM (0xC000-0xDDFF) |
|
|
10
|
+
# | 0xFE00-0xFE9F | 160 B | OAM | Sprite attribute table |
|
|
11
|
+
# | 0xFEA0-0xFEFF | 96 B | Unusable | Prohibited area |
|
|
12
|
+
# | 0xFF00-0xFF7F | 128 B | I/O Registers | Hardware control |
|
|
13
|
+
# | 0xFF80-0xFFFE | 127 B | High RAM (HRAM) | Fast RAM, accessible during DMA |
|
|
14
|
+
# | 0xFFFF | 1 B | IE Register | Interrupt Enable |
|
|
15
|
+
class Bus
|
|
16
|
+
def wire_components(
|
|
17
|
+
cartridge:,
|
|
18
|
+
ppu:,
|
|
19
|
+
wram:,
|
|
20
|
+
hram:,
|
|
21
|
+
interrupts:,
|
|
22
|
+
apu:,
|
|
23
|
+
timer:,
|
|
24
|
+
serial:,
|
|
25
|
+
joypad:,
|
|
26
|
+
dma:
|
|
27
|
+
)
|
|
28
|
+
@cartridge = cartridge
|
|
29
|
+
@ppu = ppu
|
|
30
|
+
@wram = wram
|
|
31
|
+
@hram = hram
|
|
32
|
+
@interrupts = interrupts
|
|
33
|
+
@apu = apu
|
|
34
|
+
@timer = timer
|
|
35
|
+
@serial = serial
|
|
36
|
+
@joypad = joypad
|
|
37
|
+
@dma = dma
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Delegates the read to the proper component based on the address
|
|
41
|
+
# and returns the 8-bit value that was stored there.
|
|
42
|
+
def read_byte(address:, caller: nil)
|
|
43
|
+
return 0xFF if @dma.active?
|
|
44
|
+
&& caller.is_a?(Cpu)
|
|
45
|
+
&& address < 0xFF00
|
|
46
|
+
|
|
47
|
+
if address <= 0x7FFF
|
|
48
|
+
@cartridge.read_rom(address)
|
|
49
|
+
elsif address <= 0x9FFF
|
|
50
|
+
@ppu.read_vram(address:)
|
|
51
|
+
elsif address <= 0xBFFF
|
|
52
|
+
@cartridge.read_ram(address)
|
|
53
|
+
elsif address <= 0xDFFF
|
|
54
|
+
@wram.read_byte(address:)
|
|
55
|
+
elsif address <= 0xFDFF
|
|
56
|
+
@wram.read_byte(address: address - 0x2000)
|
|
57
|
+
elsif address <= 0xFE9F
|
|
58
|
+
@ppu.read_oam(address:)
|
|
59
|
+
elsif address <= 0xFEFF
|
|
60
|
+
0xFF
|
|
61
|
+
elsif address <= 0xFF7F
|
|
62
|
+
read_io(address)
|
|
63
|
+
elsif address <= 0xFFFE
|
|
64
|
+
@hram.read_byte(address:)
|
|
65
|
+
elsif address == 0xFFFF
|
|
66
|
+
@interrupts.ie_register
|
|
67
|
+
else
|
|
68
|
+
raise "Not implemented bus read at $#{address.to_s(16)}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Delegates the write to the proper component based on the address
|
|
73
|
+
# and stores a 8-bit value at that location.
|
|
74
|
+
def write_byte(address:, value:, caller: nil)
|
|
75
|
+
return if @dma.active?
|
|
76
|
+
&& caller.is_a?(Cpu)
|
|
77
|
+
&& address < 0xFF00
|
|
78
|
+
|
|
79
|
+
if address <= 0x7FFF
|
|
80
|
+
@cartridge.write_rom(address, value)
|
|
81
|
+
elsif address <= 0x9FFF
|
|
82
|
+
@ppu.write_vram(address:, value:)
|
|
83
|
+
elsif address <= 0xBFFF
|
|
84
|
+
@cartridge.write_ram(address - 0xA000, value)
|
|
85
|
+
elsif address <= 0xDFFF
|
|
86
|
+
@wram.write_byte(address:, value:)
|
|
87
|
+
elsif address <= 0xFDFF
|
|
88
|
+
@wram.write_byte(address: address - 0x2000, value:)
|
|
89
|
+
elsif address <= 0xFE9F
|
|
90
|
+
@ppu.write_oam(address:, value:)
|
|
91
|
+
elsif address <= 0xFEFF
|
|
92
|
+
nil
|
|
93
|
+
elsif address <= 0xFF7F
|
|
94
|
+
write_io(address, value)
|
|
95
|
+
elsif address <= 0xFFFE
|
|
96
|
+
@hram.write_byte(address:, value:)
|
|
97
|
+
elsif address == 0xFFFF
|
|
98
|
+
@interrupts.ie_register = value
|
|
99
|
+
else
|
|
100
|
+
raise "Not implemented bus write at $#{address.to_s(16)}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Extracts the IO registers read logic for organization.
|
|
107
|
+
def read_io(address)
|
|
108
|
+
case address
|
|
109
|
+
when 0xFF00 then @joypad.p1
|
|
110
|
+
when 0xFF01 then @serial.sb
|
|
111
|
+
when 0xFF02 then @serial.sc
|
|
112
|
+
when 0xFF04 then @timer.div
|
|
113
|
+
when 0xFF05 then @timer.tima
|
|
114
|
+
when 0xFF06 then @timer.tma
|
|
115
|
+
when 0xFF07 then @timer.tac
|
|
116
|
+
when 0xFF0F then @interrupts.if_register
|
|
117
|
+
when 0xFF40 then @ppu.registers.lcdc.value
|
|
118
|
+
when 0xFF41 then @ppu.registers.stat.value
|
|
119
|
+
when 0xFF42 then @ppu.registers.scy
|
|
120
|
+
when 0xFF43 then @ppu.registers.scx
|
|
121
|
+
when 0xFF44 then @ppu.registers.ly
|
|
122
|
+
when 0xFF45 then @ppu.registers.lyc
|
|
123
|
+
when 0xFF46 then @dma.internal_latch
|
|
124
|
+
when 0xFF47 then @ppu.registers.bgp
|
|
125
|
+
when 0xFF48 then @ppu.registers.obp0
|
|
126
|
+
when 0xFF49 then @ppu.registers.obp1
|
|
127
|
+
when 0xFF4A then @ppu.registers.wy
|
|
128
|
+
when 0xFF4B then @ppu.registers.wx
|
|
129
|
+
else
|
|
130
|
+
0xFF
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Extracts the IO registers write logic for organization.
|
|
135
|
+
def write_io(address, value)
|
|
136
|
+
case address
|
|
137
|
+
when 0xFF00 then @joypad.p1 = value
|
|
138
|
+
when 0xFF01 then @serial.sb = value
|
|
139
|
+
when 0xFF02 then @serial.sc = value
|
|
140
|
+
when 0xFF04 then @timer.div = value
|
|
141
|
+
when 0xFF05 then @timer.tima = value
|
|
142
|
+
when 0xFF06 then @timer.tma = value
|
|
143
|
+
when 0xFF07 then @timer.tac = value
|
|
144
|
+
when 0xFF0F then @interrupts.if_register = value
|
|
145
|
+
when 0xFF40 then @ppu.registers.lcdc = value
|
|
146
|
+
when 0xFF41 then @ppu.registers.stat = value
|
|
147
|
+
when 0xFF42 then @ppu.registers.scy = value
|
|
148
|
+
when 0xFF43 then @ppu.registers.scx = value
|
|
149
|
+
when 0xFF44 then nil # @ppu.registers.ly = value -> read-only
|
|
150
|
+
when 0xFF45 then @ppu.registers.lyc = value
|
|
151
|
+
when 0xFF46 then @dma.request_transfer(source_value: value)
|
|
152
|
+
when 0xFF47 then @ppu.registers.bgp = value
|
|
153
|
+
when 0xFF48 then @ppu.registers.obp0 = value
|
|
154
|
+
when 0xFF49 then @ppu.registers.obp1 = value
|
|
155
|
+
when 0xFF4A then @ppu.registers.wy = value
|
|
156
|
+
when 0xFF4B then @ppu.registers.wx = value
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amaterasu
|
|
4
|
+
module GameBoy
|
|
5
|
+
class Cpu
|
|
6
|
+
module Instructions
|
|
7
|
+
# Handles the logic related to all possible ADC instructions
|
|
8
|
+
#
|
|
9
|
+
# - ADC A, r8
|
|
10
|
+
# - ADC A, [HL]
|
|
11
|
+
# - ADC A, n8
|
|
12
|
+
class Adc < Base
|
|
13
|
+
# @param cpu [Cpu] Holds a direct reference to the main Cpu object.
|
|
14
|
+
# @param source [Symbol] Operator, can be a register, :mem_hl, :imm8.
|
|
15
|
+
def initialize(cpu:, source:)
|
|
16
|
+
super(cpu:)
|
|
17
|
+
|
|
18
|
+
@mnemonic = "ADC A, #{source}"
|
|
19
|
+
@logic = build_logic(source)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Builds the logic for all ADC instructions.
|
|
25
|
+
# Returns a lambda object to be called by the CPU.
|
|
26
|
+
def build_logic(source)
|
|
27
|
+
case source
|
|
28
|
+
when :a then -> { adc_a(@registers.a) }
|
|
29
|
+
when :b then -> { adc_a(@registers.b) }
|
|
30
|
+
when :c then -> { adc_a(@registers.c) }
|
|
31
|
+
when :d then -> { adc_a(@registers.d) }
|
|
32
|
+
when :e then -> { adc_a(@registers.e) }
|
|
33
|
+
when :h then -> { adc_a(@registers.h) }
|
|
34
|
+
when :l then -> { adc_a(@registers.l) }
|
|
35
|
+
when :mem_hl then -> { adc_a(@cpu.bus_read(address: @registers.hl)) }
|
|
36
|
+
when :imm8 then -> { adc_a(@cpu.fetch_next_byte) }
|
|
37
|
+
else
|
|
38
|
+
raise ArgumentError, 'Unknown Adc source'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# ADCs a given value plus the Carry flag into register A.
|
|
43
|
+
#
|
|
44
|
+
# - Sets the Zero flag if the result is zero, otherwise clears it.
|
|
45
|
+
# - Sets the Subtraction flag to zero.
|
|
46
|
+
# - Sets the Half Carry flag if there was overflow from Bit 3, otherwise clears it.
|
|
47
|
+
# - Sets the Carry flag if there was overflow from Bit 7, otherwise clears it.
|
|
48
|
+
def adc_a(value)
|
|
49
|
+
acc = @registers.a
|
|
50
|
+
carry_in = @registers.c_flag
|
|
51
|
+
result = @registers.a + value + carry_in
|
|
52
|
+
|
|
53
|
+
@registers.z_flag = result.nobits?(0xFF)
|
|
54
|
+
@registers.n_flag = false
|
|
55
|
+
@registers.h_flag = (acc & 0x0F) + (value & 0x0F) + carry_in > 0x0F
|
|
56
|
+
@registers.c_flag = result > 0xFF
|
|
57
|
+
|
|
58
|
+
@registers.a = result
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amaterasu
|
|
4
|
+
module GameBoy
|
|
5
|
+
class Cpu
|
|
6
|
+
module Instructions
|
|
7
|
+
# Handles the logic related to all possible ADD 16-bit instructions.
|
|
8
|
+
#
|
|
9
|
+
# - ADD HL, BC
|
|
10
|
+
# - ADD HL, DE
|
|
11
|
+
# - ADD HL, HL
|
|
12
|
+
# - ADD HL, SP
|
|
13
|
+
# - ADD SP, sig8
|
|
14
|
+
class Add16 < Base
|
|
15
|
+
def initialize(cpu:, source:, target:)
|
|
16
|
+
super(cpu:)
|
|
17
|
+
|
|
18
|
+
@mnemonic = "ADD #{format_operand(source)}, #{format_operand(target)}"
|
|
19
|
+
@logic = build_logic(source, target)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Builds the logic for each ADD 16-bit instruction.
|
|
25
|
+
# @return [Proc] A lambda object to be called by the CPU.
|
|
26
|
+
def build_logic(source, target)
|
|
27
|
+
return -> { add16_sig8 } if target == :sp
|
|
28
|
+
|
|
29
|
+
case source
|
|
30
|
+
when :bc then -> { add16(@registers.bc) }
|
|
31
|
+
when :de then -> { add16(@registers.de) }
|
|
32
|
+
when :hl then -> { add16(@registers.hl) }
|
|
33
|
+
when :sp then -> { add16(@registers.sp) }
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, 'Unknown Add16 source'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# M-cycle 1: Fetches the instruction opcode.
|
|
40
|
+
# M-cycle 2: 16-bit add operation + flag logic + set result.
|
|
41
|
+
def add16(reg16_value)
|
|
42
|
+
hl_value = @registers.hl
|
|
43
|
+
result = @cpu.add16(@registers.hl, reg16_value)
|
|
44
|
+
|
|
45
|
+
@registers.n_flag = false
|
|
46
|
+
@registers.h_flag = (hl_value & 0x0FFF) + (reg16_value & 0x0FFF) > 0x0FFF
|
|
47
|
+
@registers.c_flag = result > 0xFFFF
|
|
48
|
+
|
|
49
|
+
@registers.hl = result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# M-cycle 1: Fetches the instruction opcode.
|
|
53
|
+
# M-cycle 2: Fetches the next byte in the PC.
|
|
54
|
+
# M-cycle 3: Signs the value + internal processing.
|
|
55
|
+
# M-cycle 4: 16-bit Add operation + flag logic + set result.
|
|
56
|
+
def add16_sig8
|
|
57
|
+
sp = @registers.sp
|
|
58
|
+
unsigned_byte = @cpu.fetch_next_byte
|
|
59
|
+
offset = @cpu.sign_value(unsigned_byte)
|
|
60
|
+
@cpu.internal_processing
|
|
61
|
+
result = @cpu.add16(@registers.sp, offset)
|
|
62
|
+
|
|
63
|
+
@registers.clear_flags
|
|
64
|
+
@registers.h_flag = (sp & 0x0F) + (unsigned_byte & 0x0F) > 0x0F
|
|
65
|
+
@registers.c_flag = (sp & 0xFF) + (unsigned_byte & 0xFF) > 0xFF
|
|
66
|
+
|
|
67
|
+
@registers.sp = result
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|