Drawing a sprite that textures from its target in the GE's pixel order
made Burnout Dominator about 60% slower in wall time: it copies its
framebuffer onto itself in 32-pixel strips twice a frame, and those were
drawn pixel by pixel on the main thread. When each pixel reads only the
texel it writes (through mode, nearest, level 0, the target's format and
stride, texture coordinates equal to the pixel), the order can't change
what any pixel reads, so such a sprite is drawn like any other. Burnout
renders the same, within 3% of before the pixel order.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A sprite that reads what it draws goes through the texture cache pixel by
pixel: rows left to right, top to bottom, texture lines loaded as samples
first read them, and pixels reaching memory a 16-byte block at a time, a
pixel after the next block starts (four with bilinear filtering). Fitted to
geprobe exp81 and exp204-exp209 on hardware; Split Second's blur passes now
match the PSP.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Measured on hardware (ppsspp-re geprobe exp177-202, docs/ge-texture-cache.md): 64 lines of 16 bytes x 8
rows in 16 sets of 4 ways, LRU, the set index split between column and band bits by texel size. A line is
tagged by its position in the texture and its mip level only, so another texture read without TEXFLUSH
gets the first one's bytes where their lines coincide; lines load raw bytes on first read and nothing but
TEXFLUSH drops them, not rendering into the texture, block transfers or CPU writes.
TexCache steps every textured primitive through that, in submission order, from its texel footprint. A
line remembers where it came from and its bytes are copied only before that memory changes (drawing, block
transfers, copies, and anything at all when the CPU is about to run). While every line is from the one
texture read since TEXFLUSH and nothing has been copied, nothing read can differ from memory, so primitives
only log what they read, and TEXFLUSH usually drops the log unread. A primitive that reads bytes differing
from memory, or draws over what it reads, is drawn alone from its texture with what it read from the cache
laid over memory. That replaces SelfTextureSnapshot and its "fits in 8 KB" rule.
All the cache experiments match the PSP exactly except exp177, which samples a block transfer's
destination without TEXSYNC. Within a primitive this still loads every line before it draws; the GE loads
them as it rasterizes (serpentine rows, writes reaching memory a 16-byte block late), which Split
Second's blur (the one dump that changes) depends on. About 2% slower in Tekken 6 and God of War.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The software renderer kept depth linear in VRAM, where the GE stores it
in the layout GPU/Common/DepthSwizzle.h describes, from the EDRAM address
translation and the color format. So a texture or anything else sharing
VRAM with a depth buffer saw depth land in the wrong places (the player's
shadow in Silent Hill: Origins, #22415). Now every depth access goes
through the layout: the generic pixel functions, the 2x2 depth test, the
depth clear in 16-pixel runs, and the x86 pixel JIT, whose IDs carry the
translation so the layout's constants are compiled in. It changes with
the color format and sceGeEdramSetAddrTranslation, after a flush.
Reading VRAM through its 0x04200000 and 0x04600000 mirrors undoes the
layout, as the GE does: texturing from them (drawn from a linear copy
made after a flush), and block transfers from or to them (Hayate no
Gotoku restores its depth buffer through 0x04600000, #17878). Saved and
debugger depth buffers read through the layout, so they're linear.
Socom's lens flare dump now matches a PSP replay where it changes. About
2-3% more cycles in God of War, Tekken 6, Wipeout and Burnout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A full state ring flushed the binner, and the next state change filled
it again in the same way. Now the flush also doubles the ring, up to
32768 states, so it happens at most a few times. Nothing draws right
after a flush, so the ring can move then. In the benchmark games, no
more than 512 states were queued between flushes, so 4096 stays the
starting size.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The binner kept a bounding box of every queued item's range, updated for
each item, only to tell whether anything had been queued since the last
flush. A flag says the same. Expand, which grew the box, is now
ItemQueued.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each queued primitive adds its vertex flags (alpha, fog, colors) to its
state, and states are optimized for those flags before their items are
distributed. A state can still gain primitives after some of its items
are in tiles. Re-optimizing it then changed its pixel and sampler
functions and IDs while threads drew with them. With the x86 pixel JIT,
the span function could turn null between checking and calling it, as
in Coded Arms' frame dump. God of War hit this about four times a frame.
Now a state is live once any of its items is in a tile, until the tiles
are drawn and reset, and a live state isn't changed. A primitive that
would change a live state's flags starts a new, unoptimized state from
the same registers instead, optimized for its own primitives only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every tile a triangle touches sets it up again, so larger tiles do less
of that. The tile size is now picked at startup, as large as it can be
with at least sixteen tiles per thread over the screen. With fewer, a
scene whose drawing is concentrated on part of the screen leaves threads
idle: on eight threads, 128x32 tiles (four per thread) took LocoRoco a
third longer than 64x16, and God of War 5% longer. So eight threads keep
64x16, and four or fewer get larger tiles.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Waking a worker is a condition variable signal, a system call, and the main thread paid one per idle
worker every 48 queued items. With Tekken 6's tiny draws (about 5 vertices each) the workers keep
running dry, and those signals were about 10% of the main thread, which is what limits it there.
Now no more threads are woken than tiles have work, and only the first is woken from the main thread;
it wakes the rest.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A state that textures from what it draws to, or whose scissor reaches
past the stride, used to switch the binner to one thread, and every
primitive was drawn alone, copying the whole texture for its snapshot.
In Wipeout, God of War and GTA that was a third to half of the time.
Now it's decided per primitive. One is drawn after everything queued
and before what follows only if it needs that: it textures from texels
a queued primitive (or itself) writes, it writes texels a queued one
still has to read, or it reaches past the stride. Reads are bounded by
the vertices' texture coordinates plus the filter's neighbors and
mapped to tiles of the target; queued writes and reads are tracked per
tile. Everything else goes to the tiles as usual. The in-order ones are
split into bands of rows when large, since each pixel reads only the
snapshot, and the snapshot now only copies the previous primitive's
columns.
Same results on 263 frame dumps and 8 games.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each thread used to own a band of the screen, fixed when the work was
first split, and got its own copies of the primitives touching it. Bands
were rarely balanced (edge bands had half the work of middle ones, and
on big.LITTLE CPUs half the threads are slower), and primitives queued
up to the 2048 limit before the threads started.
Now primitives go to 64x16 tiles, as indices into the shared queue, in
batches of 16. A thread takes any tile with work and draws its items in
order; only one thread holds a tile at a time, so per-pixel order is
kept. A flush or a full queue has the main thread draw tiles too instead
of waiting, and when there's only a little work it does that alone
rather than wake the threads. Items are reclaimed once every tile has
drawn them. This also drops the per-thread queues, up to 23 MB.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A texture that fits in the GE's 8 KB texture cache keeps what was first
read until TEXFLUSH, across primitives and draws, so a later self-textured
draw doesn't see an earlier one's writes (gpu/probe exp174: three passes
over a 16x16 4444 buffer differ in 177 pixels with and without a flush
between them, both now exact). FF Type-0's (NPJH50443) blur; two of its
PSP replays become exact.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pixels past the framebuffer's stride land at the start of the next row,
which another binning task may be drawing, so the result raced. Tokimeki
Memorial (NPJH50127) blurs a 128 wide buffer under a full screen scissor;
its PSP replay is now exact (gpu/probe exp166).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GE samples through its texture cache, so a primitive that textures from
its own render target mostly reads the old contents (geprobe exp81, exp159,
exp160). Give each such primitive a snapshot, refreshing only the rows the
previous one drew.
3rd Birthday (ULUS10567) bloom becomes exact on PSP replays, Burnout
Dominator's glare 93% -> 98% and Ridge Racer's blur 40% -> 94% of pixels.
The remaining differences are rows where the cache's lazily loaded 8-row
blocks already see the primitive's own writes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Flush() on an empty queue now trims the state and CLUT rings, since its
callers flush because one is full and push right after.
- BinQueue::Full() uses >=, so an overshoot can't go unnoticed.
- IsExactSelfRender compares against the target the queued draws were
binned for, not gstate, which already has the next one during a flush.
- A depth test without depth writes marks the depth buffer as read.
- The DarkStalkers untextured sprite recomputes the binner state around it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
These own GPU objects, memory or refcounts in their destructors (or assert
there that they were torn down), so a copy would double-free. Nothing copies
them today; this keeps it that way. The manager base classes cover every
backend's subclass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A code space clear frees the functions the current state points to, but
the state was kept as long as the GE registers didn't change. Track the
clear generations and recompute, also when a compile during the state
computation clears the caches.
Also skip the binner flush for compiles on builds without the software
JIT, where Compile() does nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When we flush we mark all pending writes zero, but we rely on this being
set to detect self-render.
TRANSFORM_ALL was wrong as well, sometimes clearing BINNER_RANGE.
This generally detects overlap more accurately using a dirty rectangles
approach. Also detects render to self much more accurately, including
with depth.
Some games (i.e. VC3) benefit from an early drain, since they get more
done while processing more verts. Others finish the draw quickly, and
then cause significant overhead in queueing new threads.
This attempts to balance the two, and improves Call of Duty and Blade
Dancer.