Commit Graph
10274 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 60100c4254 - Fixes #13676
Vulkan: Batch Jackass' tiled in-place color grade

Jackass: The Game color grades the framebuffer in 32x64 tiles, one channel
per pass, texturing from the framebuffer it renders to. Each of the 225
draws became a depal pass plus a pass drawing it back, 450 render passes
per frame. A new queue hack, enabled by the TiledDepalHack compat flag,
sorts these pairs into waves of non-overlapping tiles and runs each wave
as one depal pass and one draw pass, leaving 6.

Output is byte-identical to before on both dumps from the issue, at 1x
and 3x on desktop, and at 1x on both phones below.

Time saved per post-processed frame, from replaying the dumps from the
issue with headless --bench (each replay renders that frame once):

  Redmi 9A (PowerVR GE8320):  ~145ms at 1x, ~180ms at 2x
  Unisoc UMS9632 (Mali-G57):  ~55ms at 1x and 2x

Either is more than a whole 33ms frame, so on those GPUs this pass alone
ruled out 30fps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:08:58 +02:00
Henrik Rydgård 4585f70500 Merge pull request #22545 from hrydgard/gpu-fallback-fixes
Fixes to GPU feature fallbacks
2026-10-13 16:46:34 +02:00
Henrik RydgårdandClaude Opus 5.5 85d751ebe1 Keep near plane clipping in the fragment depth clamp fallback
The fallback set the rasterizer's Z to a constant so nothing would clip, which
lost the clipping at the PSP's near plane too. Hardware depth clamp loses it
the same way, which the vertex shader can only make up for with a clip plane.

Give the rasterizer (z + w) / 65536 instead: 0 at the near plane, so it clips
there, and past w only beyond 65535 * w. And without clip distances, use the
fragment fallback instead of hardware depth clamp.

With --disable-gpu-features for all three on the Mac, the framedumps set now
differs from the full render by 15570 px instead of 184229 (WALL-E, Hokuto no
Ken, Frontier Gate, FF Zero). The full render doesn't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:06:49 +02:00
Henrik Rydgård b4b4390c49 Merge pull request #22543 from hrydgard/android-headless-gpu
Android: Add setup for headless GPU rendering test
2026-10-13 15:02:11 +02:00
Henrik RydgårdandClaude Opus 5.5 2d61d58bbe Make the fragment depth clamp fallback match hardware depth clamp
For GPUs without depth clamp (old Mali), found by comparing with
--disable-gpu-features=depth-clamp on the Mac:

- Write Z / 65536 like the vertex shader, not / 65535.
- Pass the fragment shader Z after the vertex depth rounding, which hardware
  clamp sees too (Puyo Puyo Fever 2's ground at Z = 0).
- Also clamp draws whose Z only touches 0 or 65535.
- Draws that skip the bounds check (skinned, morphed) get the fragment clamp
  and min/max discard wherever the hardware versions would be on, since those
  don't depend on the check either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:34 +02:00
Henrik RydgårdandClaude Opus 5.5 7c118f2ee3 Headless: Add --hardware-transform and --disable-gpu-features
--disable-gpu-features=clip-distance,cull-distance,depth-clamp acts as if the
GPU lacked those, to test the fallbacks for old Mali on any machine. It
replaces the simulateOldMali constant. --hardware-transform=false runs every
draw through software transform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:34 +02:00
Henrik RydgårdandClaude Opus 5.5 3193031e7b Bounds-check PRIM draws of any size, not only up to 2500 vertices
Bigger draws skipped the check and kept whatever clip flags the previous draw
left. Checked, the ones that cross the clip volume in Z get software clipping
where the GPU needs it: on the Mac (no cull distances), Valkyria Chronicles II
and Street Riders get closer to the PSP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:34 +02:00
Henrik RydgårdandClaude Opus 5.5 9073aad504 Cull and clip check Bezier and spline patches like PRIM
Curves skipped the per-draw bounding box check and relied on the vertex
shader's near clip plane. On old Mali (no clip distances), that left the
geometry behind the PSP's near plane to the GPU: Test Drive's hills reached
over the sky.

A patch lies within the convex hull of its control points, so run the PRIM
check on those: a patch entirely outside is skipped, and only one with points
outside the clip volume in Z goes through software transform. Missing clip
distances are now also a reason to software-clip, for PRIM too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:34 +02:00
Henrik Rydgård 0af0d86470 Merge pull request #22542 from hrydgard/faster-build
Faster CMake builds: libc++ transitive includes, less glslang/SPIRV-Cross, include cleanup
2026-10-13 12:57:43 +02:00
Henrik RydgårdandClaude Opus 5.5 e602aa1548 Add includes that were only arriving transitively
VertexDecoderRiscV.cpp got <cfloat> through SmallDataConvert.h, and three
headers used malloc/free without <cstdlib>, which libc++ no longer brings in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:36:06 +02:00
Henrik Rydgård 3c25f21bae Merge pull request #22541 from hrydgard/libretro-fork-fixes
Various bugfixes from libretro's experimental fork
2026-10-13 12:26:27 +02:00
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 ec76a4d56e GE dump playback: Refuse sections over 1 GB
A zstd frame doesn't always declare its size, and a small one can expand to
gigabytes, so this is the limit that keeps a crafted dump from exhausting
memory. The largest section in the 1042-dump test set is 82 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:05:35 +02:00
Henrik RydgårdandClaude Opus 5.5 9c2cf98b84 GE dump playback: Clamp the remaining writes, bound the loader's allocations
Follows up the command range validation:
- Don't require anything of the data commands' sizes or alignment. Version 2
  dumps have empty VERTICES and CLUT commands, and old recorders didn't align
  anything. Registers() drops a trailing partial word instead.
- Split register blocks bigger than the list buffer, which they overran.
- Clamp MEMCPYDATA and MEMSET to valid memory. Only their start was checked.
- Check the compressed sizes against the file, and the decompressed sizes
  against what the frame declares, before allocating. Some version 5 dumps
  leave the size out of the zstd frame; those are checked after decompressing.
- Don't let a large FRAMEBUF size wrap around in MarkNeededTargets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:01:29 +02:00
Acts1631 80b813f507 Validate GE replay commands before execution
Crafted ppdmp command ranges can extend past the decompressed push
buffer. Frame buffer playback can then copy adjacent host heap data
into emulated VRAM, exposing process memory through rendered output.

Validate each command's type, range, and minimum payload size before
constructing the replay executor.
2026-10-13 09:21:14 +02:00
LibretroAdmin 664f6ff5ad D3D11: work on a context that refuses ID3D11DeviceContext1
D3D11DrawContext takes its base context from the deviceContext
argument, and the blend state is bound through the base context, which
accepts an ID3D11BlendState1 as is. ID3D11DeviceContext1 is optional:
RetroArch's threaded video hands v1 cores a deferred-context proxy that
answers ID3D11DeviceContext only, and the core now starts on it.
2026-10-13 02:23:48 +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