rnes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +28 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +59 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/rnes +10 -0
- data/lib/rnes.rb +22 -0
- data/lib/rnes/cpu.rb +1058 -0
- data/lib/rnes/cpu_bus.rb +105 -0
- data/lib/rnes/cpu_registers.rb +151 -0
- data/lib/rnes/dma_controller.rb +35 -0
- data/lib/rnes/emulator.rb +61 -0
- data/lib/rnes/errors.rb +46 -0
- data/lib/rnes/image.rb +32 -0
- data/lib/rnes/ines_header.rb +89 -0
- data/lib/rnes/interrupt_line.rb +30 -0
- data/lib/rnes/keypad.rb +51 -0
- data/lib/rnes/logger.rb +142 -0
- data/lib/rnes/operation.rb +52 -0
- data/lib/rnes/operation/records.rb +1477 -0
- data/lib/rnes/parts_factory.rb +99 -0
- data/lib/rnes/ppu.rb +383 -0
- data/lib/rnes/ppu/colors.rb +70 -0
- data/lib/rnes/ppu_bus.rb +60 -0
- data/lib/rnes/ppu_registers.rb +111 -0
- data/lib/rnes/ram.rb +23 -0
- data/lib/rnes/rom.rb +19 -0
- data/lib/rnes/rom_loader.rb +97 -0
- data/lib/rnes/terminal_renderer.rb +59 -0
- data/lib/rnes/version.rb +3 -0
- data/rnes.gemspec +31 -0
- metadata +153 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'rnes/cpu_bus'
|
2
|
+
require 'rnes/cpu'
|
3
|
+
require 'rnes/dma_controller'
|
4
|
+
require 'rnes/interrupt_line'
|
5
|
+
require 'rnes/keypad'
|
6
|
+
require 'rnes/ppu'
|
7
|
+
require 'rnes/ppu_bus'
|
8
|
+
require 'rnes/ram'
|
9
|
+
require 'rnes/rom'
|
10
|
+
require 'rnes/terminal_renderer'
|
11
|
+
|
12
|
+
module Rnes
|
13
|
+
class PartsFactory
|
14
|
+
CHARACTER_RAM_BYTESIZE = 2**12
|
15
|
+
|
16
|
+
VIDEO_RAM_BYTESIZE = 2**13
|
17
|
+
|
18
|
+
WORKING_RAM_BYTESIZE = 2**11
|
19
|
+
|
20
|
+
# @return [Rnes::Ram]
|
21
|
+
def character_ram
|
22
|
+
@character_ram ||= ::Rnes::Ram.new(bytesize: CHARACTER_RAM_BYTESIZE)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Rnes::Cpu]
|
26
|
+
def cpu
|
27
|
+
@cpu ||= ::Rnes::Cpu.new(
|
28
|
+
bus: cpu_bus,
|
29
|
+
interrupt_line: interrupt_line,
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Rnes::CpuBus]
|
34
|
+
def cpu_bus
|
35
|
+
@cpu_bus ||= ::Rnes::CpuBus.new(
|
36
|
+
dma_controller: dma_controller,
|
37
|
+
keypad1: keypad1,
|
38
|
+
keypad2: keypad2,
|
39
|
+
ppu: ppu,
|
40
|
+
ram: working_ram,
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Rnes::DmaController]
|
45
|
+
def dma_controller
|
46
|
+
@dma_controller ||= ::Rnes::DmaController.new(
|
47
|
+
ppu: ppu,
|
48
|
+
working_ram: working_ram,
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Rnes::InterruptLine]
|
53
|
+
def interrupt_line
|
54
|
+
@interrupt_line ||= ::Rnes::InterruptLine.new
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Rnes::Keypad]
|
58
|
+
def keypad1
|
59
|
+
@keypad1 ||= ::Rnes::Keypad.new
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Rnes::Keypad]
|
63
|
+
def keypad2
|
64
|
+
@keypad2 ||= ::Rnes::Keypad.new
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Rnes::Ppu]
|
68
|
+
def ppu
|
69
|
+
@ppu ||= ::Rnes::Ppu.new(
|
70
|
+
bus: ppu_bus,
|
71
|
+
interrupt_line: interrupt_line,
|
72
|
+
renderer: renderer,
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Rnes::PpuBus]
|
77
|
+
def ppu_bus
|
78
|
+
@ppu_bus ||= ::Rnes::PpuBus.new(
|
79
|
+
character_ram: character_ram,
|
80
|
+
video_ram: video_ram,
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Rnes::TerminalRenderer]
|
85
|
+
def renderer
|
86
|
+
@renderer ||= ::Rnes::TerminalRenderer.new
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Rnes::Ram]
|
90
|
+
def video_ram
|
91
|
+
@video_ram ||= ::Rnes::Ram.new(bytesize: VIDEO_RAM_BYTESIZE)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [Rnes::Ram]
|
95
|
+
def working_ram
|
96
|
+
@working_ram ||= ::Rnes::Ram.new(bytesize: WORKING_RAM_BYTESIZE)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/rnes/ppu.rb
ADDED
@@ -0,0 +1,383 @@
|
|
1
|
+
require 'rnes/errors'
|
2
|
+
require 'rnes/image'
|
3
|
+
require 'rnes/ppu_registers'
|
4
|
+
require 'rnes/ppu/colors'
|
5
|
+
require 'rnes/ram'
|
6
|
+
|
7
|
+
module Rnes
|
8
|
+
class Ppu
|
9
|
+
ADDRESS_TO_START_ATTRIBUTE_TABLE = 0x23C0
|
10
|
+
|
11
|
+
ADDRESS_TO_START_NAME_TABLE = 0x2000
|
12
|
+
|
13
|
+
ADDRESS_TO_START_BACKGROUND_PALETTE_TABLE = 0x3F00
|
14
|
+
|
15
|
+
ADDRESS_TO_START_SPRITE_PALETTE_TABLE = 0x3F10
|
16
|
+
|
17
|
+
BLOCK_HEIGHT = 16
|
18
|
+
|
19
|
+
BLOCK_WIDTH = 16
|
20
|
+
|
21
|
+
CYCLES_PER_LINE = 341
|
22
|
+
|
23
|
+
SPRITE_RAM_BYTESIZE = 2**8
|
24
|
+
|
25
|
+
SPRITES_COUNT = 64
|
26
|
+
|
27
|
+
TILE_HEIGHT = 8
|
28
|
+
|
29
|
+
TILE_WIDTH = 8
|
30
|
+
|
31
|
+
V_BLANK_HEIGHT = 21
|
32
|
+
|
33
|
+
VISIBLE_WINDOW_HEIGHT = 240
|
34
|
+
|
35
|
+
VISIBLE_WINDOW_WIDTH = 256
|
36
|
+
|
37
|
+
# @note For debug use.
|
38
|
+
# @param [Integer]
|
39
|
+
# @return [Integer]
|
40
|
+
attr_accessor :cycle
|
41
|
+
|
42
|
+
# @note For debug use.
|
43
|
+
# @param [Integer]
|
44
|
+
# @return [Integer]
|
45
|
+
attr_accessor :line
|
46
|
+
|
47
|
+
# @note For debug use.
|
48
|
+
# @return [Array<Rnes::Image>]
|
49
|
+
attr_reader :image
|
50
|
+
|
51
|
+
# @note For debug use.
|
52
|
+
# @return [Rnes::PpuRegisters]
|
53
|
+
attr_reader :registers
|
54
|
+
|
55
|
+
# @param [Rnes::PpuBus] bus
|
56
|
+
# @param [Rnes::InterruptLine] interrupt_line
|
57
|
+
# @param [Rnes::TerminalRenderer] renderer
|
58
|
+
def initialize(bus:, interrupt_line:, renderer:)
|
59
|
+
@bus = bus
|
60
|
+
@cycle = 0
|
61
|
+
@image = ::Rnes::Image.new(height: VISIBLE_WINDOW_HEIGHT, width: VISIBLE_WINDOW_WIDTH)
|
62
|
+
@interrupt_line = interrupt_line
|
63
|
+
@line = 0
|
64
|
+
@registers = ::Rnes::PpuRegisters.new
|
65
|
+
@renderer = renderer
|
66
|
+
@sprite_ram = ::Rnes::Ram.new(bytesize: SPRITE_RAM_BYTESIZE)
|
67
|
+
@sprite_ram_address = 0x00
|
68
|
+
@video_ram_address = 0x0000
|
69
|
+
@writing_to_scroll_registers = false
|
70
|
+
@writing_video_ram_address = false
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param [Integer] address
|
74
|
+
# @return [Integer]
|
75
|
+
def read(address)
|
76
|
+
case address
|
77
|
+
when 0x0002
|
78
|
+
registers.status
|
79
|
+
when 0x0007
|
80
|
+
read_from_video_ram
|
81
|
+
else
|
82
|
+
raise ::Rnes::Errors::InvalidPpuAddressError, address
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def tick
|
87
|
+
if on_visible_cycle? && x_in_tile.zero?
|
88
|
+
draw_background_8pixels
|
89
|
+
end
|
90
|
+
if on_right_end_cycle?
|
91
|
+
self.cycle = 0
|
92
|
+
if on_bottom_end_line?
|
93
|
+
self.line = 0
|
94
|
+
deassert_nmi
|
95
|
+
clear_sprite_hit
|
96
|
+
clear_v_blank
|
97
|
+
draw_sprites
|
98
|
+
render_image
|
99
|
+
else
|
100
|
+
self.line += 1
|
101
|
+
if on_line_to_start_v_blank?
|
102
|
+
set_v_blank
|
103
|
+
if v_blank_interrupt_enabled?
|
104
|
+
assert_nmi
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
else
|
109
|
+
self.cycle += 1
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [Integer] index
|
114
|
+
# @param [Integer] value
|
115
|
+
def transfer_sprite_data(index:, value:)
|
116
|
+
address = (@sprite_ram_address + index) % SPRITE_RAM_BYTESIZE
|
117
|
+
@sprite_ram.write(address, value)
|
118
|
+
end
|
119
|
+
|
120
|
+
# @param [Integer] address
|
121
|
+
# @param [Integer] value
|
122
|
+
# @return [Integer]
|
123
|
+
def write(address, value)
|
124
|
+
case address
|
125
|
+
when 0x0000
|
126
|
+
registers.control1 = value
|
127
|
+
when 0x0001
|
128
|
+
registers.control2 = value
|
129
|
+
when 0x0003
|
130
|
+
write_sprite_ram_address(value)
|
131
|
+
when 0x0004
|
132
|
+
write_to_sprite_ram(value)
|
133
|
+
when 0x0005
|
134
|
+
write_to_scroll_registers(value)
|
135
|
+
when 0x0006
|
136
|
+
write_video_ram_address(value)
|
137
|
+
when 0x0007
|
138
|
+
write_to_video_ram(value)
|
139
|
+
else
|
140
|
+
raise ::Rnes::Errors::InvalidPpuAddressError, address
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def assert_nmi
|
147
|
+
@interrupt_line.assert_nmi
|
148
|
+
end
|
149
|
+
|
150
|
+
def clear_sprite_hit
|
151
|
+
registers.toggle_sprite_hit_bit(false)
|
152
|
+
end
|
153
|
+
|
154
|
+
def clear_v_blank
|
155
|
+
registers.toggle_in_v_blank_bit(false)
|
156
|
+
end
|
157
|
+
|
158
|
+
def deassert_nmi
|
159
|
+
@interrupt_line.deassert_nmi
|
160
|
+
end
|
161
|
+
|
162
|
+
def draw_background_8pixels
|
163
|
+
character_address_offset = registers.has_background_bank_bit? ? 0x1000 : 0
|
164
|
+
character_index = read_from_name_table(tile_index)
|
165
|
+
character_line_low_byte_address = TILE_HEIGHT * 2 * character_index + y_in_tile + character_address_offset
|
166
|
+
character_line_low_byte = read_from_character_rom(character_line_low_byte_address)
|
167
|
+
character_line_high_byte = read_from_character_rom(character_line_low_byte_address + 8)
|
168
|
+
|
169
|
+
block_id = 0
|
170
|
+
block_id |= 0b01 if (x % BLOCK_WIDTH).odd?
|
171
|
+
block_id |= 0b10 if (y % BLOCK_HEIGHT).odd?
|
172
|
+
mini_palette_ids_byte = read_from_attribute_table(tile_index)
|
173
|
+
mini_palette_id = (mini_palette_ids_byte >> (block_id * 2)) & 0b11
|
174
|
+
|
175
|
+
TILE_WIDTH.times do |x_in_character|
|
176
|
+
index_in_character_line_byte = TILE_WIDTH - 1 - x_in_character
|
177
|
+
background_palette_index = character_line_low_byte[index_in_character_line_byte] | character_line_high_byte[index_in_character_line_byte] << 1 | mini_palette_id << 2
|
178
|
+
color_id = read_from_background_palette_table(background_palette_index)
|
179
|
+
@image.write(
|
180
|
+
value: ::Rnes::Ppu::COLORS[color_id],
|
181
|
+
x: x + x_in_character,
|
182
|
+
y: y,
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# @note
|
188
|
+
# struct Sprite {
|
189
|
+
# U8 y;
|
190
|
+
# U8 tile;
|
191
|
+
# U8 attr;
|
192
|
+
# U8 x;
|
193
|
+
# }
|
194
|
+
#
|
195
|
+
# attr 76543210
|
196
|
+
# ||| `+- palette
|
197
|
+
# ||`------ priority (0: front, 1: back)
|
198
|
+
# |`------- horizontal flip
|
199
|
+
# `-------- vertical flip
|
200
|
+
def draw_sprites
|
201
|
+
character_address_offset = registers.has_sprite_bank_bit? ? 0x1000 : 0
|
202
|
+
SPRITES_COUNT.times do |i|
|
203
|
+
base_sprite_ram_address = i * 4
|
204
|
+
y_for_sprite = (read_from_sprite_ram(base_sprite_ram_address) - TILE_HEIGHT)
|
205
|
+
next if y_for_sprite.negative?
|
206
|
+
name_table_index = read_from_sprite_ram(base_sprite_ram_address + 1)
|
207
|
+
sprite_attribute_byte = read_from_sprite_ram(base_sprite_ram_address + 2)
|
208
|
+
x_for_sprite = read_from_sprite_ram(base_sprite_ram_address + 3)
|
209
|
+
|
210
|
+
character_index = read_from_name_table(name_table_index)
|
211
|
+
|
212
|
+
mini_palette_id = sprite_attribute_byte & 0b11
|
213
|
+
|
214
|
+
TILE_HEIGHT.times do |y_in_character|
|
215
|
+
character_line_low_byte_address = TILE_HEIGHT * 2 * character_index + y_in_character + character_address_offset
|
216
|
+
character_line_low_byte = read_from_character_rom(character_line_low_byte_address)
|
217
|
+
character_line_high_byte = read_from_character_rom(character_line_low_byte_address + 8)
|
218
|
+
TILE_WIDTH.times do |x_in_character|
|
219
|
+
index_in_character_line_byte = TILE_WIDTH - 1 - x_in_character
|
220
|
+
background_palette_index = character_line_low_byte[index_in_character_line_byte] | character_line_high_byte[index_in_character_line_byte] << 1 | mini_palette_id << 2
|
221
|
+
color_id = read_from_background_palette_table(background_palette_index)
|
222
|
+
@image.write(
|
223
|
+
value: ::Rnes::Ppu::COLORS[color_id],
|
224
|
+
x: x_for_sprite + x_in_character,
|
225
|
+
y: y_for_sprite + y_in_character,
|
226
|
+
)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# @return [Boolean]
|
233
|
+
def on_bottom_end_line?
|
234
|
+
line == VISIBLE_WINDOW_HEIGHT + V_BLANK_HEIGHT
|
235
|
+
end
|
236
|
+
|
237
|
+
# @return [Boolean]
|
238
|
+
def on_line_to_start_v_blank?
|
239
|
+
line == VISIBLE_WINDOW_HEIGHT
|
240
|
+
end
|
241
|
+
|
242
|
+
# @return [Boolean]
|
243
|
+
def on_right_end_cycle?
|
244
|
+
cycle == CYCLES_PER_LINE - 1
|
245
|
+
end
|
246
|
+
|
247
|
+
# @return [Boolean]
|
248
|
+
def on_visible_cycle?
|
249
|
+
(0...VISIBLE_WINDOW_WIDTH).cover?(x) && (0...VISIBLE_WINDOW_HEIGHT).cover?(y)
|
250
|
+
end
|
251
|
+
|
252
|
+
# @param [Integer] index
|
253
|
+
# @return [Integer] 4-color-palette IDs of 4 blocks, as 8 bit data.
|
254
|
+
def read_from_attribute_table(index)
|
255
|
+
@bus.read(ADDRESS_TO_START_ATTRIBUTE_TABLE + index)
|
256
|
+
end
|
257
|
+
|
258
|
+
# @param [Integer] index
|
259
|
+
# @return [Integer]
|
260
|
+
def read_from_name_table(index)
|
261
|
+
@bus.read(ADDRESS_TO_START_NAME_TABLE + index)
|
262
|
+
end
|
263
|
+
|
264
|
+
# @param [Integer] index
|
265
|
+
# @return [Integer]
|
266
|
+
def read_from_background_palette_table(index)
|
267
|
+
@bus.read(ADDRESS_TO_START_BACKGROUND_PALETTE_TABLE + index)
|
268
|
+
end
|
269
|
+
|
270
|
+
# @param [Integer] index
|
271
|
+
# @return [Integer]
|
272
|
+
def read_from_character_rom(index)
|
273
|
+
@bus.read(index)
|
274
|
+
end
|
275
|
+
|
276
|
+
# @return [Integer]
|
277
|
+
def read_from_video_ram
|
278
|
+
value = @bus.read(@video_ram_address)
|
279
|
+
@video_ram_address += video_ram_address_offset
|
280
|
+
value
|
281
|
+
end
|
282
|
+
|
283
|
+
# @param [Integer] address
|
284
|
+
# @return [Integer]
|
285
|
+
def read_from_sprite_ram(address)
|
286
|
+
@sprite_ram.read(address)
|
287
|
+
end
|
288
|
+
|
289
|
+
def render_image
|
290
|
+
@renderer.render(@image)
|
291
|
+
end
|
292
|
+
|
293
|
+
def set_v_blank
|
294
|
+
registers.set_in_v_blank_bit
|
295
|
+
end
|
296
|
+
|
297
|
+
# @return [Integer]
|
298
|
+
def tile_index
|
299
|
+
y_of_tile * (VISIBLE_WINDOW_WIDTH / TILE_WIDTH) + x_of_tile
|
300
|
+
end
|
301
|
+
|
302
|
+
# @return [Boolean]
|
303
|
+
def v_blank_interrupt_enabled?
|
304
|
+
@registers.has_v_blank_irq_enabled_bit?
|
305
|
+
end
|
306
|
+
|
307
|
+
# @return [Integer]
|
308
|
+
def video_ram_address_offset
|
309
|
+
if registers.has_large_video_ram_address_offset_bit?
|
310
|
+
32
|
311
|
+
else
|
312
|
+
1
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# @param [Integer] address
|
317
|
+
def write_sprite_ram_address(address)
|
318
|
+
@sprite_ram_address = address
|
319
|
+
end
|
320
|
+
|
321
|
+
# @param [Integer] value
|
322
|
+
def write_to_scroll_registers(value)
|
323
|
+
if @writing_to_scroll_registers
|
324
|
+
@registers.scroll_vertical = value
|
325
|
+
else
|
326
|
+
@registers.scroll_horizontal = value
|
327
|
+
end
|
328
|
+
@writing_to_scroll_registers = !@writing_to_scroll_registers
|
329
|
+
end
|
330
|
+
|
331
|
+
# @param [Integer] value
|
332
|
+
def write_to_sprite_ram(value)
|
333
|
+
@sprite_ram.write(@sprite_ram_address, value)
|
334
|
+
@sprite_ram_address += 1
|
335
|
+
end
|
336
|
+
|
337
|
+
# @param [Integer] address
|
338
|
+
def write_video_ram_address(address)
|
339
|
+
if @writing_video_ram_address
|
340
|
+
@video_ram_address |= address
|
341
|
+
else
|
342
|
+
@video_ram_address = address << 8
|
343
|
+
end
|
344
|
+
@writing_video_ram_address = !@writing_video_ram_address
|
345
|
+
end
|
346
|
+
|
347
|
+
# @param [Integer] value
|
348
|
+
def write_to_video_ram(value)
|
349
|
+
@bus.write(@video_ram_address, value)
|
350
|
+
@video_ram_address += video_ram_address_offset
|
351
|
+
end
|
352
|
+
|
353
|
+
# @return [Integer]
|
354
|
+
def x
|
355
|
+
cycle - 1
|
356
|
+
end
|
357
|
+
|
358
|
+
# @return [Integer]
|
359
|
+
def x_in_tile
|
360
|
+
x % TILE_WIDTH
|
361
|
+
end
|
362
|
+
|
363
|
+
# @return [Integer]
|
364
|
+
def x_of_tile
|
365
|
+
x / TILE_WIDTH
|
366
|
+
end
|
367
|
+
|
368
|
+
# @return [Integer]
|
369
|
+
def y
|
370
|
+
line
|
371
|
+
end
|
372
|
+
|
373
|
+
# @return [Integer]
|
374
|
+
def y_in_tile
|
375
|
+
y % TILE_HEIGHT
|
376
|
+
end
|
377
|
+
|
378
|
+
# @return [Integer]
|
379
|
+
def y_of_tile
|
380
|
+
y / TILE_HEIGHT
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|