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,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ # Models the joypad inputs and logic.
6
+ class Joypad
7
+ BIT_MASK_UNUSED_BITS = 0b11000000
8
+
9
+ BIT_MASK_BUTTONS_SELECT_BIT = 0b00100000
10
+ BIT_MASK_DPAD_SELECT_BIT = 0b00010000
11
+
12
+ # Maps all face buttons to its relevant bit.
13
+ FACE_BUTTONS = {
14
+ a: 0,
15
+ b: 1,
16
+ select: 2,
17
+ start: 3
18
+ }.freeze
19
+
20
+ # Maps all D-pad buttons to its relevant bit.
21
+ DPAD_BUTTONS = {
22
+ right: 0,
23
+ left: 1,
24
+ up: 2,
25
+ down: 3
26
+ }.freeze
27
+
28
+ # Creates a joypad object.
29
+ #
30
+ # - Holds interrupts instance to request a :joypad interrupt.
31
+ # - @p1 register only holds the relevant selection bits (Bits 5 and 4).
32
+ # - Dpad and button state are tracked as separate nibbles.
33
+ def initialize(interrupts, skip_boot_rom: true)
34
+ @interrupts = interrupts
35
+ @p1 = skip_boot_rom ? 0xCF : 0x00
36
+
37
+ @dpad = 0xF
38
+ @buttons = 0xF
39
+ end
40
+
41
+ # Returns the 8-bit value stored in the P1 register.
42
+ #
43
+ # - Bits 7 and 6 are unused, they always read 1.
44
+ # - The actual @p1 register only holds the 2 selection bits (Bit 5 and 4).
45
+ # - Calculate the buttons state based on the pressed buttons and use it as the lower nibble.
46
+ def p1
47
+ BIT_MASK_UNUSED_BITS | @p1 | buttons_state
48
+ end
49
+
50
+ # Sets a 8-bit value into the P1 register.
51
+ #
52
+ # - The lower nibble is read-only.
53
+ # - Bit 5 is used to select the face buttons.
54
+ # - Bit 4 is used to select the d-pad.
55
+ def p1=(value)
56
+ old_state = buttons_state
57
+ @p1 = value & 0b00110000
58
+ new_state = buttons_state
59
+
60
+ falling_edges = old_state & ~new_state # checks for bits that went 1 -> 0
61
+ request_interrupt if falling_edges.anybits?(0x0F)
62
+ end
63
+
64
+ # @param button [Symbol] Dpad button pressed (:up, :down, :left, :right).
65
+ def press_dpad(button)
66
+ relevant_bit = DPAD_BUTTONS[button]
67
+ return if (@dpad >> relevant_bit).nobits?(1) # already pressed
68
+
69
+ clear_mask = ~(1 << relevant_bit)
70
+ @dpad &= clear_mask
71
+
72
+ request_interrupt if dpad_selected?
73
+ end
74
+
75
+ # @param button [Symbol] Dpad button released (:up, :down, :left, :right).
76
+ def release_dpad(button)
77
+ relevant_bit = DPAD_BUTTONS[button]
78
+ return if (@dpad >> relevant_bit).anybits?(1) # already released
79
+
80
+ set_mask = (1 << relevant_bit)
81
+ @dpad |= set_mask
82
+ end
83
+
84
+ # @param button [Symbol] Face button pressed (:a, :b, :start, :select).
85
+ def press_face(button)
86
+ relevant_bit = FACE_BUTTONS[button]
87
+ return if (@buttons >> relevant_bit).nobits?(1) # already pressed
88
+
89
+ clear_mask = ~(1 << relevant_bit)
90
+ @buttons &= clear_mask
91
+
92
+ request_interrupt if buttons_selected?
93
+ end
94
+
95
+ # @param button [Symbol] Face button released (:a, :b, :start, :select).
96
+ def release_face(button)
97
+ relevant_bit = FACE_BUTTONS[button]
98
+ return if (@buttons >> relevant_bit).anybits?(1) # already released
99
+
100
+ set_mask = (1 << relevant_bit)
101
+ @buttons |= set_mask
102
+ end
103
+
104
+ private
105
+
106
+ def request_interrupt
107
+ @interrupts.request(:joypad)
108
+ end
109
+
110
+ def dpad_selected?
111
+ (@p1 >> 4).nobits?(1)
112
+ end
113
+
114
+ def buttons_selected?
115
+ (@p1 >> 5).nobits?(1)
116
+ end
117
+
118
+ def buttons_state
119
+ return @dpad & @buttons if dpad_selected? && buttons_selected?
120
+ return @dpad if dpad_selected?
121
+ return @buttons if buttons_selected?
122
+
123
+ 0xF
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Oam
6
+ # Models each Sprite that is stored inside the Object Attribute Memory (OAM).
7
+ class Sprite
8
+ SIZE_IN_BYTES = 4
9
+
10
+ BIT_MASK_BG_WIN_PRIORITY_SET = 1 << 7
11
+ BIT_MASK_OBJ_Y_FLIPPED = 1 << 6
12
+ BIT_MASK_OBJ_X_FLIPPED = 1 << 5
13
+ BIT_MASK_USE_OBP1_PALETTE = 1 << 4
14
+
15
+ # @param oam_data [Array] Reference to the original OAM @data.
16
+ # @param index [Integer] Sprite index within OAM (0 - 39).
17
+ def initialize(oam_data:, index:)
18
+ @oam_data = oam_data
19
+ @base_offset = index * SIZE_IN_BYTES
20
+ end
21
+
22
+ # Screen Y position + 16, this means if Y = 0
23
+ # the object top pixel is offscreen at (- 16).
24
+ #
25
+ # @return [Integer] First byte of the Sprite in OAM.
26
+ def y
27
+ @oam_data[@base_offset]
28
+ end
29
+
30
+ # Where the top-most pixel starts at (top row).
31
+ #
32
+ def y_screen_pos
33
+ y - 16
34
+ end
35
+
36
+ # @return [Integer] Byte 1 of the Sprite.
37
+ def x
38
+ @oam_data[@base_offset + 1]
39
+ end
40
+
41
+ def x_screen_pos
42
+ x - 8
43
+ end
44
+
45
+ # @return [Integer] Byte 2 of the Sprite.
46
+ def tile_index(obj_size_8x16, y_flipped, current_obj_y)
47
+ return @oam_data[@base_offset + 2] unless obj_size_8x16
48
+
49
+ if current_obj_y >= 0 && current_obj_y < 8
50
+ y_flipped ? bottom_half : top_half
51
+ else
52
+ y_flipped ? top_half : bottom_half
53
+ end
54
+ end
55
+
56
+ def top_half
57
+ @oam_data[@base_offset + 2] & 0xFE
58
+ end
59
+
60
+ def bottom_half
61
+ @oam_data[@base_offset + 2] | 0x01
62
+ end
63
+
64
+ # @return [Integer] Byte 3 of the Sprite.
65
+ def attributes
66
+ @oam_data[@base_offset + 3]
67
+ end
68
+
69
+ # If this bit is set in the Sprite attributes
70
+ # it means that the Background or Window should
71
+ # be displayed on top of the Sprite *UNLESS* the
72
+ # Background/Window pixel has color_id = 0b00,
73
+ # in which case the Sprite pixel is shown.
74
+ #
75
+ # @return [Boolean]
76
+ def bg_win_priority_set?
77
+ (attributes & BIT_MASK_BG_WIN_PRIORITY_SET) != 0
78
+ end
79
+
80
+ # @return [Boolean] Whether or not the Sprite is flipped horizontally.
81
+ def y_flipped?
82
+ (attributes & BIT_MASK_OBJ_Y_FLIPPED) != 0
83
+ end
84
+
85
+ # @return [Boolean] Whether or not the Sprite is flipped vertically.
86
+ def x_flipped?
87
+ (attributes & BIT_MASK_OBJ_X_FLIPPED) != 0
88
+ end
89
+
90
+ # @return [Boolean] Selects which Color Palette to use for the Sprite pixels.
91
+ def use_obp1_palette?
92
+ (attributes & BIT_MASK_USE_OBP1_PALETTE) != 0
93
+ end
94
+
95
+ # @return [String] Custom inspect for easier debugging.
96
+ def inspect
97
+ '#<Sprite ' \
98
+ "y_pos=$#{format('%02X', y)} " \
99
+ "x_pos=$#{format('%02X', x)} " \
100
+ "tile_index=$#{format('%02X', top_half)} " \
101
+ "attributes=#{format('%08b', attributes)}>"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ # Models the OAM (Object Attribute Memory) from the DMG Game Boy.
6
+ class Oam < Ram
7
+ START_ADDRESS = 0xFE00
8
+ END_ADDRESS = 0xFE9F
9
+ SPRITE_ENTRIES = 40
10
+ SIZE = (END_ADDRESS - START_ADDRESS) + 1 #=> 160 bytes
11
+
12
+ def initialize
13
+ super(size: SIZE, offset: START_ADDRESS)
14
+
15
+ @sprites = Array.new(SPRITE_ENTRIES) do |sprite_index|
16
+ Sprite.new(oam_data: @data, index: sprite_index)
17
+ end
18
+ end
19
+
20
+ def sprite(index)
21
+ @sprites[index]
22
+ end
23
+
24
+ def each_sprite(&)
25
+ @sprites.each(&)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Ppu
6
+ module Modes
7
+ # Defines the behavior of the PPU when LCD Control Bit 7 is 0.
8
+ class Disabled
9
+ attr_reader :name, :number
10
+
11
+ def initialize(ppu)
12
+ @ppu = ppu
13
+ @lcd_control = @ppu.registers.lcdc
14
+
15
+ @name = 'DISABLED'
16
+ @number = 0
17
+ end
18
+
19
+ def tick
20
+ return unless @lcd_control.lcd_enabled?
21
+
22
+ @ppu.reset_states
23
+ @ppu.set_mode(:oam_scan)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Ppu
6
+ module Modes
7
+ # Defines the behavior of the Ppu during HBlank mode.
8
+ class HBlank
9
+ attr_reader :name, :number
10
+
11
+ def initialize(ppu)
12
+ @ppu = ppu
13
+
14
+ @name = 'HBLANK'
15
+ @number = 0
16
+ end
17
+
18
+ def tick
19
+ return unless @ppu.dots == DOTS_PER_SCANLINE
20
+
21
+ @ppu.reset_for_scanline
22
+ @ppu.increment_ly
23
+
24
+ if @ppu.registers.ly < VISIBLE_SCANLINES
25
+ @ppu.set_mode(:oam_scan)
26
+ elsif @ppu.registers.ly == VISIBLE_SCANLINES
27
+ @ppu.set_mode(:v_blank)
28
+ @ppu.request_interrupt(:v_blank)
29
+ end
30
+ end
31
+
32
+ def inspect
33
+ '#<Amaterasu::GameBoy::Ppu::Modes::HBlank ' \
34
+ "@name='#{@name}' " \
35
+ "@number=#{@number} "
36
+ end
37
+
38
+ def to_s
39
+ "#{@name} (##{@number}) | WAITING FOR SCANLINE TO FINISH"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Ppu
6
+ module Modes
7
+ # Defines the behavior of the PPU during OAM Scan mode.
8
+ class OamScan
9
+ # Each Sprite takes 2 dots to scan.
10
+ SCAN_DURATION_IN_DOTS = 80
11
+
12
+ attr_reader :name, :number
13
+
14
+ # @param ppu [Amaterasu::GameBoy::Ppu] Reference to the PPU instance.
15
+ def initialize(ppu)
16
+ @ppu = ppu
17
+
18
+ @name = 'OAM SCAN'
19
+ @number = 2
20
+
21
+ @sprite_count = 0
22
+ @sprite_index = 0
23
+ @current_sprite = nil
24
+ end
25
+
26
+ # This method is called by the PPU each T-cycle
27
+ # for all visible scanlines (0 - 143) and when
28
+ # dots are between 0 and 79.
29
+ #
30
+ # Takes exactly 2 dots to fetch a sprite so every
31
+ # even dot (0, 2, 4, ..., 78) is a NO-OP.
32
+ def tick
33
+ if @ppu.dots.odd?
34
+ @current_sprite = @ppu.fetch_sprite_at(@sprite_index) #=> 0 - 39
35
+ @sprite_index += 1
36
+
37
+ if sprite_within_current_scanline? && @sprite_count < Ppu::MAX_SPRITES_PER_SCANLINE
38
+ @ppu.sprite_buffer << @current_sprite
39
+ @sprite_count += 1
40
+ end
41
+ end
42
+
43
+ return unless @ppu.dots == SCAN_DURATION_IN_DOTS
44
+
45
+ sort_sprites_by_x_positions unless @ppu.sprite_buffer.empty?
46
+ @sprite_count = 0
47
+ @sprite_index = 0
48
+ @ppu.wy_eq_ly = true if window_triggered?
49
+ @ppu.set_mode(:rendering)
50
+ end
51
+
52
+ # Custom inspect to prevent circular dependencies.
53
+ def inspect
54
+ '#<Amaterasu::GameBoy::Ppu::Modes::OamScan ' \
55
+ "@name='#{@name}' " \
56
+ "@number=#{@number} " \
57
+ "@sprite_buffer=#{@sprite_buffer}"
58
+ end
59
+
60
+ # Custom to_s method to use in the Ppu#log_state method.
61
+ def to_s
62
+ "#{@name} (##{@number}) | " \
63
+ "SCANNED: Sprite ##{format('%02d', @sprite_index)} | " \
64
+ "BUFFER: #{@ppu.sprite_buffer} (#{@ppu.sprite_buffer.size})"
65
+ end
66
+
67
+ private
68
+
69
+ def window_triggered?
70
+ return false unless @ppu.registers.lcdc.window_enabled?
71
+ return false unless @ppu.registers.ly == @ppu.registers.wy
72
+ return false if @ppu.wy_eq_ly
73
+
74
+ true
75
+ end
76
+
77
+ def sprite_within_current_scanline?
78
+ sprite_height = @ppu.registers.lcdc.obj_size_8x16? ? 16 : 8
79
+
80
+ @ppu.registers.ly >= @current_sprite.y_screen_pos
81
+ && @ppu.registers.ly < @current_sprite.y_screen_pos + sprite_height
82
+ end
83
+
84
+ # TODO: benchmark the performance of both
85
+ def sort_sprites_by_x_positions
86
+ @ppu.sprite_buffer.sort_by!(&:x)
87
+ # @ppu.sprite_buffer.sort! { |a, b| a.x <=> b.x }
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Ppu
6
+ module Modes
7
+ class Rendering
8
+ # Responsible for fetching Background and Window tiles,
9
+ # decoding the pixels and outputting them into the BG/WIN FIFO.
10
+ #
11
+ # Despite the similarity it behaves differently than the SpriteFetcher.
12
+ class BgWinFetcher
13
+ attr_reader :fetch_mode, :step
14
+
15
+ def initialize(ppu, bg_win_fifo)
16
+ @ppu = ppu
17
+ @bg_win_fifo = bg_win_fifo
18
+
19
+ @step = :fetch_tile_index
20
+ @bg_fetcher_x = 0
21
+ @window_fetcher_x = 0
22
+ @fetch_mode = :bg
23
+
24
+ @fetch_duration = 6
25
+ @sleep_duration = 2
26
+ @warming_up = true
27
+
28
+ @tile_index = nil
29
+ @tile_data_low = nil
30
+ @tile_data_high = nil
31
+ @tile_pixels = nil
32
+ end
33
+
34
+ # Core fetch state machine.
35
+ #
36
+ def tick
37
+ case @step
38
+ when :fetch_tile_index then fetch_tile_index
39
+ when :fetch_tile_data_low then fetch_tile_data_low
40
+ when :fetch_tile_data_high then fetch_tile_data_high
41
+ when :push then push
42
+ when :sleep then sleep
43
+ end
44
+ end
45
+
46
+ def activate_window!
47
+ @bg_win_fifo.clear
48
+ @fetch_mode = :window
49
+ reset_cycle
50
+ end
51
+
52
+ def increment_window_y
53
+ return unless @window_drawed
54
+
55
+ @ppu.window_y_count += 1
56
+ @window_drawed = false
57
+ end
58
+
59
+ def reset_for_scanline
60
+ reset_cycle
61
+ @fetch_mode = :bg
62
+ @warming_up = true
63
+ @bg_fetcher_x = 0
64
+ @window_fetcher_x = 0
65
+ increment_window_y
66
+ end
67
+
68
+ private
69
+
70
+ # Fetches the BG Tile index at the current Tile map.
71
+ def fetch_tile_index
72
+ @fetch_duration -= 1
73
+ return unless @fetch_duration == 4
74
+
75
+ tile_x = @fetch_mode == :bg ? current_bg_tile_x : current_window_tile_x
76
+ tile_y = @fetch_mode == :bg ? current_bg_tile_y : current_window_tile_y
77
+
78
+ @tile_index = current_tile_map.tile_index_at(
79
+ tile_x: tile_x,
80
+ tile_y: tile_y
81
+ )
82
+
83
+ @step = :fetch_tile_data_low
84
+ end
85
+
86
+ # Fetches the low byte of the BG or Window Tile Row
87
+ # that overlaps with the current row/line being drawn.
88
+ def fetch_tile_data_low
89
+ @fetch_duration -= 1
90
+ return unless @fetch_duration == 2
91
+
92
+ current_y = @fetch_mode == :bg ? current_bg_y : current_window_y
93
+ @tile_data_low = current_tile_data.tile_at(@tile_index).data_low(current_y)
94
+
95
+ @step = :fetch_tile_data_high
96
+ end
97
+
98
+ # Fetches the high byte of the BG or Window Tile Row
99
+ # that overlaps with the current row/line being drawn.
100
+ def fetch_tile_data_high
101
+ @fetch_duration -= 1
102
+ return unless @fetch_duration == 0
103
+
104
+ current_y = @fetch_mode == :bg ? current_bg_y : current_window_y
105
+ @tile_data_high = current_tile_data.tile_at(@tile_index).data_high(current_y)
106
+ fetch_tile_pixels
107
+ @warming_up ? discard_pixels : attempt_push_into_fifo
108
+ end
109
+
110
+ # Fetches the BG or Window Tile pixels.
111
+ def fetch_tile_pixels
112
+ @tile_pixels = Vram::Tile::PIXELS_LOOKUP[(@tile_data_high << 8) | @tile_data_low]
113
+ end
114
+
115
+ def discard_pixels
116
+ @tile_pixels = nil
117
+ @warming_up = false
118
+ reset_cycle
119
+ end
120
+
121
+ def attempt_push_into_fifo
122
+ push_successful = @bg_win_fifo.push?(@tile_pixels)
123
+
124
+ if push_successful
125
+ if @fetch_mode == :bg
126
+ @bg_fetcher_x += 1
127
+ else
128
+ @window_fetcher_x += 1
129
+ @window_drawed = true
130
+ end
131
+
132
+ @step = :sleep
133
+ else
134
+ @step = :push
135
+ end
136
+ end
137
+
138
+ def push
139
+ push_successful = @bg_win_fifo.push?(@tile_pixels)
140
+ return unless push_successful
141
+
142
+ reset_cycle
143
+ end
144
+
145
+ def sleep
146
+ @sleep_duration -= 1
147
+ reset_cycle if @sleep_duration == 0
148
+ end
149
+
150
+ def reset_cycle
151
+ @step = :fetch_tile_index
152
+ @fetch_duration = 6
153
+ @sleep_duration = 2
154
+ @tile_index = nil
155
+ @tile_data_low = nil
156
+ @tile_data_high = nil
157
+ @tile_pixels = nil
158
+ end
159
+
160
+ def current_bg_y
161
+ @ppu.registers.ly + @ppu.registers.scy
162
+ end
163
+
164
+ def current_bg_tile_x
165
+ (@ppu.registers.scx / Vram::Tile::PIXEL_WIDTH) + @bg_fetcher_x
166
+ end
167
+
168
+ def current_bg_tile_y
169
+ current_bg_y / Vram::Tile::PIXEL_HEIGHT
170
+ end
171
+
172
+ def current_window_y
173
+ @ppu.window_y_count
174
+ end
175
+
176
+ def current_window_tile_x
177
+ @window_fetcher_x
178
+ end
179
+
180
+ def current_window_tile_y
181
+ @ppu.window_y_count / Vram::Tile::PIXEL_HEIGHT
182
+ end
183
+
184
+ # @return [Vram::TileMap]
185
+ def current_tile_map
186
+ @fetch_mode == :bg ? @ppu.bg_tile_map : @ppu.window_tile_map
187
+ end
188
+
189
+ # @return [Vram::TileData]
190
+ def current_tile_data
191
+ @ppu.bg_win_tile_data
192
+ end
193
+
194
+ def to_s
195
+ "Mode: #{@fetch_mode.upcase} | " \
196
+ "Step: #{@step.upcase} | " \
197
+ "PIX: #{@tile_pixels}"
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end