Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions runtime/include/duckstation_texture_pack.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions runtime/include/hd_texture_pack.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
100 changes: 100 additions & 0 deletions runtime/include/texture_pack_preload.hpp
Original file line number Diff line number Diff line change
@@ -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
108 changes: 105 additions & 3 deletions runtime/src/duckstation_texture_pack.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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; };
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions runtime/src/gpu_hd_textures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <array>
#include <cmath>
#include <vector>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
Expand All @@ -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;
Expand Down Expand Up @@ -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. */
Expand Down
Loading
Loading