feat: build a zone's 3D data on worker threads

Opening a zone the first time built its terrain, scene objects and manifests
and ran ImageMagick on the web thread, stalling the dashboard for seconds.

- Workers: the shared pool plus Workers::Reply (answer at once when built,
  else from a worker via Web::Defer).
- terrain_chunks/terrain_layers/scene/paths/scenery/flairs (world3d, property
  and showcase routes) and terrain textures go through it; results are built
  once in OnceCaches, the .raw is read once per zone for chunks, layers and
  flairs, deflated bodies are cached thread-safely.
- ImageMagick conversions are deduplicated and written under a temporary name.
- Workers don't query the CDClient, read settings or call mongoose: ZoneTable,
  render components, flairs, object names, LOT kinds and terrain texture names
  are read at startup; client_location is read once; base64 is plain C++.
- Logger writes one line at a time (mutex; localtime's buffer is shared).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 23:57:48 -05:00
parent 20f70ac0de
commit 17689cd663
20 changed files with 895 additions and 305 deletions

View File

@@ -13,6 +13,12 @@
* client's res folder (client_location). Textures are converted to PNG with ImageMagick and cached.
*/
namespace ClientAssets {
/**
* Read what is read once (the res folder from client_location, the objects' names) at startup, on the main
* thread, so the dashboard's worker threads only ever read it: they must not query the CDClient or read settings.
*/
void Preload();
std::string ItemName(LOT lot);
// An object's raw Objects.name (not localized), or nullopt when the LOT isn't in the CDClient
@@ -27,10 +33,14 @@ namespace ClientAssets {
// Relative path inside res/ using only safe characters and no traversal
bool IsSafeAssetPath(const std::string& path);
// A DDS or PNG texture from res/ as PNG, scaled down to fit maxSize. `opaque` drops the alpha channel first, for
// A DDS or PNG texture from res/ as PNG, scaled down to fit maxSize (converted once, even when asked for from
// several threads at once). `opaque` drops the alpha channel first, for
// textures whose alpha isn't transparency (terrain textures have alpha 0 everywhere; kept, they'd come out black)
std::optional<std::string> TextureAsPng(const std::string& assetPath, uint32_t maxSize, bool opaque = false);
// Whether TextureAsPng has the texture converted already (so it's quick)
bool TextureAsPngReady(const std::string& assetPath, uint32_t maxSize, bool opaque = false);
// Raw bytes of a file inside res/ (matched ignoring case)
std::optional<std::string> ReadResFile(const std::string& relativePath);
@@ -40,7 +50,7 @@ namespace ClientAssets {
// The client's res/ folder (empty when client_location isn't set)
std::filesystem::path ResFolder();
// ResolveResFile inside a res/ folder from ResFolder: reads no settings, so worker threads may call it
// ResolveResFile inside a res/ folder from ResFolder
std::optional<std::filesystem::path> ResolveResFile(const std::string& relativePath, const std::filesystem::path& res);
// The res/-relative path of a file called `fileName` (ignoring case) anywhere under res/<folder>