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.
data/lib/psx/disasm.rb ADDED
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PSX
4
+ module Disasm
5
+ REG_NAMES = %w[
6
+ zero at v0 v1 a0 a1 a2 a3
7
+ t0 t1 t2 t3 t4 t5 t6 t7
8
+ s0 s1 s2 s3 s4 s5 s6 s7
9
+ t8 t9 k0 k1 gp sp fp ra
10
+ ].freeze
11
+
12
+ COP0_NAMES = {
13
+ 3 => "BPC", 5 => "BDA", 6 => "JUMPDEST", 7 => "DCIC",
14
+ 8 => "BadVAddr", 9 => "BDAM", 11 => "BPCM", 12 => "SR",
15
+ 13 => "CAUSE", 14 => "EPC", 15 => "PRId"
16
+ }.freeze
17
+
18
+ class << self
19
+ def disassemble(pc, instruction)
20
+ return format("%08X: %08X nop", pc, instruction) if instruction == 0
21
+
22
+ opcode = (instruction >> 26) & 0x3F
23
+ text = decode(pc, instruction, opcode)
24
+ format("%08X: %08X %s", pc, instruction, text)
25
+ end
26
+
27
+ private
28
+
29
+ def decode(pc, instruction, opcode)
30
+ case opcode
31
+ when 0x00 then decode_special(instruction)
32
+ when 0x01 then decode_bcondz(pc, instruction)
33
+ when 0x02 then decode_j("j", pc, instruction)
34
+ when 0x03 then decode_j("jal", pc, instruction)
35
+ when 0x04 then decode_branch("beq", pc, instruction)
36
+ when 0x05 then decode_branch("bne", pc, instruction)
37
+ when 0x06 then decode_branch_z("blez", pc, instruction)
38
+ when 0x07 then decode_branch_z("bgtz", pc, instruction)
39
+ when 0x08 then decode_imm_arith("addi", instruction)
40
+ when 0x09 then decode_imm_arith("addiu", instruction)
41
+ when 0x0A then decode_imm_arith("slti", instruction)
42
+ when 0x0B then decode_imm_arith("sltiu", instruction)
43
+ when 0x0C then decode_imm_logic("andi", instruction)
44
+ when 0x0D then decode_imm_logic("ori", instruction)
45
+ when 0x0E then decode_imm_logic("xori", instruction)
46
+ when 0x0F then decode_lui(instruction)
47
+ when 0x10 then decode_cop0(instruction)
48
+ when 0x12 then decode_cop2(instruction)
49
+ when 0x20 then decode_load("lb", instruction)
50
+ when 0x21 then decode_load("lh", instruction)
51
+ when 0x22 then decode_load("lwl", instruction)
52
+ when 0x23 then decode_load("lw", instruction)
53
+ when 0x24 then decode_load("lbu", instruction)
54
+ when 0x25 then decode_load("lhu", instruction)
55
+ when 0x26 then decode_load("lwr", instruction)
56
+ when 0x28 then decode_store("sb", instruction)
57
+ when 0x29 then decode_store("sh", instruction)
58
+ when 0x2A then decode_store("swl", instruction)
59
+ when 0x2B then decode_store("sw", instruction)
60
+ when 0x2E then decode_store("swr", instruction)
61
+ else
62
+ format("??? (op=%02X)", opcode)
63
+ end
64
+ end
65
+
66
+ def decode_special(instruction)
67
+ funct = instruction & 0x3F
68
+ rs = (instruction >> 21) & 0x1F
69
+ rt = (instruction >> 16) & 0x1F
70
+ rd = (instruction >> 11) & 0x1F
71
+ shamt = (instruction >> 6) & 0x1F
72
+
73
+ case funct
74
+ when 0x00 then format("sll %s, %s, %d", reg(rd), reg(rt), shamt)
75
+ when 0x02 then format("srl %s, %s, %d", reg(rd), reg(rt), shamt)
76
+ when 0x03 then format("sra %s, %s, %d", reg(rd), reg(rt), shamt)
77
+ when 0x04 then format("sllv %s, %s, %s", reg(rd), reg(rt), reg(rs))
78
+ when 0x06 then format("srlv %s, %s, %s", reg(rd), reg(rt), reg(rs))
79
+ when 0x07 then format("srav %s, %s, %s", reg(rd), reg(rt), reg(rs))
80
+ when 0x08 then format("jr %s", reg(rs))
81
+ when 0x09 then format("jalr %s, %s", reg(rd), reg(rs))
82
+ when 0x0C then "syscall"
83
+ when 0x0D then "break"
84
+ when 0x10 then format("mfhi %s", reg(rd))
85
+ when 0x11 then format("mthi %s", reg(rs))
86
+ when 0x12 then format("mflo %s", reg(rd))
87
+ when 0x13 then format("mtlo %s", reg(rs))
88
+ when 0x18 then format("mult %s, %s", reg(rs), reg(rt))
89
+ when 0x19 then format("multu %s, %s", reg(rs), reg(rt))
90
+ when 0x1A then format("div %s, %s", reg(rs), reg(rt))
91
+ when 0x1B then format("divu %s, %s", reg(rs), reg(rt))
92
+ when 0x20 then format("add %s, %s, %s", reg(rd), reg(rs), reg(rt))
93
+ when 0x21 then format("addu %s, %s, %s", reg(rd), reg(rs), reg(rt))
94
+ when 0x22 then format("sub %s, %s, %s", reg(rd), reg(rs), reg(rt))
95
+ when 0x23 then format("subu %s, %s, %s", reg(rd), reg(rs), reg(rt))
96
+ when 0x24 then format("and %s, %s, %s", reg(rd), reg(rs), reg(rt))
97
+ when 0x25 then format("or %s, %s, %s", reg(rd), reg(rs), reg(rt))
98
+ when 0x26 then format("xor %s, %s, %s", reg(rd), reg(rs), reg(rt))
99
+ when 0x27 then format("nor %s, %s, %s", reg(rd), reg(rs), reg(rt))
100
+ when 0x2A then format("slt %s, %s, %s", reg(rd), reg(rs), reg(rt))
101
+ when 0x2B then format("sltu %s, %s, %s", reg(rd), reg(rs), reg(rt))
102
+ else
103
+ format("??? (special funct=%02X)", funct)
104
+ end
105
+ end
106
+
107
+ def decode_bcondz(pc, instruction)
108
+ rs = (instruction >> 21) & 0x1F
109
+ rt = (instruction >> 16) & 0x1F
110
+ imm = instruction & 0xFFFF
111
+ offset = sign_extend16(imm) << 2
112
+ target = (pc + 4 + offset) & 0xFFFF_FFFF
113
+
114
+ link = (rt & 0x10) != 0
115
+ bgez = (rt & 0x01) != 0
116
+
117
+ name = if bgez
118
+ link ? "bgezal" : "bgez"
119
+ else
120
+ link ? "bltzal" : "bltz"
121
+ end
122
+
123
+ format("%s %s, 0x%08X", name, reg(rs), target)
124
+ end
125
+
126
+ def decode_j(name, pc, instruction)
127
+ target = instruction & 0x03FF_FFFF
128
+ addr = ((pc + 4) & 0xF000_0000) | (target << 2)
129
+ format("%s 0x%08X", name, addr)
130
+ end
131
+
132
+ def decode_branch(name, pc, instruction)
133
+ rs = (instruction >> 21) & 0x1F
134
+ rt = (instruction >> 16) & 0x1F
135
+ imm = instruction & 0xFFFF
136
+ offset = sign_extend16(imm) << 2
137
+ target = (pc + 4 + offset) & 0xFFFF_FFFF
138
+ format("%s %s, %s, 0x%08X", name, reg(rs), reg(rt), target)
139
+ end
140
+
141
+ def decode_branch_z(name, pc, instruction)
142
+ rs = (instruction >> 21) & 0x1F
143
+ imm = instruction & 0xFFFF
144
+ offset = sign_extend16(imm) << 2
145
+ target = (pc + 4 + offset) & 0xFFFF_FFFF
146
+ format("%s %s, 0x%08X", name, reg(rs), target)
147
+ end
148
+
149
+ def decode_imm_arith(name, instruction)
150
+ rs = (instruction >> 21) & 0x1F
151
+ rt = (instruction >> 16) & 0x1F
152
+ imm = sign_extend16(instruction & 0xFFFF)
153
+ format("%s %s, %s, %d", name, reg(rt), reg(rs), imm)
154
+ end
155
+
156
+ def decode_imm_logic(name, instruction)
157
+ rs = (instruction >> 21) & 0x1F
158
+ rt = (instruction >> 16) & 0x1F
159
+ imm = instruction & 0xFFFF
160
+ format("%s %s, %s, 0x%04X", name, reg(rt), reg(rs), imm)
161
+ end
162
+
163
+ def decode_lui(instruction)
164
+ rt = (instruction >> 16) & 0x1F
165
+ imm = instruction & 0xFFFF
166
+ format("lui %s, 0x%04X", reg(rt), imm)
167
+ end
168
+
169
+ def decode_load(name, instruction)
170
+ rs = (instruction >> 21) & 0x1F
171
+ rt = (instruction >> 16) & 0x1F
172
+ imm = sign_extend16(instruction & 0xFFFF)
173
+ format("%s %s, %d(%s)", name, reg(rt), imm, reg(rs))
174
+ end
175
+
176
+ def decode_store(name, instruction)
177
+ rs = (instruction >> 21) & 0x1F
178
+ rt = (instruction >> 16) & 0x1F
179
+ imm = sign_extend16(instruction & 0xFFFF)
180
+ format("%s %s, %d(%s)", name, reg(rt), imm, reg(rs))
181
+ end
182
+
183
+ def decode_cop0(instruction)
184
+ cop_op = (instruction >> 21) & 0x1F
185
+ rt = (instruction >> 16) & 0x1F
186
+ rd = (instruction >> 11) & 0x1F
187
+
188
+ case cop_op
189
+ when 0x00 then format("mfc0 %s, %s", reg(rt), cop0_reg(rd))
190
+ when 0x04 then format("mtc0 %s, %s", reg(rt), cop0_reg(rd))
191
+ when 0x10 then "rfe"
192
+ else
193
+ format("cop0 (op=%02X)", cop_op)
194
+ end
195
+ end
196
+
197
+ def decode_cop2(instruction)
198
+ cop_op = (instruction >> 21) & 0x1F
199
+ rt = (instruction >> 16) & 0x1F
200
+ rd = (instruction >> 11) & 0x1F
201
+
202
+ case cop_op
203
+ when 0x00 then format("mfc2 %s, $%d", reg(rt), rd)
204
+ when 0x02 then format("cfc2 %s, $%d", reg(rt), rd)
205
+ when 0x04 then format("mtc2 %s, $%d", reg(rt), rd)
206
+ when 0x06 then format("ctc2 %s, $%d", reg(rt), rd)
207
+ else
208
+ cmd = instruction & 0x1FF_FFFF
209
+ format("cop2 cmd=0x%07X", cmd)
210
+ end
211
+ end
212
+
213
+ def reg(n)
214
+ "$#{REG_NAMES[n]}"
215
+ end
216
+
217
+ def cop0_reg(n)
218
+ COP0_NAMES[n] || "$#{n}"
219
+ end
220
+
221
+ def sign_extend16(val)
222
+ (val & 0x8000) != 0 ? (val - 0x1_0000) : val
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sdl2"
4
+
5
+ module PSX
6
+ class Display
7
+ WIDTH = 320
8
+ HEIGHT = 240
9
+ SCALE = 2 # 640x480 window
10
+
11
+ def initialize(title: "PSX-Ruby")
12
+ SDL2.init(SDL2::INIT_VIDEO)
13
+
14
+ @window = SDL2::Window.create(
15
+ title,
16
+ SDL2::Window::POS_CENTERED,
17
+ SDL2::Window::POS_CENTERED,
18
+ WIDTH * SCALE,
19
+ HEIGHT * SCALE,
20
+ 0
21
+ )
22
+
23
+ @renderer = @window.create_renderer(-1, SDL2::Renderer::Flags::ACCELERATED)
24
+
25
+ # Input state
26
+ @quit_requested = false
27
+ @controller_state = 0xFFFF # All buttons released (active low)
28
+
29
+ # Performance tracking
30
+ @frame_count = 0
31
+ @last_fps_time = Time.now
32
+ @fps = 0
33
+
34
+ # Cache texture reference
35
+ @texture = nil
36
+
37
+ # Pre-allocated RGBA buffer (320x240x4 = 307,200 bytes)
38
+ @rgba_buffer = "\x00".b * (WIDTH * HEIGHT * 4)
39
+ @rgba_buffer_size = WIDTH * HEIGHT * 4
40
+
41
+ # Track if we've seen real content
42
+ @has_rendered_content = false
43
+ end
44
+
45
+ def update(framebuffer)
46
+ width = framebuffer[:width]
47
+ height = framebuffer[:height]
48
+
49
+ # GPU now provides pre-packed RGBA string
50
+ rgba = framebuffer[:rgba]
51
+
52
+ # Check if framebuffer has any non-zero content (only until first content seen)
53
+ if rgba && !@has_rendered_content
54
+ # Quick check: sample a few bytes instead of iterating all
55
+ has_content = rgba.getbyte(0) != 0 || rgba.getbyte(rgba.bytesize / 2) != 0
56
+ @has_rendered_content = true if has_content
57
+ end
58
+
59
+ if rgba && @has_rendered_content
60
+ @rgba_buffer = rgba
61
+ else
62
+ # Use cached loading screen if size matches
63
+ buffer_size = width * height * 4
64
+ if @loading_screen_size != buffer_size
65
+ # Generate loading screen pattern once
66
+ rgba_arr = []
67
+ height.times do |y|
68
+ width.times do |x|
69
+ if (x % 32 < 2) || (y % 32 < 2)
70
+ rgba_arr.push(40, 40, 80, 255) # Grid lines
71
+ else
72
+ rgba_arr.push(0, 0, 40, 255) # Dark blue
73
+ end
74
+ end
75
+ end
76
+ @loading_screen = rgba_arr.pack("C*")
77
+ @loading_screen_size = buffer_size
78
+ end
79
+ @rgba_buffer = @loading_screen
80
+ end
81
+
82
+ # Create SDL surface from RGBA data
83
+ surface = SDL2::Surface.from_string(
84
+ @rgba_buffer,
85
+ width,
86
+ height,
87
+ 32, # depth (bits per pixel)
88
+ width * 4, # pitch (bytes per row)
89
+ 0x000000FF, # R mask
90
+ 0x0000FF00, # G mask
91
+ 0x00FF0000, # B mask
92
+ 0xFF000000 # A mask
93
+ )
94
+
95
+ # Create texture from surface
96
+ @texture&.destroy
97
+ @texture = @renderer.create_texture_from(surface)
98
+ surface.destroy
99
+
100
+ # Render (stretch to window size)
101
+ @renderer.copy(@texture, nil, nil)
102
+ @renderer.present
103
+
104
+ # Update FPS counter
105
+ @frame_count += 1
106
+ @total_frames = (@total_frames || 0) + 1
107
+ now = Time.now
108
+ elapsed = now - @last_fps_time
109
+ if elapsed >= 1.0
110
+ @fps = (@frame_count / elapsed).round(1)
111
+ status = has_content ? "RENDERING" : "Loading..."
112
+ @window.title = "PSX-Ruby - #{@fps} FPS - Frame #{@total_frames} - #{status}"
113
+ @frame_count = 0
114
+ @last_fps_time = now
115
+ end
116
+ end
117
+
118
+ def poll_events
119
+ while (event = SDL2::Event.poll)
120
+ case event
121
+ when SDL2::Event::Quit
122
+ @quit_requested = true
123
+ when SDL2::Event::KeyDown
124
+ handle_key(event.scancode, true)
125
+ when SDL2::Event::KeyUp
126
+ handle_key(event.scancode, false)
127
+ end
128
+ end
129
+ end
130
+
131
+ def quit_requested?
132
+ @quit_requested
133
+ end
134
+
135
+ def controller_state
136
+ @controller_state
137
+ end
138
+
139
+ def close
140
+ @texture&.destroy
141
+ @renderer.destroy
142
+ @window.destroy
143
+ end
144
+
145
+ private
146
+
147
+ # PS1 controller button bits
148
+ BUTTON_SELECT = 0
149
+ BUTTON_L3 = 1
150
+ BUTTON_R3 = 2
151
+ BUTTON_START = 3
152
+ BUTTON_UP = 4
153
+ BUTTON_RIGHT = 5
154
+ BUTTON_DOWN = 6
155
+ BUTTON_LEFT = 7
156
+ BUTTON_L2 = 8
157
+ BUTTON_R2 = 9
158
+ BUTTON_L1 = 10
159
+ BUTTON_R1 = 11
160
+ BUTTON_TRIANGLE = 12
161
+ BUTTON_CIRCLE = 13
162
+ BUTTON_CROSS = 14
163
+ BUTTON_SQUARE = 15
164
+
165
+ # Keyboard to controller mapping
166
+ KEY_MAP = {
167
+ SDL2::Key::Scan::UP => BUTTON_UP,
168
+ SDL2::Key::Scan::DOWN => BUTTON_DOWN,
169
+ SDL2::Key::Scan::LEFT => BUTTON_LEFT,
170
+ SDL2::Key::Scan::RIGHT => BUTTON_RIGHT,
171
+ SDL2::Key::Scan::Z => BUTTON_CROSS,
172
+ SDL2::Key::Scan::X => BUTTON_CIRCLE,
173
+ SDL2::Key::Scan::A => BUTTON_SQUARE,
174
+ SDL2::Key::Scan::S => BUTTON_TRIANGLE,
175
+ SDL2::Key::Scan::RETURN => BUTTON_START,
176
+ SDL2::Key::Scan::SPACE => BUTTON_SELECT,
177
+ SDL2::Key::Scan::Q => BUTTON_L1,
178
+ SDL2::Key::Scan::W => BUTTON_R1,
179
+ SDL2::Key::Scan::E => BUTTON_L2,
180
+ SDL2::Key::Scan::R => BUTTON_R2
181
+ }.freeze
182
+
183
+ def handle_key(scancode, pressed)
184
+ # Handle quit on Escape
185
+ if scancode == SDL2::Key::Scan::ESCAPE && pressed
186
+ @quit_requested = true
187
+ return
188
+ end
189
+
190
+ button = KEY_MAP[scancode]
191
+ return unless button
192
+
193
+ if pressed
194
+ @controller_state &= ~(1 << button) # Active low
195
+ else
196
+ @controller_state |= (1 << button)
197
+ end
198
+ end
199
+ end
200
+ end