GBRb 0.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -0
- data/Rakefile +6 -0
- data/bin/display +9 -0
- data/bin/gbrb +12 -0
- data/gbrb.gemspec +26 -0
- data/lib/gbrb.rb +9 -0
- data/lib/gbrb/bios +256 -0
- data/lib/gbrb/cartridge.rb +15 -0
- data/lib/gbrb/cpu.rb +7 -0
- data/lib/gbrb/cpu/concatenated_register.rb +44 -0
- data/lib/gbrb/cpu/flags_register.rb +42 -0
- data/lib/gbrb/cpu/instruction.rb +648 -0
- data/lib/gbrb/cpu/register.rb +44 -0
- data/lib/gbrb/cpu/register_ensemble.rb +59 -0
- data/lib/gbrb/cpu/z80.rb +584 -0
- data/lib/gbrb/gb.rb +125 -0
- data/lib/gbrb/graphics.rb +14 -0
- data/lib/gbrb/graphics/gpu.rb +196 -0
- data/lib/gbrb/graphics/mode_clock.rb +51 -0
- data/lib/gbrb/graphics/screen_client.rb +31 -0
- data/lib/gbrb/graphics/screen_server.rb +90 -0
- data/lib/gbrb/mmu.rb +96 -0
- data/lib/gbrb/version.rb +3 -0
- data/misc/parse_tiles +27 -0
- data/perf/cpu_perf_spec.rb +23 -0
- data/spec/gbrb/cartridge_spec.rb +19 -0
- data/spec/gbrb/cpu/concatenated_register_spec.rb +36 -0
- data/spec/gbrb/cpu/flags_register_spec.rb +91 -0
- data/spec/gbrb/cpu/instruction_spec.rb +283 -0
- data/spec/gbrb/cpu/register_ensemble_spec.rb +84 -0
- data/spec/gbrb/cpu/register_spec.rb +86 -0
- data/spec/gbrb/cpu/z80_spec.rb +2534 -0
- data/spec/gbrb/graphics/mode_clock_spec.rb +82 -0
- data/spec/gbrb/mmu_spec.rb +61 -0
- data/spec/gbrb/version_spec.rb +10 -0
- data/spec/spec_helper.rb +4 -0
- metadata +154 -0
data/lib/gbrb/gb.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require_relative 'mmu'
|
2
|
+
require_relative 'cpu/z80'
|
3
|
+
require_relative 'graphics/gpu'
|
4
|
+
require_relative 'cartridge'
|
5
|
+
|
6
|
+
require 'thread'
|
7
|
+
|
8
|
+
module GBRb
|
9
|
+
class GB
|
10
|
+
def initialize cartridge_path='', debug=false
|
11
|
+
@debug = debug
|
12
|
+
|
13
|
+
@gpu_send = Queue.new
|
14
|
+
@gpu_receive = Queue.new
|
15
|
+
|
16
|
+
@mmu = GBRb::MMU.new read_bios, create_cartridge(cartridge_path), @gpu_send
|
17
|
+
@cpu = GBRb::CPU::Z80.new @mmu
|
18
|
+
@gpu = GBRb::Graphics::GPU.new @mmu, @gpu_send, @gpu_receive
|
19
|
+
end
|
20
|
+
|
21
|
+
def power_on
|
22
|
+
@power = :on
|
23
|
+
@gpu.power_on
|
24
|
+
@mode = @debug ? :break : :default
|
25
|
+
@breakpoint = nil
|
26
|
+
|
27
|
+
puts " pc | sp | a | b | c | d | e | h | l | f | CB | OP Code |" unless @mode == :default
|
28
|
+
|
29
|
+
loop do
|
30
|
+
if @mode == :break
|
31
|
+
new_mode = nil
|
32
|
+
until new_mode
|
33
|
+
new_mode = debug_input
|
34
|
+
end
|
35
|
+
@mode = new_mode
|
36
|
+
end
|
37
|
+
|
38
|
+
pc = @cpu.r.pc.read
|
39
|
+
op_code = @mmu.read_byte(pc).to_i
|
40
|
+
prefix = @cpu.instance_eval '@cb_prefix'
|
41
|
+
|
42
|
+
begin
|
43
|
+
@gpu_send.push [:cpu, @cpu.step]
|
44
|
+
rescue GBRb::CPU::InstructionNotImplemented => e
|
45
|
+
display_status prefix, op_code
|
46
|
+
@gpu.power_off
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
@gpu_receive.pop
|
51
|
+
if @debug
|
52
|
+
display_status prefix, op_code
|
53
|
+
if @step
|
54
|
+
@step -= 1
|
55
|
+
if @step == 0
|
56
|
+
@breakpoint = @cpu.r.pc.read
|
57
|
+
@step = nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
if @cpu.r.pc.read == @breakpoint
|
61
|
+
@breakpoint = nil
|
62
|
+
@mode = :break
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def debug_help
|
70
|
+
"Available modes: (b)reakpoint (h)elp (q)uit (s)tep (t)iles"
|
71
|
+
end
|
72
|
+
|
73
|
+
def debug_input
|
74
|
+
input = STDIN.gets
|
75
|
+
case input.chars.first
|
76
|
+
when 'b'
|
77
|
+
@breakpoint = input[1..-1].to_i 16
|
78
|
+
:debug
|
79
|
+
when 'h'
|
80
|
+
STDERR.puts debug_help
|
81
|
+
when 'm'
|
82
|
+
range = input[1..-1].split.map{|el| el.to_i 16}.map{|el| el/0x10*0x10}
|
83
|
+
if range.length == 2
|
84
|
+
STDERR.puts @mmu.dump(range.first, range.last)
|
85
|
+
else
|
86
|
+
STDERR.puts @mmu.dump
|
87
|
+
end
|
88
|
+
when 'q'
|
89
|
+
@gpu.power_off
|
90
|
+
exit
|
91
|
+
when 's'
|
92
|
+
@step = input[1..-1].to_i
|
93
|
+
@step = 1 if @step == 0
|
94
|
+
:debug
|
95
|
+
when 't'
|
96
|
+
@gpu.dump_tiles
|
97
|
+
nil
|
98
|
+
else
|
99
|
+
@step = 1
|
100
|
+
:debug
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def display_status prefix, opcode
|
105
|
+
@cpu.r.each {|register| STDOUT.write ("0x"+register.read.to_s(16)).center(8) + "|" }
|
106
|
+
STDOUT.write "#{prefix ? ' ✓ ' : ' ' }|#{opcode.to_s(16).center(9)}|"
|
107
|
+
STDOUT.write "\n"
|
108
|
+
end
|
109
|
+
|
110
|
+
def read_bios
|
111
|
+
File.read(File.expand_path('../bios', __FILE__)).split.map{ |el| el.to_i(16) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_cartridge path
|
115
|
+
puts path
|
116
|
+
f = File.open(path, 'r')
|
117
|
+
c = Cartridge.new f
|
118
|
+
f.close
|
119
|
+
c
|
120
|
+
rescue Errno::ENOENT
|
121
|
+
raise InvalidCartridge, path
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require_relative '../graphics'
|
2
|
+
require_relative 'mode_clock'
|
3
|
+
require_relative 'screen_client'
|
4
|
+
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
module GBRb::Graphics
|
8
|
+
class GPU
|
9
|
+
include ModeClock
|
10
|
+
|
11
|
+
CONTROL_REGISTER = 0xff40
|
12
|
+
SCROLL_Y_ADDRESS = 0xff42
|
13
|
+
SCROLL_X_ADDRESS = 0xff43
|
14
|
+
LINE_ADDRESS = 0xff44
|
15
|
+
PALETTE_REGISTER = 0xff47
|
16
|
+
TILE_MAP_0 = 0x9800
|
17
|
+
TILE_MAP_1 = 0x9c00
|
18
|
+
TILE_SET_START = 0x8000
|
19
|
+
|
20
|
+
def initialize memory, incoming, outgoing
|
21
|
+
@memory = memory
|
22
|
+
@incoming = incoming
|
23
|
+
@outgoing = outgoing
|
24
|
+
reset
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset
|
28
|
+
super
|
29
|
+
create_frame_buffer
|
30
|
+
create_tiles
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_tiles
|
34
|
+
@tiles = []
|
35
|
+
0x0200.times do
|
36
|
+
a = []
|
37
|
+
8.times do
|
38
|
+
a << Array.new(8) {0}
|
39
|
+
end
|
40
|
+
@tiles << a
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_tile address
|
45
|
+
tile = ((address & 0x1ffe) >> 4) & 0x1ff
|
46
|
+
y = ((address & 0x1ffe) >> 1) & 0x07
|
47
|
+
8.times do |x|
|
48
|
+
x_mask = 1 << (7-x)
|
49
|
+
@tiles[tile][y][x] = ((@memory.read_byte(address) & x_mask) == x_mask ? 1 : 0) + ((@memory.read_byte(address+1) & x_mask) == x_mask ? 2 : 0)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_frame_buffer
|
54
|
+
@frame_buffer = Array.new(HEIGHT) { Array.new(WIDTH) {0} }
|
55
|
+
end
|
56
|
+
|
57
|
+
def power_on
|
58
|
+
trap(:CHLD) { exit }
|
59
|
+
@screen_pid = fork { start_screen }
|
60
|
+
|
61
|
+
Thread.abort_on_exception = true
|
62
|
+
Thread.new do
|
63
|
+
loop do
|
64
|
+
message = @incoming.pop
|
65
|
+
case message.first
|
66
|
+
when :cpu
|
67
|
+
self.update! message.last.to_i if display_on?
|
68
|
+
@outgoing.push :nothing
|
69
|
+
when :mmu
|
70
|
+
self.update_tile message.last.to_i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def power_off
|
77
|
+
Process.kill :HUP, @screen_pid
|
78
|
+
Process.wait @screen_pid
|
79
|
+
end
|
80
|
+
|
81
|
+
def render_scan_line
|
82
|
+
screen_y_adjusted = line + scroll_y
|
83
|
+
initial_tilemap_offset = current_tile_map_address + ((screen_y_adjusted & 0xff)/8) * 32
|
84
|
+
initial_line_offset = (scroll_x >> 3) %32
|
85
|
+
initial_tile_x = scroll_x & 7
|
86
|
+
initial_tile_y = screen_y_adjusted & 7
|
87
|
+
draw_scan_line initial_tilemap_offset, initial_line_offset, initial_tile_x, initial_tile_y
|
88
|
+
end
|
89
|
+
|
90
|
+
def draw_scan_line tilemap_offset, horizontal_offset, tile_x, tile_y
|
91
|
+
tile_index = calculate_tile_number tilemap_offset, horizontal_offset
|
92
|
+
|
93
|
+
row = []
|
94
|
+
WIDTH.times do
|
95
|
+
palette_index = @tiles[tile_index][tile_y][tile_x]
|
96
|
+
color = palette[palette_index]
|
97
|
+
row << color
|
98
|
+
tile_x += 1
|
99
|
+
if tile_x == 0x08
|
100
|
+
tile_x = 0x00
|
101
|
+
horizontal_offset = (horizontal_offset + 1) % 32
|
102
|
+
tile_index = calculate_tile_number tilemap_offset, horizontal_offset
|
103
|
+
end
|
104
|
+
end
|
105
|
+
@frame_buffer[line] = row
|
106
|
+
end
|
107
|
+
|
108
|
+
def calculate_tile_number tilemap_offset, line_offset
|
109
|
+
id = @memory.read_byte(tilemap_offset + line_offset)
|
110
|
+
id += 256 if background_tile_set == 0x10 and id < 128
|
111
|
+
id
|
112
|
+
end
|
113
|
+
|
114
|
+
def palette
|
115
|
+
value = @memory.read_byte PALETTE_REGISTER
|
116
|
+
colors = []
|
117
|
+
4.times do |i|
|
118
|
+
mask = 0b11 << 2*i
|
119
|
+
colors << (((value & mask) >> 2*i) ^ 0b11)
|
120
|
+
end
|
121
|
+
colors
|
122
|
+
end
|
123
|
+
|
124
|
+
def dump_tiles
|
125
|
+
puts @tiles.inspect
|
126
|
+
end
|
127
|
+
|
128
|
+
def draw_frame_buffer
|
129
|
+
screen.write @frame_buffer if display_on?
|
130
|
+
end
|
131
|
+
|
132
|
+
def line
|
133
|
+
@memory.read_byte LINE_ADDRESS
|
134
|
+
end
|
135
|
+
|
136
|
+
def line= value
|
137
|
+
@memory.write_byte LINE_ADDRESS, value
|
138
|
+
end
|
139
|
+
|
140
|
+
def scroll_y
|
141
|
+
@memory.read_byte SCROLL_Y_ADDRESS
|
142
|
+
end
|
143
|
+
|
144
|
+
def scroll_x
|
145
|
+
@memory.read_byte SCROLL_X_ADDRESS
|
146
|
+
end
|
147
|
+
|
148
|
+
def control_register
|
149
|
+
@memory.read_byte CONTROL_REGISTER
|
150
|
+
end
|
151
|
+
|
152
|
+
def background_on?
|
153
|
+
control_register & 0x01 == 0x01
|
154
|
+
end
|
155
|
+
|
156
|
+
def sprites_on?
|
157
|
+
control_register & 0x02 == 0x02
|
158
|
+
end
|
159
|
+
|
160
|
+
def sprite_size
|
161
|
+
control_register & 0x04 == 0x04 ? [8,16] : [8, 8]
|
162
|
+
end
|
163
|
+
|
164
|
+
def background_tile_map
|
165
|
+
control_register & 0x08
|
166
|
+
end
|
167
|
+
|
168
|
+
def current_tile_map_address
|
169
|
+
background_tile_map == 0x08 ? TILE_MAP_1 : TILE_MAP_0
|
170
|
+
end
|
171
|
+
|
172
|
+
def background_tile_set
|
173
|
+
control_register & 0x10 == 0x10
|
174
|
+
end
|
175
|
+
|
176
|
+
def window_on?
|
177
|
+
control_register & 0x20 == 0x20
|
178
|
+
end
|
179
|
+
|
180
|
+
def window_tile_map
|
181
|
+
control_register & 0x40 == 0x40
|
182
|
+
end
|
183
|
+
|
184
|
+
def display_on?
|
185
|
+
control_register & 0x80 == 0x80
|
186
|
+
end
|
187
|
+
|
188
|
+
def screen
|
189
|
+
@screen ||= GBRb::Graphics::ScreenClient.new
|
190
|
+
end
|
191
|
+
|
192
|
+
def start_screen
|
193
|
+
exec 'bin/display'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative '../graphics'
|
2
|
+
|
3
|
+
module GBRb::Graphics
|
4
|
+
module ModeClock
|
5
|
+
attr_reader :mode
|
6
|
+
|
7
|
+
def reset
|
8
|
+
@clock = 0
|
9
|
+
@mode = 2
|
10
|
+
self.line = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def update! time_increment
|
14
|
+
@clock += time_increment
|
15
|
+
|
16
|
+
case mode
|
17
|
+
when 2
|
18
|
+
if @clock >= 80
|
19
|
+
@clock = 0
|
20
|
+
@mode = 3
|
21
|
+
end
|
22
|
+
when 3
|
23
|
+
if @clock >= 172
|
24
|
+
@clock = 0
|
25
|
+
@mode = 0
|
26
|
+
render_scan_line
|
27
|
+
end
|
28
|
+
when 0
|
29
|
+
if @clock >= 204
|
30
|
+
@clock = 0
|
31
|
+
self.line = line.to_i + 1
|
32
|
+
if line == 144
|
33
|
+
@mode = 1
|
34
|
+
draw_frame_buffer
|
35
|
+
else
|
36
|
+
@mode = 2
|
37
|
+
end
|
38
|
+
end
|
39
|
+
when 1
|
40
|
+
if @clock >= 456
|
41
|
+
self.line = line + 1
|
42
|
+
@clock = 0
|
43
|
+
if line > 153
|
44
|
+
self.line = 0
|
45
|
+
@mode = 2
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative '../graphics'
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module GBRb::Graphics
|
6
|
+
class ScreenClient
|
7
|
+
def initialize
|
8
|
+
trap(:PIPE) { exit }
|
9
|
+
@connection = UNIXSocket.new SOCKET_PATH
|
10
|
+
rescue Errno::ECONNREFUSED
|
11
|
+
sleep 0.1
|
12
|
+
retry
|
13
|
+
end
|
14
|
+
|
15
|
+
def write data
|
16
|
+
@connection.write format data
|
17
|
+
end
|
18
|
+
|
19
|
+
def format data
|
20
|
+
raise WrongPixelCount unless data.length == HEIGHT and data.first.length == WIDTH
|
21
|
+
|
22
|
+
d = "P6 #{WIDTH} #{HEIGHT} #{MAX_COLOR} "
|
23
|
+
data.each do |row|
|
24
|
+
row.each do |pixel|
|
25
|
+
d << pixel.chr * 3
|
26
|
+
end
|
27
|
+
end
|
28
|
+
d << "\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require_relative '../graphics'
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module GBRb::Graphics
|
6
|
+
class ScreenServer
|
7
|
+
def initialize
|
8
|
+
@consumer, @producer = IO.pipe
|
9
|
+
create_server
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_server
|
13
|
+
File.unlink SOCKET_PATH if File.exist? SOCKET_PATH
|
14
|
+
@server = UNIXServer.new SOCKET_PATH
|
15
|
+
end
|
16
|
+
|
17
|
+
def incoming_handler
|
18
|
+
@consumer.close
|
19
|
+
trap(:PIPE) { exit }
|
20
|
+
trap(:CHLD) { exit }
|
21
|
+
trap(:HUP) { kill_window }
|
22
|
+
Socket.accept_loop(@server) do |connection|
|
23
|
+
loop do
|
24
|
+
request = connection.gets
|
25
|
+
@producer.write request if request
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def tk_process
|
31
|
+
@producer.close
|
32
|
+
|
33
|
+
require 'tk'
|
34
|
+
require 'thread'
|
35
|
+
|
36
|
+
Thread.abort_on_exception = true
|
37
|
+
|
38
|
+
Window.new @consumer
|
39
|
+
end
|
40
|
+
|
41
|
+
def kill_window
|
42
|
+
Process.kill :HUP, @window_pid
|
43
|
+
Process.wait @window_pid
|
44
|
+
end
|
45
|
+
|
46
|
+
def power_on
|
47
|
+
@window_pid = fork { tk_process }
|
48
|
+
incoming_handler
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
class Window
|
53
|
+
def initialize incoming
|
54
|
+
tkroot = TkRoot.new do
|
55
|
+
title "GBRb"
|
56
|
+
resizable false, false
|
57
|
+
end
|
58
|
+
|
59
|
+
display = Display.new tkroot
|
60
|
+
|
61
|
+
Thread.new do
|
62
|
+
loop do
|
63
|
+
data = incoming.gets
|
64
|
+
begin
|
65
|
+
display.update_pixels data if data
|
66
|
+
rescue RuntimeError => e
|
67
|
+
next
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
tkroot.mainloop
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Display
|
77
|
+
def initialize root
|
78
|
+
@root = root
|
79
|
+
@display = TkPhotoImage.new
|
80
|
+
@label = TkLabel.new @root, image: @display
|
81
|
+
@label.pack
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_pixels formatted_data
|
85
|
+
@label.configure(image: (TkPhotoImage.new data: formatted_data))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|