rubyboy 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c4b114437f8fdfe135e5dbb47c8ce18de914bf3bff25376a28d54a4550d3bfc
4
- data.tar.gz: f6f8714c97f42513c68d2a371742490a3a7ed295e056965e2bf47a50495fad0f
3
+ metadata.gz: a90f9cbc5a0ac4a9f9b971f0c4454421dc83ae9686222c275679f173e7503232
4
+ data.tar.gz: 52833516da6531982602c373acb11a74d292db6221b8ad34d4420a94be7fe017
5
5
  SHA512:
6
- metadata.gz: 810c319bb3b7766b277ba715c82d959bf97e5f16ad35994f3a84b7bd2726f24adac056427e067313b73c2f093c957d1a8f060b41b979083a34ceca5a9b20d956
7
- data.tar.gz: 70b411a71e7604f00b3f16257d8ccceec77795d0156e79f04c85b95093853a755fa75d949045f752c325f095e3427049fd71127436e50f77aa3f90be401009a2
6
+ metadata.gz: ef45830027a11b9827e8660bef2fbb45a5a0fc867486aef58ece8a930acae959eb66ac5709d425e8562d1a546608cbb4cce040efc9e17940b627dfc9095c4a7b
7
+ data.tar.gz: d8564cc7f5c66dbce015fead7ff770527736943e9c1de3858ed813c1cc012c1e9c5b9248010d16ee39a5fa668e720eb9cda93443b2097260b3b24688b9acdd42
data/.rubocop.yml CHANGED
@@ -14,6 +14,9 @@ Layout/LineLength:
14
14
  Metrics/AbcSize:
15
15
  Enabled: false
16
16
 
17
+ Metrics/BlockNesting:
18
+ Enabled: false
19
+
17
20
  Metrics/CyclomaticComplexity:
18
21
  Enabled: false
19
22
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2023-11-20
4
+
5
+ - Fix interrupt
6
+ - Fix sprite priority
7
+ - Implement joypad
8
+ - Pass bgbtest
9
+
10
+ ## [0.3.0] - 2023-11-19
11
+
12
+ - Fix bg rendering
13
+ - Add render_window
14
+ - Add render_sprites
15
+ - Add interrupt in ppu
16
+ - Add oam_dma_transfer
17
+ - Use raylib for rendering
18
+ - Pass dmg-acid2 test
19
+
3
20
  ## [0.2.0] - 2023-11-14
4
21
 
5
22
  - Add MBC1
Binary file
Binary file
data/lib/rubyboy/bus.rb CHANGED
@@ -8,11 +8,12 @@ module Rubyboy
8
8
  class Bus
9
9
  attr_accessor :ppu, :rom, :interrupt
10
10
 
11
- def initialize(ppu, rom, timer, interrupt)
11
+ def initialize(ppu, rom, timer, interrupt, joypad)
12
12
  @ppu = ppu
13
13
  @rom = rom
14
14
  @ram = Ram.new
15
15
  @mbc = Cartridge::Factory.create(rom, @ram)
16
+ @joypad = joypad
16
17
 
17
18
  @interrupt = interrupt
18
19
  @timer = timer
@@ -27,62 +28,37 @@ module Rubyboy
27
28
  when 0x0000..0x7fff
28
29
  @mbc.read_byte(addr)
29
30
  when 0x8000..0x9fff
30
- @ppu.vram[addr - 0x8000]
31
- when 0xa000..0xdfff
31
+ @ppu.read_byte(addr)
32
+ when 0xa000..0xbfff
32
33
  @mbc.read_byte(addr)
34
+ when 0xc000..0xcfff
35
+ @ram.wram1[addr - 0xc000]
36
+ when 0xd000..0xdfff
37
+ @ram.wram2[addr - 0xd000]
33
38
  when 0xe000..0xfdff
34
39
  # echo ram
35
40
  when 0xfe00..0xfe9f
36
- @oam[addr - 0xfe00]
41
+ @ppu.read_byte(addr)
37
42
  when 0xfea0..0xfeff
38
43
  # unused
39
44
  0xff
40
45
  when 0xff00
41
- # joypad
42
- @tmp[addr] ||= 0
46
+ @joypad.read_byte(addr)
43
47
  when 0xff01..0xff02
44
48
  # serial
45
49
  @tmp[addr] ||= 0
46
50
  when 0xff04..0xff07
47
51
  @timer.read_byte(addr)
48
52
  when 0xff0f
49
- @interrupt.if
53
+ @interrupt.read_byte(addr)
50
54
  when 0xff10..0xff26
51
55
  # sound
52
56
  @tmp[addr] ||= 0
53
57
  when 0xff30..0xff3f
54
58
  # wave pattern ram
55
59
  @tmp[addr] ||= 0
56
- when 0xff40
57
- @ppu.lcdc
58
- when 0xff41
59
- # stat
60
- @tmp[addr] ||= 0
61
- when 0xff42
62
- @ppu.scy
63
- when 0xff43
64
- @ppu.scx
65
- when 0xff44
66
- @ppu.ly
67
- when 0xff45
68
- @ppu.lyc
69
- when 0xff46
70
- # dma
71
- @tmp[addr] ||= 0
72
- when 0xff47
73
- @ppu.bgp
74
- when 0xff48
75
- # obp0
76
- @tmp[addr] ||= 0
77
- when 0xff49
78
- # obp1
79
- @tmp[addr] ||= 0
80
- when 0xff4a
81
- # wy
82
- @tmp[addr] ||= 0
83
- when 0xff4b
84
- # wx
85
- @tmp[addr] ||= 0
60
+ when 0xff40..0xff4b
61
+ @ppu.read_byte(addr)
86
62
  when 0xff4f
87
63
  # vbk
88
64
  @tmp[addr] ||= 0
@@ -101,7 +77,7 @@ module Rubyboy
101
77
  when 0xff80..0xfffe
102
78
  @ram.hram[addr - 0xff80]
103
79
  when 0xffff
104
- @interrupt.ie
80
+ @interrupt.read_byte(addr)
105
81
  else
106
82
  0xff
107
83
  end
@@ -112,61 +88,40 @@ module Rubyboy
112
88
  when 0x0000..0x7fff
113
89
  @mbc.write_byte(addr, value)
114
90
  when 0x8000..0x9fff
115
- @ppu.vram[addr - 0x8000] = value
116
- when 0xa000..0xdfff
91
+ @ppu.write_byte(addr, value)
92
+ when 0xa000..0xbfff
117
93
  @mbc.write_byte(addr, value)
94
+ when 0xc000..0xcfff
95
+ @ram.wram1[addr - 0xc000] = value
96
+ when 0xd000..0xdfff
97
+ @ram.wram2[addr - 0xd000] = value
118
98
  when 0xe000..0xfdff
119
99
  # echo ram
120
100
  when 0xfe00..0xfe9f
121
- @oam[addr - 0xfe00] = value
101
+ @ppu.write_byte(addr, value)
122
102
  when 0xfea0..0xfeff
123
103
  # unused
124
104
  when 0xff00
125
- # joypad
126
- @tmp[addr] = value
105
+ @joypad.write_byte(addr, value)
127
106
  when 0xff01..0xff02
128
107
  # serial
129
108
  @tmp[addr] = value
130
109
  when 0xff04..0xff07
131
110
  @timer.write_byte(addr, value)
132
111
  when 0xff0f
133
- @interrupt.if = value
112
+ @interrupt.write_byte(addr, value)
134
113
  when 0xff10..0xff26
135
114
  # sound
136
115
  @tmp[addr] = value
137
116
  when 0xff30..0xff3f
138
117
  # wave pattern ram
139
118
  @tmp[addr] = value
140
- when 0xff40
141
- @ppu.lcdc = value
142
- when 0xff41
143
- # stat
144
- @tmp[addr] = value
145
- when 0xff42
146
- @ppu.scy = value
147
- when 0xff43
148
- @ppu.scx = value
149
- when 0xff44
150
- @ppu.ly = value
151
- when 0xff45
152
- @ppu.lyc = value
153
119
  when 0xff46
154
- # dma
155
- @tmp[addr] = value
156
- when 0xff47
157
- @ppu.bgp = value
158
- when 0xff48
159
- # obp0
160
- @tmp[addr] = value
161
- when 0xff49
162
- # obp1
163
- @tmp[addr] = value
164
- when 0xff4a
165
- # wy
166
- @tmp[addr] = value
167
- when 0xff4b
168
- # wx
169
- @tmp[addr] = value
120
+ 0xa0.times do |i|
121
+ write_byte(0xfe00 + i, read_byte((value << 8) + i))
122
+ end
123
+ when 0xff40..0xff4b
124
+ @ppu.write_byte(addr, value)
170
125
  when 0xff4f
171
126
  # vbk
172
127
  @tmp[addr] = value
@@ -185,7 +140,7 @@ module Rubyboy
185
140
  when 0xff80..0xfffe
186
141
  @ram.hram[addr - 0xff80] = value
187
142
  when 0xffff
188
- @interrupt.ie = value
143
+ @interrupt.write_byte(addr, value)
189
144
  end
190
145
  end
191
146
 
@@ -12,6 +12,8 @@ module Rubyboy
12
12
  Nombc.new(rom)
13
13
  when 0x01..0x03
14
14
  Mbc1.new(rom, ram)
15
+ when 0x08..0x09
16
+ Nombc.new(rom)
15
17
  else
16
18
  raise "Unsupported cartridge type: #{cartridge_type}"
17
19
  end
@@ -30,10 +30,6 @@ module Rubyboy
30
30
  else
31
31
  0xff
32
32
  end
33
- when 0xc000..0xcfff
34
- @ram.wram1[addr - 0xc000]
35
- when 0xd000..0xdfff
36
- @ram.wram2[addr - 0xd000]
37
33
  else
38
34
  raise "not implemented: read_byte #{addr}"
39
35
  end
@@ -43,11 +39,9 @@ module Rubyboy
43
39
  case addr
44
40
  when 0x0000..0x1fff
45
41
  @ram_enable = value & 0x0f == 0x0a
46
- @rom.data[addr] = value
47
42
  when 0x2000..0x3fff
48
43
  @rom_bank = value & 0x1f
49
44
  @rom_bank = 1 if @rom_bank.zero?
50
- @rom.data[addr] = value
51
45
  when 0x4000..0x5fff
52
46
  @ram_bank = value & 0x03
53
47
  when 0x6000..0x7fff
@@ -60,10 +54,6 @@ module Rubyboy
60
54
  @ram.eram[addr - 0xa000] = value
61
55
  end
62
56
  end
63
- when 0xc000..0xcfff
64
- @ram.wram1[addr - 0xc000] = value
65
- when 0xd000..0xdfff
66
- @ram.wram2[addr - 0xd000] = value
67
57
  end
68
58
  end
69
59
  end
@@ -8,7 +8,12 @@ module Rubyboy
8
8
  end
9
9
 
10
10
  def read_byte(addr)
11
- @rom.data[addr]
11
+ case addr
12
+ when 0x0000..0x7fff
13
+ @rom.data[addr]
14
+ else
15
+ raise "not implemented: read_byte #{addr}"
16
+ end
12
17
  end
13
18
 
14
19
  def write_byte(_addr, _value)
data/lib/rubyboy/cpu.rb CHANGED
@@ -85,12 +85,12 @@ module Rubyboy
85
85
 
86
86
  @bus.interrupt.if &= (~(1 << i)) & 0xff
87
87
  @ime = false
88
- @bus.write_word(@sp - 2, @pc)
89
88
  @sp -= 2
89
+ @bus.write_word(@sp, @pc)
90
90
  @pc = pcs[i]
91
- opcode = read_byte(@pc)
92
91
  break
93
92
  end
93
+ return 20
94
94
  end
95
95
 
96
96
  if @ime_delay
@@ -11,6 +11,24 @@ module Rubyboy
11
11
  @if = 0
12
12
  end
13
13
 
14
+ def read_byte(addr)
15
+ case addr
16
+ when 0xff0f
17
+ @if
18
+ when 0xffff
19
+ @ie
20
+ end
21
+ end
22
+
23
+ def write_byte(addr, value)
24
+ case addr
25
+ when 0xff0f
26
+ @if = value
27
+ when 0xffff
28
+ @ie = value
29
+ end
30
+ end
31
+
14
32
  def interrupts
15
33
  @if & @ie & 0x1f
16
34
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ class Joypad
5
+ def initialize(interupt)
6
+ @mode = 0xcf
7
+ @action = 0xff
8
+ @direction = 0xff
9
+ @interupt = interupt
10
+ end
11
+
12
+ def read_byte(addr)
13
+ raise "not implemented: write_byte #{addr}" unless addr == 0xff00
14
+
15
+ res = @mode | 0xcf
16
+ res &= @direction if @mode[4].zero?
17
+ res &= @action if @mode[5].zero?
18
+
19
+ res
20
+ end
21
+
22
+ def write_byte(addr, value)
23
+ raise "not implemented: write_byte #{addr}" unless addr == 0xff00
24
+
25
+ @mode = value & 0x30
26
+ @mode |= 0xc0
27
+ end
28
+
29
+ def direction_button(button)
30
+ @direction = button | 0xf0
31
+
32
+ @interupt.request(0b0001_0000) if button < 0b1111
33
+ end
34
+
35
+ def action_button(button)
36
+ @action = button | 0xf0
37
+
38
+ @interupt.request(0b0001_0000) if button < 0b1111
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'raylib'
4
+
5
+ module Rubyboy
6
+ class Lcd
7
+ include Raylib
8
+
9
+ WIDTH = 160
10
+ HEIGHT = 144
11
+ SCALE = 4
12
+
13
+ def initialize
14
+ InitWindow(WIDTH * SCALE, HEIGHT * SCALE, 'RUBY BOY')
15
+ image = GenImageColor(WIDTH, HEIGHT, BLACK)
16
+ image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8
17
+ @texture = LoadTextureFromImage(image)
18
+ end
19
+
20
+ def draw(pixel_data)
21
+ UpdateTexture(@texture, pixel_data)
22
+
23
+ BeginDrawing()
24
+ ClearBackground(BLACK)
25
+ DrawTextureEx(@texture, Vector2.create(0, 0), 0.0, SCALE, WHITE)
26
+ EndDrawing()
27
+ end
28
+
29
+ def window_should_close?
30
+ WindowShouldClose()
31
+ end
32
+
33
+ def close_window
34
+ CloseWindow()
35
+ end
36
+ end
37
+ end
data/lib/rubyboy/ppu.rb CHANGED
@@ -2,74 +2,270 @@
2
2
 
3
3
  module Rubyboy
4
4
  class Ppu
5
- attr_accessor :lcdc, :scy, :scx, :ly, :lyc, :bgp, :vram, :cycles
5
+ attr_reader :buffer
6
6
 
7
- def initialize
7
+ MODE = {
8
+ hblank: 0,
9
+ vblank: 1,
10
+ oam_scan: 2,
11
+ drawing: 3
12
+ }.freeze
13
+
14
+ LCD_WIDTH = 160
15
+ LCD_HEIGHT = 144
16
+
17
+ OAM_SCAN_CYCLES = 80
18
+ DRAWING_CYCLES = 172
19
+ HBLANK_CYCLES = 204
20
+ ONE_LINE_CYCLES = OAM_SCAN_CYCLES + DRAWING_CYCLES + HBLANK_CYCLES
21
+
22
+ def initialize(interrupt)
23
+ @mode = MODE[:oam_scan]
8
24
  @lcdc = 0x91
25
+ @stat = 0x00
9
26
  @scy = 0x00
10
27
  @scx = 0x00
11
28
  @ly = 0x00
12
29
  @lyc = 0x00
13
- @bgp = 0xfc
14
- @vram = Array.new(8192, 0x00)
30
+ @obp0 = 0x00
31
+ @obp1 = 0x00
32
+ @wy = 0x00
33
+ @wx = 0x00
34
+ @bgp = 0x00
35
+ @vram = Array.new(0x2000, 0x00)
36
+ @oam = Array.new(0xa0, 0x00)
37
+ @wly = 0x00
15
38
  @cycles = 0
39
+ @interrupt = interrupt
40
+ @buffer = Array.new(144 * 160, 0x00)
41
+ @bg_pixels = Array.new(LCD_WIDTH, 0x00)
42
+ end
43
+
44
+ def read_byte(addr)
45
+ case addr
46
+ when 0x8000..0x9fff
47
+ @mode == MODE[:drawing] ? 0xff : @vram[addr - 0x8000]
48
+ when 0xfe00..0xfe9f
49
+ @mode == MODE[:oam_scan] || @mode == MODE[:drawing] ? 0xff : @oam[addr - 0xfe00]
50
+ when 0xff40
51
+ @lcdc
52
+ when 0xff41
53
+ @stat | 0x80 | @mode
54
+ when 0xff42
55
+ @scy
56
+ when 0xff43
57
+ @scx
58
+ when 0xff44
59
+ @ly
60
+ when 0xff45
61
+ @lyc
62
+ when 0xff47
63
+ @bgp
64
+ when 0xff48
65
+ @obp0
66
+ when 0xff49
67
+ @obp1
68
+ when 0xff4a
69
+ @wy
70
+ when 0xff4b
71
+ @wx
72
+ end
73
+ end
74
+
75
+ def write_byte(addr, value)
76
+ case addr
77
+ when 0x8000..0x9fff
78
+ @vram[addr - 0x8000] = value if @mode != MODE[:drawing]
79
+ when 0xfe00..0xfe9f
80
+ @oam[addr - 0xfe00] = value if @mode != MODE[:oam_scan] && @mode != MODE[:drawing]
81
+ when 0xff40
82
+ @lcdc = value
83
+ when 0xff41
84
+ @stat = value & 0x78
85
+ when 0xff42
86
+ @scy = value
87
+ when 0xff43
88
+ @scx = value
89
+ when 0xff44
90
+ # ly is read only
91
+ when 0xff45
92
+ @lyc = value
93
+ when 0xff46
94
+ # dma
95
+ when 0xff47
96
+ @bgp = value
97
+ when 0xff48
98
+ @obp0 = value
99
+ when 0xff49
100
+ @obp1 = value
101
+ when 0xff4a
102
+ @wy = value
103
+ when 0xff4b
104
+ @wx = value
105
+ end
16
106
  end
17
107
 
18
108
  def step(cycles)
109
+ return false if @lcdc[7].zero?
110
+
111
+ res = false
19
112
  @cycles += cycles
20
- return if @cycles < 456
21
113
 
22
- @cycles -= 456
23
- @ly = (@ly + 1) % 154
114
+ case @mode
115
+ when MODE[:oam_scan]
116
+ if @cycles >= OAM_SCAN_CYCLES
117
+ @cycles -= OAM_SCAN_CYCLES
118
+ @mode = MODE[:drawing]
119
+ end
120
+ when MODE[:drawing]
121
+ if @cycles >= DRAWING_CYCLES
122
+ render_bg
123
+ render_window
124
+ render_sprites
125
+ @cycles -= DRAWING_CYCLES
126
+ @mode = MODE[:hblank]
127
+ @interrupt.request(0b0000_0010) if @stat[3] == 1
128
+ end
129
+ when MODE[:hblank]
130
+ if @cycles >= HBLANK_CYCLES
131
+ @cycles -= HBLANK_CYCLES
132
+ @ly += 1
133
+ handle_ly_eq_lyc
134
+
135
+ if @ly == LCD_HEIGHT
136
+ @mode = MODE[:vblank]
137
+ @interrupt.request(0b0000_0001)
138
+ @interrupt.request(0b0000_0010) if @stat[4] == 1
139
+ else
140
+ @mode = MODE[:oam_scan]
141
+ @interrupt.request(0b0000_0010) if @stat[5] == 1
142
+ end
143
+ end
144
+ when MODE[:vblank]
145
+ if @cycles >= ONE_LINE_CYCLES
146
+ @cycles -= ONE_LINE_CYCLES
147
+ @ly += 1
148
+ handle_ly_eq_lyc
149
+
150
+ if @ly == 154
151
+ @ly = 0
152
+ @wly = 0
153
+ handle_ly_eq_lyc
154
+ @mode = MODE[:oam_scan]
155
+ @interrupt.request(0b0000_0010) if @stat[5] == 1
156
+ res = true
157
+ end
158
+ end
159
+ end
160
+
161
+ res
162
+ end
163
+
164
+ def render_bg
165
+ return if @lcdc[0].zero?
166
+
167
+ y = (@ly + @scy) % 256
168
+ LCD_WIDTH.times do |i|
169
+ x = (i + @scx) % 256
170
+ tile_index = get_tile_index(@lcdc[3], x, y)
171
+ pixel = get_pixel(tile_index, x, y)
172
+ @buffer[@ly * LCD_WIDTH + i] = get_color(@bgp, pixel)
173
+ @bg_pixels[i] = pixel
174
+ end
24
175
  end
25
176
 
26
- def draw_bg
27
- tile_map_addr = @lcdc[3].zero? ? 0x9800 : 0x9c00
28
- tile_data_addr = @lcdc[4].zero? ? 0x9000 : 0x8000
177
+ def render_window
178
+ return if @lcdc[0].zero? || @lcdc[5].zero? || @ly < @wy
29
179
 
30
- bg = Array.new(256) { Array.new(256, 0x00) }
180
+ rendered = false
181
+ y = @wly
182
+ LCD_WIDTH.times do |i|
183
+ next if i < @wx - 7
31
184
 
32
- 32.times do |y|
33
- 32.times do |x|
34
- tile_map_index = (y * 32 + x)
35
- tile_index = @vram[tile_map_addr - 0x8000 + tile_map_index]
36
- # TODO: if @lcdc[4] == 0 then the addr becomes signed
185
+ rendered = true
186
+ x = i - (@wx - 7)
187
+ tile_index = get_tile_index(@lcdc[6], x, y)
188
+ pixel = get_pixel(tile_index, x, y)
189
+ @buffer[@ly * LCD_WIDTH + i] = get_color(@bgp, pixel)
190
+ @bg_pixels[i] = pixel
191
+ end
192
+ @wly += 1 if rendered
193
+ end
37
194
 
38
- tile = @vram[tile_data_addr - 0x8000 + tile_index * 16, 16]
195
+ def render_sprites
196
+ return if @lcdc[1].zero?
39
197
 
40
- 8.times do |i|
41
- pixelsa = tile[i * 2]
42
- pixelsb = tile[i * 2 + 1]
198
+ sprite_height = @lcdc[2].zero? ? 8 : 16
43
199
 
44
- 8.times do |j|
45
- c = (pixelsb[7 - j] << 1) + pixelsa[7 - j]
46
- bg[y * 8 + i][x * 8 + j] = get_color(c)
47
- end
48
- end
49
- end
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
208
+
209
+ sprite
50
210
  end
51
- view_port = Array.new(144) { Array.new(160, 0x00) }
211
+ sprites = sprites.take(10).sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
212
+
213
+ sprites.each do |sprite|
214
+ pallet = sprite[:flags][4].zero? ? @obp0 : @obp1
215
+ tile_index = sprite[:tile_index]
216
+ tile_index &= 0xfe if sprite_height == 16
217
+ y = (@ly - sprite[:y]) % 256
218
+ y = sprite_height - y - 1 if sprite[:flags][6] == 1
219
+ tile_index = (tile_index + 1) % 256 if y >= 8
220
+ y %= 8
221
+
222
+ 8.times do |x|
223
+ x_flipped = sprite[:flags][5] == 1 ? 7 - x : x
224
+
225
+ pixel = get_pixel(tile_index, x_flipped, y)
226
+ i = (sprite[:x] + x) % 256
52
227
 
53
- 144.times do |y|
54
- 160.times do |x|
55
- view_port[y][x] = bg[(y + @scy) % 144][(x + @scx) % 160]
228
+ next if pixel.zero? || i >= LCD_WIDTH
229
+ next if sprite[:flags][7] == 1 && @bg_pixels[i] != 0
230
+
231
+ @buffer[@ly * LCD_WIDTH + i] = get_color(pallet, pixel)
56
232
  end
57
233
  end
58
- view_port
59
234
  end
60
235
 
61
236
  private
62
237
 
63
- def get_color(color_num)
64
- case color_num
65
- when 0
66
- [0xff, 0xff, 0xff, 0xff]
67
- when 1
68
- [0xcc, 0xcc, 0xcc, 0xff]
69
- when 2
70
- [0x77, 0x77, 0x77, 0xff]
71
- when 3
72
- [0x00, 0x00, 0x00, 0xff]
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
243
+ end
244
+
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)
247
+ end
248
+
249
+ def get_color(pallet, pixel)
250
+ case (pallet >> (pixel * 2)) & 0b11
251
+ when 0 then 0xff
252
+ when 1 then 0xaa
253
+ when 2 then 0x55
254
+ when 3 then 0x00
255
+ end
256
+ end
257
+
258
+ def to_signed_byte(byte)
259
+ byte &= 0xff
260
+ byte > 127 ? byte - 256 : byte
261
+ end
262
+
263
+ def handle_ly_eq_lyc
264
+ if @ly == @lyc
265
+ @stat |= 0x04
266
+ @interrupt.request(0b0000_0010) if @stat[6] == 1
267
+ else
268
+ @stat &= 0xfb
73
269
  end
74
270
  end
75
271
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyboy
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/rubyboy.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'gosu'
3
+ require 'raylib'
4
4
  require 'benchmark'
5
5
  require_relative 'rubyboy/version'
6
6
  require_relative 'rubyboy/bus'
@@ -8,52 +8,76 @@ require_relative 'rubyboy/cpu'
8
8
  require_relative 'rubyboy/ppu'
9
9
  require_relative 'rubyboy/rom'
10
10
  require_relative 'rubyboy/timer'
11
+ require_relative 'rubyboy/lcd'
12
+ require_relative 'rubyboy/joypad'
11
13
 
12
14
  module Rubyboy
13
- class Console < Gosu::Window
14
- SCALE = 4
15
+ class Console
16
+ include Raylib
15
17
 
16
18
  def initialize(rom_data)
17
- super(160 * SCALE, 144 * SCALE, false)
18
- # TODO: Display title
19
- self.caption = 'RUBY BOY'
20
- @total_cycles = 0
21
-
19
+ load_raylib
22
20
  rom = Rom.new(rom_data)
23
21
  interrupt = Interrupt.new
24
- @ppu = Ppu.new
22
+ @ppu = Ppu.new(interrupt)
25
23
  @timer = Timer.new(interrupt)
26
- @bus = Bus.new(@ppu, rom, @timer, interrupt)
24
+ @joypad = Joypad.new(interrupt)
25
+ @bus = Bus.new(@ppu, rom, @timer, interrupt, @joypad)
27
26
  @cpu = Cpu.new(@bus)
27
+ @lcd = Lcd.new
28
28
  end
29
29
 
30
- def update
31
- while @total_cycles < 70224
30
+ def start
31
+ until @lcd.window_should_close?
32
+ key_input_check
32
33
  cycles = @cpu.exec
33
- @total_cycles += cycles
34
34
  @timer.step(cycles)
35
- @ppu.step(cycles)
35
+ draw if @ppu.step(cycles)
36
36
  end
37
- @total_cycles -= 70224
37
+ @lcd.close_window
38
38
  rescue StandardError => e
39
39
  p e.to_s[0, 100]
40
40
  raise e
41
41
  end
42
42
 
43
+ private
44
+
43
45
  def draw
44
- pixel_data = @ppu.draw_bg.flatten.pack('C*')
45
- @image = Gosu::Image.from_blob(160, 144, pixel_data)
46
- @image.draw(0, 0, 0, SCALE, SCALE)
46
+ pixel_data = buffer_to_pixel_data(@ppu.buffer)
47
+ @lcd.draw(pixel_data)
48
+ end
49
+
50
+ def buffer_to_pixel_data(buffer)
51
+ buffer.map do |row|
52
+ [row, row, row]
53
+ end.flatten.pack('C*')
47
54
  end
48
55
 
49
- def draw_pixel(x, y, color)
50
- x *= SCALE
51
- y *= SCALE
52
- c = Gosu::Color.new(255, color[0], color[1], color[2])
53
- draw_quad(x, y, c, x + SCALE, y, c, x, y + SCALE, c, x + SCALE, y + SCALE, c)
56
+ def key_input_check
57
+ direction = (IsKeyUp(KEY_D) && 1 || 0) | ((IsKeyUp(KEY_A) && 1 || 0) << 1) | ((IsKeyUp(KEY_W) && 1 || 0) << 2) | ((IsKeyUp(KEY_S) && 1 || 0) << 3)
58
+ action = (IsKeyUp(KEY_K) && 1 || 0) | ((IsKeyUp(KEY_J) && 1 || 0) << 1) | ((IsKeyUp(KEY_U) && 1 || 0) << 2) | ((IsKeyUp(KEY_I) && 1 || 0) << 3)
59
+ @joypad.direction_button(direction)
60
+ @joypad.action_button(action)
61
+ end
62
+
63
+ def load_raylib
64
+ shared_lib_path = "#{Gem::Specification.find_by_name('raylib-bindings').full_gem_path}/lib/"
65
+ case RUBY_PLATFORM
66
+ when /mswin|msys|mingw/ # Windows
67
+ Raylib.load_lib("#{shared_lib_path}libraylib.dll")
68
+ when /darwin/ # macOS
69
+ Raylib.load_lib("#{shared_lib_path}libraylib.dylib")
70
+ when /linux/ # Ubuntu Linux (x86_64 or aarch64)
71
+ arch = RUBY_PLATFORM.split('-')[0]
72
+ Raylib.load_lib(shared_lib_path + "libraylib.#{arch}.so")
73
+ else
74
+ raise "Unknown system: #{RUBY_PLATFORM}"
75
+ end
76
+
77
+ SetTraceLogLevel(LOG_ERROR)
54
78
  end
55
79
  end
56
80
  end
57
81
 
58
- rom_data = File.open(File.expand_path('roms/cpu_instrs/cpu_instrs.gb', __dir__), 'r') { _1.read.bytes }
59
- Rubyboy::Console.new(rom_data).show
82
+ rom_data = File.open(File.expand_path('roms/bgbtest.gb', __dir__), 'r') { _1.read.bytes }
83
+ Rubyboy::Console.new(rom_data).start
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyboy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sacckey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-14 00:00:00.000000000 Z
11
+ date: 2023-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: gosu
14
+ name: raylib-bindings
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.4'
19
+ version: 0.5.7
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.4'
26
+ version: 0.5.7
27
27
  description:
28
28
  email:
29
29
  executables: []
@@ -38,6 +38,7 @@ files:
38
38
  - README.md
39
39
  - Rakefile
40
40
  - lib/opcodes.json
41
+ - lib/roms/bgbtest.gb
41
42
  - lib/roms/cpu_instrs/cpu_instrs.gb
42
43
  - lib/roms/cpu_instrs/individual/01-special.gb
43
44
  - lib/roms/cpu_instrs/individual/02-interrupts.gb
@@ -51,6 +52,7 @@ files:
51
52
  - lib/roms/cpu_instrs/individual/10-bit ops.gb
52
53
  - lib/roms/cpu_instrs/individual/11-op a,(hl).gb
53
54
  - lib/roms/cpu_instrs/readme.txt
55
+ - lib/roms/dmg-acid2.gb
54
56
  - lib/roms/hello-world.gb
55
57
  - lib/roms/instr_timing/instr_timing.gb
56
58
  - lib/roms/instr_timing/readme.txt
@@ -62,6 +64,8 @@ files:
62
64
  - lib/rubyboy/cartridge/nombc.rb
63
65
  - lib/rubyboy/cpu.rb
64
66
  - lib/rubyboy/interrupt.rb
67
+ - lib/rubyboy/joypad.rb
68
+ - lib/rubyboy/lcd.rb
65
69
  - lib/rubyboy/ppu.rb
66
70
  - lib/rubyboy/ram.rb
67
71
  - lib/rubyboy/register.rb