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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a7a1e5da6315c41ee938269c787e81c13431d1bcb6cd97b5122ec612fa8d0c6a
|
|
4
|
+
data.tar.gz: f16bb2c40428dedd7fa234ef19f250682e2b171f743688ef49595edac095d51a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b4701b76ec82805ea3ff3a474cfb7c6cc2e42d518c88cc9e81983134171d0360feb6e69863da282102bfadf3ecc76f5a9249b0e66af76d70e2347e6de8284fc4
|
|
7
|
+
data.tar.gz: c27d4922fcb211752bb804913a9a2cc5ce5f033b65cf79efc1233bf616702f7a4652d1668f14df131a081929ca930c87eba7e595d23acb56f69599fb169d2824
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-05-15
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- MIPS R3000A CPU with proper delay-slot / exception EPC handling.
|
|
8
|
+
- COP0 and GTE (coprocessor 2) coverage sufficient for the BIOS boot path.
|
|
9
|
+
- Software-rasteriser GPU covering most GP0/GP1 commands, textures (4/8/15
|
|
10
|
+
bit + CLUT), semi-transparency, mask bit.
|
|
11
|
+
- DMA channels: OTC, GPU (block & linked-list), SPU, CDROM.
|
|
12
|
+
- Interrupt controller, root counters, CD-ROM stub, SPU stub, SIO0 digital
|
|
13
|
+
pad (slot 1).
|
|
14
|
+
- Boots the SCPH1001 BIOS into the Memory Card / CD-ROM shell menu.
|
|
15
|
+
- `psx` SDL-backed CLI front-end.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Chris Hasiński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# PSX
|
|
2
|
+
|
|
3
|
+
[](spec/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A work-in-progress PlayStation 1 emulator written in pure Ruby.
|
|
7
|
+
|
|
8
|
+
It boots the SCPH1001 BIOS into the **Memory Card / CD-ROM shell** and can
|
|
9
|
+
run a chunk of the [JaCzekanski/ps1-tests](https://github.com/JaCzekanski/ps1-tests)
|
|
10
|
+
suite. There is no audio, the CD-ROM is a stub (no disc image loading yet),
|
|
11
|
+
and the GPU is a software rasteriser with rough texture sampling — so don't
|
|
12
|
+
expect to play games. Think of it as an executable spec for the PS1.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
gem install psx
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You will also need SDL2 on your system (the gem depends on `ruby-sdl2`):
|
|
23
|
+
|
|
24
|
+
- macOS: `brew install sdl2`
|
|
25
|
+
- Debian / Ubuntu: `sudo apt install libsdl2-dev`
|
|
26
|
+
- Arch: `sudo pacman -S sdl2`
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
You must supply your own BIOS ROM (typically `SCPH1001.BIN`, 512 KB). PSX
|
|
31
|
+
does not ship one — BIOS images are copyrighted.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
psx path/to/SCPH1001.BIN
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run `psx --help` for the option list. Keyboard controls (slot 1 digital pad):
|
|
38
|
+
|
|
39
|
+
| Key | Button |
|
|
40
|
+
| -------------- | -------- |
|
|
41
|
+
| Arrow keys | D-pad |
|
|
42
|
+
| Z | Cross |
|
|
43
|
+
| X | Circle |
|
|
44
|
+
| A | Square |
|
|
45
|
+
| S | Triangle |
|
|
46
|
+
| Enter | Start |
|
|
47
|
+
| Space | Select |
|
|
48
|
+
| Q / W | L1 / R1 |
|
|
49
|
+
| E / R | L2 / R2 |
|
|
50
|
+
| Escape | Quit |
|
|
51
|
+
|
|
52
|
+
The BIOS shell idles on the Sony logo until you press Triangle, which opens
|
|
53
|
+
the Memory Card / CD-ROM menu.
|
|
54
|
+
|
|
55
|
+
## Programmatic use
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require "psx"
|
|
59
|
+
|
|
60
|
+
emu = PSX::Emulator.new("SCPH1001.BIN")
|
|
61
|
+
emu.run(steps: 300_000_000) # boot to shell
|
|
62
|
+
emu.controller_state_proc = -> { 0xEFFF } # press Triangle
|
|
63
|
+
emu.run(steps: 30_000_000)
|
|
64
|
+
emu.save_screenshot("menu.ppm")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The emulator exposes the major components individually: `emu.cpu`,
|
|
68
|
+
`emu.memory`, `emu.gpu`, `emu.dma`, `emu.cdrom`, `emu.sio0`, `emu.interrupts`,
|
|
69
|
+
`emu.timers`.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
git clone https://github.com/khasinski/psx
|
|
75
|
+
cd psx
|
|
76
|
+
bundle install
|
|
77
|
+
bundle exec rake test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `bin/` directory contains development tools that are *not* shipped with
|
|
81
|
+
the gem:
|
|
82
|
+
|
|
83
|
+
| Script | Purpose |
|
|
84
|
+
| ----------------- | ----------------------------------------------------------------------- |
|
|
85
|
+
| `bin/psx-ruby` | Headless boot of the BIOS (no SDL window). |
|
|
86
|
+
| `bin/psx-test` | Run a PS-EXE on top of the BIOS, diff against ps1-tests reference logs. |
|
|
87
|
+
| `bin/psx-smoke` | Boot the BIOS and dump PPM screenshots at intervals. |
|
|
88
|
+
| `bin/psx-trace` | PC histogram + I_STAT/I_MASK summary at checkpoints. |
|
|
89
|
+
| `bin/psx-disasm` | Disassemble a range of physical addresses after N cycles of boot. |
|
|
90
|
+
| `bin/psx-biostrace` | Tally A/B/C jump-table calls; dump the last N calls. |
|
|
91
|
+
| `bin/psx-puts-trace` | Capture the kernel's debug TTY (`puts2`, `printf`, …). |
|
|
92
|
+
| `bin/psx-memwatch`| Log reads/writes to specific addresses with the PC that did them. |
|
|
93
|
+
| `bin/psx-dumpmem` | Hex-dump a range of memory after N cycles of boot. |
|
|
94
|
+
|
|
95
|
+
### Running ps1-tests
|
|
96
|
+
|
|
97
|
+
The `bin/psx-test` runner expects a local checkout of
|
|
98
|
+
[JaCzekanski/ps1-tests](https://github.com/JaCzekanski/ps1-tests):
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
git clone https://github.com/JaCzekanski/ps1-tests.git .tests
|
|
102
|
+
bundle exec ruby bin/psx-test -e .tests/cpu/cop/psx.log .tests/cpu/cop/cop.exe
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
107
|
+
What works:
|
|
108
|
+
|
|
109
|
+
- MIPS R3000A CPU with delay slots, exceptions, COP0
|
|
110
|
+
- GTE (passes the `gte/test-all` shape, several coverage gaps)
|
|
111
|
+
- GPU: GP0 polygons / lines / rectangles, textured / shaded / semi-transparent
|
|
112
|
+
primitives, CPU↔VRAM blits, mask bit. Software rasteriser, no PGXP.
|
|
113
|
+
- DMA channels for OTC, GPU (block / linked list), SPU (stub), CDROM (stub)
|
|
114
|
+
- Interrupt controller, root counters
|
|
115
|
+
- CD-ROM stub (responds to BIOS probe; no disc image support)
|
|
116
|
+
- SIO0 digital pad (slot 1)
|
|
117
|
+
- SPU stub (mirrors SPUCNT → SPUSTAT; no actual audio synthesis)
|
|
118
|
+
- Boots SCPH1001 into the Memory Card menu
|
|
119
|
+
|
|
120
|
+
What doesn't:
|
|
121
|
+
|
|
122
|
+
- No CD-ROM image loading — can't boot real games
|
|
123
|
+
- No SPU audio synthesis (no sound)
|
|
124
|
+
- Several ps1-tests fail (CD-ROM timing, code-in-IO bus errors)
|
|
125
|
+
- Texture sampling has visible glitches on the shell UI
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT. See [LICENSE](LICENSE).
|
data/exe/psx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# PlayStation 1 emulator — graphical front-end. Loads a BIOS image and runs
|
|
5
|
+
# the emulator with an SDL window for display + keyboard controller input.
|
|
6
|
+
|
|
7
|
+
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
|
|
8
|
+
|
|
9
|
+
require "psx"
|
|
10
|
+
|
|
11
|
+
def usage
|
|
12
|
+
warn <<~USAGE
|
|
13
|
+
PSX — PlayStation 1 emulator (#{PSX::VERSION})
|
|
14
|
+
|
|
15
|
+
Usage: psx [options] <bios.bin>
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-f, --fps N Target FPS (default: 60)
|
|
19
|
+
-n, --no-frameskip Disable frame skipping
|
|
20
|
+
-v, --version Print version and exit
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
|
|
23
|
+
Controls (keyboard, slot 1 digital pad):
|
|
24
|
+
Arrow keys D-pad
|
|
25
|
+
Z Cross (confirm)
|
|
26
|
+
X Circle (cancel)
|
|
27
|
+
A Square
|
|
28
|
+
S Triangle
|
|
29
|
+
Enter Start
|
|
30
|
+
Space Select
|
|
31
|
+
Q / W L1 / R1
|
|
32
|
+
E / R L2 / R2
|
|
33
|
+
Escape Quit
|
|
34
|
+
|
|
35
|
+
A BIOS ROM (e.g. SCPH1001.BIN, 512 KB) is required and must be provided
|
|
36
|
+
by the user; PSX-Ruby does not ship one.
|
|
37
|
+
USAGE
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
target_fps = 60
|
|
42
|
+
frameskip = true
|
|
43
|
+
bios_path = nil
|
|
44
|
+
|
|
45
|
+
args = ARGV.dup
|
|
46
|
+
while (arg = args.shift)
|
|
47
|
+
case arg
|
|
48
|
+
when "-f", "--fps"
|
|
49
|
+
target_fps = args.shift&.to_i
|
|
50
|
+
usage if target_fps.nil? || target_fps <= 0
|
|
51
|
+
when "-n", "--no-frameskip"
|
|
52
|
+
frameskip = false
|
|
53
|
+
when "-v", "--version"
|
|
54
|
+
puts PSX::VERSION
|
|
55
|
+
exit 0
|
|
56
|
+
when "-h", "--help"
|
|
57
|
+
usage
|
|
58
|
+
else
|
|
59
|
+
bios_path = arg
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
usage unless bios_path
|
|
64
|
+
|
|
65
|
+
unless File.exist?(bios_path)
|
|
66
|
+
warn "Error: BIOS file not found: #{bios_path}"
|
|
67
|
+
exit 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
puts "PSX-Ruby #{PSX::VERSION}"
|
|
71
|
+
puts "Loading BIOS: #{bios_path}"
|
|
72
|
+
puts "Target FPS: #{target_fps}"
|
|
73
|
+
puts "Frame skip: #{frameskip ? 'enabled' : 'disabled'}"
|
|
74
|
+
puts "YJIT: #{defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? ? 'enabled' : 'disabled'}"
|
|
75
|
+
puts ""
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
emu = PSX::Emulator.new(bios_path)
|
|
79
|
+
emu.run_with_display(target_fps: target_fps, frameskip: frameskip)
|
|
80
|
+
rescue Interrupt
|
|
81
|
+
puts "\nInterrupted by user"
|
|
82
|
+
rescue => e
|
|
83
|
+
warn "\nError: #{e.message}"
|
|
84
|
+
warn e.backtrace.first(5).join("\n")
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
data/lib/psx/bios.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
class BIOS
|
|
5
|
+
SIZE = 512 * 1024 # 512 KB
|
|
6
|
+
|
|
7
|
+
def initialize(path)
|
|
8
|
+
@data = File.binread(path)
|
|
9
|
+
|
|
10
|
+
if @data.bytesize != SIZE
|
|
11
|
+
raise ArgumentError, "Invalid BIOS size: expected #{SIZE} bytes, got #{@data.bytesize}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Pre-compute 32-bit word array for fast read32 access
|
|
15
|
+
# This trades memory for speed (512KB -> 512KB + 128K integers)
|
|
16
|
+
@words = Array.new(SIZE / 4) do |i|
|
|
17
|
+
offset = i * 4
|
|
18
|
+
@data.getbyte(offset) |
|
|
19
|
+
(@data.getbyte(offset + 1) << 8) |
|
|
20
|
+
(@data.getbyte(offset + 2) << 16) |
|
|
21
|
+
(@data.getbyte(offset + 3) << 24)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read8(offset)
|
|
26
|
+
@data.getbyte(offset)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read16(offset)
|
|
30
|
+
@data.getbyte(offset) | (@data.getbyte(offset + 1) << 8)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def read32(offset)
|
|
34
|
+
# Fast path: aligned access from pre-computed array
|
|
35
|
+
if (offset & 3) == 0
|
|
36
|
+
@words[offset >> 2]
|
|
37
|
+
else
|
|
38
|
+
# Unaligned access (rare)
|
|
39
|
+
@data.getbyte(offset) |
|
|
40
|
+
(@data.getbyte(offset + 1) << 8) |
|
|
41
|
+
(@data.getbyte(offset + 2) << 16) |
|
|
42
|
+
(@data.getbyte(offset + 3) << 24)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/psx/cdrom.rb
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
# Minimal CD-ROM controller stub.
|
|
5
|
+
#
|
|
6
|
+
# Enough to convince the BIOS that there's a CD subsystem present with no
|
|
7
|
+
# disc inserted, so it proceeds to the shell screen. Handles the commands
|
|
8
|
+
# the BIOS issues during boot: GetStat, Init, GetID, Setmode, Test, etc.
|
|
9
|
+
#
|
|
10
|
+
# We don't model timing accurately. Responses are queued and delivered one
|
|
11
|
+
# per tick (called from the emulator's frame loop), respecting that the
|
|
12
|
+
# previous IRQ must be acknowledged and the previous response FIFO drained
|
|
13
|
+
# before the next response shows up.
|
|
14
|
+
class CDROM
|
|
15
|
+
# Status register (0x1F801800 read) bits
|
|
16
|
+
STAT_INDEX_MASK = 0x03
|
|
17
|
+
STAT_ADPCM_BUSY = 0x04
|
|
18
|
+
STAT_PARAMETER_FIFO_EMPTY = 0x08
|
|
19
|
+
STAT_PARAMETER_FIFO_NOT_FULL = 0x10
|
|
20
|
+
STAT_RESPONSE_FIFO_NOT_EMPTY = 0x20
|
|
21
|
+
STAT_DATA_FIFO_NOT_EMPTY = 0x40
|
|
22
|
+
STAT_BUSY = 0x80
|
|
23
|
+
|
|
24
|
+
# "stat" byte returned in command responses.
|
|
25
|
+
# Bit 4 = shell open (we say closed); bit 0 = error.
|
|
26
|
+
# We default to 0x02 (motor on, no error, no disc seek done) for no-disc.
|
|
27
|
+
DEFAULT_STAT = 0x02
|
|
28
|
+
|
|
29
|
+
def initialize(interrupts:)
|
|
30
|
+
@interrupts = interrupts
|
|
31
|
+
reset
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reset
|
|
35
|
+
@index = 0
|
|
36
|
+
@parameters = []
|
|
37
|
+
@response = []
|
|
38
|
+
@irq_enable = 0
|
|
39
|
+
@irq_flags = 0 # bits 0-2 = pending INT type (0..7)
|
|
40
|
+
@pending = [] # [[int_type, [bytes...]], ...]
|
|
41
|
+
@stat = DEFAULT_STAT
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- Bus interface ------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def read8(reg)
|
|
47
|
+
case reg & 3
|
|
48
|
+
when 0 then status
|
|
49
|
+
when 1
|
|
50
|
+
# Response FIFO
|
|
51
|
+
v = @response.shift || 0
|
|
52
|
+
v & 0xFF
|
|
53
|
+
when 2
|
|
54
|
+
# Data FIFO (no data, just return 0)
|
|
55
|
+
0
|
|
56
|
+
when 3
|
|
57
|
+
case @index
|
|
58
|
+
when 0, 2 then @irq_enable | 0xE0
|
|
59
|
+
when 1, 3 then @irq_flags | 0xE0
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def write8(reg, value)
|
|
65
|
+
v = value & 0xFF
|
|
66
|
+
case reg & 3
|
|
67
|
+
when 0
|
|
68
|
+
@index = v & 3
|
|
69
|
+
when 1
|
|
70
|
+
case @index
|
|
71
|
+
when 0 then execute_command(v)
|
|
72
|
+
# other indices: audio map / right CD audio routing — ignore
|
|
73
|
+
end
|
|
74
|
+
when 2
|
|
75
|
+
case @index
|
|
76
|
+
when 0
|
|
77
|
+
@parameters.push(v) if @parameters.size < 16
|
|
78
|
+
when 1
|
|
79
|
+
@irq_enable = v & 0x1F
|
|
80
|
+
# 2/3 = audio routing — ignore
|
|
81
|
+
end
|
|
82
|
+
when 3
|
|
83
|
+
case @index
|
|
84
|
+
when 0
|
|
85
|
+
# Request register: bit 7 BFRD enables data buffer read; bit 5 SMEN.
|
|
86
|
+
# Both unused by our stub.
|
|
87
|
+
when 1
|
|
88
|
+
# Ack IRQ flags and optionally reset parameter FIFO (bit 6).
|
|
89
|
+
@irq_flags &= ~(v & 0x1F)
|
|
90
|
+
@parameters.clear if (v & 0x40) != 0
|
|
91
|
+
# 2/3 = audio volume apply — ignore
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Advance one step in the response queue. Call this periodically (e.g.
|
|
97
|
+
# once per VBlank). One delivery per call: a response only appears once
|
|
98
|
+
# the previous one has been acknowledged.
|
|
99
|
+
def tick
|
|
100
|
+
return if @pending.empty?
|
|
101
|
+
return if @irq_flags != 0 # previous IRQ not yet acknowledged
|
|
102
|
+
return if !@response.empty? # previous response not yet drained
|
|
103
|
+
|
|
104
|
+
type, data = @pending.shift
|
|
105
|
+
@response.concat(data)
|
|
106
|
+
@irq_flags = type & 0x07
|
|
107
|
+
@interrupts.request(Interrupts::IRQ_CDROM) if (@irq_enable & @irq_flags) != 0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def status
|
|
113
|
+
s = @index & STAT_INDEX_MASK
|
|
114
|
+
s |= STAT_PARAMETER_FIFO_EMPTY if @parameters.empty?
|
|
115
|
+
s |= STAT_PARAMETER_FIFO_NOT_FULL if @parameters.size < 16
|
|
116
|
+
s |= STAT_RESPONSE_FIFO_NOT_EMPTY unless @response.empty?
|
|
117
|
+
s
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def execute_command(cmd)
|
|
121
|
+
# Drain any leftover response bytes from a prior command. Real hardware
|
|
122
|
+
# has an 8-deep response FIFO that the BIOS may not fully read after an
|
|
123
|
+
# error (e.g. INT5 on GetID with no disc returns 8 bytes but the BIOS
|
|
124
|
+
# only consumes a couple). Without this, our `tick` stays blocked
|
|
125
|
+
# forever waiting for `@response.empty?` and no further commands fire.
|
|
126
|
+
@response.clear
|
|
127
|
+
case cmd
|
|
128
|
+
when 0x01 # Getstat
|
|
129
|
+
queue(3, [@stat])
|
|
130
|
+
when 0x02 # Setloc (MM, SS, FF)
|
|
131
|
+
@parameters.clear
|
|
132
|
+
queue(3, [@stat])
|
|
133
|
+
when 0x06 # ReadN
|
|
134
|
+
queue(3, [@stat])
|
|
135
|
+
queue(5, [0x11, 0x80]) # no disc -> error
|
|
136
|
+
when 0x07 # MotorOn
|
|
137
|
+
queue(3, [@stat])
|
|
138
|
+
queue(2, [@stat])
|
|
139
|
+
when 0x08 # Stop
|
|
140
|
+
queue(3, [@stat])
|
|
141
|
+
queue(2, [@stat])
|
|
142
|
+
when 0x09 # Pause
|
|
143
|
+
queue(3, [@stat])
|
|
144
|
+
queue(2, [@stat])
|
|
145
|
+
when 0x0A # Init
|
|
146
|
+
queue(3, [@stat])
|
|
147
|
+
queue(2, [@stat])
|
|
148
|
+
when 0x0B # Mute
|
|
149
|
+
queue(3, [@stat])
|
|
150
|
+
when 0x0C # Demute
|
|
151
|
+
queue(3, [@stat])
|
|
152
|
+
when 0x0D # Setfilter
|
|
153
|
+
queue(3, [@stat])
|
|
154
|
+
when 0x0E # Setmode
|
|
155
|
+
queue(3, [@stat])
|
|
156
|
+
when 0x0F # Getparam
|
|
157
|
+
queue(3, [@stat, 0x00, 0x00, 0x00])
|
|
158
|
+
when 0x10 # GetlocL
|
|
159
|
+
queue(3, [0, 0, 0, 0, 0, 0, 0, 0])
|
|
160
|
+
when 0x11 # GetlocP
|
|
161
|
+
queue(3, [1, 1, 0, 0, 0, 0, 0, 0])
|
|
162
|
+
when 0x13 # GetTN (number of tracks)
|
|
163
|
+
queue(3, [@stat, 0x01, 0x01])
|
|
164
|
+
when 0x14 # GetTD
|
|
165
|
+
queue(3, [@stat, 0x00, 0x02])
|
|
166
|
+
when 0x15 # SeekL
|
|
167
|
+
queue(3, [@stat])
|
|
168
|
+
queue(2, [@stat])
|
|
169
|
+
when 0x16 # SeekP
|
|
170
|
+
queue(3, [@stat])
|
|
171
|
+
queue(2, [@stat])
|
|
172
|
+
when 0x19 # Test
|
|
173
|
+
sub = @parameters.shift
|
|
174
|
+
case sub
|
|
175
|
+
when 0x20 # Get BIOS date/version
|
|
176
|
+
queue(3, [0x94, 0x09, 0x19, 0xC0])
|
|
177
|
+
else
|
|
178
|
+
queue(3, [@stat])
|
|
179
|
+
end
|
|
180
|
+
when 0x1A # GetID
|
|
181
|
+
# No-disc response per nocash spec: INT3(stat) then
|
|
182
|
+
# INT5(11h, 80h, 00h, 00h, 00h, 00h, 00h, 00h). 0x80 in byte 1 is
|
|
183
|
+
# the "no disc / not ready" error code.
|
|
184
|
+
queue(3, [@stat])
|
|
185
|
+
queue(5, [0x11, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
|
186
|
+
when 0x1B # ReadS
|
|
187
|
+
queue(3, [@stat])
|
|
188
|
+
queue(5, [0x11, 0x80])
|
|
189
|
+
when 0x1E # ReadTOC
|
|
190
|
+
queue(3, [@stat])
|
|
191
|
+
queue(2, [@stat])
|
|
192
|
+
else
|
|
193
|
+
queue(3, [@stat]) # default ack to keep BIOS moving
|
|
194
|
+
end
|
|
195
|
+
@parameters.clear unless cmd == 0x19 # Test consumes its own params
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def queue(int_type, data)
|
|
199
|
+
@pending.push([int_type, data])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
data/lib/psx/cop0.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
# Coprocessor 0 - System Control
|
|
5
|
+
# Handles exceptions, interrupts, and memory management
|
|
6
|
+
class COP0
|
|
7
|
+
# Register indices
|
|
8
|
+
BPC = 3 # Breakpoint on execute
|
|
9
|
+
BDA = 5 # Breakpoint on data access
|
|
10
|
+
JUMPDEST = 6 # Jump destination (for branch delay debugging)
|
|
11
|
+
DCIC = 7 # Breakpoint control
|
|
12
|
+
BADVADDR = 8 # Bad virtual address
|
|
13
|
+
BDAM = 9 # Data access breakpoint mask
|
|
14
|
+
BPCM = 11 # Execute breakpoint mask
|
|
15
|
+
SR = 12 # Status register
|
|
16
|
+
CAUSE = 13 # Exception cause
|
|
17
|
+
EPC = 14 # Exception program counter
|
|
18
|
+
PRID = 15 # Processor ID
|
|
19
|
+
|
|
20
|
+
# Status register bits
|
|
21
|
+
SR_IEC = 0x01 # Interrupt Enable Current
|
|
22
|
+
SR_KUC = 0x02 # Kernel/User Mode Current (0=kernel)
|
|
23
|
+
SR_IEP = 0x04 # Interrupt Enable Previous
|
|
24
|
+
SR_KUP = 0x08 # Kernel/User Mode Previous
|
|
25
|
+
SR_IEO = 0x10 # Interrupt Enable Old
|
|
26
|
+
SR_KUO = 0x20 # Kernel/User Mode Old
|
|
27
|
+
SR_IM = 0xFF00 # Interrupt Mask (bits 8-15)
|
|
28
|
+
SR_ISC = 0x1_0000 # Isolate Cache
|
|
29
|
+
SR_SWC = 0x2_0000 # Swap Caches
|
|
30
|
+
SR_PZ = 0x4_0000 # Parity Zero
|
|
31
|
+
SR_CM = 0x8_0000 # Cache Miss
|
|
32
|
+
SR_PE = 0x10_0000 # Parity Error
|
|
33
|
+
SR_TS = 0x20_0000 # TLB Shutdown
|
|
34
|
+
SR_BEV = 0x40_0000 # Boot Exception Vectors
|
|
35
|
+
SR_RE = 0x200_0000 # Reverse Endianness
|
|
36
|
+
SR_CU0 = 0x1000_0000 # COP0 Enable
|
|
37
|
+
SR_CU1 = 0x2000_0000 # COP1 Enable (no FPU on PSX, but enable bit exists)
|
|
38
|
+
SR_CU2 = 0x4000_0000 # COP2 (GTE) Enable
|
|
39
|
+
SR_CU3 = 0x8000_0000 # COP3 Enable
|
|
40
|
+
|
|
41
|
+
# Exception codes (for CAUSE register bits 2-6)
|
|
42
|
+
EXC_INT = 0x00 # Interrupt
|
|
43
|
+
EXC_MOD = 0x01 # TLB modification
|
|
44
|
+
EXC_TLBL = 0x02 # TLB load
|
|
45
|
+
EXC_TLBS = 0x03 # TLB store
|
|
46
|
+
EXC_ADEL = 0x04 # Address error (load/fetch)
|
|
47
|
+
EXC_ADES = 0x05 # Address error (store)
|
|
48
|
+
EXC_IBE = 0x06 # Bus error (instruction fetch)
|
|
49
|
+
EXC_DBE = 0x07 # Bus error (data load/store)
|
|
50
|
+
EXC_SYS = 0x08 # Syscall
|
|
51
|
+
EXC_BP = 0x09 # Breakpoint
|
|
52
|
+
EXC_RI = 0x0A # Reserved instruction
|
|
53
|
+
EXC_CPU = 0x0B # Coprocessor unusable
|
|
54
|
+
EXC_OV = 0x0C # Arithmetic overflow
|
|
55
|
+
|
|
56
|
+
# Interrupt bits in CAUSE (bits 8-15, matches SR mask)
|
|
57
|
+
IRQ_SOFTWARE0 = 0x0100
|
|
58
|
+
IRQ_SOFTWARE1 = 0x0200
|
|
59
|
+
IRQ_HARDWARE = 0x0400 # External hardware interrupt (active when I_STAT & I_MASK != 0)
|
|
60
|
+
|
|
61
|
+
attr_reader :regs
|
|
62
|
+
|
|
63
|
+
def initialize
|
|
64
|
+
@regs = Array.new(32, 0)
|
|
65
|
+
@regs[PRID] = 0x0000_0002 # R3000A processor ID
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read(reg)
|
|
69
|
+
@regs[reg]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def write(reg, value)
|
|
73
|
+
case reg
|
|
74
|
+
when SR
|
|
75
|
+
# Most bits are writable
|
|
76
|
+
@regs[SR] = value & 0xF4C7_9C3F
|
|
77
|
+
when CAUSE
|
|
78
|
+
# Only software interrupt bits (8-9) are writable
|
|
79
|
+
@regs[CAUSE] = (value & 0x0300) | (@regs[CAUSE] & ~0x0300)
|
|
80
|
+
when PRID
|
|
81
|
+
# Read-only
|
|
82
|
+
else
|
|
83
|
+
@regs[reg] = value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def sr
|
|
88
|
+
@regs[SR]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def sr=(value)
|
|
92
|
+
write(SR, value)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def cause
|
|
96
|
+
@regs[CAUSE]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def epc
|
|
100
|
+
@regs[EPC]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def cache_isolated?
|
|
104
|
+
(@regs[SR] & SR_ISC) != 0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def bev?
|
|
108
|
+
(@regs[SR] & SR_BEV) != 0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def interrupts_enabled?
|
|
112
|
+
(@regs[SR] & SR_IEC) != 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def interrupt_pending?
|
|
116
|
+
# Check if any unmasked interrupt is pending
|
|
117
|
+
return false unless interrupts_enabled?
|
|
118
|
+
|
|
119
|
+
mask = (@regs[SR] >> 8) & 0xFF
|
|
120
|
+
pending = (@regs[CAUSE] >> 8) & 0xFF
|
|
121
|
+
(mask & pending) != 0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def set_hardware_irq(active)
|
|
125
|
+
if active
|
|
126
|
+
@regs[CAUSE] |= IRQ_HARDWARE
|
|
127
|
+
else
|
|
128
|
+
@regs[CAUSE] &= ~IRQ_HARDWARE
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Enter exception handler. Returns the exception vector address.
|
|
133
|
+
# `coprocessor` is the COP number (0-3) reported in CAUSE.CE for
|
|
134
|
+
# Coprocessor-Unusable exceptions.
|
|
135
|
+
def enter_exception(code, pc, in_delay_slot: false, bad_addr: nil, coprocessor: nil)
|
|
136
|
+
@regs[EPC] = in_delay_slot ? pc - 4 : pc
|
|
137
|
+
@regs[BADVADDR] = bad_addr if bad_addr
|
|
138
|
+
|
|
139
|
+
# CAUSE: preserve interrupt-pending bits, set ExcCode, BD, and CE.
|
|
140
|
+
ce = coprocessor ? ((coprocessor & 0x3) << 28) : 0
|
|
141
|
+
@regs[CAUSE] = (@regs[CAUSE] & 0x0000_FF00) |
|
|
142
|
+
(code << 2) |
|
|
143
|
+
ce |
|
|
144
|
+
(in_delay_slot ? 0x8000_0000 : 0)
|
|
145
|
+
|
|
146
|
+
# Shift IE/KU stack: current -> previous -> old.
|
|
147
|
+
mode = @regs[SR] & 0x3F
|
|
148
|
+
@regs[SR] = (@regs[SR] & ~0x3F) | ((mode << 2) & 0x3F)
|
|
149
|
+
|
|
150
|
+
bev? ? 0xBFC0_0180 : 0x8000_0080
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Return from exception (RFE instruction)
|
|
154
|
+
def return_from_exception
|
|
155
|
+
# Shift interrupt enable/kernel mode bits back
|
|
156
|
+
# Old -> Previous -> Current
|
|
157
|
+
mode = @regs[SR] & 0x3F
|
|
158
|
+
@regs[SR] = (@regs[SR] & ~0x0F) | ((mode >> 2) & 0x0F)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|