Skip to content

Async CTX virtual texture tile loading#2517

Draft
juliendms wants to merge 3 commits into
CelestiaProject:masterfrom
juliendms:feat/async-ctx-loader
Draft

Async CTX virtual texture tile loading#2517
juliendms wants to merge 3 commits into
CelestiaProject:masterfrom
juliendms:feat/async-ctx-loader

Conversation

@juliendms
Copy link
Copy Markdown

@juliendms juliendms commented May 8, 2026

Summary

Move CTX (virtual texture) tile loading off the render thread. Today, every cache miss in VirtualTexture::getTile synchronously reads a tile file, decodes it, and uploads it to GL inside the patch render loop -- so a pan that reveals a dozen new patches stalls the frame for a dozen load+upload roundtrips.

After this change:

  • A single shared worker thread (TileLoader, in an anonymous namespace inside virtualtex.cpp) handles file I/O and image decode for all VirtualTexture instances in the process.
  • The render thread performs bounded GL uploads (kMaxUploadsPerFrame = 4 per VirtualTexture per frame) from a per-owner result queue at the start of beginUsage().
  • Tile gains a State enum (NotLoaded / Queued / Ready / Failed) that replaces the implicit tex == nullptr / loadFailed pair. State is read and written exclusively on the render thread; the worker treats Tile* as an opaque cookie that round-trips through the request/result pair unchanged.
  • Cancellation is by id: each VirtualTexture registers a uint64_t with the loader on construction and deregisters on destruction. Worker results are delivered under the registry mutex, so a destroyed owner can never see a stale callback.

Design notes

  • No public interface change. LODSphereMesh is untouched. getTile, beginUsage, endUsage, bind signatures are unchanged.
  • No GL on the worker thread. Image::load is the only worker-side call; ImageTexture construction is exclusively in beginUsage.
  • getTile becomes non-blocking. On a miss it enqueues a request (idempotent via the state check) and returns the deepest already-Ready ancestor. The first frame after a tile becomes visible may show a coarser ancestor; once the worker delivers and beginUsage uploads, subsequent frames show the finer tile.
  • FIFO request queue, unbounded. No priority, no drop policy in V1.
  • Owner registry under a mutex. The worker pushes the result while holding the same mutex unregisterOwner takes, so result-queue lifetime and registry membership are kept consistent.

User-visible effect

Smooth panning across high-LOD CTX surfaces. Freshly-revealed patches show a brief lower-LOD blur for a handful of frames instead of a per-pan freeze.

Disclaimer: The code in this PR was generated by AI (Anthropic Claude).

juliendms added 3 commits May 11, 2026 15:00
Today every cache miss in VirtualTexture::getTile synchronously reads a
tile file, decodes it, and uploads it to GL inside the patch render
loop -- so a pan that reveals a dozen new patches stalls the frame for a
dozen load+upload roundtrips.

After this change, file I/O and image decode happen on a single shared
worker thread (TileLoader, in an anonymous namespace inside
virtualtex.cpp); the render thread only performs bounded GL uploads
(kMaxUploadsPerFrame = 4 per VirtualTexture per frame) from a per-owner
result queue at the start of beginUsage().

The Tile struct gains a State enum (NotLoaded / Queued / Ready / Failed)
that replaces the implicit tex==nullptr / loadFailed pair. State is
read and written exclusively on the render thread; the worker treats
Tile* as an opaque cookie that round-trips through the request/result
pair unchanged.

Cancellation is by id: each VirtualTexture registers a uint64_t with
the loader on construction and deregisters on destruction. Worker
results are delivered under the registry mutex, so a destroyed owner
never sees a stale callback.

Eviction (issue CelestiaProject#2447) is intentionally out of scope; ticks /
tilesRequested are kept around for the follow-up.
Replace std::lock_guard with std::scoped_lock with CTAD throughout the
TileLoader singleton and beginUsage drain. Drop redundant
nResolutionLevels initializer (in-class init covers it). Use
std::atomic default seq_cst memory order for the shutdown flag (the
flag is not on a hot path, sequential consistency is the safer
default). Mark requestLoad const since it doesn't mutate *this. Make
readyTile in getTile a const Tile* since only ImageTexture::getName()
(which is const) is called on it. Convert the bounded-upload for-loop
in beginUsage to a while-loop and use auto/auto* for the iterator
locals. Extract the post-increment in registerOwner so the read and
write are on separate lines.

No behavioural change.
@juliendms juliendms force-pushed the feat/async-ctx-loader branch from 54820b7 to 6621bc8 Compare May 11, 2026 13:02
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.

1 participant