rubyboy 0.4.0 → 1.1.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.
@@ -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