mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #736: bounded HD replacement preload
Current-master rebase retains both source-retirement and preload test entry points. Full native runtime build and eight relevant checks pass. Prior combined title regression and owner visual acceptance recorded; owner explicitly authorized this merge.
This commit is contained in:
@@ -170,6 +170,12 @@ int duck_texture_pack_match_parts(DuckTexturePack* pack,
|
||||
* Release the lease even after pack destruction. */
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* pack, size_t bytes);
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* pack, uint64_t entry_id);
|
||||
/* Elective full preload with the hd_texture_pack_preload contract: bounded
|
||||
* aggregate decoded bytes and concurrency, pinned images outside the decode
|
||||
* budget, everything not preloaded left to the asynchronous decode path.
|
||||
* Returns 1 complete, 0 partial (see out), -1 on error. */
|
||||
int duck_texture_pack_preload(DuckTexturePack* pack, size_t max_bytes,
|
||||
unsigned max_threads, HdTexturePreloadResult* out);
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* pack, uint64_t entry_id,
|
||||
DuckTexturePixels* out_pixels);
|
||||
void duck_texture_pixels_release(DuckTexturePixels* pixels);
|
||||
|
||||
@@ -189,6 +189,27 @@ typedef struct HdTexturePixels {
|
||||
|
||||
void hd_texture_pack_set_decode_budget(HdTexturePack* pack,
|
||||
size_t budget_bytes);
|
||||
/* Elective full preload: decode unambiguous replacements up front and pin
|
||||
* them, bounded by max_bytes of decoded RGBA in aggregate and by max_threads
|
||||
* concurrent decodes (the caller's thread included). Pinned images are kept
|
||||
* outside the on-demand decode budget, which stays as configured. Images not
|
||||
* preloaded (over the cap, unreadable, malformed, a worker failure) are left
|
||||
* to the normal asynchronous, budgeted decode path; nothing is marked failed
|
||||
* by the preload. Returns 1 when every image was preloaded, 0 for a partial
|
||||
* preload (see out), -1 on error. Repeated calls count already pinned images
|
||||
* without reading/decoding them again; max_bytes caps the pack's total pinned
|
||||
* residency. A cap smaller than existing residency is rejected with -1 and
|
||||
* the existing residency reported; it does not discard live pinned images. */
|
||||
typedef struct HdTexturePreloadResult {
|
||||
int requested; /* unambiguous replacements considered */
|
||||
int loaded; /* resident and pinned, including earlier calls */
|
||||
int over_budget; /* skipped: the aggregate max_bytes was reached */
|
||||
int failed; /* unreadable, malformed, too large, or worker error */
|
||||
int worker_errors; /* of failed: a decode that threw (caught) */
|
||||
size_t bytes; /* total decoded RGBA pinned by this pack */
|
||||
} HdTexturePreloadResult;
|
||||
int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes,
|
||||
unsigned max_threads, HdTexturePreloadResult* out);
|
||||
int hd_texture_pack_request_decode(HdTexturePack* pack,
|
||||
uint32_t texture_hash,
|
||||
uint32_t palette_hash);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef PSX_TEXTURE_PACK_PRELOAD_HPP
|
||||
#define PSX_TEXTURE_PACK_PRELOAD_HPP
|
||||
|
||||
/* Shared machinery for the elective full texture-pack preload
|
||||
* (hd_texture_pack_preload, duck_texture_pack_preload): an aggregate
|
||||
* decoded-byte reservation and a bounded worker pool that can neither leak a
|
||||
* joinable std::thread nor let an exception escape a thread entry. */
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace psx_texpreload {
|
||||
|
||||
/* Test-only fault injection. 1: the first item's decode throws
|
||||
* std::bad_alloc. 2: creating the second pool thread throws std::system_error.
|
||||
* 3: publication fails after the first pinned image. */
|
||||
inline std::atomic<int>& fault_mode() {
|
||||
static std::atomic<int> mode{0};
|
||||
return mode;
|
||||
}
|
||||
inline void after_publish(size_t count) {
|
||||
if (count == 1 && fault_mode().load() == 3) throw std::bad_alloc();
|
||||
}
|
||||
|
||||
/* Aggregate decoded bytes, reserved before an image is decoded. */
|
||||
class Budget {
|
||||
public:
|
||||
explicit Budget(size_t cap) : cap_(cap) {}
|
||||
bool reserve(size_t bytes) {
|
||||
size_t cur = used_.load();
|
||||
do {
|
||||
if (bytes > cap_ || cur > cap_ - bytes) return false;
|
||||
} while (!used_.compare_exchange_weak(cur, cur + bytes));
|
||||
return true;
|
||||
}
|
||||
void release(size_t bytes) { used_.fetch_sub(bytes); }
|
||||
private:
|
||||
size_t cap_;
|
||||
std::atomic<size_t> used_{0};
|
||||
};
|
||||
|
||||
/* Per-item outcome, written by exactly one worker. */
|
||||
enum ItemStatus : unsigned char { kPending = 0, kLoaded, kFailed, kOverBudget };
|
||||
|
||||
/* Joins every started thread when the pool goes out of scope, including while
|
||||
* unwinding from a failed std::thread construction. */
|
||||
struct Joiner {
|
||||
std::vector<std::thread>& threads;
|
||||
~Joiner() {
|
||||
for (auto& t : threads)
|
||||
if (t.joinable()) t.join();
|
||||
}
|
||||
};
|
||||
|
||||
/* Runs item(k) for every k < n on at most max_threads threads, the caller
|
||||
* being one of them. An exception from item(k) is caught and counted, never
|
||||
* propagated out of a thread entry. A failure to start a thread only lowers
|
||||
* concurrency. Returns the number of items whose item(k) threw. */
|
||||
template <class F>
|
||||
size_t run(size_t n, unsigned max_threads, F&& item) {
|
||||
std::atomic<size_t> next{0}, errors{0};
|
||||
auto work = [&]() noexcept {
|
||||
for (size_t k; (k = next.fetch_add(1)) < n;) {
|
||||
try {
|
||||
if (k == 0 && fault_mode().load() == 1) throw std::bad_alloc();
|
||||
item(k);
|
||||
} catch (...) {
|
||||
errors.fetch_add(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
unsigned hw = std::thread::hardware_concurrency();
|
||||
if (hw == 0) hw = 1;
|
||||
const unsigned threads = std::max(1u, std::min(max_threads ? max_threads : 1u, hw));
|
||||
std::vector<std::thread> pool;
|
||||
{
|
||||
Joiner joiner{pool};
|
||||
for (unsigned t = 1; t < threads && t < n; ++t) {
|
||||
try {
|
||||
if (t == 1 && fault_mode().load() == 2)
|
||||
throw std::system_error(
|
||||
std::make_error_code(std::errc::resource_unavailable_try_again));
|
||||
pool.emplace_back(work);
|
||||
} catch (...) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
work();
|
||||
}
|
||||
return errors.load();
|
||||
}
|
||||
|
||||
} // namespace psx_texpreload
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "texture_image_decode.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "../third_party/xxhash.h"
|
||||
#include "png_write.h"
|
||||
@@ -183,6 +184,7 @@ struct Decode {
|
||||
std::string path;
|
||||
std::shared_ptr<Image> image;
|
||||
uint64_t use = 0;
|
||||
bool pinned = false; /* preloaded: never evicted */
|
||||
};
|
||||
struct Dump { fs::path path; std::shared_ptr<Image> image; };
|
||||
struct DumpHistory { std::unordered_set<std::string> names; std::deque<std::string> order; };
|
||||
@@ -231,6 +233,7 @@ struct Worker {
|
||||
size_t dump_bytes = 0;
|
||||
uint64_t tick = 0;
|
||||
uint64_t decoded_images = 0, decode_evictions = 0;
|
||||
size_t pinned_count = 0; /* preloaded entries: outside the 512-entry cap */
|
||||
bool stop = false;
|
||||
bool dump_busy = false;
|
||||
std::string dump_error;
|
||||
@@ -243,7 +246,7 @@ struct Worker {
|
||||
while (used + incoming > budget) {
|
||||
auto candidate = decoded.end();
|
||||
for (auto it = decoded.begin(); it != decoded.end(); ++it)
|
||||
if (it->second.status == DecodeStatus::Ready &&
|
||||
if (it->second.status == DecodeStatus::Ready && !it->second.pinned &&
|
||||
(candidate == decoded.end() || it->second.use < candidate->second.use)) candidate = it;
|
||||
if (candidate == decoded.end()) break;
|
||||
used -= candidate->second.image->rgba.size();
|
||||
@@ -1141,11 +1144,11 @@ int duck_texture_pack_request_decode(DuckTexturePack* p, uint64_t id) {
|
||||
}
|
||||
if (!w.budget) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
if (w.decode_queue.size() >= kMaxDecodeQueue) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (w.decoded.size() >= 512) {
|
||||
if (w.decoded.size() - w.pinned_count >= 512) {
|
||||
auto victim = w.decoded.end();
|
||||
for (auto item = w.decoded.begin(); item != w.decoded.end(); ++item)
|
||||
if ((item->second.status == DecodeStatus::Ready || item->second.status == DecodeStatus::Failed) &&
|
||||
(victim == w.decoded.end() || item->second.use < victim->second.use)) victim = item;
|
||||
!item->second.pinned && (victim == w.decoded.end() || item->second.use < victim->second.use)) victim = item;
|
||||
if (victim == w.decoded.end()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (victim->second.image) w.used -= victim->second.image->rgba.size();
|
||||
w.decoded.erase(victim);
|
||||
@@ -1155,6 +1158,105 @@ int duck_texture_pack_request_decode(DuckTexturePack* p, uint64_t id) {
|
||||
w.wake.notify_one(); return HD_TEXTURE_LOOKUP_NONE;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
int duck_texture_pack_preload(DuckTexturePack* p, size_t max_bytes, unsigned max_threads,
|
||||
HdTexturePreloadResult* out) {
|
||||
HdTexturePreloadResult r{};
|
||||
if (out) *out = r;
|
||||
if (!p) return -1;
|
||||
try {
|
||||
namespace tp = psx_texpreload;
|
||||
Worker& w = p->worker;
|
||||
std::vector<uint64_t> ids;
|
||||
size_t per_image = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(w.mutex);
|
||||
per_image = w.budget;
|
||||
/* The cap and result describe the pack's total pinned residency,
|
||||
* including earlier successful calls or publication prefixes. */
|
||||
for (const auto& item : w.decoded)
|
||||
if (item.second.pinned && item.second.image)
|
||||
r.bytes += item.second.image->rgba.size();
|
||||
for (size_t i = 0; i < p->entries.size(); ++i) {
|
||||
if (p->entries[i].ambiguous) continue;
|
||||
++r.requested;
|
||||
const auto it = w.decoded.find(i + 1);
|
||||
if (it != w.decoded.end() && it->second.pinned) ++r.loaded;
|
||||
}
|
||||
if (r.bytes > max_bytes) {
|
||||
r.failed = r.requested - r.loaded;
|
||||
if (out) *out = r;
|
||||
return -1; /* A smaller cap cannot discard live pinned data. */
|
||||
}
|
||||
for (size_t i = 0; i < p->entries.size(); ++i) {
|
||||
if (p->entries[i].ambiguous) continue;
|
||||
const auto it = w.decoded.find(i + 1);
|
||||
if (it == w.decoded.end() || !it->second.pinned) ids.push_back(i + 1);
|
||||
}
|
||||
}
|
||||
std::vector<std::shared_ptr<Image>> images(ids.size());
|
||||
std::vector<unsigned char> status(ids.size(), tp::kPending);
|
||||
tp::Budget budget(max_bytes - r.bytes);
|
||||
r.worker_errors = static_cast<int>(tp::run(ids.size(), max_threads, [&](size_t k) {
|
||||
status[k] = tp::kFailed;
|
||||
const std::string& path = p->entries[ids[k] - 1].path;
|
||||
std::ifstream input(fs::u8path(path), std::ios::binary | std::ios::ate);
|
||||
const auto length = input ? input.tellg() : std::streampos(-1);
|
||||
if (length <= 0 || static_cast<uint64_t>(length) > kMaxEncoded) return;
|
||||
std::vector<uint8_t> encoded(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length)) return;
|
||||
TextureImageProbe probe; std::string error;
|
||||
if (!texture_image_probe(encoded.data(), encoded.size(), per_image, probe, error)) return;
|
||||
const size_t need = static_cast<size_t>(probe.width) * probe.height * 4u;
|
||||
if (!budget.reserve(need)) { status[k] = tp::kOverBudget; return; }
|
||||
struct Reservation {
|
||||
tp::Budget& b; size_t n; bool keep = false;
|
||||
~Reservation() { if (!keep) b.release(n); }
|
||||
} reservation{budget, need};
|
||||
DecodedTextureImage decoded;
|
||||
if (!texture_image_decode(encoded.data(), encoded.size(), need, decoded, error) ||
|
||||
decoded.rgba.size() != need) return;
|
||||
auto image = std::make_shared<Image>(); image->w = decoded.width; image->h = decoded.height;
|
||||
image->rgba = std::move(decoded.rgba);
|
||||
images[k] = std::move(image);
|
||||
reservation.keep = true;
|
||||
status[k] = tp::kLoaded;
|
||||
}));
|
||||
std::lock_guard<std::mutex> lock(w.mutex);
|
||||
for (size_t k = 0; k < ids.size(); ++k) {
|
||||
if (status[k] == tp::kOverBudget) { ++r.over_budget; continue; }
|
||||
if (status[k] != tp::kLoaded) continue;
|
||||
auto it = w.decoded.find(ids[k]);
|
||||
if (it != w.decoded.end()) {
|
||||
/* Already pinned, or an on-demand decode in flight: leave it. */
|
||||
if (it->second.pinned || it->second.status == DecodeStatus::Queued ||
|
||||
it->second.status == DecodeStatus::Loading) continue;
|
||||
}
|
||||
const size_t previous_bytes = it != w.decoded.end() && it->second.image
|
||||
? it->second.image->rgba.size() : 0;
|
||||
Decode entry;
|
||||
/* Prepare throwing metadata before touching an existing entry. */
|
||||
entry.path = p->entries[ids[k] - 1].path;
|
||||
entry.image = std::move(images[k]); entry.status = DecodeStatus::Ready;
|
||||
entry.pinned = true; entry.use = ++w.tick;
|
||||
const size_t bytes = entry.image->rgba.size();
|
||||
if (it == w.decoded.end()) w.decoded.emplace(ids[k], std::move(entry));
|
||||
else it->second = std::move(entry);
|
||||
w.used -= previous_bytes;
|
||||
r.bytes += bytes;
|
||||
++w.pinned_count; ++r.loaded;
|
||||
++w.decoded_images;
|
||||
tp::after_publish(r.loaded);
|
||||
}
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r;
|
||||
return r.loaded == r.requested ? 1 : 0;
|
||||
} catch (...) {
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r; /* Account the pinned prefix even on publication failure. */
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* p, uint64_t id, DuckTexturePixels* out) {
|
||||
if (out) *out = {};
|
||||
if (!p || !out) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -25,6 +26,11 @@ constexpr size_t kCompositionBudget = 64u * 1024u * 1024u;
|
||||
constexpr size_t kCompositionCount = 256;
|
||||
constexpr uint32_t kCompositionDimension = 8192;
|
||||
constexpr size_t kMaxParts = 64;
|
||||
/* Elective full preload of an installed pack: at most this much decoded RGBA
|
||||
* pinned across both formats, decoded by at most this many threads. Anything
|
||||
* beyond decodes on demand under the normal budget. */
|
||||
constexpr size_t kPreloadMaxBytes = size_t{512} * 1024u * 1024u;
|
||||
constexpr unsigned kPreloadThreads = 4;
|
||||
struct Composition {
|
||||
std::vector<uint64_t> signature;
|
||||
std::vector<uint8_t> rgba;
|
||||
@@ -289,6 +295,32 @@ extern "C" int gpu_hd_textures_configure(const char* root, int replacements,
|
||||
std::filesystem::create_directories(directory / "dumps");
|
||||
}
|
||||
if (!duck_texture_pack_create(root, &next->duck, error, capacity)) return 0;
|
||||
if (next->replacements) {
|
||||
/* Replacements decoded and resident before the game draws, within
|
||||
* kPreloadMaxBytes; the rest stays on the budgeted async path. */
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
HdTexturePreloadResult dr{}, br{};
|
||||
const int dstatus = duck_texture_pack_preload(next->duck, kPreloadMaxBytes,
|
||||
kPreloadThreads, &dr);
|
||||
int bstatus = 1;
|
||||
if (next->beetle) {
|
||||
const size_t left = dr.bytes < kPreloadMaxBytes ? kPreloadMaxBytes - dr.bytes : 0;
|
||||
bstatus = hd_texture_pack_preload(next->beetle, left, kPreloadThreads, &br);
|
||||
}
|
||||
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
const int requested = dr.requested + br.requested;
|
||||
if (requested > 0 || dstatus < 0 || bstatus < 0)
|
||||
std::fprintf(stdout, "psxrecomp: texture pack: preloaded %d/%d images (%.1f MiB) in %lld ms"
|
||||
"%s; %d over the %u MiB preload cap, %d failed (%d worker errors);"
|
||||
" the rest decode on demand\n",
|
||||
dr.loaded + br.loaded, requested, (dr.bytes + br.bytes) / 1048576.0,
|
||||
static_cast<long long>(ms),
|
||||
(dstatus < 0 || bstatus < 0) ? " (preload error)" :
|
||||
(dstatus == 1 && bstatus == 1) ? " (complete)" : " (partial)",
|
||||
dr.over_budget + br.over_budget, static_cast<unsigned>(kPreloadMaxBytes / (1024u * 1024u)),
|
||||
dr.failed + br.failed, dr.worker_errors + br.worker_errors);
|
||||
}
|
||||
/* Quiesce renderer users (GL render thread: dump_draw appends to the
|
||||
* old session's sources) before flushing its dumps or reading its
|
||||
* residency; the cache is rebuilt from the new session. */
|
||||
|
||||
+155
-30
@@ -1,10 +1,12 @@
|
||||
#include "hd_texture_pack.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
|
||||
#ifndef HD_TEXTURE_PACK_DISABLE_PNG_DECODE
|
||||
#include "../third_party/stb_image.h" /* declarations only; implementation is shared */
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <cctype>
|
||||
@@ -452,6 +454,7 @@ struct DecodeItem {
|
||||
std::shared_ptr<DecodedImage> image;
|
||||
size_t bytes = 0;
|
||||
uint64_t last_used = 0;
|
||||
bool pinned = false; /* preloaded: never evicted */
|
||||
};
|
||||
|
||||
struct DecodeCache {
|
||||
@@ -484,7 +487,7 @@ void evict_decode_cache(DecodeCache& cache, size_t incoming = 0) {
|
||||
while (cache.used + incoming > cache.budget) {
|
||||
auto victim = cache.items.end();
|
||||
for (auto it = cache.items.begin(); it != cache.items.end(); ++it) {
|
||||
if (it->second.state != DecodeState::Ready) continue;
|
||||
if (it->second.state != DecodeState::Ready || it->second.pinned) continue;
|
||||
if (victim == cache.items.end() ||
|
||||
it->second.last_used < victim->second.last_used)
|
||||
victim = it;
|
||||
@@ -515,38 +518,46 @@ void decode_worker(DecodeCache* cache) {
|
||||
path = it->second.path;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encoded;
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
||||
std::streamoff length = -1;
|
||||
if (input) length = static_cast<std::streamoff>(input.tellg());
|
||||
if (length > 0 && static_cast<uint64_t>(length) <= kMaxPngFileBytes &&
|
||||
static_cast<uint64_t>(length) <=
|
||||
static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
||||
encoded.resize(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length))
|
||||
encoded.clear();
|
||||
}
|
||||
}
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int components = 0;
|
||||
stbi_uc* loaded = encoded.empty() ? nullptr : stbi_load_from_memory(
|
||||
encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &components, 4);
|
||||
std::shared_ptr<DecodedImage> decoded;
|
||||
size_t bytes = 0;
|
||||
if (loaded && width > 0 && height > 0 &&
|
||||
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) <=
|
||||
std::numeric_limits<size_t>::max() / 4) {
|
||||
bytes = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
||||
decoded = std::make_shared<DecodedImage>();
|
||||
decoded->width = static_cast<uint32_t>(width);
|
||||
decoded->height = static_cast<uint32_t>(height);
|
||||
decoded->rgba.assign(loaded, loaded + bytes);
|
||||
/* Allocation failure (encoded buffer, RGBA copy) must not escape the
|
||||
* thread entry: the item fails like an undecodable image. */
|
||||
try {
|
||||
std::vector<uint8_t> encoded;
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
||||
std::streamoff length = -1;
|
||||
if (input) length = static_cast<std::streamoff>(input.tellg());
|
||||
if (length > 0 && static_cast<uint64_t>(length) <= kMaxPngFileBytes &&
|
||||
static_cast<uint64_t>(length) <=
|
||||
static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
||||
encoded.resize(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length))
|
||||
encoded.clear();
|
||||
}
|
||||
}
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int components = 0;
|
||||
std::unique_ptr<stbi_uc, void (*)(void*)> loaded(
|
||||
encoded.empty() ? nullptr : stbi_load_from_memory(
|
||||
encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &components, 4),
|
||||
stbi_image_free);
|
||||
if (loaded && width > 0 && height > 0 &&
|
||||
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) <=
|
||||
std::numeric_limits<size_t>::max() / 4) {
|
||||
bytes = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
||||
decoded = std::make_shared<DecodedImage>();
|
||||
decoded->width = static_cast<uint32_t>(width);
|
||||
decoded->height = static_cast<uint32_t>(height);
|
||||
decoded->rgba.assign(loaded.get(), loaded.get() + bytes);
|
||||
}
|
||||
} catch (...) {
|
||||
decoded.reset();
|
||||
bytes = 0;
|
||||
}
|
||||
if (loaded) stbi_image_free(loaded);
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache->mutex);
|
||||
auto it = cache->items.find(key);
|
||||
@@ -1317,6 +1328,120 @@ void hd_texture_pack_set_decode_budget(HdTexturePack* pack,
|
||||
evict_decode_cache(pack->decode);
|
||||
}
|
||||
|
||||
int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes, unsigned max_threads,
|
||||
HdTexturePreloadResult* out) {
|
||||
HdTexturePreloadResult r{};
|
||||
if (out) *out = r;
|
||||
#ifdef HD_TEXTURE_PACK_DISABLE_PNG_DECODE
|
||||
(void)pack; (void)max_bytes; (void)max_threads;
|
||||
return -1;
|
||||
#else
|
||||
if (!pack) return -1;
|
||||
try {
|
||||
namespace tp = psx_texpreload;
|
||||
std::vector<std::pair<uint64_t, std::string>> work_items;
|
||||
size_t per_image = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pack->decode.mutex);
|
||||
per_image = pack->decode.budget;
|
||||
for (const auto& item : pack->decode.items)
|
||||
if (item.second.pinned) r.bytes += item.second.bytes;
|
||||
for (const auto& entry : pack->entries) {
|
||||
if (entry.second.ambiguous) continue;
|
||||
++r.requested;
|
||||
const auto it = pack->decode.items.find(entry.first);
|
||||
if (it != pack->decode.items.end() && it->second.pinned) ++r.loaded;
|
||||
}
|
||||
if (r.bytes > max_bytes) {
|
||||
r.failed = r.requested - r.loaded;
|
||||
if (out) *out = r;
|
||||
return -1;
|
||||
}
|
||||
for (const auto& entry : pack->entries) {
|
||||
if (entry.second.ambiguous) continue;
|
||||
const auto it = pack->decode.items.find(entry.first);
|
||||
if (it == pack->decode.items.end() || !it->second.pinned)
|
||||
work_items.emplace_back(entry.first, entry.second.replacement_path);
|
||||
}
|
||||
}
|
||||
std::vector<std::shared_ptr<DecodedImage>> images(work_items.size());
|
||||
std::vector<unsigned char> status(work_items.size(), tp::kPending);
|
||||
tp::Budget budget(max_bytes - r.bytes);
|
||||
r.worker_errors = static_cast<int>(tp::run(work_items.size(), max_threads, [&](size_t k) {
|
||||
status[k] = tp::kFailed;
|
||||
std::ifstream input(work_items[k].second, std::ios::binary | std::ios::ate);
|
||||
const std::streamoff length = input ? static_cast<std::streamoff>(input.tellg()) : -1;
|
||||
if (length <= 0 || static_cast<uint64_t>(length) > kMaxPngFileBytes ||
|
||||
static_cast<uint64_t>(length) > static_cast<uint64_t>(std::numeric_limits<int>::max()))
|
||||
return;
|
||||
std::vector<uint8_t> encoded(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length)) return;
|
||||
int width = 0, height = 0, components = 0;
|
||||
if (!stbi_info_from_memory(encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &components) ||
|
||||
width <= 0 || height <= 0 ||
|
||||
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) >
|
||||
static_cast<uint64_t>(per_image) / 4)
|
||||
return;
|
||||
const size_t need = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
||||
if (!budget.reserve(need)) { status[k] = tp::kOverBudget; return; }
|
||||
struct Reservation {
|
||||
tp::Budget& b; size_t n; bool keep = false;
|
||||
~Reservation() { if (!keep) b.release(n); }
|
||||
} reservation{budget, need};
|
||||
int lw = 0, lh = 0, lc = 0;
|
||||
std::unique_ptr<stbi_uc, void (*)(void*)> loaded(
|
||||
stbi_load_from_memory(encoded.data(), static_cast<int>(encoded.size()),
|
||||
&lw, &lh, &lc, 4),
|
||||
stbi_image_free);
|
||||
if (!loaded || lw != width || lh != height) return;
|
||||
auto image = std::make_shared<DecodedImage>();
|
||||
image->width = static_cast<uint32_t>(width);
|
||||
image->height = static_cast<uint32_t>(height);
|
||||
image->rgba.assign(loaded.get(), loaded.get() + need);
|
||||
images[k] = std::move(image);
|
||||
reservation.keep = true;
|
||||
status[k] = tp::kLoaded;
|
||||
}));
|
||||
std::lock_guard<std::mutex> lock(pack->decode.mutex);
|
||||
for (size_t k = 0; k < work_items.size(); ++k) {
|
||||
if (status[k] == tp::kOverBudget) { ++r.over_budget; continue; }
|
||||
if (status[k] != tp::kLoaded) continue;
|
||||
auto it = pack->decode.items.find(work_items[k].first);
|
||||
if (it != pack->decode.items.end()) {
|
||||
/* Already pinned, or an on-demand decode in flight: leave it. */
|
||||
if (it->second.pinned || it->second.state == DecodeState::Queued ||
|
||||
it->second.state == DecodeState::Loading) continue;
|
||||
}
|
||||
const size_t previous_bytes = it != pack->decode.items.end() &&
|
||||
it->second.state == DecodeState::Ready ? it->second.bytes : 0;
|
||||
DecodeItem item;
|
||||
/* Prepare throwing metadata before touching an existing entry. */
|
||||
item.path = work_items[k].second;
|
||||
item.bytes = images[k]->rgba.size();
|
||||
item.image = std::move(images[k]);
|
||||
item.state = DecodeState::Ready; item.pinned = true;
|
||||
item.last_used = ++pack->decode.tick;
|
||||
const size_t bytes = item.bytes;
|
||||
if (it == pack->decode.items.end())
|
||||
pack->decode.items.emplace(work_items[k].first, std::move(item));
|
||||
else it->second = std::move(item);
|
||||
pack->decode.used -= previous_bytes;
|
||||
r.bytes += bytes; ++r.loaded;
|
||||
tp::after_publish(r.loaded);
|
||||
}
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r;
|
||||
return r.loaded == r.requested ? 1 : 0;
|
||||
} catch (...) {
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r; /* Account the pinned prefix even on publication failure. */
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int hd_texture_pack_request_decode(HdTexturePack* pack,
|
||||
uint32_t texture_hash,
|
||||
uint32_t palette_hash) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
#include <stdexcept>
|
||||
#include <atomic>
|
||||
#include "../src/png_write.h"
|
||||
|
||||
#include <array>
|
||||
@@ -109,6 +112,98 @@ bool decode_fails(DuckTexturePack* pack, uint64_t id) {
|
||||
return status == HD_TEXTURE_LOOKUP_ERROR;
|
||||
}
|
||||
|
||||
void preload(const fs::path& root) {
|
||||
namespace tp = psx_texpreload;
|
||||
const char* name = "texpage-P4-3631D2A04EC85311-9CBCDEA9305D7528-64x256-4-2-8x2-P0-15.png";
|
||||
png(root/"replacements"/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);
|
||||
HdTexturePreloadResult r{};
|
||||
check(duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r) == 1 && r.loaded == 1 &&
|
||||
r.requested == 1 && r.bytes == 16*4*4 && r.failed == 0,"preload decodes the pack");
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"preloaded fixture matches");
|
||||
DuckTexturePixels pixels{};
|
||||
check(duck_texture_pack_acquire_decoded(pack.get(),match.entry_id,&pixels) == HD_TEXTURE_LOOKUP_FOUND &&
|
||||
pixels.width == 16,"preloaded image is resident without a decode request");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
duck_texture_pack_set_decode_budget(pack.get(),1);
|
||||
check(duck_texture_pack_preload(pack.get(),256,1,&r) == 1 && r.loaded == 1 &&
|
||||
r.failed == 0 && r.bytes == 256 && r.worker_errors == 0,
|
||||
"repeat counts existing residency without decoding under the reduced budget");
|
||||
check(duck_texture_pack_preload(pack.get(),255,1,&r) == -1 && r.bytes == 256,
|
||||
"a smaller lifetime cap rejects without hiding pinned residency");
|
||||
check(duck_texture_pack_acquire_decoded(pack.get(),match.entry_id,&pixels) == HD_TEXTURE_LOOKUP_FOUND,
|
||||
"a preloaded image survives budget eviction");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
}
|
||||
{ /* Over the aggregate cap: partial, nothing pinned, async path still decodes. */
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
HdTexturePreloadResult r{};
|
||||
check(duck_texture_pack_preload(pack.get(),16,4,&r) == 0 && r.loaded == 0 && r.over_budget == 1 &&
|
||||
r.bytes == 0,"oversized image is skipped by the preload cap");
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"capped fixture matches");
|
||||
DuckTexturePixels pixels = decoded(pack.get(),match.entry_id);
|
||||
check(pixels.width == 16,"an image the preload skipped decodes on demand");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
}
|
||||
{ /* Injected worker failure: caught, reported, image left to the async path. */
|
||||
auto pack = create(root);
|
||||
tp::fault_mode() = 1;
|
||||
HdTexturePreloadResult r{};
|
||||
const int status = duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r);
|
||||
tp::fault_mode() = 0;
|
||||
check(status == 0 && r.worker_errors == 1 && r.failed == 1 && r.loaded == 0,
|
||||
"a throwing decode is caught and reported as a partial preload");
|
||||
}
|
||||
{ /* Publication can fail after retaining a pinned prefix. */
|
||||
auto pack = create(root);
|
||||
tp::fault_mode() = 3;
|
||||
HdTexturePreloadResult r{};
|
||||
const int status = duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r);
|
||||
tp::fault_mode() = 0;
|
||||
check(status == -1 && r.loaded == 1 && r.bytes == 16*4*4,
|
||||
"publication failure still reports every retained byte");
|
||||
check(duck_texture_pack_preload(pack.get(),256,1,&r) == 1 &&
|
||||
r.loaded == 1 && r.failed == 0 && r.bytes == 256,
|
||||
"publication retry accounts the retained prefix without decoding again");
|
||||
}
|
||||
{ /* Malformed image: failed, not pinned, async decode reports failure. */
|
||||
std::ofstream(root/"replacements"/name,std::ios::binary|std::ios::trunc) << "not a png";
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
HdTexturePreloadResult r{};
|
||||
check(duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r) == 0 && r.failed == 1 &&
|
||||
r.worker_errors == 0,"malformed image fails the preload item");
|
||||
DuckTextureMatch match{};
|
||||
if (duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND)
|
||||
check(decode_fails(pack.get(),match.entry_id),"malformed image still fails on demand");
|
||||
}
|
||||
}
|
||||
|
||||
void preload_pool() {
|
||||
namespace tp = psx_texpreload;
|
||||
std::atomic<int> ran{0};
|
||||
tp::fault_mode() = 2; /* the second thread cannot be created */
|
||||
size_t errors = tp::run(32,8,[&](size_t){ ++ran; });
|
||||
check(errors == 0 && ran == 32,"thread creation failure lowers concurrency, all items still run");
|
||||
tp::fault_mode() = 1; /* the first item throws bad_alloc */
|
||||
ran = 0;
|
||||
errors = tp::run(32,8,[&](size_t){ ++ran; });
|
||||
tp::fault_mode() = 0;
|
||||
check(errors == 1 && ran == 31,"a throwing item is caught and counted");
|
||||
ran = 0;
|
||||
errors = tp::run(32,8,[&](size_t k){ if (k % 4 == 0) throw std::runtime_error("x"); ++ran; });
|
||||
check(errors == 8 && ran == 24,"item exceptions never escape a pool thread");
|
||||
tp::Budget budget(100);
|
||||
check(budget.reserve(60) && !budget.reserve(41) && budget.reserve(40) && !budget.reserve(1),
|
||||
"aggregate budget reservations are bounded");
|
||||
budget.release(50);
|
||||
check(budget.reserve(50),"released bytes are reusable");
|
||||
}
|
||||
|
||||
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. */
|
||||
@@ -624,7 +719,8 @@ int main(int argc, char** argv) {
|
||||
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"); release_while_another_source_splits(root/"release-split");
|
||||
resident_checkpoint(root/"resident"); preload(root/"preload"); preload_pool();
|
||||
release_while_another_source_splits(root/"release-split");
|
||||
/* 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());
|
||||
|
||||
@@ -425,7 +425,7 @@ static void composition_scene(const char* root) {
|
||||
}
|
||||
}
|
||||
|
||||
static void cache_eviction_scene(const char* root) {
|
||||
static void cache_pressure_scene(const char* root) {
|
||||
uint16_t pressure_words[16]; for(int i=0;i<16;++i) pressure_words[i]=0x1234;
|
||||
DuckTextureKey key={0}; key.kind=DUCK_TEXTURE_UPLOAD;key.depth=HD_TEXTURE_DEPTH_16BPP;
|
||||
key.source_hash=duck_texture_hash_words_le(pressure_words,16);
|
||||
@@ -446,7 +446,7 @@ static void cache_eviction_scene(const char* root) {
|
||||
composed_key=image.cache_key;
|
||||
gpu_hd_textures_release_image(&image);if(!composed_key) SDL_Delay(1);
|
||||
}
|
||||
check(composed_key!=0,"complete composition ready before decoder eviction");
|
||||
check(composed_key!=0,"complete composition ready before cache pressure");
|
||||
gr_fill_rect(0,0,16,16,0x03e0);gr_draw_textured_rect(2,2,4,4,0,0,0,0,texture_page);capture();
|
||||
GpuHdTextureDiag before,after;gpu_hd_textures_get_diag(&before);
|
||||
gr_vram_transfer_in(640,64,16,1,pressure_words);int ready=0;
|
||||
@@ -455,17 +455,24 @@ static void cache_eviction_scene(const char* root) {
|
||||
gpu_hd_textures_release_image(&image);if(!ready) SDL_Delay(1);
|
||||
}
|
||||
check(ready,"full CPU budget pressure image decoded");gpu_hd_textures_get_diag(&after);
|
||||
check(after.decoded_bytes==1024*1024 && after.decode_evictions>before.decode_evictions,
|
||||
"pressure image evicts original decoded replacements");
|
||||
/* All fixture images fit in the bounded preload and remain pinned.
|
||||
* The configured 1 MiB on-demand budget must not evict that preload.
|
||||
* decoded_bytes accounts only the on-demand cache; pinned bytes are
|
||||
* separately bounded and are not charged against this budget. */
|
||||
check(before.decoded_images>=7 && before.decoded_bytes==0 &&
|
||||
after.decoded_bytes==before.decoded_bytes &&
|
||||
after.decoded_images==before.decoded_images &&
|
||||
after.decode_evictions==before.decode_evictions,
|
||||
"cache pressure retains pinned preloaded replacements");
|
||||
check(gpu_hd_textures_acquire_gl_draw(texture_page,0,0,bounds,0,0,&image) && !image.rgba,
|
||||
"GPU-resident replacement remains ready after CPU eviction");gpu_hd_textures_release_image(&image);
|
||||
"GPU-resident replacement remains ready under cache pressure");gpu_hd_textures_release_image(&image);
|
||||
check(gpu_hd_textures_acquire_draw(tp,0,0,parts,0,1,&image) && image.cache_key==composed_key,
|
||||
"complete composition survives eviction of all source PNGs");gpu_hd_textures_release_image(&image);
|
||||
"complete composition remains warm under cache pressure");gpu_hd_textures_release_image(&image);
|
||||
gr_draw_textured_rect(2,2,4,4,0,0,0,0,texture_page);capture();
|
||||
check(sample(8,8)[0]>200,"GPU-resident replacement still draws original HD detail");
|
||||
SDL_Delay(20);gpu_hd_textures_get_diag(&before);
|
||||
check(before.decoded_images==after.decoded_images && before.gl_uploads==after.gl_uploads,
|
||||
"warm draws do not decode or upload again after CPU eviction");
|
||||
"warm draws do not decode or upload again under cache pressure");
|
||||
pack_config(root,0);check(gpu_hd_textures_reload(error,sizeof(error)),"eviction fixture cleanup reload");
|
||||
state();gr_vram_transfer_in(512,0,4,4,source_words);wait_ready(0);wait_ready(1);
|
||||
}
|
||||
@@ -571,7 +578,7 @@ int main(int argc,char** argv) {
|
||||
"ST black alpha127 remains occupied semitransparent black");
|
||||
gr_set_semi_transparency(0,0);
|
||||
composition_scene(argv[1]);state();wait_ready(0);wait_ready(1);
|
||||
cache_eviction_scene(argv[1]);
|
||||
cache_pressure_scene(argv[1]);
|
||||
|
||||
/* A queued draw must consume its source before a later source overwrite.
|
||||
* The next draw falls back because upload identity has been invalidated. */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
@@ -530,6 +531,76 @@ void test_bounded_async_decode(Fixture& fixture) {
|
||||
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() {
|
||||
@@ -542,6 +613,7 @@ int main() {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user