rubyboy 0.4.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Rubyboy
6
4
  class Interrupt
7
- attr_accessor :ie, :if
5
+ INTERRUPTS = {
6
+ vblank: 0,
7
+ lcd: 1,
8
+ timer: 2,
9
+ serial: 3,
10
+ joypad: 4
11
+ }.freeze
8
12
 
9
13
  def initialize
10
14
  @ie = 0
@@ -34,7 +38,11 @@ module Rubyboy
34
38
  end
35
39
 
36
40
  def request(interrupt)
37
- @if |= interrupt
41
+ @if |= (1 << INTERRUPTS[interrupt])
42
+ end
43
+
44
+ def reset_flag(i)
45
+ @if &= (~(1 << i)) & 0xff
38
46
  end
39
47
  end
40
48
  end
@@ -13,8 +13,8 @@ module Rubyboy
13
13
  raise "not implemented: write_byte #{addr}" unless addr == 0xff00
14
14
 
15
15
  res = @mode | 0xcf
16
- res &= @direction if @mode[4].zero?
17
- res &= @action if @mode[5].zero?
16
+ res &= @direction if @mode[4] == 0
17
+ res &= @action if @mode[5] == 0
18
18
 
19
19
  res
20
20
  end
@@ -29,13 +29,13 @@ module Rubyboy
29
29
  def direction_button(button)
30
30
  @direction = button | 0xf0
31
31
 
32
- @interupt.request(0b0001_0000) if button < 0b1111
32
+ @interupt.request(:joypad) if button < 0b1111
33
33
  end
34
34
 
35
35
  def action_button(button)
36
36
  @action = button | 0xf0
37
37
 
38
- @interupt.request(0b0001_0000) if button < 0b1111
38
+ @interupt.request(:joypad) if button < 0b1111
39
39
  end
40
40
  end
41
41
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Direct16
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Direct8
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class HlDec
6
+ def initialize(value = 0)
7
+ @value = value
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class HlInc
6
+ def initialize(value = 0)
7
+ @value = value
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Immediate16
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Immediate8
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Indirect
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Register16
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module Operand
5
+ class Register8
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class Operand
5
+ attr_reader :type, :value
6
+
7
+ def initialize(type:, value: 0)
8
+ @type = type
9
+ @value = value
10
+ end
11
+ end
12
+ end
data/lib/rubyboy/ppu.rb CHANGED
@@ -11,6 +11,33 @@ module Rubyboy
11
11
  drawing: 3
12
12
  }.freeze
13
13
 
14
+ LCDC = {
15
+ bg_window_enable: 0,
16
+ sprite_enable: 1,
17
+ sprite_size: 2,
18
+ bg_tile_map_area: 3,
19
+ bg_window_tile_data_area: 4,
20
+ window_enable: 5,
21
+ window_tile_map_area: 6,
22
+ lcd_ppu_enable: 7
23
+ }.freeze
24
+
25
+ STAT = {
26
+ ly_eq_lyc: 2,
27
+ hblank: 3,
28
+ vblank: 4,
29
+ oam_scan: 5,
30
+ lyc: 6
31
+ }.freeze
32
+
33
+ SPRITE_FLAGS = {
34
+ bank: 3,
35
+ dmg_palette: 4,
36
+ x_flip: 5,
37
+ y_flip: 6,
38
+ priority: 7
39
+ }.freeze
40
+
14
41
  LCD_WIDTH = 160
15
42
  LCD_HEIGHT = 144
16
43
 
@@ -90,8 +117,6 @@ module Rubyboy
90
117
  # ly is read only
91
118
  when 0xff45
92
119
  @lyc = value
93
- when 0xff46
94
- # dma
95
120
  when 0xff47
96
121
  @bgp = value
97
122
  when 0xff48
@@ -106,7 +131,7 @@ module Rubyboy
106
131
  end
107
132
 
108
133
  def step(cycles)
109
- return false if @lcdc[7].zero?
134
+ return false if @lcdc[LCDC[:lcd_ppu_enable]] == 0
110
135
 
111
136
  res = false
112
137
  @cycles += cycles
@@ -124,7 +149,7 @@ module Rubyboy
124
149
  render_sprites
125
150
  @cycles -= DRAWING_CYCLES
126
151
  @mode = MODE[:hblank]
127
- @interrupt.request(0b0000_0010) if @stat[3] == 1
152
+ @interrupt.request(:lcd) if @stat[STAT[:hblank]] == 1
128
153
  end
129
154
  when MODE[:hblank]
130
155
  if @cycles >= HBLANK_CYCLES
@@ -134,11 +159,11 @@ module Rubyboy
134
159
 
135
160
  if @ly == LCD_HEIGHT
136
161
  @mode = MODE[:vblank]
137
- @interrupt.request(0b0000_0001)
138
- @interrupt.request(0b0000_0010) if @stat[4] == 1
162
+ @interrupt.request(:vblank)
163
+ @interrupt.request(:lcd) if @stat[STAT[:vblank]] == 1
139
164
  else
140
165
  @mode = MODE[:oam_scan]
141
- @interrupt.request(0b0000_0010) if @stat[5] == 1
166
+ @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
142
167
  end
143
168
  end
144
169
  when MODE[:vblank]
@@ -152,7 +177,7 @@ module Rubyboy
152
177
  @wly = 0
153
178
  handle_ly_eq_lyc
154
179
  @mode = MODE[:oam_scan]
155
- @interrupt.request(0b0000_0010) if @stat[5] == 1
180
+ @interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
156
181
  res = true
157
182
  end
158
183
  end
@@ -162,30 +187,34 @@ module Rubyboy
162
187
  end
163
188
 
164
189
  def render_bg
165
- return if @lcdc[0].zero?
190
+ return if @lcdc[LCDC[:bg_window_enable]] == 0
166
191
 
167
192
  y = (@ly + @scy) % 256
193
+ tile_map_addr = @lcdc[LCDC[:bg_tile_map_area]] == 0 ? 0x1800 : 0x1c00
194
+ tile_map_addr += (y / 8) * 32
168
195
  LCD_WIDTH.times do |i|
169
196
  x = (i + @scx) % 256
170
- tile_index = get_tile_index(@lcdc[3], x, y)
171
- pixel = get_pixel(tile_index, x, y)
197
+ tile_index = get_tile_index(tile_map_addr + (x / 8))
198
+ pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
172
199
  @buffer[@ly * LCD_WIDTH + i] = get_color(@bgp, pixel)
173
200
  @bg_pixels[i] = pixel
174
201
  end
175
202
  end
176
203
 
177
204
  def render_window
178
- return if @lcdc[0].zero? || @lcdc[5].zero? || @ly < @wy
205
+ return if @lcdc[LCDC[:bg_window_enable]] == 0 || @lcdc[LCDC[:window_enable]] == 0 || @ly < @wy
179
206
 
180
207
  rendered = false
181
208
  y = @wly
209
+ tile_map_addr = @lcdc[LCDC[:window_tile_map_area]] == 0 ? 0x1800 : 0x1c00
210
+ tile_map_addr += (y / 8) * 32
182
211
  LCD_WIDTH.times do |i|
183
212
  next if i < @wx - 7
184
213
 
185
214
  rendered = true
186
215
  x = i - (@wx - 7)
187
- tile_index = get_tile_index(@lcdc[6], x, y)
188
- pixel = get_pixel(tile_index, x, y)
216
+ tile_index = get_tile_index(tile_map_addr + (x / 8))
217
+ pixel = get_pixel(tile_index << 4, 7 - (x % 8), (y % 8) * 2)
189
218
  @buffer[@ly * LCD_WIDTH + i] = get_color(@bgp, pixel)
190
219
  @bg_pixels[i] = pixel
191
220
  end
@@ -193,40 +222,41 @@ module Rubyboy
193
222
  end
194
223
 
195
224
  def render_sprites
196
- return if @lcdc[1].zero?
225
+ return if @lcdc[LCDC[:sprite_enable]] == 0
197
226
 
198
- sprite_height = @lcdc[2].zero? ? 8 : 16
227
+ sprite_height = @lcdc[LCDC[:sprite_size]] == 0 ? 8 : 16
228
+ sprites = []
229
+ cnt = 0
199
230
 
200
- sprites = @oam.each_slice(4).filter_map do |sprite_attr|
201
- sprite = {
202
- y: (sprite_attr[0] - 16) % 256,
203
- x: (sprite_attr[1] - 8) % 256,
204
- tile_index: sprite_attr[2],
205
- flags: sprite_attr[3]
206
- }
207
- next if sprite[:y] > @ly || sprite[:y] + sprite_height <= @ly
231
+ @oam.each_slice(4) do |y, x, tile_index, flags|
232
+ y = (y - 16) % 256
233
+ x = (x - 8) % 256
234
+ next if y > @ly || y + sprite_height <= @ly
208
235
 
209
- sprite
236
+ sprites << { y:, x:, tile_index:, flags: }
237
+ cnt += 1
238
+ break if cnt == 10
210
239
  end
211
- sprites = sprites.take(10).sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
240
+ sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
212
241
 
213
242
  sprites.each do |sprite|
214
- pallet = sprite[:flags][4].zero? ? @obp0 : @obp1
243
+ flags = sprite[:flags]
244
+ pallet = flags[SPRITE_FLAGS[:dmg_palette]] == 0 ? @obp0 : @obp1
215
245
  tile_index = sprite[:tile_index]
216
246
  tile_index &= 0xfe if sprite_height == 16
217
247
  y = (@ly - sprite[:y]) % 256
218
- y = sprite_height - y - 1 if sprite[:flags][6] == 1
248
+ y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1
219
249
  tile_index = (tile_index + 1) % 256 if y >= 8
220
250
  y %= 8
221
251
 
222
252
  8.times do |x|
223
- x_flipped = sprite[:flags][5] == 1 ? 7 - x : x
253
+ x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x
224
254
 
225
- pixel = get_pixel(tile_index, x_flipped, y)
255
+ pixel = get_pixel(tile_index << 4, 7 - x_flipped, (y % 8) * 2)
226
256
  i = (sprite[:x] + x) % 256
227
257
 
228
- next if pixel.zero? || i >= LCD_WIDTH
229
- next if sprite[:flags][7] == 1 && @bg_pixels[i] != 0
258
+ next if pixel == 0 || i >= LCD_WIDTH
259
+ next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0
230
260
 
231
261
  @buffer[@ly * LCD_WIDTH + i] = get_color(pallet, pixel)
232
262
  end
@@ -235,15 +265,13 @@ module Rubyboy
235
265
 
236
266
  private
237
267
 
238
- def get_tile_index(tile_map_area, x, y)
239
- tile_map_addr = tile_map_area.zero? ? 0x1800 : 0x1c00
240
- tile_map_index = (y / 8) * 32 + (x / 8)
241
- tile_index = @vram[tile_map_addr + tile_map_index]
242
- @lcdc[4].zero? ? to_signed_byte(tile_index) + 256 : tile_index
268
+ def get_tile_index(tile_map_addr)
269
+ tile_index = @vram[tile_map_addr]
270
+ @lcdc[LCDC[:bg_window_tile_data_area]] == 0 ? to_signed_byte(tile_index) + 256 : tile_index
243
271
  end
244
272
 
245
- def get_pixel(tile_index, x, y)
246
- @vram[tile_index * 16 + (y % 8) * 2][7 - (x % 8)] + (@vram[tile_index * 16 + (y % 8) * 2 + 1][7 - (x % 8)] << 1)
273
+ def get_pixel(tile_index, c, r)
274
+ @vram[tile_index + r][c] + (@vram[tile_index + r + 1][c] << 1)
247
275
  end
248
276
 
249
277
  def get_color(pallet, pixel)
@@ -263,7 +291,7 @@ module Rubyboy
263
291
  def handle_ly_eq_lyc
264
292
  if @ly == @lyc
265
293
  @stat |= 0x04
266
- @interrupt.request(0b0000_0010) if @stat[6] == 1
294
+ @interrupt.request(:lcd) if @stat[STAT[:lyc]] == 1
267
295
  else
268
296
  @stat &= 0xfb
269
297
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Rubyboy
6
4
  class Register
7
5
  attr_reader :value
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class Registers
5
+ attr_reader :value
6
+
7
+ def initialize
8
+ @a = 0x01
9
+ @b = 0x00
10
+ @c = 0x13
11
+ @d = 0x00
12
+ @e = 0xd8
13
+ @h = 0x01
14
+ @l = 0x4d
15
+ @f = 0xb0
16
+ end
17
+
18
+ def read8(register)
19
+ case register
20
+ when :a then @a
21
+ when :b then @b
22
+ when :c then @c
23
+ when :d then @d
24
+ when :e then @e
25
+ when :h then @h
26
+ when :l then @l
27
+ when :f then @f
28
+ end
29
+ end
30
+
31
+ def write8(register, value)
32
+ value &= 0xff
33
+ case register
34
+ when :a then @a = value
35
+ when :b then @b = value
36
+ when :c then @c = value
37
+ when :d then @d = value
38
+ when :e then @e = value
39
+ when :h then @h = value
40
+ when :l then @l = value
41
+ when :f then @f = value & 0xf0
42
+ end
43
+ end
44
+
45
+ def read16(register)
46
+ case register
47
+ when :af then (@a << 8) | @f
48
+ when :bc then (@b << 8) | @c
49
+ when :de then (@d << 8) | @e
50
+ when :hl then (@h << 8) | @l
51
+ end
52
+ end
53
+
54
+ def write16(register, value)
55
+ value &= 0xffff
56
+ case register
57
+ when :af
58
+ @a = (value >> 8) & 0xff
59
+ @f = value & 0xf0
60
+ when :bc
61
+ @b = (value >> 8) & 0xff
62
+ @c = value & 0xff
63
+ when :de
64
+ @d = (value >> 8) & 0xff
65
+ @e = value & 0xff
66
+ when :hl
67
+ @h = (value >> 8) & 0xff
68
+ @l = value & 0xff
69
+ end
70
+ end
71
+
72
+ def increment16(register)
73
+ write16(register, read16(register) + 1)
74
+ end
75
+
76
+ def decrement16(register)
77
+ write16(register, read16(register) - 1)
78
+ end
79
+ end
80
+ end
data/lib/rubyboy/rom.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rubyboy
4
4
  class Rom
5
- attr_accessor :data, :entroy_point, :logo, :title, :new_licensee_code, :sgb_flag, :cartridge_type, :rom_size, :ram_size, :destination_code, :old_licensee_code, :mask_rom_version_number, :header_checksum, :global_checksum
5
+ attr_reader :data, :entroy_point, :logo, :title, :new_licensee_code, :sgb_flag, :cartridge_type, :rom_size, :ram_size, :destination_code, :old_licensee_code, :mask_rom_version_number, :header_checksum, :global_checksum
6
6
 
7
7
  LOGO_DUMP = %w[
8
8
  CE ED 66 66 CC 0D 00 0B 03 73 00 83 00 0C 00 0D
@@ -18,49 +18,20 @@ module Rubyboy
18
18
  private
19
19
 
20
20
  def load_data
21
- # The Cartridge Header
22
- # see: https://gbdev.io/pandocs/The_Cartridge_Header.html
23
-
24
- # 0x100 - 0x103: Entry Point
25
21
  @entroy_point = @data[0x100..0x103]
26
- # p data[0x102..0x103].pack('C*').unpack('v').first
27
-
28
- # 0x104 - 0x133: Nintendo Logo
29
22
  @logo = @data[0x104..0x133]
30
23
  raise 'logo is not match' unless @logo == LOGO_DUMP
31
24
 
32
- # 0x134 - 0x143: Title
33
25
  @title = @data[0x134..0x143]
34
- # p data[0x134..0x143].pack('C*').strip
35
-
36
- # 0x144 - 0x145: New Licensee Code
37
26
  @new_licensee_code = @data[0x144..0x145]
38
-
39
- # 0x146: SGB Flag
40
27
  @sgb_flag = @data[0x146]
41
-
42
- # 0x147: Cartridge Type
43
28
  @cartridge_type = @data[0x147]
44
-
45
- # 0x148: ROM Size
46
29
  @rom_size = @data[0x148]
47
-
48
- # 0x149: RAM Size
49
30
  @ram_size = @data[0x149]
50
-
51
- # 0x14A: Destination Code
52
31
  @destination_code = @data[0x14A]
53
-
54
- # 0x14B: Old Licensee Code
55
32
  @old_licensee_code = @data[0x14B]
56
-
57
- # 0x14C: Mask ROM Version number
58
33
  @mask_rom_version_number = @data[0x14C]
59
-
60
- # 0x14D: Header Checksum
61
34
  @header_checksum = @data[0x14D]
62
-
63
- # 0x14E - 0x14F: Global Checksum
64
35
  @global_checksum = @data[0x14E..0x14F]
65
36
  end
66
37
  end
data/lib/rubyboy/timer.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Rubyboy
6
4
  class Timer
7
5
  def initialize(interrupt)
@@ -21,7 +19,7 @@ module Rubyboy
21
19
  @div += after_cycles / 256 - before_cycles / 256
22
20
  @div &= 0xffff
23
21
 
24
- return if @tac[2].zero?
22
+ return if @tac[2] == 0
25
23
 
26
24
  divider = case @tac & 0b11
27
25
  when 0b00 then 1024
@@ -35,8 +33,8 @@ module Rubyboy
35
33
 
36
34
  return if @tima < 256
37
35
 
38
- @tima &= 0xff
39
- @interrupt.request(0b0000_0100)
36
+ @tima = @tma
37
+ @interrupt.request(:timer)
40
38
  end
41
39
 
42
40
  def read_byte(byte)
@@ -56,6 +54,7 @@ module Rubyboy
56
54
  case byte
57
55
  when 0xff04
58
56
  @div = 0
57
+ @cycles = 0
59
58
  when 0xff05
60
59
  @tima = value
61
60
  when 0xff06
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyboy
4
- VERSION = '0.4.0'
4
+ VERSION = '1.1.0'
5
5
  end
data/lib/rubyboy.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'raylib'
4
- require 'benchmark'
5
- require_relative 'rubyboy/version'
6
4
  require_relative 'rubyboy/bus'
7
5
  require_relative 'rubyboy/cpu'
8
6
  require_relative 'rubyboy/ppu'
@@ -10,29 +8,33 @@ require_relative 'rubyboy/rom'
10
8
  require_relative 'rubyboy/timer'
11
9
  require_relative 'rubyboy/lcd'
12
10
  require_relative 'rubyboy/joypad'
11
+ require_relative 'rubyboy/interrupt'
13
12
 
14
13
  module Rubyboy
15
14
  class Console
16
15
  include Raylib
17
16
 
18
- def initialize(rom_data)
17
+ def initialize(rom_path)
19
18
  load_raylib
19
+ rom_data = File.open(rom_path, 'r') { _1.read.bytes }
20
20
  rom = Rom.new(rom_data)
21
21
  interrupt = Interrupt.new
22
22
  @ppu = Ppu.new(interrupt)
23
23
  @timer = Timer.new(interrupt)
24
24
  @joypad = Joypad.new(interrupt)
25
25
  @bus = Bus.new(@ppu, rom, @timer, interrupt, @joypad)
26
- @cpu = Cpu.new(@bus)
26
+ @cpu = Cpu.new(@bus, interrupt)
27
27
  @lcd = Lcd.new
28
28
  end
29
29
 
30
30
  def start
31
31
  until @lcd.window_should_close?
32
- key_input_check
33
32
  cycles = @cpu.exec
34
33
  @timer.step(cycles)
35
- draw if @ppu.step(cycles)
34
+ if @ppu.step(cycles)
35
+ draw
36
+ key_input_check
37
+ end
36
38
  end
37
39
  @lcd.close_window
38
40
  rescue StandardError => e
@@ -40,6 +42,21 @@ module Rubyboy
40
42
  raise e
41
43
  end
42
44
 
45
+ def bench
46
+ cnt = 0
47
+ start_time = Time.now
48
+ while cnt < 1500
49
+ cycles = @cpu.exec
50
+ @timer.step(cycles)
51
+ if @ppu.step(cycles)
52
+ key_input_check
53
+ cnt += 1
54
+ end
55
+ end
56
+
57
+ Time.now - start_time
58
+ end
59
+
43
60
  private
44
61
 
45
62
  def draw
@@ -78,6 +95,3 @@ module Rubyboy
78
95
  end
79
96
  end
80
97
  end
81
-
82
- rom_data = File.open(File.expand_path('roms/bgbtest.gb', __dir__), 'r') { _1.read.bytes }
83
- Rubyboy::Console.new(rom_data).start