Files
ppsspp/GPU/Common/ReplacedTexture.h
T
Claude 03391c66cb Texture replacement: Support palette-indexed replacement images
- Fixes #15145

A replacement listed in a new [indexed] section of textures.ini holds
palette indices instead of colors: a palette PNG (the file's palette index)
or an 8-bit grayscale PNG (the gray value), at any resolution. When the
texture is built, the indices go through the game's current CLUT, including
the CLUT index shift, mask and start position, and the result is uploaded as
an ordinary RGBA texture. A game that swaps or customizes a palette keeps
working with a high-resolution replacement.

[indexed] keys use the [hashes] format and are matched with the same
wildcards, so a key with a zero CLUT hash covers every palette of the
texture. The contents hash only covers the indices, so one image is shared
by all palettes (the loaded data is shared, each palette still has its own
texture cache entry).

Saving does not dump indexed textures yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:42:39 +02:00

263 lines
6.7 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;
// The files hold palette indices (palette or grayscale PNG) that the game's own palette colors.
bool indexed = false;
};
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 levelCache_
};
// 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(const ReplacedTexture &) = delete;
ReplacedTexture &operator=(const ReplacedTexture &) = delete;
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 IsIndexed() const {
return desc_.indexed;
}
// Indexed textures only. Size of the stored indices, before padding to the full size.
void GetIndexDataSize(int level, int *w, int *h) const {
_dbg_assert_(IsIndexed() && (size_t)level < levels_.size());
*w = levels_[level].w;
*h = levels_[level].h;
}
// Indexed textures only. Copies the indices, one byte each, tightly packed.
bool CopyIndexLevelTo(int level, uint8_t *out, size_t outDataSize);
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;
};