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>
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>
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>
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>
The display can point inside a larger framebuffer: Silent Hill flips
between two halves of a 1024-wide 565 buffer. Presenting it found the
framebuffer and the offset, but screenshots looked up only an exact
address match, and otherwise read RAM, which with a hardware renderer
holds something stale (Silent Hill: Origins' savestate thumbnails showed
the loading screen). Screenshots of the display now use the same lookup
and read back the 480x272 at the offset.
Headless screenshots go the same way: eight Silent Hill frame dumps
(Origins and Shattered Memories) now match the PSP exactly on Vulkan and
OpenGL, from 0.8k-104k differing pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game does shadow mapping without depth textures (#16126):
- It renders the scene once into 44170000 with depth, then draws it again
into 44170400 (the same buffer 256 pixels to the right) with the same
depth address and an EQUAL depth test. We render 44170400 into the
same framebuffer at an X offset, which moves the depth test along with
the color, so it compared against the wrong depth and the shadow pass
drew nothing. Copy the depth over to the offset once per pass.
- It reads its 256x256 565 light-depth map as CLUT32. The reinterpret to
8888 went into an existing 8888 buffer at the same address that was
only 128x128, which cut the map off below row 128. Grow it first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewport (and u_xywh, which must match it) used the framebuffer's
estimated drawing size, so anything drawn past it was clipped even though the
buffer has the rows and the scissor allows them. Tokimeki Memorial 4 draws its
characters to y 271 into a buffer whose height was guessed from a 256 tall
viewport: the bottom 16 rows were cut off, a line through every character, and
its arms broke when the two halves were stitched (#6379, #11249).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ResizeFramebufFBO makes the resized buffer the current render target, and
the rebind after the upload then kept drawing to it. The game's draws to its
own render target went into the uploaded buffer until it changed the
framebuffer pointer. God of War: Ghost of Sparta and Chains of Olympus lost
most of their scenes this way (#12215, #11953, #16170, #11123, #12169).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With alpha above 0.5 the dst factor goes past 1, which 2x alpha in the shader
clamps: Super Stardust composites its bloom at alpha 0xFF, doubling the dst,
and came out at 1x without the bloom brightening. Same through mode reasoning
as for DSTCOLOR + DOUBLESRCALPHA. Also fixes Ford Bold Moves Street Racing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GE doesn't clip rectangles: one with a vertex behind the camera or outside
the screen range is dropped whole (the software renderer's ProcessRect, from
gpu/clipping/guardband). The hardware backends drew them, stretched to the far
off-screen vertex.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result is dst * (src + 2a), up to 3x the dst, which fixed function
blending can't reach: doubling alpha in the shader clamps it to 2x. MotorStorm
Arctic Edge and Tomb Raider Anniversary brighten their whole frame this way
and came out much too dark. Through mode draws are mostly full screen passes
that don't overlap themselves, so the framebuffer copy is safe there; 3D draws
keep the approximation (copies there caused overlap problems in Silent Hill).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GE's bilinear filter truncates (4-bit weights, twice), and rounding the
GPU's finer filtered value made each texel about a level brighter. God Eater
Burst's bloom is faint and feeds back into itself pass after pass, so that
roughly doubled it: its dumps go from 15-31k px off the PSP to 0.4-3k, the
#17033 ones from 79k/107k to 25k/47k. Split/Second and Call of Duty: Road to
Victory improve too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GE's blend terms are floor((2c+1)(2f+1)/1024), which truncates, while the
hardware multiplies by f/255 and rounds, so every pass of a fixed color blend
chain came out up to a level brighter. God Eater Burst's bloom is many such
passes and ended up far too bright.
For additive FIXA/FIXB blends that aren't just 0 and 0xFFFFFF, the shader
now computes the src term exactly and lowers it by 0.5 - (2B+1)/1024, and the
blend constant is the GE's dst factor (2B+1)/512, so the hardware's rounding
of the sum lands on the GE's floor (except where the src term would go below
zero). Blend constants are passed as floats to make that possible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Skinning did each component's weighted bone entries and its chain of
GEAdds separately in scalar code. The components now go in lanes, with
GEMulFloat24x4 for the weighted entries, one-term row sums for the
products and GEAdd4 for the sums, in the same order. Checked against the
scalar version on every vertex of 143 skinned frame dumps.
GEMulFloat24x4 matches ProductToFloat24, so inf and NaN bone entries
behave as before. GEAdd4 handles two zero operands without its fallback.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The same lerps as the GE-exact Bezier path, four at a time. At each v step,
four control columns per vector share the v blending factors; then each row
is evaluated at four u steps, each lane with its own six blending factors.
A vector's u steps can straddle a segment boundary, so the row's points are
gathered per lane, or splatted when all four are in one segment.
The unit test compares it bit for bit with the scalar code, including
tessellations that split a vector across segments. The GE-exact spline
benchmark runs about twice as fast on an M1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four of the GE's lerps at once: both operands go to 16-bit fixed point at
the larger exponent by scaling with a power of two and truncating, then the
integer lerp (split into byte halves so both multiplies fit 16 bits), then
back. The patch's four control columns run in the four lanes at each v
step, then four u steps of a row at once, with the row's first level of
lerps converted once per row. Lanes whose exponents are tiny take the scalar
lerp, and the normals' cross product stays scalar for now.
The scalar code stays as the reference (g_splineGEScalar), and the unit test
compares the two bit for bit on random control points. The GE-exact Bezier
benchmark goes from 3.4 to about 8.8 Mverts/s on an M1; the float SIMD path
does about 100.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the software renderer, which now stores depth as the GE does, a
16-bit read or write at VRAM's 0x04200000 or 0x04600000 mirror goes to
where the GE's 16-bit or 32-bit color layout puts that depth pixel
(ppsspp-re geprobe exp88: the mirrors are the layout's exact inverse).
Games access depth there with nothing else, so other widths stay plain.
The accessors check the address, then whether the GPU core is the
software renderer (CoreParameter); the hardware renderers keep depth plain
and handle the layout their own way. The renderer keeps the layouts up to
date with the EDRAM address translation. With it, the JITs send 16-bit
loads and stores to the interpreter, and the IR passes don't change a
load's width to or from 16 bits.
The pspautotests that read depth through the mirrors now do so 16 bits
at a time, re-recorded on hardware unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Where the GE stores each depth pixel, from the EDRAM address translation
and the color format: with 32-bit color, address bits 5 up to log2(T) - 1
rotate left by one, then (T << 3) | 0x40 is XORed in (0x600, and bits 9
and 10 swapped, at T 0). The CPU sees the same mapping through VRAM's
0x04200000 and 0x04600000 mirrors. Measured in ppsspp-re geprobe exp8-10
(every pixel of 512x272 buffers at each T) and exp88; the unit test checks
it against the model fitted there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tessellator's output was queued as ordinary vertices and run through
a vertex decoder at flush, although the decoded layout is the same as
SimpleVertex. That decode did two things: the UV scale/offset, and
clearing gstate_c.vertexFullAlpha. The tessellator now does both, and
SubmitCurve writes its output where DecodeVerts would have put it and
queues it as predecoded. DecodeVerts only copies such a draw, when it
decodes elsewhere, like the Vulkan and GLES push buffers.
The software renderer decodes by itself and keeps the old path, as do
through mode curves and anything with draws already queued.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each row of vertices is evaluated by folding the patch's four control point
rows into four columns with the v weights, then weighting those along u,
four vertices per register with one attribute component each. Cross
products, normalization and the pole test become plain per-lane math, and
the result is transposed into SimpleVertex order on the way out.
The weights are stored one row per term so four steps load as one vector,
and the control points are padded to full vectors in a single array.
Colors now round to nearest on every platform. The SSE path already did,
NEON and the scalar path truncated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The copy for texturing from the current render target was cropped to the
UV bounds, but bilinear filtering at the edges reads one texel beyond them,
which then came from whatever the temp FBO last held. On Vulkan that put
magenta streaks into Need for Speed Shift's sun glow, and also hit Call of
Duty, Burnout Dominator (#12831) and Assassin's Creed Bloodlines (#15821).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fragment Z is the PSP's Z / 65536, and flat draws land exactly on an
integer Z. floor(z * 65535) put that Z - Z/65536 just above the boundary, so
float error (which differs with W) could drop it by one: Patlabor redraws a
HUD icon at the same depth with GEQUAL, and the second draw failed on
Vulkan. Floor Z + 0.01 instead, and store Z / 65535, as clears and the depth
clamp path do. Also fixes Heroes Phantasia #6485.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rectangle's depth is flat, so right at a depth range limit our float
rounding drops or keeps the whole thing. Mana Khemia 2 draws a conversation
background at GE screen Z 1 with minz 1, and we computed 0, so it vanished
(#12858). When the GE's depth would land on the other side of the range
check, use it; otherwise keep ours, which matches the hardware-transformed
draws a rectangle may be depth tested against (rounding all rectangle depths
to the GE's broke the Need for Speed HUD).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without them nothing culls triangles beyond the far plane: depth clamp draws
them, as with the stands over the pitch in FIFA Soccer on macOS. Fixes FIFA
and Hellboy on OpenGL there, and DBZ Tenkaichi Tag Team, Monster Hunter 3rd
and Street Riders on both backends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With one vertex behind the near plane, the GE splits the remaining quad from
the vertex before the clipped one (gpu/probe exp43, exp44, as in SoftGPU's
Clipper.cpp). The other diagonal could put a far off-screen near-plane vertex
in both halves, so range culling dropped the whole triangle: the floor in
#16131's box test vanished. The index generator reverses triangles to flip
culling, so the batch's order is passed along.
Triangles entirely beyond the far plane are now culled even when in front of
the near plane, with the cull-distance epsilon, so z/w = 1.0000172 (exactly
1.0 to the GE) still counts as inside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing writes depth then, so the shader write is wasted. On Apple's OpenGL
it also broke Tales of Phantasia (PixelDepthRounding): once the shader writes
gl_FragDepth, a discarded fragment still writes stencil, so a color-tested
stencil pass marked the whole area and the background tiles drawn with
stencil EQUAL 0 vanished.
New FS ID bit, so both shader caches get a version bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the PSP the color and alpha/stencil write masks apply to clears like to
drawing (gpu/stencil/writemask, ppsspp-re geprobe exp254). Clear-mode draws
now take the same per-channel heuristic as other draws, and a clear with RGB
masked is drawn rather than done as a hardware clear.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>