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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
They reach the thousands (bytes across a wide framebuffer), past what fp16
holds to a fraction, and NVIDIA's Vulkan driver runs the default lowp as fp16
(as in #22481). The byte picked out of each pixel could then land on the wrong
one. No change on the M1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GE doesn't divide by a q <= 0: it samples the last texel in u and v,
whatever s and t are, in clamp and repeat modes alike (gpu/texmtx/negq). The
hardware backends divided, which mirrors a projected texture behind its
projector: Fate/Extra's character shadows ran up the school's walls (#18667).
The software renderer sampled (0, 0) instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shader depal CLUT8 mode now handles 565, 5551 and 4444 framebuffers
(the format goes in the depal format bits, which this mode didn't fill
before), rebuilding the 16-bit pixel and taking its low or high byte.
This replaces SOCOMClut8Replacement, which assumed the high byte.
- The GE samples a pixel at its corner, the GPU at its center. With u
stepping two bytes per pixel (SOCOM and Bend's games use u = 1 + 2x)
that's a whole byte, so the shader steps back to the corner.
- The CLUT index shift, mask and start are applied, as in the other
depal modes.
- The self-render copy region counted CLUT8's U in pixels, not bytes.
SOCOM, Star Wars: The Force Unleashed and the NPJG90068 dumps improve a
lot. Syphon Filter and Resistance: Retribution now read the right bytes,
which exposes a 16-bit precision difference in their bright pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flare's occlusion result is rendered into every other 16-bit pixel of a
palette whose other half is uploaded from memory, then read back as a CLUT.
Two things broke it at 2x:
- The CLUT8-from-8888 shader depal picked the byte out of each pixel using
the scaled texture size, so at 2x it read the wrong byte. It now works in
PSP units (from u_texclamp, which it now always gets) and samples the
pixel's center, not its edge, which is a render pixel boundary when
scaled.
- The palette upload was linearly filtered when stretched, blending each
16-bit half with its neighbour. Burnout Dominator joins Ridge Racer in
NearestFilteringOnFramebufferCreate.
1x and 3x only worked because their sample points happened to land inside
the right texels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The float depal shader built the index as a * 128 + b * 64, but the byte the
GE reads is the pixel's high byte, a << 7 | b << 2 | g >> 3, so the glow came
out about a third as strong as on the PSP.
The depal pass also reset curTextureWidth to the buffer width, undoing the
doubled width that U (which counts bytes) needs. Each 240-pixel strip then
sampled the whole framebuffer squashed in half, leaving a ghost and a seam
down the middle. The crop bounds mixed bytes and pixels the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DIRTY_MIPBIAS wasn't part of DIRTY_BASE_UNIFORMS, so on Vulkan and D3D11 a
new bias (u_mipBias for 3D textures, u_texLodBias since the last commit)
only reached the shader along with some other base uniform. The fast prim
loop also changed texlevel in auto mode without dirtying it. Fixes the F1
Grand Prix railings on Vulkan, where the texture was drawn from the
smallest mip level.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MoltenVK doesn't support a sampler mipLodBias (the portability subset's
samplerMipLodBias is false), so the PSP's LOD bias did nothing on Mac and
iOS, and biased textures came out a mip level blurrier than on OpenGL and
the PSP: MLB's signs and crowd, Burnout Legends, MediEvil, Ridge Racer 2.
Query the portability subset, and where the bias is missing, pass the auto
mode bias to texture() from a uniform instead.
VK_ENABLE_BETA_EXTENSIONS, which declares the portability subset, is now
defined for App Store iOS builds too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compute the product as (c+0.5) * (2f+1)/512, which is still exact in fp32, and
use mediump where highp isn't available instead of falling back to the old
blend. Simulated over all inputs, about 3% of fp16 results come out a level
too bright, against nearly half with the plain blend factors. With mediump
forced on NVIDIA's Vulkan driver (which runs it as fp16), the Split/Second
bloom dumps land within 2% of the highp result.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fragment shader's default precision is lowp, which NVIDIA's Vulkan driver
runs as fp16, and the product (2c+1)(2f+1) reaches 511*511, past fp16's range.
Split/Second's bloom saturated whole channels; its two bloom dumps go from MSE
2354/9305 to 80/153, the same as OpenGL.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both DirtyVRAM and GetTargetFlags counted blocks from the size alone, so a
range starting partway into a block missed its last block: a write there didn't
mark it dirty, and a snapshot didn't check it. And GetTargetFlags marked the
partly covered last block clean, since its `i < blocks` was always true. Either
way a later snapshot could be flagged unchanged while the dump didn't have the
data. Memcpy and memset also marked partly covered blocks clean.
Checked against the recorder's own copy of what the dump delivered: recording
from savestates, 4511 of Cars Race-O-Rama's unchanged snapshots and 103 of MX
vs ATV Reflex's didn't match VRAM (small textures at unaligned addresses), and
none do now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 16-to-32-bit reinterpret sampled a quarter texel either side of the
render pixel, which only lands in the two 16-bit halves of a 32-bit pixel at
1x: at 3x, two of every three render pixels took both halves from the same
16-bit pixel. The 32-to-16-bit one picked the half by render pixel too. Now
both go by the PSP pixel. Cars Race-O-Rama rewrites its 8888 frame through
565 views in 16-pixel columns, and drew vertical stripes at 2x and up
(#15914); now it renders like 1x. Nothing changes at 1x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game reduces the scene to 8x8 on the GPU, block transfers that into RAM,
and the CPU sets its brightness adaptation from it. With BlockTransferAllowCreateFB
the copy went into a GPU-side buffer instead, the CPU read stale RAM, and the
game kept getting darker on the hardware backends (#15914).
New compat flag BlockTransferReadbackSmall: transfers of up to 64 pixels into
RAM are still read back, a frame late, so it doesn't stall. Brightness from
the issue's savestate now follows the software renderer's (72 vs 84 at 6 s,
84 vs 95 at 20 s, from 25 and falling), at the same speed as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Me & My Katamari textures a 512x512 render target through 0x04288000, and later
snapshots read the textures past its depth buffer at linear addresses. Through
the swizzled depth mirror those came back permuted, which left a black band
over the horizon. Other snapshots at a mirror still go through it, since they
hold depth as the game saw it there. Software now matches the PSP on the three
Katamari dumps (#16131).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recorder leaves VRAM out of a snapshot as unchanged when an earlier snapshot
had it, and that earlier one can be a render target's, which software playback
skipped. Copy such a target snapshot (around what the replay drew), as the PSP
replayer now does. Software now matches the PSP on the three Madden NFL 10 dumps
(#16131), Need for Speed ProStreet (#16175) and Hammerin' Hero, and no dump gets
worse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadFramebufferForDebug scaled the buffer down, cropped to its drawn size,
whenever the render width was more than the drawn width times maxScaleFactor.
At 1x that's any buffer wider than what has been drawn in it, though nothing
needs scaling.
Headless compares at maxScaleFactor 1, so this cut off most of the screen in
Gangs of London, which draws a 256 px wide region: its dumps went from 78 to
62,017 pixels differing from the PSP.
Now it only scales down when the render scale is above maxScaleFactor, and
then scales the whole buffer, since a display at an offset can sit past the
drawn width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShiftDepthForOffsetRendering copied through a single-sampled temp buffer, so
with MSAA CopyDepthRect fell back to a depth blit, which Vulkan can't do with
multisampled images (PerformBlit asserts). Give the temp buffer the same
samples and copy; multisampled depth is never blitted or drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MSAA, a framebuffer textured as CLUT indices was read resolved, so the
indices along edges were averages of the samples and looked up wrong colors.
Dragon Ball Z: Tenkaichi Tag Team draws its outlines from a CLUT32 lookup of
its framebuffer, and they came out noisy and miscolored. Copy each pixel's
first sample with texelFetch into a temporary buffer and look up that.
This needs the multisampled color images to be sampled, and transitioned
back to attachment layout before rendering to them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MSAA, a pixel that a primitive only partly covers otherwise
extrapolates them to the pixel center, outside the primitive, and samples
textures outside its UV range. Without MSAA nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BlitFramebuffer drew depth with a raster copy. With MSAA that reads the
single-sampled depth, which is never resolved: Silent Hill: Origins resizes
its light buffer mid-frame, and with garbage depth its depth-EQUAL light
passes drew nothing, so the flashlight was missing. The raster copy isn't
exact without MSAA either: Burnout Dominator's recursive rendering (#11100)
goes from 112k to 6k pixels off the PSP with a copy. Now depth is copied
wherever the backend can copy just part of it (Vulkan).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CALL/RET only check stackptr against exactly 32 and 0, and getList() doesn't
check the ID at all.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ConvertStencilFunc5551 rewrites INCR to INVERT when the test is EQUAL 0,
which is only right where the test passed and the value is known to be 0.
It rewrote sFail too, so a pixel that failed against a set stencil got it
cleared again. Warriors Orochi 2 draws its sky in layers that each test
EQUAL 0 and INCR on pass and fail, so every other layer drew again over
the previous one and covered the mountains (#20867): 28650 -> 10285
pixels off the PSP on Vulkan and OpenGL. NOTEQUAL 0's INVERT to ZERO had
the same problem on sFail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>