psx 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/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +129 -0
- data/exe/psx +86 -0
- data/lib/psx/bios.rb +46 -0
- data/lib/psx/cdrom.rb +202 -0
- data/lib/psx/cop0.rb +161 -0
- data/lib/psx/cpu.rb +964 -0
- data/lib/psx/disasm.rb +226 -0
- data/lib/psx/display.rb +200 -0
- data/lib/psx/dma.rb +406 -0
- data/lib/psx/gpu.rb +1116 -0
- data/lib/psx/gte.rb +775 -0
- data/lib/psx/interrupts.rb +79 -0
- data/lib/psx/memory.rb +382 -0
- data/lib/psx/ram.rb +47 -0
- data/lib/psx/sio0.rb +261 -0
- data/lib/psx/spu.rb +110 -0
- data/lib/psx/timers.rb +175 -0
- data/lib/psx/version.rb +5 -0
- data/lib/psx.rb +354 -0
- metadata +114 -0
data/lib/psx.rb
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "psx/version"
|
|
4
|
+
require_relative "psx/bios"
|
|
5
|
+
require_relative "psx/ram"
|
|
6
|
+
require_relative "psx/cop0"
|
|
7
|
+
require_relative "psx/interrupts"
|
|
8
|
+
require_relative "psx/dma"
|
|
9
|
+
require_relative "psx/gpu"
|
|
10
|
+
require_relative "psx/timers"
|
|
11
|
+
require_relative "psx/cdrom"
|
|
12
|
+
require_relative "psx/sio0"
|
|
13
|
+
require_relative "psx/spu"
|
|
14
|
+
require_relative "psx/memory"
|
|
15
|
+
require_relative "psx/cpu"
|
|
16
|
+
require_relative "psx/disasm"
|
|
17
|
+
require_relative "psx/display"
|
|
18
|
+
|
|
19
|
+
module PSX
|
|
20
|
+
class Emulator
|
|
21
|
+
attr_reader :cpu, :memory, :interrupts, :dma, :gpu, :timers, :cdrom, :sio0
|
|
22
|
+
attr_accessor :controller_state_proc
|
|
23
|
+
|
|
24
|
+
# Timing constants
|
|
25
|
+
CPU_FREQ = 33_868_800 # 33.8688 MHz
|
|
26
|
+
CYCLES_PER_FRAME = CPU_FREQ / 60 # ~560K cycles per frame at 60Hz
|
|
27
|
+
CYCLES_PER_SCANLINE = CYCLES_PER_FRAME / 263 # NTSC has 263 scanlines
|
|
28
|
+
|
|
29
|
+
def initialize(bios_path)
|
|
30
|
+
bios = BIOS.new(bios_path)
|
|
31
|
+
ram = RAM.new
|
|
32
|
+
@interrupts = Interrupts.new
|
|
33
|
+
@spu = SPU.new
|
|
34
|
+
@dma = DMA.new(interrupts: @interrupts, spu: @spu)
|
|
35
|
+
@gpu = GPU.new(interrupts: @interrupts)
|
|
36
|
+
@timers = Timers.new(interrupts: @interrupts)
|
|
37
|
+
@cdrom = CDROM.new(interrupts: @interrupts)
|
|
38
|
+
@controller_state_proc = -> { 0xFFFF }
|
|
39
|
+
@sio0 = SIO0.new(interrupts: @interrupts, controller_state: -> { @controller_state_proc.call })
|
|
40
|
+
@memory = Memory.new(
|
|
41
|
+
bios: bios, ram: ram, interrupts: @interrupts,
|
|
42
|
+
dma: @dma, timers: @timers, cdrom: @cdrom, sio0: @sio0, spu: @spu
|
|
43
|
+
)
|
|
44
|
+
@memory.gpu = @gpu
|
|
45
|
+
@cpu = CPU.new(@memory, interrupts: @interrupts)
|
|
46
|
+
|
|
47
|
+
@cycle_count = 0
|
|
48
|
+
@frame_count = 0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run(steps: nil, debug: false)
|
|
52
|
+
if debug
|
|
53
|
+
run_debug(steps)
|
|
54
|
+
elsif steps
|
|
55
|
+
run_fast(steps)
|
|
56
|
+
else
|
|
57
|
+
run_forever
|
|
58
|
+
end
|
|
59
|
+
rescue CPU::ExecutionError => e
|
|
60
|
+
puts "Execution error: #{e.message}"
|
|
61
|
+
puts @cpu.dump_registers
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Fast path for Ruby CPU: known number of steps, no debug
|
|
66
|
+
def run_fast(steps)
|
|
67
|
+
cpu = @cpu
|
|
68
|
+
cycle_count = @cycle_count
|
|
69
|
+
frame_count = @frame_count
|
|
70
|
+
timers = @timers
|
|
71
|
+
interrupts = @interrupts
|
|
72
|
+
gpu = @gpu
|
|
73
|
+
remaining = steps
|
|
74
|
+
|
|
75
|
+
sio0 = @sio0
|
|
76
|
+
dma = @dma
|
|
77
|
+
tick_threshold = 64
|
|
78
|
+
while remaining > 0
|
|
79
|
+
remaining -= 1
|
|
80
|
+
cpu.step
|
|
81
|
+
|
|
82
|
+
# Inlined tick_devices. cpu.step_cycles is the effective cycle cost
|
|
83
|
+
# of the instruction we just ran (1 for ALU, 2 for loads). Using it
|
|
84
|
+
# instead of a constant 1 keeps the VBlank period in line with the
|
|
85
|
+
# BIOS' own timing expectations, so VSync waits don't time out.
|
|
86
|
+
cycles = cpu.step_cycles
|
|
87
|
+
cycle_count += cycles
|
|
88
|
+
if cycle_count >= tick_threshold
|
|
89
|
+
tick_threshold = cycle_count + 64
|
|
90
|
+
timers.tick(64)
|
|
91
|
+
sio0.tick(64)
|
|
92
|
+
dma.tick_cycles(64)
|
|
93
|
+
end
|
|
94
|
+
if cycle_count >= CYCLES_PER_FRAME
|
|
95
|
+
cycle_count = 0
|
|
96
|
+
tick_threshold = 64
|
|
97
|
+
frame_count += 1
|
|
98
|
+
interrupts.request(Interrupts::IRQ_VBLANK)
|
|
99
|
+
gpu.vblank
|
|
100
|
+
@cdrom.tick
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@cycle_count = cycle_count
|
|
105
|
+
@frame_count = frame_count
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def run_forever
|
|
109
|
+
loop do
|
|
110
|
+
@cpu.step
|
|
111
|
+
tick_devices
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def run_debug(steps)
|
|
116
|
+
count = 0
|
|
117
|
+
loop do
|
|
118
|
+
puts @cpu.disassemble_current
|
|
119
|
+
puts @cpu.dump_registers if count % 10 == 0
|
|
120
|
+
@cpu.step
|
|
121
|
+
tick_devices
|
|
122
|
+
count += 1
|
|
123
|
+
break if steps && count >= steps
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Run for specified number of frames
|
|
128
|
+
def run_frames(frames)
|
|
129
|
+
frames.times do |f|
|
|
130
|
+
run(steps: CYCLES_PER_FRAME)
|
|
131
|
+
@interrupts.request(Interrupts::IRQ_VBLANK)
|
|
132
|
+
@frame_count += 1
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Run with graphical display (threaded: emulation in background)
|
|
137
|
+
def run_with_display(target_fps: 60, frameskip: true)
|
|
138
|
+
display = Display.new(title: "PSX-Ruby - Loading BIOS...")
|
|
139
|
+
@controller_state_proc = -> { display.controller_state }
|
|
140
|
+
render_interval = 1.0 / target_fps
|
|
141
|
+
|
|
142
|
+
# Run many cycles between renders for speed
|
|
143
|
+
cycles_per_chunk = 100_000 # Smaller chunks for better responsiveness
|
|
144
|
+
|
|
145
|
+
puts "Starting emulation with display (threaded)..."
|
|
146
|
+
puts "Note: BIOS takes ~40 seconds to show Sony logo"
|
|
147
|
+
puts "Controls: Arrow keys=D-pad, Z=Cross, X=Circle, A=Square, S=Triangle"
|
|
148
|
+
puts " Enter=Start, Space=Select, Q/W=L1/R1, Escape=Quit"
|
|
149
|
+
puts ""
|
|
150
|
+
|
|
151
|
+
# Shared state
|
|
152
|
+
@emu_mutex = Mutex.new
|
|
153
|
+
@quit_flag = false
|
|
154
|
+
@emu_error = nil
|
|
155
|
+
@total_cycles = 0
|
|
156
|
+
|
|
157
|
+
# Emulation thread
|
|
158
|
+
emu_thread = Thread.new do
|
|
159
|
+
loop do
|
|
160
|
+
break if @quit_flag
|
|
161
|
+
|
|
162
|
+
begin
|
|
163
|
+
@emu_mutex.synchronize do
|
|
164
|
+
run(steps: cycles_per_chunk)
|
|
165
|
+
@total_cycles += cycles_per_chunk
|
|
166
|
+
end
|
|
167
|
+
rescue CPU::ExecutionError => e
|
|
168
|
+
@emu_error = e
|
|
169
|
+
break
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Small yield to let main thread get lock
|
|
173
|
+
Thread.pass if @total_cycles % 500_000 == 0
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Main thread: SDL events and rendering
|
|
178
|
+
last_render = Time.now
|
|
179
|
+
last_status = Time.now
|
|
180
|
+
last_status_cycles = 0
|
|
181
|
+
loop do
|
|
182
|
+
# Poll SDL events (must be on main thread on macOS)
|
|
183
|
+
display.poll_events
|
|
184
|
+
if display.quit_requested?
|
|
185
|
+
@quit_flag = true
|
|
186
|
+
break
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check for emulation errors
|
|
190
|
+
if @emu_error
|
|
191
|
+
puts "CPU Error: #{@emu_error.message}"
|
|
192
|
+
@emu_mutex.synchronize { puts @cpu.dump_registers }
|
|
193
|
+
@quit_flag = true
|
|
194
|
+
break
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Render at target FPS. Important: do NOT toggle @gpu.vblank here.
|
|
198
|
+
# The emulator's run loop is the authoritative driver of vblank
|
|
199
|
+
# (CYCLES_PER_FRAME boundaries). Toggling here at wall-clock 60 Hz on
|
|
200
|
+
# top of that confuses BIOS-side vsync counters and trips
|
|
201
|
+
# SystemErrorUnresolvedException.
|
|
202
|
+
now = Time.now
|
|
203
|
+
elapsed = now - last_render
|
|
204
|
+
if elapsed >= render_interval
|
|
205
|
+
@emu_mutex.synchronize do
|
|
206
|
+
@frame_count += 1
|
|
207
|
+
display.update(@gpu.framebuffer)
|
|
208
|
+
end
|
|
209
|
+
last_render = now
|
|
210
|
+
else
|
|
211
|
+
# Sleep for remaining time to hit target FPS
|
|
212
|
+
sleep_time = render_interval - elapsed
|
|
213
|
+
sleep(sleep_time * 0.8) if sleep_time > 0.001 # Don't oversleep
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Periodic status line so progress is visible from the console.
|
|
217
|
+
if now - last_status >= 2.0
|
|
218
|
+
delta = @total_cycles - last_status_cycles
|
|
219
|
+
ips = delta.to_f / (now - last_status)
|
|
220
|
+
printf "frames=%5d cycles=%10d %.1f Mips PC=%08X\n",
|
|
221
|
+
@frame_count, @total_cycles, ips / 1_000_000.0, @cpu.pc
|
|
222
|
+
$stdout.flush
|
|
223
|
+
last_status = now
|
|
224
|
+
last_status_cycles = @total_cycles
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Wait for emulation thread to finish
|
|
229
|
+
emu_thread.join(1.0) # Wait up to 1 second
|
|
230
|
+
|
|
231
|
+
display.close
|
|
232
|
+
puts "\nEmulation ended after #{@frame_count} frames (#{@total_cycles} cycles)"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Load a PS-EXE executable into RAM and prepare the CPU to run it.
|
|
236
|
+
# Standard PSX EXE format: 2048-byte header (magic "PS-X EXE"), followed
|
|
237
|
+
# by the text/data section that must be copied to dest_addr. After load
|
|
238
|
+
# the CPU is positioned at the executable's entry point with GP and SP
|
|
239
|
+
# set per the header (or the conventional defaults if zero).
|
|
240
|
+
def load_psexe(path)
|
|
241
|
+
data = File.binread(path)
|
|
242
|
+
raise "PS-EXE smaller than header: #{data.bytesize} bytes" if data.bytesize < 0x800
|
|
243
|
+
magic = data.byteslice(0, 8)
|
|
244
|
+
raise "Not a PS-EXE (magic=#{magic.inspect})" unless magic == "PS-X EXE"
|
|
245
|
+
|
|
246
|
+
initial_pc = data.byteslice(0x10, 4).unpack1("V")
|
|
247
|
+
initial_gp = data.byteslice(0x14, 4).unpack1("V")
|
|
248
|
+
dest_addr = data.byteslice(0x18, 4).unpack1("V")
|
|
249
|
+
file_size = data.byteslice(0x1C, 4).unpack1("V")
|
|
250
|
+
memfill_start = data.byteslice(0x20, 4).unpack1("V")
|
|
251
|
+
memfill_size = data.byteslice(0x24, 4).unpack1("V")
|
|
252
|
+
stack_addr = data.byteslice(0x28, 4).unpack1("V")
|
|
253
|
+
_stack_size = data.byteslice(0x2C, 4).unpack1("V")
|
|
254
|
+
|
|
255
|
+
payload = data.byteslice(0x800, file_size) || ""
|
|
256
|
+
payload.each_byte.with_index do |b, i|
|
|
257
|
+
@memory.write8((dest_addr + i) & 0xFFFF_FFFF, b)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if memfill_size.positive?
|
|
261
|
+
memfill_size.times do |i|
|
|
262
|
+
@memory.write8((memfill_start + i) & 0xFFFF_FFFF, 0)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
sp = stack_addr.zero? ? 0x801F_FFF0 : stack_addr
|
|
267
|
+
@cpu.regs[28] = initial_gp & 0xFFFF_FFFF
|
|
268
|
+
@cpu.regs[29] = sp & 0xFFFF_FFFF
|
|
269
|
+
@cpu.regs[30] = sp & 0xFFFF_FFFF
|
|
270
|
+
@cpu.regs[31] = 0 # return-to-zero on exit
|
|
271
|
+
@cpu.pc = initial_pc
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
entry: initial_pc, gp: initial_gp, sp: sp, dest: dest_addr,
|
|
275
|
+
size: file_size, memfill: [memfill_start, memfill_size]
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Save framebuffer as PPM image
|
|
280
|
+
def save_screenshot(filename)
|
|
281
|
+
fb = @gpu.framebuffer
|
|
282
|
+
rgba = fb[:rgba]
|
|
283
|
+
width = fb[:width]
|
|
284
|
+
height = fb[:height]
|
|
285
|
+
|
|
286
|
+
# Extract RGB from RGBA (skip alpha bytes)
|
|
287
|
+
rgb_data = String.new(capacity: width * height * 3)
|
|
288
|
+
i = 0
|
|
289
|
+
(width * height).times do
|
|
290
|
+
rgb_data << rgba.getbyte(i).chr << rgba.getbyte(i + 1).chr << rgba.getbyte(i + 2).chr
|
|
291
|
+
i += 4
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
File.open(filename, "wb") do |f|
|
|
295
|
+
f.puts "P6"
|
|
296
|
+
f.puts "#{width} #{height}"
|
|
297
|
+
f.puts "255"
|
|
298
|
+
f.write rgb_data
|
|
299
|
+
end
|
|
300
|
+
puts "Saved screenshot to #{filename} (#{width}x#{height})"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Save framebuffer as ASCII art (for terminal)
|
|
304
|
+
def ascii_screenshot(width: 80)
|
|
305
|
+
fb = @gpu.framebuffer
|
|
306
|
+
rgba = fb[:rgba]
|
|
307
|
+
scale_x = fb[:width].to_f / width
|
|
308
|
+
height = (fb[:height] / scale_x / 2).to_i # /2 because terminal chars are ~2x tall
|
|
309
|
+
|
|
310
|
+
chars = " .:-=+*#%@"
|
|
311
|
+
|
|
312
|
+
lines = []
|
|
313
|
+
height.times do |y|
|
|
314
|
+
line = +"" # Unfrozen string
|
|
315
|
+
width.times do |x|
|
|
316
|
+
src_x = (x * scale_x).to_i
|
|
317
|
+
src_y = (y * scale_x * 2).to_i
|
|
318
|
+
# RGBA format: 4 bytes per pixel
|
|
319
|
+
idx = (src_y * fb[:width] + src_x) * 4
|
|
320
|
+
r = rgba.getbyte(idx) || 0
|
|
321
|
+
g = rgba.getbyte(idx + 1) || 0
|
|
322
|
+
b = rgba.getbyte(idx + 2) || 0
|
|
323
|
+
brightness = (r + g + b) / 3.0 / 255.0
|
|
324
|
+
char_idx = (brightness * (chars.length - 1)).to_i
|
|
325
|
+
line << chars[char_idx]
|
|
326
|
+
end
|
|
327
|
+
lines << line
|
|
328
|
+
end
|
|
329
|
+
lines.join("\n")
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
private
|
|
333
|
+
|
|
334
|
+
def tick_devices
|
|
335
|
+
@cycle_count += 1
|
|
336
|
+
|
|
337
|
+
# Tick timers less frequently for performance
|
|
338
|
+
if @cycle_count % 64 == 0
|
|
339
|
+
@timers.tick(64)
|
|
340
|
+
@sio0.tick(64)
|
|
341
|
+
@dma.tick_cycles(64)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# VBlank every frame
|
|
345
|
+
if @cycle_count >= CYCLES_PER_FRAME
|
|
346
|
+
@cycle_count = 0
|
|
347
|
+
@frame_count += 1
|
|
348
|
+
@interrupts.request(Interrupts::IRQ_VBLANK)
|
|
349
|
+
@gpu.vblank # Toggle interlace field
|
|
350
|
+
@cdrom.tick
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: psx
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chris Hasiński
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruby-sdl2
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13'
|
|
54
|
+
description: |
|
|
55
|
+
A work-in-progress PlayStation 1 emulator written entirely in Ruby.
|
|
56
|
+
Implements the MIPS R3000A CPU, GTE, GPU (software rasteriser), DMA,
|
|
57
|
+
interrupts, timers, CD-ROM stub, SIO0 controller, and a minimal SPU —
|
|
58
|
+
enough to boot the SCPH1001 BIOS into the Memory Card / CD-ROM shell.
|
|
59
|
+
Ships an SDL2-backed front-end via the `psx` command. A BIOS image is
|
|
60
|
+
not included and must be supplied by the user.
|
|
61
|
+
email:
|
|
62
|
+
- krzysztof.hasinski@gmail.com
|
|
63
|
+
executables:
|
|
64
|
+
- psx
|
|
65
|
+
extensions: []
|
|
66
|
+
extra_rdoc_files: []
|
|
67
|
+
files:
|
|
68
|
+
- CHANGELOG.md
|
|
69
|
+
- LICENSE
|
|
70
|
+
- README.md
|
|
71
|
+
- exe/psx
|
|
72
|
+
- lib/psx.rb
|
|
73
|
+
- lib/psx/bios.rb
|
|
74
|
+
- lib/psx/cdrom.rb
|
|
75
|
+
- lib/psx/cop0.rb
|
|
76
|
+
- lib/psx/cpu.rb
|
|
77
|
+
- lib/psx/disasm.rb
|
|
78
|
+
- lib/psx/display.rb
|
|
79
|
+
- lib/psx/dma.rb
|
|
80
|
+
- lib/psx/gpu.rb
|
|
81
|
+
- lib/psx/gte.rb
|
|
82
|
+
- lib/psx/interrupts.rb
|
|
83
|
+
- lib/psx/memory.rb
|
|
84
|
+
- lib/psx/ram.rb
|
|
85
|
+
- lib/psx/sio0.rb
|
|
86
|
+
- lib/psx/spu.rb
|
|
87
|
+
- lib/psx/timers.rb
|
|
88
|
+
- lib/psx/version.rb
|
|
89
|
+
homepage: https://github.com/khasinski/psx
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
source_code_uri: https://github.com/khasinski/psx
|
|
94
|
+
bug_tracker_uri: https://github.com/khasinski/psx/issues
|
|
95
|
+
changelog_uri: https://github.com/khasinski/psx/blob/main/CHANGELOG.md
|
|
96
|
+
rubygems_mfa_required: 'true'
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: 3.2.0
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubygems_version: 4.0.6
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: PlayStation 1 emulator written in pure Ruby
|
|
114
|
+
test_files: []
|