Skip to content

Warm the deck's card images at match start - #11731

Open
liamiak wants to merge 2 commits into
Card-Forge:masterfrom
liamiak:warm-image-cache
Open

Warm the deck's card images at match start#11731
liamiak wants to merge 2 commits into
Card-Forge:masterfrom
liamiak:warm-image-cache

Conversation

@liamiak

@liamiak liamiak commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Opening a zone view that shows a whole library - a tutor search, most visibly - decodes every card
image at the moment it opens. On a 525-card highlander deck with the art already downloaded that is
5.5 seconds of JPEG decode on the EDT, and the player waits for all of it.

Decode them at match start instead, one card per EDT event, once the match views are up.

525-card deck, art present before after
first library open 5,470 ms 2,180 ms
reopen 3 ms 3 ms

The warm itself is ~10 ms per event. Measured in a real match rather than a harness: 550 cards
(both decks) warmed in 5,356 ms of spread-out EDT work, during which the game played normally with
no perceptible stutter. A 60-card deck is the same work scaled down, so no cap seemed worth adding.

Two scope cuts keep it small. It warms originals only - they carry the decode cost and do not
depend on the size the view eventually asks for, so this needs no advance knowledge of that size.
And it passes useDefaultIfNotFound=false, so a card with no local image costs a failed file lookup
and is skipped: nothing is rendered, and the online fetcher is not involved. That avoids
FCardImageRenderer's non-thread-safe statics without needing a worker thread, which is why this is
~50 lines and touches no threading.

It starts after the match views open, because the desktop clears the image cache when switching
screens and would otherwise discard the work, and it cancels whenever the cache is cleared. Exposed
as a default no-op on IGuiBase; only the desktop GUI opts in.

Depends on #11295. Without the larger image cache this does almost nothing: on plain master a
500-card warm went 8,451 ms -> 8,068 ms, because a 400-entry cache evicts the warmed originals
faster than they are stored. The two are complementary - #11295 makes a large view stay cached,
this makes its first open cheap.

Memory. Warming allocates the decoded originals early rather than additionally. Opening a
525-card library costs +589 MB of heap on master; warming first and then opening costs +593 MB - the
same peak, paid at match start instead of mid-tutor. What is genuinely new is the player who never
opens a large zone view: they hold ~506 MB (525-card deck), or proportionally less (~60 MB for 60
cards), that they would not otherwise have allocated. The cache holds soft values, so it is
reclaimed under memory pressure, and a screen switch clears it entirely.

No test. Every test in this repo drives PlayerControllerAi, so none of them reach this code
path; a test here would assert nothing. It was verified by instrumenting both ends of the hook and
playing a real match, and by the measurements above.

Full desktop suite: 360 tests, 0 failures, 6 skipped. Checkstyle clean.

Written with Claude Opus 5 (also recorded in the commit co-authors).

liamiak1 and others added 2 commits August 28, 2026 12:56
Opening a large zone view - most visibly the library during a tutor search -
froze the UI for seconds, and froze it again on every sort, close and reopen.

The cache is capped at 400 entries while a single large zone view needs two per
card: the decoded original and the scaled copy the panel paints. A 500-card
library therefore evicted everything it had just loaded and rebuilt the whole
view on every refresh.

400 was never a considered choice for this workload, it is just what this
preference has always defaulted to, so treat that exact value as unset and use
1500; any other value is one somebody chose and is left alone. Also switch to
soft values so memory pressure rather than entry count is what ultimately
evicts, which is what this class already documented itself as doing.

Measured on a 500-card library with no images downloaded: reopening the view
cost ~1.8s and re-requesting at a different size ~4.5s, both now ~1ms. Costs
about 40MB of heap on that measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first view that shows a large library decodes every image at the moment
it opens. Decode them one per EDT event once the match views are up instead,
so the work happens while nobody is waiting.

Originals only: they carry the decode cost and do not depend on the size the
view eventually asks for. Cards with no local image cost a failed file lookup
and are skipped rather than rendered or downloaded.

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