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/gpu.rb
ADDED
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PSX
|
|
4
|
+
# Graphics Processing Unit
|
|
5
|
+
# Handles 2D/3D rendering to VRAM
|
|
6
|
+
class GPU
|
|
7
|
+
# VRAM dimensions
|
|
8
|
+
VRAM_WIDTH = 1024
|
|
9
|
+
VRAM_HEIGHT = 512
|
|
10
|
+
|
|
11
|
+
# Status register bits
|
|
12
|
+
STAT_TEXTURE_PAGE_X = 0x0000_000F # Texture page X base (N*64)
|
|
13
|
+
STAT_TEXTURE_PAGE_Y = 0x0000_0010 # Texture page Y base (N*256)
|
|
14
|
+
STAT_SEMI_TRANSPARENCY = 0x0000_0060 # Semi-transparency mode
|
|
15
|
+
STAT_TEXTURE_DEPTH = 0x0000_0180 # Texture color depth
|
|
16
|
+
STAT_DITHER = 0x0000_0200 # Dither enabled
|
|
17
|
+
STAT_DRAW_TO_DISPLAY = 0x0000_0400 # Drawing to display area allowed
|
|
18
|
+
STAT_SET_MASK_BIT = 0x0000_0800 # Set mask bit when drawing
|
|
19
|
+
STAT_DRAW_PIXELS = 0x0000_1000 # Draw pixels (0=always, 1=not to masked)
|
|
20
|
+
STAT_INTERLACE_FIELD = 0x0000_2000 # Interlace field
|
|
21
|
+
STAT_REVERSE_FLAG = 0x0000_4000 # Reverse flag
|
|
22
|
+
STAT_TEXTURE_DISABLE = 0x0000_8000 # Texture disable
|
|
23
|
+
STAT_HORIZONTAL_RES2 = 0x0001_0000 # Horizontal resolution 2
|
|
24
|
+
STAT_HORIZONTAL_RES1 = 0x0006_0000 # Horizontal resolution 1
|
|
25
|
+
STAT_VERTICAL_RES = 0x0008_0000 # Vertical resolution (0=240, 1=480)
|
|
26
|
+
STAT_VIDEO_MODE = 0x0010_0000 # Video mode (0=NTSC, 1=PAL)
|
|
27
|
+
STAT_COLOR_DEPTH = 0x0020_0000 # Display color depth (0=15bit, 1=24bit)
|
|
28
|
+
STAT_VERTICAL_INTERLACE = 0x0040_0000 # Vertical interlace
|
|
29
|
+
STAT_DISPLAY_ENABLE = 0x0080_0000 # Display enable (0=enabled, 1=disabled)
|
|
30
|
+
STAT_IRQ = 0x0100_0000 # IRQ flag
|
|
31
|
+
STAT_DMA_REQUEST = 0x0200_0000 # DMA request
|
|
32
|
+
STAT_CMD_READY = 0x0400_0000 # Ready for command
|
|
33
|
+
STAT_VRAM_TO_CPU_READY = 0x0800_0000 # Ready for VRAM to CPU transfer
|
|
34
|
+
STAT_DMA_READY = 0x1000_0000 # Ready for DMA
|
|
35
|
+
STAT_DMA_DIRECTION = 0x6000_0000 # DMA direction
|
|
36
|
+
STAT_DRAWING_ODD = 0x8000_0000 # Drawing odd lines (interlace)
|
|
37
|
+
|
|
38
|
+
# GP0 command types
|
|
39
|
+
CMD_NOP = 0x00
|
|
40
|
+
CMD_CLEAR_CACHE = 0x01
|
|
41
|
+
CMD_FILL_RECT = 0x02
|
|
42
|
+
CMD_POLY_BASE = 0x20 # 0x20-0x3F polygons
|
|
43
|
+
CMD_LINE_BASE = 0x40 # 0x40-0x5F lines
|
|
44
|
+
CMD_RECT_BASE = 0x60 # 0x60-0x7F rectangles
|
|
45
|
+
CMD_COPY_VRAM_VRAM = 0x80
|
|
46
|
+
CMD_COPY_CPU_VRAM = 0xA0
|
|
47
|
+
CMD_COPY_VRAM_CPU = 0xC0
|
|
48
|
+
CMD_ENV_BASE = 0xE0 # 0xE0-0xEF environment commands
|
|
49
|
+
|
|
50
|
+
attr_reader :vram
|
|
51
|
+
|
|
52
|
+
def initialize(interrupts: nil)
|
|
53
|
+
@interrupts = interrupts
|
|
54
|
+
|
|
55
|
+
# VRAM: 1024x512 16-bit pixels
|
|
56
|
+
@vram = Array.new(VRAM_WIDTH * VRAM_HEIGHT, 0)
|
|
57
|
+
|
|
58
|
+
# Status register
|
|
59
|
+
@status = STAT_CMD_READY | STAT_DMA_READY | STAT_VRAM_TO_CPU_READY
|
|
60
|
+
|
|
61
|
+
# Display settings
|
|
62
|
+
@display_enabled = false
|
|
63
|
+
@display_start_x = 0
|
|
64
|
+
@display_start_y = 0
|
|
65
|
+
@display_h_start = 0x200
|
|
66
|
+
@display_h_end = 0xC00
|
|
67
|
+
@display_v_start = 0x10
|
|
68
|
+
@display_v_end = 0x100
|
|
69
|
+
@video_mode = :ntsc
|
|
70
|
+
@horizontal_res = 320
|
|
71
|
+
@vertical_res = 240
|
|
72
|
+
@color_depth_24 = false
|
|
73
|
+
@interlaced = false
|
|
74
|
+
|
|
75
|
+
# Drawing area
|
|
76
|
+
@draw_area_left = 0
|
|
77
|
+
@draw_area_top = 0
|
|
78
|
+
@draw_area_right = 0
|
|
79
|
+
@draw_area_bottom = 0
|
|
80
|
+
@draw_offset_x = 0
|
|
81
|
+
@draw_offset_y = 0
|
|
82
|
+
|
|
83
|
+
# Texture settings
|
|
84
|
+
@texture_page_x = 0
|
|
85
|
+
@texture_page_y = 0
|
|
86
|
+
@texture_depth = 0 # 0=4bit, 1=8bit, 2=15bit
|
|
87
|
+
@semi_transparency = 0
|
|
88
|
+
@texture_window_mask_x = 0
|
|
89
|
+
@texture_window_mask_y = 0
|
|
90
|
+
@texture_window_offset_x = 0
|
|
91
|
+
@texture_window_offset_y = 0
|
|
92
|
+
@texture_disable_allow = false
|
|
93
|
+
|
|
94
|
+
# Mask settings
|
|
95
|
+
@set_mask_bit = false
|
|
96
|
+
@check_mask_bit = false
|
|
97
|
+
|
|
98
|
+
# Command buffer for multi-word commands
|
|
99
|
+
@cmd_buffer = []
|
|
100
|
+
@cmd_remaining = 0
|
|
101
|
+
@current_cmd = 0
|
|
102
|
+
|
|
103
|
+
# VRAM transfer state
|
|
104
|
+
@vram_transfer_x = 0
|
|
105
|
+
@vram_transfer_y = 0
|
|
106
|
+
@vram_transfer_start_x = 0 # Starting X for line wrap detection
|
|
107
|
+
@vram_transfer_width = 0
|
|
108
|
+
@vram_transfer_height = 0
|
|
109
|
+
@vram_transfer_count = 0
|
|
110
|
+
@vram_transfer_mode = nil # :cpu_to_vram or :vram_to_cpu
|
|
111
|
+
@vram_read_buffer = []
|
|
112
|
+
|
|
113
|
+
# Framebuffer caching
|
|
114
|
+
@framebuffer_dirty = true
|
|
115
|
+
@framebuffer_cache = nil
|
|
116
|
+
|
|
117
|
+
# DMA direction
|
|
118
|
+
@dma_direction = 0
|
|
119
|
+
|
|
120
|
+
# Interlace field (toggled by VBlank)
|
|
121
|
+
@odd_field = false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Called by emulator on VBlank to toggle interlace field
|
|
125
|
+
def vblank
|
|
126
|
+
@odd_field = !@odd_field
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def status
|
|
130
|
+
stat = @status
|
|
131
|
+
|
|
132
|
+
# Update dynamic bits
|
|
133
|
+
stat &= ~STAT_DISPLAY_ENABLE
|
|
134
|
+
stat |= STAT_DISPLAY_ENABLE unless @display_enabled
|
|
135
|
+
|
|
136
|
+
stat &= ~STAT_DMA_DIRECTION
|
|
137
|
+
stat |= (@dma_direction << 29) & STAT_DMA_DIRECTION
|
|
138
|
+
|
|
139
|
+
# Bit 31: Drawing odd lines in interlace mode
|
|
140
|
+
stat &= ~STAT_DRAWING_ODD
|
|
141
|
+
stat |= STAT_DRAWING_ODD if @odd_field
|
|
142
|
+
|
|
143
|
+
stat
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def read_data
|
|
147
|
+
if @vram_transfer_mode == :vram_to_cpu && !@vram_read_buffer.empty?
|
|
148
|
+
@vram_read_buffer.shift
|
|
149
|
+
else
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# GP0 - Rendering commands and VRAM access
|
|
155
|
+
def gp0(value)
|
|
156
|
+
if @vram_transfer_mode == :cpu_to_vram
|
|
157
|
+
vram_write_data(value)
|
|
158
|
+
return
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if @cmd_remaining > 0
|
|
162
|
+
@cmd_buffer << value
|
|
163
|
+
@cmd_remaining -= 1
|
|
164
|
+
|
|
165
|
+
if @cmd_remaining == 0
|
|
166
|
+
execute_gp0_command
|
|
167
|
+
end
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
cmd = (value >> 24) & 0xFF
|
|
172
|
+
@current_cmd = cmd
|
|
173
|
+
@cmd_buffer = [value]
|
|
174
|
+
|
|
175
|
+
case cmd
|
|
176
|
+
when CMD_NOP
|
|
177
|
+
# Do nothing
|
|
178
|
+
when CMD_CLEAR_CACHE
|
|
179
|
+
# Clear texture cache - ignore for now
|
|
180
|
+
when CMD_FILL_RECT
|
|
181
|
+
@cmd_remaining = 2
|
|
182
|
+
when 0x20..0x3F
|
|
183
|
+
# Polygons
|
|
184
|
+
@cmd_remaining = polygon_word_count(cmd) - 1
|
|
185
|
+
when 0x40..0x5F
|
|
186
|
+
# Lines
|
|
187
|
+
@cmd_remaining = line_word_count(cmd) - 1
|
|
188
|
+
when 0x60..0x7F
|
|
189
|
+
# Rectangles
|
|
190
|
+
@cmd_remaining = rectangle_word_count(cmd) - 1
|
|
191
|
+
when CMD_COPY_VRAM_VRAM
|
|
192
|
+
@cmd_remaining = 3
|
|
193
|
+
when CMD_COPY_CPU_VRAM
|
|
194
|
+
@cmd_remaining = 2
|
|
195
|
+
when CMD_COPY_VRAM_CPU
|
|
196
|
+
@cmd_remaining = 2
|
|
197
|
+
when 0xE1
|
|
198
|
+
gp0_draw_mode(value)
|
|
199
|
+
when 0xE2
|
|
200
|
+
gp0_texture_window(value)
|
|
201
|
+
when 0xE3
|
|
202
|
+
gp0_draw_area_top_left(value)
|
|
203
|
+
when 0xE4
|
|
204
|
+
gp0_draw_area_bottom_right(value)
|
|
205
|
+
when 0xE5
|
|
206
|
+
gp0_draw_offset(value)
|
|
207
|
+
when 0xE6
|
|
208
|
+
gp0_mask_settings(value)
|
|
209
|
+
else
|
|
210
|
+
# Unknown command - ignore
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
execute_gp0_command if @cmd_remaining == 0 && @cmd_buffer.length > 0
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# GP1 - Display control
|
|
217
|
+
def gp1(value)
|
|
218
|
+
cmd = (value >> 24) & 0xFF
|
|
219
|
+
|
|
220
|
+
case cmd
|
|
221
|
+
when 0x00
|
|
222
|
+
gp1_reset
|
|
223
|
+
when 0x01
|
|
224
|
+
gp1_reset_command_buffer
|
|
225
|
+
when 0x02
|
|
226
|
+
gp1_acknowledge_irq
|
|
227
|
+
when 0x03
|
|
228
|
+
gp1_display_enable(value)
|
|
229
|
+
when 0x04
|
|
230
|
+
gp1_dma_direction(value)
|
|
231
|
+
when 0x05
|
|
232
|
+
gp1_display_start(value)
|
|
233
|
+
when 0x06
|
|
234
|
+
gp1_horizontal_range(value)
|
|
235
|
+
when 0x07
|
|
236
|
+
gp1_vertical_range(value)
|
|
237
|
+
when 0x08
|
|
238
|
+
gp1_display_mode(value)
|
|
239
|
+
when 0x09
|
|
240
|
+
gp1_texture_disable(value)
|
|
241
|
+
when 0x10..0x1F
|
|
242
|
+
gp1_gpu_info(value)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def gp1_texture_disable(value)
|
|
247
|
+
@texture_disable_allow = (value & 1) != 0
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Get current framebuffer as RGBA packed binary string for display
|
|
251
|
+
# Returns hash with :width, :height, :rgba (binary string)
|
|
252
|
+
# Uses caching to avoid regenerating unchanged frames
|
|
253
|
+
def framebuffer
|
|
254
|
+
# Return cached framebuffer if VRAM hasn't changed
|
|
255
|
+
if !@framebuffer_dirty && @framebuffer_cache &&
|
|
256
|
+
@framebuffer_cache[:width] == @horizontal_res &&
|
|
257
|
+
@framebuffer_cache[:height] == @vertical_res
|
|
258
|
+
return @framebuffer_cache
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
width = @horizontal_res
|
|
262
|
+
height = @vertical_res
|
|
263
|
+
num_pixels = width * height
|
|
264
|
+
|
|
265
|
+
# Build RGBA array and pack once (faster than building string)
|
|
266
|
+
rgba_arr = Array.new(num_pixels * 4)
|
|
267
|
+
dst_i = 0
|
|
268
|
+
|
|
269
|
+
height.times do |y|
|
|
270
|
+
vram_row = (@display_start_y + y) * VRAM_WIDTH + @display_start_x
|
|
271
|
+
width.times do |x|
|
|
272
|
+
color16 = @vram[vram_row + x] || 0
|
|
273
|
+
|
|
274
|
+
# Convert 15-bit to 24-bit RGB
|
|
275
|
+
rgba_arr[dst_i] = (color16 & 0x001F) << 3 # R
|
|
276
|
+
rgba_arr[dst_i + 1] = (color16 & 0x03E0) >> 2 # G
|
|
277
|
+
rgba_arr[dst_i + 2] = (color16 & 0x7C00) >> 7 # B
|
|
278
|
+
rgba_arr[dst_i + 3] = 255 # A
|
|
279
|
+
dst_i += 4
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
@framebuffer_dirty = false
|
|
284
|
+
@framebuffer_cache = { width: width, height: height, rgba: rgba_arr.pack("C*") }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Mark framebuffer as needing regeneration (call when VRAM is modified)
|
|
288
|
+
def mark_dirty
|
|
289
|
+
@framebuffer_dirty = true
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
# GP0 command execution
|
|
295
|
+
def execute_gp0_command
|
|
296
|
+
case @current_cmd
|
|
297
|
+
when CMD_FILL_RECT
|
|
298
|
+
mark_dirty
|
|
299
|
+
gp0_fill_rect
|
|
300
|
+
when 0x20..0x3F
|
|
301
|
+
mark_dirty
|
|
302
|
+
gp0_polygon
|
|
303
|
+
when 0x40..0x5F
|
|
304
|
+
mark_dirty
|
|
305
|
+
gp0_line
|
|
306
|
+
when 0x60..0x7F
|
|
307
|
+
mark_dirty
|
|
308
|
+
gp0_rectangle
|
|
309
|
+
when CMD_COPY_VRAM_VRAM
|
|
310
|
+
mark_dirty
|
|
311
|
+
gp0_copy_vram_vram
|
|
312
|
+
when CMD_COPY_CPU_VRAM
|
|
313
|
+
mark_dirty
|
|
314
|
+
gp0_copy_cpu_vram
|
|
315
|
+
when CMD_COPY_VRAM_CPU
|
|
316
|
+
gp0_copy_vram_cpu # Reading from VRAM doesn't modify it
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
@cmd_buffer = []
|
|
320
|
+
@current_cmd = 0
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Word counts for multi-word commands
|
|
324
|
+
def polygon_word_count(cmd)
|
|
325
|
+
# Bit 0: Gouraud (adds colors for each vertex)
|
|
326
|
+
# Bit 2: Textured (adds UV + clut for each vertex)
|
|
327
|
+
# Bit 3: Quad (4 vertices instead of 3)
|
|
328
|
+
gouraud = (cmd & 0x10) != 0
|
|
329
|
+
textured = (cmd & 0x04) != 0
|
|
330
|
+
quad = (cmd & 0x08) != 0
|
|
331
|
+
|
|
332
|
+
vertices = quad ? 4 : 3
|
|
333
|
+
words = 1 # Command + color
|
|
334
|
+
|
|
335
|
+
if textured
|
|
336
|
+
# Each vertex has position + texcoord
|
|
337
|
+
words += vertices * 2
|
|
338
|
+
words += 1 if gouraud # Extra colors if gouraud
|
|
339
|
+
words += (vertices - 1) if gouraud # Colors for other vertices
|
|
340
|
+
else
|
|
341
|
+
words += vertices # Just positions
|
|
342
|
+
words += (vertices - 1) if gouraud # Colors for other vertices
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
words
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def line_word_count(cmd)
|
|
349
|
+
# Bit 3: Polyline
|
|
350
|
+
# Bit 4: Gouraud
|
|
351
|
+
gouraud = (cmd & 0x10) != 0
|
|
352
|
+
polyline = (cmd & 0x08) != 0
|
|
353
|
+
|
|
354
|
+
if polyline
|
|
355
|
+
# Polyline - variable length, terminated by 0x5555_5555 or 0x5000_5000
|
|
356
|
+
# For now, just handle 2-point lines
|
|
357
|
+
gouraud ? 4 : 3
|
|
358
|
+
else
|
|
359
|
+
gouraud ? 4 : 3
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def rectangle_word_count(cmd)
|
|
364
|
+
# Bits 3-4: Size (0=variable, 1=1x1, 2=8x8, 3=16x16)
|
|
365
|
+
# Bit 2: Textured
|
|
366
|
+
size = (cmd >> 3) & 0x3
|
|
367
|
+
textured = (cmd & 0x04) != 0
|
|
368
|
+
|
|
369
|
+
words = 2 # Command+color, position
|
|
370
|
+
words += 1 if textured # Texcoord
|
|
371
|
+
words += 1 if size == 0 # Variable size needs dimensions
|
|
372
|
+
|
|
373
|
+
words
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# GP0 Drawing commands
|
|
377
|
+
def gp0_fill_rect
|
|
378
|
+
color = @cmd_buffer[0] & 0x00FF_FFFF
|
|
379
|
+
pos = @cmd_buffer[1]
|
|
380
|
+
size = @cmd_buffer[2]
|
|
381
|
+
|
|
382
|
+
x = pos & 0x3F0 # Aligned to 16 pixels
|
|
383
|
+
y = (pos >> 16) & 0x1FF
|
|
384
|
+
w = ((size & 0x3FF) + 0xF) & ~0xF # Round up to 16
|
|
385
|
+
h = (size >> 16) & 0x1FF
|
|
386
|
+
|
|
387
|
+
r = color & 0xFF
|
|
388
|
+
g = (color >> 8) & 0xFF
|
|
389
|
+
b = (color >> 16) & 0xFF
|
|
390
|
+
pixel = rgb_to_vram(r, g, b)
|
|
391
|
+
|
|
392
|
+
h.times do |dy|
|
|
393
|
+
w.times do |dx|
|
|
394
|
+
vx = (x + dx) % VRAM_WIDTH
|
|
395
|
+
vy = (y + dy) % VRAM_HEIGHT
|
|
396
|
+
@vram[vy * VRAM_WIDTH + vx] = pixel
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def gp0_polygon
|
|
402
|
+
cmd = @current_cmd
|
|
403
|
+
gouraud = (cmd & 0x10) != 0
|
|
404
|
+
textured = (cmd & 0x04) != 0
|
|
405
|
+
quad = (cmd & 0x08) != 0
|
|
406
|
+
semi_transparent = (cmd & 0x02) != 0
|
|
407
|
+
raw_texture = (cmd & 0x01) != 0
|
|
408
|
+
|
|
409
|
+
vertices = quad ? 4 : 3
|
|
410
|
+
|
|
411
|
+
# Parse vertices
|
|
412
|
+
points = []
|
|
413
|
+
colors = []
|
|
414
|
+
texcoords = []
|
|
415
|
+
|
|
416
|
+
idx = 0
|
|
417
|
+
vertices.times do |v|
|
|
418
|
+
# First word always has color (or just first vertex if not gouraud)
|
|
419
|
+
if v == 0 || gouraud
|
|
420
|
+
c = @cmd_buffer[idx]
|
|
421
|
+
colors << { r: c & 0xFF, g: (c >> 8) & 0xFF, b: (c >> 16) & 0xFF }
|
|
422
|
+
idx += 1
|
|
423
|
+
else
|
|
424
|
+
colors << colors[0]
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Position
|
|
428
|
+
pos = @cmd_buffer[idx]
|
|
429
|
+
x = (pos & 0xFFFF)
|
|
430
|
+
x = x - 0x10000 if x >= 0x8000 # Sign extend
|
|
431
|
+
y = (pos >> 16) & 0xFFFF
|
|
432
|
+
y = y - 0x10000 if y >= 0x8000
|
|
433
|
+
points << { x: x + @draw_offset_x, y: y + @draw_offset_y }
|
|
434
|
+
idx += 1
|
|
435
|
+
|
|
436
|
+
# Texcoord
|
|
437
|
+
if textured
|
|
438
|
+
tc = @cmd_buffer[idx]
|
|
439
|
+
texcoords << { u: tc & 0xFF, v: (tc >> 8) & 0xFF, clut: (tc >> 16) & 0xFFFF }
|
|
440
|
+
idx += 1
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Draw triangles
|
|
445
|
+
if textured
|
|
446
|
+
# Get CLUT from first texcoord (bits 16-31)
|
|
447
|
+
clut = texcoords[0][:clut]
|
|
448
|
+
|
|
449
|
+
# Get texture page from second texcoord (bits 16-31)
|
|
450
|
+
# This overrides the global texture page settings for this primitive
|
|
451
|
+
tpage = texcoords[1][:clut] # Reusing :clut field, it's actually tpage for vertex 1
|
|
452
|
+
tex_page_x = (tpage & 0x0F) * 64
|
|
453
|
+
tex_page_y = ((tpage >> 4) & 0x01) * 256
|
|
454
|
+
tex_depth = (tpage >> 7) & 0x03
|
|
455
|
+
|
|
456
|
+
# A textured polygon's tpage parameter also rewrites GPUSTAT bits 0-8
|
|
457
|
+
# (and, when GP1 09 allowed it, bit 15 from tpage bit 11). Bits 9-10
|
|
458
|
+
# remain whatever GP0 E1 set them to (verified by ps1-tests gpu/gp0-e1).
|
|
459
|
+
@status = (@status & ~0x01FF) | (tpage & 0x01FF)
|
|
460
|
+
tex_disable = (@texture_disable_allow && (tpage & (1 << 11)) != 0) ? 0x8000 : 0
|
|
461
|
+
@status = (@status & ~0x8000) | tex_disable
|
|
462
|
+
|
|
463
|
+
if quad
|
|
464
|
+
draw_textured_triangle(points[0], points[1], points[2],
|
|
465
|
+
texcoords[0], texcoords[1], texcoords[2],
|
|
466
|
+
colors[0], clut, tex_page_x, tex_page_y, tex_depth, raw_texture)
|
|
467
|
+
draw_textured_triangle(points[1], points[2], points[3],
|
|
468
|
+
texcoords[1], texcoords[2], texcoords[3],
|
|
469
|
+
colors[1], clut, tex_page_x, tex_page_y, tex_depth, raw_texture)
|
|
470
|
+
else
|
|
471
|
+
draw_textured_triangle(points[0], points[1], points[2],
|
|
472
|
+
texcoords[0], texcoords[1], texcoords[2],
|
|
473
|
+
colors[0], clut, tex_page_x, tex_page_y, tex_depth, raw_texture)
|
|
474
|
+
end
|
|
475
|
+
else
|
|
476
|
+
if quad
|
|
477
|
+
draw_triangle(points[0], points[1], points[2], colors[0], colors[1], colors[2], gouraud)
|
|
478
|
+
draw_triangle(points[1], points[2], points[3], colors[1], colors[2], colors[3], gouraud)
|
|
479
|
+
else
|
|
480
|
+
draw_triangle(points[0], points[1], points[2], colors[0], colors[1], colors[2], gouraud)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def gp0_line
|
|
486
|
+
cmd = @current_cmd
|
|
487
|
+
gouraud = (cmd & 0x10) != 0
|
|
488
|
+
|
|
489
|
+
c0 = @cmd_buffer[0]
|
|
490
|
+
color0 = { r: c0 & 0xFF, g: (c0 >> 8) & 0xFF, b: (c0 >> 16) & 0xFF }
|
|
491
|
+
|
|
492
|
+
pos0 = @cmd_buffer[1]
|
|
493
|
+
x0 = (pos0 & 0xFFFF)
|
|
494
|
+
x0 = x0 - 0x10000 if x0 >= 0x8000
|
|
495
|
+
y0 = (pos0 >> 16) & 0xFFFF
|
|
496
|
+
y0 = y0 - 0x10000 if y0 >= 0x8000
|
|
497
|
+
|
|
498
|
+
if gouraud
|
|
499
|
+
c1 = @cmd_buffer[2]
|
|
500
|
+
color1 = { r: c1 & 0xFF, g: (c1 >> 8) & 0xFF, b: (c1 >> 16) & 0xFF }
|
|
501
|
+
pos1 = @cmd_buffer[3]
|
|
502
|
+
else
|
|
503
|
+
color1 = color0
|
|
504
|
+
pos1 = @cmd_buffer[2]
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
x1 = (pos1 & 0xFFFF)
|
|
508
|
+
x1 = x1 - 0x10000 if x1 >= 0x8000
|
|
509
|
+
y1 = (pos1 >> 16) & 0xFFFF
|
|
510
|
+
y1 = y1 - 0x10000 if y1 >= 0x8000
|
|
511
|
+
|
|
512
|
+
draw_line(
|
|
513
|
+
x0 + @draw_offset_x, y0 + @draw_offset_y,
|
|
514
|
+
x1 + @draw_offset_x, y1 + @draw_offset_y,
|
|
515
|
+
color0, color1, gouraud
|
|
516
|
+
)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def gp0_rectangle
|
|
520
|
+
cmd = @current_cmd
|
|
521
|
+
textured = (cmd & 0x04) != 0
|
|
522
|
+
raw_texture = (cmd & 0x01) != 0
|
|
523
|
+
size_mode = (cmd >> 3) & 0x3
|
|
524
|
+
|
|
525
|
+
c = @cmd_buffer[0]
|
|
526
|
+
color = { r: c & 0xFF, g: (c >> 8) & 0xFF, b: (c >> 16) & 0xFF }
|
|
527
|
+
|
|
528
|
+
pos = @cmd_buffer[1]
|
|
529
|
+
x = (pos & 0xFFFF)
|
|
530
|
+
x = x - 0x10000 if x >= 0x8000
|
|
531
|
+
y = (pos >> 16) & 0xFFFF
|
|
532
|
+
y = y - 0x10000 if y >= 0x8000
|
|
533
|
+
x += @draw_offset_x
|
|
534
|
+
y += @draw_offset_y
|
|
535
|
+
|
|
536
|
+
idx = 2
|
|
537
|
+
tex_u = 0
|
|
538
|
+
tex_v = 0
|
|
539
|
+
clut_x = 0
|
|
540
|
+
clut_y = 0
|
|
541
|
+
|
|
542
|
+
if textured
|
|
543
|
+
tc = @cmd_buffer[idx]
|
|
544
|
+
tex_u = tc & 0xFF
|
|
545
|
+
tex_v = (tc >> 8) & 0xFF
|
|
546
|
+
clut = (tc >> 16) & 0xFFFF
|
|
547
|
+
clut_x = (clut & 0x3F) * 16
|
|
548
|
+
clut_y = (clut >> 6) & 0x1FF
|
|
549
|
+
idx += 1
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
case size_mode
|
|
553
|
+
when 0 # Variable
|
|
554
|
+
dims = @cmd_buffer[idx]
|
|
555
|
+
w = dims & 0xFFFF
|
|
556
|
+
h = (dims >> 16) & 0xFFFF
|
|
557
|
+
when 1 # 1x1
|
|
558
|
+
w = 1
|
|
559
|
+
h = 1
|
|
560
|
+
when 2 # 8x8
|
|
561
|
+
w = 8
|
|
562
|
+
h = 8
|
|
563
|
+
when 3 # 16x16
|
|
564
|
+
w = 16
|
|
565
|
+
h = 16
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
if textured
|
|
569
|
+
draw_textured_rect(x, y, w, h, tex_u, tex_v, clut_x, clut_y, color, raw_texture)
|
|
570
|
+
else
|
|
571
|
+
draw_rect(x, y, w, h, color)
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def gp0_copy_vram_vram
|
|
576
|
+
src = @cmd_buffer[1]
|
|
577
|
+
dst = @cmd_buffer[2]
|
|
578
|
+
size = @cmd_buffer[3]
|
|
579
|
+
|
|
580
|
+
src_x = src & 0x3FF
|
|
581
|
+
src_y = (src >> 16) & 0x1FF
|
|
582
|
+
dst_x = dst & 0x3FF
|
|
583
|
+
dst_y = (dst >> 16) & 0x1FF
|
|
584
|
+
w = (((size & 0x3FF) - 1) & 0x3FF) + 1
|
|
585
|
+
h = ((((size >> 16) & 0x1FF) - 1) & 0x1FF) + 1
|
|
586
|
+
|
|
587
|
+
h.times do |dy|
|
|
588
|
+
w.times do |dx|
|
|
589
|
+
sx = (src_x + dx) % VRAM_WIDTH
|
|
590
|
+
sy = (src_y + dy) % VRAM_HEIGHT
|
|
591
|
+
dx2 = (dst_x + dx) % VRAM_WIDTH
|
|
592
|
+
dy2 = (dst_y + dy) % VRAM_HEIGHT
|
|
593
|
+
@vram[dy2 * VRAM_WIDTH + dx2] = @vram[sy * VRAM_WIDTH + sx]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def gp0_copy_cpu_vram
|
|
599
|
+
pos = @cmd_buffer[1]
|
|
600
|
+
size = @cmd_buffer[2]
|
|
601
|
+
|
|
602
|
+
@vram_transfer_x = pos & 0x3FF
|
|
603
|
+
@vram_transfer_y = (pos >> 16) & 0x1FF
|
|
604
|
+
@vram_transfer_start_x = @vram_transfer_x # Remember starting X for line wrap
|
|
605
|
+
w = (((size & 0xFFFF) - 1) & 0x3FF) + 1
|
|
606
|
+
h = ((((size >> 16) & 0xFFFF) - 1) & 0x1FF) + 1
|
|
607
|
+
@vram_transfer_width = w
|
|
608
|
+
@vram_transfer_height = h
|
|
609
|
+
@vram_transfer_count = ((w * h + 1) & ~1) / 2 # Words to transfer
|
|
610
|
+
@vram_transfer_mode = :cpu_to_vram
|
|
611
|
+
|
|
612
|
+
@status &= ~STAT_CMD_READY
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def gp0_copy_vram_cpu
|
|
616
|
+
pos = @cmd_buffer[1]
|
|
617
|
+
size = @cmd_buffer[2]
|
|
618
|
+
|
|
619
|
+
x = pos & 0x3FF
|
|
620
|
+
y = (pos >> 16) & 0x1FF
|
|
621
|
+
w = (((size & 0xFFFF) - 1) & 0x3FF) + 1
|
|
622
|
+
h = ((((size >> 16) & 0xFFFF) - 1) & 0x1FF) + 1
|
|
623
|
+
|
|
624
|
+
# Read VRAM into buffer
|
|
625
|
+
@vram_read_buffer = []
|
|
626
|
+
h.times do |dy|
|
|
627
|
+
row_start = ((y + dy) % VRAM_HEIGHT) * VRAM_WIDTH
|
|
628
|
+
x_pos = x
|
|
629
|
+
(w / 2.0).ceil.times do
|
|
630
|
+
p0 = @vram[row_start + (x_pos % VRAM_WIDTH)]
|
|
631
|
+
x_pos += 1
|
|
632
|
+
p1 = @vram[row_start + (x_pos % VRAM_WIDTH)]
|
|
633
|
+
x_pos += 1
|
|
634
|
+
@vram_read_buffer << ((p1 << 16) | p0)
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
@vram_transfer_mode = :vram_to_cpu
|
|
639
|
+
@status |= STAT_VRAM_TO_CPU_READY
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def vram_write_data(value)
|
|
643
|
+
return if @vram_transfer_count <= 0
|
|
644
|
+
|
|
645
|
+
vram_write_pixel(value & 0xFFFF)
|
|
646
|
+
vram_write_pixel((value >> 16) & 0xFFFF)
|
|
647
|
+
|
|
648
|
+
@vram_transfer_count -= 1
|
|
649
|
+
|
|
650
|
+
if @vram_transfer_count <= 0
|
|
651
|
+
@vram_transfer_mode = nil
|
|
652
|
+
@status |= STAT_CMD_READY
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def vram_write_pixel(pixel)
|
|
657
|
+
idx = @vram_transfer_y * VRAM_WIDTH + (@vram_transfer_x % VRAM_WIDTH)
|
|
658
|
+
|
|
659
|
+
# Mask bit settings apply to CPU-to-VRAM blits (verified by
|
|
660
|
+
# ps1-tests gpu/mask-bit).
|
|
661
|
+
unless @check_mask_bit && (@vram[idx] & 0x8000) != 0
|
|
662
|
+
@vram[idx] = pixel | (@set_mask_bit ? 0x8000 : 0)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
@vram_transfer_x += 1
|
|
666
|
+
if @vram_transfer_x >= @vram_transfer_start_x + @vram_transfer_width
|
|
667
|
+
@vram_transfer_x = @vram_transfer_start_x
|
|
668
|
+
@vram_transfer_y = (@vram_transfer_y + 1) % VRAM_HEIGHT
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# GP0 Environment commands
|
|
673
|
+
def gp0_draw_mode(value)
|
|
674
|
+
@texture_page_x = (value & 0x0F) * 64
|
|
675
|
+
@texture_page_y = ((value >> 4) & 0x01) * 256
|
|
676
|
+
@semi_transparency = (value >> 5) & 0x03
|
|
677
|
+
@texture_depth = (value >> 7) & 0x03
|
|
678
|
+
|
|
679
|
+
@status = (@status & ~0x07FF) | (value & 0x07FF)
|
|
680
|
+
|
|
681
|
+
# GPUSTAT bit 15 (Texture Disable) comes from E1 bit 11, but only when
|
|
682
|
+
# GP1 09 ("Allow Texture Disable") has been set. With allow=false an E1
|
|
683
|
+
# write force-clears bit 15 (verified by ps1-tests gpu/gp0-e1).
|
|
684
|
+
bit15 = (@texture_disable_allow && (value & (1 << 11)) != 0) ? 0x8000 : 0
|
|
685
|
+
@status = (@status & ~0x8000) | bit15
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def gp0_texture_window(value)
|
|
689
|
+
# Store raw values (in 8-texel units), don't pre-multiply
|
|
690
|
+
@texture_window_mask_x = value & 0x1F
|
|
691
|
+
@texture_window_mask_y = (value >> 5) & 0x1F
|
|
692
|
+
@texture_window_offset_x = (value >> 10) & 0x1F
|
|
693
|
+
@texture_window_offset_y = (value >> 15) & 0x1F
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def gp0_draw_area_top_left(value)
|
|
697
|
+
@draw_area_left = value & 0x3FF
|
|
698
|
+
@draw_area_top = (value >> 10) & 0x1FF
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def gp0_draw_area_bottom_right(value)
|
|
702
|
+
@draw_area_right = value & 0x3FF
|
|
703
|
+
@draw_area_bottom = (value >> 10) & 0x1FF
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def gp0_draw_offset(value)
|
|
707
|
+
x = value & 0x7FF
|
|
708
|
+
y = (value >> 11) & 0x7FF
|
|
709
|
+
@draw_offset_x = x >= 0x400 ? x - 0x800 : x
|
|
710
|
+
@draw_offset_y = y >= 0x400 ? y - 0x800 : y
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def gp0_mask_settings(value)
|
|
714
|
+
@set_mask_bit = (value & 0x01) != 0
|
|
715
|
+
@check_mask_bit = (value & 0x02) != 0
|
|
716
|
+
|
|
717
|
+
@status = (@status & ~0x1800) | ((value & 0x03) << 11)
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# GP1 commands
|
|
721
|
+
def gp1_reset
|
|
722
|
+
@status = STAT_CMD_READY | STAT_DMA_READY | STAT_VRAM_TO_CPU_READY | STAT_DISPLAY_ENABLE
|
|
723
|
+
@display_enabled = false
|
|
724
|
+
@dma_direction = 0
|
|
725
|
+
@display_start_x = 0
|
|
726
|
+
@display_start_y = 0
|
|
727
|
+
@display_h_start = 0x200
|
|
728
|
+
@display_h_end = 0xC00
|
|
729
|
+
@display_v_start = 0x10
|
|
730
|
+
@display_v_end = 0x100
|
|
731
|
+
@draw_area_left = 0
|
|
732
|
+
@draw_area_top = 0
|
|
733
|
+
@draw_area_right = 0
|
|
734
|
+
@draw_area_bottom = 0
|
|
735
|
+
@draw_offset_x = 0
|
|
736
|
+
@draw_offset_y = 0
|
|
737
|
+
@cmd_buffer = []
|
|
738
|
+
@cmd_remaining = 0
|
|
739
|
+
@vram_transfer_mode = nil
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def gp1_reset_command_buffer
|
|
743
|
+
@cmd_buffer = []
|
|
744
|
+
@cmd_remaining = 0
|
|
745
|
+
@vram_transfer_mode = nil
|
|
746
|
+
@status |= STAT_CMD_READY
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def gp1_acknowledge_irq
|
|
750
|
+
@status &= ~STAT_IRQ
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def gp1_display_enable(value)
|
|
754
|
+
@display_enabled = (value & 0x01) == 0
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def gp1_dma_direction(value)
|
|
758
|
+
@dma_direction = value & 0x03
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def gp1_display_start(value)
|
|
762
|
+
@display_start_x = value & 0x3FE # 10 bits, even
|
|
763
|
+
@display_start_y = (value >> 10) & 0x1FF
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def gp1_horizontal_range(value)
|
|
767
|
+
@display_h_start = value & 0xFFF
|
|
768
|
+
@display_h_end = (value >> 12) & 0xFFF
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def gp1_vertical_range(value)
|
|
772
|
+
@display_v_start = value & 0x3FF
|
|
773
|
+
@display_v_end = (value >> 10) & 0x3FF
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def gp1_display_mode(value)
|
|
777
|
+
hr1 = value & 0x03
|
|
778
|
+
@vertical_res = (value & 0x04) != 0 ? 480 : 240
|
|
779
|
+
@video_mode = (value & 0x08) != 0 ? :pal : :ntsc
|
|
780
|
+
@color_depth_24 = (value & 0x10) != 0
|
|
781
|
+
@interlaced = (value & 0x20) != 0
|
|
782
|
+
hr2 = (value & 0x40) != 0 ? 1 : 0
|
|
783
|
+
|
|
784
|
+
@horizontal_res = case hr1
|
|
785
|
+
when 0 then hr2 == 1 ? 368 : 256
|
|
786
|
+
when 1 then 320
|
|
787
|
+
when 2 then 512
|
|
788
|
+
when 3 then 640
|
|
789
|
+
else 320
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# Update status
|
|
793
|
+
@status = (@status & ~0x007F_0000) |
|
|
794
|
+
((hr1 << 17) & STAT_HORIZONTAL_RES1) |
|
|
795
|
+
((hr2 << 16) & STAT_HORIZONTAL_RES2) |
|
|
796
|
+
((@vertical_res == 480 ? 1 : 0) << 19) |
|
|
797
|
+
((@video_mode == :pal ? 1 : 0) << 20) |
|
|
798
|
+
((@color_depth_24 ? 1 : 0) << 21) |
|
|
799
|
+
((@interlaced ? 1 : 0) << 22)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def gp1_gpu_info(value)
|
|
803
|
+
# GPU info - returns requested information
|
|
804
|
+
# For now just acknowledge
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Software rendering
|
|
808
|
+
def draw_pixel(x, y, r, g, b)
|
|
809
|
+
return if x < @draw_area_left || x > @draw_area_right
|
|
810
|
+
return if y < @draw_area_top || y > @draw_area_bottom
|
|
811
|
+
return if x < 0 || x >= VRAM_WIDTH || y < 0 || y >= VRAM_HEIGHT
|
|
812
|
+
|
|
813
|
+
idx = y * VRAM_WIDTH + x
|
|
814
|
+
|
|
815
|
+
if @check_mask_bit && (@vram[idx] & 0x8000) != 0
|
|
816
|
+
return # Masked pixel
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
pixel = rgb_to_vram(r, g, b)
|
|
820
|
+
pixel |= 0x8000 if @set_mask_bit
|
|
821
|
+
|
|
822
|
+
@vram[idx] = pixel
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def rgb_to_vram(r, g, b)
|
|
826
|
+
((r >> 3) & 0x1F) |
|
|
827
|
+
(((g >> 3) & 0x1F) << 5) |
|
|
828
|
+
(((b >> 3) & 0x1F) << 10)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def draw_rect(x, y, w, h, color)
|
|
832
|
+
h.times do |dy|
|
|
833
|
+
w.times do |dx|
|
|
834
|
+
draw_pixel(x + dx, y + dy, color[:r], color[:g], color[:b])
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def draw_textured_rect(x, y, w, h, tex_u, tex_v, clut_x, clut_y, base_color, raw_texture)
|
|
840
|
+
h.times do |dy|
|
|
841
|
+
w.times do |dx|
|
|
842
|
+
# Calculate texture coordinates
|
|
843
|
+
u = (tex_u + dx) & 0xFF
|
|
844
|
+
v = (tex_v + dy) & 0xFF
|
|
845
|
+
|
|
846
|
+
# Sample texture
|
|
847
|
+
texel = sample_texture(u, v, clut_x, clut_y, @texture_page_x, @texture_page_y, @texture_depth)
|
|
848
|
+
|
|
849
|
+
# Skip transparent texels
|
|
850
|
+
next if texel_transparent?(texel)
|
|
851
|
+
|
|
852
|
+
# Convert to RGB
|
|
853
|
+
tex_color = vram_to_rgb(texel)
|
|
854
|
+
|
|
855
|
+
if raw_texture
|
|
856
|
+
# Raw texture - use texture color directly
|
|
857
|
+
draw_pixel(x + dx, y + dy, tex_color[:r], tex_color[:g], tex_color[:b])
|
|
858
|
+
else
|
|
859
|
+
# Modulate with base color
|
|
860
|
+
r = ((tex_color[:r] * base_color[:r]) >> 7).clamp(0, 255)
|
|
861
|
+
g = ((tex_color[:g] * base_color[:g]) >> 7).clamp(0, 255)
|
|
862
|
+
b = ((tex_color[:b] * base_color[:b]) >> 7).clamp(0, 255)
|
|
863
|
+
draw_pixel(x + dx, y + dy, r, g, b)
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def draw_line(x0, y0, x1, y1, c0, c1, gouraud)
|
|
870
|
+
# Bresenham's line algorithm with optional color interpolation
|
|
871
|
+
dx = (x1 - x0).abs
|
|
872
|
+
dy = -(y1 - y0).abs
|
|
873
|
+
sx = x0 < x1 ? 1 : -1
|
|
874
|
+
sy = y0 < y1 ? 1 : -1
|
|
875
|
+
err = dx + dy
|
|
876
|
+
|
|
877
|
+
steps = [dx.abs, dy.abs].max
|
|
878
|
+
steps = 1 if steps == 0
|
|
879
|
+
|
|
880
|
+
loop do
|
|
881
|
+
t = steps > 0 ? (gouraud ? ((x0 - x0).abs + (y0 - y0).abs).to_f / steps : 0) : 0
|
|
882
|
+
if gouraud && steps > 0
|
|
883
|
+
progress = ((x0 - x0).abs + (y0 - y0).abs).to_f / steps
|
|
884
|
+
r = (c0[:r] + (c1[:r] - c0[:r]) * progress).to_i
|
|
885
|
+
g = (c0[:g] + (c1[:g] - c0[:g]) * progress).to_i
|
|
886
|
+
b = (c0[:b] + (c1[:b] - c0[:b]) * progress).to_i
|
|
887
|
+
else
|
|
888
|
+
r, g, b = c0[:r], c0[:g], c0[:b]
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
draw_pixel(x0, y0, r, g, b)
|
|
892
|
+
|
|
893
|
+
break if x0 == x1 && y0 == y1
|
|
894
|
+
|
|
895
|
+
e2 = 2 * err
|
|
896
|
+
if e2 >= dy
|
|
897
|
+
err += dy
|
|
898
|
+
x0 += sx
|
|
899
|
+
end
|
|
900
|
+
if e2 <= dx
|
|
901
|
+
err += dx
|
|
902
|
+
y0 += sy
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def draw_triangle(p0, p1, p2, c0, c1, c2, gouraud)
|
|
908
|
+
# Simple scanline triangle rasterization
|
|
909
|
+
# Sort vertices by Y
|
|
910
|
+
verts = [[p0, c0], [p1, c1], [p2, c2]].sort_by { |v, _| v[:y] }
|
|
911
|
+
v0, col0 = verts[0]
|
|
912
|
+
v1, col1 = verts[1]
|
|
913
|
+
v2, col2 = verts[2]
|
|
914
|
+
|
|
915
|
+
return if v2[:y] == v0[:y] # Degenerate triangle
|
|
916
|
+
|
|
917
|
+
# Flat color if not gouraud
|
|
918
|
+
unless gouraud
|
|
919
|
+
col1 = col0
|
|
920
|
+
col2 = col0
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# Rasterize
|
|
924
|
+
(v0[:y].to_i..v2[:y].to_i).each do |y|
|
|
925
|
+
next if y < @draw_area_top || y > @draw_area_bottom
|
|
926
|
+
|
|
927
|
+
# Find X bounds for this scanline
|
|
928
|
+
if y < v1[:y]
|
|
929
|
+
# Upper part of triangle
|
|
930
|
+
next if v1[:y] == v0[:y]
|
|
931
|
+
t1 = (y - v0[:y]).to_f / (v1[:y] - v0[:y])
|
|
932
|
+
x1 = v0[:x] + (v1[:x] - v0[:x]) * t1
|
|
933
|
+
c_left = interp_color(col0, col1, t1)
|
|
934
|
+
else
|
|
935
|
+
# Lower part of triangle
|
|
936
|
+
next if v2[:y] == v1[:y]
|
|
937
|
+
t1 = (y - v1[:y]).to_f / (v2[:y] - v1[:y])
|
|
938
|
+
x1 = v1[:x] + (v2[:x] - v1[:x]) * t1
|
|
939
|
+
c_left = interp_color(col1, col2, t1)
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
t2 = (y - v0[:y]).to_f / (v2[:y] - v0[:y])
|
|
943
|
+
x2 = v0[:x] + (v2[:x] - v0[:x]) * t2
|
|
944
|
+
c_right = interp_color(col0, col2, t2)
|
|
945
|
+
|
|
946
|
+
# Ensure x1 < x2
|
|
947
|
+
if x1 > x2
|
|
948
|
+
x1, x2 = x2, x1
|
|
949
|
+
c_left, c_right = c_right, c_left
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
# Draw scanline
|
|
953
|
+
x_start = [x1.to_i, @draw_area_left].max
|
|
954
|
+
x_end = [x2.to_i, @draw_area_right].min
|
|
955
|
+
|
|
956
|
+
(x_start..x_end).each do |x|
|
|
957
|
+
if gouraud && x2 != x1
|
|
958
|
+
t = (x - x1) / (x2 - x1)
|
|
959
|
+
color = interp_color(c_left, c_right, t)
|
|
960
|
+
else
|
|
961
|
+
color = c_left
|
|
962
|
+
end
|
|
963
|
+
draw_pixel(x, y, color[:r], color[:g], color[:b])
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def interp_color(c0, c1, t)
|
|
969
|
+
{
|
|
970
|
+
r: (c0[:r] + (c1[:r] - c0[:r]) * t).to_i.clamp(0, 255),
|
|
971
|
+
g: (c0[:g] + (c1[:g] - c0[:g]) * t).to_i.clamp(0, 255),
|
|
972
|
+
b: (c0[:b] + (c1[:b] - c0[:b]) * t).to_i.clamp(0, 255)
|
|
973
|
+
}
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# Sample a texel from VRAM
|
|
977
|
+
# Returns 15-bit color value or nil if transparent
|
|
978
|
+
def sample_texture(u, v, clut_x, clut_y, tex_page_x, tex_page_y, tex_depth)
|
|
979
|
+
# Apply texture window wrapping
|
|
980
|
+
# Formula: texcoord = (texcoord AND NOT(Mask*8)) OR ((Offset AND Mask)*8)
|
|
981
|
+
u = (u & ~(@texture_window_mask_x * 8)) | ((@texture_window_offset_x & @texture_window_mask_x) * 8)
|
|
982
|
+
v = (v & ~(@texture_window_mask_y * 8)) | ((@texture_window_offset_y & @texture_window_mask_y) * 8)
|
|
983
|
+
|
|
984
|
+
case tex_depth
|
|
985
|
+
when 0 # 4-bit CLUT
|
|
986
|
+
# Each 16-bit VRAM word holds 4 texels
|
|
987
|
+
texel_x = tex_page_x + (u / 4)
|
|
988
|
+
texel_y = tex_page_y + v
|
|
989
|
+
word = @vram[(texel_y % VRAM_HEIGHT) * VRAM_WIDTH + (texel_x % VRAM_WIDTH)] || 0
|
|
990
|
+
|
|
991
|
+
# Extract 4-bit index based on u position
|
|
992
|
+
shift = (u & 3) * 4
|
|
993
|
+
index = (word >> shift) & 0x0F
|
|
994
|
+
|
|
995
|
+
# Look up in CLUT
|
|
996
|
+
clut_addr = clut_y * VRAM_WIDTH + clut_x + index
|
|
997
|
+
@vram[clut_addr] || 0
|
|
998
|
+
|
|
999
|
+
when 1 # 8-bit CLUT
|
|
1000
|
+
# Each 16-bit VRAM word holds 2 texels
|
|
1001
|
+
texel_x = tex_page_x + (u / 2)
|
|
1002
|
+
texel_y = tex_page_y + v
|
|
1003
|
+
word = @vram[(texel_y % VRAM_HEIGHT) * VRAM_WIDTH + (texel_x % VRAM_WIDTH)] || 0
|
|
1004
|
+
|
|
1005
|
+
# Extract 8-bit index
|
|
1006
|
+
shift = (u & 1) * 8
|
|
1007
|
+
index = (word >> shift) & 0xFF
|
|
1008
|
+
|
|
1009
|
+
# Look up in CLUT
|
|
1010
|
+
clut_addr = clut_y * VRAM_WIDTH + clut_x + index
|
|
1011
|
+
@vram[clut_addr] || 0
|
|
1012
|
+
|
|
1013
|
+
when 2 # 15-bit direct
|
|
1014
|
+
texel_x = tex_page_x + u
|
|
1015
|
+
texel_y = tex_page_y + v
|
|
1016
|
+
@vram[(texel_y % VRAM_HEIGHT) * VRAM_WIDTH + (texel_x % VRAM_WIDTH)] || 0
|
|
1017
|
+
|
|
1018
|
+
else
|
|
1019
|
+
0
|
|
1020
|
+
end
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# Convert 15-bit VRAM color to RGB hash
|
|
1024
|
+
def vram_to_rgb(color16)
|
|
1025
|
+
{
|
|
1026
|
+
r: (color16 & 0x001F) << 3,
|
|
1027
|
+
g: (color16 & 0x03E0) >> 2,
|
|
1028
|
+
b: (color16 & 0x7C00) >> 7
|
|
1029
|
+
}
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
# Check if texel is transparent (bit 15 = 0 and color = 0)
|
|
1033
|
+
def texel_transparent?(color16)
|
|
1034
|
+
color16 == 0
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# Draw textured triangle with UV interpolation
|
|
1038
|
+
def draw_textured_triangle(p0, p1, p2, t0, t1, t2, base_color, clut, tex_page_x, tex_page_y, tex_depth, raw_texture)
|
|
1039
|
+
# Extract CLUT position
|
|
1040
|
+
clut_x = (clut & 0x3F) * 16
|
|
1041
|
+
clut_y = (clut >> 6) & 0x1FF
|
|
1042
|
+
|
|
1043
|
+
# Sort vertices by Y, keeping UVs in sync
|
|
1044
|
+
verts = [[p0, t0], [p1, t1], [p2, t2]].sort_by { |v, _| v[:y] }
|
|
1045
|
+
v0, uv0 = verts[0]
|
|
1046
|
+
v1, uv1 = verts[1]
|
|
1047
|
+
v2, uv2 = verts[2]
|
|
1048
|
+
|
|
1049
|
+
return if v2[:y] == v0[:y] # Degenerate triangle
|
|
1050
|
+
|
|
1051
|
+
# Rasterize with UV interpolation
|
|
1052
|
+
(v0[:y].to_i..v2[:y].to_i).each do |y|
|
|
1053
|
+
next if y < @draw_area_top || y > @draw_area_bottom
|
|
1054
|
+
|
|
1055
|
+
# Find X bounds and interpolate UVs for this scanline
|
|
1056
|
+
if y < v1[:y]
|
|
1057
|
+
next if v1[:y] == v0[:y]
|
|
1058
|
+
t1_interp = (y - v0[:y]).to_f / (v1[:y] - v0[:y])
|
|
1059
|
+
x1 = v0[:x] + (v1[:x] - v0[:x]) * t1_interp
|
|
1060
|
+
u1 = uv0[:u] + (uv1[:u] - uv0[:u]) * t1_interp
|
|
1061
|
+
v1_tex = uv0[:v] + (uv1[:v] - uv0[:v]) * t1_interp
|
|
1062
|
+
else
|
|
1063
|
+
next if v2[:y] == v1[:y]
|
|
1064
|
+
t1_interp = (y - v1[:y]).to_f / (v2[:y] - v1[:y])
|
|
1065
|
+
x1 = v1[:x] + (v2[:x] - v1[:x]) * t1_interp
|
|
1066
|
+
u1 = uv1[:u] + (uv2[:u] - uv1[:u]) * t1_interp
|
|
1067
|
+
v1_tex = uv1[:v] + (uv2[:v] - uv1[:v]) * t1_interp
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
t2_interp = (y - v0[:y]).to_f / (v2[:y] - v0[:y])
|
|
1071
|
+
x2 = v0[:x] + (v2[:x] - v0[:x]) * t2_interp
|
|
1072
|
+
u2 = uv0[:u] + (uv2[:u] - uv0[:u]) * t2_interp
|
|
1073
|
+
v2_tex = uv0[:v] + (uv2[:v] - uv0[:v]) * t2_interp
|
|
1074
|
+
|
|
1075
|
+
# Ensure x1 < x2
|
|
1076
|
+
if x1 > x2
|
|
1077
|
+
x1, x2 = x2, x1
|
|
1078
|
+
u1, u2 = u2, u1
|
|
1079
|
+
v1_tex, v2_tex = v2_tex, v1_tex
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
# Draw scanline with texture sampling
|
|
1083
|
+
x_start = [x1.to_i, @draw_area_left].max
|
|
1084
|
+
x_end = [x2.to_i, @draw_area_right].min
|
|
1085
|
+
x_span = x2 - x1
|
|
1086
|
+
|
|
1087
|
+
(x_start..x_end).each do |x|
|
|
1088
|
+
# Interpolate UV
|
|
1089
|
+
t = x_span > 0 ? (x - x1) / x_span : 0
|
|
1090
|
+
u = (u1 + (u2 - u1) * t).to_i & 0xFF
|
|
1091
|
+
v_coord = (v1_tex + (v2_tex - v1_tex) * t).to_i & 0xFF
|
|
1092
|
+
|
|
1093
|
+
# Sample texture
|
|
1094
|
+
texel = sample_texture(u, v_coord, clut_x, clut_y, tex_page_x, tex_page_y, tex_depth)
|
|
1095
|
+
|
|
1096
|
+
# Skip transparent texels
|
|
1097
|
+
next if texel_transparent?(texel)
|
|
1098
|
+
|
|
1099
|
+
# Convert to RGB
|
|
1100
|
+
tex_color = vram_to_rgb(texel)
|
|
1101
|
+
|
|
1102
|
+
if raw_texture
|
|
1103
|
+
# Raw texture - use texture color directly
|
|
1104
|
+
draw_pixel(x, y, tex_color[:r], tex_color[:g], tex_color[:b])
|
|
1105
|
+
else
|
|
1106
|
+
# Modulate with base color
|
|
1107
|
+
r = ((tex_color[:r] * base_color[:r]) >> 7).clamp(0, 255)
|
|
1108
|
+
g = ((tex_color[:g] * base_color[:g]) >> 7).clamp(0, 255)
|
|
1109
|
+
b = ((tex_color[:b] * base_color[:b]) >> 7).clamp(0, 255)
|
|
1110
|
+
draw_pixel(x, y, r, g, b)
|
|
1111
|
+
end
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
end
|
|
1115
|
+
end
|
|
1116
|
+
end
|