Merge branch 'review/736-preload-repeat-20261024' into review/psx-prs-20261024

# Conflicts:
#	runtime/include/hd_texture_pack.h
#	runtime/src/duckstation_texture_pack.cpp
#	runtime/src/hd_texture_pack.cpp
#	runtime/tests/test_duckstation_texture_pack.cpp
#	runtime/tests/test_hd_texture_pack.cpp
This commit is contained in:
Matthew Stanley
2026-10-24 08:33:10 -07:00
5 changed files with 71 additions and 15 deletions
+6 -3
View File
@@ -187,14 +187,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);
+26 -7
View File
@@ -1142,17 +1142,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<uint64_t> 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<std::mutex> lock(p->worker.mutex);
per_image = p->worker.budget;
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);
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;
@@ -1179,8 +1199,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<int>(ids.size());
Worker& w = p->worker; std::lock_guard<std::mutex> lock(w.mutex);
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;
+20 -5
View File
@@ -1305,17 +1305,33 @@ int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes, unsigned max_
try {
namespace tp = psx_texpreload;
std::vector<std::pair<uint64_t, std::string>> 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<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);
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);
@@ -1353,7 +1369,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<int>(work_items.size());
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; }
@@ -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";
+11
View File
@@ -527,6 +527,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,
@@ -568,6 +576,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);
}
}