Commit Graph
3729 Commits
Author SHA1 Message Date
Henrik Rydgård 88f4c12437 Merge pull request #22553 from hrydgard/gpu-fallback-fixes
GLES and fallback rendering fixes from running the framedumps on Android
2026-10-14 09:20:38 +02:00
Henrik RydgårdandClaude Opus 5.5 e48086484c Blend in the shader where stencil to alpha needs dual source blending we lack
A draw that blends with the source alpha while setting the stencil (the
alpha on the PSP) needs dual source blending to write both. Without it the
alpha came out wrong, and later draws that blend with the destination alpha
differed: WipEout Pure, Juiced 2 and 16131 UCES00001 on a Mali-G71 and a
PowerVR GE8320. With framebuffer fetch, blend those in the shader instead,
which matches the Mac's Vulkan render now. Without fetch it would take a
framebuffer copy per draw, too slow for something this common.

A draw that writes no color can't need the source alpha, so it writes the
stencil as alpha directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:33 +02:00
Henrik RydgårdandClaude Opus 5.5 b9780bc132 Keep the stencil when resizing a framebuffer
ResizeFramebufFBO copied color and depth into the new FBO but left its
stencil cleared. Need for Speed Shift grows its framebuffer from 480 to 512
wide just before stencil testing, and the frame came out far too dark on
Android (112k px off the PSP on an Adreno 840, now 4k). MoltenVK happened
to keep it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:33 +02:00
Henrik RydgårdandClaude Opus 5.5 3e4458c7b7 Rebind the render target after writing stencil to another framebuffer
PerformWriteStencilFromMemory bound the framebuffer it wrote the stencil to
and, unless it blitted, left it bound. When that wasn't the current render
target, the following draws landed in it. Draws that copy the target for
shader blending rebind it themselves, which hid this; with framebuffer fetch
they don't, and OutRun 2006's masked water draws went into the old 5551
buffer at the address instead of the new 565 one.

Over the framedumps set on Vulkan, 9 dumps get closer to the PSP (OutRun
ULUS10064_0002 12295 -> 483 px, F1 2009, God of War) and one a little further
(16131 ULUS10193, a slight brightness shift).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:32 +02:00
Henrik RydgårdandClaude Opus 5.5 b25344ab7b Discard against one-sided depth ranges in the fragment fallback
Without clip distances, the fragment shader's min/max Z discard only came on
when both minz and maxz were set, while the clip planes take each side on
its own. Pursuit Force's sky has minz 0 and maxz 300 and lies past 300, so
the PSP drops it (its replays of 11399 UCES00019 and 11216 UCUS98703 show
that), but GPUs without clip distances drew it over the scene: 13.9k px off
the PSP on a Mali-G71, now 8 as with clip distances. The discard can now
come with the fragment depth clamp, for the side that's at the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:32 +02:00
Henrik RydgårdandClaude Opus 5.5 026bbf56d6 GLES: Declare u_minZmaxZ highp in both shaders
The fragment shader's min/max Z discard declared it with the default
precision, the vertex shader with highp, and PowerVR refuses to link such a
program, so every draw that needed the discard was dropped. On a PowerVR
GE8320 that was Street Riders, Valkyria Chronicles, Flatout and Frontier
Gate, and with the discard taking one-sided depth ranges too (next commit),
most of Pursuit Force.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:32 +02:00
Henrik RydgårdandClaude Opus 5.5 5fb61b4e40 GLES: Use highp for the texcoord and the math in CLUT8 depal
Picking a byte out of a 16-bit framebuffer needs more than the defaults. The
texcoord reached the shader as a mediump varying, which Adreno keeps in fp16:
its byte coordinate (u times up to 2048) then picks the wrong byte for every
other one past 512, and SOCOM Tactical Strike's night vision came out striped
in the right half. On a PowerVR GE8320 the default lowp in the rest of the
shader made SOCOM's night vision and full screen effect far too bright (53k
and 17k px off the Mac's Vulkan render, now 0). Vulkan already uses highp.

Where the fragment shader has full precision, the vertex shader now writes the
texcoord in highp, and CLUT8 depal reads it that way and computes in highp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:32 +02:00
Henrik RydgårdandClaude Opus 5.5 7080085571 GLES: Apply the texture LOD bias in the shader
GL_TEXTURE_LOD_BIAS is desktop GL only, and the queue runner skipped it on
GLES, but GLES still reported sampler LOD bias as supported, so the bias was
dropped and textures used the wrong mip levels: Super Monkey Ball Adventure's
intro came out blurred on the Adreno 840. Report it unsupported, so the shader
applies it (GPU_USE_SHADER_LOD_BIAS), and give the GL backend the uniform that
Vulkan has in its uniform block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:33:32 +02:00
Henrik RydgårdandClaude Opus 5.5 9237bade22 x86 vertex decoder JIT: Fix the paths for constants out of RIP range
PosFloat loaded its clamp constants into XMM4/XMM5 but clamped with XMM2/XMM3,
and the S8/S16 morphs overwrote the morph weight pointer with the constant's
address. These only run when the JIT cache lands more than 2GB from the
constants, which CPUEquivalence's allocations made happen on Linux CI
(SplineTessellation). Checked by forcing every RipAccessible() false, which
SplineTessellation and VertexJit now pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:11:59 +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å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 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å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å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 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 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å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 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 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 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
Henrik RydgårdandClaude Opus 5.5 a404f2ae7c Use the best available precision for the CLUT8 depal byte coordinates
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>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 763bcde73e Sample the last texel where the projected q isn't positive
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>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 b9b1a29c42 Read CLUT8 from 16-bit framebuffers in the shader, replacing the SOCOM hack
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>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 8a852ec47c Fix the Burnout Dominator lens flare at even render scales
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>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 b28d70c6fe Fix the SOCOM CLUT8-from-5551 depal: index and texture width
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>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 b28a975d88 Update the mip bias uniforms when only the bias changes
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>
2026-10-10 01:04:23 +02:00
Henrik RydgårdandClaude Opus 5.5 1fe8071875 Vulkan: Apply the texture LOD bias in the shader where samplers ignore it
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>
2026-10-10 01:04:23 +02:00
Henrik Rydgård 0ac523c4b5 Merge pull request #22475 from hrydgard/cars-brightness
Tantalus games: Fix brightness, fix vertical stripes
2026-10-09 19:22:00 +02:00
Henrik RydgårdandClaude Opus 5.5 ce310e69a6 Keep the GE-exact FIXA/FIXB blend within fp16's range
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>
2026-10-09 18:50:22 +02:00
Henrik RydgårdandClaude Opus 5.5 b882155d23 Only use the GE-exact FIXA/FIXB blend with highp in the fragment shader
Old GLES devices without highp fragment floats get the previous plain multiply.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 18:43:26 +02:00
Henrik RydgårdandClaude Opus 5.5 f35ddce87f Vulkan: Do the GE-exact FIXA/FIXB blend math in highp
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>
2026-10-09 18:43:26 +02:00
Henrik RydgårdandClaude Opus 5.5 e896203f32 Reinterpret between 16- and 32-bit framebuffers by PSP pixel at higher render scales
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>
2026-10-09 17:01:37 +02:00
Henrik RydgårdandClaude Opus 5.5 a9b18a9192 Compat: Read back Cars Race-O-Rama's luminance reduction for its brightness adaptation
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>
2026-10-09 17:01:37 +02:00
Henrik Rydgård 7ec4d02b24 Merge pull request #22472 from hrydgard/msaa-fixes
MSAA fixes by Claude
2026-10-09 16:53:35 +02:00
Henrik RydgårdandClaude Opus 5.5 ed7de3ea76 Readback: Only scale a framebuffer down when it's rendered above maxScaleFactor
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>
2026-10-09 16:22:05 +02:00
Henrik RydgårdandClaude Opus 5.5 896eb31e87 Copy depth for offset rendering with MSAA too
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>
2026-10-09 15:49:15 +02:00
Henrik RydgårdandClaude Opus 5.5 4202368b1e Vulkan: Look up multisampled framebuffers through a CLUT by their first sample
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>
2026-10-09 15:49:15 +02:00
Henrik RydgårdandClaude Opus 5.5 70c5b8f07f Vulkan: Interpolate texture coordinates and colors at the centroid
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>
2026-10-09 15:49:15 +02:00
Henrik RydgårdandClaude Opus 5.5 71367037c0 Copy depth between framebuffers instead of drawing it, where supported
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>
2026-10-09 15:49:15 +02:00