rubyboy 0.3.0 → 1.0.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +7 -15
- data/exe/rubyboy +7 -0
- data/lib/rubyboy/bus.rb +5 -13
- data/lib/rubyboy/cpu.rb +1018 -1503
- data/lib/rubyboy/interrupt.rb +12 -4
- data/lib/rubyboy/joypad.rb +41 -0
- data/lib/rubyboy/lcd.rb +0 -20
- data/lib/rubyboy/operand/direct16.rb +13 -0
- data/lib/rubyboy/operand/direct8.rb +13 -0
- data/lib/rubyboy/operand/hl_dec.rb +11 -0
- data/lib/rubyboy/operand/hl_inc.rb +11 -0
- data/lib/rubyboy/operand/immediate16.rb +13 -0
- data/lib/rubyboy/operand/immediate8.rb +13 -0
- data/lib/rubyboy/operand/indirect.rb +13 -0
- data/lib/rubyboy/operand/register16.rb +13 -0
- data/lib/rubyboy/operand/register8.rb +13 -0
- data/lib/rubyboy/operand.rb +12 -0
- data/lib/rubyboy/ppu.rb +56 -24
- data/lib/rubyboy/register.rb +0 -2
- data/lib/rubyboy/registers.rb +80 -0
- data/lib/rubyboy/rom.rb +1 -30
- data/lib/rubyboy/timer.rb +3 -4
- data/lib/rubyboy/version.rb +1 -1
- data/lib/rubyboy.rb +41 -9
- metadata +17 -3
data/lib/rubyboy/interrupt.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -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(:joypad) if button < 0b1111
|
33
|
+
end
|
34
|
+
|
35
|
+
def action_button(button)
|
36
|
+
@action = button | 0xf0
|
37
|
+
|
38
|
+
@interupt.request(:joypad) if button < 0b1111
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/rubyboy/lcd.rb
CHANGED
@@ -11,7 +11,6 @@ module Rubyboy
|
|
11
11
|
SCALE = 4
|
12
12
|
|
13
13
|
def initialize
|
14
|
-
load_raylib
|
15
14
|
InitWindow(WIDTH * SCALE, HEIGHT * SCALE, 'RUBY BOY')
|
16
15
|
image = GenImageColor(WIDTH, HEIGHT, BLACK)
|
17
16
|
image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8
|
@@ -34,24 +33,5 @@ module Rubyboy
|
|
34
33
|
def close_window
|
35
34
|
CloseWindow()
|
36
35
|
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def load_raylib
|
41
|
-
shared_lib_path = "#{Gem::Specification.find_by_name('raylib-bindings').full_gem_path}/lib/"
|
42
|
-
case RUBY_PLATFORM
|
43
|
-
when /mswin|msys|mingw/ # Windows
|
44
|
-
Raylib.load_lib("#{shared_lib_path}libraylib.dll")
|
45
|
-
when /darwin/ # macOS
|
46
|
-
Raylib.load_lib("#{shared_lib_path}libraylib.dylib")
|
47
|
-
when /linux/ # Ubuntu Linux (x86_64 or aarch64)
|
48
|
-
arch = RUBY_PLATFORM.split('-')[0]
|
49
|
-
Raylib.load_lib(shared_lib_path + "libraylib.#{arch}.so")
|
50
|
-
else
|
51
|
-
raise "Unknown system: #{RUBY_PLATFORM}"
|
52
|
-
end
|
53
|
-
|
54
|
-
SetTraceLogLevel(LOG_ERROR)
|
55
|
-
end
|
56
36
|
end
|
57
37
|
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
|
|
@@ -38,6 +65,7 @@ module Rubyboy
|
|
38
65
|
@cycles = 0
|
39
66
|
@interrupt = interrupt
|
40
67
|
@buffer = Array.new(144 * 160, 0x00)
|
68
|
+
@bg_pixels = Array.new(LCD_WIDTH, 0x00)
|
41
69
|
end
|
42
70
|
|
43
71
|
def read_byte(addr)
|
@@ -89,8 +117,6 @@ module Rubyboy
|
|
89
117
|
# ly is read only
|
90
118
|
when 0xff45
|
91
119
|
@lyc = value
|
92
|
-
when 0xff46
|
93
|
-
# dma
|
94
120
|
when 0xff47
|
95
121
|
@bgp = value
|
96
122
|
when 0xff48
|
@@ -105,7 +131,7 @@ module Rubyboy
|
|
105
131
|
end
|
106
132
|
|
107
133
|
def step(cycles)
|
108
|
-
return false if @lcdc[
|
134
|
+
return false if @lcdc[LCDC[:lcd_ppu_enable]].zero?
|
109
135
|
|
110
136
|
res = false
|
111
137
|
@cycles += cycles
|
@@ -123,7 +149,7 @@ module Rubyboy
|
|
123
149
|
render_sprites
|
124
150
|
@cycles -= DRAWING_CYCLES
|
125
151
|
@mode = MODE[:hblank]
|
126
|
-
@interrupt.request(
|
152
|
+
@interrupt.request(:lcd) if @stat[STAT[:hblank]] == 1
|
127
153
|
end
|
128
154
|
when MODE[:hblank]
|
129
155
|
if @cycles >= HBLANK_CYCLES
|
@@ -133,11 +159,11 @@ module Rubyboy
|
|
133
159
|
|
134
160
|
if @ly == LCD_HEIGHT
|
135
161
|
@mode = MODE[:vblank]
|
136
|
-
@interrupt.request(
|
137
|
-
@interrupt.request(
|
162
|
+
@interrupt.request(:vblank)
|
163
|
+
@interrupt.request(:lcd) if @stat[STAT[:vblank]] == 1
|
138
164
|
else
|
139
165
|
@mode = MODE[:oam_scan]
|
140
|
-
@interrupt.request(
|
166
|
+
@interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
|
141
167
|
end
|
142
168
|
end
|
143
169
|
when MODE[:vblank]
|
@@ -151,7 +177,7 @@ module Rubyboy
|
|
151
177
|
@wly = 0
|
152
178
|
handle_ly_eq_lyc
|
153
179
|
@mode = MODE[:oam_scan]
|
154
|
-
@interrupt.request(
|
180
|
+
@interrupt.request(:lcd) if @stat[STAT[:oam_scan]] == 1
|
155
181
|
res = true
|
156
182
|
end
|
157
183
|
end
|
@@ -161,19 +187,20 @@ module Rubyboy
|
|
161
187
|
end
|
162
188
|
|
163
189
|
def render_bg
|
164
|
-
return if @lcdc[
|
190
|
+
return if @lcdc[LCDC[:bg_window_enable]].zero?
|
165
191
|
|
166
192
|
y = (@ly + @scy) % 256
|
167
193
|
LCD_WIDTH.times do |i|
|
168
194
|
x = (i + @scx) % 256
|
169
|
-
tile_index = get_tile_index(@lcdc[
|
195
|
+
tile_index = get_tile_index(@lcdc[LCDC[:bg_tile_map_area]], x, y)
|
170
196
|
pixel = get_pixel(tile_index, x, y)
|
171
197
|
@buffer[@ly * LCD_WIDTH + i] = get_color(@bgp, pixel)
|
198
|
+
@bg_pixels[i] = pixel
|
172
199
|
end
|
173
200
|
end
|
174
201
|
|
175
202
|
def render_window
|
176
|
-
return if @lcdc[
|
203
|
+
return if @lcdc[LCDC[:bg_window_enable]].zero? || @lcdc[LCDC[:window_enable]].zero? || @ly < @wy
|
177
204
|
|
178
205
|
rendered = false
|
179
206
|
y = @wly
|
@@ -182,19 +209,21 @@ module Rubyboy
|
|
182
209
|
|
183
210
|
rendered = true
|
184
211
|
x = i - (@wx - 7)
|
185
|
-
tile_index = get_tile_index(@lcdc[
|
212
|
+
tile_index = get_tile_index(@lcdc[LCDC[:window_tile_map_area]], x, y)
|
186
213
|
pixel = get_pixel(tile_index, x, y)
|
187
214
|
@buffer[@ly * LCD_WIDTH + i] = get_color(@bgp, pixel)
|
215
|
+
@bg_pixels[i] = pixel
|
188
216
|
end
|
189
217
|
@wly += 1 if rendered
|
190
218
|
end
|
191
219
|
|
192
220
|
def render_sprites
|
193
|
-
return if @lcdc[
|
194
|
-
|
195
|
-
sprite_height = @lcdc[2].zero? ? 8 : 16
|
221
|
+
return if @lcdc[LCDC[:sprite_enable]].zero?
|
196
222
|
|
197
|
-
|
223
|
+
sprite_height = @lcdc[LCDC[:sprite_size]].zero? ? 8 : 16
|
224
|
+
sprites = []
|
225
|
+
cnt = 0
|
226
|
+
@oam.each_slice(4).each do |sprite_attr|
|
198
227
|
sprite = {
|
199
228
|
y: (sprite_attr[0] - 16) % 256,
|
200
229
|
x: (sprite_attr[1] - 8) % 256,
|
@@ -203,27 +232,30 @@ module Rubyboy
|
|
203
232
|
}
|
204
233
|
next if sprite[:y] > @ly || sprite[:y] + sprite_height <= @ly
|
205
234
|
|
206
|
-
sprite
|
235
|
+
sprites << sprite
|
236
|
+
cnt += 1
|
237
|
+
break if cnt == 10
|
207
238
|
end
|
208
|
-
sprites = sprites.
|
239
|
+
sprites = sprites.sort_by.with_index { |sprite, i| [-sprite[:x], -i] }
|
209
240
|
|
210
241
|
sprites.each do |sprite|
|
211
|
-
|
242
|
+
flags = sprite[:flags]
|
243
|
+
pallet = flags[SPRITE_FLAGS[:dmg_palette]].zero? ? @obp0 : @obp1
|
212
244
|
tile_index = sprite[:tile_index]
|
213
245
|
tile_index &= 0xfe if sprite_height == 16
|
214
246
|
y = (@ly - sprite[:y]) % 256
|
215
|
-
y = sprite_height - y - 1 if
|
247
|
+
y = sprite_height - y - 1 if flags[SPRITE_FLAGS[:y_flip]] == 1
|
216
248
|
tile_index = (tile_index + 1) % 256 if y >= 8
|
217
249
|
y %= 8
|
218
250
|
|
219
251
|
8.times do |x|
|
220
|
-
x_flipped =
|
252
|
+
x_flipped = flags[SPRITE_FLAGS[:x_flip]] == 1 ? 7 - x : x
|
221
253
|
|
222
254
|
pixel = get_pixel(tile_index, x_flipped, y)
|
223
255
|
i = (sprite[:x] + x) % 256
|
224
256
|
|
225
257
|
next if pixel.zero? || i >= LCD_WIDTH
|
226
|
-
next if
|
258
|
+
next if flags[SPRITE_FLAGS[:priority]] == 1 && @bg_pixels[i] != 0
|
227
259
|
|
228
260
|
@buffer[@ly * LCD_WIDTH + i] = get_color(pallet, pixel)
|
229
261
|
end
|
@@ -236,7 +268,7 @@ module Rubyboy
|
|
236
268
|
tile_map_addr = tile_map_area.zero? ? 0x1800 : 0x1c00
|
237
269
|
tile_map_index = (y / 8) * 32 + (x / 8)
|
238
270
|
tile_index = @vram[tile_map_addr + tile_map_index]
|
239
|
-
@lcdc[
|
271
|
+
@lcdc[LCDC[:bg_window_tile_data_area]].zero? ? to_signed_byte(tile_index) + 256 : tile_index
|
240
272
|
end
|
241
273
|
|
242
274
|
def get_pixel(tile_index, x, y)
|
@@ -260,7 +292,7 @@ module Rubyboy
|
|
260
292
|
def handle_ly_eq_lyc
|
261
293
|
if @ly == @lyc
|
262
294
|
@stat |= 0x04
|
263
|
-
@interrupt.request(
|
295
|
+
@interrupt.request(:lcd) if @stat[STAT[:lyc]] == 1
|
264
296
|
else
|
265
297
|
@stat &= 0xfb
|
266
298
|
end
|
data/lib/rubyboy/register.rb
CHANGED
@@ -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
|
-
|
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)
|
@@ -35,8 +33,8 @@ module Rubyboy
|
|
35
33
|
|
36
34
|
return if @tima < 256
|
37
35
|
|
38
|
-
@tima
|
39
|
-
@interrupt.request(
|
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
|
data/lib/rubyboy/version.rb
CHANGED