From c3ff07814cfa722c393fb26c10e27d1d1c4ac2bd Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 08:31:16 -0700 Subject: [PATCH] Account existing pinned textures across preload retries 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. --- runtime/include/hd_texture_pack.h | 9 +++-- runtime/src/duckstation_texture_pack.cpp | 33 +++++++++++++++---- runtime/src/hd_texture_pack.cpp | 25 +++++++++++--- .../tests/test_duckstation_texture_pack.cpp | 8 +++++ runtime/tests/test_hd_texture_pack.cpp | 11 +++++++ 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/runtime/include/hd_texture_pack.h b/runtime/include/hd_texture_pack.h index 165adb63..25591505 100644 --- a/runtime/include/hd_texture_pack.h +++ b/runtime/include/hd_texture_pack.h @@ -196,14 +196,17 @@ void hd_texture_pack_set_decode_budget(HdTexturePack* pack, * 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. */ + * 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; /* decoded and pinned */ + 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; /* decoded RGBA pinned */ + 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); diff --git a/runtime/src/duckstation_texture_pack.cpp b/runtime/src/duckstation_texture_pack.cpp index 2c93fa18..3ca48b1b 100644 --- a/runtime/src/duckstation_texture_pack.cpp +++ b/runtime/src/duckstation_texture_pack.cpp @@ -1165,17 +1165,37 @@ int duck_texture_pack_preload(DuckTexturePack* p, size_t max_bytes, unsigned max if (!p) return -1; try { namespace tp = psx_texpreload; + Worker& w = p->worker; std::vector ids; - for (size_t i = 0; i < p->entries.size(); ++i) - if (!p->entries[i].ambiguous) ids.push_back(i + 1); size_t per_image = 0; { - std::lock_guard lock(p->worker.mutex); - per_image = p->worker.budget; + std::lock_guard 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> images(ids.size()); std::vector status(ids.size(), tp::kPending); - tp::Budget budget(max_bytes); + tp::Budget budget(max_bytes - r.bytes); r.worker_errors = static_cast(tp::run(ids.size(), max_threads, [&](size_t k) { status[k] = tp::kFailed; const std::string& path = p->entries[ids[k] - 1].path; @@ -1202,8 +1222,7 @@ int duck_texture_pack_preload(DuckTexturePack* p, size_t max_bytes, unsigned max reservation.keep = true; status[k] = tp::kLoaded; })); - r.requested = static_cast(ids.size()); - Worker& w = p->worker; std::lock_guard lock(w.mutex); + std::lock_guard 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; diff --git a/runtime/src/hd_texture_pack.cpp b/runtime/src/hd_texture_pack.cpp index a27c8747..10e2af2a 100644 --- a/runtime/src/hd_texture_pack.cpp +++ b/runtime/src/hd_texture_pack.cpp @@ -1340,17 +1340,33 @@ int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes, unsigned max_ try { namespace tp = psx_texpreload; std::vector> work_items; - for (const auto& entry : pack->entries) - if (!entry.second.ambiguous) - work_items.emplace_back(entry.first, entry.second.replacement_path); size_t per_image = 0; { std::lock_guard 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> images(work_items.size()); std::vector status(work_items.size(), tp::kPending); - tp::Budget budget(max_bytes); + tp::Budget budget(max_bytes - r.bytes); r.worker_errors = static_cast(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); @@ -1388,7 +1404,6 @@ int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes, unsigned max_ reservation.keep = true; status[k] = tp::kLoaded; })); - r.requested = static_cast(work_items.size()); std::lock_guard lock(pack->decode.mutex); for (size_t k = 0; k < work_items.size(); ++k) { if (status[k] == tp::kOverBudget) { ++r.over_budget; continue; } diff --git a/runtime/tests/test_duckstation_texture_pack.cpp b/runtime/tests/test_duckstation_texture_pack.cpp index 1a4b6019..52eb9b24 100644 --- a/runtime/tests/test_duckstation_texture_pack.cpp +++ b/runtime/tests/test_duckstation_texture_pack.cpp @@ -130,6 +130,11 @@ void preload(const fs::path& root) { 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); @@ -162,6 +167,9 @@ void preload(const fs::path& root) { 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"; diff --git a/runtime/tests/test_hd_texture_pack.cpp b/runtime/tests/test_hd_texture_pack.cpp index 15e09347..79c867eb 100644 --- a/runtime/tests/test_hd_texture_pack.cpp +++ b/runtime/tests/test_hd_texture_pack.cpp @@ -545,6 +545,14 @@ void test_bounded_preload(Fixture& fixture) { (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, @@ -586,6 +594,9 @@ void test_bounded_preload(Fixture& fixture) { 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); } }