Skip to content

Preload HD replacements within a bounded startup budget - #736

Merged
mstan merged 6 commits into
masterfrom
feat/hd-preload
Oct 24, 2026
Merged

mstan merged 6 commits into
masterfrom
feat/hd-preload

Conversation

@tetrisgm

@tetrisgm tetrisgm commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Decode selected HD texture replacements before gameplay with at most four workers and a shared 512 MiB decoded-RGBA preload cap. Pin successfully preloaded images separately from the on-demand decode cache; images outside the cap continue through the existing asynchronous path.

Repeated preload calls account for existing pinned residency without decoding it again. A smaller cap rejects safely while retaining live image leases, and partial publication failures report the resident prefix accurately.

Validation: both texture-pack unit suites pass, including repeat/cap/error/lease checks. The combined real NVIDIA GL fixture passes 209 checks with high-resolution windows off, 208 with them on, and the 19-check native reference case. Its cache-pressure assertions now verify pinned retention while on-demand eviction coverage remains in the pack tests. Large external-pack startup/memory measurements are not claimed.

@mstan mstan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full preload can be a useful elective texture-pack mode, but this head cannot merge safely. Both preload functions bypass the decode budget and pin the whole pack; DuckStation passes SIZE_MAX to the decoder and then grows the budget to usage. A pack can therefore exhaust RAM before gameplay, with up to 16 concurrent decodes. There are also process-termination paths: failure while constructing a later std::thread destroys earlier joinable threads; the Beetle worker allows allocation exceptions to escape its thread entry. An outer catch cannot recover either case. Please provide a bounded preload contract (aggregate decoded bytes and bounded concurrency), RAII joining on partial thread creation, caught worker failures, and an explicit failed/partial-load result rather than silently treating skipped images as successful preload. Reuse the existing asynchronous/budgeted fallback when preload cannot complete. Cover both formats, malformed/oversized images and injected worker/thread failure. Installing a pack is already elective; that does not make an unbounded crash path acceptable. Coordinate with #717 rather than independently bypassing its cache policy. Tracking: beads-ek8k.

@tetrisgm

Copy link
Copy Markdown
Member Author

Addressed in 4d76bed: preload now takes an aggregate decoded-byte cap and a thread cap (runtime uses 512 MiB / 4). Each image is probed and reserved against the cap before decode; the decode budget is no longer grown, and pinned images sit outside it. The pool joins every started thread on unwind, tolerates thread-creation failure, and catches worker exceptions. The Beetle async worker no longer lets bad_alloc escape. Images not preloaded stay on the async budgeted path (no longer marked Failed), and HdTexturePreloadResult reports loaded / over-cap / failed / worker errors with complete (1) / partial (0) / error (-1). Tests cover both formats: over-cap fallback, malformed images, injected worker and thread failures. Coordination with #717's cache policy is still open.

@mstan

mstan commented Oct 22, 2026

Copy link
Copy Markdown
Member

Re-reviewed 4d76bed. The aggregate reservation, four-worker limit, RAII joins, explicit partial results and async fallback address most of the original feedback. Two boundaries still need attention before merge. In both preload functions the publication loop can throw after earlier entries have been pinned, but the catch returns -1 with out still zero. gpu_hd_textures_configure then gives the second format the entire 512 MiB cap because dr.bytes is zero, even though the first format already owns a pinned prefix. Keep publication transactional or report/account every successfully published byte on all error exits; cover a failure after one successful publication. Also settle the cache/preload policy with #717: the runtime currently starts this pinned preload automatically for every enabled replacement pack, with an additional pool outside the existing cache budget. Document why that is the intended elective preload contract rather than a separate overlapping cache policy. I am retaining the hold while that policy and failure contract are resolved. These are code-review findings; the new head has not been included in the running game batch. Tracking: beads-ek8k.

@mstan

mstan commented Oct 23, 2026

Copy link
Copy Markdown
Member

I fixed the objective publication-error bug directly in 25f453a. Throwing metadata is prepared before replacing a cache entry; every pinned prefix byte is returned on error, and DuckStation counters update per successful publication. Injected failure after the first pinned image passes for both formats, including retained-byte accounting. The combined native pack fixtures also pass.

The remaining hold is the elective residency policy: selecting a pack currently starts an additional pinned pool of up to 512 MiB outside the existing decode cache. Please settle/document one bounded residency contract with #717: explain why automatic preload is the intended behavior for every selected pack, or offer it as a pack performance option. Also define whether the aggregate cap covers a pack's lifetime or one invocation, and qualify repeated preload/reset behavior against already pinned entries. Failure/budget correctness must remain unconditional; an opt-in switch is not a substitute for fixing those paths. Tracking: beads-kgof.

@mstan mstan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed 8a6217f. Repeat preload calls now account for already pinned residency, preserve live leases when rejecting a smaller cap, avoid redundant decode, and accurately report the published prefix after failures. Updated the hardware cache-pressure fixture to verify pinned retention separately from the on-demand budget; the pack tests retain actual on-demand eviction coverage.

Both pack suites and the combined real GL HD cases pass (209/208 checks plus native reference 19). The PR description now reflects the four-worker/shared-512-MiB cap and asynchronous fallback instead of promising an unlimited whole-pack preload.

@mstan mstan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cumulative pinned preload accounting, retry/cap handling and publication-error prefix accounting are repaired. The bounded four-worker/shared-512-MiB startup contract and on-demand remainder are reflected in the title/description. Pinned-residency and on-demand eviction expectations are separately covered.

The exact head is included in review/psx-prs-20261024 (b788fda). The selected Tomba!, Mega Man X6 and Ape Escape baseline/candidate builds pass gameplay and current-build save/load; original cards are unchanged. Capped runtime checks show no material frame-rate regression in the tested scenes. The owner has now confirmed all three title windows look fine.

Scope: Tomba/MMX6 use documented compatible remote ancestors because their latest feature heads also fail on frozen master due missing engine APIs; this is not qualification of those latest feature heads or of real multiplayer, whole-game content, external R4 assets, peak throughput, or physical fullscreen compositor behavior. Separate held PRs/drafts retain their own feature-contract questions. No merge is performed by this review.

tetrisgm and others added 6 commits October 24, 2026 12:35
Replacement images were decoded on demand by a single low-priority
worker, so a texture first seen mid-race appeared late and could be
evicted again. When replacements are on, configuring a pack now decodes
every unambiguous image (DuckStation and Beetle formats) across all
cores and pins it in RAM before the game draws; pinned images are never
evicted by the decode budget. Logs the count, size and time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both preload functions now take an aggregate decoded-byte cap and a thread
cap (runtime: 512 MiB, 4 threads). Each image is probed and its RGBA
reserved against the cap before decoding; per-image limits follow the
pack's decode budget, which is no longer grown to usage (pinned images sit
outside it, and outside the Duck 512-entry cap). The pool (texture_pack_
preload.hpp) joins every started thread on unwinding, survives thread
creation failure, and catches worker exceptions. Images not preloaded are
left to the asynchronous budgeted path instead of being marked failed, and
HdTexturePreloadResult reports loaded / over-cap / failed / worker errors
with a complete-or-partial status. The Beetle async worker no longer lets
allocation failures escape its thread entry.

Tests cover both formats: complete preload, over-cap skip with on-demand
fallback, malformed images, injected worker and thread-creation failures,
and budget reservation bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

@mstan mstan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto current master6f84a29f6 after the other five approved PRs. Conflict resolution preserves both #717 source-release/split retirement coverage and this PR's preload/preload-pool checks. Range-diff confirms production changes and all four repair/fixture commits are unchanged; only test main call composition differs. Full optimized native runtime builds and all eight relevant pack/package/override/postfx/host-time tests pass. Existing combined title gameplay/current-build snapshots/performance and owner visual acceptance remain applicable. Owner explicitly authorized this merge.

@mstan
mstan merged commit d19d8c7 into master Oct 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants