SoftGPU: Fix bin queue bookkeeping and dependency tracking

- 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) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-29 13:16:40 -06:00
1 parent 74553bfa1a
commit 9d3022642b
3 files changed
+29 -6

No files matched your search

+19 -4
View File
@@ -209,6 +209,7 @@ void BinManager::UpdateState() {
const bool hadDepth = pendingWrites_[1].base != 0;
if (HasDirty(SoftDirty::BINNER_RANGE)) {
drawTargetAddr_ = gstate.getFrameBufAddress();
DrawingCoords scissorTL(gstate.getScissorX1(), gstate.getScissorY1());
DrawingCoords scissorBR(std::min(gstate.getScissorX2(), gstate.getRegionX2()), std::min(gstate.getScissorY2(), gstate.getRegionY2()));
ScreenCoords screenScissorTL = TransformUnit::DrawingToScreen(scissorTL, 0);
@@ -292,14 +293,14 @@ bool BinManager::HasTextureWrite(const RasterizerState &state) {
return false;
}
bool BinManager::IsExactSelfRender(const Rasterizer::RasterizerState &state, const BinItem &item) {
bool BinManager::IsExactSelfRender(const Rasterizer::RasterizerState &state, const BinItem &item) const {
if (item.type != BinItemType::SPRITE && item.type != BinItemType::RECT)
return false;
if (state.textureProj || state.maxTexLevel > 0)
return false;
// Only possible if the texture is 1:1.
if ((state.texaddr[0] & 0x0F1FFFFF) != (gstate.getFrameBufAddress() & 0x0F1FFFFF))
if ((state.texaddr[0] & 0x0F1FFFFF) != (drawTargetAddr_ & 0x0F1FFFFF))
return false;
int bufferPixelWidth = BufferFormatBytesPerPixel(state.pixelID.FBFormat());
int texturePixelWidth = textureBitsPerPixel[state.samplerID.texfmt] / 8;
@@ -361,8 +362,13 @@ void BinManager::MarkPendingWrites(const Rasterizer::RasterizerState &state) {
constexpr uint32_t mirrorMask = 0x041FFFFF;
const uint32_t bpp = state.pixelID.FBFormat() == GE_FORMAT_8888 ? 4 : 2;
pendingWrites_[0].Expand(gstate.getFrameBufAddress() & mirrorMask, bpp, gstate.FrameBufStride(), scissorTL, scissorBR);
if (state.pixelID.depthWrite)
if (state.pixelID.depthWrite) {
pendingWrites_[1].Expand(gstate.getDepthBufAddress() & mirrorMask, 2, gstate.DepthBufStride(), scissorTL, scissorBR);
} else if (gstate.isDepthTestEnabled() && !gstate.isModeClear()) {
// Testing without writing still reads the depth buffer, so a transfer into it has to wait.
const uint32_t depthAddr = gstate.getDepthBufAddress() & mirrorMask;
pendingReads_[depthAddr].Expand(depthAddr, 2, gstate.DepthBufStride(), scissorTL, scissorBR);
}
}
inline void BinDirtyRange::Expand(uint32_t newBase, uint32_t bpp, uint32_t stride, const DrawingCoords &tl, const DrawingCoords &br) {
@@ -584,8 +590,17 @@ void BinManager::Drain(bool flushing) {
}
void BinManager::Flush(const char *reason) {
if (queueRange_.x1 == 0x7FFFFFFF)
if (queueRange_.x1 == 0x7FFFFFFF) {
// Nothing queued, so nothing refers to the older states and CLUTs. Trim them anyway: callers
// flush because one of these rings is full, and push into it right after.
while (states_.Size() > 1) {
states_.SkipNext();
}
while (cluts_.Size() > 1) {
cluts_.SkipNext();
}
return;
}
double st = 0.0;
const bool collectDebugStats = g_coreCollectDebugStats;
+5 -2
View File
@@ -132,7 +132,7 @@ struct BinQueue {
}
bool Full() const {
return size_ == N - 1;
return size_ >= N - 1;
}
bool NearFull() const {
@@ -278,13 +278,16 @@ private:
const char *slowestFlushReason_ = nullptr;
double slowestFlushTime_ = 0.0;
int lastFlipstats_ = 0;
// The framebuffer the queued draws render to. A framebuffer change flushes first, so it's one for all
// of them, and during that flush gstate already has the new one.
u32 drawTargetAddr_ = 0;
int enqueues_ = 0;
int mostThreads_ = 0;
void MarkPendingReads(const Rasterizer::RasterizerState &state);
void MarkPendingWrites(const Rasterizer::RasterizerState &state);
bool HasTextureWrite(const Rasterizer::RasterizerState &state);
static bool IsExactSelfRender(const Rasterizer::RasterizerState &state, const BinItem &item);
bool IsExactSelfRender(const Rasterizer::RasterizerState &state, const BinItem &item) const;
void OptimizePendingStates(uint16_t first, uint16_t last);
BinCoords Scissor(BinCoords range);
BinCoords Range(const VertexData &v0, const VertexData &v1, const VertexData &v2);
+5
View File
@@ -506,11 +506,16 @@ bool RectangleFastPath(const VertexData &v0, const VertexData &v1, BinManager &b
if (g_needsClearAfterDialog) {
g_needsClearAfterDialog = false;
// Afterwards, we also need to clear the actual destination. Can do a fast rectfill.
// The binner's state was computed with texturing on, so recompute it around the sprite.
const SoftDirty texDirty = SoftDirty::SAMPLER_BASIC | SoftDirty::SAMPLER_TEXLIST | SoftDirty::RAST_TEX | SoftDirty::BINNER_OVERLAP;
gstate.textureMapEnable &= ~1;
binner.SetDirty(texDirty);
binner.UpdateState();
VertexData newV1 = v1;
newV1.color0 = 0xFF000000;
binner.AddSprite(v0, newV1);
gstate.textureMapEnable |= 1;
binner.SetDirty(texDirty);
}
return true;
} else {