Skip to content

perf(resource-packs): cache converted packs by content - #430

Open
AlexProgrammerDE wants to merge 1 commit into
ViaVersionAddons:mainfrom
StackAnvil:stackanvil/north-star
Open

AlexProgrammerDE wants to merge 1 commit into
ViaVersionAddons:mainfrom
StackAnvil:stackanvil/north-star

Conversation

@AlexProgrammerDE

@AlexProgrammerDE AlexProgrammerDE commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

Converting a resource-pack stack on every join repeats expensive ZIP work and can advertise stale output when the pack content changes. Cache conversions by the effective pack stack and content hash, and verify cached ZIPs before reuse.

Write converted packs and their index atomically, give identical content a stable advertised hash and ID, and make ZIP entry order deterministic. Add focused cache, load-state, and resource negotiation tests. Build on the resource loading changes merged in upstream PR #457. The current cache proposal is #430.

Store typed converter metadata inside the cached ZIP and restore it for
each connection on disk, memory and shared in-flight cache hits. Entity
scales, attachable readiness and unresolved model diagnostics otherwise
disappear when conversion is skipped. Tests compare miss and hit metadata
and isolate the restored map from other connections.

Refresh the conversion fingerprint when block particle bindings change so
previously cached models are not reused without their particle sprite.

Refresh the fingerprint for composed block bones and expanded carrier
definitions so cached packs retain the correct geometry and state selectors.

Refresh the fingerprint when the converter omits elements without rendered
faces. Existing cached packs retain the invalid empty face objects and
must rebuild with valid invisible Java elements and remapped groups.

Include a resource-format revision independently of the selected conversion
profile. Native caption translations add a separate resource archive; older
converted packs must not survive this emitted-resource change merely because
their source files and converter profile are unchanged. Advance this revision
when shared output formats change, including for profiles selected by later
features in the patch stack.

Advance the resource-format revision for the new native particle archive.
Cached conversions produced before particle transport must rebuild even
when the source pack contents and selected converter profile are unchanged.

Advance the emitted-resource epoch for the native actor archive. Cached
packs from older converters lack graphs and pack provenance required by
proxy client rendering, even when their input resource hashes still match.

Keep converter metadata from the completed conversion instead of inflating
the entire ZIP to recover it. Reuse the calculated hash and descriptor
after atomic disk writes, while still verifying persisted cache hits.
Rebuild truncated ZIPs or malformed cached metadata from source so cache
corruption cannot repeatedly reject joins. Preserve archive bytes and
normalized immutable metadata across memory and disk results.

Stream disk-cache misses into a temporary ZIP while computing its hash.
Publish the completed archive atomically before its index, and remove
partial staging files on failure. Avoid allocating the complete ZIP in
heap memory while retaining identical archive bytes and descriptors.

Restore entity scales as finite floats, matching the converter producer
and entity renderer. Reject numbers outside the float domain so persisted
metadata cannot disconnect a connection during custom actor spawning.

Compress large converted ZIPs with Apache Commons Compress scatter/gather
workers. Cap each archive at four workers and use temporary disk storage
instead of buffering every compressed entry on the heap. Preserve sorted
entry order, UTF-8 names, timestamps, file bytes, and deterministic output
across worker counts. Small archives retain the existing sequential writer.

Wait for all workers before closing backing stores and removing the owned
temporary directory, including after interruption and failed writes. Keep
interrupt status and surface compression failures to the existing cache
retry path. Regression tests exercise ordering, content, cancellation,
cleanup, failed output, retry, and output ownership.

Cached older archives remain valid because their resources and metadata
have the same meanings. No resource-format revision is needed for this
packaging change. The parallel path consumes temporary disk space and has
more allocation than the sequential writer; it does not replace RakNet
liveness while acquisition, prompts, downloads, or client reloads are slow.

Expose the same bounded ZIP writer for ordered resource archives. Keep
caller ordering so embedded library headers precede indexed resources,
while ordinary converted packs retain sorted paths. Reuse failure cleanup
and worker ownership instead of maintaining another parallel compressor.

Make shared language parsing atomic for concurrent pack preparation.
Bundled Content instances are reused across connections; HashMap
computeIfAbsent races during concurrent cold definition loads and can
throw ConcurrentModificationException. Retain one immutable parsed table
per path without serializing unrelated language paths.

Use a segmented Commons IO buffer for memory archives, avoiding copies
of all preceding bytes each time the output grows. Keep disk streaming
and deterministic archive bytes unchanged. Verify equivalence across
buffer growth and the existing cache failure, retry and worker cases.

Allow producers to mark already compressed payloads for stored ZIP output.
Preserve that choice across content merges, reset it after ordinary writes,
and retain the previous choice when a replacement fails. Keep payload
writes behind one Content method so storage implementations cannot leave
stale compression metadata. Content subclasses now implement putBytes.

Sequential output calculates the required stored-entry size and CRC.
Scatter/gather output uses the same explicit method without another
deflation pass. Keep ordinary entries compressed, sorted and deterministic.
Older cached packs remain valid because only ZIP methods change.

Read converted entity scales through Number.floatValue(). Pack metadata can
contain boxed doubles, and casting them to Float disconnects clients while
spawning custom actors. The numeric conversion accepts both representations.

BREAKING CHANGE: Content subclasses implement protected putBytes instead
of overriding put. The public put method now maintains ZIP metadata.

Advance the emitted-resource format identity when sidebar image metadata gains source dimensions and its aspect-ratio setting. Rebuild cached packs so the add-on does not receive stale metadata.

Separate in-flight deduplication from completed conversion retention.
Keep at most four completed packs under a 64 MiB archive and serialized
metadata budget, evicting least recently used results. Remove completed
futures on success and failure; active HTTP clients retain their own pack
reference, and disk archives remain available after eviction. Clear both
maps on shutdown. Rejoining servers with distinct pack fingerprints must
not retain every converted ZIP and native metadata object indefinitely.
Regression tests cover LRU reuse, eviction, oversized archives and retry.

Move completed download verification, ZIP decoding and resource definition preparation onto bounded workers. Keep publication on the connection event loop with current-session guards. Complete outstanding work during shutdown and close ZIP inflater resources immediately. Preserve the four-entry, 64 MiB cache limit from the current stack. Count received download chunks without repeatedly scanning the receipt array.

Complete cache retention and eviction before exposing converted packs to callers.
Returning the worker future allowed caller continuations to reuse a pending
oversized result before cache bookkeeping ran. A gated regression reproduces
the ordering failure without sleeps and verifies a new conversion follows
when completed packs are not retained.

Co-authored-by: Miles Tech Studios 291174896+milesalexg-tech@users.noreply.github.com

This is the first upstreamable change from StackAnvil's viabedrock patch stack. The PR branch contains this patch alone, based on upstream.

Purpose

Reuse converted resource packs when their source content and conversion identity match.
Keep the cache patch independent of later native rendering features.
The pinned upstream base already includes the pack-list wire correction from PR #457.
The active cache proposal is PR #430.

Review focus

Fingerprints include source bytes, pack order, conversion profiles and the resource format revision.
Later native archives and UI conversion extend that identity in their owning patches.
A changed source or output format must prevent reuse of obsolete conversions.

Disk writes publish a complete ZIP before its fingerprint index.
Temporary files disappear after failed writes, and a later request can retry.
Deterministic entry order preserves the advertised hash and ID for identical output.
Disk-cache misses stream into the temporary ZIP while calculating SHA-1.
Memory and disabled modes retain their byte-array output.

Typed converter metadata survives fresh conversions, shared hits and reopened disk caches.
Finite model scales retain their float type.
Malformed metadata or truncated archives trigger conversion and atomic replacement.
The fallback renderer accepts numeric scale metadata through Number.floatValue().

Completed downloads verify hashes and decode ZIPs on bounded workers.
Definition preparation, decryption and source persistence also use bounded workers.
The event loop publishes results only for the current transfer and session.
Queue rejection, decoder errors and shutdown use the existing negotiation error path.

The completed cache retains at most four entries and 64 MiB.
Eviction completes before callers receive the conversion future.
This ordering prevents oversized results from remaining reusable through the pending map.

Validation

The standalone patch builds and passes its tests and both Checkstyle tasks on the pinned upstream base.
Tests cover concurrent reuse, eviction ordering, source changes, metadata restoration, damaged archives, partial writes, transfer order, stale publication and shutdown.
The first patch still uses Java 17; the full stack adopts Java 25 separately.

Historical full-stack joins against BDS 1.26.51.1 verified reconnect reuse and refreshed output after texture and version changes.
Those joins used compatible StackAnvil ViaProxy artifacts and included a Fabulously Optimized client.
They establish those observed flows, not a fresh live comparison of this cleanup.

Performance evidence

A private synthetic benchmark contains 64 MiB across 64 prepared entries.
After two warm-up iterations, seven measured iterations produced the same archive hash.
Median allocation decreased from 445 MiB to 316 MiB.
Median elapsed time decreased from 1041 ms to 996 ms on the test host.

A separate packaging benchmark uses five captured CubeCraft packs and 42 licensed image layers from Bedrock 1.26.51.1.
The output contains 15,823 entries and occupies about 86.7 MiB.
Java 25 uses four processors and a 2 GiB heap on Linux.
Seven measured iterations follow two warm-up iterations with identical prepared content.
Streaming ZIP output reduced median packaging allocation from 346.6 MiB to 8.0 MiB.
Median packaging time decreased from 1633 ms to 1599 ms.
Both paths produced identical ZIP bytes, SHA-1, size and entry content.
The implementation retains existing compression behavior.

A historical cold ViaProxy replay installed 216 recorded skins unchanged and submitted resolved custom actor models.
Both converted packs loaded, and the scene payload hash matched the recording.
Conversions took 2253 ms and 2015 ms in that run.
Client reloads each took roughly two to three seconds at the log's one-second resolution.
Model parsing and baking remained visible costs.

These measurements cover prepared packaging and the recorded replay.
They do not establish current network latency, licensed acquisition speed, native rendering parity or live server frame time.
Raw packs, licensed images, recordings and profiles remain private.

@AlexProgrammerDE
AlexProgrammerDE marked this pull request as ready for review September 26, 2026 07:49
@AlexProgrammerDE
AlexProgrammerDE force-pushed the stackanvil/north-star branch 2 times, most recently from ea70eea to af37749 Compare September 26, 2026 10:58
Aiden-Boyd added a commit to Aiden-Boyd/ViaBedrock that referenced this pull request Oct 4, 2026
Move initial lighting off the connection event loop, limit chunk send bursts, and coalesce queued chunk ticks. Reopen chest menus using the server's authoritative 27/54-slot content size and document the separate Fabric addon.

Adapt resource-pack cache and protocol fixes with regression tests from upstream PR ViaVersionAddons#430 (AlexProgrammerDE / StackAnvil). Preserve stable converted ZIP hashes and IDs, verify disk cache bytes, and encode current download-list counts.
Converting a resource-pack stack on every join repeats expensive ZIP work and can advertise stale output when the pack content changes. Cache conversions by the effective pack stack and content hash, and verify cached ZIPs before reuse.

Write converted packs and their index atomically, give identical content a stable advertised hash and ID, and make ZIP entry order deterministic. Add focused cache, load-state, and resource negotiation tests. Build on the resource loading changes merged in upstream PR ViaVersionAddons#457. The current cache proposal is ViaVersionAddons#430.

Store typed converter metadata inside the cached ZIP and restore it for
each connection on disk, memory and shared in-flight cache hits. Entity
scales, attachable readiness and unresolved model diagnostics otherwise
disappear when conversion is skipped. Tests compare miss and hit metadata
and isolate the restored map from other connections.

Refresh the conversion fingerprint when block particle bindings change so
previously cached models are not reused without their particle sprite.

Refresh the fingerprint for composed block bones and expanded carrier
definitions so cached packs retain the correct geometry and state selectors.

Refresh the fingerprint when the converter omits elements without rendered
faces. Existing cached packs retain the invalid empty face objects and
must rebuild with valid invisible Java elements and remapped groups.

Include a resource-format revision independently of the selected conversion
profile. Native caption translations add a separate resource archive; older
converted packs must not survive this emitted-resource change merely because
their source files and converter profile are unchanged. Advance this revision
when shared output formats change, including for profiles selected by later
features in the patch stack.

Advance the resource-format revision for the new native particle archive.
Cached conversions produced before particle transport must rebuild even
when the source pack contents and selected converter profile are unchanged.

Advance the emitted-resource epoch for the native actor archive. Cached
packs from older converters lack graphs and pack provenance required by
proxy client rendering, even when their input resource hashes still match.

Keep converter metadata from the completed conversion instead of inflating
the entire ZIP to recover it. Reuse the calculated hash and descriptor
after atomic disk writes, while still verifying persisted cache hits.
Rebuild truncated ZIPs or malformed cached metadata from source so cache
corruption cannot repeatedly reject joins. Preserve archive bytes and
normalized immutable metadata across memory and disk results.

Stream disk-cache misses into a temporary ZIP while computing its hash.
Publish the completed archive atomically before its index, and remove
partial staging files on failure. Avoid allocating the complete ZIP in
heap memory while retaining identical archive bytes and descriptors.

Restore entity scales as finite floats, matching the converter producer
and entity renderer. Reject numbers outside the float domain so persisted
metadata cannot disconnect a connection during custom actor spawning.

Compress large converted ZIPs with Apache Commons Compress scatter/gather
workers. Cap each archive at four workers and use temporary disk storage
instead of buffering every compressed entry on the heap. Preserve sorted
entry order, UTF-8 names, timestamps, file bytes, and deterministic output
across worker counts. Small archives retain the existing sequential writer.

Wait for all workers before closing backing stores and removing the owned
temporary directory, including after interruption and failed writes. Keep
interrupt status and surface compression failures to the existing cache
retry path. Regression tests exercise ordering, content, cancellation,
cleanup, failed output, retry, and output ownership.

Cached older archives remain valid because their resources and metadata
have the same meanings. No resource-format revision is needed for this
packaging change. The parallel path consumes temporary disk space and has
more allocation than the sequential writer; it does not replace RakNet
liveness while acquisition, prompts, downloads, or client reloads are slow.

Expose the same bounded ZIP writer for ordered resource archives. Keep
caller ordering so embedded library headers precede indexed resources,
while ordinary converted packs retain sorted paths. Reuse failure cleanup
and worker ownership instead of maintaining another parallel compressor.

Make shared language parsing atomic for concurrent pack preparation.
Bundled Content instances are reused across connections; HashMap
computeIfAbsent races during concurrent cold definition loads and can
throw ConcurrentModificationException. Retain one immutable parsed table
per path without serializing unrelated language paths.

Use a segmented Commons IO buffer for memory archives, avoiding copies
of all preceding bytes each time the output grows. Keep disk streaming
and deterministic archive bytes unchanged. Verify equivalence across
buffer growth and the existing cache failure, retry and worker cases.

Allow producers to mark already compressed payloads for stored ZIP output.
Preserve that choice across content merges, reset it after ordinary writes,
and retain the previous choice when a replacement fails. Keep payload
writes behind one Content method so storage implementations cannot leave
stale compression metadata. Content subclasses now implement putBytes.

Sequential output calculates the required stored-entry size and CRC.
Scatter/gather output uses the same explicit method without another
deflation pass. Keep ordinary entries compressed, sorted and deterministic.
Older cached packs remain valid because only ZIP methods change.

Read converted entity scales through Number.floatValue(). Pack metadata can
contain boxed doubles, and casting them to Float disconnects clients while
spawning custom actors. The numeric conversion accepts both representations.

BREAKING CHANGE: Content subclasses implement protected putBytes instead
of overriding put. The public put method now maintains ZIP metadata.

Advance the emitted-resource format identity when sidebar image metadata gains source dimensions and its aspect-ratio setting. Rebuild cached packs so the add-on does not receive stale metadata.

Separate in-flight deduplication from completed conversion retention.
Keep at most four completed packs under a 64 MiB archive and serialized
metadata budget, evicting least recently used results. Remove completed
futures on success and failure; active HTTP clients retain their own pack
reference, and disk archives remain available after eviction. Clear both
maps on shutdown. Rejoining servers with distinct pack fingerprints must
not retain every converted ZIP and native metadata object indefinitely.
Regression tests cover LRU reuse, eviction, oversized archives and retry.

Move completed download verification, ZIP decoding and resource definition preparation onto bounded workers. Keep publication on the connection event loop with current-session guards. Complete outstanding work during shutdown and close ZIP inflater resources immediately. Preserve the four-entry, 64 MiB cache limit from the current stack. Count received download chunks without repeatedly scanning the receipt array.

Complete cache retention and eviction before exposing converted packs to callers.
Returning the worker future allowed caller continuations to reuse a pending
oversized result before cache bookkeeping ran. A gated regression reproduces
the ordering failure without sleeps and verifies a new conversion follows
when completed packs are not retained.

Co-authored-by: Miles Tech Studios <291174896+milesalexg-tech@users.noreply.github.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.

3 participants