feat: convert 3D scenery models on worker threads with deferred replies

The dashboard's web server answers one request at a time, so converting a big
.nif (glom files up to tens of MB) held up every other request, flairs included.

- dWeb: Web::Defer hands a request to another thread; the reply is sent from the
  web thread on its next poll (DeferredQueue). A client that leaves first cancels
  it and the late reply is dropped. The synchronous route API is unchanged.
- Web::Shutdown closes connections while the state their close events touch is
  still alive; the destructor no longer runs handlers during static destruction
  (stopping the dashboard aborted in ~WSClient).
- WorkerPool: priority lanes, with one thread only for urgent work (flairs,
  small models, textures), and limited background work.
- Scenery: mesh and texture routes (and the showcase's) convert on the pool;
  thread-safe memory and disk caches, one conversion per model at a time with
  waiters sharing it; zones are converted ahead onto the disk cache while viewed.
- Setting scenery_workers (0: half the cores, 2 to 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 19:10:18 -05:00
parent 97ac7d04ef
commit bab092e5ee
21 changed files with 1309 additions and 176 deletions

View File

@@ -894,8 +894,19 @@ Shadows and Hidden objects (off by default), remembered per account.
scene object's model and the zone's sky, from the game client's files (needs `client_location`). Models load nearest
first; the detail setting picks the model level of detail, how far objects are drawn (1400/800/450 units), texture
sharpness and a memory budget. Switch it off with *Scenery*. The 3D world view draws the same as its *Models* layer
(on by default, Medium detail), plus the terrain's flairs. Converted models are cached in memory and in
`dDashboardServer/scenery_cache` next to the server (at most 512 MB); nothing needs ImageMagick. Endpoints:
(on by default, Medium detail), plus the terrain's flairs. Converted models are cached in memory (64 MB) and in
`dDashboardServer/scenery_cache` next to the server (at most 512 MB); nothing needs ImageMagick.
Converting a model the caches don't have yet (a big "glom" file takes a moment) happens on a few worker threads, so
the dashboard keeps answering everything else meanwhile: the route hands the request to a worker (`Web::Defer`) and
the web thread sends the answer when it is ready. Flairs and small models (up to 256 KB) go first, and one of the
threads only takes those, so the grass around the camera never waits behind a big model; models of 4 MB and more wait
behind smaller ones. A model asked for twice at once (two viewers) is converted once. When a zone's scenery or flair
manifest is asked for, the zone's models are also converted ahead of time onto the disk cache, flairs and smallest
first, at the detail its viewer last used: only when nothing else waits, on at most half of the threads besides the
flairs' one, stopping when nobody has viewed the zone for 90 seconds or the disk cache is three quarters full (it
never evicts for this). The number of threads is `scenery_workers` in `dashboardconfig.ini` (Settings > Dashboard >
Web server; 0, the default, picks half the CPU cores, 2 to 4; read at startup). Endpoints:
`/api/properties/:id/scenery`, `/api/world3d/:zone/scenery`, `/api/world3d/:zone/flairs`,
`/api/scenery/:zone/mesh/:asset?lod=`, `/api/scenery/:zone/texture/:asset/:slot?lod=`. The world view's other data:
`/api/world3d/:zone/scene` (objects and scenes), `/terrain_chunks` and `/terrain_layers` (the terrain file; sent