Files
ppsspp/GPU/Common/ReplacedTexture.h
T
Henrik RydgårdandClaude Opus 5.5 45d997c3ea Texture replacement: Fix DDS mip double free and VFS lifetime
- DDS files with mips stored level 0's file reference in level 1 too, so it
  was freed twice.
- The VFS was deleted on config changes (and ini reloads) while load tasks
  still used it. Now each cached texture waits for its task and releases its
  file references through the old VFS first, and reloads afterwards.
- A failed ini reload turns replacement off instead of leaving it on without
  a VFS.
- Release file references on purge.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-29 09:35:17 -06:00

246 lines
6.0 KiB
C++

// Copyright (c) 2016- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#pragma once
#include <mutex>
#include <string>
#include "Common/File/VFS/VFS.h"
#include "Common/GPU/thin3d.h"
#include "Core/ConfigValues.h"
#include "GPU/Common/ImageCommon.h"
class TextureReplacer;
class LimitedWaitable;
// For forward compatibility, we specify the hash.
enum class ReplacedTextureHash {
QUICK,
XXH32,
XXH64,
};
enum class ReplacedImageType {
PNG,
ZIM,
DDS,
BASIS, // TODO: Might not even do this, KTX2 is a better container.
KTX2,
INVALID,
};
static const int MAX_REPLACEMENT_MIP_LEVELS = 12; // 12 should be plenty, 8 is the max mip levels supported by the PSP.
enum class ReplacementState : uint32_t {
UNLOADED,
PENDING,
NOT_FOUND, // Also used on error loading the images.
ACTIVE,
CANCEL_INIT,
COUNT, // Not a valid state
};
const char *StateString(ReplacementState state);
struct GPUFormatSupport {
bool bc123;
bool astc;
bool bc7;
bool etc2;
};
struct ReplacementCacheKey {
u64 cachekey; // Split into two u32?
u32 hash;
ReplacementCacheKey() : cachekey(0), hash(0) {}
ReplacementCacheKey(u64 ckey, u32 h) : cachekey(ckey), hash(h) {}
bool operator ==(const ReplacementCacheKey &k) const {
return k.cachekey == cachekey && k.hash == hash;
}
bool operator <(const ReplacementCacheKey &k) const {
if (k.cachekey == cachekey) {
return k.hash < hash;
}
return k.cachekey < cachekey;
}
// Access the parts.
u64 CacheKey() const { return cachekey; }
u32 ClutHash() const { return (u32)(cachekey & 0xFFFFFFFFULL); }
u32 Address() const { return (u32)(cachekey >> 32); }
u32 ContentsHash() const { return hash; }
void ZeroAddress() {
cachekey &= 0xFFFFFFFFULL;
}
void ZeroClutHash() {
cachekey &= 0xFFFFFFFF00000000ULL;
}
void ZeroContentsHash() {
hash = 0;
}
};
struct ReplacementDesc {
int newW;
int newH;
ReplacementCacheKey cacheKey;
int w;
int h;
TextureFiltering forceFiltering;
std::string hashfiles;
Path basePath;
std::vector<std::string> filenames;
std::string logId;
GPUFormatSupport formatSupport;
};
class ReplacedTexture;
// These aren't actually all replaced, they can also represent a placeholder for a not-found
// replacement (texture == nullptr).
struct ReplacedTextureRef {
ReplacedTexture *texture; // shortcut
std::string hashfiles; // key into the cache
};
// Metadata about a given texture level.
struct ReplacedTextureLevel {
// Data dimensions
int w = 0;
int h = 0;
// PSP texture dimensions
int fullW = 0;
int fullH = 0;
int fullDataSize = 0;
// To be able to reload, we need to be able to reopen, unfortunate we can't use zip_file_t.
// TODO: This really belongs on the level in the cache, not in the individual ReplacedTextureLevel objects.
VFSFileReference *fileRef = nullptr;
};
class ReplacedTexture {
public:
ReplacedTexture(VFSBackend *vfs, const ReplacementDesc &desc);
~ReplacedTexture();
inline ReplacementState State() const {
return state_;
}
void SetState(ReplacementState state) {
_dbg_assert_(state != state_);
state_ = state;
}
void GetSize(int level, int *w, int *h) const {
_dbg_assert_(State() == ReplacementState::ACTIVE);
_dbg_assert_((size_t)level < levels_.size());
*w = levels_[level].fullW;
*h = levels_[level].fullH;
}
int GetLevelDataSizeAfterCopy(int level) const {
// Includes padding etc.
return levels_[level].fullDataSize;
}
size_t GetTotalDataSize() const {
if (State() != ReplacementState::ACTIVE) {
return 0;
}
size_t sz = 0;
for (auto &data : data_) {
sz += data.size();
}
return sz;
}
bool ForceFiltering(TextureFiltering *forceFiltering) const {
if (desc_.forceFiltering != (TextureFiltering)0) {
*forceFiltering = desc_.forceFiltering;
return true;
} else {
return false;
}
}
int NumLevels() const {
_dbg_assert_(State() == ReplacementState::ACTIVE);
return (int)levels_.size();
}
Draw::DataFormat Format() const {
_dbg_assert_(State() == ReplacementState::ACTIVE);
return fmt;
}
const ReplacementDesc &Desc() const {
return desc_;
}
TextureAlpha AlphaStatus() const {
return alphaStatus_;
}
bool Poll(double budget);
bool CopyLevelTo(int level, uint8_t *out, size_t outDataSize, int rowPitch);
std::string logId_;
private:
enum class LoadLevelResult {
LOAD_ERROR = 0,
CONTINUE = 1,
DONE = 2,
};
void Prepare(VFSBackend *vfs);
// Waits for any load task, then drops all data and file references, and detaches from the VFS.
// The texture reloads from scratch once it gets a VFS again. Must run before the VFS is deleted.
void Unload();
void ReleaseLevels();
LoadLevelResult LoadLevelData(VFSFileReference *fileRef, const std::string &filename, int level, Draw::DataFormat *pixelFormat);
void PurgeIfNotUsedSinceTime(double t);
std::vector<std::vector<uint8_t>> data_;
std::vector<ReplacedTextureLevel> levels_;
// Image format of mip level 0; mixing formats across levels is not
// allowed (container formats like KTX2/DDS manage their own mip chain).
ReplacedImageType firstImageType_ = ReplacedImageType::INVALID;
double lastUsed_ = 0.0;
LimitedWaitable *threadWaitable_ = nullptr;
std::mutex lock_;
Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED;
TextureAlpha alphaStatus_ = TextureAlpha::Any;
double lastUsed = 0.0;
std::atomic<ReplacementState> state_ = ReplacementState::UNLOADED;
VFSBackend *vfs_ = nullptr;
ReplacementDesc desc_;
friend class TextureReplacer;
friend class ReplacedTextureTask;
};