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

@@ -47,8 +47,8 @@ namespace {
* Where a normalized (lowercase) res/ path is on disk. Unpacked clients keep their original mixed case, so on
* case-sensitive file systems each part is matched ignoring case.
*/
std::filesystem::path ResolveRes(const std::string& normalized) {
auto current = ClientRes();
std::filesystem::path ResolveResIn(const std::filesystem::path& res, const std::string& normalized) {
auto current = res;
std::error_code ec;
for (const auto& part : GeneralUtils::SplitString(normalized, '/')) {
if (part.empty()) continue;
@@ -72,6 +72,10 @@ namespace {
return current;
}
std::filesystem::path ResolveRes(const std::string& normalized) {
return ResolveResIn(ClientRes(), normalized);
}
std::optional<std::string> ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) return std::nullopt;
@@ -338,6 +342,19 @@ namespace ClientAssets {
return path;
}
std::filesystem::path ResFolder() {
return ClientRes();
}
std::optional<std::filesystem::path> ResolveResFile(const std::string& relativePath, const std::filesystem::path& res) {
const auto normalized = NormalizeAssetPath(relativePath);
if (res.empty() || !IsSafeAssetPath(normalized)) return std::nullopt;
auto path = ResolveResIn(res, normalized);
std::error_code ec;
if (!std::filesystem::is_regular_file(path, ec)) return std::nullopt;
return path;
}
std::optional<std::string> FindResFile(const std::string& folder, const std::string& fileName) {
const auto normalized = NormalizeAssetPath(folder);
if (ClientRes().empty() || !IsSafeAssetPath(normalized)) return std::nullopt;