SoftGPU: Self-texturing primitives sample the texture as it was before them

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>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-02 15:09:43 -06:00
1 parent 7df47b336b
commit 74e2c8f7ee
3 files changed
+63 -1

No files matched your search

+52 -1
View File
@@ -273,8 +273,51 @@ void BinManager::UpdateState() {
ClearDirty(SoftDirty::BINNER_RANGE);
}
pendingOverlap_ = pendingOverlap_ || selfRender;
selfRender_ = selfRender;
ClearDirty(SoftDirty::BINNER_OVERLAP);
}
states_[stateIndex_].selfTexture = selfRender_;
}
// The GE samples through its texture cache, so a primitive that textures from the buffer it draws to
// mostly sees that buffer as it was before the primitive (exp81, exp159, exp160). The cache actually
// fills 8-row blocks as the primitive first reads them, so rows drawn before then show through; this
// doesn't model that.
const RasterizerState &BinManager::SelfTextureSnapshot(const BinItem &item, const RasterizerState &state) {
constexpr uint32_t mirrorMask = 0x041FFFFF;
const uint32_t bits = textureBitsPerPixel[state.samplerID.texfmt];
const uint32_t fbBpp = state.pixelID.FBFormat() == GE_FORMAT_8888 ? 4 : 2;
const uint32_t fbStrideBytes = state.pixelID.cached.framebufStride * fbBpp;
selfTexState_ = state;
for (int i = 0; i <= state.maxTexLevel; ++i) {
const u8 *src = state.texptr[i];
const uint32_t bytes = state.texbufw[i] * bits / 8 * state.samplerID.cached.sizes[i].h;
if (!src || !Memory::IsValidRange(state.texaddr[i], bytes))
continue;
std::vector<u8> &buf = selfTexBuf_[i];
if (!selfTexValid_ || selfTexAddr_[i] != state.texaddr[i] || buf.size() != bytes) {
buf.assign(src, src + bytes);
selfTexAddr_[i] = state.texaddr[i];
} else {
// Only the rows the previous primitive drew have changed.
const DrawingCoords tl = TransformUnit::ScreenToDrawing(selfTexLastRange_.x1, selfTexLastRange_.y1);
const DrawingCoords br = TransformUnit::ScreenToDrawing(selfTexLastRange_.x2, selfTexLastRange_.y2);
const int64_t rowsStart = (int64_t)(drawTargetAddr_ & mirrorMask) + tl.y * fbStrideBytes;
const int64_t rowsEnd = (int64_t)(drawTargetAddr_ & mirrorMask) + (br.y + 1) * fbStrideBytes;
const int64_t texStart = state.texaddr[i] & mirrorMask;
const int64_t start = std::max(rowsStart, texStart) - texStart;
const int64_t end = std::min(rowsEnd, texStart + bytes) - texStart;
if (start < end)
memcpy(buf.data() + start, src + start, (size_t)(end - start));
}
selfTexState_.texptr[i] = buf.data();
}
// A depth write could change the texture outside the color rows, so then copy all of it each time.
selfTexValid_ = !state.pixelID.depthWrite;
selfTexLastRange_ = item.range;
return selfTexState_;
}
bool BinManager::HasTextureWrite(const RasterizerState &state) {
@@ -536,9 +579,17 @@ void BinManager::Drain(bool flushing) {
if (taskRanges_.size() <= 1) {
PROFILE_THIS_SCOPE("bin_drain_single");
// Anything (transfers, the CPU) may have written memory since the last drain.
selfTexValid_ = false;
while (!queue_.Empty()) {
const BinItem &item = queue_.PeekNext();
DrawBinItem(item, states_[item.stateIndex]);
const RasterizerState &state = states_[item.stateIndex];
if (state.selfTexture) {
DrawBinItem(item, SelfTextureSnapshot(item, state));
} else {
selfTexValid_ = false;
DrawBinItem(item, state);
}
queue_.SkipNext();
}
} else {
+9
View File
@@ -268,6 +268,14 @@ private:
std::unordered_map<uint32_t, BinDirtyRange> pendingReads_;
bool pendingOverlap_ = false;
// Whether the current state textures from what it draws to, and the texture as it was before the
// primitive being drawn for one that does.
bool selfRender_ = false;
Rasterizer::RasterizerState selfTexState_;
std::vector<u8> selfTexBuf_[8];
uint32_t selfTexAddr_[8]{};
bool selfTexValid_ = false;
BinCoords selfTexLastRange_{};
bool creatingState_ = false;
// JIT clear generations when the current state was computed.
int jitGen_ = -1;
@@ -288,6 +296,7 @@ private:
void MarkPendingWrites(const Rasterizer::RasterizerState &state);
bool HasTextureWrite(const Rasterizer::RasterizerState &state);
bool IsExactSelfRender(const Rasterizer::RasterizerState &state, const BinItem &item) const;
const Rasterizer::RasterizerState &SelfTextureSnapshot(const BinItem &item, const Rasterizer::RasterizerState &state);
void OptimizePendingStates(uint16_t first, uint16_t last);
BinCoords Scissor(BinCoords range);
BinCoords Range(const VertexData &v0, const VertexData &v1, const VertexData &v2);
+2
View File
@@ -90,6 +90,8 @@ struct RasterizerState {
bool magFilt : 1;
bool antialiasLines : 1;
bool textureProj : 1;
// Textures from the buffer it draws to (BinManager::SelfTextureSnapshot).
bool selfTexture : 1;
};
#if defined(SOFTGPU_MEMORY_TAGGING_DETAILED) || defined(SOFTGPU_MEMORY_TAGGING_BASIC)