GPU: Track video frames in GPUCommon, so they expire for the blit cost too

The blit cost remembered only the last buffer a decoder wrote into, forever.
Move the texture cache's video list (with its ageing out a few flips after
the last write) into GPUCommon, so the texture cache, the blit cost and
SoftGPU all share one. That also counts both of a double-buffered player's
frames, which exposed that a clear drawn with texturing still enabled was
being charged as a blit - skip clears and draws without texture coordinates.

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-25 17:00:11 -06:00
1 parent 8f70003cce
commit 242ef0a984
7 files changed
+80 -82

No files matched your search

+2 -2
View File
@@ -1561,7 +1561,7 @@ bool FramebufferManagerCommon::DrawFramebufferToOutput(const DisplayLayoutConfig
constexpr float v0 = 0.0f, v1 = 1.0f;
if (useBufferedRendering_) {
presentation_->UpdateUniforms(textureCache_->VideoIsPlaying());
presentation_->UpdateUniforms(gpu->VideoIsPlaying());
presentation_->SourceTexture(pixelsTex, 512, 272);
presentation_->RunPostshaderPasses(config, flags, uvRotation, u0, v0, u1, v1);
}
@@ -1728,7 +1728,7 @@ void FramebufferManagerCommon::PrepareCopyDisplayToOutput(const DisplayLayoutCon
int actualWidth = (vfb->bufferWidth * vfb->renderWidth) / vfb->width;
int actualHeight = (vfb->bufferHeight * vfb->renderHeight) / vfb->height;
presentation_->UpdateUniforms(textureCache_->VideoIsPlaying());
presentation_->UpdateUniforms(gpu->VideoIsPlaying());
presentation_->SourceFramebuffer(vfb->fbo, actualWidth, actualHeight);
presentation_->RunPostshaderPasses(config, flags, uvRotation, u0, v0, u1, v1);
}
+3 -50
View File
@@ -42,11 +42,11 @@
#include "GPU/Common/GPUStateUtils.h"
#include "GPU/ge_constants.h"
#include "GPU/Debugger/Record.h"
#include "GPU/GPUCommon.h"
#include "GPU/GPUState.h"
#include "Core/Util/PPGeDraw.h"
// Videos should be updated every few frames, so we forget quickly.
#define VIDEO_DECIMATE_AGE 4
// If a texture hasn't been seen for this many frames, get rid of it.
#define TEXTURE_KILL_AGE 200
@@ -153,7 +153,7 @@ void TextureCacheCommon::StartFrame() {
gpuStats.perFrame.numReplacerTrackedTex = replacer_.GetNumTrackedTextures();
gpuStats.perFrame.numCachedReplacedTextures = replacer_.GetNumCachedReplacedTextures();
}
gpuStats.perFrame.numVideoTextures = (int)videos_.size();
gpuStats.perFrame.numVideoTextures = (int)gpu->Videos().size();
if (texelsScaledThisFrame_) {
VERBOSE_LOG(Log::TexCache, "Scaled %d texels", texelsScaledThisFrame_);
@@ -1134,26 +1134,11 @@ void TextureCacheCommon::Decimate(const TexCacheEntry *const exceptThisOne, bool
}
}
// Decimate known videos (so the list doesn't grow unboundedly or we start to misidentify textures as video).
for (auto iter = videos_.begin(); iter != videos_.end(); ) {
if (iter->flips + VIDEO_DECIMATE_AGE < gpuStats.totals.numFlips) {
iter = videos_.erase(iter);
} else {
++iter;
}
}
replacer_.Decimate(forcePressure ? ReplacerDecimateMode::FORCE_PRESSURE : ReplacerDecimateMode::NEW_FRAME);
}
bool TextureCacheCommon::IsVideo(u32 texaddr) const {
texaddr &= 0x3FFFFFFF;
for (const VideoInfo &info : videos_) {
if (texaddr >= info.addr && texaddr < info.addr + info.size) {
return true;
}
}
return false;
return gpu->IsVideo(texaddr);
}
void TextureCacheCommon::NotifyFramebuffer(VirtualFramebuffer *framebuffer, FramebufferNotification msg) {
@@ -1509,37 +1494,6 @@ void TextureCacheCommon::NotifyConfigChanged() {
replacer_.NotifyConfigChanged();
}
void TextureCacheCommon::NoteVideoRange(u32 addr, u32 size) {
addr &= 0x3FFFFFFF;
// A game blits its video frame every displayed frame while waiting for the next one, so the
// same few buffers arrive over and over. Refresh the one we already have rather than stacking
// a duplicate per frame - IsVideo() scans this linearly.
for (VideoInfo &info : videos_) {
if (info.addr == addr) {
info.size = size;
info.flips = gpuStats.totals.numFlips;
return;
}
}
videos_.push_back({ addr, size, gpuStats.totals.numFlips });
}
void TextureCacheCommon::NotifyWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat fmt) {
NoteVideoRange(addr, (u32)size);
}
// A block copy of a video frame is still a video frame, and games do move them around: Dragon Ball
// Z - Shin Budokai: Another Road colour-converts into RAM, sceDmacMemcpy's the result into VRAM and
// textures from there, never sampling the converted buffer itself. Without carrying the status
// across the copy, what we actually sample looks like an ordinary texture that happens to have new
// contents every frame, so we hash it, miss, and rebuild it - forever.
void TextureCacheCommon::NotifyVideoCopy(u32 dst, u32 src, int size) {
if (size <= 0 || !IsVideo(src)) {
return;
}
NoteVideoRange(dst, (u32)size);
}
void TextureCacheCommon::LoadClut(u32 clutAddr, u32 loadBytes, GPURecord::Recorder *recorder) {
if (loadBytes == 0) {
// Don't accidentally overwrite clutTotalBytes_ with a zero.
@@ -2769,7 +2723,6 @@ void TextureCacheCommon::Clear(bool delete_them) {
cache_.clear();
secondCache_.clear();
}
videos_.clear();
if (dynamicClutFbo_) {
dynamicClutFbo_->Release();
-16
View File
@@ -362,8 +362,6 @@ public:
// FramebufferManager keeps TextureCache updated about what regions of memory are being rendered to,
// so that it can invalidate TexCacheEntries pointed at those addresses.
void NotifyFramebuffer(VirtualFramebuffer *framebuffer, FramebufferNotification msg);
void NotifyWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat fmt);
void NotifyVideoCopy(u32 dst, u32 src, int size);
int NumLoadedTextures() const {
return (int)cache_.size();
@@ -374,9 +372,6 @@ public:
bool IsFakeMipmapChange() {
return PSP_CoreParameter().compat.flags().FakeMipmapChange && gstate.getTexLevelMode() == GE_TEXLEVEL_MODE_CONST;
}
bool VideoIsPlaying() {
return !videos_.empty();
}
virtual bool GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level, bool *isFramebuffer) { return false; }
virtual void StartFrame();
@@ -393,15 +388,6 @@ public:
const size_t CacheSizeEstimate() const;
const size_t SecondCacheSizeEstimate() const;
struct VideoInfo {
u32 addr;
u32 size;
int flips;
};
const std::vector<VideoInfo> &Videos() const {
return videos_;
}
protected:
bool PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEntry *entry);
@@ -446,7 +432,6 @@ protected:
virtual void BoundFramebufferTexture() {}
bool IsVideo(u32 texaddr) const;
void NoteVideoRange(u32 addr, u32 size);
static TextureAlpha CheckCLUTAlpha(const uint8_t *pixelData, GEPaletteFormat clutFmt, int w);
@@ -473,7 +458,6 @@ protected:
// The secondary cache uses the texture hash and clut hash as the key.
TexCache secondCache_;
std::vector<VideoInfo> videos_;
AlignedVector<u32, 16> tmpTexBuf32_;
AlignedVector<u32, 16> tmpTexBufRearrange_;
+54 -7
View File
@@ -149,11 +149,14 @@ int GPUCommon::EstimatePerVertexCost() {
}
int GPUCommon::EstimateVideoBlitCycles(GEPrimitiveType prim, const void *verts, const void *inds, int count, const VertexDecoder *dec, u32 vertType) const {
if (prim != GE_PRIM_RECTANGLES || !gstate.isModeThrough() || !gstate.isTextureMapEnabled() || videoFrameSize_ == 0) {
// A clear, or a draw without texture coordinates, doesn't sample the texture even if texturing
// is still enabled from an earlier draw.
if (prim != GE_PRIM_RECTANGLES || !gstate.isModeThrough() || !gstate.isTextureMapEnabled() ||
gstate.isModeClear() || (vertType & GE_VTYPE_TC_MASK) == 0) {
return 0;
}
const u32 texAddr = gstate.getTextureAddress(0) & 0x3FFFFFFF;
if (texAddr < (videoFrameAddr_ & 0x3FFFFFFF) || texAddr >= (videoFrameAddr_ & 0x3FFFFFFF) + videoFrameSize_) {
if (!IsVideo(texAddr)) {
return 0;
}
if ((vertType & GE_VTYPE_POS_MASK) != GE_VTYPE_POS_16BIT) {
@@ -205,6 +208,46 @@ int GPUCommon::EstimateVideoBlitCycles(GEPrimitiveType prim, const void *verts,
return (int)((float)usToCycles(PowerScaleFromDefaultClock(1000)) * ns / 1000000.0f);
}
// How many flips a video range stays known after the last frame written to it.
static const int VIDEO_DECIMATE_AGE = 4;
bool GPUCommon::IsVideo(u32 addr) const {
addr &= 0x3FFFFFFF;
for (const VideoInfo &info : videos_) {
if (addr >= info.addr && addr < info.addr + info.size) {
return true;
}
}
return false;
}
void GPUCommon::NoteVideoRange(u32 addr, u32 size) {
addr &= 0x3FFFFFFF;
// A game blits its video frame every displayed frame while waiting for the next one, so the
// same few buffers arrive over and over. Refresh the one we already have rather than stacking
// a duplicate per frame - IsVideo() scans this linearly.
for (VideoInfo &info : videos_) {
if (info.addr == addr) {
info.size = size;
info.flips = gpuStats.totals.numFlips;
return;
}
}
videos_.push_back({ addr, size, gpuStats.totals.numFlips });
}
// So the list doesn't grow unboundedly, and a buffer reused for something else once the movie is
// over stops being treated as video.
void GPUCommon::DecimateVideos() {
for (auto iter = videos_.begin(); iter != videos_.end(); ) {
if (iter->flips + VIDEO_DECIMATE_AGE < gpuStats.totals.numFlips) {
iter = videos_.erase(iter);
} else {
++iter;
}
}
}
void GPUCommon::PopDLQueue() {
if(!dlQueue.empty()) {
dlQueue.pop_front();
@@ -711,6 +754,7 @@ u32 GPUCommon::Break(int mode) {
void GPUCommon::PSPFrame() {
immCount_ = 0;
DecimateVideos();
if (dumpNextFrame_) {
NOTICE_LOG(Log::G3D, "DUMPING THIS FRAME");
dumpThisFrame_ = true;
@@ -2065,9 +2109,14 @@ void GPUCommon::DoBlockTransfer(u32 skipDrawReason) {
cyclesExecuted += ((height * width * bpp) * 16) / 10;
}
// A block copy of a video frame is still a video frame, and games do move them around: Dragon Ball
// Z - Shin Budokai: Another Road colour-converts into RAM, sceDmacMemcpy's the result into VRAM and
// textures from there, never sampling the converted buffer itself. Without carrying the status
// across the copy, what we actually sample looks like an ordinary texture that happens to have new
// contents every frame, so we hash it, miss, and rebuild it - forever.
void GPUCommon::NotifyVideoCopy(u32 dest, u32 src, int size) {
if (textureCache_) {
textureCache_->NotifyVideoCopy(dest, src, size);
if (size > 0 && IsVideo(src)) {
NoteVideoRange(dest, (u32)size);
}
}
@@ -2166,12 +2215,10 @@ bool GPUCommon::PerformWriteColorFromMemory(u32 dest, int size) {
}
void GPUCommon::PerformWriteFormattedFromMemory(u32 addr, int size, int frameWidth, GEBufferFormat format) {
videoFrameAddr_ = addr;
videoFrameSize_ = size;
NoteVideoRange(addr, (u32)size);
if (Memory::IsVRAMAddress(addr)) {
framebufferManager_->PerformWriteFormattedFromMemory(addr, size, frameWidth, format);
}
textureCache_->NotifyWriteFormattedFromMemory(addr, size, frameWidth, format);
InvalidateCache(addr, size, GPU_INVALIDATE_SAFE);
}
+18 -3
View File
@@ -192,6 +192,21 @@ public:
// anything else. We don't model fill rate in general, but a movie player can be paced by it.
int EstimateVideoBlitCycles(GEPrimitiveType prim, const void *verts, const void *inds, int count, const VertexDecoder *dec, u32 vertType) const;
// Memory a video decoder has written a frame into recently (PerformWriteFormattedFromMemory),
// or a copy of such a frame (NotifyVideoCopy). An entry ages out a few flips after its last write.
struct VideoInfo {
u32 addr;
u32 size;
int flips;
};
bool IsVideo(u32 addr) const;
bool VideoIsPlaying() const {
return !videos_.empty();
}
const std::vector<VideoInfo> &Videos() const {
return videos_;
}
virtual void Flush();
#ifdef USE_CRT_DBG
@@ -360,9 +375,9 @@ protected:
u64 drawCompleteTicks;
u64 busyTicks;
// The last buffer a video decoder wrote a frame into, see PerformWriteFormattedFromMemory.
u32 videoFrameAddr_ = 0;
u32 videoFrameSize_ = 0;
void NoteVideoRange(u32 addr, u32 size);
void DecimateVideos();
std::vector<VideoInfo> videos_;
int downcount;
u64 startingTicks;
+1 -2
View File
@@ -1285,8 +1285,7 @@ void SoftGPU::InvalidateCache(u32 addr, int size, GPUInvalidationType type)
void SoftGPU::PerformWriteFormattedFromMemory(u32 addr, int size, int width, GEBufferFormat format)
{
// Nothing to update, but remember it for EstimateVideoBlitCycles.
videoFrameAddr_ = addr;
videoFrameSize_ = size;
NoteVideoRange(addr, (u32)size);
}
bool SoftGPU::PerformMemoryCopy(u32 dest, u32 src, int size, GPUCopyFlag flags) {
+2 -2
View File
@@ -243,9 +243,9 @@ void DrawTexturesWindow(ImConfig &cfg, TextureCacheCommon *textureCache) {
replacementStateCounts[(int)ReplacementState::ACTIVE],
replacementStateCounts[(int)ReplacementState::CANCEL_INIT]);
}
if (textureCache->Videos().size()) {
if (gpu->Videos().size()) {
if (ImGui::CollapsingHeader("Tracked video playback memory")) {
for (auto &video : textureCache->Videos()) {
for (auto &video : gpu->Videos()) {
ImGui::Text("%08x: %d flips, size = %d", video.addr, video.flips, video.size);
}
}