saveIndexed = true in [options] saves the CLUT4/CLUT8 textures listed in
[indexed] as palette PNGs, saveIndexed = all saves every one of them.
The file is named <hash>_indexed.png, with the CLUT hash zeroed so one
file serves every palette of the texture. The other textures are dumped
as before.
The indices are decoded from PSP memory and the palette comes from the
current CLUT, so the dump looks like the game's texture. Add the file to
[hashes] and [indexed] to use it as a replacement.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An [indexed] entry set to gpu uploads the indices as an R8 image, and
the CLUT8_INDEXED depal shader applies the palette. A texture whose
palette changes a lot then costs no CPU time per palette, and keeps one
texture instead of one per palette.
Once such a replacement is found, the texture is looked up without the
CLUT hash, like with the TextureCLUTInShader compat flag. Only the first
mip level is used, and the replacer's filtering overrides are skipped
since filtering indices mixes up palette entries. Without shader bitwise
ops it falls back to coloring on the CPU.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Indexed replacements can now also be KTX2 files in the R8_UNORM format,
uncompressed or zstd supercompressed, with the mip levels stored in the
file. Lossy formats (Basis, BC) are not usable, since an error in an index
picks the wrong color.
The template textures.ini documents the supported files.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 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>
- Reloading the ini clears the per-key lookup caches, which could keep
saying "no replacement" for textures the new ini replaces.
- With ignoreAddress, hash ranges were skipped when sizing the
replacement, though ComputeHash applies them. cache_ is now keyed by
the full key, so its lookups hit too.
- Textures sharing files but differing in size, hash range or filtering
no longer share one ReplacedTexture (the first one's settings won).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Release the file reference when loading a level fails or finds nothing,
since only a loaded level takes ownership of it.
- Free the PNG image when the size changed since the header was read.
- Delete the VFS when a pack without an ini has no hash-named textures.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
A `video = true` in textures.ini opted a pack into replacing and dumping video
textures. Both halves are a bad deal. Dumping writes a file per decoded frame,
which fills a disk rather than producing anything a pack can use, and replacing
means a hash lookup on content that is different every frame and will never be
found twice.
It was also the only reason the texture cache still hashed video textures at
all, so it cost every game that has ever played a cutscene, not just the packs
that set it.
Video textures are now never replaced and never dumped, and skipHash is simply
isVideo. An existing ini keeping the key is harmless - unknown options are
ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A hashrange of 'addr,w,h = 0,0' passed validation (0 isn't bigger than the source),
became desc_.newW/newH, and ReplacedTexture::Prepare divides by them. A post-shader
SSAA level multiplies the render resolution with no upper bound, while the
texture-shader Scale sitting a few lines away is checked against 2..8.
The per-game override filename from [games] went straight to LoadFromVFS with no
check, while the [hashes] filenames and ReplacedTexture::Prepare both run theirs
through HasParentDirComponent. For a directory-backed pack DirectoryReader resolves
it against the pack directory, so '../../..' reads anything on disk - and texture
packs are third-party downloads. Check it the same way. (Zip-backed packs weren't
affected.)
Turning replacement off mid-session did 'delete vfs_; vfs_ = nullptr;' without
updating the cached ReplacedTextures that hold the same pointer - LoadIni has a fixup
loop for exactly this when it swaps the VFS, and the disable path needed one too.
Decimate(ALL) right after doesn't help: it only frees their data, it doesn't erase
the entries. A texture still PENDING (or one whose try_lock in Decimate failed) then
used the freed VFS from a worker thread, or from ~ReplacedTexture's ReleaseFile.
Clear the back-pointers, and make the destructor and Prepare() tolerate a null one.
Memory::IsValidAddress and friends tested the extended-RAM range with
(address & 0x3F000000), i.e. at 16MB granularity, so they accepted the whole 16MB
block containing the end of RAM. That's harmless at 32MB and 64MB, but the Sora no
Kiseki SC/3rd HD remasters run with 0x04C00000, so addresses from 0x0CC00000 to
0x0CFFFFFF read as valid, and MaxSizeAtAddress then underflowed to ~4GB there -
which defeats ClampValidSizeAt and IsValidRange entirely for that window. Mask
with 0x3FFFFFFF instead, in all five helpers and the copies in MemMapFunctions.cpp.
IsValidTextureAddress's extended-RAM branch repeated the first branch's whole mask
rather than just its alignment bits, so it was dead code and extended RAM was never
accepted as a texture source.
ComputeTextureHash checked IsValidAddress(addr + sizeInRAM), i.e. only the end
address, which can land in a different valid region than the start - a VRAM texture
with a large enough computed size ends exactly at the base of RAM and "passes"
while reading far past the 8MB VRAM view. Use IsValidRange.
TextureReplacer::ComputeHash's strided path had no range check at all, unlike the
contiguous path right above it. Also clamp the pack-supplied reduce-hash factor to
1.0 - it's a reduction, and the ini parser only rejects exactly 0.
ZipExtractFileToMemory read an uninitialized zip_stat when zip_stat_index failed
(it ignored the return value) and sized a host allocation directly from the zip's
declared uncompressed size. Reached just by opening an archive.
Memory::Reinit ignored Init()'s return value, and DoState fed it a memory size
taken straight from the savestate. A bogus size made the map fail to allocate and
left base null, after which DoMemoryVoid wrote RAM through it. Validate the size,
propagate the failure, and roll back to the previous size if reinit fails.
314 pspautotests pass, all unit tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
Add unittest/TestTextureReplacer which creates a fictive texture pack
(textures.ini with readable invented hashes plus real PNG files), loads
it via the replacer, and verifies lookups, filtering, hashranges, mip
levels, and missing/ignored entries.
To make the replacer runnable outside the emulator:
- The constructor now accepts a null DrawContext (formats just default
to unsupported).
- FindReplacement/FindFiltering use the replaceEnabled_ member instead
of the global config.
- Added TextureReplacer::LoadPackForTesting() to load an ini from a
path directly.
Texture pack filenames/aliases from textures.ini were used to build read
and write paths with no '..' check, letting a malicious pack read or
write files outside the pack directory.
- LoadIniValues rejects entries with a parent dir component via
HasParentDirComponent.
- ReplacedTexture::Prepare skips such filenames as defense in depth.
- PSPLoaders savestate migration now uses the shared HasPathTraversal
helper instead of inline separator checks.
Reported by Joseph on Discord.
Sometimes, things could align perfectly so allocations happend exactly
at the end of a pushbuffer. At the same time, we allow our vertex decoder to write an extra few
bytes if it needs to for speed. Unfortunately I missed this interaction,
resulting in some uncommon crashes that were especially common with
heavy-geometry things like modified GTA LCS with PS2 assets, for
example.
The problem was reported with Vulkan, but our OpenGL backend had the
same issue too.
* Rename LogType to Log
* Explicitly use the Log:: enum when logging. Allows for autocomplete when editing.
* Mac/ARM64 buildfix
* Do the same with the hle result log macros
* Rename the log names to mixed case while at it.
* iOS buildfix
* Qt buildfix attempt, ARM32 buildfix