mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
f9db2561d0
* fix: reuse resident HD textures and publish live background dumps * test: keep residency stubs local to the filtering fixture
617 lines
43 KiB
C++
617 lines
43 KiB
C++
#define STB_IMAGE_IMPLEMENTATION
|
|
#include "../third_party/stb_image.h"
|
|
#include "duckstation_texture_pack.h"
|
|
#include "../src/png_write.h"
|
|
|
|
#include <array>
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
namespace {
|
|
int failures = 0;
|
|
void check(bool ok, const char* what) {
|
|
if (!ok) { std::fprintf(stderr,"FAIL: %s\n",what); ++failures; }
|
|
}
|
|
using Pack = std::unique_ptr<DuckTexturePack,decltype(&duck_texture_pack_destroy)>;
|
|
Pack create(const fs::path& root) {
|
|
DuckTexturePack* pack = nullptr; char error[512]{};
|
|
check(duck_texture_pack_create(root.u8string().c_str(),&pack,error,sizeof(error)) == 1,error);
|
|
return Pack(pack,&duck_texture_pack_destroy);
|
|
}
|
|
std::vector<uint16_t> words(size_t n) {
|
|
std::vector<uint16_t> result(n);
|
|
for (size_t i = 0; i < n; ++i) result[i] = static_cast<uint16_t>((i*7919)^0xa51c);
|
|
return result;
|
|
}
|
|
void place(std::vector<uint16_t>& vram, unsigned x, unsigned y, unsigned w,
|
|
const std::vector<uint16_t>& source) {
|
|
for (size_t i = 0; i < source.size(); ++i) vram[(y+i/w)*1024+x+i%w] = source[i];
|
|
}
|
|
HdTextureDrawQuery query(std::vector<uint16_t>& vram, uint16_t page_x, uint16_t page_y,
|
|
uint8_t depth, uint8_t u0, uint8_t u1, uint8_t v0, uint8_t v1,
|
|
uint16_t clut_x = 512, uint16_t clut_y = 400) {
|
|
return {page_x,page_y,depth,u0,u1,v0,v1,clut_x,clut_y,vram.data(),vram.size()};
|
|
}
|
|
void png(const fs::path& path, unsigned w = 4, unsigned h = 4) {
|
|
fs::create_directories(path.parent_path());
|
|
std::vector<uint8_t> pixels(size_t(w)*h*4);
|
|
constexpr uint8_t alpha[] = {0,127,128,242,243,255};
|
|
for (size_t i = 0; i < size_t(w)*h; ++i) {
|
|
pixels[i*4] = 23; pixels[i*4+1] = 57; pixels[i*4+2] = 91; pixels[i*4+3] = alpha[i%6];
|
|
}
|
|
#ifdef _WIN32
|
|
FILE* file = _wfopen(path.c_str(),L"wb");
|
|
#else
|
|
FILE* file = std::fopen(path.c_str(),"wb");
|
|
#endif
|
|
check(file != nullptr,"open original test PNG");
|
|
if (file) { check(png_write_rgba(file,pixels.data(),w,h) == 1,"encode original test PNG"); std::fclose(file); }
|
|
}
|
|
void config(const fs::path& root, const char* contents) {
|
|
fs::create_directories(root);
|
|
std::ofstream file(root/"config.yaml"); file << contents;
|
|
}
|
|
std::vector<fs::path> dumps(const fs::path& root) {
|
|
std::vector<fs::path> result;
|
|
if (fs::exists(root/"dumps")) for (const auto& file : fs::directory_iterator(root/"dumps"))
|
|
if (file.path().extension() == ".png") result.push_back(file.path());
|
|
std::sort(result.begin(),result.end()); return result;
|
|
}
|
|
std::vector<uint8_t> read_png(const fs::path& file, unsigned w, unsigned h) {
|
|
std::ifstream input(file,std::ios::binary);
|
|
std::vector<uint8_t> encoded((std::istreambuf_iterator<char>(input)),{}), result;
|
|
int width = 0, height = 0, channels = 0;
|
|
auto* pixels = encoded.empty() ? nullptr : stbi_load_from_memory(encoded.data(),static_cast<int>(encoded.size()),&width,&height,&channels,4);
|
|
check(pixels && width == int(w) && height == int(h),"dump PNG has expected dimensions");
|
|
if (pixels) { result.assign(pixels,pixels+size_t(width)*height*4); stbi_image_free(pixels); }
|
|
return result;
|
|
}
|
|
void flush(DuckTexturePack* pack) {
|
|
char error[512]{};
|
|
check(duck_texture_pack_flush_dumps(pack,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,error);
|
|
}
|
|
std::string filename(const DuckTextureKey& key) {
|
|
char name[192]{}; check(duck_texture_format_name(&key,name,sizeof(name)),"format fixture name");
|
|
return std::string(name)+".png";
|
|
}
|
|
DuckTexturePixels decoded(DuckTexturePack* pack, uint64_t id) {
|
|
DuckTexturePixels pixels{};
|
|
check(duck_texture_pack_request_decode(pack,id) >= 0,"queue PNG decode");
|
|
const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(5);
|
|
int status = HD_TEXTURE_LOOKUP_NONE;
|
|
do {
|
|
status = duck_texture_pack_acquire_decoded(pack,id,&pixels);
|
|
if (status != HD_TEXTURE_LOOKUP_NONE) break;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
} while (std::chrono::steady_clock::now() < end);
|
|
check(status == HD_TEXTURE_LOOKUP_FOUND,"background PNG decode becomes ready");
|
|
return pixels;
|
|
}
|
|
bool decode_fails(DuckTexturePack* pack, uint64_t id) {
|
|
int status = duck_texture_pack_request_decode(pack,id);
|
|
const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(5);
|
|
while (status == HD_TEXTURE_LOOKUP_NONE && std::chrono::steady_clock::now() < end) {
|
|
DuckTexturePixels pixels{};
|
|
status = duck_texture_pack_acquire_decoded(pack,id,&pixels);
|
|
duck_texture_pixels_release(&pixels);
|
|
if (status == HD_TEXTURE_LOOKUP_NONE) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
}
|
|
return status == HD_TEXTURE_LOOKUP_ERROR;
|
|
}
|
|
|
|
void fixed_hash_vectors() {
|
|
/* Independently produced by Python xxhash 3.6.0's native wheel. These
|
|
* literal expectations cover XXH3's short/medium/long-input boundaries. */
|
|
const std::pair<size_t,uint64_t> vectors[] = {
|
|
{0,0x2d06800538d394c2ULL},{1,0xc5779b2f56fbe2e1ULL},{2,0x0edb68960f55aef6ULL},
|
|
{3,0xfce9d54aec3f9d32ULL},{4,0x3631d2a04ec85311ULL},{8,0x000ca76cb9b329dcULL},
|
|
{9,0xab6a9091cf532d8aULL},{16,0x9cbcdea9305d7528ULL},{17,0x2f133c3e193cbe48ULL},
|
|
{32,0x3e4a087da5cd5990ULL},{64,0xc3e3ef9ad7f08dacULL},{65,0x740c4e7d1690d606ULL},
|
|
{120,0x5942d561231419b2ULL},{121,0x152cb5857c4535f3ULL},{128,0x351cd56a8fe50e66ULL},
|
|
{129,0x38a45de7ae84906eULL},{256,0x0005d0b83a005f4eULL},{8192,0xc68a00dcca652920ULL}
|
|
};
|
|
for (const auto& v : vectors) {
|
|
const auto input = words(v.first);
|
|
check(duck_texture_hash_words_le(input.data(),input.size()) == v.second,"fixed independent LE16 XXH3 vector");
|
|
}
|
|
std::vector<uint16_t> vram(1024*512,0xf00d);
|
|
place(vram,101,39,2,words(8));
|
|
check(duck_texture_hash_rect(vram.data(),vram.size(),101,39,2,4) == 0x000ca76cb9b329dcULL,
|
|
"strided native rectangle excludes unrelated VRAM words");
|
|
check(duck_texture_hash_rect(vram.data(),vram.size(),1023,0,2,1) == 0,"wrapped hash rectangle rejected");
|
|
}
|
|
|
|
void names() {
|
|
DuckTextureKey key{};
|
|
check(duck_texture_parse_name("texupload-P4-0123456789abcdef-FEDCBA9876543210-64x256-0-192-64x64-P0-14.png",&key) == 1,
|
|
"literal upstream documented upload filename");
|
|
check(key.source_hash == 0x0123456789abcdefULL && key.palette_hash == 0xfedcba9876543210ULL &&
|
|
key.source_width_words == 64 && key.offset_y == 192 && key.width == 64 && key.palette_max == 14,
|
|
"filename dimensions distinguish source words from expanded texels");
|
|
check(duck_texture_parse_name("texpage-STC16-0123456789ABCDEF-256x256-8-16-32x64.PNG",&key) == 1 &&
|
|
key.kind == DUCK_TEXTURE_PAGE && key.semitransparent && key.depth == 2 && !key.palette_hash,
|
|
"direct ST filename omits palette fields");
|
|
for (const char* extension : {"jpg","jpeg","webp"})
|
|
check(duck_texture_parse_name((std::string("texupload-C16-0123456789ABCDEF-4x2-0-0-4x2.")+extension).c_str(),&key),
|
|
"JPEG and WebP canonical payload extensions accepted");
|
|
const char* invalid[] = {
|
|
"texpage-P4-12345678-0123456789ABCDEF-64x256-0-0-4x1-P0-15.png",
|
|
"texpage-P4-0123456789ABCDEF-0123456789ABCDEF-64x256-0-0-3x1-P0-15.png",
|
|
"texpage-P4-0123456789ABCDEF-0123456789ABCDEF-64x256-0-0-4x1-P0-16.png",
|
|
"texpage-P8-0123456789ABCDEF-0123456789ABCDEF-128x256-0-0-4x1-P15-0.png",
|
|
"texupload-C16-0123456789ABCDEF-0x256-0-0-4x1.png",
|
|
"texupload-C16-0123456789ABCDEF-65536x1-0-0-4x1.png",
|
|
"texpage-C16-0123456789ABCDEF-256x256-255-0-2x1.png",
|
|
"texpage-C16-0123456789ABCDEF-256x256-0-0-4x1.png.old",
|
|
"../texpage-C16-0123456789ABCDEF-256x256-0-0-4x1.png"
|
|
};
|
|
for (const char* name : invalid) check(!duck_texture_parse_name(name,&key),"targeted malformed/unsupported name rejected");
|
|
}
|
|
|
|
void page_and_decode(const fs::path& root) {
|
|
const char* name = "texpage-P4-3631D2A04EC85311-9CBCDEA9305D7528-64x256-4-2-8x2-P0-15.png";
|
|
png(root/"replacements"/"nested"/name,16,4);
|
|
std::vector<uint16_t> vram(1024*512);
|
|
place(vram,65,258,2,words(4)); place(vram,512,400,16,words(16));
|
|
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
|
DuckTextureMatch match{};
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"fixed subpage fixture matches");
|
|
check(match.origin_u == 4 && match.origin_v == 2 && match.source_width == 8 && match.source_height == 2,
|
|
"crop extent and origin are page-space texels");
|
|
auto pixels = decoded(pack.get(),match.entry_id);
|
|
check(pixels.width == 16 && pixels.height == 4,"arbitrary PNG scaling uses image dimensions");
|
|
constexpr uint8_t alpha[] = {0,127,128,242,243,255};
|
|
if (pixels.rgba) for (unsigned i = 0; i < 6; ++i)
|
|
check(pixels.rgba[i*4+3] == alpha[i],"decode retains raw alpha boundary bytes");
|
|
const uint16_t old = vram[258*1024+65];
|
|
vram[258*1024+65] ^= 1; duck_texture_pack_invalidate(pack.get(),65,258,1,1);
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"write invalidates positive page cache");
|
|
vram[258*1024+65] = old; duck_texture_pack_invalidate(pack.get(),65,258,1,1);
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"write invalidates negative page cache");
|
|
vram[400*1024+512] ^= 1; duck_texture_pack_invalidate(pack.get(),512,400,1,1);
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"CLUT write invalidates cached match");
|
|
pack.reset();
|
|
check(pixels.rgba && pixels.rgba[0] == 23,"pixel lease outlives pack and decode worker");
|
|
duck_texture_pixels_release(&pixels);
|
|
}
|
|
|
|
void upload(const fs::path& root) {
|
|
png(root/"replacements"/"texupload-C16-000CA76CB9B329DC-4x2-2-0-2x2.png",4,8);
|
|
std::vector<uint16_t> vram(1024*512); place(vram,66,257,4,words(8));
|
|
auto pack = create(root); auto q = query(vram,64,256,2,4,5,1,2);
|
|
DuckTextureMatch match{};
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"upload names require original source identity");
|
|
check(duck_texture_pack_track_upload(pack.get(),66,257,4,2,vram.data(),vram.size()),"track post-write native upload");
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND && match.origin_u == 4 && match.origin_v == 1,
|
|
"upload crop has correct source origin");
|
|
duck_texture_pack_invalidate(pack.get(),66,257,1,1); /* outside replacement crop */
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"partial source overwrite drops entire upload identity");
|
|
duck_texture_pack_track_upload(pack.get(),66,257,4,2,vram.data(),vram.size());
|
|
duck_texture_pack_reset_tracking(pack.get());
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"savestate/reset clears upload identity");
|
|
check(!duck_texture_pack_track_upload(pack.get(),1023,0,2,1,vram.data(),vram.size()),"wrapped upload explicitly falls back");
|
|
}
|
|
|
|
void reload_tracking(const fs::path& root) {
|
|
std::vector<uint16_t> vram(1024*512); place(vram,66,257,4,words(8));
|
|
auto old = create(root);
|
|
check(duck_texture_pack_track_upload(old.get(),66,257,4,2,vram.data(),vram.size()),"capture upload before replacement file exists");
|
|
png(root/"replacements"/"texupload-C16-000CA76CB9B329DC-4x2-2-0-2x2.png");
|
|
auto refreshed = create(root); auto q = query(vram,64,256,2,4,5,1,2); DuckTextureMatch match{};
|
|
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"newly indexed replacement initially has no upload identity");
|
|
check(duck_texture_pack_copy_tracking(refreshed.get(),old.get()),"same-session reload copies upload tracking");
|
|
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"copied tracking makes newly indexed replacement immediately match");
|
|
duck_texture_pack_invalidate(refreshed.get(),66,257,1,1);
|
|
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"destination tracking invalidates independently");
|
|
auto second = create(root); duck_texture_pack_copy_tracking(second.get(),old.get());
|
|
check(duck_texture_pack_match(second.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"destination invalidation leaves original tracker intact");
|
|
}
|
|
|
|
void palettes(const fs::path& root) {
|
|
std::vector<uint16_t> vram(1024*512); place(vram,128,0,1,words(2));
|
|
place(vram,1008,400,16,words(16));
|
|
const char* edge = "texpage-P8-0EDB68960F55AEF6-9CBCDEA9305D7528-128x256-0-0-2x2-P0-255.png";
|
|
png(root/"edge"/"replacements"/edge);
|
|
auto pack = create(root/"edge"); auto q = query(vram,128,0,1,0,1,0,1,1008,400); DuckTextureMatch match{};
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"full P8 edge CLUT hashes remaining 16 words");
|
|
vram[400*1024] = 42; duck_texture_pack_invalidate(pack.get(),0,400,1,1);
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"full P8 CLUT hash does not wrap into row start");
|
|
const char* partial = "texpage-P8-0EDB68960F55AEF6-3631D2A04EC85311-128x256-0-0-2x2-P8-11.png";
|
|
png(root/"partial"/"replacements"/partial);
|
|
place(vram,512,400,16,words(16)); q.clut_x = 512;
|
|
pack = create(root/"partial");
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"reduced P8 min8-max11 hashes prefix4 per upstream pin");
|
|
vram[400*1024+520] ^= 1; duck_texture_pack_invalidate(pack.get(),520,400,1,1);
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"pinned prefix quirk ignores palette8 despite declared minimum8");
|
|
vram[400*1024+512] ^= 1; duck_texture_pack_invalidate(pack.get(),512,400,1,1);
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"reduced P8 prefix change stops replacement");
|
|
q.clut_x = 1023;
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"misaligned/edge reduced CLUT rejected");
|
|
}
|
|
|
|
void st_alias_duplicates(const fs::path& root) {
|
|
const char* ordinary = "texpage-C16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
|
const char* st = "texpage-STC16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
|
png(root/"paired"/"replacements"/ordinary); png(root/"paired"/"replacements"/st);
|
|
std::vector<uint16_t> vram(1024*512); place(vram,64,0,4,words(8)); auto q = query(vram,64,0,2,0,3,0,1);
|
|
auto pack = create(root/"paired"); DuckTextureMatch match{};
|
|
check(duck_texture_pack_match_draw(pack.get(),&q,0,&match) == HD_TEXTURE_LOOKUP_FOUND && !match.key.semitransparent,
|
|
"ordinary primitive prefers ordinary filename");
|
|
check(duck_texture_pack_match_draw(pack.get(),&q,1,&match) == HD_TEXTURE_LOOKUP_FOUND && match.key.semitransparent,
|
|
"semitransparent primitive prefers ST filename");
|
|
png(root/"alias"/"replacements"/"art"/"sample.png");
|
|
fs::create_directories(root/"alias");
|
|
std::ofstream config(root/"alias"/"config.yaml");
|
|
config << "MaxVRAMWriteCoalesceWidth: 1\nBogusOption: true\nAliases:\n " << std::string(ordinary).substr(0,std::strlen(ordinary)-4)
|
|
<< ": |\n art/sample.png\n"; config.close();
|
|
pack = create(root/"alias");
|
|
check(duck_texture_pack_match_draw(pack.get(),&q,1,&match) == HD_TEXTURE_LOOKUP_FOUND && !match.key.semitransparent,
|
|
"bounded literal alias and opposite-convention fallback");
|
|
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
|
DuckTextureConfig settings{}; duck_texture_pack_get_config(pack.get(),&settings);
|
|
check(settings.max_vram_write_coalesce_width == 1,"exact root-level coalesce option recognized");
|
|
check(info.ignored_count && std::strstr(info.diagnostic,"BogusOption"),"unknown config option explicitly diagnosed");
|
|
png(root/"duplicate"/"replacements"/"a"/ordinary); png(root/"duplicate"/"replacements"/"b"/ordinary);
|
|
pack = create(root/"duplicate");
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_AMBIGUOUS,"recursive duplicate keys fall back deterministically");
|
|
q.u_first = 254; q.u_last = 1;
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"wrapped UV interval falls back");
|
|
}
|
|
|
|
void failed_payloads(const fs::path& root) {
|
|
const char* name = "texpage-C16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
|
png(root/"budget"/"replacements"/name);
|
|
std::vector<uint16_t> vram(1024*512); place(vram,64,0,4,words(8)); auto q = query(vram,64,0,2,0,3,0,1);
|
|
auto pack = create(root/"budget"); DuckTextureMatch match{};
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"over-budget payload identity remains valid metadata");
|
|
duck_texture_pack_set_decode_budget(pack.get(),8);
|
|
check(decode_fails(pack.get(),match.entry_id),"over-budget decoded image fails before pixel allocation");
|
|
fs::create_directories(root/"malformed"/"replacements");
|
|
std::ofstream invalid(root/"malformed"/"replacements"/name,std::ios::binary); invalid << "not PNG"; invalid.close();
|
|
pack = create(root/"malformed");
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"malformed PNG identity can be indexed without render-thread decode");
|
|
check(decode_fails(pack.get(),match.entry_id),"malformed PNG fails asynchronously for native fallback");
|
|
const fs::path oversized = root/"dimension"/"replacements"/name;
|
|
png(oversized);
|
|
std::ifstream file(oversized,std::ios::binary); std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)),{}); file.close();
|
|
/* Forge a valid IHDR width above the decode dimension bound. Keeping the
|
|
* tiny original IDAT makes the fixture inexpensive and verifies rejection
|
|
* using image metadata before an enormous allocation could happen. */
|
|
bytes[16] = 0; bytes[17] = 0; bytes[18] = 0x23; bytes[19] = 0x28; /* 9000 */
|
|
const uint32_t crc = png_crc_update(0xffffffffu,bytes.data()+12,17)^0xffffffffu;
|
|
for (unsigned i = 0; i < 4; ++i) bytes[29+i] = static_cast<uint8_t>(crc >> (24-8*i));
|
|
std::ofstream changed(oversized,std::ios::binary|std::ios::trunc);
|
|
changed.write(reinterpret_cast<const char*>(bytes.data()),bytes.size()); changed.close();
|
|
pack = create(root/"dimension"); duck_texture_pack_match(pack.get(),&q,&match);
|
|
check(decode_fails(pack.get(),match.entry_id),"oversized PNG IHDR fails within native fallback bounds");
|
|
}
|
|
|
|
void dumping(const fs::path& root) {
|
|
config(root,"DumpTexturePages: true\nDumpTextureWidthThreshold: 1\nDumpTextureHeightThreshold: 1\nReducePaletteRange: false\n");
|
|
auto pack = create(root); std::vector<uint16_t> vram(1024*512);
|
|
vram[0] = 0x3210; vram[400*1024+1] = 0x001f; vram[400*1024+2] = 0x8000; vram[400*1024+3] = 0xffff;
|
|
auto q = query(vram,0,0,0,0,3,0,0,0,400); char error[512]{};
|
|
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"queue native ST page dump");
|
|
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_NONE,"dump session deduplicates without filesystem access");
|
|
pack.reset(); /* required to finish queued background writes */
|
|
const fs::path file = root/"dumps"/"texpage-STP4-A92BF769517E0361-59786C612FB45363-64x256-0-0-4x1-P0-15.png";
|
|
check(fs::is_regular_file(file),"fixed independent hash filename published at worker shutdown");
|
|
int width = 0, height = 0, channels = 0;
|
|
std::ifstream encoded_file(file,std::ios::binary);
|
|
std::vector<uint8_t> encoded((std::istreambuf_iterator<char>(encoded_file)),{});
|
|
auto* image = encoded.empty() ? nullptr : stbi_load_from_memory(encoded.data(),static_cast<int>(encoded.size()),&width,&height,&channels,4);
|
|
constexpr uint8_t expected[] = {0,0,0,0,255,0,0,255,0,0,0,143,255,255,255,143};
|
|
check(image && width == 4 && height == 1 && !std::memcmp(image,expected,sizeof(expected)),"dump decodes native indices/CLUT and ST alpha");
|
|
if (image) stbi_image_free(image);
|
|
std::ofstream owner(file,std::ios::binary|std::ios::trunc); owner << "owner edit"; owner.close();
|
|
pack = create(root); duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)); pack.reset();
|
|
std::ifstream edited(file,std::ios::binary); std::string payload((std::istreambuf_iterator<char>(edited)),{});
|
|
check(payload == "owner edit","existing owner dump is never overwritten");
|
|
config(root/"upload","DumpTextureWidthThreshold: 1\nDumpTextureHeightThreshold: 1\nReducePaletteRange: false\n");
|
|
pack = create(root/"upload");
|
|
duck_texture_pack_track_upload(pack.get(),0,0,1,1,vram.data(),vram.size());
|
|
check(duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"tracked source emits texupload dump");
|
|
pack.reset();
|
|
check(fs::is_regular_file(root/"upload"/"dumps"/"texupload-P4-A92BF769517E0361-59786C612FB45363-1x1-0-0-4x1-P0-15.png"),
|
|
"upload dump retains original source dimensions");
|
|
/* PNG fixtures remain independent of the VRAM pointer used by replacement
|
|
* matching, so dumping never reads already-replaced pixels. */
|
|
}
|
|
|
|
void official_binary_oracle(const fs::path& root) {
|
|
/* Literal oracle from the official DuckStation 697599c47 binary, driven
|
|
* by a separately authored synthetic PS-X EXE. No source-derived expected
|
|
* filename or hash is used here. Both normal/semi uses must aggregate. */
|
|
std::vector<uint16_t> vram(1024*512), source(10*56);
|
|
constexpr uint16_t pattern[]{0x3210,0x7654,0xba98,0xfedc};
|
|
for (size_t i = 0; i < source.size(); ++i) source[i] = pattern[i%4];
|
|
place(vram,640,0,10,source);
|
|
for (unsigned i = 1; i < 16; ++i) {
|
|
vram[256*1024+i] = static_cast<uint16_t>((i*1537)&0x7fff);
|
|
vram[257*1024+i] = static_cast<uint16_t>(((i*293)&0x7fff)|0x8000);
|
|
}
|
|
auto pack = create(root); char error[512]{};
|
|
check(duck_texture_pack_track_upload(pack.get(),640,0,10,56,vram.data(),vram.size()),"oracle immutable upload tracked");
|
|
auto a = query(vram,640,0,0,0,31,0,31,0,256);
|
|
check(duck_texture_pack_dump_draw(pack.get(),&a,0,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"oracle first ordinary use recorded");
|
|
a.u_last = 35;
|
|
check(duck_texture_pack_dump_draw(pack.get(),&a,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"oracle nested semi use expands same record");
|
|
auto b = query(vram,640,0,0,0,39,32,55,0,257);
|
|
check(duck_texture_pack_dump_draw(pack.get(),&b,0,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"oracle second palette crop recorded");
|
|
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
|
check(info.pending_dump_sources == 1 && info.pending_palette_records == 2 && info.queued_dump_count == 0,
|
|
"authoring observes one source/two palettes and writes nothing before retirement");
|
|
check(dumps(root).empty(),"oracle draws do not emit per-draw files");
|
|
place(vram,640,0,10,std::vector<uint16_t>(560));
|
|
check(duck_texture_pack_track_upload(pack.get(),640,0,10,56,vram.data(),vram.size()),"oracle full overwrite retires original snapshot");
|
|
flush(pack.get()); duck_texture_pack_get_info(pack.get(),&info);
|
|
check(info.queued_dump_count == 2 && info.pending_palette_records == 0,"oracle retirement queues exactly two identities");
|
|
const char* names[]{
|
|
"texupload-STP4-18DDF9B731440D20-0DAE5F73C88719B1-10x56-0-0-36x32-P0-15.png",
|
|
"texupload-P4-18DDF9B731440D20-AF506869B80F5CE7-10x56-0-32-40x24-P0-15.png"};
|
|
check(dumps(root).size() == 2,"official binary golden fixture produces only its two PNGs");
|
|
for (unsigned variant = 0; variant < 2; ++variant) {
|
|
const unsigned width = variant ? 40 : 36, height = variant ? 24 : 32, y0 = variant ? 32 : 0;
|
|
const auto pixels = read_png(root/"dumps"/names[variant],width,height);
|
|
bool correct = pixels.size() == size_t(width)*height*4;
|
|
for (unsigned y = 0; correct && y < height; ++y) for (unsigned x = 0; x < width; ++x) {
|
|
const unsigned index = (pattern[((y+y0)*10+x/4)%4]>>((x%4)*4))&15;
|
|
const uint16_t color = index ? static_cast<uint16_t>(variant ? ((index*293)&0x7fff)|0x8000 : (index*1537)&0x7fff) : 0;
|
|
const size_t at = (size_t(y)*width+x)*4;
|
|
for (unsigned c = 0; c < 3; ++c) correct &= pixels[at+c] == (((color>>(5*c))&31)*255+15)/31;
|
|
correct &= pixels[at+3] == (color ? 255 : 0);
|
|
}
|
|
check(correct,"official binary golden crop retains original pixels and captured palette");
|
|
}
|
|
}
|
|
|
|
void defaults_and_options(const fs::path& root) {
|
|
auto pack = create(root/"defaults"); DuckTextureConfig c{};
|
|
duck_texture_pack_get_config(pack.get(),&c);
|
|
check(!c.dump_texture_pages && !c.dump_full_texture_pages && !c.dump_c16_textures && c.reduce_palette_range &&
|
|
c.texture_dump_width_threshold == 16 && c.texture_dump_height_threshold == 16 &&
|
|
!c.max_vram_write_splits && !c.max_vram_write_coalesce_width && !c.max_vram_write_coalesce_height &&
|
|
!c.convert_copies_to_writes && !c.replacement_scale_linear_filter && c.dump_vram_write_force_alpha_channel &&
|
|
c.vram_write_dump_width_threshold == 128 && c.vram_write_dump_height_threshold == 128,
|
|
"pinned native authoring defaults are exact");
|
|
std::vector<uint16_t> vram(1024*512); char error[512]{};
|
|
auto q = query(vram,0,0,0,0,63,0,31,0,400);
|
|
check(duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)) == HD_TEXTURE_LOOKUP_NONE,"default no upload means no page fallback");
|
|
duck_texture_pack_track_upload(pack.get(),0,0,4,15,vram.data(),vram.size()); q.u_last = 15; q.v_last = 14;
|
|
duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)); flush(pack.get());
|
|
check(dumps(root/"defaults").empty(),"height below minimum suppresses dump even when width meets threshold");
|
|
duck_texture_pack_track_upload(pack.get(),0,0,16,16,vram.data(),vram.size()); q.depth = 2; q.v_last = 15;
|
|
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_NONE,"C16 dumps disabled by default");
|
|
flush(pack.get()); check(dumps(root/"defaults").empty(),"default C16 has no delayed output");
|
|
|
|
config(root/"options","DumpTexturePages: true\nDumpFullTexturePages: true\nDumpTextureForceAlphaChannel: true\n"
|
|
"DumpC16Textures: true\nReducePaletteRange: false\nDumpTextureWidthThreshold: 1\nDumpTextureHeightThreshold: 1\n"
|
|
"MaxVRAMWriteSplits: 3\nMaxVRAMWriteCoalesceWidth: 9\nMaxVRAMWriteCoalesceHeight: 11\nConvertCopiesToWrites: true\n"
|
|
"ReplacementScaleLinearFilter: true\nDumpVRAMWriteForceAlphaChannel: false\nDumpVRAMWriteWidthThreshold: 3\nDumpVRAMWriteHeightThreshold: 5\n"
|
|
"Options:\n DumpTexturePages: false\nUnknownOption: true\n");
|
|
pack = create(root/"options"); duck_texture_pack_get_config(pack.get(),&c);
|
|
check(c.dump_texture_pages && c.dump_full_texture_pages && c.dump_texture_force_alpha_channel && c.dump_c16_textures &&
|
|
!c.reduce_palette_range && c.max_vram_write_splits == 3 && c.max_vram_write_coalesce_width == 9 &&
|
|
c.max_vram_write_coalesce_height == 11 && c.convert_copies_to_writes && duck_texture_pack_linear_filter(pack.get()) &&
|
|
!c.dump_vram_write_force_alpha_channel && c.vram_write_dump_width_threshold == 3 && c.vram_write_dump_height_threshold == 5,
|
|
"exact root-level scalar options parsed without nested Options override");
|
|
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
|
check(std::strstr(info.diagnostic,"Options") && std::strstr(info.diagnostic,"UnknownOption"),"unsupported nested and unknown options diagnosed");
|
|
q = query(vram,0,0,2,0,0,0,0);
|
|
duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)); flush(pack.get());
|
|
const auto files = dumps(root/"options"); check(files.size() == 1,"full page force-alpha emits one page");
|
|
if (!files.empty()) {
|
|
DuckTextureKey key{}; duck_texture_parse_name(files[0].filename().u8string().c_str(),&key);
|
|
check(key.width == 256 && key.height == 256 && !key.semitransparent,"fullpage and force-alpha filename conventions");
|
|
const auto pixels = read_png(files[0],256,256);
|
|
check(pixels.size() == 256*256*4 && pixels[0] == 0 && pixels[3] == 255,"force-alpha preserves opaque native zero");
|
|
}
|
|
config(root/"zero","MaxHashCacheVRAMUsageMB: 0\n"); pack = create(root/"zero");
|
|
check(!duck_texture_pack_track_upload(pack.get(),0,0,16,16,vram.data(),vram.size()),"zero snapshot budget falls back without blocking");
|
|
duck_texture_pack_get_info(pack.get(),&info); check(std::strstr(info.diagnostic,"budget"),"snapshot budget fallback diagnosed");
|
|
flush(pack.get());
|
|
}
|
|
|
|
void immutable_palettes_and_pages(const fs::path& root) {
|
|
std::vector<uint16_t> vram(1024*512);
|
|
place(vram,0,0,8,std::vector<uint16_t>(8*16,0x3210)); vram[400*1024+1] = 0x001f;
|
|
auto pack = create(root/"upload"); auto q = query(vram,0,0,0,0,15,0,15,0,400); char error[512]{};
|
|
duck_texture_pack_track_upload(pack.get(),0,0,8,16,vram.data(),vram.size());
|
|
duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)); q.u_last = 31;
|
|
duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error));
|
|
vram[400*1024+1] = 0x83e0; duck_texture_pack_invalidate(pack.get(),1,400,1,1);
|
|
duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error));
|
|
/* Overwrite both source and CLUT before retirement; neither may be read
|
|
* by the writer instead of the saved original words/palettes. */
|
|
place(vram,0,0,8,std::vector<uint16_t>(8*16)); vram[400*1024+1] = 0x7c00;
|
|
duck_texture_pack_invalidate(pack.get(),0,0,8,16); flush(pack.get());
|
|
const auto files = dumps(root/"upload"); check(files.size() == 2,"palette versions keep separate lifetime records");
|
|
bool old_red = false, saved_green = false;
|
|
for (const auto& file : files) {
|
|
DuckTextureKey key{}; duck_texture_parse_name(file.filename().u8string().c_str(),&key);
|
|
check(key.width == 32 && key.height == 16 && key.semitransparent && key.palette_min == 0 && key.palette_max == 3,
|
|
"nested crops union, ordinary+semi aggregate, palette bounds reduced");
|
|
const auto pixels = read_png(file,32,16);
|
|
if (pixels.size() >= 8) { old_red |= pixels[4] == 255 && pixels[5] == 0 && pixels[7] == 255;
|
|
saved_green |= pixels[4] == 0 && pixels[5] == 255 && pixels[7] == 143; }
|
|
}
|
|
check(old_red && saved_green,"captured palette versions retain original colors and ST flags");
|
|
|
|
config(root/"page","DumpTexturePages: true\nReducePaletteRange: false\n");
|
|
vram.assign(vram.size(),0); place(vram,0,0,8,std::vector<uint16_t>(8*16,0x1111));
|
|
vram[400*1024+1] = 0x001f; vram[401*1024+1] = 0x03e0;
|
|
pack = create(root/"page"); q = query(vram,0,0,0,0,15,0,15,0,400);
|
|
duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)); auto b = q; b.clut_y = 401;
|
|
duck_texture_pack_dump_draw(pack.get(),&b,0,error,sizeof(error));
|
|
vram[401*1024+1] = 0x7c00; duck_texture_pack_invalidate(pack.get(),1,401,1,1);
|
|
q.u_last = 31; duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)); flush(pack.get());
|
|
const auto pagefiles = dumps(root/"page"); check(pagefiles.size() == 2,"changing CLUT B does not fragment CLUT A lifetime");
|
|
bool a_union = false, b_original = false;
|
|
for (const auto& file : pagefiles) {
|
|
DuckTextureKey key{}; duck_texture_parse_name(file.filename().u8string().c_str(),&key);
|
|
const auto pixels = read_png(file,key.width,key.height);
|
|
if (!pixels.empty()) { a_union |= key.width == 32 && pixels[0] == 255;
|
|
b_original |= key.width == 16 && pixels[1] == 255 && pixels[2] == 0; }
|
|
}
|
|
check(a_union && b_original,"page authoring residency is isolated per CLUT and snapshots old palettes");
|
|
}
|
|
|
|
void split_coalesce_copy_parts(const fs::path& root) {
|
|
std::vector<uint16_t> vram(1024*512); place(vram,0,0,4,std::vector<uint16_t>(4*16,0x3210));
|
|
vram[400*1024+1] = 0x1f; vram[400*1024+2] = 0x3e0; vram[400*1024+3] = 0x7c00;
|
|
DuckTextureKey key{}; key.depth = 0; key.source_width_words = 4; key.source_height = 16; key.width = 16; key.height = 16;
|
|
key.source_hash = duck_texture_hash_rect(vram.data(),vram.size(),0,0,4,16);
|
|
key.palette_hash = duck_texture_hash_words_le(vram.data()+400*1024,4); key.palette_max = 3;
|
|
config(root/"split","MaxVRAMWriteSplits: 2\n"); png(root/"split"/"replacements"/filename(key),16,16);
|
|
auto pack = create(root/"split"); duck_texture_pack_track_upload(pack.get(),0,0,4,16,vram.data(),vram.size());
|
|
auto q = query(vram,0,0,0,0,15,0,15,0,400); char error[512]{};
|
|
duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error));
|
|
vram[1024] = 0; duck_texture_pack_invalidate(pack.get(),0,1,1,1);
|
|
std::array<DuckTextureMatch,64> parts{}; size_t count = 0;
|
|
check(duck_texture_pack_match_parts(pack.get(),&q,0,parts.data(),parts.size(),&count) == HD_TEXTURE_LOOKUP_FOUND && count == 3,
|
|
"partial overwrite splits original identity into three surviving clips");
|
|
unsigned area = 0; for (size_t i = 0; i < count; ++i) area += unsigned(parts[i].clip_width)*parts[i].clip_height;
|
|
check(area == 252,"split clips exclude exactly four overwritten native texels");
|
|
check(duck_texture_pack_match_parts(pack.get(),&q,0,parts.data(),1,&count) == HD_TEXTURE_LOOKUP_ERROR && count == 0,
|
|
"insufficient multi-part output capacity never truncates composition");
|
|
auto cut = q; cut.u_last = 3; cut.v_first = cut.v_last = 1;
|
|
DuckTextureMatch match{}; check(duck_texture_pack_match(pack.get(),&cut,&match) == HD_TEXTURE_LOOKUP_NONE,"overwritten region has native fallback");
|
|
flush(pack.get()); const auto files = dumps(root/"split"); check(files.size() == 1,"split preserves pending original upload observation");
|
|
if (!files.empty()) { const auto pixels = read_png(files[0],16,16); check(pixels.size() >= 68 && pixels[68] == 255,"split dump uses immutable preoverwrite words"); }
|
|
|
|
config(root/"coalesce","MaxVRAMWriteCoalesceHeight: 8\n");
|
|
vram.assign(vram.size(),0); place(vram,0,0,4,std::vector<uint16_t>(4*16,0x1111)); vram[400*1024+1] = 31;
|
|
pack = create(root/"coalesce"); duck_texture_pack_track_upload(pack.get(),0,0,4,8,vram.data(),vram.size());
|
|
duck_texture_pack_track_upload(pack.get(),0,8,4,8,vram.data(),vram.size());
|
|
q = query(vram,0,0,0,0,15,0,15,0,400); duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)); flush(pack.get());
|
|
const auto coalesced = dumps(root/"coalesce"); check(coalesced.size() == 1,"unused adjacent writes coalesce before authoring use");
|
|
if (!coalesced.empty()) { DuckTextureKey k{}; duck_texture_parse_name(coalesced[0].filename().u8string().c_str(),&k);
|
|
check(k.source_width_words == 4 && k.source_height == 16,"coalesced original extent retained in filename"); }
|
|
|
|
config(root/"copy","ConvertCopiesToWrites: true\nDumpC16Textures: true\nDumpTextureWidthThreshold: 1\nDumpTextureHeightThreshold: 1\n");
|
|
vram.assign(vram.size(),0); place(vram,64,0,4,words(64)); vram[0] = 0x8001;
|
|
const uint64_t oldhash = duck_texture_hash_rect(vram.data(),vram.size(),64,0,4,16);
|
|
auto changed = vram; changed[64+1024] = vram[0];
|
|
DuckTextureKey updated{}; updated.depth = 2; updated.source_width_words = 4; updated.source_height = 16;
|
|
updated.width = 4; updated.height = 16; updated.source_hash = duck_texture_hash_rect(changed.data(),changed.size(),64,0,4,16);
|
|
png(root/"copy"/"replacements"/filename(updated));
|
|
pack = create(root/"copy"); duck_texture_pack_track_upload(pack.get(),64,0,4,16,vram.data(),vram.size());
|
|
q = query(vram,64,0,2,0,3,0,15); duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error));
|
|
check(duck_texture_pack_begin_copy(pack.get(),0,0,64,1,1,1,vram.data(),vram.size()),"copy begins before native write and retires old observation");
|
|
vram[64+1024] = 0x8001;
|
|
check(duck_texture_pack_end_copy(pack.get(),vram.data(),vram.size()),"copy commit records postmask native destination");
|
|
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND && match.key.source_hash == updated.source_hash && updated.source_hash != oldhash,
|
|
"copy-to-write rehashes whole ORIGINAL destination extent");
|
|
flush(pack.get()); const auto copyfiles = dumps(root/"copy"); check(copyfiles.size() == 1,"copy flush preserves old destination authored texture");
|
|
|
|
pack = create(root/"copy_default"); duck_texture_pack_track_upload(pack.get(),64,0,4,16,vram.data(),vram.size());
|
|
duck_texture_pack_begin_copy(pack.get(),0,0,64,1,1,1,vram.data(),vram.size());
|
|
duck_texture_pack_end_copy(pack.get(),vram.data(),vram.size());
|
|
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
|
check(info.snapshot_bytes == 0,"default copy drops destination identity rather than cloning source provenance");
|
|
|
|
config(root/"parts","MaxVRAMWriteSplits: 2\n");
|
|
vram.assign(vram.size(),0); place(vram,0,0,8,std::vector<uint16_t>(8*16,0x3210));
|
|
key.source_width_words = 8; key.source_hash = duck_texture_hash_rect(vram.data(),vram.size(),0,0,8,16);
|
|
key.palette_hash = duck_texture_hash_words_le(vram.data()+400*1024,4); key.width = 16;
|
|
png(root/"parts"/"replacements"/filename(key)); key.semitransparent = 1;
|
|
png(root/"parts"/"replacements"/filename(key)); key.offset_x = 16;
|
|
png(root/"parts"/"replacements"/filename(key));
|
|
pack = create(root/"parts"); duck_texture_pack_track_upload(pack.get(),0,0,8,16,vram.data(),vram.size());
|
|
q = query(vram,0,0,0,0,31,0,15,0,400);
|
|
check(duck_texture_pack_match_parts(pack.get(),&q,0,parts.data(),parts.size(),&count) == HD_TEXTURE_LOOKUP_FOUND && count == 2,
|
|
"adjacent replacement rectangles compose across one source");
|
|
bool left_ordinary = false, right_st = false;
|
|
for (size_t i = 0; i < count; ++i) { left_ordinary |= parts[i].clip_u == 0 && !parts[i].key.semitransparent;
|
|
right_st |= parts[i].clip_u == 16 && parts[i].key.semitransparent; }
|
|
check(left_ordinary && right_st,"same-coverage ST preference does not remove another rectangle's only ST asset");
|
|
check(duck_texture_pack_match_draw(pack.get(),&q,0,&match) == HD_TEXTURE_LOOKUP_NONE,"single-cover API preserves native fallback for composed draw");
|
|
}
|
|
|
|
void resident_checkpoint(const fs::path& root) {
|
|
std::vector<uint16_t> vram(1024*512);
|
|
place(vram,0,0,8,std::vector<uint16_t>(8*16,0x3210));
|
|
vram[400*1024+1] = 0x001f;
|
|
auto pack = create(root); char error[512]{};
|
|
duck_texture_pack_track_upload(pack.get(),0,0,8,16,vram.data(),vram.size());
|
|
auto q = query(vram,0,0,0,0,15,0,15,0,400);
|
|
duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error));
|
|
check(duck_texture_pack_checkpoint_dumps(pack.get(),error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"resident background checkpoint queues without retirement");
|
|
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
|
check(info.queued_dump_count == 1 && info.pending_dump_sources == 0,
|
|
"checkpoint publishes a still-resident source");
|
|
duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error));
|
|
duck_texture_pack_checkpoint_dumps(pack.get(),error,sizeof(error));
|
|
duck_texture_pack_get_info(pack.get(),&info);
|
|
check(info.queued_dump_count == 1,"unchanged resident draws do not requeue PNGs");
|
|
/* A later disjoint crop must retain the previous crop and captured CLUT.
|
|
* Its union is 32 wide, rather than a separate right-hand 16-wide image. */
|
|
q.u_first = 16; q.u_last = 31;
|
|
duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error));
|
|
vram[400*1024+1] = 0x7c00;
|
|
duck_texture_pack_checkpoint_dumps(pack.get(),error,sizeof(error));
|
|
flush(pack.get());
|
|
const auto files = dumps(root);
|
|
check(files.size() == 2,"resident checkpoint and expanded final crop are durable");
|
|
bool union_found = false;
|
|
for (const auto& file : files) {
|
|
DuckTextureKey key{}; duck_texture_parse_name(file.filename().u8string().c_str(),&key);
|
|
if (key.width == 32) {
|
|
const auto pixels = read_png(file,32,16);
|
|
union_found = key.offset_x == 0 && key.semitransparent && pixels.size() > 8 && pixels[4] == 255 && pixels[6] == 0;
|
|
}
|
|
}
|
|
check(union_found,"checkpoint preserves palette originals and earlier crop union");
|
|
}
|
|
void copied_pack_ownership(const fs::path& root) {
|
|
std::vector<uint16_t> vram(1024*512); place(vram,0,0,8,std::vector<uint16_t>(8*16,0x3210));
|
|
vram[400*1024+1] = 31;
|
|
auto original = create(root/"same"); duck_texture_pack_track_upload(original.get(),0,0,8,16,vram.data(),vram.size());
|
|
auto q = query(vram,0,0,0,0,15,0,15,0,400); char error[512]{};
|
|
duck_texture_pack_dump_draw(original.get(),&q,0,error,sizeof(error));
|
|
{
|
|
auto copied = create(root/"same"); check(duck_texture_pack_copy_tracking(copied.get(),original.get()),"same-root tracking clone with pending observations");
|
|
DuckTexturePackInfo info{}; duck_texture_pack_get_info(copied.get(),&info);
|
|
check(info.pending_dump_sources == 1 && info.pending_palette_records == 1,"cloned pack owns independent palette record count");
|
|
} /* Copy destroyed FIRST: original still has valid owned observations. */
|
|
q.u_last = 31;
|
|
check(duck_texture_pack_dump_draw(original.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,
|
|
"original remains valid and expands union after copy destruction");
|
|
flush(original.get()); check(dumps(root/"same").size() == 2,"copy destruction does not steal original larger crop");
|
|
duck_texture_pack_dump_draw(original.get(),&q,1,error,sizeof(error));
|
|
auto elsewhere = create(root/"different"); check(duck_texture_pack_copy_tracking(elsewhere.get(),original.get()),"different-root clone retains provenance");
|
|
original.reset(); flush(elsewhere.get());
|
|
check(dumps(root/"different").size() == 1,"different root does not share filename deduplication suppression");
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
const fs::path root = fs::temp_directory_path()/fs::u8path("psx-duck-texture-\xCE\xA9-"+
|
|
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
try {
|
|
fixed_hash_vectors(); names(); page_and_decode(root/"page"); upload(root/"upload"); reload_tracking(root/"reload");
|
|
palettes(root/"palettes"); st_alias_duplicates(root/"keys"); failed_payloads(root/"invalid"); dumping(root/"dump");
|
|
official_binary_oracle(root/"official-oracle"); defaults_and_options(root/"defaults");
|
|
immutable_palettes_and_pages(root/"snapshots"); split_coalesce_copy_parts(root/"lifecycle"); copied_pack_ownership(root/"ownership");
|
|
resident_checkpoint(root/"resident");
|
|
/* Every artifact is created beneath this fresh, explicitly named test
|
|
* root. No user-provided paths participate in recursive cleanup. */
|
|
if (argc > 1 && !std::strcmp(argv[1],"--keep-artifacts")) std::printf("ARTIFACT_ROOT: %s\n",root.u8string().c_str());
|
|
else fs::remove_all(root);
|
|
} catch (const std::exception& e) { check(false,e.what()); }
|
|
if (failures) { std::fprintf(stderr,"test_duckstation_texture_pack: %d failure(s)\n",failures); return 1; }
|
|
std::puts("PASS: DuckStation format, independent hashes, native matching, decode and dumps");
|
|
return 0;
|
|
}
|