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.
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,14 @@
1
+ module GBRb
2
+ module Graphics
3
+ WIDTH=160
4
+ HEIGHT=144
5
+
6
+ NUMBER_OF_COLORS = 4
7
+ MAX_COLOR = NUMBER_OF_COLORS - 1
8
+
9
+ SOCKET_PATH= '/tmp/gbrb.sock'
10
+
11
+ class WrongPixelCount < StandardError; end
12
+ end
13
+ end
14
+
@@ -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
+