Commit Graph
10131 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 63a05db8d2 SoftGPU: Skin the x, y and z components together in vector lanes
Skinning did each component's weighted bone entries and its chain of
GEAdds separately in scalar code. The components now go in lanes, with
GEMulFloat24x4 for the weighted entries, one-term row sums for the
products and GEAdd4 for the sums, in the same order. Checked against the
scalar version on every vertex of 143 skinned frame dumps.

GEMulFloat24x4 matches ProductToFloat24, so inf and NaN bone entries
behave as before. GEAdd4 handles two zero operands without its fallback.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 c9a2b773e3 GEMath: Build GERecip and GERsqrt results from exponent bits
Adds the exponent instead of calling ldexpf, except for reciprocals that
can be denormal. Identical results for every normal float24 input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 a9d8b6d3c5 GEMath: GERowSum4 takes its left operands as one vector
They're truncated to float24 with one AND, and their exponents, fractions
and signs worked out for all terms at once, each term then broadcasting its
lane (new CrossSIMD Vec4S32::SplatLane). The term count becomes a template
parameter so the lanes are constants. About 75-83 M transforms/s on an M1,
from 68; results unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 c4ce2e3635 SoftGPU: Use GERowSum4 for the transform's row sums
The four clip space rows of a vertex, the texture matrix's three, the world
normal's three, the vector to a light, and each row of the combined matrix
now take one GERowSum4 call. Same results (pspautotests, frametests and
the probe regression set unchanged).

GERowSum4 scales its result by adding to the exponent instead of
multiplying, and CrossSIMD gets a Vec4S32::Max for the largest lsb. About
68 M transforms/s against 26 for four scalar GERowSums on an M1 (a pure
NEON version with per-lane shifts did about 79); replaying the frametest
dumps gets only about 1% faster, up to 11% on Taiko no Tatsujin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:57:09 +02:00
Henrik RydgårdandClaude Opus 5.5 59eccf499e GEMath: Add GERowSum4, four row sums at once
Four rows sharing their left operands, like a vertex transform's: with
float24 operands each exact product is a 32-bit integer, (1.a' * 1.b') >> 15
= 32768 + a' + b' + (a' b' >> 15), and the terms are aligned by scaling with
a power of two and truncating. Lanes whose result exponent is out of a
single float scale's reach take the scalar GERowSum. Tested bit for bit
against GERowSum on random operands (arm64 and x86-64). Not used yet.

Also adds an arithmetic Vec4S32::Shr and the float/int bit casts to
CrossSIMD.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:55:42 +02:00
Henrik RydgårdandClaude Opus 5.5 111fa6ed95 GPU: Copy overlapping block transfer rows as the GE does
The GE reads the source in aligned 16-byte blocks, four at a time, into a
buffer of four destination-aligned 16-byte slots, and a read of a block a
slot holds returns the slot's bytes, stale ones included. So a transfer
shifting a row right by less than 64 bytes reads some of what it has just
written, in a pattern that isn't a forward or backward copy. Fitted to
ppsspp-re geprobe exp219 and exp220 on hardware (562 of 570 cases exact).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik RydgårdandClaude Opus 5.5 1a90aa6047 GPU: Count the cycles of commands the hardware backends consume by hand
The fast matrix loads and the joined PRIMs read their commands themselves, then skipped them with
UpdatePC(pc, pc + n * 4), which counts the commands up to pc and resumes counting after the skipped
ones: those were never counted, as for a jump. Now pc moves first and UpdatePC counts up to it. The
software renderer runs those commands one at a time, so the backends now agree on the timing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik RydgårdandClaude Opus 5.5 834e617cc1 SoftGPU: Carry UVs and normals into formats without them as the GE does
gpu/vertices/carry, recorded on a PSP: a draw whose format has no UVs uses the last ones read, by any
earlier draw in draw order, textured or not. They're kept as read and get the scale and offset of the
draw that uses them. Normals carry over the same way.

We kept the UVs after scaling, only read them with texturing on, and after a cached indexed draw
carried the highest vertex instead of the last one drawn. The carried values move from statics in
ReadVertex to TransformUnit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik Rydgård f0e0368a77 Merge pull request #22456 from hrydgard/spline-simd
SIMD-optimize splines and beziers
2026-10-08 20:27:34 +02:00
Henrik RydgårdandClaude Opus 5.5 ce97cb1201 Splines: Vectorize the GE-exact spline evaluation
The same lerps as the GE-exact Bezier path, four at a time. At each v step,
four control columns per vector share the v blending factors; then each row
is evaluated at four u steps, each lane with its own six blending factors.
A vector's u steps can straddle a segment boundary, so the row's points are
gathered per lane, or splatted when all four are in one segment.

The unit test compares it bit for bit with the scalar code, including
tessellations that split a vector across segments. The GE-exact spline
benchmark runs about twice as fast on an M1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:33:20 +02:00
Henrik RydgårdandClaude Opus 5.5 2db4096cec Splines: Vectorize the GE-exact Bezier evaluation
Four of the GE's lerps at once: both operands go to 16-bit fixed point at
the larger exponent by scaling with a power of two and truncating, then the
integer lerp (split into byte halves so both multiplies fit 16 bits), then
back. The patch's four control columns run in the four lanes at each v
step, then four u steps of a row at once, with the row's first level of
lerps converted once per row. Lanes whose exponents are tiny take the scalar
lerp, and the normals' cross product stays scalar for now.

The scalar code stays as the reference (g_splineGEScalar), and the unit test
compares the two bit for bit on random control points. The GE-exact Bezier
benchmark goes from 3.4 to about 8.8 Mverts/s on an M1; the float SIMD path
does about 100.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:00:19 +02:00
Henrik RydgårdandClaude Opus 5.5 50ba8e459b SoftGPU: Keep a not-black color test when dithering, fog or a logic op is on
The software renderer drops a color test that only rejects black under blending that adds to the
destination, since rejecting black then only skips adding nothing. With dithering on it doesn't:
a rejected pixel keeps the destination, an accepted black one gets the dither added. Ridge Racer 2
(20839) adds a glow that way and came out one step darker wherever the dither matrix was negative;
Burnout Legends' bloom (16119) too. Fog and logic ops would change the destination the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 75e79ef2db GE dump playback: Let the GE finish the list before reusing its buffer
At the end of the 256 KB list buffer, playback jumped back to its start and kept writing, so when
the GE was more than a lap behind, the next lap overwrote commands it hadn't reached. Warriors
(14660) lost half its draws, and GTA LCS (15149) some. Finish the list, wait for it, and start a
new one instead, as the PSP replayer does. Both now match the PSP's replay exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 a26ef51a0c GE dump playback: Restore old-layout contexts from old dumps
Dumps recorded before savedContextVersion 1 hold PPSSPP's old context layout, with the matrices
as raw floats, but GEState::Restore picks the layout from savedContextVersion, which only a
savestate sets. Playback restored them as the new layout, so the matrices became garbage and
the scene vanished (4140 drew only its HUD). Tell the layouts apart by the END the new one ends
with, as the PSP replayer does. Other callers are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 937053063c GE dump playback: Give unaligned CLUTs and textures an aligned copy
Older recorders reused data at any offset in the dump, and Prince of Persia: Rival Swords
(11587, 12943) has a CLUT stored one byte into the previous one. The GE ignores the low four
bits of a CLUT or texture address, so the LOADCLUT read 15 bytes early and the palette
channel-swap pass came out red. Map CLUTs, textures and transfer sources that aren't 16-byte
aligned to a separate aligned copy.

Also updates LocoRoco 12058's software reference, which now matches the PSP's replay exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 eb59b00a37 GE dump playback: Ignore a second INIT in a dump
Before 71210f3fa2, a second dump request during a recording started it again, writing another
INIT (and initial CLUT) into the dump: the GE state of that moment, which already includes the
register writes the dump only has after it. Playback applied it, so the last draws ran with the
wrong state: GOD EATER BURST (13950) and GOD EATER 2 (6343) came out black and Street Riders
(14746) garbled. The recorder only writes an INIT when it starts, so skip any later one. The 15
such dumps in the GitHub issue set now match the PSP's replay, 10 of them exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 a418514376 GE dump playback: Load the initial CLUT, and show the last framebuffer when the display is off
The recorder saves the CLUT the GE had loaded right after INIT, as a CLUT command with no
LOADCLUT to follow, unlike the CLUTs it records at a LOADCLUT. Playback only pointed the CLUT
address at it, so draws before the game's first LOADCLUT used whatever CLUT was loaded before:
HotBrain (16131, ULUS10268) drew its save screen background black. Load it, and put the game's
CLUT address back.

A dump whose last DISPLAY turns the display off (address 0), or has none, now shows the last
framebuffer drawn to (Auditorium 9213). The PSP replayer does both the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 2317ecbfbd GE dump playback: Write recorded memory into the depth mirrors as the PSP does
A dump records a texture at a swizzled VRAM mirror as the game sampled
it, and the PSP's replayer writes it back with the CPU, which the PSP
sends through the depth layout at any access width. PPSSPP's playback
wrote it raw, so with depth stored in the GE's layout the texture came
back permuted: IL-2's depth texture at 0x04710000 and the two Gensou
Suikoden swizzle dumps. Texture copies, memcpys and memsets into a
mirror now go through the layout, 32 aligned bytes at a time. Against
PSP replays: both Suikoden dumps are now exact (from 1912 and 8083
pixels off), Star Wars #15832 is 67 off (from 2590) and IL-2 894 (from
70505, the rest from a write the next draw reads before it lands).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:30 +02:00
Henrik RydgårdandClaude Opus 5.5 ae32477764 Core: Let 16-bit CPU accesses through the depth mirrors see the GE's layout
With the software renderer, which now stores depth as the GE does, a
16-bit read or write at VRAM's 0x04200000 or 0x04600000 mirror goes to
where the GE's 16-bit or 32-bit color layout puts that depth pixel
(ppsspp-re geprobe exp88: the mirrors are the layout's exact inverse).
Games access depth there with nothing else, so other widths stay plain.

The accessors check the address, then whether the GPU core is the
software renderer (CoreParameter); the hardware renderers keep depth plain
and handle the layout their own way. The renderer keeps the layouts up to
date with the EDRAM address translation. With it, the JITs send 16-bit
loads and stores to the interpreter, and the IR passes don't change a
load's width to or from 16 bits.
The pspautotests that read depth through the mirrors now do so 16 bits
at a time, re-recorded on hardware unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:23 +02:00
Henrik RydgårdandClaude Opus 5.5 a11fd22553 SoftGPU: Store depth in VRAM as the GE does
The software renderer kept depth linear in VRAM, where the GE stores it
in the layout GPU/Common/DepthSwizzle.h describes, from the EDRAM address
translation and the color format. So a texture or anything else sharing
VRAM with a depth buffer saw depth land in the wrong places (the player's
shadow in Silent Hill: Origins, #22415). Now every depth access goes
through the layout: the generic pixel functions, the 2x2 depth test, the
depth clear in 16-pixel runs, and the x86 pixel JIT, whose IDs carry the
translation so the layout's constants are compiled in. It changes with
the color format and sceGeEdramSetAddrTranslation, after a flush.

Reading VRAM through its 0x04200000 and 0x04600000 mirrors undoes the
layout, as the GE does: texturing from them (drawn from a linear copy
made after a flush), and block transfers from or to them (Hayate no
Gotoku restores its depth buffer through 0x04600000, #17878). Saved and
debugger depth buffers read through the layout, so they're linear.

Socom's lens flare dump now matches a PSP replay where it changes. About
2-3% more cycles in God of War, Tekken 6, Wipeout and Burnout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:48 +02:00
Henrik RydgårdandClaude Opus 5.5 84582c10f1 GPU: Add the GE's depth buffer layout in VRAM
Where the GE stores each depth pixel, from the EDRAM address translation
and the color format: with 32-bit color, address bits 5 up to log2(T) - 1
rotate left by one, then (T << 3) | 0x40 is XORed in (0x600, and bits 9
and 10 swapped, at T 0). The CPU sees the same mapping through VRAM's
0x04200000 and 0x04600000 mirrors. Measured in ppsspp-re geprobe exp8-10
(every pixel of 512x272 buffers at each T) and exp88; the unit test checks
it against the model fitted there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:48 +02:00
Henrik RydgårdandClaude Opus 5.5 e94d6f9bd2 Splines: Skip the second vertex decode of tessellated curves
The tessellator's output was queued as ordinary vertices and run through
a vertex decoder at flush, although the decoded layout is the same as
SimpleVertex. That decode did two things: the UV scale/offset, and
clearing gstate_c.vertexFullAlpha. The tessellator now does both, and
SubmitCurve writes its output where DecodeVerts would have put it and
queues it as predecoded. DecodeVerts only copies such a draw, when it
decodes elsewhere, like the Vulkan and GLES push buffers.

The software renderer decodes by itself and keeps the old path, as do
through mode curves and anything with draws already queued.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:12:17 +02:00
Henrik RydgårdandClaude Opus 5.5 f4cf9ab3fd Splines: Use MulAdd, and round colors with one instruction
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:12:10 +02:00
Henrik RydgårdandClaude Opus 5.5 0b0a30217b Splines: Tessellate four vertices at a time with CrossSIMD
Each row of vertices is evaluated by folding the patch's four control point
rows into four columns with the v weights, then weighting those along u,
four vertices per register with one attribute component each. Cross
products, normalization and the pole test become plain per-lane math, and
the result is transposed into SimpleVertex order on the way out.

The weights are stored one row per term so four steps load as one vector,
and the control points are padded to full vectors in a single array.

Colors now round to nearest on every platform. The SSE path already did,
NEON and the scalar path truncated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:12:10 +02:00
Henrik RydgårdandClaude Opus 5.5 73b5d2a4a2 Self-texture copy: Include the texel past each edge of the UV bounds
The copy for texturing from the current render target was cropped to the
UV bounds, but bilinear filtering at the edges reads one texel beyond them,
which then came from whatever the temp FBO last held. On Vulkan that put
magenta streaks into Need for Speed Shift's sun glow, and also hit Call of
Duty, Burnout Dominator (#12831) and Assassin's Creed Bloodlines (#15821).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 f1a6232841 Pixel depth rounding: Floor with room for float error, store Z like clears
The fragment Z is the PSP's Z / 65536, and flat draws land exactly on an
integer Z. floor(z * 65535) put that Z - Z/65536 just above the boundary, so
float error (which differs with W) could drop it by one: Patlabor redraws a
HUD icon at the same depth with GEQUAL, and the second draw failed on
Vulkan. Floor Z + 0.01 instead, and store Z / 65535, as clears and the depth
clamp path do. Also fixes Heroes Phantasia #6485.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 b4619757ad Software transform: Check rectangle depth against the range like the GE
A rectangle's depth is flat, so right at a depth range limit our float
rounding drops or keeps the whole thing. Mana Khemia 2 draws a conversation
background at GE screen Z 1 with minz 1, and we computed 0, so it vanished
(#12858). When the GE's depth would land on the other side of the range
check, use it; otherwise keep ours, which matches the hardware-transformed
draws a rectangle may be depth tested against (rounding all rectangle depths
to the GE's broke the Need for Speed HUD).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 964b5dc00a SoftGPU: Move the GE clip and viewport helpers to GEMath.h
So the hardware backends' software transform can use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 0b4c29c883 Clip and cull in software when the GPU lacks cull distances
Without them nothing culls triangles beyond the far plane: depth clamp draws
them, as with the stands over the pitch in FIFA Soccer on macOS. Fixes FIFA
and Hellboy on OpenGL there, and DBZ Tenkaichi Tag Team, Monster Hunter 3rd
and Street Riders on both backends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 7c5eda25f4 Software transform clipping: split clipped quads like the GE, cull beyond far first
With one vertex behind the near plane, the GE splits the remaining quad from
the vertex before the clipped one (gpu/probe exp43, exp44, as in SoftGPU's
Clipper.cpp). The other diagonal could put a far off-screen near-plane vertex
in both halves, so range culling dropped the whole triangle: the floor in
#16131's box test vanished. The index generator reverses triangles to flip
culling, so the batch's order is passed along.

Triangles entirely beyond the far plane are now culled even when in front of
the near plane, with the cull-distance epsilon, so z/w = 1.0000172 (exactly
1.0 to the GE) still counts as inside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 03c5c4e115 Don't write fragment depth when the depth test is effectively off
Nothing writes depth then, so the shader write is wasted. On Apple's OpenGL
it also broke Tales of Phantasia (PixelDepthRounding): once the shader writes
gl_FragDepth, a discarded fragment still writes stencil, so a color-tested
stencil pass marked the whole area and the background tiles drawn with
stencil EQUAL 0 vanished.

New FS ID bit, so both shader caches get a version bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 709e6729ad GLES: Enable the clip planes the vertex shader actually writes
The vertex shader puts min/max Z in clip distances 0 and 1 and the near plane
in 2, but the program flags still followed an older layout and never enabled
plane 2. Geometry crossing the near plane was then only depth clamped, and
smeared across the screen (Hellboy, FIFA Soccer, Hoto no Ken frame dumps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:24 +02:00
Henrik RydgårdandClaude Opus 5.5 d0e6174389 Apply the pixel masks to clears in the hardware backends
On the PSP the color and alpha/stencil write masks apply to clears like to
drawing (gpu/stencil/writemask, ppsspp-re geprobe exp254). Clear-mode draws
now take the same per-channel heuristic as other draws, and a clear with RGB
masked is drawn rather than done as a hardware clear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:42:27 +02:00
Henrik RydgårdandClaude Opus 5.5 feaa41fadb Always emulate partial color write masks in the shader, drop ShaderColorBitmask
Partial red, green and blue write masks now always go through the shader with
a framebuffer read. Across the frame dump set this only improves games outside
the old list (Split/Second, Ultimate Ghosts 'n Goblins, older DiRT 2 and OutRun
dumps without a game ID). Partial alpha masks keep the old heuristic: alpha is
the stencil, with its own write mask, and taking the shader path for them made
Pro Yakyu Spirits and Gangs of London worse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:18:05 +02:00
Henrik Rydgård 8e596032f1 Merge pull request #22446 from hrydgard/misc-rendering-fixes
Misc rendering fixes
2026-10-08 11:16:14 +02:00
Henrik RydgårdandClaude Opus 5.5 89fabe21fb Rebuild the stencil from alpha after reinterpreting a framebuffer
On the PSP the stencil is the alpha bits, so after the colors of another
format are reinterpreted into a 5551, 4444 or 8888 buffer, its stencil has to
follow the new alpha. Ultimate Ghosts 'n Goblins sets 5551 alpha with a masked
565 blue write and then stencil tests on it; Vulkan drew its glow through a
stale stencil. The memory stencil upload's pipeline and passes are shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:45:57 +02:00
Henrik RydgårdandClaude Opus 5.5 bcfb4daa78 Depth readback: Scale by 65536, and decode in full precision
The vertex shader stores depth as z / 65536, so reading it back with 65535
came out one too low whenever z's fraction was below z / 65536. Both the CPU
conversion and the shader path (used on GLES and at higher render
resolutions) had it. The shader also ran at the fragment shader's default
lowp, which mobile GPUs honor; 24 bits of depth need highp, sampler included
on GLES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 b40c8a0197 TextureCLUTInShader: Build the CLUT texture from the raw CLUT
The GLES texture cache keeps 16-bit CLUTs in clutBuf_ converted to its own
component order, and GetClutTexture converted them again, so Fushigi no
Dungeon 4's tiles came out black on OpenGL. The framebuffer depal path
already passed clutBufRaw_.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik RydgårdandClaude Opus 5.5 0b79588838 Use shader depal for CLUT8 from an 8888 framebuffer also when rendering to it
The separate depal pass reads whole pixels, not the byte each texel picks, so
Star Wars: The Force Unleashed's color grade turned magenta. Vulkan and
OpenGL now match each other and the PSP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:18 +02:00
Henrik Rydgård 3c6c7f548b Merge pull request #22433 from hrydgard/softgpu-threading
SoftGPU: Improve threading
2026-10-06 08:09:17 -06:00
Henrik Rydgård adda3ab1de Merge pull request #22434 from hrydgard/softgpu-nan-vertex
SoftGPU: Treat a NaN screen coordinate as outside the range
2026-10-05 21:49:34 -06:00
Henrik Rydgård a26160f66e Merge pull request #22431 from hrydgard/safe-path-component
Validate untrusted IDs before using them as directory names
2026-10-05 20:17:08 -06:00
Henrik RydgårdandClaude Opus 5.5 6e5258f570 SoftGPU: Declare the wake chain as u64 for LeastSignificantSetBit
Linux's uint64_t is unsigned long, which neither of its u32 and u64
(unsigned long long) overloads takes exactly, so the call was ambiguous.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:28:21 -06:00
Henrik RydgårdandClaude Opus 5.5 f949fd5187 SoftGPU: Treat a NaN screen coordinate as outside the range
The range check was written as tests for outside, which are all false for
NaN, so a NaN vertex passed and was converted to an integer. That differs
by CPU: arm64 gives 0, which lands off screen, while x86-64 gives INT_MIN,
which wraps, and the triangle's area overflowed into a huge sliver. In the
recording-opengl and recording-vulkan dumps that drew a beam across the
top right that a PSP replay of the dump doesn't show. Now such a vertex is
flagged, and its triangles dropped, on both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:09:55 -06:00
Henrik RydgårdandClaude Opus 5.5 aa39b6460a SoftGPU: Grow the state ring when it fills
A full state ring flushed the binner, and the next state change filled
it again in the same way. Now the flush also doubles the ring, up to
32768 states, so it happens at most a few times. Nothing draws right
after a flush, so the ring can move then. In the benchmark games, no
more than 512 states were queued between flushes, so 4096 stays the
starting size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 9d842d74af SoftGPU: Track whether anything was queued since a flush with a flag
The binner kept a bounding box of every queued item's range, updated for
each item, only to tell whether anything had been queued since the last
flush. A flag says the same. Expand, which grew the box, is now
ItemQueued.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 f015bd111b SoftGPU: Don't change a state the threads may be drawing with
Each queued primitive adds its vertex flags (alpha, fog, colors) to its
state, and states are optimized for those flags before their items are
distributed. A state can still gain primitives after some of its items
are in tiles. Re-optimizing it then changed its pixel and sampler
functions and IDs while threads drew with them. With the x86 pixel JIT,
the span function could turn null between checking and calling it, as
in Coded Arms' frame dump. God of War hit this about four times a frame.

Now a state is live once any of its items is in a tile, until the tiles
are drawn and reset, and a live state isn't changed. A primitive that
would change a live state's flags starts a new, unoptimized state from
the same registers instead, optimized for its own primitives only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 8f699056f9 SoftGPU: Don't flush for JIT compiles where there's no JIT
The pixel and sampler JITs only exist on x86-64, but elsewhere a cache
miss still flushed the binner before the compile that did nothing, and
the cache never filled. So on ARM every switch to a new pixel or sampler
state was a full flush: hundreds per frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:17 -06:00
Henrik RydgårdandClaude Opus 5.5 15d0972f86 SoftGPU: Fix merging pending write ranges that start lower in the buffer
Expanding a pending range by a rectangle starting below its start added
a negative row count to an unsigned height, which wrapped to millions of
rows. Overlap tests then overflowed and missed real overlaps. Now a
merged range is whole rows from the lower start to the higher end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 c6c953ff47 SoftGPU: Order texturing from pending drawing per primitive, not by flushing
A draw whose texture rectangle overlapped what queued primitives write
flushed, once per draw. NeedsOrder already orders each primitive by the
texels it actually reads, so let it. God of War's bloom passes texture
from the main framebuffer declared as 512x512, which reaches into the
buffer they draw to, and flushed every 30 primitives: 6% faster there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00