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.
Files changed (158) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +54 -0
  5. data/Gemfile +32 -0
  6. data/Gemfile.lock +267 -0
  7. data/LICENSE +21 -0
  8. data/README.md +115 -0
  9. data/Steepfile +7 -0
  10. data/exe/amaterasu +23 -0
  11. data/lib/amaterasu/cartridge/mbc1.rb +56 -0
  12. data/lib/amaterasu/cartridge/rom.rb +118 -0
  13. data/lib/amaterasu/cartridge.rb +68 -0
  14. data/lib/amaterasu/cli.rb +60 -0
  15. data/lib/amaterasu/emulator.rb +121 -0
  16. data/lib/amaterasu/game_boy/apu.rb +12 -0
  17. data/lib/amaterasu/game_boy/bus.rb +161 -0
  18. data/lib/amaterasu/game_boy/cpu/instructions/adc.rb +64 -0
  19. data/lib/amaterasu/game_boy/cpu/instructions/add16.rb +73 -0
  20. data/lib/amaterasu/game_boy/cpu/instructions/add8.rb +63 -0
  21. data/lib/amaterasu/game_boy/cpu/instructions/and.rb +62 -0
  22. data/lib/amaterasu/game_boy/cpu/instructions/base.rb +38 -0
  23. data/lib/amaterasu/game_boy/cpu/instructions/call.rb +48 -0
  24. data/lib/amaterasu/game_boy/cpu/instructions/cb_bit.rb +52 -0
  25. data/lib/amaterasu/game_boy/cpu/instructions/cb_res.rb +49 -0
  26. data/lib/amaterasu/game_boy/cpu/instructions/cb_rl.rb +70 -0
  27. data/lib/amaterasu/game_boy/cpu/instructions/cb_rlc.rb +68 -0
  28. data/lib/amaterasu/game_boy/cpu/instructions/cb_rr.rb +70 -0
  29. data/lib/amaterasu/game_boy/cpu/instructions/cb_rrc.rb +68 -0
  30. data/lib/amaterasu/game_boy/cpu/instructions/cb_set.rb +51 -0
  31. data/lib/amaterasu/game_boy/cpu/instructions/cb_sla.rb +69 -0
  32. data/lib/amaterasu/game_boy/cpu/instructions/cb_sra.rb +71 -0
  33. data/lib/amaterasu/game_boy/cpu/instructions/cb_srl.rb +69 -0
  34. data/lib/amaterasu/game_boy/cpu/instructions/cb_swap.rb +67 -0
  35. data/lib/amaterasu/game_boy/cpu/instructions/cp.rb +61 -0
  36. data/lib/amaterasu/game_boy/cpu/instructions/daa.rb +59 -0
  37. data/lib/amaterasu/game_boy/cpu/instructions/dec.rb +64 -0
  38. data/lib/amaterasu/game_boy/cpu/instructions/di.rb +21 -0
  39. data/lib/amaterasu/game_boy/cpu/instructions/ei.rb +19 -0
  40. data/lib/amaterasu/game_boy/cpu/instructions/halt.rb +19 -0
  41. data/lib/amaterasu/game_boy/cpu/instructions/inc.rb +64 -0
  42. data/lib/amaterasu/game_boy/cpu/instructions/jp.rb +54 -0
  43. data/lib/amaterasu/game_boy/cpu/instructions/jr.rb +45 -0
  44. data/lib/amaterasu/game_boy/cpu/instructions/ld16.rb +79 -0
  45. data/lib/amaterasu/game_boy/cpu/instructions/ld8.rb +210 -0
  46. data/lib/amaterasu/game_boy/cpu/instructions/ldh.rb +61 -0
  47. data/lib/amaterasu/game_boy/cpu/instructions/misc.rb +53 -0
  48. data/lib/amaterasu/game_boy/cpu/instructions/nop.rb +19 -0
  49. data/lib/amaterasu/game_boy/cpu/instructions/or.rb +56 -0
  50. data/lib/amaterasu/game_boy/cpu/instructions/pop.rb +39 -0
  51. data/lib/amaterasu/game_boy/cpu/instructions/push.rb +43 -0
  52. data/lib/amaterasu/game_boy/cpu/instructions/ret.rb +70 -0
  53. data/lib/amaterasu/game_boy/cpu/instructions/rotate.rb +120 -0
  54. data/lib/amaterasu/game_boy/cpu/instructions/rst.rb +33 -0
  55. data/lib/amaterasu/game_boy/cpu/instructions/sbc.rb +64 -0
  56. data/lib/amaterasu/game_boy/cpu/instructions/stop.rb +19 -0
  57. data/lib/amaterasu/game_boy/cpu/instructions/sub.rb +63 -0
  58. data/lib/amaterasu/game_boy/cpu/instructions/xor.rb +60 -0
  59. data/lib/amaterasu/game_boy/cpu/instructions.rb +600 -0
  60. data/lib/amaterasu/game_boy/cpu/registers.rb +264 -0
  61. data/lib/amaterasu/game_boy/cpu.rb +232 -0
  62. data/lib/amaterasu/game_boy/dma.rb +114 -0
  63. data/lib/amaterasu/game_boy/interrupts.rb +108 -0
  64. data/lib/amaterasu/game_boy/joypad.rb +127 -0
  65. data/lib/amaterasu/game_boy/oam/sprite.rb +106 -0
  66. data/lib/amaterasu/game_boy/oam.rb +29 -0
  67. data/lib/amaterasu/game_boy/ppu/modes/disabled.rb +29 -0
  68. data/lib/amaterasu/game_boy/ppu/modes/h_blank.rb +45 -0
  69. data/lib/amaterasu/game_boy/ppu/modes/oam_scan.rb +93 -0
  70. data/lib/amaterasu/game_boy/ppu/modes/rendering/bg_win_fetcher.rb +204 -0
  71. data/lib/amaterasu/game_boy/ppu/modes/rendering/pixel_emitter.rb +83 -0
  72. data/lib/amaterasu/game_boy/ppu/modes/rendering/pixel_fifo.rb +70 -0
  73. data/lib/amaterasu/game_boy/ppu/modes/rendering/sprite_fetcher.rb +140 -0
  74. data/lib/amaterasu/game_boy/ppu/modes/rendering.rb +108 -0
  75. data/lib/amaterasu/game_boy/ppu/modes/v_blank.rb +43 -0
  76. data/lib/amaterasu/game_boy/ppu/modes.rb +22 -0
  77. data/lib/amaterasu/game_boy/ppu/registers/lcd_control.rb +57 -0
  78. data/lib/amaterasu/game_boy/ppu/registers/lcd_status.rb +88 -0
  79. data/lib/amaterasu/game_boy/ppu/registers.rb +131 -0
  80. data/lib/amaterasu/game_boy/ppu.rb +207 -0
  81. data/lib/amaterasu/game_boy/ram.rb +70 -0
  82. data/lib/amaterasu/game_boy/serial.rb +91 -0
  83. data/lib/amaterasu/game_boy/timer.rb +230 -0
  84. data/lib/amaterasu/game_boy/vram/tile.rb +68 -0
  85. data/lib/amaterasu/game_boy/vram/tile_data.rb +52 -0
  86. data/lib/amaterasu/game_boy/vram/tile_map.rb +71 -0
  87. data/lib/amaterasu/game_boy/vram.rb +51 -0
  88. data/lib/amaterasu/hal/console.rb +23 -0
  89. data/lib/amaterasu/hal/sdl2/bindings.rb +59 -0
  90. data/lib/amaterasu/hal/sdl2.rb +127 -0
  91. data/lib/amaterasu/utils/bit_ops.rb +22 -0
  92. data/lib/amaterasu.rb +13 -0
  93. data/sig/akane/cartridge/rom.rbs +29 -0
  94. data/sig/akane/cartridge.rbs +12 -0
  95. data/sig/akane/cli.rbs +16 -0
  96. data/sig/akane/emulator.rbs +19 -0
  97. data/sig/akane/game_boy/apu.rbs +7 -0
  98. data/sig/akane/game_boy/bus.rbs +25 -0
  99. data/sig/akane/game_boy/cpu/instructions/adc.rbs +18 -0
  100. data/sig/akane/game_boy/cpu/instructions/add16.rbs +19 -0
  101. data/sig/akane/game_boy/cpu/instructions/add8.rbs +18 -0
  102. data/sig/akane/game_boy/cpu/instructions/and.rbs +18 -0
  103. data/sig/akane/game_boy/cpu/instructions/base.rbs +20 -0
  104. data/sig/akane/game_boy/cpu/instructions/call.rbs +18 -0
  105. data/sig/akane/game_boy/cpu/instructions/cb_bit.rbs +20 -0
  106. data/sig/akane/game_boy/cpu/instructions/cb_res.rbs +19 -0
  107. data/sig/akane/game_boy/cpu/instructions/cb_rl.rbs +21 -0
  108. data/sig/akane/game_boy/cpu/instructions/cb_rlc.rbs +21 -0
  109. data/sig/akane/game_boy/cpu/instructions/cb_rr.rbs +21 -0
  110. data/sig/akane/game_boy/cpu/instructions/cb_rrc.rbs +21 -0
  111. data/sig/akane/game_boy/cpu/instructions/cb_set.rbs +20 -0
  112. data/sig/akane/game_boy/cpu/instructions/cb_sla.rbs +21 -0
  113. data/sig/akane/game_boy/cpu/instructions/cb_sra.rbs +21 -0
  114. data/sig/akane/game_boy/cpu/instructions/cb_srl.rbs +21 -0
  115. data/sig/akane/game_boy/cpu/instructions/cb_swap.rbs +21 -0
  116. data/sig/akane/game_boy/cpu/instructions/cp.rbs +18 -0
  117. data/sig/akane/game_boy/cpu/instructions/daa.rbs +17 -0
  118. data/sig/akane/game_boy/cpu/instructions/dec.rbs +19 -0
  119. data/sig/akane/game_boy/cpu/instructions/di.rbs +13 -0
  120. data/sig/akane/game_boy/cpu/instructions/ei.rbs +13 -0
  121. data/sig/akane/game_boy/cpu/instructions/halt.rbs +13 -0
  122. data/sig/akane/game_boy/cpu/instructions/inc.rbs +19 -0
  123. data/sig/akane/game_boy/cpu/instructions/jp.rbs +18 -0
  124. data/sig/akane/game_boy/cpu/instructions/jr.rbs +18 -0
  125. data/sig/akane/game_boy/cpu/instructions/ld16.rbs +18 -0
  126. data/sig/akane/game_boy/cpu/instructions/ld8.rbs +31 -0
  127. data/sig/akane/game_boy/cpu/instructions/ldh.rbs +23 -0
  128. data/sig/akane/game_boy/cpu/instructions/misc.rbs +20 -0
  129. data/sig/akane/game_boy/cpu/instructions/nop.rbs +13 -0
  130. data/sig/akane/game_boy/cpu/instructions/or.rbs +18 -0
  131. data/sig/akane/game_boy/cpu/instructions/pop.rbs +17 -0
  132. data/sig/akane/game_boy/cpu/instructions/push.rbs +18 -0
  133. data/sig/akane/game_boy/cpu/instructions/ret.rbs +20 -0
  134. data/sig/akane/game_boy/cpu/instructions/rotate.rbs +23 -0
  135. data/sig/akane/game_boy/cpu/instructions/rst.rbs +17 -0
  136. data/sig/akane/game_boy/cpu/instructions/sbc.rbs +19 -0
  137. data/sig/akane/game_boy/cpu/instructions/stop.rbs +13 -0
  138. data/sig/akane/game_boy/cpu/instructions/sub.rbs +18 -0
  139. data/sig/akane/game_boy/cpu/instructions/xor.rbs +19 -0
  140. data/sig/akane/game_boy/cpu/instructions.rbs +12 -0
  141. data/sig/akane/game_boy/cpu/registers.rbs +56 -0
  142. data/sig/akane/game_boy/cpu.rbs +39 -0
  143. data/sig/akane/game_boy/interrupts.rbs +28 -0
  144. data/sig/akane/game_boy/joypad.rbs +25 -0
  145. data/sig/akane/game_boy/oam/sprite.rbs +30 -0
  146. data/sig/akane/game_boy/ppu/modes/disabled.rbs +17 -0
  147. data/sig/akane/game_boy/ppu/modes/h_blank.rbs +20 -0
  148. data/sig/akane/game_boy/ppu/modes/oam_scan.rbs +28 -0
  149. data/sig/akane/game_boy/ppu/modes/rendering.rbs +26 -0
  150. data/sig/akane/game_boy/ppu/modes/v_blank.rbs +20 -0
  151. data/sig/akane/game_boy/ppu/modes.rbs +13 -0
  152. data/sig/akane/game_boy/ppu.rbs +59 -0
  153. data/sig/akane/game_boy/ram.rbs +16 -0
  154. data/sig/akane/game_boy/serial.rbs +21 -0
  155. data/sig/akane/game_boy/timer.rbs +30 -0
  156. data/sig/akane/utils/bit_ops.rbs +11 -0
  157. data/sig/akane.rbs +3 -0
  158. 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ # Models the APU behavior from the Game Boy.
6
+ class Apu
7
+ def tick
8
+ # tick logic
9
+ end
10
+ end
11
+ end
12
+ 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