Commit Graph
10259 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 a361dfc490 Drop unused Config.h and SmallDataConvert.h includes
Config.h is edited about 100 times a year and each edit recompiled 198
files; now 172. SmallDataConvert.h pulls in arm_neon.h and was included by
headers that don't use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:13:42 +02:00
Henrik RydgårdandClaude Opus 5.5 531a75f045 Add AccurateSplines compat flag, enable for Coded Arms
- Fixes #21391

Makes the hardware backends tessellate Bezier patches and splines with the
GE-exact evaluation the software renderer uses. Fixes the z-fighting glow
under the floor grates in Coded Arms.

AGENTS.md: Put "- Fixes #N" first in commit messages

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 14:04:38 +02:00
Henrik RydgårdandClaude Opus 5.5 4e12f25c70 Readback: Don't write a stale buffer on failure, and clamp the height
Depth readback and the GLES stencil readback converted whatever their
buffer last held into PSP memory when the copy failed. The generic stencil
readback asked for depth, which asserts on Vulkan and D3D11. And
ReadFramebufferToMemory clamped the width to the buffer but not the height,
which a block transfer can exceed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 12:15:56 +02:00
Henrik RydgårdandClaude Opus 5.5 ae68a7e73f Coded Arms: Contagion: Read back the stencil the crosshair checks
The crosshair turns red, and lock-on works, when stencil bit 3 is set at
the centre of the frame, which the game tests by reading the pixel from
VRAM with the CPU (#17936). Hook that and read back the stencil of that
row into the alpha bytes in RAM. The alpha the hardware backends keep
doesn't follow stencil everywhere, so reading color isn't enough.

Adds PerformReadbackStencilToMemory for this (8888 only for now).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 12:15:56 +02:00
Henrik RydgårdandClaude Opus 5.5 275fc53061 GE dump recording: Record one more frame when the display shows the previous one
Silent Hill: Origins shows each frame one late: the first draw of a frame
copies the previous one into its 565 display buffer. A dump of one frame
then displays a copy of the frame before it, taken from memory, and none
of what it draws. When the display buffer's last write came before most of
the frame's drawing, the recorder now records the next frame too, whose
copy puts the recorded frame on the display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:04:25 +02:00
Henrik RydgårdandClaude Opus 5.5 c4da98e889 GE dump recording: Read back framebuffers drawn before the recording
With a hardware renderer, a framebuffer only exists on the GPU, so a dump
copied whatever stale data RAM held when a game sampled a buffer it drew
in an earlier frame. Silent Hill: Origins shows the previous frame by
copying it into its 565 display buffer at the start of the next one, so
its dumps displayed old scratch data. The recorder now reads such buffers
back once before copying them out (not ones drawn during the recording,
which replay draws again).

It asks for the copy without creating a framebuffer: with
BlockTransferAllowCreateFB (which Silent Hill has) a plain readback makes
a new framebuffer at the destination and never writes RAM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:04:25 +02:00
Henrik Rydgård 0ee8b5adf8 Merge pull request #22509 from hrydgard/buzz-video
Upload video frames written partway into a framebuffer. Fixes Buzz Master Quiz videos
2026-10-11 23:21:39 +02:00
Henrik RydgårdandClaude Opus 5.5 75865683bb Upload video frames written partway into a framebuffer
Buzz! Master Quiz converts its question videos (sceMpegBaseCscAvcRange)
straight into the middle of the frame it has drawn, at 84,69. The
framebuffer upload only matched writes starting at a framebuffer's
address, so the hardware backends never showed the video. Pass the
written width along and draw just that rectangle into the framebuffer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 22:50:57 +02:00
Henrik RydgårdandClaude Opus 5.5 5704b972e7 iOS: Remove OpenGL support from the app
The app now always uses the Metal view controller with Vulkan (MoltenVK),
for both the App Store and sideload builds. The GL view controller is gone,
and Common/GPU/OpenGL (except GLFeatures, used by the shader generators) and
GPU/GLES are no longer compiled for it. PPSSPP_API_ANY_GL is now off for
all of iOS except the libretro core, which still uses GLES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:28 +02:00
Henrik RydgårdandClaude Opus 5.5 2aaf13d8f8 Upload only the copied part of a row in a memcpy to a framebuffer
A memcpy of part of one row into a framebuffer was drawn at the buffer's full
width, pulling in whatever followed the source. Everybody's Golf copies its
loading screen spinner back to the screen 64 pixels at a time, so its sprite's
next rows were drawn alongside, as a row of discs across the bottom.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 15:10:34 +02:00
Henrik RydgårdandClaude Opus 5.5 50696c5b1f Remove the "game uses sequential copies" readback heuristic
After more than three partial readbacks of one buffer in a frame,
ReadFramebufferToMemory read the whole buffer instead for the rest of the
session, writing rows the game never asked for.

Everybody's Golf 2 reads back four portraits' color and depth per frame, which
turned it on. The whole 480x273 depth buffer then got written to VRAM, and its
last row is where the game keeps its texture heap. A texture header was
overwritten: the portrait showed the scene behind it, and the next time the
portraits were built the game crashed writing through a null pointer.

It was added for speed in Grand Knights History (c42d04829f), which copies many
slivers of a framebuffer, so that game may now do more readbacks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:50:19 +02:00
Henrik RydgårdandClaude Opus 5.5 648766dad4 Everybody's Golf 2: Read back color and depth for the player portraits
The game builds each 64x64 portrait by memcpying a rect of the framebuffer one
partial row at a time, then setting each texel's alpha bit from the depth
buffer, which it reads directly from VRAM. Neither got read back: the row
copies don't line up with a framebuffer, and depth is never downloaded.

Hook the copy function to read back both, whole rows at a time. That needed two
fixes to requested depth copies: they were dropped when the depth buffer lies
above all color buffers, and only an exact full-buffer size matched.

The portrait texture now matches the software renderer, though it still
doesn't show up on screen with Vulkan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 13:25:10 +02:00
Henrik Rydgård 6779143827 Merge pull request #22486 from hrydgard/softgpu-x86-opt
SoftGPU: Do an optimization pass on x86-64 too
2026-10-10 15:48:04 +02:00
Henrik Rydgård e312af9b04 Merge pull request #22483 from hrydgard/reinterpret-subpixel
Further improve the Tantalus games
2026-10-10 15:27:21 +02:00
Henrik Rydgård 32fc6c0462 Support the "FastEmulatedGPU" compat flag in the software renderer too. 2026-10-10 15:25:20 +02:00
Henrik RydgårdandClaude Opus 5.5 6245285ca7 SoftGPU: Don't linger in drawing threads on Windows, and less in transform helpers
On native Windows (Ryzen 9 9950X3D), lingering only paid in the transform
helpers. With no drawing-thread linger and 100 us for the helpers, wall
time stayed within 1% of 200/300 us in Daxter, GTA VCS, Wipeout Pure,
Tekken 6, Burnout Dominator, LocoRoco and Ridge Racer, with 9-27% less
CPU time. No linger at all cost up to 7% (Daxter), almost all of it from
the transform helpers.

The linger times are now per platform; Linux keeps the WSL2 values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:15:03 +02:00
Henrik RydgårdandClaude Opus 5.5 c53448dd6b SoftGPU: Pick the mip level once for staged rows of a triangle with one q
Where the mip level comes from q (slope mode, or auto with UV planes) and q is
the same over the whole triangle, its pixels all have one level. The staged
spans went through ApplyTexturing for each four pixels anyway, to work out that
level and call the quad sampler; a variant of DrawStagedSpans now works it out
once and calls the quad sampler directly. It's a separate variant, as the extra
code in the shared one cost other games about 1% of instructions on an M1.

LocoRoco takes 7.5% fewer instructions and 5% less wall time, Wipeout 2% fewer
instructions; the others are unchanged. The frame dump set renders the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 e415e667d0 SoftGPU: Draw 1:1 sprites as rectangles unless DrawSprite has a fast path for them
DrawSprite is only fast in its single pixel paths; for any other pixel state
(fog, depth test, other blending) it sampled and drew a pixel at a time, while
DrawRectangle draws four in vector lanes. Such sprites now go to DrawRectangle.
On an M1: Tekken 6 and LocoRoco take 4-5% fewer instructions, Burnout 1%. The
frame dump set renders the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 c32a01965b SoftGPU: Multiply light colors with 16-bit multiplies
The light color products went through Vec4<int>'s multiply, a 32-bit
lane multiply that SSE2 emulates, though their factors are at most 10
bits. MulSmall uses CrossSIMD's Mul16 where Vec4<int> has a register.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 3540e4ef64 SoftGPU: Scale by powers of two through the exponent bits
GEDot3 and GERowSum called ldexpf for each result, and the UV plane
setup ldexp and ilogb for every triangle and tile, about 2% of the CPU
time between them. Adding to the exponent field gives the same values
while they stay normal (ScaleByPow2Exact, which calls ldexpf otherwise),
and the planes' values and shifts always are. 2-4% fewer instructions
in every game measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 564098a40d SoftGPU: Remove what only the JIT caches used
The pixel and sampler ID hashes and equality, the functions describing
the IDs for the JIT's code pointers, and SamplerID's linear and fetch
bits, which only told the JIT cache's entries apart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 16a5046124 SoftGPU: Read bilinear CLUT and 8888 texels in vector lanes
The four-pixel bilinear sampler read its sixteen texels one at a time,
each with its CLUT lookup and index transform, and loaded them as
vectors afterwards. For CLUT4 and CLUT8 with a 32-bit palette and no
index shift (by far the most common), and for 8888, the texels are now
gathered in lanes: the index bytes, the nibble, the mask and start, then
the palette entries. CrossSIMD gets Vec4S32::Gather and GatherU8 for it,
one load per lane. LocoRoco executes 5.5% fewer instructions, Burnout 3%.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 af72713272 SoftGPU: Pick the drawing thread count and lingering per platform
Measured on an M1: lingering cost wall time at every length tried, as
waking a thread is cheap on macOS, and seven or eight drawing threads
beat six. So Apple platforms use eight and don't linger, and Android
doesn't linger either (spinning costs battery). Windows and Linux keep
six and lingering, which was measured under WSL2 only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 a00ead3cb1 SoftGPU: Draw a run's primitives while its vertices are transformed
StartRun waited until all of a run's vertices were transformed before
the first primitive was assembled and binned. Now each chunk records the
run it was done for, and the draws take each vertex as soon as its chunk
is done (RunNeed), doing other chunks themselves while it isn't, so the
emulation thread bins while the helpers transform. Whatever reuses the
decode buffer or the transform state finishes the run first
(FinishRun), and so does its last draw.

God of War takes 3.5 s for five emulated seconds (from 3.6 s), Wipeout
3.2 s (from 3.3 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 6096ea682c SoftGPU: Splat Vec4s in registers, and three more stalls
Vec4<int> and Vec4<float>'s AssignToAll went through the four value
constructor, which stores the values one by one, so whatever loaded the
vector next stalled: the edge functions' setup per triangle and tile,
among others. They now splat in a register on SSE and NEON.
ReadSpanDepth loads its four depths as one vector where they're stored
together, rather than through an array, and ComputePlane is inline, as
returning its plane through memory stalled the UV planes' setup the same
way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 c33c848c61 SoftGPU: Find a self-texturing region's tiles without 64-bit divisions
ForTargetTiles divided by the framebuffer stride in 64 bits twice per
texture row, and by the bytes per pixel and tile sizes, which are powers
of two. The offsets fit 32 bits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 bcc52b61f3 SoftGPU: Build a triangle's start and UV ramps in registers
The edge functions' start built a constant vector through Vec4<int>'s
four value constructor, which stores the values one by one, and the UV
ramps went through a stack array: each reloaded as a vector, a store
forwarding stall per triangle and tile. The constant is now loaded from
a static array, and the ramps {0, d, 2d, 3d} are made from d in lanes.
Wipeout takes 3.4 s for five emulated seconds (from 3.5 s), God of War
3.6 s (from 3.7 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 40d055c84c SoftGPU: Transform shorter runs, and single draws, as runs
A run is faster than its draws one by one even when it's too short for
the helpers to join (16 vertices beat 32, 64 and 128 in God of War),
and a single large draw benefits from the helpers too (Wipeout). Chunks
are now 32 vertices. God of War takes 3.7 s for five emulated seconds
(from 3.9 s), Wipeout 3.5 s (from 3.54 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 ce1f8d0a9e SoftGPU: Inline TransformClutIndex
The bilinear quad sampler called it for every CLUT texel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 ce83a7d366 SoftGPU: Keep a tile's tail off the drawing threads' cache line
A tile's head and busy flag, which the drawing threads write, shared a
cache line with its tail, which the emulation thread writes for every
item it hands the tile. And a drawing thread waiting for work read
every tile's head and tail again and again. Now the tail has a line of
its own, and a waiting thread watches one counter that DistributeItems
bumps, looking at the tiles only when it changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 061038e0bb SoftGPU: Don't skin in the vertex decoder for the transform unit
ApplyGESkinning computes skinned positions and normals the way the GE
does and overwrites what the decoder skinned in float, so that was
wasted, 4% of the emulation thread in God of War. A new decoder option,
callerSkins, leaves out the weights, position and normal steps for
weighted vertices outside through mode and only reserves the float
position and normal. The software renderer's transform unit gets its
own decoders with it; bounding boxes, splines and the debugger still
use the ones that skin. Checked to give the same output with the vertex
decoder JIT as without the option.

God of War takes 4.0 s for five emulated seconds (from 4.1 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:01:49 +02:00
Henrik RydgårdandClaude Opus 5.5 93a3c55f47 SoftGPU: Queue primitives without locked increments or a stack copy
BinQueue's head, tail and size were atomics, so each push was two
locked increments, but only the emulation thread uses them: the drawing
threads read items, which the tiles publish. And each BinItem was built
on the stack in pieces and then copied with wider loads, a store
forwarding stall. Now the indices are plain and items are built in
place (PushItem). AddTriangle was 8% of the emulation thread's time in
God of War, which now takes 4.1 s for five emulated seconds (from 4.2 s),
Wipeout 3.5 s (from 3.7 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:56:03 +02:00
Henrik RydgårdandClaude Opus 5.5 fe57332aa1 SoftGPU: Keep the dirty flags in 32 bits
There are 24 of them. SoftDirty(-1), which some paths set, now means
just those (plus unused bits up to 31) rather than 64 bits of which
40 never got cleared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:56:03 +02:00
Henrik RydgårdandClaude Opus 5.5 bdac4fff45 SoftGPU: Transform a run of draws' vertices together, on helper threads
God of War draws thousands of short strips per frame, each continuing
the previous one's vertices with nothing changed in between. At the
first draw of such a run, SoftGPU now looks ahead in the display list
for the draws that follow it, past only commands that can't change
anything (NOPs, unchanged state, VADDR to where the vertices continue),
and the transform unit decodes the whole run at once and then skins and
transforms it in chunks that two helper threads take alongside the
emulation thread. The draws then read their vertices from that, and the
UV and normal they carry to the next draw are updated as reading their
last vertex would.

The helpers wait a moment for the next run before their tasks end, as
the drawing threads do: waking them costs the emulation thread a system
call. The thresholds were picked under WSL2, where that costs more than
on bare metal, so they're worth measuring again there.

God of War takes 4.2 s for five emulated seconds (from 4.5 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:55:57 +02:00
Henrik RydgårdandClaude Opus 5.5 f02936575d SoftGPU: Multiply small lane values with 16-bit multiplies
SSE2 has no 32-bit lane multiply, so Vec4S32::Mul is emulated with two
pmuludq and shuffles, and the bilinear sampler spent a tenth of its time
there. The texel row offsets, the texture functions, fog and blending
only multiply values that fit 16 bits, which Mul16 does in one pmaddwd.
The bilinear lerp of two channels packed as 16-bit halves uses the new
MulHalves (pmullw), which multiplies both at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 bed877dabc SoftGPU: Skin four vertices at a time
ApplyGESkinning took a vertex at a time with its components in three of
four lanes. Now four vertices go through each bone together, a lane
each, with the bone matrix entries, the weights and the positions taken
apart once for the GE's multiply (GEMulOperand). A bone whose weight is
zero in some lanes adds zeros there, leaving the sums as skipping it
would. The per-component order of the adds is the GE's as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 5151e5d10f SoftGPU: Transform vertex positions four at a time
A draw's vertices now go through the position stage of ReadVertex (the
clip coordinates, the viewport, the screen rounding and the GE's fog)
four at a time, a vertex per lane, before the rest. GERowSumLanes is
GERowSum4 with a lane per vertex instead of per matrix row, sharing the
positions' parts between the five rows, and GERecip4 is GERecip for four
w's. A group with a lane those don't handle (non-finite coordinates, a
w that's zero, denormal or too large) goes through ReadVertex as before.

Wipeout takes 3.8 s for five emulated seconds (from 4.2 s), God of War
4.7 s (from 5.0 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 1f891ac0e9 SoftGPU: Keep drawing threads around briefly, and use at most six
A drawing task ended as soon as it found no work, so the emulation
thread woke threads again and again, each a system call it paid for
(about 15 us under WSL2). A thread now waits up to 200 us for more,
unless the emulation thread is waiting for the drawing to finish.

More drawing threads also slow the emulation thread, which is what
limits these games, more than they help it: in God of War and Wipeout
on a 16 core Ryzen, six beat four, eight and the 31 the thread manager
has. With both, God of War takes 5.0 s for five emulated seconds (from
5.2 s with four threads, 7.4 s with all), Wipeout 4.2 s (from 4.7 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 63213a69f9 SoftGPU: Take a draw's matrices apart once for GERowSum4
GERowSum4 split every matrix entry into exponent, fraction and sign for
each vertex. GERowSumRows does that once when the transform state is
computed, for the clip matrix, the world matrix's normal rows, the fog
row and the texture matrix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 e971bb1439 SoftGPU: Transpose a span's colors in registers for per-pixel texturing
ApplyTexturing's per-pixel path built each pixel's color from four
strided channel values, which the compiler stored in pieces and then
loaded as a vector, a store forwarding stall per pixel. On x86 and
ARM64, where the sampler takes its color in a register, the channel
rows are transposed in registers instead, and back afterwards.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 ce47944216 SoftGPU: Gather a span's UV plane values in registers
GEUVSpan stored the four 64-bit plane values, and the reciprocal table's
entries, to arrays one by one and loaded them as vectors, a store
forwarding stall each time, which took most of its time on x86.
CrossSIMD gets LoadS64Low (the low halves of four 64-bit values) and
LoadPairs (four pairs of 32-bit values split into two vectors) to do it
without memory. Wipeout runs about 2% faster.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 716aa36ca8 SoftGPU: Queue four times as many primitives
With 2048, God of War filled the queue often enough that the emulation
thread spent a fifth of its drawing time waiting for room. With 8192 it
stays ahead of the drawing threads: about 6% less wall time there (with
eight drawing threads), and the same in Wipeout. Larger made no
difference.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 1752a2e53b SoftGPU: Transform a vertex's screen position in vector lanes, without stalls
ReadVertex took the GE's viewport arithmetic one coordinate at a time,
with a reciprocal of w for each, and stored the position, the UVs and
the whole vertex in pieces that were then reloaded with wider loads,
each a store forwarding stall. x, y and z now go through one GERecip and
the four-lane multiply and add (the scalar path stays for non-finite
input and zero or denormal w), the position is loaded as a vector, and
the vertex is written straight into its destination.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 a89fdb2c9a SoftGPU: Remove the pixel and sampler JITs
The C++ paths draw and sample four pixels at a time in vector lanes,
while a JIT function only does one, and with a JIT function in use the
span and quad paths were skipped. On x86 the JITs had become slower:
Wipeout took 13% more wall time and 25% more CPU with them, God of War
3% more. They were also a second implementation of every GE rule, and
ARM never had them.

The SoftwareRendererJit setting goes with them. RasterizerRegCache.h
keeps only the types the pixel and sampler functions take, as
RasterizerTypes.h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:48 +02:00
Henrik RydgårdandClaude Opus 5.5 3cd6bf9d27 Tantalus depth fog: read depth at each render pixel's own sub-position
The Tantalus games (Cars Race-O-Rama, MX vs ATV Reflex and Untamed, SpongeBob)
turn depth into fog with a CLUT16 read of the swizzled depth mirror, stretched
2x into a 565 view of an 8888 buffer. A following 8888 copy writes the scene's
RGB and keeps the alpha, so each pixel's fog factor is the upper 565 half.

At a higher render scale, the depth X swizzle snapped every render pixel to its
PSP pixel's left edge, so the fog came out in PSP-pixel blocks. It now keeps the
position within the PSP pixel. That leaves the upper half sampling the right
half of the depth texel, half a pixel off the scene, which drew magenta halos on
the right of objects in Cars. With the new DepthCLUTIntoHalves compat flag, the
depal pass outputs twice as wide so each render pixel reads its own 8888 pixel's
depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:48:05 +02:00
Henrik RydgårdandClaude Opus 5.5 97a64d597e Keep the render pixels within a PSP pixel in 16/32-bit reinterprets
At a higher render scale each PSP pixel spans several render pixels, and the
reinterpret mapped them all to the PSP pixel's first one, which pixel-doubled
the result horizontally. A render pixel k into its PSP pixel now reads the
render pixels k into the PSP pixels it's made of. Cars Race-O-Rama renders at
the full render scale again, without the magenta stripes along edges that the
doubling caused. 1x is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:23:59 +02:00
Henrik RydgårdandClaude Opus 5.5 f8187919dc CLUT8 depal: Pick the byte the GE does where a pixel center is on a byte edge
Reading a 16-bit framebuffer as CLUT8 with u = 1 + 2x puts every pixel center
exactly on a byte edge. The GE lands on the edge only when the sprite's u
gradient is exact (gpu/probe exp256), otherwise just below it. The software
transform now nudges the short sprites' u down a little, sprite by sprite, and
the shader truncates to the GE's 1/16 texel. SOCOM keeps reading its pixels'
high bytes; Syphon Filter and Resistance: Retribution's 64x128 bright pass
now reads the next pixels' low bytes, as on the PSP.

Syphon Filter's warehouse and dark room dumps get much closer to the PSP
(18310 -> 7192, 9885 -> 3925 px on Vulkan), and Killzone: Liberation's improve.
Resistance's and Syphon Filter mission 5's get further away, from the 5551
precision difference their bright pass also depends on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 5232627ba2 GEMath: GESpriteUPlaneExact, whether a sprite's u gradient is exact
A sprite's u gradient comes from the setup reciprocal of its whole area, so it's
exact only for a power of two area, whatever the height: the Δy only cancels on
paper. Checked against exp87's sizes and exp256, where 64x128 CLUT8 sprites with
u = 1 + 2x read the next pixel's low byte on a PSP and 64x272 ones their own
high byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 962c542f6f CLUT8 depal: Sample like the GE, just below the PSP pixel's center
The GE samples at the pixel center like the GPU; the comment saying it uses the
corner was wrong. SOCOM's u = 1 + 2x reads the high byte because a sprite's
fixed-point UV gradient falls just short of a texel edge (gpu/probe exp87,
exp163). Move to the PSP pixel's center and 1/16 byte below it, the GE's step,
instead of to its left edge. That's the same byte for one or two bytes per
pixel, but not for four, where the edge could pick the component before. No
frame dump changes, at 1x or 2x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 70bc01925e Read the CLUT8 depal byte of an 8888 framebuffer exactly
int(t * 254.99) gave each byte value minus one: c / 255 * 254.99 lands just
below c. With 255.99 it's c for every byte. The texel also gets the best
available precision: in fp16 the product rounds, and 16 of the 256 bytes
came out one low. Burnout Dominator's car select lens flare gets closer to the
PSP on Vulkan (57 -> 48 px); nothing else in the frame dump set changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:04:23 +02:00