rnes 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/.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
|