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,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ # Models the built-in clock timer inside the Game Boy.
6
+ #
7
+ # As of now it is implemented using M-cycle accuracy,
8
+ # I might change it afterwards to T-cycle accuracy, but
9
+ # as of now all acceptance tests are passing, so no need.
10
+ class Timer
11
+ # Each tick advances 4 T-cycles / 1 M-cycle
12
+ T_CYCLES = 4
13
+
14
+ # Master clock defined by the hardware specs (in T-cycles).
15
+ MASTER_CLOCK_FREQUENCY = 4_194_304
16
+
17
+ # Frequency in which TIMA increments once for each TAC clock select.
18
+ TIMA_INCREMENT_FREQUENCIES = [
19
+ 4_096,
20
+ 262_144,
21
+ 65_536,
22
+ 16_384
23
+ ].freeze
24
+
25
+ # How many T-cycles are needed to increment TIMA once for each clock select.
26
+ TIMA_INCREMENT_CYCLES = [
27
+ MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b00],
28
+ MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b01],
29
+ MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b10],
30
+ MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b11]
31
+ ].freeze
32
+
33
+ # TIMA only increments if there is a falling edge.
34
+ # The value needs to be divided by 2 to achieve the correct value.
35
+ # The given bit needs to flip twice to reach a falling edge (0 -> 1 and 1 -> 0).
36
+ COUNTER_FALLING_EDGE_CYCLES = [
37
+ TIMA_INCREMENT_CYCLES[0b00] / 2,
38
+ TIMA_INCREMENT_CYCLES[0b01] / 2,
39
+ TIMA_INCREMENT_CYCLES[0b10] / 2,
40
+ TIMA_INCREMENT_CYCLES[0b11] / 2
41
+ ].freeze
42
+
43
+ # In binary a given Bit N always flips its value after 2^N ticks.
44
+ # Based on the number of cycles derived above you can find the correct bit to watch.
45
+ #
46
+ # Example for clock select 0b00:
47
+ # - 1024 T-cycles to increment TIMA.
48
+ # - So we need to find a Bit N that flips (1024 / 2) times to achieve a falling edge.
49
+ # - 2^N = 512 => N = log2(512) => N = 9 (Watch Bit 9 from the system counter).
50
+ COUNTER_BITS_TO_WATCH = [
51
+ Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b00]).round,
52
+ Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b01]).round,
53
+ Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b10]).round,
54
+ Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b11]).round
55
+ ].freeze
56
+
57
+ # Returns the 8-bit value stored in the TIMA (Timer Counter) register.
58
+ attr_reader :tima
59
+
60
+ # Returns the 8-bit value stored in the TMA (Timer Modulo) register.
61
+ attr_reader :tma
62
+
63
+ # Returns the 8-bit value stored in the TAC (Timer Control) register.
64
+ attr_reader :tac
65
+
66
+ # Creates an instance of the timer.
67
+ #
68
+ # - Needs the interrupts instance to request a timer interrupt.
69
+ # - Has an internal-only 16-bit counter that increments each T-cycle.
70
+ def initialize(interrupts, skip_boot_rom: true, trace_timer: false)
71
+ @interrupts = interrupts
72
+ @trace_timer = trace_timer
73
+
74
+ @counter = skip_boot_rom ? 0xABCC : 0x0000
75
+ @tima = 0x00
76
+ @tma = 0x00
77
+ @tac = skip_boot_rom ? 0xF8 : 0x00
78
+
79
+ @tac_enable_bit = @tac[2]
80
+ @tac_clock_select_bits = @tac & 0b11
81
+ @counter_watched_bit_pos = COUNTER_BITS_TO_WATCH[@tac_clock_select_bits]
82
+
83
+ @state = :running
84
+ @tima_overflow = false
85
+ end
86
+
87
+ # Reading DIV only exposes the upper byte of the system counter.
88
+ #
89
+ # @return [Integer]
90
+ def div
91
+ (@counter >> 8) & 0xFF
92
+ end
93
+
94
+ # Writing to DIV register always resets the system counter.
95
+ #
96
+ # When the value is reset to 0x0000, if the current bit being "watched"
97
+ # goes from 1 -> 0 it can trigger a TIMA increment due to a falling edge
98
+ # in the joint signal.
99
+ #
100
+ # @param value [Integer] Value is ignored and resets the whole @counter.
101
+ def div=(_value)
102
+ old_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
103
+ @counter = 0x0000
104
+ new_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
105
+
106
+ increment_tima if falling_edge?(old_signal, new_signal)
107
+ end
108
+
109
+ # Sets a 8-bit value into the TIMA register.
110
+ #
111
+ # Obscure behaviors:
112
+ # - If the CPU tries to write to TIMA the same cycle it was already reloaded
113
+ # with TMA, the write is completely ignored and TIMA keeps the TMA value.
114
+ # - If the CPU tries to write to TIMA the cycle immediately after it overflows,
115
+ # but hasn't been reloaded with TMA yet, the write succeeds, TIMA keeps the
116
+ # value written by the CPU and the reload is cancelled as if the overflow never happened.
117
+ #
118
+ # @param value [Integer] 8-bit value to store in the TIMA register.
119
+ def tima=(value)
120
+ return if @state == :tima_reloaded
121
+
122
+ @state = :tima_reloaded if @state == :tima_reload_pending
123
+ @tima = value & 0xFF
124
+ end
125
+
126
+ # Sets a 8-bit value into the TMA register.
127
+ #
128
+ # Obscure behavior:
129
+ # - If the CPU writes to TMA the cycle after TIMA was already reloaded,
130
+ # TIMA is reloaded a second time with the new value because the TMA latch
131
+ # remains open for 2 M-cycles (the original reload + the next).
132
+ #
133
+ # @param value [Integer] 8-bit value to store in the TMA register.
134
+ def tma=(value)
135
+ @tma = value & 0xFF
136
+ @tima = @tma if @state == :tima_reloaded
137
+ end
138
+
139
+ # Sets a 8-bit value into the TAC register.
140
+ #
141
+ # There are 2 distinct cases in which writing to TAC can cause
142
+ # a sporadic TIMA increment:
143
+ # - TAC Enable (Bit 2) going from 1 -> 0 can cause a falling edge in the joint signal.
144
+ # - Changing the TAC Clock Select (Bits 1-0) can also cause a falling edge
145
+ # if the previous bit selected from the counter was 1, and the new one is 0.
146
+ #
147
+ # @param value [Integer] 8-bit value to store in the TAC register.
148
+ def tac=(value)
149
+ old_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
150
+
151
+ @tac = value & 0xFF
152
+ @tac_enable_bit = @tac[2]
153
+ @tac_clock_select_bits = @tac & 0b11
154
+ @counter_watched_bit_pos = COUNTER_BITS_TO_WATCH[@tac_clock_select_bits]
155
+
156
+ new_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
157
+
158
+ increment_tima if falling_edge?(old_signal, new_signal)
159
+ end
160
+
161
+ # Advances the system counter, increments TIMA if needed and handles TIMA overflow logic.
162
+ #
163
+ # - Counter value should wrap around 0xFFFF (16-bit).
164
+ # - The Counter is always counting independent from all the other logic.
165
+ # - The tick is being implemented in M-cycle precision, so each tick is 4 T-cycles.
166
+ # - After TIMA overflows, there is a 1 M-cycle delay before
167
+ # setting TMA into TIMA and requesting the Timer interrupt.
168
+ def tick
169
+ old_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
170
+ @counter = (@counter + T_CYCLES) & 0xFFFF
171
+ new_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
172
+
173
+ case @state
174
+ when :running
175
+ increment_tima if falling_edge?(old_signal, new_signal)
176
+ when :tima_reload_pending
177
+ @tima = @tma
178
+ @interrupts.request(:timer)
179
+ @state = :tima_reloaded
180
+ when :tima_reloaded
181
+ @state = :running
182
+ end
183
+
184
+ log_state(old_signal, new_signal) if @trace_timer
185
+ end
186
+
187
+ private
188
+
189
+ # Increments TIMA once, the register is 8-bit so it wraps around 0xFF.
190
+ # Sets a new Timer state when the overflow occurs to be handled in the M-cycle after.
191
+ def increment_tima
192
+ @tima = (@tima + 1) & 0xFF
193
+ @state = :tima_reload_pending if @tima.zero?
194
+ end
195
+
196
+ # Checks for a falling edge (1 -> 0) in the joint signal.
197
+ # Joint signal is a bitwise AND between:
198
+ # - TAC enable bit (Bit 2)
199
+ # - Currently selected bit in the system counter
200
+ #
201
+ # @param old_signal [Integer] Either 0 or 1.
202
+ # @param new_signal [Integer] Either 0 or 1.
203
+ # @return [Boolean]
204
+ def falling_edge?(old_signal, new_signal)
205
+ old_signal == 1 && new_signal == 0
206
+ end
207
+
208
+ # Logs the state of the Timer for each M-cycle if the Timer trace is set.
209
+ #
210
+ # @param old_signal [Integer] Either 1 or 0.
211
+ # @param new_signal [Integer] Either 1 or 0.
212
+ def log_state(old_signal, new_signal)
213
+ $stdout.printf(
214
+ 'COUNTER: $%<n>04X (%<n>06d) || :%<state>-20s || ' \
215
+ 'TIMA: $%<tima>02X || TMA: $%<tma>02X || TAC: %<tac>08b || ' \
216
+ 'OLD SIGNAL: %<os>d => NEW SIGNAL: %<ns>d || ' \
217
+ "INTERRUPT: %<int>d\n",
218
+ n: @counter,
219
+ state: @state.upcase,
220
+ tima: @tima,
221
+ tma: @tma,
222
+ tac: @tac,
223
+ os: old_signal,
224
+ ns: new_signal,
225
+ int: @interrupts.if_register[2]
226
+ )
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Vram
6
+ # Models each Tile that lives in the VRAM.
7
+ class Tile
8
+ PIXEL_HEIGHT = 8
9
+ PIXEL_WIDTH = 8
10
+ SIZE_IN_BYTES = 16
11
+ SIZE_BIT_MASK = 0b111
12
+
13
+ # Pre-computes all possible 8 pixel values given the low and high bytes
14
+ # for the Game Boy address range (0x0000 - 0xFFFF).
15
+ #
16
+ # Usage:
17
+ # Memory values: $3C (Low), $7E (High)
18
+ # PIXELS_LOOKUP[(0x7E << 8) | 0x3C] #=> [0, 2, 3, 3, 3, 3, 2, 0]
19
+ PIXELS_LOOKUP = Array.new(65_536) do |idx|
20
+ low_byte = idx & 0xFF
21
+ high_byte = (idx >> 8) & 0xFF
22
+
23
+ Array.new(8) do |i|
24
+ bit = 7 - i
25
+ low_bit = (low_byte >> bit) & 1
26
+ high_bit = (high_byte >> bit) & 1
27
+
28
+ (high_bit << 1) | low_bit
29
+ end.freeze
30
+ end.freeze
31
+
32
+ attr_reader :data
33
+
34
+ def initialize(vram_data:, tile_index:)
35
+ @vram_data = vram_data
36
+ @tile_index = tile_index
37
+ @base_offset = tile_index * SIZE_IN_BYTES
38
+ end
39
+
40
+ # @return [Integer] The low byte of the tile at a given row.
41
+ def data_low(current_y)
42
+ current_tile_y = current_y & SIZE_BIT_MASK
43
+ row_within_tile = current_tile_y * 2
44
+
45
+ @vram_data[@base_offset + row_within_tile]
46
+ end
47
+
48
+ # @return [Integer] The high byte of the tile at a given row.
49
+ def data_high(current_y)
50
+ current_tile_y = current_y & SIZE_BIT_MASK
51
+ row_within_tile = current_tile_y * 2
52
+
53
+ @vram_data[@base_offset + row_within_tile + 1]
54
+ end
55
+
56
+ def pixel_row(low_byte, high_byte)
57
+ PIXELS_LOOKUP[(high_byte << 8) | low_byte]
58
+ end
59
+
60
+ def inspect
61
+ '#<Tile ' \
62
+ "@tile_index=#{@tile_index} " \
63
+ "@base_offset=#{@base_offset}>"
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Vram
6
+ # Models the Tile Data that lives in the VRAM.
7
+ class TileData
8
+ TILE_SIZE = 16
9
+ TILE_ENTRIES = 384
10
+
11
+ attr_reader :tiles, :addressing_mode, :base_offset
12
+
13
+ # @param vram_data [Array] Original VRAM @data array object.
14
+ def initialize(vram_data:)
15
+ @addressing_mode = :unsigned
16
+ @base_offset = 0x0000
17
+
18
+ @tiles = Array.new(TILE_ENTRIES) do |index|
19
+ Tile.new(vram_data: vram_data, tile_index: index)
20
+ end
21
+ end
22
+
23
+ # @param mode [Symbol] Either :unsigned or :signed.
24
+ def addressing_mode=(mode)
25
+ @addressing_mode = mode
26
+ @base_offset = mode == :unsigned ? 0x0000 : 0x1000
27
+ end
28
+
29
+ # Fetches a Tile based on a given index, if the addressing mode
30
+ # is set to :signed, we need to sign the value before fetching
31
+ # the tile.
32
+ #
33
+ # @param tile_index [Integer] 8-bit value representing the tile index.
34
+ # @return [Vram::Tile]
35
+ def tile_at(tile_index)
36
+ tile_index = sign_value(tile_index) if @addressing_mode == :signed
37
+ tile_offset = @base_offset / Tile::SIZE_IN_BYTES
38
+
39
+ @tiles[tile_offset + tile_index]
40
+ end
41
+
42
+ private
43
+
44
+ # @param index [Integer] The current Tile index (8-bit value).
45
+ # @return [Integer] A value between -128 and +127.
46
+ def sign_value(index)
47
+ index >= 128 ? index - 256 : index
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ class Vram
6
+ # Models the Tile Map that live in VRAM.
7
+ #
8
+ # - Each Tile Map is a 32 x 32 grid of Tile indices.
9
+ # - Each Tile index is exactly 1 byte.
10
+ # - So each Tile Map has exactly 32 * 32 * 1 = 1024 bytes (1 KiB).
11
+ #
12
+ # Grid representation in 2 dimensions, each (X, Y) pair -> 1 byte:
13
+ #
14
+ # X = 0 X = 1 X = 2 X = 31
15
+ #
16
+ # Y = 0 (0, 0) (1, 0) (2, 0) ... (31, 0) <- Row 0
17
+ #
18
+ # Y = 1 (0, 1) (1, 1) (2, 1) ... (31, 1) <- Row 1
19
+ #
20
+ # Y = 2 (0, 2) (1, 2) (2, 2) ... (31, 2) <- Row 2
21
+ #
22
+ # ... ... ... ... ... ...
23
+ #
24
+ # Y = 31 (0, 31) (1, 31) (2, 31) ... (31, 31) <- Row 31
25
+ #
26
+ # ↑ ↑ ↑ ↑
27
+ #
28
+ # Column 0 Column 1 Column 2 ... Column 31
29
+ #
30
+ class TileMap
31
+ # Total number of columns in the grid.
32
+ GRID_WIDTH = 32
33
+
34
+ # Total number of rows in the grid.
35
+ GRID_HEIGHT = 32
36
+
37
+ # Mask to keep values between 0 and 31.
38
+ BIT_MASK_WRAP_VALUE = 0x1F
39
+
40
+ # @param vram_data [Array] Reference to the original VRAM data.
41
+ # @param base_offset [Integer] Tile map start address within the VRAM data.
42
+ def initialize(vram_data:, base_offset:)
43
+ @vram_data = vram_data
44
+ @base_offset = base_offset
45
+ end
46
+
47
+ # Fetches a Tile index from the Tile Map at a given (X, Y) position.
48
+ #
49
+ # Before looking up in which column or row the Tile index is,
50
+ # we need to wrap the values around 32 (0x1F).
51
+ #
52
+ # Since the Memory is a flat array (single dimension) we need to
53
+ # offset the given Y value by the Grid WIDTH to "jump over" the rows in
54
+ # between and reach the correct row.
55
+ #
56
+ # @param tile_x [Integer] Which column in the grid.
57
+ # @param tile_y [Integer] Which row in the grid.
58
+ # @return [Integer] 1 byte representing the Tile index at the given (X, Y).
59
+ def tile_index_at(tile_x:, tile_y:)
60
+ grid_row = tile_y & BIT_MASK_WRAP_VALUE
61
+ grid_column = tile_x & BIT_MASK_WRAP_VALUE
62
+
63
+ tile_index_row = grid_row * GRID_WIDTH
64
+ tile_index_column = grid_column
65
+
66
+ @vram_data[@base_offset + tile_index_row + tile_index_column]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module GameBoy
5
+ # Models the VRAM (Video RAM) from the DMG Game Boy.
6
+ #
7
+ # The VRAM address range can be divided into 2 main parts.
8
+ # 1. Tile Data:
9
+ # - Lives in 0x8000 -> 0x97FF (6144 bytes)
10
+ # - Each tile is composed of 8 x 8 = 64 pixels
11
+ # - Each pixel is encoded as 2 bits in the DMG Game Boy
12
+ # - So each tile has 64 * 2 = 128 bits (16 bytes)
13
+ # 2. Tile Maps:
14
+ # - Has a total of 2 Tile Maps (0x9800 -> 0x9BFF) / (0x9C00 -> 0x9FFF)
15
+ # - Each Tile Map is a 32 x 32 grid of Tile indices (each index is 1 byte)
16
+ # - So each Tile Map has exactly 1024 bytes (1 KiB)
17
+ class Vram < Ram
18
+ # VRAM Start Address in the Memory Map
19
+ START_ADDRESS = 0x8000
20
+
21
+ # VRAM End Address in the Memory Map
22
+ END_ADDRESS = 0x9FFF
23
+
24
+ # VRAM Size in bytes: 8192 bytes (8 KiB)
25
+ SIZE_IN_BYTES = (END_ADDRESS - START_ADDRESS) + 1
26
+
27
+ # Exposes the tile data and maps to the PPU
28
+ attr_reader :tile_data,
29
+ :tile_map_low,
30
+ :tile_map_high
31
+
32
+ # Creates "lens objects" passing the original VRAM @data Array,
33
+ # when a value is written into VRAM it will be correctly read
34
+ # by all the Tile Data and Tile Maps.
35
+ def initialize
36
+ super(size: SIZE_IN_BYTES, offset: START_ADDRESS)
37
+
38
+ @tile_data = TileData.new(vram_data: @data)
39
+ @tile_map_low = TileMap.new(vram_data: @data, base_offset: 0x1800)
40
+ @tile_map_high = TileMap.new(vram_data: @data, base_offset: 0x1C00)
41
+ end
42
+
43
+ def inspect
44
+ '#<Vram ' \
45
+ "@tile_data=#{@tile_data} " \
46
+ "@tile_map_low=#{@tile_map_low} " \
47
+ "@tile_map_high=#{@tile_map_high}>"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amaterasu
4
+ module HAL
5
+ # Console display renderer.
6
+ class Console
7
+ LCD_WIDTH = 160
8
+ LCD_HEIGHT = 144
9
+
10
+ DOUBLE_CHAR = '▀'
11
+ CONSOLE_CHARS = [' ', '░', '▒', '█'].freeze
12
+ # CONSOLE_CHARS = [' ', ':', '#', '@'].freeze
13
+
14
+ def initialize
15
+ $stdout.print("\e[?1049h")
16
+ end
17
+
18
+ def shutdown
19
+ $stdout.print("\e[?1049l")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module Amaterasu
6
+ module HAL
7
+ class SDL2
8
+ # Implement all SDL2 bindings needed.
9
+ module Bindings
10
+ extend FFI::Library
11
+
12
+ ffi_lib 'SDL2'
13
+
14
+ INIT_VIDEO = 0x00000020
15
+ WINDOWPOS_CENTERED = 0x2FFF0000
16
+ WINDOW_SHOWN = 0x00000004
17
+ PIXELFORMAT_ARGB8888 = 0x16362004
18
+ TEXTUREACCESS_STREAMING = 1
19
+ RENDERER_ACCELERATED = 0x00000002
20
+ QUIT = 0x100
21
+ EVENT_SIZE = 56
22
+
23
+ SCANCODE_UP = 82
24
+ SCANCODE_DOWN = 81
25
+ SCANCODE_LEFT = 80
26
+ SCANCODE_RIGHT = 79
27
+
28
+ SCANCODE_Z = 29
29
+ SCANCODE_X = 27
30
+ SCANCODE_RETURN = 40
31
+ SCANCODE_RSHIFT = 229
32
+
33
+ attach_function :init, :SDL_Init, [:uint32], :int
34
+ attach_function :quit, :SDL_Quit, [], :void
35
+ attach_function :get_error, :SDL_GetError, [], :string
36
+
37
+ attach_function :create_window, :SDL_CreateWindow, %i[string int int int int uint32], :pointer
38
+ attach_function :set_window_title, :SDL_SetWindowTitle, %i[pointer string], :void
39
+ attach_function :destroy_window, :SDL_DestroyWindow, [:pointer], :void
40
+
41
+ attach_function :create_renderer, :SDL_CreateRenderer, %i[pointer int uint32], :pointer
42
+ attach_function :destroy_renderer, :SDL_DestroyRenderer, [:pointer], :void
43
+
44
+ attach_function :create_texture, :SDL_CreateTexture, %i[pointer uint32 int int int], :pointer
45
+ attach_function :update_texture, :SDL_UpdateTexture, %i[pointer pointer pointer int], :int
46
+ attach_function :destroy_texture, :SDL_DestroyTexture, [:pointer], :void
47
+
48
+ attach_function :render_copy, :SDL_RenderCopy, %i[pointer pointer pointer pointer], :int
49
+ attach_function :render_present, :SDL_RenderPresent, [:pointer], :void
50
+ attach_function :render_clear, :SDL_RenderClear, [:pointer], :int
51
+
52
+ attach_function :poll_event, :SDL_PollEvent, [:pointer], :int
53
+
54
+ attach_function :get_keyboard_state, :SDL_GetKeyboardState, [:pointer], :pointer
55
+ attach_function :pump_events, :SDL_PumpEvents, [], :void
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module Amaterasu
6
+ module HAL
7
+ class SDL2
8
+ LCD = Bindings
9
+ LCD_WIDTH = 160
10
+ LCD_HEIGHT = 144
11
+ SCALE = 3
12
+
13
+ PALETTE = [0xFFFFFFFF, 0xFFAAAAAA, 0xFF555555, 0xFF000000].freeze
14
+
15
+ attr_accessor :joypad
16
+
17
+ def initialize
18
+ LCD.init(LCD::INIT_VIDEO)
19
+ @window = LCD.create_window(
20
+ 'Amaterasu',
21
+ LCD::WINDOWPOS_CENTERED,
22
+ LCD::WINDOWPOS_CENTERED,
23
+ LCD_WIDTH * SCALE,
24
+ LCD_HEIGHT * SCALE,
25
+ LCD::WINDOW_SHOWN
26
+ )
27
+ @renderer = LCD.create_renderer(@window, -1, LCD::RENDERER_ACCELERATED)
28
+ @texture = LCD.create_texture(
29
+ @renderer,
30
+ LCD::PIXELFORMAT_ARGB8888,
31
+ LCD::TEXTUREACCESS_STREAMING,
32
+ LCD_WIDTH,
33
+ LCD_HEIGHT
34
+ )
35
+
36
+ @pixel_buffer = FFI::MemoryPointer.new(:uint32, LCD_WIDTH * LCD_HEIGHT)
37
+ @event = FFI::MemoryPointer.new(:uint8, LCD::EVENT_SIZE)
38
+ @frame_count = 0
39
+ @fps_timer = Process.clock_gettime(Process::CLOCK_MONOTONIC)
40
+ end
41
+
42
+ def draw(framebuffer)
43
+ @pixel_buffer.write_array_of_uint32(framebuffer.map { |shade| PALETTE[shade] })
44
+ LCD.update_texture(@texture, FFI::Pointer::NULL, @pixel_buffer, LCD_WIDTH * 4)
45
+ LCD.render_copy(@renderer, @texture, FFI::Pointer::NULL, FFI::Pointer::NULL)
46
+ LCD.render_present(@renderer)
47
+
48
+ @frame_count += 1
49
+ @elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @fps_timer
50
+
51
+ if @elapsed >= 1.0
52
+ fps = @frame_count / @elapsed
53
+ # LCD.set_window_title(@window, "Amaterasu | FPS: #{fps.round(2)}")
54
+ $stdout.print "\rFPS: #{fps.round(2)} "
55
+ $stdout.flush
56
+ end
57
+
58
+ while LCD.poll_event(@event) == 1
59
+ # first 4 bytes of the event struct are the event type (uint32)
60
+ type = @event.read_uint32
61
+ case type
62
+ when LCD::QUIT
63
+ shutdown
64
+ exit
65
+ end
66
+ end
67
+
68
+ keyboard_state = LCD.get_keyboard_state(nil)
69
+
70
+ if keyboard_state.get_uint8(LCD::SCANCODE_UP) == 0
71
+ joypad.release_dpad(:up)
72
+ else
73
+ joypad.press_dpad(:up)
74
+ end
75
+
76
+ if keyboard_state.get_uint8(LCD::SCANCODE_DOWN) == 0
77
+ joypad.release_dpad(:down)
78
+ else
79
+ joypad.press_dpad(:down)
80
+ end
81
+
82
+ if keyboard_state.get_uint8(LCD::SCANCODE_RIGHT) == 0
83
+ joypad.release_dpad(:right)
84
+ else
85
+ joypad.press_dpad(:right)
86
+ end
87
+
88
+ if keyboard_state.get_uint8(LCD::SCANCODE_LEFT) == 0
89
+ joypad.release_dpad(:left)
90
+ else
91
+ joypad.press_dpad(:left)
92
+ end
93
+
94
+ if keyboard_state.get_uint8(LCD::SCANCODE_Z) == 0
95
+ joypad.release_face(:a)
96
+ else
97
+ joypad.press_face(:a)
98
+ end
99
+
100
+ if keyboard_state.get_uint8(LCD::SCANCODE_X) == 0
101
+ joypad.release_face(:b)
102
+ else
103
+ joypad.press_face(:b)
104
+ end
105
+
106
+ if keyboard_state.get_uint8(LCD::SCANCODE_RETURN) == 0
107
+ joypad.release_face(:start)
108
+ else
109
+ joypad.press_face(:start)
110
+ end
111
+
112
+ if keyboard_state.get_uint8(LCD::SCANCODE_RSHIFT) == 0
113
+ joypad.release_face(:select)
114
+ else
115
+ joypad.press_face(:select)
116
+ end
117
+ end
118
+
119
+ def shutdown
120
+ LCD.destroy_texture(@texture)
121
+ LCD.destroy_renderer(@renderer)
122
+ LCD.destroy_window(@window)
123
+ LCD.quit
124
+ end
125
+ end
126
+ end
127
+ end