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); } }