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
data/lib/rnes/cpu_bus.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'rnes/errors'
|
2
|
+
|
3
|
+
module Rnes
|
4
|
+
class CpuBus
|
5
|
+
# @param [Rnes::Rom]
|
6
|
+
# @return [Rnes::Rom]
|
7
|
+
attr_accessor :program_rom
|
8
|
+
|
9
|
+
# @param [Rnes::DmaController] dma_controller
|
10
|
+
# @param [Rnes::Keypad] keypad1
|
11
|
+
# @param [Rnes::Keypad] keypad2
|
12
|
+
# @param [Rnes::Ppu] ppu
|
13
|
+
# @param [Rnes::Ram] ram
|
14
|
+
def initialize(dma_controller:, keypad1:, keypad2:, ppu:, ram:)
|
15
|
+
@dma_controller = dma_controller
|
16
|
+
@keypad1 = keypad1
|
17
|
+
@keypad2 = keypad2
|
18
|
+
@ppu = ppu
|
19
|
+
@ram = ram
|
20
|
+
end
|
21
|
+
|
22
|
+
# @todo
|
23
|
+
# @param [Integer]
|
24
|
+
# @return [Integer]
|
25
|
+
def read(address)
|
26
|
+
case address
|
27
|
+
when 0x0000..0x07FF
|
28
|
+
@ram.read(address)
|
29
|
+
when 0x0800..0x1FFF
|
30
|
+
@ram.read(address - 0x0800)
|
31
|
+
when 0x2000..0x2007
|
32
|
+
@ppu.read(address - 0x2000)
|
33
|
+
when 0x2008..0x3FFF
|
34
|
+
@ppu.read(address - 0x2008)
|
35
|
+
when 0x4016
|
36
|
+
@keypad1.read
|
37
|
+
when 0x4017
|
38
|
+
@keypad2.read
|
39
|
+
when 0x4000..0x401F
|
40
|
+
0 # TODO: I/O port for APU, etc
|
41
|
+
when 0x4020..0x5FFF
|
42
|
+
0 # TODO: extended RAM on special mappers
|
43
|
+
when 0x6000..0x7FFF
|
44
|
+
0 # TODO: battery-backed-up RAM
|
45
|
+
when 0x8000..0xBFFF
|
46
|
+
try_to_read_program_rom(address - 0x8000)
|
47
|
+
when 0xC000..0xFFFF
|
48
|
+
try_to_read_program_rom(address - offset_on_reading_program_rom_higher_region)
|
49
|
+
else
|
50
|
+
raise ::Rnes::Errors::InvalidCpuBusAddressError, address
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @todo
|
55
|
+
# @param [Integer] address
|
56
|
+
# @param [Integer] value
|
57
|
+
def write(address, value)
|
58
|
+
case address
|
59
|
+
when 0x0000..0x07FF
|
60
|
+
@ram.write(address, value)
|
61
|
+
when 0x0800..0x1FFF
|
62
|
+
@ram.write(address - 0x0800, value)
|
63
|
+
when 0x2000..0x2007
|
64
|
+
@ppu.write(address - 0x2000, value)
|
65
|
+
when 0x2008..0x3FFF
|
66
|
+
@ppu.write(address - 0x2008, value)
|
67
|
+
when 0x4014
|
68
|
+
@dma_controller.request_transfer(address_hint: value)
|
69
|
+
when 0x4016
|
70
|
+
@keypad1.write(value)
|
71
|
+
when 0x4017
|
72
|
+
@keypad2.write(value)
|
73
|
+
when 0x4000..0x401F
|
74
|
+
0 # TODO: I/O port for APU, etc
|
75
|
+
when 0x4020..0x5FFF
|
76
|
+
0 # TODO: extended RAM on special mappers
|
77
|
+
when 0x6000..0x7FFF
|
78
|
+
0 # TODO: battery-backed-up RAM
|
79
|
+
else
|
80
|
+
raise ::Rnes::Errors::InvalidCpuBusAddressError, address
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# @return [Boolean]
|
87
|
+
def attatched_to_large_program_rom?
|
88
|
+
@program_rom.bytesize > 16 * 2**10
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Integer]
|
92
|
+
def offset_on_reading_program_rom_higher_region
|
93
|
+
attatched_to_large_program_rom? ? 0x8000 : 0xC000
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param [Integer] address
|
97
|
+
def try_to_read_program_rom(address)
|
98
|
+
if @program_rom
|
99
|
+
@program_rom.read(address)
|
100
|
+
else
|
101
|
+
raise ::Rnes::Errors::ProgramRomNotConnectedError
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Rnes
|
2
|
+
class CpuRegisters
|
3
|
+
CARRY_BIT_INDEX = 0
|
4
|
+
ZERO_BIT_INDEX = 1
|
5
|
+
INTERRUPT_BIT_INDEX = 2
|
6
|
+
DECIMAL_BIT_INDEX = 3
|
7
|
+
BREAK_BIT_INDEX = 4
|
8
|
+
RESERVED_BIT_INDEX = 5
|
9
|
+
OVERFLOW_BIT_INDEX = 6
|
10
|
+
NEGATIVE_BIT_INDEX = 7
|
11
|
+
|
12
|
+
# @param [Integer]
|
13
|
+
# @return [Integer]
|
14
|
+
attr_accessor :accumulator
|
15
|
+
|
16
|
+
# @param [Integer]
|
17
|
+
# @return [Integer]
|
18
|
+
attr_accessor :index_x
|
19
|
+
|
20
|
+
# @param [Integer]
|
21
|
+
# @return [Integer]
|
22
|
+
attr_accessor :index_y
|
23
|
+
|
24
|
+
# @param [Integer]
|
25
|
+
# @return [Integer]
|
26
|
+
attr_accessor :program_counter
|
27
|
+
|
28
|
+
# @param [Integer]
|
29
|
+
# @return [Integer]
|
30
|
+
attr_accessor :stack_pointer
|
31
|
+
|
32
|
+
# @param [Integer]
|
33
|
+
# @return [Integer]
|
34
|
+
attr_accessor :status
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@accumulator = 0x00
|
38
|
+
@index_x = 0x00
|
39
|
+
@index_y = 0x00
|
40
|
+
@program_counter = 0x0000
|
41
|
+
@stack_pointer = 0x0000
|
42
|
+
@status = 0b00000000
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Boolean]
|
46
|
+
def break?
|
47
|
+
@status[BREAK_BIT_INDEX] == 1
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param [Boolean] boolean
|
51
|
+
def break=(boolean)
|
52
|
+
toggle_bit(BREAK_BIT_INDEX, boolean)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Boolean]
|
56
|
+
def carry?
|
57
|
+
@status[CARRY_BIT_INDEX] == 1
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [Boolean] boolean
|
61
|
+
def carry=(boolean)
|
62
|
+
toggle_bit(CARRY_BIT_INDEX, boolean)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Integer]
|
66
|
+
def carry_bit
|
67
|
+
@status[CARRY_BIT_INDEX]
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean]
|
71
|
+
def decimal?
|
72
|
+
@status[DECIMAL_BIT_INDEX] == 1
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param [Boolean] boolean
|
76
|
+
def decimal=(boolean)
|
77
|
+
toggle_bit(DECIMAL_BIT_INDEX, boolean)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Boolean]
|
81
|
+
def interrupt?
|
82
|
+
@status[INTERRUPT_BIT_INDEX] == 1
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [Boolean] boolean
|
86
|
+
def interrupt=(boolean)
|
87
|
+
toggle_bit(INTERRUPT_BIT_INDEX, boolean)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Boolean]
|
91
|
+
def negative?
|
92
|
+
@status[NEGATIVE_BIT_INDEX] == 1
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [Boolean] boolean
|
96
|
+
def negative=(boolean)
|
97
|
+
toggle_bit(NEGATIVE_BIT_INDEX, boolean)
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Boolean]
|
101
|
+
def overflow?
|
102
|
+
@status[OVERFLOW_BIT_INDEX] == 1
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [Boolean]
|
106
|
+
def reserved?
|
107
|
+
@status[RESERVED_BIT_INDEX] == 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# @param [Boolean] boolean
|
111
|
+
def reserved=(boolean)
|
112
|
+
toggle_bit(RESERVED_BIT_INDEX, boolean)
|
113
|
+
end
|
114
|
+
|
115
|
+
def reset
|
116
|
+
@accumulator = 0x00
|
117
|
+
@index_x = 0x00
|
118
|
+
@index_y = 0x00
|
119
|
+
@program_counter = 0x0000
|
120
|
+
@stack_pointer = 0x1FD
|
121
|
+
@status = 0b00110100
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param [Boolean] boolean
|
125
|
+
def overflow=(boolean)
|
126
|
+
toggle_bit(OVERFLOW_BIT_INDEX, boolean)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [Boolean]
|
130
|
+
def zero?
|
131
|
+
@status[ZERO_BIT_INDEX] == 1
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param [Boolean] boolean
|
135
|
+
def zero=(boolean)
|
136
|
+
toggle_bit(ZERO_BIT_INDEX, boolean)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# @param [Integer] index
|
142
|
+
# @param [Boolean] boolean
|
143
|
+
def toggle_bit(index, boolean)
|
144
|
+
if boolean
|
145
|
+
@status |= 1 << index
|
146
|
+
else
|
147
|
+
@status &= ~(1 << index)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rnes
|
2
|
+
class DmaController
|
3
|
+
TRANSFER_BYTESIZE = 2**8
|
4
|
+
|
5
|
+
# @param [Rnes::Ppu] ppu
|
6
|
+
# @param [Rnes::Ram] working_ram
|
7
|
+
def initialize(ppu:, working_ram:)
|
8
|
+
@ppu = ppu
|
9
|
+
@requested = false
|
10
|
+
@working_ram = working_ram
|
11
|
+
end
|
12
|
+
|
13
|
+
def transfer_if_requested
|
14
|
+
if @requested
|
15
|
+
transfer
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Integer] address_hint
|
20
|
+
def request_transfer(address_hint:)
|
21
|
+
@requested = true
|
22
|
+
@working_ram_address = address_hint << 8
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def transfer
|
28
|
+
TRANSFER_BYTESIZE.times do |index|
|
29
|
+
value = @working_ram.read(@working_ram_address + index)
|
30
|
+
@ppu.transfer_sprite_data(index: index, value: value)
|
31
|
+
end
|
32
|
+
@requested = false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
require 'rnes/logger'
|
3
|
+
require 'rnes/parts_factory'
|
4
|
+
require 'rnes/rom_loader'
|
5
|
+
|
6
|
+
module Rnes
|
7
|
+
class Emulator
|
8
|
+
LOG_FILE_NAME = 'rnes.log'.freeze
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
parts_factory = ::Rnes::PartsFactory.new
|
12
|
+
@cpu = parts_factory.cpu
|
13
|
+
@cpu_bus = parts_factory.cpu_bus
|
14
|
+
@dma_controller = parts_factory.dma_controller
|
15
|
+
@keypad1 = parts_factory.keypad1
|
16
|
+
@keypad2 = parts_factory.keypad2
|
17
|
+
@ppu = parts_factory.ppu
|
18
|
+
@ppu_bus = parts_factory.ppu_bus
|
19
|
+
@logger = ::Rnes::Logger.new(cpu: @cpu, path: LOG_FILE_NAME, ppu: @ppu)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Array<Integer>] rom_bytes
|
23
|
+
def load_rom(rom_bytes)
|
24
|
+
rom_loader = ::Rnes::RomLoader.new(rom_bytes)
|
25
|
+
copy(from: rom_loader.character_rom, to: @ppu_bus.character_ram)
|
26
|
+
@cpu_bus.program_rom = rom_loader.program_rom
|
27
|
+
@cpu.reset
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
$stdin.noecho do
|
32
|
+
loop do
|
33
|
+
if @logger
|
34
|
+
@logger.puts
|
35
|
+
end
|
36
|
+
tick
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def tick
|
42
|
+
@dma_controller.transfer_if_requested
|
43
|
+
(@cpu.tick * 3).times do
|
44
|
+
@ppu.tick
|
45
|
+
end
|
46
|
+
@keypad1.check
|
47
|
+
@keypad2.check
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @param [Rnes::Rom] from
|
53
|
+
# @param [Rnes::Ram] to
|
54
|
+
def copy(from:, to:)
|
55
|
+
from.bytesize.times do |address|
|
56
|
+
value = from.read(address)
|
57
|
+
to.write(address, value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/rnes/errors.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rnes
|
2
|
+
module Errors
|
3
|
+
class BaseError < ::StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class BaseInvalidAddressError < BaseError
|
7
|
+
# @param [Integer] address
|
8
|
+
def initialize(address)
|
9
|
+
@address = address
|
10
|
+
super(to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [String]
|
14
|
+
def to_s
|
15
|
+
format('Invalid address: 0x%04X', @address)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class InvalidAddressingModeError < BaseError
|
20
|
+
end
|
21
|
+
|
22
|
+
class InvalidCpuBusAddressError < BaseInvalidAddressError
|
23
|
+
end
|
24
|
+
|
25
|
+
class InvalidInesFormatError < BaseError
|
26
|
+
end
|
27
|
+
|
28
|
+
class InvalidOperationCodeError < BaseError
|
29
|
+
end
|
30
|
+
|
31
|
+
class InvalidOperationError < BaseError
|
32
|
+
end
|
33
|
+
|
34
|
+
class InvalidPpuAddressError < BaseInvalidAddressError
|
35
|
+
end
|
36
|
+
|
37
|
+
class InvalidPpuBusAddressError < BaseInvalidAddressError
|
38
|
+
end
|
39
|
+
|
40
|
+
class ProgramRomNotConnectedError < BaseError
|
41
|
+
end
|
42
|
+
|
43
|
+
class StackPointerOverflowError < BaseError
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/rnes/image.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rnes
|
2
|
+
class Image
|
3
|
+
# @return [Integer]
|
4
|
+
attr_reader :height
|
5
|
+
|
6
|
+
# @return [Integer]
|
7
|
+
attr_reader :width
|
8
|
+
|
9
|
+
# @param [Integer] height
|
10
|
+
# @param [Integer] width
|
11
|
+
def initialize(height:, width:)
|
12
|
+
@bytes = Array.new(height * width) do
|
13
|
+
[0, 0, 0]
|
14
|
+
end
|
15
|
+
@height = height
|
16
|
+
@width = width
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Integer] x
|
20
|
+
# @param [Integer] y
|
21
|
+
def read(x:, y:)
|
22
|
+
@bytes[@width * y + x]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [Array<Integer>] rgb
|
26
|
+
# @param [Integer] x
|
27
|
+
# @param [Integer] y
|
28
|
+
def write(value:, x:, y:)
|
29
|
+
@bytes[@width * y + x] = value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Rnes
|
2
|
+
class InesHeader
|
3
|
+
BYTESIZE = 16
|
4
|
+
|
5
|
+
PREFIX_BYTES = [
|
6
|
+
0x4E, # N
|
7
|
+
0x45, # E
|
8
|
+
0x53, # S
|
9
|
+
0x1A, # end-of-file in MS-DOS
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
# @param [Array<Integer>] bytes
|
13
|
+
def initialize(bytes)
|
14
|
+
@bytes = bytes
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Integer]
|
18
|
+
def bytesize
|
19
|
+
BYTESIZE
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Integer]
|
23
|
+
def character_ram_bytesize
|
24
|
+
@bytes[8]
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Integer]
|
28
|
+
def character_rom_bytesize
|
29
|
+
@bytes[5] * 8 * 2**10
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Boolean]
|
33
|
+
def has_battery_backed_program_rom_bit?
|
34
|
+
flags1[1] == 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Boolean]
|
38
|
+
def has_mirror_ignoring_bit?
|
39
|
+
flags1[3] == 1
|
40
|
+
end
|
41
|
+
|
42
|
+
# @note Trainers are 512 bytes of code which is loaded into $7000 before the game starts for hacker use.
|
43
|
+
# @return [Boolean]
|
44
|
+
def has_trainer_bit?
|
45
|
+
flags1[2] == 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Boolean]
|
49
|
+
def has_vertical_mirroring_bit?
|
50
|
+
flags1[0] == 1
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Integer]
|
54
|
+
def mapper_number
|
55
|
+
flags2 & 0b11110000 | flags1 >> 4
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [Integer]
|
59
|
+
def program_rom_bytesize
|
60
|
+
@bytes[4] * 16 * 2**10
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Integer]
|
64
|
+
def trainer_bytesize
|
65
|
+
if has_trainer_bit?
|
66
|
+
512
|
67
|
+
else
|
68
|
+
0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Boolean]
|
73
|
+
def valid?
|
74
|
+
@bytes[0..3] == PREFIX_BYTES
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# @return [Integer]
|
80
|
+
def flags1
|
81
|
+
@bytes[6]
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Integer]
|
85
|
+
def flags2
|
86
|
+
@bytes[7]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|