mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
c3ff07814c
Bound reservations against total retained residency, skip decoding pinned images, and report complete repeated loads truthfully. Cover cap reductions and publication retry in both formats. Tracking: beads-eio.3.386.
625 lines
26 KiB
C++
625 lines
26 KiB
C++
#define STB_IMAGE_IMPLEMENTATION
|
|
#include "../third_party/stb_image.h"
|
|
#include "hd_texture_pack.h"
|
|
#include "texture_pack_preload.hpp"
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void check(bool condition, const char* message) {
|
|
if (!condition) {
|
|
std::fprintf(stderr, "FAIL: %s\n", message);
|
|
++failures;
|
|
}
|
|
}
|
|
std::string key_name(uint32_t texture_hash, uint32_t palette_hash) {
|
|
std::ostringstream out;
|
|
out << std::hex << std::nouppercase << texture_hash << '-'
|
|
<< palette_hash << ".png";
|
|
return out.str();
|
|
}
|
|
|
|
std::string padded_key_name(uint32_t texture_hash, uint32_t palette_hash) {
|
|
std::ostringstream out;
|
|
out << std::hex << std::nouppercase << std::setw(8) << std::setfill('0')
|
|
<< texture_hash << '-' << palette_hash << ".png";
|
|
return out.str();
|
|
}
|
|
|
|
void touch(const fs::path& path) {
|
|
std::ofstream output(path, std::ios::binary);
|
|
output.put('\0');
|
|
}
|
|
|
|
uint32_t png_crc(const uint8_t* data, size_t size) {
|
|
uint32_t crc = 0xFFFFFFFFu;
|
|
for (size_t i = 0; i < size; ++i) {
|
|
crc ^= data[i];
|
|
for (int bit = 0; bit < 8; ++bit)
|
|
crc = (crc >> 1) ^ ((crc & 1u) ? 0xEDB88320u : 0u);
|
|
}
|
|
return crc ^ 0xFFFFFFFFu;
|
|
}
|
|
|
|
void append_be32(std::vector<uint8_t>& bytes, uint32_t value) {
|
|
bytes.push_back(static_cast<uint8_t>(value >> 24));
|
|
bytes.push_back(static_cast<uint8_t>(value >> 16));
|
|
bytes.push_back(static_cast<uint8_t>(value >> 8));
|
|
bytes.push_back(static_cast<uint8_t>(value));
|
|
}
|
|
|
|
void append_png_chunk(std::vector<uint8_t>& png, const char type[4],
|
|
const std::vector<uint8_t>& payload) {
|
|
append_be32(png, static_cast<uint32_t>(payload.size()));
|
|
const size_t crc_start = png.size();
|
|
png.insert(png.end(), type, type + 4);
|
|
png.insert(png.end(), payload.begin(), payload.end());
|
|
append_be32(png, png_crc(png.data() + crc_start, png.size() - crc_start));
|
|
}
|
|
|
|
/* Generate an original, tiny RGBA PNG in the test itself. The single stored
|
|
* DEFLATE block keeps the fixture independent of an encoder library. */
|
|
void write_test_png(const fs::path& path, uint32_t width, uint32_t height) {
|
|
std::vector<uint8_t> raw;
|
|
raw.reserve(static_cast<size_t>(height) * (1u + width * 4u));
|
|
for (uint32_t y = 0; y < height; ++y) {
|
|
raw.push_back(0); /* PNG filter: None */
|
|
for (uint32_t x = 0; x < width; ++x) {
|
|
raw.push_back(static_cast<uint8_t>(x * 3u));
|
|
raw.push_back(static_cast<uint8_t>(y * 17u));
|
|
raw.push_back(0xA5u);
|
|
/* Exercise packs that use 7-bit alpha (127 = opaque),
|
|
* not conventional 8-bit PNG opacity. Keep the fixture aligned
|
|
* with the shipping assets so the renderer's alpha contract stays
|
|
* covered by the pack test. */
|
|
raw.push_back(0x7Fu);
|
|
}
|
|
}
|
|
check(raw.size() <= 65535u, "generated PNG fits one stored DEFLATE block");
|
|
|
|
std::vector<uint8_t> zlib{0x78u, 0x01u, 0x01u};
|
|
const uint16_t length = static_cast<uint16_t>(raw.size());
|
|
const uint16_t inverse = static_cast<uint16_t>(~length);
|
|
zlib.push_back(static_cast<uint8_t>(length));
|
|
zlib.push_back(static_cast<uint8_t>(length >> 8));
|
|
zlib.push_back(static_cast<uint8_t>(inverse));
|
|
zlib.push_back(static_cast<uint8_t>(inverse >> 8));
|
|
zlib.insert(zlib.end(), raw.begin(), raw.end());
|
|
uint32_t s1 = 1, s2 = 0;
|
|
for (uint8_t value : raw) { s1 = (s1 + value) % 65521u; s2 = (s2 + s1) % 65521u; }
|
|
append_be32(zlib, (s2 << 16) | s1);
|
|
|
|
std::vector<uint8_t> png{0x89u, 'P', 'N', 'G', 0x0Du, 0x0Au, 0x1Au, 0x0Au};
|
|
std::vector<uint8_t> ihdr;
|
|
append_be32(ihdr, width); append_be32(ihdr, height);
|
|
ihdr.insert(ihdr.end(), {8u, 6u, 0u, 0u, 0u});
|
|
append_png_chunk(png, "IHDR", ihdr);
|
|
append_png_chunk(png, "IDAT", zlib);
|
|
append_png_chunk(png, "IEND", {});
|
|
std::ofstream output(path, std::ios::binary);
|
|
output.write(reinterpret_cast<const char*>(png.data()),
|
|
static_cast<std::streamsize>(png.size()));
|
|
}
|
|
|
|
struct TempTree {
|
|
fs::path path;
|
|
|
|
TempTree() {
|
|
const auto tick = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
path = fs::temp_directory_path() /
|
|
("psxrecomp_hd_pack_" + std::to_string(tick));
|
|
fs::create_directories(path);
|
|
}
|
|
|
|
~TempTree() {
|
|
std::error_code ec;
|
|
fs::remove_all(path, ec);
|
|
}
|
|
};
|
|
|
|
struct Fixture {
|
|
TempTree temp;
|
|
fs::path root;
|
|
fs::path replacements;
|
|
std::vector<uint16_t> vram;
|
|
std::vector<uint16_t> upload_words;
|
|
std::vector<uint16_t> wrap_words;
|
|
uint32_t upload_hash = 0;
|
|
uint32_t wrap_hash = 0;
|
|
uint32_t palette4_hash = 0;
|
|
uint32_t palette8_hash = 0;
|
|
|
|
Fixture()
|
|
: root(temp.path / "pack"),
|
|
replacements(root / "Example-texture-replacements"),
|
|
vram(1024u * 512u, 0),
|
|
upload_words(8u * 4u),
|
|
wrap_words(4u * 4u) {
|
|
fs::create_directories(replacements);
|
|
for (unsigned i = 0; i < upload_words.size(); ++i)
|
|
upload_words[i] = static_cast<uint16_t>(0x1200u + i * 17u);
|
|
for (unsigned i = 0; i < wrap_words.size(); ++i)
|
|
wrap_words[i] = static_cast<uint16_t>(0xA000u + i * 13u);
|
|
upload_hash = hd_texture_crc32_words_le(upload_words.data(), upload_words.size());
|
|
wrap_hash = hd_texture_crc32_words_le(wrap_words.data(), wrap_words.size());
|
|
|
|
/* Both palettes wrap at the right edge, proving full-CLUT gathering. */
|
|
for (unsigned i = 0; i < 16; ++i)
|
|
vram[7u * 1024u + ((1020u + i) & 1023u)] =
|
|
static_cast<uint16_t>(0x0100u + i * 3u);
|
|
for (unsigned i = 0; i < 256; ++i)
|
|
vram[8u * 1024u + ((900u + i) & 1023u)] =
|
|
static_cast<uint16_t>(0x2000u + i * 5u);
|
|
palette4_hash = hd_texture_hash_clut(
|
|
vram.data(), vram.size(), 1020, 7, HD_TEXTURE_DEPTH_4BPP);
|
|
palette8_hash = hd_texture_hash_clut(
|
|
vram.data(), vram.size(), 900, 8, HD_TEXTURE_DEPTH_8BPP);
|
|
|
|
/* Native upload is 8 words * 4 pixels/word by 4 rows. The 64x8 PNG is
|
|
* a valid uniform 2x replacement for the live GL aspect check. */
|
|
write_test_png(replacements / key_name(upload_hash, palette4_hash), 64, 8);
|
|
touch(replacements / key_name(upload_hash, palette8_hash));
|
|
touch(replacements / key_name(wrap_hash, 0));
|
|
touch(replacements / "not-a-pack-key.png");
|
|
touch(replacements / "123456789-1.png"); /* component is > 8 hex digits */
|
|
|
|
std::ofstream hashes(root / "Hashes.ini");
|
|
hashes << "[Textures]\n"
|
|
<< std::hex << upload_hash << '-' << palette4_hash
|
|
<< " = Test/Logical/Four bit.png\n"
|
|
<< "# comments and sections are ignored\n";
|
|
}
|
|
};
|
|
|
|
HdTexturePack* open_pack(const fs::path& root) {
|
|
HdTexturePack* pack = nullptr;
|
|
char error[512]{};
|
|
check(hd_texture_pack_create(root.string().c_str(), &pack,
|
|
error, sizeof(error)) == 1,
|
|
error[0] ? error : "pack opens");
|
|
return pack;
|
|
}
|
|
|
|
HdTextureDrawQuery query4(const std::vector<uint16_t>& vram,
|
|
uint8_t u_first,
|
|
uint8_t u_last) {
|
|
HdTextureDrawQuery query{};
|
|
query.page_x = 10;
|
|
query.page_y = 20;
|
|
query.depth = HD_TEXTURE_DEPTH_4BPP;
|
|
query.u_first = u_first;
|
|
query.u_last = u_last;
|
|
query.v_first = 0;
|
|
query.v_last = 3;
|
|
query.clut_x = 1020;
|
|
query.clut_y = 7;
|
|
query.vram = vram.data();
|
|
query.vram_word_count = vram.size();
|
|
return query;
|
|
}
|
|
|
|
void test_indexed_palette_fallback() {
|
|
Fixture f;
|
|
std::ostringstream stem; stem<<std::hex<<f.upload_hash;
|
|
touch(f.replacements/(stem.str()+"-index.png"));
|
|
auto* pack=open_pack(f.root); if(!pack)return;
|
|
hd_texture_pack_track_upload(pack,10,20,8,4,f.upload_words.data(),f.upload_words.size(),nullptr);
|
|
auto q=query4(f.vram,0,31); HdTextureMatch match{};
|
|
check(hd_texture_pack_match(pack,&q,&match)==HD_TEXTURE_LOOKUP_FOUND &&
|
|
match.entry.palette_hash==f.palette4_hash,"exact palette wins over indexed fallback");
|
|
f.vram[7*1024+1020]^=1;
|
|
hd_texture_pack_invalidate(pack,1020,7,1,1);
|
|
check(hd_texture_pack_match(pack,&q,&match)==HD_TEXTURE_LOOKUP_FOUND &&
|
|
match.entry.palette_hash==HD_TEXTURE_PALETTE_INDEXED,"changed CLUT uses palette-independent index key");
|
|
q.depth=HD_TEXTURE_DEPTH_16BPP; q.u_last=7;
|
|
check(hd_texture_pack_match(pack,&q,&match)==HD_TEXTURE_LOOKUP_NONE,"16bpp never uses indexed palette fallback");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
|
|
void test_crc_exact_little_endian() {
|
|
const uint16_t ascii_words[] = {0x3231, 0x3433, 0x3635, 0x3837};
|
|
check(hd_texture_crc32_words_le(ascii_words, 4) == 0x9AE0DAAFu,
|
|
"CRC32 is IEEE/zlib over explicit little-endian word bytes");
|
|
check(hd_texture_crc32_words_le(nullptr, 0) == 0,
|
|
"empty CRC32 is the IEEE empty-buffer value");
|
|
}
|
|
|
|
void test_scan_mapping(Fixture& fixture) {
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
|
|
HdTexturePackInfo info{};
|
|
hd_texture_pack_get_info(pack, &info);
|
|
check(info.unique_key_count == 3,
|
|
"scanner accepts only valid numeric PNG keys");
|
|
check(info.ambiguous_key_count == 0, "fixture has no ambiguous aliases");
|
|
check(info.logical_mapping_count == 1, "Hashes.ini logical map is parsed");
|
|
check(std::string(info.replacement_root).find("-texture-replacements") !=
|
|
std::string::npos,
|
|
"pack root resolves its single replacement child");
|
|
|
|
HdTexturePackEntry entry{};
|
|
check(hd_texture_pack_lookup(pack, fixture.upload_hash,
|
|
fixture.palette4_hash, &entry) ==
|
|
HD_TEXTURE_LOOKUP_FOUND,
|
|
"numeric texture/palette key lookup succeeds");
|
|
check(std::string(entry.logical_path) == "Test/Logical/Four bit.png",
|
|
"lookup exposes the logical path from Hashes.ini");
|
|
hd_texture_pack_destroy(pack);
|
|
|
|
/* The replacement directory is also a valid direct root; sibling
|
|
* Hashes.ini remains discoverable. */
|
|
pack = open_pack(fixture.replacements);
|
|
if (pack) {
|
|
check(hd_texture_pack_lookup(pack, fixture.upload_hash,
|
|
fixture.palette4_hash, &entry) ==
|
|
HD_TEXTURE_LOOKUP_FOUND && entry.logical_path[0] != '\0',
|
|
"direct replacement root finds sibling Hashes.ini");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
_putenv_s("PSXRECOMP_HD_TEXTURE_ROOT", fixture.root.string().c_str());
|
|
#else
|
|
setenv("PSXRECOMP_HD_TEXTURE_ROOT", fixture.root.string().c_str(), 1);
|
|
#endif
|
|
pack = nullptr;
|
|
char error[128]{};
|
|
check(hd_texture_pack_create(nullptr, &pack, error, sizeof(error)) == 1,
|
|
"environment root is used when explicit path is absent");
|
|
hd_texture_pack_destroy(pack);
|
|
#ifdef _WIN32
|
|
_putenv_s("PSXRECOMP_HD_TEXTURE_ROOT", "");
|
|
#else
|
|
unsetenv("PSXRECOMP_HD_TEXTURE_ROOT");
|
|
#endif
|
|
}
|
|
|
|
void test_palette_match_and_invalidation(Fixture& fixture) {
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
uint32_t hash = 0;
|
|
check(hd_texture_pack_track_upload(pack, 10, 20, 8, 4,
|
|
fixture.upload_words.data(),
|
|
fixture.upload_words.size(), &hash) == 1 &&
|
|
hash == fixture.upload_hash,
|
|
"upload tracking returns exact content hash");
|
|
|
|
HdTextureDrawQuery four = query4(fixture.vram, 0, 31);
|
|
HdTextureMatch match{};
|
|
check(hd_texture_pack_match(pack, &four, &match) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"4bpp page/depth/UV query matches a containing upload");
|
|
check(match.entry.palette_hash == fixture.palette4_hash &&
|
|
match.source_word_x == 0 && match.source_y == 0,
|
|
"match reports palette key and upload-relative origin");
|
|
|
|
/* The live GP0 adapter derives page base/depth from texpage. Page X/Y are
|
|
* fixed hardware bases, so UV 40/20 reaches the upload at VRAM 10/20 in
|
|
* 4bpp (four pixels per word). */
|
|
check(hd_texture_pack_match_draw(
|
|
pack, 0x0000u, 1020, 7, 40, 71, 20, 23,
|
|
fixture.vram.data(), fixture.vram.size(), &match) ==
|
|
HD_TEXTURE_LOOKUP_FOUND && match.source_word_x == 0 &&
|
|
match.source_y == 0,
|
|
"live texpage/CLUT/UV adapter resolves a 4bpp replacement hit");
|
|
check(hd_texture_pack_match_draw(
|
|
pack, 0x0180u, 1020, 7, 0, 0, 0, 0,
|
|
fixture.vram.data(), fixture.vram.size(), &match) ==
|
|
HD_TEXTURE_LOOKUP_ERROR,
|
|
"reserved texpage depth fails closed");
|
|
|
|
/* Entry 20 is outside a 16-color CLUT and must not alter a 4bpp match. */
|
|
const size_t outside4 = 7u * 1024u + ((1020u + 20u) & 1023u);
|
|
fixture.vram[outside4] ^= 0x55AAu;
|
|
check(hd_texture_pack_match(pack, &four, &match) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"4bpp hashes exactly 16 CLUT entries, not adjacent VRAM");
|
|
fixture.vram[outside4] ^= 0x55AAu;
|
|
const size_t inside4 = 7u * 1024u + ((1020u + 15u) & 1023u);
|
|
fixture.vram[inside4] ^= 1u;
|
|
check(hd_texture_pack_match(pack, &four, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"changing the sixteenth 4bpp CLUT entry rejects the palette");
|
|
fixture.vram[inside4] ^= 1u;
|
|
|
|
HdTextureDrawQuery eight{};
|
|
eight.page_x = 10;
|
|
eight.page_y = 20;
|
|
eight.depth = HD_TEXTURE_DEPTH_8BPP;
|
|
eight.u_first = 0;
|
|
eight.u_last = 15;
|
|
eight.v_first = 0;
|
|
eight.v_last = 3;
|
|
eight.clut_x = 900;
|
|
eight.clut_y = 8;
|
|
eight.vram = fixture.vram.data();
|
|
eight.vram_word_count = fixture.vram.size();
|
|
check(hd_texture_pack_match(pack, &eight, &match) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"8bpp query matches using the complete 256-entry CLUT");
|
|
const size_t entry200 = 8u * 1024u + ((900u + 200u) & 1023u);
|
|
fixture.vram[entry200] ^= 1u;
|
|
check(hd_texture_pack_match(pack, &eight, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"changing CLUT entry 200 rejects an 8bpp palette");
|
|
fixture.vram[entry200] ^= 1u;
|
|
|
|
/* Intersect invalidation preserves disjoint surviving fragments. */
|
|
hd_texture_pack_invalidate(pack, 14, 20, 4, 4);
|
|
HdTextureDrawQuery left = query4(fixture.vram, 0, 15);
|
|
check(hd_texture_pack_match(pack, &left, &match) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"non-intersected split of an upload remains matchable");
|
|
check(hd_texture_pack_match(pack, &four, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"query crossing an invalidated upload fragment falls back");
|
|
hd_texture_pack_invalidate(pack, 11, 21, 1, 1);
|
|
check(hd_texture_pack_match(pack, &left, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"single-word intersect invalidation punches a real residency hole");
|
|
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
|
|
void test_tracking_savestate_continuity(Fixture& fixture) {
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
check(hd_texture_pack_track_upload(pack, 10, 20, 8, 4,
|
|
fixture.upload_words.data(),
|
|
fixture.upload_words.size(), nullptr) == 1,
|
|
"savestate fixture upload tracks");
|
|
hd_texture_pack_invalidate(pack, 14, 20, 4, 4);
|
|
const HdTextureDrawQuery left = query4(fixture.vram, 0, 15);
|
|
const HdTextureDrawQuery full = query4(fixture.vram, 0, 31);
|
|
HdTextureMatch before{};
|
|
check(hd_texture_pack_match(pack, &left, &before) ==
|
|
HD_TEXTURE_LOOKUP_FOUND,
|
|
"savestate fixture retains a split upload fragment");
|
|
|
|
uint8_t* state = nullptr;
|
|
size_t state_size = 0;
|
|
check(hd_texture_pack_tracking_state_save(
|
|
pack, &state, &state_size) == 1 && state && state_size > 24,
|
|
"upload residency serializes to a non-empty portable wire");
|
|
check(hd_texture_pack_tracking_state_check(state, state_size) == 1,
|
|
"serialized upload residency passes side-effect-free preflight");
|
|
|
|
hd_texture_pack_reset_tracking(pack);
|
|
HdTextureMatch match{};
|
|
check(hd_texture_pack_match(pack, &left, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"full-VRAM restore reset removes host upload identity");
|
|
check(hd_texture_pack_tracking_state_load(pack, state, state_size) == 1,
|
|
"saved upload residency restores after the VRAM reset");
|
|
check(hd_texture_pack_tracking_upload_count(pack) == 1,
|
|
"restore recovers the exact live upload count");
|
|
check(hd_texture_pack_match(pack, &left, &match) ==
|
|
HD_TEXTURE_LOOKUP_FOUND &&
|
|
match.upload_serial == before.upload_serial,
|
|
"restored tracker preserves replacement match and upload identity");
|
|
check(hd_texture_pack_match(pack, &full, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"restored tracker preserves the invalidated residency hole");
|
|
std::vector<uint8_t> corrupt(state, state + state_size);
|
|
corrupt[20] = 1; /* reserved header word */
|
|
check(hd_texture_pack_tracking_state_check(corrupt.data(), corrupt.size()) == 0,
|
|
"corrupt tracker header fails closed");
|
|
check(hd_texture_pack_tracking_state_check(state, state_size - 1) == 0,
|
|
"truncated tracker wire fails closed");
|
|
check(hd_texture_pack_tracking_state_load(
|
|
pack, corrupt.data(), corrupt.size()) == 0 &&
|
|
hd_texture_pack_match(pack, &left, &match) ==
|
|
HD_TEXTURE_LOOKUP_FOUND,
|
|
"failed tracker load leaves the current residency untouched");
|
|
hd_texture_pack_invalidate(pack, 10, 20, 1, 1);
|
|
check(hd_texture_pack_match(pack, &left, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"restored tracker rebuilds invalidation bounds before GPU draws");
|
|
std::free(state);
|
|
hd_texture_pack_destroy(pack);
|
|
|
|
state = nullptr;
|
|
state_size = 0;
|
|
check(hd_texture_pack_tracking_state_save(
|
|
nullptr, &state, &state_size) == 1 && state_size == 24 &&
|
|
hd_texture_pack_tracking_state_check(state, state_size) == 1 &&
|
|
hd_texture_pack_tracking_state_load(nullptr, state, state_size) == 1,
|
|
"renderer-independent states carry a valid empty HD tracker");
|
|
std::free(state);
|
|
}
|
|
|
|
void test_wrapping(Fixture& fixture) {
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
uint32_t hash = 0;
|
|
check(hd_texture_pack_track_upload(pack, 1022, 510, 4, 4,
|
|
fixture.wrap_words.data(),
|
|
fixture.wrap_words.size(), &hash) == 1,
|
|
"upload crossing both VRAM edges is tracked");
|
|
HdTextureDrawQuery query{};
|
|
query.page_x = 1022;
|
|
query.page_y = 510;
|
|
query.depth = HD_TEXTURE_DEPTH_16BPP;
|
|
query.u_first = 0;
|
|
query.u_last = 3;
|
|
query.v_first = 0;
|
|
query.v_last = 3;
|
|
query.vram = fixture.vram.data();
|
|
query.vram_word_count = fixture.vram.size();
|
|
HdTextureMatch match{};
|
|
check(hd_texture_pack_match(pack, &query, &match) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"wrapped page/UV footprint is covered by one logical upload");
|
|
check(match.entry.texture_hash == fixture.wrap_hash,
|
|
"wrapped match preserves original logical upload hash");
|
|
hd_texture_pack_invalidate(pack, 0, 0, 1, 1);
|
|
check(hd_texture_pack_match(pack, &query, &match) == HD_TEXTURE_LOOKUP_NONE,
|
|
"physical-edge invalidation intersects a wrapped upload");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
|
|
void test_ambiguity_fallback() {
|
|
TempTree temp;
|
|
const fs::path direct = temp.path / "ambiguous-texture-replacements";
|
|
fs::create_directories(direct);
|
|
uint16_t word = 0;
|
|
uint32_t hash = 0;
|
|
for (unsigned value = 0; value <= 0xFFFFu; ++value) {
|
|
word = static_cast<uint16_t>(value);
|
|
hash = hd_texture_crc32_words_le(&word, 1);
|
|
if (hash != 0 && hash < 0x10000000u) break;
|
|
}
|
|
check(hash < 0x10000000u, "found a short numeric CRC for alias test");
|
|
touch(direct / key_name(hash, 0));
|
|
touch(direct / padded_key_name(hash, 0));
|
|
|
|
HdTexturePack* pack = open_pack(direct);
|
|
if (!pack) return;
|
|
HdTexturePackEntry entry{};
|
|
check(hd_texture_pack_lookup(pack, hash, 0, &entry) ==
|
|
HD_TEXTURE_LOOKUP_AMBIGUOUS,
|
|
"numeric filename aliases are rejected as ambiguous");
|
|
|
|
std::vector<uint16_t> vram(1024u * 512u, 0);
|
|
check(hd_texture_pack_track_upload(pack, 30, 40, 1, 1,
|
|
&word, 1, nullptr) == 1,
|
|
"ambiguous fixture upload tracks");
|
|
HdTextureDrawQuery query{};
|
|
query.page_x = 30;
|
|
query.page_y = 40;
|
|
query.depth = HD_TEXTURE_DEPTH_16BPP;
|
|
query.vram = vram.data();
|
|
query.vram_word_count = vram.size();
|
|
HdTextureMatch match{};
|
|
check(hd_texture_pack_match(pack, &query, &match) ==
|
|
HD_TEXTURE_LOOKUP_AMBIGUOUS,
|
|
"draw ambiguity fails closed instead of choosing enumeration order");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
|
|
void test_bounded_async_decode(Fixture& fixture) {
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
check(hd_texture_pack_track_upload(pack, 10, 20, 8, 4,
|
|
fixture.upload_words.data(),
|
|
fixture.upload_words.size(), nullptr) == 1,
|
|
"decode fixture upload tracks");
|
|
int status = hd_texture_pack_request_decode(
|
|
pack, fixture.upload_hash, fixture.palette4_hash);
|
|
check(status == HD_TEXTURE_LOOKUP_NONE,
|
|
"first decode request queues work instead of decoding synchronously");
|
|
|
|
HdTexturePixels pixels{};
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
do {
|
|
status = hd_texture_pack_acquire_decoded(
|
|
pack, fixture.upload_hash, fixture.palette4_hash, &pixels);
|
|
if (status == HD_TEXTURE_LOOKUP_FOUND) break;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
} while (std::chrono::steady_clock::now() < deadline);
|
|
check(status == HD_TEXTURE_LOOKUP_FOUND,
|
|
"bounded worker publishes a decoded replacement");
|
|
check(pixels.rgba && pixels.width == 64 && pixels.height == 8 &&
|
|
pixels.stride == 64u * 4u,
|
|
"decoded lease exposes expected RGBA dimensions");
|
|
hd_texture_pack_destroy(pack);
|
|
check(pixels.rgba && pixels.rgba[2] == 0xA5u && pixels.rgba[3] == 0x7Fu,
|
|
"decoded lease preserves the pack's 7-bit opaque alpha");
|
|
hd_texture_pixels_release(&pixels);
|
|
}
|
|
|
|
void test_bounded_preload(Fixture& fixture) {
|
|
namespace tp = psx_texpreload;
|
|
{
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
HdTexturePreloadResult r{};
|
|
/* One valid 64x8 PNG; the other keys are empty (malformed) files. */
|
|
const int status = hd_texture_pack_preload(pack, size_t{64} << 20, 4, &r);
|
|
check(status >= 0 && r.loaded == 1 && r.bytes == 64u * 8u * 4u,
|
|
"preload pins the valid replacement");
|
|
check(r.requested == r.loaded + r.failed + r.over_budget &&
|
|
(r.requested == 1 ? status == 1 : (status == 0 && r.failed >= 1)),
|
|
"malformed replacements are reported, not counted as preloaded");
|
|
hd_texture_pack_set_decode_budget(pack, 1);
|
|
HdTexturePreloadResult repeat{};
|
|
check(hd_texture_pack_preload(pack,64u*8u*4u,1,&repeat) >= 0 &&
|
|
repeat.loaded == 1 && repeat.bytes == 64u*8u*4u &&
|
|
repeat.failed == repeat.requested - 1,
|
|
"repeat counts pinned images without decoding them under a reduced budget");
|
|
check(hd_texture_pack_preload(pack,64u*8u*4u-1,1,&repeat) == -1 &&
|
|
repeat.bytes == 64u*8u*4u,
|
|
"a smaller lifetime cap rejects and reports retained residency");
|
|
check(hd_texture_pack_track_upload(pack, 10, 20, 8, 4,
|
|
fixture.upload_words.data(),
|
|
fixture.upload_words.size(), nullptr) == 1,
|
|
"preload fixture upload tracks");
|
|
HdTexturePixels pixels{};
|
|
check(hd_texture_pack_acquire_decoded(pack, fixture.upload_hash, fixture.palette4_hash,
|
|
&pixels) == HD_TEXTURE_LOOKUP_FOUND &&
|
|
pixels.width == 64,
|
|
"a preloaded image is resident and survives budget eviction");
|
|
hd_texture_pixels_release(&pixels);
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
{
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
HdTexturePreloadResult r{};
|
|
check(hd_texture_pack_preload(pack, 64, 4, &r) == 0 && r.loaded == 0 &&
|
|
r.over_budget == 1 && r.bytes == 0,
|
|
"the aggregate cap skips an oversized image");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
{
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
tp::fault_mode() = 1;
|
|
HdTexturePreloadResult r{};
|
|
const int status = hd_texture_pack_preload(pack, size_t{64} << 20, 4, &r);
|
|
tp::fault_mode() = 0;
|
|
check(status == 0 && r.worker_errors == 1 && r.loaded + 1 + r.over_budget <= r.requested,
|
|
"an injected worker failure is caught and reported as partial");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
{
|
|
HdTexturePack* pack = open_pack(fixture.root);
|
|
if (!pack) return;
|
|
tp::fault_mode() = 3;
|
|
HdTexturePreloadResult r{};
|
|
const int status = hd_texture_pack_preload(pack, size_t{64} << 20, 4, &r);
|
|
tp::fault_mode() = 0;
|
|
check(status == -1 && r.loaded == 1 && r.bytes == 64u * 8u * 4u,
|
|
"publication failure still accounts the retained pinned image");
|
|
check(hd_texture_pack_preload(pack,64u*8u*4u,1,&r) >= 0 &&
|
|
r.loaded == 1 && r.bytes == 64u*8u*4u && r.failed == r.requested-1,
|
|
"publication retry accounts existing pinned residency");
|
|
hd_texture_pack_destroy(pack);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
test_crc_exact_little_endian();
|
|
test_indexed_palette_fallback();
|
|
Fixture fixture;
|
|
test_scan_mapping(fixture);
|
|
test_palette_match_and_invalidation(fixture);
|
|
test_tracking_savestate_continuity(fixture);
|
|
test_wrapping(fixture);
|
|
test_ambiguity_fallback();
|
|
test_bounded_async_decode(fixture);
|
|
test_bounded_preload(fixture);
|
|
|
|
if (failures) {
|
|
std::fprintf(stderr, "test_hd_texture_pack: %d failure(s)\n", failures);
|
|
return 1;
|
|
}
|
|
std::puts("PASS: external HD texture pack scan/hash/tracker semantics");
|
|
return 0;
|
|
}
|