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

@@ -342,7 +342,7 @@ void RegisterShowcaseRoutes() {
if (!zone) return;
const auto asset = PathId<uint32_t>(context.path, 5);
if (!asset) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid model");
Scenery::ReplyMesh(reply, *zone, *asset, lodOf(context));
Scenery::ReplyMesh(reply, context, *zone, *asset, lodOf(context));
});
Route(eHTTPMethod::GET, "/api/showcase/scenery/:zone/texture/:asset/:slot", PUBLIC, "A scenery texture of a property zone as DDS for the showcase's 3D view",
@@ -352,6 +352,6 @@ void RegisterShowcaseRoutes() {
const auto asset = PathId<uint32_t>(context.path, 5);
const auto slot = PathId<uint32_t>(context.path, 6);
if (!asset || !slot) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid model or texture");
Scenery::ReplyTexture(reply, *zone, *asset, *slot, lodOf(context));
Scenery::ReplyTexture(reply, context, *zone, *asset, *slot, lodOf(context));
});
}